@toolbaux/guardian 0.1.13 → 0.1.15
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/dist/commands/init.js +84 -2
- package/dist/commands/mcp-serve.js +56 -6
- package/dist/extract/codebase-intel.js +15 -10
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 2. .specs/ directory
|
|
7
7
|
* 3. Pre-commit hook that auto-runs extract + context injection
|
|
8
8
|
* 4. Injects guardian context block into CLAUDE.md
|
|
9
|
-
* 5.
|
|
9
|
+
* 5. Claude Code hooks (.claude/settings.json + MCP-first enforcement)
|
|
10
|
+
* 6. Adds .specs/ to .gitignore exclusion (tracked by default)
|
|
10
11
|
*
|
|
11
12
|
* Safe to run multiple times — only creates what's missing.
|
|
12
13
|
*/
|
|
@@ -22,6 +23,26 @@ const DEFAULT_CONFIG = {
|
|
|
22
23
|
mode: "full",
|
|
23
24
|
},
|
|
24
25
|
};
|
|
26
|
+
const CLAUDE_CODE_HOOK_SCRIPT = `#!/bin/bash
|
|
27
|
+
# Guardian MCP-first hook — ensures AI tools use Guardian MCP before reading source files.
|
|
28
|
+
# Installed by: guardian init
|
|
29
|
+
|
|
30
|
+
INPUT=$(cat)
|
|
31
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
32
|
+
|
|
33
|
+
cat >&2 <<BLOCK
|
|
34
|
+
BLOCKED: Use Guardian MCP tools before reading source files.
|
|
35
|
+
|
|
36
|
+
Use these MCP tools first:
|
|
37
|
+
- guardian_orient — get codebase overview
|
|
38
|
+
- guardian_search — find features by keyword
|
|
39
|
+
- guardian_context — deep dive into a specific area
|
|
40
|
+
|
|
41
|
+
Then you can read individual files as needed.
|
|
42
|
+
BLOCK
|
|
43
|
+
|
|
44
|
+
exit 2
|
|
45
|
+
`;
|
|
25
46
|
const HOOK_SCRIPT = `#!/bin/sh
|
|
26
47
|
# guardian pre-commit hook — keeps architecture context fresh
|
|
27
48
|
# Installed by: guardian init
|
|
@@ -157,7 +178,9 @@ export async function runInit(options) {
|
|
|
157
178
|
await fs.writeFile(claudeMdPath, content, "utf8");
|
|
158
179
|
console.log(" ✓ Created CLAUDE.md with guardian context block");
|
|
159
180
|
}
|
|
160
|
-
// 5.
|
|
181
|
+
// 5. Set up Claude Code hooks (.claude/settings.json + hook script)
|
|
182
|
+
await setupClaudeCodeHooks(root, specsDir);
|
|
183
|
+
// 6. Run initial extract + context injection
|
|
161
184
|
console.log("\n Running initial extraction...");
|
|
162
185
|
try {
|
|
163
186
|
const { runExtract } = await import("./extract.js");
|
|
@@ -208,3 +231,62 @@ async function dirExists(p) {
|
|
|
208
231
|
return false;
|
|
209
232
|
}
|
|
210
233
|
}
|
|
234
|
+
async function setupClaudeCodeHooks(root, specsDir) {
|
|
235
|
+
const claudeDir = path.join(root, ".claude");
|
|
236
|
+
const hooksDir = path.join(claudeDir, "hooks");
|
|
237
|
+
const settingsPath = path.join(claudeDir, "settings.json");
|
|
238
|
+
const hookScriptPath = path.join(hooksDir, "mcp-first.sh");
|
|
239
|
+
await fs.mkdir(hooksDir, { recursive: true });
|
|
240
|
+
// Write the hook script
|
|
241
|
+
if (!(await fileExists(hookScriptPath))) {
|
|
242
|
+
await fs.writeFile(hookScriptPath, CLAUDE_CODE_HOOK_SCRIPT, "utf8");
|
|
243
|
+
await fs.chmod(hookScriptPath, 0o755);
|
|
244
|
+
console.log(" ✓ Created Claude Code MCP-first hook (.claude/hooks/mcp-first.sh)");
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log(" · Claude Code hook already exists");
|
|
248
|
+
}
|
|
249
|
+
// Write or merge .claude/settings.json
|
|
250
|
+
let settings = {};
|
|
251
|
+
if (await fileExists(settingsPath)) {
|
|
252
|
+
try {
|
|
253
|
+
settings = JSON.parse(await fs.readFile(settingsPath, "utf8"));
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Corrupted file — overwrite
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Add MCP server config
|
|
260
|
+
if (!settings.mcpServers)
|
|
261
|
+
settings.mcpServers = {};
|
|
262
|
+
const mcpServers = settings.mcpServers;
|
|
263
|
+
if (!mcpServers.guardian) {
|
|
264
|
+
mcpServers.guardian = {
|
|
265
|
+
command: "guardian",
|
|
266
|
+
args: ["mcp-serve", "--specs", specsDir],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// Add PreToolUse hook
|
|
270
|
+
const hookEntry = {
|
|
271
|
+
matcher: "Read|Glob|Grep",
|
|
272
|
+
hooks: [
|
|
273
|
+
{
|
|
274
|
+
type: "command",
|
|
275
|
+
if: "Read(//*/src/*)|Glob(*src*)|Grep(*src*)",
|
|
276
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/mcp-first.sh',
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
if (!settings.hooks)
|
|
281
|
+
settings.hooks = {};
|
|
282
|
+
const hooks = settings.hooks;
|
|
283
|
+
if (!hooks.PreToolUse) {
|
|
284
|
+
hooks.PreToolUse = [hookEntry];
|
|
285
|
+
console.log(" ✓ Configured Claude Code PreToolUse hook in .claude/settings.json");
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
console.log(" · Claude Code PreToolUse hook already configured");
|
|
289
|
+
}
|
|
290
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
291
|
+
console.log(" ✓ Updated .claude/settings.json (MCP server + hooks)");
|
|
292
|
+
}
|
|
@@ -258,11 +258,45 @@ async function impact(args) {
|
|
|
258
258
|
async function search(args) {
|
|
259
259
|
const d = await loadIntel();
|
|
260
260
|
const q = args.query.toLowerCase();
|
|
261
|
+
// Endpoints: match path, handler, or service calls
|
|
261
262
|
const eps = Object.values(d.api_registry || {}).filter((ep) => ep.path?.toLowerCase().includes(q) || ep.handler?.toLowerCase().includes(q) ||
|
|
262
263
|
ep.service_calls?.some((s) => s.toLowerCase().includes(q))).slice(0, 8).map((ep) => `${ep.method} ${ep.path} [${ep.module}]`);
|
|
264
|
+
// Models: match name or fields
|
|
263
265
|
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`);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
+
// Modules: match id, imports, or exports
|
|
267
|
+
const mods = (d.service_map || []).filter((m) => m.id?.toLowerCase().includes(q) ||
|
|
268
|
+
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}]`);
|
|
269
|
+
// Exports: match exported symbol names across all modules
|
|
270
|
+
const exports = [];
|
|
271
|
+
for (const m of d.service_map || []) {
|
|
272
|
+
for (const sym of m.exports || []) {
|
|
273
|
+
if (sym.toLowerCase().includes(q)) {
|
|
274
|
+
exports.push(`${sym} [${m.id}]`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Files: match file paths across all modules
|
|
279
|
+
const files = [];
|
|
280
|
+
for (const m of d.service_map || []) {
|
|
281
|
+
for (const f of m.files || []) {
|
|
282
|
+
if (f.toLowerCase().includes(q)) {
|
|
283
|
+
files.push(`${f} [${m.id}]`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Enums: match name or values
|
|
288
|
+
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}]`);
|
|
289
|
+
// Background tasks: match name or kind
|
|
290
|
+
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}`);
|
|
291
|
+
// Frontend pages: match path or component
|
|
292
|
+
const pages = (d.frontend_pages || []).filter((p) => p.path?.toLowerCase().includes(q) || p.component?.toLowerCase().includes(q) ||
|
|
293
|
+
p.api_calls?.some((c) => c.toLowerCase().includes(q))).slice(0, 5).map((p) => `${p.path} → ${p.component}`);
|
|
294
|
+
return compact({
|
|
295
|
+
ep: eps, mod: models, m: mods,
|
|
296
|
+
exports: exports.slice(0, 10),
|
|
297
|
+
files: files.slice(0, 8),
|
|
298
|
+
enums, tasks, pages,
|
|
299
|
+
});
|
|
266
300
|
}
|
|
267
301
|
async function model(args) {
|
|
268
302
|
const d = await loadIntel();
|
|
@@ -307,7 +341,7 @@ const TOOLS = [
|
|
|
307
341
|
},
|
|
308
342
|
{
|
|
309
343
|
name: "guardian_search",
|
|
310
|
-
description: "Find endpoints, models, modules by keyword. Returns compact one-line results.",
|
|
344
|
+
description: "Find endpoints, models, modules, exported symbols, files, enums, tasks, and pages by keyword. Returns compact one-line results.",
|
|
311
345
|
inputSchema: {
|
|
312
346
|
type: "object",
|
|
313
347
|
properties: {
|
|
@@ -354,8 +388,12 @@ async function handleRequest(req) {
|
|
|
354
388
|
case "initialize":
|
|
355
389
|
respond(req.id, {
|
|
356
390
|
protocolVersion: "2024-11-05",
|
|
357
|
-
capabilities: {
|
|
358
|
-
|
|
391
|
+
capabilities: {
|
|
392
|
+
tools: {},
|
|
393
|
+
resources: {},
|
|
394
|
+
prompts: {},
|
|
395
|
+
},
|
|
396
|
+
serverInfo: { name: "guardian", version: "0.1.13" },
|
|
359
397
|
});
|
|
360
398
|
break;
|
|
361
399
|
case "initialized":
|
|
@@ -364,6 +402,15 @@ async function handleRequest(req) {
|
|
|
364
402
|
case "tools/list":
|
|
365
403
|
respond(req.id, { tools: TOOLS });
|
|
366
404
|
break;
|
|
405
|
+
case "resources/list":
|
|
406
|
+
respond(req.id, { resources: [] });
|
|
407
|
+
break;
|
|
408
|
+
case "resources/templates/list":
|
|
409
|
+
respond(req.id, { resourceTemplates: [] });
|
|
410
|
+
break;
|
|
411
|
+
case "prompts/list":
|
|
412
|
+
respond(req.id, { prompts: [] });
|
|
413
|
+
break;
|
|
367
414
|
case "tools/call": {
|
|
368
415
|
const toolName = req.params?.name;
|
|
369
416
|
const toolArgs = req.params?.arguments || {};
|
|
@@ -400,7 +447,10 @@ async function handleRequest(req) {
|
|
|
400
447
|
break;
|
|
401
448
|
}
|
|
402
449
|
default:
|
|
403
|
-
|
|
450
|
+
// Notifications (no id) don't need a response
|
|
451
|
+
if (req.id != null) {
|
|
452
|
+
respondError(req.id, -32601, `Method not found: ${req.method}`);
|
|
453
|
+
}
|
|
404
454
|
}
|
|
405
455
|
}
|
|
406
456
|
// ── Entry point ──
|
|
@@ -68,16 +68,21 @@ export function buildCodebaseIntelligence(architecture, ux) {
|
|
|
68
68
|
values: en.values,
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
// service_map
|
|
72
|
-
const serviceMap = architecture.modules.map((m) =>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
// service_map (with exports and files for search)
|
|
72
|
+
const serviceMap = architecture.modules.map((m) => {
|
|
73
|
+
const allExports = (m.exports || []).flatMap((e) => (e.exports || []).map((x) => x.name));
|
|
74
|
+
return {
|
|
75
|
+
id: m.id,
|
|
76
|
+
path: m.path,
|
|
77
|
+
type: m.type,
|
|
78
|
+
layer: m.layer,
|
|
79
|
+
file_count: m.files.length,
|
|
80
|
+
endpoint_count: m.endpoints.length,
|
|
81
|
+
imports: m.imports,
|
|
82
|
+
exports: [...new Set(allExports)],
|
|
83
|
+
files: m.files,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
81
86
|
// frontend_pages
|
|
82
87
|
const frontendPages = ux.pages.map((p) => ({
|
|
83
88
|
path: p.path,
|
package/package.json
CHANGED