@nerviq/cli 1.20.1 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +23 -23
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/src/activity.js +1039 -1039
  5. package/src/adoption-advisor.js +299 -299
  6. package/src/aider/config-parser.js +166 -166
  7. package/src/aider/context.js +4 -1
  8. package/src/aider/deep-review.js +316 -316
  9. package/src/aider/domain-packs.js +303 -303
  10. package/src/aider/freshness.js +93 -93
  11. package/src/aider/governance.js +253 -253
  12. package/src/aider/interactive.js +334 -334
  13. package/src/aider/mcp-packs.js +329 -329
  14. package/src/aider/patch.js +214 -214
  15. package/src/aider/plans.js +186 -186
  16. package/src/aider/premium.js +360 -360
  17. package/src/aider/setup.js +404 -404
  18. package/src/aider/techniques.js +312 -67
  19. package/src/analyze.js +951 -951
  20. package/src/anti-patterns.js +485 -485
  21. package/src/audit/instruction-files.js +180 -180
  22. package/src/audit/recommendations.js +577 -577
  23. package/src/auto-suggest.js +154 -154
  24. package/src/badge.js +13 -13
  25. package/src/behavioral-drift.js +801 -801
  26. package/src/benchmark.js +67 -67
  27. package/src/catalog.js +103 -103
  28. package/src/certification.js +128 -128
  29. package/src/codex/config-parser.js +183 -183
  30. package/src/codex/context.js +223 -223
  31. package/src/codex/deep-review.js +493 -493
  32. package/src/codex/domain-packs.js +394 -394
  33. package/src/codex/freshness.js +84 -84
  34. package/src/codex/governance.js +192 -192
  35. package/src/codex/interactive.js +618 -618
  36. package/src/codex/mcp-packs.js +914 -914
  37. package/src/codex/patch.js +209 -209
  38. package/src/codex/plans.js +251 -251
  39. package/src/codex/premium.js +614 -614
  40. package/src/codex/setup.js +591 -591
  41. package/src/continuous-ops.js +681 -681
  42. package/src/copilot/activity.js +309 -309
  43. package/src/copilot/deep-review.js +346 -346
  44. package/src/copilot/domain-packs.js +372 -372
  45. package/src/copilot/freshness.js +57 -57
  46. package/src/copilot/governance.js +222 -222
  47. package/src/copilot/interactive.js +406 -406
  48. package/src/copilot/mcp-packs.js +826 -826
  49. package/src/copilot/plans.js +253 -253
  50. package/src/copilot/premium.js +451 -451
  51. package/src/copilot/setup.js +488 -488
  52. package/src/cost-tracking.js +61 -61
  53. package/src/cursor/activity.js +301 -301
  54. package/src/cursor/config-parser.js +265 -265
  55. package/src/cursor/context.js +256 -256
  56. package/src/cursor/deep-review.js +334 -334
  57. package/src/cursor/domain-packs.js +368 -368
  58. package/src/cursor/freshness.js +65 -65
  59. package/src/cursor/governance.js +229 -229
  60. package/src/cursor/interactive.js +391 -391
  61. package/src/cursor/mcp-packs.js +828 -828
  62. package/src/cursor/plans.js +254 -254
  63. package/src/cursor/premium.js +469 -469
  64. package/src/cursor/setup.js +488 -488
  65. package/src/dashboard.js +493 -493
  66. package/src/deep-review.js +428 -428
  67. package/src/deprecation.js +98 -98
  68. package/src/diff-only.js +280 -280
  69. package/src/doctor.js +119 -119
  70. package/src/domain-pack-expansion.js +1033 -1033
  71. package/src/domain-packs.js +387 -387
  72. package/src/feedback.js +178 -178
  73. package/src/fix-engine.js +783 -783
  74. package/src/fix-prompts.js +122 -122
  75. package/src/formatters/sarif.js +115 -115
  76. package/src/freshness.js +74 -74
  77. package/src/gemini/config-parser.js +275 -275
  78. package/src/gemini/deep-review.js +559 -559
  79. package/src/gemini/domain-packs.js +393 -393
  80. package/src/gemini/freshness.js +66 -66
  81. package/src/gemini/governance.js +201 -201
  82. package/src/gemini/interactive.js +860 -860
  83. package/src/gemini/mcp-packs.js +915 -915
  84. package/src/gemini/plans.js +269 -269
  85. package/src/gemini/premium.js +760 -760
  86. package/src/gemini/setup.js +692 -692
  87. package/src/governance.js +72 -72
  88. package/src/harmony/add.js +68 -68
  89. package/src/harmony/advisor.js +333 -333
  90. package/src/harmony/canon.js +565 -565
  91. package/src/harmony/cli.js +591 -591
  92. package/src/harmony/drift.js +401 -401
  93. package/src/harmony/governance.js +313 -313
  94. package/src/harmony/memory.js +239 -239
  95. package/src/harmony/sync.js +475 -475
  96. package/src/harmony/watch.js +370 -370
  97. package/src/hook-validation.js +342 -342
  98. package/src/index.js +271 -271
  99. package/src/init.js +184 -184
  100. package/src/instruction-surfaces.js +185 -185
  101. package/src/integrations.js +144 -144
  102. package/src/interactive.js +118 -118
  103. package/src/locales/en.json +1 -1
  104. package/src/locales/es.json +1 -1
  105. package/src/mcp-packs.js +830 -830
  106. package/src/mcp-server.js +726 -726
  107. package/src/mcp-validation.js +337 -337
  108. package/src/nerviq-sync.json +7 -7
  109. package/src/opencode/config-parser.js +109 -109
  110. package/src/opencode/context.js +247 -247
  111. package/src/opencode/deep-review.js +313 -313
  112. package/src/opencode/domain-packs.js +262 -262
  113. package/src/opencode/freshness.js +66 -66
  114. package/src/opencode/governance.js +159 -159
  115. package/src/opencode/interactive.js +392 -392
  116. package/src/opencode/mcp-packs.js +705 -705
  117. package/src/opencode/patch.js +184 -184
  118. package/src/opencode/plans.js +231 -231
  119. package/src/opencode/premium.js +413 -413
  120. package/src/opencode/setup.js +449 -449
  121. package/src/opencode/techniques.js +27 -27
  122. package/src/operating-profile.js +574 -574
  123. package/src/org.js +152 -152
  124. package/src/permission-rules.js +218 -218
  125. package/src/plans.js +839 -839
  126. package/src/platform-change-manifest.js +86 -86
  127. package/src/plugins.js +110 -110
  128. package/src/policy-layers.js +210 -210
  129. package/src/profiles.js +124 -124
  130. package/src/prompt-injection.js +74 -74
  131. package/src/public-api.js +173 -173
  132. package/src/recommendation-rules.js +84 -84
  133. package/src/repo-archetype.js +386 -386
  134. package/src/secret-patterns.js +39 -39
  135. package/src/server.js +527 -527
  136. package/src/setup/analysis.js +607 -607
  137. package/src/setup/runtime.js +172 -172
  138. package/src/setup.js +677 -677
  139. package/src/shared/capabilities.js +194 -194
  140. package/src/source-urls.js +132 -132
  141. package/src/stack-checks.js +565 -565
  142. package/src/supplemental-checks.js +13 -13
  143. package/src/synergy/adaptive.js +261 -261
  144. package/src/synergy/compensation.js +137 -137
  145. package/src/synergy/evidence.js +193 -193
  146. package/src/synergy/learning.js +199 -199
  147. package/src/synergy/patterns.js +227 -227
  148. package/src/synergy/ranking.js +83 -83
  149. package/src/synergy/report.js +165 -165
  150. package/src/synergy/routing.js +146 -146
  151. package/src/techniques/api.js +407 -407
  152. package/src/techniques/automation.js +316 -316
  153. package/src/techniques/compliance.js +257 -257
  154. package/src/techniques/hygiene.js +294 -294
  155. package/src/techniques/instructions.js +243 -243
  156. package/src/techniques/observability.js +226 -226
  157. package/src/techniques/optimization.js +142 -142
  158. package/src/techniques/quality.js +318 -318
  159. package/src/techniques/security.js +237 -237
  160. package/src/techniques/shared.js +443 -443
  161. package/src/techniques/stacks.js +2294 -2294
  162. package/src/techniques/tools.js +106 -106
  163. package/src/techniques/workflow.js +413 -413
  164. package/src/techniques.js +81 -81
  165. package/src/terminology.js +73 -73
  166. package/src/token-estimate.js +35 -35
  167. package/src/usage-patterns.js +99 -99
  168. package/src/verification-metadata.js +145 -145
  169. package/src/watch.js +247 -247
  170. package/src/windsurf/activity.js +302 -302
  171. package/src/windsurf/config-parser.js +267 -267
  172. package/src/windsurf/deep-review.js +337 -337
  173. package/src/windsurf/domain-packs.js +370 -370
  174. package/src/windsurf/freshness.js +36 -36
  175. package/src/windsurf/governance.js +231 -231
  176. package/src/windsurf/interactive.js +388 -388
  177. package/src/windsurf/mcp-packs.js +792 -792
  178. package/src/windsurf/plans.js +247 -247
  179. package/src/windsurf/premium.js +468 -468
  180. package/src/windsurf/setup.js +471 -471
  181. package/src/workspace.js +375 -375
