@parall/claude-agent 1.31.0 → 1.32.0

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.
package/src/index.ts CHANGED
@@ -1,8 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import * as os from "node:os";
4
- import { ParallAgentGateway, createPlatformConfigManager, createLogger, childLogger, isPlatformManagedProfile, parseShutdownDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource } from "@parall/agent-core";
5
- import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
3
+ import * as os from 'node:os';
4
+ import {
5
+ ParallAgentGateway,
6
+ createPlatformConfigManager,
7
+ createLogger,
8
+ createOtelLogger,
9
+ childLogger,
10
+ isPlatformManagedProfile,
11
+ isPlatformModelOverride,
12
+ resolveRuntimeModel,
13
+ parseShutdownDeadlineMs,
14
+ parseForkDeadlineMs,
15
+ parseDispatchDeadlineMs,
16
+ parseProviderConfig,
17
+ clearAllProviderCreds,
18
+ llmSource,
19
+ initAgentTelemetry,
20
+ } from '@parall/agent-core';
21
+ import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
6
22
  import {
7
23
  buildClaudeRuntimeKey,
8
24
  contextFilePathForSession,
@@ -10,12 +26,12 @@ import {
10
26
  resolveWsUrl,
11
27
  sessionStateFilePathForRuntime,
12
28
  stepIdFilePathForSession,
13
- } from "./config.js";
14
- import { ClaudeCodeAdapter } from "./dispatch.js";
15
- import { ClaudeSessionManager } from "./session-manager.js";
16
- import { ensureClaudeWorkspace } from "./workspace.js";
29
+ } from './config.js';
30
+ import { ClaudeCodeAdapter } from './dispatch.js';
31
+ import { ClaudeSessionManager } from './session-manager.js';
32
+ import { ensureClaudeWorkspace } from './workspace.js';
17
33
 
18
- const log = createLogger("claude-agent");
34
+ const log = createLogger('claude-agent');
19
35
  let activeLog = log;
20
36
 
