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.
Files changed (31) hide show
  1. package/README.md +152 -55
  2. package/dist/{chunk-5NCRRHU7.js → chunk-JPDU5ASR.js} +9 -1
  3. package/dist/chunk-JPDU5ASR.js.map +1 -0
  4. package/dist/chunk-KS3PATTI.js +429 -0
  5. package/dist/chunk-KS3PATTI.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-H57D4GXB.js → config-74UP7RRD.js} +109 -2
  10. package/dist/config-74UP7RRD.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-OTODBBPP.js} +19 -3
  14. package/dist/init-OTODBBPP.js.map +1 -0
  15. package/dist/ollama-PGPTPYS4.js +14 -0
  16. package/dist/{schema-TBXFNCIG.js → schema-TEWSY7EF.js} +4 -2
  17. package/dist/{tracker-HCWPUZIO.js → tracker-4ORSFJQB.js} +4 -2
  18. package/dist/{uninstall-CNGJWJYQ.js → uninstall-PN7724RX.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-H57D4GXB.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-PGPTPYS4.js.map} +0 -0
  29. /package/dist/{schema-TBXFNCIG.js.map → schema-TEWSY7EF.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-PN7724RX.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/post-tool.ts","../../src/engines/claude.ts","../../src/hooks/diff-extractor.ts","../../src/filter/bash-filter.ts","../../src/session/drift.ts"],"sourcesContent":["import { join } from \"node:path\";\nimport { loadConfig, DEFAULT_CONFIG } from \"../config/schema.js\";\nimport type { Config, HookPayload, ExplanationResult } from \"../config/schema.js\";\nimport { callOllama, type EngineOutcome } from \"../engines/ollama.js\";\nimport { callClaude } from \"../engines/claude.js\";\nimport {\n extractEditDiff,\n extractNewFileDiff,\n buildDiffFromEdit,\n buildDiffFromMultiEdit,\n isExcluded,\n} from \"./diff-extractor.js\";\nimport { shouldCaptureBash } from \"../filter/bash-filter.js\";\nimport { formatExplanationBox, formatDriftAlert, formatSkipNotice, formatErrorNotice } from \"../format/box.js\";\nimport { recordEntry, readSession, cleanStaleSessionFiles } from \"../session/tracker.js\";\nimport { analyzeDrift, shouldAlertDrift } from \"../session/drift.js\";\nimport { getCached, setCached } from \"../cache/explanation-cache.js\";\n\nconst output: string[] = [];\n\nfunction addOutput(text: string): void {\n output.push(text);\n}\n\n/**\n * Emit the Claude Code hook JSON on stdout so the accumulated output\n * appears as a system message in the user's terminal. Always exit 0 so\n * Claude Code is never blocked.\n */\nfunction safeExit(): never {\n if (output.length > 0) {\n // Leading newline separates the box from Claude Code's \"PostToolUse:X says:\"\n // prefix, which otherwise renders on the same line as the top border.\n const systemMessage = \"\\n\" + output.join(\"\\n\");\n process.stdout.write(JSON.stringify({ systemMessage }) + \"\\n\");\n }\n process.exit(0);\n}\n\nasync function readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let data = \"\";\n process.stdin.setEncoding(\"utf-8\");\n process.stdin.on(\"data\", (chunk) => {\n data += chunk;\n });\n process.stdin.on(\"end\", () => resolve(data));\n process.stdin.on(\"error\", () => resolve(data));\n // Safety timeout: if stdin has no data in 2s, resolve empty.\n setTimeout(() => resolve(data), 2000);\n });\n}\n\nfunction parsePayload(raw: string): HookPayload | null {\n try {\n const parsed = JSON.parse(raw);\n if (typeof parsed.session_id === \"string\" && typeof parsed.tool_name === \"string\") {\n return parsed as HookPayload;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction loadConfigSafe(cwd: string): Config {\n // loadConfig already falls back to the global config at\n // ~/.code-explainer.config.json when the project path doesn't exist,\n // and to built-in defaults if neither exists.\n try {\n return loadConfig(join(cwd, \"code-explainer.config.json\"));\n } catch {\n return DEFAULT_CONFIG;\n }\n}\n\nfunction isHookEnabled(toolName: string, config: Config): boolean {\n const lower = toolName.toLowerCase();\n if (lower === \"edit\" || lower === \"multiedit\") return config.hooks.edit;\n if (lower === \"write\") return config.hooks.write;\n if (lower === \"bash\") return config.hooks.bash;\n return false;\n}\n\nasync function runEngine(\n filePath: string,\n diff: string,\n config: Config,\n userPrompt: string | undefined,\n signal: AbortSignal\n): Promise<EngineOutcome> {\n if (signal.aborted) {\n return { kind: \"skip\", reason: \"interrupted by user\" };\n }\n if (config.engine === \"ollama\") {\n return callOllama({ filePath, diff, config });\n }\n return callClaude({ filePath, diff, config, userPrompt });\n}\n\nasync function main(): Promise<void> {\n // Interrupt handler — always exit 0 on Ctrl+C.\n const controller = new AbortController();\n process.on(\"SIGINT\", () => {\n controller.abort();\n addOutput(formatSkipNotice(\"interrupted by user\"));\n safeExit();\n });\n\n const raw = await readStdin();\n if (!raw.trim()) safeExit();\n\n const payload = parsePayload(raw);\n if (!payload) safeExit();\n\n const cwd = payload.cwd || process.cwd();\n const config = loadConfigSafe(cwd);\n\n if (!isHookEnabled(payload.tool_name, config)) safeExit();\n\n cleanStaleSessionFiles();\n\n // Pass session_id to downstream modules via env (so summary/session-end\n // commands pick the right session without re-parsing the payload).\n process.env.CODE_EXPLAINER_SESSION_ID = payload.session_id;\n\n // Extract the diff based on tool name.\n let filePath: string;\n let diff: string;\n let isNewFile = false;\n\n const lowerTool = payload.tool_name.toLowerCase();\n if (lowerTool === \"edit\" || lowerTool === \"multiedit\" || lowerTool === \"write\") {\n const input = payload.tool_input as {\n file_path?: string;\n filePath?: string;\n old_string?: string;\n new_string?: string;\n oldString?: string;\n newString?: string;\n edits?: Array<{ old_string?: string; new_string?: string; oldString?: string; newString?: string }>;\n };\n const target = input.file_path ?? input.filePath;\n if (!target) safeExit();\n filePath = target as string;\n\n if (isExcluded(filePath, config.exclude)) safeExit();\n\n // Preferred path: use the payload's old/new strings directly. This works\n // for untracked files (very common) and is always more accurate than git\n // diff, which may miss changes on files that were created and edited in\n // the same session without a commit.\n let result;\n if (lowerTool === \"edit\") {\n const oldStr = input.old_string ?? input.oldString ?? \"\";\n const newStr = input.new_string ?? input.newString ?? \"\";\n if (oldStr || newStr) {\n result = buildDiffFromEdit(filePath, oldStr, newStr);\n } else {\n result = extractEditDiff(filePath, cwd);\n }\n } else if (lowerTool === \"multiedit\") {\n if (input.edits && input.edits.length > 0) {\n result = buildDiffFromMultiEdit(filePath, input.edits);\n } else {\n result = extractEditDiff(filePath, cwd);\n }\n } else {\n result = extractNewFileDiff(filePath, cwd);\n }\n\n if (result.kind === \"empty\") safeExit();\n if (result.kind === \"skip\") {\n addOutput(formatSkipNotice(result.reason));\n safeExit();\n }\n if (result.kind === \"binary\") {\n addOutput(formatSkipNotice(result.message));\n safeExit();\n }\n diff = result.content;\n isNewFile = result.kind === \"new-file\";\n } else if (lowerTool === \"bash\") {\n const input = payload.tool_input as { command?: string };\n const command = input.command ?? \"\";\n if (!command || !shouldCaptureBash(command)) safeExit();\n filePath = \"<bash command>\";\n diff = command;\n } else {\n safeExit();\n }\n\n // Cache check.\n const cacheKey = `${filePath}\\n${diff}`;\n const cached = getCached(payload.session_id, cacheKey);\n let result: ExplanationResult | null = null;\n\n if (cached) {\n result = cached;\n } else {\n const outcome = await runEngine(filePath, diff, config, undefined, controller.signal);\n if (outcome.kind === \"skip\") {\n addOutput(formatSkipNotice(outcome.reason));\n safeExit();\n }\n if (outcome.kind === \"error\") {\n addOutput(formatErrorNotice(outcome.problem, outcome.cause, outcome.fix));\n safeExit();\n }\n result = outcome.result;\n setCached(payload.session_id, cacheKey, result);\n }\n\n // Path-heuristic drift analysis (only meaningful for Edit/Write).\n let driftReason: string | undefined;\n if (!isNewFile || filePath !== \"<bash command>\") {\n const priorEntries = readSession(payload.session_id);\n if (filePath !== \"<bash command>\") {\n const analysis = analyzeDrift(filePath, priorEntries);\n if (analysis.isUnrelated) {\n driftReason = analysis.reason;\n }\n }\n }\n\n // Print the explanation box.\n addOutput(formatExplanationBox(filePath, result.summary, result.risk, result.riskReason));\n\n // Record the entry.\n recordEntry(payload.session_id, {\n file: filePath,\n timestamp: Date.now(),\n risk: result.risk,\n summary: result.summary,\n unrelated: !!driftReason,\n });\n\n // Drift alert at threshold.\n const updated = readSession(payload.session_id);\n const driftCheck = shouldAlertDrift(updated);\n if (driftCheck.shouldAlert) {\n addOutput(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles));\n }\n\n safeExit();\n}\n\nmain().catch(() => {\n // Never fail the hook — always exit 0.\n safeExit();\n});\n","import { execFile } from \"node:child_process\";\nimport type { Config, ExplanationResult, RiskLevel } from \"../config/schema.js\";\nimport { buildClaudePrompt } from \"../prompts/templates.js\";\nimport type { EngineOutcome } from \"./ollama.js\";\n\nexport interface ClaudeCallInputs {\n filePath: string;\n diff: string;\n config: Config;\n userPrompt?: string;\n}\n\nfunction extractJson(text: string): string | null {\n const trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n const fenceMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?```/);\n if (fenceMatch) {\n return fenceMatch[1].trim();\n }\n const start = trimmed.indexOf(\"{\");\n const end = trimmed.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return trimmed.slice(start, end + 1);\n }\n return null;\n}\n\nfunction parseResponse(rawText: string): ExplanationResult | null {\n const json = extractJson(rawText);\n if (!json) return null;\n try {\n const parsed = JSON.parse(json);\n if (\n typeof parsed.summary === \"string\" &&\n typeof parsed.risk === \"string\" &&\n typeof parsed.riskReason === \"string\"\n ) {\n const risk = parsed.risk as RiskLevel;\n if (![\"none\", \"low\", \"medium\", \"high\"].includes(risk)) return null;\n return { summary: parsed.summary, risk, riskReason: parsed.riskReason };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction truncateText(text: string, max: number): string {\n if (text.length <= max) return text;\n return text.slice(0, max) + \"...\";\n}\n\ninterface ExecResult {\n stdout: string;\n stderr: string;\n code: number | null;\n}\n\nfunction runClaude(prompt: string, timeoutMs: number): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const child = execFile(\n \"claude\",\n [\"-p\", prompt],\n {\n timeout: timeoutMs,\n maxBuffer: 1024 * 1024 * 2, // 2MB\n windowsHide: true,\n },\n (err, stdout, stderr) => {\n if (err) {\n const e = err as NodeJS.ErrnoException & { killed?: boolean; signal?: string };\n if (e.code === \"ENOENT\") {\n reject(Object.assign(new Error(\"claude CLI not found\"), { code: \"ENOENT\" }));\n return;\n }\n if (e.killed || e.signal === \"SIGTERM\") {\n reject(Object.assign(new Error(\"claude timed out\"), { code: \"TIMEOUT\" }));\n return;\n }\n // Include stderr for context\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: e.code as unknown as number ?? 1 });\n return;\n }\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: 0 });\n }\n );\n child.on(\"error\", (err) => {\n reject(err);\n });\n });\n}\n\nexport async function callClaude(inputs: ClaudeCallInputs): Promise<EngineOutcome> {\n const prompt = buildClaudePrompt(inputs.config.detailLevel, {\n filePath: inputs.filePath,\n diff: inputs.diff,\n userPrompt: inputs.userPrompt,\n language: inputs.config.language,\n });\n\n try {\n const result = await runClaude(prompt, inputs.config.skipIfSlowMs);\n\n if (result.code !== 0) {\n const combined = `${result.stderr}\\n${result.stdout}`.toLowerCase();\n if (/auth|login|unauthorized|not authenticated|api key/i.test(combined)) {\n return {\n kind: \"error\",\n problem: \"Claude Code is not authenticated\",\n cause: \"The 'claude' CLI requires a valid login\",\n fix: \"Run 'claude login' in a terminal, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI returned an error\",\n cause: result.stderr.trim() || `exit code ${result.code}`,\n fix: \"Run 'claude --help' to verify the CLI works, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n\n if (!result.stdout.trim()) {\n return { kind: \"skip\", reason: \"Claude returned an empty response\" };\n }\n\n const parsed = parseResponse(result.stdout);\n if (parsed) {\n return { kind: \"ok\", result: parsed };\n }\n\n // Malformed output: fall back to truncated raw text.\n return {\n kind: \"ok\",\n result: {\n summary: truncateText(result.stdout.trim(), 200),\n risk: \"none\",\n riskReason: \"\",\n },\n };\n } catch (err) {\n const e = err as Error & { code?: string };\n if (e.code === \"ENOENT\") {\n return {\n kind: \"error\",\n problem: \"Claude CLI not found\",\n cause: \"The 'claude' command is not installed or not on PATH\",\n fix: \"Install Claude Code, or switch to Ollama engine via 'npx vibe-code-explainer config'\",\n };\n }\n if (e.code === \"TIMEOUT\") {\n return {\n kind: \"skip\",\n reason: `explanation took too long (>${inputs.config.skipIfSlowMs}ms)`,\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI invocation failed\",\n cause: e.message,\n fix: \"Check that 'claude' works by running 'claude --help' in a terminal\",\n };\n }\n}\n","import { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\n\nexport type DiffResult =\n | { kind: \"diff\"; content: string; lines: number; truncated: boolean }\n | { kind: \"new-file\"; content: string; lines: number; truncated: boolean }\n | { kind: \"binary\"; message: string }\n | { kind: \"empty\" }\n | { kind: \"skip\"; reason: string };\n\nconst MAX_DIFF_LINES = 200;\nconst HEAD_LINES = 150;\nconst TAIL_LINES = 50;\n\nfunction truncateDiff(content: string): { content: string; lines: number; truncated: boolean } {\n const lines = content.split(\"\\n\");\n if (lines.length <= MAX_DIFF_LINES) {\n return { content, lines: lines.length, truncated: false };\n }\n const head = lines.slice(0, HEAD_LINES);\n const tail = lines.slice(-TAIL_LINES);\n const omitted = lines.length - HEAD_LINES - TAIL_LINES;\n const truncated = [\n ...head,\n `[...truncated, ${omitted} more lines not shown]`,\n ...tail,\n ].join(\"\\n\");\n return { content: truncated, lines: lines.length, truncated: true };\n}\n\nfunction runGit(args: string[], cwd: string): string {\n return execFileSync(\"git\", args, { cwd, encoding: \"utf-8\", maxBuffer: 1024 * 1024 * 10 });\n}\n\n/**\n * Build a unified-style diff directly from an Edit tool's old_string/new_string\n * payload. More reliable than `git diff` because it works even on untracked\n * files (the common case: user asks Claude to edit a file that was just\n * created and never committed). Multi-line strings produce proper line-by-line\n * - / + markers so the model can tell additions apart from modifications.\n */\nexport function buildDiffFromEdit(\n filePath: string,\n oldString: string,\n newString: string\n): DiffResult {\n if (!oldString && !newString) return { kind: \"empty\" };\n\n const oldLines = oldString ? oldString.split(\"\\n\") : [];\n const newLines = newString ? newString.split(\"\\n\") : [];\n\n const header = `--- a/${filePath}\\n+++ b/${filePath}\\n@@ Edit @@`;\n const minus = oldLines.map((l) => `-${l}`).join(\"\\n\");\n const plus = newLines.map((l) => `+${l}`).join(\"\\n\");\n\n const parts = [header, minus, plus].filter((s) => s.length > 0);\n const content = parts.join(\"\\n\");\n\n const { content: final, lines, truncated } = truncateDiff(content);\n return { kind: \"diff\", content: final, lines, truncated };\n}\n\n/**\n * Build a combined unified-style diff from a MultiEdit payload's edits array.\n */\nexport function buildDiffFromMultiEdit(\n filePath: string,\n edits: Array<{ old_string?: string; new_string?: string; oldString?: string; newString?: string }>\n): DiffResult {\n if (!edits || edits.length === 0) return { kind: \"empty\" };\n\n const header = `--- a/${filePath}\\n+++ b/${filePath}`;\n const hunks: string[] = [];\n\n for (let i = 0; i < edits.length; i++) {\n const e = edits[i];\n const oldStr = e.old_string ?? e.oldString ?? \"\";\n const newStr = e.new_string ?? e.newString ?? \"\";\n if (!oldStr && !newStr) continue;\n\n const oldLines = oldStr ? oldStr.split(\"\\n\") : [];\n const newLines = newStr ? newStr.split(\"\\n\") : [];\n const minus = oldLines.map((l) => `-${l}`).join(\"\\n\");\n const plus = newLines.map((l) => `+${l}`).join(\"\\n\");\n\n hunks.push(`@@ Edit ${i + 1} of ${edits.length} @@`);\n if (minus) hunks.push(minus);\n if (plus) hunks.push(plus);\n }\n\n if (hunks.length === 0) return { kind: \"empty\" };\n\n const content = [header, ...hunks].join(\"\\n\");\n const { content: final, lines, truncated } = truncateDiff(content);\n return { kind: \"diff\", content: final, lines, truncated };\n}\n\nexport function extractEditDiff(filePath: string, cwd: string): DiffResult {\n // Check if we're in a git repo.\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n return { kind: \"skip\", reason: \"not inside a git repository\" };\n }\n\n // Check for binary.\n try {\n const numstat = runGit([\"diff\", \"--numstat\", \"--\", filePath], cwd).trim();\n if (numstat.startsWith(\"-\\t-\\t\")) {\n return { kind: \"binary\", message: `Binary file modified: ${filePath}` };\n }\n } catch {\n // Non-fatal, fall through to diff.\n }\n\n let diffOutput = \"\";\n try {\n diffOutput = runGit([\"diff\", \"--no-color\", \"--\", filePath], cwd);\n } catch {\n diffOutput = \"\";\n }\n\n if (!diffOutput.trim()) {\n // File may be untracked (newly created via Write/Edit on a fresh file).\n return extractNewFileDiff(filePath, cwd);\n }\n\n const { content, lines, truncated } = truncateDiff(diffOutput);\n return { kind: \"diff\", content, lines, truncated };\n}\n\nexport function extractNewFileDiff(filePath: string, cwd: string): DiffResult {\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n // Not a git repo — fall back to reading the file if possible.\n return readFileAsNewDiff(filePath);\n }\n\n // Check if file is untracked.\n let untracked = \"\";\n try {\n untracked = runGit([\"ls-files\", \"--others\", \"--exclude-standard\", \"--\", filePath], cwd).trim();\n } catch {\n untracked = \"\";\n }\n\n if (untracked) {\n return readFileAsNewDiff(filePath);\n }\n\n // Might be a file with no changes, or tracked without a diff.\n return { kind: \"empty\" };\n}\n\nfunction readFileAsNewDiff(filePath: string): DiffResult {\n if (!existsSync(filePath)) {\n return { kind: \"skip\", reason: `file not found: ${filePath}` };\n }\n\n try {\n const stat = statSync(filePath);\n if (stat.size > 2 * 1024 * 1024) {\n return { kind: \"skip\", reason: `file too large (${Math.round(stat.size / 1024)}KB)` };\n }\n\n const raw = readFileSync(filePath, \"utf-8\");\n if (!raw.trim()) {\n return { kind: \"empty\" };\n }\n\n // Check for binary content (null bytes).\n if (raw.includes(\"\\0\")) {\n return { kind: \"binary\", message: `Binary file created: ${filePath}` };\n }\n\n const withMarkers = raw.split(\"\\n\").map((l) => `+ ${l}`).join(\"\\n\");\n const diff = `--- /dev/null\\n+++ b/${filePath}\\n${withMarkers}`;\n const { content, lines, truncated } = truncateDiff(diff);\n return { kind: \"new-file\", content, lines, truncated };\n } catch {\n return { kind: \"skip\", reason: \"could not read file\" };\n }\n}\n\n/**\n * Minimal glob matcher supporting *, **, and simple extensions.\n * Matches POSIX-style paths (caller normalizes).\n *\n * - `*.ext` matches `file.ext` in any directory\n * - `dir/**` matches anything under `dir/` recursively\n * - `**\\/file.ts` matches `file.ts` anywhere\n */\nexport function matchesGlob(filePath: string, pattern: string): boolean {\n const normalized = filePath.replace(/\\\\/g, \"/\");\n const normalizedPattern = pattern.replace(/\\\\/g, \"/\");\n\n // Build regex from the pattern\n let regexSrc = \"\";\n let i = 0;\n while (i < normalizedPattern.length) {\n const ch = normalizedPattern[i];\n if (ch === \"*\") {\n if (normalizedPattern[i + 1] === \"*\") {\n // ** matches anything (including /)\n regexSrc += \".*\";\n i += 2;\n if (normalizedPattern[i] === \"/\") i++; // consume trailing /\n } else {\n // * matches anything except /\n regexSrc += \"[^/]*\";\n i++;\n }\n } else if (ch === \"?\") {\n regexSrc += \"[^/]\";\n i++;\n } else if (/[.+^${}()|[\\]]/.test(ch)) {\n regexSrc += \"\\\\\" + ch;\n i++;\n } else {\n regexSrc += ch;\n i++;\n }\n }\n\n // If the pattern has no directory component, match the filename anywhere.\n const hasSlash = normalizedPattern.includes(\"/\");\n const anchored = hasSlash\n ? new RegExp(`^${regexSrc}$`)\n : new RegExp(`(^|/)${regexSrc}$`);\n\n return anchored.test(normalized);\n}\n\nexport function isExcluded(filePath: string, patterns: string[]): boolean {\n return patterns.some((p) => matchesGlob(filePath, p));\n}\n","/**\n * Bash command filter — decides whether a Bash command should trigger an\n * explanation. Filters to filesystem-mutating and state-changing commands\n * while skipping read-only operations.\n */\n\n// Commands that modify filesystem or project state.\nconst MUTATING_COMMANDS = new Set([\n \"rm\",\n \"mv\",\n \"cp\",\n \"mkdir\",\n \"rmdir\",\n \"chmod\",\n \"chown\",\n \"ln\",\n \"touch\",\n \"dd\",\n]);\n\n// Commands that need a specific subcommand/flag to be mutating.\nconst CONTEXTUAL_COMMANDS: Record<string, RegExp> = {\n npm: /\\b(install|add|remove|uninstall|update|ci|link|unlink|init|publish)\\b/,\n yarn: /\\b(add|remove|install|upgrade|init|publish|link|unlink)\\b/,\n pnpm: /\\b(add|remove|install|update|link|unlink|publish)\\b/,\n pip: /\\b(install|uninstall)\\b/,\n pip3: /\\b(install|uninstall)\\b/,\n brew: /\\b(install|uninstall|reinstall|upgrade|link|unlink|tap|untap)\\b/,\n apt: /\\b(install|remove|purge|upgrade|update)\\b/,\n \"apt-get\": /\\b(install|remove|purge|upgrade|update)\\b/,\n git: /\\b(checkout|reset|revert|rebase|merge|commit|push|pull|clean|stash|rm|mv|init|clone|cherry-pick|restore|switch)\\b/,\n sed: /(?:^|\\s)-i\\b/,\n curl: /(?:^|\\s)-[a-zA-Z]*o\\b|--output\\b/,\n wget: /.*/,\n tar: /(?:^|\\s)-[a-zA-Z]*x\\b|--extract\\b|(?:^|\\s)-[a-zA-Z]*c\\b|--create\\b/,\n unzip: /.*/,\n docker: /\\b(run|build|push|pull|rm|rmi|exec|start|stop|kill)\\b/,\n make: /.*/,\n cargo: /\\b(build|run|install|add|remove|update|publish)\\b/,\n go: /\\b(build|install|get|mod)\\b/,\n bun: /\\b(install|add|remove|run|build|init|create|link|unlink)\\b/,\n deno: /\\b(install|compile|bundle|run)\\b/,\n};\n\n// Commands that are always read-only and never trigger.\nconst READONLY_COMMANDS = new Set([\n \"ls\",\n \"cat\",\n \"head\",\n \"tail\",\n \"grep\",\n \"find\",\n \"which\",\n \"whereis\",\n \"type\",\n \"echo\",\n \"printf\",\n \"pwd\",\n \"whoami\",\n \"id\",\n \"date\",\n \"uname\",\n \"df\",\n \"du\",\n \"ps\",\n \"top\",\n \"htop\",\n \"stat\",\n \"file\",\n \"wc\",\n \"sort\",\n \"uniq\",\n \"diff\",\n \"man\",\n \"help\",\n \"history\",\n \"tree\",\n \"less\",\n \"more\",\n \"env\",\n \"printenv\",\n \"test\",\n \"true\",\n \"false\",\n]);\n\n/**\n * Split a command string on pipe, semicolon, and logical operators.\n * Returns each sub-command with leading whitespace trimmed.\n */\nexport function splitCommandChain(command: string): string[] {\n // Split on unquoted chain operators. A simple split is good enough for\n // the vibe-coder case; we explicitly do not handle exotic quoting.\n return command\n .split(/(?:\\|\\||&&|[|;])/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Check if a single sub-command (e.g., \"rm file.txt\") should trigger.\n */\nexport function subCommandShouldCapture(subCmd: string): boolean {\n // Detect redirections (> or >>) — always capture.\n if (/(?<!\\d)>>?(?!\\d)/.test(subCmd)) {\n // Bare redirections like `ls > out.txt` still count as mutating.\n return true;\n }\n\n const tokens = subCmd.trim().split(/\\s+/);\n if (tokens.length === 0) return false;\n\n // Skip env-var assignments like `FOO=bar cmd`.\n let idx = 0;\n while (idx < tokens.length && /^[A-Z_][A-Z0-9_]*=/.test(tokens[idx])) {\n idx++;\n }\n const head = tokens[idx];\n if (!head) return false;\n\n // Strip leading path (e.g., /usr/bin/rm -> rm).\n const bin = head.split(/[/\\\\]/).pop() ?? head;\n\n if (READONLY_COMMANDS.has(bin)) return false;\n if (MUTATING_COMMANDS.has(bin)) return true;\n\n const contextPattern = CONTEXTUAL_COMMANDS[bin];\n if (contextPattern) {\n const rest = tokens.slice(idx + 1).join(\" \");\n return contextPattern.test(rest);\n }\n\n return false;\n}\n\n/**\n * Decide whether a full command string should trigger a code-explainer\n * explanation. Returns true if ANY sub-command in the chain is mutating.\n */\nexport function shouldCaptureBash(command: string): boolean {\n const parts = splitCommandChain(command);\n return parts.some((p) => subCommandShouldCapture(p));\n}\n","import type { SessionEntry } from \"./tracker.js\";\n\nconst SENSITIVE_PATTERNS = [\n /(^|\\/)\\.env(\\.|$)/i,\n /(^|\\/)payment/i,\n /(^|\\/)billing/i,\n /(^|\\/)stripe/i,\n /(^|\\/)auth/i,\n /(^|\\/)credential/i,\n /(^|\\/)secret/i,\n /(^|\\/)\\.ssh\\//i,\n];\n\nfunction topLevelDir(path: string): string {\n const norm = path.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n const parts = norm.split(\"/\").filter(Boolean);\n return parts[0] ?? \"\";\n}\n\nexport function matchesSensitivePattern(filePath: string): boolean {\n return SENSITIVE_PATTERNS.some((re) => re.test(filePath));\n}\n\nexport interface DriftAnalysis {\n isUnrelated: boolean;\n reason?: string;\n}\n\n/**\n * Path-heuristic drift detection for the Ollama engine.\n * Flags a new file as unrelated if:\n * 1. It matches a sensitive pattern (env, payment, auth, secrets) AND\n * the session did not start in a similarly-sensitive area.\n * 2. It lives in a different top-level directory than every file\n * edited so far in the session (cross-module drift).\n *\n * Returns `isUnrelated: false` for the first few edits (not enough\n * context to judge).\n */\nexport function analyzeDrift(\n newFilePath: string,\n priorEntries: SessionEntry[]\n): DriftAnalysis {\n // Not enough context yet for the first edit.\n if (priorEntries.length === 0) {\n return { isUnrelated: false };\n }\n\n const priorFiles = Array.from(new Set(priorEntries.map((e) => e.file)));\n const priorTopDirs = new Set(priorFiles.map(topLevelDir));\n const priorHasSensitive = priorFiles.some(matchesSensitivePattern);\n\n // Sensitive-pattern drift: the new file is in a sensitive area but\n // prior session was not working there.\n if (matchesSensitivePattern(newFilePath) && !priorHasSensitive) {\n return {\n isUnrelated: true,\n reason: `touches sensitive area (${newFilePath}) that was not part of earlier edits`,\n };\n }\n\n // Cross-module drift: only flag after at least 2 prior edits established\n // a working area.\n if (priorEntries.length >= 2) {\n const newTop = topLevelDir(newFilePath);\n if (newTop && !priorTopDirs.has(newTop)) {\n return {\n isUnrelated: true,\n reason: `is in a different top-level area (${newTop}) than earlier edits (${Array.from(priorTopDirs).join(\", \")})`,\n };\n }\n }\n\n return { isUnrelated: false };\n}\n\nexport interface DriftThresholdResult {\n shouldAlert: boolean;\n totalFiles: number;\n unrelatedFiles: string[];\n}\n\nconst DRIFT_ALERT_THRESHOLD = 3;\n\n/**\n * Decide whether to surface a drift alert based on accumulated session state.\n * Fires once every time the unrelated count crosses a multiple of the threshold.\n */\nexport function shouldAlertDrift(entries: SessionEntry[]): DriftThresholdResult {\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(\n new Set(entries.filter((e) => e.unrelated).map((e) => e.file))\n );\n\n // Alert exactly when we hit the threshold (not every call afterwards).\n const shouldAlert =\n unrelatedFiles.length > 0 &&\n unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0 &&\n entries.filter((e) => e.unrelated).length ===\n entries.filter((e) => e.unrelated).length;\n\n // Fire specifically on the edit that caused us to cross the threshold.\n const lastEntry = entries[entries.length - 1];\n const lastWasUnrelated = lastEntry?.unrelated ?? false;\n const crossedThreshold =\n lastWasUnrelated && unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0;\n\n return {\n shouldAlert: crossedThreshold && shouldAlert,\n totalFiles: uniqueFiles.length,\n unrelatedFiles,\n };\n}\n"],"mappings":"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","names":["result"]}
