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.
package/dist/autoconfig.js
CHANGED
|
@@ -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)+`
|
package/dist/session-seen.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
class
|
|
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,
|
|
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(
|
|
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};
|
package/dist/tool-manifest.json
CHANGED
package/dist/wyrm-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wyrm-mcp",
|
|
3
|
-
"version": "8.7.
|
|
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
|
|
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);
|