Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
13 kB · 466 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'import { randomUUID } from 'crypto'import { getOauthConfig } from 'src/constants/oauth.js'import { getOrganizationUUID } from 'src/services/oauth/client.js'import z from 'zod/v4'import { getClaudeAIOAuthTokens } from '../auth.js'import { logForDebugging } from '../debug.js'import { parseGitHubRepository } from '../detectRepository.js'import { errorMessage, toError } from '../errors.js'import { lazySchema } from '../lazySchema.js'import { logError } from '../log.js'import { sleep } from '../sleep.js'import { jsonStringify } from '../slowOperations.js'
// Retry configuration for teleport API requestsconst TELEPORT_RETRY_DELAYS = [2000, 4000, 8000, 16000] // 4 retries with exponential backoffconst MAX_TELEPORT_RETRIES = TELEPORT_RETRY_DELAYS.length
export const CCR_BYOC_BETA = 'ccr-byoc-2025-07-29'
/** * Checks if an axios error is a transient network error that should be retried */export function isTransientNetworkError(error: unknown): boolean { if (!axios.isAxiosError(error)) { return false }
// Retry on network errors (no response received) if (!error.response) { return true }
// Retry on server errors (5xx) if (error.response.status >= 500) { return true }
// Don't retry on client errors (4xx) - they're not transient return false}
/** * Makes an axios GET request with automatic retry for transient network errors * Uses exponential backoff: 2s, 4s, 8s, 16s (4 retries = 5 total attempts) */export async function axiosGetWithRetry<T>( url: string, config?: AxiosRequestConfig,): Promise<AxiosResponse<T>> { let lastError: unknown
for (let attempt = 0; attempt <= MAX_TELEPORT_RETRIES; attempt++) { try { return await axios.get<T>(url, config) } catch (error) { lastError = error
// Don't retry if this isn't a transient error if (!isTransientNetworkError(error)) { throw error }
// Don't retry if we've exhausted all retries if (attempt >= MAX_TELEPORT_RETRIES) { logForDebugging( `Teleport request failed after ${attempt + 1} attempts: ${errorMessage(error)}`, ) throw error }
const delay = TELEPORT_RETRY_DELAYS[attempt] ?? 2000 logForDebugging( `Teleport request failed (attempt ${attempt + 1}/${MAX_TELEPORT_RETRIES + 1}), retrying in ${delay}ms: ${errorMessage(error)}`, ) await sleep(delay) } }
throw lastError}
// Types matching the actual Sessions API response from api/schemas/sessions/sessions.pyexport type SessionStatus = 'requires_action' | 'running' | 'idle' | 'archived'
export type GitSource = { type: 'git_repository' url: string revision?: string | null allow_unrestricted_git_push?: boolean}
export type KnowledgeBaseSource = { type: 'knowledge_base' knowledge_base_id: string}
export type SessionContextSource = GitSource | KnowledgeBaseSource
// Outcome types from api/schemas/sandbox.pyexport type OutcomeGitInfo = { type: 'github' repo: string branches: string[]}
export type GitRepositoryOutcome = { type: 'git_repository' git_info: OutcomeGitInfo}
export type Outcome = GitRepositoryOutcome
export type SessionContext = { sources: SessionContextSource[] cwd: string outcomes: Outcome[] | null custom_system_prompt: string | null append_system_prompt: string | null model: string | null // Seed filesystem with a git bundle on Files API seed_bundle_file_id?: string github_pr?: { owner: string; repo: string; number: number } reuse_outcome_branches?: boolean}
export type SessionResource = { type: 'session' id: string title: string | null session_status: SessionStatus environment_id: string created_at: string updated_at: string session_context: SessionContext}
export type ListSessionsResponse = { data: SessionResource[] has_more: boolean first_id: string | null last_id: string | null}
export const CodeSessionSchema = lazySchema(() => z.object({ id: z.string(), title: z.string(), description: z.string(), status: z.enum([ 'idle', 'working', 'waiting', 'completed', 'archived', 'cancelled', 'rejected', ]), repo: z .object({ name: z.string(), owner: z.object({ login: z.string(), }), default_branch: z.string().optional(), }) .nullable(), turns: z.array(z.string()), created_at: z.string(), updated_at: z.string(), }),)
// Export the inferred type from the Zod schemaexport type CodeSession = z.infer<ReturnType<typeof CodeSessionSchema>>
/** * Validates and prepares for API requests * @returns Object containing access token and organization UUID */export async function prepareApiRequest(): Promise<{ accessToken: string orgUUID: string}> { const accessToken = getClaudeAIOAuthTokens()?.accessToken if (accessToken === undefined) { throw new Error( 'Claude Code web sessions require authentication with a Claude.ai account. API key authentication is not sufficient. Please run /login to authenticate, or check your authentication status with /status.', ) }
const orgUUID = await getOrganizationUUID() if (!orgUUID) { throw new Error('Unable to get organization UUID') }
return { accessToken, orgUUID }}
/** * Fetches code sessions from the new Sessions API (/v1/sessions) * @returns Array of code sessions */export async function fetchCodeSessionsFromSessionsAPI(): Promise< CodeSession[]> { const { accessToken, orgUUID } = await prepareApiRequest()
const url = `${getOauthConfig().BASE_API_URL}/v1/sessions`
try { const headers = { ...getOAuthHeaders(accessToken), 'anthropic-beta': 'ccr-byoc-2025-07-29', 'x-organization-uuid': orgUUID, }
const response = await axiosGetWithRetry<ListSessionsResponse>(url, { headers, })
if (response.status !== 200) { throw new Error(`Failed to fetch code sessions: ${response.statusText}`) }
// Transform SessionResource[] to CodeSession[] format const sessions: CodeSession[] = response.data.data.map(session => { // Extract repository info from git sources const gitSource = session.session_context.sources.find( (source): source is GitSource => source.type === 'git_repository', )
let repo: CodeSession['repo'] = null if (gitSource?.url) { // Parse GitHub URL using the existing utility function const repoPath = parseGitHubRepository(gitSource.url) if (repoPath) { const [owner, name] = repoPath.split('/') if (owner && name) { repo = { name, owner: { login: owner, }, default_branch: gitSource.revision || undefined, } } } }
return { id: session.id, title: session.title || 'Untitled', description: '', // SessionResource doesn't have description field status: session.session_status as CodeSession['status'], // Map session_status to status repo, turns: [], // SessionResource doesn't have turns field created_at: session.created_at, updated_at: session.updated_at, } })
return sessions } catch (error) { const err = toError(error) logError(err) throw error }}
/** * Creates OAuth headers for API requests * @param accessToken The OAuth access token * @returns Headers object with Authorization, Content-Type, and anthropic-version */export function getOAuthHeaders(accessToken: string): Record<string, string> { return { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', }}
/** * Fetches a single session by ID from the Sessions API * @param sessionId The session ID to fetch * @returns The session resource */export async function fetchSession( sessionId: string,): Promise<SessionResource> { const { accessToken, orgUUID } = await prepareApiRequest()
const url = `${getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}` const headers = { ...getOAuthHeaders(accessToken), 'anthropic-beta': 'ccr-byoc-2025-07-29', 'x-organization-uuid': orgUUID, }
const response = await axios.get<SessionResource>(url, { headers, timeout: 15000, validateStatus: status => status < 500, })
if (response.status !== 200) { // Extract error message from response if available const errorData = response.data as { error?: { message?: string } } const apiMessage = errorData?.error?.message
if (response.status === 404) { throw new Error(`Session not found: ${sessionId}`) }
if (response.status === 401) { throw new Error('Session expired. Please run /login to sign in again.') }
throw new Error( apiMessage || `Failed to fetch session: ${response.status} ${response.statusText}`, ) }
return response.data}
/** * Extracts the first branch name from a session's git repository outcomes * @param session The session resource to extract from * @returns The first branch name, or undefined if none found */export function getBranchFromSession( session: SessionResource,): string | undefined { const gitOutcome = session.session_context.outcomes?.find( (outcome): outcome is GitRepositoryOutcome => outcome.type === 'git_repository', ) return gitOutcome?.git_info?.branches[0]}
/** * Content for a remote session message. * Accepts a plain string or an array of content blocks (text, image, etc.) * following the Anthropic API messages spec. */export type RemoteMessageContent = | string | Array<{ type: string; [key: string]: unknown }>
/** * Sends a user message event to an existing remote session via the Sessions API * @param sessionId The session ID to send the event to * @param messageContent The user message content (string or content blocks) * @param opts.uuid Optional UUID for the event — callers that added a local * UserMessage first should pass its UUID so echo filtering can dedup * @returns Promise<boolean> True if successful, false otherwise */export async function sendEventToRemoteSession( sessionId: string, messageContent: RemoteMessageContent, opts?: { uuid?: string },): Promise<boolean> { try { const { accessToken, orgUUID } = await prepareApiRequest()
const url = `${getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}/events` const headers = { ...getOAuthHeaders(accessToken), 'anthropic-beta': 'ccr-byoc-2025-07-29', 'x-organization-uuid': orgUUID, }
const userEvent = { uuid: opts?.uuid ?? randomUUID(), session_id: sessionId, type: 'user', parent_tool_use_id: null, message: { role: 'user', content: messageContent, }, }
const requestBody = { events: [userEvent], }
logForDebugging( `[sendEventToRemoteSession] Sending event to session ${sessionId}`, ) // The endpoint may block until the CCR worker is ready. Observed ~2.6s // in normal cases; allow a generous margin for cold-start containers. const response = await axios.post(url, requestBody, { headers, validateStatus: status => status < 500, timeout: 30000, })
if (response.status === 200 || response.status === 201) { logForDebugging( `[sendEventToRemoteSession] Successfully sent event to session ${sessionId}`, ) return true }
logForDebugging( `[sendEventToRemoteSession] Failed with status ${response.status}: ${jsonStringify(response.data)}`, ) return false } catch (error) { logForDebugging(`[sendEventToRemoteSession] Error: ${errorMessage(error)}`) return false }}
/** * Updates the title of an existing remote session via the Sessions API * @param sessionId The session ID to update * @param title The new title for the session * @returns Promise<boolean> True if successful, false otherwise */export async function updateSessionTitle( sessionId: string, title: string,): Promise<boolean> { try { const { accessToken, orgUUID } = await prepareApiRequest()
const url = `${getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}` const headers = { ...getOAuthHeaders(accessToken), 'anthropic-beta': 'ccr-byoc-2025-07-29', 'x-organization-uuid': orgUUID, }
logForDebugging( `[updateSessionTitle] Updating title for session ${sessionId}: "${title}"`, ) const response = await axios.patch( url, { title }, { headers, validateStatus: status => status < 500, }, )
if (response.status === 200) { logForDebugging( `[updateSessionTitle] Successfully updated title for session ${sessionId}`, ) return true }
logForDebugging( `[updateSessionTitle] Failed with status ${response.status}: ${jsonStringify(response.data)}`, ) return false } catch (error) { logForDebugging(`[updateSessionTitle] Error: ${errorMessage(error)}`) return false }}