wyrm-mcp 8.1.0 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities.js +3 -3
- package/dist/cloud/cli.js +8 -4
- package/dist/cloud/sync-daemon-entrypoint.js +2 -0
- package/dist/cloud/sync-daemon.js +1 -0
- package/dist/cloud/sync-engine.js +3 -3
- package/dist/database.js +7 -7
- package/dist/design.js +5 -5
- package/dist/handlers/cloud.js +11 -11
- package/dist/handlers/companion.js +1 -1
- package/dist/handlers/recall.js +4 -4
- package/dist/handlers/resources.js +1 -1
- package/dist/handlers/session.js +14 -12
- package/dist/migrations.js +1 -1
- package/dist/providers/embedding-provider.js +1 -1
- package/dist/receipt.js +1 -1
- package/dist/setup-wizard.js +5 -0
- package/dist/setup.js +3 -3
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/wyrm-cli.js +120 -116
- package/dist/wyrm-manifest.json +1 -1
- package/package.json +1 -1
package/dist/capabilities.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{homedir as
|
|
1
|
+
import{homedir as l}from"os";import{join as a,dirname as c}from"path";import{readFileSync as m,existsSync as y}from"fs";import{resolveEmbeddingState as h}from"./providers/embedding-provider.js";import{fileURLToPath as d}from"url";const w=d(import.meta.url),n=c(w),u=`Wyrm is not "AI memory" in the file-write sense. It's a layered intelligence
|
|
2
2
|
substrate that solves the four specific problems AI assistants share:
|
|
3
3
|
|
|
4
4
|
1. Amnesia. Sessions inherit prior state via lossless rehydration.
|
|
@@ -11,5 +11,5 @@ substrate that solves the four specific problems AI assistants share:
|
|
|
11
11
|
Every Wyrm session is local-first SQLite with FTS5 + hybrid semantic search.
|
|
12
12
|
Encryption is operator-controlled. Federation is opt-in per row. The audit
|
|
13
13
|
log is hash-chained and Ed25519-signable for SOC2/HIPAA scenarios. None of
|
|
14
|
-
this requires you to leave your laptop.`,
|
|
15
|
-
`)}export{
|
|
14
|
+
this requires you to leave your laptop.`,p=[{name:"Counter-pattern memory",what:"Records failed approaches with (scope, target, description) and blocks the same suggestion next time.",why:"Other memory tools learn only from success. Wyrm refuses to repeat mistakes \u2014 saves the most expensive class of tokens (the regenerated wrong answer).",tools:["wyrm_failure_record","wyrm_failure_check","wyrm_failure_list","wyrm_failure_resolve"]},{name:"Ground truths (versioned, cascade-aware)",what:"Validated project facts (architecture, constraints, decisions) \u2014 each truth carries a version, last-verified timestamp, and outbound decision links.",why:"Truths are injected first into every wyrm_context_build call so the AI never works from contradicted assumptions. When a truth changes, dependent decisions auto-invalidate.",tools:["wyrm_truth_set","wyrm_truth_get","wyrm_decided_because","wyrm_decision_upstream","wyrm_decision_downstream","wyrm_decision_invalidate"]},{name:"Reasoning scaffolds",what:'Saved step-by-step checklists for problem types ("debug a slow query", "ship a migration") that surface automatically when a task description matches.',why:"Replaces ad-hoc rediscovery of process knowledge with structured prompts. Cheaper than re-explaining the playbook every session.",tools:["wyrm_scaffold_save","wyrm_scaffold_get"]},{name:"Lossless session rehydration",what:"wyrm_session_rehydrate produces a complete briefing markdown a fresh AI ingests to inherit prior state \u2014 decisions, open quests, failures, recent commits, files touched.",why:"Cross-session continuity without re-exploration. The single biggest token-saver in the toolkit.",tools:["wyrm_session_rehydrate","wyrm_session_prime","wyrm_session_start","wyrm_session_update"]},{name:"Distillation queue",what:"Candidate memory artifacts extracted from sessions land in a review queue; the operator approves or rejects each.",why:"Bayesian confidence updates after each review \u2014 Wyrm gets sharper with use without auto-trusting noise.",tools:["wyrm_distill","wyrm_review","wyrm_remember","wyrm_recall","wyrm_feedback"]},{name:"Knowledge graph",what:"Named entities + typed directional relationships, neighborhood traversal, shortest-path queries.",why:'When the AI asks "how does X relate to Y?", Wyrm answers in O(log n) instead of grepping the corpus.',tools:["wyrm_entity_add","wyrm_entity_link","wyrm_entity_search","wyrm_entity_graph","wyrm_entity_path","wyrm_entity_merge"]},{name:"Cross-repo symbol index",what:"Workspace-wide function/class/type index across TS/JS/Python/Rust/Go/PHP/Ruby with caller resolution.",why:`Symbol lookups don't require re-grepping every project. "Who calls handleAuth?" answers immediately.`,tools:["wyrm_symbol_index","wyrm_symbol_search","wyrm_symbol_callers","wyrm_symbol_stats"]},{name:"OODA agent loop (autonomous goals)",what:"wyrm_goal_set persists an objective with success criteria. The wyrm-loop daemon runs Observe\u2192Orient\u2192Decide\u2192Act every interval, dispatching whitelisted tools. Replayable, capped, auditable.",why:"Lets you hand the AI a multi-step objective and walk away. No data-exfil tools accessible from inside the loop.",tools:["wyrm_goal_set","wyrm_goal_list","wyrm_goal_pause","wyrm_goal_resume","wyrm_goal_complete","wyrm_goal_abandon","wyrm_goal_iterations","wyrm_act","wyrm_agent_init","wyrm_agent_status","wyrm_agent_stop","wyrm_agent_restart"]},{name:"Outbound MCP client",what:"Wyrm calls other MCP servers (GitHub, Slack, Linear, \u2026) over stdio. The agent loop dispatches them like internal tools.",why:"Composability: Wyrm becomes the orchestration layer over your entire MCP stack instead of a sibling.",tools:["wyrm_mcp_register","wyrm_mcp_list","wyrm_mcp_disable","wyrm_mcp_tools","wyrm_call_external"]},{name:"Hybrid semantic search",what:"wyrm_search fuses FTS5 keyword candidates with vector candidates (Ollama nomic-embed-text by default; OpenAI optional) via Reciprocal Rank Fusion.",why:`Catches both "exact term" and "semantically similar" hits in one call. Silently falls back to FTS5-only if Ollama isn't running.`,tools:["wyrm_search","wyrm_vector_setup"]},{name:"Multi-agent presence + work-stealing",what:"Claude, Copilot, Cursor, and Codex announce themselves via TTL heartbeats. Quests can be claimed exclusively.",why:"No two agents step on the same quest. The work-stealing semantics scale to teams.",tools:["wyrm_presence_announce","wyrm_presence_list","wyrm_presence_release","wyrm_quest_claim","wyrm_quest_release"]},{name:"Federated team sync",what:"Per-row is_shared flag, /sync/push endpoint, conflict-as-quest semantics. PII never leaves the box unless explicitly shared.",why:"Team memory without a SaaS. No silent overwrites \u2014 every conflict becomes a quest the operator resolves.",tools:["wyrm_share","wyrm_unshare","wyrm_sync","wyrm_sync_export","wyrm_sync_import","wyrm_sync_conflicts","wyrm_sync_resolve"]},{name:"Compliance audit trail",what:"Hash-chained audit log with optional Ed25519 signatures. wyrm_audit_export produces tamper-evident bundles.",why:"SOC2/HIPAA reviewers get a real artifact. The hash chain detects any tampering after the fact.",tools:["wyrm_audit_log","wyrm_audit_export","wyrm_audit_verify"]},{name:"Hour ledger + invoice generation",what:"Derives billable hours from session content and emits markdown invoices.",why:"For solo operators and small teams: Wyrm is also the time tracker. No second tool to keep in sync.",tools:["wyrm_hours_report","wyrm_invoice_generate","wyrm_cost_report"]},{name:"Self-improvement analytics",what:"Per-tool error rate, p50/p95 latency, cost-per-tool. Wyrm watches Wyrm.",why:"Lets you spot which tools are hot, which are slow, which fail \u2014 input for both ops tuning and product decisions.",tools:["wyrm_analytics_dashboard","wyrm_tool_analytics","wyrm_usage","wyrm_stats"]}];function _(){try{const t=[a(n,"..","package.json"),a(n,"..","..","package.json")];for(const e of t)if(y(e))return JSON.parse(m(e,"utf-8")).version??"unknown"}catch{}return"unknown"}function x(t,e,s){const o=process.env.WYRM_DB_PATH??a(l(),".wyrm","wyrm.db"),i=s.isAgentRunning(),r=h();return{product:"Wyrm",version:_(),pitch:u,install:{dbPath:o,nodeVersion:process.version,platform:`${process.platform}-${process.arch}`},runtime:{vectorProvider:s.detectVectorProvider?s.detectVectorProvider():r.resolved,vectorReason:r.reason,vectorFix:r.fix,encryption:s.hasEncryption(),federation:s.isFederationEnabled(),agentLoop:i===null?"unknown":i?"running":"stopped"},features:p,toolCount:e}}function S(t){const e=[];e.push(`# \u{F115D} ${t.product} ${t.version} \u2014 Capability Briefing`),e.push(""),e.push(t.pitch),e.push("");const s=t.runtime.vectorProvider==="none"?"vectors OFF (FTS-only recall)":`${t.runtime.vectorProvider} embeddings`;e.push(`**Runtime:** ${t.toolCount} MCP tools \xB7 ${s} \xB7 ${t.runtime.encryption?"encryption ON":"encryption off"} \xB7 agent ${t.runtime.agentLoop}`),t.runtime.vectorReason&&(e.push(""),e.push(`> WARNING: semantic recall degraded (${t.runtime.vectorReason}) - recall is FTS5 keyword-only. Fix: ${t.runtime.vectorFix}`)),e.push(`**Install:** ${t.install.platform} \xB7 Node ${t.install.nodeVersion} \xB7 DB ${t.install.dbPath}`),e.push(""),e.push('## What Wyrm gives you beyond "AI memory"'),e.push("");for(const o of t.features)e.push(`### ${o.name}`),e.push(`- **What:** ${o.what}`),e.push(`- **Why:** ${o.why}`),e.push(`- **Tools:** \`${o.tools.join("`, `")}\``),e.push("");return e.join(`
|
|
15
|
+
`)}export{x as getCapabilities,S as renderCapabilityBriefing};
|
package/dist/cloud/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{randomBytes as
|
|
1
|
+
import{randomBytes as I}from"node:crypto";import{writeFileSync as N}from"node:fs";import{hostname as P}from"node:os";import{resolve as R,dirname as L,join as A}from"node:path";import{fileURLToPath as O}from"node:url";import{createInterface as W}from"node:readline";import{CloudClient as d,CloudError as b,DEFAULT_BASE as p,SESSION_FILE as f,loadSession as i,saveSession as _,clearSession as E}from"./client.js";import{getMasterKey as M,keyExists as h,keyFilePath as g}from"./crypto.js";import{computeMachineFp as x,classifySession as F}from"./machine-id.js";import{runSync as B,CopiedSessionError as U}from"./sync-engine.js";import{SovereignSyncManager as j}from"./sync-daemon.js";import{initializeLicense as G,hasFeature as H,getTier as K}from"../license.js";function w(){G(),H("cloud_backup")||(console.error("\u{F115D} Wyrm Cloud sync requires a Pro license or higher."),console.error(` Current tier: ${K()}`),console.error(" Upgrade: https://ghosts.lk/wyrm"),console.error(" Already paid? Activate your license (wyrm_license tool, or place"),console.error(" the signed JSON at ~/.wyrm/license.json) and retry."),process.exit(1))}function v(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KiB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MiB`:`${(e/(1024*1024*1024)).toFixed(2)} GiB`}function $(e){return e?new Date(e).toISOString().replace("T"," ").slice(0,16)+" UTC":"\u2014"}async function fe(e){const t=e[0],n=e.slice(1);try{switch(t){case void 0:case"help":case"--help":case"-h":return Y();case"login":return await q(n);case"logout":return await z();case"status":return await J();case"doctor":return Q();case"devices":return await V(n);case"sync":return await X(n);case"daemon":return await ee(n);case"export":return await oe(n);case"delete":return await se(n);case"recovery":return await ne(n);default:console.error(`Unknown subcommand: wyrm cloud ${t}`),console.error("Try `wyrm cloud help`"),process.exit(1)}}catch(o){if(o instanceof U&&process.exit(1),o instanceof b)o.status===401?console.error("\u2718 Not authenticated. Run `wyrm cloud login`."):o.status===403?console.error(`\u2718 Forbidden: ${o.message}`):o.status===0?(console.error(`\u2718 ${o.message}`),console.error(" Check your internet connection. If wyrm.ghosts.lk is unreachable, see status at https://www.cloudflarestatus.com")):o.status===501?console.error(`\u2718 Service not configured: ${o.message}`):console.error(`\u2718 ${o.message} (HTTP ${o.status})`);else if(o instanceof Error){const r=o.message;r.includes("Not logged in")?console.error("\u2718 Not logged in. Run `wyrm cloud login`."):r.includes("Wyrm Cloud key")?console.error(`\u2718 Encryption-key error: ${r}`):console.error(`\u2718 ${r}`)}else console.error("\u2718 Unexpected error:",o);process.exit(1)}}function Y(){console.log(`\u{F115D} wyrm cloud \u2014 sync your Wyrm memory across devices
|
|
2
2
|
|
|
3
3
|
USAGE:
|
|
4
4
|
wyrm cloud <subcommand> [args]
|
|
@@ -15,6 +15,10 @@ SUBCOMMANDS:
|
|
|
15
15
|
visibility opt-in). Skips telemetry / FTS / logs.
|
|
16
16
|
--force proceeds past a copied-session warning
|
|
17
17
|
(or set WYRM_ALLOW_COPIED_SESSION=1).
|
|
18
|
+
daemon <start|stop|restart|status> [--interval=<min>]
|
|
19
|
+
Hands-off sovereign sync: run 'sync' every interval
|
|
20
|
+
(default 10 min) as a background daemon. Logs to
|
|
21
|
+
~/.wyrm/wyrm-sovereign-sync.log.
|
|
18
22
|
doctor Diagnose this device's cloud identity (device_id,
|
|
19
23
|
machine fingerprint match, copied-session check).
|
|
20
24
|
export [path] Download full account dump (deltas stay encrypted).
|
|
@@ -32,6 +36,6 @@ CONSTITUTION:
|
|
|
32
36
|
|
|
33
37
|
DOMAIN:
|
|
34
38
|
https://wyrm.ghosts.lk
|
|
35
|
-
`)}async function
|
|
36
|
-
\u2718 Login failed: ${
|
|
37
|
-
\u26A0 Logged in but could not register device: ${t instanceof Error?t.message:t}`),console.error(" Re-run `wyrm cloud login` to retry.");return}console.log(""),console.log(`\u2713 Signed in as ${y}`),console.log(`\u2713 Device registered: ${m}`),console.log(`\u2713 Session saved to ${f}`);const C=!h();R(),C?(console.log(`\u2713 Encryption key generated at ${g()} (0600)`),console.log(""),console.log(" \u26A0 Back up the key file. Losing it means losing access to your synced data."),console.log(" The Wyrm Cloud server CANNOT recover it \u2014 that's the operator-owns-data"),console.log(" guarantee. Run `wyrm cloud recovery show` now to get a 24-word phrase,"),console.log(" or copy the key file to a password manager / encrypted backup.")):console.log(`\u2713 Encryption key reused from ${g()}`),console.log(""),console.log("Next: `wyrm cloud sync` to push your memory.")}async function K(){const e=d();if(!e){console.log("Not logged in.");return}try{await new a(e.base,e.session).logout(),console.log("\u2713 Session revoked server-side.")}catch(n){console.error(`\u26A0 Could not revoke server-side: ${n instanceof Error?n.message:n}`),console.error(" Local session cleared anyway.")}b(),console.log(`\u2713 Local session removed (${f}).`),console.log(" Note: ~/.wyrm/cloud.key kept. Delete manually if you want a clean slate.")}async function H(){const e=d();if(!e){console.log("Not logged in. Run `wyrm cloud login`.");return}const n=new a(e.base,e.session);try{const s=await n.me(),o=await n.syncStatus();console.log(""),console.log(`\u{F115D} Wyrm Cloud \u2014 ${s.account.email}`),console.log(""),console.log(` Tier: ${o.tier}`),console.log(` Storage: ${w(o.storage.used_bytes)} / ${w(o.storage.limit_bytes)} (${o.storage.percent}%)`),console.log(` Deltas: ${o.deltas}`),console.log(` Devices: ${o.devices}`),console.log(` Providers:${s.identities.map(r=>" "+r.provider).join(",")}`),console.log(""),console.log(` This machine: ${e.device_name??"(not registered)"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none)"}`),console.log(` Session age: ${Math.round((Date.now()-e.created_at)/(1440*60*1e3))} day(s)`),x(e),console.log(""),h()?o.deltas>0&&(console.log(` Key: ${g()} (0600)`),console.log(` \u26A0 Back this file up. Losing it = losing access to your ${o.deltas} deltas.`),console.log("")):(console.log(" \u26A0 ENCRYPTION KEY MISSING"),console.log(` Expected at: ${g()}`),console.log(" Without this file, deltas already pushed cannot be decrypted."),console.log(" If you have a backup, restore it before running sync."),console.log(""))}catch(s){throw s instanceof k&&s.status===401&&(console.error("Session expired or invalid. Run `wyrm cloud login` again."),process.exit(1)),s}}function x(e){const n=O(e.machine_fp);n.state==="match"?console.log(" Identity: \u2713 machine fingerprint matches (not a copied session)"):n.state==="adopt"?console.log(" Identity: \u2014 no fingerprint yet (pre-7.0.3 session; adopted on next sync)"):(console.log(" Identity: \u26A0 FINGERPRINT MISMATCH \u2014 this session looks COPIED"),console.log(` stored ${n.stored.slice(0,12)}\u2026 vs this machine ${n.current.slice(0,12)}\u2026`),console.log(" device_id collision \u2192 cross-device pull returns 0 silently."),console.log(" Fix: rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json (KEEP cloud.key),"),console.log(" then `wyrm cloud login`. (Or `wyrm cloud sync --force` if intended.)"))}function Y(){const e=d();if(console.log(""),console.log("\u{F115D} Wyrm Cloud \u2014 device identity doctor"),console.log(""),!e){console.log(" Not logged in. Run `wyrm cloud login`."),console.log("");return}console.log(` Account: ${e.email??e.account_id.slice(0,8)+"\u2026"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none \u2014 re-run `wyrm cloud login`)"}`),console.log(` Server: ${e.base}`),console.log(` Stored fp: ${e.machine_fp?e.machine_fp.slice(0,12)+"\u2026":"(none \u2014 pre-7.0.3 session)"}`),console.log(` This box fp: ${E().slice(0,12)}\u2026`),x(e),console.log(""),console.log(" Only ~/.wyrm/cloud.key is meant to be shared across your machines."),console.log(" cloud.json + cloud-cursor.json + machine-id are per-device."),console.log("")}async function q(e){const n=e[0],s=a.fromSession();if(n==="revoke"){const c=e[1];c||(console.error("Usage: wyrm cloud devices revoke <device-id>"),process.exit(1)),await s.revokeDevice(c),console.log(`\u2713 Device ${c.slice(0,8)}\u2026 revoked.`);return}const{devices:o}=await s.listDevices(),l=d()?.device_id;console.log(""),console.log(`\u{F115D} Devices (${o.length})`),console.log("");for(const c of o){const i=c.id===l?"\u2605":" ",u=c.revoked?"(revoked)":"";console.log(` ${i} ${c.id.slice(0,8)}\u2026 ${c.name.padEnd(30)} last_seen=${G(c.last_seen_at)} ${u}`)}console.log(""),console.log(" \u2605 = this machine"),console.log(" Revoke: wyrm cloud devices revoke <device-id>"),console.log("")}async function z(e){T();const n=e.includes("--dry-run"),s=e.includes("--all"),o=e.includes("--force");h()||(console.error("\u26A0 Encryption key not found at",g()),console.error(" A new key will be generated, but it will NOT decrypt deltas"),console.error(" pushed by other devices using a different key. If you have a backup,"),console.error(" restore it to ~/.wyrm/cloud.key (0600) before running sync."),console.error(" Continuing in 5 seconds \u2014 press Ctrl+C to abort."),await new Promise(l=>{setTimeout(l,5e3)})),s&&(console.log("\u{1F310} --all mode: syncing every semantic table (ignoring visibility flags)."),console.log(" Skipped: per-device logs, FTS shadows, sync internals."),console.log(""));const r=await L({dryRun:n,all:s,force:o});if(console.log(""),console.log("\u{F115D} Sync complete"),console.log(""),console.log(` Pushed: ${r.pushed} row(s)${n?" (dry run)":""}`),console.log(` Pulled: ${r.pulled} row(s)`),console.log(` Deleted: ${r.deleted_local} row(s) (peer tombstones)`),r.errors.length>0){console.log(""),console.log(" Errors:");for(const l of r.errors)console.log(` - ${l}`)}console.log("")}async function J(e){T();const n=a.fromSession(),s=e.find(c=>!c.startsWith("-")),o=P(s??`wyrm-cloud-export-${new Date().toISOString().slice(0,10)}.json`);console.log(" Fetching account export\u2026");const r=await n.accountExport(),l=JSON.stringify(r,null,2);I(o,l,{mode:384}),console.log(""),console.log(`\u2713 Exported to ${o}`),console.log(""),console.log(` Account: ${r.account.email} (${r.account.id.slice(0,8)}\u2026)`),console.log(` Devices: ${r.devices.length}`),console.log(` Identities: ${r.identities.length}`),console.log(` Orgs: ${r.orgs.length}`),console.log(` Deltas: ${r.deltas.length}`),console.log(` Size: ${w(l.length)}`),console.log(""),console.log(" Deltas are still AES-256-GCM ciphertext. To decrypt them you"),console.log(` need the key at ${g()}. Back that up alongside this file.`),console.log("")}async function Q(e){e.includes("--confirm")||(console.error("\u2718 This permanently deletes your Wyrm Cloud account."),console.error(" Re-run with --confirm to proceed:"),console.error(" wyrm cloud delete --confirm"),process.exit(1));const s=d();s||(console.error("\u2718 Not logged in. Nothing to delete."),process.exit(1)),console.log(""),console.log("\u26A0 You are about to PERMANENTLY DELETE your Wyrm Cloud account."),console.log(""),console.log(` Account: ${s.email??s.account_id}`),console.log(` Server: ${s.base}`),console.log(""),console.log(" This wipes all encrypted blobs, devices, identities, and org"),console.log(" memberships you own. The local ~/.wyrm/cloud.key is kept so"),console.log(" any backups remain decryptable. This action CANNOT be undone."),console.log(""),(await _(' Type "DELETE my account" to confirm: ')).trim()!=="DELETE my account"&&(console.error("\u2718 Confirmation phrase did not match. Aborting."),process.exit(1)),await new a(s.base,s.session).accountDelete(),b(),console.log(""),console.log("\u2713 Account deleted on server."),console.log(`\u2713 Local session removed (${f}).`),console.log(` Kept: ${g()} (in case you need to decrypt an old export).`),console.log("")}function _(e){return new Promise(n=>{const s=A({input:process.stdin,output:process.stdout});s.question(e,o=>{s.close(),n(o)})})}async function V(e){const n=e[0];if(n==="show"){const{keyToMnemonic:s}=await import("./recovery.js"),r=s().split(" ");console.log(""),console.log("\u{1F511} Wyrm Cloud recovery phrase (BIP39, 24 words)"),console.log("");for(let l=0;l<4;l++){const c=r.slice(l*6,l*6+6).map((i,u)=>`${String(l*6+u+1).padStart(2)}. ${i.padEnd(10)}`).join(" ");console.log(` ${c}`)}console.log(""),console.log(" Write these down on PAPER and keep them somewhere safe."),console.log(" Anyone with this phrase can decrypt your synced Wyrm memory."),console.log(" Wyrm Cloud has NO COPY of this. Lose it = lose your data."),console.log("");return}if(n==="restore"){const s=e.includes("--overwrite");console.log(""),console.log("\u{1F511} Restore master key from BIP39 recovery phrase"),console.log(""),console.log(" Enter your 24 words separated by spaces (case-insensitive)."),console.log(" Press Enter when done."),console.log("");const o=await _(" > "),{mnemonicToKey:r}=await import("./recovery.js"),{path:l,overwrote:c}=r(o,{overwrite:s});console.log(""),console.log(`\u2713 Master key restored to ${l} (0600)`),c&&console.log(" Replaced existing key (you passed --overwrite)."),console.log(" Run `wyrm cloud sync` to pull deltas with the restored key."),console.log("");return}console.error("Usage:"),console.error(" wyrm cloud recovery show"),console.error(" wyrm cloud recovery restore [--overwrite]"),process.exit(1)}export{ie as cmdCloud};
|
|
39
|
+
`)}async function q(e){i()&&(console.error("Already logged in. Run `wyrm cloud logout` first to switch accounts."),process.exit(1));const t=new d(p),n=I(32).toString("hex");let o;try{o=await t.cliInit(n)}catch(l){console.error(`\u2718 Could not start login: ${l instanceof Error?l.message:l}`),process.exit(1)}console.log(""),console.log("\u{F115D} Sign in to Wyrm Cloud"),console.log(""),console.log(" 1. Open this URL on any device:"),console.log(` ${o.browser_url}`),console.log(""),console.log(" 2. Type this code when prompted:"),console.log(` ${o.code}`),console.log(""),console.log(" 3. Sign in with Google or GitHub."),console.log(""),console.log(` Waiting (expires in ${Math.round(o.expires_in_sec/60)} min)\u2026`);const s=Date.now()+o.expires_in_sec*1e3;let c,a;for(;Date.now()<s;){await new Promise(l=>{setTimeout(l,3e3)});try{const l=await t.cliPoll(n);if(l.ready&&l.session){c=l.session,a=l.account_id;break}}catch(l){console.error(`
|
|
40
|
+
\u2718 Login failed: ${l instanceof Error?l.message:l}`),process.exit(1)}}(!c||!a)&&(console.error("\n\u2718 Login timed out. Run `wyrm cloud login` again."),process.exit(1)),_({base:p,session:c,account_id:a,created_at:Date.now()});const u=new d(p,c);let y="";try{y=(await u.me()).account.email}catch{}const m=`${P()} (${process.platform})`;let k;try{k=(await u.registerDevice(m)).device.id;const S=i();S&&_({...S,email:y,device_id:k,device_name:m,machine_fp:x()})}catch(l){console.error(`
|
|
41
|
+
\u26A0 Logged in but could not register device: ${l instanceof Error?l.message:l}`),console.error(" Re-run `wyrm cloud login` to retry.");return}console.log(""),console.log(`\u2713 Signed in as ${y}`),console.log(`\u2713 Device registered: ${m}`),console.log(`\u2713 Session saved to ${f}`);const D=!h();M(),D?(console.log(`\u2713 Encryption key generated at ${g()} (0600)`),console.log(""),console.log(" \u26A0 Back up the key file. Losing it means losing access to your synced data."),console.log(" The Wyrm Cloud server CANNOT recover it \u2014 that's the operator-owns-data"),console.log(" guarantee. Run `wyrm cloud recovery show` now to get a 24-word phrase,"),console.log(" or copy the key file to a password manager / encrypted backup.")):console.log(`\u2713 Encryption key reused from ${g()}`),console.log(""),console.log("Next: `wyrm cloud sync` to push your memory.")}async function z(){const e=i();if(!e){console.log("Not logged in.");return}try{await new d(e.base,e.session).logout(),console.log("\u2713 Session revoked server-side.")}catch(t){console.error(`\u26A0 Could not revoke server-side: ${t instanceof Error?t.message:t}`),console.error(" Local session cleared anyway.")}E(),console.log(`\u2713 Local session removed (${f}).`),console.log(" Note: ~/.wyrm/cloud.key kept. Delete manually if you want a clean slate.")}async function J(){const e=i();if(!e){console.log("Not logged in. Run `wyrm cloud login`.");return}const t=new d(e.base,e.session);try{const n=await t.me(),o=await t.syncStatus();console.log(""),console.log(`\u{F115D} Wyrm Cloud \u2014 ${n.account.email}`),console.log(""),console.log(` Tier: ${o.tier}`),console.log(` Storage: ${v(o.storage.used_bytes)} / ${v(o.storage.limit_bytes)} (${o.storage.percent}%)`),console.log(` Deltas: ${o.deltas}`),console.log(` Devices: ${o.devices}`),console.log(` Providers:${n.identities.map(r=>" "+r.provider).join(",")}`),console.log(""),console.log(` This machine: ${e.device_name??"(not registered)"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none)"}`),console.log(` Session age: ${Math.round((Date.now()-e.created_at)/(1440*60*1e3))} day(s)`),T(e),console.log(""),h()?o.deltas>0&&(console.log(` Key: ${g()} (0600)`),console.log(` \u26A0 Back this file up. Losing it = losing access to your ${o.deltas} deltas.`),console.log("")):(console.log(" \u26A0 ENCRYPTION KEY MISSING"),console.log(` Expected at: ${g()}`),console.log(" Without this file, deltas already pushed cannot be decrypted."),console.log(" If you have a backup, restore it before running sync."),console.log(""))}catch(n){throw n instanceof b&&n.status===401&&(console.error("Session expired or invalid. Run `wyrm cloud login` again."),process.exit(1)),n}}function T(e){const t=F(e.machine_fp);t.state==="match"?console.log(" Identity: \u2713 machine fingerprint matches (not a copied session)"):t.state==="adopt"?console.log(" Identity: \u2014 no fingerprint yet (pre-7.0.3 session; adopted on next sync)"):(console.log(" Identity: \u26A0 FINGERPRINT MISMATCH \u2014 this session looks COPIED"),console.log(` stored ${t.stored.slice(0,12)}\u2026 vs this machine ${t.current.slice(0,12)}\u2026`),console.log(" device_id collision \u2192 cross-device pull returns 0 silently."),console.log(" Fix: rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json (KEEP cloud.key),"),console.log(" then `wyrm cloud login`. (Or `wyrm cloud sync --force` if intended.)"))}function Q(){const e=i();if(console.log(""),console.log("\u{F115D} Wyrm Cloud \u2014 device identity doctor"),console.log(""),!e){console.log(" Not logged in. Run `wyrm cloud login`."),console.log("");return}console.log(` Account: ${e.email??e.account_id.slice(0,8)+"\u2026"}`),console.log(` Device ID: ${e.device_id?e.device_id.slice(0,8)+"\u2026":"(none \u2014 re-run `wyrm cloud login`)"}`),console.log(` Server: ${e.base}`),console.log(` Stored fp: ${e.machine_fp?e.machine_fp.slice(0,12)+"\u2026":"(none \u2014 pre-7.0.3 session)"}`),console.log(` This box fp: ${x().slice(0,12)}\u2026`),T(e),console.log(""),console.log(" Only ~/.wyrm/cloud.key is meant to be shared across your machines."),console.log(" cloud.json + cloud-cursor.json + machine-id are per-device."),console.log("")}async function V(e){const t=e[0],n=d.fromSession();if(t==="revoke"){const c=e[1];c||(console.error("Usage: wyrm cloud devices revoke <device-id>"),process.exit(1)),await n.revokeDevice(c),console.log(`\u2713 Device ${c.slice(0,8)}\u2026 revoked.`);return}const{devices:o}=await n.listDevices(),s=i()?.device_id;console.log(""),console.log(`\u{F115D} Devices (${o.length})`),console.log("");for(const c of o){const a=c.id===s?"\u2605":" ",u=c.revoked?"(revoked)":"";console.log(` ${a} ${c.id.slice(0,8)}\u2026 ${c.name.padEnd(30)} last_seen=${$(c.last_seen_at)} ${u}`)}console.log(""),console.log(" \u2605 = this machine"),console.log(" Revoke: wyrm cloud devices revoke <device-id>"),console.log("")}async function X(e){w();const t=e.includes("--dry-run"),n=e.includes("--all"),o=e.includes("--force");h()||(console.error("\u26A0 Encryption key not found at",g()),console.error(" A new key will be generated, but it will NOT decrypt deltas"),console.error(" pushed by other devices using a different key. If you have a backup,"),console.error(" restore it to ~/.wyrm/cloud.key (0600) before running sync."),console.error(" Continuing in 5 seconds \u2014 press Ctrl+C to abort."),await new Promise(s=>{setTimeout(s,5e3)})),n&&(console.log("\u{1F310} --all mode: syncing every semantic table (ignoring visibility flags)."),console.log(" Skipped: per-device logs, FTS shadows, sync internals."),console.log(""));const r=await B({dryRun:t,all:n,force:o});if(console.log(""),console.log("\u{F115D} Sync complete"),console.log(""),console.log(` Pushed: ${r.pushed} row(s)${t?" (dry run)":""}`),console.log(` Pulled: ${r.pulled} row(s)`),console.log(` Deleted: ${r.deleted_local} row(s) (peer tombstones)`),r.errors.length>0){console.log(""),console.log(" Errors:");for(const s of r.errors)console.log(` - ${s}`)}console.log("")}function Z(){const e=A(L(O(import.meta.url)),"sync-daemon-entrypoint.js");return new j(e)}async function ee(e){w();const t=e[0]??"status",n=e.find(s=>s.startsWith("--interval=")),o=n?Number(n.split("=")[1]):void 0,r=Z();switch(t){case"start":{i()||(console.error("\u2718 Not logged in. Run `wyrm cloud login` first."),process.exit(1));const s=r.start({interval_minutes:o});s.ok||(console.error(`\u{F115D} sovereign-sync daemon: ${s.reason??"failed to start"}${s.pid?` (pid ${s.pid})`:""}`),process.exit(s.pid?0:1)),console.log(`\u{F115D} \u2713 sovereign-sync daemon started (pid ${s.pid}). Logs: ~/.wyrm/wyrm-sovereign-sync.log`);return}case"stop":{const s=r.stop();console.log(s.ok?"\u{F115D} \u2713 sovereign-sync daemon stopped":`\u{F115D} sovereign-sync daemon: ${s.reason??"failed to stop"}`);return}case"restart":{r.stop(),await new Promise(c=>{setTimeout(c,500)});const s=r.start({interval_minutes:o});console.log(s.ok?`\u{F115D} \u2713 sovereign-sync daemon restarted (pid ${s.pid})`:`\u{F115D} restart failed: ${s.reason??"unknown"}`);return}case"status":{const s=r.status();console.log("\u{F115D} Sovereign-sync daemon"),console.log(` Running: ${s.running?`\u2713 yes (pid ${s.pid})`:"\u2717 no"}`),s.state?.started_ts&&console.log(` Started: ${$(s.state.started_ts)}`),s.state?.last_tick_ts&&(console.log(` Last tick: ${$(s.state.last_tick_ts)} (exit ${s.state.last_tick_exit??"\u2014"})`),s.state.last_tick_summary&&console.log(` Last result: ${s.state.last_tick_summary}`));return}default:console.error(`Unknown daemon action: ${t}. Use: start, stop, restart, status`),process.exit(1)}}async function oe(e){w();const t=d.fromSession(),n=e.find(c=>!c.startsWith("-")),o=R(n??`wyrm-cloud-export-${new Date().toISOString().slice(0,10)}.json`);console.log(" Fetching account export\u2026");const r=await t.accountExport(),s=JSON.stringify(r,null,2);N(o,s,{mode:384}),console.log(""),console.log(`\u2713 Exported to ${o}`),console.log(""),console.log(` Account: ${r.account.email} (${r.account.id.slice(0,8)}\u2026)`),console.log(` Devices: ${r.devices.length}`),console.log(` Identities: ${r.identities.length}`),console.log(` Orgs: ${r.orgs.length}`),console.log(` Deltas: ${r.deltas.length}`),console.log(` Size: ${v(s.length)}`),console.log(""),console.log(" Deltas are still AES-256-GCM ciphertext. To decrypt them you"),console.log(` need the key at ${g()}. Back that up alongside this file.`),console.log("")}async function se(e){e.includes("--confirm")||(console.error("\u2718 This permanently deletes your Wyrm Cloud account."),console.error(" Re-run with --confirm to proceed:"),console.error(" wyrm cloud delete --confirm"),process.exit(1));const n=i();n||(console.error("\u2718 Not logged in. Nothing to delete."),process.exit(1)),console.log(""),console.log("\u26A0 You are about to PERMANENTLY DELETE your Wyrm Cloud account."),console.log(""),console.log(` Account: ${n.email??n.account_id}`),console.log(` Server: ${n.base}`),console.log(""),console.log(" This wipes all encrypted blobs, devices, identities, and org"),console.log(" memberships you own. The local ~/.wyrm/cloud.key is kept so"),console.log(" any backups remain decryptable. This action CANNOT be undone."),console.log(""),(await C(' Type "DELETE my account" to confirm: ')).trim()!=="DELETE my account"&&(console.error("\u2718 Confirmation phrase did not match. Aborting."),process.exit(1)),await new d(n.base,n.session).accountDelete(),E(),console.log(""),console.log("\u2713 Account deleted on server."),console.log(`\u2713 Local session removed (${f}).`),console.log(` Kept: ${g()} (in case you need to decrypt an old export).`),console.log("")}function C(e){return new Promise(t=>{const n=W({input:process.stdin,output:process.stdout});n.question(e,o=>{n.close(),t(o)})})}async function ne(e){const t=e[0];if(t==="show"){const{keyToMnemonic:n}=await import("./recovery.js"),r=n().split(" ");console.log(""),console.log("\u{1F511} Wyrm Cloud recovery phrase (BIP39, 24 words)"),console.log("");for(let s=0;s<4;s++){const c=r.slice(s*6,s*6+6).map((a,u)=>`${String(s*6+u+1).padStart(2)}. ${a.padEnd(10)}`).join(" ");console.log(` ${c}`)}console.log(""),console.log(" Write these down on PAPER and keep them somewhere safe."),console.log(" Anyone with this phrase can decrypt your synced Wyrm memory."),console.log(" Wyrm Cloud has NO COPY of this. Lose it = lose your data."),console.log("");return}if(t==="restore"){const n=e.includes("--overwrite");console.log(""),console.log("\u{1F511} Restore master key from BIP39 recovery phrase"),console.log(""),console.log(" Enter your 24 words separated by spaces (case-insensitive)."),console.log(" Press Enter when done."),console.log("");const o=await C(" > "),{mnemonicToKey:r}=await import("./recovery.js"),{path:s,overwrote:c}=r(o,{overwrite:n});console.log(""),console.log(`\u2713 Master key restored to ${s} (0600)`),c&&console.log(" Replaced existing key (you passed --overwrite)."),console.log(" Run `wyrm cloud sync` to pull deltas with the restored key."),console.log("");return}console.error("Usage:"),console.error(" wyrm cloud recovery show"),console.error(" wyrm cloud recovery restore [--overwrite]"),process.exit(1)}export{fe as cmdCloud};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{spawn as a}from"child_process";import{fileURLToPath as l}from"url";import{dirname as m,join as p}from"path";import{loadSession as d}from"./client.js";import{writeSovereignState as n,DEFAULT_SOVEREIGN_INTERVAL_MS as g}from"./sync-daemon.js";const u=p(m(l(import.meta.url)),"..","wyrm-cli.js");function i(e){process.stdout.write(`[${new Date().toISOString()}] ${e}
|
|
2
|
+
`)}function c(){return new Promise(e=>{const s=a(process.execPath,[u,"cloud","sync"],{stdio:["ignore","pipe","pipe"],env:{...process.env}});let o="";s.stdout.on("data",t=>{o+=t.toString()}),s.stderr.on("data",t=>{o+=t.toString()}),s.on("close",t=>{const r=o.replace(/\s+/g," ").trim().slice(0,300);i(`tick exit=${t}: ${r}`),n({last_tick_ts:Date.now(),last_tick_exit:t,last_tick_summary:r}),e()}),s.on("error",t=>{i(`tick spawn error: ${t.message}`),n({last_tick_ts:Date.now(),last_tick_exit:null,last_tick_summary:`spawn error: ${t.message}`}),e()})})}async function _(){const e=Number(process.argv[2])||g,s=d();(!s||!s.device_id)&&(console.error("[sovereign-sync] device not logged in (run `wyrm cloud login`) \u2014 refusing to start"),process.exit(2)),i(`sovereign-sync started \xB7 interval=${e}ms \xB7 device=${s.device_id.slice(0,8)}\u2026`),await c();const o=setInterval(()=>{c()},e),t=r=>{clearInterval(o),i(`${r} \u2014 exiting`),process.exit(0)};process.on("SIGTERM",()=>t("SIGTERM")),process.on("SIGINT",()=>t("SIGINT"))}_().catch(e=>{console.error("[sovereign-sync] fatal:",e),process.exit(1)});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawn as g}from"child_process";import{existsSync as o,readFileSync as d,writeFileSync as p,mkdirSync as E,openSync as y,closeSync as S,unlinkSync as m,statSync as _,renameSync as v}from"fs";import{homedir as k}from"os";import{join as a}from"path";const n=a(k(),".wyrm"),c=a(n,"wyrm-sovereign-sync.pid"),t=a(n,"wyrm-sovereign-sync.log"),u=a(n,"wyrm-sovereign-sync.state"),I=1e6,L=0,P=600*1e3;class x{daemonEntrypoint;constructor(e){this.daemonEntrypoint=e,h()}start(e={}){if(this.isAlive())return{ok:!1,pid:this.readPid(),reason:"already running"};N();const i=(e.interval_minutes??P/6e4)*6e4,f=y(t,"a"),l=y(t,"a"),s=g(process.execPath,[this.daemonEntrypoint,String(i)],{detached:!0,stdio:["ignore",f,l],env:{...process.env}});return s.unref(),S(f),S(l),typeof s.pid!="number"?{ok:!1,reason:"spawn returned no pid"}:(p(c,String(s.pid)),w({started_ts:Date.now()}),{ok:!0,pid:s.pid})}stop(){if(!this.isAlive())return this.cleanPid(),{ok:!1,reason:"not running"};const e=this.readPid();if(!e)return{ok:!1,reason:"pid file unreadable"};try{return process.kill(e,"SIGTERM"),{ok:!0}}catch(i){return{ok:!1,reason:i?.message??String(i)}}}status(){const e=this.isAlive();return{running:e,pid:e?this.readPid():void 0,state:o(u)?A():void 0}}isAlive(){if(!o(c))return!1;const e=this.readPid();if(!e)return!1;try{return process.kill(e,L),!0}catch{return!1}}readPid(){try{const e=Number(d(c,"utf8").trim());return Number.isFinite(e)&&e>0?e:void 0}catch{return}}cleanPid(){try{m(c)}catch{}}}function w(r){h();let e={};try{e=JSON.parse(d(u,"utf8"))}catch{}p(u,JSON.stringify({...e,...r},null,2))}function h(){o(n)||E(n,{recursive:!0})}function A(){try{return JSON.parse(d(u,"utf8"))}catch{return}}function N(){if(o(t))try{if(_(t).size>I){const r=`${t}.1`;try{m(r)}catch{}v(t,r)}}catch{}}export{P as DEFAULT_SOVEREIGN_INTERVAL_MS,x as SovereignSyncManager,w as writeSovereignState};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{readFileSync as O,writeFileSync as k,existsSync as D}from"node:fs";import{join as I}from"node:path";import{WyrmDB as
|
|
1
|
+
import{readFileSync as O,writeFileSync as k,existsSync as D}from"node:fs";import{join as I}from"node:path";import{WyrmDB as L,resolveDbPath as C}from"../database.js";import{CloudClient as $,loadSession as A,saveSession as v,resolveCloudDir as j}from"./client.js";import{encrypt as S,decrypt as U}from"./crypto.js";import{classifySession as x,computeMachineFp as P}from"./machine-id.js";function T(){return I(j(),"cloud-cursor.json")}const E=[{kind:"truth",table:"ground_truths",hasVisibility:!0},{kind:"memory_artifact",table:"memory_artifacts",hasVisibility:!0},{kind:"quest",table:"quests",hasVisibility:!0},{kind:"design_token",table:"design_tokens",hasVisibility:!0},{kind:"design_reference",table:"design_references",hasVisibility:!0},{kind:"skill",table:"skills",hasVisibility:!0}],W=new Set(["sessions","agent_actions","agent_presence","session_seen_artifacts","tool_call_log","llm_query_log","external_call_log","token_savings_log","cost_tracking","usage_events","audit_log","indexing_queue","vectors","symbol_index","update_check","sync_conflicts","sync_log","sync_tombstones","wyrm_cloud_sync_state","mcp_client_configs","watch_dirs","global_context","schema_versions","failure_patterns"]),F=/_fts(_config|_data|_docsize|_idx)?$/;function M(e){const t=e.getDatabase(),s=[...E],r=new Set(E.map(o=>o.table));try{const o=t.prepare(`SELECT name FROM sqlite_master
|
|
2
2
|
WHERE type='table' AND name NOT LIKE 'sqlite_%'`).all();for(const n of o){const l=n.name;if(r.has(l)||W.has(l)||F.test(l)||!h.test(l))continue;const _=t.prepare(`PRAGMA table_info(${l})`).all();if(!_.some(c=>c.name==="id"))continue;const d=_.some(c=>c.name==="cross_project_visibility");s.push({kind:l,table:l,hasVisibility:d})}}catch{}return s}function ie(e){return e}function H(e,t){if(!h.test(t))return new Set;try{const r=e.getDatabase().prepare(`PRAGMA table_info(${t})`).all();return new Set(r.filter(o=>h.test(o.name)).map(o=>o.name))}catch{return new Set}}const h=/^[a-z_][a-z0-9_]*$/i;function V(){const e={pull_cursor:0,last_push_updated_ms:0,last_push_at:0,last_pull_at:0},t=T();if(!D(t))return e;try{const s=JSON.parse(O(t,"utf-8"));return{...e,...s}}catch{return e}}function q(e){k(T(),JSON.stringify(e,null,2),{mode:384})}function N(e){e.getDatabase().exec(`
|
|
3
3
|
CREATE TABLE IF NOT EXISTS sync_tombstones (
|
|
4
4
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -11,9 +11,9 @@ import{readFileSync as O,writeFileSync as k,existsSync as D}from"node:fs";import
|
|
|
11
11
|
CREATE INDEX IF NOT EXISTS idx_sync_tombstones_pending
|
|
12
12
|
ON sync_tombstones(pushed_at_ms);
|
|
13
13
|
`)}function ae(e,t,s){if(typeof t!="string"||t.length===0||t.length>64)return;N(e),e.getDatabase().prepare(`INSERT OR IGNORE INTO sync_tombstones (kind, row_id, deleted_at_ms)
|
|
14
|
-
VALUES (?, ?, ?)`).run(t,String(s),Date.now())}class Y extends Error{constructor(t){super(t),this.name="CopiedSessionError"}}function G(e){return!!e||process.env.WYRM_ALLOW_COPIED_SESSION==="1"}function B(e,t,s){const r=
|
|
14
|
+
VALUES (?, ?, ?)`).run(t,String(s),Date.now())}class Y extends Error{constructor(t){super(t),this.name="CopiedSessionError"}}function G(e){return!!e||process.env.WYRM_ALLOW_COPIED_SESSION==="1"}function B(e,t,s){const r=x(e.machine_fp);if(r.state==="match")return;if(r.state==="adopt"){t(P());return}const o=["","\u26A0 WYRM CLOUD: this session looks COPIED from another machine.","",` Stored machine fingerprint: ${r.stored.slice(0,12)}\u2026`,` This machine's fingerprint: ${r.current.slice(0,12)}\u2026`,""," Both machines now share ONE device_id. The cloud pull query skips a"," device's own deltas, so each machine filters the OTHER's changes out:"," push works but pull silently returns 0 rows (peer memory looks MISSING).",""," FIX \u2014 give this machine its own identity (keep your encryption key):"," rm ~/.wyrm/cloud.json ~/.wyrm/cloud-cursor.json # NOT cloud.key"," wyrm cloud login",""," If this is a legitimate change (e.g. you renamed this host), re-run with:"," wyrm cloud sync --force (or set WYRM_ALLOW_COPIED_SESSION=1)",""];if(G(s)){console.error(o.join(`
|
|
15
15
|
`)),console.error(" Proceeding anyway (--force / WYRM_ALLOW_COPIED_SESSION=1); fingerprint updated."),console.error(""),t(r.current);return}throw console.error(o.join(`
|
|
16
|
-
`)),new Y("Cloud session looks copied from another machine (device_id collision). Re-login or pass --force.")}async function ce(e={}){const t=A();if(!t||!t.device_id)throw new Error("Not logged in or device not registered. Run `wyrm cloud login`.");B(t,i=>{v({...t,machine_fp:i})},e.force);const s=new
|
|
16
|
+
`)),new Y("Cloud session looks copied from another machine (device_id collision). Re-login or pass --force.")}async function ce(e={}){const t=A();if(!t||!t.device_id)throw new Error("Not logged in or device not registered. Run `wyrm cloud login`.");B(t,i=>{v({...t,machine_fp:i})},e.force);const s=new $(t.base,t.session),r=V(),o=new L(C());N(o);const n={pushed:0,pulled:0,deleted_local:0,errors:[]},l=e.all?M(o):E,_=K(o,l,r.last_push_updated_ms,!!e.all,n.errors),d=X(o),c=[..._.map(i=>({kind:i.kind,row_id:String(i.row_id),visibility:i.visibility,payload_b64:S(JSON.stringify(i.row)),updated_at:i.updated_at})),...d.map(i=>({kind:"tombstone",row_id:`${i.kind}:${i.row_id}`,visibility:"org",payload_b64:S(JSON.stringify({kind:i.kind,row_id:i.row_id,deleted_at_ms:i.deleted_at_ms})),updated_at:i.deleted_at_ms}))];if(c.length>0&&!e.dryRun){let m=r.last_push_updated_ms;for(let a=0;a<c.length;a+=25){const p=c.slice(a,a+25);try{const u=await s.syncPush(t.device_id,p);if(n.pushed+=u.accepted,u.rejected>0)for(const f of u.rejected_details)n.errors.push(`push: ${f.reason} on ${f.id.slice(0,8)}\u2026`);for(const f of p)f.updated_at&&f.updated_at>m&&(m=f.updated_at)}catch(u){n.errors.push(`push chunk failed: ${u instanceof Error?u.message:u}`)}}r.last_push_updated_ms=m,r.last_push_at=Date.now(),d.length>0&&z(o,d.map(a=>a.id))}else n.pushed=c.length;if(!e.dryRun){let i=r.pull_cursor,m=0;e:for(;m<50;){let a;try{a=await s.syncPull(t.device_id,i,100)}catch(p){n.errors.push(`pull failed: ${p instanceof Error?p.message:p}`);break}if(a.deltas.length===0)break;for(const p of a.deltas)try{const u=U(p.payload_b64);if(p.kind==="tombstone"){const{kind:f,row_id:b}=JSON.parse(u);Z(o,l,f,b)&&n.deleted_local++}else{const f=JSON.parse(u);Q(o,l,p.kind,p.row_id,f),n.pulled++}}catch(u){n.errors.push(`apply ${p.kind}:${p.row_id}: ${u instanceof Error?u.message:u}`);break e}if(i=a.next_cursor,!a.has_more)break;m++}r.pull_cursor=i,r.last_pull_at=Date.now()}return q(r),n}function J(e){if(typeof e!="string"||e.length===0)return 0;const t=e.includes("T")?e:e.replace(" ","T")+"Z",s=Date.parse(t);return Number.isFinite(s)?s:0}function g(e,t,s){try{return e.getDatabase().prepare(`PRAGMA table_info(${t})`).all().some(n=>n.name===s)}catch{return!1}}function K(e,t,s,r,o){const n=[],l=e.getDatabase(),_=g(e,"projects","sync_policy");for(const d of t)if(h.test(d.table)&&!(!r&&!d.hasVisibility))try{const c=g(e,d.table,"updated_at"),i=d.hasVisibility?"WHERE cross_project_visibility IN ('org', 'public')":"WHERE 1=1",m=_&&g(e,d.table,"project_id")?"AND (project_id IS NULL OR project_id IN (SELECT id FROM projects WHERE sync_policy IN ('cloud', 'team')))":"",a=c?"AND (? = 0 OR updated_at IS NULL OR strftime('%s', updated_at) * 1000 > ?)":"",p=c?"ORDER BY (updated_at IS NULL) ASC, updated_at ASC":"",u=`SELECT * FROM ${d.table} ${i} ${m} ${a} ${p}`,f=l.prepare(u),b=c?f.all(s,s):f.all();for(const y of b){const w=J(y.updated_at);if(s>0&&w>0&&w<=s)continue;const R=d.hasVisibility?String(y.cross_project_visibility??"within"):"org";n.push({kind:d.kind,row_id:y.id??"?",visibility:R,row:y,updated_at:w||Date.now()})}}catch(c){const i=c instanceof Error?c.message:String(c);if(/no such table/i.test(i))continue;o.push(`collect ${d.table}: ${i}`)}return n.sort((d,c)=>(d.updated_at??0)-(c.updated_at??0)),n}function X(e){const t=e.getDatabase();try{return t.prepare(`SELECT id, kind, row_id, deleted_at_ms
|
|
17
17
|
FROM sync_tombstones
|
|
18
18
|
WHERE pushed_at_ms IS NULL
|
|
19
19
|
ORDER BY id ASC
|
package/dist/database.js
CHANGED
|
@@ -90,11 +90,11 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
|
|
|
90
90
|
SELECT COALESCE(SUM(tokens_estimate), 0) as total
|
|
91
91
|
FROM sessions WHERE project_id = ? AND is_archived = 0
|
|
92
92
|
`).get(t).total}addQuest(t,e,s,r="medium",n){const i=C(),a=this.db.prepare(`
|
|
93
|
-
INSERT INTO quests (project_id, title, description, priority, tags, agent_id, run_id)
|
|
94
|
-
VALUES (?, ?, ?, ?, ?, ?,
|
|
93
|
+
INSERT INTO quests (project_id, title, description, priority, tags, agent_id, run_id, updated_at)
|
|
94
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
95
95
|
RETURNING *
|
|
96
96
|
`).get(t,e,s||"",r,n||null,i.agent_id,i.run_id);return m(this.db,{projectId:t,kind:"quest",refTable:"quests",refId:a.id}),a}updateQuest(t,e){const s=e==="completed"?new Date().toISOString():null,r=this.db.prepare(`
|
|
97
|
-
UPDATE quests SET status = ?, completed_at =
|
|
97
|
+
UPDATE quests SET status = ?, completed_at = ?, updated_at = datetime('now') WHERE id = ? RETURNING *
|
|
98
98
|
`).get(e,s,t);return r&&m(this.db,{projectId:r.project_id,kind:"quest",refTable:"quests",refId:t}),r}getPendingQuests(t){return this.db.prepare(`
|
|
99
99
|
SELECT * FROM quests
|
|
100
100
|
WHERE project_id = ? AND status IN ('pending', 'in_progress')
|
|
@@ -147,7 +147,7 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
|
|
|
147
147
|
updated_at = datetime('now')
|
|
148
148
|
`).run(t,e)}getGlobalContext(t){return this.db.prepare(`
|
|
149
149
|
SELECT value FROM global_context WHERE key = ?
|
|
150
|
-
`).get(t)?.value}getAllGlobalContext(){const t=this.db.prepare("SELECT key, value FROM global_context").all(),e={};for(const s of t)e[s.key]=s.value;return e}registerSkill(t,e,s,r,n,i,a,o){const c=o?.tier??"atomic",l=JSON.stringify(Array.isArray(o?.governs)?o.governs:[]),p=JSON.stringify(Array.isArray(o?.composes)?o.composes:[]),
|
|
150
|
+
`).get(t)?.value}getAllGlobalContext(){const t=this.db.prepare("SELECT key, value FROM global_context").all(),e={};for(const s of t)e[s.key]=s.value;return e}registerSkill(t,e,s,r,n,i,a,o){const c=o?.tier??"atomic",l=JSON.stringify(Array.isArray(o?.governs)?o.governs:[]),p=JSON.stringify(Array.isArray(o?.composes)?o.composes:[]),_=y({skillPath:s,name:t}),g=_?.content??null,S=_?.sha256??null,f=_?new Date().toISOString():null,E=this.resilience.withRetrySync(()=>this.db.prepare(`
|
|
151
151
|
INSERT INTO skills (name, description, skill_path, category, author, version, tags, tier, governs, composes, content, content_sha256, content_updated_at, is_active, usage_count)
|
|
152
152
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)
|
|
153
153
|
ON CONFLICT(name) DO UPDATE SET
|
|
@@ -169,13 +169,13 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
|
|
|
169
169
|
updated_at = datetime('now'),
|
|
170
170
|
is_active = 1
|
|
171
171
|
RETURNING *
|
|
172
|
-
`).get(t,e,s,r||null,n||null,i||null,a||null,c,l,p,
|
|
172
|
+
`).get(t,e,s,r||null,n||null,i||null,a||null,c,l,p,g,S,f),"registerSkill");if(!E.success)throw E.error||new Error("Failed to register skill");return E.data}getSkill(t){const e=this.db.prepare("SELECT * FROM skills WHERE name = ?").get(t);return e&&this.db.prepare("UPDATE skills SET last_used = datetime('now'), usage_count = usage_count + 1 WHERE id = ?").run(e.id),e}backfillSkillContent(t={}){const e=t.read??y,s=this.db.prepare("SELECT id, name, skill_path, content_sha256 FROM skills").all();let r=0,n=0,i=0;const a=this.db.prepare("UPDATE skills SET content = ?, content_sha256 = ?, content_updated_at = ?, updated_at = datetime('now') WHERE id = ?");return this.db.transaction(()=>{for(const c of s){const l=e({skillPath:c.skill_path,name:c.name,skillsDir:t.skillsDir});if(!l){i++;continue}if(c.content_sha256&&c.content_sha256===l.sha256){n++;continue}a.run(l.content,l.sha256,new Date().toISOString(),c.id),r++}})(),{total:s.length,filled:r,unchanged:n,missing:i}}exportSkillContent(t,e={}){const s=e.includeInactive?"":" WHERE is_active = 1",r=this.db.prepare(`SELECT name, content FROM skills${s} ORDER BY name`).all();let n=0,i=0,a=0;const o=new Set;for(const c of r){if(c.content==null){i++;continue}let l=G(c.name);if(!l){i++;continue}o.has(l)&&(l=`${l}-${V(c.name).slice(0,8)}`,a++),o.add(l);const p=d(t,l);b(p,{recursive:!0}),I(d(p,"SKILL.md"),c.content,"utf-8"),n++}return{total:r.length,written:n,skipped_no_content:i,collisions:a}}setSkillVisibility(t,e){return this.db.prepare("UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now') WHERE name = ? RETURNING *").get(e,t)}setAllSkillsVisibility(t,e={}){const s=[],r=[t];e.includeInactive||s.push("is_active = 1"),e.tier&&(s.push("tier = ?"),r.push(e.tier));const n=s.length?" WHERE "+s.join(" AND "):"";return this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now')${n}`).run(...r).changes}listSkills(t,e,s,r){let n="SELECT * FROM skills WHERE 1=1";const i=[];if(t!==void 0&&(n+=" AND is_active = ?",i.push(t?1:0)),e&&(n+=" AND category = ?",i.push(e)),r&&(n+=" AND tier = ?",i.push(r)),s){const a=h(s);a&&(n+=" AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)",i.push(a))}return n+=" ORDER BY updated_at DESC",this.db.prepare(n).all(...i)}searchSkills(t,e=20,s){const r=h(t);if(!r)return[];const n=s?" AND s.tier = ?":"",i=this.db.prepare(`
|
|
173
173
|
SELECT s.* FROM skills s
|
|
174
174
|
JOIN skills_fts ON s.id = skills_fts.rowid
|
|
175
175
|
WHERE skills_fts MATCH ?${n}
|
|
176
176
|
ORDER BY rank
|
|
177
177
|
LIMIT ?
|
|
178
|
-
`);return s?i.all(r,s,e):i.all(r,e)}getSkillGraph(t){const e=this.db.prepare("SELECT * FROM skills").all(),s=new Map;for(const o of e)s.set(o.name,o);const r=o=>{if(!o)return[];try{const c=JSON.parse(o);return Array.isArray(c)?c.filter(l=>typeof l=="string"):[]}catch{return[]}},n=(o,c)=>{const l=s.get(o);if(!l)return null;const p=r(l.governs),
|
|
178
|
+
`);return s?i.all(r,s,e):i.all(r,e)}getSkillGraph(t){const e=this.db.prepare("SELECT * FROM skills").all(),s=new Map;for(const o of e)s.set(o.name,o);const r=o=>{if(!o)return[];try{const c=JSON.parse(o);return Array.isArray(c)?c.filter(l=>typeof l=="string"):[]}catch{return[]}},n=(o,c)=>{const l=s.get(o);if(!l)return null;const p=r(l.governs),_=r(l.composes),g={name:l.name,tier:l.tier??"atomic",description:l.description,governs:p,composes:_,children:[]},S=[];if(!c.has(o)){const f=new Set(c).add(o);for(const E of p){const T=n(E,f);T?g.children.push(T):S.push(E)}}return S.length&&(g.missing=S),g};if(t){const o=n(t,new Set);return o?[o]:[]}const i=e.filter(o=>(o.tier??"atomic")==="god").map(o=>o.name).sort(),a=[];for(const o of i){const c=n(o,new Set);c&&a.push(c)}return a}updateSkill(t,e){const s=[],r=[];return e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.skill_path!==void 0&&(s.push("skill_path = ?"),r.push(e.skill_path)),e.category!==void 0&&(s.push("category = ?"),r.push(e.category)),e.is_active!==void 0&&(s.push("is_active = ?"),r.push(e.is_active?1:0)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),e.version!==void 0&&(s.push("version = ?"),r.push(e.version)),s.length===0?this.getSkill(t):(s.push("updated_at = datetime('now')"),r.push(t),this.db.prepare(`
|
|
179
179
|
UPDATE skills SET ${s.join(", ")} WHERE name = ? RETURNING *
|
|
180
180
|
`).get(...r))}deleteSkill(t){return this.db.prepare("DELETE FROM skills WHERE name = ?").run(t).changes>0}deactivateSkill(t){return this.updateSkill(t,{is_active:!1})}activateSkill(t){return this.updateSkill(t,{is_active:!0})}getSkillStats(){const t=this.db.prepare("SELECT COUNT(*) as count FROM skills").get().count,e=this.db.prepare("SELECT COUNT(*) as count FROM skills WHERE is_active = 1").get().count,s=this.db.prepare(`
|
|
181
181
|
SELECT category, COUNT(*) as count FROM skills WHERE category IS NOT NULL GROUP BY category
|
|
@@ -193,7 +193,7 @@ import{openDatabase as k}from"./storage/index.js";import{existsSync as u,mkdirSy
|
|
|
193
193
|
WHERE project_id = ?
|
|
194
194
|
AND (tags = ? OR tags LIKE ? OR tags LIKE ? OR tags LIKE ?)
|
|
195
195
|
LIMIT 1
|
|
196
|
-
`).get(t,e,`${e},%`,`%,${e}`,`%,${e},%`)}updateQuestFields(t,e){const s=[],r=[];if(e.title!==void 0&&(s.push("title = ?"),r.push(e.title)),e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),s.length===0)return this.db.prepare("SELECT * FROM quests WHERE id = ?").get(t);r.push(t);const n=this.db.prepare(`UPDATE quests SET ${s.join(", ")} WHERE id = ? RETURNING *`).get(...r);return n&&m(this.db,{projectId:n.project_id,kind:"quest",refTable:"quests",refId:t}),n}insertData(t,e,s,r,n){const i=this.resilience.withRetrySync(()=>this.db.prepare(`
|
|
196
|
+
`).get(t,e,`${e},%`,`%,${e}`,`%,${e},%`)}updateQuestFields(t,e){const s=[],r=[];if(e.title!==void 0&&(s.push("title = ?"),r.push(e.title)),e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),s.length===0)return this.db.prepare("SELECT * FROM quests WHERE id = ?").get(t);s.push("updated_at = datetime('now')"),r.push(t);const n=this.db.prepare(`UPDATE quests SET ${s.join(", ")} WHERE id = ? RETURNING *`).get(...r);return n&&m(this.db,{projectId:n.project_id,kind:"quest",refTable:"quests",refId:t}),n}insertData(t,e,s,r,n){const i=this.resilience.withRetrySync(()=>this.db.prepare(`
|
|
197
197
|
INSERT INTO data_lake (project_id, category, key, value, metadata)
|
|
198
198
|
VALUES (?, ?, ?, ?, ?)
|
|
199
199
|
RETURNING *
|
package/dist/design.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const d=new Set(["color","type","spacing","motion","shadow","radius","breakpoint","custom"]),c=new Set(["url","image","palette","snippet"]);class E{db;constructor(e){this.db=e}setToken(e){if(!d.has(e.category))throw new Error(`invalid category: ${e.category} (must be one of ${[...d].join(", ")})`);if(!e.key||!e.value)throw new Error("key and value are required");return this.db.prepare(`
|
|
2
2
|
INSERT INTO design_tokens (project_id, category, key, value, notes, source)
|
|
3
3
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
4
4
|
ON CONFLICT(project_id, category, key) DO UPDATE SET
|
|
@@ -20,10 +20,10 @@ const a=new Set(["color","type","spacing","motion","shadow","radius","breakpoint
|
|
|
20
20
|
`).all(e)}deleteToken(e,r,t){return(this.db.prepare(`
|
|
21
21
|
DELETE FROM design_tokens
|
|
22
22
|
WHERE project_id = ? AND category = ? AND key = ?
|
|
23
|
-
`).run(e,r,t).changes??0)>0}formatForContext(e){const r=this.listTokens(e);if(r.length===0)return"";const t=new Map;for(const s of r){const o=t.get(s.category)??[];o.push(s),t.set(s.category,o)}const n=[];n.push("### \u{1F3A8} Design tokens");for(const[s,o]of t){n.push(""),n.push(`**${s}:**`);for(const
|
|
24
|
-
`)}addReference(e){if(!
|
|
25
|
-
INSERT INTO design_references (project_id, kind, location, title, notes, tags)
|
|
26
|
-
VALUES (?, ?, ?, ?, ?,
|
|
23
|
+
`).run(e,r,t).changes??0)>0}formatForContext(e){const r=this.listTokens(e);if(r.length===0)return"";const t=new Map;for(const s of r){const o=t.get(s.category)??[];o.push(s),t.set(s.category,o)}const n=[];n.push("### \u{1F3A8} Design tokens");for(const[s,o]of t){n.push(""),n.push(`**${s}:**`);for(const a of o)n.push(`- \`${a.key}\` = \`${a.value}\`${a.notes?` \u2014 ${a.notes}`:""}`)}return n.join(`
|
|
24
|
+
`)}addReference(e){if(!c.has(e.kind))throw new Error(`invalid kind: ${e.kind} (must be one of ${[...c].join(", ")})`);if(!e.location)throw new Error("location is required");const r=this.db.prepare(`
|
|
25
|
+
INSERT INTO design_references (project_id, kind, location, title, notes, tags, updated_at)
|
|
26
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
27
27
|
`).run(e.projectId??null,e.kind,e.location,e.title??null,e.notes??null,e.tags??null);return this.getReference(Number(r.lastInsertRowid))}getReference(e){return this.db.prepare("SELECT * FROM design_references WHERE id = ?").get(e)??null}listReferences(e={}){const r=Math.min(e.limit??50,200);if(e.tag){const t=`%${e.tag}%`;return e.projectId!=null?this.db.prepare(`
|
|
28
28
|
SELECT * FROM design_references
|
|
29
29
|
WHERE project_id = ? AND tags LIKE ?
|
package/dist/handlers/cloud.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import{TOOL_ANNOTATIONS as u}from"../tool-annotations.js";import{CloudSyncDaemon as
|
|
1
|
+
import{TOOL_ANNOTATIONS as u}from"../tool-annotations.js";import{CloudSyncDaemon as h}from"../cloud-sync.js";import{getCrypto as g,initializeCrypto as f}from"../crypto.js";import{activateLicense as k,getLicenseInfo as w,getTier as v,hasFeature as d}from"../license.js";import{loadSession as x}from"../cloud/client.js";import{execFile as b}from"node:child_process";import{promisify as _}from"node:util";import{fileURLToPath as $}from"node:url";import{dirname as S,join as E}from"node:path";const T=_(b);async function P(r){const c=x();if(!c||!c.device_id)return{ok:!1,text:`\u{F115D} Sovereign cloud sync: this device is not logged in.
|
|
2
2
|
The login is an interactive device flow, so run it in a terminal:
|
|
3
3
|
wyrm cloud login
|
|
4
|
-
Then retry this action. (Status/sync here drive an already-registered device.)`};const a=S($(
|
|
4
|
+
Then retry this action. (Status/sync here drive an already-registered device.)`};const a=E(S($(import.meta.url)),"..","wyrm-cli.js");try{const{stdout:o,stderr:i}=await T(process.execPath,[a,"cloud",...r],{timeout:18e4,maxBuffer:4194304,env:{...process.env}});return{ok:!0,text:(o||"").trim()||(i||"").trim()||"(no output)"}}catch(o){return{ok:!1,text:`\u{F115D} Sovereign cloud sync failed.
|
|
5
5
|
${[o?.stdout,o?.stderr].filter(Boolean).join(`
|
|
6
|
-
`).trim()||(o?.message??"unknown error")}`}}}const
|
|
6
|
+
`).trim()||(o?.message??"unknown error")}`}}}const q=[{name:"wyrm_license",description:"View current license status, tier, and available features",inputSchema:{type:"object",properties:{}},annotations:u.wyrm_license,aliases:[],handler:async(r,c)=>{const{cachedResponse:a,sync:o}=c,i=r||{},n=w(),e=v();let t=`\u{F115D} **Wyrm License Status**
|
|
7
7
|
|
|
8
8
|
`;if(t+=`- **Tier:** ${e.charAt(0).toUpperCase()+e.slice(1)}
|
|
9
9
|
`,n.valid&&n.key){t+=`- **Key:** ${n.key}
|
|
@@ -12,7 +12,7 @@ ${[o?.stdout,o?.stderr].filter(Boolean).join(`
|
|
|
12
12
|
`,t+=`- **Status:** \u2713 Valid
|
|
13
13
|
|
|
14
14
|
`,t+=`### Features
|
|
15
|
-
`;for(const
|
|
15
|
+
`;for(const l of n.features)t+=`- \u2713 ${l}
|
|
16
16
|
`}else t+=`- **Status:** Free tier (no license key)
|
|
17
17
|
|
|
18
18
|
`,t+=`### Free Features
|
|
@@ -28,7 +28,7 @@ ${[o?.stdout,o?.stderr].filter(Boolean).join(`
|
|
|
28
28
|
`,t+=`- Priority support
|
|
29
29
|
|
|
30
30
|
`,t+=`### Already have a license?
|
|
31
|
-
`,t+="Activate it \u2014 paste the license JSON into the `wyrm_activate` tool, or save it to `~/.wyrm/license.json` and restart Wyrm.\n",t+="Manage or download your license at https://account.ghosts.lk \xB7 buy one at https://ghosts.lk/wyrm.";return a(t)}},{name:"wyrm_activate",description:"Activate a license key to unlock Pro/Team/Enterprise features",inputSchema:{type:"object",properties:{license:{type:"string",description:"License JSON string (signed license from Ghost Protocol)"}},required:["license"]},annotations:u.wyrm_activate,aliases:[],handler:async(r,c)=>{const a=r?.license;if(!a)throw new Error("License JSON string required");try{const o=
|
|
31
|
+
`,t+="Activate it \u2014 paste the license JSON into the `wyrm_activate` tool, or save it to `~/.wyrm/license.json` and restart Wyrm.\n",t+="Manage or download your license at https://account.ghosts.lk \xB7 buy one at https://ghosts.lk/wyrm.";return a(t)}},{name:"wyrm_activate",description:"Activate a license key to unlock Pro/Team/Enterprise features",inputSchema:{type:"object",properties:{license:{type:"string",description:"License JSON string (signed license from Ghost Protocol)"}},required:["license"]},annotations:u.wyrm_activate,aliases:[],handler:async(r,c)=>{const a=r?.license;if(!a)throw new Error("License JSON string required");try{const o=k(a);return o.valid?{content:[{type:"text",text:`\u{F115D} \u2713 License activated!
|
|
32
32
|
|
|
33
33
|
- **Tier:** ${o.tier}
|
|
34
34
|
- **Features:** ${o.features.join(", ")}
|
|
@@ -71,11 +71,11 @@ Visit https://ghosts.lk/wyrm to upgrade.`}]};const n=r?.action;switch(n){case"ba
|
|
|
71
71
|
|
|
72
72
|
\u26A0\uFE0F Restart Wyrm to use the restored database.`}]}}case"list":{const e=await a.listBackups();if(e.length===0)return{content:[{type:"text",text:"\u{F115D} No backups found."}]};let t=`\u{F115D} **Cloud Backups** (${e.length} found)
|
|
73
73
|
|
|
74
|
-
`;for(const
|
|
75
|
-
`;return{content:[{type:"text",text:t}]}}case"prune":{const e=r?.keepCount||10;return{content:[{type:"text",text:`\u{F115D} \u2713 Pruned ${(await a.pruneBackups(e)).deleted} old backup(s). Kept ${e} most recent.`}]}}default:return{content:[{type:"text",text:`Unknown backup action: ${n}. Use: backup, restore, list, prune`}],isError:!0}}}},{name:"wyrm_cloud_sync",description:"Cloud sync. The SOVEREIGN path is the promoted one: zero-knowledge, per-row AES-256-GCM E2E-encrypted delta sync with tombstone propagation \u2014 the cloud never sees plaintext. Use action 'sovereign' to sync (push+pull), 'sovereign-preview' for a dry run, 'sovereign-status' to show device + sync state. (First-time login is an interactive device flow: run `wyrm cloud login` in a terminal once.) The start/stop/restart/status/force-sync actions drive the LEGACY whole-DB snapshot daemon (last-write-wins by mtime, weaker) \u2014 retained only for simple full-DB backup; it is NOT the sovereign path. Prefer sovereign for multi-device memory.",inputSchema:{type:"object",properties:{action:{type:"string",description:"Sovereign (preferred): sovereign, sovereign-preview, sovereign-status. Legacy snapshot daemon: start, stop, restart, status, force-sync.",enum:["sovereign","sovereign-preview","sovereign-status","start","stop","restart","status","force-sync"]},all:{type:"boolean",description:"Sovereign only: sync every semantic table, ignoring visibility flags (default: false)"},interval_minutes:{type:"number",description:"Legacy snapshot only: sync interval (default: 10)"},keep_count:{type:"number",description:"Legacy snapshot only: snapshots to retain after pruning (default: 20)"}},required:["action"]},annotations:u.wyrm_cloud_sync,aliases:[],handler:async(r,c)=>{const{cloudBackup:a,cloudSyncManager:o,db:i,sync:n}=c,e=r||{},t=r?.action
|
|
76
|
-
Visit https://ghosts.lk/wyrm to upgrade. (The sovereign path \u2014 action=sovereign \u2014 is the preferred sync.)`}]};const
|
|
77
|
-
`)}]}}case"force-sync":{const
|
|
78
|
-
Visit https://ghosts.lk/wyrm to upgrade.`}]};const o=r?.action,i=
|
|
74
|
+
`;for(const l of e)t+=`- **${l.timestamp}** \u2014 ${(l.db_size/1024).toFixed(1)} KB, encrypted: ${l.encrypted}, machine: ${l.machine_id.slice(0,8)}...
|
|
75
|
+
`;return{content:[{type:"text",text:t}]}}case"prune":{const e=r?.keepCount||10;return{content:[{type:"text",text:`\u{F115D} \u2713 Pruned ${(await a.pruneBackups(e)).deleted} old backup(s). Kept ${e} most recent.`}]}}default:return{content:[{type:"text",text:`Unknown backup action: ${n}. Use: backup, restore, list, prune`}],isError:!0}}}},{name:"wyrm_cloud_sync",description:"Cloud sync. The SOVEREIGN path is the promoted one: zero-knowledge, per-row AES-256-GCM E2E-encrypted delta sync with tombstone propagation \u2014 the cloud never sees plaintext. Use action 'sovereign' to sync (push+pull), 'sovereign-preview' for a dry run, 'sovereign-status' to show device + sync state. For hands-off periodic sync, 'sovereign-daemon-start' / 'sovereign-daemon-stop' / 'sovereign-daemon-status' run a background daemon. (First-time login is an interactive device flow: run `wyrm cloud login` in a terminal once.) The start/stop/restart/status/force-sync actions drive the LEGACY whole-DB snapshot daemon (last-write-wins by mtime, weaker) \u2014 retained only for simple full-DB backup; it is NOT the sovereign path. Prefer sovereign for multi-device memory.",inputSchema:{type:"object",properties:{action:{type:"string",description:"Sovereign (preferred): sovereign, sovereign-preview, sovereign-status, sovereign-daemon-start, sovereign-daemon-stop, sovereign-daemon-status. Legacy snapshot daemon: start, stop, restart, status, force-sync.",enum:["sovereign","sovereign-preview","sovereign-status","sovereign-daemon-start","sovereign-daemon-stop","sovereign-daemon-status","start","stop","restart","status","force-sync"]},all:{type:"boolean",description:"Sovereign only: sync every semantic table, ignoring visibility flags (default: false)"},interval_minutes:{type:"number",description:"Legacy snapshot only: sync interval (default: 10)"},keep_count:{type:"number",description:"Legacy snapshot only: snapshots to retain after pruning (default: 20)"}},required:["action"]},annotations:u.wyrm_cloud_sync,aliases:[],handler:async(r,c)=>{const{cloudBackup:a,cloudSyncManager:o,db:i,sync:n}=c,e=r||{},t=r?.action,l={sovereign:r?.all?["sync","--all"]:["sync"],"sovereign-preview":["sync","--dry-run"],"sovereign-status":["status"],"sovereign-daemon-start":["daemon","start"],"sovereign-daemon-stop":["daemon","stop"],"sovereign-daemon-status":["daemon","status"]};if(t in l){const s=await P(l[t]);return{content:[{type:"text",text:s.text}],isError:!s.ok}}if(!d("cloud_backup"))return{content:[{type:"text",text:`\u{F115D} The legacy snapshot daemon requires a Pro license or higher.
|
|
76
|
+
Visit https://ghosts.lk/wyrm to upgrade. (The sovereign path \u2014 action=sovereign \u2014 is the preferred sync.)`}]};const y=r?.interval_minutes,m=r?.keep_count;switch(t){case"start":{const s=o.start({interval_minutes:y,keep_count:m});return s.ok?{content:[{type:"text",text:`\u{F115D} \u2713 cloud-sync daemon started (pid ${s.pid}). Logs: ~/.wyrm/wyrm-cloud-sync.log`}]}:{content:[{type:"text",text:`\u{F115D} cloud-sync: ${s.reason??"failed to start"}${s.pid?` (pid ${s.pid})`:""}`}],isError:!s.pid}}case"stop":{const s=o.stop();return{content:[{type:"text",text:s.ok?"\u{F115D} \u2713 cloud-sync daemon stopped":`\u{F115D} cloud-sync: ${s.reason??"failed to stop"}`}],isError:!s.ok}}case"restart":{o.stop(),await new Promise(p=>{setTimeout(p,500)});const s=o.start({interval_minutes:y,keep_count:m});return{content:[{type:"text",text:s.ok?`\u{F115D} \u2713 cloud-sync daemon restarted (pid ${s.pid})`:`\u{F115D} cloud-sync restart failed: ${s.reason??"unknown"}`}],isError:!s.ok}}case"status":{const s=o.status(),p=["\u{F115D} **Legacy Snapshot Daemon Status** (not the sovereign path \u2014 see action=sovereign-status)"];return p.push(`- Running: ${s.running?"\u2713 yes":"\u2717 no"}`),s.pid&&p.push(`- PID: ${s.pid}`),s.state&&(s.state.last_uploaded_ts&&p.push(`- Last upload: ${new Date(s.state.last_uploaded_ts).toISOString()}${s.state.last_uploaded_key?` (${s.state.last_uploaded_key})`:""}`),s.state.last_restored_key&&p.push(`- Last restore: ${s.state.last_restored_key}`),s.state.last_check_ts&&p.push(`- Last check: ${new Date(s.state.last_check_ts).toISOString()}`),p.push(`- Machine: ${s.state.machine}`)),{content:[{type:"text",text:p.join(`
|
|
77
|
+
`)}]}}case"force-sync":{const p=await new h(a,i.getDatabasePath()).tick();return{content:[{type:"text",text:`\u{F115D} force-sync: ${JSON.stringify(p,null,2)}`}]}}default:return{content:[{type:"text",text:`Unknown cloud-sync action: ${t}. Use: start, stop, restart, status, force-sync`}],isError:!0}}}},{name:"wyrm_encrypt_setup",description:"Set up or check encryption status for sensitive data (Pro+)",inputSchema:{type:"object",properties:{action:{type:"string",description:"Action: status, enable, test",enum:["status","enable","test"]},password:{type:"string",description:"Encryption password (min 8 chars, required for enable)"}},required:["action"]},annotations:u.wyrm_encrypt_setup,aliases:[],handler:async(r,c)=>{const a=r||{};if(!d("encryption"))return{content:[{type:"text",text:`\u{F115D} Encryption setup requires a Pro license or higher.
|
|
78
|
+
Visit https://ghosts.lk/wyrm to upgrade.`}]};const o=r?.action,i=g();switch(o){case"status":{const n=i.isEnabled();let e=`\u{F115D} **Encryption Status**
|
|
79
79
|
|
|
80
80
|
`;return e+=`- **Enabled:** ${n?"\u2713 Yes":"\u2717 No"}
|
|
81
81
|
`,e+=`- **Algorithm:** AES-256-GCM
|
|
@@ -87,4 +87,4 @@ Visit https://ghosts.lk/wyrm to upgrade.`}]};const o=r?.action,i=h();switch(o){c
|
|
|
87
87
|
|
|
88
88
|
\u26A0\uFE0F **Store your password safely.** Lost passwords cannot be recovered.
|
|
89
89
|
New data lake entries will be encrypted at rest.`}]})}case"test":{if(!i.isEnabled())return{content:[{type:"text",text:'\u{F115D} Encryption not enabled. Run with action "enable" first.'}]};const n="Wyrm encryption test "+Date.now(),e=i.encrypt(n),t=i.decrypt(e);return{content:[{type:"text",text:`\u{F115D} Encryption test: ${t===n?"\u2713 PASSED":"\u2717 FAILED"}
|
|
90
|
-
- Encrypted ${n.length} chars \u2192 decrypted back to ${t.length} chars`}]}}default:return{content:[{type:"text",text:`Unknown encrypt action: ${o}. Use: status, enable, test`}],isError:!0}}}}];export{
|
|
90
|
+
- Encrypted ${n.length} chars \u2192 decrypted back to ${t.length} chars`}]}}default:return{content:[{type:"text",text:`Unknown encrypt action: ${o}. Use: status, enable, test`}],isError:!0}}}}];export{q as cloudToolSpecs};
|
|
@@ -13,7 +13,7 @@ ${t.errors.map(r=>`- ${r}`).join(`
|
|
|
13
13
|
`),t.injected.length>0&&(e+="AI models in this project will now call `wyrm_session_prime` at the start of each conversation."),{content:[{type:"text",text:e}]}}},{name:"buddy",description:"Use when another buddy-compatible MCP server (or the operator) asks Wyrm for a brief, data-grounded status reply - the Buddy Protocol v1.0 well-known entry point. Companion params switch to the full companion reply; bare calls stay protocol-pure. Cycle-protected.",inputSchema:{type:"object",properties:{from_buddy:{type:"string",description:"Caller identity"},project_hint:{type:"string",description:"Scope the reply"},mood_hint:{type:"string",enum:["normal","celebratory","stuck","quiet"]},size:{type:"string",enum:["full","compact","mini"]},format:{type:"string",enum:["markdown","json"],description:"Peer mode only."},project_path:{type:"string",description:"Companion mode: project root (default cwd)"},persona:{type:"string",enum:["wyrm","drogo","berlin","sumair","custom"],description:"Companion voice."},persona_name:{type:"string",description:"For persona=custom"},mood:{type:"string",description:"Companion override (mood_hint values)"},federate:{type:"boolean",description:"Fold in other buddy MCPs"}}},annotations:l.buddy,aliases:[],handler:async(o,c)=>{const{WYRM_PACKAGE_VERSION:i,db:a,runCompanionBuddy:t}=c,e=o||{};if(b(o??{}))return await t(o??{});const r=o,s=g(a.getDatabase(),r,i);return r.format==="json"?{content:[{type:"text",text:JSON.stringify(s.json,null,2)}]}:{content:[{type:"text",text:s.markdown}]}}},{name:"wyrm_buddy",description:"Friendly, data-grounded coding companion. Returns a brief greeting + project state + suggested next step sourced entirely from Wyrm's real data. Every claim cites a real DB row ID \u2014 no hallucinated encouragement. Personas: wyrm (default, dragon-themed), drogo (gruff), berlin (precise), sumair (warm), custom. Moods: normal / celebratory / stuck / quiet \u2014 auto-detected. Sizes: full (10-line PhantomDragon-style mascot), compact (5-line workspace-friendly), mini (single-line inline prefix). Federates with other registered MCPs that expose `buddy`, `*_buddy`, or `buddy_*` tools \u2014 their replies fold into a 'From other buddies' section.",inputSchema:{type:"object",properties:{project_path:{type:"string",description:"Project root. Default: current cwd if registered."},mood:{type:"string",enum:["normal","celebratory","stuck","quiet"],description:"Override the auto-detected mood."},persona:{type:"string",enum:["wyrm","drogo","berlin","sumair","custom"],description:"Voice. Default: WYRM_BUDDY_DEFAULT_PERSONA env or 'wyrm'."},persona_name:{type:"string",description:"Required when persona === 'custom'."},size:{type:"string",enum:["full","compact","mini"],description:"Mascot size. 'full' = 10-line head+shoulder banner (default), 'compact' = 5-line workspace-friendly, 'mini' = single-line inline prefix on the greeting."},federate:{type:"boolean",description:"Query other registered MCPs with *_buddy tools and fold their replies in. Default: true if any are registered."}}},annotations:l.wyrm_buddy,aliases:[],handler:async(o,c)=>{const{runCompanionBuddy:i}=c;return await i(o??{})}},{name:"wyrm_token_set",description:"Set a design token for a project. Tokens are colour / type / spacing / motion / shadow / radius / breakpoint / custom \u2014 the design system primitives the project is built on. Stored first-class so AI helpers working on creative tasks don't have to re-derive the system from scattered CSS each time. Upserts on (project_id, category, key).",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project root path."},category:{type:"string",enum:["color","type","spacing","motion","shadow","radius","breakpoint","custom"],description:"Token category."},key:{type:"string",description:"Short slug, e.g. 'primary' / 'h1' / 'page-padding-md'."},value:{type:"string",description:"Actual value: '#00B89F' / 'Inter 900 2.5rem' / '1.25rem' / etc."},notes:{type:"string",description:"Optional why / when-to-use guidance."},source:{type:"string",enum:["user","derived","imported","observed"],description:"How the token was determined. Default: 'user'."}},required:["projectPath","category","key","value"]},annotations:l.wyrm_token_set,aliases:[],handler:async(o,c)=>{const{db:i,designSystem:a}=c,{projectPath:t,category:e,key:r,value:s,notes:p,source:u}=o,n=i.getProject(t);if(!n)return{content:[{type:"text",text:`Project not found: ${t}`}],isError:!0};const d=a().setToken({projectId:n.id,category:e,key:r,value:s,notes:p,source:u});return{content:[{type:"text",text:`\u{1F3A8} token set: \`${d.category}/${d.key}\` = \`${d.value}\``+(d.notes?` \u2014 ${d.notes}`:"")}]}}},{name:"wyrm_token_get",description:"Retrieve all design tokens for a project, optionally filtered by category. Returns the design system primitives the project is built on.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project root path."},category:{type:"string",enum:["color","type","spacing","motion","shadow","radius","breakpoint","custom"],description:"Filter by category. Omit for all tokens."}},required:["projectPath"]},annotations:l.wyrm_token_get,aliases:[],handler:async(o,c)=>{const{db:i,designSystem:a}=c,{projectPath:t,category:e}=o,r=i.getProject(t);if(!r)return{content:[{type:"text",text:`Project not found: ${t}`}],isError:!0};const s=a().formatForContext(r.id);if(!s)return{content:[{type:"text",text:`_(no design tokens for ${r.name} yet \u2014 use \`wyrm_token_set\` to start the design system)_`}]};if(e){const p=a().listTokens(r.id,e);if(p.length===0)return{content:[{type:"text",text:`_(no ${e} tokens for ${r.name})_`}]};const u=[`### \u{1F3A8} ${e} tokens for ${r.name}`,""];for(const n of p)u.push(`- \`${n.key}\` = \`${n.value}\`${n.notes?` \u2014 ${n.notes}`:""}`);return{content:[{type:"text",text:u.join(`
|
|
14
14
|
`)}]}}return{content:[{type:"text",text:s}]}}},{name:"wyrm_token_delete",description:"Remove a design token from a project.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project root path."},category:{type:"string",enum:["color","type","spacing","motion","shadow","radius","breakpoint","custom"]},key:{type:"string",description:"Token key to remove."}},required:["projectPath","category","key"]},annotations:l.wyrm_token_delete,aliases:[],handler:async(o,c)=>{const{db:i,designSystem:a}=c,{projectPath:t,category:e,key:r}=o,s=i.getProject(t);return s?{content:[{type:"text",text:a().deleteToken(s.id,e,r)?`\u{1F5D1} removed \`${e}/${r}\``:`not found: \`${e}/${r}\``}]}:{content:[{type:"text",text:`Project not found: ${t}`}],isError:!0}}},{name:"wyrm_reference_add",description:"Clip a design reference (URL, image path, palette, or inline snippet) for inspiration. Stored per-project (or globally if no project path). Tag aggressively \u2014 tags drive future search and recall when building a new project in the same vein.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project root path. Omit to attach as a global reference."},kind:{type:"string",enum:["url","image","palette","snippet"],description:"Reference type."},location:{type:"string",description:"URL, file path, or inline payload (e.g. palette like '#00B89F,#ff5a55,#0a0c0e' or a code snippet)."},title:{type:"string",description:"Short title \u2014 what is this reference?"},notes:{type:"string",description:"Why you saved it / what to remember about it."},tags:{type:"string",description:"Comma-separated tags \u2014 'hero,scroll-tied,framer-motion'."}},required:["kind","location"]},annotations:l.wyrm_reference_add,aliases:[],handler:async(o,c)=>{const{db:i,designSystem:a}=c,{projectPath:t,kind:e,location:r,title:s,notes:p,tags:u}=o;let n=null;if(t){const m=i.getProject(t);if(!m)return{content:[{type:"text",text:`Project not found: ${t}`}],isError:!0};n=m.id}const d=a().addReference({projectId:n,kind:e,location:r,title:s,notes:p,tags:u}),y=n?` (project ${n})`:" (global)";return{content:[{type:"text",text:`\u{1F4CC} reference #${d.id} saved${y}: \`${d.kind}\` ${d.title??d.location.slice(0,60)}${d.tags?` \xB7 tags: ${d.tags}`:""}`}]}}},{name:"wyrm_reference_list",description:"Browse saved design references. Filter by project or tag. Default: 50 most recent across all projects.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Limit to a specific project."},tag:{type:"string",description:"Filter to references containing this tag."},limit:{type:"number",description:"Max results (default 50, max 200)."}}},annotations:l.wyrm_reference_list,aliases:[],handler:async(o,c)=>{const{db:i,designSystem:a}=c,{projectPath:t,tag:e,limit:r}=o;let s;if(t){const n=i.getProject(t);if(!n)return{content:[{type:"text",text:`Project not found: ${t}`}],isError:!0};s=n.id}const p=a().listReferences({projectId:s,tag:e,limit:r});if(p.length===0)return{content:[{type:"text",text:"_(no references match)_"}]};const u=[`\u{1F4CC} **${p.length} reference${p.length===1?"":"s"}**`];for(const n of p){const d=n.project_id?`p${n.project_id}`:"global";u.push(`- [${n.id}/${d}] \`${n.kind}\` ${n.title??n.location.slice(0,60)}${n.tags?` \xB7 ${n.tags}`:""}`)}return{content:[{type:"text",text:u.join(`
|
|
15
15
|
`)}]}}},{name:"wyrm_reference_search",description:"Full-text search across design references' title / notes / tags. Returns matching references sorted by recency.",inputSchema:{type:"object",properties:{query:{type:"string",description:"FTS5 query \u2014 keywords, phrases in quotes."},projectPath:{type:"string",description:"Limit to a specific project."},limit:{type:"number",description:"Max results (default 20, max 100)."}},required:["query"]},annotations:l.wyrm_reference_search,aliases:[],handler:async(o,c)=>{const{db:i,designSystem:a}=c,{query:t,projectPath:e,limit:r}=o;let s;if(e){const n=i.getProject(e);if(!n)return{content:[{type:"text",text:`Project not found: ${e}`}],isError:!0};s=n.id}const p=a().searchReferences(t,{projectId:s,limit:r});if(p.length===0)return{content:[{type:"text",text:`_(no references match "${t}")_`}]};const u=[`\u{1F50D} **${p.length} reference${p.length===1?"":"s"}** matching "${t}"`];for(const n of p){const d=n.project_id?`p${n.project_id}`:"global";u.push(`- [${n.id}/${d}] \`${n.kind}\` ${n.title??n.location.slice(0,60)}${n.tags?` \xB7 ${n.tags}`:""}`)}return{content:[{type:"text",text:u.join(`
|
|
16
|
-
`)}]}}},{name:"wyrm_capabilities",description:"Use to learn what your memory system can actually do - feature inventory, why each matters, runtime state, and which tool to reach for. Call at session start instead of re-discovering the surface.",inputSchema:{type:"object",properties:{format:{type:"string",enum:["json","markdown"]}}},annotations:l.wyrm_capabilities,aliases:[],handler:async(o,c)=>{const{WYRM_TOOL_COUNT:i,agentDaemon:a,db:t}=c,{format:e="markdown"}=o,r=f(t.getDatabase(),i,{
|
|
16
|
+
`)}]}}},{name:"wyrm_capabilities",description:"Use to learn what your memory system can actually do - feature inventory, why each matters, runtime state, and which tool to reach for. Call at session start instead of re-discovering the surface.",inputSchema:{type:"object",properties:{format:{type:"string",enum:["json","markdown"]}}},annotations:l.wyrm_capabilities,aliases:[],handler:async(o,c)=>{const{WYRM_TOOL_COUNT:i,agentDaemon:a,db:t}=c,{format:e="markdown"}=o,r=f(t.getDatabase(),i,{hasEncryption:()=>!!process.env.WYRM_ENCRYPTION_KEY,isFederationEnabled:()=>!0,isAgentRunning:()=>{try{return a.status().running}catch{return null}}});return e==="json"?{content:[{type:"text",text:JSON.stringify(r,null,2)}]}:{content:[{type:"text",text:w(r)}]}}},{name:"wyrm_intro",description:"Return a plain-English explanation of what Wyrm is and what it does. Aimed at HUMANS who don't yet understand Wyrm's value \u2014 co-founders, operators, designers, anyone who didn't read the docs. Call this when a human user asks 'what does Wyrm even do?' or seems unaware Wyrm is involved in their AI assistant's behavior. NOT for AI-agent self-orientation (use wyrm_capabilities for that).",inputSchema:{type:"object",properties:{}},annotations:l.wyrm_intro,aliases:[],handler:async(o,c)=>{const{WYRM_PACKAGE_VERSION:i}=c,{renderIntro:a}=await import("../visibility.js");return{content:[{type:"text",text:a(i)}]}}},{name:"wyrm_migrate_prompt",description:"Rewrite the Wyrm-managed block in this project's AI-client instruction files (.cursor/rules, .github/copilot-instructions.md, CLAUDE.md, etc.) to the current canonical version. Operator-authored content outside the <!-- wyrm:start --> / <!-- wyrm:end --> markers is preserved verbatim. Dry-run by default; pass apply:true to write changes.",inputSchema:{type:"object",properties:{project_path:{type:"string",description:"Project root containing the client config files."},apply:{type:"boolean",description:"Default false (dry-run). Set true to actually write changes."}},required:["project_path"]},annotations:l.wyrm_migrate_prompt,aliases:[],handler:async(o,c)=>{const{migrateProject:i,renderMigrationReport:a}=await import("../migrate-prompt.js"),{WYRM_INJECT_BLOCK:t}=await import("../autoconfig.js"),{project_path:e,apply:r=!1}=o;if(!e)return{content:[{type:"text",text:"project_path required"}],isError:!0};const s=i({projectPath:e,newBlock:t,apply:r});return{content:[{type:"text",text:a(s,r)}]}}},{name:"wyrm_digest",description:"Render a plain-English summary of what Wyrm has done for the operator over a period. Shows counts (sessions tracked, quests completed, repeated failures blocked, ground truths set, references clipped, hours logged) and highlights so a non-technical operator can see Wyrm's actual contribution. Use this when the user asks 'what did Wyrm do?', 'is Wyrm doing anything?', or to demonstrate value to skeptics.",inputSchema:{type:"object",properties:{period:{type:"string",enum:["today","week","month","quarter","year","all"],description:"Reporting window (default: week)."}}},annotations:l.wyrm_digest,aliases:[],handler:async(o,c)=>{const{db:i}=c,{gatherDigest:a,renderDigest:t}=await import("../visibility.js"),{period:e="week"}=o,r=a(i.getDatabase(),e);return{content:[{type:"text",text:t(r)}]}}},{name:"wyrm_check_update",description:"Check whether a newer wyrm-mcp is available on npm. Cached 24h by default; pass force:true to bypass cache.",inputSchema:{type:"object",properties:{force:{type:"boolean",description:"Bypass the 24-hour cache and re-query the registry."}}},annotations:l.wyrm_check_update,aliases:[],handler:async(o,c)=>{const{WYRM_PACKAGE_VERSION:i,db:a}=c,{force:t=!1}=o,e=await j(a.getDatabase(),i,{force:t});let r=`\u{F115D} **Update check**
|
|
17
17
|
|
|
18
18
|
- Current: ${e.current}
|
|
19
19
|
- Latest: ${e.latest??"unknown (offline?)"}
|
package/dist/handlers/recall.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import{TOOL_ANNOTATIONS as L}from"../tool-annotations.js";import{renderResult as
|
|
1
|
+
import{TOOL_ANNOTATIONS as L}from"../tool-annotations.js";import{renderResult as T,withGlyph as k}from"../render.js";import{memoryUri as A,resourceLink as H}from"./resources.js";import{buildDeterminismReceipt as N,attestation as U}from"../receipt.js";import{rerankConfigured as W,rerankEgressHost as F}from"../rerank.js";const V=[{name:"wyrm_recall",description:"Use before starting any non-trivial task - what do we know about this? Pull up relevant lessons learned, proven patterns, anti-patterns, and reasoning traces from past work. Hybrid lexical+semantic retrieval, fully local, no LLM. Mark what you recalled as useful (or not) with wyrm_feedback.",inputSchema:{type:"object",properties:{projectPath:{type:"string"},query:{type:"string",description:"The task or problem"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]},limit:{type:"number",description:"Default 10"},minConfidence:{type:"number",description:"0-1 (default 0)"},detail:{type:"string",enum:["full","link"],description:"Default full"}},required:["projectPath","query"]},outputSchema:{type:"object",properties:{query:{type:"string"},count:{type:"integer"},results:{type:"array",items:{type:"object",properties:{id:{type:"integer"},kind:{type:"string"},relevance:{type:"number"},problem:{type:"string"},validated_fix:{type:["string","null"]},confidence:{type:"number"},reuse_count:{type:"integer"}},required:["id","kind","relevance","problem","confidence","reuse_count"]}}},required:["query","count","results"]},annotations:L.wyrm_recall,aliases:[],handler:async($,{store:E,raw:S,memory:v,vectors:h,clientSupportsResources:P})=>{const{projectPath:w,query:d,kind:R,limit:j,minConfidence:q,detail:g}=$,u=E.getProject(w);if(!u)return{content:[{type:"text",text:`Project not found: ${w}`}],isError:!0};const p=!!h(),O=p&&W()&&/^(1|true|yes|on)$/i.test(process.env.WYRM_RERANK??"");let r;const b={kind:R,limit:j??10,minConfidence:q,rerank:O,onStats:t=>{r=t}},a=p?await v.recallHybrid(u.id,d,b):v.recall(u.id,d,b),c=r!==void 0,f=r?.rerankMs!=null,s=h(),M=typeof s?.getProvider=="function"?s.getProvider():void 0;let l;if(c&&typeof s?.indexCoverage=="function")try{l=s.indexCoverage()}catch{}let m=null;if(!c)if(p){let t=-1;if(typeof s?.getStats=="function")try{t=s.getStats().total}catch{}m=t===0?"empty_index":"embed_error"}else{const{resolveEmbeddingState:t}=await import("../providers/embedding-provider.js");m=t().reason||"vector_store_unavailable"}const C=N({vectorsAvailable:c,rerankActive:f,embedModel:c?M?.model??(process.env.WYRM_EMBED_MODEL||"nomic-embed-text"):null,rerankModel:f&&process.env.WYRM_RERANK_MODEL||null,embedEgressHost:c?M?.remoteHost??null:null,rerankEgressHost:f?F():null,stageMs:r?{fts5:r.ftsMs,vector:r.vectorMs,fusion:r.fusionMs,...r.rerankMs!=null?{rerank:r.rerankMs}:{}}:void 0,indexStale:l?.stale?{activeModel:l.activeModel,indexedUnder:Object.keys(l.otherModels)}:void 0,ftsOnlyReason:m});if(a.length>0){const t=a.map(o=>o.artifact.id),e=t.map(()=>"?").join(",");S().prepare(`UPDATE memory_artifacts SET last_accessed_at = datetime('now'), access_count = access_count + 1 WHERE id IN (${e})`).run(...t)}const x=t=>Math.round(t*1e4)/1e4,y={query:d,count:a.length,results:a.map(t=>{const e=t.artifact,o={id:e.id,kind:e.kind,relevance:x(t.relevance_score),problem:e.problem,outcome:e.outcome,confidence:e.confidence==null?e.confidence:x(e.confidence),reuse_count:e.reuse_count,reuse_success_count:e.reuse_success_count};return e.constraints!=null&&(o.constraints=e.constraints),e.validated_fix!=null&&(o.validated_fix=e.validated_fix),e.why_it_worked!=null&&(o.why_it_worked=e.why_it_worked),o}),receipt:C},_=T(y,(t,e)=>{const o=`
|
|
2
2
|
|
|
3
|
-
_${
|
|
3
|
+
_${U(t.receipt)}_`;if(t.count===0)return k(e.brand,`No relevant memory found for: "${t.query}"`)+"\n\nUse `wyrm_remember` to store knowledge as you work."+o;let i=k(e.brand,`**Memory Recall** -- "${t.query}" (${t.count} found)`)+`
|
|
4
4
|
|
|
5
5
|
`;for(const n of t.results){i+=`### ${n.kind} (ID: ${n.id}) -- relevance ${(n.relevance*100).toFixed(0)}%
|
|
6
6
|
`,i+=`**Problem:** ${n.problem}
|
|
7
7
|
`,n.constraints&&(i+=`**Constraints:** ${n.constraints}
|
|
8
8
|
`),n.validated_fix&&(i+=`**Solution:** ${n.validated_fix}
|
|
9
9
|
`),n.why_it_worked&&(i+=`**Why:** ${n.why_it_worked}
|
|
10
|
-
`),n.outcome==="negative"&&(i+=
|
|
10
|
+
`),n.outcome==="negative"&&(i+=k(e.warn,"_This approach failed -- see solution for what to do instead_")+`
|
|
11
11
|
`);const D=n.confidence==null?"unknown":`${(n.confidence*100).toFixed(0)}%`;i+=`_Confidence: ${D} | Used ${n.reuse_count}x | Success rate: ${n.reuse_count>0?(n.reuse_success_count/n.reuse_count*100).toFixed(0)+"%":"not yet used"}_
|
|
12
12
|
|
|
13
|
-
`}return i+="_Use `wyrm_feedback` to record whether these were helpful._",i+o},{exemptStructured:!0});if((
|
|
13
|
+
`}return i+="_Use `wyrm_feedback` to record whether these were helpful._",i+o},{exemptStructured:!0});if((g==="link"||g!=="full"&&!!P?.())&&y.count>0){const t=y.results.map(e=>H(A(e.id),`memory #${e.id} (${e.kind})`,{description:e.problem.slice(0,120),mimeType:"application/json"}));return{..._,content:[..._.content,...t]}}return _}}];export{V as recallToolSpecs};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getCapabilities as f,renderCapabilityBriefing as y}from"../capabilities.js";import{decodeCursor as g}from"../keyset.js";const o="wyrm://",c="application/json",l="text/markdown",T=["truths","failures","quests","memory","stats"];function
|
|
1
|
+
import{getCapabilities as f,renderCapabilityBriefing as y}from"../capabilities.js";import{decodeCursor as g}from"../keyset.js";const o="wyrm://",c="application/json",l="text/markdown",T=["truths","failures","quests","memory","stats"];function R(r){return`${o}memory/${r}`}function $(r,t,e){const i=`${o}project/${r}/${t}`;return e?`${i}?cursor=${e}`:i}const h=`${o}capabilities`,j=`${o}stats`;function C(r,t,e){return{type:"resource_link",uri:r,name:t,...e?.description?{description:e.description}:{},...e?.mimeType?{mimeType:e.mimeType}:{}}}function d(r){if(!/^\d{1,15}$/.test(r))return null;const t=Number(r);return Number.isSafeInteger(t)&&t>=0?t:null}function E(r){if(typeof r!="string"||!r.startsWith(o))return null;let t=r.slice(o.length),e;const i=t.indexOf("?");if(i>=0){const s=t.slice(i+1);t=t.slice(0,i);const a=/^cursor=([A-Za-z0-9_-]{1,512})$/.exec(s);if(!a)return null;e=a[1]}if(/[\x00-\x1f\x7f]/.test(t)||t.includes(".."))return null;const n=t.split("/");if(n.some(s=>s.length===0))return null;if(n.length===1)return e!==void 0?null:n[0]==="capabilities"?{kind:"capabilities"}:n[0]==="stats"?{kind:"stats"}:null;if(n.length===2&&n[0]==="memory"){if(e!==void 0)return null;const s=d(n[1]);return s===null?null:{kind:"memory",id:s}}if(n.length===3&&n[0]==="project"){const s=d(n[1]);if(s===null)return null;const a=n[2];return T.includes(a)?e!==void 0&&a!=="memory"?null:{kind:"project",id:s,leaf:a,...e!==void 0?{cursor:e}:{}}:null}return null}function N(){return[{uri:h,name:"Wyrm capabilities",description:"Feature inventory + runtime state \u2014 what this memory system can do and which tool to reach for. The resource form of wyrm_capabilities.",mimeType:l},{uri:j,name:"Wyrm global stats",description:"Global database stats (projects, sessions, quests, data points, size). The resource form of wyrm_stats.",mimeType:c}]}function I(){return[{uriTemplate:`${o}memory/{id}`,name:"Memory artifact",description:"A single memory artifact (lesson/pattern/anti-pattern/heuristic/reasoning trace) by id.",mimeType:c},{uriTemplate:`${o}project/{id}/truths`,name:"Project ground truths",description:"A project's current ground truths (the authoritative facts).",mimeType:c},{uriTemplate:`${o}project/{id}/failures`,name:"Project failures",description:"A project's unresolved failure patterns (the negative-learning set).",mimeType:c},{uriTemplate:`${o}project/{id}/quests`,name:"Project quests",description:"A project's open quests (pending + in-progress).",mimeType:c},{uriTemplate:`${o}project/{id}/memory`,name:"Project memory",description:"A project's validated memory artifacts.",mimeType:c},{uriTemplate:`${o}project/{id}/stats`,name:"Project memory stats",description:"A project's memory artifact stats (totals by kind, avg confidence).",mimeType:c}]}const m=200;class p extends Error{uri;constructor(t,e){super(e),this.uri=t,this.name="ResourceNotFoundError"}}function u(r,t){return{uri:r,mimeType:c,text:JSON.stringify(t,null,2)}}function x(r,t){const e=E(r);if(!e)throw new p(r,`Unknown or malformed resource URI: ${r}`);switch(e.kind){case"capabilities":{const i=f(t.db.getDatabase(),t.WYRM_TOOL_COUNT,{hasEncryption:()=>!!process.env.WYRM_ENCRYPTION_KEY,isFederationEnabled:()=>!0,isAgentRunning:()=>{try{return t.agentDaemon.status().running}catch{return null}}});return{uri:r,mimeType:l,text:y(i)}}case"stats":return u(r,t.db.getStats());case"memory":{const i=t.memory.get(e.id);if(!i)throw new p(r,`Memory artifact not found: ${e.id}`);return u(r,i)}case"project":{if(!t.db.getProjectById(e.id))throw new p(r,`Project not found: ${e.id}`);switch(e.leaf){case"truths":return u(r,{project_id:e.id,truths:t.groundTruths.getCurrent(e.id)});case"failures":return u(r,{project_id:e.id,failures:t.failures.list(e.id,m)});case"quests":{const n=t.db.getDatabase().prepare("SELECT * FROM quests WHERE project_id = ? AND status IN ('pending','in_progress') ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END, id LIMIT ?").all(e.id,m);return u(r,{project_id:e.id,quests:n})}case"memory":{const n=g(e.cursor),s=t.memory.listPage(e.id,{pageSize:m,after:n}),a=u(r,{project_id:e.id,artifacts:s.items,...s.nextCursor?{next_cursor:s.nextCursor}:{}});return s.nextCursor&&(a.nextCursor=s.nextCursor),a}case"stats":return u(r,{project_id:e.id,stats:t.memory.getStats(e.id)})}}}}export{h as CAPABILITIES_URI,p as ResourceNotFoundError,j as STATS_URI,o as WYRM_URI_SCHEME,I as listWyrmResourceTemplates,N as listWyrmResources,R as memoryUri,E as parseWyrmUri,$ as projectUri,x as readWyrmResource,C as resourceLink};
|