wyrm-mcp 8.7.6 → 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 +3 -15
- package/dist/autoconfig.js +17 -17
- package/dist/cloud/sync-engine.js +2 -2
- package/dist/connectors/ingest.js +2 -2
- package/dist/database.js +19 -19
- package/dist/failure-patterns.js +40 -40
- package/dist/federation.js +12 -12
- package/dist/handlers/capture.js +62 -62
- package/dist/handlers/failure.js +7 -7
- package/dist/handlers/recall.js +20 -18
- package/dist/handlers/session.js +22 -22
- package/dist/handlers/share.js +4 -4
- package/dist/handlers/syncops.js +4 -4
- package/dist/http-auth.js +2 -2
- package/dist/http-fast.js +5 -5
- package/dist/memory-artifacts.js +27 -27
- package/dist/project-resolve.js +1 -1
- package/dist/security.js +1 -1
- package/dist/skill-author.js +2 -2
- package/dist/sub-agent.js +11 -11
- package/dist/tasks.js +1 -1
- package/dist/tool-annotations.js +1 -1
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/ui-dashboard.js +14 -7
- package/dist/wyrm-cli.js +51 -47
- package/dist/wyrm-guard.js +15 -13
- package/dist/wyrm-manifest.json +1 -1
- package/package.json +8 -7
- package/scripts/hooks/wyrm-session-rehydrate.mjs +1 -1
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/
|
|
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
|
|
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://
|
|
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
|
|
package/dist/autoconfig.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}${
|
|
44
|
-
`,"utf-8")}}else l(r,`${
|
|
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
|
|
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{
|
|
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
|
|
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=>{
|
|
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{
|
|
2
|
-
`)}async function
|
|
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
|
|
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){
|
|
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=
|
|
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}=
|
|
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
|
|
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),
|
|
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
|
-
`+
|
|
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,
|
|
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
|
|
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(
|
|
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(),
|
|
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:
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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(),
|
|
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};
|