Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
8.0 kB · 268 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269import { appendFile, mkdir, symlink, unlink } from 'fs/promises'import memoize from 'lodash-es/memoize.js'import { dirname, join } from 'path'import { getSessionId } from 'src/bootstrap/state.js'
import { type BufferedWriter, createBufferedWriter } from './bufferedWriter.js'import { registerCleanup } from './cleanupRegistry.js'import { type DebugFilter, parseDebugFilter, shouldShowDebugMessage,} from './debugFilter.js'import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js'import { getFsImplementation } from './fsOperations.js'import { writeToStderr } from './process.js'import { jsonStringify } from './slowOperations.js'
export type DebugLogLevel = 'verbose' | 'debug' | 'info' | 'warn' | 'error'
const LEVEL_ORDER: Record<DebugLogLevel, number> = { verbose: 0, debug: 1, info: 2, warn: 3, error: 4,}
/** * Minimum log level to include in debug output. Defaults to 'debug', which * filters out 'verbose' messages. Set CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose to * include high-volume diagnostics (e.g. full statusLine command, shell, cwd, * stdout/stderr) that would otherwise drown out useful debug output. */export const getMinDebugLogLevel = memoize((): DebugLogLevel => { const raw = process.env.CLAUDE_CODE_DEBUG_LOG_LEVEL?.toLowerCase().trim() if (raw && Object.hasOwn(LEVEL_ORDER, raw)) { return raw as DebugLogLevel } return 'debug'})
let runtimeDebugEnabled = false
export const isDebugMode = memoize((): boolean => { return ( runtimeDebugEnabled || isEnvTruthy(process.env.DEBUG) || isEnvTruthy(process.env.DEBUG_SDK) || process.argv.includes('--debug') || process.argv.includes('-d') || isDebugToStdErr() || // Also check for --debug=pattern syntax process.argv.some(arg => arg.startsWith('--debug=')) || // --debug-file implicitly enables debug mode getDebugFilePath() !== null )})
/** * Enables debug logging mid-session (e.g. via /debug). Non-ants don't write * debug logs by default, so this lets them start capturing without restarting * with --debug. Returns true if logging was already active. */export function enableDebugLogging(): boolean { const wasActive = isDebugMode() || process.env.USER_TYPE === 'ant' runtimeDebugEnabled = true isDebugMode.cache.clear?.() return wasActive}
// Extract and parse debug filter from command line arguments// Exported for testing purposesexport const getDebugFilter = memoize((): DebugFilter | null => { // Look for --debug=pattern in argv const debugArg = process.argv.find(arg => arg.startsWith('--debug=')) if (!debugArg) { return null }
// Extract the pattern after the equals sign const filterPattern = debugArg.substring('--debug='.length) return parseDebugFilter(filterPattern)})
export const isDebugToStdErr = memoize((): boolean => { return ( process.argv.includes('--debug-to-stderr') || process.argv.includes('-d2e') )})
export const getDebugFilePath = memoize((): string | null => { for (let i = 0; i < process.argv.length; i++) { const arg = process.argv[i]! if (arg.startsWith('--debug-file=')) { return arg.substring('--debug-file='.length) } if (arg === '--debug-file' && i + 1 < process.argv.length) { return process.argv[i + 1]! } } return null})
function shouldLogDebugMessage(message: string): boolean { if (process.env.NODE_ENV === 'test' && !isDebugToStdErr()) { return false }
// Non-ants only write debug logs when debug mode is active (via --debug at // startup or /debug mid-session). Ants always log for /share, bug reports. if (process.env.USER_TYPE !== 'ant' && !isDebugMode()) { return false }
if ( typeof process === 'undefined' || typeof process.versions === 'undefined' || typeof process.versions.node === 'undefined' ) { return false }
const filter = getDebugFilter() return shouldShowDebugMessage(message, filter)}
let hasFormattedOutput = falseexport function setHasFormattedOutput(value: boolean): void { hasFormattedOutput = value}export function getHasFormattedOutput(): boolean { return hasFormattedOutput}
let debugWriter: BufferedWriter | null = nulllet pendingWrite: Promise<void> = Promise.resolve()
// Module-level so .bind captures only its explicit args, not the// writeFn closure's parent scope (Jarred, #22257).async function appendAsync( needMkdir: boolean, dir: string, path: string, content: string,): Promise<void> { if (needMkdir) { await mkdir(dir, { recursive: true }).catch(() => {}) } await appendFile(path, content) void updateLatestDebugLogSymlink()}
function noop(): void {}
function getDebugWriter(): BufferedWriter { if (!debugWriter) { let ensuredDir: string | null = null debugWriter = createBufferedWriter({ writeFn: content => { const path = getDebugLogPath() const dir = dirname(path) const needMkdir = ensuredDir !== dir ensuredDir = dir if (isDebugMode()) { // immediateMode: must stay sync. Async writes are lost on direct // process.exit() and keep the event loop alive in beforeExit // handlers (infinite loop with Perfetto tracing). See #22257. if (needMkdir) { try { getFsImplementation().mkdirSync(dir) } catch { // Directory already exists } } getFsImplementation().appendFileSync(path, content) void updateLatestDebugLogSymlink() return } // Buffered path (ants without --debug): flushes ~1/sec so chain // depth stays ~1. .bind over a closure so only the bound args are // retained, not this scope. pendingWrite = pendingWrite .then(appendAsync.bind(null, needMkdir, dir, path, content)) .catch(noop) }, flushIntervalMs: 1000, maxBufferSize: 100, immediateMode: isDebugMode(), }) registerCleanup(async () => { debugWriter?.dispose() await pendingWrite }) } return debugWriter}
export async function flushDebugLogs(): Promise<void> { debugWriter?.flush() await pendingWrite}
export function logForDebugging( message: string, { level }: { level: DebugLogLevel } = { level: 'debug', },): void { if (LEVEL_ORDER[level] < LEVEL_ORDER[getMinDebugLogLevel()]) { return } if (!shouldLogDebugMessage(message)) { return }
// Multiline messages break the jsonl output format, so make any multiline messages JSON. if (hasFormattedOutput && message.includes('\n')) { message = jsonStringify(message) } const timestamp = new Date().toISOString() const output = `${timestamp} [${level.toUpperCase()}] ${message.trim()}\n` if (isDebugToStdErr()) { writeToStderr(output) return }
getDebugWriter().write(output)}
export function getDebugLogPath(): string { return ( getDebugFilePath() ?? process.env.CLAUDE_CODE_DEBUG_LOGS_DIR ?? join(getClaudeConfigHomeDir(), 'debug', `${getSessionId()}.txt`) )}
/** * Updates the latest debug log symlink to point to the current debug log file. * Creates or updates a symlink at ~/.claude/debug/latest */const updateLatestDebugLogSymlink = memoize(async (): Promise<void> => { try { const debugLogPath = getDebugLogPath() const debugLogsDir = dirname(debugLogPath) const latestSymlinkPath = join(debugLogsDir, 'latest')
await unlink(latestSymlinkPath).catch(() => {}) await symlink(debugLogPath, latestSymlinkPath) } catch { // Silently fail if symlink creation fails }})
/** * Logs errors for Ants only, always visible in production. */export function logAntError(context: string, error: unknown): void { if (process.env.USER_TYPE !== 'ant') { return }
if (error instanceof Error && error.stack) { logForDebugging(`[ANT-ONLY] ${context} stack trace:\n${error.stack}`, { level: 'error', }) }}