wyrm-mcp 7.2.1 → 7.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.js +1 -60
  4. package/dist/agent-daemon.js +4 -281
  5. package/dist/agent-loop.js +7 -332
  6. package/dist/analytics.js +13 -236
  7. package/dist/attribution.js +1 -49
  8. package/dist/audit.js +2 -457
  9. package/dist/auto-capture.js +3 -138
  10. package/dist/auto-orchestrator.js +1 -325
  11. package/dist/autoconfig.js +39 -840
  12. package/dist/buddy-runner.js +1 -109
  13. package/dist/buddy.js +14 -564
  14. package/dist/build-flags.js +1 -17
  15. package/dist/capabilities.js +3 -183
  16. package/dist/capture.js +1 -56
  17. package/dist/causality.js +6 -107
  18. package/dist/cli.js +20 -281
  19. package/dist/cloud/cli.js +5 -541
  20. package/dist/cloud/client.js +1 -221
  21. package/dist/cloud/crypto.js +1 -85
  22. package/dist/cloud/machine-id.js +2 -113
  23. package/dist/cloud/recovery.js +1 -60
  24. package/dist/cloud/sync-engine.js +7 -543
  25. package/dist/cloud-backup.js +5 -579
  26. package/dist/cloud-profile.js +1 -138
  27. package/dist/cloud-sync-entrypoint.js +1 -47
  28. package/dist/cloud-sync.js +2 -309
  29. package/dist/constellation.js +12 -168
  30. package/dist/context-build-budgeted.js +4 -144
  31. package/dist/context-ranking.js +1 -69
  32. package/dist/crypto.js +1 -179
  33. package/dist/daemon-write-endpoint.js +1 -290
  34. package/dist/daemon-writer.js +2 -406
  35. package/dist/database.js +43 -1110
  36. package/dist/deprecations.js +2 -162
  37. package/dist/design.js +13 -141
  38. package/dist/event-replication.js +1 -112
  39. package/dist/events-sse.js +7 -43
  40. package/dist/events.js +6 -238
  41. package/dist/failure-patterns.js +42 -659
  42. package/dist/federation.js +12 -236
  43. package/dist/goals.js +13 -101
  44. package/dist/golden.js +3 -355
  45. package/dist/handlers/agent.js +4 -165
  46. package/dist/handlers/alias-adapters.js +1 -129
  47. package/dist/handlers/aliases.js +1 -171
  48. package/dist/handlers/audit.js +1 -87
  49. package/dist/handlers/boundary.js +1 -221
  50. package/dist/handlers/capture.js +73 -1109
  51. package/dist/handlers/causality.js +7 -114
  52. package/dist/handlers/cloud.js +85 -382
  53. package/dist/handlers/companion.js +28 -459
  54. package/dist/handlers/datalake.js +7 -187
  55. package/dist/handlers/dispatch-context.js +0 -22
  56. package/dist/handlers/entity.js +25 -256
  57. package/dist/handlers/events.js +16 -335
  58. package/dist/handlers/failure.js +13 -340
  59. package/dist/handlers/goals.js +4 -296
  60. package/dist/handlers/intelligence.js +126 -674
  61. package/dist/handlers/invoicing.js +1 -70
  62. package/dist/handlers/mcpclient.js +6 -137
  63. package/dist/handlers/orchestration.js +40 -125
  64. package/dist/handlers/output-schemas.js +1 -24
  65. package/dist/handlers/presence.js +3 -99
  66. package/dist/handlers/project.js +28 -182
  67. package/dist/handlers/prompts.js +6 -157
  68. package/dist/handlers/quest.js +4 -224
  69. package/dist/handlers/recall.js +11 -218
  70. package/dist/handlers/registry.js +1 -167
  71. package/dist/handlers/resources.js +1 -288
  72. package/dist/handlers/review.js +11 -74
  73. package/dist/handlers/run.js +17 -487
  74. package/dist/handlers/search.js +15 -326
  75. package/dist/handlers/session.js +28 -615
  76. package/dist/handlers/share.js +8 -184
  77. package/dist/handlers/shims.js +1 -464
  78. package/dist/handlers/skill.js +67 -449
  79. package/dist/handlers/survivors.js +1 -120
  80. package/dist/handlers/symbols.js +8 -109
  81. package/dist/handlers/syncops.js +4 -302
  82. package/dist/handlers/types.js +1 -27
  83. package/dist/harvest.js +5 -191
  84. package/dist/hours.js +7 -156
  85. package/dist/http-auth.js +3 -321
  86. package/dist/http-fast.js +21 -1137
  87. package/dist/icons.js +1 -47
  88. package/dist/index.js +2 -924
  89. package/dist/indexer.js +4 -145
  90. package/dist/intelligence.js +31 -261
  91. package/dist/internal-dispatch.js +3 -212
  92. package/dist/keyset.js +1 -110
  93. package/dist/knowledge-graph.js +12 -176
  94. package/dist/license.js +2 -441
  95. package/dist/logger.js +2 -199
  96. package/dist/maintenance.js +2 -148
  97. package/dist/mcp-client.js +6 -262
  98. package/dist/memory-artifacts.js +30 -449
  99. package/dist/migrate-prompt.js +2 -124
  100. package/dist/migrations.js +40 -655
  101. package/dist/performance.js +1 -228
  102. package/dist/presence.js +11 -140
  103. package/dist/priority-embed.js +5 -164
  104. package/dist/providers/embedding-provider.js +1 -196
  105. package/dist/readonly-gate.js +1 -29
  106. package/dist/rehydration.js +9 -157
  107. package/dist/reindex.js +1 -88
  108. package/dist/render-target.js +21 -514
  109. package/dist/render.js +4 -280
  110. package/dist/repl-guard.js +1 -173
  111. package/dist/replication-daemon-entrypoint.js +1 -31
  112. package/dist/replication-daemon.js +2 -262
  113. package/dist/resilience.js +1 -591
  114. package/dist/reverse-bridge.js +5 -360
  115. package/dist/security.js +1 -244
  116. package/dist/session-seen.js +3 -51
  117. package/dist/setup.js +1 -260
  118. package/dist/skill-author.js +5 -168
  119. package/dist/spec-kit.js +1 -191
  120. package/dist/sqlite-busy.js +1 -154
  121. package/dist/statusline.js +11 -315
  122. package/dist/sub-agent.js +13 -262
  123. package/dist/summarizer.js +13 -139
  124. package/dist/symbols.js +7 -283
  125. package/dist/sync.js +5 -359
  126. package/dist/tasks-dispatch.js +1 -84
  127. package/dist/tasks.js +1 -282
  128. package/dist/token-budget.js +1 -143
  129. package/dist/tool-analytics.js +7 -129
  130. package/dist/tool-annotations.js +1 -365
  131. package/dist/tool-manifest-v2.json +1 -1
  132. package/dist/tool-manifest.json +1 -1
  133. package/dist/tool-profiles.js +1 -75
  134. package/dist/trace-harvest.js +6 -244
  135. package/dist/types.js +1 -30
  136. package/dist/ui-dashboard.js +41 -50
  137. package/dist/ulid.js +1 -81
  138. package/dist/validate.js +1 -129
  139. package/dist/vault.js +1 -534
  140. package/dist/vectors.js +3 -184
  141. package/dist/version-check.js +4 -136
  142. package/dist/visibility.js +19 -155
  143. package/dist/wyrm-cli.js +98 -2464
  144. package/dist/wyrm-guard.js +14 -424
  145. package/dist/wyrm-loop.js +3 -150
  146. package/dist/wyrm-manifest.json +1 -1
  147. package/dist/wyrm-statusline-daemon.js +1 -11
  148. package/dist/wyrm-statusline.js +4 -56
  149. package/dist/wyrm-ui.js +9 -77
  150. package/package.json +4 -2
