Download src/utils/teleport/api.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 13.3 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/utils/teleport/api.ts
- Command line
-
hf download hf://chenbhao/codev/src/utils/teleport/api.ts
-
curl -L -o api.ts https://huggingface.co/chenbhao/codev/resolve/main/src/utils/teleport/api.ts
13.3 kB
| import 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 requests | |
| const TELEPORT_RETRY_DELAYS = [2000, 4000, 8000, 16000] // 4 retries with exponential backoff | |
| const 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.py | |
| export 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.py | |
| export 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 schema | |
| export 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 | |
| } | |
| } | |