sparda-mcp 0.5.2 → 0.5.4

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.
@@ -13,14 +13,18 @@ const MARK_START = '// >>> sparda-injection (do not edit this block) >>>';
13
13
  const MARK_END = '// <<< sparda-injection <<<';
14
14
 
15
15
  export function generateExpress({ cwd, entryFile, moduleType, port, routes }) {
16
- const taken = new Set(['sparda_info', 'sparda_list_disabled_tools', 'sparda_get_context']);
16
+ const taken = new Set([
17
+ 'sparda_info',
18
+ 'sparda_list_disabled_tools',
19
+ 'sparda_get_context',
20
+ ]);
17
21
  const tools = {};
18
22
  for (const r of routes) {
19
23
  const name = toolNameFor(r, taken);
20
24
  tools[name] = {
21
25
  method: r.method.toUpperCase(),
22
26
  path: r.path,
23
- enabled: !r.mutating, // write-safety: mutating tools off by default
27
+ enabled: !r.mutating, // write-safety: mutating tools off by default
24
28
  pathParams: r.params.filter((p) => p.in === 'path').map((p) => p.name),
25
29
  description: r.description,
26
30
  params: r.params,
@@ -44,7 +48,9 @@ export function generateExpress({ cwd, entryFile, moduleType, port, routes }) {
44
48
  tool: name,
45
49
  decision: 'audit',
46
50
  risk: 'medium',
47
- reasons: [`route structure modified (fingerprint changed from ${oldFp} to ${fp})`],
51
+ reasons: [
52
+ `route structure modified (fingerprint changed from ${oldFp} to ${fp})`,
53
+ ],
48
54
  });
49
55
  if (sparding.events.length > 100) sparding.events.shift();
50
56
  }
@@ -55,12 +61,21 @@ export function generateExpress({ cwd, entryFile, moduleType, port, routes }) {
55
61
  const localKey = prev?.localKey ?? crypto.randomUUID();
56
62
  const entryAbs = path.resolve(cwd, entryFile);
57
63
  const entryDir = path.dirname(entryAbs);
58
- const ext = entryFile.endsWith('.ts') ? '.ts' : entryFile.endsWith('.mjs') ? '.mjs' : entryFile.endsWith('.cjs') ? '.cjs' : '.js';
64
+ const ext = entryFile.endsWith('.ts')
65
+ ? '.ts'
66
+ : entryFile.endsWith('.mjs')
67
+ ? '.mjs'
68
+ : entryFile.endsWith('.cjs')
69
+ ? '.cjs'
70
+ : '.js';
59
71
  const routerFileName = `sparda-router${ext}`;
60
72
  const routerAbs = path.join(entryDir, routerFileName);
61
73
 
62
74
  // --- render router from template
63
- let tpl = fs.readFileSync(path.join(__dirname, '..', '..', 'templates', 'express-router.txt'), 'utf8');
75
+ let tpl = fs.readFileSync(
76
+ path.join(__dirname, '..', '..', 'templates', 'express-router.txt'),
77
+ 'utf8',
78
+ );
64
79
  const isESM = moduleType === 'esm';
65
80
  const isTS = ext === '.ts';
66
81
 
@@ -76,19 +91,25 @@ export function generateExpress({ cwd, entryFile, moduleType, port, routes }) {
76
91
  if (isESM) {
77
92
  importLine = "import express, { Request, Response, NextFunction } from 'express';";
78
93
  } else {
79
- importLine = "import type { Request, Response, NextFunction } from 'express';\nconst express = require('express');";
94
+ importLine =
95
+ "import type { Request, Response, NextFunction } from 'express';\nconst express = require('express');";
80
96
  }
81
97
  } else {
82
- importLine = isESM ? "import express from 'express';" : "const express = require('express');";
98
+ importLine = isESM
99
+ ? "import express from 'express';"
100
+ : "const express = require('express');";
83
101
  }
84
102
 
85
103
  tpl = tpl
86
104
  .replace('__IMPORT_LINE__', importLine)
87
105
  .replace('__SPARDING_POLICIES__', JSON.stringify(sparding.policies ?? {}))
88
- .replace('__ROUTER_DECL__', isESM
89
- ? 'export const spardaRouter = express.Router();'
90
- : 'const spardaRouter = express.Router();\nmodule.exports = { spardaRouter };')
91
- .replace('__JSON_MW__', 'express.json()')
106
+ .replace(
107
+ '__ROUTER_DECL__',
108
+ isESM
109
+ ? 'export const spardaRouter = express.Router();'
110
+ : 'const spardaRouter = express.Router();\nmodule.exports = { spardaRouter };',
111
+ )
112
+ .replace('__JSON_MW__', "express.json({ limit: '64kb' })")
92
113
  .replace('__TOOLS_JSON__', JSON.stringify(tools, null, 2))
93
114
  .replace('__LOCAL_KEY__', localKey)
94
115
  .replace('__PORT__', String(port))
@@ -117,7 +138,12 @@ export function generateExpress({ cwd, entryFile, moduleType, port, routes }) {
117
138
  generatedFiles: [path.relative(cwd, routerAbs).split(path.sep).join('/')],
118
139
  injectedFiles: injection.injected ? [entryFile] : [],
119
140
  createdAt: new Date().toISOString(),
120
- tools: Object.fromEntries(Object.entries(tools).map(([k, v]) => [k, { method: v.method, path: v.path, enabled: v.enabled }])),
141
+ tools: Object.fromEntries(
142
+ Object.entries(tools).map(([k, v]) => [
143
+ k,
144
+ { method: v.method, path: v.path, enabled: v.enabled },
145
+ ]),
146
+ ),
121
147
  ...(gitignore ? { gitignore } : {}),
122
148
  ...(prev?.semantic ? { semantic: prev.semantic } : {}),
123
149
  ...(prev?.immune ? { immune: prev.immune } : {}),
@@ -126,7 +152,12 @@ export function generateExpress({ cwd, entryFile, moduleType, port, routes }) {
126
152
  };
127
153
  atomicWrite(path.join(cwd, 'sparda.json'), JSON.stringify(manifest, null, 2) + '\n');
128
154
 
129
- return { tools, manifest, routerFile: path.relative(cwd, routerAbs).split(path.sep).join('/'), injection };
155
+ return {
156
+ tools,
157
+ manifest,
158
+ routerFile: path.relative(cwd, routerAbs).split(path.sep).join('/'),
159
+ injection,
160
+ };
130
161
  }
131
162
 
132
163
  function injectIntoEntry({ entryAbs, moduleType, routerFileName, cwd }) {
@@ -136,19 +167,31 @@ function injectIntoEntry({ entryAbs, moduleType, routerFileName, cwd }) {
136
167
  fs.mkdirSync(backupDir, { recursive: true });
137
168
  fs.writeFileSync(path.join(backupDir, `${path.basename(entryAbs)}.${Date.now()}`), src);
138
169
 
139
- const importSpec = `./${routerFileName.replace(/\.(ts|js|mjs|cjs)$/, moduleType === 'esm' && routerFileName.endsWith('.js') ? '.js' : '')}`.replace(/\.$/, '');
140
- const importLine = moduleType === 'esm'
141
- ? `import { spardaRouter } from '${importSpec}';`
142
- : `const { spardaRouter } = require('${importSpec}');`;
170
+ const importSpec =
171
+ `./${routerFileName.replace(/\.(ts|js|mjs|cjs)$/, moduleType === 'esm' && routerFileName.endsWith('.js') ? '.js' : '')}`.replace(
172
+ /\.$/,
173
+ '',
174
+ );
175
+ const importLine =
176
+ moduleType === 'esm'
177
+ ? `import { spardaRouter } from '${importSpec}';`
178
+ : `const { spardaRouter } = require('${importSpec}');`;
143
179
 
144
180
  // strip previous injection blocks (idempotence)
145
181
  let body = src;
146
- const blockRx = new RegExp(`\\n?${escapeRx(MARK_START)}[\\s\\S]*?${escapeRx(MARK_END)}\\n?`, 'g');
182
+ const blockRx = new RegExp(
183
+ `\\n?${escapeRx(MARK_START)}[\\s\\S]*?${escapeRx(MARK_END)}\\n?`,
184
+ 'g',
185
+ );
147
186
  body = body.replace(blockRx, '\n');
148
187
 
149
188
  let ast;
150
189
  try {
151
- ast = parse(body, { sourceType: 'unambiguous', plugins: ['typescript', 'jsx'], attachComment: false });
190
+ ast = parse(body, {
191
+ sourceType: 'unambiguous',
192
+ plugins: ['typescript', 'jsx'],
193
+ attachComment: false,
194
+ });
152
195
  } catch {
153
196
  return manualFallback(importLine);
154
197
  }
@@ -160,7 +203,8 @@ function injectIntoEntry({ entryAbs, moduleType, routerFileName, cwd }) {
160
203
  let appName = 'app';
161
204
 
162
205
  for (const node of ast.program.body) {
163
- if (node.type === 'ImportDeclaration') lastImportLine = Math.max(lastImportLine, node.loc.end.line);
206
+ if (node.type === 'ImportDeclaration')
207
+ lastImportLine = Math.max(lastImportLine, node.loc.end.line);
164
208
  if (node.type === 'VariableDeclaration') {
165
209
  for (const d of node.declarations) {
166
210
  if (d.init?.type === 'CallExpression' && d.init.callee?.name === 'express') {
@@ -172,18 +216,27 @@ function injectIntoEntry({ entryAbs, moduleType, routerFileName, cwd }) {
172
216
  }
173
217
  }
174
218
  }
175
- if (node.type === 'ExpressionStatement' &&
176
- node.expression.type === 'CallExpression' &&
177
- node.expression.callee?.property?.name === 'listen') {
219
+ if (
220
+ node.type === 'ExpressionStatement' &&
221
+ node.expression.type === 'CallExpression' &&
222
+ node.expression.callee?.property?.name === 'listen'
223
+ ) {
178
224
  listenLine = node.loc.start.line;
179
225
  }
180
226
  }
181
227
 
182
- const useBlock = [MARK_START, importLine, `${appName}.use('/mcp', spardaRouter);`, MARK_END];
228
+ const useBlock = [
229
+ MARK_START,
230
+ importLine,
231
+ `${appName}.use('/mcp', spardaRouter);`,
232
+ MARK_END,
233
+ ];
183
234
 
184
235
  let insertAt; // 0-based index AFTER which we DON'T insert; we splice before index
185
- if (appAssignLine !== null) insertAt = appAssignLine; // after app = express()
186
- else if (listenLine !== null) insertAt = listenLine - 1; // before app.listen
236
+ if (appAssignLine !== null)
237
+ insertAt = appAssignLine; // after app = express()
238
+ else if (listenLine !== null)
239
+ insertAt = listenLine - 1; // before app.listen
187
240
  else return manualFallback(importLine);
188
241
 
189
242
  lines.splice(insertAt, 0, ...useBlock);
@@ -212,7 +265,10 @@ export function removeInjection(cwd, manifest) {
212
265
  const abs = path.resolve(cwd, relFile);
213
266
  if (!fs.existsSync(abs)) continue;
214
267
  const src = fs.readFileSync(abs, 'utf8');
215
- const blockRx = new RegExp(`\\n?${escapeRx(MARK_START)}[\\s\\S]*?${escapeRx(MARK_END)}`, 'g');
268
+ const blockRx = new RegExp(
269
+ `\\n?${escapeRx(MARK_START)}[\\s\\S]*?${escapeRx(MARK_END)}`,
270
+ 'g',
271
+ );
216
272
  const out = src.replace(blockRx, '');
217
273
  try {
218
274
  parse(out, { sourceType: 'unambiguous', plugins: ['typescript', 'jsx'] });
@@ -240,4 +296,6 @@ function ensureGitignore(cwd) {
240
296
  return 'created';
241
297
  }
242
298
 
243
- function escapeRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
299
+ function escapeRx(s) {
300
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
301
+ }
@@ -12,15 +12,26 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
12
  const MARK_START = '# >>> sparda-injection (do not edit this block) >>>';
13
13
  const MARK_END = '# <<< sparda-injection <<<';
14
14
 
15
- export function generateFastAPI({ cwd, entryFile, port, routes, entryAppVars, pythonCmd = 'python' }) {
16
- const taken = new Set(['sparda_info', 'sparda_list_disabled_tools', 'sparda_get_context']);
15
+ export function generateFastAPI({
16
+ cwd,
17
+ entryFile,
18
+ port,
19
+ routes,
20
+ entryAppVars,
21
+ pythonCmd = 'python',
22
+ }) {
23
+ const taken = new Set([
24
+ 'sparda_info',
25
+ 'sparda_list_disabled_tools',
26
+ 'sparda_get_context',
27
+ ]);
17
28
  const tools = {};
18
29
  for (const r of routes) {
19
30
  const name = toolNameFor(r, taken);
20
31
  tools[name] = {
21
32
  method: r.method.toUpperCase(),
22
33
  path: r.path,
23
- enabled: !r.mutating, // write-safety: mutating tools off by default
34
+ enabled: !r.mutating, // write-safety: mutating tools off by default
24
35
  pathParams: r.params.filter((p) => p.in === 'path').map((p) => p.name),
25
36
  description: r.description,
26
37
  params: r.params,
@@ -44,7 +55,9 @@ export function generateFastAPI({ cwd, entryFile, port, routes, entryAppVars, py
44
55
  tool: name,
45
56
  decision: 'audit',
46
57
  risk: 'medium',
47
- reasons: [`route structure modified (fingerprint changed from ${oldFp} to ${fp})`],
58
+ reasons: [
59
+ `route structure modified (fingerprint changed from ${oldFp} to ${fp})`,
60
+ ],
48
61
  });
49
62
  if (sparding.events.length > 100) sparding.events.shift();
50
63
  }
@@ -59,19 +72,31 @@ export function generateFastAPI({ cwd, entryFile, port, routes, entryAppVars, py
59
72
  const routerAbs = path.join(entryDir, routerFileName);
60
73
 
61
74
  // --- render router from template
62
- let tpl = fs.readFileSync(path.join(__dirname, '..', '..', 'templates', 'fastapi-router.txt'), 'utf8');
75
+ let tpl = fs.readFileSync(
76
+ path.join(__dirname, '..', '..', 'templates', 'fastapi-router.txt'),
77
+ 'utf8',
78
+ );
63
79
  tpl = tpl
64
80
  // double-stringify: a JSON string literal is also a valid Python string literal,
65
81
  // and json.loads() in the template turns it into real Python values (E-009)
66
82
  .replace('__TOOLS_JSON__', JSON.stringify(JSON.stringify(tools)))
67
- .replace('__SPARDING_POLICIES__', JSON.stringify(JSON.stringify(sparding.policies ?? {})))
83
+ .replace(
84
+ '__SPARDING_POLICIES__',
85
+ JSON.stringify(JSON.stringify(sparding.policies ?? {})),
86
+ )
68
87
  .replace('__LOCAL_KEY__', localKey)
69
88
  .replace('__PORT__', String(port));
70
89
 
71
90
  atomicWrite(routerAbs, tpl);
72
91
 
73
92
  // --- inject into entry file (regex-based with compilation safety check)
74
- const injection = injectIntoEntry({ entryAbs, routerFileName, cwd, entryAppVars, pythonCmd });
93
+ const injection = injectIntoEntry({
94
+ entryAbs,
95
+ routerFileName,
96
+ cwd,
97
+ entryAppVars,
98
+ pythonCmd,
99
+ });
75
100
 
76
101
  // record what init did to .gitignore so `remove` can revert it exactly (hard rule #4, E-010)
77
102
  const gitignore = ensureGitignore(cwd) ?? prev?.gitignore ?? null;
@@ -86,7 +111,12 @@ export function generateFastAPI({ cwd, entryFile, port, routes, entryAppVars, py
86
111
  generatedFiles: [path.relative(cwd, routerAbs).split(path.sep).join('/')],
87
112
  injectedFiles: injection.injected ? [entryFile] : [],
88
113
  createdAt: new Date().toISOString(),
89
- tools: Object.fromEntries(Object.entries(tools).map(([k, v]) => [k, { method: v.method, path: v.path, enabled: v.enabled }])),
114
+ tools: Object.fromEntries(
115
+ Object.entries(tools).map(([k, v]) => [
116
+ k,
117
+ { method: v.method, path: v.path, enabled: v.enabled },
118
+ ]),
119
+ ),
90
120
  ...(gitignore ? { gitignore } : {}),
91
121
  ...(prev?.semantic ? { semantic: prev.semantic } : {}),
92
122
  ...(prev?.immune ? { immune: prev.immune } : {}),
@@ -96,12 +126,17 @@ export function generateFastAPI({ cwd, entryFile, port, routes, entryAppVars, py
96
126
 
97
127
  atomicWrite(path.join(cwd, 'sparda.json'), JSON.stringify(manifest, null, 2) + '\n');
98
128
 
99
- return { tools, manifest, routerFile: path.relative(cwd, routerAbs).split(path.sep).join('/'), injection };
129
+ return {
130
+ tools,
131
+ manifest,
132
+ routerFile: path.relative(cwd, routerAbs).split(path.sep).join('/'),
133
+ injection,
134
+ };
100
135
  }
101
136
 
102
- function injectIntoEntry({ entryAbs, routerFileName, cwd, entryAppVars, pythonCmd }) {
137
+ function injectIntoEntry({ entryAbs, cwd, pythonCmd }) {
103
138
  const src = fs.readFileSync(entryAbs, 'utf8');
104
-
139
+
105
140
  // backup first
106
141
  const backupDir = path.join(cwd, '.sparda', 'backup');
107
142
  fs.mkdirSync(backupDir, { recursive: true });
@@ -119,7 +154,10 @@ function injectIntoEntry({ entryAbs, routerFileName, cwd, entryAppVars, pythonCm
119
154
 
120
155
  // strip previous injection blocks (idempotence)
121
156
  let body = src;
122
- const blockRx = new RegExp(`\\r?\\n?${escapeRx(MARK_START)}[\\s\\S]*?${escapeRx(MARK_END)}\\r?\\n?`, 'g');
157
+ const blockRx = new RegExp(
158
+ `\\r?\\n?${escapeRx(MARK_START)}[\\s\\S]*?${escapeRx(MARK_END)}\\r?\\n?`,
159
+ 'g',
160
+ );
123
161
  body = body.replace(blockRx, eol);
124
162
 
125
163
  // Let's find the FastAPI() call assignment to inject right after it
@@ -128,7 +166,7 @@ function injectIntoEntry({ entryAbs, routerFileName, cwd, entryAppVars, pythonCm
128
166
  if (!match) {
129
167
  return {
130
168
  injected: false,
131
- manual: [importLine, 'app.include_router(sparda_router, prefix="/mcp")']
169
+ manual: [importLine, 'app.include_router(sparda_router, prefix="/mcp")'],
132
170
  };
133
171
  }
134
172
 
@@ -140,7 +178,7 @@ function injectIntoEntry({ entryAbs, routerFileName, cwd, entryAppVars, pythonCm
140
178
  MARK_START,
141
179
  `${indent}${importLine}`,
142
180
  `${indent}${appName}.include_router(sparda_router, prefix="/mcp")`,
143
- MARK_END
181
+ MARK_END,
144
182
  ];
145
183
 
146
184
  // We find where the matching line starts and ends in order to insert right after it.
@@ -157,7 +195,7 @@ function injectIntoEntry({ entryAbs, routerFileName, cwd, entryAppVars, pythonCm
157
195
  if (insertAt === -1) {
158
196
  return {
159
197
  injected: false,
160
- manual: [importLine, `${appName}.include_router(sparda_router, prefix="/mcp")`]
198
+ manual: [importLine, `${appName}.include_router(sparda_router, prefix="/mcp")`],
161
199
  };
162
200
  }
163
201
 
@@ -168,7 +206,7 @@ function injectIntoEntry({ entryAbs, routerFileName, cwd, entryAppVars, pythonCm
168
206
  if (!verifyPythonSyntax(entryAbs, out, pythonCmd)) {
169
207
  return {
170
208
  injected: false,
171
- manual: [importLine, `${appName}.include_router(sparda_router, prefix="/mcp")`]
209
+ manual: [importLine, `${appName}.include_router(sparda_router, prefix="/mcp")`],
172
210
  };
173
211
  }
174
212
 
@@ -182,9 +220,12 @@ export function removeInjection(cwd, manifest, pythonCmd = 'python') {
182
220
  const abs = path.resolve(cwd, relFile);
183
221
  if (!fs.existsSync(abs)) continue;
184
222
  const src = fs.readFileSync(abs, 'utf8');
185
- const blockRx = new RegExp(`\\r?\\n?${escapeRx(MARK_START)}[\\s\\S]*?${escapeRx(MARK_END)}`, 'g');
223
+ const blockRx = new RegExp(
224
+ `\\r?\\n?${escapeRx(MARK_START)}[\\s\\S]*?${escapeRx(MARK_END)}`,
225
+ 'g',
226
+ );
186
227
  const out = src.replace(blockRx, '');
187
-
228
+
188
229
  if (verifyPythonSyntax(abs, out, pythonCmd)) {
189
230
  atomicWrite(abs, out);
190
231
  results.push({ file: relFile, ok: true });
@@ -199,7 +240,10 @@ function verifyPythonSyntax(filePath, content, pythonCmd) {
199
240
  const tmpFile = `${filePath}.syntax-check.py`;
200
241
  try {
201
242
  fs.writeFileSync(tmpFile, content);
202
- const args = pythonCmd === 'py' ? ['-3', '-m', 'py_compile', tmpFile] : ['-m', 'py_compile', tmpFile];
243
+ const args =
244
+ pythonCmd === 'py'
245
+ ? ['-3', '-m', 'py_compile', tmpFile]
246
+ : ['-m', 'py_compile', tmpFile];
203
247
  // py_compile cold-starts slowly on Windows; a 2s budget falsely failed clean
204
248
  // removals (rule #4) and post-injection checks under load. 5s matches the
205
249
  // test-side syntax budget and only bounds the worst case, never the happy path.
@@ -231,4 +275,6 @@ function ensureGitignore(cwd) {
231
275
  return 'created';
232
276
  }
233
277
 
234
- function escapeRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
278
+ function escapeRx(s) {
279
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
280
+ }
@@ -16,7 +16,12 @@ export function carryOverManifest(cwd, tools) {
16
16
  }
17
17
  for (const [name, t] of Object.entries(tools)) {
18
18
  const old = prev?.tools?.[name];
19
- if (old && typeof old.enabled === 'boolean' && old.method === t.method && old.path === t.path) {
19
+ if (
20
+ old &&
21
+ typeof old.enabled === 'boolean' &&
22
+ old.method === t.method &&
23
+ old.path === t.path
24
+ ) {
20
25
  t.enabled = old.enabled;
21
26
  }
22
27
  }
@@ -24,16 +29,18 @@ export function carryOverManifest(cwd, tools) {
24
29
  }
25
30
 
26
31
  export function defaultSpardingMemory(prev) {
27
- return prev?.sparding ?? {
28
- version: 1,
29
- policies: {
30
- reads: 'allow',
31
- writes: 'require_human',
32
- deletes: 'block',
33
- requireProofAfterWrite: true,
34
- },
35
- events: [],
36
- failures: {},
37
- toolFingerprints: {},
38
- };
32
+ return (
33
+ prev?.sparding ?? {
34
+ version: 1,
35
+ policies: {
36
+ reads: 'allow',
37
+ writes: 'require_human',
38
+ deletes: 'block',
39
+ requireProofAfterWrite: true,
40
+ },
41
+ events: [],
42
+ failures: {},
43
+ toolFingerprints: {},
44
+ }
45
+ );
39
46
  }
package/src/index.js CHANGED
@@ -2,7 +2,9 @@
2
2
  // SPARDA — Turn any codebase into an MCP server. Residual Labs.
3
3
  import { readFileSync } from 'node:fs';
4
4
 
5
- const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
5
+ const VERSION = JSON.parse(
6
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
7
+ ).version;
6
8
  const [, , cmd, ...rest] = process.argv;
7
9
 
8
10
  const flags = new Set(rest.filter((a) => a.startsWith('--')));
@@ -16,12 +18,19 @@ const opts = {
16
18
  verbose: flags.has('--verbose'),
17
19
  quiet: flags.has('--quiet'),
18
20
  probe: flags.has('--probe'),
21
+ html: flags.has('--html'),
22
+ json: flags.has('--json'),
19
23
  port: getOpt('port', null),
20
24
  cwd: process.cwd(),
21
25
  };
22
26
 
23
27
  try {
24
28
  switch (cmd) {
29
+ case 'demo': {
30
+ const { runDemo } = await import('./commands/demo.js');
31
+ await runDemo(opts);
32
+ break;
33
+ }
25
34
  case 'init': {
26
35
  const { runInit } = await import('./commands/init.js');
27
36
  await runInit(opts);
@@ -53,19 +62,27 @@ try {
53
62
  await runHook(opts);
54
63
  break;
55
64
  }
65
+ case 'report': {
66
+ const { runReport } = await import('./commands/report.js');
67
+ await runReport(opts);
68
+ break;
69
+ }
56
70
  default:
57
71
  console.log(`SPARDA v${VERSION} — Turn any codebase into an MCP server.
58
72
 
59
73
  Usage:
74
+ npx sparda-mcp demo Guided tour on a bundled app — no setup, nothing touched
60
75
  npx sparda-mcp init Scan project, generate & inject the MCP router
61
76
  npx sparda-mcp dev Run the MCP stdio bridge (connect Claude Desktop)
62
77
  npx sparda-mcp sync Re-sync the router after route changes (no prompts)
63
78
  npx sparda-mcp hook Install the git sentinel (auto-sync after commits)
64
79
  npx sparda-mcp remove Remove SPARDA from this project (clean git diff)
65
80
  npx sparda-mcp doctor Diagnose your setup
81
+ npx sparda-mcp report The black box: what AI agents did to this app
66
82
 
67
83
  Flags: --yes (skip prompts) --port <n> --quiet --verbose
68
84
  --probe (init: also run the app to discover dynamic routes the AST missed)
85
+ --html / --json (report: write .sparda/report.html / print raw JSON)
69
86
 
70
87
  By Residual Labs — residual-labs.fr`);
71
88
  process.exit(cmd ? 1 : 0);
@@ -74,6 +91,9 @@ By Residual Labs — residual-labs.fr`);
74
91
  console.error(`\n✗ ${err?.message ?? err}`);
75
92
  if (err?.hint) console.error(` → ${err.hint}`);
76
93
  if (opts.verbose && err?.stack) console.error(err.stack);
77
- else console.error(' (run with --verbose for details — or open an issue: github.com/zyx77550/sparda/issues)');
94
+ else
95
+ console.error(
96
+ ' (run with --verbose for details — or open an issue: github.com/zyx77550/sparda/issues)',
97
+ );
78
98
  process.exit(err?.code === 'USER' ? 1 : 2);
79
99
  }