wyrm-mcp 7.3.2 → 7.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +8 -14
  2. package/dist/activation.js +59 -1
  3. package/dist/agent-daemon.js +281 -4
  4. package/dist/agent-loop.js +332 -7
  5. package/dist/analytics.js +236 -13
  6. package/dist/attribution.js +49 -1
  7. package/dist/audit.js +457 -2
  8. package/dist/auto-capture.js +138 -3
  9. package/dist/auto-orchestrator.js +325 -1
  10. package/dist/autoconfig.d.ts +50 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1115 -39
  13. package/dist/autoconfig.js.map +1 -1
  14. package/dist/buddy-runner.js +109 -1
  15. package/dist/buddy.js +564 -14
  16. package/dist/build-flags.js +15 -1
  17. package/dist/capabilities.js +183 -3
  18. package/dist/capture.js +56 -1
  19. package/dist/causality.js +148 -8
  20. package/dist/cli.js +281 -20
  21. package/dist/cloud/cli.js +541 -5
  22. package/dist/cloud/client.js +221 -1
  23. package/dist/cloud/crypto.js +85 -1
  24. package/dist/cloud/machine-id.js +113 -2
  25. package/dist/cloud/recovery.js +60 -1
  26. package/dist/cloud/sync-engine.js +543 -7
  27. package/dist/cloud-backup.js +579 -5
  28. package/dist/cloud-profile.js +138 -1
  29. package/dist/cloud-sync-entrypoint.js +47 -1
  30. package/dist/cloud-sync.js +309 -2
  31. package/dist/connectors/bridge-source.d.ts +46 -0
  32. package/dist/connectors/bridge-source.d.ts.map +1 -0
  33. package/dist/connectors/bridge-source.js +77 -0
  34. package/dist/connectors/bridge-source.js.map +1 -0
  35. package/dist/connectors/index.d.ts +24 -0
  36. package/dist/connectors/index.d.ts.map +1 -0
  37. package/dist/connectors/index.js +69 -0
  38. package/dist/connectors/index.js.map +1 -0
  39. package/dist/connectors/ingest.d.ts +16 -0
  40. package/dist/connectors/ingest.d.ts.map +1 -0
  41. package/dist/connectors/ingest.js +116 -0
  42. package/dist/connectors/ingest.js.map +1 -0
  43. package/dist/connectors/types.d.ts +99 -0
  44. package/dist/connectors/types.d.ts.map +1 -0
  45. package/dist/connectors/types.js +17 -0
  46. package/dist/connectors/types.js.map +1 -0
  47. package/dist/constellation.js +168 -12
  48. package/dist/content-signature.js +45 -1
  49. package/dist/context-build-budgeted.js +144 -4
  50. package/dist/context-ranking.js +69 -1
  51. package/dist/crypto.js +179 -1
  52. package/dist/daemon-write-endpoint.js +290 -1
  53. package/dist/daemon-writer.js +406 -2
  54. package/dist/database.js +1278 -53
  55. package/dist/deprecations.js +162 -2
  56. package/dist/design.js +141 -13
  57. package/dist/event-replication.js +112 -1
  58. package/dist/events-sse.js +43 -7
  59. package/dist/events.js +238 -6
  60. package/dist/failure-patterns.d.ts +107 -0
  61. package/dist/failure-patterns.d.ts.map +1 -1
  62. package/dist/failure-patterns.js +924 -43
  63. package/dist/failure-patterns.js.map +1 -1
  64. package/dist/federation.js +236 -12
  65. package/dist/goals.js +101 -13
  66. package/dist/golden.js +355 -3
  67. package/dist/handlers/agent.js +165 -4
  68. package/dist/handlers/alias-adapters.js +129 -1
  69. package/dist/handlers/aliases.js +171 -1
  70. package/dist/handlers/audit.js +87 -1
  71. package/dist/handlers/boundary.js +221 -1
  72. package/dist/handlers/capture.js +1114 -73
  73. package/dist/handlers/causality.js +119 -9
  74. package/dist/handlers/cloud.js +382 -85
  75. package/dist/handlers/companion.js +459 -28
  76. package/dist/handlers/datalake.js +187 -7
  77. package/dist/handlers/dispatch-context.js +22 -0
  78. package/dist/handlers/entity.js +256 -25
  79. package/dist/handlers/events.js +335 -16
  80. package/dist/handlers/failure.d.ts.map +1 -1
  81. package/dist/handlers/failure.js +408 -13
  82. package/dist/handlers/failure.js.map +1 -1
  83. package/dist/handlers/goals.js +296 -4
  84. package/dist/handlers/intelligence.js +681 -126
  85. package/dist/handlers/invoicing.js +70 -1
  86. package/dist/handlers/mcpclient.js +137 -6
  87. package/dist/handlers/orchestration.js +125 -40
  88. package/dist/handlers/output-schemas.js +24 -1
  89. package/dist/handlers/presence.js +99 -3
  90. package/dist/handlers/project.js +182 -28
  91. package/dist/handlers/prompts.js +157 -6
  92. package/dist/handlers/quest.js +224 -4
  93. package/dist/handlers/recall.js +237 -13
  94. package/dist/handlers/registry.js +167 -1
  95. package/dist/handlers/resources.js +288 -1
  96. package/dist/handlers/review.js +74 -11
  97. package/dist/handlers/run.js +498 -16
  98. package/dist/handlers/search.js +338 -15
  99. package/dist/handlers/session.js +643 -31
  100. package/dist/handlers/share.js +184 -8
  101. package/dist/handlers/shims.js +464 -1
  102. package/dist/handlers/skill.js +449 -67
  103. package/dist/handlers/survivors.js +120 -1
  104. package/dist/handlers/symbols.js +109 -8
  105. package/dist/handlers/syncops.js +302 -4
  106. package/dist/handlers/types.js +27 -1
  107. package/dist/harvest.js +191 -5
  108. package/dist/hours.js +156 -7
  109. package/dist/http-auth.js +321 -3
  110. package/dist/http-fast.js +1302 -22
  111. package/dist/icons.js +47 -1
  112. package/dist/importers.js +268 -1
  113. package/dist/index.js +840 -2
  114. package/dist/indexer.js +145 -4
  115. package/dist/intelligence.js +261 -31
  116. package/dist/internal-dispatch.js +212 -3
  117. package/dist/keyset.js +110 -1
  118. package/dist/knowledge-graph.js +176 -12
  119. package/dist/license.js +441 -2
  120. package/dist/logger.js +199 -2
  121. package/dist/maintenance.js +148 -2
  122. package/dist/mcp-client.js +262 -6
  123. package/dist/memory-artifacts.js +596 -32
  124. package/dist/migrate-prompt.js +124 -2
  125. package/dist/migrations.d.ts.map +1 -1
  126. package/dist/migrations.js +799 -42
  127. package/dist/migrations.js.map +1 -1
  128. package/dist/performance.js +228 -1
  129. package/dist/presence.js +140 -11
  130. package/dist/priority-embed.js +164 -5
  131. package/dist/providers/embedding-provider.js +196 -1
  132. package/dist/readonly-gate.js +29 -1
  133. package/dist/receipt.js +43 -1
  134. package/dist/rehydration.js +157 -9
  135. package/dist/reindex.js +88 -1
  136. package/dist/render-target.js +544 -21
  137. package/dist/render.js +280 -4
  138. package/dist/repl-guard.js +173 -1
  139. package/dist/replication-daemon-entrypoint.js +31 -1
  140. package/dist/replication-daemon.js +262 -2
  141. package/dist/rerank.js +142 -1
  142. package/dist/resilience.js +591 -1
  143. package/dist/reverse-bridge.js +360 -5
  144. package/dist/security.js +244 -1
  145. package/dist/session-seen.js +51 -3
  146. package/dist/setup.js +260 -1
  147. package/dist/skill-author.js +168 -5
  148. package/dist/spec-kit.js +191 -1
  149. package/dist/sqlite-busy.js +154 -1
  150. package/dist/statusline.js +315 -11
  151. package/dist/sub-agent.js +262 -13
  152. package/dist/summarizer.js +139 -13
  153. package/dist/symbols.js +283 -7
  154. package/dist/sync.js +359 -5
  155. package/dist/tasks-dispatch.js +84 -1
  156. package/dist/tasks.js +282 -1
  157. package/dist/token-budget.js +143 -1
  158. package/dist/tool-analytics.js +129 -7
  159. package/dist/tool-annotations.js +365 -1
  160. package/dist/tool-manifest-v2.json +1 -1
  161. package/dist/tool-manifest.json +1 -1
  162. package/dist/tool-profiles.js +75 -1
  163. package/dist/trace-harvest.js +244 -6
  164. package/dist/types.js +30 -1
  165. package/dist/ui-dashboard.js +50 -41
  166. package/dist/ulid.js +81 -1
  167. package/dist/usage-tracker.js +66 -1
  168. package/dist/validate.js +129 -1
  169. package/dist/vault.js +534 -1
  170. package/dist/vector-init.js +67 -1
  171. package/dist/vectors.js +184 -3
  172. package/dist/version-check.js +136 -4
  173. package/dist/visibility.js +155 -19
  174. package/dist/wyrm-cli.js +2845 -101
  175. package/dist/wyrm-cli.js.map +1 -1
  176. package/dist/wyrm-guard.d.ts.map +1 -1
  177. package/dist/wyrm-guard.js +475 -14
  178. package/dist/wyrm-guard.js.map +1 -1
  179. package/dist/wyrm-loop.js +150 -3
  180. package/dist/wyrm-manifest.json +1 -1
  181. package/dist/wyrm-statusline-daemon.js +11 -1
  182. package/dist/wyrm-statusline.js +56 -4
  183. package/dist/wyrm-ui.js +77 -9
  184. package/package.json +1 -1