1
+ {"version":3,"sources":["../../src/hooks/post-tool.ts","../../src/engines/claude.ts","../../src/hooks/diff-extractor.ts","../../src/filter/bash-filter.ts","../../src/session/drift.ts"],"sourcesContent":["import { join } from \"node:path\";\nimport { loadConfig, DEFAULT_CONFIG } from \"../config/schema.js\";\nimport type { Config, HookPayload, ExplanationResult } from \"../config/schema.js\";\nimport { callOllama, type EngineOutcome } from \"../engines/ollama.js\";\nimport { callClaude } from \"../engines/claude.js\";\nimport {\n extractEditDiff,\n extractNewFileDiff,\n buildDiffFromEdit,\n buildDiffFromMultiEdit,\n isExcluded,\n} from \"./diff-extractor.js\";\nimport { shouldCaptureBash } from \"../filter/bash-filter.js\";\nimport { formatExplanationBox, formatDriftAlert, formatSkipNotice, formatErrorNotice } from \"../format/box.js\";\nimport { recordEntry, readSession, getRecentSummaries, cleanStaleSessionFiles } from \"../session/tracker.js\";\nimport { analyzeDrift, shouldAlertDrift } from \"../session/drift.js\";\nimport { getCached, setCached } from \"../cache/explanation-cache.js\";\n\nconst output: string[] = [];\n\nfunction addOutput(text: string): void {\n output.push(text);\n}\n\n/**\n * Emit the Claude Code hook JSON on stdout so the accumulated output\n * appears as a system message in the user's terminal. Always exit 0 so\n * Claude Code is never blocked.\n */\nfunction safeExit(): never {\n if (output.length > 0) {\n // Leading newline separates the box from Claude Code's \"PostToolUse:X says:\"\n // prefix, which otherwise renders on the same line as the top border.\n const systemMessage = \"\\n\" + output.join(\"\\n\");\n process.stdout.write(JSON.stringify({ systemMessage }) + \"\\n\");\n }\n process.exit(0);\n}\n\nasync function readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let data = \"\";\n process.stdin.setEncoding(\"utf-8\");\n process.stdin.on(\"data\", (chunk) => {\n data += chunk;\n });\n process.stdin.on(\"end\", () => resolve(data));\n process.stdin.on(\"error\", () => resolve(data));\n // Safety timeout: if stdin has no data in 2s, resolve empty.\n setTimeout(() => resolve(data), 2000);\n });\n}\n\nfunction parsePayload(raw: string): HookPayload | null {\n try {\n const parsed = JSON.parse(raw);\n if (typeof parsed.session_id === \"string\" && typeof parsed.tool_name === \"string\") {\n return parsed as HookPayload;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction loadConfigSafe(cwd: string): Config {\n // loadConfig already falls back to the global config at\n // ~/.code-explainer.config.json when the project path doesn't exist,\n // and to built-in defaults if neither exists.\n try {\n return loadConfig(join(cwd, \"code-explainer.config.json\"));\n } catch {\n return DEFAULT_CONFIG;\n }\n}\n\nfunction isHookEnabled(toolName: string, config: Config): boolean {\n const lower = toolName.toLowerCase();\n if (lower === \"edit\" || lower === \"multiedit\") return config.hooks.edit;\n if (lower === \"write\") return config.hooks.write;\n if (lower === \"bash\") return config.hooks.bash;\n return false;\n}\n\nasync function runEngine(\n filePath: string,\n diff: string,\n config: Config,\n userPrompt: string | undefined,\n recentSummaries: string[],\n signal: AbortSignal\n): Promise<EngineOutcome> {\n if (signal.aborted) {\n return { kind: \"skip\", reason: \"interrupted by user\" };\n }\n if (config.engine === \"ollama\") {\n return callOllama({ filePath, diff, config, recentSummaries });\n }\n return callClaude({ filePath, diff, config, userPrompt, recentSummaries });\n}\n\nasync function main(): Promise<void> {\n // Interrupt handler — always exit 0 on Ctrl+C.\n const controller = new AbortController();\n process.on(\"SIGINT\", () => {\n controller.abort();\n addOutput(formatSkipNotice(\"interrupted by user\"));\n safeExit();\n });\n\n const raw = await readStdin();\n if (!raw.trim()) safeExit();\n\n const payload = parsePayload(raw);\n if (!payload) safeExit();\n\n const cwd = payload.cwd || process.cwd();\n const config = loadConfigSafe(cwd);\n\n if (!isHookEnabled(payload.tool_name, config)) safeExit();\n\n cleanStaleSessionFiles();\n\n // Pass session_id to downstream modules via env (so summary/session-end\n // commands pick the right session without re-parsing the payload).\n process.env.CODE_EXPLAINER_SESSION_ID = payload.session_id;\n\n // Extract the diff based on tool name.\n let filePath: string;\n let diff: string;\n\n const lowerTool = payload.tool_name.toLowerCase();\n if (lowerTool === \"edit\" || lowerTool === \"multiedit\" || lowerTool === \"write\") {\n const input = payload.tool_input as {\n file_path?: string;\n filePath?: string;\n old_string?: string;\n new_string?: string;\n oldString?: string;\n newString?: string;\n edits?: Array<{ old_string?: string; new_string?: string; oldString?: string; newString?: string }>;\n };\n const target = input.file_path ?? input.filePath;\n if (!target) safeExit();\n filePath = target as string;\n\n if (isExcluded(filePath, config.exclude)) safeExit();\n\n // Preferred path: use the payload's old/new strings directly. This works\n // for untracked files (very common) and is always more accurate than git\n // diff, which may miss changes on files that were created and edited in\n // the same session without a commit.\n let result;\n if (lowerTool === \"edit\") {\n const oldStr = input.old_string ?? input.oldString ?? \"\";\n const newStr = input.new_string ?? input.newString ?? \"\";\n if (oldStr || newStr) {\n result = buildDiffFromEdit(filePath, oldStr, newStr);\n } else {\n result = extractEditDiff(filePath, cwd);\n }\n } else if (lowerTool === \"multiedit\") {\n if (input.edits && input.edits.length > 0) {\n result = buildDiffFromMultiEdit(filePath, input.edits);\n } else {\n result = extractEditDiff(filePath, cwd);\n }\n } else {\n result = extractNewFileDiff(filePath, cwd);\n }\n\n if (result.kind === \"empty\") safeExit();\n if (result.kind === \"skip\") {\n addOutput(formatSkipNotice(result.reason));\n safeExit();\n }\n if (result.kind === \"binary\") {\n addOutput(formatSkipNotice(result.message));\n safeExit();\n }\n diff = result.content;\n } else if (lowerTool === \"bash\") {\n const input = payload.tool_input as { command?: string };\n const command = input.command ?? \"\";\n if (!command || !shouldCaptureBash(command)) safeExit();\n filePath = \"<bash command>\";\n diff = command;\n } else {\n safeExit();\n }\n\n // Cache check.\n const cacheKey = `${filePath}\\n${diff}`;\n const cached = getCached(payload.session_id, cacheKey);\n let result: ExplanationResult | null = null;\n\n if (cached) {\n result = cached;\n } else {\n const recentSummaries = getRecentSummaries(payload.session_id, 3);\n const outcome = await runEngine(\n filePath,\n diff,\n config,\n undefined,\n recentSummaries,\n controller.signal\n );\n if (outcome.kind === \"skip\") {\n addOutput(formatSkipNotice(outcome.reason));\n safeExit();\n }\n if (outcome.kind === \"error\") {\n addOutput(formatErrorNotice(outcome.problem, outcome.cause, outcome.fix));\n safeExit();\n }\n result = outcome.result;\n setCached(payload.session_id, cacheKey, result);\n }\n\n // Path-heuristic drift analysis (only meaningful for Edit/Write).\n let driftReason: string | undefined;\n if (filePath !== \"<bash command>\") {\n const priorEntries = readSession(payload.session_id);\n const analysis = analyzeDrift(filePath, priorEntries);\n if (analysis.isUnrelated) {\n driftReason = analysis.reason;\n }\n }\n\n // Print the explanation box with the new structured format.\n addOutput(\n formatExplanationBox({\n filePath,\n result,\n detailLevel: config.detailLevel,\n language: config.language,\n })\n );\n\n // Record the entry. Use impact as the summary for drift/session tracking.\n const summaryForTracking = result.isSamePattern\n ? result.samePatternNote || \"Same pattern as a recent edit\"\n : result.impact;\n\n recordEntry(payload.session_id, {\n file: filePath,\n timestamp: Date.now(),\n risk: result.risk,\n summary: summaryForTracking,\n unrelated: !!driftReason,\n });\n\n // Drift alert at threshold.\n const updated = readSession(payload.session_id);\n const driftCheck = shouldAlertDrift(updated);\n if (driftCheck.shouldAlert) {\n addOutput(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles, undefined, config.language));\n }\n\n safeExit();\n}\n\nmain().catch(() => {\n // Never fail the hook — always exit 0.\n safeExit();\n});\n","import { execFile } from \"node:child_process\";\nimport type {\n Config,\n DeepDiveItem,\n ExplanationResult,\n RiskLevel,\n} from \"../config/schema.js\";\nimport { buildClaudePrompt } from \"../prompts/templates.js\";\nimport type { EngineOutcome } from \"./ollama.js\";\n\nexport interface ClaudeCallInputs {\n filePath: string;\n diff: string;\n config: Config;\n userPrompt?: string;\n recentSummaries?: string[];\n}\n\nfunction extractJson(text: string): string | null {\n const trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n const fenceMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?```/);\n if (fenceMatch) {\n return fenceMatch[1].trim();\n }\n const start = trimmed.indexOf(\"{\");\n const end = trimmed.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return trimmed.slice(start, end + 1);\n }\n return null;\n}\n\nfunction coerceString(v: unknown): string {\n return typeof v === \"string\" ? v : \"\";\n}\n\nfunction coerceDeepDive(v: unknown): DeepDiveItem[] {\n if (!Array.isArray(v)) return [];\n return v\n .filter((it): it is { term?: unknown; explanation?: unknown } => typeof it === \"object\" && it !== null)\n .map((it) => ({\n term: coerceString(it.term),\n explanation: coerceString(it.explanation),\n }))\n .filter((it) => it.term.length > 0);\n}\n\nfunction parseResponse(rawText: string): ExplanationResult | null {\n const json = extractJson(rawText);\n if (!json) return null;\n try {\n const parsed = JSON.parse(json) as Record<string, unknown>;\n const risk = coerceString(parsed.risk) as RiskLevel;\n if (![\"none\", \"low\", \"medium\", \"high\"].includes(risk)) return null;\n return {\n impact: coerceString(parsed.impact),\n howItWorks: coerceString(parsed.howItWorks),\n why: coerceString(parsed.why),\n deepDive: coerceDeepDive(parsed.deepDive),\n isSamePattern: parsed.isSamePattern === true,\n samePatternNote: coerceString(parsed.samePatternNote),\n risk,\n riskReason: coerceString(parsed.riskReason),\n };\n } catch {\n return null;\n }\n}\n\nfunction truncateText(text: string, max: number): string {\n if (text.length <= max) return text;\n return text.slice(0, max) + \"...\";\n}\n\ninterface ExecResult {\n stdout: string;\n stderr: string;\n code: number | null;\n}\n\nfunction runClaude(prompt: string, timeoutMs: number): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const child = execFile(\n \"claude\",\n [\"-p\", prompt],\n {\n timeout: timeoutMs,\n maxBuffer: 1024 * 1024 * 2, // 2MB\n windowsHide: true,\n },\n (err, stdout, stderr) => {\n if (err) {\n const e = err as NodeJS.ErrnoException & { killed?: boolean; signal?: string };\n if (e.code === \"ENOENT\") {\n reject(Object.assign(new Error(\"claude CLI not found\"), { code: \"ENOENT\" }));\n return;\n }\n if (e.killed || e.signal === \"SIGTERM\") {\n reject(Object.assign(new Error(\"claude timed out\"), { code: \"TIMEOUT\" }));\n return;\n }\n // Include stderr for context\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: e.code as unknown as number ?? 1 });\n return;\n }\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: 0 });\n }\n );\n child.on(\"error\", (err) => {\n reject(err);\n });\n });\n}\n\nexport async function callClaude(inputs: ClaudeCallInputs): Promise<EngineOutcome> {\n const prompt = buildClaudePrompt(inputs.config.detailLevel, {\n filePath: inputs.filePath,\n diff: inputs.diff,\n userPrompt: inputs.userPrompt,\n language: inputs.config.language,\n learnerLevel: inputs.config.learnerLevel,\n recentSummaries: inputs.recentSummaries,\n });\n\n try {\n const result = await runClaude(prompt, inputs.config.skipIfSlowMs);\n\n if (result.code !== 0) {\n const combined = `${result.stderr}\\n${result.stdout}`.toLowerCase();\n if (/auth|login|unauthorized|not authenticated|api key/i.test(combined)) {\n return {\n kind: \"error\",\n problem: \"Claude Code is not authenticated\",\n cause: \"The 'claude' CLI requires a valid login\",\n fix: \"Run 'claude login' in a terminal, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI returned an error\",\n cause: result.stderr.trim() || `exit code ${result.code}`,\n fix: \"Run 'claude --help' to verify the CLI works, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n\n if (!result.stdout.trim()) {\n return { kind: \"skip\", reason: \"Claude returned an empty response\" };\n }\n\n const parsed = parseResponse(result.stdout);\n if (parsed) {\n return { kind: \"ok\", result: parsed };\n }\n\n // Malformed output: fall back to truncated raw text as the impact field.\n return {\n kind: \"ok\",\n result: {\n impact: truncateText(result.stdout.trim(), 200),\n howItWorks: \"\",\n why: \"\",\n deepDive: [],\n isSamePattern: false,\n samePatternNote: \"\",\n risk: \"none\",\n riskReason: \"\",\n },\n };\n } catch (err) {\n const e = err as Error & { code?: string };\n if (e.code === \"ENOENT\") {\n return {\n kind: \"error\",\n problem: \"Claude CLI not found\",\n cause: \"The 'claude' command is not installed or not on PATH\",\n fix: \"Install Claude Code, or switch to Ollama engine via 'npx vibe-code-explainer config'\",\n };\n }\n if (e.code === \"TIMEOUT\") {\n return {\n kind: \"skip\",\n reason: `explanation took too long (>${inputs.config.skipIfSlowMs}ms)`,\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI invocation failed\",\n cause: e.message,\n fix: \"Check that 'claude' works by running 'claude --help' in a terminal\",\n };\n }\n}\n","import { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\n\nexport type DiffResult =\n | { kind: \"diff\"; content: string; lines: number; truncated: boolean }\n | { kind: \"new-file\"; content: string; lines: number; truncated: boolean }\n | { kind: \"binary\"; message: string }\n | { kind: \"empty\" }\n | { kind: \"skip\"; reason: string };\n\nconst MAX_DIFF_LINES = 200;\nconst HEAD_LINES = 150;\nconst TAIL_LINES = 50;\n\nfunction truncateDiff(content: string): { content: string; lines: number; truncated: boolean } {\n const lines = content.split(\"\\n\");\n if (lines.length <= MAX_DIFF_LINES) {\n return { content, lines: lines.length, truncated: false };\n }\n const head = lines.slice(0, HEAD_LINES);\n const tail = lines.slice(-TAIL_LINES);\n const omitted = lines.length - HEAD_LINES - TAIL_LINES;\n const truncated = [\n ...head,\n `[...truncated, ${omitted} more lines not shown]`,\n ...tail,\n ].join(\"\\n\");\n return { content: truncated, lines: lines.length, truncated: true };\n}\n\nfunction runGit(args: string[], cwd: string): string {\n return execFileSync(\"git\", args, { cwd, encoding: \"utf-8\", maxBuffer: 1024 * 1024 * 10 });\n}\n\n/**\n * Build a unified-style diff directly from an Edit tool's old_string/new_string\n * payload. More reliable than `git diff` because it works even on untracked\n * files (the common case: user asks Claude to edit a file that was just\n * created and never committed). Multi-line strings produce proper line-by-line\n * - / + markers so the model can tell additions apart from modifications.\n */\nexport function buildDiffFromEdit(\n filePath: string,\n oldString: string,\n newString: string\n): DiffResult {\n if (!oldString && !newString) return { kind: \"empty\" };\n\n const oldLines = oldString ? oldString.split(\"\\n\") : [];\n const newLines = newString ? newString.split(\"\\n\") : [];\n\n const header = `--- a/${filePath}\\n+++ b/${filePath}\\n@@ Edit @@`;\n const minus = oldLines.map((l) => `-${l}`).join(\"\\n\");\n const plus = newLines.map((l) => `+${l}`).join(\"\\n\");\n\n const parts = [header, minus, plus].filter((s) => s.length > 0);\n const content = parts.join(\"\\n\");\n\n const { content: final, lines, truncated } = truncateDiff(content);\n return { kind: \"diff\", content: final, lines, truncated };\n}\n\n/**\n * Build a combined unified-style diff from a MultiEdit payload's edits array.\n */\nexport function buildDiffFromMultiEdit(\n filePath: string,\n edits: Array<{ old_string?: string; new_string?: string; oldString?: string; newString?: string }>\n): DiffResult {\n if (!edits || edits.length === 0) return { kind: \"empty\" };\n\n const header = `--- a/${filePath}\\n+++ b/${filePath}`;\n const hunks: string[] = [];\n\n for (let i = 0; i < edits.length; i++) {\n const e = edits[i];\n const oldStr = e.old_string ?? e.oldString ?? \"\";\n const newStr = e.new_string ?? e.newString ?? \"\";\n if (!oldStr && !newStr) continue;\n\n const oldLines = oldStr ? oldStr.split(\"\\n\") : [];\n const newLines = newStr ? newStr.split(\"\\n\") : [];\n const minus = oldLines.map((l) => `-${l}`).join(\"\\n\");\n const plus = newLines.map((l) => `+${l}`).join(\"\\n\");\n\n hunks.push(`@@ Edit ${i + 1} of ${edits.length} @@`);\n if (minus) hunks.push(minus);\n if (plus) hunks.push(plus);\n }\n\n if (hunks.length === 0) return { kind: \"empty\" };\n\n const content = [header, ...hunks].join(\"\\n\");\n const { content: final, lines, truncated } = truncateDiff(content);\n return { kind: \"diff\", content: final, lines, truncated };\n}\n\nexport function extractEditDiff(filePath: string, cwd: string): DiffResult {\n // Check if we're in a git repo.\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n return { kind: \"skip\", reason: \"not inside a git repository\" };\n }\n\n // Check for binary.\n try {\n const numstat = runGit([\"diff\", \"--numstat\", \"--\", filePath], cwd).trim();\n if (numstat.startsWith(\"-\\t-\\t\")) {\n return { kind: \"binary\", message: `Binary file modified: ${filePath}` };\n }\n } catch {\n // Non-fatal, fall through to diff.\n }\n\n let diffOutput = \"\";\n try {\n diffOutput = runGit([\"diff\", \"--no-color\", \"--\", filePath], cwd);\n } catch {\n diffOutput = \"\";\n }\n\n if (!diffOutput.trim()) {\n // File may be untracked (newly created via Write/Edit on a fresh file).\n return extractNewFileDiff(filePath, cwd);\n }\n\n const { content, lines, truncated } = truncateDiff(diffOutput);\n return { kind: \"diff\", content, lines, truncated };\n}\n\nexport function extractNewFileDiff(filePath: string, cwd: string): DiffResult {\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n // Not a git repo — fall back to reading the file if possible.\n return readFileAsNewDiff(filePath);\n }\n\n // Check if file is untracked.\n let untracked = \"\";\n try {\n untracked = runGit([\"ls-files\", \"--others\", \"--exclude-standard\", \"--\", filePath], cwd).trim();\n } catch {\n untracked = \"\";\n }\n\n if (untracked) {\n return readFileAsNewDiff(filePath);\n }\n\n // Might be a file with no changes, or tracked without a diff.\n return { kind: \"empty\" };\n}\n\nfunction readFileAsNewDiff(filePath: string): DiffResult {\n if (!existsSync(filePath)) {\n return { kind: \"skip\", reason: `file not found: ${filePath}` };\n }\n\n try {\n const stat = statSync(filePath);\n if (stat.size > 2 * 1024 * 1024) {\n return { kind: \"skip\", reason: `file too large (${Math.round(stat.size / 1024)}KB)` };\n }\n\n const raw = readFileSync(filePath, \"utf-8\");\n if (!raw.trim()) {\n return { kind: \"empty\" };\n }\n\n // Check for binary content (null bytes).\n if (raw.includes(\"\\0\")) {\n return { kind: \"binary\", message: `Binary file created: ${filePath}` };\n }\n\n const withMarkers = raw.split(\"\\n\").map((l) => `+ ${l}`).join(\"\\n\");\n const diff = `--- /dev/null\\n+++ b/${filePath}\\n${withMarkers}`;\n const { content, lines, truncated } = truncateDiff(diff);\n return { kind: \"new-file\", content, lines, truncated };\n } catch {\n return { kind: \"skip\", reason: \"could not read file\" };\n }\n}\n\n/**\n * Minimal glob matcher supporting *, **, and simple extensions.\n * Matches POSIX-style paths (caller normalizes).\n *\n * - `*.ext` matches `file.ext` in any directory\n * - `dir/**` matches anything under `dir/` recursively\n * - `**\\/file.ts` matches `file.ts` anywhere\n */\nexport function matchesGlob(filePath: string, pattern: string): boolean {\n const normalized = filePath.replace(/\\\\/g, \"/\");\n const normalizedPattern = pattern.replace(/\\\\/g, \"/\");\n\n // Build regex from the pattern\n let regexSrc = \"\";\n let i = 0;\n while (i < normalizedPattern.length) {\n const ch = normalizedPattern[i];\n if (ch === \"*\") {\n if (normalizedPattern[i + 1] === \"*\") {\n // ** matches anything (including /)\n regexSrc += \".*\";\n i += 2;\n if (normalizedPattern[i] === \"/\") i++; // consume trailing /\n } else {\n // * matches anything except /\n regexSrc += \"[^/]*\";\n i++;\n }\n } else if (ch === \"?\") {\n regexSrc += \"[^/]\";\n i++;\n } else if (/[.+^${}()|[\\]]/.test(ch)) {\n regexSrc += \"\\\\\" + ch;\n i++;\n } else {\n regexSrc += ch;\n i++;\n }\n }\n\n // If the pattern has no directory component, match the filename anywhere.\n const hasSlash = normalizedPattern.includes(\"/\");\n const anchored = hasSlash\n ? new RegExp(`^${regexSrc}$`)\n : new RegExp(`(^|/)${regexSrc}$`);\n\n return anchored.test(normalized);\n}\n\nexport function isExcluded(filePath: string, patterns: string[]): boolean {\n return patterns.some((p) => matchesGlob(filePath, p));\n}\n","/**\n * Bash command filter — decides whether a Bash command should trigger an\n * explanation. Filters to filesystem-mutating and state-changing commands\n * while skipping read-only operations.\n */\n\n// Commands that modify filesystem or project state.\nconst MUTATING_COMMANDS = new Set([\n \"rm\",\n \"mv\",\n \"cp\",\n \"mkdir\",\n \"rmdir\",\n \"chmod\",\n \"chown\",\n \"ln\",\n \"touch\",\n \"dd\",\n]);\n\n// Commands that need a specific subcommand/flag to be mutating.\nconst CONTEXTUAL_COMMANDS: Record<string, RegExp> = {\n npm: /\\b(install|add|remove|uninstall|update|ci|link|unlink|init|publish)\\b/,\n yarn: /\\b(add|remove|install|upgrade|init|publish|link|unlink)\\b/,\n pnpm: /\\b(add|remove|install|update|link|unlink|publish)\\b/,\n pip: /\\b(install|uninstall)\\b/,\n pip3: /\\b(install|uninstall)\\b/,\n brew: /\\b(install|uninstall|reinstall|upgrade|link|unlink|tap|untap)\\b/,\n apt: /\\b(install|remove|purge|upgrade|update)\\b/,\n \"apt-get\": /\\b(install|remove|purge|upgrade|update)\\b/,\n git: /\\b(checkout|reset|revert|rebase|merge|commit|push|pull|clean|stash|rm|mv|init|clone|cherry-pick|restore|switch)\\b/,\n sed: /(?:^|\\s)-i\\b/,\n curl: /(?:^|\\s)-[a-zA-Z]*o\\b|--output\\b/,\n wget: /.*/,\n tar: /(?:^|\\s)-[a-zA-Z]*x\\b|--extract\\b|(?:^|\\s)-[a-zA-Z]*c\\b|--create\\b/,\n unzip: /.*/,\n docker: /\\b(run|build|push|pull|rm|rmi|exec|start|stop|kill)\\b/,\n make: /.*/,\n cargo: /\\b(build|run|install|add|remove|update|publish)\\b/,\n go: /\\b(build|install|get|mod)\\b/,\n bun: /\\b(install|add|remove|run|build|init|create|link|unlink)\\b/,\n deno: /\\b(install|compile|bundle|run)\\b/,\n};\n\n// Commands that are always read-only and never trigger.\nconst READONLY_COMMANDS = new Set([\n \"ls\",\n \"cat\",\n \"head\",\n \"tail\",\n \"grep\",\n \"find\",\n \"which\",\n \"whereis\",\n \"type\",\n \"echo\",\n \"printf\",\n \"pwd\",\n \"whoami\",\n \"id\",\n \"date\",\n \"uname\",\n \"df\",\n \"du\",\n \"ps\",\n \"top\",\n \"htop\",\n \"stat\",\n \"file\",\n \"wc\",\n \"sort\",\n \"uniq\",\n \"diff\",\n \"man\",\n \"help\",\n \"history\",\n \"tree\",\n \"less\",\n \"more\",\n \"env\",\n \"printenv\",\n \"test\",\n \"true\",\n \"false\",\n]);\n\n/**\n * Split a command string on pipe, semicolon, and logical operators.\n * Returns each sub-command with leading whitespace trimmed.\n */\nexport function splitCommandChain(command: string): string[] {\n // Split on unquoted chain operators. A simple split is good enough for\n // the vibe-coder case; we explicitly do not handle exotic quoting.\n return command\n .split(/(?:\\|\\||&&|[|;])/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Check if a single sub-command (e.g., \"rm file.txt\") should trigger.\n */\nexport function subCommandShouldCapture(subCmd: string): boolean {\n // Detect redirections (> or >>) — always capture.\n if (/(?<!\\d)>>?(?!\\d)/.test(subCmd)) {\n // Bare redirections like `ls > out.txt` still count as mutating.\n return true;\n }\n\n const tokens = subCmd.trim().split(/\\s+/);\n if (tokens.length === 0) return false;\n\n // Skip env-var assignments like `FOO=bar cmd`.\n let idx = 0;\n while (idx < tokens.length && /^[A-Z_][A-Z0-9_]*=/.test(tokens[idx])) {\n idx++;\n }\n const head = tokens[idx];\n if (!head) return false;\n\n // Strip leading path (e.g., /usr/bin/rm -> rm).\n const bin = head.split(/[/\\\\]/).pop() ?? head;\n\n if (READONLY_COMMANDS.has(bin)) return false;\n if (MUTATING_COMMANDS.has(bin)) return true;\n\n const contextPattern = CONTEXTUAL_COMMANDS[bin];\n if (contextPattern) {\n const rest = tokens.slice(idx + 1).join(\" \");\n return contextPattern.test(rest);\n }\n\n return false;\n}\n\n/**\n * Decide whether a full command string should trigger a code-explainer\n * explanation. Returns true if ANY sub-command in the chain is mutating.\n */\nexport function shouldCaptureBash(command: string): boolean {\n const parts = splitCommandChain(command);\n return parts.some((p) => subCommandShouldCapture(p));\n}\n","import type { SessionEntry } from \"./tracker.js\";\n\nconst SENSITIVE_PATTERNS = [\n /(^|\\/)\\.env(\\.|$)/i,\n /(^|\\/)payment/i,\n /(^|\\/)billing/i,\n /(^|\\/)stripe/i,\n /(^|\\/)auth/i,\n /(^|\\/)credential/i,\n /(^|\\/)secret/i,\n /(^|\\/)\\.ssh\\//i,\n];\n\nfunction topLevelDir(path: string): string {\n const norm = path.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n const parts = norm.split(\"/\").filter(Boolean);\n return parts[0] ?? \"\";\n}\n\nexport function matchesSensitivePattern(filePath: string): boolean {\n return SENSITIVE_PATTERNS.some((re) => re.test(filePath));\n}\n\nexport interface DriftAnalysis {\n isUnrelated: boolean;\n reason?: string;\n}\n\n/**\n * Path-heuristic drift detection for the Ollama engine.\n * Flags a new file as unrelated if:\n * 1. It matches a sensitive pattern (env, payment, auth, secrets) AND\n * the session did not start in a similarly-sensitive area.\n * 2. It lives in a different top-level directory than every file\n * edited so far in the session (cross-module drift).\n *\n * Returns `isUnrelated: false` for the first few edits (not enough\n * context to judge).\n */\nexport function analyzeDrift(\n newFilePath: string,\n priorEntries: SessionEntry[]\n): DriftAnalysis {\n // Not enough context yet for the first edit.\n if (priorEntries.length === 0) {\n return { isUnrelated: false };\n }\n\n const priorFiles = Array.from(new Set(priorEntries.map((e) => e.file)));\n const priorTopDirs = new Set(priorFiles.map(topLevelDir));\n const priorHasSensitive = priorFiles.some(matchesSensitivePattern);\n\n // Sensitive-pattern drift: the new file is in a sensitive area but\n // prior session was not working there.\n if (matchesSensitivePattern(newFilePath) && !priorHasSensitive) {\n return {\n isUnrelated: true,\n reason: `touches sensitive area (${newFilePath}) that was not part of earlier edits`,\n };\n }\n\n // Cross-module drift: only flag after at least 2 prior edits established\n // a working area.\n if (priorEntries.length >= 2) {\n const newTop = topLevelDir(newFilePath);\n if (newTop && !priorTopDirs.has(newTop)) {\n return {\n isUnrelated: true,\n reason: `is in a different top-level area (${newTop}) than earlier edits (${Array.from(priorTopDirs).join(\", \")})`,\n };\n }\n }\n\n return { isUnrelated: false };\n}\n\nexport interface DriftThresholdResult {\n shouldAlert: boolean;\n totalFiles: number;\n unrelatedFiles: string[];\n}\n\nconst DRIFT_ALERT_THRESHOLD = 3;\n\n/**\n * Decide whether to surface a drift alert based on accumulated session state.\n * Fires once every time the unrelated count crosses a multiple of the threshold.\n */\nexport function shouldAlertDrift(entries: SessionEntry[]): DriftThresholdResult {\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(\n new Set(entries.filter((e) => e.unrelated).map((e) => e.file))\n );\n\n // Alert exactly when we hit the threshold (not every call afterwards).\n const shouldAlert =\n unrelatedFiles.length > 0 &&\n unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0 &&\n entries.filter((e) => e.unrelated).length ===\n entries.filter((e) => e.unrelated).length;\n\n // Fire specifically on the edit that caused us to cross the threshold.\n const lastEntry = entries[entries.length - 1];\n const lastWasUnrelated = lastEntry?.unrelated ?? false;\n const crossedThreshold =\n lastWasUnrelated && unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0;\n\n return {\n shouldAlert: crossedThreshold && shouldAlert,\n totalFiles: uniqueFiles.length,\n unrelatedFiles,\n };\n}\n"],"mappings":"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","names":["result"]}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  callOllama
4
- } from "./chunk-YS2XIZIA.js";
4
+ } from "./chunk-KS3PATTI.js";
5
5
  import {
6
6
  MODEL_OPTIONS,
7
7
  detectNvidiaVram,
@@ -15,8 +15,9 @@ import {
15
15
  CONFIG_FILENAME,
16
16
  DEFAULT_CONFIG,
17
17
  LANGUAGE_NAMES,
18
+ LEARNER_LEVEL_NAMES,
18
19
  getGlobalConfigPath
19
- } from "./chunk-5NCRRHU7.js";
20
+ } from "./chunk-JPDU5ASR.js";
20
21
  import {
21
22
  __require
22
23
  } from "./chunk-7OCVIDC7.js";