package/src/mcp-server.js CHANGED
@@ -1,726 +1,726 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Nerviq MCP Server
4
- *
5
- * Exposes Nerviq capabilities as an MCP (Model Context Protocol) server
6
- * using stdio transport (stdin/stdout JSON-RPC 2.0).
7
- *
8
- * Tools:
9
- * nerviq_audit — run audit for any platform, return JSON results
10
- * nerviq_harmony — run harmony-audit, return cross-platform scores
11
- * nerviq_setup — run setup for a platform, return written files list
12
- * nerviq_drift — detect configuration drift between platforms
13
- * nerviq_check_score — quick score check with threshold gate
14
- * nerviq_get_config — read current platform config (files, settings, trust)
15
- * nerviq_apply_fix — apply a governance fix by check key
16
- *
17
- * Usage:
18
- * node src/mcp-server.js
19
- * (or via nerviq-mcp binary)
20
- *
21
- * Register in an MCP host config:
22
- * {
23
- * "mcpServers": {
24
- * "nerviq": {
25
- * "command": "npx",
26
- * "args": ["nerviq-mcp"],
27
- * "env": {}
28
- * }
29
- * }
30
- * }
31
- */
32
-
33
- 'use strict';
34
-
35
- const { version } = require('../package.json');
36
-
37
- function buildMcpAuditPayload(result, options = {}) {
38
- const verbose = Boolean(options.verbose);
39
- const normalizedCheckCount = typeof result.checkCount === 'number'
40
- ? result.checkCount
41
- : typeof result.total === 'number'
42
- ? result.total
43
- : 0;
44
-
45
- const payload = {
46
- platform: result.platform,
47
- score: result.score,
48
- passed: result.passed,
49
- failed: result.failed,
50
- total: normalizedCheckCount,
51
- checkCount: normalizedCheckCount,
52
- scoreType: result.scoreType || 'live-audit-score',
53
- grade: result.score >= 80 ? 'A' : result.score >= 60 ? 'B' : result.score >= 40 ? 'C' : 'D',
54
- criticalFailures: (result.results || [])
55
- .filter(r => r.passed === false && r.impact === 'critical')
56
- .map(r => ({ key: r.key, id: r.id, name: r.name, fix: r.fix })),
57
- highFailures: (result.results || [])
58
- .filter(r => r.passed === false && r.impact === 'high')
59
- .map(r => ({ key: r.key, id: r.id, name: r.name, fix: r.fix })),
60
- topNextActions: (result.topNextActions || []).slice(0, 3).map((item) => ({
61
- key: item.key,
62
- name: item.name,
63
- impact: item.impact,
64
- fix: item.fix,
65
- })),
66
- suggestedNextCommand: result.suggestedNextCommand || null,
67
- };
68
-
69
- if (verbose) {
70
- payload.results = (result.results || []).map(r => ({
71
- key: r.key,
72
- id: r.id,
73
- name: r.name,
74
- passed: r.passed,
75
- impact: r.impact,
76
- fix: r.passed === false ? r.fix : undefined,
77
- }));
78
- }
79
-
80
- return payload;
81
- }
82
-
83
- function buildMcpHarmonyPayload(result, options = {}) {
84
- const verbose = Boolean(options.verbose);
85
- const payload = {
86
- harmonyScore: result.harmonyScore,
87
- activePlatforms: result.activePlatforms || [],
88
- platformScores: result.platformScores || {},
89
- driftCount: result.driftCount || (result.drifts || []).length || 0,
90
- criticalDrifts: (result.drifts || [])
91
- .filter(d => d.severity === 'critical')
92
- .map(d => ({ type: d.type, description: d.description, recommendation: d.recommendation })),
93
- recommendations: (result.recommendations || []).slice(0, 5),
94
- };
95
-
96
- if (verbose) {
97
- payload.allDrifts = (result.drifts || []).map(d => ({
98
- type: d.type,
99
- severity: d.severity,
100
- description: d.description,
101
- recommendation: d.recommendation,
102
- }));
103
- }
104
-
105
- return payload;
106
- }
107
-
108
- // ─── Tool definitions ────────────────────────────────────────────────────────
109
-
110
- const TOOLS = [
111
- {
112
- name: 'nerviq_audit',
113
- description: 'Run a Nerviq audit on a project directory for a given platform. Returns JSON with score, passed/failed checks, and recommendations.',
114
- inputSchema: {
115
- type: 'object',
116
- properties: {
117
- dir: {
118
- type: 'string',
119
- description: 'Absolute path to the project directory to audit. Defaults to current working directory.',
120
- },
121
- platform: {
122
- type: 'string',
123
- description: 'Platform to audit. One of: claude, codex, cursor, copilot, gemini, windsurf, aider, opencode.',
124
- enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
125
- default: 'claude',
126
- },
127
- verbose: {
128
- type: 'boolean',
129
- description: 'Include all checks in output, not just failures.',
130
- default: false,
131
- },
132
- },
133
- required: [],
134
- },
135
- },
136
- {
137
- name: 'nerviq_harmony',
138
- description: 'Run a harmony audit across all detected AI platforms in a project. Returns cross-platform alignment scores, drift analysis, and recommendations.',
139
- inputSchema: {
140
- type: 'object',
141
- properties: {
142
- dir: {
143
- type: 'string',
144
- description: 'Absolute path to the project directory. Defaults to current working directory.',
145
- },
146
- verbose: {
147
- type: 'boolean',
148
- description: 'Include detailed per-platform results.',
149
- default: false,
150
- },
151
- },
152
- required: [],
153
- },
154
- },
155
- {
156
- name: 'nerviq_setup',
157
- description: 'Generate and write baseline configuration files for a platform in a project directory. Returns the list of files written.',
158
- inputSchema: {
159
- type: 'object',
160
- properties: {
161
- dir: {
162
- type: 'string',
163
- description: 'Absolute path to the project directory. Defaults to current working directory.',
164
- },
165
- platform: {
166
- type: 'string',
167
- description: 'Platform to set up. One of: claude, codex, cursor, copilot, gemini, windsurf, aider, opencode.',
168
- enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
169
- default: 'claude',
170
- },
171
- dryRun: {
172
- type: 'boolean',
173
- description: 'Preview files that would be written without actually writing them.',
174
- default: false,
175
- },
176
- },
177
- required: [],
178
- },
179
- },
180
- {
181
- name: 'nerviq_drift',
182
- description: 'Detect configuration drift between AI platforms in a project. Returns drift items with severity, type, and recommendations.',
183
- inputSchema: {
184
- type: 'object',
185
- properties: {
186
- dir: {
187
- type: 'string',
188
- description: 'Absolute path to the project directory. Defaults to current working directory.',
189
- },
190
- platforms: {
191
- type: 'array',
192
- items: { type: 'string' },
193
- description: 'Specific platforms to compare. Defaults to all detected platforms.',
194
- },
195
- minSeverity: {
196
- type: 'string',
197
- description: 'Minimum severity to include. One of: critical, high, medium, low.',
198
- enum: ['critical', 'high', 'medium', 'low'],
199
- default: 'low',
200
- },
201
- },
202
- required: [],
203
- },
204
- },
205
- {
206
- name: 'nerviq_check_score',
207
- description: 'Quick score check for a project — returns score, grade, and whether it meets a threshold. Lighter than a full audit.',
208
- inputSchema: {
209
- type: 'object',
210
- properties: {
211
- dir: {
212
- type: 'string',
213
- description: 'Absolute path to the project directory. Defaults to current working directory.',
214
- },
215
- platform: {
216
- type: 'string',
217
- description: 'Platform to check. Defaults to claude.',
218
- enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
219
- default: 'claude',
220
- },
221
- threshold: {
222
- type: 'number',
223
- description: 'Minimum passing score (0-100). Returns pass/fail against this threshold.',
224
- default: 0,
225
- },
226
- },
227
- required: [],
228
- },
229
- },
230
- {
231
- name: 'nerviq_get_config',
232
- description: 'Read the current AI agent configuration for a platform in a project. Returns instruction files, settings, MCP servers, hooks, and trust posture.',
233
- inputSchema: {
234
- type: 'object',
235
- properties: {
236
- dir: {
237
- type: 'string',
238
- description: 'Absolute path to the project directory. Defaults to current working directory.',
239
- },
240
- platform: {
241
- type: 'string',
242
- description: 'Platform to inspect. Defaults to claude.',
243
- enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
244
- default: 'claude',
245
- },
246
- },
247
- required: [],
248
- },
249
- },
250
- {
251
- name: 'nerviq_apply_fix',
252
- description: 'Apply a specific governance fix by check key. Runs setup/plan for the targeted check and returns what was changed.',
253
- inputSchema: {
254
- type: 'object',
255
- properties: {
256
- dir: {
257
- type: 'string',
258
- description: 'Absolute path to the project directory. Defaults to current working directory.',
259
- },
260
- platform: {
261
- type: 'string',
262
- description: 'Platform to fix. Defaults to claude.',
263
- enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
264
- default: 'claude',
265
- },
266
- checkKey: {
267
- type: 'string',
268
- description: 'The check key to fix (e.g. "claudeMd", "settingsPermissions", "hookExists"). Get available keys from nerviq_audit results.',
269
- },
270
- dryRun: {
271
- type: 'boolean',
272
- description: 'Preview the fix without applying it.',
273
- default: false,
274
- },
275
- },
276
- required: ['checkKey'],
277
- },
278
- },
279
- ];
280
-
281
- // ─── Tool handlers ───────────────────────────────────────────────────────────
282
-
283
- async function handleAudit(input) {
284
- const { audit } = require('./audit');
285
- const dir = input.dir || process.cwd();
286
- const platform = input.platform || 'claude';
287
- const verbose = Boolean(input.verbose);
288
-
289
- const result = await audit({ dir, platform, silent: true, verbose });
290
- const clean = buildMcpAuditPayload(result, { verbose });
291
- return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
292
- }
293
-
294
- async function handleHarmony(input) {
295
- const { harmonyAudit } = require('./harmony/audit');
296
- const dir = input.dir || process.cwd();
297
- const verbose = Boolean(input.verbose);
298
-
299
- const result = await harmonyAudit({ dir, silent: true });
300
- const clean = buildMcpHarmonyPayload(result, { verbose });
301
- return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
302
- }
303
-
304
- async function handleSetup(input) {
305
- const { setup } = require('./setup');
306
- const dir = input.dir || process.cwd();
307
- const platform = input.platform || 'claude';
308
- const dryRun = Boolean(input.dryRun);
309
-
310
- const result = await setup({ dir, platform, silent: true, dryRun });
311
-
312
- const clean = {
313
- platform,
314
- dryRun,
315
- writtenFiles: result.writtenFiles || [],
316
- skippedFiles: result.skippedFiles || [],
317
- message: dryRun
318
- ? `Dry run: would write ${(result.writtenFiles || []).length} file(s)`
319
- : `Setup complete: wrote ${(result.writtenFiles || []).length} file(s)`,
320
- };
321
-
322
- return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
323
- }
324
-
325
- async function handleDrift(input) {
326
- const { detectDrift } = require('./harmony/drift');
327
- const { buildCanonicalModel, detectActivePlatforms } = require('./harmony/canon');
328
- const dir = input.dir || process.cwd();
329
- const minSeverity = input.minSeverity || 'low';
330
-
331
- const SEVERITY_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
332
- const minLevel = SEVERITY_ORDER[minSeverity] || 0;
333
-
334
- const canonModel = buildCanonicalModel(dir);
335
- const activePlatforms = input.platforms && input.platforms.length > 0
336
- ? input.platforms
337
- : detectActivePlatforms(canonModel);
338
-
339
- const driftResult = detectDrift(canonModel, activePlatforms, { verbose: true });
340
-
341
- const filteredDrifts = (driftResult.drifts || [])
342
- .filter(d => (SEVERITY_ORDER[d.severity] || 0) >= minLevel);
343
-
344
- const clean = {
345
- dir,
346
- activePlatforms,
347
- totalDrifts: driftResult.drifts ? driftResult.drifts.length : 0,
348
- filteredDrifts: filteredDrifts.length,
349
- minSeverity,
350
- drifts: filteredDrifts.map(d => ({
351
- type: d.type,
352
- severity: d.severity,
353
- description: d.description,
354
- recommendation: d.recommendation || null,
355
- platforms: d.platforms || null,
356
- })),
357
- summary: driftResult.summary || null,
358
- };
359
-
360
- return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
361
- }
362
-
363
- async function handleCheckScore(input) {
364
- const { audit } = require('./audit');
365
- const dir = input.dir || process.cwd();
366
- const platform = input.platform || 'claude';
367
- const threshold = input.threshold || 0;
368
-
369
- const result = await audit({ dir, platform, silent: true });
370
- const score = result.score;
371
- const grade = score >= 80 ? 'A' : score >= 60 ? 'B' : score >= 40 ? 'C' : 'D';
372
- const pass = threshold > 0 ? score >= threshold : true;
373
-
374
- const clean = {
375
- score,
376
- grade,
377
- platform,
378
- passed: result.passed,
379
- failed: result.failed,
380
- threshold: threshold || null,
381
- pass,
382
- remediation_command: score < 70 ? `npx @nerviq/cli augment --platform ${platform}` : null,
383
- };
384
-
385
- return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
386
- }
387
-
388
- async function handleGetConfig(input) {
389
- const fs = require('fs');
390
- const path = require('path');
391
- const dir = input.dir || process.cwd();
392
- const platform = input.platform || 'claude';
393
-
394
- const config = { platform, dir, files: {} };
395
-
396
- // Platform-specific config file mappings
397
- const FILE_MAP = {
398
- claude: {
399
- instructions: ['CLAUDE.md', '.claude/CLAUDE.md'],
400
- settings: ['.claude/settings.json', '.claude/settings.local.json'],
401
- rules: '.claude/rules',
402
- hooks: '.claude/hooks',
403
- },
404
- codex: {
405
- instructions: ['AGENTS.md'],
406
- settings: ['.codex/config.toml'],
407
- rules: null,
408
- hooks: null,
409
- },
410
- gemini: {
411
- instructions: ['GEMINI.md'],
412
- settings: ['.gemini/settings.json'],
413
- rules: null,
414
- hooks: null,
415
- },
416
- copilot: {
417
- instructions: ['.github/copilot-instructions.md'],
418
- settings: [],
419
- rules: null,
420
- hooks: null,
421
- },
422
- cursor: {
423
- instructions: ['.cursorrules'],
424
- settings: [],
425
- rules: '.cursor/rules',
426
- hooks: null,
427
- },
428
- windsurf: {
429
- instructions: ['.windsurfrules'],
430
- settings: [],
431
- rules: '.windsurf/rules',
432
- hooks: null,
433
- },
434
- aider: {
435
- instructions: ['.aider.conf.yml'],
436
- settings: ['.aiderignore'],
437
- rules: null,
438
- hooks: null,
439
- },
440
- opencode: {
441
- instructions: ['opencode.json'],
442
- settings: ['.opencode'],
443
- rules: null,
444
- hooks: null,
445
- },
446
- };
447
-
448
- const mapping = FILE_MAP[platform] || FILE_MAP.claude;
449
-
450
- // Read instruction files
451
- for (const f of mapping.instructions) {
452
- const fullPath = path.join(dir, f);
453
- if (fs.existsSync(fullPath)) {
454
- try {
455
- const content = fs.readFileSync(fullPath, 'utf8');
456
- config.files[f] = { exists: true, size: content.length, lines: content.split('\n').length };
457
- } catch { config.files[f] = { exists: true, error: 'unreadable' }; }
458
- } else {
459
- config.files[f] = { exists: false };
460
- }
461
- }
462
-
463
- // Read settings files
464
- for (const f of mapping.settings) {
465
- const fullPath = path.join(dir, f);
466
- if (fs.existsSync(fullPath)) {
467
- try {
468
- const content = fs.readFileSync(fullPath, 'utf8');
469
- if (f.endsWith('.json')) {
470
- config.files[f] = { exists: true, parsed: JSON.parse(content) };
471
- } else {
472
- config.files[f] = { exists: true, size: content.length };
473
- }
474
- } catch { config.files[f] = { exists: true, error: 'parse-failed' }; }
475
- } else {
476
- config.files[f] = { exists: false };
477
- }
478
- }
479
-
480
- // Check rules dir
481
- if (mapping.rules) {
482
- const rulesPath = path.join(dir, mapping.rules);
483
- if (fs.existsSync(rulesPath)) {
484
- try {
485
- const entries = fs.readdirSync(rulesPath);
486
- config.files[mapping.rules] = { exists: true, count: entries.length, entries };
487
- } catch { config.files[mapping.rules] = { exists: true, error: 'unreadable' }; }
488
- } else {
489
- config.files[mapping.rules] = { exists: false };
490
- }
491
- }
492
-
493
- // Check hooks dir
494
- if (mapping.hooks) {
495
- const hooksPath = path.join(dir, mapping.hooks);
496
- if (fs.existsSync(hooksPath)) {
497
- try {
498
- const entries = fs.readdirSync(hooksPath);
499
- config.files[mapping.hooks] = { exists: true, count: entries.length, entries };
500
- } catch { config.files[mapping.hooks] = { exists: true, error: 'unreadable' }; }
501
- } else {
502
- config.files[mapping.hooks] = { exists: false };
503
- }
504
- }
505
-
506
- // Trust posture summary
507
- if (platform === 'claude' && config.files['.claude/settings.json'] && config.files['.claude/settings.json'].parsed) {
508
- const settings = config.files['.claude/settings.json'].parsed;
509
- config.trustPosture = {
510
- allowedTools: settings.permissions?.allow || [],
511
- deniedPatterns: settings.permissions?.deny || [],
512
- hasExplicitPermissions: !!(settings.permissions),
513
- };
514
- }
515
-
516
- return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }] };
517
- }
518
-
519
- async function handleApplyFix(input) {
520
- const { audit } = require('./audit');
521
- const { setup } = require('./setup');
522
- const dir = input.dir || process.cwd();
523
- const platform = input.platform || 'claude';
524
- const checkKey = input.checkKey;
525
- const dryRun = Boolean(input.dryRun);
526
-
527
- // First, verify the check actually fails
528
- const preAudit = await audit({ dir, platform, silent: true });
529
- const targetCheck = (preAudit.results || []).find(r => r.key === checkKey);
530
-
531
- if (!targetCheck) {
532
- return { content: [{ type: 'text', text: JSON.stringify({
533
- status: 'error',
534
- message: `Check key "${checkKey}" not found in ${platform} audit. Use nerviq_audit to see available check keys.`,
535
- }, null, 2) }] };
536
- }
537
-
538
- if (targetCheck.passed) {
539
- return { content: [{ type: 'text', text: JSON.stringify({
540
- status: 'already_passing',
541
- checkKey,
542
- message: `Check "${checkKey}" is already passing. No fix needed.`,
543
- }, null, 2) }] };
544
- }
545
-
546
- // Map check categories to setup --only targets
547
- const CATEGORY_TO_ONLY = {
548
- memory: 'instructions',
549
- security: 'permissions',
550
- automation: 'hooks',
551
- workflow: 'commands',
552
- tools: 'mcp',
553
- quality: 'instructions',
554
- git: 'instructions',
555
- };
556
-
557
- const only = CATEGORY_TO_ONLY[targetCheck.category] || null;
558
-
559
- if (dryRun) {
560
- return { content: [{ type: 'text', text: JSON.stringify({
561
- status: 'dry_run',
562
- checkKey,
563
- category: targetCheck.category,
564
- fix: targetCheck.fix,
565
- would_run: `npx @nerviq/cli setup --platform ${platform}${only ? ` --only ${only}` : ''}`,
566
- }, null, 2) }] };
567
- }
568
-
569
- // Apply the fix
570
- const setupResult = await setup({
571
- dir,
572
- platform,
573
- silent: true,
574
- only: only ? [only] : undefined,
575
- });
576
-
577
- // Re-audit to check if fix worked
578
- const postAudit = await audit({ dir, platform, silent: true });
579
- const postCheck = (postAudit.results || []).find(r => r.key === checkKey);
580
-
581
- return { content: [{ type: 'text', text: JSON.stringify({
582
- status: postCheck && postCheck.passed ? 'fixed' : 'attempted',
583
- checkKey,
584
- category: targetCheck.category,
585
- fix_description: targetCheck.fix,
586
- files_written: setupResult.writtenFiles || [],
587
- score_before: preAudit.score,
588
- score_after: postAudit.score,
589
- check_now_passing: postCheck ? postCheck.passed : false,
590
- rollback_id: setupResult.rollbackId || null,
591
- }, null, 2) }] };
592
- }
593
-
594
- // ─── JSON-RPC 2.0 / MCP stdio transport ─────────────────────────────────────
595
-
596
- function sendResponse(id, result) {
597
- const msg = JSON.stringify({ jsonrpc: '2.0', id, result });
598
- process.stdout.write(msg + '\n');
599
- }
600
-
601
- function sendError(id, code, message, data) {
602
- const msg = JSON.stringify({
603
- jsonrpc: '2.0',
604
- id: id !== undefined ? id : null,
605
- error: { code, message, ...(data ? { data } : {}) },
606
- });
607
- process.stdout.write(msg + '\n');
608
- }
609
-
610
- async function handleRequest(req) {
611
- const { id, method, params } = req;
612
-
613
- if (method === 'initialize') {
614
- return sendResponse(id, {
615
- protocolVersion: '2024-11-05',
616
- capabilities: { tools: {} },
617
- serverInfo: { name: 'nerviq', version },
618
- });
619
- }
620
-
621
- if (method === 'tools/list') {
622
- return sendResponse(id, { tools: TOOLS });
623
- }
624
-
625
- if (method === 'tools/call') {
626
- const toolName = params && params.name;
627
- const toolInput = (params && params.arguments) || {};
628
-
629
- try {
630
- let result;
631
- if (toolName === 'nerviq_audit') {
632
- result = await handleAudit(toolInput);
633
- } else if (toolName === 'nerviq_harmony') {
634
- result = await handleHarmony(toolInput);
635
- } else if (toolName === 'nerviq_setup') {
636
- result = await handleSetup(toolInput);
637
- } else if (toolName === 'nerviq_drift') {
638
- result = await handleDrift(toolInput);
639
- } else if (toolName === 'nerviq_check_score') {
640
- result = await handleCheckScore(toolInput);
641
- } else if (toolName === 'nerviq_get_config') {
642
- result = await handleGetConfig(toolInput);
643
- } else if (toolName === 'nerviq_apply_fix') {
644
- result = await handleApplyFix(toolInput);
645
- } else {
646
- return sendError(id, -32601, `Unknown tool: ${toolName}`);
647
- }
648
- return sendResponse(id, result);
649
- } catch (err) {
650
- return sendError(id, -32000, err.message, { stack: err.stack });
651
- }
652
- }
653
-
654
- if (method === 'notifications/initialized' || method === 'ping') {
655
- // No response needed for notifications; ack ping
656
- if (method === 'ping') sendResponse(id, {});
657
- return;
658
- }
659
-
660
- sendError(id, -32601, `Method not found: ${method}`);
661
- }
662
-
663
- // ─── Main loop ───────────────────────────────────────────────────────────────
664
-
665
- function main() {
666
- let buffer = '';
667
-
668
- process.stdin.setEncoding('utf8');
669
- process.stdin.on('data', (chunk) => {
670
- buffer += chunk;
671
- const lines = buffer.split('\n');
672
- buffer = lines.pop(); // keep incomplete line
673
-
674
- for (const line of lines) {
675
- const trimmed = line.trim();
676
- if (!trimmed) continue;
677
-
678
- let req;
679
- try {
680
- req = JSON.parse(trimmed);
681
- } catch {
682
- sendError(null, -32700, 'Parse error', { raw: trimmed.slice(0, 200) });
683
- continue;
684
- }
685
-
686
- handleRequest(req).catch((err) => {
687
- sendError(req.id, -32000, 'Internal error', { message: err.message });
688
- });
689
- }
690
- });
691
-
692
- process.stdin.on('end', () => {
693
- // Flush remaining buffer
694
- if (buffer.trim()) {
695
- let req;
696
- try {
697
- req = JSON.parse(buffer.trim());
698
- handleRequest(req).catch(() => {});
699
- } catch {}
700
- }
701
- process.exit(0);
702
- });
703
-
704
- // Suppress unhandled rejection crashes in MCP server context
705
- process.on('unhandledRejection', (err) => {
706
- process.stderr.write(`[nerviq-mcp] Unhandled rejection: ${err && err.message}\n`);
707
- });
708
- }
709
-
710
- if (require.main === module) {
711
- main();
712
- }
713
-
714
- module.exports = {
715
- TOOLS,
716
- buildMcpAuditPayload,
717
- buildMcpHarmonyPayload,
718
- handleAudit,
719
- handleHarmony,
720
- handleSetup,
721
- handleDrift,
722
- handleCheckScore,
723
- handleGetConfig,
724
- handleApplyFix,
725
- main,
726
- };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Nerviq MCP Server
4
+ *
5
+ * Exposes Nerviq capabilities as an MCP (Model Context Protocol) server
6
+ * using stdio transport (stdin/stdout JSON-RPC 2.0).
7
+ *
8
+ * Tools:
9
+ * nerviq_audit — run audit for any platform, return JSON results
10
+ * nerviq_harmony — run harmony-audit, return cross-platform scores
11
+ * nerviq_setup — run setup for a platform, return written files list
12
+ * nerviq_drift — detect configuration drift between platforms
13
+ * nerviq_check_score — quick score check with threshold gate
14
+ * nerviq_get_config — read current platform config (files, settings, trust)
15
+ * nerviq_apply_fix — apply a governance fix by check key
16
+ *
17
+ * Usage:
18
+ * node src/mcp-server.js
19
+ * (or via nerviq-mcp binary)
20
+ *
21
+ * Register in an MCP host config:
22
+ * {
23
+ * "mcpServers": {
24
+ * "nerviq": {
25
+ * "command": "npx",
26
+ * "args": ["nerviq-mcp"],
27
+ * "env": {}
28
+ * }
29
+ * }
30
+ * }
31
+ */
32
+
33
+ 'use strict';
34
+
35
+ const { version } = require('../package.json');
36
+
37
+ function buildMcpAuditPayload(result, options = {}) {
38
+ const verbose = Boolean(options.verbose);
39
+ const normalizedCheckCount = typeof result.checkCount === 'number'
40
+ ? result.checkCount
41
+ : typeof result.total === 'number'
42
+ ? result.total
43
+ : 0;
44
+
45
+ const payload = {
46
+ platform: result.platform,
47
+ score: result.score,
48
+ passed: result.passed,
49
+ failed: result.failed,
50
+ total: normalizedCheckCount,
51
+ checkCount: normalizedCheckCount,
52
+ scoreType: result.scoreType || 'live-audit-score',
53
+ grade: result.score >= 80 ? 'A' : result.score >= 60 ? 'B' : result.score >= 40 ? 'C' : 'D',
54
+ criticalFailures: (result.results || [])
55
+ .filter(r => r.passed === false && r.impact === 'critical')
56
+ .map(r => ({ key: r.key, id: r.id, name: r.name, fix: r.fix })),
57
+ highFailures: (result.results || [])
58
+ .filter(r => r.passed === false && r.impact === 'high')
59
+ .map(r => ({ key: r.key, id: r.id, name: r.name, fix: r.fix })),
60
+ topNextActions: (result.topNextActions || []).slice(0, 3).map((item) => ({
61
+ key: item.key,
62
+ name: item.name,
63
+ impact: item.impact,
64
+ fix: item.fix,
65
+ })),
66
+ suggestedNextCommand: result.suggestedNextCommand || null,
67
+ };
68
+
69
+ if (verbose) {
70
+ payload.results = (result.results || []).map(r => ({
71
+ key: r.key,
72
+ id: r.id,
73
+ name: r.name,
74
+ passed: r.passed,
75
+ impact: r.impact,
76
+ fix: r.passed === false ? r.fix : undefined,
77
+ }));
78
+ }
79
+
80
+ return payload;
81
+ }
82
+
83
+ function buildMcpHarmonyPayload(result, options = {}) {
84
+ const verbose = Boolean(options.verbose);
85
+ const payload = {
86
+ harmonyScore: result.harmonyScore,
87
+ activePlatforms: result.activePlatforms || [],
88
+ platformScores: result.platformScores || {},
89
+ driftCount: result.driftCount || (result.drifts || []).length || 0,
90
+ criticalDrifts: (result.drifts || [])
91
+ .filter(d => d.severity === 'critical')
92
+ .map(d => ({ type: d.type, description: d.description, recommendation: d.recommendation })),
93
+ recommendations: (result.recommendations || []).slice(0, 5),
94
+ };
95
+
96
+ if (verbose) {
97
+ payload.allDrifts = (result.drifts || []).map(d => ({
98
+ type: d.type,
99
+ severity: d.severity,
100
+ description: d.description,
101
+ recommendation: d.recommendation,
102
+ }));
103
+ }
104
+
105
+ return payload;
106
+ }
107
+
108
+ // ─── Tool definitions ────────────────────────────────────────────────────────
109
+
110
+ const TOOLS = [
111
+ {
112
+ name: 'nerviq_audit',
113
+ description: 'Run a Nerviq audit on a project directory for a given platform. Returns JSON with score, passed/failed checks, and recommendations.',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ dir: {
118
+ type: 'string',
119
+ description: 'Absolute path to the project directory to audit. Defaults to current working directory.',
120
+ },
121
+ platform: {
122
+ type: 'string',
123
+ description: 'Platform to audit. One of: claude, codex, cursor, copilot, gemini, windsurf, aider, opencode.',
124
+ enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
125
+ default: 'claude',
126
+ },
127
+ verbose: {
128
+ type: 'boolean',
129
+ description: 'Include all checks in output, not just failures.',
130
+ default: false,
131
+ },
132
+ },
133
+ required: [],
134
+ },
135
+ },
136
+ {
137
+ name: 'nerviq_harmony',
138
+ description: 'Run a harmony audit across all detected AI platforms in a project. Returns cross-platform alignment scores, drift analysis, and recommendations.',
139
+ inputSchema: {
140
+ type: 'object',
141
+ properties: {
142
+ dir: {
143
+ type: 'string',
144
+ description: 'Absolute path to the project directory. Defaults to current working directory.',
145
+ },
146
+ verbose: {
147
+ type: 'boolean',
148
+ description: 'Include detailed per-platform results.',
149
+ default: false,
150
+ },
151
+ },
152
+ required: [],
153
+ },
154
+ },
155
+ {
156
+ name: 'nerviq_setup',
157
+ description: 'Generate and write baseline configuration files for a platform in a project directory. Returns the list of files written.',
158
+ inputSchema: {
159
+ type: 'object',
160
+ properties: {
161
+ dir: {
162
+ type: 'string',
163
+ description: 'Absolute path to the project directory. Defaults to current working directory.',
164
+ },
165
+ platform: {
166
+ type: 'string',
167
+ description: 'Platform to set up. One of: claude, codex, cursor, copilot, gemini, windsurf, aider, opencode.',
168
+ enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
169
+ default: 'claude',
170
+ },
171
+ dryRun: {
172
+ type: 'boolean',
173
+ description: 'Preview files that would be written without actually writing them.',
174
+ default: false,
175
+ },
176
+ },
177
+ required: [],
178
+ },
179
+ },
180
+ {
181
+ name: 'nerviq_drift',
182
+ description: 'Detect configuration drift between AI platforms in a project. Returns drift items with severity, type, and recommendations.',
183
+ inputSchema: {
184
+ type: 'object',
185
+ properties: {
186
+ dir: {
187
+ type: 'string',
188
+ description: 'Absolute path to the project directory. Defaults to current working directory.',
189
+ },
190
+ platforms: {
191
+ type: 'array',
192
+ items: { type: 'string' },
193
+ description: 'Specific platforms to compare. Defaults to all detected platforms.',
194
+ },
195
+ minSeverity: {
196
+ type: 'string',
197
+ description: 'Minimum severity to include. One of: critical, high, medium, low.',
198
+ enum: ['critical', 'high', 'medium', 'low'],
199
+ default: 'low',
200
+ },
201
+ },
202
+ required: [],
203
+ },
204
+ },
205
+ {
206
+ name: 'nerviq_check_score',
207
+ description: 'Quick score check for a project — returns score, grade, and whether it meets a threshold. Lighter than a full audit.',
208
+ inputSchema: {
209
+ type: 'object',
210
+ properties: {
211
+ dir: {
212
+ type: 'string',
213
+ description: 'Absolute path to the project directory. Defaults to current working directory.',
214
+ },
215
+ platform: {
216
+ type: 'string',
217
+ description: 'Platform to check. Defaults to claude.',
218
+ enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
219
+ default: 'claude',
220
+ },
221
+ threshold: {
222
+ type: 'number',
223
+ description: 'Minimum passing score (0-100). Returns pass/fail against this threshold.',
224
+ default: 0,
225
+ },
226
+ },
227
+ required: [],
228
+ },
229
+ },
230
+ {
231
+ name: 'nerviq_get_config',
232
+ description: 'Read the current AI agent configuration for a platform in a project. Returns instruction files, settings, MCP servers, hooks, and trust posture.',
233
+ inputSchema: {
234
+ type: 'object',
235
+ properties: {
236
+ dir: {
237
+ type: 'string',
238
+ description: 'Absolute path to the project directory. Defaults to current working directory.',
239
+ },
240
+ platform: {
241
+ type: 'string',
242
+ description: 'Platform to inspect. Defaults to claude.',
243
+ enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
244
+ default: 'claude',
245
+ },
246
+ },
247
+ required: [],
248
+ },
249
+ },
250
+ {
251
+ name: 'nerviq_apply_fix',
252
+ description: 'Apply a specific governance fix by check key. Runs setup/plan for the targeted check and returns what was changed.',
253
+ inputSchema: {
254
+ type: 'object',
255
+ properties: {
256
+ dir: {
257
+ type: 'string',
258
+ description: 'Absolute path to the project directory. Defaults to current working directory.',
259
+ },
260
+ platform: {
261
+ type: 'string',
262
+ description: 'Platform to fix. Defaults to claude.',
263
+ enum: ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'],
264
+ default: 'claude',
265
+ },
266
+ checkKey: {
267
+ type: 'string',
268
+ description: 'The check key to fix (e.g. "claudeMd", "settingsPermissions", "hookExists"). Get available keys from nerviq_audit results.',
269
+ },
270
+ dryRun: {
271
+ type: 'boolean',
272
+ description: 'Preview the fix without applying it.',
273
+ default: false,
274
+ },
275
+ },
276
+ required: ['checkKey'],
277
+ },
278
+ },
279
+ ];
280
+
281
+ // ─── Tool handlers ───────────────────────────────────────────────────────────
282
+
283
+ async function handleAudit(input) {
284
+ const { audit } = require('./audit');
285
+ const dir = input.dir || process.cwd();
286
+ const platform = input.platform || 'claude';
287
+ const verbose = Boolean(input.verbose);
288
+
289
+ const result = await audit({ dir, platform, silent: true, verbose });
290
+ const clean = buildMcpAuditPayload(result, { verbose });
291
+ return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
292
+ }
293
+
294
+ async function handleHarmony(input) {
295
+ const { harmonyAudit } = require('./harmony/audit');
296
+ const dir = input.dir || process.cwd();
297
+ const verbose = Boolean(input.verbose);
298
+
299
+ const result = await harmonyAudit({ dir, silent: true });
300
+ const clean = buildMcpHarmonyPayload(result, { verbose });
301
+ return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
302
+ }
303
+
304
+ async function handleSetup(input) {
305
+ const { setup } = require('./setup');
306
+ const dir = input.dir || process.cwd();
307
+ const platform = input.platform || 'claude';
308
+ const dryRun = Boolean(input.dryRun);
309
+
310
+ const result = await setup({ dir, platform, silent: true, dryRun });
311
+
312
+ const clean = {
313
+ platform,
314
+ dryRun,
315
+ writtenFiles: result.writtenFiles || [],
316
+ skippedFiles: result.skippedFiles || [],
317
+ message: dryRun
318
+ ? `Dry run: would write ${(result.writtenFiles || []).length} file(s)`
319
+ : `Setup complete: wrote ${(result.writtenFiles || []).length} file(s)`,
320
+ };
321
+
322
+ return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
323
+ }
324
+
325
+ async function handleDrift(input) {
326
+ const { detectDrift } = require('./harmony/drift');
327
+ const { buildCanonicalModel, detectActivePlatforms } = require('./harmony/canon');
328
+ const dir = input.dir || process.cwd();
329
+ const minSeverity = input.minSeverity || 'low';
330
+
331
+ const SEVERITY_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
332
+ const minLevel = SEVERITY_ORDER[minSeverity] || 0;
333
+
334
+ const canonModel = buildCanonicalModel(dir);
335
+ const activePlatforms = input.platforms && input.platforms.length > 0
336
+ ? input.platforms
337
+ : detectActivePlatforms(canonModel);
338
+
339
+ const driftResult = detectDrift(canonModel, activePlatforms, { verbose: true });
340
+
341
+ const filteredDrifts = (driftResult.drifts || [])
342
+ .filter(d => (SEVERITY_ORDER[d.severity] || 0) >= minLevel);
343
+
344
+ const clean = {
345
+ dir,
346
+ activePlatforms,
347
+ totalDrifts: driftResult.drifts ? driftResult.drifts.length : 0,
348
+ filteredDrifts: filteredDrifts.length,
349
+ minSeverity,
350
+ drifts: filteredDrifts.map(d => ({
351
+ type: d.type,
352
+ severity: d.severity,
353
+ description: d.description,
354
+ recommendation: d.recommendation || null,
355
+ platforms: d.platforms || null,
356
+ })),
357
+ summary: driftResult.summary || null,
358
+ };
359
+
360
+ return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
361
+ }
362
+
363
+ async function handleCheckScore(input) {
364
+ const { audit } = require('./audit');
365
+ const dir = input.dir || process.cwd();
366
+ const platform = input.platform || 'claude';
367
+ const threshold = input.threshold || 0;
368
+
369
+ const result = await audit({ dir, platform, silent: true });
370
+ const score = result.score;
371
+ const grade = score >= 80 ? 'A' : score >= 60 ? 'B' : score >= 40 ? 'C' : 'D';
372
+ const pass = threshold > 0 ? score >= threshold : true;
373
+
374
+ const clean = {
375
+ score,
376
+ grade,
377
+ platform,
378
+ passed: result.passed,
379
+ failed: result.failed,
380
+ threshold: threshold || null,
381
+ pass,
382
+ remediation_command: score < 70 ? `npx @nerviq/cli augment --platform ${platform}` : null,
383
+ };
384
+
385
+ return { content: [{ type: 'text', text: JSON.stringify(clean, null, 2) }] };
386
+ }
387
+
388
+ async function handleGetConfig(input) {
389
+ const fs = require('fs');
390
+ const path = require('path');
391
+ const dir = input.dir || process.cwd();
392
+ const platform = input.platform || 'claude';
393
+
394
+ const config = { platform, dir, files: {} };
395
+
396
+ // Platform-specific config file mappings
397
+ const FILE_MAP = {
398
+ claude: {
399
+ instructions: ['CLAUDE.md', '.claude/CLAUDE.md'],
400
+ settings: ['.claude/settings.json', '.claude/settings.local.json'],
401
+ rules: '.claude/rules',
402
+ hooks: '.claude/hooks',
403
+ },
404
+ codex: {
405
+ instructions: ['AGENTS.md'],
406
+ settings: ['.codex/config.toml'],
407
+ rules: null,
408
+ hooks: null,
409
+ },
410
+ gemini: {
411
+ instructions: ['GEMINI.md'],
412
+ settings: ['.gemini/settings.json'],
413
+ rules: null,
414
+ hooks: null,
415
+ },
416
+ copilot: {
417
+ instructions: ['.github/copilot-instructions.md'],
418
+ settings: [],
419
+ rules: null,
420
+ hooks: null,
421
+ },
422
+ cursor: {
423
+ instructions: ['.cursorrules'],
424
+ settings: [],
425
+ rules: '.cursor/rules',
426
+ hooks: null,
427
+ },
428
+ windsurf: {
429
+ instructions: ['.windsurfrules'],
430
+ settings: [],
431
+ rules: '.windsurf/rules',
432
+ hooks: null,
433
+ },
434
+ aider: {
435
+ instructions: ['.aider.conf.yml'],
436
+ settings: ['.aiderignore'],
437
+ rules: null,
438
+ hooks: null,
439
+ },
440
+ opencode: {
441
+ instructions: ['opencode.json'],
442
+ settings: ['.opencode'],
443
+ rules: null,
444
+ hooks: null,
445
+ },
446
+ };
447
+
448
+ const mapping = FILE_MAP[platform] || FILE_MAP.claude;
449
+
450
+ // Read instruction files
451
+ for (const f of mapping.instructions) {
452
+ const fullPath = path.join(dir, f);
453
+ if (fs.existsSync(fullPath)) {
454
+ try {
455
+ const content = fs.readFileSync(fullPath, 'utf8');
456
+ config.files[f] = { exists: true, size: content.length, lines: content.split('\n').length };
457
+ } catch { config.files[f] = { exists: true, error: 'unreadable' }; }
458
+ } else {
459
+ config.files[f] = { exists: false };
460
+ }
461
+ }
462
+
463
+ // Read settings files
464
+ for (const f of mapping.settings) {
465
+ const fullPath = path.join(dir, f);
466
+ if (fs.existsSync(fullPath)) {
467
+ try {
468
+ const content = fs.readFileSync(fullPath, 'utf8');
469
+ if (f.endsWith('.json')) {
470
+ config.files[f] = { exists: true, parsed: JSON.parse(content) };
471
+ } else {
472
+ config.files[f] = { exists: true, size: content.length };
473
+ }
474
+ } catch { config.files[f] = { exists: true, error: 'parse-failed' }; }
475
+ } else {
476
+ config.files[f] = { exists: false };
477
+ }
478
+ }
479
+
480
+ // Check rules dir
481
+ if (mapping.rules) {
482
+ const rulesPath = path.join(dir, mapping.rules);
483
+ if (fs.existsSync(rulesPath)) {
484
+ try {
485
+ const entries = fs.readdirSync(rulesPath);
486
+ config.files[mapping.rules] = { exists: true, count: entries.length, entries };
487
+ } catch { config.files[mapping.rules] = { exists: true, error: 'unreadable' }; }
488
+ } else {
489
+ config.files[mapping.rules] = { exists: false };
490
+ }
491
+ }
492
+
493
+ // Check hooks dir
494
+ if (mapping.hooks) {
495
+ const hooksPath = path.join(dir, mapping.hooks);
496
+ if (fs.existsSync(hooksPath)) {
497
+ try {
498
+ const entries = fs.readdirSync(hooksPath);
499
+ config.files[mapping.hooks] = { exists: true, count: entries.length, entries };
500
+ } catch { config.files[mapping.hooks] = { exists: true, error: 'unreadable' }; }
501
+ } else {
502
+ config.files[mapping.hooks] = { exists: false };
503
+ }
504
+ }
505
+
506
+ // Trust posture summary
507
+ if (platform === 'claude' && config.files['.claude/settings.json'] && config.files['.claude/settings.json'].parsed) {
508
+ const settings = config.files['.claude/settings.json'].parsed;
509
+ config.trustPosture = {
510
+ allowedTools: settings.permissions?.allow || [],
511
+ deniedPatterns: settings.permissions?.deny || [],
512
+ hasExplicitPermissions: !!(settings.permissions),
513
+ };
514
+ }
515
+
516
+ return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }] };
517
+ }
518
+
519
+ async function handleApplyFix(input) {
520
+ const { audit } = require('./audit');
521
+ const { setup } = require('./setup');
522
+ const dir = input.dir || process.cwd();
523
+ const platform = input.platform || 'claude';
524
+ const checkKey = input.checkKey;
525
+ const dryRun = Boolean(input.dryRun);
526
+
527
+ // First, verify the check actually fails
528
+ const preAudit = await audit({ dir, platform, silent: true });
529
+ const targetCheck = (preAudit.results || []).find(r => r.key === checkKey);
530
+
531
+ if (!targetCheck) {
532
+ return { content: [{ type: 'text', text: JSON.stringify({
533
+ status: 'error',
534
+ message: `Check key "${checkKey}" not found in ${platform} audit. Use nerviq_audit to see available check keys.`,
535
+ }, null, 2) }] };
536
+ }
537
+
538
+ if (targetCheck.passed) {
539
+ return { content: [{ type: 'text', text: JSON.stringify({
540
+ status: 'already_passing',
541
+ checkKey,
542
+ message: `Check "${checkKey}" is already passing. No fix needed.`,
543
+ }, null, 2) }] };
544
+ }
545
+
546
+ // Map check categories to setup --only targets
547
+ const CATEGORY_TO_ONLY = {
548
+ memory: 'instructions',
549
+ security: 'permissions',
550
+ automation: 'hooks',
551
+ workflow: 'commands',
552
+ tools: 'mcp',
553
+ quality: 'instructions',
554
+ git: 'instructions',
555
+ };
556
+
557
+ const only = CATEGORY_TO_ONLY[targetCheck.category] || null;
558
+
559
+ if (dryRun) {
560
+ return { content: [{ type: 'text', text: JSON.stringify({
561
+ status: 'dry_run',
562
+ checkKey,
563
+ category: targetCheck.category,
564
+ fix: targetCheck.fix,
565
+ would_run: `npx @nerviq/cli setup --platform ${platform}${only ? ` --only ${only}` : ''}`,
566
+ }, null, 2) }] };
567
+ }
568
+
569
+ // Apply the fix
570
+ const setupResult = await setup({
571
+ dir,
572
+ platform,
573
+ silent: true,
574
+ only: only ? [only] : undefined,
575
+ });
576
+
577
+ // Re-audit to check if fix worked
578
+ const postAudit = await audit({ dir, platform, silent: true });
579
+ const postCheck = (postAudit.results || []).find(r => r.key === checkKey);
580
+
581
+ return { content: [{ type: 'text', text: JSON.stringify({
582
+ status: postCheck && postCheck.passed ? 'fixed' : 'attempted',
583
+ checkKey,
584
+ category: targetCheck.category,
585
+ fix_description: targetCheck.fix,
586
+ files_written: setupResult.writtenFiles || [],
587
+ score_before: preAudit.score,
588
+ score_after: postAudit.score,
589
+ check_now_passing: postCheck ? postCheck.passed : false,
590
+ rollback_id: setupResult.rollbackId || null,
591
+ }, null, 2) }] };
592
+ }
593
+
594
+ // ─── JSON-RPC 2.0 / MCP stdio transport ─────────────────────────────────────
595
+
596
+ function sendResponse(id, result) {
597
+ const msg = JSON.stringify({ jsonrpc: '2.0', id, result });
598
+ process.stdout.write(msg + '\n');
599
+ }
600
+
601
+ function sendError(id, code, message, data) {
602
+ const msg = JSON.stringify({
603
+ jsonrpc: '2.0',
604
+ id: id !== undefined ? id : null,
605
+ error: { code, message, ...(data ? { data } : {}) },
606
+ });
607
+ process.stdout.write(msg + '\n');
608
+ }
609
+
610
+ async function handleRequest(req) {
611
+ const { id, method, params } = req;
612
+
613
+ if (method === 'initialize') {
614
+ return sendResponse(id, {
615
+ protocolVersion: '2024-11-05',
616
+ capabilities: { tools: {} },
617
+ serverInfo: { name: 'nerviq', version },
618
+ });
619
+ }
620
+
621
+ if (method === 'tools/list') {
622
+ return sendResponse(id, { tools: TOOLS });
623
+ }
624
+
625
+ if (method === 'tools/call') {
626
+ const toolName = params && params.name;
627
+ const toolInput = (params && params.arguments) || {};
628
+
629
+ try {
630
+ let result;
631
+ if (toolName === 'nerviq_audit') {
632
+ result = await handleAudit(toolInput);
633
+ } else if (toolName === 'nerviq_harmony') {
634
+ result = await handleHarmony(toolInput);
635
+ } else if (toolName === 'nerviq_setup') {
636
+ result = await handleSetup(toolInput);
637
+ } else if (toolName === 'nerviq_drift') {
638
+ result = await handleDrift(toolInput);
639
+ } else if (toolName === 'nerviq_check_score') {
640
+ result = await handleCheckScore(toolInput);
641
+ } else if (toolName === 'nerviq_get_config') {
642
+ result = await handleGetConfig(toolInput);
643
+ } else if (toolName === 'nerviq_apply_fix') {
644
+ result = await handleApplyFix(toolInput);
645
+ } else {
646
+ return sendError(id, -32601, `Unknown tool: ${toolName}`);
647
+ }
648
+ return sendResponse(id, result);
649
+ } catch (err) {
650
+ return sendError(id, -32000, err.message, { stack: err.stack });
651
+ }
652
+ }
653
+
654
+ if (method === 'notifications/initialized' || method === 'ping') {
655
+ // No response needed for notifications; ack ping
656
+ if (method === 'ping') sendResponse(id, {});
657
+ return;
658
+ }
659
+
660
+ sendError(id, -32601, `Method not found: ${method}`);
661
+ }
662
+
663
+ // ─── Main loop ───────────────────────────────────────────────────────────────
664
+
665
+ function main() {
666
+ let buffer = '';
667
+
668
+ process.stdin.setEncoding('utf8');
669
+ process.stdin.on('data', (chunk) => {
670
+ buffer += chunk;
671
+ const lines = buffer.split('\n');
672
+ buffer = lines.pop(); // keep incomplete line
673
+
674
+ for (const line of lines) {
675
+ const trimmed = line.trim();
676
+ if (!trimmed) continue;
677
+
678
+ let req;
679
+ try {
680
+ req = JSON.parse(trimmed);
681
+ } catch {
682
+ sendError(null, -32700, 'Parse error', { raw: trimmed.slice(0, 200) });
683
+ continue;
684
+ }
685
+
686
+ handleRequest(req).catch((err) => {
687
+ sendError(req.id, -32000, 'Internal error', { message: err.message });
688
+ });
689
+ }
690
+ });
691
+
692
+ process.stdin.on('end', () => {
693
+ // Flush remaining buffer
694
+ if (buffer.trim()) {
695
+ let req;
696
+ try {
697
+ req = JSON.parse(buffer.trim());
698
+ handleRequest(req).catch(() => {});
699
+ } catch {}
700
+ }
701
+ process.exit(0);
702
+ });
703
+
704
+ // Suppress unhandled rejection crashes in MCP server context
705
+ process.on('unhandledRejection', (err) => {
706
+ process.stderr.write(`[nerviq-mcp] Unhandled rejection: ${err && err.message}\n`);
707
+ });
708
+ }
709
+
710
+ if (require.main === module) {
711
+ main();
712
+ }
713
+
714
+ module.exports = {
715
+ TOOLS,
716
+ buildMcpAuditPayload,
717
+ buildMcpHarmonyPayload,
718
+ handleAudit,
719
+ handleHarmony,
720
+ handleSetup,
721
+ handleDrift,
722
+ handleCheckScore,
723
+ handleGetConfig,
724
+ handleApplyFix,
725
+ main,
726
+ };