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,550 @@
1
+ import { type ChildProcess, spawn } from 'child_process'
2
+ import { createWriteStream, type WriteStream } from 'fs'
3
+ import { tmpdir } from 'os'
4
+ import { dirname, join } from 'path'
5
+ import { createInterface } from 'readline'
6
+ import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
7
+ import { debugTruncate } from './debugUtils.js'
8
+ import type {
9
+ SessionActivity,
10
+ SessionDoneStatus,
11
+ SessionHandle,
12
+ SessionSpawner,
13
+ SessionSpawnOpts,
14
+ } from './types.js'
15
+
16
+ const MAX_ACTIVITIES = 10
17
+ const MAX_STDERR_LINES = 10
18
+
19
+ /**
20
+ * Sanitize a session ID for use in file names.
21
+ * Strips any characters that could cause path traversal (e.g. `../`, `/`)
22
+ * or other filesystem issues, replacing them with underscores.
23
+ */
24
+ export function safeFilenameId(id: string): string {
25
+ return id.replace(/[^a-zA-Z0-9_-]/g, '_')
26
+ }
27
+
28
+ /**
29
+ * A control_request emitted by the child CLI when it needs permission to
30
+ * execute a **specific** tool invocation (not a general capability check).
31
+ * The bridge forwards this to the server so the user can approve/deny.
32
+ */
33
+ export type PermissionRequest = {
34
+ type: 'control_request'
35
+ request_id: string
36
+ request: {
37
+ /** Per-invocation permission check — "may I run this tool with these inputs?" */
38
+ subtype: 'can_use_tool'
39
+ tool_name: string
40
+ input: Record<string, unknown>
41
+ tool_use_id: string
42
+ }
43
+ }
44
+
45
+ type SessionSpawnerDeps = {
46
+ execPath: string
47
+ /**
48
+ * Arguments that must precede the CLI flags when spawning. Empty for
49
+ * compiled binaries (where execPath is the claude binary itself); contains
50
+ * the script path (process.argv[1]) for npm installs where execPath is the
51
+ * node runtime. Without this, node sees --sdk-url as a node option and
52
+ * exits with "bad option: --sdk-url" (see anthropics/claude-code#28334).
53
+ */
54
+ scriptArgs: string[]
55
+ env: NodeJS.ProcessEnv
56
+ verbose: boolean
57
+ sandbox: boolean
58
+ debugFile?: string
59
+ permissionMode?: string
60
+ onDebug: (msg: string) => void
61
+ onActivity?: (sessionId: string, activity: SessionActivity) => void
62
+ onPermissionRequest?: (
63
+ sessionId: string,
64
+ request: PermissionRequest,
65
+ accessToken: string,
66
+ ) => void
67
+ }
68
+
69
+ /** Map tool names to human-readable verbs for the status display. */
70
+ const TOOL_VERBS: Record<string, string> = {
71
+ Read: 'Reading',
72
+ Write: 'Writing',
73
+ Edit: 'Editing',
74
+ MultiEdit: 'Editing',
75
+ Bash: 'Running',
76
+ Glob: 'Searching',
77
+ Grep: 'Searching',
78
+ WebFetch: 'Fetching',
79
+ WebSearch: 'Searching',
80
+ Task: 'Running task',
81
+ FileReadTool: 'Reading',
82
+ FileWriteTool: 'Writing',
83
+ FileEditTool: 'Editing',
84
+ GlobTool: 'Searching',
85
+ GrepTool: 'Searching',
86
+ BashTool: 'Running',
87
+ NotebookEditTool: 'Editing notebook',
88
+ LSP: 'LSP',
89
+ }
90
+
91
+ function toolSummary(name: string, input: Record<string, unknown>): string {
92
+ const verb = TOOL_VERBS[name] ?? name
93
+ const target =
94
+ (input.file_path as string) ??
95
+ (input.filePath as string) ??
96
+ (input.pattern as string) ??
97
+ (input.command as string | undefined)?.slice(0, 60) ??
98
+ (input.url as string) ??
99
+ (input.query as string) ??
100
+ ''
101
+ if (target) {
102
+ return `${verb} ${target}`
103
+ }
104
+ return verb
105
+ }
106
+
107
+ function extractActivities(
108
+ line: string,
109
+ sessionId: string,
110
+ onDebug: (msg: string) => void,
111
+ ): SessionActivity[] {
112
+ let parsed: unknown
113
+ try {
114
+ parsed = jsonParse(line)
115
+ } catch {
116
+ return []
117
+ }
118
+
119
+ if (!parsed || typeof parsed !== 'object') {
120
+ return []
121
+ }
122
+
123
+ const msg = parsed as Record<string, unknown>
124
+ const activities: SessionActivity[] = []
125
+ const now = Date.now()
126
+
127
+ switch (msg.type) {
128
+ case 'assistant': {
129
+ const message = msg.message as Record<string, unknown> | undefined
130
+ if (!message) break
131
+ const content = message.content
132
+ if (!Array.isArray(content)) break
133
+
134
+ for (const block of content) {
135
+ if (!block || typeof block !== 'object') continue
136
+ const b = block as Record<string, unknown>
137
+
138
+ if (b.type === 'tool_use') {
139
+ const name = (b.name as string) ?? 'Tool'
140
+ const input = (b.input as Record<string, unknown>) ?? {}
141
+ const summary = toolSummary(name, input)
142
+ activities.push({
143
+ type: 'tool_start',
144
+ summary,
145
+ timestamp: now,
146
+ })
147
+ onDebug(
148
+ `[bridge:activity] sessionId=${sessionId} tool_use name=${name} ${inputPreview(input)}`,
149
+ )
150
+ } else if (b.type === 'text') {
151
+ const text = (b.text as string) ?? ''
152
+ if (text.length > 0) {
153
+ activities.push({
154
+ type: 'text',
155
+ summary: text.slice(0, 80),
156
+ timestamp: now,
157
+ })
158
+ onDebug(
159
+ `[bridge:activity] sessionId=${sessionId} text "${text.slice(0, 100)}"`,
160
+ )
161
+ }
162
+ }
163
+ }
164
+ break
165
+ }
166
+ case 'result': {
167
+ const subtype = msg.subtype as string | undefined
168
+ if (subtype === 'success') {
169
+ activities.push({
170
+ type: 'result',
171
+ summary: 'Session completed',
172
+ timestamp: now,
173
+ })
174
+ onDebug(
175
+ `[bridge:activity] sessionId=${sessionId} result subtype=success`,
176
+ )
177
+ } else if (subtype) {
178
+ const errors = msg.errors as string[] | undefined
179
+ const errorSummary = errors?.[0] ?? `Error: ${subtype}`
180
+ activities.push({
181
+ type: 'error',
182
+ summary: errorSummary,
183
+ timestamp: now,
184
+ })
185
+ onDebug(
186
+ `[bridge:activity] sessionId=${sessionId} result subtype=${subtype} error="${errorSummary}"`,
187
+ )
188
+ } else {
189
+ onDebug(
190
+ `[bridge:activity] sessionId=${sessionId} result subtype=undefined`,
191
+ )
192
+ }
193
+ break
194
+ }
195
+ default:
196
+ break
197
+ }
198
+
199
+ return activities
200
+ }
201
+
202
+ /**
203
+ * Extract plain text from a replayed SDKUserMessage NDJSON line. Returns the
204
+ * trimmed text if this looks like a real human-authored message, otherwise
205
+ * undefined so the caller keeps waiting for the first real message.
206
+ */
207
+ function extractUserMessageText(
208
+ msg: Record<string, unknown>,
209
+ ): string | undefined {
210
+ // Skip tool-result user messages (wrapped subagent results) and synthetic
211
+ // caveat messages — neither is human-authored.
212
+ if (msg.parent_tool_use_id != null || msg.isSynthetic || msg.isReplay)
213
+ return undefined
214
+
215
+ const message = msg.message as Record<string, unknown> | undefined
216
+ const content = message?.content
217
+ let text: string | undefined
218
+ if (typeof content === 'string') {
219
+ text = content
220
+ } else if (Array.isArray(content)) {
221
+ for (const block of content) {
222
+ if (
223
+ block &&
224
+ typeof block === 'object' &&
225
+ (block as Record<string, unknown>).type === 'text'
226
+ ) {
227
+ text = (block as Record<string, unknown>).text as string | undefined
228
+ break
229
+ }
230
+ }
231
+ }
232
+ text = text?.trim()
233
+ return text ? text : undefined
234
+ }
235
+
236
+ /** Build a short preview of tool input for debug logging. */
237
+ function inputPreview(input: Record<string, unknown>): string {
238
+ const parts: string[] = []
239
+ for (const [key, val] of Object.entries(input)) {
240
+ if (typeof val === 'string') {
241
+ parts.push(`${key}="${val.slice(0, 100)}"`)
242
+ }
243
+ if (parts.length >= 3) break
244
+ }
245
+ return parts.join(' ')
246
+ }
247
+
248
+ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
249
+ return {
250
+ spawn(opts: SessionSpawnOpts, dir: string): SessionHandle {
251
+ // Debug file resolution:
252
+ // 1. If deps.debugFile is provided, use it with session ID suffix for uniqueness
253
+ // 2. If verbose or ant build, auto-generate a temp file path
254
+ // 3. Otherwise, no debug file
255
+ const safeId = safeFilenameId(opts.sessionId)
256
+ let debugFile: string | undefined
257
+ if (deps.debugFile) {
258
+ const ext = deps.debugFile.lastIndexOf('.')
259
+ if (ext > 0) {
260
+ debugFile = `${deps.debugFile.slice(0, ext)}-${safeId}${deps.debugFile.slice(ext)}`
261
+ } else {
262
+ debugFile = `${deps.debugFile}-${safeId}`
263
+ }
264
+ } else if (deps.verbose || process.env.USER_TYPE === 'ant') {
265
+ debugFile = join(tmpdir(), 'claude', `bridge-session-${safeId}.log`)
266
+ }
267
+
268
+ // Transcript file: write raw NDJSON lines for post-hoc analysis.
269
+ // Placed alongside the debug file when one is configured.
270
+ let transcriptStream: WriteStream | null = null
271
+ let transcriptPath: string | undefined
272
+ if (deps.debugFile) {
273
+ transcriptPath = join(
274
+ dirname(deps.debugFile),
275
+ `bridge-transcript-${safeId}.jsonl`,
276
+ )
277
+ transcriptStream = createWriteStream(transcriptPath, { flags: 'a' })
278
+ transcriptStream.on('error', err => {
279
+ deps.onDebug(
280
+ `[bridge:session] Transcript write error: ${err.message}`,
281
+ )
282
+ transcriptStream = null
283
+ })
284
+ deps.onDebug(`[bridge:session] Transcript log: ${transcriptPath}`)
285
+ }
286
+
287
+ const args = [
288
+ ...deps.scriptArgs,
289
+ '--print',
290
+ '--sdk-url',
291
+ opts.sdkUrl,
292
+ '--session-id',
293
+ opts.sessionId,
294
+ '--input-format',
295
+ 'stream-json',
296
+ '--output-format',
297
+ 'stream-json',
298
+ '--replay-user-messages',
299
+ ...(deps.verbose ? ['--verbose'] : []),
300
+ ...(debugFile ? ['--debug-file', debugFile] : []),
301
+ ...(deps.permissionMode
302
+ ? ['--permission-mode', deps.permissionMode]
303
+ : []),
304
+ ]
305
+
306
+ const env: NodeJS.ProcessEnv = {
307
+ ...deps.env,
308
+ // Strip the bridge's OAuth token so the child CC process uses
309
+ // the session access token for inference instead.
310
+ CLAUDE_CODE_OAUTH_TOKEN: undefined,
311
+ CLAUDE_CODE_ENVIRONMENT_KIND: 'bridge',
312
+ ...(deps.sandbox && { CLAUDE_CODE_FORCE_SANDBOX: '1' }),
313
+ CLAUDE_CODE_SESSION_ACCESS_TOKEN: opts.accessToken,
314
+ // v1: HybridTransport (WS reads + POST writes) to Session-Ingress.
315
+ // Harmless in v2 mode — transportUtils checks CLAUDE_CODE_USE_CCR_V2 first.
316
+ CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2: '1',
317
+ // v2: SSETransport + CCRClient to CCR's /v1/code/sessions/* endpoints.
318
+ // Same env vars environment-manager sets in the container path.
319
+ ...(opts.useCcrV2 && {
320
+ CLAUDE_CODE_USE_CCR_V2: '1',
321
+ CLAUDE_CODE_WORKER_EPOCH: String(opts.workerEpoch),
322
+ }),
323
+ }
324
+
325
+ deps.onDebug(
326
+ `[bridge:session] Spawning sessionId=${opts.sessionId} sdkUrl=${opts.sdkUrl} accessToken=${opts.accessToken ? 'present' : 'MISSING'}`,
327
+ )
328
+ deps.onDebug(`[bridge:session] Child args: ${args.join(' ')}`)
329
+ if (debugFile) {
330
+ deps.onDebug(`[bridge:session] Debug log: ${debugFile}`)
331
+ }
332
+
333
+ // Pipe all three streams: stdin for control, stdout for NDJSON parsing,
334
+ // stderr for error capture and diagnostics.
335
+ const child: ChildProcess = spawn(deps.execPath, args, {
336
+ cwd: dir,
337
+ stdio: ['pipe', 'pipe', 'pipe'],
338
+ env,
339
+ windowsHide: true,
340
+ })
341
+
342
+ deps.onDebug(
343
+ `[bridge:session] sessionId=${opts.sessionId} pid=${child.pid}`,
344
+ )
345
+
346
+ const activities: SessionActivity[] = []
347
+ let currentActivity: SessionActivity | null = null
348
+ const lastStderr: string[] = []
349
+ let sigkillSent = false
350
+ let firstUserMessageSeen = false
351
+
352
+ // Buffer stderr for error diagnostics
353
+ if (child.stderr) {
354
+ const stderrRl = createInterface({ input: child.stderr })
355
+ stderrRl.on('line', line => {
356
+ // Forward stderr to bridge's stderr in verbose mode
357
+ if (deps.verbose) {
358
+ process.stderr.write(line + '\n')
359
+ }
360
+ // Ring buffer of last N lines
361
+ if (lastStderr.length >= MAX_STDERR_LINES) {
362
+ lastStderr.shift()
363
+ }
364
+ lastStderr.push(line)
365
+ })
366
+ }
367
+
368
+ // Parse NDJSON from child stdout
369
+ if (child.stdout) {
370
+ const rl = createInterface({ input: child.stdout })
371
+ rl.on('line', line => {
372
+ // Write raw NDJSON to transcript file
373
+ if (transcriptStream) {
374
+ transcriptStream.write(line + '\n')
375
+ }
376
+
377
+ // Log all messages flowing from the child CLI to the bridge
378
+ deps.onDebug(
379
+ `[bridge:ws] sessionId=${opts.sessionId} <<< ${debugTruncate(line)}`,
380
+ )
381
+
382
+ // In verbose mode, forward raw output to stderr
383
+ if (deps.verbose) {
384
+ process.stderr.write(line + '\n')
385
+ }
386
+
387
+ const extracted = extractActivities(
388
+ line,
389
+ opts.sessionId,
390
+ deps.onDebug,
391
+ )
392
+ for (const activity of extracted) {
393
+ // Maintain ring buffer
394
+ if (activities.length >= MAX_ACTIVITIES) {
395
+ activities.shift()
396
+ }
397
+ activities.push(activity)
398
+ currentActivity = activity
399
+
400
+ deps.onActivity?.(opts.sessionId, activity)
401
+ }
402
+
403
+ // Detect control_request and replayed user messages.
404
+ // extractActivities parses the same line but swallows parse errors
405
+ // and skips 'user' type — re-parse here is cheap (NDJSON lines are
406
+ // small) and keeps each path self-contained.
407
+ {
408
+ let parsed: unknown
409
+ try {
410
+ parsed = jsonParse(line)
411
+ } catch {
412
+ // Non-JSON line, skip detection
413
+ }
414
+ if (parsed && typeof parsed === 'object') {
415
+ const msg = parsed as Record<string, unknown>
416
+
417
+ if (msg.type === 'control_request') {
418
+ const request = msg.request as
419
+ | Record<string, unknown>
420
+ | undefined
421
+ if (
422
+ request?.subtype === 'can_use_tool' &&
423
+ deps.onPermissionRequest
424
+ ) {
425
+ deps.onPermissionRequest(
426
+ opts.sessionId,
427
+ parsed as PermissionRequest,
428
+ opts.accessToken,
429
+ )
430
+ }
431
+ // interrupt is turn-level; the child handles it internally (print.ts)
432
+ } else if (
433
+ msg.type === 'user' &&
434
+ !firstUserMessageSeen &&
435
+ opts.onFirstUserMessage
436
+ ) {
437
+ const text = extractUserMessageText(msg)
438
+ if (text) {
439
+ firstUserMessageSeen = true
440
+ opts.onFirstUserMessage(text)
441
+ }
442
+ }
443
+ }
444
+ }
445
+ })
446
+ }
447
+
448
+ const done = new Promise<SessionDoneStatus>(resolve => {
449
+ child.on('close', (code, signal) => {
450
+ // Close transcript stream on exit
451
+ if (transcriptStream) {
452
+ transcriptStream.end()
453
+ transcriptStream = null
454
+ }
455
+
456
+ if (signal === 'SIGTERM' || signal === 'SIGINT') {
457
+ deps.onDebug(
458
+ `[bridge:session] sessionId=${opts.sessionId} interrupted signal=${signal} pid=${child.pid}`,
459
+ )
460
+ resolve('interrupted')
461
+ } else if (code === 0) {
462
+ deps.onDebug(
463
+ `[bridge:session] sessionId=${opts.sessionId} completed exit_code=0 pid=${child.pid}`,
464
+ )
465
+ resolve('completed')
466
+ } else {
467
+ deps.onDebug(
468
+ `[bridge:session] sessionId=${opts.sessionId} failed exit_code=${code} pid=${child.pid}`,
469
+ )
470
+ resolve('failed')
471
+ }
472
+ })
473
+
474
+ child.on('error', err => {
475
+ deps.onDebug(
476
+ `[bridge:session] sessionId=${opts.sessionId} spawn error: ${err.message}`,
477
+ )
478
+ resolve('failed')
479
+ })
480
+ })
481
+
482
+ const handle: SessionHandle = {
483
+ sessionId: opts.sessionId,
484
+ done,
485
+ activities,
486
+ accessToken: opts.accessToken,
487
+ lastStderr,
488
+ get currentActivity(): SessionActivity | null {
489
+ return currentActivity
490
+ },
491
+ kill(): void {
492
+ if (!child.killed) {
493
+ deps.onDebug(
494
+ `[bridge:session] Sending SIGTERM to sessionId=${opts.sessionId} pid=${child.pid}`,
495
+ )
496
+ // On Windows, child.kill('SIGTERM') throws; use default signal.
497
+ if (process.platform === 'win32') {
498
+ child.kill()
499
+ } else {
500
+ child.kill('SIGTERM')
501
+ }
502
+ }
503
+ },
504
+ forceKill(): void {
505
+ // Use separate flag because child.killed is set when kill() is called,
506
+ // not when the process exits. We need to send SIGKILL even after SIGTERM.
507
+ if (!sigkillSent && child.pid) {
508
+ sigkillSent = true
509
+ deps.onDebug(
510
+ `[bridge:session] Sending SIGKILL to sessionId=${opts.sessionId} pid=${child.pid}`,
511
+ )
512
+ if (process.platform === 'win32') {
513
+ child.kill()
514
+ } else {
515
+ child.kill('SIGKILL')
516
+ }
517
+ }
518
+ },
519
+ writeStdin(data: string): void {
520
+ if (child.stdin && !child.stdin.destroyed) {
521
+ deps.onDebug(
522
+ `[bridge:ws] sessionId=${opts.sessionId} >>> ${debugTruncate(data)}`,
523
+ )
524
+ child.stdin.write(data)
525
+ }
526
+ },
527
+ updateAccessToken(token: string): void {
528
+ handle.accessToken = token
529
+ // Send the fresh token to the child process via stdin. The child's
530
+ // StructuredIO handles update_environment_variables messages by
531
+ // setting process.env directly, so getSessionIngressAuthToken()
532
+ // picks up the new token on the next refreshHeaders call.
533
+ handle.writeStdin(
534
+ jsonStringify({
535
+ type: 'update_environment_variables',
536
+ variables: { CLAUDE_CODE_SESSION_ACCESS_TOKEN: token },
537
+ }) + '\n',
538
+ )
539
+ deps.onDebug(
540
+ `[bridge:session] Sent token refresh via stdin for sessionId=${opts.sessionId}`,
541
+ )
542
+ },
543
+ }
544
+
545
+ return handle
546
+ },
547
+ }
548
+ }
549
+
550
+ export { extractActivities as _extractActivitiesForTesting }