21
37
  async function getAgentMeWithLegacyFallback(client: ParallClient, orgId: string) {
@@ -35,169 +51,202 @@ function resolveProviderEnv(): void {
35
51
  if (!pc) return;
36
52
  clearAllProviderCreds(process.env);
37
53
  const source = llmSource(pc);
38
- if (source === "parall") {
54
+ if (source === 'parall') {
39
55
  process.env.ANTHROPIC_AUTH_TOKEN = process.env.PRLL_API_KEY;
40
56
  process.env.ANTHROPIC_BASE_URL = `${process.env.PRLL_API_URL}/api/llm`;
41
- process.env.PRLL_CLAUDE_ALLOW_API_KEY = "1";
42
- } else if (source === "custom") {
57
+ process.env.PRLL_CLAUDE_ALLOW_API_KEY = '1';
58
+ } else if (source === 'custom') {
43
59
  if (pc.anthropic_auth_token) {
44
60
  process.env.ANTHROPIC_AUTH_TOKEN = pc.anthropic_auth_token;
45
- process.env.PRLL_CLAUDE_ALLOW_API_KEY = "1";
61
+ process.env.PRLL_CLAUDE_ALLOW_API_KEY = '1';
46
62
  }
47
63
  if (pc.anthropic_base_url) process.env.ANTHROPIC_BASE_URL = pc.anthropic_base_url;
48
64
  }
49
65
  }
50
66
 
51
67
  async function main() {
52
- resolveProviderEnv();
53
- const config = resolveClaudeAgentConfig(process.env);
68
+ const telemetry = await initAgentTelemetry('parall-claude-agent', 'claude-code');
69
+ activeLog = createOtelLogger('agent', 'claude-agent');
70
+ try {
71
+ resolveProviderEnv();
72
+ const config = resolveClaudeAgentConfig(process.env);
54
73
 
55
- if (
56
- config.allowApiKey &&
57
- (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN)
58
- ) {
59
- // Surface the billing path: default is OAuth (Claude.ai subscription); this
60
- // branch means the operator opted into Anthropic API pay-per-use, and the
61
- // whole point of the strip default is that "silent" is the bad state.
62
- log.warn(
63
- "PRLL_CLAUDE_ALLOW_API_KEY=1 — ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN will reach the Claude CLI; billing routes through Anthropic API pay-per-use instead of Claude.ai OAuth.",
64
- );
65
- }
74
+ if (config.allowApiKey && (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN)) {
75
+ // Surface the billing path: default is OAuth (Claude.ai subscription); this
76
+ // branch means the operator opted into Anthropic API pay-per-use, and the
77
+ // whole point of the strip default is that "silent" is the bad state.
78
+ activeLog.warn(
79
+ 'PRLL_CLAUDE_ALLOW_API_KEY=1 ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN will reach the Claude CLI; billing routes through Anthropic API pay-per-use instead of Claude.ai OAuth.',
80
+ );
81
+ }
66
82
 
67
- const client = new ParallClient({
68
- baseUrl: config.apiUrl,
69
- token: config.apiKey,
70
- swimlaneName: config.swimlaneName,
71
- });
83
+ const client = new ParallClient({
84
+ baseUrl: config.apiUrl,
85
+ token: config.apiKey,
86
+ swimlaneName: config.swimlaneName,
87
+ });
72
88
 
73
- const me = await getAgentMeWithLegacyFallback(client, config.orgId);
74
- const agentUserId = me.id;
75
- const agentLog = childLogger(log, agentUserId);
76
- activeLog = agentLog;
77
- ensureClaudeWorkspace(config.workspaceDir, agentLog, {
78
- userId: agentUserId,
79
- displayName: me.display_name,
80
- description: me.agent_profile?.description ?? undefined,
81
- });
82
- const runtimeKey = config.runtimeKey || buildClaudeRuntimeKey(agentUserId);
83
- const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
84
- const sessionManager = new ClaudeSessionManager(
85
- runtimeKey,
86
- sessionStateFilePath,
87
- agentLog,
88
- );
89
+ const me = await getAgentMeWithLegacyFallback(client, config.orgId);
90
+ const agentUserId = me.id;
91
+ const agentLog = childLogger(activeLog, agentUserId);
92
+ activeLog = agentLog;
93
+ ensureClaudeWorkspace(config.workspaceDir, agentLog, {
94
+ userId: agentUserId,
95
+ displayName: me.display_name,
96
+ description: me.agent_profile?.description ?? undefined,
97
+ });
98
+ const runtimeKey = config.runtimeKey || buildClaudeRuntimeKey(agentUserId);
99
+ const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
100
+ const sessionManager = new ClaudeSessionManager(runtimeKey, sessionStateFilePath, agentLog);
89
101
 
90
- const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
91
- const ws = new ParallWs({
92
- getTicket: () => client.getWsTicket(),
93
- wsUrl: resolvedWsUrl,
94
- });
102
+ const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
103
+ const ws = new ParallWs({
104
+ getTicket: () => client.getWsTicket(),
105
+ wsUrl: resolvedWsUrl,
106
+ });
95
107
 
96
- const configMgr = createPlatformConfigManager({ client, stateDir: config.stateDir, runtimeType: "claude-code", log: agentLog });
97
- const platformDefaults = await configMgr.fetch();
98
- let platformManaged = isPlatformManagedProfile(me.agent_profile);
99
- const resolvedModel = platformManaged ? (platformDefaults.model ?? config.model) : config.model;
100
- const resolvedEffort = platformManaged
101
- ? (platformDefaults.thinkingEffort ?? (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined))
102
- : (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined);
103
- if (platformDefaults.model) agentLog.info(`platform config: model=${platformDefaults.model} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
104
- if (platformDefaults.thinkingEffort) agentLog.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
108
+ const configMgr = createPlatformConfigManager({
109
+ client,
110
+ stateDir: config.stateDir,
111
+ runtimeType: 'claude-code',
112
+ log: agentLog,
113
+ });
114
+ const platformDefaults = await configMgr.fetch();
115
+ let platformManaged = isPlatformManagedProfile(me.agent_profile);
116
+ // Model precedence: operator override (platform-managed) > env > server floor.
117
+ // The server delivers a model floor for Parall-proxy routes even when
118
+ // model_management=self, so the CLI never falls back to a built-in default
119
+ // the catalog might reject — but that floor must lose to an explicit env
120
+ // override (env > floor). A profile is an operator-override only when it is
121
+ // platform-managed AND not explicitly self-managed: this treats legacy hosted
122
+ // agents (model_management=null, resolved to platform server-side) as
123
+ // overrides, while excluding local self agents whose delivered model is a
124
+ // floor. effort stays an operator override (platform).
125
+ let platformModelOverride = isPlatformModelOverride(me.agent_profile);
126
+ const resolvedModel = resolveRuntimeModel(
127
+ platformModelOverride,
128
+ platformDefaults.model,
129
+ config.model,
130
+ );
131
+ const resolvedEffort = platformManaged
132
+ ? (platformDefaults.thinkingEffort ??
133
+ (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined))
134
+ : process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined;
135
+ if (platformDefaults.model)
136
+ agentLog.info(
137
+ `platform config: model=${platformDefaults.model} → resolved=${resolvedModel ?? 'cli-default'} (${platformModelOverride ? 'platform-managed' : 'floor/self'})`,
138
+ );
139
+ if (platformDefaults.thinkingEffort)
140
+ agentLog.info(
141
+ `platform config: thinking_effort=${platformDefaults.thinkingEffort} (${platformModelOverride ? 'applied' : 'ignored, model_management=self'})`,
142
+ );
105
143
 
106
- const adapter = new ClaudeCodeAdapter({
107
- claudeBin: config.claudeBin,
108
- claudeHome: config.claudeHome,
109
- workspaceDir: config.workspaceDir,
110
- model: resolvedModel,
111
- permissionMode: config.permissionMode,
112
- allowedTools: config.allowedTools,
113
- disallowedTools: config.disallowedTools,
114
- additionalDirs: config.additionalDirs,
115
- appendSystemPrompt: config.appendSystemPrompt,
116
- allowApiKey: config.allowApiKey,
117
- sessionManager,
118
- // Static Parall credentials. The long-lived subprocess must authenticate
119
- // against Parall's API across many dispatches, so these cannot come from
120
- // a per-dispatch DispatchContext.
121
- apiUrl: config.apiUrl,
122
- apiKey: config.apiKey,
123
- orgId: config.orgId,
124
- contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
125
- stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
126
- });
127
- if (resolvedEffort) {
128
- adapter.updateConfig({ effort: resolvedEffort });
129
- }
144
+ const adapter = new ClaudeCodeAdapter({
145
+ claudeBin: config.claudeBin,
146
+ claudeHome: config.claudeHome,
147
+ workspaceDir: config.workspaceDir,
148
+ model: resolvedModel,
149
+ permissionMode: config.permissionMode,
150
+ allowedTools: config.allowedTools,
151
+ disallowedTools: config.disallowedTools,
152
+ additionalDirs: config.additionalDirs,
153
+ appendSystemPrompt: config.appendSystemPrompt,
154
+ allowApiKey: config.allowApiKey,
155
+ sessionManager,
156
+ // Static Parall credentials. The long-lived subprocess must authenticate
157
+ // against Parall's API across many dispatches, so these cannot come from
158
+ // a per-dispatch DispatchContext.
159
+ apiUrl: config.apiUrl,
160
+ apiKey: config.apiKey,
161
+ orgId: config.orgId,
162
+ contextFilePathForSession: (sessionKey) =>
163
+ contextFilePathForSession(config.stateDir, sessionKey),
164
+ stepIdFilePathForSession: (sessionKey) =>
165
+ stepIdFilePathForSession(config.stateDir, sessionKey),
166
+ });
167
+ if (resolvedEffort) {
168
+ adapter.updateConfig({ effort: resolvedEffort });
169
+ }
130
170
 
131
- const gateway = new ParallAgentGateway({
132
- accountId: agentUserId,
133
- client,
134
- ws,
135
- connectionLabel: resolvedWsUrl,
136
- config: {
137
- parall_url: config.apiUrl,
138
- api_key: config.apiKey,
139
- org_id: config.orgId,
140
- },
141
- agentUserId,
142
- runtimeType: "claude-code",
143
- runtimeKey,
144
- runtimeRef: {
145
- hostname: os.hostname(),
146
- pid: process.pid,
147
- workspace_dir: config.workspaceDir,
148
- claude_home: config.claudeHome,
149
- },
150
- dispatchAdapter: adapter,
151
- log: agentLog,
152
- shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
153
- contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
154
- stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
155
- onConfigUpdate: async () => {
156
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
157
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
158
- const updated = await configMgr.fetch();
159
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
160
- adapter.updateConfig({
161
- model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
162
- effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
163
- });
164
- },
165
- onSessionReady: async () => {
166
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
167
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
168
- const updated = await configMgr.fetch();
169
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
170
- adapter.updateConfig({
171
- model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
172
- effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
173
- });
174
- },
175
- // Long-lived `claude` subprocesses outlive a single dispatch, so the
176
- // gateway's shuttingDown flag alone does not tear them down. Piggyback on
177
- // onBeforeDisconnect to close stdin and SIGTERM any survivors after
178
- // in-flight drains finish.
179
- onBeforeDisconnect: () => adapter.shutdown(),
180
- onNewSession: async () => {
181
- sessionManager.clearMainSession();
182
- adapter.resetProcesses();
183
- },
184
- onSessionStale: () => {
185
- sessionManager.clearMainSession();
186
- adapter.resetProcesses();
187
- },
188
- });
171
+ const gateway = new ParallAgentGateway({
172
+ accountId: agentUserId,
173
+ client,
174
+ ws,
175
+ connectionLabel: resolvedWsUrl,
176
+ config: {
177
+ parall_url: config.apiUrl,
178
+ api_key: config.apiKey,
179
+ org_id: config.orgId,
180
+ },
181
+ agentUserId,
182
+ runtimeType: 'claude-code',
183
+ runtimeKey,
184
+ runtimeRef: {
185
+ hostname: os.hostname(),
186
+ pid: process.pid,
187
+ workspace_dir: config.workspaceDir,
188
+ claude_home: config.claudeHome,
189
+ },
190
+ dispatchAdapter: adapter,
191
+ log: agentLog,
192
+ shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
193
+ forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
194
+ dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
195
+ contextFilePathForSession: (sessionKey) =>
196
+ contextFilePathForSession(config.stateDir, sessionKey),
197
+ stepIdFilePathForSession: (sessionKey) =>
198
+ stepIdFilePathForSession(config.stateDir, sessionKey),
199
+ onConfigUpdate: async () => {
200
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
201
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
202
+ platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
203
+ const updated = await configMgr.fetch();
204
+ const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
205
+ adapter.updateConfig({
206
+ model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
207
+ effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
208
+ });
209
+ },
210
+ onSessionReady: async () => {
211
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
212
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
213
+ platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
214
+ const updated = await configMgr.fetch();
215
+ const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
216
+ adapter.updateConfig({
217
+ model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
218
+ effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
219
+ });
220
+ },
221
+ // Long-lived `claude` subprocesses outlive a single dispatch, so the
222
+ // gateway's shuttingDown flag alone does not tear them down. Piggyback on
223
+ // onBeforeDisconnect to close stdin and SIGTERM any survivors after
224
+ // in-flight drains finish.
225
+ onBeforeDisconnect: () => adapter.shutdown(),
226
+ onNewSession: async () => {
227
+ sessionManager.clearMainSession();
228
+ adapter.resetProcesses();
229
+ },
230
+ onSessionStale: () => {
231
+ sessionManager.clearMainSession();
232
+ adapter.resetProcesses();
233
+ },
234
+ });
189
235
 
190
- const abortController = new AbortController();
191
- const abort = () => abortController.abort();
192
- process.on("SIGINT", abort);
193
- process.on("SIGTERM", abort);
236
+ const abortController = new AbortController();
237
+ const abort = () => abortController.abort();
238
+ process.on('SIGINT', abort);
239
+ process.on('SIGTERM', abort);
194
240
 
195
- try {
196
- agentLog.info(`starting self-hosted Claude runtime for ${me.display_name} (${agentUserId})`);
197
- await gateway.run(abortController.signal);
241
+ try {
242
+ agentLog.info(`starting self-hosted Claude runtime for ${me.display_name} (${agentUserId})`);
243
+ await gateway.run(abortController.signal);
244
+ } finally {
245
+ process.off('SIGINT', abort);
246
+ process.off('SIGTERM', abort);
247
+ }
198
248
  } finally {
199
- process.off("SIGINT", abort);
200
- process.off("SIGTERM", abort);
249
+ await telemetry.shutdown();
201
250
  }
202
251
  }
203
252
 
@@ -1,9 +1,9 @@
1
- import type { Readable } from "node:stream";
2
- import type { RuntimeEvent } from "@parall/agent-core";
1
+ import type { Readable } from 'node:stream';
2
+ import type { RuntimeEvent } from '@parall/agent-core';
3
3
 
4
4
  export type ClaudeParsedEvent =
5
5
  | RuntimeEvent
6
- | { type: "session_id"; sessionId: string }
6
+ | { type: 'session_id'; sessionId: string }
7
7
  // turn_end is emitted for every `result` frame (both `is_error: true` and
8
8
  // `is_error: false`); `isError` carries the frame's status so the consumer
9
9
  // can tell a clean turn boundary from a failed one without inspecting the
@@ -14,7 +14,7 @@ export type ClaudeParsedEvent =
14
14
  // stream. For a short-lived `--print <prompt>` invocation the final
15
15
  // `result` frame arrives right before stdout EOF, so this event is
16
16
  // redundant there — safe to ignore.
17
- | { type: "turn_end"; sessionId?: string; isError: boolean };
17
+ | { type: 'turn_end'; sessionId?: string; isError: boolean };
18
18
 
19
19
  type ToolUseMeta = {
20
20
  toolName: string;
@@ -22,26 +22,26 @@ type ToolUseMeta = {
22
22
  };
23
23
 
24
24
  function asTrimmedString(value: unknown): string | undefined {
25
- if (typeof value !== "string") return undefined;
25
+ if (typeof value !== 'string') return undefined;
26
26
  const trimmed = value.trim();
27
27
  return trimmed.length > 0 ? trimmed : undefined;
28
28
  }
29
29
 
30
30
  function parseEventTimestampMs(event: unknown): number | undefined {
31
- if (!event || typeof event !== "object") return undefined;
31
+ if (!event || typeof event !== 'object') return undefined;
32
32
  const timestamp = (event as { timestamp?: unknown }).timestamp;
33
- if (typeof timestamp !== "string") return undefined;
33
+ if (typeof timestamp !== 'string') return undefined;
34
34
  const ms = Date.parse(timestamp);
35
35
  return Number.isFinite(ms) ? ms : undefined;
36
36
  }
37
37
 
38
38
  function stringifyContent(value: unknown): string {
39
- if (typeof value === "string") return value;
39
+ if (typeof value === 'string') return value;
40
40
  if (Array.isArray(value)) {
41
41
  return value
42
42
  .map((item) => {
43
- if (typeof item === "string") return item;
44
- if (!item || typeof item !== "object") return "";
43
+ if (typeof item === 'string') return item;
44
+ if (!item || typeof item !== 'object') return '';
45
45
  const text = asTrimmedString((item as { text?: unknown }).text);
46
46
  if (text) return text;
47
47
  const thinking = asTrimmedString((item as { thinking?: unknown }).thinking);
@@ -49,19 +49,19 @@ function stringifyContent(value: unknown): string {
49
49
  return JSON.stringify(item);
50
50
  })
51
51
  .filter(Boolean)
52
- .join("\n");
52
+ .join('\n');
53
53
  }
54
- if (value && typeof value === "object") {
54
+ if (value && typeof value === 'object') {
55
55
  return JSON.stringify(value);
56
56
  }
57
- return "";
57
+ return '';
58
58
  }
59
59
 
60
60
  function normalizeToolResultContent(content: unknown, fallback: unknown): string {
61
61
  const direct = stringifyContent(content).trim();
62
62
  if (direct) return direct;
63
63
 
64
- if (fallback && typeof fallback === "object") {
64
+ if (fallback && typeof fallback === 'object') {
65
65
  const stdout = asTrimmedString((fallback as { stdout?: unknown }).stdout);
66
66
  const stderr = asTrimmedString((fallback as { stderr?: unknown }).stderr);
67
67
  if (stdout && stderr) return `${stdout}\n${stderr}`;
@@ -69,20 +69,20 @@ function normalizeToolResultContent(content: unknown, fallback: unknown): string
69
69
  if (stderr) return stderr;
70
70
  }
71
71
 
72
- return "";
72
+ return '';
73
73
  }
74
74
 
75
75
  async function* readJsonLines(readable: Readable): AsyncGenerator<string> {
76
- let buffer = "";
77
- readable.setEncoding("utf8");
76
+ let buffer = '';
77
+ readable.setEncoding('utf8');
78
78
  for await (const chunk of readable) {
79
79
  buffer += chunk;
80
- let newlineIndex = buffer.indexOf("\n");
80
+ let newlineIndex = buffer.indexOf('\n');
81
81
  while (newlineIndex >= 0) {
82
82
  const line = buffer.slice(0, newlineIndex).trim();
83
83
  buffer = buffer.slice(newlineIndex + 1);
84
84
  if (line) yield line;
85
- newlineIndex = buffer.indexOf("\n");
85
+ newlineIndex = buffer.indexOf('\n');
86
86
  }
87
87
  }
88
88
 
@@ -90,7 +90,9 @@ async function* readJsonLines(readable: Readable): AsyncGenerator<string> {
90
90
  if (tail) yield tail;
91
91
  }
92
92
 
93
- export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator<ClaudeParsedEvent> {
93
+ export async function* parseClaudeStreamJson(
94
+ readable: Readable,
95
+ ): AsyncGenerator<ClaudeParsedEvent> {
94
96
  const toolUses = new Map<string, ToolUseMeta>();
95
97
 
96
98
  for await (const line of readJsonLines(readable)) {
@@ -101,46 +103,47 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
101
103
  continue;
102
104
  }
103
105
 
104
- if (!event || typeof event !== "object") continue;
106
+ if (!event || typeof event !== 'object') continue;
105
107
  const now = Date.now();
106
108
  const eventTimestampMs = parseEventTimestampMs(event) ?? now;
107
109
  const eventRecord = event as Record<string, unknown>;
108
110
 
109
- if (eventRecord.type === "system" && eventRecord.subtype === "init") {
111
+ if (eventRecord.type === 'system' && eventRecord.subtype === 'init') {
110
112
  const sessionId = asTrimmedString(eventRecord.session_id);
111
113
  if (sessionId) {
112
- yield { type: "session_id", sessionId };
114
+ yield { type: 'session_id', sessionId };
113
115
  }
114
116
  continue;
115
117
  }
116
118
 
117
- if (eventRecord.type === "assistant") {
119
+ if (eventRecord.type === 'assistant') {
118
120
  if (eventRecord.error) continue;
119
121
 
120
122
  const message = eventRecord.message;
121
- const content = message && typeof message === "object"
122
- ? (message as { content?: unknown }).content
123
- : undefined;
123
+ const content =
124
+ message && typeof message === 'object'
125
+ ? (message as { content?: unknown }).content
126
+ : undefined;
124
127
  if (!Array.isArray(content)) continue;
125
128
 
126
129
  for (const block of content) {
127
- if (!block || typeof block !== "object") continue;
130
+ if (!block || typeof block !== 'object') continue;
128
131
  const blockRecord = block as Record<string, unknown>;
129
132
 
130
133
  switch (blockRecord.type) {
131
- case "thinking": {
134
+ case 'thinking': {
132
135
  const text = asTrimmedString(blockRecord.thinking);
133
- if (text) yield { type: "thinking", text };
136
+ if (text) yield { type: 'thinking', text };
134
137
  break;
135
138
  }
136
139
 
137
- case "text": {
140
+ case 'text': {
138
141
  const text = asTrimmedString(blockRecord.text);
139
- if (text) yield { type: "text", text, project: true };
142
+ if (text) yield { type: 'text', text, project: true };
140
143
  break;
141
144
  }
142
145
 
143
- case "tool_use": {
146
+ case 'tool_use': {
144
147
  const callId = asTrimmedString(blockRecord.id);
145
148
  const toolName = asTrimmedString(blockRecord.name);
146
149
  if (!callId || !toolName) break;
@@ -151,7 +154,7 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
151
154
  });
152
155
 
153
156
  yield {
154
- type: "tool_call",
157
+ type: 'tool_call',
155
158
  callId,
156
159
  toolName,
157
160
  input: blockRecord.input ?? {},
@@ -164,17 +167,18 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
164
167
  continue;
165
168
  }
166
169
 
167
- if (eventRecord.type === "user") {
170
+ if (eventRecord.type === 'user') {
168
171
  const message = eventRecord.message;
169
- const content = message && typeof message === "object"
170
- ? (message as { content?: unknown }).content
171
- : undefined;
172
+ const content =
173
+ message && typeof message === 'object'
174
+ ? (message as { content?: unknown }).content
175
+ : undefined;
172
176
  if (!Array.isArray(content)) continue;
173
177
 
174
178
  for (const block of content) {
175
- if (!block || typeof block !== "object") continue;
179
+ if (!block || typeof block !== 'object') continue;
176
180
  const blockRecord = block as Record<string, unknown>;
177
- if (blockRecord.type !== "tool_result") continue;
181
+ if (blockRecord.type !== 'tool_result') continue;
178
182
 
179
183
  const callId = asTrimmedString(blockRecord.tool_use_id);
180
184
  if (!callId) continue;
@@ -183,11 +187,11 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
183
187
  const output = normalizeToolResultContent(blockRecord.content, eventRecord.tool_use_result);
184
188
 
185
189
  yield {
186
- type: "tool_result",
190
+ type: 'tool_result',
187
191
  callId,
188
- toolName: meta?.toolName ?? "unknown",
192
+ toolName: meta?.toolName ?? 'unknown',
189
193
  output,
190
- ...(blockRecord.is_error === true ? { error: output || "Tool call failed" } : {}),
194
+ ...(blockRecord.is_error === true ? { error: output || 'Tool call failed' } : {}),
191
195
  ...(meta ? { durationMs: Math.max(0, eventTimestampMs - meta.startedAtMs) } : {}),
192
196
  };
193
197
  toolUses.delete(callId);
@@ -195,13 +199,14 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
195
199
  continue;
196
200
  }
197
201
 
198
- if (eventRecord.type === "result") {
202
+ if (eventRecord.type === 'result') {
199
203
  const isError = eventRecord.is_error === true;
200
204
  if (isError) {
201
- const message = asTrimmedString(eventRecord.result)
202
- || asTrimmedString(eventRecord.error)
203
- || "Claude dispatch failed";
204
- yield { type: "error", message };
205
+ const message =
206
+ asTrimmedString(eventRecord.result) ||
207
+ asTrimmedString(eventRecord.error) ||
208
+ 'Claude dispatch failed';
209
+ yield { type: 'error', message };
205
210
  }
206
211
  // Turn boundary: the subprocess lives across many turns, so any
207
212
  // tool_use entries that never matched a tool_result this turn
@@ -210,7 +215,7 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
210
215
  // the map grow unbounded or mislabel durations on id collisions.
211
216
  toolUses.clear();
212
217
  yield {
213
- type: "turn_end",
218
+ type: 'turn_end',
214
219
  sessionId: asTrimmedString(eventRecord.session_id),
215
220
  isError,
216
221
  };