wyrm-mcp 8.0.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,16 +68,24 @@ Most memory tools store successes. Wyrm also records dead-ends and blocks the re
68
68
 
69
69
  ### Recall that finds things by meaning, not just keywords
70
70
 
71
- `wyrm_recall` runs keyword search (FTS5) and semantic search over a vector index, fuses them, and reranks. It is hybrid by default, no configuration required. For higher accuracy you can opt into NVIDIA NIM retrieval (below).
71
+ `wyrm_recall` runs keyword search (FTS5) and semantic search over a vector index, fuses them, and reranks. It is hybrid by default, no configuration required. It also weighs recency, temporal cues, and confirmed reuse, so the memory that fits the moment ranks first. On a real-set LoCoMo benchmark committed in the repo, the deterministic no-LLM floor reaches recall@10 59.9%, and local hybrid recall lifts it to 72.6%. For higher accuracy you can opt into NVIDIA NIM retrieval (below).
72
72
 
73
73
  ### Local-first, and honest about egress
74
74
 
75
75
  By default nothing leaves your machine. The database is a single SQLite file at `~/.wyrm/wyrm.db`. When you do opt into a hosted embedding path, Wyrm reports exactly what left and where, in a determinism receipt and on its health endpoint. The privacy claim is one you can verify from the runtime, not just the docs.
76
76
 
77
+ ### Every write leaves a receipt
78
+
79
+ Every memory write returns a structured receipt that says what happened to it: stored, queued for review, merged, aliased, or dropped, and why. The receipts are ledgered too, so `wyrm digest --writes` reconstructs a day's writes offline and `wyrm_stats` shows the outcomes and the review-queue depth. You can audit what the agent actually committed to memory, not just trust that it did.
80
+
77
81
  ### Built for one agent or a fleet
78
82
 
79
83
  Every memory is attributed to the agent and run that produced it, so a swarm of agents can share one accountable memory bus, with failures kept private to your account by default. A live event stream keeps devices in sync.
80
84
 
85
+ ### Untrusted input stays out of the context brief
86
+
87
+ Every artifact is tagged with where it came from: you, an agent, an import, or an untrusted source. Content on the untrusted lane is categorically withheld from the context briefs the model reads, whatever it contains, and imported content is detector-gated and marked. The brief-surface quarantine was hardened against a full garak red-team of 622 real jailbreak payloads: zero escapes from the untrusted lane by construction, and the detector flagged 80.7% of the rest with zero false positives on benign prose. That red-team runs as a CI gate.
88
+
81
89
  ## NVIDIA NIM retrieval (optional)
82
90
 
83
91
  Wyrm can use NVIDIA NIM for embeddings and reranking when accuracy is worth a hosted call. On a retrieval benchmark committed in the repo, recall@1 moved from 33% on the local baseline to 47% with NIM embeddings and 52% with NIM reranking added. It is an explicit opt-in, off by default, and the egress is disclosed on every call.
@@ -0,0 +1 @@
1
+ const d=new Set(["the","a","an","this","that","these","those","it","we","i","you","they","and","or","but","if","then","else","when","while","for","to","of","in","on","at","by","with","from","as","is","was","are","were","be","been","so","do","did","done","use","used","using","add","added","fix","fixed","run","ran","set","get","got","now","not","no","yes","per","via","see","note","todo","done","new","old","all","any","each","every","here","there","why","how","what","which","who","lesson","pattern","session","auto"]),h=e=>d.has(e.toLowerCase());function p(e){if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(e)||/[0-9a-f]{10,}/i.test(e)&&(e.match(/[0-9a-f]/gi)?.length??0)/e.length>.7||(e.match(/[a-z]/gi)?.length??0)<3)return!0;if(!/[._\-/]/.test(e)){const s=(e.match(/[aeiou]/gi)?.length??0)/e.length;if(e.length>12&&s<.3||/\d/.test(e)&&e.length>10)return!0}return!1}function c(e){return e.length<3||e.length>40||p(e)?!1:/^[a-z][a-z0-9]*[A-Z]/.test(e)||/^[A-Z][a-z0-9]+[A-Z]/.test(e)?!0:/^[a-z0-9]+([._-][a-z0-9]+)+$/i.test(e)&&/[._-]/.test(e)?/[._]/.test(e)||e.split("-").length>=3:!1}function w(e,l=24){if(!e)return[];const s=new Map,r=(n,t)=>{const a=n.trim().replace(/[.,;:!?)('"]+$/,"").replace(/^["'(]+/,"").trim();if(a.length<3||a.length>60)return;const o=a.toLowerCase();h(o)||s.has(o)||s.set(o,{name:a,type:t})},f=e.match(/`([^`]{2,60})`/g)??[];for(const n of f){const t=n.slice(1,-1).trim();(c(t)||/^[a-z_]+\(/i.test(t))&&r(t.replace(/\(.*$/,""),"tech")}const i=e.replace(/```[\s\S]*?```/g," ").replace(/`[^`]*`/g," "),u=i.match(/\b([A-Z][a-z0-9]+(?:[ ][A-Z][a-z0-9]+){1,3})\b/g)??[];for(let n of u){let t=n.split(/\s+/);for(;t.length&&h(t[0]);)t=t.slice(1);for(;t.length&&h(t[t.length-1]);)t=t.slice(0,-1);t.length>=2&&r(t.join(" "),"concept")}for(const n of i.match(/[A-Za-z_][A-Za-z0-9._-]{2,39}/g)??[])c(n)&&r(n,"tech");const g=/\b(Wyrm|Cloudflare|SQLite|Ollama|NVIDIA|NIM|Ed25519|LoCoMo|Paddle|Stripe|Vectorize|Workers|D1|R2|KV|Astro|Next\.?js|Postgres|Redis|Baileys|WhatsApp|Anthropic|Claude|garak|MCP)\b/gi;for(const n of i.match(g)??[])r(n,"tech");return[...s.values()].slice(0,l)}export{w as extractEntityCandidates};
@@ -0,0 +1,4 @@
1
+ import{extractEntityCandidates as y}from"./entity-extract.js";const R=6;function N(t,i,_,a){const s=y(_);if(s.length===0)return{created:0,linked:0,entities:0};const d=t.prepare("SELECT id FROM entities WHERE project_id = ? AND LOWER(name) = LOWER(?) LIMIT 1"),l=t.prepare("INSERT INTO entities (project_id, name, type, created_by) VALUES (?, ?, ?, 'auto-extract')"),h=t.prepare("SELECT id, weight FROM relationships WHERE project_id = ? AND source_entity_id = ? AND target_entity_id = ? AND relationship_type = 'co_occurs' LIMIT 1"),u=t.prepare("UPDATE relationships SET weight = MIN(1.0, weight + 0.1) WHERE id = ?"),p=t.prepare("INSERT INTO relationships (project_id, source_entity_id, target_entity_id, relationship_type, weight, confidence, source_memory, extraction_method, created_by) VALUES (?, ?, ?, 'co_occurs', 0.3, 1.0, ?, 'auto-extract', 'auto-extract')");let r=0;const E=[];for(const e of s)try{const n=d.get(i,e.name);if(n){E.push(n.id);continue}const c=l.run(i,e.name,e.type);E.push(c.lastInsertRowid),r++}catch{}let g=0;const o=E.slice(0,R);for(let e=0;e<o.length;e++)for(let n=e+1;n<o.length;n++){const c=Math.min(o[e],o[n]),f=Math.max(o[e],o[n]);if(c!==f)try{const m=h.get(i,c,f);m?u.run(m.id):(p.run(i,c,f,a??null),g++)}catch{}}return{created:r,linked:g,entities:E.length}}function T(t,i,_={}){const a=t.prepare(`SELECT problem, validated_fix FROM memory_artifacts
2
+ WHERE project_id = ? AND needs_review = 0 AND supersedes_id IS NULL
3
+ ORDER BY id DESC LIMIT ?`).all(i,_.limit??5e3);let s=0,d=0,l=0;return t.transaction(u=>{for(const p of u){const r=N(t,i,`${p.problem}
4
+ ${p.validated_fix??""}`);s+=r.created,d+=r.linked,l+=r.entities}})(a),{created:s,linked:d,entities:l,artifactsScanned:a.length}}export{T as backfillEntities,N as populateEntities};
@@ -1,5 +1,7 @@
1
- function w(a=process.env){const t=Number(a.WYRM_RECALL_GRAPH_WEIGHT);return Number.isFinite(t)?Math.max(0,Math.min(.5,t)):.15}function y(a,t,h){const c=new Set(h.toLowerCase().split(/[^a-z0-9]+/).filter(e=>e.length>=3));if(c.size===0)return null;const r=a.prepare("SELECT id, name FROM entities WHERE project_id = ? LIMIT 500").all(t);if(r.length===0)return null;const l=new Set(r.map(e=>e.id));let s=[];try{s=a.prepare("SELECT ea.entity_id AS entity_id, ea.alias AS alias FROM entity_aliases ea JOIN entities e ON e.id = ea.entity_id WHERE e.project_id = ? LIMIT 1000").all(t).filter(e=>l.has(e.entity_id))}catch{}const d=new Map(r.map(e=>[e.id,e.name.toLowerCase()])),n=new Map,u=e=>{const i=e.toLowerCase().split(/[^a-z0-9]+/).filter(o=>o.length>=3);return i.length>0&&i.every(o=>c.has(o))};for(const e of r){if(n.size>=5)break;u(e.name)&&n.set(e.id,e.name.toLowerCase())}for(const e of s){if(n.size>=5)break;if(!n.has(e.entity_id)&&u(e.alias)){const i=d.get(e.entity_id);i&&n.set(e.entity_id,i)}}if(n.size===0)return null;const f=[...n.keys()],m=f.map(()=>"?").join(","),M=a.prepare(`
1
+ function L(s=process.env){const n=Number(s.WYRM_RECALL_GRAPH_WEIGHT);return Number.isFinite(n)?Math.max(0,Math.min(.5,n)):.15}function R(s,n,p){const l=new Set(p.toLowerCase().split(/[^a-z0-9]+/).filter(e=>e.length>=3));if(l.size===0)return null;let a=[];try{const e=[...l].map(t=>`"${t.replace(/"/g,"")}"`).join(" OR ");a=s.prepare(`SELECT e.id AS id, e.name AS name FROM entities_fts
2
+ JOIN entities e ON e.id = entities_fts.rowid
3
+ WHERE entities_fts MATCH ? AND e.project_id = ? LIMIT 80`).all(e,n)}catch{a=s.prepare("SELECT id, name FROM entities WHERE project_id = ? LIMIT 500").all(n)}if(a.length===0)return null;const c=new Set(a.map(e=>e.id));let o=[];if(c.size>0)try{const e=[...c].map(()=>"?").join(",");o=s.prepare(`SELECT entity_id, alias FROM entity_aliases WHERE entity_id IN (${e})`).all(...c)}catch{}const _=new Map(a.map(e=>[e.id,e.name.toLowerCase()])),i=new Map,u=e=>{const t=e.toLowerCase().split(/[^a-z0-9]+/).filter(r=>r.length>=3);return t.length>0&&t.every(r=>l.has(r))};for(const e of a){if(i.size>=5)break;u(e.name)&&i.set(e.id,e.name.toLowerCase())}for(const e of o){if(i.size>=5)break;if(!i.has(e.entity_id)&&u(e.alias)){const t=_.get(e.entity_id);t&&i.set(e.entity_id,t)}}if(i.size===0)return null;const f=[...i.keys()],w=f.map(()=>"?").join(","),E=s.prepare(`
2
4
  SELECT source_entity_id AS s, target_entity_id AS t, MAX(weight) AS w FROM relationships
3
- WHERE project_id = ? AND (source_entity_id IN (${m}) OR target_entity_id IN (${m}))
5
+ WHERE project_id = ? AND (source_entity_id IN (${w}) OR target_entity_id IN (${w}))
4
6
  GROUP BY s, t LIMIT 50
