@nerviq/cli 1.9.0 → 1.11.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/bin/cli.js CHANGED
@@ -47,7 +47,7 @@ function levenshtein(a, b) {
47
47
  return matrix[a.length][b.length];
48
48
  }
49
49
 
50
- function suggestCommand(input) {
50
+ function suggestCommand(input) {
51
51
  const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
52
52
  let best = null;
53
53
  let bestDistance = Infinity;
@@ -58,12 +58,33 @@ function suggestCommand(input) {
58
58
  bestDistance = distance;
59
59
  }
60
60
  }
61
- return bestDistance <= 3 ? best : null;
62
- }
63
-
64
- function parseArgs(rawArgs) {
65
- const flags = [];
66
- let command = 'audit';
61
+ return bestDistance <= 3 ? best : null;
62
+ }
63
+
64
+ function parseNonNegativeIntegerFlag(value, flagName) {
65
+ const parsed = Number(value);
66
+ if (!Number.isInteger(parsed) || parsed < 0) {
67
+ throw new Error(`${flagName} requires a non-negative integer`);
68
+ }
69
+ return parsed;
70
+ }
71
+
72
+ function parseWebhookHeader(rawValue) {
73
+ const separator = rawValue.indexOf(':');
74
+ if (separator <= 0) {
75
+ throw new Error('--webhook-header requires NAME: VALUE');
76
+ }
77
+ const name = rawValue.slice(0, separator).trim();
78
+ const value = rawValue.slice(separator + 1).trim();
79
+ if (!name || !value) {
80
+ throw new Error('--webhook-header requires NAME: VALUE');
81
+ }
82
+ return { name, value };
83
+ }
84
+
85
+ function parseArgs(rawArgs) {
86
+ const flags = [];
87
+ let command = 'audit';
67
88
  let threshold = null;
68
89
  let out = null;
69
90
  let planFile = null;
@@ -79,9 +100,12 @@ function parseArgs(rawArgs) {
79
100
  let feedbackScoreDelta = null;
80
101
  let platform = 'claude';
81
102
  let format = null;
82
- let port = null;
83
- let workspace = null;
84
- let webhookUrl = null;
103
+ let port = null;
104
+ let workspace = null;
105
+ let webhookUrl = null;
106
+ let webhookHeaders = [];
107
+ let webhookRetries = null;
108
+ let snapshotTags = [];
85
109
  let commandSet = false;
86
110
  let extraArgs = [];
87
111
  let convertFrom = null;
@@ -89,19 +113,20 @@ function parseArgs(rawArgs) {
89
113
  let migrateFrom = null;
90
114
  let migrateTo = null;
91
115
  let checkVersion = null;
92
- let external = null;
93
- let repos = [];
94
- let teamProfile = null;
95
- let lang = null;
116
+ let external = null;
117
+ let repos = [];
118
+ let teamProfile = null;
119
+ let lang = null;
120
+ let commandExplicit = false;
96
121
 
97
122
  for (let i = 0; i < rawArgs.length; i++) {
98
123
  const arg = rawArgs[i];
99
124
 
100
- 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 === '--external' || arg === '--team-profile' || arg === '--lang') {
101
- const value = rawArgs[i + 1];
102
- if (!value || value.startsWith('--')) {
103
- throw new Error(`${arg} requires a value`);
104
- }
125
+ 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') {
126
+ const value = rawArgs[i + 1];
127
+ if (!value || value.startsWith('--')) {
128
+ throw new Error(`${arg} requires a value`);
129
+ }
105
130
  if (arg === '--threshold') threshold = value;
106
131
  if (arg === '--out') out = value;
107
132
  if (arg === '--plan') planFile = value;
@@ -120,15 +145,18 @@ function parseArgs(rawArgs) {
120
145
  if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
121
146
  if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
122
147
  if (arg === '--port') port = value.trim();
123
- if (arg === '--workspace') workspace = value.trim();
124
- if (arg === '--check-version') checkVersion = value.trim();
125
- if (arg === '--webhook') webhookUrl = value.trim();
126
- if (arg === '--external') external = value.trim();
127
- if (arg === '--team-profile') teamProfile = value.trim();
128
- if (arg === '--lang') lang = value.trim().toLowerCase();
129
- i++;
130
- continue;
131
- }
148
+ if (arg === '--workspace') workspace = value.trim();
149
+ if (arg === '--check-version') checkVersion = value.trim();
150
+ if (arg === '--webhook') webhookUrl = value.trim();
151
+ if (arg === '--webhook-header') webhookHeaders.push(parseWebhookHeader(value));
152
+ if (arg === '--webhook-retries') webhookRetries = parseNonNegativeIntegerFlag(value.trim(), '--webhook-retries');
153
+ if (arg === '--external') external = value.trim();
154
+ if (arg === '--team-profile') teamProfile = value.trim();
155
+ if (arg === '--lang') lang = value.trim().toLowerCase();
156
+ if (arg === '--tag') snapshotTags.push(value.trim());
157
+ i++;
158
+ continue;
159
+ }
132
160
 
133
161
  if (arg.startsWith('--lang=')) {
134
162
  lang = arg.split('=').slice(1).join('=').trim().toLowerCase();
@@ -140,10 +168,15 @@ function parseArgs(rawArgs) {
140
168
  continue;
141
169
  }
142
170
 
143
- if (arg.startsWith('--external=')) {
144
- external = arg.split('=').slice(1).join('=').trim();
145
- continue;
146
- }
171
+ if (arg.startsWith('--external=')) {
172
+ external = arg.split('=').slice(1).join('=').trim();
173
+ continue;
174
+ }
175
+
176
+ if (arg.startsWith('--tag=')) {
177
+ snapshotTags.push(arg.split('=').slice(1).join('=').trim());
178
+ continue;
179
+ }
147
180
 
148
181
  if (arg === '--repos') {
149
182
  // Collect all following non-flag args as repo paths (supports comma-separated too)
@@ -246,54 +279,100 @@ function parseArgs(rawArgs) {
246
279
  continue;
247
280
  }
248
281
 
249
- if (arg.startsWith('--check-version=')) {
250
- checkVersion = arg.split('=').slice(1).join('=').trim();
251
- continue;
282
+ if (arg.startsWith('--check-version=')) {
283
+ checkVersion = arg.split('=').slice(1).join('=').trim();
284
+ continue;
285
+ }
286
+
287
+ if (arg.startsWith('--webhook=')) {
288
+ webhookUrl = arg.split('=').slice(1).join('=').trim();
289
+ continue;
290
+ }
291
+
292
+ if (arg.startsWith('--webhook-header=')) {
293
+ webhookHeaders.push(parseWebhookHeader(arg.split('=').slice(1).join('=')));
294
+ continue;
295
+ }
296
+
297
+ if (arg.startsWith('--webhook-retries=')) {
298
+ webhookRetries = parseNonNegativeIntegerFlag(arg.split('=').slice(1).join('=').trim(), '--webhook-retries');
299
+ continue;
300
+ }
301
+
302
+ if (arg.startsWith('--')) {
303
+ flags.push(arg);
304
+ continue;
252
305
  }
253
306
 
254
- if (arg.startsWith('--')) {
255
- flags.push(arg);
256
- continue;
257
- }
258
-
259
- if (!commandSet) {
260
- command = arg;
261
- commandSet = true;
262
- } else {
263
- extraArgs.push(arg);
264
- }
265
- }
307
+ if (!commandSet) {
308
+ command = arg;
309
+ commandSet = true;
310
+ commandExplicit = true;
311
+ } else {
312
+ extraArgs.push(arg);
313
+ }
314
+ }
266
315
 
267
316
  const normalizedCommand = COMMAND_ALIASES[command] || command;
268
317
 
269
- return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, external, repos, teamProfile, lang };
270
- }
271
-
272
- function printWorkspaceSummary(summary, options) {
273
- if (options.json) {
274
- console.log(JSON.stringify(summary, null, 2));
275
- return;
276
- }
277
-
278
- console.log('');
279
- console.log('\x1b[1m nerviq workspace audit\x1b[0m');
280
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
281
- console.log(` Root: ${summary.rootDir}`);
282
- console.log(` Platform: ${summary.platform}`);
283
- console.log(` Workspaces: ${summary.workspaceCount}`);
284
- console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
285
- console.log('');
286
- console.log('\x1b[1m Workspace Score Pass Total Top action\x1b[0m');
287
- console.log(' ' + '─'.repeat(72));
288
- for (const item of summary.workspaces) {
289
- const score = item.score === null ? 'ERR' : String(item.score);
290
- const topAction = item.error || item.topAction || '-';
291
- console.log(` ${item.workspace.padEnd(26)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
292
- }
293
- console.log('');
294
- }
295
-
296
- function printScanDetail(summary, options) {
318
+ return { flags, command, commandExplicit, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, webhookHeaders, webhookRetries, external, repos, teamProfile, lang, snapshotTags };
319
+ }
320
+
321
+ function printWorkspaceSummary(summary, options) {
322
+ if (options.json) {
323
+ console.log(JSON.stringify(summary, null, 2));
324
+ return;
325
+ }
326
+
327
+ const rootScore = summary.rootGovernance?.score === null ? 'ERR' : `${summary.rootGovernance?.score ?? 0}/100`;
328
+ const workspaceAverage = summary.workspaceAggregate?.score ?? summary.averageScore;
329
+
330
+ console.log('');
331
+ console.log('\x1b[1m nerviq workspace audit\x1b[0m');
332
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
333
+ console.log(` Root: ${summary.rootDir}`);
334
+ console.log(` Platform: ${summary.platform}`);
335
+ console.log(` Workspaces: ${summary.workspaceCount}`);
336
+ if (summary.patterns?.length > 0) {
337
+ console.log(` Selection: ${summary.patterns.join(', ')}`);
338
+ }
339
+ console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
340
+ console.log(` Workspace audit average: \x1b[1m${workspaceAverage}/100\x1b[0m`);
341
+ if (summary.profileBreakdown?.length > 0) {
342
+ const profileLine = summary.profileBreakdown
343
+ .map((item) => `${item.profileLabel} (${item.workspaceCount})`)
344
+ .join(', ');
345
+ console.log(` Workspace profiles: ${profileLine}`);
346
+ }
347
+ console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
348
+ console.log(' Aggregate vs package: per-workspace scores can legitimately trail the root repo score in a monorepo.');
349
+ console.log(' Stack-specific checks: Go, Python, Node, and other workspace types can have different applicable totals.');
350
+ console.log('');
351
+ console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
352
+ console.log(' ' + '─'.repeat(96));
353
+ for (const item of summary.workspaces) {
354
+ const score = item.score === null ? 'ERR' : String(item.score);
355
+ const topAction = item.error || item.topAction || '-';
356
+ const profile = (item.workspaceProfile?.label || 'General workspace').slice(0, 20);
357
+ console.log(` ${item.workspace.padEnd(26)} ${profile.padEnd(20)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
358
+ if (item.stackLabels?.length > 0) {
359
+ console.log(`\x1b[2m Stacks: ${item.stackLabels.join(', ')}\x1b[0m`);
360
+ }
361
+ }
362
+ console.log('');
363
+ }
364
+
365
+ function printCompareCheckSection(title, items, prefix) {
366
+ if (!Array.isArray(items) || items.length === 0) return;
367
+ console.log(` ${title} (${items.length}):`);
368
+ for (const item of items) {
369
+ const impact = item.impact ? ` [${item.impact}]` : '';
370
+ const category = item.category ? ` — ${item.category}` : '';
371
+ console.log(` ${prefix} ${item.key}${impact}: ${item.name}${category}`);
372
+ }
373
+ }
374
+
375
+ function printScanDetail(summary, options) {
297
376
  if (options.json) {
298
377
  console.log(JSON.stringify(summary, null, 2));
299
378
  return;
@@ -366,17 +445,18 @@ function printOrgSummary(summary, options) {
366
445
  console.log('');
367
446
  }
368
447
 
369
- const HELP = `
370
- nerviq v${version}
371
- The intelligent nervous system for AI coding agents.
372
- Audit, align, and amplify every platform on every project.
373
-
374
- DISCOVER
375
- nerviq audit Quick scan: score + top 3 gaps (default)
376
- nerviq audit --full Full audit with all checks, weakest areas, badge
377
- nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
378
- nerviq audit --json Machine-readable JSON output (for CI)
379
- nerviq audit --workspace packages/* Audit each workspace in a monorepo
448
+ const HELP = `
449
+ nerviq v${version}
450
+ The intelligent nervous system for AI coding agents.
451
+ Audit, align, and amplify every platform on every project.
452
+ New here? Run: nerviq --beginner
453
+
454
+ DISCOVER
455
+ nerviq audit Quick scan: score + top 3 gaps (default)
456
+ nerviq audit --full Full audit with all checks, weakest areas, badge
457
+ nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
458
+ nerviq audit --json Machine-readable JSON output (for CI)
459
+ nerviq audit --workspace packages/* Audit monorepo workspaces with stack-specific package profiles
380
460
  nerviq scan dir1 dir2 Compare multiple repos side-by-side
381
461
  nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
382
462
  nerviq catalog Full check catalog (all 8 platforms)
@@ -411,34 +491,35 @@ const HELP = `
411
491
  nerviq apply --dry-run Preview changes without writing
412
492
 
413
493
  GOVERN
414
- nerviq governance Permission profiles + hooks + policy packs
494
+ nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
415
495
  nerviq governance --json Machine-readable governance summary
416
- nerviq benchmark Before/after score in isolated temp copy
496
+ nerviq benchmark Baseline vs projected score in isolated temp copy
417
497
  nerviq benchmark --external /path Benchmark an external repo
418
498
  nerviq freshness Show verification freshness for all checks
419
499
  nerviq certify Generate certification badge for your project
420
500
 
421
501
  CROSS-PLATFORM
422
- nerviq harmony-audit Drift detection across all active platforms
423
- nerviq harmony-sync Preview cross-platform sync (dry run)
424
- nerviq harmony-sync --fix Apply cross-platform sync (write files)
425
- nerviq harmony-sync --json JSON output for CI/automation
426
- nerviq harmony-add <platform> Add a new platform to the project
427
- nerviq synergy-report Multi-agent amplification opportunities
502
+ nerviq harmony-audit Drift detection across all active platforms (GA)
503
+ nerviq harmony-sync Preview cross-platform sync (dry run, GA)
504
+ nerviq harmony-sync --fix Apply cross-platform sync (write files, GA)
505
+ nerviq harmony-sync --json JSON output for CI/automation
506
+ nerviq harmony-add <platform> Add a new platform to the project
507
+ nerviq synergy-report [EXPERIMENTAL] Static-rule multi-agent amplification report
428
508
  nerviq convert --from X --to Y Convert configs between platforms
429
509
  nerviq migrate --platform X Platform version migration helper
430
510
  nerviq migrate --platform cursor --from v2 --to v3
431
511
 
432
512
  MONITOR
433
- nerviq dashboard Generate static HTML dashboard report
513
+ nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
434
514
  nerviq dashboard --out F Save dashboard to custom file
435
515
  nerviq dashboard --open Open dashboard in browser after generating
436
516
  nerviq watch Live config monitoring (re-audits on file change)
437
- nerviq history Score history from saved snapshots
438
- nerviq compare Latest vs previous snapshot diff
439
- nerviq trend Score trend over time
440
- nerviq trend --out report.md Export trend report as markdown
441
- nerviq feedback Record recommendation outcomes
517
+ nerviq history Audit snapshot history from saved snapshots
518
+ nerviq compare Detailed per-check diff between latest two audit snapshots
519
+ nerviq trend Audit snapshot trend over time
520
+ nerviq trend --out report.md Export trend report as markdown
521
+ nerviq audit --snapshot --tag "pre-refactor" Save a named audit snapshot
522
+ nerviq feedback Record recommendation outcomes
442
523
 
443
524
  TEAM PROFILES
444
525
  nerviq profile save <name> Save current preferences as a named profile
@@ -448,7 +529,7 @@ const HELP = `
448
529
 
449
530
  ADVANCED
450
531
  nerviq deep-review AI-powered config review (opt-in, uses API key)
451
- nerviq serve --port 3000 Start local Nerviq REST API server
532
+ nerviq serve --port 3000 Start local Nerviq REST API server + OpenAPI contract
452
533
  nerviq badge Generate shields.io badge markdown
453
534
  nerviq rules-export Export recommendation rules as JSON
454
535
  nerviq rules-export --out F Save rules to file
@@ -463,32 +544,37 @@ const HELP = `
463
544
  --only A,B Limit plan/apply to selected proposal IDs
464
545
  --profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
465
546
  --team-profile N Load a saved team profile for audit (overrides threshold/platform)
466
- --mcp-pack A,B Merge MCP packs into setup (e.g. context7-docs,next-devtools)
467
- --check-version V Pin catalog to a specific version (warn on mismatch)
468
- --format NAME Output format: json | sarif | otel
469
- --webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
470
- --external PATH Benchmark an external repo instead of cwd
471
- --port N Port for \`serve\` (default: 3000)
472
- --workspace GLOBS Audit workspaces separately (e.g. packages/* or apps/web,apps/api)
473
- --snapshot Save snapshot artifact under .claude/nerviq/snapshots/
474
- --full Show full audit output (all checks, weakest areas, badge)
547
+ --mcp-pack A,B Merge MCP packs into setup (live tool connectors; e.g. context7-docs,next-devtools)
548
+ --check-version V Pin catalog to a specific version (warn on mismatch)
549
+ --format NAME Output format: json | sarif | otel
550
+ --webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
551
+ --webhook-header H Add a custom webhook header (repeat; format: Name: Value)
552
+ --webhook-retries N Retry transient webhook failures N times (default: 2)
553
+ --external PATH Benchmark an external repo instead of cwd
554
+ --port N Port for \`serve\` (default: 3000)
555
+ --workspace GLOBS Audit workspaces separately with root/package score semantics and stack-specific profiles
556
+ --snapshot Save snapshot artifact under .claude/nerviq/snapshots/
557
+ --tag LABEL Tag the saved snapshot (use with --snapshot; repeat or comma-separate for more)
558
+ --full Show full audit output (all checks, weakest areas, badge)
475
559
  --lite Short top-3 scan (default behavior since v1.5.2)
476
560
  --dry-run Preview changes without writing files
477
561
  --config-only Only write config files (.claude/, rules, hooks) — never source code
478
562
  --verbose Full audit + medium-priority recommendations
479
- --show-deprecated Show deprecated checks (excluded from scoring)
480
- --json Output as JSON
481
- --auto Apply all generated files without prompting
482
- --key NAME Feedback: recommendation key (e.g. permissionDeny)
563
+ --show-deprecated Show deprecated checks (excluded from scoring)
564
+ --json Output as JSON
565
+ --auto Apply all generated files without prompting
566
+ --beginner Show only the 5 starter commands for first-time users
567
+ --key NAME Feedback: recommendation key (e.g. permissionDeny)
483
568
  --status VALUE Feedback: accepted | rejected | deferred
484
569
  --effect VALUE Feedback: positive | neutral | negative
485
570
  --score-delta N Feedback: observed score delta
486
571
  --help Show this help
487
572
  --version Show version
488
573
 
489
- EXAMPLES
490
- npx nerviq
491
- npx nerviq --lite
574
+ EXAMPLES
575
+ npx nerviq --beginner
576
+ npx nerviq
577
+ npx nerviq --lite
492
578
  npx nerviq --platform cursor
493
579
  npx nerviq audit --workspace packages/*
494
580
  npx nerviq --platform codex augment
@@ -505,9 +591,34 @@ const HELP = `
505
591
  npx nerviq feedback --key permissionDeny --status accepted --effect positive
506
592
 
507
593
  EXIT CODES
508
- 0 Success
509
- 1 Error, unknown command, or score below --threshold
510
- `;
594
+ 0 Success
595
+ 1 Error, unknown command, or score below --threshold
596
+ `;
597
+
598
+ const BEGINNER_HELP = `
599
+ nerviq v${version}
600
+ Start here.
601
+
602
+ If this is your first time, learn just these 5 commands:
603
+
604
+ STARTER COMMANDS
605
+ nerviq audit Score the repo and show the top gaps
606
+ nerviq setup Generate a starter-safe baseline
607
+ nerviq fix Fix what can be fixed or show manual fix guidance
608
+ nerviq augment Show an improvement plan without writing
609
+ nerviq doctor Check install health, freshness, and platform detection
610
+
611
+ SIMPLE PATH
612
+ 1. nerviq audit
613
+ 2. nerviq setup --auto
614
+ 3. nerviq fix --all-critical --auto
615
+ 4. nerviq augment
616
+ 5. nerviq doctor
617
+
618
+ WHEN YOU ARE READY
619
+ nerviq --help Show the full command set
620
+ Docs: https://nerviq.net/docs/getting-started
621
+ `;
511
622
 
512
623
  async function main() {
513
624
  let parsed;
@@ -518,24 +629,29 @@ async function main() {
518
629
  process.exit(1);
519
630
  }
520
631
 
521
- const { flags, command, normalizedCommand } = parsed;
632
+ const { flags, command, commandExplicit, normalizedCommand } = parsed;
522
633
 
523
634
  // Initialize i18n with --lang flag or NERVIQ_LANG env var
524
635
  if (parsed.lang) {
525
636
  initI18n(parsed.lang);
526
637
  }
527
638
 
528
- if (flags.includes('--help') || command === 'help') {
529
- console.log(HELP);
530
- process.exit(0);
531
- }
532
-
533
- if (flags.includes('--version') || command === 'version') {
534
- console.log(version);
535
- process.exit(0);
536
- }
537
-
538
- const options = {
639
+ if (flags.includes('--version') || command === 'version') {
640
+ console.log(version);
641
+ process.exit(0);
642
+ }
643
+
644
+ if (flags.includes('--beginner') && (!commandExplicit || flags.includes('--help') || command === 'help')) {
645
+ console.log(BEGINNER_HELP);
646
+ process.exit(0);
647
+ }
648
+
649
+ if (flags.includes('--help') || command === 'help') {
650
+ console.log(HELP);
651
+ process.exit(0);
652
+ }
653
+
654
+ const options = {
539
655
  verbose: flags.includes('--verbose'),
540
656
  json: flags.includes('--json'),
541
657
  auto: flags.includes('--auto'),
@@ -557,13 +673,21 @@ async function main() {
557
673
  require: parsed.requireChecks,
558
674
  platform: parsed.platform || 'claude',
559
675
  format: parsed.format || null,
560
- port: parsed.port !== null ? Number(parsed.port) : null,
561
- workspace: parsed.workspace || null,
562
- webhookUrl: parsed.webhookUrl || null,
563
- lang: parsed.lang || null,
564
- external: parsed.external || null,
565
- dir: process.cwd()
566
- };
676
+ port: parsed.port !== null ? Number(parsed.port) : null,
677
+ workspace: parsed.workspace || null,
678
+ webhookUrl: parsed.webhookUrl || null,
679
+ webhookHeaders: Object.fromEntries((parsed.webhookHeaders || []).map((entry) => [entry.name, entry.value])),
680
+ webhookRetries: parsed.webhookRetries ?? 2,
681
+ lang: parsed.lang || null,
682
+ external: parsed.external || null,
683
+ snapshotTags: parsed.snapshotTags || [],
684
+ dir: process.cwd()
685
+ };
686
+
687
+ if (options.snapshotTags.length > 0 && !options.snapshot) {
688
+ console.error('\n Error: --tag requires --snapshot.\n');
689
+ process.exit(1);
690
+ }
567
691
 
568
692
  if (parsed.checkVersion) {
569
693
  if (parsed.checkVersion !== version) {
@@ -746,9 +870,9 @@ async function main() {
746
870
  process.exit(1);
747
871
  }
748
872
  process.exit(0);
749
- } else if (normalizedCommand === 'history') {
750
- const { formatHistory, readSnapshotIndex } = require('../src/activity');
751
- // Handle --prune N
873
+ } else if (normalizedCommand === 'history') {
874
+ const { formatHistory, readSnapshotIndex } = require('../src/activity');
875
+ // Handle --prune N
752
876
  const pruneIdx = flags.indexOf('--prune');
753
877
  if (pruneIdx >= 0) {
754
878
  const keepCount = parseInt(flags[pruneIdx + 1] || parsed.extraArgs[0], 10) || 10;
@@ -756,7 +880,7 @@ async function main() {
756
880
  const pathMod = require('path');
757
881
  const entries = readSnapshotIndex(options.dir);
758
882
  if (entries.length <= keepCount) {
759
- console.log(`\n Nothing to prune (${entries.length} snapshots, keeping ${keepCount}).\n`);
883
+ console.log(`\n Nothing to prune (${entries.length} audit snapshots, keeping ${keepCount}).\n`);
760
884
  } else {
761
885
  const toRemove = entries.slice(0, entries.length - keepCount);
762
886
  let removed = 0;
@@ -767,42 +891,78 @@ async function main() {
767
891
  const kept = entries.slice(entries.length - keepCount);
768
892
  const indexPath = pathMod.join(options.dir, '.nerviq', 'snapshots', 'index.json');
769
893
  try { fsMod.writeFileSync(indexPath, JSON.stringify(kept, null, 2), 'utf8'); } catch {}
770
- console.log(`\n Pruned ${removed} snapshots, kept ${kept.length}.\n`);
894
+ console.log(`\n Pruned ${removed} audit snapshots, kept ${kept.length}.\n`);
771
895
  }
772
896
  process.exit(0);
773
897
  }
774
898
  console.log('');
775
- console.log(formatHistory(options.dir));
776
- console.log('');
777
- process.exit(0);
778
- } else if (normalizedCommand === 'compare') {
779
- const { compareLatest } = require('../src/activity');
780
- const result = compareLatest(options.dir);
781
- if (!result) {
782
- console.log('\n Need at least 2 snapshots to compare. Run `npx nerviq --snapshot` twice.\n');
783
- process.exit(0);
784
- }
899
+ console.log(formatHistory(options.dir));
900
+ console.log('');
901
+ process.exit(0);
902
+ } else if (normalizedCommand === 'compare') {
903
+ const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags } = require('../src/activity');
904
+ const result = compareLatest(options.dir);
905
+ if (!result) {
906
+ console.log('');
907
+ console.log(formatSnapshotBootstrap(options.dir, 'compare'));
908
+ console.log('');
909
+ process.exit(0);
910
+ }
785
911
  if (options.json) {
786
912
  console.log(JSON.stringify(result, null, 2));
787
- } else {
788
- const sign = result.delta.score >= 0 ? '+' : '';
789
- console.log('');
790
- console.log(` Previous: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})`);
791
- console.log(` Current: ${result.current.score}/100 (${result.current.date?.split('T')[0]})`);
792
- console.log(` Delta: ${sign}${result.delta.score} points`);
793
- console.log(` Trend: ${result.trend}`);
794
- if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
795
- if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
796
- console.log('');
797
- }
798
- process.exit(0);
799
- } else if (normalizedCommand === 'trend') {
800
- const { exportTrendReport } = require('../src/activity');
801
- const report = exportTrendReport(options.dir);
802
- if (!report) {
803
- console.log('\n No snapshots found. Run `npx nerviq --snapshot` to start tracking.\n');
804
- process.exit(0);
805
- }
913
+ } else {
914
+ const sign = result.delta.score >= 0 ? '+' : '';
915
+ console.log('');
916
+ console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})${formatSnapshotTags(result.previous.tags)}`);
917
+ console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})${formatSnapshotTags(result.current.tags)}`);
918
+ console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
919
+ console.log(` Trend: ${result.trend}`);
920
+ if (result.detailedDiffAvailable) {
921
+ console.log('');
922
+ console.log(' Detailed check diff:');
923
+ printCompareCheckSection('Regressions', result.regressionDetails, '🔴');
924
+ printCompareCheckSection('Improvements', result.improvementDetails, '✅');
925
+ printCompareCheckSection('Newly applicable', result.newlyApplicableDetails, '🆕');
926
+ printCompareCheckSection('No longer applicable', result.noLongerApplicableDetails, '');
927
+ if (Array.isArray(result.newChecks) && result.newChecks.length > 0) {
928
+ printCompareCheckSection('New checks', result.newChecks, '➕');
929
+ }
930
+ if (Array.isArray(result.removedChecks) && result.removedChecks.length > 0) {
931
+ printCompareCheckSection('Removed checks', result.removedChecks, '➖');
932
+ }
933
+ if (
934
+ result.regressionDetails.length === 0 &&
935
+ result.improvementDetails.length === 0 &&
936
+ result.newlyApplicableDetails.length === 0 &&
937
+ result.noLongerApplicableDetails.length === 0 &&
938
+ result.newChecks.length === 0 &&
939
+ result.removedChecks.length === 0
940
+ ) {
941
+ console.log(' No per-check state changes detected.');
942
+ }
943
+ } else {
944
+ if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
945
+ if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
946
+ }
947
+ console.log('');
948
+ }
949
+ process.exit(0);
950
+ } else if (normalizedCommand === 'trend') {
951
+ const { exportTrendReport, getHistory, formatSnapshotBootstrap } = require('../src/activity');
952
+ const auditHistory = getHistory(options.dir, 2);
953
+ if (auditHistory.length < 2) {
954
+ console.log('');
955
+ console.log(formatSnapshotBootstrap(options.dir, 'trend'));
956
+ console.log('');
957
+ process.exit(0);
958
+ }
959
+ const report = exportTrendReport(options.dir);
960
+ if (!report) {
961
+ console.log('');
962
+ console.log(formatSnapshotBootstrap(options.dir, 'trend'));
963
+ console.log('');
964
+ process.exit(0);
965
+ }
806
966
  if (options.out) {
807
967
  require('fs').writeFileSync(options.out, report, 'utf8');
808
968
  console.log(`\n Trend report exported to ${options.out}\n`);
@@ -904,9 +1064,10 @@ async function main() {
904
1064
  process.exit(0);
905
1065
  } else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
906
1066
  const report = await analyzeProject({ ...options, mode: normalizedCommand });
907
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
908
- sourceCommand: normalizedCommand,
909
- }) : null;
1067
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
1068
+ tags: options.snapshotTags,
1069
+ sourceCommand: normalizedCommand,
1070
+ }) : null;
910
1071
  if (options.out && !options.json) {
911
1072
  const fs = require('fs');
912
1073
  const md = exportMarkdown(report);
@@ -1037,9 +1198,10 @@ async function main() {
1037
1198
  fs.writeFileSync(options.out, content, 'utf8');
1038
1199
  }
1039
1200
  printGovernanceSummary(summary, options);
1040
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
1041
- sourceCommand: normalizedCommand,
1042
- }) : null;
1201
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
1202
+ tags: options.snapshotTags,
1203
+ sourceCommand: normalizedCommand,
1204
+ }) : null;
1043
1205
  if (options.out && !options.json) {
1044
1206
  console.log(` Governance report written to ${options.out}`);
1045
1207
  console.log('');
@@ -1051,9 +1213,10 @@ async function main() {
1051
1213
  }
1052
1214
  } else if (normalizedCommand === 'benchmark') {
1053
1215
  const report = await runBenchmark(options);
1054
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
1055
- sourceCommand: normalizedCommand,
1056
- }) : null;
1216
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
1217
+ tags: options.snapshotTags,
1218
+ sourceCommand: normalizedCommand,
1219
+ }) : null;
1057
1220
  if (options.out) {
1058
1221
  writeBenchmarkReport(report, options.out);
1059
1222
  }
@@ -1147,9 +1310,10 @@ async function main() {
1147
1310
  const address = server.address();
1148
1311
  const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
1149
1312
  console.log('');
1150
- console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
1151
- console.log(' Endpoints: /api/health, /api/catalog, /api/audit, /api/harmony');
1152
- console.log('');
1313
+ console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
1314
+ console.log(' Endpoints: /api/openapi.json, /api/health, /api/catalog, /api/audit, /api/harmony');
1315
+ console.log(` Contract: http://127.0.0.1:${resolvedPort}/api/openapi.json`);
1316
+ console.log('');
1153
1317
 
1154
1318
  const closeServer = () => {
1155
1319
  server.close(() => process.exit(0));
@@ -1927,14 +2091,15 @@ async function main() {
1927
2091
  process.exit(0);
1928
2092
  } else if (normalizedCommand === 'setup') {
1929
2093
  await setup(options);
1930
- if (options.snapshot) {
1931
- const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
1932
- const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
1933
- sourceCommand: 'setup',
1934
- });
1935
- if (!options.json) {
1936
- console.log(` Snapshot saved: ${snapshot.relativePath}`);
1937
- }
2094
+ if (options.snapshot) {
2095
+ const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2096
+ const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
2097
+ tags: options.snapshotTags,
2098
+ sourceCommand: 'setup',
2099
+ });
2100
+ if (!options.json) {
2101
+ console.log(` Snapshot saved: ${snapshot.relativePath}`);
2102
+ }
1938
2103
  }
1939
2104
  } else {
1940
2105
  if (options.workspace) {
@@ -1972,18 +2137,26 @@ async function main() {
1972
2137
  // Generic webhook: send full JSON audit result
1973
2138
  payload = { platform: result.platform, score: result.score, passed: result.passed, failed: result.failed, results: result.results };
1974
2139
  }
1975
- const webhookResp = await sendWebhook(options.webhookUrl, payload);
1976
- if (!options.json) {
1977
- if (webhookResp.ok) {
1978
- console.log(` Webhook sent: ${options.webhookUrl} (${webhookResp.status})`);
1979
- } else {
1980
- console.error(` Webhook failed: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
1981
- }
1982
- }
1983
- } catch (webhookErr) {
1984
- if (!options.json) console.error(` Webhook error: ${webhookErr.message}`);
1985
- }
1986
- }
2140
+ const webhookResp = await sendWebhook(options.webhookUrl, payload, {
2141
+ headers: options.webhookHeaders,
2142
+ retries: options.webhookRetries,
2143
+ });
2144
+ if (!options.json) {
2145
+ if (webhookResp.ok) {
2146
+ const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
2147
+ console.log(` Webhook sent${retryNote}: ${options.webhookUrl} (${webhookResp.status})`);
2148
+ } else {
2149
+ const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
2150
+ console.error(` Webhook failed${retryNote}: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
2151
+ }
2152
+ }
2153
+ } catch (webhookErr) {
2154
+ if (!options.json) {
2155
+ const retryNote = webhookErr.attempts > 1 ? ` after ${webhookErr.attempts} attempts` : '';
2156
+ console.error(` Webhook error${retryNote}: ${webhookErr.message}`);
2157
+ }
2158
+ }
2159
+ }
1987
2160
  if (options.feedback && !options.json && options.format === null) {
1988
2161
  const feedbackTargets = options.lite
1989
2162
  ? (result.liteSummary?.topNextActions || [])
@@ -2003,9 +2176,10 @@ async function main() {
2003
2176
  console.log('');
2004
2177
  }
2005
2178
  }
2006
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
2007
- sourceCommand: normalizedCommand,
2008
- }) : null;
2179
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
2180
+ tags: options.snapshotTags,
2181
+ sourceCommand: normalizedCommand,
2182
+ }) : null;
2009
2183
  if (snapshot && !options.json) {
2010
2184
  console.log(` Snapshot saved: ${snapshot.relativePath}`);
2011
2185
  console.log(` Snapshot index: ${snapshot.indexPath}`);