@nerviq/cli 1.29.0 → 1.29.1

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 (80) hide show
  1. package/CHANGELOG.md +1527 -1493
  2. package/README.md +550 -538
  3. package/SECURITY.md +82 -82
  4. package/bin/cli.js +2562 -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 +67 -67
  17. package/src/aider/activity.js +226 -226
  18. package/src/aider/context.js +162 -162
  19. package/src/aider/freshness.js +123 -123
  20. package/src/aider/techniques.js +3465 -3465
  21. package/src/audit/layers.js +180 -180
  22. package/src/audit.js +1032 -1032
  23. package/src/benchmark.js +299 -299
  24. package/src/codex/activity.js +324 -324
  25. package/src/codex/freshness.js +142 -142
  26. package/src/codex/techniques.js +4895 -4895
  27. package/src/context.js +326 -326
  28. package/src/continuous-ops.js +11 -1
  29. package/src/convert.js +340 -340
  30. package/src/copilot/config-parser.js +280 -280
  31. package/src/copilot/context.js +218 -218
  32. package/src/copilot/freshness.js +177 -177
  33. package/src/copilot/patch.js +238 -238
  34. package/src/copilot/techniques.js +3578 -3578
  35. package/src/cursor/freshness.js +194 -194
  36. package/src/cursor/patch.js +243 -243
  37. package/src/cursor/techniques.js +3735 -3735
  38. package/src/doctor.js +201 -201
  39. package/src/fix-engine.js +511 -8
  40. package/src/formatters/csv.js +86 -86
  41. package/src/formatters/junit.js +123 -123
  42. package/src/formatters/markdown.js +164 -164
  43. package/src/formatters/otel.js +151 -151
  44. package/src/freshness.js +156 -156
  45. package/src/gemini/activity.js +402 -402
  46. package/src/gemini/context.js +290 -290
  47. package/src/gemini/freshness.js +183 -183
  48. package/src/gemini/patch.js +229 -229
  49. package/src/gemini/techniques.js +3811 -3811
  50. package/src/governance.js +533 -533
  51. package/src/harmony/audit.js +306 -306
  52. package/src/i18n.js +63 -63
  53. package/src/insights.js +119 -119
  54. package/src/integrations.js +134 -134
  55. package/src/locales/en.json +33 -33
  56. package/src/locales/es.json +33 -33
  57. package/src/migrate.js +354 -354
  58. package/src/opencode/activity.js +286 -286
  59. package/src/opencode/freshness.js +137 -137
  60. package/src/opencode/techniques.js +3450 -3450
  61. package/src/setup/analysis.js +12 -12
  62. package/src/setup.js +7 -6
  63. package/src/shallow-risk/index.js +56 -56
  64. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -50
  65. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -46
  66. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -46
  67. package/src/shallow-risk/patterns/agent-config-missing-file.js +317 -317
  68. package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -49
  69. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -34
  70. package/src/shallow-risk/patterns/hook-script-missing.js +70 -70
  71. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -52
  72. package/src/shallow-risk/shared.js +648 -648
  73. package/src/source-urls.js +295 -295
  74. package/src/state-paths.js +85 -85
  75. package/src/supplemental-checks.js +805 -805
  76. package/src/telemetry.js +160 -160
  77. package/src/windsurf/context.js +359 -359
  78. package/src/windsurf/freshness.js +194 -194
  79. package/src/windsurf/patch.js +231 -231
  80. package/src/windsurf/techniques.js +3779 -3779
