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 CHANGED
@@ -1,13 +1,19 @@
1
- # explain-my-error
2
-
3
1
  ![explain-my-error CLI screenshot](./cli-screenshot.svg)
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
- try {
37
- return JSON.parse(trimmed);
38
- } catch {
39
- const match = trimmed.match(/\{[\s\S]*\}/);
40
- if (!match) {
41
- throw new Error("AI did not return valid JSON.");
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 JSON.parse(match[0]);
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 rawCauses = data.common_causes;
68
- const commonCauses = Array.isArray(rawCauses) ? rawCauses.map((item) => stringifyField(item)).filter(Boolean) : stringifyField(rawCauses).split(/\n|,/).map((item) => item.trim()).filter(Boolean);
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: stringifyField(data.title),
71
- explanation: stringifyField(data.explanation),
72
- common_causes: commonCauses,
73
- fix: stringifyField(data.fix),
74
- code_example: stringifyField(data.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 = "You are a senior software engineer. Explain the programming error and return JSON with fields: title, explanation, common_causes, fix, code_example, eli5.";
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: `Error message:
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.map((cause, index) => pc.white(`${index + 1}. ${cause}`)).join("\n");
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
- async function runExplainCommand(errorMessage, deps = {}) {
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
- log.info(formatOutput(result));
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
  }