yzcode-cli 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/assistant/sessionHistory.ts +87 -0
  2. package/bootstrap/state.ts +1769 -0
  3. package/bridge/bridgeApi.ts +539 -0
  4. package/bridge/bridgeConfig.ts +48 -0
  5. package/bridge/bridgeDebug.ts +135 -0
  6. package/bridge/bridgeEnabled.ts +202 -0
  7. package/bridge/bridgeMain.ts +2999 -0
  8. package/bridge/bridgeMessaging.ts +461 -0
  9. package/bridge/bridgePermissionCallbacks.ts +43 -0
  10. package/bridge/bridgePointer.ts +210 -0
  11. package/bridge/bridgeStatusUtil.ts +163 -0
  12. package/bridge/bridgeUI.ts +530 -0
  13. package/bridge/capacityWake.ts +56 -0
  14. package/bridge/codeSessionApi.ts +168 -0
  15. package/bridge/createSession.ts +384 -0
  16. package/bridge/debugUtils.ts +141 -0
  17. package/bridge/envLessBridgeConfig.ts +165 -0
  18. package/bridge/flushGate.ts +71 -0
  19. package/bridge/inboundAttachments.ts +175 -0
  20. package/bridge/inboundMessages.ts +80 -0
  21. package/bridge/initReplBridge.ts +569 -0
  22. package/bridge/jwtUtils.ts +256 -0
  23. package/bridge/pollConfig.ts +110 -0
  24. package/bridge/pollConfigDefaults.ts +82 -0
  25. package/bridge/remoteBridgeCore.ts +1008 -0
  26. package/bridge/replBridge.ts +2406 -0
  27. package/bridge/replBridgeHandle.ts +36 -0
  28. package/bridge/replBridgeTransport.ts +370 -0
  29. package/bridge/sessionIdCompat.ts +57 -0
  30. package/bridge/sessionRunner.ts +550 -0
  31. package/bridge/trustedDevice.ts +210 -0
  32. package/bridge/types.ts +262 -0
  33. package/bridge/workSecret.ts +127 -0
  34. package/buddy/CompanionSprite.tsx +371 -0
  35. package/buddy/companion.ts +133 -0
  36. package/buddy/prompt.ts +36 -0
  37. package/buddy/sprites.ts +514 -0
  38. package/buddy/types.ts +148 -0
  39. package/buddy/useBuddyNotification.tsx +98 -0
  40. package/coordinator/coordinatorMode.ts +369 -0
  41. package/memdir/findRelevantMemories.ts +141 -0
  42. package/memdir/memdir.ts +507 -0
  43. package/memdir/memoryAge.ts +53 -0
  44. package/memdir/memoryScan.ts +94 -0
  45. package/memdir/memoryTypes.ts +271 -0
  46. package/memdir/paths.ts +278 -0
  47. package/memdir/teamMemPaths.ts +292 -0
  48. package/memdir/teamMemPrompts.ts +100 -0
  49. package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
  50. package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
  51. package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
  52. package/migrations/migrateFennecToOpus.ts +45 -0
  53. package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
  54. package/migrations/migrateOpusToOpus1m.ts +43 -0
  55. package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
  56. package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
  57. package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
  58. package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
  59. package/migrations/resetProToOpusDefault.ts +51 -0
  60. package/native-ts/color-diff/index.ts +999 -0
  61. package/native-ts/file-index/index.ts +370 -0
  62. package/native-ts/yoga-layout/enums.ts +134 -0
  63. package/native-ts/yoga-layout/index.ts +2578 -0
  64. package/outputStyles/loadOutputStylesDir.ts +98 -0
  65. package/package.json +22 -5
  66. package/plugins/builtinPlugins.ts +159 -0
  67. package/plugins/bundled/index.ts +23 -0
  68. package/schemas/hooks.ts +222 -0
  69. package/screens/Doctor.tsx +575 -0
  70. package/screens/REPL.tsx +5006 -0
  71. package/screens/ResumeConversation.tsx +399 -0
  72. package/server/createDirectConnectSession.ts +88 -0
  73. package/server/directConnectManager.ts +213 -0
  74. package/server/types.ts +57 -0
  75. package/skills/bundled/batch.ts +124 -0
  76. package/skills/bundled/claudeApi.ts +196 -0
  77. package/skills/bundled/claudeApiContent.ts +75 -0
  78. package/skills/bundled/claudeInChrome.ts +34 -0
  79. package/skills/bundled/debug.ts +103 -0
  80. package/skills/bundled/index.ts +79 -0
  81. package/skills/bundled/keybindings.ts +339 -0
  82. package/skills/bundled/loop.ts +92 -0
  83. package/skills/bundled/loremIpsum.ts +282 -0
  84. package/skills/bundled/remember.ts +82 -0
  85. package/skills/bundled/scheduleRemoteAgents.ts +447 -0
  86. package/skills/bundled/simplify.ts +69 -0
  87. package/skills/bundled/skillify.ts +197 -0
  88. package/skills/bundled/stuck.ts +79 -0
  89. package/skills/bundled/updateConfig.ts +475 -0
  90. package/skills/bundled/verify/SKILL.md +3 -0
  91. package/skills/bundled/verify/examples/cli.md +3 -0
  92. package/skills/bundled/verify/examples/server.md +3 -0
  93. package/skills/bundled/verify.ts +30 -0
  94. package/skills/bundled/verifyContent.ts +13 -0
  95. package/skills/bundledSkills.ts +220 -0
  96. package/skills/loadSkillsDir.ts +1086 -0
  97. package/skills/mcpSkillBuilders.ts +44 -0
  98. package/tasks/DreamTask/DreamTask.ts +157 -0
  99. package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
  100. package/tasks/InProcessTeammateTask/types.ts +121 -0
  101. package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
  102. package/tasks/LocalMainSessionTask.ts +479 -0
  103. package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
  104. package/tasks/LocalShellTask/guards.ts +41 -0
  105. package/tasks/LocalShellTask/killShellTasks.ts +76 -0
  106. package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
  107. package/tasks/pillLabel.ts +82 -0
  108. package/tasks/stopTask.ts +100 -0
  109. package/tasks/types.ts +46 -0
  110. package/upstreamproxy/relay.ts +455 -0
  111. package/upstreamproxy/upstreamproxy.ts +285 -0
  112. package/vim/motions.ts +82 -0
  113. package/vim/operators.ts +556 -0
  114. package/vim/textObjects.ts +186 -0
  115. package/vim/transitions.ts +490 -0
  116. package/vim/types.ts +199 -0
  117. package/voice/voiceModeEnabled.ts +54 -0
