vibe-code-explainer 0.2.1 → 0.3.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 (31) hide show
  1. package/README.md +105 -44
  2. package/dist/{chunk-5NCRRHU7.js → chunk-RK7ZFN4W.js} +10 -2
  3. package/dist/chunk-RK7ZFN4W.js.map +1 -0
  4. package/dist/chunk-Y45OIJ6Q.js +429 -0
  5. package/dist/chunk-Y45OIJ6Q.js.map +1 -0
  6. package/dist/chunk-Y55I7ZS5.js +604 -0
  7. package/dist/chunk-Y55I7ZS5.js.map +1 -0
  8. package/dist/cli/index.js +6 -6
  9. package/dist/{config-5PDPXG7Z.js → config-AHHWBME7.js} +19 -2
  10. package/dist/config-AHHWBME7.js.map +1 -0
  11. package/dist/hooks/post-tool.js +62 -26
  12. package/dist/hooks/post-tool.js.map +1 -1
  13. package/dist/{init-KUVD2YGA.js → init-ZR6C4F7Q.js} +19 -3
  14. package/dist/init-ZR6C4F7Q.js.map +1 -0
  15. package/dist/ollama-LAUI7N5U.js +14 -0
  16. package/dist/{schema-TBXFNCIG.js → schema-YEJIXFMK.js} +4 -2
  17. package/dist/{tracker-HCWPUZIO.js → tracker-4ORSFJQB.js} +4 -2
  18. package/dist/{uninstall-CNGJWJYQ.js → uninstall-AIH4HVPZ.js} +2 -2
  19. package/package.json +1 -1
  20. package/dist/chunk-5NCRRHU7.js.map +0 -1
  21. package/dist/chunk-W67RX53R.js +0 -347
  22. package/dist/chunk-W67RX53R.js.map +0 -1
  23. package/dist/chunk-YS2XIZIA.js +0 -544
  24. package/dist/chunk-YS2XIZIA.js.map +0 -1
  25. package/dist/config-5PDPXG7Z.js.map +0 -1
  26. package/dist/init-KUVD2YGA.js.map +0 -1
  27. package/dist/ollama-34TOVCUY.js +0 -12
  28. /package/dist/{ollama-34TOVCUY.js.map → ollama-LAUI7N5U.js.map} +0 -0
  29. /package/dist/{schema-TBXFNCIG.js.map → schema-YEJIXFMK.js.map} +0 -0
  30. /package/dist/{tracker-HCWPUZIO.js.map → tracker-4ORSFJQB.js.map} +0 -0
  31. /package/dist/{uninstall-CNGJWJYQ.js.map → uninstall-AIH4HVPZ.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session/tracker.ts","../src/cache/explanation-cache.ts","../src/format/box.ts"],"sourcesContent":["import { existsSync, readFileSync, appendFileSync, unlinkSync, mkdirSync, readdirSync, statSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { RiskLevel } from \"../config/schema.js\";\nimport { clearCache } from \"../cache/explanation-cache.js\";\nimport { formatDriftAlert, printToStderr } from \"../format/box.js\";\n\nconst TWO_HOURS_MS = 2 * 60 * 60 * 1000;\n\nexport interface SessionEntry {\n file: string;\n timestamp: number;\n risk: RiskLevel;\n summary: string;\n unrelated?: boolean;\n}\n\nfunction getUserTmpDir(): string {\n const dir = join(tmpdir(), `code-explainer-${process.getuid?.() ?? \"user\"}`);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n return dir;\n}\n\nexport function getSessionFilePath(sessionId: string): string {\n return join(getUserTmpDir(), `session-${sessionId}.jsonl`);\n}\n\nexport function recordEntry(sessionId: string, entry: SessionEntry): void {\n const path = getSessionFilePath(sessionId);\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n } catch {\n // Non-fatal\n }\n}\n\nexport function readSession(sessionId: string): SessionEntry[] {\n const path = getSessionFilePath(sessionId);\n if (!existsSync(path)) return [];\n\n try {\n const content = readFileSync(path, \"utf-8\");\n return content\n .split(\"\\n\")\n .filter((l) => l.trim())\n .map((line) => {\n try {\n return JSON.parse(line) as SessionEntry;\n } catch {\n return null;\n }\n })\n .filter((e): e is SessionEntry => e !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * Get the last N recorded summaries for this session, oldest-first.\n * Used to feed prompt context for \"same pattern\" detection.\n */\nexport function getRecentSummaries(sessionId: string, n: number): string[] {\n const entries = readSession(sessionId);\n if (entries.length === 0) return [];\n return entries.slice(-n).map((e) => `${e.file}: ${e.summary}`);\n}\n\nexport function cleanStaleSessionFiles(): void {\n try {\n const dir = getUserTmpDir();\n const now = Date.now();\n const entries = readdirSync(dir);\n for (const name of entries) {\n if (!name.startsWith(\"session-\") && !name.startsWith(\"cache-\")) continue;\n const filePath = join(dir, name);\n try {\n const stat = statSync(filePath);\n if (now - stat.mtimeMs > TWO_HOURS_MS) {\n unlinkSync(filePath);\n }\n } catch {\n // ignore\n }\n }\n } catch {\n // ignore\n }\n}\n\nfunction getSessionIdFromEnv(): string | undefined {\n return process.env.CODE_EXPLAINER_SESSION_ID;\n}\n\nfunction findLatestSession(): string | undefined {\n try {\n const dir = getUserTmpDir();\n const entries = readdirSync(dir)\n .filter((n) => n.startsWith(\"session-\") && n.endsWith(\".jsonl\"))\n .map((n) => ({\n name: n,\n id: n.slice(\"session-\".length, -\".jsonl\".length),\n mtime: statSync(join(dir, n)).mtimeMs,\n }))\n .sort((a, b) => b.mtime - a.mtime);\n return entries[0]?.id;\n } catch {\n return undefined;\n }\n}\n\nexport async function printSummary(): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n process.stderr.write(\"[code-explainer] No active session found. Session data is created when Claude Code makes changes.\\n\");\n return;\n }\n\n const entries = readSession(sessionId);\n if (entries.length === 0) {\n process.stderr.write(`[code-explainer] Session '${sessionId}' has no recorded changes yet.\\n`);\n return;\n }\n\n const related = entries.filter((e) => !e.unrelated);\n const unrelated = entries.filter((e) => e.unrelated);\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(new Set(unrelated.map((e) => e.file)));\n\n const alert = formatDriftAlert(uniqueFiles.length, unrelatedFiles);\n printToStderr(alert);\n\n process.stderr.write(`\\nTotal changes: ${entries.length}\\n`);\n process.stderr.write(`Files touched: ${uniqueFiles.length}\\n`);\n process.stderr.write(`Related changes: ${related.length}\\n`);\n process.stderr.write(`Unrelated/risky: ${unrelated.length}\\n`);\n\n const risks: Record<RiskLevel, number> = { none: 0, low: 0, medium: 0, high: 0 };\n for (const e of entries) risks[e.risk]++;\n process.stderr.write(`\\nRisk breakdown:\\n`);\n process.stderr.write(` None: ${risks.none}\\n`);\n process.stderr.write(` Low: ${risks.low}\\n`);\n process.stderr.write(` Medium: ${risks.medium}\\n`);\n process.stderr.write(` High: ${risks.high}\\n`);\n}\n\nexport async function endSession(): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n process.stderr.write(\"[code-explainer] No active session to end.\\n\");\n return;\n }\n\n const sessionPath = getSessionFilePath(sessionId);\n if (existsSync(sessionPath)) {\n try {\n unlinkSync(sessionPath);\n } catch {\n // ignore\n }\n }\n clearCache(sessionId);\n process.stderr.write(`[code-explainer] Session '${sessionId}' ended. State cleared.\\n`);\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync, appendFileSync, unlinkSync, mkdirSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { ExplanationResult } from \"../config/schema.js\";\n\nfunction getUserTmpDir(): string {\n const dir = join(tmpdir(), `code-explainer-${process.getuid?.() ?? \"user\"}`);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n return dir;\n}\n\nexport function getCacheFilePath(sessionId: string): string {\n return join(getUserTmpDir(), `cache-${sessionId}.jsonl`);\n}\n\nexport function hashDiff(diff: string): string {\n return createHash(\"sha256\").update(diff, \"utf-8\").digest(\"hex\");\n}\n\ninterface CacheEntry {\n hash: string;\n result: ExplanationResult;\n}\n\nexport function getCached(sessionId: string, diff: string): ExplanationResult | undefined {\n const path = getCacheFilePath(sessionId);\n if (!existsSync(path)) return undefined;\n\n const hash = hashDiff(diff);\n try {\n const content = readFileSync(path, \"utf-8\");\n const lines = content.split(\"\\n\").filter((l) => l.trim());\n\n // Iterate in reverse so the most recent entry wins on duplicates.\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const entry = JSON.parse(lines[i]) as CacheEntry;\n if (entry.hash === hash) {\n return entry.result;\n }\n } catch {\n // Skip malformed line\n }\n }\n } catch {\n return undefined;\n }\n return undefined;\n}\n\nexport function setCached(sessionId: string, diff: string, result: ExplanationResult): void {\n const path = getCacheFilePath(sessionId);\n const entry: CacheEntry = { hash: hashDiff(diff), result };\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n } catch {\n // Cache write failures are non-fatal\n }\n}\n\nexport function clearCache(sessionId: string): void {\n const path = getCacheFilePath(sessionId);\n if (existsSync(path)) {\n try {\n unlinkSync(path);\n } catch {\n // ignore\n }\n }\n}\n","import pc from \"picocolors\";\nimport type { ExplanationResult, Language, RiskLevel, DetailLevel } from \"../config/schema.js\";\n\n// ===========================================================================\n// Section header translations per language\n// ===========================================================================\n\ninterface SectionLabels {\n impact: string;\n howItWorks: string;\n why: string;\n deepDive: string;\n risk: string;\n riskNone: string;\n riskLow: string;\n riskMedium: string;\n riskHigh: string;\n samePatternFallback: string;\n}\n\nconst LABELS: Record<Language, SectionLabels> = {\n en: {\n impact: \"Impact\",\n howItWorks: \"How it works\",\n why: \"Why\",\n deepDive: \"Deeper dive\",\n risk: \"Risk\",\n riskNone: \"None\",\n riskLow: \"Low\",\n riskMedium: \"Medium\",\n riskHigh: \"High\",\n samePatternFallback: \"Same pattern as before applied to this file.\",\n },\n pt: {\n impact: \"Impacto\",\n howItWorks: \"Como funciona\",\n why: \"Por que\",\n deepDive: \"Pra aprofundar\",\n risk: \"Risco\",\n riskNone: \"Nenhum\",\n riskLow: \"Baixo\",\n riskMedium: \"M\\u00e9dio\",\n riskHigh: \"Alto\",\n samePatternFallback: \"Mesmo padr\\u00e3o anterior aplicado a este arquivo.\",\n },\n es: {\n impact: \"Impacto\",\n howItWorks: \"C\\u00f3mo funciona\",\n why: \"Por qu\\u00e9\",\n deepDive: \"Para profundizar\",\n risk: \"Riesgo\",\n riskNone: \"Ninguno\",\n riskLow: \"Bajo\",\n riskMedium: \"Medio\",\n riskHigh: \"Alto\",\n samePatternFallback: \"Mismo patr\\u00f3n anterior aplicado a este archivo.\",\n },\n fr: {\n impact: \"Impact\",\n howItWorks: \"Comment \\u00e7a marche\",\n why: \"Pourquoi\",\n deepDive: \"Pour approfondir\",\n risk: \"Risque\",\n riskNone: \"Aucun\",\n riskLow: \"Faible\",\n riskMedium: \"Moyen\",\n riskHigh: \"\\u00c9lev\\u00e9\",\n samePatternFallback: \"M\\u00eame motif que pr\\u00e9c\\u00e9demment, appliqu\\u00e9 \\u00e0 ce fichier.\",\n },\n de: {\n impact: \"Auswirkung\",\n howItWorks: \"Wie es funktioniert\",\n why: \"Warum\",\n deepDive: \"Mehr lernen\",\n risk: \"Risiko\",\n riskNone: \"Keines\",\n riskLow: \"Gering\",\n riskMedium: \"Mittel\",\n riskHigh: \"Hoch\",\n samePatternFallback: \"Gleiches Muster wie zuvor auf diese Datei angewendet.\",\n },\n it: {\n impact: \"Impatto\",\n howItWorks: \"Come funziona\",\n why: \"Perch\\u00e9\",\n deepDive: \"Per approfondire\",\n risk: \"Rischio\",\n riskNone: \"Nessuno\",\n riskLow: \"Basso\",\n riskMedium: \"Medio\",\n riskHigh: \"Alto\",\n samePatternFallback: \"Stesso schema applicato a questo file.\",\n },\n zh: {\n impact: \"\\u5f71\\u54cd\",\n howItWorks: \"\\u5982\\u4f55\\u5de5\\u4f5c\",\n why: \"\\u4e3a\\u4ec0\\u4e48\",\n deepDive: \"\\u6df1\\u5165\\u5b66\\u4e60\",\n risk: \"\\u98ce\\u9669\",\n riskNone: \"\\u65e0\",\n riskLow: \"\\u4f4e\",\n riskMedium: \"\\u4e2d\",\n riskHigh: \"\\u9ad8\",\n samePatternFallback: \"\\u540c\\u6837\\u7684\\u6a21\\u5f0f\\u5e94\\u7528\\u5230\\u6b64\\u6587\\u4ef6\\u3002\",\n },\n ja: {\n impact: \"\\u5f71\\u97ff\",\n howItWorks: \"\\u4ed5\\u7d44\\u307f\",\n why: \"\\u306a\\u305c\",\n deepDive: \"\\u3055\\u3089\\u306b\\u5b66\\u3076\",\n risk: \"\\u30ea\\u30b9\\u30af\",\n riskNone: \"\\u306a\\u3057\",\n riskLow: \"\\u4f4e\",\n riskMedium: \"\\u4e2d\",\n riskHigh: \"\\u9ad8\",\n samePatternFallback: \"\\u4ee5\\u524d\\u3068\\u540c\\u3058\\u30d1\\u30bf\\u30fc\\u30f3\\u3092\\u3053\\u306e\\u30d5\\u30a1\\u30a4\\u30eb\\u306b\\u9069\\u7528\\u3002\",\n },\n ko: {\n impact: \"\\uc601\\ud5a5\",\n howItWorks: \"\\uc791\\ub3d9 \\ubc29\\uc2dd\",\n why: \"\\uc774\\uc720\",\n deepDive: \"\\ub354 \\uc54c\\uc544\\ubcf4\\uae30\",\n risk: \"\\uc704\\ud5d8\",\n riskNone: \"\\uc5c6\\uc74c\",\n riskLow: \"\\ub0ae\\uc74c\",\n riskMedium: \"\\ubcf4\\ud1b5\",\n riskHigh: \"\\ub192\\uc74c\",\n samePatternFallback: \"\\uc774\\uc804\\uacfc \\ub3d9\\uc77c\\ud55c \\ud328\\ud134\\uc774 \\uc774 \\ud30c\\uc77c\\uc5d0 \\uc801\\uc6a9\\ub418\\uc5c8\\uc2b5\\ub2c8\\ub2e4.\",\n },\n};\n\nfunction getLabels(language: Language): SectionLabels {\n return LABELS[language] ?? LABELS.en;\n}\n\n// ===========================================================================\n// Color helpers\n// ===========================================================================\n\nfunction isNoColor(): boolean {\n return \"NO_COLOR\" in process.env || process.env.TERM === \"dumb\";\n}\n\nfunction color(fn: (s: string) => string, text: string): string {\n return isNoColor() ? text : fn(text);\n}\n\nfunction getTerminalWidth(): number {\n return process.stderr.columns || 80;\n}\n\n// ===========================================================================\n// Risk presentation\n// ===========================================================================\n\nfunction riskBorderColor(risk: RiskLevel): (s: string) => string {\n switch (risk) {\n case \"none\": return pc.green;\n case \"low\": return pc.yellow;\n case \"medium\": return pc.yellow;\n case \"high\": return pc.red;\n }\n}\n\nfunction riskIcon(risk: RiskLevel): string {\n if (isNoColor()) {\n switch (risk) {\n case \"none\": return \"[OK]\";\n case \"low\": return \"[!]\";\n case \"medium\": return \"[!!]\";\n case \"high\": return \"[!!!]\";\n }\n }\n switch (risk) {\n case \"none\": return color(pc.green, \"\\u2713\");\n case \"low\": return color(pc.yellow, \"\\u26a0\");\n case \"medium\": return color(pc.yellow, \"\\u26a0\");\n case \"high\": return color(pc.red, \"\\u{1F6A8}\");\n }\n}\n\nfunction riskLabelText(risk: RiskLevel, labels: SectionLabels): string {\n switch (risk) {\n case \"none\": return labels.riskNone;\n case \"low\": return labels.riskLow;\n case \"medium\": return labels.riskMedium;\n case \"high\": return labels.riskHigh;\n }\n}\n\nfunction riskLabelColor(risk: RiskLevel): (s: string) => string {\n switch (risk) {\n case \"none\": return pc.green;\n case \"low\": return pc.yellow;\n case \"medium\": return pc.yellow;\n case \"high\": return pc.red;\n }\n}\n\n// ===========================================================================\n// Inline code highlighting (`backticks` -> bright cyan)\n// ===========================================================================\n\nfunction highlightInlineCode(text: string): string {\n if (isNoColor()) return text;\n return text.replace(/`([^`]+)`/g, (_, code: string) => color(pc.cyan, code));\n}\n\n// ===========================================================================\n// Word wrap that respects a content width (no ANSI awareness needed since\n// we wrap BEFORE adding color)\n// ===========================================================================\n\nfunction wrapText(text: string, maxWidth: number): string[] {\n const out: string[] = [];\n for (const raw of text.split(\"\\n\")) {\n if (raw.length <= maxWidth) {\n out.push(raw);\n continue;\n }\n let remaining = raw;\n while (remaining.length > maxWidth) {\n let breakAt = remaining.lastIndexOf(\" \", maxWidth);\n if (breakAt <= 0) breakAt = maxWidth;\n out.push(remaining.slice(0, breakAt));\n remaining = remaining.slice(breakAt).trimStart();\n }\n if (remaining) out.push(remaining);\n }\n return out;\n}\n\n// ===========================================================================\n// Box construction\n// ===========================================================================\n\nconst BOX_TITLE = \"vibe-code-explainer\";\nconst PAD_LEFT = 2;\nconst PAD_RIGHT = 2;\n\ninterface BoxLine {\n text: string; // Already styled\n raw: string; // Raw (uncolored) version, used for width calculation\n}\n\nfunction line(raw: string, styled?: string): BoxLine {\n return { text: styled ?? raw, raw };\n}\n\nfunction blankLine(): BoxLine {\n return line(\"\");\n}\n\nfunction buildBoxOutput(\n contentLines: BoxLine[],\n borderColor: (s: string) => string\n): string {\n const width = Math.min(getTerminalWidth() - 2, 70);\n const innerWidth = width - 2; // chars between │ │\n\n const top = `\\u256d\\u2500 ${color(pc.dim, BOX_TITLE)} ${\"\\u2500\".repeat(Math.max(0, innerWidth - BOX_TITLE.length - 4))}\\u2500\\u256e`;\n const bottom = `\\u2570${\"\\u2500\".repeat(innerWidth)}\\u256f`;\n\n const sideChar = color(borderColor, \"\\u2502\");\n\n const middle = contentLines.map((bl) => {\n const padding = \" \".repeat(Math.max(0, innerWidth - bl.raw.length - PAD_LEFT - PAD_RIGHT));\n return `${sideChar}${\" \".repeat(PAD_LEFT)}${bl.text}${padding}${\" \".repeat(PAD_RIGHT)}${sideChar}`;\n });\n\n return [color(borderColor, top), ...middle, color(borderColor, bottom)].join(\"\\n\");\n}\n\n// ===========================================================================\n// Section rendering\n// ===========================================================================\n\ninterface SectionDef {\n header: string;\n headerColor: (s: string) => string;\n body: string;\n innerWidth: number;\n}\n\nfunction renderSection(def: SectionDef): BoxLine[] {\n const out: BoxLine[] = [];\n const headerRaw = `\\u25b8 ${def.header}`;\n const headerStyled = color(pc.bold, color(def.headerColor, headerRaw));\n out.push(line(headerRaw, headerStyled));\n\n const bodyMax = def.innerWidth - PAD_LEFT - PAD_RIGHT - 2; // 2 = body indent\n const wrapped = wrapText(def.body, bodyMax);\n for (const w of wrapped) {\n const indented = ` ${w}`;\n const styled = ` ${highlightInlineCode(w)}`;\n out.push(line(indented, styled));\n }\n\n return out;\n}\n\n// ===========================================================================\n// Public API\n// ===========================================================================\n\nexport interface BoxInputs {\n filePath: string;\n result: ExplanationResult;\n detailLevel: DetailLevel;\n language: Language;\n}\n\nexport function formatExplanationBox(inputs: BoxInputs): string {\n const labels = getLabels(inputs.language);\n const result = inputs.result;\n const borderFn = riskBorderColor(result.risk);\n const lines: BoxLine[] = [];\n const innerWidth = Math.min(getTerminalWidth() - 2, 70) - 2;\n\n lines.push(blankLine());\n\n // File path with 📄 icon, bright cyan + bold\n const filePathRaw = `\\ud83d\\udcc4 ${inputs.filePath}`;\n const filePathStyled = color(pc.bold, color(pc.cyan, filePathRaw));\n lines.push(line(filePathRaw, filePathStyled));\n\n // Same-pattern collapse: short note, no teaching sections\n if (result.isSamePattern) {\n lines.push(blankLine());\n const noteRaw = result.samePatternNote || labels.samePatternFallback;\n const noteWrapped = wrapText(noteRaw, innerWidth - PAD_LEFT - PAD_RIGHT);\n for (const w of noteWrapped) {\n lines.push(line(w, color(pc.dim, w)));\n }\n } else {\n // Impact (always shown when not collapsed)\n if (result.impact) {\n lines.push(blankLine());\n if (inputs.detailLevel === \"minimal\") {\n // Minimal: no header, just the text\n const wrapped = wrapText(result.impact, innerWidth - PAD_LEFT - PAD_RIGHT);\n for (const w of wrapped) {\n lines.push(line(w, highlightInlineCode(w)));\n }\n } else {\n const sec = renderSection({\n header: labels.impact,\n headerColor: pc.cyan,\n body: result.impact,\n innerWidth,\n });\n lines.push(...sec);\n }\n }\n\n // How it works (standard + verbose)\n if (inputs.detailLevel !== \"minimal\" && result.howItWorks) {\n lines.push(blankLine());\n const sec = renderSection({\n header: labels.howItWorks,\n headerColor: pc.green,\n body: result.howItWorks,\n innerWidth,\n });\n lines.push(...sec);\n }\n\n // Why (standard + verbose)\n if (inputs.detailLevel !== \"minimal\" && result.why) {\n lines.push(blankLine());\n const sec = renderSection({\n header: labels.why,\n headerColor: pc.magenta,\n body: result.why,\n innerWidth,\n });\n lines.push(...sec);\n }\n\n // Deep dive (verbose only)\n if (\n inputs.detailLevel === \"verbose\" &&\n result.deepDive &&\n result.deepDive.length > 0\n ) {\n lines.push(blankLine());\n const headerRaw = `\\u25b8 ${labels.deepDive}`;\n const headerStyled = color(pc.bold, color(pc.dim, headerRaw));\n lines.push(line(headerRaw, headerStyled));\n const itemMax = innerWidth - PAD_LEFT - PAD_RIGHT - 4; // dash + padding\n for (const item of result.deepDive) {\n const text = `${item.term}: ${item.explanation}`;\n const wrapped = wrapText(text, itemMax);\n for (let i = 0; i < wrapped.length; i++) {\n const prefix = i === 0 ? \" \\u2014 \" : \" \";\n const raw = `${prefix}${wrapped[i]}`;\n const styled = `${prefix}${highlightInlineCode(wrapped[i])}`;\n lines.push(line(raw, styled));\n }\n }\n }\n }\n\n // Divider before risk\n lines.push(blankLine());\n const dividerWidth = innerWidth - PAD_LEFT - PAD_RIGHT;\n const dividerRaw = \"\\u2504\".repeat(dividerWidth);\n lines.push(line(dividerRaw, color(pc.dim, dividerRaw)));\n lines.push(blankLine());\n\n // Risk row\n const riskHeaderRaw = `${stripAnsi(riskIcon(result.risk))} ${labels.risk}: ${riskLabelText(result.risk, labels)}`;\n const riskHeaderStyled = `${riskIcon(result.risk)} ${color(pc.bold, color(riskLabelColor(result.risk), `${labels.risk}: ${riskLabelText(result.risk, labels)}`))}`;\n lines.push(line(riskHeaderRaw, riskHeaderStyled));\n\n if (result.risk !== \"none\" && result.riskReason) {\n const reasonMax = innerWidth - PAD_LEFT - PAD_RIGHT - 3;\n const wrapped = wrapText(result.riskReason, reasonMax);\n for (const w of wrapped) {\n const raw = ` ${w}`;\n const styled = ` ${color(pc.dim, color(riskLabelColor(result.risk), w))}`;\n lines.push(line(raw, styled));\n }\n }\n\n lines.push(blankLine());\n\n return buildBoxOutput(lines, borderFn);\n}\n\n// ===========================================================================\n// Misc box variants (skip notice, error notice, drift alert)\n// ===========================================================================\n\nexport function formatSkipNotice(reason: string): string {\n return color(pc.dim, `[code-explainer] skipped: ${reason}`);\n}\n\nexport function formatErrorNotice(problem: string, cause: string, fix: string): string {\n return color(pc.yellow, `[code-explainer] ${problem}. ${cause}. Fix: ${fix}.`);\n}\n\nexport function formatDriftAlert(\n totalFiles: number,\n unrelatedFiles: string[],\n userRequest?: string,\n language: Language = \"en\"\n): string {\n const labels = getLabels(language);\n const lines: BoxLine[] = [];\n const innerWidth = Math.min(getTerminalWidth() - 2, 70) - 2;\n\n lines.push(blankLine());\n\n const headerRaw = `\\u26a1 SESSION DRIFT`;\n const headerStyled = color(pc.bold, color(pc.yellow, headerRaw));\n lines.push(line(headerRaw, headerStyled));\n\n lines.push(blankLine());\n\n const summaryRaw = `Claude has modified ${totalFiles} files this session.`;\n lines.push(line(summaryRaw));\n\n const unrelatedRaw = `${unrelatedFiles.length} may be unrelated:`;\n lines.push(line(unrelatedRaw));\n\n for (const file of unrelatedFiles) {\n const truncated = file.length > innerWidth - 8 ? file.slice(0, innerWidth - 11) + \"...\" : file;\n const raw = ` \\u2022 ${truncated}`;\n const styled = ` ${color(pc.yellow, \"\\u2022\")} ${truncated}`;\n lines.push(line(raw, styled));\n }\n\n if (userRequest) {\n lines.push(blankLine());\n const requestLines = wrapText(`Your request: \"${userRequest}\"`, innerWidth - PAD_LEFT - PAD_RIGHT);\n for (const w of requestLines) {\n lines.push(line(w, color(pc.dim, w)));\n }\n }\n\n lines.push(blankLine());\n const noticeRaw = `\\u26a0 Consider reviewing these changes.`;\n lines.push(line(noticeRaw, color(pc.bold, color(pc.yellow, noticeRaw))));\n lines.push(blankLine());\n\n return buildBoxOutput(lines, pc.yellow);\n}\n\n/**\n * Write directly to the controlling terminal — Claude Code captures stdio,\n * but for non-hook contexts (init, summary, warmup) we want output on the\n * actual terminal. Falls back to stderr.\n */\nexport function printToStderr(text: string): void {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require(\"node:fs\") as typeof import(\"node:fs\");\n const ttyPath = process.platform === \"win32\" ? \"\\\\\\\\.\\\\CONOUT$\" : \"/dev/tty\";\n const fd = fs.openSync(ttyPath, \"w\");\n fs.writeSync(fd, text + \"\\n\");\n fs.closeSync(fd);\n } catch {\n process.stderr.write(text + \"\\n\");\n }\n}\n\nfunction stripAnsi(s: string): string {\n // eslint-disable-next-line no-control-regex\n return s.replace(/\\u001b\\[[0-9;]*m/g, \"\");\n}\n"],"mappings":"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","names":["existsSync","readFileSync","appendFileSync","unlinkSync","mkdirSync","tmpdir","join","getUserTmpDir","join","tmpdir","existsSync","mkdirSync","appendFileSync","readFileSync","line","unlinkSync"]}