package/src/i18n.js CHANGED
@@ -1,63 +1,63 @@
1
- /**
2
- * Minimal i18n module for nerviq CLI.
3
- *
4
- * Supports locale files in src/locales/<lang>.json.
5
- * Falls back to English for missing keys.
6
- */
7
-
8
- const path = require('path');
9
- const fs = require('fs');
10
-
11
- const SUPPORTED_LOCALES = ['en', 'es'];
12
- const DEFAULT_LOCALE = 'en';
13
-
14
- let currentLocale = DEFAULT_LOCALE;
15
- let messages = {};
16
- let fallbackMessages = {};
17
-
18
- function loadLocale(locale) {
19
- const file = path.join(__dirname, 'locales', `${locale}.json`);
20
- try {
21
- return JSON.parse(fs.readFileSync(file, 'utf8'));
22
- } catch {
23
- return {};
24
- }
25
- }
26
-
27
- /**
28
- * Initialize i18n with a locale string (e.g. 'en', 'es').
29
- * Call this once at CLI startup.
30
- */
31
- function init(locale) {
32
- const lang = (locale || process.env.NERVIQ_LANG || DEFAULT_LOCALE).toLowerCase().slice(0, 2);
33
- currentLocale = SUPPORTED_LOCALES.includes(lang) ? lang : DEFAULT_LOCALE;
34
- fallbackMessages = loadLocale(DEFAULT_LOCALE);
35
- messages = currentLocale === DEFAULT_LOCALE ? fallbackMessages : loadLocale(currentLocale);
36
- }
37
-
38
- /**
39
- * Translate a key with optional interpolation.
40
- *
41
- * Usage:
42
- * t('audit.score', { score: 85, passed: 20, total: 25 })
43
- * // => "Score: 85/100 (20/25 checks passing)"
44
- */
45
- function t(key, params = {}) {
46
- let msg = messages[key] || fallbackMessages[key] || key;
47
- for (const [k, v] of Object.entries(params)) {
48
- msg = msg.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
49
- }
50
- return msg;
51
- }
52
-
53
- /**
54
- * Get current locale.
55
- */
56
- function getLocale() {
57
- return currentLocale;
58
- }
59
-
60
- // Auto-init with default on first require
61
- init();
62
-
63
- module.exports = { init, t, getLocale, SUPPORTED_LOCALES };
1
+ /**
2
+ * Minimal i18n module for nerviq CLI.
3
+ *
4
+ * Supports locale files in src/locales/<lang>.json.
5
+ * Falls back to English for missing keys.
6
+ */
7
+
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ const SUPPORTED_LOCALES = ['en', 'es'];
12
+ const DEFAULT_LOCALE = 'en';
13
+
14
+ let currentLocale = DEFAULT_LOCALE;
15
+ let messages = {};
16
+ let fallbackMessages = {};
17
+
18
+ function loadLocale(locale) {
19
+ const file = path.join(__dirname, 'locales', `${locale}.json`);
20
+ try {
21
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Initialize i18n with a locale string (e.g. 'en', 'es').
29
+ * Call this once at CLI startup.
30
+ */
31
+ function init(locale) {
32
+ const lang = (locale || process.env.NERVIQ_LANG || DEFAULT_LOCALE).toLowerCase().slice(0, 2);
33
+ currentLocale = SUPPORTED_LOCALES.includes(lang) ? lang : DEFAULT_LOCALE;
34
+ fallbackMessages = loadLocale(DEFAULT_LOCALE);
35
+ messages = currentLocale === DEFAULT_LOCALE ? fallbackMessages : loadLocale(currentLocale);
36
+ }
37
+
38
+ /**
39
+ * Translate a key with optional interpolation.
40
+ *
41
+ * Usage:
42
+ * t('audit.score', { score: 85, passed: 20, total: 25 })
43
+ * // => "Score: 85/100 (20/25 checks passing)"
44
+ */
45
+ function t(key, params = {}) {
46
+ let msg = messages[key] || fallbackMessages[key] || key;
47
+ for (const [k, v] of Object.entries(params)) {
48
+ msg = msg.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
49
+ }
50
+ return msg;
51
+ }
52
+
53
+ /**
54
+ * Get current locale.
55
+ */
56
+ function getLocale() {
57
+ return currentLocale;
58
+ }
59
+
60
+ // Auto-init with default on first require
61
+ init();
62
+
63
+ module.exports = { init, t, getLocale, SUPPORTED_LOCALES };
package/src/insights.js CHANGED
@@ -1,119 +1,119 @@
1
- /**
2
- * Anonymous insights collection - opt-in, privacy-first.
3
- *
4
- * What we collect (anonymously, no PII):
5
- * - Score distribution (10/100, 45/100, etc.)
6
- * - Stack detection (React+TS, Python+Docker, etc.)
7
- * - Which checks fail most
8
- * - Which checks pass most
9
- * - OS + Node version
10
- *
11
- * What we NEVER collect:
12
- * - File contents, paths, or project names
13
- * - IP addresses or user identity
14
- * - API keys, tokens, or credentials
15
- * - Any data if user opts out
16
- *
17
- * Users can opt out with: npx nerviq --no-insights
18
- * Or set env: NERVIQ_NO_INSIGHTS=1
19
- */
20
-
21
- const https = require('https');
22
- const os = require('os');
23
-
24
- const INSIGHTS_ENDPOINT = 'https://insights.nerviq.net/v1/report';
25
- const TIMEOUT_MS = 3000;
26
-
27
- function shouldCollect() {
28
- // Opt-IN: only collect if user explicitly enables
29
- if (process.env.NERVIQ_INSIGHTS === '1') return true;
30
- if (process.argv.includes('--insights')) return true;
31
- return false;
32
- }
33
-
34
- function buildPayload(auditResult) {
35
- // Only anonymous aggregate data - no PII, no file contents, no paths
36
- const failedChecks = auditResult.results
37
- .filter(r => !r.passed)
38
- .map(r => r.key);
39
-
40
- const passedChecks = auditResult.results
41
- .filter(r => r.passed)
42
- .map(r => r.key);
43
-
44
- return {
45
- v: 1,
46
- score: auditResult.score,
47
- passed: auditResult.passed,
48
- failed: auditResult.failed,
49
- stacks: (auditResult.stacks || []).map(s => s.label),
50
- failedChecks,
51
- passedChecks,
52
- platform: os.platform(),
53
- nodeVersion: process.version,
54
- toolVersion: require('../package.json').version,
55
- timestamp: new Date().toISOString(),
56
- };
57
- }
58
-
59
- function sendInsights(auditResult) {
60
- if (!shouldCollect()) return;
61
-
62
- try {
63
- const payload = JSON.stringify(buildPayload(auditResult));
64
- const url = new URL(INSIGHTS_ENDPOINT);
65
-
66
- const req = https.request({
67
- hostname: url.hostname,
68
- path: url.pathname,
69
- method: 'POST',
70
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
71
- timeout: TIMEOUT_MS,
72
- });
73
-
74
- // Fire and forget - never block the CLI
75
- req.on('error', () => {}); // silently ignore
76
- req.on('timeout', () => req.destroy());
77
- req.write(payload);
78
- req.end();
79
- } catch (e) {
80
- // Never let insights crash the CLI
81
- }
82
- }
83
-
84
- /**
85
- * Generate insights summary from local audit history.
86
- * This runs locally - no network needed.
87
- */
88
- function getLocalInsights(auditResult) {
89
- const { results } = auditResult;
90
- const applicable = results.filter(r => r.passed !== null);
91
- const failed = applicable.filter(r => r.passed === false);
92
-
93
- // Top 3 most impactful fixes
94
- const impactOrder = { critical: 3, high: 2, medium: 1 };
95
- const topFixes = [...failed]
96
- .sort((a, b) => (impactOrder[b.impact] || 0) - (impactOrder[a.impact] || 0))
97
- .slice(0, 3)
98
- .map(r => ({ name: r.name, impact: r.impact, fix: r.fix }));
99
-
100
- // Score breakdown by category
101
- const categories = {};
102
- for (const r of applicable) {
103
- const cat = r.category || 'other';
104
- if (!categories[cat]) categories[cat] = { passed: 0, total: 0 };
105
- categories[cat].total++;
106
- if (r.passed) categories[cat].passed++;
107
- }
108
-
109
- // Weakest categories
110
- const weakest = Object.entries(categories)
111
- .map(([name, data]) => ({ name, score: Math.round((data.passed / data.total) * 100), ...data }))
112
- .filter(c => c.score < 100)
113
- .sort((a, b) => a.score - b.score)
114
- .slice(0, 3);
115
-
116
- return { topFixes, categories, weakest, totalScore: auditResult.score };
117
- }
118
-
119
- module.exports = { sendInsights, getLocalInsights, shouldCollect };
1
+ /**
2
+ * Anonymous insights collection - opt-in, privacy-first.
3
+ *
4
+ * What we collect (anonymously, no PII):
5
+ * - Score distribution (10/100, 45/100, etc.)
6
+ * - Stack detection (React+TS, Python+Docker, etc.)
7
+ * - Which checks fail most
8
+ * - Which checks pass most
9
+ * - OS + Node version
10
+ *
11
+ * What we NEVER collect:
12
+ * - File contents, paths, or project names
13
+ * - IP addresses or user identity
14
+ * - API keys, tokens, or credentials
15
+ * - Any data if user opts out
16
+ *
17
+ * Users can opt out with: npx nerviq --no-insights
18
+ * Or set env: NERVIQ_NO_INSIGHTS=1
19
+ */
20
+
21
+ const https = require('https');
22
+ const os = require('os');
23
+
24
+ const INSIGHTS_ENDPOINT = 'https://insights.nerviq.net/v1/report';
25
+ const TIMEOUT_MS = 3000;
26
+
27
+ function shouldCollect() {
28
+ // Opt-IN: only collect if user explicitly enables
29
+ if (process.env.NERVIQ_INSIGHTS === '1') return true;
30
+ if (process.argv.includes('--insights')) return true;
31
+ return false;
32
+ }
33
+
34
+ function buildPayload(auditResult) {
35
+ // Only anonymous aggregate data - no PII, no file contents, no paths
36
+ const failedChecks = auditResult.results
37
+ .filter(r => !r.passed)
38
+ .map(r => r.key);
39
+
40
+ const passedChecks = auditResult.results
41
+ .filter(r => r.passed)
42
+ .map(r => r.key);
43
+
44
+ return {
45
+ v: 1,
46
+ score: auditResult.score,
47
+ passed: auditResult.passed,
48
+ failed: auditResult.failed,
49
+ stacks: (auditResult.stacks || []).map(s => s.label),
50
+ failedChecks,
51
+ passedChecks,
52
+ platform: os.platform(),
53
+ nodeVersion: process.version,
54
+ toolVersion: require('../package.json').version,
55
+ timestamp: new Date().toISOString(),
56
+ };
57
+ }
58
+
59
+ function sendInsights(auditResult) {
60
+ if (!shouldCollect()) return;
61
+
62
+ try {
63
+ const payload = JSON.stringify(buildPayload(auditResult));
64
+ const url = new URL(INSIGHTS_ENDPOINT);
65
+
66
+ const req = https.request({
67
+ hostname: url.hostname,
68
+ path: url.pathname,
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
71
+ timeout: TIMEOUT_MS,
72
+ });
73
+
74
+ // Fire and forget - never block the CLI
75
+ req.on('error', () => {}); // silently ignore
76
+ req.on('timeout', () => req.destroy());
77
+ req.write(payload);
78
+ req.end();
79
+ } catch (e) {
80
+ // Never let insights crash the CLI
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Generate insights summary from local audit history.
86
+ * This runs locally - no network needed.
87
+ */
88
+ function getLocalInsights(auditResult) {
89
+ const { results } = auditResult;
90
+ const applicable = results.filter(r => r.passed !== null);
91
+ const failed = applicable.filter(r => r.passed === false);
92
+
93
+ // Top 3 most impactful fixes
94
+ const impactOrder = { critical: 3, high: 2, medium: 1 };
95
+ const topFixes = [...failed]
96
+ .sort((a, b) => (impactOrder[b.impact] || 0) - (impactOrder[a.impact] || 0))
97
+ .slice(0, 3)
98
+ .map(r => ({ name: r.name, impact: r.impact, fix: r.fix }));
99
+
100
+ // Score breakdown by category
101
+ const categories = {};
102
+ for (const r of applicable) {
103
+ const cat = r.category || 'other';
104
+ if (!categories[cat]) categories[cat] = { passed: 0, total: 0 };
105
+ categories[cat].total++;
106
+ if (r.passed) categories[cat].passed++;
107
+ }
108
+
109
+ // Weakest categories
110
+ const weakest = Object.entries(categories)
111
+ .map(([name, data]) => ({ name, score: Math.round((data.passed / data.total) * 100), ...data }))
112
+ .filter(c => c.score < 100)
113
+ .sort((a, b) => a.score - b.score)
114
+ .slice(0, 3);
115
+
116
+ return { topFixes, categories, weakest, totalScore: auditResult.score };
117
+ }
118
+
119
+ module.exports = { sendInsights, getLocalInsights, shouldCollect };
@@ -1,15 +1,15 @@
1
- /**
2
- * Nerviq Integrations
3
- *
4
- * Webhook dispatch and message formatting for Slack, Discord,
5
- * and generic HTTP endpoints.
6
- *
7
- * All functions are synchronous-friendly; sendWebhook is async
8
- * (uses built-in https module, no external dependencies).
9
- */
10
-
11
- 'use strict';
12
-
1
+ /**
2
+ * Nerviq Integrations
3
+ *
4
+ * Webhook dispatch and message formatting for Slack, Discord,
5
+ * and generic HTTP endpoints.
6
+ *
7
+ * All functions are synchronous-friendly; sendWebhook is async
8
+ * (uses built-in https module, no external dependencies).
9
+ */
10
+
11
+ 'use strict';
12
+
13
13
  const https = require('https');
14
14
  const http = require('http');
15
15
  const { URL } = require('url');
@@ -65,11 +65,11 @@ function sendWebhookOnce(parsed, body, opts = {}) {
65
65
  }
66
66
 
67
67
  // ─── Webhook delivery ────────────────────────────────────────────────────────
68
-
69
- /**
70
- * POST JSON payload to a webhook URL.
71
- * @param {string} url - Destination URL (http or https)
72
- * @param {object} payload - JSON-serialisable object
68
+
69
+ /**
70
+ * POST JSON payload to a webhook URL.
71
+ * @param {string} url - Destination URL (http or https)
72
+ * @param {object} payload - JSON-serialisable object
73
73
  * @param {object} [opts]
74
74
  * @param {number} [opts.timeoutMs=10000]
75
75
  * @param {object} [opts.headers]
@@ -117,124 +117,124 @@ async function sendWebhook(url, payload, opts = {}) {
117
117
 
118
118
  return { ok: false, status: 0, body: '', attempts: maxAttempts };
119
119
  }
120
-
121
- // ─── Slack formatting ─────────────────────────────────────────────────────────
122
-
123
- /**
124
- * Format an audit result as a Slack Block Kit message payload.
125
- * @param {object} auditResult - Result from audit()
126
- * @returns {object} Slack-compatible message payload (blocks API)
127
- */
128
- function formatSlackMessage(auditResult) {
129
- const score = auditResult.score ?? 0;
130
- const platform = auditResult.platform ?? 'claude';
131
- const emoji = score >= 70 ? ':white_check_mark:' : score >= 40 ? ':warning:' : ':x:';
132
- const color = score >= 70 ? 'good' : score >= 40 ? 'warning' : 'danger';
133
-
134
- const criticals = (auditResult.results || [])
135
- .filter((r) => r.passed === false && r.impact === 'critical')
136
- .slice(0, 5);
137
-
138
- const sections = [
139
- {
140
- type: 'header',
141
- text: { type: 'plain_text', text: `${emoji} Nerviq Audit — ${platform}`, emoji: true },
142
- },
143
- {
144
- type: 'section',
145
- fields: [
146
- { type: 'mrkdwn', text: `*Score*\n${score}/100` },
147
- { type: 'mrkdwn', text: `*Checks*\n${auditResult.passed ?? 0} pass / ${auditResult.failed ?? 0} fail` },
148
- ],
149
- },
150
- ];
151
-
152
- if (criticals.length > 0) {
153
- sections.push({ type: 'divider' });
154
- sections.push({
155
- type: 'section',
156
- text: {
157
- type: 'mrkdwn',
158
- text: `*Critical gaps:*\n${criticals.map((r) => `• ${r.name}`).join('\n')}`,
159
- },
160
- });
161
- }
162
-
163
- if (auditResult.suggestedNextCommand) {
164
- sections.push({
165
- type: 'section',
166
- text: { type: 'mrkdwn', text: `*Next step:* \`${auditResult.suggestedNextCommand}\`` },
167
- });
168
- }
169
-
170
- // Also include legacy attachment for clients that don't support blocks
171
- return {
172
- blocks: sections,
173
- attachments: [
174
- {
175
- color,
176
- fallback: `Nerviq audit (${platform}): ${score}/100 — ${auditResult.passed ?? 0} pass, ${auditResult.failed ?? 0} fail`,
177
- },
178
- ],
179
- };
180
- }
181
-
182
- // ─── Discord formatting ───────────────────────────────────────────────────────
183
-
184
- /**
185
- * Format an audit result as a Discord webhook embed payload.
186
- * @param {object} auditResult - Result from audit()
187
- * @returns {object} Discord-compatible webhook payload (embeds)
188
- */
120
+
121
+ // ─── Slack formatting ─────────────────────────────────────────────────────────
122
+
123
+ /**
124
+ * Format an audit result as a Slack Block Kit message payload.
125
+ * @param {object} auditResult - Result from audit()
126
+ * @returns {object} Slack-compatible message payload (blocks API)
127
+ */
128
+ function formatSlackMessage(auditResult) {
129
+ const score = auditResult.score ?? 0;
130
+ const platform = auditResult.platform ?? 'claude';
131
+ const emoji = score >= 70 ? ':white_check_mark:' : score >= 40 ? ':warning:' : ':x:';
132
+ const color = score >= 70 ? 'good' : score >= 40 ? 'warning' : 'danger';
133
+
134
+ const criticals = (auditResult.results || [])
135
+ .filter((r) => r.passed === false && r.impact === 'critical')
136
+ .slice(0, 5);
137
+
138
+ const sections = [
139
+ {
140
+ type: 'header',
141
+ text: { type: 'plain_text', text: `${emoji} Nerviq Audit — ${platform}`, emoji: true },
142
+ },
143
+ {
144
+ type: 'section',
145
+ fields: [
146
+ { type: 'mrkdwn', text: `*Score*\n${score}/100` },
147
+ { type: 'mrkdwn', text: `*Checks*\n${auditResult.passed ?? 0} pass / ${auditResult.failed ?? 0} fail` },
148
+ ],
149
+ },
150
+ ];
151
+
152
+ if (criticals.length > 0) {
153
+ sections.push({ type: 'divider' });
154
+ sections.push({
155
+ type: 'section',
156
+ text: {
157
+ type: 'mrkdwn',
158
+ text: `*Critical gaps:*\n${criticals.map((r) => `• ${r.name}`).join('\n')}`,
159
+ },
160
+ });
161
+ }
162
+
163
+ if (auditResult.suggestedNextCommand) {
164
+ sections.push({
165
+ type: 'section',
166
+ text: { type: 'mrkdwn', text: `*Next step:* \`${auditResult.suggestedNextCommand}\`` },
167
+ });
168
+ }
169
+
170
+ // Also include legacy attachment for clients that don't support blocks
171
+ return {
172
+ blocks: sections,
173
+ attachments: [
174
+ {
175
+ color,
176
+ fallback: `Nerviq audit (${platform}): ${score}/100 — ${auditResult.passed ?? 0} pass, ${auditResult.failed ?? 0} fail`,
177
+ },
178
+ ],
179
+ };
180
+ }
181
+
182
+ // ─── Discord formatting ───────────────────────────────────────────────────────
183
+
184
+ /**
185
+ * Format an audit result as a Discord webhook embed payload.
186
+ * @param {object} auditResult - Result from audit()
187
+ * @returns {object} Discord-compatible webhook payload (embeds)
188
+ */
189
189
  function formatDiscordMessage(auditResult) {
190
- const score = auditResult.score ?? 0;
191
- const platform = auditResult.platform ?? 'claude';
192
- const color = score >= 70 ? 0x2ecc71 : score >= 40 ? 0xf39c12 : 0xe74c3c; // green / yellow / red
193
- const icon = score >= 70 ? '✅' : score >= 40 ? '⚠️' : '❌';
194
-
195
- const criticals = (auditResult.results || [])
196
- .filter((r) => r.passed === false && r.impact === 'critical')
197
- .slice(0, 5);
198
-
199
- const highs = (auditResult.results || [])
200
- .filter((r) => r.passed === false && r.impact === 'high')
201
- .slice(0, 3);
202
-
203
- const fields = [
204
- { name: 'Score', value: `**${score}/100**`, inline: true },
205
- { name: 'Pass / Fail', value: `${auditResult.passed ?? 0} / ${auditResult.failed ?? 0}`, inline: true },
206
- { name: 'Platform', value: platform, inline: true },
207
- ];
208
-
209
- if (criticals.length > 0) {
210
- fields.push({
211
- name: '🚨 Critical',
212
- value: criticals.map((r) => `• ${r.name}`).join('\n'),
213
- inline: false,
214
- });
215
- }
216
-
217
- if (highs.length > 0) {
218
- fields.push({
219
- name: '⚠️ High',
220
- value: highs.map((r) => `• ${r.name}`).join('\n'),
221
- inline: false,
222
- });
223
- }
224
-
225
- if (auditResult.suggestedNextCommand) {
226
- fields.push({ name: '▶️ Next step', value: `\`${auditResult.suggestedNextCommand}\``, inline: false });
227
- }
228
-
229
- return {
230
- embeds: [
231
- {
232
- title: `${icon} Nerviq Audit — ${platform}`,
233
- color,
234
- fields,
235
- footer: { text: `nerviq v${require('../package.json').version} • ${new Date().toISOString()}` },
236
- },
237
- ],
190
+ const score = auditResult.score ?? 0;
191
+ const platform = auditResult.platform ?? 'claude';
192
+ const color = score >= 70 ? 0x2ecc71 : score >= 40 ? 0xf39c12 : 0xe74c3c; // green / yellow / red
193
+ const icon = score >= 70 ? '✅' : score >= 40 ? '⚠️' : '❌';
194
+
195
+ const criticals = (auditResult.results || [])
196
+ .filter((r) => r.passed === false && r.impact === 'critical')
197
+ .slice(0, 5);
198
+
199
+ const highs = (auditResult.results || [])
200
+ .filter((r) => r.passed === false && r.impact === 'high')
201
+ .slice(0, 3);
202
+
203
+ const fields = [
204
+ { name: 'Score', value: `**${score}/100**`, inline: true },
205
+ { name: 'Pass / Fail', value: `${auditResult.passed ?? 0} / ${auditResult.failed ?? 0}`, inline: true },
206
+ { name: 'Platform', value: platform, inline: true },
207
+ ];
208
+
209
+ if (criticals.length > 0) {
210
+ fields.push({
211
+ name: '🚨 Critical',
212
+ value: criticals.map((r) => `• ${r.name}`).join('\n'),
213
+ inline: false,
214
+ });
215
+ }
216
+
217
+ if (highs.length > 0) {
218
+ fields.push({
219
+ name: '⚠️ High',
220
+ value: highs.map((r) => `• ${r.name}`).join('\n'),
221
+ inline: false,
222
+ });
223
+ }
224
+
225
+ if (auditResult.suggestedNextCommand) {
226
+ fields.push({ name: '▶️ Next step', value: `\`${auditResult.suggestedNextCommand}\``, inline: false });
227
+ }
228
+
229
+ return {
230
+ embeds: [
231
+ {
232
+ title: `${icon} Nerviq Audit — ${platform}`,
233
+ color,
234
+ fields,
235
+ footer: { text: `nerviq v${require('../package.json').version} • ${new Date().toISOString()}` },
236
+ },
237
+ ],
238
238
  };
239
239
  }
240
240