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