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 +2 -0
- package/dist/handlers/companion.js +1 -1
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/wyrm-cli.js +2 -2
- package/dist/wyrm-manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/hooks/wyrm-session-capture.mjs +29 -4
- package/scripts/hooks/wyrm-session-prune.mjs +27 -2
- package/scripts/hooks/wyrm-session-rehydrate.mjs +32 -3
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
|
|
package/dist/tool-manifest.json
CHANGED
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(`
|
package/dist/wyrm-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wyrm-mcp",
|
|
3
|
-
"version": "8.5.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
82
|
+
const res = runWyrm(['rehydrate', '--path', cwd, '--quiet', '--max-chars', '6000'], {
|
|
54
83
|
timeout: 15000,
|
|
55
84
|
encoding: 'utf8',
|
|
56
85
|
});
|