@nerviq/cli 1.12.0 → 1.14.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.
@@ -346,6 +346,237 @@ async function runHarmonyGovernance(options) {
346
346
  return summary;
347
347
  }
348
348
 
349
+ // ─── Command: harmony score ──────────────────────────────────────────────────
350
+
351
+ /**
352
+ * Output a standalone Harmony Score (0-100) with optional badge and CI threshold.
353
+ *
354
+ * Options:
355
+ * --json JSON output
356
+ * --badge Print shields.io badge markdown
357
+ * --threshold N Exit with code 1 if score < N (for CI gates)
358
+ * --quiet Score number only (for piping)
359
+ */
360
+ async function runHarmonyScore(options) {
361
+ const dir = resolveDir(options);
362
+ const { harmonyAudit } = require('./audit');
363
+ const result = await harmonyAudit({ dir, silent: true });
364
+
365
+ const score = result.harmonyScore;
366
+ const threshold = parseInt(options.threshold, 10) || 0;
367
+ const pass = score >= threshold;
368
+
369
+ if (options.json) {
370
+ const output = {
371
+ harmonyScore: score,
372
+ platforms: result.platformScores,
373
+ activePlatforms: result.activePlatforms.map(p => p.platform),
374
+ driftCount: result.drift.drifts.length,
375
+ threshold: threshold || null,
376
+ pass,
377
+ };
378
+ if (options.badge) {
379
+ output.badge = getHarmonyBadgeMarkdown(score);
380
+ output.badgeUrl = getHarmonyBadgeUrl(score);
381
+ }
382
+ console.log(JSON.stringify(output, null, 2));
383
+ return output;
384
+ }
385
+
386
+ if (options.quiet) {
387
+ console.log(score);
388
+ return { score, pass };
389
+ }
390
+
391
+ console.log('');
392
+ console.log(c(' Harmony Score', 'bold'));
393
+ console.log(c(' ═══════════════════════════════════════', 'dim'));
394
+ console.log('');
395
+
396
+ // Score with color bar
397
+ const barWidth = 30;
398
+ const filled = Math.round((score / 100) * barWidth);
399
+ const empty = barWidth - filled;
400
+ const scoreColor = score >= 80 ? 'green' : score >= 50 ? 'yellow' : 'red';
401
+ const bar = c('\u2588'.repeat(filled), scoreColor) + c('\u2591'.repeat(empty), 'dim');
402
+ console.log(` ${bar} ${c(`${score}/100`, scoreColor)}`);
403
+ console.log('');
404
+
405
+ // Per-platform breakdown
406
+ for (const ap of result.activePlatforms) {
407
+ const ps = result.platformScores[ap.platform];
408
+ const psColor = ps >= 70 ? 'green' : ps >= 40 ? 'yellow' : 'red';
409
+ console.log(` ${ap.platform.padEnd(12)} ${ps !== null ? c(`${ps}/100`, psColor) : c('n/a', 'dim')}`);
410
+ }
411
+ console.log('');
412
+
413
+ // Drift summary
414
+ const driftCount = result.drift.drifts.length;
415
+ if (driftCount > 0) {
416
+ const critical = result.drift.drifts.filter(d => d.severity === 'critical').length;
417
+ const high = result.drift.drifts.filter(d => d.severity === 'high').length;
418
+ let driftMsg = ` ${driftCount} drift issue${driftCount !== 1 ? 's' : ''}`;
419
+ if (critical > 0) driftMsg += c(` (${critical} critical)`, 'red');
420
+ else if (high > 0) driftMsg += c(` (${high} high)`, 'yellow');
421
+ console.log(driftMsg);
422
+ console.log(c(' Run "nerviq harmony-audit" for details.', 'dim'));
423
+ console.log('');
424
+ }
425
+
426
+ // Badge output
427
+ if (options.badge) {
428
+ console.log(c(' Badge:', 'bold'));
429
+ console.log(` ${getHarmonyBadgeMarkdown(score)}`);
430
+ console.log('');
431
+ }
432
+
433
+ // Threshold check
434
+ if (threshold > 0) {
435
+ if (pass) {
436
+ console.log(c(` Threshold: ${score} >= ${threshold} PASS`, 'green'));
437
+ } else {
438
+ console.log(c(` Threshold: ${score} < ${threshold} FAIL`, 'red'));
439
+ }
440
+ console.log('');
441
+ }
442
+
443
+ return { score, pass, platforms: result.platformScores };
444
+ }
445
+
446
+ // ─── Harmony Badge helpers ───────────────────────────────────────────────────
447
+
448
+ function getHarmonyBadgeUrl(score) {
449
+ const color = score >= 80 ? 'brightgreen' : score >= 60 ? 'yellow' : score >= 40 ? 'orange' : 'red';
450
+ const label = encodeURIComponent('Harmony Score');
451
+ const message = encodeURIComponent(`${score}/100`);
452
+ return `https://img.shields.io/badge/${label}-${message}-${color}`;
453
+ }
454
+
455
+ function getHarmonyBadgeMarkdown(score) {
456
+ const url = getHarmonyBadgeUrl(score);
457
+ return `[![Harmony Score](${url})](https://github.com/nerviq/nerviq)`;
458
+ }
459
+
460
+ // ─── Command: harmony demo ──────────────────────────────────────────────────
461
+
462
+ /**
463
+ * Zero-setup demo: creates a temporary multi-platform project, runs harmony
464
+ * audit on it, and shows how Nerviq detects cross-platform drift.
465
+ *
466
+ * This lets new users see Harmony's value instantly without configuring anything.
467
+ */
468
+ async function runHarmonyDemo(options) {
469
+ const fs = require('fs');
470
+ const os = require('os');
471
+ const { harmonyAudit } = require('./audit');
472
+
473
+ console.log('');
474
+ console.log(c(' Harmony Demo — Zero-Setup Cross-Platform Drift Detection', 'bold'));
475
+ console.log(c(' ═══════════════════════════════════════════════════════', 'dim'));
476
+ console.log('');
477
+ console.log(c(' Creating a sample multi-platform project...', 'dim'));
478
+ console.log('');
479
+
480
+ // Create temp directory with realistic multi-platform configs
481
+ const demoDir = path.join(os.tmpdir(), `nerviq-harmony-demo-${Date.now()}`);
482
+ fs.mkdirSync(demoDir, { recursive: true });
483
+ fs.mkdirSync(path.join(demoDir, '.claude'), { recursive: true });
484
+ fs.mkdirSync(path.join(demoDir, '.cursor'), { recursive: true });
485
+ fs.mkdirSync(path.join(demoDir, '.github'), { recursive: true });
486
+
487
+ // Claude config — well-configured
488
+ fs.writeFileSync(path.join(demoDir, 'CLAUDE.md'), [
489
+ '# Project Instructions',
490
+ '',
491
+ '## Architecture',
492
+ 'This is a Node.js API with PostgreSQL. Use Express for routing.',
493
+ '',
494
+ '## Testing',
495
+ 'Run tests with `npm test`. All PRs require passing tests.',
496
+ '',
497
+ '## Security',
498
+ '- Never commit .env files',
499
+ '- Use parameterized queries for all database access',
500
+ '- Validate all user input',
501
+ '',
502
+ '## Code Style',
503
+ '- Use ESLint with the project config',
504
+ '- Prefer async/await over callbacks',
505
+ '- Add JSDoc comments for public functions',
506
+ ].join('\n'));
507
+
508
+ fs.writeFileSync(path.join(demoDir, '.claude', 'settings.json'), JSON.stringify({
509
+ permissions: {
510
+ allow: ['Read', 'Glob', 'Grep'],
511
+ deny: ['Bash(rm -rf *)'],
512
+ },
513
+ model: 'claude-sonnet-4-6',
514
+ }, null, 2));
515
+
516
+ // Cursor config — intentionally drifted (different rules, less security)
517
+ fs.writeFileSync(path.join(demoDir, '.cursorrules'), [
518
+ 'You are a helpful coding assistant.',
519
+ 'This is a Node.js project using Express.',
520
+ 'Write clean, readable code.',
521
+ // Missing: security rules, testing rules, architecture details
522
+ ].join('\n'));
523
+
524
+ // Copilot config — partial coverage
525
+ fs.writeFileSync(path.join(demoDir, '.github', 'copilot-instructions.md'), [
526
+ '# Copilot Instructions',
527
+ '',
528
+ 'This is a Node.js Express API project.',
529
+ 'Use TypeScript-style JSDoc annotations.',
530
+ 'Follow RESTful conventions for API endpoints.',
531
+ // Missing: security, testing, architecture details
532
+ ].join('\n'));
533
+
534
+ // Add a package.json for realism
535
+ fs.writeFileSync(path.join(demoDir, 'package.json'), JSON.stringify({
536
+ name: 'harmony-demo-project',
537
+ version: '1.0.0',
538
+ scripts: { test: 'jest' },
539
+ }, null, 2));
540
+
541
+ console.log(c(' Demo project created with 3 platforms:', 'bold'));
542
+ console.log(` ${c('Claude', 'green')} — Well-configured (CLAUDE.md + settings.json)`);
543
+ console.log(` ${c('Cursor', 'yellow')} — Basic rules only (.cursorrules)`);
544
+ console.log(` ${c('Copilot', 'yellow')} — Partial coverage (copilot-instructions.md)`);
545
+ console.log('');
546
+ console.log(c(' Intentional drift injected:', 'bold'));
547
+ console.log(` ${c('\u2718', 'red')} Security rules only in Claude, missing from Cursor & Copilot`);
548
+ console.log(` ${c('\u2718', 'red')} Testing instructions only in Claude`);
549
+ console.log(` ${c('\u2718', 'red')} Architecture details inconsistent across platforms`);
550
+ console.log(` ${c('\u2718', 'red')} Trust posture differs (Claude has explicit permissions)`);
551
+ console.log('');
552
+ console.log(c(' Running Harmony Audit...', 'dim'));
553
+ console.log('');
554
+
555
+ // Run the actual harmony audit on the demo project
556
+ const result = await harmonyAudit({ dir: demoDir, silent: false, verbose: !!options.verbose });
557
+
558
+ console.log('');
559
+ console.log(c(' ═══════════════════════════════════════════════════════', 'dim'));
560
+ console.log(c(' What you just saw:', 'bold'));
561
+ console.log('');
562
+ console.log(' Nerviq Harmony detected real configuration drift between');
563
+ console.log(' 3 AI coding platforms in your project — differences in');
564
+ console.log(' instructions, security posture, and tool coverage that');
565
+ console.log(' cause inconsistent AI behavior.');
566
+ console.log('');
567
+ console.log(c(' Try it on your own project:', 'bold'));
568
+ console.log(` ${c('npx @nerviq/cli harmony-audit', 'blue')}`);
569
+ console.log(` ${c('npx @nerviq/cli harmony-score --threshold 70', 'blue')}`);
570
+ console.log('');
571
+
572
+ // Clean up
573
+ try {
574
+ fs.rmSync(demoDir, { recursive: true, force: true });
575
+ } catch (_e) { /* cleanup optional */ }
576
+
577
+ return result;
578
+ }
579
+
349
580
  module.exports = {
350
581
  runHarmonyAudit,
351
582
  runHarmonySync,
@@ -353,4 +584,8 @@ module.exports = {
353
584
  runHarmonyAdvise,
354
585
  runHarmonyWatch,
355
586
  runHarmonyGovernance,
587
+ runHarmonyScore,
588
+ runHarmonyDemo,
589
+ getHarmonyBadgeUrl,
590
+ getHarmonyBadgeMarkdown,
356
591
  };
package/src/index.js CHANGED
@@ -240,6 +240,7 @@ module.exports = {
240
240
  const { startHarmonyWatch } = require('./harmony/watch');
241
241
  const { saveHarmonyState, loadHarmonyState, getHarmonyHistory } = require('./harmony/memory');
242
242
  const { getHarmonyGovernanceSummary, formatHarmonyGovernanceReport } = require('./harmony/governance');
243
+ const { getHarmonyBadgeUrl, getHarmonyBadgeMarkdown } = require('./harmony/cli');
243
244
  return {
244
245
  buildCanonicalModel, detectActivePlatforms, detectDrift, formatDriftReport,
245
246
  harmonyAudit, formatHarmonyAuditReport,
@@ -247,6 +248,7 @@ module.exports = {
247
248
  generateStrategicAdvice, PLATFORM_STRENGTHS,
248
249
  startHarmonyWatch, saveHarmonyState, loadHarmonyState, getHarmonyHistory,
249
250
  getHarmonyGovernanceSummary, formatHarmonyGovernanceReport,
251
+ getHarmonyBadgeUrl, getHarmonyBadgeMarkdown,
250
252
  };
251
253
  })(),
252
254
  // Synergy (cross-platform amplification)
package/src/mcp-server.js CHANGED
@@ -29,7 +29,78 @@
29
29
 
30
30
  'use strict';
31
31
 
32
- const { version } = require('../package.json');
32
+ const { version } = require('../package.json');
33
+
34
+ function buildMcpAuditPayload(result, options = {}) {
35
+ const verbose = Boolean(options.verbose);
36
+ const normalizedCheckCount = typeof result.checkCount === 'number'
37
+ ? result.checkCount
38
+ : typeof result.total === 'number'
39
+ ? result.total
40
+ : 0;
41
+
42
+ const payload = {
43
+ platform: result.platform,
44
+ score: result.score,
45
+ passed: result.passed,
46
+ failed: result.failed,
47
+ total: normalizedCheckCount,
48
+ checkCount: normalizedCheckCount,
49
+ scoreType: result.scoreType || 'live-audit-score',
50
+ grade: result.score >= 80 ? 'A' : result.score >= 60 ? 'B' : result.score >= 40 ? 'C' : 'D',
51
+ criticalFailures: (result.results || [])
52
+ .filter(r => r.passed === false && r.impact === 'critical')
53
+ .map(r => ({ key: r.key, id: r.id, name: r.name, fix: r.fix })),
54
+ highFailures: (result.results || [])
55
+ .filter(r => r.passed === false && r.impact === 'high')
56
+ .map(r => ({ key: r.key, id: r.id, name: r.name, fix: r.fix })),
57
+ topNextActions: (result.topNextActions || []).slice(0, 3).map((item) => ({
58
+ key: item.key,
59
+ name: item.name,
60
+ impact: item.impact,
61
+ fix: item.fix,
62
+ })),
63
+ suggestedNextCommand: result.suggestedNextCommand || null,
64
+ };
65
+
66
+ if (verbose) {
67
+ payload.results = (result.results || []).map(r => ({
68
+ key: r.key,
69
+ id: r.id,
70
+ name: r.name,
71
+ passed: r.passed,
72
+ impact: r.impact,
73
+ fix: r.passed === false ? r.fix : undefined,
74
+ }));
75
+ }
76
+
77
+ return payload;
78
+ }
79
+
80
+ function buildMcpHarmonyPayload(result, options = {}) {
81
+ const verbose = Boolean(options.verbose);
82
+ const payload = {
83
+ harmonyScore: result.harmonyScore,
84
+ activePlatforms: result.activePlatforms || [],
85
+ platformScores: result.platformScores || {},
86
+ driftCount: result.driftCount || (result.drifts || []).length || 0,
87
+ criticalDrifts: (result.drifts || [])
88
+ .filter(d => d.severity === 'critical')
89
+ .map(d => ({ type: d.type, description: d.description, recommendation: d.recommendation })),
90
+ recommendations: (result.recommendations || []).slice(0, 5),
91
+ };
92
+
93
+ if (verbose) {
94
+ payload.allDrifts = (result.drifts || []).map(d => ({
95
+ type: d.type,
96
+ severity: d.severity,
97
+ description: d.description,
98
+ recommendation: d.recommendation,
99
+ }));
100
+ }
101
+
102
+ return payload;
103
+ }
33
104
 
34
105
  // ─── Tool definitions ────────────────────────────────────────────────────────
35
106
 
@@ -132,74 +203,26 @@ const TOOLS = [
132
203
 
133
204
  // ─── Tool handlers ───────────────────────────────────────────────────────────
134
205
 
135
- async function handleAudit(input) {
206
+ async function handleAudit(input) {
136
207
  const { audit } = require('./audit');
137
208
  const dir = input.dir || process.cwd();
138
209
  const platform = input.platform || 'claude';
139
210
  const verbose = Boolean(input.verbose);
140
211
 
141
- const result = await audit({ dir, platform, silent: true, verbose });
142
-
143
- // Return clean JSON without ANSI codes
144
- const clean = {
145
- platform: result.platform,
146
- score: result.score,
147
- passed: result.passed,
148
- failed: result.failed,
149
- total: result.total,
150
- grade: result.score >= 80 ? 'A' : result.score >= 60 ? 'B' : result.score >= 40 ? 'C' : 'D',
151
- criticalFailures: (result.results || [])
152
- .filter(r => r.passed === false && r.impact === 'critical')
153
- .map(r => ({ key: r.key, id: r.id, name: r.name, fix: r.fix })),
154
- highFailures: (result.results || [])
155
- .filter(r => r.passed === false && r.impact === 'high')
156
- .map(r => ({ key: r.key, id: r.id, name: r.name, fix: r.fix })),
157
- suggestedNextCommand: result.suggestedNextCommand || null,
158
- };
159
-
160
- if (verbose) {
161
- clean.allResults = (result.results || []).map(r => ({
162
- key: r.key,
163
- id: r.id,
164
- name: r.name,
165
- passed: r.passed,
166
- impact: r.impact,
167
- fix: r.passed === false ? r.fix : undefined,
168
- }));
169
- }
170
-
171
- return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
172
- }
212
+ const result = await audit({ dir, platform, silent: true, verbose });
213
+ const clean = buildMcpAuditPayload(result, { verbose });
214
+ return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
215
+ }
173
216
 
174
217
  async function handleHarmony(input) {
175
218
  const { harmonyAudit } = require('./harmony/audit');
176
219
  const dir = input.dir || process.cwd();
177
220
  const verbose = Boolean(input.verbose);
178
221
 
179
- const result = await harmonyAudit({ dir, silent: true });
180
-
181
- const clean = {
182
- harmonyScore: result.harmonyScore,
183
- activePlatforms: result.activePlatforms || [],
184
- platformScores: result.platformScores || {},
185
- driftCount: result.driftCount || 0,
186
- criticalDrifts: (result.drifts || [])
187
- .filter(d => d.severity === 'critical')
188
- .map(d => ({ type: d.type, description: d.description, recommendation: d.recommendation })),
189
- recommendations: (result.recommendations || []).slice(0, 5),
190
- };
191
-
192
- if (verbose) {
193
- clean.allDrifts = (result.drifts || []).map(d => ({
194
- type: d.type,
195
- severity: d.severity,
196
- description: d.description,
197
- recommendation: d.recommendation,
198
- }));
199
- }
200
-
201
- return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
202
- }
222
+ const result = await harmonyAudit({ dir, silent: true });
223
+ const clean = buildMcpHarmonyPayload(result, { verbose });
224
+ return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
225
+ }
203
226
 
204
227
  async function handleSetup(input) {
205
228
  const { setup } = require('./setup');
@@ -370,4 +393,17 @@ function main() {
370
393
  });
371
394
  }
