wyrm-mcp 8.5.4 → 8.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,6 +43,8 @@ npm install -g wyrm-mcp # install
43
43
  wyrm-setup # wire it into your AI clients, then restart them
44
44
  ```
45
45
 
46
+ > **On npm v12+**, npm denies dependency install scripts by default, which skips `better-sqlite3`'s native build — the install *succeeds* but `wyrm` then fails with *"Could not locate the bindings file"*. Install with the build allow-listed instead: `npm install -g wyrm-mcp --allow-scripts=wyrm-mcp,better-sqlite3` (`wyrm update` already does this for you). See [TROUBLESHOOTING.md](https://github.com/Ghosts-Protocol-Pvt-Ltd/Wyrm/blob/main/TROUBLESHOOTING.md).
47
+
46
48
  Then, from inside your client, ask it to call `wyrm_capabilities` to confirm the connection. The everyday loop is four steps the agent runs on its own once the habit sets in:
47
49
 
48
50
  ```
@@ -21,7 +21,7 @@ ${t.errors.map(r=>`- ${r}`).join(`
21
21
  - Checked: ${e.checkedAt} (${e.source})`;return e.updateAvailable&&(r+="\n\nRun `wyrm_self_update` with `confirm: true` to upgrade, or `npm install -g wyrm-mcp@latest` from your shell."),{content:[{type:"text",text:r}]}}},{name:"wyrm_self_update",description:"Run `npm install -g wyrm-mcp@latest` to upgrade. Returns the install transcript. Requires the operator to have write access to the global npm prefix.",inputSchema:{type:"object",properties:{confirm:{type:"boolean",description:"Must be true to actually run the upgrade (safety guard)."}},required:["confirm"]},annotations:l.wyrm_self_update,aliases:[],handler:async(o,c)=>{const{confirm:i=!1}=o;return i?{content:[{type:"text",text:`\u{F115D} **Self-update transcript**
22
22
 
23
23
  \`\`\`
24
- ${await new Promise(t=>{const e=_("npm",["install","-g","wyrm-mcp@latest"],{stdio:["ignore","pipe","pipe"]}),r=[];e.stdout.on("data",s=>r.push(s.toString())),e.stderr.on("data",s=>r.push(s.toString())),e.on("close",s=>{r.push(`
24
+ ${await new Promise(t=>{const e=_("npm",["install","-g","wyrm-mcp@latest","--allow-scripts=wyrm-mcp,better-sqlite3"],{stdio:["ignore","pipe","pipe"]}),r=[];e.stdout.on("data",s=>r.push(s.toString())),e.stderr.on("data",s=>r.push(s.toString())),e.on("close",s=>{r.push(`
25
25
  [exit ${s??"unknown"}]`),t(r.join(""))}),e.on("error",s=>t(`spawn failed: ${s.message}`))})}
26
26
  \`\`\`
27
27
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v2",
3
- "version": "8.5.4",
3
+ "version": "8.5.6",
4
4
  "derivedFrom": "standard tier MINUS device-local/egress/subprocess (src/cloud-profile.ts)",
5
5
  "count": 19,
6
6
  "tools": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "8.5.4",
3
+ "version": "8.5.6",
4
4
  "tools": [
5
5
  {
6
6
  "name": "wyrm_recall",
package/dist/wyrm-cli.js CHANGED
@@ -75,8 +75,8 @@ Delete these ${g.length} artifact(s)? Type CONFIRM to proceed: `,$)});if(h.close
75
75
  Database size: ${d.dbSize}`))}finally{s.close()}}async function St(l){const{positional:c,flags:e}=E(l),i=c[0]??"trend",n=M().version??null,t=j();try{const s=t.getDatabase();if(i==="snapshot"){const o=N(e.kind)||"all",r=[];if((o==="all"||o==="health")&&ce(s,"health",ze(s),n)!=null&&r.push("health"),(o==="all"||o==="effectiveness")&&ce(s,"effectiveness",Qe(s),n)!=null&&r.push("effectiveness"),o==="retrieval"||e.full===!0){const d=await Ct();d&&ce(s,"retrieval",d,n)!=null?r.push("retrieval"):d||console.log(p.dim(" retrieval: bench unavailable (run from the repo with bench/ present) \u2014 skipped"))}r.length?b(`metrics snapshot recorded: ${r.join(", ")} @ ${n??"unknown"}`):m("metrics snapshot: nothing recorded");return}if(i==="trend"){const o=N(e.kind),r=Number(N(e.limit))||14,d=o?[o]:["health","effectiveness","retrieval"];let a=!1;for(const u of d){const g=be(s,u,r);if(!g.length)continue;a=!0,k(`${u} \u2014 ${g.length} snapshot(s)`);const f=jt[u],y=g.slice().reverse().map(h=>[h.captured_at.slice(0,16),h.wyrm_version??"-",...f.map(w=>Et(h.metrics[w]))]);console.log(D(["when","ver",...f],y));for(const h of Xe(g,u))console.log(" "+p.yellow("!")+" "+h)}a||(console.log(p.dim(" No metrics yet. Record the first snapshot: wyrm metrics snapshot")),console.log(p.dim(" (health + effectiveness are cheap; add --full for retrieval recall@k.)")));return}if(i==="show"){const o=N(e.kind)||"health",r=be(s,o,1);if(!r.length){console.log(p.dim(` No ${o} snapshot yet.`));return}k(`latest ${o} @ ${r[0].captured_at} (${r[0].wyrm_version??"-"})`),console.log(JSON.stringify(r[0].metrics,null,2));return}m("Usage: wyrm metrics <snapshot [--kind all|health|effectiveness|retrieval] [--full] | trend [--kind K] [--limit N] | show [--kind K]>"),process.exitCode=1}finally{t.close()}}const jt={health:["memories","vectors","vector_coverage_pct","review_queue","unresolved_failures","provider"],effectiveness:["failure_blocks","rehydrate_calls","recall_calls","tokens_saved_total"],retrieval:["recall_at_1","recall_at_10","mrr","mode","queries"]};function Et(l){return l==null?"-":typeof l=="number"?Number.isInteger(l)?String(l):l.toFixed(3):String(l)}async function Ct(){try{const l=z(Q(import.meta.url)),c=O(l,"..","bench"),e=O(c,"lib","retrieval-eval.mjs"),i=O(c,"eval-corpus.json");if(!L(e)||!L(i))return null;const n=process.env.WYRM_VECTOR_PROVIDER||"none";process.env.WYRM_LIVE_MEMORY="0";const{evaluate:t}=await import(e),{WyrmDB:s}=await import("./database.js"),{MemoryArtifacts:o}=await import("./memory-artifacts.js"),r=JSON.parse(Y(i,"utf8")),d=`/tmp/wyrm-metrics-eval-${process.pid}.db`,a=new s(d);let u;try{u=t(r,a,o,{k:10})}finally{a.close();for(const f of["","-wal","-shm"])try{Le(d+f,{force:!0})}catch{}}const g=u.quality.overall;return{mode:n==="none"?"fts-floor":n,k:10,recall_at_1:g.recall_at_1??null,recall_at_10:g.recall_at_k??null,mrr:g.mrr??null,queries:g.n??u.performance?.queries??null,p50_ms:u.performance?.p50_ms??null,p95_ms:u.performance?.p95_ms??null}}catch{return null}}async function Rt(l){const{resolveEmbeddingState:c}=await import("./providers/embedding-provider.js"),e=j();let i=0;const n=(s,o)=>{console.log(` ${p.green("\u2714")} ${s} \u2014 ${p.dim(o)}`)},t=(s,o,r)=>{i++,console.log(` ${p.red("\u2716")} ${s} \u2014 ${o}`),console.log(` ${p.yellow("fix:")} ${r}`)};try{const s=e.getDatabase();k("wyrm doctor");const o=c();o.reason===""&&o.resolved!=="local"?n("Embedding provider",`${o.resolved} (${o.model})${o.egressHost?` \xB7 egress: ${o.egressHost}`:" \xB7 local"}`):o.resolved==="local"?t("Embedding provider","local-hash (hash-384) is NOT semantic \u2014 test-only vectors",o.fix||"use NVIDIA NIM: set WYRM_VECTOR_PROVIDER=nim + NVIDIA_API_KEY"):o.reason==="ollama_deprecated"?t("Embedding provider","ollama (deprecated \u2014 leaves in the next major)",o.fix):t("Embedding provider",`none (${o.reason})`,o.fix);const r=s.prepare("SELECT COUNT(*) AS n FROM memory_artifacts").get().n;let d=0;try{d=s.prepare("SELECT COUNT(*) AS n FROM vectors").get().n}catch{}o.resolved==="none"||o.resolved==="local"?t("Vector index",`${d} vector(s) / ${r} memories \u2014 recall is FTS5 keyword-only (~59.9% vs 72.2% hybrid recall@10)`,o.fix||"enable a provider, then: wyrm index rebuild"):d===0&&r>0?t("Vector index",`provider '${o.resolved}' is configured but 0 of ${r} memories are indexed`,"wyrm index rebuild"):n("Vector index",`${d} vector(s) over ${r} memories`);try{const g=s.prepare("SELECT COUNT(*) AS n FROM memory_artifacts_fts").get().n;g===r?n("FTS index",`${g}/${r} rows in sync`):t("FTS index",`${g} FTS rows vs ${r} memories \u2014 keyword recall is missing rows`,"INSERT INTO memory_artifacts_fts(memory_artifacts_fts) VALUES('rebuild') via wyrm maintenance")}catch{t("FTS index","memory_artifacts_fts unreadable","wyrm maintenance")}const a=s.prepare("SELECT COUNT(*) AS n FROM failure_patterns WHERE resolved = 0").get().n;n("Failure firewall",`${a} unresolved pattern(s) \xB7 matching = exact signature + FTS5 fuzzy (no vector tier in this build)`);const u=s.prepare("SELECT COUNT(*) AS n FROM memory_artifacts WHERE needs_review = 1").get().n;u>0?t("Review queue",`${u} memor${u===1?"y":"ies"} invisible to recall until approved`,"wyrm review"):n("Review queue","empty");try{const g=s.prepare("SELECT MAX(version) AS v FROM schema_versions").get().v;n("Schema",`migration v${g??0}`)}catch{t("Schema","schema_versions unreadable","wyrm maintenance")}console.log(""),i===0?b("All checks passed \u2014 nothing is silently off."):(m(`${i} check(s) DEGRADED \u2014 the fixes above are exact.`),process.exitCode=1)}finally{e.close()}}async function xt(l){const{positional:c,flags:e}=E(l),i=c[0]??"list",n=j();try{const t=n.getDatabase();if(i==="list"){const s=t.prepare("SELECT id, scope, target, occurrences, last_seen FROM failure_patterns WHERE resolved = 0 ORDER BY last_seen DESC LIMIT 25").all();if(k("Unresolved failures"),!s.length){console.log(p.dim(" none \u2014 the firewall has nothing active."));return}console.log(D(["ID","Scope","Target","Seen","Last seen"],s.map(o=>[String(o.id),o.scope,o.target.slice(0,48),String(o.occurrences),o.last_seen]))),console.log(p.dim(`
76
76
  resolve one: wyrm failure resolve <id>`));return}if(i==="resolve"){const s=Number(c[1]);if(!Number.isInteger(s)||s<=0){m('Usage: wyrm failure resolve <id> [--note "root cause fixed by ..."]'),process.exitCode=1;return}const o=N(e.note);(t.prepare("UPDATE failure_patterns SET resolved = 1, resolution_note = ?, last_seen = datetime('now') WHERE id = ? AND resolved = 0").run(o||"resolved via wyrm failure resolve",s).changes??0)>0?b(`Failure #${s} resolved \u2014 it will no longer flag.`):(m(`No unresolved failure #${s}.`),process.exitCode=1);return}m("Usage: wyrm failure <list|resolve <id> [--note N]>"),process.exitCode=1}finally{n.close()}}async function Tt(l){const{positional:c,flags:e}=E(l),i=c[0]??"list",n=j();try{if(i==="add"){const t=c[1]??process.cwd(),s=Ne(t);if(!L(s)){m(`Path does not exist: ${s}`),process.exitCode=1;return}const o=N(e.name)||Me(s),r=n.registerProject(o,s);b(`Project #${r.id} '${r.name}' registered at ${r.path}`);return}if(i==="list"){const t=n.getAllProjects(50);k("Projects"),console.log(D(["ID","Name","Path"],t.map(s=>[String(s.id),s.name,s.path])));return}m("Usage: wyrm project <add [path] [--name N] | list>"),process.exitCode=1}finally{n.close()}}async function Dt(l){const{positional:c,flags:e}=E(l),i="Usage: wyrm index <setup|rebuild|status> [--provider auto|local|nim|openai|none] [--model M] [--project P] [--dry-run]";if(e.help===!0||typeof e.help=="string"){console.log(i);return}const n=c[0]??"status",{createVectorStore:t}=await import("./vectors.js"),o={provider:e.provider??process.env.WYRM_VECTOR_PROVIDER??"auto",model:e.model,apiKey:e["api-key"]??process.env.OPENAI_API_KEY,ollamaUrl:e["ollama-url"]};if(n==="setup"){const{createProvider:d}=await import("./providers/embedding-provider.js"),a=d(o);if(!await a.isReady()&&o.provider!=="none"){m(`Provider not ready: ${a.name}. Check the configuration and try again.`),process.exitCode=1;return}b(`Vector provider verified: ${a.name} (model ${a.model}, ${a.dimensions}d)`),console.log(p.dim(" The MCP server reads WYRM_VECTOR_PROVIDER (and provider-specific env) at boot \u2014")),console.log(p.dim(` set WYRM_VECTOR_PROVIDER=${a.name==="none"?"none":o.provider} in the server env, then: wyrm index rebuild`));return}const r=j();try{const d=r.getDatabase(),a=t(o,d);if(n==="status"){const u=a.getStats();k("Vector index");const g=[["Provider",u.provider],["Model",u.model],["Vectors",String(u.total)],...Object.entries(u.byType).map(([f,y])=>[` ${f}`,String(y)])];console.log(D(["Field","Value"],g));return}if(n==="rebuild"){const{reindexProjects:u}=await import("./reindex.js"),g=e["dry-run"]===!0,f=e.project;let y;if(f){const $=r.getProject(f)??x(r,f);if(!$){m(`Project not found: ${f}`),process.exitCode=1;return}y=[$.id]}else y=r.getAllProjects(1e3).map($=>$.id);const{indexed:h,skipped:w}=await u(d,a,y,{dryRun:g,onError:($,C)=>m(`${$}: ${JSON.stringify(C)}`)});b(`Reindex ${g?"(dry run) ":""}\u2014 ${y.length} project(s), ${h} indexed, ${w} skipped`);return}m(i),process.exitCode=1}finally{r.close()}}function N(l){return typeof l=="string"?l:""}function It(l){try{return new URL(l).host}catch{return l}}async function Pt(l){const{flags:c}=E(l),{getUpdateStatus:e}=await import("./version-check.js"),i=M().version??"0.0.0",n=j();let t;try{t=await e(n.getDatabase(),i,{force:c.force===!0||c.check===!0})}finally{n.close()}if(k("Wyrm update"),console.log(D(["Field","Value"],[["Current",t.current],["Latest",t.latest??"unknown (offline?)"],["Update available",t.updateAvailable?"yes":"no"],["Checked",`${t.checkedAt} (${t.source})`]])),c.check===!0)return;if(!t.updateAvailable&&c.force!==!0){console.log(p.dim(`
77
77
  Already up to date. (Use --force to reinstall anyway.)`));return}console.log(p.dim(`
78
- Running: npm install -g wyrm-mcp@latest
79
- `));const s=X("npm",["install","-g","wyrm-mcp@latest"],{stdio:"inherit",shell:!1});s.status===0?b("Updated. Restart your MCP clients to pick up the new binary."):(m(`npm install exited with ${s.status??"unknown"}`),process.exitCode=s.status??1)}async function Nt(l){const{positional:c,flags:e}=E(l),i=c[0],n=e.project??process.cwd();if(i==="inject"){const{injectSystemPrompt:t}=await import("./autoconfig.js"),s=typeof e.clients=="string"?e.clients.split(",").map(r=>r.trim()).filter(Boolean):[],o=t(n,s);k("System prompt injection");for(const r of o.injected)console.log(` + ${r}`);for(const r of o.skipped)console.log(p.dim(` o skipped (unknown client): ${r}`));for(const r of o.errors)m(r);o.injected.length>0&&b("AI clients in this project will now call wyrm_session_prime at conversation start."),o.errors.length>0&&(process.exitCode=1);return}if(i==="migrate"){const{migrateProject:t,renderMigrationReport:s}=await import("./migrate-prompt.js"),{WYRM_INJECT_BLOCK:o}=await import("./autoconfig.js"),r=e.apply===!0,d=t({projectPath:n,newBlock:o,apply:r});console.log(s(d,r));return}m("Usage: wyrm prompt inject [--project <path>] [--clients copilot,cursor] | wyrm prompt migrate [--project <path>] [--apply]"),process.exitCode=1}async function Mt(l){const{positional:c,flags:e}=E(l);(c[0]??"report")!=="report"&&(m("Usage: wyrm hours report --from YYYY-MM-DD --to YYYY-MM-DD [--project <name>] [--session-hours H] [--json]"),process.exit(1));const n=e.from,t=e.to;(!n||!t)&&(m("--from and --to are required (YYYY-MM-DD)"),process.exit(1));const{HourLedger:s}=await import("./hours.js"),o=j();try{const r=e.project,d=r?x(o,r):null;if(r&&!d){m(`Project not found: ${r}`),process.exitCode=1;return}const a=new s(o.getDatabase()).report({range_start:n,range_end:t,project_id:d?.id,default_session_hours:ee(e["session-hours"],1)});if(e.json===!0){console.log(JSON.stringify(a,null,2));return}if(k(`Hours ${a.range.start} \u2192 ${a.range.end}`),a.by_project.length===0){console.log(p.dim(" No sessions in range."));return}console.log(D(["Project","Sessions","Hours"],a.by_project.map(u=>[u.project_name,String(u.session_count),u.hours.toFixed(2)]))),console.log(`
78
+ Running: npm install -g wyrm-mcp@latest --allow-scripts=wyrm-mcp,better-sqlite3
79
+ `));const s=X("npm",["install","-g","wyrm-mcp@latest","--allow-scripts=wyrm-mcp,better-sqlite3"],{stdio:"inherit",shell:!1});s.status===0?b("Updated. Restart your MCP clients to pick up the new binary."):(m(`npm install exited with ${s.status??"unknown"}`),process.exitCode=s.status??1)}async function Nt(l){const{positional:c,flags:e}=E(l),i=c[0],n=e.project??process.cwd();if(i==="inject"){const{injectSystemPrompt:t}=await import("./autoconfig.js"),s=typeof e.clients=="string"?e.clients.split(",").map(r=>r.trim()).filter(Boolean):[],o=t(n,s);k("System prompt injection");for(const r of o.injected)console.log(` + ${r}`);for(const r of o.skipped)console.log(p.dim(` o skipped (unknown client): ${r}`));for(const r of o.errors)m(r);o.injected.length>0&&b("AI clients in this project will now call wyrm_session_prime at conversation start."),o.errors.length>0&&(process.exitCode=1);return}if(i==="migrate"){const{migrateProject:t,renderMigrationReport:s}=await import("./migrate-prompt.js"),{WYRM_INJECT_BLOCK:o}=await import("./autoconfig.js"),r=e.apply===!0,d=t({projectPath:n,newBlock:o,apply:r});console.log(s(d,r));return}m("Usage: wyrm prompt inject [--project <path>] [--clients copilot,cursor] | wyrm prompt migrate [--project <path>] [--apply]"),process.exitCode=1}async function Mt(l){const{positional:c,flags:e}=E(l);(c[0]??"report")!=="report"&&(m("Usage: wyrm hours report --from YYYY-MM-DD --to YYYY-MM-DD [--project <name>] [--session-hours H] [--json]"),process.exit(1));const n=e.from,t=e.to;(!n||!t)&&(m("--from and --to are required (YYYY-MM-DD)"),process.exit(1));const{HourLedger:s}=await import("./hours.js"),o=j();try{const r=e.project,d=r?x(o,r):null;if(r&&!d){m(`Project not found: ${r}`),process.exitCode=1;return}const a=new s(o.getDatabase()).report({range_start:n,range_end:t,project_id:d?.id,default_session_hours:ee(e["session-hours"],1)});if(e.json===!0){console.log(JSON.stringify(a,null,2));return}if(k(`Hours ${a.range.start} \u2192 ${a.range.end}`),a.by_project.length===0){console.log(p.dim(" No sessions in range."));return}console.log(D(["Project","Sessions","Hours"],a.by_project.map(u=>[u.project_name,String(u.session_count),u.hours.toFixed(2)]))),console.log(`
80
80
  Total: ${a.total_hours.toFixed(2)}h across ${a.entries.length} session(s)`+(a.estimated_sessions>0?p.dim(` (${a.estimated_sessions} estimated)`):""))}finally{o.close()}}async function Ot(l){const{positional:c,flags:e}=E(l),i=c[0]??"generate",n=e.client,t=ee(e.rate,NaN),s=e.from,o=e.to;(i!=="generate"||!n||!Number.isFinite(t)||!s||!o)&&(m('Usage: wyrm invoice generate --client <name> --rate <usd/hour> --from YYYY-MM-DD --to YYYY-MM-DD [--project <name>] [--number INV-X] [--currency USD] [--notes "\u2026"] [--out <path>]'),process.exit(1));const{HourLedger:r}=await import("./hours.js"),d=j();try{const a=e.project,u=a?x(d,a):null;if(a&&!u){m(`Project not found: ${a}`),process.exitCode=1;return}const g=new r(d.getDatabase()).invoice({client_name:n,hourly_rate_usd:t,range_start:s,range_end:o,project_id:u?.id,invoice_number:e.number,currency:e.currency,notes:e.notes,business_name:e["business-name"],business_address:e["business-address"],business_contact:e["business-contact"],client_address:e["client-address"],default_session_hours:ee(e["session-hours"],1)}),f=e.out;if(f){const{writeFileSync:y}=await import("node:fs");y(f,g+`
81
81
  `,"utf-8"),b(`Invoice written to ${f}`)}else process.stdout.write(g+`
82
82
  `)}finally{d.close()}}async function At(l){const{positional:c,flags:e}=E(l),i=c[0]??"status",{AgentDaemon:n}=await import("./agent-daemon.js"),t=j();try{const s=new n(t.getDatabase());switch(i){case"init":case"start":{const o=Math.max(10,Math.min(I(e.interval,600),86400)),r=s.start({interval_seconds:o,max_steps:e["max-steps"]!==void 0&&I(e["max-steps"],0)||void 0,project_path:e.project,verbose:e.verbose===!0});if(!r.ok){m(`Agent init failed: ${r.error}`),process.exitCode=1;return}const d=r.status;b(`Agent ${d.pid!=null&&d.pid!==r.pid?"already running":"started"} \u2014 pid ${d.pid}`),console.log(` Interval: ${o}s \xB7 Active goals: ${d.active_goals} \xB7 Total iterations: ${d.total_iterations}`),console.log(p.dim(` Log: ${d.log_file}`));return}case"status":{const o=s.status();k("Wyrm agent"),console.log(o.running?` RUNNING \u2014 pid ${o.pid}${o.started_at?` (since ${o.started_at})`:""}`:" NOT RUNNING. Start it with: wyrm agent init"),console.log(` Active goals: ${o.active_goals} \xB7 Total iterations: ${o.total_iterations}`),o.last_action&&console.log(` Last action (${o.last_action.ran_at}): ${o.last_action.summary} [${o.last_action.result_status??"?"}]`),e.log===!0&&(console.log(`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "8.5.4",
3
+ "version": "8.5.6",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "wyrm-mcp",
3
- "version": "8.5.4",
3
+ "version": "8.5.6",
4
4
  "mcpName": "lk.ghosts/wyrm",
5
5
  "description": "Local-first persistent memory for AI agents over MCP. Ground truths, negative learning (recorded failures block repeats), decision causality, hybrid recall, live memory streams, run-attributed fleet memory — a structured SQLite memory on your machine, no cloud or LLM required. Claude / Copilot / Cursor / Windsurf / Codex.",
6
6
  "type": "module",
@@ -16,7 +16,32 @@
16
16
  */
17
17
  import { readFileSync, existsSync } from 'node:fs';
18
18
  import { spawnSync } from 'node:child_process';
19
- import { basename } from 'node:path';
19
+ import { basename, join } from 'node:path';
20
+ import { homedir } from 'node:os';
21
+
22
+ // Portable, self-healing `wyrm` resolution: bare command first (honors PATH),
23
+ // then npm/user CLI-install locations that exist here — so a minimal PATH
24
+ // (containers / CI / cron) doesn't silently drop the capture. NOT /usr/bin: a
25
+ // distro or global scoped-package can put an unrelated `wyrm` there, and
26
+ // resolving to the wrong binary is worse than not resolving at all.
27
+ function wyrmCandidates() {
28
+ const home = homedir();
29
+ return [
30
+ 'wyrm',
31
+ join(home, '.npm-global', 'bin', 'wyrm'),
32
+ join(home, '.local', 'bin', 'wyrm'),
33
+ join(home, 'node_modules', '.bin', 'wyrm'),
34
+ '/usr/local/bin/wyrm',
35
+ '/opt/homebrew/bin/wyrm',
36
+ ].filter((c) => c === 'wyrm' || existsSync(c));
37
+ }
38
+ function runWyrm(args, opts) {
39
+ for (const bin of wyrmCandidates()) {
40
+ const r = spawnSync(bin, args, opts);
41
+ if (!(r.error && r.error.code === 'ENOENT')) return r; // spawned (even if non-zero exit)
42
+ }
43
+ return { status: 127, stdout: '', error: new Error('wyrm not found') };
44
+ }
20
45
 
21
46
  function extractText(content) {
22
47
  if (content == null) return '';
@@ -49,7 +74,7 @@ const tpath = payload.transcript_path;
49
74
  // harness dies without a SessionEnd, pid-liveness marks the row DEAD anyway.
50
75
  try {
51
76
  if (event === 'SessionEnd' && payload.session_id) {
52
- spawnSync('wyrm', ['presence', 'release', '--agent', `claude-${String(payload.session_id).slice(0, 8)}`],
77
+ runWyrm(['presence', 'release', '--agent', `claude-${String(payload.session_id).slice(0, 8)}`],
53
78
  { timeout: 10000, stdio: 'ignore' });
54
79
  }
55
80
  } catch { /* never break the session */ }
@@ -105,7 +130,7 @@ if (process.env.WYRM_CAPTURE_DRYRUN === '1') {
105
130
  }
106
131
 
107
132
  try {
108
- spawnSync('wyrm', ['capture', content, '--project', project, '--mode', 'memory'], {
133
+ runWyrm(['capture', content, '--project', project, '--mode', 'memory'], {
109
134
  timeout: 25000,
110
135
  stdio: 'ignore',
111
136
  });
@@ -121,7 +146,7 @@ try {
121
146
  if (payload.session_id) sessionArgs.push('--run', String(payload.session_id).slice(0, 64));
122
147
  if (objective) sessionArgs.push('--objectives', objective);
123
148
  sessionArgs.push('--completed', content);
124
- spawnSync('wyrm', sessionArgs, { timeout: 25000, stdio: 'ignore' });
149
+ runWyrm(sessionArgs, { timeout: 25000, stdio: 'ignore' });
125
150
  } catch { /* older wyrm without `session log`, or write failed — never break the session */ }
126
151
 
127
152
  process.exit(0);
@@ -17,11 +17,36 @@
17
17
  *
18
18
  * @copyright 2026 Ghost Protocol (Pvt) Ltd.
19
19
  */
20
- import { readFileSync } from 'node:fs';
20
+ import { readFileSync, existsSync } from 'node:fs';
21
21
  import { spawnSync } from 'node:child_process';
22
+ import { homedir } from 'node:os';
23
+ import { join } from 'node:path';
22
24
 
23
25
  if (process.env.WYRM_AUTO_PRUNE !== '1') process.exit(0); // opt-in only
24
26
 
27
+ // Portable, self-healing `wyrm` resolution: bare command first (honors PATH),
28
+ // then npm/user CLI-install locations that exist here — so a minimal PATH
29
+ // (containers / CI / cron) doesn't silently skip the prune. NOT /usr/bin: a
30
+ // distro or global scoped-package can put an unrelated `wyrm` there.
31
+ function wyrmCandidates() {
32
+ const home = homedir();
33
+ return [
34
+ 'wyrm',
35
+ join(home, '.npm-global', 'bin', 'wyrm'),
36
+ join(home, '.local', 'bin', 'wyrm'),
37
+ join(home, 'node_modules', '.bin', 'wyrm'),
38
+ '/usr/local/bin/wyrm',
39
+ '/opt/homebrew/bin/wyrm',
40
+ ].filter((c) => c === 'wyrm' || existsSync(c));
41
+ }
42
+ function runWyrm(args, opts) {
43
+ for (const bin of wyrmCandidates()) {
44
+ const r = spawnSync(bin, args, opts);
45
+ if (!(r.error && r.error.code === 'ENOENT')) return r; // spawned (even if non-zero exit)
46
+ }
47
+ return { status: 127, stdout: '', error: new Error('wyrm not found') };
48
+ }
49
+
25
50
  let payload = {};
26
51
  try { payload = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { /* no/invalid stdin */ }
27
52
 
@@ -34,7 +59,7 @@ try {
34
59
  // Scope by EXACT project path (--path), not a directory-name match, so we
35
60
  // never touch another project's memories. If this directory maps to no
36
61
  // project, prune exits without deleting anything (fine).
37
- spawnSync('wyrm', [
62
+ runWyrm([
38
63
  'prune', '--no-dry-run', '--yes',
39
64
  '--path', cwd,
40
65
  '--min-confidence', minConf,
@@ -16,8 +16,37 @@
16
16
  *
17
17
  * @copyright 2026 Ghost Protocol (Pvt) Ltd.
18
18
  */
19
- import { readFileSync } from 'node:fs';
19
+ import { readFileSync, existsSync } from 'node:fs';
20
20
  import { spawnSync } from 'node:child_process';
21
+ import { homedir } from 'node:os';
22
+ import { join } from 'node:path';
23
+
24
+ // ── Portable, self-healing `wyrm` resolution ────────────────────────────────
25
+ // Claude Code can spawn hooks with a minimal PATH (containers, CI, cron, non-
26
+ // login shells), which would silently disable memory. Try the bare command
27
+ // first (zero overhead where PATH is fine); only on ENOENT fall back to well-
28
+ // known npm/user CLI-install locations that actually exist on THIS machine.
29
+ // Deliberately NOT /usr/bin: a distro or global scoped-package can put an
30
+ // unrelated `wyrm` there (e.g. the http-server entrypoint), and resolving to
31
+ // the wrong binary is worse than not resolving at all.
32
+ function wyrmCandidates() {
33
+ const home = homedir();
34
+ return [
35
+ 'wyrm',
36
+ join(home, '.npm-global', 'bin', 'wyrm'),
37
+ join(home, '.local', 'bin', 'wyrm'),
38
+ join(home, 'node_modules', '.bin', 'wyrm'),
39
+ '/usr/local/bin/wyrm',
40
+ '/opt/homebrew/bin/wyrm',
41
+ ].filter((c) => c === 'wyrm' || existsSync(c));
42
+ }
43
+ function runWyrm(args, opts) {
44
+ for (const bin of wyrmCandidates()) {
45
+ const r = spawnSync(bin, args, opts);
46
+ if (!(r.error && r.error.code === 'ENOENT')) return r; // spawned (even if non-zero exit)
47
+ }
48
+ return { status: 127, stdout: '', error: new Error('wyrm not found') };
49
+ }
21
50
 
22
51
  let payload = {};
23
52
  try { payload = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { /* no/invalid stdin */ }
@@ -36,7 +65,7 @@ const cwd = (typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : proc
36
65
  try {
37
66
  if (payload.session_id) {
38
67
  const agentId = `claude-${String(payload.session_id).slice(0, 8)}`;
39
- spawnSync('wyrm', [
68
+ runWyrm([
40
69
  'presence', 'announce',
41
70
  '--agent', agentId,
42
71
  '--kind', 'claude',
@@ -50,7 +79,7 @@ let brief = '';
50
79
  try {
51
80
  // --quiet keeps the "nothing to restore" note off stderr; --path scopes to
52
81
  // this directory's project; --max-chars bounds the injection.
53
- const res = spawnSync('wyrm', ['rehydrate', '--path', cwd, '--quiet', '--max-chars', '6000'], {
82
+ const res = runWyrm(['rehydrate', '--path', cwd, '--quiet', '--max-chars', '6000'], {
54
83
  timeout: 15000,
55
84
  encoding: 'utf8',
56
85
  });