@toolbaux/guardian 0.1.23 → 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 CHANGED
@@ -85,16 +85,18 @@ The block between markers is replaced on every save (VSCode extension) and every
85
85
 
86
86
  Guardian includes an MCP server that Claude Code and Cursor connect to automatically. The VSCode extension sets this up on first activation — no manual config needed.
87
87
 
88
- **6 compact tools available to AI:**
88
+ **8 compact tools available to AI:**
89
89
 
90
90
  | Tool | Tokens | Purpose |
91
91
  |------|--------|---------|
92
92
  | `guardian_orient` | ~100 | Project summary at session start |
93
93
  | `guardian_context` | ~50-80 | File or endpoint dependencies before editing |
94
94
  | `guardian_impact` | ~30 | What breaks if you change a file |
95
- | `guardian_search` | ~70 | Find endpoints, models, modules by keyword |
95
+ | `guardian_search` | ~70 | Find endpoints, models, modules, and functions by keyword |
96
96
  | `guardian_model` | ~90 | Full field details (only when needed) |
97
97
  | `guardian_metrics` | ~50 | Session usage stats |
98
+ | `guardian_grep` | ~40 | Semantic grep — search symbols and literals across the codebase |
99
+ | `guardian_glob` | ~30 | Semantic file discovery — find files by pattern with module context |
98
100
 
99
101
  All responses are compact JSON — no pretty-printing, no verbose keys. Repeated calls are cached (30s TTL). Usage metrics tracked per session.
100
102
 
@@ -229,8 +231,8 @@ npm install && npm run build && npm link
229
231
 
230
232
  ```bash
231
233
  guardian init # config, .specs dir, pre-commit hook, CLAUDE.md
232
- guardian extract # full architecture + UX snapshots + docs
233
- guardian extract --backend sqlite # same + builds guardian.db with FTS index
234
+ guardian extract # full architecture + UX snapshots + guardian.db (default: sqlite)
235
+ guardian extract --backend file # file-only mode, skips guardian.db
234
236
  guardian generate --ai-context # compact ~3K token AI context only
235
237
  ```
236
238
 
package/dist/cli.js CHANGED
@@ -62,7 +62,7 @@ program
62
62
  .option("--no-file-graph", "Exclude file-level dependency graph")
63
63
  .option("--config <path>", "Path to guardian.config.json")
64
64
  .option("--docs-mode <mode>", "Docs mode (lean|full)")
