@nerviq/cli 1.14.0 → 1.15.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/README.md CHANGED
@@ -217,7 +217,7 @@ All successful operational responses are wrapped in a JSON envelope:
217
217
  {
218
218
  "data": {},
219
219
  "meta": {
220
- "version": "1.14.0",
220
+ "version": "1.15.0",
221
221
  "timestamp": "2026-04-11T12:00:00.000Z"
222
222
  }
223
223
  }
package/bin/cli.js CHANGED
@@ -8,6 +8,7 @@ const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, ren
8
8
  const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
9
9
  const { writeSnapshotArtifact, writeRollbackArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
10
10
  const { collectFeedback } = require('../src/feedback');
11
+ const { collectAnonymousEvent } = require('../src/telemetry');
11
12
  const { recordPattern, getPriorityAdjustment, formatUsageSummary } = require('../src/usage-patterns');
12
13
  const { startServer } = require('../src/server');
13
14
  const { auditWorkspaces } = require('../src/workspace');
@@ -104,6 +105,7 @@ function parseArgs(rawArgs) {
104
105
  let format = null;
105
106
  let port = null;
106
107
  let workspace = null;
108
+ let targetDir = null;
107
109
  let webhookUrl = null;
108
110
  let webhookHeaders = [];
109
111
  let webhookRetries = null;
@@ -134,7 +136,7 @@ function parseArgs(rawArgs) {
134
136
  for (let i = 0; i < rawArgs.length; i++) {
135
137
  const arg = rawArgs[i];
136
138
 
137
- if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format' || arg === '--from' || arg === '--to' || arg === '--port' || arg === '--workspace' || arg === '--check-version' || arg === '--webhook' || arg === '--webhook-header' || arg === '--webhook-retries' || arg === '--external' || arg === '--team-profile' || arg === '--lang' || arg === '--tag' || arg === '--milestone' || arg === '--campaign' || arg === '--diff-base' || arg === '--diff-head' || arg === '--drift-mode' || arg === '--owner' || arg === '--reason' || arg === '--expires' || arg === '--scope' || arg === '--class') {
139
+ if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--dir' || arg === '--format' || arg === '--from' || arg === '--to' || arg === '--port' || arg === '--workspace' || arg === '--check-version' || arg === '--webhook' || arg === '--webhook-header' || arg === '--webhook-retries' || arg === '--external' || arg === '--team-profile' || arg === '--lang' || arg === '--tag' || arg === '--milestone' || arg === '--campaign' || arg === '--diff-base' || arg === '--diff-head' || arg === '--drift-mode' || arg === '--owner' || arg === '--reason' || arg === '--expires' || arg === '--scope' || arg === '--class') {
138
140
  const value = rawArgs[i + 1];
139
141
  if (!value || value.startsWith('--')) {
140
142
  throw new Error(`${arg} requires a value`);
@@ -153,6 +155,7 @@ function parseArgs(rawArgs) {
153
155
  if (arg === '--source') feedbackSource = value.trim();
154
156
  if (arg === '--score-delta') feedbackScoreDelta = value.trim();
155
157
  if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
158
+ if (arg === '--dir') targetDir = require('path').resolve(value.trim());
156
159
  if (arg === '--format') format = value.trim().toLowerCase();
157
160
  if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
158
161
  if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
@@ -337,6 +340,11 @@ function parseArgs(rawArgs) {
337
340
  continue;
338
341
  }
339
342
 
343
+ if (arg.startsWith('--dir=')) {
344
+ targetDir = require('path').resolve(arg.split('=').slice(1).join('=').trim());
345
+ continue;
346
+ }
347
+
340
348
  if (arg.startsWith('--format=')) {
341
349
  format = arg.split('=').slice(1).join('=').trim().toLowerCase();
342
350
  continue;
@@ -388,7 +396,7 @@ function parseArgs(rawArgs) {
388
396
 
389
397
  const normalizedCommand = COMMAND_ALIASES[command] || command;
390
398
 
391
- return { flags, command, commandExplicit, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, platformExplicit, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, webhookHeaders, webhookRetries, external, repos, teamProfile, lang, snapshotTags, snapshotMilestone, campaigns, diffBase, diffHead, driftMode, exceptionOwner, exceptionReason, exceptionExpires, exceptionScope, exceptionClass };
399
+ return { flags, command, commandExplicit, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, platformExplicit, format, port, workspace, targetDir, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, webhookHeaders, webhookRetries, external, repos, teamProfile, lang, snapshotTags, snapshotMilestone, campaigns, diffBase, diffHead, driftMode, exceptionOwner, exceptionReason, exceptionExpires, exceptionScope, exceptionClass };
392
400
  }
393
401
 
394
402
  function printWorkspaceSummary(summary, options) {
@@ -663,6 +671,7 @@ const HELP = `
663
671
 
664
672
  OPTIONS
665
673
  --platform NAME Platform: claude (default), codex, cursor, copilot, gemini, windsurf, aider, opencode
674
+ --dir PATH Target directory to audit (default: current directory)
666
675
  --threshold N Exit code 1 if score < N (CI gate)
667
676
  --require A,B Exit code 1 if named checks fail
668
677
  --out FILE Write output to file (JSON or markdown)
@@ -694,6 +703,7 @@ const HELP = `
694
703
  --verbose Full audit + medium-priority recommendations
695
704
  --show-deprecated Show deprecated checks (excluded from scoring)
696
705
  --json Output as JSON
706
+ --agent-mode Non-interactive JSON output for AI agents (setup/audit)
697
707
  --auto Apply all generated files without prompting
698
708
  --beginner Show only the 5 starter commands for first-time users
699
709
  --key NAME Feedback: recommendation key (e.g. permissionDeny)
@@ -735,8 +745,9 @@ const HELP = `
735
745
  npx nerviq feedback --key permissionDeny --status accepted --effect positive
736
746
 
737
747
  EXIT CODES
738
- 0 Success
739
- 1 Error, unknown command, or score below --threshold
748
+ 0 Success (score meets threshold, or no threshold set)
749
+ 1 Threshold not met (score below --threshold)
750
+ 2 Runtime error (unknown command, missing files, crash)
740
751
  `;
741
752
 
742
753
  const BEGINNER_HELP = `
@@ -770,7 +781,7 @@ async function main() {
770
781
  parsed = parseArgs(args);
771
782
  } catch (err) {
772
783
  console.error(`\n Error: ${err.message}\n`);
773
- process.exit(1);
784
+ process.exit(2);
774
785
  }
775
786
 
776
787
  const { flags, command, commandExplicit, normalizedCommand } = parsed;
@@ -807,6 +818,7 @@ async function main() {
807
818
  fix: flags.includes('--fix'),
808
819
  badge: flags.includes('--badge'),
809
820
  quiet: flags.includes('--quiet'),
821
+ agentMode: flags.includes('--agent-mode'),
810
822
  autoSync: flags.includes('--auto-sync'),
811
823
  dryRun: flags.includes('--dry-run'),
812
824
  configOnly: flags.includes('--config-only'),
@@ -842,32 +854,32 @@ async function main() {
842
854
  exceptionExpires: parsed.exceptionExpires || null,
843
855
  exceptionScope: parsed.exceptionScope || null,
844
856
  exceptionClass: parsed.exceptionClass || null,
845
- dir: process.cwd()
857
+ dir: parsed.targetDir || process.cwd()
846
858
  };
847
859
 
848
860
  if (options.snapshotTags.length > 0 && !options.snapshot) {
849
861
  console.error('\n Error: --tag requires --snapshot.\n');
850
- process.exit(1);
862
+ process.exit(2);
851
863
  }
852
864
 
853
865
  if (options.snapshotMilestone && !options.snapshot) {
854
866
  console.error('\n Error: --milestone requires --snapshot.\n');
855
- process.exit(1);
867
+ process.exit(2);
856
868
  }
857
869
 
858
870
  if (options.snapshotMilestone && !SNAPSHOT_MILESTONES.includes(options.snapshotMilestone)) {
859
871
  console.error(`\n Error: Unsupported milestone '${options.snapshotMilestone}'. Use one of: ${SNAPSHOT_MILESTONES.join(', ')}.\n`);
860
- process.exit(1);
872
+ process.exit(2);
861
873
  }
862
874
 
863
875
  if (options.diffOnly && options.snapshot) {
864
876
  console.error('\n Error: --diff-only cannot be combined with --snapshot because diff-only scores are not comparable to full audit snapshots.\n');
865
- process.exit(1);
877
+ process.exit(2);
866
878
  }
867
879
 
868
880
  if (options.driftMode && !['ci', 'pr', 'watch'].includes(options.driftMode)) {
869
881
  console.error(`\n Error: Unsupported drift mode '${options.driftMode}'. Use ci, pr, or watch.\n`);
870
- process.exit(1);
882
+ process.exit(2);
871
883
  }
872
884
 
873
885
  if (parsed.checkVersion) {
@@ -967,7 +979,7 @@ async function main() {
967
979
  console.error(' Fix: Run nerviq --help to see all available commands.');
968
980
  }
969
981
  console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
970
- process.exit(1);
982
+ process.exit(2);
971
983
  }
972
984
 
973
985
  if (!require('fs').existsSync(options.dir)) {
@@ -975,7 +987,7 @@ async function main() {
975
987
  console.error(' Why: The current working directory does not exist or is not accessible.');
976
988
  console.error(' Fix: cd into your project directory first, then run nerviq.');
977
989
  console.error(' Docs: https://github.com/nerviq/nerviq#getting-started\n');
978
- process.exit(1);
990
+ process.exit(2);
979
991
  }
980
992
 
981
993
  if (['setup', 'apply', 'benchmark'].includes(normalizedCommand)) {
@@ -1002,7 +1014,7 @@ async function main() {
1002
1014
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1003
1015
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform codex.`);
1004
1016
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1005
- process.exit(1);
1017
+ process.exit(2);
1006
1018
  }
1007
1019
  }
1008
1020
 
@@ -1010,7 +1022,7 @@ async function main() {
1010
1022
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1011
1023
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform gemini.`);
1012
1024
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1013
- process.exit(1);
1025
+ process.exit(2);
1014
1026
  }
1015
1027
  }
1016
1028
 
@@ -1018,7 +1030,7 @@ async function main() {
1018
1030
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1019
1031
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform copilot.`);
1020
1032
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1021
- process.exit(1);
1033
+ process.exit(2);
1022
1034
  }
1023
1035
  }
1024
1036
 
@@ -1026,7 +1038,7 @@ async function main() {
1026
1038
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1027
1039
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform cursor.`);
1028
1040
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1029
- process.exit(1);
1041
+ process.exit(2);
1030
1042
  }
1031
1043
  }
1032
1044
 
@@ -1035,7 +1047,7 @@ async function main() {
1035
1047
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1036
1048
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform ${plat}.`);
1037
1049
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1038
- process.exit(1);
1050
+ process.exit(2);
1039
1051
  }
1040
1052
  }
1041
1053
  }
