vibe-code-explainer 0.3.6 → 0.3.7

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.
@@ -707,6 +707,6 @@ ${diff}`;
707
707
  safeExit();
708
708
  }
709
709
  main().catch(() => {
710
- safeExit();
710
+ process.exit(0);
711
711
  });
712
712
  //# sourceMappingURL=post-tool.js.map
@@ -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 } from \"../engines/ollama.js\";\nimport { callClaude } from \"../engines/claude.js\";\nimport type { EngineOutcome } from \"../engines/types.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\";\nimport { isSafeSessionId } from \"../session/session-id.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 *\n * Uses the write-then-exit-in-callback pattern: on piped stdio (which is\n * how Claude Code invokes the hook) a bare `process.exit(0)` right after\n * `process.stdout.write(...)` can truncate the buffered payload. Waiting\n * for the write callback ensures the JSON envelope reaches the parent\n * process. A backstop timeout guarantees eventual exit even if the stream\n * never drains.\n */\nfunction safeExit(): never {\n if (output.length === 0) {\n process.exit(0);\n }\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 const payload = JSON.stringify({ systemMessage }) + \"\\n\";\n process.stdout.write(payload, () => process.exit(0));\n // Backstop: force exit in 500ms if the callback never fires (e.g. stdout\n // detached). Accept occasional truncation over hanging the pipe.\n setTimeout(() => process.exit(0), 500);\n // Unreachable: either the write callback or the timeout terminates the\n // process. Throw only to satisfy the `never` return type.\n throw new Error(\"unreachable\");\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 !== \"object\" || parsed === null) return null;\n // session_id must be a safe identifier — it's interpolated into\n // tmpdir paths, so an attacker-controlled value like `../../evil`\n // would escape the user-private dir.\n if (!isSafeSessionId(parsed.session_id)) return null;\n if (typeof parsed.tool_name !== \"string\") return null;\n // tool_input is declared required in the schema and dereferenced below;\n // reject payloads where it's missing or not an object rather than\n // relying on the top-level catch to swallow a later TypeError silently.\n if (typeof parsed.tool_input !== \"object\" || parsed.tool_input === null) return null;\n return parsed as HookPayload;\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 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, recentSummaries });\n}\n\ninterface DiffTarget {\n filePath: string;\n diff: string;\n}\n\n/**\n * Build a diff target from an Edit/Write/MultiEdit payload. Returns null and\n * calls safeExit() internally for cases that should not produce an explanation\n * (empty diff, excluded file, binary file, missing file path).\n */\nfunction buildEditWriteDiff(payload: HookPayload, config: Config, cwd: string): DiffTarget | null {\n const lowerTool = payload.tool_name.toLowerCase();\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\n const target = input.file_path ?? input.filePath;\n if (!target) { safeExit(); }\n const 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 result = (oldStr || newStr) ? buildDiffFromEdit(filePath, oldStr, newStr) : extractEditDiff(filePath, cwd);\n } else if (lowerTool === \"multiedit\") {\n result = (input.edits && input.edits.length > 0)\n ? buildDiffFromMultiEdit(filePath, input.edits)\n : extractEditDiff(filePath, cwd);\n } else {\n result = extractNewFileDiff(filePath, cwd);\n }\n\n if (result.kind === \"empty\") { safeExit(); }\n if (result.kind === \"skip\") { addOutput(formatSkipNotice(result.reason)); safeExit(); }\n if (result.kind === \"binary\") { addOutput(formatSkipNotice(result.message)); safeExit(); }\n\n return { filePath, diff: result.content };\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 // Resolve filePath and diff based on tool type.\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 target = buildEditWriteDiff(payload, config, cwd);\n if (!target) safeExit();\n ({ filePath, diff } = target as DiffTarget);\n } else if (lowerTool === \"bash\") {\n const input = payload.tool_input as { command?: string };\n const command = input.command ?? \"\";\n if (!command || !shouldCaptureBash(command, config.bashFilter.capturePatterns)) safeExit();\n filePath = \"<bash command>\";\n diff = command;\n } else {\n safeExit();\n }\n\n // Read session once — reused for recent summaries (prompt context) and\n // drift analysis to avoid two disk reads per hook invocation.\n const isBash = filePath === \"<bash command>\";\n const priorEntries = isBash ? [] : readSession(payload.session_id);\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, priorEntries);\n const outcome = await runEngine(filePath, diff, config, recentSummaries, 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 (!isBash) {\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 — build fresh post-write list from priorEntries\n // plus the entry we just recorded, without another disk read.\n const entryJustRecorded: import(\"../session/tracker.js\").SessionEntry = {\n file: filePath,\n timestamp: Date.now(),\n risk: result.risk,\n summary: summaryForTracking,\n unrelated: !!driftReason,\n };\n const updatedEntries = [...priorEntries, entryJustRecorded];\n const driftCheck = shouldAlertDrift(updatedEntries);\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 { Config } from \"../config/schema.js\";\nimport { buildClaudePrompt } from \"../prompts/templates.js\";\nimport type { EngineOutcome } from \"./types.js\";\nimport { parseResponse, truncateText } from \"./parse.js\";\n\nexport interface ClaudeCallInputs {\n filePath: string;\n diff: string;\n config: Config;\n recentSummaries?: string[];\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 // Guard prompt building so config-enum drift cannot throw out of the engine\n // (top-level main() would otherwise swallow silently with no skip notice).\n let prompt: string;\n try {\n prompt = buildClaudePrompt(inputs.config.detailLevel, {\n filePath: inputs.filePath,\n diff: inputs.diff,\n language: inputs.config.language,\n learnerLevel: inputs.config.learnerLevel,\n recentSummaries: inputs.recentSummaries,\n });\n } catch (err) {\n return {\n kind: \"error\",\n problem: \"Failed to build Claude prompt\",\n cause: (err as Error).message || String(err),\n fix: \"Check detailLevel/learnerLevel/language values via 'npx vibe-code-explainer config'\",\n };\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\n/**\n * Inspect the first 8KB of the raw file bytes to decide whether content is\n * binary BEFORE decoding as UTF-8. Node's `readFileSync(path, 'utf-8')`\n * replaces invalid bytes with U+FFFD rather than preserving null bytes,\n * which means binary files without literal NULs (PNG/WASM/.mo/most\n * proprietary formats) would silently pass a later `raw.includes(\"\\0\")`\n * check and be sent to the LLM as garbled \"text\".\n */\nfunction looksBinary(buf: Buffer): boolean {\n const sample = buf.length > 8192 ? buf.subarray(0, 8192) : buf;\n if (sample.length === 0) return false;\n if (sample.indexOf(0) !== -1) return true;\n let nonPrint = 0;\n for (let i = 0; i < sample.length; i++) {\n const b = sample[i];\n // Common whitespace (tab, LF, CR), printable ASCII, or UTF-8 continuation\n // / high-bit bytes. Count anything else as suspicious.\n if (\n b === 0x09 ||\n b === 0x0a ||\n b === 0x0d ||\n (b >= 0x20 && b <= 0x7e) ||\n b >= 0x80\n ) {\n continue;\n }\n nonPrint++;\n }\n return nonPrint / sample.length > 0.3;\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 buf = readFileSync(filePath);\n if (buf.length === 0) {\n return { kind: \"empty\" };\n }\n if (looksBinary(buf)) {\n return { kind: \"binary\", message: `Binary file created: ${filePath}` };\n }\n\n const raw = buf.toString(\"utf-8\");\n if (!raw.trim()) {\n return { kind: \"empty\" };\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.\n *\n * Default posture: capture-unless-readonly.\n * Any command NOT on the READONLY list is assumed potentially mutating and\n * triggers an explanation. Known mutating commands and contextual commands are\n * checked explicitly, but unknown commands also trigger — it is safer to\n * over-explain than to silently skip a destructive but unfamiliar command.\n */\n\n// Commands that modify filesystem or project state — explicit capture list.\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 \"tee\",\n \"install\",\n \"truncate\",\n \"shred\",\n \"rsync\",\n \"scp\",\n \"sftp\",\n \"mount\",\n \"umount\",\n \"kill\",\n \"killall\",\n \"pkill\",\n \"crontab\",\n \"useradd\",\n \"userdel\",\n \"usermod\",\n \"groupadd\",\n \"groupdel\",\n \"passwd\",\n \"chpasswd\",\n \"visudo\",\n \"systemctl\",\n \"service\",\n \"launchctl\",\n // Note: brew is in CONTEXTUAL_COMMANDS (finer-grained control); do not add here.\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 *\n * Scope / limitations:\n * - Does NOT parse quotes, heredocs, or subshell boundaries (`$(...)`, backticks).\n * A command like `echo 'a ; rm x'` will be (incorrectly) split on the quoted `;`.\n * - Does NOT handle the background operator `&` — `cmd1 & cmd2` is treated as one.\n * - Does NOT unescape backslash-escaped operators.\n *\n * This is a vibe-coder heuristic, not a shell parser. The bash filter's\n * safer posture (capture-unless-readonly, recursive mutating-token scan)\n * catches the cases this splitter misses.\n */\nexport function splitCommandChain(command: string): string[] {\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 // Capture-unless-readonly: unknown commands are assumed potentially mutating.\n return true;\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 *\n * Pass `capturePatterns` from config.bashFilter.capturePatterns to also\n * match user-defined literal substrings before applying the built-in rules.\n * This lets users add patterns like \"mydeployscript\" or \"terraform apply\".\n */\nexport function shouldCaptureBash(command: string, capturePatterns: string[] = []): boolean {\n // User-defined literal patterns take priority — if any pattern is a\n // substring of the raw command string, capture immediately.\n if (capturePatterns.length > 0 && capturePatterns.some((p) => command.includes(p))) {\n return true;\n }\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, on the single edit that takes the unique-unrelated-file count\n * to exactly DRIFT_ALERT_THRESHOLD. Further unrelated files in the same\n * session do not refire — the user can run `summary` for a full picture.\n * This avoids the alert-fatigue pattern where naturally cross-module work\n * in a monorepo would trigger repeated alerts at 3, 6, 9, etc.\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 const lastEntry = entries[entries.length - 1];\n const lastWasUnrelated = lastEntry?.unrelated ?? false;\n\n // Single-fire: alert only on the invocation that takes unique unrelated\n // file count to exactly the threshold, and only when the triggering edit\n // itself was the unrelated one (otherwise we'd alert on a benign edit that\n // simply happened to be logged after a drift crossing).\n const shouldAlert =\n lastWasUnrelated && unrelatedFiles.length === DRIFT_ALERT_THRESHOLD;\n\n return {\n shouldAlert,\n totalFiles: uniqueFiles.length,\n unrelatedFiles,\n };\n}\n"],"mappings":"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","names":[]}
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 } from \"../engines/ollama.js\";\nimport { callClaude } from \"../engines/claude.js\";\nimport type { EngineOutcome } from \"../engines/types.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\";\nimport { isSafeSessionId } from \"../session/session-id.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 *\n * Uses the write-then-exit-in-callback pattern: on piped stdio (which is\n * how Claude Code invokes the hook) a bare `process.exit(0)` right after\n * `process.stdout.write(...)` can truncate the buffered payload. Waiting\n * for the write callback ensures the JSON envelope reaches the parent\n * process. A backstop timeout guarantees eventual exit even if the stream\n * never drains.\n */\nfunction safeExit(): never {\n if (output.length === 0) {\n process.exit(0);\n }\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 const payload = JSON.stringify({ systemMessage }) + \"\\n\";\n process.stdout.write(payload, () => process.exit(0));\n // Backstop: force exit in 500ms if the callback never fires (e.g. stdout\n // detached). Accept occasional truncation over hanging the pipe.\n setTimeout(() => process.exit(0), 500);\n // Unreachable: either the write callback or the timeout terminates the\n // process. Throw only to satisfy the `never` return type.\n throw new Error(\"unreachable\");\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 !== \"object\" || parsed === null) return null;\n // session_id must be a safe identifier — it's interpolated into\n // tmpdir paths, so an attacker-controlled value like `../../evil`\n // would escape the user-private dir.\n if (!isSafeSessionId(parsed.session_id)) return null;\n if (typeof parsed.tool_name !== \"string\") return null;\n // tool_input is declared required in the schema and dereferenced below;\n // reject payloads where it's missing or not an object rather than\n // relying on the top-level catch to swallow a later TypeError silently.\n if (typeof parsed.tool_input !== \"object\" || parsed.tool_input === null) return null;\n return parsed as HookPayload;\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 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, recentSummaries });\n}\n\ninterface DiffTarget {\n filePath: string;\n diff: string;\n}\n\n/**\n * Build a diff target from an Edit/Write/MultiEdit payload. Returns null and\n * calls safeExit() internally for cases that should not produce an explanation\n * (empty diff, excluded file, binary file, missing file path).\n */\nfunction buildEditWriteDiff(payload: HookPayload, config: Config, cwd: string): DiffTarget | null {\n const lowerTool = payload.tool_name.toLowerCase();\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\n const target = input.file_path ?? input.filePath;\n if (!target) { safeExit(); }\n const 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 result = (oldStr || newStr) ? buildDiffFromEdit(filePath, oldStr, newStr) : extractEditDiff(filePath, cwd);\n } else if (lowerTool === \"multiedit\") {\n result = (input.edits && input.edits.length > 0)\n ? buildDiffFromMultiEdit(filePath, input.edits)\n : extractEditDiff(filePath, cwd);\n } else {\n result = extractNewFileDiff(filePath, cwd);\n }\n\n if (result.kind === \"empty\") { safeExit(); }\n if (result.kind === \"skip\") { addOutput(formatSkipNotice(result.reason)); safeExit(); }\n if (result.kind === \"binary\") { addOutput(formatSkipNotice(result.message)); safeExit(); }\n\n return { filePath, diff: result.content };\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 // Resolve filePath and diff based on tool type.\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 target = buildEditWriteDiff(payload, config, cwd);\n if (!target) safeExit();\n ({ filePath, diff } = target as DiffTarget);\n } else if (lowerTool === \"bash\") {\n const input = payload.tool_input as { command?: string };\n const command = input.command ?? \"\";\n if (!command || !shouldCaptureBash(command, config.bashFilter.capturePatterns)) safeExit();\n filePath = \"<bash command>\";\n diff = command;\n } else {\n safeExit();\n }\n\n // Read session once — reused for recent summaries (prompt context) and\n // drift analysis to avoid two disk reads per hook invocation.\n const isBash = filePath === \"<bash command>\";\n const priorEntries = isBash ? [] : readSession(payload.session_id);\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, priorEntries);\n const outcome = await runEngine(filePath, diff, config, recentSummaries, 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 (!isBash) {\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 — build fresh post-write list from priorEntries\n // plus the entry we just recorded, without another disk read.\n const entryJustRecorded: import(\"../session/tracker.js\").SessionEntry = {\n file: filePath,\n timestamp: Date.now(),\n risk: result.risk,\n summary: summaryForTracking,\n unrelated: !!driftReason,\n };\n const updatedEntries = [...priorEntries, entryJustRecorded];\n const driftCheck = shouldAlertDrift(updatedEntries);\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 // Do NOT call safeExit() here: safeExit() throws after writing its payload,\n // which causes this catch to fire and call safeExit() again, producing two\n // JSON lines on stdout. Claude Code concatenates them, JSON.parse fails, and\n // the explanation is silently dropped. Just exit cleanly instead.\n process.exit(0);\n});\n","import { execFile } from \"node:child_process\";\nimport type { Config } from \"../config/schema.js\";\nimport { buildClaudePrompt } from \"../prompts/templates.js\";\nimport type { EngineOutcome } from \"./types.js\";\nimport { parseResponse, truncateText } from \"./parse.js\";\n\nexport interface ClaudeCallInputs {\n filePath: string;\n diff: string;\n config: Config;\n recentSummaries?: string[];\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 // Guard prompt building so config-enum drift cannot throw out of the engine\n // (top-level main() would otherwise swallow silently with no skip notice).\n let prompt: string;\n try {\n prompt = buildClaudePrompt(inputs.config.detailLevel, {\n filePath: inputs.filePath,\n diff: inputs.diff,\n language: inputs.config.language,\n learnerLevel: inputs.config.learnerLevel,\n recentSummaries: inputs.recentSummaries,\n });\n } catch (err) {\n return {\n kind: \"error\",\n problem: \"Failed to build Claude prompt\",\n cause: (err as Error).message || String(err),\n fix: \"Check detailLevel/learnerLevel/language values via 'npx vibe-code-explainer config'\",\n };\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\n/**\n * Inspect the first 8KB of the raw file bytes to decide whether content is\n * binary BEFORE decoding as UTF-8. Node's `readFileSync(path, 'utf-8')`\n * replaces invalid bytes with U+FFFD rather than preserving null bytes,\n * which means binary files without literal NULs (PNG/WASM/.mo/most\n * proprietary formats) would silently pass a later `raw.includes(\"\\0\")`\n * check and be sent to the LLM as garbled \"text\".\n */\nfunction looksBinary(buf: Buffer): boolean {\n const sample = buf.length > 8192 ? buf.subarray(0, 8192) : buf;\n if (sample.length === 0) return false;\n if (sample.indexOf(0) !== -1) return true;\n let nonPrint = 0;\n for (let i = 0; i < sample.length; i++) {\n const b = sample[i];\n // Common whitespace (tab, LF, CR), printable ASCII, or UTF-8 continuation\n // / high-bit bytes. Count anything else as suspicious.\n if (\n b === 0x09 ||\n b === 0x0a ||\n b === 0x0d ||\n (b >= 0x20 && b <= 0x7e) ||\n b >= 0x80\n ) {\n continue;\n }\n nonPrint++;\n }\n return nonPrint / sample.length > 0.3;\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 buf = readFileSync(filePath);\n if (buf.length === 0) {\n return { kind: \"empty\" };\n }\n if (looksBinary(buf)) {\n return { kind: \"binary\", message: `Binary file created: ${filePath}` };\n }\n\n const raw = buf.toString(\"utf-8\");\n if (!raw.trim()) {\n return { kind: \"empty\" };\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.\n *\n * Default posture: capture-unless-readonly.\n * Any command NOT on the READONLY list is assumed potentially mutating and\n * triggers an explanation. Known mutating commands and contextual commands are\n * checked explicitly, but unknown commands also trigger — it is safer to\n * over-explain than to silently skip a destructive but unfamiliar command.\n */\n\n// Commands that modify filesystem or project state — explicit capture list.\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 \"tee\",\n \"install\",\n \"truncate\",\n \"shred\",\n \"rsync\",\n \"scp\",\n \"sftp\",\n \"mount\",\n \"umount\",\n \"kill\",\n \"killall\",\n \"pkill\",\n \"crontab\",\n \"useradd\",\n \"userdel\",\n \"usermod\",\n \"groupadd\",\n \"groupdel\",\n \"passwd\",\n \"chpasswd\",\n \"visudo\",\n \"systemctl\",\n \"service\",\n \"launchctl\",\n // Note: brew is in CONTEXTUAL_COMMANDS (finer-grained control); do not add here.\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 *\n * Scope / limitations:\n * - Does NOT parse quotes, heredocs, or subshell boundaries (`$(...)`, backticks).\n * A command like `echo 'a ; rm x'` will be (incorrectly) split on the quoted `;`.\n * - Does NOT handle the background operator `&` — `cmd1 & cmd2` is treated as one.\n * - Does NOT unescape backslash-escaped operators.\n *\n * This is a vibe-coder heuristic, not a shell parser. The bash filter's\n * safer posture (capture-unless-readonly, recursive mutating-token scan)\n * catches the cases this splitter misses.\n */\nexport function splitCommandChain(command: string): string[] {\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 // Capture-unless-readonly: unknown commands are assumed potentially mutating.\n return true;\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 *\n * Pass `capturePatterns` from config.bashFilter.capturePatterns to also\n * match user-defined literal substrings before applying the built-in rules.\n * This lets users add patterns like \"mydeployscript\" or \"terraform apply\".\n */\nexport function shouldCaptureBash(command: string, capturePatterns: string[] = []): boolean {\n // User-defined literal patterns take priority — if any pattern is a\n // substring of the raw command string, capture immediately.\n if (capturePatterns.length > 0 && capturePatterns.some((p) => command.includes(p))) {\n return true;\n }\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, on the single edit that takes the unique-unrelated-file count\n * to exactly DRIFT_ALERT_THRESHOLD. Further unrelated files in the same\n * session do not refire — the user can run `summary` for a full picture.\n * This avoids the alert-fatigue pattern where naturally cross-module work\n * in a monorepo would trigger repeated alerts at 3, 6, 9, etc.\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 const lastEntry = entries[entries.length - 1];\n const lastWasUnrelated = lastEntry?.unrelated ?? false;\n\n // Single-fire: alert only on the invocation that takes unique unrelated\n // file count to exactly the threshold, and only when the triggering edit\n // itself was the unrelated one (otherwise we'd alert on a benign edit that\n // simply happened to be logged after a drift crossing).\n const shouldAlert =\n lastWasUnrelated && unrelatedFiles.length === DRIFT_ALERT_THRESHOLD;\n\n return {\n shouldAlert,\n totalFiles: uniqueFiles.length,\n unrelatedFiles,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "vibe-code-explainer",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Real-time diff explanations for vibe coders using Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
7
- "vibe-code-explainer": "./dist/cli/index.js"
7
+ "vibe-code-explainer": "dist/cli/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsup",