wyrm-mcp 9.2.4-next.0 → 9.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BENCHMARKS.md CHANGED
@@ -61,7 +61,7 @@ Retrieval recall is the floor. Two more, all reproducible, all local (added 2026
61
61
 
62
62
  ### Negative learning — the firewall benchmark (the race we own)
63
63
 
64
- No public memory benchmark measures this. `bench/negative-learning.mjs` asks: when an agent is about to **repeat** a recorded mistake, does `wyrm_failure_check` block it, and stay silent on novel actions? Deterministic, no LLM, no cloud. Measured on wyrm-mcp 9.1.2, 2026-09-12 (`bench/firewall-receipt.json`):
64
+ No public memory benchmark measures this. `bench/negative-learning.mjs` asks: when an agent is about to **repeat** a recorded mistake, does `wyrm_failure_check` block it, and stay silent on novel actions? Deterministic, no LLM, no cloud. The figures are those of the committed receipt (`bench/firewall-receipt.json`, regenerated on wyrm-mcp 9.2.1, 2026-09-14):
65
65
 
66
66
  | probe | result |
67
67
  |---|---|
@@ -70,7 +70,13 @@ No public memory benchmark measures this. `bench/negative-learning.mjs` asks: wh
70
70
  | novel actions | **precision 100%** · specificity 100% (0 false blocks of 16) |
71
71
  | confidence separation | 0.86 |
72
72
  | staleness oracle | passes |
73
- | `checkVerdict` latency | p50 2.7 ms · p95 6.1 ms |
73
+ | `checkVerdict` latency, in-process (advisory) | p50 0.29 ms · p95 0.58 ms |
74
+
75
+ What the latency row is. The bench times `FailurePatterns.checkVerdict()` called in-process with `process.hrtime`: 48 calls in one process (16 same-action, 16 reworded, 16 novel) against a temporary store that is already open and holds the 16 recorded failures. No warm-up pass is discarded. It is the cost of the check itself. It is not what a hook pays per tool call: a fresh `wyrm-guard` process also pays Node start-up and the store open, and `bench/guard-budget.mjs` measures that separately (median 62 to 66 ms over 30 spawns per path against a 202-row store, same machine, 2026-09-21).
76
+
77
+ Both are measurements on one machine on one date, not guarantees. The committed figure and the re-runs come from the same machine, a 12th-gen Intel Core i5 laptop (i5-12500H, 16 threads, Linux). Five re-runs on 2026-09-21 (wyrm-mcp 9.2.3, Node 22.23, 1-minute load near 3) gave a median between 0.33 and 0.60 ms and a 95th percentile between 0.67 and 1.47 ms. Latency is advisory: no floor gates it, and it is left out of the receipt's `content_sha256` so that a re-run on the same code reproduces the same hash. `scripts/check-claims.mjs` fails the build if the row above stops matching the committed receipt.
78
+
79
+ Correction, 2026-09-21: until this date the row read a median of 2.7 ms and a 95th percentile of 6.1 ms. That was a single 9.1.2 run on 2026-09-12, and the row was not refreshed when the receipt was regenerated two days later. The 9.1.2 source re-measured on 2026-09-21 gave a median of 0.34 to 0.38 ms, so the old figure described the machine during that one run, not the engine.
74
80
 
75
81
  The reworded tier used to sit at 12.5% (8.5.3), because the fuzzy probe was a conjunctive-AND FTS kept precision-first by design. Target normalization (canonical command matching, flag order and `=` form folded, positionals kept in order) closed that gap without giving up a single false block. Run: `node bench/negative-learning.mjs`.
76
82
 
package/COMPARISON.md CHANGED
@@ -57,6 +57,11 @@ convs / 1,982 evidence-bearing QA, k=10; re-run 2026-06-27, HEAD).
57
57
  \* the embedding model is a local Ollama call, not a generative LLM; the FTS floor needs
58
58
  no model at all and is the CI-gated, fully-deterministic number.
59
59
 
60
+ These rows were measured through Ollama. Since 8.7.0 the default install embeds
61
+ in-process with a bundled int8 build of the same model and needs no Ollama. Swapped into
62
+ the same harness on 2026-08-14 it measured recall@1 32.5, recall@5 60.0, recall@10 72.0,
63
+ MRR .443 (see [`BENCHMARKS.md`](./BENCHMARKS.md)).
64
+
60
65
  ### Competitors — QA answer accuracy (LLM-judge), vendor-self-reported
61
66
 
62
67
  | System | Benchmark | Metric | Score | Source |
package/README.md CHANGED
@@ -72,7 +72,7 @@ Most memory tools store successes. Wyrm also records dead-ends and blocks the re
72
72
 
73
73
  ### Local-first, and honest about egress
74
74
 
75
- By default no memory data leaves your machine. The database is a single SQLite file at `~/.wyrm/wyrm.db`. The only default outbound calls are a daily npm version poll and, on installs with a license key, a signed revocation-list fetch — both off with `WYRM_NO_VERSION_CHECK=1` and `WYRM_LICENSE_REFRESH=0` respectively; the free tier's one-time model download is a plain fetch that sends nothing. `docs/EGRESS.md` in the repository enumerates every destination, and if you find an outbound call that is not on that table, that is a bug worth reporting. 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.
75
+ By default no memory data leaves your machine. The database is a single SQLite file at `~/.wyrm/wyrm.db`. The only default outbound calls are a daily npm version poll and, on installs with a license key or an account sign-in, a signed revocation-list fetch and a license check (off with `WYRM_NO_VERSION_CHECK=1` and `WYRM_LICENSE_REFRESH=0` respectively); the free tier's one-time model download is a plain fetch that sends nothing. `docs/EGRESS.md` in the repository enumerates every destination, and if you find an outbound call that is not on that table, that is a bug worth reporting. 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
77
  ### Every write leaves a receipt
78
78
 
@@ -116,7 +116,9 @@ Wyrm is **free to use forever** — the local memory, the firewall, the recall,
116
116
  - **Team** — $199/mo · shared team memory, up to 25 seats, admin dashboard
117
117
  - **Enterprise** — $499/mo · unlimited seats, SSO/SAML, custom SLA, on-premise option
118
118
 
