wyrm-mcp 7.3.2 → 7.4.0

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 (200) hide show
  1. package/README.md +12 -18
  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 +51 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1121 -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 +1 -0
  81. package/dist/handlers/failure.d.ts.map +1 -1
  82. package/dist/handlers/failure.js +458 -13
  83. package/dist/handlers/failure.js.map +1 -1
  84. package/dist/handlers/goals.js +296 -4
  85. package/dist/handlers/intelligence.d.ts.map +1 -1
  86. package/dist/handlers/intelligence.js +690 -126
  87. package/dist/handlers/intelligence.js.map +1 -1
  88. package/dist/handlers/invoicing.js +70 -1
  89. package/dist/handlers/mcpclient.js +137 -6
  90. package/dist/handlers/orchestration.js +125 -40
  91. package/dist/handlers/output-schemas.js +24 -1
  92. package/dist/handlers/presence.js +99 -3
  93. package/dist/handlers/project.js +182 -28
  94. package/dist/handlers/prompts.js +157 -6
  95. package/dist/handlers/quest.js +224 -4
  96. package/dist/handlers/recall.js +237 -13
  97. package/dist/handlers/registry.js +167 -1
  98. package/dist/handlers/resources.js +288 -1
  99. package/dist/handlers/review.js +74 -11
  100. package/dist/handlers/run.js +498 -16
  101. package/dist/handlers/search.js +338 -15
  102. package/dist/handlers/session.js +643 -31
  103. package/dist/handlers/share.js +184 -8
  104. package/dist/handlers/shims.js +464 -1
  105. package/dist/handlers/skill.js +449 -67
  106. package/dist/handlers/survivors.js +120 -1
  107. package/dist/handlers/symbols.js +109 -8
  108. package/dist/handlers/syncops.js +302 -4
  109. package/dist/handlers/types.js +27 -1
  110. package/dist/harvest.js +191 -5
  111. package/dist/hours.js +156 -7
  112. package/dist/http-auth.js +321 -3
  113. package/dist/http-fast.d.ts.map +1 -1
  114. package/dist/http-fast.js +1308 -22
  115. package/dist/http-fast.js.map +1 -1
  116. package/dist/icons.js +47 -1
  117. package/dist/importers.js +268 -1
  118. package/dist/index.js +844 -2
  119. package/dist/index.js.map +1 -1
  120. package/dist/indexer.js +145 -4
  121. package/dist/intelligence.d.ts +16 -0
  122. package/dist/intelligence.d.ts.map +1 -1
  123. package/dist/intelligence.js +283 -31
  124. package/dist/intelligence.js.map +1 -1
  125. package/dist/internal-dispatch.js +212 -3
  126. package/dist/keyset.js +110 -1
  127. package/dist/knowledge-graph.js +176 -12
  128. package/dist/license.js +441 -2
  129. package/dist/logger.js +199 -2
  130. package/dist/maintenance.js +148 -2
  131. package/dist/mcp-client.js +262 -6
  132. package/dist/memory-artifacts.js +596 -32
  133. package/dist/migrate-prompt.js +124 -2
  134. package/dist/migrations.d.ts.map +1 -1
  135. package/dist/migrations.js +799 -42
  136. package/dist/migrations.js.map +1 -1
  137. package/dist/performance.js +228 -1
  138. package/dist/presence.js +140 -11
  139. package/dist/priority-embed.js +164 -5
  140. package/dist/providers/embedding-provider.js +196 -1
  141. package/dist/readonly-gate.js +29 -1
  142. package/dist/receipt.js +43 -1
  143. package/dist/rehydration.js +157 -9
  144. package/dist/reindex.js +88 -1
  145. package/dist/render-target.js +544 -21
  146. package/dist/render.js +280 -4
  147. package/dist/repl-guard.js +173 -1
  148. package/dist/replication-daemon-entrypoint.js +31 -1
  149. package/dist/replication-daemon.js +262 -2
  150. package/dist/rerank.js +142 -1
  151. package/dist/resilience.js +591 -1
  152. package/dist/reverse-bridge.js +360 -5
  153. package/dist/security.js +244 -1
  154. package/dist/session-seen.js +51 -3
  155. package/dist/setup.js +260 -1
  156. package/dist/skill-author.js +168 -5
  157. package/dist/spec-kit.js +191 -1
  158. package/dist/sqlite-busy.js +154 -1
  159. package/dist/statusline.js +315 -11
  160. package/dist/sub-agent.js +262 -13
  161. package/dist/summarizer.js +139 -13
  162. package/dist/symbols.js +283 -7
  163. package/dist/sync.js +359 -5
  164. package/dist/tasks-dispatch.js +84 -1
  165. package/dist/tasks.js +282 -1
  166. package/dist/token-budget.js +143 -1
  167. package/dist/tool-analytics.js +129 -7
  168. package/dist/tool-annotations.js +365 -1
  169. package/dist/tool-manifest-v2.json +1 -1
  170. package/dist/tool-manifest.json +1 -1
  171. package/dist/tool-profiles.js +75 -1
  172. package/dist/trace-harvest.js +244 -6
  173. package/dist/truth-cascade.d.ts +13 -0
  174. package/dist/truth-cascade.d.ts.map +1 -0
  175. package/dist/truth-cascade.js +4 -0
  176. package/dist/truth-cascade.js.map +1 -0
  177. package/dist/types.js +30 -1
  178. package/dist/ui-dashboard.js +50 -41
  179. package/dist/ulid.js +81 -1
  180. package/dist/usage-tracker.js +66 -1
  181. package/dist/validate.js +129 -1
  182. package/dist/vault.js +534 -1
  183. package/dist/vector-init.js +67 -1
  184. package/dist/vectors.js +184 -3
  185. package/dist/version-check.d.ts.map +1 -1
  186. package/dist/version-check.js +145 -4
  187. package/dist/version-check.js.map +1 -1
  188. package/dist/visibility.js +155 -19
  189. package/dist/wyrm-cli.js +2849 -101
  190. package/dist/wyrm-cli.js.map +1 -1
  191. package/dist/wyrm-guard.d.ts.map +1 -1
  192. package/dist/wyrm-guard.js +475 -14
  193. package/dist/wyrm-guard.js.map +1 -1
  194. package/dist/wyrm-loop.js +174 -3
  195. package/dist/wyrm-loop.js.map +1 -1
  196. package/dist/wyrm-manifest.json +1 -1
  197. package/dist/wyrm-statusline-daemon.js +11 -1
  198. package/dist/wyrm-statusline.js +56 -4
  199. package/dist/wyrm-ui.js +77 -9
  200. package/package.json +3 -2
