@nerviq/cli 1.14.0 → 1.16.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 +1 -1
- package/bin/cli.js +101 -24
- package/package.json +1 -1
- package/src/audit/recommendations.js +46 -0
- package/src/context.js +23 -1
- package/src/mcp-server.js +631 -314
- package/src/techniques/hygiene.js +2 -2
- package/src/techniques/quality.js +6 -5
package/README.md
CHANGED
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) {
|
|
@@ -560,7 +568,8 @@ const HELP = `
|
|
|
560
568
|
New here? Run: nerviq --beginner
|
|
561
569
|
|
|
562
570
|
DISCOVER
|
|
563
|
-
nerviq audit Quick scan: score + top 3 gaps (
|
|
571
|
+
nerviq audit Quick scan: score + top 3 gaps (Harmony-first when 2+ platforms detected)
|
|
572
|
+
nerviq audit --no-harmony-first Skip the cross-platform Harmony header
|
|
564
573
|
nerviq audit --full Full audit with all checks, weakest areas, badge
|
|
565
574
|
nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
|
|
566
575
|
nerviq audit --json Machine-readable JSON output (for CI)
|
|
@@ -663,6 +672,7 @@ const HELP = `
|
|
|
663
672
|
|
|
664
673
|
OPTIONS
|
|
665
674
|
--platform NAME Platform: claude (default), codex, cursor, copilot, gemini, windsurf, aider, opencode
|
|
675
|
+
--dir PATH Target directory to audit (default: current directory)
|
|
666
676
|
--threshold N Exit code 1 if score < N (CI gate)
|
|
667
677
|
--require A,B Exit code 1 if named checks fail
|
|
668
678
|
--out FILE Write output to file (JSON or markdown)
|
|
@@ -694,6 +704,7 @@ const HELP = `
|
|
|
694
704
|
--verbose Full audit + medium-priority recommendations
|
|
695
705
|
--show-deprecated Show deprecated checks (excluded from scoring)
|
|
696
706
|
--json Output as JSON
|
|
707
|
+
--agent-mode Non-interactive JSON output for AI agents (setup/audit)
|
|
697
708
|
--auto Apply all generated files without prompting
|
|
698
709
|
--beginner Show only the 5 starter commands for first-time users
|
|
699
710
|
--key NAME Feedback: recommendation key (e.g. permissionDeny)
|
|
@@ -735,8 +746,9 @@ const HELP = `
|
|
|
735
746
|
npx nerviq feedback --key permissionDeny --status accepted --effect positive
|
|
736
747
|
|
|
737
748
|
EXIT CODES
|
|
738
|
-
0 Success
|
|
739
|
-
1
|
|
749
|
+
0 Success (score meets threshold, or no threshold set)
|
|
750
|
+
1 Threshold not met (score below --threshold)
|
|
751
|
+
2 Runtime error (unknown command, missing files, crash)
|
|
740
752
|
`;
|
|
741
753
|
|
|
742
754
|
const BEGINNER_HELP = `
|
|
@@ -770,7 +782,7 @@ async function main() {
|
|
|
770
782
|
parsed = parseArgs(args);
|
|
771
783
|
} catch (err) {
|
|
772
784
|
console.error(`\n Error: ${err.message}\n`);
|
|
773
|
-
process.exit(
|
|
785
|
+
process.exit(2);
|
|
774
786
|
}
|
|
775
787
|
|
|
776
788
|
const { flags, command, commandExplicit, normalizedCommand } = parsed;
|
|
@@ -807,6 +819,7 @@ async function main() {
|
|
|
807
819
|
fix: flags.includes('--fix'),
|
|
808
820
|
badge: flags.includes('--badge'),
|
|
809
821
|
quiet: flags.includes('--quiet'),
|
|
822
|
+
agentMode: flags.includes('--agent-mode'),
|
|
810
823
|
autoSync: flags.includes('--auto-sync'),
|
|
811
824
|
dryRun: flags.includes('--dry-run'),
|
|
812
825
|
configOnly: flags.includes('--config-only'),
|
|
@@ -834,6 +847,7 @@ async function main() {
|
|
|
834
847
|
historyView: flags.includes('--history'),
|
|
835
848
|
compareView: flags.includes('--compare'),
|
|
836
849
|
diffOnly: flags.includes('--diff-only'),
|
|
850
|
+
noHarmonyFirst: flags.includes('--no-harmony-first'),
|
|
837
851
|
diffBase: parsed.diffBase || null,
|
|
838
852
|
diffHead: parsed.diffHead || null,
|
|
839
853
|
driftMode: parsed.driftMode || null,
|
|
@@ -842,32 +856,32 @@ async function main() {
|
|
|
842
856
|
exceptionExpires: parsed.exceptionExpires || null,
|
|
843
857
|
exceptionScope: parsed.exceptionScope || null,
|
|
844
858
|
exceptionClass: parsed.exceptionClass || null,
|
|
845
|
-
dir: process.cwd()
|
|
859
|
+
dir: parsed.targetDir || process.cwd()
|
|
846
860
|
};
|
|
847
861
|
|
|
848
862
|
if (options.snapshotTags.length > 0 && !options.snapshot) {
|
|
849
863
|
console.error('\n Error: --tag requires --snapshot.\n');
|
|
850
|
-
process.exit(
|
|
864
|
+
process.exit(2);
|
|
851
865
|
}
|
|
852
866
|
|
|
853
867
|
if (options.snapshotMilestone && !options.snapshot) {
|
|
854
868
|
console.error('\n Error: --milestone requires --snapshot.\n');
|
|
855
|
-
process.exit(
|
|
869
|
+
process.exit(2);
|
|
856
870
|
}
|
|
857
871
|
|
|
858
872
|
if (options.snapshotMilestone && !SNAPSHOT_MILESTONES.includes(options.snapshotMilestone)) {
|
|
859
873
|
console.error(`\n Error: Unsupported milestone '${options.snapshotMilestone}'. Use one of: ${SNAPSHOT_MILESTONES.join(', ')}.\n`);
|
|
860
|
-
process.exit(
|
|
874
|
+
process.exit(2);
|
|
861
875
|
}
|
|
862
876
|
|
|
863
877
|
if (options.diffOnly && options.snapshot) {
|
|
864
878
|
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(
|
|
879
|
+
process.exit(2);
|
|
866
880
|
}
|
|
867
881
|
|
|
868
882
|
if (options.driftMode && !['ci', 'pr', 'watch'].includes(options.driftMode)) {
|
|
869
883
|
console.error(`\n Error: Unsupported drift mode '${options.driftMode}'. Use ci, pr, or watch.\n`);
|
|
870
|
-
process.exit(
|
|
884
|
+
process.exit(2);
|
|
871
885
|
}
|
|
872
886
|
|
|
873
887
|
if (parsed.checkVersion) {
|
|
@@ -967,7 +981,7 @@ async function main() {
|
|
|
967
981
|
console.error(' Fix: Run nerviq --help to see all available commands.');
|
|
968
982
|
}
|
|
969
983
|
console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
|
|
970
|
-
process.exit(
|
|
984
|
+
process.exit(2);
|
|
971
985
|
}
|
|
972
986
|
|
|
973
987
|
if (!require('fs').existsSync(options.dir)) {
|
|
@@ -975,7 +989,7 @@ async function main() {
|
|
|
975
989
|
console.error(' Why: The current working directory does not exist or is not accessible.');
|
|
976
990
|
console.error(' Fix: cd into your project directory first, then run nerviq.');
|
|
977
991
|
console.error(' Docs: https://github.com/nerviq/nerviq#getting-started\n');
|
|
978
|
-
process.exit(
|
|
992
|
+
process.exit(2);
|
|
979
993
|
}
|
|
980
994
|
|
|
981
995
|
if (['setup', 'apply', 'benchmark'].includes(normalizedCommand)) {
|
|
@@ -1002,7 +1016,7 @@ async function main() {
|
|
|
1002
1016
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
1003
1017
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform codex.`);
|
|
1004
1018
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
1005
|
-
process.exit(
|
|
1019
|
+
process.exit(2);
|
|
1006
1020
|
}
|
|
1007
1021
|
}
|
|
1008
1022
|
|
|
@@ -1010,7 +1024,7 @@ async function main() {
|
|
|
1010
1024
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
1011
1025
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform gemini.`);
|
|
1012
1026
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
1013
|
-
process.exit(
|
|
1027
|
+
process.exit(2);
|
|
1014
1028
|
}
|
|
1015
1029
|
}
|
|
1016
1030
|
|
|
@@ -1018,7 +1032,7 @@ async function main() {
|
|
|
1018
1032
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
1019
1033
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform copilot.`);
|
|
1020
1034
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
1021
|
-
process.exit(
|
|
1035
|
+
process.exit(2);
|
|
1022
1036
|
}
|
|
1023
1037
|
}
|
|
1024
1038
|
|
|
@@ -1026,7 +1040,7 @@ async function main() {
|
|
|
1026
1040
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
1027
1041
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform cursor.`);
|
|
1028
1042
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
1029
|
-
process.exit(
|
|
1043
|
+
process.exit(2);
|
|
1030
1044
|
}
|
|
1031
1045
|
}
|
|
1032
1046
|
|
|
@@ -1035,7 +1049,7 @@ async function main() {
|
|
|
1035
1049
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
1036
1050
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform ${plat}.`);
|
|
1037
1051
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
1038
|
-
process.exit(
|
|
1052
|
+
process.exit(2);
|
|
1039
1053
|
}
|
|
1040
1054
|
}
|
|
1041
1055
|
}
|
|
@@ -1045,7 +1059,7 @@ async function main() {
|
|
|
1045
1059
|
if (scanDirs.length === 0) {
|
|
1046
1060
|
console.error('\n Error: scan requires at least one directory argument.');
|
|
1047
1061
|
console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
|
|
1048
|
-
process.exit(
|
|
1062
|
+
process.exit(2);
|
|
1049
1063
|
}
|
|
1050
1064
|
const summary = await scanOrg(scanDirs, options);
|
|
1051
1065
|
printScanDetail(summary, options);
|
|
@@ -1073,7 +1087,7 @@ async function main() {
|
|
|
1073
1087
|
console.error('\n Error: org requires `scan` or `policy`.');
|
|
1074
1088
|
console.error(' Usage: npx nerviq org scan dir1 dir2 dir3');
|
|
1075
1089
|
console.error(' npx nerviq org policy [dir]\n');
|
|
1076
|
-
process.exit(
|
|
1090
|
+
process.exit(2);
|
|
1077
1091
|
}
|
|
1078
1092
|
const summary = await scanOrg(scanDirs, options);
|
|
1079
1093
|
if (options.json) {
|
|
@@ -1617,6 +1631,7 @@ async function main() {
|
|
|
1617
1631
|
return;
|
|
1618
1632
|
} else if (normalizedCommand === 'harmony-audit') {
|
|
1619
1633
|
const { runHarmonyAudit } = require('../src/harmony/cli');
|
|
1634
|
+
collectAnonymousEvent('harmony-audit', { dir: options.dir });
|
|
1620
1635
|
await runHarmonyAudit(options);
|
|
1621
1636
|
process.exit(0);
|
|
1622
1637
|
} else if (normalizedCommand === 'harmony-sync') {
|
|
@@ -2448,7 +2463,29 @@ async function main() {
|
|
|
2448
2463
|
await runInit(options.dir, flags);
|
|
2449
2464
|
process.exit(0);
|
|
2450
2465
|
} else if (normalizedCommand === 'setup') {
|
|
2451
|
-
|
|
2466
|
+
collectAnonymousEvent('setup', { platform: options.platform, dir: options.dir });
|
|
2467
|
+
const setupResult = await setup({ ...options, silent: options.agentMode || options.json });
|
|
2468
|
+
if (options.agentMode) {
|
|
2469
|
+
// Agent-mode: structured JSON output with next steps
|
|
2470
|
+
const postAudit = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
2471
|
+
const agentOutput = {
|
|
2472
|
+
status: setupResult.created > 0 ? 'files_created' : 'already_configured',
|
|
2473
|
+
created: setupResult.created,
|
|
2474
|
+
skipped: setupResult.skipped,
|
|
2475
|
+
written_files: setupResult.writtenFiles,
|
|
2476
|
+
preserved_files: setupResult.preservedFiles,
|
|
2477
|
+
detected_stacks: setupResult.stacks.map(s => s.key),
|
|
2478
|
+
rollback_id: setupResult.rollbackId,
|
|
2479
|
+
post_setup_score: postAudit.score,
|
|
2480
|
+
next_commands: [
|
|
2481
|
+
'npx @nerviq/cli audit --json',
|
|
2482
|
+
setupResult.created > 0 ? `npx @nerviq/cli augment --platform ${options.platform}` : null,
|
|
2483
|
+
postAudit.score < 70 ? 'npx @nerviq/cli plan' : null,
|
|
2484
|
+
].filter(Boolean),
|
|
2485
|
+
};
|
|
2486
|
+
console.log(JSON.stringify(agentOutput, null, 2));
|
|
2487
|
+
process.exit(0);
|
|
2488
|
+
}
|
|
2452
2489
|
if (options.snapshot) {
|
|
2453
2490
|
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
2454
2491
|
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
@@ -2469,8 +2506,32 @@ async function main() {
|
|
|
2469
2506
|
}
|
|
2470
2507
|
process.exit(0);
|
|
2471
2508
|
}
|
|
2509
|
+
// MOAT-01: Harmony-first default — when 2+ platforms and platform not explicit
|
|
2510
|
+
let harmonyFirstResult = null;
|
|
2511
|
+
if (!options.platformExplicit && !options.noHarmonyFirst && !options.diffOnly && !options.driftMode && !options.workspace) {
|
|
2512
|
+
const detected = detectPlatforms(options.dir) || [];
|
|
2513
|
+
if (detected.length >= 2) {
|
|
2514
|
+
try {
|
|
2515
|
+
const { harmonyAudit } = require('../src/harmony/audit');
|
|
2516
|
+
harmonyFirstResult = await harmonyAudit({ dir: options.dir, silent: true });
|
|
2517
|
+
if (!options.json && harmonyFirstResult) {
|
|
2518
|
+
const hs = harmonyFirstResult.harmonyScore;
|
|
2519
|
+
const driftCount = (harmonyFirstResult.drift && harmonyFirstResult.drift.drifts) ? harmonyFirstResult.drift.drifts.length : 0;
|
|
2520
|
+
const platformLabels = (harmonyFirstResult.activePlatforms || []).map(p => p.label || p.platform).join(' + ');
|
|
2521
|
+
const color = hs >= 70 ? '\x1b[32m' : hs >= 40 ? '\x1b[33m' : '\x1b[31m';
|
|
2522
|
+
const issueWord = driftCount === 1 ? 'issue' : 'issues';
|
|
2523
|
+
console.log('');
|
|
2524
|
+
console.log(`\x1b[1m Harmony Score: ${color}${hs}/100\x1b[0m — ${driftCount} drift ${issueWord} across ${detected.length} platforms (${platformLabels})`);
|
|
2525
|
+
console.log('\x1b[2m Run `nerviq harmony-audit` for the full cross-platform report. Use --no-harmony-first to hide.\x1b[0m');
|
|
2526
|
+
}
|
|
2527
|
+
} catch {
|
|
2528
|
+
harmonyFirstResult = null;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2472
2533
|
let result;
|
|
2473
|
-
const renderAuditJsonLocally = options.json && Boolean(options.driftMode);
|
|
2534
|
+
const renderAuditJsonLocally = options.json && (Boolean(options.driftMode) || Boolean(harmonyFirstResult));
|
|
2474
2535
|
if (options.diffOnly) {
|
|
2475
2536
|
const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
|
|
2476
2537
|
const fullResult = await audit({ ...options, silent: true });
|
|
@@ -2485,6 +2546,14 @@ async function main() {
|
|
|
2485
2546
|
: await audit(options);
|
|
2486
2547
|
}
|
|
2487
2548
|
|
|
2549
|
+
// ── Telemetry (opt-in, local only) ──
|
|
2550
|
+
collectAnonymousEvent('audit', {
|
|
2551
|
+
platform: result.platform || options.platform,
|
|
2552
|
+
score: result.score,
|
|
2553
|
+
checkCount: Array.isArray(result.results) ? result.results.length : null,
|
|
2554
|
+
dir: options.dir,
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2488
2557
|
if (options.driftMode) {
|
|
2489
2558
|
const { buildContinuousStatus, formatContinuousStatus } = require('../src/continuous-ops');
|
|
2490
2559
|
let campaigns = [];
|
|
@@ -2537,9 +2606,17 @@ async function main() {
|
|
|
2537
2606
|
}
|
|
2538
2607
|
}
|
|
2539
2608
|
} else if (renderAuditJsonLocally) {
|
|
2609
|
+
const harmonyEnvelope = harmonyFirstResult ? {
|
|
2610
|
+
harmony: {
|
|
2611
|
+
score: harmonyFirstResult.harmonyScore,
|
|
2612
|
+
driftCount: (harmonyFirstResult.drift && harmonyFirstResult.drift.drifts) ? harmonyFirstResult.drift.drifts.length : 0,
|
|
2613
|
+
platforms: (harmonyFirstResult.activePlatforms || []).map(p => p.platform),
|
|
2614
|
+
},
|
|
2615
|
+
} : {};
|
|
2540
2616
|
console.log(JSON.stringify({
|
|
2541
2617
|
version,
|
|
2542
2618
|
timestamp: new Date().toISOString(),
|
|
2619
|
+
...harmonyEnvelope,
|
|
2543
2620
|
...result,
|
|
2544
2621
|
}, null, 2));
|
|
2545
2622
|
} else {
|
|
@@ -2664,7 +2741,7 @@ async function main() {
|
|
|
2664
2741
|
} catch (err) {
|
|
2665
2742
|
console.error(`\n Error: ${err.message}`);
|
|
2666
2743
|
console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
|
|
2667
|
-
process.exit(
|
|
2744
|
+
process.exit(2);
|
|
2668
2745
|
}
|
|
2669
2746
|
}
|
|
2670
2747
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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
|
-
|
|
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
|
/**
|