/** Default per-session timeout (24 hours). */ export const DEFAULT_SESSION_TIMEOUT_MS = 24 * 60 * 60 * 1000 /** Reusable login guidance appended to bridge auth errors. */ export const BRIDGE_LOGIN_INSTRUCTION = 'Remote Control is only available with claude.ai subscriptions. Please use `/login` to sign in with your claude.ai account.' /** Full error printed when `claude remote-control` is run without auth. */ export const BRIDGE_LOGIN_ERROR = 'Error: You must be logged in to use Remote Control.\n\n' + BRIDGE_LOGIN_INSTRUCTION /** Shown when the user disconnects Remote Control (via /remote-control or ultraplan launch). */ export const REMOTE_CONTROL_DISCONNECTED_MSG = 'Remote Control disconnected.' // --- Protocol types for the environments API --- export type WorkData = { type: 'session' | 'healthcheck' id: string } export type WorkResponse = { id: string type: 'work' environment_id: string state: string data: WorkData secret: string // base64url-encoded JSON created_at: string } export type WorkSecret = { version: number session_ingress_token: string api_base_url: string sources: Array<{ type: string git_info?: { type: string; repo: string; ref?: string; token?: string } }> auth: Array<{ type: string; token: string }> claude_code_args?: Record | null mcp_config?: unknown | null environment_variables?: Record | null /** * Server-driven CCR v2 selector. Set by prepare_work_secret() when the * session was created via the v2 compat layer (ccr_v2_compat_enabled). * Same field the BYOC runner reads at environment-runner/sessionExecutor.ts. */ use_code_sessions?: boolean } export type SessionDoneStatus = 'completed' | 'failed' | 'interrupted' export type SessionActivityType = 'tool_start' | 'text' | 'result' | 'error' export type SessionActivity = { type: SessionActivityType summary: string // e.g. "Editing src/foo.ts", "Reading package.json" timestamp: number } /** * How `claude remote-control` chooses session working directories. * - `single-session`: one session in cwd, bridge tears down when it ends * - `worktree`: persistent server, every session gets an isolated git worktree * - `same-dir`: persistent server, every session shares cwd (can stomp each other) */ export type SpawnMode = 'single-session' | 'worktree' | 'same-dir' /** * Well-known worker_type values THIS codebase produces. Sent as * `metadata.worker_type` at environment registration so claude.ai can filter * the session picker by origin (e.g. assistant tab only shows assistant * workers). The backend treats this as an opaque string — desktop cowork * sends `"cowork"`, which isn't in this union. REPL code uses this narrow * type for its own exhaustiveness; wire-level fields accept any string. */ export type BridgeWorkerType = 'claude_code' | 'claude_code_assistant' export type BridgeConfig = { dir: string machineName: string branch: string gitRepoUrl: string | null maxSessions: number spawnMode: SpawnMode verbose: boolean sandbox: boolean /** Client-generated UUID identifying this bridge instance. */ bridgeId: string /** * Sent as metadata.worker_type so web clients can filter by origin. * Backend treats this as opaque — any string, not just BridgeWorkerType. */ workerType: string /** Client-generated UUID for idempotent environment registration. */ environmentId: string /** * Backend-issued environment_id to reuse on re-register. When set, the * backend treats registration as a reconnect to the existing environment * instead of creating a new one. Used by `claude remote-control * --session-id` resume. Must be a backend-format ID — client UUIDs are * rejected with 400. */ reuseEnvironmentId?: string /** API base URL the bridge is connected to (used for polling). */ apiBaseUrl: string /** Session ingress base URL for WebSocket connections (may differ from apiBaseUrl locally). */ sessionIngressUrl: string /** Debug file path passed via --debug-file. */ debugFile?: string /** Per-session timeout in milliseconds. Sessions exceeding this are killed. */ sessionTimeoutMs?: number } // --- Dependency interfaces (for testability) --- /** * A control_response event sent back to a session (e.g. a permission decision). * The `subtype` is `'success'` per the SDK protocol; the inner `response` * carries the permission decision payload (e.g. `{ behavior: 'allow' }`). */ export type PermissionResponseEvent = { type: 'control_response' response: { subtype: 'success' request_id: string response: Record } } export type BridgeApiClient = { registerBridgeEnvironment(config: BridgeConfig): Promise<{ environment_id: string environment_secret: string }> pollForWork( environmentId: string, environmentSecret: string, signal?: AbortSignal, reclaimOlderThanMs?: number, ): Promise acknowledgeWork( environmentId: string, workId: string, sessionToken: string, ): Promise /** Stop a work item via the environments API. */ stopWork(environmentId: string, workId: string, force: boolean): Promise /** Deregister/delete the bridge environment on graceful shutdown. */ deregisterEnvironment(environmentId: string): Promise /** Send a permission response (control_response) to a session via the session events API. */ sendPermissionResponseEvent( sessionId: string, event: PermissionResponseEvent, sessionToken: string, ): Promise /** Archive a session so it no longer appears as active on the server. */ archiveSession(sessionId: string): Promise /** * Force-stop stale worker instances and re-queue a session on an environment. * Used by `--session-id` to resume a session after the original bridge died. */ reconnectSession(environmentId: string, sessionId: string): Promise /** * Send a lightweight heartbeat for an active work item, extending its lease. * Uses SessionIngressAuth (JWT, no DB hit) instead of EnvironmentSecretAuth. * Returns the server's response with lease status. */ heartbeatWork( environmentId: string, workId: string, sessionToken: string, ): Promise<{ lease_extended: boolean; state: string }> } export type SessionHandle = { sessionId: string done: Promise kill(): void forceKill(): void activities: SessionActivity[] // ring buffer of recent activities (last ~10) currentActivity: SessionActivity | null // most recent accessToken: string // session_ingress_token for API calls lastStderr: string[] // ring buffer of last stderr lines writeStdin(data: string): void // write directly to child stdin /** Update the access token for a running session (e.g. after token refresh). */ updateAccessToken(token: string): void } export type SessionSpawnOpts = { sessionId: string sdkUrl: string accessToken: string /** When true, spawn the child with CCR v2 env vars (SSE transport + CCRClient). */ useCcrV2?: boolean /** Required when useCcrV2 is true. Obtained from POST /worker/register. */ workerEpoch?: number /** * Fires once with the text of the first real user message seen on the * child's stdout (via --replay-user-messages). Lets the caller derive a * session title when none exists yet. Tool-result and synthetic user * messages are skipped. */ onFirstUserMessage?: (text: string) => void } export type SessionSpawner = { spawn(opts: SessionSpawnOpts, dir: string): SessionHandle } export type BridgeLogger = { printBanner(config: BridgeConfig, environmentId: string): void logSessionStart(sessionId: string, prompt: string): void logSessionComplete(sessionId: string, durationMs: number): void logSessionFailed(sessionId: string, error: string): void logStatus(message: string): void logVerbose(message: string): void logError(message: string): void /** Log a reconnection success event after recovering from connection errors. */ logReconnected(disconnectedMs: number): void /** Show idle status with repo/branch info and shimmer animation. */ updateIdleStatus(): void /** Show reconnecting status in the live display. */ updateReconnectingStatus(delayStr: string, elapsedStr: string): void updateSessionStatus( sessionId: string, elapsed: string, activity: SessionActivity, trail: string[], ): void clearStatus(): void /** Set repository info for status line display. */ setRepoInfo(repoName: string, branch: string): void /** Set debug log glob shown above the status line (ant users). */ setDebugLogPath(path: string): void /** Transition to "Attached" state when a session starts. */ setAttached(sessionId: string): void /** Show failed status in the live display. */ updateFailedStatus(error: string): void /** Toggle QR code visibility. */ toggleQr(): void /** Update the " of sessions" indicator and spawn mode hint. */ updateSessionCount(active: number, max: number, mode: SpawnMode): void /** Update the spawn mode shown in the session-count line. Pass null to hide (single-session or toggle unavailable). */ setSpawnModeDisplay(mode: 'same-dir' | 'worktree' | null): void /** Register a new session for multi-session display (called after spawn succeeds). */ addSession(sessionId: string, url: string): void /** Update the per-session activity summary (tool being run) in the multi-session list. */ updateSessionActivity(sessionId: string, activity: SessionActivity): void /** * Set a session's display title. In multi-session mode, updates the bullet list * entry. In single-session mode, also shows the title in the main status line. * Triggers a render (guarded against reconnecting/failed states). */ setSessionTitle(sessionId: string, title: string): void /** Remove a session from the multi-session display when it ends. */ removeSession(sessionId: string): void /** Force a re-render of the status display (for multi-session activity refresh). */ refreshDisplay(): void }