@nerviq/cli 1.0.1 → 1.2.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.
Files changed (39) hide show
  1. package/bin/cli.js +170 -73
  2. package/package.json +1 -1
  3. package/src/activity.js +20 -0
  4. package/src/aider/domain-packs.js +27 -2
  5. package/src/aider/mcp-packs.js +231 -0
  6. package/src/aider/techniques.js +3210 -1397
  7. package/src/audit.js +257 -2
  8. package/src/catalog.js +18 -2
  9. package/src/codex/domain-packs.js +23 -1
  10. package/src/codex/mcp-packs.js +254 -0
  11. package/src/codex/techniques.js +4738 -3257
  12. package/src/copilot/domain-packs.js +23 -1
  13. package/src/copilot/mcp-packs.js +254 -0
  14. package/src/copilot/techniques.js +3433 -1936
  15. package/src/cursor/domain-packs.js +23 -1
  16. package/src/cursor/mcp-packs.js +257 -0
  17. package/src/cursor/techniques.js +3697 -1869
  18. package/src/deprecation.js +98 -0
  19. package/src/domain-pack-expansion.js +571 -0
  20. package/src/domain-packs.js +25 -2
  21. package/src/formatters/otel.js +151 -0
  22. package/src/gemini/domain-packs.js +23 -1
  23. package/src/gemini/mcp-packs.js +257 -0
  24. package/src/gemini/techniques.js +3734 -2238
  25. package/src/integrations.js +194 -0
  26. package/src/mcp-packs.js +233 -0
  27. package/src/opencode/domain-packs.js +23 -1
  28. package/src/opencode/mcp-packs.js +231 -0
  29. package/src/opencode/techniques.js +3500 -1687
  30. package/src/org.js +68 -0
  31. package/src/source-urls.js +410 -260
  32. package/src/stack-checks.js +565 -0
  33. package/src/supplemental-checks.js +767 -0
  34. package/src/techniques.js +2929 -1449
  35. package/src/telemetry.js +160 -0
  36. package/src/windsurf/domain-packs.js +23 -1
  37. package/src/windsurf/mcp-packs.js +257 -0
  38. package/src/windsurf/techniques.js +3647 -1834
  39. package/src/workspace.js +233 -0
package/src/audit.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Audit engine - evaluates project against CLAUDEX technique database.
3
3
  */
4
4
 
5
+ const path = require('path');
5
6
  const { TECHNIQUES: CLAUDE_TECHNIQUES, STACKS } = require('./techniques');
6
7
  const { ProjectContext } = require('./context');
7
8
  const { CODEX_TECHNIQUES } = require('./codex/techniques');
@@ -27,7 +28,11 @@ const { sendInsights, getLocalInsights } = require('./insights');
27
28
  const { getRecommendationOutcomeSummary, getRecommendationAdjustment } = require('./activity');
28
29
  const { getFeedbackSummary } = require('./feedback');
29
30
  const { formatSarif } = require('./formatters/sarif');
31
+ const { formatOtelMetrics } = require('./formatters/otel');
30
32
  const { loadPlugins, mergePluginChecks } = require('./plugins');
33
+ const { hasWorkspaceConfig, detectWorkspaceGlobs, detectWorkspaces } = require('./workspace');
34
+ const { detectDeprecationWarnings } = require('./deprecation');
35
+ const { version: packageVersion } = require('../package.json');
31
36
 
