@nerviq/cli 1.29.0 → 1.30.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 (93) hide show
  1. package/CHANGELOG.md +1764 -1493
  2. package/README.md +568 -538
  3. package/SECURITY.md +78 -82
  4. package/bin/cli.js +2838 -2558
  5. package/docs/api-reference.md +356 -356
  6. package/docs/audit-fix.md +109 -0
  7. package/docs/autofix.md +3 -62
  8. package/docs/getting-started.md +1 -1
  9. package/docs/index.html +592 -592
  10. package/docs/integration-contracts.md +287 -287
  11. package/docs/maintenance.md +128 -128
  12. package/docs/new-platform-guide.md +202 -202
  13. package/docs/release-process.md +63 -0
  14. package/docs/shallow-risk.md +244 -244
  15. package/docs/why-nerviq.md +82 -82
  16. package/package.json +75 -67
  17. package/sdk/README.md +12 -3
  18. package/sdk/examples/langchain-integration.md +128 -0
  19. package/sdk/examples/self-governing-agent.js +135 -0
  20. package/sdk/index.d.ts +115 -0
  21. package/sdk/index.js +94 -0
  22. package/sdk/package.json +11 -0
  23. package/src/activity.js +13 -0
  24. package/src/aider/activity.js +226 -226
  25. package/src/aider/context.js +162 -162
  26. package/src/aider/freshness.js +123 -123
  27. package/src/aider/techniques.js +3465 -3465
  28. package/src/audit/layers.js +180 -180
  29. package/src/audit.js +1133 -1032
  30. package/src/auto-suggest.js +9 -2
  31. package/src/behavioral-drift.js +37 -2
  32. package/src/benchmark.js +299 -299
  33. package/src/codex/activity.js +324 -324
  34. package/src/codex/freshness.js +149 -142
  35. package/src/codex/techniques.js +4895 -4895
  36. package/src/context.js +326 -326
  37. package/src/continuous-ops.js +11 -1
  38. package/src/convert.js +340 -340
  39. package/src/copilot/config-parser.js +280 -280
  40. package/src/copilot/context.js +218 -218
  41. package/src/copilot/freshness.js +184 -177
  42. package/src/copilot/patch.js +238 -238
  43. package/src/copilot/techniques.js +3578 -3578
  44. package/src/cursor/freshness.js +194 -194
  45. package/src/cursor/patch.js +243 -243
  46. package/src/cursor/techniques.js +3735 -3735
  47. package/src/doctor.js +201 -201
  48. package/src/fix-engine.js +511 -8
  49. package/src/formatters/csv.js +86 -86
  50. package/src/formatters/junit.js +123 -123
  51. package/src/formatters/markdown.js +164 -164
  52. package/src/formatters/otel.js +151 -151
  53. package/src/freshness.js +163 -156
  54. package/src/gemini/activity.js +402 -402
  55. package/src/gemini/context.js +290 -290
  56. package/src/gemini/freshness.js +188 -188
  57. package/src/gemini/patch.js +229 -229
  58. package/src/gemini/techniques.js +3811 -3811
  59. package/src/governance.js +533 -533
  60. package/src/harmony/audit.js +306 -306
  61. package/src/i18n.js +63 -63
  62. package/src/insights.js +119 -119
  63. package/src/integrations.js +134 -134
  64. package/src/locales/en.json +33 -33
  65. package/src/locales/es.json +33 -33
  66. package/src/migrate.js +354 -354
  67. package/src/opencode/activity.js +286 -286
  68. package/src/opencode/freshness.js +137 -137
  69. package/src/opencode/techniques.js +3450 -3450
  70. package/src/safe-glyph.js +97 -0
  71. package/src/setup/analysis.js +12 -12
  72. package/src/setup.js +13 -6
  73. package/src/shallow-risk/index.js +113 -56
  74. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +51 -50
  75. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +47 -46
  76. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +47 -46
  77. package/src/shallow-risk/patterns/agent-config-framework-version-mismatch.js +138 -0
  78. package/src/shallow-risk/patterns/agent-config-missing-file.js +318 -317
  79. package/src/shallow-risk/patterns/agent-config-script-not-in-package-json.js +108 -0
  80. package/src/shallow-risk/patterns/agent-config-secret-literal.js +52 -49
  81. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +35 -34
  82. package/src/shallow-risk/patterns/hook-script-missing.js +71 -70
  83. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +53 -52
  84. package/src/shallow-risk/shared.js +653 -648
  85. package/src/source-urls.js +295 -295
  86. package/src/state-paths.js +85 -85
  87. package/src/supplemental-checks.js +805 -805
  88. package/src/telemetry.js +160 -160
  89. package/src/watch.js +46 -0
  90. package/src/windsurf/context.js +359 -359
  91. package/src/windsurf/freshness.js +194 -194
  92. package/src/windsurf/patch.js +231 -231
  93. package/src/windsurf/techniques.js +3779 -3779
