wyrm-mcp 8.0.1 → 8.2.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.
package/dist/cloud/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import{randomBytes as D}from"node:crypto";import{writeFileSync as I}from"node:fs";import{hostname as N}from"node:os";import{resolve as P}from"node:path";import{createInterface as A}from"node:readline";import{CloudClient as a,CloudError as k,DEFAULT_BASE as p,SESSION_FILE as f,loadSession as d,saveSession as S,clearSession as b}from"./client.js";import{getMasterKey as R,keyExists as h,keyFilePath as g}from"./crypto.js";import{computeMachineFp as E,classifySession as O}from"./machine-id.js";import{runSync as L,CopiedSessionError as W}from"./sync-engine.js";import{initializeLicense as F,hasFeature as M,getTier as B}from"../license.js";function T(){F(),M("cloud_backup")||(console.error("\u{F115D} Wyrm Cloud sync requires a Pro license or higher."),console.error(` Current tier: ${B()}`),console.error(" Upgrade: https://ghosts.lk/wyrm"),console.error(" Already paid? Activate your license (wyrm_license tool, or place"),console.error(" the signed JSON at ~/.wyrm/license.json) and retry."),process.exit(1))}function w(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KiB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MiB`:`${(e/(1024*1024*1024)).toFixed(2)} GiB`}function G(e){return e?new Date(e).toISOString().replace("T"," ").slice(0,16)+" UTC":"\u2014"}async function ie(e){const n=e[0],s=e.slice(1);try{switch(n){case void 0:case"help":case"--help":case"-h":return U();case"login":return await j(s);case"logout":return await K();case"status":return await H();case"doctor":return Y();case"devices":return await q(s);case"sync":return await z(s);case"export":return await J(s);case"delete":return await Q(s);case"recovery":return await V(s);default:console.error(`Unknown subcommand: wyrm cloud ${n}`),console.error("Try `wyrm cloud help`"),process.exit(1)}}catch(o){if(o instanceof W&&process.exit(1),o instanceof k)o.status===401?console.error("\u2718 Not authenticated. Run `wyrm cloud login`."):o.status===403?console.error(`\u2718 Forbidden: ${o.message}`):o.status===0?(console.error(`\u2718 ${o.message}`),console.error(" Check your internet connection. If wyrm.ghosts.lk is unreachable, see status at https://www.cloudflarestatus.com")):o.status===501?console.error(`\u2718 Service not configured: ${o.message}`):console.error(`\u2718 ${o.message} (HTTP ${o.status})`);else if(o instanceof Error){const r=o.message;r.includes("Not logged in")?console.error("\u2718 Not logged in. Run `wyrm cloud login`."):r.includes("Wyrm Cloud key")?console.error(`\u2718 Encryption-key error: ${r}`):console.error(`\u2718 ${r}`)}else console.error("\u2718 Unexpected error:",o);process.exit(1)}}function U(){console.log(`\u{F115D} wyrm cloud \u2014 sync your Wyrm memory across devices
1
+ import{randomBytes as I}from"node:crypto";import{writeFileSync as N}from"node:fs";import{hostname as P}from"node:os";import{resolve as R,dirname as L,join as A}from"node:path";import{fileURLToPath as O}from"node:url";import{createInterface as W}from"node:readline";import{CloudClient as d,CloudError as b,DEFAULT_BASE as p,SESSION_FILE as f,loadSession as i,saveSession as _,clearSession as E}from"./client.js";import{getMasterKey as M,keyExists as h,keyFilePath as g}from"./crypto.js";import{computeMachineFp as x,classifySession as F}from"./machine-id.js";import{runSync as B,CopiedSessionError as U}from"./sync-engine.js";import{SovereignSyncManager as j}from"./sync-daemon.js";import{initializeLicense as G,hasFeature as H,getTier as K}from"../license.js";function w(){G(),H("cloud_backup")||(console.error("\u{F115D} Wyrm Cloud sync requires a Pro license or higher."),console.error(` Current tier: ${K()}`),console.error(" Upgrade: https://ghosts.lk/wyrm"),console.error(" Already paid? Activate your license (wyrm_license tool, or place"),console.error(" the signed JSON at ~/.wyrm/license.json) and retry."),process.exit(1))}function v(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KiB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MiB`:`${(e/(1024*1024*1024)).toFixed(2)} GiB`}function $(e){return e?new Date(e).toISOString().replace("T"," ").slice(0,16)+" UTC":"\u2014"}async function fe(e){const t=e[0],n=e.slice(1);try{switch(t){case void 0:case"help":case"--help":case"-h":return Y();case"login":return await q(n);case"logout":return await z();case"status":return await J();case"doctor":return Q();case"devices":return await V(n);case"sync":return await X(n);case"daemon":return await ee(n);case"export":return await oe(n);case"delete":return await se(n);case"recovery":return await ne(n);default:console.error(`Unknown subcommand: wyrm cloud ${t}`),console.error("Try `wyrm cloud help`"),process.exit(1)}}catch(o){if(o instanceof U&&process.exit(1),o instanceof b)o.status===401?console.error("\u2718 Not authenticated. Run `wyrm cloud login`."):o.status===403?console.error(`\u2718 Forbidden: ${o.message}`):o.status===0?(console.error(`\u2718 ${o.message}`),console.error(" Check your internet connection. If wyrm.ghosts.lk is unreachable, see status at https://www.cloudflarestatus.com")):o.status===501?console.error(`\u2718 Service not configured: ${o.message}`):console.error(`\u2718 ${o.message} (HTTP ${o.status})`);else if(o instanceof Error){const r=o.message;r.includes("Not logged in")?console.error("\u2718 Not logged in. Run `wyrm cloud login`."):r.includes("Wyrm Cloud key")?console.error(`\u2718 Encryption-key error: ${r}`):console.error(`\u2718 ${r}`)}else console.error("\u2718 Unexpected error:",o);process.exit(1)}}function Y(){console.log(`\u{F115D} wyrm cloud \u2014 sync your Wyrm memory across devices
2
2
 
3
3
  USAGE:
4
4
  wyrm cloud <subcommand> [args]
@@ -15,6 +15,10 @@ SUBCOMMANDS:
15
15
  visibility opt-in). Skips telemetry / FTS / logs.
16
16
  --force proceeds past a copied-session warning
17
17
  (or set WYRM_ALLOW_COPIED_SESSION=1).
18
+ daemon <start|stop|restart|status> [--interval=<min>]
19
+ Hands-off sovereign sync: run 'sync' every interval
20
+ (default 10 min) as a background daemon. Logs to
21
+ ~/.wyrm/wyrm-sovereign-sync.log.
18
22
  doctor Diagnose this device's cloud identity (device_id,
19
23
  machine fingerprint match, copied-session check).
20
24
  export [path] Download full account dump (deltas stay encrypted).
@@ -32,6 +36,6 @@ CONSTITUTION:
32
36
 
33
37
  DOMAIN:
34
38
  https://wyrm.ghosts.lk
