sapper-ai 0.5.0 → 0.6.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/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAaA,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAiCpF"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAkBA,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAuFpF"}
package/dist/cli.js CHANGED
@@ -45,7 +45,12 @@ const node_path_1 = require("node:path");
45
45
  const readline = __importStar(require("node:readline"));
46
46
  const select_1 = __importDefault(require("@inquirer/select"));
47
47
  const presets_1 = require("./presets");
48
+ const policyYaml_1 = require("./policyYaml");
49
+ const harden_1 = require("./harden");
50
+ const quarantine_1 = require("./quarantine");
51
+ const wrapConfig_1 = require("./mcp/wrapConfig");
48
52
  const scan_1 = require("./scan");
53
+ const env_1 = require("./utils/env");
49
54
  async function runCli(argv = process.argv.slice(2)) {
50
55
  if (argv[0] === '--help' || argv[0] === '-h') {
51
56
  printUsage();
@@ -62,7 +67,49 @@ async function runCli(argv = process.argv.slice(2)) {
62
67
  printUsage();
63
68
  return 1;
64
69
  }
65
- return (0, scan_1.runScan)(scanOptions);
70
+ const scanExitCode = await (0, scan_1.runScan)(scanOptions);
71
+ const shouldOfferHarden = parsed.noPrompt !== true &&
72
+ process.stdout.isTTY === true &&
73
+ process.stdin.isTTY === true &&
74
+ (0, env_1.isCiEnv)(process.env) !== true &&
75
+ (parsed.harden === true || (await (0, harden_1.getHardenPlanSummary)({ includeSystem: true })).actions.length > 0);
76
+ if (shouldOfferHarden) {
77
+ const hardenExitCode = await (0, harden_1.runHarden)({
78
+ apply: true,
79
+ includeSystem: true,
80
+ });
81
+ if (scanExitCode === 0 && hardenExitCode !== 0) {
82
+ return hardenExitCode;
83
+ }
84
+ }
85
+ return scanExitCode;
86
+ }
87
+ if (argv[0] === 'harden') {
88
+ const parsed = parseHardenArgs(argv.slice(1));
89
+ if (!parsed) {
90
+ printUsage();
91
+ return 1;
92
+ }
93
+ return (0, harden_1.runHarden)(parsed);
94
+ }
95
+ if (argv[0] === 'mcp') {
96
+ const parsed = parseMcpArgs(argv.slice(1));
97
+ if (!parsed) {
98
+ printUsage();
99
+ return 1;
100
+ }
101
+ return runMcpCommand(parsed);
102
+ }
103
+ if (argv[0] === 'quarantine') {
104
+ const parsed = parseQuarantineArgs(argv.slice(1));
105
+ if (!parsed) {
106
+ printUsage();
107
+ return 1;
108
+ }
109
+ if (parsed.command === 'quarantine_list') {
110
+ return (0, quarantine_1.runQuarantineList)({ quarantineDir: parsed.quarantineDir });
111
+ }
112
+ return (0, quarantine_1.runQuarantineRestore)({ id: parsed.id, quarantineDir: parsed.quarantineDir, force: parsed.force });
66
113
  }
67
114
  if (argv[0] === 'dashboard') {
68
115
  return runDashboard();
@@ -84,10 +131,20 @@ Usage:
84
131
  sapper-ai scan --deep Current directory + subdirectories
85
132
  sapper-ai scan --system AI system paths (~/.claude, ~/.cursor, ...)
86
133
  sapper-ai scan ./path Scan a specific file/directory
134
+ sapper-ai scan --policy ./sapperai.config.yaml Use explicit policy path (fatal if invalid)
87
135
  sapper-ai scan --fix Quarantine blocked files
88
136
  sapper-ai scan --ai Deep scan with AI analysis (requires OPENAI_API_KEY)
137
+ sapper-ai scan --no-prompt Disable all prompts (CI-safe)
138
+ sapper-ai scan --harden After scan, offer to apply recommended hardening
89
139
  sapper-ai scan --no-open Skip opening report in browser
90
140
  sapper-ai scan --no-save Skip saving scan results to ~/.sapperai/scans/
141
+ sapper-ai harden Plan recommended setup changes (no writes)
142
+ sapper-ai harden --apply Apply recommended project changes
143
+ sapper-ai harden --include-system Include system changes (home directory)
144
+ sapper-ai mcp wrap-config Wrap MCP servers to run behind sapperai-proxy (defaults to Claude Code config)
145
+ sapper-ai mcp unwrap-config Undo MCP wrapping
146
+ sapper-ai quarantine list List quarantined files
147
+ sapper-ai quarantine restore <id> [--force] Restore quarantined file by id
91
148
  sapper-ai init Interactive setup wizard
92
149
  sapper-ai dashboard Launch web dashboard
93
150
  sapper-ai --help Show this help
@@ -97,17 +154,30 @@ Learn more: https://github.com/sapper-ai/sapperai
97
154
  }
98
155
  function parseScanArgs(argv) {
99
156
  const targets = [];
157
+ let policyPath;
100
158
  let fix = false;
101
159
  let deep = false;
102
160
  let system = false;
103
161
  let ai = false;
104
162
  let noSave = false;
105
163
  let noOpen = false;
106
- for (const arg of argv) {
164
+ let noPrompt = false;
165
+ let harden = false;
166
+ for (let index = 0; index < argv.length; index += 1) {
167
+ const arg = argv[index];
168
+ const nextArg = argv[index + 1];
107
169
  if (arg === '--fix') {
108
170
  fix = true;
109
171
  continue;
110
172
  }
173
+ if (arg === '--policy') {
174
+ if (!nextArg || nextArg.startsWith('-')) {
175
+ return null;
176
+ }
177
+ policyPath = nextArg;
178
+ index += 1;
179
+ continue;
180
+ }
111
181
  if (arg === '--deep') {
112
182
  deep = true;
113
183
  continue;
@@ -120,6 +190,14 @@ function parseScanArgs(argv) {
120
190
  ai = true;
121
191
  continue;
122
192
  }
193
+ if (arg === '--no-prompt') {
194
+ noPrompt = true;
195
+ continue;
196
+ }
197
+ if (arg === '--harden') {
198
+ harden = true;
199
+ continue;
200
+ }
123
201
  if (arg === '--no-open') {
124
202
  noOpen = true;
125
203
  continue;
@@ -133,7 +211,221 @@ function parseScanArgs(argv) {
133
211
  }
134
212
  targets.push(arg);
135
213
  }
136
- return { targets, fix, deep, system, ai, noSave, noOpen };
214
+ return { targets, policyPath, fix, deep, system, ai, noSave, noOpen, noPrompt, harden };
215
+ }
216
+ function parseHardenArgs(argv) {
217
+ let apply = false;
218
+ let includeSystem = false;
219
+ let yes = false;
220
+ let noPrompt = false;
221
+ let force = false;
222
+ let workflowVersion;
223
+ let mcpVersion;
224
+ for (let index = 0; index < argv.length; index += 1) {
225
+ const arg = argv[index];
226
+ const nextArg = argv[index + 1];
227
+ if (arg === '--dry-run') {
228
+ apply = false;
229
+ continue;
230
+ }
231
+ if (arg === '--apply') {
232
+ apply = true;
233
+ continue;
234
+ }
235
+ if (arg === '--include-system') {
236
+ includeSystem = true;
237
+ continue;
238
+ }
239
+ if (arg === '--yes') {
240
+ yes = true;
241
+ continue;
242
+ }
243
+ if (arg === '--no-prompt') {
244
+ noPrompt = true;
245
+ continue;
246
+ }
247
+ if (arg === '--force') {
248
+ force = true;
249
+ continue;
250
+ }
251
+ if (arg === '--workflow-version') {
252
+ if (!nextArg || nextArg.startsWith('-'))
253
+ return null;
254
+ workflowVersion = nextArg;
255
+ index += 1;
256
+ continue;
257
+ }
258
+ if (arg === '--mcp-version') {
259
+ if (!nextArg || nextArg.startsWith('-'))
260
+ return null;
261
+ mcpVersion = nextArg;
262
+ index += 1;
263
+ continue;
264
+ }
265
+ return null;
266
+ }
267
+ return {
268
+ apply,
269
+ includeSystem,
270
+ yes,
271
+ noPrompt,
272
+ force,
273
+ workflowVersion,
274
+ mcpVersion,
275
+ };
276
+ }
277
+ function parseMcpArgs(argv) {
278
+ const subcommand = argv[0];
279
+ const rest = argv.slice(1);
280
+ if (!subcommand)
281
+ return null;
282
+ const defaultConfigPath = (0, node_path_1.join)((0, node_os_1.homedir)(), '.config', 'claude-code', 'config.json');
283
+ let configPath = defaultConfigPath;
284
+ let format = 'jsonc';
285
+ let dryRun = false;
286
+ let mcpVersion;
287
+ for (let index = 0; index < rest.length; index += 1) {
288
+ const arg = rest[index];
289
+ const nextArg = rest[index + 1];
290
+ if (arg === '--config') {
291
+ if (!nextArg || nextArg.startsWith('-'))
292
+ return null;
293
+ configPath = nextArg;
294
+ format = 'json';
295
+ index += 1;
296
+ continue;
297
+ }
298
+ if (arg === '--jsonc') {
299
+ format = 'jsonc';
300
+ continue;
301
+ }
302
+ if (arg === '--dry-run') {
303
+ dryRun = true;
304
+ continue;
305
+ }
306
+ if (arg === '--mcp-version') {
307
+ if (!nextArg || nextArg.startsWith('-'))
308
+ return null;
309
+ mcpVersion = nextArg;
310
+ index += 1;
311
+ continue;
312
+ }
313
+ return null;
314
+ }
315
+ if (subcommand === 'wrap-config') {
316
+ return { command: 'mcp_wrap_config', configPath, format, dryRun, mcpVersion };
317
+ }
318
+ if (subcommand === 'unwrap-config') {
319
+ return { command: 'mcp_unwrap_config', configPath, format, dryRun };
320
+ }
321
+ return null;
322
+ }
323
+ async function runMcpCommand(args) {
324
+ if (args.command === 'mcp_unwrap_config') {
325
+ const result = await (0, wrapConfig_1.unwrapMcpConfigFile)({
326
+ filePath: args.configPath,
327
+ format: args.format,
328
+ dryRun: args.dryRun,
329
+ });
330
+ if (!result.changed) {
331
+ console.log('No changes needed.');
332
+ return 0;
333
+ }
334
+ if (result.restoredFromBackupPath) {
335
+ if (args.dryRun) {
336
+ console.log(`Config parse failed; would restore from backup: ${result.restoredFromBackupPath}`);
337
+ return 0;
338
+ }
339
+ console.log(`Config parse failed; restored from backup: ${result.restoredFromBackupPath}`);
340
+ if (result.backupPath)
341
+ console.log(`Backup: ${result.backupPath}`);
342
+ return 0;
343
+ }
344
+ if (args.dryRun) {
345
+ console.log(`Would unwrap ${result.changedServers.length} server(s): ${result.changedServers.join(', ')}`);
346
+ return 0;
347
+ }
348
+ console.log(`Unwrapped ${result.changedServers.length} server(s): ${result.changedServers.join(', ')}`);
349
+ if (result.backupPath)
350
+ console.log(`Backup: ${result.backupPath}`);
351
+ return 0;
352
+ }
353
+ if (!(0, wrapConfig_1.checkNpxAvailable)()) {
354
+ console.error("npx is not available on PATH. Install Node.js/npm and retry.");
355
+ return 1;
356
+ }
357
+ const envVersion = process.env.SAPPERAI_MCP_VERSION?.trim();
358
+ const installedVersion = (0, wrapConfig_1.resolveInstalledPackageVersion)('@sapper-ai/mcp');
359
+ const mcpVersion = (args.mcpVersion ?? envVersion ?? installedVersion ?? '').trim();
360
+ if (!mcpVersion) {
361
+ console.error("Missing MCP version. Provide '--mcp-version <semver>' or install @sapper-ai/mcp.");
362
+ return 1;
363
+ }
364
+ const result = await (0, wrapConfig_1.wrapMcpConfigFile)({
365
+ filePath: args.configPath,
366
+ mcpVersion,
367
+ format: args.format,
368
+ dryRun: args.dryRun,
369
+ });
370
+ if (!result.changed) {
371
+ console.log('No changes needed.');
372
+ return 0;
373
+ }
374
+ if (args.dryRun) {
375
+ console.log(`Would wrap ${result.changedServers.length} server(s): ${result.changedServers.join(', ')}`);
376
+ return 0;
377
+ }
378
+ console.log(`Wrapped ${result.changedServers.length} server(s): ${result.changedServers.join(', ')}`);
379
+ if (result.backupPath)
380
+ console.log(`Backup: ${result.backupPath}`);
381
+ return 0;
382
+ }
383
+ function parseQuarantineArgs(argv) {
384
+ const subcommand = argv[0];
385
+ const rest = argv.slice(1);
386
+ if (!subcommand)
387
+ return null;
388
+ let quarantineDir;
389
+ let force = false;
390
+ if (subcommand === 'list') {
391
+ for (let index = 0; index < rest.length; index += 1) {
392
+ const arg = rest[index];
393
+ const nextArg = rest[index + 1];
394
+ if (arg === '--quarantine-dir') {
395
+ if (!nextArg || nextArg.startsWith('-'))
396
+ return null;
397
+ quarantineDir = nextArg;
398
+ index += 1;
399
+ continue;
400
+ }
401
+ return null;
402
+ }
403
+ return { command: 'quarantine_list', quarantineDir };
404
+ }
405
+ if (subcommand === 'restore') {
406
+ const id = rest[0];
407
+ if (!id)
408
+ return null;
409
+ const tail = rest.slice(1);
410
+ for (let index = 0; index < tail.length; index += 1) {
411
+ const arg = tail[index];
412
+ const nextArg = tail[index + 1];
413
+ if (arg === '--force') {
414
+ force = true;
415
+ continue;
416
+ }
417
+ if (arg === '--quarantine-dir') {
418
+ if (!nextArg || nextArg.startsWith('-'))
419
+ return null;
420
+ quarantineDir = nextArg;
421
+ index += 1;
422
+ continue;
423
+ }
424
+ return null;
425
+ }
426
+ return { command: 'quarantine_restore', id, quarantineDir, force };
427
+ }
428
+ return null;
137
429
  }
138
430
  function displayPath(path) {
139
431
  const home = (0, node_os_1.homedir)();
@@ -176,6 +468,7 @@ async function resolveScanOptions(args) {
176
468
  fix: args.fix,
177
469
  noSave: args.noSave,
178
470
  noOpen: args.noOpen,
471
+ policyPath: args.policyPath,
179
472
  };
180
473
  if (args.system) {
181
474
  if (args.targets.length > 0) {
@@ -204,8 +497,8 @@ async function resolveScanOptions(args) {
204
497
  if (args.deep) {
205
498
  return { ...common, targets: [cwd], deep: true, ai: args.ai, scopeLabel: 'Current + subdirectories' };
206
499
  }
207
- if (process.stdout.isTTY !== true) {
208
- return { ...common, targets: [cwd], deep: true, ai: false, scopeLabel: 'Current + subdirectories' };
500
+ if (args.noPrompt === true || process.stdout.isTTY !== true) {
501
+ return { ...common, targets: [cwd], deep: true, ai: args.ai, scopeLabel: 'Current + subdirectories' };
209
502
  }
210
503
  const scope = await promptScanScope(cwd);
211
504
  const ai = args.ai ? true : await promptScanDepth();
@@ -285,9 +578,9 @@ async function runInitWizard() {
285
578
  '# Generated by: sapper-ai init',
286
579
  '# Docs: https://github.com/sapper-ai/sapperai',
287
580
  '',
288
- ...buildPolicyYaml(selectedPreset, auditLogPath),
289
581
  ];
290
- (0, node_fs_1.writeFileSync)(outputPath, `${lines.join('\n')}\n`, 'utf8');
582
+ const body = (0, policyYaml_1.renderPolicyYaml)(selectedPreset, auditLogPath);
583
+ (0, node_fs_1.writeFileSync)(outputPath, `${lines.join('\n')}\n${body}`, 'utf8');
291
584
  console.log(`\n Created ${outputPath}\n`);
292
585
  console.log(' Quick start:\n');
293
586
  console.log(" import { createGuard } from 'sapper-ai'");
@@ -296,29 +589,6 @@ async function runInitWizard() {
296
589
  console.log();
297
590
  rl.close();
298
591
  }
299
- function buildPolicyYaml(preset, auditLogPath) {
300
- const p = presets_1.presets[preset].policy;
301
- const lines = [];
302
- lines.push(`mode: ${p.mode}`);
303
- lines.push(`defaultAction: ${p.defaultAction}`);
304
- lines.push(`failOpen: ${p.failOpen}`);
305
- lines.push('');
306
- lines.push('detectors:');
307
- const detectors = p.detectors ?? ['rules'];
308
- for (const d of detectors) {
309
- lines.push(` - ${d}`);
310
- }
311
- lines.push('');
312
- lines.push('thresholds:');
313
- const thresholds = p.thresholds ?? {};
314
- lines.push(` riskThreshold: ${thresholds.riskThreshold ?? 0.7}`);
315
- lines.push(` blockMinConfidence: ${thresholds.blockMinConfidence ?? 0.5}`);
316
- if (auditLogPath) {
317
- lines.push('');
318
- lines.push(`auditLogPath: ${auditLogPath}`);
319
- }
320
- return lines;
321
- }
322
592
  function isDirectExecution(argv) {
323
593
  const entry = argv[1];
324
594
  if (!entry) {
@@ -0,0 +1,28 @@
1
+ type HardenScope = 'project' | 'system';
2
+ export interface HardenPlanSummary {
3
+ repoRoot: string;
4
+ notes: string[];
5
+ actions: Array<{
6
+ id: string;
7
+ scope: HardenScope;
8
+ title: string;
9
+ paths: string[];
10
+ }>;
11
+ }
12
+ export interface HardenOptions {
13
+ cwd?: string;
14
+ env?: NodeJS.ProcessEnv;
15
+ includeSystem?: boolean;
16
+ dryRun?: boolean;
17
+ apply?: boolean;
18
+ yes?: boolean;
19
+ noPrompt?: boolean;
20
+ force?: boolean;
21
+ workflowVersion?: string;
22
+ mcpVersion?: string;
23
+ write?: (text: string) => void;
24
+ }
25
+ export declare function getHardenPlanSummary(options?: HardenOptions): Promise<HardenPlanSummary>;
26
+ export declare function runHarden(options?: HardenOptions): Promise<number>;
27
+ export {};
28
+ //# sourceMappingURL=harden.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harden.d.ts","sourceRoot":"","sources":["../src/harden.ts"],"names":[],"mappings":"AAeA,KAAK,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAA;AAUvC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,KAAK,CAAC;QACb,EAAE,EAAE,MAAM,CAAA;QACV,KAAK,EAAE,WAAW,CAAA;QAClB,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,EAAE,CAAA;KAChB,CAAC,CAAA;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AAoLD,wBAAsB,oBAAoB,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAOlG;AAED,wBAAsB,SAAS,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgG5E"}