@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 +17 -0
- package/README.md +154 -14
- package/bin/cli.js +154 -100
- package/package.json +3 -3
- package/src/audit.js +8 -1
- package/src/certification.js +100 -0
- package/src/index.js +7 -0
- package/src/plugins.js +110 -0
- package/src/public-api.js +173 -0
- package/src/server.js +123 -0
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
|
[](https://www.npmjs.com/package/@nerviq/cli)
|
|
6
6
|
[](LICENSE)
|
|
7
|
+
[](https://github.com/nerviq/nerviq)
|
|
7
8
|
|
|
8
9
|
---
|
|
9
10
|
|
|
10
|
-
###
|
|
11
|
+
### 8 Platforms Supported
|
|
11
12
|
|
|
12
|
-
Nerviq
|
|
13
|
+
Nerviq v1.0 ships with full audit, setup, governance, and benchmark support for **8 AI coding platforms**:
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
--
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
--
|
|
260
|
-
--
|
|
261
|
-
--
|
|
262
|
-
--
|
|
263
|
-
--
|
|
264
|
-
--
|
|
265
|
-
--
|
|
266
|
-
--
|
|
267
|
-
--
|
|
268
|
-
--
|
|
269
|
-
--
|
|
270
|
-
--
|
|
271
|
-
--
|
|
272
|
-
--
|
|
273
|
-
--
|
|
274
|
-
--
|
|
275
|
-
--
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
293
|
+
npx nerviq --platform cursor
|
|
281
294
|
npx nerviq --platform codex augment
|
|
282
|
-
npx nerviq
|
|
283
|
-
npx nerviq
|
|
284
|
-
npx nerviq --
|
|
285
|
-
npx nerviq --platform
|
|
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
|
|
295
|
-
npx nerviq
|
|
296
|
-
npx nerviq
|
|
297
|
-
npx nerviq
|
|
298
|
-
npx nerviq
|
|
299
|
-
|
|
300
|
-
|
|
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.
|
|
4
|
-
"description": "The intelligent nervous system for AI coding agents —
|
|
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(
|
|
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 `[](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
|
+
};
|