@@ -271,6 +272,20 @@ async function runInit(args) {
271
272
  initialValue: "en"
272
273
  });
273
274
  handleCancel(languageChoice);
275
+ let learnerLevelChoice = DEFAULT_CONFIG.learnerLevel;
276
+ if (detailChoice !== "minimal") {
277
+ const choice = await select({
278
+ message: "What's your programming knowledge level? (used to calibrate explanations)",
279
+ options: Object.keys(LEARNER_LEVEL_NAMES).map((code) => ({
280
+ label: LEARNER_LEVEL_NAMES[code],
281
+ value: code,
282
+ hint: code === "intermediate" ? "default" : void 0
283
+ })),
284
+ initialValue: "intermediate"
285
+ });
286
+ handleCancel(choice);
287
+ learnerLevelChoice = choice;
288
+ }
274
289
  let ollamaModel = DEFAULT_CONFIG.ollamaModel;
275
290
  if (engineChoice === "ollama") {
276
291
  const status = await checkOllama();
@@ -320,6 +335,7 @@ Start it with: ${pc.cyan("ollama serve")} (in a separate terminal).`,
320
335
  engine: engineChoice,
321
336
  detailLevel: detailChoice,
322
337
  language: languageChoice,
338
+ learnerLevel: learnerLevelChoice,
323
339
  ollamaModel
324
340
  };
325
341
  const projectRoot = process.cwd();
@@ -360,4 +376,4 @@ Every Claude Code session on ${homedir()} will now explain every Edit, Write, an
360
376
  export {
361
377
  runInit
362
378
  };
363
- //# sourceMappingURL=init-KUVD2YGA.js.map
379
+ //# sourceMappingURL=init-OTODBBPP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/init.ts","../src/detect/platform.ts"],"sourcesContent":["import { intro, outro, select, confirm, cancel, isCancel, spinner, note } from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport { execFileSync, spawn } from \"node:child_process\";\nimport { existsSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n DEFAULT_CONFIG,\n CONFIG_FILENAME,\n getGlobalConfigPath,\n LANGUAGE_NAMES,\n LEARNER_LEVEL_NAMES,\n type Config,\n type Engine,\n type DetailLevel,\n type Language,\n type LearnerLevel,\n} from \"../config/schema.js\";\nimport { detectPlatform, ollamaInstallCommand } from \"../detect/platform.js\";\nimport { detectNvidiaVram, pickModelForVram, MODEL_OPTIONS } from \"../detect/vram.js\";\nimport { mergeHooksIntoSettings, mergeHooksIntoUserSettings } from \"../config/merge.js\";\nimport { callOllama } from \"../engines/ollama.js\";\n\ntype InstallScope = \"project\" | \"global\";\n\nfunction isInsideNpxCache(path: string): boolean {\n const norm = path.replace(/\\\\/g, \"/\").toLowerCase();\n return norm.includes(\"/_npx/\") || norm.includes(\"/.npm/_npx/\");\n}\n\nfunction isInsideGlobalNpmRoot(path: string): boolean {\n const norm = path.replace(/\\\\/g, \"/\").toLowerCase();\n return norm.includes(\"/node_modules/vibe-code-explainer/\") &&\n (norm.includes(\"/npm/\") || norm.includes(\"/npm-global/\") || norm.includes(\"/appdata/roaming/npm/\") || norm.includes(\"/.nvm/\"));\n}\n\nasync function runNpmInstall(args: string[], cwd: string): Promise<void> {\n await new Promise<void>((resolvePromise, rejectPromise) => {\n const child = spawn(\"npm\", args, {\n stdio: \"inherit\",\n cwd,\n shell: process.platform === \"win32\",\n });\n child.on(\"error\", rejectPromise);\n child.on(\"close\", (code) => {\n if (code === 0) resolvePromise();\n else rejectPromise(new Error(`npm install exited with code ${code}`));\n });\n });\n}\n\nfunction resolveGlobalHookScriptPath(): string | null {\n // Find the global npm root and build the hook path.\n try {\n const root = execFileSync(\"npm\", [\"root\", \"-g\"], {\n encoding: \"utf-8\",\n shell: process.platform === \"win32\",\n }).trim();\n return join(root, \"vibe-code-explainer\", \"dist\", \"hooks\", \"post-tool.js\");\n } catch {\n return null;\n }\n}\n\nasync function ensureProjectInstall(projectRoot: string): Promise<string> {\n const thisFile = fileURLToPath(import.meta.url);\n\n if (!isInsideNpxCache(thisFile)) {\n const distDir = resolve(thisFile, \"..\");\n return join(distDir, \"hooks\", \"post-tool.js\");\n }\n\n note(\n \"Installing vibe-code-explainer as a dev dependency so the hook path is stable...\",\n \"Local install\"\n );\n\n const pkgPath = join(projectRoot, \"package.json\");\n if (!existsSync(pkgPath)) {\n writeFileSync(\n pkgPath,\n JSON.stringify({ name: basename(projectRoot), private: true, version: \"0.0.0\" }, null, 2) + \"\\n\"\n );\n }\n\n await runNpmInstall([\"install\", \"--save-dev\", \"vibe-code-explainer\"], projectRoot);\n return join(projectRoot, \"node_modules\", \"vibe-code-explainer\", \"dist\", \"hooks\", \"post-tool.js\");\n}\n\nasync function ensureGlobalInstall(): Promise<string> {\n const thisFile = fileURLToPath(import.meta.url);\n if (isInsideGlobalNpmRoot(thisFile)) {\n const distDir = resolve(thisFile, \"..\");\n return join(distDir, \"hooks\", \"post-tool.js\");\n }\n\n note(\n \"Installing vibe-code-explainer globally so every project picks it up...\",\n \"Global install\"\n );\n\n await runNpmInstall([\"install\", \"-g\", \"vibe-code-explainer\"], process.cwd());\n\n const resolved = resolveGlobalHookScriptPath();\n if (!resolved) {\n throw new Error(\n \"Global install completed but 'npm root -g' failed. Run 'npm root -g' manually to locate the install path and file an issue.\"\n );\n }\n return resolved;\n}\n\nasync function checkOllama(): Promise<\"running\" | \"installed-not-running\" | \"missing\"> {\n try {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), 1500);\n const res = await fetch(\"http://localhost:11434/api/tags\", { signal: ctrl.signal });\n clearTimeout(timer);\n if (res.ok) return \"running\";\n } catch {\n // fall through\n }\n\n try {\n execFileSync(\"ollama\", [\"--version\"], { stdio: \"ignore\" });\n return \"installed-not-running\";\n } catch {\n return \"missing\";\n }\n}\n\nasync function pullModel(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\n return new Promise((resolvePromise) => {\n const child = spawn(\"ollama\", [\"pull\", model], { stdio: \"inherit\" });\n child.on(\"error\", () => {\n process.stderr.write(pc.red(\"\\nFailed to run `ollama pull`. Make sure Ollama is running.\\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\nasync function runWarmup(config: Config): Promise<void> {\n const s = spinner();\n s.start(`Warming up ${config.ollamaModel}`);\n\n const outcome = await callOllama({\n filePath: \"warmup.txt\",\n diff: \"+ hello world\",\n config: { ...config, skipIfSlowMs: 60000 },\n });\n\n if (outcome.kind === \"ok\") {\n s.stop(\"Warmup complete. First real explanation will be fast.\");\n } else if (outcome.kind === \"error\") {\n s.stop(`Warmup failed: ${outcome.problem}`);\n } else {\n s.stop(`Warmup skipped: ${outcome.reason}`);\n }\n}\n\nasync function pickModel(): Promise<string | symbol> {\n const vram = detectNvidiaVram();\n if (vram) {\n const recommended = pickModelForVram(vram.totalMb);\n note(\n `Detected ${pc.green(vram.gpuName)} with ${pc.green(`${Math.round(vram.totalMb / 1024)} GB VRAM`)}.\\nRecommended model: ${pc.cyan(recommended)}`,\n \"GPU detection\"\n );\n return recommended;\n }\n\n note(\"No NVIDIA GPU detected (or nvidia-smi unavailable). Pick a model that fits your machine.\", \"GPU detection\");\n const choice = await select({\n message: \"Which model should code-explainer use?\",\n options: MODEL_OPTIONS.map((m) => ({\n label: m.label,\n value: m.model,\n hint: m.hint,\n })),\n initialValue: \"qwen3.5:4b\",\n });\n return choice;\n}\n\nfunction handleCancel<T>(value: T | symbol): asserts value is T {\n if (isCancel(value)) {\n cancel(\"Setup cancelled.\");\n process.exit(0);\n }\n}\n\nexport async function runInit(args: string[]): Promise<void> {\n const skipWarmup = args.includes(\"--skip-warmup\");\n\n intro(pc.bold(\"code-explainer setup\"));\n\n // Step 1: Install scope — project or global\n const scope = await select<InstallScope>({\n message: \"Where should code-explainer be installed?\",\n options: [\n {\n label: \"This project only\",\n value: \"project\",\n hint: \"Hooks in .claude/settings.local.json, config in this folder\",\n },\n {\n label: \"Globally (every project)\",\n value: \"global\",\n hint: \"Hooks in ~/.claude/settings.json, config in ~/.code-explainer.config.json\",\n },\n ],\n initialValue: \"project\",\n });\n handleCancel(scope);\n\n // Step 2: Engine\n const engineChoice = await select<Engine>({\n message: \"Which explanation engine do you want to use?\",\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: \"ollama\",\n });\n handleCancel(engineChoice);\n\n // Step 3: Detail level\n const detailChoice = await select<DetailLevel>({\n message: \"How detailed should explanations be?\",\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: \"standard\",\n });\n handleCancel(detailChoice);\n\n // Step 4: Language\n const languageChoice = await select<Language>({\n message: \"What language should explanations be written in?\",\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: \"en\",\n });\n handleCancel(languageChoice);\n\n // Step 5: Learner level (only meaningful when teaching is on, i.e. not minimal)\n let learnerLevelChoice: LearnerLevel = DEFAULT_CONFIG.learnerLevel;\n if (detailChoice !== \"minimal\") {\n const choice = await select<LearnerLevel>({\n message: \"What's your programming knowledge level? (used to calibrate explanations)\",\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: \"intermediate\",\n });\n handleCancel(choice);\n learnerLevelChoice = choice;\n }\n\n // Step 6: Ollama-specific setup\n let ollamaModel = DEFAULT_CONFIG.ollamaModel;\n\n if (engineChoice === \"ollama\") {\n const status = await checkOllama();\n\n if (status === \"missing\") {\n const platform = detectPlatform();\n const installCmd = ollamaInstallCommand(platform);\n note(\n `Ollama is not installed.\\nInstall with: ${pc.cyan(installCmd)}\\nOr visit: ${pc.cyan(\"https://ollama.com/download\")}`,\n \"Missing prerequisite\"\n );\n const proceed = await confirm({\n message: \"Install Ollama manually and continue after it's ready?\",\n initialValue: true,\n });\n handleCancel(proceed);\n if (!proceed) {\n cancel(\"Setup paused. Run 'npx vibe-code-explainer init' again after installing Ollama.\");\n process.exit(0);\n }\n } else if (status === \"installed-not-running\") {\n note(\n `Ollama is installed but the service isn't running.\\nStart it with: ${pc.cyan(\"ollama serve\")} (in a separate terminal).`,\n \"Ollama not running\"\n );\n }\n\n const modelChoice = await pickModel();\n handleCancel(modelChoice);\n ollamaModel = modelChoice;\n\n const pullOk = await pullModel(ollamaModel);\n if (!pullOk) {\n const skipPull = await confirm({\n message: \"Continue without pulling the model? (You'll need to run 'ollama pull' manually.)\",\n initialValue: false,\n });\n handleCancel(skipPull);\n if (!skipPull) {\n cancel(\"Setup aborted.\");\n process.exit(1);\n }\n }\n }\n\n // Build config\n const config: Config = {\n ...DEFAULT_CONFIG,\n engine: engineChoice,\n detailLevel: detailChoice,\n language: languageChoice,\n learnerLevel: learnerLevelChoice,\n ollamaModel,\n };\n\n // Write config file (global or project path)\n const projectRoot = process.cwd();\n const configPath = scope === \"global\" ? getGlobalConfigPath() : join(projectRoot, CONFIG_FILENAME);\n\n if (existsSync(configPath)) {\n const overwrite = await confirm({\n message: `${configPath} already exists. Overwrite?`,\n initialValue: false,\n });\n handleCancel(overwrite);\n if (!overwrite) {\n cancel(\"Setup aborted to avoid overwriting existing config.\");\n process.exit(0);\n }\n }\n\n writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\");\n\n // Install and wire up hooks\n let hookScript: string;\n let mergeResult;\n\n if (scope === \"global\") {\n hookScript = await ensureGlobalInstall();\n mergeResult = mergeHooksIntoUserSettings(hookScript);\n } else {\n hookScript = await ensureProjectInstall(projectRoot);\n mergeResult = mergeHooksIntoSettings(projectRoot, hookScript);\n }\n\n note(\n `${pc.green(\"\\u2713\")} Wrote ${pc.cyan(configPath)}\\n${pc.green(\"\\u2713\")} ${mergeResult.created ? \"Created\" : \"Updated\"} ${pc.cyan(mergeResult.path)}`,\n \"Configuration saved\"\n );\n\n // Warmup\n if (engineChoice === \"ollama\" && !skipWarmup) {\n await runWarmup(config);\n }\n\n const whereMsg =\n scope === \"global\"\n ? `\\nEvery Claude Code session on ${homedir()} will now explain every Edit, Write, and destructive Bash command.`\n : \"\\nClaude Code sessions in this project will now explain every Edit, Write, and destructive Bash command.\";\n\n outro(pc.bold(\"code-explainer is active.\") + whereMsg);\n}\n","import { platform } from \"node:os\";\n\nexport type Platform = \"windows\" | \"macos\" | \"linux\" | \"wsl\" | \"unknown\";\n\nexport function detectPlatform(): Platform {\n const p = platform();\n if (p === \"win32\") return \"windows\";\n if (p === \"darwin\") return \"macos\";\n if (p === \"linux\") {\n // WSL detection: Microsoft string in /proc/version\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 release = fs.readFileSync(\"/proc/version\", \"utf-8\").toLowerCase();\n if (release.includes(\"microsoft\") || release.includes(\"wsl\")) return \"wsl\";\n } catch {\n // ignore\n }\n return \"linux\";\n }\n return \"unknown\";\n}\n\nexport function ollamaInstallCommand(p: Platform): string {\n switch (p) {\n case \"macos\":\n return \"brew install ollama\";\n case \"windows\":\n return \"winget install Ollama.Ollama\";\n case \"linux\":\n case \"wsl\":\n return \"curl -fsSL https://ollama.com/install.sh | sh\";\n default:\n return \"Visit https://ollama.com/download to install Ollama\";\n }\n}\n"],"mappings":"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","names":["platform"]}
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ callOllama,
4
+ parseResponse,
5
+ runWarmup
6
+ } from "./chunk-KS3PATTI.js";
7
+ import "./chunk-JPDU5ASR.js";
8
+ import "./chunk-7OCVIDC7.js";
9
+ export {
10
+ callOllama,
11
+ parseResponse,
12
+ runWarmup
13
+ };
14
+ //# sourceMappingURL=ollama-PGPTPYS4.js.map
@@ -3,15 +3,17 @@ import {
3
3
  CONFIG_FILENAME,
4
4
  DEFAULT_CONFIG,
5
5
  LANGUAGE_NAMES,
6
+ LEARNER_LEVEL_NAMES,
6
7
  getGlobalConfigPath,
7
8
  loadConfig
8
- } from "./chunk-5NCRRHU7.js";
9
+ } from "./chunk-JPDU5ASR.js";
9
10
  import "./chunk-7OCVIDC7.js";