5
- `).all(t,...f,...f),p=new Map;for(const e of M){const i=n.has(e.s)?e.t:e.s;if(n.has(i))continue;const o=d.get(i);if(!o)continue;const _=Math.max(0,Math.min(1,e.w??1));(p.get(o)??0)<_&&p.set(o,_)}const g=[...p.entries()].slice(0,10).map(([e,i])=>({name:e,weight:i}));return{direct:[...n.values()],neighbors:g}}function E(a,t,h){const c=Math.max(0,Math.min(.5,h));if(!t||c===0)return 1;const r=a.toLowerCase();for(const s of t.direct)if(s.length>=3&&r.includes(s))return 1+c;let l=0;for(const s of t.neighbors)s.name.length>=3&&r.includes(s.name)&&(l=Math.max(l,s.weight));return l>0?1+c/2*l:1}export{E as graphMultiplier,y as queryGraphContext,w as recallGraphWeight};
7
+ `).all(n,...f,...f),h=new Set,d=new Map;for(const e of E){const t=i.has(e.s)?e.t:e.s;if(i.has(t))continue;const r=Math.max(0,Math.min(1,e.w??1));(d.get(t)??0)<r&&d.set(t,r),h.add(t)}const m=new Map;if(h.size>0){const e=[...h].map(()=>"?").join(","),t=s.prepare(`SELECT id, name FROM entities WHERE id IN (${e})`).all(...h);for(const r of t){const M=r.name.toLowerCase(),g=d.get(r.id)??0;(m.get(M)??0)<g&&m.set(M,g)}}const y=[...m.entries()].slice(0,10).map(([e,t])=>({name:e,weight:t}));return{direct:[...i.values()],neighbors:y}}function I(s,n,p){const l=Math.max(0,Math.min(.5,p));if(!n||l===0)return 1;const a=s.toLowerCase();for(const o of n.direct)if(o.length>=3&&a.includes(o))return 1+l;let c=0;for(const o of n.neighbors)o.name.length>=3&&a.includes(o.name)&&(c=Math.max(c,o.weight));return c>0?1+l/2*c:1}export{I as graphMultiplier,R as queryGraphContext,L as recallGraphWeight};
@@ -1,4 +1,9 @@
1
- import{TOOL_ANNOTATIONS as u}from"../tool-annotations.js";import{CloudSyncDaemon as m}from"../cloud-sync.js";import{getCrypto as h,initializeCrypto as k}from"../crypto.js";import{activateLicense as f,getLicenseInfo as b,getTier as x,hasFeature as d}from"../license.js";const S=[{name:"wyrm_license",description:"View current license status, tier, and available features",inputSchema:{type:"object",properties:{}},annotations:u.wyrm_license,aliases:[],handler:async(o,i)=>{const{cachedResponse:a,sync:r}=i,c=o||{},n=b(),e=x();let t=`\u{F115D} **Wyrm License Status**
1
+ import{TOOL_ANNOTATIONS as u}from"../tool-annotations.js";import{CloudSyncDaemon as m}from"../cloud-sync.js";import{getCrypto as h,initializeCrypto as f}from"../crypto.js";import{activateLicense as g,getLicenseInfo as k,getTier as w,hasFeature as d}from"../license.js";import{loadSession as x}from"../cloud/client.js";import{execFile as b}from"node:child_process";import{promisify as v}from"node:util";import{fileURLToPath as _}from"node:url";import{dirname as $,join as S}from"node:path";const E=v(b);async function T(r){const c=x();if(!c||!c.device_id)return{ok:!1,text:`\u{F115D} Sovereign cloud sync: this device is not logged in.
2
+ The login is an interactive device flow, so run it in a terminal:
3
+ wyrm cloud login
4
+ Then retry this action. (Status/sync here drive an already-registered device.)`};const a=S($(_(import.meta.url)),"..","wyrm-cli.js");try{const{stdout:o,stderr:i}=await E(process.execPath,[a,"cloud",...r],{timeout:18e4,maxBuffer:4194304,env:{...process.env}});return{ok:!0,text:(o||"").trim()||(i||"").trim()||"(no output)"}}catch(o){return{ok:!1,text:`\u{F115D} Sovereign cloud sync failed.
5
+ ${[o?.stdout,o?.stderr].filter(Boolean).join(`
6
+ `).trim()||(o?.message??"unknown error")}`}}}const O=[{name:"wyrm_license",description:"View current license status, tier, and available features",inputSchema:{type:"object",properties:{}},annotations:u.wyrm_license,aliases:[],handler:async(r,c)=>{const{cachedResponse:a,sync:o}=c,i=r||{},n=k(),e=w();let t=`\u{F115D} **Wyrm License Status**
2
7
 
3
8
  `;if(t+=`- **Tier:** ${e.charAt(0).toUpperCase()+e.slice(1)}
4
9
  `,n.valid&&n.key){t+=`- **Key:** ${n.key}
@@ -7,7 +12,7 @@ import{TOOL_ANNOTATIONS as u}from"../tool-annotations.js";import{CloudSyncDaemon
7
12
  `,t+=`- **Status:** \u2713 Valid
8
13
 
9
14
  `,t+=`### Features
10
- `;for(const l of n.features)t+=`- \u2713 ${l}
15
+ `;for(const p of n.features)t+=`- \u2713 ${p}
11
16
  `}else t+=`- **Status:** Free tier (no license key)
12
17
 
13
18
  `,t+=`### Free Features
@@ -23,13 +28,13 @@ import{TOOL_ANNOTATIONS as u}from"../tool-annotations.js";import{CloudSyncDaemon
23
28
  `,t+=`- Priority support
24
29
 
25
30
  `,t+=`### Already have a license?
26
- `,t+="Activate it \u2014 paste the license JSON into the `wyrm_activate` tool, or save it to `~/.wyrm/license.json` and restart Wyrm.\n",t+="Manage or download your license at https://account.ghosts.lk \xB7 buy one at https://ghosts.lk/wyrm.";return a(t)}},{name:"wyrm_activate",description:"Activate a license key to unlock Pro/Team/Enterprise features",inputSchema:{type:"object",properties:{license:{type:"string",description:"License JSON string (signed license from Ghost Protocol)"}},required:["license"]},annotations:u.wyrm_activate,aliases:[],handler:async(o,i)=>{const a=o?.license;if(!a)throw new Error("License JSON string required");try{const r=f(a);return r.valid?{content:[{type:"text",text:`\u{F115D} \u2713 License activated!
31
+ `,t+="Activate it \u2014 paste the license JSON into the `wyrm_activate` tool, or save it to `~/.wyrm/license.json` and restart Wyrm.\n",t+="Manage or download your license at https://account.ghosts.lk \xB7 buy one at https://ghosts.lk/wyrm.";return a(t)}},{name:"wyrm_activate",description:"Activate a license key to unlock Pro/Team/Enterprise features",inputSchema:{type:"object",properties:{license:{type:"string",description:"License JSON string (signed license from Ghost Protocol)"}},required:["license"]},annotations:u.wyrm_activate,aliases:[],handler:async(r,c)=>{const a=r?.license;if(!a)throw new Error("License JSON string required");try{const o=g(a);return o.valid?{content:[{type:"text",text:`\u{F115D} \u2713 License activated!
27
32
 
28
- - **Tier:** ${r.tier}
29
- - **Features:** ${r.features.join(", ")}
33
+ - **Tier:** ${o.tier}
34
+ - **Features:** ${o.features.join(", ")}
30
35
 
31
- Restart Wyrm to apply all features.`}]}:{content:[{type:"text",text:`\u{F115D} \u2717 License activation failed: ${r.error||"unknown error"}`}],isError:!0}}catch{return{content:[{type:"text",text:"\u{F115D} \u2717 Invalid license format. Please verify your license key."}],isError:!0}}}},{name:"wyrm_analytics_dashboard",description:"View usage analytics dashboard \u2014 tool calls, tokens, costs, cache efficiency (Pro+)",inputSchema:{type:"object",properties:{days:{type:"number",description:"Number of days to analyze (default: 30)"}}},annotations:u.wyrm_analytics_dashboard,aliases:[],handler:async(o,i)=>{const{analytics:a}=i,r=o||{};if(!d("analytics"))return{content:[{type:"text",text:`\u{F115D} Analytics requires a Pro license or higher.
32
- Visit https://ghosts.lk/wyrm to upgrade.`}]};const c=o?.days||30,n=a.dashboard(c);let e=`\u{F115D} **Usage Analytics** (last ${c} days)
36
+ Restart Wyrm to apply all features.`}]}:{content:[{type:"text",text:`\u{F115D} \u2717 License activation failed: ${o.error||"unknown error"}`}],isError:!0}}catch{return{content:[{type:"text",text:"\u{F115D} \u2717 Invalid license format. Please verify your license key."}],isError:!0}}}},{name:"wyrm_analytics_dashboard",description:"View usage analytics dashboard \u2014 tool calls, tokens, costs, cache efficiency (Pro+)",inputSchema:{type:"object",properties:{days:{type:"number",description:"Number of days to analyze (default: 30)"}}},annotations:u.wyrm_analytics_dashboard,aliases:[],handler:async(r,c)=>{const{analytics:a}=c,o=r||{};if(!d("analytics"))return{content:[{type:"text",text:`\u{F115D} Analytics requires a Pro license or higher.
37
+ Visit https://ghosts.lk/wyrm to upgrade.`}]};const i=r?.days||30,n=a.dashboard(i);let e=`\u{F115D} **Usage Analytics** (last ${i} days)
33
38
 
34
39
  `;if(e+=`### Overview
35
40
  `,e+=`- **Total Tool Calls:** ${n.summary.total_calls.toLocaleString()}
@@ -44,42 +49,42 @@ Visit https://ghosts.lk/wyrm to upgrade.`}]};const c=o?.days||30,n=a.dashboard(c
44
49
  `;e+=`
45
50
  `}if(n.daily.length>0){e+=`### Recent Activity
46
51
  `;for(const t of n.daily.slice(-7))e+=`- ${t.date}: ${t.calls} calls, ${t.tokens.toLocaleString()} tokens
47
- `}return{content:[{type:"text",text:e}]}}},{name:"wyrm_cost_report",description:"View estimated API cost report for current billing period (Pro+)",inputSchema:{type:"object",properties:{period:{type:"string",description:"Period in YYYY-MM format (default: current month)"}}},annotations:u.wyrm_cost_report,aliases:[],handler:async(o,i)=>{const{analytics:a}=i,r=o||{};if(!d("analytics"))return{content:[{type:"text",text:`\u{F115D} Cost reports require a Pro license or higher.
48
- Visit https://ghosts.lk/wyrm to upgrade.`}]};const c=o?.period,n=a.costReport(c);let e=`\u{F115D} **Cost Report** \u2014 ${n.period}
52
+ `}return{content:[{type:"text",text:e}]}}},{name:"wyrm_cost_report",description:"View estimated API cost report for current billing period (Pro+)",inputSchema:{type:"object",properties:{period:{type:"string",description:"Period in YYYY-MM format (default: current month)"}}},annotations:u.wyrm_cost_report,aliases:[],handler:async(r,c)=>{const{analytics:a}=c,o=r||{};if(!d("analytics"))return{content:[{type:"text",text:`\u{F115D} Cost reports require a Pro license or higher.
53
+ Visit https://ghosts.lk/wyrm to upgrade.`}]};const i=r?.period,n=a.costReport(i);let e=`\u{F115D} **Cost Report** \u2014 ${n.period}
49
54
 
50
55
  `;if(e+=`- **Total Cost:** $${n.total_cost_usd.toFixed(4)}
51
56
  `,e+=`- **Projected Monthly:** $${n.projected_monthly_usd.toFixed(2)}
52
57
 
53
58
  `,n.tools.length>0){e+=`### By Tool
54
59
  `;for(const t of n.tools.slice(0,10))e+=`- **${t.tool}** \u2014 ${t.calls} calls, $${t.cost_usd.toFixed(4)}
55
- `}return{content:[{type:"text",text:e}]}}},{name:"wyrm_cloud_backup",description:"Backup Wyrm database to encrypted cloud storage (Pro+)",inputSchema:{type:"object",properties:{action:{type:"string",description:"Action: backup, restore, list, prune",enum:["backup","restore","list","prune"]},backupKey:{type:"string",description:"Backup key for restore (from list)"},keepCount:{type:"number",description:"Number of backups to keep when pruning (default: 10)"}},required:["action"]},annotations:u.wyrm_cloud_backup,aliases:[],handler:async(o,i)=>{const{cloudBackup:a,db:r}=i,c=o||{};if(!d("cloud_backup"))return{content:[{type:"text",text:`\u{F115D} Cloud backup requires a Pro license or higher.
56
- Visit https://ghosts.lk/wyrm to upgrade.`}]};const n=o?.action;switch(n){case"backup":{const e=await a.backup(r.getDatabasePath()),t=e.metadata;return{content:[{type:"text",text:`\u{F115D} \u2713 Backup complete!
60
+ `}return{content:[{type:"text",text:e}]}}},{name:"wyrm_cloud_backup",description:"Legacy whole-DB snapshot backup to encrypted cloud storage (Pro+): backup, restore, list, prune. This is a full-database snapshot (last-write-wins on restore), NOT the sovereign per-row sync \u2014 for multi-device memory use wyrm_cloud_sync action=sovereign. Keep this for simple point-in-time backups.",inputSchema:{type:"object",properties:{action:{type:"string",description:"Action: backup, restore, list, prune",enum:["backup","restore","list","prune"]},backupKey:{type:"string",description:"Backup key for restore (from list)"},keepCount:{type:"number",description:"Number of backups to keep when pruning (default: 10)"}},required:["action"]},annotations:u.wyrm_cloud_backup,aliases:[],handler:async(r,c)=>{const{cloudBackup:a,db:o}=c,i=r||{};if(!d("cloud_backup"))return{content:[{type:"text",text:`\u{F115D} Cloud backup requires a Pro license or higher.
61
+ Visit https://ghosts.lk/wyrm to upgrade.`}]};const n=r?.action;switch(n){case"backup":{const e=await a.backup(o.getDatabasePath()),t=e.metadata;return{content:[{type:"text",text:`\u{F115D} \u2713 Backup complete!
57
62
 
58
63
  - **Key:** ${e.key}
59
64
  - **Size:** ${(t.db_size/1024).toFixed(1)} KB \u2192 ${(t.compressed_size/1024).toFixed(1)} KB compressed
60
65
  - **Encrypted:** ${t.encrypted?"Yes":"No"}
61
- - **Checksum:** ${t.checksum.slice(0,16)}...`}]}}case"restore":{const e=o?.backupKey;if(!e)throw new Error("backupKey required for restore");const t=await a.restore(e,r.getDatabasePath());return{content:[{type:"text",text:`\u{F115D} \u2713 Restore complete!
66
+ - **Checksum:** ${t.checksum.slice(0,16)}...`}]}}case"restore":{const e=r?.backupKey;if(!e)throw new Error("backupKey required for restore");const t=await a.restore(e,o.getDatabasePath());return{content:[{type:"text",text:`\u{F115D} \u2713 Restore complete!
62
67
 
63
68
  - **Restored:** ${t.restored?"Yes":"No"}
64
69
  - **Size:** ${(t.size/1024).toFixed(1)} KB
65
- - **Previous DB backed up to:** ${r.getDatabasePath()}.bak
70
+ - **Previous DB backed up to:** ${o.getDatabasePath()}.bak
66
71
 
67
72
  \u26A0\uFE0F Restart Wyrm to use the restored database.`}]}}case"list":{const e=await a.listBackups();if(e.length===0)return{content:[{type:"text",text:"\u{F115D} No backups found."}]};let t=`\u{F115D} **Cloud Backups** (${e.length} found)
68
73
 
69
- `;for(const l of e)t+=`- **${l.timestamp}** \u2014 ${(l.db_size/1024).toFixed(1)} KB, encrypted: ${l.encrypted}, machine: ${l.machine_id.slice(0,8)}...
70
- `;return{content:[{type:"text",text:t}]}}case"prune":{const e=o?.keepCount||10;return{content:[{type:"text",text:`\u{F115D} \u2713 Pruned ${(await a.pruneBackups(e)).deleted} old backup(s). Kept ${e} most recent.`}]}}default:return{content:[{type:"text",text:`Unknown backup action: ${n}. Use: backup, restore, list, prune`}],isError:!0}}}},{name:"wyrm_cloud_sync",description:"Start, stop, or check the cloud-sync daemon \u2014 Phase 2 multi-device sync. Periodically backs up the DB to R2 and restores on startup if a newer snapshot exists. Last-write-wins by mtime.",inputSchema:{type:"object",properties:{action:{type:"string",description:"Action: start, stop, restart, status, force-sync",enum:["start","stop","restart","status","force-sync"]},interval_minutes:{type:"number",description:"Sync interval (default: 10)"},keep_count:{type:"number",description:"Snapshots to retain after pruning (default: 20)"}},required:["action"]},annotations:u.wyrm_cloud_sync,aliases:[],handler:async(o,i)=>{const{cloudBackup:a,cloudSyncManager:r,db:c,sync:n}=i,e=o||{};if(!d("cloud_backup"))return{content:[{type:"text",text:`\u{F115D} Cloud sync requires a Pro license or higher.
71
- Visit https://ghosts.lk/wyrm to upgrade.`}]};const t=o?.action,l=o?.interval_minutes,y=o?.keep_count;switch(t){case"start":{const s=r.start({interval_minutes:l,keep_count:y});return s.ok?{content:[{type:"text",text:`\u{F115D} \u2713 cloud-sync daemon started (pid ${s.pid}). Logs: ~/.wyrm/wyrm-cloud-sync.log`}]}:{content:[{type:"text",text:`\u{F115D} cloud-sync: ${s.reason??"failed to start"}${s.pid?` (pid ${s.pid})`:""}`}],isError:!s.pid}}case"stop":{const s=r.stop();return{content:[{type:"text",text:s.ok?"\u{F115D} \u2713 cloud-sync daemon stopped":`\u{F115D} cloud-sync: ${s.reason??"failed to stop"}`}],isError:!s.ok}}case"restart":{r.stop(),await new Promise(p=>{setTimeout(p,500)});const s=r.start({interval_minutes:l,keep_count:y});return{content:[{type:"text",text:s.ok?`\u{F115D} \u2713 cloud-sync daemon restarted (pid ${s.pid})`:`\u{F115D} cloud-sync restart failed: ${s.reason??"unknown"}`}],isError:!s.ok}}case"status":{const s=r.status(),p=["\u{F115D} **Cloud Sync Status**"];return p.push(`- Running: ${s.running?"\u2713 yes":"\u2717 no"}`),s.pid&&p.push(`- PID: ${s.pid}`),s.state&&(s.state.last_uploaded_ts&&p.push(`- Last upload: ${new Date(s.state.last_uploaded_ts).toISOString()}${s.state.last_uploaded_key?` (${s.state.last_uploaded_key})`:""}`),s.state.last_restored_key&&p.push(`- Last restore: ${s.state.last_restored_key}`),s.state.last_check_ts&&p.push(`- Last check: ${new Date(s.state.last_check_ts).toISOString()}`),p.push(`- Machine: ${s.state.machine}`)),{content:[{type:"text",text:p.join(`
72
- `)}]}}case"force-sync":{const p=await new m(a,c.getDatabasePath()).tick();return{content:[{type:"text",text:`\u{F115D} force-sync: ${JSON.stringify(p,null,2)}`}]}}default:return{content:[{type:"text",text:`Unknown cloud-sync action: ${t}. Use: start, stop, restart, status, force-sync`}],isError:!0}}}},{name:"wyrm_encrypt_setup",description:"Set up or check encryption status for sensitive data (Pro+)",inputSchema:{type:"object",properties:{action:{type:"string",description:"Action: status, enable, test",enum:["status","enable","test"]},password:{type:"string",description:"Encryption password (min 8 chars, required for enable)"}},required:["action"]},annotations:u.wyrm_encrypt_setup,aliases:[],handler:async(o,i)=>{const a=o||{};if(!d("encryption"))return{content:[{type:"text",text:`\u{F115D} Encryption setup requires a Pro license or higher.
73
- Visit https://ghosts.lk/wyrm to upgrade.`}]};const r=o?.action,c=h();switch(r){case"status":{const n=c.isEnabled();let e=`\u{F115D} **Encryption Status**
74
+ `;for(const p of e)t+=`- **${p.timestamp}** \u2014 ${(p.db_size/1024).toFixed(1)} KB, encrypted: ${p.encrypted}, machine: ${p.machine_id.slice(0,8)}...
75
+ `;return{content:[{type:"text",text:t}]}}case"prune":{const e=r?.keepCount||10;return{content:[{type:"text",text:`\u{F115D} \u2713 Pruned ${(await a.pruneBackups(e)).deleted} old backup(s). Kept ${e} most recent.`}]}}default:return{content:[{type:"text",text:`Unknown backup action: ${n}. Use: backup, restore, list, prune`}],isError:!0}}}},{name:"wyrm_cloud_sync",description:"Cloud sync. The SOVEREIGN path is the promoted one: zero-knowledge, per-row AES-256-GCM E2E-encrypted delta sync with tombstone propagation \u2014 the cloud never sees plaintext. Use action 'sovereign' to sync (push+pull), 'sovereign-preview' for a dry run, 'sovereign-status' to show device + sync state. (First-time login is an interactive device flow: run `wyrm cloud login` in a terminal once.) The start/stop/restart/status/force-sync actions drive the LEGACY whole-DB snapshot daemon (last-write-wins by mtime, weaker) \u2014 retained only for simple full-DB backup; it is NOT the sovereign path. Prefer sovereign for multi-device memory.",inputSchema:{type:"object",properties:{action:{type:"string",description:"Sovereign (preferred): sovereign, sovereign-preview, sovereign-status. Legacy snapshot daemon: start, stop, restart, status, force-sync.",enum:["sovereign","sovereign-preview","sovereign-status","start","stop","restart","status","force-sync"]},all:{type:"boolean",description:"Sovereign only: sync every semantic table, ignoring visibility flags (default: false)"},interval_minutes:{type:"number",description:"Legacy snapshot only: sync interval (default: 10)"},keep_count:{type:"number",description:"Legacy snapshot only: snapshots to retain after pruning (default: 20)"}},required:["action"]},annotations:u.wyrm_cloud_sync,aliases:[],handler:async(r,c)=>{const{cloudBackup:a,cloudSyncManager:o,db:i,sync:n}=c,e=r||{},t=r?.action;if(t==="sovereign"||t==="sovereign-preview"||t==="sovereign-status"){const s=t==="sovereign"?r?.all?["sync","--all"]:["sync"]:t==="sovereign-preview"?["sync","--dry-run"]:["status"],l=await T(s);return{content:[{type:"text",text:l.text}],isError:!l.ok}}if(!d("cloud_backup"))return{content:[{type:"text",text:`\u{F115D} The legacy snapshot daemon requires a Pro license or higher.
76
+ Visit https://ghosts.lk/wyrm to upgrade. (The sovereign path \u2014 action=sovereign \u2014 is the preferred sync.)`}]};const p=r?.interval_minutes,y=r?.keep_count;switch(t){case"start":{const s=o.start({interval_minutes:p,keep_count:y});return s.ok?{content:[{type:"text",text:`\u{F115D} \u2713 cloud-sync daemon started (pid ${s.pid}). Logs: ~/.wyrm/wyrm-cloud-sync.log`}]}:{content:[{type:"text",text:`\u{F115D} cloud-sync: ${s.reason??"failed to start"}${s.pid?` (pid ${s.pid})`:""}`}],isError:!s.pid}}case"stop":{const s=o.stop();return{content:[{type:"text",text:s.ok?"\u{F115D} \u2713 cloud-sync daemon stopped":`\u{F115D} cloud-sync: ${s.reason??"failed to stop"}`}],isError:!s.ok}}case"restart":{o.stop(),await new Promise(l=>{setTimeout(l,500)});const s=o.start({interval_minutes:p,keep_count:y});return{content:[{type:"text",text:s.ok?`\u{F115D} \u2713 cloud-sync daemon restarted (pid ${s.pid})`:`\u{F115D} cloud-sync restart failed: ${s.reason??"unknown"}`}],isError:!s.ok}}case"status":{const s=o.status(),l=["\u{F115D} **Legacy Snapshot Daemon Status** (not the sovereign path \u2014 see action=sovereign-status)"];return l.push(`- Running: ${s.running?"\u2713 yes":"\u2717 no"}`),s.pid&&l.push(`- PID: ${s.pid}`),s.state&&(s.state.last_uploaded_ts&&l.push(`- Last upload: ${new Date(s.state.last_uploaded_ts).toISOString()}${s.state.last_uploaded_key?` (${s.state.last_uploaded_key})`:""}`),s.state.last_restored_key&&l.push(`- Last restore: ${s.state.last_restored_key}`),s.state.last_check_ts&&l.push(`- Last check: ${new Date(s.state.last_check_ts).toISOString()}`),l.push(`- Machine: ${s.state.machine}`)),{content:[{type:"text",text:l.join(`
77
+ `)}]}}case"force-sync":{const l=await new m(a,i.getDatabasePath()).tick();return{content:[{type:"text",text:`\u{F115D} force-sync: ${JSON.stringify(l,null,2)}`}]}}default:return{content:[{type:"text",text:`Unknown cloud-sync action: ${t}. Use: start, stop, restart, status, force-sync`}],isError:!0}}}},{name:"wyrm_encrypt_setup",description:"Set up or check encryption status for sensitive data (Pro+)",inputSchema:{type:"object",properties:{action:{type:"string",description:"Action: status, enable, test",enum:["status","enable","test"]},password:{type:"string",description:"Encryption password (min 8 chars, required for enable)"}},required:["action"]},annotations:u.wyrm_encrypt_setup,aliases:[],handler:async(r,c)=>{const a=r||{};if(!d("encryption"))return{content:[{type:"text",text:`\u{F115D} Encryption setup requires a Pro license or higher.
78
+ Visit https://ghosts.lk/wyrm to upgrade.`}]};const o=r?.action,i=h();switch(o){case"status":{const n=i.isEnabled();let e=`\u{F115D} **Encryption Status**
74
79
 
75
80
  `;return e+=`- **Enabled:** ${n?"\u2713 Yes":"\u2717 No"}
76
81
  `,e+=`- **Algorithm:** AES-256-GCM
77
82
  `,n?e+=`- **Status:** Active \u2014 new data is encrypted at rest
78
- `:e+='\nTo enable, run: `wyrm_encrypt_setup` with action "enable" and a strong password.\n',{content:[{type:"text",text:e}]}}case"enable":{const n=o?.password;return!n||n.length<8?{content:[{type:"text",text:"\u{F115D} Password must be at least 8 characters."}],isError:!0}:(k(n),{content:[{type:"text",text:`\u{F115D} \u2713 Encryption enabled!
83
+ `:e+='\nTo enable, run: `wyrm_encrypt_setup` with action "enable" and a strong password.\n',{content:[{type:"text",text:e}]}}case"enable":{const n=r?.password;return!n||n.length<8?{content:[{type:"text",text:"\u{F115D} Password must be at least 8 characters."}],isError:!0}:(f(n),{content:[{type:"text",text:`\u{F115D} \u2713 Encryption enabled!
79
84
 
80
85
  - **Algorithm:** AES-256-GCM
81
86
  - **Key derived from:** your password (PBKDF2)
82
87
 
83
88
  \u26A0\uFE0F **Store your password safely.** Lost passwords cannot be recovered.
84
- New data lake entries will be encrypted at rest.`}]})}case"test":{if(!c.isEnabled())return{content:[{type:"text",text:'\u{F115D} Encryption not enabled. Run with action "enable" first.'}]};const n="Wyrm encryption test "+Date.now(),e=c.encrypt(n),t=c.decrypt(e);return{content:[{type:"text",text:`\u{F115D} Encryption test: ${t===n?"\u2713 PASSED":"\u2717 FAILED"}
85
- - Encrypted ${n.length} chars \u2192 decrypted back to ${t.length} chars`}]}}default:return{content:[{type:"text",text:`Unknown encrypt action: ${r}. Use: status, enable, test`}],isError:!0}}}}];export{S as cloudToolSpecs};
89
+ New data lake entries will be encrypted at rest.`}]})}case"test":{if(!i.isEnabled())return{content:[{type:"text",text:'\u{F115D} Encryption not enabled. Run with action "enable" first.'}]};const n="Wyrm encryption test "+Date.now(),e=i.encrypt(n),t=i.decrypt(e);return{content:[{type:"text",text:`\u{F115D} Encryption test: ${t===n?"\u2713 PASSED":"\u2717 FAILED"}
90
+ - Encrypted ${n.length} chars \u2192 decrypted back to ${t.length} chars`}]}}default:return{content:[{type:"text",text:`Unknown encrypt action: ${o}. Use: status, enable, test`}],isError:!0}}}}];export{O as cloudToolSpecs};
@@ -1,4 +1,4 @@
1
- import{pruneWriteLedger as _}from"./receipts.js";import{metabolizeEnabled as E,runMetabolize as h}from"./metabolize.js";function R(c,o={}){const{db:n,sessionSeen:d,failures:a,presence:u}=c,{vacuum:p,archiveDays:i}=o,r=[];if(i){const e=n.getAllProjects(1e3);let s=0;for(const t of e)s+=n.archiveOldSessions(t.id,i);r.push(`Archived ${s} old sessions`)}p&&(n.vacuum(),r.push("Vacuumed database"));try{const e=parseInt(process.env.WYRM_SEEN_TTL_DAYS??"7",10),s=d.prune(Number.isFinite(e)&&e>0?e:7);s>0&&r.push(`Pruned ${s} session_seen_artifacts rows older than ${e}d`)}catch{}try{const e=parseInt(process.env.WYRM_EVENT_RETAIN_DAYS??"90",10),s=parseInt(process.env.WYRM_EVENT_MAX_PER_PROJECT??"5000",10),{deleted:t}=n.pruneEvents({olderThanDays:Number.isFinite(e)?e:90,maxPerProject:Number.isFinite(s)?s:5e3});t>0&&r.push(`Pruned ${t} Live Memory events (retention)`)}catch{}try{if(E()){const e=h(n.getDatabase());e.exactMerged+e.nearCandidatesQueued+e.decayed>0&&r.push(`Metabolize: ${e.exactMerged} exact dupes superseded, ${e.nearCandidatesQueued} near-dup candidates queued, ${e.decayed} stale auto-captures decayed`)}}catch{}try{const e=_(n.getDatabase());e>0&&r.push(`Pruned ${e} write_ledger receipts (retention)`)}catch{}try{const e=parseInt(process.env.WYRM_EVENT_RETAIN_DAYS??"90",10),s=Number.isFinite(e)&&e>0?e:90,t=a.pruneBlocks(s);t>0&&r.push(`Pruned ${t} failure_blocks rows older than ${s}d (retention)`)}catch{}try{const e=u.reap();(e.presence>0||e.claims>0)&&r.push(`Reaped ${e.presence} stale presence row(s) and ${e.claims} expired quest claim(s)`)}catch{}try{const e=parseInt(process.env.WYRM_EVENT_RETAIN_DAYS??"90",10),s=Number.isFinite(e)&&e>0?e:90,t=n.getDatabase().prepare("DELETE FROM run_briefs WHERE created_at < datetime('now', ?)").run(`-${s} days`).changes;t>0&&r.push(`Pruned ${t} run_briefs rows older than ${s}d (retention)`)}catch{}try{const e=parseInt(process.env.WYRM_RUN_BRIEFS_MAX??"512",10),s=Number.isFinite(e)&&e>0?e:512,t=n.getDatabase().prepare(`
1
+ import{pruneWriteLedger as _}from"./receipts.js";import{metabolizeEnabled as E,metabolizeDefaultEnabled as f,runMetabolize as h}from"./metabolize.js";function R(c,o={}){const{db:n,sessionSeen:d,failures:a,presence:u}=c,{vacuum:p,archiveDays:i}=o,s=[];if(i){const e=n.getAllProjects(1e3);let r=0;for(const t of e)r+=n.archiveOldSessions(t.id,i);s.push(`Archived ${r} old sessions`)}p&&(n.vacuum(),s.push("Vacuumed database"));try{const e=parseInt(process.env.WYRM_SEEN_TTL_DAYS??"7",10),r=d.prune(Number.isFinite(e)&&e>0?e:7);r>0&&s.push(`Pruned ${r} session_seen_artifacts rows older than ${e}d`)}catch{}try{const e=parseInt(process.env.WYRM_EVENT_RETAIN_DAYS??"90",10),r=parseInt(process.env.WYRM_EVENT_MAX_PER_PROJECT??"5000",10),{deleted:t}=n.pruneEvents({olderThanDays:Number.isFinite(e)?e:90,maxPerProject:Number.isFinite(r)?r:5e3});t>0&&s.push(`Pruned ${t} Live Memory events (retention)`)}catch{}try{if(f()){const e=h(n.getDatabase(),{near:E()});e.exactMerged+e.nearCandidatesQueued+e.decayed>0&&s.push(`Metabolize: ${e.exactMerged} exact dupes superseded, ${e.nearCandidatesQueued} near-dup candidates queued, ${e.decayed} stale auto-captures decayed`)}}catch{}try{const e=_(n.getDatabase());e>0&&s.push(`Pruned ${e} write_ledger receipts (retention)`)}catch{}try{const e=parseInt(process.env.WYRM_EVENT_RETAIN_DAYS??"90",10),r=Number.isFinite(e)&&e>0?e:90,t=a.pruneBlocks(r);t>0&&s.push(`Pruned ${t} failure_blocks rows older than ${r}d (retention)`)}catch{}try{const e=u.reap();(e.presence>0||e.claims>0)&&s.push(`Reaped ${e.presence} stale presence row(s) and ${e.claims} expired quest claim(s)`)}catch{}try{const e=parseInt(process.env.WYRM_EVENT_RETAIN_DAYS??"90",10),r=Number.isFinite(e)&&e>0?e:90,t=n.getDatabase().prepare("DELETE FROM run_briefs WHERE created_at < datetime('now', ?)").run(`-${r} days`).changes;t>0&&s.push(`Pruned ${t} run_briefs rows older than ${r}d (retention)`)}catch{}try{const e=parseInt(process.env.WYRM_RUN_BRIEFS_MAX??"512",10),r=Number.isFinite(e)&&e>0?e:512,t=n.getDatabase().prepare(`
2
2
  DELETE FROM run_briefs WHERE rowid IN (
3
3
  SELECT rowid FROM run_briefs ORDER BY created_at DESC, rowid DESC LIMIT -1 OFFSET ?
4
- )`).run(s).changes;t>0&&r.push(`Pruned ${t} run_briefs rows over the ${s}-row cap (WYRM_RUN_BRIEFS_MAX)`)}catch{}try{const e=parseInt(process.env.WYRM_RUN_INACTIVE_HOURS??"24",10),s=a.sweepRunQuarantine({runInactiveHours:Number.isFinite(e)&&e>0?e:24});(s.promoted>0||s.expired>0||s.runs_abandoned>0)&&r.push(`Run-quarantine sweep: promoted ${s.promoted} failure(s), expired ${s.expired} abandoned-run failure(s), TTL-abandoned ${s.runs_abandoned} stalled run(s)`),s.degraded&&r.push(`Run-quarantine sweep: ${s.degraded}`)}catch{}return n.checkpoint(),r.push("Checkpointed WAL"),{lines:r,dbSize:n.getStats().dbSize}}export{R as runMaintenance};
4
+ )`).run(r).changes;t>0&&s.push(`Pruned ${t} run_briefs rows over the ${r}-row cap (WYRM_RUN_BRIEFS_MAX)`)}catch{}try{const e=parseInt(process.env.WYRM_RUN_INACTIVE_HOURS??"24",10),r=a.sweepRunQuarantine({runInactiveHours:Number.isFinite(e)&&e>0?e:24});(r.promoted>0||r.expired>0||r.runs_abandoned>0)&&s.push(`Run-quarantine sweep: promoted ${r.promoted} failure(s), expired ${r.expired} abandoned-run failure(s), TTL-abandoned ${r.runs_abandoned} stalled run(s)`),r.degraded&&s.push(`Run-quarantine sweep: ${r.degraded}`)}catch{}return n.checkpoint(),s.push("Checkpointed WAL"),{lines:s,dbSize:n.getStats().dbSize}}export{R as runMaintenance};
@@ -1,9 +1,10 @@
1
- import{emitEvent as ne}from"./events.js";import{sanitizeFtsQuery as P,buildFtsMatchQuery as K}from"./security.js";import{getActor as se}from"./handlers/boundary.js";import{buildPage as ie,DEFAULT_PAGE_SIZE as re}from"./keyset.js";import{rerankConfigured as ae,rerankCandidates as ce}from"./rerank.js";import{autoSummaryPenalty as Y,recallRecencyWeight as j,recallRecencyHalfLifeDays as G,recencyMultiplier as V,extractTemporalWindow as z,recallTemporalWeight as X,temporalMultiplier as J,recallUsefulnessWeight as Q,usefulnessMultiplier as Z,looksInjectionShaped as oe,trustMarker as de}from"./context-ranking.js";import{queryGraphContext as le,graphMultiplier as fe,recallGraphWeight as ue}from"./graph-recall.js";const q=40,me=10,_e=.3;class Ce{db;vectorStore;setVectorStore(t){this.vectorStore=t}getVectorStore(){return this.vectorStore}constructor(t){this.db=t}indexArtifact(t){if(!this.vectorStore)return;const e=`${t.problem}${t.validated_fix?" "+t.validated_fix:""}`.slice(0,2e3);this.vectorStore.addVector(e,"artifact",t.id,t.project_id).catch(()=>{})}add(t,e){if(typeof e.problem!="string"||!e.problem.trim())throw new Error("memory.add: 'problem' is required (a non-empty string).");const n=e.tags?.length?e.tags.join(","):null,s=se(),r=e.createdBy??"local",a=e.sourceTrust??(r.startsWith("bridge:")?"operator":r.startsWith("import:")||(n??"").includes("imported_from:")?"imported":"agent");let o=e.needsReview??0,i=null;if(o===0&&a==="agent"&&s.agent_id&&(process.env.WYRM_PROBATION??"")==="1"){const d=Number(process.env.WYRM_PROBATION_HOURS),D=Number.isFinite(d)&&d>=1?d:24,p=this.db.prepare("SELECT MIN(created_at) AS f FROM memory_artifacts WHERE agent_id = ?").get(s.agent_id),N=p.f?new Date(p.f.replace(" ","T")+"Z").getTime():NaN;(!Number.isFinite(N)||Date.now()-N<D*36e5)&&(o=1,i="probation")}const m=i?n?`${n},${i}`:i:n,_=this.db.prepare(`
1
+ import{emitEvent as ne}from"./events.js";import{sanitizeFtsQuery as P,buildFtsMatchQuery as K}from"./security.js";import{getActor as se}from"./handlers/boundary.js";import{buildPage as ie,DEFAULT_PAGE_SIZE as re}from"./keyset.js";import{rerankConfigured as ae,rerankCandidates as ce}from"./rerank.js";import{autoSummaryPenalty as Y,recallRecencyWeight as j,recallRecencyHalfLifeDays as G,recencyMultiplier as V,extractTemporalWindow as z,recallTemporalWeight as X,temporalMultiplier as J,recallUsefulnessWeight as Q,usefulnessMultiplier as Z,looksInjectionShaped as oe,trustMarker as de}from"./context-ranking.js";import{queryGraphContext as le,graphMultiplier as fe,recallGraphWeight as ue}from"./graph-recall.js";import{populateEntities as me}from"./entity-populate.js";const q=40,_e=10,he=.3;class ye{db;vectorStore;setVectorStore(t){this.vectorStore=t}getVectorStore(){return this.vectorStore}constructor(t){this.db=t}indexArtifact(t){if(!this.vectorStore)return;const e=`${t.problem}${t.validated_fix?" "+t.validated_fix:""}`.slice(0,2e3);this.vectorStore.addVector(e,"artifact",t.id,t.project_id).catch(()=>{})}add(t,e){if(typeof e.problem!="string"||!e.problem.trim())throw new Error("memory.add: 'problem' is required (a non-empty string).");const n=e.tags?.length?e.tags.join(","):null,s=se(),r=e.createdBy??"local",a=e.sourceTrust??(r.startsWith("bridge:")?"operator":r.startsWith("import:")||(n??"").includes("imported_from:")?"imported":"agent");let o=e.needsReview??0,i=null;if(o===0&&a==="agent"&&s.agent_id&&(process.env.WYRM_PROBATION??"")==="1"){const d=Number(process.env.WYRM_PROBATION_HOURS),D=Number.isFinite(d)&&d>=1?d:24,p=this.db.prepare("SELECT MIN(created_at) AS f FROM memory_artifacts WHERE agent_id = ?").get(s.agent_id),N=p.f?new Date(p.f.replace(" ","T")+"Z").getTime():NaN;(!Number.isFinite(N)||Date.now()-N<D*36e5)&&(o=1,i="probation")}const m=i?n?`${n},${i}`:i:n,_=this.db.prepare(`
2
2
  INSERT INTO memory_artifacts
3
3
  (project_id, kind, problem, constraints, validated_fix, why_it_worked,
4
4
  outcome, source_session_id, tags, confidence, needs_review, created_by, agent_id, run_id, source_trust)
5
5
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6
- `).run(t,e.kind,e.problem.trim(),e.constraints?.trim()??null,e.validatedFix?.trim()??null,e.whyItWorked?.trim()??null,e.outcome??"neutral",e.sourceSessionId??null,m,e.confidence??1,o,r,s.agent_id,s.run_id,a),c=this.get(_.lastInsertRowid);return o||(ne(this.db,{projectId:t,kind:"capture",refTable:"memory_artifacts",refId:c.id,isShared:!!c.is_shared}),this.indexArtifact(c)),c}get(t){return this.db.prepare("SELECT * FROM memory_artifacts WHERE id = ?").get(t)??null}update(t,e){return this.db.prepare(`
6
+ `).run(t,e.kind,e.problem.trim(),e.constraints?.trim()??null,e.validatedFix?.trim()??null,e.whyItWorked?.trim()??null,e.outcome??"neutral",e.sourceSessionId??null,m,e.confidence??1,o,r,s.agent_id,s.run_id,a),c=this.get(_.lastInsertRowid);if(!o){ne(this.db,{projectId:t,kind:"capture",refTable:"memory_artifacts",refId:c.id,isShared:!!c.is_shared}),this.indexArtifact(c);try{const d=`${c.problem}${c.validated_fix?`
7
+ `+c.validated_fix:""}`;me(this.db,t,d,`mem:${c.id}`)}catch{}}return c}get(t){return this.db.prepare("SELECT * FROM memory_artifacts WHERE id = ?").get(t)??null}update(t,e){return this.db.prepare(`
7
8
  UPDATE memory_artifacts SET
8
9
  confidence = COALESCE(?, confidence),
9
10
  validated_fix = COALESCE(?, validated_fix),
@@ -22,7 +23,7 @@ import{emitEvent as ne}from"./events.js";import{sanitizeFtsQuery as P,buildFtsMa
22
23
  END,
23
24
  updated_at = datetime('now')
24
25
  WHERE id = ?
25
- `).run(e?1:0,t)}recall(t,e,n={}){const s=n.limit??10,r=n.minConfidence??0,a=this.searchByFts(t,e,q,n.kind,r),o=this.searchByTags(t,e,q,n.kind,r),i=new Set,m=[];a.forEach((c,d)=>{i.add(c.id),m.push({artifact:c,inFts:!0,inTag:!1,ftsRank:d})});for(const c of o)if(!i.has(c.id))i.add(c.id),m.push({artifact:c,inFts:!1,inTag:!0});else{const d=m.find(D=>D.artifact.id===c.id);d&&(d.inTag=!0)}return m.filter(({artifact:c})=>c.confidence>=r).map(({artifact:c,inFts:d,inTag:D,ftsRank:p})=>{let N;if(d){const u=1-(p??0)/(a.length+1);N=(D?.7:.55)+.45*u}else N=.4;N*=c.confidence,c.outcome==="positive"&&(N*=1.1);const M=(Date.now()-new Date(c.last_validated_at).getTime())/(1e3*60*60*24),w=Math.max(.5,1-M/180);return N*=w,{artifact:c,relevance_score:Math.min(1,N),match_type:d&&D?"both":d?"fts":"tag"}}).sort((c,d)=>d.relevance_score-c.relevance_score).slice(0,s)}async recallHybrid(t,e,n={}){const s=n.limit??10,r=n.minConfidence??0,a=n.rerank===!0&&ae(),o=Math.min(Math.max(s*2,20),50),i=a?o:s;if(!this.vectorStore)return this.recall(t,e,n);const m=Math.max(50,i*5),_=Date.now(),c=this.searchByFts(t,e,m,n.kind,r),d=Date.now()-_,D=Date.now();let p=[];try{p=(await this.vectorStore.search(e,m,t,["artifact"])).map(E=>({id:E.content_id,s:E.similarity}))}catch{}const N=Date.now()-D;if(p.length===0)return this.recall(t,e,n);const M=Date.now(),w=60,u=Number(process.env.WYRM_RERANK_ALPHA),g=Number.isFinite(u)?Math.max(0,Math.min(1,u)):.7,A=new Map;if(process.env.WYRM_RERANK_FUSION==="rrf")c.forEach((f,E)=>A.set(f.id,(A.get(f.id)??0)+1/(w+E+1))),p.forEach((f,E)=>A.set(f.id,(A.get(f.id)??0)+1/(w+E+1)));else{const f=c.length+1;c.forEach((L,te)=>A.set(L.id,(A.get(L.id)??0)+(1-g)*(1-te/f)));const E=p.map(L=>L.s),R=Math.min(...E),F=Math.max(...E),I=F-R;p.forEach(L=>A.set(L.id,(A.get(L.id)??0)+g*(I>1e-9?(L.s-R)/I:1)))}const O=new Set(c.map(f=>f.id)),C=new Set(p.map(f=>f.id)),$=new Map(c.map(f=>[f.id,f])),y=[...A.entries()].sort((f,E)=>E[1]-f[1]),x=j(),W=G(),l=X(),h=l>0?z(e):null,S=Q(),k=ue(),b=k>0?le(this.db,t,e):null,v=new Date,U=Math.min(y.length,Math.max(i*4,50)),H=[];for(const[f,E]of y.slice(0,U)){const R=$.get(f)??this.get(f)??void 0;if(!R||R.confidence<r||R.needs_review===1||R.supersedes_id!=null||n.kind&&R.kind!==n.kind)continue;const F=R.updated_at??R.created_at;let I=E*V(F,x,W,v);h&&(I*=J(R.created_at,h,l,v)),I*=Z(R.reuse_count,R.reuse_success_count,S),b&&(I*=fe(`${R.problem} ${R.validated_fix??""}`,b,k)),H.push({id:f,s:I,art:R})}H.sort((f,E)=>E.s-f.s);const ee=H[0]?.s??1,T=[];for(const{id:f,s:E,art:R}of H){if(T.length>=i)break;const F=O.has(f)&&C.has(f)?"hybrid":C.has(f)?"vector":"fts";T.push({artifact:R,relevance_score:Math.min(1,E/ee),match_type:F})}const B=Date.now()-M;if(a&&T.length>0){const f=Date.now(),E=await this.rerankResults(e,T);return n.onStats?.({ftsMs:d,vectorMs:N,fusionMs:B,rerankMs:Date.now()-f,candidates:T.length}),E.slice(0,s)}return n.onStats?.({ftsMs:d,vectorMs:N,fusionMs:B,candidates:T.length}),T}async rerankResults(t,e){const n=e.map(i=>({id:i.artifact.id,text:`${i.artifact.problem}${i.artifact.validated_fix?" "+i.artifact.validated_fix:""}`.slice(0,2e3)})),s=await ce(t,n);if(!s)return e;const r=new Map(e.map(i=>[i.artifact.id,i])),a=[],o=s.length;return s.forEach((i,m)=>{const _=r.get(i.id);_&&a.push({..._,relevance_score:o>0?(o-m)/o:_.relevance_score})}),a}async recallHybridGlobal(t,e={}){const n=e.limit??10,s=e.minConfidence??0,r=Math.max(50,n*5),a=this.searchByFtsGlobal(t,r,e.kind,s),o=()=>a.slice(0,n).map((l,h)=>({artifact:l,relevance_score:1-h/(a.length+1),match_type:"fts"}));if(!this.vectorStore)return o();let i=[];try{i=(await this.vectorStore.search(t,r,void 0,["artifact"])).map(h=>({id:h.content_id,s:h.similarity}))}catch{}if(i.length===0)return o();const m=60,_=Number(process.env.WYRM_RERANK_ALPHA),c=Number.isFinite(_)?Math.max(0,Math.min(1,_)):.7,d=new Map;if(process.env.WYRM_RERANK_FUSION==="rrf")a.forEach((l,h)=>d.set(l.id,(d.get(l.id)??0)+1/(m+h+1))),i.forEach((l,h)=>d.set(l.id,(d.get(l.id)??0)+1/(m+h+1)));else{const l=a.length+1;a.forEach((v,U)=>d.set(v.id,(d.get(v.id)??0)+(1-c)*(1-U/l)));const h=i.map(v=>v.s),S=Math.min(...h),k=Math.max(...h),b=k-S;i.forEach(v=>d.set(v.id,(d.get(v.id)??0)+c*(b>1e-9?(v.s-S)/b:1)))}const D=new Set(a.map(l=>l.id)),p=new Set(i.map(l=>l.id)),N=new Map(a.map(l=>[l.id,l])),M=[...d.entries()].sort((l,h)=>h[1]-l[1]),w=j(),u=G(),g=X(),A=g>0?z(t):null,O=Q(),C=new Date,$=Math.min(M.length,Math.max(n*4,50)),y=[];for(const[l,h]of M.slice(0,$)){const S=N.get(l)??this.get(l)??void 0;if(!S||S.confidence<s||S.needs_review===1||S.supersedes_id!=null||e.kind&&S.kind!==e.kind)continue;const k=S.updated_at??S.created_at;let b=h*V(k,w,u,C);A&&(b*=J(S.created_at,A,g,C)),b*=Z(S.reuse_count,S.reuse_success_count,O),y.push({id:l,s:b,art:S})}y.sort((l,h)=>h.s-l.s);const x=y[0]?.s??1,W=[];for(const{id:l,s:h,art:S}of y){if(W.length>=n)break;const k=D.has(l)&&p.has(l)?"hybrid":p.has(l)?"vector":"fts";W.push({artifact:S,relevance_score:Math.min(1,h/x),match_type:k})}return W}searchByFtsGlobal(t,e,n,s=0){if(!this.db)return[];let r="";try{const i=P(t);r=i?K(i):""}catch{return[]}if(!r)return[];const a=n?"AND a.kind = ?":"",o=[r,s];n&&o.push(n),o.push(e);try{return this.db.prepare(`
26
+ `).run(e?1:0,t)}recall(t,e,n={}){const s=n.limit??10,r=n.minConfidence??0,a=this.searchByFts(t,e,q,n.kind,r),o=this.searchByTags(t,e,q,n.kind,r),i=new Set,m=[];a.forEach((c,d)=>{i.add(c.id),m.push({artifact:c,inFts:!0,inTag:!1,ftsRank:d})});for(const c of o)if(!i.has(c.id))i.add(c.id),m.push({artifact:c,inFts:!1,inTag:!0});else{const d=m.find(D=>D.artifact.id===c.id);d&&(d.inTag=!0)}return m.filter(({artifact:c})=>c.confidence>=r).map(({artifact:c,inFts:d,inTag:D,ftsRank:p})=>{let N;if(d){const u=1-(p??0)/(a.length+1);N=(D?.7:.55)+.45*u}else N=.4;N*=c.confidence,c.outcome==="positive"&&(N*=1.1);const b=(Date.now()-new Date(c.last_validated_at).getTime())/(1e3*60*60*24),w=Math.max(.5,1-b/180);return N*=w,{artifact:c,relevance_score:Math.min(1,N),match_type:d&&D?"both":d?"fts":"tag"}}).sort((c,d)=>d.relevance_score-c.relevance_score).slice(0,s)}async recallHybrid(t,e,n={}){const s=n.limit??10,r=n.minConfidence??0,a=n.rerank===!0&&ae(),o=Math.min(Math.max(s*2,20),50),i=a?o:s;if(!this.vectorStore)return this.recall(t,e,n);const m=Math.max(50,i*5),_=Date.now(),c=this.searchByFts(t,e,m,n.kind,r),d=Date.now()-_,D=Date.now();let p=[];try{p=(await this.vectorStore.search(e,m,t,["artifact"])).map(E=>({id:E.content_id,s:E.similarity}))}catch{}const N=Date.now()-D;if(p.length===0)return this.recall(t,e,n);const b=Date.now(),w=60,u=Number(process.env.WYRM_RERANK_ALPHA),g=Number.isFinite(u)?Math.max(0,Math.min(1,u)):.7,A=new Map;if(process.env.WYRM_RERANK_FUSION==="rrf")c.forEach((f,E)=>A.set(f.id,(A.get(f.id)??0)+1/(w+E+1))),p.forEach((f,E)=>A.set(f.id,(A.get(f.id)??0)+1/(w+E+1)));else{const f=c.length+1;c.forEach((L,te)=>A.set(L.id,(A.get(L.id)??0)+(1-g)*(1-te/f)));const E=p.map(L=>L.s),R=Math.min(...E),F=Math.max(...E),I=F-R;p.forEach(L=>A.set(L.id,(A.get(L.id)??0)+g*(I>1e-9?(L.s-R)/I:1)))}const O=new Set(c.map(f=>f.id)),C=new Set(p.map(f=>f.id)),$=new Map(c.map(f=>[f.id,f])),y=[...A.entries()].sort((f,E)=>E[1]-f[1]),H=j(),W=G(),l=X(),h=l>0?z(e):null,S=Q(),k=ue(),M=k>0?le(this.db,t,e):null,v=new Date,U=Math.min(y.length,Math.max(i*4,50)),x=[];for(const[f,E]of y.slice(0,U)){const R=$.get(f)??this.get(f)??void 0;if(!R||R.confidence<r||R.needs_review===1||R.supersedes_id!=null||n.kind&&R.kind!==n.kind)continue;const F=R.updated_at??R.created_at;let I=E*V(F,H,W,v);h&&(I*=J(R.created_at,h,l,v)),I*=Z(R.reuse_count,R.reuse_success_count,S),M&&(I*=fe(`${R.problem} ${R.validated_fix??""}`,M,k)),x.push({id:f,s:I,art:R})}x.sort((f,E)=>E.s-f.s);const ee=x[0]?.s??1,T=[];for(const{id:f,s:E,art:R}of x){if(T.length>=i)break;const F=O.has(f)&&C.has(f)?"hybrid":C.has(f)?"vector":"fts";T.push({artifact:R,relevance_score:Math.min(1,E/ee),match_type:F})}const B=Date.now()-b;if(a&&T.length>0){const f=Date.now(),E=await this.rerankResults(e,T);return n.onStats?.({ftsMs:d,vectorMs:N,fusionMs:B,rerankMs:Date.now()-f,candidates:T.length}),E.slice(0,s)}return n.onStats?.({ftsMs:d,vectorMs:N,fusionMs:B,candidates:T.length}),T}async rerankResults(t,e){const n=e.map(i=>({id:i.artifact.id,text:`${i.artifact.problem}${i.artifact.validated_fix?" "+i.artifact.validated_fix:""}`.slice(0,2e3)})),s=await ce(t,n);if(!s)return e;const r=new Map(e.map(i=>[i.artifact.id,i])),a=[],o=s.length;return s.forEach((i,m)=>{const _=r.get(i.id);_&&a.push({..._,relevance_score:o>0?(o-m)/o:_.relevance_score})}),a}async recallHybridGlobal(t,e={}){const n=e.limit??10,s=e.minConfidence??0,r=Math.max(50,n*5),a=this.searchByFtsGlobal(t,r,e.kind,s),o=()=>a.slice(0,n).map((l,h)=>({artifact:l,relevance_score:1-h/(a.length+1),match_type:"fts"}));if(!this.vectorStore)return o();let i=[];try{i=(await this.vectorStore.search(t,r,void 0,["artifact"])).map(h=>({id:h.content_id,s:h.similarity}))}catch{}if(i.length===0)return o();const m=60,_=Number(process.env.WYRM_RERANK_ALPHA),c=Number.isFinite(_)?Math.max(0,Math.min(1,_)):.7,d=new Map;if(process.env.WYRM_RERANK_FUSION==="rrf")a.forEach((l,h)=>d.set(l.id,(d.get(l.id)??0)+1/(m+h+1))),i.forEach((l,h)=>d.set(l.id,(d.get(l.id)??0)+1/(m+h+1)));else{const l=a.length+1;a.forEach((v,U)=>d.set(v.id,(d.get(v.id)??0)+(1-c)*(1-U/l)));const h=i.map(v=>v.s),S=Math.min(...h),k=Math.max(...h),M=k-S;i.forEach(v=>d.set(v.id,(d.get(v.id)??0)+c*(M>1e-9?(v.s-S)/M:1)))}const D=new Set(a.map(l=>l.id)),p=new Set(i.map(l=>l.id)),N=new Map(a.map(l=>[l.id,l])),b=[...d.entries()].sort((l,h)=>h[1]-l[1]),w=j(),u=G(),g=X(),A=g>0?z(t):null,O=Q(),C=new Date,$=Math.min(b.length,Math.max(n*4,50)),y=[];for(const[l,h]of b.slice(0,$)){const S=N.get(l)??this.get(l)??void 0;if(!S||S.confidence<s||S.needs_review===1||S.supersedes_id!=null||e.kind&&S.kind!==e.kind)continue;const k=S.updated_at??S.created_at;let M=h*V(k,w,u,C);A&&(M*=J(S.created_at,A,g,C)),M*=Z(S.reuse_count,S.reuse_success_count,O),y.push({id:l,s:M,art:S})}y.sort((l,h)=>h.s-l.s);const H=y[0]?.s??1,W=[];for(const{id:l,s:h,art:S}of y){if(W.length>=n)break;const k=D.has(l)&&p.has(l)?"hybrid":p.has(l)?"vector":"fts";W.push({artifact:S,relevance_score:Math.min(1,h/H),match_type:k})}return W}searchByFtsGlobal(t,e,n,s=0){if(!this.db)return[];let r="";try{const i=P(t);r=i?K(i):""}catch{return[]}if(!r)return[];const a=n?"AND a.kind = ?":"",o=[r,s];n&&o.push(n),o.push(e);try{return this.db.prepare(`
26
27
  SELECT a.* FROM memory_artifacts a
27
28
  JOIN memory_artifacts_fts ON a.id = memory_artifacts_fts.rowid
28
29
  WHERE memory_artifacts_fts MATCH ?
@@ -57,11 +58,11 @@ import{emitEvent as ne}from"./events.js";import{sanitizeFtsQuery as P,buildFtsMa
57
58
  ${s}
58
59
  ORDER BY confidence DESC, created_at DESC
59
60
  LIMIT ?
60
- `).all(...r)}buildContextBrief(t,e,n={}){const s=n.kinds??["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],r=n.maxItems??me,a=n.minConfidence??_e,o=this.recall(t,e,{limit:r*2,minConfidence:a});o.sort((u,g)=>(Y(u.artifact.problem)>0?1:0)-(Y(g.artifact.problem)>0?1:0));const i=[],_=o.filter(u=>{const g=u.artifact.source_trust;return g==="untrusted"||g==="imported"&&oe(`${u.artifact.problem} ${u.artifact.validated_fix??""}`)?(i.push(u.artifact.id),!1):!0}).filter(u=>s.includes(u.artifact.kind)).slice(0,r),c=new Map;for(const u of _){const g=c.get(u.artifact.kind)??[];g.push(u),c.set(u.artifact.kind,g)}const d={pattern:"\u2705 Proven Patterns",heuristic:"\u{1F4A1} Heuristics",reasoning_trace:"\u{1F9E0} Past Reasoning",lesson:"\u{1F4DA} Lessons Learned",anti_pattern:"\u26A0\uFE0F Anti-Patterns to Avoid"},D=["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],p=[],N=[];let M=0;for(const u of D){if(!s.includes(u))continue;const g=c.get(u)??[];if(!g.length)continue;const A=[];for(const O of g){if(M>=r)break;const C=O.artifact;let y=`${de(C.source_trust)}**Problem:** ${C.problem}`;C.constraints&&(y+=`
61
+ `).all(...r)}buildContextBrief(t,e,n={}){const s=n.kinds??["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],r=n.maxItems??_e,a=n.minConfidence??he,o=this.recall(t,e,{limit:r*2,minConfidence:a});o.sort((u,g)=>(Y(u.artifact.problem)>0?1:0)-(Y(g.artifact.problem)>0?1:0));const i=[],_=o.filter(u=>{const g=u.artifact.source_trust;return g==="untrusted"||g==="imported"&&oe(`${u.artifact.problem} ${u.artifact.validated_fix??""}`)?(i.push(u.artifact.id),!1):!0}).filter(u=>s.includes(u.artifact.kind)).slice(0,r),c=new Map;for(const u of _){const g=c.get(u.artifact.kind)??[];g.push(u),c.set(u.artifact.kind,g)}const d={pattern:"\u2705 Proven Patterns",heuristic:"\u{1F4A1} Heuristics",reasoning_trace:"\u{1F9E0} Past Reasoning",lesson:"\u{1F4DA} Lessons Learned",anti_pattern:"\u26A0\uFE0F Anti-Patterns to Avoid"},D=["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],p=[],N=[];let b=0;for(const u of D){if(!s.includes(u))continue;const g=c.get(u)??[];if(!g.length)continue;const A=[];for(const O of g){if(b>=r)break;const C=O.artifact;let y=`${de(C.source_trust)}**Problem:** ${C.problem}`;C.constraints&&(y+=`
61
62
  _Constraints:_ ${C.constraints}`),C.validated_fix&&(y+=`
62
63
  _Solution:_ ${C.validated_fix}`),C.why_it_worked&&(y+=`
63
64
  _Why it worked:_ ${C.why_it_worked}`),C.outcome==="negative"&&(y+=`
64
- _Note: This approach failed \u2014 avoid it_`),A.push(y),N.push(C.id),M++}A.length&&p.push({heading:d[u],items:A,source:u})}let w="";if(p.length>0){w+=`---
65
+ _Note: This approach failed \u2014 avoid it_`),A.push(y),N.push(C.id),b++}A.length&&p.push({heading:d[u],items:A,source:u})}let w="";if(p.length>0){w+=`---
65
66
  ## \u{F115D} Memory Brief
66
67
  _Relevant past knowledge from Wyrm:_
67
68
 
@@ -89,4 +90,4 @@ _Relevant past knowledge from Wyrm:_
89
90
  ${r?"AND project_id = ?":""}
90
91
  ORDER BY confidence ASC
91
92
  LIMIT 500
92
- `).all(...a);return s||o.length===0?{candidates:o,deleted:0,dryRun:s}:{candidates:o,deleted:this.deleteArtifacts(o.map(i=>i.id)),dryRun:s}}deleteArtifacts(t){if(t.length===0)return 0;const e=t.map(()=>"?").join(",");return this.db.prepare(`DELETE FROM memory_artifacts WHERE id IN (${e}) AND needs_review = 0`).run(...t).changes}}export{Ce as MemoryArtifacts};
93
+ `).all(...a);return s||o.length===0?{candidates:o,deleted:0,dryRun:s}:{candidates:o,deleted:this.deleteArtifacts(o.map(i=>i.id)),dryRun:s}}deleteArtifacts(t){if(t.length===0)return 0;const e=t.map(()=>"?").join(",");return this.db.prepare(`DELETE FROM memory_artifacts WHERE id IN (${e}) AND needs_review = 0`).run(...t).changes}}export{ye as MemoryArtifacts};
@@ -11,16 +11,16 @@ import{createHash as g}from"crypto";import{recordWrite as E}from"./receipts.js";
11
11
  WHERE needs_review = 0 AND supersedes_id IS NULL
12
12
  ORDER BY id
13
13
  `).all()}function x(t,e={}){const r=p(t,e.projectId),n=new Map;for(const s of r){const u=`${s.project_id}:${h(s.problem+`
14
- `+(s.validated_fix??""))}`,a=n.get(u);a?a.push(s):n.set(u,[s])}let i=0,o=0;const d=t.prepare("UPDATE memory_artifacts SET supersedes_id = ?, updated_at = datetime('now') WHERE id = ?");for(const s of n.values()){if(s.length<2)continue;o++;const u=s[0];for(const a of s.slice(1))e.dryRun||(d.run(u.id,a.id),E(t,{tool:"wyrm_metabolize",outcome:"merged",refTable:"mem",refId:a.id,reason:`exact duplicate of mem:${u.id} \u2014 superseded (not deleted; sync-safe)`,source:"metabolize",projectId:a.project_id})),i++}return{merged:i,groups:o}}const R=new Set(["the","a","an","and","or","to","of","in","on","for","with","is","was","are","this","that","it","as","at","by"]);function w(t,e=3){const r=t.toLowerCase().split(/[^a-z0-9]+/).filter(i=>i.length>1&&!R.has(i)),n=new Set;for(let i=0;i+e<=r.length;i++)n.add(r.slice(i,i+e).join(" "));return n}function $(t,e){if(t.size===0||e.size===0)return 0;let r=0;const[n,i]=t.size<=e.size?[t,e]:[e,t];for(const o of n)i.has(o)&&r++;return r/(t.size+e.size-r)}function L(t,e={}){const r=e.threshold??.82,n=e.maxPairs??20,o=p(t,e.projectId).map(a=>({r:a,s:w(`${a.problem} ${a.validated_fix??""}`.slice(0,1500))})).filter(a=>a.s.size>=8);let d=0,s=0;const u=t.prepare("SELECT 1 FROM memory_artifacts WHERE tags LIKE ? ESCAPE '\\' LIMIT 1");for(let a=0;a<o.length&&d<n;a++)for(let m=a+1;m<o.length&&d<n;m++){if(o[a].r.project_id!==o[m].r.project_id)continue;s++;const f=$(o[a].s,o[m].s);if(f<r)continue;const c=o[a].r,l=o[m].r,_="nd:"+h(`${Math.min(c.id,l.id)}:${Math.max(c.id,l.id)}`).slice(0,16);u.get("%"+_.replace(/[%_\\]/g,y=>"\\"+y)+"%")||(e.dryRun||(t.prepare(`
14
+ `+(s.validated_fix??""))}`,a=n.get(u);a?a.push(s):n.set(u,[s])}let i=0,o=0;const d=t.prepare("UPDATE memory_artifacts SET supersedes_id = ?, updated_at = datetime('now') WHERE id = ?");for(const s of n.values()){if(s.length<2)continue;o++;const u=s[0];for(const a of s.slice(1))e.dryRun||(d.run(u.id,a.id),E(t,{tool:"wyrm_metabolize",outcome:"merged",refTable:"mem",refId:a.id,reason:`exact duplicate of mem:${u.id} \u2014 superseded (not deleted; sync-safe)`,source:"metabolize",projectId:a.project_id})),i++}return{merged:i,groups:o}}const R=new Set(["the","a","an","and","or","to","of","in","on","for","with","is","was","are","this","that","it","as","at","by"]);function w(t,e=3){const r=t.toLowerCase().split(/[^a-z0-9]+/).filter(i=>i.length>1&&!R.has(i)),n=new Set;for(let i=0;i+e<=r.length;i++)n.add(r.slice(i,i+e).join(" "));return n}function L(t,e){if(t.size===0||e.size===0)return 0;let r=0;const[n,i]=t.size<=e.size?[t,e]:[e,t];for(const o of n)i.has(o)&&r++;return r/(t.size+e.size-r)}function $(t,e={}){const r=e.threshold??.82,n=e.maxPairs??20,o=p(t,e.projectId).map(a=>({r:a,s:w(`${a.problem} ${a.validated_fix??""}`.slice(0,1500))})).filter(a=>a.s.size>=8);let d=0,s=0;const u=t.prepare("SELECT 1 FROM memory_artifacts WHERE tags LIKE ? ESCAPE '\\' LIMIT 1");for(let a=0;a<o.length&&d<n;a++)for(let m=a+1;m<o.length&&d<n;m++){if(o[a].r.project_id!==o[m].r.project_id)continue;s++;const f=L(o[a].s,o[m].s);if(f<r)continue;const c=o[a].r,l=o[m].r,_="nd:"+h(`${Math.min(c.id,l.id)}:${Math.max(c.id,l.id)}`).slice(0,16);u.get("%"+_.replace(/[%_\\]/g,y=>"\\"+y)+"%")||(e.dryRun||(t.prepare(`
15
15
  INSERT INTO memory_artifacts (project_id, kind, problem, validated_fix, confidence, needs_review, tags, created_by)
16
16
  VALUES (?, 'pattern', ?, ?, ?, 1, ?, 'metabolize')
17
17
  `).run(c.project_id,`MERGE CANDIDATE (similarity ${(f*100).toFixed(0)}%): mem:${c.id} and mem:${l.id} look like the same memory. Approve to keep both; to merge, supersede one via review.`,`mem:${c.id}: ${c.problem.slice(0,160)}
18
18
  ---
19
- mem:${l.id}: ${l.problem.slice(0,160)}`,f,`metabolize,near-dup,${_}`),E(t,{tool:"wyrm_metabolize",outcome:"queued",reason:`near-duplicate candidate mem:${c.id} ~ mem:${l.id} (${(f*100).toFixed(0)}%) \u2014 review to merge`,source:"metabolize",projectId:c.project_id})),d++)}return{queued:d,comparedPairs:s}}function z(t,e={}){const r=e.olderThanDays??60,n=e.factor??.9,i=e.floor??.3,o=t.prepare(`
19
+ mem:${l.id}: ${l.problem.slice(0,160)}`,f,`metabolize,near-dup,${_}`),E(t,{tool:"wyrm_metabolize",outcome:"queued",reason:`near-duplicate candidate mem:${c.id} ~ mem:${l.id} (${(f*100).toFixed(0)}%) \u2014 review to merge`,source:"metabolize",projectId:c.project_id})),d++)}return{queued:d,comparedPairs:s}}function v(t,e={}){const r=e.olderThanDays??60,n=e.factor??.9,i=e.floor??.3,o=t.prepare(`
20
20
  SELECT id FROM memory_artifacts
21
21
  WHERE needs_review = 0 AND supersedes_id IS NULL
22
22
  AND reuse_count = 0
23
23
  AND confidence > ?
24
24
  AND updated_at < datetime('now', ?)
25
25
  AND (problem LIKE '[auto:%' OR created_by LIKE 'bridge:%' OR created_by = 'auto-extract')
26
- `).all(i,`-${r} days`);if(!e.dryRun&&o.length>0){const d=t.prepare("UPDATE memory_artifacts SET confidence = MAX(?, confidence * ?), updated_at = datetime('now') WHERE id = ?");for(const{id:s}of o)d.run(i,n,s)}return{decayed:o.length}}function S(t,e={}){const r=p(t,e.projectId).length,n=x(t,e),i=e.near===!1?{queued:0}:L(t,e),o=z(t,e);return{scanned:r,exactMerged:n.merged,nearCandidatesQueued:i.queued,decayed:o.decayed,dryRun:!!e.dryRun}}function j(t=process.env){const e=(t.WYRM_METABOLIZE??"").toLowerCase();return e==="1"||e==="true"||e==="yes"}function D(t,e){const r=e.trim();if(/^[a-z][a-z0-9_.-]*\s*=\s*(true|false|\d+(\.\d+)?|"[^"]{0,40}"|'[^']{0,40}'|[a-z0-9_.-]{1,40})$/i.test(r))return!0;const n=t.trim().toLowerCase();return!!(r.toLowerCase().startsWith(n)&&/=\s*\S+$/.test(r)&&r.length<80&&!/[.!?]\s/.test(r))}export{z as decayStaleAutoCaptures,D as isConfigShapedTruth,$ as jaccard,x as mergeExactDuplicates,j as metabolizeEnabled,L as queueNearDuplicates,S as runMetabolize,w as shingles};
26
+ `).all(i,`-${r} days`);if(!e.dryRun&&o.length>0){const d=t.prepare("UPDATE memory_artifacts SET confidence = MAX(?, confidence * ?), updated_at = datetime('now') WHERE id = ?");for(const{id:s}of o)d.run(i,n,s)}return{decayed:o.length}}function b(t,e={}){const r=p(t,e.projectId).length,n=x(t,e),i=e.near===!1?{queued:0}:$(t,e),o=v(t,e);return{scanned:r,exactMerged:n.merged,nearCandidatesQueued:i.queued,decayed:o.decayed,dryRun:!!e.dryRun}}function D(t=process.env){const e=(t.WYRM_METABOLIZE??"").toLowerCase();return e==="1"||e==="true"||e==="yes"}function I(t=process.env){const e=(t.WYRM_METABOLIZE??"").toLowerCase();return e!=="0"&&e!=="false"&&e!=="no"&&e!=="off"}function S(t,e){const r=e.trim();if(/^[a-z][a-z0-9_.-]*\s*=\s*(true|false|\d+(\.\d+)?|"[^"]{0,40}"|'[^']{0,40}'|[a-z0-9_.-]{1,40})$/i.test(r))return!0;const n=t.trim().toLowerCase();return!!(r.toLowerCase().startsWith(n)&&/=\s*\S+$/.test(r)&&r.length<80&&!/[.!?]\s/.test(r))}export{v as decayStaleAutoCaptures,S as isConfigShapedTruth,L as jaccard,x as mergeExactDuplicates,I as metabolizeDefaultEnabled,D as metabolizeEnabled,$ as queueNearDuplicates,b as runMetabolize,w as shingles};
@@ -210,7 +210,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
210
210
  timestamp TEXT DEFAULT (datetime('now'))
211
211
  );
212
212
  CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_events(timestamp);
213
- `)}},{version:3,description:"Add visibility and created_by columns for future multi-user support",up:e=>{const t=["sessions","quests","context","data_lake"];for(const E of t){const i=e.prepare(`PRAGMA table_info(${E})`).all().map(T=>T.name);i.includes("visibility")||e.exec(`ALTER TABLE ${E} ADD COLUMN visibility TEXT DEFAULT 'private'`),i.includes("created_by")||e.exec(`ALTER TABLE ${E} ADD COLUMN created_by TEXT DEFAULT 'local'`)}}},{version:4,description:"Knowledge graph \u2014 entities, aliases, relationships with FTS5",up:e=>{e.exec(`
213
+ `)}},{version:3,description:"Add visibility and created_by columns for future multi-user support",up:e=>{const E=["sessions","quests","context","data_lake"];for(const t of E){const i=e.prepare(`PRAGMA table_info(${t})`).all().map(T=>T.name);i.includes("visibility")||e.exec(`ALTER TABLE ${t} ADD COLUMN visibility TEXT DEFAULT 'private'`),i.includes("created_by")||e.exec(`ALTER TABLE ${t} ADD COLUMN created_by TEXT DEFAULT 'local'`)}}},{version:4,description:"Knowledge graph \u2014 entities, aliases, relationships with FTS5",up:e=>{e.exec(`
214
214
  -- Entities: named things that can be linked together
215
215
  CREATE TABLE IF NOT EXISTS entities (
216
216
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -844,8 +844,8 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
844
844
  ON token_savings_log(session_id, timestamp DESC);
845
845
  CREATE INDEX IF NOT EXISTS idx_savings_category
846
846
  ON token_savings_log(category, timestamp DESC);
847
- `);const t=["ground_truths","memory_artifacts","quests","decision_edges","design_references","design_tokens"];for(const E of t)try{if(e.prepare(`PRAGMA table_info(${E})`).all().some(i=>i.name==="cross_project_visibility"))continue;e.exec(`
848
- ALTER TABLE ${E} ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within'
847
+ `);const E=["ground_truths","memory_artifacts","quests","decision_edges","design_references","design_tokens"];for(const t of E)try{if(e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name==="cross_project_visibility"))continue;e.exec(`
848
+ ALTER TABLE ${t} ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within'
849
849
  CHECK (cross_project_visibility IN ('within', 'org', 'public'));
850
850
  `)}catch{}}},{version:15,description:"Live Memory v6.4 \u2014 append-only events log + wyrm_meta (device identity)",up:e=>{e.exec(`
851
851
  -- Node-local KV (stable device id for cross-device event identity, etc.)
@@ -905,13 +905,13 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
905
905
  tokenize='porter unicode61'
906
906
  );
907
907
  INSERT INTO skills_fts(skills_fts) VALUES('rebuild');
908
- `)}},{version:17,description:'Backfill cross_project_visibility on decision_edges (migration 14 named a non-existent "decisions" table)',up:e=>{try{e.prepare("PRAGMA table_info(decision_edges)").all().some(E=>E.name==="cross_project_visibility")||e.exec(`
908
+ `)}},{version:17,description:'Backfill cross_project_visibility on decision_edges (migration 14 named a non-existent "decisions" table)',up:e=>{try{e.prepare("PRAGMA table_info(decision_edges)").all().some(t=>t.name==="cross_project_visibility")||e.exec(`
909
909
  ALTER TABLE decision_edges ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within'
910
910
  CHECK (cross_project_visibility IN ('within', 'org', 'public'));
911
- `)}catch{}}},{version:18,description:"GOD-SKILL SPEC v2 \u2014 skill tier/governs/composes + spec-kit registry (specs table)",up:e=>{const t=e.prepare("PRAGMA table_info(skills)").all(),E=s=>t.some(i=>i.name===s);E("tier")||e.exec(`
911
+ `)}catch{}}},{version:18,description:"GOD-SKILL SPEC v2 \u2014 skill tier/governs/composes + spec-kit registry (specs table)",up:e=>{const E=e.prepare("PRAGMA table_info(skills)").all(),t=s=>E.some(i=>i.name===s);t("tier")||e.exec(`
912
912
  ALTER TABLE skills ADD COLUMN tier TEXT NOT NULL DEFAULT 'atomic'
913
913
  CHECK (tier IN ('atomic', 'mega', 'god'));
914
- `),E("governs")||e.exec("ALTER TABLE skills ADD COLUMN governs TEXT NOT NULL DEFAULT '[]';"),E("composes")||e.exec("ALTER TABLE skills ADD COLUMN composes TEXT NOT NULL DEFAULT '[]';"),e.exec("CREATE INDEX IF NOT EXISTS idx_skills_tier ON skills(tier);"),e.exec(`
914
+ `),t("governs")||e.exec("ALTER TABLE skills ADD COLUMN governs TEXT NOT NULL DEFAULT '[]';"),t("composes")||e.exec("ALTER TABLE skills ADD COLUMN composes TEXT NOT NULL DEFAULT '[]';"),e.exec("CREATE INDEX IF NOT EXISTS idx_skills_tier ON skills(tier);"),e.exec(`
915
915
  CREATE TABLE IF NOT EXISTS specs (
916
916
  id INTEGER PRIMARY KEY AUTOINCREMENT,
917
917
  project_id INTEGER NOT NULL,
@@ -929,7 +929,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
929
929
  `)}},{version:19,description:"Grove sync policy: projects.sync_policy gate (private by default) for cloud + federation isolation",up:e=>{e.prepare("PRAGMA table_info(projects)").all().some(o=>o.name==="sync_policy")||e.exec(`
930
930
  ALTER TABLE projects ADD COLUMN sync_policy TEXT NOT NULL DEFAULT 'private'
931
931
  CHECK (sync_policy IN ('private', 'cloud', 'team'));
932
- `);const E=(o,a)=>{try{return e.prepare(`PRAGMA table_info(${o})`).all().some(r=>r.name===a)}catch{return!1}},s=o=>{try{return e.prepare(o).all().map(a=>a.pid)}catch{return[]}},i=new Set;for(const o of["ground_truths","memory_artifacts","quests","design_tokens","design_references"])if(!(!E(o,"cross_project_visibility")||!E(o,"project_id")))for(const a of s(`SELECT DISTINCT project_id AS pid FROM ${o}
932
+ `);const t=(o,a)=>{try{return e.prepare(`PRAGMA table_info(${o})`).all().some(r=>r.name===a)}catch{return!1}},s=o=>{try{return e.prepare(o).all().map(a=>a.pid)}catch{return[]}},i=new Set;for(const o of["ground_truths","memory_artifacts","quests","design_tokens","design_references"])if(!(!t(o,"cross_project_visibility")||!t(o,"project_id")))for(const a of s(`SELECT DISTINCT project_id AS pid FROM ${o}
933
933
  WHERE cross_project_visibility IN ('org', 'public') AND project_id IS NOT NULL`))i.add(a);const T=e.prepare("UPDATE projects SET sync_policy = 'cloud' WHERE id = ? AND sync_policy = 'private'");for(const o of i)T.run(o)}},{version:20,description:"v7 F2 (T008) \u2014 run-native provenance: runs + run_agents tables, agent_id/run_id attribution on 9 tables, failure_patterns.quarantine_scope",up:e=>{e.exec(`
934
934
  CREATE TABLE IF NOT EXISTS runs (
935
935
  run_id TEXT PRIMARY KEY,
@@ -955,11 +955,11 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
955
955
  FOREIGN KEY (run_id) REFERENCES runs(run_id) ON DELETE CASCADE
956
956
  );
957
957
  CREATE INDEX IF NOT EXISTS idx_run_agents_agent ON run_agents(agent_id);
958
- `);const t=(s,i)=>{try{return e.prepare(`PRAGMA table_info(${s})`).all().some(T=>T.name===i)}catch{return!1}},E=["memory_artifacts","failure_patterns","decision_edges","ground_truths","quests","sessions","quest_claims","events","audit_log"];for(const s of E)t(s,"agent_id")||e.exec(`ALTER TABLE ${s} ADD COLUMN agent_id TEXT;`),t(s,"run_id")||e.exec(`ALTER TABLE ${s} ADD COLUMN run_id TEXT;`);e.exec(`
958
+ `);const E=(s,i)=>{try{return e.prepare(`PRAGMA table_info(${s})`).all().some(T=>T.name===i)}catch{return!1}},t=["memory_artifacts","failure_patterns","decision_edges","ground_truths","quests","sessions","quest_claims","events","audit_log"];for(const s of t)E(s,"agent_id")||e.exec(`ALTER TABLE ${s} ADD COLUMN agent_id TEXT;`),E(s,"run_id")||e.exec(`ALTER TABLE ${s} ADD COLUMN run_id TEXT;`);e.exec(`
959
959
  CREATE INDEX IF NOT EXISTS idx_failure_run ON failure_patterns(run_id) WHERE run_id IS NOT NULL;
960
960
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id) WHERE run_id IS NOT NULL;
961
961
  CREATE INDEX IF NOT EXISTS idx_artifacts_run ON memory_artifacts(run_id) WHERE run_id IS NOT NULL;
962
- `),t("failure_patterns","quarantine_scope")||(e.exec(`
962
+ `),E("failure_patterns","quarantine_scope")||(e.exec(`
963
963
  ALTER TABLE failure_patterns ADD COLUMN quarantine_scope TEXT NOT NULL DEFAULT 'project'
964
964
  CHECK (quarantine_scope IN ('run', 'project', 'global'));
965
965
  `),e.exec("UPDATE failure_patterns SET quarantine_scope = 'global' WHERE project_id IS NULL;")),e.exec("CREATE INDEX IF NOT EXISTS idx_failure_quarantine ON failure_patterns(quarantine_scope, resolved);")}},{version:21,description:"v7 F2 (T015) \u2014 failure_confirmations: distinct-agent confirmation ledger behind run-quarantine auto-promotion",up:e=>{e.exec(`
@@ -990,10 +990,10 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
990
990
  created_at TEXT DEFAULT (datetime('now')),
991
991
  PRIMARY KEY (run_id, role)
992
992
  );
993
- `)}},{version:24,description:"v7 F3 (T029) \u2014 claims/presence hardening: role on quest_claims, run_id/role on agent_presence, run-scoped partial indexes",up:e=>{const t=(E,s)=>{try{return e.prepare(`PRAGMA table_info(${E})`).all().some(i=>i.name===s)}catch{return!1}};t("quest_claims","role")||e.exec("ALTER TABLE quest_claims ADD COLUMN role TEXT;"),t("agent_presence","run_id")||e.exec("ALTER TABLE agent_presence ADD COLUMN run_id TEXT;"),t("agent_presence","role")||e.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT;"),e.exec(`
993
+ `)}},{version:24,description:"v7 F3 (T029) \u2014 claims/presence hardening: role on quest_claims, run_id/role on agent_presence, run-scoped partial indexes",up:e=>{const E=(t,s)=>{try{return e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===s)}catch{return!1}};E("quest_claims","role")||e.exec("ALTER TABLE quest_claims ADD COLUMN role TEXT;"),E("agent_presence","run_id")||e.exec("ALTER TABLE agent_presence ADD COLUMN run_id TEXT;"),E("agent_presence","role")||e.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT;"),e.exec(`
994
994
  CREATE INDEX IF NOT EXISTS idx_quest_claims_run ON quest_claims(run_id) WHERE run_id IS NOT NULL;
995
995
  CREATE INDEX IF NOT EXISTS idx_presence_run ON agent_presence(run_id) WHERE run_id IS NOT NULL;
996
- `)}},{version:25,description:"Skills portability \u2014 store SKILL.md content (+ sha/ts) in the registry and make skills cloud-sync-eligible behind the per-row visibility gate",up:e=>{const t=(E,s)=>{try{return e.prepare(`PRAGMA table_info(${E})`).all().some(i=>i.name===s)}catch{return!1}};t("skills","content")||e.exec("ALTER TABLE skills ADD COLUMN content TEXT;"),t("skills","content_sha256")||e.exec("ALTER TABLE skills ADD COLUMN content_sha256 TEXT;"),t("skills","content_updated_at")||e.exec("ALTER TABLE skills ADD COLUMN content_updated_at TEXT;"),t("skills","cross_project_visibility")||e.exec("ALTER TABLE skills ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within';"),t("skills","is_shared")||e.exec("ALTER TABLE skills ADD COLUMN is_shared INTEGER NOT NULL DEFAULT 0;"),e.exec("CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(cross_project_visibility) WHERE cross_project_visibility != 'within';")}},{version:26,description:"Reverse-bridge rejection tombstones \u2014 remember a once-rejected outside-prose sig so the next sweep never re-queues content the operator already deleted",up:e=>{e.exec(`
996
+ `)}},{version:25,description:"Skills portability \u2014 store SKILL.md content (+ sha/ts) in the registry and make skills cloud-sync-eligible behind the per-row visibility gate",up:e=>{const E=(t,s)=>{try{return e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===s)}catch{return!1}};E("skills","content")||e.exec("ALTER TABLE skills ADD COLUMN content TEXT;"),E("skills","content_sha256")||e.exec("ALTER TABLE skills ADD COLUMN content_sha256 TEXT;"),E("skills","content_updated_at")||e.exec("ALTER TABLE skills ADD COLUMN content_updated_at TEXT;"),E("skills","cross_project_visibility")||e.exec("ALTER TABLE skills ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within';"),E("skills","is_shared")||e.exec("ALTER TABLE skills ADD COLUMN is_shared INTEGER NOT NULL DEFAULT 0;"),e.exec("CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(cross_project_visibility) WHERE cross_project_visibility != 'within';")}},{version:26,description:"Reverse-bridge rejection tombstones \u2014 remember a once-rejected outside-prose sig so the next sweep never re-queues content the operator already deleted",up:e=>{e.exec(`
997
997
  CREATE TABLE IF NOT EXISTS reverse_bridge_tombstones (
998
998
  project_id INTEGER NOT NULL,
999
999
  sig TEXT NOT NULL,
@@ -1024,7 +1024,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
1024
1024
  ON routing_memory(project_id, task_type, ts DESC);
1025
1025
  CREATE INDEX IF NOT EXISTS idx_routing_memory_task
1026
1026
  ON routing_memory(task_type, ts DESC);
1027
- `)}},{version:29,description:"Failure Firewall v2 \u2014 content tier: a normalized signature of the CODE that broke, so the opt-in PreToolUse guard can block an edit that re-introduces a recorded failure pattern (not just an identical target path)",up:e=>{e.prepare("PRAGMA table_info(failure_patterns)").all().some(E=>E.name==="content_sig")||e.exec("ALTER TABLE failure_patterns ADD COLUMN content_sig TEXT;")}},{version:30,description:"Failure Firewall staleness oracle \u2014 failure_anchors: the source files a failure was recorded against (project-relative path + normalized-content sha + stat fast-path fields), so the firewall can tell a still-true block from one whose source has since changed and downgrade it to advisory",up:e=>{e.exec(`
1027
+ `)}},{version:29,description:"Failure Firewall v2 \u2014 content tier: a normalized signature of the CODE that broke, so the opt-in PreToolUse guard can block an edit that re-introduces a recorded failure pattern (not just an identical target path)",up:e=>{e.prepare("PRAGMA table_info(failure_patterns)").all().some(t=>t.name==="content_sig")||e.exec("ALTER TABLE failure_patterns ADD COLUMN content_sig TEXT;")}},{version:30,description:"Failure Firewall staleness oracle \u2014 failure_anchors: the source files a failure was recorded against (project-relative path + normalized-content sha + stat fast-path fields), so the firewall can tell a still-true block from one whose source has since changed and downgrade it to advisory",up:e=>{e.exec(`
1028
1028
  CREATE TABLE IF NOT EXISTS failure_anchors (
1029
1029
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1030
1030
  failure_id INTEGER NOT NULL REFERENCES failure_patterns(id) ON DELETE CASCADE,
@@ -1036,7 +1036,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
1036
1036
  );
1037
1037
  CREATE UNIQUE INDEX IF NOT EXISTS ux_failure_anchors_failure_rel
1038
1038
  ON failure_anchors(failure_id, rel_path);
1039
- `)}},{version:31,description:"FABLE skill tier \u2014 widen skills.tier to (atomic, mega, god, fable): model-vintage master-craft skills authored by Fable-class models during real builds, every law validated by shipping the artifact in the authoring session (fable-skill-spec)",up:e=>{const t=e.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'skills'").get();!t?.sql||t.sql.includes("'fable'")||e.exec(`
1039
+ `)}},{version:31,description:"FABLE skill tier \u2014 widen skills.tier to (atomic, mega, god, fable): model-vintage master-craft skills authored by Fable-class models during real builds, every law validated by shipping the artifact in the authoring session (fable-skill-spec)",up:e=>{const E=e.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'skills'").get();!E?.sql||E.sql.includes("'fable'")||e.exec(`
1040
1040
  CREATE TABLE skills_v31 (
1041
1041
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1042
1042
  name TEXT UNIQUE NOT NULL,
@@ -1113,17 +1113,40 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
1113
1113
  );
1114
1114
  CREATE INDEX IF NOT EXISTS idx_write_ledger_ts ON write_ledger(ts);
1115
1115
  CREATE INDEX IF NOT EXISTS idx_write_ledger_tool ON write_ledger(tool, ts);
1116
- `)}},{version:33,description:"v8 F5 \u2014 source_trust provenance lane on memory artifacts (v8-A5): operator > agent > imported > untrusted. Conservative backfill: bridge:* (the operator's own daily record) \u2192 operator; import:*/imported_from tags \u2192 imported; everything else stays agent. Unknown NEVER backfills to operator.",up:e=>{e.prepare("PRAGMA table_info(memory_artifacts)").all().some(E=>E.name==="source_trust")||e.exec(`
1116
+ `)}},{version:33,description:"v8 F5 \u2014 source_trust provenance lane on memory artifacts (v8-A5): operator > agent > imported > untrusted. Conservative backfill: bridge:* (the operator's own daily record) \u2192 operator; import:*/imported_from tags \u2192 imported; everything else stays agent. Unknown NEVER backfills to operator.",up:e=>{e.prepare("PRAGMA table_info(memory_artifacts)").all().some(t=>t.name==="source_trust")||e.exec(`
1117
1117
  ALTER TABLE memory_artifacts ADD COLUMN source_trust TEXT NOT NULL DEFAULT 'agent'
1118
1118
  CHECK (source_trust IN ('operator', 'agent', 'imported', 'untrusted'));
1119
1119
  `),e.exec(`
1120
1120
  UPDATE memory_artifacts SET source_trust = 'operator' WHERE created_by LIKE 'bridge:%';
1121
1121
  UPDATE memory_artifacts SET source_trust = 'imported'
1122
1122
  WHERE created_by LIKE 'import:%' OR tags LIKE '%imported_from:%';
1123
- `)}}];function d(e){e.exec(`
1123
+ `)}},{version:34,description:"Sovereign-sync GA correctness: AFTER DELETE triggers on the 6 synced tables auto-record sync_tombstones, so a row deleted on one device propagates as a tombstone and does NOT resurrect from another device on the next pull. Exhaustive by construction \u2014 no delete callsite can bypass a trigger (recordTombstone had zero callers, the GA blocker found 2026-07-17).",up:e=>{e.exec(`
1124
+ CREATE TABLE IF NOT EXISTS sync_tombstones (
1125
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1126
+ kind TEXT NOT NULL,
1127
+ row_id TEXT NOT NULL,
1128
+ deleted_at_ms INTEGER NOT NULL,
1129
+ pushed_at_ms INTEGER,
1130
+ UNIQUE(kind, row_id)
1131
+ );
1132
+ CREATE INDEX IF NOT EXISTS idx_sync_tombstones_pending ON sync_tombstones(pushed_at_ms);
1133
+ `);const E=[["ground_truths","truth"],["memory_artifacts","memory_artifact"],["quests","quest"],["design_tokens","design_token"],["design_references","design_reference"],["skills","skill"]];for(const[t,s]of E)e.exec(`
1134
+ CREATE TRIGGER IF NOT EXISTS trg_tombstone_${t} AFTER DELETE ON ${t}
1135
+ BEGIN
1136
+ INSERT OR IGNORE INTO sync_tombstones (kind, row_id, deleted_at_ms)
1137
+ VALUES ('${s}', CAST(OLD.id AS TEXT), CAST(strftime('%s','now') AS INTEGER) * 1000);
1138
+ END;
1139
+ `)}},{version:35,description:"Grove-gate the tombstone triggers (quest #113): a tombstone must record ONLY when the deleted row would have SYNCED, matching the upsert gate exactly (cross_project_visibility IN org/public AND grove sync_policy IN cloud/team). Migration 34 recorded a tombstone for EVERY delete, so a PRIVATE grove's (or unshared skill's) deletion leaked content-free deletion metadata to the cloud as noise. Recreates all 6 triggers gated.",up:e=>{const E=[["ground_truths","truth"],["memory_artifacts","memory_artifact"],["quests","quest"],["design_tokens","design_token"],["design_references","design_reference"],["skills","skill"]];for(const[t,s]of E){e.exec(`DROP TRIGGER IF EXISTS trg_tombstone_${t};`);const i=t==="skills"?"OLD.cross_project_visibility IN ('org','public')":"OLD.cross_project_visibility IN ('org','public') AND (OLD.project_id IS NULL OR (SELECT sync_policy FROM projects WHERE id = OLD.project_id) IN ('cloud','team'))";e.exec(`
1140
+ CREATE TRIGGER trg_tombstone_${t} AFTER DELETE ON ${t}
1141
+ WHEN ${i}
1142
+ BEGIN
1143
+ INSERT OR IGNORE INTO sync_tombstones (kind, row_id, deleted_at_ms)
1144
+ VALUES ('${s}', CAST(OLD.id AS TEXT), CAST(strftime('%s','now') AS INTEGER) * 1000);
1145
+ END;
1146
+ `)}}}];function d(e){e.exec(`
1124
1147
  CREATE TABLE IF NOT EXISTS schema_versions (
1125
1148
  version INTEGER PRIMARY KEY,
1126
1149
  description TEXT NOT NULL,
1127
1150
  applied_at TEXT DEFAULT (datetime('now'))
1128
1151
  );
1129
- `);const t=e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v,E=n.filter(T=>T.version>t).slice().sort((T,o)=>T.version-o.version);if(E.length===0)return[];const s=[],i=e.prepare("INSERT INTO schema_versions (version, description) VALUES (?, ?)");for(const T of E)e.transaction(()=>{T.up(e),i.run(T.version,T.description)})(),s.push(T);return s}function N(e){try{return e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v}catch{return 0}}export{N as getSchemaVersion,n as migrations,d as runMigrations};
1152
+ `);const E=e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v,t=n.filter(T=>T.version>E).slice().sort((T,o)=>T.version-o.version);if(t.length===0)return[];const s=[],i=e.prepare("INSERT INTO schema_versions (version, description) VALUES (?, ?)");for(const T of t)e.transaction(()=>{T.up(e),i.run(T.version,T.description)})(),s.push(T);return s}function N(e){try{return e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v}catch{return 0}}export{N as getSchemaVersion,n as migrations,d as runMigrations};
package/dist/setup.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import{detectClients as g,autoConfigureAll as u,removeFromAll as p,configureSpecific as $,reconfAll as h,getStatusSummary as y,findWyrmServerPath as b,getDefaultDbPath as v,loadWyrmMeta as k,exportServerConfig as P}from"./autoconfig.js";import{c as o,icons as r,BANNER as w,MINI_BANNER as a,printError as f,printSection as i}from"./cli.js";import{runWizard as C}from"./setup-wizard.js";function I(n){const e=n.slice(2);if(e.length===0)return{command:"auto"};const c={command:"auto"};for(let l=0;l<e.length;l++)switch(e[l]){case"--check":case"-c":case"check":case"status":c.command="check";break;case"--remove":case"-r":case"remove":case"uninstall":c.command="remove";break;case"--only":case"-o":case"only":c.command="only",e[l+1]&&(c.clientIds=e[l+1].split(",").map(t=>t.trim()),l++);break;case"--reconf":case"--reconfigure":case"reconf":c.command="reconf";break;case"--list":case"-l":case"list":c.command="list";break;case"--export":case"export":{c.command="export";const t=e[l+1];(t==="stdio"||t==="vscode"||t==="mcp-json")&&(c.exportFormat=t,l++);break}case"--server":case"-s":e[l+1]&&(c.serverPath=e[l+1],l++);break;case"--db":case"-d":e[l+1]&&(c.dbPath=e[l+1],l++);break;case"--wizard":case"wizard":c.command="wizard";break;case"--no-wizard":case"--plain":c.noWizard=!0;break;case"--help":case"-h":case"help":c.command="help";break}return c}function d(n){console.log("");const e=n.filter(s=>s.action==="configured"),c=n.filter(s=>s.action==="updated"),l=n.filter(s=>s.action==="skipped"),t=n.filter(s=>s.action==="failed");for(const s of e)console.log(` ${r.success} ${s.client.icon} ${o.green(s.client.name)} \u2014 ${o.success("configured")}`),s.backup&&console.log(` ${o.dim(`backup \u2192 ${s.backup}`)}`);for(const s of c)console.log(` ${r.sync} ${s.client.icon} ${o.blue(s.client.name)} \u2014 ${o.cyan("updated")}`),s.backup&&console.log(` ${o.dim(`backup \u2192 ${s.backup}`)}`);for(const s of l)console.log(` ${o.dim(` \u25CB ${s.client.icon} ${s.client.name} \u2014 ${s.message}`)}`);for(const s of t)console.log(` ${r.error} ${s.client.icon} ${o.red(s.client.name)} \u2014 ${o.error(s.message)}`);console.log("");const m=e.length+c.length;m>0?(console.log(` ${o.success(`${r.dragon} Wyrm connected to ${m} AI client(s)`)}`),console.log(` ${o.dim("Switch AIs anytime \u2014 run wyrm-setup again to reconnect")}`)):t.length>0?console.log(` ${o.error("Some configurations failed. Check errors above.")}`):console.log(` ${o.dim("No AI clients detected. Install one and try again.")}`),console.log("")}function A(){console.log(a),console.log(""),console.log(`${o.bold("Usage:")} wyrm-setup ${o.dim("[command] [options]")}`),console.log(""),console.log(`${o.bold("Commands:")}`),console.log(` ${o.cyan("(no args)")} Auto-detect and configure all AI clients`),console.log(` ${o.cyan("check")} Show current configuration status`),console.log(` ${o.cyan("list")} List all supported AI clients`),console.log(` ${o.cyan("remove")} Remove Wyrm from all AI clients`),console.log(` ${o.cyan("reconf")} Re-configure previously configured clients`),console.log(` ${o.cyan("only X,Y")} Configure specific clients only`),console.log(` ${o.cyan("export [fmt]")} Print a portable config (fmt: mcp-json|vscode|stdio) for any MCP host`),console.log(` ${o.cyan("help")} Show this help message`),console.log(""),console.log(`${o.bold("Options:")}`),console.log(` ${o.cyan("--server P")} Override Wyrm MCP server path`),console.log(` ${o.cyan("--db P")} Override Wyrm database path`),console.log(""),console.log(`${o.bold("Client IDs:")}`);const n=g();for(const e of n)console.log(` ${e.icon} ${o.cyan(e.id.padEnd(18))} ${e.name}`);console.log(""),console.log(`${o.bold("Examples:")}`),console.log(` wyrm-setup ${o.dim("# Auto-configure everything")}`),console.log(` wyrm-setup check ${o.dim("# See what's configured")}`),console.log(` wyrm-setup only vscode-copilot,cursor ${o.dim("# Only VS Code + Cursor")}`),console.log(` wyrm-setup --server /path/to/wyrm/dist/index.js ${o.dim("# Custom server path")}`),console.log(` wyrm-setup remove ${o.dim("# Remove from all clients")}`),console.log(` wyrm-setup export stdio > wyrm.json ${o.dim("# Portable config for any MCP host")}`),console.log("")}function S(){console.log(a),console.log(""),i("Supported AI Clients"),console.log("");const n=g();for(const e of n){const c=e.detected?e.configured?o.success("\u25CF connected"):o.yellow("\u25D0 available"):o.dim("\u25CB not found"),l=e.version?` ${o.dim(`v${e.version}`)}`:"";console.log(` ${e.icon} ${o.bold(e.name.padEnd(20))} ${o.cyan(e.id.padEnd(18))} ${c}${l}`),console.log(` ${o.dim(`Config: ${e.configPath}`)}`)}console.log("")}async function x(){const n=I(process.argv);if(n.command==="wizard"||n.command==="auto"&&!n.noWizard){if(await C({serverPath:n.serverPath,dbPath:n.dbPath}))return;n.command==="wizard"&&(n.command="auto")}switch(n.command){case"help":A();break;case"check":{console.log(a),console.log(""),console.log(y());break}case"list":S();break;case"auto":{console.log(w),i("Auto-Configure"),console.log("");const e=n.serverPath||b(),c=n.dbPath||v();console.log(` ${r.sword} Server: ${o.cyan(e)}`),console.log(` ${r.treasure} DB: ${o.cyan(c)}`),console.log(""),console.log(` ${o.dim("Scanning for AI clients...")}`);const l=u({serverPath:e,dbPath:c});d(l);break}case"only":{(!n.clientIds||n.clientIds.length===0)&&(f("No client IDs specified. Use: wyrm-setup only vscode-copilot,cursor"),process.exit(1)),console.log(a),i(`Configure: ${n.clientIds.join(", ")}`);const e=$(n.clientIds,{serverPath:n.serverPath,dbPath:n.dbPath});d(e);break}case"remove":{console.log(a),i("Remove Wyrm from AI Clients"),console.log(""),console.log(` ${o.warning("Removing Wyrm configuration from all AI clients...")}`);const e=p(),c=e.filter(t=>t.action==="configured"),l=e.filter(t=>t.action==="skipped");for(const t of c)console.log(` ${r.check} ${t.client.icon} ${t.client.name} \u2014 removed`),t.backup&&console.log(` ${o.dim(`backup \u2192 ${t.backup}`)}`);for(const t of l)console.log(` ${o.dim(` \u25CB ${t.client.icon} ${t.client.name} \u2014 ${t.message}`)}`);console.log(""),c.length>0&&console.log(` ${o.success(`Removed from ${c.length} client(s). Run wyrm-setup to reconnect.`)}`),console.log("");break}case"reconf":{console.log(a),i("Re-Configure");const e=k();console.log(e?` ${o.dim(`Restoring config for: ${e.configuredClients.join(", ")}`)}`:` ${o.dim("No previous config found \u2014 running full auto-configure")}`);const c=h();d(c);break}case"export":{const e=n.exportFormat??"mcp-json",c=P(e,{serverPath:n.serverPath,dbPath:n.dbPath});process.stderr.write(`${o.dim(`# Wyrm MCP config (${e}). Paste into any MCP-compatible host.`)}
3
- `),process.stdout.write(JSON.stringify(c,null,2)+`
4
- `);break}}}x().catch(n=>{f(`Setup failed: ${n}`),process.exit(1)});
2
+ import{detectClients as m,autoConfigureAll as u,removeFromAll as p,configureSpecific as $,reconfAll as y,getStatusSummary as h,findWyrmServerPath as b,getDefaultDbPath as v,loadWyrmMeta as k,exportServerConfig as P}from"./autoconfig.js";import{c as o,icons as r,BANNER as C,MINI_BANNER as i,printError as f,printSection as a}from"./cli.js";function w(c){const e=c.slice(2);if(e.length===0)return{command:"auto"};const n={command:"auto"};for(let l=0;l<e.length;l++)switch(e[l]){case"--check":case"-c":case"check":case"status":n.command="check";break;case"--remove":case"-r":case"remove":case"uninstall":n.command="remove";break;case"--only":case"-o":case"only":n.command="only",e[l+1]&&(n.clientIds=e[l+1].split(",").map(t=>t.trim()),l++);break;case"--reconf":case"--reconfigure":case"reconf":n.command="reconf";break;case"--list":case"-l":case"list":n.command="list";break;case"--export":case"export":{n.command="export";const t=e[l+1];(t==="stdio"||t==="vscode"||t==="mcp-json")&&(n.exportFormat=t,l++);break}case"--server":case"-s":e[l+1]&&(n.serverPath=e[l+1],l++);break;case"--db":case"-d":e[l+1]&&(n.dbPath=e[l+1],l++);break;case"--help":case"-h":case"help":n.command="help";break}return n}function g(c){console.log("");const e=c.filter(s=>s.action==="configured"),n=c.filter(s=>s.action==="updated"),l=c.filter(s=>s.action==="skipped"),t=c.filter(s=>s.action==="failed");for(const s of e)console.log(` ${r.success} ${s.client.icon} ${o.green(s.client.name)} \u2014 ${o.success("configured")}`),s.backup&&console.log(` ${o.dim(`backup \u2192 ${s.backup}`)}`);for(const s of n)console.log(` ${r.sync} ${s.client.icon} ${o.blue(s.client.name)} \u2014 ${o.cyan("updated")}`),s.backup&&console.log(` ${o.dim(`backup \u2192 ${s.backup}`)}`);for(const s of l)console.log(` ${o.dim(` \u25CB ${s.client.icon} ${s.client.name} \u2014 ${s.message}`)}`);for(const s of t)console.log(` ${r.error} ${s.client.icon} ${o.red(s.client.name)} \u2014 ${o.error(s.message)}`);console.log("");const d=e.length+n.length;d>0?(console.log(` ${o.success(`${r.dragon} Wyrm connected to ${d} AI client(s)`)}`),console.log(` ${o.dim("Switch AIs anytime \u2014 run wyrm-setup again to reconnect")}`)):t.length>0?console.log(` ${o.error("Some configurations failed. Check errors above.")}`):console.log(` ${o.dim("No AI clients detected. Install one and try again.")}`),console.log("")}function I(){console.log(i),console.log(""),console.log(`${o.bold("Usage:")} wyrm-setup ${o.dim("[command] [options]")}`),console.log(""),console.log(`${o.bold("Commands:")}`),console.log(` ${o.cyan("(no args)")} Auto-detect and configure all AI clients`),console.log(` ${o.cyan("check")} Show current configuration status`),console.log(` ${o.cyan("list")} List all supported AI clients`),console.log(` ${o.cyan("remove")} Remove Wyrm from all AI clients`),console.log(` ${o.cyan("reconf")} Re-configure previously configured clients`),console.log(` ${o.cyan("only X,Y")} Configure specific clients only`),console.log(` ${o.cyan("export [fmt]")} Print a portable config (fmt: mcp-json|vscode|stdio) for any MCP host`),console.log(` ${o.cyan("help")} Show this help message`),console.log(""),console.log(`${o.bold("Options:")}`),console.log(` ${o.cyan("--server P")} Override Wyrm MCP server path`),console.log(` ${o.cyan("--db P")} Override Wyrm database path`),console.log(""),console.log(`${o.bold("Client IDs:")}`);const c=m();for(const e of c)console.log(` ${e.icon} ${o.cyan(e.id.padEnd(18))} ${e.name}`);console.log(""),console.log(`${o.bold("Examples:")}`),console.log(` wyrm-setup ${o.dim("# Auto-configure everything")}`),console.log(` wyrm-setup check ${o.dim("# See what's configured")}`),console.log(` wyrm-setup only vscode-copilot,cursor ${o.dim("# Only VS Code + Cursor")}`),console.log(` wyrm-setup --server /path/to/wyrm/dist/index.js ${o.dim("# Custom server path")}`),console.log(` wyrm-setup remove ${o.dim("# Remove from all clients")}`),console.log(` wyrm-setup export stdio > wyrm.json ${o.dim("# Portable config for any MCP host")}`),console.log("")}function A(){console.log(i),console.log(""),a("Supported AI Clients"),console.log("");const c=m();for(const e of c){const n=e.detected?e.configured?o.success("\u25CF connected"):o.yellow("\u25D0 available"):o.dim("\u25CB not found"),l=e.version?` ${o.dim(`v${e.version}`)}`:"";console.log(` ${e.icon} ${o.bold(e.name.padEnd(20))} ${o.cyan(e.id.padEnd(18))} ${n}${l}`),console.log(` ${o.dim(`Config: ${e.configPath}`)}`)}console.log("")}async function S(){const c=w(process.argv);switch(c.command){case"help":I();break;case"check":{console.log(i),console.log(""),console.log(h());break}case"list":A();break;case"auto":{console.log(C),a("Auto-Configure"),console.log("");const e=c.serverPath||b(),n=c.dbPath||v();console.log(` ${r.sword} Server: ${o.cyan(e)}`),console.log(` ${r.treasure} DB: ${o.cyan(n)}`),console.log(""),console.log(` ${o.dim("Scanning for AI clients...")}`);const l=u({serverPath:e,dbPath:n});g(l);break}case"only":{(!c.clientIds||c.clientIds.length===0)&&(f("No client IDs specified. Use: wyrm-setup only vscode-copilot,cursor"),process.exit(1)),console.log(i),a(`Configure: ${c.clientIds.join(", ")}`);const e=$(c.clientIds,{serverPath:c.serverPath,dbPath:c.dbPath});g(e);break}case"remove":{console.log(i),a("Remove Wyrm from AI Clients"),console.log(""),console.log(` ${o.warning("Removing Wyrm configuration from all AI clients...")}`);const e=p(),n=e.filter(t=>t.action==="configured"),l=e.filter(t=>t.action==="skipped");for(const t of n)console.log(` ${r.check} ${t.client.icon} ${t.client.name} \u2014 removed`),t.backup&&console.log(` ${o.dim(`backup \u2192 ${t.backup}`)}`);for(const t of l)console.log(` ${o.dim(` \u25CB ${t.client.icon} ${t.client.name} \u2014 ${t.message}`)}`);console.log(""),n.length>0&&console.log(` ${o.success(`Removed from ${n.length} client(s). Run wyrm-setup to reconnect.`)}`),console.log("");break}case"reconf":{console.log(i),a("Re-Configure");const e=k();console.log(e?` ${o.dim(`Restoring config for: ${e.configuredClients.join(", ")}`)}`:` ${o.dim("No previous config found \u2014 running full auto-configure")}`);const n=y();g(n);break}case"export":{const e=c.exportFormat??"mcp-json",n=P(e,{serverPath:c.serverPath,dbPath:c.dbPath});process.stderr.write(`${o.dim(`# Wyrm MCP config (${e}). Paste into any MCP-compatible host.`)}
3
+ `),process.stdout.write(JSON.stringify(n,null,2)+`
4
+ `);break}}}S().catch(c=>{f(`Setup failed: ${c}`),process.exit(1)});
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v2",
3
- "version": "8.0.1",
3
+ "version": "8.1.0",
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.0.1",
3
+ "version": "8.1.0",
4
4
  "tools": [
5
5
  {
6
6
  "name": "wyrm_recall",