wyrm-mcp 9.1.1 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BENCHMARKS.md +8 -4
- package/README.md +9 -1
- package/dist/autoconfig.js +25 -25
- package/dist/database.js +30 -27
- package/dist/handlers/capture.js +40 -40
- package/dist/handlers/review.js +1 -1
- package/dist/http-fast.js +5 -5
- package/dist/intelligence.js +25 -22
- package/dist/maintenance.js +2 -2
- package/dist/memory-artifacts.js +27 -26
- package/dist/migrations.js +5 -5
- package/dist/providers/embedding-provider.js +1 -1
- package/dist/rerank.js +1 -1
- package/dist/security.js +1 -1
- package/dist/tool-analytics.js +4 -4
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/vectors.js +3 -3
- package/dist/wyrm-cli.js +140 -139
- package/dist/wyrm-manifest.json +1 -1
- package/package.json +1 -1
- package/skills/wyrm-nvidia-nim/SKILL.md +26 -17
package/BENCHMARKS.md
CHANGED
|
@@ -61,16 +61,20 @@ Retrieval recall is the floor. Two more, all reproducible, all local (added 2026
|
|
|
61
61
|
|
|
62
62
|
### Negative learning — the firewall benchmark (the race we own)
|
|
63
63
|
|
|
64
|
-
No public memory benchmark measures this. `bench/negative-learning.mjs` asks: when an agent is about to **repeat** a recorded mistake, does `wyrm_failure_check` block it
|
|
64
|
+
No public memory benchmark measures this. `bench/negative-learning.mjs` asks: when an agent is about to **repeat** a recorded mistake, does `wyrm_failure_check` block it, and stay silent on novel actions? Deterministic, no LLM, no cloud. Measured on wyrm-mcp 9.1.2, 2026-09-12 (`bench/firewall-receipt.json`):
|
|
65
65
|
|
|
66
66
|
| probe | result |
|
|
67
67
|
|---|---|
|
|
68
68
|
| same action re-attempted (the guard's real input) | **recall 100%** (16/16) |
|
|
69
|
+
| reworded restatement | **recall 100%** (16/16) |
|
|
69
70
|
| novel actions | **precision 100%** · specificity 100% (0 false blocks of 16) |
|
|
70
|
-
|
|
|
71
|
-
|
|
|
71
|
+
| confidence separation | 0.86 |
|
|
72
|
+
| staleness oracle | passes |
|
|
73
|
+
| `checkVerdict` latency | p50 2.7 ms · p95 6.1 ms |
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
The reworded tier used to sit at 12.5% (8.5.3), because the fuzzy probe was a conjunctive-AND FTS kept precision-first by design. Target normalization (canonical command matching, flag order and `=` form folded, positionals kept in order) closed that gap without giving up a single false block. Run: `node bench/negative-learning.mjs`.
|
|
76
|
+
|
|
77
|
+
The end-to-end version of the same question, a blind agent working real tickets with and without the firewall in front of its actions, is the agent A/B: repeated-failure rate 38.9% (14/36) without the firewall to 8.3% (3/36) with it, 11 blocks, relative reduction 78.6% (bootstrap 95% CI 45.5% to 100%, n=36 per arm, 3 repetitions). Method, per-scenario table and the stated limits: `docs/firewall-receipt.md`.
|
|
74
78
|
|
|
75
79
|
### Embedding-model sweep
|
|
76
80
|
|
package/README.md
CHANGED
|
@@ -50,6 +50,14 @@ capture → store durable facts, lessons, and tasks as you go
|
|
|
50
50
|
|
|
51
51
|
Every number Wyrm publishes comes from a benchmark committed to the source, reproducible on your own data. The negative-learning firewall and the recall lift are the two that matter most, and both are covered below.
|
|
52
52
|
|
|
53
|
+
### The Firewall Receipt
|
|
54
|
+
|
|
55
|
+
The receipt is the measured proof of one claim: when a failure is on record, the firewall blocks the repeat, stays silent on novel actions, and drops the block once the anchored source drifts, deterministically and with no model in the loop. On wyrm-mcp 9.1.2, measured 2026-09-12, the deterministic bench scores recall 100% (32/32) on same-action and reworded repeats with precision 100% (0 false blocks of 16), and in the blind agent A/B the repeated-failure rate falls from 38.9% (14/36) to 8.3% (3/36), a 78.6% relative reduction with a bootstrap 95% CI of 45.5% to 100% (n=36 per arm). The method, the per-scenario table and the two limits the numbers carry are in [docs/firewall-receipt.md](docs/firewall-receipt.md); the deterministic receipt is regenerated on every push in CI as the artifact `firewall-receipt-node22.x` ([workflow](https://github.com/Ghosts-Protocol-Pvt-Ltd/wyrm-mcp/actions/workflows/ci.yml)).
|
|
56
|
+
|
|
57
|
+
The failure firewall in twenty seconds: a failure is on record, the agent proposes the same deploy, and the PreToolUse hook refuses it with the recorded reason before the command runs. A variant with an added flag gets an advisory, not a block, which is the honest edge of the deterministic match. Every line in the clip is real hook output.
|
|
58
|
+
|
|
59
|
+

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