@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
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ const TERMINOLOGY = {
4
+ governance: {
5
+ label: 'governance',
6
+ description: 'the rollout safety layer: permissions, hooks, profiles, and policy packs',
7
+ },
8
+ hooks: {
9
+ label: 'hooks',
10
+ description: 'auto-run checks or scripts triggered before or after agent tool actions',
11
+ },
12
+ denyRules: {
13
+ label: 'deny rules',
14
+ description: 'explicit blocks for risky reads or commands like .env access or rm -rf',
15
+ },
16
+ mcp: {
17
+ label: 'MCP',
18
+ description: 'live external tool connectors for docs, APIs, databases, and other systems',
19
+ },
20
+ };
21
+
22
+ const TERM_ORDER = ['governance', 'hooks', 'denyRules', 'mcp'];
23
+
24
+ function normalizeTermKeys(keys = []) {
25
+ const seen = new Set();
26
+ for (const key of keys) {
27
+ if (!TERMINOLOGY[key]) continue;
28
+ seen.add(key);
29
+ }
30
+ return TERM_ORDER.filter((key) => seen.has(key));
31
+ }
32
+
33
+ function collectAuditTerminology(result = {}) {
34
+ const terms = new Set();
35
+ const texts = [];
36
+
37
+ for (const item of result.topNextActions || []) {
38
+ texts.push(item.name || '', item.fix || '', item.why || '', item.module || '', ...(item.signals || []));
39
+ }
40
+
41
+ for (const item of result.results || []) {
42
+ if (item.passed === false) {
43
+ texts.push(item.key || '', item.name || '', item.fix || '', item.category || '');
44
+ }
45
+ }
46
+
47
+ const blob = texts.join('\n');
48
+ if (/\bhook/i.test(blob)) terms.add('hooks');
49
+ if (/\bdeny rules?\b|permissions?\.deny|bypasspermissions|\.env access|rm -rf/i.test(blob)) terms.add('denyRules');
50
+ if (/\bmcp\b|context7|external tool/i.test(blob)) terms.add('mcp');
51
+ if (/\bgovernance\b|policy pack|permission profile/i.test(blob) || terms.size > 0) terms.add('governance');
52
+
53
+ return normalizeTermKeys([...terms]);
54
+ }
55
+
56
+ function formatTerminologyLines(keys, options = {}) {
57
+ const normalized = normalizeTermKeys(keys);
58
+ if (normalized.length === 0) return [];
59
+ const title = options.title || ' Terms used here:';
60
+ const indent = options.indent || ' ';
61
+ const bullet = options.bullet || '-';
62
+
63
+ return [
64
+ title,
65
+ ...normalized.map((key) => `${indent}${bullet} ${TERMINOLOGY[key].label}: ${TERMINOLOGY[key].description}`),
66
+ ];
67
+ }
68
+
69
+ module.exports = {
70
+ TERMINOLOGY,
71
+ collectAuditTerminology,
72
+ formatTerminologyLines,
73
+ };
@@ -0,0 +1,35 @@
1
+ function splitIdentifierSegments(value) {
2
+ return String(value || '')
3
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
4
+ .replace(/[_./:-]+/g, ' ')
5
+ .split(/\s+/)
6
+ .filter(Boolean);
7
+ }
8
+
9
+ function estimateSegmentTokens(segment) {
10
+ const charCount = [...String(segment || '')].length;
11
+ return Math.max(1, Math.ceil(charCount / 4));
12
+ }
13
+
14
+ function estimateTokenCount(text) {
15
+ if (typeof text !== 'string' || !text) return 0;
16
+
17
+ const parts = text.match(/[\p{L}\p{N}_]+|[^\s]/gu) || [];
18
+ let total = 0;
19
+
20
+ for (const part of parts) {
21
+ if (/^[\p{L}\p{N}_]+$/u.test(part)) {
22
+ const segments = splitIdentifierSegments(part);
23
+ total += segments.reduce((sum, segment) => sum + estimateSegmentTokens(segment), 0);
24
+ continue;
25
+ }
26
+
27
+ total += 1;
28
+ }
29
+
30
+ return total;
31
+ }
32
+
33
+ module.exports = {
34
+ estimateTokenCount,
35
+ };
package/src/watch.js CHANGED
@@ -7,6 +7,8 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { audit } = require('./audit');
10
+ const { detectPlatforms } = require('./public-api');
11
+ const { buildContinuousStatus, formatContinuousStatus } = require('./continuous-ops');
10
12
 