372
395
 
373
- main();
396
+ if (require.main === module) {
397
+ main();
398
+ }
399
+
400
+ module.exports = {
401
+ TOOLS,
402
+ buildMcpAuditPayload,
403
+ buildMcpHarmonyPayload,
404
+ handleAudit,
405
+ handleHarmony,
406
+ handleSetup,
407
+ handleDrift,
408
+ main,
409
+ };
@@ -29,18 +29,39 @@ const P0_SOURCES = [
29
29
  stalenessThresholdDays: 14,
30
30
  verifiedAt: '2026-04-07',
31
31
  },
32
- {
33
- key: 'opencode-plugin-api',
34
- label: 'OpenCode Plugin API',
35
- url: 'https://opencode.ai/docs/plugins/',
36
- stalenessThresholdDays: 30,
37
- verifiedAt: '2026-04-07',
38
- },
39
- {
40
- key: 'opencode-permissions-docs',
41
- label: 'OpenCode Permissions Documentation',
42
- url: 'https://opencode.ai/docs/permissions/',
43
- stalenessThresholdDays: 30,
32
+ {
33
+ key: 'opencode-plugin-api',
34
+ label: 'OpenCode Plugin API',
35
+ url: 'https://opencode.ai/docs/plugins/',
36
+ stalenessThresholdDays: 30,
37
+ verifiedAt: '2026-04-07',
38
+ },
39
+ {
40
+ key: 'opencode-agents-docs',
41
+ label: 'OpenCode Agents',
42
+ url: 'https://opencode.ai/docs/agents/',
43
+ stalenessThresholdDays: 14,
44
+ verifiedAt: '2026-04-10',
45
+ },
46
+ {
47
+ key: 'opencode-models-docs',
48
+ label: 'OpenCode Models',
49
+ url: 'https://opencode.ai/docs/models',
50
+ stalenessThresholdDays: 14,
51
+ verifiedAt: '2026-04-10',
52
+ },
53
+ {
54
+ key: 'opencode-github-docs',
55
+ label: 'OpenCode GitHub Integration',
56
+ url: 'https://opencode.ai/docs/github/',
57
+ stalenessThresholdDays: 14,
58
+ verifiedAt: '2026-04-10',
59
+ },
60
+ {
61
+ key: 'opencode-permissions-docs',
62
+ label: 'OpenCode Permissions Documentation',
63
+ url: 'https://opencode.ai/docs/permissions/',
64
+ stalenessThresholdDays: 30,
44
65
  verifiedAt: '2026-04-07',
45
66
  },
46
67
  ];
