@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 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
- // ── Tool implementations ──
102
- async function fileContext(args) {
103
- const data = await loadIntel();
104
- const file = args.file.replace(/^\.\//, "");
105
- // Find which module this file belongs to
106
- const module = data.service_map?.find((m) => m.path && file.startsWith(m.path.replace(/^\.\//, "")));
107
- // Find endpoints in this file
108
- const endpoints = Object.values(data.api_registry || {}).filter((ep) => ep.file && file.includes(ep.file.replace(/^\.\//, "")));
109
- // Find models in this file
110
- const models = Object.values(data.model_registry || {}).filter((m) => m.file && file.includes(m.file.replace(/^\.\//, "")));
111
- // Find which endpoints call services defined in this file
112
- const calledBy = [];
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
- for (const [key, ep] of Object.entries(data.api_registry || {})) {
115
- const e = ep;
116
- if (e.service_calls?.some((s) => s.toLowerCase().includes(fileName.toLowerCase()))) {
117
- calledBy.push(`${e.method} ${e.path} (${e.handler})`);
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
- // Find what this file's endpoints call
121
- const calls = endpoints.flatMap((ep) => (ep.service_calls || []).filter((s) => !["str", "dict", "int", "len", "float", "max", "join", "getattr"].includes(s)));
122
- // Find frontend pages that use APIs from this module
123
- const pages = (data.frontend_pages || []).filter((p) => p.api_calls?.some((call) => endpoints.some((ep) => call.includes(ep.path?.split("{")[0]))));
124
- return JSON.stringify({
125
- file,
126
- module: module ? { id: module.id, layer: module.layer, file_count: module.file_count, imports: module.imports } : null,
127
- endpoints_in_file: endpoints.map((ep) => `${ep.method} ${ep.path} → ${ep.handler}`),
128
- models_in_file: models.map((m) => `${m.name} (${m.framework}, ${m.fields?.length || 0} fields)`),
129
- calls_downstream: [...new Set(calls)],
130
- called_by_upstream: calledBy.slice(0, 10),
131
- frontend_pages_using: pages.map((p) => p.path),
132
- coupling: module?.coupling_score ?? null,
133
- }, null, 2);
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 data = await loadIntel();
192
+ const d = await loadIntel();
137
193
  const q = args.query.toLowerCase();
138
- const types = (args.types || "models,endpoints,modules").split(",").map((t) => t.trim());
139
- const results = {};
140
- if (types.includes("endpoints")) {
141
- results.endpoints = Object.values(data.api_registry || {})
142
- .filter((ep) => ep.path?.toLowerCase().includes(q) ||
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 impactCheck(args) {
189
- const data = await loadIntel();
190
- const file = args.file.replace(/^\.\//, "");
191
- // Find all endpoints in this file
192
- const endpoints = Object.values(data.api_registry || {}).filter((ep) => ep.file && file.includes(ep.file.replace(/^\.\//, "")));
193
- // Find all models in this file
194
- const models = Object.values(data.model_registry || {}).filter((m) => m.file && file.includes(m.file.replace(/^\.\//, "")));
195
- // Find endpoints that USE these models
196
- const modelNames = new Set(models.map((m) => m.name));
197
- const affectedEndpoints = Object.values(data.api_registry || {}).filter((ep) => ep.request_schema && modelNames.has(ep.request_schema) ||
198
- ep.response_schema && modelNames.has(ep.response_schema));
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: "guardian_file_context",
235
- description: "Get upstream/downstream dependencies, endpoints, models, and coupling for a file. Call this BEFORE editing any file.",
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: "guardian_search",
246
- description: "Search the codebase for endpoints, models, or modules matching a keyword.",
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
- query: { type: "string", description: "Search keyword (e.g. 'session', 'auth', 'TTS')" },
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: ["query"],
227
+ required: ["target"],
254
228
  },
255
229
  },
256
230
  {
257
- name: "guardian_endpoint_trace",
258
- description: "Trace an API endpoint's full chain: frontend callers, handler, service calls, models, AI operations.",
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
- method: { type: "string", description: "HTTP method (GET, POST, PUT, DELETE)" },
263
- path: { type: "string", description: "Endpoint path (e.g. '/sessions/start')" },
236
+ target: { type: "string", description: "File path to check" },
264
237
  },
265
- required: ["method", "path"],
238
+ required: ["target"],
266
239
  },
267
240
  },
268
241
  {
269
- name: "guardian_impact_check",
270
- description: "Check what endpoints, models, modules, and pages are affected if you change a file. Call this BEFORE making changes to high-coupling files.",
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
- file: { type: "string", description: "File path to check impact for" },
247
+ query: { type: "string", description: "Search keyword" },
275
248
  },
276
- required: ["file"],
249
+ required: ["query"],
277
250
  },
278
251
  },
279
252
  {
280
- name: "guardian_overview",
281
- description: "Get project summary: modules, pages, top endpoints, counts. Call this at session start for orientation.",
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: "Get MCP usage metrics for this session: calls made, tokens spent, tokens saved, cache hits. Call at end of session to evaluate guardian's usefulness.",
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
- guardian_file_context: fileContext,
270
+ guardian_orient: orient,
271
+ guardian_context: context,
272
+ guardian_impact: impact,
298
273
  guardian_search: search,
299
- guardian_endpoint_trace: endpointTrace,
300
- guardian_impact_check: impactCheck,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolbaux/guardian",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "description": "Architectural intelligence for codebases. Verify that AI-generated code matches your architectural intent.",
6
6
  "keywords": [