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,168 @@
1
+ /**
2
+ * Thin HTTP wrappers for the CCR v2 code-session API.
3
+ *
4
+ * Separate file from remoteBridgeCore.ts so the SDK /bridge subpath can
5
+ * export createCodeSession + fetchRemoteCredentials without bundling the
6
+ * heavy CLI tree (analytics, transport, etc.). Callers supply explicit
7
+ * accessToken + baseUrl — no implicit auth or config reads.
8
+ */
9
+
10
+ import axios from 'axios'
11
+ import { logForDebugging } from '../utils/debug.js'
12
+ import { errorMessage } from '../utils/errors.js'
13
+ import { jsonStringify } from '../utils/slowOperations.js'
14
+ import { extractErrorDetail } from './debugUtils.js'
15
+
16
+ const ANTHROPIC_VERSION = '2023-06-01'
17
+
18
+ function oauthHeaders(accessToken: string): Record<string, string> {
19
+ return {
20
+ Authorization: `Bearer ${accessToken}`,
21
+ 'Content-Type': 'application/json',
22
+ 'anthropic-version': ANTHROPIC_VERSION,
23
+ }
24
+ }
25
+
26
+ export async function createCodeSession(
27
+ baseUrl: string,
28
+ accessToken: string,
29
+ title: string,
30
+ timeoutMs: number,
31
+ tags?: string[],
32
+ ): Promise<string | null> {
33
+ const url = `${baseUrl}/v1/code/sessions`
34
+ let response
35
+ try {
36
+ response = await axios.post(
37
+ url,
38
+ // bridge: {} is the positive signal for the oneof runner — omitting it
39
+ // (or sending environment_id: "") now 400s. BridgeRunner is an empty
40
+ // message today; it's a placeholder for future bridge-specific options.
41
+ { title, bridge: {}, ...(tags?.length ? { tags } : {}) },
42
+ {
43
+ headers: oauthHeaders(accessToken),
44
+ timeout: timeoutMs,
45
+ validateStatus: s => s < 500,
46
+ },
47
+ )
48
+ } catch (err: unknown) {
49
+ logForDebugging(
50
+ `[code-session] Session create request failed: ${errorMessage(err)}`,
51
+ )
52
+ return null
53
+ }
54
+
55
+ if (response.status !== 200 && response.status !== 201) {
56
+ const detail = extractErrorDetail(response.data)
57
+ logForDebugging(
58
+ `[code-session] Session create failed ${response.status}${detail ? `: ${detail}` : ''}`,
59
+ )
60
+ return null
61
+ }
62
+
63
+ const data: unknown = response.data
64
+ if (
65
+ !data ||
66
+ typeof data !== 'object' ||
67
+ !('session' in data) ||
68
+ !data.session ||
69
+ typeof data.session !== 'object' ||
70
+ !('id' in data.session) ||
71
+ typeof data.session.id !== 'string' ||
72
+ !data.session.id.startsWith('cse_')
73
+ ) {
74
+ logForDebugging(
75
+ `[code-session] No session.id (cse_*) in response: ${jsonStringify(data).slice(0, 200)}`,
76
+ )
77
+ return null
78
+ }
79
+ return data.session.id
80
+ }
81
+
82
+ /**
83
+ * Credentials from POST /bridge. JWT is opaque — do not decode.
84
+ * Each /bridge call bumps worker_epoch server-side (it IS the register).
85
+ */
86
+ export type RemoteCredentials = {
87
+ worker_jwt: string
88
+ api_base_url: string
89
+ expires_in: number
90
+ worker_epoch: number
91
+ }
92
+
93
+ export async function fetchRemoteCredentials(
94
+ sessionId: string,
95
+ baseUrl: string,
96
+ accessToken: string,
97
+ timeoutMs: number,
98
+ trustedDeviceToken?: string,
99
+ ): Promise<RemoteCredentials | null> {
100
+ const url = `${baseUrl}/v1/code/sessions/${sessionId}/bridge`
101
+ const headers = oauthHeaders(accessToken)
102
+ if (trustedDeviceToken) {
103
+ headers['X-Trusted-Device-Token'] = trustedDeviceToken
104
+ }
105
+ let response
106
+ try {
107
+ response = await axios.post(
108
+ url,
109
+ {},
110
+ {
111
+ headers,
112
+ timeout: timeoutMs,
113
+ validateStatus: s => s < 500,
114
+ },
115
+ )
116
+ } catch (err: unknown) {
117
+ logForDebugging(
118
+ `[code-session] /bridge request failed: ${errorMessage(err)}`,
119
+ )
120
+ return null
121
+ }
122
+
123
+ if (response.status !== 200) {
124
+ const detail = extractErrorDetail(response.data)
125
+ logForDebugging(
126
+ `[code-session] /bridge failed ${response.status}${detail ? `: ${detail}` : ''}`,
127
+ )
128
+ return null
129
+ }
130
+
131
+ const data: unknown = response.data
132
+ if (
133
+ data === null ||
134
+ typeof data !== 'object' ||
135
+ !('worker_jwt' in data) ||
136
+ typeof data.worker_jwt !== 'string' ||
137
+ !('expires_in' in data) ||
138
+ typeof data.expires_in !== 'number' ||
139
+ !('api_base_url' in data) ||
140
+ typeof data.api_base_url !== 'string' ||
141
+ !('worker_epoch' in data)
142
+ ) {
143
+ logForDebugging(
144
+ `[code-session] /bridge response malformed (need worker_jwt, expires_in, api_base_url, worker_epoch): ${jsonStringify(data).slice(0, 200)}`,
145
+ )
146
+ return null
147
+ }
148
+ // protojson serializes int64 as a string to avoid JS precision loss;
149
+ // Go may also return a number depending on encoder settings.
150
+ const rawEpoch = data.worker_epoch
151
+ const epoch = typeof rawEpoch === 'string' ? Number(rawEpoch) : rawEpoch
152
+ if (
153
+ typeof epoch !== 'number' ||
154
+ !Number.isFinite(epoch) ||
155
+ !Number.isSafeInteger(epoch)
156
+ ) {
157
+ logForDebugging(
158
+ `[code-session] /bridge worker_epoch invalid: ${jsonStringify(rawEpoch)}`,
159
+ )
160
+ return null
161
+ }
162
+ return {
163
+ worker_jwt: data.worker_jwt,
164
+ api_base_url: data.api_base_url,
165
+ expires_in: data.expires_in,
166
+ worker_epoch: epoch,
167
+ }
168
+ }
@@ -0,0 +1,384 @@
1
+ import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
2
+ import { logForDebugging } from '../utils/debug.js'
3
+ import { errorMessage } from '../utils/errors.js'
4
+ import { extractErrorDetail } from './debugUtils.js'
5
+ import { toCompatSessionId } from './sessionIdCompat.js'
6
+
7
+ type GitSource = {
8
+ type: 'git_repository'
9
+ url: string
10
+ revision?: string
11
+ }
12
+
13
+ type GitOutcome = {
14
+ type: 'git_repository'
15
+ git_info: { type: 'github'; repo: string; branches: string[] }
16
+ }
17
+
18
+ // Events must be wrapped in { type: 'event', data: <sdk_message> } for the
19
+ // POST /v1/sessions endpoint (discriminated union format).
20
+ type SessionEvent = {
21
+ type: 'event'
22
+ data: SDKMessage
23
+ }
24
+
25
+ /**
26
+ * Create a session on a bridge environment via POST /v1/sessions.
27
+ *
28
+ * Used by both `claude remote-control` (empty session so the user has somewhere to
29
+ * type immediately) and `/remote-control` (session pre-populated with conversation
30
+ * history).
31
+ *
32
+ * Returns the session ID on success, or null if creation fails (non-fatal).
33
+ */
34
+ export async function createBridgeSession({
35
+ environmentId,
36
+ title,
37
+ events,
38
+ gitRepoUrl,
39
+ branch,
40
+ signal,
41
+ baseUrl: baseUrlOverride,
42
+ getAccessToken,
43
+ permissionMode,
44
+ }: {
45
+ environmentId: string
46
+ title?: string
47
+ events: SessionEvent[]
48
+ gitRepoUrl: string | null
49
+ branch: string
50
+ signal: AbortSignal
51
+ baseUrl?: string
52
+ getAccessToken?: () => string | undefined
53
+ permissionMode?: string
54
+ }): Promise<string | null> {
55
+ const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
56
+ const { getOrganizationUUID } = await import('../services/oauth/client.js')
57
+ const { getOauthConfig } = await import('../constants/oauth.js')
58
+ const { getOAuthHeaders } = await import('../utils/teleport/api.js')
59
+ const { parseGitHubRepository } = await import('../utils/detectRepository.js')
60
+ const { getDefaultBranch } = await import('../utils/git.js')
61
+ const { getMainLoopModel } = await import('../utils/model/model.js')
62
+ const { default: axios } = await import('axios')
63
+
64
+ const accessToken =
65
+ getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
66
+ if (!accessToken) {
67
+ logForDebugging('[bridge] No access token for session creation')
68
+ return null
69
+ }
70
+
71
+ const orgUUID = await getOrganizationUUID()
72
+ if (!orgUUID) {
73
+ logForDebugging('[bridge] No org UUID for session creation')
74
+ return null
75
+ }
76
+
77
+ // Build git source and outcome context
78
+ let gitSource: GitSource | null = null
79
+ let gitOutcome: GitOutcome | null = null
80
+
81
+ if (gitRepoUrl) {
82
+ const { parseGitRemote } = await import('../utils/detectRepository.js')
83
+ const parsed = parseGitRemote(gitRepoUrl)
84
+ if (parsed) {
85
+ const { host, owner, name } = parsed
86
+ const revision = branch || (await getDefaultBranch()) || undefined
87
+ gitSource = {
88
+ type: 'git_repository',
89
+ url: `https://${host}/${owner}/${name}`,
90
+ revision,
91
+ }
92
+ gitOutcome = {
93
+ type: 'git_repository',
94
+ git_info: {
95
+ type: 'github',
96
+ repo: `${owner}/${name}`,
97
+ branches: [`claude/${branch || 'task'}`],
98
+ },
99
+ }
100
+ } else {
101
+ // Fallback: try parseGitHubRepository for owner/repo format
102
+ const ownerRepo = parseGitHubRepository(gitRepoUrl)
103
+ if (ownerRepo) {
104
+ const [owner, name] = ownerRepo.split('/')
105
+ if (owner && name) {
106
+ const revision = branch || (await getDefaultBranch()) || undefined
107
+ gitSource = {
108
+ type: 'git_repository',
109
+ url: `https://github.com/${owner}/${name}`,
110
+ revision,
111
+ }
112
+ gitOutcome = {
113
+ type: 'git_repository',
114
+ git_info: {
115
+ type: 'github',
116
+ repo: `${owner}/${name}`,
117
+ branches: [`claude/${branch || 'task'}`],
118
+ },
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ const requestBody = {
126
+ ...(title !== undefined && { title }),
127
+ events,
128
+ session_context: {
129
+ sources: gitSource ? [gitSource] : [],
130
+ outcomes: gitOutcome ? [gitOutcome] : [],
131
+ model: getMainLoopModel(),
132
+ },
133
+ environment_id: environmentId,
134
+ source: 'remote-control',
135
+ ...(permissionMode && { permission_mode: permissionMode }),
136
+ }
137
+
138
+ const headers = {
139
+ ...getOAuthHeaders(accessToken),
140
+ 'anthropic-beta': 'ccr-byoc-2025-07-29',
141
+ 'x-organization-uuid': orgUUID,
142
+ }
143
+
144
+ const url = `${baseUrlOverride ?? getOauthConfig().BASE_API_URL}/v1/sessions`
145
+ let response
146
+ try {
147
+ response = await axios.post(url, requestBody, {
148
+ headers,
149
+ signal,
150
+ validateStatus: s => s < 500,
151
+ })
152
+ } catch (err: unknown) {
153
+ logForDebugging(
154
+ `[bridge] Session creation request failed: ${errorMessage(err)}`,
155
+ )
156
+ return null
157
+ }
158
+ const isSuccess = response.status === 200 || response.status === 201
159
+
160
+ if (!isSuccess) {
161
+ const detail = extractErrorDetail(response.data)
162
+ logForDebugging(
163
+ `[bridge] Session creation failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
164
+ )
165
+ return null
166
+ }
167
+
168
+ const sessionData: unknown = response.data
169
+ if (
170
+ !sessionData ||
171
+ typeof sessionData !== 'object' ||
172
+ !('id' in sessionData) ||
173
+ typeof sessionData.id !== 'string'
174
+ ) {
175
+ logForDebugging('[bridge] No session ID in response')
176
+ return null
177
+ }
178
+
179
+ return sessionData.id
180
+ }
181
+
182
+ /**
183
+ * Fetch a bridge session via GET /v1/sessions/{id}.
184
+ *
185
+ * Returns the session's environment_id (for `--session-id` resume) and title.
186
+ * Uses the same org-scoped headers as create/archive — the environments-level
187
+ * client in bridgeApi.ts uses a different beta header and no org UUID, which
188
+ * makes the Sessions API return 404.
189
+ */
190
+ export async function getBridgeSession(
191
+ sessionId: string,
192
+ opts?: { baseUrl?: string; getAccessToken?: () => string | undefined },
193
+ ): Promise<{ environment_id?: string; title?: string } | null> {
194
+ const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
195
+ const { getOrganizationUUID } = await import('../services/oauth/client.js')
196
+ const { getOauthConfig } = await import('../constants/oauth.js')
197
+ const { getOAuthHeaders } = await import('../utils/teleport/api.js')
198
+ const { default: axios } = await import('axios')
199
+
200
+ const accessToken =
201
+ opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
202
+ if (!accessToken) {
203
+ logForDebugging('[bridge] No access token for session fetch')
204
+ return null
205
+ }
206
+
207
+ const orgUUID = await getOrganizationUUID()
208
+ if (!orgUUID) {
209
+ logForDebugging('[bridge] No org UUID for session fetch')
210
+ return null
211
+ }
212
+
213
+ const headers = {
214
+ ...getOAuthHeaders(accessToken),
215
+ 'anthropic-beta': 'ccr-byoc-2025-07-29',
216
+ 'x-organization-uuid': orgUUID,
217
+ }
218
+
219
+ const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}`
220
+ logForDebugging(`[bridge] Fetching session ${sessionId}`)
221
+
222
+ let response
223
+ try {
224
+ response = await axios.get<{ environment_id?: string; title?: string }>(
225
+ url,
226
+ { headers, timeout: 10_000, validateStatus: s => s < 500 },
227
+ )
228
+ } catch (err: unknown) {
229
+ logForDebugging(
230
+ `[bridge] Session fetch request failed: ${errorMessage(err)}`,
231
+ )
232
+ return null
233
+ }
234
+
235
+ if (response.status !== 200) {
236
+ const detail = extractErrorDetail(response.data)
237
+ logForDebugging(
238
+ `[bridge] Session fetch failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
239
+ )
240
+ return null
241
+ }
242
+
243
+ return response.data
244
+ }
245
+
246
+ /**
247
+ * Archive a bridge session via POST /v1/sessions/{id}/archive.
248
+ *
249
+ * The CCR server never auto-archives sessions — archival is always an
250
+ * explicit client action. Both `claude remote-control` (standalone bridge) and the
251
+ * always-on `/remote-control` REPL bridge call this during shutdown to archive any
252
+ * sessions that are still alive.
253
+ *
254
+ * The archive endpoint accepts sessions in any status (running, idle,
255
+ * requires_action, pending) and returns 409 if already archived, making
256
+ * it safe to call even if the server-side runner already archived the
257
+ * session.
258
+ *
259
+ * Callers must handle errors — this function has no try/catch; 5xx,
260
+ * timeouts, and network errors throw. Archival is best-effort during
261
+ * cleanup; call sites wrap with .catch().
262
+ */
263
+ export async function archiveBridgeSession(
264
+ sessionId: string,
265
+ opts?: {
266
+ baseUrl?: string
267
+ getAccessToken?: () => string | undefined
268
+ timeoutMs?: number
269
+ },
270
+ ): Promise<void> {
271
+ const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
272
+ const { getOrganizationUUID } = await import('../services/oauth/client.js')
273
+ const { getOauthConfig } = await import('../constants/oauth.js')
274
+ const { getOAuthHeaders } = await import('../utils/teleport/api.js')
275
+ const { default: axios } = await import('axios')
276
+
277
+ const accessToken =
278
+ opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
279
+ if (!accessToken) {
280
+ logForDebugging('[bridge] No access token for session archive')
281
+ return
282
+ }
283
+
284
+ const orgUUID = await getOrganizationUUID()
285
+ if (!orgUUID) {
286
+ logForDebugging('[bridge] No org UUID for session archive')
287
+ return
288
+ }
289
+
290
+ const headers = {
291
+ ...getOAuthHeaders(accessToken),
292
+ 'anthropic-beta': 'ccr-byoc-2025-07-29',
293
+ 'x-organization-uuid': orgUUID,
294
+ }
295
+
296
+ const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}/archive`
297
+ logForDebugging(`[bridge] Archiving session ${sessionId}`)
298
+
299
+ const response = await axios.post(
300
+ url,
301
+ {},
302
+ {
303
+ headers,
304
+ timeout: opts?.timeoutMs ?? 10_000,
305
+ validateStatus: s => s < 500,
306
+ },
307
+ )
308
+
309
+ if (response.status === 200) {
310
+ logForDebugging(`[bridge] Session ${sessionId} archived successfully`)
311
+ } else {
312
+ const detail = extractErrorDetail(response.data)
313
+ logForDebugging(
314
+ `[bridge] Session archive failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
315
+ )
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Update the title of a bridge session via PATCH /v1/sessions/{id}.
321
+ *
322
+ * Called when the user renames a session via /rename while a bridge
323
+ * connection is active, so the title stays in sync on claude.ai/code.
324
+ *
325
+ * Errors are swallowed — title sync is best-effort.
326
+ */
327
+ export async function updateBridgeSessionTitle(
328
+ sessionId: string,
329
+ title: string,
330
+ opts?: { baseUrl?: string; getAccessToken?: () => string | undefined },
331
+ ): Promise<void> {
332
+ const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
333
+ const { getOrganizationUUID } = await import('../services/oauth/client.js')
334
+ const { getOauthConfig } = await import('../constants/oauth.js')
335
+ const { getOAuthHeaders } = await import('../utils/teleport/api.js')
336
+ const { default: axios } = await import('axios')
337
+
338
+ const accessToken =
339
+ opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
340
+ if (!accessToken) {
341
+ logForDebugging('[bridge] No access token for session title update')
342
+ return
343
+ }
344
+
345
+ const orgUUID = await getOrganizationUUID()
346
+ if (!orgUUID) {
347
+ logForDebugging('[bridge] No org UUID for session title update')
348
+ return
349
+ }
350
+
351
+ const headers = {
352
+ ...getOAuthHeaders(accessToken),
353
+ 'anthropic-beta': 'ccr-byoc-2025-07-29',
354
+ 'x-organization-uuid': orgUUID,
355
+ }
356
+
357
+ // Compat gateway only accepts session_* (compat/convert.go:27). v2 callers
358
+ // pass raw cse_*; retag here so all callers can pass whatever they hold.
359
+ // Idempotent for v1's session_* and bridgeMain's pre-converted compatSessionId.
360
+ const compatId = toCompatSessionId(sessionId)
361
+ const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${compatId}`
362
+ logForDebugging(`[bridge] Updating session title: ${compatId} → ${title}`)
363
+
364
+ try {
365
+ const response = await axios.patch(
366
+ url,
367
+ { title },
368
+ { headers, timeout: 10_000, validateStatus: s => s < 500 },
369
+ )
370
+
371
+ if (response.status === 200) {
372
+ logForDebugging(`[bridge] Session title updated successfully`)
373
+ } else {
374
+ const detail = extractErrorDetail(response.data)
375
+ logForDebugging(
376
+ `[bridge] Session title update failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
377
+ )
378
+ }
379
+ } catch (err: unknown) {
380
+ logForDebugging(
381
+ `[bridge] Session title update request failed: ${errorMessage(err)}`,
382
+ )
383
+ }
384
+ }
@@ -0,0 +1,141 @@
1
+ import {
2
+ type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
3
+ logEvent,
4
+ } from '../services/analytics/index.js'
5
+ import { logForDebugging } from '../utils/debug.js'
6
+ import { errorMessage } from '../utils/errors.js'
7
+ import { jsonStringify } from '../utils/slowOperations.js'
8
+
9
+ const DEBUG_MSG_LIMIT = 2000
10
+
11
+ const SECRET_FIELD_NAMES = [
12
+ 'session_ingress_token',
13
+ 'environment_secret',
14
+ 'access_token',
15
+ 'secret',
16
+ 'token',
17
+ ]
18
+
19
+ const SECRET_PATTERN = new RegExp(
20
+ `"(${SECRET_FIELD_NAMES.join('|')})"\\s*:\\s*"([^"]*)"`,
21
+ 'g',
22
+ )
23
+
24
+ const REDACT_MIN_LENGTH = 16
25
+
26
+ export function redactSecrets(s: string): string {
27
+ return s.replace(SECRET_PATTERN, (_match, field: string, value: string) => {
28
+ if (value.length < REDACT_MIN_LENGTH) {
29
+ return `"${field}":"[REDACTED]"`
30
+ }
31
+ const redacted = `${value.slice(0, 8)}...${value.slice(-4)}`
32
+ return `"${field}":"${redacted}"`
33
+ })
34
+ }
35
+
36
+ /** Truncate a string for debug logging, collapsing newlines. */
37
+ export function debugTruncate(s: string): string {
38
+ const flat = s.replace(/\n/g, '\\n')
39
+ if (flat.length <= DEBUG_MSG_LIMIT) {
40
+ return flat
41
+ }
42
+ return flat.slice(0, DEBUG_MSG_LIMIT) + `... (${flat.length} chars)`
43
+ }
44
+
45
+ /** Truncate a JSON-serializable value for debug logging. */
46
+ export function debugBody(data: unknown): string {
47
+ const raw = typeof data === 'string' ? data : jsonStringify(data)
48
+ const s = redactSecrets(raw)
49
+ if (s.length <= DEBUG_MSG_LIMIT) {
50
+ return s
51
+ }
52
+ return s.slice(0, DEBUG_MSG_LIMIT) + `... (${s.length} chars)`
53
+ }
54
+
55
+ /**
56
+ * Extract a descriptive error message from an axios error (or any error).
57
+ * For HTTP errors, appends the server's response body message if available,
58
+ * since axios's default message only includes the status code.
59
+ */
60
+ export function describeAxiosError(err: unknown): string {
61
+ const msg = errorMessage(err)
62
+ if (err && typeof err === 'object' && 'response' in err) {
63
+ const response = (err as { response?: { data?: unknown } }).response
64
+ if (response?.data && typeof response.data === 'object') {
65
+ const data = response.data as Record<string, unknown>
66
+ const detail =
67
+ typeof data.message === 'string'
68
+ ? data.message
69
+ : typeof data.error === 'object' &&
70
+ data.error &&
71
+ 'message' in data.error &&
72
+ typeof (data.error as Record<string, unknown>).message ===
73
+ 'string'
74
+ ? (data.error as Record<string, unknown>).message
75
+ : undefined
76
+ if (detail) {
77
+ return `${msg}: ${detail}`
78
+ }
79
+ }
80
+ }
81
+ return msg
82
+ }
83
+
84
+ /**
85
+ * Extract the HTTP status code from an axios error, if present.
86
+ * Returns undefined for non-HTTP errors (e.g. network failures).
87
+ */
88
+ export function extractHttpStatus(err: unknown): number | undefined {
89
+ if (
90
+ err &&
91
+ typeof err === 'object' &&
92
+ 'response' in err &&
93
+ (err as { response?: { status?: unknown } }).response &&
94
+ typeof (err as { response: { status?: unknown } }).response.status ===
95
+ 'number'
96
+ ) {
97
+ return (err as { response: { status: number } }).response.status
98
+ }
99
+ return undefined
100
+ }
101
+
102
+ /**
103
+ * Pull a human-readable message out of an API error response body.
104
+ * Checks `data.message` first, then `data.error.message`.
105
+ */
106
+ export function extractErrorDetail(data: unknown): string | undefined {
107
+ if (!data || typeof data !== 'object') return undefined
108
+ if ('message' in data && typeof data.message === 'string') {
109
+ return data.message
110
+ }
111
+ if (
112
+ 'error' in data &&
113
+ data.error !== null &&
114
+ typeof data.error === 'object' &&
115
+ 'message' in data.error &&
116
+ typeof data.error.message === 'string'
117
+ ) {
118
+ return data.error.message
119
+ }
120
+ return undefined
121
+ }
122
+
123
+ /**
124
+ * Log a bridge init skip — debug message + `tengu_bridge_repl_skipped`
125
+ * analytics event. Centralizes the event name and the AnalyticsMetadata
126
+ * cast so call sites don't each repeat the 5-line boilerplate.
127
+ */
128
+ export function logBridgeSkip(
129
+ reason: string,
130
+ debugMsg?: string,
131
+ v2?: boolean,
132
+ ): void {
133
+ if (debugMsg) {
134
+ logForDebugging(debugMsg)
135
+ }
136
+ logEvent('tengu_bridge_repl_skipped', {
137
+ reason:
138
+ reason as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
139
+ ...(v2 !== undefined && { v2 }),
140
+ })
141
+ }