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.
- package/LICENSE +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- package/package.json +4 -2
package/dist/cloud-profile.js
CHANGED
|
@@ -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)});
|
package/dist/cloud-sync.js
CHANGED
|
@@ -1,309 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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};
|