diff --git a/plugins/obsidian/README.md b/plugins/obsidian/README.md index 37c8fb9..f719660 100644 --- a/plugins/obsidian/README.md +++ b/plugins/obsidian/README.md @@ -6,10 +6,11 @@ Lineage view and tier visibility for [Noema](https://github.com/Fail-Safe/Noema) - **Lineage sidebar.** When you open a trace, the sidebar shows its `derived_from` ancestors and the traces derived from it, both clickable. Useful for navigating "where did this come from / what came out of this" without leaving the editor. - **Tier badge in the status bar.** Shows `[s]` / `[m]` / `[L]` for the currently-open trace and a tooltip note that long-tier traces are immutable. +- **Tier badges in the file explorer.** Shows the same tier shorthand beside every trace in the configured traces folder, including while disconnected from the MCP endpoint. Enabled by default and independently toggleable in the plugin settings. - **Connection status.** The same status bar item shows whether the plugin is connected to a `noema serve --transport http` endpoint. A keyed-mode server that rejects (or requires) the bearer key shows `noema: unauthorized` instead of `noema: disconnected`, and pops a one-time notice pointing you at the bearer-key setting — so a wrong key reads as a credential problem, not an unreachable server. - **Noema-backed trace search.** `Noema: Search traces` calls the connected cortex's `search_traces` MCP tool and opens the selected trace in Obsidian. The plugin setting chooses `hybrid`, `semantic`, or `lexical`; the modal can show 5 or 10 results. Server-side `cortex.md` still owns embedding configuration and `hybrid_weight`. -That's intentionally the whole feature set for v0.3. File-explorer decorations and federation status panels are reasonable next-version additions but aren't here yet. +That's intentionally the whole feature set for v0.4. Saved search views and federation status panels are reasonable next-version additions but aren't here yet. ## Setup @@ -31,6 +32,7 @@ That's intentionally the whole feature set for v0.3. File-explorer decorations a - **HTTP endpoint** — e.g. `https://noema.local:3000` - **Bearer key** — required if the server is in keyed mode (`NOEMA_MCP_KEY` or `access.shared_key_file`); leave empty for open-mode (loopback only). - **Test connection** — click to probe the endpoint immediately and get a notice telling you whether it connected, was rejected (HTTP 401, fix the key), or was unreachable. + - **Show tier badges in file explorer** — enabled by default; turn it off to hide `[s]`, `[m]`, and `[L]` beside trace filenames. - **Noema search mode** — used by `Noema: Search traces`; defaults to `Hybrid`. 6. Open the lineage sidebar via the command palette: `Noema: Open lineage view`. @@ -60,4 +62,4 @@ Produces `main.js` next to `manifest.json`. For development, `npm run dev` watch Noema is intentionally lightweight infrastructure — markdown files plus a SQLite index, no opinion about your editor. This plugin matches that spirit: it adds the pieces of UI that genuinely benefit from being inside Obsidian (lineage navigation, tier visibility, trace creation, appends, and Noema-ranked trace search) and stays out of the way for everything else. Editing happens in Obsidian's native editor and file management uses Obsidian's native file explorer. -If you want richer integration (file-explorer decorations, saved search views, federation status panel), file an issue against the main repo with the use case — they're easy to add as separate, opt-in commands. +If you want richer integration (saved search views or a federation status panel), file an issue against the main repo with the use case — they're easy to add as separate, opt-in commands. diff --git a/plugins/obsidian/main.js b/plugins/obsidian/main.js index ea60eab..964b96a 100644 --- a/plugins/obsidian/main.js +++ b/plugins/obsidian/main.js @@ -3,6 +3,6 @@ THIS IS A GENERATED/BUNDLED FILE BY ESBUILD For source, see https://github.com/Fail-Safe/Noema/tree/main/plugins/obsidian */ -var H=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.prototype.hasOwnProperty;var ne=(s,i)=>{for(var e in i)H(s,e,{get:i[e],enumerable:!0})},ie=(s,i,e,t)=>{if(i&&typeof i=="object"||typeof i=="function")for(let n of ee(i))!te.call(s,n)&&n!==e&&H(s,n,{get:()=>i[n],enumerable:!(t=Q(i,n))||t.enumerable});return s};var re=s=>ie(H({},"__esModule",{value:!0}),s);var he={};ne(he,{default:()=>M});module.exports=re(he);var d=require("obsidian");var m=require("obsidian"),O={endpoint:"",bearerKey:"",tracesFolder:"traces",defaultAuthor:"",searchMode:"hybrid"},I=class extends m.PluginSettingTab{constructor(e,t){super(e,t);this.plugin=t}display(){let{containerEl:e}=this;e.empty(),e.createEl("h2",{text:"Noema"}),e.createEl("p",{text:"Connect this Obsidian vault to a running noema serve --transport http endpoint to see lineage and tier metadata for traces. The vault should be opened on the cortex root (the directory containing cortex.md)."}),new m.Setting(e).setName("HTTP endpoint").setDesc("Base URL of the noema HTTP server, e.g. https://noema.local:3000").addText(t=>t.setPlaceholder("https://hostname:3000").setValue(this.plugin.settings.endpoint).onChange(async n=>{this.plugin.settings.endpoint=n.trim(),await this.plugin.saveSettings(),this.plugin.refreshClient()})),new m.Setting(e).setName("Bearer key").setDesc("Shared MCP access key (NOEMA_MCP_KEY). Required for keyed-mode servers; leave empty for open-mode (loopback only).").addText(t=>{t.setPlaceholder("(secret)").setValue(this.plugin.settings.bearerKey).onChange(async n=>{this.plugin.settings.bearerKey=n,await this.plugin.saveSettings(),this.plugin.refreshClient()}),t.inputEl.type="password"}),new m.Setting(e).setName("Test connection").setDesc("Probe the endpoint now with the current key and report the result.").addButton(t=>t.setButtonText("Test connection").onClick(async()=>{t.setDisabled(!0),t.setButtonText("Testing\u2026");try{await this.plugin.testConnection()}finally{t.setButtonText("Test connection"),t.setDisabled(!1)}})),new m.Setting(e).setName("Traces folder").setDesc("Folder inside the vault that contains trace markdown files. Default: traces").addText(t=>t.setPlaceholder("traces").setValue(this.plugin.settings.tracesFolder).onChange(async n=>{this.plugin.settings.tracesFolder=n.trim()||"traces",await this.plugin.saveSettings()})),new m.Setting(e).setName("Default author").setDesc("Stamped on traces created via the 'New trace' command. Leave empty to let the cortex's default apply. Free-form \u2014 typical values are a username, agent name, or 'agent/handle'.").addText(t=>t.setPlaceholder("e.g. alice or agent/researcher-1").setValue(this.plugin.settings.defaultAuthor).onChange(async n=>{this.plugin.settings.defaultAuthor=n.trim(),await this.plugin.saveSettings()})),new m.Setting(e).setName("Noema search mode").setDesc("Used by the 'Search traces' command. Hybrid uses the cortex's server-side hybrid_weight.").addDropdown(t=>t.addOption("hybrid","Hybrid").addOption("semantic","Semantic").addOption("lexical","Lexical").setValue(this.plugin.settings.searchMode).onChange(async n=>{this.plugin.settings.searchMode=n,await this.plugin.saveSettings()}))}};var b=require("obsidian");function v(s,i){let e=s.metadataCache.getFileCache(i),t=e==null?void 0:e.frontmatter;return t?{id:typeof t.id=="string"?t.id:void 0,title:typeof t.title=="string"?t.title:void 0,type:typeof t.type=="string"?t.type:void 0,tier:se(t.tier)}:null}function se(s){if(typeof s!="string")return"short";let i=s.toLowerCase();return i==="short"||i==="mid"||i==="long"?i:"unknown"}function E(s){switch(s){case"short":return"s";case"mid":return"m";case"long":return"L";default:return"?"}}function P(s){switch(s){case"short":return"short-term";case"mid":return"mid-term";case"long":return"long-term (immutable)";default:return"unknown tier"}}var x="noema-lineage-view",C=class extends b.ItemView{constructor(e,t){super(e);this.plugin=t;this.currentTraceId=null;this.bodyEl=null}getViewType(){return x}getDisplayText(){return"Noema lineage"}getIcon(){return"git-merge"}async onOpen(){this.contentEl.empty(),this.contentEl.addClass("noema-lineage-view"),this.contentEl.createEl("div",{cls:"noema-lineage-header"}).createEl("h3",{text:"Lineage"}),this.bodyEl=this.contentEl.createEl("div",{cls:"noema-lineage-body"}),this.renderEmpty("Open a trace to see its lineage."),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>{this.refreshFromActive()})),await this.refreshFromActive()}async refreshFromActive(){var n;let e=this.app.workspace.getActiveFile();if(!e){this.renderEmpty("No file is open.");return}let t=v(this.app,e);if(!(t!=null&&t.id)){this.renderEmpty("This file isn't a trace (no `id` in frontmatter)."),this.currentTraceId=null;return}t.id!==this.currentTraceId&&(this.currentTraceId=t.id,await this.fetchAndRender(t.id,(n=t.title)!=null?n:"(untitled)",t.tier))}async fetchAndRender(e,t,n){if(!this.bodyEl)return;this.bodyEl.empty(),this.bodyEl.createEl("div",{cls:"noema-lineage-loading",text:`Loading lineage for ${e}\u2026`});let r=this.plugin.client;if(!r){this.renderEmpty("Configure the noema HTTP endpoint in settings to load lineage.");return}let a;try{a=await r.traceLineage(e)}catch(o){this.renderError(`Couldn't load lineage: ${o instanceof Error?o.message:String(o)}`);return}this.renderLineage(e,t,n,a)}renderLineage(e,t,n,r){if(!this.bodyEl)return;this.bodyEl.empty(),this.renderSection("Derived from",r.derivedFrom);let a=this.bodyEl.createEl("div",{cls:"noema-lineage-current"});a.createEl("div",{cls:"noema-lineage-current-glyph",text:`[${E(n)}]`}),a.createEl("div",{cls:"noema-lineage-current-title",text:t}),a.createEl("div",{cls:"noema-lineage-current-id",text:e}),a.setAttr("title",P(n)),this.renderSection("Derived by",r.derivedBy),r.derivedFrom.length===0&&r.derivedBy.length===0&&this.bodyEl.createEl("div",{cls:"noema-lineage-empty",text:"This trace has no recorded derivations."})}renderSection(e,t){var a,o;if(!this.bodyEl||t.length===0)return;let n=this.bodyEl.createEl("div",{cls:"noema-lineage-section"});n.createEl("div",{cls:"noema-lineage-section-label",text:e});let r=n.createEl("div",{cls:"noema-lineage-section-list"});for(let l of t){let c=this.findTraceFile(l),p=c?v(this.app,c):null,h=(a=p==null?void 0:p.tier)!=null?a:"unknown",L=(o=p==null?void 0:p.title)!=null?o:"(unknown)",u=r.createEl("div",{cls:"noema-lineage-row"});u.createEl("span",{cls:"noema-lineage-glyph",text:`[${E(h)}]`});let y=u.createEl("a",{cls:"noema-lineage-link",text:L,href:"#"});y.setAttr("data-trace-id",l),y.setAttr("title",`${l} (${P(h)})`),u.appendChild(document.createTextNode(" ")),u.createEl("span",{cls:"noema-lineage-id",text:l}),y.addEventListener("click",g=>{g.preventDefault(),c?this.app.workspace.getLeaf(!1).openFile(c):new b.Notice(`Trace ${l} not found in this vault.`)})}}findTraceFile(e){let n=`${this.plugin.settings.tracesFolder.replace(/\/+$/,"")}/${e}.md`,r=this.app.vault.getFileByPath(n);return r instanceof b.TFile?r:null}renderEmpty(e){this.bodyEl&&(this.bodyEl.empty(),this.bodyEl.createEl("div",{cls:"noema-lineage-empty",text:e}))}renderError(e){this.bodyEl&&(this.bodyEl.empty(),this.bodyEl.createEl("div",{cls:"noema-lineage-error",text:e}))}};var W=class extends Error{constructor(e,t,n){super(t);this.code=e;this.data=n;this.name="JsonRpcError"}},T=class extends Error{constructor(e){super("MCP server rejected the bearer key (HTTP 401)");this.serverMessage=e;this.name="UnauthorizedError"}},q=class extends Error{constructor(i,e){super(`${i}: ${z(e)}`),this.name="TransportError"}};function z(s){var n;if(s==null)return"unknown connection error";let i=s instanceof Error?s.message.trim():String(s).trim(),e=s instanceof Error?s.cause:void 0,t=(n=U(e))!=null?n:U(s);if(t){let r={ECONNREFUSED:"connection refused",ECONNRESET:"connection reset",ETIMEDOUT:"connection timed out",ENOTFOUND:"DNS lookup failed",EAI_AGAIN:"DNS lookup temporarily failed",CERT_HAS_EXPIRED:"TLS certificate expired",DEPTH_ZERO_SELF_SIGNED_CERT:"TLS certificate is self-signed",UNABLE_TO_VERIFY_LEAF_SIGNATURE:"TLS certificate could not be verified"}[t];return`${r!=null?r:i||"connection failed"} (${t})`}return i||"unknown connection error"}function U(s){if(!s||typeof s!="object"||!("code"in s))return null;let i=s.code;return typeof i=="string"&&i?i:null}async function _(s,i,e){try{return await fetch(s,i)}catch(t){throw new q(e,t)}}var Y=["fact","decision","preference","context","skill","intent","observation","note"],ae="2025-03-26",N=class{constructor(i,e){this.endpoint=i;this.bearerKey=e;this.nextId=1;this.sessionId=null;this.sessionPromise=null}updateConfig(i,e){this.endpoint=i,this.bearerKey=e,this.sessionId=null,this.sessionPromise=null}async cortexIdentity(){let i=await this.callToolText("cortex_identity",{});return JSON.parse(i)}async traceLineage(i){let e=await this.callToolText("trace_lineage",{id:i});return oe(e,i)}async appendTrace(i,e){await this.callToolText("append_trace",{id:i,content:e})}async createTrace(i){let e={title:i.title,type:i.type,body:i.body};i.author&&(e.author=i.author),i.tags&&i.tags.length>0&&(e.tags=i.tags.join(",")),i.derivedFrom&&i.derivedFrom.length>0&&(e.derived_from=i.derivedFrom.join(","));let t=await this.callToolText("create_trace",e),n=t.match(/Trace created:\s*(\S+)/);if(!n)throw new Error(`create_trace: unexpected response shape: ${t}`);return n[1]}async searchTraces(i,e){let t=await this.callToolText("search_traces",{query:i,mode:e});return le(t)}async callToolText(i,e){let t=await this.callTool(i,e);if(t.isError){let n=t.content.map(r=>{var a;return(a=r.text)!=null?a:""}).join(` -`).trim();throw new Error(`tool ${i}: ${n||"unknown error"}`)}return t.content.map(n=>{var r;return(r=n.text)!=null?r:""}).join("")}async callTool(i,e){await this.ensureSession();try{return await this.callToolOnce(i,e)}catch(t){if(t instanceof F)return this.sessionId=null,this.sessionPromise=null,await this.ensureSession(),await this.callToolOnce(i,e);throw t}}async callToolOnce(i,e){return await this.rpc({method:"tools/call",params:{name:i,arguments:e}})}async ensureSession(){this.sessionId||(this.sessionPromise||(this.sessionPromise=this.handshake().catch(i=>{throw this.sessionPromise=null,i})),await this.sessionPromise)}async handshake(){let i=this.buildHeaders(),e=this.mcpUrl(),t={jsonrpc:"2.0",id:this.nextId++,method:"initialize",params:{protocolVersion:ae,capabilities:{},clientInfo:{name:"noema-obsidian",version:"0.3.0"}}},n=await _(e,{method:"POST",headers:i,body:JSON.stringify(t)},"initialize request");if(n.status===401)throw new T(await D(n));if(!n.ok)throw new Error(`initialize: HTTP ${n.status}: ${n.statusText}`);await J(n,t.id);let r=n.headers.get("Mcp-Session-Id");if(!r)throw new Error("initialize: server did not return Mcp-Session-Id header");this.sessionId=r;let a={...this.buildHeaders(),"Mcp-Session-Id":r},o=await _(e,{method:"POST",headers:a,body:JSON.stringify({jsonrpc:"2.0",method:"notifications/initialized",params:{}})},"initialized notification");if(o.status===401)throw new T(await D(o));if(!o.ok)throw new Error(`initialized notification: HTTP ${o.status}: ${o.statusText}`)}async rpc(i){let e=this.nextId++,t=this.buildHeaders();this.sessionId&&(t["Mcp-Session-Id"]=this.sessionId);let n=await _(this.mcpUrl(),{method:"POST",headers:t,body:JSON.stringify({jsonrpc:"2.0",id:e,...i})},"MCP request");if(n.status===401)throw new T(await D(n));if(n.status===404)throw new F;if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);let r=await J(n,e);if(r.error)throw new W(r.error.code,r.error.message,r.error.data);if(r.result===void 0)throw new Error("response missing result");return r.result}buildHeaders(){let i={"Content-Type":"application/json",Accept:"application/json, text/event-stream"};return this.bearerKey&&(i.Authorization=`Bearer ${this.bearerKey}`),i}mcpUrl(){return this.endpoint.replace(/\/+$/,"")+"/mcp"}};async function J(s,i){var r;if(!((r=s.headers.get("Content-Type"))!=null?r:"").toLowerCase().includes("text/event-stream"))return await s.json();let t=[];for(let a of(await s.text()).split(/\r?\n\r?\n/)){let o=a.split(/\r?\n/).filter(l=>l.startsWith("data:")).map(l=>l.slice(5).replace(/^ /,"")).join(` -`);if(o)try{t.push(JSON.parse(o))}catch(l){}}let n=t.find(a=>a.id===i);if(!n)throw new Error(`SSE response missing JSON-RPC id ${i}`);return n}var F=class extends Error{constructor(){super("invalid MCP session"),this.name="InvalidSessionError"}};async function D(s){try{let i=(await s.text()).trim();if(!i)return;try{let e=JSON.parse(i);if(e&&typeof e.error=="string")return e.error}catch(e){}return i}catch(i){return}}function oe(s,i){let e=s.split(/\r?\n/),t=[],n=[],r=i;for(let a of e){let o=a.trim();o.startsWith("Trace:")?r=o.slice(6).trim():o.startsWith("Derived from:")?t=G(o.slice(13)):o.startsWith("Derived by:")&&(n=G(o.slice(11)))}return{traceId:r,derivedFrom:t,derivedBy:n}}function le(s){var t,n,r,a,o,l,c,p,h,L;let i=[],e=s.split(/\r?\n/);for(let u=0;u$.trim());f.length<2||f[0]!==K||i.push({id:f[0],title:(l=f[1])!=null?l:"",type:(c=f[2])!=null?c:"",author:(p=f[3])!=null?p:"",tags:(h=f[4])!=null?h:"",created:(L=f[5])!=null?L:""})}return i}function G(s){let i=s.trim();return!i||i==="(none)"?[]:i.split(",").map(e=>e.trim()).filter(Boolean)}var S=require("obsidian");var A=class extends S.Modal{constructor(e,t){super(e);this.plugin=t;this.submitting=!1}onOpen(){let{contentEl:e}=this;e.empty(),e.addClass("noema-create-modal"),this.titleEl.setText("New trace"),this.titleInput=this.row("Title","input",{type:"text",placeholder:"Why we chose local storage"}),this.typeSelect=this.row("Type","select");for(let r of Y){let a=this.typeSelect.createEl("option",{text:r});a.value=r}this.typeSelect.value="note",this.tagsInput=this.row("Tags","input",{type:"text",placeholder:"storage, local-first, architecture"}),this.descriptor("Comma- or semicolon-separated. Optional."),this.tagWarningsEl=e.createEl("div",{cls:"noema-create-tag-warnings"}),this.tagWarningsEl.style.display="none",this.tagsInput.addEventListener("input",()=>this.refreshTagWarnings()),this.derivedFromInput=this.row("Derived from","input",{type:"text",placeholder:"20260328-language-candidates, 20260329-runtime-survey"}),this.descriptor("Comma-separated trace IDs this one is derived from. Optional."),this.errorEl=e.createEl("div",{cls:"noema-create-error"}),this.errorEl.style.display="none";let t=e.createEl("div",{cls:"noema-create-buttons"});t.createEl("button",{text:"Cancel"}).addEventListener("click",()=>this.close()),this.submitBtn=t.createEl("button",{text:"Create",cls:"mod-cta"}),this.submitBtn.addEventListener("click",()=>this.submit()),this.titleInput.addEventListener("keydown",r=>{r.key==="Enter"&&(r.preventDefault(),this.submit())}),setTimeout(()=>this.titleInput.focus(),0)}onClose(){this.contentEl.empty()}row(e,t,n){let r=this.contentEl.createEl("div",{cls:"noema-create-row"});r.createEl("label",{text:e});let a=r.createEl(t);if(n)for(let[o,l]of Object.entries(n))a.setAttribute(o,l);return a}descriptor(e){this.contentEl.createEl("div",{cls:"noema-create-hint",text:e})}refreshTagWarnings(){let e=V(this.tagsInput.value),t=ce(e);if(this.tagWarningsEl.empty(),t.length===0){this.tagWarningsEl.style.display="none";return}this.tagWarningsEl.style.display="";for(let n of t){let r=this.tagWarningsEl.createEl("div",{cls:"noema-create-tag-warning"});r.createEl("span",{cls:"noema-create-tag-warning-glyph",text:"\u26A0"}),r.createEl("code",{cls:"noema-create-tag-warning-tag",text:n.tag}),r.appendChild(document.createTextNode(" \u2014 ")),r.createEl("span",{cls:"noema-create-tag-warning-reason",text:n.reason})}}async submit(){if(this.submitting)return;this.errorEl.style.display="none",this.errorEl.empty();let e=this.titleInput.value.trim();if(!e){this.showError("Title is required."),this.titleInput.focus();return}let t=this.typeSelect.value,n=V(this.tagsInput.value),r=V(this.derivedFromInput.value),a=this.plugin.client;if(!a){this.showError("No noema endpoint configured. Set one in Settings \u2192 Noema.");return}let o={title:e,type:t,body:" ",author:this.plugin.settings.defaultAuthor||void 0,tags:n.length>0?n:void 0,derivedFrom:r.length>0?r:void 0};this.submitting=!0,this.submitBtn.disabled=!0,this.submitBtn.setText("Creating\u2026");try{let l=await a.createTrace(o);this.close(),new S.Notice(`Created ${l}`),await this.openTraceFile(l)}catch(l){let c=l instanceof Error?l.message:String(l);this.showError(`Couldn't create trace: ${c}`),this.submitBtn.disabled=!1,this.submitBtn.setText("Create")}finally{this.submitting=!1}}async openTraceFile(e){let n=`${this.plugin.settings.tracesFolder.replace(/\/+$/,"")}/${e}.md`;for(let r=0;r<20;r++){let a=this.app.vault.getFileByPath(n);if(a instanceof S.TFile){await this.app.workspace.getLeaf(!1).openFile(a);return}await de(100)}}showError(e){this.errorEl.setText(e),this.errorEl.style.display=""}};function V(s){return s?s.split(/[,;]/).map(i=>i.trim()).filter(Boolean):[]}function ce(s){let i=[];for(let e of s)/^[0-9_-]+$/.test(e)&&i.push({tag:e,reason:"Obsidian requires at least one letter. Pure-numeric tags are not surfaced in its tag panel."}),e.includes(".")&&i.push({tag:e,reason:'Obsidian interprets dots as nested-tag separators (so "a.b" becomes tag tree a/b).'});return i}function de(s){return new Promise(i=>setTimeout(i,s))}var X=require("obsidian"),R=class{constructor(i,e){this.app=i;this.plugin=e;this.bannerEl=null;this.currentFile=null}refresh(){let i=this.app.workspace.getActiveViewOfType(X.MarkdownView);if(!i){this.removeBanner();return}let e=i.file;if(!e){this.removeBanner();return}let t=this.shouldWarn(e);if(!t){this.removeBanner();return}this.showBanner(i,e,t)}removeAll(){this.removeBanner()}shouldWarn(i){let e=this.app.metadataCache.getFileCache(i),t=e==null?void 0:e.frontmatter;if(!t)return null;if((typeof t.tier=="string"?t.tier.toLowerCase():"")==="long")return{kind:"long-tier"};let r=t.source_locked===!0||t.source_locked==="true",a=typeof t.origin=="string"?t.origin:"",o=this.plugin.cortexName;return r&&a&&o&&a!==o?{kind:"source-locked",origin:a}:null}showBanner(i,e,t){if(this.bannerEl&&this.currentFile===e&&this.bannerEl.dataset.kind===t.kind)return;this.removeBanner();let n=i.containerEl.querySelector(".view-content");if(!(n instanceof HTMLElement))return;let r=document.createElement("div");r.className="noema-immutable-warning",r.dataset.kind=t.kind,r.createSpan({cls:"noema-immutable-warning-glyph"}).setText(t.kind==="long-tier"?"[L]":"[locked]");let o=r.createSpan({cls:"noema-immutable-warning-text"});t.kind==="long-tier"?o.setText("Long-tier trace \u2014 immutable except via tier_votes. Edits will not be accepted by the cortex."):o.setText(`Source-locked trace from peer "${t.origin}" \u2014 local edits will not be accepted by the cortex.`),n.prepend(r),this.bannerEl=r,this.currentFile=e}removeBanner(){this.bannerEl&&(this.bannerEl.remove(),this.bannerEl=null),this.currentFile=null}};var k=require("obsidian");var j=class extends k.Modal{constructor(e,t,n,r,a,o){super(e);this.plugin=t;this.targetFile=n;this.targetId=r;this.targetTitle=a;this.targetTier=o;this.submitting=!1}onOpen(){let{contentEl:e}=this;e.empty(),e.addClass("noema-append-modal"),this.titleEl.setText("Append to trace");let t=e.createEl("div",{cls:"noema-append-target"});t.createEl("span",{cls:"noema-append-target-glyph"}).setText(`[${E(this.targetTier)}]`),t.createEl("span",{cls:"noema-append-target-title"}).setText(this.targetTitle),t.createEl("div",{cls:"noema-append-target-id"}).setText(this.targetId),e.createEl("div",{cls:"noema-append-label"}).setText("Content to append"),this.contentInput=e.createEl("textarea",{cls:"noema-append-content",attr:{rows:"6",placeholder:"Type or paste content to append\u2026"}}),e.createEl("div",{cls:"noema-append-hint"}).setText("A newline is inserted automatically between the existing body and your content if needed. \u2318+Enter to submit."),this.errorEl=e.createEl("div",{cls:"noema-append-error"}),this.errorEl.style.display="none";let c=e.createEl("div",{cls:"noema-append-buttons"});c.createEl("button",{text:"Cancel"}).addEventListener("click",()=>this.close()),this.submitBtn=c.createEl("button",{text:"Append",cls:"mod-cta"}),this.submitBtn.addEventListener("click",()=>this.submit()),this.contentInput.addEventListener("keydown",h=>{h.key==="Enter"&&(h.metaKey||h.ctrlKey)&&(h.preventDefault(),this.submit())}),setTimeout(()=>this.contentInput.focus(),0)}onClose(){this.contentEl.empty()}async submit(){if(this.submitting)return;this.errorEl.style.display="none",this.errorEl.empty();let t=this.contentInput.value.replace(/\s+$/,"");if(!t){this.showError("Content is required."),this.contentInput.focus();return}let n=this.plugin.client;if(!n){this.showError("No noema endpoint configured. Set one in Settings \u2192 Noema.");return}this.submitting=!0,this.submitBtn.disabled=!0,this.submitBtn.setText("Appending\u2026");try{await n.appendTrace(this.targetId,t),this.close(),new k.Notice(`Appended to ${this.targetId}`)}catch(r){let a=r instanceof Error?r.message:String(r);this.showError(`Couldn't append: ${a}`),this.submitBtn.disabled=!1,this.submitBtn.setText("Append")}finally{this.submitting=!1}}showError(e){this.errorEl.setText(e),this.errorEl.style.display=""}};function Z(s){var t;let i=s.app.workspace.getActiveFile();if(!(i instanceof k.TFile))return!1;let e=v(s.app,i);return e!=null&&e.id?(new j(s.app,s,i,e.id,(t=e.title)!=null?t:"(untitled)",e.tier).open(),!0):!1}var w=require("obsidian"),B=class extends w.Modal{constructor(e,t){super(e);this.plugin=t;this.searching=!1}onOpen(){let{contentEl:e}=this;e.empty(),e.addClass("noema-search-modal"),this.titleEl.setText("Search traces");let t=e.createEl("div",{cls:"noema-search-controls"});this.queryInput=t.createEl("input",{cls:"noema-search-query",attr:{type:"search",placeholder:"Search traces"}}),this.modeSelect=t.createEl("select",{cls:"noema-search-mode"});for(let n of["hybrid","semantic","lexical"]){let r=this.modeSelect.createEl("option",{text:n});r.value=n}this.modeSelect.value=this.plugin.settings.searchMode,this.limitSelect=t.createEl("select",{cls:"noema-search-limit"});for(let n of[5,10]){let r=this.limitSelect.createEl("option",{text:`${n} results`});r.value=String(n)}this.limitSelect.value="5",this.searchBtn=t.createEl("button",{text:"Search",cls:"mod-cta noema-search-submit"}),this.errorEl=e.createEl("div",{cls:"noema-search-error"}),this.errorEl.style.display="none",this.resultEl=e.createEl("div",{cls:"noema-search-results"}),this.searchBtn.addEventListener("click",()=>this.search()),this.queryInput.addEventListener("keydown",n=>{n.key==="Enter"&&(n.preventDefault(),this.search())}),setTimeout(()=>this.queryInput.focus(),0)}onClose(){this.contentEl.empty()}async search(){if(this.searching)return;this.hideError(),this.resultEl.empty();let e=this.queryInput.value.trim();if(!e){this.showError("Search query is required."),this.queryInput.focus();return}let t=this.plugin.client;if(!t){this.showError("No noema endpoint configured. Set one in Settings -> Noema.");return}this.searching=!0,this.searchBtn.disabled=!0,this.searchBtn.setText("Searching...");try{let n=this.modeSelect.value,r=await t.searchTraces(e,n),a=Number.parseInt(this.limitSelect.value,10)||5;this.renderResults(r.slice(0,a),r.length)}catch(n){let r=n instanceof Error?n.message:String(n);this.showError(`Search failed: ${r}`)}finally{this.searching=!1,this.searchBtn.disabled=!1,this.searchBtn.setText("Search")}}renderResults(e,t){var n;if(this.resultEl.empty(),e.length===0){this.resultEl.createEl("div",{cls:"noema-search-empty",text:"No matching traces."});return}t>e.length&&this.resultEl.createEl("div",{cls:"noema-search-count",text:`Showing ${e.length} of ${t} results`});for(let r of e){let a=this.resultEl.createEl("button",{cls:"noema-search-result"});a.type="button";let o=(n=this.localTitle(r.id))!=null?n:r.title;a.createEl("div",{cls:"noema-search-result-title",text:o}),a.createEl("div",{cls:"noema-search-result-id",text:r.id});let l=a.createEl("div",{cls:"noema-search-result-meta"});r.type&&l.createEl("span",{text:r.type}),r.author&&l.createEl("span",{text:r.author}),r.created&&l.createEl("span",{text:r.created}),a.addEventListener("click",()=>this.openTrace(r.id))}}async openTrace(e){let t=this.plugin.settings.tracesFolder.replace(/\/+$/,""),n=this.app.vault.getFileByPath(`${t}/${e}.md`);if(!(n instanceof w.TFile)){new w.Notice(`Noema: ${e}.md was not found in ${t}.`);return}await this.app.workspace.getLeaf(!1).openFile(n),this.close()}localTitle(e){var o;let t=this.plugin.settings.tracesFolder.replace(/\/+$/,""),n=this.app.vault.getFileByPath(`${t}/${e}.md`);if(!(n instanceof w.TFile))return null;let r=this.app.metadataCache.getFileCache(n),a=(o=r==null?void 0:r.frontmatter)==null?void 0:o.title;return typeof a=="string"&&a.trim()?a.trim():null}showError(e){this.errorEl.setText(e),this.errorEl.style.display=""}hideError(){this.errorEl.empty(),this.errorEl.style.display="none"}};var pe=3e4,M=class extends d.Plugin{constructor(){super(...arguments);this.settings=O;this.client=null;this.cortexName="";this.statusBarEl=null;this.connState="disconnected";this.immutableWarning=null}async onload(){await this.loadSettings(),this.refreshClient(),this.statusBarEl=this.addStatusBarItem(),this.statusBarEl.addClass("noema-status"),this.renderStatus(),this.addSettingTab(new I(this.app,this)),this.registerView(x,e=>new C(e,this)),this.addCommand({id:"open-lineage-view",name:"Open lineage view",callback:()=>this.activateLineageView()}),this.addCommand({id:"refresh-lineage",name:"Refresh lineage view",callback:async()=>{let e=this.app.workspace.getLeavesOfType(x);for(let t of e){let n=t.view;n instanceof C&&await n.refreshFromActive()}}}),this.addCommand({id:"create-trace",name:"New trace",callback:()=>{new A(this.app,this).open()}}),this.addCommand({id:"search-traces",name:"Search traces",callback:()=>{new B(this.app,this).open()}}),this.addCommand({id:"append-to-trace",name:"Append to current trace",checkCallback:e=>{let t=this.app.workspace.getActiveFile();return t?e?t.extension==="md":(Z(this)||new d.Notice("Open a trace first to append to it."),!0):!1}}),this.immutableWarning=new R(this.app,this),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>{var e;this.renderStatus(),(e=this.immutableWarning)==null||e.refresh()})),this.registerEvent(this.app.metadataCache.on("changed",()=>{var e;this.renderStatus(),(e=this.immutableWarning)==null||e.refresh()})),this.immutableWarning.refresh(),this.pingConnection(),this.registerInterval(window.setInterval(()=>this.pingConnection(),pe))}onunload(){var e;(e=this.immutableWarning)==null||e.removeAll()}async loadSettings(){let e=await this.loadData();this.settings={...O,...e!=null?e:{}}}async saveSettings(){await this.saveData(this.settings)}refreshClient(){if(!this.settings.endpoint){this.client=null,this.connState="disconnected",this.renderStatus();return}this.client?this.client.updateConfig(this.settings.endpoint,this.settings.bearerKey):this.client=new N(this.settings.endpoint,this.settings.bearerKey),this.pingConnection()}async probe(){if(!this.client)return this.cortexName="",{state:"disconnected"};try{let e=await this.client.cortexIdentity();return this.cortexName=e.name,{state:"connected"}}catch(e){return this.cortexName="",{state:e instanceof T?"unauthorized":"disconnected",error:e}}}async pingConnection(){let e=await this.probe();this.setConnState(e.state),this.renderStatus()}async testConnection(){if(!this.settings.endpoint){new d.Notice("Noema: set an HTTP endpoint first.");return}this.client||(this.client=new N(this.settings.endpoint,this.settings.bearerKey));let e=await this.probe();switch(this.connState=e.state,this.renderStatus(),e.state){case"connected":new d.Notice(`Noema: connected to ${this.cortexName||this.settings.endpoint}.`);break;case"unauthorized":new d.Notice(this.settings.bearerKey?`Noema: ${this.settings.endpoint} rejected the bearer key (HTTP 401). Check the key below.`:`Noema: ${this.settings.endpoint} requires a bearer key (HTTP 401). Set one below.`,8e3);break;case"disconnected":new d.Notice(`Noema: couldn't connect to ${this.settings.endpoint}: ${z(e.error)}.`,8e3);break}}setConnState(e){let t=this.connState;if(this.connState=e,e==="unauthorized"&&t!=="unauthorized"){let n=this.settings.bearerKey?"the bearer key was rejected":"this server requires a bearer key";new d.Notice(`Noema: ${n} (HTTP 401). Update it in the Noema plugin settings.`,8e3)}}renderStatus(){if(!this.statusBarEl)return;this.statusBarEl.empty();let e=this.app.workspace.getActiveFile();if(e instanceof d.TFile){let n=v(this.app,e);if(n!=null&&n.id){let r=this.statusBarEl.createSpan({cls:"noema-status-glyph"});r.setText(`[${E(n.tier)}]`),r.setAttr("title",P(n.tier)),this.statusBarEl.createSpan({cls:"noema-status-divider",text:" "})}}let t=this.statusBarEl.createSpan({cls:"noema-status-conn"});if(!this.settings.endpoint){t.setText("noema: unconfigured"),t.setAttr("title","Set an HTTP endpoint in settings to connect.");return}this.connState==="connected"?(t.setText(`noema: ${this.cortexName||"connected"}`),t.setAttr("title",`Connected to ${this.settings.endpoint}`),t.addClass("noema-status-ok")):this.connState==="unauthorized"?(t.setText("noema: unauthorized"),t.setAttr("title",`${this.settings.endpoint} rejected the bearer key (HTTP 401). Check the key in Noema settings.`),t.addClass("noema-status-err")):(t.setText("noema: disconnected"),t.setAttr("title",`Couldn't reach ${this.settings.endpoint}`),t.addClass("noema-status-err"))}async activateLineageView(){var n;let{workspace:e}=this.app,t=(n=e.getLeavesOfType(x)[0])!=null?n:null;t||(t=e.getRightLeaf(!1),t||(t=e.getLeaf(!0)),await t.setViewState({type:x,active:!0})),e.revealLeaf(t)}}; +var D=Object.defineProperty;var re=Object.getOwnPropertyDescriptor;var ne=Object.getOwnPropertyNames;var se=Object.prototype.hasOwnProperty;var ae=(s,i)=>{for(var e in i)D(s,e,{get:i[e],enumerable:!0})},oe=(s,i,e,t)=>{if(i&&typeof i=="object"||typeof i=="function")for(let r of ne(i))!se.call(s,r)&&r!==e&&D(s,r,{get:()=>i[r],enumerable:!(t=re(i,r))||t.enumerable});return s};var le=s=>oe(D({},"__esModule",{value:!0}),s);var ve={};ae(ve,{default:()=>O});module.exports=le(ve);var d=require("obsidian");var m=require("obsidian"),W={endpoint:"",bearerKey:"",tracesFolder:"traces",showFileExplorerTierBadges:!0,defaultAuthor:"",searchMode:"hybrid"},B=class extends m.PluginSettingTab{constructor(e,t){super(e,t);this.plugin=t}display(){let{containerEl:e}=this;e.empty(),e.createEl("h2",{text:"Noema"}),e.createEl("p",{text:"Connect this Obsidian vault to a running noema serve --transport http endpoint to see lineage and tier metadata for traces. The vault should be opened on the cortex root (the directory containing cortex.md)."}),new m.Setting(e).setName("HTTP endpoint").setDesc("Base URL of the noema HTTP server, e.g. https://noema.local:3000").addText(t=>t.setPlaceholder("https://hostname:3000").setValue(this.plugin.settings.endpoint).onChange(async r=>{this.plugin.settings.endpoint=r.trim(),await this.plugin.saveSettings(),this.plugin.refreshClient()})),new m.Setting(e).setName("Bearer key").setDesc("Shared MCP access key (NOEMA_MCP_KEY). Required for keyed-mode servers; leave empty for open-mode (loopback only).").addText(t=>{t.setPlaceholder("(secret)").setValue(this.plugin.settings.bearerKey).onChange(async r=>{this.plugin.settings.bearerKey=r,await this.plugin.saveSettings(),this.plugin.refreshClient()}),t.inputEl.type="password"}),new m.Setting(e).setName("Test connection").setDesc("Probe the endpoint now with the current key and report the result.").addButton(t=>t.setButtonText("Test connection").onClick(async()=>{t.setDisabled(!0),t.setButtonText("Testing\u2026");try{await this.plugin.testConnection()}finally{t.setButtonText("Test connection"),t.setDisabled(!1)}})),new m.Setting(e).setName("Traces folder").setDesc("Folder inside the vault that contains trace markdown files. Default: traces").addText(t=>t.setPlaceholder("traces").setValue(this.plugin.settings.tracesFolder).onChange(async r=>{this.plugin.settings.tracesFolder=r.trim()||"traces",await this.plugin.saveSettings(),this.plugin.refreshFileExplorerTierBadges()})),new m.Setting(e).setName("Show tier badges in file explorer").setDesc("Show [s], [m], and [L] beside traces in the configured traces folder.").addToggle(t=>t.setValue(this.plugin.settings.showFileExplorerTierBadges).onChange(async r=>{this.plugin.settings.showFileExplorerTierBadges=r,await this.plugin.saveSettings(),this.plugin.refreshFileExplorerTierBadges()})),new m.Setting(e).setName("Default author").setDesc("Stamped on traces created via the 'New trace' command. Leave empty to let the cortex's default apply. Free-form \u2014 typical values are a username, agent name, or 'agent/handle'.").addText(t=>t.setPlaceholder("e.g. alice or agent/researcher-1").setValue(this.plugin.settings.defaultAuthor).onChange(async r=>{this.plugin.settings.defaultAuthor=r.trim(),await this.plugin.saveSettings()})),new m.Setting(e).setName("Noema search mode").setDesc("Used by the 'Search traces' command. Hybrid uses the cortex's server-side hybrid_weight.").addDropdown(t=>t.addOption("hybrid","Hybrid").addOption("semantic","Semantic").addOption("lexical","Lexical").setValue(this.plugin.settings.searchMode).onChange(async r=>{this.plugin.settings.searchMode=r,await this.plugin.saveSettings()}))}};var x=require("obsidian");function g(s,i){let e=s.metadataCache.getFileCache(i),t=e==null?void 0:e.frontmatter;return t?{id:typeof t.id=="string"?t.id:void 0,title:typeof t.title=="string"?t.title:void 0,type:typeof t.type=="string"?t.type:void 0,tier:ce(t.tier)}:null}function ce(s){if(typeof s!="string")return"short";let i=s.toLowerCase();return i==="short"||i==="mid"||i==="long"?i:"unknown"}function f(s){switch(s){case"short":return"s";case"mid":return"m";case"long":return"L";default:return"?"}}function T(s){switch(s){case"short":return"short-term";case"mid":return"mid-term";case"long":return"long-term (immutable)";default:return"unknown tier"}}var S="noema-lineage-view",P=class extends x.ItemView{constructor(e,t){super(e);this.plugin=t;this.currentTraceId=null;this.bodyEl=null}getViewType(){return S}getDisplayText(){return"Noema lineage"}getIcon(){return"git-merge"}async onOpen(){this.contentEl.empty(),this.contentEl.addClass("noema-lineage-view"),this.contentEl.createEl("div",{cls:"noema-lineage-header"}).createEl("h3",{text:"Lineage"}),this.bodyEl=this.contentEl.createEl("div",{cls:"noema-lineage-body"}),this.renderEmpty("Open a trace to see its lineage."),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>{this.refreshFromActive()})),await this.refreshFromActive()}async refreshFromActive(){var r;let e=this.app.workspace.getActiveFile();if(!e){this.renderEmpty("No file is open.");return}let t=g(this.app,e);if(!(t!=null&&t.id)){this.renderEmpty("This file isn't a trace (no `id` in frontmatter)."),this.currentTraceId=null;return}t.id!==this.currentTraceId&&(this.currentTraceId=t.id,await this.fetchAndRender(t.id,(r=t.title)!=null?r:"(untitled)",t.tier))}async fetchAndRender(e,t,r){if(!this.bodyEl)return;this.bodyEl.empty(),this.bodyEl.createEl("div",{cls:"noema-lineage-loading",text:`Loading lineage for ${e}\u2026`});let n=this.plugin.client;if(!n){this.renderEmpty("Configure the noema HTTP endpoint in settings to load lineage.");return}let a;try{a=await n.traceLineage(e)}catch(o){this.renderError(`Couldn't load lineage: ${o instanceof Error?o.message:String(o)}`);return}this.renderLineage(e,t,r,a)}renderLineage(e,t,r,n){if(!this.bodyEl)return;this.bodyEl.empty(),this.renderSection("Derived from",n.derivedFrom);let a=this.bodyEl.createEl("div",{cls:"noema-lineage-current"});a.createEl("div",{cls:"noema-lineage-current-glyph",text:`[${f(r)}]`}),a.createEl("div",{cls:"noema-lineage-current-title",text:t}),a.createEl("div",{cls:"noema-lineage-current-id",text:e}),a.setAttr("title",T(r)),this.renderSection("Derived by",n.derivedBy),n.derivedFrom.length===0&&n.derivedBy.length===0&&this.bodyEl.createEl("div",{cls:"noema-lineage-empty",text:"This trace has no recorded derivations."})}renderSection(e,t){var a,o;if(!this.bodyEl||t.length===0)return;let r=this.bodyEl.createEl("div",{cls:"noema-lineage-section"});r.createEl("div",{cls:"noema-lineage-section-label",text:e});let n=r.createEl("div",{cls:"noema-lineage-section-list"});for(let l of t){let c=this.findTraceFile(l),p=c?g(this.app,c):null,h=(a=p==null?void 0:p.tier)!=null?a:"unknown",N=(o=p==null?void 0:p.title)!=null?o:"(unknown)",u=n.createEl("div",{cls:"noema-lineage-row"});u.createEl("span",{cls:"noema-lineage-glyph",text:`[${f(h)}]`});let y=u.createEl("a",{cls:"noema-lineage-link",text:N,href:"#"});y.setAttr("data-trace-id",l),y.setAttr("title",`${l} (${T(h)})`),u.appendChild(document.createTextNode(" ")),u.createEl("span",{cls:"noema-lineage-id",text:l}),y.addEventListener("click",v=>{v.preventDefault(),c?this.app.workspace.getLeaf(!1).openFile(c):new x.Notice(`Trace ${l} not found in this vault.`)})}}findTraceFile(e){let r=`${this.plugin.settings.tracesFolder.replace(/\/+$/,"")}/${e}.md`,n=this.app.vault.getFileByPath(r);return n instanceof x.TFile?n:null}renderEmpty(e){this.bodyEl&&(this.bodyEl.empty(),this.bodyEl.createEl("div",{cls:"noema-lineage-empty",text:e}))}renderError(e){this.bodyEl&&(this.bodyEl.empty(),this.bodyEl.createEl("div",{cls:"noema-lineage-error",text:e}))}};var z=class extends Error{constructor(e,t,r){super(t);this.code=e;this.data=r;this.name="JsonRpcError"}},w=class extends Error{constructor(e){super("MCP server rejected the bearer key (HTTP 401)");this.serverMessage=e;this.name="UnauthorizedError"}},j=class extends Error{constructor(i,e){super(`${i}: ${K(e)}`),this.name="TransportError"}};function K(s){var r;if(s==null)return"unknown connection error";let i=s instanceof Error?s.message.trim():String(s).trim(),e=s instanceof Error?s.cause:void 0,t=(r=Y(e))!=null?r:Y(s);if(t){let n={ECONNREFUSED:"connection refused",ECONNRESET:"connection reset",ETIMEDOUT:"connection timed out",ENOTFOUND:"DNS lookup failed",EAI_AGAIN:"DNS lookup temporarily failed",CERT_HAS_EXPIRED:"TLS certificate expired",DEPTH_ZERO_SELF_SIGNED_CERT:"TLS certificate is self-signed",UNABLE_TO_VERIFY_LEAF_SIGNATURE:"TLS certificate could not be verified"}[t];return`${n!=null?n:i||"connection failed"} (${t})`}return i||"unknown connection error"}function Y(s){if(!s||typeof s!="object"||!("code"in s))return null;let i=s.code;return typeof i=="string"&&i?i:null}async function q(s,i,e){try{return await fetch(s,i)}catch(t){throw new j(e,t)}}var Q=["fact","decision","preference","context","skill","intent","observation","note"],de="2025-03-26",C=class{constructor(i,e){this.endpoint=i;this.bearerKey=e;this.nextId=1;this.sessionId=null;this.sessionPromise=null}updateConfig(i,e){this.endpoint=i,this.bearerKey=e,this.sessionId=null,this.sessionPromise=null}async cortexIdentity(){let i=await this.callToolText("cortex_identity",{});return JSON.parse(i)}async traceLineage(i){let e=await this.callToolText("trace_lineage",{id:i});return pe(e,i)}async appendTrace(i,e){await this.callToolText("append_trace",{id:i,content:e})}async createTrace(i){let e={title:i.title,type:i.type,body:i.body};i.author&&(e.author=i.author),i.tags&&i.tags.length>0&&(e.tags=i.tags.join(",")),i.derivedFrom&&i.derivedFrom.length>0&&(e.derived_from=i.derivedFrom.join(","));let t=await this.callToolText("create_trace",e),r=t.match(/Trace created:\s*(\S+)/);if(!r)throw new Error(`create_trace: unexpected response shape: ${t}`);return r[1]}async searchTraces(i,e){let t=await this.callToolText("search_traces",{query:i,mode:e});return he(t)}async callToolText(i,e){let t=await this.callTool(i,e);if(t.isError){let r=t.content.map(n=>{var a;return(a=n.text)!=null?a:""}).join(` +`).trim();throw new Error(`tool ${i}: ${r||"unknown error"}`)}return t.content.map(r=>{var n;return(n=r.text)!=null?n:""}).join("")}async callTool(i,e){await this.ensureSession();try{return await this.callToolOnce(i,e)}catch(t){if(t instanceof I)return this.sessionId=null,this.sessionPromise=null,await this.ensureSession(),await this.callToolOnce(i,e);throw t}}async callToolOnce(i,e){return await this.rpc({method:"tools/call",params:{name:i,arguments:e}})}async ensureSession(){this.sessionId||(this.sessionPromise||(this.sessionPromise=this.handshake().catch(i=>{throw this.sessionPromise=null,i})),await this.sessionPromise)}async handshake(){let i=this.buildHeaders(),e=this.mcpUrl(),t={jsonrpc:"2.0",id:this.nextId++,method:"initialize",params:{protocolVersion:de,capabilities:{},clientInfo:{name:"noema-obsidian",version:"0.4.0"}}},r=await q(e,{method:"POST",headers:i,body:JSON.stringify(t)},"initialize request");if(r.status===401)throw new w(await V(r));if(!r.ok)throw new Error(`initialize: HTTP ${r.status}: ${r.statusText}`);await X(r,t.id);let n=r.headers.get("Mcp-Session-Id");if(!n)throw new Error("initialize: server did not return Mcp-Session-Id header");this.sessionId=n;let a={...this.buildHeaders(),"Mcp-Session-Id":n},o=await q(e,{method:"POST",headers:a,body:JSON.stringify({jsonrpc:"2.0",method:"notifications/initialized",params:{}})},"initialized notification");if(o.status===401)throw new w(await V(o));if(!o.ok)throw new Error(`initialized notification: HTTP ${o.status}: ${o.statusText}`)}async rpc(i){let e=this.nextId++,t=this.buildHeaders();this.sessionId&&(t["Mcp-Session-Id"]=this.sessionId);let r=await q(this.mcpUrl(),{method:"POST",headers:t,body:JSON.stringify({jsonrpc:"2.0",id:e,...i})},"MCP request");if(r.status===401)throw new w(await V(r));if(r.status===404)throw new I;if(!r.ok)throw new Error(`HTTP ${r.status}: ${r.statusText}`);let n=await X(r,e);if(n.error)throw new z(n.error.code,n.error.message,n.error.data);if(n.result===void 0)throw new Error("response missing result");return n.result}buildHeaders(){let i={"Content-Type":"application/json",Accept:"application/json, text/event-stream"};return this.bearerKey&&(i.Authorization=`Bearer ${this.bearerKey}`),i}mcpUrl(){return this.endpoint.replace(/\/+$/,"")+"/mcp"}};async function X(s,i){var n;if(!((n=s.headers.get("Content-Type"))!=null?n:"").toLowerCase().includes("text/event-stream"))return await s.json();let t=[];for(let a of(await s.text()).split(/\r?\n\r?\n/)){let o=a.split(/\r?\n/).filter(l=>l.startsWith("data:")).map(l=>l.slice(5).replace(/^ /,"")).join(` +`);if(o)try{t.push(JSON.parse(o))}catch(l){}}let r=t.find(a=>a.id===i);if(!r)throw new Error(`SSE response missing JSON-RPC id ${i}`);return r}var I=class extends Error{constructor(){super("invalid MCP session"),this.name="InvalidSessionError"}};async function V(s){try{let i=(await s.text()).trim();if(!i)return;try{let e=JSON.parse(i);if(e&&typeof e.error=="string")return e.error}catch(e){}return i}catch(i){return}}function pe(s,i){let e=s.split(/\r?\n/),t=[],r=[],n=i;for(let a of e){let o=a.trim();o.startsWith("Trace:")?n=o.slice(6).trim():o.startsWith("Derived from:")?t=Z(o.slice(13)):o.startsWith("Derived by:")&&(r=Z(o.slice(11)))}return{traceId:n,derivedFrom:t,derivedBy:r}}function he(s){var t,r,n,a,o,l,c,p,h,N;let i=[],e=s.split(/\r?\n/);for(let u=0;u_.trim());E.length<2||E[0]!==J||i.push({id:E[0],title:(l=E[1])!=null?l:"",type:(c=E[2])!=null?c:"",author:(p=E[3])!=null?p:"",tags:(h=E[4])!=null?h:"",created:(N=E[5])!=null?N:""})}return i}function Z(s){let i=s.trim();return!i||i==="(none)"?[]:i.split(",").map(e=>e.trim()).filter(Boolean)}var k=require("obsidian");var A=class extends k.Modal{constructor(e,t){super(e);this.plugin=t;this.submitting=!1}onOpen(){let{contentEl:e}=this;e.empty(),e.addClass("noema-create-modal"),this.titleEl.setText("New trace"),this.titleInput=this.row("Title","input",{type:"text",placeholder:"Why we chose local storage"}),this.typeSelect=this.row("Type","select");for(let n of Q){let a=this.typeSelect.createEl("option",{text:n});a.value=n}this.typeSelect.value="note",this.tagsInput=this.row("Tags","input",{type:"text",placeholder:"storage, local-first, architecture"}),this.descriptor("Comma- or semicolon-separated. Optional."),this.tagWarningsEl=e.createEl("div",{cls:"noema-create-tag-warnings"}),this.tagWarningsEl.style.display="none",this.tagsInput.addEventListener("input",()=>this.refreshTagWarnings()),this.derivedFromInput=this.row("Derived from","input",{type:"text",placeholder:"20260328-language-candidates, 20260329-runtime-survey"}),this.descriptor("Comma-separated trace IDs this one is derived from. Optional."),this.errorEl=e.createEl("div",{cls:"noema-create-error"}),this.errorEl.style.display="none";let t=e.createEl("div",{cls:"noema-create-buttons"});t.createEl("button",{text:"Cancel"}).addEventListener("click",()=>this.close()),this.submitBtn=t.createEl("button",{text:"Create",cls:"mod-cta"}),this.submitBtn.addEventListener("click",()=>this.submit()),this.titleInput.addEventListener("keydown",n=>{n.key==="Enter"&&(n.preventDefault(),this.submit())}),setTimeout(()=>this.titleInput.focus(),0)}onClose(){this.contentEl.empty()}row(e,t,r){let n=this.contentEl.createEl("div",{cls:"noema-create-row"});n.createEl("label",{text:e});let a=n.createEl(t);if(r)for(let[o,l]of Object.entries(r))a.setAttribute(o,l);return a}descriptor(e){this.contentEl.createEl("div",{cls:"noema-create-hint",text:e})}refreshTagWarnings(){let e=U(this.tagsInput.value),t=ue(e);if(this.tagWarningsEl.empty(),t.length===0){this.tagWarningsEl.style.display="none";return}this.tagWarningsEl.style.display="";for(let r of t){let n=this.tagWarningsEl.createEl("div",{cls:"noema-create-tag-warning"});n.createEl("span",{cls:"noema-create-tag-warning-glyph",text:"\u26A0"}),n.createEl("code",{cls:"noema-create-tag-warning-tag",text:r.tag}),n.appendChild(document.createTextNode(" \u2014 ")),n.createEl("span",{cls:"noema-create-tag-warning-reason",text:r.reason})}}async submit(){if(this.submitting)return;this.errorEl.style.display="none",this.errorEl.empty();let e=this.titleInput.value.trim();if(!e){this.showError("Title is required."),this.titleInput.focus();return}let t=this.typeSelect.value,r=U(this.tagsInput.value),n=U(this.derivedFromInput.value),a=this.plugin.client;if(!a){this.showError("No noema endpoint configured. Set one in Settings \u2192 Noema.");return}let o={title:e,type:t,body:" ",author:this.plugin.settings.defaultAuthor||void 0,tags:r.length>0?r:void 0,derivedFrom:n.length>0?n:void 0};this.submitting=!0,this.submitBtn.disabled=!0,this.submitBtn.setText("Creating\u2026");try{let l=await a.createTrace(o);this.close(),new k.Notice(`Created ${l}`),await this.openTraceFile(l)}catch(l){let c=l instanceof Error?l.message:String(l);this.showError(`Couldn't create trace: ${c}`),this.submitBtn.disabled=!1,this.submitBtn.setText("Create")}finally{this.submitting=!1}}async openTraceFile(e){let r=`${this.plugin.settings.tracesFolder.replace(/\/+$/,"")}/${e}.md`;for(let n=0;n<20;n++){let a=this.app.vault.getFileByPath(r);if(a instanceof k.TFile){await this.app.workspace.getLeaf(!1).openFile(a);return}await me(100)}}showError(e){this.errorEl.setText(e),this.errorEl.style.display=""}};function U(s){return s?s.split(/[,;]/).map(i=>i.trim()).filter(Boolean):[]}function ue(s){let i=[];for(let e of s)/^[0-9_-]+$/.test(e)&&i.push({tag:e,reason:"Obsidian requires at least one letter. Pure-numeric tags are not surfaced in its tag panel."}),e.includes(".")&&i.push({tag:e,reason:'Obsidian interprets dots as nested-tag separators (so "a.b" becomes tag tree a/b).'});return i}function me(s){return new Promise(i=>setTimeout(i,s))}var ee=require("obsidian"),M=class{constructor(i,e){this.app=i;this.plugin=e;this.bannerEl=null;this.currentFile=null}refresh(){let i=this.app.workspace.getActiveViewOfType(ee.MarkdownView);if(!i){this.removeBanner();return}let e=i.file;if(!e){this.removeBanner();return}let t=this.shouldWarn(e);if(!t){this.removeBanner();return}this.showBanner(i,e,t)}removeAll(){this.removeBanner()}shouldWarn(i){let e=this.app.metadataCache.getFileCache(i),t=e==null?void 0:e.frontmatter;if(!t)return null;if((typeof t.tier=="string"?t.tier.toLowerCase():"")==="long")return{kind:"long-tier"};let n=t.source_locked===!0||t.source_locked==="true",a=typeof t.origin=="string"?t.origin:"",o=this.plugin.cortexName;return n&&a&&o&&a!==o?{kind:"source-locked",origin:a}:null}showBanner(i,e,t){if(this.bannerEl&&this.currentFile===e&&this.bannerEl.dataset.kind===t.kind)return;this.removeBanner();let r=i.containerEl.querySelector(".view-content");if(!(r instanceof HTMLElement))return;let n=document.createElement("div");n.className="noema-immutable-warning",n.dataset.kind=t.kind,n.createSpan({cls:"noema-immutable-warning-glyph"}).setText(t.kind==="long-tier"?"[L]":"[locked]");let o=n.createSpan({cls:"noema-immutable-warning-text"});t.kind==="long-tier"?o.setText("Long-tier trace \u2014 immutable except via tier_votes. Edits will not be accepted by the cortex."):o.setText(`Source-locked trace from peer "${t.origin}" \u2014 local edits will not be accepted by the cortex.`),r.prepend(n),this.bannerEl=n,this.currentFile=e}removeBanner(){this.bannerEl&&(this.bannerEl.remove(),this.bannerEl=null),this.currentFile=null}};var L=require("obsidian");var G=class extends L.Modal{constructor(e,t,r,n,a,o){super(e);this.plugin=t;this.targetFile=r;this.targetId=n;this.targetTitle=a;this.targetTier=o;this.submitting=!1}onOpen(){let{contentEl:e}=this;e.empty(),e.addClass("noema-append-modal"),this.titleEl.setText("Append to trace");let t=e.createEl("div",{cls:"noema-append-target"});t.createEl("span",{cls:"noema-append-target-glyph"}).setText(`[${f(this.targetTier)}]`),t.createEl("span",{cls:"noema-append-target-title"}).setText(this.targetTitle),t.createEl("div",{cls:"noema-append-target-id"}).setText(this.targetId),e.createEl("div",{cls:"noema-append-label"}).setText("Content to append"),this.contentInput=e.createEl("textarea",{cls:"noema-append-content",attr:{rows:"6",placeholder:"Type or paste content to append\u2026"}}),e.createEl("div",{cls:"noema-append-hint"}).setText("A newline is inserted automatically between the existing body and your content if needed. \u2318+Enter to submit."),this.errorEl=e.createEl("div",{cls:"noema-append-error"}),this.errorEl.style.display="none";let c=e.createEl("div",{cls:"noema-append-buttons"});c.createEl("button",{text:"Cancel"}).addEventListener("click",()=>this.close()),this.submitBtn=c.createEl("button",{text:"Append",cls:"mod-cta"}),this.submitBtn.addEventListener("click",()=>this.submit()),this.contentInput.addEventListener("keydown",h=>{h.key==="Enter"&&(h.metaKey||h.ctrlKey)&&(h.preventDefault(),this.submit())}),setTimeout(()=>this.contentInput.focus(),0)}onClose(){this.contentEl.empty()}async submit(){if(this.submitting)return;this.errorEl.style.display="none",this.errorEl.empty();let t=this.contentInput.value.replace(/\s+$/,"");if(!t){this.showError("Content is required."),this.contentInput.focus();return}let r=this.plugin.client;if(!r){this.showError("No noema endpoint configured. Set one in Settings \u2192 Noema.");return}this.submitting=!0,this.submitBtn.disabled=!0,this.submitBtn.setText("Appending\u2026");try{await r.appendTrace(this.targetId,t),this.close(),new L.Notice(`Appended to ${this.targetId}`)}catch(n){let a=n instanceof Error?n.message:String(n);this.showError(`Couldn't append: ${a}`),this.submitBtn.disabled=!1,this.submitBtn.setText("Append")}finally{this.submitting=!1}}showError(e){this.errorEl.setText(e),this.errorEl.style.display=""}};function te(s){var t;let i=s.app.workspace.getActiveFile();if(!(i instanceof L.TFile))return!1;let e=g(s.app,i);return e!=null&&e.id?(new G(s.app,s,i,e.id,(t=e.title)!=null?t:"(untitled)",e.tier).open(),!0):!1}var b=require("obsidian"),R=class extends b.Modal{constructor(e,t){super(e);this.plugin=t;this.searching=!1}onOpen(){let{contentEl:e}=this;e.empty(),e.addClass("noema-search-modal"),this.titleEl.setText("Search traces");let t=e.createEl("div",{cls:"noema-search-controls"});this.queryInput=t.createEl("input",{cls:"noema-search-query",attr:{type:"search",placeholder:"Search traces"}}),this.modeSelect=t.createEl("select",{cls:"noema-search-mode"});for(let r of["hybrid","semantic","lexical"]){let n=this.modeSelect.createEl("option",{text:r});n.value=r}this.modeSelect.value=this.plugin.settings.searchMode,this.limitSelect=t.createEl("select",{cls:"noema-search-limit"});for(let r of[5,10]){let n=this.limitSelect.createEl("option",{text:`${r} results`});n.value=String(r)}this.limitSelect.value="5",this.searchBtn=t.createEl("button",{text:"Search",cls:"mod-cta noema-search-submit"}),this.errorEl=e.createEl("div",{cls:"noema-search-error"}),this.errorEl.style.display="none",this.resultEl=e.createEl("div",{cls:"noema-search-results"}),this.searchBtn.addEventListener("click",()=>this.search()),this.queryInput.addEventListener("keydown",r=>{r.key==="Enter"&&(r.preventDefault(),this.search())}),setTimeout(()=>this.queryInput.focus(),0)}onClose(){this.contentEl.empty()}async search(){if(this.searching)return;this.hideError(),this.resultEl.empty();let e=this.queryInput.value.trim();if(!e){this.showError("Search query is required."),this.queryInput.focus();return}let t=this.plugin.client;if(!t){this.showError("No noema endpoint configured. Set one in Settings -> Noema.");return}this.searching=!0,this.searchBtn.disabled=!0,this.searchBtn.setText("Searching...");try{let r=this.modeSelect.value,n=await t.searchTraces(e,r),a=Number.parseInt(this.limitSelect.value,10)||5;this.renderResults(n.slice(0,a),n.length)}catch(r){let n=r instanceof Error?r.message:String(r);this.showError(`Search failed: ${n}`)}finally{this.searching=!1,this.searchBtn.disabled=!1,this.searchBtn.setText("Search")}}renderResults(e,t){var r;if(this.resultEl.empty(),e.length===0){this.resultEl.createEl("div",{cls:"noema-search-empty",text:"No matching traces."});return}t>e.length&&this.resultEl.createEl("div",{cls:"noema-search-count",text:`Showing ${e.length} of ${t} results`});for(let n of e){let a=this.resultEl.createEl("button",{cls:"noema-search-result"});a.type="button";let o=(r=this.localTitle(n.id))!=null?r:n.title;a.createEl("div",{cls:"noema-search-result-title",text:o}),a.createEl("div",{cls:"noema-search-result-id",text:n.id});let l=a.createEl("div",{cls:"noema-search-result-meta"});n.type&&l.createEl("span",{text:n.type}),n.author&&l.createEl("span",{text:n.author}),n.created&&l.createEl("span",{text:n.created}),a.addEventListener("click",()=>this.openTrace(n.id))}}async openTrace(e){let t=this.plugin.settings.tracesFolder.replace(/\/+$/,""),r=this.app.vault.getFileByPath(`${t}/${e}.md`);if(!(r instanceof b.TFile)){new b.Notice(`Noema: ${e}.md was not found in ${t}.`);return}await this.app.workspace.getLeaf(!1).openFile(r),this.close()}localTitle(e){var o;let t=this.plugin.settings.tracesFolder.replace(/\/+$/,""),r=this.app.vault.getFileByPath(`${t}/${e}.md`);if(!(r instanceof b.TFile))return null;let n=this.app.metadataCache.getFileCache(r),a=(o=n==null?void 0:n.frontmatter)==null?void 0:o.title;return typeof a=="string"&&a.trim()?a.trim():null}showError(e){this.errorEl.setText(e),this.errorEl.style.display=""}hideError(){this.errorEl.empty(),this.errorEl.style.display="none"}};var H=require("obsidian");var ge="file-explorer",ie=".nav-file-title[data-path]",F="noema-file-tier-badge",$=class{constructor(i,e,t,r){this.app=i;this.plugin=e;this.getTracesFolder=t;this.getEnabled=r;this.observers=new Map;this.active=!1;this.refreshFrame=null}start(){this.active=!0,this.app.workspace.onLayoutReady(()=>{this.active&&(this.bindExplorerViews(),this.plugin.registerEvent(this.app.workspace.on("layout-change",()=>this.bindExplorerViews())),this.plugin.registerEvent(this.app.metadataCache.on("changed",i=>this.refreshFile(i.path))),this.plugin.registerEvent(this.app.vault.on("create",()=>this.refresh())),this.plugin.registerEvent(this.app.vault.on("rename",()=>this.refresh())),this.plugin.registerEvent(this.app.vault.on("delete",()=>this.refresh())))})}stop(){this.active=!1,this.refreshFrame!==null&&(window.cancelAnimationFrame(this.refreshFrame),this.refreshFrame=null);for(let[i,e]of this.observers)e.disconnect(),this.removeBadges(i);this.observers.clear()}refresh(){!this.active||this.refreshFrame!==null||(this.refreshFrame=window.requestAnimationFrame(()=>{this.refreshFrame=null;for(let i of this.observers.keys())i.querySelectorAll(ie).forEach(e=>this.decorateRow(e))}))}bindExplorerViews(){let i=new Set(this.app.workspace.getLeavesOfType(ge).map(e=>e.view.containerEl));for(let[e,t]of this.observers)i.has(e)||(t.disconnect(),this.removeBadges(e),this.observers.delete(e));for(let e of i){if(this.observers.has(e))continue;let t=new MutationObserver(()=>this.refresh());t.observe(e,{childList:!0,subtree:!0}),this.observers.set(e,t)}this.refresh()}refreshFile(i){if(this.active)for(let e of this.observers.keys())e.querySelectorAll(ie).forEach(t=>{t.dataset.path===i&&this.decorateRow(t)})}decorateRow(i){if(!this.getEnabled()){this.removeBadge(i);return}let e=i.dataset.path,t=(0,H.normalizePath)(this.getTracesFolder().trim()||"traces").replace(/\/$/,"");if(!e||!e.startsWith(`${t}/`)){this.removeBadge(i);return}let r=this.app.vault.getAbstractFileByPath(e);if(!(r instanceof H.TFile)||r.extension!=="md"){this.removeBadge(i);return}let n=g(this.app,r);if(!(n!=null&&n.id)){this.removeBadge(i);return}let a=i.querySelector(`.${F}`);if(!a){a=document.createElement("span"),a.classList.add(F);let c=i.querySelector(".nav-file-title-content");c?c.before(a):i.prepend(a)}let o=`[${f(n.tier)}]`,l=T(n.tier);a.className=`${F} noema-file-tier-${n.tier}`,a.textContent!==o&&(a.textContent=o),a.dataset.tier=n.tier,a.setAttribute("title",l),a.setAttribute("aria-label",l)}removeBadge(i){var e;(e=i.querySelector(`.${F}`))==null||e.remove()}removeBadges(i){i.querySelectorAll(`.${F}`).forEach(e=>e.remove())}};var fe=3e4,O=class extends d.Plugin{constructor(){super(...arguments);this.settings=W;this.client=null;this.cortexName="";this.statusBarEl=null;this.connState="disconnected";this.immutableWarning=null;this.fileExplorerTierBadges=null}async onload(){await this.loadSettings(),this.refreshClient(),this.statusBarEl=this.addStatusBarItem(),this.statusBarEl.addClass("noema-status"),this.renderStatus(),this.addSettingTab(new B(this.app,this)),this.registerView(S,e=>new P(e,this)),this.addCommand({id:"open-lineage-view",name:"Open lineage view",callback:()=>this.activateLineageView()}),this.addCommand({id:"refresh-lineage",name:"Refresh lineage view",callback:async()=>{let e=this.app.workspace.getLeavesOfType(S);for(let t of e){let r=t.view;r instanceof P&&await r.refreshFromActive()}}}),this.addCommand({id:"create-trace",name:"New trace",callback:()=>{new A(this.app,this).open()}}),this.addCommand({id:"search-traces",name:"Search traces",callback:()=>{new R(this.app,this).open()}}),this.addCommand({id:"append-to-trace",name:"Append to current trace",checkCallback:e=>{let t=this.app.workspace.getActiveFile();return t?e?t.extension==="md":(te(this)||new d.Notice("Open a trace first to append to it."),!0):!1}}),this.immutableWarning=new M(this.app,this),this.fileExplorerTierBadges=new $(this.app,this,()=>this.settings.tracesFolder,()=>this.settings.showFileExplorerTierBadges),this.fileExplorerTierBadges.start(),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>{var e;this.renderStatus(),(e=this.immutableWarning)==null||e.refresh()})),this.registerEvent(this.app.metadataCache.on("changed",()=>{var e;this.renderStatus(),(e=this.immutableWarning)==null||e.refresh()})),this.immutableWarning.refresh(),this.pingConnection(),this.registerInterval(window.setInterval(()=>this.pingConnection(),fe))}onunload(){var e,t;(e=this.immutableWarning)==null||e.removeAll(),(t=this.fileExplorerTierBadges)==null||t.stop()}async loadSettings(){let e=await this.loadData();this.settings={...W,...e!=null?e:{}}}async saveSettings(){await this.saveData(this.settings)}refreshFileExplorerTierBadges(){var e;(e=this.fileExplorerTierBadges)==null||e.refresh()}refreshClient(){if(!this.settings.endpoint){this.client=null,this.connState="disconnected",this.renderStatus();return}this.client?this.client.updateConfig(this.settings.endpoint,this.settings.bearerKey):this.client=new C(this.settings.endpoint,this.settings.bearerKey),this.pingConnection()}async probe(){if(!this.client)return this.cortexName="",{state:"disconnected"};try{let e=await this.client.cortexIdentity();return this.cortexName=e.name,{state:"connected"}}catch(e){return this.cortexName="",{state:e instanceof w?"unauthorized":"disconnected",error:e}}}async pingConnection(){let e=await this.probe();this.setConnState(e.state),this.renderStatus()}async testConnection(){if(!this.settings.endpoint){new d.Notice("Noema: set an HTTP endpoint first.");return}this.client||(this.client=new C(this.settings.endpoint,this.settings.bearerKey));let e=await this.probe();switch(this.connState=e.state,this.renderStatus(),e.state){case"connected":new d.Notice(`Noema: connected to ${this.cortexName||this.settings.endpoint}.`);break;case"unauthorized":new d.Notice(this.settings.bearerKey?`Noema: ${this.settings.endpoint} rejected the bearer key (HTTP 401). Check the key below.`:`Noema: ${this.settings.endpoint} requires a bearer key (HTTP 401). Set one below.`,8e3);break;case"disconnected":new d.Notice(`Noema: couldn't connect to ${this.settings.endpoint}: ${K(e.error)}.`,8e3);break}}setConnState(e){let t=this.connState;if(this.connState=e,e==="unauthorized"&&t!=="unauthorized"){let r=this.settings.bearerKey?"the bearer key was rejected":"this server requires a bearer key";new d.Notice(`Noema: ${r} (HTTP 401). Update it in the Noema plugin settings.`,8e3)}}renderStatus(){if(!this.statusBarEl)return;this.statusBarEl.empty();let e=this.app.workspace.getActiveFile();if(e instanceof d.TFile){let r=g(this.app,e);if(r!=null&&r.id){let n=this.statusBarEl.createSpan({cls:"noema-status-glyph"});n.setText(`[${f(r.tier)}]`),n.setAttr("title",T(r.tier)),this.statusBarEl.createSpan({cls:"noema-status-divider",text:" "})}}let t=this.statusBarEl.createSpan({cls:"noema-status-conn"});if(!this.settings.endpoint){t.setText("noema: unconfigured"),t.setAttr("title","Set an HTTP endpoint in settings to connect.");return}this.connState==="connected"?(t.setText(`noema: ${this.cortexName||"connected"}`),t.setAttr("title",`Connected to ${this.settings.endpoint}`),t.addClass("noema-status-ok")):this.connState==="unauthorized"?(t.setText("noema: unauthorized"),t.setAttr("title",`${this.settings.endpoint} rejected the bearer key (HTTP 401). Check the key in Noema settings.`),t.addClass("noema-status-err")):(t.setText("noema: disconnected"),t.setAttr("title",`Couldn't reach ${this.settings.endpoint}`),t.addClass("noema-status-err"))}async activateLineageView(){var r;let{workspace:e}=this.app,t=(r=e.getLeavesOfType(S)[0])!=null?r:null;t||(t=e.getRightLeaf(!1),t||(t=e.getLeaf(!0)),await t.setViewState({type:S,active:!0})),e.revealLeaf(t)}}; diff --git a/plugins/obsidian/manifest.json b/plugins/obsidian/manifest.json index 8e3b6d6..94232a7 100644 --- a/plugins/obsidian/manifest.json +++ b/plugins/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "noema", "name": "Noema", - "version": "0.3.0", + "version": "0.4.0", "minAppVersion": "1.4.0", "description": "Lineage view, tier visibility, and Noema-backed search for Noema cortex traces. Connects to a `noema serve --transport http` endpoint.", "author": "Mark Baker (https://github.com/Fail-Safe)", diff --git a/plugins/obsidian/package-lock.json b/plugins/obsidian/package-lock.json index c865bb5..4e62208 100644 --- a/plugins/obsidian/package-lock.json +++ b/plugins/obsidian/package-lock.json @@ -1,12 +1,12 @@ { "name": "noema-obsidian", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "noema-obsidian", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "devDependencies": { "@types/node": "^20", diff --git a/plugins/obsidian/package.json b/plugins/obsidian/package.json index 1e01eb3..be2d05b 100644 --- a/plugins/obsidian/package.json +++ b/plugins/obsidian/package.json @@ -1,6 +1,6 @@ { "name": "noema-obsidian", - "version": "0.3.0", + "version": "0.4.0", "description": "Lineage view, tier visibility, and Noema-backed search for Noema cortex traces inside Obsidian.", "main": "main.js", "scripts": { diff --git a/plugins/obsidian/src/file-explorer-tiers.ts b/plugins/obsidian/src/file-explorer-tiers.ts new file mode 100644 index 0000000..bc65319 --- /dev/null +++ b/plugins/obsidian/src/file-explorer-tiers.ts @@ -0,0 +1,150 @@ +import { App, normalizePath, Plugin, TFile } from "obsidian"; +import { readTraceMetadata, tierGlyph, tierLabel } from "./tier-status"; + +const FILE_EXPLORER_VIEW_TYPE = "file-explorer"; +const FILE_ROW_SELECTOR = ".nav-file-title[data-path]"; +const BADGE_CLASS = "noema-file-tier-badge"; + +// FileExplorerTierBadges adds tier visibility to Obsidian's native file +// explorer without taking ownership of its tree. Obsidian does not expose a +// public row-render hook, so the DOM selectors are deliberately isolated in +// this class. Metadata and lifecycle updates still use supported APIs. +export class FileExplorerTierBadges { + private readonly observers = new Map(); + private active = false; + private refreshFrame: number | null = null; + + constructor( + private readonly app: App, + private readonly plugin: Plugin, + private readonly getTracesFolder: () => string, + private readonly getEnabled: () => boolean + ) {} + + start(): void { + this.active = true; + this.app.workspace.onLayoutReady(() => { + if (!this.active) return; + this.bindExplorerViews(); + + this.plugin.registerEvent( + this.app.workspace.on("layout-change", () => this.bindExplorerViews()) + ); + this.plugin.registerEvent( + this.app.metadataCache.on("changed", (file) => this.refreshFile(file.path)) + ); + this.plugin.registerEvent(this.app.vault.on("create", () => this.refresh())); + this.plugin.registerEvent(this.app.vault.on("rename", () => this.refresh())); + this.plugin.registerEvent(this.app.vault.on("delete", () => this.refresh())); + }); + } + + stop(): void { + this.active = false; + if (this.refreshFrame !== null) { + window.cancelAnimationFrame(this.refreshFrame); + this.refreshFrame = null; + } + for (const [root, observer] of this.observers) { + observer.disconnect(); + this.removeBadges(root); + } + this.observers.clear(); + } + + refresh(): void { + if (!this.active || this.refreshFrame !== null) return; + this.refreshFrame = window.requestAnimationFrame(() => { + this.refreshFrame = null; + for (const root of this.observers.keys()) { + root + .querySelectorAll(FILE_ROW_SELECTOR) + .forEach((row) => this.decorateRow(row)); + } + }); + } + + private bindExplorerViews(): void { + const currentRoots = new Set( + this.app.workspace + .getLeavesOfType(FILE_EXPLORER_VIEW_TYPE) + .map((leaf) => leaf.view.containerEl) + ); + + for (const [root, observer] of this.observers) { + if (currentRoots.has(root)) continue; + observer.disconnect(); + this.removeBadges(root); + this.observers.delete(root); + } + + for (const root of currentRoots) { + if (this.observers.has(root)) continue; + const observer = new MutationObserver(() => this.refresh()); + observer.observe(root, { childList: true, subtree: true }); + this.observers.set(root, observer); + } + + this.refresh(); + } + + private refreshFile(path: string): void { + if (!this.active) return; + for (const root of this.observers.keys()) { + root.querySelectorAll(FILE_ROW_SELECTOR).forEach((row) => { + if (row.dataset.path === path) this.decorateRow(row); + }); + } + } + + private decorateRow(row: HTMLElement): void { + if (!this.getEnabled()) { + this.removeBadge(row); + return; + } + + const path = row.dataset.path; + const folder = normalizePath(this.getTracesFolder().trim() || "traces").replace(/\/$/, ""); + if (!path || !path.startsWith(`${folder}/`)) { + this.removeBadge(row); + return; + } + + const file = this.app.vault.getAbstractFileByPath(path); + if (!(file instanceof TFile) || file.extension !== "md") { + this.removeBadge(row); + return; + } + + const metadata = readTraceMetadata(this.app, file); + if (!metadata?.id) { + this.removeBadge(row); + return; + } + + let badge = row.querySelector(`.${BADGE_CLASS}`); + if (!badge) { + badge = document.createElement("span"); + badge.classList.add(BADGE_CLASS); + const title = row.querySelector(".nav-file-title-content"); + if (title) title.before(badge); + else row.prepend(badge); + } + + const glyph = `[${tierGlyph(metadata.tier)}]`; + const label = tierLabel(metadata.tier); + badge.className = `${BADGE_CLASS} noema-file-tier-${metadata.tier}`; + if (badge.textContent !== glyph) badge.textContent = glyph; + badge.dataset.tier = metadata.tier; + badge.setAttribute("title", label); + badge.setAttribute("aria-label", label); + } + + private removeBadge(row: HTMLElement): void { + row.querySelector(`.${BADGE_CLASS}`)?.remove(); + } + + private removeBadges(root: HTMLElement): void { + root.querySelectorAll(`.${BADGE_CLASS}`).forEach((badge) => badge.remove()); + } +} diff --git a/plugins/obsidian/src/main.ts b/plugins/obsidian/src/main.ts index 95fd161..a303f69 100644 --- a/plugins/obsidian/src/main.ts +++ b/plugins/obsidian/src/main.ts @@ -7,6 +7,7 @@ import { CreateTraceModal } from "./create-modal"; import { ImmutableWarning } from "./immutable-warning"; import { openAppendModalFromActive } from "./append-modal"; import { SearchModal } from "./search-modal"; +import { FileExplorerTierBadges } from "./file-explorer-tiers"; const STATUS_PING_INTERVAL_MS = 30_000; @@ -44,6 +45,7 @@ export default class NoemaPlugin extends Plugin { // unreachable server (actionable: check the endpoint/network). private connState: ConnState = "disconnected"; private immutableWarning: ImmutableWarning | null = null; + private fileExplorerTierBadges: FileExplorerTierBadges | null = null; async onload(): Promise { await this.loadSettings(); @@ -119,6 +121,13 @@ export default class NoemaPlugin extends Plugin { }); this.immutableWarning = new ImmutableWarning(this.app, this); + this.fileExplorerTierBadges = new FileExplorerTierBadges( + this.app, + this, + () => this.settings.tracesFolder, + () => this.settings.showFileExplorerTierBadges + ); + this.fileExplorerTierBadges.start(); // Re-render the status bar AND immutable-warning banner when // the active file changes (tier glyph follows the user) or @@ -159,6 +168,7 @@ export default class NoemaPlugin extends Plugin { // Clean up any lingering banner DOM so a plugin reload during // active development doesn't leave orphan elements behind. this.immutableWarning?.removeAll(); + this.fileExplorerTierBadges?.stop(); } async loadSettings(): Promise { @@ -170,6 +180,10 @@ export default class NoemaPlugin extends Plugin { await this.saveData(this.settings); } + refreshFileExplorerTierBadges(): void { + this.fileExplorerTierBadges?.refresh(); + } + // refreshClient is called from the settings tab when the // endpoint or bearer key changes. We keep a single McpClient // instance so the JSON-RPC id counter stays monotonic across diff --git a/plugins/obsidian/src/mcp-client.ts b/plugins/obsidian/src/mcp-client.ts index ec3192a..6b6159b 100644 --- a/plugins/obsidian/src/mcp-client.ts +++ b/plugins/obsidian/src/mcp-client.ts @@ -343,7 +343,7 @@ export class McpClient { params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, - clientInfo: { name: "noema-obsidian", version: "0.3.0" }, + clientInfo: { name: "noema-obsidian", version: "0.4.0" }, }, }; const resp = await mcpFetch( diff --git a/plugins/obsidian/src/settings.ts b/plugins/obsidian/src/settings.ts index 8a781a2..5a84567 100644 --- a/plugins/obsidian/src/settings.ts +++ b/plugins/obsidian/src/settings.ts @@ -5,6 +5,7 @@ export interface NoemaSettings { endpoint: string; bearerKey: string; tracesFolder: string; + showFileExplorerTierBadges: boolean; defaultAuthor: string; searchMode: "lexical" | "semantic" | "hybrid"; } @@ -13,14 +14,16 @@ export interface NoemaSettings { // the plugin starts in "disconnected" state on a fresh install rather // than blindly trying to reach a localhost URL. tracesFolder defaults // to "traces" because that's the cortex layout convention; users with -// a non-standard layout (rare) can override. defaultAuthor is empty -// by default — the create-trace flow omits the author field entirely -// when the setting is empty, letting the cortex's own author logic -// decide what to record. +// a non-standard layout (rare) can override. File explorer badges are +// visible by default and can be hidden as an Obsidian display preference. +// defaultAuthor is empty by default — the create-trace flow omits the +// author field entirely when the setting is empty, letting the cortex's +// own author logic decide what to record. export const DEFAULT_SETTINGS: NoemaSettings = { endpoint: "", bearerKey: "", tracesFolder: "traces", + showFileExplorerTierBadges: true, defaultAuthor: "", searchMode: "hybrid", }; @@ -100,6 +103,20 @@ export class NoemaSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.tracesFolder = value.trim() || "traces"; await this.plugin.saveSettings(); + this.plugin.refreshFileExplorerTierBadges(); + }) + ); + + new Setting(containerEl) + .setName("Show tier badges in file explorer") + .setDesc("Show [s], [m], and [L] beside traces in the configured traces folder.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.showFileExplorerTierBadges) + .onChange(async (value) => { + this.plugin.settings.showFileExplorerTierBadges = value; + await this.plugin.saveSettings(); + this.plugin.refreshFileExplorerTierBadges(); }) ); diff --git a/plugins/obsidian/styles.css b/plugins/obsidian/styles.css index 491dd35..9e4e7d3 100644 --- a/plugins/obsidian/styles.css +++ b/plugins/obsidian/styles.css @@ -15,6 +15,33 @@ margin-right: 4px; } +/* ---- File explorer tier badges ---- */ + +.noema-file-tier-badge { + display: inline-block; + flex: 0 0 auto; + min-width: 2.25em; + margin-inline-end: 4px; + font-family: var(--font-monospace); + font-size: var(--font-ui-smaller); + font-weight: 600; + line-height: var(--line-height-tight, 1.2); + text-align: center; + color: var(--text-faint); +} + +.noema-file-tier-mid { + color: var(--text-muted); +} + +.noema-file-tier-long { + color: var(--text-warning, var(--text-accent)); +} + +.noema-file-tier-unknown { + color: var(--text-error, var(--text-muted)); +} + .noema-status-ok { color: var(--text-success, var(--text-normal)); }