source dump of claude code
at main 268 lines 8.0 kB view raw
1import { appendFile, mkdir, symlink, unlink } from 'fs/promises' 2import memoize from 'lodash-es/memoize.js' 3import { dirname, join } from 'path' 4import { getSessionId } from 'src/bootstrap/state.js' 5 6import { type BufferedWriter, createBufferedWriter } from './bufferedWriter.js' 7import { registerCleanup } from './cleanupRegistry.js' 8import { 9 type DebugFilter, 10 parseDebugFilter, 11 shouldShowDebugMessage, 12} from './debugFilter.js' 13import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js' 14import { getFsImplementation } from './fsOperations.js' 15import { writeToStderr } from './process.js' 16import { jsonStringify } from './slowOperations.js' 17 18export type DebugLogLevel = 'verbose' | 'debug' | 'info' | 'warn' | 'error' 19 20const LEVEL_ORDER: Record<DebugLogLevel, number> = { 21 verbose: 0, 22 debug: 1, 23 info: 2, 24 warn: 3, 25 error: 4, 26} 27 28/** 29 * Minimum log level to include in debug output. Defaults to 'debug', which 30 * filters out 'verbose' messages. Set CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose to 31 * include high-volume diagnostics (e.g. full statusLine command, shell, cwd, 32 * stdout/stderr) that would otherwise drown out useful debug output. 33 */ 34export const getMinDebugLogLevel = memoize((): DebugLogLevel => { 35 const raw = process.env.CLAUDE_CODE_DEBUG_LOG_LEVEL?.toLowerCase().trim() 36 if (raw && Object.hasOwn(LEVEL_ORDER, raw)) { 37 return raw as DebugLogLevel 38 } 39 return 'debug' 40}) 41 42let runtimeDebugEnabled = false 43 44export const isDebugMode = memoize((): boolean => { 45 return ( 46 runtimeDebugEnabled || 47 isEnvTruthy(process.env.DEBUG) || 48 isEnvTruthy(process.env.DEBUG_SDK) || 49 process.argv.includes('--debug') || 50 process.argv.includes('-d') || 51 isDebugToStdErr() || 52 // Also check for --debug=pattern syntax 53 process.argv.some(arg => arg.startsWith('--debug=')) || 54 // --debug-file implicitly enables debug mode 55 getDebugFilePath() !== null 56 ) 57}) 58 59/** 60 * Enables debug logging mid-session (e.g. via /debug). Non-ants don't write 61 * debug logs by default, so this lets them start capturing without restarting 62 * with --debug. Returns true if logging was already active. 63 */ 64export function enableDebugLogging(): boolean { 65 const wasActive = isDebugMode() || process.env.USER_TYPE === 'ant' 66 runtimeDebugEnabled = true 67 isDebugMode.cache.clear?.() 68 return wasActive 69} 70 71// Extract and parse debug filter from command line arguments 72// Exported for testing purposes 73export const getDebugFilter = memoize((): DebugFilter | null => { 74 // Look for --debug=pattern in argv 75 const debugArg = process.argv.find(arg => arg.startsWith('--debug=')) 76 if (!debugArg) { 77 return null 78 } 79 80 // Extract the pattern after the equals sign 81 const filterPattern = debugArg.substring('--debug='.length) 82 return parseDebugFilter(filterPattern) 83}) 84 85export const isDebugToStdErr = memoize((): boolean => { 86 return ( 87 process.argv.includes('--debug-to-stderr') || process.argv.includes('-d2e') 88 ) 89}) 90 91export const getDebugFilePath = memoize((): string | null => { 92 for (let i = 0; i < process.argv.length; i++) { 93 const arg = process.argv[i]! 94 if (arg.startsWith('--debug-file=')) { 95 return arg.substring('--debug-file='.length) 96 } 97 if (arg === '--debug-file' && i + 1 < process.argv.length) { 98 return process.argv[i + 1]! 99 } 100 } 101 return null 102}) 103 104function shouldLogDebugMessage(message: string): boolean { 105 if (process.env.NODE_ENV === 'test' && !isDebugToStdErr()) { 106 return false 107 } 108 109 // Non-ants only write debug logs when debug mode is active (via --debug at 110 // startup or /debug mid-session). Ants always log for /share, bug reports. 111 if (process.env.USER_TYPE !== 'ant' && !isDebugMode()) { 112 return false 113 } 114 115 if ( 116 typeof process === 'undefined' || 117 typeof process.versions === 'undefined' || 118 typeof process.versions.node === 'undefined' 119 ) { 120 return false 121 } 122 123 const filter = getDebugFilter() 124 return shouldShowDebugMessage(message, filter) 125} 126 127let hasFormattedOutput = false 128export function setHasFormattedOutput(value: boolean): void { 129 hasFormattedOutput = value 130} 131export function getHasFormattedOutput(): boolean { 132 return hasFormattedOutput 133} 134 135let debugWriter: BufferedWriter | null = null 136let pendingWrite: Promise<void> = Promise.resolve() 137 138// Module-level so .bind captures only its explicit args, not the 139// writeFn closure's parent scope (Jarred, #22257). 140async function appendAsync( 141 needMkdir: boolean, 142 dir: string, 143 path: string, 144 content: string, 145): Promise<void> { 146 if (needMkdir) { 147 await mkdir(dir, { recursive: true }).catch(() => {}) 148 } 149 await appendFile(path, content) 150 void updateLatestDebugLogSymlink() 151} 152 153function noop(): void {} 154 155function getDebugWriter(): BufferedWriter { 156 if (!debugWriter) { 157 let ensuredDir: string | null = null 158 debugWriter = createBufferedWriter({ 159 writeFn: content => { 160 const path = getDebugLogPath() 161 const dir = dirname(path) 162 const needMkdir = ensuredDir !== dir 163 ensuredDir = dir 164 if (isDebugMode()) { 165 // immediateMode: must stay sync. Async writes are lost on direct 166 // process.exit() and keep the event loop alive in beforeExit 167 // handlers (infinite loop with Perfetto tracing). See #22257. 168 if (needMkdir) { 169 try { 170 getFsImplementation().mkdirSync(dir) 171 } catch { 172 // Directory already exists 173 } 174 } 175 getFsImplementation().appendFileSync(path, content) 176 void updateLatestDebugLogSymlink() 177 return 178 } 179 // Buffered path (ants without --debug): flushes ~1/sec so chain 180 // depth stays ~1. .bind over a closure so only the bound args are 181 // retained, not this scope. 182 pendingWrite = pendingWrite 183 .then(appendAsync.bind(null, needMkdir, dir, path, content)) 184 .catch(noop) 185 }, 186 flushIntervalMs: 1000, 187 maxBufferSize: 100, 188 immediateMode: isDebugMode(), 189 }) 190 registerCleanup(async () => { 191 debugWriter?.dispose() 192 await pendingWrite 193 }) 194 } 195 return debugWriter 196} 197 198export async function flushDebugLogs(): Promise<void> { 199 debugWriter?.flush() 200 await pendingWrite 201} 202 203export function logForDebugging( 204 message: string, 205 { level }: { level: DebugLogLevel } = { 206 level: 'debug', 207 }, 208): void { 209 if (LEVEL_ORDER[level] < LEVEL_ORDER[getMinDebugLogLevel()]) { 210 return 211 } 212 if (!shouldLogDebugMessage(message)) { 213 return 214 } 215 216 // Multiline messages break the jsonl output format, so make any multiline messages JSON. 217 if (hasFormattedOutput && message.includes('\n')) { 218 message = jsonStringify(message) 219 } 220 const timestamp = new Date().toISOString() 221 const output = `${timestamp} [${level.toUpperCase()}] ${message.trim()}\n` 222 if (isDebugToStdErr()) { 223 writeToStderr(output) 224 return 225 } 226 227 getDebugWriter().write(output) 228} 229 230export function getDebugLogPath(): string { 231 return ( 232 getDebugFilePath() ?? 233 process.env.CLAUDE_CODE_DEBUG_LOGS_DIR ?? 234 join(getClaudeConfigHomeDir(), 'debug', `${getSessionId()}.txt`) 235 ) 236} 237 238/** 239 * Updates the latest debug log symlink to point to the current debug log file. 240 * Creates or updates a symlink at ~/.claude/debug/latest 241 */ 242const updateLatestDebugLogSymlink = memoize(async (): Promise<void> => { 243 try { 244 const debugLogPath = getDebugLogPath() 245 const debugLogsDir = dirname(debugLogPath) 246 const latestSymlinkPath = join(debugLogsDir, 'latest') 247 248 await unlink(latestSymlinkPath).catch(() => {}) 249 await symlink(debugLogPath, latestSymlinkPath) 250 } catch { 251 // Silently fail if symlink creation fails 252 } 253}) 254 255/** 256 * Logs errors for Ants only, always visible in production. 257 */ 258export function logAntError(context: string, error: unknown): void { 259 if (process.env.USER_TYPE !== 'ant') { 260 return 261 } 262 263 if (error instanceof Error && error.stack) { 264 logForDebugging(`[ANT-ONLY] ${context} stack trace:\n${error.stack}`, { 265 level: 'error', 266 }) 267 } 268}