wyrm-mcp 8.5.3 → 8.5.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/handlers/session.js +8 -8
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- 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/dist/handlers/session.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{basename as X}from"path";import{createHash as Z}from"crypto";import{blockText as Q}from"./types.js";import{TOOL_ANNOTATIONS as T}from"../tool-annotations.js";import{renderResult as v,withGlyph as N,cacheKeyFor as O}from"../render.js";import{computeStaleness as ee}from"../intelligence.js";import{ValidationError as te,asInt as se,asString as ne}from"../validate.js";import{sanitizeActorId as ie}from"./boundary.js";import{makeEstimator as oe}from"../token-budget.js";function G(e){return!e||!e.thread.trim()?null:`### \u{1F9F5} Where We Left Off _(the living thread \u2014 what we were working on and thinking about)_
|
|
1
|
+
import{basename as X}from"path";import{createHash as Z}from"crypto";import{blockText as Q}from"./types.js";import{TOOL_ANNOTATIONS as T}from"../tool-annotations.js";import{renderResult as v,withGlyph as N,cacheKeyFor as O}from"../render.js";import{computeStaleness as ee}from"../intelligence.js";import{ValidationError as te,asInt as se,asString as ne}from"../validate.js";import{sanitizeActorId as ie}from"./boundary.js";import{makeEstimator as oe}from"../token-budget.js";import{resolveProjectForPath as re}from"../project-resolve.js";function G(e){return!e||!e.thread.trim()?null:`### \u{1F9F5} Where We Left Off _(the living thread \u2014 what we were working on and thinking about)_
|
|
2
2
|
`+`_updated ${e.updated_at}${e.updated_by?" \xB7 "+e.updated_by:""} \xB7 ${e.turn_count} turn(s)_`+`
|
|
3
3
|
|
|
4
|
-
`+e.thread.slice(-1800)}const
|
|
5
|
-
`;const n=new Map;for(const i of e)n.has(i.category)||n.set(i.category,[]),n.get(i.category).push(i);for(const[i,r]of n){s+=`**${
|
|
4
|
+
`+e.thread.slice(-1800)}const ce={tech_stack:"\u{1F6E0}\uFE0F Tech Stack",architecture:"\u{1F3D7}\uFE0F Architecture",constraint:"\u{1F6A7} Constraints",decision:"\u{1F4CB} Decisions",contact:"\u{1F464} Contacts",other:"\u{1F4CC} Other Facts"};function J(e,t){let s=`### \u{1F4DA} Project Ground Truths _(treat as authoritative \u2014 do not infer or contradict)_
|
|
5
|
+
`;const n=new Map;for(const i of e)n.has(i.category)||n.set(i.category,[]),n.get(i.category).push(i);for(const[i,r]of n){s+=`**${ce[i]??i}:**
|
|
6
6
|
`;for(const a of r){let p="";if(t){const l=ee(a);l!==null&&l>.7&&(p="[\u26A0\uFE0F STALE] ")}s+=`- ${p}**${a.key}:** ${a.value}${a.rationale?` _(${a.rationale})_`:""}
|
|
7
7
|
`}}return s}const V=`
|
|
8
8
|
SELECT * FROM quests
|
|
@@ -13,11 +13,11 @@ import{basename as X}from"path";import{createHash as Z}from"crypto";import{block
|
|
|
13
13
|
LIMIT 5
|
|
14
14
|
`;function Y(e){const t={critical:"\u{1F534}",high:"\u{1F7E0}",medium:"\u{1F7E1}",low:"\u{1F7E2}"};let s=`### \u{1F3AF} Active Quests
|
|
15
15
|
`;for(const n of e)s+=`- ${t[n.priority]??"\u2022"} #${n.id}: ${n.title}
|
|
16
|
-
`;return s}function
|
|
16
|
+
`;return s}function P(e,t,s,n){if(s)return e.getProjectById(s);if(n)return t.prepare("SELECT * FROM projects WHERE LOWER(name) LIKE LOWER(?) LIMIT 1").get(`%${n}%`);const{id:i}=re(t,process.cwd());return i!=null?e.getProjectById(i):t.prepare(`
|
|
17
17
|
SELECT p.* FROM projects p
|
|
18
18
|
JOIN sessions s ON s.project_id = p.id
|
|
19
19
|
ORDER BY s.created_at DESC LIMIT 1
|
|
20
|
-
`).get()}function
|
|
20
|
+
`).get()}function q(e){const t=e.getAllProjects(20);return t.length===0?{content:[{type:"text",text:"No projects found. Run `wyrm_scan_projects` first."}],isError:!0}:{content:[{type:"text",text:`Project not found. Available projects:
|
|
21
21
|
${t.map(n=>`- ${n.name} (ID: ${n.id})`).join(`
|
|
22
22
|
`)}`}],isError:!0}}const K=(e,t)=>{const s=e.session_id!==null?`
|
|
23
23
|
Session logged: #${e.session_id}`:"";return N(t.brand,`Wyrm primed for **${e.project_name}** | ${e.truth_count} truth${e.truth_count!==1?"s":""} - ${e.quest_count} quest${e.quest_count!==1?"s":""} - ${e.memory_count} memor${e.memory_count!==1?"ies":"y"}${s}`)},L=(e,t)=>{const s=K(e,t);return e.sections.length>0?e.sections.map(n=>n.text).join(`
|
|
@@ -28,17 +28,17 @@ Session logged: #${e.session_id}`:"";return N(t.brand,`Wyrm primed for **${e.pro
|
|
|
28
28
|
|
|
29
29
|
`+s:s+`
|
|
30
30
|
|
|
31
|
-
_No memory stored yet -- start working and Wyrm will learn._`},A={summary:K};async function F(e,t,s){try{const{logSavings:n}=await import("../statusline.js"),i=Q(s.content[0]).length,r=Math.round(i/4),a=e.prepare("SELECT id FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(t);n(e,"wyrm_session_prime","recovered_context",r,a?.id)}catch{}}const
|
|
31
|
+
_No memory stored yet -- start working and Wyrm will learn._`},A={summary:K};async function F(e,t,s){try{const{logSavings:n}=await import("../statusline.js"),i=Q(s.content[0]).length,r=Math.round(i/4),a=e.prepare("SELECT id FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(t);n(e,"wyrm_session_prime","recovered_context",r,a?.id)}catch{}}const ae={continuity:{path:"continuity",tool:"wyrm_session_prime"},truths:{path:"truths",tool:"wyrm_truth_get"},scaffold:{path:"scaffolds",tool:"wyrm_context_build"},memory:{path:"memory",tool:"wyrm_recall"},quests:{path:"quests",tool:"wyrm_all_quests"}},de=48,pe=1200;async function ue(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a}){const{project_id:p,project_name:l,task:d,log_session:y}=e,c=ie(e.run_id);if(c===null)throw new te("run_id",'must be <=64 printable-ASCII chars (no ";")');const u=ne("role",e.role,{maxLen:200})??"",j=se("token_budget",e.token_budget,{min:1})??pe,_=s(),w=()=>_.prepare("SELECT body_json FROM run_briefs WHERE run_id = ? AND role = ?").get(c,u),h=p||l?P(t,_,p,l):void 0;if((p||l)&&!h)return q(t);let S=w();if(S&&h){const o=JSON.parse(S.body_json);if(o.project_id!==h.id)return{content:[{type:"text",text:`Fleet brief for (run ${c}, role '${u}') is pinned to project '${o.project_name}' (ID: ${o.project_id}) \u2014 refusing to serve it for requested project '${h.name}' (ID: ${h.id}). Use a distinct role for a second project, or prime without run_id.`}],isError:!0}}if(!S){const o=h??P(t,_,p,l);if(!o)return q(t);const m=[],$=G(t.getContinuity(o.id));$&&m.push({kind:"continuity",text:$});const k=i.getCurrent(o.id);k.length>0&&m.push({kind:"truths",text:J(k,!1).slice(0,1200)});const x=d??(u||void 0);if(x){const b=r.findBest(x,o.id,.3);b&&m.push({kind:"scaffold",text:r.formatForContext(b,1500)})}const M=n.buildContextBrief(o.id,x??"general",{maxItems:10,minConfidence:.3});M.text&&m.push({kind:"memory",text:M.text.slice(0,2e3)});const I=_.prepare(V).all(o.id);I.length>0&&m.push({kind:"quests",text:Y(I).slice(0,800)});const D=oe(),R=[];let C=de;for(const b of m){const W=D.count(b.text)+2;if(C+W<=j)R.push(b),C+=W;else{const H=ae[b.kind]??{path:b.kind,tool:"wyrm_search"},U=`[beyond token_budget] wyrm://project/${o.id}/${H.path} -- fetch via ${H.tool}`;R.push({kind:b.kind,text:U}),C+=D.count(U)+2}}let B=null;y&&(B=t.createSession(o.id,{objectives:d??"Session started via wyrm_session_prime",summary:""}).id);const z={project_id:o.id,project_name:o.name,truth_count:i.getStats(o.id).current,quest_count:I.length,memory_count:n.getStats(o.id).total,session_id:B,sections:R,run_id:c,role:u,token_budget:j};_.prepare("INSERT OR IGNORE INTO run_briefs (run_id, role, body_json, token_budget) VALUES (?, ?, ?, ?)").run(c,u,JSON.stringify(z),j),S=w()}const g=JSON.parse(S.body_json),E=v(g,L,A);if(e.for_spawn===!0){const o=v(g,L,{...A,exemptStructured:!0}),m=Q(o.content[0]),$=Z("sha256").update(m,"utf8").digest("hex"),k={project_id:g.project_id,project_name:g.project_name,session_id:g.session_id??null,sections:g.sections,for_spawn:!0,run_id:c,role:u,brief:m,brief_hash:$,brief_chars:m.length,cache_key:{table:"run_briefs",run_id:c,role:u}};await F(_,g.project_id,o);const x={content:[{type:"text",text:m}],structuredContent:k};return a.set(O("wyrm_session_prime",JSON.stringify(e)),x,3e4),x}return await F(_,g.project_id,E),a.set(O("wyrm_session_prime",JSON.stringify(e)),E,3e4),E}const Ee=[{name:"wyrm_session_start",description:"Start or continue a session for a project",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project path"},objectives:{type:"string",description:"Session objectives"}},required:["projectPath"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project:{type:"string"},date:{type:"string"},objectives:{type:["string","null"]}},required:["session_id","project","date","objectives"]},annotations:T.wyrm_session_start,aliases:[],handler:(e,{store:t})=>{const{projectPath:s,objectives:n}=e;let i=t.getProject(s);i||(i=t.registerProject(X(s),s));let r=t.getTodaySession(i.id);r?n&&(r=t.updateSession(r.id,{objectives:r.objectives?`${r.objectives}
|
|
32
32
|
${n}`:n})):r=t.createSession(i.id,{objectives:n||""}),t.archiveOldSessions(i.id,10);const a={session_id:r.id,project:i.name,date:r.date,objectives:r.objectives||null};return v(a,(p,l)=>N(l.brand,`Session ${p.session_id} for ${p.project}`)+`
|
|
33
33
|
**Date:** ${p.date}
|
|
34
34
|
**Objectives:** ${p.objectives??"None set"}`)}},{name:"wyrm_session_update",description:"Update the current session with completed work, issues, or notes",inputSchema:{type:"object",properties:{projectPath:{type:"string"},completed:{type:"string",description:"What was completed"},issues:{type:"string"},commits:{type:"string",description:"Git commits made"},notes:{type:"string"}},required:["projectPath"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project:{type:"string"},updated:{type:"array",items:{type:"string",enum:["completed","issues","commits","notes"]}}},required:["session_id","project","updated"]},annotations:T.wyrm_session_update,aliases:[],handler:(e,{store:t,indexing:s})=>{const{projectPath:n,completed:i,issues:r,commits:a,notes:p}=e,l=t.getProject(n);if(!l)return{content:[{type:"text",text:"Project not found"}],isError:!0};let d=t.getTodaySession(l.id);d||(d=t.createSession(l.id,{}));const y={};i&&(y.completed=d.completed?`${d.completed}
|
|
35
35
|
${i}`:i),r&&(y.issues=d.issues?`${d.issues}
|
|
36
36
|
${r}`:r),a&&(y.commits=d.commits?`${d.commits}
|
|
37
37
|
${a}`:a),p&&(y.notes=d.notes?`${d.notes}
|
|
38
|
-
${p}`:p),d=t.updateSession(d.id,y);const c=[i,r,p].filter(Boolean).join(" \xB7 ");c&&t.updateContinuity(l.id,c,"session_update"),s()?.enqueue("session",d.id,l.id);const u={session_id:d.id,project:l.name,updated:Object.keys(y)};return v(u,(j,_)=>N(_.brand,`Session updated for ${j.project}`))}},{name:"wyrm_session_rehydrate",description:"Lossless session rehydration. Given a past session ID, returns a complete briefing markdown a fresh AI agent can ingest to inherit prior state: objectives, completed work, notes, summary, current ground truths, open quests, validated patterns (artifacts), unresolved failure patterns. Cross-session continuity no other AI memory tool offers.",inputSchema:{type:"object",properties:{session_id:{type:"number",description:"ID of the session to rehydrate"},include_artifacts:{type:"boolean",description:"Include memory artifacts (default true)"},include_failures:{type:"boolean",description:"Include unresolved failure patterns (default true)"},max_truth_chars:{type:"number",description:"Cap on chars used for ground truths (default 2000)"}},required:["session_id"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project_name:{type:"string"},project_path:{type:"string"},briefing_markdown:{type:"string"},context_chars:{type:"integer"},attached:{type:"object",properties:{quests:{type:"integer"},truths:{type:"integer"},artifacts:{type:"integer"},failures:{type:"integer"}},required:["quests","truths","artifacts","failures"]}},required:["session_id","project_name","project_path","briefing_markdown","context_chars","attached"]},annotations:T.wyrm_session_rehydrate,aliases:[],handler:(e,{rehydration:t})=>{const{session_id:s,include_artifacts:n,include_failures:i,max_truth_chars:r}=e,a=t.rehydrate(s,{include_artifacts:n,include_failures:i,max_truth_chars:r});return a?v({...a},p=>p.briefing_markdown,{exemptStructured:!0}):{content:[{type:"text",text:`Session #${s} not found.`}],isError:!0}}},{name:"wyrm_session_prime",description:"Use first at session start - load everything you need to know before starting work, and catch me up on where we left off last time: ground truths, memory brief, reasoning scaffold, and active quests in one call. First call every agent makes in a run.",inputSchema:{type:"object",properties:{project_id:{type:"number"},project_name:{type:"string"},task:{type:"string",description:"Drives scaffold + memory selection"},log_session:{type:"boolean",description:"Also log a session row"},run_id:{type:"string",description:"Fleet: brief cached per (run_id, role)"},role:{type:"string"},token_budget:{type:"number"},for_spawn:{type:"boolean"}}},outputSchema:{type:"object",properties:{project_id:{type:"integer"},project_name:{type:"string"},session_id:{type:["integer","null"]},sections:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["project_id","project_name","session_id","sections"]},annotations:T.wyrm_session_prime,aliases:[],handler:async(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a})=>{const{project_id:p,project_name:l,task:d,log_session:y}=e;if(e.run_id!==void 0&&e.run_id!==null)return
|
|
38
|
+
${p}`:p),d=t.updateSession(d.id,y);const c=[i,r,p].filter(Boolean).join(" \xB7 ");c&&t.updateContinuity(l.id,c,"session_update"),s()?.enqueue("session",d.id,l.id);const u={session_id:d.id,project:l.name,updated:Object.keys(y)};return v(u,(j,_)=>N(_.brand,`Session updated for ${j.project}`))}},{name:"wyrm_session_rehydrate",description:"Lossless session rehydration. Given a past session ID, returns a complete briefing markdown a fresh AI agent can ingest to inherit prior state: objectives, completed work, notes, summary, current ground truths, open quests, validated patterns (artifacts), unresolved failure patterns. Cross-session continuity no other AI memory tool offers.",inputSchema:{type:"object",properties:{session_id:{type:"number",description:"ID of the session to rehydrate"},include_artifacts:{type:"boolean",description:"Include memory artifacts (default true)"},include_failures:{type:"boolean",description:"Include unresolved failure patterns (default true)"},max_truth_chars:{type:"number",description:"Cap on chars used for ground truths (default 2000)"}},required:["session_id"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project_name:{type:"string"},project_path:{type:"string"},briefing_markdown:{type:"string"},context_chars:{type:"integer"},attached:{type:"object",properties:{quests:{type:"integer"},truths:{type:"integer"},artifacts:{type:"integer"},failures:{type:"integer"}},required:["quests","truths","artifacts","failures"]}},required:["session_id","project_name","project_path","briefing_markdown","context_chars","attached"]},annotations:T.wyrm_session_rehydrate,aliases:[],handler:(e,{rehydration:t})=>{const{session_id:s,include_artifacts:n,include_failures:i,max_truth_chars:r}=e,a=t.rehydrate(s,{include_artifacts:n,include_failures:i,max_truth_chars:r});return a?v({...a},p=>p.briefing_markdown,{exemptStructured:!0}):{content:[{type:"text",text:`Session #${s} not found.`}],isError:!0}}},{name:"wyrm_session_prime",description:"Use first at session start - load everything you need to know before starting work, and catch me up on where we left off last time: ground truths, memory brief, reasoning scaffold, and active quests in one call. First call every agent makes in a run.",inputSchema:{type:"object",properties:{project_id:{type:"number"},project_name:{type:"string"},task:{type:"string",description:"Drives scaffold + memory selection"},log_session:{type:"boolean",description:"Also log a session row"},run_id:{type:"string",description:"Fleet: brief cached per (run_id, role)"},role:{type:"string"},token_budget:{type:"number"},for_spawn:{type:"boolean"}}},outputSchema:{type:"object",properties:{project_id:{type:"integer"},project_name:{type:"string"},session_id:{type:["integer","null"]},sections:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["project_id","project_name","session_id","sections"]},annotations:T.wyrm_session_prime,aliases:[],handler:async(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a})=>{const{project_id:p,project_name:l,task:d,log_session:y}=e;if(e.run_id!==void 0&&e.run_id!==null)return ue(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a});const c=P(t,s(),p,l);if(!c)return q(t);const u=[],j=G(t.getContinuity(c.id));j&&u.push({kind:"continuity",text:j});const _=i.getCurrent(c.id);if(_.length>0&&u.push({kind:"truths",text:J(_,!0).slice(0,1200)}),d){const f=r.findBest(d,c.id,.3);f&&u.push({kind:"scaffold",text:r.formatForContext(f,1500)})}const w=n.buildContextBrief(c.id,d??"general",{maxItems:10,minConfidence:.3});w.text&&u.push({kind:"memory",text:w.text.slice(0,2e3)});const h=s().prepare(V).all(c.id);h.length>0&&u.push({kind:"quests",text:Y(h).slice(0,800)});try{const f=s().prepare("SELECT COUNT(*) AS n FROM memory_artifacts WHERE project_id = ? AND needs_review = 1").get(c.id).n;f>0&&u.push({kind:"review_queue",text:`### \u{1F4E5} Review Queue
|
|
39
39
|
${f} captured memor${f===1?"y awaits":"ies await"} review (wyrm_review) \u2014 queued memory is invisible to recall until approved.`})}catch{}try{const{resolveEmbeddingState:f}=await import("../providers/embedding-provider.js"),o=f();let m=-1;try{m=s().prepare("SELECT COUNT(*) AS n FROM vectors").get().n}catch{m=0}o.resolved==="none"?u.push({kind:"vector_health",text:`### [!] Semantic Recall is OFF
|
|
40
40
|
No embedding provider is active (${o.reason||"no provider"}) - recall is FTS5 keyword-only (~59.9% vs 72.2% hybrid recall@10). Fix: ${o.fix}`}):o.resolved==="local"?u.push({kind:"vector_health",text:`### [!] Semantic Recall is NON-SEMANTIC
|
|
41
41
|
Provider 'local' (hash-384) is a test-grade fallback, not a real embedder - recall quality is at the FTS floor. Fix: ${o.fix}`}):m===0&&u.push({kind:"vector_health",text:`### [!] Vector Index is EMPTY
|
|
42
42
|
Provider '${o.resolved}' (${o.model}) is configured but nothing is indexed - recall is running FTS5-only until the index is built. Fix: wyrm index rebuild`})}catch{}try{const{vaultAdvisory:f}=await import("../vault.js"),o=f();o.length>0&&u.push({kind:"vault",text:`### \u{1F510} Credential Vault
|
|
43
43
|
`+o.map(m=>`- ${m}`).join(`
|
|
44
|
-
`)})}catch{}let S=null;y&&(S=t.createSession(c.id,{objectives:d??"Session started via wyrm_session_prime",summary:""}).id);const g={project_id:c.id,project_name:c.name,truth_count:i.getStats(c.id).current,quest_count:h.length,memory_count:n.getStats(c.id).total,session_id:S,sections:u},E=v(g,L,A);return await F(s(),c.id,E),a.set(O("wyrm_session_prime",JSON.stringify(e)),E,3e4),E}}];export{
|
|
44
|
+
`)})}catch{}let S=null;y&&(S=t.createSession(c.id,{objectives:d??"Session started via wyrm_session_prime",summary:""}).id);const g={project_id:c.id,project_name:c.name,truth_count:i.getStats(c.id).current,quest_count:h.length,memory_count:n.getStats(c.id).total,session_id:S,sections:u},E=v(g,L,A);return await F(s(),c.id,E),a.set(O("wyrm_session_prime",JSON.stringify(e)),E,3e4),E}}];export{Ee as sessionToolSpecs};
|
package/dist/tool-manifest.json
CHANGED
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.5",
|
|
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
|
});
|