@@ -1045,7 +1057,7 @@ async function main() {
1045
1057
  if (scanDirs.length === 0) {
1046
1058
  console.error('\n Error: scan requires at least one directory argument.');
1047
1059
  console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
1048
- process.exit(1);
1060
+ process.exit(2);
1049
1061
  }
1050
1062
  const summary = await scanOrg(scanDirs, options);
1051
1063
  printScanDetail(summary, options);
@@ -1073,7 +1085,7 @@ async function main() {
1073
1085
  console.error('\n Error: org requires `scan` or `policy`.');
1074
1086
  console.error(' Usage: npx nerviq org scan dir1 dir2 dir3');
1075
1087
  console.error(' npx nerviq org policy [dir]\n');
1076
- process.exit(1);
1088
+ process.exit(2);
1077
1089
  }
1078
1090
  const summary = await scanOrg(scanDirs, options);
1079
1091
  if (options.json) {
@@ -1617,6 +1629,7 @@ async function main() {
1617
1629
  return;
1618
1630
  } else if (normalizedCommand === 'harmony-audit') {
1619
1631
  const { runHarmonyAudit } = require('../src/harmony/cli');
1632
+ collectAnonymousEvent('harmony-audit', { dir: options.dir });
1620
1633
  await runHarmonyAudit(options);
1621
1634
  process.exit(0);
1622
1635
  } else if (normalizedCommand === 'harmony-sync') {
@@ -2448,7 +2461,29 @@ async function main() {
2448
2461
  await runInit(options.dir, flags);
2449
2462
  process.exit(0);
2450
2463
  } else if (normalizedCommand === 'setup') {
2451
- await setup(options);
2464
+ collectAnonymousEvent('setup', { platform: options.platform, dir: options.dir });
2465
+ const setupResult = await setup({ ...options, silent: options.agentMode || options.json });
2466
+ if (options.agentMode) {
2467
+ // Agent-mode: structured JSON output with next steps
2468
+ const postAudit = await audit({ dir: options.dir, silent: true, platform: options.platform });
2469
+ const agentOutput = {
2470
+ status: setupResult.created > 0 ? 'files_created' : 'already_configured',
2471
+ created: setupResult.created,
2472
+ skipped: setupResult.skipped,
2473
+ written_files: setupResult.writtenFiles,
2474
+ preserved_files: setupResult.preservedFiles,
2475
+ detected_stacks: setupResult.stacks.map(s => s.key),
2476
+ rollback_id: setupResult.rollbackId,
2477
+ post_setup_score: postAudit.score,
2478
+ next_commands: [
2479
+ 'npx @nerviq/cli audit --json',
2480
+ setupResult.created > 0 ? `npx @nerviq/cli augment --platform ${options.platform}` : null,
2481
+ postAudit.score < 70 ? 'npx @nerviq/cli plan' : null,
2482
+ ].filter(Boolean),
2483
+ };
2484
+ console.log(JSON.stringify(agentOutput, null, 2));
2485
+ process.exit(0);
2486
+ }
2452
2487
  if (options.snapshot) {
2453
2488
  const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2454
2489
  const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
@@ -2485,6 +2520,14 @@ async function main() {
2485
2520
  : await audit(options);
2486
2521
  }
2487
2522
 
2523
+ // ── Telemetry (opt-in, local only) ──
2524
+ collectAnonymousEvent('audit', {
2525
+ platform: result.platform || options.platform,
2526
+ score: result.score,
2527
+ checkCount: Array.isArray(result.results) ? result.results.length : null,
2528
+ dir: options.dir,
2529
+ });
2530
+
2488
2531
  if (options.driftMode) {
2489
2532
  const { buildContinuousStatus, formatContinuousStatus } = require('../src/continuous-ops');
2490
2533
  let campaigns = [];
@@ -2664,7 +2707,7 @@ async function main() {
2664
2707
  } catch (err) {
2665
2708
  console.error(`\n Error: ${err.message}`);
2666
2709
  console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
2667
- process.exit(1);
2710
+ process.exit(2);
2668
2711
  }
2669
2712
  }
2670
2713
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -379,6 +379,7 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
379
379
  sourceUrl,
380
380
  module: CATEGORY_MODULES[category] || category,
381
381
  fix,
382
+ remediation_command: getRemediationCommand(key, category, options.platform),
382
383
  priorityScore,
383
384
  why: ACTION_RATIONALES[key] || fix,
384
385
  risk: riskFromImpact(impact),
@@ -399,6 +400,51 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
399
400
  });
400
401
  }
401
402
 
403
+ /**
404
+ * Map check keys/categories to a shell command an agent can run to fix the issue.
405
+ * Returns null when no automated fix is available.
406
+ */
407
+ function getRemediationCommand(key, category, platform) {
408
+ const plat = platform || 'claude';
409
+
410
+ // Key-specific remediation commands
411
+ const KEY_COMMANDS = {
412
+ claudeMd: 'npx @nerviq/cli setup',
413
+ agentsMd: 'npx @nerviq/cli setup --platform codex',
414
+ geminiMd: 'npx @nerviq/cli setup --platform gemini',
415
+ copilotInstructions: 'npx @nerviq/cli setup --platform copilot',
416
+ cursorRules: 'npx @nerviq/cli setup --platform cursor',
417
+ windsurfRules: 'npx @nerviq/cli setup --platform windsurf',
418
+ aiderConfig: 'npx @nerviq/cli setup --platform aider',
419
+ opencodeConfig: 'npx @nerviq/cli setup --platform opencode',
420
+ settingsPermissions: 'npx @nerviq/cli plan --only permissions',
421
+ permissionDeny: 'npx @nerviq/cli plan --only permissions',
422
+ noBypassPermissions: 'npx @nerviq/cli plan --only permissions',
423
+ secretsProtection: 'npx @nerviq/cli plan --only permissions',
424
+ verificationLoop: `npx @nerviq/cli augment --platform ${plat}`,
425
+ lintCommand: `npx @nerviq/cli augment --platform ${plat}`,
426
+ testCommand: `npx @nerviq/cli augment --platform ${plat}`,
427
+ buildCommand: `npx @nerviq/cli augment --platform ${plat}`,
428
+ hookExists: 'npx @nerviq/cli plan --only hooks',
429
+ preCommitHook: 'npx @nerviq/cli plan --only hooks',
430
+ commandsExist: 'npx @nerviq/cli plan --only commands',
431
+ mcpServers: 'npx @nerviq/cli plan --mcp-pack context7',
432
+ };
433
+
434
+ if (KEY_COMMANDS[key]) return KEY_COMMANDS[key];
435
+
436
+ // Category-level fallback
437
+ const CATEGORY_COMMANDS = {
438
+ memory: `npx @nerviq/cli setup --platform ${plat}`,
439
+ security: `npx @nerviq/cli plan --only permissions --platform ${plat}`,
440
+ automation: `npx @nerviq/cli plan --only hooks --platform ${plat}`,
441
+ workflow: `npx @nerviq/cli plan --only commands --platform ${plat}`,
442
+ tools: `npx @nerviq/cli plan --mcp-pack context7 --platform ${plat}`,
443
+ };
444
+
445
+ return CATEGORY_COMMANDS[category] || `npx @nerviq/cli augment --platform ${plat}`;
446
+ }
447
+
402
448
  function getNextScoreMilestone(score) {
403
449
  return SCORE_MILESTONES.find((milestone) => score < milestone) || null;
404
450
  }
package/src/context.js CHANGED
@@ -74,10 +74,32 @@ class ProjectContext {
74
74
 
75
75
  /**
76
76
  * Return the contents of the project's CLAUDE.md (root or .claude/ location).
77
+ * If CLAUDE.md contains only a reference to another file (e.g., "AGENTS.md"),
78
+ * follows that reference and returns the referenced file's content appended.
77
79
  * @returns {string|null} File content or null if not found.
78
80
  */
79
81
  claudeMdContent() {
80
- return this.fileContent('CLAUDE.md') || this.fileContent('.claude/CLAUDE.md');
82
+ const raw = this.fileContent('CLAUDE.md') || this.fileContent('.claude/CLAUDE.md');
83
+ if (!raw) return null;
84
+
85
+ // If the file is very short and looks like a file reference, follow it.
86
+ // Pattern: a single line that is just a filename (e.g., "AGENTS.md" or "docs/CODING.md")
87
+ const trimmed = raw.trim();
88
+ if (trimmed.length < 200 && /^[a-zA-Z0-9_./-]+\.(md|txt|rst)$/m.test(trimmed)) {
89
+ const lines = trimmed.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
90
+ let combined = raw;
91
+ for (const line of lines) {
92
+ if (/^[a-zA-Z0-9_./-]+\.(md|txt|rst)$/.test(line)) {
93
+ const referenced = this.fileContent(line);
94
+ if (referenced) {
95
+ combined += '\n' + referenced;
96
+ }
97
+ }
98
+ }
99
+ return combined;
100
+ }
101
+
102
+ return raw;
81
103
  }
82
104
 
83
105
  /**