package/dist/cli/index.js CHANGED
@@ -6,29 +6,29 @@ var command = args[0];
6
6
  async function main() {
7
7
  switch (command) {
8
8
  case "init": {
9
- const { runInit } = await import("../init-KUVD2YGA.js");
9
+ const { runInit } = await import("../init-ZR6C4F7Q.js");
10
10
  await runInit(args.slice(1));
11
11
  break;
12
12
  }
13
13
  case "config": {
14
- const { runConfig } = await import("../config-5PDPXG7Z.js");
14
+ const { runConfig } = await import("../config-AHHWBME7.js");
15
15
  await runConfig();
16
16
  break;
17
17
  }
18
18
  case "uninstall": {
19
- const { runUninstall } = await import("../uninstall-CNGJWJYQ.js");
19
+ const { runUninstall } = await import("../uninstall-AIH4HVPZ.js");
20
20
  await runUninstall();
21
21
  break;
22
22
  }
23
23
  case "summary": {
24
- const { printSummary } = await import("../tracker-HCWPUZIO.js");
24
+ const { printSummary } = await import("../tracker-4ORSFJQB.js");
25
25
  await printSummary();
26
26
  break;
27
27
  }
28
28
  case "session": {
29
29
  const subcommand = args[1];
30
30
  if (subcommand === "end") {
31
- const { endSession } = await import("../tracker-HCWPUZIO.js");
31
+ const { endSession } = await import("../tracker-4ORSFJQB.js");
32
32
  await endSession();
33
33
  } else {
34
34
  console.error("[code-explainer] Unknown session command. Usage: code-explainer session end");
@@ -37,7 +37,7 @@ async function main() {
37
37
  break;
38
38
  }
39
39
  case "warmup": {
40
- const { runWarmup } = await import("../ollama-34TOVCUY.js");
40
+ const { runWarmup } = await import("../ollama-LAUI7N5U.js");
41
41
  await runWarmup();
42
42
  break;
43
43
  }
@@ -6,9 +6,10 @@ import {
6
6
  CONFIG_FILENAME,
7
7
  DEFAULT_CONFIG,
8
8
  LANGUAGE_NAMES,
9
+ LEARNER_LEVEL_NAMES,
9
10
  getGlobalConfigPath,
10
11
  loadConfig
11
- } from "./chunk-5NCRRHU7.js";
12
+ } from "./chunk-RK7ZFN4W.js";
12
13
  import "./chunk-7OCVIDC7.js";
13
14
 
14
15
  // src/cli/config.ts
@@ -91,6 +92,7 @@ function renderCurrent(config) {
91
92
  `${pc.bold("Ollama URL: ")} ${config.ollamaUrl}`,
92
93
  `${pc.bold("Detail level: ")} ${config.detailLevel}`,
93
94
  `${pc.bold("Language: ")} ${LANGUAGE_NAMES[config.language]}`,
95
+ `${pc.bold("Learner level:")} ${LEARNER_LEVEL_NAMES[config.learnerLevel]}`,
94
96
  `${pc.bold("Hooks: ")} ${hooks.join(" \u2713 ") || "(all disabled)"}`,
95
97
  `${pc.bold("Excluded: ")} ${excluded}`,
96
98
  `${pc.bold("Skip if slow: ")} ${timeoutLabel}`
@@ -197,6 +199,19 @@ async function changeLanguage(config) {
197
199
  handleCancel(value);
198
200
  return { ...config, language: value };
199
201
  }
202
+ async function changeLevel(config) {
203
+ const value = await select({
204
+ message: "Programming knowledge level",
205
+ options: Object.keys(LEARNER_LEVEL_NAMES).map((code) => ({
206
+ label: LEARNER_LEVEL_NAMES[code],
207
+ value: code,
208
+ hint: code === "intermediate" ? "default" : void 0
209
+ })),
210
+ initialValue: config.learnerLevel
211
+ });
212
+ handleCancel(value);
213
+ return { ...config, learnerLevel: value };
214
+ }
200
215
  async function changeHooks(config) {
201
216
  const editOn = await confirm({ message: "Explain file edits?", initialValue: config.hooks.edit });
202
217
  handleCancel(editOn);
@@ -294,6 +309,7 @@ Run ${pc.cyan("npx vibe-code-explainer init")} first.`
294
309
  { label: "Ollama URL", value: "url" },
295
310
  { label: "Detail level", value: "detail" },
296
311
  { label: "Language", value: "language" },
312
+ { label: "Learner level", value: "level" },
297
313
  { label: "Enable/disable hooks", value: "hooks" },
298
314
  { label: "File exclusions", value: "exclude" },
299
315
  { label: "Latency timeout", value: "timeout" },
@@ -307,6 +323,7 @@ Run ${pc.cyan("npx vibe-code-explainer init")} first.`
307
323
  if (choice === "url") config = await changeUrl(config);
308
324
  if (choice === "detail") config = await changeDetail(config);
309
325
  if (choice === "language") config = await changeLanguage(config);
326
+ if (choice === "level") config = await changeLevel(config);
310
327
  if (choice === "hooks") config = await changeHooks(config);
311
328
  if (choice === "exclude") config = await changeExclude(config);
312
329
  if (choice === "timeout") config = await changeTimeout(config);
@@ -317,4 +334,4 @@ Run ${pc.cyan("npx vibe-code-explainer init")} first.`
317
334
  export {
318
335
  runConfig
319
336
  };
320
- //# sourceMappingURL=config-5PDPXG7Z.js.map
337
+ //# sourceMappingURL=config-AHHWBME7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/config.ts"],"sourcesContent":["import { intro, outro, select, confirm, text, cancel, isCancel, note } from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport { spawn } from \"node:child_process\";\nimport { existsSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport {\n DEFAULT_CONFIG,\n loadConfig,\n LANGUAGE_NAMES,\n LEARNER_LEVEL_NAMES,\n CONFIG_FILENAME,\n getGlobalConfigPath,\n type Config,\n type Engine,\n type DetailLevel,\n type Language,\n type LearnerLevel,\n} from \"../config/schema.js\";\nimport { MODEL_OPTIONS } from \"../detect/vram.js\";\n\ninterface OllamaTagResponse {\n models?: Array<{ name?: string; model?: string }>;\n}\n\nasync function listInstalledOllamaModels(url: string): Promise<string[] | null> {\n try {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), 3000);\n const res = await fetch(`${url}/api/tags`, { signal: ctrl.signal });\n clearTimeout(timer);\n if (!res.ok) return null;\n const data = (await res.json()) as OllamaTagResponse;\n if (!data.models) return [];\n return data.models\n .map((m) => m.name ?? m.model ?? \"\")\n .filter((n) => n.length > 0);\n } catch {\n return null;\n }\n}\n\nfunction normalizeModelName(name: string): string {\n // Ollama sometimes returns tags as \"qwen3.5:9b\" and sometimes as\n // \"qwen3.5:9b-q4_K_M\". Compare on the base \"<model>:<tag>\" prefix.\n return name.toLowerCase().split(/[-_]/)[0];\n}\n\nfunction hasModel(installed: string[], wanted: string): boolean {\n const wantedNorm = normalizeModelName(wanted);\n return installed.some((n) => {\n const base = n.toLowerCase();\n if (base === wanted.toLowerCase()) return true;\n // Match \"qwen3.5:9b\" when Ollama returns \"qwen3.5:9b\" exactly\n if (base === wanted.toLowerCase()) return true;\n // Looser match for variant tags\n return normalizeModelName(base).startsWith(wantedNorm);\n });\n}\n\nasync function pullOllamaModel(model: string): Promise<boolean> {\n note(\n `Pulling ${pc.cyan(model)}\\n${pc.dim(\"This can take a while on the first run (several GB download).\")}`,\n \"Downloading model\"\n );\n return new Promise((resolvePromise) => {\n const child = spawn(\"ollama\", [\"pull\", model], { stdio: \"inherit\" });\n child.on(\"error\", () => {\n process.stderr.write(\n pc.red(\"\\nFailed to run `ollama pull`. Make sure Ollama is installed and running.\\n\")\n );\n resolvePromise(false);\n });\n child.on(\"close\", (code) => {\n if (code === 0) {\n process.stdout.write(pc.green(`\\n\\u2713 Pulled ${model}\\n`));\n resolvePromise(true);\n } else {\n process.stderr.write(pc.red(`\\n\\u2717 ollama pull exited with code ${code}\\n`));\n resolvePromise(false);\n }\n });\n });\n}\n\n\nfunction handleCancel<T>(value: T | symbol): asserts value is T {\n if (isCancel(value)) {\n cancel(\"Exited without saving.\");\n process.exit(0);\n }\n}\n\nfunction renderCurrent(config: Config): string {\n const hooks: string[] = [];\n if (config.hooks.edit) hooks.push(\"Edit\");\n if (config.hooks.write) hooks.push(\"Write\");\n if (config.hooks.bash) hooks.push(\"Bash\");\n\n const excluded = config.exclude.length > 0 ? config.exclude.join(\", \") : \"(none)\";\n const timeoutLabel =\n config.skipIfSlowMs === 0 ? \"Never skip\" : `${Math.round(config.skipIfSlowMs / 1000)}s`;\n\n return [\n `${pc.bold(\"Engine: \")} ${config.engine === \"ollama\" ? \"Local LLM (Ollama)\" : \"Claude Code (native)\"}`,\n `${pc.bold(\"Model: \")} ${config.ollamaModel}`,\n `${pc.bold(\"Ollama URL: \")} ${config.ollamaUrl}`,\n `${pc.bold(\"Detail level: \")} ${config.detailLevel}`,\n `${pc.bold(\"Language: \")} ${LANGUAGE_NAMES[config.language]}`,\n `${pc.bold(\"Learner level:\")} ${LEARNER_LEVEL_NAMES[config.learnerLevel]}`,\n `${pc.bold(\"Hooks: \")} ${hooks.join(\" \\u2713 \") || \"(all disabled)\"}`,\n `${pc.bold(\"Excluded: \")} ${excluded}`,\n `${pc.bold(\"Skip if slow: \")} ${timeoutLabel}`,\n ].join(\"\\n\");\n}\n\ntype MenuChoice =\n | \"engine\"\n | \"model\"\n | \"url\"\n | \"detail\"\n | \"language\"\n | \"level\"\n | \"hooks\"\n | \"exclude\"\n | \"timeout\"\n | \"back\";\n\nasync function changeEngine(config: Config): Promise<Config> {\n const value = await select<Engine>({\n message: \"Explanation engine\",\n options: [\n { label: \"Local LLM (Ollama)\", value: \"ollama\", hint: \"free, private, works offline\" },\n { label: \"Claude Code (native)\", value: \"claude\", hint: \"best quality, uses API tokens\" },\n ],\n initialValue: config.engine,\n });\n handleCancel(value);\n return { ...config, engine: value };\n}\n\nasync function changeModel(config: Config): Promise<Config> {\n const value = await select({\n message: \"Ollama model\",\n options: MODEL_OPTIONS.map((m) => ({\n label: m.label,\n value: m.model,\n hint: m.hint,\n })),\n initialValue: config.ollamaModel,\n });\n handleCancel(value);\n\n if (value === config.ollamaModel) {\n // Nothing actually changed; skip the download check.\n return config;\n }\n\n // Check whether Ollama already has the model pulled. If not, offer to pull it.\n const installed = await listInstalledOllamaModels(config.ollamaUrl);\n if (installed === null) {\n note(\n `Could not reach Ollama at ${pc.cyan(config.ollamaUrl)}. The model will be selected, but you'll need to pull it manually with ${pc.cyan(`ollama pull ${value}`)} before the first explanation.`,\n \"Ollama unreachable\"\n );\n return { ...config, ollamaModel: value };\n }\n\n if (hasModel(installed, value)) {\n note(`${pc.green(\"\\u2713\")} Model ${pc.cyan(value)} is already installed.`, \"Model ready\");\n return { ...config, ollamaModel: value };\n }\n\n const shouldPull = await confirm({\n message: `Model ${value} is not installed locally. Pull it now?`,\n initialValue: true,\n });\n handleCancel(shouldPull);\n\n if (!shouldPull) {\n note(\n `Saved the selection, but you must run ${pc.cyan(`ollama pull ${value}`)} before it works.`,\n \"Model not pulled\"\n );\n return { ...config, ollamaModel: value };\n }\n\n const pullOk = await pullOllamaModel(value);\n if (!pullOk) {\n note(\n `Pull failed. Saving the model selection anyway — run ${pc.cyan(`ollama pull ${value}`)} manually when Ollama is reachable.`,\n \"Pull failed\"\n );\n }\n return { ...config, ollamaModel: value };\n}\n\nasync function changeUrl(config: Config): Promise<Config> {\n const value = await text({\n message: \"Ollama endpoint URL\",\n initialValue: config.ollamaUrl,\n validate(v) {\n try {\n new URL(v);\n return;\n } catch {\n return \"Must be a valid URL (e.g., http://localhost:11434)\";\n }\n },\n });\n handleCancel(value);\n return { ...config, ollamaUrl: value };\n}\n\nasync function changeDetail(config: Config): Promise<Config> {\n const value = await select<DetailLevel>({\n message: \"Detail level\",\n options: [\n { label: \"Standard\", value: \"standard\", hint: \"1-2 sentence explanation per change (recommended)\" },\n { label: \"Minimal\", value: \"minimal\", hint: \"one short sentence per change\" },\n { label: \"Verbose\", value: \"verbose\", hint: \"detailed bullet-point breakdown\" },\n ],\n initialValue: config.detailLevel,\n });\n handleCancel(value);\n return { ...config, detailLevel: value };\n}\n\nasync function changeLanguage(config: Config): Promise<Config> {\n const value = await select<Language>({\n message: \"Language for explanations\",\n options: (Object.keys(LANGUAGE_NAMES) as Language[]).map((code) => ({\n label: LANGUAGE_NAMES[code],\n value: code,\n hint: code === \"en\" ? \"default\" : undefined,\n })),\n initialValue: config.language,\n });\n handleCancel(value);\n return { ...config, language: value };\n}\n\nasync function changeLevel(config: Config): Promise<Config> {\n const value = await select<LearnerLevel>({\n message: \"Programming knowledge level\",\n options: (Object.keys(LEARNER_LEVEL_NAMES) as LearnerLevel[]).map((code) => ({\n label: LEARNER_LEVEL_NAMES[code],\n value: code,\n hint: code === \"intermediate\" ? \"default\" : undefined,\n })),\n initialValue: config.learnerLevel,\n });\n handleCancel(value);\n return { ...config, learnerLevel: value };\n}\n\nasync function changeHooks(config: Config): Promise<Config> {\n const editOn = await confirm({ message: \"Explain file edits?\", initialValue: config.hooks.edit });\n handleCancel(editOn);\n const writeOn = await confirm({ message: \"Explain new files?\", initialValue: config.hooks.write });\n handleCancel(writeOn);\n const bashOn = await confirm({\n message: \"Explain destructive Bash commands (rm, git reset, etc.)?\",\n initialValue: config.hooks.bash,\n });\n handleCancel(bashOn);\n\n return {\n ...config,\n hooks: { edit: editOn, write: writeOn, bash: bashOn },\n };\n}\n\nasync function changeExclude(config: Config): Promise<Config> {\n const action = await select({\n message: `Current exclusions: ${config.exclude.join(\", \") || \"(none)\"}`,\n options: [\n { label: \"Add a pattern\", value: \"add\", hint: \"e.g., *.generated.*\" },\n { label: \"Remove a pattern\", value: \"remove\" },\n { label: \"Reset to defaults\", value: \"reset\", hint: DEFAULT_CONFIG.exclude.join(\", \") },\n { label: \"Back\", value: \"back\" },\n ],\n });\n handleCancel(action);\n\n if (action === \"back\") return config;\n if (action === \"reset\") return { ...config, exclude: [...DEFAULT_CONFIG.exclude] };\n\n if (action === \"add\") {\n const pattern = await text({ message: \"Glob pattern to exclude (e.g., *.generated.*)\" });\n handleCancel(pattern);\n if (!pattern.trim()) return config;\n const exclude = Array.from(new Set([...config.exclude, pattern.trim()]));\n return { ...config, exclude };\n }\n\n if (action === \"remove\") {\n if (config.exclude.length === 0) {\n note(\"No exclusions to remove.\", \"Exclusions\");\n return config;\n }\n const target = await select({\n message: \"Which pattern to remove?\",\n options: config.exclude.map((p) => ({ label: p, value: p })),\n });\n handleCancel(target);\n const exclude = config.exclude.filter((p) => p !== target);\n return { ...config, exclude };\n }\n\n return config;\n}\n\nasync function changeTimeout(config: Config): Promise<Config> {\n const value = await select<number>({\n message: \"Skip explanation if it takes longer than...\",\n options: [\n { label: \"5 seconds\", value: 5000, hint: \"fast, may skip complex changes\" },\n { label: \"8 seconds\", value: 8000, hint: \"balanced (recommended)\" },\n { label: \"15 seconds\", value: 15000, hint: \"patient, rarely skips\" },\n { label: \"Never skip\", value: 0, hint: \"always wait for the explanation\" },\n ],\n initialValue: config.skipIfSlowMs,\n });\n handleCancel(value);\n return { ...config, skipIfSlowMs: value };\n}\n\nexport async function runConfig(): Promise<void> {\n // Prefer project config if present; otherwise edit the global config.\n const projectPath = join(process.cwd(), CONFIG_FILENAME);\n const globalPath = getGlobalConfigPath();\n\n let configPath: string;\n let scope: \"project\" | \"global\";\n if (existsSync(projectPath)) {\n configPath = projectPath;\n scope = \"project\";\n } else if (existsSync(globalPath)) {\n configPath = globalPath;\n scope = \"global\";\n } else {\n intro(pc.bold(\"code-explainer config\"));\n cancel(\n `No config file found.\\nSearched: ${pc.cyan(projectPath)}\\n ${pc.cyan(globalPath)}\\nRun ${pc.cyan(\"npx vibe-code-explainer init\")} first.`\n );\n process.exit(1);\n }\n\n intro(pc.bold(`code-explainer config (${scope})`));\n\n let config = loadConfig(configPath);\n\n while (true) {\n note(renderCurrent(config), \"Current settings\");\n\n const choice = await select<MenuChoice>({\n message: \"What would you like to change?\",\n options: [\n { label: \"Engine\", value: \"engine\" },\n { label: \"Model\", value: \"model\" },\n { label: \"Ollama URL\", value: \"url\" },\n { label: \"Detail level\", value: \"detail\" },\n { label: \"Language\", value: \"language\" },\n { label: \"Learner level\", value: \"level\" },\n { label: \"Enable/disable hooks\", value: \"hooks\" },\n { label: \"File exclusions\", value: \"exclude\" },\n { label: \"Latency timeout\", value: \"timeout\" },\n { label: \"Back (save and exit)\", value: \"back\" },\n ],\n });\n handleCancel(choice);\n\n if (choice === \"back\") break;\n if (choice === \"engine\") config = await changeEngine(config);\n if (choice === \"model\") config = await changeModel(config);\n if (choice === \"url\") config = await changeUrl(config);\n if (choice === \"detail\") config = await changeDetail(config);\n if (choice === \"language\") config = await changeLanguage(config);\n if (choice === \"level\") config = await changeLevel(config);\n if (choice === \"hooks\") config = await changeHooks(config);\n if (choice === \"exclude\") config = await changeExclude(config);\n if (choice === \"timeout\") config = await changeTimeout(config);\n\n writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\");\n }\n\n outro(pc.green(\"Settings saved.\"));\n}\n"],"mappings":"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","names":[]}
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  buildClaudePrompt,
4
4
  callOllama
5
- } from "../chunk-YS2XIZIA.js";
5
+ } from "../chunk-Y45OIJ6Q.js";
6
6
  import {
7
7
  DEFAULT_CONFIG,
8
8
  loadConfig
9
- } from "../chunk-5NCRRHU7.js";
9
+ } from "../chunk-RK7ZFN4W.js";
10
10
  import {
11
11
  cleanStaleSessionFiles,
12
12
  formatDriftAlert,
@@ -14,10 +14,11 @@ import {
14
14
  formatExplanationBox,
15
15
  formatSkipNotice,
16
16
  getCached,
17
+ getRecentSummaries,
17
18
  readSession,
18
19
  recordEntry,
19
20
  setCached
20
- } from "../chunk-W67RX53R.js";
21
+ } from "../chunk-Y55I7ZS5.js";
21
22
  import "../chunk-7OCVIDC7.js";
22
23
 
23
24
  // src/hooks/post-tool.ts
@@ -41,17 +42,33 @@ function extractJson(text) {
41
42
  }
42
43
  return null;
43
44
  }
45
+ function coerceString(v) {
46
+ return typeof v === "string" ? v : "";
47
+ }
48
+ function coerceDeepDive(v) {
49
+ if (!Array.isArray(v)) return [];
50
+ return v.filter((it) => typeof it === "object" && it !== null).map((it) => ({
51
+ term: coerceString(it.term),
52
+ explanation: coerceString(it.explanation)
53
+ })).filter((it) => it.term.length > 0);
54
+ }
44
55
  function parseResponse(rawText) {
45
56
  const json = extractJson(rawText);
46
57
  if (!json) return null;
47
58
  try {
48
59
  const parsed = JSON.parse(json);
49
- if (typeof parsed.summary === "string" && typeof parsed.risk === "string" && typeof parsed.riskReason === "string") {
50
- const risk = parsed.risk;
51
- if (!["none", "low", "medium", "high"].includes(risk)) return null;
52
- return { summary: parsed.summary, risk, riskReason: parsed.riskReason };
53
- }
54
- return null;
60
+ const risk = coerceString(parsed.risk);
61
+ if (!["none", "low", "medium", "high"].includes(risk)) return null;
62
+ return {
63
+ impact: coerceString(parsed.impact),
64
+ howItWorks: coerceString(parsed.howItWorks),
65
+ why: coerceString(parsed.why),
66
+ deepDive: coerceDeepDive(parsed.deepDive),
67
+ isSamePattern: parsed.isSamePattern === true,
68
+ samePatternNote: coerceString(parsed.samePatternNote),
69
+ risk,
70
+ riskReason: coerceString(parsed.riskReason)
71
+ };
55
72
  } catch {
56
73
  return null;
57
74
  }
@@ -98,7 +115,9 @@ async function callClaude(inputs) {
98
115
  filePath: inputs.filePath,
99
116
  diff: inputs.diff,
100
117
  userPrompt: inputs.userPrompt,
101
- language: inputs.config.language
118
+ language: inputs.config.language,
119
+ learnerLevel: inputs.config.learnerLevel,
120
+ recentSummaries: inputs.recentSummaries
102
121
  });
103
122
  try {
104
123
  const result = await runClaude(prompt, inputs.config.skipIfSlowMs);
@@ -130,7 +149,12 @@ ${result.stdout}`.toLowerCase();
130
149
  return {
131
150
  kind: "ok",
132
151
  result: {
133
- summary: truncateText(result.stdout.trim(), 200),
152
+ impact: truncateText(result.stdout.trim(), 200),
153
+ howItWorks: "",
154
+ why: "",
155
+ deepDive: [],
156
+ isSamePattern: false,
157
+ samePatternNote: "",
134
158
  risk: "none",
135
159
  riskReason: ""
136
160
  }
@@ -538,14 +562,14 @@ function isHookEnabled(toolName, config) {
538
562
  if (lower === "bash") return config.hooks.bash;
539
563
  return false;
540
564
  }
541
- async function runEngine(filePath, diff, config, userPrompt, signal) {
565
+ async function runEngine(filePath, diff, config, userPrompt, recentSummaries, signal) {
542
566
  if (signal.aborted) {
543
567
  return { kind: "skip", reason: "interrupted by user" };
544
568
  }
545
569
  if (config.engine === "ollama") {
546
- return callOllama({ filePath, diff, config });
570
+ return callOllama({ filePath, diff, config, recentSummaries });
547
571
  }
548
- return callClaude({ filePath, diff, config, userPrompt });
572
+ return callClaude({ filePath, diff, config, userPrompt, recentSummaries });
549
573
  }
550
574
  async function main() {
551
575
  const controller = new AbortController();
@@ -565,7 +589,6 @@ async function main() {
565
589
  process.env.CODE_EXPLAINER_SESSION_ID = payload.session_id;
566
590
  let filePath;
567
591
  let diff;
568
- let isNewFile = false;
569
592
  const lowerTool = payload.tool_name.toLowerCase();
570
593
  if (lowerTool === "edit" || lowerTool === "multiedit" || lowerTool === "write") {
571
594
  const input = payload.tool_input;
@@ -601,7 +624,6 @@ async function main() {
601
624
  safeExit();
602
625
  }
603
626
  diff = result2.content;
604
- isNewFile = result2.kind === "new-file";
605
627
  } else if (lowerTool === "bash") {
606
628
  const input = payload.tool_input;
607
629
  const command = input.command ?? "";
@@ -618,7 +640,15 @@ ${diff}`;
618
640
  if (cached) {
619
641
  result = cached;
620
642
  } else {
621
- const outcome = await runEngine(filePath, diff, config, void 0, controller.signal);
643
+ const recentSummaries = getRecentSummaries(payload.session_id, 3);
644
+ const outcome = await runEngine(
645
+ filePath,
646
+ diff,
647
+ config,
648
+ void 0,
649
+ recentSummaries,
650
+ controller.signal
651
+ );
622
652
  if (outcome.kind === "skip") {
623
653
  addOutput(formatSkipNotice(outcome.reason));
624
654
  safeExit();
@@ -631,27 +661,33 @@ ${diff}`;
631
661
  setCached(payload.session_id, cacheKey, result);
632
662
  }
633
663
  let driftReason;
634
- if (!isNewFile || filePath !== "<bash command>") {
664
+ if (filePath !== "<bash command>") {
635
665
  const priorEntries = readSession(payload.session_id);
636
- if (filePath !== "<bash command>") {
637
- const analysis = analyzeDrift(filePath, priorEntries);
638
- if (analysis.isUnrelated) {
639
- driftReason = analysis.reason;
640
- }
666
+ const analysis = analyzeDrift(filePath, priorEntries);
667
+ if (analysis.isUnrelated) {
668
+ driftReason = analysis.reason;
641
669
  }
642
670
  }
643
- addOutput(formatExplanationBox(filePath, result.summary, result.risk, result.riskReason));
671
+ addOutput(
672
+ formatExplanationBox({
673
+ filePath,
674
+ result,
675
+ detailLevel: config.detailLevel,
676
+ language: config.language
677
+ })
678
+ );
679
+ const summaryForTracking = result.isSamePattern ? result.samePatternNote || "Same pattern as a recent edit" : result.impact;
644
680
  recordEntry(payload.session_id, {
645
681
  file: filePath,
646
682
  timestamp: Date.now(),
647
683
  risk: result.risk,
648
- summary: result.summary,
684
+ summary: summaryForTracking,
649
685
  unrelated: !!driftReason
650
686
  });
651
687
  const updated = readSession(payload.session_id);
652
688
  const driftCheck = shouldAlertDrift(updated);
653
689
  if (driftCheck.shouldAlert) {
654
- addOutput(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles));
690
+ addOutput(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles, void 0, config.language));
655
691
  }
656
692
  safeExit();
657
693
  }