hackmyagent 0.17.0 → 0.17.2

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.
Files changed (52) hide show
  1. package/dist/.integrity-manifest.json +1 -1
  2. package/dist/checker/skill-identifier.d.ts.map +1 -1
  3. package/dist/checker/skill-identifier.js +4 -3
  4. package/dist/checker/skill-identifier.js.map +1 -1
  5. package/dist/cli.js +201 -58
  6. package/dist/cli.js.map +1 -1
  7. package/dist/hardening/index.d.ts +1 -1
  8. package/dist/hardening/index.d.ts.map +1 -1
  9. package/dist/hardening/index.js +2 -1
  10. package/dist/hardening/index.js.map +1 -1
  11. package/dist/hardening/scanner.d.ts +16 -0
  12. package/dist/hardening/scanner.d.ts.map +1 -1
  13. package/dist/hardening/scanner.js +28 -28
  14. package/dist/hardening/scanner.js.map +1 -1
  15. package/dist/hardening/taxonomy.d.ts +5 -0
  16. package/dist/hardening/taxonomy.d.ts.map +1 -1
  17. package/dist/hardening/taxonomy.js +66 -0
  18. package/dist/hardening/taxonomy.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/nanomind-core/analyzers/credential-analyzer.js +6 -0
  24. package/dist/nanomind-core/analyzers/credential-analyzer.js.map +1 -1
  25. package/dist/nanomind-core/compiler/semantic-compiler.js +7 -2
  26. package/dist/nanomind-core/compiler/semantic-compiler.js.map +1 -1
  27. package/dist/nanomind-core/index.d.ts +2 -0
  28. package/dist/nanomind-core/index.d.ts.map +1 -1
  29. package/dist/nanomind-core/index.js +12 -2
  30. package/dist/nanomind-core/index.js.map +1 -1
  31. package/dist/nanomind-core/inference/analm-infer.py +104 -0
  32. package/dist/nanomind-core/inference/security-analyst.d.ts +95 -0
  33. package/dist/nanomind-core/inference/security-analyst.d.ts.map +1 -0
  34. package/dist/nanomind-core/inference/security-analyst.js +372 -0
  35. package/dist/nanomind-core/inference/security-analyst.js.map +1 -0
  36. package/dist/nanomind-core/orchestrate.d.ts +7 -0
  37. package/dist/nanomind-core/orchestrate.d.ts.map +1 -1
  38. package/dist/nanomind-core/orchestrate.js +68 -2
  39. package/dist/nanomind-core/orchestrate.js.map +1 -1
  40. package/dist/nanomind-core/scanner-bridge.d.ts.map +1 -1
  41. package/dist/nanomind-core/scanner-bridge.js +33 -5
  42. package/dist/nanomind-core/scanner-bridge.js.map +1 -1
  43. package/dist/registry/client.d.ts +5 -0
  44. package/dist/registry/client.d.ts.map +1 -1
  45. package/dist/registry/client.js +10 -1
  46. package/dist/registry/client.js.map +1 -1
  47. package/dist/registry/publish.d.ts.map +1 -1
  48. package/dist/registry/publish.js +3 -4
  49. package/dist/registry/publish.js.map +1 -1
  50. package/dist/scanner/external-scanner.js +2 -2
  51. package/dist/scanner/external-scanner.js.map +1 -1
  52. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -267,9 +267,23 @@ Examples:
267
267
  return;
268
268
  }
269
269
  // npm package name: download, run full HMA scan, clean up
270
+ // On npm 404, fall through to skill check (skill identifiers look like @scope/name)
270
271
  if (looksLikeNpmPackage(skill)) {
271
- await checkNpmPackage(skill, options);
272
- return;
272
+ try {
273
+ await checkNpmPackage(skill, options);
274
+ return;
275
+ }
276
+ catch (npmErr) {
277
+ if (npmErr instanceof Error && npmErr.name === 'NpmNotFoundError') {
278
+ // Not on npm — fall through to skill check
279
+ if (!options.json && !globalCiMode) {
280
+ console.error(`Package "${skill}" not found on npm. Trying as skill identifier...`);
281
+ }
282
+ }
283
+ else {
284
+ throw npmErr; // Re-throw non-404 errors
285
+ }
286
+ }
273
287
  }
274
288
  // --rescan only applies to targets that otherwise hit the registry cache.
