@toolbaux/guardian 0.1.22 → 0.2.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.
- package/README.md +6 -4
- 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/context.js +87 -29
- 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 +4 -1
- package/dist/commands/feature-context.js +1 -1
- package/dist/commands/generate.js +83 -10
- package/dist/commands/init.js +89 -56
- package/dist/commands/intel.js +70 -1
- package/dist/commands/mcp-serve.js +155 -316
- package/dist/commands/search.js +642 -14
- package/dist/config.js +1 -0
- package/dist/db/embeddings.js +113 -0
- package/dist/db/file-specs-store.js +174 -0
- package/dist/db/fts-builder.js +390 -0
- package/dist/db/index.js +55 -0
- package/dist/db/specs-store.js +13 -0
- package/dist/db/sqlite-specs-store.js +934 -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 +4 -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,319 +93,100 @@ 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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
109
|
+
return runCli(["search", "--query", args.query, "--format", "json", "--backend", "auto", "--input", specsInputDir]);
|
|
110
|
+
}
|
|
111
|
+
async function model(args) {
|
|
112
|
+
return runCli(["search", "--model", args.name, "--input", specsInputDir]);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* guardian_grep — semantic grep via guardian search.
|
|
116
|
+
*
|
|
117
|
+
* Replaces raw Grep tool calls. Runs guardian BM25+vector search and returns
|
|
118
|
+
* matching symbols (file:line:name) and files, formatted like grep output.
|
|
119
|
+
* Claude gets richer context (call-graph, authority) with zero token overhead.
|
|
120
|
+
*/
|
|
121
|
+
async function grep(args) {
|
|
122
|
+
const raw = await runCli([
|
|
123
|
+
"search", "--query", args.query, "--format", "json", "--backend", "auto", "--input", specsInputDir,
|
|
124
|
+
]);
|
|
125
|
+
try {
|
|
126
|
+
const data = JSON.parse(raw);
|
|
127
|
+
const lines = [`guardian_grep("${args.query}")`];
|
|
128
|
+
if (data.symbols?.length) {
|
|
129
|
+
lines.push("\nSymbols (file:line: name):");
|
|
130
|
+
for (const s of data.symbols.slice(0, 25)) {
|
|
131
|
+
lines.push(` ${s.file}:${s.line}: ${s.name}`);
|
|
295
132
|
}
|
|
296
133
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
for (const f of m.files || []) {
|
|
302
|
-
if (f.toLowerCase().includes(q)) {
|
|
303
|
-
files.push(`${f} [${m.id}]`);
|
|
134
|
+
if (data.files?.length) {
|
|
135
|
+
lines.push("\nFiles:");
|
|
136
|
+
for (const f of data.files.slice(0, 15)) {
|
|
137
|
+
lines.push(` ${f.file_path}`);
|
|
304
138
|
}
|
|
305
139
|
}
|
|
140
|
+
if (lines.length === 1)
|
|
141
|
+
lines.push(" (no matches — try a different query)");
|
|
142
|
+
return lines.join("\n");
|
|
306
143
|
}
|
|
307
|
-
|
|
308
|
-
|
|
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: ranked search across names, literals, calls — capped at 10 to prevent flooding
|
|
315
|
-
const fnHits = [];
|
|
316
|
-
const fi = await loadFuncIntel();
|
|
317
|
-
if (fi) {
|
|
318
|
-
// Build a field map: file → field names (augments fn hits with model fields)
|
|
319
|
-
const fileToFields = new Map();
|
|
320
|
-
for (const m of Object.values(d.model_registry || {})) {
|
|
321
|
-
if (!m.file)
|
|
322
|
-
continue;
|
|
323
|
-
const existing = fileToFields.get(m.file) ?? [];
|
|
324
|
-
fileToFields.set(m.file, [...existing, ...(m.fields ?? [])]);
|
|
325
|
-
}
|
|
326
|
-
const scored = [];
|
|
327
|
-
const seen = new Set();
|
|
328
|
-
for (const fn of (fi.functions ?? [])) {
|
|
329
|
-
const nameNorm = (fn.name ?? "").toLowerCase();
|
|
330
|
-
const fileNorm = (fn.file ?? "").toLowerCase();
|
|
331
|
-
const callsNorm = (fn.calls ?? []).map((c) => c.toLowerCase());
|
|
332
|
-
const litsNorm = [...(fn.stringLiterals ?? []), ...(fn.regexPatterns ?? [])].map((l) => l.toLowerCase());
|
|
333
|
-
let score = 0;
|
|
334
|
-
if (nameNorm === q)
|
|
335
|
-
score = 1.0;
|
|
336
|
-
else if (nameNorm.includes(q))
|
|
337
|
-
score = 0.7;
|
|
338
|
-
else if (callsNorm.some((c) => c.includes(q)))
|
|
339
|
-
score = 0.5;
|
|
340
|
-
else if (litsNorm.some((l) => l.includes(q)))
|
|
341
|
-
score = 0.3;
|
|
342
|
-
else if (fileNorm.includes(q))
|
|
343
|
-
score = 0.2;
|
|
344
|
-
if (score > 0) {
|
|
345
|
-
scored.push({ fn, score });
|
|
346
|
-
seen.add(`${fn.file}:${fn.name}`);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
// Also surface literal_index hits not already captured
|
|
350
|
-
const litIndex = fi.literal_index ?? {};
|
|
351
|
-
for (const [key, hits] of Object.entries(litIndex)) {
|
|
352
|
-
if (!key.includes(q))
|
|
353
|
-
continue;
|
|
354
|
-
for (const h of hits) {
|
|
355
|
-
const uid = `${h.file}:${h.function}`;
|
|
356
|
-
if (seen.has(uid))
|
|
357
|
-
continue;
|
|
358
|
-
seen.add(uid);
|
|
359
|
-
const fn = fi.functions.find((f) => f.file === h.file && f.name === h.function);
|
|
360
|
-
scored.push({ fn: fn ?? { name: h.function, file: h.file, lines: [h.line, h.line], calls: [], stringLiterals: [], regexPatterns: [] }, score: 0.25 });
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
// Sort by score desc, take top 10
|
|
364
|
-
scored.sort((a, b) => b.score - a.score);
|
|
365
|
-
const projectRoot = process.cwd();
|
|
366
|
-
for (const { fn } of scored.slice(0, 10)) {
|
|
367
|
-
const relFile = fn.file?.startsWith("/") ? require("path").relative(projectRoot, fn.file) : fn.file;
|
|
368
|
-
const fields = fileToFields.get(fn.file) ?? [];
|
|
369
|
-
const fieldSuffix = fields.length > 0 ? ` fields:${fields.slice(0, 5).join(",")}` : "";
|
|
370
|
-
fnHits.push(`${fn.name} [${relFile}:${fn.lines?.[0]}]${fieldSuffix}`);
|
|
371
|
-
}
|
|
144
|
+
catch {
|
|
145
|
+
return raw; // passthrough if search returns plain text
|
|
372
146
|
}
|
|
373
|
-
return compact({
|
|
374
|
-
ep: eps, mod: models, m: mods,
|
|
375
|
-
exports: exports.slice(0, 10),
|
|
376
|
-
files: files.slice(0, 8),
|
|
377
|
-
enums, tasks, pages,
|
|
378
|
-
...(fnHits.length > 0 ? { fns: fnHits } : {}),
|
|
379
|
-
});
|
|
380
147
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
148
|
+
/**
|
|
149
|
+
* guardian_glob — semantic file discovery via guardian search.
|
|
150
|
+
*
|
|
151
|
+
* Replaces raw Glob tool calls. Extracts meaningful keywords from the glob
|
|
152
|
+
* pattern and searches the guardian index for matching files. Falls back to
|
|
153
|
+
* guiding the user toward a more descriptive query for pure extension patterns.
|
|
154
|
+
*/
|
|
155
|
+
async function glob(args) {
|
|
156
|
+
// Extract keywords: "src/auth/**/*.ts" → "auth", "src/middleware/error*" → "middleware error"
|
|
157
|
+
const keywords = args.pattern
|
|
158
|
+
.replace(/\*\*?/g, " ")
|
|
159
|
+
.replace(/\.\w+$/, "") // strip trailing extension
|
|
160
|
+
.replace(/[[\]{}]/g, " ")
|
|
161
|
+
.split(/[/\s]+/)
|
|
162
|
+
.filter(s => s.length > 2 && !/^(src|lib|dist|app|index)$/.test(s))
|
|
163
|
+
.join(" ")
|
|
164
|
+
.trim();
|
|
165
|
+
if (!keywords) {
|
|
166
|
+
return [
|
|
167
|
+
`guardian_glob("${args.pattern}"): pattern has no meaningful keywords.`,
|
|
168
|
+
`Use guardian_search with a descriptive query instead, e.g.:`,
|
|
169
|
+
` guardian_search("TypeScript source files") — or describe what you're looking for.`,
|
|
170
|
+
].join("\n");
|
|
171
|
+
}
|
|
172
|
+
const raw = await runCli([
|
|
173
|
+
"search", "--query", keywords, "--format", "json", "--backend", "auto", "--input", specsInputDir,
|
|
174
|
+
]);
|
|
175
|
+
try {
|
|
176
|
+
const data = JSON.parse(raw);
|
|
177
|
+
const files = data.files ?? [];
|
|
178
|
+
const lines = [
|
|
179
|
+
`guardian_glob("${args.pattern}") — searched: "${keywords}"`,
|
|
180
|
+
`\nMatching files:`,
|
|
181
|
+
...files.slice(0, 20).map(f => ` ${f.file_path}`),
|
|
182
|
+
];
|
|
183
|
+
if (files.length === 0)
|
|
184
|
+
lines.push(" (no matches)");
|
|
185
|
+
return lines.join("\n");
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return raw;
|
|
189
|
+
}
|
|
392
190
|
}
|
|
393
191
|
// ── MCP protocol ──
|
|
394
192
|
const TOOLS = [
|
|
@@ -446,6 +244,39 @@ const TOOLS = [
|
|
|
446
244
|
description: "MCP usage stats for this session. Call at end to evaluate guardian's usefulness.",
|
|
447
245
|
inputSchema: { type: "object", properties: {} },
|
|
448
246
|
},
|
|
247
|
+
{
|
|
248
|
+
name: "guardian_grep",
|
|
249
|
+
description: [
|
|
250
|
+
"Semantic grep — find symbols and files matching a keyword or pattern.",
|
|
251
|
+
"Use INSTEAD of the Grep tool. Returns matching function/class names with file:line locations.",
|
|
252
|
+
"Backed by BM25 + call-graph authority so relevant source definitions surface first.",
|
|
253
|
+
"Example: guardian_grep('validate token') → auth.py:42: validate_token, middleware.py:18: check_jwt",
|
|
254
|
+
].join(" "),
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
query: { type: "string", description: "Keyword or phrase to search for (natural language OK)" },
|
|
259
|
+
path: { type: "string", description: "Optional: restrict to files under this path prefix" },
|
|
260
|
+
},
|
|
261
|
+
required: ["query"],
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "guardian_glob",
|
|
266
|
+
description: [
|
|
267
|
+
"Semantic file discovery — find files matching a path pattern.",
|
|
268
|
+
"Use INSTEAD of the Glob tool. Extracts keywords from the pattern and searches the guardian index.",
|
|
269
|
+
"Example: guardian_glob('src/auth/**/*.ts') → searches for 'auth typescript' files.",
|
|
270
|
+
"For pure extension globs with no path context, use guardian_search with a descriptive query.",
|
|
271
|
+
].join(" "),
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
pattern: { type: "string", description: "Glob pattern (e.g. 'src/auth/**/*.ts', '**/middleware*')" },
|
|
276
|
+
},
|
|
277
|
+
required: ["pattern"],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
449
280
|
];
|
|
450
281
|
const TOOL_HANDLERS = {
|
|
451
282
|
guardian_orient: orient,
|
|
@@ -453,7 +284,9 @@ const TOOL_HANDLERS = {
|
|
|
453
284
|
guardian_impact: impact,
|
|
454
285
|
guardian_search: search,
|
|
455
286
|
guardian_model: model,
|
|
456
|
-
guardian_metrics: async () =>
|
|
287
|
+
guardian_metrics: async () => JSON.stringify(metrics.summary()),
|
|
288
|
+
guardian_grep: grep,
|
|
289
|
+
guardian_glob: glob,
|
|
457
290
|
};
|
|
458
291
|
function respond(id, result) {
|
|
459
292
|
const msg = JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
@@ -514,6 +347,8 @@ async function handleRequest(req) {
|
|
|
514
347
|
const result = await handler(toolArgs);
|
|
515
348
|
setCache(cacheKey, result);
|
|
516
349
|
metrics.record(toolName, toolArgs, result, false);
|
|
350
|
+
// Write session flag so the PreToolUse hook knows guardian was called recently
|
|
351
|
+
fs.writeFile(SESSION_FLAG, Date.now().toString(), "utf8").catch(() => { });
|
|
517
352
|
respond(req.id, {
|
|
518
353
|
content: [{ type: "text", text: result }],
|
|
519
354
|
});
|
|
@@ -537,30 +372,34 @@ async function handleRequest(req) {
|
|
|
537
372
|
export async function runMcpServe(options) {
|
|
538
373
|
const specsDir = path.resolve(options.specs);
|
|
539
374
|
const quiet = options.quiet ?? false;
|
|
540
|
-
|
|
541
|
-
funcIntelPath = path.join(specsDir, "machine", "function-intelligence.json");
|
|
542
|
-
// Pre-load intelligence
|
|
543
|
-
await loadIntel();
|
|
544
|
-
await loadFuncIntel();
|
|
375
|
+
specsInputDir = specsDir;
|
|
545
376
|
// Log to stderr (stdout is for MCP protocol)
|
|
546
377
|
if (!quiet) {
|
|
547
|
-
process.stderr.write(`Guardian MCP server started.
|
|
378
|
+
process.stderr.write(`Guardian MCP server started. Specs: ${specsDir}\n`);
|
|
548
379
|
process.stderr.write(`Tools: ${TOOLS.map((t) => t.name).join(", ")}\n`);
|
|
549
380
|
}
|
|
550
381
|
// Read JSON-RPC messages from stdin, line by line
|
|
551
382
|
const rl = readline.createInterface({ input: process.stdin });
|
|
552
|
-
|
|
383
|
+
// Track in-flight async handlers so we can drain before exit.
|
|
384
|
+
// Previously all handlers were instant (in-process); now they spawn subprocesses.
|
|
385
|
+
const pending = [];
|
|
386
|
+
rl.on("line", (line) => {
|
|
553
387
|
if (!line.trim())
|
|
554
388
|
return;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
389
|
+
const p = (async () => {
|
|
390
|
+
try {
|
|
391
|
+
const req = JSON.parse(line);
|
|
392
|
+
await handleRequest(req);
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
respondError(null, -32700, `Parse error: ${err.message}`);
|
|
396
|
+
}
|
|
397
|
+
})();
|
|
398
|
+
pending.push(p);
|
|
562
399
|
});
|
|
563
400
|
rl.on("close", async () => {
|
|
401
|
+
// Drain all in-flight handlers before persisting metrics and exiting.
|
|
402
|
+
await Promise.allSettled(pending);
|
|
564
403
|
// Persist session metrics to .specs/machine/mcp-metrics.jsonl
|
|
565
404
|
const metricsPath = path.join(specsDir, "machine", "mcp-metrics.jsonl");
|
|
566
405
|
try {
|