types.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. /** Default per-session timeout (24 hours). */
  2. export const DEFAULT_SESSION_TIMEOUT_MS = 24 * 60 * 60 * 1000
  3. /** Reusable login guidance appended to bridge auth errors. */
  4. export const BRIDGE_LOGIN_INSTRUCTION =
  5. 'Remote Control is only available with claude.ai subscriptions. Please use `/login` to sign in with your claude.ai account.'
  6. /** Full error printed when `claude remote-control` is run without auth. */
  7. export const BRIDGE_LOGIN_ERROR =
  8. 'Error: You must be logged in to use Remote Control.\n\n' +
  9. BRIDGE_LOGIN_INSTRUCTION
  10. /** Shown when the user disconnects Remote Control (via /remote-control or ultraplan launch). */
  11. export const REMOTE_CONTROL_DISCONNECTED_MSG = 'Remote Control disconnected.'
  12. // --- Protocol types for the environments API ---
  13. export type WorkData = {
  14. type: 'session' | 'healthcheck'
  15. id: string
  16. }
  17. export type WorkResponse = {
  18. id: string
  19. type: 'work'
  20. environment_id: string
  21. state: string
  22. data: WorkData
  23. secret: string // base64url-encoded JSON
  24. created_at: string
  25. }
  26. export type WorkSecret = {
  27. version: number
  28. session_ingress_token: string
  29. api_base_url: string
  30. sources: Array<{
  31. type: string
  32. git_info?: { type: string; repo: string; ref?: string; token?: string }
  33. }>
  34. auth: Array<{ type: string; token: string }>
  35. claude_code_args?: Record<string, string> | null
  36. mcp_config?: unknown | null
  37. environment_variables?: Record<string, string> | null
  38. /**
  39. * Server-driven CCR v2 selector. Set by prepare_work_secret() when the
  40. * session was created via the v2 compat layer (ccr_v2_compat_enabled).
  41. * Same field the BYOC runner reads at environment-runner/sessionExecutor.ts.
  42. */
  43. use_code_sessions?: boolean
  44. }
  45. export type SessionDoneStatus = 'completed' | 'failed' | 'interrupted'
  46. export type SessionActivityType = 'tool_start' | 'text' | 'result' | 'error'
  47. export type SessionActivity = {
  48. type: SessionActivityType
  49. summary: string // e.g. "Editing src/foo.ts", "Reading package.json"
  50. timestamp: number
  51. }
  52. /**
  53. * How `claude remote-control` chooses session working directories.
  54. * - `single-session`: one session in cwd, bridge tears down when it ends
  55. * - `worktree`: persistent server, every session gets an isolated git worktree
  56. * - `same-dir`: persistent server, every session shares cwd (can stomp each other)
  57. */
  58. export type SpawnMode = 'single-session' | 'worktree' | 'same-dir'
  59. /**
  60. * Well-known worker_type values THIS codebase produces. Sent as
  61. * `metadata.worker_type` at environment registration so claude.ai can filter
  62. * the session picker by origin (e.g. assistant tab only shows assistant
  63. * workers). The backend treats this as an opaque string — desktop cowork
  64. * sends `"cowork"`, which isn't in this union. REPL code uses this narrow
  65. * type for its own exhaustiveness; wire-level fields accept any string.
  66. */
  67. export type BridgeWorkerType = 'claude_code' | 'claude_code_assistant'
  68. export type BridgeConfig = {
  69. dir: string
  70. machineName: string
  71. branch: string
  72. gitRepoUrl: string | null
  73. maxSessions: number
  74. spawnMode: SpawnMode
  75. verbose: boolean
  76. sandbox: boolean
  77. /** Client-generated UUID identifying this bridge instance. */
  78. bridgeId: string
  79. /**
  80. * Sent as metadata.worker_type so web clients can filter by origin.
  81. * Backend treats this as opaque — any string, not just BridgeWorkerType.
  82. */
  83. workerType: string
  84. /** Client-generated UUID for idempotent environment registration. */
  85. environmentId: string
  86. /**
  87. * Backend-issued environment_id to reuse on re-register. When set, the
  88. * backend treats registration as a reconnect to the existing environment
  89. * instead of creating a new one. Used by `claude remote-control
  90. * --session-id` resume. Must be a backend-format ID — client UUIDs are
  91. * rejected with 400.
  92. */
  93. reuseEnvironmentId?: string
  94. /** API base URL the bridge is connected to (used for polling). */
  95. apiBaseUrl: string
  96. /** Session ingress base URL for WebSocket connections (may differ from apiBaseUrl locally). */
  97. sessionIngressUrl: string
  98. /** Debug file path passed via --debug-file. */
  99. debugFile?: string
  100. /** Per-session timeout in milliseconds. Sessions exceeding this are killed. */
  101. sessionTimeoutMs?: number
  102. }
  103. // --- Dependency interfaces (for testability) ---
  104. /**
  105. * A control_response event sent back to a session (e.g. a permission decision).
  106. * The `subtype` is `'success'` per the SDK protocol; the inner `response`
  107. * carries the permission decision payload (e.g. `{ behavior: 'allow' }`).
  108. */
  109. export type PermissionResponseEvent = {
  110. type: 'control_response'
  111. response: {
  112. subtype: 'success'
  113. request_id: string
  114. response: Record<string, unknown>
  115. }
  116. }
  117. export type BridgeApiClient = {
  118. registerBridgeEnvironment(config: BridgeConfig): Promise<{
  119. environment_id: string
  120. environment_secret: string
  121. }>
  122. pollForWork(
  123. environmentId: string,
  124. environmentSecret: string,
  125. signal?: AbortSignal,
  126. reclaimOlderThanMs?: number,
  127. ): Promise<WorkResponse | null>
  128. acknowledgeWork(
  129. environmentId: string,
  130. workId: string,
  131. sessionToken: string,
  132. ): Promise<void>
  133. /** Stop a work item via the environments API. */
  134. stopWork(environmentId: string, workId: string, force: boolean): Promise<void>
  135. /** Deregister/delete the bridge environment on graceful shutdown. */
  136. deregisterEnvironment(environmentId: string): Promise<void>
  137. /** Send a permission response (control_response) to a session via the session events API. */
  138. sendPermissionResponseEvent(
  139. sessionId: string,
  140. event: PermissionResponseEvent,
  141. sessionToken: string,
  142. ): Promise<void>
  143. /** Archive a session so it no longer appears as active on the server. */
  144. archiveSession(sessionId: string): Promise<void>
  145. /**
  146. * Force-stop stale worker instances and re-queue a session on an environment.
  147. * Used by `--session-id` to resume a session after the original bridge died.
  148. */
  149. reconnectSession(environmentId: string, sessionId: string): Promise<void>
  150. /**
  151. * Send a lightweight heartbeat for an active work item, extending its lease.
  152. * Uses SessionIngressAuth (JWT, no DB hit) instead of EnvironmentSecretAuth.
  153. * Returns the server's response with lease status.
  154. */
  155. heartbeatWork(
  156. environmentId: string,
  157. workId: string,
  158. sessionToken: string,
  159. ): Promise<{ lease_extended: boolean; state: string }>
  160. }
  161. export type SessionHandle = {
  162. sessionId: string
  163. done: Promise<SessionDoneStatus>
  164. kill(): void
  165. forceKill(): void
  166. activities: SessionActivity[] // ring buffer of recent activities (last ~10)
  167. currentActivity: SessionActivity | null // most recent
  168. accessToken: string // session_ingress_token for API calls
  169. lastStderr: string[] // ring buffer of last stderr lines
  170. writeStdin(data: string): void // write directly to child stdin
  171. /** Update the access token for a running session (e.g. after token refresh). */
  172. updateAccessToken(token: string): void
  173. }
  174. export type SessionSpawnOpts = {
  175. sessionId: string
  176. sdkUrl: string
  177. accessToken: string
  178. /** When true, spawn the child with CCR v2 env vars (SSE transport + CCRClient). */
  179. useCcrV2?: boolean
  180. /** Required when useCcrV2 is true. Obtained from POST /worker/register. */
  181. workerEpoch?: number
  182. /**
  183. * Fires once with the text of the first real user message seen on the
  184. * child's stdout (via --replay-user-messages). Lets the caller derive a
  185. * session title when none exists yet. Tool-result and synthetic user
  186. * messages are skipped.
  187. */
  188. onFirstUserMessage?: (text: string) => void
  189. }
  190. export type SessionSpawner = {
  191. spawn(opts: SessionSpawnOpts, dir: string): SessionHandle
  192. }
  193. export type BridgeLogger = {
  194. printBanner(config: BridgeConfig, environmentId: string): void
  195. logSessionStart(sessionId: string, prompt: string): void
  196. logSessionComplete(sessionId: string, durationMs: number): void
  197. logSessionFailed(sessionId: string, error: string): void
  198. logStatus(message: string): void
  199. logVerbose(message: string): void
  200. logError(message: string): void
  201. /** Log a reconnection success event after recovering from connection errors. */
  202. logReconnected(disconnectedMs: number): void
  203. /** Show idle status with repo/branch info and shimmer animation. */
  204. updateIdleStatus(): void
  205. /** Show reconnecting status in the live display. */
  206. updateReconnectingStatus(delayStr: string, elapsedStr: string): void
  207. updateSessionStatus(
  208. sessionId: string,
  209. elapsed: string,
  210. activity: SessionActivity,
  211. trail: string[],
  212. ): void
  213. clearStatus(): void
  214. /** Set repository info for status line display. */
  215. setRepoInfo(repoName: string, branch: string): void
  216. /** Set debug log glob shown above the status line (ant users). */
  217. setDebugLogPath(path: string): void
  218. /** Transition to "Attached" state when a session starts. */
  219. setAttached(sessionId: string): void
  220. /** Show failed status in the live display. */
  221. updateFailedStatus(error: string): void
  222. /** Toggle QR code visibility. */
  223. toggleQr(): void
  224. /** Update the "<n> of <m> sessions" indicator and spawn mode hint. */
  225. updateSessionCount(active: number, max: number, mode: SpawnMode): void
  226. /** Update the spawn mode shown in the session-count line. Pass null to hide (single-session or toggle unavailable). */
  227. setSpawnModeDisplay(mode: 'same-dir' | 'worktree' | null): void
  228. /** Register a new session for multi-session display (called after spawn succeeds). */
  229. addSession(sessionId: string, url: string): void
  230. /** Update the per-session activity summary (tool being run) in the multi-session list. */
  231. updateSessionActivity(sessionId: string, activity: SessionActivity): void
  232. /**
  233. * Set a session's display title. In multi-session mode, updates the bullet list
  234. * entry. In single-session mode, also shows the title in the main status line.
  235. * Triggers a render (guarded against reconnecting/failed states).
  236. */
  237. setSessionTitle(sessionId: string, title: string): void
  238. /** Remove a session from the multi-session display when it ends. */
  239. removeSession(sessionId: string): void
  240. /** Force a re-render of the status display (for multi-session activity refresh). */
  241. refreshDisplay(): void
  242. }