32
37
  const COLORS = {
33
38
  reset: '\x1b[0m',
@@ -58,6 +63,8 @@ function formatLocation(file, line) {
58
63
 
59
64
  const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
60
65
  const WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
66
+ const LARGE_INSTRUCTION_WARN_BYTES = 50 * 1024;
67
+ const LARGE_INSTRUCTION_SKIP_BYTES = 1024 * 1024;
61
68
  const CATEGORY_MODULES = {
62
69
  memory: 'CLAUDE.md',
63
70
  quality: 'verification',
@@ -350,6 +357,171 @@ function getAuditSpec(platform = 'claude') {
350
357
  };
351
358
  }
352
359
 
360
+ function normalizeRelativePath(filePath) {
361
+ return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
362
+ }
363
+
364
+ function addPath(target, filePath) {
365
+ if (!filePath || typeof filePath !== 'string') return;
366
+ target.add(normalizeRelativePath(filePath));
367
+ }
368
+
369
+ function addDirFiles(ctx, target, dirPath, filter) {
370
+ if (typeof ctx.dirFiles !== 'function') return;
371
+ for (const file of ctx.dirFiles(dirPath)) {
372
+ if (filter && !filter.test(file)) continue;
373
+ addPath(target, path.join(dirPath, file));
374
+ }
375
+ }
376
+
377
+ function instructionFileCandidates(spec, ctx) {
378
+ const candidates = new Set();
379
+
380
+ if (spec.platform === 'claude') {
381
+ addPath(candidates, 'CLAUDE.md');
382
+ addPath(candidates, '.claude/CLAUDE.md');
383
+ addDirFiles(ctx, candidates, '.claude/rules', /\.md$/i);
384
+ addDirFiles(ctx, candidates, '.claude/commands', /\.md$/i);
385
+ addDirFiles(ctx, candidates, '.claude/agents', /\.md$/i);
386
+ if (typeof ctx.dirFiles === 'function') {
387
+ for (const skillDir of ctx.dirFiles('.claude/skills')) {
388
+ addPath(candidates, path.join('.claude', 'skills', skillDir, 'SKILL.md'));
389
+ }
390
+ }
391
+ }
392
+
393
+ if (spec.platform === 'codex') {
394
+ addPath(candidates, 'AGENTS.md');
395
+ addPath(candidates, 'AGENTS.override.md');
396
+ addPath(candidates, typeof ctx.agentsMdPath === 'function' ? ctx.agentsMdPath() : null);
397
+ addDirFiles(ctx, candidates, 'codex/rules');
398
+ addDirFiles(ctx, candidates, '.codex/rules');
399
+ if (typeof ctx.skillDirs === 'function') {
400
+ for (const skillDir of ctx.skillDirs()) {
401
+ addPath(candidates, path.join('.agents', 'skills', skillDir, 'SKILL.md'));
402
+ }
403
+ }
404
+ }
405
+
406
+ if (spec.platform === 'gemini') {
407
+ addPath(candidates, 'GEMINI.md');
408
+ addPath(candidates, '.gemini/GEMINI.md');
409
+ addDirFiles(ctx, candidates, '.gemini/agents', /\.md$/i);
410
+ if (typeof ctx.skillDirs === 'function') {
411
+ for (const skillDir of ctx.skillDirs()) {
412
+ addPath(candidates, path.join('.gemini', 'skills', skillDir, 'SKILL.md'));
413
+ }
414
+ }
415
+ }
416
+
417
+ if (spec.platform === 'copilot') {
418
+ addPath(candidates, '.github/copilot-instructions.md');
419
+ addDirFiles(ctx, candidates, '.github/instructions', /\.instructions\.md$/i);
420
+ addDirFiles(ctx, candidates, '.github/prompts', /\.prompt\.md$/i);
421
+ }
422
+
423
+ if (spec.platform === 'cursor') {
424
+ addPath(candidates, '.cursorrules');
425
+ addDirFiles(ctx, candidates, '.cursor/rules', /\.mdc$/i);
426
+ addDirFiles(ctx, candidates, '.cursor/commands', /\.md$/i);
427
+ }
428
+
429
+ if (spec.platform === 'windsurf') {
430
+ addPath(candidates, '.windsurfrules');
431
+ addDirFiles(ctx, candidates, '.windsurf/rules', /\.md$/i);
432
+ addDirFiles(ctx, candidates, '.windsurf/workflows', /\.md$/i);
433
+ addDirFiles(ctx, candidates, '.windsurf/memories', /\.(md|json)$/i);
434
+ }
435
+
436
+ if (spec.platform === 'aider' && typeof ctx.conventionFiles === 'function') {
437
+ for (const file of ctx.conventionFiles()) {
438
+ addPath(candidates, file);
439
+ }
440
+ }
441
+
442
+ if (spec.platform === 'opencode') {
443
+ addPath(candidates, 'AGENTS.md');
444
+ addPath(candidates, 'CLAUDE.md');
445
+ addDirFiles(ctx, candidates, '.opencode/commands', /\.(md|markdown|ya?ml)$/i);
446
+ if (typeof ctx.skillDirs === 'function') {
447
+ for (const skillDir of ctx.skillDirs()) {
448
+ addPath(candidates, path.join('.opencode', 'commands', skillDir, 'SKILL.md'));
449
+ }
450
+ }
451
+ }
452
+
453
+ return [...candidates];
454
+ }
455
+
456
+ function inspectInstructionFiles(spec, ctx) {
457
+ const warnings = [];
458
+
459
+ for (const filePath of instructionFileCandidates(spec, ctx)) {
460
+ const byteCount = typeof ctx.fileSizeBytes === 'function' ? ctx.fileSizeBytes(filePath) : null;
461
+ if (!Number.isFinite(byteCount) || byteCount <= LARGE_INSTRUCTION_WARN_BYTES) continue;
462
+
463
+ const content = typeof ctx.fileContent === 'function' ? ctx.fileContent(filePath) : null;
464
+ warnings.push({
465
+ file: normalizeRelativePath(filePath),
466
+ byteCount,
467
+ lineCount: typeof content === 'string' ? content.split(/\r?\n/).length : null,
468
+ skipped: byteCount > LARGE_INSTRUCTION_SKIP_BYTES,
469
+ severity: byteCount > LARGE_INSTRUCTION_SKIP_BYTES ? 'critical' : 'warning',
470
+ message: byteCount > LARGE_INSTRUCTION_SKIP_BYTES
471
+ ? 'Instruction file exceeds 1MB and will be skipped during audit.'
472
+ : 'Instruction file exceeds 50KB. Audit will continue, but this file may reduce runtime clarity.',
473
+ });
474
+ }
475
+
476
+ return warnings;
477
+ }
478
+
479
+ function guardSkippedInstructionFiles(ctx, warnings) {
480
+ const skippedFiles = new Set(
481
+ warnings.filter((item) => item.skipped).map((item) => normalizeRelativePath(item.file))
482
+ );
483
+
484
+ if (skippedFiles.size === 0) return;
485
+
486
+ const originalFileContent = typeof ctx.fileContent === 'function' ? ctx.fileContent.bind(ctx) : null;
487
+ const originalLineNumber = typeof ctx.lineNumber === 'function' ? ctx.lineNumber.bind(ctx) : null;
488
+
489
+ if (originalFileContent) {
490
+ ctx.fileContent = (filePath) => {
491
+ if (skippedFiles.has(normalizeRelativePath(filePath))) return null;
492
+ return originalFileContent(filePath);
493
+ };
494
+ }
495
+
496
+ if (originalLineNumber) {
497
+ ctx.lineNumber = (filePath, matcher) => {
498
+ if (skippedFiles.has(normalizeRelativePath(filePath))) return null;
499
+ return originalLineNumber(filePath, matcher);
500
+ };
501
+ }
502
+ }
503
+
504
+ function buildWorkspaceHint(dir) {
505
+ if (!hasWorkspaceConfig(dir)) {
506
+ return null;
507
+ }
508
+
509
+ const patterns = detectWorkspaceGlobs(dir);
510
+ const workspaces = detectWorkspaces(dir);
511
+ if (patterns.length === 0 && workspaces.length === 0) {
512
+ return null;
513
+ }
514
+
515
+ return {
516
+ detected: true,
517
+ patterns,
518
+ workspaces,
519
+ suggestedCommand: patterns.length > 0
520
+ ? `npx nerviq audit --workspace ${patterns.join(',')}`
521
+ : `npx nerviq audit --workspace ${workspaces.join(',')}`,
522
+ };
523
+ }
524
+
353
525
  function riskFromImpact(impact) {
354
526
  if (impact === 'critical') return 'high';
355
527
  if (impact === 'high') return 'medium';
@@ -702,12 +874,20 @@ function printLiteAudit(result, dir) {
702
874
  if (result.platformScopeNote) {
703
875
  console.log(colorize(` Scope: ${result.platformScopeNote.message}`, 'dim'));
704
876
  }
877
+ if (result.workspaceHint && result.workspaceHint.workspaces.length > 0) {
878
+ console.log(colorize(` Workspaces: ${result.workspaceHint.workspaces.join(', ')}`, 'dim'));
879
+ }
705
880
  if (result.platformCaveats && result.platformCaveats.length > 0) {
706
881
  console.log(colorize(' Platform caveats:', 'yellow'));
707
882
  result.platformCaveats.slice(0, 2).forEach((item) => {
708
883
  console.log(colorize(` - ${item.title}: ${item.message}`, 'dim'));
709
884
  });
710
885
  }
886
+ if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
887
+ result.largeInstructionFiles.slice(0, 2).forEach((item) => {
888
+ console.log(colorize(` Large file: ${item.file} (${Math.round(item.byteCount / 1024)}KB)`, 'yellow'));
889
+ });
890
+ }
711
891
  console.log('');
712
892
 
713
893
  if (result.failed === 0) {
@@ -750,10 +930,13 @@ async function audit(options) {
750
930
  const spec = getAuditSpec(options.platform || 'claude');
751
931
  const silent = options.silent || false;
752
932
  const ctx = new spec.ContextClass(options.dir);
933
+ const largeInstructionFiles = inspectInstructionFiles(spec, ctx);
934
+ guardSkippedInstructionFiles(ctx, largeInstructionFiles);
753
935
  const stacks = ctx.detectStacks(STACKS);
754
936
  const results = [];
755
937
  const outcomeSummary = getRecommendationOutcomeSummary(options.dir);
756
938
  const fpFeedback = getFeedbackSummary(options.dir);
939
+ const workspaceHint = buildWorkspaceHint(options.dir);
757
940
 
758
941
  // Load and merge plugin checks
759
942
  const plugins = loadPlugins(options.dir);
@@ -775,6 +958,24 @@ async function audit(options) {
775
958
  });
776
959
  }
777
960
 
961
+ if (largeInstructionFiles.length > 0) {
962
+ results.push({
963
+ key: 'largeInstructionFile',
964
+ id: null,
965
+ name: 'Large instruction file warning',
966
+ category: 'performance',
967
+ impact: 'medium',
968
+ rating: null,
969
+ fix: 'Split oversized instruction files so they stay under 50KB, and keep any single instruction file below 1MB.',
970
+ sourceUrl: null,
971
+ confidence: 'high',
972
+ file: largeInstructionFiles[0].file,
973
+ line: null,
974
+ passed: null,
975
+ details: largeInstructionFiles,
976
+ });
977
+ }
978
+
778
979
  // null = not applicable (skip), true = pass, false = fail
779
980
  const applicable = results.filter(r => r.passed !== null);
780
981
  const skipped = results.filter(r => r.passed === null);
@@ -825,6 +1026,23 @@ async function audit(options) {
825
1026
  const categoryScores = computeCategoryScores(applicable, passed);
826
1027
  const platformScopeNote = getPlatformScopeNote(spec, ctx);
827
1028
  const platformCaveats = getPlatformCaveats(spec, ctx);
1029
+ const deprecationWarnings = detectDeprecationWarnings(failed, packageVersion);
1030
+ const warnings = [
1031
+ ...largeInstructionFiles.map((item) => ({
1032
+ kind: 'large-instruction-file',
1033
+ severity: item.severity,
1034
+ message: item.message,
1035
+ file: item.file,
1036
+ lineCount: item.lineCount,
1037
+ byteCount: item.byteCount,
1038
+ skipped: item.skipped,
1039
+ })),
1040
+ ...deprecationWarnings.map((item) => ({
1041
+ kind: 'deprecated-feature',
1042
+ severity: 'warning',
1043
+ ...item,
1044
+ })),
1045
+ ];
828
1046
  const recommendedDomainPacks = spec.platform === 'codex'
829
1047
  ? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
830
1048
  : [];
@@ -850,6 +1068,10 @@ async function audit(options) {
850
1068
  totalEntries: outcomeSummary.totalEntries,
851
1069
  keysTracked: outcomeSummary.keys,
852
1070
  },
1071
+ largeInstructionFiles,
1072
+ deprecationWarnings,
1073
+ warnings,
1074
+ workspaceHint,
853
1075
  platformScopeNote,
854
1076
  platformCaveats,
855
1077
  recommendedDomainPacks,
@@ -867,9 +1089,8 @@ async function audit(options) {
867
1089
  }
868
1090
 
869
1091
  if (options.json) {
870
- const { version } = require('../package.json');
871
1092
  console.log(JSON.stringify({
872
- version,
1093
+ version: packageVersion,
873
1094
  timestamp: new Date().toISOString(),
874
1095
  ...result
875
1096
  }, null, 2));
@@ -881,6 +1102,11 @@ async function audit(options) {
881
1102
  return result;
882
1103
  }
883
1104
 
1105
+ if (options.format === 'otel') {
1106
+ console.log(JSON.stringify(formatOtelMetrics(result), null, 2));
1107
+ return result;
1108
+ }
1109
+
884
1110
  if (options.lite) {
885
1111
  printLiteAudit(result, options.dir);
886
1112
  sendInsights(result);
@@ -914,6 +1140,35 @@ async function audit(options) {
914
1140
  console.log('');
915
1141
  }
916
1142
 
1143
+ if (largeInstructionFiles.length > 0) {
1144
+ console.log(colorize(' Large instruction files', 'yellow'));
1145
+ for (const item of largeInstructionFiles) {
1146
+ const sizeKb = Math.round(item.byteCount / 1024);
1147
+ console.log(colorize(` ${item.file} (${sizeKb}KB, ${item.lineCount || '?'} lines)`, 'bold'));
1148
+ console.log(colorize(` → ${item.message}`, 'dim'));
1149
+ }
1150
+ console.log('');
1151
+ }
1152
+
1153
+ if (deprecationWarnings.length > 0) {
1154
+ console.log(colorize(' Deprecated feature warnings', 'yellow'));
1155
+ for (const item of deprecationWarnings) {
1156
+ console.log(colorize(` ${item.feature}`, 'bold'));
1157
+ console.log(colorize(` → ${item.message}`, 'dim'));
1158
+ console.log(colorize(` Alternative: ${item.alternative}`, 'dim'));
1159
+ }
1160
+ console.log('');
1161
+ }
1162
+
1163
+ if (workspaceHint && !options.workspace) {
1164
+ console.log(colorize(' Monorepo detected', 'blue'));
1165
+ if (workspaceHint.workspaces.length > 0) {
1166
+ console.log(colorize(` Workspaces: ${workspaceHint.workspaces.join(', ')}`, 'dim'));
1167
+ }
1168
+ console.log(colorize(` Tip: ${workspaceHint.suggestedCommand}`, 'dim'));
1169
+ console.log('');
1170
+ }
1171
+
917
1172
  if (stacks.length > 0) {
918
1173
  console.log(colorize(` Detected: ${stacks.map(s => s.label).join(', ')}`, 'blue'));
919
1174
  }
package/src/catalog.js CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const { version } = require('../package.json');
8
9
 
9
10
  const { TECHNIQUES: CLAUDE_TECHNIQUES } = require('./techniques');
10
11
  const { CODEX_TECHNIQUES } = require('./codex/techniques');
@@ -31,7 +32,7 @@ const PLATFORM_MAP = {
31
32
  * Generate a unified catalog array from all platform technique files.
32
33
  * Each entry contains:
33
34
  * platform, id, key, name, category, impact, rating, fix, sourceUrl,
34
- * confidence, template, deprecated
35
+ * confidence, lastVerified, template, deprecated
35
36
  */
36
37
  function generateCatalog() {
37
38
  const catalog = [];
@@ -62,6 +63,7 @@ function generateCatalog() {
62
63
  fix: tech.fix ?? null,
63
64
  sourceUrl: tech.sourceUrl ?? null,
64
65
  confidence: tech.confidence ?? null,
66
+ lastVerified: tech.lastVerified ?? null,
65
67
  template: tech.template ?? null,
66
68
  deprecated: tech.deprecated ?? false,
67
69
  });
@@ -71,6 +73,20 @@ function generateCatalog() {
71
73
  return catalog;
72
74
  }
73
75
 
76
+ /**
77
+ * Generate a catalog envelope with version metadata.
78
+ * @returns {{ catalogVersion: string, generatedAt: string, totalChecks: number, checks: Object[] }}
79
+ */
80
+ function generateCatalogWithVersion() {
81
+ const checks = generateCatalog();
82
+ return {
83
+ catalogVersion: version,
84
+ generatedAt: new Date().toISOString(),
85
+ totalChecks: checks.length,
86
+ checks,
87
+ };
88
+ }
89
+
74
90
  /**
75
91
  * Write the catalog as formatted JSON to the given output path.
76
92
  * @param {string} outputPath - Absolute or relative path for the JSON file
@@ -84,4 +100,4 @@ function writeCatalogJson(outputPath) {
84
100
  return { path: resolved, count: catalog.length };
85
101
  }
86
102
 
87
- module.exports = { generateCatalog, writeCatalogJson };
103
+ module.exports = { generateCatalog, generateCatalogWithVersion, writeCatalogJson };
@@ -1,4 +1,6 @@
1
- const CODEX_DOMAIN_PACKS = [
1
+ const { buildAdditionalDomainPacks, detectAdditionalDomainPacks } = require('../domain-pack-expansion');
2
+
3
+ const BASE_CODEX_DOMAIN_PACKS = [
2
4
  {
3
5
  key: 'baseline-general',
4
6
  label: 'Baseline General',
@@ -162,6 +164,13 @@ const CODEX_DOMAIN_PACKS = [
162
164
  },
163
165
  ];
164
166
 
167
+ const CODEX_DOMAIN_PACKS = [
168
+ ...BASE_CODEX_DOMAIN_PACKS,
169
+ ...buildAdditionalDomainPacks('codex', {
170
+ existingKeys: new Set(BASE_CODEX_DOMAIN_PACKS.map((pack) => pack.key)),
171
+ }),
172
+ ];
173
+
165
174
  function uniqueByKey(items) {
166
175
  const seen = new Set();
167
176
  const result = [];
@@ -354,6 +363,19 @@ function detectCodexDomainPacks(ctx, stacks = [], assets = {}) {
354
363
  ]);
355
364
  }
356
365
 
366
+ detectAdditionalDomainPacks({
367
+ ctx,
368
+ pkg,
369
+ deps,
370
+ stackKeys,
371
+ addMatch,
372
+ hasBackend,
373
+ hasFrontend,
374
+ hasInfra,
375
+ hasCi,
376
+ isEnterpriseGoverned,
377
+ });
378
+
357
379
  if (matches.length === 0) {
358
380
  addMatch('baseline-general', [
359
381
  'No stronger platform-specific domain dominated, so a safe general Codex baseline is the best starting point.',