source dump of claude code
at main 384 lines 12 kB view raw
1import type { SDKMessage } from '../entrypoints/agentSdkTypes.js' 2import { logForDebugging } from '../utils/debug.js' 3import { errorMessage } from '../utils/errors.js' 4import { extractErrorDetail } from './debugUtils.js' 5import { toCompatSessionId } from './sessionIdCompat.js' 6 7type GitSource = { 8 type: 'git_repository' 9 url: string 10 revision?: string 11} 12 13type GitOutcome = { 14 type: 'git_repository' 15 git_info: { type: 'github'; repo: string; branches: string[] } 16} 17 18// Events must be wrapped in { type: 'event', data: <sdk_message> } for the 19// POST /v1/sessions endpoint (discriminated union format). 20type SessionEvent = { 21 type: 'event' 22 data: SDKMessage 23} 24 25/** 26 * Create a session on a bridge environment via POST /v1/sessions. 27 * 28 * Used by both `claude remote-control` (empty session so the user has somewhere to 29 * type immediately) and `/remote-control` (session pre-populated with conversation 30 * history). 31 * 32 * Returns the session ID on success, or null if creation fails (non-fatal). 33 */ 34export async function createBridgeSession({ 35 environmentId, 36 title, 37 events, 38 gitRepoUrl, 39 branch, 40 signal, 41 baseUrl: baseUrlOverride, 42 getAccessToken, 43 permissionMode, 44}: { 45 environmentId: string 46 title?: string 47 events: SessionEvent[] 48 gitRepoUrl: string | null 49 branch: string 50 signal: AbortSignal 51 baseUrl?: string 52 getAccessToken?: () => string | undefined 53 permissionMode?: string 54}): Promise<string | null> { 55 const { getClaudeAIOAuthTokens } = await import('../utils/auth.js') 56 const { getOrganizationUUID } = await import('../services/oauth/client.js') 57 const { getOauthConfig } = await import('../constants/oauth.js') 58 const { getOAuthHeaders } = await import('../utils/teleport/api.js') 59 const { parseGitHubRepository } = await import('../utils/detectRepository.js') 60 const { getDefaultBranch } = await import('../utils/git.js') 61 const { getMainLoopModel } = await import('../utils/model/model.js') 62 const { default: axios } = await import('axios') 63 64 const accessToken = 65 getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken 66 if (!accessToken) { 67 logForDebugging('[bridge] No access token for session creation') 68 return null 69 } 70 71 const orgUUID = await getOrganizationUUID() 72 if (!orgUUID) { 73 logForDebugging('[bridge] No org UUID for session creation') 74 return null 75 } 76 77 // Build git source and outcome context 78 let gitSource: GitSource | null = null 79 let gitOutcome: GitOutcome | null = null 80 81 if (gitRepoUrl) { 82 const { parseGitRemote } = await import('../utils/detectRepository.js') 83 const parsed = parseGitRemote(gitRepoUrl) 84 if (parsed) { 85 const { host, owner, name } = parsed 86 const revision = branch || (await getDefaultBranch()) || undefined 87 gitSource = { 88 type: 'git_repository', 89 url: `https://${host}/${owner}/${name}`, 90 revision, 91 } 92 gitOutcome = { 93 type: 'git_repository', 94 git_info: { 95 type: 'github', 96 repo: `${owner}/${name}`, 97 branches: [`claude/${branch || 'task'}`], 98 }, 99 } 100 } else { 101 // Fallback: try parseGitHubRepository for owner/repo format 102 const ownerRepo = parseGitHubRepository(gitRepoUrl) 103 if (ownerRepo) { 104 const [owner, name] = ownerRepo.split('/') 105 if (owner && name) { 106 const revision = branch || (await getDefaultBranch()) || undefined 107 gitSource = { 108 type: 'git_repository', 109 url: `https://github.com/${owner}/${name}`, 110 revision, 111 } 112 gitOutcome = { 113 type: 'git_repository', 114 git_info: { 115 type: 'github', 116 repo: `${owner}/${name}`, 117 branches: [`claude/${branch || 'task'}`], 118 }, 119 } 120 } 121 } 122 } 123 } 124 125 const requestBody = { 126 ...(title !== undefined && { title }), 127 events, 128 session_context: { 129 sources: gitSource ? [gitSource] : [], 130 outcomes: gitOutcome ? [gitOutcome] : [], 131 model: getMainLoopModel(), 132 }, 133 environment_id: environmentId, 134 source: 'remote-control', 135 ...(permissionMode && { permission_mode: permissionMode }), 136 } 137 138 const headers = { 139 ...getOAuthHeaders(accessToken), 140 'anthropic-beta': 'ccr-byoc-2025-07-29', 141 'x-organization-uuid': orgUUID, 142 } 143 144 const url = `${baseUrlOverride ?? getOauthConfig().BASE_API_URL}/v1/sessions` 145 let response 146 try { 147 response = await axios.post(url, requestBody, { 148 headers, 149 signal, 150 validateStatus: s => s < 500, 151 }) 152 } catch (err: unknown) { 153 logForDebugging( 154 `[bridge] Session creation request failed: ${errorMessage(err)}`, 155 ) 156 return null 157 } 158 const isSuccess = response.status === 200 || response.status === 201 159 160 if (!isSuccess) { 161 const detail = extractErrorDetail(response.data) 162 logForDebugging( 163 `[bridge] Session creation failed with status ${response.status}${detail ? `: ${detail}` : ''}`, 164 ) 165 return null 166 } 167 168 const sessionData: unknown = response.data 169 if ( 170 !sessionData || 171 typeof sessionData !== 'object' || 172 !('id' in sessionData) || 173 typeof sessionData.id !== 'string' 174 ) { 175 logForDebugging('[bridge] No session ID in response') 176 return null 177 } 178 179 return sessionData.id 180} 181 182/** 183 * Fetch a bridge session via GET /v1/sessions/{id}. 184 * 185 * Returns the session's environment_id (for `--session-id` resume) and title. 186 * Uses the same org-scoped headers as create/archive — the environments-level 187 * client in bridgeApi.ts uses a different beta header and no org UUID, which 188 * makes the Sessions API return 404. 189 */ 190export async function getBridgeSession( 191 sessionId: string, 192 opts?: { baseUrl?: string; getAccessToken?: () => string | undefined }, 193): Promise<{ environment_id?: string; title?: string } | null> { 194 const { getClaudeAIOAuthTokens } = await import('../utils/auth.js') 195 const { getOrganizationUUID } = await import('../services/oauth/client.js') 196 const { getOauthConfig } = await import('../constants/oauth.js') 197 const { getOAuthHeaders } = await import('../utils/teleport/api.js') 198 const { default: axios } = await import('axios') 199 200 const accessToken = 201 opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken 202 if (!accessToken) { 203 logForDebugging('[bridge] No access token for session fetch') 204 return null 205 } 206 207 const orgUUID = await getOrganizationUUID() 208 if (!orgUUID) { 209 logForDebugging('[bridge] No org UUID for session fetch') 210 return null 211 } 212 213 const headers = { 214 ...getOAuthHeaders(accessToken), 215 'anthropic-beta': 'ccr-byoc-2025-07-29', 216 'x-organization-uuid': orgUUID, 217 } 218 219 const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}` 220 logForDebugging(`[bridge] Fetching session ${sessionId}`) 221 222 let response 223 try { 224 response = await axios.get<{ environment_id?: string; title?: string }>( 225 url, 226 { headers, timeout: 10_000, validateStatus: s => s < 500 }, 227 ) 228 } catch (err: unknown) { 229 logForDebugging( 230 `[bridge] Session fetch request failed: ${errorMessage(err)}`, 231 ) 232 return null 233 } 234 235 if (response.status !== 200) { 236 const detail = extractErrorDetail(response.data) 237 logForDebugging( 238 `[bridge] Session fetch failed with status ${response.status}${detail ? `: ${detail}` : ''}`, 239 ) 240 return null 241 } 242 243 return response.data 244} 245 246/** 247 * Archive a bridge session via POST /v1/sessions/{id}/archive. 248 * 249 * The CCR server never auto-archives sessions — archival is always an 250 * explicit client action. Both `claude remote-control` (standalone bridge) and the 251 * always-on `/remote-control` REPL bridge call this during shutdown to archive any 252 * sessions that are still alive. 253 * 254 * The archive endpoint accepts sessions in any status (running, idle, 255 * requires_action, pending) and returns 409 if already archived, making 256 * it safe to call even if the server-side runner already archived the 257 * session. 258 * 259 * Callers must handle errors — this function has no try/catch; 5xx, 260 * timeouts, and network errors throw. Archival is best-effort during 261 * cleanup; call sites wrap with .catch(). 262 */ 263export async function archiveBridgeSession( 264 sessionId: string, 265 opts?: { 266 baseUrl?: string 267 getAccessToken?: () => string | undefined 268 timeoutMs?: number 269 }, 270): Promise<void> { 271 const { getClaudeAIOAuthTokens } = await import('../utils/auth.js') 272 const { getOrganizationUUID } = await import('../services/oauth/client.js') 273 const { getOauthConfig } = await import('../constants/oauth.js') 274 const { getOAuthHeaders } = await import('../utils/teleport/api.js') 275 const { default: axios } = await import('axios') 276 277 const accessToken = 278 opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken 279 if (!accessToken) { 280 logForDebugging('[bridge] No access token for session archive') 281 return 282 } 283 284 const orgUUID = await getOrganizationUUID() 285 if (!orgUUID) { 286 logForDebugging('[bridge] No org UUID for session archive') 287 return 288 } 289 290 const headers = { 291 ...getOAuthHeaders(accessToken), 292 'anthropic-beta': 'ccr-byoc-2025-07-29', 293 'x-organization-uuid': orgUUID, 294 } 295 296 const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}/archive` 297 logForDebugging(`[bridge] Archiving session ${sessionId}`) 298 299 const response = await axios.post( 300 url, 301 {}, 302 { 303 headers, 304 timeout: opts?.timeoutMs ?? 10_000, 305 validateStatus: s => s < 500, 306 }, 307 ) 308 309 if (response.status === 200) { 310 logForDebugging(`[bridge] Session ${sessionId} archived successfully`) 311 } else { 312 const detail = extractErrorDetail(response.data) 313 logForDebugging( 314 `[bridge] Session archive failed with status ${response.status}${detail ? `: ${detail}` : ''}`, 315 ) 316 } 317} 318 319/** 320 * Update the title of a bridge session via PATCH /v1/sessions/{id}. 321 * 322 * Called when the user renames a session via /rename while a bridge 323 * connection is active, so the title stays in sync on claude.ai/code. 324 * 325 * Errors are swallowed — title sync is best-effort. 326 */ 327export async function updateBridgeSessionTitle( 328 sessionId: string, 329 title: string, 330 opts?: { baseUrl?: string; getAccessToken?: () => string | undefined }, 331): Promise<void> { 332 const { getClaudeAIOAuthTokens } = await import('../utils/auth.js') 333 const { getOrganizationUUID } = await import('../services/oauth/client.js') 334 const { getOauthConfig } = await import('../constants/oauth.js') 335 const { getOAuthHeaders } = await import('../utils/teleport/api.js') 336 const { default: axios } = await import('axios') 337 338 const accessToken = 339 opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken 340 if (!accessToken) { 341 logForDebugging('[bridge] No access token for session title update') 342 return 343 } 344 345 const orgUUID = await getOrganizationUUID() 346 if (!orgUUID) { 347 logForDebugging('[bridge] No org UUID for session title update') 348 return 349 } 350 351 const headers = { 352 ...getOAuthHeaders(accessToken), 353 'anthropic-beta': 'ccr-byoc-2025-07-29', 354 'x-organization-uuid': orgUUID, 355 } 356 357 // Compat gateway only accepts session_* (compat/convert.go:27). v2 callers 358 // pass raw cse_*; retag here so all callers can pass whatever they hold. 359 // Idempotent for v1's session_* and bridgeMain's pre-converted compatSessionId. 360 const compatId = toCompatSessionId(sessionId) 361 const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${compatId}` 362 logForDebugging(`[bridge] Updating session title: ${compatId}${title}`) 363 364 try { 365 const response = await axios.patch( 366 url, 367 { title }, 368 { headers, timeout: 10_000, validateStatus: s => s < 500 }, 369 ) 370 371 if (response.status === 200) { 372 logForDebugging(`[bridge] Session title updated successfully`) 373 } else { 374 const detail = extractErrorDetail(response.data) 375 logForDebugging( 376 `[bridge] Session title update failed with status ${response.status}${detail ? `: ${detail}` : ''}`, 377 ) 378 } 379 } catch (err: unknown) { 380 logForDebugging( 381 `[bridge] Session title update request failed: ${errorMessage(err)}`, 382 ) 383 } 384}