11
13
  const COLORS = {
12
14
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
@@ -131,12 +133,14 @@ function closeWatchers(watchers) {
131
133
 
132
134
  async function watch(options) {
133
135
  const recursiveSupported = supportsNativeRecursiveWatch();
136
+ const watchMode = options.driftMode || 'watch';
134
137
 
135
138
  console.log('');
136
139
  console.log(c(' nerviq watch mode', 'bold'));
137
140
  console.log(c(' ═══════════════════════════════════════', 'dim'));
138
141
  console.log(c(` Watching: ${options.dir}`, 'dim'));
139
142
  console.log(c(` Mode: ${recursiveSupported ? 'native recursive directories' : 'expanded directory fallback (cross-platform safe)'}`, 'dim'));
143
+ console.log(c(` Continuous mode: ${watchMode}`, 'dim'));
140
144
  console.log(c(' Press Ctrl+C to stop', 'dim'));
141
145
  console.log('');
142
146
 
@@ -147,6 +151,13 @@ async function watch(options) {
147
151
  lastScore = result.score;
148
152
  console.log(` ${c('Initial score:', 'bold')} ${scoreColor(result.score)}`);
149
153
  console.log(` ${result.passed} / ${result.passed + result.failed} checks passing`);
154
+ const continuousStatus = buildContinuousStatus({
155
+ dir: options.dir,
156
+ auditResult: result,
157
+ mode: watchMode,
158
+ currentPlatforms: detectPlatforms(options.dir),
159
+ });
160
+ console.log(formatContinuousStatus(continuousStatus, { compact: true }));
150
161
  console.log('');
151
162
  } catch (e) {
152
163
  console.log(c(` Initial audit failed: ${e.message}`, 'dim'));
@@ -184,8 +195,15 @@ async function watch(options) {
184
195
  const result = await audit({ ...options, silent: true });
185
196
  const delta = lastScore !== null ? result.score - lastScore : 0;
186
197
  const arrow = delta > 0 ? c(`+${delta}`, 'green') : delta < 0 ? c(String(delta), 'yellow') : '';
198
+ const continuousStatus = buildContinuousStatus({
199
+ dir: options.dir,
200
+ auditResult: result,
201
+ mode: watchMode,
202
+ currentPlatforms: detectPlatforms(options.dir),
203
+ });
187
204
 
188
205
  console.log(` Score: ${scoreColor(result.score)} ${arrow} (${result.passed}/${result.passed + result.failed} passing)`);
206
+ console.log(formatContinuousStatus(continuousStatus, { compact: true }));
189
207
 
190
208
  if (lastScore !== null && result.score > lastScore) {
191
209
  console.log(c(' Nice improvement!', 'green'));
@@ -26,11 +26,12 @@
26
26
  const fs = require('fs');
27
27
  const os = require('os');
28
28
  const path = require('path');
29
- const { WindsurfProjectContext } = require('./context');
30
- const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
31
- const { attachSourceUrls } = require('../source-urls');
32
- const { buildStackChecks } = require('../stack-checks');
33
- const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
29
+ const { WindsurfProjectContext } = require('./context');
30
+ const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
31
+ const { attachSourceUrls } = require('../source-urls');
32
+ const { buildStackChecks } = require('../stack-checks');
33
+ const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
34
+ const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
34
35
  const { tryParseJson, validateMcpEnvVars } = require('./config-parser');
35
36
 
36
37
  // ─── Shared helpers ─────────────────────────────────────────────────────────
@@ -2161,13 +2162,17 @@ const WINDSURF_TECHNIQUES = {
2161
2162
  fix: 'Document prompt caching strategy to reduce Windsurf API costs.',
2162
2163
  template: null, file: () => 'AGENTS.md', line: () => null,
2163
2164
  },
2164
- wsCostBudgetDefined: {
2165
- id: 'WS-T48', name: 'AI cost budget or usage limits documented',
2166
- check: (ctx) => { const docs = (ctx.fileContent('AGENTS.md') || '') + (ctx.fileContent('README.md') || ''); if (!docs.trim()) return null; return /cost.{0,15}budget|spending.{0,15}limit|usage.{0,15}limit/i.test(docs); },
2167
- impact: 'low', rating: 2, category: 'cost-optimization',
2168
- fix: 'Document AI cost budget in README.md.',
2169
- template: null, file: () => 'README.md', line: () => null,
2170
- },
2165
+ wsCostBudgetDefined: {
2166
+ id: 'WS-T48', name: 'AI cost budget or per-run usage tracking documented',
2167
+ check: (ctx) => {
2168
+ const docs = (ctx.fileContent('AGENTS.md') || '') + (ctx.fileContent('README.md') || '');
2169
+ if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
2170
+ return hasCostBudgetOrUsageTracking(docs, ctx);
2171
+ },
2172
+ impact: 'low', rating: 2, category: 'cost-optimization',
2173
+ fix: 'Document AI cost guardrails or per-run usage tracking so Windsurf usage is measurable over time.',
2174
+ template: null, file: () => 'README.md', line: () => null,
2175
+ },
2171
2176
 
2172
2177
  // ============================================================
2173
2178
  // === PYTHON STACK CHECKS (category: 'python') ===============
package/src/workspace.js CHANGED
@@ -177,6 +177,104 @@ function summarizeAuditResult(result, scoreType, scope) {
177
177
  };
178
178
  }
179
179
 
180
+ function summarizeWorkspaceEntry(result, workspacePath, absPath, platform) {
181
+ const stackKeys = (result.stacks || []).map((item) => item.key);
182
+ const stackLabels = (result.stacks || []).map((item) => item.label);
183
+ return {
184
+ name: path.basename(workspacePath),
185
+ workspace: workspacePath,
186
+ dir: absPath,
187
+ platform,
188
+ stackKeys,
189
+ stackLabels,
190
+ workspaceProfile: classifyWorkspaceProfile(stackKeys),
191
+ ...summarizeAuditResult(result, 'workspace-live-audit', 'workspace-package'),
192
+ };
193
+ }
194
+
195
+ function classifyWorkspaceProfile(stackKeys) {
196
+ const keys = new Set(Array.isArray(stackKeys) ? stackKeys : []);
197
+ const matchAny = (candidates) => candidates.some((candidate) => keys.has(candidate));
198
+
199
+ if (matchAny(['go'])) {
200
+ return { key: 'go-workspace', label: 'Go workspace' };
201
+ }
202
+ if (matchAny(['python', 'django', 'fastapi'])) {
203
+ return { key: 'python-workspace', label: 'Python workspace' };
204
+ }
205
+ if (matchAny(['dotnet'])) {
206
+ return { key: 'dotnet-workspace', label: '.NET workspace' };
207
+ }
208
+ if (matchAny(['java', 'spring'])) {
209
+ return { key: 'java-workspace', label: 'Java workspace' };
210
+ }
211
+ if (matchAny(['flutter', 'dart'])) {
212
+ return { key: 'flutter-workspace', label: 'Flutter workspace' };
213
+ }
214
+ if (matchAny(['swift'])) {
215
+ return { key: 'swift-workspace', label: 'Swift workspace' };
216
+ }
217
+ if (matchAny(['kotlin'])) {
218
+ return { key: 'kotlin-workspace', label: 'Kotlin workspace' };
219
+ }
220
+ if (matchAny(['react', 'nextjs', 'node', 'typescript', 'javascript', 'nestjs', 'vue', 'angular', 'svelte'])) {
221
+ return { key: 'node-workspace', label: 'Node / JS workspace' };
222
+ }
223
+
224
+ return { key: 'general-workspace', label: 'General workspace' };
225
+ }
226
+
227
+ function buildProfileBreakdown(results) {
228
+ const grouped = new Map();
229
+
230
+ for (const item of results) {
231
+ const profileKey = item.workspaceProfile?.key || 'general-workspace';
232
+ const profileLabel = item.workspaceProfile?.label || 'General workspace';
233
+ if (!grouped.has(profileKey)) {
234
+ grouped.set(profileKey, {
235
+ profileKey,
236
+ profileLabel,
237
+ scoreType: 'workspace-live-audit',
238
+ workspaceCount: 0,
239
+ workspaces: [],
240
+ stackLabels: new Set(),
241
+ scores: [],
242
+ totals: [],
243
+ });
244
+ }
245
+
246
+ const entry = grouped.get(profileKey);
247
+ entry.workspaceCount += 1;
248
+ entry.workspaces.push(item.workspace);
249
+ for (const label of item.stackLabels || []) {
250
+ entry.stackLabels.add(label);
251
+ }
252
+ if (typeof item.score === 'number') {
253
+ entry.scores.push(item.score);
254
+ }
255
+ if (typeof item.total === 'number') {
256
+ entry.totals.push(item.total);
257
+ }
258
+ }
259
+
260
+ return [...grouped.values()]
261
+ .map((entry) => ({
262
+ profileKey: entry.profileKey,
263
+ profileLabel: entry.profileLabel,
264
+ scoreType: 'workspace-live-audit',
265
+ workspaceCount: entry.workspaceCount,
266
+ averageScore: entry.scores.length > 0
267
+ ? Math.round(entry.scores.reduce((sum, value) => sum + value, 0) / entry.scores.length)
268
+ : 0,
269
+ averageTotal: entry.totals.length > 0
270
+ ? Math.round(entry.totals.reduce((sum, value) => sum + value, 0) / entry.totals.length)
271
+ : 0,
272
+ stackLabels: [...entry.stackLabels].sort(),
273
+ workspaces: entry.workspaces.sort(),
274
+ }))
275
+ .sort((left, right) => left.profileLabel.localeCompare(right.profileLabel));
276
+ }
277
+
180
278
  async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
181
279
  const { audit } = require('./audit');
182
280
  const rootDir = path.resolve(dir);
@@ -207,14 +305,7 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
207
305
  const absPath = path.join(rootDir, workspacePath);
208
306
  try {
209
307
  const result = await audit({ dir: absPath, platform, silent: true });
210
- results.push({
211
- name: path.basename(workspacePath),
212
- workspace: workspacePath,
213
- dir: absPath,
214
- platform,
215
- ...summarizeAuditResult(result, 'workspace-live-audit', 'workspace-package'),
216
- result,
217
- });
308
+ results.push(summarizeWorkspaceEntry(result, workspacePath, absPath, platform));
218
309
  } catch (error) {
219
310
  results.push({
220
311
  name: path.basename(workspacePath),
@@ -227,6 +318,9 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
227
318
  passed: 0,
228
319
  total: 0,
229
320
  topAction: null,
321
+ stackKeys: [],
322
+ stackLabels: [],
323
+ workspaceProfile: { key: 'general-workspace', label: 'General workspace' },
230
324
  error: error.message,
231
325
  });
232
326
  }
@@ -238,6 +332,7 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
238
332
  : 0;
239
333
  const maxScore = validScores.length > 0 ? Math.max(...validScores) : 0;
240
334
  const minScore = validScores.length > 0 ? Math.min(...validScores) : 0;
335
+ const profileBreakdown = buildProfileBreakdown(results);
241
336
 
242
337
  return {
243
338
  summaryType: 'monorepo-workspace-audit',
@@ -254,10 +349,12 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
254
349
  maxScore,
255
350
  minScore,
256
351
  },
352
+ profileBreakdown,
257
353
  scoreSemantics: {
258
354
  rootGovernance: 'Root repo live audit for shared instructions, hooks, permissions, and top-level governance files.',
259
355
  workspaceAggregate: 'Average of the selected workspace live audit scores. This is a package coverage rollup, not the root repo score.',
260
356
  workspaceEntries: 'Each workspace row is a package-level live audit. Package scores can differ from the root governance score for legitimate reasons.',
357
+ workspaceProfiles: 'Workspace totals can differ because each package uses a stack-specific check profile based on detected languages and frameworks.',
261
358
  },
262
359
  workspaces: results,
263
360
  detectedWorkspaces: workspacePaths,