@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.
@@ -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: {
@@ -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: { tools: {} },
358
- serverInfo: { name: "guardian", version: "0.1.0" },
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
- respondError(req.id, -32601, `Method not found: ${req.method}`);
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
- 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.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "description": "Architectural intelligence for codebases. Verify that AI-generated code matches your architectural intent.",
6
6
  "keywords": [