wyrm-mcp 8.3.0 → 8.3.1

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.
@@ -1,4 +1,4 @@
1
- import{homedir as l}from"os";import{join as a,dirname as c}from"path";import{readFileSync as m,existsSync as y}from"fs";import{resolveEmbeddingState as h}from"./providers/embedding-provider.js";import{fileURLToPath as d}from"url";const w=d(import.meta.url),n=c(w),u=`Wyrm is not "AI memory" in the file-write sense. It's a layered intelligence
1
+ import{homedir as l}from"os";import{join as a,dirname as c}from"path";import{readFileSync as m,existsSync as y}from"fs";import{resolveEmbeddingState as h}from"./providers/embedding-provider.js";import{fileURLToPath as d}from"url";const u=d(import.meta.url),n=c(u),w=`Wyrm is not "AI memory" in the file-write sense. It's a layered intelligence
2
2
  substrate that solves the four specific problems AI assistants share:
3
3
 
4
4
  1. Amnesia. Sessions inherit prior state via lossless rehydration.
@@ -11,5 +11,5 @@ substrate that solves the four specific problems AI assistants share:
11
11
  Every Wyrm session is local-first SQLite with FTS5 + hybrid semantic search.
12
12
  Encryption is operator-controlled. Federation is opt-in per row. The audit
13
13
  log is hash-chained and Ed25519-signable for SOC2/HIPAA scenarios. None of