10
11
  export {
11
12
  CONFIG_FILENAME,
12
13
  DEFAULT_CONFIG,
13
14
  LANGUAGE_NAMES,
15
+ LEARNER_LEVEL_NAMES,
14
16
  getGlobalConfigPath,
15
17
  loadConfig
16
18
  };
17
- //# sourceMappingURL=schema-TBXFNCIG.js.map
19
+ //# sourceMappingURL=schema-TEWSY7EF.js.map
@@ -2,18 +2,20 @@
2
2
  import {
3
3
  cleanStaleSessionFiles,
4
4
  endSession,
5
+ getRecentSummaries,
5
6
  getSessionFilePath,
6
7
  printSummary,
7
8
  readSession,
8
9
  recordEntry
9
- } from "./chunk-W67RX53R.js";
10
+ } from "./chunk-Y55I7ZS5.js";
10
11
  import "./chunk-7OCVIDC7.js";
11
12
  export {
12
13
  cleanStaleSessionFiles,
13
14
  endSession,
15
+ getRecentSummaries,
14
16
  getSessionFilePath,
15
17
  printSummary,
16
18
  readSession,
17
19
  recordEntry
18
20
  };
19
- //# sourceMappingURL=tracker-HCWPUZIO.js.map
21
+ //# sourceMappingURL=tracker-4ORSFJQB.js.map
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  CONFIG_FILENAME,
8
8
  getGlobalConfigPath
