@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,86 @@
1
+ 'use strict';
2
+
3
+ const claudeFreshness = require('./freshness');
4
+ const codexFreshness = require('./codex/freshness');
5
+ const cursorFreshness = require('./cursor/freshness');
6
+ const copilotFreshness = require('./copilot/freshness');
7
+ const geminiFreshness = require('./gemini/freshness');
8
+ const windsurfFreshness = require('./windsurf/freshness');
9
+ const aiderFreshness = require('./aider/freshness');
10
+ const opencodeFreshness = require('./opencode/freshness');
11
+
12
+ const DAILY_FRESHNESS_WORKFLOW = {
13
+ workflow: '.github/workflows/freshness-check.yml',
14
+ cadence: 'Daily at 06:00 UTC plus manual dispatch',
15
+ issuePolicy: 'Open or refresh GitHub issues for stale P0 sources without failing the main CI pipeline.',
16
+ };
17
+
18
+ const PLATFORM_CHANGE_MANIFEST = [
19
+ { key: 'claude', label: 'Claude Code', modulePath: 'src/freshness.js', freshness: claudeFreshness },
20
+ { key: 'codex', label: 'Codex', modulePath: 'src/codex/freshness.js', freshness: codexFreshness },
21
+ { key: 'cursor', label: 'Cursor', modulePath: 'src/cursor/freshness.js', freshness: cursorFreshness },
22
+ { key: 'copilot', label: 'Copilot', modulePath: 'src/copilot/freshness.js', freshness: copilotFreshness },
23
+ { key: 'gemini', label: 'Gemini CLI', modulePath: 'src/gemini/freshness.js', freshness: geminiFreshness },
24
+ { key: 'windsurf', label: 'Windsurf', modulePath: 'src/windsurf/freshness.js', freshness: windsurfFreshness },
25
+ { key: 'aider', label: 'Aider', modulePath: 'src/aider/freshness.js', freshness: aiderFreshness },
26
+ { key: 'opencode', label: 'OpenCode', modulePath: 'src/opencode/freshness.js', freshness: opencodeFreshness },
27
+ ].map((entry) => {
28
+ const sources = (entry.freshness.P0_SOURCES || []).map((source) => ({
29
+ key: source.key,
30
+ label: source.label,
31
+ url: source.url,
32
+ stalenessThresholdDays: source.stalenessThresholdDays,
33
+ verifiedAt: source.verifiedAt || null,
34
+ }));
35
+ const thresholds = [...new Set(sources.map((source) => source.stalenessThresholdDays))].sort((a, b) => a - b);
36
+
37
+ return {
38
+ key: entry.key,
39
+ label: entry.label,
40
+ modulePath: entry.modulePath,
41
+ trackedSources: sources,
42
+ trackedSourceCount: sources.length,
43
+ reviewCadence: {
44
+ automation: DAILY_FRESHNESS_WORKFLOW.cadence,
45
+ thresholdsDays: thresholds,
46
+ manualExpectation: thresholds.includes(14)
47
+ ? 'Review high-volatility sources weekly and verify any stale source immediately.'
48
+ : 'Review sources at least monthly and verify any stale source immediately.',
49
+ },
50
+ freshnessWorkflow: {
51
+ ...DAILY_FRESHNESS_WORKFLOW,
52
+ manualTrigger: true,
53
+ },
54
+ updateTriggers: (entry.freshness.PROPAGATION_CHECKLIST || []).map((item) => ({
55
+ trigger: item.trigger,
56
+ targets: item.targets || [],
57
+ })),
58
+ };
59
+ });
60
+
61
+ function getPlatformChangeManifest() {
62
+ return JSON.parse(JSON.stringify(PLATFORM_CHANGE_MANIFEST));
63
+ }
64
+
65
+ function summarizePlatformChangeManifest() {
66
+ const manifest = getPlatformChangeManifest();
67
+ return {
68
+ platformCount: manifest.length,
69
+ trackedSourceCount: manifest.reduce((sum, entry) => sum + entry.trackedSourceCount, 0),
70
+ workflow: DAILY_FRESHNESS_WORKFLOW,
71
+ platforms: manifest.map((entry) => ({
72
+ key: entry.key,
73
+ label: entry.label,
74
+ trackedSourceCount: entry.trackedSourceCount,
75
+ thresholdDays: entry.reviewCadence.thresholdsDays,
76
+ updateTriggerCount: entry.updateTriggers.length,
77
+ })),
78
+ };
79
+ }
80
+
81
+ module.exports = {
82
+ DAILY_FRESHNESS_WORKFLOW,
83
+ PLATFORM_CHANGE_MANIFEST,
84
+ getPlatformChangeManifest,
85
+ summarizePlatformChangeManifest,
86
+ };
@@ -0,0 +1,210 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { applyProfileToOptions } = require('./profiles');
4
+
5
+ const POLICY_FILES = {
6
+ org: path.join('.nerviq', 'org-policy.json'),
7
+ team: path.join('.nerviq', 'team-policy.json'),
8
+ repo: path.join('.nerviq', 'repo-policy.json'),
9
+ };
10
+
11
+ function normalizeArray(values) {
12
+ const seen = new Set();
13
+ const result = [];
14
+ for (const value of Array.isArray(values) ? values : []) {
15
+ const normalized = `${value || ''}`.trim();
16
+ if (!normalized || seen.has(normalized)) continue;
17
+ seen.add(normalized);
18
+ result.push(normalized);
19
+ }
20
+ return result;
21
+ }
22
+
23
+ function readPolicyFile(filePath, layer) {
24
+ if (!filePath || !fs.existsSync(filePath)) return null;
25
+
26
+ let raw;
27
+ try {
28
+ raw = fs.readFileSync(filePath, 'utf8');
29
+ } catch {
30
+ return {
31
+ layer,
32
+ path: filePath,
33
+ valid: false,
34
+ error: 'could not read policy file',
35
+ };
36
+ }
37
+
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(raw);
41
+ } catch {
42
+ return {
43
+ layer,
44
+ path: filePath,
45
+ valid: false,
46
+ error: 'invalid JSON',
47
+ };
48
+ }
49
+
50
+ return {
51
+ layer,
52
+ path: filePath,
53
+ valid: true,
54
+ policy: {
55
+ name: parsed.name || `${layer} policy`,
56
+ description: parsed.description || '',
57
+ platforms: normalizeArray(parsed.platforms || []),
58
+ threshold: parsed.threshold != null ? Number(parsed.threshold) : null,
59
+ requireChecks: normalizeArray(parsed.requireChecks || []),
60
+ suppressedChecks: normalizeArray(parsed.suppressedChecks || []),
61
+ priorityBoosts: normalizeArray(parsed.priorityBoosts || []),
62
+ customWeights: parsed.customWeights && typeof parsed.customWeights === 'object' ? parsed.customWeights : {},
63
+ },
64
+ };
65
+ }
66
+
67
+ function findAncestorOrgPolicy(dir) {
68
+ let current = path.resolve(dir);
69
+ let lastFound = null;
70
+
71
+ while (true) {
72
+ const candidate = path.join(current, POLICY_FILES.org);
73
+ if (fs.existsSync(candidate)) {
74
+ lastFound = candidate;
75
+ }
76
+ const parent = path.dirname(current);
77
+ if (parent === current) break;
78
+ current = parent;
79
+ }
80
+
81
+ return lastFound;
82
+ }
83
+
84
+ function resolvePolicyLayers(dir) {
85
+ const absoluteDir = path.resolve(dir);
86
+ const layers = [];
87
+
88
+ const orgLayer = readPolicyFile(findAncestorOrgPolicy(absoluteDir), 'org');
89
+ if (orgLayer) layers.push(orgLayer);
90
+
91
+ const teamLayer = readPolicyFile(path.join(absoluteDir, POLICY_FILES.team), 'team');
92
+ if (teamLayer) layers.push(teamLayer);
93
+
94
+ const repoLayer = readPolicyFile(path.join(absoluteDir, POLICY_FILES.repo), 'repo');
95
+ if (repoLayer) layers.push(repoLayer);
96
+
97
+ const resolved = {
98
+ platforms: [],
99
+ threshold: null,
100
+ requireChecks: [],
101
+ suppressedChecks: [],
102
+ priorityBoosts: [],
103
+ customWeights: {},
104
+ description: '',
105
+ };
106
+
107
+ const fieldSources = {
108
+ platforms: [],
109
+ threshold: null,
110
+ requireChecks: [],
111
+ suppressedChecks: [],
112
+ priorityBoosts: [],
113
+ customWeights: [],
114
+ description: null,
115
+ };
116
+
117
+ for (const layer of layers) {
118
+ if (!layer.valid || !layer.policy) continue;
119
+ const { policy } = layer;
120
+
121
+ if (policy.platforms.length > 0) {
122
+ resolved.platforms = [...policy.platforms];
123
+ fieldSources.platforms = [layer.layer];
124
+ }
125
+
126
+ if (policy.threshold != null && Number.isFinite(policy.threshold)) {
127
+ resolved.threshold = policy.threshold;
128
+ fieldSources.threshold = layer.layer;
129
+ }
130
+
131
+ if (policy.requireChecks.length > 0) {
132
+ resolved.requireChecks = normalizeArray([...resolved.requireChecks, ...policy.requireChecks]);
133
+ fieldSources.requireChecks = normalizeArray([...fieldSources.requireChecks, layer.layer]);
134
+ }
135
+
136
+ if (policy.suppressedChecks.length > 0) {
137
+ resolved.suppressedChecks = normalizeArray([...resolved.suppressedChecks, ...policy.suppressedChecks]);
138
+ fieldSources.suppressedChecks = normalizeArray([...fieldSources.suppressedChecks, layer.layer]);
139
+ }
140
+
141
+ if (policy.priorityBoosts.length > 0) {
142
+ resolved.priorityBoosts = normalizeArray([...resolved.priorityBoosts, ...policy.priorityBoosts]);
143
+ fieldSources.priorityBoosts = normalizeArray([...fieldSources.priorityBoosts, layer.layer]);
144
+ }
145
+
146
+ if (Object.keys(policy.customWeights).length > 0) {
147
+ resolved.customWeights = { ...resolved.customWeights, ...policy.customWeights };
148
+ fieldSources.customWeights = normalizeArray([...fieldSources.customWeights, layer.layer]);
149
+ }
150
+
151
+ if (policy.description) {
152
+ resolved.description = policy.description;
153
+ fieldSources.description = layer.layer;
154
+ }
155
+ }
156
+
157
+ return {
158
+ dir: absoluteDir,
159
+ overrideOrder: ['org', 'team', 'repo', 'explicit-cli'],
160
+ layers,
161
+ resolved,
162
+ fieldSources,
163
+ };
164
+ }
165
+
166
+ function applyPolicyLayersToOptions(contract, options) {
167
+ if (!contract || !contract.resolved) {
168
+ return { ...options };
169
+ }
170
+
171
+ return applyProfileToOptions(contract.resolved, options);
172
+ }
173
+
174
+ function formatPolicyContract(contract) {
175
+ if (!contract || !Array.isArray(contract.layers) || contract.layers.length === 0) {
176
+ return ' No org/team/repo policy layers found.';
177
+ }
178
+
179
+ const lines = [
180
+ ' Policy layers (override order: org -> team -> repo -> explicit CLI):',
181
+ ];
182
+
183
+ for (const layer of contract.layers) {
184
+ if (!layer.valid) {
185
+ lines.push(` - ${layer.layer}: ${layer.path} [invalid: ${layer.error}]`);
186
+ continue;
187
+ }
188
+
189
+ const policy = layer.policy || {};
190
+ const details = [];
191
+ if (policy.platforms?.length > 0) details.push(`platforms=${policy.platforms.join(', ')}`);
192
+ if (policy.threshold != null) details.push(`threshold=${policy.threshold}`);
193
+ if (policy.requireChecks?.length > 0) details.push(`require=${policy.requireChecks.join(', ')}`);
194
+ lines.push(` - ${layer.layer}: ${path.relative(contract.dir, layer.path) || layer.path}${details.length > 0 ? ` (${details.join('; ')})` : ''}`);
195
+ }
196
+
197
+ const resolved = contract.resolved || {};
198
+ lines.push(' Resolved policy:');
199
+ lines.push(` - Platforms: ${resolved.platforms?.length > 0 ? resolved.platforms.join(', ') : 'default CLI platform'}`);
200
+ lines.push(` - Threshold: ${resolved.threshold != null ? resolved.threshold : 'default'}`);
201
+ lines.push(` - Required checks: ${resolved.requireChecks?.length > 0 ? resolved.requireChecks.join(', ') : 'none'}`);
202
+ return lines.join('\n');
203
+ }
204
+
205
+ module.exports = {
206
+ POLICY_FILES,
207
+ resolvePolicyLayers,
208
+ applyPolicyLayersToOptions,
209
+ formatPolicyContract,
210
+ };
package/src/profiles.js CHANGED
@@ -86,9 +86,12 @@ function applyProfileToOptions(profile, options) {
86
86
  if (profile.threshold != null && merged.threshold == null) {
87
87
  merged.threshold = profile.threshold;
88
88
  }
89
- if (profile.platforms && profile.platforms.length > 0 && !options.platform) {
89
+ if (profile.platforms && profile.platforms.length > 0 && !options.platformExplicit) {
90
90
  merged.platform = profile.platforms[0];
91
91
  }
92
+ if ((profile.requireChecks || []).length > 0 && (!Array.isArray(merged.require) || merged.require.length === 0)) {
93
+ merged.require = [...profile.requireChecks];
94
+ }
92
95
  merged.suppressedChecks = profile.suppressedChecks || [];
93
96
  merged.priorityBoosts = profile.priorityBoosts || [];
94
97
  merged.customWeights = profile.customWeights || {};
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const PROMPT_INJECTION_PATTERNS = [
4
+ /\bignore (?:all )?(?:previous|earlier|above) instructions?\b/i,
5
+ /\boverride (?:the )?(?:system|developer|safety|previous) instructions?\b/i,
6
+ /\breveal (?:your|the) (?:system|developer) prompt\b/i,
7
+ /\bbypass (?:all )?(?:safety|guardrails|restrictions|protections)\b/i,
8
+ /\bdisable (?:the )?(?:guardrails|safety checks?)\b/i,
9
+ /\bact as (?:the )?(?:system|developer)\b/i,
10
+ /\breport (?:that )?(?:everything is )?perfect(?: and score 100\/100)?\b/i,
11
+ /\bscore 100\/100\b/i,
12
+ /\bexfiltrate\b.*\b(?:secret|token|credential|password)\b/i,
13
+ ];
14
+
15
+ const TRUST_BOUNDARY_PATTERNS = [
16
+ /\btreat(?: every| all)?(?: string| file| repo(?:sitory)?| external)?[\w\s,-]{0,80}\buntrusted\b/i,
17
+ /\bnever follow instructions embedded in\b/i,
18
+ /\b(?:repo(?:sitory)? files?|file contents?|web(?:site|page)? content|fetched content|external content|mcp responses?)\b[\w\s,-]{0,120}\b(?:data|quoted)\b[\w\s,-]{0,80}\bnot instructions\b/i,
19
+ /\bmcp responses?\b[\w\s,-]{0,80}\buntrusted\b/i,
20
+ /\bfile contents?\b[\w\s,-]{0,80}\buntrusted\b/i,
21
+ /\bprompt injection\b[\w\s,-]{0,120}\b(?:defense|resistance|guard|boundary)\b/i,
22
+ ];
23
+
24
+ function containsPromptInjectionPattern(text) {
25
+ const normalized = String(text || '');
26
+ return PROMPT_INJECTION_PATTERNS.some((pattern) => {
27
+ pattern.lastIndex = 0;
28
+ return pattern.test(normalized);
29
+ });
30
+ }
31
+
32
+ function hasPromptInjectionDefenseGuidance(text) {
33
+ const normalized = String(text || '');
34
+ if (!normalized.trim()) return false;
35
+ return TRUST_BOUNDARY_PATTERNS.some((pattern) => {
36
+ pattern.lastIndex = 0;
37
+ return pattern.test(normalized);
38
+ });
39
+ }
40
+
41
+ function hasMcpPromptInjectionDefenseGuidance(text) {
42
+ const normalized = String(text || '');
43
+ if (!/\bmcp\b/i.test(normalized)) return false;
44
+ return hasPromptInjectionDefenseGuidance(normalized);
45
+ }
46
+
47
+ function collectHookBlocks(settings) {
48
+ if (!settings || !settings.hooks || typeof settings.hooks !== 'object') return [];
49
+ const blocks = [];
50
+ for (const [eventName, entries] of Object.entries(settings.hooks)) {
51
+ if (!Array.isArray(entries)) continue;
52
+ for (const entry of entries) {
53
+ blocks.push({ eventName, entry });
54
+ }
55
+ }
56
+ return blocks;
57
+ }
58
+
59
+ function hasInjectionDefenseHookConfigured(settings) {
60
+ return collectHookBlocks(settings).some(({ eventName, entry }) => {
61
+ if (eventName !== 'PostToolUse') return false;
62
+ const matcher = `${entry && entry.matcher ? entry.matcher : ''}`;
63
+ if (!/WebFetch|WebSearch|Read|Grep|Glob|mcp__/i.test(matcher)) return false;
64
+ const hooks = Array.isArray(entry && entry.hooks) ? entry.hooks : [];
65
+ return hooks.some((hook) => /injection|prompt|sanitize|untrusted/i.test(`${hook && hook.command ? hook.command : ''}`));
66
+ });
67
+ }
68
+
69
+ module.exports = {
70
+ containsPromptInjectionPattern,
71
+ hasPromptInjectionDefenseGuidance,
72
+ hasMcpPromptInjectionDefenseGuidance,
73
+ hasInjectionDefenseHookConfigured,
74
+ };