wyrm-mcp 7.2.0 → 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 (156) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.d.ts.map +1 -1
  4. package/dist/activation.js +1 -44
  5. package/dist/activation.js.map +1 -1
  6. package/dist/agent-daemon.js +4 -281
  7. package/dist/agent-loop.js +7 -332
  8. package/dist/analytics.js +13 -236
  9. package/dist/attribution.js +1 -49
  10. package/dist/audit.js +2 -457
  11. package/dist/auto-capture.js +3 -138
  12. package/dist/auto-orchestrator.js +1 -325
  13. package/dist/autoconfig.js +39 -840
  14. package/dist/buddy-runner.js +1 -109
  15. package/dist/buddy.js +14 -564
  16. package/dist/build-flags.js +1 -17
  17. package/dist/capabilities.js +3 -183
  18. package/dist/capture.js +1 -56
  19. package/dist/causality.js +6 -107
  20. package/dist/cli.js +20 -281
  21. package/dist/cloud/cli.js +5 -541
  22. package/dist/cloud/client.js +1 -221
  23. package/dist/cloud/crypto.js +1 -85
  24. package/dist/cloud/machine-id.js +2 -113
  25. package/dist/cloud/recovery.js +1 -60
  26. package/dist/cloud/sync-engine.js +7 -543
  27. package/dist/cloud-backup.js +5 -579
  28. package/dist/cloud-profile.js +1 -138
  29. package/dist/cloud-sync-entrypoint.js +1 -47
  30. package/dist/cloud-sync.js +2 -309
  31. package/dist/constellation.js +12 -168
  32. package/dist/context-build-budgeted.js +4 -144
  33. package/dist/context-ranking.js +1 -69
  34. package/dist/crypto.js +1 -179
  35. package/dist/daemon-write-endpoint.js +1 -290
  36. package/dist/daemon-writer.js +2 -406
  37. package/dist/database.js +43 -1110
  38. package/dist/deprecations.js +2 -162
  39. package/dist/design.js +13 -141
  40. package/dist/event-replication.js +1 -112
  41. package/dist/events-sse.js +7 -43
  42. package/dist/events.js +6 -238
  43. package/dist/failure-patterns.js +42 -659
  44. package/dist/federation.js +12 -236
  45. package/dist/goals.js +13 -101
  46. package/dist/golden.js +3 -355
  47. package/dist/handlers/agent.js +4 -165
  48. package/dist/handlers/alias-adapters.js +1 -129
  49. package/dist/handlers/aliases.js +1 -171
  50. package/dist/handlers/audit.js +1 -87
  51. package/dist/handlers/boundary.js +1 -221
  52. package/dist/handlers/capture.js +73 -1109
  53. package/dist/handlers/causality.js +7 -114
  54. package/dist/handlers/cloud.js +85 -382
  55. package/dist/handlers/companion.js +28 -459
  56. package/dist/handlers/datalake.js +7 -187
  57. package/dist/handlers/dispatch-context.js +0 -22
  58. package/dist/handlers/entity.js +25 -256
  59. package/dist/handlers/events.js +16 -335
  60. package/dist/handlers/failure.js +13 -340
  61. package/dist/handlers/goals.js +4 -296
  62. package/dist/handlers/intelligence.js +126 -674
  63. package/dist/handlers/invoicing.js +1 -70
  64. package/dist/handlers/mcpclient.js +6 -137
  65. package/dist/handlers/orchestration.js +40 -125
  66. package/dist/handlers/output-schemas.js +1 -24
  67. package/dist/handlers/presence.js +3 -99
  68. package/dist/handlers/project.js +28 -182
  69. package/dist/handlers/prompts.js +6 -157
  70. package/dist/handlers/quest.js +4 -224
  71. package/dist/handlers/recall.js +11 -218
  72. package/dist/handlers/registry.js +1 -167
  73. package/dist/handlers/resources.js +1 -288
  74. package/dist/handlers/review.js +11 -74
  75. package/dist/handlers/run.js +17 -487
  76. package/dist/handlers/search.js +15 -326
  77. package/dist/handlers/session.js +28 -615
  78. package/dist/handlers/share.js +8 -184
  79. package/dist/handlers/shims.js +1 -464
  80. package/dist/handlers/skill.js +67 -449
  81. package/dist/handlers/survivors.js +1 -120
  82. package/dist/handlers/symbols.js +8 -109
  83. package/dist/handlers/syncops.js +4 -302
  84. package/dist/handlers/types.js +1 -27
  85. package/dist/harvest.js +5 -191
  86. package/dist/hours.js +7 -156
  87. package/dist/http-auth.js +3 -321
  88. package/dist/http-fast.js +21 -1137
  89. package/dist/icons.js +1 -47
  90. package/dist/index.js +2 -924
  91. package/dist/indexer.js +4 -145
  92. package/dist/intelligence.js +31 -261
  93. package/dist/internal-dispatch.js +3 -212
  94. package/dist/keyset.js +1 -110
  95. package/dist/knowledge-graph.js +12 -176
  96. package/dist/license.d.ts +11 -0
  97. package/dist/license.d.ts.map +1 -1
  98. package/dist/license.js +2 -414
  99. package/dist/license.js.map +1 -1
  100. package/dist/logger.js +2 -199
  101. package/dist/maintenance.js +2 -148
  102. package/dist/mcp-client.js +6 -262
  103. package/dist/memory-artifacts.js +30 -449
  104. package/dist/migrate-prompt.js +2 -124
  105. package/dist/migrations.js +40 -655
  106. package/dist/performance.js +1 -228
  107. package/dist/presence.js +11 -140
  108. package/dist/priority-embed.js +5 -164
  109. package/dist/providers/embedding-provider.js +1 -196
  110. package/dist/readonly-gate.js +1 -29
  111. package/dist/rehydration.js +9 -157
  112. package/dist/reindex.js +1 -88
  113. package/dist/render-target.js +21 -514
  114. package/dist/render.js +4 -280
  115. package/dist/repl-guard.js +1 -173
  116. package/dist/replication-daemon-entrypoint.js +1 -31
  117. package/dist/replication-daemon.js +2 -262
  118. package/dist/resilience.js +1 -591
  119. package/dist/reverse-bridge.js +5 -360
  120. package/dist/security.js +1 -244
  121. package/dist/session-seen.js +3 -51
  122. package/dist/setup.js +1 -260
  123. package/dist/skill-author.js +5 -168
  124. package/dist/spec-kit.js +1 -191
  125. package/dist/sqlite-busy.js +1 -154
  126. package/dist/statusline.js +11 -315
  127. package/dist/sub-agent.js +13 -262
  128. package/dist/summarizer.js +13 -139
  129. package/dist/symbols.js +7 -283
  130. package/dist/sync.js +5 -359
  131. package/dist/tasks-dispatch.js +1 -84
  132. package/dist/tasks.js +1 -282
  133. package/dist/token-budget.js +1 -143
  134. package/dist/tool-analytics.js +7 -129
  135. package/dist/tool-annotations.js +1 -365
  136. package/dist/tool-manifest-v2.json +1 -1
  137. package/dist/tool-manifest.json +1 -1
  138. package/dist/tool-profiles.js +1 -75
  139. package/dist/trace-harvest.js +6 -244
  140. package/dist/types.js +1 -30
  141. package/dist/ui-dashboard.js +41 -50
  142. package/dist/ulid.js +1 -81
  143. package/dist/validate.js +1 -129
  144. package/dist/vault.js +1 -534
  145. package/dist/vectors.js +3 -184
  146. package/dist/version-check.js +4 -136
  147. package/dist/visibility.js +19 -155
  148. package/dist/wyrm-cli.js +98 -2451
  149. package/dist/wyrm-cli.js.map +1 -1
  150. package/dist/wyrm-guard.js +14 -424
  151. package/dist/wyrm-loop.js +3 -150
  152. package/dist/wyrm-manifest.json +1 -1
  153. package/dist/wyrm-statusline-daemon.js +1 -11
  154. package/dist/wyrm-statusline.js +4 -56
  155. package/dist/wyrm-ui.js +9 -77
  156. 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};