Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
12 kB · 384 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'import { logForDebugging } from '../utils/debug.js'import { errorMessage } from '../utils/errors.js'import { extractErrorDetail } from './debugUtils.js'import { toCompatSessionId } from './sessionIdCompat.js'
type GitSource = { type: 'git_repository' url: string revision?: string}
type GitOutcome = { type: 'git_repository' git_info: { type: 'github'; repo: string; branches: string[] }}
// Events must be wrapped in { type: 'event', data: <sdk_message> } for the// POST /v1/sessions endpoint (discriminated union format).type SessionEvent = { type: 'event' data: SDKMessage}
/** * Create a session on a bridge environment via POST /v1/sessions. * * Used by both `claude remote-control` (empty session so the user has somewhere to * type immediately) and `/remote-control` (session pre-populated with conversation * history). * * Returns the session ID on success, or null if creation fails (non-fatal). */export async function createBridgeSession({ environmentId, title, events, gitRepoUrl, branch, signal, baseUrl: baseUrlOverride, getAccessToken, permissionMode,}: { environmentId: string title?: string events: SessionEvent[] gitRepoUrl: string | null branch: string signal: AbortSignal baseUrl?: string getAccessToken?: () => string | undefined permissionMode?: string}): Promise<string | null> { const { getClaudeAIOAuthTokens } = await import('../utils/auth.js') const { getOrganizationUUID } = await import('../services/oauth/client.js') const { getOauthConfig } = await import('../constants/oauth.js') const { getOAuthHeaders } = await import('../utils/teleport/api.js') const { parseGitHubRepository } = await import('../utils/detectRepository.js') const { getDefaultBranch } = await import('../utils/git.js') const { getMainLoopModel } = await import('../utils/model/model.js') const { default: axios } = await import('axios')
const accessToken = getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken if (!accessToken) { logForDebugging('[bridge] No access token for session creation') return null }
const orgUUID = await getOrganizationUUID() if (!orgUUID) { logForDebugging('[bridge] No org UUID for session creation') return null }
// Build git source and outcome context let gitSource: GitSource | null = null let gitOutcome: GitOutcome | null = null
if (gitRepoUrl) { const { parseGitRemote } = await import('../utils/detectRepository.js') const parsed = parseGitRemote(gitRepoUrl) if (parsed) { const { host, owner, name } = parsed const revision = branch || (await getDefaultBranch()) || undefined gitSource = { type: 'git_repository', url: `https://${host}/${owner}/${name}`, revision, } gitOutcome = { type: 'git_repository', git_info: { type: 'github', repo: `${owner}/${name}`, branches: [`claude/${branch || 'task'}`], }, } } else { // Fallback: try parseGitHubRepository for owner/repo format const ownerRepo = parseGitHubRepository(gitRepoUrl) if (ownerRepo) { const [owner, name] = ownerRepo.split('/') if (owner && name) { const revision = branch || (await getDefaultBranch()) || undefined gitSource = { type: 'git_repository', url: `https://github.com/${owner}/${name}`, revision, } gitOutcome = { type: 'git_repository', git_info: { type: 'github', repo: `${owner}/${name}`, branches: [`claude/${branch || 'task'}`], }, } } } } }
const requestBody = { ...(title !== undefined && { title }), events, session_context: { sources: gitSource ? [gitSource] : [], outcomes: gitOutcome ? [gitOutcome] : [], model: getMainLoopModel(), }, environment_id: environmentId, source: 'remote-control', ...(permissionMode && { permission_mode: permissionMode }), }
const headers = { ...getOAuthHeaders(accessToken), 'anthropic-beta': 'ccr-byoc-2025-07-29', 'x-organization-uuid': orgUUID, }
const url = `${baseUrlOverride ?? getOauthConfig().BASE_API_URL}/v1/sessions` let response try { response = await axios.post(url, requestBody, { headers, signal, validateStatus: s => s < 500, }) } catch (err: unknown) { logForDebugging( `[bridge] Session creation request failed: ${errorMessage(err)}`, ) return null } const isSuccess = response.status === 200 || response.status === 201
if (!isSuccess) { const detail = extractErrorDetail(response.data) logForDebugging( `[bridge] Session creation failed with status ${response.status}${detail ? `: ${detail}` : ''}`, ) return null }
const sessionData: unknown = response.data if ( !sessionData || typeof sessionData !== 'object' || !('id' in sessionData) || typeof sessionData.id !== 'string' ) { logForDebugging('[bridge] No session ID in response') return null }
return sessionData.id}
/** * Fetch a bridge session via GET /v1/sessions/{id}. * * Returns the session's environment_id (for `--session-id` resume) and title. * Uses the same org-scoped headers as create/archive — the environments-level * client in bridgeApi.ts uses a different beta header and no org UUID, which * makes the Sessions API return 404. */export async function getBridgeSession( sessionId: string, opts?: { baseUrl?: string; getAccessToken?: () => string | undefined },): Promise<{ environment_id?: string; title?: string } | null> { const { getClaudeAIOAuthTokens } = await import('../utils/auth.js') const { getOrganizationUUID } = await import('../services/oauth/client.js') const { getOauthConfig } = await import('../constants/oauth.js') const { getOAuthHeaders } = await import('../utils/teleport/api.js') const { default: axios } = await import('axios')
const accessToken = opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken if (!accessToken) { logForDebugging('[bridge] No access token for session fetch') return null }
const orgUUID = await getOrganizationUUID() if (!orgUUID) { logForDebugging('[bridge] No org UUID for session fetch') return null }
const headers = { ...getOAuthHeaders(accessToken), 'anthropic-beta': 'ccr-byoc-2025-07-29', 'x-organization-uuid': orgUUID, }
const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}` logForDebugging(`[bridge] Fetching session ${sessionId}`)
let response try { response = await axios.get<{ environment_id?: string; title?: string }>( url, { headers, timeout: 10_000, validateStatus: s => s < 500 }, ) } catch (err: unknown) { logForDebugging( `[bridge] Session fetch request failed: ${errorMessage(err)}`, ) return null }
if (response.status !== 200) { const detail = extractErrorDetail(response.data) logForDebugging( `[bridge] Session fetch failed with status ${response.status}${detail ? `: ${detail}` : ''}`, ) return null }
return response.data}
/** * Archive a bridge session via POST /v1/sessions/{id}/archive. * * The CCR server never auto-archives sessions — archival is always an * explicit client action. Both `claude remote-control` (standalone bridge) and the * always-on `/remote-control` REPL bridge call this during shutdown to archive any * sessions that are still alive. * * The archive endpoint accepts sessions in any status (running, idle, * requires_action, pending) and returns 409 if already archived, making * it safe to call even if the server-side runner already archived the * session. * * Callers must handle errors — this function has no try/catch; 5xx, * timeouts, and network errors throw. Archival is best-effort during * cleanup; call sites wrap with .catch(). */export async function archiveBridgeSession( sessionId: string, opts?: { baseUrl?: string getAccessToken?: () => string | undefined timeoutMs?: number },): Promise<void> { const { getClaudeAIOAuthTokens } = await import('../utils/auth.js') const { getOrganizationUUID } = await import('../services/oauth/client.js') const { getOauthConfig } = await import('../constants/oauth.js') const { getOAuthHeaders } = await import('../utils/teleport/api.js') const { default: axios } = await import('axios')
const accessToken = opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken if (!accessToken) { logForDebugging('[bridge] No access token for session archive') return }
const orgUUID = await getOrganizationUUID() if (!orgUUID) { logForDebugging('[bridge] No org UUID for session archive') return }
const headers = { ...getOAuthHeaders(accessToken), 'anthropic-beta': 'ccr-byoc-2025-07-29', 'x-organization-uuid': orgUUID, }
const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}/archive` logForDebugging(`[bridge] Archiving session ${sessionId}`)
const response = await axios.post( url, {}, { headers, timeout: opts?.timeoutMs ?? 10_000, validateStatus: s => s < 500, }, )
if (response.status === 200) { logForDebugging(`[bridge] Session ${sessionId} archived successfully`) } else { const detail = extractErrorDetail(response.data) logForDebugging( `[bridge] Session archive failed with status ${response.status}${detail ? `: ${detail}` : ''}`, ) }}
/** * Update the title of a bridge session via PATCH /v1/sessions/{id}. * * Called when the user renames a session via /rename while a bridge * connection is active, so the title stays in sync on claude.ai/code. * * Errors are swallowed — title sync is best-effort. */export async function updateBridgeSessionTitle( sessionId: string, title: string, opts?: { baseUrl?: string; getAccessToken?: () => string | undefined },): Promise<void> { const { getClaudeAIOAuthTokens } = await import('../utils/auth.js') const { getOrganizationUUID } = await import('../services/oauth/client.js') const { getOauthConfig } = await import('../constants/oauth.js') const { getOAuthHeaders } = await import('../utils/teleport/api.js') const { default: axios } = await import('axios')
const accessToken = opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken if (!accessToken) { logForDebugging('[bridge] No access token for session title update') return }
const orgUUID = await getOrganizationUUID() if (!orgUUID) { logForDebugging('[bridge] No org UUID for session title update') return }
const headers = { ...getOAuthHeaders(accessToken), 'anthropic-beta': 'ccr-byoc-2025-07-29', 'x-organization-uuid': orgUUID, }
// Compat gateway only accepts session_* (compat/convert.go:27). v2 callers // pass raw cse_*; retag here so all callers can pass whatever they hold. // Idempotent for v1's session_* and bridgeMain's pre-converted compatSessionId. const compatId = toCompatSessionId(sessionId) const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${compatId}` logForDebugging(`[bridge] Updating session title: ${compatId} → ${title}`)
try { const response = await axios.patch( url, { title }, { headers, timeout: 10_000, validateStatus: s => s < 500 }, )
if (response.status === 200) { logForDebugging(`[bridge] Session title updated successfully`) } else { const detail = extractErrorDetail(response.data) logForDebugging( `[bridge] Session title update failed with status ${response.status}${detail ? `: ${detail}` : ''}`, ) } } catch (err: unknown) { logForDebugging( `[bridge] Session title update request failed: ${errorMessage(err)}`, ) }}