@nerviq/cli 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/bin/cli.js +1067 -1008
- package/package.json +1 -1
- package/src/audit/recommendations.js +46 -0
- package/src/audit.js +127 -127
- package/src/context.js +23 -1
- package/src/harmony/cli.js +235 -0
- package/src/index.js +2 -0
- package/src/mcp-server.js +631 -314
- package/src/techniques/hygiene.js +2 -2
- package/src/techniques/quality.js +6 -5
package/src/harmony/cli.js
CHANGED
|
@@ -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 `[](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)
|