@toolbaux/guardian 0.1.11 → 0.1.12
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 +52 -0
- package/dist/commands/mcp-serve.js +131 -158
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,58 @@ Guardian auto-injects architecture context into `CLAUDE.md` so your AI tool read
|
|
|
79
79
|
|
|
80
80
|
The block between markers is replaced on every save (VSCode extension) and every commit (pre-commit hook). Your manual content outside the markers is never touched.
|
|
81
81
|
|
|
82
|
+
## MCP Server — AI Tools Connect Directly
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
**6 compact tools available to AI:**
|
|
87
|
+
|
|
88
|
+
| Tool | Tokens | Purpose |
|
|
89
|
+
|------|--------|---------|
|
|
90
|
+
| `guardian_orient` | ~100 | Project summary at session start |
|
|
91
|
+
| `guardian_context` | ~50-80 | File or endpoint dependencies before editing |
|
|
92
|
+
| `guardian_impact` | ~30 | What breaks if you change a file |
|
|
93
|
+
| `guardian_search` | ~70 | Find endpoints, models, modules by keyword |
|
|
94
|
+
| `guardian_model` | ~90 | Full field details (only when needed) |
|
|
95
|
+
| `guardian_metrics` | ~50 | Session usage stats |
|
|
96
|
+
|
|
97
|
+
All responses are compact JSON — no pretty-printing, no verbose keys. Repeated calls are cached (30s TTL). Usage metrics tracked per session.
|
|
98
|
+
|
|
99
|
+
**Manual setup** (if the extension doesn't auto-configure):
|
|
100
|
+
|
|
101
|
+
Create `.mcp.json` at your project root:
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"guardian": {
|
|
106
|
+
"command": "guardian",
|
|
107
|
+
"args": ["mcp-serve", "--specs", ".specs"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## VSCode Extension
|
|
114
|
+
|
|
115
|
+
Install from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=toolbaux.toolbaux-guardian):
|
|
116
|
+
|
|
117
|
+
Search "ToolBaux Guardian" in Extensions, or:
|
|
118
|
+
```
|
|
119
|
+
Cmd+Shift+P → "Extensions: Install from VSIX"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**What it does automatically:**
|
|
123
|
+
- Creates `.specs/`, config, and pre-commit hook on first activation
|
|
124
|
+
- Configures MCP server for Claude Code and Cursor (`.mcp.json`)
|
|
125
|
+
- Extracts architecture on every file save (5s debounce)
|
|
126
|
+
- Shows drift status in status bar: `✓ Guardian: stable · 35 ep · 8 pg`
|
|
127
|
+
|
|
128
|
+
**Commands** (Cmd+Shift+P):
|
|
129
|
+
- Guardian: Initialize Project
|
|
130
|
+
- Guardian: Generate AI Context
|
|
131
|
+
- Guardian: Drift Check
|
|
132
|
+
- Guardian: Generate Constraints
|
|
133
|
+
|
|
82
134
|
## Key Commands
|
|
83
135
|
|
|
84
136
|
```bash
|
|
@@ -98,208 +98,181 @@ async function loadIntel() {
|
|
|
98
98
|
}
|
|
99
99
|
return intel;
|
|
100
100
|
}
|
|
101
|
-
// ──
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
// ── Helpers ──
|
|
102
|
+
const SKIP_SERVICES = new Set(["str", "dict", "int", "len", "float", "max", "join", "getattr", "lower", "open", "params.append", "updates.append"]);
|
|
103
|
+
function compact(obj) {
|
|
104
|
+
return JSON.stringify(obj);
|
|
105
|
+
}
|
|
106
|
+
function findModule(data, file) {
|
|
107
|
+
return data.service_map?.find((m) => m.path && file.replace(/^\.\//, "").startsWith(m.path.replace(/^\.\//, "")));
|
|
108
|
+
}
|
|
109
|
+
function findEndpointsInFile(data, file) {
|
|
110
|
+
const f = file.replace(/^\.\//, "");
|
|
111
|
+
return Object.values(data.api_registry || {}).filter((ep) => ep.file && f.includes(ep.file.replace(/^\.\//, "")));
|
|
112
|
+
}
|
|
113
|
+
function findModelsInFile(data, file) {
|
|
114
|
+
const f = file.replace(/^\.\//, "");
|
|
115
|
+
return Object.values(data.model_registry || {}).filter((m) => m.file && f.includes(m.file.replace(/^\.\//, "")));
|
|
116
|
+
}
|
|
117
|
+
// ── Tool implementations (compact JSON, no redundancy) ──
|
|
118
|
+
async function orient() {
|
|
119
|
+
const d = await loadIntel();
|
|
120
|
+
const c = d.meta?.counts || {};
|
|
121
|
+
const mods = (d.service_map || []).filter((m) => m.file_count > 0);
|
|
122
|
+
const topMods = mods.sort((a, b) => (b.endpoint_count || 0) - (a.endpoint_count || 0)).slice(0, 6);
|
|
123
|
+
return compact({
|
|
124
|
+
p: d.meta?.project,
|
|
125
|
+
ep: c.endpoints, mod: c.models, pg: c.pages, m: c.modules,
|
|
126
|
+
top: topMods.map((m) => [m.id, m.endpoint_count, m.layer]),
|
|
127
|
+
pages: (d.frontend_pages || []).map((p) => p.path),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function context(args) {
|
|
131
|
+
const d = await loadIntel();
|
|
132
|
+
const t = args.target;
|
|
133
|
+
// Check if target is an endpoint (e.g. "POST /sessions/start")
|
|
134
|
+
const epMatch = t.match(/^(GET|POST|PUT|DELETE|PATCH)\s+(.+)$/i);
|
|
135
|
+
if (epMatch) {
|
|
136
|
+
const ep = d.api_registry?.[`${epMatch[1].toUpperCase()} ${epMatch[2]}`]
|
|
137
|
+
|| Object.values(d.api_registry || {}).find((e) => e.method === epMatch[1].toUpperCase() && e.path === epMatch[2]);
|
|
138
|
+
if (!ep)
|
|
139
|
+
return compact({ err: "not found" });
|
|
140
|
+
const svcs = (ep.service_calls || []).filter((s) => !SKIP_SERVICES.has(s));
|
|
141
|
+
return compact({
|
|
142
|
+
ep: `${ep.method} ${ep.path}`, h: ep.handler, f: ep.file, m: ep.module,
|
|
143
|
+
req: ep.request_schema, res: ep.response_schema,
|
|
144
|
+
calls: svcs, ai: ep.ai_operations?.length || 0,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Otherwise treat as file path
|
|
148
|
+
const file = t.replace(/^\.\//, "");
|
|
149
|
+
const mod = findModule(d, file);
|
|
150
|
+
const eps = findEndpointsInFile(d, file);
|
|
151
|
+
const models = findModelsInFile(d, file);
|
|
113
152
|
const fileName = path.basename(file, path.extname(file));
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
calledBy.push(`${
|
|
153
|
+
const calledBy = [];
|
|
154
|
+
for (const ep of Object.values(d.api_registry || {})) {
|
|
155
|
+
if (ep.service_calls?.some((s) => s.toLowerCase().includes(fileName.toLowerCase()))) {
|
|
156
|
+
calledBy.push(`${ep.method} ${ep.path}`);
|
|
118
157
|
}
|
|
119
158
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
159
|
+
const calls = eps.flatMap((ep) => (ep.service_calls || []).filter((s) => !SKIP_SERVICES.has(s)));
|
|
160
|
+
return compact({
|
|
161
|
+
f: file,
|
|
162
|
+
mod: mod ? [mod.id, mod.layer] : null,
|
|
163
|
+
ep: eps.map((e) => `${e.method} ${e.path}`),
|
|
164
|
+
models: models.map((m) => [m.name, m.fields?.length || 0]),
|
|
165
|
+
calls: [...new Set(calls)],
|
|
166
|
+
calledBy: calledBy.slice(0, 8),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function impact(args) {
|
|
170
|
+
const d = await loadIntel();
|
|
171
|
+
const file = args.target.replace(/^\.\//, "");
|
|
172
|
+
const eps = findEndpointsInFile(d, file);
|
|
173
|
+
const models = findModelsInFile(d, file);
|
|
174
|
+
const modelNames = new Set(models.map((m) => m.name));
|
|
175
|
+
const affectedEps = Object.values(d.api_registry || {}).filter((ep) => (ep.request_schema && modelNames.has(ep.request_schema)) ||
|
|
176
|
+
(ep.response_schema && modelNames.has(ep.response_schema)));
|
|
177
|
+
const mod = findModule(d, file);
|
|
178
|
+
const depMods = mod ? (d.service_map || []).filter((m) => m.imports?.includes(mod.id)) : [];
|
|
179
|
+
const affectedPages = (d.frontend_pages || []).filter((p) => p.api_calls?.some((call) => eps.some((ep) => call.includes(ep.path?.split("{")[0]))));
|
|
180
|
+
const total = eps.length + affectedEps.length + depMods.length + affectedPages.length;
|
|
181
|
+
return compact({
|
|
182
|
+
f: file,
|
|
183
|
+
risk: total > 5 ? "HIGH" : total > 2 ? "MED" : "LOW",
|
|
184
|
+
ep: eps.map((e) => `${e.method} ${e.path}`),
|
|
185
|
+
models: models.map((m) => m.name),
|
|
186
|
+
affectedEp: affectedEps.map((e) => `${e.method} ${e.path}`),
|
|
187
|
+
depMods: depMods.map((m) => m.id),
|
|
188
|
+
pages: affectedPages.map((p) => p.path),
|
|
189
|
+
});
|
|
134
190
|
}
|
|
135
191
|
async function search(args) {
|
|
136
|
-
const
|
|
192
|
+
const d = await loadIntel();
|
|
137
193
|
const q = args.query.toLowerCase();
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
ep.handler?.toLowerCase().includes(q) ||
|
|
144
|
-
ep.service_calls?.some((s) => s.toLowerCase().includes(q)))
|
|
145
|
-
.slice(0, 10)
|
|
146
|
-
.map((ep) => `${ep.method} ${ep.path} → ${ep.handler} [${ep.module}]`);
|
|
147
|
-
}
|
|
148
|
-
if (types.includes("models")) {
|
|
149
|
-
results.models = Object.values(data.model_registry || {})
|
|
150
|
-
.filter((m) => m.name?.toLowerCase().includes(q) ||
|
|
151
|
-
m.fields?.some((f) => f.toLowerCase().includes(q)))
|
|
152
|
-
.slice(0, 10)
|
|
153
|
-
.map((m) => `${m.name} (${m.framework}, ${m.fields?.length} fields, ${m.file})`);
|
|
154
|
-
}
|
|
155
|
-
if (types.includes("modules")) {
|
|
156
|
-
results.modules = (data.service_map || [])
|
|
157
|
-
.filter((m) => m.id?.toLowerCase().includes(q) ||
|
|
158
|
-
m.path?.toLowerCase().includes(q))
|
|
159
|
-
.slice(0, 10)
|
|
160
|
-
.map((m) => `${m.id} (${m.type}, ${m.endpoint_count} eps, ${m.file_count} files, imports: ${m.imports?.join(",") || "none"})`);
|
|
161
|
-
}
|
|
162
|
-
return JSON.stringify(results, null, 2);
|
|
163
|
-
}
|
|
164
|
-
async function endpointTrace(args) {
|
|
165
|
-
const data = await loadIntel();
|
|
166
|
-
const key = `${args.method.toUpperCase()} ${args.path}`;
|
|
167
|
-
const ep = data.api_registry?.[key] || Object.values(data.api_registry || {}).find((e) => e.method === args.method.toUpperCase() && e.path === args.path);
|
|
168
|
-
if (!ep)
|
|
169
|
-
return JSON.stringify({ error: `Endpoint ${key} not found` });
|
|
170
|
-
// Find which frontend pages call this endpoint
|
|
171
|
-
const frontendCallers = (data.frontend_pages || []).filter((p) => p.api_calls?.some((call) => call.includes(args.path.split("{")[0])));
|
|
172
|
-
// Find what models this endpoint uses
|
|
173
|
-
const models = Object.values(data.model_registry || {}).filter((m) => ep.request_schema === m.name || ep.response_schema === m.name);
|
|
174
|
-
return JSON.stringify({
|
|
175
|
-
endpoint: `${ep.method} ${ep.path}`,
|
|
176
|
-
handler: ep.handler,
|
|
177
|
-
file: ep.file,
|
|
178
|
-
module: ep.module,
|
|
179
|
-
request_schema: ep.request_schema,
|
|
180
|
-
response_schema: ep.response_schema,
|
|
181
|
-
service_calls: ep.service_calls,
|
|
182
|
-
ai_operations: ep.ai_operations,
|
|
183
|
-
patterns: ep.patterns,
|
|
184
|
-
models_used: models.map((m) => ({ name: m.name, fields: m.fields })),
|
|
185
|
-
frontend_callers: frontendCallers.map((p) => p.path),
|
|
186
|
-
}, null, 2);
|
|
194
|
+
const eps = Object.values(d.api_registry || {}).filter((ep) => ep.path?.toLowerCase().includes(q) || ep.handler?.toLowerCase().includes(q) ||
|
|
195
|
+
ep.service_calls?.some((s) => s.toLowerCase().includes(q))).slice(0, 8).map((ep) => `${ep.method} ${ep.path} [${ep.module}]`);
|
|
196
|
+
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`);
|
|
197
|
+
const mods = (d.service_map || []).filter((m) => m.id?.toLowerCase().includes(q)).slice(0, 5).map((m) => `${m.id}:${m.endpoint_count}ep`);
|
|
198
|
+
return compact({ ep: eps, mod: models, m: mods });
|
|
187
199
|
}
|
|
188
|
-
async function
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// Find modules that import from this file's module
|
|
200
|
-
const fileModule = data.service_map?.find((m) => m.path && file.startsWith(m.path.replace(/^\.\//, "")));
|
|
201
|
-
const dependentModules = fileModule
|
|
202
|
-
? (data.service_map || []).filter((m) => m.imports?.includes(fileModule.id))
|
|
203
|
-
: [];
|
|
204
|
-
// Find frontend pages affected
|
|
205
|
-
const affectedPages = (data.frontend_pages || []).filter((p) => p.api_calls?.some((call) => endpoints.some((ep) => call.includes(ep.path?.split("{")[0]))));
|
|
206
|
-
return JSON.stringify({
|
|
207
|
-
file,
|
|
208
|
-
direct_endpoints: endpoints.map((ep) => `${ep.method} ${ep.path}`),
|
|
209
|
-
models_defined: models.map((m) => m.name),
|
|
210
|
-
endpoints_using_these_models: affectedEndpoints.map((ep) => `${ep.method} ${ep.path}`),
|
|
211
|
-
dependent_modules: dependentModules.map((m) => m.id),
|
|
212
|
-
affected_frontend_pages: affectedPages.map((p) => p.path),
|
|
213
|
-
risk: endpoints.length + affectedEndpoints.length + dependentModules.length > 5 ? "HIGH" : "LOW",
|
|
214
|
-
}, null, 2);
|
|
215
|
-
}
|
|
216
|
-
async function overview() {
|
|
217
|
-
const data = await loadIntel();
|
|
218
|
-
return JSON.stringify({
|
|
219
|
-
project: data.meta?.project,
|
|
220
|
-
counts: data.meta?.counts,
|
|
221
|
-
modules: (data.service_map || [])
|
|
222
|
-
.filter((m) => m.file_count > 0)
|
|
223
|
-
.map((m) => ({ id: m.id, type: m.type, layer: m.layer, endpoints: m.endpoint_count, files: m.file_count, imports: m.imports })),
|
|
224
|
-
pages: (data.frontend_pages || []).map((p) => ({ route: p.path, component: p.component })),
|
|
225
|
-
top_endpoints: Object.values(data.api_registry || {})
|
|
226
|
-
.sort((a, b) => (b.service_calls?.length || 0) - (a.service_calls?.length || 0))
|
|
227
|
-
.slice(0, 5)
|
|
228
|
-
.map((ep) => `${ep.method} ${ep.path} (${ep.service_calls?.length || 0} service calls)`),
|
|
229
|
-
}, null, 2);
|
|
200
|
+
async function model(args) {
|
|
201
|
+
const d = await loadIntel();
|
|
202
|
+
const m = d.model_registry?.[args.name];
|
|
203
|
+
if (!m)
|
|
204
|
+
return compact({ err: "not found" });
|
|
205
|
+
const usedBy = Object.values(d.api_registry || {}).filter((ep) => ep.request_schema === args.name || ep.response_schema === args.name).map((ep) => `${ep.method} ${ep.path}`);
|
|
206
|
+
return compact({
|
|
207
|
+
name: m.name, fw: m.framework, f: m.file,
|
|
208
|
+
fields: m.fields, rels: m.relationships,
|
|
209
|
+
usedBy,
|
|
210
|
+
});
|
|
230
211
|
}
|
|
231
212
|
// ── MCP protocol ──
|
|
232
213
|
const TOOLS = [
|
|
233
214
|
{
|
|
234
|
-
name: "
|
|
235
|
-
description: "
|
|
236
|
-
inputSchema: {
|
|
237
|
-
type: "object",
|
|
238
|
-
properties: {
|
|
239
|
-
file: { type: "string", description: "File path relative to project root (e.g. 'backend/service-conversation/engine.py')" },
|
|
240
|
-
},
|
|
241
|
-
required: ["file"],
|
|
242
|
-
},
|
|
215
|
+
name: "guardian_orient",
|
|
216
|
+
description: "Compact project summary. Call at session start. Returns: project name, counts, top modules, page routes.",
|
|
217
|
+
inputSchema: { type: "object", properties: {} },
|
|
243
218
|
},
|
|
244
219
|
{
|
|
245
|
-
name: "
|
|
246
|
-
description: "
|
|
220
|
+
name: "guardian_context",
|
|
221
|
+
description: "Get dependencies for a file or endpoint. Pass a file path (e.g. 'backend/service-conversation/engine.py') or an endpoint (e.g. 'POST /sessions/start').",
|
|
247
222
|
inputSchema: {
|
|
248
223
|
type: "object",
|
|
249
224
|
properties: {
|
|
250
|
-
|
|
251
|
-
types: { type: "string", description: "Comma-separated: models,endpoints,modules (default: all)" },
|
|
225
|
+
target: { type: "string", description: "File path or 'METHOD /path' endpoint" },
|
|
252
226
|
},
|
|
253
|
-
required: ["
|
|
227
|
+
required: ["target"],
|
|
254
228
|
},
|
|
255
229
|
},
|
|
256
230
|
{
|
|
257
|
-
name: "
|
|
258
|
-
description: "
|
|
231
|
+
name: "guardian_impact",
|
|
232
|
+
description: "What breaks if you change this file? Returns affected endpoints, models, modules, pages, and risk level.",
|
|
259
233
|
inputSchema: {
|
|
260
234
|
type: "object",
|
|
261
235
|
properties: {
|
|
262
|
-
|
|
263
|
-
path: { type: "string", description: "Endpoint path (e.g. '/sessions/start')" },
|
|
236
|
+
target: { type: "string", description: "File path to check" },
|
|
264
237
|
},
|
|
265
|
-
required: ["
|
|
238
|
+
required: ["target"],
|
|
266
239
|
},
|
|
267
240
|
},
|
|
268
241
|
{
|
|
269
|
-
name: "
|
|
270
|
-
description: "
|
|
242
|
+
name: "guardian_search",
|
|
243
|
+
description: "Find endpoints, models, modules by keyword. Returns compact one-line results.",
|
|
271
244
|
inputSchema: {
|
|
272
245
|
type: "object",
|
|
273
246
|
properties: {
|
|
274
|
-
|
|
247
|
+
query: { type: "string", description: "Search keyword" },
|
|
275
248
|
},
|
|
276
|
-
required: ["
|
|
249
|
+
required: ["query"],
|
|
277
250
|
},
|
|
278
251
|
},
|
|
279
252
|
{
|
|
280
|
-
name: "
|
|
281
|
-
description: "Get
|
|
253
|
+
name: "guardian_model",
|
|
254
|
+
description: "Get full field list and usage for a specific model. Only call when you need field details.",
|
|
282
255
|
inputSchema: {
|
|
283
256
|
type: "object",
|
|
284
|
-
properties: {
|
|
257
|
+
properties: {
|
|
258
|
+
name: { type: "string", description: "Model name (e.g. 'StartSessionRequest')" },
|
|
259
|
+
},
|
|
260
|
+
required: ["name"],
|
|
285
261
|
},
|
|
286
262
|
},
|
|
287
263
|
{
|
|
288
264
|
name: "guardian_metrics",
|
|
289
|
-
description: "
|
|
290
|
-
inputSchema: {
|
|
291
|
-
type: "object",
|
|
292
|
-
properties: {},
|
|
293
|
-
},
|
|
265
|
+
description: "MCP usage stats for this session. Call at end to evaluate guardian's usefulness.",
|
|
266
|
+
inputSchema: { type: "object", properties: {} },
|
|
294
267
|
},
|
|
295
268
|
];
|
|
296
269
|
const TOOL_HANDLERS = {
|
|
297
|
-
|
|
270
|
+
guardian_orient: orient,
|
|
271
|
+
guardian_context: context,
|
|
272
|
+
guardian_impact: impact,
|
|
298
273
|
guardian_search: search,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
guardian_overview: overview,
|
|
302
|
-
guardian_metrics: async () => JSON.stringify(metrics.summary(), null, 2),
|
|
274
|
+
guardian_model: model,
|
|
275
|
+
guardian_metrics: async () => compact(metrics.summary()),
|
|
303
276
|
};
|
|
304
277
|
function respond(id, result) {
|
|
305
278
|
const msg = JSON.stringify({ jsonrpc: "2.0", id, result });
|
package/package.json
CHANGED