vibe-code-explainer 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VJN7Y4SI.js → chunk-JA5NWYY7.js} +5 -4
- package/dist/chunk-JA5NWYY7.js.map +1 -0
- package/dist/cli/index.js +2 -2
- package/dist/hooks/post-tool.js +2 -2
- package/dist/hooks/post-tool.js.map +1 -1
- package/dist/{tracker-Y2G5DW6Y.js → tracker-YZWTVUQG.js} +2 -2
- package/package.json +2 -2
- package/dist/chunk-VJN7Y4SI.js.map +0 -1
- /package/dist/{tracker-Y2G5DW6Y.js.map → tracker-YZWTVUQG.js.map} +0 -0
|
@@ -364,14 +364,15 @@ function blankLine() {
|
|
|
364
364
|
function buildBoxOutput(contentLines, borderColor) {
|
|
365
365
|
const width = Math.min(getTerminalWidth() - 2, 70);
|
|
366
366
|
const innerWidth = width - 2;
|
|
367
|
-
const
|
|
368
|
-
const
|
|
367
|
+
const dashesRight = "\u2500".repeat(Math.max(0, innerWidth - BOX_TITLE.length - 4));
|
|
368
|
+
const top = rgb(borderColor, `\u256D\u2500 `) + dim(BOX_TITLE) + rgb(borderColor, ` ${dashesRight}\u2500\u256E`);
|
|
369
|
+
const bottom = rgb(borderColor, `\u2570${"\u2500".repeat(innerWidth)}\u256F`);
|
|
369
370
|
const sideChar = rgb(borderColor, "\u2502");
|
|
370
371
|
const middle = contentLines.map((bl) => {
|
|
371
372
|
const padding = " ".repeat(Math.max(0, innerWidth - bl.raw.length - PAD_LEFT - PAD_RIGHT));
|
|
372
373
|
return `${sideChar}${" ".repeat(PAD_LEFT)}${bl.text}${padding}${" ".repeat(PAD_RIGHT)}${sideChar}`;
|
|
373
374
|
});
|
|
374
|
-
return [
|
|
375
|
+
return [top, ...middle, bottom].join("\n");
|
|
375
376
|
}
|
|
376
377
|
function renderSection(def) {
|
|
377
378
|
const out = [];
|
|
@@ -715,4 +716,4 @@ export {
|
|
|
715
716
|
printSummary,
|
|
716
717
|
endSession
|
|
717
718
|
};
|
|
718
|
-
//# sourceMappingURL=chunk-
|
|
719
|
+
//# sourceMappingURL=chunk-JA5NWYY7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/session/tracker.ts","../src/cache/explanation-cache.ts","../src/session/session-id.ts","../src/session/tmpdir.ts","../src/format/box.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { RiskLevel } from \"../config/schema.js\";\nimport { clearCache } from \"../cache/explanation-cache.js\";\nimport { formatDriftAlert, printToStderr } from \"../format/box.js\";\nimport { assertSafeSessionId } from \"./session-id.js\";\nimport { getUserTmpDir } from \"./tmpdir.js\";\n\nconst TWO_HOURS_MS = 2 * 60 * 60 * 1000;\n// Minimum interval between stale-file cleanup runs to avoid stat-ing the\n// tmpdir on every hook invocation (which fires for every Edit/Write/Bash).\nconst CLEANUP_THROTTLE_MS = 60 * 1000;\n\nexport interface SessionEntry {\n file: string;\n timestamp: number;\n risk: RiskLevel;\n summary: string;\n unrelated?: boolean;\n}\n\nexport function getSessionFilePath(sessionId: string): string {\n assertSafeSessionId(sessionId);\n return join(getUserTmpDir(), `session-${sessionId}.jsonl`);\n}\n\nexport function recordEntry(sessionId: string, entry: SessionEntry): void {\n const path = getSessionFilePath(sessionId);\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n } catch {\n // Non-fatal\n }\n}\n\nexport function readSession(sessionId: string): SessionEntry[] {\n const path = getSessionFilePath(sessionId);\n if (!existsSync(path)) return [];\n\n try {\n const content = readFileSync(path, \"utf-8\");\n return content\n .split(\"\\n\")\n .filter((l) => l.trim())\n .map((line) => {\n try {\n return JSON.parse(line) as SessionEntry;\n } catch {\n return null;\n }\n })\n .filter((e): e is SessionEntry => e !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * Get the last N recorded summaries for this session, oldest-first.\n * Used to feed prompt context for \"same pattern\" detection.\n *\n * Pass `entries` if you've already called readSession() to avoid a second\n * disk read within the same hook invocation.\n */\nexport function getRecentSummaries(sessionId: string, n: number, entries?: SessionEntry[]): string[] {\n const all = entries ?? readSession(sessionId);\n if (all.length === 0) return [];\n return all.slice(-n).map((e) => `${e.file}: ${e.summary}`);\n}\n\nfunction getCleanupTimestampPath(): string {\n return join(getUserTmpDir(), \".last-cleanup\");\n}\n\nexport function cleanStaleSessionFiles(): void {\n try {\n const tsPath = getCleanupTimestampPath();\n const now = Date.now();\n\n // Throttle: skip if we cleaned up recently.\n if (existsSync(tsPath)) {\n try {\n const ts = parseInt(readFileSync(tsPath, \"utf-8\").trim(), 10);\n if (!isNaN(ts) && now - ts < CLEANUP_THROTTLE_MS) return;\n } catch {\n // If the timestamp file is malformed, proceed with cleanup.\n }\n }\n\n // Record the timestamp before cleaning so concurrent invocations see it.\n try {\n writeFileSync(tsPath, String(now), { mode: 0o600 });\n } catch {\n // Non-fatal — proceed with cleanup even if we can't update the timestamp.\n }\n\n const dir = getUserTmpDir();\n const entries = readdirSync(dir);\n for (const name of entries) {\n if (!name.startsWith(\"session-\") && !name.startsWith(\"cache-\")) continue;\n const filePath = join(dir, name);\n try {\n const stat = statSync(filePath);\n if (now - stat.mtimeMs > TWO_HOURS_MS) {\n unlinkSync(filePath);\n }\n } catch {\n // ignore\n }\n }\n } catch {\n // ignore\n }\n}\n\nfunction getSessionIdFromEnv(): string | undefined {\n return process.env.CODE_EXPLAINER_SESSION_ID;\n}\n\nfunction findLatestSession(): string | undefined {\n try {\n const dir = getUserTmpDir();\n const entries = readdirSync(dir)\n .filter((n) => n.startsWith(\"session-\") && n.endsWith(\".jsonl\"))\n .map((n) => ({\n name: n,\n id: n.slice(\"session-\".length, -\".jsonl\".length),\n mtime: statSync(join(dir, n)).mtimeMs,\n }))\n .sort((a, b) => b.mtime - a.mtime);\n return entries[0]?.id;\n } catch {\n return undefined;\n }\n}\n\nexport async function printSummary({ json = false }: { json?: boolean } = {}): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n if (json) {\n process.stdout.write(JSON.stringify({ error: \"No active session found\" }) + \"\\n\");\n } else {\n process.stderr.write(\"[code-explainer] No active session found. Session data is created when Claude Code makes changes.\\n\");\n }\n return;\n }\n\n const entries = readSession(sessionId);\n if (entries.length === 0) {\n if (json) {\n process.stdout.write(JSON.stringify({ sessionId, totalChanges: 0, files: [], risks: { none: 0, low: 0, medium: 0, high: 0 } }) + \"\\n\");\n } else {\n process.stderr.write(`[code-explainer] Session '${sessionId}' has no recorded changes yet.\\n`);\n }\n return;\n }\n\n const related = entries.filter((e) => !e.unrelated);\n const unrelated = entries.filter((e) => e.unrelated);\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(new Set(unrelated.map((e) => e.file)));\n\n const risks: Record<RiskLevel, number> = { none: 0, low: 0, medium: 0, high: 0 };\n for (const e of entries) risks[e.risk]++;\n\n if (json) {\n process.stdout.write(JSON.stringify({\n sessionId,\n totalChanges: entries.length,\n filesCount: uniqueFiles.length,\n relatedChanges: related.length,\n unrelatedChanges: unrelated.length,\n unrelatedFiles,\n risks,\n entries: entries.map((e) => ({ file: e.file, risk: e.risk, summary: e.summary, unrelated: !!e.unrelated })),\n }, null, 2) + \"\\n\");\n return;\n }\n\n const alert = formatDriftAlert(uniqueFiles.length, unrelatedFiles);\n printToStderr(alert);\n\n process.stderr.write(`\\nTotal changes: ${entries.length}\\n`);\n process.stderr.write(`Files touched: ${uniqueFiles.length}\\n`);\n process.stderr.write(`Related changes: ${related.length}\\n`);\n process.stderr.write(`Unrelated/risky: ${unrelated.length}\\n`);\n\n process.stderr.write(`\\nRisk breakdown:\\n`);\n process.stderr.write(` None: ${risks.none}\\n`);\n process.stderr.write(` Low: ${risks.low}\\n`);\n process.stderr.write(` Medium: ${risks.medium}\\n`);\n process.stderr.write(` High: ${risks.high}\\n`);\n}\n\nexport async function endSession(): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n process.stderr.write(\"[code-explainer] No active session to end.\\n\");\n return;\n }\n\n const sessionPath = getSessionFilePath(sessionId);\n if (existsSync(sessionPath)) {\n try {\n unlinkSync(sessionPath);\n } catch {\n // ignore\n }\n }\n clearCache(sessionId);\n process.stderr.write(`[code-explainer] Session '${sessionId}' ended. State cleared.\\n`);\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync, appendFileSync, writeFileSync, renameSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ExplanationResult } from \"../config/schema.js\";\nimport { assertSafeSessionId } from \"../session/session-id.js\";\nimport { getUserTmpDir } from \"../session/tmpdir.js\";\n\n// Rotate the JSONL cache file when it reaches this many lines to prevent\n// unbounded growth within a single long session.\nconst CACHE_ROTATE_THRESHOLD = 500;\n// After rotation, keep the N most recent unique entries (by hash).\nconst CACHE_COMPACT_TARGET = 250;\n\nexport function getCacheFilePath(sessionId: string): string {\n assertSafeSessionId(sessionId);\n return join(getUserTmpDir(), `cache-${sessionId}.jsonl`);\n}\n\nexport function hashDiff(diff: string): string {\n return createHash(\"sha256\").update(diff, \"utf-8\").digest(\"hex\");\n}\n\ninterface CacheEntry {\n hash: string;\n result: ExplanationResult;\n}\n\n/**\n * If the cache file has grown beyond CACHE_ROTATE_THRESHOLD lines, compact\n * it: deduplicate by hash (keeping the last occurrence) and write back the\n * CACHE_COMPACT_TARGET most recent unique entries atomically via a .tmp file.\n *\n * Atomic write (writeFileSync + renameSync) prevents a crash mid-write from\n * leaving a truncated cache. Non-fatal — any error is silently swallowed.\n *\n * Note: concurrent hook invocations can both pass the size check and attempt\n * to compact the same file. The last rename wins; both will produce a valid\n * compacted file, so data integrity is preserved without a lockfile.\n */\nfunction rotateCacheIfNeeded(path: string): void {\n try {\n const content = readFileSync(path, \"utf-8\");\n const lines = content.split(\"\\n\").filter((l) => l.trim());\n if (lines.length <= CACHE_ROTATE_THRESHOLD) return;\n\n // Deduplicate by hash — later lines overwrite earlier (newest wins).\n const seen = new Map<string, CacheEntry>();\n for (const line of lines) {\n try {\n const entry = JSON.parse(line) as CacheEntry;\n seen.set(entry.hash, entry);\n } catch {\n // skip malformed lines\n }\n }\n\n // Keep the CACHE_COMPACT_TARGET most recent unique entries.\n const unique = Array.from(seen.values());\n const compacted = unique.slice(-CACHE_COMPACT_TARGET);\n\n const tmp = path + \".tmp\";\n writeFileSync(tmp, compacted.map((e) => JSON.stringify(e)).join(\"\\n\") + \"\\n\", { mode: 0o600 });\n renameSync(tmp, path);\n } catch {\n // Non-fatal — rotation failure just means the file keeps growing.\n }\n}\n\nexport function getCached(sessionId: string, diff: string): ExplanationResult | undefined {\n const path = getCacheFilePath(sessionId);\n if (!existsSync(path)) return undefined;\n\n const hash = hashDiff(diff);\n try {\n const content = readFileSync(path, \"utf-8\");\n const lines = content.split(\"\\n\").filter((l) => l.trim());\n\n // Iterate in reverse so the most recent entry wins on duplicates.\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const entry = JSON.parse(lines[i]) as CacheEntry;\n if (entry.hash === hash) {\n return entry.result;\n }\n } catch {\n // Skip malformed line\n }\n }\n } catch {\n return undefined;\n }\n return undefined;\n}\n\nexport function setCached(sessionId: string, diff: string, result: ExplanationResult): void {\n const path = getCacheFilePath(sessionId);\n const entry: CacheEntry = { hash: hashDiff(diff), result };\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n rotateCacheIfNeeded(path);\n } catch {\n // Cache write failures are non-fatal\n }\n}\n\nexport function clearCache(sessionId: string): void {\n const path = getCacheFilePath(sessionId);\n if (existsSync(path)) {\n try {\n unlinkSync(path);\n } catch {\n // ignore\n }\n }\n}\n","/**\n * Session ID validation. Claude Code generates UUID-style IDs, but the\n * value is untrusted input from the hook's stdin — an attacker-controlled\n * session_id like `../../evil` would escape the user tmpdir and drop files\n * at arbitrary paths. Reject anything outside [A-Za-z0-9_-]{1,64}.\n */\nconst SAFE_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\n\nexport function isSafeSessionId(id: unknown): id is string {\n return typeof id === \"string\" && SAFE_ID_PATTERN.test(id);\n}\n\n/**\n * Defence-in-depth: throw when an unsafe ID reaches a path builder.\n * The hook's parsePayload already rejects unsafe IDs, so this is only\n * reachable via an internal caller that forgot to validate.\n */\nexport function assertSafeSessionId(id: string): void {\n if (!isSafeSessionId(id)) {\n throw new Error(`unsafe session id: ${JSON.stringify(id)}`);\n }\n}\n","import { existsSync, mkdirSync } from \"node:fs\";\nimport { tmpdir, userInfo } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Per-user tmp subdirectory for session and cache files.\n * Suffixes with the OS username (works cross-platform; `process.getuid`\n * is undefined on Windows) so a shared %TEMP% on a multi-user Windows\n * box does not leak one user's session state into another's.\n */\nexport function getUserTmpDir(): string {\n let suffix: string;\n try {\n const info = userInfo();\n suffix = typeof info.username === \"string\" && info.username ? info.username : \"user\";\n } catch {\n suffix = \"user\";\n }\n // Defensive: keep the suffix itself path-safe.\n suffix = suffix.replace(/[^A-Za-z0-9_-]/g, \"_\").slice(0, 64) || \"user\";\n const dir = join(tmpdir(), `code-explainer-${suffix}`);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n return dir;\n}\n","import type { ExplanationResult, Language, RiskLevel, DetailLevel } from \"../config/schema.js\";\n\n// ===========================================================================\n// Section header translations per language\n// ===========================================================================\n\ninterface SectionLabels {\n impact: string;\n howItWorks: string;\n why: string;\n deepDive: string;\n risk: string;\n riskNone: string;\n riskLow: string;\n riskMedium: string;\n riskHigh: string;\n samePatternFallback: string;\n}\n\nconst LABELS: Record<Language, SectionLabels> = {\n en: {\n impact: \"Impact\",\n howItWorks: \"How it works\",\n why: \"Why\",\n deepDive: \"Deeper dive\",\n risk: \"Risk\",\n riskNone: \"None\",\n riskLow: \"Low\",\n riskMedium: \"Medium\",\n riskHigh: \"High\",\n samePatternFallback: \"Same pattern as before applied to this file.\",\n },\n pt: {\n impact: \"Impacto\",\n howItWorks: \"Como funciona\",\n why: \"Por que\",\n deepDive: \"Pra aprofundar\",\n risk: \"Risco\",\n riskNone: \"Nenhum\",\n riskLow: \"Baixo\",\n riskMedium: \"M\\u00e9dio\",\n riskHigh: \"Alto\",\n samePatternFallback: \"Mesmo padr\\u00e3o anterior aplicado a este arquivo.\",\n },\n es: {\n impact: \"Impacto\",\n howItWorks: \"C\\u00f3mo funciona\",\n why: \"Por qu\\u00e9\",\n deepDive: \"Para profundizar\",\n risk: \"Riesgo\",\n riskNone: \"Ninguno\",\n riskLow: \"Bajo\",\n riskMedium: \"Medio\",\n riskHigh: \"Alto\",\n samePatternFallback: \"Mismo patr\\u00f3n anterior aplicado a este archivo.\",\n },\n fr: {\n impact: \"Impact\",\n howItWorks: \"Comment \\u00e7a marche\",\n why: \"Pourquoi\",\n deepDive: \"Pour approfondir\",\n risk: \"Risque\",\n riskNone: \"Aucun\",\n riskLow: \"Faible\",\n riskMedium: \"Moyen\",\n riskHigh: \"\\u00c9lev\\u00e9\",\n samePatternFallback: \"M\\u00eame motif que pr\\u00e9c\\u00e9demment, appliqu\\u00e9 \\u00e0 ce fichier.\",\n },\n de: {\n impact: \"Auswirkung\",\n howItWorks: \"Wie es funktioniert\",\n why: \"Warum\",\n deepDive: \"Mehr lernen\",\n risk: \"Risiko\",\n riskNone: \"Keines\",\n riskLow: \"Gering\",\n riskMedium: \"Mittel\",\n riskHigh: \"Hoch\",\n samePatternFallback: \"Gleiches Muster wie zuvor auf diese Datei angewendet.\",\n },\n it: {\n impact: \"Impatto\",\n howItWorks: \"Come funziona\",\n why: \"Perch\\u00e9\",\n deepDive: \"Per approfondire\",\n risk: \"Rischio\",\n riskNone: \"Nessuno\",\n riskLow: \"Basso\",\n riskMedium: \"Medio\",\n riskHigh: \"Alto\",\n samePatternFallback: \"Stesso schema applicato a questo file.\",\n },\n zh: {\n impact: \"\\u5f71\\u54cd\",\n howItWorks: \"\\u5982\\u4f55\\u5de5\\u4f5c\",\n why: \"\\u4e3a\\u4ec0\\u4e48\",\n deepDive: \"\\u6df1\\u5165\\u5b66\\u4e60\",\n risk: \"\\u98ce\\u9669\",\n riskNone: \"\\u65e0\",\n riskLow: \"\\u4f4e\",\n riskMedium: \"\\u4e2d\",\n riskHigh: \"\\u9ad8\",\n samePatternFallback: \"\\u540c\\u6837\\u7684\\u6a21\\u5f0f\\u5e94\\u7528\\u5230\\u6b64\\u6587\\u4ef6\\u3002\",\n },\n ja: {\n impact: \"\\u5f71\\u97ff\",\n howItWorks: \"\\u4ed5\\u7d44\\u307f\",\n why: \"\\u306a\\u305c\",\n deepDive: \"\\u3055\\u3089\\u306b\\u5b66\\u3076\",\n risk: \"\\u30ea\\u30b9\\u30af\",\n riskNone: \"\\u306a\\u3057\",\n riskLow: \"\\u4f4e\",\n riskMedium: \"\\u4e2d\",\n riskHigh: \"\\u9ad8\",\n samePatternFallback: \"\\u4ee5\\u524d\\u3068\\u540c\\u3058\\u30d1\\u30bf\\u30fc\\u30f3\\u3092\\u3053\\u306e\\u30d5\\u30a1\\u30a4\\u30eb\\u306b\\u9069\\u7528\\u3002\",\n },\n ko: {\n impact: \"\\uc601\\ud5a5\",\n howItWorks: \"\\uc791\\ub3d9 \\ubc29\\uc2dd\",\n why: \"\\uc774\\uc720\",\n deepDive: \"\\ub354 \\uc54c\\uc544\\ubcf4\\uae30\",\n risk: \"\\uc704\\ud5d8\",\n riskNone: \"\\uc5c6\\uc74c\",\n riskLow: \"\\ub0ae\\uc74c\",\n riskMedium: \"\\ubcf4\\ud1b5\",\n riskHigh: \"\\ub192\\uc74c\",\n samePatternFallback: \"\\uc774\\uc804\\uacfc \\ub3d9\\uc77c\\ud55c \\ud328\\ud134\\uc774 \\uc774 \\ud30c\\uc77c\\uc5d0 \\uc801\\uc6a9\\ub418\\uc5c8\\uc2b5\\ub2c8\\ub2e4.\",\n },\n};\n\nfunction getLabels(language: Language): SectionLabels {\n return LABELS[language] ?? LABELS.en;\n}\n\n// ===========================================================================\n// Color helpers — soft palette via truecolor (24-bit) escapes.\n// Most modern terminals (Windows Terminal, iTerm2, VS Code, gnome-terminal)\n// support truecolor. NO_COLOR and TERM=dumb still produce plain text.\n// ===========================================================================\n\nconst RESET = \"\\u001b[0m\";\nconst BOLD = \"\\u001b[1m\";\nconst DIM = \"\\u001b[2m\";\n\n// Project palette (softer than saturated ANSI)\nconst PALETTE = {\n blue: [91, 158, 245], // #5B9EF5\n green: [91, 245, 160], // #5BF5A0\n yellow: [245, 200, 91], // #F5C85B\n red: [245, 91, 91], // #F55B5B\n purple: [224, 91, 245], // #E05BF5\n white: [255, 255, 255], // #FFFFFF\n} as const;\n\ntype PaletteKey = keyof typeof PALETTE;\n\nfunction isNoColor(): boolean {\n return \"NO_COLOR\" in process.env || process.env.TERM === \"dumb\";\n}\n\nfunction rgb(name: PaletteKey, text: string): string {\n if (isNoColor()) return text;\n const [r, g, b] = PALETTE[name];\n return `\\u001b[38;2;${r};${g};${b}m${text}${RESET}`;\n}\n\nfunction bold(text: string): string {\n if (isNoColor()) return text;\n return `${BOLD}${text}${RESET}`;\n}\n\nfunction dim(text: string): string {\n if (isNoColor()) return text;\n return `${DIM}${text}${RESET}`;\n}\n\nfunction boldRgb(name: PaletteKey, text: string): string {\n if (isNoColor()) return text;\n const [r, g, b] = PALETTE[name];\n return `${BOLD}\\u001b[38;2;${r};${g};${b}m${text}${RESET}`;\n}\n\nfunction dimRgb(name: PaletteKey, text: string): string {\n if (isNoColor()) return text;\n const [r, g, b] = PALETTE[name];\n return `${DIM}\\u001b[38;2;${r};${g};${b}m${text}${RESET}`;\n}\n\nfunction getTerminalWidth(): number {\n return process.stderr.columns || 80;\n}\n\n// ===========================================================================\n// Risk presentation\n// ===========================================================================\n\nfunction riskBorderColor(risk: RiskLevel): PaletteKey {\n switch (risk) {\n case \"none\": return \"green\";\n case \"low\": return \"yellow\";\n case \"medium\": return \"yellow\";\n case \"high\": return \"red\";\n }\n}\n\nfunction riskIcon(risk: RiskLevel): string {\n if (isNoColor()) {\n switch (risk) {\n case \"none\": return \"[OK]\";\n case \"low\": return \"[!]\";\n case \"medium\": return \"[!!]\";\n case \"high\": return \"[!!!]\";\n }\n }\n switch (risk) {\n case \"none\": return rgb(\"green\", \"\\u2713\");\n case \"low\": return rgb(\"yellow\", \"\\u26a0\");\n case \"medium\": return rgb(\"yellow\", \"\\u26a0\");\n case \"high\": return rgb(\"red\", \"\\u{1F6A8}\");\n }\n}\n\nfunction riskLabelText(risk: RiskLevel, labels: SectionLabels): string {\n switch (risk) {\n case \"none\": return labels.riskNone;\n case \"low\": return labels.riskLow;\n case \"medium\": return labels.riskMedium;\n case \"high\": return labels.riskHigh;\n }\n}\n\nfunction riskLabelColor(risk: RiskLevel): PaletteKey {\n switch (risk) {\n case \"none\": return \"green\";\n case \"low\": return \"yellow\";\n case \"medium\": return \"yellow\";\n case \"high\": return \"red\";\n }\n}\n\n// ===========================================================================\n// Inline code highlighting (`backticks` -> soft blue)\n// ===========================================================================\n\nfunction highlightInlineCode(text: string): string {\n if (isNoColor()) return text;\n return text.replace(/`([^`]+)`/g, (_, code: string) => rgb(\"blue\", code));\n}\n\n// ===========================================================================\n// Word wrap that respects a content width (no ANSI awareness needed since\n// we wrap BEFORE adding color)\n// ===========================================================================\n\nfunction wrapText(text: string, maxWidth: number): string[] {\n const out: string[] = [];\n for (const raw of text.split(\"\\n\")) {\n if (raw.length <= maxWidth) {\n out.push(raw);\n continue;\n }\n let remaining = raw;\n while (remaining.length > maxWidth) {\n let breakAt = remaining.lastIndexOf(\" \", maxWidth);\n if (breakAt <= 0) breakAt = maxWidth;\n out.push(remaining.slice(0, breakAt));\n remaining = remaining.slice(breakAt).trimStart();\n }\n if (remaining) out.push(remaining);\n }\n return out;\n}\n\n// ===========================================================================\n// Box construction\n// ===========================================================================\n\nconst BOX_TITLE = \"vibe-code-explainer\";\nconst PAD_LEFT = 2;\nconst PAD_RIGHT = 2;\n\ninterface BoxLine {\n text: string; // Already styled\n raw: string; // Raw (uncolored) version, used for width calculation\n}\n\nfunction line(raw: string, styled?: string): BoxLine {\n return { text: styled ?? raw, raw };\n}\n\nfunction blankLine(): BoxLine {\n return line(\"\");\n}\n\nfunction buildBoxOutput(\n contentLines: BoxLine[],\n borderColor: PaletteKey\n): string {\n const width = Math.min(getTerminalWidth() - 2, 70);\n const innerWidth = width - 2; // chars between │ │\n\n const dashesRight = \"\\u2500\".repeat(Math.max(0, innerWidth - BOX_TITLE.length - 4));\n const top =\n rgb(borderColor, `\\u256d\\u2500 `) +\n dim(BOX_TITLE) +\n rgb(borderColor, ` ${dashesRight}\\u2500\\u256e`);\n const bottom = rgb(borderColor, `\\u2570${\"\\u2500\".repeat(innerWidth)}\\u256f`);\n\n const sideChar = rgb(borderColor, \"\\u2502\");\n\n const middle = contentLines.map((bl) => {\n const padding = \" \".repeat(Math.max(0, innerWidth - bl.raw.length - PAD_LEFT - PAD_RIGHT));\n return `${sideChar}${\" \".repeat(PAD_LEFT)}${bl.text}${padding}${\" \".repeat(PAD_RIGHT)}${sideChar}`;\n });\n\n return [top, ...middle, bottom].join(\"\\n\");\n}\n\n// ===========================================================================\n// Section rendering\n// ===========================================================================\n\ninterface SectionDef {\n header: string;\n headerColor: PaletteKey;\n body: string;\n innerWidth: number;\n dimHeader?: boolean;\n}\n\nfunction renderSection(def: SectionDef): BoxLine[] {\n const out: BoxLine[] = [];\n const headerRaw = `\\u25b8 ${def.header}`;\n const headerStyled = def.dimHeader\n ? dimRgb(def.headerColor, headerRaw)\n : boldRgb(def.headerColor, headerRaw);\n out.push(line(headerRaw, headerStyled));\n\n const bodyMax = def.innerWidth - PAD_LEFT - PAD_RIGHT - 2; // 2 = body indent\n const wrapped = wrapText(def.body, bodyMax);\n for (const w of wrapped) {\n const indented = ` ${w}`;\n const styled = ` ${highlightInlineCode(w)}`;\n out.push(line(indented, styled));\n }\n\n return out;\n}\n\n// ===========================================================================\n// Public API\n// ===========================================================================\n\nexport interface BoxInputs {\n filePath: string;\n result: ExplanationResult;\n detailLevel: DetailLevel;\n language: Language;\n}\n\nexport function formatExplanationBox(inputs: BoxInputs): string {\n const labels = getLabels(inputs.language);\n const result = inputs.result;\n const borderKey = riskBorderColor(result.risk);\n const lines: BoxLine[] = [];\n const innerWidth = Math.min(getTerminalWidth() - 2, 70) - 2;\n\n lines.push(blankLine());\n\n // File path with 📄 icon, soft blue + bold\n const filePathRaw = `\\ud83d\\udcc4 ${inputs.filePath}`;\n const filePathStyled = boldRgb(\"blue\", filePathRaw);\n lines.push(line(filePathRaw, filePathStyled));\n\n // Same-pattern collapse: short note, no teaching sections\n if (result.isSamePattern) {\n lines.push(blankLine());\n const noteRaw = result.samePatternNote || labels.samePatternFallback;\n const noteWrapped = wrapText(noteRaw, innerWidth - PAD_LEFT - PAD_RIGHT);\n for (const w of noteWrapped) {\n lines.push(line(w, dim(w)));\n }\n } else {\n // Impact (always shown when not collapsed)\n if (result.impact) {\n lines.push(blankLine());\n if (inputs.detailLevel === \"minimal\") {\n // Minimal: no header, just the text\n const wrapped = wrapText(result.impact, innerWidth - PAD_LEFT - PAD_RIGHT);\n for (const w of wrapped) {\n lines.push(line(w, highlightInlineCode(w)));\n }\n } else {\n const sec = renderSection({\n header: labels.impact,\n headerColor: \"blue\",\n body: result.impact,\n innerWidth,\n });\n lines.push(...sec);\n }\n }\n\n // How it works (standard + verbose)\n if (inputs.detailLevel !== \"minimal\" && result.howItWorks) {\n lines.push(blankLine());\n const sec = renderSection({\n header: labels.howItWorks,\n headerColor: \"green\",\n body: result.howItWorks,\n innerWidth,\n });\n lines.push(...sec);\n }\n\n // Why (standard + verbose)\n if (inputs.detailLevel !== \"minimal\" && result.why) {\n lines.push(blankLine());\n const sec = renderSection({\n header: labels.why,\n headerColor: \"purple\",\n body: result.why,\n innerWidth,\n });\n lines.push(...sec);\n }\n\n // Deep dive (verbose only) — uses white-dim header to sit quieter\n if (\n inputs.detailLevel === \"verbose\" &&\n result.deepDive &&\n result.deepDive.length > 0\n ) {\n lines.push(blankLine());\n const headerRaw = `\\u25b8 ${labels.deepDive}`;\n const headerStyled = dimRgb(\"white\", headerRaw);\n lines.push(line(headerRaw, headerStyled));\n const itemMax = innerWidth - PAD_LEFT - PAD_RIGHT - 4;\n for (const item of result.deepDive) {\n const text = `${item.term}: ${item.explanation}`;\n const wrapped = wrapText(text, itemMax);\n for (let i = 0; i < wrapped.length; i++) {\n const prefix = i === 0 ? \" \\u2014 \" : \" \";\n const raw = `${prefix}${wrapped[i]}`;\n const styled = `${prefix}${highlightInlineCode(wrapped[i])}`;\n lines.push(line(raw, styled));\n }\n }\n }\n }\n\n // Divider before risk\n lines.push(blankLine());\n const dividerWidth = innerWidth - PAD_LEFT - PAD_RIGHT;\n const dividerRaw = \"\\u2504\".repeat(dividerWidth);\n lines.push(line(dividerRaw, dim(dividerRaw)));\n lines.push(blankLine());\n\n // Risk row\n const riskKey = riskLabelColor(result.risk);\n const riskHeaderRaw = `${stripAnsi(riskIcon(result.risk))} ${labels.risk}: ${riskLabelText(result.risk, labels)}`;\n const riskHeaderStyled = `${riskIcon(result.risk)} ${boldRgb(riskKey, `${labels.risk}: ${riskLabelText(result.risk, labels)}`)}`;\n lines.push(line(riskHeaderRaw, riskHeaderStyled));\n\n if (result.risk !== \"none\" && result.riskReason) {\n const reasonMax = innerWidth - PAD_LEFT - PAD_RIGHT - 3;\n const wrapped = wrapText(result.riskReason, reasonMax);\n for (const w of wrapped) {\n const raw = ` ${w}`;\n const styled = ` ${dimRgb(riskKey, w)}`;\n lines.push(line(raw, styled));\n }\n }\n\n lines.push(blankLine());\n\n return buildBoxOutput(lines, borderKey);\n}\n\n// ===========================================================================\n// Misc box variants (skip notice, error notice, drift alert)\n// ===========================================================================\n\nexport function formatSkipNotice(reason: string): string {\n return dim(`[code-explainer] skipped: ${reason}`);\n}\n\nexport function formatErrorNotice(problem: string, cause: string, fix: string): string {\n return rgb(\"yellow\", `[code-explainer] ${problem}. ${cause}. Fix: ${fix}.`);\n}\n\nexport function formatDriftAlert(\n totalFiles: number,\n unrelatedFiles: string[],\n userRequest?: string,\n language: Language = \"en\"\n): string {\n const labels = getLabels(language);\n const lines: BoxLine[] = [];\n const innerWidth = Math.min(getTerminalWidth() - 2, 70) - 2;\n\n lines.push(blankLine());\n\n const headerRaw = `\\u26a1 SESSION DRIFT`;\n const headerStyled = boldRgb(\"yellow\", headerRaw);\n lines.push(line(headerRaw, headerStyled));\n\n lines.push(blankLine());\n\n const summaryRaw = `Claude has modified ${totalFiles} files this session.`;\n lines.push(line(summaryRaw));\n\n const unrelatedRaw = `${unrelatedFiles.length} may be unrelated:`;\n lines.push(line(unrelatedRaw));\n\n for (const file of unrelatedFiles) {\n const truncated = file.length > innerWidth - 8 ? file.slice(0, innerWidth - 11) + \"...\" : file;\n const raw = ` \\u2022 ${truncated}`;\n const styled = ` ${rgb(\"yellow\", \"\\u2022\")} ${truncated}`;\n lines.push(line(raw, styled));\n }\n\n if (userRequest) {\n lines.push(blankLine());\n const requestLines = wrapText(`Your request: \"${userRequest}\"`, innerWidth - PAD_LEFT - PAD_RIGHT);\n for (const w of requestLines) {\n lines.push(line(w, dim(w)));\n }\n }\n\n lines.push(blankLine());\n const noticeRaw = `\\u26a0 Consider reviewing these changes.`;\n lines.push(line(noticeRaw, boldRgb(\"yellow\", noticeRaw)));\n lines.push(blankLine());\n\n return buildBoxOutput(lines, \"yellow\");\n}\n\n/**\n * Write directly to the controlling terminal — Claude Code captures stdio,\n * but for non-hook contexts (init, summary, warmup) we want output on the\n * actual terminal. Falls back to stderr.\n */\nexport function printToStderr(text: string): void {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require(\"node:fs\") as typeof import(\"node:fs\");\n const ttyPath = process.platform === \"win32\" ? \"\\\\\\\\.\\\\CONOUT$\" : \"/dev/tty\";\n const fd = fs.openSync(ttyPath, \"w\");\n fs.writeSync(fd, text + \"\\n\");\n fs.closeSync(fd);\n } catch {\n process.stderr.write(text + \"\\n\");\n }\n}\n\nfunction stripAnsi(s: string): string {\n // eslint-disable-next-line no-control-regex\n return s.replace(/\\u001b\\[[0-9;]*m/g, \"\");\n}\n"],"mappings":"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","names":["existsSync","readFileSync","writeFileSync","appendFileSync","unlinkSync","join","existsSync","join","join","line","existsSync","join","appendFileSync","existsSync","readFileSync","line","writeFileSync","unlinkSync"]}
|
package/dist/cli/index.js
CHANGED
|
@@ -28,14 +28,14 @@ async function main() {
|
|
|
28
28
|
case "summary": {
|
|
29
29
|
const { flags } = parseFlags(args.slice(1));
|
|
30
30
|
const json = flagBool(flags, "json", "j");
|
|
31
|
-
const { printSummary } = await import("../tracker-
|
|
31
|
+
const { printSummary } = await import("../tracker-YZWTVUQG.js");
|
|
32
32
|
await printSummary({ json });
|
|
33
33
|
break;
|
|
34
34
|
}
|
|
35
35
|
case "session": {
|
|
36
36
|
const subcommand = args[1];
|
|
37
37
|
if (subcommand === "end") {
|
|
38
|
-
const { endSession } = await import("../tracker-
|
|
38
|
+
const { endSession } = await import("../tracker-YZWTVUQG.js");
|
|
39
39
|
await endSession();
|
|
40
40
|
} else {
|
|
41
41
|
console.error("[code-explainer] Unknown session command. Usage: code-explainer session end");
|
package/dist/hooks/post-tool.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
readSession,
|
|
22
22
|
recordEntry,
|
|
23
23
|
setCached
|
|
24
|
-
} from "../chunk-
|
|
24
|
+
} from "../chunk-JA5NWYY7.js";
|
|
25
25
|
import "../chunk-7OCVIDC7.js";
|
|
26
26
|
|
|
27
27
|
// src/hooks/post-tool.ts
|
|
@@ -707,6 +707,6 @@ ${diff}`;
|
|
|
707
707
|
safeExit();
|
|
708
708
|
}
|
|
709
709
|
main().catch(() => {
|
|
710
|
-
|
|
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":[]}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
printSummary,
|
|
8
8
|
readSession,
|
|
9
9
|
recordEntry
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JA5NWYY7.js";
|
|
11
11
|
import "./chunk-7OCVIDC7.js";
|
|
12
12
|
export {
|
|
13
13
|
cleanStaleSessionFiles,
|
|
@@ -18,4 +18,4 @@ export {
|
|
|
18
18
|
readSession,
|
|
19
19
|
recordEntry
|
|
20
20
|
};
|
|
21
|
-
//# sourceMappingURL=tracker-
|
|
21
|
+
//# sourceMappingURL=tracker-YZWTVUQG.js.map
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-code-explainer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Real-time diff explanations for vibe coders using Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"vibe-code-explainer": "
|
|
7
|
+
"vibe-code-explainer": "dist/cli/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsup",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/session/tracker.ts","../src/cache/explanation-cache.ts","../src/session/session-id.ts","../src/session/tmpdir.ts","../src/format/box.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { RiskLevel } from \"../config/schema.js\";\nimport { clearCache } from \"../cache/explanation-cache.js\";\nimport { formatDriftAlert, printToStderr } from \"../format/box.js\";\nimport { assertSafeSessionId } from \"./session-id.js\";\nimport { getUserTmpDir } from \"./tmpdir.js\";\n\nconst TWO_HOURS_MS = 2 * 60 * 60 * 1000;\n// Minimum interval between stale-file cleanup runs to avoid stat-ing the\n// tmpdir on every hook invocation (which fires for every Edit/Write/Bash).\nconst CLEANUP_THROTTLE_MS = 60 * 1000;\n\nexport interface SessionEntry {\n file: string;\n timestamp: number;\n risk: RiskLevel;\n summary: string;\n unrelated?: boolean;\n}\n\nexport function getSessionFilePath(sessionId: string): string {\n assertSafeSessionId(sessionId);\n return join(getUserTmpDir(), `session-${sessionId}.jsonl`);\n}\n\nexport function recordEntry(sessionId: string, entry: SessionEntry): void {\n const path = getSessionFilePath(sessionId);\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n } catch {\n // Non-fatal\n }\n}\n\nexport function readSession(sessionId: string): SessionEntry[] {\n const path = getSessionFilePath(sessionId);\n if (!existsSync(path)) return [];\n\n try {\n const content = readFileSync(path, \"utf-8\");\n return content\n .split(\"\\n\")\n .filter((l) => l.trim())\n .map((line) => {\n try {\n return JSON.parse(line) as SessionEntry;\n } catch {\n return null;\n }\n })\n .filter((e): e is SessionEntry => e !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * Get the last N recorded summaries for this session, oldest-first.\n * Used to feed prompt context for \"same pattern\" detection.\n *\n * Pass `entries` if you've already called readSession() to avoid a second\n * disk read within the same hook invocation.\n */\nexport function getRecentSummaries(sessionId: string, n: number, entries?: SessionEntry[]): string[] {\n const all = entries ?? readSession(sessionId);\n if (all.length === 0) return [];\n return all.slice(-n).map((e) => `${e.file}: ${e.summary}`);\n}\n\nfunction getCleanupTimestampPath(): string {\n return join(getUserTmpDir(), \".last-cleanup\");\n}\n\nexport function cleanStaleSessionFiles(): void {\n try {\n const tsPath = getCleanupTimestampPath();\n const now = Date.now();\n\n // Throttle: skip if we cleaned up recently.\n if (existsSync(tsPath)) {\n try {\n const ts = parseInt(readFileSync(tsPath, \"utf-8\").trim(), 10);\n if (!isNaN(ts) && now - ts < CLEANUP_THROTTLE_MS) return;\n } catch {\n // If the timestamp file is malformed, proceed with cleanup.\n }\n }\n\n // Record the timestamp before cleaning so concurrent invocations see it.\n try {\n writeFileSync(tsPath, String(now), { mode: 0o600 });\n } catch {\n // Non-fatal — proceed with cleanup even if we can't update the timestamp.\n }\n\n const dir = getUserTmpDir();\n const entries = readdirSync(dir);\n for (const name of entries) {\n if (!name.startsWith(\"session-\") && !name.startsWith(\"cache-\")) continue;\n const filePath = join(dir, name);\n try {\n const stat = statSync(filePath);\n if (now - stat.mtimeMs > TWO_HOURS_MS) {\n unlinkSync(filePath);\n }\n } catch {\n // ignore\n }\n }\n } catch {\n // ignore\n }\n}\n\nfunction getSessionIdFromEnv(): string | undefined {\n return process.env.CODE_EXPLAINER_SESSION_ID;\n}\n\nfunction findLatestSession(): string | undefined {\n try {\n const dir = getUserTmpDir();\n const entries = readdirSync(dir)\n .filter((n) => n.startsWith(\"session-\") && n.endsWith(\".jsonl\"))\n .map((n) => ({\n name: n,\n id: n.slice(\"session-\".length, -\".jsonl\".length),\n mtime: statSync(join(dir, n)).mtimeMs,\n }))\n .sort((a, b) => b.mtime - a.mtime);\n return entries[0]?.id;\n } catch {\n return undefined;\n }\n}\n\nexport async function printSummary({ json = false }: { json?: boolean } = {}): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n if (json) {\n process.stdout.write(JSON.stringify({ error: \"No active session found\" }) + \"\\n\");\n } else {\n process.stderr.write(\"[code-explainer] No active session found. Session data is created when Claude Code makes changes.\\n\");\n }\n return;\n }\n\n const entries = readSession(sessionId);\n if (entries.length === 0) {\n if (json) {\n process.stdout.write(JSON.stringify({ sessionId, totalChanges: 0, files: [], risks: { none: 0, low: 0, medium: 0, high: 0 } }) + \"\\n\");\n } else {\n process.stderr.write(`[code-explainer] Session '${sessionId}' has no recorded changes yet.\\n`);\n }\n return;\n }\n\n const related = entries.filter((e) => !e.unrelated);\n const unrelated = entries.filter((e) => e.unrelated);\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(new Set(unrelated.map((e) => e.file)));\n\n const risks: Record<RiskLevel, number> = { none: 0, low: 0, medium: 0, high: 0 };\n for (const e of entries) risks[e.risk]++;\n\n if (json) {\n process.stdout.write(JSON.stringify({\n sessionId,\n totalChanges: entries.length,\n filesCount: uniqueFiles.length,\n relatedChanges: related.length,\n unrelatedChanges: unrelated.length,\n unrelatedFiles,\n risks,\n entries: entries.map((e) => ({ file: e.file, risk: e.risk, summary: e.summary, unrelated: !!e.unrelated })),\n }, null, 2) + \"\\n\");\n return;\n }\n\n const alert = formatDriftAlert(uniqueFiles.length, unrelatedFiles);\n printToStderr(alert);\n\n process.stderr.write(`\\nTotal changes: ${entries.length}\\n`);\n process.stderr.write(`Files touched: ${uniqueFiles.length}\\n`);\n process.stderr.write(`Related changes: ${related.length}\\n`);\n process.stderr.write(`Unrelated/risky: ${unrelated.length}\\n`);\n\n process.stderr.write(`\\nRisk breakdown:\\n`);\n process.stderr.write(` None: ${risks.none}\\n`);\n process.stderr.write(` Low: ${risks.low}\\n`);\n process.stderr.write(` Medium: ${risks.medium}\\n`);\n process.stderr.write(` High: ${risks.high}\\n`);\n}\n\nexport async function endSession(): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n process.stderr.write(\"[code-explainer] No active session to end.\\n\");\n return;\n }\n\n const sessionPath = getSessionFilePath(sessionId);\n if (existsSync(sessionPath)) {\n try {\n unlinkSync(sessionPath);\n } catch {\n // ignore\n }\n }\n clearCache(sessionId);\n process.stderr.write(`[code-explainer] Session '${sessionId}' ended. State cleared.\\n`);\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync, appendFileSync, writeFileSync, renameSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ExplanationResult } from \"../config/schema.js\";\nimport { assertSafeSessionId } from \"../session/session-id.js\";\nimport { getUserTmpDir } from \"../session/tmpdir.js\";\n\n// Rotate the JSONL cache file when it reaches this many lines to prevent\n// unbounded growth within a single long session.\nconst CACHE_ROTATE_THRESHOLD = 500;\n// After rotation, keep the N most recent unique entries (by hash).\nconst CACHE_COMPACT_TARGET = 250;\n\nexport function getCacheFilePath(sessionId: string): string {\n assertSafeSessionId(sessionId);\n return join(getUserTmpDir(), `cache-${sessionId}.jsonl`);\n}\n\nexport function hashDiff(diff: string): string {\n return createHash(\"sha256\").update(diff, \"utf-8\").digest(\"hex\");\n}\n\ninterface CacheEntry {\n hash: string;\n result: ExplanationResult;\n}\n\n/**\n * If the cache file has grown beyond CACHE_ROTATE_THRESHOLD lines, compact\n * it: deduplicate by hash (keeping the last occurrence) and write back the\n * CACHE_COMPACT_TARGET most recent unique entries atomically via a .tmp file.\n *\n * Atomic write (writeFileSync + renameSync) prevents a crash mid-write from\n * leaving a truncated cache. Non-fatal — any error is silently swallowed.\n *\n * Note: concurrent hook invocations can both pass the size check and attempt\n * to compact the same file. The last rename wins; both will produce a valid\n * compacted file, so data integrity is preserved without a lockfile.\n */\nfunction rotateCacheIfNeeded(path: string): void {\n try {\n const content = readFileSync(path, \"utf-8\");\n const lines = content.split(\"\\n\").filter((l) => l.trim());\n if (lines.length <= CACHE_ROTATE_THRESHOLD) return;\n\n // Deduplicate by hash — later lines overwrite earlier (newest wins).\n const seen = new Map<string, CacheEntry>();\n for (const line of lines) {\n try {\n const entry = JSON.parse(line) as CacheEntry;\n seen.set(entry.hash, entry);\n } catch {\n // skip malformed lines\n }\n }\n\n // Keep the CACHE_COMPACT_TARGET most recent unique entries.\n const unique = Array.from(seen.values());\n const compacted = unique.slice(-CACHE_COMPACT_TARGET);\n\n const tmp = path + \".tmp\";\n writeFileSync(tmp, compacted.map((e) => JSON.stringify(e)).join(\"\\n\") + \"\\n\", { mode: 0o600 });\n renameSync(tmp, path);\n } catch {\n // Non-fatal — rotation failure just means the file keeps growing.\n }\n}\n\nexport function getCached(sessionId: string, diff: string): ExplanationResult | undefined {\n const path = getCacheFilePath(sessionId);\n if (!existsSync(path)) return undefined;\n\n const hash = hashDiff(diff);\n try {\n const content = readFileSync(path, \"utf-8\");\n const lines = content.split(\"\\n\").filter((l) => l.trim());\n\n // Iterate in reverse so the most recent entry wins on duplicates.\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const entry = JSON.parse(lines[i]) as CacheEntry;\n if (entry.hash === hash) {\n return entry.result;\n }\n } catch {\n // Skip malformed line\n }\n }\n } catch {\n return undefined;\n }\n return undefined;\n}\n\nexport function setCached(sessionId: string, diff: string, result: ExplanationResult): void {\n const path = getCacheFilePath(sessionId);\n const entry: CacheEntry = { hash: hashDiff(diff), result };\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n rotateCacheIfNeeded(path);\n } catch {\n // Cache write failures are non-fatal\n }\n}\n\nexport function clearCache(sessionId: string): void {\n const path = getCacheFilePath(sessionId);\n if (existsSync(path)) {\n try {\n unlinkSync(path);\n } catch {\n // ignore\n }\n }\n}\n","/**\n * Session ID validation. Claude Code generates UUID-style IDs, but the\n * value is untrusted input from the hook's stdin — an attacker-controlled\n * session_id like `../../evil` would escape the user tmpdir and drop files\n * at arbitrary paths. Reject anything outside [A-Za-z0-9_-]{1,64}.\n */\nconst SAFE_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\n\nexport function isSafeSessionId(id: unknown): id is string {\n return typeof id === \"string\" && SAFE_ID_PATTERN.test(id);\n}\n\n/**\n * Defence-in-depth: throw when an unsafe ID reaches a path builder.\n * The hook's parsePayload already rejects unsafe IDs, so this is only\n * reachable via an internal caller that forgot to validate.\n */\nexport function assertSafeSessionId(id: string): void {\n if (!isSafeSessionId(id)) {\n throw new Error(`unsafe session id: ${JSON.stringify(id)}`);\n }\n}\n","import { existsSync, mkdirSync } from \"node:fs\";\nimport { tmpdir, userInfo } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Per-user tmp subdirectory for session and cache files.\n * Suffixes with the OS username (works cross-platform; `process.getuid`\n * is undefined on Windows) so a shared %TEMP% on a multi-user Windows\n * box does not leak one user's session state into another's.\n */\nexport function getUserTmpDir(): string {\n let suffix: string;\n try {\n const info = userInfo();\n suffix = typeof info.username === \"string\" && info.username ? info.username : \"user\";\n } catch {\n suffix = \"user\";\n }\n // Defensive: keep the suffix itself path-safe.\n suffix = suffix.replace(/[^A-Za-z0-9_-]/g, \"_\").slice(0, 64) || \"user\";\n const dir = join(tmpdir(), `code-explainer-${suffix}`);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n return dir;\n}\n","import type { ExplanationResult, Language, RiskLevel, DetailLevel } from \"../config/schema.js\";\n\n// ===========================================================================\n// Section header translations per language\n// ===========================================================================\n\ninterface SectionLabels {\n impact: string;\n howItWorks: string;\n why: string;\n deepDive: string;\n risk: string;\n riskNone: string;\n riskLow: string;\n riskMedium: string;\n riskHigh: string;\n samePatternFallback: string;\n}\n\nconst LABELS: Record<Language, SectionLabels> = {\n en: {\n impact: \"Impact\",\n howItWorks: \"How it works\",\n why: \"Why\",\n deepDive: \"Deeper dive\",\n risk: \"Risk\",\n riskNone: \"None\",\n riskLow: \"Low\",\n riskMedium: \"Medium\",\n riskHigh: \"High\",\n samePatternFallback: \"Same pattern as before applied to this file.\",\n },\n pt: {\n impact: \"Impacto\",\n howItWorks: \"Como funciona\",\n why: \"Por que\",\n deepDive: \"Pra aprofundar\",\n risk: \"Risco\",\n riskNone: \"Nenhum\",\n riskLow: \"Baixo\",\n riskMedium: \"M\\u00e9dio\",\n riskHigh: \"Alto\",\n samePatternFallback: \"Mesmo padr\\u00e3o anterior aplicado a este arquivo.\",\n },\n es: {\n impact: \"Impacto\",\n howItWorks: \"C\\u00f3mo funciona\",\n why: \"Por qu\\u00e9\",\n deepDive: \"Para profundizar\",\n risk: \"Riesgo\",\n riskNone: \"Ninguno\",\n riskLow: \"Bajo\",\n riskMedium: \"Medio\",\n riskHigh: \"Alto\",\n samePatternFallback: \"Mismo patr\\u00f3n anterior aplicado a este archivo.\",\n },\n fr: {\n impact: \"Impact\",\n howItWorks: \"Comment \\u00e7a marche\",\n why: \"Pourquoi\",\n deepDive: \"Pour approfondir\",\n risk: \"Risque\",\n riskNone: \"Aucun\",\n riskLow: \"Faible\",\n riskMedium: \"Moyen\",\n riskHigh: \"\\u00c9lev\\u00e9\",\n samePatternFallback: \"M\\u00eame motif que pr\\u00e9c\\u00e9demment, appliqu\\u00e9 \\u00e0 ce fichier.\",\n },\n de: {\n impact: \"Auswirkung\",\n howItWorks: \"Wie es funktioniert\",\n why: \"Warum\",\n deepDive: \"Mehr lernen\",\n risk: \"Risiko\",\n riskNone: \"Keines\",\n riskLow: \"Gering\",\n riskMedium: \"Mittel\",\n riskHigh: \"Hoch\",\n samePatternFallback: \"Gleiches Muster wie zuvor auf diese Datei angewendet.\",\n },\n it: {\n impact: \"Impatto\",\n howItWorks: \"Come funziona\",\n why: \"Perch\\u00e9\",\n deepDive: \"Per approfondire\",\n risk: \"Rischio\",\n riskNone: \"Nessuno\",\n riskLow: \"Basso\",\n riskMedium: \"Medio\",\n riskHigh: \"Alto\",\n samePatternFallback: \"Stesso schema applicato a questo file.\",\n },\n zh: {\n impact: \"\\u5f71\\u54cd\",\n howItWorks: \"\\u5982\\u4f55\\u5de5\\u4f5c\",\n why: \"\\u4e3a\\u4ec0\\u4e48\",\n deepDive: \"\\u6df1\\u5165\\u5b66\\u4e60\",\n risk: \"\\u98ce\\u9669\",\n riskNone: \"\\u65e0\",\n riskLow: \"\\u4f4e\",\n riskMedium: \"\\u4e2d\",\n riskHigh: \"\\u9ad8\",\n samePatternFallback: \"\\u540c\\u6837\\u7684\\u6a21\\u5f0f\\u5e94\\u7528\\u5230\\u6b64\\u6587\\u4ef6\\u3002\",\n },\n ja: {\n impact: \"\\u5f71\\u97ff\",\n howItWorks: \"\\u4ed5\\u7d44\\u307f\",\n why: \"\\u306a\\u305c\",\n deepDive: \"\\u3055\\u3089\\u306b\\u5b66\\u3076\",\n risk: \"\\u30ea\\u30b9\\u30af\",\n riskNone: \"\\u306a\\u3057\",\n riskLow: \"\\u4f4e\",\n riskMedium: \"\\u4e2d\",\n riskHigh: \"\\u9ad8\",\n samePatternFallback: \"\\u4ee5\\u524d\\u3068\\u540c\\u3058\\u30d1\\u30bf\\u30fc\\u30f3\\u3092\\u3053\\u306e\\u30d5\\u30a1\\u30a4\\u30eb\\u306b\\u9069\\u7528\\u3002\",\n },\n ko: {\n impact: \"\\uc601\\ud5a5\",\n howItWorks: \"\\uc791\\ub3d9 \\ubc29\\uc2dd\",\n why: \"\\uc774\\uc720\",\n deepDive: \"\\ub354 \\uc54c\\uc544\\ubcf4\\uae30\",\n risk: \"\\uc704\\ud5d8\",\n riskNone: \"\\uc5c6\\uc74c\",\n riskLow: \"\\ub0ae\\uc74c\",\n riskMedium: \"\\ubcf4\\ud1b5\",\n riskHigh: \"\\ub192\\uc74c\",\n samePatternFallback: \"\\uc774\\uc804\\uacfc \\ub3d9\\uc77c\\ud55c \\ud328\\ud134\\uc774 \\uc774 \\ud30c\\uc77c\\uc5d0 \\uc801\\uc6a9\\ub418\\uc5c8\\uc2b5\\ub2c8\\ub2e4.\",\n },\n};\n\nfunction getLabels(language: Language): SectionLabels {\n return LABELS[language] ?? LABELS.en;\n}\n\n// ===========================================================================\n// Color helpers — soft palette via truecolor (24-bit) escapes.\n// Most modern terminals (Windows Terminal, iTerm2, VS Code, gnome-terminal)\n// support truecolor. NO_COLOR and TERM=dumb still produce plain text.\n// ===========================================================================\n\nconst RESET = \"\\u001b[0m\";\nconst BOLD = \"\\u001b[1m\";\nconst DIM = \"\\u001b[2m\";\n\n// Project palette (softer than saturated ANSI)\nconst PALETTE = {\n blue: [91, 158, 245], // #5B9EF5\n green: [91, 245, 160], // #5BF5A0\n yellow: [245, 200, 91], // #F5C85B\n red: [245, 91, 91], // #F55B5B\n purple: [224, 91, 245], // #E05BF5\n white: [255, 255, 255], // #FFFFFF\n} as const;\n\ntype PaletteKey = keyof typeof PALETTE;\n\nfunction isNoColor(): boolean {\n return \"NO_COLOR\" in process.env || process.env.TERM === \"dumb\";\n}\n\nfunction rgb(name: PaletteKey, text: string): string {\n if (isNoColor()) return text;\n const [r, g, b] = PALETTE[name];\n return `\\u001b[38;2;${r};${g};${b}m${text}${RESET}`;\n}\n\nfunction bold(text: string): string {\n if (isNoColor()) return text;\n return `${BOLD}${text}${RESET}`;\n}\n\nfunction dim(text: string): string {\n if (isNoColor()) return text;\n return `${DIM}${text}${RESET}`;\n}\n\nfunction boldRgb(name: PaletteKey, text: string): string {\n if (isNoColor()) return text;\n const [r, g, b] = PALETTE[name];\n return `${BOLD}\\u001b[38;2;${r};${g};${b}m${text}${RESET}`;\n}\n\nfunction dimRgb(name: PaletteKey, text: string): string {\n if (isNoColor()) return text;\n const [r, g, b] = PALETTE[name];\n return `${DIM}\\u001b[38;2;${r};${g};${b}m${text}${RESET}`;\n}\n\nfunction getTerminalWidth(): number {\n return process.stderr.columns || 80;\n}\n\n// ===========================================================================\n// Risk presentation\n// ===========================================================================\n\nfunction riskBorderColor(risk: RiskLevel): PaletteKey {\n switch (risk) {\n case \"none\": return \"green\";\n case \"low\": return \"yellow\";\n case \"medium\": return \"yellow\";\n case \"high\": return \"red\";\n }\n}\n\nfunction riskIcon(risk: RiskLevel): string {\n if (isNoColor()) {\n switch (risk) {\n case \"none\": return \"[OK]\";\n case \"low\": return \"[!]\";\n case \"medium\": return \"[!!]\";\n case \"high\": return \"[!!!]\";\n }\n }\n switch (risk) {\n case \"none\": return rgb(\"green\", \"\\u2713\");\n case \"low\": return rgb(\"yellow\", \"\\u26a0\");\n case \"medium\": return rgb(\"yellow\", \"\\u26a0\");\n case \"high\": return rgb(\"red\", \"\\u{1F6A8}\");\n }\n}\n\nfunction riskLabelText(risk: RiskLevel, labels: SectionLabels): string {\n switch (risk) {\n case \"none\": return labels.riskNone;\n case \"low\": return labels.riskLow;\n case \"medium\": return labels.riskMedium;\n case \"high\": return labels.riskHigh;\n }\n}\n\nfunction riskLabelColor(risk: RiskLevel): PaletteKey {\n switch (risk) {\n case \"none\": return \"green\";\n case \"low\": return \"yellow\";\n case \"medium\": return \"yellow\";\n case \"high\": return \"red\";\n }\n}\n\n// ===========================================================================\n// Inline code highlighting (`backticks` -> soft blue)\n// ===========================================================================\n\nfunction highlightInlineCode(text: string): string {\n if (isNoColor()) return text;\n return text.replace(/`([^`]+)`/g, (_, code: string) => rgb(\"blue\", code));\n}\n\n// ===========================================================================\n// Word wrap that respects a content width (no ANSI awareness needed since\n// we wrap BEFORE adding color)\n// ===========================================================================\n\nfunction wrapText(text: string, maxWidth: number): string[] {\n const out: string[] = [];\n for (const raw of text.split(\"\\n\")) {\n if (raw.length <= maxWidth) {\n out.push(raw);\n continue;\n }\n let remaining = raw;\n while (remaining.length > maxWidth) {\n let breakAt = remaining.lastIndexOf(\" \", maxWidth);\n if (breakAt <= 0) breakAt = maxWidth;\n out.push(remaining.slice(0, breakAt));\n remaining = remaining.slice(breakAt).trimStart();\n }\n if (remaining) out.push(remaining);\n }\n return out;\n}\n\n// ===========================================================================\n// Box construction\n// ===========================================================================\n\nconst BOX_TITLE = \"vibe-code-explainer\";\nconst PAD_LEFT = 2;\nconst PAD_RIGHT = 2;\n\ninterface BoxLine {\n text: string; // Already styled\n raw: string; // Raw (uncolored) version, used for width calculation\n}\n\nfunction line(raw: string, styled?: string): BoxLine {\n return { text: styled ?? raw, raw };\n}\n\nfunction blankLine(): BoxLine {\n return line(\"\");\n}\n\nfunction buildBoxOutput(\n contentLines: BoxLine[],\n borderColor: PaletteKey\n): string {\n const width = Math.min(getTerminalWidth() - 2, 70);\n const innerWidth = width - 2; // chars between │ │\n\n const top = `\\u256d\\u2500 ${dim(BOX_TITLE)} ${\"\\u2500\".repeat(Math.max(0, innerWidth - BOX_TITLE.length - 4))}\\u2500\\u256e`;\n const bottom = `\\u2570${\"\\u2500\".repeat(innerWidth)}\\u256f`;\n\n const sideChar = rgb(borderColor, \"\\u2502\");\n\n const middle = contentLines.map((bl) => {\n const padding = \" \".repeat(Math.max(0, innerWidth - bl.raw.length - PAD_LEFT - PAD_RIGHT));\n return `${sideChar}${\" \".repeat(PAD_LEFT)}${bl.text}${padding}${\" \".repeat(PAD_RIGHT)}${sideChar}`;\n });\n\n return [rgb(borderColor, top), ...middle, rgb(borderColor, bottom)].join(\"\\n\");\n}\n\n// ===========================================================================\n// Section rendering\n// ===========================================================================\n\ninterface SectionDef {\n header: string;\n headerColor: PaletteKey;\n body: string;\n innerWidth: number;\n dimHeader?: boolean;\n}\n\nfunction renderSection(def: SectionDef): BoxLine[] {\n const out: BoxLine[] = [];\n const headerRaw = `\\u25b8 ${def.header}`;\n const headerStyled = def.dimHeader\n ? dimRgb(def.headerColor, headerRaw)\n : boldRgb(def.headerColor, headerRaw);\n out.push(line(headerRaw, headerStyled));\n\n const bodyMax = def.innerWidth - PAD_LEFT - PAD_RIGHT - 2; // 2 = body indent\n const wrapped = wrapText(def.body, bodyMax);\n for (const w of wrapped) {\n const indented = ` ${w}`;\n const styled = ` ${highlightInlineCode(w)}`;\n out.push(line(indented, styled));\n }\n\n return out;\n}\n\n// ===========================================================================\n// Public API\n// ===========================================================================\n\nexport interface BoxInputs {\n filePath: string;\n result: ExplanationResult;\n detailLevel: DetailLevel;\n language: Language;\n}\n\nexport function formatExplanationBox(inputs: BoxInputs): string {\n const labels = getLabels(inputs.language);\n const result = inputs.result;\n const borderKey = riskBorderColor(result.risk);\n const lines: BoxLine[] = [];\n const innerWidth = Math.min(getTerminalWidth() - 2, 70) - 2;\n\n lines.push(blankLine());\n\n // File path with 📄 icon, soft blue + bold\n const filePathRaw = `\\ud83d\\udcc4 ${inputs.filePath}`;\n const filePathStyled = boldRgb(\"blue\", filePathRaw);\n lines.push(line(filePathRaw, filePathStyled));\n\n // Same-pattern collapse: short note, no teaching sections\n if (result.isSamePattern) {\n lines.push(blankLine());\n const noteRaw = result.samePatternNote || labels.samePatternFallback;\n const noteWrapped = wrapText(noteRaw, innerWidth - PAD_LEFT - PAD_RIGHT);\n for (const w of noteWrapped) {\n lines.push(line(w, dim(w)));\n }\n } else {\n // Impact (always shown when not collapsed)\n if (result.impact) {\n lines.push(blankLine());\n if (inputs.detailLevel === \"minimal\") {\n // Minimal: no header, just the text\n const wrapped = wrapText(result.impact, innerWidth - PAD_LEFT - PAD_RIGHT);\n for (const w of wrapped) {\n lines.push(line(w, highlightInlineCode(w)));\n }\n } else {\n const sec = renderSection({\n header: labels.impact,\n headerColor: \"blue\",\n body: result.impact,\n innerWidth,\n });\n lines.push(...sec);\n }\n }\n\n // How it works (standard + verbose)\n if (inputs.detailLevel !== \"minimal\" && result.howItWorks) {\n lines.push(blankLine());\n const sec = renderSection({\n header: labels.howItWorks,\n headerColor: \"green\",\n body: result.howItWorks,\n innerWidth,\n });\n lines.push(...sec);\n }\n\n // Why (standard + verbose)\n if (inputs.detailLevel !== \"minimal\" && result.why) {\n lines.push(blankLine());\n const sec = renderSection({\n header: labels.why,\n headerColor: \"purple\",\n body: result.why,\n innerWidth,\n });\n lines.push(...sec);\n }\n\n // Deep dive (verbose only) — uses white-dim header to sit quieter\n if (\n inputs.detailLevel === \"verbose\" &&\n result.deepDive &&\n result.deepDive.length > 0\n ) {\n lines.push(blankLine());\n const headerRaw = `\\u25b8 ${labels.deepDive}`;\n const headerStyled = dimRgb(\"white\", headerRaw);\n lines.push(line(headerRaw, headerStyled));\n const itemMax = innerWidth - PAD_LEFT - PAD_RIGHT - 4;\n for (const item of result.deepDive) {\n const text = `${item.term}: ${item.explanation}`;\n const wrapped = wrapText(text, itemMax);\n for (let i = 0; i < wrapped.length; i++) {\n const prefix = i === 0 ? \" \\u2014 \" : \" \";\n const raw = `${prefix}${wrapped[i]}`;\n const styled = `${prefix}${highlightInlineCode(wrapped[i])}`;\n lines.push(line(raw, styled));\n }\n }\n }\n }\n\n // Divider before risk\n lines.push(blankLine());\n const dividerWidth = innerWidth - PAD_LEFT - PAD_RIGHT;\n const dividerRaw = \"\\u2504\".repeat(dividerWidth);\n lines.push(line(dividerRaw, dim(dividerRaw)));\n lines.push(blankLine());\n\n // Risk row\n const riskKey = riskLabelColor(result.risk);\n const riskHeaderRaw = `${stripAnsi(riskIcon(result.risk))} ${labels.risk}: ${riskLabelText(result.risk, labels)}`;\n const riskHeaderStyled = `${riskIcon(result.risk)} ${boldRgb(riskKey, `${labels.risk}: ${riskLabelText(result.risk, labels)}`)}`;\n lines.push(line(riskHeaderRaw, riskHeaderStyled));\n\n if (result.risk !== \"none\" && result.riskReason) {\n const reasonMax = innerWidth - PAD_LEFT - PAD_RIGHT - 3;\n const wrapped = wrapText(result.riskReason, reasonMax);\n for (const w of wrapped) {\n const raw = ` ${w}`;\n const styled = ` ${dimRgb(riskKey, w)}`;\n lines.push(line(raw, styled));\n }\n }\n\n lines.push(blankLine());\n\n return buildBoxOutput(lines, borderKey);\n}\n\n// ===========================================================================\n// Misc box variants (skip notice, error notice, drift alert)\n// ===========================================================================\n\nexport function formatSkipNotice(reason: string): string {\n return dim(`[code-explainer] skipped: ${reason}`);\n}\n\nexport function formatErrorNotice(problem: string, cause: string, fix: string): string {\n return rgb(\"yellow\", `[code-explainer] ${problem}. ${cause}. Fix: ${fix}.`);\n}\n\nexport function formatDriftAlert(\n totalFiles: number,\n unrelatedFiles: string[],\n userRequest?: string,\n language: Language = \"en\"\n): string {\n const labels = getLabels(language);\n const lines: BoxLine[] = [];\n const innerWidth = Math.min(getTerminalWidth() - 2, 70) - 2;\n\n lines.push(blankLine());\n\n const headerRaw = `\\u26a1 SESSION DRIFT`;\n const headerStyled = boldRgb(\"yellow\", headerRaw);\n lines.push(line(headerRaw, headerStyled));\n\n lines.push(blankLine());\n\n const summaryRaw = `Claude has modified ${totalFiles} files this session.`;\n lines.push(line(summaryRaw));\n\n const unrelatedRaw = `${unrelatedFiles.length} may be unrelated:`;\n lines.push(line(unrelatedRaw));\n\n for (const file of unrelatedFiles) {\n const truncated = file.length > innerWidth - 8 ? file.slice(0, innerWidth - 11) + \"...\" : file;\n const raw = ` \\u2022 ${truncated}`;\n const styled = ` ${rgb(\"yellow\", \"\\u2022\")} ${truncated}`;\n lines.push(line(raw, styled));\n }\n\n if (userRequest) {\n lines.push(blankLine());\n const requestLines = wrapText(`Your request: \"${userRequest}\"`, innerWidth - PAD_LEFT - PAD_RIGHT);\n for (const w of requestLines) {\n lines.push(line(w, dim(w)));\n }\n }\n\n lines.push(blankLine());\n const noticeRaw = `\\u26a0 Consider reviewing these changes.`;\n lines.push(line(noticeRaw, boldRgb(\"yellow\", noticeRaw)));\n lines.push(blankLine());\n\n return buildBoxOutput(lines, \"yellow\");\n}\n\n/**\n * Write directly to the controlling terminal — Claude Code captures stdio,\n * but for non-hook contexts (init, summary, warmup) we want output on the\n * actual terminal. Falls back to stderr.\n */\nexport function printToStderr(text: string): void {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require(\"node:fs\") as typeof import(\"node:fs\");\n const ttyPath = process.platform === \"win32\" ? \"\\\\\\\\.\\\\CONOUT$\" : \"/dev/tty\";\n const fd = fs.openSync(ttyPath, \"w\");\n fs.writeSync(fd, text + \"\\n\");\n fs.closeSync(fd);\n } catch {\n process.stderr.write(text + \"\\n\");\n }\n}\n\nfunction stripAnsi(s: string): string {\n // eslint-disable-next-line no-control-regex\n return s.replace(/\\u001b\\[[0-9;]*m/g, \"\");\n}\n"],"mappings":"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","names":["existsSync","readFileSync","writeFileSync","appendFileSync","unlinkSync","join","existsSync","join","join","line","existsSync","join","appendFileSync","existsSync","readFileSync","line","writeFileSync","unlinkSync"]}
|
|
File without changes
|