275
289
  // For skill identifiers we fall through to the registry lookup below.
@@ -494,22 +508,10 @@ function displayUnifiedCheck(opts) {
494
508
  guidance: f.guidance,
495
509
  attackClass: f.attackClass,
496
510
  }));
497
- // Score governance-only scans more fairly governance gaps aren't code vulns
498
- const hasCodeFindings = issues.some(f => {
499
- const cat = (f.category || '').toLowerCase();
500
- const id = f.checkId || '';
501
- return cat !== 'governance' && cat !== 'injection-hardening' && cat !== 'trust-hierarchy'
502
- && !id.startsWith('AST-GOV') && !id.startsWith('AST-GOVERN')
503
- && !id.startsWith('AST-PROMPT') && !id.startsWith('AST-HEARTBEAT');
504
- });
505
- if (hasCodeFindings) {
506
- score = critical > 0 ? Math.max(10, 100 - critical * 20 - high * 10 - medium * 5) : high > 0 ? Math.max(30, 100 - high * 10 - medium * 5) : Math.max(50, 100 - medium * 5 - low * 2);
507
- }
508
- else {
509
- // Governance-only: floor at 25, each finding costs less
510
- score = Math.max(25, 100 - critical * 8 - high * 5 - medium * 3 - low * 1);
511
- }
512
- maxScore = 100;
511
+ // Use the canonical scoring formula (exponential decay + 0.4x governance weight)
512
+ const scoreResult = (0, index_1.calculateSecurityScore)(issues);
513
+ score = scoreResult.score;
514
+ maxScore = scoreResult.maxScore;
513
515
  }
514
516
  else if (registry?.found) {
515
517
  score = Math.round(registry.trustScore * 100);
@@ -2266,6 +2268,7 @@ Examples:
2266
2268
  .option('-l, --level <level>', 'Benchmark level: L1 (Essential), L2 (Standard), L3 (Hardened)', 'L1')
2267
2269
  .option('-c, --category <name>', 'Filter to specific benchmark category')
2268
2270
  .option('--deep', 'Maximum analysis: static + semantic + behavioral simulation + adaptive attacks (~30s per file)')
2271
+ .option('--analm', 'AI-powered threat analysis using AnaLM (requires analm setup)')
2269
2272
  .option('--static-only', 'Disable semantic analysis and simulation (static checks only, fast, deterministic)')
2270
2273
  .option('--scan-depth <depth>', 'CAAT scan depth: quick (config+creds only), standard (default), deep (+ simulation)', 'standard')
2271
2274
  .option('--ci-publish', 'Submit scan results to registry CI endpoint (requires CI_SCAN_HMAC_SECRET env)')
@@ -2358,7 +2361,7 @@ Examples:
2358
2361
  catch { /* daemon not installed */ }
2359
2362
  }
2360
2363
  const onProgress = format === 'text'
2361
- ? (msg) => process.stdout.write(msg)
2364
+ ? (msg) => process.stdout.write(msg.endsWith('\n') ? msg : msg + '\n')
2362
2365
  : undefined;
2363
2366
  // Show analysis mode to user
2364
2367
  if (format === 'text') {
@@ -2394,15 +2397,16 @@ Examples:
2394
2397
  const scanDurationMs = Date.now() - scanStartMs;
2395
2398
  // NanoMind Semantic Compiler: AST-based analysis runs alongside static checks
2396
2399
  // Defense-in-depth: static findings can NEVER be suppressed, only upgraded
2400
+ const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
2401
+ const existingFindings = result.allFindings || result.findings || [];
2402
+ const nmResult = await orchestrateNanoMind(targetDir, existingFindings, {
2403
+ staticOnly: isStaticOnly,
2404
+ ci: options.ci,
2405
+ deep: isDeep,
2406
+ analm: options.analm,
2407
+ silent: format !== 'text',
2408
+ });
2397
2409
  {
2398
- const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
2399
- const existingFindings = result.allFindings || result.findings || [];
2400
- const nmResult = await orchestrateNanoMind(targetDir, existingFindings, {
2401
- staticOnly: isStaticOnly,
2402
- ci: options.ci,
2403
- deep: isDeep,
2404
- silent: format !== 'text',
2405
- });
2406
2410
  // Re-apply all filters after NanoMind merge (merge uses allFindings which is unfiltered)
2407
2411
  const refiltered = await scanner.reapplyIgnoreFilters(nmResult.mergedFindings, targetDir);
2408
2412
  if (result.allFindings) {
@@ -2591,7 +2595,10 @@ Examples:
2591
2595
  publishStatus = { success: false, error: msg };
2592
2596
  }
2593
2597
  }
2594
- const jsonOutput = publishStatus ? { ...result, publish: publishStatus } : result;
2598
+ const jsonBase = nmResult.analystFindings?.length
2599
+ ? { ...result, analystFindings: nmResult.analystFindings }
2600
+ : result;
2601
+ const jsonOutput = publishStatus ? { ...jsonBase, publish: publishStatus } : jsonBase;
2595
2602
  if (options.output) {
2596
2603
  require('fs').writeFileSync(options.output, JSON.stringify(jsonOutput, null, 2) + '\n');
2597
2604
  console.error(`Report written to ${options.output}`);
@@ -2746,6 +2753,42 @@ Examples:
2746
2753
  if (summaryParts.length > 0) {
2747
2754
  console.log(`${summaryParts.join(' | ')}\n`);
2748
2755
  }
2756
+ // Analyst findings (--analyze)
2757
+ if (nmResult.analystFindings && nmResult.analystFindings.length > 0) {
2758
+ console.log(`${colors.cyan}--- AnaLM Analysis ---${RESET()}\n`);
2759
+ for (const af of nmResult.analystFindings) {
2760
+ const r = af.result;
2761
+ if (af.taskType === 'threatAnalysis') {
2762
+ const level = String(r.threatLevel ?? 'unknown').toUpperCase();
2763
+ const levelColor = level === 'CRITICAL' || level === 'HIGH' ? colors.red : level === 'MEDIUM' ? colors.yellow : colors.dim;
2764
+ console.log(` ${levelColor}${level}${RESET()} ${r.attackVector ?? ''}`);
2765
+ if (r.description)
2766
+ console.log(` ${r.description}`);
2767
+ if (Array.isArray(r.mitigations) && r.mitigations.length > 0) {
2768
+ for (const m of r.mitigations) {
2769
+ console.log(` ${colors.cyan}Fix:${RESET()} ${m}`);
2770
+ }
2771
+ }
2772
+ }
2773
+ else if (af.taskType === 'credentialContextClassification') {
2774
+ const cls = String(r.classification ?? 'unknown');
2775
+ const clsColor = cls === 'real' ? colors.red : cls === 'test' || cls === 'example' ? colors.green : colors.yellow;
2776
+ console.log(` Credential: ${clsColor}${cls}${RESET()}`);
2777
+ if (r.reasoning)
2778
+ console.log(` ${r.reasoning}`);
2779
+ }
2780
+ else {
2781
+ // Generic display for other task types
2782
+ console.log(` ${af.taskType}: ${JSON.stringify(r)}`);
2783
+ }
2784
+ console.log(` ${colors.dim}Confidence: ${Math.round(af.confidence * 100)}% | ${af.modelVersion} (${af.durationMs}ms)${RESET()}`);
2785
+ console.log();
2786
+ }
2787
+ }
2788
+ // Analyst hint (shown when model is available but --analyze not used)
2789
+ if (nmResult.analystHint && issues.length > 0) {
2790
+ console.log(`${colors.dim}Tip: ${nmResult.analystHint}${RESET()}\n`);
2791
+ }
2749
2792
  // Dry-run summary
2750
2793
  if (result.dryRun) {
2751
2794
  const wouldFixCount = issues.filter((f) => f.wouldFix).length;
@@ -2796,7 +2839,8 @@ Examples:
2796
2839
  console.error('Error: --registry-key or REGISTRY_API_KEY env is required when using --version-id');
2797
2840
  process.exit(1);
2798
2841
  }
2799
- const client = new core.RegistryClient({ registryUrl, apiKey: registryKey });
2842
+ const atcToken = process.env.ATC_TOKEN;
2843
+ const client = new core.RegistryClient({ registryUrl, apiKey: registryKey, atcToken });
2800
2844
  const payload = core.buildScanReport(options.versionId, result.findings);
2801
2845
  await client.reportScanResult(payload);
2802
2846
  console.log(`Registry: scan results reported for version ${options.versionId}`);
@@ -3240,7 +3284,7 @@ Examples:
3240
3284
  .argument('<target>', 'Target hostname or IP address')
3241
3285
  .option('--json', 'Output as JSON (for scripting/CI)')
3242
3286
  .option('-p, --ports <ports>', 'Comma-separated ports to scan (default: common MCP ports)')
3243
- .option('-t, --timeout <ms>', 'Connection timeout in milliseconds', '5000')
3287
+ .option('-t, --timeout <ms>', 'Connection timeout in milliseconds', '2000')
3244
3288
  .option('-v, --verbose', 'Show detailed finding information')
3245
3289
  .action(async (target, options) => {
3246
3290
  try {
@@ -3256,11 +3300,11 @@ Examples:
3256
3300
  `\n`);
3257
3301
  process.exit(1);
3258
3302
  }
3259
- const timeoutMs = parseInt(options.timeout ?? '5000', 10);
3303
+ const timeoutMs = parseInt(options.timeout ?? '2000', 10);
3260
3304
  const customPorts = options.ports
3261
3305
  ? options.ports.split(',').map((p) => parseInt(p.trim(), 10))
3262
3306
  : undefined;
3263
- const portCount = customPorts?.length ?? 5;
3307
+ const portCount = customPorts?.length ?? 2;
3264
3308
  if (!options.json) {
3265
3309
  console.log(`\nScanning ${target} (${portCount} ports, ${timeoutMs}ms timeout)...\n`);
3266
3310
  }
@@ -3566,7 +3610,8 @@ Examples:
3566
3610
  console.error('Error: --registry-key or REGISTRY_API_KEY env is required when using --version-id');
3567
3611
  process.exit(1);
3568
3612
  }
3569
- const client = new core.RegistryClient({ registryUrl, apiKey: registryKey });
3613
+ const atcToken = process.env.ATC_TOKEN;
3614
+ const client = new core.RegistryClient({ registryUrl, apiKey: registryKey, atcToken });
3570
3615
  const payload = core.buildAttackReport(options.versionId, report);
3571
3616
  await client.reportScanResult(payload);
3572
3617
  console.log(`Registry: attack results reported for version ${options.versionId}`);
@@ -5575,6 +5620,28 @@ Examples:
5575
5620
  if (opts.json) {
5576
5621
  writeJsonStdout(result);
5577
5622
  }
5623
+ else if (result.found) {
5624
+ // Use the unified display (same as `check --no-scan`) for visual consistency
5625
+ const registryData = {
5626
+ found: true,
5627
+ name: result.name,
5628
+ trustScore: result.trustScore,
5629
+ trustLevel: result.trustLevel,
5630
+ verdict: result.verdict,
5631
+ scanStatus: result.scanStatus,
5632
+ lastScannedAt: result.lastScannedAt,
5633
+ packageType: result.packageType,
5634
+ recommendation: result.recommendation,
5635
+ cveCount: result.cveCount,
5636
+ communityScans: result.communityScans,
5637
+ dependencies: result.dependencies ? {
5638
+ totalDeps: result.dependencies.totalDeps,
5639
+ vulnerableDeps: result.dependencies.vulnerableDeps,
5640
+ minTrustLevel: result.dependencies.minTrustLevel,
5641
+ } : undefined,
5642
+ };
5643
+ displayUnifiedCheck({ name: packageName, registry: registryData, verbose: false });
5644
+ }
5578
5645
  else {
5579
5646
  process.stdout.write(formatTrustCheck(result));
5580
5647
  }
@@ -5593,7 +5660,7 @@ program
5593
5660
  .option('-d, --directory <dir>', 'Scan a specific directory to collect check metadata from findings')
5594
5661
  .option('--json', 'Output as JSON (default)')
5595
5662
  .action(async (options) => {
5596
- const { getAttackClass, getTaxonomyMap } = require('./hardening/taxonomy');
5663
+ const { getAttackClass, getTaxonomyMap, getCheckSeverity } = require('./hardening/taxonomy');
5597
5664
  // Build static registry from taxonomy map (covers all known checks)
5598
5665
  const taxMap = getTaxonomyMap();
5599
5666
  const metadata = {};
@@ -5605,7 +5672,7 @@ program
5605
5672
  name: checkId,
5606
5673
  category: prefix.toLowerCase(),
5607
5674
  attackClass: taxMap[checkId] || '',
5608
- severity: '',
5675
+ severity: getCheckSeverity(checkId),
5609
5676
  };
5610
5677
  }
5611
5678
  // If a directory is provided, enrich with actual finding data (names, severity, etc.)
@@ -6094,6 +6161,63 @@ program
6094
6161
  }
6095
6162
  console.log(`\nYour skill is ready. Verify security with: hackmyagent secure ${outputDir}/`);
6096
6163
  });
