@nerviq/cli 1.10.0 → 1.12.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 (57) hide show
  1. package/README.md +176 -47
  2. package/bin/cli.js +842 -287
  3. package/package.json +2 -2
  4. package/src/activity.js +225 -59
  5. package/src/adoption-advisor.js +299 -0
  6. package/src/aider/freshness.js +28 -25
  7. package/src/aider/techniques.js +16 -11
  8. package/src/analyze.js +131 -1
  9. package/src/anti-patterns.js +17 -2
  10. package/src/audit.js +197 -96
  11. package/src/behavioral-drift.js +801 -0
  12. package/src/benchmark.js +15 -10
  13. package/src/continuous-ops.js +681 -0
  14. package/src/cost-tracking.js +61 -0
  15. package/src/cursor/techniques.js +17 -12
  16. package/src/deep-review.js +83 -0
  17. package/src/diff-only.js +280 -0
  18. package/src/doctor.js +118 -55
  19. package/src/governance.js +72 -50
  20. package/src/hook-validation.js +342 -0
  21. package/src/index.js +7 -1
  22. package/src/integrations.js +144 -60
  23. package/src/mcp-validation.js +337 -0
  24. package/src/opencode/techniques.js +12 -7
  25. package/src/operating-profile.js +574 -0
  26. package/src/org.js +97 -13
  27. package/src/permission-rules.js +218 -0
  28. package/src/plans.js +192 -8
  29. package/src/platform-change-manifest.js +86 -0
  30. package/src/policy-layers.js +210 -0
  31. package/src/profiles.js +4 -1
  32. package/src/prompt-injection.js +74 -0
  33. package/src/repo-archetype.js +386 -0
  34. package/src/secret-patterns.js +9 -0
  35. package/src/server.js +398 -3
  36. package/src/setup.js +36 -2
  37. package/src/source-urls.js +132 -132
  38. package/src/supplemental-checks.js +13 -12
  39. package/src/techniques/api.js +407 -0
  40. package/src/techniques/automation.js +316 -0
  41. package/src/techniques/compliance.js +257 -0
  42. package/src/techniques/hygiene.js +294 -0
  43. package/src/techniques/instructions.js +243 -0
  44. package/src/techniques/observability.js +226 -0
  45. package/src/techniques/optimization.js +142 -0
  46. package/src/techniques/quality.js +317 -0
  47. package/src/techniques/security.js +237 -0
  48. package/src/techniques/shared.js +443 -0
  49. package/src/techniques/stacks.js +2294 -0
  50. package/src/techniques/tools.js +106 -0
  51. package/src/techniques/workflow.js +413 -0
  52. package/src/techniques.js +78 -5611
  53. package/src/terminology.js +73 -0
  54. package/src/token-estimate.js +35 -0
  55. package/src/watch.js +18 -0
  56. package/src/windsurf/techniques.js +17 -12
  57. package/src/workspace.js +105 -8
package/src/doctor.js CHANGED
@@ -5,11 +5,13 @@
5
5
  * Checks: Node version, dependencies, platform detection, freshness gates.
6
6
  */
7
7
 
8
- 'use strict';
9
-
10
- const fs = require('fs');
11
- const path = require('path');
12
- const { version } = require('../package.json');
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { version } = require('../package.json');
13
+ const { validateDeclaredMcpServers } = require('./mcp-validation');
14
+ const { validateDeclaredHooks } = require('./hook-validation');
13
15
 
