vibe-code-explainer 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -55
- package/dist/{chunk-5NCRRHU7.js → chunk-JPDU5ASR.js} +9 -1
- package/dist/chunk-JPDU5ASR.js.map +1 -0
- package/dist/chunk-KS3PATTI.js +429 -0
- package/dist/chunk-KS3PATTI.js.map +1 -0
- package/dist/chunk-Y55I7ZS5.js +604 -0
- package/dist/chunk-Y55I7ZS5.js.map +1 -0
- package/dist/cli/index.js +6 -6
- package/dist/{config-H57D4GXB.js → config-74UP7RRD.js} +109 -2
- package/dist/config-74UP7RRD.js.map +1 -0
- package/dist/hooks/post-tool.js +62 -26
- package/dist/hooks/post-tool.js.map +1 -1
- package/dist/{init-KUVD2YGA.js → init-OTODBBPP.js} +19 -3
- package/dist/init-OTODBBPP.js.map +1 -0
- package/dist/ollama-PGPTPYS4.js +14 -0
- package/dist/{schema-TBXFNCIG.js → schema-TEWSY7EF.js} +4 -2
- package/dist/{tracker-HCWPUZIO.js → tracker-4ORSFJQB.js} +4 -2
- package/dist/{uninstall-CNGJWJYQ.js → uninstall-PN7724RX.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-5NCRRHU7.js.map +0 -1
- package/dist/chunk-W67RX53R.js +0 -347
- package/dist/chunk-W67RX53R.js.map +0 -1
- package/dist/chunk-YS2XIZIA.js +0 -544
- package/dist/chunk-YS2XIZIA.js.map +0 -1
- package/dist/config-H57D4GXB.js.map +0 -1
- package/dist/init-KUVD2YGA.js.map +0 -1
- package/dist/ollama-34TOVCUY.js +0 -12
- /package/dist/{ollama-34TOVCUY.js.map → ollama-PGPTPYS4.js.map} +0 -0
- /package/dist/{schema-TBXFNCIG.js.map → schema-TEWSY7EF.js.map} +0 -0
- /package/dist/{tracker-HCWPUZIO.js.map → tracker-4ORSFJQB.js.map} +0 -0
- /package/dist/{uninstall-CNGJWJYQ.js.map → uninstall-PN7724RX.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-
|
|
9
|
+
const { runInit } = await import("../init-OTODBBPP.js");
|
|
10
10
|
await runInit(args.slice(1));
|
|
11
11
|
break;
|
|
12
12
|
}
|
|
13
13
|
case "config": {
|
|
14
|
-
const { runConfig } = await import("../config-
|
|
14
|
+
const { runConfig } = await import("../config-74UP7RRD.js");
|
|
15
15
|
await runConfig();
|
|
16
16
|
break;
|
|
17
17
|
}
|
|
18
18
|
case "uninstall": {
|
|
19
|
-
const { runUninstall } = await import("../uninstall-
|
|
19
|
+
const { runUninstall } = await import("../uninstall-PN7724RX.js");
|
|
20
20
|
await runUninstall();
|
|
21
21
|
break;
|
|
22
22
|
}
|
|
23
23
|
case "summary": {
|
|
24
|
-
const { printSummary } = await import("../tracker-
|
|
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-
|
|
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-
|
|
40
|
+
const { runWarmup } = await import("../ollama-PGPTPYS4.js");
|
|
41
41
|
await runWarmup();
|
|
42
42
|
break;
|
|
43
43
|
}
|
|
@@ -6,16 +6,73 @@ 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-
|
|
12
|
+
} from "./chunk-JPDU5ASR.js";
|
|
12
13
|
import "./chunk-7OCVIDC7.js";
|
|
13
14
|
|
|
14
15
|
// src/cli/config.ts
|
|
15
16
|
import { intro, outro, select, confirm, text, cancel, isCancel, note } from "@clack/prompts";
|
|
16
17
|
import pc from "picocolors";
|
|
18
|
+
import { spawn } from "child_process";
|
|
17
19
|
import { existsSync, writeFileSync } from "fs";
|
|
18
20
|
import { join } from "path";
|
|
21
|
+
async function listInstalledOllamaModels(url) {
|
|
22
|
+
try {
|
|
23
|
+
const ctrl = new AbortController();
|
|
24
|
+
const timer = setTimeout(() => ctrl.abort(), 3e3);
|
|
25
|
+
const res = await fetch(`${url}/api/tags`, { signal: ctrl.signal });
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
if (!res.ok) return null;
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
if (!data.models) return [];
|
|
30
|
+
return data.models.map((m) => m.name ?? m.model ?? "").filter((n) => n.length > 0);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function normalizeModelName(name) {
|
|
36
|
+
return name.toLowerCase().split(/[-_]/)[0];
|
|
37
|
+
}
|
|
38
|
+
function hasModel(installed, wanted) {
|
|
39
|
+
const wantedNorm = normalizeModelName(wanted);
|
|
40
|
+
return installed.some((n) => {
|
|
41
|
+
const base = n.toLowerCase();
|
|
42
|
+
if (base === wanted.toLowerCase()) return true;
|
|
43
|
+
if (base === wanted.toLowerCase()) return true;
|
|
44
|
+
return normalizeModelName(base).startsWith(wantedNorm);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function pullOllamaModel(model) {
|
|
48
|
+
note(
|
|
49
|
+
`Pulling ${pc.cyan(model)}
|
|
50
|
+
${pc.dim("This can take a while on the first run (several GB download).")}`,
|
|
51
|
+
"Downloading model"
|
|
52
|
+
);
|
|
53
|
+
return new Promise((resolvePromise) => {
|
|
54
|
+
const child = spawn("ollama", ["pull", model], { stdio: "inherit" });
|
|
55
|
+
child.on("error", () => {
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
pc.red("\nFailed to run `ollama pull`. Make sure Ollama is installed and running.\n")
|
|
58
|
+
);
|
|
59
|
+
resolvePromise(false);
|
|
60
|
+
});
|
|
61
|
+
child.on("close", (code) => {
|
|
62
|
+
if (code === 0) {
|
|
63
|
+
process.stdout.write(pc.green(`
|
|
64
|
+
\u2713 Pulled ${model}
|
|
65
|
+
`));
|
|
66
|
+
resolvePromise(true);
|
|
67
|
+
} else {
|
|
68
|
+
process.stderr.write(pc.red(`
|
|
69
|
+
\u2717 ollama pull exited with code ${code}
|
|
70
|
+
`));
|
|
71
|
+
resolvePromise(false);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
19
76
|
function handleCancel(value) {
|
|
20
77
|
if (isCancel(value)) {
|
|
21
78
|
cancel("Exited without saving.");
|
|
@@ -35,6 +92,7 @@ function renderCurrent(config) {
|
|
|
35
92
|
`${pc.bold("Ollama URL: ")} ${config.ollamaUrl}`,
|
|
36
93
|
`${pc.bold("Detail level: ")} ${config.detailLevel}`,
|
|
37
94
|
`${pc.bold("Language: ")} ${LANGUAGE_NAMES[config.language]}`,
|
|
95
|
+
`${pc.bold("Learner level:")} ${LEARNER_LEVEL_NAMES[config.learnerLevel]}`,
|
|
38
96
|
`${pc.bold("Hooks: ")} ${hooks.join(" \u2713 ") || "(all disabled)"}`,
|
|
39
97
|
`${pc.bold("Excluded: ")} ${excluded}`,
|
|
40
98
|
`${pc.bold("Skip if slow: ")} ${timeoutLabel}`
|
|
@@ -63,6 +121,40 @@ async function changeModel(config) {
|
|
|
63
121
|
initialValue: config.ollamaModel
|
|
64
122
|
});
|
|
65
123
|
handleCancel(value);
|
|
124
|
+
if (value === config.ollamaModel) {
|
|
125
|
+
return config;
|
|
126
|
+
}
|
|
127
|
+
const installed = await listInstalledOllamaModels(config.ollamaUrl);
|
|
128
|
+
if (installed === null) {
|
|
129
|
+
note(
|
|
130
|
+
`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.`,
|
|
131
|
+
"Ollama unreachable"
|
|
132
|
+
);
|
|
133
|
+
return { ...config, ollamaModel: value };
|
|
134
|
+
}
|
|
135
|
+
if (hasModel(installed, value)) {
|
|
136
|
+
note(`${pc.green("\u2713")} Model ${pc.cyan(value)} is already installed.`, "Model ready");
|
|
137
|
+
return { ...config, ollamaModel: value };
|
|
138
|
+
}
|
|
139
|
+
const shouldPull = await confirm({
|
|
140
|
+
message: `Model ${value} is not installed locally. Pull it now?`,
|
|
141
|
+
initialValue: true
|
|
142
|
+
});
|
|
143
|
+
handleCancel(shouldPull);
|
|
144
|
+
if (!shouldPull) {
|
|
145
|
+
note(
|
|
146
|
+
`Saved the selection, but you must run ${pc.cyan(`ollama pull ${value}`)} before it works.`,
|
|
147
|
+
"Model not pulled"
|
|
148
|
+
);
|
|
149
|
+
return { ...config, ollamaModel: value };
|
|
150
|
+
}
|
|
151
|
+
const pullOk = await pullOllamaModel(value);
|
|
152
|
+
if (!pullOk) {
|
|
153
|
+
note(
|
|
154
|
+
`Pull failed. Saving the model selection anyway \u2014 run ${pc.cyan(`ollama pull ${value}`)} manually when Ollama is reachable.`,
|
|
155
|
+
"Pull failed"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
66
158
|
return { ...config, ollamaModel: value };
|
|
67
159
|
}
|
|
68
160
|
async function changeUrl(config) {
|
|
@@ -107,6 +199,19 @@ async function changeLanguage(config) {
|
|
|
107
199
|
handleCancel(value);
|
|
108
200
|
return { ...config, language: value };
|
|
109
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
|
+
}
|
|
110
215
|
async function changeHooks(config) {
|
|
111
216
|
const editOn = await confirm({ message: "Explain file edits?", initialValue: config.hooks.edit });
|
|
112
217
|
handleCancel(editOn);
|
|
@@ -204,6 +309,7 @@ Run ${pc.cyan("npx vibe-code-explainer init")} first.`
|
|
|
204
309
|
{ label: "Ollama URL", value: "url" },
|
|
205
310
|
{ label: "Detail level", value: "detail" },
|
|
206
311
|
{ label: "Language", value: "language" },
|
|
312
|
+
{ label: "Learner level", value: "level" },
|
|
207
313
|
{ label: "Enable/disable hooks", value: "hooks" },
|
|
208
314
|
{ label: "File exclusions", value: "exclude" },
|
|
209
315
|
{ label: "Latency timeout", value: "timeout" },
|
|
@@ -217,6 +323,7 @@ Run ${pc.cyan("npx vibe-code-explainer init")} first.`
|
|
|
217
323
|
if (choice === "url") config = await changeUrl(config);
|
|
218
324
|
if (choice === "detail") config = await changeDetail(config);
|
|
219
325
|
if (choice === "language") config = await changeLanguage(config);
|
|
326
|
+
if (choice === "level") config = await changeLevel(config);
|
|
220
327
|
if (choice === "hooks") config = await changeHooks(config);
|
|
221
328
|
if (choice === "exclude") config = await changeExclude(config);
|
|
222
329
|
if (choice === "timeout") config = await changeTimeout(config);
|
|
@@ -227,4 +334,4 @@ Run ${pc.cyan("npx vibe-code-explainer init")} first.`
|
|
|
227
334
|
export {
|
|
228
335
|
runConfig
|
|
229
336
|
};
|
|
230
|
-
//# sourceMappingURL=config-
|
|
337
|
+
//# sourceMappingURL=config-74UP7RRD.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":[]}
|
package/dist/hooks/post-tool.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
buildClaudePrompt,
|
|
4
4
|
callOllama
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-KS3PATTI.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_CONFIG,
|
|
8
8
|
loadConfig
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-JPDU5ASR.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-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
664
|
+
if (filePath !== "<bash command>") {
|
|
635
665
|
const priorEntries = readSession(payload.session_id);
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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(
|
|
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:
|
|
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
|
}
|