6164
+ // analm: manage the AnaLM generative model
6165
+ const analmCmd = program
6166
+ .command('analm')
6167
+ .description('Manage the AnaLM model for AI-powered security analysis');
6168
+ analmCmd
6169
+ .command('setup')
6170
+ .description('Download the AnaLM model')
6171
+ .action(async () => {
6172
+ const { getAnalystStatus, setupAnalystModel } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/inference/security-analyst.js')));
6173
+ const status = await getAnalystStatus();
6174
+ console.log('AnaLM (NanoMind Security Analyst)');
6175
+ console.log(` Platform: ${status.platform}`);
6176
+ console.log(` Backend: ${status.backend === 'none' ? 'not available' : status.backend}`);
6177
+ console.log(` Model: ${status.modelCached ? 'cached' : 'not downloaded'}`);
6178
+ console.log('');
6179
+ if (status.backend === 'none') {
6180
+ console.log('No supported inference backend found.');
6181
+ if (process.platform !== 'darwin') {
6182
+ console.log('Currently requires Apple Silicon Mac with MLX.');
6183
+ console.log('Cross-platform support (llama.cpp/GGUF) coming soon.');
6184
+ }
6185
+ else {
6186
+ console.log('Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh');
6187
+ }
6188
+ process.exit(1);
6189
+ }
6190
+ if (status.modelCached) {
6191
+ console.log('Model already downloaded. Use --analm with any scan command.');
6192
+ return;
6193
+ }
6194
+ const ok = await setupAnalystModel(false);
6195
+ if (!ok)
6196
+ process.exit(1);
6197
+ });
6198
+ analmCmd
6199
+ .command('status')
6200
+ .description('Check the status of AnaLM model and runtime')
6201
+ .action(async () => {
6202
+ const { getAnalystStatus } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/inference/security-analyst.js')));
6203
+ const status = await getAnalystStatus();
6204
+ console.log('AnaLM (NanoMind Security Analyst)');
6205
+ console.log(` Platform: ${status.platform}`);
6206
+ console.log(` Backend: ${status.backend === 'none' ? `${colors.red}not available${RESET()}` : `${colors.green}${status.backend}${RESET()}`}`);
6207
+ console.log(` Model: ${status.modelCached ? `${colors.green}cached${RESET()}` : `${colors.yellow}not downloaded${RESET()}`}`);
6208
+ console.log(` Ready: ${status.available ? `${colors.green}yes${RESET()}` : `${colors.yellow}no${RESET()}`}`);
6209
+ console.log('');
6210
+ if (status.available) {
6211
+ console.log('Use --analm with any scan command for AI-powered analysis.');
6212
+ console.log(` Example: hackmyagent secure ./my-agent --analm`);
6213
+ }
6214
+ else if (status.backend !== 'none') {
6215
+ console.log(`Run: hackmyagent analm setup`);
6216
+ }
6217
+ else if (process.platform !== 'darwin') {
6218
+ console.log('Cross-platform support (llama.cpp/GGUF) coming soon.');
6219
+ }
6220
+ });
6097
6221
  // ============================================================================