package/src/telemetry.js CHANGED
@@ -1,160 +1,160 @@
1
- /**
2
- * Nerviq Opt-In Telemetry Foundation
3
- *
4
- * Collects anonymous usage events ONLY when NERVIQ_TELEMETRY=1 is set.
5
- * No PII, no file contents, no absolute paths are ever stored.
6
- * Events are stored locally in <projectDir>/.nerviq/telemetry.json.
7
- *
8
- * This module is the foundation layer — actual transmission to a dashboard
9
- * is an explicit opt-in step configured separately.
10
- *
11
- * Privacy guarantees:
12
- * - No usernames, emails, or identifiers
13
- * - No file contents or code
14
- * - No absolute paths (only hashed project fingerprint)
15
- * - Stored only on local disk
16
- * - Never sent anywhere without additional explicit configuration
17
- */
18
-
19
- 'use strict';
20
-
21
- const fs = require('fs');
22
- const path = require('path');
23
- const os = require('os');
24
- const crypto = require('crypto');
25
-
26
- const TELEMETRY_FILE = path.join(os.homedir(), '.nerviq', 'telemetry.json');
27
- const MAX_EVENTS = 500; // cap file size at ~500 events
28
-
29
- // ─── Opt-in check ─────────────────────────────────────────────────────────────
30
-
31
- /**
32
- * Returns true only when the user has explicitly set NERVIQ_TELEMETRY=1.
33
- * Telemetry is opt-IN, not opt-out.
34
- * @returns {boolean}
35
- */
36
- function shouldCollectTelemetry() {
37
- return process.env.NERVIQ_TELEMETRY === '1';
38
- }
39
-
40
- // ─── Anonymous fingerprinting ─────────────────────────────────────────────────
41
-
42
- /**
43
- * Creates a one-way hash of the project directory.
44
- * This allows grouping events by project without exposing the path.
45
- * @param {string} dir
46
- * @returns {string} 8-char hex fingerprint
47
- */
48
- function hashProject(dir) {
49
- try {
50
- return crypto.createHash('sha256').update(dir).digest('hex').slice(0, 8);
51
- } catch {
52
- return 'unknown';
53
- }
54
- }
55
-
56
- // ─── Event collection ────────────────────────────────────────────────────────
57
-
58
- /**
59
- * Collect an anonymous usage event and append it to the local telemetry file.
60
- * Does nothing unless shouldCollectTelemetry() returns true.
61
- *
62
- * @param {string} event - Event name (e.g. 'audit', 'setup', 'convert')
63
- * @param {object} [data] - Additional anonymous data
64
- * @param {string} [data.platform] - Platform name (claude, codex, etc.)
65
- * @param {number} [data.score] - Audit score
66
- * @param {number} [data.checkCount] - Total checks evaluated
67
- * @param {number} [data.durationMs] - Execution time in ms
68
- * @param {string} [data.dir] - Project dir (hashed before storage)
69
- * @returns {object|null} The recorded event object, or null if telemetry is off
70
- */
71
- function collectAnonymousEvent(event, data = {}) {
72
- if (!shouldCollectTelemetry()) return null;
73
-
74
- const record = {
75
- event: String(event),
76
- platform: data.platform || null,
77
- score: typeof data.score === 'number' ? data.score : null,
78
- checkCount: typeof data.checkCount === 'number' ? data.checkCount : null,
79
- durationMs: typeof data.durationMs === 'number' ? Math.round(data.durationMs) : null,
80
- timestamp: new Date().toISOString(),
81
- nodeVersion: process.version,
82
- os: `${os.platform()}-${os.arch()}`,
83
- projectFingerprint: data.dir ? hashProject(data.dir) : null,
84
- // Explicitly omit: paths, file contents, usernames, email, tokens
85
- };
86
-
87
- try {
88
- const telemetryDir = path.dirname(TELEMETRY_FILE);
89
- fs.mkdirSync(telemetryDir, { recursive: true });
90
-
91
- let events = [];
92
- if (fs.existsSync(TELEMETRY_FILE)) {
93
- try {
94
- const raw = fs.readFileSync(TELEMETRY_FILE, 'utf8');
95
- const parsed = JSON.parse(raw);
96
- events = Array.isArray(parsed.events) ? parsed.events : [];
97
- } catch {
98
- events = [];
99
- }
100
- }
101
-
102
- events.push(record);
103
-
104
- // Cap at MAX_EVENTS to prevent unbounded growth
105
- if (events.length > MAX_EVENTS) {
106
- events = events.slice(events.length - MAX_EVENTS);
107
- }
108
-
109
- const payload = {
110
- version: 1,
111
- telemetryOptIn: true,
112
- note: 'Local telemetry only. Set NERVIQ_TELEMETRY=0 or unset to disable.',
113
- events,
114
- };
115
-
116
- fs.writeFileSync(TELEMETRY_FILE, JSON.stringify(payload, null, 2), 'utf8');
117
- } catch {
118
- // Telemetry failures are always silent — never block main flow
119
- }
120
-
121
- return record;
122
- }
123
-
124
- // ─── Read local telemetry ─────────────────────────────────────────────────────
125
-
126
- /**
127
- * Read the local telemetry file.
128
- * @returns {{ version: number, events: object[] } | null}
129
- */
130
- function readLocalTelemetry() {
131
- try {
132
- if (!fs.existsSync(TELEMETRY_FILE)) return null;
133
- return JSON.parse(fs.readFileSync(TELEMETRY_FILE, 'utf8'));
134
- } catch {
135
- return null;
136
- }
137
- }
138
-
139
- /**
140
- * Clear all local telemetry events.
141
- * @returns {boolean} true if cleared successfully
142
- */
143
- function clearLocalTelemetry() {
144
- try {
145
- if (fs.existsSync(TELEMETRY_FILE)) {
146
- fs.writeFileSync(TELEMETRY_FILE, JSON.stringify({ version: 1, events: [] }, null, 2), 'utf8');
147
- }
148
- return true;
149
- } catch {
150
- return false;
151
- }
152
- }
153
-
154
- module.exports = {
155
- shouldCollectTelemetry,
156
- collectAnonymousEvent,
157
- readLocalTelemetry,
158
- clearLocalTelemetry,
159
- TELEMETRY_FILE,
160
- };
1
+ /**
2
+ * Nerviq Opt-In Telemetry Foundation
3
+ *
4
+ * Collects anonymous usage events ONLY when NERVIQ_TELEMETRY=1 is set.
5
+ * No PII, no file contents, no absolute paths are ever stored.
6
+ * Events are stored locally in <projectDir>/.nerviq/telemetry.json.
7
+ *
8
+ * This module is the foundation layer — actual transmission to a dashboard
9
+ * is an explicit opt-in step configured separately.
10
+ *
11
+ * Privacy guarantees:
12
+ * - No usernames, emails, or identifiers
13
+ * - No file contents or code
14
+ * - No absolute paths (only hashed project fingerprint)
15
+ * - Stored only on local disk
16
+ * - Never sent anywhere without additional explicit configuration
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const os = require('os');
24
+ const crypto = require('crypto');
25
+
26
+ const TELEMETRY_FILE = path.join(os.homedir(), '.nerviq', 'telemetry.json');
27
+ const MAX_EVENTS = 500; // cap file size at ~500 events
28
+
29
+ // ─── Opt-in check ─────────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Returns true only when the user has explicitly set NERVIQ_TELEMETRY=1.
33
+ * Telemetry is opt-IN, not opt-out.
34
+ * @returns {boolean}
35
+ */
36
+ function shouldCollectTelemetry() {
37
+ return process.env.NERVIQ_TELEMETRY === '1';
38
+ }
39
+
40
+ // ─── Anonymous fingerprinting ─────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Creates a one-way hash of the project directory.
44
+ * This allows grouping events by project without exposing the path.
45
+ * @param {string} dir
46
+ * @returns {string} 8-char hex fingerprint
47
+ */
48
+ function hashProject(dir) {
49
+ try {
50
+ return crypto.createHash('sha256').update(dir).digest('hex').slice(0, 8);
51
+ } catch {
52
+ return 'unknown';
53
+ }
54
+ }
55
+
56
+ // ─── Event collection ────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Collect an anonymous usage event and append it to the local telemetry file.
60
+ * Does nothing unless shouldCollectTelemetry() returns true.
61
+ *
62
+ * @param {string} event - Event name (e.g. 'audit', 'setup', 'convert')
63
+ * @param {object} [data] - Additional anonymous data
64
+ * @param {string} [data.platform] - Platform name (claude, codex, etc.)
65
+ * @param {number} [data.score] - Audit score
66
+ * @param {number} [data.checkCount] - Total checks evaluated
67
+ * @param {number} [data.durationMs] - Execution time in ms
68
+ * @param {string} [data.dir] - Project dir (hashed before storage)
69
+ * @returns {object|null} The recorded event object, or null if telemetry is off
70
+ */
71
+ function collectAnonymousEvent(event, data = {}) {
72
+ if (!shouldCollectTelemetry()) return null;
73
+
74
+ const record = {
75
+ event: String(event),
76
+ platform: data.platform || null,
77
+ score: typeof data.score === 'number' ? data.score : null,
78
+ checkCount: typeof data.checkCount === 'number' ? data.checkCount : null,
79
+ durationMs: typeof data.durationMs === 'number' ? Math.round(data.durationMs) : null,
80
+ timestamp: new Date().toISOString(),
81
+ nodeVersion: process.version,
82
+ os: `${os.platform()}-${os.arch()}`,
83
+ projectFingerprint: data.dir ? hashProject(data.dir) : null,
84
+ // Explicitly omit: paths, file contents, usernames, email, tokens
85
+ };
86
+
87
+ try {
88
+ const telemetryDir = path.dirname(TELEMETRY_FILE);
89
+ fs.mkdirSync(telemetryDir, { recursive: true });
90
+
91
+ let events = [];
92
+ if (fs.existsSync(TELEMETRY_FILE)) {
93
+ try {
94
+ const raw = fs.readFileSync(TELEMETRY_FILE, 'utf8');
95
+ const parsed = JSON.parse(raw);
96
+ events = Array.isArray(parsed.events) ? parsed.events : [];
97
+ } catch {
98
+ events = [];
99
+ }
100
+ }
101
+
102
+ events.push(record);
103
+
104
+ // Cap at MAX_EVENTS to prevent unbounded growth
105
+ if (events.length > MAX_EVENTS) {
106
+ events = events.slice(events.length - MAX_EVENTS);
107
+ }
108
+
109
+ const payload = {
110
+ version: 1,
111
+ telemetryOptIn: true,
112
+ note: 'Local telemetry only. Set NERVIQ_TELEMETRY=0 or unset to disable.',
113
+ events,
114
+ };
115
+
116
+ fs.writeFileSync(TELEMETRY_FILE, JSON.stringify(payload, null, 2), 'utf8');
117
+ } catch {
118
+ // Telemetry failures are always silent — never block main flow
119
+ }
120
+
121
+ return record;
122
+ }
123
+
124
+ // ─── Read local telemetry ─────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Read the local telemetry file.
128
+ * @returns {{ version: number, events: object[] } | null}
129
+ */
130
+ function readLocalTelemetry() {
131
+ try {
132
+ if (!fs.existsSync(TELEMETRY_FILE)) return null;
133
+ return JSON.parse(fs.readFileSync(TELEMETRY_FILE, 'utf8'));
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Clear all local telemetry events.
141
+ * @returns {boolean} true if cleared successfully
142
+ */
143
+ function clearLocalTelemetry() {
144
+ try {
145
+ if (fs.existsSync(TELEMETRY_FILE)) {
146
+ fs.writeFileSync(TELEMETRY_FILE, JSON.stringify({ version: 1, events: [] }, null, 2), 'utf8');
147
+ }
148
+ return true;
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ module.exports = {
155
+ shouldCollectTelemetry,
156
+ collectAnonymousEvent,
157
+ readLocalTelemetry,
158
+ clearLocalTelemetry,
159
+ TELEMETRY_FILE,
160
+ };
package/src/watch.js CHANGED
@@ -144,6 +144,40 @@ async function watch(options) {
144
144
  console.log(c(' Press Ctrl+C to stop', 'dim'));
145
145
  console.log('');
146
146
 
147
+ // LOOP-01: alert state — track which named alerts fired last cycle so we
148
+ // can emit "new alert / cleared alert" lines per change rather than the
149
+ // full list every save. The user-lab's "spellcheck for prompts" framing
150
+ // wants action-on-change, not score-deltas alone.
151
+ const alertsEnabled = options.alerts !== false; // default-on; pass --no-alerts to disable
152
+ const lastAlerts = new Set();
153
+ function buildAlertSet(result) {
154
+ const set = new Set();
155
+ if (result && result.staleReferences && result.staleReferences.byKey) {
156
+ for (const [key, count] of Object.entries(result.staleReferences.byKey)) {
157
+ set.add(`stale:${key}:${count}`);
158
+ }
159
+ }
160
+ if (Array.isArray(result && result.shallowRiskHints)) {
161
+ for (const h of result.shallowRiskHints) {
162
+ if (h && h.key && h.severity === 'critical') {
163
+ set.add(`critical:${h.key}:${h.file || ''}`);
164
+ }
165
+ }
166
+ }
167
+ return set;
168
+ }
169
+ function emitAlertDiff(prev, curr) {
170
+ if (!alertsEnabled) return;
171
+ const newAlerts = [...curr].filter((a) => !prev.has(a));
172
+ const cleared = [...prev].filter((a) => !curr.has(a));
173
+ for (const a of newAlerts) {
174
+ console.log(c(` 🔔 NEW: ${a}`, 'yellow'));
175
+ }
176
+ for (const a of cleared) {
177
+ console.log(c(` ✓ CLEARED: ${a}`, 'green'));
178
+ }
179
+ }
180
+
147
181
  // Initial audit
148
182
  let lastScore = null;
149
183
  try {
@@ -151,6 +185,9 @@ async function watch(options) {
151
185
  lastScore = result.score;
152
186
  console.log(` ${c('Initial score:', 'bold')} ${scoreColor(result.score)}`);
153
187
  console.log(` ${result.passed} / ${result.passed + result.failed} checks passing`);
188
+ if (alertsEnabled && result.staleReferences && result.staleReferences.count > 0) {
189
+ console.log(c(` 📌 Initial alerts: ${result.staleReferences.count} stale reference(s)`, 'yellow'));
190
+ }
154
191
  const continuousStatus = buildContinuousStatus({
155
192
  dir: options.dir,
156
193
  auditResult: result,
@@ -159,6 +196,8 @@ async function watch(options) {
159
196
  });
160
197
  console.log(formatContinuousStatus(continuousStatus, { compact: true }));
161
198
  console.log('');
199
+ const initialAlerts = buildAlertSet(result);
200
+ for (const a of initialAlerts) lastAlerts.add(a);
162
201
  } catch (e) {
163
202
  console.log(c(` Initial audit failed: ${e.message}`, 'dim'));
164
203
  }
@@ -205,6 +244,13 @@ async function watch(options) {
205
244
  console.log(` Score: ${scoreColor(result.score)} ${arrow} (${result.passed}/${result.passed + result.failed} passing)`);
206
245
  console.log(formatContinuousStatus(continuousStatus, { compact: true }));
207
246
 
247
+ // LOOP-01: emit named alert diff (NEW / CLEARED) per change, so the
248
+ // developer gets action-on-change feedback, not just score deltas.
249
+ const currentAlerts = buildAlertSet(result);
250
+ emitAlertDiff(lastAlerts, currentAlerts);
251
+ lastAlerts.clear();
252
+ for (const a of currentAlerts) lastAlerts.add(a);
253
+
208
254
  if (lastScore !== null && result.score > lastScore) {
209
255
  console.log(c(' Nice improvement!', 'green'));
210
256
  } else if (lastScore !== null && result.score < lastScore) {