@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 +6 -4
- package/dist/cli.js +1 -1
- package/dist/commands/context.js +87 -29
- package/dist/commands/extract.js +4 -1
- package/dist/commands/generate.js +83 -10
- package/dist/commands/init.js +88 -56
- package/dist/commands/intel.js +23 -0
- package/dist/commands/mcp-serve.js +112 -0
- package/dist/commands/search.js +43 -3
- package/dist/config.js +1 -0
- package/dist/db/embeddings.js +113 -0
- package/dist/db/fts-builder.js +85 -0
- package/dist/db/sqlite-specs-store.js +496 -3
- package/package.json +2 -1
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
|
-
**
|
|
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 +
|
|
233
|
-
guardian extract --backend
|
|
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: '
|
|
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,
|
package/dist/commands/context.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|
package/dist/commands/extract.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
package/dist/commands/init.js
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
35
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "default"')
|
|
36
|
+
FLAG="/tmp/guardian-used-\${SESSION_ID}"
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
//
|
|
300
|
+
// MCP server registration
|
|
263
301
|
if (!settings.mcpServers)
|
|
264
302
|
settings.mcpServers = {};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
}
|
package/dist/commands/intel.js
CHANGED
|
@@ -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
|
}
|