65
- .option("--backend <backend>", "Storage backend: 'file' (default) or 'sqlite' (also builds guardian.db + FTS index)")
65
+ .option("--backend <backend>", "Storage backend: 'sqlite' (default, builds guardian.db + FTS index) or 'file'")
66
66
  .action(async (projectRoot, options) => {
67
67
  await runExtract({
68
68
  projectRoot,
@@ -5,21 +5,90 @@ import { loadArchitectureDiff, loadHeatmap } from "../extract/compress.js";
5
5
  import { renderContextBlock } from "../extract/context-block.js";
6
6
  import { resolveMachineInputDir } from "../output-layout.js";
7
7
  import { DEFAULT_SPECS_DIR } from "../config.js";
8
+ import { SqliteSpecsStore, DB_FILENAME } from "../db/sqlite-specs-store.js";
9
+ /** Open a SqliteSpecsStore if guardian.db exists, return null otherwise. */
10
+ async function tryOpenStore(specsDir) {
11
+ const dbPath = path.join(specsDir, DB_FILENAME);
12
+ try {
13
+ await fs.stat(dbPath);
14
+ const store = new SqliteSpecsStore(specsDir);
15
+ await store.init();
16
+ return store;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ /** Reconstruct the SI report shape renderContextBlock needs from module_metrics rows. */
23
+ function siFromMetrics(rows) {
24
+ return rows.map(r => ({
25
+ feature: r.module,
26
+ structure: { nodes: r.nodes, edges: r.edges },
27
+ metrics: { depth: 0, fanout_avg: 0, fanout_max: 0, density: 0, has_cycles: false },
28
+ scores: { depth_score: 0, fanout_score: 0, density_score: 0, cycle_score: 0, query_score: 0 },
29
+ confidence: { value: r.confidence, level: r.confidence_level },
30
+ ambiguity: { level: "LOW" },
31
+ classification: {
32
+ depth_level: r.depth_level,
33
+ propagation: r.propagation,
34
+ compressible: r.compressible,
35
+ },
36
+ recommendation: {
37
+ primary: { pattern: r.pattern, confidence: r.confidence },
38
+ fallback: { pattern: "", condition: "" },
39
+ avoid: [],
40
+ },
41
+ guardrails: { enforce_if_confidence_above: 0.7 },
42
+ override: { allowed: true, requires_reason: true },
43
+ }));
44
+ }
8
45
  export async function runContext(options) {
9
46
  const inputDir = await resolveMachineInputDir(options.input || DEFAULT_SPECS_DIR);
10
- const { architecture, ux } = await loadSnapshots(inputDir);
47
+ // inputDir resolves to .specs/machine/; DB lives one level up at .specs/guardian.db
48
+ const specsDir = path.dirname(inputDir);
49
+ const store = await tryOpenStore(specsDir);
50
+ let architecture;
51
+ let ux;
52
+ let si;
53
+ try {
54
+ // ── Load snapshots: DB first, file fallback ─────────────────────────────
55
+ if (store) {
56
+ const archEntry = await store.readSpec("architecture.snapshot");
57
+ const uxEntry = await store.readSpec("ux.snapshot");
58
+ if (archEntry && uxEntry) {
59
+ architecture = yaml.load(archEntry.content);
60
+ ux = yaml.load(uxEntry.content);
61
+ }
62
+ else {
63
+ ({ architecture, ux } = await loadSnapshotsFromFiles(inputDir));
64
+ }
65
+ }
66
+ else {
67
+ ({ architecture, ux } = await loadSnapshotsFromFiles(inputDir));
68
+ }
69
+ // ── Load SI reports: module_metrics table first, file fallback ──────────
70
+ if (store) {
71
+ const rows = store.readModuleMetrics();
72
+ if (rows.length > 0) {
73
+ si = siFromMetrics(rows);
74
+ }
75
+ }
76
+ if (!si) {
77
+ try {
78
+ const siRaw = await fs.readFile(path.join(inputDir, "structural-intelligence.json"), "utf8");
79
+ si = JSON.parse(siRaw);
80
+ }
81
+ catch { /* not available */ }
82
+ }
83
+ }
84
+ finally {
85
+ if (store)
86
+ await store.close();
87
+ }
11
88
  const [diff, heatmap] = await Promise.all([
12
89
  loadArchitectureDiff(inputDir),
13
90
  loadHeatmap(inputDir)
14
91
  ]);
15
- // Load structural intelligence if available
16
- let si;
17
- try {
18
- const siPath = path.join(inputDir, "structural-intelligence.json");
19
- const siRaw = await fs.readFile(siPath, "utf8");
20
- si = JSON.parse(siRaw);
21
- }
22
- catch { /* not available */ }
23
92
  const content = renderContextBlock(architecture, ux, {
24
93
  focusQuery: options.focus,
25
94
  maxLines: normalizeMaxLines(options.maxLines),
@@ -38,16 +107,18 @@ export async function runContext(options) {
38
107
  await fs.writeFile(outputPath, next, "utf8");
39
108
  console.log(`Wrote ${outputPath}`);
40
109
  }
41
- async function loadSnapshots(inputDir) {
110
+ async function loadSnapshotsFromFiles(inputDir) {
42
111
  const architecturePath = path.join(inputDir, "architecture.snapshot.yaml");
43
112
  const uxPath = path.join(inputDir, "ux.snapshot.yaml");
44
- let architectureRaw;
45
- let uxRaw;
46
113
  try {
47
- [architectureRaw, uxRaw] = await Promise.all([
114
+ const [architectureRaw, uxRaw] = await Promise.all([
48
115
  fs.readFile(architecturePath, "utf8"),
49
116
  fs.readFile(uxPath, "utf8")
50
117
  ]);
118
+ return {
119
+ architecture: yaml.load(architectureRaw),
120
+ ux: yaml.load(uxRaw)
121
+ };
51
122
  }
52
123
  catch (error) {
53
124
  if (error.code === "ENOENT") {
@@ -55,10 +126,6 @@ async function loadSnapshots(inputDir) {
55
126
  }
56
127
  throw error;
57
128
  }
58
- return {
59
- architecture: yaml.load(architectureRaw),
60
- ux: yaml.load(uxRaw)
61
- };
62
129
  }
63
130
  async function readIfExists(filePath) {
64
131
  try {
@@ -75,37 +142,28 @@ function stripExistingSpecGuardBlocks(content) {
75
142
  .replace(/<!-- guardian:auto-context -->[\s\S]*?<!-- \/guardian:auto-context -->/g, "<!-- guardian:auto-context -->\n<!-- /guardian:auto-context -->")
76
143
  .replace(/\n{3,}/g, "\n\n");
77
144
  }
78
- /**
79
- * Inject context into a file that has <!-- guardian:auto-context --> markers.
80
- * Replaces content between the markers instead of appending.
81
- */
82
145
  function injectIntoAutoContext(existing, contextBlock) {
83
146
  const marker = "<!-- guardian:auto-context -->";
84
147
  const endMarker = "<!-- /guardian:auto-context -->";
85
148
  if (!existing.includes(marker)) {
86
- // No auto-context markers — fall back to append behavior
87
149
  const cleaned = stripExistingSpecGuardBlocks(existing).trim();
88
150
  return cleaned.length > 0 ? `${cleaned}\n\n${contextBlock}\n` : `${contextBlock}\n`;
89
151
  }
90
- // Replace content between markers
91
152
  const startIdx = existing.indexOf(marker);
92
153
  const endIdx = existing.indexOf(endMarker);
93
- if (startIdx === -1 || endIdx === -1) {
154
+ if (startIdx === -1 || endIdx === -1)
94
155
  return existing;
95
- }
96
156
  const before = existing.slice(0, startIdx + marker.length);
97
157
  const after = existing.slice(endIdx);
98
158
  return `${before}\n${contextBlock}\n${after}`;
99
159
  }
100
160
  function normalizeMaxLines(value) {
101
- if (typeof value === "number" && Number.isFinite(value)) {
161
+ if (typeof value === "number" && Number.isFinite(value))
102
162
  return value;
103
- }
104
163
  if (typeof value === "string" && value.trim().length > 0) {
105
164
  const parsed = Number.parseInt(value, 10);
106
- if (Number.isFinite(parsed) && parsed > 0) {
165
+ if (Number.isFinite(parsed) && parsed > 0)
107
166
  return parsed;
108
- }
109
167
  }
110
168
  return undefined;
111
169
  }
@@ -5,13 +5,16 @@ import { runIntel } from "./intel.js";
5
5
  import { runGenerate } from "./generate.js";
6
6
  import { runContext } from "./context.js";
7
7
  export async function runExtract(options) {
8
+ // Default to sqlite so every extract builds guardian.db automatically.
9
+ // Pass --backend file to opt out (e.g. CI environments that don't need search).
10
+ const backend = options.backend ?? "sqlite";
8
11
  const { architecturePath, uxPath } = await extractProject(options);
9
12
  console.log(`Wrote ${architecturePath}`);
10
13
  console.log(`Wrote ${uxPath}`);
11
14
  // Auto-build codebase intelligence after every extract
12
15
  const specsDir = path.resolve(options.output);
13
16
  try {
14
- await runIntel({ specs: specsDir, backend: options.backend });
17
+ await runIntel({ specs: specsDir, backend });
15
18
  }
16
19
  catch {
17
20
  // Non-fatal — intel build failure should not break extract
@@ -1,26 +1,99 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import yaml from "js-yaml";
3
4
  import { buildSnapshots } from "../extract/index.js";
4
5
  import { renderContextBlock } from "../extract/context-block.js";
5
6
  import { getOutputLayout } from "../output-layout.js";
6
7
  import { DEFAULT_SPECS_DIR } from "../config.js";
7
8
  import { analyzeDepth } from "../extract/analyzers/depth.js";
9
+ import { SqliteSpecsStore, DB_FILENAME } from "../db/sqlite-specs-store.js";
8
10
  export async function runGenerate(options) {
9
11
  if (!options.aiContext) {
10
12
  throw new Error("`guardian generate` currently supports `--ai-context` only.");
11
13
  }
12
14
  const outputRoot = path.resolve(options.output ?? DEFAULT_SPECS_DIR);
13
15
  const layout = getOutputLayout(outputRoot);
14
- const { architecture, ux } = await buildSnapshots({
15
- projectRoot: options.projectRoot,
16
- backendRoot: options.backendRoot,
17
- frontendRoot: options.frontendRoot,
18
- output: outputRoot,
19
- includeFileGraph: true,
20
- configPath: options.configPath
21
- });
22
- // Load persisted Structural Intelligence reports emitted by `guardian extract`
23
- const siReports = await loadStructuralIntelligenceReports(layout.machineDir);
16
+ // ── Load snapshots: DB first, full re-extraction as fallback ──────────────
17
+ // When guardian.db exists (built by extract), load snapshots from the specs
18
+ // table instead of re-analysing the whole codebase. This is ~10× faster.
19
+ let architecture;
20
+ let ux;
21
+ let siReports;
22
+ const dbPath = path.join(outputRoot, DB_FILENAME);
23
+ let store = null;
24
+ try {
25
+ await fs.stat(dbPath);
26
+ store = new SqliteSpecsStore(outputRoot);
27
+ await store.init();
28
+ }
29
+ catch {
30
+ store = null;
31
+ }
32
+ try {
33
+ if (store) {
34
+ const archEntry = await store.readSpec("architecture.snapshot");
35
+ const uxEntry = await store.readSpec("ux.snapshot");
36
+ if (archEntry && uxEntry) {
37
+ console.log("[guardian] Loading snapshots from guardian.db");
38
+ architecture = yaml.load(archEntry.content);
39
+ ux = yaml.load(uxEntry.content);
40
+ }
41
+ else {
42
+ console.log("[guardian] Snapshots not in DB — extracting from codebase");
43
+ ({ architecture, ux } = await buildSnapshots({
44
+ projectRoot: options.projectRoot,
45
+ backendRoot: options.backendRoot,
46
+ frontendRoot: options.frontendRoot,
47
+ output: outputRoot,
48
+ includeFileGraph: true,
49
+ configPath: options.configPath
50
+ }));
51
+ }
52
+ // SI from module_metrics, fall back to file
53
+ const metricRows = store.readModuleMetrics();
54
+ if (metricRows.length > 0) {
55
+ siReports = metricRows.map(r => ({
56
+ feature: r.module,
57
+ structure: { nodes: r.nodes, edges: r.edges },
58
+ metrics: { depth: 0, fanout_avg: 0, fanout_max: 0, density: 0, has_cycles: false },
59
+ scores: { depth_score: 0, fanout_score: 0, density_score: 0, cycle_score: 0, query_score: 0 },
60
+ confidence: { value: r.confidence, level: r.confidence_level },
61
+ ambiguity: { level: "LOW" },
62
+ classification: {
63
+ depth_level: r.depth_level,
64
+ propagation: r.propagation,
65
+ compressible: r.compressible,
66
+ },
67
+ recommendation: {
68
+ primary: { pattern: r.pattern, confidence: r.confidence },
69
+ fallback: { pattern: "", condition: "" },
70
+ avoid: [],
71
+ },
72
+ guardrails: { enforce_if_confidence_above: 0.7 },
73
+ override: { allowed: true, requires_reason: true },
74
+ }));
75
+ }
76
+ else {
77
+ siReports = await loadStructuralIntelligenceReports(layout.machineDir);
78
+ }
79
+ }
80
+ else {
81
+ console.log("[guardian] No guardian.db found — extracting from codebase");
82
+ ({ architecture, ux } = await buildSnapshots({
83
+ projectRoot: options.projectRoot,
84
+ backendRoot: options.backendRoot,
85
+ frontendRoot: options.frontendRoot,
86
+ output: outputRoot,
87
+ includeFileGraph: true,
88
+ configPath: options.configPath
89
+ }));
90
+ siReports = await loadStructuralIntelligenceReports(layout.machineDir);
91
+ }
92
+ }
93
+ finally {
94
+ if (store)
95
+ await store.close();
96
+ }
24
97
  // If a --focus query is provided, prepend a real-time SI report for that query
25
98
  if (options.focus) {
26
99
  try {
@@ -13,32 +13,62 @@
13
13
  */
14
14
  import fs from "node:fs/promises";
15
15
  import path from "node:path";
16
+ import { randomUUID } from "node:crypto";
16
17
  import { DEFAULT_SPECS_DIR } from "../config.js";
17
18
  const DEFAULT_CONFIG = {
18
19
  docs: {
19
20
  mode: "full",
20
21
  },
21
22
  };
23
+ /**
24
+ * Hook script written to .claude/hooks/mcp-first.sh
25
+ *
26
+ * Blocks Read/Glob/Grep until a guardian MCP tool has been called in the
27
+ * current session. Session state lives in /tmp/guardian-used-<SESSION_ID>
28
+ * and is set by the PostToolUse hook (guardian-used.sh) below.
29
+ */
22
30
  const CLAUDE_CODE_HOOK_SCRIPT = `#!/bin/bash
23
- # Guardian MCP-first hook — ensures AI tools use Guardian MCP before reading source files.
24
- # Installed by: guardian init
31
+ # Guardian MCP-first hook — blocks Read/Glob/Grep until guardian tools are used.
32
+ # Installed by: guardian init (v3 — session-scoped flag, no time drift)
25
33
 
26
34
  INPUT=$(cat)
27
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
35
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "default"')
36
+ FLAG="/tmp/guardian-used-\${SESSION_ID}"
28
37
 
29
- cat >&2 <<BLOCK
30
- BLOCKED: Use Guardian MCP tools before reading source files.
38
+ # If guardian was called in this session, allow all file operations.
39
+ if [ -f "$FLAG" ]; then
40
+ exit 0
41
+ fi
42
+
43
+ cat >&2 <<'BLOCK'
44
+ BLOCKED: Use Guardian MCP tools before exploring source files.
31
45
 
32
- Use these MCP tools first:
33
- - guardian_orientget codebase overview
34
- - guardian_search find features by keyword
35
- - guardian_context deep dive into a specific area
46
+ Call one of these first:
47
+ guardian_search("your query")find files/symbols/endpoints by keyword
48
+ guardian_grep("pattern") semantic grep (replaces Grep tool)
49
+ guardian_glob("src/auth/**") semantic file discovery (replaces Glob tool)
50
+ guardian_orient() — get codebase overview
36
51
 
37
- Then you can read individual files as needed.
52
+ File reads are unblocked automatically for the rest of this session.
38
53
  BLOCK
39
54
 
40
55
  exit 2
41
56
  `;
57
+ /**
58
+ * Hook script written to .claude/hooks/guardian-used.sh
59
+ *
60
+ * Called by PostToolUse after any guardian_* tool. Sets the session flag
61
+ * that mcp-first.sh checks, unblocking subsequent Read/Glob/Grep calls.
62
+ */
63
+ const GUARDIAN_USED_SCRIPT = `#!/bin/bash
64
+ # Guardian PostToolUse hook — marks guardian as used for this session.
65
+ # Installed by: guardian init
66
+
67
+ INPUT=$(cat)
68
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "default"')
69
+ touch "/tmp/guardian-used-\${SESSION_ID}"
70
+ exit 0
71
+ `;
42
72
  const HOOK_SCRIPT = `#!/bin/sh
43
73
  # guardian pre-commit hook — keeps architecture context fresh
44
74
  # Installed by: guardian init
@@ -80,6 +110,22 @@ export async function runInit(options) {
80
110
  else {
81
111
  console.log(" · guardian.config.json already exists");
82
112
  }
113
+ // 2b. Ensure project_id is present in guardian.config.json
114
+ try {
115
+ const raw = await fs.readFile(configPath, "utf8");
116
+ const cfg = JSON.parse(raw);
117
+ if (!cfg.project_id) {
118
+ cfg.project_id = randomUUID();
119
+ await fs.writeFile(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
120
+ console.log(" ✓ Added project_id to guardian.config.json");
121
+ }
122
+ else {
123
+ console.log(" · guardian.config.json already has project_id");
124
+ }
125
+ }
126
+ catch {
127
+ // Non-fatal — config may not be valid JSON yet
128
+ }
83
129
  // 3. Install pre-commit hook
84
130
  if (!options.skipHook) {
85
131
  const gitDir = path.join(root, ".git");
@@ -213,18 +259,13 @@ async function setupClaudeCodeHooks(root, specsDir) {
213
259
  try {
214
260
  mcpConfig = JSON.parse(await fs.readFile(mcpJsonPath, "utf8"));
215
261
  }
216
- catch {
217
- // Corrupted — overwrite
218
- }
262
+ catch { /* corrupted — overwrite */ }
219
263
  }
220
264
  if (!mcpConfig.mcpServers)
221
265
  mcpConfig.mcpServers = {};
222
266
  const servers = mcpConfig.mcpServers;
223
267
  if (!servers.guardian) {
224
- servers.guardian = {
225
- command: "guardian",
226
- args: ["mcp-serve", "--specs", specsDir],
227
- };
268
+ servers.guardian = { command: "guardian", args: ["mcp-serve", "--specs", specsDir] };
228
269
  await fs.writeFile(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf8");
229
270
  console.log(" ✓ Created .mcp.json (MCP server config)");
230
271
  }
@@ -238,58 +279,49 @@ async function setupClaudeCodeHooks(root, specsDir) {
238
279
  const claudeDir = path.join(root, ".claude");
239
280
  const hooksDir = path.join(claudeDir, "hooks");
240
281
  const settingsPath = path.join(claudeDir, "settings.json");
241
- const hookScriptPath = path.join(hooksDir, "mcp-first.sh");
282
+ const mcpFirstPath = path.join(hooksDir, "mcp-first.sh");
283
+ const guardianUsedPath = path.join(hooksDir, "guardian-used.sh");
242
284
  await fs.mkdir(hooksDir, { recursive: true });
243
- // Write the hook script
244
- if (!(await fileExists(hookScriptPath))) {
245
- await fs.writeFile(hookScriptPath, CLAUDE_CODE_HOOK_SCRIPT, "utf8");
246
- await fs.chmod(hookScriptPath, 0o755);
247
- console.log(" ✓ Created Claude Code MCP-first hook (.claude/hooks/mcp-first.sh)");
248
- }
249
- else {
250
- console.log(" · Claude Code hook already exists");
251
- }
285
+ // Always overwrite hook scripts so they stay in sync with this version of guardian.
286
+ await fs.writeFile(mcpFirstPath, CLAUDE_CODE_HOOK_SCRIPT, "utf8");
287
+ await fs.chmod(mcpFirstPath, 0o755);
288
+ console.log(" ✓ Wrote .claude/hooks/mcp-first.sh (PreToolUse — blocks until guardian called)");
289
+ await fs.writeFile(guardianUsedPath, GUARDIAN_USED_SCRIPT, "utf8");
290
+ await fs.chmod(guardianUsedPath, 0o755);
291
+ console.log(" ✓ Wrote .claude/hooks/guardian-used.sh (PostToolUse — sets session flag)");
252
292
  // Write or merge .claude/settings.json
253
293
  let settings = {};
254
294
  if (await fileExists(settingsPath)) {
255
295
  try {
256
296
  settings = JSON.parse(await fs.readFile(settingsPath, "utf8"));
257
297
  }
258
- catch {
259
- // Corrupted file — overwrite
260
- }
298
+ catch { /* corrupted — overwrite */ }
261
299
  }
262
- // Add MCP server config
300
+ // MCP server registration
263
301
  if (!settings.mcpServers)
264
302
  settings.mcpServers = {};
265
- const mcpServers = settings.mcpServers;
266
- if (!mcpServers.guardian) {
267
- mcpServers.guardian = {
268
- command: "guardian",
269
- args: ["mcp-serve", "--specs", specsDir],
270
- };
271
- }
272
- // Add PreToolUse hook
273
- const hookEntry = {
274
- matcher: "Read|Glob|Grep",
275
- hooks: [
303
+ settings.mcpServers.guardian = {
304
+ command: "guardian",
305
+ args: ["mcp-serve", "--specs", specsDir],
306
+ };
307
+ // Hooks always overwrite to keep in sync with installed scripts.
308
+ settings.hooks = {
309
+ // PreToolUse: block Read/Glob/Grep until a guardian tool has been called.
310
+ // The script itself handles the session-flag check — no "if" filter needed here.
311
+ PreToolUse: [
312
+ {
313
+ matcher: "Read|Glob|Grep",
314
+ hooks: [{ type: "command", command: ".claude/hooks/mcp-first.sh" }],
315
+ },
316
+ ],
317
+ // PostToolUse: set the session flag after any guardian MCP tool call.
318
+ PostToolUse: [
276
319
  {
277
- type: "command",
278
- if: "Read(//*/src/*)|Glob(*src*)|Grep(*src*)",
279
- command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/mcp-first.sh',
320
+ matcher: "mcp__guardian__guardian_search|mcp__guardian__guardian_orient|mcp__guardian__guardian_context|mcp__guardian__guardian_impact|mcp__guardian__guardian_grep|mcp__guardian__guardian_glob",
321
+ hooks: [{ type: "command", command: ".claude/hooks/guardian-used.sh" }],
280
322
  },
281
323
  ],
282
324
  };
283
- if (!settings.hooks)
284
- settings.hooks = {};
285
- const hooks = settings.hooks;
286
- if (!hooks.PreToolUse) {
287
- hooks.PreToolUse = [hookEntry];
288
- console.log(" ✓ Configured Claude Code PreToolUse hook in .claude/settings.json");
289
- }
290
- else {
291
- console.log(" · Claude Code PreToolUse hook already configured");
292
- }
293
325
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
294
- console.log(" ✓ Updated .claude/settings.json (MCP server + hooks)");
326
+ console.log(" ✓ Updated .claude/settings.json (MCP server + PreToolUse + PostToolUse hooks)");
295
327
  }
@@ -13,6 +13,7 @@ import { writeCodebaseIntelligenceViaStore } from "../extract/codebase-intel.js"
13
13
  import { getOutputLayout } from "../output-layout.js";
14
14
  import { SqliteSpecsStore } from "../db/sqlite-specs-store.js";
15
15
  import { populateFTSIndex } from "../db/fts-builder.js";
16
+ import { embedFunctions } from "../db/embeddings.js";
16
17
  export async function runIntel(options) {
17
18
  const specsDir = path.resolve(options.specs);
18
19
  const layout = getOutputLayout(specsDir);
@@ -49,6 +50,28 @@ export async function runIntel(options) {
49
50
  catch { /* not generated yet — skip */ }
50
51
  populateFTSIndex(store, intel, arch, funcIntel);
51
52
  console.log(`Built FTS5 search index (${Object.keys(intel.api_registry ?? {}).length} endpoints indexed)`);
53
+ // Populate module_metrics from structural-intelligence.json (if present).
54
+ try {
55
+ const siRaw = await (await import("node:fs/promises")).readFile((await import("node:path")).join(machineDir, "structural-intelligence.json"), "utf8");
56
+ const siReports = JSON.parse(siRaw);
57
+ if (Array.isArray(siReports) && siReports.length > 0) {
58
+ store.rebuildModuleMetrics(siReports);
59
+ console.log(`Indexed ${siReports.length} module metrics`);
60
+ }
61
+ }
62
+ catch { /* structural-intelligence.json not generated yet — skip */ }
63
+ // Embed functions for semantic (vector) search.
64
+ // Uses local on-device model by default (no API key needed).
65
+ // If OPENAI_API_KEY is set, uses OpenAI text-embedding-3-small (better quality).
66
+ if (funcIntel?.functions?.length) {
67
+ console.log(`[guardian embed] embedding ${funcIntel.functions.length} functions…`);
68
+ try {
69
+ await embedFunctions(store, funcIntel.functions, process.env.OPENAI_API_KEY);
70
+ }
71
+ catch (err) {
72
+ console.warn(`[guardian embed] skipped: ${err.message}`);
73
+ }
74
+ }
52
75
  }
53
76
  console.log(`Wrote guardian.db → ${layout.rootDir}`);
54
77
  }