wyrm-mcp 8.7.4 → 8.7.5

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.
@@ -4,7 +4,7 @@ import{existsSync as d,readFileSync as p,writeFileSync as l,mkdirSync as P,copyF
4
4
  `)[0];break}}}catch{}}function ee(e,t){switch(t.configFormat){case"vscode":return e.mcp?.servers?.wyrm!==void 0;case"mcp-json":return e[t.mcpKey]?.wyrm!==void 0;case"zed":return e[t.mcpKey]?.wyrm!==void 0;default:return!1}}function T(){try{const n=v("which",["wyrm-mcp"],{encoding:"utf-8",timeout:5e3});if(n.stdout?.trim())return n.stdout.trim()}catch{}try{const s=v("npm",["root","-g"],{encoding:"utf-8",timeout:5e3}).stdout?.trim();if(s){const r=c(s,"wyrm-mcp","dist","index.js");if(d(r))return r}}catch{}const e=V(__dirname,"index.js");if(d(e))return e;const t=c(m(),".wyrm","node_modules","wyrm-mcp","dist","index.js");if(d(t))return t;const o=[c(m(),"Git Projects","Wyrm","packages","mcp-server","dist","index.js"),c(m(),"projects","Wyrm","packages","mcp-server","dist","index.js"),c(m(),"dev","Wyrm","packages","mcp-server","dist","index.js")];for(const n of o)if(d(n))return n;return"wyrm-mcp"}function D(){return c(m(),".wyrm","wyrm.db")}function ye(e="mcp-json",t){const o=t?.serverPath??T(),n=t?.dbPath??D(),s=o==="wyrm-mcp",r={command:s?"npx":"node",args:s?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:n}};switch(e){case"stdio":return r;case"vscode":return{mcp:{servers:{wyrm:r}}};default:return{mcpServers:{wyrm:r}}}}function te(e,t){const o=t.serverPath,n=o==="wyrm-mcp",s=o.endsWith(".js");switch(e.configFormat){case"vscode":return{command:n?"npx":"node",args:n?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:t.dbPath}};case"mcp-json":return{command:n?"npx":"node",args:n?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:t.dbPath}};case"zed":return{command:n?"npx":"node",args:n?["wyrm-mcp"]:[o],env:{WYRM_DB_PATH:t.dbPath}};default:return{}}}function U(e,t){if(!e.detected)return{client:e,action:"skipped",message:`${e.name} not detected`};try{const o=w(e.configPath);d(o)||P(o,{recursive:!0});let n={},s=!1;if(d(e.configPath))try{const a=p(e.configPath,"utf-8");n=$(a),s=!0}catch{n={}}let r;if(s){r=`${e.configPath}.wyrm-backup`,l(r,p(e.configPath),{mode:384});try{j(r,384)}catch{}}const i=te(e,t);switch(e.configFormat){case"vscode":{n.mcp||(n.mcp={});const a=n.mcp;a.servers||(a.servers={});const f=a.servers;f.wyrm=i;break}case"mcp-json":{n[e.mcpKey]||(n[e.mcpKey]={});const a=n[e.mcpKey];a.wyrm=i;break}case"zed":{n[e.mcpKey]||(n[e.mcpKey]={});const a=n[e.mcpKey];a.wyrm=i;break}}l(e.configPath,JSON.stringify(n,null,2)+`
5
5
  `,"utf-8");const u=e.configured?"updated":"configured";return e.configured=!0,{client:e,action:u,message:`${e.name} ${u==="configured"?"configured":"updated"} successfully`,backup:r}}catch(o){return{client:e,action:"failed",message:`Failed to configure ${e.name}: ${o}`}}}function we(e,t){try{if(!d(e.configPath))return{client:e,action:"skipped",message:"No config file"};const o=p(e.configPath,"utf-8");let n;try{n=$(o)}catch{return{client:e,action:"failed",message:"Config file is not valid JSON; refusing to touch it"}}const r=(e.configFormat==="vscode"?n.mcp?.servers:n[e.mcpKey])?.wyrm;if(!r)return{client:e,action:"skipped",message:"Wyrm not configured in this client"};const i=`${e.configPath}.wyrm-backup`;l(i,o,{encoding:"utf-8",mode:384});try{j(i,384)}catch{}return r.env={...r.env,...t},l(e.configPath,JSON.stringify(n,null,2)+`
6
6
  `,"utf-8"),{client:e,action:"updated",message:`env: ${Object.keys(t).join(", ")}`,backup:i}}catch(o){return{client:e,action:"failed",message:String(o)}}}function oe(e){if(!e.detected||!e.configured)return{client:e,action:"skipped",message:`${e.name} not configured`};try{if(!d(e.configPath))return{client:e,action:"skipped",message:`${e.name} config not found`};const t=p(e.configPath,"utf-8"),o=$(t),n=`${e.configPath}.wyrm-backup`;l(n,t,{mode:384});try{j(n,384)}catch{}switch(e.configFormat){case"vscode":{const r=o.mcp?.servers;r&&delete r.wyrm;break}case"mcp-json":case"zed":{const s=o[e.mcpKey];s&&delete s.wyrm;break}}return l(e.configPath,JSON.stringify(o,null,2)+`
7
- `,"utf-8"),e.configured=!1,{client:e,action:"configured",message:`Removed Wyrm from ${e.name}`,backup:n}}catch(t){return{client:e,action:"failed",message:`Failed to remove from ${e.name}: ${t}`}}}function A(){return{id:"claude-code-hooks",name:"Claude Code (auto-memory)",icon:"\u{1FA9D}",configPath:c(m(),".claude","settings.json"),configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}const R=[{script:"wyrm-session-capture.mjs",events:["SessionEnd","PreCompact"]},{script:"wyrm-session-rehydrate.mjs",events:["SessionStart"]},{script:"wyrm-session-prune.mjs",events:["SessionEnd"]},{script:"wyrm-tool-call-trace.mjs",events:["PostToolUse"]},{script:"wyrm-run-auto.mjs",events:["SessionStart",{event:"PreToolUse",matcher:"Task"},"SessionEnd"]}];function ne(){const e=c(m(),".claude");if(!d(e))return null;const t=A();try{const o=c(e,"hooks");d(o)||P(o,{recursive:!0});const n=w(N(import.meta.url)),s=c(n,"..","scripts","hooks"),r=t.configPath;let i={};if(d(r))try{i=JSON.parse(p(r,"utf-8"))}catch{i={}}const u=i.hooks??{};let a=!1;const f=[];for(const h of R){const g=c(s,h.script);if(!d(g)){f.push(h.script);continue}const W=c(o,h.script);q(g,W);try{j(W,493)}catch{}const O=`node ${W}`;for(const k of h.events){const x=typeof k=="string"?k:k.event,M=typeof k=="string"?void 0:k.matcher,E=Array.isArray(u[x])?u[x]:[];if(!E.some(S=>Array.isArray(S.hooks)&&S.hooks.some(Y=>Y.command===O))){const S=M?{matcher:M,hooks:[{type:"command",command:O,timeout:30}]}:{hooks:[{type:"command",command:O,timeout:30}]};E.push(S),a=!0}u[x]=E}}i.hooks=u,l(r,JSON.stringify(i,null,2)+`
7
+ `,"utf-8"),e.configured=!1,{client:e,action:"configured",message:`Removed Wyrm from ${e.name}`,backup:n}}catch(t){return{client:e,action:"failed",message:`Failed to remove from ${e.name}: ${t}`}}}function A(){return{id:"claude-code-hooks",name:"Claude Code (auto-memory)",icon:"\u{1FA9D}",configPath:c(m(),".claude","settings.json"),configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}const R=[{script:"wyrm-session-capture.mjs",events:["SessionEnd","PreCompact"]},{script:"wyrm-session-rehydrate.mjs",events:["SessionStart"]},{script:"wyrm-session-prune.mjs",events:["SessionEnd"]},{script:"wyrm-tool-call-trace.mjs",events:["PostToolUse"]},{script:"wyrm-run-auto.mjs",events:["SessionStart",{event:"PreToolUse",matcher:"Task"},"SessionEnd"]},{script:"wyrm-citation-audit.mjs",events:["Stop"]}];function ne(){const e=c(m(),".claude");if(!d(e))return null;const t=A();try{const o=c(e,"hooks");d(o)||P(o,{recursive:!0});const n=w(N(import.meta.url)),s=c(n,"..","scripts","hooks"),r=t.configPath;let i={};if(d(r))try{i=JSON.parse(p(r,"utf-8"))}catch{i={}}const u=i.hooks??{};let a=!1;const f=[];for(const h of R){const g=c(s,h.script);if(!d(g)){f.push(h.script);continue}const W=c(o,h.script);q(g,W);try{j(W,493)}catch{}const O=`node ${W}`;for(const k of h.events){const x=typeof k=="string"?k:k.event,M=typeof k=="string"?void 0:k.matcher,E=Array.isArray(u[x])?u[x]:[];if(!E.some(S=>Array.isArray(S.hooks)&&S.hooks.some(Y=>Y.command===O))){const S=M?{matcher:M,hooks:[{type:"command",command:O,timeout:30}]}:{hooks:[{type:"command",command:O,timeout:30}]};E.push(S),a=!0}u[x]=E}}i.hooks=u,l(r,JSON.stringify(i,null,2)+`
8
8
  `,"utf-8"),t.configured=!0;const y=f.length?` (missing from package: ${f.join(", ")})`:"";return{client:t,action:a?"configured":"skipped",message:(a?"auto-memory hooks installed":"already installed")+y}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function ve(){const e=c(m(),".claude");if(!d(e))return null;const t=A();try{const o=w(N(import.meta.url)),n=`node ${c(o,"wyrm-statusline.js")}`,s=t.configPath;let r={};if(d(s))try{r=JSON.parse(p(s,"utf-8"))}catch{r={}}const i=r.statusLine;if(i?.command&&!/wyrm-statusline/.test(i.command))return{client:t,action:"skipped",message:`a non-Wyrm statusLine is already set (${i.command}); not overwriting`};const u=i?.command===n;return r.statusLine={type:"command",command:n,padding:0},l(s,JSON.stringify(r,null,2)+`
9
9
  `,"utf-8"),{client:t,action:u?"skipped":"configured",message:u?"buddy statusline already installed":"Wyrm buddy statusline installed \u2014 persistent in the Claude Code TUI"}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function ke(){const e=c(m(),".claude","settings.json");if(!d(e))return null;const t=A();try{const o=JSON.parse(p(e,"utf-8")),n=o.statusLine;return n?.command&&/wyrm-statusline/.test(n.command)?(delete o.statusLine,l(e,JSON.stringify(o,null,2)+`
10
10
  `,"utf-8"),{client:t,action:"configured",message:"Wyrm statusline removed"}):{client:t,action:"skipped",message:"no Wyrm statusline set"}}catch(o){return{client:t,action:"failed",message:o instanceof Error?o.message:String(o)}}}function re(){const e=c(m(),".claude","settings.json");if(!d(e))return null;const t=A();try{const o=JSON.parse(p(e,"utf-8")),n=c(m(),".claude","hooks"),s=new Set(R.map(u=>`node ${c(n,u.script)}`)),r=new Set(R.flatMap(u=>u.events.map(a=>typeof a=="string"?a:a.event))),i=o.hooks;if(i){for(const u of r)Array.isArray(i[u])&&(i[u]=i[u].map(a=>({...a,hooks:Array.isArray(a.hooks)?a.hooks.filter(f=>!s.has(f.command??"")):a.hooks})).filter(a=>!Array.isArray(a.hooks)||a.hooks.length>0),i[u].length===0&&delete i[u]);Object.keys(i).length===0&&delete o.hooks}return l(e,JSON.stringify(o,null,2)+`
@@ -1,11 +1,11 @@
1
- class d{db;constructor(e){this.db=e}mark(e,t,s,n){this.db.prepare(`
1
+ class f{db;constructor(e){this.db=e}mark(e,i,s,a){this.db.prepare(`
2
2
  INSERT INTO session_seen_artifacts (session_id, artifact_id, artifact_kind, render_mode)
3
3
  VALUES (?, ?, ?, ?)
4
4
  ON CONFLICT(session_id, artifact_id, artifact_kind)
5
5
  DO UPDATE SET shown_at = datetime('now'), render_mode = excluded.render_mode
6
- `).run(e,t,s,n)}markBulk(e,t){const s=this.db.prepare(`
6
+ `).run(e,i,s,a)}markBulk(e,i){const s=this.db.prepare(`
7
7
  INSERT INTO session_seen_artifacts (session_id, artifact_id, artifact_kind, render_mode)
8
8
  VALUES (?, ?, ?, ?)
9
9
  ON CONFLICT(session_id, artifact_id, artifact_kind)
10
10
  DO UPDATE SET shown_at = datetime('now'), render_mode = excluded.render_mode
11
- `);this.db.transaction(i=>{for(const r of i)s.run(e,r.id,r.kind,r.mode)})(t)}getSeen(e){const t=this.db.prepare("SELECT artifact_id, artifact_kind FROM session_seen_artifacts WHERE session_id = ? AND render_mode = 'inline'").all(e);return new Set(t.map(s=>`${s.artifact_kind}:${s.artifact_id}`))}prune(e=7){return this.db.prepare("DELETE FROM session_seen_artifacts WHERE shown_at < datetime('now', ? || ' days')").run(`-${e}`).changes??0}count(){return this.db.prepare("SELECT COUNT(*) as n FROM session_seen_artifacts").get().n}}export{d as SessionSeen};
11
+ `);this.db.transaction(r=>{for(const n of r)s.run(e,n.id,n.kind,n.mode)})(i)}getSeen(e){const i=this.db.prepare("SELECT artifact_id, artifact_kind FROM session_seen_artifacts WHERE session_id = ? AND render_mode = 'inline'").all(e);return new Set(i.map(s=>`${s.artifact_kind}:${s.artifact_id}`))}attest(e,i){const s=this.db.prepare("SELECT artifact_id, artifact_kind FROM session_seen_artifacts WHERE session_id = ?").all(e),a=new Set(s.map(t=>`${t.artifact_kind}:${t.artifact_id}`)),r=[],n=[],d=new Set;for(const t of i){const o=`${t.kind}:${t.id}`;d.has(o)||(d.add(o),(a.has(o)?r:n).push({id:t.id,kind:t.kind}))}const c=s.filter(t=>!d.has(`${t.artifact_kind}:${t.artifact_id}`)).map(t=>({id:t.artifact_id,kind:t.artifact_kind}));return{served:r,unserved:n,unseen:c,ok:n.length===0}}prune(e=7){return this.db.prepare("DELETE FROM session_seen_artifacts WHERE shown_at < datetime('now', ? || ' days')").run(`-${e}`).changes??0}count(){return this.db.prepare("SELECT COUNT(*) as n FROM session_seen_artifacts").get().n}}export{f as SessionSeen};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v2",
3
- "version": "8.7.4",
3
+ "version": "8.7.5",
4
4
  "derivedFrom": "standard tier MINUS device-local/egress/subprocess (src/cloud-profile.ts)",
5
5
  "count": 20,
6
6
  "tools": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "8.7.4",
3
+ "version": "8.7.5",
4
4
  "tools": [
5
5
  {
6
6
  "name": "wyrm_recall",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "8.7.4",
3
+ "version": "8.7.5",
4
4
  "generatedBy": "scripts/gen-tool-manifest.mjs (from the dist/ registry modules — never hand-edited)",
5
5
  "tiers": {
6
6
  "core": [
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "wyrm-mcp",
3
- "version": "8.7.4",
3
+ "version": "8.7.5",
4
4
  "mcpName": "lk.ghosts/wyrm",
5
- "description": "Local-first persistent memory for AI agents over MCP. Ground truths, negative learning (recorded failures block repeats), decision causality, hybrid recall, negotiated recall (clean-mode injection you steer), live memory streams, run-attributed fleet memory a structured SQLite memory on your machine, no cloud or LLM required. Claude / Copilot / Cursor / Windsurf / Codex.",
5
+ "description": "Local-first persistent memory for AI agents over MCP. Ground truths, negative learning (recorded failures block repeats), decision causality, hybrid recall, negotiated recall (clean-mode injection you steer), live memory streams, run-attributed fleet memory \u2014 a structured SQLite memory on your machine, no cloud or LLM required. Claude / Copilot / Cursor / Windsurf / Codex.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "bin": {
@@ -29,6 +29,7 @@
29
29
  "scripts/hooks/wyrm-session-prune.mjs",
30
30
  "scripts/hooks/wyrm-tool-call-trace.mjs",
31
31
  "scripts/hooks/wyrm-run-auto.mjs",
32
+ "scripts/hooks/wyrm-citation-audit.mjs",
32
33
  "README.md",
33
34
  "BENCHMARKS.md",
34
35
  "COMPARISON.md",
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Wyrm citation audit (Stop hook) — enforcement for the grounding rules.
4
+ *
5
+ * 8.7.4 ships server instructions telling the model to cite the record id for
6
+ * anything it claims from memory and never to assert what it did not retrieve.
7
+ * Those are directives, and a directive is not a guarantee. This makes it a
8
+ * check.
9
+ *
10
+ * The transcript is the ledger. Everything Wyrm actually put in front of this
11
+ * session is in it: tool results, the SessionStart recall brief, injected
12
+ * prompt context. So a citation naming a record that never appeared in any of
13
+ * them was not retrieved — it was produced. That is decidable by set
14
+ * membership, with no model and no second opinion.
15
+ *
16
+ * Advisory by design: it reports, it does not block. A false accusation that
17
+ * silently eats a turn would be worse than the problem it polices.
18
+ *
19
+ * Claude Code passes a JSON payload on stdin including `transcript_path`.
20
+ */
21
+ import { readFileSync } from 'node:fs';
22
+
23
+ /**
24
+ * Canonical citation form the grounding rules mandate: `mem:5508`, `truth:921`.
25
+ *
26
+ * Horizontal whitespace only. `\s*` would span a newline and read prose like
27
+ * "...without being a JS failure:\n\n1. Phone with Data Saver" as `failure:1`,
28
+ * which is how the first version produced its only false positive across 25
29
+ * real transcripts. A citation never straddles a line break.
30
+ */
31
+ const CITE = /\b(mem|memory|truth|quest|failure)[ \t]*[:#][ \t]*(\d{1,7})\b/gi;
32
+
33
+ /** Normalize the aliases Wyrm renders (`mem:` and `memory #` are one kind). */
34
+ function norm(kind) {
35
+ const k = kind.toLowerCase();
36
+ return k === 'memory' ? 'mem' : k;
37
+ }
38
+
39
+ function idsIn(text) {
40
+ const out = new Set();
41
+ if (!text) return out;
42
+ for (const m of String(text).matchAll(CITE)) out.add(`${norm(m[1])}:${m[2]}`);
43
+ return out;
44
+ }
45
+
46
+ let payload = {};
47
+ try { payload = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { /* no/invalid stdin */ }
48
+ if (!payload || typeof payload !== 'object') payload = {};
49
+
50
+ const tpath = payload.transcript_path;
51
+ if (!tpath) process.exit(0);
52
+
53
+ let lines;
54
+ try { lines = readFileSync(tpath, 'utf8').split('\n'); } catch { process.exit(0); }
55
+
56
+ const served = new Set(); // every id Wyrm put in front of this session
57
+ const cited = new Set(); // every id the FINAL assistant turn asserted
58
+ let lastAssistantText = [];
59
+
60
+ for (const line of lines) {
61
+ if (!line.trim()) continue;
62
+ let o;
63
+ try { o = JSON.parse(line); } catch { continue; }
64
+ const type = o.type;
65
+
66
+ // Anything that is not the model's own prose is a hand-off TO the model:
67
+ // tool results, the SessionStart brief, injected context, attachments.
68
+ if (type === 'user' || type === 'system' || type === 'attachment') {
69
+ const content = o?.message?.content ?? o?.content ?? o?.text ?? '';
70
+ if (typeof content === 'string') {
71
+ for (const id of idsIn(content)) served.add(id);
72
+ } else if (Array.isArray(content)) {
73
+ for (const c of content) {
74
+ if (!c || typeof c !== 'object') continue;
75
+ const t = typeof c.content === 'string'
76
+ ? c.content
77
+ : Array.isArray(c.content)
78
+ ? c.content.map((x) => (typeof x === 'string' ? x : x?.text ?? '')).join('\n')
79
+ : (c.text ?? '');
80
+ for (const id of idsIn(t)) served.add(id);
81
+ }
82
+ }
83
+ continue;
84
+ }
85
+
86
+ if (type === 'assistant') {
87
+ const blocks = o?.message?.content;
88
+ if (!Array.isArray(blocks)) continue;
89
+ // A new assistant turn replaces the previous candidate: we audit the
90
+ // response the user actually ends up reading, not the whole conversation.
91
+ const texts = blocks.filter((c) => c?.type === 'text').map((c) => c.text ?? '');
92
+ if (texts.length) lastAssistantText = texts;
93
+ // A tool_use is the model asking, not asserting — its arguments echo ids
94
+ // it is looking up and must not count as a claim.
95
+ }
96
+ }
97
+
98
+ for (const t of lastAssistantText) for (const id of idsIn(t)) cited.add(id);
99
+
100
+ const fabricated = [...cited].filter((id) => !served.has(id));
101
+
102
+ if (fabricated.length === 0) process.exit(0);
103
+
104
+ // Surface it. Exit 0 with context so the finding reaches the transcript
105
+ // without killing the turn.
106
+ const detail = fabricated.sort().join(', ');
107
+ console.log(JSON.stringify({
108
+ hookSpecificOutput: {
109
+ hookEventName: 'Stop',
110
+ additionalContext:
111
+ `<wyrm-citation-audit>\n` +
112
+ `UNGROUNDED CITATION(S): ${detail}\n` +
113
+ `These record ids appear in the reply but were never returned by a Wyrm tool, ` +
114
+ `the SessionStart brief, or injected context in this session. ` +
115
+ `Per the grounding rules: cite only what was actually retrieved. ` +
116
+ `Retrieve them and correct the claim, or drop the citation.\n` +
117
+ `</wyrm-citation-audit>`,
118
+ },
119
+ }));
120
+ process.exit(0);