@@ -1,2 +1,262 @@
1
- import{spawn as b}from"child_process";import{existsSync as h,readFileSync as j,writeFileSync as M,statSync as I,mkdirSync as _,openSync as N,closeSync as w,unlinkSync as k,renameSync as U}from"fs";import{homedir as T}from"os";import{join as m}from"path";import{replicateOnce as A}from"./event-replication.js";import{checkPeerUrl as R,isValidTokenEnvName as g}from"./repl-guard.js";const l=m(T(),".wyrm"),p=m(l,"wyrm-replication.pid"),c=m(l,"wyrm-replication.log"),E=6e4,v=5e3,x=1e6,L=0,u="repl:peers",S=50;function $(r){if(!r||typeof r.baseUrl!="string")return{ok:!1,reason:"baseUrl required"};const e=R(i(r.baseUrl));return!e.ok&&!/loopback|private|link-local/.test(e.reason)?{ok:!1,reason:e.reason}:!r.projectPath||typeof r.projectPath!="string"?{ok:!1,reason:"projectPath required"}:r.tokenEnv&&!g(r.tokenEnv)?{ok:!1,reason:`tokenEnv must be a WYRM_-prefixed env var name (got ${r.tokenEnv})`}:{ok:!0}}const i=r=>String(r).replace(/\/+$/,"");function d(r){try{const e=r.getMeta(u),n=e?JSON.parse(e):[];return Array.isArray(n)?n:[]}catch{return[]}}function B(r,e){const n=$(e);if(!n.ok)throw new Error(n.reason||"invalid peer");const s={baseUrl:i(e.baseUrl),projectPath:e.projectPath,...e.remoteProject?{remoteProject:e.remoteProject}:{},...e.tokenEnv?{tokenEnv:e.tokenEnv}:{}},t=d(r).filter(o=>!(i(o.baseUrl)===s.baseUrl&&o.projectPath===s.projectPath));return t.push(s),t.length>S&&t.splice(0,t.length-S),r.setMeta(u,JSON.stringify(t)),t}function W(r,e,n){const s=i(e),t=d(r).filter(o=>!(i(o.baseUrl)===s&&o.projectPath===n));return r.setMeta(u,JSON.stringify(t)),t}class q{db;intervalMs;constructor(e,n=E){this.db=e,this.intervalMs=n,P(),this.intervalMs=Math.max(v,n)}async tick(e=globalThis.fetch){if(!this.db.liveMemoryEnabled())return[];const n=d(this.db),s=[];for(const t of n){const o=this.db.getProject(t.projectPath);if(!o){s.push({peer:t.baseUrl,errors:["project not found: "+t.projectPath]});continue}const y=t.tokenEnv&&g(t.tokenEnv)?process.env[t.tokenEnv]:void 0;try{const a=await A(this.db,{baseUrl:i(t.baseUrl),token:y,localProjectId:o.id,remoteProject:t.remoteProject||o.name},e);s.push({peer:t.baseUrl,project:o.name,...a})}catch(a){s.push({peer:t.baseUrl,project:o.name,errors:[a instanceof Error?a.message:String(a)]})}}return s}async run(){f(`replication daemon started \xB7 interval=${this.intervalMs}ms \xB7 peers=${d(this.db).length}`);let e=!1;const n=setInterval(async()=>{if(!e){e=!0;try{const t=await this.tick();for(const o of t)(o.errors.length||(o.pulled||0)+(o.pushed||0)>0)&&f(`tick: ${JSON.stringify(o)}`)}catch(t){f(`tick error: ${t instanceof Error?t.message:String(t)}`)}finally{e=!1}}},this.intervalMs),s=t=>{clearInterval(n),f(`${t} \u2014 shutting down`);try{this.db.close?.()}catch{}process.exit(0)};process.on("SIGTERM",()=>s("SIGTERM")),process.on("SIGINT",()=>s("SIGINT"))}}class z{daemonEntrypoint;constructor(e){this.daemonEntrypoint=e,P()}start(e={}){if(this.isAlive())return{ok:!1,pid:this.readPid(),reason:"already running"};F();const n=Number(e.interval_minutes),s=Number.isFinite(n)&&n>0?Math.round(n*6e4):E,t=N(c,"a");try{const o=b(process.execPath,[this.daemonEntrypoint,String(s)],{detached:!0,stdio:["ignore",t,t],env:{...process.env}});return o.unref(),typeof o.pid!="number"?{ok:!1,reason:"spawn returned no pid"}:(M(p,String(o.pid)),{ok:!0,pid:o.pid})}finally{w(t)}}stop(){if(!this.isAlive())return this.cleanPid(),{ok:!1,reason:"not running"};const e=this.readPid();if(!e)return{ok:!1,reason:"pid file unreadable"};try{return process.kill(e,"SIGTERM"),{ok:!0}}catch(n){return{ok:!1,reason:n instanceof Error?n.message:String(n)}}}status(){const e=this.isAlive();return{running:e,pid:e?this.readPid()??void 0:void 0}}isAlive(){if(!h(p))return!1;const e=this.readPid();if(!e)return!1;try{return process.kill(e,L),!0}catch{return!1}}readPid(){try{const e=Number(j(p,"utf8").trim());return Number.isFinite(e)&&e>0?e:void 0}catch{return}}cleanPid(){try{k(p)}catch{}}}function P(){h(l)||_(l,{recursive:!0})}function f(r){process.stdout.write(`[${new Date().toISOString()}] ${r}
2
- `)}function F(){if(h(c))try{if(I(c).size>x){const r=`${c}.1`;try{k(r)}catch{}U(c,r)}}catch{}}export{E as DEFAULT_INTERVAL_MS,v as MIN_INTERVAL_MS,u as PEERS_META_KEY,q as ReplicationDaemon,z as ReplicationManager,B as addPeer,d as listPeers,W as removePeer,$ as validatePeerEntry};
1
+ /**
2
+ * Wyrm Live Memory v6.4 (Phase 3b) always-on replication daemon.
3
+ *
4
+ * Wraps the Phase-3 `replicateOnce` primitive in a background loop that syncs a
5
+ * registry of peers on an interval. Mirrors the `CloudSyncDaemon` /
6
+ * `CloudSyncManager` shape (detached child + PID file + liveness + rotating log).
7
+ *
8
+ * Secret hygiene: a peer's bearer token is NEVER stored in the DB. The registry
9
+ * holds the NAME of an env var (`tokenEnv`); the token is resolved from
10
+ * `process.env` at tick time, so the SQLite file never contains a credential.
11
+ *
12
+ * The daemon shares the MCP server's DB (`~/.wyrm/wyrm.db` by default, or
13
+ * `WYRM_DB_PATH`) — better-sqlite3 WAL supports the concurrent connections.
14
+ *
15
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
16
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
17
+ */
18
+ import { spawn } from 'child_process';
19
+ import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, openSync, closeSync, unlinkSync, renameSync, } from 'fs';
20
+ import { homedir } from 'os';
21
+ import { join } from 'path';
22
+ import { replicateOnce } from './event-replication.js';
23
+ import { checkPeerUrl, isValidTokenEnvName } from './repl-guard.js';
24
+ const WYRM_DIR = join(homedir(), '.wyrm');
25
+ const PID_FILE = join(WYRM_DIR, 'wyrm-replication.pid');
26
+ const LOG_FILE = join(WYRM_DIR, 'wyrm-replication.log');
27
+ const DEFAULT_INTERVAL_MS = 60_000; // 1 min — events are small + incremental
28
+ const MIN_INTERVAL_MS = 5_000; // floor: don't hammer a peer
29
+ const LOG_ROTATE_BYTES = 1_000_000; // 1 MB
30
+ const LIVENESS_SIGNAL = 0;
31
+ const PEERS_META_KEY = 'repl:peers';
32
+ const MAX_PEERS = 50;
33
+ /** Validate a peer entry at registration time (scheme/URL + tokenEnv name). */
34
+ export function validatePeerEntry(entry) {
35
+ if (!entry || typeof entry.baseUrl !== 'string')
36
+ return { ok: false, reason: 'baseUrl required' };
37
+ const url = checkPeerUrl(cleanBase(entry.baseUrl));
38
+ // Registration is lenient on private hosts (the egress guard re-checks at fetch
39
+ // time); only reject an unparseable URL or a bad scheme here.
40
+ if (!url.ok && !/loopback|private|link-local/.test(url.reason))
41
+ return { ok: false, reason: url.reason };
42
+ if (!entry.projectPath || typeof entry.projectPath !== 'string')
43
+ return { ok: false, reason: 'projectPath required' };
44
+ if (entry.tokenEnv && !isValidTokenEnvName(entry.tokenEnv)) {
45
+ return { ok: false, reason: `tokenEnv must be a WYRM_-prefixed env var name (got ${entry.tokenEnv})` };
46
+ }
47
+ return { ok: true };
48
+ }
49
+ // ==================== REGISTRY ====================
50
+ const cleanBase = (u) => String(u).replace(/\/+$/, '');
51
+ /** Read the persisted peer registry (tolerant of corruption — returns []). */
52
+ export function listPeers(db) {
53
+ try {
54
+ const raw = db.getMeta(PEERS_META_KEY);
55
+ const parsed = raw ? JSON.parse(raw) : [];
56
+ return Array.isArray(parsed) ? parsed : [];
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ }
62
+ /** Upsert a peer (keyed on baseUrl + projectPath). Throws on an invalid entry. Returns the new registry. */
63
+ export function addPeer(db, entry) {
64
+ const v = validatePeerEntry(entry);
65
+ if (!v.ok)
66
+ throw new Error(v.reason || 'invalid peer');
67
+ const e = {
68
+ baseUrl: cleanBase(entry.baseUrl),
69
+ projectPath: entry.projectPath,
70
+ ...(entry.remoteProject ? { remoteProject: entry.remoteProject } : {}),
71
+ ...(entry.tokenEnv ? { tokenEnv: entry.tokenEnv } : {}),
72
+ };
73
+ const peers = listPeers(db).filter((p) => !(cleanBase(p.baseUrl) === e.baseUrl && p.projectPath === e.projectPath));
74
+ peers.push(e);
75
+ if (peers.length > MAX_PEERS)
76
+ peers.splice(0, peers.length - MAX_PEERS);
77
+ db.setMeta(PEERS_META_KEY, JSON.stringify(peers));
78
+ return peers;
79
+ }
80
+ /** Remove a peer by baseUrl + projectPath. Returns the new registry. */
81
+ export function removePeer(db, baseUrl, projectPath) {
82
+ const target = cleanBase(baseUrl);
83
+ const peers = listPeers(db).filter((p) => !(cleanBase(p.baseUrl) === target && p.projectPath === projectPath));
84
+ db.setMeta(PEERS_META_KEY, JSON.stringify(peers));
85
+ return peers;
86
+ }
87
+ // ==================== DAEMON ====================
88
+ export class ReplicationDaemon {
89
+ db;
90
+ intervalMs;
91
+ constructor(db, intervalMs = DEFAULT_INTERVAL_MS) {
92
+ this.db = db;
93
+ this.intervalMs = intervalMs;
94
+ ensureWyrmDir();
95
+ this.intervalMs = Math.max(MIN_INTERVAL_MS, intervalMs);
96
+ }
97
+ /** Run one replication round over all registered peers. Never throws. */
98
+ async tick(fetchImpl = globalThis.fetch) {
99
+ if (!this.db.liveMemoryEnabled())
100
+ return [];
101
+ const peers = listPeers(this.db);
102
+ const results = [];
103
+ for (const p of peers) {
104
+ const project = this.db.getProject(p.projectPath);
105
+ if (!project) {
106
+ results.push({ peer: p.baseUrl, errors: ['project not found: ' + p.projectPath] });
107
+ continue;
108
+ }
109
+ // Resolve the token only from a validly-named env var (never an arbitrary process secret).
110
+ const token = p.tokenEnv && isValidTokenEnvName(p.tokenEnv) ? process.env[p.tokenEnv] : undefined;
111
+ try {
112
+ const r = await replicateOnce(this.db, { baseUrl: cleanBase(p.baseUrl), token, localProjectId: project.id, remoteProject: p.remoteProject || project.name }, fetchImpl);
113
+ results.push({ peer: p.baseUrl, project: project.name, ...r });
114
+ }
115
+ catch (err) {
116
+ results.push({ peer: p.baseUrl, project: project.name, errors: [err instanceof Error ? err.message : String(err)] });
117
+ }
118
+ }
119
+ return results;
120
+ }
121
+ /** Long-running loop. Spawned detached; logs to stdout (redirected to LOG_FILE). */
122
+ async run() {
123
+ log(`replication daemon started · interval=${this.intervalMs}ms · peers=${listPeers(this.db).length}`);
124
+ let inFlight = false;
125
+ const timer = setInterval(async () => {
126
+ if (inFlight)
127
+ return; // never overlap ticks
128
+ inFlight = true;
129
+ try {
130
+ const results = await this.tick();
131
+ for (const r of results) {
132
+ if (r.errors.length || (r.pulled || 0) + (r.pushed || 0) > 0) {
133
+ log(`tick: ${JSON.stringify(r)}`);
134
+ }
135
+ }
136
+ }
137
+ catch (e) {
138
+ log(`tick error: ${e instanceof Error ? e.message : String(e)}`);
139
+ }
140
+ finally {
141
+ inFlight = false;
142
+ }
143
+ }, this.intervalMs);
144
+ const shutdown = (signal) => {
145
+ clearInterval(timer);
146
+ log(`${signal} — shutting down`);
147
+ try {
148
+ this.db.close?.();
149
+ }
150
+ catch { /* best-effort WAL checkpoint + close */ }
151
+ process.exit(0);
152
+ };
153
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
154
+ process.on('SIGINT', () => shutdown('SIGINT'));
155
+ }
156
+ }
157
+ // ==================== MANAGER ====================
158
+ export class ReplicationManager {
159
+ daemonEntrypoint;
160
+ constructor(daemonEntrypoint) {
161
+ this.daemonEntrypoint = daemonEntrypoint;
162
+ ensureWyrmDir();
163
+ }
164
+ start(opts = {}) {
165
+ if (this.isAlive())
166
+ return { ok: false, pid: this.readPid(), reason: 'already running' };
167
+ rotateLogIfBig();
168
+ const mins = Number(opts.interval_minutes);
169
+ const intervalMs = Number.isFinite(mins) && mins > 0 ? Math.round(mins * 60_000) : DEFAULT_INTERVAL_MS;
170
+ // One shared fd for both stdout+stderr; try/finally so a spawn throw can't leak it.
171
+ const logFd = openSync(LOG_FILE, 'a');
172
+ try {
173
+ const child = spawn(process.execPath, [this.daemonEntrypoint, String(intervalMs)], {
174
+ detached: true,
175
+ stdio: ['ignore', logFd, logFd],
176
+ env: { ...process.env },
177
+ });
178
+ child.unref();
179
+ if (typeof child.pid !== 'number')
180
+ return { ok: false, reason: 'spawn returned no pid' };
181
+ writeFileSync(PID_FILE, String(child.pid));
182
+ return { ok: true, pid: child.pid };
183
+ }
184
+ finally {
185
+ closeSync(logFd);
186
+ }
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
+ try {
197
+ process.kill(pid, 'SIGTERM');
198
+ return { ok: true };
199
+ }
200
+ catch (e) {
201
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
202
+ }
203
+ }
204
+ status() {
205
+ const running = this.isAlive();
206
+ return { running, pid: running ? this.readPid() ?? undefined : undefined };
207
+ }
208
+ isAlive() {
209
+ if (!existsSync(PID_FILE))
210
+ return false;
211
+ const pid = this.readPid();
212
+ if (!pid)
213
+ return false;
214
+ try {
215
+ process.kill(pid, LIVENESS_SIGNAL);
216
+ return true;
217
+ }
218
+ catch {
219
+ return false;
220
+ }
221
+ }
222
+ readPid() {
223
+ try {
224
+ const n = Number(readFileSync(PID_FILE, 'utf8').trim());
225
+ return Number.isFinite(n) && n > 0 ? n : undefined;
226
+ }
227
+ catch {
228
+ return undefined;
229
+ }
230
+ }
231
+ cleanPid() {
232
+ try {
233
+ unlinkSync(PID_FILE);
234
+ }
235
+ catch { /* idempotent */ }
236
+ }
237
+ }
238
+ // ==================== HELPERS ====================
239
+ function ensureWyrmDir() {
240
+ if (!existsSync(WYRM_DIR))
241
+ mkdirSync(WYRM_DIR, { recursive: true });
242
+ }
243
+ function log(msg) {
244
+ process.stdout.write(`[${new Date().toISOString()}] ${msg}\n`);
245
+ }
246
+ function rotateLogIfBig() {
247
+ if (!existsSync(LOG_FILE))
248
+ return;
249
+ try {
250
+ if (statSync(LOG_FILE).size > LOG_ROTATE_BYTES) {
251
+ const rotated = `${LOG_FILE}.1`;
252
+ try {
253
+ unlinkSync(rotated);
254
+ }
255
+ catch { /* ignore */ }
256
+ renameSync(LOG_FILE, rotated);
257
+ }
258
+ }
259
+ catch { /* non-fatal */ }
260
+ }
261
+ export { DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS, PEERS_META_KEY };
262
+ //# sourceMappingURL=replication-daemon.js.map
package/dist/rerank.js CHANGED
@@ -1 +1,142 @@
1
- const E=5,h=500,M="http://localhost:8088";function T(){return!!(process.env.WYRM_RERANK_MODEL&&process.env.WYRM_RERANK_MODEL.trim())}function m(){const t=Number(process.env.WYRM_RERANK_TIMEOUT);return Number.isFinite(t)&&t>0?t:500}function p(){return((process.env.WYRM_RERANK_URL||"").trim()||M).replace(/\/+$/,"")}async function A(t,o,l,s,f){try{const a=await fetch(`${o}/v1/rerank`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:t,query:l,documents:s.map(n=>n.text),top_n:s.length}),signal:AbortSignal.timeout(f)});if(!a.ok)return null;const r=(await a.json())?.results;if(!Array.isArray(r)||r.length===0)return null;const i=new Map;for(const n of r){if(typeof n.index!="number"||n.index<0||n.index>=s.length)continue;const e=typeof n.relevance_score=="number"?n.relevance_score:typeof n.score=="number"?n.score:null;e==null||!Number.isFinite(e)||i.set(s[n.index].id,e)}return i.size>0?i:null}catch{return null}}async function d(t,o,l={}){const s=process.env.WYRM_RERANK_MODEL?.trim();if(!s||!t||o.length===0)return null;const f=p(),a=l.timeoutMs??m(),R=Math.max(1,Math.min(5,l.batchSize??5)),r=new Map;for(let n=0;n<o.length;n+=R){const e=o.slice(n,n+R),c=await A(s,f,t,e,a);if(c)for(const[u,_]of c)r.set(u,_)}if(r.size===0)return null;const i=new Map(o.map((n,e)=>[n.id,e]));return[...o].sort((n,e)=>{const c=r.get(n.id),u=r.get(e.id);return c!=null&&u!=null?u-c:c!=null?-1:u!=null?1:(i.get(n.id)??0)-(i.get(e.id)??0)})}export{d as rerankCandidates,T as rerankConfigured};
1
+ /**
2
+ * Cross-encoder rerank stage for hybrid recall — Ghost Protocol local
3
+ * multi-model ROUTER, subsystem wyrm-routing-rerank.
4
+ *
5
+ * The optional final leg of `recallHybrid`: FTS ⊕ vector fusion produces a
6
+ * top-K candidate set, this re-scores each candidate AGAINST the query with a
7
+ * true cross-encoder (query+document attend jointly, unlike the bi-encoder
8
+ * cosine the fusion uses), and the caller keeps the top-N. Cross-encoders are
9
+ * the strongest reranking signal in IR; the cost is a forward pass per
10
+ * candidate, which is why this is depth-bounded (top-K, not the whole corpus)
11
+ * and OFF by default.
12
+ *
13
+ * MODEL: bge-reranker-v2-m3 served by llama.cpp's OpenAI-style `/v1/rerank`
14
+ * endpoint (Jina/Cohere reranker wire shape:
15
+ * POST { model, query, documents: string[], top_n } ->
16
+ * { results: [{ index, relevance_score }, ...] }).
17
+ * Configured ENTIRELY by environment so Wyrm carries NO new dependency and the
18
+ * reranker is opt-in:
19
+ * WYRM_RERANK_MODEL — model id passed to /v1/rerank. UNSET ⇒ rerank is a
20
+ * no-op (the whole feature is gated on this).
21
+ * WYRM_RERANK_URL — base URL of the llama.cpp server
22
+ * (default http://localhost:8088).
23
+ * WYRM_RERANK_TIMEOUT — per-request timeout in ms (default 500).
24
+ *
25
+ * RESILIENCE CONTRACT (the recallHybrid `rerank:true` promise): this NEVER
26
+ * throws and NEVER hangs. On ANY error — model unset, endpoint down, timeout,
27
+ * malformed body, partial coverage — it returns null and the caller falls back
28
+ * to the fusion order. A reranker that is slow or absent must degrade recall to
29
+ * "exactly the fusion result", never to an error or a stall.
30
+ *
31
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
32
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
33
+ */
34
+ /** Max documents sent in one /v1/rerank request (the task's batch<=5 cap). A
35
+ * larger candidate set is split into sequential batches of this size. */
36
+ const RERANK_BATCH = 5;
37
+ /** Default per-request timeout (ms) when WYRM_RERANK_TIMEOUT is unset/invalid. */
38
+ const DEFAULT_TIMEOUT_MS = 500;
39
+ /** Default llama.cpp server base URL when WYRM_RERANK_URL is unset. */
40
+ const DEFAULT_RERANK_URL = 'http://localhost:8088';
41
+ /** True when a rerank model is configured (the feature is opt-in via env). */
42
+ export function rerankConfigured() {
43
+ return !!(process.env.WYRM_RERANK_MODEL && process.env.WYRM_RERANK_MODEL.trim());
44
+ }
45
+ function rerankTimeoutMs() {
46
+ const raw = Number(process.env.WYRM_RERANK_TIMEOUT);
47
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS;
48
+ }
49
+ function rerankBaseUrl() {
50
+ const raw = (process.env.WYRM_RERANK_URL || '').trim();
51
+ return (raw || DEFAULT_RERANK_URL).replace(/\/+$/, '');
52
+ }
53
+ /** One /v1/rerank call over a <=RERANK_BATCH slice. Returns an id→score map,
54
+ * or null on any failure (timeout, non-200, malformed body). Never throws. */
55
+ async function rerankBatch(model, baseUrl, query, batch, timeoutMs) {
56
+ try {
57
+ const res = await fetch(`${baseUrl}/v1/rerank`, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({
61
+ model,
62
+ query,
63
+ documents: batch.map((c) => c.text),
64
+ top_n: batch.length,
65
+ }),
66
+ signal: AbortSignal.timeout(timeoutMs),
67
+ });
68
+ if (!res.ok)
69
+ return null;
70
+ const data = (await res.json());
71
+ const results = data?.results;
72
+ if (!Array.isArray(results) || results.length === 0)
73
+ return null;
74
+ const out = new Map();
75
+ for (const r of results) {
76
+ // The reranker returns `index` into the documents array we sent; map it
77
+ // back to the candidate id. Tolerate `score` as an alias for some builds.
78
+ if (typeof r.index !== 'number' || r.index < 0 || r.index >= batch.length)
79
+ continue;
80
+ const score = typeof r.relevance_score === 'number' ? r.relevance_score
81
+ : typeof r.score === 'number' ? r.score : null;
82
+ if (score == null || !Number.isFinite(score))
83
+ continue;
84
+ out.set(batch[r.index].id, score);
85
+ }
86
+ return out.size > 0 ? out : null;
87
+ }
88
+ catch {
89
+ // timeout / connection refused / abort / JSON error — all degrade to null.
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Rerank `candidates` against `query` with the configured cross-encoder and
95
+ * return them ordered best-first (highest relevance_score). Sends <=batchSize
96
+ * documents per /v1/rerank call (sequential batches). Returns null — the
97
+ * signal for "fall back to fusion order" — when:
98
+ * - no model is configured (WYRM_RERANK_MODEL unset),
99
+ * - there are no candidates,
100
+ * - the endpoint is unreachable/slow/erroring for EVERY batch,
101
+ * - or no candidate received a usable score.
102
+ * A PARTIAL success (some batches scored, some not) still returns an order:
103
+ * scored candidates rank by their cross-encoder score, unscored ones keep
104
+ * their original (fusion) order and trail the scored set — strictly no worse
105
+ * than the fusion order, and never a thrown error.
106
+ */
107
+ export async function rerankCandidates(query, candidates, opts = {}) {
108
+ const model = process.env.WYRM_RERANK_MODEL?.trim();
109
+ if (!model)
110
+ return null; // feature gated on the model env
111
+ if (!query || candidates.length === 0)
112
+ return null;
113
+ const baseUrl = rerankBaseUrl();
114
+ const timeoutMs = opts.timeoutMs ?? rerankTimeoutMs();
115
+ const batchSize = Math.max(1, Math.min(RERANK_BATCH, opts.batchSize ?? RERANK_BATCH));
116
+ const scores = new Map();
117
+ for (let i = 0; i < candidates.length; i += batchSize) {
118
+ const batch = candidates.slice(i, i + batchSize);
119
+ const batchScores = await rerankBatch(model, baseUrl, query, batch, timeoutMs);
120
+ if (batchScores)
121
+ for (const [id, s] of batchScores)
122
+ scores.set(id, s);
123
+ }
124
+ if (scores.size === 0)
125
+ return null; // nothing scored → fusion order wins
126
+ // Scored candidates first (by cross-encoder score desc); any unscored ones
127
+ // (a failed batch) keep their original relative order and trail — so the
128
+ // result is never worse than the incoming fusion order.
129
+ const origIndex = new Map(candidates.map((c, i) => [c.id, i]));
130
+ return [...candidates].sort((a, b) => {
131
+ const sa = scores.get(a.id);
132
+ const sb = scores.get(b.id);
133
+ if (sa != null && sb != null)
134
+ return sb - sa;
135
+ if (sa != null)
136
+ return -1; // scored before unscored
137
+ if (sb != null)
138
+ return 1;
139
+ return (origIndex.get(a.id) ?? 0) - (origIndex.get(b.id) ?? 0); // stable
140
+ });
141
+ }
142
+ //# sourceMappingURL=rerank.js.map