wyrm-mcp 7.2.1 → 7.2.2

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