wyrm-mcp 9.3.1 → 9.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +1 -1
  2. package/dist/autoconfig.js +22 -22
  3. package/dist/backup-cli.js +6 -6
  4. package/dist/backup-r2.js +1 -0
  5. package/dist/backup.js +1 -1
  6. package/dist/build-flags.js +1 -1
  7. package/dist/cloud-backup.js +4 -4
  8. package/dist/cloud-sync-entrypoint.js +1 -1
  9. package/dist/cloud-sync.js +2 -2
  10. package/dist/context-build-budgeted.js +4 -4
  11. package/dist/context-ranking.js +1 -1
  12. package/dist/crypto.js +1 -1
  13. package/dist/daemon-write-endpoint.js +1 -1
  14. package/dist/database.js +17 -11
  15. package/dist/egress-gate.js +1 -0
  16. package/dist/erasure-cli.js +3 -3
  17. package/dist/failure-patterns.js +79 -48
  18. package/dist/fts-health.js +1 -0
  19. package/dist/handlers/capture.js +50 -50
  20. package/dist/handlers/cloud.js +21 -21
  21. package/dist/handlers/datalake.js +5 -5
  22. package/dist/handlers/failure.js +22 -14
  23. package/dist/handlers/intelligence.js +45 -45
  24. package/dist/handlers/recall.js +17 -17
  25. package/dist/handlers/search.js +8 -8
  26. package/dist/handlers/session.js +22 -26
  27. package/dist/handlers/syncops.js +4 -4
  28. package/dist/hook-refresh.js +2 -0
  29. package/dist/http-fast.js +8 -8
  30. package/dist/import-rules.js +1 -0
  31. package/dist/index-health.js +34 -21
  32. package/dist/index.js +2 -2
  33. package/dist/indexer.js +4 -4
  34. package/dist/intelligence.js +30 -28
  35. package/dist/internal-dispatch.js +3 -3
  36. package/dist/lane-provenance.js +5 -4
  37. package/dist/live-embedding-state.js +2 -2
  38. package/dist/maintenance-cli.js +2 -0
  39. package/dist/memory-artifacts.js +62 -35
  40. package/dist/migrations.js +41 -61
  41. package/dist/providers/bundled.js +1 -1
  42. package/dist/providers/embedding-provider.js +1 -1
  43. package/dist/recorded-text-screen.js +1 -0
  44. package/dist/redact-backfill.js +1 -1
  45. package/dist/rehydration.js +37 -24
  46. package/dist/reindex.js +1 -1
  47. package/dist/replication-daemon-entrypoint.js +1 -1
  48. package/dist/rerank.js +1 -1
  49. package/dist/review-hold.js +8 -0
  50. package/dist/security.js +1 -1
  51. package/dist/self-maintenance.js +7 -4
  52. package/dist/stale-servers.js +2 -2
  53. package/dist/storage/index.js +4 -4
  54. package/dist/storage/node-sqlite-facade.js +1 -1
  55. package/dist/tool-manifest-v2.json +1 -1
  56. package/dist/tool-manifest.json +1 -1
  57. package/dist/update-verify.js +3 -0
  58. package/dist/vault.js +1 -1
  59. package/dist/vector-init.js +1 -1
  60. package/dist/vectors.js +6 -3
  61. package/dist/wyrm-cli.js +84 -81
  62. package/dist/wyrm-guard.js +17 -16
  63. package/dist/wyrm-loop.js +2 -2
  64. package/dist/wyrm-manifest.json +1 -1
  65. package/package.json +2 -1
  66. package/scripts/hooks/wyrm-hook-trust.mjs +116 -0
  67. package/scripts/hooks/wyrm-prompt-context.mjs +113 -97
  68. package/scripts/hooks/wyrm-session-rehydrate.mjs +58 -8
  69. package/scripts/postinstall.cjs +22 -0
  70. package/skills/wyrm-nvidia-nim/SKILL.md +31 -5
package/README.md CHANGED
@@ -88,7 +88,7 @@ Every artifact is tagged with where it came from: you, an agent, an import, or a
88
88
 
89
89
  ## NVIDIA NIM retrieval (optional)
90
90
 
91
- Local vectors are on by default. NIM is the further step up, for embeddings and reranking, when accuracy is worth a hosted call. On the LoCoMo retrieval benchmark committed in the repo (2 conversations, k=10, measured September 2026), the default NIM embedding model `nvidia/nemotron-3-embed-1b` reached recall@1 39.9% and recall@10 76.7%; adding the NIM reranker (`nvidia/llama-nemotron-rerank-vl-1b-v2`) raised recall@1 to 55.5% across the same 301 questions, at about one extra second per query. Figures published here earlier were measured on models NVIDIA retired on 25 August 2026, and have been withdrawn. It is an explicit opt-in, off by default, and the egress is disclosed on every call.
91
+ Local vectors are on by default. NIM is the further step up, for embeddings and reranking, when accuracy is worth a hosted call. On the LoCoMo retrieval benchmark committed in the repo (2 conversations, k=10, measured September 2026), the default NIM embedding model `nvidia/nemotron-3-embed-1b` reached recall@1 39.9% and recall@10 76.7%; adding the NIM reranker (`nvidia/llama-nemotron-rerank-vl-1b-v2`) raised recall@1 to 55.5% across the same 301 questions, at about one extra second per query. Figures published here earlier were measured on models NVIDIA retired on 25 August 2026, and have been withdrawn. It is an explicit opt-in, off by default, and the egress is disclosed on every call. NIM only receives text from groves you set to cloud or team: a private grove (every project starts as one) and every data-lake row are embedded on the bundled on-device model and never sent (see `docs/EGRESS.md` in the repository).
92
92
 
93
93
  The guided path is `wyrm upgrade`: a free API key, masked key entry, a live call to validate the key before anything is written, and an optional one-time reindex of existing memories on the new tier. To configure it by hand instead:
94
94
 