119
- Plans and self-serve checkout: **[account.ghosts.lk/pricing](https://ghosts.lk/pricing)**. Custom or on-premise, email [ryan@ghosts.lk](mailto:ryan@ghosts.lk).
119
+ Paid plans are arranged by email, not a checkout: write to **[support@ghosts.lk](mailto:support@ghosts.lk)** and we will set it up with you. Custom or on-premise, same address. The plans are listed at [ghosts.lk/wyrm](https://ghosts.lk/wyrm#pricing).
120
+
121
+ Local Wyrm stays free. If it is useful to you and you want to support the work, you can [buy us a coffee](https://buymeacoffee.com/ghosts.lk). That is a donation, not a plan.
120
122
 
121
123
  ## Community and feedback
122
124
 
@@ -1,4 +1,4 @@
1
- import{join as R,resolve as _,relative as M,isAbsolute as L}from"path";import{homedir as N}from"os";import{existsSync as T}from"fs";import{createInterface as F}from"readline";import{c as d,formatTable as v,printSection as k,printSuccess as S,printError as g,printWarning as C}from"./cli.js";import{createBackup as I,listBackups as U,pruneBackups as D,planRotation as O,restoreBackup as j,scanBackupHygiene as P,applyLegacyActions as H,defaultBackupDir as V,vaultBackupKey as A,DEFAULT_KEEP_DAILY as z,DEFAULT_KEEP_WEEKLY as G}from"./backup.js";function q(n){const a=[],e={};for(let u=0;u<n.length;u++){const l=n[u];if(l.startsWith("--")){const i=n[u+1];i!==void 0&&!i.startsWith("--")?(e[l.slice(2)]=i,u++):e[l.slice(2)]=!0}else a.push(l)}return{positional:a,flags:e}}function J(){return process.env.WYRM_DB_PATH??R(N(),".wyrm","wyrm.db")}function W(n,a,e){const u=typeof n=="string"?n:a,l=u===void 0?NaN:parseInt(u,10);return Number.isFinite(l)&&l>=0?l:e}function m(n){return n<1024?`${n} B`:n<1024**2?`${(n/1024).toFixed(1)} KB`:n<1024**3?`${(n/1024**2).toFixed(1)} MB`:`${(n/1024**3).toFixed(2)} GB`}function Q(n){const a=(Date.now()-n.getTime())/36e5;return a<1?"under 1h":a<48?`${Math.floor(a)}h`:`${Math.floor(a/24)}d`}function Y(n){const a=typeof n["passphrase-env"]=="string"?n["passphrase-env"]:"WYRM_BACKUP_PASSPHRASE",e=process.env[a];if(!e)throw new Error(`set ${a} to the backup passphrase (read from the environment, never from the command line)`);return{source:"passphrase",secret:e}}function X(n){return a=>a==="vault"?A(!1):Y(n)}async function B(n,a){if(!process.stdin.isTTY)return!1;const e=F({input:process.stdin,output:process.stdout});try{return(await new Promise(l=>e.question(n,l))).trim()===a}finally{e.close()}}function f(n,a){const e=M(a,n);return e&&!e.startsWith("..")&&!L(e)?e:n}const K=["wyrm backup create [--encrypt | --passphrase-env <VAR>] [--label <name>] [--no-prune]","wyrm backup list","wyrm backup prune [--dry-run] [--keep-daily <n>] [--keep-weekly <n>] [--include-labelled]","wyrm backup restore <file> [--dry-run | --apply [--yes]] [--target <db>] [--passphrase-env <VAR>]","wyrm backup legacy [--dry-run | --apply [--adopt | --delete --yes]]","wyrm backup cloud [--prune <prefix> | --stale-days <n>] [--yes]"];async function re(n){const{positional:a,flags:e}=q(n),u=a[0]??"list",l=J(),i=typeof e.dir=="string"?_(e.dir):V(l),w=W(e["keep-daily"],process.env.WYRM_BACKUP_KEEP_DAILY,z),$=W(e["keep-weekly"],process.env.WYRM_BACKUP_KEEP_WEEKLY,G);try{switch(u){case"create":{const s=e.encrypt===!0?A(!0):typeof e["passphrase-env"]=="string"?Y(e):void 0,t=await I({dbPath:l,dir:i,label:typeof e.label=="string"?e.label:void 0,key:s});if(S(`Snapshot ${t.file} \xB7 ${m(t.bytes)} \xB7 ${t.encrypted?`encrypted (${s.source} key)`:"plaintext"} \xB7 mode 0600 in ${i}`),s||console.log(d.dim(" plaintext: anyone who can read this file can read the whole store. --encrypt keys it from the vault.")),e["no-prune"]!==!0){const c=D(i,{daily:w,weekly:$,dryRun:!1});c.remove.length>0&&console.log(` Rotation removed ${c.remove.length} older snapshot(s) (keeping ${w} daily, ${$} weekly).`)}return}case"list":case"ls":{const s=U(i);if(k(`Snapshots in ${i}`),s.length===0)console.log(d.dim(" none yet: wyrm backup create --encrypt"));else{const c=O(s,{daily:w,weekly:$}),y=new Set(c.keep.map(o=>o.path)),r=new Set(c.pinned.map(o=>o.path));console.log(v(["File","Size","Mode","Encrypted","Age","Rotation"],s.map(o=>[o.file,m(o.bytes),`0${o.mode.toString(8)}`,o.encrypted?"yes":"no",Q(o.at),r.has(o.path)?"pinned (labelled)":y.has(o.path)?"keep":"next prune"])))}const t=P({dbPath:l,dir:i});(t.legacy.length>0||t.exposed.length>0)&&C(`${t.legacy.length} database copy(ies) outside rotation, ${t.exposed.length} item(s) readable by others: wyrm backup legacy --dry-run`);return}case"prune":{const s=e["dry-run"]===!0,t=D(i,{daily:w,weekly:$,dryRun:s,includeLabelled:e["include-labelled"]===!0});k(s?"Rotation (dry run)":"Rotation"),console.log(` keep ${t.keep.length} (${w} daily, ${$} weekly, newest always)`),t.pinned.length>0&&console.log(` pinned ${t.pinned.length} labelled snapshot(s); --include-labelled rotates them too`);for(const c of t.remove)console.log(` ${s?"would remove":"removed"} ${c.file} ${m(c.bytes)}`);t.remove.length===0?console.log(d.dim(" nothing to remove")):s&&console.log(d.dim(`
1
+ import{join as R,resolve as _,relative as M,isAbsolute as L}from"path";import{homedir as N}from"os";import{existsSync as T}from"fs";import{createInterface as F}from"readline";import{c as d,formatTable as v,printSection as k,printSuccess as S,printError as g,printWarning as C}from"./cli.js";import{createBackup as I,listBackups as U,pruneBackups as D,planRotation as O,restoreBackup as j,scanBackupHygiene as P,applyLegacyActions as H,defaultBackupDir as V,vaultBackupKey as A,DEFAULT_KEEP_DAILY as z,DEFAULT_KEEP_WEEKLY as G}from"./backup.js";function q(n){const a=[],e={};for(let u=0;u<n.length;u++){const l=n[u];if(l.startsWith("--")){const i=n[u+1];i!==void 0&&!i.startsWith("--")?(e[l.slice(2)]=i,u++):e[l.slice(2)]=!0}else a.push(l)}return{positional:a,flags:e}}function J(){return process.env.WYRM_DB_PATH??R(N(),".wyrm","wyrm.db")}function W(n,a,e){const u=typeof n=="string"?n:a,l=u===void 0?NaN:parseInt(u,10);return Number.isFinite(l)&&l>=0?l:e}function m(n){return n<1024?`${n} B`:n<1024**2?`${(n/1024).toFixed(1)} KB`:n<1024**3?`${(n/1024**2).toFixed(1)} MB`:`${(n/1024**3).toFixed(2)} GB`}function Q(n){const a=(Date.now()-n.getTime())/36e5;return a<1?"under 1h":a<48?`${Math.floor(a)}h`:`${Math.floor(a/24)}d`}function Y(n){const a=typeof n["passphrase-env"]=="string"?n["passphrase-env"]:"WYRM_BACKUP_PASSPHRASE",e=process.env[a];if(!e)throw new Error(`set ${a} to the backup passphrase (read from the environment, never from the command line)`);return{source:"passphrase",secret:e}}function X(n){return a=>a==="vault"?A(!1):Y(n)}async function B(n,a){if(!process.stdin.isTTY)return!1;const e=F({input:process.stdin,output:process.stdout});try{return(await new Promise(l=>{e.question(n,l)})).trim()===a}finally{e.close()}}function f(n,a){const e=M(a,n);return e&&!e.startsWith("..")&&!L(e)?e:n}const K=["wyrm backup create [--encrypt | --passphrase-env <VAR>] [--label <name>] [--no-prune]","wyrm backup list","wyrm backup prune [--dry-run] [--keep-daily <n>] [--keep-weekly <n>] [--include-labelled]","wyrm backup restore <file> [--dry-run | --apply [--yes]] [--target <db>] [--passphrase-env <VAR>]","wyrm backup legacy [--dry-run | --apply [--adopt | --delete --yes]]","wyrm backup cloud [--prune <prefix> | --stale-days <n>] [--yes]"];async function re(n){const{positional:a,flags:e}=q(n),u=a[0]??"list",l=J(),i=typeof e.dir=="string"?_(e.dir):V(l),w=W(e["keep-daily"],process.env.WYRM_BACKUP_KEEP_DAILY,z),$=W(e["keep-weekly"],process.env.WYRM_BACKUP_KEEP_WEEKLY,G);try{switch(u){case"create":{const s=e.encrypt===!0?A(!0):typeof e["passphrase-env"]=="string"?Y(e):void 0,t=await I({dbPath:l,dir:i,label:typeof e.label=="string"?e.label:void 0,key:s});if(S(`Snapshot ${t.file} \xB7 ${m(t.bytes)} \xB7 ${t.encrypted?`encrypted (${s.source} key)`:"plaintext"} \xB7 mode 0600 in ${i}`),s||console.log(d.dim(" plaintext: anyone who can read this file can read the whole store. --encrypt keys it from the vault.")),e["no-prune"]!==!0){const c=D(i,{daily:w,weekly:$,dryRun:!1});c.remove.length>0&&console.log(` Rotation removed ${c.remove.length} older snapshot(s) (keeping ${w} daily, ${$} weekly).`)}return}case"list":case"ls":{const s=U(i);if(k(`Snapshots in ${i}`),s.length===0)console.log(d.dim(" none yet: wyrm backup create --encrypt"));else{const c=O(s,{daily:w,weekly:$}),y=new Set(c.keep.map(o=>o.path)),r=new Set(c.pinned.map(o=>o.path));console.log(v(["File","Size","Mode","Encrypted","Age","Rotation"],s.map(o=>[o.file,m(o.bytes),`0${o.mode.toString(8)}`,o.encrypted?"yes":"no",Q(o.at),r.has(o.path)?"pinned (labelled)":y.has(o.path)?"keep":"next prune"])))}const t=P({dbPath:l,dir:i});(t.legacy.length>0||t.exposed.length>0)&&C(`${t.legacy.length} database copy(ies) outside rotation, ${t.exposed.length} item(s) readable by others: wyrm backup legacy --dry-run`);return}case"prune":{const s=e["dry-run"]===!0,t=D(i,{daily:w,weekly:$,dryRun:s,includeLabelled:e["include-labelled"]===!0});k(s?"Rotation (dry run)":"Rotation"),console.log(` keep ${t.keep.length} (${w} daily, ${$} weekly, newest always)`),t.pinned.length>0&&console.log(` pinned ${t.pinned.length} labelled snapshot(s); --include-labelled rotates them too`);for(const c of t.remove)console.log(` ${s?"would remove":"removed"} ${c.file} ${m(c.bytes)}`);t.remove.length===0?console.log(d.dim(" nothing to remove")):s&&console.log(d.dim(`
2
2
  Dry run: nothing was deleted. Re-run without --dry-run to remove these.`));return}case"restore":{const s=a[1];if(!s){g(`usage: ${K[3]}`),process.exitCode=1;return}const t=T(s)?_(s):R(i,s);if(!T(t)){g(`no snapshot at ${s} (also looked in ${i})`),process.exitCode=1;return}const c=e.apply===!0&&e["dry-run"]!==!0,y=typeof e.target=="string"?_(e.target):l;if(c&&e.yes!==!0&&!await B(`Restore ${f(t,i)} over ${y}? The current store is snapshotted first. Type RESTORE to continue: `,"RESTORE")){g("Restore not confirmed; nothing changed. Pass --yes to skip the prompt."),process.exitCode=1;return}const r=await j({file:t,targetPath:y,apply:c,workDir:R(i,".work"),keyFor:X(e),safetyDir:i});k(c?"Restored":"Restore check (dry run)"),console.log(` snapshot ${f(t,i)}${r.inspection.encrypted?" (encrypted)":""}`),console.log(` integrity ${r.inspection.integrity}`),console.log(` schema v${r.inspection.schemaVersion}`);const o=Object.keys(r.inspection.counts);console.log(v(["Table","Snapshot",r.current?"Current store":"Current store (absent)"],o.map(h=>[h,String(r.inspection.counts[h]),r.current?String(r.current[h]??"-"):"-"]))),c?(S(`Restored into ${y}; integrity ${r.targetIntegrity}, row counts match the snapshot.`),r.safetySnapshot&&console.log(` The store as it was before is ${f(r.safetySnapshot,i)}.`),console.log(d.dim(" Restart open Wyrm sessions so their servers read the restored store."))):console.log(d.dim(`
3
3
  Dry run: nothing was written. Re-run with --apply to restore.`));return}case"legacy":{const s=e.apply===!0&&e["dry-run"]!==!0,t=P({dbPath:l,dir:i});k(s?"Legacy backups":"Legacy backups (dry run)");const c=t.legacy.reduce((o,h)=>o+h.bytes,0);console.log(` ${t.legacy.length} database copy(ies) outside rotation, ${m(c)}`);for(const o of t.legacy)console.log(` ${f(o.path,t.home)} 0${o.mode.toString(8)} ${m(o.bytes)}${o.sidecars.length?` +${o.sidecars.length} sidecar(s)`:""}`);process.platform==="win32"?console.log(" File permissions: not checked on Windows, where access is governed by ACLs rather than mode bits."):console.log(` ${t.exposed.length} item(s) readable by group or others`);for(const o of t.exposed)console.log(` ${f(o.path,t.home)} 0${o.mode.toString(8)}`);if(!s){console.log(d.dim(`
4
4
  Dry run: nothing changed.`)),console.log(d.dim(" --apply tighten permissions (files 0600, folders 0700)")),console.log(d.dim(" --apply --adopt also move the copies into rotation, where prune ages them out")),console.log(d.dim(" --apply --delete delete the copies instead (asks you to type DELETE unless --yes)"));return}const y=e.delete===!0;if(y&&e.yes!==!0&&!await B(`Delete ${t.legacy.length} database copy(ies)? This cannot be undone. Type DELETE to continue: `,"DELETE")){g("Delete not confirmed; nothing was deleted. Pass --yes to skip the prompt."),process.exitCode=1;return}const r=H({dbPath:l,dir:i,tighten:!0,adopt:e.adopt===!0,deleteCopies:y});S(`Tightened ${r.tightened} item(s); adopted ${r.adopted.length}; deleted ${r.deleted.length}.`);for(const o of r.adopted)console.log(` ${f(o.from,t.home)} -> ${f(o.to,t.home)}`);for(const o of r.skipped)C(`skipped ${f(o.path,t.home)}: ${o.why}`);return}case"cloud":{const{WyrmCloudBackup:s}=await import("./cloud-backup.js"),{WYRM_R2_ENDPOINT:t,WYRM_R2_ACCESS_KEY:c,WYRM_R2_SECRET_KEY:y}=process.env;if(!t||!c||!y){g("cloud snapshots need WYRM_R2_ENDPOINT, WYRM_R2_ACCESS_KEY and WYRM_R2_SECRET_KEY (and WYRM_R2_BUCKET if not wyrm-backups)."),process.exitCode=1;return}const r=new s;r.configure({endpoint:t,bucket:process.env.WYRM_R2_BUCKET||"wyrm-backups",accessKeyId:c,secretAccessKey:y});const o=await r.listSnapshotPrefixes();k("Cloud snapshot prefixes"),console.log(v(["Prefix","Machine","Snapshots","Size","Newest"],o.map(p=>[p.prefix,p.current?"this machine":"other",String(p.snapshots),m(p.bytes),p.newest?p.newest.slice(0,10):"-"])));const h=typeof e["stale-days"]=="string"?parseInt(e["stale-days"],10):NaN,x=typeof e.prune=="string"?[e.prune]:Number.isFinite(h)&&h>0?o.filter(p=>!p.current&&p.newest&&Date.now()-Date.parse(p.newest)>h*864e5).map(p=>p.prefix):[];if(x.length===0){console.log(d.dim(`
package/dist/bridge.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as h,writeFileSync as D,readdirSync as k,existsSync as F}from"fs";import{createHash as _}from"crypto";import{join as y}from"path";import{homedir as g}from"os";import{MemoryArtifacts as b}from"./memory-artifacts.js";import{recordWrite as R}from"./receipts.js";import{buildRenderPlan as I,renderToDisk as L,renderForClient as A,makeRenderDeps as W}from"./render-target.js";import{makeBridgeDeps as x,sweepProject as $}from"./reverse-bridge.js";import{runAsWriter as q}from"./handlers/boundary.js";function B(){return process.env.WYRM_BRIDGE_CONFIG??y(g(),".wyrm","bridge.json")}function C(){return process.env.WYRM_BRIDGE_STATE??y(g(),".wyrm","bridge-state.json")}function re(){try{return JSON.parse(h(B(),"utf8"))}catch{return null}}function H(){try{return JSON.parse(h(C(),"utf8"))}catch{return{}}}function v(t){try{D(C(),JSON.stringify(t,null,1))}catch{}}function N(t,i,r){const n=r??y(g(),".wyrm","remember-distill-state.json");if(!F(n))return 0;let c=0;try{const e=JSON.parse(h(n,"utf8"));for(const[d,a]of Object.entries(e)){const s=`${t}:${d}`;i[s]||(i[s]=a,c++)}}catch{}return c}const J=t=>_("sha1").update(t).digest("hex");function G(t,i,r,n,c={}){const e={source:r.name,type:"remember",stored:0,dropped:0,queued:0,rendered:0},d=r.windowDays&&r.windowDays>=1?r.windowDays:7,a=r.maxPerRun&&r.maxPerRun>=1?r.maxPerRun:80,s=t.prepare("SELECT id FROM projects WHERE path = ?").get(r.projectPath);if(!s)return e.note=`no project at ${r.projectPath}`,e;const p=new Date(Date.now()-d*864e5);let u=[];try{u=k(r.dir).filter(o=>/^today-\d{4}-\d{2}-\d{2}(\.done)?\.md$/.test(o)).map(o=>({f:o,date:o.match(/\d{4}-\d{2}-\d{2}/)[0]})).filter(({date:o})=>new Date(o+"T00:00:00")>=p).sort((o,m)=>m.date.localeCompare(o.date))}catch{return e.note=`cannot read ${r.dir}`,e}let f=0;for(const{f:o,date:m}of u){const S=`${r.name}:today-${m}`,w=new Set(n[S]??[]);let E="";try{E=h(y(r.dir,o),"utf8")}catch{continue}const O=E.split(/^## /m).slice(1).map(l=>"## "+l.trim());for(const l of O){if(f>=a)break;if(l.length<30)continue;const j=J(l);if(w.has(j))continue;if(c.dryRun){f++,e.stored++;continue}const P=`[bridge:${r.name} ${m}] ${l}`.slice(0,2400),T=q("bridge:remember",()=>i.add(s.id,{kind:"pattern",problem:P,confidence:.75,needsReview:0,createdBy:`bridge:${r.name}`}));R(t,{tool:"wyrm_bridge",outcome:"stored",refTable:"mem",refId:T.id,source:`bridge:${r.name}`,projectId:s.id}),w.add(j),f++,e.stored++}n[S]=[...w]}return f>=a&&(e.note=`hit per-run cap ${a}; remainder next run`),e}async function M(t,i,r,n={}){const c={source:i.name,type:"render",stored:0,dropped:0,queued:0,rendered:0},e=t.prepare("SELECT id, name, path FROM projects WHERE path = ?").get(i.projectPath);if(!e)return c.note=`no project at ${i.projectPath}`,c;const d=i.clients??[],a=W(t);try{const s=I(a,e,r),p={"MEMORY.md":s.memoryMd};for(const f of d){const o=A(f,s.model,r);p[o.relPath]=o.block}const u=await $(x(t),{id:e.id,path:e.path},p,{dryRun:n.dryRun});c.queued+=u.added,u.added>0&&R(t,{tool:"wyrm_bridge",outcome:"queued",reason:`render guard: ${u.added} human edit(s) harvested to review before overwrite`,source:`bridge:${i.name}`,projectId:e.id})}catch{}if(!n.dryRun){const{writes:s}=L(a,e,r,{clients:d});c.rendered=s.filter(p=>p.action!=="skipped").length}return c}async function Y(t,i,r={}){const n={source:i.name,type:"reverse",stored:0,dropped:0,queued:0,rendered:0},c=t.prepare("SELECT id, path FROM projects WHERE path = ?").get(i.projectPath);if(!c)return n.note=`no project at ${i.projectPath}`,n;const e=await $(x(t),c,{},{dryRun:r.dryRun});return n.queued=e.added,n.dropped=e.skipped,e.added>0&&R(t,{tool:"wyrm_bridge",outcome:"queued",reason:`reverse sweep: ${e.added} native-file edit(s) queued for review`,source:`bridge:${i.name}`,projectId:c.id}),n}async function ne(t,i,r,n={}){const c=new b(t),e=H(),d=[];for(const a of i.sources)n.only&&a.name!==n.only||(a.type==="remember"?(N(a.name,e)>0&&v(e),d.push(G(t,c,a,e,n))):a.type==="render"?d.push(await M(t,a,r,n)):d.push(await Y(t,a,n)));return n.dryRun||v(e),d}async function oe(t,i,r,n={}){const c=Math.max(2,n.intervalSec??15)*1e3,e=i.sources.filter(p=>p.type==="render");if(e.length===0)return;const d=new Map;for(const p of e){const u=t.prepare("SELECT id FROM projects WHERE path = ?").get(p.projectPath);u&&d.set(p.name,u.id)}let a=n.fromCursor??t.prepare("SELECT COALESCE(MAX(cursor), 0) AS c FROM events").get().c,s=0;for(;n.iterations==null||s<n.iterations;){s++,await new Promise(o=>{setTimeout(o,c)});const p=t.prepare("SELECT DISTINCT project_id FROM events WHERE cursor > ?").all(a);if(a=t.prepare("SELECT COALESCE(MAX(cursor), 0) AS c FROM events").get().c,p.length===0)continue;const u=new Set(p.map(o=>String(o.project_id))),f=[];for(const o of e){const m=d.get(o.name);if(!(m==null||!u.has(String(m))))try{f.push(await M(t,o,r()))}catch{}}f.length>0&&n.onCycle?.(f)}}export{N as adoptLegacyDistillState,B as configPath,re as loadConfig,H as loadState,ne as runBridge,G as runRememberSource,M as runRenderSource,Y as runReverseSource,v as saveState,C as statePath,oe as watchBridge};
1
+ import{readFileSync as h,writeFileSync as k,readdirSync as F,existsSync as _}from"fs";import{createHash as I}from"crypto";import{join as y}from"path";import{homedir as j}from"os";import{MemoryArtifacts as L}from"./memory-artifacts.js";import{recordWrite as R}from"./receipts.js";import{buildRenderPlan as A,renderToDisk as B,renderForClient as W,makeRenderDeps as q}from"./render-target.js";import{makeBridgeDeps as x,sweepProject as $}from"./reverse-bridge.js";import{runAsWriter as b}from"./handlers/boundary.js";import{projectByPathKey as w}from"./project-resolve.js";function H(){return process.env.WYRM_BRIDGE_CONFIG??y(j(),".wyrm","bridge.json")}function C(){return process.env.WYRM_BRIDGE_STATE??y(j(),".wyrm","bridge-state.json")}function ne(){try{return JSON.parse(h(H(),"utf8"))}catch{return null}}function N(){try{return JSON.parse(h(C(),"utf8"))}catch{return{}}}function v(e){try{k(C(),JSON.stringify(e,null,1))}catch{}}function J(e,n,r){const o=r??y(j(),".wyrm","remember-distill-state.json");if(!_(o))return 0;let c=0;try{const t=JSON.parse(h(o,"utf8"));for(const[s,a]of Object.entries(t)){const p=`${e}:${s}`;n[p]||(n[p]=a,c++)}}catch{}return c}const G=e=>I("sha1").update(e).digest("hex");function Y(e,n,r,o,c={}){const t={source:r.name,type:"remember",stored:0,dropped:0,queued:0,rendered:0},s=r.windowDays&&r.windowDays>=1?r.windowDays:7,a=r.maxPerRun&&r.maxPerRun>=1?r.maxPerRun:80,p=e.prepare("SELECT id FROM projects WHERE path = ?").get(r.projectPath)??w(e,r.projectPath);if(!p)return t.note=`no project at ${r.projectPath}`,t;const d=new Date(Date.now()-s*864e5);let u=[];try{u=F(r.dir).filter(i=>/^today-\d{4}-\d{2}-\d{2}(\.done)?\.md$/.test(i)).map(i=>({f:i,date:i.match(/\d{4}-\d{2}-\d{2}/)[0]})).filter(({date:i})=>new Date(i+"T00:00:00")>=d).sort((i,m)=>m.date.localeCompare(i.date))}catch{return t.note=`cannot read ${r.dir}`,t}let f=0;for(const{f:i,date:m}of u){const S=`${r.name}:today-${m}`,g=new Set(o[S]??[]);let E="";try{E=h(y(r.dir,i),"utf8")}catch{continue}const O=E.split(/^## /m).slice(1).map(l=>"## "+l.trim());for(const l of O){if(f>=a)break;if(l.length<30)continue;const P=G(l);if(g.has(P))continue;if(c.dryRun){f++,t.stored++;continue}const T=`[bridge:${r.name} ${m}] ${l}`.slice(0,2400),D=b("bridge:remember",()=>n.add(p.id,{kind:"pattern",problem:T,confidence:.75,needsReview:0,createdBy:`bridge:${r.name}`}));R(e,{tool:"wyrm_bridge",outcome:"stored",refTable:"mem",refId:D.id,source:`bridge:${r.name}`,projectId:p.id}),g.add(P),f++,t.stored++}o[S]=[...g]}return f>=a&&(t.note=`hit per-run cap ${a}; remainder next run`),t}async function M(e,n,r,o={}){const c={source:n.name,type:"render",stored:0,dropped:0,queued:0,rendered:0},t=e.prepare("SELECT id, name, path FROM projects WHERE path = ?").get(n.projectPath)??w(e,n.projectPath);if(!t)return c.note=`no project at ${n.projectPath}`,c;const s=n.clients??[],a=q(e);try{const p=A(a,t,r),d={"MEMORY.md":p.memoryMd};for(const f of s){const i=W(f,p.model,r);d[i.relPath]=i.block}const u=await $(x(e),{id:t.id,path:t.path},d,{dryRun:o.dryRun});c.queued+=u.added,u.added>0&&R(e,{tool:"wyrm_bridge",outcome:"queued",reason:`render guard: ${u.added} human edit(s) harvested to review before overwrite`,source:`bridge:${n.name}`,projectId:t.id})}catch{}if(!o.dryRun){const{writes:p}=B(a,t,r,{clients:s});c.rendered=p.filter(d=>d.action!=="skipped").length}return c}async function X(e,n,r={}){const o={source:n.name,type:"reverse",stored:0,dropped:0,queued:0,rendered:0},c=e.prepare("SELECT id, path FROM projects WHERE path = ?").get(n.projectPath)??w(e,n.projectPath);if(!c)return o.note=`no project at ${n.projectPath}`,o;const t=await $(x(e),c,{},{dryRun:r.dryRun});return o.queued=t.added,o.dropped=t.skipped,t.added>0&&R(e,{tool:"wyrm_bridge",outcome:"queued",reason:`reverse sweep: ${t.added} native-file edit(s) queued for review`,source:`bridge:${n.name}`,projectId:c.id}),o}async function ie(e,n,r,o={}){const c=new L(e),t=N(),s=[];for(const a of n.sources)o.only&&a.name!==o.only||(a.type==="remember"?(J(a.name,t)>0&&v(t),s.push(Y(e,c,a,t,o))):a.type==="render"?s.push(await M(e,a,r,o)):s.push(await X(e,a,o)));return o.dryRun||v(t),s}async function ae(e,n,r,o={}){const c=Math.max(2,o.intervalSec??15)*1e3,t=n.sources.filter(d=>d.type==="render");if(t.length===0)return;const s=new Map;for(const d of t){const u=e.prepare("SELECT id FROM projects WHERE path = ?").get(d.projectPath)??w(e,d.projectPath);u&&s.set(d.name,u.id)}let a=o.fromCursor??e.prepare("SELECT COALESCE(MAX(cursor), 0) AS c FROM events").get().c,p=0;for(;o.iterations==null||p<o.iterations;){p++,await new Promise(i=>{setTimeout(i,c)});const d=e.prepare("SELECT DISTINCT project_id FROM events WHERE cursor > ?").all(a);if(a=e.prepare("SELECT COALESCE(MAX(cursor), 0) AS c FROM events").get().c,d.length===0)continue;const u=new Set(d.map(i=>String(i.project_id))),f=[];for(const i of t){const m=s.get(i.name);if(!(m==null||!u.has(String(m))))try{f.push(await M(e,i,r()))}catch{}}f.length>0&&o.onCycle?.(f)}}export{J as adoptLegacyDistillState,H as configPath,ne as loadConfig,N as loadState,ie as runBridge,Y as runRememberSource,M as runRenderSource,X as runReverseSource,v as saveState,C as statePath,ae as watchBridge};
package/dist/buddy.js CHANGED
@@ -1,11 +1,11 @@
1
- import{ICON as m}from"./icons.js";const $=[" __----~~~~~~~~~~~------___"," . . ~~//====...... __--~ ~~"," -. \\_|// |||\\\\ ~~~~~~::::... /~"].join(`
2
- `),S=[" __---~~~.==~||\\=_ -_--~/_-~|- |\\\\ \\\\ _/~"," .=~ | \\\\-_ '-~7 /- / || \\ /"," .~ .~ | \\\\ -_ / /- / || \\"," / ____ / | \\\\ ~-_/ /|- _/ .|| \\"," |~~ ~~|--~~~~--_ \\ ~==-/ | \\~--===~~ .\\"," ' ~-| /| |-~\\~~ __--~~"," ' \\~~|\\~~ --~~~~__-~"," \\~~~==---____"].join(`
3
- `);function R(e){switch(e){case"normal":return" ___-==_ _-~o~ \\/ ||| \\\\ _/~~-";case"celebratory":return" ___-==_ _-~^~ \\/ ||| \\\\ * _/~~-";case"stuck":return" ___-==_ _-~-~ \\/ ||| \\\\ ? _/~~-";case"quiet":return null;default:return null}}const b="1.0";function I(e,o,i){const u=(o.from_buddy??"").toLowerCase().startsWith("wyrm");let s=null;if(o.project_hint&&!u)try{const r=e.prepare("SELECT id, name FROM projects WHERE path = ?").get(o.project_hint);if(r)s=r;else{const d=e.prepare("SELECT id, name FROM projects WHERE name = ? COLLATE NOCASE LIMIT 1").get(o.project_hint);d&&(s=d)}}catch{}const t=o.size??"compact",a=o.mood_hint??"normal",l=[];if(s){const r=L(e,s.id);r.topPendingQuest&&l.push({kind:"quest",id:`quest:${r.topPendingQuest.id}`,title:r.topPendingQuest.title}),r.recentlyCompletedQuest&&l.push({kind:"completion",id:`quest:${r.recentlyCompletedQuest.id}`,title:r.recentlyCompletedQuest.title}),r.activeGoal&&l.push({kind:"goal",id:`goal:${r.activeGoal.id}`,title:r.activeGoal.title}),r.unresolvedFailures[0]&&l.push({kind:"failure",id:`failure:${r.unresolvedFailures[0].id}`,title:r.unresolvedFailures[0].description.slice(0,60)})}else try{const r=e.prepare(`SELECT id, title, project_id FROM quests WHERE status='pending'
4
- ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END LIMIT 1`).get();r&&l.push({kind:"quest",id:`quest:${r.id}`,title:r.title})}catch{}let c;if(u)c="(cycle: Wyrm already in the call chain, skipping)";else if(s){const r=l.length;c=`Wyrm sees ${r} highlight${r===1?"":"s"} for project ${s.name}.`}else c=`Wyrm watching across ${T(e)} projects.`;let n;if(u)n="_(wyrm: cycle detected, no reply)_";else if(t==="mini")n=`<--==(${a==="celebratory"?"^":a==="stuck"?"-":"o"})~~~~~~__ ${c}`;else if(t==="compact"){const r=["\u{F115D} **wyrm:** "+c];for(const d of l.slice(0,3))r.push(`- \`${d.id}\` ${d.title}`);n=r.join(`
1
+ import{ICON as m}from"./icons.js";import{projectByPathKey as $}from"./project-resolve.js";const S=[" __----~~~~~~~~~~~------___"," . . ~~//====...... __--~ ~~"," -. \\_|// |||\\\\ ~~~~~~::::... /~"].join(`
2
+ `),R=[" __---~~~.==~||\\=_ -_--~/_-~|- |\\\\ \\\\ _/~"," .=~ | \\\\-_ '-~7 /- / || \\ /"," .~ .~ | \\\\ -_ / /- / || \\"," / ____ / | \\\\ ~-_/ /|- _/ .|| \\"," |~~ ~~|--~~~~--_ \\ ~==-/ | \\~--===~~ .\\"," ' ~-| /| |-~\\~~ __--~~"," ' \\~~|\\~~ --~~~~__-~"," \\~~~==---____"].join(`
3
+ `);function T(e){switch(e){case"normal":return" ___-==_ _-~o~ \\/ ||| \\\\ _/~~-";case"celebratory":return" ___-==_ _-~^~ \\/ ||| \\\\ * _/~~-";case"stuck":return" ___-==_ _-~-~ \\/ ||| \\\\ ? _/~~-";case"quiet":return null;default:return null}}const b="1.0";function H(e,o,i){const u=(o.from_buddy??"").toLowerCase().startsWith("wyrm");let s=null;if(o.project_hint&&!u)try{const r=e.prepare("SELECT id, name FROM projects WHERE path = ?").get(o.project_hint)??$(e,o.project_hint);if(r)s=r;else{const d=e.prepare("SELECT id, name FROM projects WHERE name = ? COLLATE NOCASE LIMIT 1").get(o.project_hint);d&&(s=d)}}catch{}const t=o.size??"compact",a=o.mood_hint??"normal",l=[];if(s){const r=D(e,s.id);r.topPendingQuest&&l.push({kind:"quest",id:`quest:${r.topPendingQuest.id}`,title:r.topPendingQuest.title}),r.recentlyCompletedQuest&&l.push({kind:"completion",id:`quest:${r.recentlyCompletedQuest.id}`,title:r.recentlyCompletedQuest.title}),r.activeGoal&&l.push({kind:"goal",id:`goal:${r.activeGoal.id}`,title:r.activeGoal.title}),r.unresolvedFailures[0]&&l.push({kind:"failure",id:`failure:${r.unresolvedFailures[0].id}`,title:r.unresolvedFailures[0].description.slice(0,60)})}else try{const r=e.prepare(`SELECT id, title, project_id FROM quests WHERE status='pending'
4
+ ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END LIMIT 1`).get();r&&l.push({kind:"quest",id:`quest:${r.id}`,title:r.title})}catch{}let c;if(u)c="(cycle: Wyrm already in the call chain, skipping)";else if(s){const r=l.length;c=`Wyrm sees ${r} highlight${r===1?"":"s"} for project ${s.name}.`}else c=`Wyrm watching across ${C(e)} projects.`;let n;if(u)n="_(wyrm: cycle detected, no reply)_";else if(t==="mini")n=`<--==(${a==="celebratory"?"^":a==="stuck"?"-":"o"})~~~~~~__ ${c}`;else if(t==="compact"){const r=["\u{F115D} **wyrm:** "+c];for(const d of l.slice(0,3))r.push(`- \`${d.id}\` ${d.title}`);n=r.join(`
5
5
  `)}else{const r=["\u{F115D} **wyrm** ("+(s?.name??"global")+")"];if(r.push(""),r.push(c),l.length>0){r.push("");for(const d of l)r.push(`- \`${d.id}\` ${d.title}`)}n=r.join(`
6
- `)}return{markdown:n,json:{buddy:{name:"wyrm",version:i,protocol:b,mood:a,summary:c,highlights:l}},cycleDetected:u}}function T(e){try{return e.prepare("SELECT COUNT(*) as n FROM projects").get().n}catch{return 0}}function C(e,o="full"){const i=R(e);return i===null?"":o==="mini"?w(e):o==="compact"?O(e):[$,i,S].join(`
7
- `)}function O(e){return[" __--~~==~~--___"," .~//==.. ` ` __--~ ~~",` _-~${e==="celebratory"?"^":e==="stuck"?"-":"o"}~ \\/ ||\\\\ _/~~-${e==="celebratory"?" *":e==="stuck"?" ?":""}`," ~==~ | \\\\_ `-~7 -~ || \\"," .~--____==-~__--~~"].join(`
8
- `)}function w(e){return`<--==(${e==="celebratory"?"^":e==="stuck"?"-":"o"})~~~~~~__`}const E={wyrm:{morning:["morning, operator. the hoard is intact.","morning. the wyrm watches.","sun up. fresh scales, fresh code.","morning, operator. yesterday's work is in the hoard.","morning. nothing's missing from the cave.","morning. the wyrm uncoils."],midday:["back from lunch?","midday. the wyrm has been counting truths.","returning to the work?","good, half a day left.","the hoard is quiet. ready when you are.","midday check-in. all rows accounted for."],evening:["evening. golden hour for the patient operator.","evening. the wyrm sees you.","still at it? the cave is dim but the hoard glints.","evening. the day yields good code.","evening, operator. one more push?","evening. the wyrm warms the coils."],late:["still up at this hour. pace yourself. the hoard isn't going anywhere.","late. the wyrm sleeps with one eye open.","late hours. the work is good but rest is also good.","past midnight. wyrm watches over the hoard while you finish.","late. don't let the dragon outlast the rider.","late. one final commit, then go."]},drogo:{morning:["back. let's work.","morning. ready.","on the clock.","good. start.","no time wasted.","set."],midday:["returning.","pick up where you left.","back to it.","half day done.","work continues.","next."],evening:["evening. still focused.","long day. keep going.","evening shift.","one more.","time left.","finish strong."],late:["late. fine.","past hours. work or rest.","midnight code.","still here. fine.","late shift.","one last push."]},berlin:{morning:["Good morning. I have your queue ready.","Morning. Today's priorities below.","Welcome back. Status follows.","Morning. Allow me to brief you.","Good day. The state is current.","Morning. Ready when you are."],midday:["Welcome back. State summary:","Midday review.","Status remains current.","Continuing.","Picking up.","Resuming."],evening:["Good evening. End-of-day summary:","Evening status.","Late update.","Evening review.","End of working hours.","Wrapping up?"],late:["It is late. Briefly:","Late hour. Concise update:","Past hours. Quick brief:","Midnight passed. Status:","Late shift. State:","Brief late update."]},sumair:{morning:["hey, glad you're back.","morning, friend.","good to see you.","morning. you good?","welcome back.","morning. let's go."],midday:["back! how's it going?","midday vibes.","you're doing great. keep going.","glad you're here.","pick a quest, any quest.","no pressure, just progress."],evening:["evening. proud of today already.","still here? love that.","evening, friend.","you've shown up. that's the win.","late but here.","one more thing?"],late:["hey. late, but I'm here too.","still up? sleep is part of the work.","rest soon, ok?","late hour, soft work.","be kind to yourself.","we'll be here tomorrow."]},custom:{morning:["morning.","morning, friend.","back at it.","good morning.","starting fresh.","morning check."],midday:["midday.","back.","returning.","midday check.","half done.","continuing."],evening:["evening.","evening check.","long day.","almost done.","evening shift.","one more."],late:["late.","late hour.","midnight work.","past hours.","late shift.","wind down soon."]}},j={wyrm:["the hoard grows.","one more scale on the wyrm.","good work. quest closed.","the cave deepens."],drogo:["done.","closed.","ship.","next."],berlin:["Quest completed. Noted.","Resolution recorded.","Closed and logged.","Completion noted."],sumair:["nice!! \u273A","proud of you.","love that.","huge."],custom:["closed.","done.","good.","next."]},N={wyrm:["we have hit this scale before. let me show you where.","the wyrm remembers this failure pattern.","familiar territory. last time we routed around it."],drogo:["we have hit this. avoid it.","pattern. block it.","known failure. route around."],berlin:["I notice a recurring failure pattern. Allow me to highlight it.","This area has prior failures on record.","Caution: prior failures clustered here."],sumair:["we hit this one before. let's not repeat it.","I see we've been here. let's go around.","we know this trap. let's avoid it."],custom:["known failure pattern.","we hit this before.","recurring issue."]};function y(e,o,i){const u=new Date(i.getFullYear(),0,0),s=Math.floor((i.getTime()-u.getTime())/(1440*60*1e3)),t=Math.abs((o*31+s)%e.length);return e[t]}function k(e){const o=e.getHours();return o>=5&&o<12?"morning":o>=12&&o<17?"midday":o>=17&&o<23?"evening":"late"}function L(e,o,i=new Date){const s=e.prepare("SELECT name FROM projects WHERE id = ?").get(o)?.name??`project:${o}`,t=e.prepare("SELECT DISTINCT date FROM sessions WHERE project_id = ? AND date >= date('now', '-30 days') ORDER BY date DESC").all(o).map(g=>g.date);let a=0;if(t.length>0){const g=i.toISOString().slice(0,10),_=new Date(i.getTime()-1440*60*1e3).toISOString().slice(0,10);if(t[0]===g||t[0]===_){a=1;for(let p=1;p<t.length;p++){const v=new Date(new Date(t[p-1]).getTime()-864e5).toISOString().slice(0,10);if(t[p]===v)a++;else break}}}const l=e.prepare("SELECT id, date, objectives, completed FROM sessions WHERE project_id = ? ORDER BY id DESC LIMIT 1").get(o),c=e.prepare(`SELECT id, title, priority, tags FROM quests WHERE project_id = ? AND status = 'pending'
6
+ `)}return{markdown:n,json:{buddy:{name:"wyrm",version:i,protocol:b,mood:a,summary:c,highlights:l}},cycleDetected:u}}function C(e){try{return e.prepare("SELECT COUNT(*) as n FROM projects").get().n}catch{return 0}}function O(e,o="full"){const i=T(e);return i===null?"":o==="mini"?w(e):o==="compact"?j(e):[S,i,R].join(`
7
+ `)}function j(e){return[" __--~~==~~--___"," .~//==.. ` ` __--~ ~~",` _-~${e==="celebratory"?"^":e==="stuck"?"-":"o"}~ \\/ ||\\\\ _/~~-${e==="celebratory"?" *":e==="stuck"?" ?":""}`," ~==~ | \\\\_ `-~7 -~ || \\"," .~--____==-~__--~~"].join(`
8
+ `)}function w(e){return`<--==(${e==="celebratory"?"^":e==="stuck"?"-":"o"})~~~~~~__`}const E={wyrm:{morning:["morning, operator. the hoard is intact.","morning. the wyrm watches.","sun up. fresh scales, fresh code.","morning, operator. yesterday's work is in the hoard.","morning. nothing's missing from the cave.","morning. the wyrm uncoils."],midday:["back from lunch?","midday. the wyrm has been counting truths.","returning to the work?","good, half a day left.","the hoard is quiet. ready when you are.","midday check-in. all rows accounted for."],evening:["evening. golden hour for the patient operator.","evening. the wyrm sees you.","still at it? the cave is dim but the hoard glints.","evening. the day yields good code.","evening, operator. one more push?","evening. the wyrm warms the coils."],late:["still up at this hour. pace yourself. the hoard isn't going anywhere.","late. the wyrm sleeps with one eye open.","late hours. the work is good but rest is also good.","past midnight. wyrm watches over the hoard while you finish.","late. don't let the dragon outlast the rider.","late. one final commit, then go."]},drogo:{morning:["back. let's work.","morning. ready.","on the clock.","good. start.","no time wasted.","set."],midday:["returning.","pick up where you left.","back to it.","half day done.","work continues.","next."],evening:["evening. still focused.","long day. keep going.","evening shift.","one more.","time left.","finish strong."],late:["late. fine.","past hours. work or rest.","midnight code.","still here. fine.","late shift.","one last push."]},berlin:{morning:["Good morning. I have your queue ready.","Morning. Today's priorities below.","Welcome back. Status follows.","Morning. Allow me to brief you.","Good day. The state is current.","Morning. Ready when you are."],midday:["Welcome back. State summary:","Midday review.","Status remains current.","Continuing.","Picking up.","Resuming."],evening:["Good evening. End-of-day summary:","Evening status.","Late update.","Evening review.","End of working hours.","Wrapping up?"],late:["It is late. Briefly:","Late hour. Concise update:","Past hours. Quick brief:","Midnight passed. Status:","Late shift. State:","Brief late update."]},sumair:{morning:["hey, glad you're back.","morning, friend.","good to see you.","morning. you good?","welcome back.","morning. let's go."],midday:["back! how's it going?","midday vibes.","you're doing great. keep going.","glad you're here.","pick a quest, any quest.","no pressure, just progress."],evening:["evening. proud of today already.","still here? love that.","evening, friend.","you've shown up. that's the win.","late but here.","one more thing?"],late:["hey. late, but I'm here too.","still up? sleep is part of the work.","rest soon, ok?","late hour, soft work.","be kind to yourself.","we'll be here tomorrow."]},custom:{morning:["morning.","morning, friend.","back at it.","good morning.","starting fresh.","morning check."],midday:["midday.","back.","returning.","midday check.","half done.","continuing."],evening:["evening.","evening check.","long day.","almost done.","evening shift.","one more."],late:["late.","late hour.","midnight work.","past hours.","late shift.","wind down soon."]}},N={wyrm:["the hoard grows.","one more scale on the wyrm.","good work. quest closed.","the cave deepens."],drogo:["done.","closed.","ship.","next."],berlin:["Quest completed. Noted.","Resolution recorded.","Closed and logged.","Completion noted."],sumair:["nice!! \u273A","proud of you.","love that.","huge."],custom:["closed.","done.","good.","next."]},L={wyrm:["we have hit this scale before. let me show you where.","the wyrm remembers this failure pattern.","familiar territory. last time we routed around it."],drogo:["we have hit this. avoid it.","pattern. block it.","known failure. route around."],berlin:["I notice a recurring failure pattern. Allow me to highlight it.","This area has prior failures on record.","Caution: prior failures clustered here."],sumair:["we hit this one before. let's not repeat it.","I see we've been here. let's go around.","we know this trap. let's avoid it."],custom:["known failure pattern.","we hit this before.","recurring issue."]};function y(e,o,i){const u=new Date(i.getFullYear(),0,0),s=Math.floor((i.getTime()-u.getTime())/(1440*60*1e3)),t=Math.abs((o*31+s)%e.length);return e[t]}function k(e){const o=e.getHours();return o>=5&&o<12?"morning":o>=12&&o<17?"midday":o>=17&&o<23?"evening":"late"}function D(e,o,i=new Date){const s=e.prepare("SELECT name FROM projects WHERE id = ?").get(o)?.name??`project:${o}`,t=e.prepare("SELECT DISTINCT date FROM sessions WHERE project_id = ? AND date >= date('now', '-30 days') ORDER BY date DESC").all(o).map(g=>g.date);let a=0;if(t.length>0){const g=i.toISOString().slice(0,10),_=new Date(i.getTime()-1440*60*1e3).toISOString().slice(0,10);if(t[0]===g||t[0]===_){a=1;for(let p=1;p<t.length;p++){const v=new Date(new Date(t[p-1]).getTime()-864e5).toISOString().slice(0,10);if(t[p]===v)a++;else break}}}const l=e.prepare("SELECT id, date, objectives, completed FROM sessions WHERE project_id = ? ORDER BY id DESC LIMIT 1").get(o),c=e.prepare(`SELECT id, title, priority, tags FROM quests WHERE project_id = ? AND status = 'pending'
9
9
  ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END LIMIT 1`).get(o),n=(()=>{try{return e.prepare(`SELECT id, title, completed_at FROM quests WHERE project_id = ? AND status = 'completed'
10
10
  AND completed_at >= datetime('now', '-1 hour') ORDER BY completed_at DESC LIMIT 1`).get(o)}catch{return}})();let h=[];try{h=e.prepare(`SELECT id, scope, target, description, occurrences FROM failure_patterns
11
11
  WHERE (project_id = ? OR project_id IS NULL) AND resolved_at IS NULL
@@ -13,6 +13,6 @@ import{ICON as m}from"./icons.js";const $=[" __
13
13
  ORDER BY occurrences DESC, last_seen_at DESC LIMIT 5`).all(o)}catch{}let r=null;try{if(l?.objectives){const g=l.objectives.toLowerCase();r=e.prepare("SELECT id, problem_type FROM reasoning_scaffolds WHERE project_id = ? OR project_id IS NULL").all(o).find(p=>g.includes(p.problem_type.toLowerCase()))??null}}catch{}let d=null;try{d=e.prepare(`SELECT g.id, g.title, MAX(gi.created_at) as last_iteration_at
14
14
  FROM goals g LEFT JOIN goal_iterations gi ON gi.goal_id = g.id
15
15
  WHERE g.project_id = ? AND g.status = 'active'
16
- GROUP BY g.id ORDER BY g.priority, g.id LIMIT 1`).get(o)??null}catch{}let f=0;try{f=e.prepare("SELECT SUM(hours) as h FROM hour_entries WHERE project_id = ? AND date >= date('now', 'weekday 0', '-7 days')").get(o)?.h??0}catch{}return{projectId:o,projectName:s,now:i,streakDays:a,hoursThisWeek:f,lastSession:l??null,topPendingQuest:c??null,recentlyCompletedQuest:n??null,unresolvedFailures:h,matchingScaffold:r,activeGoal:d}}function D(e,o){return o||(e.recentlyCompletedQuest?"celebratory":e.unresolvedFailures.length>2?"stuck":"normal")}function M(e){return e.replace(/[^a-zA-Z0-9 _-]/g,"").slice(0,32)||"buddy"}function P(e,o={}){const i=o.persona??"wyrm",u=i==="custom"?M(o.personaName??"buddy"):i,s=D(e,o.mood),t=[];let a=0;if(s==="quiet"){const n=y(E[i][k(e.now)],e.projectId,e.now);return e.topPendingQuest?(t.push(`${n} next up: quest:${e.topPendingQuest.id} (${e.topPendingQuest.title}).`),a++):t.push(n),{markdown:t.join(`
17
- `),resolvedMood:s,resolvedPersona:{kind:i,name:u},dataPoints:a}}if(i==="wyrm"){const n=o.size??"full",h=C(s,n);h&&(n==="mini"||(t.push("```"),t.push(h),t.push("```"),t.push("")))}const l=y(E[i][k(e.now)],e.projectId,e.now),c=i==="wyrm"&&o.size==="mini";if(t.push(c?`${w(s)} ${l}`:l),t.push(""),e.streakDays>=3&&(t.push(`\u{1F4C5} ${e.streakDays}-day streak on ${e.projectName}. the wyrm approves.`),t.push(""),a++),s==="celebratory"&&e.recentlyCompletedQuest&&(t.push(`${m.celebrate} quest:${e.recentlyCompletedQuest.id} closed: *${e.recentlyCompletedQuest.title}*. ${y(j[i],e.projectId,e.now)}`),t.push(""),a++),e.lastSession){const n=(e.lastSession.objectives??"").slice(0,100);n&&(t.push(`${m.session} session #${e.lastSession.id} (${e.lastSession.date}): ${n}${n.length===100?"\u2026":""}`),t.push(""),a++)}if(e.topPendingQuest){const n=e.topPendingQuest.tags?` \xB7 tags: ${e.topPendingQuest.tags}`:"";t.push(`${m.questOpen} next up: quest:${e.topPendingQuest.id} (${e.topPendingQuest.priority}) \xB7 *${e.topPendingQuest.title}*${n}`),t.push(""),a++}else s!=="stuck"&&(t.push(`${m.questOpen} no pending quests. add one with \`wyrm_quest_add\` or just code.`),t.push(""));if(s==="stuck"&&e.unresolvedFailures.length>0){t.push(`${m.blocked} ${y(N[i],e.projectId,e.now)}`);for(const n of e.unresolvedFailures.slice(0,3))t.push(` - failure:${n.id} \xB7 ${n.scope}:${n.target} \xB7 ${n.occurrences}\xD7 \xB7 ${n.description.slice(0,80)}`),a++;t.push("")}if(e.matchingScaffold&&(t.push(`\u{1F9ED} looks like a *${e.matchingScaffold.problem_type}* job; scaffold:${e.matchingScaffold.id} is ready.`),t.push(""),a++),e.activeGoal){const n=e.activeGoal.last_iteration_at?`last loop iteration: ${new Date(e.activeGoal.last_iteration_at).toISOString().slice(0,16).replace("T"," ")} UTC`:"not yet iterated";t.push(`\u{1F525} active goal: goal:${e.activeGoal.id} *${e.activeGoal.title}* \xB7 ${n}`),t.push(""),a++}if(e.hoursThisWeek>0&&(t.push(`\u23F1 ${e.hoursThisWeek.toFixed(1)}h logged on this project this week.`),t.push(""),a++),e.externalBuddies&&e.externalBuddies.length>0){t.push("---"),t.push("## From other buddies"),t.push("");for(const n of e.externalBuddies)t.push(`### ${n.server} / ${n.tool}`),t.push(n.reply.slice(0,400)),t.push("");a+=e.externalBuddies.length}return t.push("ready when you are."),{markdown:t.join(`
18
- `),resolvedMood:s,resolvedPersona:{kind:i,name:u},dataPoints:a}}export{b as BUDDY_PROTOCOL_VERSION,P as buildBuddyOutput,I as buildPeerBuddyReply,L as gatherBuddyInputs,D as resolveMood};
16
+ GROUP BY g.id ORDER BY g.priority, g.id LIMIT 1`).get(o)??null}catch{}let f=0;try{f=e.prepare("SELECT SUM(hours) as h FROM hour_entries WHERE project_id = ? AND date >= date('now', 'weekday 0', '-7 days')").get(o)?.h??0}catch{}return{projectId:o,projectName:s,now:i,streakDays:a,hoursThisWeek:f,lastSession:l??null,topPendingQuest:c??null,recentlyCompletedQuest:n??null,unresolvedFailures:h,matchingScaffold:r,activeGoal:d}}function M(e,o){return o||(e.recentlyCompletedQuest?"celebratory":e.unresolvedFailures.length>2?"stuck":"normal")}function W(e){return e.replace(/[^a-zA-Z0-9 _-]/g,"").slice(0,32)||"buddy"}function F(e,o={}){const i=o.persona??"wyrm",u=i==="custom"?W(o.personaName??"buddy"):i,s=M(e,o.mood),t=[];let a=0;if(s==="quiet"){const n=y(E[i][k(e.now)],e.projectId,e.now);return e.topPendingQuest?(t.push(`${n} next up: quest:${e.topPendingQuest.id} (${e.topPendingQuest.title}).`),a++):t.push(n),{markdown:t.join(`
17
+ `),resolvedMood:s,resolvedPersona:{kind:i,name:u},dataPoints:a}}if(i==="wyrm"){const n=o.size??"full",h=O(s,n);h&&(n==="mini"||(t.push("```"),t.push(h),t.push("```"),t.push("")))}const l=y(E[i][k(e.now)],e.projectId,e.now),c=i==="wyrm"&&o.size==="mini";if(t.push(c?`${w(s)} ${l}`:l),t.push(""),e.streakDays>=3&&(t.push(`\u{1F4C5} ${e.streakDays}-day streak on ${e.projectName}. the wyrm approves.`),t.push(""),a++),s==="celebratory"&&e.recentlyCompletedQuest&&(t.push(`${m.celebrate} quest:${e.recentlyCompletedQuest.id} closed: *${e.recentlyCompletedQuest.title}*. ${y(N[i],e.projectId,e.now)}`),t.push(""),a++),e.lastSession){const n=(e.lastSession.objectives??"").slice(0,100);n&&(t.push(`${m.session} session #${e.lastSession.id} (${e.lastSession.date}): ${n}${n.length===100?"\u2026":""}`),t.push(""),a++)}if(e.topPendingQuest){const n=e.topPendingQuest.tags?` \xB7 tags: ${e.topPendingQuest.tags}`:"";t.push(`${m.questOpen} next up: quest:${e.topPendingQuest.id} (${e.topPendingQuest.priority}) \xB7 *${e.topPendingQuest.title}*${n}`),t.push(""),a++}else s!=="stuck"&&(t.push(`${m.questOpen} no pending quests. add one with \`wyrm_quest_add\` or just code.`),t.push(""));if(s==="stuck"&&e.unresolvedFailures.length>0){t.push(`${m.blocked} ${y(L[i],e.projectId,e.now)}`);for(const n of e.unresolvedFailures.slice(0,3))t.push(` - failure:${n.id} \xB7 ${n.scope}:${n.target} \xB7 ${n.occurrences}\xD7 \xB7 ${n.description.slice(0,80)}`),a++;t.push("")}if(e.matchingScaffold&&(t.push(`\u{1F9ED} looks like a *${e.matchingScaffold.problem_type}* job; scaffold:${e.matchingScaffold.id} is ready.`),t.push(""),a++),e.activeGoal){const n=e.activeGoal.last_iteration_at?`last loop iteration: ${new Date(e.activeGoal.last_iteration_at).toISOString().slice(0,16).replace("T"," ")} UTC`:"not yet iterated";t.push(`\u{1F525} active goal: goal:${e.activeGoal.id} *${e.activeGoal.title}* \xB7 ${n}`),t.push(""),a++}if(e.hoursThisWeek>0&&(t.push(`\u23F1 ${e.hoursThisWeek.toFixed(1)}h logged on this project this week.`),t.push(""),a++),e.externalBuddies&&e.externalBuddies.length>0){t.push("---"),t.push("## From other buddies"),t.push("");for(const n of e.externalBuddies)t.push(`### ${n.server} / ${n.tool}`),t.push(n.reply.slice(0,400)),t.push("");a+=e.externalBuddies.length}return t.push("ready when you are."),{markdown:t.join(`
18
+ `),resolvedMood:s,resolvedPersona:{kind:i,name:u},dataPoints:a}}export{b as BUDDY_PROTOCOL_VERSION,F as buildBuddyOutput,H as buildPeerBuddyReply,D as gatherBuddyInputs,M as resolveMood};
package/dist/cloud/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import{randomBytes as A}from"node:crypto";import{writeFileSync as L}from"node:fs";import{hostname as P}from"node:os";import{resolve as O,dirname as W,join as M}from"node:path";import{fileURLToPath as F}from"node:url";import{createInterface as B}from"node:readline";import{CloudClient as u,CloudError as w,DEFAULT_BASE as v,SESSION_FILE as $,loadSession as g,saveSession as C,clearSession as k}from"./client.js";import{getMasterKey as U,keyExists as b,keyFilePath as d,decrypt as j}from"./crypto.js";import{computeMachineFp as D,classifySession as G}from"./machine-id.js";import{runSync as H,CopiedSessionError as Y}from"./sync-engine.js";import{SovereignSyncManager as q,restartIntervalMinutes as K}from"./sync-daemon.js";import{initializeLicense as J,hasFeature as z,getTier as V}from"../license.js";function S(){J(),z("cloud_backup")||(console.error("\u{F115D} Wyrm Cloud sync requires a Pro license or higher."),console.error(` Current tier: ${V()}`),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 _(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 x(e){return e?new Date(e).toISOString().replace("T"," ").slice(0,16)+" UTC":"never"}async function be(e){const r=e[0],n=e.slice(1);try{switch(r){case void 0:case"help":case"--help":case"-h":return Q();case"login":return await X(n);case"logout":return await Z();case"status":return await ee();case"doctor":return oe();case"devices":return await se(n);case"sync":return await te(n);case"daemon":return await ce(n);case"export":return await le(n);case"delete":return await ie(n);case"recovery":return await ae(n);default:console.error(`Unknown subcommand: wyrm cloud ${r}`),console.error("Try `wyrm cloud help`"),process.exit(1)}}catch(t){if(t instanceof Y&&process.exit(1),t instanceof w)t.status===401?console.error("\u2718 Not authenticated. Run `wyrm cloud login`."):t.status===403?console.error(`\u2718 Forbidden: ${t.message}`):t.status===0?(console.error(`\u2718 ${t.message}`),console.error(" Check your internet connection. If wyrm.ghosts.lk is unreachable, see status at https://www.cloudflarestatus.com")):t.status===501?console.error(`\u2718 Service not configured: ${t.message}`):console.error(`\u2718 ${t.message} (HTTP ${t.status})`);else if(t instanceof Error){const s=t.message;s.includes("Not logged in")?console.error("\u2718 Not logged in. Run `wyrm cloud login`."):s.includes("Wyrm Cloud key")?console.error(`\u2718 Encryption-key error: ${s}`):console.error(`\u2718 ${s}`)}else console.error("\u2718 Unexpected error:",t);process.exit(1)}}function Q(){console.log(`\u{F115D} wyrm cloud: sync your Wyrm memory across devices
1
+ import{randomBytes as A}from"node:crypto";import{writeFileSync as L}from"node:fs";import{hostname as P}from"node:os";import{resolve as O,dirname as W,join as M}from"node:path";import{fileURLToPath as F}from"node:url";import{createInterface as B}from"node:readline";import{CloudClient as u,CloudError as w,DEFAULT_BASE as v,SESSION_FILE as $,loadSession as g,saveSession as C,clearSession as k}from"./client.js";import{getMasterKey as U,keyExists as b,keyFilePath as d,decrypt as j}from"./crypto.js";import{computeMachineFp as D,classifySession as G,cloudMachineId as H}from"./machine-id.js";import{runSync as Y,CopiedSessionError as q}from"./sync-engine.js";import{SovereignSyncManager as K,restartIntervalMinutes as J}from"./sync-daemon.js";import{initializeLicense as z,hasFeature as V,getTier as Q}from"../license.js";function S(){z(),V("cloud_backup")||(console.error("\u{F115D} Wyrm Cloud sync requires a Pro license or higher."),console.error(` Current tier: ${Q()}`),console.error(" Get Pro: write to support@ghosts.lk"),console.error(" Details: 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 _(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 x(e){return e?new Date(e).toISOString().replace("T"," ").slice(0,16)+" UTC":"never"}async function Se(e){const r=e[0],n=e.slice(1);try{switch(r){case void 0:case"help":case"--help":case"-h":return X();case"login":return await Z(n);case"logout":return await ee();case"status":return await oe();case"doctor":return se();case"devices":return await ne(n);case"sync":return await re(n);case"daemon":return await le(n);case"export":return await ie(n);case"delete":return await ae(n);case"recovery":return await de(n);default:console.error(`Unknown subcommand: wyrm cloud ${r}`),console.error("Try `wyrm cloud help`"),process.exit(1)}}catch(t){if(t instanceof q&&process.exit(1),t instanceof w)t.status===401?console.error("\u2718 Not authenticated. Run `wyrm cloud login`."):t.status===403?console.error(`\u2718 Forbidden: ${t.message}`):t.status===0?(console.error(`\u2718 ${t.message}`),console.error(" Check your internet connection. If wyrm.ghosts.lk is unreachable, see status at https://www.cloudflarestatus.com")):t.status===501?console.error(`\u2718 Service not configured: ${t.message}`):console.error(`\u2718 ${t.message} (HTTP ${t.status})`);else if(t instanceof Error){const s=t.message;s.includes("Not logged in")?console.error("\u2718 Not logged in. Run `wyrm cloud login`."):s.includes("Wyrm Cloud key")?console.error(`\u2718 Encryption-key error: ${s}`):console.error(`\u2718 ${s}`)}else console.error("\u2718 Unexpected error:",t);process.exit(1)}}function X(){console.log(`\u{F115D} wyrm cloud: sync your Wyrm memory across devices
2
2
 
3
3
  USAGE:
4
4
  wyrm cloud <subcommand> [args]
@@ -37,6 +37,6 @@ CONSTITUTION:
37
37
 
38
38
  DOMAIN:
39
39
  https://wyrm.ghosts.lk
40
- `)}async function X(e){const r=g();if(r)try{await new u(r.base,r.session).me(),console.error("Already logged in. Run `wyrm cloud logout` first to switch accounts."),process.exit(1)}catch(c){c instanceof w&&c.status===401||(console.error("Already logged in. Run `wyrm cloud logout` first to switch accounts."),process.exit(1)),console.log("Previous session expired; starting a fresh login."),k()}const n=new u(v),t=A(32).toString("hex");let s;try{s=await n.cliInit(t)}catch(c){console.error(`\u2718 Could not start login: ${c instanceof Error?c.message:c}`),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(` ${s.browser_url}`),console.log(""),console.log(" 2. Type this code when prompted:"),console.log(` ${s.code}`),console.log(""),console.log(" 3. Sign in with Google or GitHub."),console.log(""),console.log(` Waiting (expires in ${Math.round(s.expires_in_sec/60)} min)\u2026`);const l=Date.now()+s.expires_in_sec*1e3;let i,a;for(;Date.now()<l;){await new Promise(c=>{setTimeout(c,3e3)});try{const c=await n.cliPoll(t);if(c.ready&&c.session){i=c.session,a=c.account_id;break}}catch(c){console.error(`
41
- \u2718 Login failed: ${c instanceof Error?c.message:c}`),process.exit(1)}}(!i||!a)&&(console.error("\n\u2718 Login timed out. Run `wyrm cloud login` again."),process.exit(1)),C({base:v,session:i,account_id:a,created_at:Date.now()});const y=new u(v,i);let m="";try{m=(await y.me()).account.email}catch{}const f=`${P()} (${process.platform})`;let p;try{p=(await y.registerDevice(f)).device.id;const h=g();h&&C({...h,email:m,device_id:p,device_name:f,machine_fp:D()})}catch(c){console.error(`
42
- \u26A0 Logged in but could not register device: ${c instanceof Error?c.message:c}`),console.error(" Re-run `wyrm cloud login` to retry.");return}console.log(""),console.log(`\u2713 Signed in as ${m}`),console.log(`\u2713 Device registered: ${f}`),console.log(`\u2713 Session saved to ${$}`);const R=!b();if(U(),R?(console.log(`\u2713 Encryption key generated at ${d()} (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; 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 ${d()}`),p)try{const c=await y.syncStatus();if(c.deltas>0){const E=(await y.syncPull(p,0,1)).deltas[0];if(E){let T=!0;try{j(E.payload_b64)}catch{T=!1}T||(console.log(""),console.log(" \u26A0 THIS DEVICE CANNOT READ THE ACCOUNT'S DATA YET"),console.log(` Login succeeded, but the key at ${d()} cannot decrypt`),console.log(` the ${c.deltas} delta(s) already on this account. They were encrypted`),console.log(" with a different master key (e.g. this is an org account whose other"),console.log(" devices hold the shared key). Until the keys match, sync will move"),console.log(" 0 rows in BOTH directions: a connected pipe that carries nothing."),console.log(""),console.log(" Fix: adopt the account key on this device by running"),console.log(" wyrm cloud recovery restore (needs the account's 24-word phrase)"),console.log(` Heads-up: restore OVERWRITES ${d()}. If this device also has`),console.log(" its OWN separate cloud data, back that key up first, or run the org"),console.log(" sync under a separate WYRM home so both keys coexist."))}}}catch{}console.log(""),console.log("Next: `wyrm cloud sync` to push your memory.")}async function Z(){const e=g();if(!e){console.log("Not logged in.");return}try{await new u(e.base,e.session).logout(),console.log("\u2713 Session revoked server-side.")}catch(r){console.error(`\u26A0 Could not revoke server-side: ${r instanceof Error?r.message:r}`),console.error(" Local session cleared anyway.")}k(),console.log(`\u2713 Local session removed (${$}).`),console.log(" Note: ~/.wyrm/cloud.key kept. Delete manually if you want a clean slate.")}async function ee(){const e=g();if(!e){console.log("Not logged in. Run `wyrm cloud login`.");return}const r=new u(e.base,e.session);try{const n=await r.me(),t=await r.syncStatus();console.log(""),console.log(`\u{F115D} Wyrm Cloud: ${n.account.email}`),console.log(""),console.log(` Tier: ${t.tier}`),console.log(` Storage: ${_(t.storage.used_bytes)} / ${_(t.storage.limit_bytes)} (${t.storage.percent}%)`),console.log(` Deltas: ${t.deltas}`),console.log(` Devices: ${t.devices}`),console.log(` Providers:${n.identities.map(s=>" "+s.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)`),I(e),console.log(""),b()?t.deltas>0&&(console.log(` Key: ${d()} (0600)`),console.log(` \u26A0 Back this file up. Losing it = losing access to your ${t.deltas} deltas.`),console.log("")):(console.log(" \u26A0 ENCRYPTION KEY MISSING"),console.log(` Expected at: ${d()}`),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 w&&n.status===401&&(console.error("Session expired or invalid. Run `wyrm cloud login` again."),process.exit(1)),n}}function I(e){const r=G(e.machine_fp);r.state==="match"?console.log(" Identity: \u2713 machine fingerprint matches (not a copied session)"):r.state==="adopt"?console.log(" Identity: no fingerprint yet (pre-7.0.3 session; adopted on next sync)"):(console.log(" Identity: \u26A0 FINGERPRINT MISMATCH: this session looks COPIED"),console.log(` stored ${r.stored.slice(0,12)}\u2026 vs this machine ${r.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 oe(){const e=g();if(console.log(""),console.log("\u{F115D} Wyrm Cloud: 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; 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, pre-7.0.3 session)"}`),console.log(` This box fp: ${D().slice(0,12)}\u2026`),I(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 se(e){const r=e[0],n=u.fromSession();if(r==="revoke"){const i=e[1];i||(console.error("Usage: wyrm cloud devices revoke <device-id>"),process.exit(1));const a=await ne(n,i);await n.revokeDevice(a),console.log(`\u2713 Device ${a.slice(0,8)}\u2026 revoked.`);return}const t=e.includes("--full"),{devices:s}=await n.listDevices(),l=g()?.device_id;console.log(""),console.log(`\u{F115D} Devices (${s.length})`),console.log("");for(const i of s){const a=i.id===l?"\u2605":" ",y=i.revoked?"(revoked)":"",m=t?i.id:`${i.id.slice(0,8)}\u2026`;console.log(` ${a} ${m} ${i.name.padEnd(30)} last_seen=${x(i.last_seen_at)} ${y}`)}console.log(""),console.log(" \u2605 = this machine"),console.log(" Revoke: wyrm cloud devices revoke <device-id> (an id prefix as shown above is fine)"),t||console.log(" Full ids: wyrm cloud devices --full"),console.log("")}async function ne(e,r){const n=r.replace(/[….]+$/,"").trim().toLowerCase(),{devices:t}=await e.listDevices(),s=t.find(l=>l.id.toLowerCase()===n);if(s)return s.id;const o=t.filter(l=>l.id.toLowerCase().startsWith(n));if(o.length===1)return o[0].id;o.length===0&&(console.error(`\u2718 No device matches "${r}". List them with: wyrm cloud devices`),process.exit(1)),console.error(`\u2718 "${r}" is ambiguous; it matches ${o.length} devices:`);for(const l of o)console.error(` ${l.id} ${l.name}`);console.error(" Re-run with more characters, or the full id."),process.exit(1)}async function te(e){S();const r=e.includes("--dry-run"),n=e.includes("--all"),t=e.includes("--force");b()||(console.error("\u26A0 Encryption key not found at",d()),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. Press Ctrl+C to abort."),await new Promise(o=>{setTimeout(o,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 s=await H({dryRun:r,all:n,force:t});if(console.log(""),console.log(s.errors.length>0?"\u{F115D} Sync finished with errors":"\u{F115D} Sync complete"),console.log(""),console.log(` Pushed: ${s.pushed} row(s)${r?" (dry run)":""}`),console.log(` Pulled: ${s.pulled} row(s)`),console.log(` Deleted: ${s.deleted_local} row(s) (peer tombstones)`),s.quarantined&&s.quarantined>0&&console.log(` Quarantined: ${s.quarantined} undecryptable delta(s) set aside (see errors)`),s.errors.length>0){console.log(""),console.log(" Errors:");for(const o of s.errors)console.log(` - ${o}`);process.exitCode=1}console.log("")}function re(){const e=M(W(F(import.meta.url)),"sync-daemon-entrypoint.js");return new q(e)}async function ce(e){S();const r=e[0]??"status",n=e.find(o=>o.startsWith("--interval="));let t;if(n){const o=Number(n.split("=")[1]);(!Number.isFinite(o)||o<=0)&&(console.error("\u2716 --interval= expects a positive number of minutes, got "+JSON.stringify(n)),process.exit(1)),t=o}const s=re();switch(r){case"start":{g()||(console.error("\u2718 Not logged in. Run `wyrm cloud login` first."),process.exit(1));const o=s.start({interval_minutes:t});o.ok||(console.error(`\u{F115D} sovereign-sync daemon: ${o.reason??"failed to start"}${o.pid?` (pid ${o.pid})`:""}`),process.exit(o.pid?0:1)),console.log(`\u{F115D} \u2713 sovereign-sync daemon started (pid ${o.pid}). Logs: ~/.wyrm/wyrm-sovereign-sync.log`);return}case"stop":{const o=s.stop();console.log(o.ok?"\u{F115D} \u2713 sovereign-sync daemon stopped":`\u{F115D} sovereign-sync daemon: ${o.reason??"failed to stop"}`);return}case"restart":{const o=K(t,s.status().state?.interval_ms);s.stop(),await new Promise(i=>{setTimeout(i,500)});const l=s.start({interval_minutes:o});console.log(l.ok?`\u{F115D} \u2713 sovereign-sync daemon restarted (pid ${l.pid}, every ${o} min)`:`\u{F115D} restart failed: ${l.reason??"unknown"}`);return}case"status":{const o=s.status();console.log("\u{F115D} Sovereign-sync daemon"),console.log(` Running: ${o.running?`\u2713 yes (pid ${o.pid})`:"\u2717 no"}`),o.state?.interval_ms&&console.log(` Interval: every ${o.state.interval_ms/6e4} min`),o.state?.started_ts&&console.log(` Started: ${x(o.state.started_ts)}`),o.state?.last_tick_ts&&(console.log(` Last tick: ${x(o.state.last_tick_ts)} (exit ${o.state.last_tick_exit??"n/a"})`),o.state.last_tick_summary&&console.log(` Last result: ${o.state.last_tick_summary}`));return}default:console.error(`Unknown daemon action: ${r}. Use: start, stop, restart, status`),process.exit(1)}}async function le(e){S();const r=u.fromSession(),n=e.find(l=>!l.startsWith("-")),t=O(n??`wyrm-cloud-export-${new Date().toISOString().slice(0,10)}.json`);console.log(" Fetching account export\u2026");const s=await r.accountExport(),o=JSON.stringify(s,null,2);L(t,o,{mode:384}),console.log(""),console.log(`\u2713 Exported to ${t}`),console.log(""),console.log(` Account: ${s.account.email} (${s.account.id.slice(0,8)}\u2026)`),console.log(` Devices: ${s.devices.length}`),console.log(` Identities: ${s.identities.length}`),console.log(` Orgs: ${s.orgs.length}`),console.log(` Deltas: ${s.deltas.length}`),console.log(` Size: ${_(o.length)}`),console.log(""),console.log(" Deltas are still AES-256-GCM ciphertext. To decrypt them you"),console.log(` need the key at ${d()}. Back that up alongside this file.`),console.log("")}async function ie(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=g();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 N(' Type "DELETE my account" to confirm: ')).trim()!=="DELETE my account"&&(console.error("\u2718 Confirmation phrase did not match. Aborting."),process.exit(1)),await new u(n.base,n.session).accountDelete(),k(),console.log(""),console.log("\u2713 Account deleted on server."),console.log(`\u2713 Local session removed (${$}).`),console.log(` Kept: ${d()} (in case you need to decrypt an old export).`),console.log("")}function N(e){return new Promise(r=>{const n=B({input:process.stdin,output:process.stdout});n.question(e,t=>{n.close(),r(t)})})}async function ae(e){const r=e[0];if(r==="show"){const{keyToMnemonic:n}=await import("./recovery.js"),s=n().split(" ");console.log(""),console.log("\u{1F511} Wyrm Cloud recovery phrase (BIP39, 24 words)"),console.log("");for(let o=0;o<4;o++){const l=s.slice(o*6,o*6+6).map((i,a)=>`${String(o*6+a+1).padStart(2)}. ${i.padEnd(10)}`).join(" ");console.log(` ${l}`)}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(r==="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 t=await N(" > "),{mnemonicToKey:s}=await import("./recovery.js"),{path:o,overwrote:l}=s(t,{overwrite:n});console.log(""),console.log(`\u2713 Master key restored to ${o} (0600)`),l&&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{be as cmdCloud};
40
+ `)}async function Z(e){const r=g();if(r)try{await new u(r.base,r.session).me(),console.error("Already logged in. Run `wyrm cloud logout` first to switch accounts."),process.exit(1)}catch(c){c instanceof w&&c.status===401||(console.error("Already logged in. Run `wyrm cloud logout` first to switch accounts."),process.exit(1)),console.log("Previous session expired; starting a fresh login."),k()}const n=new u(v),t=A(32).toString("hex");let s;try{s=await n.cliInit(t)}catch(c){console.error(`\u2718 Could not start login: ${c instanceof Error?c.message:c}`),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(` ${s.browser_url}`),console.log(""),console.log(" 2. Type this code when prompted:"),console.log(` ${s.code}`),console.log(""),console.log(" 3. Sign in with Google or GitHub."),console.log(""),console.log(` Waiting (expires in ${Math.round(s.expires_in_sec/60)} min)\u2026`);const l=Date.now()+s.expires_in_sec*1e3;let i,a;for(;Date.now()<l;){await new Promise(c=>{setTimeout(c,3e3)});try{const c=await n.cliPoll(t);if(c.ready&&c.session){i=c.session,a=c.account_id;break}}catch(c){console.error(`
41
+ \u2718 Login failed: ${c instanceof Error?c.message:c}`),process.exit(1)}}(!i||!a)&&(console.error("\n\u2718 Login timed out. Run `wyrm cloud login` again."),process.exit(1)),C({base:v,session:i,account_id:a,created_at:Date.now()});const y=new u(v,i);let m="";try{m=(await y.me()).account.email}catch{}const f=`${P()} (${process.platform})`;let p;try{p=(await y.registerDevice(f,void 0,H())).device.id;const h=g();h&&C({...h,email:m,device_id:p,device_name:f,machine_fp:D()})}catch(c){console.error(`
42
+ \u26A0 Logged in but could not register device: ${c instanceof Error?c.message:c}`),console.error(" Re-run `wyrm cloud login` to retry.");return}console.log(""),console.log(`\u2713 Signed in as ${m}`),console.log(`\u2713 Device registered: ${f}`),console.log(`\u2713 Session saved to ${$}`);const R=!b();if(U(),R?(console.log(`\u2713 Encryption key generated at ${d()} (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; 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 ${d()}`),p)try{const c=await y.syncStatus();if(c.deltas>0){const E=(await y.syncPull(p,0,1)).deltas[0];if(E){let T=!0;try{j(E.payload_b64)}catch{T=!1}T||(console.log(""),console.log(" \u26A0 THIS DEVICE CANNOT READ THE ACCOUNT'S DATA YET"),console.log(` Login succeeded, but the key at ${d()} cannot decrypt`),console.log(` the ${c.deltas} delta(s) already on this account. They were encrypted`),console.log(" with a different master key (e.g. this is an org account whose other"),console.log(" devices hold the shared key). Until the keys match, sync will move"),console.log(" 0 rows in BOTH directions: a connected pipe that carries nothing."),console.log(""),console.log(" Fix: adopt the account key on this device by running"),console.log(" wyrm cloud recovery restore (needs the account's 24-word phrase)"),console.log(` Heads-up: restore OVERWRITES ${d()}. If this device also has`),console.log(" its OWN separate cloud data, back that key up first, or run the org"),console.log(" sync under a separate WYRM home so both keys coexist."))}}}catch{}console.log(""),console.log("Next: `wyrm cloud sync` to push your memory.")}async function ee(){const e=g();if(!e){console.log("Not logged in.");return}try{await new u(e.base,e.session).logout(),console.log("\u2713 Session revoked server-side.")}catch(r){console.error(`\u26A0 Could not revoke server-side: ${r instanceof Error?r.message:r}`),console.error(" Local session cleared anyway.")}k(),console.log(`\u2713 Local session removed (${$}).`),console.log(" Note: ~/.wyrm/cloud.key kept. Delete manually if you want a clean slate.")}async function oe(){const e=g();if(!e){console.log("Not logged in. Run `wyrm cloud login`.");return}const r=new u(e.base,e.session);try{const n=await r.me(),t=await r.syncStatus();console.log(""),console.log(`\u{F115D} Wyrm Cloud: ${n.account.email}`),console.log(""),console.log(` Tier: ${t.tier}`),console.log(` Storage: ${_(t.storage.used_bytes)} / ${_(t.storage.limit_bytes)} (${t.storage.percent}%)`),console.log(` Deltas: ${t.deltas}`),console.log(` Devices: ${t.devices}`),console.log(` Providers:${n.identities.map(s=>" "+s.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)`),I(e),console.log(""),b()?t.deltas>0&&(console.log(` Key: ${d()} (0600)`),console.log(` \u26A0 Back this file up. Losing it = losing access to your ${t.deltas} deltas.`),console.log("")):(console.log(" \u26A0 ENCRYPTION KEY MISSING"),console.log(` Expected at: ${d()}`),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 w&&n.status===401&&(console.error("Session expired or invalid. Run `wyrm cloud login` again."),process.exit(1)),n}}function I(e){const r=G(e.machine_fp);r.state==="match"?console.log(" Identity: \u2713 machine fingerprint matches (not a copied session)"):r.state==="adopt"?console.log(" Identity: no fingerprint yet (pre-7.0.3 session; adopted on next sync)"):(console.log(" Identity: \u26A0 FINGERPRINT MISMATCH: this session looks COPIED"),console.log(` stored ${r.stored.slice(0,12)}\u2026 vs this machine ${r.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 se(){const e=g();if(console.log(""),console.log("\u{F115D} Wyrm Cloud: 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; 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, pre-7.0.3 session)"}`),console.log(` This box fp: ${D().slice(0,12)}\u2026`),I(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 ne(e){const r=e[0],n=u.fromSession();if(r==="revoke"){const i=e[1];i||(console.error("Usage: wyrm cloud devices revoke <device-id>"),process.exit(1));const a=await te(n,i);await n.revokeDevice(a),console.log(`\u2713 Device ${a.slice(0,8)}\u2026 revoked.`);return}const t=e.includes("--full"),{devices:s}=await n.listDevices(),l=g()?.device_id;console.log(""),console.log(`\u{F115D} Devices (${s.length})`),console.log("");for(const i of s){const a=i.id===l?"\u2605":" ",y=i.revoked?"(revoked)":"",m=t?i.id:`${i.id.slice(0,8)}\u2026`;console.log(` ${a} ${m} ${i.name.padEnd(30)} last_seen=${x(i.last_seen_at)} ${y}`)}console.log(""),console.log(" \u2605 = this machine"),console.log(" Revoke: wyrm cloud devices revoke <device-id> (an id prefix as shown above is fine)"),t||console.log(" Full ids: wyrm cloud devices --full"),console.log("")}async function te(e,r){const n=r.replace(/[….]+$/,"").trim().toLowerCase(),{devices:t}=await e.listDevices(),s=t.find(l=>l.id.toLowerCase()===n);if(s)return s.id;const o=t.filter(l=>l.id.toLowerCase().startsWith(n));if(o.length===1)return o[0].id;o.length===0&&(console.error(`\u2718 No device matches "${r}". List them with: wyrm cloud devices`),process.exit(1)),console.error(`\u2718 "${r}" is ambiguous; it matches ${o.length} devices:`);for(const l of o)console.error(` ${l.id} ${l.name}`);console.error(" Re-run with more characters, or the full id."),process.exit(1)}async function re(e){S();const r=e.includes("--dry-run"),n=e.includes("--all"),t=e.includes("--force");b()||(console.error("\u26A0 Encryption key not found at",d()),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. Press Ctrl+C to abort."),await new Promise(o=>{setTimeout(o,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 s=await Y({dryRun:r,all:n,force:t});if(console.log(""),console.log(s.errors.length>0?"\u{F115D} Sync finished with errors":"\u{F115D} Sync complete"),console.log(""),console.log(` Pushed: ${s.pushed} row(s)${r?" (dry run)":""}`),console.log(` Pulled: ${s.pulled} row(s)`),console.log(` Deleted: ${s.deleted_local} row(s) (peer tombstones)`),s.quarantined&&s.quarantined>0&&console.log(` Quarantined: ${s.quarantined} undecryptable delta(s) set aside (see errors)`),s.errors.length>0){console.log(""),console.log(" Errors:");for(const o of s.errors)console.log(` - ${o}`);process.exitCode=1}console.log("")}function ce(){const e=M(W(F(import.meta.url)),"sync-daemon-entrypoint.js");return new K(e)}async function le(e){S();const r=e[0]??"status",n=e.find(o=>o.startsWith("--interval="));let t;if(n){const o=Number(n.split("=")[1]);(!Number.isFinite(o)||o<=0)&&(console.error("\u2716 --interval= expects a positive number of minutes, got "+JSON.stringify(n)),process.exit(1)),t=o}const s=ce();switch(r){case"start":{g()||(console.error("\u2718 Not logged in. Run `wyrm cloud login` first."),process.exit(1));const o=s.start({interval_minutes:t});o.ok||(console.error(`\u{F115D} sovereign-sync daemon: ${o.reason??"failed to start"}${o.pid?` (pid ${o.pid})`:""}`),process.exit(o.pid?0:1)),console.log(`\u{F115D} \u2713 sovereign-sync daemon started (pid ${o.pid}). Logs: ~/.wyrm/wyrm-sovereign-sync.log`);return}case"stop":{const o=s.stop();console.log(o.ok?"\u{F115D} \u2713 sovereign-sync daemon stopped":`\u{F115D} sovereign-sync daemon: ${o.reason??"failed to stop"}`);return}case"restart":{const o=J(t,s.status().state?.interval_ms);s.stop(),await new Promise(i=>{setTimeout(i,500)});const l=s.start({interval_minutes:o});console.log(l.ok?`\u{F115D} \u2713 sovereign-sync daemon restarted (pid ${l.pid}, every ${o} min)`:`\u{F115D} restart failed: ${l.reason??"unknown"}`);return}case"status":{const o=s.status();console.log("\u{F115D} Sovereign-sync daemon"),console.log(` Running: ${o.running?`\u2713 yes (pid ${o.pid})`:"\u2717 no"}`),o.state?.interval_ms&&console.log(` Interval: every ${o.state.interval_ms/6e4} min`),o.state?.started_ts&&console.log(` Started: ${x(o.state.started_ts)}`),o.state?.last_tick_ts&&(console.log(` Last tick: ${x(o.state.last_tick_ts)} (exit ${o.state.last_tick_exit??"n/a"})`),o.state.last_tick_summary&&console.log(` Last result: ${o.state.last_tick_summary}`));return}default:console.error(`Unknown daemon action: ${r}. Use: start, stop, restart, status`),process.exit(1)}}async function ie(e){S();const r=u.fromSession(),n=e.find(l=>!l.startsWith("-")),t=O(n??`wyrm-cloud-export-${new Date().toISOString().slice(0,10)}.json`);console.log(" Fetching account export\u2026");const s=await r.accountExport(),o=JSON.stringify(s,null,2);L(t,o,{mode:384}),console.log(""),console.log(`\u2713 Exported to ${t}`),console.log(""),console.log(` Account: ${s.account.email} (${s.account.id.slice(0,8)}\u2026)`),console.log(` Devices: ${s.devices.length}`),console.log(` Identities: ${s.identities.length}`),console.log(` Orgs: ${s.orgs.length}`),console.log(` Deltas: ${s.deltas.length}`),console.log(` Size: ${_(o.length)}`),console.log(""),console.log(" Deltas are still AES-256-GCM ciphertext. To decrypt them you"),console.log(` need the key at ${d()}. Back that up alongside this file.`),console.log("")}async function ae(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=g();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 N(' Type "DELETE my account" to confirm: ')).trim()!=="DELETE my account"&&(console.error("\u2718 Confirmation phrase did not match. Aborting."),process.exit(1)),await new u(n.base,n.session).accountDelete(),k(),console.log(""),console.log("\u2713 Account deleted on server."),console.log(`\u2713 Local session removed (${$}).`),console.log(` Kept: ${d()} (in case you need to decrypt an old export).`),console.log("")}function N(e){return new Promise(r=>{const n=B({input:process.stdin,output:process.stdout});n.question(e,t=>{n.close(),r(t)})})}async function de(e){const r=e[0];if(r==="show"){const{keyToMnemonic:n}=await import("./recovery.js"),s=n().split(" ");console.log(""),console.log("\u{1F511} Wyrm Cloud recovery phrase (BIP39, 24 words)"),console.log("");for(let o=0;o<4;o++){const l=s.slice(o*6,o*6+6).map((i,a)=>`${String(o*6+a+1).padStart(2)}. ${i.padEnd(10)}`).join(" ");console.log(` ${l}`)}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(r==="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 t=await N(" > "),{mnemonicToKey:s}=await import("./recovery.js"),{path:o,overwrote:l}=s(t,{overwrite:n});console.log(""),console.log(`\u2713 Master key restored to ${o} (0600)`),l&&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{Se as cmdCloud};
@@ -1 +1 @@
1
- import{readFileSync as w,writeFileSync as g,existsSync as S,mkdirSync as b,chmodSync as E,unlinkSync as O}from"node:fs";import{join as y}from"node:path";import{homedir as T}from"node:os";const k=process.env.WYRM_CLOUD_URL??"https://wyrm.ghosts.lk";function d(){return process.env.WYRM_CLOUD_DIR??y(T(),".wyrm")}const x=d(),N=y(x,"cloud.json");function m(){return y(d(),"cloud.json")}function q(){const s=d();S(s)||b(s,{recursive:!0,mode:448})}function D(){const s=m();if(!S(s))return null;try{return JSON.parse(w(s,"utf-8"))}catch{return null}}function L(s){q();const t=m();g(t,JSON.stringify(s,null,2),{mode:384});try{E(t,384)}catch{}}function R(){try{O(m())}catch{}}class u extends Error{status;body;constructor(t,e,r){super(r),this.status=t,this.body=e,this.name="CloudError"}}class v{base;bearer;constructor(t,e){this.base=t,this.bearer=e}static fromSession(){const t=D();if(!t)throw new u(401,null,"Not logged in. Run `wyrm cloud login` first.");return new v(t.base,t.session)}async req(t,e={}){const r={"content-type":"application/json","user-agent":"wyrm-cli/cloud",...e.headers??{}};this.bearer&&(r.authorization=`Bearer ${this.bearer}`);const n=4;let l=null;for(let i=0;i<=n;i++){const p=new AbortController,f=setTimeout(()=>p.abort(),6e4);let o;try{o=await fetch(`${this.base}${t}`,{...e,headers:r,signal:p.signal})}catch(c){if(clearTimeout(f),l=c.name==="AbortError"?new u(0,null,`Timed out reaching ${this.base}${t} after 60s`):new u(0,null,`Network error: ${c instanceof Error?c.message:c}`),i<n){await this.backoff(i,null);continue}throw l}if(clearTimeout(f),(o.status===429||o.status>=500)&&i<n){await this.backoff(i,o.headers.get("retry-after"));continue}const h=await o.text();let a=null;try{a=h?JSON.parse(h):null}catch{a=h}if(!o.ok){const c=a?.message??a?.error??`HTTP ${o.status}`;throw new u(o.status,a,c)}return a}throw l??new u(0,null,`request to ${t} failed after ${n} retries`)}async backoff(t,e){let r;e&&/^\d+$/.test(e)?r=Math.min(3e4,parseInt(e,10)*1e3):(r=Math.min(3e4,250*Math.pow(2,t)),r+=Math.floor(Math.random()*(r*.25))),await new Promise(n=>{setTimeout(n,r)})}async cliInit(t){return this.req("/api/v1/auth/cli-init",{method:"POST",body:JSON.stringify({poll_token:t})})}async cliPoll(t){return this.req("/api/v1/auth/cli-poll",{method:"POST",body:JSON.stringify({poll_token:t})})}async me(){return this.req("/api/v1/auth/me")}async logout(){return this.req("/api/v1/auth/logout",{method:"POST"})}async registerDevice(t,e){return this.req("/api/v1/devices",{method:"POST",body:JSON.stringify({name:t,pubkey:e})})}async listDevices(){return this.req("/api/v1/devices")}async revokeDevice(t){return this.req(`/api/v1/devices/${encodeURIComponent(t)}`,{method:"DELETE"})}async syncStatus(){return this.req("/api/v1/sync/status")}async syncPush(t,e){return this.req("/api/v1/sync/push",{method:"POST",body:JSON.stringify({device_id:t,deltas:e})})}async accountExport(){return this.req("/api/v1/account/export")}async accountDelete(){return this.req("/api/v1/account",{method:"DELETE",body:JSON.stringify({confirm:"DELETE my account"})})}async syncPull(t,e,r=100){const n=new URLSearchParams({device_id:t,since:String(e),limit:String(r)});return this.req(`/api/v1/sync/pull?${n.toString()}`)}}export{x as CLOUD_DIR,v as CloudClient,u as CloudError,k as DEFAULT_BASE,N as SESSION_FILE,R as clearSession,D as loadSession,d as resolveCloudDir,L as saveSession,m as sessionFilePath};
1
+ import{readFileSync as w,writeFileSync as b,existsSync as S,mkdirSync as g,chmodSync as E,unlinkSync as _}from"node:fs";import{join as y}from"node:path";import{homedir as O}from"node:os";const k=process.env.WYRM_CLOUD_URL??"https://wyrm.ghosts.lk";function d(){return process.env.WYRM_CLOUD_DIR??y(O(),".wyrm")}const T=d(),R=y(T,"cloud.json");function m(){return y(d(),"cloud.json")}function x(){const n=d();S(n)||g(n,{recursive:!0,mode:448})}function D(){const n=m();if(!S(n))return null;try{return JSON.parse(w(n,"utf-8"))}catch{return null}}function L(n){x();const t=m();b(t,JSON.stringify(n,null,2),{mode:384});try{E(t,384)}catch{}}function C(){try{_(m())}catch{}}class u extends Error{status;body;constructor(t,e,r){super(r),this.status=t,this.body=e,this.name="CloudError"}}const q=/^[A-Za-z0-9_-]{16,128}$/;class v{base;bearer;constructor(t,e){this.base=t,this.bearer=e}static fromSession(){const t=D();if(!t)throw new u(401,null,"Not logged in. Run `wyrm cloud login` first.");return new v(t.base,t.session)}async req(t,e={}){const r={"content-type":"application/json","user-agent":"wyrm-cli/cloud",...e.headers??{}};this.bearer&&(r.authorization=`Bearer ${this.bearer}`);const s=4;let l=null;for(let i=0;i<=s;i++){const f=new AbortController,p=setTimeout(()=>f.abort(),6e4);let o;try{o=await fetch(`${this.base}${t}`,{...e,headers:r,signal:f.signal})}catch(c){if(clearTimeout(p),l=c.name==="AbortError"?new u(0,null,`Timed out reaching ${this.base}${t} after 60s`):new u(0,null,`Network error: ${c instanceof Error?c.message:c}`),i<s){await this.backoff(i,null);continue}throw l}if(clearTimeout(p),(o.status===429||o.status>=500)&&i<s){await this.backoff(i,o.headers.get("retry-after"));continue}const h=await o.text();let a=null;try{a=h?JSON.parse(h):null}catch{a=h}if(!o.ok){const c=a?.message??a?.error??`HTTP ${o.status}`;throw new u(o.status,a,c)}return a}throw l??new u(0,null,`request to ${t} failed after ${s} retries`)}async backoff(t,e){let r;e&&/^\d+$/.test(e)?r=Math.min(3e4,parseInt(e,10)*1e3):(r=Math.min(3e4,250*Math.pow(2,t)),r+=Math.floor(Math.random()*(r*.25))),await new Promise(s=>{setTimeout(s,r)})}async cliInit(t){return this.req("/api/v1/auth/cli-init",{method:"POST",body:JSON.stringify({poll_token:t})})}async cliPoll(t){return this.req("/api/v1/auth/cli-poll",{method:"POST",body:JSON.stringify({poll_token:t})})}async me(){return this.req("/api/v1/auth/me")}async logout(){return this.req("/api/v1/auth/logout",{method:"POST"})}async registerDevice(t,e,r){const s={name:t,pubkey:e};return r!==void 0&&q.test(r)&&(s.machine_id=r),this.req("/api/v1/devices",{method:"POST",body:JSON.stringify(s)})}async listDevices(){return this.req("/api/v1/devices")}async revokeDevice(t){return this.req(`/api/v1/devices/${encodeURIComponent(t)}`,{method:"DELETE"})}async syncStatus(){return this.req("/api/v1/sync/status")}async syncPush(t,e){return this.req("/api/v1/sync/push",{method:"POST",body:JSON.stringify({device_id:t,deltas:e})})}async accountExport(){return this.req("/api/v1/account/export")}async accountDelete(){return this.req("/api/v1/account",{method:"DELETE",body:JSON.stringify({confirm:"DELETE my account"})})}async syncPull(t,e,r=100){const s=new URLSearchParams({device_id:t,since:String(e),limit:String(r)});return this.req(`/api/v1/sync/pull?${s.toString()}`)}}export{T as CLOUD_DIR,v as CloudClient,u as CloudError,k as DEFAULT_BASE,R as SESSION_FILE,C as clearSession,D as loadSession,d as resolveCloudDir,L as saveSession,m as sessionFilePath};
@@ -1,2 +1,2 @@
1
- import{createHash as i,randomBytes as c}from"node:crypto";import{readFileSync as s,writeFileSync as a,existsSync as n,mkdirSync as m,chmodSync as f}from"node:fs";import{join as u}from"node:path";import{hostname as h}from"node:os";import{resolveCloudDir as o}from"./client.js";function d(){return u(o(),"machine-id")}const p=/^[0-9a-f]{32}$/;function l(){const t=o();n(t)||m(t,{recursive:!0,mode:448})}function y(){const t=d();if(n(t))try{const e=s(t,"utf-8").trim();if(p.test(e))return e}catch{}l();const r=c(16).toString("hex");a(t,r,{mode:384});try{f(t,384)}catch{}return r}function S(){const t=h(),r=y();return i("sha256").update(`${t}
2
- ${r}`).digest("hex").slice(0,32)}function v(t){if(!t)return{state:"adopt"};const r=S();return t===r?{state:"match"}:{state:"mismatch",stored:t,current:r}}export{v as classifySession,S as computeMachineFp,y as getInstallId,d as machineIdFilePath};
1
+ import{createHash as n,randomBytes as s}from"node:crypto";import{readFileSync as u,writeFileSync as a,existsSync as i,mkdirSync as f,chmodSync as m}from"node:fs";import{join as d}from"node:path";import{hostname as h}from"node:os";import{resolveCloudDir as o}from"./client.js";function p(){return d(o(),"machine-id")}const l=/^[0-9a-f]{32}$/;function y(){const t=o();i(t)||f(t,{recursive:!0,mode:448})}function x(){const t=p();if(i(t))try{const r=u(t,"utf-8").trim();if(l.test(r))return r}catch{}y();const e=s(16).toString("hex");a(t,e,{mode:384});try{m(t,384)}catch{}return e}function c(){const t=h(),e=x();return n("sha256").update(`${t}
2
+ ${e}`).digest("hex").slice(0,32)}const I="wyrm-cloud-device:";function v(t=c){let e;try{e=t()}catch{return}if(!(typeof e!="string"||e.trim()===""))return n("sha256").update(`${I}${e}`).digest("hex")}function w(t){if(!t)return{state:"adopt"};const e=c();return t===e?{state:"match"}:{state:"mismatch",stored:t,current:e}}export{w as classifySession,v as cloudMachineId,c as computeMachineFp,x as getInstallId,p as machineIdFilePath};
package/dist/database.js CHANGED
@@ -1,14 +1,14 @@
1
- import{openDatabase as L}from"./storage/index.js";import{existsSync as u,mkdirSync as O,readdirSync as A,statSync as w,writeFileSync as j,chmodSync as M}from"fs";import{homedir as y}from"os";import{join as d,basename as v,resolve as H,normalize as F}from"path";import{spawnSync as T}from"child_process";import{getResilienceManager as W}from"./resilience.js";import{getStoreLogger as U}from"./logger.js";import{runMigrations as x,schemaSkew as P,getSchemaVersion as q}from"./migrations.js";import{recordGovernanceEvent as b}from"./governance.js";import{enforceReadOnlyOpen as B,newerStoreMessage as $,newerStoreWritesAllowed as Y,NEWER_STORE_OVERRIDE_ENV as G}from"./store-readonly.js";import{describeStoreError as De}from"./store-readonly.js";import{validateProjectPath as C,buildFtsMatchQuery as m,redactCredentials as k}from"./security.js";import{emitEvent as E,eventsSince as V,subscribeEvents as J,isLiveMemoryEnabled as Q,ingestRemoteEvent as z,eventsForPush as K,pruneEvents as Z,getMeta as X,setMeta as ee}from"./events.js";import{getActor as D}from"./handlers/boundary.js";import{eraseLakeRows as te}from"./erasure.js";import{readSkillContent as I,slugify as se,skillContentSha as re}from"./skill-author.js";import{resolveProjectForPath as ie}from"./project-resolve.js";function Oe(){return process.env.WYRM_DB_PATH??process.env.WYRM_DB??d(y(),".wyrm","wyrm.db")}function ne(f,t){const e=(s,r)=>{try{u(s)&&M(s,r)}catch{}};t&&e(t,448),e(f,384),e(`${f}-wal`,384),e(`${f}-shm`,384)}const oe={truth:{select:`SELECT t.cross_project_visibility AS vis, t.project_id AS pid, p.name AS pname, p.sync_policy AS policy
1
+ import{openDatabase as A}from"./storage/index.js";import{existsSync as u,mkdirSync as N,readdirSync as w,statSync as j,writeFileSync as M,chmodSync as H}from"fs";import{homedir as y}from"os";import{join as l,basename as v,resolve as F,normalize as W}from"path";import{spawnSync as T}from"child_process";import{getResilienceManager as U}from"./resilience.js";import{getStoreLogger as x}from"./logger.js";import{runMigrations as P,schemaSkew as q,getSchemaVersion as B}from"./migrations.js";import{recordGovernanceEvent as b}from"./governance.js";import{enforceReadOnlyOpen as $,newerStoreMessage as Y,newerStoreWritesAllowed as G,NEWER_STORE_OVERRIDE_ENV as V}from"./store-readonly.js";import{describeStoreError as Ie}from"./store-readonly.js";import{validateProjectPath as C,buildFtsMatchQuery as m,redactCredentials as k}from"./security.js";import{emitEvent as E,eventsSince as J,subscribeEvents as Q,isLiveMemoryEnabled as z,ingestRemoteEvent as K,eventsForPush as Z,pruneEvents as X,getMeta as ee,setMeta as te}from"./events.js";import{getActor as D}from"./handlers/boundary.js";import{eraseLakeRows as se}from"./erasure.js";import{readSkillContent as I,slugify as re,skillContentSha as ie}from"./skill-author.js";import{resolveProjectForPath as ne,projectIdByPathKey as L}from"./project-resolve.js";function ye(){return process.env.WYRM_DB_PATH??process.env.WYRM_DB??l(y(),".wyrm","wyrm.db")}function oe(f,t){const e=(s,r)=>{try{u(s)&&H(s,r)}catch{}};t&&e(t,448),e(f,384),e(`${f}-wal`,384),e(`${f}-shm`,384)}const ae={truth:{select:`SELECT t.cross_project_visibility AS vis, t.project_id AS pid, p.name AS pname, p.sync_policy AS policy
2
2
  FROM ground_truths t LEFT JOIN projects p ON p.id = t.project_id WHERE t.id = ?`,update:"UPDATE ground_truths SET cross_project_visibility = ?, updated_at = datetime('now') WHERE id = ?"},memory:{select:`SELECT t.cross_project_visibility AS vis, t.project_id AS pid, p.name AS pname, p.sync_policy AS policy
3
3
  FROM memory_artifacts t LEFT JOIN projects p ON p.id = t.project_id WHERE t.id = ?`,update:"UPDATE memory_artifacts SET cross_project_visibility = ?, updated_at = datetime('now') WHERE id = ?"},quest:{select:`SELECT t.cross_project_visibility AS vis, t.project_id AS pid, p.name AS pname, p.sync_policy AS policy
4
- FROM quests t LEFT JOIN projects p ON p.id = t.project_id WHERE t.id = ?`,update:"UPDATE quests SET cross_project_visibility = ?, updated_at = datetime('now') WHERE id = ?"}},ae=new Set(["within","org","public"]);class ye{db;BATCH_SIZE=1e3;resilience;logger;dbPath;constructor(t){const e=d(y(),".wyrm");u(e)||O(e,{recursive:!0,mode:448}),this.dbPath=t||d(e,"wyrm.db"),this.logger=U(this.dbPath),this.resilience=W(),this.db=this.initializeDatabase(this.dbPath),ne(this.dbPath,e),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("cache_size = -64000"),this.db.pragma("temp_store = MEMORY"),this.db.pragma("busy_timeout = 5000"),this.db.pragma("mmap_size = 268435456"),this.db.pragma("page_size = 4096"),this.db.pragma("foreign_keys = ON"),this.db.pragma("secure_delete = ON");const s=x(this.db),r=P(this.db);r.storeIsNewer&&(Y()?this.logger.warn(`This store is at schema v${r.store}, newer than this Wyrm knows (v${r.binary}). ${G}=1 is set, so this older binary is writing to it under older rules. Upgrade wyrm-mcp on this machine.`):(B(this.db,r),this.logger.warn($(r.store,r.binary)))),s.length>0&&this.logger.info(`Applied ${s.length} migration(s), now at v${q(this.db)}`),this.recoverIncompleteOperations()}getDatabase(){return this.db}getDatabasePath(){return this.dbPath}initializeDatabase(t){const e=this.resilience.withRetrySync(()=>L(t),"database_init",{maxAttempts:3,baseDelayMs:500});if(!e.success)throw this.logger.error("Failed to initialize database",{path:t,error:e.error?.message}),e.error||new Error("Database initialization failed");return e.data}recoverIncompleteOperations(){const t=this.resilience.getIncompleteOperations();for(const e of t)this.logger.warn("Found incomplete operation from previous session",{operation:e.operation,stage:e.stage,id:e.id}),e.operation==="batch_insert"&&this.logger.info("Batch insert was incomplete - data may need re-import"),this.resilience.completeCheckpoint(e.id)}addWatchDir(t,e=!0){return C(t),this.db.prepare(`
4
+ FROM quests t LEFT JOIN projects p ON p.id = t.project_id WHERE t.id = ?`,update:"UPDATE quests SET cross_project_visibility = ?, updated_at = datetime('now') WHERE id = ?"}},ce=new Set(["within","org","public"]);class Ce{db;BATCH_SIZE=1e3;resilience;logger;dbPath;constructor(t){const e=l(y(),".wyrm");u(e)||N(e,{recursive:!0,mode:448}),this.dbPath=t||l(e,"wyrm.db"),this.logger=x(this.dbPath),this.resilience=U(),this.db=this.initializeDatabase(this.dbPath),oe(this.dbPath,e),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("cache_size = -64000"),this.db.pragma("temp_store = MEMORY"),this.db.pragma("busy_timeout = 5000"),this.db.pragma("mmap_size = 268435456"),this.db.pragma("page_size = 4096"),this.db.pragma("foreign_keys = ON"),this.db.pragma("secure_delete = ON");const s=P(this.db),r=q(this.db);r.storeIsNewer&&(G()?this.logger.warn(`This store is at schema v${r.store}, newer than this Wyrm knows (v${r.binary}). ${V}=1 is set, so this older binary is writing to it under older rules. Upgrade wyrm-mcp on this machine.`):($(this.db,r),this.logger.warn(Y(r.store,r.binary)))),s.length>0&&this.logger.info(`Applied ${s.length} migration(s), now at v${B(this.db)}`),this.recoverIncompleteOperations()}getDatabase(){return this.db}getDatabasePath(){return this.dbPath}initializeDatabase(t){const e=this.resilience.withRetrySync(()=>A(t),"database_init",{maxAttempts:3,baseDelayMs:500});if(!e.success)throw this.logger.error("Failed to initialize database",{path:t,error:e.error?.message}),e.error||new Error("Database initialization failed");return e.data}recoverIncompleteOperations(){const t=this.resilience.getIncompleteOperations();for(const e of t)this.logger.warn("Found incomplete operation from previous session",{operation:e.operation,stage:e.stage,id:e.id}),e.operation==="batch_insert"&&this.logger.info("Batch insert was incomplete - data may need re-import"),this.resilience.completeCheckpoint(e.id)}addWatchDir(t,e=!0){return C(t),this.db.prepare(`
5
5
  INSERT INTO watch_dirs (path, recursive)
6
6
  VALUES (?, ?)
7
7
  ON CONFLICT(path) DO UPDATE SET recursive = excluded.recursive
8
8
  RETURNING *
9
- `).get(t,e?1:0)}getWatchDirs(){return this.db.prepare("SELECT * FROM watch_dirs").all()}removeWatchDir(t){this.db.prepare("DELETE FROM watch_dirs WHERE path = ?").run(t)}scanForProjects(t,e=!0){C(t);const s=[],r=(i,n=0)=>{if(!(n>3&&e))try{const a=A(i,{withFileTypes:!0});for(const o of a){if(!o.isDirectory()||o.name.startsWith(".")&&o.name!==".git")continue;const c=d(i,o.name),l=d(c,".git");if(u(l)){const p=this.registerProjectFromPath(c);p&&s.push(p)}else e&&n<3&&r(c,n+1)}}catch{}};return r(t),this.db.prepare(`
9
+ `).get(t,e?1:0)}getWatchDirs(){return this.db.prepare("SELECT * FROM watch_dirs").all()}removeWatchDir(t){this.db.prepare("DELETE FROM watch_dirs WHERE path = ?").run(t)}scanForProjects(t,e=!0){C(t);const s=[],r=(i,n=0)=>{if(!(n>3&&e))try{const a=w(i,{withFileTypes:!0});for(const o of a){if(!o.isDirectory()||o.name.startsWith(".")&&o.name!==".git")continue;const c=l(i,o.name),d=l(c,".git");if(u(d)){const p=this.registerProjectFromPath(c);p&&s.push(p)}else e&&n<3&&r(c,n+1)}}catch{}};return r(t),this.db.prepare(`
10
10
  UPDATE watch_dirs SET last_scan = datetime('now') WHERE path = ?
11
- `).run(t),s}scanAllWatchDirs(){const t=this.getWatchDirs(),e=[];for(const s of t){const r=this.scanForProjects(s.path,!!s.recursive);e.push(...r)}return e}registerProjectFromPath(t){try{const e=F(H(t));if(!u(e)||!w(e).isDirectory())return null;const s=v(e);let r,i,n,a;try{const o=T("git",["config","--get","remote.origin.url"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});o.status===0&&(r=o.stdout.trim());const c=T("git",["rev-parse","--abbrev-ref","HEAD"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});c.status===0&&(i=c.stdout.trim());const l=T("git",["log","-1","--format=%h %s"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});l.status===0&&(n=l.stdout.trim())}catch{}return u(d(e,"package.json"))?(a="Node.js",u(d(e,"next.config.js"))||u(d(e,"next.config.ts"))||u(d(e,"next.config.mjs"))?a="Next.js":u(d(e,"vite.config.ts"))&&(a="Vite")):u(d(e,"requirements.txt"))||u(d(e,"pyproject.toml"))?a="Python":u(d(e,"composer.json"))?a="PHP":u(d(e,"Cargo.toml"))?a="Rust":u(d(e,"go.mod"))&&(a="Go"),this.registerProject(s,e,r,a,n,i)}catch{return null}}registerProject(t,e,s,r,i,n){return this.db.prepare(`
11
+ `).run(t),s}scanAllWatchDirs(){const t=this.getWatchDirs(),e=[];for(const s of t){const r=this.scanForProjects(s.path,!!s.recursive);e.push(...r)}return e}registerProjectFromPath(t){try{const e=W(F(t));if(!u(e)||!j(e).isDirectory())return null;const s=v(e);let r,i,n,a;try{const o=T("git",["config","--get","remote.origin.url"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});o.status===0&&(r=o.stdout.trim());const c=T("git",["rev-parse","--abbrev-ref","HEAD"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});c.status===0&&(i=c.stdout.trim());const d=T("git",["log","-1","--format=%h %s"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});d.status===0&&(n=d.stdout.trim())}catch{}return u(l(e,"package.json"))?(a="Node.js",u(l(e,"next.config.js"))||u(l(e,"next.config.ts"))||u(l(e,"next.config.mjs"))?a="Next.js":u(l(e,"vite.config.ts"))&&(a="Vite")):u(l(e,"requirements.txt"))||u(l(e,"pyproject.toml"))?a="Python":u(l(e,"composer.json"))?a="PHP":u(l(e,"Cargo.toml"))?a="Rust":u(l(e,"go.mod"))&&(a="Go"),this.registerProject(s,e,r,a,n,i)}catch{return null}}registerProject(t,e,s,r,i,n,a=process.platform){const o=()=>this.db.prepare(`
12
12
  INSERT INTO projects (name, path, repo, stack, last_commit, branch)
13
13
  VALUES (?, ?, ?, ?, ?, ?)
14
14
  ON CONFLICT(path) DO UPDATE SET
@@ -19,7 +19,17 @@ import{openDatabase as L}from"./storage/index.js";import{existsSync as u,mkdirSy
19
19
  branch = COALESCE(excluded.branch, projects.branch),
20
20
  updated_at = datetime('now')
21
21
  RETURNING *
22
- `).get(t,e,s||null,r||null,i||null,n||null)}getProject(t){return this.db.prepare("SELECT * FROM projects WHERE path = ?").get(t)}getProjectForPath(t){const{id:e,root:s}=ie(this.db,t);return e!==null&&s!==null?{id:e,path:s}:void 0}getProjectById(t){return this.db.prepare("SELECT * FROM projects WHERE id = ?").get(t)}getProjectByName(t){return this.db.prepare("SELECT * FROM projects WHERE name = ?").get(t)}getAllProjects(t=100,e=0){return this.db.prepare(`
22
+ `).get(t,e,s||null,r||null,i||null,n||null);return a!=="win32"?o():this.db.transaction(()=>{const d=this.db.prepare("SELECT 1 FROM projects WHERE path = ?").get(e)?void 0:L(this.db,e,a);return d===void 0?o():this.db.prepare(`
23
+ UPDATE projects SET
24
+ name = ?,
25
+ repo = COALESCE(?, repo),
26
+ stack = COALESCE(?, stack),
27
+ last_commit = COALESCE(?, last_commit),
28
+ branch = COALESCE(?, branch),
29
+ updated_at = datetime('now')
30
+ WHERE id = ?
31
+ RETURNING *
32
+ `).get(t,s||null,r||null,i||null,n||null,d)}).immediate()}getProject(t,e=process.platform){const s=this.db.prepare("SELECT * FROM projects WHERE path = ?").get(t);if(s||e!=="win32")return s;const r=L(this.db,t,e);return r===void 0?void 0:this.getProjectById(r)}getProjectForPath(t,e=process.platform){const{id:s,root:r}=ne(this.db,t,e);return s!==null&&r!==null?{id:s,path:r}:void 0}getProjectById(t){return this.db.prepare("SELECT * FROM projects WHERE id = ?").get(t)}getProjectByName(t){return this.db.prepare("SELECT * FROM projects WHERE name = ?").get(t)}getAllProjects(t=100,e=0){return this.db.prepare(`
23
33
  SELECT * FROM projects ORDER BY updated_at DESC LIMIT ? OFFSET ?
24
34
  `).all(t,e)}searchProjects(t){const e=`%${t}%`;return this.db.prepare(`
25
35
  SELECT * FROM projects
@@ -36,13 +46,13 @@ import{openDatabase as L}from"./storage/index.js";import{existsSync as u,mkdirSy
36
46
 
37
47
  `+o:o;c.length>i&&(c=`[\u2026earlier continuity trimmed; gist retained below\u2026]
38
48
 
39
- `+c.slice(c.length-i));const l=(n?.turn_count??0)+1;this.resilience.withRetrySync(()=>(this.db.prepare(`
49
+ `+c.slice(c.length-i));const d=(n?.turn_count??0)+1;this.resilience.withRetrySync(()=>(this.db.prepare(`
40
50
  INSERT INTO project_continuity (project_id, thread, updated_at, updated_by, turn_count)
41
51
  VALUES (?, ?, ?, ?, ?)
42
52
  ON CONFLICT(project_id) DO UPDATE SET
43
53
  thread = excluded.thread, updated_at = excluded.updated_at,
44
54
  updated_by = excluded.updated_by, turn_count = excluded.turn_count
45
- `).run(t,c,a,s??null,l),!0),"updateContinuity")}getContinuity(t){return this.db.prepare("SELECT thread, updated_at, updated_by, turn_count FROM project_continuity WHERE project_id = ?").get(t)??null}recordRouting(t){const e=t.projectId==null?-1:t.projectId,s=(t.taskType??"").trim(),r=(t.modelId??"").trim();if(!s)throw new Error("recordRouting: taskType is required");if(!r)throw new Error("recordRouting: modelId is required");let i=null;if(Array.isArray(t.toolNames)){const o=[...new Set(t.toolNames.map(c=>String(c).trim()).filter(Boolean))];i=o.length?o.join(","):null}else if(typeof t.toolNames=="string"){const o=[...new Set(t.toolNames.split(",").map(c=>c.trim()).filter(Boolean))];i=o.length?o.join(","):null}const n=t.latencyMs==null||!Number.isFinite(t.latencyMs)?null:Math.max(0,Math.round(t.latencyMs));let a=-1;return this.resilience.withRetrySync(()=>{const o=this.db.prepare(`
55
+ `).run(t,c,a,s??null,d),!0),"updateContinuity")}getContinuity(t){return this.db.prepare("SELECT thread, updated_at, updated_by, turn_count FROM project_continuity WHERE project_id = ?").get(t)??null}recordRouting(t){const e=t.projectId==null?-1:t.projectId,s=(t.taskType??"").trim(),r=(t.modelId??"").trim();if(!s)throw new Error("recordRouting: taskType is required");if(!r)throw new Error("recordRouting: modelId is required");let i=null;if(Array.isArray(t.toolNames)){const o=[...new Set(t.toolNames.map(c=>String(c).trim()).filter(Boolean))];i=o.length?o.join(","):null}else if(typeof t.toolNames=="string"){const o=[...new Set(t.toolNames.split(",").map(c=>c.trim()).filter(Boolean))];i=o.length?o.join(","):null}const n=t.latencyMs==null||!Number.isFinite(t.latencyMs)?null:Math.max(0,Math.round(t.latencyMs));let a=-1;return this.resilience.withRetrySync(()=>{const o=this.db.prepare(`
46
56
  INSERT INTO routing_memory (project_id, task_type, model_id, tool_names, success, latency_ms)
47
57
  VALUES (?, ?, ?, ?, ?, ?)
48
58
  `).run(e,s,r,i,t.success?1:0,n);return a=Number(o.lastInsertRowid),!0},"recordRouting"),a}recallRouting(t,e={}){const s=(t??"").trim();if(!s)return[];const r=Math.min(100,Math.max(1,e.limit??10)),i=["task_type = ?"],n=[s];e.projectId!=null&&(i.push("(project_id = ? OR project_id = -1)"),n.push(e.projectId)),e.sinceDays!=null&&Number.isFinite(e.sinceDays)&&e.sinceDays>0&&(i.push("ts >= datetime('now', ?)"),n.push(`-${Math.round(e.sinceDays)} days`)),n.push(r);try{return this.db.prepare(`
@@ -160,7 +170,7 @@ import{openDatabase as L}from"./storage/index.js";import{existsSync as u,mkdirSy
160
170
  SELECT value FROM context WHERE project_id = ? AND key = ?
161
171
  `).get(t,e)?.value}getAllContext(t){const e=this.db.prepare(`
162
172
  SELECT key, value FROM context WHERE project_id = ?
163
- `).all(t),s={};for(const r of e)s[r.key]=r.value;return s}liveMemoryEnabled(){return Q()}eventsSince(t,e=0,s=100){return V(this.db,t,e,s)}subscribeEvents(t,e=20){return J(this.db,t,e)}publishEvent(t){E(this.db,t)}pruneEvents(t={}){return Z(this.db,t)}ingestRemoteEvent(t,e){return z(this.db,t,e)}eventsForPush(t,e=0,s=200){return K(this.db,t,e,s)}getMeta(t){return X(this.db,t)}setMeta(t,e){ee(this.db,t,e)}setGlobalContext(t,e){this.db.prepare(`
173
+ `).all(t),s={};for(const r of e)s[r.key]=r.value;return s}liveMemoryEnabled(){return z()}eventsSince(t,e=0,s=100){return J(this.db,t,e,s)}subscribeEvents(t,e=20){return Q(this.db,t,e)}publishEvent(t){E(this.db,t)}pruneEvents(t={}){return X(this.db,t)}ingestRemoteEvent(t,e){return K(this.db,t,e)}eventsForPush(t,e=0,s=200){return Z(this.db,t,e,s)}getMeta(t){return ee(this.db,t)}setMeta(t,e){te(this.db,t,e)}setGlobalContext(t,e){this.db.prepare(`
164
174
  INSERT INTO global_context (key, value)
165
175
  VALUES (?, ?)
166
176
  ON CONFLICT(key) DO UPDATE SET
@@ -168,7 +178,7 @@ import{openDatabase as L}from"./storage/index.js";import{existsSync as u,mkdirSy
168
178
  updated_at = datetime('now')
169
179
  `).run(t,e)}getGlobalContext(t){return this.db.prepare(`
170
180
  SELECT value FROM global_context WHERE key = ?
171
- `).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,i,n,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:[]),_=I({skillPath:s,name:t}),g=_?.content??null,S=_?.sha256??null,R=_?new Date().toISOString():null,h=this.resilience.withRetrySync(()=>this.db.prepare(`
181
+ `).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,i,n,a,o){const c=o?.tier??"atomic",d=JSON.stringify(Array.isArray(o?.governs)?o.governs:[]),p=JSON.stringify(Array.isArray(o?.composes)?o.composes:[]),_=I({skillPath:s,name:t}),g=_?.content??null,S=_?.sha256??null,R=_?new Date().toISOString():null,h=this.resilience.withRetrySync(()=>this.db.prepare(`
172
182
  INSERT INTO skills (name, description, skill_path, category, author, version, tags, tier, governs, composes, content, content_sha256, content_updated_at, is_active, usage_count)
173
183
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)
174
184
  ON CONFLICT(name) DO UPDATE SET
@@ -190,13 +200,13 @@ import{openDatabase as L}from"./storage/index.js";import{existsSync as u,mkdirSy
190
200
  updated_at = datetime('now'),
191
201
  is_active = 1
192
202
  RETURNING *
193
- `).get(t,e,s,r||null,i||null,n||null,a||null,c,l,p,g,S,R),"registerSkill");if(!h.success)throw h.error||new Error("Failed to register skill");return h.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??I,s=this.db.prepare("SELECT id, name, skill_path, content_sha256 FROM skills").all();let r=0,i=0,n=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){n++;continue}if(c.content_sha256&&c.content_sha256===l.sha256){i++;continue}a.run(l.content,l.sha256,new Date().toISOString(),c.id),r++}})(),{total:s.length,filled:r,unchanged:i,missing:n}}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 i=0,n=0,a=0;const o=new Set;for(const c of r){if(c.content==null){n++;continue}let l=se(c.name);if(!l){n++;continue}o.has(l)&&(l=`${l}-${re(c.name).slice(0,8)}`,a++),o.add(l);const p=d(t,l);O(p,{recursive:!0}),j(d(p,"SKILL.md"),c.content,"utf-8"),i++}return{total:r.length,written:i,skipped_no_content:n,collisions:a}}setSkillVisibility(t,e){return this.db.transaction(()=>{const s=this.db.prepare("UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now') WHERE name = ? RETURNING *").get(e,t);return s&&b(this.db,"visibility.set",{kind:"skill",row_id:s.id,to:e}),s})()}setAllSkillsVisibility(t,e={}){const s=[],r=[t];e.includeInactive||s.push("is_active = 1"),e.tier&&(s.push("tier = ?"),r.push(e.tier));const i=s.length?" WHERE "+s.join(" AND "):"";return this.db.transaction(()=>{const n=this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now')${i}`).run(...r);return n.changes>0&&b(this.db,"visibility.set",{kind:"skill",count:n.changes,to:t}),n.changes})()}setRowVisibility(t,e,s){const r=oe[t];if(!r)throw new Error(`Unknown visibility kind: ${String(t)} (use truth|memory|quest)`);if(!ae.has(s))throw new Error(`Unknown visibility: ${String(s)} (use within|org|public)`);const i=this.db.prepare(r.select).get(e),n={kind:t,id:e,visibility:s};if(!i)return{...n,outcome:"not_found",projectId:null,projectName:null,syncPolicy:null,egressEligible:!1};const a={projectId:i.pid,projectName:i.pname,syncPolicy:i.policy,previous:i.vis},o=c=>c!=="within"&&(i.pid==null||i.policy==="cloud"||i.policy==="team");return s!=="within"&&i.policy==="private"?{...n,...a,outcome:"refused_private_grove",egressEligible:!1}:i.vis===s?{...n,...a,outcome:"unchanged",egressEligible:o(s)}:(this.db.transaction(()=>{this.db.prepare(r.update).run(s,e),b(this.db,"visibility.set",{kind:t,row_id:e,from:i.vis,to:s},{projectId:i.pid})})(),{...n,...a,outcome:"updated",egressEligible:o(s)})}listSkills(t,e,s,r){let i="SELECT * FROM skills WHERE 1=1";const n=[];if(t!==void 0&&(i+=" AND is_active = ?",n.push(t?1:0)),e&&(i+=" AND category = ?",n.push(e)),r&&(i+=" AND tier = ?",n.push(r)),s){const a=m(s);a&&(i+=" AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)",n.push(a))}return i+=" ORDER BY updated_at DESC",this.db.prepare(i).all(...n)}searchSkills(t,e=20,s){const r=m(t);if(!r)return[];const i=s?" AND s.tier = ?":"",n=this.db.prepare(`
203
+ `).get(t,e,s,r||null,i||null,n||null,a||null,c,d,p,g,S,R),"registerSkill");if(!h.success)throw h.error||new Error("Failed to register skill");return h.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??I,s=this.db.prepare("SELECT id, name, skill_path, content_sha256 FROM skills").all();let r=0,i=0,n=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 d=e({skillPath:c.skill_path,name:c.name,skillsDir:t.skillsDir});if(!d){n++;continue}if(c.content_sha256&&c.content_sha256===d.sha256){i++;continue}a.run(d.content,d.sha256,new Date().toISOString(),c.id),r++}})(),{total:s.length,filled:r,unchanged:i,missing:n}}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 i=0,n=0,a=0;const o=new Set;for(const c of r){if(c.content==null){n++;continue}let d=re(c.name);if(!d){n++;continue}o.has(d)&&(d=`${d}-${ie(c.name).slice(0,8)}`,a++),o.add(d);const p=l(t,d);N(p,{recursive:!0}),M(l(p,"SKILL.md"),c.content,"utf-8"),i++}return{total:r.length,written:i,skipped_no_content:n,collisions:a}}setSkillVisibility(t,e){return this.db.transaction(()=>{const s=this.db.prepare("UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now') WHERE name = ? RETURNING *").get(e,t);return s&&b(this.db,"visibility.set",{kind:"skill",row_id:s.id,to:e}),s})()}setAllSkillsVisibility(t,e={}){const s=[],r=[t];e.includeInactive||s.push("is_active = 1"),e.tier&&(s.push("tier = ?"),r.push(e.tier));const i=s.length?" WHERE "+s.join(" AND "):"";return this.db.transaction(()=>{const n=this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now')${i}`).run(...r);return n.changes>0&&b(this.db,"visibility.set",{kind:"skill",count:n.changes,to:t}),n.changes})()}setRowVisibility(t,e,s){const r=ae[t];if(!r)throw new Error(`Unknown visibility kind: ${String(t)} (use truth|memory|quest)`);if(!ce.has(s))throw new Error(`Unknown visibility: ${String(s)} (use within|org|public)`);const i=this.db.prepare(r.select).get(e),n={kind:t,id:e,visibility:s};if(!i)return{...n,outcome:"not_found",projectId:null,projectName:null,syncPolicy:null,egressEligible:!1};const a={projectId:i.pid,projectName:i.pname,syncPolicy:i.policy,previous:i.vis},o=c=>c!=="within"&&(i.pid==null||i.policy==="cloud"||i.policy==="team");return s!=="within"&&i.policy==="private"?{...n,...a,outcome:"refused_private_grove",egressEligible:!1}:i.vis===s?{...n,...a,outcome:"unchanged",egressEligible:o(s)}:(this.db.transaction(()=>{this.db.prepare(r.update).run(s,e),b(this.db,"visibility.set",{kind:t,row_id:e,from:i.vis,to:s},{projectId:i.pid})})(),{...n,...a,outcome:"updated",egressEligible:o(s)})}listSkills(t,e,s,r){let i="SELECT * FROM skills WHERE 1=1";const n=[];if(t!==void 0&&(i+=" AND is_active = ?",n.push(t?1:0)),e&&(i+=" AND category = ?",n.push(e)),r&&(i+=" AND tier = ?",n.push(r)),s){const a=m(s);a&&(i+=" AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)",n.push(a))}return i+=" ORDER BY updated_at DESC",this.db.prepare(i).all(...n)}searchSkills(t,e=20,s){const r=m(t);if(!r)return[];const i=s?" AND s.tier = ?":"",n=this.db.prepare(`
194
204
  SELECT s.* FROM skills s
195
205
  JOIN skills_fts ON s.id = skills_fts.rowid
196
206
  WHERE skills_fts MATCH ?${i}
197
207
  ORDER BY rank
198
208
  LIMIT ?
199
- `);return s?n.all(r,s,e):n.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[]}},i=(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 R=new Set(c).add(o);for(const h of p){const N=i(h,R);N?g.children.push(N):S.push(h)}}return S.length&&(g.missing=S),g};if(t){const o=i(t,new Set);return o?[o]:[]}const n=e.filter(o=>(o.tier??"atomic")==="god").map(o=>o.name).sort(),a=[];for(const o of n){const c=i(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(`
209
+ `);return s?n.all(r,s,e):n.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(d=>typeof d=="string"):[]}catch{return[]}},i=(o,c)=>{const d=s.get(o);if(!d)return null;const p=r(d.governs),_=r(d.composes),g={name:d.name,tier:d.tier??"atomic",description:d.description,governs:p,composes:_,children:[]},S=[];if(!c.has(o)){const R=new Set(c).add(o);for(const h of p){const O=i(h,R);O?g.children.push(O):S.push(h)}}return S.length&&(g.missing=S),g};if(t){const o=i(t,new Set);return o?[o]:[]}const n=e.filter(o=>(o.tier??"atomic")==="god").map(o=>o.name).sort(),a=[];for(const o of n){const c=i(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(`
200
210
  UPDATE skills SET ${s.join(", ")} WHERE name = ? RETURNING *
201
211
  `).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(`
202
212
  SELECT category, COUNT(*) as count FROM skills WHERE category IS NOT NULL GROUP BY category
@@ -221,7 +231,7 @@ import{openDatabase as L}from"./storage/index.js";import{existsSync as u,mkdirSy
221
231
  `).get(t,e,s,r,i?JSON.stringify(i):null),"insertData");if(!n.success)throw n.error||new Error("Insert data failed");return n.data}batchWrites(t,e){return this.db.transaction(r=>r.map(i=>e(i)))(t)}insertDataBatch(t){const e=this.resilience.generateOperationId("batch_insert"),s=this.BATCH_SIZE;let r=0;this.resilience.createCheckpoint(e,"batch_insert","started",{totalItems:t.length,batchSize:s});const i=this.db.prepare(`
222
232
  INSERT INTO data_lake (project_id, category, key, value, metadata)
223
233
  VALUES (?, ?, ?, ?, ?)
224
- `);try{for(let n=0;n<t.length;n+=s){const a=t.slice(n,n+s),o=Math.floor(n/s)+1;this.resilience.updateCheckpoint(e,`batch_${o}`,{processed:n,currentBatch:o});const c=this.resilience.withRetrySync(()=>this.batchWrites(a,l=>(i.run(l.projectId,l.category,l.key,l.value,l.metadata?JSON.stringify(l.metadata):null),1)).length,`batch_insert_${o}`,{maxAttempts:3});if(!c.success){this.logger.error("Batch insert failed",{batch:o,processed:r,error:c.error?.message}),this.resilience.updateCheckpoint(e,"partial_failure",{inserted:r,failedAt:n});const l=c.error??new Error(`Batch insert failed at chunk ${o}`);throw l.inserted=r,l}r+=c.data}return this.resilience.completeCheckpoint(e),r}catch(n){throw n!==null&&typeof n=="object"&&"inserted"in n||(this.logger.error("Batch insert exception",{inserted:r,error:n.message}),this.resilience.updateCheckpoint(e,"exception",{inserted:r,error:n.message}),n!==null&&typeof n=="object"&&(n.inserted=r)),n}}queryData(t,e,s=100,r=0){return e?this.db.prepare(`
234
+ `);try{for(let n=0;n<t.length;n+=s){const a=t.slice(n,n+s),o=Math.floor(n/s)+1;this.resilience.updateCheckpoint(e,`batch_${o}`,{processed:n,currentBatch:o});const c=this.resilience.withRetrySync(()=>this.batchWrites(a,d=>(i.run(d.projectId,d.category,d.key,d.value,d.metadata?JSON.stringify(d.metadata):null),1)).length,`batch_insert_${o}`,{maxAttempts:3});if(!c.success){this.logger.error("Batch insert failed",{batch:o,processed:r,error:c.error?.message}),this.resilience.updateCheckpoint(e,"partial_failure",{inserted:r,failedAt:n});const d=c.error??new Error(`Batch insert failed at chunk ${o}`);throw d.inserted=r,d}r+=c.data}return this.resilience.completeCheckpoint(e),r}catch(n){throw n!==null&&typeof n=="object"&&"inserted"in n||(this.logger.error("Batch insert exception",{inserted:r,error:n.message}),this.resilience.updateCheckpoint(e,"exception",{inserted:r,error:n.message}),n!==null&&typeof n=="object"&&(n.inserted=r)),n}}queryData(t,e,s=100,r=0){return e?this.db.prepare(`
225
235
  SELECT * FROM data_lake
226
236
  WHERE project_id = ? AND category = ?
227
237
  ORDER BY created_at DESC
@@ -263,8 +273,8 @@ import{openDatabase as L}from"./storage/index.js";import{existsSync as u,mkdirSy
263
273
  WHERE project_id = ?
264
274
  GROUP BY category
265
275
  ORDER BY count DESC
266
- `).all(t)}forgetData(t){const e=[],s=[];if(t.match){e.push("(key LIKE ? OR value LIKE ?)");const p=`%${t.match}%`;s.push(p,p)}if(t.dataset&&(e.push("category = ?"),s.push(t.dataset)),typeof t.olderThanDays=="number"&&(e.push("created_at < datetime('now', ?)"),s.push(`-${Math.max(0,Math.floor(t.olderThanDays))} days`)),t.projectId!=null&&(e.push("project_id = ?"),s.push(t.projectId)),e.length===0)return{matched:0,deleted:0,samples:[],counts:{},erasureId:null};const r=e.join(" AND "),i=this.db.prepare(`SELECT id, category, key FROM data_lake WHERE ${r}`).all(...s),n=i.length,a=this.db.prepare(`SELECT category, key, created_at FROM data_lake WHERE ${r} ORDER BY created_at LIMIT 5`).all(...s),o=t.dryRun!==!1,c=te(this.db,i,{subject:t.match,dryRun:o,reason:t.reason??(t.match?"subject":t.dataset?"source":"age"),predicate:{match:!!t.match,dataset:t.dataset??null,older_than_days:typeof t.olderThanDays=="number"?t.olderThanDays:null,project:t.projectId!=null},actor:t.actor??null}),l=o?0:c.counts.data_lake??0;return{matched:n,deleted:l,samples:a,counts:c.counts,erasureId:c.erasureId}}deleteDataCategory(t,e){return this.db.prepare(`
276
+ `).all(t)}forgetData(t){const e=[],s=[];if(t.match){e.push("(key LIKE ? OR value LIKE ?)");const p=`%${t.match}%`;s.push(p,p)}if(t.dataset&&(e.push("category = ?"),s.push(t.dataset)),typeof t.olderThanDays=="number"&&(e.push("created_at < datetime('now', ?)"),s.push(`-${Math.max(0,Math.floor(t.olderThanDays))} days`)),t.projectId!=null&&(e.push("project_id = ?"),s.push(t.projectId)),e.length===0)return{matched:0,deleted:0,samples:[],counts:{},erasureId:null};const r=e.join(" AND "),i=this.db.prepare(`SELECT id, category, key FROM data_lake WHERE ${r}`).all(...s),n=i.length,a=this.db.prepare(`SELECT category, key, created_at FROM data_lake WHERE ${r} ORDER BY created_at LIMIT 5`).all(...s),o=t.dryRun!==!1,c=se(this.db,i,{subject:t.match,dryRun:o,reason:t.reason??(t.match?"subject":t.dataset?"source":"age"),predicate:{match:!!t.match,dataset:t.dataset??null,older_than_days:typeof t.olderThanDays=="number"?t.olderThanDays:null,project:t.projectId!=null},actor:t.actor??null}),d=o?0:c.counts.data_lake??0;return{matched:n,deleted:d,samples:a,counts:c.counts,erasureId:c.erasureId}}deleteDataCategory(t,e){return this.db.prepare(`
267
277
  DELETE FROM data_lake WHERE project_id = ? AND category = ?
268
278
  `).run(t,e).changes}*streamSessions(t){const e=this.db.prepare(`
269
279
  SELECT * FROM sessions WHERE project_id = ? ORDER BY date DESC
270
- `);for(const s of e.iterate(t))yield s}*streamData(t,e){const s=e?this.db.prepare("SELECT * FROM data_lake WHERE project_id = ? AND category = ?"):this.db.prepare("SELECT * FROM data_lake WHERE project_id = ?"),r=e?[t,e]:[t];for(const i of s.iterate(...r))yield i}getStats(){const t=this.db.prepare("SELECT COUNT(*) as count FROM projects").get(),e=this.db.prepare("SELECT COUNT(*) as count FROM sessions").get(),s=this.db.prepare("SELECT COUNT(*) as count FROM quests").get(),r=this.db.prepare("SELECT COUNT(*) as count FROM memory_artifacts").get(),i=this.db.prepare("SELECT COUNT(*) as count FROM data_lake").get(),n=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE is_archived = 0").get(),a=this.db.pragma("page_count",{simple:!0}),o=this.db.pragma("page_size",{simple:!0}),c=a*o/(1024*1024);return{projects:t.count,sessions:e.count,quests:s.count,artifacts:r.count,dataPoints:i.count,totalTokens:n.total,dbSize:`${c.toFixed(2)} MB`}}getProjectStats(t){const e=this.db.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(t),s=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status IN ('pending', 'in_progress')").get(t),r=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status = 'completed'").get(t),i=this.db.prepare("SELECT COUNT(*) as count FROM data_lake WHERE project_id = ?").get(t),n=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE project_id = ? AND is_archived = 0").get(t);return{sessions:e.count,quests:{pending:s.count,completed:r.count},dataPoints:i.count,tokens:n.total}}estimateTokens(t){return Math.ceil(t.length/4)}vacuum(){this.db.exec("VACUUM")}checkpoint(){this.db.pragma("wal_checkpoint(TRUNCATE)")}getResilienceStatus(){const t=this.resilience.getCircuitStatus(),e=this.resilience.getIncompleteOperations();return{circuitState:t.state,failures:t.failures,incompleteOps:e.length}}resetCircuitBreaker(){this.resilience.resetCircuit()}close(){try{this.checkpoint(),this.logger.info("Database checkpoint completed")}catch(t){this.logger.error("Checkpoint failed during close",{error:t.message})}try{this.db.close(),this.logger.info("Database closed successfully")}catch(t){this.logger.error("Database close failed",{error:t.message})}}checkIntegrity(){const t=[];try{const e=this.db.pragma("integrity_check",{simple:!0});e!=="ok"&&t.push(`Integrity check failed: ${e}`)}catch(e){t.push(`Integrity check error: ${e.message}`)}try{const e=this.db.pragma("foreign_key_check");e.length>0&&t.push(`Foreign key violations: ${e.length}`)}catch(e){t.push(`FK check error: ${e.message}`)}return{ok:t.length===0,issues:t}}}export{ye as WyrmDB,De as describeStoreError,Oe as resolveDbPath,ne as tightenStorePermissions};
280
+ `);for(const s of e.iterate(t))yield s}*streamData(t,e){const s=e?this.db.prepare("SELECT * FROM data_lake WHERE project_id = ? AND category = ?"):this.db.prepare("SELECT * FROM data_lake WHERE project_id = ?"),r=e?[t,e]:[t];for(const i of s.iterate(...r))yield i}getStats(){const t=this.db.prepare("SELECT COUNT(*) as count FROM projects").get(),e=this.db.prepare("SELECT COUNT(*) as count FROM sessions").get(),s=this.db.prepare("SELECT COUNT(*) as count FROM quests").get(),r=this.db.prepare("SELECT COUNT(*) as count FROM memory_artifacts").get(),i=this.db.prepare("SELECT COUNT(*) as count FROM data_lake").get(),n=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE is_archived = 0").get(),a=this.db.pragma("page_count",{simple:!0}),o=this.db.pragma("page_size",{simple:!0}),c=a*o/(1024*1024);return{projects:t.count,sessions:e.count,quests:s.count,artifacts:r.count,dataPoints:i.count,totalTokens:n.total,dbSize:`${c.toFixed(2)} MB`}}getProjectStats(t){const e=this.db.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(t),s=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status IN ('pending', 'in_progress')").get(t),r=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status = 'completed'").get(t),i=this.db.prepare("SELECT COUNT(*) as count FROM data_lake WHERE project_id = ?").get(t),n=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE project_id = ? AND is_archived = 0").get(t);return{sessions:e.count,quests:{pending:s.count,completed:r.count},dataPoints:i.count,tokens:n.total}}estimateTokens(t){return Math.ceil(t.length/4)}vacuum(){this.db.exec("VACUUM")}checkpoint(){this.db.pragma("wal_checkpoint(TRUNCATE)")}getResilienceStatus(){const t=this.resilience.getCircuitStatus(),e=this.resilience.getIncompleteOperations();return{circuitState:t.state,failures:t.failures,incompleteOps:e.length}}resetCircuitBreaker(){this.resilience.resetCircuit()}close(){try{this.checkpoint(),this.logger.info("Database checkpoint completed")}catch(t){this.logger.error("Checkpoint failed during close",{error:t.message})}try{this.db.close(),this.logger.info("Database closed successfully")}catch(t){this.logger.error("Database close failed",{error:t.message})}}checkIntegrity(){const t=[];try{const e=this.db.pragma("integrity_check",{simple:!0});e!=="ok"&&t.push(`Integrity check failed: ${e}`)}catch(e){t.push(`Integrity check error: ${e.message}`)}try{const e=this.db.pragma("foreign_key_check");e.length>0&&t.push(`Foreign key violations: ${e.length}`)}catch(e){t.push(`FK check error: ${e.message}`)}return{ok:t.length===0,issues:t}}}export{Ce as WyrmDB,Ie as describeStoreError,ye as resolveDbPath,oe as tightenStorePermissions};