@@ -0,0 +1,539 @@
1
+ import axios from 'axios'
2
+
3
+ import { debugBody, extractErrorDetail } from './debugUtils.js'
4
+ import {
5
+ BRIDGE_LOGIN_INSTRUCTION,
6
+ type BridgeApiClient,
7
+ type BridgeConfig,
8
+ type PermissionResponseEvent,
9
+ type WorkResponse,
10
+ } from './types.js'
11
+
12
+ type BridgeApiDeps = {
13
+ baseUrl: string
14
+ getAccessToken: () => string | undefined
15
+ runnerVersion: string
16
+ onDebug?: (msg: string) => void
17
+ /**
18
+ * Called on 401 to attempt OAuth token refresh. Returns true if refreshed,
19
+ * in which case the request is retried once. Injected because
20
+ * handleOAuth401Error from utils/auth.ts transitively pulls in config.ts →
21
+ * file.ts → permissions/filesystem.ts → sessionStorage.ts → commands.ts
22
+ * (~1300 modules). Daemon callers using env-var tokens omit this — their
23
+ * tokens don't refresh, so 401 goes straight to BridgeFatalError.
24
+ */
25
+ onAuth401?: (staleAccessToken: string) => Promise<boolean>
26
+ /**
27
+ * Returns the trusted device token to send as X-Trusted-Device-Token on
28
+ * bridge API calls. Bridge sessions have SecurityTier=ELEVATED on the
29
+ * server (CCR v2); when the server's enforcement flag is on,
30
+ * ConnectBridgeWorker requires a trusted device at JWT-issuance.
31
+ * Optional — when absent or returning undefined, the header is omitted
32
+ * and the server falls through to its flag-off/no-op path. The CLI-side
33
+ * gate is tengu_sessions_elevated_auth_enforcement (see trustedDevice.ts).
34
+ */
35
+ getTrustedDeviceToken?: () => string | undefined
36
+ }
37
+
38
+ const BETA_HEADER = 'environments-2025-11-01'
39
+
40
+ /** Allowlist pattern for server-provided IDs used in URL path segments. */
41
+ const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/
42
+
43
+ /**
44
+ * Validate that a server-provided ID is safe to interpolate into a URL path.
45
+ * Prevents path traversal (e.g. `../../admin`) and injection via IDs that
46
+ * contain slashes, dots, or other special characters.
47
+ */
48
+ export function validateBridgeId(id: string, label: string): string {
49
+ if (!id || !SAFE_ID_PATTERN.test(id)) {
50
+ throw new Error(`Invalid ${label}: contains unsafe characters`)
51
+ }
52
+ return id
53
+ }
54
+
55
+ /** Fatal bridge errors that should not be retried (e.g. auth failures). */
56
+ export class BridgeFatalError extends Error {
57
+ readonly status: number
58
+ /** Server-provided error type, e.g. "environment_expired". */
59
+ readonly errorType: string | undefined
60
+ constructor(message: string, status: number, errorType?: string) {
61
+ super(message)
62
+ this.name = 'BridgeFatalError'
63
+ this.status = status
64
+ this.errorType = errorType
65
+ }
66
+ }
67
+
68
+ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
69
+ function debug(msg: string): void {
70
+ deps.onDebug?.(msg)
71
+ }
72
+
73
+ let consecutiveEmptyPolls = 0
74
+ const EMPTY_POLL_LOG_INTERVAL = 100
75
+
76
+ function getHeaders(accessToken: string): Record<string, string> {
77
+ const headers: Record<string, string> = {
78
+ Authorization: `Bearer ${accessToken}`,
79
+ 'Content-Type': 'application/json',
80
+ 'anthropic-version': '2023-06-01',
81
+ 'anthropic-beta': BETA_HEADER,
82
+ 'x-environment-runner-version': deps.runnerVersion,
83
+ }
84
+ const deviceToken = deps.getTrustedDeviceToken?.()
85
+ if (deviceToken) {
86
+ headers['X-Trusted-Device-Token'] = deviceToken
87
+ }
88
+ return headers
89
+ }
90
+
91
+ function resolveAuth(): string {
92
+ const accessToken = deps.getAccessToken()
93
+ if (!accessToken) {
94
+ throw new Error(BRIDGE_LOGIN_INSTRUCTION)
95
+ }
96
+ return accessToken
97
+ }
98
+
99
+ /**
100
+ * Execute an OAuth-authenticated request with a single retry on 401.
101
+ * On 401, attempts token refresh via handleOAuth401Error (same pattern as
102
+ * withRetry.ts for v1/messages). If refresh succeeds, retries the request
103
+ * once with the new token. If refresh fails or the retry also returns 401,
104
+ * the 401 response is returned for handleErrorStatus to throw BridgeFatalError.
105
+ */
106
+ async function withOAuthRetry<T>(
107
+ fn: (accessToken: string) => Promise<{ status: number; data: T }>,
108
+ context: string,
109
+ ): Promise<{ status: number; data: T }> {
110
+ const accessToken = resolveAuth()
111
+ const response = await fn(accessToken)
112
+
113
+ if (response.status !== 401) {
114
+ return response
115
+ }
116
+
117
+ if (!deps.onAuth401) {
118
+ debug(`[bridge:api] ${context}: 401 received, no refresh handler`)
119
+ return response
120
+ }
121
+
122
+ // Attempt token refresh — matches the pattern in withRetry.ts
123
+ debug(`[bridge:api] ${context}: 401 received, attempting token refresh`)
124
+ const refreshed = await deps.onAuth401(accessToken)
125
+ if (refreshed) {
126
+ debug(`[bridge:api] ${context}: Token refreshed, retrying request`)
127
+ const newToken = resolveAuth()
128
+ const retryResponse = await fn(newToken)
129
+ if (retryResponse.status !== 401) {
130
+ return retryResponse
131
+ }
132
+ debug(`[bridge:api] ${context}: Retry after refresh also got 401`)
133
+ } else {
134
+ debug(`[bridge:api] ${context}: Token refresh failed`)
135
+ }
136
+
137
+ // Refresh failed — return 401 for handleErrorStatus to throw
138
+ return response
139
+ }
140
+
141
+ return {
142
+ async registerBridgeEnvironment(
143
+ config: BridgeConfig,
144
+ ): Promise<{ environment_id: string; environment_secret: string }> {
145
+ debug(
146
+ `[bridge:api] POST /v1/environments/bridge bridgeId=${config.bridgeId}`,
147
+ )
148
+
149
+ const response = await withOAuthRetry(
150
+ (token: string) =>
151
+ axios.post<{
152
+ environment_id: string
153
+ environment_secret: string
154
+ }>(
155
+ `${deps.baseUrl}/v1/environments/bridge`,
156
+ {
157
+ machine_name: config.machineName,
158
+ directory: config.dir,
159
+ branch: config.branch,
160
+ git_repo_url: config.gitRepoUrl,
161
+ // Advertise session capacity so claude.ai/code can show
162
+ // "2/4 sessions" badges and only block the picker when
163
+ // actually at capacity. Backends that don't yet accept
164
+ // this field will silently ignore it.
165
+ max_sessions: config.maxSessions,
166
+ // worker_type lets claude.ai filter environments by origin
167
+ // (e.g. assistant picker only shows assistant-mode workers).
168
+ // Desktop cowork app sends "cowork"; we send a distinct value.
169
+ metadata: { worker_type: config.workerType },
170
+ // Idempotent re-registration: if we have a backend-issued
171
+ // environment_id from a prior session (--session-id resume),
172
+ // send it back so the backend reattaches instead of creating
173
+ // a new env. The backend may still hand back a fresh ID if
174
+ // the old one expired — callers must compare the response.
175
+ ...(config.reuseEnvironmentId && {
176
+ environment_id: config.reuseEnvironmentId,
177
+ }),
178
+ },
179
+ {
180
+ headers: getHeaders(token),
181
+ timeout: 15_000,
182
+ validateStatus: status => status < 500,
183
+ },
184
+ ),
185
+ 'Registration',
186
+ )
187
+
188
+ handleErrorStatus(response.status, response.data, 'Registration')
189
+ debug(
190
+ `[bridge:api] POST /v1/environments/bridge -> ${response.status} environment_id=${response.data.environment_id}`,
191
+ )
192
+ debug(
193
+ `[bridge:api] >>> ${debugBody({ machine_name: config.machineName, directory: config.dir, branch: config.branch, git_repo_url: config.gitRepoUrl, max_sessions: config.maxSessions, metadata: { worker_type: config.workerType } })}`,
194
+ )
195
+ debug(`[bridge:api] <<< ${debugBody(response.data)}`)
196
+ return response.data
197
+ },
198
+
199
+ async pollForWork(
200
+ environmentId: string,
201
+ environmentSecret: string,
202
+ signal?: AbortSignal,
203
+ reclaimOlderThanMs?: number,
204
+ ): Promise<WorkResponse | null> {
205
+ validateBridgeId(environmentId, 'environmentId')
206
+
207
+ // Save and reset so errors break the "consecutive empty" streak.
208
+ // Restored below when the response is truly empty.
209
+ const prevEmptyPolls = consecutiveEmptyPolls
210
+ consecutiveEmptyPolls = 0
211
+
212
+ const response = await axios.get<WorkResponse | null>(
213
+ `${deps.baseUrl}/v1/environments/${environmentId}/work/poll`,
214
+ {
215
+ headers: getHeaders(environmentSecret),
216
+ params:
217
+ reclaimOlderThanMs !== undefined
218
+ ? { reclaim_older_than_ms: reclaimOlderThanMs }
219
+ : undefined,
220
+ timeout: 10_000,
221
+ signal,
222
+ validateStatus: status => status < 500,
223
+ },
224
+ )
225
+
226
+ handleErrorStatus(response.status, response.data, 'Poll')
227
+
228
+ // Empty body or null = no work available
229
+ if (!response.data) {
230
+ consecutiveEmptyPolls = prevEmptyPolls + 1
231
+ if (
232
+ consecutiveEmptyPolls === 1 ||
233
+ consecutiveEmptyPolls % EMPTY_POLL_LOG_INTERVAL === 0
234
+ ) {
235
+ debug(
236
+ `[bridge:api] GET .../work/poll -> ${response.status} (no work, ${consecutiveEmptyPolls} consecutive empty polls)`,
237
+ )
238
+ }
239
+ return null
240
+ }
241
+
242
+ debug(
243
+ `[bridge:api] GET .../work/poll -> ${response.status} workId=${response.data.id} type=${response.data.data?.type}${response.data.data?.id ? ` sessionId=${response.data.data.id}` : ''}`,
244
+ )
245
+ debug(`[bridge:api] <<< ${debugBody(response.data)}`)
246
+ return response.data
247
+ },
248
+
249
+ async acknowledgeWork(
250
+ environmentId: string,
251
+ workId: string,
252
+ sessionToken: string,
253
+ ): Promise<void> {
254
+ validateBridgeId(environmentId, 'environmentId')
255
+ validateBridgeId(workId, 'workId')
256
+
257
+ debug(`[bridge:api] POST .../work/${workId}/ack`)
258
+
259
+ const response = await axios.post(
260
+ `${deps.baseUrl}/v1/environments/${environmentId}/work/${workId}/ack`,
261
+ {},
262
+ {
263
+ headers: getHeaders(sessionToken),
264
+ timeout: 10_000,
265
+ validateStatus: s => s < 500,
266
+ },
267
+ )
268
+
269
+ handleErrorStatus(response.status, response.data, 'Acknowledge')
270
+ debug(`[bridge:api] POST .../work/${workId}/ack -> ${response.status}`)
271
+ },
272
+
273
+ async stopWork(
274
+ environmentId: string,
275
+ workId: string,
276
+ force: boolean,
277
+ ): Promise<void> {
278
+ validateBridgeId(environmentId, 'environmentId')
279
+ validateBridgeId(workId, 'workId')
280
+
281
+ debug(`[bridge:api] POST .../work/${workId}/stop force=${force}`)
282
+
283
+ const response = await withOAuthRetry(
284
+ (token: string) =>
285
+ axios.post(
286
+ `${deps.baseUrl}/v1/environments/${environmentId}/work/${workId}/stop`,
287
+ { force },
288
+ {
289
+ headers: getHeaders(token),
290
+ timeout: 10_000,
291
+ validateStatus: s => s < 500,
292
+ },
293
+ ),
294
+ 'StopWork',
295
+ )
296
+
297
+ handleErrorStatus(response.status, response.data, 'StopWork')
298
+ debug(`[bridge:api] POST .../work/${workId}/stop -> ${response.status}`)
299
+ },
300
+
301
+ async deregisterEnvironment(environmentId: string): Promise<void> {
302
+ validateBridgeId(environmentId, 'environmentId')
303
+
304
+ debug(`[bridge:api] DELETE /v1/environments/bridge/${environmentId}`)
305
+
306
+ const response = await withOAuthRetry(
307
+ (token: string) =>
308
+ axios.delete(
309
+ `${deps.baseUrl}/v1/environments/bridge/${environmentId}`,
310
+ {
311
+ headers: getHeaders(token),
312
+ timeout: 10_000,
313
+ validateStatus: s => s < 500,
314
+ },
315
+ ),
316
+ 'Deregister',
317
+ )
318
+
319
+ handleErrorStatus(response.status, response.data, 'Deregister')
320
+ debug(
321
+ `[bridge:api] DELETE /v1/environments/bridge/${environmentId} -> ${response.status}`,
322
+ )
323
+ },
324
+
325
+ async archiveSession(sessionId: string): Promise<void> {
326
+ validateBridgeId(sessionId, 'sessionId')
327
+
328
+ debug(`[bridge:api] POST /v1/sessions/${sessionId}/archive`)
329
+
330
+ const response = await withOAuthRetry(
331
+ (token: string) =>
332
+ axios.post(
333
+ `${deps.baseUrl}/v1/sessions/${sessionId}/archive`,
334
+ {},
335
+ {
336
+ headers: getHeaders(token),
337
+ timeout: 10_000,
338
+ validateStatus: s => s < 500,
339
+ },
340
+ ),
341
+ 'ArchiveSession',
342
+ )
343
+
344
+ // 409 = already archived (idempotent, not an error)
345
+ if (response.status === 409) {
346
+ debug(
347
+ `[bridge:api] POST /v1/sessions/${sessionId}/archive -> 409 (already archived)`,
348
+ )
349
+ return
350
+ }
351
+
352
+ handleErrorStatus(response.status, response.data, 'ArchiveSession')
353
+ debug(
354
+ `[bridge:api] POST /v1/sessions/${sessionId}/archive -> ${response.status}`,
355
+ )
356
+ },
357
+
358
+ async reconnectSession(
359
+ environmentId: string,
360
+ sessionId: string,
361
+ ): Promise<void> {
362
+ validateBridgeId(environmentId, 'environmentId')
363
+ validateBridgeId(sessionId, 'sessionId')
364
+
365
+ debug(
366
+ `[bridge:api] POST /v1/environments/${environmentId}/bridge/reconnect session_id=${sessionId}`,
367
+ )
368
+
369
+ const response = await withOAuthRetry(
370
+ (token: string) =>
371
+ axios.post(
372
+ `${deps.baseUrl}/v1/environments/${environmentId}/bridge/reconnect`,
373
+ { session_id: sessionId },
374
+ {
375
+ headers: getHeaders(token),
376
+ timeout: 10_000,
377
+ validateStatus: s => s < 500,
378
+ },
379
+ ),
380
+ 'ReconnectSession',
381
+ )
382
+
383
+ handleErrorStatus(response.status, response.data, 'ReconnectSession')
384
+ debug(`[bridge:api] POST .../bridge/reconnect -> ${response.status}`)
385
+ },
386
+
387
+ async heartbeatWork(
388
+ environmentId: string,
389
+ workId: string,
390
+ sessionToken: string,
391
+ ): Promise<{ lease_extended: boolean; state: string }> {
392
+ validateBridgeId(environmentId, 'environmentId')
393
+ validateBridgeId(workId, 'workId')
394
+
395
+ debug(`[bridge:api] POST .../work/${workId}/heartbeat`)
396
+
397
+ const response = await axios.post<{
398
+ lease_extended: boolean
399
+ state: string
400
+ last_heartbeat: string
401
+ ttl_seconds: number
402
+ }>(
403
+ `${deps.baseUrl}/v1/environments/${environmentId}/work/${workId}/heartbeat`,
404
+ {},
405
+ {
406
+ headers: getHeaders(sessionToken),
407
+ timeout: 10_000,
408
+ validateStatus: s => s < 500,
409
+ },
410
+ )
411
+
412
+ handleErrorStatus(response.status, response.data, 'Heartbeat')
413
+ debug(
414
+ `[bridge:api] POST .../work/${workId}/heartbeat -> ${response.status} lease_extended=${response.data.lease_extended} state=${response.data.state}`,
415
+ )
416
+ return response.data
417
+ },
418
+
419
+ async sendPermissionResponseEvent(
420
+ sessionId: string,
421
+ event: PermissionResponseEvent,
422
+ sessionToken: string,
423
+ ): Promise<void> {
424
+ validateBridgeId(sessionId, 'sessionId')
425
+
426
+ debug(
427
+ `[bridge:api] POST /v1/sessions/${sessionId}/events type=${event.type}`,
428
+ )
429
+
430
+ const response = await axios.post(
431
+ `${deps.baseUrl}/v1/sessions/${sessionId}/events`,
432
+ { events: [event] },
433
+ {
434
+ headers: getHeaders(sessionToken),
435
+ timeout: 10_000,
436
+ validateStatus: s => s < 500,
437
+ },
438
+ )
439
+
440
+ handleErrorStatus(
441
+ response.status,
442
+ response.data,
443
+ 'SendPermissionResponseEvent',
444
+ )
445
+ debug(
446
+ `[bridge:api] POST /v1/sessions/${sessionId}/events -> ${response.status}`,
447
+ )
448
+ debug(`[bridge:api] >>> ${debugBody({ events: [event] })}`)
449
+ debug(`[bridge:api] <<< ${debugBody(response.data)}`)
450
+ },
451
+ }
452
+ }
453
+
454
+ function handleErrorStatus(
455
+ status: number,
456
+ data: unknown,
457
+ context: string,
458
+ ): void {
459
+ if (status === 200 || status === 204) {
460
+ return
461
+ }
462
+ const detail = extractErrorDetail(data)
463
+ const errorType = extractErrorTypeFromData(data)
464
+ switch (status) {
465
+ case 401:
466
+ throw new BridgeFatalError(
467
+ `${context}: Authentication failed (401)${detail ? `: ${detail}` : ''}. ${BRIDGE_LOGIN_INSTRUCTION}`,
468
+ 401,
469
+ errorType,
470
+ )
471
+ case 403:
472
+ throw new BridgeFatalError(
473
+ isExpiredErrorType(errorType)
474
+ ? 'Remote Control session has expired. Please restart with `claude remote-control` or /remote-control.'
475
+ : `${context}: Access denied (403)${detail ? `: ${detail}` : ''}. Check your organization permissions.`,
476
+ 403,
477
+ errorType,
478
+ )
479
+ case 404:
480
+ throw new BridgeFatalError(
481
+ detail ??
482
+ `${context}: Not found (404). Remote Control may not be available for this organization.`,
483
+ 404,
484
+ errorType,
485
+ )
486
+ case 410:
487
+ throw new BridgeFatalError(
488
+ detail ??
489
+ 'Remote Control session has expired. Please restart with `claude remote-control` or /remote-control.',
490
+ 410,
491
+ errorType ?? 'environment_expired',
492
+ )
493
+ case 429:
494
+ throw new Error(`${context}: Rate limited (429). Polling too frequently.`)
495
+ default:
496
+ throw new Error(
497
+ `${context}: Failed with status ${status}${detail ? `: ${detail}` : ''}`,
498
+ )
499
+ }
500
+ }
501
+
502
+ /** Check whether an error type string indicates a session/environment expiry. */
503
+ export function isExpiredErrorType(errorType: string | undefined): boolean {
504
+ if (!errorType) {
505
+ return false
506
+ }
507
+ return errorType.includes('expired') || errorType.includes('lifetime')
508
+ }
509
+
510
+ /**
511
+ * Check whether a BridgeFatalError is a suppressible 403 permission error.
512
+ * These are 403 errors for scopes like 'external_poll_sessions' or operations
513
+ * like StopWork that fail because the user's role lacks 'environments:manage'.
514
+ * They don't affect core functionality and shouldn't be shown to users.
515
+ */
516
+ export function isSuppressible403(err: BridgeFatalError): boolean {
517
+ if (err.status !== 403) {
518
+ return false
519
+ }
520
+ return (
521
+ err.message.includes('external_poll_sessions') ||
522
+ err.message.includes('environments:manage')
523
+ )
524
+ }
525
+
526
+ function extractErrorTypeFromData(data: unknown): string | undefined {
527
+ if (data && typeof data === 'object') {
528
+ if (
529
+ 'error' in data &&
530
+ data.error &&
531
+ typeof data.error === 'object' &&
532
+ 'type' in data.error &&
533
+ typeof data.error.type === 'string'
534
+ ) {
535
+ return data.error.type
536
+ }
537
+ }
538
+ return undefined
539
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Shared bridge auth/URL resolution. Consolidates the ant-only
3
+ * CLAUDE_BRIDGE_* dev overrides that were previously copy-pasted across
4
+ * a dozen files — inboundAttachments, BriefTool/upload, bridgeMain,
5
+ * initReplBridge, remoteBridgeCore, daemon workers, /rename,
6
+ * /remote-control.
7
+ *
8
+ * Two layers: *Override() returns the ant-only env var (or undefined);
9
+ * the non-Override versions fall through to the real OAuth store/config.
10
+ * Callers that compose with a different auth source (e.g. daemon workers
11
+ * using IPC auth) use the Override getters directly.
12
+ */
13
+
14
+ import { getOauthConfig } from '../constants/oauth.js'
15
+ import { getClaudeAIOAuthTokens } from '../utils/auth.js'
16
+
17
+ /** Ant-only dev override: CLAUDE_BRIDGE_OAUTH_TOKEN, else undefined. */
18
+ export function getBridgeTokenOverride(): string | undefined {
19
+ return (
20
+ (process.env.USER_TYPE === 'ant' &&
21
+ process.env.CLAUDE_BRIDGE_OAUTH_TOKEN) ||
22
+ undefined
23
+ )
24
+ }
25
+
26
+ /** Ant-only dev override: CLAUDE_BRIDGE_BASE_URL, else undefined. */
27
+ export function getBridgeBaseUrlOverride(): string | undefined {
28
+ return (
29
+ (process.env.USER_TYPE === 'ant' && process.env.CLAUDE_BRIDGE_BASE_URL) ||
30
+ undefined
31
+ )
32
+ }
33
+
34
+ /**
35
+ * Access token for bridge API calls: dev override first, then the OAuth
36
+ * keychain. Undefined means "not logged in".
37
+ */
38
+ export function getBridgeAccessToken(): string | undefined {
39
+ return getBridgeTokenOverride() ?? getClaudeAIOAuthTokens()?.accessToken
40
+ }
41
+
42
+ /**
43
+ * Base URL for bridge API calls: dev override first, then the production
44
+ * OAuth config. Always returns a URL.
45
+ */
46
+ export function getBridgeBaseUrl(): string {
47
+ return getBridgeBaseUrlOverride() ?? getOauthConfig().BASE_API_URL
48
+ }