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/scan.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export interface ScanOptions {
2
2
  targets?: string[];
3
+ policyPath?: string;
3
4
  fix?: boolean;
4
5
  deep?: boolean;
5
6
  system?: boolean;
@@ -14,10 +15,16 @@ export interface ScanResult {
14
15
  scope: string;
15
16
  target: string;
16
17
  ai: boolean;
18
+ filters: {
19
+ configLikeOnly: boolean;
20
+ };
17
21
  summary: {
18
22
  totalFiles: number;
23
+ eligibleFiles: number;
19
24
  scannedFiles: number;
20
25
  skippedFiles: number;
26
+ skippedNotEligible: number;
27
+ skippedEmptyOrUnreadable: number;
21
28
  threats: number;
22
29
  };
23
30
  findings: Array<{
@@ -30,6 +37,12 @@ export interface ScanResult {
30
37
  snippet: string;
31
38
  detectors: string[];
32
39
  aiAnalysis: string | null;
40
+ ruleMatches: Array<{
41
+ label: string;
42
+ severity: 'high' | 'medium';
43
+ matchText: string;
44
+ context: string;
45
+ }>;
33
46
  }>;
34
47
  }
35
48
  export declare function runScan(options?: ScanOptions): Promise<number>;
@@ -1 +1 @@
1
- {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAeD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAA;QAClB,YAAY,EAAE,MAAM,CAAA;QACpB,YAAY,EAAE,MAAM,CAAA;QACpB,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,QAAQ,EAAE,KAAK,CAAC;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,UAAU,EAAE,MAAM,CAAA;QAClB,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,OAAO,EAAE,MAAM,EAAE,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;QACf,SAAS,EAAE,MAAM,EAAE,CAAA;QACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAC1B,CAAC,CAAA;CACH;AAqYD,wBAAsB,OAAO,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyPxE"}
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAgBD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,EAAE;QACP,cAAc,EAAE,OAAO,CAAA;KACxB,CAAA;IACD,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAA;QAClB,aAAa,EAAE,MAAM,CAAA;QACrB,YAAY,EAAE,MAAM,CAAA;QACpB,YAAY,EAAE,MAAM,CAAA;QACpB,kBAAkB,EAAE,MAAM,CAAA;QAC1B,wBAAwB,EAAE,MAAM,CAAA;QAChC,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,QAAQ,EAAE,KAAK,CAAC;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,UAAU,EAAE,MAAM,CAAA;QAClB,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,OAAO,EAAE,MAAM,EAAE,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;QACf,SAAS,EAAE,MAAM,EAAE,CAAA;QACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,WAAW,EAAE,KAAK,CAAC;YACjB,KAAK,EAAE,MAAM,CAAA;YACb,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAA;YAC3B,SAAS,EAAE,MAAM,CAAA;YACjB,OAAO,EAAE,MAAM,CAAA;SAChB,CAAC,CAAA;KACH,CAAC,CAAA;CACH;AAgcD,wBAAsB,OAAO,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkRxE"}
package/dist/scan.js CHANGED
@@ -34,13 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runScan = runScan;
37
- const node_fs_1 = require("node:fs");
38
37
  const promises_1 = require("node:fs/promises");
39
38
  const node_os_1 = require("node:os");
40
39
  const node_path_1 = require("node:path");
41
40
  const core_1 = require("@sapper-ai/core");
42
41
  const presets_1 = require("./presets");
43
- const CONFIG_FILE_NAMES = ['sapperai.config.yaml', 'sapperai.config.yml'];
42
+ const repoRoot_1 = require("./utils/repoRoot");
44
43
  const GREEN = '\x1b[32m';
45
44
  const YELLOW = '\x1b[33m';
46
45
  const RED = '\x1b[31m';
@@ -55,21 +54,18 @@ const SYSTEM_SCAN_PATHS = (() => {
55
54
  (0, node_path_1.join)(home, 'Library', 'Application Support', 'Claude'),
56
55
  ];
57
56
  })();
58
- function findConfigFile(cwd) {
59
- for (const name of CONFIG_FILE_NAMES) {
60
- const fullPath = (0, node_path_1.resolve)(cwd, name);
61
- if ((0, node_fs_1.existsSync)(fullPath)) {
62
- return fullPath;
63
- }
57
+ function resolvePolicy(cwd, options) {
58
+ const manager = new core_1.PolicyManager();
59
+ const explicitPath = options.policyPath ?? process.env.SAPPERAI_POLICY_PATH;
60
+ if (explicitPath) {
61
+ return manager.loadFromFile(explicitPath);
64
62
  }
65
- return null;
66
- }
67
- function resolvePolicy(cwd) {
68
- const configPath = findConfigFile(cwd);
69
- if (!configPath) {
63
+ const repoRoot = (0, repoRoot_1.findRepoRoot)(cwd);
64
+ const resolved = (0, core_1.resolvePolicyPath)({ repoRoot, homeDir: (0, node_os_1.homedir)() });
65
+ if (!resolved) {
70
66
  return { ...presets_1.presets.standard.policy };
71
67
  }
72
- return new core_1.PolicyManager().loadFromFile(configPath);
68
+ return manager.loadFromFile(resolved.path);
73
69
  }
74
70
  function getThresholds(policy) {
75
71
  const extended = policy;
@@ -231,11 +227,11 @@ async function readFileIfPresent(filePath) {
231
227
  }
232
228
  async function scanFile(filePath, policy, scanner, detectors, fix, quarantineManager) {
233
229
  if (!(0, core_1.isConfigLikeFile)(filePath)) {
234
- return { scanned: false };
230
+ return { scanned: false, skipReason: 'not_eligible' };
235
231
  }
236
232
  const raw = await readFileIfPresent(filePath);
237
233
  if (raw === null || raw.trim().length === 0) {
238
- return { scanned: false };
234
+ return { scanned: false, skipReason: 'empty_or_unreadable' };
239
235
  }
240
236
  const fileSurface = (0, core_1.normalizeSurfaceText)(raw);
241
237
  const targetType = (0, core_1.classifyTargetType)(filePath);
@@ -243,6 +239,11 @@ async function scanFile(filePath, policy, scanner, detectors, fix, quarantineMan
243
239
  {
244
240
  id: `${targetType}:${(0, core_1.buildEntryName)(filePath)}`,
245
241
  surface: fileSurface,
242
+ meta: {
243
+ scanSource: 'file_surface',
244
+ sourcePath: filePath,
245
+ sourceType: targetType,
246
+ },
246
247
  },
247
248
  ];
248
249
  if (filePath.endsWith('.json')) {
@@ -250,7 +251,15 @@ async function scanFile(filePath, policy, scanner, detectors, fix, quarantineMan
250
251
  const parsed = JSON.parse(raw);
251
252
  const mcpTargets = (0, core_1.collectMcpTargetsFromJson)(filePath, parsed);
252
253
  for (const t of mcpTargets) {
253
- targets.push({ id: `${t.type}:${t.name}`, surface: t.surface });
254
+ targets.push({
255
+ id: `${t.type}:${t.name}`,
256
+ surface: t.surface,
257
+ meta: {
258
+ scanSource: 'file_surface',
259
+ sourcePath: t.source,
260
+ sourceType: t.type,
261
+ },
262
+ });
254
263
  }
255
264
  }
256
265
  catch {
@@ -259,7 +268,7 @@ async function scanFile(filePath, policy, scanner, detectors, fix, quarantineMan
259
268
  let bestDecision = null;
260
269
  let bestThreat = null;
261
270
  for (const target of targets) {
262
- const decision = await scanner.scanTool(target.id, target.surface, policy, detectors);
271
+ const decision = await scanner.scanTool(target.id, target.surface, policy, detectors, target.meta);
263
272
  if (!bestDecision || decision.risk > bestDecision.risk) {
264
273
  bestDecision = decision;
265
274
  }
@@ -299,6 +308,37 @@ function extractPatternsFromReasons(reasons) {
299
308
  function toDetectorsList(decision) {
300
309
  return uniq(decision.evidence.map((e) => e.detectorId));
301
310
  }
311
+ function extractRuleMatches(decision) {
312
+ const evidence = Array.isArray(decision.evidence) ? decision.evidence : [];
313
+ const output = evidence.find((e) => e && e.detectorId === 'rules');
314
+ if (!output || !output.evidence || typeof output.evidence !== 'object') {
315
+ return [];
316
+ }
317
+ const maybeMatches = output.evidence.matches;
318
+ if (!Array.isArray(maybeMatches)) {
319
+ return [];
320
+ }
321
+ const results = [];
322
+ for (const m of maybeMatches) {
323
+ if (!m || typeof m !== 'object')
324
+ continue;
325
+ const match = m;
326
+ const label = typeof match.label === 'string' ? match.label : '';
327
+ const severity = match.severity === 'high' ? 'high' : 'medium';
328
+ const matchText = typeof match.matchText === 'string' ? match.matchText : '';
329
+ const context = typeof match.context === 'string'
330
+ ? match.context
331
+ : typeof match.sample === 'string'
332
+ ? match.sample
333
+ : '';
334
+ if (!label || !matchText)
335
+ continue;
336
+ results.push({ label, severity, matchText, context });
337
+ if (results.length >= 24)
338
+ break;
339
+ }
340
+ return results;
341
+ }
302
342
  function truncateSnippet(text, maxChars) {
303
343
  if (text.length <= maxChars) {
304
344
  return text;
@@ -313,6 +353,7 @@ async function buildScanResult(params) {
313
353
  const reasons = f.decision.reasons;
314
354
  const patterns = extractPatternsFromReasons(reasons);
315
355
  const detectors = toDetectorsList(f.decision);
356
+ const ruleMatches = extractRuleMatches(f.decision);
316
357
  return {
317
358
  filePath: f.filePath,
318
359
  risk: f.decision.risk,
@@ -323,6 +364,7 @@ async function buildScanResult(params) {
323
364
  snippet,
324
365
  detectors,
325
366
  aiAnalysis: f.aiAnalysis ?? null,
367
+ ruleMatches,
326
368
  };
327
369
  }));
328
370
  return {
@@ -331,10 +373,16 @@ async function buildScanResult(params) {
331
373
  scope: params.scope,
332
374
  target: params.target,
333
375
  ai: params.ai,
376
+ filters: {
377
+ configLikeOnly: true,
378
+ },
334
379
  summary: {
335
380
  totalFiles: params.totalFiles,
381
+ eligibleFiles: params.eligibleFiles,
336
382
  scannedFiles: params.scannedFiles,
337
383
  skippedFiles: params.skippedFiles,
384
+ skippedNotEligible: params.skippedNotEligible,
385
+ skippedEmptyOrUnreadable: params.skippedEmptyOrUnreadable,
338
386
  threats: params.threats,
339
387
  },
340
388
  findings,
@@ -342,7 +390,7 @@ async function buildScanResult(params) {
342
390
  }
343
391
  async function runScan(options = {}) {
344
392
  const cwd = process.cwd();
345
- const policy = resolvePolicy(cwd);
393
+ const policy = resolvePolicy(cwd, { policyPath: options.policyPath });
346
394
  const fix = options.fix === true;
347
395
  const aiEnabled = options.ai === true;
348
396
  let llmConfig = null;
@@ -384,6 +432,8 @@ async function runScan(options = {}) {
384
432
  }
385
433
  const files = Array.from(fileSet).sort();
386
434
  console.log(` Collecting files... ${files.length} files found`);
435
+ const eligibleByName = files.filter((f) => (0, core_1.isConfigLikeFile)(f)).length;
436
+ console.log(` Filter: config-like only (${eligibleByName} eligible / ${files.length} total)`);
387
437
  console.log();
388
438
  if (aiEnabled) {
389
439
  console.log(' Phase 1/2: Rules scan');
@@ -391,6 +441,9 @@ async function runScan(options = {}) {
391
441
  }
392
442
  const scannedFindings = [];
393
443
  let scannedFiles = 0;
444
+ let eligibleFiles = 0;
445
+ let skippedNotEligible = 0;
446
+ let skippedEmptyOrUnreadable = 0;
394
447
  const total = files.length;
395
448
  const progressWidth = Math.max(10, Math.min(30, (process.stdout.columns ?? 80) - 30));
396
449
  for (let i = 0; i < files.length; i += 1) {
@@ -408,6 +461,15 @@ async function runScan(options = {}) {
408
461
  }
409
462
  }
410
463
  const result = await scanFile(filePath, policy, scanner, detectors, fix, quarantineManager);
464
+ if (result.skipReason === 'not_eligible') {
465
+ skippedNotEligible += 1;
466
+ continue;
467
+ }
468
+ eligibleFiles += 1;
469
+ if (result.skipReason === 'empty_or_unreadable') {
470
+ skippedEmptyOrUnreadable += 1;
471
+ continue;
472
+ }
411
473
  if (result.scanned && result.decision) {
412
474
  scannedFiles += 1;
413
475
  scannedFindings.push({ filePath, decision: result.decision, quarantinedId: result.quarantinedId });
@@ -454,7 +516,11 @@ async function runScan(options = {}) {
454
516
  const surface = (0, core_1.normalizeSurfaceText)(raw);
455
517
  const targetType = (0, core_1.classifyTargetType)(finding.filePath);
456
518
  const id = `${targetType}:${(0, core_1.buildEntryName)(finding.filePath)}`;
457
- const aiDecision = await scanner.scanTool(id, surface, aiPolicy, aiDetectors);
519
+ const aiDecision = await scanner.scanTool(id, surface, aiPolicy, aiDetectors, {
520
+ scanSource: 'file_surface',
521
+ sourcePath: finding.filePath,
522
+ sourceType: targetType,
523
+ });
458
524
  const mergedReasons = uniq([...finding.decision.reasons, ...aiDecision.reasons]);
459
525
  const existingEvidence = finding.decision.evidence;
460
526
  const mergedEvidence = [...existingEvidence];
@@ -490,15 +556,18 @@ async function runScan(options = {}) {
490
556
  }
491
557
  }
492
558
  }
493
- const skippedFiles = total - scannedFiles;
559
+ const skippedFiles = skippedNotEligible + skippedEmptyOrUnreadable;
494
560
  const threats = scannedFindings.filter((f) => isThreat(f.decision, policy));
495
561
  const scanResult = await buildScanResult({
496
562
  scope: scopeLabel,
497
563
  target: targets.join(', '),
498
564
  ai: aiEnabled,
499
565
  totalFiles: total,
566
+ eligibleFiles,
500
567
  scannedFiles,
501
568
  skippedFiles,
569
+ skippedNotEligible,
570
+ skippedEmptyOrUnreadable,
502
571
  threats: threats.length,
503
572
  findings: scannedFindings,
504
573
  });
@@ -531,12 +600,12 @@ async function runScan(options = {}) {
531
600
  }
532
601
  }
533
602
  if (threats.length === 0) {
534
- const msg = ` ✓ All clear — ${scannedFiles} files scanned, 0 threats detected`;
603
+ const msg = ` ✓ All clear — ${scannedFiles}/${eligibleFiles} eligible files scanned, 0 threats detected (${total} total files)`;
535
604
  console.log(color ? `${GREEN}${msg}${RESET}` : msg);
536
605
  console.log();
537
606
  }
538
607
  else {
539
- const warn = ` ⚠ ${scannedFiles} files scanned, ${threats.length} threats detected`;
608
+ const warn = ` ⚠ ${scannedFiles}/${eligibleFiles} eligible files scanned, ${threats.length} threats detected (${total} total files)`;
540
609
  console.log(color ? `${RED}${warn}${RESET}` : warn);
541
610
  console.log();
542
611
  const tableLines = renderFindingsTable(threats, {
@@ -0,0 +1,3 @@
1
+ export declare function parseBoolean(value: string | undefined, fallback: boolean): boolean;
2
+ export declare function isCiEnv(env?: NodeJS.ProcessEnv): boolean;
3
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAQlF;AAED,wBAAgB,OAAO,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAYrE"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseBoolean = parseBoolean;
4
+ exports.isCiEnv = isCiEnv;
5
+ function parseBoolean(value, fallback) {
6
+ if (value === undefined)
7
+ return fallback;
8
+ const normalized = value.trim().toLowerCase();
9
+ if (normalized === 'true' || normalized === '1' || normalized === 'yes')
10
+ return true;
11
+ if (normalized === 'false' || normalized === '0' || normalized === 'no')
12
+ return false;
13
+ return fallback;
14
+ }
15
+ function isCiEnv(env = process.env) {
16
+ // CI providers generally set CI=true; also treat GitHub Actions as CI even if CI is unset.
17
+ const ci = env.CI;
18
+ if (ci && ci.trim().length > 0 && ci !== '0' && ci.toLowerCase() !== 'false') {
19
+ return true;
20
+ }
21
+ if (env.GITHUB_ACTIONS && env.GITHUB_ACTIONS !== 'false') {
22
+ return true;
23
+ }
24
+ return false;
25
+ }
@@ -0,0 +1,5 @@
1
+ export declare function readFileIfExists(filePath: string): Promise<string | null>;
2
+ export declare function ensureDir(dirPath: string): Promise<void>;
3
+ export declare function backupFile(originalPath: string): Promise<string>;
4
+ export declare function atomicWriteFile(filePath: string, content: string): Promise<void>;
5
+ //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/utils/fs.ts"],"names":[],"mappings":"AAIA,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAM/E;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAkBD,wBAAsB,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKtE;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAStF"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readFileIfExists = readFileIfExists;
4
+ exports.ensureDir = ensureDir;
5
+ exports.backupFile = backupFile;
6
+ exports.atomicWriteFile = atomicWriteFile;
7
+ const node_fs_1 = require("node:fs");
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_path_1 = require("node:path");
10
+ async function readFileIfExists(filePath) {
11
+ try {
12
+ return await (0, promises_1.readFile)(filePath, 'utf8');
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ async function ensureDir(dirPath) {
19
+ await (0, promises_1.mkdir)(dirPath, { recursive: true });
20
+ }
21
+ function nextBackupPath(originalPath) {
22
+ const first = `${originalPath}.bak`;
23
+ if (!(0, node_fs_1.existsSync)(first)) {
24
+ return first;
25
+ }
26
+ for (let i = 1; i < 1000; i += 1) {
27
+ const candidate = `${originalPath}.bak.${i}`;
28
+ if (!(0, node_fs_1.existsSync)(candidate)) {
29
+ return candidate;
30
+ }
31
+ }
32
+ throw new Error(`Unable to find free backup path for ${originalPath}`);
33
+ }
34
+ async function backupFile(originalPath) {
35
+ const backupPath = nextBackupPath(originalPath);
36
+ await ensureDir((0, node_path_1.dirname)(backupPath));
37
+ await (0, promises_1.copyFile)(originalPath, backupPath);
38
+ return backupPath;
39
+ }
40
+ async function atomicWriteFile(filePath, content) {
41
+ const dir = (0, node_path_1.dirname)(filePath);
42
+ await ensureDir(dir);
43
+ const tmpName = `.${(0, node_path_1.basename)(filePath)}.tmp.${process.pid}.${Date.now()}`;
44
+ const tmpPath = (0, node_path_1.join)(dir, tmpName);
45
+ await (0, promises_1.writeFile)(tmpPath, content, 'utf8');
46
+ await (0, promises_1.rename)(tmpPath, filePath);
47
+ }
@@ -0,0 +1,2 @@
1
+ export declare function findRepoRoot(startDir: string): string;
2
+ //# sourceMappingURL=repoRoot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repoRoot.d.ts","sourceRoot":"","sources":["../../src/utils/repoRoot.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiBrD"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findRepoRoot = findRepoRoot;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ function findRepoRoot(startDir) {
7
+ const start = (0, node_path_1.resolve)(startDir);
8
+ let current = start;
9
+ while (true) {
10
+ const gitPath = (0, node_path_1.resolve)(current, '.git');
11
+ if ((0, node_fs_1.existsSync)(gitPath)) {
12
+ return current;
13
+ }
14
+ const parent = (0, node_path_1.dirname)(current);
15
+ if (parent === current) {
16
+ return start;
17
+ }
18
+ current = parent;
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isSemver(version: string): boolean;
2
+ //# sourceMappingURL=semver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semver.d.ts","sourceRoot":"","sources":["../../src/utils/semver.ts"],"names":[],"mappings":"AAGA,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEjD"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSemver = isSemver;
4
+ const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
5
+ function isSemver(version) {
6
+ return SEMVER_RE.test(version.trim());
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-ai",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "AI security guardrails - single install, sensible defaults",
5
5
  "keywords": [
6
6
  "security",
@@ -40,17 +40,14 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@inquirer/select": "^4.0.0",
43
- "@sapper-ai/core": "0.2.1",
43
+ "@sapper-ai/core": "0.2.2",
44
+ "@sapper-ai/mcp": "0.3.0",
44
45
  "@sapper-ai/types": "0.2.1"
45
46
  },
46
47
  "peerDependencies": {
47
- "@sapper-ai/mcp": "^0.2.1",
48
- "@sapper-ai/openai": "^0.2.1"
48
+ "@sapper-ai/openai": "^0.2.2"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
- "@sapper-ai/mcp": {
52
- "optional": true
53
- },
54
51
  "@sapper-ai/openai": {
55
52
  "optional": true
56
53
  }