Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
12 kB · 362 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363import { feature } from 'bun:bundle'import type { BetaMessageStreamParams } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'import { readdir, readFile, stat } from 'fs/promises'import memoize from 'lodash-es/memoize.js'import { join } from 'path'import type { QuerySource } from 'src/constants/querySource.js'import { setLastAPIRequest, setLastAPIRequestMessages,} from '../bootstrap/state.js'import { TICK_TAG } from '../constants/xml.js'import { type LogOption, type SerializedMessage, sortLogs,} from '../types/logs.js'import { CACHE_PATHS } from './cachePaths.js'import { stripDisplayTags, stripDisplayTagsAllowEmpty } from './displayTags.js'import { isEnvTruthy } from './envUtils.js'import { toError } from './errors.js'import { isEssentialTrafficOnly } from './privacyLevel.js'import { jsonParse } from './slowOperations.js'
/** * Gets the display title for a log/session with fallback logic. * Skips firstPrompt if it starts with a tick/goal tag (autonomous mode auto-prompt). * Strips display-unfriendly tags (like <ide_opened_file>) from the result. * Falls back to a truncated session ID when no other title is available. */export function getLogDisplayTitle( log: LogOption, defaultTitle?: string,): string { // Skip firstPrompt if it's a tick/goal message (autonomous mode auto-prompt) const isAutonomousPrompt = log.firstPrompt?.startsWith(`<${TICK_TAG}>`) // Strip display-unfriendly tags (command-name, ide_opened_file, etc.) early // so that command-only prompts (e.g. /clear) become empty and fall through // to the next fallback instead of showing raw XML tags. // Note: stripDisplayTags returns the original when stripping yields empty, // so we call stripDisplayTagsAllowEmpty to detect command-only prompts. const strippedFirstPrompt = log.firstPrompt ? stripDisplayTagsAllowEmpty(log.firstPrompt) : '' const useFirstPrompt = strippedFirstPrompt && !isAutonomousPrompt const title = log.agentName || log.customTitle || log.summary || (useFirstPrompt ? strippedFirstPrompt : undefined) || defaultTitle || // For autonomous sessions without other context, show a meaningful label (isAutonomousPrompt ? 'Autonomous session' : undefined) || // Fall back to truncated session ID for lite logs with no metadata (log.sessionId ? log.sessionId.slice(0, 8) : '') || '' // Strip display-unfriendly tags (like <ide_opened_file>) for cleaner titles return stripDisplayTags(title).trim()}
export function dateToFilename(date: Date): string { return date.toISOString().replace(/[:.]/g, '-')}
// In-memory error log for recent errors// Moved from bootstrap/state.ts to break import cycleconst MAX_IN_MEMORY_ERRORS = 100let inMemoryErrorLog: Array<{ error: string; timestamp: string }> = []
function addToInMemoryErrorLog(errorInfo: { error: string timestamp: string}): void { if (inMemoryErrorLog.length >= MAX_IN_MEMORY_ERRORS) { inMemoryErrorLog.shift() // Remove oldest error } inMemoryErrorLog.push(errorInfo)}
/** * Sink interface for the error logging backend */export type ErrorLogSink = { logError: (error: Error) => void logMCPError: (serverName: string, error: unknown) => void logMCPDebug: (serverName: string, message: string) => void getErrorsPath: () => string getMCPLogsPath: (serverName: string) => string}
// Queued events for events logged before sink is attachedtype QueuedErrorEvent = | { type: 'error'; error: Error } | { type: 'mcpError'; serverName: string; error: unknown } | { type: 'mcpDebug'; serverName: string; message: string }
const errorQueue: QueuedErrorEvent[] = []
// Sink - initialized during app startuplet errorLogSink: ErrorLogSink | null = null
/** * Attach the error log sink that will receive all error events. * Queued events are drained immediately to ensure no errors are lost. * * Idempotent: if a sink is already attached, this is a no-op. This allows * calling from both the preAction hook (for subcommands) and setup() (for * the default command) without coordination. */export function attachErrorLogSink(newSink: ErrorLogSink): void { if (errorLogSink !== null) { return } errorLogSink = newSink
// Drain the queue immediately - errors should not be delayed if (errorQueue.length > 0) { const queuedEvents = [...errorQueue] errorQueue.length = 0
for (const event of queuedEvents) { switch (event.type) { case 'error': errorLogSink.logError(event.error) break case 'mcpError': errorLogSink.logMCPError(event.serverName, event.error) break case 'mcpDebug': errorLogSink.logMCPDebug(event.serverName, event.message) break } } }}
/** * Logs an error to multiple destinations for debugging and monitoring. * * This function logs errors to: * - Debug logs (visible via `claude --debug` or `tail -f ~/.claude/debug/latest`) * - In-memory error log (accessible via `getInMemoryErrors()`, useful for including * in bug reports or displaying recent errors to users) * - Persistent error log file (only for internal 'ant' users, stored in ~/.claude/errors/) * * Usage: * ```ts * logError(new Error('Failed to connect')) * ``` * * To view errors: * - Debug: Run `claude --debug` or `tail -f ~/.claude/debug/latest` * - In-memory: Call `getInMemoryErrors()` to get recent errors for the current session */const isHardFailMode = memoize((): boolean => { return process.argv.includes('--hard-fail')})
export function logError(error: unknown): void { const err = toError(error) if (feature('HARD_FAIL') && isHardFailMode()) { // biome-ignore lint/suspicious/noConsole:: intentional crash output console.error('[HARD FAIL] logError called with:', err.stack || err.message) // eslint-disable-next-line custom-rules/no-process-exit process.exit(1) } try { // Check if error reporting should be disabled if ( // Cloud providers (Bedrock/Vertex/Foundry) always disable features isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) || isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) || isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) || process.env.DISABLE_ERROR_REPORTING || isEssentialTrafficOnly() ) { return }
const errorStr = err.stack || err.message
const errorInfo = { error: errorStr, timestamp: new Date().toISOString(), }
// Always add to in-memory log (no dependencies needed) addToInMemoryErrorLog(errorInfo)
// If sink not attached, queue the event if (errorLogSink === null) { errorQueue.push({ type: 'error', error: err }) return }
errorLogSink.logError(err) } catch { // pass }}
export function getInMemoryErrors(): { error: string; timestamp: string }[] { return [...inMemoryErrorLog]}
/** * Loads the list of error logs * @returns List of error logs sorted by date */export function loadErrorLogs(): Promise<LogOption[]> { return loadLogList(CACHE_PATHS.errors())}
/** * Gets an error log by its index * @param index Index in the sorted list of logs (0-based) * @returns Log data or null if not found */export async function getErrorLogByIndex( index: number,): Promise<LogOption | null> { const logs = await loadErrorLogs() return logs[index] || null}
/** * Internal function to load and process logs from a specified path * @param path Directory containing logs * @returns Array of logs sorted by date * @private */async function loadLogList(path: string): Promise<LogOption[]> { let files: Awaited<ReturnType<typeof readdir>> try { files = await readdir(path, { withFileTypes: true }) } catch { logError(new Error(`No logs found at ${path}`)) return [] } const logData = await Promise.all( files.map(async (file, i) => { const fullPath = join(path, file.name) const content = await readFile(fullPath, { encoding: 'utf8' }) const messages = jsonParse(content) as SerializedMessage[] const firstMessage = messages[0] const lastMessage = messages[messages.length - 1] const firstPrompt = firstMessage?.type === 'user' && typeof firstMessage?.message?.content === 'string' ? firstMessage?.message?.content : 'No prompt'
// For new random filenames, we'll get stats from the file itself const fileStats = await stat(fullPath)
// Check if it's a sidechain by looking at filename const isSidechain = fullPath.includes('sidechain')
// For new files, use the file modified time as date const date = dateToFilename(fileStats.mtime)
return { date, fullPath, messages, value: i, // hack: overwritten after sorting, right below this created: parseISOString(firstMessage?.timestamp || date), modified: lastMessage?.timestamp ? parseISOString(lastMessage.timestamp) : parseISOString(date), firstPrompt: firstPrompt.split('\n')[0]?.slice(0, 50) + (firstPrompt.length > 50 ? '…' : '') || 'No prompt', messageCount: messages.length, isSidechain, } }), )
return sortLogs(logData.filter(_ => _ !== null)).map((_, i) => ({ ..._, value: i, }))}
function parseISOString(s: string): Date { const b = s.split(/\D+/) return new Date( Date.UTC( parseInt(b[0]!, 10), parseInt(b[1]!, 10) - 1, parseInt(b[2]!, 10), parseInt(b[3]!, 10), parseInt(b[4]!, 10), parseInt(b[5]!, 10), parseInt(b[6]!, 10), ), )}
export function logMCPError(serverName: string, error: unknown): void { try { // If sink not attached, queue the event if (errorLogSink === null) { errorQueue.push({ type: 'mcpError', serverName, error }) return }
errorLogSink.logMCPError(serverName, error) } catch { // Silently fail }}
export function logMCPDebug(serverName: string, message: string): void { try { // If sink not attached, queue the event if (errorLogSink === null) { errorQueue.push({ type: 'mcpDebug', serverName, message }) return }
errorLogSink.logMCPDebug(serverName, message) } catch { // Silently fail }}
/** * Captures the last API request for inclusion in bug reports. */export function captureAPIRequest( params: BetaMessageStreamParams, querySource?: QuerySource,): void { // startsWith, not exact match — users with non-default output styles get // variants like 'repl_main_thread:outputStyle:Explanatory' (querySource.ts). if (!querySource || !querySource.startsWith('repl_main_thread')) { return }
// Store params WITHOUT messages to avoid retaining the entire conversation // for all users. Messages are already persisted to the transcript file and // available via React state. const { messages, ...paramsWithoutMessages } = params setLastAPIRequest(paramsWithoutMessages) // For ant users only: also keep a reference to the final messages array so // /share's serialized_conversation.json captures the exact post-compaction, // CLAUDE.md-injected payload the API received. Overwritten each turn; // dumpPrompts.ts already holds 5 full request bodies for ants, so this is // not a new retention class. setLastAPIRequestMessages(process.env.USER_TYPE === 'ant' ? messages : null)}
/** * Reset error log state for testing purposes only. * @internal */export function _resetErrorLogForTesting(): void { errorLogSink = null errorQueue.length = 0 inMemoryErrorLog = []}