@toolbaux/guardian 0.1.21 → 0.1.23
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/README.md +1 -1
- package/dist/adapters/runner.js +72 -3
- package/dist/adapters/typescript-adapter.js +24 -10
- package/dist/benchmarking/metrics/context-coverage.js +82 -0
- package/dist/benchmarking/metrics/drift-score.js +104 -0
- package/dist/benchmarking/metrics/search-recall.js +207 -0
- package/dist/benchmarking/metrics/token-efficiency.js +79 -0
- package/dist/benchmarking/report.js +131 -0
- package/dist/benchmarking/runner.js +175 -0
- package/dist/benchmarking/types.js +13 -0
- package/dist/cli.js +53 -10
- package/dist/commands/benchmark.js +62 -0
- package/dist/commands/discrepancy.js +1 -1
- package/dist/commands/doc-generate.js +1 -1
- package/dist/commands/doc-html.js +1 -1
- package/dist/commands/extract.js +1 -1
- package/dist/commands/feature-context.js +1 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/intel.js +47 -1
- package/dist/commands/mcp-serve.js +48 -288
- package/dist/commands/search.js +602 -14
- package/dist/db/file-specs-store.js +174 -0
- package/dist/db/fts-builder.js +305 -0
- package/dist/db/index.js +55 -0
- package/dist/db/specs-store.js +13 -0
- package/dist/db/sqlite-specs-store.js +441 -0
- package/dist/extract/codebase-intel.js +31 -2
- package/dist/extract/compress.js +70 -3
- package/dist/extract/context-block.js +11 -2
- package/dist/extract/function-intel.js +5 -2
- package/dist/extract/index.js +1 -23
- package/dist/extract/writer.js +6 -0
- package/package.json +3 -1
|
@@ -17,6 +17,21 @@
|
|
|
17
17
|
import fs from "node:fs/promises";
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import readline from "node:readline";
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
21
|
+
// ── CLI proxy ──
|
|
22
|
+
// Resolve the guardian CLI binary relative to this file (dist/cli.js).
|
|
23
|
+
const CLI_BIN = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../cli.js");
|
|
24
|
+
/** Run a guardian CLI subcommand and return stdout. */
|
|
25
|
+
function runCli(args) {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const proc = spawn(process.execPath, [CLI_BIN, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
28
|
+
let out = "";
|
|
29
|
+
let err = "";
|
|
30
|
+
proc.stdout.on("data", (d) => { out += d.toString(); });
|
|
31
|
+
proc.stderr.on("data", (d) => { err += d.toString(); });
|
|
32
|
+
proc.on("close", () => resolve(out.trim() || err.trim() || "{}"));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
20
35
|
const metrics = {
|
|
21
36
|
session_start: Date.now(),
|
|
22
37
|
calls: [],
|
|
@@ -64,9 +79,11 @@ const metrics = {
|
|
|
64
79
|
};
|
|
65
80
|
},
|
|
66
81
|
};
|
|
67
|
-
// ──
|
|
82
|
+
// ── Session flag — written on every guardian tool call so the PreToolUse hook knows guardian is active ──
|
|
83
|
+
const SESSION_FLAG = "/tmp/guardian-last-call";
|
|
84
|
+
// ── Response cache (dedup repeated queries within a session) ──
|
|
68
85
|
const responseCache = new Map();
|
|
69
|
-
const CACHE_TTL = 30_000;
|
|
86
|
+
const CACHE_TTL = 30_000;
|
|
70
87
|
function getCached(key) {
|
|
71
88
|
const entry = responseCache.get(key);
|
|
72
89
|
if (entry && Date.now() - entry.time < CACHE_TTL)
|
|
@@ -76,286 +93,23 @@ function getCached(key) {
|
|
|
76
93
|
function setCache(key, text) {
|
|
77
94
|
responseCache.set(key, { text, time: Date.now() });
|
|
78
95
|
}
|
|
79
|
-
// ──
|
|
80
|
-
|
|
81
|
-
let
|
|
82
|
-
let lastLoadTime = 0;
|
|
83
|
-
async function loadIntel() {
|
|
84
|
-
// Reload if file changed (check every 5s max)
|
|
85
|
-
const now = Date.now();
|
|
86
|
-
if (intel && now - lastLoadTime < 5000)
|
|
87
|
-
return intel;
|
|
88
|
-
try {
|
|
89
|
-
const raw = await fs.readFile(intelPath, "utf8");
|
|
90
|
-
intel = JSON.parse(raw);
|
|
91
|
-
lastLoadTime = now;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// Return cached or empty
|
|
95
|
-
if (!intel) {
|
|
96
|
-
intel = { api_registry: {}, model_registry: {}, service_map: [], frontend_pages: [], meta: { project: "unknown", counts: {} } };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return intel;
|
|
100
|
-
}
|
|
101
|
-
// ── Function intelligence loader ──
|
|
102
|
-
let funcIntel = null;
|
|
103
|
-
let funcIntelPath = "";
|
|
104
|
-
let funcIntelLoadTime = 0;
|
|
105
|
-
async function loadFuncIntel() {
|
|
106
|
-
if (!funcIntelPath)
|
|
107
|
-
return null;
|
|
108
|
-
const now = Date.now();
|
|
109
|
-
if (funcIntel && now - funcIntelLoadTime < 5000)
|
|
110
|
-
return funcIntel;
|
|
111
|
-
try {
|
|
112
|
-
const raw = await fs.readFile(funcIntelPath, "utf8");
|
|
113
|
-
funcIntel = JSON.parse(raw);
|
|
114
|
-
funcIntelLoadTime = now;
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
// File may not exist yet — not an error
|
|
118
|
-
}
|
|
119
|
-
return funcIntel;
|
|
120
|
-
}
|
|
121
|
-
// ── Helpers ──
|
|
122
|
-
const SKIP_SERVICES = new Set(["str", "dict", "int", "len", "float", "max", "join", "getattr", "lower", "open", "params.append", "updates.append"]);
|
|
123
|
-
function compact(obj) {
|
|
124
|
-
return JSON.stringify(obj);
|
|
125
|
-
}
|
|
126
|
-
function normalize(p) {
|
|
127
|
-
return p.replace(/^\.\//, "").replace(/\/\//g, "/");
|
|
128
|
-
}
|
|
129
|
-
function findModule(data, file) {
|
|
130
|
-
const f = normalize(file);
|
|
131
|
-
return data.service_map?.find((m) => {
|
|
132
|
-
const mp = normalize(m.path || "");
|
|
133
|
-
return mp && (f.startsWith(mp + "/") || f === mp);
|
|
134
|
-
}) || data.service_map?.find((m) => {
|
|
135
|
-
// Fallback: match by module ID (handles doubled paths)
|
|
136
|
-
const mid = normalize(m.id || "");
|
|
137
|
-
return mid && f.includes(mid);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
function findEndpointsInFile(data, file) {
|
|
141
|
-
const f = normalize(file);
|
|
142
|
-
const basename = path.basename(f);
|
|
143
|
-
return Object.values(data.api_registry || {}).filter((ep) => {
|
|
144
|
-
const ef = normalize(ep.file || "");
|
|
145
|
-
return ef && (f.includes(ef) || ef.includes(f) || ef.endsWith(basename));
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
function findModelsInFile(data, file) {
|
|
149
|
-
const f = normalize(file);
|
|
150
|
-
const basename = path.basename(f);
|
|
151
|
-
return Object.values(data.model_registry || {}).filter((m) => {
|
|
152
|
-
const mf = normalize(m.file || "");
|
|
153
|
-
return mf && (f.includes(mf) || mf.includes(f) || mf.endsWith(basename));
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
// ── Tool implementations (compact JSON, no redundancy) ──
|
|
96
|
+
// ── Tool implementations — thin CLI proxies ──
|
|
97
|
+
// All logic lives in `guardian search`. MCP tools are just structured wrappers.
|
|
98
|
+
let specsInputDir = "";
|
|
157
99
|
async function orient() {
|
|
158
|
-
|
|
159
|
-
const contextPath = path.join(path.dirname(intelPath), "architecture-context.md");
|
|
160
|
-
try {
|
|
161
|
-
const raw = await fs.readFile(contextPath, "utf8");
|
|
162
|
-
// Extract the content between guardian:context markers
|
|
163
|
-
const match = raw.match(/<!-- guardian:context[^>]*-->([\s\S]*?)<!-- \/guardian:context -->/);
|
|
164
|
-
if (match) {
|
|
165
|
-
// Parse the markdown into compact structured data
|
|
166
|
-
const content = match[1];
|
|
167
|
-
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
168
|
-
// Extract key sections
|
|
169
|
-
const desc = raw.match(/Description: (.+)/)?.[1] || "";
|
|
170
|
-
const codeMap = lines.find((l) => l.startsWith("**Backend:**")) || "";
|
|
171
|
-
// Module map with exports
|
|
172
|
-
const moduleLines = lines.filter((l) => l.startsWith("- **backend/") || l.startsWith("- **frontend/"));
|
|
173
|
-
const modules = moduleLines.map((l) => {
|
|
174
|
-
const m = l.match(/\*\*([^*]+)\*\*\s*\(([^)]+)\)\s*[—–-]\s*(.*)/);
|
|
175
|
-
return m ? [m[1], m[2], m[3].slice(0, 60)] : null;
|
|
176
|
-
}).filter(Boolean);
|
|
177
|
-
// Dependencies
|
|
178
|
-
const deps = lines.filter((l) => l.includes("→")).map((l) => l.replace(/^- /, ""));
|
|
179
|
-
// High-coupling files
|
|
180
|
-
const coupling = lines.filter((l) => l.match(/score \d/)).map((l) => l.replace(/^- /, ""));
|
|
181
|
-
// Structural intelligence
|
|
182
|
-
const si = lines.filter((l) => l.includes("depth=")).map((l) => l.replace(/^- /, ""));
|
|
183
|
-
// Model-endpoint map
|
|
184
|
-
const modelEp = lines.filter((l) => l.includes("endpoints) ->")).map((l) => l.replace(/^- /, ""));
|
|
185
|
-
return compact({
|
|
186
|
-
desc: desc.slice(0, 120),
|
|
187
|
-
map: codeMap,
|
|
188
|
-
modules,
|
|
189
|
-
deps,
|
|
190
|
-
coupling: coupling.slice(0, 5),
|
|
191
|
-
si: si.slice(0, 5),
|
|
192
|
-
modelEp,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
catch { }
|
|
197
|
-
// Fallback: build from codebase-intelligence.json
|
|
198
|
-
const d = await loadIntel();
|
|
199
|
-
const c = d.meta?.counts || {};
|
|
200
|
-
// Compute endpoint counts from api_registry (service_map counts are often 0)
|
|
201
|
-
const epByMod = {};
|
|
202
|
-
for (const ep of Object.values(d.api_registry || {})) {
|
|
203
|
-
epByMod[ep.module] = (epByMod[ep.module] || 0) + 1;
|
|
204
|
-
}
|
|
205
|
-
const mods = (d.service_map || []).filter((m) => m.file_count > 0);
|
|
206
|
-
const topMods = mods
|
|
207
|
-
.map((m) => ({ ...m, ep_count: epByMod[m.id] || 0 }))
|
|
208
|
-
.sort((a, b) => b.ep_count - a.ep_count)
|
|
209
|
-
.slice(0, 6);
|
|
210
|
-
return compact({
|
|
211
|
-
p: d.meta?.project,
|
|
212
|
-
ep: c.endpoints, mod: c.models, pg: c.pages, m: c.modules,
|
|
213
|
-
top: topMods.map((m) => [m.id, m.ep_count, m.layer]),
|
|
214
|
-
pages: (d.frontend_pages || []).map((p) => p.path),
|
|
215
|
-
});
|
|
100
|
+
return runCli(["search", "--orient", "--input", specsInputDir]);
|
|
216
101
|
}
|
|
217
102
|
async function context(args) {
|
|
218
|
-
|
|
219
|
-
const t = args.target;
|
|
220
|
-
// Check if target is an endpoint (e.g. "POST /sessions/start")
|
|
221
|
-
const epMatch = t.match(/^(GET|POST|PUT|DELETE|PATCH)\s+(.+)$/i);
|
|
222
|
-
if (epMatch) {
|
|
223
|
-
const ep = d.api_registry?.[`${epMatch[1].toUpperCase()} ${epMatch[2]}`]
|
|
224
|
-
|| Object.values(d.api_registry || {}).find((e) => e.method === epMatch[1].toUpperCase() && e.path === epMatch[2]);
|
|
225
|
-
if (!ep)
|
|
226
|
-
return compact({ err: "not found" });
|
|
227
|
-
const svcs = (ep.service_calls || []).filter((s) => !SKIP_SERVICES.has(s));
|
|
228
|
-
return compact({
|
|
229
|
-
ep: `${ep.method} ${ep.path}`, h: ep.handler, f: ep.file, m: ep.module,
|
|
230
|
-
req: ep.request_schema, res: ep.response_schema,
|
|
231
|
-
calls: svcs, ai: ep.ai_operations?.length || 0,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
// Otherwise treat as file path
|
|
235
|
-
const file = t.replace(/^\.\//, "");
|
|
236
|
-
const mod = findModule(d, file);
|
|
237
|
-
const eps = findEndpointsInFile(d, file);
|
|
238
|
-
const models = findModelsInFile(d, file);
|
|
239
|
-
const fileName = path.basename(file, path.extname(file));
|
|
240
|
-
const calledBy = [];
|
|
241
|
-
for (const ep of Object.values(d.api_registry || {})) {
|
|
242
|
-
if (ep.service_calls?.some((s) => s.toLowerCase().includes(fileName.toLowerCase()))) {
|
|
243
|
-
calledBy.push(`${ep.method} ${ep.path}`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
const calls = eps.flatMap((ep) => (ep.service_calls || []).filter((s) => !SKIP_SERVICES.has(s)));
|
|
247
|
-
return compact({
|
|
248
|
-
f: file,
|
|
249
|
-
mod: mod ? [mod.id, mod.layer] : null,
|
|
250
|
-
ep: eps.map((e) => `${e.method} ${e.path}`),
|
|
251
|
-
models: models.map((m) => [m.name, m.fields?.length || 0]),
|
|
252
|
-
calls: [...new Set(calls)],
|
|
253
|
-
calledBy: calledBy.slice(0, 8),
|
|
254
|
-
});
|
|
103
|
+
return runCli(["search", "--file", args.target, "--input", specsInputDir]);
|
|
255
104
|
}
|
|
256
105
|
async function impact(args) {
|
|
257
|
-
|
|
258
|
-
const file = args.target.replace(/^\.\//, "");
|
|
259
|
-
const eps = findEndpointsInFile(d, file);
|
|
260
|
-
const models = findModelsInFile(d, file);
|
|
261
|
-
const modelNames = new Set(models.map((m) => m.name));
|
|
262
|
-
const affectedEps = Object.values(d.api_registry || {}).filter((ep) => (ep.request_schema && modelNames.has(ep.request_schema)) ||
|
|
263
|
-
(ep.response_schema && modelNames.has(ep.response_schema)));
|
|
264
|
-
const mod = findModule(d, file);
|
|
265
|
-
const depMods = mod ? (d.service_map || []).filter((m) => m.imports?.includes(mod.id)) : [];
|
|
266
|
-
const affectedPages = (d.frontend_pages || []).filter((p) => p.api_calls?.some((call) => eps.some((ep) => call.includes(ep.path?.split("{")[0]))));
|
|
267
|
-
const total = eps.length + affectedEps.length + depMods.length + affectedPages.length;
|
|
268
|
-
return compact({
|
|
269
|
-
f: file,
|
|
270
|
-
risk: total > 5 ? "HIGH" : total > 2 ? "MED" : "LOW",
|
|
271
|
-
ep: eps.map((e) => `${e.method} ${e.path}`),
|
|
272
|
-
models: models.map((m) => m.name),
|
|
273
|
-
affectedEp: affectedEps.map((e) => `${e.method} ${e.path}`),
|
|
274
|
-
depMods: depMods.map((m) => m.id),
|
|
275
|
-
pages: affectedPages.map((p) => p.path),
|
|
276
|
-
});
|
|
106
|
+
return runCli(["search", "--impact", args.target, "--input", specsInputDir]);
|
|
277
107
|
}
|
|
278
108
|
async function search(args) {
|
|
279
|
-
|
|
280
|
-
const q = args.query.toLowerCase();
|
|
281
|
-
// Endpoints: match path, handler, or service calls
|
|
282
|
-
const eps = Object.values(d.api_registry || {}).filter((ep) => ep.path?.toLowerCase().includes(q) || ep.handler?.toLowerCase().includes(q) ||
|
|
283
|
-
ep.service_calls?.some((s) => s.toLowerCase().includes(q))).slice(0, 8).map((ep) => `${ep.method} ${ep.path} [${ep.module}]`);
|
|
284
|
-
// Models: match name or fields
|
|
285
|
-
const models = Object.values(d.model_registry || {}).filter((m) => m.name?.toLowerCase().includes(q) || m.fields?.some((f) => f.toLowerCase().includes(q))).slice(0, 8).map((m) => `${m.name}:${m.fields?.length}f`);
|
|
286
|
-
// Modules: match id, imports, or exports
|
|
287
|
-
const mods = (d.service_map || []).filter((m) => m.id?.toLowerCase().includes(q) ||
|
|
288
|
-
m.imports?.some((i) => i.toLowerCase().includes(q))).slice(0, 5).map((m) => `${m.id}:${m.file_count}files,${m.endpoint_count}ep [${m.layer}]`);
|
|
289
|
-
// Exports: match exported symbol names across all modules
|
|
290
|
-
const exports = [];
|
|
291
|
-
for (const m of d.service_map || []) {
|
|
292
|
-
for (const sym of m.exports || []) {
|
|
293
|
-
if (sym.toLowerCase().includes(q)) {
|
|
294
|
-
exports.push(`${sym} [${m.id}]`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// Files: match file paths across all modules
|
|
299
|
-
const files = [];
|
|
300
|
-
for (const m of d.service_map || []) {
|
|
301
|
-
for (const f of m.files || []) {
|
|
302
|
-
if (f.toLowerCase().includes(q)) {
|
|
303
|
-
files.push(`${f} [${m.id}]`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// Enums: match name or values
|
|
308
|
-
const enums = Object.values(d.enum_registry || {}).filter((e) => e.name?.toLowerCase().includes(q) || e.values?.some((v) => v.toLowerCase().includes(q))).slice(0, 5).map((e) => `${e.name}:${e.values?.length}vals [${e.file}]`);
|
|
309
|
-
// Background tasks: match name or kind
|
|
310
|
-
const tasks = (d.background_tasks || []).filter((t) => t.name?.toLowerCase().includes(q) || t.kind?.toLowerCase().includes(q)).slice(0, 5).map((t) => `${t.name} [${t.kind}] ${t.file}`);
|
|
311
|
-
// Frontend pages: match path or component
|
|
312
|
-
const pages = (d.frontend_pages || []).filter((p) => p.path?.toLowerCase().includes(q) || p.component?.toLowerCase().includes(q) ||
|
|
313
|
-
p.api_calls?.some((c) => c.toLowerCase().includes(q))).slice(0, 5).map((p) => `${p.path} → ${p.component}`);
|
|
314
|
-
// Functions: search literal_index (tactic:simp, sorry, string patterns) + function names
|
|
315
|
-
const fnHits = [];
|
|
316
|
-
const fi = await loadFuncIntel();
|
|
317
|
-
if (fi) {
|
|
318
|
-
// Literal index: exact key match + partial key match
|
|
319
|
-
const litIndex = fi.literal_index ?? {};
|
|
320
|
-
for (const [key, hits] of Object.entries(litIndex)) {
|
|
321
|
-
if (key.includes(q)) {
|
|
322
|
-
for (const h of hits.slice(0, 3)) {
|
|
323
|
-
fnHits.push(`${h.function} [${h.file}:${h.line}] (lit:${key})`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
if (fnHits.length >= 10)
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
// Function names: match by name
|
|
330
|
-
if (fnHits.length < 10) {
|
|
331
|
-
for (const fn of (fi.functions ?? [])) {
|
|
332
|
-
if (fn.name?.toLowerCase().includes(q)) {
|
|
333
|
-
fnHits.push(`${fn.name} [${fn.file}:${fn.lines?.[0]}]`);
|
|
334
|
-
if (fnHits.length >= 10)
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return compact({
|
|
341
|
-
ep: eps, mod: models, m: mods,
|
|
342
|
-
exports: exports.slice(0, 10),
|
|
343
|
-
files: files.slice(0, 8),
|
|
344
|
-
enums, tasks, pages,
|
|
345
|
-
...(fnHits.length > 0 ? { fns: fnHits } : {}),
|
|
346
|
-
});
|
|
109
|
+
return runCli(["search", "--query", args.query, "--format", "json", "--backend", "auto", "--input", specsInputDir]);
|
|
347
110
|
}
|
|
348
111
|
async function model(args) {
|
|
349
|
-
|
|
350
|
-
const m = d.model_registry?.[args.name];
|
|
351
|
-
if (!m)
|
|
352
|
-
return compact({ err: "not found" });
|
|
353
|
-
const usedBy = Object.values(d.api_registry || {}).filter((ep) => ep.request_schema === args.name || ep.response_schema === args.name).map((ep) => `${ep.method} ${ep.path}`);
|
|
354
|
-
return compact({
|
|
355
|
-
name: m.name, fw: m.framework, f: m.file,
|
|
356
|
-
fields: m.fields, rels: m.relationships,
|
|
357
|
-
usedBy,
|
|
358
|
-
});
|
|
112
|
+
return runCli(["search", "--model", args.name, "--input", specsInputDir]);
|
|
359
113
|
}
|
|
360
114
|
// ── MCP protocol ──
|
|
361
115
|
const TOOLS = [
|
|
@@ -420,7 +174,7 @@ const TOOL_HANDLERS = {
|
|
|
420
174
|
guardian_impact: impact,
|
|
421
175
|
guardian_search: search,
|
|
422
176
|
guardian_model: model,
|
|
423
|
-
guardian_metrics: async () =>
|
|
177
|
+
guardian_metrics: async () => JSON.stringify(metrics.summary()),
|
|
424
178
|
};
|
|
425
179
|
function respond(id, result) {
|
|
426
180
|
const msg = JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
@@ -481,6 +235,8 @@ async function handleRequest(req) {
|
|
|
481
235
|
const result = await handler(toolArgs);
|
|
482
236
|
setCache(cacheKey, result);
|
|
483
237
|
metrics.record(toolName, toolArgs, result, false);
|
|
238
|
+
// Write session flag so the PreToolUse hook knows guardian was called recently
|
|
239
|
+
fs.writeFile(SESSION_FLAG, Date.now().toString(), "utf8").catch(() => { });
|
|
484
240
|
respond(req.id, {
|
|
485
241
|
content: [{ type: "text", text: result }],
|
|
486
242
|
});
|
|
@@ -504,30 +260,34 @@ async function handleRequest(req) {
|
|
|
504
260
|
export async function runMcpServe(options) {
|
|
505
261
|
const specsDir = path.resolve(options.specs);
|
|
506
262
|
const quiet = options.quiet ?? false;
|
|
507
|
-
|
|
508
|
-
funcIntelPath = path.join(specsDir, "machine", "function-intelligence.json");
|
|
509
|
-
// Pre-load intelligence
|
|
510
|
-
await loadIntel();
|
|
511
|
-
await loadFuncIntel();
|
|
263
|
+
specsInputDir = specsDir;
|
|
512
264
|
// Log to stderr (stdout is for MCP protocol)
|
|
513
265
|
if (!quiet) {
|
|
514
|
-
process.stderr.write(`Guardian MCP server started.
|
|
266
|
+
process.stderr.write(`Guardian MCP server started. Specs: ${specsDir}\n`);
|
|
515
267
|
process.stderr.write(`Tools: ${TOOLS.map((t) => t.name).join(", ")}\n`);
|
|
516
268
|
}
|
|
517
269
|
// Read JSON-RPC messages from stdin, line by line
|
|
518
270
|
const rl = readline.createInterface({ input: process.stdin });
|
|
519
|
-
|
|
271
|
+
// Track in-flight async handlers so we can drain before exit.
|
|
272
|
+
// Previously all handlers were instant (in-process); now they spawn subprocesses.
|
|
273
|
+
const pending = [];
|
|
274
|
+
rl.on("line", (line) => {
|
|
520
275
|
if (!line.trim())
|
|
521
276
|
return;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
277
|
+
const p = (async () => {
|
|
278
|
+
try {
|
|
279
|
+
const req = JSON.parse(line);
|
|
280
|
+
await handleRequest(req);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
respondError(null, -32700, `Parse error: ${err.message}`);
|
|
284
|
+
}
|
|
285
|
+
})();
|
|
286
|
+
pending.push(p);
|
|
529
287
|
});
|
|
530
288
|
rl.on("close", async () => {
|
|
289
|
+
// Drain all in-flight handlers before persisting metrics and exiting.
|
|
290
|
+
await Promise.allSettled(pending);
|
|
531
291
|
// Persist session metrics to .specs/machine/mcp-metrics.jsonl
|
|
532
292
|
const metricsPath = path.join(specsDir, "machine", "mcp-metrics.jsonl");
|
|
533
293
|
try {
|