@@ -1,138 +1 @@
1
- /**
2
- * v7 F4 (T042) — Cloud parity v2: the cloud-capable subset of the STANDARD tier.
3
- *
4
- * Cloud parity v1 (`cloud-tools.json`, contract `wyrm-cloud-memory-v1`) was a
5
- * HAND-CURATED 9-name list: the memory/search/truth/quest/stats core the
6
- * mcp.wyrm.ghosts.lk edge first shipped. v2 makes the cloud subset a
7
- * MEASURABLE PROPERTY of the frozen surface instead of a guess (spec FR-4 /
8
- * §7 criterion 12, Article V — "per-tool cloudSupported flags generated from
9
- * the registry, cannot lie; cloud parity is a measurable property").
10
- *
11
- * THE v2 RULE (spec.md:334-336):
12
- *
13
- * cloud-capable v2 = STANDARD tier
14
- * − device-local
15
- * − egress
16
- * − subprocess
17
- *
18
- * The three exclusion classes below are the tools a STATELESS WORKER cannot
19
- * honestly serve against a remote D1/KV-backed store:
20
- *
21
- * - SUBPROCESS — spawns a local OS process / long-running loop / index build
22
- * on the operator's machine. Meaningless on the edge (no process to spawn,
23
- * no local Ollama, no daemon). wyrm_run (the agent run loop) +
24
- * wyrm_maintenance (reindex / vector-backfill).
25
- *
26
- * - EGRESS — reaches OUT to another network endpoint (a peer Worker, a third
27
- * -party MCP server, a federation peer, a companion model). The edge is the
28
- * server, not a client; it must not fan out. wyrm_call_external + wyrm_mcp
29
- * (outbound MCP client) + wyrm_replication (peer push/pull) + wyrm_share
30
- * (federation egress) + buddy (companion external replies).
31
- *
32
- * - DEVICE-LOCAL — semantically bound to THIS machine / process / on-disk
33
- * session, or a multi-action shim with a device-local action that touches
34
- * the local filesystem. A stateless Worker has no "this device", no local
35
- * agent presence, no per-process OODA loop, no server-self capability report
36
- * that matches the edge's surface, no local disk to write SKILL.md to, and no
37
- * local tree to scan. wyrm_act + wyrm_presence + wyrm_session +
38
- * wyrm_capabilities + wyrm_skill (create/register write SKILL.md) +
39
- * wyrm_project (scan walks the tree; sync reads/writes local .wyrm folders).
40
- *
41
- * Everything else on the standard tier is a pure local-DB memory op (capture /
42
- * recall / search / truth / quest / goal / review / entity / reference / audit /
43
- * causality / design-token / stats / failure / context) — the edge serves these
44
- * against its remote store with identical semantics. (NOTE: skill and project
45
- * are NOT in this list — their multi-action surfaces include a device-local FS
46
- * write/scan, so they are excluded above, not served pure-DB.)
47
- *
48
- * The sets are CLOSED over the standard tier: cloud-profile.test.ts asserts
49
- * every excluded name is a standard-tier member and the three classes are
50
- * disjoint, so the subtraction can never silently include a legacy/core-only
51
- * name or double-count. The measured v2 count is whatever this derivation
52
- * yields — published, never asserted to a magic literal beyond the spec's
53
- * ~18-22 sanity band.
54
- *
55
- * Article V (vendor-neutral): the exclusion is by TOOL CAPABILITY CLASS, never
56
- * by client/vendor name. Article VI (back-compat): every v1 name still
57
- * resolves under v2 — the 9 v1 names are either v2 members or aliases whose
58
- * target is a v2 member (cloudV1ResolvesUnderV2).
59
- *
60
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
61
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
62
- */
63
- import { FROZEN_TOOLS } from './tool-profiles.js';
64
- /** Contract names. v1 stays for back-compat; v2 is the expanded subset. */
65
- export const CLOUD_CONTRACT_V1 = 'wyrm-cloud-memory-v1';
66
- export const CLOUD_CONTRACT_V2 = 'wyrm-cloud-memory-v2';
67
- /**
68
- * SUBPROCESS — spawns a local process / loop / index build. Cannot run on a
69
- * stateless edge Worker.
70
- */
71
- export const CLOUD_EXCLUDE_SUBPROCESS = new Set([
72
- 'wyrm_run', // the agent run loop (start|join|status|debrief|end)
73
- 'wyrm_maintenance', // reindex / vector-backfill — local index build
74
- ]);
75
- /**
76
- * EGRESS — reaches out to another network endpoint. The edge is the server,
77
- * not a fan-out client.
78
- */
79
- export const CLOUD_EXCLUDE_EGRESS = new Set([
80
- 'wyrm_call_external', // call a registered external MCP tool
81
- 'wyrm_mcp', // outbound MCP client management (register/list/tools/disable)
82
- 'wyrm_replication', // peer push/pull + the Live-Memory event mesh
83
- 'wyrm_share', // cross-device federation share/unshare
84
- 'buddy', // companion buddy — collects external buddy replies
85
- ]);
86
- /**
87
- * DEVICE-LOCAL — bound to THIS machine / process / on-disk session; has no
88
- * meaning on a stateless Worker.
89
- */
90
- export const CLOUD_EXCLUDE_DEVICE_LOCAL = new Set([
91
- 'wyrm_act', // the per-process OODA-loop dispatcher
92
- 'wyrm_presence', // local agent presence on this device
93
- 'wyrm_session', // on-disk session lifecycle (start/update/rehydrate)
94
- 'wyrm_capabilities', // server-self capability report (matches THIS surface)
95
- // Multi-action noun shims whose ADVERTISED surface includes a device-local
96
- // action that writes/scans the operator's local filesystem — meaningless on a
97
- // stateless edge Worker. Under the COARSE name-level v2 derivation, a tool
98
- // whose surface includes ANY device-local action must NOT be declared
99
- // cloudSupported, even though some of its other actions are pure-DB. Declaring
100
- // them cloud-capable would make the v2 manifest LIE about parity (Article V).
101
- 'wyrm_skill', // action=create/register writes SKILL.md under WYRM_SKILLS_DIR (skill-author.ts:deploySkill)
102
- 'wyrm_project', // action=scan walks the local tree; action=sync reads/writes local .wyrm folders
103
- ]);
104
- /** The union of all three exclusion classes (device-local ∪ egress ∪ subprocess). */
105
- export const CLOUD_EXCLUDED_FROM_STANDARD = new Set([
106
- ...CLOUD_EXCLUDE_SUBPROCESS,
107
- ...CLOUD_EXCLUDE_EGRESS,
108
- ...CLOUD_EXCLUDE_DEVICE_LOCAL,
109
- ]);
110
- /**
111
- * THE cloud-capable v2 set — STANDARD tier minus the three exclusion classes.
112
- * Computed (never hand-listed) so the manifest cannot lie about parity.
113
- */
114
- export const CLOUD_CAPABLE_V2 = new Set([...FROZEN_TOOLS].filter((name) => !CLOUD_EXCLUDED_FROM_STANDARD.has(name)));
115
- /** Sorted array view (codepoint order) — deterministic iteration / manifests. */
116
- export const CLOUD_CAPABLE_V2_NAMES = [...CLOUD_CAPABLE_V2].sort();
117
- /** The measured v2 tool count (whatever the derivation yields). */
118
- export const CLOUD_CAPABLE_V2_COUNT = CLOUD_CAPABLE_V2.size;
119
- /**
120
- * v1 → v2 resolution check (Article VI): every v1 cloud name must still be
121
- * answered under v2 — either it IS a v2 member, or it is a hidden alias whose
122
- * routed target is a v2 member. `aliasTargetOf` is injected (the alias spine
123
- * lives in handlers/aliases.ts; passing it keeps cloud-profile.ts free of a
124
- * registry import cycle).
125
- */
126
- export function cloudV1ResolvesUnderV2(v1Names, aliasTargetOf) {
127
- const unresolved = [];
128
- for (const name of v1Names) {
129
- if (CLOUD_CAPABLE_V2.has(name))
130
- continue;
131
- const target = aliasTargetOf(name);
132
- if (target && CLOUD_CAPABLE_V2.has(target))
133
- continue;
134
- unresolved.push(name);
135
- }
136
- return { resolves: unresolved.length === 0, unresolved };
137
- }
138
- //# sourceMappingURL=cloud-profile.js.map
1
+ import{FROZEN_TOOLS as s}from"./tool-profiles.js";const E="wyrm-cloud-memory-v1",O="wyrm-cloud-memory-v2",c=new Set(["wyrm_run","wyrm_maintenance"]),C=new Set(["wyrm_call_external","wyrm_mcp","wyrm_replication","wyrm_share","buddy"]),m=new Set(["wyrm_act","wyrm_presence","wyrm_session","wyrm_capabilities","wyrm_skill","wyrm_project"]),w=new Set([...c,...C,...m]),e=new Set([...s].filter(r=>!w.has(r))),p=[...e].sort(),y=e.size;function D(r,_){const t=[];for(const o of r){if(e.has(o))continue;const n=_(o);n&&e.has(n)||t.push(o)}return{resolves:t.length===0,unresolved:t}}export{e as CLOUD_CAPABLE_V2,y as CLOUD_CAPABLE_V2_COUNT,p as CLOUD_CAPABLE_V2_NAMES,E as CLOUD_CONTRACT_V1,O as CLOUD_CONTRACT_V2,w as CLOUD_EXCLUDED_FROM_STANDARD,m as CLOUD_EXCLUDE_DEVICE_LOCAL,C as CLOUD_EXCLUDE_EGRESS,c as CLOUD_EXCLUDE_SUBPROCESS,D as cloudV1ResolvesUnderV2};
@@ -1,47 +1 @@
1
- /**
2
- * Wyrm Cloud Sync — daemon entrypoint.
3
- *
4
- * Spawned by `CloudSyncManager.start()` as a detached child process.
5
- * Reads R2 credentials from the inherited environment, instantiates the
6
- * backup client, and runs the sync loop until SIGTERM/SIGINT.
7
- *
8
- * Not meant to be invoked directly — go through the `wyrm_cloud_sync`
9
- * MCP tool which handles PID file management.
10
- *
11
- * argv[2] = interval_ms
12
- * argv[3] = keep_count
13
- *
14
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
15
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
16
- */
17
- import { WyrmCloudBackup } from './cloud-backup.js';
18
- import { CloudSyncDaemon, DEFAULT_INTERVAL_MS, DEFAULT_KEEP_COUNT, DEFAULT_DB_PATH } from './cloud-sync.js';
19
- async function main() {
20
- const intervalMs = Number(process.argv[2]) || DEFAULT_INTERVAL_MS;
21
- const keepCount = Number(process.argv[3]) || DEFAULT_KEEP_COUNT;
22
- // Configure cloud backup from environment — same code path as index.ts
23
- const cloud = new WyrmCloudBackup();
24
- if (process.env.WYRM_R2_ENDPOINT &&
25
- process.env.WYRM_R2_ACCESS_KEY &&
26
- process.env.WYRM_R2_SECRET_KEY) {
27
- cloud.configure({
28
- endpoint: process.env.WYRM_R2_ENDPOINT,
29
- bucket: process.env.WYRM_R2_BUCKET || 'wyrm-backups',
30
- accessKeyId: process.env.WYRM_R2_ACCESS_KEY,
31
- secretAccessKey: process.env.WYRM_R2_SECRET_KEY,
32
- }, process.env.WYRM_ENCRYPTION_KEY);
33
- }
34
- else {
35
- console.error('[cloud-sync-entrypoint] WYRM_R2_* env vars unset — refusing to start');
36
- process.exit(2);
37
- }
38
- const dbPath = process.env.WYRM_DB_PATH || DEFAULT_DB_PATH;
39
- const daemon = new CloudSyncDaemon(cloud, dbPath, intervalMs, keepCount);
40
- console.log(`[cloud-sync] started · interval=${intervalMs}ms · keep=${keepCount} · db=${dbPath}`);
41
- await daemon.run();
42
- }
43
- main().catch((e) => {
44
- console.error('[cloud-sync-entrypoint] fatal:', e);
45
- process.exit(1);
46
- });
47
- //# sourceMappingURL=cloud-sync-entrypoint.js.map
1
+ import{WyrmCloudBackup as r}from"./cloud-backup.js";import{CloudSyncDaemon as t,DEFAULT_INTERVAL_MS as _,DEFAULT_KEEP_COUNT as p,DEFAULT_DB_PATH as R}from"./cloud-sync.js";async function E(){const e=Number(process.argv[2])||_,s=Number(process.argv[3])||p,o=new r;process.env.WYRM_R2_ENDPOINT&&process.env.WYRM_R2_ACCESS_KEY&&process.env.WYRM_R2_SECRET_KEY?o.configure({endpoint:process.env.WYRM_R2_ENDPOINT,bucket:process.env.WYRM_R2_BUCKET||"wyrm-backups",accessKeyId:process.env.WYRM_R2_ACCESS_KEY,secretAccessKey:process.env.WYRM_R2_SECRET_KEY},process.env.WYRM_ENCRYPTION_KEY):(console.error("[cloud-sync-entrypoint] WYRM_R2_* env vars unset \u2014 refusing to start"),process.exit(2));const n=process.env.WYRM_DB_PATH||R,c=new t(o,n,e,s);console.log(`[cloud-sync] started \xB7 interval=${e}ms \xB7 keep=${s} \xB7 db=${n}`),await c.run()}E().catch(e=>{console.error("[cloud-sync-entrypoint] fatal:",e),process.exit(1)});
@@ -1,309 +1,2 @@
1
- /**
2
- * Wyrm Cloud Sync Phase 2 snapshot-sync daemon.
3
- *
4
- * Wraps `WyrmCloudBackup` (Phase 1) with a periodic upload + bootstrap
5
- * restore so a single operator can switch between machines with R2 as
6
- * the rendezvous point. Conflict policy: last-write-wins by mtime.
7
- * Not CRDT — designed for one operator on one machine at a time.
8
- *
9
- * Architecture is documented in docs/CLOUD-SYNC.md.
10
- *
11
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
12
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
13
- */
14
- import { spawn } from 'child_process';
15
- import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, openSync, closeSync, unlinkSync, renameSync, } from 'fs';
16
- import { createHash } from 'crypto';
17
- import { homedir, hostname } from 'os';
18
- import { join } from 'path';
19
- // ==================== CONSTANTS ====================
20
- const WYRM_DIR = join(homedir(), '.wyrm');
21
- const PID_FILE = join(WYRM_DIR, 'wyrm-cloud-sync.pid');
22
- const LOG_FILE = join(WYRM_DIR, 'wyrm-cloud-sync.log');
23
- const STATE_FILE = join(WYRM_DIR, 'wyrm-cloud-sync.state');
24
- const DEFAULT_DB_PATH = join(homedir(), '.copilot', 'session-store.db');
25
- const DEFAULT_INTERVAL_MS = 10 * 60 * 1000; // 10 min
26
- const DEFAULT_KEEP_COUNT = 20;
27
- const DRIFT_MS = 60_000; // 1 min clock-skew tolerance
28
- const LOG_ROTATE_BYTES = 1_000_000; // 1 MB
29
- const PROCESS_LIVENESS_SIGNAL = 0; // ping with signal 0 — check only
30
- // ==================== DAEMON ====================
31
- /**
32
- * Long-running daemon. One instance per machine.
33
- *
34
- * Lifecycle:
35
- * bootstrap() once on start — restore from R2 if remote is newer
36
- * tick() every interval — hash local DB, upload if changed, prune
37
- * shutdown() on SIGTERM — final tick to capture pending writes
38
- */
39
- export class CloudSyncDaemon {
40
- cloud;
41
- dbPath;
42
- intervalMs;
43
- keepCount;
44
- constructor(cloud, dbPath = DEFAULT_DB_PATH, intervalMs = DEFAULT_INTERVAL_MS, keepCount = DEFAULT_KEEP_COUNT) {
45
- this.cloud = cloud;
46
- this.dbPath = dbPath;
47
- this.intervalMs = intervalMs;
48
- this.keepCount = keepCount;
49
- ensureWyrmDir();
50
- }
51
- /**
52
- * On startup, compare latest cloud snapshot vs local DB mtime.
53
- * If cloud is newer (beyond clock-skew drift), restore it.
54
- */
55
- async bootstrap() {
56
- if (!this.cloud.isConfigured()) {
57
- return { action: 'no-config', reason: 'WYRM_R2_* env vars unset' };
58
- }
59
- let remote;
60
- try {
61
- remote = await this.cloud.listBackupsWithKeys();
62
- }
63
- catch (e) {
64
- return { action: 'no-remote', reason: e.message ?? String(e) };
65
- }
66
- if (remote.length === 0) {
67
- return { action: 'no-op', reason: 'no remote snapshots yet' };
68
- }
69
- // listBackupsWithKeys returns newest first
70
- const latest = remote[0];
71
- const remoteMs = Date.parse(latest.metadata.timestamp);
72
- const localMs = existsSync(this.dbPath) ? statSync(this.dbPath).mtimeMs : 0;
73
- if (remoteMs > localMs + DRIFT_MS) {
74
- await this.cloud.restore(latest.key, this.dbPath);
75
- this.patchState({ last_restored_key: latest.key });
76
- return { action: 'restored', key: latest.key };
77
- }
78
- return { action: 'no-op', reason: 'local DB is current' };
79
- }
80
- /**
81
- * One sync tick. Cheap when nothing changed (just a SHA-256 of the DB).
82
- */
83
- async tick() {
84
- if (!this.cloud.isConfigured()) {
85
- return { action: 'error', reason: 'WYRM_R2_* env vars unset' };
86
- }
87
- if (!existsSync(this.dbPath)) {
88
- return { action: 'no-db' };
89
- }
90
- const hash = sha256(this.dbPath);
91
- const state = this.readState();
92
- this.patchState({ last_check_ts: Date.now() });
93
- if (hash === state.last_uploaded_hash) {
94
- return { action: 'unchanged', hash };
95
- }
96
- try {
97
- const { key } = await this.cloud.backup(this.dbPath);
98
- this.patchState({
99
- last_uploaded_hash: hash,
100
- last_uploaded_ts: Date.now(),
101
- last_uploaded_key: key,
102
- });
103
- await this.cloud.pruneBackups(this.keepCount);
104
- return { action: 'uploaded', key, hash };
105
- }
106
- catch (e) {
107
- return { action: 'error', reason: e.message ?? String(e) };
108
- }
109
- }
110
- /**
111
- * Long-running loop. Should be spawned as a detached child via Manager.
112
- * Logs to STDOUT (which the spawn redirects to LOG_FILE).
113
- */
114
- async run() {
115
- const bootstrapResult = await this.bootstrap();
116
- log(`bootstrap: ${JSON.stringify(bootstrapResult)}`);
117
- const timer = setInterval(async () => {
118
- try {
119
- const result = await this.tick();
120
- if (result.action !== 'unchanged') {
121
- log(`tick: ${JSON.stringify(result)}`);
122
- }
123
- }
124
- catch (e) {
125
- log(`tick error: ${e.message ?? e}`);
126
- }
127
- }, this.intervalMs);
128
- const shutdown = async (signal) => {
129
- clearInterval(timer);
130
- log(`${signal} — final tick before exit`);
131
- try {
132
- const final = await this.tick();
133
- log(`final tick: ${JSON.stringify(final)}`);
134
- }
135
- catch (e) {
136
- log(`final tick error: ${e.message ?? e}`);
137
- }
138
- process.exit(0);
139
- };
140
- process.on('SIGTERM', () => void shutdown('SIGTERM'));
141
- process.on('SIGINT', () => void shutdown('SIGINT'));
142
- }
143
- readState() {
144
- return readState();
145
- }
146
- patchState(patch) {
147
- patchState(patch);
148
- }
149
- }
150
- // ==================== MANAGER ====================
151
- /**
152
- * Spawn/stop/status for the daemon — used by the MCP tool handler.
153
- * Guarantees single-instance via PID file + liveness check.
154
- */
155
- export class CloudSyncManager {
156
- daemonEntrypoint;
157
- constructor(daemonEntrypoint) {
158
- this.daemonEntrypoint = daemonEntrypoint;
159
- ensureWyrmDir();
160
- }
161
- start(opts = {}) {
162
- if (this.isAlive()) {
163
- return { ok: false, pid: this.readPid(), reason: 'already running' };
164
- }
165
- rotateLogIfBig();
166
- const intervalMs = (opts.interval_minutes ?? DEFAULT_INTERVAL_MS / 60_000) * 60_000;
167
- const keepCount = opts.keep_count ?? DEFAULT_KEEP_COUNT;
168
- const out = openSync(LOG_FILE, 'a');
169
- const err = openSync(LOG_FILE, 'a');
170
- const child = spawn(process.execPath, [
171
- this.daemonEntrypoint,
172
- String(intervalMs),
173
- String(keepCount),
174
- ], {
175
- detached: true,
176
- stdio: ['ignore', out, err],
177
- env: { ...process.env },
178
- });
179
- child.unref();
180
- closeSync(out);
181
- closeSync(err);
182
- if (typeof child.pid !== 'number') {
183
- return { ok: false, reason: 'spawn returned no pid' };
184
- }
185
- writeFileSync(PID_FILE, String(child.pid));
186
- return { ok: true, pid: child.pid };
187
- }
188
- stop() {
189
- if (!this.isAlive()) {
190
- this.cleanPid();
191
- return { ok: false, reason: 'not running' };
192
- }
193
- const pid = this.readPid();
194
- if (!pid) {
195
- return { ok: false, reason: 'pid file unreadable' };
196
- }
197
- try {
198
- process.kill(pid, 'SIGTERM');
199
- // Give it up to 5s to exit cleanly. Caller can poll status().
200
- return { ok: true };
201
- }
202
- catch (e) {
203
- return { ok: false, reason: e.message ?? String(e) };
204
- }
205
- }
206
- status() {
207
- const running = this.isAlive();
208
- return {
209
- running,
210
- pid: running ? this.readPid() ?? undefined : undefined,
211
- state: existsSync(STATE_FILE) ? safeReadState() : undefined,
212
- };
213
- }
214
- async forceSync(daemon) {
215
- return daemon.tick();
216
- }
217
- isAlive() {
218
- if (!existsSync(PID_FILE))
219
- return false;
220
- const pid = this.readPid();
221
- if (!pid)
222
- return false;
223
- try {
224
- process.kill(pid, PROCESS_LIVENESS_SIGNAL);
225
- return true;
226
- }
227
- catch {
228
- return false;
229
- }
230
- }
231
- readPid() {
232
- try {
233
- const raw = readFileSync(PID_FILE, 'utf8').trim();
234
- const n = Number(raw);
235
- return Number.isFinite(n) && n > 0 ? n : undefined;
236
- }
237
- catch {
238
- return undefined;
239
- }
240
- }
241
- cleanPid() {
242
- try {
243
- unlinkSync(PID_FILE);
244
- }
245
- catch { /* idempotent */ }
246
- }
247
- }
248
- // ==================== HELPERS ====================
249
- function ensureWyrmDir() {
250
- if (!existsSync(WYRM_DIR))
251
- mkdirSync(WYRM_DIR, { recursive: true });
252
- }
253
- function sha256(filePath) {
254
- // For typical Wyrm DBs (<100MB) a single readFileSync is fine.
255
- // If DBs grow much larger this should switch to a streaming hash.
256
- return createHash('sha256').update(readFileSync(filePath)).digest('hex');
257
- }
258
- function emptyState() {
259
- return {
260
- last_uploaded_hash: '',
261
- last_uploaded_ts: 0,
262
- last_uploaded_key: null,
263
- last_restored_key: null,
264
- last_check_ts: 0,
265
- machine: hostname(),
266
- };
267
- }
268
- function readState() {
269
- return safeReadState() ?? emptyState();
270
- }
271
- function safeReadState() {
272
- try {
273
- const parsed = JSON.parse(readFileSync(STATE_FILE, 'utf8'));
274
- return { ...emptyState(), ...parsed };
275
- }
276
- catch {
277
- return undefined;
278
- }
279
- }
280
- function patchState(patch) {
281
- const cur = readState();
282
- writeFileSync(STATE_FILE, JSON.stringify({ ...cur, ...patch, machine: hostname() }, null, 2));
283
- }
284
- function log(msg) {
285
- const line = `[${new Date().toISOString()}] ${msg}\n`;
286
- // Daemon's stdout/stderr is already redirected to LOG_FILE by the spawn().
287
- // When tick() is called from the MCP tool (not daemon), the message is dropped — that's fine.
288
- process.stdout.write(line);
289
- }
290
- function rotateLogIfBig() {
291
- if (!existsSync(LOG_FILE))
292
- return;
293
- try {
294
- const size = statSync(LOG_FILE).size;
295
- if (size > LOG_ROTATE_BYTES) {
296
- const rotated = `${LOG_FILE}.1`;
297
- try {
298
- unlinkSync(rotated);
299
- }
300
- catch { /* ignore */ }
301
- renameSync(LOG_FILE, rotated);
302
- }
303
- }
304
- catch { /* non-fatal */ }
305
- }
306
- // ==================== EXPORTS ====================
307
- export { ensureWyrmDir, readState, sha256 };
308
- export { DEFAULT_DB_PATH, DEFAULT_INTERVAL_MS, DEFAULT_KEEP_COUNT };
309
- //# sourceMappingURL=cloud-sync.js.map
1
+ import{spawn as D}from"child_process";import{existsSync as a,readFileSync as p,writeFileSync as k,statSync as g,mkdirSync as N,openSync as E,closeSync as b,unlinkSync as v,renameSync as A}from"fs";import{createHash as L}from"crypto";import{homedir as w,hostname as I}from"os";import{join as u}from"path";const d=u(w(),".wyrm"),h=u(d,"wyrm-cloud-sync.pid"),o=u(d,"wyrm-cloud-sync.log"),f=u(d,"wyrm-cloud-sync.state"),P=u(w(),".copilot","session-store.db"),y=600*1e3,S=20,O=6e4,C=1e6,F=0;class Y{cloud;dbPath;intervalMs;keepCount;constructor(t,e=P,n=y,r=S){this.cloud=t,this.dbPath=e,this.intervalMs=n,this.keepCount=r,_()}async bootstrap(){if(!this.cloud.isConfigured())return{action:"no-config",reason:"WYRM_R2_* env vars unset"};let t;try{t=await this.cloud.listBackupsWithKeys()}catch(i){return{action:"no-remote",reason:i.message??String(i)}}if(t.length===0)return{action:"no-op",reason:"no remote snapshots yet"};const e=t[0],n=Date.parse(e.metadata.timestamp),r=a(this.dbPath)?g(this.dbPath).mtimeMs:0;return n>r+O?(await this.cloud.restore(e.key,this.dbPath),this.patchState({last_restored_key:e.key}),{action:"restored",key:e.key}):{action:"no-op",reason:"local DB is current"}}async tick(){if(!this.cloud.isConfigured())return{action:"error",reason:"WYRM_R2_* env vars unset"};if(!a(this.dbPath))return{action:"no-db"};const t=T(this.dbPath),e=this.readState();if(this.patchState({last_check_ts:Date.now()}),t===e.last_uploaded_hash)return{action:"unchanged",hash:t};try{const{key:n}=await this.cloud.backup(this.dbPath);return this.patchState({last_uploaded_hash:t,last_uploaded_ts:Date.now(),last_uploaded_key:n}),await this.cloud.pruneBackups(this.keepCount),{action:"uploaded",key:n,hash:t}}catch(n){return{action:"error",reason:n.message??String(n)}}}async run(){const t=await this.bootstrap();c(`bootstrap: ${JSON.stringify(t)}`);const e=setInterval(async()=>{try{const r=await this.tick();r.action!=="unchanged"&&c(`tick: ${JSON.stringify(r)}`)}catch(r){c(`tick error: ${r.message??r}`)}},this.intervalMs),n=async r=>{clearInterval(e),c(`${r} \u2014 final tick before exit`);try{const i=await this.tick();c(`final tick: ${JSON.stringify(i)}`)}catch(i){c(`final tick error: ${i.message??i}`)}process.exit(0)};process.on("SIGTERM",()=>{n("SIGTERM")}),process.on("SIGINT",()=>{n("SIGINT")})}readState(){return m()}patchState(t){x(t)}}class z{daemonEntrypoint;constructor(t){this.daemonEntrypoint=t,_()}start(t={}){if(this.isAlive())return{ok:!1,pid:this.readPid(),reason:"already running"};$();const e=(t.interval_minutes??y/6e4)*6e4,n=t.keep_count??S,r=E(o,"a"),i=E(o,"a"),l=D(process.execPath,[this.daemonEntrypoint,String(e),String(n)],{detached:!0,stdio:["ignore",r,i],env:{...process.env}});return l.unref(),b(r),b(i),typeof l.pid!="number"?{ok:!1,reason:"spawn returned no pid"}:(k(h,String(l.pid)),{ok:!0,pid:l.pid})}stop(){if(!this.isAlive())return this.cleanPid(),{ok:!1,reason:"not running"};const t=this.readPid();if(!t)return{ok:!1,reason:"pid file unreadable"};try{return process.kill(t,"SIGTERM"),{ok:!0}}catch(e){return{ok:!1,reason:e.message??String(e)}}}status(){const t=this.isAlive();return{running:t,pid:t?this.readPid()??void 0:void 0,state:a(f)?R():void 0}}async forceSync(t){return t.tick()}isAlive(){if(!a(h))return!1;const t=this.readPid();if(!t)return!1;try{return process.kill(t,F),!0}catch{return!1}}readPid(){try{const t=p(h,"utf8").trim(),e=Number(t);return Number.isFinite(e)&&e>0?e:void 0}catch{return}}cleanPid(){try{v(h)}catch{}}}function _(){a(d)||N(d,{recursive:!0})}function T(s){return L("sha256").update(p(s)).digest("hex")}function M(){return{last_uploaded_hash:"",last_uploaded_ts:0,last_uploaded_key:null,last_restored_key:null,last_check_ts:0,machine:I()}}function m(){return R()??M()}function R(){try{const s=JSON.parse(p(f,"utf8"));return{...M(),...s}}catch{return}}function x(s){const t=m();k(f,JSON.stringify({...t,...s,machine:I()},null,2))}function c(s){const t=`[${new Date().toISOString()}] ${s}
2
+ `;process.stdout.write(t)}function $(){if(a(o))try{if(g(o).size>C){const t=`${o}.1`;try{v(t)}catch{}A(o,t)}}catch{}}export{Y as CloudSyncDaemon,z as CloudSyncManager,P as DEFAULT_DB_PATH,y as DEFAULT_INTERVAL_MS,S as DEFAULT_KEEP_COUNT,_ as ensureWyrmDir,m as readState,T as sha256};