14
16
  const COLORS = {
15
17
  reset: '\x1b[0m',
@@ -25,11 +27,11 @@ function c(text, color) {
25
27
  return `${COLORS[color] || ''}${text}${COLORS.reset}`;
26
28
  }
27
29
 
28
- const PLATFORM_SIGNALS = {
29
- claude: ['CLAUDE.md', '.claude/CLAUDE.md', '.claude/settings.json'],
30
- codex: ['AGENTS.md', '.codex/', '.codex/config.toml'],
31
- cursor: ['.cursor/rules/', '.cursor/mcp.json', '.cursorrules'],
32
- copilot: ['.github/copilot-instructions.md', '.github/'],
30
+ const PLATFORM_SIGNALS = {
31
+ claude: ['CLAUDE.md', '.claude/CLAUDE.md', '.claude/settings.json', '.mcp.json'],
32
+ codex: ['AGENTS.md', '.codex/', '.codex/config.toml'],
33
+ cursor: ['.cursor/rules/', '.cursor/mcp.json', '.cursorrules'],
34
+ copilot: ['.github/copilot-instructions.md', '.github/', '.vscode/mcp.json'],
33
35
  gemini: ['GEMINI.md', '.gemini/', '.gemini/settings.json'],
34
36
  windsurf: ['.windsurf/', '.windsurfrules', '.windsurf/rules/'],
35
37
  aider: ['.aider.conf.yml', '.aider.model.settings.yml'],
@@ -162,46 +164,59 @@ function checkGitRepo(dir) {
162
164
 
163
165
  // ─── Main doctor function ────────────────────────────────────────────────────
164
166
 
165
- async function runDoctor({ dir = process.cwd(), json = false, verbose = false } = {}) {
166
- const startMs = Date.now();
167
-
168
- const checks = [
167
+ async function runDoctor({ dir = process.cwd(), json = false, verbose = false } = {}) {
168
+ const startMs = Date.now();
169
+
170
+ const checks = [
169
171
  checkNodeVersion(),
170
172
  checkDeps(),
171
173
  checkJestInstalled(),
172
174
  checkCliPermissions(),
173
175
  checkGitRepo(dir),
174
176
  checkPlatformDetection(dir),
175
- ];
176
-
177
- const freshnessChecks = checkFreshnessGates();
178
-
179
- const totalPass = checks.filter(c => c.status === 'pass').length;
180
- const totalWarn = checks.filter(c => c.status === 'warn').length;
181
- const totalFail = checks.filter(c => c.status === 'fail').length;
182
-
183
- const freshPass = freshnessChecks.filter(f => f.status === 'pass').length;
184
- const freshWarn = freshnessChecks.filter(f => f.status !== 'pass').length;
185
-
186
- const overallOk = totalFail === 0;
187
- const elapsed = Date.now() - startMs;
177
+ ];
178
+
179
+ const detectedPlatforms = (checks.find(c => c.detected) || {}).detected || [];
180
+ const freshnessChecks = checkFreshnessGates();
181
+ const mcpSummary = await validateDeclaredMcpServers({ dir, detectedPlatforms });
182
+ const hookSummary = validateDeclaredHooks({ dir, detectedPlatforms });
183
+
184
+ const totalPass = checks.filter(c => c.status === 'pass').length;
185
+ const totalWarn = checks.filter(c => c.status === 'warn').length;
186
+ const totalFail = checks.filter(c => c.status === 'fail').length;
188
187
 
189
- if (json) {
190
- return JSON.stringify({
191
- nerviq: version,
188
+ const freshPass = freshnessChecks.filter(f => f.status === 'pass').length;
189
+ const freshWarn = freshnessChecks.filter(f => f.status !== 'pass').length;
190
+
191
+ const overallOk = totalFail === 0 && mcpSummary.fail === 0 && hookSummary.fail === 0;
192
+ const elapsed = Date.now() - startMs;
193
+
194
+ if (json) {
195
+ return JSON.stringify({
196
+ nerviq: version,
192
197
  node: process.version,
193
198
  dir,
194
199
  overallOk,
195
200
  checks,
196
- freshnessChecks,
197
- totalPass,
198
- totalWarn,
199
- totalFail,
200
- freshPass,
201
- freshWarn,
202
- elapsed,
203
- }, null, 2);
204
- }
201
+ freshnessChecks,
202
+ mcpChecks: mcpSummary.checks,
203
+ hookChecks: hookSummary.checks,
204
+ totalPass,
205
+ totalWarn,
206
+ totalFail,
207
+ freshPass,
208
+ freshWarn,
209
+ mcpDeclared: mcpSummary.declared,
210
+ mcpPass: mcpSummary.pass,
211
+ mcpWarn: mcpSummary.warn,
212
+ mcpFail: mcpSummary.fail,
213
+ hookDeclared: hookSummary.declared,
214
+ hookPass: hookSummary.pass,
215
+ hookWarn: hookSummary.warn,
216
+ hookFail: hookSummary.fail,
217
+ elapsed,
218
+ }, null, 2);
219
+ }
205
220
 
206
221
  const lines = [''];
207
222
  lines.push(c(` nerviq doctor v${version}`, 'bold'));
@@ -219,10 +234,9 @@ async function runDoctor({ dir = process.cwd(), json = false, verbose = false }
219
234
  }
220
235
 
221
236
  // Platform detection detail
222
- const detectedPlatforms = (checks.find(c => c.detected) || {}).detected || [];
223
- if (detectedPlatforms.length > 0) {
224
- lines.push('');
225
- lines.push(c(' Detected Platforms', 'bold'));
237
+ if (detectedPlatforms.length > 0) {
238
+ lines.push('');
239
+ lines.push(c(' Detected Platforms', 'bold'));
226
240
  for (const p of detectedPlatforms) {
227
241
  lines.push(` ${c('✓', 'green')} ${p}`);
228
242
  }
@@ -231,18 +245,67 @@ async function runDoctor({ dir = process.cwd(), json = false, verbose = false }
231
245
  // Freshness
232
246
  lines.push('');
233
247
  lines.push(c(' Freshness Gates', 'bold'));
234
- for (const f of freshnessChecks) {
235
- const icon = f.status === 'pass' ? c('✓', 'green') : c('⚠', 'yellow');
236
- const label = f.platform.padEnd(12);
237
- lines.push(` ${icon} ${label} ${c(f.detail || f.status, f.status === 'pass' ? 'dim' : 'yellow')}`);
238
- }
239
-
240
- lines.push('');
241
- lines.push(c(' Summary', 'bold'));
242
- lines.push(` Checks: ${c(String(totalPass), 'green')} pass ${totalWarn > 0 ? c(String(totalWarn), 'yellow') + ' warn ' : ''}${totalFail > 0 ? c(String(totalFail), 'red') + ' fail' : ''}`);
243
- lines.push(` Freshness: ${c(String(freshPass), 'green')} fresh ${freshWarn > 0 ? c(String(freshWarn), 'yellow') + ' stale/unverified' : ''}`);
244
- lines.push(` Status: ${overallOk ? c('✓ Healthy', 'green') : c('✗ Issues found', 'red')}`);
245
- lines.push(` Duration: ${elapsed}ms`);
248
+ for (const f of freshnessChecks) {
249
+ const icon = f.status === 'pass' ? c('✓', 'green') : c('⚠', 'yellow');
250
+ const label = f.platform.padEnd(12);
251
+ lines.push(` ${icon} ${label} ${c(f.detail || f.status, f.status === 'pass' ? 'dim' : 'yellow')}`);
252
+ }
253
+
254
+ lines.push('');
255
+ lines.push(c(' MCP Servers', 'bold'));
256
+ if (mcpSummary.checks.length === 0) {
257
+ lines.push(c(' No declared MCP servers found in the detected project surfaces.', 'dim'));
258
+ } else {
259
+ for (const item of mcpSummary.checks) {
260
+ const icon = item.status === 'pass'
261
+ ? c('✓', 'green')
262
+ : item.status === 'warn'
263
+ ? c('⚠', 'yellow')
264
+ : c('✗', 'red');
265
+ const label = `${item.platform}/${item.scope}`.padEnd(16);
266
+ lines.push(` ${icon} ${label} ${item.serverName} ${c(item.detail, item.status === 'pass' ? 'dim' : item.status === 'warn' ? 'yellow' : 'red')}`);
267
+ if (verbose && item.source) {
268
+ lines.push(c(` Source: ${item.source}`, 'dim'));
269
+ }
270
+ if (item.fix && (verbose || item.status === 'fail')) {
271
+ lines.push(c(` Fix: ${item.fix}`, item.status === 'fail' ? 'yellow' : 'dim'));
272
+ }
273
+ }
274
+ }
275
+
276
+ lines.push('');
277
+ lines.push(c(' Hook Runtime', 'bold'));
278
+ if (hookSummary.checks.length === 0) {
279
+ lines.push(c(' No declared hooks found in the detected project surfaces.', 'dim'));
280
+ } else {
281
+ for (const item of hookSummary.checks) {
282
+ const icon = item.status === 'pass'
283
+ ? c('✓', 'green')
284
+ : item.status === 'warn'
285
+ ? c('⚠', 'yellow')
286
+ : c('✗', 'red');
287
+ const label = `${item.platform}/${item.validationMode}`.padEnd(16);
288
+ lines.push(` ${icon} ${label} ${item.label} ${c(item.detail, item.status === 'pass' ? 'dim' : item.status === 'warn' ? 'yellow' : 'red')}`);
289
+ if (verbose && item.script) {
290
+ lines.push(c(` Script: ${item.script}`, 'dim'));
291
+ }
292
+ if (verbose && item.executable) {
293
+ lines.push(c(` Runtime: ${item.executable}`, 'dim'));
294
+ }
295
+ if (item.fix && (verbose || item.status === 'fail')) {
296
+ lines.push(c(` Fix: ${item.fix}`, item.status === 'fail' ? 'yellow' : 'dim'));
297
+ }
298
+ }
299
+ }
300
+
301
+ lines.push('');
302
+ lines.push(c(' Summary', 'bold'));
303
+ lines.push(` Checks: ${c(String(totalPass), 'green')} pass ${totalWarn > 0 ? c(String(totalWarn), 'yellow') + ' warn ' : ''}${totalFail > 0 ? c(String(totalFail), 'red') + ' fail' : ''}`);
304
+ lines.push(` Freshness: ${c(String(freshPass), 'green')} fresh ${freshWarn > 0 ? c(String(freshWarn), 'yellow') + ' stale/unverified' : ''}`);
305
+ lines.push(` MCP: ${c(String(mcpSummary.pass), 'green')} pass ${mcpSummary.warn > 0 ? c(String(mcpSummary.warn), 'yellow') + ' warn ' : ''}${mcpSummary.fail > 0 ? c(String(mcpSummary.fail), 'red') + ' fail' : ''}${c(`(${mcpSummary.declared} declared)`, 'dim')}`);
306
+ lines.push(` Hooks: ${c(String(hookSummary.pass), 'green')} pass ${hookSummary.warn > 0 ? c(String(hookSummary.warn), 'yellow') + ' warn ' : ''}${hookSummary.fail > 0 ? c(String(hookSummary.fail), 'red') + ' fail' : ''}${c(`(${hookSummary.declared} declared)`, 'dim')}`);
307
+ lines.push(` Status: ${overallOk ? c('✓ Healthy', 'green') : c('✗ Issues found', 'red')}`);
308
+ lines.push(` Duration: ${elapsed}ms`);
246
309
  lines.push('');
247
310
 
248
311
  if (!overallOk) {
package/src/governance.js CHANGED
@@ -1,6 +1,7 @@
1
- const { DOMAIN_PACKS } = require('./domain-packs');
2
- const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
3
- const { getCodexGovernanceSummary } = require('./codex/governance');
1
+ const { DOMAIN_PACKS } = require('./domain-packs');
2
+ const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
3
+ const { getCodexGovernanceSummary } = require('./codex/governance');
4
+ const { formatTerminologyLines } = require('./terminology');
4
5
 
5
6
  const PERMISSION_PROFILES = [
6
7
  {
@@ -103,19 +104,19 @@ const HOOK_REGISTRY = [
103
104
  dryRunExample: 'Edit a catalog file and verify duplicate check runs without blocking.',
104
105
  rollbackPath: 'Remove the PostToolUse hook entry from settings.',
105
106
  },
106
- {
107
- key: 'injection-defense',
108
- file: '.claude/hooks/injection-defense.sh',
109
- triggerPoint: 'PostToolUse',
110
- matcher: 'WebFetch|WebSearch',
111
- purpose: 'Scans web tool outputs for common prompt injection patterns.',
112
- filesTouched: ['tools/failure-log.txt'],
113
- sideEffects: ['Logs alerts to failure log.', 'Returns a systemMessage warning if patterns detected.'],
114
- risk: 'low',
115
- riskLevel: 'low',
116
- dryRunExample: 'Run a WebFetch and verify output is scanned for injection patterns.',
117
- rollbackPath: 'Remove the PostToolUse hook entry from settings.',
118
- },
107
+ {
108
+ key: 'injection-defense',
109
+ file: '.claude/hooks/injection-defense.js',
110
+ triggerPoint: 'PostToolUse',
111
+ matcher: 'WebFetch|WebSearch|Read|Grep|Glob|mcp__.*',
112
+ purpose: 'Scans external content flows for common prompt injection patterns and logs suspicious findings.',
113
+ filesTouched: ['.claude/logs/prompt-injection-alerts.log'],
114
+ sideEffects: ['Appends an alert line when suspicious external content is detected.'],
115
+ risk: 'low',
116
+ riskLevel: 'low',
117
+ dryRunExample: 'Run a WebFetch or MCP-backed tool call and verify suspicious content is logged for review.',
118
+ rollbackPath: 'Remove the PostToolUse hook entry from settings.',
119
+ },
119
120
  {
120
121
  key: 'trust-drift-check',
121
122
  file: '.claude/hooks/trust-drift-check.sh',
@@ -298,23 +299,24 @@ function buildHookConfig(hookFiles, profileKey) {
298
299
  return {};
299
300
  }
300
301
 
301
- // Detect hook runtime: .js files use node, .sh files use bash
302
- const hookCommand = (file) => {
303
- if (file.endsWith('.js')) return `node .claude/hooks/${file}`;
304
- return `bash .claude/hooks/${file}`;
305
- };
306
- const isSecrets = (f) => f === 'protect-secrets.sh' || f === 'protect-secrets.js';
307
- const isSession = (f) => f === 'session-start.sh' || f === 'session-start.js';
308
-
309
- const hookConfig = {
310
- PostToolUse: [{
311
- matcher: 'Write|Edit',
312
- hooks: uniqueFiles
313
- .filter(file => !isSecrets(file) && !isSession(file))
314
- .map(file => ({
315
- type: 'command',
316
- command: hookCommand(file),
317
- timeout: 10,
302
+ // Detect hook runtime: .js files use node, .sh files use bash
303
+ const hookCommand = (file) => {
304
+ if (file.endsWith('.js')) return `node .claude/hooks/${file}`;
305
+ return `bash .claude/hooks/${file}`;
306
+ };
307
+ const isSecrets = (f) => f === 'protect-secrets.sh' || f === 'protect-secrets.js';
308
+ const isSession = (f) => f === 'session-start.sh' || f === 'session-start.js';
309
+ const isInjection = (f) => f === 'injection-defense.sh' || f === 'injection-defense.js';
310
+
311
+ const hookConfig = {
312
+ PostToolUse: [{
313
+ matcher: 'Write|Edit',
314
+ hooks: uniqueFiles
315
+ .filter(file => !isSecrets(file) && !isSession(file) && !isInjection(file))
316
+ .map(file => ({
317
+ type: 'command',
318
+ command: hookCommand(file),
319
+ timeout: 10,
318
320
  })),
319
321
  }],
320
322
  };
@@ -332,16 +334,29 @@ function buildHookConfig(hookFiles, profileKey) {
332
334
  }
333
335
 
334
336
  const sessionFile = uniqueFiles.find(isSession);
335
- if (sessionFile) {
336
- hookConfig.SessionStart = [{
337
- matcher: '*',
338
- hooks: [{
339
- type: 'command',
337
+ if (sessionFile) {
338
+ hookConfig.SessionStart = [{
339
+ matcher: '*',
340
+ hooks: [{
341
+ type: 'command',
340
342
  command: hookCommand(sessionFile),
341
343
  timeout: 5,
342
- }],
343
- }];
344
- }
344
+ }],
345
+ }];
346
+ }
347
+
348
+ const injectionFile = uniqueFiles.find(isInjection);
349
+ if (injectionFile) {
350
+ hookConfig.PostToolUse = hookConfig.PostToolUse || [];
351
+ hookConfig.PostToolUse.push({
352
+ matcher: 'WebFetch|WebSearch|Read|Grep|Glob|mcp__.*',
353
+ hooks: [{
354
+ type: 'command',
355
+ command: hookCommand(injectionFile),
356
+ timeout: 5,
357
+ }],
358
+ });
359
+ }
345
360
 
346
361
  if ((hookConfig.PostToolUse[0].hooks || []).length === 0) {
347
362
  delete hookConfig.PostToolUse;
@@ -406,10 +421,15 @@ function printGovernanceSummary(summary, options = {}) {
406
421
  console.log('');
407
422
  console.log(` nerviq ${summary.platformLabel.toLowerCase()} governance`);
408
423
  console.log(' ═══════════════════════════════════════');
409
- console.log(` Safe defaults, hook transparency, and pilot guidance for ${summary.platformLabel}.`);
410
- console.log('');
411
-
412
- console.log(' Permission Profiles');
424
+ console.log(` Safe defaults, hook transparency, and pilot guidance for ${summary.platformLabel}.`);
425
+ console.log('');
426
+
427
+ for (const line of formatTerminologyLines(['governance', 'hooks', 'denyRules', 'mcp'])) {
428
+ console.log(line);
429
+ }
430
+ console.log('');
431
+
432
+ console.log(' Permission Profiles');
413
433
  for (const profile of summary.permissionProfiles) {
414
434
  console.log(` - ${profile.label} [${profile.risk}]`);
415
435
  console.log(` ${profile.useWhen}`);
@@ -570,11 +590,13 @@ function renderGovernanceMarkdown(summary) {
570
590
  return lines.join('\n');
571
591
  }
572
592
 
573
- module.exports = {
574
- PERMISSION_PROFILES,
575
- getPermissionProfile,
576
- isWritableProfile,
577
- ensureWritableProfile,
593
+ module.exports = {
594
+ PERMISSION_PROFILES,
595
+ HOOK_REGISTRY,
596
+ POLICY_PACKS,
597
+ getPermissionProfile,
598
+ isWritableProfile,
599
+ ensureWritableProfile,
578
600
  buildSettingsForProfile,
579
601
  getGovernanceSummary,
580
602
  printGovernanceSummary,