wyrm-mcp 9.1.1 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BENCHMARKS.md CHANGED
@@ -61,16 +61,20 @@ Retrieval recall is the floor. Two more, all reproducible, all local (added 2026
61
61
 
62
62
  ### Negative learning — the firewall benchmark (the race we own)
63
63
 
64
- No public memory benchmark measures this. `bench/negative-learning.mjs` asks: when an agent is about to **repeat** a recorded mistake, does `wyrm_failure_check` block it and stay silent on novel actions? Deterministic, no LLM, no cloud.
64
+ No public memory benchmark measures this. `bench/negative-learning.mjs` asks: when an agent is about to **repeat** a recorded mistake, does `wyrm_failure_check` block it, and stay silent on novel actions? Deterministic, no LLM, no cloud. Measured on wyrm-mcp 9.1.2, 2026-09-12 (`bench/firewall-receipt.json`):
65
65
 
66
66
  | probe | result |
67
67
  |---|---|
68
68
  | same action re-attempted (the guard's real input) | **recall 100%** (16/16) |
69
+ | reworded restatement | **recall 100%** (16/16) |
69
70
  | novel actions | **precision 100%** · specificity 100% (0 false blocks of 16) |
70
- | reworded restatement | recall 12.5% — conjunctive-AND FTS is precision-first *by design* |
71
- | `checkVerdict` latency | p50 ~0.1 ms |
71
+ | confidence separation | 0.86 |
72
+ | staleness oracle | passes |
73
+ | `checkVerdict` latency | p50 2.7 ms · p95 6.1 ms |
72
74
 
73
- Wyrm blocks literal repeats deterministically with zero false alarms. The reworded tier is intentionally conservative; a stopword filter on the fuzzy probe (so incidental filler words can't break the conjunctive-AND) recovered the filler-broken matches — **6.3% 12.5% with precision held at 100%** (`tests/firewall-recall.test.ts` guards it). Going higher needs OR/threshold matching that would trade away that precision, so the firewall stays precision-first. Run: `node bench/negative-learning.mjs`.
75
+ The reworded tier used to sit at 12.5% (8.5.3), because the fuzzy probe was a conjunctive-AND FTS kept precision-first by design. Target normalization (canonical command matching, flag order and `=` form folded, positionals kept in order) closed that gap without giving up a single false block. Run: `node bench/negative-learning.mjs`.
76
+
77
+ The end-to-end version of the same question, a blind agent working real tickets with and without the firewall in front of its actions, is the agent A/B: repeated-failure rate 38.9% (14/36) without the firewall to 8.3% (3/36) with it, 11 blocks, relative reduction 78.6% (bootstrap 95% CI 45.5% to 100%, n=36 per arm, 3 repetitions). Method, per-scenario table and the stated limits: `docs/firewall-receipt.md`.
74
78
 
75
79
  ### Embedding-model sweep
76
80
 
package/README.md CHANGED
@@ -50,6 +50,14 @@ capture → store durable facts, lessons, and tasks as you go
50
50
 
51
51
  Every number Wyrm publishes comes from a benchmark committed to the source, reproducible on your own data. The negative-learning firewall and the recall lift are the two that matter most, and both are covered below.
52
52
 
53
+ ### The Firewall Receipt
54
+
55
+ The receipt is the measured proof of one claim: when a failure is on record, the firewall blocks the repeat, stays silent on novel actions, and drops the block once the anchored source drifts, deterministically and with no model in the loop. On wyrm-mcp 9.1.2, measured 2026-09-12, the deterministic bench scores recall 100% (32/32) on same-action and reworded repeats with precision 100% (0 false blocks of 16), and in the blind agent A/B the repeated-failure rate falls from 38.9% (14/36) to 8.3% (3/36), a 78.6% relative reduction with a bootstrap 95% CI of 45.5% to 100% (n=36 per arm). The method, the per-scenario table and the two limits the numbers carry are in [docs/firewall-receipt.md](docs/firewall-receipt.md); the deterministic receipt is regenerated on every push in CI as the artifact `firewall-receipt-node22.x` ([workflow](https://github.com/Ghosts-Protocol-Pvt-Ltd/wyrm-mcp/actions/workflows/ci.yml)).
56
+
57
+ The failure firewall in twenty seconds: a failure is on record, the agent proposes the same deploy, and the PreToolUse hook refuses it with the recorded reason before the command runs. A variant with an added flag gets an advisory, not a block, which is the honest edge of the deterministic match. Every line in the clip is real hook output.
58
+
59
+ ![The firewall blocking a repeated deploy](assets/firewall-receipt-20s.gif)
60
+
53
61
  ## Why Wyrm
54
62
 
55
63
  ### It remembers what failed, not just what worked
@@ -78,7 +86,7 @@ Every artifact is tagged with where it came from: you, an agent, an import, or a
78
86
 
79
87
  ## NVIDIA NIM retrieval (optional)
80
88
 
81
- Local vectors are on by default. NIM is the further step up, for embeddings and reranking, when accuracy is worth a hosted call. On a retrieval benchmark committed in the repo, recall@1 moved from 33% on the local baseline to 47% with NIM embeddings and 52% with NIM reranking added. It is an explicit opt-in, off by default, and the egress is disclosed on every call.
89
+ Local vectors are on by default. NIM is the further step up, for embeddings and reranking, when accuracy is worth a hosted call. On the LoCoMo retrieval benchmark committed in the repo (2 conversations, k=10, measured September 2026), the default NIM embedding model `nvidia/nemotron-3-embed-1b` reached recall@1 39.9% and recall@10 76.7%; adding the NIM reranker (`nvidia/llama-nemotron-rerank-vl-1b-v2`) raised recall@1 to 55.5% across the same 301 questions, at about one extra second per query. Figures published here earlier were measured on models NVIDIA retired on 25 August 2026, and have been withdrawn. It is an explicit opt-in, off by default, and the egress is disclosed on every call.
82
90
 
83
91
  The guided path is `wyrm upgrade`: a free API key, masked key entry, a live call to validate the key before anything is written, and an optional one-time reindex of existing memories on the new tier. To configure it by hand instead:
84
92
 
@@ -1,21 +1,21 @@
1
- import{existsSync as d,readFileSync as g,writeFileSync as p,mkdirSync as P,copyFileSync as X,chmodSync as C}from"fs";import{homedir as h,platform as b}from"os";import{join as a,dirname as w,resolve as ee}from"path";import{fileURLToPath as _}from"url";import{spawnSync as v}from"child_process";function te(){const e=h(),t=b(),o=t==="darwin"?a(e,"Library","Application Support","Code","User"):t==="win32"?a(e,"AppData","Roaming","Code","User"):a(e,".config","Code","User"),n=t==="darwin"?a(e,"Library","Application Support","Code - Insiders","User"):t==="win32"?a(e,"AppData","Roaming","Code - Insiders","User"):a(e,".config","Code - Insiders","User"),s=t==="darwin"?a(e,"Library","Application Support","Claude"):t==="win32"?a(e,"AppData","Roaming","Claude"):a(e,".config","claude"),r=t==="darwin"?a(e,".cursor"):t==="win32"?a(e,".cursor"):a(e,".cursor"),i=t==="darwin"?a(e,".codeium","windsurf"):t==="win32"?a(e,".codeium","windsurf"):a(e,".codeium","windsurf"),u=t==="darwin"?a(e,".config","zed"):t==="win32"?a(e,"AppData","Roaming","Zed"):a(e,".config","zed"),c=a(e,".continue");return[{id:"vscode-copilot",name:"VS Code (Copilot)",icon:"\u{1F4BB}",configPath:a(o,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"vscode-insiders",name:"VS Code Insiders",icon:"\u{1F7E2}",configPath:a(n,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"claude-desktop",name:"Claude Desktop",icon:"\u{1F916}",configPath:a(s,"claude_desktop_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"claude-code",name:"Claude Code",icon:"\u2733\uFE0F",configPath:a(e,".claude.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"cursor",name:"Cursor",icon:"\u{1F4D0}",configPath:a(r,"mcp.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"windsurf",name:"Windsurf",icon:"\u{1F3C4}",configPath:a(i,"mcp_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"zed",name:"Zed",icon:"\u26A1",configPath:a(u,"settings.json"),configFormat:"zed",mcpKey:"context_servers",detected:!1,configured:!1},{id:"continue",name:"Continue",icon:"\u{1F504}",configPath:a(c,"config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1}]}function oe(e){try{const t=b()==="win32"?"where":"which",o=v(t,[e],{encoding:"utf-8",timeout:5e3});return o.status===0&&!!o.stdout&&o.stdout.trim().length>0}catch{return!1}}function A(){const e=te();for(const t of e){if(t.id==="claude-code")t.detected=d(t.configPath)||oe("claude");else{const o=w(t.configPath);t.detected=d(o)}if(t.detected&&d(t.configPath))try{const o=g(t.configPath,"utf-8"),n=E(o);t.configured=re(n,t)}catch{t.configured=!1}t.detected&&(t.version=ne(t))}return e}function ne(e){try{switch(e.id){case"vscode-copilot":case"vscode-insiders":{const t=e.id==="vscode-insiders"?"code-insiders":"code",o=v(t,["--version"],{encoding:"utf-8",timeout:5e3});if(o.stdout)return o.stdout.split(`
2
- `)[0];break}case"cursor":{const t=v("cursor",["--version"],{encoding:"utf-8",timeout:5e3});if(t.stdout)return t.stdout.split(`
3
- `)[0];break}case"claude-code":{const t=v("claude",["--version"],{encoding:"utf-8",timeout:5e3});if(t.stdout)return t.stdout.trim().split(`
4
- `)[0];break}}}catch{}}function re(e,t){switch(t.configFormat){case"vscode":return e.mcp?.servers?.wyrm!==void 0;case"mcp-json":return e[t.mcpKey]?.wyrm!==void 0;case"zed":return e[t.mcpKey]?.wyrm!==void 0;default:return!1}}function M(){try{const n=v("which",["wyrm-mcp"],{encoding:"utf-8",timeout:5e3});if(n.stdout?.trim())return n.stdout.trim()}catch{}try{const s=v("npm",["root","-g"],{encoding:"utf-8",timeout:5e3}).stdout?.trim();if(s){const r=a(s,"wyrm-mcp","dist","index.js");if(d(r))return r}}catch{}const e=ee(__dirname,"index.js");if(d(e))return e;const t=a(h(),".wyrm","node_modules","wyrm-mcp","dist","index.js");if(d(t))return t;const o=[a(h(),"Git Projects","Wyrm","packages","mcp-server","dist","index.js"),a(h(),"projects","Wyrm","packages","mcp-server","dist","index.js"),a(h(),"dev","Wyrm","packages","mcp-server","dist","index.js")];for(const n of o)if(d(n))return n;return"wyrm-mcp"}function D(){return a(h(),".wyrm","wyrm.db")}function Ae(e="mcp-json",t){const o=t?.serverPath??M(),n=t?.dbPath??D(),s=o==="wyrm-mcp",r={command:s?"npx":"node",args:s?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:n}};switch(e){case"stdio":return r;case"vscode":return{mcp:{servers:{wyrm:r}}};default:return{mcpServers:{wyrm:r}}}}function se(e,t){const o=t.serverPath,n=o==="wyrm-mcp",s=o.endsWith(".js");switch(e.configFormat){case"vscode":return{command:n?"npx":"node",args:n?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:t.dbPath}};case"mcp-json":return{command:n?"npx":"node",args:n?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:t.dbPath}};case"zed":return{command:n?"npx":"node",args:n?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:t.dbPath}};default:return{}}}function H(e,t){if(!e.detected)return{client:e,action:"skipped",message:`${e.name} not detected`};try{const o=w(e.configPath);d(o)||P(o,{recursive:!0});let n={},s=!1;if(d(e.configPath))try{const c=g(e.configPath,"utf-8");n=E(c),s=!0}catch{n={}}let r;if(s){r=`${e.configPath}.wyrm-backup`,p(r,g(e.configPath),{mode:384});try{C(r,384)}catch{}}const i=se(e,t);switch(e.configFormat){case"vscode":{n.mcp||(n.mcp={});const c=n.mcp;c.servers||(c.servers={});const f=c.servers;f.wyrm=i;break}case"mcp-json":{n[e.mcpKey]||(n[e.mcpKey]={});const c=n[e.mcpKey];c.wyrm=i;break}case"zed":{n[e.mcpKey]||(n[e.mcpKey]={});const c=n[e.mcpKey];c.wyrm=i;break}}p(e.configPath,JSON.stringify(n,null,2)+`
5
- `,"utf-8");const u=e.configured?"updated":"configured";return e.configured=!0,{client:e,action:u,message:`${e.name} ${u==="configured"?"configured":"updated"} successfully`,backup:r}}catch(o){return{client:e,action:"failed",message:`Failed to configure ${e.name}: ${o}`}}}function je(e,t){try{if(!d(e.configPath))return{client:e,action:"skipped",message:"No config file"};const o=g(e.configPath,"utf-8");let n;try{n=E(o)}catch{return{client:e,action:"failed",message:"Config file is not valid JSON; refusing to touch it"}}const r=(e.configFormat==="vscode"?n.mcp?.servers:n[e.mcpKey])?.wyrm;if(!r)return{client:e,action:"skipped",message:"Wyrm not configured in this client"};const i=`${e.configPath}.wyrm-backup`;p(i,o,{encoding:"utf-8",mode:384});try{C(i,384)}catch{}return r.env={...r.env,...t},p(e.configPath,JSON.stringify(n,null,2)+`
6
- `,"utf-8"),{client:e,action:"updated",message:`env: ${Object.keys(t).join(", ")}`,backup:i}}catch(o){return{client:e,action:"failed",message:String(o)}}}function ie(e){if(!e.detected||!e.configured)return{client:e,action:"skipped",message:`${e.name} not configured`};try{if(!d(e.configPath))return{client:e,action:"skipped",message:`${e.name} config not found`};const t=g(e.configPath,"utf-8"),o=E(t),n=`${e.configPath}.wyrm-backup`;p(n,t,{mode:384});try{C(n,384)}catch{}switch(e.configFormat){case"vscode":{const r=o.mcp?.servers;r&&delete r.wyrm;break}case"mcp-json":case"zed":{const s=o[e.mcpKey];s&&delete s.wyrm;break}}return p(e.configPath,JSON.stringify(o,null,2)+`
7
- `,"utf-8"),e.configured=!1,{client:e,action:"configured",message:`Removed Wyrm from ${e.name}`,backup:n}}catch(t){return{client:e,action:"failed",message:`Failed to remove from ${e.name}: ${t}`}}}function O(){return{id:"claude-code-hooks",name:"Claude Code (auto-memory)",icon:"\u{1FA9D}",configPath:a(h(),".claude","settings.json"),configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}const J=[{script:"wyrm-session-capture.mjs",events:["SessionEnd","PreCompact"]},{script:"wyrm-session-rehydrate.mjs",events:["SessionStart"]},{script:"wyrm-session-prune.mjs",events:["SessionEnd"]},{script:"wyrm-tool-call-trace.mjs",events:["PostToolUse"]},{script:"wyrm-run-auto.mjs",events:["SessionStart",{event:"PreToolUse",matcher:"Task"},"SessionEnd"]},{script:"wyrm-citation-audit.mjs",events:["Stop"]}];function ce(){const e=a(h(),".claude");if(!d(e))return null;const t=O();try{const o=a(e,"hooks");d(o)||P(o,{recursive:!0});const n=w(_(import.meta.url)),s=a(n,"..","scripts","hooks"),r=t.configPath;let i={};if(d(r))try{i=JSON.parse(g(r,"utf-8"))}catch{i={}}const u=i.hooks??{};let c=!1;const f=[];for(const l of J){const y=a(s,l.script);if(!d(y)){f.push(l.script);continue}const T=a(o,l.script);X(y,T);try{C(T,493)}catch{}const W=`node ${T}`;for(const S of l.events){const N=typeof S=="string"?S:S.event,L=typeof S=="string"?void 0:S.matcher,x=Array.isArray(u[N])?u[N]:[];if(!x.some(j=>Array.isArray(j.hooks)&&j.hooks.some(Z=>Z.command===W))){const j=L?{matcher:L,hooks:[{type:"command",command:W,timeout:30}]}:{hooks:[{type:"command",command:W,timeout:30}]};x.push(j),c=!0}u[N]=x}}i.hooks=u,p(r,JSON.stringify(i,null,2)+`
8
- `,"utf-8"),t.configured=!0;const m=f.length?` (missing from package: ${f.join(", ")})`:"";return{client:t,action:c?"configured":"skipped",message:(c?"auto-memory hooks installed":"already installed")+m}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function Ce(){const e=a(h(),".claude");if(!d(e))return null;const t=O();try{const o=w(_(import.meta.url)),n=`node ${a(o,"wyrm-statusline.js")}`,s=t.configPath;let r={};if(d(s))try{r=JSON.parse(g(s,"utf-8"))}catch{r={}}const i=r.statusLine;if(i?.command&&!/wyrm-statusline/.test(i.command))return{client:t,action:"skipped",message:`a non-Wyrm statusLine is already set (${i.command}); not overwriting`};const u=i?.command===n;return r.statusLine={type:"command",command:n,padding:0},p(s,JSON.stringify(r,null,2)+`
9
- `,"utf-8"),{client:t,action:u?"skipped":"configured",message:u?"buddy statusline already installed":"Wyrm buddy statusline installed \u2014 persistent in the Claude Code TUI"}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function _e(){const e=a(h(),".claude","settings.json");if(!d(e))return null;const t=O();try{const o=JSON.parse(g(e,"utf-8")),n=o.statusLine;return n?.command&&/wyrm-statusline/.test(n.command)?(delete o.statusLine,p(e,JSON.stringify(o,null,2)+`
10
- `,"utf-8"),{client:t,action:"configured",message:"Wyrm statusline removed"}):{client:t,action:"skipped",message:"no Wyrm statusline set"}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function ae(){const e=a(h(),".claude","settings.json");if(!d(e))return null;const t=O();try{const o=JSON.parse(g(e,"utf-8")),n=a(h(),".claude","hooks"),s=new Set(J.map(u=>`node ${a(n,u.script)}`)),r=new Set(J.flatMap(u=>u.events.map(c=>typeof c=="string"?c:c.event))),i=o.hooks;if(i){for(const u of r)Array.isArray(i[u])&&(i[u]=i[u].map(c=>({...c,hooks:Array.isArray(c.hooks)?c.hooks.filter(f=>!s.has(f.command??"")):c.hooks})).filter(c=>!Array.isArray(c.hooks)||c.hooks.length>0),i[u].length===0&&delete i[u]);Object.keys(i).length===0&&delete o.hooks}return p(e,JSON.stringify(o,null,2)+`
11
- `,"utf-8"),{client:t,action:"configured",message:"auto-memory hooks removed"}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}const I=/wyrm-guard/,G=5,ue=["Bash","Edit|Write|MultiEdit|NotebookEdit"];function k(e){return e.settingsPath??process.env.CLAUDE_SETTINGS??a(h(),".claude","settings.json")}function R(e){return{id:"claude-code-guard",name:"Claude Code (failure firewall)",icon:"\u{1F6E1}\uFE0F",configPath:e,configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}function $(e){return b()==="win32"?`"${e.replace(/"/g,'""')}"`:`'${e.replace(/'/g,"'\\''")}'`}function de(){const e=w(_(import.meta.url)),t=a(e,"wyrm-guard.js");if(d(t))return`node ${$(t)}`;try{const o=b()==="win32"?"where":"which",s=v(o,["wyrm-guard"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
12
- `)[0]?.trim();if(s)return $(s)}catch{}return null}function U(e,t){const o=e.hooks;if(!o||typeof o!="object"||Array.isArray(o))return!1;let n=!1;for(const s of Object.keys(o)){const r=o[s];if(!Array.isArray(r))continue;let i=!1;const u=[];for(const c of r){const f=c;if(!f||typeof f!="object"||!Array.isArray(f.hooks)){u.push(c);continue}const m=f.hooks.filter(l=>!t.test(l?.command??""));if(m.length===f.hooks.length){u.push(c);continue}n=!0,i=!0,m.length>0&&u.push({...f,hooks:m})}i&&(u.length===0?delete o[s]:o[s]=u)}return n&&Object.keys(o).length===0&&delete e.hooks,n}function z(e){return U(e,I)}function fe(e={}){if(!!!(e.settingsPath??process.env.CLAUDE_SETTINGS)&&!d(a(h(),".claude")))return{client:R(k(e)),action:"skipped",message:"failure firewall: ~/.claude not found on this machine (needs the Claude Code CLI). Not armed. Install Claude Code then re-run `wyrm guard`"};const o=k(e),n=R(o);try{const s=e.guardCommand??de();if(!s)return{client:n,action:"skipped",message:"wyrm-guard bin not found (npm i -g wyrm-mcp, or build dist/) \u2014 firewall hooks not installed"};P(w(o),{recursive:!0});let r={};if(d(o))try{const y=JSON.parse(g(o,"utf-8"));if(typeof y!="object"||y===null||Array.isArray(y))throw new Error("not an object");r=y}catch{return{client:n,action:"failed",message:`${o} is not valid JSON \u2014 refusing to overwrite it (fix or remove the file, then re-run)`}}const i=r.hooks;if(i!==void 0&&(typeof i!="object"||i===null||Array.isArray(i)))return{client:n,action:"failed",message:`${o} "hooks" has an unexpected shape (not a plain object) \u2014 refusing to modify (fix the file, then re-run)`};const u=i?.PreToolUse;if(u!==void 0&&!Array.isArray(u))return{client:n,action:"failed",message:`${o} hooks.PreToolUse has an unexpected shape (not an array) \u2014 refusing to modify (fix the file, then re-run)`};const c=JSON.stringify(r);z(r);const f=r.hooks??{},m=Array.isArray(f.PreToolUse)?f.PreToolUse:[];for(const y of ue)m.push({matcher:y,hooks:[{type:"command",command:s,timeout:G}]});f.PreToolUse=m,r.hooks=f;const l=JSON.stringify(r)!==c;return l&&p(o,JSON.stringify(r,null,2)+`
13
- `,"utf-8"),n.configured=!0,{client:n,action:l?"configured":"skipped",message:l?"failure-firewall hooks installed (PreToolUse: Bash + Edit|Write|MultiEdit|NotebookEdit)":"failure-firewall hooks already installed"}}catch(s){return{client:n,action:"failed",message:s instanceof Error?s.message:String(s)}}}function me(e={}){const t=k(e);if(!d(t))return null;const o=R(t);try{let n;try{const r=JSON.parse(g(t,"utf-8"));if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("not an object");n=r}catch{return{client:o,action:"failed",message:`${t} is not valid JSON \u2014 not touching it`}}return z(n)?(p(t,JSON.stringify(n,null,2)+`
14
- `,"utf-8"),{client:o,action:"configured",message:"failure-firewall hooks removed"}):{client:o,action:"skipped",message:"no wyrm-guard hooks installed"}}catch(n){return{client:o,action:"failed",message:n instanceof Error?n.message:String(n)}}}function Oe(e={}){const t=k(e),o=[];try{const s=JSON.parse(g(t,"utf-8")).hooks??{};for(const r of Object.values(s))if(Array.isArray(r))for(const i of r)for(const u of i?.hooks??[]){const c=u?.command;c&&I.test(c)&&o.push(c)}}catch{}return{settingsPath:t,installed:o.length>0,commands:o}}const K=/wyrm-memo/,le=[{event:"PreToolUse",matcher:"Read"},{event:"PreToolUse",matcher:"Bash"},{event:"PostToolUse",matcher:"Read"},{event:"PostToolUse",matcher:"Bash"}];function Y(e){return{id:"claude-code-memo",name:"Claude Code (token firewall)",icon:"",configPath:e,configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}function pe(){const e=w(_(import.meta.url)),t=a(e,"wyrm-memo.js");if(d(t))return`node ${$(t)}`;try{const o=b()==="win32"?"where":"which",s=v(o,["wyrm-memo"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
15
- `)[0]?.trim();if(s)return $(s)}catch{}return null}function he(e={}){const t=k(e),o=!!(e.settingsPath??process.env.CLAUDE_SETTINGS),n=Y(t);if(!o&&!d(a(h(),".claude")))return{client:n,action:"skipped",message:"token firewall: ~/.claude not found on this machine (needs the Claude Code CLI). Not armed. Install Claude Code then re-run `wyrm memo`"};try{const s=e.memoCommand??pe();if(!s)return{client:n,action:"skipped",message:"wyrm-memo bin not found (npm i -g wyrm-mcp, or build dist/). Token-firewall hooks not installed"};P(w(t),{recursive:!0});let r={};if(d(t))try{const m=JSON.parse(g(t,"utf-8"));if(typeof m!="object"||m===null||Array.isArray(m))throw new Error("not an object");r=m}catch{return{client:n,action:"failed",message:`${t} is not valid JSON \u2014 refusing to overwrite it (fix or remove the file, then re-run)`}}const i=r.hooks;if(i!==void 0&&(typeof i!="object"||i===null||Array.isArray(i)))return{client:n,action:"failed",message:`${t} "hooks" has an unexpected shape (not a plain object) \u2014 refusing to modify`};for(const m of["PreToolUse","PostToolUse"]){const l=i?.[m];if(l!==void 0&&!Array.isArray(l))return{client:n,action:"failed",message:`${t} hooks.${m} has an unexpected shape (not an array) \u2014 refusing to modify`}}const u=JSON.stringify(r);U(r,K);const c=r.hooks??{};for(const{event:m,matcher:l}of le){const y=Array.isArray(c[m])?c[m]:[];y.push({matcher:l,hooks:[{type:"command",command:s,timeout:G}]}),c[m]=y}r.hooks=c;const f=JSON.stringify(r)!==u;return f&&p(t,JSON.stringify(r,null,2)+`
16
- `,"utf-8"),n.configured=!0,{client:n,action:f?"configured":"skipped",message:f?"token-firewall hooks installed (PreToolUse + PostToolUse: Read, Bash). Mode: observe (set WYRM_MEMO_MODE=warn to hand maps back)":"token-firewall hooks already installed"}}catch(s){return{client:n,action:"failed",message:s instanceof Error?s.message:String(s)}}}function $e(e={}){const t=k(e);if(!d(t))return null;const o=Y(t);try{let n;try{const r=JSON.parse(g(t,"utf-8"));if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("not an object");n=r}catch{return{client:o,action:"failed",message:`${t} is not valid JSON \u2014 not touching it`}}return U(n,K)?(p(t,JSON.stringify(n,null,2)+`
17
- `,"utf-8"),{client:o,action:"configured",message:"token-firewall hooks removed"}):{client:o,action:"skipped",message:"no wyrm-memo hooks installed"}}catch(n){return{client:o,action:"failed",message:n instanceof Error?n.message:String(n)}}}function Ee(e={}){const t=k(e),o=[];try{const s=JSON.parse(g(t,"utf-8")).hooks??{};for(const r of Object.values(s))if(Array.isArray(r))for(const i of r)for(const u of i?.hooks??[]){const c=u?.command;c&&K.test(c)&&o.push(c)}}catch{}return{settingsPath:t,installed:o.length>0,commands:o}}function ge(e){const t={serverPath:e?.serverPath||M(),dbPath:e?.dbPath||D(),httpPort:e?.httpPort},o=a(h(),".wyrm");d(o)||P(o,{recursive:!0}),q(t);const n=A(),s=[];for(const c of n)s.push(H(c,t));const r=ce();r&&s.push(r);const i=fe();i&&s.push(i);const u=he();return u&&s.push(u),s}function Te(){const e=A(),t=[];for(const s of e)t.push(ie(s));const o=ae();o&&t.push(o);const n=me();return n&&t.push(n),t}function ye(e,t){const o={serverPath:t?.serverPath||M(),dbPath:t?.dbPath||D(),httpPort:t?.httpPort};q(o);const n=A(),s=[];for(const r of n)e.includes(r.id)&&s.push(H(r,o));return s}function q(e){const t=a(h(),".wyrm","wyrm-config.json"),o=A().filter(s=>s.configured).map(s=>s.id),n={version:"3.0.0",serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort,configuredClients:o,lastSetup:new Date().toISOString(),autoUpdate:!0};p(t,JSON.stringify(n,null,2)+`
18
- `,"utf-8")}function V(){const e=a(h(),".wyrm","wyrm-config.json");if(!d(e))return null;try{return JSON.parse(g(e,"utf-8"))}catch{return null}}function We(){const e=V();return e?ye(e.configuredClients,{serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort}):ge()}const Q="<!-- wyrm:start -->",F="<!-- wyrm:end -->",B=`${Q}
1
+ import{existsSync as f,readFileSync as y,writeFileSync as p,mkdirSync as P,copyFileSync as X,chmodSync as C}from"fs";import{homedir as h,platform as b}from"os";import{join as a,dirname as v,resolve as ee}from"path";import{fileURLToPath as _}from"url";import{spawnSync as w}from"child_process";function te(){const e=h(),t=b(),o=t==="darwin"?a(e,"Library","Application Support","Code","User"):t==="win32"?a(e,"AppData","Roaming","Code","User"):a(e,".config","Code","User"),n=t==="darwin"?a(e,"Library","Application Support","Code - Insiders","User"):t==="win32"?a(e,"AppData","Roaming","Code - Insiders","User"):a(e,".config","Code - Insiders","User"),s=t==="darwin"?a(e,"Library","Application Support","Claude"):t==="win32"?a(e,"AppData","Roaming","Claude"):a(e,".config","claude"),r=t==="darwin"?a(e,".cursor"):t==="win32"?a(e,".cursor"):a(e,".cursor"),i=t==="darwin"?a(e,".codeium","windsurf"):t==="win32"?a(e,".codeium","windsurf"):a(e,".codeium","windsurf"),c=t==="darwin"?a(e,".config","zed"):t==="win32"?a(e,"AppData","Roaming","Zed"):a(e,".config","zed"),u=a(e,".continue");return[{id:"vscode-copilot",name:"VS Code (Copilot)",icon:"\u{1F4BB}",configPath:a(o,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"vscode-insiders",name:"VS Code Insiders",icon:"\u{1F7E2}",configPath:a(n,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"claude-desktop",name:"Claude Desktop",icon:"\u{1F916}",configPath:a(s,"claude_desktop_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"claude-code",name:"Claude Code",icon:"\u2733\uFE0F",configPath:a(e,".claude.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"cursor",name:"Cursor",icon:"\u{1F4D0}",configPath:a(r,"mcp.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"windsurf",name:"Windsurf",icon:"\u{1F3C4}",configPath:a(i,"mcp_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"zed",name:"Zed",icon:"\u26A1",configPath:a(c,"settings.json"),configFormat:"zed",mcpKey:"context_servers",detected:!1,configured:!1},{id:"continue",name:"Continue",icon:"\u{1F504}",configPath:a(u,"config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1}]}function oe(e){try{const t=b()==="win32"?"where":"which",o=w(t,[e],{encoding:"utf-8",timeout:5e3});return o.status===0&&!!o.stdout&&o.stdout.trim().length>0}catch{return!1}}function A(){const e=te();for(const t of e){if(t.id==="claude-code")t.detected=f(t.configPath)||oe("claude");else{const o=v(t.configPath);t.detected=f(o)}if(t.detected&&f(t.configPath))try{const o=y(t.configPath,"utf-8"),n=$(o);t.configured=re(n,t)}catch{t.configured=!1}t.detected&&(t.version=ne(t))}return e}function ne(e){try{switch(e.id){case"vscode-copilot":case"vscode-insiders":{const t=e.id==="vscode-insiders"?"code-insiders":"code",o=w(t,["--version"],{encoding:"utf-8",timeout:5e3});if(o.stdout)return o.stdout.split(`
2
+ `)[0];break}case"cursor":{const t=w("cursor",["--version"],{encoding:"utf-8",timeout:5e3});if(t.stdout)return t.stdout.split(`
3
+ `)[0];break}case"claude-code":{const t=w("claude",["--version"],{encoding:"utf-8",timeout:5e3});if(t.stdout)return t.stdout.trim().split(`
4
+ `)[0];break}}}catch{}}function re(e,t){switch(t.configFormat){case"vscode":return e.mcp?.servers?.wyrm!==void 0;case"mcp-json":return e[t.mcpKey]?.wyrm!==void 0;case"zed":return e[t.mcpKey]?.wyrm!==void 0;default:return!1}}function M(){try{const n=w("which",["wyrm-mcp"],{encoding:"utf-8",timeout:5e3});if(n.stdout?.trim())return n.stdout.trim()}catch{}try{const s=w("npm",["root","-g"],{encoding:"utf-8",timeout:5e3}).stdout?.trim();if(s){const r=a(s,"wyrm-mcp","dist","index.js");if(f(r))return r}}catch{}const e=ee(__dirname,"index.js");if(f(e))return e;const t=a(h(),".wyrm","node_modules","wyrm-mcp","dist","index.js");if(f(t))return t;const o=[a(h(),"Git Projects","Wyrm","packages","mcp-server","dist","index.js"),a(h(),"projects","Wyrm","packages","mcp-server","dist","index.js"),a(h(),"dev","Wyrm","packages","mcp-server","dist","index.js")];for(const n of o)if(f(n))return n;return"wyrm-mcp"}function D(){return a(h(),".wyrm","wyrm.db")}function je(e="mcp-json",t){const o=t?.serverPath??M(),n=t?.dbPath??D(),s=o==="wyrm-mcp",r={command:s?"npx":"node",args:s?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:n}};switch(e){case"stdio":return r;case"vscode":return{mcp:{servers:{wyrm:r}}};default:return{mcpServers:{wyrm:r}}}}function se(e,t){const o=t.serverPath,n=o==="wyrm-mcp",s=o.endsWith(".js");switch(e.configFormat){case"vscode":return{command:n?"npx":"node",args:n?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:t.dbPath}};case"mcp-json":return{command:n?"npx":"node",args:n?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:t.dbPath}};case"zed":return{command:n?"npx":"node",args:n?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:t.dbPath}};default:return{}}}function ie(e,t){if(!e||typeof e!="object"||Array.isArray(e))return{entry:t,preservedLauncher:!1};const o=e,n=o.env&&typeof o.env=="object"&&!Array.isArray(o.env)?o.env:{},s=t.env&&typeof t.env=="object"&&!Array.isArray(t.env)?t.env:{};if(o.command===t.command&&JSON.stringify(o.args??null)===JSON.stringify(t.args??null))return{entry:{...t,env:{...s,...n}},preservedLauncher:!1};const i={...n};for(const[c,u]of Object.entries(s))c in i||(i[c]=u);return{entry:{...o,env:i},preservedLauncher:!0}}function H(e,t){if(!e.detected)return{client:e,action:"skipped",message:`${e.name} not detected`};try{const o=v(e.configPath);f(o)||P(o,{recursive:!0});let n={},s=!1;if(f(e.configPath))try{const d=y(e.configPath,"utf-8");n=$(d),s=!0}catch{n={}}let r;if(s){r=`${e.configPath}.wyrm-backup`,p(r,y(e.configPath),{mode:384});try{C(r,384)}catch{}}const i=se(e,t);let c=!1;const u=(d,l)=>{const g=ie(d.wyrm,l);return d.wyrm=g.entry,g.preservedLauncher};switch(e.configFormat){case"vscode":{n.mcp||(n.mcp={});const d=n.mcp;d.servers||(d.servers={});const l=d.servers;c=u(l,i)||c;break}case"mcp-json":{n[e.mcpKey]||(n[e.mcpKey]={});const d=n[e.mcpKey];c=u(d,i)||c;break}case"zed":{n[e.mcpKey]||(n[e.mcpKey]={});const d=n[e.mcpKey];c=u(d,i)||c;break}}p(e.configPath,JSON.stringify(n,null,2)+`
5
+ `,"utf-8");const m=e.configured?"updated":"configured";return e.configured=!0,{client:e,action:m,message:`${e.name} ${m==="configured"?"configured":"updated"} successfully`+(c?" (kept your custom launch command; only missing env was added)":""),backup:r}}catch(o){return{client:e,action:"failed",message:`Failed to configure ${e.name}: ${o}`}}}function Ce(e,t){try{if(!f(e.configPath))return{client:e,action:"skipped",message:"No config file"};const o=y(e.configPath,"utf-8");let n;try{n=$(o)}catch{return{client:e,action:"failed",message:"Config file is not valid JSON; refusing to touch it"}}const r=(e.configFormat==="vscode"?n.mcp?.servers:n[e.mcpKey])?.wyrm;if(!r)return{client:e,action:"skipped",message:"Wyrm not configured in this client"};const i=`${e.configPath}.wyrm-backup`;p(i,o,{encoding:"utf-8",mode:384});try{C(i,384)}catch{}return r.env={...r.env,...t},p(e.configPath,JSON.stringify(n,null,2)+`
6
+ `,"utf-8"),{client:e,action:"updated",message:`env: ${Object.keys(t).join(", ")}`,backup:i}}catch(o){return{client:e,action:"failed",message:String(o)}}}function ce(e){if(!e.detected||!e.configured)return{client:e,action:"skipped",message:`${e.name} not configured`};try{if(!f(e.configPath))return{client:e,action:"skipped",message:`${e.name} config not found`};const t=y(e.configPath,"utf-8"),o=$(t),n=`${e.configPath}.wyrm-backup`;p(n,t,{mode:384});try{C(n,384)}catch{}switch(e.configFormat){case"vscode":{const r=o.mcp?.servers;r&&delete r.wyrm;break}case"mcp-json":case"zed":{const s=o[e.mcpKey];s&&delete s.wyrm;break}}return p(e.configPath,JSON.stringify(o,null,2)+`
7
+ `,"utf-8"),e.configured=!1,{client:e,action:"configured",message:`Removed Wyrm from ${e.name}`,backup:n}}catch(t){return{client:e,action:"failed",message:`Failed to remove from ${e.name}: ${t}`}}}function O(){return{id:"claude-code-hooks",name:"Claude Code (auto-memory)",icon:"\u{1FA9D}",configPath:a(h(),".claude","settings.json"),configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}const J=[{script:"wyrm-session-capture.mjs",events:["SessionEnd","PreCompact"]},{script:"wyrm-session-rehydrate.mjs",events:["SessionStart"]},{script:"wyrm-session-prune.mjs",events:["SessionEnd"]},{script:"wyrm-tool-call-trace.mjs",events:["PostToolUse"]},{script:"wyrm-run-auto.mjs",events:["SessionStart",{event:"PreToolUse",matcher:"Task"},"SessionEnd"]},{script:"wyrm-citation-audit.mjs",events:["Stop"]}];function ae(){const e=a(h(),".claude");if(!f(e))return null;const t=O();try{const o=a(e,"hooks");f(o)||P(o,{recursive:!0});const n=v(_(import.meta.url)),s=a(n,"..","scripts","hooks"),r=t.configPath;let i={};if(f(r))try{i=JSON.parse(y(r,"utf-8"))}catch{i={}}const c=i.hooks??{};let u=!1;const m=[];for(const l of J){const g=a(s,l.script);if(!f(g)){m.push(l.script);continue}const N=a(o,l.script);X(g,N);try{C(N,493)}catch{}const T=`node ${N}`;for(const S of l.events){const W=typeof S=="string"?S:S.event,B=typeof S=="string"?void 0:S.matcher,x=Array.isArray(c[W])?c[W]:[];if(!x.some(j=>Array.isArray(j.hooks)&&j.hooks.some(Z=>Z.command===T))){const j=B?{matcher:B,hooks:[{type:"command",command:T,timeout:30}]}:{hooks:[{type:"command",command:T,timeout:30}]};x.push(j),u=!0}c[W]=x}}i.hooks=c,p(r,JSON.stringify(i,null,2)+`
8
+ `,"utf-8"),t.configured=!0;const d=m.length?` (missing from package: ${m.join(", ")})`:"";return{client:t,action:u?"configured":"skipped",message:(u?"auto-memory hooks installed":"already installed")+d}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function _e(){const e=a(h(),".claude");if(!f(e))return null;const t=O();try{const o=v(_(import.meta.url)),n=`node ${a(o,"wyrm-statusline.js")}`,s=t.configPath;let r={};if(f(s))try{r=JSON.parse(y(s,"utf-8"))}catch{r={}}const i=r.statusLine;if(i?.command&&!/wyrm-statusline/.test(i.command))return{client:t,action:"skipped",message:`a non-Wyrm statusLine is already set (${i.command}); not overwriting`};const c=i?.command===n;return r.statusLine={type:"command",command:n,padding:0},p(s,JSON.stringify(r,null,2)+`
9
+ `,"utf-8"),{client:t,action:c?"skipped":"configured",message:c?"buddy statusline already installed":"Wyrm buddy statusline installed \u2014 persistent in the Claude Code TUI"}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function Oe(){const e=a(h(),".claude","settings.json");if(!f(e))return null;const t=O();try{const o=JSON.parse(y(e,"utf-8")),n=o.statusLine;return n?.command&&/wyrm-statusline/.test(n.command)?(delete o.statusLine,p(e,JSON.stringify(o,null,2)+`
10
+ `,"utf-8"),{client:t,action:"configured",message:"Wyrm statusline removed"}):{client:t,action:"skipped",message:"no Wyrm statusline set"}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function ue(){const e=a(h(),".claude","settings.json");if(!f(e))return null;const t=O();try{const o=JSON.parse(y(e,"utf-8")),n=a(h(),".claude","hooks"),s=new Set(J.map(c=>`node ${a(n,c.script)}`)),r=new Set(J.flatMap(c=>c.events.map(u=>typeof u=="string"?u:u.event))),i=o.hooks;if(i){for(const c of r)Array.isArray(i[c])&&(i[c]=i[c].map(u=>({...u,hooks:Array.isArray(u.hooks)?u.hooks.filter(m=>!s.has(m.command??"")):u.hooks})).filter(u=>!Array.isArray(u.hooks)||u.hooks.length>0),i[c].length===0&&delete i[c]);Object.keys(i).length===0&&delete o.hooks}return p(e,JSON.stringify(o,null,2)+`
11
+ `,"utf-8"),{client:t,action:"configured",message:"auto-memory hooks removed"}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}const I=/wyrm-guard/,G=5,de=["Bash","Edit|Write|MultiEdit|NotebookEdit"];function k(e){return e.settingsPath??process.env.CLAUDE_SETTINGS??a(h(),".claude","settings.json")}function R(e){return{id:"claude-code-guard",name:"Claude Code (failure firewall)",icon:"\u{1F6E1}\uFE0F",configPath:e,configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}function E(e){return b()==="win32"?`"${e.replace(/"/g,'""')}"`:`'${e.replace(/'/g,"'\\''")}'`}function fe(){const e=v(_(import.meta.url)),t=a(e,"wyrm-guard.js");if(f(t))return`node ${E(t)}`;try{const o=b()==="win32"?"where":"which",s=w(o,["wyrm-guard"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
12
+ `)[0]?.trim();if(s)return E(s)}catch{}return null}function U(e,t){const o=e.hooks;if(!o||typeof o!="object"||Array.isArray(o))return!1;let n=!1;for(const s of Object.keys(o)){const r=o[s];if(!Array.isArray(r))continue;let i=!1;const c=[];for(const u of r){const m=u;if(!m||typeof m!="object"||!Array.isArray(m.hooks)){c.push(u);continue}const d=m.hooks.filter(l=>!t.test(l?.command??""));if(d.length===m.hooks.length){c.push(u);continue}n=!0,i=!0,d.length>0&&c.push({...m,hooks:d})}i&&(c.length===0?delete o[s]:o[s]=c)}return n&&Object.keys(o).length===0&&delete e.hooks,n}function z(e){return U(e,I)}function me(e={}){if(!!!(e.settingsPath??process.env.CLAUDE_SETTINGS)&&!f(a(h(),".claude")))return{client:R(k(e)),action:"skipped",message:"failure firewall: ~/.claude not found on this machine (needs the Claude Code CLI). Not armed. Install Claude Code then re-run `wyrm guard`"};const o=k(e),n=R(o);try{const s=e.guardCommand??fe();if(!s)return{client:n,action:"skipped",message:"wyrm-guard bin not found (npm i -g wyrm-mcp, or build dist/) \u2014 firewall hooks not installed"};P(v(o),{recursive:!0});let r={};if(f(o))try{const g=JSON.parse(y(o,"utf-8"));if(typeof g!="object"||g===null||Array.isArray(g))throw new Error("not an object");r=g}catch{return{client:n,action:"failed",message:`${o} is not valid JSON \u2014 refusing to overwrite it (fix or remove the file, then re-run)`}}const i=r.hooks;if(i!==void 0&&(typeof i!="object"||i===null||Array.isArray(i)))return{client:n,action:"failed",message:`${o} "hooks" has an unexpected shape (not a plain object) \u2014 refusing to modify (fix the file, then re-run)`};const c=i?.PreToolUse;if(c!==void 0&&!Array.isArray(c))return{client:n,action:"failed",message:`${o} hooks.PreToolUse has an unexpected shape (not an array) \u2014 refusing to modify (fix the file, then re-run)`};const u=JSON.stringify(r);z(r);const m=r.hooks??{},d=Array.isArray(m.PreToolUse)?m.PreToolUse:[];for(const g of de)d.push({matcher:g,hooks:[{type:"command",command:s,timeout:G}]});m.PreToolUse=d,r.hooks=m;const l=JSON.stringify(r)!==u;return l&&p(o,JSON.stringify(r,null,2)+`
13
+ `,"utf-8"),n.configured=!0,{client:n,action:l?"configured":"skipped",message:l?"failure-firewall hooks installed (PreToolUse: Bash + Edit|Write|MultiEdit|NotebookEdit)":"failure-firewall hooks already installed"}}catch(s){return{client:n,action:"failed",message:s instanceof Error?s.message:String(s)}}}function le(e={}){const t=k(e);if(!f(t))return null;const o=R(t);try{let n;try{const r=JSON.parse(y(t,"utf-8"));if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("not an object");n=r}catch{return{client:o,action:"failed",message:`${t} is not valid JSON \u2014 not touching it`}}return z(n)?(p(t,JSON.stringify(n,null,2)+`
14
+ `,"utf-8"),{client:o,action:"configured",message:"failure-firewall hooks removed"}):{client:o,action:"skipped",message:"no wyrm-guard hooks installed"}}catch(n){return{client:o,action:"failed",message:n instanceof Error?n.message:String(n)}}}function Ee(e={}){const t=k(e),o=[];try{const s=JSON.parse(y(t,"utf-8")).hooks??{};for(const r of Object.values(s))if(Array.isArray(r))for(const i of r)for(const c of i?.hooks??[]){const u=c?.command;u&&I.test(u)&&o.push(u)}}catch{}return{settingsPath:t,installed:o.length>0,commands:o}}const L=/wyrm-memo/,pe=[{event:"PreToolUse",matcher:"Read"},{event:"PreToolUse",matcher:"Bash"},{event:"PostToolUse",matcher:"Read"},{event:"PostToolUse",matcher:"Bash"}];function Y(e){return{id:"claude-code-memo",name:"Claude Code (token firewall)",icon:"",configPath:e,configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}function he(){const e=v(_(import.meta.url)),t=a(e,"wyrm-memo.js");if(f(t))return`node ${E(t)}`;try{const o=b()==="win32"?"where":"which",s=w(o,["wyrm-memo"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
15
+ `)[0]?.trim();if(s)return E(s)}catch{}return null}function ge(e={}){const t=k(e),o=!!(e.settingsPath??process.env.CLAUDE_SETTINGS),n=Y(t);if(!o&&!f(a(h(),".claude")))return{client:n,action:"skipped",message:"token firewall: ~/.claude not found on this machine (needs the Claude Code CLI). Not armed. Install Claude Code then re-run `wyrm memo`"};try{const s=e.memoCommand??he();if(!s)return{client:n,action:"skipped",message:"wyrm-memo bin not found (npm i -g wyrm-mcp, or build dist/). Token-firewall hooks not installed"};P(v(t),{recursive:!0});let r={};if(f(t))try{const d=JSON.parse(y(t,"utf-8"));if(typeof d!="object"||d===null||Array.isArray(d))throw new Error("not an object");r=d}catch{return{client:n,action:"failed",message:`${t} is not valid JSON \u2014 refusing to overwrite it (fix or remove the file, then re-run)`}}const i=r.hooks;if(i!==void 0&&(typeof i!="object"||i===null||Array.isArray(i)))return{client:n,action:"failed",message:`${t} "hooks" has an unexpected shape (not a plain object) \u2014 refusing to modify`};for(const d of["PreToolUse","PostToolUse"]){const l=i?.[d];if(l!==void 0&&!Array.isArray(l))return{client:n,action:"failed",message:`${t} hooks.${d} has an unexpected shape (not an array) \u2014 refusing to modify`}}const c=JSON.stringify(r);U(r,L);const u=r.hooks??{};for(const{event:d,matcher:l}of pe){const g=Array.isArray(u[d])?u[d]:[];g.push({matcher:l,hooks:[{type:"command",command:s,timeout:G}]}),u[d]=g}r.hooks=u;const m=JSON.stringify(r)!==c;return m&&p(t,JSON.stringify(r,null,2)+`
16
+ `,"utf-8"),n.configured=!0,{client:n,action:m?"configured":"skipped",message:m?"token-firewall hooks installed (PreToolUse + PostToolUse: Read, Bash). Mode: observe (set WYRM_MEMO_MODE=warn to hand maps back)":"token-firewall hooks already installed"}}catch(s){return{client:n,action:"failed",message:s instanceof Error?s.message:String(s)}}}function $e(e={}){const t=k(e);if(!f(t))return null;const o=Y(t);try{let n;try{const r=JSON.parse(y(t,"utf-8"));if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("not an object");n=r}catch{return{client:o,action:"failed",message:`${t} is not valid JSON \u2014 not touching it`}}return U(n,L)?(p(t,JSON.stringify(n,null,2)+`
17
+ `,"utf-8"),{client:o,action:"configured",message:"token-firewall hooks removed"}):{client:o,action:"skipped",message:"no wyrm-memo hooks installed"}}catch(n){return{client:o,action:"failed",message:n instanceof Error?n.message:String(n)}}}function Ne(e={}){const t=k(e),o=[];try{const s=JSON.parse(y(t,"utf-8")).hooks??{};for(const r of Object.values(s))if(Array.isArray(r))for(const i of r)for(const c of i?.hooks??[]){const u=c?.command;u&&L.test(u)&&o.push(u)}}catch{}return{settingsPath:t,installed:o.length>0,commands:o}}function ye(e){const t={serverPath:e?.serverPath||M(),dbPath:e?.dbPath||D(),httpPort:e?.httpPort},o=a(h(),".wyrm");f(o)||P(o,{recursive:!0}),q(t);const n=A(),s=[];for(const u of n)s.push(H(u,t));const r=ae();r&&s.push(r);const i=me();i&&s.push(i);const c=ge();return c&&s.push(c),s}function Te(){const e=A(),t=[];for(const s of e)t.push(ce(s));const o=ue();o&&t.push(o);const n=le();return n&&t.push(n),t}function ve(e,t){const o={serverPath:t?.serverPath||M(),dbPath:t?.dbPath||D(),httpPort:t?.httpPort};q(o);const n=A(),s=[];for(const r of n)e.includes(r.id)&&s.push(H(r,o));return s}function q(e){const t=a(h(),".wyrm","wyrm-config.json"),o=A().filter(s=>s.configured).map(s=>s.id),n={version:"3.0.0",serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort,configuredClients:o,lastSetup:new Date().toISOString(),autoUpdate:!0};p(t,JSON.stringify(n,null,2)+`
18
+ `,"utf-8")}function V(){const e=a(h(),".wyrm","wyrm-config.json");if(!f(e))return null;try{return JSON.parse(y(e,"utf-8"))}catch{return null}}function We(){const e=V();return e?ve(e.configuredClients,{serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort}):ye()}const Q="<!-- wyrm:start -->",K="<!-- wyrm:end -->",F=`${Q}
19
19
  ## Wyrm Memory + Agent (auto-managed \u2014 do not edit this section)
20
20
 
21
21
  **Session start** \u2014 call \`wyrm_session_prime\` first to load project context (ground truths, open quests, recent sessions, scaffolds) before responding.
@@ -39,23 +39,23 @@ import{existsSync as d,readFileSync as g,writeFileSync as p,mkdirSync as P,copyF
39
39
  **Keep wyrm-mcp current** \u2014 \`wyrm_check_update\` returns whether a newer version is on npm (cached 24h). When one is, surface it to the operator and offer \`wyrm_self_update\` (with \`confirm:true\`) to upgrade.
40
40
 
41
41
  **Buddy companion** \u2014 call \`wyrm_buddy\` for warm, data-grounded check-ins. At session start (after \`wyrm_session_prime\`) for long-running work, and when the operator completes a quest or hits a milestone. The buddy speaks in a chosen persona but every claim sources a real DB row \u2014 speak it verbatim, never invent additional encouragement. Auto-federates with other registered MCPs that expose \`*_buddy\` tools.
42
- ${F}`;function Ne(e,t){const o={injected:[],skipped:[],errors:[]},n=(r,i)=>{try{const u=w(r);if(d(u)||P(u,{recursive:!0}),d(r)){const c=g(r,"utf-8"),f=c.indexOf(Q),m=c.indexOf(F);if(f!==-1&&m!==-1&&m>f){const l=c.slice(0,f),y=c.slice(m+F.length);p(r,`${l}${B}${y}`,"utf-8")}else{const l=c.endsWith(`
42
+ ${K}`;function xe(e,t){const o={injected:[],skipped:[],errors:[]},n=(r,i)=>{try{const c=v(r);if(f(c)||P(c,{recursive:!0}),f(r)){const u=y(r,"utf-8"),m=u.indexOf(Q),d=u.indexOf(K);if(m!==-1&&d!==-1&&d>m){const l=u.slice(0,m),g=u.slice(d+K.length);p(r,`${l}${F}${g}`,"utf-8")}else{const l=u.endsWith(`
43
43
  `)?`
44
44
  `:`
45
45
 
46
- `;p(r,`${c}${l}${B}
47
- `,"utf-8")}}else p(r,`${B}
48
- `,"utf-8");o.injected.push(i)}catch(u){o.errors.push(`${i}: ${u}`)}},s=t.length===0?["copilot","cursor"]:t;for(const r of s){const i=r.toLowerCase();if(i==="copilot"||i==="vscode-copilot"){const u=a(e,".github","copilot-instructions.md");n(u,".github/copilot-instructions.md")}else if(i==="cursor"){const u=a(e,".cursor","rules");n(u,".cursor/rules")}else o.skipped.push(r)}return o}function E(e){let t="",o=!1;for(let n=0;n<e.length;n++){const s=e[n];if(o){if(t+=s,s==="\\"){t+=e[++n]??"";continue}s==='"'&&(o=!1);continue}if(s==='"'){o=!0,t+=s;continue}if(s==="/"&&e[n+1]==="/"){for(;n<e.length&&e[n]!==`
46
+ `;p(r,`${u}${l}${F}
47
+ `,"utf-8")}}else p(r,`${F}
48
+ `,"utf-8");o.injected.push(i)}catch(c){o.errors.push(`${i}: ${c}`)}},s=t.length===0?["copilot","cursor"]:t;for(const r of s){const i=r.toLowerCase();if(i==="copilot"||i==="vscode-copilot"){const c=a(e,".github","copilot-instructions.md");n(c,".github/copilot-instructions.md")}else if(i==="cursor"){const c=a(e,".cursor","rules");n(c,".cursor/rules")}else o.skipped.push(r)}return o}function $(e){let t="",o=!1;for(let n=0;n<e.length;n++){const s=e[n];if(o){if(t+=s,s==="\\"){t+=e[++n]??"";continue}s==='"'&&(o=!1);continue}if(s==='"'){o=!0,t+=s;continue}if(s==="/"&&e[n+1]==="/"){for(;n<e.length&&e[n]!==`
49
49
  `;)n++;t+=`
50
50
  `;continue}if(s==="/"&&e[n+1]==="*"){for(n+=2;n<e.length&&!(e[n]==="*"&&e[n+1]==="/");)n++;n+=1;continue}if(s===","){let r=n+1;for(;;){for(;r<e.length&&/\s/.test(e[r]);)r++;if(e[r]==="/"&&e[r+1]==="/"){for(;r<e.length&&e[r]!==`
51
- `;)r++;continue}if(e[r]==="/"&&e[r+1]==="*"){for(r+=2;r<e.length&&!(e[r]==="*"&&e[r+1]==="/");)r++;r+=2;continue}break}if(e[r]==="}"||e[r]==="]")continue}t+=s}return JSON.parse(t)}function xe(){const e=A(),t=e.filter(r=>r.detected),o=e.filter(r=>r.configured);let n=`\u{F115D} Wyrm Auto-Configure Status
51
+ `;)r++;continue}if(e[r]==="/"&&e[r+1]==="*"){for(r+=2;r<e.length&&!(e[r]==="*"&&e[r+1]==="/");)r++;r+=2;continue}break}if(e[r]==="}"||e[r]==="]")continue}t+=s}return JSON.parse(t)}function Me(){const e=A(),t=e.filter(r=>r.detected),o=e.filter(r=>r.configured);let n=`\u{F115D} Wyrm Auto-Configure Status
52
52
 
53
53
  `;n+=` Detected: ${t.length}/${e.length} AI clients
54
54
  `,n+=` Configured: ${o.length}/${t.length} clients
55
55
 
56
- `;for(const r of e){const i=r.detected?r.configured?" \u25CF":" \u25D0":" \u25CB",u=r.version?` (${r.version})`:"",c=r.detected?r.configured?"configured \u2713":"detected \u2014 not configured":"not found";n+=`${i} ${r.icon} ${r.name}${u}: ${c}
56
+ `;for(const r of e){const i=r.detected?r.configured?" \u25CF":" \u25D0":" \u25CB",c=r.version?` (${r.version})`:"",u=r.detected?r.configured?"configured \u2713":"detected \u2014 not configured":"not found";n+=`${i} ${r.icon} ${r.name}${c}: ${u}
57
57
  `}const s=V();return s&&(n+=`
58
58
  Server: ${s.serverPath}
59
59
  `,n+=` DB: ${s.dbPath}
60
60
  `,n+=` Last: ${new Date(s.lastSetup).toLocaleString()}
61
- `),n}export{G as GUARD_HOOK_TIMEOUT_SEC,le as MEMO_HOOK_EVENTS,B as WYRM_INJECT_BLOCK,je as applyWyrmEnvToClient,ge as autoConfigureAll,H as configureClient,ye as configureSpecific,A as detectClients,Ae as exportServerConfig,M as findWyrmServerPath,D as getDefaultDbPath,xe as getStatusSummary,Ne as injectSystemPrompt,ce as installClaudeCodeHooks,Ce as installClaudeStatusline,fe as installWyrmGuardHooks,he as installWyrmMemoHooks,V as loadWyrmMeta,E as parseJsonWithComments,$ as quoteHookPath,We as reconfAll,ae as removeClaudeCodeHooks,_e as removeClaudeStatusline,Te as removeFromAll,ie as removeFromClient,me as removeWyrmGuardHooks,$e as removeWyrmMemoHooks,de as resolveWyrmGuardCommand,pe as resolveWyrmMemoCommand,Oe as wyrmGuardHookStatus,Ee as wyrmMemoHookStatus};
61
+ `),n}export{G as GUARD_HOOK_TIMEOUT_SEC,pe as MEMO_HOOK_EVENTS,F as WYRM_INJECT_BLOCK,Ce as applyWyrmEnvToClient,ye as autoConfigureAll,H as configureClient,ve as configureSpecific,A as detectClients,je as exportServerConfig,M as findWyrmServerPath,D as getDefaultDbPath,Me as getStatusSummary,xe as injectSystemPrompt,ae as installClaudeCodeHooks,_e as installClaudeStatusline,me as installWyrmGuardHooks,ge as installWyrmMemoHooks,V as loadWyrmMeta,ie as mergeServerEntry,$ as parseJsonWithComments,E as quoteHookPath,We as reconfAll,ue as removeClaudeCodeHooks,Oe as removeClaudeStatusline,Te as removeFromAll,ce as removeFromClient,le as removeWyrmGuardHooks,$e as removeWyrmMemoHooks,fe as resolveWyrmGuardCommand,he as resolveWyrmMemoCommand,Ee as wyrmGuardHookStatus,Ne as wyrmMemoHookStatus};
package/dist/database.js CHANGED
@@ -1,11 +1,14 @@
1
- import{openDatabase as D}from"./storage/index.js";import{existsSync as u,mkdirSync as O,readdirSync as L,statSync as I,writeFileSync as M,chmodSync as A}from"fs";import{homedir as N}from"os";import{join as d,basename as w,resolve as j,normalize as v}from"path";import{spawnSync as T}from"child_process";import{getResilienceManager as H}from"./resilience.js";import{WyrmLogger as F}from"./logger.js";import{runMigrations as W,getSchemaVersion as U}from"./migrations.js";import{validateProjectPath as y,buildFtsMatchQuery as h}from"./security.js";import{emitEvent as m,eventsSince as x,subscribeEvents as P,isLiveMemoryEnabled as B,ingestRemoteEvent as q,eventsForPush as $,pruneEvents as Y,getMeta as G,setMeta as V}from"./events.js";import{getActor as C}from"./handlers/boundary.js";import{readSkillContent as k,slugify as J,skillContentSha as z}from"./skill-author.js";import{resolveProjectForPath as Q}from"./project-resolve.js";function ue(){return process.env.WYRM_DB_PATH??process.env.WYRM_DB??d(N(),".wyrm","wyrm.db")}function K(S,t){const e=(s,r)=>{try{u(s)&&A(s,r)}catch{}};t&&e(t,448),e(S,384),e(`${S}-wal`,384),e(`${S}-shm`,384)}class pe{db;BATCH_SIZE=1e3;resilience;logger;dbPath;constructor(t){const e=d(N(),".wyrm");u(e)||O(e,{recursive:!0,mode:448}),this.dbPath=t||d(e,"wyrm.db"),this.logger=new F,this.resilience=H(),this.db=this.initializeDatabase(this.dbPath),K(this.dbPath,e),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("cache_size = -64000"),this.db.pragma("temp_store = MEMORY"),this.db.pragma("busy_timeout = 5000"),this.db.pragma("mmap_size = 268435456"),this.db.pragma("page_size = 4096"),this.db.pragma("foreign_keys = ON");const s=W(this.db);s.length>0&&this.logger.info(`Applied ${s.length} migration(s), now at v${U(this.db)}`),this.recoverIncompleteOperations()}getDatabase(){return this.db}getDatabasePath(){return this.dbPath}initializeDatabase(t){const e=this.resilience.withRetrySync(()=>D(t),"database_init",{maxAttempts:3,baseDelayMs:500});if(!e.success)throw this.logger.error("Failed to initialize database",{path:t,error:e.error?.message}),e.error||new Error("Database initialization failed");return e.data}recoverIncompleteOperations(){const t=this.resilience.getIncompleteOperations();for(const e of t)this.logger.warn("Found incomplete operation from previous session",{operation:e.operation,stage:e.stage,id:e.id}),e.operation==="batch_insert"&&this.logger.info("Batch insert was incomplete - data may need re-import"),this.resilience.completeCheckpoint(e.id)}addWatchDir(t,e=!0){return y(t),this.db.prepare(`
1
+ import{openDatabase as L}from"./storage/index.js";import{existsSync as u,mkdirSync as O,readdirSync as I,statSync as A,writeFileSync as w,chmodSync as M}from"fs";import{homedir as y}from"os";import{join as d,basename as j,resolve as v,normalize as H}from"path";import{spawnSync as T}from"child_process";import{getResilienceManager as F}from"./resilience.js";import{WyrmLogger as W}from"./logger.js";import{runMigrations as U,schemaSkew as x,getSchemaVersion as P}from"./migrations.js";import{validateProjectPath as N,buildFtsMatchQuery as h,redactCredentials as C}from"./security.js";import{emitEvent as m,eventsSince as B,subscribeEvents as q,isLiveMemoryEnabled as $,ingestRemoteEvent as Y,eventsForPush as G,pruneEvents as V,getMeta as J,setMeta as z}from"./events.js";import{getActor as k}from"./handlers/boundary.js";import{readSkillContent as D,slugify as Q,skillContentSha as K}from"./skill-author.js";import{resolveProjectForPath as Z}from"./project-resolve.js";function me(){return process.env.WYRM_DB_PATH??process.env.WYRM_DB??d(y(),".wyrm","wyrm.db")}function X(f,t){const e=(s,r)=>{try{u(s)&&M(s,r)}catch{}};t&&e(t,448),e(f,384),e(`${f}-wal`,384),e(`${f}-shm`,384)}const ee={truth:{select:`SELECT t.cross_project_visibility AS vis, t.project_id AS pid, p.name AS pname, p.sync_policy AS policy
2
+ FROM ground_truths t LEFT JOIN projects p ON p.id = t.project_id WHERE t.id = ?`,update:"UPDATE ground_truths SET cross_project_visibility = ?, updated_at = datetime('now') WHERE id = ?"},memory:{select:`SELECT t.cross_project_visibility AS vis, t.project_id AS pid, p.name AS pname, p.sync_policy AS policy
3
+ FROM memory_artifacts t LEFT JOIN projects p ON p.id = t.project_id WHERE t.id = ?`,update:"UPDATE memory_artifacts SET cross_project_visibility = ?, updated_at = datetime('now') WHERE id = ?"},quest:{select:`SELECT t.cross_project_visibility AS vis, t.project_id AS pid, p.name AS pname, p.sync_policy AS policy
4
+ FROM quests t LEFT JOIN projects p ON p.id = t.project_id WHERE t.id = ?`,update:"UPDATE quests SET cross_project_visibility = ?, updated_at = datetime('now') WHERE id = ?"}},te=new Set(["within","org","public"]);class _e{db;BATCH_SIZE=1e3;resilience;logger;dbPath;constructor(t){const e=d(y(),".wyrm");u(e)||O(e,{recursive:!0,mode:448}),this.dbPath=t||d(e,"wyrm.db"),this.logger=new W,this.resilience=F(),this.db=this.initializeDatabase(this.dbPath),X(this.dbPath,e),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("cache_size = -64000"),this.db.pragma("temp_store = MEMORY"),this.db.pragma("busy_timeout = 5000"),this.db.pragma("mmap_size = 268435456"),this.db.pragma("page_size = 4096"),this.db.pragma("foreign_keys = ON"),this.db.pragma("secure_delete = ON");const s=U(this.db),r=x(this.db);r.storeIsNewer&&this.logger.warn(`This store is at schema v${r.store}, newer than this Wyrm knows (v${r.binary}). An older binary is writing to a newer store under older rules. Upgrade Wyrm on this machine.`),s.length>0&&this.logger.info(`Applied ${s.length} migration(s), now at v${P(this.db)}`),this.recoverIncompleteOperations()}getDatabase(){return this.db}getDatabasePath(){return this.dbPath}initializeDatabase(t){const e=this.resilience.withRetrySync(()=>L(t),"database_init",{maxAttempts:3,baseDelayMs:500});if(!e.success)throw this.logger.error("Failed to initialize database",{path:t,error:e.error?.message}),e.error||new Error("Database initialization failed");return e.data}recoverIncompleteOperations(){const t=this.resilience.getIncompleteOperations();for(const e of t)this.logger.warn("Found incomplete operation from previous session",{operation:e.operation,stage:e.stage,id:e.id}),e.operation==="batch_insert"&&this.logger.info("Batch insert was incomplete - data may need re-import"),this.resilience.completeCheckpoint(e.id)}addWatchDir(t,e=!0){return N(t),this.db.prepare(`
2
5
  INSERT INTO watch_dirs (path, recursive)
3
6
  VALUES (?, ?)
4
7
  ON CONFLICT(path) DO UPDATE SET recursive = excluded.recursive
5
8
  RETURNING *
6
- `).get(t,e?1:0)}getWatchDirs(){return this.db.prepare("SELECT * FROM watch_dirs").all()}removeWatchDir(t){this.db.prepare("DELETE FROM watch_dirs WHERE path = ?").run(t)}scanForProjects(t,e=!0){y(t);const s=[],r=(n,i=0)=>{if(!(i>3&&e))try{const o=L(n,{withFileTypes:!0});for(const a of o){if(!a.isDirectory()||a.name.startsWith(".")&&a.name!==".git")continue;const c=d(n,a.name),l=d(c,".git");if(u(l)){const p=this.registerProjectFromPath(c);p&&s.push(p)}else e&&i<3&&r(c,i+1)}}catch{}};return r(t),this.db.prepare(`
9
+ `).get(t,e?1:0)}getWatchDirs(){return this.db.prepare("SELECT * FROM watch_dirs").all()}removeWatchDir(t){this.db.prepare("DELETE FROM watch_dirs WHERE path = ?").run(t)}scanForProjects(t,e=!0){N(t);const s=[],r=(i,n=0)=>{if(!(n>3&&e))try{const a=I(i,{withFileTypes:!0});for(const o of a){if(!o.isDirectory()||o.name.startsWith(".")&&o.name!==".git")continue;const c=d(i,o.name),l=d(c,".git");if(u(l)){const p=this.registerProjectFromPath(c);p&&s.push(p)}else e&&n<3&&r(c,n+1)}}catch{}};return r(t),this.db.prepare(`
7
10
  UPDATE watch_dirs SET last_scan = datetime('now') WHERE path = ?
8
- `).run(t),s}scanAllWatchDirs(){const t=this.getWatchDirs(),e=[];for(const s of t){const r=this.scanForProjects(s.path,!!s.recursive);e.push(...r)}return e}registerProjectFromPath(t){try{const e=v(j(t));if(!u(e)||!I(e).isDirectory())return null;const s=w(e);let r,n,i,o;try{const a=T("git",["config","--get","remote.origin.url"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});a.status===0&&(r=a.stdout.trim());const c=T("git",["rev-parse","--abbrev-ref","HEAD"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});c.status===0&&(n=c.stdout.trim());const l=T("git",["log","-1","--format=%h %s"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});l.status===0&&(i=l.stdout.trim())}catch{}return u(d(e,"package.json"))?(o="Node.js",u(d(e,"next.config.js"))||u(d(e,"next.config.ts"))||u(d(e,"next.config.mjs"))?o="Next.js":u(d(e,"vite.config.ts"))&&(o="Vite")):u(d(e,"requirements.txt"))||u(d(e,"pyproject.toml"))?o="Python":u(d(e,"composer.json"))?o="PHP":u(d(e,"Cargo.toml"))?o="Rust":u(d(e,"go.mod"))&&(o="Go"),this.registerProject(s,e,r,o,i,n)}catch{return null}}registerProject(t,e,s,r,n,i){return this.db.prepare(`
11
+ `).run(t),s}scanAllWatchDirs(){const t=this.getWatchDirs(),e=[];for(const s of t){const r=this.scanForProjects(s.path,!!s.recursive);e.push(...r)}return e}registerProjectFromPath(t){try{const e=H(v(t));if(!u(e)||!A(e).isDirectory())return null;const s=j(e);let r,i,n,a;try{const o=T("git",["config","--get","remote.origin.url"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});o.status===0&&(r=o.stdout.trim());const c=T("git",["rev-parse","--abbrev-ref","HEAD"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});c.status===0&&(i=c.stdout.trim());const l=T("git",["log","-1","--format=%h %s"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});l.status===0&&(n=l.stdout.trim())}catch{}return u(d(e,"package.json"))?(a="Node.js",u(d(e,"next.config.js"))||u(d(e,"next.config.ts"))||u(d(e,"next.config.mjs"))?a="Next.js":u(d(e,"vite.config.ts"))&&(a="Vite")):u(d(e,"requirements.txt"))||u(d(e,"pyproject.toml"))?a="Python":u(d(e,"composer.json"))?a="PHP":u(d(e,"Cargo.toml"))?a="Rust":u(d(e,"go.mod"))&&(a="Go"),this.registerProject(s,e,r,a,n,i)}catch{return null}}registerProject(t,e,s,r,i,n){return this.db.prepare(`
9
12
  INSERT INTO projects (name, path, repo, stack, last_commit, branch)
10
13
  VALUES (?, ?, ?, ?, ?, ?)
11
14
  ON CONFLICT(path) DO UPDATE SET
@@ -16,33 +19,33 @@ import{openDatabase as D}from"./storage/index.js";import{existsSync as u,mkdirSy
16
19
  branch = COALESCE(excluded.branch, projects.branch),
17
20
  updated_at = datetime('now')
18
21
  RETURNING *
19
- `).get(t,e,s||null,r||null,n||null,i||null)}getProject(t){return this.db.prepare("SELECT * FROM projects WHERE path = ?").get(t)}getProjectForPath(t){const{id:e,root:s}=Q(this.db,t);return e!==null&&s!==null?{id:e,path:s}:void 0}getProjectById(t){return this.db.prepare("SELECT * FROM projects WHERE id = ?").get(t)}getProjectByName(t){return this.db.prepare("SELECT * FROM projects WHERE name = ?").get(t)}getAllProjects(t=100,e=0){return this.db.prepare(`
22
+ `).get(t,e,s||null,r||null,i||null,n||null)}getProject(t){return this.db.prepare("SELECT * FROM projects WHERE path = ?").get(t)}getProjectForPath(t){const{id:e,root:s}=Z(this.db,t);return e!==null&&s!==null?{id:e,path:s}:void 0}getProjectById(t){return this.db.prepare("SELECT * FROM projects WHERE id = ?").get(t)}getProjectByName(t){return this.db.prepare("SELECT * FROM projects WHERE name = ?").get(t)}getAllProjects(t=100,e=0){return this.db.prepare(`
20
23
  SELECT * FROM projects ORDER BY updated_at DESC LIMIT ? OFFSET ?
21
24
  `).all(t,e)}searchProjects(t){const e=`%${t}%`;return this.db.prepare(`
22
25
  SELECT * FROM projects
23
26
  WHERE name LIKE ? OR stack LIKE ? OR repo LIKE ?
24
27
  ORDER BY updated_at DESC
25
28
  LIMIT 50
26
- `).all(e,e,e)}createSession(t,e){const s=this.estimateTokens((e.objectives||"")+(e.completed||"")+(e.issues||"")+(e.notes||"")),r=C(),n=this.resilience.withRetrySync(()=>this.db.prepare(`
29
+ `).all(e,e,e)}createSession(t,e){const s=this.estimateTokens((e.objectives||"")+(e.completed||"")+(e.issues||"")+(e.notes||"")),r=k(),i=this.resilience.withRetrySync(()=>this.db.prepare(`
27
30
  INSERT INTO sessions (project_id, date, objectives, completed, issues, commits, files_changed, notes, tokens_estimate, agent_id, run_id)
28
31
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
29
32
  RETURNING *
30
- `).get(t,e.date||new Date().toISOString().split("T")[0],e.objectives||"",e.completed||"",e.issues||"",e.commits||"",e.files_changed||"",e.notes||"",s,r.agent_id,r.run_id),"createSession");if(!n.success)throw n.error||new Error("Failed to create session");return m(this.db,{projectId:t,kind:"session_update",refTable:"sessions",refId:n.data.id}),n.data}updateSession(t,e){const s=[],r=[],n=new Set(["objectives","completed","issues","commits","files_changed","notes","summary","is_archived"]);for(const[o,a]of Object.entries(e))n.has(o)&&(s.push(`${o} = ?`),r.push(a));if(s.length===0)return this.getSession(t);if(e.objectives||e.completed||e.issues||e.notes){const o=this.getSession(t);if(o){const a=this.estimateTokens((e.objectives||o.objectives)+(e.completed||o.completed)+(e.issues||o.issues)+(e.notes||o.notes));s.push("tokens_estimate = ?"),r.push(a)}}r.push(t);const i=this.resilience.withRetrySync(()=>this.db.prepare(`
33
+ `).get(t,e.date||new Date().toISOString().split("T")[0],e.objectives||"",e.completed||"",e.issues||"",e.commits||"",e.files_changed||"",e.notes||"",s,r.agent_id,r.run_id),"createSession");if(!i.success)throw i.error||new Error("Failed to create session");return m(this.db,{projectId:t,kind:"session_update",refTable:"sessions",refId:i.data.id}),i.data}updateSession(t,e){const s=[],r=[],i=new Set(["objectives","completed","issues","commits","files_changed","notes","summary","is_archived"]);for(const[a,o]of Object.entries(e))i.has(a)&&(s.push(`${a} = ?`),r.push(o));if(s.length===0)return this.getSession(t);if(e.objectives||e.completed||e.issues||e.notes){const a=this.getSession(t);if(a){const o=this.estimateTokens((e.objectives||a.objectives)+(e.completed||a.completed)+(e.issues||a.issues)+(e.notes||a.notes));s.push("tokens_estimate = ?"),r.push(o)}}r.push(t);const n=this.resilience.withRetrySync(()=>this.db.prepare(`
31
34
  UPDATE sessions SET ${s.join(", ")} WHERE id = ? RETURNING *
32
- `).get(...r),"updateSession");if(!i.success)throw i.error||new Error("Failed to update session");return m(this.db,{projectId:i.data.project_id,kind:"session_update",refTable:"sessions",refId:t}),i.data}getSession(t){return this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(t)}updateContinuity(t,e,s){const r=e.trim();if(!r)return;const n=12e3,i=this.db.prepare("SELECT thread, turn_count FROM project_continuity WHERE project_id = ?").get(t),o=new Date().toISOString(),a=`[${o}${s?" \xB7 "+s:""}] ${r}`;let c=i?.thread?i.thread+`
35
+ `).get(...r),"updateSession");if(!n.success)throw n.error||new Error("Failed to update session");return m(this.db,{projectId:n.data.project_id,kind:"session_update",refTable:"sessions",refId:t}),n.data}getSession(t){return this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(t)}updateContinuity(t,e,s){const r=e.trim();if(!r)return;const i=12e3,n=this.db.prepare("SELECT thread, turn_count FROM project_continuity WHERE project_id = ?").get(t),a=new Date().toISOString(),o=`[${a}${s?" \xB7 "+s:""}] ${r}`;let c=n?.thread?n.thread+`
33
36
 
34
- `+a:a;c.length>n&&(c=`[\u2026earlier continuity trimmed \u2014 gist retained below\u2026]
37
+ `+o:o;c.length>i&&(c=`[\u2026earlier continuity trimmed \u2014 gist retained below\u2026]
35
38
 
36
- `+c.slice(c.length-n));const l=(i?.turn_count??0)+1;this.resilience.withRetrySync(()=>(this.db.prepare(`
39
+ `+c.slice(c.length-i));const l=(n?.turn_count??0)+1;this.resilience.withRetrySync(()=>(this.db.prepare(`
37
40
  INSERT INTO project_continuity (project_id, thread, updated_at, updated_by, turn_count)
38
41
  VALUES (?, ?, ?, ?, ?)
39
42
  ON CONFLICT(project_id) DO UPDATE SET
40
43
  thread = excluded.thread, updated_at = excluded.updated_at,
41
44
  updated_by = excluded.updated_by, turn_count = excluded.turn_count
42
- `).run(t,c,o,s??null,l),!0),"updateContinuity")}getContinuity(t){return this.db.prepare("SELECT thread, updated_at, updated_by, turn_count FROM project_continuity WHERE project_id = ?").get(t)??null}recordRouting(t){const e=t.projectId==null?-1:t.projectId,s=(t.taskType??"").trim(),r=(t.modelId??"").trim();if(!s)throw new Error("recordRouting: taskType is required");if(!r)throw new Error("recordRouting: modelId is required");let n=null;if(Array.isArray(t.toolNames)){const a=[...new Set(t.toolNames.map(c=>String(c).trim()).filter(Boolean))];n=a.length?a.join(","):null}else if(typeof t.toolNames=="string"){const a=[...new Set(t.toolNames.split(",").map(c=>c.trim()).filter(Boolean))];n=a.length?a.join(","):null}const i=t.latencyMs==null||!Number.isFinite(t.latencyMs)?null:Math.max(0,Math.round(t.latencyMs));let o=-1;return this.resilience.withRetrySync(()=>{const a=this.db.prepare(`
45
+ `).run(t,c,a,s??null,l),!0),"updateContinuity")}getContinuity(t){return this.db.prepare("SELECT thread, updated_at, updated_by, turn_count FROM project_continuity WHERE project_id = ?").get(t)??null}recordRouting(t){const e=t.projectId==null?-1:t.projectId,s=(t.taskType??"").trim(),r=(t.modelId??"").trim();if(!s)throw new Error("recordRouting: taskType is required");if(!r)throw new Error("recordRouting: modelId is required");let i=null;if(Array.isArray(t.toolNames)){const o=[...new Set(t.toolNames.map(c=>String(c).trim()).filter(Boolean))];i=o.length?o.join(","):null}else if(typeof t.toolNames=="string"){const o=[...new Set(t.toolNames.split(",").map(c=>c.trim()).filter(Boolean))];i=o.length?o.join(","):null}const n=t.latencyMs==null||!Number.isFinite(t.latencyMs)?null:Math.max(0,Math.round(t.latencyMs));let a=-1;return this.resilience.withRetrySync(()=>{const o=this.db.prepare(`
43
46
  INSERT INTO routing_memory (project_id, task_type, model_id, tool_names, success, latency_ms)
44
47
  VALUES (?, ?, ?, ?, ?, ?)
45
- `).run(e,s,r,n,t.success?1:0,i);return o=Number(a.lastInsertRowid),!0},"recordRouting"),o}recallRouting(t,e={}){const s=(t??"").trim();if(!s)return[];const r=Math.min(100,Math.max(1,e.limit??10)),n=["task_type = ?"],i=[s];e.projectId!=null&&(n.push("(project_id = ? OR project_id = -1)"),i.push(e.projectId)),e.sinceDays!=null&&Number.isFinite(e.sinceDays)&&e.sinceDays>0&&(n.push("ts >= datetime('now', ?)"),i.push(`-${Math.round(e.sinceDays)} days`)),i.push(r);try{return this.db.prepare(`
48
+ `).run(e,s,r,i,t.success?1:0,n);return a=Number(o.lastInsertRowid),!0},"recordRouting"),a}recallRouting(t,e={}){const s=(t??"").trim();if(!s)return[];const r=Math.min(100,Math.max(1,e.limit??10)),i=["task_type = ?"],n=[s];e.projectId!=null&&(i.push("(project_id = ? OR project_id = -1)"),n.push(e.projectId)),e.sinceDays!=null&&Number.isFinite(e.sinceDays)&&e.sinceDays>0&&(i.push("ts >= datetime('now', ?)"),n.push(`-${Math.round(e.sinceDays)} days`)),n.push(r);try{return this.db.prepare(`
46
49
  SELECT
47
50
  model_id,
48
51
  COALESCE(tool_names, '') AS tool_names,
@@ -52,11 +55,11 @@ import{openDatabase as D}from"./storage/index.js";import{existsSync as u,mkdirSy
52
55
  AVG(CASE WHEN latency_ms IS NOT NULL THEN latency_ms END) AS avg_latency_ms,
53
56
  MAX(ts) AS last_ts
54
57
  FROM routing_memory
55
- WHERE ${n.join(" AND ")}
58
+ WHERE ${i.join(" AND ")}
56
59
  GROUP BY model_id, COALESCE(tool_names, '')
57
60
  ORDER BY (CAST(SUM(success) AS REAL) / COUNT(*)) DESC, runs DESC, last_ts DESC
58
61
  LIMIT ?
59
- `).all(...i).map(a=>({model_id:a.model_id,tool_names:a.tool_names,runs:a.runs,successes:a.successes,failures:a.failures,success_rate:a.runs>0?a.successes/a.runs:0,avg_latency_ms:a.avg_latency_ms==null?null:Math.round(a.avg_latency_ms),last_ts:a.last_ts}))}catch{return[]}}getRecentSessions(t,e=5){return this.db.prepare(`
62
+ `).all(...n).map(o=>({model_id:o.model_id,tool_names:o.tool_names,runs:o.runs,successes:o.successes,failures:o.failures,success_rate:o.runs>0?o.successes/o.runs:0,avg_latency_ms:o.avg_latency_ms==null?null:Math.round(o.avg_latency_ms),last_ts:o.last_ts}))}catch{return[]}}getRecentSessions(t,e=5){return this.db.prepare(`
60
63
  SELECT * FROM sessions
61
64
  WHERE project_id = ? AND is_archived = 0
62
65
  ORDER BY date DESC, id DESC
@@ -89,11 +92,11 @@ import{openDatabase as D}from"./storage/index.js";import{existsSync as u,mkdirSy
89
92
  `).run(t,t,e).changes}getSessionTokenUsage(t){return this.db.prepare(`
90
93
  SELECT COALESCE(SUM(tokens_estimate), 0) as total
91
94
  FROM sessions WHERE project_id = ? AND is_archived = 0
92
- `).get(t).total}addQuest(t,e,s,r="medium",n){const i=C(),o=this.db.prepare(`
95
+ `).get(t).total}addQuest(t,e,s,r="medium",i){const n=k(),a=this.db.prepare(`
93
96
  INSERT INTO quests (project_id, title, description, priority, tags, agent_id, run_id, updated_at)
94
97
  VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
95
98
  RETURNING *
96
- `).get(t,e,s||"",r,n||null,i.agent_id,i.run_id);return m(this.db,{projectId:t,kind:"quest",refTable:"quests",refId:o.id}),o}updateQuest(t,e){const s=e==="completed"?new Date().toISOString():null,r=this.db.prepare(`
99
+ `).get(t,C(e),C(s||""),r,i||null,n.agent_id,n.run_id);return m(this.db,{projectId:t,kind:"quest",refTable:"quests",refId:a.id}),a}updateQuest(t,e){const s=e==="completed"?new Date().toISOString():null,r=this.db.prepare(`
97
100
  UPDATE quests SET status = ?, completed_at = ?, updated_at = datetime('now') WHERE id = ? RETURNING *
98
101
  `).get(e,s,t);return r&&m(this.db,{projectId:r.project_id,kind:"quest",refTable:"quests",refId:t}),r}getPendingQuests(t){return this.db.prepare(`
99
102
  SELECT * FROM quests
@@ -139,7 +142,7 @@ import{openDatabase as D}from"./storage/index.js";import{existsSync as u,mkdirSy
139
142
  SELECT value FROM context WHERE project_id = ? AND key = ?
140
143
  `).get(t,e)?.value}getAllContext(t){const e=this.db.prepare(`
141
144
  SELECT key, value FROM context WHERE project_id = ?
142
- `).all(t),s={};for(const r of e)s[r.key]=r.value;return s}liveMemoryEnabled(){return B()}eventsSince(t,e=0,s=100){return x(this.db,t,e,s)}subscribeEvents(t,e=20){return P(this.db,t,e)}publishEvent(t){m(this.db,t)}pruneEvents(t={}){return Y(this.db,t)}ingestRemoteEvent(t,e){return q(this.db,t,e)}eventsForPush(t,e=0,s=200){return $(this.db,t,e,s)}getMeta(t){return G(this.db,t)}setMeta(t,e){V(this.db,t,e)}setGlobalContext(t,e){this.db.prepare(`
145
+ `).all(t),s={};for(const r of e)s[r.key]=r.value;return s}liveMemoryEnabled(){return $()}eventsSince(t,e=0,s=100){return B(this.db,t,e,s)}subscribeEvents(t,e=20){return q(this.db,t,e)}publishEvent(t){m(this.db,t)}pruneEvents(t={}){return V(this.db,t)}ingestRemoteEvent(t,e){return Y(this.db,t,e)}eventsForPush(t,e=0,s=200){return G(this.db,t,e,s)}getMeta(t){return J(this.db,t)}setMeta(t,e){z(this.db,t,e)}setGlobalContext(t,e){this.db.prepare(`
143
146
  INSERT INTO global_context (key, value)
144
147
  VALUES (?, ?)
145
148
  ON CONFLICT(key) DO UPDATE SET
@@ -147,7 +150,7 @@ import{openDatabase as D}from"./storage/index.js";import{existsSync as u,mkdirSy
147
150
  updated_at = datetime('now')
148
151
  `).run(t,e)}getGlobalContext(t){return this.db.prepare(`
149
152
  SELECT value FROM global_context WHERE key = ?
150
- `).get(t)?.value}getAllGlobalContext(){const t=this.db.prepare("SELECT key, value FROM global_context").all(),e={};for(const s of t)e[s.key]=s.value;return e}registerSkill(t,e,s,r,n,i,o,a){const c=a?.tier??"atomic",l=JSON.stringify(Array.isArray(a?.governs)?a.governs:[]),p=JSON.stringify(Array.isArray(a?.composes)?a.composes:[]),_=k({skillPath:s,name:t}),g=_?.content??null,f=_?.sha256??null,R=_?new Date().toISOString():null,E=this.resilience.withRetrySync(()=>this.db.prepare(`
153
+ `).get(t)?.value}getAllGlobalContext(){const t=this.db.prepare("SELECT key, value FROM global_context").all(),e={};for(const s of t)e[s.key]=s.value;return e}registerSkill(t,e,s,r,i,n,a,o){const c=o?.tier??"atomic",l=JSON.stringify(Array.isArray(o?.governs)?o.governs:[]),p=JSON.stringify(Array.isArray(o?.composes)?o.composes:[]),_=D({skillPath:s,name:t}),g=_?.content??null,S=_?.sha256??null,R=_?new Date().toISOString():null,E=this.resilience.withRetrySync(()=>this.db.prepare(`
151
154
  INSERT INTO skills (name, description, skill_path, category, author, version, tags, tier, governs, composes, content, content_sha256, content_updated_at, is_active, usage_count)
152
155
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)
153
156
  ON CONFLICT(name) DO UPDATE SET
@@ -169,17 +172,17 @@ import{openDatabase as D}from"./storage/index.js";import{existsSync as u,mkdirSy
169
172
  updated_at = datetime('now'),
170
173
  is_active = 1
171
174
  RETURNING *
172
- `).get(t,e,s,r||null,n||null,i||null,o||null,c,l,p,g,f,R),"registerSkill");if(!E.success)throw E.error||new Error("Failed to register skill");return E.data}getSkill(t){const e=this.db.prepare("SELECT * FROM skills WHERE name = ?").get(t);return e&&this.db.prepare("UPDATE skills SET last_used = datetime('now'), usage_count = usage_count + 1 WHERE id = ?").run(e.id),e}backfillSkillContent(t={}){const e=t.read??k,s=this.db.prepare("SELECT id, name, skill_path, content_sha256 FROM skills").all();let r=0,n=0,i=0;const o=this.db.prepare("UPDATE skills SET content = ?, content_sha256 = ?, content_updated_at = ?, updated_at = datetime('now') WHERE id = ?");return this.db.transaction(()=>{for(const c of s){const l=e({skillPath:c.skill_path,name:c.name,skillsDir:t.skillsDir});if(!l){i++;continue}if(c.content_sha256&&c.content_sha256===l.sha256){n++;continue}o.run(l.content,l.sha256,new Date().toISOString(),c.id),r++}})(),{total:s.length,filled:r,unchanged:n,missing:i}}exportSkillContent(t,e={}){const s=e.includeInactive?"":" WHERE is_active = 1",r=this.db.prepare(`SELECT name, content FROM skills${s} ORDER BY name`).all();let n=0,i=0,o=0;const a=new Set;for(const c of r){if(c.content==null){i++;continue}let l=J(c.name);if(!l){i++;continue}a.has(l)&&(l=`${l}-${z(c.name).slice(0,8)}`,o++),a.add(l);const p=d(t,l);O(p,{recursive:!0}),M(d(p,"SKILL.md"),c.content,"utf-8"),n++}return{total:r.length,written:n,skipped_no_content:i,collisions:o}}setSkillVisibility(t,e){return this.db.prepare("UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now') WHERE name = ? RETURNING *").get(e,t)}setAllSkillsVisibility(t,e={}){const s=[],r=[t];e.includeInactive||s.push("is_active = 1"),e.tier&&(s.push("tier = ?"),r.push(e.tier));const n=s.length?" WHERE "+s.join(" AND "):"";return this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now')${n}`).run(...r).changes}listSkills(t,e,s,r){let n="SELECT * FROM skills WHERE 1=1";const i=[];if(t!==void 0&&(n+=" AND is_active = ?",i.push(t?1:0)),e&&(n+=" AND category = ?",i.push(e)),r&&(n+=" AND tier = ?",i.push(r)),s){const o=h(s);o&&(n+=" AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)",i.push(o))}return n+=" ORDER BY updated_at DESC",this.db.prepare(n).all(...i)}searchSkills(t,e=20,s){const r=h(t);if(!r)return[];const n=s?" AND s.tier = ?":"",i=this.db.prepare(`
175
+ `).get(t,e,s,r||null,i||null,n||null,a||null,c,l,p,g,S,R),"registerSkill");if(!E.success)throw E.error||new Error("Failed to register skill");return E.data}getSkill(t){const e=this.db.prepare("SELECT * FROM skills WHERE name = ?").get(t);return e&&this.db.prepare("UPDATE skills SET last_used = datetime('now'), usage_count = usage_count + 1 WHERE id = ?").run(e.id),e}backfillSkillContent(t={}){const e=t.read??D,s=this.db.prepare("SELECT id, name, skill_path, content_sha256 FROM skills").all();let r=0,i=0,n=0;const a=this.db.prepare("UPDATE skills SET content = ?, content_sha256 = ?, content_updated_at = ?, updated_at = datetime('now') WHERE id = ?");return this.db.transaction(()=>{for(const c of s){const l=e({skillPath:c.skill_path,name:c.name,skillsDir:t.skillsDir});if(!l){n++;continue}if(c.content_sha256&&c.content_sha256===l.sha256){i++;continue}a.run(l.content,l.sha256,new Date().toISOString(),c.id),r++}})(),{total:s.length,filled:r,unchanged:i,missing:n}}exportSkillContent(t,e={}){const s=e.includeInactive?"":" WHERE is_active = 1",r=this.db.prepare(`SELECT name, content FROM skills${s} ORDER BY name`).all();let i=0,n=0,a=0;const o=new Set;for(const c of r){if(c.content==null){n++;continue}let l=Q(c.name);if(!l){n++;continue}o.has(l)&&(l=`${l}-${K(c.name).slice(0,8)}`,a++),o.add(l);const p=d(t,l);O(p,{recursive:!0}),w(d(p,"SKILL.md"),c.content,"utf-8"),i++}return{total:r.length,written:i,skipped_no_content:n,collisions:a}}setSkillVisibility(t,e){return this.db.prepare("UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now') WHERE name = ? RETURNING *").get(e,t)}setAllSkillsVisibility(t,e={}){const s=[],r=[t];e.includeInactive||s.push("is_active = 1"),e.tier&&(s.push("tier = ?"),r.push(e.tier));const i=s.length?" WHERE "+s.join(" AND "):"";return this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now')${i}`).run(...r).changes}setRowVisibility(t,e,s){const r=ee[t];if(!r)throw new Error(`Unknown visibility kind: ${String(t)} (use truth|memory|quest)`);if(!te.has(s))throw new Error(`Unknown visibility: ${String(s)} (use within|org|public)`);const i=this.db.prepare(r.select).get(e),n={kind:t,id:e,visibility:s};if(!i)return{...n,outcome:"not_found",projectId:null,projectName:null,syncPolicy:null,egressEligible:!1};const a={projectId:i.pid,projectName:i.pname,syncPolicy:i.policy,previous:i.vis},o=c=>c!=="within"&&(i.pid==null||i.policy==="cloud"||i.policy==="team");return s!=="within"&&i.policy==="private"?{...n,...a,outcome:"refused_private_grove",egressEligible:!1}:i.vis===s?{...n,...a,outcome:"unchanged",egressEligible:o(s)}:(this.db.prepare(r.update).run(s,e),{...n,...a,outcome:"updated",egressEligible:o(s)})}listSkills(t,e,s,r){let i="SELECT * FROM skills WHERE 1=1";const n=[];if(t!==void 0&&(i+=" AND is_active = ?",n.push(t?1:0)),e&&(i+=" AND category = ?",n.push(e)),r&&(i+=" AND tier = ?",n.push(r)),s){const a=h(s);a&&(i+=" AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)",n.push(a))}return i+=" ORDER BY updated_at DESC",this.db.prepare(i).all(...n)}searchSkills(t,e=20,s){const r=h(t);if(!r)return[];const i=s?" AND s.tier = ?":"",n=this.db.prepare(`
173
176
  SELECT s.* FROM skills s
174
177
  JOIN skills_fts ON s.id = skills_fts.rowid
175
- WHERE skills_fts MATCH ?${n}
178
+ WHERE skills_fts MATCH ?${i}
176
179
  ORDER BY rank
177
180
  LIMIT ?
178
- `);return s?i.all(r,s,e):i.all(r,e)}getSkillGraph(t){const e=this.db.prepare("SELECT * FROM skills").all(),s=new Map;for(const a of e)s.set(a.name,a);const r=a=>{if(!a)return[];try{const c=JSON.parse(a);return Array.isArray(c)?c.filter(l=>typeof l=="string"):[]}catch{return[]}},n=(a,c)=>{const l=s.get(a);if(!l)return null;const p=r(l.governs),_=r(l.composes),g={name:l.name,tier:l.tier??"atomic",description:l.description,governs:p,composes:_,children:[]},f=[];if(!c.has(a)){const R=new Set(c).add(a);for(const E of p){const b=n(E,R);b?g.children.push(b):f.push(E)}}return f.length&&(g.missing=f),g};if(t){const a=n(t,new Set);return a?[a]:[]}const i=e.filter(a=>(a.tier??"atomic")==="god").map(a=>a.name).sort(),o=[];for(const a of i){const c=n(a,new Set);c&&o.push(c)}return o}updateSkill(t,e){const s=[],r=[];return e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.skill_path!==void 0&&(s.push("skill_path = ?"),r.push(e.skill_path)),e.category!==void 0&&(s.push("category = ?"),r.push(e.category)),e.is_active!==void 0&&(s.push("is_active = ?"),r.push(e.is_active?1:0)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),e.version!==void 0&&(s.push("version = ?"),r.push(e.version)),s.length===0?this.getSkill(t):(s.push("updated_at = datetime('now')"),r.push(t),this.db.prepare(`
181
+ `);return s?n.all(r,s,e):n.all(r,e)}getSkillGraph(t){const e=this.db.prepare("SELECT * FROM skills").all(),s=new Map;for(const o of e)s.set(o.name,o);const r=o=>{if(!o)return[];try{const c=JSON.parse(o);return Array.isArray(c)?c.filter(l=>typeof l=="string"):[]}catch{return[]}},i=(o,c)=>{const l=s.get(o);if(!l)return null;const p=r(l.governs),_=r(l.composes),g={name:l.name,tier:l.tier??"atomic",description:l.description,governs:p,composes:_,children:[]},S=[];if(!c.has(o)){const R=new Set(c).add(o);for(const E of p){const b=i(E,R);b?g.children.push(b):S.push(E)}}return S.length&&(g.missing=S),g};if(t){const o=i(t,new Set);return o?[o]:[]}const n=e.filter(o=>(o.tier??"atomic")==="god").map(o=>o.name).sort(),a=[];for(const o of n){const c=i(o,new Set);c&&a.push(c)}return a}updateSkill(t,e){const s=[],r=[];return e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.skill_path!==void 0&&(s.push("skill_path = ?"),r.push(e.skill_path)),e.category!==void 0&&(s.push("category = ?"),r.push(e.category)),e.is_active!==void 0&&(s.push("is_active = ?"),r.push(e.is_active?1:0)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),e.version!==void 0&&(s.push("version = ?"),r.push(e.version)),s.length===0?this.getSkill(t):(s.push("updated_at = datetime('now')"),r.push(t),this.db.prepare(`
179
182
  UPDATE skills SET ${s.join(", ")} WHERE name = ? RETURNING *
180
183
  `).get(...r))}deleteSkill(t){return this.db.prepare("DELETE FROM skills WHERE name = ?").run(t).changes>0}deactivateSkill(t){return this.updateSkill(t,{is_active:!1})}activateSkill(t){return this.updateSkill(t,{is_active:!0})}getSkillStats(){const t=this.db.prepare("SELECT COUNT(*) as count FROM skills").get().count,e=this.db.prepare("SELECT COUNT(*) as count FROM skills WHERE is_active = 1").get().count,s=this.db.prepare(`
181
184
  SELECT category, COUNT(*) as count FROM skills WHERE category IS NOT NULL GROUP BY category
182
- `).all(),r={};for(const n of s)r[n.category]=n.count;return{total:t,active:e,byCategory:r}}upsertSpec(t,e,s,r,n=0){return this.db.prepare(`
185
+ `).all(),r={};for(const i of s)r[i.category]=i.count;return{total:t,active:e,byCategory:r}}upsertSpec(t,e,s,r,i=0){return this.db.prepare(`
183
186
  INSERT INTO specs (project_id, spec_dir, title, summary, task_count)
184
187
  VALUES (?, ?, ?, ?, ?)
185
188
  ON CONFLICT(project_id, spec_dir) DO UPDATE SET
@@ -188,19 +191,19 @@ import{openDatabase as D}from"./storage/index.js";import{existsSync as u,mkdirSy
188
191
  task_count = excluded.task_count,
189
192
  updated_at = datetime('now')
190
193
  RETURNING *
191
- `).get(t,e,s||null,r||null,n)}getSpec(t,e){return this.db.prepare("SELECT * FROM specs WHERE project_id = ? AND spec_dir = ?").get(t,e)}listSpecs(t){return t!==void 0?this.db.prepare("SELECT * FROM specs WHERE project_id = ? ORDER BY updated_at DESC").all(t):this.db.prepare("SELECT * FROM specs ORDER BY updated_at DESC").all()}findQuestBySpecSignature(t,e){return this.db.prepare(`
194
+ `).get(t,e,s||null,r||null,i)}getSpec(t,e){return this.db.prepare("SELECT * FROM specs WHERE project_id = ? AND spec_dir = ?").get(t,e)}listSpecs(t){return t!==void 0?this.db.prepare("SELECT * FROM specs WHERE project_id = ? ORDER BY updated_at DESC").all(t):this.db.prepare("SELECT * FROM specs ORDER BY updated_at DESC").all()}findQuestBySpecSignature(t,e){return this.db.prepare(`
192
195
  SELECT * FROM quests
193
196
  WHERE project_id = ?
194
197
  AND (tags = ? OR tags LIKE ? OR tags LIKE ? OR tags LIKE ?)
195
198
  LIMIT 1
196
- `).get(t,e,`${e},%`,`%,${e}`,`%,${e},%`)}updateQuestFields(t,e){const s=[],r=[];if(e.title!==void 0&&(s.push("title = ?"),r.push(e.title)),e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),s.length===0)return this.db.prepare("SELECT * FROM quests WHERE id = ?").get(t);s.push("updated_at = datetime('now')"),r.push(t);const n=this.db.prepare(`UPDATE quests SET ${s.join(", ")} WHERE id = ? RETURNING *`).get(...r);return n&&m(this.db,{projectId:n.project_id,kind:"quest",refTable:"quests",refId:t}),n}insertData(t,e,s,r,n){const i=this.resilience.withRetrySync(()=>this.db.prepare(`
199
+ `).get(t,e,`${e},%`,`%,${e}`,`%,${e},%`)}updateQuestFields(t,e){const s=[],r=[];if(e.title!==void 0&&(s.push("title = ?"),r.push(e.title)),e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),s.length===0)return this.db.prepare("SELECT * FROM quests WHERE id = ?").get(t);s.push("updated_at = datetime('now')"),r.push(t);const i=this.db.prepare(`UPDATE quests SET ${s.join(", ")} WHERE id = ? RETURNING *`).get(...r);return i&&m(this.db,{projectId:i.project_id,kind:"quest",refTable:"quests",refId:t}),i}insertData(t,e,s,r,i){const n=this.resilience.withRetrySync(()=>this.db.prepare(`
197
200
  INSERT INTO data_lake (project_id, category, key, value, metadata)
198
201
  VALUES (?, ?, ?, ?, ?)
199
202
  RETURNING *
200
- `).get(t,e,s,r,n?JSON.stringify(n):null),"insertData");if(!i.success)throw i.error||new Error("Insert data failed");return i.data}batchWrites(t,e){return this.db.transaction(r=>r.map(n=>e(n)))(t)}insertDataBatch(t){const e=this.resilience.generateOperationId("batch_insert"),s=this.BATCH_SIZE;let r=0;this.resilience.createCheckpoint(e,"batch_insert","started",{totalItems:t.length,batchSize:s});const n=this.db.prepare(`
203
+ `).get(t,e,s,r,i?JSON.stringify(i):null),"insertData");if(!n.success)throw n.error||new Error("Insert data failed");return n.data}batchWrites(t,e){return this.db.transaction(r=>r.map(i=>e(i)))(t)}insertDataBatch(t){const e=this.resilience.generateOperationId("batch_insert"),s=this.BATCH_SIZE;let r=0;this.resilience.createCheckpoint(e,"batch_insert","started",{totalItems:t.length,batchSize:s});const i=this.db.prepare(`
201
204
  INSERT INTO data_lake (project_id, category, key, value, metadata)
202
205
  VALUES (?, ?, ?, ?, ?)
203
- `);try{for(let i=0;i<t.length;i+=s){const o=t.slice(i,i+s),a=Math.floor(i/s)+1;this.resilience.updateCheckpoint(e,`batch_${a}`,{processed:i,currentBatch:a});const c=this.resilience.withRetrySync(()=>this.batchWrites(o,l=>(n.run(l.projectId,l.category,l.key,l.value,l.metadata?JSON.stringify(l.metadata):null),1)).length,`batch_insert_${a}`,{maxAttempts:3});if(!c.success){this.logger.error("Batch insert failed",{batch:a,processed:r,error:c.error?.message}),this.resilience.updateCheckpoint(e,"partial_failure",{inserted:r,failedAt:i});const l=c.error??new Error(`Batch insert failed at chunk ${a}`);throw l.inserted=r,l}r+=c.data}return this.resilience.completeCheckpoint(e),r}catch(i){throw i!==null&&typeof i=="object"&&"inserted"in i||(this.logger.error("Batch insert exception",{inserted:r,error:i.message}),this.resilience.updateCheckpoint(e,"exception",{inserted:r,error:i.message}),i!==null&&typeof i=="object"&&(i.inserted=r)),i}}queryData(t,e,s=100,r=0){return e?this.db.prepare(`
206
+ `);try{for(let n=0;n<t.length;n+=s){const a=t.slice(n,n+s),o=Math.floor(n/s)+1;this.resilience.updateCheckpoint(e,`batch_${o}`,{processed:n,currentBatch:o});const c=this.resilience.withRetrySync(()=>this.batchWrites(a,l=>(i.run(l.projectId,l.category,l.key,l.value,l.metadata?JSON.stringify(l.metadata):null),1)).length,`batch_insert_${o}`,{maxAttempts:3});if(!c.success){this.logger.error("Batch insert failed",{batch:o,processed:r,error:c.error?.message}),this.resilience.updateCheckpoint(e,"partial_failure",{inserted:r,failedAt:n});const l=c.error??new Error(`Batch insert failed at chunk ${o}`);throw l.inserted=r,l}r+=c.data}return this.resilience.completeCheckpoint(e),r}catch(n){throw n!==null&&typeof n=="object"&&"inserted"in n||(this.logger.error("Batch insert exception",{inserted:r,error:n.message}),this.resilience.updateCheckpoint(e,"exception",{inserted:r,error:n.message}),n!==null&&typeof n=="object"&&(n.inserted=r)),n}}queryData(t,e,s=100,r=0){return e?this.db.prepare(`
204
207
  SELECT * FROM data_lake
205
208
  WHERE project_id = ? AND category = ?
206
209
  ORDER BY created_at DESC
@@ -242,8 +245,8 @@ import{openDatabase as D}from"./storage/index.js";import{existsSync as u,mkdirSy
242
245
  WHERE project_id = ?
243
246
  GROUP BY category
244
247
  ORDER BY count DESC
245
- `).all(t)}forgetData(t){const e=[],s=[];if(t.match){e.push("(key LIKE ? OR value LIKE ?)");const a=`%${t.match}%`;s.push(a,a)}if(t.dataset&&(e.push("category = ?"),s.push(t.dataset)),typeof t.olderThanDays=="number"&&(e.push("created_at < datetime('now', ?)"),s.push(`-${Math.max(0,Math.floor(t.olderThanDays))} days`)),t.projectId!=null&&(e.push("project_id = ?"),s.push(t.projectId)),e.length===0)return{matched:0,deleted:0,samples:[]};const r=e.join(" AND "),n=this.db.prepare(`SELECT COUNT(*) AS n FROM data_lake WHERE ${r}`).get(...s).n,i=this.db.prepare(`SELECT category, key, created_at FROM data_lake WHERE ${r} ORDER BY created_at LIMIT 5`).all(...s);if(t.dryRun!==!1)return{matched:n,deleted:0,samples:i};const o=this.db.prepare(`DELETE FROM data_lake WHERE ${r}`).run(...s).changes;return{matched:n,deleted:o,samples:i}}deleteDataCategory(t,e){return this.db.prepare(`
248
+ `).all(t)}forgetData(t){const e=[],s=[];if(t.match){e.push("(key LIKE ? OR value LIKE ?)");const o=`%${t.match}%`;s.push(o,o)}if(t.dataset&&(e.push("category = ?"),s.push(t.dataset)),typeof t.olderThanDays=="number"&&(e.push("created_at < datetime('now', ?)"),s.push(`-${Math.max(0,Math.floor(t.olderThanDays))} days`)),t.projectId!=null&&(e.push("project_id = ?"),s.push(t.projectId)),e.length===0)return{matched:0,deleted:0,samples:[]};const r=e.join(" AND "),i=this.db.prepare(`SELECT COUNT(*) AS n FROM data_lake WHERE ${r}`).get(...s).n,n=this.db.prepare(`SELECT category, key, created_at FROM data_lake WHERE ${r} ORDER BY created_at LIMIT 5`).all(...s);if(t.dryRun!==!1)return{matched:i,deleted:0,samples:n};const a=this.db.prepare(`DELETE FROM data_lake WHERE ${r}`).run(...s).changes;return{matched:i,deleted:a,samples:n}}deleteDataCategory(t,e){return this.db.prepare(`
246
249
  DELETE FROM data_lake WHERE project_id = ? AND category = ?
247
250
  `).run(t,e).changes}*streamSessions(t){const e=this.db.prepare(`
248
251
  SELECT * FROM sessions WHERE project_id = ? ORDER BY date DESC
249
- `);for(const s of e.iterate(t))yield s}*streamData(t,e){const s=e?this.db.prepare("SELECT * FROM data_lake WHERE project_id = ? AND category = ?"):this.db.prepare("SELECT * FROM data_lake WHERE project_id = ?"),r=e?[t,e]:[t];for(const n of s.iterate(...r))yield n}getStats(){const t=this.db.prepare("SELECT COUNT(*) as count FROM projects").get(),e=this.db.prepare("SELECT COUNT(*) as count FROM sessions").get(),s=this.db.prepare("SELECT COUNT(*) as count FROM quests").get(),r=this.db.prepare("SELECT COUNT(*) as count FROM memory_artifacts").get(),n=this.db.prepare("SELECT COUNT(*) as count FROM data_lake").get(),i=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE is_archived = 0").get(),o=this.db.pragma("page_count",{simple:!0}),a=this.db.pragma("page_size",{simple:!0}),c=o*a/(1024*1024);return{projects:t.count,sessions:e.count,quests:s.count,artifacts:r.count,dataPoints:n.count,totalTokens:i.total,dbSize:`${c.toFixed(2)} MB`}}getProjectStats(t){const e=this.db.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(t),s=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status IN ('pending', 'in_progress')").get(t),r=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status = 'completed'").get(t),n=this.db.prepare("SELECT COUNT(*) as count FROM data_lake WHERE project_id = ?").get(t),i=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE project_id = ? AND is_archived = 0").get(t);return{sessions:e.count,quests:{pending:s.count,completed:r.count},dataPoints:n.count,tokens:i.total}}estimateTokens(t){return Math.ceil(t.length/4)}vacuum(){this.db.exec("VACUUM")}checkpoint(){this.db.pragma("wal_checkpoint(TRUNCATE)")}getResilienceStatus(){const t=this.resilience.getCircuitStatus(),e=this.resilience.getIncompleteOperations();return{circuitState:t.state,failures:t.failures,incompleteOps:e.length}}resetCircuitBreaker(){this.resilience.resetCircuit()}close(){try{this.checkpoint(),this.logger.info("Database checkpoint completed")}catch(t){this.logger.error("Checkpoint failed during close",{error:t.message})}try{this.db.close(),this.logger.info("Database closed successfully")}catch(t){this.logger.error("Database close failed",{error:t.message})}}checkIntegrity(){const t=[];try{const e=this.db.pragma("integrity_check",{simple:!0});e!=="ok"&&t.push(`Integrity check failed: ${e}`)}catch(e){t.push(`Integrity check error: ${e.message}`)}try{const e=this.db.pragma("foreign_key_check");e.length>0&&t.push(`Foreign key violations: ${e.length}`)}catch(e){t.push(`FK check error: ${e.message}`)}return{ok:t.length===0,issues:t}}}export{pe as WyrmDB,ue as resolveDbPath,K as tightenStorePermissions};
252
+ `);for(const s of e.iterate(t))yield s}*streamData(t,e){const s=e?this.db.prepare("SELECT * FROM data_lake WHERE project_id = ? AND category = ?"):this.db.prepare("SELECT * FROM data_lake WHERE project_id = ?"),r=e?[t,e]:[t];for(const i of s.iterate(...r))yield i}getStats(){const t=this.db.prepare("SELECT COUNT(*) as count FROM projects").get(),e=this.db.prepare("SELECT COUNT(*) as count FROM sessions").get(),s=this.db.prepare("SELECT COUNT(*) as count FROM quests").get(),r=this.db.prepare("SELECT COUNT(*) as count FROM memory_artifacts").get(),i=this.db.prepare("SELECT COUNT(*) as count FROM data_lake").get(),n=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE is_archived = 0").get(),a=this.db.pragma("page_count",{simple:!0}),o=this.db.pragma("page_size",{simple:!0}),c=a*o/(1024*1024);return{projects:t.count,sessions:e.count,quests:s.count,artifacts:r.count,dataPoints:i.count,totalTokens:n.total,dbSize:`${c.toFixed(2)} MB`}}getProjectStats(t){const e=this.db.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(t),s=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status IN ('pending', 'in_progress')").get(t),r=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status = 'completed'").get(t),i=this.db.prepare("SELECT COUNT(*) as count FROM data_lake WHERE project_id = ?").get(t),n=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE project_id = ? AND is_archived = 0").get(t);return{sessions:e.count,quests:{pending:s.count,completed:r.count},dataPoints:i.count,tokens:n.total}}estimateTokens(t){return Math.ceil(t.length/4)}vacuum(){this.db.exec("VACUUM")}checkpoint(){this.db.pragma("wal_checkpoint(TRUNCATE)")}getResilienceStatus(){const t=this.resilience.getCircuitStatus(),e=this.resilience.getIncompleteOperations();return{circuitState:t.state,failures:t.failures,incompleteOps:e.length}}resetCircuitBreaker(){this.resilience.resetCircuit()}close(){try{this.checkpoint(),this.logger.info("Database checkpoint completed")}catch(t){this.logger.error("Checkpoint failed during close",{error:t.message})}try{this.db.close(),this.logger.info("Database closed successfully")}catch(t){this.logger.error("Database close failed",{error:t.message})}}checkIntegrity(){const t=[];try{const e=this.db.pragma("integrity_check",{simple:!0});e!=="ok"&&t.push(`Integrity check failed: ${e}`)}catch(e){t.push(`Integrity check error: ${e.message}`)}try{const e=this.db.pragma("foreign_key_check");e.length>0&&t.push(`Foreign key violations: ${e.length}`)}catch(e){t.push(`FK check error: ${e.message}`)}return{ok:t.length===0,issues:t}}}export{_e as WyrmDB,me as resolveDbPath,X as tightenStorePermissions};