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,322 +1,18 @@
1
- /**
2
- * Wyrm statusline daemon + query helpers (6.0 phase 1).
3
- *
4
- * Architecture (per spec 018, decision #1):
5
- *
6
- * wyrm-statusline (binary)
7
- * ↓ Unix socket
8
- * wyrm-statusline-daemon (separate process)
9
- * ↓ in-process
10
- * WyrmDB (business-logic layer — same as MCP server uses)
11
- * ↓
12
- * SQLite
13
- *
14
- * The daemon is auto-spawned by the binary on first call and idle-times-out
15
- * after IDLE_TIMEOUT_MS. Correctness over speed — direct SQLite reads from
16
- * the statusline binary would skip visibility filters, encryption-at-rest
17
- * (future), and audit trail. The MCP server itself doesn't host this; the
18
- * statusline runs in its own daemon for the same reason a unix command-line
19
- * tool runs separately from a server: low resource footprint, no MCP
20
- * client required.
21
- *
22
- * Status flags (per spec 018, decision #3):
23
- * WYRM_STATUSLINE_SHOW_SAVINGS=1 — include "saved" counter (experimental)
24
- * WYRM_STATUSLINE_PRIVATE=1 — collapse to "<brand> ●●●" for screen-share safety
25
- *
26
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
27
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
28
- */
29
- import { existsSync, mkdirSync, unlinkSync, writeFileSync, realpathSync } from 'fs';
30
- import { join as pathJoin } from 'path';
31
- import { homedir } from 'os';
32
- import { createServer, createConnection } from 'net';
33
- import { spawn } from 'child_process';
34
- import { WyrmDB } from './database.js';
35
- import { ICON } from './icons.js';
36
- const SOCKET_PATH = pathJoin(homedir(), '.wyrm', 'statusline.sock');
37
- const PID_PATH = pathJoin(homedir(), '.wyrm', 'statusline.pid');
38
- const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
39
- // The brand mark rendered in SILVER (the Ghost Protocol dragon colour). Truecolor
40
- // #C0C0C0; degrades gracefully on 256-colour terminals. Empty WYRM_BRAND → no mark.
41
- const SILVER = '\x1b[38;2;192;192;192m';
42
- const ANSI_RESET = '\x1b[0m';
43
- const brandMark = ICON.brand ? `${SILVER}${ICON.brand}${ANSI_RESET}` : '';
44
- /**
45
- * Look up the active project for a given cwd.
46
- * Walks up directory tree matching against `projects.path`.
47
- */
48
- function resolveProject(db, cwd) {
49
- if (!cwd)
50
- return null;
51
- let current = cwd;
52
- for (let i = 0; i < 12; i++) {
53
- const row = db.prepare('SELECT id, name FROM projects WHERE path = ?').get(current);
54
- if (row)
55
- return row;
56
- const parent = pathJoin(current, '..');
57
- if (parent === current)
58
- break;
59
- current = parent;
60
- }
61
- return null;
62
- }
63
- function num(db, sql, ...params) {
64
- try {
65
- const row = db.prepare(sql).get(...params);
66
- return row?.n ?? 0;
67
- }
68
- catch {
69
- return 0;
70
- }
71
- }
72
- /**
73
- * Compute the statusline payload from the live DB.
74
- */
75
- export function computeStatusline(db, req) {
76
- // Privacy mode short-circuit — never read project state.
77
- // Per-call flag takes precedence; falls back to env for daemon-default.
78
- const privateMode = req.privateMode ?? (process.env.WYRM_STATUSLINE_PRIVATE === '1');
79
- if (privateMode) {
80
- return {
81
- text: `${brandMark} ●●●`,
82
- raw: { project: null, activeQuests: 0, contextTokensUsed: 0, contextBudget: 0, blockedThisSession: 0, truthCount: 0, tokensSaved: null },
83
- };
84
- }
85
- const project = resolveProject(db, req.cwd);
86
- const projectId = project?.id;
87
- const showSaved = req.showSavings ?? (process.env.WYRM_STATUSLINE_SHOW_SAVINGS === '1');
88
- const activeQuests = projectId
89
- ? num(db, `SELECT COUNT(*) AS n FROM quests WHERE project_id = ? AND status = 'pending'`, projectId)
90
- : 0;
91
- const truthCount = projectId
92
- ? num(db, `SELECT COUNT(*) AS n FROM ground_truths WHERE project_id = ?`, projectId)
93
- : 0;
94
- // Blocked-this-session is approximated as "failure_check hits since the
95
- // current session's start time." If no session id is provided, falls back
96
- // to "last 24h" so the statusline isn't blank on first call.
97
- // v7 F3 (T019) fix, found while writing the renderer fixture tests: both
98
- // queries referenced columns that have never existed (failure_patterns has
99
- // `last_seen`, sessions mark their start in `created_at` — there is no
100
- // last_seen_at/started_at), so num()'s catch silently pinned the blocked
101
- // counter to 0 and the ✖ segment could never render. Locked by
102
- // tests/render-fixtures.test.ts.
103
- let blockedThisSession = 0;
104
- if (req.sessionId) {
105
- blockedThisSession = num(db, `SELECT COALESCE(SUM(occurrences), 0) AS n FROM failure_patterns
106
- WHERE last_seen >= (SELECT created_at FROM sessions WHERE id = ?)`, req.sessionId);
107
- }
108
- else {
109
- blockedThisSession = num(db, `SELECT COALESCE(SUM(occurrences), 0) AS n FROM failure_patterns WHERE last_seen >= datetime('now', '-1 day')`);
110
- }
111
- // Context-token usage — best-effort from the latest wyrm_context_build call.
112
- // Falls back to 0 if the log isn't populated yet.
113
- let contextTokensUsed = 0;
114
- const contextBudget = 200000; // 200K default; refined by spec 014 budget resolver
115
- try {
116
- const row = db.prepare(`
1
+ import{existsSync as C,mkdirSync as I,unlinkSync as x,writeFileSync as k,realpathSync as M}from"fs";import{join as S}from"path";import{homedir as h}from"os";import{createServer as $,createConnection as A}from"net";import{spawn as N}from"child_process";import{WyrmDB as L}from"./database.js";import{ICON as d}from"./icons.js";const y=S(h(),".wyrm","statusline.sock"),v=S(h(),".wyrm","statusline.pid"),D=300*1e3,W="\x1B[38;2;192;192;192m",U="\x1B[0m",m=d.brand?`${W}${d.brand}${U}`:"";function j(e,n){if(!n)return null;let o=n;for(let t=0;t<12;t++){const r=e.prepare("SELECT id, name FROM projects WHERE path = ?").get(o);if(r)return r;const s=S(o,"..");if(s===o)break;o=s}return null}function f(e,n,...o){try{return e.prepare(n).get(...o)?.n??0}catch{return 0}}function H(e,n){if(n.privateMode??process.env.WYRM_STATUSLINE_PRIVATE==="1")return{text:`${m} \u25CF\u25CF\u25CF`,raw:{project:null,activeQuests:0,contextTokensUsed:0,contextBudget:0,blockedThisSession:0,truthCount:0,tokensSaved:null}};const t=j(e,n.cwd),r=t?.id,s=n.showSavings??process.env.WYRM_STATUSLINE_SHOW_SAVINGS==="1",i=r?f(e,"SELECT COUNT(*) AS n FROM quests WHERE project_id = ? AND status = 'pending'",r):0,a=r?f(e,"SELECT COUNT(*) AS n FROM ground_truths WHERE project_id = ?",r):0;let c=0;n.sessionId?c=f(e,`SELECT COALESCE(SUM(occurrences), 0) AS n FROM failure_patterns
2
+ WHERE last_seen >= (SELECT created_at FROM sessions WHERE id = ?)`,n.sessionId):c=f(e,"SELECT COALESCE(SUM(occurrences), 0) AS n FROM failure_patterns WHERE last_seen >= datetime('now', '-1 day')");let l=0;const w=2e5;try{l=e.prepare(`
117
3
  SELECT estimated_tokens AS n FROM token_savings_log
118
4
  WHERE category = 'cached_preamble' AND session_id = ?
119
5
  ORDER BY id DESC LIMIT 1
120
- `).get(req.sessionId ?? -1);
121
- contextTokensUsed = row?.n ?? 0;
122
- }
123
- catch { /* table may not exist on older DBs */ }
124
- // Tokens-saved counter (opt-in display, default-on tracking per spec).
125
- let tokensSaved = null;
126
- if (showSaved) {
127
- try {
128
- const row = db.prepare(`
6
+ `).get(n.sessionId??-1)?.n??0}catch{}let p=null;if(s)try{p=e.prepare(`
129
7
  SELECT COALESCE(SUM(estimated_tokens), 0) AS n FROM token_savings_log
130
8
  WHERE session_id = ?
131
- `).get(req.sessionId ?? -1);
132
- tokensSaved = row.n;
133
- }
134
- catch {
135
- tokensSaved = 0;
136
- }
137
- }
138
- const parts = [`${brandMark} Wyrm`];
139
- if (project)
140
- parts.push(project.name);
141
- parts.push(`${activeQuests} ${ICON.questOpen}`);
142
- if (contextTokensUsed > 0)
143
- parts.push(`${formatTokens(contextTokensUsed)}/${formatTokens(contextBudget)}`);
144
- if (tokensSaved !== null && tokensSaved > 0)
145
- parts.push(`~${formatTokens(tokensSaved)} saved`);
146
- if (blockedThisSession > 0)
147
- parts.push(`${ICON.blocked} ${blockedThisSession}`);
148
- if (truthCount > 0)
149
- parts.push(`${ICON.truth} ${truthCount}`);
150
- return {
151
- text: parts.join(' · '),
152
- raw: { project: project?.name ?? null, activeQuests, contextTokensUsed, contextBudget, blockedThisSession, truthCount, tokensSaved },
153
- };
154
- }
155
- function formatTokens(n) {
156
- if (n >= 1000)
157
- return `${Math.round(n / 100) / 10}k`;
158
- return String(n);
159
- }
160
- /**
161
- * Log a token-savings estimate for the current session.
162
- * Safe no-op if migration 14 hasn't run yet (older DB).
163
- */
164
- export function logSavings(db, toolName, category, estimatedTokens, sessionId, confidence = 'conservative') {
165
- try {
166
- db.prepare(`
9
+ `).get(n.sessionId??-1).n}catch{p=0}const u=[`${m} Wyrm`];return t&&u.push(t.name),u.push(`${i} ${d.questOpen}`),l>0&&u.push(`${g(l)}/${g(w)}`),p!==null&&p>0&&u.push(`~${g(p)} saved`),c>0&&u.push(`${d.blocked} ${c}`),a>0&&u.push(`${d.truth} ${a}`),{text:u.join(" \xB7 "),raw:{project:t?.name??null,activeQuests:i,contextTokensUsed:l,contextBudget:w,blockedThisSession:c,truthCount:a,tokensSaved:p}}}function g(e){return e>=1e3?`${Math.round(e/100)/10}k`:String(e)}function Y(e,n,o,t,r,s="conservative"){try{e.prepare(`
167
10
  INSERT INTO token_savings_log (session_id, tool_name, category, estimated_tokens, confidence)
168
11
  VALUES (?, ?, ?, ?, ?)
169
- `).run(sessionId ?? null, toolName, category, estimatedTokens, confidence);
170
- }
171
- catch { /* migration 14 may not have run; best-effort */ }
172
- }
173
- // ---------------------------------------------------------------------------
174
- // Daemon — runs as a separate process, talks via Unix socket.
175
- // ---------------------------------------------------------------------------
176
- let idleTimer = null;
177
- let server = null;
178
- function resetIdleTimer() {
179
- if (idleTimer)
180
- clearTimeout(idleTimer);
181
- idleTimer = setTimeout(() => {
182
- try {
183
- server?.close();
184
- }
185
- catch { /* swallow */ }
186
- cleanup();
187
- process.exit(0);
188
- }, IDLE_TIMEOUT_MS);
189
- }
190
- function cleanup() {
191
- try {
192
- unlinkSync(SOCKET_PATH);
193
- }
194
- catch { /* fine if missing */ }
195
- try {
196
- unlinkSync(PID_PATH);
197
- }
198
- catch { /* fine */ }
199
- }
200
- /**
201
- * Run the statusline daemon. Listens on SOCKET_PATH, dispatches every
202
- * incoming JSON request through computeStatusline(). Exits after
203
- * IDLE_TIMEOUT_MS of no requests.
204
- */
205
- export function runDaemon() {
206
- const wyrmDir = pathJoin(homedir(), '.wyrm');
207
- if (!existsSync(wyrmDir))
208
- mkdirSync(wyrmDir, { recursive: true });
209
- cleanup(); // clear stale socket if a prior daemon crashed
210
- const wyrmDB = new WyrmDB();
211
- const db = wyrmDB.getDatabase();
212
- server = createServer((sock) => {
213
- let buf = '';
214
- sock.on('data', (chunk) => {
215
- buf += chunk.toString();
216
- const newlineIdx = buf.indexOf('\n');
217
- if (newlineIdx === -1)
218
- return;
219
- const line = buf.slice(0, newlineIdx);
220
- buf = buf.slice(newlineIdx + 1);
221
- try {
222
- const req = JSON.parse(line);
223
- const resp = computeStatusline(db, req);
224
- sock.write(`${JSON.stringify(resp)}\n`);
225
- }
226
- catch (e) {
227
- sock.write(`${JSON.stringify({ text: `${brandMark} Wyrm`, error: String(e) })}\n`);
228
- }
229
- sock.end();
230
- resetIdleTimer();
231
- });
232
- sock.on('error', () => { });
233
- });
234
- server.on('error', (e) => {
235
- // EADDRINUSE means another daemon already owns the socket. Do NOT
236
- // cleanup() — that would delete the working daemon's socket + PID
237
- // files, killing the live instance. Just exit silently and let the
238
- // existing daemon serve.
239
- if (e.code === 'EADDRINUSE') {
240
- process.exit(0);
241
- }
242
- console.error('wyrm-statusline-daemon: socket error', e);
243
- cleanup();
244
- process.exit(1);
245
- });
246
- server.listen(SOCKET_PATH, () => {
247
- try {
248
- writeFileSync(PID_PATH, String(process.pid));
249
- }
250
- catch { /* tolerant */ }
251
- resetIdleTimer();
252
- });
253
- // Daemon exits cleanly on SIGTERM / SIGINT.
254
- process.on('SIGTERM', () => { cleanup(); process.exit(0); });
255
- process.on('SIGINT', () => { cleanup(); process.exit(0); });
256
- }
257
- /**
258
- * Client-side: query a running daemon, spawn one if missing.
259
- * Used by the wyrm-statusline binary.
260
- */
261
- export async function queryDaemon(req) {
262
- return new Promise((resolve) => {
263
- const tryConnect = (attempt) => {
264
- const sock = createConnection({ path: SOCKET_PATH }, () => {
265
- let buf = '';
266
- sock.on('data', (chunk) => {
267
- buf += chunk.toString();
268
- if (buf.includes('\n')) {
269
- const line = buf.split('\n')[0];
270
- try {
271
- const resp = JSON.parse(line);
272
- resolve(resp);
273
- }
274
- catch {
275
- resolve({ text: `${brandMark} Wyrm`, raw: { project: null, activeQuests: 0, contextTokensUsed: 0, contextBudget: 200000, blockedThisSession: 0, truthCount: 0, tokensSaved: null } });
276
- }
277
- sock.end();
278
- }
279
- });
280
- sock.write(`${JSON.stringify(req)}\n`);
281
- });
282
- sock.on('error', () => {
283
- if (attempt === 0) {
284
- // No daemon running — spawn one detached, then retry.
285
- spawnDaemon();
286
- setTimeout(() => tryConnect(1), 300);
287
- }
288
- else {
289
- // Daemon didn't come up — return a degraded line rather than fail.
290
- resolve({
291
- text: `${brandMark} Wyrm`,
292
- raw: { project: null, activeQuests: 0, contextTokensUsed: 0, contextBudget: 200000, blockedThisSession: 0, truthCount: 0, tokensSaved: null },
293
- });
294
- }
295
- });
296
- };
297
- tryConnect(0);
298
- });
299
- }
300
- function spawnDaemon(daemonPath) {
301
- // The daemon ships as a sibling of the wyrm-statusline binary inside the
302
- // package's dist/ directory. When the binary is installed globally, npm
303
- // creates a symlink in <prefix>/bin → <prefix>/lib/node_modules/.../dist/.
304
- // process.argv[1] usually resolves to the symlink path, so we follow the
305
- // symlink with realpath to land in dist/ where the daemon actually lives.
306
- let target = daemonPath;
307
- if (!target) {
308
- let argv1 = process.argv[1] || '';
309
- try {
310
- if (argv1)
311
- argv1 = realpathSync(argv1);
312
- }
313
- catch { /* fine — fall through with raw argv1 */ }
314
- target = pathJoin(pathJoin(argv1, '..'), 'wyrm-statusline-daemon.js');
315
- }
316
- const proc = spawn(process.execPath, [target], {
317
- detached: true,
318
- stdio: 'ignore',
319
- });
320
- proc.unref();
321
- }
322
- //# sourceMappingURL=statusline.js.map
12
+ `).run(r??null,n,o,t,s)}catch{}}let _=null,T=null;function O(){_&&clearTimeout(_),_=setTimeout(()=>{try{T?.close()}catch{}E(),process.exit(0)},D)}function E(){try{x(y)}catch{}try{x(v)}catch{}}function K(){const e=S(h(),".wyrm");C(e)||I(e,{recursive:!0}),E();const o=new L().getDatabase();T=$(t=>{let r="";t.on("data",s=>{r+=s.toString();const i=r.indexOf(`
13
+ `);if(i===-1)return;const a=r.slice(0,i);r=r.slice(i+1);try{const c=JSON.parse(a),l=H(o,c);t.write(`${JSON.stringify(l)}
14
+ `)}catch(c){t.write(`${JSON.stringify({text:`${m} Wyrm`,error:String(c)})}
15
+ `)}t.end(),O()}),t.on("error",()=>{})}),T.on("error",t=>{t.code==="EADDRINUSE"&&process.exit(0),console.error("wyrm-statusline-daemon: socket error",t),E(),process.exit(1)}),T.listen(y,()=>{try{k(v,String(process.pid))}catch{}O()}),process.on("SIGTERM",()=>{E(),process.exit(0)}),process.on("SIGINT",()=>{E(),process.exit(0)})}async function z(e){return new Promise(n=>{const o=t=>{const r=A({path:y},()=>{let s="";r.on("data",i=>{if(s+=i.toString(),s.includes(`
16
+ `)){const a=s.split(`
17
+ `)[0];try{const c=JSON.parse(a);n(c)}catch{n({text:`${m} Wyrm`,raw:{project:null,activeQuests:0,contextTokensUsed:0,contextBudget:2e5,blockedThisSession:0,truthCount:0,tokensSaved:null}})}r.end()}}),r.write(`${JSON.stringify(e)}
18
+ `)});r.on("error",()=>{t===0?(b(),setTimeout(()=>o(1),300)):n({text:`${m} Wyrm`,raw:{project:null,activeQuests:0,contextTokensUsed:0,contextBudget:2e5,blockedThisSession:0,truthCount:0,tokensSaved:null}})})};o(0)})}function b(e){let n=e;if(!n){let t=process.argv[1]||"";try{t&&(t=M(t))}catch{}n=S(S(t,".."),"wyrm-statusline-daemon.js")}N(process.execPath,[n],{detached:!0,stdio:"ignore"}).unref()}export{H as computeStatusline,Y as logSavings,z as queryDaemon,K as runDaemon};
package/dist/sub-agent.js CHANGED
@@ -1,292 +1,43 @@
1
- /**
2
- * Sub-agent embedding — `wyrm_ask` (Tier 3.10).
3
- *
4
- * Wyrm stops being just an MCP server and becomes the agent that knows
5
- * everything about itself. `wyrm_ask(query)` assembles relevant context
6
- * from FTS + truths + sessions + symbols + failures, then runs the query
7
- * through a chat LLM (Ollama auto-detected on localhost:11434, OpenAI
8
- * fallback if `OPENAI_API_KEY` is set), and returns a grounded answer.
9
- *
10
- * If no LLM is reachable (no Ollama, no OpenAI key), Wyrm still returns
11
- * the assembled context — the user gets the same data, the LLM step is
12
- * just skipped. Degraded mode is explicit, not silent.
13
- *
14
- * Every invocation is logged to `llm_query_log` for cost tracking + answer
15
- * quality analysis.
16
- *
17
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
18
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
19
- */
20
- import { sanitizeFtsQuery } from './security.js';
21
- const DEFAULT_OLLAMA_URL = 'http://localhost:11434';
22
- const DEFAULT_OLLAMA_MODEL = 'llama3.2';
23
- const DEFAULT_OPENAI_MODEL = 'gpt-4o-mini';
24
- export class SubAgent {
25
- db;
26
- constructor(db) {
27
- this.db = db;
28
- }
29
- /** Assemble the most relevant context pieces for a query. */
30
- assembleContext(query, projectId, maxChars) {
31
- const pieces = [];
32
- let chars = 0;
33
- const counts = {};
34
- const push = (kind, text) => {
35
- const len = text.length;
36
- if (chars + len > maxChars)
37
- return false;
38
- pieces.push({ kind, text });
39
- chars += len;
40
- counts[kind] = (counts[kind] ?? 0) + 1;
41
- return true;
42
- };
43
- const ftsQuery = sanitizeFtsQuery(query.slice(0, 200));
44
- // 1. Current ground truths for the project (most authoritative)
45
- if (projectId != null) {
46
- const truths = this.db.prepare(`
1
+ import{sanitizeFtsQuery as O}from"./security.js";const w="http://localhost:11434",g="llama3.2",L="gpt-4o-mini";class x{db;constructor(t){this.db=t}assembleContext(t,s,i){const l=[];let c=0;const r={},a=(n,e)=>{const o=e.length;return c+o>i?!1:(l.push({kind:n,text:e}),c+=o,r[n]=(r[n]??0)+1,!0)},u=O(t.slice(0,200));if(s!=null){const n=this.db.prepare(`
47
2
  SELECT category, key, value, rationale, confidence
48
3
  FROM ground_truths
49
4
  WHERE project_id = ? AND is_current = 1
50
5
  ORDER BY confidence DESC
51
6
  LIMIT 30
52
- `).all(projectId);
53
- for (const t of truths) {
54
- const text = `[truth] ${t.category}.${t.key} = ${t.value}${t.rationale ? ` (${t.rationale})` : ''}`;
55
- if (!push('truth', text))
56
- break;
57
- }
58
- }
59
- // 2. FTS-matched sessions (recent / relevant work logs)
60
- if (ftsQuery) {
61
- try {
62
- const sessions = this.db.prepare(`
7
+ `).all(s);for(const e of n){const o=`[truth] ${e.category}.${e.key} = ${e.value}${e.rationale?` (${e.rationale})`:""}`;if(!a("truth",o))break}}if(u)try{const n=this.db.prepare(`
63
8
  SELECT s.id, s.date, s.summary, s.objectives, s.completed
64
9
  FROM sessions s
65
10
  JOIN sessions_fts fts ON fts.rowid = s.id
66
11
  WHERE sessions_fts MATCH ?
67
- ${projectId != null ? 'AND s.project_id = ?' : ''}
12
+ ${s!=null?"AND s.project_id = ?":""}
68
13
  ORDER BY s.date DESC
69
14
  LIMIT 8
70
- `).all(...(projectId != null ? [ftsQuery, projectId] : [ftsQuery]));
71
- for (const s of sessions) {
72
- const blob = [s.summary, s.completed, s.objectives].filter(Boolean).join(' ').slice(0, 600);
73
- if (blob && !push('session', `[session#${s.id} ${s.date}] ${blob}`))
74
- break;
75
- }
76
- }
77
- catch { /* FTS table may not exist on very old DBs */ }
78
- }
79
- // 3. Memory artifacts (validated patterns) that match
80
- if (ftsQuery) {
81
- try {
82
- const arts = this.db.prepare(`
15
+ `).all(...s!=null?[u,s]:[u]);for(const e of n){const o=[e.summary,e.completed,e.objectives].filter(Boolean).join(" ").slice(0,600);if(o&&!a("session",`[session#${e.id} ${e.date}] ${o}`))break}}catch{}if(u)try{const n=this.db.prepare(`
83
16
  SELECT ma.id, ma.problem, ma.validated_fix, ma.why_it_worked
84
17
  FROM memory_artifacts ma
85
18
  JOIN memory_artifacts_fts fts ON fts.rowid = ma.id
86
19
  WHERE memory_artifacts_fts MATCH ?
87
- ${projectId != null ? 'AND ma.project_id = ?' : ''}
20
+ ${s!=null?"AND ma.project_id = ?":""}
88
21
  AND (ma.needs_review = 0 OR ma.needs_review IS NULL)
89
22
  LIMIT 6
90
- `).all(...(projectId != null ? [ftsQuery, projectId] : [ftsQuery]));
91
- for (const a of arts) {
92
- const text = `[artifact#${a.id}] ${a.problem} → ${a.validated_fix ?? ''}${a.why_it_worked ? ` (${a.why_it_worked})` : ''}`;
93
- if (!push('artifact', text.slice(0, 500)))
94
- break;
95
- }
96
- }
97
- catch { /* table or FTS may not exist */ }
98
- }
99
- // 4. Unresolved failure patterns (counter-examples — "don't try this")
100
- if (projectId != null) {
101
- try {
102
- const fails = this.db.prepare(`
23
+ `).all(...s!=null?[u,s]:[u]);for(const e of n){const o=`[artifact#${e.id}] ${e.problem} \u2192 ${e.validated_fix??""}${e.why_it_worked?` (${e.why_it_worked})`:""}`;if(!a("artifact",o.slice(0,500)))break}}catch{}if(s!=null)try{const n=this.db.prepare(`
103
24
  SELECT id, scope, target, description, why_failed
104
25
  FROM failure_patterns
105
26
  WHERE resolved = 0 AND (project_id = ? OR project_id IS NULL)
106
27
  ORDER BY occurrences DESC, last_seen DESC
107
28
  LIMIT 5
108
- `).all(projectId);
109
- for (const f of fails) {
110
- const text = `[failure#${f.id} avoid] ${f.scope}:${f.target} — ${f.description}${f.why_failed ? ` (${f.why_failed})` : ''}`;
111
- if (!push('failure', text.slice(0, 400)))
112
- break;
113
- }
114
- }
115
- catch { /* table may not exist on pre-v3.9 DBs */ }
116
- }
117
- // 5. Symbol matches (cross-repo definitions)
118
- try {
119
- const symLike = `%${query.replace(/[%_]/g, '').slice(0, 50)}%`;
120
- const syms = this.db.prepare(`
29
+ `).all(s);for(const e of n){const o=`[failure#${e.id} avoid] ${e.scope}:${e.target} \u2014 ${e.description}${e.why_failed?` (${e.why_failed})`:""}`;if(!a("failure",o.slice(0,400)))break}}catch{}try{const n=`%${t.replace(/[%_]/g,"").slice(0,50)}%`,e=this.db.prepare(`
121
30
  SELECT symbol, kind, language, file_path, line, signature
122
31
  FROM symbol_index
123
32
  WHERE symbol LIKE ?
124
- ${projectId != null ? 'AND project_id = ?' : ''}
33
+ ${s!=null?"AND project_id = ?":""}
125
34
  LIMIT 10
126
- `).all(...(projectId != null ? [symLike, projectId] : [symLike]));
127
- for (const s of syms) {
128
- const text = `[symbol ${s.language}:${s.kind}] ${s.symbol} @ ${s.file_path}:${s.line}`;
129
- if (!push('symbol', text))
130
- break;
131
- }
132
- }
133
- catch { /* table may not exist on pre-v3.9 DBs */ }
134
- const summary = Object.entries(counts)
135
- .map(([k, v]) => `${k}:${v}`)
136
- .join(',');
137
- return { pieces, summary, chars };
138
- }
139
- /** Run an LLM completion against Ollama with the assembled context.
140
- * Returns null on connection failure so caller can fall back. */
141
- async tryOllama(query, context, url, model) {
142
- const prompt = this.buildPrompt(query, context);
143
- try {
144
- const res = await fetch(`${url.replace(/\/$/, '')}/api/generate`, {
145
- method: 'POST',
146
- headers: { 'Content-Type': 'application/json' },
147
- body: JSON.stringify({ model, prompt, stream: false }),
148
- signal: AbortSignal.timeout(60_000),
149
- });
150
- if (!res.ok)
151
- return null;
152
- const data = await res.json();
153
- if (!data.response)
154
- return null;
155
- return {
156
- answer: data.response.trim(),
157
- tokens_in: data.prompt_eval_count,
158
- tokens_out: data.eval_count,
159
- };
160
- }
161
- catch {
162
- return null;
163
- }
164
- }
165
- /** Run an LLM completion against OpenAI chat API. */
166
- async tryOpenAI(query, context, apiKey, model) {
167
- try {
168
- const res = await fetch('https://api.openai.com/v1/chat/completions', {
169
- method: 'POST',
170
- headers: {
171
- 'Content-Type': 'application/json',
172
- 'Authorization': `Bearer ${apiKey}`,
173
- },
174
- body: JSON.stringify({
175
- model,
176
- messages: [
177
- { role: 'system', content: 'You are Wyrm, a project-memory assistant. Answer the user\'s question using ONLY the supplied context. If the context is insufficient, say so explicitly. Be terse.' },
178
- { role: 'user', content: this.buildPrompt(query, context) },
179
- ],
180
- temperature: 0.2,
181
- }),
182
- signal: AbortSignal.timeout(60_000),
183
- });
184
- if (!res.ok)
185
- return null;
186
- const data = await res.json();
187
- const text = data.choices?.[0]?.message?.content?.trim();
188
- if (!text)
189
- return null;
190
- return {
191
- answer: text,
192
- tokens_in: data.usage?.prompt_tokens,
193
- tokens_out: data.usage?.completion_tokens,
194
- };
195
- }
196
- catch {
197
- return null;
198
- }
199
- }
200
- buildPrompt(query, context) {
201
- return [
202
- 'You are answering a question using the project memory below.',
203
- 'Cite specific items by their bracketed tags (e.g. [truth], [session#42], [failure#7]).',
204
- 'If the context does not contain the answer, say so plainly.',
205
- '',
206
- '=== CONTEXT ===',
207
- context,
208
- '=== END CONTEXT ===',
209
- '',
210
- `Question: ${query}`,
211
- '',
212
- 'Answer:',
213
- ].join('\n');
214
- }
215
- /** Main entry — query → context → LLM → answer. */
216
- async ask(input) {
217
- const start = Date.now();
218
- const maxChars = Math.min(Math.max(2_000, input.max_context_chars ?? 12_000), 50_000);
219
- const { pieces, summary, chars } = this.assembleContext(input.query, input.project_id, maxChars);
220
- const contextText = pieces.map(p => p.text).join('\n');
221
- // Resolve model preference
222
- const override = input.model_override?.split(':') ?? [];
223
- const preferKind = override[0]; // 'ollama' | 'openai' | undefined
224
- const preferModel = override[1];
225
- const ollamaUrl = input.ollama_url ?? process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
226
- const openaiKey = input.openai_api_key ?? process.env.OPENAI_API_KEY;
227
- let llmResult = null;
228
- let modelUsed = 'none';
229
- // Try preferred kind first, fall back to the other.
230
- const tryOrder = preferKind === 'openai'
231
- ? ['openai', 'ollama']
232
- : ['ollama', 'openai'];
233
- for (const kind of tryOrder) {
234
- if (llmResult)
235
- break;
236
- if (kind === 'ollama') {
237
- const model = preferKind === 'ollama' ? (preferModel || DEFAULT_OLLAMA_MODEL) : DEFAULT_OLLAMA_MODEL;
238
- const r = await this.tryOllama(input.query, contextText, ollamaUrl, model);
239
- if (r) {
240
- llmResult = r;
241
- modelUsed = `ollama:${model}`;
242
- }
243
- }
244
- else if (kind === 'openai' && openaiKey) {
245
- const model = preferKind === 'openai' ? (preferModel || DEFAULT_OPENAI_MODEL) : DEFAULT_OPENAI_MODEL;
246
- const r = await this.tryOpenAI(input.query, contextText, openaiKey, model);
247
- if (r) {
248
- llmResult = r;
249
- modelUsed = `openai:${model}`;
250
- }
251
- }
252
- }
253
- const latency = Date.now() - start;
254
- const degraded = llmResult === null;
255
- const answer = llmResult?.answer ?? this.degradedAnswer(input.query, pieces);
256
- // Log to llm_query_log (fire-and-forget)
257
- try {
258
- this.db.prepare(`
35
+ `).all(...s!=null?[n,s]:[n]);for(const o of e){const m=`[symbol ${o.language}:${o.kind}] ${o.symbol} @ ${o.file_path}:${o.line}`;if(!a("symbol",m))break}}catch{}const _=Object.entries(r).map(([n,e])=>`${n}:${e}`).join(",");return{pieces:l,summary:_,chars:c}}async tryOllama(t,s,i,l){const c=this.buildPrompt(t,s);try{const r=await fetch(`${i.replace(/\/$/,"")}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:l,prompt:c,stream:!1}),signal:AbortSignal.timeout(6e4)});if(!r.ok)return null;const a=await r.json();return a.response?{answer:a.response.trim(),tokens_in:a.prompt_eval_count,tokens_out:a.eval_count}:null}catch{return null}}async tryOpenAI(t,s,i,l){try{const c=await fetch("https://api.openai.com/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${i}`},body:JSON.stringify({model:l,messages:[{role:"system",content:"You are Wyrm, a project-memory assistant. Answer the user's question using ONLY the supplied context. If the context is insufficient, say so explicitly. Be terse."},{role:"user",content:this.buildPrompt(t,s)}],temperature:.2}),signal:AbortSignal.timeout(6e4)});if(!c.ok)return null;const r=await c.json(),a=r.choices?.[0]?.message?.content?.trim();return a?{answer:a,tokens_in:r.usage?.prompt_tokens,tokens_out:r.usage?.completion_tokens}:null}catch{return null}}buildPrompt(t,s){return["You are answering a question using the project memory below.","Cite specific items by their bracketed tags (e.g. [truth], [session#42], [failure#7]).","If the context does not contain the answer, say so plainly.","","=== CONTEXT ===",s,"=== END CONTEXT ===","",`Question: ${t}`,"","Answer:"].join(`
36
+ `)}async ask(t){const s=Date.now(),i=Math.min(Math.max(2e3,t.max_context_chars??12e3),5e4),{pieces:l,summary:c,chars:r}=this.assembleContext(t.query,t.project_id,i),a=l.map(h=>h.text).join(`
37
+ `),u=t.model_override?.split(":")??[],_=u[0],n=u[1],e=t.ollama_url??process.env.OLLAMA_URL??w,o=t.openai_api_key??process.env.OPENAI_API_KEY;let m=null,y="none";const $=_==="openai"?["openai","ollama"]:["ollama","openai"];for(const h of $){if(m)break;if(h==="ollama"){const d=_==="ollama"&&n||g,f=await this.tryOllama(t.query,a,e,d);f&&(m=f,y=`ollama:${d}`)}else if(h==="openai"&&o){const d=_==="openai"&&n||L,f=await this.tryOpenAI(t.query,a,o,d);f&&(m=f,y=`openai:${d}`)}}const b=Date.now()-s,p=m===null,E=m?.answer??this.degradedAnswer(t.query,l);try{this.db.prepare(`
259
38
  INSERT INTO llm_query_log
260
39
  (query, project_id, context_summary, context_chars, response,
261
40
  model, tokens_in, tokens_out, latency_ms, error_message)
262
41
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
263
- `).run(input.query.slice(0, 1000), input.project_id ?? null, summary, chars, answer.slice(0, 4000), modelUsed, llmResult?.tokens_in ?? null, llmResult?.tokens_out ?? null, latency, degraded ? 'No LLM available degraded to context-only' : null);
264
- }
265
- catch { /* logging never breaks the answer path */ }
266
- return {
267
- query: input.query,
268
- answer,
269
- model: modelUsed,
270
- context_summary: summary,
271
- context_chars: chars,
272
- latency_ms: latency,
273
- tokens_in: llmResult?.tokens_in,
274
- tokens_out: llmResult?.tokens_out,
275
- degraded,
276
- error: degraded ? 'No LLM available (no Ollama on localhost:11434 and no OPENAI_API_KEY). Returning raw context.' : undefined,
277
- };
278
- }
279
- /** Fallback when no LLM is reachable — return the assembled context
280
- * directly so the caller still gets the data, just unprocessed. */
281
- degradedAnswer(query, pieces) {
282
- if (pieces.length === 0)
283
- return `_No matching context found in Wyrm for "${query}". No LLM available to synthesize._`;
284
- const lines = [];
285
- lines.push(`_LLM not available — returning ${pieces.length} matched context pieces:_`);
286
- lines.push('');
287
- for (const p of pieces)
288
- lines.push(p.text);
289
- return lines.join('\n');
290
- }
291
- }
292
- //# sourceMappingURL=sub-agent.js.map
42
+ `).run(t.query.slice(0,1e3),t.project_id??null,c,r,E.slice(0,4e3),y,m?.tokens_in??null,m?.tokens_out??null,b,p?"No LLM available \u2014 degraded to context-only":null)}catch{}return{query:t.query,answer:E,model:y,context_summary:c,context_chars:r,latency_ms:b,tokens_in:m?.tokens_in,tokens_out:m?.tokens_out,degraded:p,error:p?"No LLM available (no Ollama on localhost:11434 and no OPENAI_API_KEY). Returning raw context.":void 0}}degradedAnswer(t,s){if(s.length===0)return`_No matching context found in Wyrm for "${t}". No LLM available to synthesize._`;const i=[];i.push(`_LLM not available \u2014 returning ${s.length} matched context pieces:_`),i.push("");for(const l of s)i.push(l.text);return i.join(`
43
+ `)}}export{x as SubAgent};