6098
6222
  // npm package scanning helpers (used by `check <package>`)
6099
6223
  // ============================================================================
@@ -6438,6 +6562,9 @@ const AI_TOOLING_PATH_PATTERNS = [
6438
6562
  /^\.aider/,
6439
6563
  /^\.copilot\//,
6440
6564
  /^\.github\/copilot/,
6565
+ /\.env\.example$/i, // Example env files are not real credentials
6566
+ /\.env\.sample$/i,
6567
+ /\.env\.template$/i,
6441
6568
  ];
6442
6569
  /** Governance-related categories/checkId prefixes that are noise on AI tooling files */
6443
6570
  const GOVERNANCE_CATEGORIES = new Set([
@@ -6473,13 +6600,12 @@ function filterLocalOnlyFindings(result, scanner) {
6473
6600
  // Remove local-only categories (git, permissions, env, etc.)
6474
6601
  if (PACKAGE_SCAN_LOCAL_ONLY_CATEGORIES.has(f.category))
6475
6602
  return false;
6476
- // Remove governance findings on AI tooling files (CLAUDE.md, .claude/, etc.)
6477
- if (f.file && isAiToolingFile(f.file)) {
6478
- if (GOVERNANCE_CATEGORIES.has(f.category))
6479
- return false;
6480
- if (GOVERNANCE_CHECK_PREFIXES.some(p => f.checkId.startsWith(p)))
6481
- return false;
6482
- }
6603
+ // Exclude ALL findings on AI tooling files (CLAUDE.md, .claude/, .cursorrules, etc.)
6604
+ // These files contain instructions to AI assistants, not package source code.
6605
+ // Credential patterns, injection patterns, and governance findings in these
6606
+ // files are false positives — they describe security practices, not vulnerabilities.
6607
+ if (f.file && isAiToolingFile(f.file))
6608
+ return false;
6483
6609
  return true;
6484
6610
  });
6485
6611
  // Demote test file findings to low severity (test code patterns are
@@ -7054,7 +7180,35 @@ async function checkNpmPackage(name, options) {
7054
7180
  const { stdout } = await execAsync('npm', ['pack', name, '--pack-destination', tempDir], { timeout: 60000 });
7055
7181
  const tarball = stdout.trim().split('\n').pop();
7056
7182
  await execAsync('tar', ['xzf', join(tempDir, tarball), '-C', tempDir], { timeout: 30000 });
7057
- const packageDir = join(tempDir, 'package');
7183
+ // npm tarballs normally extract to 'package/', but some packages (e.g. @types/*)
7184
+ // may use a different directory name. Detect the actual extracted directory.
7185
+ const { readdir, stat } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
7186
+ let packageDir = join(tempDir, 'package');
7187
+ try {
7188
+ await stat(packageDir);
7189
+ }
7190
+ catch {
7191
+ // 'package/' doesn't exist — find the extracted directory (skip the .tgz file)
7192
+ const entries = await readdir(tempDir);
7193
+ const dirs = [];
7194
+ for (const entry of entries) {
7195
+ if (entry.endsWith('.tgz') || entry.endsWith('.tar.gz'))
7196
+ continue;
7197
+ const s = await stat(join(tempDir, entry));
7198
+ if (s.isDirectory())
7199
+ dirs.push(entry);
7200
+ }
7201
+ if (dirs.length === 1) {
7202
+ packageDir = join(tempDir, dirs[0]);
7203
+ }
7204
+ else if (dirs.length === 0) {
7205
+ throw new Error(`Tarball extraction produced no directory in ${tempDir}`);
7206
+ }
7207
+ else {
7208
+ // Multiple dirs — pick the first non-hidden one
7209
+ packageDir = join(tempDir, dirs.find(d => !d.startsWith('.')) || dirs[0]);
7210
+ }
7211
+ }
7058
7212
  // Run full HMA scan + NanoMind (same pipeline as `secure`)
7059
7213
  const scanner = new index_1.HardeningScanner();
7060
7214
  const result = await scanner.scan({ targetDir: packageDir, autoFix: false });
@@ -7137,21 +7291,10 @@ async function checkNpmPackage(name, options) {
7137
7291
  const message = err instanceof Error ? err.message : String(err);
7138
7292
  // Clean npm error messages
7139
7293
  if (message.includes('404') || message.includes('Not Found')) {
7140
- console.error(`Error: Package "${name}" not found on npm.`);
7141
- // Suggest similar packages via npm registry search
7142
- try {
7143
- const suggestions = await suggestSimilarPackages(name);
7144
- if (suggestions.length > 0) {
7145
- console.error(`\nDid you mean?`);
7146
- for (const s of suggestions) {
7147
- console.error(` ${s}`);
7148
- }
7149
- console.error();
7150
- }
7151
- }
7152
- catch {
7153
- // Search failed — just show the original error
7154
- }
7294
+ // Throw a typed error so the router can fall through to skill check
7295
+ const notFound = new Error(`NPM_NOT_FOUND:${name}`);
7296
+ notFound.name = 'NpmNotFoundError';
7297
+ throw notFound;
7155
7298
  }
7156
7299
  else {
7157
7300
  console.error(`Error: ${message}`);