@nerviq/cli 0.9.4 → 0.9.6

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
@@ -8,6 +8,7 @@ const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, ren
8
8
  const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
9
9
  const { writeSnapshotArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
10
10
  const { collectFeedback } = require('../src/feedback');
11
+ const { startServer } = require('../src/server');
11
12
  const { version } = require('../package.json');
12
13
 
13
14
  const args = process.argv.slice(2);
@@ -21,7 +22,7 @@ const COMMAND_ALIASES = {
21
22
  gov: 'governance',
22
23
  outcome: 'feedback',
23
24
  };
24
- const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'help', 'version'];
25
+ const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'serve', 'help', 'version'];
25
26
 
26
27
  function levenshtein(a, b) {
27
28
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -72,6 +73,7 @@ function parseArgs(rawArgs) {
72
73
  let feedbackScoreDelta = null;
73
74
  let platform = 'claude';
74
75
  let format = null;
76
+ let port = null;
75
77
  let commandSet = false;
76
78
  let extraArgs = [];
77
79
  let convertFrom = null;
@@ -82,7 +84,7 @@ function parseArgs(rawArgs) {
82
84
  for (let i = 0; i < rawArgs.length; i++) {
83
85
  const arg = rawArgs[i];
84
86
 
85
- 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') {
87
+ 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') {
86
88
  const value = rawArgs[i + 1];
87
89
  if (!value || value.startsWith('--')) {
88
90
  throw new Error(`${arg} requires a value`);
@@ -104,6 +106,7 @@ function parseArgs(rawArgs) {
104
106
  if (arg === '--format') format = value.trim().toLowerCase();
105
107
  if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
106
108
  if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
109
+ if (arg === '--port') port = value.trim();
107
110
  i++;
108
111
  continue;
109
112
  }
@@ -183,6 +186,11 @@ function parseArgs(rawArgs) {
183
186
  continue;
184
187
  }
185
188
 
189
+ if (arg.startsWith('--port=')) {
190
+ port = arg.split('=').slice(1).join('=').trim();
191
+ continue;
192
+ }
193
+
186
194
  if (arg.startsWith('--')) {
187
195
  flags.push(arg);
188
196
  continue;
@@ -198,7 +206,7 @@ function parseArgs(rawArgs) {
198
206
 
199
207
  const normalizedCommand = COMMAND_ALIASES[command] || command;
200
208
 
201
- return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo };
209
+ return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo };
202
210
  }
203
211
 
204
212
  const HELP = `
@@ -234,9 +242,15 @@ const HELP = `
234
242
  npx nerviq deep-review AI-powered config review (opt-in, uses API)
235
243
  npx nerviq interactive Step-by-step guided wizard
236
244
  npx nerviq watch Live monitoring on config changes with cross-platform watch fallback
245
+ npx nerviq serve --port 3000 Start the local Nerviq HTTP API
237
246
  npx nerviq badge Generate shields.io badge markdown
238
247
  npx nerviq feedback Record recommendation outcomes or show local outcome summary
239
248
 
249
+ Catalog:
250
+ npx nerviq catalog Show check catalog summary for all 8 platforms
251
+ npx nerviq catalog --json Full catalog as JSON
252
+ npx nerviq catalog --out catalog.json Write catalog to file
253
+
240
254
  Utilities:
241
255
  npx nerviq doctor Self-diagnostics: Node version, deps, freshness gates, platform detection
242
256
  npx nerviq convert --from claude --to codex Convert config between platforms
@@ -258,6 +272,7 @@ const HELP = `
258
272
  --score-delta N Optional observed score delta tied to the outcome
259
273
  --platform NAME Choose platform surface (claude default, codex advisory/build preview)
260
274
  --format NAME Output format for audit results (json, sarif)
275
+ --port N Port for \`serve\` (default: 3000)
261
276
  --feedback After audit output, prompt "Was this helpful? (y/n)" for each displayed top action and save answers locally
262
277
  --snapshot Save a normalized snapshot artifact under .claude/nerviq/snapshots/
263
278
  --lite Show a short top-3 quick scan with one clear next command
@@ -293,6 +308,7 @@ const HELP = `
293
308
  npx nerviq benchmark --out benchmark.md
294
309
  npx nerviq feedback
295
310
  npx nerviq feedback --key permissionDeny --status accepted --effect positive --score-delta 12
311
+ npx nerviq serve --port 3000
296
312
  npx nerviq --json --threshold 60
297
313
  npx nerviq setup --auto
298
314
  npx nerviq interactive
@@ -340,6 +356,7 @@ async function main() {
340
356
  require: parsed.requireChecks,
341
357
  platform: parsed.platform || 'claude',
342
358
  format: parsed.format || null,
359
+ port: parsed.port !== null ? Number(parsed.port) : null,
343
360
  dir: process.cwd()
344
361
  };
345
362
 
@@ -353,6 +370,11 @@ async function main() {
353
370
  process.exit(1);
354
371
  }
355
372
 
373
+ if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
374
+ console.error('\n Error: --port must be an integer between 0 and 65535.\n');
375
+ process.exit(1);
376
+ }
377
+
356
378
  if (options.threshold !== null && (!Number.isFinite(options.threshold) || options.threshold < 0 || options.threshold > 100)) {
357
379
  console.error('\n Error: --threshold must be a number between 0 and 100.\n');
358
380
  process.exit(1);
@@ -391,7 +413,7 @@ async function main() {
391
413
  const FULL_COMMAND_SET = new Set([
392
414
  'audit', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
393
415
  'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
394
- 'history', 'compare', 'trend', 'feedback', 'help', 'version',
416
+ 'history', 'compare', 'trend', 'feedback', 'catalog', 'serve', 'help', 'version',
395
417
  // Harmony + Synergy (cross-platform)
396
418
  'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
397
419
  'harmony-watch', 'harmony-governance', 'synergy-report',
@@ -713,6 +735,58 @@ async function main() {
713
735
  } else if (normalizedCommand === 'watch') {
714
736
  const { watch } = require('../src/watch');
715
737
  await watch(options);
738
+ } else if (normalizedCommand === 'catalog') {
739
+ const { generateCatalog, writeCatalogJson } = require('../src/catalog');
740
+ if (options.out) {
741
+ const result = writeCatalogJson(options.out);
742
+ if (options.json) {
743
+ console.log(JSON.stringify({ path: result.path, count: result.count }));
744
+ } else {
745
+ console.log(`\n Catalog written to ${result.path} (${result.count} checks)\n`);
746
+ }
747
+ } else {
748
+ const catalog = generateCatalog();
749
+ if (options.json) {
750
+ console.log(JSON.stringify(catalog, null, 2));
751
+ } else {
752
+ // Print summary table
753
+ const platforms = {};
754
+ for (const entry of catalog) {
755
+ platforms[entry.platform] = (platforms[entry.platform] || 0) + 1;
756
+ }
757
+ console.log('');
758
+ console.log('\x1b[1m nerviq check catalog\x1b[0m');
759
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
760
+ console.log(` Total checks: \x1b[1m${catalog.length}\x1b[0m`);
761
+ console.log('');
762
+ for (const [plat, count] of Object.entries(platforms)) {
763
+ console.log(` ${plat.padEnd(12)} ${count} checks`);
764
+ }
765
+ console.log('');
766
+ console.log(' Use --json for full output or --out catalog.json to write file.');
767
+ console.log('');
768
+ }
769
+ }
770
+ process.exit(0);
771
+ } else if (normalizedCommand === 'serve') {
772
+ const server = await startServer({
773
+ port: options.port == null ? 3000 : options.port,
774
+ baseDir: options.dir,
775
+ });
776
+ const address = server.address();
777
+ const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
778
+ console.log('');
779
+ console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
780
+ console.log(' Endpoints: /api/health, /api/catalog, /api/audit, /api/harmony');
781
+ console.log('');
782
+
783
+ const closeServer = () => {
784
+ server.close(() => process.exit(0));
785
+ };
786
+
787
+ process.on('SIGINT', closeServer);
788
+ process.on('SIGTERM', closeServer);
789
+ return;
716
790
  } else if (normalizedCommand === 'doctor') {
717
791
  const { runDoctor } = require('../src/doctor');
718
792
  const output = await runDoctor({ dir: options.dir, json: options.json, verbose: options.verbose });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "The intelligent nervous system for AI coding agents — audit, align, and amplify every platform on every project.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -20,7 +20,9 @@
20
20
  "test": "node test/run.js",
21
21
  "test:jest": "jest",
22
22
  "test:coverage": "jest --coverage",
23
- "test:all": "node test/run.js && node test/check-matrix.js && node test/codex-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/security-tests.js && jest"
23
+ "test:all": "node test/run.js && node test/check-matrix.js && node test/codex-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-check-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-check-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-check-matrix.js && node test/cursor-golden-matrix.js && node test/security-tests.js && jest",
24
+ "benchmark:perf": "node tools/benchmark.js",
25
+ "catalog": "node -e \"const {generateCatalog}=require('./src/catalog');console.log(JSON.stringify(generateCatalog(),null,2))\""
24
26
  },
25
27
  "keywords": [
26
28
  "nerviq",
package/src/audit.js CHANGED
@@ -26,6 +26,7 @@ const { getBadgeMarkdown } = require('./badge');
26
26
  const { sendInsights, getLocalInsights } = require('./insights');
27
27
  const { getRecommendationOutcomeSummary, getRecommendationAdjustment } = require('./activity');
28
28
  const { formatSarif } = require('./formatters/sarif');
29
+ const { loadPlugins, mergePluginChecks } = require('./plugins');
29
30
 
30
31
  const COLORS = {
31
32
  reset: '\x1b[0m',
@@ -728,8 +729,14 @@ async function audit(options) {
728
729
  const results = [];
729
730
  const outcomeSummary = getRecommendationOutcomeSummary(options.dir);
730
731
 
732
+ // Load and merge plugin checks
733
+ const plugins = loadPlugins(options.dir);
734
+ const techniques = plugins.length > 0
735
+ ? mergePluginChecks(spec.techniques, plugins)
736
+ : spec.techniques;
737
+
731
738
  // Run all technique checks
732
- for (const [key, technique] of Object.entries(spec.techniques)) {
739
+ for (const [key, technique] of Object.entries(techniques)) {
733
740
  const passed = technique.check(ctx);
734
741
  const file = typeof technique.file === 'function' ? (technique.file(ctx) ?? null) : (technique.file ?? null);
735
742
  const line = typeof technique.line === 'function' ? (technique.line(ctx) ?? null) : (technique.line ?? null);
package/src/catalog.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Public Check Catalog Generator
3
+ * Reads ALL technique files from all 8 platforms and generates a unified JSON catalog.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const { TECHNIQUES: CLAUDE_TECHNIQUES } = require('./techniques');
10
+ const { CODEX_TECHNIQUES } = require('./codex/techniques');
11
+ const { GEMINI_TECHNIQUES } = require('./gemini/techniques');
12
+ const { COPILOT_TECHNIQUES } = require('./copilot/techniques');
13
+ const { CURSOR_TECHNIQUES } = require('./cursor/techniques');
14
+ const { WINDSURF_TECHNIQUES } = require('./windsurf/techniques');
15
+ const { AIDER_TECHNIQUES } = require('./aider/techniques');
16
+ const { OPENCODE_TECHNIQUES } = require('./opencode/techniques');
17
+ const { attachSourceUrls } = require('./source-urls');
18
+
19
+ const PLATFORM_MAP = {
20
+ claude: CLAUDE_TECHNIQUES,
21
+ codex: CODEX_TECHNIQUES,
22
+ gemini: GEMINI_TECHNIQUES,
23
+ copilot: COPILOT_TECHNIQUES,
24
+ cursor: CURSOR_TECHNIQUES,
25
+ windsurf: WINDSURF_TECHNIQUES,
26
+ aider: AIDER_TECHNIQUES,
27
+ opencode: OPENCODE_TECHNIQUES,
28
+ };
29
+
30
+ /**
31
+ * Generate a unified catalog array from all platform technique files.
32
+ * Each entry contains:
33
+ * platform, id, key, name, category, impact, rating, fix, sourceUrl,
34
+ * confidence, template, deprecated
35
+ */
36
+ function generateCatalog() {
37
+ const catalog = [];
38
+
39
+ for (const [platform, techniques] of Object.entries(PLATFORM_MAP)) {
40
+ // Clone techniques so we don't mutate the originals
41
+ const cloned = {};
42
+ for (const [key, tech] of Object.entries(techniques)) {
43
+ cloned[key] = { ...tech };
44
+ }
45
+
46
+ // Attach source URLs
47
+ try {
48
+ attachSourceUrls(platform, cloned);
49
+ } catch (_) {
50
+ // If source URLs fail for a platform, continue without them
51
+ }
52
+
53
+ for (const [key, tech] of Object.entries(cloned)) {
54
+ catalog.push({
55
+ platform,
56
+ id: tech.id ?? null,
57
+ key,
58
+ name: tech.name ?? null,
59
+ category: tech.category ?? null,
60
+ impact: tech.impact ?? null,
61
+ rating: tech.rating ?? null,
62
+ fix: tech.fix ?? null,
63
+ sourceUrl: tech.sourceUrl ?? null,
64
+ confidence: tech.confidence ?? null,
65
+ template: tech.template ?? null,
66
+ deprecated: tech.deprecated ?? false,
67
+ });
68
+ }
69
+ }
70
+
71
+ return catalog;
72
+ }
73
+
74
+ /**
75
+ * Write the catalog as formatted JSON to the given output path.
76
+ * @param {string} outputPath - Absolute or relative path for the JSON file
77
+ * @returns {{ path: string, count: number }} Written path and entry count
78
+ */
79
+ function writeCatalogJson(outputPath) {
80
+ const catalog = generateCatalog();
81
+ const resolved = path.resolve(outputPath);
82
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
83
+ fs.writeFileSync(resolved, JSON.stringify(catalog, null, 2) + '\n', 'utf8');
84
+ return { path: resolved, count: catalog.length };
85
+ }
86
+
87
+ module.exports = { generateCatalog, writeCatalogJson };
package/src/index.js CHANGED
@@ -87,6 +87,8 @@ const { setupOpenCode } = require('./opencode/setup');
87
87
  const { getOpenCodeGovernanceSummary } = require('./opencode/governance');
88
88
  const { runOpenCodeDeepReview } = require('./opencode/deep-review');
89
89
  const { opencodeInteractive } = require('./opencode/interactive');
90
+ const { detectPlatforms, getCatalog, synergyReport } = require('./public-api');
91
+ const { createServer, startServer } = require('./server');
90
92
 
91
93
  module.exports = {
92
94
  audit,
@@ -96,6 +98,11 @@ module.exports = {
96
98
  applyProposalBundle,
97
99
  getGovernanceSummary,
98
100
  runBenchmark,
101
+ detectPlatforms,
102
+ getCatalog,
103
+ synergyReport,
104
+ createServer,
105
+ startServer,
99
106
  DOMAIN_PACKS,
100
107
  detectDomainPacks,
101
108
  MCP_PACKS,
package/src/plugins.js ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Plugin system for Nerviq.
3
+ * Allows users to extend audits with custom checks via nerviq.config.js.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const REQUIRED_CHECK_FIELDS = ['id', 'name', 'check', 'impact', 'category', 'fix'];
10
+ const VALID_IMPACTS = ['critical', 'high', 'medium', 'low'];
11
+
12
+ /**
13
+ * Validate a single plugin object.
14
+ * Returns { valid: true } or { valid: false, errors: [...] }.
15
+ */
16
+ function validatePlugin(plugin) {
17
+ const errors = [];
18
+
19
+ if (!plugin || typeof plugin !== 'object') {
20
+ return { valid: false, errors: ['Plugin must be a non-null object'] };
21
+ }
22
+
23
+ if (!plugin.name || typeof plugin.name !== 'string') {
24
+ errors.push('Plugin must have a non-empty string "name" field');
25
+ }
26
+
27
+ if (!plugin.checks || typeof plugin.checks !== 'object' || Array.isArray(plugin.checks)) {
28
+ errors.push('Plugin must have a "checks" object');
29
+ return { valid: false, errors };
30
+ }
31
+
32
+ for (const [key, check] of Object.entries(plugin.checks)) {
33
+ for (const field of REQUIRED_CHECK_FIELDS) {
34
+ if (check[field] === undefined || check[field] === null) {
35
+ errors.push(`Check "${key}" is missing required field "${field}"`);
36
+ }
37
+ }
38
+
39
+ if (typeof check.check !== 'function') {
40
+ errors.push(`Check "${key}" field "check" must be a function`);
41
+ }
42
+
43
+ if (check.impact && !VALID_IMPACTS.includes(check.impact)) {
44
+ errors.push(`Check "${key}" has invalid impact "${check.impact}". Must be one of: ${VALID_IMPACTS.join(', ')}`);
45
+ }
46
+ }
47
+
48
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
49
+ }
50
+
51
+ /**
52
+ * Load plugins from nerviq.config.js in the given directory.
53
+ * Returns an array of plugin objects, or [] if no config file exists.
54
+ */
55
+ function loadPlugins(dir) {
56
+ const configPath = path.join(dir, 'nerviq.config.js');
57
+
58
+ if (!fs.existsSync(configPath)) {
59
+ return [];
60
+ }
61
+
62
+ let config;
63
+ try {
64
+ config = require(configPath);
65
+ } catch (err) {
66
+ console.error(`Failed to load nerviq.config.js: ${err.message}`);
67
+ return [];
68
+ }
69
+
70
+ if (!config || !Array.isArray(config.plugins)) {
71
+ return [];
72
+ }
73
+
74
+ const validPlugins = [];
75
+ for (const plugin of config.plugins) {
76
+ const result = validatePlugin(plugin);
77
+ if (result.valid) {
78
+ validPlugins.push(plugin);
79
+ } else {
80
+ console.error(`Plugin "${plugin && plugin.name || 'unknown'}" is invalid: ${result.errors.join('; ')}`);
81
+ }
82
+ }
83
+
84
+ return validPlugins;
85
+ }
86
+
87
+ /**
88
+ * Merge plugin checks into the existing techniques object.
89
+ * Plugin checks are prefixed with "plugin:" to avoid key collisions.
90
+ * Returns a new merged techniques object (does not mutate the original).
91
+ */
92
+ function mergePluginChecks(techniques, plugins) {
93
+ const merged = { ...techniques };
94
+
95
+ for (const plugin of plugins) {
96
+ for (const [key, check] of Object.entries(plugin.checks)) {
97
+ const prefixedKey = `plugin:${plugin.name}:${key}`;
98
+ merged[prefixedKey] = {
99
+ ...check,
100
+ pluginName: plugin.name,
101
+ sourceUrl: check.sourceUrl || null,
102
+ confidence: check.confidence !== undefined ? check.confidence : 0.5,
103
+ };
104
+ }
105
+ }
106
+
107
+ return merged;
108
+ }
109
+
110
+ module.exports = { loadPlugins, mergePluginChecks, validatePlugin };
@@ -0,0 +1,173 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { audit } = require('./audit');
4
+ const { harmonyAudit } = require('./harmony/audit');
5
+ const { generateCatalog } = require('./catalog');
6
+ const { compoundAudit, calculateAmplification } = require('./synergy/evidence');
7
+ const { analyzeCompensation } = require('./synergy/compensation');
8
+ const { discoverPatterns } = require('./synergy/patterns');
9
+ const { rankRecommendations } = require('./synergy/ranking');
10
+ const { generateSynergyReport } = require('./synergy/report');
11
+ const { routeTask } = require('./synergy/routing');
12
+ const { CodexProjectContext } = require('./codex/context');
13
+ const { GeminiProjectContext } = require('./gemini/context');
14
+ const { CopilotProjectContext } = require('./copilot/context');
15
+ const { CursorProjectContext } = require('./cursor/context');
16
+ const { WindsurfProjectContext } = require('./windsurf/context');
17
+ const { AiderProjectContext } = require('./aider/context');
18
+ const { OpenCodeProjectContext } = require('./opencode/context');
19
+
20
+ const PLATFORM_ORDER = [
21
+ 'claude',
22
+ 'codex',
23
+ 'gemini',
24
+ 'copilot',
25
+ 'cursor',
26
+ 'windsurf',
27
+ 'aider',
28
+ 'opencode',
29
+ ];
30
+
31
+ const PLATFORM_DETECTORS = {
32
+ claude: (dir) => exists(path.join(dir, 'CLAUDE.md')) || exists(path.join(dir, '.claude')),
33
+ codex: (dir) => CodexProjectContext.isCodexRepo(dir),
34
+ gemini: (dir) => GeminiProjectContext.isGeminiRepo(dir),
35
+ copilot: (dir) => CopilotProjectContext.isCopilotRepo(dir),
36
+ cursor: (dir) => CursorProjectContext.isCursorRepo(dir),
37
+ windsurf: (dir) => WindsurfProjectContext.isWindsurfRepo(dir),
38
+ aider: (dir) => AiderProjectContext.isAiderRepo(dir),
39
+ opencode: (dir) => OpenCodeProjectContext.isOpenCodeRepo(dir),
40
+ };
41
+
42
+ const IMPACT_SCORES = {
43
+ critical: 5,
44
+ high: 4,
45
+ medium: 3,
46
+ low: 2,
47
+ };
48
+
49
+ function exists(targetPath) {
50
+ try {
51
+ return fs.existsSync(targetPath);
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function resolveDir(dir) {
58
+ return path.resolve(dir || '.');
59
+ }
60
+
61
+ function detectPlatforms(dir) {
62
+ const resolvedDir = resolveDir(dir);
63
+ return PLATFORM_ORDER.filter((platform) => {
64
+ const detect = PLATFORM_DETECTORS[platform];
65
+ return typeof detect === 'function' ? detect(resolvedDir) : false;
66
+ });
67
+ }
68
+
69
+ function getCatalog() {
70
+ return generateCatalog();
71
+ }
72
+
73
+ function buildPatternHistory(dir, platformAudits) {
74
+ const timestamp = new Date().toISOString();
75
+ return Object.entries(platformAudits).map(([platform, result]) => ({
76
+ dir,
77
+ platform,
78
+ score: result.score,
79
+ findings: result.results || [],
80
+ timestamp,
81
+ }));
82
+ }
83
+
84
+ function buildRecommendationPool(platformAudits, compensation) {
85
+ const recommendations = [];
86
+
87
+ for (const [platform, result] of Object.entries(platformAudits)) {
88
+ const topActions = Array.isArray(result.topNextActions) ? result.topNextActions : [];
89
+ for (const action of topActions) {
90
+ recommendations.push({
91
+ key: action.key,
92
+ name: action.name,
93
+ description: action.fix,
94
+ impact: action.impact,
95
+ sourcePlatform: platform,
96
+ applicablePlatforms: [platform],
97
+ validatedOn: [platform],
98
+ baseScore: IMPACT_SCORES[action.impact] || 1,
99
+ });
100
+ }
101
+ }
102
+
103
+ for (const addition of compensation.recommendedAdditions || []) {
104
+ recommendations.push({
105
+ key: `add-${addition.platform}`,
106
+ name: `Add ${addition.platform}`,
107
+ description: `Covers ${addition.wouldCover.map((item) => item.label).join(', ')}`,
108
+ impact: addition.wouldCover.length >= 2 ? 'high' : 'medium',
109
+ sourcePlatform: addition.platform,
110
+ applicablePlatforms: [addition.platform],
111
+ validatedOn: [],
112
+ fillsGap: true,
113
+ baseScore: Math.max(2, Math.min(5, Math.round(addition.estimatedBenefit / Math.max(1, addition.wouldCover.length)))),
114
+ });
115
+ }
116
+
117
+ return recommendations;
118
+ }
119
+
120
+ async function synergyReport(dir) {
121
+ const resolvedDir = resolveDir(dir);
122
+ const activePlatforms = detectPlatforms(resolvedDir);
123
+ const platformAudits = {};
124
+ const errors = [];
125
+
126
+ for (const platform of activePlatforms) {
127
+ try {
128
+ platformAudits[platform] = await audit({
129
+ dir: resolvedDir,
130
+ platform,
131
+ silent: true,
132
+ });
133
+ } catch (error) {
134
+ errors.push({ platform, message: error.message });
135
+ }
136
+ }
137
+
138
+ const compound = compoundAudit(platformAudits);
139
+ const amplification = calculateAmplification(platformAudits);
140
+ const compensation = analyzeCompensation(activePlatforms, platformAudits);
141
+ const patterns = discoverPatterns(buildPatternHistory(resolvedDir, platformAudits)).patterns;
142
+ const recommendations = rankRecommendations(
143
+ buildRecommendationPool(platformAudits, compensation),
144
+ activePlatforms
145
+ );
146
+ const report = generateSynergyReport({
147
+ platformAudits,
148
+ activePlatforms,
149
+ recommendations,
150
+ });
151
+
152
+ return {
153
+ dir: resolvedDir,
154
+ activePlatforms,
155
+ platformAudits,
156
+ compound,
157
+ amplification,
158
+ compensation,
159
+ patterns,
160
+ recommendations,
161
+ errors,
162
+ report,
163
+ };
164
+ }
165
+
166
+ module.exports = {
167
+ audit,
168
+ harmonyAudit,
169
+ detectPlatforms,
170
+ getCatalog,
171
+ routeTask,
172
+ synergyReport,
173
+ };
package/src/server.js ADDED
@@ -0,0 +1,123 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { URL } = require('url');
5
+ const { version } = require('../package.json');
6
+ const { audit } = require('./audit');
7
+ const { harmonyAudit } = require('./harmony/audit');
8
+ const { getCatalog } = require('./public-api');
9
+
10
+ const SUPPORTED_PLATFORMS = new Set([
11
+ 'claude',
12
+ 'codex',
13
+ 'gemini',
14
+ 'copilot',
15
+ 'cursor',
16
+ 'windsurf',
17
+ 'aider',
18
+ 'opencode',
19
+ ]);
20
+
21
+ function sendJson(res, statusCode, payload) {
22
+ const body = JSON.stringify(payload, null, 2);
23
+ res.writeHead(statusCode, {
24
+ 'Content-Type': 'application/json; charset=utf-8',
25
+ 'Content-Length': Buffer.byteLength(body),
26
+ 'Cache-Control': 'no-store',
27
+ });
28
+ res.end(body);
29
+ }
30
+
31
+ function resolveRequestDir(baseDir, rawDir) {
32
+ const requested = rawDir || '.';
33
+ const resolved = path.isAbsolute(requested)
34
+ ? requested
35
+ : path.resolve(baseDir, requested);
36
+
37
+ if (!fs.existsSync(resolved)) {
38
+ const error = new Error(`Directory not found: ${resolved}`);
39
+ error.statusCode = 400;
40
+ throw error;
41
+ }
42
+
43
+ return resolved;
44
+ }
45
+
46
+ function normalizePlatform(rawPlatform) {
47
+ const platform = (rawPlatform || 'claude').toLowerCase();
48
+ if (!SUPPORTED_PLATFORMS.has(platform)) {
49
+ const error = new Error(`Unsupported platform '${rawPlatform}'.`);
50
+ error.statusCode = 400;
51
+ throw error;
52
+ }
53
+ return platform;
54
+ }
55
+
56
+ function createServer(options = {}) {
57
+ const baseDir = path.resolve(options.baseDir || process.cwd());
58
+
59
+ return http.createServer(async (req, res) => {
60
+ const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
61
+
62
+ if (req.method !== 'GET') {
63
+ sendJson(res, 405, { error: 'Method not allowed' });
64
+ return;
65
+ }
66
+
67
+ try {
68
+ if (requestUrl.pathname === '/api/health') {
69
+ sendJson(res, 200, {
70
+ status: 'ok',
71
+ version,
72
+ checks: getCatalog().length,
73
+ });
74
+ return;
75
+ }
76
+
77
+ if (requestUrl.pathname === '/api/catalog') {
78
+ sendJson(res, 200, getCatalog());
79
+ return;
80
+ }
81
+
82
+ if (requestUrl.pathname === '/api/audit') {
83
+ const dir = resolveRequestDir(baseDir, requestUrl.searchParams.get('dir'));
84
+ const platform = normalizePlatform(requestUrl.searchParams.get('platform'));
85
+ const result = await audit({ dir, platform, silent: true });
86
+ sendJson(res, 200, result);
87
+ return;
88
+ }
89
+
90
+ if (requestUrl.pathname === '/api/harmony') {
91
+ const dir = resolveRequestDir(baseDir, requestUrl.searchParams.get('dir'));
92
+ const result = await harmonyAudit({ dir, silent: true });
93
+ sendJson(res, 200, result);
94
+ return;
95
+ }
96
+
97
+ sendJson(res, 404, { error: 'Not found' });
98
+ } catch (error) {
99
+ sendJson(res, error.statusCode || 500, {
100
+ error: error.message,
101
+ });
102
+ }
103
+ });
104
+ }
105
+
106
+ function startServer(options = {}) {
107
+ const port = options.port == null ? 3000 : Number(options.port);
108
+ const host = options.host || '127.0.0.1';
109
+ const server = createServer(options);
110
+
111
+ return new Promise((resolve, reject) => {
112
+ server.once('error', reject);
113
+ server.listen(port, host, () => {
114
+ server.off('error', reject);
115
+ resolve(server);
116
+ });
117
+ });
118
+ }
119
+
120
+ module.exports = {
121
+ createServer,
122
+ startServer,
123
+ };
@@ -1,10 +1,8 @@
1
1
  /**
2
- * Official source URL registry for platform technique catalogs.
2
+ * Official source URL + confidence registry for platform technique catalogs.
3
3
  *
4
- * These URLs intentionally point to the nearest authoritative official page for
5
- * a given category or check. Some advisory/internal heuristics do not have a
6
- * single line-item normative doc, so they fall back to the closest official
7
- * platform page that governs the surrounding feature area.
4
+ * We attach metadata at export time so the catalogs stay maintainable without
5
+ * hand-editing hundreds of technique literals.
8
6
  */
9
7
 
10
8
  const SOURCE_URLS = {
@@ -52,7 +50,7 @@ const SOURCE_URLS = {
52
50
  mcp: 'https://developers.openai.com/codex/mcp',
53
51
  skills: 'https://developers.openai.com/codex/skills',
54
52
  agents: 'https://developers.openai.com/codex/subagents',
55
- automation: 'https://developers.openai.com/codex/cli',
53
+ automation: 'https://developers.openai.com/codex/app/automations',
56
54
  review: 'https://developers.openai.com/codex/cli',
57
55
  local: 'https://developers.openai.com/codex/app/local-environments',
58
56
  'quality-deep': 'https://developers.openai.com/codex/feature-maturity',
@@ -66,6 +64,7 @@ const SOURCE_URLS = {
66
64
  codexAutomationAppPrereqAcknowledged: 'https://developers.openai.com/codex/app/automations',
67
65
  codexGitHubActionSafeStrategy: 'https://developers.openai.com/codex/github-action',
68
66
  codexGitHubActionPromptSourceExclusive: 'https://developers.openai.com/codex/github-action',
67
+ codexGitHubActionSinglePromptSource: 'https://developers.openai.com/codex/github-action',
69
68
  codexGitHubActionTriggerAllowlistsExplicit: 'https://developers.openai.com/codex/github-action',
70
69
  codexCiAuthUsesManagedKey: 'https://developers.openai.com/codex/github-action',
71
70
  codexPluginConfigValid: 'https://developers.openai.com/codex/skills',
@@ -84,9 +83,9 @@ const SOURCE_URLS = {
84
83
  sandbox: 'https://geminicli.com/docs/cli/sandbox/',
85
84
  agents: 'https://geminicli.com/docs/core/subagents/',
86
85
  skills: 'https://geminicli.com/docs/cli/skills/',
87
- automation: 'https://google-gemini.github.io/gemini-cli/docs/cli/headless.html',
86
+ automation: 'https://geminicli.com/docs/get-started/',
88
87
  extensions: 'https://geminicli.com/docs/extensions/',
89
- review: 'https://ai.google.dev/gemini-api/docs/coding-agents',
88
+ review: 'https://geminicli.com/docs/get-started/',
90
89
  'quality-deep': 'https://geminicli.com/docs/get-started/',
91
90
  commands: 'https://geminicli.com/docs/cli/custom-commands/',
92
91
  advisory: 'https://geminicli.com/docs/get-started/',
@@ -96,60 +95,60 @@ const SOURCE_URLS = {
96
95
  },
97
96
  },
98
97
  copilot: {
99
- defaultUrl: 'https://docs.github.com/copilot',
98
+ defaultUrl: 'https://docs.github.com/en/copilot',
100
99
  byCategory: {
101
- instructions: 'https://docs.github.com/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot',
102
- config: 'https://code.visualstudio.com/docs/copilot/customization/custom-instructions',
103
- trust: 'https://code.visualstudio.com/docs/copilot/security',
104
- mcp: 'https://code.visualstudio.com/docs/copilot/chat/mcp-servers',
100
+ instructions: 'https://docs.github.com/en/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot',
101
+ config: 'https://docs.github.com/en/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot',
102
+ trust: 'https://docs.github.com/en/copilot/responsible-use-of-github-copilot-features/github-copilot-data-handling',
103
+ mcp: 'https://docs.github.com/en/copilot/customizing-copilot/using-model-context-protocol/extending-copilot-chat-with-mcp',
105
104
  'cloud-agent': 'https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-coding-agent',
106
105
  organization: 'https://docs.github.com/en/copilot/how-tos/administer-copilot/manage-for-organization/manage-policies',
107
- 'prompt-files': 'https://code.visualstudio.com/docs/copilot/customization/prompt-files',
108
- 'skills-agents': 'https://code.visualstudio.com/docs/copilot/agents/overview',
106
+ 'prompt-files': 'https://docs.github.com/en/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot',
107
+ 'skills-agents': 'https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-coding-agent',
109
108
  'ci-automation': 'https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/customize-the-agent-environment',
110
109
  enterprise: 'https://docs.github.com/en/copilot/how-tos/administer-copilot/manage-for-enterprise',
111
110
  extensions: 'https://docs.github.com/en/copilot/building-copilot-extensions/about-building-copilot-extensions',
112
- 'quality-deep': 'https://docs.github.com/copilot',
113
- advisory: 'https://docs.github.com/copilot',
114
- freshness: 'https://github.blog/changelog/',
111
+ 'quality-deep': 'https://docs.github.com/en/copilot',
112
+ advisory: 'https://docs.github.com/en/copilot',
113
+ freshness: 'https://docs.github.com/en/copilot',
115
114
  },
116
115
  },
117
116
  cursor: {
118
- defaultUrl: 'https://cursor.com/docs',
117
+ defaultUrl: 'https://docs.cursor.com/',
119
118
  byCategory: {
120
- rules: 'https://cursor.com/docs/context/rules',
121
- config: 'https://cursor.com/docs',
122
- trust: 'https://cursor.com/docs/enterprise/privacy-and-data-governance',
119
+ rules: 'https://docs.cursor.com/context/rules',
120
+ config: 'https://docs.cursor.com/',
121
+ trust: 'https://docs.cursor.com/enterprise/privacy-and-data-governance',
123
122
  'agent-mode': 'https://docs.cursor.com/en/chat/agent',
124
- mcp: 'https://cursor.com/docs/cli/mcp',
123
+ mcp: 'https://docs.cursor.com/cli/mcp',
125
124
  'instructions-quality': 'https://docs.cursor.com/guides/working-with-context',
126
125
  'background-agents': 'https://docs.cursor.com/en/background-agents',
127
- automations: 'https://cursor.com/docs/cloud-agent/automations',
128
- enterprise: 'https://cursor.com/docs/enterprise',
129
- bugbot: 'https://cursor.com/docs/bugbot',
130
- 'cross-surface': 'https://cursor.com/docs',
126
+ automations: 'https://docs.cursor.com/en/background-agents/automations',
127
+ enterprise: 'https://docs.cursor.com/enterprise',
128
+ bugbot: 'https://docs.cursor.com/bugbot',
129
+ 'cross-surface': 'https://docs.cursor.com/',
131
130
  'quality-deep': 'https://docs.cursor.com/guides/working-with-context',
132
- advisory: 'https://cursor.com/docs',
133
- freshness: 'https://cursor.com/changelog',
131
+ advisory: 'https://docs.cursor.com/',
132
+ freshness: 'https://docs.cursor.com/',
134
133
  },
135
134
  },
136
135
  windsurf: {
137
- defaultUrl: 'https://docs.windsurf.com/windsurf/cascade',
136
+ defaultUrl: 'https://docs.windsurf.com/windsurf/cascade/cascade',
138
137
  byCategory: {
139
- rules: 'https://windsurf.com/university/general-education/intro-rules-memories',
138
+ rules: 'https://docs.windsurf.com/windsurf/cascade/cascade',
140
139
  config: 'https://docs.windsurf.com/windsurf/cascade/cascade',
141
- trust: 'https://windsurf.com/security',
140
+ trust: 'https://docs.windsurf.com/windsurf/cascade/cascade',
142
141
  'cascade-agent': 'https://docs.windsurf.com/windsurf/cascade/agents-md',
143
142
  mcp: 'https://docs.windsurf.com/windsurf/cascade/mcp',
144
143
  'instructions-quality': 'https://docs.windsurf.com/windsurf/cascade/agents-md',
145
144
  workflows: 'https://docs.windsurf.com/windsurf/cascade/workflows',
146
145
  memories: 'https://docs.windsurf.com/windsurf/cascade/memories',
147
- enterprise: 'https://windsurf.com/security',
146
+ enterprise: 'https://docs.windsurf.com/windsurf/cascade/cascade',
148
147
  cascadeignore: 'https://docs.windsurf.com/windsurf/cascade/cascade',
149
148
  'cross-surface': 'https://docs.windsurf.com/windsurf/cascade/cascade',
150
149
  'quality-deep': 'https://docs.windsurf.com/windsurf/cascade/cascade',
151
150
  advisory: 'https://docs.windsurf.com/windsurf/cascade/cascade',
152
- freshness: 'https://windsurf.com/changelog',
151
+ freshness: 'https://docs.windsurf.com/windsurf/cascade/cascade',
153
152
  },
154
153
  },
155
154
  aider: {
@@ -162,36 +161,76 @@ const SOURCE_URLS = {
162
161
  conventions: 'https://aider.chat/docs/usage/conventions.html',
163
162
  architecture: 'https://aider.chat/docs/usage/modes.html',
164
163
  security: 'https://aider.chat/docs/config/dotenv.html',
165
- ci: 'https://aider.chat/docs/scripting.html',
166
- quality: 'https://aider.chat/docs/usage/lint-test.html',
164
+ ci: 'https://aider.chat/docs/usage/modes.html',
165
+ quality: 'https://aider.chat/docs/usage/modes.html',
167
166
  'workflow-patterns': 'https://aider.chat/docs/usage/modes.html',
168
- 'editor-integration': 'https://aider.chat/docs/config/editor.html',
167
+ 'editor-integration': 'https://aider.chat/docs/config.html',
169
168
  'release-readiness': 'https://aider.chat/docs/',
170
169
  },
171
170
  },
172
171
  opencode: {
173
- defaultUrl: 'https://opencode.ai/docs/',
172
+ defaultUrl: 'https://github.com/sst/opencode',
174
173
  byCategory: {
175
- instructions: 'https://opencode.ai/docs/rules/',
176
- config: 'https://opencode.ai/docs/config/',
177
- permissions: 'https://opencode.ai/docs/permissions',
178
- plugins: 'https://opencode.ai/docs/plugins/',
179
- security: 'https://opencode.ai/docs/tools/',
180
- mcp: 'https://opencode.ai/docs/mcp-servers/',
181
- ci: 'https://opencode.ai/docs/github/',
182
- 'quality-deep': 'https://opencode.ai/docs/',
183
- skills: 'https://opencode.ai/docs/skills/',
184
- agents: 'https://opencode.ai/docs/agents/',
185
- commands: 'https://opencode.ai/docs/commands/',
186
- tui: 'https://opencode.ai/docs/themes/',
187
- governance: 'https://opencode.ai/docs/github/',
188
- 'release-freshness': 'https://opencode.ai/docs/',
189
- 'mixed-agent': 'https://opencode.ai/docs/modes/',
190
- propagation: 'https://opencode.ai/docs/config/',
174
+ instructions: 'https://github.com/sst/opencode/blob/dev/AGENTS.md',
175
+ config: 'https://github.com/sst/opencode/tree/dev/.opencode',
176
+ permissions: 'https://github.com/sst/opencode/tree/dev/.opencode',
177
+ plugins: 'https://github.com/sst/opencode/tree/dev/.opencode',
178
+ security: 'https://github.com/sst/opencode/blob/dev/SECURITY.md',
179
+ mcp: 'https://github.com/sst/opencode/tree/dev/.opencode',
180
+ ci: 'https://github.com/sst/opencode/tree/dev/.github',
181
+ 'quality-deep': 'https://github.com/sst/opencode/blob/dev/README.md',
182
+ skills: 'https://github.com/sst/opencode/tree/dev/.opencode',
183
+ agents: 'https://github.com/sst/opencode/blob/dev/AGENTS.md',
184
+ commands: 'https://github.com/sst/opencode/tree/dev/.opencode',
185
+ tui: 'https://github.com/sst/opencode/blob/dev/README.md',
186
+ governance: 'https://github.com/sst/opencode/blob/dev/SECURITY.md',
187
+ 'release-freshness': 'https://github.com/sst/opencode/releases',
188
+ 'mixed-agent': 'https://github.com/sst/opencode/blob/dev/AGENTS.md',
189
+ propagation: 'https://github.com/sst/opencode/tree/dev/.opencode',
191
190
  },
192
191
  },
193
192
  };
194
193
 
194
+ const STALE_CONFIDENCE_IDS = new Set([
195
+ 'CX-B04',
196
+ 'CX-B09',
197
+ 'CX-C05',
198
+ 'CX-C06',
199
+ ]);
200
+
201
+ const RUNTIME_CONFIDENCE_IDS = {
202
+ codex: new Set([
203
+ 'CX-B01',
204
+ 'CX-C01',
205
+ 'CX-C02',
206
+ 'CX-C03',
207
+ 'CX-D01',
208
+ 'CX-E02',
209
+ 'CX-H02',
210
+ 'CX-H03',
211
+ 'CX-I01',
212
+ ]),
213
+ gemini: new Set(['GM-Q01', 'GM-Q02', 'GM-Q03', 'GM-Q04', 'GM-Q05']),
214
+ copilot: new Set(['CP-Q01', 'CP-Q02', 'CP-Q03', 'CP-Q04', 'CP-Q05']),
215
+ };
216
+
217
+ function hasRuntimeVerificationSignal(technique) {
218
+ const haystack = `${technique.name || ''}\n${technique.fix || ''}`;
219
+ return /experiment(?:ally)? confirmed|confirmed by (?:live )?experiment|current runtime|runtime evidence|runtime-verified|validated in current runtime|observed in current runtime|measured in live experiment|reproduced in runtime|confirmed by experiment/i.test(haystack);
220
+ }
221
+
222
+ function resolveConfidence(platform, technique) {
223
+ if (STALE_CONFIDENCE_IDS.has(technique.id)) {
224
+ return 0.3;
225
+ }
226
+
227
+ if (RUNTIME_CONFIDENCE_IDS[platform]?.has(technique.id) || hasRuntimeVerificationSignal(technique)) {
228
+ return 0.9;
229
+ }
230
+
231
+ return 0.7;
232
+ }
233
+
195
234
  function attachSourceUrls(platform, techniques) {
196
235
  const mapping = SOURCE_URLS[platform];
197
236
  if (!mapping) {
@@ -199,15 +238,17 @@ function attachSourceUrls(platform, techniques) {
199
238
  }
200
239
 
201
240
  for (const [key, technique] of Object.entries(techniques)) {
202
- if (technique.sourceUrl) continue;
203
241
  const resolved =
204
242
  mapping.byKey?.[key] ||
205
243
  mapping.byCategory?.[technique.category] ||
206
244
  mapping.defaultUrl;
245
+
207
246
  if (!resolved) {
208
247
  throw new Error(`No sourceUrl mapping found for ${platform}:${key}`);
209
248
  }
249
+
210
250
  technique.sourceUrl = resolved;
251
+ technique.confidence = resolveConfidence(platform, technique);
211
252
  }
212
253
 
213
254
  return techniques;