vibe-code-explainer 0.1.2 → 0.1.4
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-DPYDNBM3.js → chunk-W67RX53R.js} +13 -3
- package/dist/{chunk-DPYDNBM3.js.map → chunk-W67RX53R.js.map} +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/hooks/post-tool.js +17 -9
- package/dist/hooks/post-tool.js.map +1 -1
- package/dist/{tracker-E5FBZ64E.js → tracker-HCWPUZIO.js} +2 -2
- package/package.json +1 -1
- /package/dist/{tracker-E5FBZ64E.js.map → tracker-HCWPUZIO.js.map} +0 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
__require
|
|
4
|
+
} from "./chunk-7OCVIDC7.js";
|
|
2
5
|
|
|
3
6
|
// src/session/tracker.ts
|
|
4
7
|
import { existsSync as existsSync2, readFileSync as readFileSync2, appendFileSync as appendFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync2, readdirSync, statSync } from "fs";
|
|
@@ -187,7 +190,15 @@ function formatErrorNotice(problem, cause, fix) {
|
|
|
187
190
|
return color(pc.yellow, `[code-explainer] ${problem}. ${cause}. Fix: ${fix}.`);
|
|
188
191
|
}
|
|
189
192
|
function printToStderr(text) {
|
|
190
|
-
|
|
193
|
+
try {
|
|
194
|
+
const fs = __require("fs");
|
|
195
|
+
const ttyPath = process.platform === "win32" ? "\\\\.\\CONOUT$" : "/dev/tty";
|
|
196
|
+
const fd = fs.openSync(ttyPath, "w");
|
|
197
|
+
fs.writeSync(fd, text + "\n");
|
|
198
|
+
fs.closeSync(fd);
|
|
199
|
+
} catch {
|
|
200
|
+
process.stderr.write(text + "\n");
|
|
201
|
+
}
|
|
191
202
|
}
|
|
192
203
|
|
|
193
204
|
// src/session/tracker.ts
|
|
@@ -326,7 +337,6 @@ export {
|
|
|
326
337
|
formatDriftAlert,
|
|
327
338
|
formatSkipNotice,
|
|
328
339
|
formatErrorNotice,
|
|
329
|
-
printToStderr,
|
|
330
340
|
getSessionFilePath,
|
|
331
341
|
recordEntry,
|
|
332
342
|
readSession,
|
|
@@ -334,4 +344,4 @@ export {
|
|
|
334
344
|
printSummary,
|
|
335
345
|
endSession
|
|
336
346
|
};
|
|
337
|
-
//# sourceMappingURL=chunk-
|
|
347
|
+
//# sourceMappingURL=chunk-W67RX53R.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/session/tracker.ts","../src/cache/explanation-cache.ts","../src/format/box.ts"],"sourcesContent":["import { existsSync, readFileSync, appendFileSync, unlinkSync, mkdirSync, readdirSync, statSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { RiskLevel } from \"../config/schema.js\";\nimport { clearCache } from \"../cache/explanation-cache.js\";\nimport { formatDriftAlert, printToStderr } from \"../format/box.js\";\n\nconst TWO_HOURS_MS = 2 * 60 * 60 * 1000;\n\nexport interface SessionEntry {\n file: string;\n timestamp: number;\n risk: RiskLevel;\n summary: string;\n unrelated?: boolean;\n}\n\nfunction getUserTmpDir(): string {\n const dir = join(tmpdir(), `code-explainer-${process.getuid?.() ?? \"user\"}`);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n return dir;\n}\n\nexport function getSessionFilePath(sessionId: string): string {\n return join(getUserTmpDir(), `session-${sessionId}.jsonl`);\n}\n\nexport function recordEntry(sessionId: string, entry: SessionEntry): void {\n const path = getSessionFilePath(sessionId);\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n } catch {\n // Non-fatal\n }\n}\n\nexport function readSession(sessionId: string): SessionEntry[] {\n const path = getSessionFilePath(sessionId);\n if (!existsSync(path)) return [];\n\n try {\n const content = readFileSync(path, \"utf-8\");\n return content\n .split(\"\\n\")\n .filter((l) => l.trim())\n .map((line) => {\n try {\n return JSON.parse(line) as SessionEntry;\n } catch {\n return null;\n }\n })\n .filter((e): e is SessionEntry => e !== null);\n } catch {\n return [];\n }\n}\n\nexport function cleanStaleSessionFiles(): void {\n try {\n const dir = getUserTmpDir();\n const now = Date.now();\n const entries = readdirSync(dir);\n for (const name of entries) {\n if (!name.startsWith(\"session-\") && !name.startsWith(\"cache-\")) continue;\n const filePath = join(dir, name);\n try {\n const stat = statSync(filePath);\n if (now - stat.mtimeMs > TWO_HOURS_MS) {\n unlinkSync(filePath);\n }\n } catch {\n // ignore\n }\n }\n } catch {\n // ignore\n }\n}\n\nfunction getSessionIdFromEnv(): string | undefined {\n return process.env.CODE_EXPLAINER_SESSION_ID;\n}\n\nfunction findLatestSession(): string | undefined {\n try {\n const dir = getUserTmpDir();\n const entries = readdirSync(dir)\n .filter((n) => n.startsWith(\"session-\") && n.endsWith(\".jsonl\"))\n .map((n) => ({\n name: n,\n id: n.slice(\"session-\".length, -\".jsonl\".length),\n mtime: statSync(join(dir, n)).mtimeMs,\n }))\n .sort((a, b) => b.mtime - a.mtime);\n return entries[0]?.id;\n } catch {\n return undefined;\n }\n}\n\nexport async function printSummary(): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n process.stderr.write(\"[code-explainer] No active session found. Session data is created when Claude Code makes changes.\\n\");\n return;\n }\n\n const entries = readSession(sessionId);\n if (entries.length === 0) {\n process.stderr.write(`[code-explainer] Session '${sessionId}' has no recorded changes yet.\\n`);\n return;\n }\n\n const related = entries.filter((e) => !e.unrelated);\n const unrelated = entries.filter((e) => e.unrelated);\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(new Set(unrelated.map((e) => e.file)));\n\n const alert = formatDriftAlert(uniqueFiles.length, unrelatedFiles);\n printToStderr(alert);\n\n process.stderr.write(`\\nTotal changes: ${entries.length}\\n`);\n process.stderr.write(`Files touched: ${uniqueFiles.length}\\n`);\n process.stderr.write(`Related changes: ${related.length}\\n`);\n process.stderr.write(`Unrelated/risky: ${unrelated.length}\\n`);\n\n const risks: Record<RiskLevel, number> = { none: 0, low: 0, medium: 0, high: 0 };\n for (const e of entries) risks[e.risk]++;\n process.stderr.write(`\\nRisk breakdown:\\n`);\n process.stderr.write(` None: ${risks.none}\\n`);\n process.stderr.write(` Low: ${risks.low}\\n`);\n process.stderr.write(` Medium: ${risks.medium}\\n`);\n process.stderr.write(` High: ${risks.high}\\n`);\n}\n\nexport async function endSession(): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n process.stderr.write(\"[code-explainer] No active session to end.\\n\");\n return;\n }\n\n const sessionPath = getSessionFilePath(sessionId);\n if (existsSync(sessionPath)) {\n try {\n unlinkSync(sessionPath);\n } catch {\n // ignore\n }\n }\n clearCache(sessionId);\n process.stderr.write(`[code-explainer] Session '${sessionId}' ended. State cleared.\\n`);\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync, appendFileSync, unlinkSync, mkdirSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { ExplanationResult } from \"../config/schema.js\";\n\nfunction getUserTmpDir(): string {\n const dir = join(tmpdir(), `code-explainer-${process.getuid?.() ?? \"user\"}`);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n return dir;\n}\n\nexport function getCacheFilePath(sessionId: string): string {\n return join(getUserTmpDir(), `cache-${sessionId}.jsonl`);\n}\n\nexport function hashDiff(diff: string): string {\n return createHash(\"sha256\").update(diff, \"utf-8\").digest(\"hex\");\n}\n\ninterface CacheEntry {\n hash: string;\n result: ExplanationResult;\n}\n\nexport function getCached(sessionId: string, diff: string): ExplanationResult | undefined {\n const path = getCacheFilePath(sessionId);\n if (!existsSync(path)) return undefined;\n\n const hash = hashDiff(diff);\n try {\n const content = readFileSync(path, \"utf-8\");\n const lines = content.split(\"\\n\").filter((l) => l.trim());\n\n // Iterate in reverse so the most recent entry wins on duplicates.\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const entry = JSON.parse(lines[i]) as CacheEntry;\n if (entry.hash === hash) {\n return entry.result;\n }\n } catch {\n // Skip malformed line\n }\n }\n } catch {\n return undefined;\n }\n return undefined;\n}\n\nexport function setCached(sessionId: string, diff: string, result: ExplanationResult): void {\n const path = getCacheFilePath(sessionId);\n const entry: CacheEntry = { hash: hashDiff(diff), result };\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n } catch {\n // Cache write failures are non-fatal\n }\n}\n\nexport function clearCache(sessionId: string): void {\n const path = getCacheFilePath(sessionId);\n if (existsSync(path)) {\n try {\n unlinkSync(path);\n } catch {\n // ignore\n }\n }\n}\n","import pc from \"picocolors\";\nimport type { RiskLevel } from \"../config/schema.js\";\n\nfunction isNoColor(): boolean {\n return \"NO_COLOR\" in process.env || process.env.TERM === \"dumb\";\n}\n\nfunction color(fn: (s: string) => string, text: string): string {\n return isNoColor() ? text : fn(text);\n}\n\nfunction getTerminalWidth(): number {\n return process.stderr.columns || 80;\n}\n\nfunction wrapText(text: string, maxWidth: number): string[] {\n const lines: string[] = [];\n for (const raw of text.split(\"\\n\")) {\n if (raw.length <= maxWidth) {\n lines.push(raw);\n } else {\n let remaining = raw;\n while (remaining.length > maxWidth) {\n let breakAt = remaining.lastIndexOf(\" \", maxWidth);\n if (breakAt <= 0) breakAt = maxWidth;\n lines.push(remaining.slice(0, breakAt));\n remaining = remaining.slice(breakAt).trimStart();\n }\n if (remaining) lines.push(remaining);\n }\n }\n return lines;\n}\n\nfunction riskIcon(risk: RiskLevel): string {\n if (isNoColor()) {\n switch (risk) {\n case \"none\": return \"[OK]\";\n case \"low\": return \"[LOW]\";\n case \"medium\": return \"[WARN]\";\n case \"high\": return \"[DANGER]\";\n }\n }\n switch (risk) {\n case \"none\": return color(pc.green, \"\\u2705\");\n case \"low\": return color(pc.yellow, \"\\u26A0\\uFE0F\");\n case \"medium\": return color(pc.yellow, \"\\u26A0\\uFE0F\");\n case \"high\": return color(pc.red, \"\\u{1F6A8}\");\n }\n}\n\nfunction riskLabel(risk: RiskLevel): string {\n switch (risk) {\n case \"none\": return \"None\";\n case \"low\": return \"Low\";\n case \"medium\": return \"Medium\";\n case \"high\": return \"High\";\n }\n}\n\nexport function formatExplanationBox(\n filePath: string,\n summary: string,\n risk: RiskLevel,\n riskReason: string\n): string {\n const width = Math.min(getTerminalWidth() - 4, 60);\n const contentWidth = width - 4; // \"│ \" prefix + \" \" suffix\n\n const isRisky = risk === \"medium\" || risk === \"high\";\n const headerLabel = isRisky ? \"code-explainer \\u26A0\\uFE0F\" : \"code-explainer\";\n const topBorder = `\\u250C\\u2500 ${headerLabel} ${ \"\\u2500\".repeat(Math.max(0, width - headerLabel.length - 4))}\\u2500`;\n const bottomBorder = `\\u2514${ \"\\u2500\".repeat(width)}\\u2500`;\n\n const lines: string[] = [];\n lines.push(isRisky ? color(pc.yellow, topBorder) : color(pc.dim, topBorder));\n\n // File path\n const fileDisplay = isRisky ? `${riskIcon(risk)} ${filePath}` : ` ${filePath}`;\n lines.push(`${color(pc.dim, \"\\u2502\")}${fileDisplay}`);\n lines.push(`${color(pc.dim, \"\\u2502\")}`);\n\n // Summary\n const summaryLines = wrapText(summary, contentWidth);\n for (const line of summaryLines) {\n lines.push(`${color(pc.dim, \"\\u2502\")} ${line}`);\n }\n\n lines.push(`${color(pc.dim, \"\\u2502\")}`);\n\n // Risk line\n const riskText = `Risk: ${riskIcon(risk)} ${riskLabel(risk)}`;\n lines.push(`${color(pc.dim, \"\\u2502\")} ${riskText}`);\n\n // Risk reason (if any)\n if (riskReason) {\n const reasonLines = wrapText(riskReason, contentWidth);\n for (const line of reasonLines) {\n lines.push(`${color(pc.dim, \"\\u2502\")} ${color(pc.dim, line)}`);\n }\n }\n\n lines.push(isRisky ? color(pc.yellow, bottomBorder) : color(pc.dim, bottomBorder));\n\n return lines.join(\"\\n\");\n}\n\nexport function formatDriftAlert(\n totalFiles: number,\n unrelatedFiles: string[],\n userRequest?: string\n): string {\n const width = Math.min(getTerminalWidth() - 4, 60);\n const contentWidth = width - 4;\n\n const headerLabel = \"code-explainer \\u26A1 SESSION DRIFT\";\n const topBorder = `\\u250C\\u2500 ${headerLabel} ${ \"\\u2500\".repeat(Math.max(0, width - headerLabel.length - 4))}\\u2500`;\n const bottomBorder = `\\u2514${ \"\\u2500\".repeat(width)}\\u2500`;\n\n const lines: string[] = [];\n lines.push(color(pc.yellow, topBorder));\n\n lines.push(`${color(pc.dim, \"\\u2502\")} Claude has modified ${totalFiles} files this session.`);\n lines.push(`${color(pc.dim, \"\\u2502\")} ${unrelatedFiles.length} may be unrelated:`);\n\n for (const file of unrelatedFiles) {\n const truncated = file.length > contentWidth - 4 ? file.slice(0, contentWidth - 7) + \"...\" : file;\n lines.push(`${color(pc.dim, \"\\u2502\")} ${color(pc.yellow, \"\\u2022\")} ${truncated}`);\n }\n\n if (userRequest) {\n lines.push(`${color(pc.dim, \"\\u2502\")}`);\n const requestLines = wrapText(`Your request: \"${userRequest}\"`, contentWidth);\n for (const line of requestLines) {\n lines.push(`${color(pc.dim, \"\\u2502\")} ${line}`);\n }\n }\n\n lines.push(`${color(pc.dim, \"\\u2502\")} ${color(pc.yellow, \"\\u26A0\\uFE0F Consider reviewing these changes.\")}`);\n lines.push(color(pc.yellow, bottomBorder));\n\n return lines.join(\"\\n\");\n}\n\nexport function formatSkipNotice(reason: string): string {\n return color(pc.dim, `[code-explainer] skipped: ${reason}`);\n}\n\nexport function formatErrorNotice(problem: string, cause: string, fix: string): string {\n return color(pc.yellow, `[code-explainer] ${problem}. ${cause}. Fix: ${fix}.`);\n}\n\nexport function printToStderr(text: string): void {\n process.stderr.write(text + \"\\n\");\n}\n"],"mappings":"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","names":["existsSync","readFileSync","appendFileSync","unlinkSync","mkdirSync","tmpdir","join","getUserTmpDir","join","tmpdir","existsSync","mkdirSync","appendFileSync","readFileSync","unlinkSync"]}
|
|
1
|
+
{"version":3,"sources":["../src/session/tracker.ts","../src/cache/explanation-cache.ts","../src/format/box.ts"],"sourcesContent":["import { existsSync, readFileSync, appendFileSync, unlinkSync, mkdirSync, readdirSync, statSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { RiskLevel } from \"../config/schema.js\";\nimport { clearCache } from \"../cache/explanation-cache.js\";\nimport { formatDriftAlert, printToStderr } from \"../format/box.js\";\n\nconst TWO_HOURS_MS = 2 * 60 * 60 * 1000;\n\nexport interface SessionEntry {\n file: string;\n timestamp: number;\n risk: RiskLevel;\n summary: string;\n unrelated?: boolean;\n}\n\nfunction getUserTmpDir(): string {\n const dir = join(tmpdir(), `code-explainer-${process.getuid?.() ?? \"user\"}`);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n return dir;\n}\n\nexport function getSessionFilePath(sessionId: string): string {\n return join(getUserTmpDir(), `session-${sessionId}.jsonl`);\n}\n\nexport function recordEntry(sessionId: string, entry: SessionEntry): void {\n const path = getSessionFilePath(sessionId);\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n } catch {\n // Non-fatal\n }\n}\n\nexport function readSession(sessionId: string): SessionEntry[] {\n const path = getSessionFilePath(sessionId);\n if (!existsSync(path)) return [];\n\n try {\n const content = readFileSync(path, \"utf-8\");\n return content\n .split(\"\\n\")\n .filter((l) => l.trim())\n .map((line) => {\n try {\n return JSON.parse(line) as SessionEntry;\n } catch {\n return null;\n }\n })\n .filter((e): e is SessionEntry => e !== null);\n } catch {\n return [];\n }\n}\n\nexport function cleanStaleSessionFiles(): void {\n try {\n const dir = getUserTmpDir();\n const now = Date.now();\n const entries = readdirSync(dir);\n for (const name of entries) {\n if (!name.startsWith(\"session-\") && !name.startsWith(\"cache-\")) continue;\n const filePath = join(dir, name);\n try {\n const stat = statSync(filePath);\n if (now - stat.mtimeMs > TWO_HOURS_MS) {\n unlinkSync(filePath);\n }\n } catch {\n // ignore\n }\n }\n } catch {\n // ignore\n }\n}\n\nfunction getSessionIdFromEnv(): string | undefined {\n return process.env.CODE_EXPLAINER_SESSION_ID;\n}\n\nfunction findLatestSession(): string | undefined {\n try {\n const dir = getUserTmpDir();\n const entries = readdirSync(dir)\n .filter((n) => n.startsWith(\"session-\") && n.endsWith(\".jsonl\"))\n .map((n) => ({\n name: n,\n id: n.slice(\"session-\".length, -\".jsonl\".length),\n mtime: statSync(join(dir, n)).mtimeMs,\n }))\n .sort((a, b) => b.mtime - a.mtime);\n return entries[0]?.id;\n } catch {\n return undefined;\n }\n}\n\nexport async function printSummary(): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n process.stderr.write(\"[code-explainer] No active session found. Session data is created when Claude Code makes changes.\\n\");\n return;\n }\n\n const entries = readSession(sessionId);\n if (entries.length === 0) {\n process.stderr.write(`[code-explainer] Session '${sessionId}' has no recorded changes yet.\\n`);\n return;\n }\n\n const related = entries.filter((e) => !e.unrelated);\n const unrelated = entries.filter((e) => e.unrelated);\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(new Set(unrelated.map((e) => e.file)));\n\n const alert = formatDriftAlert(uniqueFiles.length, unrelatedFiles);\n printToStderr(alert);\n\n process.stderr.write(`\\nTotal changes: ${entries.length}\\n`);\n process.stderr.write(`Files touched: ${uniqueFiles.length}\\n`);\n process.stderr.write(`Related changes: ${related.length}\\n`);\n process.stderr.write(`Unrelated/risky: ${unrelated.length}\\n`);\n\n const risks: Record<RiskLevel, number> = { none: 0, low: 0, medium: 0, high: 0 };\n for (const e of entries) risks[e.risk]++;\n process.stderr.write(`\\nRisk breakdown:\\n`);\n process.stderr.write(` None: ${risks.none}\\n`);\n process.stderr.write(` Low: ${risks.low}\\n`);\n process.stderr.write(` Medium: ${risks.medium}\\n`);\n process.stderr.write(` High: ${risks.high}\\n`);\n}\n\nexport async function endSession(): Promise<void> {\n const sessionId = getSessionIdFromEnv() ?? findLatestSession();\n if (!sessionId) {\n process.stderr.write(\"[code-explainer] No active session to end.\\n\");\n return;\n }\n\n const sessionPath = getSessionFilePath(sessionId);\n if (existsSync(sessionPath)) {\n try {\n unlinkSync(sessionPath);\n } catch {\n // ignore\n }\n }\n clearCache(sessionId);\n process.stderr.write(`[code-explainer] Session '${sessionId}' ended. State cleared.\\n`);\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync, appendFileSync, unlinkSync, mkdirSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { ExplanationResult } from \"../config/schema.js\";\n\nfunction getUserTmpDir(): string {\n const dir = join(tmpdir(), `code-explainer-${process.getuid?.() ?? \"user\"}`);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n return dir;\n}\n\nexport function getCacheFilePath(sessionId: string): string {\n return join(getUserTmpDir(), `cache-${sessionId}.jsonl`);\n}\n\nexport function hashDiff(diff: string): string {\n return createHash(\"sha256\").update(diff, \"utf-8\").digest(\"hex\");\n}\n\ninterface CacheEntry {\n hash: string;\n result: ExplanationResult;\n}\n\nexport function getCached(sessionId: string, diff: string): ExplanationResult | undefined {\n const path = getCacheFilePath(sessionId);\n if (!existsSync(path)) return undefined;\n\n const hash = hashDiff(diff);\n try {\n const content = readFileSync(path, \"utf-8\");\n const lines = content.split(\"\\n\").filter((l) => l.trim());\n\n // Iterate in reverse so the most recent entry wins on duplicates.\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const entry = JSON.parse(lines[i]) as CacheEntry;\n if (entry.hash === hash) {\n return entry.result;\n }\n } catch {\n // Skip malformed line\n }\n }\n } catch {\n return undefined;\n }\n return undefined;\n}\n\nexport function setCached(sessionId: string, diff: string, result: ExplanationResult): void {\n const path = getCacheFilePath(sessionId);\n const entry: CacheEntry = { hash: hashDiff(diff), result };\n try {\n appendFileSync(path, JSON.stringify(entry) + \"\\n\", { mode: 0o600 });\n } catch {\n // Cache write failures are non-fatal\n }\n}\n\nexport function clearCache(sessionId: string): void {\n const path = getCacheFilePath(sessionId);\n if (existsSync(path)) {\n try {\n unlinkSync(path);\n } catch {\n // ignore\n }\n }\n}\n","import pc from \"picocolors\";\nimport type { RiskLevel } from \"../config/schema.js\";\n\nfunction isNoColor(): boolean {\n return \"NO_COLOR\" in process.env || process.env.TERM === \"dumb\";\n}\n\nfunction color(fn: (s: string) => string, text: string): string {\n return isNoColor() ? text : fn(text);\n}\n\nfunction getTerminalWidth(): number {\n return process.stderr.columns || 80;\n}\n\nfunction wrapText(text: string, maxWidth: number): string[] {\n const lines: string[] = [];\n for (const raw of text.split(\"\\n\")) {\n if (raw.length <= maxWidth) {\n lines.push(raw);\n } else {\n let remaining = raw;\n while (remaining.length > maxWidth) {\n let breakAt = remaining.lastIndexOf(\" \", maxWidth);\n if (breakAt <= 0) breakAt = maxWidth;\n lines.push(remaining.slice(0, breakAt));\n remaining = remaining.slice(breakAt).trimStart();\n }\n if (remaining) lines.push(remaining);\n }\n }\n return lines;\n}\n\nfunction riskIcon(risk: RiskLevel): string {\n if (isNoColor()) {\n switch (risk) {\n case \"none\": return \"[OK]\";\n case \"low\": return \"[LOW]\";\n case \"medium\": return \"[WARN]\";\n case \"high\": return \"[DANGER]\";\n }\n }\n switch (risk) {\n case \"none\": return color(pc.green, \"\\u2705\");\n case \"low\": return color(pc.yellow, \"\\u26A0\\uFE0F\");\n case \"medium\": return color(pc.yellow, \"\\u26A0\\uFE0F\");\n case \"high\": return color(pc.red, \"\\u{1F6A8}\");\n }\n}\n\nfunction riskLabel(risk: RiskLevel): string {\n switch (risk) {\n case \"none\": return \"None\";\n case \"low\": return \"Low\";\n case \"medium\": return \"Medium\";\n case \"high\": return \"High\";\n }\n}\n\nexport function formatExplanationBox(\n filePath: string,\n summary: string,\n risk: RiskLevel,\n riskReason: string\n): string {\n const width = Math.min(getTerminalWidth() - 4, 60);\n const contentWidth = width - 4; // \"│ \" prefix + \" \" suffix\n\n const isRisky = risk === \"medium\" || risk === \"high\";\n const headerLabel = isRisky ? \"code-explainer \\u26A0\\uFE0F\" : \"code-explainer\";\n const topBorder = `\\u250C\\u2500 ${headerLabel} ${ \"\\u2500\".repeat(Math.max(0, width - headerLabel.length - 4))}\\u2500`;\n const bottomBorder = `\\u2514${ \"\\u2500\".repeat(width)}\\u2500`;\n\n const lines: string[] = [];\n lines.push(isRisky ? color(pc.yellow, topBorder) : color(pc.dim, topBorder));\n\n // File path\n const fileDisplay = isRisky ? `${riskIcon(risk)} ${filePath}` : ` ${filePath}`;\n lines.push(`${color(pc.dim, \"\\u2502\")}${fileDisplay}`);\n lines.push(`${color(pc.dim, \"\\u2502\")}`);\n\n // Summary\n const summaryLines = wrapText(summary, contentWidth);\n for (const line of summaryLines) {\n lines.push(`${color(pc.dim, \"\\u2502\")} ${line}`);\n }\n\n lines.push(`${color(pc.dim, \"\\u2502\")}`);\n\n // Risk line\n const riskText = `Risk: ${riskIcon(risk)} ${riskLabel(risk)}`;\n lines.push(`${color(pc.dim, \"\\u2502\")} ${riskText}`);\n\n // Risk reason (if any)\n if (riskReason) {\n const reasonLines = wrapText(riskReason, contentWidth);\n for (const line of reasonLines) {\n lines.push(`${color(pc.dim, \"\\u2502\")} ${color(pc.dim, line)}`);\n }\n }\n\n lines.push(isRisky ? color(pc.yellow, bottomBorder) : color(pc.dim, bottomBorder));\n\n return lines.join(\"\\n\");\n}\n\nexport function formatDriftAlert(\n totalFiles: number,\n unrelatedFiles: string[],\n userRequest?: string\n): string {\n const width = Math.min(getTerminalWidth() - 4, 60);\n const contentWidth = width - 4;\n\n const headerLabel = \"code-explainer \\u26A1 SESSION DRIFT\";\n const topBorder = `\\u250C\\u2500 ${headerLabel} ${ \"\\u2500\".repeat(Math.max(0, width - headerLabel.length - 4))}\\u2500`;\n const bottomBorder = `\\u2514${ \"\\u2500\".repeat(width)}\\u2500`;\n\n const lines: string[] = [];\n lines.push(color(pc.yellow, topBorder));\n\n lines.push(`${color(pc.dim, \"\\u2502\")} Claude has modified ${totalFiles} files this session.`);\n lines.push(`${color(pc.dim, \"\\u2502\")} ${unrelatedFiles.length} may be unrelated:`);\n\n for (const file of unrelatedFiles) {\n const truncated = file.length > contentWidth - 4 ? file.slice(0, contentWidth - 7) + \"...\" : file;\n lines.push(`${color(pc.dim, \"\\u2502\")} ${color(pc.yellow, \"\\u2022\")} ${truncated}`);\n }\n\n if (userRequest) {\n lines.push(`${color(pc.dim, \"\\u2502\")}`);\n const requestLines = wrapText(`Your request: \"${userRequest}\"`, contentWidth);\n for (const line of requestLines) {\n lines.push(`${color(pc.dim, \"\\u2502\")} ${line}`);\n }\n }\n\n lines.push(`${color(pc.dim, \"\\u2502\")} ${color(pc.yellow, \"\\u26A0\\uFE0F Consider reviewing these changes.\")}`);\n lines.push(color(pc.yellow, bottomBorder));\n\n return lines.join(\"\\n\");\n}\n\nexport function formatSkipNotice(reason: string): string {\n return color(pc.dim, `[code-explainer] skipped: ${reason}`);\n}\n\nexport function formatErrorNotice(problem: string, cause: string, fix: string): string {\n return color(pc.yellow, `[code-explainer] ${problem}. ${cause}. Fix: ${fix}.`);\n}\n\n/**\n * Write directly to the controlling terminal. Claude Code captures stdout\n * and stderr from hooks, so they never appear in the user's terminal unless\n * we bypass stdio and write to the tty device directly.\n *\n * Falls back to stderr if the tty cannot be opened (non-interactive runs,\n * CI, tests) so output is not silently swallowed.\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"],"mappings":"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","names":["existsSync","readFileSync","appendFileSync","unlinkSync","mkdirSync","tmpdir","join","getUserTmpDir","join","tmpdir","existsSync","mkdirSync","appendFileSync","readFileSync","unlinkSync"]}
|
package/dist/cli/index.js
CHANGED
|
@@ -21,14 +21,14 @@ async function main() {
|
|
|
21
21
|
break;
|
|
22
22
|
}
|
|
23
23
|
case "summary": {
|
|
24
|
-
const { printSummary } = await import("../tracker-
|
|
24
|
+
const { printSummary } = await import("../tracker-HCWPUZIO.js");
|
|
25
25
|
await printSummary();
|
|
26
26
|
break;
|
|
27
27
|
}
|
|
28
28
|
case "session": {
|
|
29
29
|
const subcommand = args[1];
|
|
30
30
|
if (subcommand === "end") {
|
|
31
|
-
const { endSession } = await import("../tracker-
|
|
31
|
+
const { endSession } = await import("../tracker-HCWPUZIO.js");
|
|
32
32
|
await endSession();
|
|
33
33
|
} else {
|
|
34
34
|
console.error("[code-explainer] Unknown session command. Usage: code-explainer session end");
|
package/dist/hooks/post-tool.js
CHANGED
|
@@ -14,11 +14,10 @@ import {
|
|
|
14
14
|
formatExplanationBox,
|
|
15
15
|
formatSkipNotice,
|
|
16
16
|
getCached,
|
|
17
|
-
printToStderr,
|
|
18
17
|
readSession,
|
|
19
18
|
recordEntry,
|
|
20
19
|
setCached
|
|
21
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-W67RX53R.js";
|
|
22
21
|
import "../chunk-7OCVIDC7.js";
|
|
23
22
|
|
|
24
23
|
// src/hooks/post-tool.ts
|
|
@@ -454,7 +453,16 @@ function shouldAlertDrift(entries) {
|
|
|
454
453
|
}
|
|
455
454
|
|
|
456
455
|
// src/hooks/post-tool.ts
|
|
456
|
+
var output = [];
|
|
457
|
+
function addOutput(text) {
|
|
458
|
+
output.push(text);
|
|
459
|
+
}
|
|
457
460
|
function safeExit() {
|
|
461
|
+
if (output.length > 0) {
|
|
462
|
+
process.stdout.write(
|
|
463
|
+
JSON.stringify({ systemMessage: output.join("\n") }) + "\n"
|
|
464
|
+
);
|
|
465
|
+
}
|
|
458
466
|
process.exit(0);
|
|
459
467
|
}
|
|
460
468
|
async function readStdin() {
|
|
@@ -505,7 +513,7 @@ async function main() {
|
|
|
505
513
|
const controller = new AbortController();
|
|
506
514
|
process.on("SIGINT", () => {
|
|
507
515
|
controller.abort();
|
|
508
|
-
|
|
516
|
+
addOutput(formatSkipNotice("interrupted by user"));
|
|
509
517
|
safeExit();
|
|
510
518
|
});
|
|
511
519
|
const raw = await readStdin();
|
|
@@ -530,11 +538,11 @@ async function main() {
|
|
|
530
538
|
const result2 = lowerTool === "write" ? extractNewFileDiff(filePath, cwd) : extractEditDiff(filePath, cwd);
|
|
531
539
|
if (result2.kind === "empty") safeExit();
|
|
532
540
|
if (result2.kind === "skip") {
|
|
533
|
-
|
|
541
|
+
addOutput(formatSkipNotice(result2.reason));
|
|
534
542
|
safeExit();
|
|
535
543
|
}
|
|
536
544
|
if (result2.kind === "binary") {
|
|
537
|
-
|
|
545
|
+
addOutput(formatSkipNotice(result2.message));
|
|
538
546
|
safeExit();
|
|
539
547
|
}
|
|
540
548
|
diff = result2.content;
|
|
@@ -557,11 +565,11 @@ ${diff}`;
|
|
|
557
565
|
} else {
|
|
558
566
|
const outcome = await runEngine(filePath, diff, config, void 0, controller.signal);
|
|
559
567
|
if (outcome.kind === "skip") {
|
|
560
|
-
|
|
568
|
+
addOutput(formatSkipNotice(outcome.reason));
|
|
561
569
|
safeExit();
|
|
562
570
|
}
|
|
563
571
|
if (outcome.kind === "error") {
|
|
564
|
-
|
|
572
|
+
addOutput(formatErrorNotice(outcome.problem, outcome.cause, outcome.fix));
|
|
565
573
|
safeExit();
|
|
566
574
|
}
|
|
567
575
|
result = outcome.result;
|
|
@@ -577,7 +585,7 @@ ${diff}`;
|
|
|
577
585
|
}
|
|
578
586
|
}
|
|
579
587
|
}
|
|
580
|
-
|
|
588
|
+
addOutput(formatExplanationBox(filePath, result.summary, result.risk, result.riskReason));
|
|
581
589
|
recordEntry(payload.session_id, {
|
|
582
590
|
file: filePath,
|
|
583
591
|
timestamp: Date.now(),
|
|
@@ -588,7 +596,7 @@ ${diff}`;
|
|
|
588
596
|
const updated = readSession(payload.session_id);
|
|
589
597
|
const driftCheck = shouldAlertDrift(updated);
|
|
590
598
|
if (driftCheck.shouldAlert) {
|
|
591
|
-
|
|
599
|
+
addOutput(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles));
|
|
592
600
|
}
|
|
593
601
|
safeExit();
|
|
594
602
|
}
|
|
@@ -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 { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { loadConfig, DEFAULT_CONFIG } from \"../config/schema.js\";\nimport type { Config, HookPayload, ExplanationResult } from \"../config/schema.js\";\nimport { callOllama, type EngineOutcome } from \"../engines/ollama.js\";\nimport { callClaude } from \"../engines/claude.js\";\nimport { extractEditDiff, extractNewFileDiff, isExcluded } from \"./diff-extractor.js\";\nimport { shouldCaptureBash } from \"../filter/bash-filter.js\";\nimport { formatExplanationBox, formatDriftAlert, formatSkipNotice, formatErrorNotice, printToStderr } from \"../format/box.js\";\nimport { recordEntry, readSession, cleanStaleSessionFiles } from \"../session/tracker.js\";\nimport { analyzeDrift, shouldAlertDrift } from \"../session/drift.js\";\nimport { getCached, setCached } from \"../cache/explanation-cache.js\";\n\n/**\n * Always exit 0 — never block Claude Code on a hook failure.\n */\nfunction safeExit(): never {\n process.exit(0);\n}\n\nasync function readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let data = \"\";\n process.stdin.setEncoding(\"utf-8\");\n process.stdin.on(\"data\", (chunk) => {\n data += chunk;\n });\n process.stdin.on(\"end\", () => resolve(data));\n process.stdin.on(\"error\", () => resolve(data));\n // Safety timeout: if stdin has no data in 2s, resolve empty.\n setTimeout(() => resolve(data), 2000);\n });\n}\n\nfunction parsePayload(raw: string): HookPayload | null {\n try {\n const parsed = JSON.parse(raw);\n if (typeof parsed.session_id === \"string\" && typeof parsed.tool_name === \"string\") {\n return parsed as HookPayload;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction loadConfigSafe(cwd: string): Config {\n const path = join(cwd, \"code-explainer.config.json\");\n if (!existsSync(path)) return DEFAULT_CONFIG;\n return loadConfig(path);\n}\n\nfunction isHookEnabled(toolName: string, config: Config): boolean {\n const lower = toolName.toLowerCase();\n if (lower === \"edit\" || lower === \"multiedit\") return config.hooks.edit;\n if (lower === \"write\") return config.hooks.write;\n if (lower === \"bash\") return config.hooks.bash;\n return false;\n}\n\nasync function runEngine(\n filePath: string,\n diff: string,\n config: Config,\n userPrompt: string | undefined,\n signal: AbortSignal\n): Promise<EngineOutcome> {\n if (signal.aborted) {\n return { kind: \"skip\", reason: \"interrupted by user\" };\n }\n if (config.engine === \"ollama\") {\n return callOllama({ filePath, diff, config });\n }\n return callClaude({ filePath, diff, config, userPrompt });\n}\n\nasync function main(): Promise<void> {\n // Interrupt handler — always exit 0 on Ctrl+C.\n const controller = new AbortController();\n process.on(\"SIGINT\", () => {\n controller.abort();\n printToStderr(formatSkipNotice(\"interrupted by user\"));\n safeExit();\n });\n\n const raw = await readStdin();\n if (!raw.trim()) safeExit();\n\n const payload = parsePayload(raw);\n if (!payload) safeExit();\n\n const cwd = payload.cwd || process.cwd();\n const config = loadConfigSafe(cwd);\n\n if (!isHookEnabled(payload.tool_name, config)) safeExit();\n\n cleanStaleSessionFiles();\n\n // Pass session_id to downstream modules via env (so summary/session-end\n // commands pick the right session without re-parsing the payload).\n process.env.CODE_EXPLAINER_SESSION_ID = payload.session_id;\n\n // Extract the diff based on tool name.\n let filePath: string;\n let diff: string;\n let isNewFile = false;\n\n const lowerTool = payload.tool_name.toLowerCase();\n if (lowerTool === \"edit\" || lowerTool === \"multiedit\" || lowerTool === \"write\") {\n const input = payload.tool_input as { file_path?: string; filePath?: string };\n const target = input.file_path ?? input.filePath;\n if (!target) safeExit();\n filePath = target as string;\n\n if (isExcluded(filePath, config.exclude)) safeExit();\n\n const result = lowerTool === \"write\"\n ? extractNewFileDiff(filePath, cwd)\n : extractEditDiff(filePath, cwd);\n\n if (result.kind === \"empty\") safeExit();\n if (result.kind === \"skip\") {\n printToStderr(formatSkipNotice(result.reason));\n safeExit();\n }\n if (result.kind === \"binary\") {\n printToStderr(formatSkipNotice(result.message));\n safeExit();\n }\n diff = result.content;\n isNewFile = result.kind === \"new-file\";\n } else if (lowerTool === \"bash\") {\n const input = payload.tool_input as { command?: string };\n const command = input.command ?? \"\";\n if (!command || !shouldCaptureBash(command)) safeExit();\n filePath = \"<bash command>\";\n diff = command;\n } else {\n safeExit();\n }\n\n // Cache check.\n const cacheKey = `${filePath}\\n${diff}`;\n const cached = getCached(payload.session_id, cacheKey);\n let result: ExplanationResult | null = null;\n\n if (cached) {\n result = cached;\n } else {\n const outcome = await runEngine(filePath, diff, config, undefined, controller.signal);\n if (outcome.kind === \"skip\") {\n printToStderr(formatSkipNotice(outcome.reason));\n safeExit();\n }\n if (outcome.kind === \"error\") {\n printToStderr(formatErrorNotice(outcome.problem, outcome.cause, outcome.fix));\n safeExit();\n }\n result = outcome.result;\n setCached(payload.session_id, cacheKey, result);\n }\n\n // Path-heuristic drift analysis (only meaningful for Edit/Write).\n let driftReason: string | undefined;\n if (!isNewFile || filePath !== \"<bash command>\") {\n const priorEntries = readSession(payload.session_id);\n if (filePath !== \"<bash command>\") {\n const analysis = analyzeDrift(filePath, priorEntries);\n if (analysis.isUnrelated) {\n driftReason = analysis.reason;\n }\n }\n }\n\n // Print the explanation box.\n printToStderr(formatExplanationBox(filePath, result.summary, result.risk, result.riskReason));\n\n // Record the entry.\n recordEntry(payload.session_id, {\n file: filePath,\n timestamp: Date.now(),\n risk: result.risk,\n summary: result.summary,\n unrelated: !!driftReason,\n });\n\n // Drift alert at threshold.\n const updated = readSession(payload.session_id);\n const driftCheck = shouldAlertDrift(updated);\n if (driftCheck.shouldAlert) {\n printToStderr(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles));\n }\n\n safeExit();\n}\n\nmain().catch(() => {\n // Never fail the hook — always exit 0.\n safeExit();\n});\n","import { execFile } from \"node:child_process\";\nimport type { Config, ExplanationResult, RiskLevel } from \"../config/schema.js\";\nimport { buildClaudePrompt } from \"../prompts/templates.js\";\nimport type { EngineOutcome } from \"./ollama.js\";\n\nexport interface ClaudeCallInputs {\n filePath: string;\n diff: string;\n config: Config;\n userPrompt?: string;\n}\n\nfunction extractJson(text: string): string | null {\n const trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n const fenceMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?```/);\n if (fenceMatch) {\n return fenceMatch[1].trim();\n }\n const start = trimmed.indexOf(\"{\");\n const end = trimmed.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return trimmed.slice(start, end + 1);\n }\n return null;\n}\n\nfunction parseResponse(rawText: string): ExplanationResult | null {\n const json = extractJson(rawText);\n if (!json) return null;\n try {\n const parsed = JSON.parse(json);\n if (\n typeof parsed.summary === \"string\" &&\n typeof parsed.risk === \"string\" &&\n typeof parsed.riskReason === \"string\"\n ) {\n const risk = parsed.risk as RiskLevel;\n if (![\"none\", \"low\", \"medium\", \"high\"].includes(risk)) return null;\n return { summary: parsed.summary, risk, riskReason: parsed.riskReason };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction truncateText(text: string, max: number): string {\n if (text.length <= max) return text;\n return text.slice(0, max) + \"...\";\n}\n\ninterface ExecResult {\n stdout: string;\n stderr: string;\n code: number | null;\n}\n\nfunction runClaude(prompt: string, timeoutMs: number): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const child = execFile(\n \"claude\",\n [\"-p\", prompt],\n {\n timeout: timeoutMs,\n maxBuffer: 1024 * 1024 * 2, // 2MB\n windowsHide: true,\n },\n (err, stdout, stderr) => {\n if (err) {\n const e = err as NodeJS.ErrnoException & { killed?: boolean; signal?: string };\n if (e.code === \"ENOENT\") {\n reject(Object.assign(new Error(\"claude CLI not found\"), { code: \"ENOENT\" }));\n return;\n }\n if (e.killed || e.signal === \"SIGTERM\") {\n reject(Object.assign(new Error(\"claude timed out\"), { code: \"TIMEOUT\" }));\n return;\n }\n // Include stderr for context\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: e.code as unknown as number ?? 1 });\n return;\n }\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: 0 });\n }\n );\n child.on(\"error\", (err) => {\n reject(err);\n });\n });\n}\n\nexport async function callClaude(inputs: ClaudeCallInputs): Promise<EngineOutcome> {\n const prompt = buildClaudePrompt(inputs.config.detailLevel, {\n filePath: inputs.filePath,\n diff: inputs.diff,\n userPrompt: inputs.userPrompt,\n });\n\n try {\n const result = await runClaude(prompt, inputs.config.skipIfSlowMs);\n\n if (result.code !== 0) {\n const combined = `${result.stderr}\\n${result.stdout}`.toLowerCase();\n if (/auth|login|unauthorized|not authenticated|api key/i.test(combined)) {\n return {\n kind: \"error\",\n problem: \"Claude Code is not authenticated\",\n cause: \"The 'claude' CLI requires a valid login\",\n fix: \"Run 'claude login' in a terminal, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI returned an error\",\n cause: result.stderr.trim() || `exit code ${result.code}`,\n fix: \"Run 'claude --help' to verify the CLI works, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n\n if (!result.stdout.trim()) {\n return { kind: \"skip\", reason: \"Claude returned an empty response\" };\n }\n\n const parsed = parseResponse(result.stdout);\n if (parsed) {\n return { kind: \"ok\", result: parsed };\n }\n\n // Malformed output: fall back to truncated raw text.\n return {\n kind: \"ok\",\n result: {\n summary: truncateText(result.stdout.trim(), 200),\n risk: \"none\",\n riskReason: \"\",\n },\n };\n } catch (err) {\n const e = err as Error & { code?: string };\n if (e.code === \"ENOENT\") {\n return {\n kind: \"error\",\n problem: \"Claude CLI not found\",\n cause: \"The 'claude' command is not installed or not on PATH\",\n fix: \"Install Claude Code, or switch to Ollama engine via 'npx vibe-code-explainer config'\",\n };\n }\n if (e.code === \"TIMEOUT\") {\n return {\n kind: \"skip\",\n reason: `explanation took too long (>${inputs.config.skipIfSlowMs}ms)`,\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI invocation failed\",\n cause: e.message,\n fix: \"Check that 'claude' works by running 'claude --help' in a terminal\",\n };\n }\n}\n","import { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\n\nexport type DiffResult =\n | { kind: \"diff\"; content: string; lines: number; truncated: boolean }\n | { kind: \"new-file\"; content: string; lines: number; truncated: boolean }\n | { kind: \"binary\"; message: string }\n | { kind: \"empty\" }\n | { kind: \"skip\"; reason: string };\n\nconst MAX_DIFF_LINES = 200;\nconst HEAD_LINES = 150;\nconst TAIL_LINES = 50;\n\nfunction truncateDiff(content: string): { content: string; lines: number; truncated: boolean } {\n const lines = content.split(\"\\n\");\n if (lines.length <= MAX_DIFF_LINES) {\n return { content, lines: lines.length, truncated: false };\n }\n const head = lines.slice(0, HEAD_LINES);\n const tail = lines.slice(-TAIL_LINES);\n const omitted = lines.length - HEAD_LINES - TAIL_LINES;\n const truncated = [\n ...head,\n `[...truncated, ${omitted} more lines not shown]`,\n ...tail,\n ].join(\"\\n\");\n return { content: truncated, lines: lines.length, truncated: true };\n}\n\nfunction runGit(args: string[], cwd: string): string {\n return execFileSync(\"git\", args, { cwd, encoding: \"utf-8\", maxBuffer: 1024 * 1024 * 10 });\n}\n\nexport function extractEditDiff(filePath: string, cwd: string): DiffResult {\n // Check if we're in a git repo.\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n return { kind: \"skip\", reason: \"not inside a git repository\" };\n }\n\n // Check for binary.\n try {\n const numstat = runGit([\"diff\", \"--numstat\", \"--\", filePath], cwd).trim();\n if (numstat.startsWith(\"-\\t-\\t\")) {\n return { kind: \"binary\", message: `Binary file modified: ${filePath}` };\n }\n } catch {\n // Non-fatal, fall through to diff.\n }\n\n let diffOutput = \"\";\n try {\n diffOutput = runGit([\"diff\", \"--no-color\", \"--\", filePath], cwd);\n } catch {\n diffOutput = \"\";\n }\n\n if (!diffOutput.trim()) {\n // File may be untracked (newly created via Write/Edit on a fresh file).\n return extractNewFileDiff(filePath, cwd);\n }\n\n const { content, lines, truncated } = truncateDiff(diffOutput);\n return { kind: \"diff\", content, lines, truncated };\n}\n\nexport function extractNewFileDiff(filePath: string, cwd: string): DiffResult {\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n // Not a git repo — fall back to reading the file if possible.\n return readFileAsNewDiff(filePath);\n }\n\n // Check if file is untracked.\n let untracked = \"\";\n try {\n untracked = runGit([\"ls-files\", \"--others\", \"--exclude-standard\", \"--\", filePath], cwd).trim();\n } catch {\n untracked = \"\";\n }\n\n if (untracked) {\n return readFileAsNewDiff(filePath);\n }\n\n // Might be a file with no changes, or tracked without a diff.\n return { kind: \"empty\" };\n}\n\nfunction readFileAsNewDiff(filePath: string): DiffResult {\n if (!existsSync(filePath)) {\n return { kind: \"skip\", reason: `file not found: ${filePath}` };\n }\n\n try {\n const stat = statSync(filePath);\n if (stat.size > 2 * 1024 * 1024) {\n return { kind: \"skip\", reason: `file too large (${Math.round(stat.size / 1024)}KB)` };\n }\n\n const raw = readFileSync(filePath, \"utf-8\");\n if (!raw.trim()) {\n return { kind: \"empty\" };\n }\n\n // Check for binary content (null bytes).\n if (raw.includes(\"\\0\")) {\n return { kind: \"binary\", message: `Binary file created: ${filePath}` };\n }\n\n const withMarkers = raw.split(\"\\n\").map((l) => `+ ${l}`).join(\"\\n\");\n const diff = `--- /dev/null\\n+++ b/${filePath}\\n${withMarkers}`;\n const { content, lines, truncated } = truncateDiff(diff);\n return { kind: \"new-file\", content, lines, truncated };\n } catch {\n return { kind: \"skip\", reason: \"could not read file\" };\n }\n}\n\n/**\n * Minimal glob matcher supporting *, **, and simple extensions.\n * Matches POSIX-style paths (caller normalizes).\n *\n * - `*.ext` matches `file.ext` in any directory\n * - `dir/**` matches anything under `dir/` recursively\n * - `**\\/file.ts` matches `file.ts` anywhere\n */\nexport function matchesGlob(filePath: string, pattern: string): boolean {\n const normalized = filePath.replace(/\\\\/g, \"/\");\n const normalizedPattern = pattern.replace(/\\\\/g, \"/\");\n\n // Build regex from the pattern\n let regexSrc = \"\";\n let i = 0;\n while (i < normalizedPattern.length) {\n const ch = normalizedPattern[i];\n if (ch === \"*\") {\n if (normalizedPattern[i + 1] === \"*\") {\n // ** matches anything (including /)\n regexSrc += \".*\";\n i += 2;\n if (normalizedPattern[i] === \"/\") i++; // consume trailing /\n } else {\n // * matches anything except /\n regexSrc += \"[^/]*\";\n i++;\n }\n } else if (ch === \"?\") {\n regexSrc += \"[^/]\";\n i++;\n } else if (/[.+^${}()|[\\]]/.test(ch)) {\n regexSrc += \"\\\\\" + ch;\n i++;\n } else {\n regexSrc += ch;\n i++;\n }\n }\n\n // If the pattern has no directory component, match the filename anywhere.\n const hasSlash = normalizedPattern.includes(\"/\");\n const anchored = hasSlash\n ? new RegExp(`^${regexSrc}$`)\n : new RegExp(`(^|/)${regexSrc}$`);\n\n return anchored.test(normalized);\n}\n\nexport function isExcluded(filePath: string, patterns: string[]): boolean {\n return patterns.some((p) => matchesGlob(filePath, p));\n}\n","/**\n * Bash command filter — decides whether a Bash command should trigger an\n * explanation. Filters to filesystem-mutating and state-changing commands\n * while skipping read-only operations.\n */\n\n// Commands that modify filesystem or project state.\nconst MUTATING_COMMANDS = new Set([\n \"rm\",\n \"mv\",\n \"cp\",\n \"mkdir\",\n \"rmdir\",\n \"chmod\",\n \"chown\",\n \"ln\",\n \"touch\",\n \"dd\",\n]);\n\n// Commands that need a specific subcommand/flag to be mutating.\nconst CONTEXTUAL_COMMANDS: Record<string, RegExp> = {\n npm: /\\b(install|add|remove|uninstall|update|ci|link|unlink|init|publish)\\b/,\n yarn: /\\b(add|remove|install|upgrade|init|publish|link|unlink)\\b/,\n pnpm: /\\b(add|remove|install|update|link|unlink|publish)\\b/,\n pip: /\\b(install|uninstall)\\b/,\n pip3: /\\b(install|uninstall)\\b/,\n brew: /\\b(install|uninstall|reinstall|upgrade|link|unlink|tap|untap)\\b/,\n apt: /\\b(install|remove|purge|upgrade|update)\\b/,\n \"apt-get\": /\\b(install|remove|purge|upgrade|update)\\b/,\n git: /\\b(checkout|reset|revert|rebase|merge|commit|push|pull|clean|stash|rm|mv|init|clone|cherry-pick|restore|switch)\\b/,\n sed: /(?:^|\\s)-i\\b/,\n curl: /(?:^|\\s)-[a-zA-Z]*o\\b|--output\\b/,\n wget: /.*/,\n tar: /(?:^|\\s)-[a-zA-Z]*x\\b|--extract\\b|(?:^|\\s)-[a-zA-Z]*c\\b|--create\\b/,\n unzip: /.*/,\n docker: /\\b(run|build|push|pull|rm|rmi|exec|start|stop|kill)\\b/,\n make: /.*/,\n cargo: /\\b(build|run|install|add|remove|update|publish)\\b/,\n go: /\\b(build|install|get|mod)\\b/,\n bun: /\\b(install|add|remove|run|build|init|create|link|unlink)\\b/,\n deno: /\\b(install|compile|bundle|run)\\b/,\n};\n\n// Commands that are always read-only and never trigger.\nconst READONLY_COMMANDS = new Set([\n \"ls\",\n \"cat\",\n \"head\",\n \"tail\",\n \"grep\",\n \"find\",\n \"which\",\n \"whereis\",\n \"type\",\n \"echo\",\n \"printf\",\n \"pwd\",\n \"whoami\",\n \"id\",\n \"date\",\n \"uname\",\n \"df\",\n \"du\",\n \"ps\",\n \"top\",\n \"htop\",\n \"stat\",\n \"file\",\n \"wc\",\n \"sort\",\n \"uniq\",\n \"diff\",\n \"man\",\n \"help\",\n \"history\",\n \"tree\",\n \"less\",\n \"more\",\n \"env\",\n \"printenv\",\n \"test\",\n \"true\",\n \"false\",\n]);\n\n/**\n * Split a command string on pipe, semicolon, and logical operators.\n * Returns each sub-command with leading whitespace trimmed.\n */\nexport function splitCommandChain(command: string): string[] {\n // Split on unquoted chain operators. A simple split is good enough for\n // the vibe-coder case; we explicitly do not handle exotic quoting.\n return command\n .split(/(?:\\|\\||&&|[|;])/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Check if a single sub-command (e.g., \"rm file.txt\") should trigger.\n */\nexport function subCommandShouldCapture(subCmd: string): boolean {\n // Detect redirections (> or >>) — always capture.\n if (/(?<!\\d)>>?(?!\\d)/.test(subCmd)) {\n // Bare redirections like `ls > out.txt` still count as mutating.\n return true;\n }\n\n const tokens = subCmd.trim().split(/\\s+/);\n if (tokens.length === 0) return false;\n\n // Skip env-var assignments like `FOO=bar cmd`.\n let idx = 0;\n while (idx < tokens.length && /^[A-Z_][A-Z0-9_]*=/.test(tokens[idx])) {\n idx++;\n }\n const head = tokens[idx];\n if (!head) return false;\n\n // Strip leading path (e.g., /usr/bin/rm -> rm).\n const bin = head.split(/[/\\\\]/).pop() ?? head;\n\n if (READONLY_COMMANDS.has(bin)) return false;\n if (MUTATING_COMMANDS.has(bin)) return true;\n\n const contextPattern = CONTEXTUAL_COMMANDS[bin];\n if (contextPattern) {\n const rest = tokens.slice(idx + 1).join(\" \");\n return contextPattern.test(rest);\n }\n\n return false;\n}\n\n/**\n * Decide whether a full command string should trigger a code-explainer\n * explanation. Returns true if ANY sub-command in the chain is mutating.\n */\nexport function shouldCaptureBash(command: string): boolean {\n const parts = splitCommandChain(command);\n return parts.some((p) => subCommandShouldCapture(p));\n}\n","import type { SessionEntry } from \"./tracker.js\";\n\nconst SENSITIVE_PATTERNS = [\n /(^|\\/)\\.env(\\.|$)/i,\n /(^|\\/)payment/i,\n /(^|\\/)billing/i,\n /(^|\\/)stripe/i,\n /(^|\\/)auth/i,\n /(^|\\/)credential/i,\n /(^|\\/)secret/i,\n /(^|\\/)\\.ssh\\//i,\n];\n\nfunction topLevelDir(path: string): string {\n const norm = path.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n const parts = norm.split(\"/\").filter(Boolean);\n return parts[0] ?? \"\";\n}\n\nexport function matchesSensitivePattern(filePath: string): boolean {\n return SENSITIVE_PATTERNS.some((re) => re.test(filePath));\n}\n\nexport interface DriftAnalysis {\n isUnrelated: boolean;\n reason?: string;\n}\n\n/**\n * Path-heuristic drift detection for the Ollama engine.\n * Flags a new file as unrelated if:\n * 1. It matches a sensitive pattern (env, payment, auth, secrets) AND\n * the session did not start in a similarly-sensitive area.\n * 2. It lives in a different top-level directory than every file\n * edited so far in the session (cross-module drift).\n *\n * Returns `isUnrelated: false` for the first few edits (not enough\n * context to judge).\n */\nexport function analyzeDrift(\n newFilePath: string,\n priorEntries: SessionEntry[]\n): DriftAnalysis {\n // Not enough context yet for the first edit.\n if (priorEntries.length === 0) {\n return { isUnrelated: false };\n }\n\n const priorFiles = Array.from(new Set(priorEntries.map((e) => e.file)));\n const priorTopDirs = new Set(priorFiles.map(topLevelDir));\n const priorHasSensitive = priorFiles.some(matchesSensitivePattern);\n\n // Sensitive-pattern drift: the new file is in a sensitive area but\n // prior session was not working there.\n if (matchesSensitivePattern(newFilePath) && !priorHasSensitive) {\n return {\n isUnrelated: true,\n reason: `touches sensitive area (${newFilePath}) that was not part of earlier edits`,\n };\n }\n\n // Cross-module drift: only flag after at least 2 prior edits established\n // a working area.\n if (priorEntries.length >= 2) {\n const newTop = topLevelDir(newFilePath);\n if (newTop && !priorTopDirs.has(newTop)) {\n return {\n isUnrelated: true,\n reason: `is in a different top-level area (${newTop}) than earlier edits (${Array.from(priorTopDirs).join(\", \")})`,\n };\n }\n }\n\n return { isUnrelated: false };\n}\n\nexport interface DriftThresholdResult {\n shouldAlert: boolean;\n totalFiles: number;\n unrelatedFiles: string[];\n}\n\nconst DRIFT_ALERT_THRESHOLD = 3;\n\n/**\n * Decide whether to surface a drift alert based on accumulated session state.\n * Fires once every time the unrelated count crosses a multiple of the threshold.\n */\nexport function shouldAlertDrift(entries: SessionEntry[]): DriftThresholdResult {\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(\n new Set(entries.filter((e) => e.unrelated).map((e) => e.file))\n );\n\n // Alert exactly when we hit the threshold (not every call afterwards).\n const shouldAlert =\n unrelatedFiles.length > 0 &&\n unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0 &&\n entries.filter((e) => e.unrelated).length ===\n entries.filter((e) => e.unrelated).length;\n\n // Fire specifically on the edit that caused us to cross the threshold.\n const lastEntry = entries[entries.length - 1];\n const lastWasUnrelated = lastEntry?.unrelated ?? false;\n const crossedThreshold =\n lastWasUnrelated && unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0;\n\n return {\n shouldAlert: crossedThreshold && shouldAlert,\n totalFiles: uniqueFiles.length,\n unrelatedFiles,\n };\n}\n"],"mappings":"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","names":["existsSync","existsSync","result"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/post-tool.ts","../../src/engines/claude.ts","../../src/hooks/diff-extractor.ts","../../src/filter/bash-filter.ts","../../src/session/drift.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { loadConfig, DEFAULT_CONFIG } from \"../config/schema.js\";\nimport type { Config, HookPayload, ExplanationResult } from \"../config/schema.js\";\nimport { callOllama, type EngineOutcome } from \"../engines/ollama.js\";\nimport { callClaude } from \"../engines/claude.js\";\nimport { extractEditDiff, extractNewFileDiff, isExcluded } from \"./diff-extractor.js\";\nimport { shouldCaptureBash } from \"../filter/bash-filter.js\";\nimport { formatExplanationBox, formatDriftAlert, formatSkipNotice, formatErrorNotice } from \"../format/box.js\";\nimport { recordEntry, readSession, cleanStaleSessionFiles } from \"../session/tracker.js\";\nimport { analyzeDrift, shouldAlertDrift } from \"../session/drift.js\";\nimport { getCached, setCached } from \"../cache/explanation-cache.js\";\n\nconst output: string[] = [];\n\nfunction addOutput(text: string): void {\n output.push(text);\n}\n\n/**\n * Emit the Claude Code hook JSON on stdout so the accumulated output\n * appears as a system message in the user's terminal. Always exit 0 so\n * Claude Code is never blocked.\n */\nfunction safeExit(): never {\n if (output.length > 0) {\n process.stdout.write(\n JSON.stringify({ systemMessage: output.join(\"\\n\") }) + \"\\n\"\n );\n }\n process.exit(0);\n}\n\nasync function readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let data = \"\";\n process.stdin.setEncoding(\"utf-8\");\n process.stdin.on(\"data\", (chunk) => {\n data += chunk;\n });\n process.stdin.on(\"end\", () => resolve(data));\n process.stdin.on(\"error\", () => resolve(data));\n // Safety timeout: if stdin has no data in 2s, resolve empty.\n setTimeout(() => resolve(data), 2000);\n });\n}\n\nfunction parsePayload(raw: string): HookPayload | null {\n try {\n const parsed = JSON.parse(raw);\n if (typeof parsed.session_id === \"string\" && typeof parsed.tool_name === \"string\") {\n return parsed as HookPayload;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction loadConfigSafe(cwd: string): Config {\n const path = join(cwd, \"code-explainer.config.json\");\n if (!existsSync(path)) return DEFAULT_CONFIG;\n return loadConfig(path);\n}\n\nfunction isHookEnabled(toolName: string, config: Config): boolean {\n const lower = toolName.toLowerCase();\n if (lower === \"edit\" || lower === \"multiedit\") return config.hooks.edit;\n if (lower === \"write\") return config.hooks.write;\n if (lower === \"bash\") return config.hooks.bash;\n return false;\n}\n\nasync function runEngine(\n filePath: string,\n diff: string,\n config: Config,\n userPrompt: string | undefined,\n signal: AbortSignal\n): Promise<EngineOutcome> {\n if (signal.aborted) {\n return { kind: \"skip\", reason: \"interrupted by user\" };\n }\n if (config.engine === \"ollama\") {\n return callOllama({ filePath, diff, config });\n }\n return callClaude({ filePath, diff, config, userPrompt });\n}\n\nasync function main(): Promise<void> {\n // Interrupt handler — always exit 0 on Ctrl+C.\n const controller = new AbortController();\n process.on(\"SIGINT\", () => {\n controller.abort();\n addOutput(formatSkipNotice(\"interrupted by user\"));\n safeExit();\n });\n\n const raw = await readStdin();\n if (!raw.trim()) safeExit();\n\n const payload = parsePayload(raw);\n if (!payload) safeExit();\n\n const cwd = payload.cwd || process.cwd();\n const config = loadConfigSafe(cwd);\n\n if (!isHookEnabled(payload.tool_name, config)) safeExit();\n\n cleanStaleSessionFiles();\n\n // Pass session_id to downstream modules via env (so summary/session-end\n // commands pick the right session without re-parsing the payload).\n process.env.CODE_EXPLAINER_SESSION_ID = payload.session_id;\n\n // Extract the diff based on tool name.\n let filePath: string;\n let diff: string;\n let isNewFile = false;\n\n const lowerTool = payload.tool_name.toLowerCase();\n if (lowerTool === \"edit\" || lowerTool === \"multiedit\" || lowerTool === \"write\") {\n const input = payload.tool_input as { file_path?: string; filePath?: string };\n const target = input.file_path ?? input.filePath;\n if (!target) safeExit();\n filePath = target as string;\n\n if (isExcluded(filePath, config.exclude)) safeExit();\n\n const result = lowerTool === \"write\"\n ? extractNewFileDiff(filePath, cwd)\n : extractEditDiff(filePath, cwd);\n\n if (result.kind === \"empty\") safeExit();\n if (result.kind === \"skip\") {\n addOutput(formatSkipNotice(result.reason));\n safeExit();\n }\n if (result.kind === \"binary\") {\n addOutput(formatSkipNotice(result.message));\n safeExit();\n }\n diff = result.content;\n isNewFile = result.kind === \"new-file\";\n } else if (lowerTool === \"bash\") {\n const input = payload.tool_input as { command?: string };\n const command = input.command ?? \"\";\n if (!command || !shouldCaptureBash(command)) safeExit();\n filePath = \"<bash command>\";\n diff = command;\n } else {\n safeExit();\n }\n\n // Cache check.\n const cacheKey = `${filePath}\\n${diff}`;\n const cached = getCached(payload.session_id, cacheKey);\n let result: ExplanationResult | null = null;\n\n if (cached) {\n result = cached;\n } else {\n const outcome = await runEngine(filePath, diff, config, undefined, controller.signal);\n if (outcome.kind === \"skip\") {\n addOutput(formatSkipNotice(outcome.reason));\n safeExit();\n }\n if (outcome.kind === \"error\") {\n addOutput(formatErrorNotice(outcome.problem, outcome.cause, outcome.fix));\n safeExit();\n }\n result = outcome.result;\n setCached(payload.session_id, cacheKey, result);\n }\n\n // Path-heuristic drift analysis (only meaningful for Edit/Write).\n let driftReason: string | undefined;\n if (!isNewFile || filePath !== \"<bash command>\") {\n const priorEntries = readSession(payload.session_id);\n if (filePath !== \"<bash command>\") {\n const analysis = analyzeDrift(filePath, priorEntries);\n if (analysis.isUnrelated) {\n driftReason = analysis.reason;\n }\n }\n }\n\n // Print the explanation box.\n addOutput(formatExplanationBox(filePath, result.summary, result.risk, result.riskReason));\n\n // Record the entry.\n recordEntry(payload.session_id, {\n file: filePath,\n timestamp: Date.now(),\n risk: result.risk,\n summary: result.summary,\n unrelated: !!driftReason,\n });\n\n // Drift alert at threshold.\n const updated = readSession(payload.session_id);\n const driftCheck = shouldAlertDrift(updated);\n if (driftCheck.shouldAlert) {\n addOutput(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles));\n }\n\n safeExit();\n}\n\nmain().catch(() => {\n // Never fail the hook — always exit 0.\n safeExit();\n});\n","import { execFile } from \"node:child_process\";\nimport type { Config, ExplanationResult, RiskLevel } from \"../config/schema.js\";\nimport { buildClaudePrompt } from \"../prompts/templates.js\";\nimport type { EngineOutcome } from \"./ollama.js\";\n\nexport interface ClaudeCallInputs {\n filePath: string;\n diff: string;\n config: Config;\n userPrompt?: string;\n}\n\nfunction extractJson(text: string): string | null {\n const trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n const fenceMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?```/);\n if (fenceMatch) {\n return fenceMatch[1].trim();\n }\n const start = trimmed.indexOf(\"{\");\n const end = trimmed.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return trimmed.slice(start, end + 1);\n }\n return null;\n}\n\nfunction parseResponse(rawText: string): ExplanationResult | null {\n const json = extractJson(rawText);\n if (!json) return null;\n try {\n const parsed = JSON.parse(json);\n if (\n typeof parsed.summary === \"string\" &&\n typeof parsed.risk === \"string\" &&\n typeof parsed.riskReason === \"string\"\n ) {\n const risk = parsed.risk as RiskLevel;\n if (![\"none\", \"low\", \"medium\", \"high\"].includes(risk)) return null;\n return { summary: parsed.summary, risk, riskReason: parsed.riskReason };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction truncateText(text: string, max: number): string {\n if (text.length <= max) return text;\n return text.slice(0, max) + \"...\";\n}\n\ninterface ExecResult {\n stdout: string;\n stderr: string;\n code: number | null;\n}\n\nfunction runClaude(prompt: string, timeoutMs: number): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const child = execFile(\n \"claude\",\n [\"-p\", prompt],\n {\n timeout: timeoutMs,\n maxBuffer: 1024 * 1024 * 2, // 2MB\n windowsHide: true,\n },\n (err, stdout, stderr) => {\n if (err) {\n const e = err as NodeJS.ErrnoException & { killed?: boolean; signal?: string };\n if (e.code === \"ENOENT\") {\n reject(Object.assign(new Error(\"claude CLI not found\"), { code: \"ENOENT\" }));\n return;\n }\n if (e.killed || e.signal === \"SIGTERM\") {\n reject(Object.assign(new Error(\"claude timed out\"), { code: \"TIMEOUT\" }));\n return;\n }\n // Include stderr for context\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: e.code as unknown as number ?? 1 });\n return;\n }\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: 0 });\n }\n );\n child.on(\"error\", (err) => {\n reject(err);\n });\n });\n}\n\nexport async function callClaude(inputs: ClaudeCallInputs): Promise<EngineOutcome> {\n const prompt = buildClaudePrompt(inputs.config.detailLevel, {\n filePath: inputs.filePath,\n diff: inputs.diff,\n userPrompt: inputs.userPrompt,\n });\n\n try {\n const result = await runClaude(prompt, inputs.config.skipIfSlowMs);\n\n if (result.code !== 0) {\n const combined = `${result.stderr}\\n${result.stdout}`.toLowerCase();\n if (/auth|login|unauthorized|not authenticated|api key/i.test(combined)) {\n return {\n kind: \"error\",\n problem: \"Claude Code is not authenticated\",\n cause: \"The 'claude' CLI requires a valid login\",\n fix: \"Run 'claude login' in a terminal, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI returned an error\",\n cause: result.stderr.trim() || `exit code ${result.code}`,\n fix: \"Run 'claude --help' to verify the CLI works, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n\n if (!result.stdout.trim()) {\n return { kind: \"skip\", reason: \"Claude returned an empty response\" };\n }\n\n const parsed = parseResponse(result.stdout);\n if (parsed) {\n return { kind: \"ok\", result: parsed };\n }\n\n // Malformed output: fall back to truncated raw text.\n return {\n kind: \"ok\",\n result: {\n summary: truncateText(result.stdout.trim(), 200),\n risk: \"none\",\n riskReason: \"\",\n },\n };\n } catch (err) {\n const e = err as Error & { code?: string };\n if (e.code === \"ENOENT\") {\n return {\n kind: \"error\",\n problem: \"Claude CLI not found\",\n cause: \"The 'claude' command is not installed or not on PATH\",\n fix: \"Install Claude Code, or switch to Ollama engine via 'npx vibe-code-explainer config'\",\n };\n }\n if (e.code === \"TIMEOUT\") {\n return {\n kind: \"skip\",\n reason: `explanation took too long (>${inputs.config.skipIfSlowMs}ms)`,\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI invocation failed\",\n cause: e.message,\n fix: \"Check that 'claude' works by running 'claude --help' in a terminal\",\n };\n }\n}\n","import { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\n\nexport type DiffResult =\n | { kind: \"diff\"; content: string; lines: number; truncated: boolean }\n | { kind: \"new-file\"; content: string; lines: number; truncated: boolean }\n | { kind: \"binary\"; message: string }\n | { kind: \"empty\" }\n | { kind: \"skip\"; reason: string };\n\nconst MAX_DIFF_LINES = 200;\nconst HEAD_LINES = 150;\nconst TAIL_LINES = 50;\n\nfunction truncateDiff(content: string): { content: string; lines: number; truncated: boolean } {\n const lines = content.split(\"\\n\");\n if (lines.length <= MAX_DIFF_LINES) {\n return { content, lines: lines.length, truncated: false };\n }\n const head = lines.slice(0, HEAD_LINES);\n const tail = lines.slice(-TAIL_LINES);\n const omitted = lines.length - HEAD_LINES - TAIL_LINES;\n const truncated = [\n ...head,\n `[...truncated, ${omitted} more lines not shown]`,\n ...tail,\n ].join(\"\\n\");\n return { content: truncated, lines: lines.length, truncated: true };\n}\n\nfunction runGit(args: string[], cwd: string): string {\n return execFileSync(\"git\", args, { cwd, encoding: \"utf-8\", maxBuffer: 1024 * 1024 * 10 });\n}\n\nexport function extractEditDiff(filePath: string, cwd: string): DiffResult {\n // Check if we're in a git repo.\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n return { kind: \"skip\", reason: \"not inside a git repository\" };\n }\n\n // Check for binary.\n try {\n const numstat = runGit([\"diff\", \"--numstat\", \"--\", filePath], cwd).trim();\n if (numstat.startsWith(\"-\\t-\\t\")) {\n return { kind: \"binary\", message: `Binary file modified: ${filePath}` };\n }\n } catch {\n // Non-fatal, fall through to diff.\n }\n\n let diffOutput = \"\";\n try {\n diffOutput = runGit([\"diff\", \"--no-color\", \"--\", filePath], cwd);\n } catch {\n diffOutput = \"\";\n }\n\n if (!diffOutput.trim()) {\n // File may be untracked (newly created via Write/Edit on a fresh file).\n return extractNewFileDiff(filePath, cwd);\n }\n\n const { content, lines, truncated } = truncateDiff(diffOutput);\n return { kind: \"diff\", content, lines, truncated };\n}\n\nexport function extractNewFileDiff(filePath: string, cwd: string): DiffResult {\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n // Not a git repo — fall back to reading the file if possible.\n return readFileAsNewDiff(filePath);\n }\n\n // Check if file is untracked.\n let untracked = \"\";\n try {\n untracked = runGit([\"ls-files\", \"--others\", \"--exclude-standard\", \"--\", filePath], cwd).trim();\n } catch {\n untracked = \"\";\n }\n\n if (untracked) {\n return readFileAsNewDiff(filePath);\n }\n\n // Might be a file with no changes, or tracked without a diff.\n return { kind: \"empty\" };\n}\n\nfunction readFileAsNewDiff(filePath: string): DiffResult {\n if (!existsSync(filePath)) {\n return { kind: \"skip\", reason: `file not found: ${filePath}` };\n }\n\n try {\n const stat = statSync(filePath);\n if (stat.size > 2 * 1024 * 1024) {\n return { kind: \"skip\", reason: `file too large (${Math.round(stat.size / 1024)}KB)` };\n }\n\n const raw = readFileSync(filePath, \"utf-8\");\n if (!raw.trim()) {\n return { kind: \"empty\" };\n }\n\n // Check for binary content (null bytes).\n if (raw.includes(\"\\0\")) {\n return { kind: \"binary\", message: `Binary file created: ${filePath}` };\n }\n\n const withMarkers = raw.split(\"\\n\").map((l) => `+ ${l}`).join(\"\\n\");\n const diff = `--- /dev/null\\n+++ b/${filePath}\\n${withMarkers}`;\n const { content, lines, truncated } = truncateDiff(diff);\n return { kind: \"new-file\", content, lines, truncated };\n } catch {\n return { kind: \"skip\", reason: \"could not read file\" };\n }\n}\n\n/**\n * Minimal glob matcher supporting *, **, and simple extensions.\n * Matches POSIX-style paths (caller normalizes).\n *\n * - `*.ext` matches `file.ext` in any directory\n * - `dir/**` matches anything under `dir/` recursively\n * - `**\\/file.ts` matches `file.ts` anywhere\n */\nexport function matchesGlob(filePath: string, pattern: string): boolean {\n const normalized = filePath.replace(/\\\\/g, \"/\");\n const normalizedPattern = pattern.replace(/\\\\/g, \"/\");\n\n // Build regex from the pattern\n let regexSrc = \"\";\n let i = 0;\n while (i < normalizedPattern.length) {\n const ch = normalizedPattern[i];\n if (ch === \"*\") {\n if (normalizedPattern[i + 1] === \"*\") {\n // ** matches anything (including /)\n regexSrc += \".*\";\n i += 2;\n if (normalizedPattern[i] === \"/\") i++; // consume trailing /\n } else {\n // * matches anything except /\n regexSrc += \"[^/]*\";\n i++;\n }\n } else if (ch === \"?\") {\n regexSrc += \"[^/]\";\n i++;\n } else if (/[.+^${}()|[\\]]/.test(ch)) {\n regexSrc += \"\\\\\" + ch;\n i++;\n } else {\n regexSrc += ch;\n i++;\n }\n }\n\n // If the pattern has no directory component, match the filename anywhere.\n const hasSlash = normalizedPattern.includes(\"/\");\n const anchored = hasSlash\n ? new RegExp(`^${regexSrc}$`)\n : new RegExp(`(^|/)${regexSrc}$`);\n\n return anchored.test(normalized);\n}\n\nexport function isExcluded(filePath: string, patterns: string[]): boolean {\n return patterns.some((p) => matchesGlob(filePath, p));\n}\n","/**\n * Bash command filter — decides whether a Bash command should trigger an\n * explanation. Filters to filesystem-mutating and state-changing commands\n * while skipping read-only operations.\n */\n\n// Commands that modify filesystem or project state.\nconst MUTATING_COMMANDS = new Set([\n \"rm\",\n \"mv\",\n \"cp\",\n \"mkdir\",\n \"rmdir\",\n \"chmod\",\n \"chown\",\n \"ln\",\n \"touch\",\n \"dd\",\n]);\n\n// Commands that need a specific subcommand/flag to be mutating.\nconst CONTEXTUAL_COMMANDS: Record<string, RegExp> = {\n npm: /\\b(install|add|remove|uninstall|update|ci|link|unlink|init|publish)\\b/,\n yarn: /\\b(add|remove|install|upgrade|init|publish|link|unlink)\\b/,\n pnpm: /\\b(add|remove|install|update|link|unlink|publish)\\b/,\n pip: /\\b(install|uninstall)\\b/,\n pip3: /\\b(install|uninstall)\\b/,\n brew: /\\b(install|uninstall|reinstall|upgrade|link|unlink|tap|untap)\\b/,\n apt: /\\b(install|remove|purge|upgrade|update)\\b/,\n \"apt-get\": /\\b(install|remove|purge|upgrade|update)\\b/,\n git: /\\b(checkout|reset|revert|rebase|merge|commit|push|pull|clean|stash|rm|mv|init|clone|cherry-pick|restore|switch)\\b/,\n sed: /(?:^|\\s)-i\\b/,\n curl: /(?:^|\\s)-[a-zA-Z]*o\\b|--output\\b/,\n wget: /.*/,\n tar: /(?:^|\\s)-[a-zA-Z]*x\\b|--extract\\b|(?:^|\\s)-[a-zA-Z]*c\\b|--create\\b/,\n unzip: /.*/,\n docker: /\\b(run|build|push|pull|rm|rmi|exec|start|stop|kill)\\b/,\n make: /.*/,\n cargo: /\\b(build|run|install|add|remove|update|publish)\\b/,\n go: /\\b(build|install|get|mod)\\b/,\n bun: /\\b(install|add|remove|run|build|init|create|link|unlink)\\b/,\n deno: /\\b(install|compile|bundle|run)\\b/,\n};\n\n// Commands that are always read-only and never trigger.\nconst READONLY_COMMANDS = new Set([\n \"ls\",\n \"cat\",\n \"head\",\n \"tail\",\n \"grep\",\n \"find\",\n \"which\",\n \"whereis\",\n \"type\",\n \"echo\",\n \"printf\",\n \"pwd\",\n \"whoami\",\n \"id\",\n \"date\",\n \"uname\",\n \"df\",\n \"du\",\n \"ps\",\n \"top\",\n \"htop\",\n \"stat\",\n \"file\",\n \"wc\",\n \"sort\",\n \"uniq\",\n \"diff\",\n \"man\",\n \"help\",\n \"history\",\n \"tree\",\n \"less\",\n \"more\",\n \"env\",\n \"printenv\",\n \"test\",\n \"true\",\n \"false\",\n]);\n\n/**\n * Split a command string on pipe, semicolon, and logical operators.\n * Returns each sub-command with leading whitespace trimmed.\n */\nexport function splitCommandChain(command: string): string[] {\n // Split on unquoted chain operators. A simple split is good enough for\n // the vibe-coder case; we explicitly do not handle exotic quoting.\n return command\n .split(/(?:\\|\\||&&|[|;])/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Check if a single sub-command (e.g., \"rm file.txt\") should trigger.\n */\nexport function subCommandShouldCapture(subCmd: string): boolean {\n // Detect redirections (> or >>) — always capture.\n if (/(?<!\\d)>>?(?!\\d)/.test(subCmd)) {\n // Bare redirections like `ls > out.txt` still count as mutating.\n return true;\n }\n\n const tokens = subCmd.trim().split(/\\s+/);\n if (tokens.length === 0) return false;\n\n // Skip env-var assignments like `FOO=bar cmd`.\n let idx = 0;\n while (idx < tokens.length && /^[A-Z_][A-Z0-9_]*=/.test(tokens[idx])) {\n idx++;\n }\n const head = tokens[idx];\n if (!head) return false;\n\n // Strip leading path (e.g., /usr/bin/rm -> rm).\n const bin = head.split(/[/\\\\]/).pop() ?? head;\n\n if (READONLY_COMMANDS.has(bin)) return false;\n if (MUTATING_COMMANDS.has(bin)) return true;\n\n const contextPattern = CONTEXTUAL_COMMANDS[bin];\n if (contextPattern) {\n const rest = tokens.slice(idx + 1).join(\" \");\n return contextPattern.test(rest);\n }\n\n return false;\n}\n\n/**\n * Decide whether a full command string should trigger a code-explainer\n * explanation. Returns true if ANY sub-command in the chain is mutating.\n */\nexport function shouldCaptureBash(command: string): boolean {\n const parts = splitCommandChain(command);\n return parts.some((p) => subCommandShouldCapture(p));\n}\n","import type { SessionEntry } from \"./tracker.js\";\n\nconst SENSITIVE_PATTERNS = [\n /(^|\\/)\\.env(\\.|$)/i,\n /(^|\\/)payment/i,\n /(^|\\/)billing/i,\n /(^|\\/)stripe/i,\n /(^|\\/)auth/i,\n /(^|\\/)credential/i,\n /(^|\\/)secret/i,\n /(^|\\/)\\.ssh\\//i,\n];\n\nfunction topLevelDir(path: string): string {\n const norm = path.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n const parts = norm.split(\"/\").filter(Boolean);\n return parts[0] ?? \"\";\n}\n\nexport function matchesSensitivePattern(filePath: string): boolean {\n return SENSITIVE_PATTERNS.some((re) => re.test(filePath));\n}\n\nexport interface DriftAnalysis {\n isUnrelated: boolean;\n reason?: string;\n}\n\n/**\n * Path-heuristic drift detection for the Ollama engine.\n * Flags a new file as unrelated if:\n * 1. It matches a sensitive pattern (env, payment, auth, secrets) AND\n * the session did not start in a similarly-sensitive area.\n * 2. It lives in a different top-level directory than every file\n * edited so far in the session (cross-module drift).\n *\n * Returns `isUnrelated: false` for the first few edits (not enough\n * context to judge).\n */\nexport function analyzeDrift(\n newFilePath: string,\n priorEntries: SessionEntry[]\n): DriftAnalysis {\n // Not enough context yet for the first edit.\n if (priorEntries.length === 0) {\n return { isUnrelated: false };\n }\n\n const priorFiles = Array.from(new Set(priorEntries.map((e) => e.file)));\n const priorTopDirs = new Set(priorFiles.map(topLevelDir));\n const priorHasSensitive = priorFiles.some(matchesSensitivePattern);\n\n // Sensitive-pattern drift: the new file is in a sensitive area but\n // prior session was not working there.\n if (matchesSensitivePattern(newFilePath) && !priorHasSensitive) {\n return {\n isUnrelated: true,\n reason: `touches sensitive area (${newFilePath}) that was not part of earlier edits`,\n };\n }\n\n // Cross-module drift: only flag after at least 2 prior edits established\n // a working area.\n if (priorEntries.length >= 2) {\n const newTop = topLevelDir(newFilePath);\n if (newTop && !priorTopDirs.has(newTop)) {\n return {\n isUnrelated: true,\n reason: `is in a different top-level area (${newTop}) than earlier edits (${Array.from(priorTopDirs).join(\", \")})`,\n };\n }\n }\n\n return { isUnrelated: false };\n}\n\nexport interface DriftThresholdResult {\n shouldAlert: boolean;\n totalFiles: number;\n unrelatedFiles: string[];\n}\n\nconst DRIFT_ALERT_THRESHOLD = 3;\n\n/**\n * Decide whether to surface a drift alert based on accumulated session state.\n * Fires once every time the unrelated count crosses a multiple of the threshold.\n */\nexport function shouldAlertDrift(entries: SessionEntry[]): DriftThresholdResult {\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(\n new Set(entries.filter((e) => e.unrelated).map((e) => e.file))\n );\n\n // Alert exactly when we hit the threshold (not every call afterwards).\n const shouldAlert =\n unrelatedFiles.length > 0 &&\n unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0 &&\n entries.filter((e) => e.unrelated).length ===\n entries.filter((e) => e.unrelated).length;\n\n // Fire specifically on the edit that caused us to cross the threshold.\n const lastEntry = entries[entries.length - 1];\n const lastWasUnrelated = lastEntry?.unrelated ?? false;\n const crossedThreshold =\n lastWasUnrelated && unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0;\n\n return {\n shouldAlert: crossedThreshold && shouldAlert,\n totalFiles: uniqueFiles.length,\n unrelatedFiles,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,cAAAA,mBAAkB;AAC3B,SAAS,YAAY;;;ACDrB,SAAS,gBAAgB;AAYzB,SAAS,YAAY,MAA6B;AAChD,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,GAAG;AACpD,WAAO;AAAA,EACT;AACA,QAAM,aAAa,QAAQ,MAAM,oCAAoC;AACrE,MAAI,YAAY;AACd,WAAO,WAAW,CAAC,EAAE,KAAK;AAAA,EAC5B;AACA,QAAM,QAAQ,QAAQ,QAAQ,GAAG;AACjC,QAAM,MAAM,QAAQ,YAAY,GAAG;AACnC,MAAI,UAAU,MAAM,QAAQ,MAAM,MAAM,OAAO;AAC7C,WAAO,QAAQ,MAAM,OAAO,MAAM,CAAC;AAAA,EACrC;AACA,SAAO;AACT;AAEA,SAAS,cAAc,SAA2C;AAChE,QAAM,OAAO,YAAY,OAAO;AAChC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,QACE,OAAO,OAAO,YAAY,YAC1B,OAAO,OAAO,SAAS,YACvB,OAAO,OAAO,eAAe,UAC7B;AACA,YAAM,OAAO,OAAO;AACpB,UAAI,CAAC,CAAC,QAAQ,OAAO,UAAU,MAAM,EAAE,SAAS,IAAI,EAAG,QAAO;AAC9D,aAAO,EAAE,SAAS,OAAO,SAAS,MAAM,YAAY,OAAO,WAAW;AAAA,IACxE;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAAa,MAAc,KAAqB;AACvD,MAAI,KAAK,UAAU,IAAK,QAAO;AAC/B,SAAO,KAAK,MAAM,GAAG,GAAG,IAAI;AAC9B;AAQA,SAAS,UAAU,QAAgB,WAAwC;AACzE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,QAAQ;AAAA,MACZ;AAAA,MACA,CAAC,MAAM,MAAM;AAAA,MACb;AAAA,QACE,SAAS;AAAA,QACT,WAAW,OAAO,OAAO;AAAA;AAAA,QACzB,aAAa;AAAA,MACf;AAAA,MACA,CAAC,KAAK,QAAQ,WAAW;AACvB,YAAI,KAAK;AACP,gBAAM,IAAI;AACV,cAAI,EAAE,SAAS,UAAU;AACvB,mBAAO,OAAO,OAAO,IAAI,MAAM,sBAAsB,GAAG,EAAE,MAAM,SAAS,CAAC,CAAC;AAC3E;AAAA,UACF;AACA,cAAI,EAAE,UAAU,EAAE,WAAW,WAAW;AACtC,mBAAO,OAAO,OAAO,IAAI,MAAM,kBAAkB,GAAG,EAAE,MAAM,UAAU,CAAC,CAAC;AACxE;AAAA,UACF;AAEA,kBAAQ,EAAE,QAAQ,OAAO,SAAS,GAAG,QAAQ,OAAO,SAAS,GAAG,MAAM,EAAE,QAA6B,EAAE,CAAC;AACxG;AAAA,QACF;AACA,gBAAQ,EAAE,QAAQ,OAAO,SAAS,GAAG,QAAQ,OAAO,SAAS,GAAG,MAAM,EAAE,CAAC;AAAA,MAC3E;AAAA,IACF;AACA,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAsB,WAAW,QAAkD;AACjF,QAAM,SAAS,kBAAkB,OAAO,OAAO,aAAa;AAAA,IAC1D,UAAU,OAAO;AAAA,IACjB,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,EACrB,CAAC;AAED,MAAI;AACF,UAAM,SAAS,MAAM,UAAU,QAAQ,OAAO,OAAO,YAAY;AAEjE,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,WAAW,GAAG,OAAO,MAAM;AAAA,EAAK,OAAO,MAAM,GAAG,YAAY;AAClE,UAAI,qDAAqD,KAAK,QAAQ,GAAG;AACvE,eAAO;AAAA,UACL,MAAM;AAAA,UACN,SAAS;AAAA,UACT,OAAO;AAAA,UACP,KAAK;AAAA,QACP;AAAA,MACF;AACA,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,QACT,OAAO,OAAO,OAAO,KAAK,KAAK,aAAa,OAAO,IAAI;AAAA,QACvD,KAAK;AAAA,MACP;AAAA,IACF;AAEA,QAAI,CAAC,OAAO,OAAO,KAAK,GAAG;AACzB,aAAO,EAAE,MAAM,QAAQ,QAAQ,oCAAoC;AAAA,IACrE;AAEA,UAAM,SAAS,cAAc,OAAO,MAAM;AAC1C,QAAI,QAAQ;AACV,aAAO,EAAE,MAAM,MAAM,QAAQ,OAAO;AAAA,IACtC;AAGA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,SAAS,aAAa,OAAO,OAAO,KAAK,GAAG,GAAG;AAAA,QAC/C,MAAM;AAAA,QACN,YAAY;AAAA,MACd;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,UAAM,IAAI;AACV,QAAI,EAAE,SAAS,UAAU;AACvB,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,QACT,OAAO;AAAA,QACP,KAAK;AAAA,MACP;AAAA,IACF;AACA,QAAI,EAAE,SAAS,WAAW;AACxB,aAAO;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,+BAA+B,OAAO,OAAO,YAAY;AAAA,MACnE;AAAA,IACF;AACA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,MACT,OAAO,EAAE;AAAA,MACT,KAAK;AAAA,IACP;AAAA,EACF;AACF;;;ACnKA,SAAS,oBAAoB;AAC7B,SAAS,YAAY,cAAc,gBAAgB;AASnD,IAAM,iBAAiB;AACvB,IAAM,aAAa;AACnB,IAAM,aAAa;AAEnB,SAAS,aAAa,SAAyE;AAC7F,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,MAAI,MAAM,UAAU,gBAAgB;AAClC,WAAO,EAAE,SAAS,OAAO,MAAM,QAAQ,WAAW,MAAM;AAAA,EAC1D;AACA,QAAM,OAAO,MAAM,MAAM,GAAG,UAAU;AACtC,QAAM,OAAO,MAAM,MAAM,CAAC,UAAU;AACpC,QAAM,UAAU,MAAM,SAAS,aAAa;AAC5C,QAAM,YAAY;AAAA,IAChB,GAAG;AAAA,IACH,kBAAkB,OAAO;AAAA,IACzB,GAAG;AAAA,EACL,EAAE,KAAK,IAAI;AACX,SAAO,EAAE,SAAS,WAAW,OAAO,MAAM,QAAQ,WAAW,KAAK;AACpE;AAEA,SAAS,OAAO,MAAgB,KAAqB;AACnD,SAAO,aAAa,OAAO,MAAM,EAAE,KAAK,UAAU,SAAS,WAAW,OAAO,OAAO,GAAG,CAAC;AAC1F;AAEO,SAAS,gBAAgB,UAAkB,KAAyB;AAEzE,MAAI;AACF,WAAO,CAAC,aAAa,uBAAuB,GAAG,GAAG;AAAA,EACpD,QAAQ;AACN,WAAO,EAAE,MAAM,QAAQ,QAAQ,8BAA8B;AAAA,EAC/D;AAGA,MAAI;AACF,UAAM,UAAU,OAAO,CAAC,QAAQ,aAAa,MAAM,QAAQ,GAAG,GAAG,EAAE,KAAK;AACxE,QAAI,QAAQ,WAAW,MAAQ,GAAG;AAChC,aAAO,EAAE,MAAM,UAAU,SAAS,yBAAyB,QAAQ,GAAG;AAAA,IACxE;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,MAAI,aAAa;AACjB,MAAI;AACF,iBAAa,OAAO,CAAC,QAAQ,cAAc,MAAM,QAAQ,GAAG,GAAG;AAAA,EACjE,QAAQ;AACN,iBAAa;AAAA,EACf;AAEA,MAAI,CAAC,WAAW,KAAK,GAAG;AAEtB,WAAO,mBAAmB,UAAU,GAAG;AAAA,EACzC;AAEA,QAAM,EAAE,SAAS,OAAO,UAAU,IAAI,aAAa,UAAU;AAC7D,SAAO,EAAE,MAAM,QAAQ,SAAS,OAAO,UAAU;AACnD;AAEO,SAAS,mBAAmB,UAAkB,KAAyB;AAC5E,MAAI;AACF,WAAO,CAAC,aAAa,uBAAuB,GAAG,GAAG;AAAA,EACpD,QAAQ;AAEN,WAAO,kBAAkB,QAAQ;AAAA,EACnC;AAGA,MAAI,YAAY;AAChB,MAAI;AACF,gBAAY,OAAO,CAAC,YAAY,YAAY,sBAAsB,MAAM,QAAQ,GAAG,GAAG,EAAE,KAAK;AAAA,EAC/F,QAAQ;AACN,gBAAY;AAAA,EACd;AAEA,MAAI,WAAW;AACb,WAAO,kBAAkB,QAAQ;AAAA,EACnC;AAGA,SAAO,EAAE,MAAM,QAAQ;AACzB;AAEA,SAAS,kBAAkB,UAA8B;AACvD,MAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,WAAO,EAAE,MAAM,QAAQ,QAAQ,mBAAmB,QAAQ,GAAG;AAAA,EAC/D;AAEA,MAAI;AACF,UAAM,OAAO,SAAS,QAAQ;AAC9B,QAAI,KAAK,OAAO,IAAI,OAAO,MAAM;AAC/B,aAAO,EAAE,MAAM,QAAQ,QAAQ,mBAAmB,KAAK,MAAM,KAAK,OAAO,IAAI,CAAC,MAAM;AAAA,IACtF;AAEA,UAAM,MAAM,aAAa,UAAU,OAAO;AAC1C,QAAI,CAAC,IAAI,KAAK,GAAG;AACf,aAAO,EAAE,MAAM,QAAQ;AAAA,IACzB;AAGA,QAAI,IAAI,SAAS,IAAI,GAAG;AACtB,aAAO,EAAE,MAAM,UAAU,SAAS,wBAAwB,QAAQ,GAAG;AAAA,IACvE;AAEA,UAAM,cAAc,IAAI,MAAM,IAAI,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI;AAClE,UAAM,OAAO;AAAA,QAAwB,QAAQ;AAAA,EAAK,WAAW;AAC7D,UAAM,EAAE,SAAS,OAAO,UAAU,IAAI,aAAa,IAAI;AACvD,WAAO,EAAE,MAAM,YAAY,SAAS,OAAO,UAAU;AAAA,EACvD,QAAQ;AACN,WAAO,EAAE,MAAM,QAAQ,QAAQ,sBAAsB;AAAA,EACvD;AACF;AAUO,SAAS,YAAY,UAAkB,SAA0B;AACtE,QAAM,aAAa,SAAS,QAAQ,OAAO,GAAG;AAC9C,QAAM,oBAAoB,QAAQ,QAAQ,OAAO,GAAG;AAGpD,MAAI,WAAW;AACf,MAAI,IAAI;AACR,SAAO,IAAI,kBAAkB,QAAQ;AACnC,UAAM,KAAK,kBAAkB,CAAC;AAC9B,QAAI,OAAO,KAAK;AACd,UAAI,kBAAkB,IAAI,CAAC,MAAM,KAAK;AAEpC,oBAAY;AACZ,aAAK;AACL,YAAI,kBAAkB,CAAC,MAAM,IAAK;AAAA,MACpC,OAAO;AAEL,oBAAY;AACZ;AAAA,MACF;AAAA,IACF,WAAW,OAAO,KAAK;AACrB,kBAAY;AACZ;AAAA,IACF,WAAW,iBAAiB,KAAK,EAAE,GAAG;AACpC,kBAAY,OAAO;AACnB;AAAA,IACF,OAAO;AACL,kBAAY;AACZ;AAAA,IACF;AAAA,EACF;AAGA,QAAM,WAAW,kBAAkB,SAAS,GAAG;AAC/C,QAAM,WAAW,WACb,IAAI,OAAO,IAAI,QAAQ,GAAG,IAC1B,IAAI,OAAO,QAAQ,QAAQ,GAAG;AAElC,SAAO,SAAS,KAAK,UAAU;AACjC;AAEO,SAAS,WAAW,UAAkB,UAA6B;AACxE,SAAO,SAAS,KAAK,CAAC,MAAM,YAAY,UAAU,CAAC,CAAC;AACtD;;;ACtKA,IAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,IAAM,sBAA8C;AAAA,EAClD,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,WAAW;AAAA,EACX,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,MAAM;AACR;AAGA,IAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMM,SAAS,kBAAkB,SAA2B;AAG3D,SAAO,QACJ,MAAM,kBAAkB,EACxB,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC/B;AAKO,SAAS,wBAAwB,QAAyB;AAE/D,MAAI,mBAAmB,KAAK,MAAM,GAAG;AAEnC,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,OAAO,KAAK,EAAE,MAAM,KAAK;AACxC,MAAI,OAAO,WAAW,EAAG,QAAO;AAGhC,MAAI,MAAM;AACV,SAAO,MAAM,OAAO,UAAU,qBAAqB,KAAK,OAAO,GAAG,CAAC,GAAG;AACpE;AAAA,EACF;AACA,QAAM,OAAO,OAAO,GAAG;AACvB,MAAI,CAAC,KAAM,QAAO;AAGlB,QAAM,MAAM,KAAK,MAAM,OAAO,EAAE,IAAI,KAAK;AAEzC,MAAI,kBAAkB,IAAI,GAAG,EAAG,QAAO;AACvC,MAAI,kBAAkB,IAAI,GAAG,EAAG,QAAO;AAEvC,QAAM,iBAAiB,oBAAoB,GAAG;AAC9C,MAAI,gBAAgB;AAClB,UAAM,OAAO,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG;AAC3C,WAAO,eAAe,KAAK,IAAI;AAAA,EACjC;AAEA,SAAO;AACT;AAMO,SAAS,kBAAkB,SAA0B;AAC1D,QAAM,QAAQ,kBAAkB,OAAO;AACvC,SAAO,MAAM,KAAK,CAAC,MAAM,wBAAwB,CAAC,CAAC;AACrD;;;AC5IA,IAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,YAAY,MAAsB;AACzC,QAAM,OAAO,KAAK,QAAQ,OAAO,GAAG,EAAE,QAAQ,SAAS,EAAE;AACzD,QAAM,QAAQ,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC5C,SAAO,MAAM,CAAC,KAAK;AACrB;AAEO,SAAS,wBAAwB,UAA2B;AACjE,SAAO,mBAAmB,KAAK,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC;AAC1D;AAkBO,SAAS,aACd,aACA,cACe;AAEf,MAAI,aAAa,WAAW,GAAG;AAC7B,WAAO,EAAE,aAAa,MAAM;AAAA,EAC9B;AAEA,QAAM,aAAa,MAAM,KAAK,IAAI,IAAI,aAAa,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACtE,QAAM,eAAe,IAAI,IAAI,WAAW,IAAI,WAAW,CAAC;AACxD,QAAM,oBAAoB,WAAW,KAAK,uBAAuB;AAIjE,MAAI,wBAAwB,WAAW,KAAK,CAAC,mBAAmB;AAC9D,WAAO;AAAA,MACL,aAAa;AAAA,MACb,QAAQ,2BAA2B,WAAW;AAAA,IAChD;AAAA,EACF;AAIA,MAAI,aAAa,UAAU,GAAG;AAC5B,UAAM,SAAS,YAAY,WAAW;AACtC,QAAI,UAAU,CAAC,aAAa,IAAI,MAAM,GAAG;AACvC,aAAO;AAAA,QACL,aAAa;AAAA,QACb,QAAQ,qCAAqC,MAAM,yBAAyB,MAAM,KAAK,YAAY,EAAE,KAAK,IAAI,CAAC;AAAA,MACjH;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,aAAa,MAAM;AAC9B;AAQA,IAAM,wBAAwB;AAMvB,SAAS,iBAAiB,SAA+C;AAC9E,QAAM,cAAc,MAAM,KAAK,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAClE,QAAM,iBAAiB,MAAM;AAAA,IAC3B,IAAI,IAAI,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAAA,EAC/D;AAGA,QAAM,cACJ,eAAe,SAAS,KACxB,eAAe,SAAS,0BAA0B,KAClD,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,WACjC,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE;AAGvC,QAAM,YAAY,QAAQ,QAAQ,SAAS,CAAC;AAC5C,QAAM,mBAAmB,WAAW,aAAa;AACjD,QAAM,mBACJ,oBAAoB,eAAe,SAAS,0BAA0B;AAExE,SAAO;AAAA,IACL,aAAa,oBAAoB;AAAA,IACjC,YAAY,YAAY;AAAA,IACxB;AAAA,EACF;AACF;;;AJnGA,IAAM,SAAmB,CAAC;AAE1B,SAAS,UAAU,MAAoB;AACrC,SAAO,KAAK,IAAI;AAClB;AAOA,SAAS,WAAkB;AACzB,MAAI,OAAO,SAAS,GAAG;AACrB,YAAQ,OAAO;AAAA,MACb,KAAK,UAAU,EAAE,eAAe,OAAO,KAAK,IAAI,EAAE,CAAC,IAAI;AAAA,IACzD;AAAA,EACF;AACA,UAAQ,KAAK,CAAC;AAChB;AAEA,eAAe,YAA6B;AAC1C,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,OAAO;AACX,YAAQ,MAAM,YAAY,OAAO;AACjC,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAClC,cAAQ;AAAA,IACV,CAAC;AACD,YAAQ,MAAM,GAAG,OAAO,MAAM,QAAQ,IAAI,CAAC;AAC3C,YAAQ,MAAM,GAAG,SAAS,MAAM,QAAQ,IAAI,CAAC;AAE7C,eAAW,MAAM,QAAQ,IAAI,GAAG,GAAI;AAAA,EACtC,CAAC;AACH;AAEA,SAAS,aAAa,KAAiC;AACrD,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,OAAO,OAAO,eAAe,YAAY,OAAO,OAAO,cAAc,UAAU;AACjF,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,eAAe,KAAqB;AAC3C,QAAM,OAAO,KAAK,KAAK,4BAA4B;AACnD,MAAI,CAACC,YAAW,IAAI,EAAG,QAAO;AAC9B,SAAO,WAAW,IAAI;AACxB;AAEA,SAAS,cAAc,UAAkB,QAAyB;AAChE,QAAM,QAAQ,SAAS,YAAY;AACnC,MAAI,UAAU,UAAU,UAAU,YAAa,QAAO,OAAO,MAAM;AACnE,MAAI,UAAU,QAAS,QAAO,OAAO,MAAM;AAC3C,MAAI,UAAU,OAAQ,QAAO,OAAO,MAAM;AAC1C,SAAO;AACT;AAEA,eAAe,UACb,UACA,MACA,QACA,YACA,QACwB;AACxB,MAAI,OAAO,SAAS;AAClB,WAAO,EAAE,MAAM,QAAQ,QAAQ,sBAAsB;AAAA,EACvD;AACA,MAAI,OAAO,WAAW,UAAU;AAC9B,WAAO,WAAW,EAAE,UAAU,MAAM,OAAO,CAAC;AAAA,EAC9C;AACA,SAAO,WAAW,EAAE,UAAU,MAAM,QAAQ,WAAW,CAAC;AAC1D;AAEA,eAAe,OAAsB;AAEnC,QAAM,aAAa,IAAI,gBAAgB;AACvC,UAAQ,GAAG,UAAU,MAAM;AACzB,eAAW,MAAM;AACjB,cAAU,iBAAiB,qBAAqB,CAAC;AACjD,aAAS;AAAA,EACX,CAAC;AAED,QAAM,MAAM,MAAM,UAAU;AAC5B,MAAI,CAAC,IAAI,KAAK,EAAG,UAAS;AAE1B,QAAM,UAAU,aAAa,GAAG;AAChC,MAAI,CAAC,QAAS,UAAS;AAEvB,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACvC,QAAM,SAAS,eAAe,GAAG;AAEjC,MAAI,CAAC,cAAc,QAAQ,WAAW,MAAM,EAAG,UAAS;AAExD,yBAAuB;AAIvB,UAAQ,IAAI,4BAA4B,QAAQ;AAGhD,MAAI;AACJ,MAAI;AACJ,MAAI,YAAY;AAEhB,QAAM,YAAY,QAAQ,UAAU,YAAY;AAChD,MAAI,cAAc,UAAU,cAAc,eAAe,cAAc,SAAS;AAC9E,UAAM,QAAQ,QAAQ;AACtB,UAAM,SAAS,MAAM,aAAa,MAAM;AACxC,QAAI,CAAC,OAAQ,UAAS;AACtB,eAAW;AAEX,QAAI,WAAW,UAAU,OAAO,OAAO,EAAG,UAAS;AAEnD,UAAMC,UAAS,cAAc,UACzB,mBAAmB,UAAU,GAAG,IAChC,gBAAgB,UAAU,GAAG;AAEjC,QAAIA,QAAO,SAAS,QAAS,UAAS;AACtC,QAAIA,QAAO,SAAS,QAAQ;AAC1B,gBAAU,iBAAiBA,QAAO,MAAM,CAAC;AACzC,eAAS;AAAA,IACX;AACA,QAAIA,QAAO,SAAS,UAAU;AAC5B,gBAAU,iBAAiBA,QAAO,OAAO,CAAC;AAC1C,eAAS;AAAA,IACX;AACA,WAAOA,QAAO;AACd,gBAAYA,QAAO,SAAS;AAAA,EAC9B,WAAW,cAAc,QAAQ;AAC/B,UAAM,QAAQ,QAAQ;AACtB,UAAM,UAAU,MAAM,WAAW;AACjC,QAAI,CAAC,WAAW,CAAC,kBAAkB,OAAO,EAAG,UAAS;AACtD,eAAW;AACX,WAAO;AAAA,EACT,OAAO;AACL,aAAS;AAAA,EACX;AAGA,QAAM,WAAW,GAAG,QAAQ;AAAA,EAAK,IAAI;AACrC,QAAM,SAAS,UAAU,QAAQ,YAAY,QAAQ;AACrD,MAAI,SAAmC;AAEvC,MAAI,QAAQ;AACV,aAAS;AAAA,EACX,OAAO;AACL,UAAM,UAAU,MAAM,UAAU,UAAU,MAAM,QAAQ,QAAW,WAAW,MAAM;AACpF,QAAI,QAAQ,SAAS,QAAQ;AAC3B,gBAAU,iBAAiB,QAAQ,MAAM,CAAC;AAC1C,eAAS;AAAA,IACX;AACA,QAAI,QAAQ,SAAS,SAAS;AAC5B,gBAAU,kBAAkB,QAAQ,SAAS,QAAQ,OAAO,QAAQ,GAAG,CAAC;AACxE,eAAS;AAAA,IACX;AACA,aAAS,QAAQ;AACjB,cAAU,QAAQ,YAAY,UAAU,MAAM;AAAA,EAChD;AAGA,MAAI;AACJ,MAAI,CAAC,aAAa,aAAa,kBAAkB;AAC/C,UAAM,eAAe,YAAY,QAAQ,UAAU;AACnD,QAAI,aAAa,kBAAkB;AACjC,YAAM,WAAW,aAAa,UAAU,YAAY;AACpD,UAAI,SAAS,aAAa;AACxB,sBAAc,SAAS;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAGA,YAAU,qBAAqB,UAAU,OAAO,SAAS,OAAO,MAAM,OAAO,UAAU,CAAC;AAGxF,cAAY,QAAQ,YAAY;AAAA,IAC9B,MAAM;AAAA,IACN,WAAW,KAAK,IAAI;AAAA,IACpB,MAAM,OAAO;AAAA,IACb,SAAS,OAAO;AAAA,IAChB,WAAW,CAAC,CAAC;AAAA,EACf,CAAC;AAGD,QAAM,UAAU,YAAY,QAAQ,UAAU;AAC9C,QAAM,aAAa,iBAAiB,OAAO;AAC3C,MAAI,WAAW,aAAa;AAC1B,cAAU,iBAAiB,WAAW,YAAY,WAAW,cAAc,CAAC;AAAA,EAC9E;AAEA,WAAS;AACX;AAEA,KAAK,EAAE,MAAM,MAAM;AAEjB,WAAS;AACX,CAAC;","names":["existsSync","existsSync","result"]}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
printSummary,
|
|
7
7
|
readSession,
|
|
8
8
|
recordEntry
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-W67RX53R.js";
|
|
10
10
|
import "./chunk-7OCVIDC7.js";
|
|
11
11
|
export {
|
|
12
12
|
cleanStaleSessionFiles,
|
|
@@ -16,4 +16,4 @@ export {
|
|
|
16
16
|
readSession,
|
|
17
17
|
recordEntry
|
|
18
18
|
};
|
|
19
|
-
//# sourceMappingURL=tracker-
|
|
19
|
+
//# sourceMappingURL=tracker-HCWPUZIO.js.map
|
package/package.json
CHANGED
|
File without changes
|