35
- `)}async function j(e){d()&&(console.error("Already logged in. Run `wyrm cloud logout` first to switch accounts."),process.exit(1));const n=new a(p),s=D(32).toString("hex");let o;try{o=await n.cliInit(s)}catch(t){console.error(`\u2718 Could not start login: ${t instanceof Error?t.message:t}`),process.exit(1)}console.log(""),console.log("\u{F115D} Sign in to Wyrm Cloud"),console.log(""),console.log(" 1. Open this URL on any device:"),console.log(` ${o.browser_url}`),console.log(""),console.log(" 2. Type this code when prompted:"),console.log(` ${o.code}`),console.log(""),console.log(" 3. Sign in with Google or GitHub."),console.log(""),console.log(` Waiting (expires in ${Math.round(o.expires_in_sec/60)} min)\u2026`);const l=Date.now()+o.expires_in_sec*1e3;let c,i;for(;Date.now()<l;){await new Promise(t=>{setTimeout(t,3e3)});try{const t=await n.cliPoll(s);if(t.ready&&t.session){c=t.session,i=t.account_id;break}}catch(t){console.error(`
36
- \u2718 Login failed: ${t instanceof Error?t.message:t}`),process.exit(1)}}(!c||!i)&&(console.error("\n\u2718 Login timed out. Run `wyrm cloud login` again."),process.exit(1)),S({base:p,session:c,account_id:i,created_at:Date.now()});const u=new a(p,c);let y="";try{y=(await u.me()).account.email}catch{}const m=`${N()} (${process.platform})`;let v;try{v=(await u.registerDevice(m)).device.id;const $=d();$&&S({...$,email:y,device_id:v,device_name:m,machine_fp:E()})}catch(t){console.error(`
37
- \u26A0 Logged in but could not register device: ${t instanceof Error?t.message:t}`),console.error(" Re-run `wyrm cloud login` to retry.");return}console.log(""),console.log(`\u2713 Signed in as ${y}`),console.log(`\u2713 Device registered: ${m}`),console.log(`\u2713 Session saved to ${f}`);const C=!h();R(),C?(console.log(`\u2713 Encryption key generated at ${g()} (0600)`),console.log(""),console.log(" \u26A0 Back up the key file. Losing it means losing access to your synced data."),console.log(" The Wyrm Cloud server CANNOT recover it \u2014 that's the operator-owns-data"),console.log(" guarantee. Run `wyrm cloud recovery show` now to get a 24-word phrase,"),console.log(" or copy the key file to a password manager / encrypted backup.")):console.log(`\u2713 Encryption key reused from ${g()}`),console.log(""),console.log("Next: `wyrm cloud sync` to push your memory.")}async function K(){const e=d();if(!e){console.log("Not logged in.");return}try{await new a(e.base,e.session).logout(),console.log("\u2713 Session revoked server-side.")}catch(n){console.error(`\u26A0 Could not revoke server-side: ${n instanceof Error?n.message:n}`),console.error(" Local session cleared anyway.")}b(),console.log(`\u2713 Local session removed (${f}).`),console.log(" Note: ~/.wyrm/cloud.key kept. Delete manually if you want a clean slate.")}async function H(){const e=d();if(!e){console.log("Not logged in. Run `wyrm cloud login`.");return}const n=new a(e.base,e.session);try{const s=await n.me(),o=await n.syncStatus();console.log(""),console.log(`\u{F115D} Wyrm Cloud \u2014 ${s.account.email}`),console.log(""),console.log(` Tier: ${o.tier}`),console.log(` Storage: ${w(o.storage.used_bytes)} / ${w(o.storage.limit_bytes)} (${o.storage.percent}%)`),console.log(` Deltas: ${o.deltas}`),console.log(` Devices: ${o.devices}`),console.log(` Providers:${s.identities.map(r=>" "+r.provider).join(",")}`),console.log(""),console.log(` This machine: ${e.device_name??"(not registered)"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none)"}`),console.log(` Session age: ${Math.round((Date.now()-e.created_at)/(1440*60*1e3))} day(s)`),x(e),console.log(""),h()?o.deltas>0&&(console.log(` Key: ${g()} (0600)`),console.log(` \u26A0 Back this file up. Losing it = losing access to your ${o.deltas} deltas.`),console.log("")):(console.log(" \u26A0 ENCRYPTION KEY MISSING"),console.log(` Expected at: ${g()}`),console.log(" Without this file, deltas already pushed cannot be decrypted."),console.log(" If you have a backup, restore it before running sync."),console.log(""))}catch(s){throw s instanceof k&&s.status===401&&(console.error("Session expired or invalid. Run `wyrm cloud login` again."),process.exit(1)),s}}function x(e){const n=O(e.machine_fp);n.state==="match"?console.log(" Identity: \u2713 machine fingerprint matches (not a copied session)"):n.state==="adopt"?console.log(" Identity: \u2014 no fingerprint yet (pre-7.0.3 session; adopted on next sync)"):(console.log(" Identity: \u26A0 FINGERPRINT MISMATCH \u2014 this session looks COPIED"),console.log(` stored ${n.stored.slice(0,12)}\u2026 vs this machine ${n.current.slice(0,12)}\u2026`),console.log(" device_id collision \u2192 cross-device pull returns 0 silently."),console.log(" Fix: rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json (KEEP cloud.key),"),console.log(" then `wyrm cloud login`. (Or `wyrm cloud sync --force` if intended.)"))}function Y(){const e=d();if(console.log(""),console.log("\u{F115D} Wyrm Cloud \u2014 device identity doctor"),console.log(""),!e){console.log(" Not logged in. Run `wyrm cloud login`."),console.log("");return}console.log(` Account: ${e.email??e.account_id.slice(0,8)+"\u2026"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none \u2014 re-run `wyrm cloud login`)"}`),console.log(` Server: ${e.base}`),console.log(` Stored fp: ${e.machine_fp?e.machine_fp.slice(0,12)+"\u2026":"(none \u2014 pre-7.0.3 session)"}`),console.log(` This box fp: ${E().slice(0,12)}\u2026`),x(e),console.log(""),console.log(" Only ~/.wyrm/cloud.key is meant to be shared across your machines."),console.log(" cloud.json + cloud-cursor.json + machine-id are per-device."),console.log("")}async function q(e){const n=e[0],s=a.fromSession();if(n==="revoke"){const c=e[1];c||(console.error("Usage: wyrm cloud devices revoke <device-id>"),process.exit(1)),await s.revokeDevice(c),console.log(`\u2713 Device ${c.slice(0,8)}\u2026 revoked.`);return}const{devices:o}=await s.listDevices(),l=d()?.device_id;console.log(""),console.log(`\u{F115D} Devices (${o.length})`),console.log("");for(const c of o){const i=c.id===l?"\u2605":" ",u=c.revoked?"(revoked)":"";console.log(` ${i} ${c.id.slice(0,8)}\u2026 ${c.name.padEnd(30)} last_seen=${G(c.last_seen_at)} ${u}`)}console.log(""),console.log(" \u2605 = this machine"),console.log(" Revoke: wyrm cloud devices revoke <device-id>"),console.log("")}async function z(e){T();const n=e.includes("--dry-run"),s=e.includes("--all"),o=e.includes("--force");h()||(console.error("\u26A0 Encryption key not found at",g()),console.error(" A new key will be generated, but it will NOT decrypt deltas"),console.error(" pushed by other devices using a different key. If you have a backup,"),console.error(" restore it to ~/.wyrm/cloud.key (0600) before running sync."),console.error(" Continuing in 5 seconds \u2014 press Ctrl+C to abort."),await new Promise(l=>{setTimeout(l,5e3)})),s&&(console.log("\u{1F310} --all mode: syncing every semantic table (ignoring visibility flags)."),console.log(" Skipped: per-device logs, FTS shadows, sync internals."),console.log(""));const r=await L({dryRun:n,all:s,force:o});if(console.log(""),console.log("\u{F115D} Sync complete"),console.log(""),console.log(` Pushed: ${r.pushed} row(s)${n?" (dry run)":""}`),console.log(` Pulled: ${r.pulled} row(s)`),console.log(` Deleted: ${r.deleted_local} row(s) (peer tombstones)`),r.errors.length>0){console.log(""),console.log(" Errors:");for(const l of r.errors)console.log(` - ${l}`)}console.log("")}async function J(e){T();const n=a.fromSession(),s=e.find(c=>!c.startsWith("-")),o=P(s??`wyrm-cloud-export-${new Date().toISOString().slice(0,10)}.json`);console.log(" Fetching account export\u2026");const r=await n.accountExport(),l=JSON.stringify(r,null,2);I(o,l,{mode:384}),console.log(""),console.log(`\u2713 Exported to ${o}`),console.log(""),console.log(` Account: ${r.account.email} (${r.account.id.slice(0,8)}\u2026)`),console.log(` Devices: ${r.devices.length}`),console.log(` Identities: ${r.identities.length}`),console.log(` Orgs: ${r.orgs.length}`),console.log(` Deltas: ${r.deltas.length}`),console.log(` Size: ${w(l.length)}`),console.log(""),console.log(" Deltas are still AES-256-GCM ciphertext. To decrypt them you"),console.log(` need the key at ${g()}. Back that up alongside this file.`),console.log("")}async function Q(e){e.includes("--confirm")||(console.error("\u2718 This permanently deletes your Wyrm Cloud account."),console.error(" Re-run with --confirm to proceed:"),console.error(" wyrm cloud delete --confirm"),process.exit(1));const s=d();s||(console.error("\u2718 Not logged in. Nothing to delete."),process.exit(1)),console.log(""),console.log("\u26A0 You are about to PERMANENTLY DELETE your Wyrm Cloud account."),console.log(""),console.log(` Account: ${s.email??s.account_id}`),console.log(` Server: ${s.base}`),console.log(""),console.log(" This wipes all encrypted blobs, devices, identities, and org"),console.log(" memberships you own. The local ~/.wyrm/cloud.key is kept so"),console.log(" any backups remain decryptable. This action CANNOT be undone."),console.log(""),(await _(' Type "DELETE my account" to confirm: ')).trim()!=="DELETE my account"&&(console.error("\u2718 Confirmation phrase did not match. Aborting."),process.exit(1)),await new a(s.base,s.session).accountDelete(),b(),console.log(""),console.log("\u2713 Account deleted on server."),console.log(`\u2713 Local session removed (${f}).`),console.log(` Kept: ${g()} (in case you need to decrypt an old export).`),console.log("")}function _(e){return new Promise(n=>{const s=A({input:process.stdin,output:process.stdout});s.question(e,o=>{s.close(),n(o)})})}async function V(e){const n=e[0];if(n==="show"){const{keyToMnemonic:s}=await import("./recovery.js"),r=s().split(" ");console.log(""),console.log("\u{1F511} Wyrm Cloud recovery phrase (BIP39, 24 words)"),console.log("");for(let l=0;l<4;l++){const c=r.slice(l*6,l*6+6).map((i,u)=>`${String(l*6+u+1).padStart(2)}. ${i.padEnd(10)}`).join(" ");console.log(` ${c}`)}console.log(""),console.log(" Write these down on PAPER and keep them somewhere safe."),console.log(" Anyone with this phrase can decrypt your synced Wyrm memory."),console.log(" Wyrm Cloud has NO COPY of this. Lose it = lose your data."),console.log("");return}if(n==="restore"){const s=e.includes("--overwrite");console.log(""),console.log("\u{1F511} Restore master key from BIP39 recovery phrase"),console.log(""),console.log(" Enter your 24 words separated by spaces (case-insensitive)."),console.log(" Press Enter when done."),console.log("");const o=await _(" > "),{mnemonicToKey:r}=await import("./recovery.js"),{path:l,overwrote:c}=r(o,{overwrite:s});console.log(""),console.log(`\u2713 Master key restored to ${l} (0600)`),c&&console.log(" Replaced existing key (you passed --overwrite)."),console.log(" Run `wyrm cloud sync` to pull deltas with the restored key."),console.log("");return}console.error("Usage:"),console.error(" wyrm cloud recovery show"),console.error(" wyrm cloud recovery restore [--overwrite]"),process.exit(1)}export{ie as cmdCloud};
39
+ `)}async function q(e){i()&&(console.error("Already logged in. Run `wyrm cloud logout` first to switch accounts."),process.exit(1));const t=new d(p),n=I(32).toString("hex");let o;try{o=await t.cliInit(n)}catch(l){console.error(`\u2718 Could not start login: ${l instanceof Error?l.message:l}`),process.exit(1)}console.log(""),console.log("\u{F115D} Sign in to Wyrm Cloud"),console.log(""),console.log(" 1. Open this URL on any device:"),console.log(` ${o.browser_url}`),console.log(""),console.log(" 2. Type this code when prompted:"),console.log(` ${o.code}`),console.log(""),console.log(" 3. Sign in with Google or GitHub."),console.log(""),console.log(` Waiting (expires in ${Math.round(o.expires_in_sec/60)} min)\u2026`);const s=Date.now()+o.expires_in_sec*1e3;let c,a;for(;Date.now()<s;){await new Promise(l=>{setTimeout(l,3e3)});try{const l=await t.cliPoll(n);if(l.ready&&l.session){c=l.session,a=l.account_id;break}}catch(l){console.error(`
40
+ \u2718 Login failed: ${l instanceof Error?l.message:l}`),process.exit(1)}}(!c||!a)&&(console.error("\n\u2718 Login timed out. Run `wyrm cloud login` again."),process.exit(1)),_({base:p,session:c,account_id:a,created_at:Date.now()});const u=new d(p,c);let y="";try{y=(await u.me()).account.email}catch{}const m=`${P()} (${process.platform})`;let k;try{k=(await u.registerDevice(m)).device.id;const S=i();S&&_({...S,email:y,device_id:k,device_name:m,machine_fp:x()})}catch(l){console.error(`
41
+ \u26A0 Logged in but could not register device: ${l instanceof Error?l.message:l}`),console.error(" Re-run `wyrm cloud login` to retry.");return}console.log(""),console.log(`\u2713 Signed in as ${y}`),console.log(`\u2713 Device registered: ${m}`),console.log(`\u2713 Session saved to ${f}`);const D=!h();M(),D?(console.log(`\u2713 Encryption key generated at ${g()} (0600)`),console.log(""),console.log(" \u26A0 Back up the key file. Losing it means losing access to your synced data."),console.log(" The Wyrm Cloud server CANNOT recover it \u2014 that's the operator-owns-data"),console.log(" guarantee. Run `wyrm cloud recovery show` now to get a 24-word phrase,"),console.log(" or copy the key file to a password manager / encrypted backup.")):console.log(`\u2713 Encryption key reused from ${g()}`),console.log(""),console.log("Next: `wyrm cloud sync` to push your memory.")}async function z(){const e=i();if(!e){console.log("Not logged in.");return}try{await new d(e.base,e.session).logout(),console.log("\u2713 Session revoked server-side.")}catch(t){console.error(`\u26A0 Could not revoke server-side: ${t instanceof Error?t.message:t}`),console.error(" Local session cleared anyway.")}E(),console.log(`\u2713 Local session removed (${f}).`),console.log(" Note: ~/.wyrm/cloud.key kept. Delete manually if you want a clean slate.")}async function J(){const e=i();if(!e){console.log("Not logged in. Run `wyrm cloud login`.");return}const t=new d(e.base,e.session);try{const n=await t.me(),o=await t.syncStatus();console.log(""),console.log(`\u{F115D} Wyrm Cloud \u2014 ${n.account.email}`),console.log(""),console.log(` Tier: ${o.tier}`),console.log(` Storage: ${v(o.storage.used_bytes)} / ${v(o.storage.limit_bytes)} (${o.storage.percent}%)`),console.log(` Deltas: ${o.deltas}`),console.log(` Devices: ${o.devices}`),console.log(` Providers:${n.identities.map(r=>" "+r.provider).join(",")}`),console.log(""),console.log(` This machine: ${e.device_name??"(not registered)"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none)"}`),console.log(` Session age: ${Math.round((Date.now()-e.created_at)/(1440*60*1e3))} day(s)`),T(e),console.log(""),h()?o.deltas>0&&(console.log(` Key: ${g()} (0600)`),console.log(` \u26A0 Back this file up. Losing it = losing access to your ${o.deltas} deltas.`),console.log("")):(console.log(" \u26A0 ENCRYPTION KEY MISSING"),console.log(` Expected at: ${g()}`),console.log(" Without this file, deltas already pushed cannot be decrypted."),console.log(" If you have a backup, restore it before running sync."),console.log(""))}catch(n){throw n instanceof b&&n.status===401&&(console.error("Session expired or invalid. Run `wyrm cloud login` again."),process.exit(1)),n}}function T(e){const t=F(e.machine_fp);t.state==="match"?console.log(" Identity: \u2713 machine fingerprint matches (not a copied session)"):t.state==="adopt"?console.log(" Identity: \u2014 no fingerprint yet (pre-7.0.3 session; adopted on next sync)"):(console.log(" Identity: \u26A0 FINGERPRINT MISMATCH \u2014 this session looks COPIED"),console.log(` stored ${t.stored.slice(0,12)}\u2026 vs this machine ${t.current.slice(0,12)}\u2026`),console.log(" device_id collision \u2192 cross-device pull returns 0 silently."),console.log(" Fix: rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json (KEEP cloud.key),"),console.log(" then `wyrm cloud login`. (Or `wyrm cloud sync --force` if intended.)"))}function Q(){const e=i();if(console.log(""),console.log("\u{F115D} Wyrm Cloud \u2014 device identity doctor"),console.log(""),!e){console.log(" Not logged in. Run `wyrm cloud login`."),console.log("");return}console.log(` Account: ${e.email??e.account_id.slice(0,8)+"\u2026"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none \u2014 re-run `wyrm cloud login`)"}`),console.log(` Server: ${e.base}`),console.log(` Stored fp: ${e.machine_fp?e.machine_fp.slice(0,12)+"\u2026":"(none \u2014 pre-7.0.3 session)"}`),console.log(` This box fp: ${x().slice(0,12)}\u2026`),T(e),console.log(""),console.log(" Only ~/.wyrm/cloud.key is meant to be shared across your machines."),console.log(" cloud.json + cloud-cursor.json + machine-id are per-device."),console.log("")}async function V(e){const t=e[0],n=d.fromSession();if(t==="revoke"){const c=e[1];c||(console.error("Usage: wyrm cloud devices revoke <device-id>"),process.exit(1)),await n.revokeDevice(c),console.log(`\u2713 Device ${c.slice(0,8)}\u2026 revoked.`);return}const{devices:o}=await n.listDevices(),s=i()?.device_id;console.log(""),console.log(`\u{F115D} Devices (${o.length})`),console.log("");for(const c of o){const a=c.id===s?"\u2605":" ",u=c.revoked?"(revoked)":"";console.log(` ${a} ${c.id.slice(0,8)}\u2026 ${c.name.padEnd(30)} last_seen=${$(c.last_seen_at)} ${u}`)}console.log(""),console.log(" \u2605 = this machine"),console.log(" Revoke: wyrm cloud devices revoke <device-id>"),console.log("")}async function X(e){w();const t=e.includes("--dry-run"),n=e.includes("--all"),o=e.includes("--force");h()||(console.error("\u26A0 Encryption key not found at",g()),console.error(" A new key will be generated, but it will NOT decrypt deltas"),console.error(" pushed by other devices using a different key. If you have a backup,"),console.error(" restore it to ~/.wyrm/cloud.key (0600) before running sync."),console.error(" Continuing in 5 seconds \u2014 press Ctrl+C to abort."),await new Promise(s=>{setTimeout(s,5e3)})),n&&(console.log("\u{1F310} --all mode: syncing every semantic table (ignoring visibility flags)."),console.log(" Skipped: per-device logs, FTS shadows, sync internals."),console.log(""));const r=await B({dryRun:t,all:n,force:o});if(console.log(""),console.log("\u{F115D} Sync complete"),console.log(""),console.log(` Pushed: ${r.pushed} row(s)${t?" (dry run)":""}`),console.log(` Pulled: ${r.pulled} row(s)`),console.log(` Deleted: ${r.deleted_local} row(s) (peer tombstones)`),r.errors.length>0){console.log(""),console.log(" Errors:");for(const s of r.errors)console.log(` - ${s}`)}console.log("")}function Z(){const e=A(L(O(import.meta.url)),"sync-daemon-entrypoint.js");return new j(e)}async function ee(e){w();const t=e[0]??"status",n=e.find(s=>s.startsWith("--interval=")),o=n?Number(n.split("=")[1]):void 0,r=Z();switch(t){case"start":{i()||(console.error("\u2718 Not logged in. Run `wyrm cloud login` first."),process.exit(1));const s=r.start({interval_minutes:o});s.ok||(console.error(`\u{F115D} sovereign-sync daemon: ${s.reason??"failed to start"}${s.pid?` (pid ${s.pid})`:""}`),process.exit(s.pid?0:1)),console.log(`\u{F115D} \u2713 sovereign-sync daemon started (pid ${s.pid}). Logs: ~/.wyrm/wyrm-sovereign-sync.log`);return}case"stop":{const s=r.stop();console.log(s.ok?"\u{F115D} \u2713 sovereign-sync daemon stopped":`\u{F115D} sovereign-sync daemon: ${s.reason??"failed to stop"}`);return}case"restart":{r.stop(),await new Promise(c=>{setTimeout(c,500)});const s=r.start({interval_minutes:o});console.log(s.ok?`\u{F115D} \u2713 sovereign-sync daemon restarted (pid ${s.pid})`:`\u{F115D} restart failed: ${s.reason??"unknown"}`);return}case"status":{const s=r.status();console.log("\u{F115D} Sovereign-sync daemon"),console.log(` Running: ${s.running?`\u2713 yes (pid ${s.pid})`:"\u2717 no"}`),s.state?.started_ts&&console.log(` Started: ${$(s.state.started_ts)}`),s.state?.last_tick_ts&&(console.log(` Last tick: ${$(s.state.last_tick_ts)} (exit ${s.state.last_tick_exit??"\u2014"})`),s.state.last_tick_summary&&console.log(` Last result: ${s.state.last_tick_summary}`));return}default:console.error(`Unknown daemon action: ${t}. Use: start, stop, restart, status`),process.exit(1)}}async function oe(e){w();const t=d.fromSession(),n=e.find(c=>!c.startsWith("-")),o=R(n??`wyrm-cloud-export-${new Date().toISOString().slice(0,10)}.json`);console.log(" Fetching account export\u2026");const r=await t.accountExport(),s=JSON.stringify(r,null,2);N(o,s,{mode:384}),console.log(""),console.log(`\u2713 Exported to ${o}`),console.log(""),console.log(` Account: ${r.account.email} (${r.account.id.slice(0,8)}\u2026)`),console.log(` Devices: ${r.devices.length}`),console.log(` Identities: ${r.identities.length}`),console.log(` Orgs: ${r.orgs.length}`),console.log(` Deltas: ${r.deltas.length}`),console.log(` Size: ${v(s.length)}`),console.log(""),console.log(" Deltas are still AES-256-GCM ciphertext. To decrypt them you"),console.log(` need the key at ${g()}. Back that up alongside this file.`),console.log("")}async function se(e){e.includes("--confirm")||(console.error("\u2718 This permanently deletes your Wyrm Cloud account."),console.error(" Re-run with --confirm to proceed:"),console.error(" wyrm cloud delete --confirm"),process.exit(1));const n=i();n||(console.error("\u2718 Not logged in. Nothing to delete."),process.exit(1)),console.log(""),console.log("\u26A0 You are about to PERMANENTLY DELETE your Wyrm Cloud account."),console.log(""),console.log(` Account: ${n.email??n.account_id}`),console.log(` Server: ${n.base}`),console.log(""),console.log(" This wipes all encrypted blobs, devices, identities, and org"),console.log(" memberships you own. The local ~/.wyrm/cloud.key is kept so"),console.log(" any backups remain decryptable. This action CANNOT be undone."),console.log(""),(await C(' Type "DELETE my account" to confirm: ')).trim()!=="DELETE my account"&&(console.error("\u2718 Confirmation phrase did not match. Aborting."),process.exit(1)),await new d(n.base,n.session).accountDelete(),E(),console.log(""),console.log("\u2713 Account deleted on server."),console.log(`\u2713 Local session removed (${f}).`),console.log(` Kept: ${g()} (in case you need to decrypt an old export).`),console.log("")}function C(e){return new Promise(t=>{const n=W({input:process.stdin,output:process.stdout});n.question(e,o=>{n.close(),t(o)})})}async function ne(e){const t=e[0];if(t==="show"){const{keyToMnemonic:n}=await import("./recovery.js"),r=n().split(" ");console.log(""),console.log("\u{1F511} Wyrm Cloud recovery phrase (BIP39, 24 words)"),console.log("");for(let s=0;s<4;s++){const c=r.slice(s*6,s*6+6).map((a,u)=>`${String(s*6+u+1).padStart(2)}. ${a.padEnd(10)}`).join(" ");console.log(` ${c}`)}console.log(""),console.log(" Write these down on PAPER and keep them somewhere safe."),console.log(" Anyone with this phrase can decrypt your synced Wyrm memory."),console.log(" Wyrm Cloud has NO COPY of this. Lose it = lose your data."),console.log("");return}if(t==="restore"){const n=e.includes("--overwrite");console.log(""),console.log("\u{1F511} Restore master key from BIP39 recovery phrase"),console.log(""),console.log(" Enter your 24 words separated by spaces (case-insensitive)."),console.log(" Press Enter when done."),console.log("");const o=await C(" > "),{mnemonicToKey:r}=await import("./recovery.js"),{path:s,overwrote:c}=r(o,{overwrite:n});console.log(""),console.log(`\u2713 Master key restored to ${s} (0600)`),c&&console.log(" Replaced existing key (you passed --overwrite)."),console.log(" Run `wyrm cloud sync` to pull deltas with the restored key."),console.log("");return}console.error("Usage:"),console.error(" wyrm cloud recovery show"),console.error(" wyrm cloud recovery restore [--overwrite]"),process.exit(1)}export{fe as cmdCloud};
@@ -0,0 +1,2 @@
1
+ import{spawn as a}from"child_process";import{fileURLToPath as l}from"url";import{dirname as m,join as p}from"path";import{loadSession as d}from"./client.js";import{writeSovereignState as n,DEFAULT_SOVEREIGN_INTERVAL_MS as g}from"./sync-daemon.js";const u=p(m(l(import.meta.url)),"..","wyrm-cli.js");function i(e){process.stdout.write(`[${new Date().toISOString()}] ${e}
2
+ `)}function c(){return new Promise(e=>{const s=a(process.execPath,[u,"cloud","sync"],{stdio:["ignore","pipe","pipe"],env:{...process.env}});let o="";s.stdout.on("data",t=>{o+=t.toString()}),s.stderr.on("data",t=>{o+=t.toString()}),s.on("close",t=>{const r=o.replace(/\s+/g," ").trim().slice(0,300);i(`tick exit=${t}: ${r}`),n({last_tick_ts:Date.now(),last_tick_exit:t,last_tick_summary:r}),e()}),s.on("error",t=>{i(`tick spawn error: ${t.message}`),n({last_tick_ts:Date.now(),last_tick_exit:null,last_tick_summary:`spawn error: ${t.message}`}),e()})})}async function _(){const e=Number(process.argv[2])||g,s=d();(!s||!s.device_id)&&(console.error("[sovereign-sync] device not logged in (run `wyrm cloud login`) \u2014 refusing to start"),process.exit(2)),i(`sovereign-sync started \xB7 interval=${e}ms \xB7 device=${s.device_id.slice(0,8)}\u2026`),await c();const o=setInterval(()=>{c()},e),t=r=>{clearInterval(o),i(`${r} \u2014 exiting`),process.exit(0)};process.on("SIGTERM",()=>t("SIGTERM")),process.on("SIGINT",()=>t("SIGINT"))}_().catch(e=>{console.error("[sovereign-sync] fatal:",e),process.exit(1)});
@@ -0,0 +1 @@
1
+ import{spawn as _}from"child_process";import{existsSync as c,readFileSync as d,writeFileSync as l,mkdirSync as h,openSync as p,closeSync as y,unlinkSync as E,statSync as g,renameSync as I}from"fs";import{homedir as v}from"os";import{join as u}from"path";const n=u(v(),".wyrm"),i=u(n,"wyrm-sovereign-sync.pid"),t=u(n,"wyrm-sovereign-sync.log"),s=u(n,"wyrm-sovereign-sync.state"),L=1e6,N=0,O=600*1e3;class G{daemonEntrypoint;constructor(e){this.daemonEntrypoint=e,m()}start(e={}){if(this.isAlive())return{ok:!1,pid:this.readPid(),reason:"already running"};R();const o=(e.interval_minutes??O/6e4)*6e4,f=p(t,"a"),S=p(t,"a"),a=_(process.execPath,[this.daemonEntrypoint,String(o)],{detached:!0,stdio:["ignore",f,S],env:{...process.env}});return a.unref(),y(f),y(S),typeof a.pid!="number"?{ok:!1,reason:"spawn returned no pid"}:(l(i,String(a.pid)),k({started_ts:Date.now()}),{ok:!0,pid:a.pid})}stop(){if(!this.isAlive())return this.cleanPid(),{ok:!1,reason:"not running"};const e=this.readPid();if(!e)return{ok:!1,reason:"pid file unreadable"};try{return process.kill(e,"SIGTERM"),{ok:!0}}catch(o){return{ok:!1,reason:o?.message??String(o)}}}status(){const e=this.isAlive();return{running:e,pid:e?this.readPid():void 0,state:c(s)?P():void 0}}isAlive(){if(!c(i))return!1;const e=this.readPid();if(!e)return!1;try{return process.kill(e,N),!0}catch{return!1}}readPid(){try{const e=Number(d(i,"utf8").trim());return Number.isFinite(e)&&e>0?e:void 0}catch{return}}cleanPid(){try{E(i)}catch{}}}function k(r){m();let e={};try{e=JSON.parse(d(s,"utf8"))}catch{}l(s,JSON.stringify({...e,...r},null,2))}function m(){c(n)||h(n,{recursive:!0})}function P(){try{return JSON.parse(d(s,"utf8"))}catch{return}}function R(){if(c(t))try{if(g(t).size>L){const r=`${t}.1`;try{E(r)}catch{}I(t,r)}}catch{}}export{O as DEFAULT_SOVEREIGN_INTERVAL_MS,t as SOVEREIGN_LOG_FILE,i as SOVEREIGN_PID_FILE,s as SOVEREIGN_STATE_FILE,G as SovereignSyncManager,k as writeSovereignState};
@@ -1,4 +1,4 @@
1
- import{readFileSync as O,writeFileSync as k,existsSync as D}from"node:fs";import{join as I}from"node:path";import{WyrmDB as $,resolveDbPath as C}from"../database.js";import{CloudClient as L,loadSession as A,saveSession as v,resolveCloudDir as j}from"./client.js";import{encrypt as S,decrypt as x}from"./crypto.js";import{classifySession as P,computeMachineFp as U}from"./machine-id.js";function T(){return I(j(),"cloud-cursor.json")}const E=[{kind:"truth",table:"ground_truths",hasVisibility:!0},{kind:"memory_artifact",table:"memory_artifacts",hasVisibility:!0},{kind:"quest",table:"quests",hasVisibility:!0},{kind:"design_token",table:"design_tokens",hasVisibility:!0},{kind:"design_reference",table:"design_references",hasVisibility:!0},{kind:"skill",table:"skills",hasVisibility:!0}],W=new Set(["sessions","agent_actions","agent_presence","session_seen_artifacts","tool_call_log","llm_query_log","external_call_log","token_savings_log","cost_tracking","usage_events","audit_log","indexing_queue","vectors","symbol_index","update_check","sync_conflicts","sync_log","sync_tombstones","wyrm_cloud_sync_state","mcp_client_configs","watch_dirs","global_context","schema_versions","failure_patterns"]),F=/_fts(_config|_data|_docsize|_idx)?$/;function M(e){const t=e.getDatabase(),s=[...E],r=new Set(E.map(o=>o.table));try{const o=t.prepare(`SELECT name FROM sqlite_master
1
+ import{readFileSync as O,writeFileSync as k,existsSync as D}from"node:fs";import{join as I}from"node:path";import{WyrmDB as L,resolveDbPath as C}from"../database.js";import{CloudClient as $,loadSession as A,saveSession as v,resolveCloudDir as j}from"./client.js";import{encrypt as S,decrypt as U}from"./crypto.js";import{classifySession as x,computeMachineFp as P}from"./machine-id.js";function T(){return I(j(),"cloud-cursor.json")}const E=[{kind:"truth",table:"ground_truths",hasVisibility:!0},{kind:"memory_artifact",table:"memory_artifacts",hasVisibility:!0},{kind:"quest",table:"quests",hasVisibility:!0},{kind:"design_token",table:"design_tokens",hasVisibility:!0},{kind:"design_reference",table:"design_references",hasVisibility:!0},{kind:"skill",table:"skills",hasVisibility:!0}],W=new Set(["sessions","agent_actions","agent_presence","session_seen_artifacts","tool_call_log","llm_query_log","external_call_log","token_savings_log","cost_tracking","usage_events","audit_log","indexing_queue","vectors","symbol_index","update_check","sync_conflicts","sync_log","sync_tombstones","wyrm_cloud_sync_state","mcp_client_configs","watch_dirs","global_context","schema_versions","failure_patterns"]),F=/_fts(_config|_data|_docsize|_idx)?$/;function M(e){const t=e.getDatabase(),s=[...E],r=new Set(E.map(o=>o.table));try{const o=t.prepare(`SELECT name FROM sqlite_master
2
2
  WHERE type='table' AND name NOT LIKE 'sqlite_%'`).all();for(const n of o){const l=n.name;if(r.has(l)||W.has(l)||F.test(l)||!h.test(l))continue;const _=t.prepare(`PRAGMA table_info(${l})`).all();if(!_.some(c=>c.name==="id"))continue;const d=_.some(c=>c.name==="cross_project_visibility");s.push({kind:l,table:l,hasVisibility:d})}}catch{}return s}function ie(e){return e}function H(e,t){if(!h.test(t))return new Set;try{const r=e.getDatabase().prepare(`PRAGMA table_info(${t})`).all();return new Set(r.filter(o=>h.test(o.name)).map(o=>o.name))}catch{return new Set}}const h=/^[a-z_][a-z0-9_]*$/i;function V(){const e={pull_cursor:0,last_push_updated_ms:0,last_push_at:0,last_pull_at:0},t=T();if(!D(t))return e;try{const s=JSON.parse(O(t,"utf-8"));return{...e,...s}}catch{return e}}function q(e){k(T(),JSON.stringify(e,null,2),{mode:384})}function N(e){e.getDatabase().exec(`
3
3
  CREATE TABLE IF NOT EXISTS sync_tombstones (
4
4
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -11,9 +11,9 @@ import{readFileSync as O,writeFileSync as k,existsSync as D}from"node:fs";import
11
11
  CREATE INDEX IF NOT EXISTS idx_sync_tombstones_pending
12
12
  ON sync_tombstones(pushed_at_ms);
13
13
  `)}function ae(e,t,s){if(typeof t!="string"||t.length===0||t.length>64)return;N(e),e.getDatabase().prepare(`INSERT OR IGNORE INTO sync_tombstones (kind, row_id, deleted_at_ms)
14
- VALUES (?, ?, ?)`).run(t,String(s),Date.now())}class Y extends Error{constructor(t){super(t),this.name="CopiedSessionError"}}function G(e){return!!e||process.env.WYRM_ALLOW_COPIED_SESSION==="1"}function B(e,t,s){const r=P(e.machine_fp);if(r.state==="match")return;if(r.state==="adopt"){t(U());return}const o=["","\u26A0 WYRM CLOUD: this session looks COPIED from another machine.","",` Stored machine fingerprint: ${r.stored.slice(0,12)}\u2026`,` This machine's fingerprint: ${r.current.slice(0,12)}\u2026`,""," Both machines now share ONE device_id. The cloud pull query skips a"," device's own deltas, so each machine filters the OTHER's changes out:"," push works but pull silently returns 0 rows (peer memory looks MISSING).",""," FIX \u2014 give this machine its own identity (keep your encryption key):"," rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json # NOT cloud.key"," wyrm cloud login",""," If this is a legitimate change (e.g. you renamed this host), re-run with:"," wyrm cloud sync --force (or set WYRM_ALLOW_COPIED_SESSION=1)",""];if(G(s)){console.error(o.join(`
14
+ VALUES (?, ?, ?)`).run(t,String(s),Date.now())}class Y extends Error{constructor(t){super(t),this.name="CopiedSessionError"}}function G(e){return!!e||process.env.WYRM_ALLOW_COPIED_SESSION==="1"}function B(e,t,s){const r=x(e.machine_fp);if(r.state==="match")return;if(r.state==="adopt"){t(P());return}const o=["","\u26A0 WYRM CLOUD: this session looks COPIED from another machine.","",` Stored machine fingerprint: ${r.stored.slice(0,12)}\u2026`,` This machine's fingerprint: ${r.current.slice(0,12)}\u2026`,""," Both machines now share ONE device_id. The cloud pull query skips a"," device's own deltas, so each machine filters the OTHER's changes out:"," push works but pull silently returns 0 rows (peer memory looks MISSING).",""," FIX \u2014 give this machine its own identity (keep your encryption key):"," rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json # NOT cloud.key"," wyrm cloud login",""," If this is a legitimate change (e.g. you renamed this host), re-run with:"," wyrm cloud sync --force (or set WYRM_ALLOW_COPIED_SESSION=1)",""];if(G(s)){console.error(o.join(`
15
15
  `)),console.error(" Proceeding anyway (--force / WYRM_ALLOW_COPIED_SESSION=1); fingerprint updated."),console.error(""),t(r.current);return}throw console.error(o.join(`
16
- `)),new Y("Cloud session looks copied from another machine (device_id collision). Re-login or pass --force.")}async function ce(e={}){const t=A();if(!t||!t.device_id)throw new Error("Not logged in or device not registered. Run `wyrm cloud login`.");B(t,i=>{v({...t,machine_fp:i})},e.force);const s=new L(t.base,t.session),r=V(),o=new $(C());N(o);const n={pushed:0,pulled:0,deleted_local:0,errors:[]},l=e.all?M(o):E,_=K(o,l,r.last_push_updated_ms,!!e.all,n.errors),d=X(o),c=[..._.map(i=>({kind:i.kind,row_id:String(i.row_id),visibility:i.visibility,payload_b64:S(JSON.stringify(i.row)),updated_at:i.updated_at})),...d.map(i=>({kind:"tombstone",row_id:`${i.kind}:${i.row_id}`,visibility:"org",payload_b64:S(JSON.stringify({kind:i.kind,row_id:i.row_id,deleted_at_ms:i.deleted_at_ms})),updated_at:i.deleted_at_ms}))];if(c.length>0&&!e.dryRun){let m=r.last_push_updated_ms;for(let a=0;a<c.length;a+=50){const p=c.slice(a,a+50);try{const u=await s.syncPush(t.device_id,p);if(n.pushed+=u.accepted,u.rejected>0)for(const f of u.rejected_details)n.errors.push(`push: ${f.reason} on ${f.id.slice(0,8)}\u2026`);for(const f of p)f.updated_at&&f.updated_at>m&&(m=f.updated_at)}catch(u){n.errors.push(`push chunk failed: ${u instanceof Error?u.message:u}`)}}r.last_push_updated_ms=m,r.last_push_at=Date.now(),d.length>0&&z(o,d.map(a=>a.id))}else n.pushed=c.length;if(!e.dryRun){let i=r.pull_cursor,m=0;e:for(;m<50;){let a;try{a=await s.syncPull(t.device_id,i,100)}catch(p){n.errors.push(`pull failed: ${p instanceof Error?p.message:p}`);break}if(a.deltas.length===0)break;for(const p of a.deltas)try{const u=x(p.payload_b64);if(p.kind==="tombstone"){const{kind:f,row_id:b}=JSON.parse(u);Z(o,l,f,b)&&n.deleted_local++}else{const f=JSON.parse(u);Q(o,l,p.kind,p.row_id,f),n.pulled++}}catch(u){n.errors.push(`apply ${p.kind}:${p.row_id}: ${u instanceof Error?u.message:u}`);break e}if(i=a.next_cursor,!a.has_more)break;m++}r.pull_cursor=i,r.last_pull_at=Date.now()}return q(r),n}function J(e){if(typeof e!="string"||e.length===0)return 0;const t=e.includes("T")?e:e.replace(" ","T")+"Z",s=Date.parse(t);return Number.isFinite(s)?s:0}function g(e,t,s){try{return e.getDatabase().prepare(`PRAGMA table_info(${t})`).all().some(n=>n.name===s)}catch{return!1}}function K(e,t,s,r,o){const n=[],l=e.getDatabase(),_=g(e,"projects","sync_policy");for(const d of t)if(h.test(d.table)&&!(!r&&!d.hasVisibility))try{const c=g(e,d.table,"updated_at"),i=d.hasVisibility?"WHERE cross_project_visibility IN ('org', 'public')":"WHERE 1=1",m=_&&g(e,d.table,"project_id")?"AND (project_id IS NULL OR project_id IN (SELECT id FROM projects WHERE sync_policy IN ('cloud', 'team')))":"",a=c?"AND (? = 0 OR strftime('%s', updated_at) * 1000 > ?)":"",p=c?"ORDER BY updated_at ASC":"",u=`SELECT * FROM ${d.table} ${i} ${m} ${a} ${p}`,f=l.prepare(u),b=c?f.all(s,s):f.all();for(const y of b){const w=J(y.updated_at);if(s>0&&w>0&&w<=s)continue;const R=d.hasVisibility?String(y.cross_project_visibility??"within"):"org";n.push({kind:d.kind,row_id:y.id??"?",visibility:R,row:y,updated_at:w||Date.now()})}}catch(c){const i=c instanceof Error?c.message:String(c);if(/no such table/i.test(i))continue;o.push(`collect ${d.table}: ${i}`)}return n.sort((d,c)=>(d.updated_at??0)-(c.updated_at??0)),n}function X(e){const t=e.getDatabase();try{return t.prepare(`SELECT id, kind, row_id, deleted_at_ms
16
+ `)),new Y("Cloud session looks copied from another machine (device_id collision). Re-login or pass --force.")}async function ce(e={}){const t=A();if(!t||!t.device_id)throw new Error("Not logged in or device not registered. Run `wyrm cloud login`.");B(t,i=>{v({...t,machine_fp:i})},e.force);const s=new $(t.base,t.session),r=V(),o=new L(C());N(o);const n={pushed:0,pulled:0,deleted_local:0,errors:[]},l=e.all?M(o):E,_=K(o,l,r.last_push_updated_ms,!!e.all,n.errors),d=X(o),c=[..._.map(i=>({kind:i.kind,row_id:String(i.row_id),visibility:i.visibility,payload_b64:S(JSON.stringify(i.row)),updated_at:i.updated_at})),...d.map(i=>({kind:"tombstone",row_id:`${i.kind}:${i.row_id}`,visibility:"org",payload_b64:S(JSON.stringify({kind:i.kind,row_id:i.row_id,deleted_at_ms:i.deleted_at_ms})),updated_at:i.deleted_at_ms}))];if(c.length>0&&!e.dryRun){let m=r.last_push_updated_ms;for(let a=0;a<c.length;a+=25){const p=c.slice(a,a+25);try{const u=await s.syncPush(t.device_id,p);if(n.pushed+=u.accepted,u.rejected>0)for(const f of u.rejected_details)n.errors.push(`push: ${f.reason} on ${f.id.slice(0,8)}\u2026`);for(const f of p)f.updated_at&&f.updated_at>m&&(m=f.updated_at)}catch(u){n.errors.push(`push chunk failed: ${u instanceof Error?u.message:u}`)}}r.last_push_updated_ms=m,r.last_push_at=Date.now(),d.length>0&&z(o,d.map(a=>a.id))}else n.pushed=c.length;if(!e.dryRun){let i=r.pull_cursor,m=0;e:for(;m<50;){let a;try{a=await s.syncPull(t.device_id,i,100)}catch(p){n.errors.push(`pull failed: ${p instanceof Error?p.message:p}`);break}if(a.deltas.length===0)break;for(const p of a.deltas)try{const u=U(p.payload_b64);if(p.kind==="tombstone"){const{kind:f,row_id:b}=JSON.parse(u);Z(o,l,f,b)&&n.deleted_local++}else{const f=JSON.parse(u);Q(o,l,p.kind,p.row_id,f),n.pulled++}}catch(u){n.errors.push(`apply ${p.kind}:${p.row_id}: ${u instanceof Error?u.message:u}`);break e}if(i=a.next_cursor,!a.has_more)break;m++}r.pull_cursor=i,r.last_pull_at=Date.now()}return q(r),n}function J(e){if(typeof e!="string"||e.length===0)return 0;const t=e.includes("T")?e:e.replace(" ","T")+"Z",s=Date.parse(t);return Number.isFinite(s)?s:0}function g(e,t,s){try{return e.getDatabase().prepare(`PRAGMA table_info(${t})`).all().some(n=>n.name===s)}catch{return!1}}function K(e,t,s,r,o){const n=[],l=e.getDatabase(),_=g(e,"projects","sync_policy");for(const d of t)if(h.test(d.table)&&!(!r&&!d.hasVisibility))try{const c=g(e,d.table,"updated_at"),i=d.hasVisibility?"WHERE cross_project_visibility IN ('org', 'public')":"WHERE 1=1",m=_&&g(e,d.table,"project_id")?"AND (project_id IS NULL OR project_id IN (SELECT id FROM projects WHERE sync_policy IN ('cloud', 'team')))":"",a=c?"AND (? = 0 OR updated_at IS NULL OR strftime('%s', updated_at) * 1000 > ?)":"",p=c?"ORDER BY (updated_at IS NULL) ASC, updated_at ASC":"",u=`SELECT * FROM ${d.table} ${i} ${m} ${a} ${p}`,f=l.prepare(u),b=c?f.all(s,s):f.all();for(const y of b){const w=J(y.updated_at);if(s>0&&w>0&&w<=s)continue;const R=d.hasVisibility?String(y.cross_project_visibility??"within"):"org";n.push({kind:d.kind,row_id:y.id??"?",visibility:R,row:y,updated_at:w||Date.now()})}}catch(c){const i=c instanceof Error?c.message:String(c);if(/no such table/i.test(i))continue;o.push(`collect ${d.table}: ${i}`)}return n.sort((d,c)=>(d.updated_at??0)-(c.updated_at??0)),n}function X(e){const t=e.getDatabase();try{return t.prepare(`SELECT id, kind, row_id, deleted_at_ms
17
17
  FROM sync_tombstones
18
18
  WHERE pushed_at_ms IS NULL
19
19
  ORDER BY id ASC
package/dist/database.js CHANGED
@@ -90,11 +90,11 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
90
90
  SELECT COALESCE(SUM(tokens_estimate), 0) as total
91
91
  FROM sessions WHERE project_id = ? AND is_archived = 0
92
92
  `).get(t).total}addQuest(t,e,s,r="medium",n){const i=C(),a=this.db.prepare(`
93
- INSERT INTO quests (project_id, title, description, priority, tags, agent_id, run_id)
94
- VALUES (?, ?, ?, ?, ?, ?, ?)
93
+ INSERT INTO quests (project_id, title, description, priority, tags, agent_id, run_id, updated_at)
94
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
95
95
  RETURNING *
96
96
  `).get(t,e,s||"",r,n||null,i.agent_id,i.run_id);return m(this.db,{projectId:t,kind:"quest",refTable:"quests",refId:a.id}),a}updateQuest(t,e){const s=e==="completed"?new Date().toISOString():null,r=this.db.prepare(`
97
- UPDATE quests SET status = ?, completed_at = ? WHERE id = ? RETURNING *
97
+ UPDATE quests SET status = ?, completed_at = ?, updated_at = datetime('now') WHERE id = ? RETURNING *
98
98
  `).get(e,s,t);return r&&m(this.db,{projectId:r.project_id,kind:"quest",refTable:"quests",refId:t}),r}getPendingQuests(t){return this.db.prepare(`
99
99
  SELECT * FROM quests
100
100
  WHERE project_id = ? AND status IN ('pending', 'in_progress')
@@ -147,7 +147,7 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
147
147
  updated_at = datetime('now')
148
148
  `).run(t,e)}getGlobalContext(t){return this.db.prepare(`
149
149
  SELECT value FROM global_context WHERE key = ?
150
- `).get(t)?.value}getAllGlobalContext(){const t=this.db.prepare("SELECT key, value FROM global_context").all(),e={};for(const s of t)e[s.key]=s.value;return e}registerSkill(t,e,s,r,n,i,a,o){const c=o?.tier??"atomic",l=JSON.stringify(Array.isArray(o?.governs)?o.governs:[]),p=JSON.stringify(Array.isArray(o?.composes)?o.composes:[]),g=y({skillPath:s,name:t}),_=g?.content??null,S=g?.sha256??null,f=g?new Date().toISOString():null,E=this.resilience.withRetrySync(()=>this.db.prepare(`
150
+ `).get(t)?.value}getAllGlobalContext(){const t=this.db.prepare("SELECT key, value FROM global_context").all(),e={};for(const s of t)e[s.key]=s.value;return e}registerSkill(t,e,s,r,n,i,a,o){const c=o?.tier??"atomic",l=JSON.stringify(Array.isArray(o?.governs)?o.governs:[]),p=JSON.stringify(Array.isArray(o?.composes)?o.composes:[]),_=y({skillPath:s,name:t}),g=_?.content??null,S=_?.sha256??null,f=_?new Date().toISOString():null,E=this.resilience.withRetrySync(()=>this.db.prepare(`
151
151
  INSERT INTO skills (name, description, skill_path, category, author, version, tags, tier, governs, composes, content, content_sha256, content_updated_at, is_active, usage_count)
152
152
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)
153
153
  ON CONFLICT(name) DO UPDATE SET
@@ -169,13 +169,13 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
169
169
  updated_at = datetime('now'),
170
170
  is_active = 1
171
171
  RETURNING *
172
- `).get(t,e,s,r||null,n||null,i||null,a||null,c,l,p,_,S,f),"registerSkill");if(!E.success)throw E.error||new Error("Failed to register skill");return E.data}getSkill(t){const e=this.db.prepare("SELECT * FROM skills WHERE name = ?").get(t);return e&&this.db.prepare("UPDATE skills SET last_used = datetime('now'), usage_count = usage_count + 1 WHERE id = ?").run(e.id),e}backfillSkillContent(t={}){const e=t.read??y,s=this.db.prepare("SELECT id, name, skill_path, content_sha256 FROM skills").all();let r=0,n=0,i=0;const a=this.db.prepare("UPDATE skills SET content = ?, content_sha256 = ?, content_updated_at = ?, updated_at = datetime('now') WHERE id = ?");return this.db.transaction(()=>{for(const c of s){const l=e({skillPath:c.skill_path,name:c.name,skillsDir:t.skillsDir});if(!l){i++;continue}if(c.content_sha256&&c.content_sha256===l.sha256){n++;continue}a.run(l.content,l.sha256,new Date().toISOString(),c.id),r++}})(),{total:s.length,filled:r,unchanged:n,missing:i}}exportSkillContent(t,e={}){const s=e.includeInactive?"":" WHERE is_active = 1",r=this.db.prepare(`SELECT name, content FROM skills${s} ORDER BY name`).all();let n=0,i=0,a=0;const o=new Set;for(const c of r){if(c.content==null){i++;continue}let l=G(c.name);if(!l){i++;continue}o.has(l)&&(l=`${l}-${V(c.name).slice(0,8)}`,a++),o.add(l);const p=d(t,l);b(p,{recursive:!0}),I(d(p,"SKILL.md"),c.content,"utf-8"),n++}return{total:r.length,written:n,skipped_no_content:i,collisions:a}}setSkillVisibility(t,e){return this.db.prepare("UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now') WHERE name = ? RETURNING *").get(e,t)}setAllSkillsVisibility(t,e={}){const s=[],r=[t];e.includeInactive||s.push("is_active = 1"),e.tier&&(s.push("tier = ?"),r.push(e.tier));const n=s.length?" WHERE "+s.join(" AND "):"";return this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now')${n}`).run(...r).changes}listSkills(t,e,s,r){let n="SELECT * FROM skills WHERE 1=1";const i=[];if(t!==void 0&&(n+=" AND is_active = ?",i.push(t?1:0)),e&&(n+=" AND category = ?",i.push(e)),r&&(n+=" AND tier = ?",i.push(r)),s){const a=h(s);a&&(n+=" AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)",i.push(a))}return n+=" ORDER BY updated_at DESC",this.db.prepare(n).all(...i)}searchSkills(t,e=20,s){const r=h(t);if(!r)return[];const n=s?" AND s.tier = ?":"",i=this.db.prepare(`
172
+ `).get(t,e,s,r||null,n||null,i||null,a||null,c,l,p,g,S,f),"registerSkill");if(!E.success)throw E.error||new Error("Failed to register skill");return E.data}getSkill(t){const e=this.db.prepare("SELECT * FROM skills WHERE name = ?").get(t);return e&&this.db.prepare("UPDATE skills SET last_used = datetime('now'), usage_count = usage_count + 1 WHERE id = ?").run(e.id),e}backfillSkillContent(t={}){const e=t.read??y,s=this.db.prepare("SELECT id, name, skill_path, content_sha256 FROM skills").all();let r=0,n=0,i=0;const a=this.db.prepare("UPDATE skills SET content = ?, content_sha256 = ?, content_updated_at = ?, updated_at = datetime('now') WHERE id = ?");return this.db.transaction(()=>{for(const c of s){const l=e({skillPath:c.skill_path,name:c.name,skillsDir:t.skillsDir});if(!l){i++;continue}if(c.content_sha256&&c.content_sha256===l.sha256){n++;continue}a.run(l.content,l.sha256,new Date().toISOString(),c.id),r++}})(),{total:s.length,filled:r,unchanged:n,missing:i}}exportSkillContent(t,e={}){const s=e.includeInactive?"":" WHERE is_active = 1",r=this.db.prepare(`SELECT name, content FROM skills${s} ORDER BY name`).all();let n=0,i=0,a=0;const o=new Set;for(const c of r){if(c.content==null){i++;continue}let l=G(c.name);if(!l){i++;continue}o.has(l)&&(l=`${l}-${V(c.name).slice(0,8)}`,a++),o.add(l);const p=d(t,l);b(p,{recursive:!0}),I(d(p,"SKILL.md"),c.content,"utf-8"),n++}return{total:r.length,written:n,skipped_no_content:i,collisions:a}}setSkillVisibility(t,e){return this.db.prepare("UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now') WHERE name = ? RETURNING *").get(e,t)}setAllSkillsVisibility(t,e={}){const s=[],r=[t];e.includeInactive||s.push("is_active = 1"),e.tier&&(s.push("tier = ?"),r.push(e.tier));const n=s.length?" WHERE "+s.join(" AND "):"";return this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now')${n}`).run(...r).changes}listSkills(t,e,s,r){let n="SELECT * FROM skills WHERE 1=1";const i=[];if(t!==void 0&&(n+=" AND is_active = ?",i.push(t?1:0)),e&&(n+=" AND category = ?",i.push(e)),r&&(n+=" AND tier = ?",i.push(r)),s){const a=h(s);a&&(n+=" AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)",i.push(a))}return n+=" ORDER BY updated_at DESC",this.db.prepare(n).all(...i)}searchSkills(t,e=20,s){const r=h(t);if(!r)return[];const n=s?" AND s.tier = ?":"",i=this.db.prepare(`
173
173
  SELECT s.* FROM skills s
174
174
  JOIN skills_fts ON s.id = skills_fts.rowid
175
175
  WHERE skills_fts MATCH ?${n}
176
176
  ORDER BY rank
177
177
  LIMIT ?
178
- `);return s?i.all(r,s,e):i.all(r,e)}getSkillGraph(t){const e=this.db.prepare("SELECT * FROM skills").all(),s=new Map;for(const o of e)s.set(o.name,o);const r=o=>{if(!o)return[];try{const c=JSON.parse(o);return Array.isArray(c)?c.filter(l=>typeof l=="string"):[]}catch{return[]}},n=(o,c)=>{const l=s.get(o);if(!l)return null;const p=r(l.governs),g=r(l.composes),_={name:l.name,tier:l.tier??"atomic",description:l.description,governs:p,composes:g,children:[]},S=[];if(!c.has(o)){const f=new Set(c).add(o);for(const E of p){const T=n(E,f);T?_.children.push(T):S.push(E)}}return S.length&&(_.missing=S),_};if(t){const o=n(t,new Set);return o?[o]:[]}const i=e.filter(o=>(o.tier??"atomic")==="god").map(o=>o.name).sort(),a=[];for(const o of i){const c=n(o,new Set);c&&a.push(c)}return a}updateSkill(t,e){const s=[],r=[];return e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.skill_path!==void 0&&(s.push("skill_path = ?"),r.push(e.skill_path)),e.category!==void 0&&(s.push("category = ?"),r.push(e.category)),e.is_active!==void 0&&(s.push("is_active = ?"),r.push(e.is_active?1:0)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),e.version!==void 0&&(s.push("version = ?"),r.push(e.version)),s.length===0?this.getSkill(t):(s.push("updated_at = datetime('now')"),r.push(t),this.db.prepare(`
178
+ `);return s?i.all(r,s,e):i.all(r,e)}getSkillGraph(t){const e=this.db.prepare("SELECT * FROM skills").all(),s=new Map;for(const o of e)s.set(o.name,o);const r=o=>{if(!o)return[];try{const c=JSON.parse(o);return Array.isArray(c)?c.filter(l=>typeof l=="string"):[]}catch{return[]}},n=(o,c)=>{const l=s.get(o);if(!l)return null;const p=r(l.governs),_=r(l.composes),g={name:l.name,tier:l.tier??"atomic",description:l.description,governs:p,composes:_,children:[]},S=[];if(!c.has(o)){const f=new Set(c).add(o);for(const E of p){const T=n(E,f);T?g.children.push(T):S.push(E)}}return S.length&&(g.missing=S),g};if(t){const o=n(t,new Set);return o?[o]:[]}const i=e.filter(o=>(o.tier??"atomic")==="god").map(o=>o.name).sort(),a=[];for(const o of i){const c=n(o,new Set);c&&a.push(c)}return a}updateSkill(t,e){const s=[],r=[];return e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.skill_path!==void 0&&(s.push("skill_path = ?"),r.push(e.skill_path)),e.category!==void 0&&(s.push("category = ?"),r.push(e.category)),e.is_active!==void 0&&(s.push("is_active = ?"),r.push(e.is_active?1:0)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),e.version!==void 0&&(s.push("version = ?"),r.push(e.version)),s.length===0?this.getSkill(t):(s.push("updated_at = datetime('now')"),r.push(t),this.db.prepare(`
179
179
  UPDATE skills SET ${s.join(", ")} WHERE name = ? RETURNING *
180
180
  `).get(...r))}deleteSkill(t){return this.db.prepare("DELETE FROM skills WHERE name = ?").run(t).changes>0}deactivateSkill(t){return this.updateSkill(t,{is_active:!1})}activateSkill(t){return this.updateSkill(t,{is_active:!0})}getSkillStats(){const t=this.db.prepare("SELECT COUNT(*) as count FROM skills").get().count,e=this.db.prepare("SELECT COUNT(*) as count FROM skills WHERE is_active = 1").get().count,s=this.db.prepare(`
181
181
  SELECT category, COUNT(*) as count FROM skills WHERE category IS NOT NULL GROUP BY category
@@ -193,7 +193,7 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
193
193
  WHERE project_id = ?
194
194
  AND (tags = ? OR tags LIKE ? OR tags LIKE ? OR tags LIKE ?)
195
195
  LIMIT 1
196
- `).get(t,e,`${e},%`,`%,${e}`,`%,${e},%`)}updateQuestFields(t,e){const s=[],r=[];if(e.title!==void 0&&(s.push("title = ?"),r.push(e.title)),e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),s.length===0)return this.db.prepare("SELECT * FROM quests WHERE id = ?").get(t);r.push(t);const n=this.db.prepare(`UPDATE quests SET ${s.join(", ")} WHERE id = ? RETURNING *`).get(...r);return n&&m(this.db,{projectId:n.project_id,kind:"quest",refTable:"quests",refId:t}),n}insertData(t,e,s,r,n){const i=this.resilience.withRetrySync(()=>this.db.prepare(`
196
+ `).get(t,e,`${e},%`,`%,${e}`,`%,${e},%`)}updateQuestFields(t,e){const s=[],r=[];if(e.title!==void 0&&(s.push("title = ?"),r.push(e.title)),e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),s.length===0)return this.db.prepare("SELECT * FROM quests WHERE id = ?").get(t);s.push("updated_at = datetime('now')"),r.push(t);const n=this.db.prepare(`UPDATE quests SET ${s.join(", ")} WHERE id = ? RETURNING *`).get(...r);return n&&m(this.db,{projectId:n.project_id,kind:"quest",refTable:"quests",refId:t}),n}insertData(t,e,s,r,n){const i=this.resilience.withRetrySync(()=>this.db.prepare(`
197
197
  INSERT INTO data_lake (project_id, category, key, value, metadata)
198
198
  VALUES (?, ?, ?, ?, ?)
199
199
  RETURNING *
package/dist/design.js CHANGED
@@ -1,4 +1,4 @@
1
- const a=new Set(["color","type","spacing","motion","shadow","radius","breakpoint","custom"]),d=new Set(["url","image","palette","snippet"]);class E{db;constructor(e){this.db=e}setToken(e){if(!a.has(e.category))throw new Error(`invalid category: ${e.category} (must be one of ${[...a].join(", ")})`);if(!e.key||!e.value)throw new Error("key and value are required");return this.db.prepare(`
1
+ const d=new Set(["color","type","spacing","motion","shadow","radius","breakpoint","custom"]),c=new Set(["url","image","palette","snippet"]);class E{db;constructor(e){this.db=e}setToken(e){if(!d.has(e.category))throw new Error(`invalid category: ${e.category} (must be one of ${[...d].join(", ")})`);if(!e.key||!e.value)throw new Error("key and value are required");return this.db.prepare(`
2
2
  INSERT INTO design_tokens (project_id, category, key, value, notes, source)
3
3
  VALUES (?, ?, ?, ?, ?, ?)
4
4
  ON CONFLICT(project_id, category, key) DO UPDATE SET
@@ -20,10 +20,10 @@ const a=new Set(["color","type","spacing","motion","shadow","radius","breakpoint
20
20
  `).all(e)}deleteToken(e,r,t){return(this.db.prepare(`
21
21
  DELETE FROM design_tokens
22
22
  WHERE project_id = ? AND category = ? AND key = ?
23
- `).run(e,r,t).changes??0)>0}formatForContext(e){const r=this.listTokens(e);if(r.length===0)return"";const t=new Map;for(const s of r){const o=t.get(s.category)??[];o.push(s),t.set(s.category,o)}const n=[];n.push("### \u{1F3A8} Design tokens");for(const[s,o]of t){n.push(""),n.push(`**${s}:**`);for(const c of o)n.push(`- \`${c.key}\` = \`${c.value}\`${c.notes?` \u2014 ${c.notes}`:""}`)}return n.join(`
24
- `)}addReference(e){if(!d.has(e.kind))throw new Error(`invalid kind: ${e.kind} (must be one of ${[...d].join(", ")})`);if(!e.location)throw new Error("location is required");const r=this.db.prepare(`
25
- INSERT INTO design_references (project_id, kind, location, title, notes, tags)
26
- VALUES (?, ?, ?, ?, ?, ?)
23
+ `).run(e,r,t).changes??0)>0}formatForContext(e){const r=this.listTokens(e);if(r.length===0)return"";const t=new Map;for(const s of r){const o=t.get(s.category)??[];o.push(s),t.set(s.category,o)}const n=[];n.push("### \u{1F3A8} Design tokens");for(const[s,o]of t){n.push(""),n.push(`**${s}:**`);for(const a of o)n.push(`- \`${a.key}\` = \`${a.value}\`${a.notes?` \u2014 ${a.notes}`:""}`)}return n.join(`
24
+ `)}addReference(e){if(!c.has(e.kind))throw new Error(`invalid kind: ${e.kind} (must be one of ${[...c].join(", ")})`);if(!e.location)throw new Error("location is required");const r=this.db.prepare(`
25
+ INSERT INTO design_references (project_id, kind, location, title, notes, tags, updated_at)
26
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
27
27
  `).run(e.projectId??null,e.kind,e.location,e.title??null,e.notes??null,e.tags??null);return this.getReference(Number(r.lastInsertRowid))}getReference(e){return this.db.prepare("SELECT * FROM design_references WHERE id = ?").get(e)??null}listReferences(e={}){const r=Math.min(e.limit??50,200);if(e.tag){const t=`%${e.tag}%`;return e.projectId!=null?this.db.prepare(`
28
28
  SELECT * FROM design_references
29
29
  WHERE project_id = ? AND tags LIKE ?
@@ -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 h}from"../cloud-sync.js";import{getCrypto as g,initializeCrypto as f}from"../crypto.js";import{activateLicense as k,getLicenseInfo as w,getTier as v,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 _}from"node:util";import{fileURLToPath as $}from"node:url";import{dirname as S,join as E}from"node:path";const T=_(b);async function P(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=E(S($(import.meta.url)),"..","wyrm-cli.js");try{const{stdout:o,stderr:i}=await T(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 q=[{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=w(),e=v();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}
@@ -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=k(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
74
  `;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**
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. For hands-off periodic sync, 'sovereign-daemon-start' / 'sovereign-daemon-stop' / 'sovereign-daemon-status' run a background daemon. (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, sovereign-daemon-start, sovereign-daemon-stop, sovereign-daemon-status. Legacy snapshot daemon: start, stop, restart, status, force-sync.",enum:["sovereign","sovereign-preview","sovereign-status","sovereign-daemon-start","sovereign-daemon-stop","sovereign-daemon-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,l={sovereign:r?.all?["sync","--all"]:["sync"],"sovereign-preview":["sync","--dry-run"],"sovereign-status":["status"],"sovereign-daemon-start":["daemon","start"],"sovereign-daemon-stop":["daemon","stop"],"sovereign-daemon-status":["daemon","status"]};if(t in l){const s=await P(l[t]);return{content:[{type:"text",text:s.text}],isError:!s.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 y=r?.interval_minutes,m=r?.keep_count;switch(t){case"start":{const s=o.start({interval_minutes:y,keep_count:m});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(p=>{setTimeout(p,500)});const s=o.start({interval_minutes:y,keep_count:m});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(),p=["\u{F115D} **Legacy Snapshot Daemon Status** (not the sovereign path \u2014 see action=sovereign-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(`
77
+ `)}]}}case"force-sync":{const p=await new h(a,i.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(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=g();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{q 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};