@@ -1,18 +1,322 @@
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 D,resolveDbPath 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"),W=300*1e3,U="\x1B[38;2;192;192;192m",j="\x1B[0m",m=d.brand?`${U}${d.brand}${j}`:"";function b(t,n){if(!n)return null;let o=n;for(let e=0;e<12;e++){const r=t.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(t,n,...o){try{return t.prepare(n).get(...o)?.n??0}catch{return 0}}function H(t,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 e=b(t,n.cwd),r=e?.id,s=n.showSavings??process.env.WYRM_STATUSLINE_SHOW_SAVINGS==="1",i=r?f(t,"SELECT COUNT(*) AS n FROM quests WHERE project_id = ? AND status = 'pending'",r):0,a=r?f(t,"SELECT COUNT(*) AS n FROM ground_truths WHERE project_id = ?",r):0;let c=0;n.sessionId?c=f(t,`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(t,"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=t.prepare(`
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 Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. 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, resolveDbPath } 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(`
3
117
  SELECT estimated_tokens AS n FROM token_savings_log
4
118
  WHERE category = 'cached_preamble' AND session_id = ?
5
119
  ORDER BY id DESC LIMIT 1
6
- `).get(n.sessionId??-1)?.n??0}catch{}let p=null;if(s)try{p=t.prepare(`
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(`
7
129
  SELECT COALESCE(SUM(estimated_tokens), 0) AS n FROM token_savings_log
8
130
  WHERE session_id = ?
9
- `).get(n.sessionId??-1).n}catch{p=0}const u=[`${m} Wyrm`];return e&&u.push(e.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:e?.name??null,activeQuests:i,contextTokensUsed:l,contextBudget:w,blockedThisSession:c,truthCount:a,tokensSaved:p}}}function g(t){return t>=1e3?`${Math.round(t/100)/10}k`:String(t)}function K(t,n,o,e,r,s="conservative"){try{t.prepare(`
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(`
10
167
  INSERT INTO token_savings_log (session_id, tool_name, category, estimated_tokens, confidence)
11
168
  VALUES (?, ?, ?, ?, ?)
12
- `).run(r??null,n,o,e,s)}catch{}}let _=null,T=null;function O(){_&&clearTimeout(_),_=setTimeout(()=>{try{T?.close()}catch{}E(),process.exit(0)},W)}function E(){try{x(y)}catch{}try{x(v)}catch{}}function z(){const t=S(h(),".wyrm");C(t)||I(t,{recursive:!0}),E();const o=new D(L()).getDatabase();T=$(e=>{let r="";e.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);e.write(`${JSON.stringify(l)}
14
- `)}catch(c){e.write(`${JSON.stringify({text:`${m} Wyrm`,error:String(c)})}
15
- `)}e.end(),O()}),e.on("error",()=>{})}),T.on("error",e=>{e.code==="EADDRINUSE"&&process.exit(0),console.error("wyrm-statusline-daemon: socket error",e),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 X(t){return new Promise(n=>{const o=e=>{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(t)}
18
- `)});r.on("error",()=>{e===0?(F(),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 F(t){let n=t;if(!n){let e=process.argv[1]||"";try{e&&(e=M(e))}catch{}n=S(S(e,".."),"wyrm-statusline-daemon.js")}N(process.execPath,[n],{detached:!0,stdio:"ignore"}).unref()}export{H as computeStatusline,K as logSavings,X as queryDaemon,z as runDaemon};
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(resolveDbPath());
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
package/dist/sub-agent.js CHANGED
@@ -1,43 +1,292 @@
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(`
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 Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. 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(`
2
47
  SELECT category, key, value, rationale, confidence
3
48
  FROM ground_truths
4
49
  WHERE project_id = ? AND is_current = 1
5
50
  ORDER BY confidence DESC
6
51
  LIMIT 30
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(`
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(`
8
63
  SELECT s.id, s.date, s.summary, s.objectives, s.completed
9
64
  FROM sessions s
10
65
  JOIN sessions_fts fts ON fts.rowid = s.id
11
66
  WHERE sessions_fts MATCH ?
12
- ${s!=null?"AND s.project_id = ?":""}
67
+ ${projectId != null ? 'AND s.project_id = ?' : ''}
13
68
  ORDER BY s.date DESC
14
69
  LIMIT 8
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(`
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(`
16
83
  SELECT ma.id, ma.problem, ma.validated_fix, ma.why_it_worked
17
84
  FROM memory_artifacts ma
18
85
  JOIN memory_artifacts_fts fts ON fts.rowid = ma.id
19
86
  WHERE memory_artifacts_fts MATCH ?
20
- ${s!=null?"AND ma.project_id = ?":""}
87
+ ${projectId != null ? 'AND ma.project_id = ?' : ''}
21
88
  AND (ma.needs_review = 0 OR ma.needs_review IS NULL)
22
89
  LIMIT 6
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(`
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(`
24
103
  SELECT id, scope, target, description, why_failed
25
104
  FROM failure_patterns
26
105
  WHERE resolved = 0 AND (project_id = ? OR project_id IS NULL)
27
106
  ORDER BY occurrences DESC, last_seen DESC
28
107
  LIMIT 5
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(`
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(`
30
121
  SELECT symbol, kind, language, file_path, line, signature
31
122
  FROM symbol_index
32
123
  WHERE symbol LIKE ?
33
- ${s!=null?"AND project_id = ?":""}
124
+ ${projectId != null ? 'AND project_id = ?' : ''}
34
125
  LIMIT 10
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(`
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(`
38
259
  INSERT INTO llm_query_log
39
260
  (query, project_id, context_summary, context_chars, response,
40
261
  model, tokens_in, tokens_out, latency_ms, error_message)
41
262
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
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};
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