explain-my-error 1.0.8 → 1.0.10
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.
- package/README.md +33 -2
- package/dist/cli.js +395 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +175 -16
- package/dist/index.js +408 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/SKILL.md +41 -4
package/README.md
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
# explain-my-error
|
|
2
|
-
|
|
3
1
|

|
|
4
2
|
|
|
3
|
+
# explain-my-error
|
|
4
|
+
|
|
5
5
|
Turn confusing programming errors into clear fixes directly in your terminal.
|
|
6
6
|
|
|
7
7
|
`explain-my-error` returns:
|
|
8
8
|
|
|
9
9
|
- A plain-English explanation
|
|
10
10
|
- Common root causes
|
|
11
|
+
- Likely root cause based on context
|
|
12
|
+
- 2-3 alternative hypotheses with confidence
|
|
13
|
+
- Ranked fix plans (fast patch / proper fix / long-term fix)
|
|
14
|
+
- Framework-specific recipes (React/Next.js/Node/Express/TypeScript)
|
|
15
|
+
- Copy-paste remediation commands
|
|
16
|
+
- Verify checklist
|
|
11
17
|
- A practical fix
|
|
12
18
|
- A code example
|
|
13
19
|
- An ELI5 summary
|
|
@@ -90,6 +96,26 @@ explain-my-error explain "TypeError: Cannot read property 'map' of undefined"
|
|
|
90
96
|
eme explain "ReferenceError: x is not defined"
|
|
91
97
|
```
|
|
92
98
|
|
|
99
|
+
### Context-aware input
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
eme explain "TypeError: Cannot read property 'map' of undefined" \
|
|
103
|
+
--framework react \
|
|
104
|
+
--runtime "node 20" \
|
|
105
|
+
--stack "at App (src/App.tsx:12:5)" \
|
|
106
|
+
--code "items.map(item => item.id)"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
eme explain --stack-file ./error.log --code-file ./src/App.tsx --framework nextjs
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### JSON mode
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
eme explain "ReferenceError: x is not defined" --json
|
|
117
|
+
```
|
|
118
|
+
|
|
93
119
|
### Piped input
|
|
94
120
|
|
|
95
121
|
```bash
|
|
@@ -109,6 +135,11 @@ explain-my-error [command]
|
|
|
109
135
|
Commands:
|
|
110
136
|
|
|
111
137
|
- `explain [error...]` Explain a programming error
|
|
138
|
+
- `--json` Return structured JSON output
|
|
139
|
+
- `--stack`, `--stack-file` Add stack trace context
|
|
140
|
+
- `--code`, `--code-file` Add code snippet context
|
|
141
|
+
- `--runtime` Add runtime context
|
|
142
|
+
- `--framework` Add framework context
|
|
112
143
|
- `--help` Show CLI help
|
|
113
144
|
- `--version` Show CLI version
|
|
114
145
|
|
package/dist/cli.js
CHANGED
|
@@ -18,13 +18,45 @@ import axios from "axios";
|
|
|
18
18
|
|
|
19
19
|
// src/types/error.ts
|
|
20
20
|
import { z } from "zod";
|
|
21
|
+
var hypothesisSchema = z.object({
|
|
22
|
+
hypothesis: z.string().min(1),
|
|
23
|
+
confidence: z.number().min(0).max(1),
|
|
24
|
+
why: z.string().min(1)
|
|
25
|
+
});
|
|
26
|
+
var fixPlanSchema = z.object({
|
|
27
|
+
plan: z.enum(["fast_patch", "proper_fix", "long_term_fix"]),
|
|
28
|
+
summary: z.string().min(1),
|
|
29
|
+
steps: z.array(z.string().min(1)).min(1),
|
|
30
|
+
tradeoffs: z.object({
|
|
31
|
+
risk: z.string().min(1),
|
|
32
|
+
performance: z.string().min(1),
|
|
33
|
+
maintainability: z.string().min(1)
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
var frameworkRecipeSchema = z.object({
|
|
37
|
+
framework: z.string().min(1),
|
|
38
|
+
when_to_use: z.string().min(1),
|
|
39
|
+
steps: z.array(z.string().min(1)).min(1),
|
|
40
|
+
code_snippet: z.string().min(1)
|
|
41
|
+
});
|
|
42
|
+
var remediationBlockSchema = z.object({
|
|
43
|
+
title: z.string().min(1),
|
|
44
|
+
commands: z.array(z.string().min(1)).min(1),
|
|
45
|
+
snippet: z.string().optional()
|
|
46
|
+
});
|
|
21
47
|
var explainedErrorSchema = z.object({
|
|
22
48
|
title: z.string().min(1),
|
|
23
49
|
explanation: z.string().min(1),
|
|
24
50
|
common_causes: z.array(z.string().min(1)).min(1),
|
|
25
51
|
fix: z.string().min(1),
|
|
26
52
|
code_example: z.string().min(1),
|
|
27
|
-
eli5: z.string().min(1)
|
|
53
|
+
eli5: z.string().min(1),
|
|
54
|
+
likely_root_cause: z.string().min(1),
|
|
55
|
+
hypotheses: z.array(hypothesisSchema).min(1).max(3),
|
|
56
|
+
fix_plans: z.array(fixPlanSchema).min(3),
|
|
57
|
+
framework_recipes: z.array(frameworkRecipeSchema).min(1),
|
|
58
|
+
remediation_blocks: z.array(remediationBlockSchema).min(1),
|
|
59
|
+
verify_checklist: z.array(z.string().min(1)).min(1)
|
|
28
60
|
});
|
|
29
61
|
|
|
30
62
|
// src/services/ai.ts
|
|
@@ -33,15 +65,81 @@ var PRIMARY_GROQ_MODEL = "llama3-70b-8192";
|
|
|
33
65
|
var FALLBACK_GROQ_MODEL = process.env.GROQ_FALLBACK_MODEL ?? "llama-3.3-70b-versatile";
|
|
34
66
|
function extractJson(content) {
|
|
35
67
|
const trimmed = content.trim();
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
const tryParse = (input) => {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(input);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const sanitizeControlCharsInStrings = (input) => {
|
|
76
|
+
let result = "";
|
|
77
|
+
let inString = false;
|
|
78
|
+
let escaping = false;
|
|
79
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
80
|
+
const ch = input[i];
|
|
81
|
+
if (!inString) {
|
|
82
|
+
result += ch;
|
|
83
|
+
if (ch === '"') {
|
|
84
|
+
inString = true;
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (escaping) {
|
|
89
|
+
result += ch;
|
|
90
|
+
escaping = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === "\\") {
|
|
94
|
+
result += ch;
|
|
95
|
+
escaping = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === '"') {
|
|
99
|
+
result += ch;
|
|
100
|
+
inString = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === "\n") {
|
|
104
|
+
result += "\\n";
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (ch === "\r") {
|
|
108
|
+
result += "\\r";
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (ch === " ") {
|
|
112
|
+
result += "\\t";
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const code = ch.charCodeAt(0);
|
|
116
|
+
if (code < 32) {
|
|
117
|
+
result += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
result += ch;
|
|
42
121
|
}
|
|
43
|
-
return
|
|
122
|
+
return result;
|
|
123
|
+
};
|
|
124
|
+
const direct = tryParse(trimmed);
|
|
125
|
+
if (direct !== null) {
|
|
126
|
+
return direct;
|
|
127
|
+
}
|
|
128
|
+
const match = trimmed.match(/\{[\s\S]*\}/);
|
|
129
|
+
if (!match) {
|
|
130
|
+
throw new Error("AI did not return valid JSON.");
|
|
44
131
|
}
|
|
132
|
+
const extracted = match[0];
|
|
133
|
+
const extractedParsed = tryParse(extracted);
|
|
134
|
+
if (extractedParsed !== null) {
|
|
135
|
+
return extractedParsed;
|
|
136
|
+
}
|
|
137
|
+
const sanitized = sanitizeControlCharsInStrings(extracted);
|
|
138
|
+
const sanitizedParsed = tryParse(sanitized);
|
|
139
|
+
if (sanitizedParsed !== null) {
|
|
140
|
+
return sanitizedParsed;
|
|
141
|
+
}
|
|
142
|
+
throw new Error("AI did not return valid JSON.");
|
|
45
143
|
}
|
|
46
144
|
function stringifyField(value) {
|
|
47
145
|
if (typeof value === "string") {
|
|
@@ -59,23 +157,160 @@ function stringifyField(value) {
|
|
|
59
157
|
}
|
|
60
158
|
return String(value);
|
|
61
159
|
}
|
|
160
|
+
function toStringArray(value) {
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return value.map((item) => stringifyField(item)).map((item) => item.trim()).filter(Boolean);
|
|
163
|
+
}
|
|
164
|
+
return stringifyField(value).split(/\n|,/).map((item) => item.trim()).filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
function toConfidence(value) {
|
|
167
|
+
if (typeof value === "number") {
|
|
168
|
+
return Math.max(0, Math.min(1, value));
|
|
169
|
+
}
|
|
170
|
+
const parsed = Number(stringifyField(value));
|
|
171
|
+
if (!Number.isNaN(parsed)) {
|
|
172
|
+
return Math.max(0, Math.min(1, parsed));
|
|
173
|
+
}
|
|
174
|
+
return 0.5;
|
|
175
|
+
}
|
|
62
176
|
function normalizeResponseShape(payload) {
|
|
63
177
|
if (!payload || typeof payload !== "object") {
|
|
64
178
|
return payload;
|
|
65
179
|
}
|
|
66
180
|
const data = payload;
|
|
67
|
-
const
|
|
68
|
-
const
|
|
181
|
+
const commonCauses = toStringArray(data.common_causes);
|
|
182
|
+
const hypothesesRaw = Array.isArray(data.hypotheses) ? data.hypotheses : [];
|
|
183
|
+
const hypotheses = hypothesesRaw.map((item) => {
|
|
184
|
+
const row = item;
|
|
185
|
+
return {
|
|
186
|
+
hypothesis: stringifyField(row?.hypothesis),
|
|
187
|
+
confidence: toConfidence(row?.confidence),
|
|
188
|
+
why: stringifyField(row?.why)
|
|
189
|
+
};
|
|
190
|
+
}).filter((item) => item.hypothesis && item.why).slice(0, 3);
|
|
191
|
+
const fixPlansRaw = Array.isArray(data.fix_plans) ? data.fix_plans : [];
|
|
192
|
+
const normalizedFixPlans = fixPlansRaw.map((item, index) => {
|
|
193
|
+
const row = item;
|
|
194
|
+
const defaultPlan = index === 0 ? "fast_patch" : index === 1 ? "proper_fix" : "long_term_fix";
|
|
195
|
+
const planText = stringifyField(row?.plan || defaultPlan).toLowerCase();
|
|
196
|
+
const plan = planText === "fast_patch" || planText === "proper_fix" || planText === "long_term_fix" ? planText : defaultPlan;
|
|
197
|
+
const tradeoffs = row?.tradeoffs ?? {};
|
|
198
|
+
return {
|
|
199
|
+
plan,
|
|
200
|
+
summary: stringifyField(row?.summary),
|
|
201
|
+
steps: toStringArray(row?.steps),
|
|
202
|
+
tradeoffs: {
|
|
203
|
+
risk: stringifyField(tradeoffs.risk || "Unknown"),
|
|
204
|
+
performance: stringifyField(tradeoffs.performance || "Unknown"),
|
|
205
|
+
maintainability: stringifyField(tradeoffs.maintainability || "Unknown")
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}).filter((item) => item.summary && item.steps.length > 0);
|
|
209
|
+
const fixPlans = normalizedFixPlans.length >= 3 ? normalizedFixPlans.slice(0, 3) : [
|
|
210
|
+
{
|
|
211
|
+
plan: "fast_patch",
|
|
212
|
+
summary: stringifyField(data.fix),
|
|
213
|
+
steps: [stringifyField(data.fix) || "Apply a defensive guard around the failing line."],
|
|
214
|
+
tradeoffs: {
|
|
215
|
+
risk: "May hide deeper data contract issues.",
|
|
216
|
+
performance: "Minimal runtime overhead.",
|
|
217
|
+
maintainability: "Good short-term, revisit for root-cause cleanup."
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
plan: "proper_fix",
|
|
222
|
+
summary: "Fix data flow and initialization path.",
|
|
223
|
+
steps: [
|
|
224
|
+
"Trace where the failing value is created.",
|
|
225
|
+
"Initialize value before usage and add proper null checks.",
|
|
226
|
+
"Add a regression test for this scenario."
|
|
227
|
+
],
|
|
228
|
+
tradeoffs: {
|
|
229
|
+
risk: "Moderate code changes.",
|
|
230
|
+
performance: "Neutral to slightly improved.",
|
|
231
|
+
maintainability: "Better long-term readability and reliability."
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
plan: "long_term_fix",
|
|
236
|
+
summary: "Enforce stronger contracts and prevention tooling.",
|
|
237
|
+
steps: [
|
|
238
|
+
"Enable stricter type checks and lint rules.",
|
|
239
|
+
"Introduce runtime validation for external inputs.",
|
|
240
|
+
"Create reusable utility guards in shared modules."
|
|
241
|
+
],
|
|
242
|
+
tradeoffs: {
|
|
243
|
+
risk: "Requires broader refactor.",
|
|
244
|
+
performance: "Potential minor validation overhead.",
|
|
245
|
+
maintainability: "Highest long-term stability."
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
];
|
|
249
|
+
const frameworkRecipesRaw = Array.isArray(data.framework_recipes) ? data.framework_recipes : [];
|
|
250
|
+
const frameworkRecipes = frameworkRecipesRaw.map((item) => {
|
|
251
|
+
const row = item;
|
|
252
|
+
return {
|
|
253
|
+
framework: stringifyField(row?.framework),
|
|
254
|
+
when_to_use: stringifyField(row?.when_to_use),
|
|
255
|
+
steps: toStringArray(row?.steps),
|
|
256
|
+
code_snippet: stringifyField(row?.code_snippet)
|
|
257
|
+
};
|
|
258
|
+
}).filter(
|
|
259
|
+
(item) => item.framework && item.when_to_use && item.steps.length > 0 && item.code_snippet
|
|
260
|
+
);
|
|
261
|
+
const remediationBlocksRaw = Array.isArray(data.remediation_blocks) ? data.remediation_blocks : [];
|
|
262
|
+
const remediationBlocks = remediationBlocksRaw.map((item) => {
|
|
263
|
+
const row = item;
|
|
264
|
+
return {
|
|
265
|
+
title: stringifyField(row?.title),
|
|
266
|
+
commands: toStringArray(row?.commands),
|
|
267
|
+
snippet: stringifyField(row?.snippet) || void 0
|
|
268
|
+
};
|
|
269
|
+
}).filter((item) => item.title && item.commands.length > 0);
|
|
270
|
+
const verifyChecklist = toStringArray(data.verify_checklist);
|
|
271
|
+
const title = stringifyField(data.title);
|
|
272
|
+
const explanation = stringifyField(data.explanation);
|
|
273
|
+
const codeExample = stringifyField(data.code_example);
|
|
274
|
+
const fix = stringifyField(data.fix);
|
|
275
|
+
const likelyRootCause = stringifyField(data.likely_root_cause) || commonCauses[0] || "Insufficient context from error text.";
|
|
69
276
|
return {
|
|
70
|
-
title:
|
|
71
|
-
explanation:
|
|
72
|
-
common_causes: commonCauses,
|
|
73
|
-
fix:
|
|
74
|
-
code_example:
|
|
75
|
-
eli5: stringifyField(data.eli5)
|
|
277
|
+
title: title || "Unknown error",
|
|
278
|
+
explanation: explanation || "Unable to derive explanation from model response.",
|
|
279
|
+
common_causes: commonCauses.length > 0 ? commonCauses : ["Insufficient context in provided error text."],
|
|
280
|
+
fix: fix || "Add guards and trace value initialization before the failing line.",
|
|
281
|
+
code_example: codeExample || "// Add null checks around the failing line.",
|
|
282
|
+
eli5: stringifyField(data.eli5) || "Your code expected one thing, but got something different.",
|
|
283
|
+
likely_root_cause: likelyRootCause,
|
|
284
|
+
hypotheses: hypotheses.length > 0 ? hypotheses : [
|
|
285
|
+
{
|
|
286
|
+
hypothesis: likelyRootCause,
|
|
287
|
+
confidence: 0.6,
|
|
288
|
+
why: "Derived from error pattern and available context."
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
fix_plans: fixPlans,
|
|
292
|
+
framework_recipes: frameworkRecipes.length > 0 ? frameworkRecipes : [
|
|
293
|
+
{
|
|
294
|
+
framework: "Node.js",
|
|
295
|
+
when_to_use: "Default runtime fallback when framework is unknown.",
|
|
296
|
+
steps: [
|
|
297
|
+
"Validate all external data before use.",
|
|
298
|
+
"Guard nullable values before method calls.",
|
|
299
|
+
"Add test coverage for this failure path."
|
|
300
|
+
],
|
|
301
|
+
code_snippet: "const safeValue = input ?? defaultValue;"
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
remediation_blocks: remediationBlocks.length > 0 ? remediationBlocks : [
|
|
305
|
+
{
|
|
306
|
+
title: "Quick remediation commands",
|
|
307
|
+
commands: ["npm run test", "npm run lint"]
|
|
308
|
+
}
|
|
309
|
+
],
|
|
310
|
+
verify_checklist: verifyChecklist.length > 0 ? verifyChecklist : ["Re-run failing command.", "Confirm no regression in related flows."]
|
|
76
311
|
};
|
|
77
312
|
}
|
|
78
|
-
async function explainErrorWithAI(errorMessage) {
|
|
313
|
+
async function explainErrorWithAI(errorMessage, context = {}) {
|
|
79
314
|
const apiKey = process.env.GROQ_API_KEY;
|
|
80
315
|
if (!apiKey) {
|
|
81
316
|
throw new Error(
|
|
@@ -93,14 +328,44 @@ async function explainErrorWithAI(errorMessage) {
|
|
|
93
328
|
].join("\n")
|
|
94
329
|
);
|
|
95
330
|
}
|
|
96
|
-
const prompt =
|
|
331
|
+
const prompt = `You are a senior software engineer helping debug errors with context.
|
|
332
|
+
Return STRICT JSON only (no markdown) with fields:
|
|
333
|
+
- title
|
|
334
|
+
- explanation
|
|
335
|
+
- common_causes (string[])
|
|
336
|
+
- fix
|
|
337
|
+
- code_example
|
|
338
|
+
- eli5
|
|
339
|
+
- likely_root_cause
|
|
340
|
+
- hypotheses (2-3 items): [{ hypothesis, confidence (0..1), why }]
|
|
341
|
+
- fix_plans (exactly 3): [
|
|
342
|
+
{ plan: "fast_patch", summary, steps[], tradeoffs: { risk, performance, maintainability } },
|
|
343
|
+
{ plan: "proper_fix", summary, steps[], tradeoffs: { risk, performance, maintainability } },
|
|
344
|
+
{ plan: "long_term_fix", summary, steps[], tradeoffs: { risk, performance, maintainability } }
|
|
345
|
+
]
|
|
346
|
+
- framework_recipes (1+): [{ framework, when_to_use, steps[], code_snippet }]
|
|
347
|
+
- remediation_blocks (1+): [{ title, commands[], snippet? }]
|
|
348
|
+
- verify_checklist (string[])
|
|
349
|
+
|
|
350
|
+
Use provided runtime/framework/code/stack context to produce context-aware likely root cause and framework-specific fixes.`;
|
|
351
|
+
const contextBlock = [
|
|
352
|
+
`Error message:
|
|
353
|
+
${errorMessage}`,
|
|
354
|
+
`Runtime:
|
|
355
|
+
${context.runtime ?? "unknown"}`,
|
|
356
|
+
`Framework:
|
|
357
|
+
${context.framework ?? "unknown"}`,
|
|
358
|
+
`Stack trace:
|
|
359
|
+
${context.stackTrace ?? "not provided"}`,
|
|
360
|
+
`Code snippet:
|
|
361
|
+
${context.codeSnippet ?? "not provided"}`
|
|
362
|
+
].join("\n\n");
|
|
97
363
|
const requestBody = {
|
|
98
364
|
messages: [
|
|
99
365
|
{ role: "system", content: prompt },
|
|
100
366
|
{
|
|
101
367
|
role: "user",
|
|
102
|
-
content:
|
|
103
|
-
${errorMessage}
|
|
368
|
+
content: `${contextBlock}
|
|
104
369
|
|
|
105
370
|
Return strict JSON only.`
|
|
106
371
|
}
|
|
@@ -163,8 +428,42 @@ function block(title, body) {
|
|
|
163
428
|
const rows = body.split("\n").map((row) => pc.white(row));
|
|
164
429
|
return [pc.cyan(line()), pc.bold(pc.cyan(title)), ...rows, pc.cyan(line())].join("\n");
|
|
165
430
|
}
|
|
431
|
+
function list(items) {
|
|
432
|
+
return items.map((item, index) => `${index + 1}. ${item}`).join("\n");
|
|
433
|
+
}
|
|
166
434
|
function formatExplainedError(result) {
|
|
167
|
-
const commonCauses = result.common_causes
|
|
435
|
+
const commonCauses = list(result.common_causes);
|
|
436
|
+
const hypotheses = result.hypotheses.map(
|
|
437
|
+
(item, index) => `${index + 1}. ${item.hypothesis}
|
|
438
|
+
confidence: ${(item.confidence * 100).toFixed(0)}%
|
|
439
|
+
why: ${item.why}`
|
|
440
|
+
).join("\n\n");
|
|
441
|
+
const fixPlans = result.fix_plans.map((plan) => {
|
|
442
|
+
const label = plan.plan === "fast_patch" ? "Fast Patch" : plan.plan === "proper_fix" ? "Proper Fix" : "Long-Term Fix";
|
|
443
|
+
return [
|
|
444
|
+
`${pc.bold(pc.green(label))}: ${plan.summary}`,
|
|
445
|
+
...plan.steps.map((step, index) => ` ${index + 1}. ${step}`),
|
|
446
|
+
` tradeoffs -> risk: ${plan.tradeoffs.risk}; performance: ${plan.tradeoffs.performance}; maintainability: ${plan.tradeoffs.maintainability}`
|
|
447
|
+
].join("\n");
|
|
448
|
+
}).join("\n\n");
|
|
449
|
+
const frameworkRecipes = result.framework_recipes.map(
|
|
450
|
+
(recipe) => [
|
|
451
|
+
`${pc.bold(pc.cyan(recipe.framework))}: ${recipe.when_to_use}`,
|
|
452
|
+
...recipe.steps.map((step, index) => ` ${index + 1}. ${step}`),
|
|
453
|
+
" snippet:",
|
|
454
|
+
recipe.code_snippet.split("\n").map((lineText) => ` ${lineText}`).join("\n")
|
|
455
|
+
].join("\n")
|
|
456
|
+
).join("\n\n");
|
|
457
|
+
const remediationBlocks = result.remediation_blocks.map((blockItem) => {
|
|
458
|
+
const snippet = blockItem.snippet?.trim() ? `
|
|
459
|
+
snippet:
|
|
460
|
+
${blockItem.snippet.split("\n").map((lineText) => ` ${lineText}`).join("\n")}` : "";
|
|
461
|
+
return [
|
|
462
|
+
`${pc.bold(pc.green(blockItem.title))}`,
|
|
463
|
+
...blockItem.commands.map((command) => ` $ ${command}`)
|
|
464
|
+
].join("\n") + snippet;
|
|
465
|
+
}).join("\n\n");
|
|
466
|
+
const verifyChecklist = list(result.verify_checklist);
|
|
168
467
|
const hero = [
|
|
169
468
|
pc.cyan(line("=")),
|
|
170
469
|
framedLine(pc.bold(pc.cyan("EXPLAIN MY ERROR"))),
|
|
@@ -175,16 +474,27 @@ function formatExplainedError(result) {
|
|
|
175
474
|
hero,
|
|
176
475
|
"",
|
|
177
476
|
`${pc.bold(pc.cyan("ERROR"))}: ${pc.bold(pc.white(result.title))}`,
|
|
477
|
+
`${pc.bold(pc.yellow("LIKELY ROOT CAUSE"))}: ${pc.white(result.likely_root_cause)}`,
|
|
178
478
|
"",
|
|
179
479
|
block("EXPLANATION", result.explanation),
|
|
180
480
|
"",
|
|
181
481
|
block("COMMON CAUSES", commonCauses),
|
|
182
482
|
"",
|
|
483
|
+
block("HYPOTHESES (WITH CONFIDENCE)", hypotheses),
|
|
484
|
+
"",
|
|
485
|
+
block("RANKED FIX PLANS", fixPlans),
|
|
486
|
+
"",
|
|
183
487
|
`${pc.bold(pc.green("FIX"))}
|
|
184
488
|
${pc.white(result.fix)}`,
|
|
185
489
|
"",
|
|
186
490
|
block("CODE EXAMPLE", result.code_example),
|
|
187
491
|
"",
|
|
492
|
+
block("FRAMEWORK-SPECIFIC RECIPES", frameworkRecipes),
|
|
493
|
+
"",
|
|
494
|
+
block("COPY-PASTE REMEDIATION", remediationBlocks),
|
|
495
|
+
"",
|
|
496
|
+
block("VERIFY CHECKLIST", verifyChecklist),
|
|
497
|
+
"",
|
|
188
498
|
block("ELI5", result.eli5),
|
|
189
499
|
"",
|
|
190
500
|
pc.dim('Tip: run `eme explain "<error>"` for quick mode.')
|
|
@@ -213,7 +523,16 @@ var logger = {
|
|
|
213
523
|
};
|
|
214
524
|
|
|
215
525
|
// src/commands/explain.ts
|
|
216
|
-
|
|
526
|
+
function isDepsObject(value) {
|
|
527
|
+
if (!value || typeof value !== "object") {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
const candidate = value;
|
|
531
|
+
return "explainError" in candidate || "createSpinner" in candidate || "formatOutput" in candidate || "log" in candidate;
|
|
532
|
+
}
|
|
533
|
+
async function runExplainCommand(errorMessage, optionsOrDeps = { output: "pretty" }, depsArg = {}) {
|
|
534
|
+
const options = isDepsObject(optionsOrDeps) ? { output: "pretty" } : optionsOrDeps;
|
|
535
|
+
const deps = isDepsObject(optionsOrDeps) ? optionsOrDeps : depsArg;
|
|
217
536
|
const explainError = deps.explainError ?? explainErrorWithAI;
|
|
218
537
|
const createSpinner = deps.createSpinner ?? ((text) => ora(text).start());
|
|
219
538
|
const formatOutput = deps.formatOutput ?? formatExplainedError;
|
|
@@ -224,10 +543,14 @@ async function runExplainCommand(errorMessage, deps = {}) {
|
|
|
224
543
|
}
|
|
225
544
|
const spinner = createSpinner("Analyzing your error...");
|
|
226
545
|
try {
|
|
227
|
-
const result = await explainError(errorMessage.trim());
|
|
546
|
+
const result = await explainError(errorMessage.trim(), options.context);
|
|
228
547
|
spinner.succeed("Explanation ready.");
|
|
229
548
|
log.info("");
|
|
230
|
-
|
|
549
|
+
if (options.output === "json") {
|
|
550
|
+
log.info(JSON.stringify(result, null, 2));
|
|
551
|
+
} else {
|
|
552
|
+
log.info(formatOutput(result));
|
|
553
|
+
}
|
|
231
554
|
} catch (error) {
|
|
232
555
|
spinner.fail("Could not explain this error.");
|
|
233
556
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -255,6 +578,46 @@ async function readStdin() {
|
|
|
255
578
|
}
|
|
256
579
|
return Buffer.concat(chunks).toString("utf8").trim();
|
|
257
580
|
}
|
|
581
|
+
async function safeReadText(filePath) {
|
|
582
|
+
if (!filePath) {
|
|
583
|
+
return void 0;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
return await readFile(filePath, "utf8");
|
|
587
|
+
} catch {
|
|
588
|
+
return void 0;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async function detectFrameworkFromPackageJson() {
|
|
592
|
+
try {
|
|
593
|
+
const raw = await readFile("package.json", "utf8");
|
|
594
|
+
const pkg = JSON.parse(raw);
|
|
595
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
596
|
+
if ("next" in deps) return "Next.js";
|
|
597
|
+
if ("react" in deps) return "React";
|
|
598
|
+
if ("express" in deps) return "Express";
|
|
599
|
+
if ("typescript" in deps) return "TypeScript";
|
|
600
|
+
if ("fastify" in deps) return "Fastify";
|
|
601
|
+
if ("nestjs" in deps || "@nestjs/core" in deps) return "NestJS";
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
return void 0;
|
|
605
|
+
}
|
|
606
|
+
async function buildExplainOptions(options) {
|
|
607
|
+
const stackFromFile = await safeReadText(options.stackFile);
|
|
608
|
+
const codeFromFile = await safeReadText(options.codeFile);
|
|
609
|
+
const detectedFramework = await detectFrameworkFromPackageJson();
|
|
610
|
+
const context = {
|
|
611
|
+
stackTrace: options.stack ?? stackFromFile,
|
|
612
|
+
codeSnippet: options.code ?? codeFromFile,
|
|
613
|
+
runtime: options.runtime ?? `Node ${process.version}`,
|
|
614
|
+
framework: options.framework ?? detectedFramework ?? "unknown"
|
|
615
|
+
};
|
|
616
|
+
return {
|
|
617
|
+
output: options.json ? "json" : "pretty",
|
|
618
|
+
context
|
|
619
|
+
};
|
|
620
|
+
}
|
|
258
621
|
async function promptForError() {
|
|
259
622
|
if (!process.stdin.isTTY) {
|
|
260
623
|
return "";
|
|
@@ -363,30 +726,33 @@ Input modes:
|
|
|
363
726
|
$ pnpm run build 2>&1 | eme
|
|
364
727
|
`
|
|
365
728
|
);
|
|
366
|
-
program.command("explain").description("Explain a programming error message").argument("[error...]", "Error message to analyze").addHelpText(
|
|
729
|
+
program.command("explain").description("Explain a programming error message").argument("[error...]", "Error message to analyze").option("--json", "Output structured JSON").option("--stack <stackTrace>", "Additional stack trace context").option("--stack-file <path>", "Read stack trace from a file").option("--code <codeSnippet>", "Additional code snippet context").option("--code-file <path>", "Read code snippet from a file").option("--runtime <runtime>", "Runtime context (example: node 20, bun 1.1)").option("--framework <framework>", "Framework context (example: react, nextjs, express)").addHelpText(
|
|
367
730
|
"after",
|
|
368
731
|
`
|
|
369
732
|
Examples:
|
|
370
733
|
$ explain-my-error explain "SyntaxError: Unexpected token }"
|
|
371
734
|
$ eme explain "Module not found: Can't resolve 'axios'"
|
|
372
735
|
$ cat error.txt | explain-my-error explain
|
|
736
|
+
$ eme explain "TypeError: Cannot read property 'map' of undefined" --framework react --runtime "node 20"
|
|
737
|
+
$ eme explain --stack-file ./error.log --code-file ./src/app.ts --json
|
|
373
738
|
`
|
|
374
|
-
).action(async (errorParts) => {
|
|
739
|
+
).action(async (errorParts, options) => {
|
|
375
740
|
const inlineError = errorParts.join(" ").trim();
|
|
376
741
|
const pipedError = inlineError ? "" : await readStdinFn();
|
|
377
742
|
const promptedError = !inlineError && !pipedError ? await promptForErrorFn() : "";
|
|
378
743
|
const finalError = inlineError || pipedError || promptedError;
|
|
744
|
+
const explainOptions = await buildExplainOptions(options);
|
|
379
745
|
const hasApiKey = await ensureApiKeyFn();
|
|
380
746
|
if (!hasApiKey) {
|
|
381
747
|
log.warn("GROQ_API_KEY is required. Set it and run again.");
|
|
382
748
|
return;
|
|
383
749
|
}
|
|
384
|
-
await runExplain(finalError);
|
|
750
|
+
await runExplain(finalError, explainOptions);
|
|
385
751
|
});
|
|
386
752
|
program.action(async () => {
|
|
387
753
|
if (stdinIsTTY()) {
|
|
388
754
|
const promptedError = await promptForErrorFn();
|
|
389
|
-
await runExplain(promptedError);
|
|
755
|
+
await runExplain(promptedError, await buildExplainOptions({}));
|
|
390
756
|
return;
|
|
391
757
|
}
|
|
392
758
|
const pipedError = await readStdinFn();
|
|
@@ -399,7 +765,7 @@ Examples:
|
|
|
399
765
|
log.warn("GROQ_API_KEY is required. Set it and run again.");
|
|
400
766
|
return;
|
|
401
767
|
}
|
|
402
|
-
await runExplain(pipedError);
|
|
768
|
+
await runExplain(pipedError, await buildExplainOptions({}));
|
|
403
769
|
});
|
|
404
770
|
await program.parseAsync(argv);
|
|
405
771
|
}
|