wyrm-mcp 8.7.5 → 9.0.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/README.md CHANGED
@@ -1,11 +1,3 @@
1
- <p align="center">
2
- <img src="https://raw.githubusercontent.com/Ghosts-Protocol-Pvt-Ltd/wyrm-mcp/main/assets/dragon-mark.svg" alt="Wyrm" width="96" />
3
- </p>
4
-
5
- <p align="center">
6
- <img src="https://raw.githubusercontent.com/Ghosts-Protocol-Pvt-Ltd/wyrm-mcp/main/assets/hero.svg" alt="Wyrm — memory that survives the session" width="100%" />
7
- </p>
8
-
9
1
  <p align="center">
10
2
  <a href="https://www.npmjs.com/package/wyrm-mcp"><img src="https://img.shields.io/npm/v/wyrm-mcp?style=flat-square&labelColor=050505&color=c9ccd1&label=npm" alt="npm version" /></a>
11
3
  <a href="https://www.npmjs.com/package/wyrm-mcp"><img src="https://img.shields.io/npm/dm/wyrm-mcp?style=flat-square&labelColor=050505&color=c9ccd1&label=downloads" alt="npm downloads" /></a>
@@ -43,7 +35,7 @@ npm install -g wyrm-mcp # install
43
35
  wyrm-setup # wire it into your AI clients, then restart them
