@nerviq/cli 0.9.5 → 1.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0 — 2026-04-05
4
+ ### 🎉 First Stable Release
5
+ - 8 platforms: Claude Code, Codex, Gemini CLI, GitHub Copilot, Cursor, Windsurf, Aider, OpenCode
6
+ - 673 checks with sourceUrl and confidence on every check
7
+ - Harmony: cross-platform drift detection and alignment
8
+ - Synergy: multi-agent amplification and task routing
9
+ - Plugin system: custom checks via nerviq.config.js
10
+ - SDK: @nerviq/sdk with TypeScript types
11
+ - REST API: nerviq serve --port 3000
12
+ - MCP Server: nerviq as MCP tool provider
13
+ - VS Code Extension
14
+ - GitHub Action with SARIF support
15
+ - Performance: 226ms total audit across 8 platforms
16
+ - CLI commands: audit, setup, plan, apply, governance, benchmark, harmony-audit, synergy-report, deep-review, interactive, watch, history, compare, trend, feedback, catalog, certify, doctor, convert, migrate, serve
17
+ - 213 tests across 21 test suites
18
+ - AGPL-3.0 license
19
+
3
20
  ## [1.16.2] - 2026-04-03
4
21
 
5
22
  ### Changed
package/README.md CHANGED
@@ -4,23 +4,24 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@nerviq/cli)](https://www.npmjs.com/package/@nerviq/cli)
6
6
  [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
7
+ [![Checks: 673](https://img.shields.io/badge/checks-673-brightgreen)](https://github.com/nerviq/nerviq)
7
8
 
8
9
  ---
9
10
 
10
- ### ⚠️ Beta — Currently Claude Code only
11
+ ### 8 Platforms Supported
11
12
 
12
- Nerviq is in **beta**. The current release fully supports **Claude Code** (90 checks, audit, setup, governance, benchmark).
13
+ Nerviq v1.0 ships with full audit, setup, governance, and benchmark support for **8 AI coding platforms**:
13
14
 
14
- **Coming soon:**
15
- - Codex (OpenAI)
16
- - Gemini CLI (Google)
17
- - GitHub Copilot
18
- - Cursor
19
- - Windsurf
20
- - Aider
21
- - OpenCode
22
- - **Harmony** cross-platform drift detection
23
- - **Synergy** multi-agent amplification
15
+ | Platform | Checks | Status |
16
+ |----------|--------|--------|
17
+ | Claude Code | 90 | Full |
18
+ | Codex (OpenAI) | 83 | Full |
19
+ | Gemini CLI (Google) | 83 | Full |
20
+ | GitHub Copilot | 83 | Full |
21
+ | Cursor | 83 | Full |
22
+ | Windsurf | 83 | Full |
23
+ | Aider | 85 | Full |
24
+ | OpenCode | 83 | Full |
24
25
 
25
26
  ---
26
27
 
@@ -61,7 +62,7 @@ npx @nerviq/cli benchmark # Before/after in isolated copy
61
62
 
62
63
  No install required. Zero dependencies.
63
64
 
64
- ## 90 Checks Across 14 Categories
65
+ ## 673 Checks Across 14 Categories
65
66
 
66
67
  | Category | Checks | Examples |
67
68
  |----------|--------|---------|
@@ -79,11 +80,126 @@ No install required. Zero dependencies.
79
80
  | Features | 2 | channels, worktrees |
80
81
  | Quality Deep | 9 | freshness, contradictions, deprecated patterns |
81
82
 
83
+ ## Harmony — Cross-Platform Alignment
84
+
85
+ Harmony detects drift between your AI coding platforms and keeps them in sync.
86
+
87
+ ```bash
88
+ npx @nerviq/cli harmony-audit # Cross-platform DX audit (0-100 harmony score)
89
+ npx @nerviq/cli harmony-sync # Sync shared config across platforms
90
+ npx @nerviq/cli harmony-drift # Detect drift between platform configs
91
+ npx @nerviq/cli harmony-advise # Cross-platform improvement advice
92
+ npx @nerviq/cli harmony-watch # Live monitoring for config drift
93
+ npx @nerviq/cli harmony-governance # Unified governance across platforms
94
+ ```
95
+
96
+ ## Synergy — Multi-Agent Amplification
97
+
98
+ Synergy analyzes how your platforms work together and finds amplification opportunities.
99
+
100
+ ```bash
101
+ npx @nerviq/cli synergy-report # Multi-agent synergy analysis
102
+ ```
103
+
104
+ Synergy evaluates compound audit results, discovers compensation patterns (where one platform covers another's gaps), and ranks recommendations by cross-platform impact.
105
+
106
+ ## SDK — `@nerviq/sdk`
107
+
108
+ Programmatic access to all Nerviq capabilities:
109
+
110
+ ```js
111
+ const { audit, harmonyAudit, synergyReport, detectPlatforms } = require('@nerviq/sdk');
112
+
113
+ const result = await audit('.', 'claude');
114
+ console.log(`Score: ${result.score}/100`);
115
+
116
+ const platforms = detectPlatforms('.');
117
+ console.log(`Active platforms: ${platforms.join(', ')}`);
118
+
119
+ const harmony = await harmonyAudit('.');
120
+ console.log(`Harmony score: ${harmony.harmonyScore}/100`);
121
+ ```
122
+
123
+ ## MCP Server — `nerviq serve`
124
+
125
+ Nerviq ships with a built-in MCP-compatible HTTP server for integration with AI agents:
126
+
127
+ ```bash
128
+ npx @nerviq/cli serve --port 3000
129
+ ```
130
+
131
+ Endpoints:
132
+ - `GET /api/health` — Server health check
133
+ - `GET /api/catalog` — Full check catalog
134
+ - `POST /api/audit` — Run audit on a directory
135
+ - `GET /api/harmony` — Cross-platform harmony data
136
+
137
+ ## Plugin System — `nerviq.config.js`
138
+
139
+ Extend Nerviq with custom checks via a config file in your project root:
140
+
141
+ ```js
142
+ // nerviq.config.js
143
+ module.exports = {
144
+ plugins: [
145
+ {
146
+ name: 'my-company-checks',
147
+ checks: {
148
+ internalDocs: {
149
+ id: 'internalDocs',
150
+ name: 'Internal docs present',
151
+ check: (dir) => require('fs').existsSync(`${dir}/docs/internal.md`),
152
+ impact: 'medium',
153
+ category: 'Quality',
154
+ fix: 'Add docs/internal.md with team-specific guidelines',
155
+ },
156
+ },
157
+ },
158
+ ],
159
+ };
160
+ ```
161
+
162
+ See [docs/plugins.md](docs/plugins.md) for full plugin API reference.
163
+
164
+ ## GitHub Action
165
+
166
+ Add Nerviq to your CI pipeline:
167
+
168
+ ```yaml
169
+ # .github/workflows/nerviq.yml
170
+ name: Nerviq Audit
171
+ on: [push, pull_request]
172
+
173
+ jobs:
174
+ audit:
175
+ runs-on: ubuntu-latest
176
+ steps:
177
+ - uses: actions/checkout@v4
178
+ - uses: nerviq/nerviq@v1
179
+ with:
180
+ threshold: 60
181
+ ```
182
+
183
+ The action outputs `score`, `passed`, and `total` for use in downstream steps. Fails the workflow if the score is below the configured threshold.
184
+
185
+ ## Certification
186
+
187
+ Earn a Nerviq certification badge for your project:
188
+
189
+ ```bash
190
+ npx @nerviq/cli certify # Run certification and display badge
191
+ ```
192
+
193
+ Levels:
194
+ - **Gold** — Harmony score >= 80, all platforms >= 70
195
+ - **Silver** — Harmony score >= 60, all platforms >= 50
196
+ - **Bronze** — Any platform >= 40
197
+
82
198
  ## All Commands
83
199
 
84
200
  | Command | What it does |
85
201
  |---------|-------------|
86
- | `nerviq audit` | Score 0-100 against 90 checks |
202
+ | `nerviq audit` | Score 0-100 against 673 checks |
87
203
  | `nerviq audit --lite` | Quick top-3 scan |
88
204
  | `nerviq setup` | Generate starter-safe CLAUDE.md + hooks + commands |
89
205
  | `nerviq augment` | Repo-aware improvement plan (no writes) |
@@ -100,7 +216,20 @@ No install required. Zero dependencies.
100
216
  | `nerviq trend` | Export trend report |
101
217
  | `nerviq feedback` | Record recommendation outcomes |
102
218
  | `nerviq badge` | shields.io badge for README |
219
+ | `nerviq certify` | Certification level + badge |
103
220
  | `nerviq scan dir1 dir2` | Compare multiple repos |
221
+ | `nerviq harmony-audit` | Cross-platform DX audit |
222
+ | `nerviq harmony-sync` | Sync config across platforms |
223
+ | `nerviq harmony-drift` | Detect platform drift |
224
+ | `nerviq harmony-advise` | Cross-platform advice |
225
+ | `nerviq harmony-watch` | Live drift monitoring |
226
+ | `nerviq harmony-governance` | Unified platform governance |
227
+ | `nerviq synergy-report` | Multi-agent synergy analysis |
228
+ | `nerviq catalog` | Show check catalog for all 8 platforms |
229
+ | `nerviq doctor` | Self-diagnostics |
230
+ | `nerviq convert` | Convert config between platforms |
231
+ | `nerviq migrate` | Migrate platform config versions |
232
+ | `nerviq serve` | Start local MCP-compatible HTTP API |
104
233
 
105
234
  ## Options
106
235
 
@@ -115,6 +244,17 @@ No install required. Zero dependencies.
115
244
  | `--auto` | Apply without prompts |
116
245
  | `--verbose` | Show all recommendations |
117
246
  | `--format sarif` | SARIF output for code scanning |
247
+ | `--platform NAME` | Target platform (claude, codex, gemini, copilot, cursor, windsurf, aider, opencode) |
248
+
249
+ ## Backed by Research
250
+
251
+ Nerviq is built on the CLAUDEX knowledge engine — the largest verified catalog of AI coding agent techniques:
252
+
253
+ - **315 research documents** covering all 8 platforms
254
+ - **100+ experiments** with tested, rated results
255
+ - **673 checks** each with `sourceUrl` and `confidence` level (0.0-1.0)
256
+ - Every check is traceable to primary documentation or verified experiment
257
+ - 90-day freshness cycle: stale findings are re-verified or pruned
118
258
 
119
259
  ## Privacy
120
260
 
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', 'catalog', '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', 'certify', '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,111 +206,104 @@ 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 = `
205
213
  nerviq v${version}
206
- Score your repo's Claude Code setup. Fix gaps safely. Benchmark the impact.
207
-
208
- Start here (read-only, nothing changes):
209
- npx nerviq Audit your project (10 seconds)
210
- npx nerviq --lite Quick scan: top 3 gaps + next command
211
- npx nerviq --platform codex Audit your Codex repo setup
212
- npx nerviq --platform codex augment Codex-aware advisory pass, no writes
213
- npx nerviq --platform codex suggest-only Structured Codex report, no writes
214
- npx nerviq augment Repo-aware analysis, no writes
215
- npx nerviq suggest-only Structured report, no writes
216
-
217
- Plan and apply (when you're ready to change things):
218
- npx nerviq plan Export proposal bundles with previews
219
- npx nerviq apply Apply proposals selectively with rollback
220
- npx nerviq setup Generate starter-safe baseline
221
- npx nerviq setup --auto Apply all generated files without prompts
222
-
223
- Track progress over time:
224
- npx nerviq history Show score history from saved snapshots
225
- npx nerviq compare Compare latest vs previous snapshot
226
- npx nerviq trend --out r.md Export trend report as markdown
227
-
228
- Multi-repo:
229
- npx nerviq scan dir1 dir2 Compare multiple repos side-by-side
230
-
231
- Advanced:
232
- npx nerviq governance Permission profiles, hooks, policy packs
233
- npx nerviq benchmark Before/after in isolated temp copy
234
- npx nerviq deep-review AI-powered config review (opt-in, uses API)
235
- npx nerviq interactive Step-by-step guided wizard
236
- npx nerviq watch Live monitoring on config changes with cross-platform watch fallback
237
- npx nerviq badge Generate shields.io badge markdown
238
- npx nerviq feedback Record recommendation outcomes or show local outcome summary
239
-
240
- Catalog:
241
- npx nerviq catalog Show check catalog summary for all 8 platforms
242
- npx nerviq catalog --json Full catalog as JSON
243
- npx nerviq catalog --out catalog.json Write catalog to file
244
-
245
- Utilities:
246
- npx nerviq doctor Self-diagnostics: Node version, deps, freshness gates, platform detection
247
- npx nerviq convert --from claude --to codex Convert config between platforms
248
- npx nerviq migrate --platform cursor --from v2 --to v3 Migrate platform config to newer version
249
-
250
- Options:
251
- --threshold N Exit with code 1 if score is below N (useful for CI)
252
- --require A,B Exit with code 1 if named checks fail (e.g. --require secretsProtection,permissionDeny)
253
- --out FILE Write JSON or markdown output to a file
254
- --plan FILE Load a previously exported plan file
255
- --only A,B Limit plan/apply to selected proposal ids or technique keys
256
- --profile NAME Choose permission profile (read-only, suggest-only, safe-write, power-user, internal-research)
257
- --mcp-pack A,B Merge named MCP packs into generated settings (e.g. context7-docs,next-devtools)
258
- --key NAME Recommendation key for feedback logging (e.g. permissionDeny)
259
- --status VALUE Feedback status: accepted, rejected, deferred
260
- --effect VALUE Feedback effect: positive, neutral, negative
261
- --notes TEXT Short notes to store with a feedback event
262
- --source NAME Source label for feedback event (default: manual-cli)
263
- --score-delta N Optional observed score delta tied to the outcome
264
- --platform NAME Choose platform surface (claude default, codex advisory/build preview)
265
- --format NAME Output format for audit results (json, sarif)
266
- --feedback After audit output, prompt "Was this helpful? (y/n)" for each displayed top action and save answers locally
267
- --snapshot Save a normalized snapshot artifact under .claude/nerviq/snapshots/
268
- --lite Show a short top-3 quick scan with one clear next command
269
- --dry-run Preview apply without writing files
270
- --verbose Show all recommendations (not just critical/high)
271
- --json Output as JSON (for CI pipelines)
272
- --auto Apply all generated setup files without prompting
273
- --insights Enable anonymous usage insights (off by default)
274
- --help Show this help
275
- --version Show version
276
-
277
- Examples:
214
+ The intelligent nervous system for AI coding agents.
215
+ Audit, align, and amplify every platform on every project.
216
+
217
+ DISCOVER
218
+ nerviq audit Score your project (0-100)
219
+ nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
220
+ nerviq audit --lite Quick scan: top 3 gaps + next command
221
+ nerviq audit --json Machine-readable JSON output (for CI)
222
+ nerviq scan dir1 dir2 Compare multiple repos side-by-side
223
+ nerviq catalog Full check catalog (all 8 platforms)
224
+ nerviq catalog --json Export full check catalog as JSON
225
+
226
+ SETUP
227
+ nerviq setup Generate starter-safe baseline config files
228
+ nerviq setup --auto Apply all generated files without prompts
229
+ nerviq interactive Step-by-step guided wizard
230
+ nerviq doctor Self-diagnostics: Node, deps, freshness, platform detection
231
+
232
+ IMPROVE
233
+ nerviq augment Improvement plan (no writes)
234
+ nerviq suggest-only Structured report for sharing (no writes)
235
+ nerviq plan Export proposal bundles with diffs
236
+ nerviq plan --out plan.json Save plan to file
237
+ nerviq apply Apply proposals selectively with rollback
238
+ nerviq apply --dry-run Preview changes without writing
239
+
240
+ GOVERN
241
+ nerviq governance Permission profiles + hooks + policy packs
242
+ nerviq governance --json Machine-readable governance summary
243
+ nerviq benchmark Before/after score in isolated temp copy
244
+ nerviq certify Generate certification badge for your project
245
+
246
+ CROSS-PLATFORM
247
+ nerviq harmony-audit Drift detection across all active platforms
248
+ nerviq synergy-report Multi-agent amplification opportunities
249
+ nerviq convert --from X --to Y Convert configs between platforms
250
+ nerviq migrate --platform X Platform version migration helper
251
+ nerviq migrate --platform cursor --from v2 --to v3
252
+
253
+ MONITOR
254
+ nerviq watch Live config monitoring (re-audits on file change)
255
+ nerviq history Score history from saved snapshots
256
+ nerviq compare Latest vs previous snapshot diff
257
+ nerviq trend Score trend over time
258
+ nerviq trend --out report.md Export trend report as markdown
259
+ nerviq feedback Record recommendation outcomes
260
+
261
+ ADVANCED
262
+ nerviq deep-review AI-powered config review (opt-in, uses API key)
263
+ nerviq serve --port 3000 Start local Nerviq REST API server
264
+ nerviq badge Generate shields.io badge markdown
265
+
266
+ OPTIONS
267
+ --platform NAME Platform: claude (default), codex, cursor, copilot, gemini, windsurf, aider, opencode
268
+ --threshold N Exit code 1 if score < N (CI gate)
269
+ --require A,B Exit code 1 if named checks fail
270
+ --out FILE Write output to file (JSON or markdown)
271
+ --plan FILE Load previously exported plan file
272
+ --only A,B Limit plan/apply to selected proposal IDs
273
+ --profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
274
+ --mcp-pack A,B Merge MCP packs into setup (e.g. context7-docs,next-devtools)
275
+ --format NAME Output format: json | sarif
276
+ --port N Port for \`serve\` (default: 3000)
277
+ --snapshot Save snapshot artifact under .claude/nerviq/snapshots/
278
+ --lite Short top-3 scan with one clear next step
279
+ --dry-run Preview changes without writing files
280
+ --verbose Show all checks (not just critical/high)
281
+ --json Output as JSON
282
+ --auto Apply all generated files without prompting
283
+ --key NAME Feedback: recommendation key (e.g. permissionDeny)
284
+ --status VALUE Feedback: accepted | rejected | deferred
285
+ --effect VALUE Feedback: positive | neutral | negative
286
+ --score-delta N Feedback: observed score delta
287
+ --help Show this help
288
+ --version Show version
289
+
290
+ EXAMPLES
278
291
  npx nerviq
279
292
  npx nerviq --lite
280
- npx nerviq --platform codex
293
+ npx nerviq --platform cursor
281
294
  npx nerviq --platform codex augment
282
- npx nerviq --platform codex suggest-only --json
283
- npx nerviq --platform codex setup
284
- npx nerviq --platform codex plan --out codex-plan.json
285
- npx nerviq --platform codex --format sarif
286
- npx nerviq --snapshot
287
- npx nerviq augment
288
- npx nerviq augment --snapshot
289
- npx nerviq suggest-only --json
290
- npx nerviq governance --snapshot
291
- npx nerviq plan --out claudex-plan.json
292
- npx nerviq plan --profile safe-write
295
+ npx nerviq scan ./app ./api ./infra
296
+ npx nerviq harmony-audit
297
+ npx nerviq convert --from claude --to codex
298
+ npx nerviq migrate --platform cursor --from v2 --to v3
293
299
  npx nerviq setup --mcp-pack context7-docs
294
- npx nerviq apply --plan claudex-plan.json --only hooks,commands
295
- npx nerviq apply --mcp-pack context7-docs,next-devtools --only hooks
296
- npx nerviq apply --profile power-user --only claude-md,hooks
297
- npx nerviq governance --json
298
- npx nerviq benchmark --out benchmark.md
299
- npx nerviq feedback
300
- npx nerviq feedback --key permissionDeny --status accepted --effect positive --score-delta 12
301
- npx nerviq --json --threshold 60
302
- npx nerviq setup --auto
303
- npx nerviq interactive
304
-
305
- Exit codes:
300
+ npx nerviq apply --plan plan.json --only hooks,commands
301
+ npx nerviq serve --port 4000
302
+ npx nerviq --json --threshold 70
303
+ npx nerviq catalog --json --out catalog.json
304
+ npx nerviq feedback --key permissionDeny --status accepted --effect positive
305
+
306
+ EXIT CODES
306
307
  0 Success
307
308
  1 Error, unknown command, or score below --threshold
308
309
  `;
@@ -345,6 +346,7 @@ async function main() {
345
346
  require: parsed.requireChecks,
346
347
  platform: parsed.platform || 'claude',
347
348
  format: parsed.format || null,
349
+ port: parsed.port !== null ? Number(parsed.port) : null,
348
350
  dir: process.cwd()
349
351
  };
350
352
 
@@ -358,6 +360,11 @@ async function main() {
358
360
  process.exit(1);
359
361
  }
360
362
 
363
+ if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
364
+ console.error('\n Error: --port must be an integer between 0 and 65535.\n');
365
+ process.exit(1);
366
+ }
367
+
361
368
  if (options.threshold !== null && (!Number.isFinite(options.threshold) || options.threshold < 0 || options.threshold > 100)) {
362
369
  console.error('\n Error: --threshold must be a number between 0 and 100.\n');
363
370
  process.exit(1);
@@ -396,7 +403,7 @@ async function main() {
396
403
  const FULL_COMMAND_SET = new Set([
397
404
  'audit', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
398
405
  'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
399
- 'history', 'compare', 'trend', 'feedback', 'catalog', 'help', 'version',
406
+ 'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
400
407
  // Harmony + Synergy (cross-platform)
401
408
  'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
402
409
  'harmony-watch', 'harmony-governance', 'synergy-report',
@@ -751,6 +758,53 @@ async function main() {
751
758
  }
752
759
  }
753
760
  process.exit(0);
761
+ } else if (normalizedCommand === 'certify') {
762
+ const { certifyProject, generateCertBadge } = require('../src/certification');
763
+ const certResult = await certifyProject(options.dir);
764
+ if (options.json) {
765
+ console.log(JSON.stringify(certResult, null, 2));
766
+ } else {
767
+ console.log('');
768
+ console.log('\x1b[1m nerviq certification\x1b[0m');
769
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
770
+ console.log('');
771
+ console.log(` Level: \x1b[1m${certResult.level}\x1b[0m`);
772
+ console.log(` Harmony Score: ${certResult.harmonyScore}/100`);
773
+ console.log('');
774
+ if (Object.keys(certResult.platformScores).length > 0) {
775
+ console.log(' Platform Scores:');
776
+ for (const [plat, score] of Object.entries(certResult.platformScores)) {
777
+ const scoreColor = score >= 70 ? '\x1b[32m' : score >= 40 ? '\x1b[33m' : '\x1b[31m';
778
+ console.log(` ${plat.padEnd(12)} ${scoreColor}${score}/100\x1b[0m`);
779
+ }
780
+ console.log('');
781
+ }
782
+ console.log(' Badge:');
783
+ console.log(` ${certResult.badge}`);
784
+ console.log('');
785
+ console.log(' Add the badge to your README.md');
786
+ console.log('');
787
+ }
788
+ process.exit(0);
789
+ } else if (normalizedCommand === 'serve') {
790
+ const server = await startServer({
791
+ port: options.port == null ? 3000 : options.port,
792
+ baseDir: options.dir,
793
+ });
794
+ const address = server.address();
795
+ const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
796
+ console.log('');
797
+ console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
798
+ console.log(' Endpoints: /api/health, /api/catalog, /api/audit, /api/harmony');
799
+ console.log('');
800
+
801
+ const closeServer = () => {
802
+ server.close(() => process.exit(0));
803
+ };
804
+
805
+ process.on('SIGINT', closeServer);
806
+ process.on('SIGTERM', closeServer);
807
+ return;
754
808
  } else if (normalizedCommand === 'doctor') {
755
809
  const { runDoctor } = require('../src/doctor');
756
810
  const output = await runDoctor({ dir: options.dir, json: options.json, verbose: options.verbose });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "0.9.5",
4
- "description": "The intelligent nervous system for AI coding agents — audit, align, and amplify every platform on every project.",
3
+ "version": "1.0.0",
4
+ "description": "The intelligent nervous system for AI coding agents — 673 checks across 8 platforms. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "nerviq": "bin/cli.js",
@@ -20,7 +20,7 @@
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
24
  "benchmark:perf": "node tools/benchmark.js",
25
25
  "catalog": "node -e \"const {generateCatalog}=require('./src/catalog');console.log(JSON.stringify(generateCatalog(),null,2))\""
26
26
  },
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);
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Certification system for Nerviq.
3
+ * Evaluates a project against all active platforms and assigns a certification level.
4
+ */
5
+
6
+ const path = require('path');
7
+ const { audit } = require('./audit');
8
+ const { harmonyAudit } = require('./harmony/audit');
9
+ const { detectPlatforms } = require('./public-api');
10
+
11
+ const LEVELS = {
12
+ GOLD: 'Nerviq Certified Gold',
13
+ SILVER: 'Nerviq Certified Silver',
14
+ BRONZE: 'Nerviq Certified Bronze',
15
+ NONE: 'Not Certified',
16
+ };
17
+
18
+ const BADGE_COLORS = {
19
+ [LEVELS.GOLD]: 'gold',
20
+ [LEVELS.SILVER]: 'silver',
21
+ [LEVELS.BRONZE]: 'cd7f32',
22
+ [LEVELS.NONE]: 'lightgrey',
23
+ };
24
+
25
+ /**
26
+ * Certify a project directory.
27
+ * Runs harmony audit and per-platform audits, then determines certification level.
28
+ *
29
+ * @param {string} dir - Project directory path
30
+ * @returns {Promise<{ level: string, harmonyScore: number, platformScores: Object, badge: string }>}
31
+ */
32
+ async function certifyProject(dir) {
33
+ const resolvedDir = path.resolve(dir || '.');
34
+
35
+ // Detect active platforms
36
+ const platforms = detectPlatforms(resolvedDir);
37
+
38
+ // Run per-platform audits
39
+ const platformScores = {};
40
+ for (const platform of platforms) {
41
+ try {
42
+ const result = await audit({ dir: resolvedDir, platform, silent: true });
43
+ platformScores[platform] = result.score;
44
+ } catch {
45
+ platformScores[platform] = 0;
46
+ }
47
+ }
48
+
49
+ // Run harmony audit
50
+ let harmonyScore = 0;
51
+ try {
52
+ const harmonyResult = await harmonyAudit({ dir: resolvedDir, silent: true });
53
+ harmonyScore = harmonyResult.harmonyScore || 0;
54
+ } catch {
55
+ harmonyScore = 0;
56
+ }
57
+
58
+ // Determine certification level
59
+ const scores = Object.values(platformScores);
60
+ const allAbove70 = scores.length > 0 && scores.every(s => s >= 70);
61
+ const allAbove50 = scores.length > 0 && scores.every(s => s >= 50);
62
+ const anyAbove40 = scores.some(s => s >= 40);
63
+
64
+ let level;
65
+ if (harmonyScore >= 80 && allAbove70) {
66
+ level = LEVELS.GOLD;
67
+ } else if (harmonyScore >= 60 && allAbove50) {
68
+ level = LEVELS.SILVER;
69
+ } else if (anyAbove40) {
70
+ level = LEVELS.BRONZE;
71
+ } else {
72
+ level = LEVELS.NONE;
73
+ }
74
+
75
+ const badge = generateCertBadge(level);
76
+
77
+ return {
78
+ level,
79
+ harmonyScore,
80
+ platformScores,
81
+ platforms,
82
+ badge,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Generate a shields.io badge markdown string for a certification level.
88
+ *
89
+ * @param {string} level - One of the LEVELS values
90
+ * @returns {string} Markdown badge string
91
+ */
92
+ function generateCertBadge(level) {
93
+ const color = BADGE_COLORS[level] || 'lightgrey';
94
+ const label = encodeURIComponent('Nerviq');
95
+ const message = encodeURIComponent(level);
96
+ const url = `https://img.shields.io/badge/${label}-${message}-${color}`;
97
+ return `[![${level}](${url})](https://nerviq.net)`;
98
+ }
99
+
100
+ module.exports = { certifyProject, generateCertBadge, LEVELS };
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
+ };