@@ -1,22 +1,22 @@
1
- import{existsSync as l,readFileSync as h,writeFileSync as p,mkdirSync as j,copyFileSync as ce,chmodSync as N}from"fs";import{homedir as y,platform as v}from"os";import{join as c,dirname as w,resolve as ae}from"path";import{fileURLToPath as A}from"url";import{spawnSync as P}from"child_process";import{npmGlobalPrefixFromPath as ue}from"./cli.js";function de(){const e=y(),t=v(),n=t==="darwin"?c(e,"Library","Application Support","Code","User"):t==="win32"?c(e,"AppData","Roaming","Code","User"):c(e,".config","Code","User"),o=t==="darwin"?c(e,"Library","Application Support","Code - Insiders","User"):t==="win32"?c(e,"AppData","Roaming","Code - Insiders","User"):c(e,".config","Code - Insiders","User"),s=t==="darwin"?c(e,"Library","Application Support","Claude"):t==="win32"?c(e,"AppData","Roaming","Claude"):c(e,".config","claude"),r=t==="darwin"?c(e,".cursor"):t==="win32"?c(e,".cursor"):c(e,".cursor"),i=t==="darwin"?c(e,".codeium","windsurf"):t==="win32"?c(e,".codeium","windsurf"):c(e,".codeium","windsurf"),a=t==="darwin"?c(e,".config","zed"):t==="win32"?c(e,"AppData","Roaming","Zed"):c(e,".config","zed"),d=c(e,".continue");return[{id:"vscode-copilot",name:"VS Code (Copilot)",icon:"\u{1F4BB}",configPath:c(n,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"vscode-insiders",name:"VS Code Insiders",icon:"\u{1F7E2}",configPath:c(o,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"claude-desktop",name:"Claude Desktop",icon:"\u{1F916}",configPath:c(s,"claude_desktop_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"claude-code",name:"Claude Code",icon:"\u2733\uFE0F",configPath:c(e,".claude.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"cursor",name:"Cursor",icon:"\u{1F4D0}",configPath:c(r,"mcp.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"windsurf",name:"Windsurf",icon:"\u{1F3C4}",configPath:c(i,"mcp_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"zed",name:"Zed",icon:"\u26A1",configPath:c(a,"settings.json"),configFormat:"zed",mcpKey:"context_servers",detected:!1,configured:!1},{id:"continue",name:"Continue",icon:"\u{1F504}",configPath:c(d,"config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1}]}function fe(e){try{const t=v()==="win32"?"where":"which",n=P(t,[e],{encoding:"utf-8",timeout:5e3});return n.status===0&&!!n.stdout&&n.stdout.trim().length>0}catch{return!1}}function _(){const e=de();for(const t of e){if(t.id==="claude-code")t.detected=l(c(w(t.configPath),".claude"))||fe("claude");else{const n=w(t.configPath);t.detected=l(n)}if(t.detected&&l(t.configPath))try{const n=h(t.configPath,"utf-8"),o=T(n);t.configured=me(o,t)}catch{t.configured=!1}t.detected&&(t.version=le(t))}return e}function le(e){try{switch(e.id){case"vscode-copilot":case"vscode-insiders":{const t=e.id==="vscode-insiders"?"code-insiders":"code",n=P(t,["--version"],{encoding:"utf-8",timeout:5e3});if(n.stdout)return n.stdout.split(`
1
+ import{existsSync as l,readFileSync as h,writeFileSync as p,mkdirSync as b,chmodSync as J}from"fs";import{homedir as w,platform as k}from"os";import{join as c,dirname as v,resolve as de}from"path";import{fileURLToPath as j}from"url";import{spawnSync as P}from"child_process";import{npmGlobalPrefixFromPath as fe}from"./cli.js";import{syncWyrmHookFiles as le,describeHookSync as me,WYRM_HOOK_SUPPORT_FILES as he}from"./hook-refresh.js";function pe(){const e=w(),t=k(),n=t==="darwin"?c(e,"Library","Application Support","Code","User"):t==="win32"?c(e,"AppData","Roaming","Code","User"):c(e,".config","Code","User"),o=t==="darwin"?c(e,"Library","Application Support","Code - Insiders","User"):t==="win32"?c(e,"AppData","Roaming","Code - Insiders","User"):c(e,".config","Code - Insiders","User"),s=t==="darwin"?c(e,"Library","Application Support","Claude"):t==="win32"?c(e,"AppData","Roaming","Claude"):c(e,".config","claude"),r=t==="darwin"?c(e,".cursor"):t==="win32"?c(e,".cursor"):c(e,".cursor"),i=t==="darwin"?c(e,".codeium","windsurf"):t==="win32"?c(e,".codeium","windsurf"):c(e,".codeium","windsurf"),a=t==="darwin"?c(e,".config","zed"):t==="win32"?c(e,"AppData","Roaming","Zed"):c(e,".config","zed"),d=c(e,".continue");return[{id:"vscode-copilot",name:"VS Code (Copilot)",icon:"\u{1F4BB}",configPath:c(n,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"vscode-insiders",name:"VS Code Insiders",icon:"\u{1F7E2}",configPath:c(o,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"claude-desktop",name:"Claude Desktop",icon:"\u{1F916}",configPath:c(s,"claude_desktop_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"claude-code",name:"Claude Code",icon:"\u2733\uFE0F",configPath:c(e,".claude.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"cursor",name:"Cursor",icon:"\u{1F4D0}",configPath:c(r,"mcp.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"windsurf",name:"Windsurf",icon:"\u{1F3C4}",configPath:c(i,"mcp_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"zed",name:"Zed",icon:"\u26A1",configPath:c(a,"settings.json"),configFormat:"zed",mcpKey:"context_servers",detected:!1,configured:!1},{id:"continue",name:"Continue",icon:"\u{1F504}",configPath:c(d,"config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1}]}function ge(e){try{const t=k()==="win32"?"where":"which",n=P(t,[e],{encoding:"utf-8",timeout:5e3});return n.status===0&&!!n.stdout&&n.stdout.trim().length>0}catch{return!1}}function O(){const e=pe();for(const t of e){if(t.id==="claude-code")t.detected=l(c(v(t.configPath),".claude"))||ge("claude");else{const n=v(t.configPath);t.detected=l(n)}if(t.detected&&l(t.configPath))try{const n=h(t.configPath,"utf-8"),o=W(n);t.configured=we(o,t)}catch{t.configured=!1}t.detected&&(t.version=ye(t))}return e}function ye(e){try{switch(e.id){case"vscode-copilot":case"vscode-insiders":{const t=e.id==="vscode-insiders"?"code-insiders":"code",n=P(t,["--version"],{encoding:"utf-8",timeout:5e3});if(n.stdout)return n.stdout.split(`
2
2
  `)[0];break}case"cursor":{const t=P("cursor",["--version"],{encoding:"utf-8",timeout:5e3});if(t.stdout)return t.stdout.split(`
3
3
  `)[0];break}case"claude-code":{const t=P("claude",["--version"],{encoding:"utf-8",timeout:5e3});if(t.stdout)return t.stdout.trim().split(`
4
- `)[0];break}}}catch{}}function me(e,t){switch(t.configFormat){case"vscode":return e.mcp?.servers?.wyrm!==void 0;case"mcp-json":return e[t.mcpKey]?.wyrm!==void 0;case"zed":return e[t.mcpKey]?.wyrm!==void 0;default:return!1}}function M(e=v()){if(e!=="win32"){try{const s=P("which",["wyrm-mcp"],{encoding:"utf-8",timeout:5e3});if(s.stdout?.trim())return s.stdout.trim()}catch{}try{const r=P("npm",["root","-g"],{encoding:"utf-8",timeout:5e3}).stdout?.trim();if(r){const i=c(r,"wyrm-mcp","dist","index.js");if(l(i))return i}}catch{}}const t=ae(w(A(import.meta.url)),"index.js");if(l(t))return t;const n=c(y(),".wyrm","node_modules","wyrm-mcp","dist","index.js");if(l(n))return n;const o=[c(y(),"Git Projects","Wyrm","packages","mcp-server","dist","index.js"),c(y(),"projects","Wyrm","packages","mcp-server","dist","index.js"),c(y(),"dev","Wyrm","packages","mcp-server","dist","index.js")];for(const s of o)if(l(s))return s;return"wyrm-mcp"}function U(){return c(y(),".wyrm","wyrm.db")}function Je(e="mcp-json",t){const n=t?.serverPath??M(),o=t?.dbPath??U(),s=n==="wyrm-mcp",r={command:s?"npx":"node",args:s?["wyrm-mcp"]:[n],env:{WYRM_DB_PATH:o}};switch(e){case"stdio":return r;case"vscode":return{mcp:{servers:{wyrm:r}}};default:return{mcpServers:{wyrm:r}}}}function he(e,t){const n=t.serverPath,o=n==="wyrm-mcp",s=n.endsWith(".js");switch(e.configFormat){case"vscode":return{command:o?"npx":"node",args:o?["wyrm-mcp"]:[n],env:{WYRM_DB_PATH:t.dbPath}};case"mcp-json":return{command:o?"npx":"node",args:o?["wyrm-mcp"]:[n],env:{WYRM_DB_PATH:t.dbPath}};case"zed":return{command:o?"npx":"node",args:o?["wyrm-mcp"]:[n],env:{WYRM_DB_PATH:t.dbPath}};default:return{}}}function pe(e,t){if(!e||typeof e!="object"||Array.isArray(e))return{entry:t,preservedLauncher:!1};const n=e,o=n.env&&typeof n.env=="object"&&!Array.isArray(n.env)?n.env:{},s=t.env&&typeof t.env=="object"&&!Array.isArray(t.env)?t.env:{};if(n.command===t.command&&JSON.stringify(n.args??null)===JSON.stringify(t.args??null))return{entry:{...t,env:{...s,...o}},preservedLauncher:!1};const i={...o};for(const[a,d]of Object.entries(s))a in i||(i[a]=d);return{entry:{...n,env:i},preservedLauncher:!0}}function q(e,t){if(!e.detected)return{client:e,action:"skipped",message:`${e.name} not detected`};try{const n=w(e.configPath);l(n)||j(n,{recursive:!0});let o={},s=!1;if(l(e.configPath))try{const u=h(e.configPath,"utf-8");o=T(u),s=!0}catch{o={}}let r;if(s){r=`${e.configPath}.wyrm-backup`,p(r,h(e.configPath),{mode:384});try{N(r,384)}catch{}}const i=he(e,t);let a=!1;const d=(u,m)=>{const g=pe(u.wyrm,m);return u.wyrm=g.entry,g.preservedLauncher};switch(e.configFormat){case"vscode":{o.mcp||(o.mcp={});const u=o.mcp;u.servers||(u.servers={});const m=u.servers;a=d(m,i)||a;break}case"mcp-json":{o[e.mcpKey]||(o[e.mcpKey]={});const u=o[e.mcpKey];a=d(u,i)||a;break}case"zed":{o[e.mcpKey]||(o[e.mcpKey]={});const u=o[e.mcpKey];a=d(u,i)||a;break}}p(e.configPath,JSON.stringify(o,null,2)+`
5
- `,"utf-8");const f=e.configured?"updated":"configured";return e.configured=!0,{client:e,action:f,message:`${e.name} ${f==="configured"?"configured":"updated"} successfully`+(a?" (kept your custom launch command; only missing env was added)":""),backup:r}}catch(n){return{client:e,action:"failed",message:`Failed to configure ${e.name}: ${n}`}}}function Me(e,t){try{if(!l(e.configPath))return{client:e,action:"skipped",message:"No config file"};const n=h(e.configPath,"utf-8");let o;try{o=T(n)}catch{return{client:e,action:"failed",message:"Config file is not valid JSON; refusing to touch it"}}const r=(e.configFormat==="vscode"?o.mcp?.servers:o[e.mcpKey])?.wyrm;if(!r)return{client:e,action:"skipped",message:"Wyrm not configured in this client"};const i=`${e.configPath}.wyrm-backup`;p(i,n,{encoding:"utf-8",mode:384});try{N(i,384)}catch{}return r.env={...r.env,...t},p(e.configPath,JSON.stringify(o,null,2)+`
6
- `,"utf-8"),{client:e,action:"updated",message:`env: ${Object.keys(t).join(", ")}`,backup:i}}catch(n){return{client:e,action:"failed",message:String(n)}}}function ge(e){if(!e.detected||!e.configured)return{client:e,action:"skipped",message:`${e.name} not configured`};try{if(!l(e.configPath))return{client:e,action:"skipped",message:`${e.name} config not found`};const t=h(e.configPath,"utf-8"),n=T(t),o=`${e.configPath}.wyrm-backup`;p(o,t,{mode:384});try{N(o,384)}catch{}switch(e.configFormat){case"vscode":{const r=n.mcp?.servers;r&&delete r.wyrm;break}case"mcp-json":case"zed":{const s=n[e.mcpKey];s&&delete s.wyrm;break}}return p(e.configPath,JSON.stringify(n,null,2)+`
7
- `,"utf-8"),e.configured=!1,{client:e,action:"configured",message:`Removed Wyrm from ${e.name}`,backup:o}}catch(t){return{client:e,action:"failed",message:`Failed to remove from ${e.name}: ${t}`}}}function E(){return{id:"claude-code-hooks",name:"Claude Code (auto-memory)",icon:"\u{1FA9D}",configPath:c(y(),".claude","settings.json"),configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}const W=[{script:"wyrm-session-capture.mjs",events:["SessionEnd","PreCompact"]},{script:"wyrm-session-rehydrate.mjs",events:["SessionStart"]},{script:"wyrm-session-prune.mjs",events:["SessionEnd"]},{script:"wyrm-tool-call-trace.mjs",events:["PostToolUse"]},{script:"wyrm-run-auto.mjs",events:["SessionStart",{event:"PreToolUse",matcher:"Task"},"SessionEnd"]},{script:"wyrm-citation-audit.mjs",events:["Stop"]},{script:"wyrm-prompt-context.mjs",events:["UserPromptSubmit"]}],R=/[$`%"]/;function z(e,t=v()){return t==="win32"?R.test(e)?null:`node ${O(e,t)}`:/^[\w@%+=:,./-]+$/.test(e)?`node ${e}`:`node ${O(e,t)}`}function Y(e,t=v()){return{current:z(e,t),legacy:`node ${e}`}}function ye(e,t,n){if(t.current===null)return!1;const o=t.current;let s=!1,r=!1;for(let i=0;i<e.length;i++){const a=e[i];if(!Array.isArray(a.hooks))continue;const d=a.hooks.length;a.hooks=a.hooks.filter(f=>f.command!==o&&f.command!==t.legacy?!0:s?(r=!0,!1):(s=!0,f.command!==o&&(f.command=o,r=!0),!0)),d>0&&a.hooks.length===0&&(e.splice(i,1),i--)}if(!s){const i={type:"command",command:o,timeout:30};e.push(n?{matcher:n,hooks:[i]}:{hooks:[i]}),r=!0}return r}function we(e={}){const t=e.claudeDir??c(y(),".claude");if(!l(t))return null;const n=E();e.claudeDir&&(n.configPath=c(t,"settings.json"));try{const o=c(t,"hooks");l(o)||j(o,{recursive:!0});const s=w(A(import.meta.url)),r=c(s,"..","scripts","hooks"),i=n.configPath;let a={};if(l(i))try{a=JSON.parse(h(i,"utf-8"))}catch{a={}}const d=a.hooks??{},f=c(o,"wyrm-cli.json"),u=JSON.stringify({cli:c(s,"wyrm.js")},null,2)+`
8
- `;let m=!1;try{let k=!1;try{const S=JSON.parse(h(f,"utf-8"));k=typeof S.cli=="string"&&l(S.cli)}catch{}(ue(c(s,"wyrm.js"))!==null||!k)&&!(l(f)&&h(f,"utf-8")===u)&&(p(f,u,"utf-8"),m=!0)}catch{}let g=!1;const $=[],x=[];for(const k of W){const D=c(r,k.script);if(!l(D)){$.push(k.script);continue}const S=c(o,k.script);ce(D,S);try{N(S,493)}catch{}const H=Y(S);if(H.current===null){x.push(k.script);continue}for(const C of k.events){const J=typeof C=="string"?C:C.event,ie=typeof C=="string"?void 0:C.matcher,G=Array.isArray(d[J])?d[J]:[];ye(G,H,ie)&&(g=!0),d[J]=G}}if(a.hooks=d,p(i,JSON.stringify(a,null,2)+`
9
- `,"utf-8"),x.length===W.length-$.length&&x.length>0)return{client:n,action:"failed",message:`${o} contains a character ($, \`, % or ") that no quoting makes safe in every Windows shell, so the hooks were not wired.`};n.configured=!0;const se=$.length?` (missing from package: ${$.join(", ")})`:"";return{client:n,action:g||m?"configured":"skipped",message:(g?"auto-memory hooks installed":m?"hooks already installed; CLI location refreshed":"already installed")+se}}catch(o){return{client:n,action:"failed",message:o instanceof Error?o.message:String(o)}}}function Ue(){const e=c(y(),".claude");if(!l(e))return null;const t=E();try{const n=w(A(import.meta.url)),o=z(c(n,"wyrm-statusline.js"));if(o===null)return{client:t,action:"failed",message:'the install path contains a character ($, `, % or ") that no quoting makes safe in every Windows shell, so the statusline was not set'};const s=t.configPath;let r={};if(l(s))try{r=JSON.parse(h(s,"utf-8"))}catch{r={}}const i=r.statusLine;if(i?.command&&!/wyrm-statusline/.test(i.command))return{client:t,action:"skipped",message:`a non-Wyrm statusLine is already set (${i.command}); not overwriting`};const a=i?.command===o;return r.statusLine={type:"command",command:o,padding:0},p(s,JSON.stringify(r,null,2)+`
10
- `,"utf-8"),{client:t,action:a?"skipped":"configured",message:a?"buddy statusline already installed":"Wyrm buddy statusline installed (persistent in the Claude Code TUI)"}}catch(n){return{client:t,action:"failed",message:n instanceof Error?n.message:String(n)}}}function Re(){const e=c(y(),".claude","settings.json");if(!l(e))return null;const t=E();try{const n=JSON.parse(h(e,"utf-8")),o=n.statusLine;return o?.command&&/wyrm-statusline/.test(o.command)?(delete n.statusLine,p(e,JSON.stringify(n,null,2)+`
11
- `,"utf-8"),{client:t,action:"configured",message:"Wyrm statusline removed"}):{client:t,action:"skipped",message:"no Wyrm statusline set"}}catch(n){return{client:t,action:"failed",message:n instanceof Error?n.message:String(n)}}}function ve(e={}){const t=e.claudeDir??c(y(),".claude"),n=c(t,"settings.json");if(!l(n))return null;const o=E();e.claudeDir&&(o.configPath=n);try{const s=JSON.parse(h(n,"utf-8")),r=c(t,"hooks"),i=new Set(W.flatMap(f=>{const u=Y(c(r,f.script));return u.current===null?[u.legacy]:[u.current,u.legacy]})),a=new Set(W.flatMap(f=>f.events.map(u=>typeof u=="string"?u:u.event))),d=s.hooks;if(d){for(const f of a)Array.isArray(d[f])&&(d[f]=d[f].map(u=>({...u,hooks:Array.isArray(u.hooks)?u.hooks.filter(m=>!i.has(m.command??"")):u.hooks})).filter(u=>!Array.isArray(u.hooks)||u.hooks.length>0),d[f].length===0&&delete d[f]);Object.keys(d).length===0&&delete s.hooks}return p(n,JSON.stringify(s,null,2)+`
12
- `,"utf-8"),{client:o,action:"configured",message:"auto-memory hooks removed"}}catch(s){return{client:o,action:"failed",message:s instanceof Error?s.message:String(s)}}}const V=/wyrm-guard/,Q=5,ke=["Bash","Edit|Write|MultiEdit|NotebookEdit"];function b(e){return e.settingsPath??process.env.CLAUDE_SETTINGS??c(y(),".claude","settings.json")}function L(e){return{id:"claude-code-guard",name:"Claude Code (failure firewall)",icon:"\u{1F6E1}\uFE0F",configPath:e,configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}function O(e,t=v()){return t==="win32"?`"${e.replace(/\\/g,"/").replace(/"/g,'""')}"`:`'${e.replace(/'/g,"'\\''")}'`}function Z(e=v()){const t=w(A(import.meta.url));return e==="win32"&&R.test(t)?`the install path (${t}) contains a character ($, \`, % or ") that no quoting makes safe in every Windows shell, so the hook was not installed`:null}function X(e,t=v()){return t==="win32"&&R.test(e)?null:`node ${O(e,t)}`}function Pe(){const e=w(A(import.meta.url)),t=c(e,"wyrm-guard.js");if(l(t))return X(t);try{const n=v()==="win32"?"where":"which",s=P(n,["wyrm-guard"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
13
- `)[0]?.trim();if(s)return O(s)}catch{}return null}function K(e,t){const n=e.hooks;if(!n||typeof n!="object"||Array.isArray(n))return!1;let o=!1;for(const s of Object.keys(n)){const r=n[s];if(!Array.isArray(r))continue;let i=!1;const a=[];for(const d of r){const f=d;if(!f||typeof f!="object"||!Array.isArray(f.hooks)){a.push(d);continue}const u=f.hooks.filter(m=>!t.test(m?.command??""));if(u.length===f.hooks.length){a.push(d);continue}o=!0,i=!0,u.length>0&&a.push({...f,hooks:u})}i&&(a.length===0?delete n[s]:n[s]=a)}return o&&Object.keys(n).length===0&&delete e.hooks,o}function ee(e){return K(e,V)}function be(e={}){if(!!!(e.settingsPath??process.env.CLAUDE_SETTINGS)&&!l(c(y(),".claude")))return{client:L(b(e)),action:"skipped",message:"failure firewall: ~/.claude not found on this machine (needs the Claude Code CLI). Not armed. Install Claude Code then re-run `wyrm guard`"};const n=b(e),o=L(n);try{const s=e.guardCommand??Pe();if(!s)return{client:o,action:"skipped",message:Z()??"wyrm-guard bin not found (npm i -g wyrm-mcp, or build dist/); firewall hooks not installed"};j(w(n),{recursive:!0});let r={};if(l(n))try{const g=JSON.parse(h(n,"utf-8"));if(typeof g!="object"||g===null||Array.isArray(g))throw new Error("not an object");r=g}catch{return{client:o,action:"failed",message:`${n} is not valid JSON; refusing to overwrite it (fix or remove the file, then re-run)`}}const i=r.hooks;if(i!==void 0&&(typeof i!="object"||i===null||Array.isArray(i)))return{client:o,action:"failed",message:`${n} "hooks" has an unexpected shape (not a plain object); refusing to modify (fix the file, then re-run)`};const a=i?.PreToolUse;if(a!==void 0&&!Array.isArray(a))return{client:o,action:"failed",message:`${n} hooks.PreToolUse has an unexpected shape (not an array); refusing to modify (fix the file, then re-run)`};const d=JSON.stringify(r);ee(r);const f=r.hooks??{},u=Array.isArray(f.PreToolUse)?f.PreToolUse:[];for(const g of ke)u.push({matcher:g,hooks:[{type:"command",command:s,timeout:Q}]});f.PreToolUse=u,r.hooks=f;const m=JSON.stringify(r)!==d;return m&&p(n,JSON.stringify(r,null,2)+`
14
- `,"utf-8"),o.configured=!0,{client:o,action:m?"configured":"skipped",message:m?"failure-firewall hooks installed (PreToolUse: Bash + Edit|Write|MultiEdit|NotebookEdit)":"failure-firewall hooks already installed"}}catch(s){return{client:o,action:"failed",message:s instanceof Error?s.message:String(s)}}}function Se(e={}){const t=b(e);if(!l(t))return null;const n=L(t);try{let o;try{const r=JSON.parse(h(t,"utf-8"));if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("not an object");o=r}catch{return{client:n,action:"failed",message:`${t} is not valid JSON; not touching it`}}return ee(o)?(p(t,JSON.stringify(o,null,2)+`
15
- `,"utf-8"),{client:n,action:"configured",message:"failure-firewall hooks removed"}):{client:n,action:"skipped",message:"no wyrm-guard hooks installed"}}catch(o){return{client:n,action:"failed",message:o instanceof Error?o.message:String(o)}}}function Le(e={}){const t=b(e),n=[];try{const s=JSON.parse(h(t,"utf-8")).hooks??{};for(const r of Object.values(s))if(Array.isArray(r))for(const i of r)for(const a of i?.hooks??[]){const d=a?.command;d&&V.test(d)&&n.push(d)}}catch{}return{settingsPath:t,installed:n.length>0,commands:n}}const F=/wyrm-memo/,je=[{event:"PreToolUse",matcher:"Read"},{event:"PreToolUse",matcher:"Bash"},{event:"PostToolUse",matcher:"Read"},{event:"PostToolUse",matcher:"Bash"}];function te(e){return{id:"claude-code-memo",name:"Claude Code (token firewall)",icon:"",configPath:e,configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}function Ae(){const e=w(A(import.meta.url)),t=c(e,"wyrm-memo.js");if(l(t))return X(t);try{const n=v()==="win32"?"where":"which",s=P(n,["wyrm-memo"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
16
- `)[0]?.trim();if(s)return O(s)}catch{}return null}function Ce(e={}){const t=b(e),n=!!(e.settingsPath??process.env.CLAUDE_SETTINGS),o=te(t);if(!n&&!l(c(y(),".claude")))return{client:o,action:"skipped",message:"token firewall: ~/.claude not found on this machine (needs the Claude Code CLI). Not armed. Install Claude Code then re-run `wyrm memo`"};try{const s=e.memoCommand??Ae();if(!s)return{client:o,action:"skipped",message:Z()??"wyrm-memo bin not found (npm i -g wyrm-mcp, or build dist/). Token-firewall hooks not installed"};j(w(t),{recursive:!0});let r={};if(l(t))try{const u=JSON.parse(h(t,"utf-8"));if(typeof u!="object"||u===null||Array.isArray(u))throw new Error("not an object");r=u}catch{return{client:o,action:"failed",message:`${t} is not valid JSON; refusing to overwrite it (fix or remove the file, then re-run)`}}const i=r.hooks;if(i!==void 0&&(typeof i!="object"||i===null||Array.isArray(i)))return{client:o,action:"failed",message:`${t} "hooks" has an unexpected shape (not a plain object); refusing to modify`};for(const u of["PreToolUse","PostToolUse"]){const m=i?.[u];if(m!==void 0&&!Array.isArray(m))return{client:o,action:"failed",message:`${t} hooks.${u} has an unexpected shape (not an array); refusing to modify`}}const a=JSON.stringify(r);K(r,F);const d=r.hooks??{};for(const{event:u,matcher:m}of je){const g=Array.isArray(d[u])?d[u]:[];g.push({matcher:m,hooks:[{type:"command",command:s,timeout:Q}]}),d[u]=g}r.hooks=d;const f=JSON.stringify(r)!==a;return f&&p(t,JSON.stringify(r,null,2)+`
17
- `,"utf-8"),o.configured=!0,{client:o,action:f?"configured":"skipped",message:f?"token-firewall hooks installed (PreToolUse + PostToolUse: Read, Bash). Mode: observe (set WYRM_MEMO_MODE=warn to hand maps back)":"token-firewall hooks already installed"}}catch(s){return{client:o,action:"failed",message:s instanceof Error?s.message:String(s)}}}function Ke(e={}){const t=b(e);if(!l(t))return null;const n=te(t);try{let o;try{const r=JSON.parse(h(t,"utf-8"));if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("not an object");o=r}catch{return{client:n,action:"failed",message:`${t} is not valid JSON; not touching it`}}return K(o,F)?(p(t,JSON.stringify(o,null,2)+`
18
- `,"utf-8"),{client:n,action:"configured",message:"token-firewall hooks removed"}):{client:n,action:"skipped",message:"no wyrm-memo hooks installed"}}catch(o){return{client:n,action:"failed",message:o instanceof Error?o.message:String(o)}}}function Fe(e={}){const t=b(e),n=[];try{const s=JSON.parse(h(t,"utf-8")).hooks??{};for(const r of Object.values(s))if(Array.isArray(r))for(const i of r)for(const a of i?.hooks??[]){const d=a?.command;d&&F.test(d)&&n.push(d)}}catch{}return{settingsPath:t,installed:n.length>0,commands:n}}function _e(e){const t={serverPath:e?.serverPath||M(),dbPath:e?.dbPath||U(),httpPort:e?.httpPort},n=c(y(),".wyrm");l(n)||j(n,{recursive:!0});const o=_(),s=[];for(const d of o)s.push(q(d,t));oe(t);const r=we();r&&s.push(r);const i=be();i&&s.push(i);const a=Ce();return a&&s.push(a),s}function Ie(){const e=_(),t=[];for(const s of e)t.push(ge(s));const n=ve();n&&t.push(n);const o=Se();return o&&t.push(o),t}function Oe(e,t){const n={serverPath:t?.serverPath||M(),dbPath:t?.dbPath||U(),httpPort:t?.httpPort},o=_(),s=[];for(const r of o)e.includes(r.id)&&s.push(q(r,n));return oe(n),s}function oe(e){const t=c(y(),".wyrm","wyrm-config.json"),n=_().filter(s=>s.configured).map(s=>s.id),o={version:"3.0.0",serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort,configuredClients:n,lastSetup:new Date().toISOString(),autoUpdate:!0};p(t,JSON.stringify(o,null,2)+`
19
- `,"utf-8")}function ne(){const e=c(y(),".wyrm","wyrm-config.json");if(!l(e))return null;try{return JSON.parse(h(e,"utf-8"))}catch{return null}}function Be(){const e=ne();return e?Oe(e.configuredClients,{serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort}):_e()}const re="<!-- wyrm:start -->",I="<!-- wyrm:end -->",B=`${re}
4
+ `)[0];break}}}catch{}}function we(e,t){switch(t.configFormat){case"vscode":return e.mcp?.servers?.wyrm!==void 0;case"mcp-json":return e[t.mcpKey]?.wyrm!==void 0;case"zed":return e[t.mcpKey]?.wyrm!==void 0;default:return!1}}function R(e=k()){if(e!=="win32"){try{const s=P("which",["wyrm-mcp"],{encoding:"utf-8",timeout:5e3});if(s.stdout?.trim())return s.stdout.trim()}catch{}try{const r=P("npm",["root","-g"],{encoding:"utf-8",timeout:5e3}).stdout?.trim();if(r){const i=c(r,"wyrm-mcp","dist","index.js");if(l(i))return i}}catch{}}const t=de(v(j(import.meta.url)),"index.js");if(l(t))return t;const n=c(w(),".wyrm","node_modules","wyrm-mcp","dist","index.js");if(l(n))return n;const o=[c(w(),"Git Projects","Wyrm","packages","mcp-server","dist","index.js"),c(w(),"projects","Wyrm","packages","mcp-server","dist","index.js"),c(w(),"dev","Wyrm","packages","mcp-server","dist","index.js")];for(const s of o)if(l(s))return s;return"wyrm-mcp"}function U(){return c(w(),".wyrm","wyrm.db")}function Ie(e="mcp-json",t){const n=t?.serverPath??R(),o=t?.dbPath??U(),s=n==="wyrm-mcp",r={command:s?"npx":"node",args:s?["wyrm-mcp"]:[n],env:{WYRM_DB_PATH:o}};switch(e){case"stdio":return r;case"vscode":return{mcp:{servers:{wyrm:r}}};default:return{mcpServers:{wyrm:r}}}}function ve(e,t){const n=t.serverPath,o=n==="wyrm-mcp",s=n.endsWith(".js");switch(e.configFormat){case"vscode":return{command:o?"npx":"node",args:o?["wyrm-mcp"]:[n],env:{WYRM_DB_PATH:t.dbPath}};case"mcp-json":return{command:o?"npx":"node",args:o?["wyrm-mcp"]:[n],env:{WYRM_DB_PATH:t.dbPath}};case"zed":return{command:o?"npx":"node",args:o?["wyrm-mcp"]:[n],env:{WYRM_DB_PATH:t.dbPath}};default:return{}}}function ke(e,t){if(!e||typeof e!="object"||Array.isArray(e))return{entry:t,preservedLauncher:!1};const n=e,o=n.env&&typeof n.env=="object"&&!Array.isArray(n.env)?n.env:{},s=t.env&&typeof t.env=="object"&&!Array.isArray(t.env)?t.env:{};if(n.command===t.command&&JSON.stringify(n.args??null)===JSON.stringify(t.args??null))return{entry:{...t,env:{...s,...o}},preservedLauncher:!1};const i={...o};for(const[a,d]of Object.entries(s))a in i||(i[a]=d);return{entry:{...n,env:i},preservedLauncher:!0}}function V(e,t){if(!e.detected)return{client:e,action:"skipped",message:`${e.name} not detected`};try{const n=v(e.configPath);l(n)||b(n,{recursive:!0});let o={},s=!1;if(l(e.configPath))try{const u=h(e.configPath,"utf-8");o=W(u),s=!0}catch{o={}}let r;if(s){r=`${e.configPath}.wyrm-backup`,p(r,h(e.configPath),{mode:384});try{J(r,384)}catch{}}const i=ve(e,t);let a=!1;const d=(u,m)=>{const g=ke(u.wyrm,m);return u.wyrm=g.entry,g.preservedLauncher};switch(e.configFormat){case"vscode":{o.mcp||(o.mcp={});const u=o.mcp;u.servers||(u.servers={});const m=u.servers;a=d(m,i)||a;break}case"mcp-json":{o[e.mcpKey]||(o[e.mcpKey]={});const u=o[e.mcpKey];a=d(u,i)||a;break}case"zed":{o[e.mcpKey]||(o[e.mcpKey]={});const u=o[e.mcpKey];a=d(u,i)||a;break}}p(e.configPath,JSON.stringify(o,null,2)+`
5
+ `,"utf-8");const f=e.configured?"updated":"configured";return e.configured=!0,{client:e,action:f,message:`${e.name} ${f==="configured"?"configured":"updated"} successfully`+(a?" (kept your custom launch command; only missing env was added)":""),backup:r}}catch(n){return{client:e,action:"failed",message:`Failed to configure ${e.name}: ${n}`}}}function Fe(e,t){try{if(!l(e.configPath))return{client:e,action:"skipped",message:"No config file"};const n=h(e.configPath,"utf-8");let o;try{o=W(n)}catch{return{client:e,action:"failed",message:"Config file is not valid JSON; refusing to touch it"}}const r=(e.configFormat==="vscode"?o.mcp?.servers:o[e.mcpKey])?.wyrm;if(!r)return{client:e,action:"skipped",message:"Wyrm not configured in this client"};const i=`${e.configPath}.wyrm-backup`;p(i,n,{encoding:"utf-8",mode:384});try{J(i,384)}catch{}return r.env={...r.env,...t},p(e.configPath,JSON.stringify(o,null,2)+`
6
+ `,"utf-8"),{client:e,action:"updated",message:`env: ${Object.keys(t).join(", ")}`,backup:i}}catch(n){return{client:e,action:"failed",message:String(n)}}}function Pe(e){if(!e.detected||!e.configured)return{client:e,action:"skipped",message:`${e.name} not configured`};try{if(!l(e.configPath))return{client:e,action:"skipped",message:`${e.name} config not found`};const t=h(e.configPath,"utf-8"),n=W(t),o=`${e.configPath}.wyrm-backup`;p(o,t,{mode:384});try{J(o,384)}catch{}switch(e.configFormat){case"vscode":{const r=n.mcp?.servers;r&&delete r.wyrm;break}case"mcp-json":case"zed":{const s=n[e.mcpKey];s&&delete s.wyrm;break}}return p(e.configPath,JSON.stringify(n,null,2)+`
7
+ `,"utf-8"),e.configured=!1,{client:e,action:"configured",message:`Removed Wyrm from ${e.name}`,backup:o}}catch(t){return{client:e,action:"failed",message:`Failed to remove from ${e.name}: ${t}`}}}function N(){return{id:"claude-code-hooks",name:"Claude Code (auto-memory)",icon:"\u{1FA9D}",configPath:c(w(),".claude","settings.json"),configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}const A=[{script:"wyrm-session-capture.mjs",events:["SessionEnd","PreCompact"]},{script:"wyrm-session-rehydrate.mjs",events:["SessionStart"]},{script:"wyrm-session-prune.mjs",events:["SessionEnd"]},{script:"wyrm-tool-call-trace.mjs",events:["PostToolUse"]},{script:"wyrm-run-auto.mjs",events:["SessionStart",{event:"PreToolUse",matcher:"Task"},"SessionEnd"]},{script:"wyrm-citation-audit.mjs",events:["Stop"]},{script:"wyrm-prompt-context.mjs",events:["UserPromptSubmit"]}],He=A.map(e=>e.script),L=/[$`%"]/;function Q(e,t=k()){return t==="win32"?L.test(e)?null:`node ${$(e,t)}`:/^[\w@%+=:,./-]+$/.test(e)?`node ${e}`:`node ${$(e,t)}`}function Z(e,t=k()){return{current:Q(e,t),legacy:`node ${e}`}}function Se(e,t,n){if(t.current===null)return!1;const o=t.current;let s=!1,r=!1;for(let i=0;i<e.length;i++){const a=e[i];if(!Array.isArray(a.hooks))continue;const d=a.hooks.length;a.hooks=a.hooks.filter(f=>f.command!==o&&f.command!==t.legacy?!0:s?(r=!0,!1):(s=!0,f.command!==o&&(f.command=o,r=!0),!0)),d>0&&a.hooks.length===0&&(e.splice(i,1),i--)}if(!s){const i={type:"command",command:o,timeout:30};e.push(n?{matcher:n,hooks:[i]}:{hooks:[i]}),r=!0}return r}function be(e={}){const t=e.claudeDir??c(w(),".claude");if(!l(t))return null;const n=N();e.claudeDir&&(n.configPath=c(t,"settings.json"));try{const o=c(t,"hooks");l(o)||b(o,{recursive:!0});const s=v(j(import.meta.url)),r=c(s,"..","scripts","hooks"),i=n.configPath;let a={};if(l(i))try{a=JSON.parse(h(i,"utf-8"))}catch{a={}}const d=a.hooks??{},f=c(o,"wyrm-cli.json"),u=JSON.stringify({cli:c(s,"wyrm.js")},null,2)+`
8
+ `;let m=!1;try{let y=!1;try{const C=JSON.parse(h(f,"utf-8"));y=typeof C.cli=="string"&&l(C.cli)}catch{}(fe(c(s,"wyrm.js"))!==null||!y)&&!(l(f)&&h(f,"utf-8")===u)&&(p(f,u,"utf-8"),m=!0)}catch{}let g=!1;const T=[];let x="0.0.0";try{x=JSON.parse(h(c(s,"..","package.json"),"utf-8")).version??x}catch{}const D=le({hooksDir:o,scriptsDir:r,version:x,installMissing:!0,replaceModified:/^(1|true|yes|on)$/i.test(process.env.WYRM_HOOKS_REPLACE_MODIFIED??""),files:[...he,...A.map(y=>y.script)]}),E=D.results.filter(y=>y.action==="missing-source").map(y=>y.file),G=D.results.some(y=>y.action==="installed"||y.action==="updated");for(const y of A){if(E.includes(y.script))continue;const Y=c(o,y.script),C=Z(Y);if(C.current===null){T.push(y.script);continue}for(const _ of y.events){const M=typeof _=="string"?_:_.event,ue=typeof _=="string"?void 0:_.matcher,z=Array.isArray(d[M])?d[M]:[];Se(z,C,ue)&&(g=!0),d[M]=z}}if(a.hooks=d,p(i,JSON.stringify(a,null,2)+`
9
+ `,"utf-8"),T.length===A.length-E.length&&T.length>0)return{client:n,action:"failed",message:`${o} contains a character ($, \`, % or ") that no quoting makes safe in every Windows shell, so the hooks were not wired.`};n.configured=!0;const q=me(D).filter(y=>!y.startsWith("refreshed ")),ae=(E.length?` (missing from package: ${E.join(", ")})`:"")+(q.length?`; ${q.join("; ")}`:"");return{client:n,action:g||m||G?"configured":"skipped",message:(g?"auto-memory hooks installed":G?"hook scripts refreshed":m?"hooks already installed; CLI location refreshed":"already installed")+ae}}catch(o){return{client:n,action:"failed",message:o instanceof Error?o.message:String(o)}}}function Be(){const e=c(w(),".claude");if(!l(e))return null;const t=N();try{const n=v(j(import.meta.url)),o=Q(c(n,"wyrm-statusline.js"));if(o===null)return{client:t,action:"failed",message:'the install path contains a character ($, `, % or ") that no quoting makes safe in every Windows shell, so the statusline was not set'};const s=t.configPath;let r={};if(l(s))try{r=JSON.parse(h(s,"utf-8"))}catch{r={}}const i=r.statusLine;if(i?.command&&!/wyrm-statusline/.test(i.command))return{client:t,action:"skipped",message:`a non-Wyrm statusLine is already set (${i.command}); not overwriting`};const a=i?.command===o;return r.statusLine={type:"command",command:o,padding:0},p(s,JSON.stringify(r,null,2)+`
10
+ `,"utf-8"),{client:t,action:a?"skipped":"configured",message:a?"buddy statusline already installed":"Wyrm buddy statusline installed (persistent in the Claude Code TUI)"}}catch(n){return{client:t,action:"failed",message:n instanceof Error?n.message:String(n)}}}function Ge(){const e=c(w(),".claude","settings.json");if(!l(e))return null;const t=N();try{const n=JSON.parse(h(e,"utf-8")),o=n.statusLine;return o?.command&&/wyrm-statusline/.test(o.command)?(delete n.statusLine,p(e,JSON.stringify(n,null,2)+`
11
+ `,"utf-8"),{client:t,action:"configured",message:"Wyrm statusline removed"}):{client:t,action:"skipped",message:"no Wyrm statusline set"}}catch(n){return{client:t,action:"failed",message:n instanceof Error?n.message:String(n)}}}function je(e={}){const t=e.claudeDir??c(w(),".claude"),n=c(t,"settings.json");if(!l(n))return null;const o=N();e.claudeDir&&(o.configPath=n);try{const s=JSON.parse(h(n,"utf-8")),r=c(t,"hooks"),i=new Set(A.flatMap(f=>{const u=Z(c(r,f.script));return u.current===null?[u.legacy]:[u.current,u.legacy]})),a=new Set(A.flatMap(f=>f.events.map(u=>typeof u=="string"?u:u.event))),d=s.hooks;if(d){for(const f of a)Array.isArray(d[f])&&(d[f]=d[f].map(u=>({...u,hooks:Array.isArray(u.hooks)?u.hooks.filter(m=>!i.has(m.command??"")):u.hooks})).filter(u=>!Array.isArray(u.hooks)||u.hooks.length>0),d[f].length===0&&delete d[f]);Object.keys(d).length===0&&delete s.hooks}return p(n,JSON.stringify(s,null,2)+`
12
+ `,"utf-8"),{client:o,action:"configured",message:"auto-memory hooks removed"}}catch(s){return{client:o,action:"failed",message:s instanceof Error?s.message:String(s)}}}const X=/wyrm-guard/,ee=5,Ae=["Bash","Edit|Write|MultiEdit|NotebookEdit"];function S(e){return e.settingsPath??process.env.CLAUDE_SETTINGS??c(w(),".claude","settings.json")}function K(e){return{id:"claude-code-guard",name:"Claude Code (failure firewall)",icon:"\u{1F6E1}\uFE0F",configPath:e,configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}function $(e,t=k()){return t==="win32"?`"${e.replace(/\\/g,"/").replace(/"/g,'""')}"`:`'${e.replace(/'/g,"'\\''")}'`}function te(e=k()){const t=v(j(import.meta.url));return e==="win32"&&L.test(t)?`the install path (${t}) contains a character ($, \`, % or ") that no quoting makes safe in every Windows shell, so the hook was not installed`:null}function oe(e,t=k()){return t==="win32"&&L.test(e)?null:`node ${$(e,t)}`}function Ce(){const e=v(j(import.meta.url)),t=c(e,"wyrm-guard.js");if(l(t))return oe(t);try{const n=k()==="win32"?"where":"which",s=P(n,["wyrm-guard"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
13
+ `)[0]?.trim();if(s)return $(s)}catch{}return null}function I(e,t){const n=e.hooks;if(!n||typeof n!="object"||Array.isArray(n))return!1;let o=!1;for(const s of Object.keys(n)){const r=n[s];if(!Array.isArray(r))continue;let i=!1;const a=[];for(const d of r){const f=d;if(!f||typeof f!="object"||!Array.isArray(f.hooks)){a.push(d);continue}const u=f.hooks.filter(m=>!t.test(m?.command??""));if(u.length===f.hooks.length){a.push(d);continue}o=!0,i=!0,u.length>0&&a.push({...f,hooks:u})}i&&(a.length===0?delete n[s]:n[s]=a)}return o&&Object.keys(n).length===0&&delete e.hooks,o}function ne(e){return I(e,X)}function _e(e={}){if(!!!(e.settingsPath??process.env.CLAUDE_SETTINGS)&&!l(c(w(),".claude")))return{client:K(S(e)),action:"skipped",message:"failure firewall: ~/.claude not found on this machine (needs the Claude Code CLI). Not armed. Install Claude Code then re-run `wyrm guard`"};const n=S(e),o=K(n);try{const s=e.guardCommand??Ce();if(!s)return{client:o,action:"skipped",message:te()??"wyrm-guard bin not found (npm i -g wyrm-mcp, or build dist/); firewall hooks not installed"};b(v(n),{recursive:!0});let r={};if(l(n))try{const g=JSON.parse(h(n,"utf-8"));if(typeof g!="object"||g===null||Array.isArray(g))throw new Error("not an object");r=g}catch{return{client:o,action:"failed",message:`${n} is not valid JSON; refusing to overwrite it (fix or remove the file, then re-run)`}}const i=r.hooks;if(i!==void 0&&(typeof i!="object"||i===null||Array.isArray(i)))return{client:o,action:"failed",message:`${n} "hooks" has an unexpected shape (not a plain object); refusing to modify (fix the file, then re-run)`};const a=i?.PreToolUse;if(a!==void 0&&!Array.isArray(a))return{client:o,action:"failed",message:`${n} hooks.PreToolUse has an unexpected shape (not an array); refusing to modify (fix the file, then re-run)`};const d=JSON.stringify(r);ne(r);const f=r.hooks??{},u=Array.isArray(f.PreToolUse)?f.PreToolUse:[];for(const g of Ae)u.push({matcher:g,hooks:[{type:"command",command:s,timeout:ee}]});f.PreToolUse=u,r.hooks=f;const m=JSON.stringify(r)!==d;return m&&p(n,JSON.stringify(r,null,2)+`
14
+ `,"utf-8"),o.configured=!0,{client:o,action:m?"configured":"skipped",message:m?"failure-firewall hooks installed (PreToolUse: Bash + Edit|Write|MultiEdit|NotebookEdit)":"failure-firewall hooks already installed"}}catch(s){return{client:o,action:"failed",message:s instanceof Error?s.message:String(s)}}}function Oe(e={}){const t=S(e);if(!l(t))return null;const n=K(t);try{let o;try{const r=JSON.parse(h(t,"utf-8"));if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("not an object");o=r}catch{return{client:n,action:"failed",message:`${t} is not valid JSON; not touching it`}}return ne(o)?(p(t,JSON.stringify(o,null,2)+`
15
+ `,"utf-8"),{client:n,action:"configured",message:"failure-firewall hooks removed"}):{client:n,action:"skipped",message:"no wyrm-guard hooks installed"}}catch(o){return{client:n,action:"failed",message:o instanceof Error?o.message:String(o)}}}function qe(e={}){const t=S(e),n=[];try{const s=JSON.parse(h(t,"utf-8")).hooks??{};for(const r of Object.values(s))if(Array.isArray(r))for(const i of r)for(const a of i?.hooks??[]){const d=a?.command;d&&X.test(d)&&n.push(d)}}catch{}return{settingsPath:t,installed:n.length>0,commands:n}}const F=/wyrm-memo/,$e=[{event:"PreToolUse",matcher:"Read"},{event:"PreToolUse",matcher:"Bash"},{event:"PostToolUse",matcher:"Read"},{event:"PostToolUse",matcher:"Bash"}];function re(e){return{id:"claude-code-memo",name:"Claude Code (token firewall)",icon:"",configPath:e,configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}function Ee(){const e=v(j(import.meta.url)),t=c(e,"wyrm-memo.js");if(l(t))return oe(t);try{const n=k()==="win32"?"where":"which",s=P(n,["wyrm-memo"],{encoding:"utf-8",timeout:5e3}).stdout?.trim().split(`
16
+ `)[0]?.trim();if(s)return $(s)}catch{}return null}function Ne(e={}){const t=S(e),n=!!(e.settingsPath??process.env.CLAUDE_SETTINGS),o=re(t);if(!n&&!l(c(w(),".claude")))return{client:o,action:"skipped",message:"token firewall: ~/.claude not found on this machine (needs the Claude Code CLI). Not armed. Install Claude Code then re-run `wyrm memo`"};try{const s=e.memoCommand??Ee();if(!s)return{client:o,action:"skipped",message:te()??"wyrm-memo bin not found (npm i -g wyrm-mcp, or build dist/). Token-firewall hooks not installed"};b(v(t),{recursive:!0});let r={};if(l(t))try{const u=JSON.parse(h(t,"utf-8"));if(typeof u!="object"||u===null||Array.isArray(u))throw new Error("not an object");r=u}catch{return{client:o,action:"failed",message:`${t} is not valid JSON; refusing to overwrite it (fix or remove the file, then re-run)`}}const i=r.hooks;if(i!==void 0&&(typeof i!="object"||i===null||Array.isArray(i)))return{client:o,action:"failed",message:`${t} "hooks" has an unexpected shape (not a plain object); refusing to modify`};for(const u of["PreToolUse","PostToolUse"]){const m=i?.[u];if(m!==void 0&&!Array.isArray(m))return{client:o,action:"failed",message:`${t} hooks.${u} has an unexpected shape (not an array); refusing to modify`}}const a=JSON.stringify(r);I(r,F);const d=r.hooks??{};for(const{event:u,matcher:m}of $e){const g=Array.isArray(d[u])?d[u]:[];g.push({matcher:m,hooks:[{type:"command",command:s,timeout:ee}]}),d[u]=g}r.hooks=d;const f=JSON.stringify(r)!==a;return f&&p(t,JSON.stringify(r,null,2)+`
17
+ `,"utf-8"),o.configured=!0,{client:o,action:f?"configured":"skipped",message:f?"token-firewall hooks installed (PreToolUse + PostToolUse: Read, Bash). Mode: observe (set WYRM_MEMO_MODE=warn to hand maps back)":"token-firewall hooks already installed"}}catch(s){return{client:o,action:"failed",message:s instanceof Error?s.message:String(s)}}}function Ye(e={}){const t=S(e);if(!l(t))return null;const n=re(t);try{let o;try{const r=JSON.parse(h(t,"utf-8"));if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("not an object");o=r}catch{return{client:n,action:"failed",message:`${t} is not valid JSON; not touching it`}}return I(o,F)?(p(t,JSON.stringify(o,null,2)+`
18
+ `,"utf-8"),{client:n,action:"configured",message:"token-firewall hooks removed"}):{client:n,action:"skipped",message:"no wyrm-memo hooks installed"}}catch(o){return{client:n,action:"failed",message:o instanceof Error?o.message:String(o)}}}function ze(e={}){const t=S(e),n=[];try{const s=JSON.parse(h(t,"utf-8")).hooks??{};for(const r of Object.values(s))if(Array.isArray(r))for(const i of r)for(const a of i?.hooks??[]){const d=a?.command;d&&F.test(d)&&n.push(d)}}catch{}return{settingsPath:t,installed:n.length>0,commands:n}}function We(e){const t={serverPath:e?.serverPath||R(),dbPath:e?.dbPath||U(),httpPort:e?.httpPort},n=c(w(),".wyrm");l(n)||b(n,{recursive:!0});const o=O(),s=[];for(const d of o)s.push(V(d,t));se(t);const r=be();r&&s.push(r);const i=_e();i&&s.push(i);const a=Ne();return a&&s.push(a),s}function Ve(){const e=O(),t=[];for(const s of e)t.push(Pe(s));const n=je();n&&t.push(n);const o=Oe();return o&&t.push(o),t}function Te(e,t){const n={serverPath:t?.serverPath||R(),dbPath:t?.dbPath||U(),httpPort:t?.httpPort},o=O(),s=[];for(const r of o)e.includes(r.id)&&s.push(V(r,n));return se(n),s}function se(e){const t=c(w(),".wyrm","wyrm-config.json"),n=O().filter(s=>s.configured).map(s=>s.id),o={version:"3.0.0",serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort,configuredClients:n,lastSetup:new Date().toISOString(),autoUpdate:!0};p(t,JSON.stringify(o,null,2)+`
19
+ `,"utf-8")}function ie(){const e=c(w(),".wyrm","wyrm-config.json");if(!l(e))return null;try{return JSON.parse(h(e,"utf-8"))}catch{return null}}function Qe(){const e=ie();return e?Te(e.configuredClients,{serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort}):We()}const ce="<!-- wyrm:start -->",H="<!-- wyrm:end -->",B=`${ce}
20
20
  ## Wyrm Memory + Agent (auto-managed \u2014 do not edit this section)
21
21
 
22
22
  **Session start** \u2014 call \`wyrm_session_prime\` first to load project context (ground truths, open quests, recent sessions, scaffolds) before responding.
@@ -40,23 +40,23 @@ import{existsSync as l,readFileSync as h,writeFileSync as p,mkdirSync as j,copyF
40
40
  **Keep wyrm-mcp current** \u2014 \`wyrm_check_update\` returns whether a newer version is on npm (cached 24h). When one is, surface it to the operator and offer \`wyrm_self_update\` (with \`confirm:true\`) to upgrade.
41
41
 
42
42
  **Buddy companion** \u2014 call \`wyrm_buddy\` for warm, data-grounded check-ins. At session start (after \`wyrm_session_prime\`) for long-running work, and when the operator completes a quest or hits a milestone. The buddy speaks in a chosen persona but every claim sources a real DB row \u2014 speak it verbatim, never invent additional encouragement. Auto-federates with other registered MCPs that expose \`*_buddy\` tools.
43
- ${I}`;function He(e,t){const n={injected:[],skipped:[],errors:[]},o=(r,i)=>{try{const a=w(r);if(l(a)||j(a,{recursive:!0}),l(r)){const d=h(r,"utf-8"),f=d.indexOf(re),u=d.indexOf(I);if(f!==-1&&u!==-1&&u>f){const m=d.slice(0,f),g=d.slice(u+I.length);p(r,`${m}${B}${g}`,"utf-8")}else{const m=d.endsWith(`
43
+ ${H}`;function Ze(e,t){const n={injected:[],skipped:[],errors:[]},o=(r,i)=>{try{const a=v(r);if(l(a)||b(a,{recursive:!0}),l(r)){const d=h(r,"utf-8"),f=d.indexOf(ce),u=d.indexOf(H);if(f!==-1&&u!==-1&&u>f){const m=d.slice(0,f),g=d.slice(u+H.length);p(r,`${m}${B}${g}`,"utf-8")}else{const m=d.endsWith(`
44
44
  `)?`
45
45
  `:`
46
46
 
47
47
  `;p(r,`${d}${m}${B}
48
48
  `,"utf-8")}}else p(r,`${B}
49
- `,"utf-8");n.injected.push(i)}catch(a){n.errors.push(`${i}: ${a}`)}},s=t.length===0?["copilot","cursor"]:t;for(const r of s){const i=r.toLowerCase();if(i==="copilot"||i==="vscode-copilot"){const a=c(e,".github","copilot-instructions.md");o(a,".github/copilot-instructions.md")}else if(i==="cursor"){const a=c(e,".cursor","rules");o(a,".cursor/rules")}else n.skipped.push(r)}return n}function T(e){let t="",n=!1;for(let o=0;o<e.length;o++){const s=e[o];if(n){if(t+=s,s==="\\"){t+=e[++o]??"";continue}s==='"'&&(n=!1);continue}if(s==='"'){n=!0,t+=s;continue}if(s==="/"&&e[o+1]==="/"){for(;o<e.length&&e[o]!==`
49
+ `,"utf-8");n.injected.push(i)}catch(a){n.errors.push(`${i}: ${a}`)}},s=t.length===0?["copilot","cursor"]:t;for(const r of s){const i=r.toLowerCase();if(i==="copilot"||i==="vscode-copilot"){const a=c(e,".github","copilot-instructions.md");o(a,".github/copilot-instructions.md")}else if(i==="cursor"){const a=c(e,".cursor","rules");o(a,".cursor/rules")}else n.skipped.push(r)}return n}function W(e){let t="",n=!1;for(let o=0;o<e.length;o++){const s=e[o];if(n){if(t+=s,s==="\\"){t+=e[++o]??"";continue}s==='"'&&(n=!1);continue}if(s==='"'){n=!0,t+=s;continue}if(s==="/"&&e[o+1]==="/"){for(;o<e.length&&e[o]!==`
50
50
  `;)o++;t+=`
51
51
  `;continue}if(s==="/"&&e[o+1]==="*"){for(o+=2;o<e.length&&!(e[o]==="*"&&e[o+1]==="/");)o++;o+=1;continue}if(s===","){let r=o+1;for(;;){for(;r<e.length&&/\s/.test(e[r]);)r++;if(e[r]==="/"&&e[r+1]==="/"){for(;r<e.length&&e[r]!==`
52
- `;)r++;continue}if(e[r]==="/"&&e[r+1]==="*"){for(r+=2;r<e.length&&!(e[r]==="*"&&e[r+1]==="/");)r++;r+=2;continue}break}if(e[r]==="}"||e[r]==="]")continue}t+=s}return JSON.parse(t)}function Ge(){const e=_(),t=e.filter(r=>r.detected),n=e.filter(r=>r.configured);let o=`\u{F115D} Wyrm Auto-Configure Status
52
+ `;)r++;continue}if(e[r]==="/"&&e[r+1]==="*"){for(r+=2;r<e.length&&!(e[r]==="*"&&e[r+1]==="/");)r++;r+=2;continue}break}if(e[r]==="}"||e[r]==="]")continue}t+=s}return JSON.parse(t)}function Xe(){const e=O(),t=e.filter(r=>r.detected),n=e.filter(r=>r.configured);let o=`\u{F115D} Wyrm Auto-Configure Status
53
53
 
54
54
  `;o+=` Detected: ${t.length}/${e.length} AI clients
55
55
  `,o+=` Configured: ${n.length}/${t.length} clients
56
56
 
57
57
  `;for(const r of e){const i=r.detected?r.configured?" \u25CF":" \u25D0":" \u25CB",a=r.version?` (${r.version})`:"",d=r.detected?r.configured?"configured \u2713":"detected, not configured":"not found";o+=`${i} ${r.icon} ${r.name}${a}: ${d}
58
- `}const s=ne();return s&&(o+=`
58
+ `}const s=ie();return s&&(o+=`
59
59
  Server: ${s.serverPath}
60
60
  `,o+=` DB: ${s.dbPath}
61
61
  `,o+=` Last: ${new Date(s.lastSetup).toLocaleString()}
62
- `),o}export{Q as GUARD_HOOK_TIMEOUT_SEC,je as MEMO_HOOK_EVENTS,B as WYRM_INJECT_BLOCK,Me as applyWyrmEnvToClient,_e as autoConfigureAll,q as configureClient,Oe as configureSpecific,_ as detectClients,Je as exportServerConfig,M as findWyrmServerPath,U as getDefaultDbPath,Ge as getStatusSummary,Y as hookCommandForms,z as hookNodeCommand,He as injectSystemPrompt,we as installClaudeCodeHooks,Ue as installClaudeStatusline,be as installWyrmGuardHooks,Ce as installWyrmMemoHooks,ne as loadWyrmMeta,ye as mergeHookCommand,pe as mergeServerEntry,T as parseJsonWithComments,O as quoteHookPath,Be as reconfAll,ve as removeClaudeCodeHooks,Re as removeClaudeStatusline,Ie as removeFromAll,ge as removeFromClient,Se as removeWyrmGuardHooks,Ke as removeWyrmMemoHooks,Pe as resolveWyrmGuardCommand,Ae as resolveWyrmMemoCommand,Le as wyrmGuardHookStatus,Fe as wyrmMemoHookStatus};
62
+ `),o}export{He as CLAUDE_HOOK_SCRIPT_NAMES,ee as GUARD_HOOK_TIMEOUT_SEC,$e as MEMO_HOOK_EVENTS,B as WYRM_INJECT_BLOCK,Fe as applyWyrmEnvToClient,We as autoConfigureAll,V as configureClient,Te as configureSpecific,O as detectClients,Ie as exportServerConfig,R as findWyrmServerPath,U as getDefaultDbPath,Xe as getStatusSummary,Z as hookCommandForms,Q as hookNodeCommand,Ze as injectSystemPrompt,be as installClaudeCodeHooks,Be as installClaudeStatusline,_e as installWyrmGuardHooks,Ne as installWyrmMemoHooks,ie as loadWyrmMeta,Se as mergeHookCommand,ke as mergeServerEntry,W as parseJsonWithComments,$ as quoteHookPath,Qe as reconfAll,je as removeClaudeCodeHooks,Ge as removeClaudeStatusline,Ve as removeFromAll,Pe as removeFromClient,Oe as removeWyrmGuardHooks,Ye as removeWyrmMemoHooks,Ce as resolveWyrmGuardCommand,Ee as resolveWyrmMemoCommand,qe as wyrmGuardHookStatus,ze as wyrmMemoHookStatus};
@@ -1,6 +1,6 @@
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
- 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
- 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
- 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(`
5
- Another prefix can be a second live machine. Prune one you have retired:`)),console.log(d.dim(" wyrm backup cloud --prune <prefix> or --stale-days <n> (dry run unless --yes)"));return}const E=e.yes===!0;for(const p of x){const b=await r.pruneSnapshotPrefix(p,{dryRun:!E});console.log(` ${E?"deleted":"would delete"} ${E?b.deleted:b.objects} object(s), ${m(b.bytes)} under ${p}${b.failed?` (${b.failed} failed)`:""}`)}E||console.log(d.dim(`
6
- Dry run: nothing was deleted. Re-run with --yes to delete.`));return}default:g(`Unknown backup subcommand: ${u}`);for(const s of K)console.log(` ${s}`);process.exitCode=1}}catch(s){g(s.message),process.exitCode=1}}export{re as cmdBackup,m as fmtBytes};
1
+ import{join as R,resolve as v,relative as Y,isAbsolute as L}from"path";import{homedir as M}from"os";import{existsSync as C}from"fs";import{createInterface as F}from"readline";import{c as y,formatTable as S,printSection as k,printSuccess as x,printError as f,printWarning as _}from"./cli.js";import{createBackup as N,listBackups as U,pruneBackups as D,planRotation as I,restoreBackup as j,scanBackupHygiene as P,applyLegacyActions as O,defaultBackupDir as H,vaultBackupKey as T,readScheduledStatus as V,scheduledStatusNote as q,DEFAULT_KEEP_DAILY as z,DEFAULT_KEEP_WEEKLY as G}from"./backup.js";function J(o){const c=[],e={};for(let h=0;h<o.length;h++){const d=o[h];if(d.startsWith("--")){const a=o[h+1];a!==void 0&&!a.startsWith("--")?(e[d.slice(2)]=a,h++):e[d.slice(2)]=!0}else c.push(d)}return{positional:c,flags:e}}function Q(){return process.env.WYRM_DB_PATH??R(M(),".wyrm","wyrm.db")}function A(o,c,e){const h=typeof o=="string"?o:c,d=h===void 0?NaN:parseInt(h,10);return Number.isFinite(d)&&d>=0?d:e}function g(o){return o<1024?`${o} B`:o<1024**2?`${(o/1024).toFixed(1)} KB`:o<1024**3?`${(o/1024**2).toFixed(1)} MB`:`${(o/1024**3).toFixed(2)} GB`}function X(o){const c=(Date.now()-o.getTime())/36e5;return c<1?"under 1h":c<48?`${Math.floor(c)}h`:`${Math.floor(c/24)}d`}function B(o){const c=typeof o["passphrase-env"]=="string"?o["passphrase-env"]:"WYRM_BACKUP_PASSPHRASE",e=process.env[c];if(!e)throw new Error(`set ${c} to the backup passphrase (read from the environment, never from the command line)`);return{source:"passphrase",secret:e}}function Z(o){return c=>c==="vault"?T(!1):B(o)}async function W(o,c){if(!process.stdin.isTTY)return!1;const e=F({input:process.stdin,output:process.stdout});try{return(await new Promise(d=>{e.question(o,d)})).trim()===c}finally{e.close()}}function m(o,c){const e=Y(c,o);return e&&!e.startsWith("..")&&!L(e)?e:o}const K=["wyrm backup create [--encrypt | --passphrase-env <VAR>] [--label <name>] [--no-prune] [--upload]","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 ie(o){const{positional:c,flags:e}=J(o),h=c[0]??"list",d=Q(),a=typeof e.dir=="string"?v(e.dir):H(d),w=A(e["keep-daily"],process.env.WYRM_BACKUP_KEEP_DAILY,z),$=A(e["keep-weekly"],process.env.WYRM_BACKUP_KEEP_WEEKLY,G);try{switch(h){case"create":{const n=e.encrypt===!0?T(!0):typeof e["passphrase-env"]=="string"?B(e):void 0,t=await N({dbPath:d,dir:a,label:typeof e.label=="string"?e.label:void 0,key:n});if(x(`Snapshot ${t.file} \xB7 ${g(t.bytes)} \xB7 ${t.encrypted?`encrypted (${n.source} key)`:"plaintext"} \xB7 mode 0600 in ${a}`),n||console.log(y.dim(" plaintext: anyone who can read this file can read the whole store. --encrypt keys it from the vault.")),e["no-prune"]!==!0){const i=D(a,{daily:w,weekly:$,dryRun:!1});if(i.remove.length>0){console.log(` Rotation removed ${i.remove.length} older snapshot(s) (keeping ${w} daily, ${$} weekly; --no-prune skips this):`);for(const p of i.remove)console.log(` removed ${p.file} ${g(p.bytes)}`)}}if(e.upload===!0){const{uploadStep:i}=await import("./backup-r2.js"),p=await i(a,{required:!0,storePath:d});for(const r of p.lines)(p.failed?f:s=>console.log(` ${s}`))(r);p.failed&&(process.exitCode=1)}return}case"list":case"ls":{const n=U(a);if(k(`Snapshots in ${a}`),n.length===0)console.log(y.dim(" none yet: wyrm backup create --encrypt"));else{const p=I(n,{daily:w,weekly:$}),r=new Set(p.keep.map(l=>l.path)),s=new Set(p.pinned.map(l=>l.path));console.log(S(["File","Size","Mode","Encrypted","Age","Rotation"],n.map(l=>[l.file,g(l.bytes),`0${l.mode.toString(8)}`,l.encrypted?"yes":"no",X(l.at),s.has(l.path)?"pinned (labelled)":r.has(l.path)?"keep":"next prune"])))}const t=q(V(a),n[0]??null);t&&_(t);const i=P({dbPath:d,dir:a});(i.legacy.length>0||i.exposed.length>0)&&_(`${i.legacy.length} database copy(ies) outside rotation, ${i.exposed.length} item(s) readable by others: wyrm backup legacy --dry-run`);return}case"prune":{const n=e["dry-run"]===!0,t=D(a,{daily:w,weekly:$,dryRun:n,includeLabelled:e["include-labelled"]===!0});k(n?"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 i of t.remove)console.log(` ${n?"would remove":"removed"} ${i.file} ${g(i.bytes)}`);t.remove.length===0?console.log(y.dim(" nothing to remove")):n&&console.log(y.dim(`
2
+ Dry run: nothing was deleted. Re-run without --dry-run to remove these.`));return}case"restore":{const n=c[1];if(!n){f(`usage: ${K[3]}`),process.exitCode=1;return}const t=C(n)?v(n):R(a,n);if(!C(t)){f(`no snapshot at ${n} (also looked in ${a})`),process.exitCode=1;return}const i=e.apply===!0&&e["dry-run"]!==!0,p=typeof e.target=="string"?v(e.target):d;if(i&&e.yes!==!0&&!await W(`Restore ${m(t,a)} over ${p}? The current store is snapshotted first. Type RESTORE to continue: `,"RESTORE")){f("Restore not confirmed; nothing changed. Pass --yes to skip the prompt."),process.exitCode=1;return}const r=await j({file:t,targetPath:p,apply:i,workDir:R(a,".work"),keyFor:Z(e),safetyDir:a});k(i?"Restored":"Restore check (dry run)"),console.log(` snapshot ${m(t,a)}${r.inspection.encrypted?" (encrypted)":""}`),console.log(` integrity ${r.inspection.integrity}`),console.log(` schema v${r.inspection.schemaVersion}`);const s=Object.keys(r.inspection.counts);console.log(S(["Table","Snapshot",r.current?"Current store":"Current store (absent)"],s.map(l=>[l,String(r.inspection.counts[l]),r.current?String(r.current[l]??"-"):"-"]))),i?(x(`Restored into ${p}; integrity ${r.targetIntegrity}, row counts match the snapshot.`),r.safetySnapshot&&console.log(` The store as it was before is ${m(r.safetySnapshot,a)}.`),console.log(y.dim(" Restart open Wyrm sessions so their servers read the restored store."))):console.log(y.dim(`
3
+ Dry run: nothing was written. Re-run with --apply to restore.`));return}case"legacy":{const n=e.apply===!0&&e["dry-run"]!==!0,t=P({dbPath:d,dir:a});k(n?"Legacy backups":"Legacy backups (dry run)");const i=t.legacy.reduce((s,l)=>s+l.bytes,0);console.log(` ${t.legacy.length} database copy(ies) outside rotation, ${g(i)}`);for(const s of t.legacy)console.log(` ${m(s.path,t.home)} 0${s.mode.toString(8)} ${g(s.bytes)}${s.sidecars.length?` +${s.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 s of t.exposed)console.log(` ${m(s.path,t.home)} 0${s.mode.toString(8)}`);if(!n){console.log(y.dim(`
4
+ Dry run: nothing changed.`)),console.log(y.dim(" --apply tighten permissions (files 0600, folders 0700)")),console.log(y.dim(" --apply --adopt also move the copies into rotation, where prune ages them out")),console.log(y.dim(" --apply --delete delete the copies instead (asks you to type DELETE unless --yes)"));return}const p=e.delete===!0;if(p&&e.yes!==!0&&!await W(`Delete ${t.legacy.length} database copy(ies)? This cannot be undone. Type DELETE to continue: `,"DELETE")){f("Delete not confirmed; nothing was deleted. Pass --yes to skip the prompt."),process.exitCode=1;return}const r=O({dbPath:d,dir:a,tighten:!0,adopt:e.adopt===!0,deleteCopies:p});x(`Tightened ${r.tightened} item(s); adopted ${r.adopted.length}; deleted ${r.deleted.length}.`);for(const s of r.adopted)console.log(` ${m(s.from,t.home)} -> ${m(s.to,t.home)}`);for(const s of r.skipped)_(`skipped ${m(s.path,t.home)}: ${s.why}`);return}case"cloud":{const{WyrmCloudBackup:n}=await import("./cloud-backup.js"),{loadR2Config:t}=await import("./backup-r2.js"),i=t();if(!i){f("cloud snapshots need WYRM_R2_ENDPOINT, WYRM_R2_ACCESS_KEY and WYRM_R2_SECRET_KEY (and WYRM_R2_BUCKET if not wyrm-backups), in the environment or ~/.wyrm/cloud.env."),process.exitCode=1;return}const p=new n;p.configure(i);const r=await p.listSnapshotPrefixes();k("Cloud snapshot prefixes"),console.log(S(["Prefix","Machine","Snapshots","Size","Newest"],r.map(u=>[u.prefix,u.current?"this machine":"other",String(u.snapshots),g(u.bytes),u.newest?u.newest.slice(0,10):"-"])));const s=typeof e["stale-days"]=="string"?parseInt(e["stale-days"],10):NaN,l=typeof e.prune=="string"?[e.prune]:Number.isFinite(s)&&s>0?r.filter(u=>!u.current&&u.newest&&Date.now()-Date.parse(u.newest)>s*864e5).map(u=>u.prefix):[];if(l.length===0){console.log(y.dim(`
5
+ Another prefix can be a second live machine. Prune one you have retired:`)),console.log(y.dim(" wyrm backup cloud --prune <prefix> or --stale-days <n> (dry run unless --yes)"));return}const E=e.yes===!0;for(const u of l){const b=await p.pruneSnapshotPrefix(u,{dryRun:!E});console.log(` ${E?"deleted":"would delete"} ${E?b.deleted:b.objects} object(s), ${g(b.bytes)} under ${u}${b.failed?` (${b.failed} failed)`:""}`)}E||console.log(y.dim(`
6
+ Dry run: nothing was deleted. Re-run with --yes to delete.`));return}default:f(`Unknown backup subcommand: ${h}`);for(const n of K)console.log(` ${n}`);process.exitCode=1}}catch(n){f(n.message),process.exitCode=1}}export{ie as cmdBackup,g as fmtBytes};
@@ -0,0 +1 @@
1
+ import{existsSync as f,readFileSync as p}from"fs";import{homedir as y}from"os";import{join as m}from"path";import{listBackups as _,planRotation as E,encryptedSource as R,parseBackupName as h,DEFAULT_KEEP_DAILY as Y,DEFAULT_KEEP_WEEKLY as v}from"./backup.js";const K=["WYRM_R2_ENDPOINT","WYRM_R2_ACCESS_KEY","WYRM_R2_SECRET_KEY","WYRM_R2_BUCKET"];function S(){return m(y(),".wyrm","cloud.env")}function C(o){const r={};for(const t of o.split(/\r?\n/)){const e=t.trim();if(!e||e.startsWith("#"))continue;const n=/^(?:export\s+)?([A-Z0-9_]+)=(.*)$/.exec(e);if(!n)continue;const s=n[1];if(!K.includes(s))continue;let i=n[2].trim();const l=/^"(.*)"$/.exec(i)??/^'(.*)'$/.exec(i);l?i=l[1]:i=i.replace(/\s+#.*$/,"").trim(),i&&(r[s]=i)}return r}function W(o=process.env,r=S()){let t={};try{f(r)&&(t=C(p(r,"utf8")))}catch{}const e=l=>o[l]||t[l],n=e("WYRM_R2_ENDPOINT"),s=e("WYRM_R2_ACCESS_KEY"),i=e("WYRM_R2_SECRET_KEY");return!n||!s||!i?null:{endpoint:n,accessKeyId:s,secretAccessKey:i,bucket:e("WYRM_R2_BUCKET")||"wyrm-backups"}}function w(o,r){const t=(e,n)=>{const s=e===void 0?NaN:parseInt(e,10);return Number.isFinite(s)&&s>=0?s:n};return{daily:r?.daily??t(o.WYRM_BACKUP_KEEP_DAILY,Y),weekly:r?.weekly??t(o.WYRM_BACKUP_KEEP_WEEKLY,v)}}async function M(o){const r=o.env??process.env,t=_(o.dir).find(c=>c.encrypted&&R(c.path)!==null);if(!t)return{action:"no-encrypted-snapshot",remoteRemoved:[],remoteFailed:[]};const e=(await o.cloud.listUploadedSnapshots(o.storeId)).filter(c=>!c.legacy);let n="already-uploaded",s;e.some(c=>c.file===t.file)||(s=await o.cloud.uploadSnapshotFile(t.path,t.file,o.storeId),e.push({file:t.file,size:t.bytes}),n="uploaded");const i=[];for(const c of e){const u=h(c.file);u&&i.push({file:c.file,path:c.file,at:u.at,bytes:c.size,mode:384,encrypted:u.encrypted,label:u.label})}const l=E(i,w(r,o.policy)),a=[],d=[];for(const c of l.remove)if(c.file!==t.file)try{await o.cloud.deleteUploadedSnapshot(c.file,o.storeId),a.push(c.file)}catch{d.push(c.file)}return{action:n,file:t.file,key:s,remoteRemoved:a,remoteFailed:d}}async function F(o,r){const t=r.env??process.env,e=W(t,r.cloudEnvPath);if(!e)return r.required?{lines:["cloud copy: R2 is not configured (WYRM_R2_ENDPOINT, WYRM_R2_ACCESS_KEY, WYRM_R2_SECRET_KEY, WYRM_R2_BUCKET in the environment or ~/.wyrm/cloud.env)"],failed:!0}:{lines:[],failed:!1};try{const{WyrmCloudBackup:n,storeSnapshotId:s}=await import("./cloud-backup.js"),i=new n;i.configure(e);const l=await M({dir:o,cloud:i,env:t,storeId:s(r.storePath)}),a=[];if(l.action==="no-encrypted-snapshot")return a.push("cloud copy: no encrypted snapshot to upload; plaintext snapshots never leave this machine (wyrm backup create --encrypt keys one from the vault)"),{lines:a,failed:r.required};a.push(l.action==="uploaded"?`cloud copy: uploaded ${l.file} to ${e.bucket}`:`cloud copy: ${l.file} is already in ${e.bucket}`);for(const d of l.remoteRemoved)a.push(`cloud copy: rotation removed ${d}`);return l.remoteFailed.length>0&&a.push(`cloud copy: could not remove ${l.remoteFailed.join(", ")} (retried next run)`),{lines:a,failed:!1}}catch(n){return{lines:[`cloud copy FAILED: ${n instanceof Error?n.message:String(n)}`],failed:!0}}}export{S as defaultCloudEnvPath,W as loadR2Config,C as parseCloudEnv,M as uploadNewestEncryptedSnapshot,F as uploadStep};
package/dist/backup.js CHANGED
@@ -1 +1 @@
1
- import{chmodSync as w,closeSync as E,createReadStream as I,existsSync as y,mkdirSync as nt,openSync as C,readdirSync as A,readSync as b,renameSync as M,statSync as v,unlinkSync as P,writeSync as T,copyFileSync as ot,lstatSync as $}from"fs";import{basename as K,dirname as W,join as d}from"path";import{createCipheriv as st,createDecipheriv as at,randomBytes as R}from"crypto";import{openDatabase as k}from"./storage/index.js";import{VAULT_CIPHER as m,deriveVaultKey as ct,vaultGet as q,vaultSet as it}from"./vault.js";import{migrations as lt}from"./migrations.js";import{bumpDeviceEpoch as ut}from"./lineage.js";import{assertStoreWritable as z}from"./store-open.js";const j=/^wyrm-(\d{8})-(\d{6})(?:-([a-z0-9][a-z0-9-]{0,31}))?\.db(\.enc)?$/,ft=7,ht=4,_="wyrm.backup.key",p=Buffer.from("WYRMBAK1","ascii"),U=16,O=p.length+1+U+m.ivLength,L=Buffer.from("SQLite format 3\0","binary"),V=".work";function Y(t,e=process.env){return e.WYRM_BACKUP_DIR||d(W(t),"backups")}function x(t){nt(t,{recursive:!0,mode:448});try{w(t,448)}catch{}return t}function g(t,e=2){return String(t).padStart(e,"0")}function F(t){return`${t.getUTCFullYear()}${g(t.getUTCMonth()+1)}${g(t.getUTCDate())}-${g(t.getUTCHours())}${g(t.getUTCMinutes())}${g(t.getUTCSeconds())}`}function pt(t){return t.toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"snapshot"}function H(t,e){const n=j.exec(e);if(!n)return null;const[,r,o,i,l]=n,s=new Date(Date.UTC(+r.slice(0,4),+r.slice(4,6)-1,+r.slice(6,8),+o.slice(0,2),+o.slice(2,4),+o.slice(4,6))),a=d(t,e);let c;try{c=$(a)}catch{return null}return c.isFile()?{file:e,path:a,at:s,bytes:c.size,mode:c.mode&511,encrypted:!!l,label:i??null}:null}function yt(t){if(!y(t))return[];const e=[];for(const n of A(t)){const r=H(t,n);r&&e.push(r)}return e.sort((n,r)=>r.at.getTime()-n.at.getTime()||r.file.localeCompare(n.file))}function S(t){for(const e of[t,`${t}-wal`,`${t}-shm`,`${t}-journal`])try{y(e)&&P(e)}catch{}}function Nt(t){let e=q(_);if(!e){if(!t)throw new Error(`this snapshot is vault-keyed, but the vault holds no ${_}`);e=R(m.keyLength).toString("hex"),it(_,e)}return{source:"vault",secret:e}}function Q(t,e){if(t.source==="passphrase"){if(!t.secret)throw new Error("an empty passphrase cannot key a backup");return ct(t.secret,e)}const n=Buffer.from(t.secret,"hex");if(n.length!==m.keyLength)throw new Error("the vault backup key is malformed");return n}async function dt(t,e,n){const r=R(U),o=R(m.ivLength),i=st(m.algorithm,Q(n,r),o),l=C(e,"w",384);try{T(l,Buffer.concat([p,Buffer.from([n.source==="vault"?1:2]),r,o]));for await(const s of I(t))T(l,i.update(s));T(l,i.final()),T(l,i.getAuthTag())}finally{E(l)}}function mt(t){const e=C(t,"r");try{const n=Buffer.alloc(p.length+1);return b(e,n,0,n.length,0)<n.length||!n.subarray(0,p.length).equals(p)?null:n[p.length]===1?"vault":"passphrase"}finally{E(e)}}async function gt(t,e,n){const r=v(t).size;if(r<O+m.tagLength)throw new Error("encrypted snapshot is truncated");const o=C(t,"r"),i=Buffer.alloc(O),l=Buffer.alloc(m.tagLength);try{b(o,i,0,O,0),b(o,l,0,l.length,r-l.length)}finally{E(o)}if(!i.subarray(0,p.length).equals(p))throw new Error("not an encrypted Wyrm snapshot");const s=i[p.length]===1?"vault":"passphrase",a=i.subarray(p.length+1,p.length+1+U),c=i.subarray(p.length+1+U),f=at(m.algorithm,Q(n(s),a),c);f.setAuthTag(l);const u=C(e,"w",384);try{for await(const h of I(t,{start:O,end:r-l.length-1}))T(u,f.update(h));try{T(u,f.final())}catch{throw new Error("decryption failed: wrong key or passphrase, or the snapshot was altered")}}catch(h){throw E(u),S(e),h}E(u)}async function D(t,e){const n=k(t,{readonly:!0,fileMustExist:!0});try{typeof n.backup=="function"?await n.backup(e):n.prepare("VACUUM INTO ?").run(e)}finally{n.close()}}async function wt(t){if(!y(t.dbPath))throw new Error(`no store at ${t.dbPath}`);const e=x(t.dir),n=x(d(e,V)),r=t.now??new Date,o=t.label?pt(t.label):null,i=t.key?".db.enc":".db";let l=o,s=`wyrm-${F(r)}${l?`-${l}`:""}${i}`;for(let h=2;y(d(e,s));h++)l=`${o??"snapshot"}-${h}`,s=`wyrm-${F(r)}-${l}${i}`;const a=d(n,`${s}.partial`),c=d(e,s),f=process.umask(63);try{await Et(t.dbPath,a),t.key?await dt(a,c,t.key):M(a,c),w(c,384)}finally{S(a),process.umask(f)}const u=H(e,s);if(!u)throw new Error("snapshot written but could not be listed");return u}async function Et(t,e){const n=process.umask(63);try{S(e),await D(t,e);const r=k(e);let o="";try{r.pragma("journal_mode = DELETE"),o=String(r.pragma("quick_check",{simple:!0}))}finally{r.close()}if(o!=="ok")throw S(e),new Error(`the snapshot failed its integrity check (${o}); nothing was kept`);w(e,384)}finally{process.umask(n)}}async function kt(t,e={}){let n;try{const r=q(_);r&&(n={source:"vault",secret:r})}catch{}return wt({dbPath:t,dir:e.dir??Y(t),label:"pre-restore",key:n,now:e.now})}async function It(t,e){z(e);const n=process.umask(63);try{await D(t,e)}finally{process.umask(n)}const r=k(e);try{try{r.pragma("wal_checkpoint(TRUNCATE)")}catch{}return String(r.pragma("integrity_check",{simple:!0}))}finally{r.close()}}function St(t){return`${t.getUTCFullYear()}${g(t.getUTCMonth()+1)}${g(t.getUTCDate())}`}function Ct(t){const e=new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate())),n=e.getUTCDay()||7;e.setUTCDate(e.getUTCDate()+4-n);const r=e.getUTCFullYear(),o=Math.ceil(((e.getTime()-Date.UTC(r,0,1))/864e5+1)/7);return`${r}-W${g(o)}`}function G(t,e){return e||t.label===null||/^(snapshot|legacy)(-\d+)?$/.test(t.label)}function Tt(t,e={}){const n=Math.max(0,Math.floor(e.daily??ft)),r=Math.max(0,Math.floor(e.weekly??ht)),o=e.includeLabelled===!0,i=[...t].sort((u,h)=>h.at.getTime()-u.at.getTime()||h.file.localeCompare(u.file)),l=i.filter(u=>!G(u,o)),s=i.filter(u=>G(u,o)),a=new Set;s[0]&&a.add(s[0].path);const c=new Set,f=new Set;for(const u of s){const h=St(u.at);!c.has(h)&&c.size<n&&(c.add(h),a.add(u.path));const N=Ct(u.at);!f.has(N)&&f.size<r&&(f.add(N),a.add(u.path))}return{keep:s.filter(u=>a.has(u.path)),remove:s.filter(u=>!a.has(u.path)),pinned:l}}function Pt(t,e){const n=Tt(yt(t),e);if(!e.dryRun)for(const r of n.remove)S(r.path);return n}const bt={projects:"SELECT COUNT(*) AS n FROM projects",sessions:"SELECT COUNT(*) AS n FROM sessions",quests:"SELECT COUNT(*) AS n FROM quests",ground_truths:"SELECT COUNT(*) AS n FROM ground_truths",memory_artifacts:"SELECT COUNT(*) AS n FROM memory_artifacts",failure_patterns:"SELECT COUNT(*) AS n FROM failure_patterns",data_lake:"SELECT COUNT(*) AS n FROM data_lake",context:"SELECT COUNT(*) AS n FROM context",global_context:"SELECT COUNT(*) AS n FROM global_context",entities:"SELECT COUNT(*) AS n FROM entities",relationships:"SELECT COUNT(*) AS n FROM relationships",decision_edges:"SELECT COUNT(*) AS n FROM decision_edges",skills:"SELECT COUNT(*) AS n FROM skills",events:"SELECT COUNT(*) AS n FROM events",vectors:"SELECT COUNT(*) AS n FROM vectors",tool_call_log:"SELECT COUNT(*) AS n FROM tool_call_log",erasure_log:"SELECT COUNT(*) AS n FROM erasure_log",schema_versions:"SELECT COUNT(*) AS n FROM schema_versions"};function B(t){const e={};for(const[n,r]of Object.entries(bt))try{e[n]=t.prepare(r).get().n}catch{}return e}function $t(t){try{return t.prepare("SELECT COALESCE(MAX(version), 0) AS v FROM schema_versions").get().v}catch{return 0}}const X=lt.reduce((t,e)=>Math.max(t,e.version),0);async function J(t,e,n,r){const o=mt(t);if(!o)return r(t,!1);if(!n)throw new Error(`this snapshot is encrypted (${o}-keyed); supply its key`);const i=x(e),l=d(i,`${K(t)}.${process.pid}.restore`),s=process.umask(63);try{return await gt(t,l,n),await r(l,!0)}finally{S(l),process.umask(s)}}function Z(t,e,n){const r=Buffer.alloc(L.length),o=C(e,"r");try{b(o,r,0,r.length,0)}finally{E(o)}if(!r.equals(L))throw new Error("not a SQLite database");const i=k(e,{readonly:!0,fileMustExist:!0});try{return{file:t,encrypted:n,integrity:String(i.pragma("integrity_check",{simple:!0})),schemaVersion:$t(i),counts:B(i)}}finally{i.close()}}async function Kt(t,e){return J(t,e.workDir,e.keyFor,async(n,r)=>Z(t,n,r))}function _t(t,e){const n=new Set([...Object.keys(t),...Object.keys(e)]);for(const r of n)if(t[r]!==e[r])return!1;return!0}async function Wt(t){return J(t.file,t.workDir,t.keyFor,async(e,n)=>{const r=Z(t.file,e,n);if(r.integrity!=="ok")throw new Error(`the snapshot fails integrity_check (${r.integrity}); refusing to restore it`);if(r.schemaVersion>X)throw new Error(`the snapshot is at schema v${r.schemaVersion}, newer than this Wyrm (v${X}); upgrade Wyrm first`);let o=null;if(y(t.targetPath)){const c=k(t.targetPath,{readonly:!0,fileMustExist:!0});try{o=B(c)}finally{c.close()}}if(!t.apply)return{inspection:r,current:o,restored:!1,safetySnapshot:null,targetIntegrity:null};z(t.targetPath);let i=null;y(t.targetPath)&&(i=(await kt(t.targetPath,{dir:t.safetyDir,now:t.now})).path),await D(e,t.targetPath);const l=k(t.targetPath);let s="",a={};try{try{l.pragma("wal_checkpoint(TRUNCATE)")}catch{}s=String(l.pragma("integrity_check",{simple:!0})),a=B(l)}finally{l.close()}if(s!=="ok"||!_t(a,r.counts))throw new Error(`the restored store failed verification${i?`; the pre-restore snapshot is ${i}`:""}`);try{const c=k(t.targetPath);try{ut(c,"restored with wyrm backup restore")}finally{c.close()}}catch{}return{inspection:r,current:o,restored:!0,safetySnapshot:i,targetIntegrity:s}})}function tt(t){try{const e=C(t,"r");try{const n=Buffer.alloc(L.length);return b(e,n,0,n.length,0)===n.length&&n.equals(L)}finally{E(e)}}catch{return!1}}const et=/-(wal|shm|journal)$/;function Ut(t){return["-wal","-shm","-journal"].map(e=>`${t}${e}`).filter(e=>y(e))}function rt(t,e=process.platform){return e==="win32"?!1:(t&63)!==0}function Ot(t){const e=W(t.dbPath),n=t.dir??Y(t.dbPath),r=K(t.dbPath),o=[],i=(s,a)=>{const c=$(s),f=c.mode&511;return{path:s,kind:a,bytes:c.size,mode:f,exposed:rt(f),sidecars:a==="directory"?[]:Ut(s)}};if(y(e))for(const s of A(e)){const a=d(e,s);let c;try{c=$(a)}catch{continue}!c.isFile()||s===r||et.test(s)||!(/\.bak|\.db$|\.db\.|\.sqlite/i.test(s)||s.startsWith(`${r}.`))||o.push(i(a,tt(a)?"loose-copy":"other-file"))}const l=(s,a)=>{if(!(!y(s)||a>3)){o.push(i(s,"directory"));for(const c of A(s)){if(a===0&&c===V)continue;const f=d(s,c);let u;try{u=$(f)}catch{continue}if(u.isDirectory()){l(f,a+1);continue}if(!u.isFile()||et.test(c))continue;const h=a===0&&j.test(c)?"rotation":tt(f)?"unrotated":"other-file";o.push(i(f,h))}}};return l(n,0),{home:e,dir:n,items:o,legacy:o.filter(s=>s.kind==="loose-copy"||s.kind==="unrotated"),exposed:o.filter(s=>s.exposed||s.sidecars.some(a=>rt($(a).mode)))}}function Lt(t){return F(v(t).mtime)}function qt(t){const e=Ot({dbPath:t.dbPath,dir:t.dir}),n={tightened:0,adopted:[],deleted:[],skipped:[]};if(t.tighten)for(const r of e.exposed)try{w(r.path,r.kind==="directory"?448:384);for(const o of r.sidecars)w(o,384);n.tightened++}catch(o){n.skipped.push({path:r.path,why:`chmod failed: ${o.message}`})}if(t.deleteCopies){for(const r of e.legacy)S(r.path),n.deleted.push(r.path);return n}if(t.adopt){const r=x(e.dir);for(const o of e.legacy){const i=`${o.path}-wal`;if(y(i)&&v(i).size>0){n.skipped.push({path:o.path,why:"its -wal file is not empty; open it once with sqlite3 to checkpoint, then adopt"});continue}const l=Lt(o.path);let s=`wyrm-${l}-legacy.db`;for(let c=2;y(d(r,s));c++)s=`wyrm-${l}-legacy-${c}.db`;const a=d(r,s);try{M(o.path,a)}catch{ot(o.path,a),P(o.path)}w(a,384);for(const c of o.sidecars){const f=c.slice(o.path.length);try{M(c,`${a}${f}`),w(`${a}${f}`,384)}catch{}}n.adopted.push({from:o.path,to:a})}}return n}export{j as BACKUP_FILE_RE,X as BINARY_SCHEMA_VERSION,ft as DEFAULT_KEEP_DAILY,ht as DEFAULT_KEEP_WEEKLY,_ as VAULT_BACKUP_SECRET,qt as applyLegacyActions,wt as createBackup,Y as defaultBackupDir,mt as encryptedSource,x as ensurePrivateDir,Kt as inspectBackup,yt as listBackups,rt as modeExposed,Tt as planRotation,Pt as pruneBackups,Wt as restoreBackup,It as restoreFileInto,B as rowCounts,Ot as scanBackupHygiene,kt as snapshotBeforeRestore,Et as snapshotToFile,Nt as vaultBackupKey};
1
+ import{chmodSync as m,closeSync as w,createReadStream as I,existsSync as d,mkdirSync as ue,openSync as T,readdirSync as A,readSync as _,renameSync as L,statSync as M,unlinkSync as W,writeSync as C,copyFileSync as fe,lstatSync as v,readFileSync as he,writeFileSync as pe}from"fs";import{basename as Y,dirname as V,join as p}from"path";import{createCipheriv as ye,createDecipheriv as de,randomBytes as R}from"crypto";import{openDatabase as S}from"./storage/index.js";import{VAULT_CIPHER as g,deriveVaultKey as me,vaultGet as F,vaultGetWithoutKeyring as ge,vaultSet as ke}from"./vault.js";import{migrations as we}from"./migrations.js";import{bumpDeviceEpoch as Se}from"./lineage.js";import{assertStoreWritable as j}from"./store-open.js";const q=/^wyrm-(\d{8})-(\d{6})(?:-([a-z0-9][a-z0-9-]{0,31}))?\.db(\.enc)?$/,z=7,H=4,b="wyrm.backup.key",y=Buffer.from("WYRMBAK1","ascii"),U=16,$=y.length+1+U+g.ivLength,O=Buffer.from("SQLite format 3\0","binary"),Q=".work";function N(e,t=process.env){return t.WYRM_BACKUP_DIR||p(V(e),"backups")}function x(e){ue(e,{recursive:!0,mode:448});try{m(e,448)}catch{}return e}function k(e,t=2){return String(e).padStart(t,"0")}function D(e){return`${e.getUTCFullYear()}${k(e.getUTCMonth()+1)}${k(e.getUTCDate())}-${k(e.getUTCHours())}${k(e.getUTCMinutes())}${k(e.getUTCSeconds())}`}function Ee(e){return e.toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"snapshot"}function Te(e){const t=q.exec(e);if(!t)return null;const[,n,r,o,c]=t;return{at:new Date(Date.UTC(+n.slice(0,4),+n.slice(4,6)-1,+n.slice(6,8),+r.slice(0,2),+r.slice(2,4),+r.slice(4,6))),label:o??null,encrypted:!!c}}function G(e,t){const n=Te(t);if(!n)return null;const{at:r,label:o,encrypted:c}=n,s=p(e,t);let a;try{a=v(s)}catch{return null}return a.isFile()?{file:t,path:s,at:r,bytes:a.size,mode:a.mode&511,encrypted:c,label:o}:null}function J(e){if(!d(e))return[];const t=[];for(const n of A(e)){const r=G(e,n);r&&t.push(r)}return t.sort((n,r)=>r.at.getTime()-n.at.getTime()||r.file.localeCompare(n.file))}function E(e){for(const t of[e,`${e}-wal`,`${e}-shm`,`${e}-journal`])try{d(t)&&W(t)}catch{}}function Qe(e){let t=F(b);if(!t){if(!e)throw new Error(`this snapshot is vault-keyed, but the vault holds no ${b}`);t=R(g.keyLength).toString("hex"),ke(b,t)}return{source:"vault",secret:t}}function X(e,t){if(e.source==="passphrase"){if(!e.secret)throw new Error("an empty passphrase cannot key a backup");return me(e.secret,t)}const n=Buffer.from(e.secret,"hex");if(n.length!==g.keyLength)throw new Error("the vault backup key is malformed");return n}async function Ce(e,t,n){const r=R(U),o=R(g.ivLength),c=ye(g.algorithm,X(n,r),o),s=T(t,"w",384);try{C(s,Buffer.concat([y,Buffer.from([n.source==="vault"?1:2]),r,o]));for await(const a of I(e))C(s,c.update(a));C(s,c.final()),C(s,c.getAuthTag())}finally{w(s)}}function be(e){const t=T(e,"r");try{const n=Buffer.alloc(y.length+1);return _(t,n,0,n.length,0)<n.length||!n.subarray(0,y.length).equals(y)?null:n[y.length]===1?"vault":"passphrase"}finally{w(t)}}async function _e(e,t,n){const r=M(e).size;if(r<$+g.tagLength)throw new Error("encrypted snapshot is truncated");const o=T(e,"r"),c=Buffer.alloc($),s=Buffer.alloc(g.tagLength);try{_(o,c,0,$,0),_(o,s,0,s.length,r-s.length)}finally{w(o)}if(!c.subarray(0,y.length).equals(y))throw new Error("not an encrypted Wyrm snapshot");const a=c[y.length]===1?"vault":"passphrase",i=c.subarray(y.length+1,y.length+1+U),l=c.subarray(y.length+1+U),f=de(g.algorithm,X(n(a),i),l);f.setAuthTag(s);const u=T(t,"w",384);try{for await(const h of I(e,{start:$,end:r-s.length-1}))C(u,f.update(h));try{C(u,f.final())}catch{throw new Error("decryption failed: wrong key or passphrase, or the snapshot was altered")}}catch(h){throw w(u),E(t),h}w(u)}async function B(e,t){const n=S(e,{readonly:!0,fileMustExist:!0});try{typeof n.backup=="function"?await n.backup(t):n.prepare("VACUUM INTO ?").run(t)}finally{n.close()}}async function Z(e){if(!d(e.dbPath))throw new Error(`no store at ${e.dbPath}`);const t=x(e.dir),n=x(p(t,Q)),r=e.now??new Date,o=e.label?Ee(e.label):null,c=e.key?".db.enc":".db";let s=o,a=`wyrm-${D(r)}${s?`-${s}`:""}${c}`;for(let h=2;d(p(t,a));h++)s=`${o??"snapshot"}-${h}`,a=`wyrm-${D(r)}-${s}${c}`;const i=p(n,`${a}.partial`),l=p(t,a),f=process.umask(63);try{await ve(e.dbPath,i),e.key?await Ce(i,l,e.key):L(i,l),m(l,384)}finally{E(i),process.umask(f)}const u=G(t,a);if(!u)throw new Error("snapshot written but could not be listed");return u}async function ve(e,t){const n=process.umask(63);try{E(t),await B(e,t);const r=S(t);let o="";try{r.pragma("journal_mode = DELETE"),o=String(r.pragma("quick_check",{simple:!0}))}finally{r.close()}if(o!=="ok")throw E(t),new Error(`the snapshot failed its integrity check (${o}); nothing was kept`);m(t,384)}finally{process.umask(n)}}async function xe(e,t={}){return Z({dbPath:e,dir:t.dir??N(e),label:"pre-restore",key:Ue(),now:t.now})}function Ue(){try{const e=F(b);return e?{source:"vault",secret:e}:void 0}catch{return}}const $e=20;function ee(e,t){const n=e===void 0?NaN:parseInt(e,10);return Number.isFinite(n)&&n>=0?n:t}const te="scheduled.json";function Ge(e){try{const t=JSON.parse(he(p(e,te),"utf8"));return!t||typeof t.at!="string"||t.action!=="created"&&t.action!=="skipped"?null:t}catch{return null}}function ne(e,t){try{x(e);const n=p(e,te);pe(n,JSON.stringify(t),{mode:384}),m(n,384)}catch{}}function Je(e,t,n=Date.now()){if(!e)return"";const r=Date.parse(e.at);if(e.action==="skipped"&&e.reason&&(!t||!Number.isFinite(r)||r>=t.at.getTime())){const o=Number.isFinite(r)?Math.max(0,Math.floor((n-r)/36e5)):null,c=o===null?"":o>=24?` ${Math.floor(o/24)}d ago`:` ${o}h ago`;return`${e.by==="sweep"?"daily sweep":"wyrm maintenance"} skipped its snapshot${c}: ${e.reason}`}return e.action==="created"&&e.plaintext&&t&&e.file===t.file?`plaintext: ${e.plaintext}`:""}function Oe(e){const t=ge(b);if(t.state==="value")return{key:{source:"vault",secret:t.value}};if(t.state==="no-vault")return{plaintext:"no vault backup key is configured (there is no vault); wyrm backup create --encrypt makes one"};if(t.state==="missing")return{plaintext:"the vault holds no backup key; wyrm backup create --encrypt makes one"};if(!e)return{skip:t.state==="keychain"?"the vault's key is in the OS keychain, which a server never calls; wyrm maintenance takes the snapshot":`the vault could not be read without the keychain (${t.reason})`};try{const n=F(b);return n?{key:{source:"vault",secret:n}}:{plaintext:"the vault holds no backup key; wyrm backup create --encrypt makes one"}}catch(n){return{skip:`the vault could not be read (${(n instanceof Error?n.message:String(n)).slice(0,200)})`}}}async function Xe(e){const t=e.env??process.env;if((t.WYRM_AUTO_BACKUP??"1")==="0")return{action:"disabled",created:null,newest:null,pruned:[]};const n=e.dir??N(e.dbPath,t),r=e.now??new Date,o=Number(t.WYRM_BACKUP_INTERVAL_HOURS),c=(Number.isFinite(o)&&o>0?o:$e)*36e5,s=J(n)[0]??null;if(s&&r.getTime()-s.at.getTime()<c)return{action:"fresh",created:null,newest:s,pruned:[]};const a=e.keyring===!1?"sweep":"maintenance";let i,l;if(e.key!==void 0)i=e.key??void 0,i||(l="no backup key was given");else{const h=Oe(e.keyring!==!1);if(h.skip)return ne(n,{at:r.toISOString(),action:"skipped",by:a,reason:h.skip}),{action:"skipped",created:null,newest:s,pruned:[],reason:h.skip};i=h.key,l=h.plaintext}const f=await Z({dbPath:e.dbPath,dir:n,now:r,key:i});ne(n,{at:r.toISOString(),action:"created",by:a,file:f.file,...l?{plaintext:l}:{}});const u=Re(n,{daily:ee(t.WYRM_BACKUP_KEEP_DAILY,z),weekly:ee(t.WYRM_BACKUP_KEEP_WEEKLY,H),dryRun:!1});return{action:"created",created:f,newest:f,pruned:u.remove,...l?{plaintext:l}:{}}}async function Ze(e,t){j(t);const n=process.umask(63);try{await B(e,t)}finally{process.umask(n)}const r=S(t);try{try{r.pragma("wal_checkpoint(TRUNCATE)")}catch{}return String(r.pragma("integrity_check",{simple:!0}))}finally{r.close()}}function Ae(e){return`${e.getUTCFullYear()}${k(e.getUTCMonth()+1)}${k(e.getUTCDate())}`}function Le(e){const t=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())),n=t.getUTCDay()||7;t.setUTCDate(t.getUTCDate()+4-n);const r=t.getUTCFullYear(),o=Math.ceil(((t.getTime()-Date.UTC(r,0,1))/864e5+1)/7);return`${r}-W${k(o)}`}function re(e,t){return t||e.label===null||/^(snapshot|legacy)(-\d+)?$/.test(e.label)}function Me(e,t={}){const n=Math.max(0,Math.floor(t.daily??z)),r=Math.max(0,Math.floor(t.weekly??H)),o=t.includeLabelled===!0,c=[...e].sort((u,h)=>h.at.getTime()-u.at.getTime()||h.file.localeCompare(u.file)),s=c.filter(u=>!re(u,o)),a=c.filter(u=>re(u,o)),i=new Set;a[0]&&i.add(a[0].path);const l=new Set,f=new Set;for(const u of a){const h=Ae(u.at);!l.has(h)&&l.size<n&&(l.add(h),i.add(u.path));const K=Le(u.at);!f.has(K)&&f.size<r&&(f.add(K),i.add(u.path))}return{keep:a.filter(u=>i.has(u.path)),remove:a.filter(u=>!i.has(u.path)),pinned:s}}function Re(e,t){const n=Me(J(e),t);if(!t.dryRun)for(const r of n.remove)E(r.path);return n}const Fe={projects:"SELECT COUNT(*) AS n FROM projects",sessions:"SELECT COUNT(*) AS n FROM sessions",quests:"SELECT COUNT(*) AS n FROM quests",ground_truths:"SELECT COUNT(*) AS n FROM ground_truths",memory_artifacts:"SELECT COUNT(*) AS n FROM memory_artifacts",failure_patterns:"SELECT COUNT(*) AS n FROM failure_patterns",data_lake:"SELECT COUNT(*) AS n FROM data_lake",context:"SELECT COUNT(*) AS n FROM context",global_context:"SELECT COUNT(*) AS n FROM global_context",entities:"SELECT COUNT(*) AS n FROM entities",relationships:"SELECT COUNT(*) AS n FROM relationships",decision_edges:"SELECT COUNT(*) AS n FROM decision_edges",skills:"SELECT COUNT(*) AS n FROM skills",events:"SELECT COUNT(*) AS n FROM events",vectors:"SELECT COUNT(*) AS n FROM vectors",tool_call_log:"SELECT COUNT(*) AS n FROM tool_call_log",erasure_log:"SELECT COUNT(*) AS n FROM erasure_log",schema_versions:"SELECT COUNT(*) AS n FROM schema_versions"};function P(e){const t={};for(const[n,r]of Object.entries(Fe))try{t[n]=e.prepare(r).get().n}catch{}return t}function Ne(e){try{return e.prepare("SELECT COALESCE(MAX(version), 0) AS v FROM schema_versions").get().v}catch{return 0}}const oe=we.reduce((e,t)=>Math.max(e,t.version),0);async function ae(e,t,n,r){const o=be(e);if(!o)return r(e,!1);if(!n)throw new Error(`this snapshot is encrypted (${o}-keyed); supply its key`);const c=x(t),s=p(c,`${Y(e)}.${process.pid}.restore`),a=process.umask(63);try{return await _e(e,s,n),await r(s,!0)}finally{E(s),process.umask(a)}}function se(e,t,n){const r=Buffer.alloc(O.length),o=T(t,"r");try{_(o,r,0,r.length,0)}finally{w(o)}if(!r.equals(O))throw new Error("not a SQLite database");const c=S(t,{readonly:!0,fileMustExist:!0});try{return{file:e,encrypted:n,integrity:String(c.pragma("integrity_check",{simple:!0})),schemaVersion:Ne(c),counts:P(c)}}finally{c.close()}}async function et(e,t){return ae(e,t.workDir,t.keyFor,async(n,r)=>se(e,n,r))}function De(e,t){const n=new Set([...Object.keys(e),...Object.keys(t)]);for(const r of n)if(e[r]!==t[r])return!1;return!0}async function tt(e){return ae(e.file,e.workDir,e.keyFor,async(t,n)=>{const r=se(e.file,t,n);if(r.integrity!=="ok")throw new Error(`the snapshot fails integrity_check (${r.integrity}); refusing to restore it`);if(r.schemaVersion>oe)throw new Error(`the snapshot is at schema v${r.schemaVersion}, newer than this Wyrm (v${oe}); upgrade Wyrm first`);let o=null;if(d(e.targetPath)){const l=S(e.targetPath,{readonly:!0,fileMustExist:!0});try{o=P(l)}finally{l.close()}}if(!e.apply)return{inspection:r,current:o,restored:!1,safetySnapshot:null,targetIntegrity:null};j(e.targetPath);let c=null;d(e.targetPath)&&(c=(await xe(e.targetPath,{dir:e.safetyDir,now:e.now})).path),await B(t,e.targetPath);const s=S(e.targetPath);let a="",i={};try{try{s.pragma("wal_checkpoint(TRUNCATE)")}catch{}a=String(s.pragma("integrity_check",{simple:!0})),i=P(s)}finally{s.close()}if(a!=="ok"||!De(i,r.counts))throw new Error(`the restored store failed verification${c?`; the pre-restore snapshot is ${c}`:""}`);try{const l=S(e.targetPath);try{Se(l,e.lineageReason??"restored with wyrm backup restore")}finally{l.close()}}catch{}return{inspection:r,current:o,restored:!0,safetySnapshot:c,targetIntegrity:a}})}function ce(e){try{const t=T(e,"r");try{const n=Buffer.alloc(O.length);return _(t,n,0,n.length,0)===n.length&&n.equals(O)}finally{w(t)}}catch{return!1}}const ie=/-(wal|shm|journal)$/;function Be(e){return["-wal","-shm","-journal"].map(t=>`${e}${t}`).filter(t=>d(t))}function le(e,t=process.platform){return t==="win32"?!1:(e&63)!==0}function Pe(e){const t=V(e.dbPath),n=e.dir??N(e.dbPath),r=Y(e.dbPath),o=[],c=(a,i)=>{const l=v(a),f=l.mode&511;return{path:a,kind:i,bytes:l.size,mode:f,exposed:le(f),sidecars:i==="directory"?[]:Be(a)}};if(d(t))for(const a of A(t)){const i=p(t,a);let l;try{l=v(i)}catch{continue}!l.isFile()||a===r||ie.test(a)||!(/\.bak|\.db$|\.db\.|\.sqlite/i.test(a)||a.startsWith(`${r}.`))||o.push(c(i,ce(i)?"loose-copy":"other-file"))}const s=(a,i)=>{if(!(!d(a)||i>3)){o.push(c(a,"directory"));for(const l of A(a)){if(i===0&&l===Q)continue;const f=p(a,l);let u;try{u=v(f)}catch{continue}if(u.isDirectory()){s(f,i+1);continue}if(!u.isFile()||ie.test(l))continue;const h=i===0&&q.test(l)?"rotation":ce(f)?"unrotated":"other-file";o.push(c(f,h))}}};return s(n,0),{home:t,dir:n,items:o,legacy:o.filter(a=>a.kind==="loose-copy"||a.kind==="unrotated"),exposed:o.filter(a=>a.exposed||a.sidecars.some(i=>le(v(i).mode)))}}function Ke(e){return D(M(e).mtime)}function nt(e){const t=Pe({dbPath:e.dbPath,dir:e.dir}),n={tightened:0,adopted:[],deleted:[],skipped:[]};if(e.tighten)for(const r of t.exposed)try{m(r.path,r.kind==="directory"?448:384);for(const o of r.sidecars)m(o,384);n.tightened++}catch(o){n.skipped.push({path:r.path,why:`chmod failed: ${o.message}`})}if(e.deleteCopies){for(const r of t.legacy)E(r.path),n.deleted.push(r.path);return n}if(e.adopt){const r=x(t.dir);for(const o of t.legacy){const c=`${o.path}-wal`;if(d(c)&&M(c).size>0){n.skipped.push({path:o.path,why:"its -wal file is not empty; open it once with sqlite3 to checkpoint, then adopt"});continue}const s=Ke(o.path);let a=`wyrm-${s}-legacy.db`;for(let l=2;d(p(r,a));l++)a=`wyrm-${s}-legacy-${l}.db`;const i=p(r,a);try{L(o.path,i)}catch{fe(o.path,i),W(o.path)}m(i,384);for(const l of o.sidecars){const f=l.slice(o.path.length);try{L(l,`${i}${f}`),m(`${i}${f}`,384)}catch{}}n.adopted.push({from:o.path,to:i})}}return n}export{q as BACKUP_FILE_RE,oe as BINARY_SCHEMA_VERSION,$e as DEFAULT_BACKUP_INTERVAL_HOURS,z as DEFAULT_KEEP_DAILY,H as DEFAULT_KEEP_WEEKLY,b as VAULT_BACKUP_SECRET,nt as applyLegacyActions,Z as createBackup,N as defaultBackupDir,be as encryptedSource,x as ensurePrivateDir,Ue as existingVaultBackupKey,et as inspectBackup,J as listBackups,Xe as maybeScheduledBackup,le as modeExposed,Te as parseBackupName,Me as planRotation,Re as pruneBackups,Ge as readScheduledStatus,tt as restoreBackup,Ze as restoreFileInto,P as rowCounts,Pe as scanBackupHygiene,Je as scheduledStatusNote,xe as snapshotBeforeRestore,ve as snapshotToFile,Qe as vaultBackupKey};
@@ -1 +1 @@
1
- const I=!1;export{I as OFFICIAL_BUILD};
1
+ const t=!0;export{t as OFFICIAL_BUILD};
@@ -1,5 +1,5 @@
1
- import{createReadStream as N,createWriteStream as H,statSync as W,existsSync as $,unlinkSync as v,readFileSync as G,writeFileSync as q}from"fs";import{snapshotToFile as P,snapshotBeforeRestore as Y,restoreFileInto as F,ensurePrivateDir as C,defaultBackupDir as I}from"./backup.js";import{createGzip as Q,gunzipSync as J}from"zlib";import{pipeline as V}from"stream/promises";import{createCipheriv as X,createDecipheriv as Z,randomBytes as O,scryptSync as x,createHash as K,createHmac as A}from"crypto";import{join as f}from"path";import{hostname as tt,platform as et,arch as st,cpus as nt}from"os";const ot="3.2.0",rt=1,M="aes-256-gcm",j=16,T=32,R=16,B=32,ct=10,L=Buffer.from("SQLite format 3\0","binary");function _(c,t){const r=O(j),e=O(T),n=x(t,e,B),s=X(M,n,r),o=Buffer.concat([s.update(c),s.final()]),a=s.getAuthTag();return Buffer.concat([r,e,a,o])}function D(c,t){const r=j+T+R;if(c.length<r)throw new Error("Encrypted data too short: corrupted or not a Wyrm backup");let e=0;const n=c.subarray(e,e+=j),s=c.subarray(e,e+=T),o=c.subarray(e,e+=R),a=c.subarray(e),i=x(t,s,B),u=Z(M,i,n);u.setAuthTag(o);try{return Buffer.concat([u.update(a),u.final()])}catch{throw new Error("Decryption failed: wrong password or corrupted backup")}}class at{accessKeyId;secretAccessKey;region;endpoint;bucket;host;constructor(t){this.accessKeyId=t.accessKeyId,this.secretAccessKey=t.secretAccessKey,this.region=t.region??"auto",this.endpoint=t.endpoint.replace(/\/$/,""),this.bucket=t.bucket;const r=new URL(this.endpoint);this.host=r.host}async putObject(t,r,e="application/octet-stream"){const n=`/${this.bucket}/${E(t)}`,s={"Content-Type":e,"Content-Length":r.length.toString()},o=this.signRequest("PUT",n,s,r),a=await fetch(`${this.endpoint}${n}`,{method:"PUT",headers:o,body:r});if(!a.ok){const i=await a.text();throw new Error(`S3 PUT failed (${a.status}): ${i}`)}}async getObject(t){const r=`/${this.bucket}/${E(t)}`,e={},n=this.signRequest("GET",r,e),s=await fetch(`${this.endpoint}${r}`,{method:"GET",headers:n});if(!s.ok){const a=await s.text();throw new Error(`S3 GET failed (${s.status}): ${a}`)}const o=await s.arrayBuffer();return Buffer.from(o)}async listObjects(t){const r=[];let e;do{const n=new URLSearchParams({"list-type":"2",prefix:t});e&&n.set("continuation-token",e);const s=`/${this.bucket}?${n.toString()}`,o={},a=this.signRequest("GET",s,o),i=await fetch(`${this.endpoint}${s}`,{method:"GET",headers:a});if(!i.ok){const h=await i.text();throw new Error(`S3 LIST failed (${i.status}): ${h}`)}const u=await i.text();r.push(...mt(u)),u.match(/<IsTruncated>(.*?)<\/IsTruncated>/)?.[1]==="true"?e=u.match(/<NextContinuationToken>(.*?)<\/NextContinuationToken>/)?.[1]:e=void 0}while(e);return r}async deleteObject(t){const r=`/${this.bucket}/${E(t)}`,e={},n=this.signRequest("DELETE",r,e),s=await fetch(`${this.endpoint}${r}`,{method:"DELETE",headers:n});if(!s.ok){const o=await s.text();throw new Error(`S3 DELETE failed (${s.status}): ${o}`)}}signRequest(t,r,e,n){const s=new Date,o=it(s),a=ut(s),[i,u]=pt(r);e.host=this.host,e["x-amz-date"]=a,e["x-amz-content-sha256"]=g(n??Buffer.alloc(0));const d=Object.keys(e).map(w=>w.toLowerCase()).sort(),l=d.map(w=>`${w}:${e[Object.keys(e).find(U=>U.toLowerCase()===w)].trim()}`).join(`
1
+ import{createReadStream as W,createWriteStream as Y,statSync as G,existsSync as E,unlinkSync as q,readFileSync as x,writeFileSync as P,realpathSync as F}from"fs";import{snapshotToFile as Q,snapshotBeforeRestore as J,restoreFileInto as V,ensurePrivateDir as M,defaultBackupDir as K,encryptedSource as X,parseBackupName as g}from"./backup.js";import{createGzip as Z,gunzipSync as tt}from"zlib";import{pipeline as et}from"stream/promises";import{createCipheriv as st,createDecipheriv as nt,randomBytes as R,scryptSync as A,createHash as j,createHmac as B}from"crypto";import{join as m,resolve as ot}from"path";import{hostname as rt,platform as at,arch as ct,cpus as it}from"os";const ut="3.2.0",ht=1,L="aes-256-gcm",T=16,C=32,_=16,z=32,dt=10,D=Buffer.from("SQLite format 3\0","binary");function U(a,t){const n=R(T),e=R(C),o=A(t,e,z),s=st(L,o,n),r=Buffer.concat([s.update(a),s.final()]),c=s.getAuthTag();return Buffer.concat([n,e,c,r])}function N(a,t){const n=T+C+_;if(a.length<n)throw new Error("Encrypted data too short: corrupted or not a Wyrm backup");let e=0;const o=a.subarray(e,e+=T),s=a.subarray(e,e+=C),r=a.subarray(e,e+=_),c=a.subarray(e),i=A(t,s,z),u=nt(L,i,o);u.setAuthTag(r);try{return Buffer.concat([u.update(c),u.final()])}catch{throw new Error("Decryption failed: wrong password or corrupted backup")}}class pt{accessKeyId;secretAccessKey;region;endpoint;bucket;host;constructor(t){this.accessKeyId=t.accessKeyId,this.secretAccessKey=t.secretAccessKey,this.region=t.region??"auto",this.endpoint=t.endpoint.replace(/\/$/,""),this.bucket=t.bucket;const n=new URL(this.endpoint);this.host=n.host}async putObject(t,n,e="application/octet-stream"){const o=`/${this.bucket}/${I(t)}`,s={"Content-Type":e,"Content-Length":n.length.toString()},r=this.signRequest("PUT",o,s,n),c=await fetch(`${this.endpoint}${o}`,{method:"PUT",headers:r,body:n});if(!c.ok){const i=await c.text();throw new Error(`S3 PUT failed (${c.status}): ${i}`)}}async getObject(t){const n=`/${this.bucket}/${I(t)}`,e={},o=this.signRequest("GET",n,e),s=await fetch(`${this.endpoint}${n}`,{method:"GET",headers:o});if(!s.ok){const c=await s.text();throw new Error(`S3 GET failed (${s.status}): ${c}`)}const r=await s.arrayBuffer();return Buffer.from(r)}async listObjects(t){const n=[];let e;do{const o=new URLSearchParams({"list-type":"2",prefix:t});e&&o.set("continuation-token",e);const s=`/${this.bucket}?${o.toString()}`,r={},c=this.signRequest("GET",s,r),i=await fetch(`${this.endpoint}${s}`,{method:"GET",headers:c});if(!i.ok){const d=await i.text();throw new Error(`S3 LIST failed (${i.status}): ${d}`)}const u=await i.text();n.push(...bt(u)),u.match(/<IsTruncated>(.*?)<\/IsTruncated>/)?.[1]==="true"?e=u.match(/<NextContinuationToken>(.*?)<\/NextContinuationToken>/)?.[1]:e=void 0}while(e);return n}async deleteObject(t){const n=`/${this.bucket}/${I(t)}`,e={},o=this.signRequest("DELETE",n,e),s=await fetch(`${this.endpoint}${n}`,{method:"DELETE",headers:o});if(!s.ok){const r=await s.text();throw new Error(`S3 DELETE failed (${s.status}): ${r}`)}}signRequest(t,n,e,o){const s=new Date,r=lt(s),c=ft(s),[i,u]=wt(n);e.host=this.host,e["x-amz-date"]=c,e["x-amz-content-sha256"]=b(o??Buffer.alloc(0));const h=Object.keys(e).map(w=>w.toLowerCase()).sort(),l=h.map(w=>`${w}:${e[Object.keys(e).find(H=>H.toLowerCase()===w)].trim()}`).join(`
2
2
  `)+`
3
- `,h=d.join(";"),k=[t,i,lt(u),l,h,e["x-amz-content-sha256"]].join(`
4
- `),m=`${o}/${this.region}/s3/aws4_request`,S=["AWS4-HMAC-SHA256",a,m,g(Buffer.from(k,"utf-8"))].join(`
5
- `),y=ht(this.secretAccessKey,o,this.region,"s3"),z=dt(y,S);return e.Authorization=`AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${m}, SignedHeaders=${h}, Signature=${z}`,e}}function it(c){return c.toISOString().replace(/[-:T]/g,"").slice(0,8)}function ut(c){return c.toISOString().replace(/[-:]/g,"").replace(/\.\d{3}/,"")}function g(c){return K("sha256").update(c).digest("hex")}function b(c,t){return A("sha256",c).update(t,"utf-8").digest()}function dt(c,t){return A("sha256",c).update(t,"utf-8").digest("hex")}function ht(c,t,r,e){let n=b(`AWS4${c}`,t);return n=b(n,r),n=b(n,e),n=b(n,"aws4_request"),n}function E(c){return c.split("/").map(t=>encodeURIComponent(t)).join("/")}function pt(c){const t=c.indexOf("?");return t===-1?[c,""]:[c.slice(0,t),c.slice(t+1)]}function lt(c){return c?c.split("&").map(t=>{const[r,e]=t.split("=");return`${encodeURIComponent(decodeURIComponent(r))}=${encodeURIComponent(decodeURIComponent(e??""))}`}).sort().join("&"):""}function mt(c){const t=[],r=/<Contents>([\s\S]*?)<\/Contents>/g;let e;for(;(e=r.exec(c))!==null;){const n=e[1],s=n.match(/<Key>(.*?)<\/Key>/)?.[1]??"",o=parseInt(n.match(/<Size>(.*?)<\/Size>/)?.[1]??"0",10),a=n.match(/<LastModified>(.*?)<\/LastModified>/)?.[1]??"";t.push({key:s,size:o,lastModified:a})}return t}class jt{config=null;encryptionKey=null;s3=null;constructor(){}configure(t,r){if(!t.endpoint||!t.bucket||!t.accessKeyId||!t.secretAccessKey)throw new Error("Cloud config requires endpoint, bucket, accessKeyId, and secretAccessKey");this.config={...t,region:t.region??"auto"},this.encryptionKey=r??null,this.s3=new at(this.config)}isConfigured(){return this.config!==null&&this.s3!==null}async backup(t){if(this.assertConfigured(),!$(t))throw new Error(`Database not found: ${t}`);const r=W(t),e=this.getMachineId(),n=new Date,s=ft(n),o=`backups/${e}/${s}.wyrm.bak`,a=`backups/${e}/${s}.wyrm.meta.json`;if(this.encryptionKey===null&&process.env.WYRM_ALLOW_PLAINTEXT_BACKUP!=="1")throw new Error("Refusing to upload an unencrypted full-DB backup to cloud storage. Set WYRM_ENCRYPTION_KEY to encrypt the snapshot, or WYRM_ALLOW_PLAINTEXT_BACKUP=1 to override (NOT recommended: uploads all private rows in cleartext).");const i=C(f(I(t),".work")),u=`${Date.now()}-${process.pid}`,d=f(i,`.wyrm-cloud-${u}.db`),l=f(i,`.wyrm-cloud-${u}.gz`);try{await P(t,d),await V(N(d),Q({level:9}),H(l,{mode:384}));let h=Buffer.from(G(l));const k=h.length,m=this.encryptionKey!==null;m&&(h=_(h,this.encryptionKey));const S=g(h);await this.s3.putObject(o,h,"application/x-wyrm-backup");const y={version:rt,wyrm_version:ot,timestamp:n.toISOString(),db_size:r.size,compressed_size:k,encrypted:m,machine_id:e,checksum:S};return await this.s3.putObject(a,Buffer.from(JSON.stringify(y,null,2),"utf-8"),"application/json"),{key:o,metadata:y}}finally{p(d),p(l),p(`${d}-wal`),p(`${d}-shm`),p(`${d}-journal`)}}async listBackups(){this.assertConfigured();const r=`backups/${this.getMachineId()}/`,n=(await this.s3.listObjects(r)).filter(o=>o.key.endsWith(".wyrm.meta.json")),s=[];for(const o of n)try{const a=await this.s3.getObject(o.key),i=JSON.parse(a.toString("utf-8"));s.push(i)}catch{}return s.sort((o,a)=>new Date(a.timestamp).getTime()-new Date(o.timestamp).getTime()),s}async listBackupsWithKeys(){this.assertConfigured();const r=`backups/${this.getMachineId()}/`,n=(await this.s3.listObjects(r)).filter(o=>o.key.endsWith(".wyrm.meta.json")),s=[];for(const o of n)try{const a=await this.s3.getObject(o.key),i=JSON.parse(a.toString("utf-8")),u=o.key.replace(/\.wyrm\.meta\.json$/,".wyrm.bak");s.push({key:u,metadata:i})}catch{}return s.sort((o,a)=>new Date(a.metadata.timestamp).getTime()-new Date(o.metadata.timestamp).getTime()),s}async restore(t,r){this.assertConfigured();const e=t.replace(/\.wyrm\.bak$/,".wyrm.meta.json");let n;try{const d=await this.s3.getObject(e);n=JSON.parse(d.toString("utf-8"))}catch{throw new Error(`Backup metadata not found for key: ${t}`)}let s=await this.s3.getObject(t);const o=g(s);if(o!==n.checksum)throw new Error(`Checksum mismatch: backup may be corrupted. Expected: ${n.checksum}, Got: ${o}`);if(n.encrypted){if(!this.encryptionKey)throw new Error("Backup is encrypted but no encryption key is configured");s=D(s,this.encryptionKey)}const a=J(s);if(!a.subarray(0,L.length).equals(L))throw new Error("Downloaded backup is not a SQLite database; nothing was changed");const i=C(f(I(r),".work")),u=f(i,`.wyrm-cloud-restore-${Date.now()}-${process.pid}.db`);try{q(u,a,{mode:384}),$(r)&&await Y(r);const d=await F(u,r);if(d!=="ok")throw new Error(`Restored store failed integrity_check (${d}); the previous store is kept as a pre-restore snapshot`)}finally{p(u),p(`${u}-wal`),p(`${u}-shm`),p(`${u}-journal`)}return{restored:!0,size:a.length}}async pruneBackups(t=ct){this.assertConfigured();const e=`backups/${this.getMachineId()}/`,s=(await this.s3.listObjects(e)).filter(i=>i.key.endsWith(".wyrm.bak")).sort((i,u)=>u.lastModified.localeCompare(i.lastModified));if(s.length<=t)return{deleted:0};const o=s.slice(t);let a=0;for(const i of o){const u=i.key.replace(/\.wyrm\.bak$/,".wyrm.meta.json");try{await this.s3.deleteObject(i.key),a++}catch{}try{await this.s3.deleteObject(u)}catch{}}return{deleted:a}}machineId(){return this.getMachineId()}async listSnapshotPrefixes(){this.assertConfigured();const t=this.getMachineId(),r=new Map;for(const e of await this.s3.listObjects("backups/")){const n=/^backups\/([^/]+)\//.exec(e.key);if(!n)continue;const s=n[1],o=r.get(s)??{prefix:s,current:s===t,objects:0,snapshots:0,bytes:0,newest:null};o.objects++,o.bytes+=e.size,e.key.endsWith(".wyrm.bak")&&o.snapshots++,(!o.newest||e.lastModified>o.newest)&&(o.newest=e.lastModified),r.set(s,o)}return[...r.values()].sort((e,n)=>Number(n.current)-Number(e.current)||(n.newest??"").localeCompare(e.newest??""))}async pruneSnapshotPrefix(t,r={}){if(this.assertConfigured(),!/^[A-Za-z0-9_-]{1,64}$/.test(t))throw new Error("invalid snapshot prefix");if(t===this.getMachineId())throw new Error("refusing to prune this machine's own snapshot prefix; rotation (pruneBackups) manages it");const e=await this.s3.listObjects(`backups/${t}/`),n=e.reduce((a,i)=>a+i.size,0);let s=0,o=0;if(r.dryRun!==!1)return{prefix:t,objects:e.length,bytes:n,deleted:s,failed:o};for(const a of e)try{await this.s3.deleteObject(a.key),s++}catch{o++}return{prefix:t,objects:e.length,bytes:n,deleted:s,failed:o}}getMachineId(){const t=[tt(),et(),st(),nt()[0]?.model??"unknown-cpu"].join("|");return K("sha256").update(t).digest("hex").slice(0,16)}assertConfigured(){if(!this.isConfigured())throw new Error("Cloud backup not configured. Call configure() with R2 credentials first.")}}function ft(c){const t=c.getUTCFullYear(),r=String(c.getUTCMonth()+1).padStart(2,"0"),e=String(c.getUTCDate()).padStart(2,"0"),n=String(c.getUTCHours()).padStart(2,"0"),s=String(c.getUTCMinutes()).padStart(2,"0"),o=String(c.getUTCSeconds()).padStart(2,"0");return`${t}-${r}-${e}_${n}${s}${o}`}function p(c){try{$(c)&&v(c)}catch{}}export{jt as WyrmCloudBackup,D as decryptBuffer,_ as encryptBuffer};
3
+ `,d=h.join(";"),$=[t,i,gt(u),l,d,e["x-amz-content-sha256"]].join(`
4
+ `),f=`${r}/${this.region}/s3/aws4_request`,S=["AWS4-HMAC-SHA256",c,f,b(Buffer.from($,"utf-8"))].join(`
5
+ `),y=yt(this.secretAccessKey,r,this.region,"s3"),v=mt(y,S);return e.Authorization=`AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${f}, SignedHeaders=${d}, Signature=${v}`,e}}function lt(a){return a.toISOString().replace(/[-:T]/g,"").slice(0,8)}function ft(a){return a.toISOString().replace(/[-:]/g,"").replace(/\.\d{3}/,"")}function b(a){return j("sha256").update(a).digest("hex")}function k(a,t){return B("sha256",a).update(t,"utf-8").digest()}function mt(a,t){return B("sha256",a).update(t,"utf-8").digest("hex")}function yt(a,t,n,e){let o=k(`AWS4${a}`,t);return o=k(o,n),o=k(o,e),o=k(o,"aws4_request"),o}function I(a){return a.split("/").map(t=>encodeURIComponent(t)).join("/")}function wt(a){const t=a.indexOf("?");return t===-1?[a,""]:[a.slice(0,t),a.slice(t+1)]}function gt(a){return a?a.split("&").map(t=>{const[n,e]=t.split("=");return`${encodeURIComponent(decodeURIComponent(n))}=${encodeURIComponent(decodeURIComponent(e??""))}`}).sort().join("&"):""}function bt(a){const t=[],n=/<Contents>([\s\S]*?)<\/Contents>/g;let e;for(;(e=n.exec(a))!==null;){const o=e[1],s=o.match(/<Key>(.*?)<\/Key>/)?.[1]??"",r=parseInt(o.match(/<Size>(.*?)<\/Size>/)?.[1]??"0",10),c=o.match(/<LastModified>(.*?)<\/LastModified>/)?.[1]??"";t.push({key:s,size:r,lastModified:c})}return t}function Ot(a){let t=ot(a);try{t=F(t)}catch{}return j("sha256").update(t).digest("hex").slice(0,16)}function O(a){if(!/^[0-9a-f]{16}$/.test(a))throw new Error("invalid store snapshot folder id (expected 16 hex characters)")}class xt{config=null;encryptionKey=null;s3=null;constructor(){}configure(t,n){if(!t.endpoint||!t.bucket||!t.accessKeyId||!t.secretAccessKey)throw new Error("Cloud config requires endpoint, bucket, accessKeyId, and secretAccessKey");this.config={...t,region:t.region??"auto"},this.encryptionKey=n??null,this.s3=new pt(this.config)}isConfigured(){return this.config!==null&&this.s3!==null}async backup(t){if(this.assertConfigured(),!E(t))throw new Error(`Database not found: ${t}`);const n=G(t),e=this.getMachineId(),o=new Date,s=kt(o),r=`backups/${e}/${s}.wyrm.bak`,c=`backups/${e}/${s}.wyrm.meta.json`;if(this.encryptionKey===null&&process.env.WYRM_ALLOW_PLAINTEXT_BACKUP!=="1")throw new Error("Refusing to upload an unencrypted full-DB backup to cloud storage. Set WYRM_ENCRYPTION_KEY to encrypt the snapshot, or WYRM_ALLOW_PLAINTEXT_BACKUP=1 to override (NOT recommended: uploads all private rows in cleartext).");const i=M(m(K(t),".work")),u=`${Date.now()}-${process.pid}`,h=m(i,`.wyrm-cloud-${u}.db`),l=m(i,`.wyrm-cloud-${u}.gz`);try{await Q(t,h),await et(W(h),Z({level:9}),Y(l,{mode:384}));let d=Buffer.from(x(l));const $=d.length,f=this.encryptionKey!==null;f&&(d=U(d,this.encryptionKey));const S=b(d);await this.s3.putObject(r,d,"application/x-wyrm-backup");const y={version:ht,wyrm_version:ut,timestamp:o.toISOString(),db_size:n.size,compressed_size:$,encrypted:f,machine_id:e,checksum:S};return await this.s3.putObject(c,Buffer.from(JSON.stringify(y,null,2),"utf-8"),"application/json"),{key:r,metadata:y}}finally{p(h),p(l),p(`${h}-wal`),p(`${h}-shm`),p(`${h}-journal`)}}async listBackups(){this.assertConfigured();const n=`backups/${this.getMachineId()}/`,o=(await this.s3.listObjects(n)).filter(r=>r.key.endsWith(".wyrm.meta.json")),s=[];for(const r of o)try{const c=await this.s3.getObject(r.key),i=JSON.parse(c.toString("utf-8"));s.push(i)}catch{}return s.sort((r,c)=>new Date(c.timestamp).getTime()-new Date(r.timestamp).getTime()),s}async listBackupsWithKeys(){this.assertConfigured();const n=`backups/${this.getMachineId()}/`,o=(await this.s3.listObjects(n)).filter(r=>r.key.endsWith(".wyrm.meta.json")),s=[];for(const r of o)try{const c=await this.s3.getObject(r.key),i=JSON.parse(c.toString("utf-8")),u=r.key.replace(/\.wyrm\.meta\.json$/,".wyrm.bak");s.push({key:u,metadata:i})}catch{}return s.sort((r,c)=>new Date(c.metadata.timestamp).getTime()-new Date(r.metadata.timestamp).getTime()),s}async restore(t,n){this.assertConfigured();const e=t.replace(/\.wyrm\.bak$/,".wyrm.meta.json");let o;try{const h=await this.s3.getObject(e);o=JSON.parse(h.toString("utf-8"))}catch{throw new Error(`Backup metadata not found for key: ${t}`)}let s=await this.s3.getObject(t);const r=b(s);if(r!==o.checksum)throw new Error(`Checksum mismatch: backup may be corrupted. Expected: ${o.checksum}, Got: ${r}`);if(!this.encryptionKey)throw new Error("Refusing to restore: cloud restores must be encrypted, and no encryption key (WYRM_ENCRYPTION_KEY) is configured");if(o.encrypted!==!0)throw new Error("Refusing to restore an unencrypted cloud snapshot: its metadata is not authenticated, so anyone who can write to the bucket could have planted it. Nothing was changed.");s=N(s,this.encryptionKey);const c=tt(s);if(!c.subarray(0,D.length).equals(D))throw new Error("Downloaded backup is not a SQLite database; nothing was changed");const i=M(m(K(n),".work")),u=m(i,`.wyrm-cloud-restore-${Date.now()}-${process.pid}.db`);try{P(u,c,{mode:384}),E(n)&&await J(n);const h=await V(u,n);if(h!=="ok")throw new Error(`Restored store failed integrity_check (${h}); the previous store is kept as a pre-restore snapshot`)}finally{p(u),p(`${u}-wal`),p(`${u}-shm`),p(`${u}-journal`)}return{restored:!0,size:c.length}}async pruneBackups(t=dt){this.assertConfigured();const e=`backups/${this.getMachineId()}/`,s=(await this.s3.listObjects(e)).filter(i=>i.key.endsWith(".wyrm.bak")).sort((i,u)=>u.lastModified.localeCompare(i.lastModified));if(s.length<=t)return{deleted:0};const r=s.slice(t);let c=0;for(const i of r){const u=i.key.replace(/\.wyrm\.bak$/,".wyrm.meta.json");try{await this.s3.deleteObject(i.key),c++}catch{}try{await this.s3.deleteObject(u)}catch{}}return{deleted:c}}async listUploadedSnapshots(t){this.assertConfigured(),O(t);const n=`backups/${this.getMachineId()}/`,e=`${t}/`,o=[];for(const s of await this.s3.listObjects(n)){const r=s.key.slice(n.length);if(r.startsWith(e)){const c=r.slice(e.length);!c.includes("/")&&g(c)&&o.push({file:c,size:s.size})}else!r.includes("/")&&g(r)&&o.push({file:r,size:s.size,legacy:!0})}return o}async uploadSnapshotFile(t,n,e){this.assertConfigured(),O(e);const o=g(n);if(!o||!o.encrypted)throw new Error(`${n} is not an encrypted snapshot name (wyrm-YYYYMMDD-HHMMSS.db.enc)`);if(X(t)===null)throw new Error(`${t} is not an encrypted Wyrm snapshot; plaintext snapshots are never uploaded`);const s=`backups/${this.getMachineId()}/${e}/${n}`;return await this.s3.putObject(s,x(t),"application/x-wyrm-snapshot"),s}async deleteUploadedSnapshot(t,n){if(this.assertConfigured(),O(n),!g(t))throw new Error(`${t} is not a rotation snapshot name`);await this.s3.deleteObject(`backups/${this.getMachineId()}/${n}/${t}`)}machineId(){return this.getMachineId()}async listSnapshotPrefixes(){this.assertConfigured();const t=this.getMachineId(),n=new Map;for(const e of await this.s3.listObjects("backups/")){const o=/^backups\/([^/]+)\//.exec(e.key);if(!o)continue;const s=o[1],r=n.get(s)??{prefix:s,current:s===t,objects:0,snapshots:0,bytes:0,newest:null};r.objects++,r.bytes+=e.size,(e.key.endsWith(".wyrm.bak")||e.key.endsWith(".db.enc"))&&r.snapshots++,(!r.newest||e.lastModified>r.newest)&&(r.newest=e.lastModified),n.set(s,r)}return[...n.values()].sort((e,o)=>Number(o.current)-Number(e.current)||(o.newest??"").localeCompare(e.newest??""))}async pruneSnapshotPrefix(t,n={}){if(this.assertConfigured(),!/^[A-Za-z0-9_-]{1,64}$/.test(t))throw new Error("invalid snapshot prefix");if(t===this.getMachineId())throw new Error("refusing to prune this machine's own snapshot prefix; rotation (pruneBackups) manages it");const e=await this.s3.listObjects(`backups/${t}/`),o=e.reduce((c,i)=>c+i.size,0);let s=0,r=0;if(n.dryRun!==!1)return{prefix:t,objects:e.length,bytes:o,deleted:s,failed:r};for(const c of e)try{await this.s3.deleteObject(c.key),s++}catch{r++}return{prefix:t,objects:e.length,bytes:o,deleted:s,failed:r}}getMachineId(){const t=[rt(),at(),ct(),it()[0]?.model??"unknown-cpu"].join("|");return j("sha256").update(t).digest("hex").slice(0,16)}assertConfigured(){if(!this.isConfigured())throw new Error("Cloud backup not configured. Call configure() with R2 credentials first.")}}function kt(a){const t=a.getUTCFullYear(),n=String(a.getUTCMonth()+1).padStart(2,"0"),e=String(a.getUTCDate()).padStart(2,"0"),o=String(a.getUTCHours()).padStart(2,"0"),s=String(a.getUTCMinutes()).padStart(2,"0"),r=String(a.getUTCSeconds()).padStart(2,"0");return`${t}-${n}-${e}_${o}${s}${r}`}function p(a){try{E(a)&&q(a)}catch{}}export{xt as WyrmCloudBackup,N as decryptBuffer,U as encryptBuffer,Ot as storeSnapshotId};
@@ -1 +1 @@
1
- import{WyrmCloudBackup as r}from"./cloud-backup.js";import{CloudSyncDaemon as t,DEFAULT_INTERVAL_MS as _,DEFAULT_KEEP_COUNT as p,DEFAULT_DB_PATH as R}from"./cloud-sync.js";async function E(){const e=Number(process.argv[2])||_,s=Number(process.argv[3])||p,o=new r;process.env.WYRM_R2_ENDPOINT&&process.env.WYRM_R2_ACCESS_KEY&&process.env.WYRM_R2_SECRET_KEY?o.configure({endpoint:process.env.WYRM_R2_ENDPOINT,bucket:process.env.WYRM_R2_BUCKET||"wyrm-backups",accessKeyId:process.env.WYRM_R2_ACCESS_KEY,secretAccessKey:process.env.WYRM_R2_SECRET_KEY},process.env.WYRM_ENCRYPTION_KEY):(console.error("[cloud-sync-entrypoint] WYRM_R2_* env vars unset; refusing to start"),process.exit(2));const n=process.env.WYRM_DB_PATH||R,c=new t(o,n,e,s);console.log(`[cloud-sync] started \xB7 interval=${e}ms \xB7 keep=${s} \xB7 db=${n}`),await c.run()}E().catch(e=>{console.error("[cloud-sync-entrypoint] fatal:",e),process.exit(1)});
1
+ import{WyrmCloudBackup as r}from"./cloud-backup.js";import{CloudSyncDaemon as _,DEFAULT_INTERVAL_MS as t,DEFAULT_KEEP_COUNT as p,DEFAULT_DB_PATH as R}from"./cloud-sync.js";async function E(){const e=Number(process.argv[2])||t,s=Number(process.argv[3])||p,o=new r;process.env.WYRM_R2_ENDPOINT&&process.env.WYRM_R2_ACCESS_KEY&&process.env.WYRM_R2_SECRET_KEY?o.configure({endpoint:process.env.WYRM_R2_ENDPOINT,bucket:process.env.WYRM_R2_BUCKET||"wyrm-backups",accessKeyId:process.env.WYRM_R2_ACCESS_KEY,secretAccessKey:process.env.WYRM_R2_SECRET_KEY},process.env.WYRM_ENCRYPTION_KEY):(console.error("[cloud-sync-entrypoint] WYRM_R2_* env vars unset; refusing to start"),process.exit(2));const n=process.env.WYRM_DB_PATH||process.env.WYRM_DB||R,c=new _(o,n,e,s);console.log(`[cloud-sync] started \xB7 interval=${e}ms \xB7 keep=${s} \xB7 db=${n}`),await c.run()}E().catch(e=>{console.error("[cloud-sync-entrypoint] fatal:",e),process.exit(1)});
@@ -1,2 +1,2 @@
1
- import{spawn as D}from"child_process";import{existsSync as a,readFileSync as p,writeFileSync as k,statSync as g,mkdirSync as N,openSync as E,closeSync as b,unlinkSync as v,renameSync as A}from"fs";import{createHash as L}from"crypto";import{homedir as w,hostname as I}from"os";import{join as u}from"path";const d=u(w(),".wyrm"),h=u(d,"wyrm-cloud-sync.pid"),o=u(d,"wyrm-cloud-sync.log"),f=u(d,"wyrm-cloud-sync.state"),P=u(w(),".copilot","session-store.db"),y=600*1e3,S=20,O=6e4,C=1e6,F=0;class Y{cloud;dbPath;intervalMs;keepCount;constructor(t,e=P,n=y,r=S){this.cloud=t,this.dbPath=e,this.intervalMs=n,this.keepCount=r,_()}async bootstrap(){if(!this.cloud.isConfigured())return{action:"no-config",reason:"WYRM_R2_* env vars unset"};let t;try{t=await this.cloud.listBackupsWithKeys()}catch(i){return{action:"no-remote",reason:i.message??String(i)}}if(t.length===0)return{action:"no-op",reason:"no remote snapshots yet"};const e=t[0],n=Date.parse(e.metadata.timestamp),r=a(this.dbPath)?g(this.dbPath).mtimeMs:0;return n>r+O?(await this.cloud.restore(e.key,this.dbPath),this.patchState({last_restored_key:e.key}),{action:"restored",key:e.key}):{action:"no-op",reason:"local DB is current"}}async tick(){if(!this.cloud.isConfigured())return{action:"error",reason:"WYRM_R2_* env vars unset"};if(!a(this.dbPath))return{action:"no-db"};const t=T(this.dbPath),e=this.readState();if(this.patchState({last_check_ts:Date.now()}),t===e.last_uploaded_hash)return{action:"unchanged",hash:t};try{const{key:n}=await this.cloud.backup(this.dbPath);return this.patchState({last_uploaded_hash:t,last_uploaded_ts:Date.now(),last_uploaded_key:n}),await this.cloud.pruneBackups(this.keepCount),{action:"uploaded",key:n,hash:t}}catch(n){return{action:"error",reason:n.message??String(n)}}}async run(){const t=await this.bootstrap();c(`bootstrap: ${JSON.stringify(t)}`);const e=setInterval(async()=>{try{const r=await this.tick();r.action!=="unchanged"&&c(`tick: ${JSON.stringify(r)}`)}catch(r){c(`tick error: ${r.message??r}`)}},this.intervalMs),n=async r=>{clearInterval(e),c(`${r}: final tick before exit`);try{const i=await this.tick();c(`final tick: ${JSON.stringify(i)}`)}catch(i){c(`final tick error: ${i.message??i}`)}process.exit(0)};process.on("SIGTERM",()=>{n("SIGTERM")}),process.on("SIGINT",()=>{n("SIGINT")})}readState(){return m()}patchState(t){x(t)}}class z{daemonEntrypoint;constructor(t){this.daemonEntrypoint=t,_()}start(t={}){if(this.isAlive())return{ok:!1,pid:this.readPid(),reason:"already running"};$();const e=(t.interval_minutes??y/6e4)*6e4,n=t.keep_count??S,r=E(o,"a"),i=E(o,"a"),l=D(process.execPath,[this.daemonEntrypoint,String(e),String(n)],{detached:!0,stdio:["ignore",r,i],env:{...process.env}});return l.unref(),b(r),b(i),typeof l.pid!="number"?{ok:!1,reason:"spawn returned no pid"}:(k(h,String(l.pid)),{ok:!0,pid:l.pid})}stop(){if(!this.isAlive())return this.cleanPid(),{ok:!1,reason:"not running"};const t=this.readPid();if(!t)return{ok:!1,reason:"pid file unreadable"};try{return process.kill(t,"SIGTERM"),{ok:!0}}catch(e){return{ok:!1,reason:e.message??String(e)}}}status(){const t=this.isAlive();return{running:t,pid:t?this.readPid()??void 0:void 0,state:a(f)?R():void 0}}async forceSync(t){return t.tick()}isAlive(){if(!a(h))return!1;const t=this.readPid();if(!t)return!1;try{return process.kill(t,F),!0}catch{return!1}}readPid(){try{const t=p(h,"utf8").trim(),e=Number(t);return Number.isFinite(e)&&e>0?e:void 0}catch{return}}cleanPid(){try{v(h)}catch{}}}function _(){a(d)||N(d,{recursive:!0})}function T(s){return L("sha256").update(p(s)).digest("hex")}function M(){return{last_uploaded_hash:"",last_uploaded_ts:0,last_uploaded_key:null,last_restored_key:null,last_check_ts:0,machine:I()}}function m(){return R()??M()}function R(){try{const s=JSON.parse(p(f,"utf8"));return{...M(),...s}}catch{return}}function x(s){const t=m();k(f,JSON.stringify({...t,...s,machine:I()},null,2))}function c(s){const t=`[${new Date().toISOString()}] ${s}
2
- `;process.stdout.write(t)}function $(){if(a(o))try{if(g(o).size>C){const t=`${o}.1`;try{v(t)}catch{}A(o,t)}}catch{}}export{Y as CloudSyncDaemon,z as CloudSyncManager,P as DEFAULT_DB_PATH,y as DEFAULT_INTERVAL_MS,S as DEFAULT_KEEP_COUNT,_ as ensureWyrmDir,m as readState,T as sha256};
1
+ import{spawn as v}from"child_process";import{existsSync as i,readFileSync as u,writeFileSync as E,statSync as f,mkdirSync as C,openSync as g,closeSync as I,unlinkSync as T,renameSync as F}from"fs";import{createHash as w}from"crypto";import{homedir as L,hostname as M}from"os";import{join as d}from"path";import{openDatabase as P}from"./storage/index.js";const l=d(L(),".wyrm"),p=d(l,"wyrm-cloud-sync.pid"),o=d(l,"wyrm-cloud-sync.log"),y=d(l,"wyrm-cloud-sync.state"),R=d(L(),".wyrm","wyrm.db"),S=600*1e3,_=20,x=1e6,A=0;class j{cloud;dbPath;intervalMs;keepCount;constructor(t,e=R,r=S,s=_){this.cloud=t,this.dbPath=e,this.intervalMs=r,this.keepCount=s,m()}async bootstrap(){if(!this.cloud.isConfigured())return{action:"no-config",reason:"WYRM_R2_* env vars unset"};let t;try{t=await this.cloud.listBackupsWithKeys()}catch(r){return{action:"no-remote",reason:r.message??String(r)}}if(t.length===0)return{action:"no-op",reason:"no remote snapshots yet"};const e=t[0];return D(this.dbPath)?(await this.cloud.restore(e.key,this.dbPath),this.patchState({last_restored_key:e.key}),{action:"restored",key:e.key}):{action:"no-op",reason:"local store has data; a cloud snapshot is restored automatically only onto a missing or empty store"}}async tick(){if(!this.cloud.isConfigured())return{action:"error",reason:"WYRM_R2_* env vars unset"};if(!i(this.dbPath))return{action:"no-db"};const t=$(this.dbPath),e=this.readState();if(this.patchState({last_check_ts:Date.now()}),t===e.last_uploaded_hash)return{action:"unchanged",hash:t};try{const{key:r}=await this.cloud.backup(this.dbPath);return this.patchState({last_uploaded_hash:t,last_uploaded_ts:Date.now(),last_uploaded_key:r}),await this.cloud.pruneBackups(this.keepCount),{action:"uploaded",key:r,hash:t}}catch(r){return{action:"error",reason:r.message??String(r)}}}async run(){const t=await this.bootstrap();c(`bootstrap: ${JSON.stringify(t)}`);const e=setInterval(async()=>{try{const s=await this.tick();s.action!=="unchanged"&&c(`tick: ${JSON.stringify(s)}`)}catch(s){c(`tick error: ${s.message??s}`)}},this.intervalMs),r=async s=>{clearInterval(e),c(`${s}: final tick before exit`);try{const a=await this.tick();c(`final tick: ${JSON.stringify(a)}`)}catch(a){c(`final tick error: ${a.message??a}`)}process.exit(0)};process.on("SIGTERM",()=>{r("SIGTERM")}),process.on("SIGINT",()=>{r("SIGINT")})}readState(){return k()}patchState(t){W(t)}}class K{daemonEntrypoint;constructor(t){this.daemonEntrypoint=t,m()}start(t={}){if(this.isAlive())return{ok:!1,pid:this.readPid(),reason:"already running"};z();const e=(t.interval_minutes??S/6e4)*6e4,r=t.keep_count??_,s=g(o,"a"),a=g(o,"a"),h=v(process.execPath,[this.daemonEntrypoint,String(e),String(r)],{detached:!0,stdio:["ignore",s,a],env:{...process.env}});return h.unref(),I(s),I(a),typeof h.pid!="number"?{ok:!1,reason:"spawn returned no pid"}:(E(p,String(h.pid)),{ok:!0,pid:h.pid})}stop(){if(!this.isAlive())return this.cleanPid(),{ok:!1,reason:"not running"};const t=this.readPid();if(!t)return{ok:!1,reason:"pid file unreadable"};try{return process.kill(t,"SIGTERM"),{ok:!0}}catch(e){return{ok:!1,reason:e.message??String(e)}}}status(){const t=this.isAlive();return{running:t,pid:t?this.readPid()??void 0:void 0,state:i(y)?O():void 0}}async forceSync(t){return t.tick()}isAlive(){if(!i(p))return!1;const t=this.readPid();if(!t)return!1;try{return process.kill(t,A),!0}catch{return!1}}readPid(){try{const t=u(p,"utf8").trim(),e=Number(t);return Number.isFinite(e)&&e>0?e:void 0}catch{return}}cleanPid(){try{T(p)}catch{}}}function m(){i(l)||C(l,{recursive:!0})}const N={memory_artifacts:"SELECT 1 FROM memory_artifacts LIMIT 1",ground_truths:"SELECT 1 FROM ground_truths LIMIT 1",sessions:"SELECT 1 FROM sessions LIMIT 1",quests:"SELECT 1 FROM quests LIMIT 1",failure_patterns:"SELECT 1 FROM failure_patterns LIMIT 1",data_lake:"SELECT 1 FROM data_lake LIMIT 1",decision_edges:"SELECT 1 FROM decision_edges LIMIT 1"};function D(n){if(!i(n))return!0;const t=`${n}-wal`;if(f(n).size===0&&(!i(t)||f(t).size===0))return!0;let e;try{e=P(n,{readonly:!0,fileMustExist:!0})}catch{return!1}try{const r=new Set(e.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map(s=>s.name));for(const[s,a]of Object.entries(N))if(r.has(s)&&e.prepare(a).get()!==void 0)return!1;return!0}catch{return!1}finally{e.close()}}function $(n){const t=w("sha256").update(u(n)),e=`${n}-wal`;return i(e)&&t.update("\0wal\0").update(u(e)),t.digest("hex")}function G(n){return w("sha256").update(u(n)).digest("hex")}function b(){return{last_uploaded_hash:"",last_uploaded_ts:0,last_uploaded_key:null,last_restored_key:null,last_check_ts:0,machine:M()}}function k(){return O()??b()}function O(){try{const n=JSON.parse(u(y,"utf8"));return{...b(),...n}}catch{return}}function W(n){const t=k();E(y,JSON.stringify({...t,...n,machine:M()},null,2))}function c(n){const t=`[${new Date().toISOString()}] ${n}
2
+ `;process.stdout.write(t)}function z(){if(i(o))try{if(f(o).size>x){const t=`${o}.1`;try{T(t)}catch{}F(o,t)}}catch{}}export{j as CloudSyncDaemon,K as CloudSyncManager,R as DEFAULT_DB_PATH,S as DEFAULT_INTERVAL_MS,_ as DEFAULT_KEEP_COUNT,$ as dataFingerprint,m as ensureWyrmDir,D as localStoreIsEmpty,k as readState,G as sha256};
@@ -1,4 +1,4 @@
1
- import{applyBudget as I,resolveBudget as S}from"./token-budget.js";import{autoSummaryPenalty as j,score as b}from"./context-ranking.js";function E(o,m){const d=j(o.artifact.problem);return b({confidence:o.artifact.confidence,relevance:d<1?o.relevance_score/(1-d):o.relevance_score,updatedAt:o.artifact.updated_at,sourcePenalty:d},m)}function M(o){const{groundTruths:m,scaffoldLib:d,memory:$,toolAnalytics:x,sessionSeen:f,tokenEstimator:l,rankWeights:_,threadText:B}=o;return function(t){const r=S({callArg:t.maxTokens,envValue:process.env.WYRM_CONTEXT_TOKEN_BUDGET,clientName:null}),C=t.sessionId?f().getSeen(t.sessionId):new Set,c=(()=>{try{return B(t.project.id)}catch{return""}})(),w=c?l.count(c):0,k=m.formatForContext(t.project.id),u=l.count(k??"");let p=k??"",y=u;u>r*.5&&!t.strictBudget&&process.stderr.write(`[wyrm_context_build] ground truths consume ${u} of ${r}-token budget; body will be heavily elided. Pass strict_budget:true to elide truths too.
2
- `),t.strictBudget&&u>r*.5&&(p=(k??"").slice(0,r*2),y=l.count(p));const a=[],h=d.findBest(t.task,t.project.id);if(h){const n=d.formatForContext(h),i=h.scaffold.id;a.push({item:{kind:"scaffold",id:i,title:h.scaffold.problem_type,body:n},key:`scaffold:${i}`,inlineCost:l.count(n),stubCost:20,score:b({confidence:.9,relevance:.9,updatedAt:new Date().toISOString()},_)})}const T=$.recall(t.project.id,t.task,{limit:20,minConfidence:t.minConfidence??.2,runId:t.runId??null});for(const n of T){const i=n.artifact;if(t.kinds&&t.kinds.length>0&&!t.kinds.includes(i.kind))continue;const g=[`**${i.kind.toUpperCase()}** \xB7 ${i.problem}`,i.constraints?`Constraints: ${i.constraints}`:"",i.validated_fix?`Fix: ${i.validated_fix}`:"",i.why_it_worked?`Why: ${i.why_it_worked}`:""].filter(Boolean).join(`
3
- `);a.push({item:{kind:"memory",id:i.id,title:i.problem.slice(0,60),body:g},key:`memory:${i.id}`,inlineCost:l.count(g),stubCost:25,score:E(n,_)})}const s=I(a,{budget:r,alreadySeen:C,reserved:(t.strictBudget?0:y)+w});t.sessionId&&(f().markBulk(t.sessionId,s.inline.map(n=>({id:n.id,kind:n.kind,mode:"inline"}))),f().markBulk(t.sessionId,s.elided.map(n=>({id:n.item.id,kind:n.item.kind,mode:"stub"}))));const e=[];e.push(`\u{F115D} **Context Brief**: "${t.task}"`),e.push(""),c&&(e.push("## Working thread"),e.push(""),e.push(c),e.push("")),e.push("## Ground truths (stable preamble)"),e.push(""),e.push(p||"_no ground truths set for this project yet_"),e.push(""),e.push("## Task-relevant memory"),e.push("");for(const n of s.inline)e.push(`### [${n.kind}:${n.id}] ${n.title}`),e.push(n.body),e.push("");if(s.elided.length>0){e.push("## Elided to stubs"),e.push(""),e.push("_Below the budget cutoff or already seen this session. Recall with `wyrm_recall(id)`:_"),e.push("");for(const n of s.elided){const i=n.reason==="seen"?"shown earlier in session":`~${n.stubCost} tokens`;e.push(`- [${n.item.kind}:${n.item.id}] ${n.item.title} \xB7 ${i}`)}e.push("")}e.push("---"),e.push(`_Budget: ${s.tokensInline}/${r} tokens inline \xB7 ${s.elided.length} stub${s.elided.length===1?"":"s"} \xB7 estimator: ${l.source}_`);const v=e.join(`
4
- `);try{x.log({tool_name:"wyrm_context_build",project_id:t.project.id,args:{budget:r,items_total:a.length,items_inline:s.inline.length,items_elided:s.elided.length,tokens_inline:s.tokensInline,tokens_stubs:s.tokensStubs},success:!0,latency_ms:0})}catch{}return{content:[{type:"text",text:v}]}}}export{E as briefMemoryScore,M as makeRunBudgetedContextBuild};
1
+ import{applyBudget as j,resolveBudget as E}from"./token-budget.js";import{autoSummaryPenalty as O,episodicKind as A,score as $}from"./context-ranking.js";function D(o,f){const d=O(o.artifact.problem);return $({confidence:o.artifact.confidence,relevance:d<1?o.relevance_score/(1-d):o.relevance_score,updatedAt:o.artifact.updated_at,sourcePenalty:d},f)}const F=.75;function G(o){const{groundTruths:f,scaffoldLib:d,memory:x,toolAnalytics:B,sessionSeen:k,tokenEstimator:c,rankWeights:y,threadText:C}=o;return function(e){const r=E({callArg:e.maxTokens,envValue:process.env.WYRM_CONTEXT_TOKEN_BUDGET,clientName:null}),I=e.sessionId?k().getSeen(e.sessionId):new Set,l=(()=>{try{return C(e.project.id)}catch{return""}})(),T=l?c.count(l):0,p=f.formatForContext(e.project.id,void 0,e.task),u=c.count(p??"");let _=p??"",g=u;u>r*.5&&!e.strictBudget&&process.stderr.write(`[wyrm_context_build] ground truths consume ${u} of ${r}-token budget; body will be heavily elided. Pass strict_budget:true to elide truths too.
2
+ `),e.strictBudget&&u>r*.5&&(_=(p??"").slice(0,r*2),g=c.count(_));const a=[],h=d.findBest(e.task,e.project.id);if(h){const n=d.formatForContext(h),i=h.scaffold.id;a.push({item:{kind:"scaffold",id:i,title:h.scaffold.problem_type,body:n},key:`scaffold:${i}`,inlineCost:c.count(n),stubCost:20,score:$({confidence:.9,relevance:.9,updatedAt:new Date().toISOString()},y)})}const w=e.recalled??x.recall(e.project.id,e.task,{limit:20,minConfidence:e.minConfidence??.2,runId:e.runId??null,strict:!0});for(const n of w){const i=n.artifact;if(e.kinds&&e.kinds.length>0&&!e.kinds.includes(i.kind))continue;const m=A(i),v=m&&m!=="session digest"?F:1,b=[`**${m?`EPISODIC (${m})`:i.kind.toUpperCase()}** \xB7 ${i.problem}`,i.constraints?`Constraints: ${i.constraints}`:"",i.validated_fix?`Fix: ${i.validated_fix}`:"",i.why_it_worked?`Why: ${i.why_it_worked}`:""].filter(Boolean).join(`
3
+ `);a.push({item:{kind:"memory",id:i.id,title:i.problem.slice(0,60),body:b},key:`memory:${i.id}`,inlineCost:c.count(b),stubCost:25,score:D(n,y)*v})}const s=j(a,{budget:r,alreadySeen:I,reserved:(e.strictBudget?0:g)+T});e.sessionId&&(k().markBulk(e.sessionId,s.inline.map(n=>({id:n.id,kind:n.kind,mode:"inline"}))),k().markBulk(e.sessionId,s.elided.map(n=>({id:n.item.id,kind:n.item.kind,mode:"stub"}))));const t=[];t.push(`\u{F115D} **Context Brief**: "${e.task}"`),t.push(""),l&&(t.push("## Working thread"),t.push(""),t.push(l),t.push("")),t.push("## Ground truths (stable preamble)"),t.push(""),t.push(_||"_no ground truths set for this project yet_"),t.push(""),t.push("## Task-relevant memory"),t.push("");for(const n of s.inline)t.push(`### [${n.kind}:${n.id}] ${n.title}`),t.push(n.body),t.push("");if(s.elided.length>0){t.push("## Elided to stubs"),t.push(""),t.push("_Below the budget cutoff or already seen this session. Recall with `wyrm_recall(id)`:_"),t.push("");for(const n of s.elided){const i=n.reason==="seen"?"shown earlier in session":`~${n.stubCost} tokens`;t.push(`- [${n.item.kind}:${n.item.id}] ${n.item.title} \xB7 ${i}`)}t.push("")}t.push("---"),t.push(`_Budget: ${s.tokensInline}/${r} tokens inline \xB7 ${s.elided.length} stub${s.elided.length===1?"":"s"} \xB7 estimator: ${c.source}_`);const S=t.join(`
4
+ `);try{B.log({tool_name:"wyrm_context_build",project_id:e.project.id,args:{budget:r,items_total:a.length,items_inline:s.inline.length,items_elided:s.elided.length,tokens_inline:s.tokensInline,tokens_stubs:s.tokensStubs},success:!0,latency_ms:0})}catch{}return{content:[{type:"text",text:S}]}}}export{D as briefMemoryScore,G as makeRunBudgetedContextBuild};