Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
7.3 kB · 226 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227import type { ClientOptions } from '@anthropic-ai/sdk'import { createHash } from 'crypto'import { promises as fs } from 'fs'import { dirname, join } from 'path'import { getSessionId } from 'src/bootstrap/state.js'import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
function hashString(str: string): string { return createHash('sha256').update(str).digest('hex')}
// Cache last few API requests for ant users (e.g., for /issue command)const MAX_CACHED_REQUESTS = 5const cachedApiRequests: Array<{ timestamp: string; request: unknown }> = []
type DumpState = { initialized: boolean messageCountSeen: number lastInitDataHash: string // Cheap proxy for change detection — skips the expensive stringify+hash // when model/tools/system are structurally identical to the last call. lastInitFingerprint: string}
// Track state per session to avoid duplicating dataconst dumpState = new Map<string, DumpState>()
export function getLastApiRequests(): Array<{ timestamp: string request: unknown}> { return [...cachedApiRequests]}
export function clearApiRequestCache(): void { cachedApiRequests.length = 0}
export function clearDumpState(agentIdOrSessionId: string): void { dumpState.delete(agentIdOrSessionId)}
export function clearAllDumpState(): void { dumpState.clear()}
export function addApiRequestToCache(requestData: unknown): void { if (process.env.USER_TYPE !== 'ant') return cachedApiRequests.push({ timestamp: new Date().toISOString(), request: requestData, }) if (cachedApiRequests.length > MAX_CACHED_REQUESTS) { cachedApiRequests.shift() }}
export function getDumpPromptsPath(agentIdOrSessionId?: string): string { return join( getClaudeConfigHomeDir(), 'dump-prompts', `${agentIdOrSessionId ?? getSessionId()}.jsonl`, )}
function appendToFile(filePath: string, entries: string[]): void { if (entries.length === 0) return fs.mkdir(dirname(filePath), { recursive: true }) .then(() => fs.appendFile(filePath, entries.join('\n') + '\n')) .catch(() => {})}
function initFingerprint(req: Record<string, unknown>): string { const tools = req.tools as Array<{ name?: string }> | undefined const system = req.system as unknown[] | string | undefined const sysLen = typeof system === 'string' ? system.length : Array.isArray(system) ? system.reduce( (n: number, b) => n + ((b as { text?: string }).text?.length ?? 0), 0, ) : 0 const toolNames = tools?.map(t => t.name ?? '').join(',') ?? '' return `${req.model}|${toolNames}|${sysLen}`}
function dumpRequest( body: string, ts: string, state: DumpState, filePath: string,): void { try { const req = jsonParse(body) as Record<string, unknown> addApiRequestToCache(req)
if (process.env.USER_TYPE !== 'ant') return const entries: string[] = [] const messages = (req.messages ?? []) as Array<{ role?: string }>
// Write init data (system, tools, metadata) on first request, // and a system_update entry whenever it changes. // Cheap fingerprint first: system+tools don't change between turns, // so skip the 300ms stringify when the shape is unchanged. const fingerprint = initFingerprint(req) if (!state.initialized || fingerprint !== state.lastInitFingerprint) { const { messages: _, ...initData } = req const initDataStr = jsonStringify(initData) const initDataHash = hashString(initDataStr) state.lastInitFingerprint = fingerprint if (!state.initialized) { state.initialized = true state.lastInitDataHash = initDataHash // Reuse initDataStr rather than re-serializing initData inside a wrapper. // timestamp from toISOString() contains no chars needing JSON escaping. entries.push( `{"type":"init","timestamp":"${ts}","data":${initDataStr}}`, ) } else if (initDataHash !== state.lastInitDataHash) { state.lastInitDataHash = initDataHash entries.push( `{"type":"system_update","timestamp":"${ts}","data":${initDataStr}}`, ) } }
// Write only new user messages (assistant messages captured in response) for (const msg of messages.slice(state.messageCountSeen)) { if (msg.role === 'user') { entries.push( jsonStringify({ type: 'message', timestamp: ts, data: msg }), ) } } state.messageCountSeen = messages.length
appendToFile(filePath, entries) } catch { // Ignore parsing errors }}
export function createDumpPromptsFetch( agentIdOrSessionId: string,): ClientOptions['fetch'] { const filePath = getDumpPromptsPath(agentIdOrSessionId)
return async (input: RequestInfo | URL, init?: RequestInit) => { const state = dumpState.get(agentIdOrSessionId) ?? { initialized: false, messageCountSeen: 0, lastInitDataHash: '', lastInitFingerprint: '', } dumpState.set(agentIdOrSessionId, state)
let timestamp: string | undefined
if (init?.method === 'POST' && init.body) { timestamp = new Date().toISOString() // Parsing + stringifying the request (system prompt + tool schemas = MBs) // takes hundreds of ms. Defer so it doesn't block the actual API call — // this is debug tooling for /issue, not on the critical path. setImmediate(dumpRequest, init.body as string, timestamp, state, filePath) }
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins const response = await globalThis.fetch(input, init)
// Save response async if (timestamp && response.ok && process.env.USER_TYPE === 'ant') { const cloned = response.clone() void (async () => { try { const isStreaming = cloned.headers .get('content-type') ?.includes('text/event-stream')
let data: unknown if (isStreaming && cloned.body) { // Parse SSE stream into chunks const reader = cloned.body.getReader() const decoder = new TextDecoder() let buffer = '' try { while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) } } finally { reader.releaseLock() } const chunks: unknown[] = [] for (const event of buffer.split('\n\n')) { for (const line of event.split('\n')) { if (line.startsWith('data: ') && line !== 'data: [DONE]') { try { chunks.push(jsonParse(line.slice(6))) } catch { // Ignore parse errors } } } } data = { stream: true, chunks } } else { data = await cloned.json() }
await fs.appendFile( filePath, jsonStringify({ type: 'response', timestamp, data }) + '\n', ) } catch { // Best effort } })() }
return response }}