Download src/bridge/createSession.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/bridge/createSession.ts
- Command line
-
hf download hf://chenbhao/codev/src/bridge/createSession.ts
-
curl -L -o createSession.ts https://huggingface.co/chenbhao/codev/resolve/main/src/bridge/createSession.ts
12.2 kB
| import 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)}`, | |
| ) | |
| } | |
| } | |