@toolbaux/guardian 0.1.14 → 0.1.16

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/cli.js CHANGED
@@ -343,9 +343,11 @@ program
343
343
  .command("mcp-serve")
344
344
  .description("Start Guardian MCP server for Claude Code / Cursor integration")
345
345
  .option("--specs <dir>", "Specs directory", ".specs")
346
+ .option("--quiet", "Suppress stderr output (for clients that merge streams)", false)
346
347
  .action(async (options) => {
347
348
  await runMcpServe({
348
349
  specs: options.specs,
350
+ quiet: options.quiet,
349
351
  });
350
352
  });
351
353
  program.parseAsync().catch((error) => {
@@ -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. Adds .specs/ to .gitignore exclusion (tracked by default)
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. Run initial extract + context injection
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
- const mods = (d.service_map || []).filter((m) => m.id?.toLowerCase().includes(q)).slice(0, 5).map((m) => `${m.id}:${m.endpoint_count}ep`);
265
- return compact({ ep: eps, mod: models, m: mods });
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: {
@@ -422,12 +456,15 @@ async function handleRequest(req) {
422
456
  // ── Entry point ──
423
457
  export async function runMcpServe(options) {
424
458
  const specsDir = path.resolve(options.specs);
459
+ const quiet = options.quiet ?? false;
425
460
  intelPath = path.join(specsDir, "machine", "codebase-intelligence.json");
426
461
  // Pre-load intelligence
427
462
  await loadIntel();
428
463
  // Log to stderr (stdout is for MCP protocol)
429
- process.stderr.write(`Guardian MCP server started. Intelligence: ${intelPath}\n`);
430
- process.stderr.write(`Tools: ${TOOLS.map((t) => t.name).join(", ")}\n`);
464
+ if (!quiet) {
465
+ process.stderr.write(`Guardian MCP server started. Intelligence: ${intelPath}\n`);
466
+ process.stderr.write(`Tools: ${TOOLS.map((t) => t.name).join(", ")}\n`);
467
+ }
431
468
  // Read JSON-RPC messages from stdin, line by line
432
469
  const rl = readline.createInterface({ input: process.stdin });
433
470
  rl.on("line", async (line) => {
@@ -450,10 +487,12 @@ export async function runMcpServe(options) {
450
487
  session_end: new Date().toISOString(),
451
488
  });
452
489
  await fs.appendFile(metricsPath, entry + "\n", "utf8");
453
- process.stderr.write(`Guardian metrics saved to ${metricsPath}\n`);
490
+ if (!quiet)
491
+ process.stderr.write(`Guardian metrics saved to ${metricsPath}\n`);
454
492
  }
455
493
  catch { }
456
- process.stderr.write("Guardian MCP server stopped.\n");
494
+ if (!quiet)
495
+ process.stderr.write("Guardian MCP server stopped.\n");
457
496
  process.exit(0);
458
497
  });
459
498
  }
@@ -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
- id: m.id,
74
- path: m.path,
75
- type: m.type,
76
- layer: m.layer,
77
- file_count: m.files.length,
78
- endpoint_count: m.endpoints.length,
79
- imports: m.imports,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolbaux/guardian",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "description": "Architectural intelligence for codebases. Verify that AI-generated code matches your architectural intent.",
6
6
  "keywords": [