44
36
  ```
45
37
 
46
- > **On npm v12+**, npm denies dependency install scripts by default, which skips `better-sqlite3`'s native build — the install *succeeds* but `wyrm` then fails with *"Could not locate the bindings file"*. Install with the build allow-listed instead: `npm install -g wyrm-mcp --allow-scripts=wyrm-mcp,better-sqlite3` (`wyrm update` already does this for you). See [TROUBLESHOOTING.md](https://github.com/Ghosts-Protocol-Pvt-Ltd/Wyrm/blob/main/TROUBLESHOOTING.md).
38
+ > **On npm v12+**, npm denies dependency install scripts by default, which skips `better-sqlite3`'s native build — the install *succeeds* but `wyrm` then fails with *"Could not locate the bindings file"*. Install with the build allow-listed instead: `npm install -g wyrm-mcp --allow-scripts=wyrm-mcp,better-sqlite3` (`wyrm update` already does this for you). See [TROUBLESHOOTING.md](https://github.com/Ghosts-Protocol-Pvt-Ltd/wyrm-mcp/blob/HEAD/TROUBLESHOOTING.md).
47
39
 
48
40
  Then, from inside your client, ask it to call `wyrm_capabilities` to confirm the connection. The everyday loop is four steps the agent runs on its own once the habit sets in:
49
41
 
@@ -56,10 +48,6 @@ capture → store durable facts, lessons, and tasks as you go
56
48
 
57
49
  ## Measured, not asserted
58
50
 
59
- <p align="center">
60
- <img src="https://raw.githubusercontent.com/Ghosts-Protocol-Pvt-Ltd/wyrm-mcp/main/assets/receipt.svg" alt="Wyrm measured receipt" width="720" />
61
- </p>
62
-
63
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.
64
52
 
65
53
  ## Why Wyrm
@@ -74,7 +62,7 @@ Most memory tools store successes. Wyrm also records dead-ends and blocks the re
74
62
 
75
63
  ### Local-first, and honest about egress
76
64
 
77
- By default no memory data leaves your machine. The database is a single SQLite file at `~/.wyrm/wyrm.db`. The only default outbound calls are a daily npm version poll and, on installs with a license key, a signed revocation-list fetch — both listed with their off switches in [`docs/EGRESS.md`](../../docs/EGRESS.md); the free tier's one-time model download is a plain fetch that sends nothing. When you do opt into a hosted embedding path, Wyrm reports exactly what left and where, in a determinism receipt and on its health endpoint. The privacy claim is one you can verify from the runtime, not just the docs.
65
+ By default no memory data leaves your machine. The database is a single SQLite file at `~/.wyrm/wyrm.db`. The only default outbound calls are a daily npm version poll and, on installs with a license key, a signed revocation-list fetch — both off with `WYRM_NO_VERSION_CHECK=1` and `WYRM_LICENSE_REFRESH=0` respectively; the free tier's one-time model download is a plain fetch that sends nothing. `docs/EGRESS.md` in the repository enumerates every destination, and if you find an outbound call that is not on that table, that is a bug worth reporting. When you do opt into a hosted embedding path, Wyrm reports exactly what left and where, in a determinism receipt and on its health endpoint. The privacy claim is one you can verify from the runtime, not just the docs.
78
66
 
79
67
  ### Every write leaves a receipt
80
68
 
@@ -118,7 +106,7 @@ Wyrm is **free to use forever** — the local memory, the firewall, the recall,
118
106
  - **Team** — $199/mo · shared team memory, up to 25 seats, admin dashboard
119
107
  - **Enterprise** — $499/mo · unlimited seats, SSO/SAML, custom SLA, on-premise option
120
108
 
121
- Plans and self-serve checkout: **[account.ghosts.lk/pricing](https://account.ghosts.lk/pricing)**. Custom or on-premise, email [ryan@ghosts.lk](mailto:ryan@ghosts.lk).
109
+ Plans and self-serve checkout: **[account.ghosts.lk/pricing](https://ghosts.lk/pricing)**. Custom or on-premise, email [ryan@ghosts.lk](mailto:ryan@ghosts.lk).
122
110
 
123
111
  ## Community and feedback
124
112
 
@@ -1,18 +1,18 @@
1
- import{existsSync as d,readFileSync as p,writeFileSync as l,mkdirSync as P,copyFileSync as q,chmodSync as j}from"fs";import{homedir as m,platform as C}from"os";import{join as c,dirname as w,resolve as V}from"path";import{fileURLToPath as N}from"url";import{spawnSync as v}from"child_process";function Q(){const e=m(),t=C(),o=t==="darwin"?c(e,"Library","Application Support","Code","User"):t==="win32"?c(e,"AppData","Roaming","Code","User"):c(e,".config","Code","User"),n=t==="darwin"?c(e,"Library","Application Support","Code - Insiders","User"):t==="win32"?c(e,"AppData","Roaming","Code - Insiders","User"):c(e,".config","Code - Insiders","User"),s=t==="darwin"?c(e,"Library","Application Support","Claude"):t==="win32"?c(e,"AppData","Roaming","Claude"):c(e,".config","claude"),r=t==="darwin"?c(e,".cursor"):t==="win32"?c(e,".cursor"):c(e,".cursor"),i=t==="darwin"?c(e,".codeium","windsurf"):t==="win32"?c(e,".codeium","windsurf"):c(e,".codeium","windsurf"),u=t==="darwin"?c(e,".config","zed"):t==="win32"?c(e,"AppData","Roaming","Zed"):c(e,".config","zed"),a=c(e,".continue");return[{id:"vscode-copilot",name:"VS Code (Copilot)",icon:"\u{1F4BB}",configPath:c(o,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"vscode-insiders",name:"VS Code Insiders",icon:"\u{1F7E2}",configPath:c(n,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"claude-desktop",name:"Claude Desktop",icon:"\u{1F916}",configPath:c(s,"claude_desktop_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"claude-code",name:"Claude Code",icon:"\u2733\uFE0F",configPath:c(e,".claude.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"cursor",name:"Cursor",icon:"\u{1F4D0}",configPath:c(r,"mcp.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"windsurf",name:"Windsurf",icon:"\u{1F3C4}",configPath:c(i,"mcp_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"zed",name:"Zed",icon:"\u26A1",configPath:c(u,"settings.json"),configFormat:"zed",mcpKey:"context_servers",detected:!1,configured:!1},{id:"continue",name:"Continue",icon:"\u{1F504}",configPath:c(a,"config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1}]}function Z(e){try{const t=C()==="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 b(){const e=Q();for(const t of e){if(t.id==="claude-code")t.detected=d(t.configPath)||Z("claude");else{const o=w(t.configPath);t.detected=d(o)}if(t.detected&&d(t.configPath))try{const o=p(t.configPath,"utf-8"),n=$(o);t.configured=ee(n,t)}catch{t.configured=!1}t.detected&&(t.version=X(t))}return e}function X(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(`
1
+ import{existsSync as d,readFileSync as p,writeFileSync as l,mkdirSync as P,copyFileSync as q,chmodSync as j}from"fs";import{homedir as m,platform as C}from"os";import{join as c,dirname as w,resolve as V}from"path";import{fileURLToPath as T}from"url";import{spawnSync as v}from"child_process";function Q(){const e=m(),t=C(),o=t==="darwin"?c(e,"Library","Application Support","Code","User"):t==="win32"?c(e,"AppData","Roaming","Code","User"):c(e,".config","Code","User"),n=t==="darwin"?c(e,"Library","Application Support","Code - Insiders","User"):t==="win32"?c(e,"AppData","Roaming","Code - Insiders","User"):c(e,".config","Code - Insiders","User"),s=t==="darwin"?c(e,"Library","Application Support","Claude"):t==="win32"?c(e,"AppData","Roaming","Claude"):c(e,".config","claude"),r=t==="darwin"?c(e,".cursor"):t==="win32"?c(e,".cursor"):c(e,".cursor"),i=t==="darwin"?c(e,".codeium","windsurf"):t==="win32"?c(e,".codeium","windsurf"):c(e,".codeium","windsurf"),u=t==="darwin"?c(e,".config","zed"):t==="win32"?c(e,"AppData","Roaming","Zed"):c(e,".config","zed"),a=c(e,".continue");return[{id:"vscode-copilot",name:"VS Code (Copilot)",icon:"\u{1F4BB}",configPath:c(o,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"vscode-insiders",name:"VS Code Insiders",icon:"\u{1F7E2}",configPath:c(n,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"claude-desktop",name:"Claude Desktop",icon:"\u{1F916}",configPath:c(s,"claude_desktop_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"claude-code",name:"Claude Code",icon:"\u2733\uFE0F",configPath:c(e,".claude.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"cursor",name:"Cursor",icon:"\u{1F4D0}",configPath:c(r,"mcp.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"windsurf",name:"Windsurf",icon:"\u{1F3C4}",configPath:c(i,"mcp_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"zed",name:"Zed",icon:"\u26A1",configPath:c(u,"settings.json"),configFormat:"zed",mcpKey:"context_servers",detected:!1,configured:!1},{id:"continue",name:"Continue",icon:"\u{1F504}",configPath:c(a,"config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1}]}function Z(e){try{const t=C()==="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 S(){const e=Q();for(const t of e){if(t.id==="claude-code")t.detected=d(t.configPath)||Z("claude");else{const o=w(t.configPath);t.detected=d(o)}if(t.detected&&d(t.configPath))try{const o=p(t.configPath,"utf-8"),n=$(o);t.configured=ee(n,t)}catch{t.configured=!1}t.detected&&(t.version=X(t))}return e}function X(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
2
  `)[0];break}case"cursor":{const t=v("cursor",["--version"],{encoding:"utf-8",timeout:5e3});if(t.stdout)return t.stdout.split(`
3
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 ee(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 T(){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=c(s,"wyrm-mcp","dist","index.js");if(d(r))return r}}catch{}const e=V(__dirname,"index.js");if(d(e))return e;const t=c(m(),".wyrm","node_modules","wyrm-mcp","dist","index.js");if(d(t))return t;const o=[c(m(),"Git Projects","Wyrm","packages","mcp-server","dist","index.js"),c(m(),"projects","Wyrm","packages","mcp-server","dist","index.js"),c(m(),"dev","Wyrm","packages","mcp-server","dist","index.js")];for(const n of o)if(d(n))return n;return"wyrm-mcp"}function D(){return c(m(),".wyrm","wyrm.db")}function ye(e="mcp-json",t){const o=t?.serverPath??T(),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 te(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 U(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 a=p(e.configPath,"utf-8");n=$(a),s=!0}catch{n={}}let r;if(s){r=`${e.configPath}.wyrm-backup`,l(r,p(e.configPath),{mode:384});try{j(r,384)}catch{}}const i=te(e,t);switch(e.configFormat){case"vscode":{n.mcp||(n.mcp={});const a=n.mcp;a.servers||(a.servers={});const f=a.servers;f.wyrm=i;break}case"mcp-json":{n[e.mcpKey]||(n[e.mcpKey]={});const a=n[e.mcpKey];a.wyrm=i;break}case"zed":{n[e.mcpKey]||(n[e.mcpKey]={});const a=n[e.mcpKey];a.wyrm=i;break}}l(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 we(e,t){try{if(!d(e.configPath))return{client:e,action:"skipped",message:"No config file"};const o=p(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`;l(i,o,{encoding:"utf-8",mode:384});try{j(i,384)}catch{}return r.env={...r.env,...t},l(e.configPath,JSON.stringify(n,null,2)+`
4
+ `)[0];break}}}catch{}}function ee(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 N(){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=c(s,"wyrm-mcp","dist","index.js");if(d(r))return r}}catch{}const e=V(__dirname,"index.js");if(d(e))return e;const t=c(m(),".wyrm","node_modules","wyrm-mcp","dist","index.js");if(d(t))return t;const o=[c(m(),"Git Projects","Wyrm","packages","mcp-server","dist","index.js"),c(m(),"projects","Wyrm","packages","mcp-server","dist","index.js"),c(m(),"dev","Wyrm","packages","mcp-server","dist","index.js")];for(const n of o)if(d(n))return n;return"wyrm-mcp"}function D(){return c(m(),".wyrm","wyrm.db")}function we(e="mcp-json",t){const o=t?.serverPath??N(),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 te(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 M(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 a=p(e.configPath,"utf-8");n=$(a),s=!0}catch{n={}}let r;if(s){r=`${e.configPath}.wyrm-backup`,l(r,p(e.configPath),{mode:384});try{j(r,384)}catch{}}const i=te(e,t);switch(e.configFormat){case"vscode":{n.mcp||(n.mcp={});const a=n.mcp;a.servers||(a.servers={});const f=a.servers;f.wyrm=i;break}case"mcp-json":{n[e.mcpKey]||(n[e.mcpKey]={});const a=n[e.mcpKey];a.wyrm=i;break}case"zed":{n[e.mcpKey]||(n[e.mcpKey]={});const a=n[e.mcpKey];a.wyrm=i;break}}l(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 ve(e,t){try{if(!d(e.configPath))return{client:e,action:"skipped",message:"No config file"};const o=p(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`;l(i,o,{encoding:"utf-8",mode:384});try{j(i,384)}catch{}return r.env={...r.env,...t},l(e.configPath,JSON.stringify(n,null,2)+`
6
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 oe(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=p(e.configPath,"utf-8"),o=$(t),n=`${e.configPath}.wyrm-backup`;l(n,t,{mode:384});try{j(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 l(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 A(){return{id:"claude-code-hooks",name:"Claude Code (auto-memory)",icon:"\u{1FA9D}",configPath:c(m(),".claude","settings.json"),configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}const R=[{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 ne(){const e=c(m(),".claude");if(!d(e))return null;const t=A();try{const o=c(e,"hooks");d(o)||P(o,{recursive:!0});const n=w(N(import.meta.url)),s=c(n,"..","scripts","hooks"),r=t.configPath;let i={};if(d(r))try{i=JSON.parse(p(r,"utf-8"))}catch{i={}}const u=i.hooks??{};let a=!1;const f=[];for(const h of R){const g=c(s,h.script);if(!d(g)){f.push(h.script);continue}const W=c(o,h.script);q(g,W);try{j(W,493)}catch{}const O=`node ${W}`;for(const k of h.events){const x=typeof k=="string"?k:k.event,M=typeof k=="string"?void 0:k.matcher,E=Array.isArray(u[x])?u[x]:[];if(!E.some(S=>Array.isArray(S.hooks)&&S.hooks.some(Y=>Y.command===O))){const S=M?{matcher:M,hooks:[{type:"command",command:O,timeout:30}]}:{hooks:[{type:"command",command:O,timeout:30}]};E.push(S),a=!0}u[x]=E}}i.hooks=u,l(r,JSON.stringify(i,null,2)+`
8
- `,"utf-8"),t.configured=!0;const y=f.length?` (missing from package: ${f.join(", ")})`:"";return{client:t,action:a?"configured":"skipped",message:(a?"auto-memory hooks installed":"already installed")+y}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function ve(){const e=c(m(),".claude");if(!d(e))return null;const t=A();try{const o=w(N(import.meta.url)),n=`node ${c(o,"wyrm-statusline.js")}`,s=t.configPath;let r={};if(d(s))try{r=JSON.parse(p(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},l(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 ke(){const e=c(m(),".claude","settings.json");if(!d(e))return null;const t=A();try{const o=JSON.parse(p(e,"utf-8")),n=o.statusLine;return n?.command&&/wyrm-statusline/.test(n.command)?(delete o.statusLine,l(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 re(){const e=c(m(),".claude","settings.json");if(!d(e))return null;const t=A();try{const o=JSON.parse(p(e,"utf-8")),n=c(m(),".claude","hooks"),s=new Set(R.map(u=>`node ${c(n,u.script)}`)),r=new Set(R.flatMap(u=>u.events.map(a=>typeof a=="string"?a:a.event))),i=o.hooks;if(i){for(const u of r)Array.isArray(i[u])&&(i[u]=i[u].map(a=>({...a,hooks:Array.isArray(a.hooks)?a.hooks.filter(f=>!s.has(f.command??"")):a.hooks})).filter(a=>!Array.isArray(a.hooks)||a.hooks.length>0),i[u].length===0&&delete i[u]);Object.keys(i).length===0&&delete o.hooks}return l(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 L=/wyrm-guard/,se=["Bash","Edit|Write|MultiEdit|NotebookEdit"];function _(e){return e.settingsPath??process.env.CLAUDE_SETTINGS??c(m(),".claude","settings.json")}function J(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 B(e){return C()==="win32"?`"${e.replace(/"/g,'""')}"`:`'${e.replace(/'/g,"'\\''")}'`}function ie(){const e=w(N(import.meta.url)),t=c(e,"wyrm-guard.js");if(d(t))return`node ${B(t)}`;try{const o=C()==="win32"?"where":"which",s=v(o,["wyrm-guard"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
12
- `)[0]?.trim();if(s)return B(s)}catch{}return null}function I(e){const t=e.hooks;if(!t||typeof t!="object"||Array.isArray(t))return!1;let o=!1;for(const n of Object.keys(t)){const s=t[n];if(!Array.isArray(s))continue;let r=!1;const i=[];for(const u of s){const a=u;if(!a||typeof a!="object"||!Array.isArray(a.hooks)){i.push(u);continue}const f=a.hooks.filter(y=>!L.test(y?.command??""));if(f.length===a.hooks.length){i.push(u);continue}o=!0,r=!0,f.length>0&&i.push({...a,hooks:f})}r&&(i.length===0?delete t[n]:t[n]=i)}return o&&Object.keys(t).length===0&&delete e.hooks,o}function ce(e={}){if(!!!(e.settingsPath??process.env.CLAUDE_SETTINGS)&&!d(c(m(),".claude")))return{client:J(_(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=_(e),n=J(o);try{const s=e.guardCommand??ie();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 g=JSON.parse(p(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 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 a=JSON.stringify(r);I(r);const f=r.hooks??{},y=Array.isArray(f.PreToolUse)?f.PreToolUse:[];for(const g of se)y.push({matcher:g,hooks:[{type:"command",command:s,timeout:1}]});f.PreToolUse=y,r.hooks=f;const h=JSON.stringify(r)!==a;return h&&l(o,JSON.stringify(r,null,2)+`
13
- `,"utf-8"),n.configured=!0,{client:n,action:h?"configured":"skipped",message:h?"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 ae(e={}){const t=_(e);if(!d(t))return null;const o=J(t);try{let n;try{const r=JSON.parse(p(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 I(n)?(l(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 Pe(e={}){const t=_(e),o=[];try{const s=JSON.parse(p(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 a=u?.command;a&&L.test(a)&&o.push(a)}}catch{}return{settingsPath:t,installed:o.length>0,commands:o}}function ue(e){const t={serverPath:e?.serverPath||T(),dbPath:e?.dbPath||D(),httpPort:e?.httpPort},o=c(m(),".wyrm");d(o)||P(o,{recursive:!0}),H(t);const n=b(),s=[];for(const u of n)s.push(U(u,t));const r=ne();r&&s.push(r);const i=ce();return i&&s.push(i),s}function be(){const e=b(),t=[];for(const s of e)t.push(oe(s));const o=re();o&&t.push(o);const n=ae();return n&&t.push(n),t}function de(e,t){const o={serverPath:t?.serverPath||T(),dbPath:t?.dbPath||D(),httpPort:t?.httpPort};H(o);const n=b(),s=[];for(const r of n)e.includes(r.id)&&s.push(U(r,o));return s}function H(e){const t=c(m(),".wyrm","wyrm-config.json"),o=b().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};l(t,JSON.stringify(n,null,2)+`
15
- `,"utf-8")}function G(){const e=c(m(),".wyrm","wyrm-config.json");if(!d(e))return null;try{return JSON.parse(p(e,"utf-8"))}catch{return null}}function Se(){const e=G();return e?de(e.configuredClients,{serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort}):ue()}const z="<!-- wyrm:start -->",K="<!-- wyrm:end -->",F=`${z}
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 _(){return{id:"claude-code-hooks",name:"Claude Code (auto-memory)",icon:"\u{1FA9D}",configPath:c(m(),".claude","settings.json"),configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}const R=[{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 ne(){const e=c(m(),".claude");if(!d(e))return null;const t=_();try{const o=c(e,"hooks");d(o)||P(o,{recursive:!0});const n=w(T(import.meta.url)),s=c(n,"..","scripts","hooks"),r=t.configPath;let i={};if(d(r))try{i=JSON.parse(p(r,"utf-8"))}catch{i={}}const u=i.hooks??{};let a=!1;const f=[];for(const h of R){const g=c(s,h.script);if(!d(g)){f.push(h.script);continue}const W=c(o,h.script);q(g,W);try{j(W,493)}catch{}const O=`node ${W}`;for(const k of h.events){const x=typeof k=="string"?k:k.event,F=typeof k=="string"?void 0:k.matcher,E=Array.isArray(u[x])?u[x]:[];if(!E.some(b=>Array.isArray(b.hooks)&&b.hooks.some(Y=>Y.command===O))){const b=F?{matcher:F,hooks:[{type:"command",command:O,timeout:30}]}:{hooks:[{type:"command",command:O,timeout:30}]};E.push(b),a=!0}u[x]=E}}i.hooks=u,l(r,JSON.stringify(i,null,2)+`
8
+ `,"utf-8"),t.configured=!0;const y=f.length?` (missing from package: ${f.join(", ")})`:"";return{client:t,action:a?"configured":"skipped",message:(a?"auto-memory hooks installed":"already installed")+y}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function ke(){const e=c(m(),".claude");if(!d(e))return null;const t=_();try{const o=w(T(import.meta.url)),n=`node ${c(o,"wyrm-statusline.js")}`,s=t.configPath;let r={};if(d(s))try{r=JSON.parse(p(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},l(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 Pe(){const e=c(m(),".claude","settings.json");if(!d(e))return null;const t=_();try{const o=JSON.parse(p(e,"utf-8")),n=o.statusLine;return n?.command&&/wyrm-statusline/.test(n.command)?(delete o.statusLine,l(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 re(){const e=c(m(),".claude","settings.json");if(!d(e))return null;const t=_();try{const o=JSON.parse(p(e,"utf-8")),n=c(m(),".claude","hooks"),s=new Set(R.map(u=>`node ${c(n,u.script)}`)),r=new Set(R.flatMap(u=>u.events.map(a=>typeof a=="string"?a:a.event))),i=o.hooks;if(i){for(const u of r)Array.isArray(i[u])&&(i[u]=i[u].map(a=>({...a,hooks:Array.isArray(a.hooks)?a.hooks.filter(f=>!s.has(f.command??"")):a.hooks})).filter(a=>!Array.isArray(a.hooks)||a.hooks.length>0),i[u].length===0&&delete i[u]);Object.keys(i).length===0&&delete o.hooks}return l(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 L=/wyrm-guard/,se=5,ie=["Bash","Edit|Write|MultiEdit|NotebookEdit"];function A(e){return e.settingsPath??process.env.CLAUDE_SETTINGS??c(m(),".claude","settings.json")}function J(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 B(e){return C()==="win32"?`"${e.replace(/"/g,'""')}"`:`'${e.replace(/'/g,"'\\''")}'`}function ce(){const e=w(T(import.meta.url)),t=c(e,"wyrm-guard.js");if(d(t))return`node ${B(t)}`;try{const o=C()==="win32"?"where":"which",s=v(o,["wyrm-guard"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
12
+ `)[0]?.trim();if(s)return B(s)}catch{}return null}function I(e){const t=e.hooks;if(!t||typeof t!="object"||Array.isArray(t))return!1;let o=!1;for(const n of Object.keys(t)){const s=t[n];if(!Array.isArray(s))continue;let r=!1;const i=[];for(const u of s){const a=u;if(!a||typeof a!="object"||!Array.isArray(a.hooks)){i.push(u);continue}const f=a.hooks.filter(y=>!L.test(y?.command??""));if(f.length===a.hooks.length){i.push(u);continue}o=!0,r=!0,f.length>0&&i.push({...a,hooks:f})}r&&(i.length===0?delete t[n]:t[n]=i)}return o&&Object.keys(t).length===0&&delete e.hooks,o}function ae(e={}){if(!!!(e.settingsPath??process.env.CLAUDE_SETTINGS)&&!d(c(m(),".claude")))return{client:J(A(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=A(e),n=J(o);try{const s=e.guardCommand??ce();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 g=JSON.parse(p(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 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 a=JSON.stringify(r);I(r);const f=r.hooks??{},y=Array.isArray(f.PreToolUse)?f.PreToolUse:[];for(const g of ie)y.push({matcher:g,hooks:[{type:"command",command:s,timeout:se}]});f.PreToolUse=y,r.hooks=f;const h=JSON.stringify(r)!==a;return h&&l(o,JSON.stringify(r,null,2)+`
13
+ `,"utf-8"),n.configured=!0,{client:n,action:h?"configured":"skipped",message:h?"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 ue(e={}){const t=A(e);if(!d(t))return null;const o=J(t);try{let n;try{const r=JSON.parse(p(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 I(n)?(l(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 Se(e={}){const t=A(e),o=[];try{const s=JSON.parse(p(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 a=u?.command;a&&L.test(a)&&o.push(a)}}catch{}return{settingsPath:t,installed:o.length>0,commands:o}}function de(e){const t={serverPath:e?.serverPath||N(),dbPath:e?.dbPath||D(),httpPort:e?.httpPort},o=c(m(),".wyrm");d(o)||P(o,{recursive:!0}),H(t);const n=S(),s=[];for(const u of n)s.push(M(u,t));const r=ne();r&&s.push(r);const i=ae();return i&&s.push(i),s}function be(){const e=S(),t=[];for(const s of e)t.push(oe(s));const o=re();o&&t.push(o);const n=ue();return n&&t.push(n),t}function fe(e,t){const o={serverPath:t?.serverPath||N(),dbPath:t?.dbPath||D(),httpPort:t?.httpPort};H(o);const n=S(),s=[];for(const r of n)e.includes(r.id)&&s.push(M(r,o));return s}function H(e){const t=c(m(),".wyrm","wyrm-config.json"),o=S().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};l(t,JSON.stringify(n,null,2)+`
15
+ `,"utf-8")}function G(){const e=c(m(),".wyrm","wyrm-config.json");if(!d(e))return null;try{return JSON.parse(p(e,"utf-8"))}catch{return null}}function je(){const e=G();return e?fe(e.configuredClients,{serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort}):de()}const z="<!-- wyrm:start -->",K="<!-- wyrm:end -->",U=`${z}
16
16
  ## Wyrm Memory + Agent (auto-managed \u2014 do not edit this section)
17
17
 
18
18
  **Session start** \u2014 call \`wyrm_session_prime\` first to load project context (ground truths, open quests, recent sessions, scaffolds) before responding.
@@ -36,16 +36,16 @@ import{existsSync as d,readFileSync as p,writeFileSync as l,mkdirSync as P,copyF
36
36
  **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.
37
37
 
38
38
  **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.
39
- ${K}`;function je(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 a=p(r,"utf-8"),f=a.indexOf(z),y=a.indexOf(K);if(f!==-1&&y!==-1&&y>f){const h=a.slice(0,f),g=a.slice(y+K.length);l(r,`${h}${F}${g}`,"utf-8")}else{const h=a.endsWith(`
39
+ ${K}`;function Ce(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 a=p(r,"utf-8"),f=a.indexOf(z),y=a.indexOf(K);if(f!==-1&&y!==-1&&y>f){const h=a.slice(0,f),g=a.slice(y+K.length);l(r,`${h}${U}${g}`,"utf-8")}else{const h=a.endsWith(`
40
40
  `)?`
41
41
  `:`
42
42
 
43
- `;l(r,`${a}${h}${F}
44
- `,"utf-8")}}else l(r,`${F}
43
+ `;l(r,`${a}${h}${U}
44
+ `,"utf-8")}}else l(r,`${U}
45
45
  `,"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=c(e,".github","copilot-instructions.md");n(u,".github/copilot-instructions.md")}else if(i==="cursor"){const u=c(e,".cursor","rules");n(u,".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]!==`
46
46
  `;)n++;t+=`
47
47
  `;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]!==`
48
- `;)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 Ce(){const e=b(),t=e.filter(r=>r.detected),o=e.filter(r=>r.configured);let n=`\u{F115D} Wyrm Auto-Configure Status
48
+ `;)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 _e(){const e=S(),t=e.filter(r=>r.detected),o=e.filter(r=>r.configured);let n=`\u{F115D} Wyrm Auto-Configure Status
49
49
 
50
50
  `;n+=` Detected: ${t.length}/${e.length} AI clients
51
51
  `,n+=` Configured: ${o.length}/${t.length} clients
@@ -55,4 +55,4 @@ ${K}`;function je(e,t){const o={injected:[],skipped:[],errors:[]},n=(r,i)=>{try{
55
55
  Server: ${s.serverPath}
56
56
  `,n+=` DB: ${s.dbPath}
57
57
  `,n+=` Last: ${new Date(s.lastSetup).toLocaleString()}
58
- `),n}export{F as WYRM_INJECT_BLOCK,we as applyWyrmEnvToClient,ue as autoConfigureAll,U as configureClient,de as configureSpecific,b as detectClients,ye as exportServerConfig,T as findWyrmServerPath,D as getDefaultDbPath,Ce as getStatusSummary,je as injectSystemPrompt,ne as installClaudeCodeHooks,ve as installClaudeStatusline,ce as installWyrmGuardHooks,G as loadWyrmMeta,$ as parseJsonWithComments,B as quoteHookPath,Se as reconfAll,re as removeClaudeCodeHooks,ke as removeClaudeStatusline,be as removeFromAll,oe as removeFromClient,ae as removeWyrmGuardHooks,ie as resolveWyrmGuardCommand,Pe as wyrmGuardHookStatus};
58
+ `),n}export{se as GUARD_HOOK_TIMEOUT_SEC,U as WYRM_INJECT_BLOCK,ve as applyWyrmEnvToClient,de as autoConfigureAll,M as configureClient,fe as configureSpecific,S as detectClients,we as exportServerConfig,N as findWyrmServerPath,D as getDefaultDbPath,_e as getStatusSummary,Ce as injectSystemPrompt,ne as installClaudeCodeHooks,ke as installClaudeStatusline,ae as installWyrmGuardHooks,G as loadWyrmMeta,$ as parseJsonWithComments,B as quoteHookPath,je as reconfAll,re as removeClaudeCodeHooks,Pe as removeClaudeStatusline,be as removeFromAll,oe as removeFromClient,ue as removeWyrmGuardHooks,ce as resolveWyrmGuardCommand,Se as wyrmGuardHookStatus};
@@ -1,4 +1,4 @@
1
- import{readFileSync as O,writeFileSync as D,existsSync as I}from"node:fs";import{join as $}from"node:path";import{WyrmDB as L,resolveDbPath as A}from"../database.js";import{CloudClient as C,loadSession as v,saveSession as U,resolveCloudDir as j}from"./client.js";import{encrypt as T,decrypt as x}from"./crypto.js";import{classifySession as q,computeMachineFp as P}from"./machine-id.js";function N(){return $(j(),"cloud-cursor.json")}const R=2;function F(t,e,n,o){const s=t?{...t}:{kind:e.kind,row_id:e.row_id,attempts:0,quarantined:!1,last_error:n,first_ms:o,last_ms:o};s.attempts+=1,s.last_error=n,s.last_ms=o;const r=s.attempts>=R,c=r&&!s.quarantined;return r&&(s.quarantined=!0),{entry:s,action:r?"skip":"retry",newlyQuarantined:c}}const g=[{kind:"truth",table:"ground_truths",hasVisibility:!0},{kind:"memory_artifact",table:"memory_artifacts",hasVisibility:!0},{kind:"quest",table:"quests",hasVisibility:!0},{kind:"design_token",table:"design_tokens",hasVisibility:!0},{kind:"design_reference",table:"design_references",hasVisibility:!0},{kind:"skill",table:"skills",hasVisibility:!0}],W=new Set(["sessions","agent_actions","agent_presence","session_seen_artifacts","tool_call_log","llm_query_log","external_call_log","token_savings_log","cost_tracking","usage_events","audit_log","indexing_queue","vectors","symbol_index","update_check","sync_conflicts","sync_log","sync_tombstones","wyrm_cloud_sync_state","mcp_client_configs","watch_dirs","global_context","schema_versions","failure_patterns"]),H=/_fts(_config|_data|_docsize|_idx)?$/;function M(t){const e=t.getDatabase(),n=[...g],o=new Set(g.map(s=>s.table));try{const s=e.prepare(`SELECT name FROM sqlite_master
1
+ import{readFileSync as O,writeFileSync as D,existsSync as I}from"node:fs";import{join as $}from"node:path";import{WyrmDB as L,resolveDbPath as A}from"../database.js";import{CloudClient as C,loadSession as v,saveSession as j,resolveCloudDir as U}from"./client.js";import{encrypt as T,decrypt as x}from"./crypto.js";import{classifySession as q,computeMachineFp as P}from"./machine-id.js";function N(){return $(U(),"cloud-cursor.json")}const R=2;function F(t,e,n,o){const s=t?{...t}:{kind:e.kind,row_id:e.row_id,attempts:0,quarantined:!1,last_error:n,first_ms:o,last_ms:o};s.attempts+=1,s.last_error=n,s.last_ms=o;const r=s.attempts>=R,c=r&&!s.quarantined;return r&&(s.quarantined=!0),{entry:s,action:r?"skip":"retry",newlyQuarantined:c}}const g=[{kind:"truth",table:"ground_truths",hasVisibility:!0},{kind:"memory_artifact",table:"memory_artifacts",hasVisibility:!0},{kind:"quest",table:"quests",hasVisibility:!0},{kind:"design_token",table:"design_tokens",hasVisibility:!0},{kind:"design_reference",table:"design_references",hasVisibility:!0},{kind:"skill",table:"skills",hasVisibility:!0}],W=new Set(["sessions","agent_actions","agent_presence","session_seen_artifacts","tool_call_log","llm_query_log","external_call_log","token_savings_log","cost_tracking","usage_events","audit_log","indexing_queue","vectors","symbol_index","update_check","sync_conflicts","sync_log","sync_tombstones","wyrm_cloud_sync_state","mcp_client_configs","watch_dirs","global_context","schema_versions","failure_patterns","data_lake","projects"]),H=/_fts(_config|_data|_docsize|_idx)?$/;function M(t){const e=t.getDatabase(),n=[...g],o=new Set(g.map(s=>s.table));try{const s=e.prepare(`SELECT name FROM sqlite_master
2
2
  WHERE type='table' AND name NOT LIKE 'sqlite_%'`).all();for(const r of s){const c=r.name;if(o.has(c)||W.has(c)||H.test(c)||!y.test(c))continue;const m=e.prepare(`PRAGMA table_info(${c})`).all();if(!m.some(l=>l.name==="id"))continue;const u=m.some(l=>l.name==="cross_project_visibility");n.push({kind:c,table:c,hasVisibility:u})}}catch{}return n}function ct(t){return t}function V(t,e){if(!y.test(e))return new Set;try{const o=t.getDatabase().prepare(`PRAGMA table_info(${e})`).all();return new Set(o.filter(s=>y.test(s.name)).map(s=>s.name))}catch{return new Set}}const y=/^[a-z_][a-z0-9_]*$/i;function Y(){const t={pull_cursor:0,last_push_updated_ms:0,last_push_at:0,last_pull_at:0},e=N();if(!I(e))return t;try{const n=JSON.parse(O(e,"utf-8"));return{...t,...n}}catch{return t}}function G(t){D(N(),JSON.stringify(t,null,2),{mode:384})}function k(t){t.getDatabase().exec(`
3
3
  CREATE TABLE IF NOT EXISTS sync_tombstones (
4
4
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -13,7 +13,7 @@ import{readFileSync as O,writeFileSync as D,existsSync as I}from"node:fs";import
13
13
  `)}function lt(t,e,n){if(typeof e!="string"||e.length===0||e.length>64)return;k(t),t.getDatabase().prepare(`INSERT OR IGNORE INTO sync_tombstones (kind, row_id, deleted_at_ms)
14
14
  VALUES (?, ?, ?)`).run(e,String(n),Date.now())}class B extends Error{constructor(e){super(e),this.name="CopiedSessionError"}}function J(t){return!!t||process.env.WYRM_ALLOW_COPIED_SESSION==="1"}function K(t,e,n){const o=q(t.machine_fp);if(o.state==="match")return;if(o.state==="adopt"){e(P());return}const s=["","\u26A0 WYRM CLOUD: this session looks COPIED from another machine.","",` Stored machine fingerprint: ${o.stored.slice(0,12)}\u2026`,` This machine's fingerprint: ${o.current.slice(0,12)}\u2026`,""," Both machines now share ONE device_id. The cloud pull query skips a"," device's own deltas, so each machine filters the OTHER's changes out:"," push works but pull silently returns 0 rows (peer memory looks MISSING).",""," FIX \u2014 give this machine its own identity (keep your encryption key):"," rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json # NOT cloud.key"," wyrm cloud login",""," If this is a legitimate change (e.g. you renamed this host), re-run with:"," wyrm cloud sync --force (or set WYRM_ALLOW_COPIED_SESSION=1)",""];if(J(n)){console.error(s.join(`
15
15
  `)),console.error(" Proceeding anyway (--force / WYRM_ALLOW_COPIED_SESSION=1); fingerprint updated."),console.error(""),e(o.current);return}throw console.error(s.join(`
16
- `)),new B("Cloud session looks copied from another machine (device_id collision). Re-login or pass --force.")}async function dt(t={}){const e=v();if(!e||!e.device_id)throw new Error("Not logged in or device not registered. Run `wyrm cloud login`.");K(e,a=>{U({...e,machine_fp:a})},t.force);const n=new C(e.base,e.session),o=Y(),s=new L(A());k(s);const r={pushed:0,pulled:0,deleted_local:0,errors:[],quarantined:0},c=t.all?M(s):g,m=Q(s,c,o.last_push_updated_ms,!!t.all,r.errors),u=z(s),l=[...m.map(a=>({kind:a.kind,row_id:String(a.row_id),visibility:a.visibility,payload_b64:T(JSON.stringify(a.row)),updated_at:a.updated_at})),...u.map(a=>({kind:"tombstone",row_id:`${a.kind}:${a.row_id}`,visibility:"org",payload_b64:T(JSON.stringify({kind:a.kind,row_id:a.row_id,deleted_at_ms:a.deleted_at_ms})),updated_at:a.deleted_at_ms}))];if(l.length>0&&!t.dryRun){let f=o.last_push_updated_ms;for(let i=0;i<l.length;i+=25){const d=l.slice(i,i+25);try{const p=await n.syncPush(e.device_id,d);if(r.pushed+=p.accepted,p.rejected>0)for(const _ of p.rejected_details)r.errors.push(`push: ${_.reason} on ${_.id.slice(0,8)}\u2026`);for(const _ of d)_.updated_at&&_.updated_at>f&&(f=_.updated_at)}catch(p){r.errors.push(`push chunk failed: ${p instanceof Error?p.message:p}`)}}o.last_push_updated_ms=f,o.last_push_at=Date.now(),u.length>0&&Z(s,u.map(i=>i.id))}else r.pushed=l.length;if(!t.dryRun){let a=o.pull_cursor,f=0;t:for(;f<50;){let i;try{i=await n.syncPull(e.device_id,a,100)}catch(d){r.errors.push(`pull failed: ${d instanceof Error?d.message:d}`);break}if(i.deltas.length===0)break;for(const d of i.deltas)try{const p=x(d.payload_b64);if(d.kind==="tombstone"){const{kind:_,row_id:w}=JSON.parse(p);et(s,c,_,w)&&r.deleted_local++}else{const _=JSON.parse(p);tt(s,c,d.kind,d.row_id,_),r.pulled++}}catch(p){const _=p instanceof Error?p.message:String(p),w=o.quarantine??={},{entry:h,action:E,newlyQuarantined:b}=F(w[d.id],d,_,Date.now());if(w[d.id]=h,E==="skip"){b&&(r.quarantined=(r.quarantined??0)+1),r.errors.push(`quarantined ${d.kind}:${d.row_id} after ${h.attempts} attempts (${_})`);continue}r.errors.push(`apply ${d.kind}:${d.row_id}: ${_} (attempt ${h.attempts}/${R}, will retry)`);break t}if(a=i.next_cursor,!i.has_more)break;f++}o.pull_cursor=a,o.last_pull_at=Date.now()}return G(o),r}function X(t){if(typeof t!="string"||t.length===0)return 0;const e=t.includes("T")?t:t.replace(" ","T")+"Z",n=Date.parse(e);return Number.isFinite(n)?n:0}function S(t,e,n){try{return t.getDatabase().prepare(`PRAGMA table_info(${e})`).all().some(r=>r.name===n)}catch{return!1}}function Q(t,e,n,o,s){const r=[],c=t.getDatabase(),m=S(t,"projects","sync_policy");for(const u of e)if(y.test(u.table)&&!(!o&&!u.hasVisibility))try{const l=S(t,u.table,"updated_at"),a=u.hasVisibility?"WHERE cross_project_visibility IN ('org', 'public')":"WHERE 1=1",f=m&&S(t,u.table,"project_id")?"AND (project_id IS NULL OR project_id IN (SELECT id FROM projects WHERE sync_policy IN ('cloud', 'team')))":"",i=l?"AND (? = 0 OR updated_at IS NULL OR strftime('%s', updated_at) * 1000 > ?)":"",d=l?"ORDER BY (updated_at IS NULL) ASC, updated_at ASC":"",p=`SELECT * FROM ${u.table} ${a} ${f} ${i} ${d}`,_=c.prepare(p),w=l?_.all(n,n):_.all();for(const h of w){const E=X(h.updated_at);if(n>0&&E>0&&E<=n)continue;const b=u.hasVisibility?String(h.cross_project_visibility??"within"):"org";r.push({kind:u.kind,row_id:h.id??"?",visibility:b,row:h,updated_at:E||Date.now()})}}catch(l){const a=l instanceof Error?l.message:String(l);if(/no such table/i.test(a))continue;s.push(`collect ${u.table}: ${a}`)}return r.sort((u,l)=>(u.updated_at??0)-(l.updated_at??0)),r}function z(t){const e=t.getDatabase();try{return e.prepare(`SELECT id, kind, row_id, deleted_at_ms
16
+ `)),new B("Cloud session looks copied from another machine (device_id collision). Re-login or pass --force.")}async function dt(t={}){const e=v();if(!e||!e.device_id)throw new Error("Not logged in or device not registered. Run `wyrm cloud login`.");K(e,a=>{j({...e,machine_fp:a})},t.force);const n=new C(e.base,e.session),o=Y(),s=new L(A());k(s);const r={pushed:0,pulled:0,deleted_local:0,errors:[],quarantined:0},c=t.all?M(s):g,m=Q(s,c,o.last_push_updated_ms,!!t.all,r.errors),u=z(s),l=[...m.map(a=>({kind:a.kind,row_id:String(a.row_id),visibility:a.visibility,payload_b64:T(JSON.stringify(a.row)),updated_at:a.updated_at})),...u.map(a=>({kind:"tombstone",row_id:`${a.kind}:${a.row_id}`,visibility:"org",payload_b64:T(JSON.stringify({kind:a.kind,row_id:a.row_id,deleted_at_ms:a.deleted_at_ms})),updated_at:a.deleted_at_ms}))];if(l.length>0&&!t.dryRun){let f=o.last_push_updated_ms;for(let i=0;i<l.length;i+=25){const d=l.slice(i,i+25);try{const p=await n.syncPush(e.device_id,d);if(r.pushed+=p.accepted,p.rejected>0)for(const _ of p.rejected_details)r.errors.push(`push: ${_.reason} on ${_.id.slice(0,8)}\u2026`);for(const _ of d)_.updated_at&&_.updated_at>f&&(f=_.updated_at)}catch(p){r.errors.push(`push chunk failed: ${p instanceof Error?p.message:p}`)}}o.last_push_updated_ms=f,o.last_push_at=Date.now(),u.length>0&&Z(s,u.map(i=>i.id))}else r.pushed=l.length;if(!t.dryRun){let a=o.pull_cursor,f=0;t:for(;f<50;){let i;try{i=await n.syncPull(e.device_id,a,100)}catch(d){r.errors.push(`pull failed: ${d instanceof Error?d.message:d}`);break}if(i.deltas.length===0)break;for(const d of i.deltas)try{const p=x(d.payload_b64);if(d.kind==="tombstone"){const{kind:_,row_id:w}=JSON.parse(p);et(s,c,_,w)&&r.deleted_local++}else{const _=JSON.parse(p);tt(s,c,d.kind,d.row_id,_),r.pulled++}}catch(p){const _=p instanceof Error?p.message:String(p),w=o.quarantine??={},{entry:h,action:E,newlyQuarantined:b}=F(w[d.id],d,_,Date.now());if(w[d.id]=h,E==="skip"){b&&(r.quarantined=(r.quarantined??0)+1),r.errors.push(`quarantined ${d.kind}:${d.row_id} after ${h.attempts} attempts (${_})`);continue}r.errors.push(`apply ${d.kind}:${d.row_id}: ${_} (attempt ${h.attempts}/${R}, will retry)`);break t}if(a=i.next_cursor,!i.has_more)break;f++}o.pull_cursor=a,o.last_pull_at=Date.now()}return G(o),r}function X(t){if(typeof t!="string"||t.length===0)return 0;const e=t.includes("T")?t:t.replace(" ","T")+"Z",n=Date.parse(e);return Number.isFinite(n)?n:0}function S(t,e,n){try{return t.getDatabase().prepare(`PRAGMA table_info(${e})`).all().some(r=>r.name===n)}catch{return!1}}function Q(t,e,n,o,s){const r=[],c=t.getDatabase(),m=S(t,"projects","sync_policy");for(const u of e)if(y.test(u.table)&&!(!o&&!u.hasVisibility))try{const l=S(t,u.table,"updated_at"),a=u.hasVisibility?"WHERE cross_project_visibility IN ('org', 'public')":"WHERE 1=1",f=m&&S(t,u.table,"project_id")?"AND (project_id IS NULL OR project_id IN (SELECT id FROM projects WHERE sync_policy IN ('cloud', 'team')))":"",i=l?"AND (? = 0 OR updated_at IS NULL OR strftime('%s', updated_at) * 1000 > ?)":"",d=l?"ORDER BY (updated_at IS NULL) ASC, updated_at ASC":"",p=`SELECT * FROM ${u.table} ${a} ${f} ${i} ${d}`,_=c.prepare(p),w=l?_.all(n,n):_.all();for(const h of w){const E=X(h.updated_at);if(n>0&&E>0&&E<=n)continue;const b=u.hasVisibility?String(h.cross_project_visibility??"within"):"org";r.push({kind:u.kind,row_id:h.id??"?",visibility:b,row:h,updated_at:E||Date.now()})}}catch(l){const a=l instanceof Error?l.message:String(l);if(/no such table/i.test(a))continue;s.push(`collect ${u.table}: ${a}`)}return r.sort((u,l)=>(u.updated_at??0)-(l.updated_at??0)),r}function z(t){const e=t.getDatabase();try{return e.prepare(`SELECT id, kind, row_id, deleted_at_ms
17
17
  FROM sync_tombstones
18
18
  WHERE pushed_at_ms IS NULL
19
19
  ORDER BY id ASC
@@ -1,2 +1,2 @@
1
- import{classifyCapture as $}from"../capture.js";const x="ingested_transcripts",A=5e3;function y(t){return`connector:${t}:cursor`}function m(t){return`connector:${t}:seen`}function _(t,e){const n=t.getProjectByName(e);return n?n.id:t.registerProject(e,`connector://${e}`).id}function j(t){return t.map(e=>`[${e.ts}] ${e.sender} (${e.chat}): ${e.text}`).join(`
2
- `)}async function D(t,e,n,r={}){const s=r.now??(()=>new Date),a=r.seenCap??A,h=e.rawCategory??x,u=_(t,e.workspace),d=t.getGlobalContext(y(n.name))??null,i=G(t,n.name),{messages:f,cursor:l}=await n.fetchSince(d),o=[];let p=0;for(const c of f){if(i.has(c.id)){p++;continue}o.push(c),i.add(c.id)}const g={source:n.name,workspace:e.workspace,fetched:f.length,ingested:o.length,skipped:p,cursor:l};if(o.length>0){const c=s().toISOString().replace(/[:.]/g,"-"),w=`${n.name}_${c}`,C=E(o);t.insertData(u,h,w,j(o),{source:n.name,count:o.length,chats:[...new Set(o.map(S=>S.chat))],typeCounts:C,cursor:l}),g.rawKey=w,r.distill&&await r.distill(o,{workspace:e.workspace,projectId:u})}return t.setGlobalContext(y(n.name),l),T(t,n.name,i,a),g}function E(t){const e={};for(const n of t){const{type:r}=$(n.text);e[r]=(e[r]??0)+1}return e}function G(t,e){const n=t.getGlobalContext(m(e));if(!n)return new Set;try{const r=JSON.parse(n);return new Set(Array.isArray(r)?r.map(String):[])}catch{return new Set}}function T(t,e,n,r){const s=[...n],a=s.length>r?s.slice(s.length-r):s;t.setGlobalContext(m(e),JSON.stringify(a))}export{D as runIngestion};
1
+ import{redactCredentials as $}from"../security.js";import{classifyCapture as x}from"../capture.js";const A="ingested_transcripts",_=5e3;function w(t){return`connector:${t}:cursor`}function y(t){return`connector:${t}:seen`}function j(t,e){const n=t.getProjectByName(e);return n?n.id:t.registerProject(e,`connector://${e}`).id}function E(t){return t.map(e=>`[${e.ts}] ${e.sender} (${e.chat}): ${$(e.text)}`).join(`
2
+ `)}async function K(t,e,n,r={}){const s=r.now??(()=>new Date),c=r.seenCap??_,d=e.rawCategory??A,u=j(t,e.workspace),h=t.getGlobalContext(w(n.name))??null,i=T(t,n.name),{messages:f,cursor:l}=await n.fetchSince(h),o=[];let p=0;for(const a of f){if(i.has(a.id)){p++;continue}o.push(a),i.add(a.id)}const g={source:n.name,workspace:e.workspace,fetched:f.length,ingested:o.length,skipped:p,cursor:l};if(o.length>0){const a=s().toISOString().replace(/[:.]/g,"-"),m=`${n.name}_${a}`,C=G(o);t.insertData(u,d,m,E(o),{source:n.name,count:o.length,chats:[...new Set(o.map(S=>S.chat))],typeCounts:C,cursor:l}),g.rawKey=m,r.distill&&await r.distill(o,{workspace:e.workspace,projectId:u})}return t.setGlobalContext(w(n.name),l),k(t,n.name,i,c),g}function G(t){const e={};for(const n of t){const{type:r}=x(n.text);e[r]=(e[r]??0)+1}return e}function T(t,e){const n=t.getGlobalContext(y(e));if(!n)return new Set;try{const r=JSON.parse(n);return new Set(Array.isArray(r)?r.map(String):[])}catch{return new Set}}function k(t,e,n,r){const s=[...n],c=s.length>r?s.slice(s.length-r):s;t.setGlobalContext(y(e),JSON.stringify(c))}export{K as runIngestion};
package/dist/database.js CHANGED
@@ -1,11 +1,11 @@
1
- import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSync as b,readdirSync as D,statSync as L,writeFileSync as I}from"fs";import{homedir as O}from"os";import{join as d,basename as A,resolve as M,normalize as w}from"path";import{spawnSync as R}from"child_process";import{getResilienceManager as v}from"./resilience.js";import{WyrmLogger as j}from"./logger.js";import{runMigrations as H,getSchemaVersion as F}from"./migrations.js";import{validateProjectPath as N,buildFtsMatchQuery as h}from"./security.js";import{emitEvent as m,eventsSince as W,subscribeEvents as U,isLiveMemoryEnabled as x,ingestRemoteEvent as P,eventsForPush as B,pruneEvents as q,getMeta as Y,setMeta as $}from"./events.js";import{getActor as C}from"./handlers/boundary.js";import{readSkillContent as y,slugify as G,skillContentSha as V}from"./skill-author.js";import{resolveProjectForPath as J}from"./project-resolve.js";function le(){return process.env.WYRM_DB_PATH??process.env.WYRM_DB??d(O(),".wyrm","wyrm.db")}class de{db;BATCH_SIZE=1e3;resilience;logger;dbPath;constructor(t){const e=d(O(),".wyrm");u(e)||b(e,{recursive:!0}),this.dbPath=t||d(e,"wyrm.db"),this.logger=new j,this.resilience=v(),this.db=this.initializeDatabase(this.dbPath),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=H(this.db);s.length>0&&this.logger.info(`Applied ${s.length} migration(s), now at v${F(this.db)}`),this.recoverIncompleteOperations()}getDatabase(){return this.db}getDatabasePath(){return this.dbPath}initializeDatabase(t){const e=this.resilience.withRetrySync(()=>k(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(`
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(`
2
2
  INSERT INTO watch_dirs (path, recursive)
3
3
  VALUES (?, ?)
4
4
  ON CONFLICT(path) DO UPDATE SET recursive = excluded.recursive
5
5
  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){N(t);const s=[],r=(n,i=0)=>{if(!(i>3&&e))try{const a=D(n,{withFileTypes:!0});for(const o of a){if(!o.isDirectory()||o.name.startsWith(".")&&o.name!==".git")continue;const c=d(n,o.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(`
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(`
7
7
  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=w(M(t));if(!u(e)||!L(e).isDirectory())return null;const s=A(e);let r,n,i,a;try{const o=R("git",["config","--get","remote.origin.url"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});o.status===0&&(r=o.stdout.trim());const c=R("git",["rev-parse","--abbrev-ref","HEAD"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});c.status===0&&(n=c.stdout.trim());const l=R("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"))?(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,i,n)}catch{return null}}registerProject(t,e,s,r,n,i){return this.db.prepare(`
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(`
9
9
  INSERT INTO projects (name, path, repo, stack, last_commit, branch)
10
10
  VALUES (?, ?, ?, ?, ?, ?)
11
11
  ON CONFLICT(path) DO UPDATE SET
@@ -16,7 +16,7 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
16
16
  branch = COALESCE(excluded.branch, projects.branch),
17
17
  updated_at = datetime('now')
18
18
  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}=J(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(`
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(`
20
20
  SELECT * FROM projects ORDER BY updated_at DESC LIMIT ? OFFSET ?
21
21
  `).all(t,e)}searchProjects(t){const e=`%${t}%`;return this.db.prepare(`
22
22
  SELECT * FROM projects
@@ -27,11 +27,11 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
27
27
  INSERT INTO sessions (project_id, date, objectives, completed, issues, commits, files_changed, notes, tokens_estimate, agent_id, run_id)
28
28
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
29
29
  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[a,o]of Object.entries(e))n.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 i=this.resilience.withRetrySync(()=>this.db.prepare(`
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(`
31
31
  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),a=new Date().toISOString(),o=`[${a}${s?" \xB7 "+s:""}] ${r}`;let c=i?.thread?i.thread+`
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+`
33
33
 
34
- `+o:o;c.length>n&&(c=`[\u2026earlier continuity trimmed \u2014 gist retained below\u2026]
34
+ `+a:a;c.length>n&&(c=`[\u2026earlier continuity trimmed \u2014 gist retained below\u2026]
35
35
 
36
36
  `+c.slice(c.length-n));const l=(i?.turn_count??0)+1;this.resilience.withRetrySync(()=>(this.db.prepare(`
37
37
  INSERT INTO project_continuity (project_id, thread, updated_at, updated_by, turn_count)
@@ -39,10 +39,10 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
39
39
  ON CONFLICT(project_id) DO UPDATE SET
40
40
  thread = excluded.thread, updated_at = excluded.updated_at,
41
41
  updated_by = excluded.updated_by, turn_count = excluded.turn_count
42
- `).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 n=null;if(Array.isArray(t.toolNames)){const o=[...new Set(t.toolNames.map(c=>String(c).trim()).filter(Boolean))];n=o.length?o.join(","):null}else if(typeof t.toolNames=="string"){const o=[...new Set(t.toolNames.split(",").map(c=>c.trim()).filter(Boolean))];n=o.length?o.join(","):null}const i=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(`
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(`
43
43
  INSERT INTO routing_memory (project_id, task_type, model_id, tool_names, success, latency_ms)
44
44
  VALUES (?, ?, ?, ?, ?, ?)
45
- `).run(e,s,r,n,t.success?1:0,i);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)),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(`
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(`
46
46
  SELECT
47
47
  model_id,
48
48
  COALESCE(tool_names, '') AS tool_names,
@@ -56,7 +56,7 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
56
56
  GROUP BY model_id, COALESCE(tool_names, '')
57
57
  ORDER BY (CAST(SUM(success) AS REAL) / COUNT(*)) DESC, runs DESC, last_ts DESC
58
58
  LIMIT ?
59
- `).all(...i).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(`
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(`
60
60
  SELECT * FROM sessions
61
61
  WHERE project_id = ? AND is_archived = 0
62
62
  ORDER BY date DESC, id DESC
@@ -89,11 +89,11 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
89
89
  `).run(t,t,e).changes}getSessionTokenUsage(t){return this.db.prepare(`
90
90
  SELECT COALESCE(SUM(tokens_estimate), 0) as total
91
91
  FROM sessions WHERE project_id = ? AND is_archived = 0
92
- `).get(t).total}addQuest(t,e,s,r="medium",n){const i=C(),a=this.db.prepare(`
92
+ `).get(t).total}addQuest(t,e,s,r="medium",n){const i=C(),o=this.db.prepare(`
93
93
  INSERT INTO quests (project_id, title, description, priority, tags, agent_id, run_id, updated_at)
94
94
  VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
95
95
  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:a.id}),a}updateQuest(t,e){const s=e==="completed"?new Date().toISOString():null,r=this.db.prepare(`
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(`
97
97
  UPDATE quests SET status = ?, completed_at = ?, updated_at = datetime('now') WHERE id = ? RETURNING *
98
98
  `).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
99
  SELECT * FROM quests
@@ -139,7 +139,7 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
139
139
  SELECT value FROM context WHERE project_id = ? AND key = ?
140
140
  `).get(t,e)?.value}getAllContext(t){const e=this.db.prepare(`
141
141
  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 x()}eventsSince(t,e=0,s=100){return W(this.db,t,e,s)}subscribeEvents(t,e=20){return U(this.db,t,e)}publishEvent(t){m(this.db,t)}pruneEvents(t={}){return q(this.db,t)}ingestRemoteEvent(t,e){return P(this.db,t,e)}eventsForPush(t,e=0,s=200){return B(this.db,t,e,s)}getMeta(t){return Y(this.db,t)}setMeta(t,e){$(this.db,t,e)}setGlobalContext(t,e){this.db.prepare(`
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(`
143
143
  INSERT INTO global_context (key, value)
144
144
  VALUES (?, ?)
145
145
  ON CONFLICT(key) DO UPDATE SET
@@ -147,7 +147,7 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
147
147
  updated_at = datetime('now')
148
148
  `).run(t,e)}getGlobalContext(t){return this.db.prepare(`
149
149
  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,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:[]),_=y({skillPath:s,name:t}),g=_?.content??null,S=_?.sha256??null,f=_?new Date().toISOString():null,E=this.resilience.withRetrySync(()=>this.db.prepare(`
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(`
151
151
  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
152
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)
153
153
  ON CONFLICT(name) DO UPDATE SET
@@ -169,13 +169,13 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
169
169
  updated_at = datetime('now'),
170
170
  is_active = 1
171
171
  RETURNING *
172
- `).get(t,e,s,r||null,n||null,i||null,a||null,c,l,p,g,S,f),"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??y,s=this.db.prepare("SELECT id, name, skill_path, content_sha256 FROM skills").all();let r=0,n=0,i=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){i++;continue}if(c.content_sha256&&c.content_sha256===l.sha256){n++;continue}a.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,a=0;const o=new Set;for(const c of r){if(c.content==null){i++;continue}let l=G(c.name);if(!l){i++;continue}o.has(l)&&(l=`${l}-${V(c.name).slice(0,8)}`,a++),o.add(l);const p=d(t,l);b(p,{recursive:!0}),I(d(p,"SKILL.md"),c.content,"utf-8"),n++}return{total:r.length,written:n,skipped_no_content:i,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 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 a=h(s);a&&(n+=" AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)",i.push(a))}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(`
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(`
173
173
  SELECT s.* FROM skills s
174
174
  JOIN skills_fts ON s.id = skills_fts.rowid
175
175
  WHERE skills_fts MATCH ?${n}
176
176
  ORDER BY rank
177
177
  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 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[]}},n=(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 f=new Set(c).add(o);for(const E of p){const T=n(E,f);T?g.children.push(T):S.push(E)}}return S.length&&(g.missing=S),g};if(t){const o=n(t,new Set);return o?[o]:[]}const i=e.filter(o=>(o.tier??"atomic")==="god").map(o=>o.name).sort(),a=[];for(const o of i){const c=n(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(`
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(`
179
179
  UPDATE skills SET ${s.join(", ")} WHERE name = ? RETURNING *
180
180
  `).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
181
  SELECT category, COUNT(*) as count FROM skills WHERE category IS NOT NULL GROUP BY category
@@ -200,7 +200,7 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
200
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(`
201
201
  INSERT INTO data_lake (project_id, category, key, value, metadata)
202
202
  VALUES (?, ?, ?, ?, ?)
203
- `);try{for(let i=0;i<t.length;i+=s){const a=t.slice(i,i+s),o=Math.floor(i/s)+1;this.resilience.updateCheckpoint(e,`batch_${o}`,{processed:i,currentBatch:o});const c=this.resilience.withRetrySync(()=>this.batchWrites(a,l=>(n.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:i});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(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(`
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(`
204
204
  SELECT * FROM data_lake
205
205
  WHERE project_id = ? AND category = ?
206
206
  ORDER BY created_at DESC
@@ -242,8 +242,8 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
242
242
  WHERE project_id = ?
243
243
  GROUP BY category
244
244
  ORDER BY count DESC
245
- `).all(t)}deleteDataCategory(t,e){return this.db.prepare(`
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(`
246
246
  DELETE FROM data_lake WHERE project_id = ? AND category = ?
247
247
  `).run(t,e).changes}*streamSessions(t){const e=this.db.prepare(`
248
248
  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(),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: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{de as WyrmDB,le as resolveDbPath};
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};