@nerviq/cli 1.10.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,9 +58,30 @@ function suggestCommand(input) {
58
58
  bestDistance = distance;
59
59
  }
60
60
  }
61
- return bestDistance <= 3 ? best : null;
62
- }
63
-
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
+
64
85
  function parseArgs(rawArgs) {
65
86
  const flags = [];
66
87
  let command = 'audit';
@@ -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;
@@ -98,11 +122,11 @@ function parseArgs(rawArgs) {
98
122
  for (let i = 0; i < rawArgs.length; i++) {
99
123
  const arg = rawArgs[i];
100
124
 
101
- 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') {
102
- const value = rawArgs[i + 1];
103
- if (!value || value.startsWith('--')) {
104
- throw new Error(`${arg} requires a value`);
105
- }
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
+ }
106
130
  if (arg === '--threshold') threshold = value;
107
131
  if (arg === '--out') out = value;
108
132
  if (arg === '--plan') planFile = value;
@@ -121,15 +145,18 @@ function parseArgs(rawArgs) {
121
145
  if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
122
146
  if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
123
147
  if (arg === '--port') port = value.trim();
124
- if (arg === '--workspace') workspace = value.trim();
125
- if (arg === '--check-version') checkVersion = value.trim();
126
- if (arg === '--webhook') webhookUrl = value.trim();
127
- if (arg === '--external') external = value.trim();
128
- if (arg === '--team-profile') teamProfile = value.trim();
129
- if (arg === '--lang') lang = value.trim().toLowerCase();
130
- i++;
131
- continue;
132
- }
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
+ }
133
160
 
134
161
  if (arg.startsWith('--lang=')) {
135
162
  lang = arg.split('=').slice(1).join('=').trim().toLowerCase();
@@ -141,10 +168,15 @@ function parseArgs(rawArgs) {
141
168
  continue;
142
169
  }
143
170
 
144
- if (arg.startsWith('--external=')) {
145
- external = arg.split('=').slice(1).join('=').trim();
146
- continue;
147
- }
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
+ }
148
180
 
149
181
  if (arg === '--repos') {
150
182
  // Collect all following non-flag args as repo paths (supports comma-separated too)
@@ -247,14 +279,29 @@ function parseArgs(rawArgs) {
247
279
  continue;
248
280
  }
249
281
 
250
- if (arg.startsWith('--check-version=')) {
251
- checkVersion = arg.split('=').slice(1).join('=').trim();
252
- continue;
253
- }
254
-
255
- if (arg.startsWith('--')) {
256
- flags.push(arg);
257
- 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;
258
305
  }
259
306
 
260
307
  if (!commandSet) {
@@ -268,7 +315,7 @@ function parseArgs(rawArgs) {
268
315
 
269
316
  const normalizedCommand = COMMAND_ALIASES[command] || command;
270
317
 
271
- 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, external, repos, teamProfile, lang };
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 };
272
319
  }
273
320
 
274
321
  function printWorkspaceSummary(summary, options) {
@@ -291,20 +338,41 @@ function printWorkspaceSummary(summary, options) {
291
338
  }
292
339
  console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
293
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
+ }
294
347
  console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
295
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.');
296
350
  console.log('');
297
- console.log('\x1b[1m Workspace Audit Pass Total Top action\x1b[0m');
298
- console.log(' ' + '─'.repeat(72));
351
+ console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
352
+ console.log(' ' + '─'.repeat(96));
299
353
  for (const item of summary.workspaces) {
300
354
  const score = item.score === null ? 'ERR' : String(item.score);
301
- const topAction = item.error || item.topAction || '-';
302
- console.log(` ${item.workspace.padEnd(26)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
303
- }
304
- console.log('');
305
- }
306
-
307
- function printScanDetail(summary, options) {
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) {
308
376
  if (options.json) {
309
377
  console.log(JSON.stringify(summary, null, 2));
310
378
  return;
@@ -388,7 +456,7 @@ const HELP = `
388
456
  nerviq audit --full Full audit with all checks, weakest areas, badge
389
457
  nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
390
458
  nerviq audit --json Machine-readable JSON output (for CI)
391
- nerviq audit --workspace packages/* Audit monorepo workspaces with root-vs-package score semantics
459
+ nerviq audit --workspace packages/* Audit monorepo workspaces with stack-specific package profiles
392
460
  nerviq scan dir1 dir2 Compare multiple repos side-by-side
393
461
  nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
394
462
  nerviq catalog Full check catalog (all 8 platforms)
@@ -423,7 +491,7 @@ const HELP = `
423
491
  nerviq apply --dry-run Preview changes without writing
424
492
 
425
493
  GOVERN
426
- nerviq governance Permission profiles + hooks + policy packs
494
+ nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
427
495
  nerviq governance --json Machine-readable governance summary
428
496
  nerviq benchmark Baseline vs projected score in isolated temp copy
429
497
  nerviq benchmark --external /path Benchmark an external repo
@@ -447,10 +515,11 @@ const HELP = `
447
515
  nerviq dashboard --open Open dashboard in browser after generating
448
516
  nerviq watch Live config monitoring (re-audits on file change)
449
517
  nerviq history Audit snapshot history from saved snapshots
450
- nerviq compare Latest vs previous audit snapshot diff
518
+ nerviq compare Detailed per-check diff between latest two audit snapshots
451
519
  nerviq trend Audit snapshot trend over time
452
- nerviq trend --out report.md Export trend report as markdown
453
- nerviq feedback Record recommendation outcomes
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
454
523
 
455
524
  TEAM PROFILES
456
525
  nerviq profile save <name> Save current preferences as a named profile
@@ -460,7 +529,7 @@ const HELP = `
460
529
 
461
530
  ADVANCED
462
531
  nerviq deep-review AI-powered config review (opt-in, uses API key)
463
- nerviq serve --port 3000 Start local Nerviq REST API server
532
+ nerviq serve --port 3000 Start local Nerviq REST API server + OpenAPI contract
464
533
  nerviq badge Generate shields.io badge markdown
465
534
  nerviq rules-export Export recommendation rules as JSON
466
535
  nerviq rules-export --out F Save rules to file
@@ -475,15 +544,18 @@ const HELP = `
475
544
  --only A,B Limit plan/apply to selected proposal IDs
476
545
  --profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
477
546
  --team-profile N Load a saved team profile for audit (overrides threshold/platform)
478
- --mcp-pack A,B Merge MCP packs into setup (e.g. context7-docs,next-devtools)
479
- --check-version V Pin catalog to a specific version (warn on mismatch)
480
- --format NAME Output format: json | sarif | otel
481
- --webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
482
- --external PATH Benchmark an external repo instead of cwd
483
- --port N Port for \`serve\` (default: 3000)
484
- --workspace GLOBS Audit workspaces separately and label root governance vs package scores
485
- --snapshot Save snapshot artifact under .claude/nerviq/snapshots/
486
- --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)
487
559
  --lite Short top-3 scan (default behavior since v1.5.2)
488
560
  --dry-run Preview changes without writing files
489
561
  --config-only Only write config files (.claude/, rules, hooks) — never source code
@@ -579,7 +651,7 @@ async function main() {
579
651
  process.exit(0);
580
652
  }
581
653
 
582
- const options = {
654
+ const options = {
583
655
  verbose: flags.includes('--verbose'),
584
656
  json: flags.includes('--json'),
585
657
  auto: flags.includes('--auto'),
@@ -601,13 +673,21 @@ async function main() {
601
673
  require: parsed.requireChecks,
602
674
  platform: parsed.platform || 'claude',
603
675
  format: parsed.format || null,
604
- port: parsed.port !== null ? Number(parsed.port) : null,
605
- workspace: parsed.workspace || null,
606
- webhookUrl: parsed.webhookUrl || null,
607
- lang: parsed.lang || null,
608
- external: parsed.external || null,
609
- dir: process.cwd()
610
- };
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
+ }
611
691
 
612
692
  if (parsed.checkVersion) {
613
693
  if (parsed.checkVersion !== version) {
@@ -820,7 +900,7 @@ async function main() {
820
900
  console.log('');
821
901
  process.exit(0);
822
902
  } else if (normalizedCommand === 'compare') {
823
- const { compareLatest, formatSnapshotBootstrap } = require('../src/activity');
903
+ const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags } = require('../src/activity');
824
904
  const result = compareLatest(options.dir);
825
905
  if (!result) {
826
906
  console.log('');
@@ -830,16 +910,41 @@ async function main() {
830
910
  }
831
911
  if (options.json) {
832
912
  console.log(JSON.stringify(result, null, 2));
833
- } else {
834
- const sign = result.delta.score >= 0 ? '+' : '';
835
- console.log('');
836
- console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})`);
837
- console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})`);
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)}`);
838
918
  console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
839
919
  console.log(` Trend: ${result.trend}`);
840
- if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
841
- if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
842
- console.log('');
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('');
843
948
  }
844
949
  process.exit(0);
845
950
  } else if (normalizedCommand === 'trend') {
@@ -959,9 +1064,10 @@ async function main() {
959
1064
  process.exit(0);
960
1065
  } else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
961
1066
  const report = await analyzeProject({ ...options, mode: normalizedCommand });
962
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
963
- sourceCommand: normalizedCommand,
964
- }) : null;
1067
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
1068
+ tags: options.snapshotTags,
1069
+ sourceCommand: normalizedCommand,
1070
+ }) : null;
965
1071
  if (options.out && !options.json) {
966
1072
  const fs = require('fs');
967
1073
  const md = exportMarkdown(report);
@@ -1092,9 +1198,10 @@ async function main() {
1092
1198
  fs.writeFileSync(options.out, content, 'utf8');
1093
1199
  }
1094
1200
  printGovernanceSummary(summary, options);
1095
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
1096
- sourceCommand: normalizedCommand,
1097
- }) : null;
1201
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
1202
+ tags: options.snapshotTags,
1203
+ sourceCommand: normalizedCommand,
1204
+ }) : null;
1098
1205
  if (options.out && !options.json) {
1099
1206
  console.log(` Governance report written to ${options.out}`);
1100
1207
  console.log('');
@@ -1106,9 +1213,10 @@ async function main() {
1106
1213
  }
1107
1214
  } else if (normalizedCommand === 'benchmark') {
1108
1215
  const report = await runBenchmark(options);
1109
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
1110
- sourceCommand: normalizedCommand,
1111
- }) : null;
1216
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
1217
+ tags: options.snapshotTags,
1218
+ sourceCommand: normalizedCommand,
1219
+ }) : null;
1112
1220
  if (options.out) {
1113
1221
  writeBenchmarkReport(report, options.out);
1114
1222
  }
@@ -1202,9 +1310,10 @@ async function main() {
1202
1310
  const address = server.address();
1203
1311
  const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
1204
1312
  console.log('');
1205
- console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
1206
- console.log(' Endpoints: /api/health, /api/catalog, /api/audit, /api/harmony');
1207
- 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('');
1208
1317
 
1209
1318
  const closeServer = () => {
1210
1319
  server.close(() => process.exit(0));
@@ -1982,14 +2091,15 @@ async function main() {
1982
2091
  process.exit(0);
1983
2092
  } else if (normalizedCommand === 'setup') {
1984
2093
  await setup(options);
1985
- if (options.snapshot) {
1986
- const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
1987
- const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
1988
- sourceCommand: 'setup',
1989
- });
1990
- if (!options.json) {
1991
- console.log(` Snapshot saved: ${snapshot.relativePath}`);
1992
- }
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
+ }
1993
2103
  }
1994
2104
  } else {
1995
2105
  if (options.workspace) {
@@ -2027,18 +2137,26 @@ async function main() {
2027
2137
  // Generic webhook: send full JSON audit result
2028
2138
  payload = { platform: result.platform, score: result.score, passed: result.passed, failed: result.failed, results: result.results };
2029
2139
  }
2030
- const webhookResp = await sendWebhook(options.webhookUrl, payload);
2031
- if (!options.json) {
2032
- if (webhookResp.ok) {
2033
- console.log(` Webhook sent: ${options.webhookUrl} (${webhookResp.status})`);
2034
- } else {
2035
- console.error(` Webhook failed: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
2036
- }
2037
- }
2038
- } catch (webhookErr) {
2039
- if (!options.json) console.error(` Webhook error: ${webhookErr.message}`);
2040
- }
2041
- }
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
+ }
2042
2160
  if (options.feedback && !options.json && options.format === null) {
2043
2161
  const feedbackTargets = options.lite
2044
2162
  ? (result.liteSummary?.topNextActions || [])
@@ -2058,9 +2176,10 @@ async function main() {
2058
2176
  console.log('');
2059
2177
  }
2060
2178
  }
2061
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
2062
- sourceCommand: normalizedCommand,
2063
- }) : null;
2179
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
2180
+ tags: options.snapshotTags,
2181
+ sourceCommand: normalizedCommand,
2182
+ }) : null;
2064
2183
  if (snapshot && !options.json) {
2065
2184
  console.log(` Snapshot saved: ${snapshot.relativePath}`);
2066
2185
  console.log(` Snapshot index: ${snapshot.indexPath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,438 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {