wyrm-mcp 8.5.2 → 8.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cloud/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import{randomBytes as A}from"node:crypto";import{writeFileSync as P}from"node:fs";import{hostname as O}from"node:os";import{resolve as L,dirname as W,join as M}from"node:path";import{fileURLToPath as F}from"node:url";import{createInterface as B}from"node:readline";import{CloudClient as d,CloudError as w,DEFAULT_BASE as v,SESSION_FILE as $,loadSession as g,saveSession as C,clearSession as k}from"./client.js";import{getMasterKey as U,keyExists as S,keyFilePath as a,decrypt as j}from"./crypto.js";import{computeMachineFp as I,classifySession as G}from"./machine-id.js";import{runSync as H,CopiedSessionError as Y}from"./sync-engine.js";import{SovereignSyncManager as K,restartIntervalMinutes as q}from"./sync-daemon.js";import{initializeLicense as z,hasFeature as J,getTier as V}from"../license.js";function b(){z(),J("cloud_backup")||(console.error("\u{F115D} Wyrm Cloud sync requires a Pro license or higher."),console.error(` Current tier: ${V()}`),console.error(" Upgrade: https://ghosts.lk/wyrm"),console.error(" Already paid? Activate your license (wyrm_license tool, or place"),console.error(" the signed JSON at ~/.wyrm/license.json) and retry."),process.exit(1))}function _(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KiB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MiB`:`${(e/(1024*1024*1024)).toFixed(2)} GiB`}function E(e){return e?new Date(e).toISOString().replace("T"," ").slice(0,16)+" UTC":"\u2014"}async function ke(e){const r=e[0],n=e.slice(1);try{switch(r){case void 0:case"help":case"--help":case"-h":return Q();case"login":return await X(n);case"logout":return await Z();case"status":return await ee();case"doctor":return oe();case"devices":return await se(n);case"sync":return await ne(n);case"daemon":return await re(n);case"export":return await ce(n);case"delete":return await le(n);case"recovery":return await ie(n);default:console.error(`Unknown subcommand: wyrm cloud ${r}`),console.error("Try `wyrm cloud help`"),process.exit(1)}}catch(s){if(s instanceof Y&&process.exit(1),s instanceof w)s.status===401?console.error("\u2718 Not authenticated. Run `wyrm cloud login`."):s.status===403?console.error(`\u2718 Forbidden: ${s.message}`):s.status===0?(console.error(`\u2718 ${s.message}`),console.error(" Check your internet connection. If wyrm.ghosts.lk is unreachable, see status at https://www.cloudflarestatus.com")):s.status===501?console.error(`\u2718 Service not configured: ${s.message}`):console.error(`\u2718 ${s.message} (HTTP ${s.status})`);else if(s instanceof Error){const t=s.message;t.includes("Not logged in")?console.error("\u2718 Not logged in. Run `wyrm cloud login`."):t.includes("Wyrm Cloud key")?console.error(`\u2718 Encryption-key error: ${t}`):console.error(`\u2718 ${t}`)}else console.error("\u2718 Unexpected error:",s);process.exit(1)}}function Q(){console.log(`\u{F115D} wyrm cloud \u2014 sync your Wyrm memory across devices
1
+ import{randomBytes as A}from"node:crypto";import{writeFileSync as P}from"node:fs";import{hostname as O}from"node:os";import{resolve as L,dirname as W,join as M}from"node:path";import{fileURLToPath as F}from"node:url";import{createInterface as B}from"node:readline";import{CloudClient as d,CloudError as w,DEFAULT_BASE as v,SESSION_FILE as $,loadSession as g,saveSession as C,clearSession as k}from"./client.js";import{getMasterKey as U,keyExists as S,keyFilePath as a,decrypt as j}from"./crypto.js";import{computeMachineFp as I,classifySession as G}from"./machine-id.js";import{runSync as H,CopiedSessionError as Y}from"./sync-engine.js";import{SovereignSyncManager as q,restartIntervalMinutes as K}from"./sync-daemon.js";import{initializeLicense as z,hasFeature as J,getTier as V}from"../license.js";function b(){z(),J("cloud_backup")||(console.error("\u{F115D} Wyrm Cloud sync requires a Pro license or higher."),console.error(` Current tier: ${V()}`),console.error(" Upgrade: https://ghosts.lk/wyrm"),console.error(" Already paid? Activate your license (wyrm_license tool, or place"),console.error(" the signed JSON at ~/.wyrm/license.json) and retry."),process.exit(1))}function _(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KiB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MiB`:`${(e/(1024*1024*1024)).toFixed(2)} GiB`}function E(e){return e?new Date(e).toISOString().replace("T"," ").slice(0,16)+" UTC":"\u2014"}async function ke(e){const r=e[0],n=e.slice(1);try{switch(r){case void 0:case"help":case"--help":case"-h":return Q();case"login":return await X(n);case"logout":return await Z();case"status":return await ee();case"doctor":return oe();case"devices":return await se(n);case"sync":return await ne(n);case"daemon":return await re(n);case"export":return await ce(n);case"delete":return await le(n);case"recovery":return await ie(n);default:console.error(`Unknown subcommand: wyrm cloud ${r}`),console.error("Try `wyrm cloud help`"),process.exit(1)}}catch(s){if(s instanceof Y&&process.exit(1),s instanceof w)s.status===401?console.error("\u2718 Not authenticated. Run `wyrm cloud login`."):s.status===403?console.error(`\u2718 Forbidden: ${s.message}`):s.status===0?(console.error(`\u2718 ${s.message}`),console.error(" Check your internet connection. If wyrm.ghosts.lk is unreachable, see status at https://www.cloudflarestatus.com")):s.status===501?console.error(`\u2718 Service not configured: ${s.message}`):console.error(`\u2718 ${s.message} (HTTP ${s.status})`);else if(s instanceof Error){const t=s.message;t.includes("Not logged in")?console.error("\u2718 Not logged in. Run `wyrm cloud login`."):t.includes("Wyrm Cloud key")?console.error(`\u2718 Encryption-key error: ${t}`):console.error(`\u2718 ${t}`)}else console.error("\u2718 Unexpected error:",s);process.exit(1)}}function Q(){console.log(`\u{F115D} wyrm cloud \u2014 sync your Wyrm memory across devices
2
2
 
3
3
  USAGE:
4
4
  wyrm cloud <subcommand> [args]
@@ -38,4 +38,4 @@ DOMAIN:
38
38
  https://wyrm.ghosts.lk
39
39
  `)}async function X(e){const r=g();if(r)try{await new d(r.base,r.session).me(),console.error("Already logged in. Run `wyrm cloud logout` first to switch accounts."),process.exit(1)}catch(c){c instanceof w&&c.status===401||(console.error("Already logged in. Run `wyrm cloud logout` first to switch accounts."),process.exit(1)),console.log("Previous session expired; starting a fresh login."),k()}const n=new d(v),s=A(32).toString("hex");let t;try{t=await n.cliInit(s)}catch(c){console.error(`\u2718 Could not start login: ${c instanceof Error?c.message:c}`),process.exit(1)}console.log(""),console.log("\u{F115D} Sign in to Wyrm Cloud"),console.log(""),console.log(" 1. Open this URL on any device:"),console.log(` ${t.browser_url}`),console.log(""),console.log(" 2. Type this code when prompted:"),console.log(` ${t.code}`),console.log(""),console.log(" 3. Sign in with Google or GitHub."),console.log(""),console.log(` Waiting (expires in ${Math.round(t.expires_in_sec/60)} min)\u2026`);const l=Date.now()+t.expires_in_sec*1e3;let i,u;for(;Date.now()<l;){await new Promise(c=>{setTimeout(c,3e3)});try{const c=await n.cliPoll(s);if(c.ready&&c.session){i=c.session,u=c.account_id;break}}catch(c){console.error(`
40
40
  \u2718 Login failed: ${c instanceof Error?c.message:c}`),process.exit(1)}}(!i||!u)&&(console.error("\n\u2718 Login timed out. Run `wyrm cloud login` again."),process.exit(1)),C({base:v,session:i,account_id:u,created_at:Date.now()});const y=new d(v,i);let p="";try{p=(await y.me()).account.email}catch{}const f=`${O()} (${process.platform})`;let m;try{m=(await y.registerDevice(f)).device.id;const h=g();h&&C({...h,email:p,device_id:m,device_name:f,machine_fp:I()})}catch(c){console.error(`
41
- \u26A0 Logged in but could not register device: ${c instanceof Error?c.message:c}`),console.error(" Re-run `wyrm cloud login` to retry.");return}console.log(""),console.log(`\u2713 Signed in as ${p}`),console.log(`\u2713 Device registered: ${f}`),console.log(`\u2713 Session saved to ${$}`);const R=!S();if(U(),R?(console.log(`\u2713 Encryption key generated at ${a()} (0600)`),console.log(""),console.log(" \u26A0 Back up the key file. Losing it means losing access to your synced data."),console.log(" The Wyrm Cloud server CANNOT recover it \u2014 that's the operator-owns-data"),console.log(" guarantee. Run `wyrm cloud recovery show` now to get a 24-word phrase,"),console.log(" or copy the key file to a password manager / encrypted backup.")):console.log(`\u2713 Encryption key reused from ${a()}`),m)try{const c=await y.syncStatus();if(c.deltas>0){const T=(await y.syncPull(m,0,1)).deltas[0];if(T){let x=!0;try{j(T.payload_b64)}catch{x=!1}x||(console.log(""),console.log(" \u26A0 THIS DEVICE CANNOT READ THE ACCOUNT'S DATA YET"),console.log(` Login succeeded, but the key at ${a()} cannot decrypt`),console.log(` the ${c.deltas} delta(s) already on this account. They were encrypted`),console.log(" with a different master key (e.g. this is an org account whose other"),console.log(" devices hold the shared key). Until the keys match, sync will move"),console.log(" 0 rows in BOTH directions \u2014 a connected pipe that carries nothing."),console.log(""),console.log(" Fix: adopt the account key on this device \u2014"),console.log(" wyrm cloud recovery restore (needs the account's 24-word phrase)"),console.log(` Heads-up: restore OVERWRITES ${a()}. If this device also has`),console.log(" its OWN separate cloud data, back that key up first, or run the org"),console.log(" sync under a separate WYRM home so both keys coexist."))}}}catch{}console.log(""),console.log("Next: `wyrm cloud sync` to push your memory.")}async function Z(){const e=g();if(!e){console.log("Not logged in.");return}try{await new d(e.base,e.session).logout(),console.log("\u2713 Session revoked server-side.")}catch(r){console.error(`\u26A0 Could not revoke server-side: ${r instanceof Error?r.message:r}`),console.error(" Local session cleared anyway.")}k(),console.log(`\u2713 Local session removed (${$}).`),console.log(" Note: ~/.wyrm/cloud.key kept. Delete manually if you want a clean slate.")}async function ee(){const e=g();if(!e){console.log("Not logged in. Run `wyrm cloud login`.");return}const r=new d(e.base,e.session);try{const n=await r.me(),s=await r.syncStatus();console.log(""),console.log(`\u{F115D} Wyrm Cloud \u2014 ${n.account.email}`),console.log(""),console.log(` Tier: ${s.tier}`),console.log(` Storage: ${_(s.storage.used_bytes)} / ${_(s.storage.limit_bytes)} (${s.storage.percent}%)`),console.log(` Deltas: ${s.deltas}`),console.log(` Devices: ${s.devices}`),console.log(` Providers:${n.identities.map(t=>" "+t.provider).join(",")}`),console.log(""),console.log(` This machine: ${e.device_name??"(not registered)"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none)"}`),console.log(` Session age: ${Math.round((Date.now()-e.created_at)/(1440*60*1e3))} day(s)`),D(e),console.log(""),S()?s.deltas>0&&(console.log(` Key: ${a()} (0600)`),console.log(` \u26A0 Back this file up. Losing it = losing access to your ${s.deltas} deltas.`),console.log("")):(console.log(" \u26A0 ENCRYPTION KEY MISSING"),console.log(` Expected at: ${a()}`),console.log(" Without this file, deltas already pushed cannot be decrypted."),console.log(" If you have a backup, restore it before running sync."),console.log(""))}catch(n){throw n instanceof w&&n.status===401&&(console.error("Session expired or invalid. Run `wyrm cloud login` again."),process.exit(1)),n}}function D(e){const r=G(e.machine_fp);r.state==="match"?console.log(" Identity: \u2713 machine fingerprint matches (not a copied session)"):r.state==="adopt"?console.log(" Identity: \u2014 no fingerprint yet (pre-7.0.3 session; adopted on next sync)"):(console.log(" Identity: \u26A0 FINGERPRINT MISMATCH \u2014 this session looks COPIED"),console.log(` stored ${r.stored.slice(0,12)}\u2026 vs this machine ${r.current.slice(0,12)}\u2026`),console.log(" device_id collision \u2192 cross-device pull returns 0 silently."),console.log(" Fix: rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json (KEEP cloud.key),"),console.log(" then `wyrm cloud login`. (Or `wyrm cloud sync --force` if intended.)"))}function oe(){const e=g();if(console.log(""),console.log("\u{F115D} Wyrm Cloud \u2014 device identity doctor"),console.log(""),!e){console.log(" Not logged in. Run `wyrm cloud login`."),console.log("");return}console.log(` Account: ${e.email??e.account_id.slice(0,8)+"\u2026"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none \u2014 re-run `wyrm cloud login`)"}`),console.log(` Server: ${e.base}`),console.log(` Stored fp: ${e.machine_fp?e.machine_fp.slice(0,12)+"\u2026":"(none \u2014 pre-7.0.3 session)"}`),console.log(` This box fp: ${I().slice(0,12)}\u2026`),D(e),console.log(""),console.log(" Only ~/.wyrm/cloud.key is meant to be shared across your machines."),console.log(" cloud.json + cloud-cursor.json + machine-id are per-device."),console.log("")}async function se(e){const r=e[0],n=d.fromSession();if(r==="revoke"){const l=e[1];l||(console.error("Usage: wyrm cloud devices revoke <device-id>"),process.exit(1)),await n.revokeDevice(l),console.log(`\u2713 Device ${l.slice(0,8)}\u2026 revoked.`);return}const{devices:s}=await n.listDevices(),o=g()?.device_id;console.log(""),console.log(`\u{F115D} Devices (${s.length})`),console.log("");for(const l of s){const i=l.id===o?"\u2605":" ",u=l.revoked?"(revoked)":"";console.log(` ${i} ${l.id.slice(0,8)}\u2026 ${l.name.padEnd(30)} last_seen=${E(l.last_seen_at)} ${u}`)}console.log(""),console.log(" \u2605 = this machine"),console.log(" Revoke: wyrm cloud devices revoke <device-id>"),console.log("")}async function ne(e){b();const r=e.includes("--dry-run"),n=e.includes("--all"),s=e.includes("--force");S()||(console.error("\u26A0 Encryption key not found at",a()),console.error(" A new key will be generated, but it will NOT decrypt deltas"),console.error(" pushed by other devices using a different key. If you have a backup,"),console.error(" restore it to ~/.wyrm/cloud.key (0600) before running sync."),console.error(" Continuing in 5 seconds \u2014 press Ctrl+C to abort."),await new Promise(o=>{setTimeout(o,5e3)})),n&&(console.log("\u{1F310} --all mode: syncing every semantic table (ignoring visibility flags)."),console.log(" Skipped: per-device logs, FTS shadows, sync internals."),console.log(""));const t=await H({dryRun:r,all:n,force:s});if(console.log(""),console.log("\u{F115D} Sync complete"),console.log(""),console.log(` Pushed: ${t.pushed} row(s)${r?" (dry run)":""}`),console.log(` Pulled: ${t.pulled} row(s)`),console.log(` Deleted: ${t.deleted_local} row(s) (peer tombstones)`),t.errors.length>0){console.log(""),console.log(" Errors:");for(const o of t.errors)console.log(` - ${o}`)}console.log("")}function te(){const e=M(W(F(import.meta.url)),"sync-daemon-entrypoint.js");return new K(e)}async function re(e){b();const r=e[0]??"status",n=e.find(o=>o.startsWith("--interval=")),s=n?Number(n.split("=")[1]):void 0,t=te();switch(r){case"start":{g()||(console.error("\u2718 Not logged in. Run `wyrm cloud login` first."),process.exit(1));const o=t.start({interval_minutes:s});o.ok||(console.error(`\u{F115D} sovereign-sync daemon: ${o.reason??"failed to start"}${o.pid?` (pid ${o.pid})`:""}`),process.exit(o.pid?0:1)),console.log(`\u{F115D} \u2713 sovereign-sync daemon started (pid ${o.pid}). Logs: ~/.wyrm/wyrm-sovereign-sync.log`);return}case"stop":{const o=t.stop();console.log(o.ok?"\u{F115D} \u2713 sovereign-sync daemon stopped":`\u{F115D} sovereign-sync daemon: ${o.reason??"failed to stop"}`);return}case"restart":{const o=q(s,t.status().state?.interval_ms);t.stop(),await new Promise(i=>{setTimeout(i,500)});const l=t.start({interval_minutes:o});console.log(l.ok?`\u{F115D} \u2713 sovereign-sync daemon restarted (pid ${l.pid}, every ${o} min)`:`\u{F115D} restart failed: ${l.reason??"unknown"}`);return}case"status":{const o=t.status();console.log("\u{F115D} Sovereign-sync daemon"),console.log(` Running: ${o.running?`\u2713 yes (pid ${o.pid})`:"\u2717 no"}`),o.state?.interval_ms&&console.log(` Interval: every ${o.state.interval_ms/6e4} min`),o.state?.started_ts&&console.log(` Started: ${E(o.state.started_ts)}`),o.state?.last_tick_ts&&(console.log(` Last tick: ${E(o.state.last_tick_ts)} (exit ${o.state.last_tick_exit??"\u2014"})`),o.state.last_tick_summary&&console.log(` Last result: ${o.state.last_tick_summary}`));return}default:console.error(`Unknown daemon action: ${r}. Use: start, stop, restart, status`),process.exit(1)}}async function ce(e){b();const r=d.fromSession(),n=e.find(l=>!l.startsWith("-")),s=L(n??`wyrm-cloud-export-${new Date().toISOString().slice(0,10)}.json`);console.log(" Fetching account export\u2026");const t=await r.accountExport(),o=JSON.stringify(t,null,2);P(s,o,{mode:384}),console.log(""),console.log(`\u2713 Exported to ${s}`),console.log(""),console.log(` Account: ${t.account.email} (${t.account.id.slice(0,8)}\u2026)`),console.log(` Devices: ${t.devices.length}`),console.log(` Identities: ${t.identities.length}`),console.log(` Orgs: ${t.orgs.length}`),console.log(` Deltas: ${t.deltas.length}`),console.log(` Size: ${_(o.length)}`),console.log(""),console.log(" Deltas are still AES-256-GCM ciphertext. To decrypt them you"),console.log(` need the key at ${a()}. Back that up alongside this file.`),console.log("")}async function le(e){e.includes("--confirm")||(console.error("\u2718 This permanently deletes your Wyrm Cloud account."),console.error(" Re-run with --confirm to proceed:"),console.error(" wyrm cloud delete --confirm"),process.exit(1));const n=g();n||(console.error("\u2718 Not logged in. Nothing to delete."),process.exit(1)),console.log(""),console.log("\u26A0 You are about to PERMANENTLY DELETE your Wyrm Cloud account."),console.log(""),console.log(` Account: ${n.email??n.account_id}`),console.log(` Server: ${n.base}`),console.log(""),console.log(" This wipes all encrypted blobs, devices, identities, and org"),console.log(" memberships you own. The local ~/.wyrm/cloud.key is kept so"),console.log(" any backups remain decryptable. This action CANNOT be undone."),console.log(""),(await N(' Type "DELETE my account" to confirm: ')).trim()!=="DELETE my account"&&(console.error("\u2718 Confirmation phrase did not match. Aborting."),process.exit(1)),await new d(n.base,n.session).accountDelete(),k(),console.log(""),console.log("\u2713 Account deleted on server."),console.log(`\u2713 Local session removed (${$}).`),console.log(` Kept: ${a()} (in case you need to decrypt an old export).`),console.log("")}function N(e){return new Promise(r=>{const n=B({input:process.stdin,output:process.stdout});n.question(e,s=>{n.close(),r(s)})})}async function ie(e){const r=e[0];if(r==="show"){const{keyToMnemonic:n}=await import("./recovery.js"),t=n().split(" ");console.log(""),console.log("\u{1F511} Wyrm Cloud recovery phrase (BIP39, 24 words)"),console.log("");for(let o=0;o<4;o++){const l=t.slice(o*6,o*6+6).map((i,u)=>`${String(o*6+u+1).padStart(2)}. ${i.padEnd(10)}`).join(" ");console.log(` ${l}`)}console.log(""),console.log(" Write these down on PAPER and keep them somewhere safe."),console.log(" Anyone with this phrase can decrypt your synced Wyrm memory."),console.log(" Wyrm Cloud has NO COPY of this. Lose it = lose your data."),console.log("");return}if(r==="restore"){const n=e.includes("--overwrite");console.log(""),console.log("\u{1F511} Restore master key from BIP39 recovery phrase"),console.log(""),console.log(" Enter your 24 words separated by spaces (case-insensitive)."),console.log(" Press Enter when done."),console.log("");const s=await N(" > "),{mnemonicToKey:t}=await import("./recovery.js"),{path:o,overwrote:l}=t(s,{overwrite:n});console.log(""),console.log(`\u2713 Master key restored to ${o} (0600)`),l&&console.log(" Replaced existing key (you passed --overwrite)."),console.log(" Run `wyrm cloud sync` to pull deltas with the restored key."),console.log("");return}console.error("Usage:"),console.error(" wyrm cloud recovery show"),console.error(" wyrm cloud recovery restore [--overwrite]"),process.exit(1)}export{ke as cmdCloud};
41
+ \u26A0 Logged in but could not register device: ${c instanceof Error?c.message:c}`),console.error(" Re-run `wyrm cloud login` to retry.");return}console.log(""),console.log(`\u2713 Signed in as ${p}`),console.log(`\u2713 Device registered: ${f}`),console.log(`\u2713 Session saved to ${$}`);const R=!S();if(U(),R?(console.log(`\u2713 Encryption key generated at ${a()} (0600)`),console.log(""),console.log(" \u26A0 Back up the key file. Losing it means losing access to your synced data."),console.log(" The Wyrm Cloud server CANNOT recover it \u2014 that's the operator-owns-data"),console.log(" guarantee. Run `wyrm cloud recovery show` now to get a 24-word phrase,"),console.log(" or copy the key file to a password manager / encrypted backup.")):console.log(`\u2713 Encryption key reused from ${a()}`),m)try{const c=await y.syncStatus();if(c.deltas>0){const T=(await y.syncPull(m,0,1)).deltas[0];if(T){let x=!0;try{j(T.payload_b64)}catch{x=!1}x||(console.log(""),console.log(" \u26A0 THIS DEVICE CANNOT READ THE ACCOUNT'S DATA YET"),console.log(` Login succeeded, but the key at ${a()} cannot decrypt`),console.log(` the ${c.deltas} delta(s) already on this account. They were encrypted`),console.log(" with a different master key (e.g. this is an org account whose other"),console.log(" devices hold the shared key). Until the keys match, sync will move"),console.log(" 0 rows in BOTH directions \u2014 a connected pipe that carries nothing."),console.log(""),console.log(" Fix: adopt the account key on this device \u2014"),console.log(" wyrm cloud recovery restore (needs the account's 24-word phrase)"),console.log(` Heads-up: restore OVERWRITES ${a()}. If this device also has`),console.log(" its OWN separate cloud data, back that key up first, or run the org"),console.log(" sync under a separate WYRM home so both keys coexist."))}}}catch{}console.log(""),console.log("Next: `wyrm cloud sync` to push your memory.")}async function Z(){const e=g();if(!e){console.log("Not logged in.");return}try{await new d(e.base,e.session).logout(),console.log("\u2713 Session revoked server-side.")}catch(r){console.error(`\u26A0 Could not revoke server-side: ${r instanceof Error?r.message:r}`),console.error(" Local session cleared anyway.")}k(),console.log(`\u2713 Local session removed (${$}).`),console.log(" Note: ~/.wyrm/cloud.key kept. Delete manually if you want a clean slate.")}async function ee(){const e=g();if(!e){console.log("Not logged in. Run `wyrm cloud login`.");return}const r=new d(e.base,e.session);try{const n=await r.me(),s=await r.syncStatus();console.log(""),console.log(`\u{F115D} Wyrm Cloud \u2014 ${n.account.email}`),console.log(""),console.log(` Tier: ${s.tier}`),console.log(` Storage: ${_(s.storage.used_bytes)} / ${_(s.storage.limit_bytes)} (${s.storage.percent}%)`),console.log(` Deltas: ${s.deltas}`),console.log(` Devices: ${s.devices}`),console.log(` Providers:${n.identities.map(t=>" "+t.provider).join(",")}`),console.log(""),console.log(` This machine: ${e.device_name??"(not registered)"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none)"}`),console.log(` Session age: ${Math.round((Date.now()-e.created_at)/(1440*60*1e3))} day(s)`),D(e),console.log(""),S()?s.deltas>0&&(console.log(` Key: ${a()} (0600)`),console.log(` \u26A0 Back this file up. Losing it = losing access to your ${s.deltas} deltas.`),console.log("")):(console.log(" \u26A0 ENCRYPTION KEY MISSING"),console.log(` Expected at: ${a()}`),console.log(" Without this file, deltas already pushed cannot be decrypted."),console.log(" If you have a backup, restore it before running sync."),console.log(""))}catch(n){throw n instanceof w&&n.status===401&&(console.error("Session expired or invalid. Run `wyrm cloud login` again."),process.exit(1)),n}}function D(e){const r=G(e.machine_fp);r.state==="match"?console.log(" Identity: \u2713 machine fingerprint matches (not a copied session)"):r.state==="adopt"?console.log(" Identity: \u2014 no fingerprint yet (pre-7.0.3 session; adopted on next sync)"):(console.log(" Identity: \u26A0 FINGERPRINT MISMATCH \u2014 this session looks COPIED"),console.log(` stored ${r.stored.slice(0,12)}\u2026 vs this machine ${r.current.slice(0,12)}\u2026`),console.log(" device_id collision \u2192 cross-device pull returns 0 silently."),console.log(" Fix: rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json (KEEP cloud.key),"),console.log(" then `wyrm cloud login`. (Or `wyrm cloud sync --force` if intended.)"))}function oe(){const e=g();if(console.log(""),console.log("\u{F115D} Wyrm Cloud \u2014 device identity doctor"),console.log(""),!e){console.log(" Not logged in. Run `wyrm cloud login`."),console.log("");return}console.log(` Account: ${e.email??e.account_id.slice(0,8)+"\u2026"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none \u2014 re-run `wyrm cloud login`)"}`),console.log(` Server: ${e.base}`),console.log(` Stored fp: ${e.machine_fp?e.machine_fp.slice(0,12)+"\u2026":"(none \u2014 pre-7.0.3 session)"}`),console.log(` This box fp: ${I().slice(0,12)}\u2026`),D(e),console.log(""),console.log(" Only ~/.wyrm/cloud.key is meant to be shared across your machines."),console.log(" cloud.json + cloud-cursor.json + machine-id are per-device."),console.log("")}async function se(e){const r=e[0],n=d.fromSession();if(r==="revoke"){const l=e[1];l||(console.error("Usage: wyrm cloud devices revoke <device-id>"),process.exit(1)),await n.revokeDevice(l),console.log(`\u2713 Device ${l.slice(0,8)}\u2026 revoked.`);return}const{devices:s}=await n.listDevices(),o=g()?.device_id;console.log(""),console.log(`\u{F115D} Devices (${s.length})`),console.log("");for(const l of s){const i=l.id===o?"\u2605":" ",u=l.revoked?"(revoked)":"";console.log(` ${i} ${l.id.slice(0,8)}\u2026 ${l.name.padEnd(30)} last_seen=${E(l.last_seen_at)} ${u}`)}console.log(""),console.log(" \u2605 = this machine"),console.log(" Revoke: wyrm cloud devices revoke <device-id>"),console.log("")}async function ne(e){b();const r=e.includes("--dry-run"),n=e.includes("--all"),s=e.includes("--force");S()||(console.error("\u26A0 Encryption key not found at",a()),console.error(" A new key will be generated, but it will NOT decrypt deltas"),console.error(" pushed by other devices using a different key. If you have a backup,"),console.error(" restore it to ~/.wyrm/cloud.key (0600) before running sync."),console.error(" Continuing in 5 seconds \u2014 press Ctrl+C to abort."),await new Promise(o=>{setTimeout(o,5e3)})),n&&(console.log("\u{1F310} --all mode: syncing every semantic table (ignoring visibility flags)."),console.log(" Skipped: per-device logs, FTS shadows, sync internals."),console.log(""));const t=await H({dryRun:r,all:n,force:s});if(console.log(""),console.log("\u{F115D} Sync complete"),console.log(""),console.log(` Pushed: ${t.pushed} row(s)${r?" (dry run)":""}`),console.log(` Pulled: ${t.pulled} row(s)`),console.log(` Deleted: ${t.deleted_local} row(s) (peer tombstones)`),t.quarantined&&t.quarantined>0&&console.log(` Quarantined: ${t.quarantined} undecryptable delta(s) set aside (see errors)`),t.errors.length>0){console.log(""),console.log(" Errors:");for(const o of t.errors)console.log(` - ${o}`)}console.log("")}function te(){const e=M(W(F(import.meta.url)),"sync-daemon-entrypoint.js");return new q(e)}async function re(e){b();const r=e[0]??"status",n=e.find(o=>o.startsWith("--interval=")),s=n?Number(n.split("=")[1]):void 0,t=te();switch(r){case"start":{g()||(console.error("\u2718 Not logged in. Run `wyrm cloud login` first."),process.exit(1));const o=t.start({interval_minutes:s});o.ok||(console.error(`\u{F115D} sovereign-sync daemon: ${o.reason??"failed to start"}${o.pid?` (pid ${o.pid})`:""}`),process.exit(o.pid?0:1)),console.log(`\u{F115D} \u2713 sovereign-sync daemon started (pid ${o.pid}). Logs: ~/.wyrm/wyrm-sovereign-sync.log`);return}case"stop":{const o=t.stop();console.log(o.ok?"\u{F115D} \u2713 sovereign-sync daemon stopped":`\u{F115D} sovereign-sync daemon: ${o.reason??"failed to stop"}`);return}case"restart":{const o=K(s,t.status().state?.interval_ms);t.stop(),await new Promise(i=>{setTimeout(i,500)});const l=t.start({interval_minutes:o});console.log(l.ok?`\u{F115D} \u2713 sovereign-sync daemon restarted (pid ${l.pid}, every ${o} min)`:`\u{F115D} restart failed: ${l.reason??"unknown"}`);return}case"status":{const o=t.status();console.log("\u{F115D} Sovereign-sync daemon"),console.log(` Running: ${o.running?`\u2713 yes (pid ${o.pid})`:"\u2717 no"}`),o.state?.interval_ms&&console.log(` Interval: every ${o.state.interval_ms/6e4} min`),o.state?.started_ts&&console.log(` Started: ${E(o.state.started_ts)}`),o.state?.last_tick_ts&&(console.log(` Last tick: ${E(o.state.last_tick_ts)} (exit ${o.state.last_tick_exit??"\u2014"})`),o.state.last_tick_summary&&console.log(` Last result: ${o.state.last_tick_summary}`));return}default:console.error(`Unknown daemon action: ${r}. Use: start, stop, restart, status`),process.exit(1)}}async function ce(e){b();const r=d.fromSession(),n=e.find(l=>!l.startsWith("-")),s=L(n??`wyrm-cloud-export-${new Date().toISOString().slice(0,10)}.json`);console.log(" Fetching account export\u2026");const t=await r.accountExport(),o=JSON.stringify(t,null,2);P(s,o,{mode:384}),console.log(""),console.log(`\u2713 Exported to ${s}`),console.log(""),console.log(` Account: ${t.account.email} (${t.account.id.slice(0,8)}\u2026)`),console.log(` Devices: ${t.devices.length}`),console.log(` Identities: ${t.identities.length}`),console.log(` Orgs: ${t.orgs.length}`),console.log(` Deltas: ${t.deltas.length}`),console.log(` Size: ${_(o.length)}`),console.log(""),console.log(" Deltas are still AES-256-GCM ciphertext. To decrypt them you"),console.log(` need the key at ${a()}. Back that up alongside this file.`),console.log("")}async function le(e){e.includes("--confirm")||(console.error("\u2718 This permanently deletes your Wyrm Cloud account."),console.error(" Re-run with --confirm to proceed:"),console.error(" wyrm cloud delete --confirm"),process.exit(1));const n=g();n||(console.error("\u2718 Not logged in. Nothing to delete."),process.exit(1)),console.log(""),console.log("\u26A0 You are about to PERMANENTLY DELETE your Wyrm Cloud account."),console.log(""),console.log(` Account: ${n.email??n.account_id}`),console.log(` Server: ${n.base}`),console.log(""),console.log(" This wipes all encrypted blobs, devices, identities, and org"),console.log(" memberships you own. The local ~/.wyrm/cloud.key is kept so"),console.log(" any backups remain decryptable. This action CANNOT be undone."),console.log(""),(await N(' Type "DELETE my account" to confirm: ')).trim()!=="DELETE my account"&&(console.error("\u2718 Confirmation phrase did not match. Aborting."),process.exit(1)),await new d(n.base,n.session).accountDelete(),k(),console.log(""),console.log("\u2713 Account deleted on server."),console.log(`\u2713 Local session removed (${$}).`),console.log(` Kept: ${a()} (in case you need to decrypt an old export).`),console.log("")}function N(e){return new Promise(r=>{const n=B({input:process.stdin,output:process.stdout});n.question(e,s=>{n.close(),r(s)})})}async function ie(e){const r=e[0];if(r==="show"){const{keyToMnemonic:n}=await import("./recovery.js"),t=n().split(" ");console.log(""),console.log("\u{1F511} Wyrm Cloud recovery phrase (BIP39, 24 words)"),console.log("");for(let o=0;o<4;o++){const l=t.slice(o*6,o*6+6).map((i,u)=>`${String(o*6+u+1).padStart(2)}. ${i.padEnd(10)}`).join(" ");console.log(` ${l}`)}console.log(""),console.log(" Write these down on PAPER and keep them somewhere safe."),console.log(" Anyone with this phrase can decrypt your synced Wyrm memory."),console.log(" Wyrm Cloud has NO COPY of this. Lose it = lose your data."),console.log("");return}if(r==="restore"){const n=e.includes("--overwrite");console.log(""),console.log("\u{1F511} Restore master key from BIP39 recovery phrase"),console.log(""),console.log(" Enter your 24 words separated by spaces (case-insensitive)."),console.log(" Press Enter when done."),console.log("");const s=await N(" > "),{mnemonicToKey:t}=await import("./recovery.js"),{path:o,overwrote:l}=t(s,{overwrite:n});console.log(""),console.log(`\u2713 Master key restored to ${o} (0600)`),l&&console.log(" Replaced existing key (you passed --overwrite)."),console.log(" Run `wyrm cloud sync` to pull deltas with the restored key."),console.log("");return}console.error("Usage:"),console.error(" wyrm cloud recovery show"),console.error(" wyrm cloud recovery restore [--overwrite]"),process.exit(1)}export{ke as cmdCloud};
@@ -1,5 +1,5 @@
1
- import{readFileSync as O,writeFileSync as k,existsSync as D}from"node:fs";import{join as I}from"node:path";import{WyrmDB as L,resolveDbPath as C}from"../database.js";import{CloudClient as $,loadSession as A,saveSession as v,resolveCloudDir as j}from"./client.js";import{encrypt as S,decrypt as U}from"./crypto.js";import{classifySession as x,computeMachineFp as P}from"./machine-id.js";function T(){return I(j(),"cloud-cursor.json")}const E=[{kind:"truth",table:"ground_truths",hasVisibility:!0},{kind:"memory_artifact",table:"memory_artifacts",hasVisibility:!0},{kind:"quest",table:"quests",hasVisibility:!0},{kind:"design_token",table:"design_tokens",hasVisibility:!0},{kind:"design_reference",table:"design_references",hasVisibility:!0},{kind:"skill",table:"skills",hasVisibility:!0}],W=new Set(["sessions","agent_actions","agent_presence","session_seen_artifacts","tool_call_log","llm_query_log","external_call_log","token_savings_log","cost_tracking","usage_events","audit_log","indexing_queue","vectors","symbol_index","update_check","sync_conflicts","sync_log","sync_tombstones","wyrm_cloud_sync_state","mcp_client_configs","watch_dirs","global_context","schema_versions","failure_patterns"]),F=/_fts(_config|_data|_docsize|_idx)?$/;function M(e){const t=e.getDatabase(),s=[...E],r=new Set(E.map(o=>o.table));try{const o=t.prepare(`SELECT name FROM sqlite_master
2
- WHERE type='table' AND name NOT LIKE 'sqlite_%'`).all();for(const n of o){const l=n.name;if(r.has(l)||W.has(l)||F.test(l)||!h.test(l))continue;const _=t.prepare(`PRAGMA table_info(${l})`).all();if(!_.some(c=>c.name==="id"))continue;const d=_.some(c=>c.name==="cross_project_visibility");s.push({kind:l,table:l,hasVisibility:d})}}catch{}return s}function ie(e){return e}function H(e,t){if(!h.test(t))return new Set;try{const r=e.getDatabase().prepare(`PRAGMA table_info(${t})`).all();return new Set(r.filter(o=>h.test(o.name)).map(o=>o.name))}catch{return new Set}}const h=/^[a-z_][a-z0-9_]*$/i;function V(){const e={pull_cursor:0,last_push_updated_ms:0,last_push_at:0,last_pull_at:0},t=T();if(!D(t))return e;try{const s=JSON.parse(O(t,"utf-8"));return{...e,...s}}catch{return e}}function q(e){k(T(),JSON.stringify(e,null,2),{mode:384})}function N(e){e.getDatabase().exec(`
1
+ import{readFileSync as O,writeFileSync as D,existsSync as I}from"node:fs";import{join as $}from"node:path";import{WyrmDB as L,resolveDbPath as A}from"../database.js";import{CloudClient as C,loadSession as v,saveSession as U,resolveCloudDir as j}from"./client.js";import{encrypt as T,decrypt as x}from"./crypto.js";import{classifySession as q,computeMachineFp as P}from"./machine-id.js";function N(){return $(j(),"cloud-cursor.json")}const R=2;function F(t,e,n,o){const s=t?{...t}:{kind:e.kind,row_id:e.row_id,attempts:0,quarantined:!1,last_error:n,first_ms:o,last_ms:o};s.attempts+=1,s.last_error=n,s.last_ms=o;const r=s.attempts>=R,c=r&&!s.quarantined;return r&&(s.quarantined=!0),{entry:s,action:r?"skip":"retry",newlyQuarantined:c}}const g=[{kind:"truth",table:"ground_truths",hasVisibility:!0},{kind:"memory_artifact",table:"memory_artifacts",hasVisibility:!0},{kind:"quest",table:"quests",hasVisibility:!0},{kind:"design_token",table:"design_tokens",hasVisibility:!0},{kind:"design_reference",table:"design_references",hasVisibility:!0},{kind:"skill",table:"skills",hasVisibility:!0}],W=new Set(["sessions","agent_actions","agent_presence","session_seen_artifacts","tool_call_log","llm_query_log","external_call_log","token_savings_log","cost_tracking","usage_events","audit_log","indexing_queue","vectors","symbol_index","update_check","sync_conflicts","sync_log","sync_tombstones","wyrm_cloud_sync_state","mcp_client_configs","watch_dirs","global_context","schema_versions","failure_patterns"]),H=/_fts(_config|_data|_docsize|_idx)?$/;function M(t){const e=t.getDatabase(),n=[...g],o=new Set(g.map(s=>s.table));try{const s=e.prepare(`SELECT name FROM sqlite_master
2
+ WHERE type='table' AND name NOT LIKE 'sqlite_%'`).all();for(const r of s){const c=r.name;if(o.has(c)||W.has(c)||H.test(c)||!y.test(c))continue;const m=e.prepare(`PRAGMA table_info(${c})`).all();if(!m.some(l=>l.name==="id"))continue;const u=m.some(l=>l.name==="cross_project_visibility");n.push({kind:c,table:c,hasVisibility:u})}}catch{}return n}function ct(t){return t}function V(t,e){if(!y.test(e))return new Set;try{const o=t.getDatabase().prepare(`PRAGMA table_info(${e})`).all();return new Set(o.filter(s=>y.test(s.name)).map(s=>s.name))}catch{return new Set}}const y=/^[a-z_][a-z0-9_]*$/i;function Y(){const t={pull_cursor:0,last_push_updated_ms:0,last_push_at:0,last_pull_at:0},e=N();if(!I(e))return t;try{const n=JSON.parse(O(e,"utf-8"));return{...t,...n}}catch{return t}}function G(t){D(N(),JSON.stringify(t,null,2),{mode:384})}function k(t){t.getDatabase().exec(`
3
3
  CREATE TABLE IF NOT EXISTS sync_tombstones (
4
4
  id INTEGER PRIMARY KEY AUTOINCREMENT,
5
5
  kind TEXT NOT NULL,
@@ -10,11 +10,11 @@ import{readFileSync as O,writeFileSync as k,existsSync as D}from"node:fs";import
10
10
  );
11
11
  CREATE INDEX IF NOT EXISTS idx_sync_tombstones_pending
12
12
  ON sync_tombstones(pushed_at_ms);
13
- `)}function ae(e,t,s){if(typeof t!="string"||t.length===0||t.length>64)return;N(e),e.getDatabase().prepare(`INSERT OR IGNORE INTO sync_tombstones (kind, row_id, deleted_at_ms)
14
- VALUES (?, ?, ?)`).run(t,String(s),Date.now())}class Y extends Error{constructor(t){super(t),this.name="CopiedSessionError"}}function G(e){return!!e||process.env.WYRM_ALLOW_COPIED_SESSION==="1"}function B(e,t,s){const r=x(e.machine_fp);if(r.state==="match")return;if(r.state==="adopt"){t(P());return}const o=["","\u26A0 WYRM CLOUD: this session looks COPIED from another machine.","",` Stored machine fingerprint: ${r.stored.slice(0,12)}\u2026`,` This machine's fingerprint: ${r.current.slice(0,12)}\u2026`,""," Both machines now share ONE device_id. The cloud pull query skips a"," device's own deltas, so each machine filters the OTHER's changes out:"," push works but pull silently returns 0 rows (peer memory looks MISSING).",""," FIX \u2014 give this machine its own identity (keep your encryption key):"," rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json # NOT cloud.key"," wyrm cloud login",""," If this is a legitimate change (e.g. you renamed this host), re-run with:"," wyrm cloud sync --force (or set WYRM_ALLOW_COPIED_SESSION=1)",""];if(G(s)){console.error(o.join(`
15
- `)),console.error(" Proceeding anyway (--force / WYRM_ALLOW_COPIED_SESSION=1); fingerprint updated."),console.error(""),t(r.current);return}throw console.error(o.join(`
16
- `)),new Y("Cloud session looks copied from another machine (device_id collision). Re-login or pass --force.")}async function ce(e={}){const t=A();if(!t||!t.device_id)throw new Error("Not logged in or device not registered. Run `wyrm cloud login`.");B(t,i=>{v({...t,machine_fp:i})},e.force);const s=new $(t.base,t.session),r=V(),o=new L(C());N(o);const n={pushed:0,pulled:0,deleted_local:0,errors:[]},l=e.all?M(o):E,_=K(o,l,r.last_push_updated_ms,!!e.all,n.errors),d=X(o),c=[..._.map(i=>({kind:i.kind,row_id:String(i.row_id),visibility:i.visibility,payload_b64:S(JSON.stringify(i.row)),updated_at:i.updated_at})),...d.map(i=>({kind:"tombstone",row_id:`${i.kind}:${i.row_id}`,visibility:"org",payload_b64:S(JSON.stringify({kind:i.kind,row_id:i.row_id,deleted_at_ms:i.deleted_at_ms})),updated_at:i.deleted_at_ms}))];if(c.length>0&&!e.dryRun){let m=r.last_push_updated_ms;for(let a=0;a<c.length;a+=25){const p=c.slice(a,a+25);try{const u=await s.syncPush(t.device_id,p);if(n.pushed+=u.accepted,u.rejected>0)for(const f of u.rejected_details)n.errors.push(`push: ${f.reason} on ${f.id.slice(0,8)}\u2026`);for(const f of p)f.updated_at&&f.updated_at>m&&(m=f.updated_at)}catch(u){n.errors.push(`push chunk failed: ${u instanceof Error?u.message:u}`)}}r.last_push_updated_ms=m,r.last_push_at=Date.now(),d.length>0&&z(o,d.map(a=>a.id))}else n.pushed=c.length;if(!e.dryRun){let i=r.pull_cursor,m=0;e:for(;m<50;){let a;try{a=await s.syncPull(t.device_id,i,100)}catch(p){n.errors.push(`pull failed: ${p instanceof Error?p.message:p}`);break}if(a.deltas.length===0)break;for(const p of a.deltas)try{const u=U(p.payload_b64);if(p.kind==="tombstone"){const{kind:f,row_id:b}=JSON.parse(u);Z(o,l,f,b)&&n.deleted_local++}else{const f=JSON.parse(u);Q(o,l,p.kind,p.row_id,f),n.pulled++}}catch(u){n.errors.push(`apply ${p.kind}:${p.row_id}: ${u instanceof Error?u.message:u}`);break e}if(i=a.next_cursor,!a.has_more)break;m++}r.pull_cursor=i,r.last_pull_at=Date.now()}return q(r),n}function J(e){if(typeof e!="string"||e.length===0)return 0;const t=e.includes("T")?e:e.replace(" ","T")+"Z",s=Date.parse(t);return Number.isFinite(s)?s:0}function g(e,t,s){try{return e.getDatabase().prepare(`PRAGMA table_info(${t})`).all().some(n=>n.name===s)}catch{return!1}}function K(e,t,s,r,o){const n=[],l=e.getDatabase(),_=g(e,"projects","sync_policy");for(const d of t)if(h.test(d.table)&&!(!r&&!d.hasVisibility))try{const c=g(e,d.table,"updated_at"),i=d.hasVisibility?"WHERE cross_project_visibility IN ('org', 'public')":"WHERE 1=1",m=_&&g(e,d.table,"project_id")?"AND (project_id IS NULL OR project_id IN (SELECT id FROM projects WHERE sync_policy IN ('cloud', 'team')))":"",a=c?"AND (? = 0 OR updated_at IS NULL OR strftime('%s', updated_at) * 1000 > ?)":"",p=c?"ORDER BY (updated_at IS NULL) ASC, updated_at ASC":"",u=`SELECT * FROM ${d.table} ${i} ${m} ${a} ${p}`,f=l.prepare(u),b=c?f.all(s,s):f.all();for(const y of b){const w=J(y.updated_at);if(s>0&&w>0&&w<=s)continue;const R=d.hasVisibility?String(y.cross_project_visibility??"within"):"org";n.push({kind:d.kind,row_id:y.id??"?",visibility:R,row:y,updated_at:w||Date.now()})}}catch(c){const i=c instanceof Error?c.message:String(c);if(/no such table/i.test(i))continue;o.push(`collect ${d.table}: ${i}`)}return n.sort((d,c)=>(d.updated_at??0)-(c.updated_at??0)),n}function X(e){const t=e.getDatabase();try{return t.prepare(`SELECT id, kind, row_id, deleted_at_ms
13
+ `)}function lt(t,e,n){if(typeof e!="string"||e.length===0||e.length>64)return;k(t),t.getDatabase().prepare(`INSERT OR IGNORE INTO sync_tombstones (kind, row_id, deleted_at_ms)
14
+ VALUES (?, ?, ?)`).run(e,String(n),Date.now())}class B extends Error{constructor(e){super(e),this.name="CopiedSessionError"}}function J(t){return!!t||process.env.WYRM_ALLOW_COPIED_SESSION==="1"}function K(t,e,n){const o=q(t.machine_fp);if(o.state==="match")return;if(o.state==="adopt"){e(P());return}const s=["","\u26A0 WYRM CLOUD: this session looks COPIED from another machine.","",` Stored machine fingerprint: ${o.stored.slice(0,12)}\u2026`,` This machine's fingerprint: ${o.current.slice(0,12)}\u2026`,""," Both machines now share ONE device_id. The cloud pull query skips a"," device's own deltas, so each machine filters the OTHER's changes out:"," push works but pull silently returns 0 rows (peer memory looks MISSING).",""," FIX \u2014 give this machine its own identity (keep your encryption key):"," rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json # NOT cloud.key"," wyrm cloud login",""," If this is a legitimate change (e.g. you renamed this host), re-run with:"," wyrm cloud sync --force (or set WYRM_ALLOW_COPIED_SESSION=1)",""];if(J(n)){console.error(s.join(`
15
+ `)),console.error(" Proceeding anyway (--force / WYRM_ALLOW_COPIED_SESSION=1); fingerprint updated."),console.error(""),e(o.current);return}throw console.error(s.join(`
16
+ `)),new B("Cloud session looks copied from another machine (device_id collision). Re-login or pass --force.")}async function dt(t={}){const e=v();if(!e||!e.device_id)throw new Error("Not logged in or device not registered. Run `wyrm cloud login`.");K(e,a=>{U({...e,machine_fp:a})},t.force);const n=new C(e.base,e.session),o=Y(),s=new L(A());k(s);const r={pushed:0,pulled:0,deleted_local:0,errors:[],quarantined:0},c=t.all?M(s):g,m=Q(s,c,o.last_push_updated_ms,!!t.all,r.errors),u=z(s),l=[...m.map(a=>({kind:a.kind,row_id:String(a.row_id),visibility:a.visibility,payload_b64:T(JSON.stringify(a.row)),updated_at:a.updated_at})),...u.map(a=>({kind:"tombstone",row_id:`${a.kind}:${a.row_id}`,visibility:"org",payload_b64:T(JSON.stringify({kind:a.kind,row_id:a.row_id,deleted_at_ms:a.deleted_at_ms})),updated_at:a.deleted_at_ms}))];if(l.length>0&&!t.dryRun){let f=o.last_push_updated_ms;for(let i=0;i<l.length;i+=25){const d=l.slice(i,i+25);try{const p=await n.syncPush(e.device_id,d);if(r.pushed+=p.accepted,p.rejected>0)for(const _ of p.rejected_details)r.errors.push(`push: ${_.reason} on ${_.id.slice(0,8)}\u2026`);for(const _ of d)_.updated_at&&_.updated_at>f&&(f=_.updated_at)}catch(p){r.errors.push(`push chunk failed: ${p instanceof Error?p.message:p}`)}}o.last_push_updated_ms=f,o.last_push_at=Date.now(),u.length>0&&Z(s,u.map(i=>i.id))}else r.pushed=l.length;if(!t.dryRun){let a=o.pull_cursor,f=0;t:for(;f<50;){let i;try{i=await n.syncPull(e.device_id,a,100)}catch(d){r.errors.push(`pull failed: ${d instanceof Error?d.message:d}`);break}if(i.deltas.length===0)break;for(const d of i.deltas)try{const p=x(d.payload_b64);if(d.kind==="tombstone"){const{kind:_,row_id:w}=JSON.parse(p);et(s,c,_,w)&&r.deleted_local++}else{const _=JSON.parse(p);tt(s,c,d.kind,d.row_id,_),r.pulled++}}catch(p){const _=p instanceof Error?p.message:String(p),w=o.quarantine??={},{entry:h,action:E,newlyQuarantined:b}=F(w[d.id],d,_,Date.now());if(w[d.id]=h,E==="skip"){b&&(r.quarantined=(r.quarantined??0)+1),r.errors.push(`quarantined ${d.kind}:${d.row_id} after ${h.attempts} attempts (${_})`);continue}r.errors.push(`apply ${d.kind}:${d.row_id}: ${_} (attempt ${h.attempts}/${R}, will retry)`);break t}if(a=i.next_cursor,!i.has_more)break;f++}o.pull_cursor=a,o.last_pull_at=Date.now()}return G(o),r}function X(t){if(typeof t!="string"||t.length===0)return 0;const e=t.includes("T")?t:t.replace(" ","T")+"Z",n=Date.parse(e);return Number.isFinite(n)?n:0}function S(t,e,n){try{return t.getDatabase().prepare(`PRAGMA table_info(${e})`).all().some(r=>r.name===n)}catch{return!1}}function Q(t,e,n,o,s){const r=[],c=t.getDatabase(),m=S(t,"projects","sync_policy");for(const u of e)if(y.test(u.table)&&!(!o&&!u.hasVisibility))try{const l=S(t,u.table,"updated_at"),a=u.hasVisibility?"WHERE cross_project_visibility IN ('org', 'public')":"WHERE 1=1",f=m&&S(t,u.table,"project_id")?"AND (project_id IS NULL OR project_id IN (SELECT id FROM projects WHERE sync_policy IN ('cloud', 'team')))":"",i=l?"AND (? = 0 OR updated_at IS NULL OR strftime('%s', updated_at) * 1000 > ?)":"",d=l?"ORDER BY (updated_at IS NULL) ASC, updated_at ASC":"",p=`SELECT * FROM ${u.table} ${a} ${f} ${i} ${d}`,_=c.prepare(p),w=l?_.all(n,n):_.all();for(const h of w){const E=X(h.updated_at);if(n>0&&E>0&&E<=n)continue;const b=u.hasVisibility?String(h.cross_project_visibility??"within"):"org";r.push({kind:u.kind,row_id:h.id??"?",visibility:b,row:h,updated_at:E||Date.now()})}}catch(l){const a=l instanceof Error?l.message:String(l);if(/no such table/i.test(a))continue;s.push(`collect ${u.table}: ${a}`)}return r.sort((u,l)=>(u.updated_at??0)-(l.updated_at??0)),r}function z(t){const e=t.getDatabase();try{return e.prepare(`SELECT id, kind, row_id, deleted_at_ms
17
17
  FROM sync_tombstones
18
18
  WHERE pushed_at_ms IS NULL
19
19
  ORDER BY id ASC
20
- LIMIT 500`).all()}catch{return[]}}function z(e,t){if(t.length===0)return;const s=e.getDatabase(),r=Date.now(),o=s.prepare("UPDATE sync_tombstones SET pushed_at_ms = ? WHERE id = ?"),n=s.transaction(l=>{for(const _ of l)o.run(r,_)});try{n(t)}catch{}}function Q(e,t,s,r,o){const n=t.find(a=>a.kind===s);if(!n)return;if(!h.test(n.table))throw new Error(`apply ${s}: refusing unsafe table name ${n.table}`);const l=H(e,n.table);if(l.size===0)throw new Error(`apply ${s}: ${n.table} has no readable schema`);const _=Object.keys(o).filter(a=>l.has(a)&&h.test(a));if(_.length===0)throw new Error(`apply ${s}: no columns from peer payload matched local schema`);const d=e.getDatabase(),c=_.map(()=>"?").join(","),i=_.join(","),m=_.map(a=>o[a]);try{d.prepare(`INSERT OR REPLACE INTO ${n.table} (${i}) VALUES (${c})`).run(...m)}catch(a){throw new Error(`apply ${s} failed: ${a instanceof Error?a.message:a}`)}}function Z(e,t,s,r){const o=t.find(_=>_.kind===s);return!o||!h.test(o.table)?!1:e.getDatabase().prepare(`DELETE FROM ${o.table} WHERE id = ?`).run(r).changes>0}export{Y as CopiedSessionError,E as DEFAULT_TABLES,K as collectChangedRows,B as guardCopiedSession,ae as recordTombstone,ce as runSync};
20
+ LIMIT 500`).all()}catch{return[]}}function Z(t,e){if(e.length===0)return;const n=t.getDatabase(),o=Date.now(),s=n.prepare("UPDATE sync_tombstones SET pushed_at_ms = ? WHERE id = ?"),r=n.transaction(c=>{for(const m of c)s.run(o,m)});try{r(e)}catch{}}function tt(t,e,n,o,s){const r=e.find(i=>i.kind===n);if(!r)return;if(!y.test(r.table))throw new Error(`apply ${n}: refusing unsafe table name ${r.table}`);const c=V(t,r.table);if(c.size===0)throw new Error(`apply ${n}: ${r.table} has no readable schema`);const m=Object.keys(s).filter(i=>c.has(i)&&y.test(i));if(m.length===0)throw new Error(`apply ${n}: no columns from peer payload matched local schema`);const u=t.getDatabase(),l=m.map(()=>"?").join(","),a=m.join(","),f=m.map(i=>s[i]);try{u.prepare(`INSERT OR REPLACE INTO ${r.table} (${a}) VALUES (${l})`).run(...f)}catch(i){throw new Error(`apply ${n} failed: ${i instanceof Error?i.message:i}`)}}function et(t,e,n,o){const s=e.find(m=>m.kind===n);return!s||!y.test(s.table)?!1:t.getDatabase().prepare(`DELETE FROM ${s.table} WHERE id = ?`).run(o).changes>0}export{B as CopiedSessionError,g as DEFAULT_TABLES,R as QUARANTINE_AFTER,Q as collectChangedRows,K as guardCopiedSession,F as quarantineDecision,lt as recordTombstone,dt as runSync};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v2",
3
- "version": "8.5.2",
3
+ "version": "8.5.3",
4
4
  "derivedFrom": "standard tier MINUS device-local/egress/subprocess (src/cloud-profile.ts)",
5
5
  "count": 19,
6
6
  "tools": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "8.5.2",
3
+ "version": "8.5.3",
4
4
  "tools": [
5
5
  {
6
6
  "name": "wyrm_recall",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "8.5.2",
3
+ "version": "8.5.3",
4
4
  "generatedBy": "scripts/gen-tool-manifest.mjs (from the dist/ registry modules — never hand-edited)",
5
5
  "tiers": {
6
6
  "core": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyrm-mcp",
3
- "version": "8.5.2",
3
+ "version": "8.5.3",
4
4
  "mcpName": "lk.ghosts/wyrm",
5
5
  "description": "Local-first persistent memory for AI agents over MCP. Ground truths, negative learning (recorded failures block repeats), decision causality, hybrid recall, live memory streams, run-attributed fleet memory — a structured SQLite memory on your machine, no cloud or LLM required. Claude / Copilot / Cursor / Windsurf / Codex.",
6
6
  "type": "module",