14
- this requires you to leave your laptop.`,p=[{name:"Counter-pattern memory",what:"Records failed approaches with (scope, target, description) and blocks the same suggestion next time.",why:"Other memory tools learn only from success. Wyrm refuses to repeat mistakes \u2014 saves the most expensive class of tokens (the regenerated wrong answer).",tools:["wyrm_failure_record","wyrm_failure_check","wyrm_failure_list","wyrm_failure_resolve"]},{name:"Ground truths (versioned, cascade-aware)",what:"Validated project facts (architecture, constraints, decisions) \u2014 each truth carries a version, last-verified timestamp, and outbound decision links.",why:"Truths are injected first into every wyrm_context_build call so the AI never works from contradicted assumptions. When a truth changes, dependent decisions auto-invalidate.",tools:["wyrm_truth_set","wyrm_truth_get","wyrm_decided_because","wyrm_decision_upstream","wyrm_decision_downstream","wyrm_decision_invalidate"]},{name:"Reasoning scaffolds",what:'Saved step-by-step checklists for problem types ("debug a slow query", "ship a migration") that surface automatically when a task description matches.',why:"Replaces ad-hoc rediscovery of process knowledge with structured prompts. Cheaper than re-explaining the playbook every session.",tools:["wyrm_scaffold_save","wyrm_scaffold_get"]},{name:"Lossless session rehydration",what:"wyrm_session_rehydrate produces a complete briefing markdown a fresh AI ingests to inherit prior state \u2014 decisions, open quests, failures, recent commits, files touched.",why:"Cross-session continuity without re-exploration. The single biggest token-saver in the toolkit.",tools:["wyrm_session_rehydrate","wyrm_session_prime","wyrm_session_start","wyrm_session_update"]},{name:"Distillation queue",what:"Candidate memory artifacts extracted from sessions land in a review queue; the operator approves or rejects each.",why:"Bayesian confidence updates after each review \u2014 Wyrm gets sharper with use without auto-trusting noise.",tools:["wyrm_distill","wyrm_review","wyrm_remember","wyrm_recall","wyrm_feedback"]},{name:"Knowledge graph",what:"Named entities + typed directional relationships, neighborhood traversal, shortest-path queries.",why:'When the AI asks "how does X relate to Y?", Wyrm answers in O(log n) instead of grepping the corpus.',tools:["wyrm_entity_add","wyrm_entity_link","wyrm_entity_search","wyrm_entity_graph","wyrm_entity_path","wyrm_entity_merge"]},{name:"Cross-repo symbol index",what:"Workspace-wide function/class/type index across TS/JS/Python/Rust/Go/PHP/Ruby with caller resolution.",why:`Symbol lookups don't require re-grepping every project. "Who calls handleAuth?" answers immediately.`,tools:["wyrm_symbol_index","wyrm_symbol_search","wyrm_symbol_callers","wyrm_symbol_stats"]},{name:"OODA agent loop (autonomous goals)",what:"wyrm_goal_set persists an objective with success criteria. The wyrm-loop daemon runs Observe\u2192Orient\u2192Decide\u2192Act every interval, dispatching whitelisted tools. Replayable, capped, auditable.",why:"Lets you hand the AI a multi-step objective and walk away. No data-exfil tools accessible from inside the loop.",tools:["wyrm_goal_set","wyrm_goal_list","wyrm_goal_pause","wyrm_goal_resume","wyrm_goal_complete","wyrm_goal_abandon","wyrm_goal_iterations","wyrm_act","wyrm_agent_init","wyrm_agent_status","wyrm_agent_stop","wyrm_agent_restart"]},{name:"Outbound MCP client",what:"Wyrm calls other MCP servers (GitHub, Slack, Linear, \u2026) over stdio. The agent loop dispatches them like internal tools.",why:"Composability: Wyrm becomes the orchestration layer over your entire MCP stack instead of a sibling.",tools:["wyrm_mcp_register","wyrm_mcp_list","wyrm_mcp_disable","wyrm_mcp_tools","wyrm_call_external"]},{name:"Hybrid semantic search",what:"wyrm_search fuses FTS5 keyword candidates with vector candidates (Ollama nomic-embed-text by default; OpenAI optional) via Reciprocal Rank Fusion.",why:`Catches both "exact term" and "semantically similar" hits in one call. Silently falls back to FTS5-only if Ollama isn't running.`,tools:["wyrm_search","wyrm_vector_setup"]},{name:"Multi-agent presence + work-stealing",what:"Claude, Copilot, Cursor, and Codex announce themselves via TTL heartbeats. Quests can be claimed exclusively.",why:"No two agents step on the same quest. The work-stealing semantics scale to teams.",tools:["wyrm_presence_announce","wyrm_presence_list","wyrm_presence_release","wyrm_quest_claim","wyrm_quest_release"]},{name:"Federated team sync",what:"Per-row is_shared flag, /sync/push endpoint, conflict-as-quest semantics. PII never leaves the box unless explicitly shared.",why:"Team memory without a SaaS. No silent overwrites \u2014 every conflict becomes a quest the operator resolves.",tools:["wyrm_share","wyrm_unshare","wyrm_sync","wyrm_sync_export","wyrm_sync_import","wyrm_sync_conflicts","wyrm_sync_resolve"]},{name:"Compliance audit trail",what:"Hash-chained audit log with optional Ed25519 signatures. wyrm_audit_export produces tamper-evident bundles.",why:"SOC2/HIPAA reviewers get a real artifact. The hash chain detects any tampering after the fact.",tools:["wyrm_audit_log","wyrm_audit_export","wyrm_audit_verify"]},{name:"Hour ledger + invoice generation",what:"Derives billable hours from session content and emits markdown invoices.",why:"For solo operators and small teams: Wyrm is also the time tracker. No second tool to keep in sync.",tools:["wyrm_hours_report","wyrm_invoice_generate","wyrm_cost_report"]},{name:"Self-improvement analytics",what:"Per-tool error rate, p50/p95 latency, cost-per-tool. Wyrm watches Wyrm.",why:"Lets you spot which tools are hot, which are slow, which fail \u2014 input for both ops tuning and product decisions.",tools:["wyrm_analytics_dashboard","wyrm_tool_analytics","wyrm_usage","wyrm_stats"]}];function _(){try{const t=[a(n,"..","package.json"),a(n,"..","..","package.json")];for(const e of t)if(y(e))return JSON.parse(m(e,"utf-8")).version??"unknown"}catch{}return"unknown"}function x(t,e,s){const o=process.env.WYRM_DB_PATH??a(l(),".wyrm","wyrm.db"),i=s.isAgentRunning(),r=h();return{product:"Wyrm",version:_(),pitch:u,install:{dbPath:o,nodeVersion:process.version,platform:`${process.platform}-${process.arch}`},runtime:{vectorProvider:s.detectVectorProvider?s.detectVectorProvider():r.resolved,vectorReason:r.reason,vectorFix:r.fix,encryption:s.hasEncryption(),federation:s.isFederationEnabled(),agentLoop:i===null?"unknown":i?"running":"stopped"},features:p,toolCount:e}}function S(t){const e=[];e.push(`# \u{F115D} ${t.product} ${t.version} \u2014 Capability Briefing`),e.push(""),e.push(t.pitch),e.push("");const s=t.runtime.vectorProvider==="none"?"vectors OFF (FTS-only recall)":`${t.runtime.vectorProvider} embeddings`;e.push(`**Runtime:** ${t.toolCount} MCP tools \xB7 ${s} \xB7 ${t.runtime.encryption?"encryption ON":"encryption off"} \xB7 agent ${t.runtime.agentLoop}`),t.runtime.vectorReason&&(e.push(""),e.push(`> WARNING: semantic recall degraded (${t.runtime.vectorReason}) - recall is FTS5 keyword-only. Fix: ${t.runtime.vectorFix}`)),e.push(`**Install:** ${t.install.platform} \xB7 Node ${t.install.nodeVersion} \xB7 DB ${t.install.dbPath}`),e.push(""),e.push('## What Wyrm gives you beyond "AI memory"'),e.push("");for(const o of t.features)e.push(`### ${o.name}`),e.push(`- **What:** ${o.what}`),e.push(`- **Why:** ${o.why}`),e.push(`- **Tools:** \`${o.tools.join("`, `")}\``),e.push("");return e.join(`
14
+ this requires you to leave your laptop.`,p=[{name:"Counter-pattern memory",what:"Records failed approaches with (scope, target, description) and blocks the same suggestion next time.",why:"Other memory tools learn only from success. Wyrm refuses to repeat mistakes \u2014 saves the most expensive class of tokens (the regenerated wrong answer).",tools:["wyrm_failure_record","wyrm_failure_check","wyrm_failure_list","wyrm_failure_resolve"]},{name:"Ground truths (versioned, cascade-aware)",what:"Validated project facts (architecture, constraints, decisions) \u2014 each truth carries a version, last-verified timestamp, and outbound decision links.",why:"Truths are injected first into every wyrm_context_build call so the AI never works from contradicted assumptions. When a truth changes, dependent decisions auto-invalidate.",tools:["wyrm_truth_set","wyrm_truth_get","wyrm_decided_because","wyrm_decision_upstream","wyrm_decision_downstream","wyrm_decision_invalidate"]},{name:"Reasoning scaffolds",what:'Saved step-by-step checklists for problem types ("debug a slow query", "ship a migration") that surface automatically when a task description matches.',why:"Replaces ad-hoc rediscovery of process knowledge with structured prompts. Cheaper than re-explaining the playbook every session.",tools:["wyrm_scaffold_save","wyrm_scaffold_get"]},{name:"Lossless session rehydration",what:"wyrm_session_rehydrate produces a complete briefing markdown a fresh AI ingests to inherit prior state \u2014 decisions, open quests, failures, recent commits, files touched.",why:"Cross-session continuity without re-exploration. The single biggest token-saver in the toolkit.",tools:["wyrm_session_rehydrate","wyrm_session_prime","wyrm_session_start","wyrm_session_update"]},{name:"Distillation queue",what:"Candidate memory artifacts extracted from sessions land in a review queue; the operator approves or rejects each.",why:"Bayesian confidence updates after each review \u2014 Wyrm gets sharper with use without auto-trusting noise.",tools:["wyrm_distill","wyrm_review","wyrm_remember","wyrm_recall","wyrm_feedback"]},{name:"Knowledge graph",what:"Named entities + typed directional relationships, neighborhood traversal, shortest-path queries.",why:'When the AI asks "how does X relate to Y?", Wyrm answers in O(log n) instead of grepping the corpus.',tools:["wyrm_entity_add","wyrm_entity_link","wyrm_entity_search","wyrm_entity_graph","wyrm_entity_path","wyrm_entity_merge"]},{name:"Cross-repo symbol index",what:"Workspace-wide function/class/type index across TS/JS/Python/Rust/Go/PHP/Ruby with caller resolution.",why:`Symbol lookups don't require re-grepping every project. "Who calls handleAuth?" answers immediately.`,tools:["wyrm_symbol_index","wyrm_symbol_search","wyrm_symbol_callers","wyrm_symbol_stats"]},{name:"OODA agent loop (autonomous goals)",what:"wyrm_goal_set persists an objective with success criteria. The wyrm-loop daemon runs Observe\u2192Orient\u2192Decide\u2192Act every interval, dispatching whitelisted tools. Replayable, capped, auditable.",why:"Lets you hand the AI a multi-step objective and walk away. No data-exfil tools accessible from inside the loop.",tools:["wyrm_goal_set","wyrm_goal_list","wyrm_goal_pause","wyrm_goal_resume","wyrm_goal_complete","wyrm_goal_abandon","wyrm_goal_iterations","wyrm_act","wyrm_agent_init","wyrm_agent_status","wyrm_agent_stop","wyrm_agent_restart"]},{name:"Outbound MCP client",what:"Wyrm calls other MCP servers (GitHub, Slack, Linear, \u2026) over stdio. The agent loop dispatches them like internal tools.",why:"Composability: Wyrm becomes the orchestration layer over your entire MCP stack instead of a sibling.",tools:["wyrm_mcp_register","wyrm_mcp_list","wyrm_mcp_disable","wyrm_mcp_tools","wyrm_call_external"]},{name:"Hybrid semantic search",what:"wyrm_search fuses FTS5 keyword candidates with vector candidates (Ollama nomic-embed-text by default; OpenAI optional) via Reciprocal Rank Fusion.",why:`Catches both "exact term" and "semantically similar" hits in one call. Silently falls back to FTS5-only if Ollama isn't running.`,tools:["wyrm_search","wyrm_vector_setup"]},{name:"Multi-agent presence + work-stealing",what:"Claude, Copilot, Cursor, and Codex announce themselves via TTL heartbeats. Quests can be claimed exclusively.",why:"No two agents step on the same quest. The work-stealing semantics scale to teams.",tools:["wyrm_presence_announce","wyrm_presence_list","wyrm_presence_release","wyrm_quest_claim","wyrm_quest_release"]},{name:"Federated team sync",what:"Per-row is_shared flag, /sync/push endpoint, conflict-as-quest semantics. PII never leaves the box unless explicitly shared.",why:"Team memory without a SaaS. No silent overwrites \u2014 every conflict becomes a quest the operator resolves.",tools:["wyrm_share","wyrm_unshare","wyrm_sync","wyrm_sync_export","wyrm_sync_import","wyrm_sync_conflicts","wyrm_sync_resolve"]},{name:"Compliance audit trail",what:"Hash-chained audit log with optional Ed25519 signatures. wyrm_audit_export produces tamper-evident bundles.",why:"SOC2/HIPAA reviewers get a real artifact. The hash chain detects any tampering after the fact.",tools:["wyrm_audit_log","wyrm_audit_export","wyrm_audit_verify"]},{name:"Hour ledger + invoice generation",what:"Derives billable hours from session content and emits markdown invoices.",why:"For solo operators and small teams: Wyrm is also the time tracker. No second tool to keep in sync.",tools:["wyrm_hours_report","wyrm_invoice_generate","wyrm_cost_report"]},{name:"Self-improvement analytics",what:"Per-tool error rate, p50/p95 latency, cost-per-tool. Wyrm watches Wyrm.",why:"Lets you spot which tools are hot, which are slow, which fail \u2014 input for both ops tuning and product decisions.",tools:["wyrm_analytics_dashboard","wyrm_tool_analytics","wyrm_usage","wyrm_stats"]}];function _(){try{const t=[a(n,"..","package.json"),a(n,"..","..","package.json")];for(const e of t)if(y(e))return JSON.parse(m(e,"utf-8")).version??"unknown"}catch{}return"unknown"}function x(t,e,s){const o=process.env.WYRM_DB_PATH??a(l(),".wyrm","wyrm.db"),i=s.isAgentRunning(),r=h();return{product:"Wyrm",version:_(),pitch:w,install:{dbPath:o,nodeVersion:process.version,platform:`${process.platform}-${process.arch}`},runtime:{vectorProvider:s.detectVectorProvider?s.detectVectorProvider():r.resolved,vectorReason:r.reason,vectorFix:r.fix,encryption:s.hasEncryption(),federation:s.isFederationEnabled(),agentLoop:i===null?"unknown":i?"running":"stopped"},features:p,toolCount:e}}function S(t){const e=[];e.push(`# ${t.product} ${t.version} \u2014 Capability Briefing`),e.push(""),e.push(t.pitch),e.push("");const s=t.runtime.vectorProvider==="none"?"vectors OFF (FTS-only recall)":`${t.runtime.vectorProvider} embeddings`;e.push(`**Runtime:** ${t.toolCount} MCP tools \xB7 ${s} \xB7 ${t.runtime.encryption?"encryption ON":"encryption off"} \xB7 agent ${t.runtime.agentLoop}`),t.runtime.vectorReason&&(e.push(""),t.runtime.vectorProvider==="none"?e.push(`> WARNING: semantic recall degraded (${t.runtime.vectorReason}) - recall is FTS5 keyword-only. Fix: ${t.runtime.vectorFix}`):e.push(`> NOTE: embedding provider is advisory (${t.runtime.vectorReason}) - semantic recall is running. Fix: ${t.runtime.vectorFix}`)),e.push(`**Install:** ${t.install.platform} \xB7 Node ${t.install.nodeVersion} \xB7 DB ${t.install.dbPath}`),e.push(""),e.push('## What Wyrm gives you beyond "AI memory"'),e.push("");for(const o of t.features)e.push(`### ${o.name}`),e.push(`- **What:** ${o.what}`),e.push(`- **Why:** ${o.why}`),e.push(`- **Tools:** \`${o.tools.join("`, `")}\``),e.push("");return e.join(`
15
15
  `)}export{x as getCapabilities,S as renderCapabilityBriefing};
@@ -1,13 +1,13 @@
1
- import{TOOL_ANNOTATIONS as L}from"../tool-annotations.js";import{renderResult as T,withGlyph as k}from"../render.js";import{memoryUri as A,resourceLink as H}from"./resources.js";import{buildDeterminismReceipt as N,attestation as U}from"../receipt.js";import{rerankConfigured as W,rerankEgressHost as F}from"../rerank.js";const V=[{name:"wyrm_recall",description:"Use before starting any non-trivial task - what do we know about this? Pull up relevant lessons learned, proven patterns, anti-patterns, and reasoning traces from past work. Hybrid lexical+semantic retrieval, fully local, no LLM. Mark what you recalled as useful (or not) with wyrm_feedback.",inputSchema:{type:"object",properties:{projectPath:{type:"string"},query:{type:"string",description:"The task or problem"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]},limit:{type:"number",description:"Default 10"},minConfidence:{type:"number",description:"0-1 (default 0)"},detail:{type:"string",enum:["full","link"],description:"Default full"}},required:["projectPath","query"]},outputSchema:{type:"object",properties:{query:{type:"string"},count:{type:"integer"},results:{type:"array",items:{type:"object",properties:{id:{type:"integer"},kind:{type:"string"},relevance:{type:"number"},problem:{type:"string"},validated_fix:{type:["string","null"]},confidence:{type:"number"},reuse_count:{type:"integer"}},required:["id","kind","relevance","problem","confidence","reuse_count"]}}},required:["query","count","results"]},annotations:L.wyrm_recall,aliases:[],handler:async($,{store:E,raw:S,memory:v,vectors:h,clientSupportsResources:P})=>{const{projectPath:w,query:d,kind:R,limit:j,minConfidence:q,detail:g}=$,u=E.getProject(w);if(!u)return{content:[{type:"text",text:`Project not found: ${w}`}],isError:!0};const p=!!h(),O=p&&W()&&/^(1|true|yes|on)$/i.test(process.env.WYRM_RERANK??"");let r;const b={kind:R,limit:j??10,minConfidence:q,rerank:O,onStats:t=>{r=t}},a=p?await v.recallHybrid(u.id,d,b):v.recall(u.id,d,b),c=r!==void 0,f=r?.rerankMs!=null,s=h(),M=typeof s?.getProvider=="function"?s.getProvider():void 0;let l;if(c&&typeof s?.indexCoverage=="function")try{l=s.indexCoverage()}catch{}let m=null;if(!c)if(p){let t=-1;if(typeof s?.getStats=="function")try{t=s.getStats().total}catch{}m=t===0?"empty_index":"embed_error"}else{const{resolveEmbeddingState:t}=await import("../providers/embedding-provider.js");m=t().reason||"vector_store_unavailable"}const C=N({vectorsAvailable:c,rerankActive:f,embedModel:c?M?.model??(process.env.WYRM_EMBED_MODEL||"nomic-embed-text"):null,rerankModel:f&&process.env.WYRM_RERANK_MODEL||null,embedEgressHost:c?M?.remoteHost??null:null,rerankEgressHost:f?F():null,stageMs:r?{fts5:r.ftsMs,vector:r.vectorMs,fusion:r.fusionMs,...r.rerankMs!=null?{rerank:r.rerankMs}:{}}:void 0,indexStale:l?.stale?{activeModel:l.activeModel,indexedUnder:Object.keys(l.otherModels)}:void 0,ftsOnlyReason:m});if(a.length>0){const t=a.map(o=>o.artifact.id),e=t.map(()=>"?").join(",");S().prepare(`UPDATE memory_artifacts SET last_accessed_at = datetime('now'), access_count = access_count + 1 WHERE id IN (${e})`).run(...t)}const x=t=>Math.round(t*1e4)/1e4,y={query:d,count:a.length,results:a.map(t=>{const e=t.artifact,o={id:e.id,kind:e.kind,relevance:x(t.relevance_score),problem:e.problem,outcome:e.outcome,confidence:e.confidence==null?e.confidence:x(e.confidence),reuse_count:e.reuse_count,reuse_success_count:e.reuse_success_count};return e.constraints!=null&&(o.constraints=e.constraints),e.validated_fix!=null&&(o.validated_fix=e.validated_fix),e.why_it_worked!=null&&(o.why_it_worked=e.why_it_worked),o}),receipt:C},_=T(y,(t,e)=>{const o=`
1
+ import{TOOL_ANNOTATIONS as L}from"../tool-annotations.js";import{renderResult as T,withGlyph as v}from"../render.js";import{memoryUri as A,resourceLink as N}from"./resources.js";import{buildDeterminismReceipt as H,attestation as U}from"../receipt.js";import{rerankConfigured as W,rerankEgressHost as F}from"../rerank.js";const V=[{name:"wyrm_recall",description:"Use before starting any non-trivial task - what do we know about this? Pull up relevant lessons learned, proven patterns, anti-patterns, and reasoning traces from past work. Hybrid lexical+semantic retrieval, fully local, no LLM. Mark what you recalled as useful (or not) with wyrm_feedback.",inputSchema:{type:"object",properties:{projectPath:{type:"string"},query:{type:"string",description:"The task or problem"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]},limit:{type:"number",description:"Default 10"},minConfidence:{type:"number",description:"0-1 (default 0)"},detail:{type:"string",enum:["full","link"],description:"Default full"}},required:["projectPath","query"]},outputSchema:{type:"object",properties:{query:{type:"string"},count:{type:"integer"},results:{type:"array",items:{type:"object",properties:{id:{type:"integer"},kind:{type:"string"},relevance:{type:"number"},problem:{type:"string"},validated_fix:{type:["string","null"]},confidence:{type:"number"},reuse_count:{type:"integer"}},required:["id","kind","relevance","problem","confidence","reuse_count"]}}},required:["query","count","results"]},annotations:L.wyrm_recall,aliases:[],handler:async($,{store:E,raw:S,memory:h,vectors:w,clientSupportsResources:P})=>{const{projectPath:g,query:u,kind:R,limit:j,minConfidence:q,detail:b}=$,p=E.getProject(g);if(!p)return{content:[{type:"text",text:`Project not found: ${g}`}],isError:!0};const f=!!w(),O=f&&W()&&/^(1|true|yes|on)$/i.test(process.env.WYRM_RERANK??"");let r;const x={kind:R,limit:j??10,minConfidence:q,rerank:O,onStats:t=>{r=t}},a=f?await h.recallHybrid(p.id,u,x):h.recall(p.id,u,x),l=r!==void 0,m=r?.rerankMs!=null,s=w(),y=typeof s?.getProvider=="function"?s.getProvider():void 0;let c;if(typeof s?.indexCoverage=="function")try{c=s.indexCoverage()}catch{}let d=null;if(!l){const t=y?.name;if(!f||t==="none"){const{resolveEmbeddingState:e}=await import("../providers/embedding-provider.js");d=e().reason||"vector_store_unavailable"}else if(c?.stale)d="stale_index";else{let e=-1;if(typeof s?.getStats=="function")try{e=s.getStats().total}catch{}d=e===0?"empty_index":"embed_error"}}const C=H({vectorsAvailable:l,rerankActive:m,embedModel:l?y?.model??(process.env.WYRM_EMBED_MODEL||"nomic-embed-text"):null,rerankModel:m&&process.env.WYRM_RERANK_MODEL||null,embedEgressHost:l?y?.remoteHost??null:null,rerankEgressHost:m?F():null,stageMs:r?{fts5:r.ftsMs,vector:r.vectorMs,fusion:r.fusionMs,...r.rerankMs!=null?{rerank:r.rerankMs}:{}}:void 0,indexStale:c?.stale?{activeModel:c.activeModel,indexedUnder:Object.keys(c.otherModels)}:void 0,ftsOnlyReason:d});if(a.length>0){const t=a.map(o=>o.artifact.id),e=t.map(()=>"?").join(",");S().prepare(`UPDATE memory_artifacts SET last_accessed_at = datetime('now'), access_count = access_count + 1 WHERE id IN (${e})`).run(...t)}const M=t=>Math.round(t*1e4)/1e4,_={query:u,count:a.length,results:a.map(t=>{const e=t.artifact,o={id:e.id,kind:e.kind,relevance:M(t.relevance_score),problem:e.problem,outcome:e.outcome,confidence:e.confidence==null?e.confidence:M(e.confidence),reuse_count:e.reuse_count,reuse_success_count:e.reuse_success_count};return e.constraints!=null&&(o.constraints=e.constraints),e.validated_fix!=null&&(o.validated_fix=e.validated_fix),e.why_it_worked!=null&&(o.why_it_worked=e.why_it_worked),o}),receipt:C},k=T(_,(t,e)=>{const o=`
2
2
 
3
- _${U(t.receipt)}_`;if(t.count===0)return k(e.brand,`No relevant memory found for: "${t.query}"`)+"\n\nUse `wyrm_remember` to store knowledge as you work."+o;let i=k(e.brand,`**Memory Recall** -- "${t.query}" (${t.count} found)`)+`
3
+ _${U(t.receipt)}_`;if(t.count===0)return v(e.brand,`No relevant memory found for: "${t.query}"`)+"\n\nUse `wyrm_remember` to store knowledge as you work."+o;let i=v(e.brand,`**Memory Recall** -- "${t.query}" (${t.count} found)`)+`
4
4
 
5
5
  `;for(const n of t.results){i+=`### ${n.kind} (ID: ${n.id}) -- relevance ${(n.relevance*100).toFixed(0)}%
6
6
  `,i+=`**Problem:** ${n.problem}
7
7
  `,n.constraints&&(i+=`**Constraints:** ${n.constraints}
8
8
  `),n.validated_fix&&(i+=`**Solution:** ${n.validated_fix}
9
9
  `),n.why_it_worked&&(i+=`**Why:** ${n.why_it_worked}
10
- `),n.outcome==="negative"&&(i+=k(e.warn,"_This approach failed -- see solution for what to do instead_")+`
10
+ `),n.outcome==="negative"&&(i+=v(e.warn,"_This approach failed -- see solution for what to do instead_")+`
11
11
  `);const D=n.confidence==null?"unknown":`${(n.confidence*100).toFixed(0)}%`;i+=`_Confidence: ${D} | Used ${n.reuse_count}x | Success rate: ${n.reuse_count>0?(n.reuse_success_count/n.reuse_count*100).toFixed(0)+"%":"not yet used"}_
12
12
 
13
- `}return i+="_Use `wyrm_feedback` to record whether these were helpful._",i+o},{exemptStructured:!0});if((g==="link"||g!=="full"&&!!P?.())&&y.count>0){const t=y.results.map(e=>H(A(e.id),`memory #${e.id} (${e.kind})`,{description:e.problem.slice(0,120),mimeType:"application/json"}));return{..._,content:[..._.content,...t]}}return _}}];export{V as recallToolSpecs};
13
+ `}return i+="_Use `wyrm_feedback` to record whether these were helpful._",i+o},{exemptStructured:!0});if((b==="link"||b!=="full"&&!!P?.())&&_.count>0){const t=_.results.map(e=>N(A(e.id),`memory #${e.id} (${e.kind})`,{description:e.problem.slice(0,120),mimeType:"application/json"}));return{...k,content:[...k.content,...t]}}return k}}];export{V as recallToolSpecs};
@@ -1,4 +1,4 @@
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 x,withGlyph as O,cacheKeyFor as N}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";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
4
  `+e.thread.slice(-1800)}const re={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)_
@@ -20,7 +20,7 @@ import{basename as X}from"path";import{createHash as Z}from"crypto";import{block
20
20
  `).get()}function P(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
- Session logged: #${e.session_id}`:"";return O(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(`
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(`
24
24
 
25
25
  ---
26
26
 
@@ -28,16 +28,17 @@ Session logged: #${e.session_id}`:"";return O(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 ce={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"}},ae=48,de=1200;async function pe(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})??de,_=s(),v=()=>_.prepare("SELECT body_json FROM run_briefs WHERE run_id = ? AND role = ?").get(c,u),h=p||l?q(t,_,p,l):void 0;if((p||l)&&!h)return P(t);let S=v();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??q(t,_,p,l);if(!o)return P(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 w=d??(u||void 0);if(w){const b=r.findBest(w,o.id,.3);b&&m.push({kind:"scaffold",text:r.formatForContext(b,1500)})}const M=n.buildContextBrief(o.id,w??"general",{maxItems:10,minConfidence:.3});M.text&&m.push({kind:"memory",text:M.text.slice(0,2e3)});const R=_.prepare(V).all(o.id);R.length>0&&m.push({kind:"quests",text:Y(R).slice(0,800)});const D=oe(),I=[];let C=ae;for(const b of m){const W=D.count(b.text)+2;if(C+W<=j)I.push(b),C+=W;else{const H=ce[b.kind]??{path:b.kind,tool:"wyrm_search"},U=`[beyond token_budget] wyrm://project/${o.id}/${H.path} -- fetch via ${H.tool}`;I.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:R.length,memory_count:n.getStats(o.id).total,session_id:B,sections:I,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=v()}const g=JSON.parse(S.body_json),E=x(g,L,A);if(e.for_spawn===!0){const o=x(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 w={content:[{type:"text",text:m}],structuredContent:k};return a.set(N("wyrm_session_prime",JSON.stringify(e)),w,3e4),w}return await F(_,g.project_id,E),a.set(N("wyrm_session_prime",JSON.stringify(e)),E,3e4),E}const Se=[{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
- ${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 x(a,(p,l)=>O(l.brand,`Session ${p.session_id} for ${p.project}`)+`
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 ce={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"}},ae=48,de=1200;async function pe(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})??de,_=s(),w=()=>_.prepare("SELECT body_json FROM run_briefs WHERE run_id = ? AND role = ?").get(c,u),h=p||l?q(t,_,p,l):void 0;if((p||l)&&!h)return P(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??q(t,_,p,l);if(!o)return P(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 R=_.prepare(V).all(o.id);R.length>0&&m.push({kind:"quests",text:Y(R).slice(0,800)});const D=oe(),I=[];let C=ae;for(const b of m){const W=D.count(b.text)+2;if(C+W<=j)I.push(b),C+=W;else{const H=ce[b.kind]??{path:b.kind,tool:"wyrm_search"},U=`[beyond token_budget] wyrm://project/${o.id}/${H.path} -- fetch via ${H.tool}`;I.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:R.length,memory_count:n.getStats(o.id).total,session_id:B,sections:I,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 Se=[{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
+ ${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 x(u,(j,_)=>O(_.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?x({...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 pe(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a});const c=q(t,s(),p,l);if(!c)return P(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 v=n.buildContextBrief(c.id,d??"general",{maxItems:10,minConfidence:.3});v.text&&u.push({kind:"memory",text:v.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
- ${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"||o.resolved==="local"?u.push({kind:"vector_health",text:`### [!] Semantic Recall is OFF
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}`}):m===0&&u.push({kind:"vector_health",text:`### [!] Vector Index is EMPTY
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 pe(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a});const c=q(t,s(),p,l);if(!c)return P(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
+ ${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
+ 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
+ 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
41
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
42
43
  `+o.map(m=>`- ${m}`).join(`
43
- `)})}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=x(g,L,A);return await F(s(),c.id,E),a.set(N("wyrm_session_prime",JSON.stringify(e)),E,3e4),E}}];export{Se as sessionToolSpecs};
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{Se as sessionToolSpecs};
@@ -1 +1 @@
1
- import{isLoopbackHost as _}from"../receipt.js";function m(o){try{const t=new URL(o).hostname.toLowerCase();return _(t)?null:t}catch{return null}}class b{name="local-hash";model="hash-384";dimensions=384;async embed(t){const e=new Float32Array(this.dimensions),s=t.toLowerCase().split(/\s+/);for(let r=0;r<s.length;r++){const i=s[r];for(let a=0;a<i.length;a++){const d=i.charCodeAt(a)*(r+1)*(a+1)%this.dimensions;e[d]+=1/s.length}}const n=Math.sqrt(e.reduce((r,i)=>r+i*i,0));if(n>0)for(let r=0;r<e.length;r++)e[r]/=n;return e}async isReady(){return!0}}class v{name="openai";model;dimensions;remoteHost="api.openai.com";apiKey;timeoutMs;constructor(t,e){this.apiKey=t,this.model=e||"text-embedding-3-small",this.dimensions=this.model.includes("large")?3072:1536;const s=Number(process.env.WYRM_OPENAI_TIMEOUT);this.timeoutMs=Number.isFinite(s)&&s>0?s:8e3}async embed(t){const e=await fetch("https://api.openai.com/v1/embeddings",{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify({input:t,model:this.model}),signal:AbortSignal.timeout(this.timeoutMs)});if(!e.ok){const n=await e.text();throw new Error(`OpenAI embedding API error ${e.status}: ${n}`)}const s=await e.json();return new Float32Array(s.data[0].embedding)}async isReady(){return!!this.apiKey}}class I{name="ollama";model;dimensions;remoteHost;baseUrl;constructor(t,e){this.baseUrl=t||"http://localhost:11434",this.model=e||"nomic-embed-text",this.dimensions=this.model.includes("mxbai")?1024:768,this.remoteHost=m(this.baseUrl)}async embed(t){const e=await fetch(`${this.baseUrl}/api/embeddings`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:this.model,prompt:t})});if(!e.ok){const n=await e.text();throw new Error(`Ollama embedding API error ${e.status}: ${n}`)}const s=await e.json();return new Float32Array(s.embedding)}async isReady(){try{return(await fetch(`${this.baseUrl}/api/tags`,{signal:AbortSignal.timeout(2e3)})).ok}catch{return!1}}}class p{name="nim";model;remoteHost;dims;baseUrl;apiKey;timeoutMs;static KNOWN_DIMS={"nvidia/llama-nemotron-embed-1b-v2":2048,"nvidia/llama-3.2-nv-embedqa-1b-v2":2048,"nvidia/nv-embedqa-e5-v5":1024,"nvidia/nv-embedqa-mistral-7b-v2":4096,"nvidia/nv-embed-v1":4096};get dimensions(){return this.dims}constructor(t,e,s){this.apiKey=t,this.model=e||process.env.WYRM_NIM_EMBED_MODEL||"nvidia/llama-nemotron-embed-1b-v2",this.baseUrl=(s||process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com").replace(/\/+$/,"");const n=Number(process.env.WYRM_NIM_EMBED_DIM);this.dims=Number.isFinite(n)&&n>0?n:p.KNOWN_DIMS[this.model]??2048;const r=Number(process.env.WYRM_NIM_TIMEOUT);this.timeoutMs=Number.isFinite(r)&&r>0?r:8e3,this.remoteHost=m(this.baseUrl)}async embed(t,e="passage"){let s;for(let i=0;;i++){let a=null;try{const d={"Content-Type":"application/json"};if(this.apiKey&&(d.Authorization=`Bearer ${this.apiKey}`),s=await fetch(`${this.baseUrl}/v1/embeddings`,{method:"POST",headers:d,body:JSON.stringify({input:[t],model:this.model,input_type:e,encoding_format:"float",truncate:"END"}),signal:AbortSignal.timeout(this.timeoutMs)}),s.ok||s.status!==429&&s.status<500)break;const c=Number(s.headers?.get?.("retry-after"));a=Number.isFinite(c)&&c>0?Math.min(c*1e3,15e3):500*2**i}catch(d){if(i>=2)throw d;a=500*2**i}if(i>=2)break;await new Promise(d=>setTimeout(d,a??500))}if(!s.ok){const i=await s.text();throw new Error(`NIM embedding API error ${s.status}: ${i.slice(0,300)}`)}const r=(await s.json()).data?.[0]?.embedding;if(!Array.isArray(r)||r.length===0)throw new Error("NIM embedding API returned no embedding");return r.length!==this.dims&&(this.dims=r.length),new Float32Array(r)}async isReady(){return this.remoteHost?!!this.apiKey:!0}}class u{name="none";model="none";dimensions=0;async embed(t){return new Float32Array(0)}async isReady(){return!0}}const l="set WYRM_VECTOR_PROVIDER=nim and NVIDIA_API_KEY (or WYRM_NIM_API_KEY) in the server env, then: wyrm index rebuild";function M(o){const t=o||process.env.WYRM_VECTOR_PROVIDER||"auto";switch(t){case"nim":{const e=process.env.NVIDIA_API_KEY||process.env.WYRM_NIM_API_KEY||"",s=process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com",n=m(s);if(!e&&n)return{requested:t,resolved:"none",model:null,reason:"nim_missing_key",fix:l,egressHost:null};const r=process.env.WYRM_NIM_EMBED_MODEL||"nvidia/llama-nemotron-embed-1b-v2";return{requested:t,resolved:"nim",model:r,reason:"",fix:"",egressHost:n}}case"openai":return process.env.OPENAI_API_KEY?{requested:t,resolved:"openai",model:"text-embedding-3-small",reason:"",fix:"",egressHost:"api.openai.com"}:{requested:t,resolved:"none",model:null,reason:"openai_missing_key",fix:"set OPENAI_API_KEY, or use the supported provider: "+l,egressHost:null};case"ollama":{const e=process.env.OLLAMA_URL||"http://localhost:11434";return{requested:t,resolved:"ollama",model:process.env.WYRM_EMBED_MODEL||"nomic-embed-text",reason:"ollama_deprecated",fix:"Ollama support is deprecated and leaves in the next major \u2014 "+l,egressHost:m(e)}}case"none":return{requested:t,resolved:"none",model:null,reason:"disabled",fix:"vectors were explicitly disabled; to enable: "+l,egressHost:null};case"local":return{requested:t,resolved:"local",model:"hash-384",reason:"",fix:"",egressHost:null};default:return{requested:t,resolved:"none",model:null,reason:"no_provider_configured",fix:l,egressHost:null}}}let h=!1;function A(o){switch(o?.provider||process.env.WYRM_VECTOR_PROVIDER||"auto"){case"openai":{const e=o?.apiKey||process.env.OPENAI_API_KEY;if(!e)throw new Error("OpenAI provider requires OPENAI_API_KEY");return new v(e,o?.model)}case"ollama":return h||(h=!0,console.error("Wyrm: WYRM_VECTOR_PROVIDER=ollama is deprecated \u2014 the supported embedding provider is NVIDIA NIM ("+l+")")),new I(o?.ollamaUrl||process.env.OLLAMA_URL,o?.model);case"nim":{const e=process.env.NVIDIA_API_KEY||process.env.WYRM_NIM_API_KEY||o?.apiKey||"",s=o?.nimBaseUrl||process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com";if(!e&&m(s))throw new Error("NIM provider requires NVIDIA_API_KEY (or WYRM_NIM_API_KEY) for a remote endpoint");return new p(e,o?.model,o?.nimBaseUrl)}case"none":return new u;case"auto":return new u;default:return new b}}export{b as LocalHashProvider,l as NIM_FIX_LINE,p as NimProvider,u as NoneProvider,I as OllamaProvider,v as OpenAIProvider,A as createProvider,m as hostIfRemote,M as resolveEmbeddingState};
1
+ import{isLoopbackHost as b}from"../receipt.js";function m(o){try{const e=new URL(o).hostname.toLowerCase();return b(e)?null:e}catch{return null}}class v{name="local-hash";model="hash-384";dimensions=384;async embed(e){const t=new Float32Array(this.dimensions),s=e.toLowerCase().split(/\s+/);for(let r=0;r<s.length;r++){const i=s[r];for(let a=0;a<i.length;a++){const l=i.charCodeAt(a)*(r+1)*(a+1)%this.dimensions;t[l]+=1/s.length}}const n=Math.sqrt(t.reduce((r,i)=>r+i*i,0));if(n>0)for(let r=0;r<t.length;r++)t[r]/=n;return t}async isReady(){return!0}}class y{name="openai";model;dimensions;remoteHost="api.openai.com";apiKey;timeoutMs;constructor(e,t){this.apiKey=e,this.model=t||"text-embedding-3-small",this.dimensions=this.model.includes("large")?3072:1536;const s=Number(process.env.WYRM_OPENAI_TIMEOUT);this.timeoutMs=Number.isFinite(s)&&s>0?s:8e3}async embed(e){const t=await fetch("https://api.openai.com/v1/embeddings",{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify({input:e,model:this.model}),signal:AbortSignal.timeout(this.timeoutMs)});if(!t.ok){const n=await t.text();throw new Error(`OpenAI embedding API error ${t.status}: ${n}`)}const s=await t.json();return new Float32Array(s.data[0].embedding)}async isReady(){return!!this.apiKey}}class I{name="ollama";model;dimensions;remoteHost;baseUrl;timeoutMs;constructor(e,t){this.baseUrl=e||"http://localhost:11434",this.model=t||"nomic-embed-text",this.dimensions=this.model.includes("mxbai")?1024:768,this.remoteHost=m(this.baseUrl);const s=Number(process.env.WYRM_OLLAMA_TIMEOUT);this.timeoutMs=Number.isFinite(s)&&s>0?s:8e3}async embed(e){const t=await fetch(`${this.baseUrl}/api/embeddings`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:this.model,prompt:e}),signal:AbortSignal.timeout(this.timeoutMs)});if(!t.ok){const n=await t.text();throw new Error(`Ollama embedding API error ${t.status}: ${n}`)}const s=await t.json();return new Float32Array(s.embedding)}async isReady(){try{return(await fetch(`${this.baseUrl}/api/tags`,{signal:AbortSignal.timeout(2e3)})).ok}catch{return!1}}}class p{name="nim";model;remoteHost;dims;baseUrl;apiKey;timeoutMs;static KNOWN_DIMS={"nvidia/llama-nemotron-embed-1b-v2":2048,"nvidia/llama-3.2-nv-embedqa-1b-v2":2048,"nvidia/nv-embedqa-e5-v5":1024,"nvidia/nv-embedqa-mistral-7b-v2":4096,"nvidia/nv-embed-v1":4096};get dimensions(){return this.dims}constructor(e,t,s){this.apiKey=e,this.model=t||process.env.WYRM_NIM_EMBED_MODEL||"nvidia/llama-nemotron-embed-1b-v2",this.baseUrl=(s||process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com").replace(/\/+$/,"");const n=Number(process.env.WYRM_NIM_EMBED_DIM);this.dims=Number.isFinite(n)&&n>0?n:p.KNOWN_DIMS[this.model]??2048;const r=Number(process.env.WYRM_NIM_TIMEOUT);this.timeoutMs=Number.isFinite(r)&&r>0?r:8e3,this.remoteHost=m(this.baseUrl)}async embed(e,t="passage"){let s;for(let i=0;;i++){let a=null;try{const l={"Content-Type":"application/json"};if(this.apiKey&&(l.Authorization=`Bearer ${this.apiKey}`),s=await fetch(`${this.baseUrl}/v1/embeddings`,{method:"POST",headers:l,body:JSON.stringify({input:[e],model:this.model,input_type:t,encoding_format:"float",truncate:"END"}),signal:AbortSignal.timeout(this.timeoutMs)}),s.ok||s.status!==429&&s.status<500)break;const c=Number(s.headers?.get?.("retry-after"));a=Number.isFinite(c)&&c>0?Math.min(c*1e3,15e3):500*2**i}catch(l){if(i>=2)throw l;a=500*2**i}if(i>=2)break;await new Promise(l=>setTimeout(l,a??500))}if(!s.ok){const i=await s.text();throw new Error(`NIM embedding API error ${s.status}: ${i.slice(0,300)}`)}const r=(await s.json()).data?.[0]?.embedding;if(!Array.isArray(r)||r.length===0)throw new Error("NIM embedding API returned no embedding");return r.length!==this.dims&&(this.dims=r.length),new Float32Array(r)}async isReady(){return this.remoteHost?!!this.apiKey:!0}}class u{name="none";model="none";dimensions=0;async embed(e){return new Float32Array(0)}async isReady(){return!0}}const d="set WYRM_VECTOR_PROVIDER=nim and NVIDIA_API_KEY (or WYRM_NIM_API_KEY) in the server env, then: wyrm index rebuild";function h(o){const e=String(o??"").trim().toLowerCase();return e===""?"auto":e}function M(o){try{return new URL(o),!1}catch{return!0}}function E(o){const e=h(o??process.env.WYRM_VECTOR_PROVIDER);switch(e){case"nim":{const t=process.env.NVIDIA_API_KEY||process.env.WYRM_NIM_API_KEY||"",s=process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com";if(M(s))return{requested:e,resolved:"none",model:null,reason:"nim_bad_base_url",fix:"WYRM_NIM_BASE_URL is not a valid URL (needs https://\u2026): "+s,egressHost:null};const n=m(s);if(!t&&n)return{requested:e,resolved:"none",model:null,reason:"nim_missing_key",fix:d,egressHost:null};const r=process.env.WYRM_NIM_EMBED_MODEL||"nvidia/llama-nemotron-embed-1b-v2";return{requested:e,resolved:"nim",model:r,reason:"",fix:"",egressHost:n}}case"openai":return process.env.OPENAI_API_KEY?{requested:e,resolved:"openai",model:"text-embedding-3-small",reason:"",fix:"",egressHost:"api.openai.com"}:{requested:e,resolved:"none",model:null,reason:"openai_missing_key",fix:"set OPENAI_API_KEY, or use the supported provider: "+d,egressHost:null};case"ollama":{const t=process.env.OLLAMA_URL||"http://localhost:11434";return{requested:e,resolved:"ollama",model:process.env.WYRM_EMBED_MODEL||"nomic-embed-text",reason:"ollama_deprecated",fix:"Ollama support is deprecated and leaves in the next major \u2014 "+d,egressHost:m(t),advisory:!0}}case"none":return{requested:e,resolved:"none",model:null,reason:"disabled",fix:"vectors were explicitly disabled; to enable: "+d,egressHost:null};case"local":return{requested:e,resolved:"local",model:"hash-384",reason:"local_hash_fallback",fix:"local-hash is non-semantic (test only) \u2014 "+d,egressHost:null,advisory:!0};default:return{requested:e,resolved:"none",model:null,reason:"no_provider_configured",fix:d,egressHost:null}}}let _=!1;function g(o){switch(h(o?.provider??process.env.WYRM_VECTOR_PROVIDER)){case"openai":{const t=o?.apiKey||process.env.OPENAI_API_KEY;if(!t)throw new Error("OpenAI provider requires OPENAI_API_KEY");return new y(t,o?.model)}case"ollama":return _||(_=!0,console.error("Wyrm: WYRM_VECTOR_PROVIDER=ollama is deprecated \u2014 the supported embedding provider is NVIDIA NIM ("+d+")")),new I(o?.ollamaUrl||process.env.OLLAMA_URL,o?.model);case"nim":{const t=process.env.NVIDIA_API_KEY||process.env.WYRM_NIM_API_KEY||o?.apiKey||"",s=o?.nimBaseUrl||process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com";if(!t&&m(s))throw new Error("NIM provider requires NVIDIA_API_KEY (or WYRM_NIM_API_KEY) for a remote endpoint");return new p(t,o?.model,o?.nimBaseUrl)}case"none":return new u;case"auto":return new u;case"local":return new v;default:return new u}}export{v as LocalHashProvider,d as NIM_FIX_LINE,p as NimProvider,u as NoneProvider,I as OllamaProvider,y as OpenAIProvider,g as createProvider,m as hostIfRemote,h as normalizeProviderKey,E as resolveEmbeddingState};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v2",
3
- "version": "8.3.0",
3
+ "version": "8.3.1",
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.3.0",
3
+ "version": "8.3.1",
4
4
  "tools": [
5
5
  {
6
6
  "name": "wyrm_recall",
@@ -1 +1 @@
1
- import{createVectorStore as p}from"./vectors.js";import{IndexingPipeline as E}from"./indexer.js";function v(t,s,i){let r=null,o=null;try{const n=process.env.WYRM_VECTOR_PROVIDER||"auto";if(n!=="none"){r=p({provider:n},t.getDatabase()),s.setVectorStore(r);const c=(u,a)=>{try{switch(u){case"session":{const e=t.getSession(a);return e&&[e.objectives,e.completed,e.notes,e.summary].filter(Boolean).join(" ")||null}case"quest":{const e=t.getDatabase().prepare("SELECT * FROM quests WHERE id = ?").get(a);return e&&[e.title,e.description].filter(Boolean).join(" ")||null}case"note":case"context":{const e=t.getDatabase().prepare("SELECT value FROM data_lake WHERE id = ?").all(a);if(!e.length)return null;const l=i.maybeDecrypt(e[0].value);return l.startsWith("ENCRYPTED:")?null:l}default:return null}}catch{return null}};o=new E(t.getDatabase(),r,c,{batchSize:20,maxRetries:3,intervalMs:1e4}),o.start()}}catch(n){console.error("Wyrm: Failed to initialize vector store:",n)}return{vectorStore:r,indexingPipeline:o}}export{v as initVectorSearch};
1
+ import{createVectorStore as p}from"./vectors.js";import{resolveEmbeddingState as m}from"./providers/embedding-provider.js";import{IndexingPipeline as v}from"./indexer.js";function R(t,s,i){let r=null,o=null;try{const n=process.env.WYRM_VECTOR_PROVIDER||"auto";if(m(n).resolved!=="none"){r=p({provider:n},t.getDatabase()),s.setVectorStore(r);const c=(u,a)=>{try{switch(u){case"session":{const e=t.getSession(a);return e&&[e.objectives,e.completed,e.notes,e.summary].filter(Boolean).join(" ")||null}case"quest":{const e=t.getDatabase().prepare("SELECT * FROM quests WHERE id = ?").get(a);return e&&[e.title,e.description].filter(Boolean).join(" ")||null}case"note":case"context":{const e=t.getDatabase().prepare("SELECT value FROM data_lake WHERE id = ?").all(a);if(!e.length)return null;const l=i.maybeDecrypt(e[0].value);return l.startsWith("ENCRYPTED:")?null:l}default:return null}}catch{return null}};o=new v(t.getDatabase(),r,c,{batchSize:20,maxRetries:3,intervalMs:1e4}),o.start()}}catch(n){console.error("Wyrm: Failed to initialize vector store:",n)}return{vectorStore:r,indexingPipeline:o}}export{R as initVectorSearch};
package/dist/wyrm-cli.js CHANGED
@@ -67,13 +67,13 @@ ${d.bold(`[${a.kind}] #${a.id}`)}`),console.log(d.dim("\u2500".repeat(60))),cons
67
67
  Review complete.`)}async function pt(l){const{positional:c,flags:t}=C(l),i=c[0];(!i||!["export","import","preview"].includes(i))&&(m("Usage: wyrm sync export --out <path> | wyrm sync import --from <path> | wyrm sync preview --from <path>"),process.exit(1));const{randomBytes:r,pbkdf2Sync:e,createCipheriv:s,createDecipheriv:o}=await import("crypto"),{readFileSync:n,writeFileSync:p,copyFileSync:a,unlinkSync:u,existsSync:g,chmodSync:f}=await import("fs"),{homedir:y}=await import("os"),{join:h}=await import("path"),w=(await import("better-sqlite3")).default;let $=process.env.WYRM_SYNC_PASSPHRASE??"";if(!$){const T=Q({input:process.stdin,output:process.stdout});$=await new Promise(P=>{process.stdout.write("Passphrase: "),process.stdin.isTTY&&process.stdin.setRawMode?.(!0),T.question("",U=>{process.stdin.isTTY&&process.stdin.setRawMode?.(!1),console.log(""),T.close(),P(U)})})}$||(m("Passphrase is required. Set WYRM_SYNC_PASSPHRASE or enter interactively."),process.exit(1));const E=h(y(),".wyrm"),_=j();if(i==="export"){const T=t.out;T||(m("--out <path> is required"),process.exit(1));const P=h(E,"wyrm_cli_export_temp.db");try{const U=_.getDatabase();g(P)&&u(P),U.prepare("VACUUM INTO ?").run(P);const K=n(P),H=r(32),J=r(16),Ee=e($,H,6e5,32,"sha256"),se=s("aes-256-gcm",Ee,J),Re=Buffer.concat([se.update(K),se.final()]),xe=se.getAuthTag(),Te=Buffer.from("WYRM"),le=Buffer.alloc(1);le.writeUInt8(1,0);const de=Buffer.concat([Te,le,H,J,xe,Re]);p(T,de);try{f(T,384)}catch{}try{u(P)}catch{}const De=(de.length/(1024*1024)).toFixed(2);b(`Exported to ${T} (${De} MB)`)}catch(U){try{g(P)&&u(P)}catch{}m(`Export failed: ${U}`)}_.close();return}const R=t.from;R||(m("--from <path> is required"),process.exit(1));const S=n(R);S.subarray(0,4).toString("ascii")!=="WYRM"&&(m("Invalid Wyrm snapshot file."),process.exit(1));const L=S.readUInt8(4);L!==1&&(m(`Unsupported snapshot version: ${L}`),process.exit(1));const W=S.subarray(5,37),ve=S.subarray(37,53),ke=S.subarray(53,69),Se=S.subarray(69),_e=e($,W,6e5,32,"sha256"),Z=o("aes-256-gcm",_e,ve);Z.setAuthTag(ke);let ee;try{ee=Buffer.concat([Z.update(Se),Z.final()])}catch{m("Decryption failed \u2014 wrong passphrase or corrupted file."),process.exit(1)}if(i==="preview"){const T=h(E,"wyrm_cli_preview_temp.db");g(T)&&u(T),p(T,ee);try{const P=new w(T,{readonly:!0}),U=["projects","sessions","ground_truths","memory_artifacts","quests"];k("Snapshot Preview");const K=[];for(const H of U)try{const J=P.prepare(`SELECT COUNT(*) as n FROM ${H}`).get();K.push([H,String(J.n)])}catch{K.push([H,"?"])}console.log(D(["Table","Count"],K)),P.close()}catch(P){m(`Preview failed: ${P}`)}try{g(T)&&u(T)}catch{}_.close();return}const B=_.getDatabasePath(),ce=Q({input:process.stdin,output:process.stdout}),je=await new Promise(T=>{ce.question("This will REPLACE your current database. Type CONFIRM to proceed: ",T)});if(ce.close(),je.trim()!=="CONFIRM"){console.log(d.dim("Aborted.")),_.close();return}const Ce=new Date().toISOString().replace(/[:.]/g,"-"),te=`${B}.backup.${Ce}`;a(B,te),b(`Backed up to ${te}`);const oe=h(E,"wyrm_cli_restore_temp.db");p(oe,ee),_.getDatabase().close(),a(oe,B);for(const T of["-wal","-shm"])try{g(B+T)&&u(B+T)}catch{}try{u(oe)}catch{}b(`Restored from ${R}. Backup at ${te}`)}async function ut(l){const{flags:c}=C(l),t=c.project,i=c.path,r=X(c["min-confidence"],.3),e=I(c["older-than"],90),s=c["no-dry-run"]===!0,o=c.yes===!0,n=j(),p=n.getDatabase();let a=null;if(i||t){const h=i?n.getProject(i):x(n,t);h||(m(`Project not found: ${i??t}`),n.close(),process.exit(1)),a=h.id}const u=new G(p),{candidates:g}=u.pruneStale({projectId:a,minConfidence:r,olderThanDays:e,dryRun:!0});if(k(`Prune Candidates${s?" (LIVE DELETE)":" (dry-run)"}`),g.length===0){console.log(d.dim(" No artifacts match prune criteria.")),n.close();return}const f=g.map(h=>[String(h.id),h.kind,h.problem.slice(0,60),(h.confidence*100).toFixed(0)+"%",h.last_accessed_at??"never"]);if(console.log(D(["ID","Kind","Problem","Conf","Last Accessed"],f)),console.log(`
68
68
  Total: ${g.length} candidate(s)`),!s){console.log(d.dim(`
69
69
  This is a dry run. Use --no-dry-run to delete (confirm each ID).`)),n.close();return}if(!o){const h=Q({input:process.stdin,output:process.stdout}),w=await new Promise($=>{h.question(`
70
- Delete these ${g.length} artifact(s)? Type CONFIRM to proceed: `,$)});if(h.close(),w.trim()!=="CONFIRM"){console.log(d.dim("Aborted.")),n.close();return}}const y=u.deleteArtifacts(g.map(h=>h.id));b(`Deleted ${y} artifact(s).`),n.close()}async function mt(l){const{positional:c,flags:t}=C(l),i=c[0]||"list",r=j();try{if(i==="list"||i==="ls"){const e=he(r);if(e.length===0){k("Connectors"),console.log(`${N.dim} none configured. Add one with: wyrm connector add --name <n> --url <bridge-url> --workspace <w>${N.reset}`);return}k("Connectors");const s=e.map(o=>[o.name,o.source,o.enabled?"on":"off",o.workspace,kt(o.baseUrl),o.allowlist.length?`${o.allowlist.length} chats`:"all"]);console.log(D(["Name","Source","Enabled","Workspace","Bridge","Allowlist"],s));return}if(i==="add"){const e=A(t.name),s=A(t.url),o=A(t.workspace);if(!e||!s||!o){m("usage: wyrm connector add --name <n> --url <bridge-url> --workspace <w> [--allowlist a,b] [--token T] [--disabled]"),process.exitCode=1;return}const n={name:e,source:"bridge",enabled:!t.disabled,baseUrl:s,workspace:o,allowlist:A(t.allowlist)?A(t.allowlist).split(",").map(p=>p.trim()).filter(Boolean):[],...A(t.token)?{authToken:A(t.token)}:{}};Ye(r,n),b(`Connector "${e}" saved (source=bridge, workspace=${o}). Token, if any, stored without being printed.`),console.log(`${N.dim} Tip: prefer the env var WYRM_CONNECTOR_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]/g,"_")} over --token.${N.reset}`);return}if(i==="sync"){const e=c[1]||A(t.name);if(e){const o=he(r).find(p=>p.name===e);if(!o){m(`Connector not found: ${e}`),process.exitCode=1;return}const n=await He(r,o);b(`${n.source}: fetched ${n.fetched}, ingested ${n.ingested}, skipped ${n.skipped} -> ${n.workspace}`);return}const s=await Ve(r);if(s.length===0){console.log(`${N.dim} no enabled connectors.${N.reset}`);return}for(const o of s)"error"in o?m(`${o.name}: ${o.error}`):b(`${o.source}: fetched ${o.fetched}, ingested ${o.ingested}, skipped ${o.skipped} -> ${o.workspace}`);return}m(`Unknown connector subcommand: ${i} (use list | add | sync)`),process.exitCode=1}finally{r.close()}}function we(){return V(0,"utf-8").trim()}async function ft(){const{initializeLicense:l,getLicenseInfo:c,getTier:t}=await import("./license.js");l();const i=c();k("Wyrm License");const r=[["Tier",t()],["Status",i.valid?"valid":"free tier (no license key)"]];i.valid&&i.key&&(r.push(["Key",i.key]),r.push(["Issued to",i.issuedTo??"unknown"]),r.push(["Expires",i.expiresAt?new Date(i.expiresAt).toLocaleDateString():"never"])),r.push(["Features",i.features.join(", ")||"(free)"]),console.log(D(["Field","Value"],r)),i.valid||console.log(d.dim(`
70
+ Delete these ${g.length} artifact(s)? Type CONFIRM to proceed: `,$)});if(h.close(),w.trim()!=="CONFIRM"){console.log(d.dim("Aborted.")),n.close();return}}const y=u.deleteArtifacts(g.map(h=>h.id));b(`Deleted ${y} artifact(s).`),n.close()}async function mt(l){const{positional:c,flags:t}=C(l),i=c[0]||"list",r=j();try{if(i==="list"||i==="ls"){const e=he(r);if(e.length===0){k("Connectors"),console.log(`${N.dim} none configured. Add one with: wyrm connector add --name <n> --url <bridge-url> --workspace <w>${N.reset}`);return}k("Connectors");const s=e.map(o=>[o.name,o.source,o.enabled?"on":"off",o.workspace,kt(o.baseUrl),o.allowlist.length?`${o.allowlist.length} chats`:"all"]);console.log(D(["Name","Source","Enabled","Workspace","Bridge","Allowlist"],s));return}if(i==="add"){const e=O(t.name),s=O(t.url),o=O(t.workspace);if(!e||!s||!o){m("usage: wyrm connector add --name <n> --url <bridge-url> --workspace <w> [--allowlist a,b] [--token T] [--disabled]"),process.exitCode=1;return}const n={name:e,source:"bridge",enabled:!t.disabled,baseUrl:s,workspace:o,allowlist:O(t.allowlist)?O(t.allowlist).split(",").map(p=>p.trim()).filter(Boolean):[],...O(t.token)?{authToken:O(t.token)}:{}};Ye(r,n),b(`Connector "${e}" saved (source=bridge, workspace=${o}). Token, if any, stored without being printed.`),console.log(`${N.dim} Tip: prefer the env var WYRM_CONNECTOR_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]/g,"_")} over --token.${N.reset}`);return}if(i==="sync"){const e=c[1]||O(t.name);if(e){const o=he(r).find(p=>p.name===e);if(!o){m(`Connector not found: ${e}`),process.exitCode=1;return}const n=await He(r,o);b(`${n.source}: fetched ${n.fetched}, ingested ${n.ingested}, skipped ${n.skipped} -> ${n.workspace}`);return}const s=await Ve(r);if(s.length===0){console.log(`${N.dim} no enabled connectors.${N.reset}`);return}for(const o of s)"error"in o?m(`${o.name}: ${o.error}`):b(`${o.source}: fetched ${o.fetched}, ingested ${o.ingested}, skipped ${o.skipped} -> ${o.workspace}`);return}m(`Unknown connector subcommand: ${i} (use list | add | sync)`),process.exitCode=1}finally{r.close()}}function we(){return V(0,"utf-8").trim()}async function ft(){const{initializeLicense:l,getLicenseInfo:c,getTier:t}=await import("./license.js");l();const i=c();k("Wyrm License");const r=[["Tier",t()],["Status",i.valid?"valid":"free tier (no license key)"]];i.valid&&i.key&&(r.push(["Key",i.key]),r.push(["Issued to",i.issuedTo??"unknown"]),r.push(["Expires",i.expiresAt?new Date(i.expiresAt).toLocaleDateString():"never"])),r.push(["Features",i.features.join(", ")||"(free)"]),console.log(D(["Field","Value"],r)),i.valid||console.log(d.dim(`
71
71
  Activate with: wyrm login (free) \xB7 or: wyrm activate <license.json | key>`)),console.log(d.dim(`
72
72
  \xA9 2026 Ghost Protocol (Pvt) Ltd \xB7 Proprietary \xB7 https://wyrm.ghosts.lk`)),console.log(d.dim(" Licensed under the Wyrm Terms of Service. No open-source license is granted."))}async function gt(){const l=(process.env.WYRM_ACCOUNT_URL??"https://account.ghosts.lk").replace(/\/$/,""),c=(()=>{try{return M().version??"unknown"}catch{return"unknown"}})();let t;try{const a=await fetch(`${l}/api/v1/cli/auth/start`,{method:"POST",headers:{"x-wyrm-version":c}});if(!a.ok)throw new Error(`HTTP ${a.status}`);t=await a.json()}catch(a){m(`Couldn't reach ${l} (${a instanceof Error?a.message:"network error"}).`),process.exitCode=1;return}const i=t.verification_uri_complete||t.verification_uri||`${l}/cli`;console.log(`
73
73
  ${d.cyan("Sign in to activate Wyrm (free):")}`),console.log(` 1. Open ${d.cyan(i)}`),console.log(` 2. Approve the code ${d.cyan(t.user_code)}`),console.log(d.dim(`
74
74
  Waiting for approval\u2026 (Ctrl-C to cancel)`));const r=(t.interval??3)*1e3,e=Date.now()+(t.expires_in??600)*1e3;let s="";for(;Date.now()<e;){await new Promise(a=>{setTimeout(a,r)});try{const u=await(await fetch(`${l}/api/v1/cli/auth/poll`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({device_code:t.device_code})})).json();if(u.status==="approved"&&u.token){s=u.token;break}if(u.status==="denied"||u.error==="expired"){m("Login was denied or the code expired. Run `wyrm login` again."),process.exitCode=1;return}}catch{}}if(!s){m("Login timed out. Run `wyrm login` again."),process.exitCode=1;return}let o;try{const a=await fetch(`${l}/api/v1/license/free`,{method:"POST",headers:{authorization:`Bearer ${s}`,"x-wyrm-version":c}});if(!a.ok){const u=await a.json().catch(()=>({}));m(`Activation failed (${a.status}): ${u.hint||u.error||"unknown error"}`),process.exitCode=1;return}o=JSON.stringify(await a.json())}catch(a){m(`Activation request failed (${a instanceof Error?a.message:"network error"}).`),process.exitCode=1;return}const{activateLicense:n}=await import("./license.js"),p=n(o);p.valid?(b(`Signed in & activated \u2014 ${p.tier} tier (expires ${p.expiresAt??"never"}).`),console.log(d.dim(" Restart the Wyrm MCP server / daemon to apply."))):(m(`Activation failed: ${p.error??"unknown error"}`),process.exitCode=1)}async function yt(l){const{positional:c}=C(l),t=c[0];let i;t&&q(t)?i=V(t,"utf-8"):t?i=t:process.stdin.isTTY?(m("Usage: wyrm activate <license.json path | license JSON> (or pipe the JSON on stdin)"),process.exit(1)):i=we();const{activateLicense:r}=await import("./license.js");try{const e=r(i);e.valid?(b(`License activated \u2014 ${e.tier} tier (${e.features.join(", ")})`),console.log(d.dim(" Restart Wyrm (MCP server / daemon) to apply all features."))):(m(`License activation failed: ${e.error??"unknown error"}`),process.exitCode=1)}catch{m("Invalid license format. Please verify your license key."),process.exitCode=1}}async function ht(l){const{flags:c}=C(l),{runMaintenance:t}=await import("./maintenance.js"),{FailurePatterns:i}=await import("./failure-patterns.js"),{SessionSeen:r}=await import("./session-seen.js"),{AgentPresence:e}=await import("./presence.js"),s=j();try{const o=s.getDatabase(),n=I(c["archive-days"],0),p=t({db:s,sessionSeen:new r(o),failures:new i(o),presence:new e(o)},{vacuum:c.vacuum===!0,archiveDays:n>0?n:void 0});k("Maintenance complete");for(const a of p.lines)console.log(` - ${a}`);console.log(d.dim(`
75
75
  Database size: ${p.dbSize}`))}finally{s.close()}}async function wt(l){const{resolveEmbeddingState:c}=await import("./providers/embedding-provider.js"),t=j();let i=0;const r=(s,o)=>{console.log(` ${d.green("\u2714")} ${s} \u2014 ${d.dim(o)}`)},e=(s,o,n)=>{i++,console.log(` ${d.red("\u2716")} ${s} \u2014 ${o}`),console.log(` ${d.yellow("fix:")} ${n}`)};try{const s=t.getDatabase();k("wyrm doctor");const o=c();o.reason===""&&o.resolved!=="local"?r("Embedding provider",`${o.resolved} (${o.model})${o.egressHost?` \xB7 egress: ${o.egressHost}`:" \xB7 local"}`):o.resolved==="local"?e("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"?e("Embedding provider","ollama (deprecated \u2014 leaves in the next major)",o.fix):e("Embedding provider",`none (${o.reason})`,o.fix);const n=s.prepare("SELECT COUNT(*) AS n FROM memory_artifacts").get().n;let p=0;try{p=s.prepare("SELECT COUNT(*) AS n FROM vectors").get().n}catch{}o.resolved==="none"||o.resolved==="local"?e("Vector index",`${p} vector(s) / ${n} memories \u2014 recall is FTS5 keyword-only (~59.9% vs 72.2% hybrid recall@10)`,o.fix||"enable a provider, then: wyrm index rebuild"):p===0&&n>0?e("Vector index",`provider '${o.resolved}' is configured but 0 of ${n} memories are indexed`,"wyrm index rebuild"):r("Vector index",`${p} vector(s) over ${n} memories`);try{const g=s.prepare("SELECT COUNT(*) AS n FROM memory_artifacts_fts").get().n;g===n?r("FTS index",`${g}/${n} rows in sync`):e("FTS index",`${g} FTS rows vs ${n} memories \u2014 keyword recall is missing rows`,"INSERT INTO memory_artifacts_fts(memory_artifacts_fts) VALUES('rebuild') via wyrm maintenance")}catch{e("FTS index","memory_artifacts_fts unreadable","wyrm maintenance")}const a=s.prepare("SELECT COUNT(*) AS n FROM failure_patterns WHERE resolved = 0").get().n;r("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?e("Review queue",`${u} memor${u===1?"y":"ies"} invisible to recall until approved`,"wyrm review"):r("Review queue","empty");try{const g=s.prepare("SELECT MAX(version) AS v FROM schema_versions").get().v;r("Schema",`migration v${g??0}`)}catch{e("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{t.close()}}async function bt(l){const{positional:c,flags:t}=C(l),i=c[0]??"list",r=j();try{const e=r.getDatabase();if(i==="list"){const s=e.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(d.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(d.dim(`
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=t.note??"";(e.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{r.close()}}async function $t(l){const{positional:c,flags:t}=C(l),i=c[0]??"list",r=j();try{if(i==="add"){const e=c[1]??process.cwd(),s=Ie(e);if(!q(s)){m(`Path does not exist: ${s}`),process.exitCode=1;return}const o=t.name??Pe(s),n=r.registerProject(o,s);b(`Project #${n.id} '${n.name}' registered at ${n.path}`);return}if(i==="list"){const e=r.getAllProjects(50);k("Projects"),console.log(D(["ID","Name","Path"],e.map(s=>[String(s.id),s.name,s.path])));return}m("Usage: wyrm project <add [path] [--name N] | list>"),process.exitCode=1}finally{r.close()}}async function vt(l){const{positional:c,flags:t}=C(l),i="Usage: wyrm index <setup|rebuild|status> [--provider auto|local|nim|openai|none] [--model M] [--project P] [--dry-run]";if(t.help===!0){console.log(i);return}const r=c[0]??"status",{createVectorStore:e}=await import("./vectors.js"),o={provider:t.provider??process.env.WYRM_VECTOR_PROVIDER??"auto",model:t.model,apiKey:t["api-key"]??process.env.OPENAI_API_KEY,ollamaUrl:t["ollama-url"]};if(r==="setup"){const{createProvider:p}=await import("./providers/embedding-provider.js"),a=p(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(d.dim(" The MCP server reads WYRM_VECTOR_PROVIDER (and provider-specific env) at boot \u2014")),console.log(d.dim(` set WYRM_VECTOR_PROVIDER=${a.name==="none"?"none":o.provider} in the server env, then: wyrm index rebuild`));return}const n=j();try{const p=n.getDatabase(),a=e(o,p);if(r==="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(r==="rebuild"){const{reindexProjects:u}=await import("./reindex.js"),g=t["dry-run"]===!0,f=t.project;let y;if(f){const $=n.getProject(f)??x(n,f);if(!$){m(`Project not found: ${f}`),process.exitCode=1;return}y=[$.id]}else y=n.getAllProjects(1e3).map($=>$.id);const{indexed:h,skipped:w}=await u(p,a,y,{dryRun:g,onError:($,E)=>m(`${$}: ${JSON.stringify(E)}`)});b(`Reindex ${g?"(dry run) ":""}\u2014 ${y.length} project(s), ${h} indexed, ${w} skipped`);return}m(i),process.exitCode=1}finally{n.close()}}function A(l){return typeof l=="string"?l:""}function kt(l){try{return new URL(l).host}catch{return l}}async function St(l){const{flags:c}=C(l),{getUpdateStatus:t}=await import("./version-check.js"),i=M().version??"0.0.0",r=j();let e;try{e=await t(r.getDatabase(),i,{force:c.force===!0||c.check===!0})}finally{r.close()}if(k("Wyrm update"),console.log(D(["Field","Value"],[["Current",e.current],["Latest",e.latest??"unknown (offline?)"],["Update available",e.updateAvailable?"yes":"no"],["Checked",`${e.checkedAt} (${e.source})`]])),c.check===!0)return;if(!e.updateAvailable&&c.force!==!0){console.log(d.dim(`
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=O(t.note);(e.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{r.close()}}async function $t(l){const{positional:c,flags:t}=C(l),i=c[0]??"list",r=j();try{if(i==="add"){const e=c[1]??process.cwd(),s=Ie(e);if(!q(s)){m(`Path does not exist: ${s}`),process.exitCode=1;return}const o=O(t.name)||Pe(s),n=r.registerProject(o,s);b(`Project #${n.id} '${n.name}' registered at ${n.path}`);return}if(i==="list"){const e=r.getAllProjects(50);k("Projects"),console.log(D(["ID","Name","Path"],e.map(s=>[String(s.id),s.name,s.path])));return}m("Usage: wyrm project <add [path] [--name N] | list>"),process.exitCode=1}finally{r.close()}}async function vt(l){const{positional:c,flags:t}=C(l),i="Usage: wyrm index <setup|rebuild|status> [--provider auto|local|nim|openai|none] [--model M] [--project P] [--dry-run]";if(t.help===!0||typeof t.help=="string"){console.log(i);return}const r=c[0]??"status",{createVectorStore:e}=await import("./vectors.js"),o={provider:t.provider??process.env.WYRM_VECTOR_PROVIDER??"auto",model:t.model,apiKey:t["api-key"]??process.env.OPENAI_API_KEY,ollamaUrl:t["ollama-url"]};if(r==="setup"){const{createProvider:p}=await import("./providers/embedding-provider.js"),a=p(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(d.dim(" The MCP server reads WYRM_VECTOR_PROVIDER (and provider-specific env) at boot \u2014")),console.log(d.dim(` set WYRM_VECTOR_PROVIDER=${a.name==="none"?"none":o.provider} in the server env, then: wyrm index rebuild`));return}const n=j();try{const p=n.getDatabase(),a=e(o,p);if(r==="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(r==="rebuild"){const{reindexProjects:u}=await import("./reindex.js"),g=t["dry-run"]===!0,f=t.project;let y;if(f){const $=n.getProject(f)??x(n,f);if(!$){m(`Project not found: ${f}`),process.exitCode=1;return}y=[$.id]}else y=n.getAllProjects(1e3).map($=>$.id);const{indexed:h,skipped:w}=await u(p,a,y,{dryRun:g,onError:($,E)=>m(`${$}: ${JSON.stringify(E)}`)});b(`Reindex ${g?"(dry run) ":""}\u2014 ${y.length} project(s), ${h} indexed, ${w} skipped`);return}m(i),process.exitCode=1}finally{n.close()}}function O(l){return typeof l=="string"?l:""}function kt(l){try{return new URL(l).host}catch{return l}}async function St(l){const{flags:c}=C(l),{getUpdateStatus:t}=await import("./version-check.js"),i=M().version??"0.0.0",r=j();let e;try{e=await t(r.getDatabase(),i,{force:c.force===!0||c.check===!0})}finally{r.close()}if(k("Wyrm update"),console.log(D(["Field","Value"],[["Current",e.current],["Latest",e.latest??"unknown (offline?)"],["Update available",e.updateAvailable?"yes":"no"],["Checked",`${e.checkedAt} (${e.source})`]])),c.check===!0)return;if(!e.updateAvailable&&c.force!==!0){console.log(d.dim(`
77
77
  Already up to date. (Use --force to reinstall anyway.)`));return}console.log(d.dim(`
78
78
  Running: npm install -g wyrm-mcp@latest
79
79
  `));const s=z("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 _t(l){const{positional:c,flags:t}=C(l),i=c[0],r=t.project??process.cwd();if(i==="inject"){const{injectSystemPrompt:e}=await import("./autoconfig.js"),s=typeof t.clients=="string"?t.clients.split(",").map(n=>n.trim()).filter(Boolean):[],o=e(r,s);k("System prompt injection");for(const n of o.injected)console.log(` + ${n}`);for(const n of o.skipped)console.log(d.dim(` o skipped (unknown client): ${n}`));for(const n of o.errors)m(n);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:e,renderMigrationReport:s}=await import("./migrate-prompt.js"),{WYRM_INJECT_BLOCK:o}=await import("./autoconfig.js"),n=t.apply===!0,p=e({projectPath:r,newBlock:o,apply:n});console.log(s(p,n));return}m("Usage: wyrm prompt inject [--project <path>] [--clients copilot,cursor] | wyrm prompt migrate [--project <path>] [--apply]"),process.exitCode=1}async function jt(l){const{positional:c,flags:t}=C(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 r=t.from,e=t.to;(!r||!e)&&(m("--from and --to are required (YYYY-MM-DD)"),process.exit(1));const{HourLedger:s}=await import("./hours.js"),o=j();try{const n=t.project,p=n?x(o,n):null;if(n&&!p){m(`Project not found: ${n}`),process.exitCode=1;return}const a=new s(o.getDatabase()).report({range_start:r,range_end:e,project_id:p?.id,default_session_hours:X(t["session-hours"],1)});if(t.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(d.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(`
@@ -186,7 +186,7 @@ ${d.bold("Examples:")}
186
186
  wyrm sync export --out ~/wyrm-backup.wyrm
187
187
  wyrm sync preview --from ~/wyrm-backup.wyrm
188
188
  wyrm prune --project MyApp --min-confidence 0.2 --older-than 30
189
- `)}const[,,O,...v]=process.argv;function $e(l){const c=l.ref_table?`${l.ref_table}${l.ref_id?"#"+l.ref_id:""}`:"",t=new Date(l.created_at).toLocaleTimeString();return`#${l.cursor} ${l.kind.padEnd(15)} ${c.padEnd(16)} ${Be(l.actor)} ${t}`}async function Tt(l){const{positional:c,flags:t}=C(l),i=c[0]||"since",r=j();try{if(!r.liveMemoryEnabled()){m("Live Memory is disabled (set WYRM_LIVE_MEMORY=1)."),process.exitCode=1;return}const e=(typeof t.project=="string"?t.project:"")||process.cwd(),s=r.getProject(e)??r.getProjectByName(e);if(!s){m(`Project not found: ${e}`),process.exitCode=1;return}if(i==="publish"){const o=c[1]||(typeof t.kind=="string"?t.kind:"");if(!o){m("usage: wyrm events publish <kind> --project <p> [--actor A] [--ref-table T --ref-id ID]"),process.exitCode=1;return}r.publishEvent({projectId:s.id,kind:o,refTable:typeof t["ref-table"]=="string"?t["ref-table"]:void 0,refId:typeof t["ref-id"]=="string"?t["ref-id"]:void 0,actor:typeof t.actor=="string"?t.actor:void 0}),b(`Event published (${o}) to ${s.name}`);return}if(i==="since"){const o=I(t.cursor,0),n=I(t.limit,50),p=r.eventsSince(s.id,o,n);for(const a of p)console.log($e(a));k(`${p.length} event(s) for '${s.name}' since cursor ${o}`);return}m("usage: wyrm events <publish|since> ..."),process.exitCode=1}finally{r.close()}}async function Dt(l){const{flags:c}=C(l),t=j();if(!t.liveMemoryEnabled()){m("Live Memory is disabled (set WYRM_LIVE_MEMORY=1)."),t.close(),process.exitCode=1;return}const i=(typeof c.project=="string"?c.project:"")||process.cwd(),r=t.getProject(i)??x(t,i);if(!r){m(`Project not found: ${i}`),t.close(),process.exitCode=1;return}const e=Math.max(250,I(c.interval,1e3));let s=I(c.since,t.subscribeEvents(r.id,1).cursor);k(`Watching '${r.name}' (cursor ${s}, every ${e}ms) \u2014 Ctrl-C to stop`);const o=()=>{try{for(const a of t.eventsSince(r.id,s,200))s=a.cursor,console.log($e(a))}catch{}};o();const n=setInterval(o,e),p=()=>{clearInterval(n);try{t.close()}catch{}process.exit(0)};process.on("SIGINT",p),process.on("SIGTERM",p)}async function It(l){const{flags:c}=C(l),{embedAll:t,removeAll:i,statusAll:r}=await import("./priority-embed.js"),e={projectDir:typeof c.project=="string"?c.project:void 0,allClients:c.all===!0},s=o=>console.log(` ${String(o.result??o.status).padEnd(9)} [${o.scope}] ${o.file}`);if(c.status){k("Wyrm priority embedding \u2014 status"),r(e).forEach(s);return}if(c.remove){k("Wyrm priority embedding \u2014 removed"),i(e).forEach(s);return}k("Wyrm is now FIRST-PRIORITY memory"),t(e).forEach(s);try{const{installClaudeCodeHooks:o,installClaudeStatusline:n}=await import("./autoconfig.js");o()?b("Proactive hooks installed (SessionStart rehydrate + capture + tool-trace)."):console.log(" (Claude Code not detected \u2014 skipped hook install.)");const a=n();a&&b(`Buddy statusline: ${a.message}`)}catch{}b("Wyrm will now be read first, primed proactively, and shown in the TUI at all times.")}async function Pt(l){const{flags:c}=C(l),{harvestProjects:t}=await import("./harvest.js"),{MemoryArtifacts:i}=await import("./memory-artifacts.js"),{escapeLikePattern:r}=await import("./auto-capture.js"),e=j();try{const s=e.getDatabase(),o=new i(s),n={existsBySig:(w,$)=>!!s.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(w,"%"+r($)+"%"),addCandidate:(w,$)=>o.add(w,{kind:$.kind,problem:$.text,tags:[...$.tags,$.sig],confidence:$.confidence,needsReview:1,createdBy:"harvest"})},p=typeof c.project=="string"?c.project:void 0;let a;if(p){const w=e.getProject(p)??e.getProjectByName(p);if(!w){m(`Project not found: ${p}`),process.exitCode=1;return}a=[{id:w.id,name:w.name,path:w.path}]}else a=e.getAllProjects(500).map(w=>({id:w.id,name:w.name,path:w.path}));const u=c["dry-run"]===!0||c.dry===!0,g=c.code===!0||c["include-code"]===!0,{reports:f,totalAdded:y,totalSkipped:h}=t(n,a,{dryRun:u,gitLimit:I(c.limit,30),includeCode:g});k(`Harvest ${u?"(dry run) ":""}\u2014 ${y} candidate(s), ${h} already present (${a.length} project(s))`);for(const w of f.filter($=>$.added>0).sort(($,E)=>E.added-$.added).slice(0,25))console.log(` +${String(w.added).padStart(3)} (skip ${w.skipped}) ${w.project}`);if(g&&!u){const{SymbolGraph:w}=await import("./symbols.js"),$=new w(s);let E=0,_=0;for(const R of a)try{const S=$.indexProject(R.id,R.path);E+=S.symbols,_+=S.files}catch{}console.log(` \u{1F4D0} Indexed ${E} code symbols (${_} files) \u2192 searchable via 'wyrm search'`)}!u&&y>0&&b("Review with: wyrm review")}finally{e.close()}}async function Nt(l){const c=await import("./vault.js"),[t,...i]=l;try{switch(t){case"set":{const r=i[0];if(!r){m("usage: wyrm vault set <name> (the secret is read from STDIN, never argv)"),process.exitCode=1;return}if(process.stdin.isTTY){m(`pipe the secret in, e.g.: printf %s "$TOKEN" | wyrm vault set ${r}`),process.exitCode=1;return}const s=(await import("node:fs")).readFileSync(0,"utf8").replace(/\r?\n$/,"");if(!s){m("empty secret on stdin"),process.exitCode=1;return}c.vaultSet(r,s),b(`Stored "${r}" (AES-256-GCM). Use it without exposing it: wyrm vault exec ${r} -- <command>`);break}case"get":{const r=i[0];if(!r){m("usage: wyrm vault get <name>"),process.exitCode=1;return}const e=c.vaultGet(r);if(e===void 0){m(`no secret named "${r}"`),process.exitCode=1;return}process.stdout.write(e);break}case"list":case"ls":{const r=c.vaultList();if(!r.length){console.log("(vault is empty)");break}k(`Vault \u2014 ${r.length} secret(s)`);for(const e of r)console.log(" \u2022 "+e);break}case"rm":case"remove":case"delete":{const r=i[0];if(!r){m("usage: wyrm vault rm <name>"),process.exitCode=1;return}b(c.vaultRemove(r)?`Removed "${r}"`:`(no secret named "${r}")`);break}case"exec":{const r=i[0],e=i.indexOf("--");if(!r||e===-1||e+1>=i.length){m("usage: wyrm vault exec <name> [--as ENVVAR] -- <command...>"),process.exitCode=1;return}const s=i.slice(1,e),o=s.indexOf("--as"),n=o>=0?s[o+1]:r.toUpperCase().replace(/[^A-Z0-9]+/g,"_"),p=i.slice(e+1),a=c.vaultGet(r);if(a===void 0){m(`no secret named "${r}"`),process.exitCode=1;return}const u=z(p[0],p.slice(1),{stdio:"inherit",env:{...process.env,[n]:a}});process.exitCode=u.status??1;break}case"import-npm":{const r=await import("node:fs"),e=await import("node:os"),o=(await import("node:path")).join(e.homedir(),".npmrc");if(!r.existsSync(o)){m("~/.npmrc not found"),process.exitCode=1;return}const n=r.readFileSync(o,"utf8").match(/\/\/registry\.npmjs\.org\/:_authToken=(.+)/);if(!n){m("no npm authToken found in ~/.npmrc"),process.exitCode=1;return}c.vaultSet("npm-token",n[1].trim()),b('Imported npm token \u2192 vault as "npm-token". You can now scrub the plaintext from ~/.npmrc and use: wyrm vault exec npm-token --as NODE_AUTH_TOKEN -- npm publish');break}case"setup":{const r=c.vaultPaths();if(r.secure)b(`Vault is already secure (backend: ${r.backend}, ${r.count} secret(s)).`);else{const e=r.keychainAvailable?"keychain":"passphrase";if(e==="passphrase"&&!process.env.WYRM_VAULT_PASSPHRASE){m("No OS keychain on this host. Set WYRM_VAULT_PASSPHRASE, then re-run: wyrm vault setup"),process.exitCode=1;return}const s=c.vaultSecure({backend:e});b(`Vault secured: ${s.from} \u2192 ${s.to}${s.rotated?" (key rotated)":""}.`),s.keyfileShredded&&console.log(" plaintext vault.key: shredded \u2714"),s.backup&&console.log(` ciphertext backup: ${s.backup} (delete once confirmed)`)}k("Store & use credentials safely"),console.log(' store: printf %s "$TOKEN" | wyrm vault set <name> # reads STDIN \u2014 never argv/shell history'),console.log(" use: wyrm vault exec <name> --as ENV_VAR -- <cmd> # injected as env var, never printed"),console.log(" list: wyrm vault list inspect: wyrm vault info");break}case"secure":{const r=i.indexOf("--backend"),e=r>=0?i[r+1]:"keychain";if(e!=="keychain"&&e!=="passphrase"){m("usage: wyrm vault secure [--backend keychain|passphrase] [--no-rotate]"),process.exitCode=1;return}if(e==="passphrase"&&!process.env.WYRM_VAULT_PASSPHRASE){m("set WYRM_VAULT_PASSPHRASE before: wyrm vault secure --backend passphrase"),process.exitCode=1;return}const s=!i.includes("--no-rotate"),o=c.vaultSecure({backend:e,rotate:s});b(`Vault secured: ${o.from} \u2192 ${o.to}${o.rotated?" (key rotated)":""}.`),console.log(` secrets re-encrypted: ${o.secrets}`),o.keyfileShredded&&console.log(" plaintext vault.key: shredded \u2714"),o.backup&&console.log(` ciphertext backup: ${o.backup} (delete once you've confirmed)`),console.log(e==="keychain"?" master key now lives in the OS keychain \u2014 not on disk.":" master key now derived from WYRM_VAULT_PASSPHRASE \u2014 keep that set for future use.");break}case"info":{const r=c.vaultPaths();k("Vault"),console.log(` backend: ${r.backend}`),console.log(` secrets: ${r.count}`),console.log(` store: ${r.vault} (0600)`),console.log(` key: ${r.backend==="keyfile"?r.key+" (0600)":r.backend==="keychain"?"(OS keychain \u2014 no key on disk)":"(derived from WYRM_VAULT_PASSPHRASE \u2014 no key on disk)"}`),console.log(` secure: ${r.secure?"yes \u2014 key is not a plaintext file beside the ciphertext":"NO \u2014 key sits beside ciphertext"}`),r.secure||console.log(r.keychainAvailable?" \u26A0 run `wyrm vault secure` to move the key into the OS keychain.":" \u26A0 no OS keychain found \u2014 set WYRM_VAULT_PASSPHRASE and run `wyrm vault secure --backend passphrase`.");break}default:m("usage: wyrm vault <setup|set|get|list|rm|exec|import-npm|secure|info>"),process.exitCode=1}}catch(r){m(`vault: ${r.message}`),process.exitCode=1}}function Mt(){const l=re(ne(import.meta.url)),t=[F(l,"..","skills"),F(l,"..","..","skills")].find(r=>q(r));if(!t)return[];const i=[];for(const r of Me(t,{withFileTypes:!0})){if(!r.isDirectory())continue;const e=F(t,r.name,"SKILL.md");if(!q(e))continue;let s=r.name,o="";try{const n=V(e,"utf8"),p=n.match(/^name:\s*(.+)$/m);if(p&&(s=p[1].trim()),/^description:\s*[|>]/m.test(n))o=((n.split(/^description:.*$/m)[1]??"").split(`
189
+ `)}const[,,A,...v]=process.argv;function $e(l){const c=l.ref_table?`${l.ref_table}${l.ref_id?"#"+l.ref_id:""}`:"",t=new Date(l.created_at).toLocaleTimeString();return`#${l.cursor} ${l.kind.padEnd(15)} ${c.padEnd(16)} ${Be(l.actor)} ${t}`}async function Tt(l){const{positional:c,flags:t}=C(l),i=c[0]||"since",r=j();try{if(!r.liveMemoryEnabled()){m("Live Memory is disabled (set WYRM_LIVE_MEMORY=1)."),process.exitCode=1;return}const e=(typeof t.project=="string"?t.project:"")||process.cwd(),s=r.getProject(e)??r.getProjectByName(e);if(!s){m(`Project not found: ${e}`),process.exitCode=1;return}if(i==="publish"){const o=c[1]||(typeof t.kind=="string"?t.kind:"");if(!o){m("usage: wyrm events publish <kind> --project <p> [--actor A] [--ref-table T --ref-id ID]"),process.exitCode=1;return}r.publishEvent({projectId:s.id,kind:o,refTable:typeof t["ref-table"]=="string"?t["ref-table"]:void 0,refId:typeof t["ref-id"]=="string"?t["ref-id"]:void 0,actor:typeof t.actor=="string"?t.actor:void 0}),b(`Event published (${o}) to ${s.name}`);return}if(i==="since"){const o=I(t.cursor,0),n=I(t.limit,50),p=r.eventsSince(s.id,o,n);for(const a of p)console.log($e(a));k(`${p.length} event(s) for '${s.name}' since cursor ${o}`);return}m("usage: wyrm events <publish|since> ..."),process.exitCode=1}finally{r.close()}}async function Dt(l){const{flags:c}=C(l),t=j();if(!t.liveMemoryEnabled()){m("Live Memory is disabled (set WYRM_LIVE_MEMORY=1)."),t.close(),process.exitCode=1;return}const i=(typeof c.project=="string"?c.project:"")||process.cwd(),r=t.getProject(i)??x(t,i);if(!r){m(`Project not found: ${i}`),t.close(),process.exitCode=1;return}const e=Math.max(250,I(c.interval,1e3));let s=I(c.since,t.subscribeEvents(r.id,1).cursor);k(`Watching '${r.name}' (cursor ${s}, every ${e}ms) \u2014 Ctrl-C to stop`);const o=()=>{try{for(const a of t.eventsSince(r.id,s,200))s=a.cursor,console.log($e(a))}catch{}};o();const n=setInterval(o,e),p=()=>{clearInterval(n);try{t.close()}catch{}process.exit(0)};process.on("SIGINT",p),process.on("SIGTERM",p)}async function It(l){const{flags:c}=C(l),{embedAll:t,removeAll:i,statusAll:r}=await import("./priority-embed.js"),e={projectDir:typeof c.project=="string"?c.project:void 0,allClients:c.all===!0},s=o=>console.log(` ${String(o.result??o.status).padEnd(9)} [${o.scope}] ${o.file}`);if(c.status){k("Wyrm priority embedding \u2014 status"),r(e).forEach(s);return}if(c.remove){k("Wyrm priority embedding \u2014 removed"),i(e).forEach(s);return}k("Wyrm is now FIRST-PRIORITY memory"),t(e).forEach(s);try{const{installClaudeCodeHooks:o,installClaudeStatusline:n}=await import("./autoconfig.js");o()?b("Proactive hooks installed (SessionStart rehydrate + capture + tool-trace)."):console.log(" (Claude Code not detected \u2014 skipped hook install.)");const a=n();a&&b(`Buddy statusline: ${a.message}`)}catch{}b("Wyrm will now be read first, primed proactively, and shown in the TUI at all times.")}async function Pt(l){const{flags:c}=C(l),{harvestProjects:t}=await import("./harvest.js"),{MemoryArtifacts:i}=await import("./memory-artifacts.js"),{escapeLikePattern:r}=await import("./auto-capture.js"),e=j();try{const s=e.getDatabase(),o=new i(s),n={existsBySig:(w,$)=>!!s.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(w,"%"+r($)+"%"),addCandidate:(w,$)=>o.add(w,{kind:$.kind,problem:$.text,tags:[...$.tags,$.sig],confidence:$.confidence,needsReview:1,createdBy:"harvest"})},p=typeof c.project=="string"?c.project:void 0;let a;if(p){const w=e.getProject(p)??e.getProjectByName(p);if(!w){m(`Project not found: ${p}`),process.exitCode=1;return}a=[{id:w.id,name:w.name,path:w.path}]}else a=e.getAllProjects(500).map(w=>({id:w.id,name:w.name,path:w.path}));const u=c["dry-run"]===!0||c.dry===!0,g=c.code===!0||c["include-code"]===!0,{reports:f,totalAdded:y,totalSkipped:h}=t(n,a,{dryRun:u,gitLimit:I(c.limit,30),includeCode:g});k(`Harvest ${u?"(dry run) ":""}\u2014 ${y} candidate(s), ${h} already present (${a.length} project(s))`);for(const w of f.filter($=>$.added>0).sort(($,E)=>E.added-$.added).slice(0,25))console.log(` +${String(w.added).padStart(3)} (skip ${w.skipped}) ${w.project}`);if(g&&!u){const{SymbolGraph:w}=await import("./symbols.js"),$=new w(s);let E=0,_=0;for(const R of a)try{const S=$.indexProject(R.id,R.path);E+=S.symbols,_+=S.files}catch{}console.log(` \u{1F4D0} Indexed ${E} code symbols (${_} files) \u2192 searchable via 'wyrm search'`)}!u&&y>0&&b("Review with: wyrm review")}finally{e.close()}}async function Nt(l){const c=await import("./vault.js"),[t,...i]=l;try{switch(t){case"set":{const r=i[0];if(!r){m("usage: wyrm vault set <name> (the secret is read from STDIN, never argv)"),process.exitCode=1;return}if(process.stdin.isTTY){m(`pipe the secret in, e.g.: printf %s "$TOKEN" | wyrm vault set ${r}`),process.exitCode=1;return}const s=(await import("node:fs")).readFileSync(0,"utf8").replace(/\r?\n$/,"");if(!s){m("empty secret on stdin"),process.exitCode=1;return}c.vaultSet(r,s),b(`Stored "${r}" (AES-256-GCM). Use it without exposing it: wyrm vault exec ${r} -- <command>`);break}case"get":{const r=i[0];if(!r){m("usage: wyrm vault get <name>"),process.exitCode=1;return}const e=c.vaultGet(r);if(e===void 0){m(`no secret named "${r}"`),process.exitCode=1;return}process.stdout.write(e);break}case"list":case"ls":{const r=c.vaultList();if(!r.length){console.log("(vault is empty)");break}k(`Vault \u2014 ${r.length} secret(s)`);for(const e of r)console.log(" \u2022 "+e);break}case"rm":case"remove":case"delete":{const r=i[0];if(!r){m("usage: wyrm vault rm <name>"),process.exitCode=1;return}b(c.vaultRemove(r)?`Removed "${r}"`:`(no secret named "${r}")`);break}case"exec":{const r=i[0],e=i.indexOf("--");if(!r||e===-1||e+1>=i.length){m("usage: wyrm vault exec <name> [--as ENVVAR] -- <command...>"),process.exitCode=1;return}const s=i.slice(1,e),o=s.indexOf("--as"),n=o>=0?s[o+1]:r.toUpperCase().replace(/[^A-Z0-9]+/g,"_"),p=i.slice(e+1),a=c.vaultGet(r);if(a===void 0){m(`no secret named "${r}"`),process.exitCode=1;return}const u=z(p[0],p.slice(1),{stdio:"inherit",env:{...process.env,[n]:a}});process.exitCode=u.status??1;break}case"import-npm":{const r=await import("node:fs"),e=await import("node:os"),o=(await import("node:path")).join(e.homedir(),".npmrc");if(!r.existsSync(o)){m("~/.npmrc not found"),process.exitCode=1;return}const n=r.readFileSync(o,"utf8").match(/\/\/registry\.npmjs\.org\/:_authToken=(.+)/);if(!n){m("no npm authToken found in ~/.npmrc"),process.exitCode=1;return}c.vaultSet("npm-token",n[1].trim()),b('Imported npm token \u2192 vault as "npm-token". You can now scrub the plaintext from ~/.npmrc and use: wyrm vault exec npm-token --as NODE_AUTH_TOKEN -- npm publish');break}case"setup":{const r=c.vaultPaths();if(r.secure)b(`Vault is already secure (backend: ${r.backend}, ${r.count} secret(s)).`);else{const e=r.keychainAvailable?"keychain":"passphrase";if(e==="passphrase"&&!process.env.WYRM_VAULT_PASSPHRASE){m("No OS keychain on this host. Set WYRM_VAULT_PASSPHRASE, then re-run: wyrm vault setup"),process.exitCode=1;return}const s=c.vaultSecure({backend:e});b(`Vault secured: ${s.from} \u2192 ${s.to}${s.rotated?" (key rotated)":""}.`),s.keyfileShredded&&console.log(" plaintext vault.key: shredded \u2714"),s.backup&&console.log(` ciphertext backup: ${s.backup} (delete once confirmed)`)}k("Store & use credentials safely"),console.log(' store: printf %s "$TOKEN" | wyrm vault set <name> # reads STDIN \u2014 never argv/shell history'),console.log(" use: wyrm vault exec <name> --as ENV_VAR -- <cmd> # injected as env var, never printed"),console.log(" list: wyrm vault list inspect: wyrm vault info");break}case"secure":{const r=i.indexOf("--backend"),e=r>=0?i[r+1]:"keychain";if(e!=="keychain"&&e!=="passphrase"){m("usage: wyrm vault secure [--backend keychain|passphrase] [--no-rotate]"),process.exitCode=1;return}if(e==="passphrase"&&!process.env.WYRM_VAULT_PASSPHRASE){m("set WYRM_VAULT_PASSPHRASE before: wyrm vault secure --backend passphrase"),process.exitCode=1;return}const s=!i.includes("--no-rotate"),o=c.vaultSecure({backend:e,rotate:s});b(`Vault secured: ${o.from} \u2192 ${o.to}${o.rotated?" (key rotated)":""}.`),console.log(` secrets re-encrypted: ${o.secrets}`),o.keyfileShredded&&console.log(" plaintext vault.key: shredded \u2714"),o.backup&&console.log(` ciphertext backup: ${o.backup} (delete once you've confirmed)`),console.log(e==="keychain"?" master key now lives in the OS keychain \u2014 not on disk.":" master key now derived from WYRM_VAULT_PASSPHRASE \u2014 keep that set for future use.");break}case"info":{const r=c.vaultPaths();k("Vault"),console.log(` backend: ${r.backend}`),console.log(` secrets: ${r.count}`),console.log(` store: ${r.vault} (0600)`),console.log(` key: ${r.backend==="keyfile"?r.key+" (0600)":r.backend==="keychain"?"(OS keychain \u2014 no key on disk)":"(derived from WYRM_VAULT_PASSPHRASE \u2014 no key on disk)"}`),console.log(` secure: ${r.secure?"yes \u2014 key is not a plaintext file beside the ciphertext":"NO \u2014 key sits beside ciphertext"}`),r.secure||console.log(r.keychainAvailable?" \u26A0 run `wyrm vault secure` to move the key into the OS keychain.":" \u26A0 no OS keychain found \u2014 set WYRM_VAULT_PASSPHRASE and run `wyrm vault secure --backend passphrase`.");break}default:m("usage: wyrm vault <setup|set|get|list|rm|exec|import-npm|secure|info>"),process.exitCode=1}}catch(r){m(`vault: ${r.message}`),process.exitCode=1}}function Mt(){const l=re(ne(import.meta.url)),t=[F(l,"..","skills"),F(l,"..","..","skills")].find(r=>q(r));if(!t)return[];const i=[];for(const r of Me(t,{withFileTypes:!0})){if(!r.isDirectory())continue;const e=F(t,r.name,"SKILL.md");if(!q(e))continue;let s=r.name,o="";try{const n=V(e,"utf8"),p=n.match(/^name:\s*(.+)$/m);if(p&&(s=p[1].trim()),/^description:\s*[|>]/m.test(n))o=((n.split(/^description:.*$/m)[1]??"").split(`
190
190
  `).find(g=>g.trim())??"").trim();else{const a=n.match(/^description:\s*(.+)$/m);a&&(o=a[1].trim())}}catch{}i.push({name:s,description:o})}return i.sort((r,e)=>r.name.localeCompare(e.name))}async function Ot(l){const c=l[0];if(!c||c==="help"||c==="--help"){console.log(`Usage:
191
191
  wyrm skill list show the bundled guides + your registered skills
192
192
  wyrm skill backfill-content read every skill's SKILL.md into the registry (idempotent)
@@ -197,4 +197,4 @@ private = never replicates . cloud = your own cloud backup . team = federate
197
197
  wyrm grove status
198
198
  wyrm grove policy <project|id> <private|cloud|team>`),t.close(),process.exit(1)}async function Lt(l){const c=l[0],{flags:t}=C(l.slice(1)),{ulid:i}=await import("./ulid.js"),{createRun:r,setRunStatus:e,registerAgent:s,getRun:o,getAgents:n}=await import("./handlers/run.js"),{sanitizeActorId:p}=await import("./handlers/boundary.js"),a=f=>typeof f=="string"?p(f):null,u=j(),g=u.getDatabase();try{switch(c){case"start":{const f=(typeof t.orchestrator=="string"?t.orchestrator:"cli").slice(0,200),y=a(t.agent)??p(f)??"cli",h=a(t.parent);if(h&&!o(g,h)){m(`Parent run not found: ${h}`),process.exitCode=1;return}const w=i();r(g,w,h,f),s(g,w,y,"orchestrator"),process.stdout.write(w+`
199
199
  `),process.stderr.write(`Run ${w} started by ${f}.
200
- `);break}case"join":{const f=a(t.run),y=a(t.agent);if(!f||!y){m("Usage: wyrm run join --run <RUNID> --agent <ID> [--role <ROLE>]"),process.exitCode=1;return}if(!o(g,f)){m(`Run not found: ${f}`),process.exitCode=1;return}const h=typeof t.role=="string"?t.role.slice(0,200):null;s(g,f,y,h),b(`${y} joined run ${f}${h?` as ${h}`:""}.`);break}case"end":{const f=a(t.run);if(!f){m("Usage: wyrm run end --run <RUNID> [--status completed|failed|abandoned]"),process.exitCode=1;return}if(!o(g,f)){m(`Run not found: ${f}`),process.exitCode=1;return}const y=typeof t.status=="string"?t.status:"completed",w=["completed","failed","abandoned"].includes(y)?y:"completed";e(g,f,w),b(`Run ${f} ended (${w}).`);break}case"status":{const f=a(t.run);if(!f){m("Usage: wyrm run status --run <RUNID>"),process.exitCode=1;return}const y=o(g,f);if(!y){m(`Run not found: ${f}`),process.exitCode=1;return}const h=n(g,f);k(`Run ${y.run_id} [${y.status}]`),y.orchestrator&&console.log(d.bold("Orchestrator: ")+y.orchestrator),y.parent_run_id&&console.log(d.bold("Parent: ")+y.parent_run_id),console.log(d.bold("Created: ")+y.created_at),console.log(d.bold("Updated: ")+y.updated_at),h.length>0?console.log(D(["Agent","Role","Joined"],h.map(w=>[w.agent_id,w.role??"",w.joined_at]))):console.log(d.dim(" No agents registered."));break}default:m("Usage: wyrm run <start|join|end|status> [options]"),console.log(" wyrm run start [--orchestrator NAME] [--agent ID] [--parent RUNID]"),console.log(" wyrm run join --run RUNID --agent ID [--role ROLE]"),console.log(" wyrm run end --run RUNID [--status completed|failed|abandoned]"),console.log(" wyrm run status --run RUNID"),process.exitCode=1;return}}finally{u.close()}}if(O==="--version"||O==="-v"||O==="version"){const l=M();console.log(`${l.name??"wyrm-mcp"} v${l.version??"unknown"}`)}else!O||O==="--help"||O==="-h"||O==="help"?be():(async()=>{try{switch(O){case"search":await Ge(v);break;case"ls":await Je(v);break;case"show":await ze(v);break;case"capture":await Qe(v);break;case"rehydrate":await Ze(v);break;case"session":await st(v);break;case"presence":await rt(v);break;case"digest":await Xe(v);break;case"bridge":await ot(v);break;case"metabolize":await et(v);break;case"entities":await tt(v);break;case"render":await nt(v);break;case"reverse-bridge":await it(v);break;case"import":await at(v);break;case"stats":await lt(v);break;case"review":await dt(v);break;case"sync":await pt(v);break;case"cloud":{const{cmdCloud:l}=await import("./cloud/cli.js");await l(v);break}case"grove":await At(v);break;case"run":await Lt(v);break;case"skill":await Ot(v);break;case"prune":await ut(v);break;case"license":await ft();break;case"login":await gt();break;case"activate":await yt(v);break;case"maintenance":await ht(v);break;case"doctor":await wt(v);break;case"failure":await bt(v);break;case"project":await $t(v);break;case"index":await vt(v);break;case"update":await St(v);break;case"prompt":await _t(v);break;case"hours":await jt(v);break;case"invoice":await Ct(v);break;case"agent":await Et(v);break;case"feedback":await xt(v);break;case"setup":await Rt(v);break;case"intro":{const{renderIntro:l}=await import("./visibility.js");console.log(l(M().version??"unknown"));break}case"events":await Tt(v);break;case"watch":await Dt(v);break;case"embed":await It(v);break;case"harvest":await Pt(v);break;case"vault":await Nt(v);break;case"connector":case"connectors":await mt(v);break;case"statusline":{const{installClaudeStatusline:l,removeClaudeStatusline:c}=await import("./autoconfig.js"),t=v.includes("--remove")?c():l();t?b(t.message):m("Claude Code not detected (~/.claude missing).");break}case"guard":{const{installWyrmGuardHooks:l,removeWyrmGuardHooks:c,wyrmGuardHookStatus:t}=await import("./autoconfig.js");if(v.includes("--status")){const r=t();if(console.log(` settings: ${r.settingsPath}`),r.installed){b(`wyrm-guard hooks installed (${r.commands.length} entr${r.commands.length===1?"y":"ies"})`);for(const e of r.commands)console.log(d.dim(` ${e}`))}else console.log(d.yellow(" wyrm-guard hooks not installed \u2014 run: wyrm guard"));break}const i=v.includes("--remove")||v.includes("--uninstall")?c():l();i?i.action==="failed"?(m(i.message),process.exitCode=1):b(i.message):m("Claude Code not detected (~/.claude missing).");break}case"ui":case"dashboard":v.includes("--ui")||v.push("--ui");case"serve":{const l=v.includes("--ui");if(l){const{enableDevMode:r}=await import("./http-auth.js");r()}const{server:c}=await import("./http-fast.js"),t=parseInt(process.env.WYRM_PORT??process.env.PORT??"3333",10),i=process.env.WYRM_BIND_HOST||"127.0.0.1";c.listen(t,i,()=>{if(b(`Wyrm HTTP server running on ${i}:${t}`),process.env.WYRM_UI_READONLY==="1"&&console.log("\u{1F512} READ-ONLY mode: writes + off-box egress are blocked; safe to expose."),l){const r=`http://localhost:${t}/ui`;console.log(`\u{1F5A5}\uFE0F Dashboard: ${r}`),import("child_process").then(({spawn:e})=>{const s=process.platform;try{const o=s==="darwin"?e("open",[r],{stdio:"ignore",detached:!0}):s==="win32"?e("cmd",["/c","start","",r],{stdio:"ignore",detached:!0}):e("xdg-open",[r],{stdio:"ignore",detached:!0});o.on("error",()=>{}),o.unref()}catch{}}).catch(()=>{})}});break}default:m(`Unknown command: ${O}`),be(),process.exit(1)}}catch(l){m(String(l)),process.exit(1)}})();
200
+ `);break}case"join":{const f=a(t.run),y=a(t.agent);if(!f||!y){m("Usage: wyrm run join --run <RUNID> --agent <ID> [--role <ROLE>]"),process.exitCode=1;return}if(!o(g,f)){m(`Run not found: ${f}`),process.exitCode=1;return}const h=typeof t.role=="string"?t.role.slice(0,200):null;s(g,f,y,h),b(`${y} joined run ${f}${h?` as ${h}`:""}.`);break}case"end":{const f=a(t.run);if(!f){m("Usage: wyrm run end --run <RUNID> [--status completed|failed|abandoned]"),process.exitCode=1;return}if(!o(g,f)){m(`Run not found: ${f}`),process.exitCode=1;return}const y=typeof t.status=="string"?t.status:"completed",w=["completed","failed","abandoned"].includes(y)?y:"completed";e(g,f,w),b(`Run ${f} ended (${w}).`);break}case"status":{const f=a(t.run);if(!f){m("Usage: wyrm run status --run <RUNID>"),process.exitCode=1;return}const y=o(g,f);if(!y){m(`Run not found: ${f}`),process.exitCode=1;return}const h=n(g,f);k(`Run ${y.run_id} [${y.status}]`),y.orchestrator&&console.log(d.bold("Orchestrator: ")+y.orchestrator),y.parent_run_id&&console.log(d.bold("Parent: ")+y.parent_run_id),console.log(d.bold("Created: ")+y.created_at),console.log(d.bold("Updated: ")+y.updated_at),h.length>0?console.log(D(["Agent","Role","Joined"],h.map(w=>[w.agent_id,w.role??"",w.joined_at]))):console.log(d.dim(" No agents registered."));break}default:m("Usage: wyrm run <start|join|end|status> [options]"),console.log(" wyrm run start [--orchestrator NAME] [--agent ID] [--parent RUNID]"),console.log(" wyrm run join --run RUNID --agent ID [--role ROLE]"),console.log(" wyrm run end --run RUNID [--status completed|failed|abandoned]"),console.log(" wyrm run status --run RUNID"),process.exitCode=1;return}}finally{u.close()}}if(A==="--version"||A==="-v"||A==="version"){const l=M();console.log(`${l.name??"wyrm-mcp"} v${l.version??"unknown"}`)}else!A||A==="--help"||A==="-h"||A==="help"?be():(async()=>{try{switch(A){case"search":await Ge(v);break;case"ls":await Je(v);break;case"show":await ze(v);break;case"capture":await Qe(v);break;case"rehydrate":await Ze(v);break;case"session":await st(v);break;case"presence":await rt(v);break;case"digest":await Xe(v);break;case"bridge":await ot(v);break;case"metabolize":await et(v);break;case"entities":await tt(v);break;case"render":await nt(v);break;case"reverse-bridge":await it(v);break;case"import":await at(v);break;case"stats":await lt(v);break;case"review":await dt(v);break;case"sync":await pt(v);break;case"cloud":{const{cmdCloud:l}=await import("./cloud/cli.js");await l(v);break}case"grove":await At(v);break;case"run":await Lt(v);break;case"skill":await Ot(v);break;case"prune":await ut(v);break;case"license":await ft();break;case"login":await gt();break;case"activate":await yt(v);break;case"maintenance":await ht(v);break;case"doctor":await wt(v);break;case"failure":await bt(v);break;case"project":await $t(v);break;case"index":await vt(v);break;case"update":await St(v);break;case"prompt":await _t(v);break;case"hours":await jt(v);break;case"invoice":await Ct(v);break;case"agent":await Et(v);break;case"feedback":await xt(v);break;case"setup":await Rt(v);break;case"intro":{const{renderIntro:l}=await import("./visibility.js");console.log(l(M().version??"unknown"));break}case"events":await Tt(v);break;case"watch":await Dt(v);break;case"embed":await It(v);break;case"harvest":await Pt(v);break;case"vault":await Nt(v);break;case"connector":case"connectors":await mt(v);break;case"statusline":{const{installClaudeStatusline:l,removeClaudeStatusline:c}=await import("./autoconfig.js"),t=v.includes("--remove")?c():l();t?b(t.message):m("Claude Code not detected (~/.claude missing).");break}case"guard":{const{installWyrmGuardHooks:l,removeWyrmGuardHooks:c,wyrmGuardHookStatus:t}=await import("./autoconfig.js");if(v.includes("--status")){const r=t();if(console.log(` settings: ${r.settingsPath}`),r.installed){b(`wyrm-guard hooks installed (${r.commands.length} entr${r.commands.length===1?"y":"ies"})`);for(const e of r.commands)console.log(d.dim(` ${e}`))}else console.log(d.yellow(" wyrm-guard hooks not installed \u2014 run: wyrm guard"));break}const i=v.includes("--remove")||v.includes("--uninstall")?c():l();i?i.action==="failed"?(m(i.message),process.exitCode=1):b(i.message):m("Claude Code not detected (~/.claude missing).");break}case"ui":case"dashboard":v.includes("--ui")||v.push("--ui");case"serve":{const l=v.includes("--ui");if(l){const{enableDevMode:r}=await import("./http-auth.js");r()}const{server:c}=await import("./http-fast.js"),t=parseInt(process.env.WYRM_PORT??process.env.PORT??"3333",10),i=process.env.WYRM_BIND_HOST||"127.0.0.1";c.listen(t,i,()=>{if(b(`Wyrm HTTP server running on ${i}:${t}`),process.env.WYRM_UI_READONLY==="1"&&console.log("\u{1F512} READ-ONLY mode: writes + off-box egress are blocked; safe to expose."),l){const r=`http://localhost:${t}/ui`;console.log(`\u{1F5A5}\uFE0F Dashboard: ${r}`),import("child_process").then(({spawn:e})=>{const s=process.platform;try{const o=s==="darwin"?e("open",[r],{stdio:"ignore",detached:!0}):s==="win32"?e("cmd",["/c","start","",r],{stdio:"ignore",detached:!0}):e("xdg-open",[r],{stdio:"ignore",detached:!0});o.on("error",()=>{}),o.unref()}catch{}}).catch(()=>{})}});break}default:m(`Unknown command: ${A}`),be(),process.exit(1)}}catch(l){m(String(l)),process.exit(1)}})();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "8.3.0",
3
+ "version": "8.3.1",
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.3.0",
3
+ "version": "8.3.1",
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",
@@ -33,12 +33,27 @@ function extractText(content) {
33
33
 
34
34
  let payload = {};
35
35
  try { payload = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { /* no/invalid stdin */ }
36
+ // JSON.parse('null') etc. returns a non-object without throwing — normalize or
37
+ // payload.cwd throws OUTSIDE any try and breaks "always exit 0" (crucible #6).
38
+ if (!payload || typeof payload !== 'object') payload = {};
36
39
 
37
- const cwd = (payload.cwd || process.cwd()).replace(/\/+$/, '');
40
+ const cwd = (typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd()).replace(/\/+$/, '');
38
41
  const project = basename(cwd) || 'root';
39
42
  const event = payload.hook_event_name || 'SessionEnd';
40
43
  const tpath = payload.transcript_path;
41
44
 
45
+ // Clean exit: release this session's presence row (announced by the
46
+ // SessionStart rehydrate hook). BEFORE the transcript parse and its early
47
+ // exit — a session with no readable transcript must still leave the board.
48
+ // Only on SessionEnd: PreCompact runs this script too, mid-session. If the
49
+ // harness dies without a SessionEnd, pid-liveness marks the row DEAD anyway.
50
+ try {
51
+ if (event === 'SessionEnd' && payload.session_id) {
52
+ spawnSync('wyrm', ['presence', 'release', '--agent', `claude-${String(payload.session_id).slice(0, 8)}`],
53
+ { timeout: 10000, stdio: 'ignore' });
54
+ }
55
+ } catch { /* never break the session */ }
56
+
42
57
  const userMsgs = [];
43
58
  let lastAssistant = '';
44
59
  if (tpath && existsSync(tpath)) {
@@ -48,13 +63,21 @@ if (tpath && existsSync(tpath)) {
48
63
  if (!t) continue;
49
64
  let obj;
50
65
  try { obj = JSON.parse(t); } catch { continue; }
66
+ // Skip harness-injected lines by FLAG, not just prefix: compact
67
+ // continuations and meta rows arrive as user-role text and otherwise
68
+ // become the session objective (crucible #7).
69
+ if (obj.isMeta || obj.isCompactSummary) continue;
51
70
  const msg = obj.message || {};
52
71
  const role = msg.role || obj.role;
53
72
  const text = extractText(msg.content);
54
73
  if (!text) continue;
55
74
  const s = text.trim();
56
- // Keep genuine user prose; skip tool-result / system-reminder blocks.
57
- if (role === 'user' && s && !s.startsWith('<') && !s.startsWith('[{')) userMsgs.push(s);
75
+ // Keep genuine user prose; skip tool-result / system-reminder blocks,
76
+ // Skill payloads (user-role skill-file dumps), and compact-continuation
77
+ // boilerplate (all would fill the digest's request slots with noise).
78
+ if (role === 'user' && s && !s.startsWith('<') && !s.startsWith('[{')
79
+ && !s.startsWith('Base directory for this skill')
80
+ && !s.startsWith('This session is being continued from a previous conversation')) userMsgs.push(s);
58
81
  else if (role === 'assistant') lastAssistant = s;
59
82
  }
60
83
  } catch { /* unreadable transcript — capture what we have */ }
@@ -62,7 +85,11 @@ if (tpath && existsSync(tpath)) {
62
85
 
63
86
  if (!userMsgs.length && !lastAssistant) process.exit(0);
64
87
 
65
- const objective = userMsgs[0] ? userMsgs[0].slice(0, 300) : '';
88
+ // Strip leading dashes: the objective is passed as a `--objectives <value>`
89
+ // argv, and wyrm-cli parseArgs refuses a flag value that itself starts with
90
+ // '--' (it becomes a junk boolean flag and the objective silently vanishes —
91
+ // crucible #15). A user prompt like "--help me refactor" must still be stored.
92
+ const objective = userMsgs[0] ? userMsgs[0].replace(/^-+\s*/, '').slice(0, 300) : '';
66
93
  const others = userMsgs.slice(1, 6).map((m) => m.slice(0, 120)).join(' | ');
67
94
  const tail = lastAssistant.replace(/\n/g, ' ').slice(0, 400);
68
95
 
@@ -84,4 +111,17 @@ try {
84
111
  });
85
112
  } catch { /* wyrm not on PATH or capture failed — never break the session */ }
86
113
 
114
+ // Also upsert the project's SESSION RECORD (wyrm session log, v7.8.0) — the
115
+ // write side of the SessionStart rehydrate hook. Keyed by the harness
116
+ // session id so PreCompact and SessionEnd revise the SAME record instead of
117
+ // stacking rows. Without this, `wyrm rehydrate` replays the last manually
118
+ // recorded session forever (the 2026-07-15 stale-brief diagnosis).
119
+ try {
120
+ const sessionArgs = ['session', 'log', '--path', cwd];
121
+ if (payload.session_id) sessionArgs.push('--run', String(payload.session_id).slice(0, 64));
122
+ if (objective) sessionArgs.push('--objectives', objective);
123
+ sessionArgs.push('--completed', content);
124
+ spawnSync('wyrm', sessionArgs, { timeout: 25000, stdio: 'ignore' });
125
+ } catch { /* older wyrm without `session log`, or write failed — never break the session */ }
126
+
87
127
  process.exit(0);
@@ -21,8 +21,30 @@ import { spawnSync } from 'node:child_process';
21
21
 
22
22
  let payload = {};
23
23
  try { payload = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { /* no/invalid stdin */ }
24
+ // JSON.parse('null'|'true'|'42') returns a non-object without throwing, so the
25
+ // catch above does not cover it — normalize or `payload.cwd` throws OUTSIDE any
26
+ // try and breaks the "always exit 0" contract (crucible #6).
27
+ if (!payload || typeof payload !== 'object') payload = {};
24
28
 
25
- const cwd = (payload.cwd || process.cwd()).replace(/\/+$/, '');
29
+ const cwd = (typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd()).replace(/\/+$/, '');
30
+
31
+ // Announce this session on the presence board (wyrm presence, v7.8.0) so
32
+ // concurrent agents (other Claude chats, Hermes, tallow's fleet) can see each
33
+ // other. pid = process.ppid (the harness process that spawned this hook), so
34
+ // liveness is process-identity — alive however long the chat idles, dead the
35
+ // moment the harness exits. Best-effort: presence must never delay rehydrate.
36
+ try {
37
+ if (payload.session_id) {
38
+ const agentId = `claude-${String(payload.session_id).slice(0, 8)}`;
39
+ spawnSync('wyrm', [
40
+ 'presence', 'announce',
41
+ '--agent', agentId,
42
+ '--kind', 'claude',
43
+ '--path', cwd,
44
+ '--pid', String(process.ppid),
45
+ ], { timeout: 10000, stdio: 'ignore' });
46
+ }
47
+ } catch { /* older wyrm without `presence`, or announce failed — never break the session */ }
26
48
 
27
49
  let brief = '';
28
50
  try {
@@ -37,6 +59,37 @@ try {
37
59
 
38
60
  if (!brief) process.exit(0); // nothing to restore: stay silent
39
61
 
62
+ // The brief above is scoped to THIS directory's project, but sessions are
63
+ // logged under whatever project their cwd mapped to at SessionEnd — so work
64
+ // done from other directories never reaches this project's brief and a
65
+ // cockpit dir like /home/kami rehydrates days-stale context. Append a compact
66
+ // machine-wide recency section whenever newer sessions exist in OTHER
67
+ // projects, so a fresh session at least knows they happened.
68
+ try {
69
+ const m = brief.match(/Session #(\d+)/);
70
+ // Number.isSafeInteger guard: only a clean integer ever reaches the SQL (it
71
+ // is interpolated, not bound — a NaN/exponent would corrupt the query).
72
+ const parsed = m ? Number(m[1]) : 0;
73
+ const briefId = Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
74
+ // Resolve the DB the SAME way the `wyrm` CLI does (WYRM_DB_PATH override, else
75
+ // ~/.wyrm/wyrm.db) so this recency query reads the same store that produced
76
+ // the brief (crucible #16). Skip the whole block when we have no briefId.
77
+ const dbPath = process.env.WYRM_DB_PATH || `${process.env.HOME}/.wyrm/wyrm.db`;
78
+ const q = briefId ? spawnSync('sqlite3', [
79
+ '-separator', '\t',
80
+ dbPath,
81
+ `SELECT s.id, p.name, s.date, replace(substr(coalesce(s.objectives,''),1,110), char(10), ' ')
82
+ FROM sessions s JOIN projects p ON p.id = s.project_id
83
+ WHERE s.is_archived = 0 AND s.id > ${briefId}
84
+ ORDER BY s.id DESC LIMIT 4`,
85
+ ], { timeout: 10000, encoding: 'utf8' }) : { stdout: '' };
86
+ const rows = (q.stdout || '').trim().split('\n').filter(Boolean).map((l) => l.split('\t'));
87
+ if (briefId && rows.length) {
88
+ const lines = rows.map(([id, name, date, obj]) => `- #${id} · ${name} · ${date} — ${obj}`);
89
+ brief += `\n\n## Newer sessions since this brief (other projects, machine-wide)\n${lines.join('\n')}\n_This project's own brief is #${briefId}; the sessions above are more recent. Query \`wyrm show session:<id>\` if one looks relevant._`;
90
+ }
91
+ } catch { /* sqlite3 missing or query failed — the plain brief is still good */ }
92
+
40
93
  if (process.env.WYRM_REHYDRATE_DRYRUN === '1') {
41
94
  process.stdout.write(brief + '\n');
42
95
  process.exit(0);