@@ -78,15 +99,39 @@ const PROPAGATION_CHECKLIST = [
78
99
  'src/opencode/techniques.js — update MCP checks',
79
100
  ],
80
101
  },
81
- {
82
- trigger: 'Known security bug fixed or new bug reported',
83
- targets: [
84
- 'src/opencode/techniques.js — update security checks (E02, E03, D05)',
85
- 'src/opencode/governance.js — update platformCaveats',
86
- 'src/opencode/freshness.js — verify against latest release',
87
- ],
88
- },
89
- ];
102
+ {
103
+ trigger: 'Known security bug fixed or new bug reported',
104
+ targets: [
105
+ 'src/opencode/techniques.js — update security checks (E02, E03, D05)',
106
+ 'src/opencode/governance.js — update platformCaveats',
107
+ 'src/opencode/freshness.js — verify against latest release',
108
+ ],
109
+ },
110
+ {
111
+ trigger: 'OpenCode agent or subagent behavior change',
112
+ targets: [
113
+ 'src/opencode/techniques.js — update agent and multi-session checks',
114
+ 'src/opencode/governance.js — update permission guidance for plan/build/agent surfaces',
115
+ 'src/source-urls.js — refresh OpenCode agent source mappings',
116
+ ],
117
+ },
118
+ {
119
+ trigger: 'OpenCode model catalog or provider-option change',
120
+ targets: [
121
+ 'src/opencode/techniques.js — update model-awareness and provider-option assumptions',
122
+ 'src/opencode/setup.js — update starter config guidance for model selection',
123
+ 'src/source-urls.js — refresh OpenCode model source mappings',
124
+ ],
125
+ },
126
+ {
127
+ trigger: 'OpenCode GitHub integration or workflow contract change',
128
+ targets: [
129
+ 'src/opencode/techniques.js — update GitHub/workflow checks',
130
+ 'src/opencode/setup.js — update GitHub starter guidance',
131
+ 'src/source-urls.js — refresh OpenCode GitHub source mappings',
132
+ ],
133
+ },
134
+ ];
90
135
 
91
136
  function checkReleaseGate(sourceVerifications = {}) {
92
137
  const now = new Date();