9
- } from "./chunk-5NCRRHU7.js";
9
+ } from "./chunk-JPDU5ASR.js";
10
10
  import "./chunk-7OCVIDC7.js";
11
11
 
12
12
  // src/cli/uninstall.ts
@@ -98,4 +98,4 @@ async function runUninstall() {
98
98
  export {
99
99
  runUninstall
100
100
  };
101
- //# sourceMappingURL=uninstall-CNGJWJYQ.js.map
101
+ //# sourceMappingURL=uninstall-PN7724RX.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-code-explainer",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Real-time diff explanations for vibe coders using Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/config/schema.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport type Engine = \"ollama\" | \"claude\";\nexport type DetailLevel = \"minimal\" | \"standard\" | \"verbose\";\nexport type RiskLevel = \"none\" | \"low\" | \"medium\" | \"high\";\n\nexport type Language =\n | \"en\"\n | \"pt\"\n | \"es\"\n | \"fr\"\n | \"de\"\n | \"it\"\n | \"zh\"\n | \"ja\"\n | \"ko\";\n\nexport const LANGUAGE_NAMES: Record<Language, string> = {\n en: \"English\",\n pt: \"Portuguese\",\n es: \"Spanish\",\n fr: \"French\",\n de: \"German\",\n it: \"Italian\",\n zh: \"Chinese\",\n ja: \"Japanese\",\n ko: \"Korean\",\n};\n\nexport interface HooksConfig {\n edit: boolean;\n write: boolean;\n bash: boolean;\n}\n\nexport interface BashFilterConfig {\n capturePatterns: string[];\n}\n\nexport interface Config {\n engine: Engine;\n ollamaModel: string;\n ollamaUrl: string;\n detailLevel: DetailLevel;\n language: Language;\n hooks: HooksConfig;\n exclude: string[];\n skipIfSlowMs: number;\n bashFilter: BashFilterConfig;\n}\n\nexport interface ExplanationResult {\n summary: string;\n risk: RiskLevel;\n riskReason: string;\n}\n\nexport interface HookPayload {\n session_id: string;\n transcript_path: string;\n cwd: string;\n permission_mode: string;\n hook_event_name: string;\n tool_name: string;\n tool_input: Record<string, unknown>;\n tool_response: string;\n}\n\nexport const CONFIG_FILENAME = \"code-explainer.config.json\";\n\nexport function getGlobalConfigPath(): string {\n return join(homedir(), \".code-explainer.config.json\");\n}\n\nexport const DEFAULT_CONFIG: Config = {\n engine: \"ollama\",\n ollamaModel: \"qwen3.5:4b\",\n ollamaUrl: \"http://localhost:11434\",\n detailLevel: \"standard\",\n language: \"en\",\n hooks: {\n edit: true,\n write: true,\n bash: true,\n },\n exclude: [\"*.lock\", \"dist/**\", \"node_modules/**\"],\n skipIfSlowMs: 8000,\n bashFilter: {\n capturePatterns: [\n \"rm\",\n \"mv\",\n \"cp\",\n \"mkdir\",\n \"npm install\",\n \"pip install\",\n \"yarn add\",\n \"pnpm add\",\n \"chmod\",\n \"chown\",\n \"git checkout\",\n \"git reset\",\n \"git revert\",\n \"sed -i\",\n ],\n },\n};\n\nfunction mergeConfig(base: Config, overlay: Partial<Config>): Config {\n return {\n ...base,\n ...overlay,\n hooks: { ...base.hooks, ...(overlay.hooks ?? {}) },\n bashFilter: {\n ...base.bashFilter,\n ...(overlay.bashFilter ?? {}),\n },\n };\n}\n\nfunction tryReadJson(path: string): Partial<Config> | null {\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf-8\")) as Partial<Config>;\n } catch {\n return null;\n }\n}\n\n/**\n * Load config with three-level resolution, most specific first:\n * 1. Project config (passed as configPath) — overrides everything\n * 2. Global user config (~/.code-explainer.config.json)\n * 3. Built-in defaults\n *\n * A project config that lacks a field falls through to the global; a global\n * that lacks a field falls through to defaults. This lets a global install\n * set everyone's defaults while still allowing per-project overrides.\n */\nexport function loadConfig(configPath: string): Config {\n const globalConfig = tryReadJson(getGlobalConfigPath());\n const projectConfig = tryReadJson(configPath);\n\n let result = DEFAULT_CONFIG;\n if (globalConfig) result = mergeConfig(result, globalConfig);\n if (projectConfig) result = mergeConfig(result, projectConfig);\n return result;\n}\n"],"mappings":";;;AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,eAAe;AACxB,SAAS,YAAY;AAiBd,IAAM,iBAA2C;AAAA,EACtD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAyCO,IAAM,kBAAkB;AAExB,SAAS,sBAA8B;AAC5C,SAAO,KAAK,QAAQ,GAAG,6BAA6B;AACtD;AAEO,IAAM,iBAAyB;AAAA,EACpC,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,WAAW;AAAA,EACX,aAAa;AAAA,EACb,UAAU;AAAA,EACV,OAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA,EACA,SAAS,CAAC,UAAU,WAAW,iBAAiB;AAAA,EAChD,cAAc;AAAA,EACd,YAAY;AAAA,IACV,iBAAiB;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,YAAY,MAAc,SAAkC;AACnE,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG;AAAA,IACH,OAAO,EAAE,GAAG,KAAK,OAAO,GAAI,QAAQ,SAAS,CAAC,EAAG;AAAA,IACjD,YAAY;AAAA,MACV,GAAG,KAAK;AAAA,MACR,GAAI,QAAQ,cAAc,CAAC;AAAA,IAC7B;AAAA,EACF;AACF;AAEA,SAAS,YAAY,MAAsC;AACzD,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYO,SAAS,WAAW,YAA4B;AACrD,QAAM,eAAe,YAAY,oBAAoB,CAAC;AACtD,QAAM,gBAAgB,YAAY,UAAU;AAE5C,MAAI,SAAS;AACb,MAAI,aAAc,UAAS,YAAY,QAAQ,YAAY;AAC3D,MAAI,cAAe,UAAS,YAAY,QAAQ,aAAa;AAC7D,SAAO;AACT;","names":[]}