@parall/claude-agent 1.30.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 } 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) {
@@ -30,151 +46,207 @@ async function getAgentMeWithLegacyFallback(client: ParallClient, orgId: string)
30
46
  }
31
47
  }
32
48
 
49
+ function resolveProviderEnv(): void {
50
+ const pc = parseProviderConfig(process.env);
51
+ if (!pc) return;
52
+ clearAllProviderCreds(process.env);
53
+ const source = llmSource(pc);
54
+ if (source === 'parall') {
55
+ process.env.ANTHROPIC_AUTH_TOKEN = process.env.PRLL_API_KEY;
56
+ process.env.ANTHROPIC_BASE_URL = `${process.env.PRLL_API_URL}/api/llm`;
57
+ process.env.PRLL_CLAUDE_ALLOW_API_KEY = '1';
58
+ } else if (source === 'custom') {
59
+ if (pc.anthropic_auth_token) {
60
+ process.env.ANTHROPIC_AUTH_TOKEN = pc.anthropic_auth_token;
61
+ process.env.PRLL_CLAUDE_ALLOW_API_KEY = '1';
62
+ }
63
+ if (pc.anthropic_base_url) process.env.ANTHROPIC_BASE_URL = pc.anthropic_base_url;
64
+ }
65
+ }
66
+
33
67
  async function main() {
34
- 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);
35
73
 
36
- if (
37
- config.allowApiKey &&
38
- (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN)
39
- ) {
40
- // Surface the billing path: default is OAuth (Claude.ai subscription); this
41
- // branch means the operator opted into Anthropic API pay-per-use, and the
42
- // whole point of the strip default is that "silent" is the bad state.
43
- log.warn(
44
- "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.",
45
- );
46
- }
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
+ }
47
82
 
48
- const client = new ParallClient({
49
- baseUrl: config.apiUrl,
50
- token: config.apiKey,
51
- swimlaneName: config.swimlaneName,
52
- });
83
+ const client = new ParallClient({
84
+ baseUrl: config.apiUrl,
85
+ token: config.apiKey,
86
+ swimlaneName: config.swimlaneName,
87
+ });
53
88
 
54
- const me = await getAgentMeWithLegacyFallback(client, config.orgId);
55
- const agentUserId = me.id;
56
- const agentLog = childLogger(log, agentUserId);
57
- activeLog = agentLog;
58
- ensureClaudeWorkspace(config.workspaceDir, agentLog, {
59
- userId: agentUserId,
60
- displayName: me.display_name,
61
- description: me.agent_profile?.description ?? undefined,
62
- });
63
- const runtimeKey = config.runtimeKey || buildClaudeRuntimeKey(agentUserId);
64
- const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
65
- const sessionManager = new ClaudeSessionManager(
66
- runtimeKey,
67
- sessionStateFilePath,
68
- agentLog,
69
- );
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);
70
101
 
71
- const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
72
- const ws = new ParallWs({
73
- getTicket: () => client.getWsTicket(),
74
- wsUrl: resolvedWsUrl,
75
- });
102
+ const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
103
+ const ws = new ParallWs({
104
+ getTicket: () => client.getWsTicket(),
105
+ wsUrl: resolvedWsUrl,
106
+ });
76
107
 
77
- const configMgr = createPlatformConfigManager({ client, stateDir: config.stateDir, runtimeType: "claude-code", log: agentLog });
78
- const platformDefaults = await configMgr.fetch();
79
- let platformManaged = isPlatformManagedProfile(me.agent_profile);
80
- const resolvedModel = platformManaged ? (platformDefaults.model ?? config.model) : config.model;
81
- const resolvedEffort = platformManaged
82
- ? (platformDefaults.thinkingEffort ?? (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined))
83
- : (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined);
84
- if (platformDefaults.model) agentLog.info(`platform config: model=${platformDefaults.model} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
85
- 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
+ );
86
143
 
87
- const adapter = new ClaudeCodeAdapter({
88
- claudeBin: config.claudeBin,
89
- claudeHome: config.claudeHome,
90
- workspaceDir: config.workspaceDir,
91
- model: resolvedModel,
92
- permissionMode: config.permissionMode,
93
- allowedTools: config.allowedTools,
94
- disallowedTools: config.disallowedTools,
95
- additionalDirs: config.additionalDirs,
96
- appendSystemPrompt: config.appendSystemPrompt,
97
- allowApiKey: config.allowApiKey,
98
- sessionManager,
99
- // Static Parall credentials. The long-lived subprocess must authenticate
100
- // against Parall's API across many dispatches, so these cannot come from
101
- // a per-dispatch DispatchContext.
102
- apiUrl: config.apiUrl,
103
- apiKey: config.apiKey,
104
- orgId: config.orgId,
105
- contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
106
- stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
107
- });
108
- if (resolvedEffort) {
109
- adapter.updateConfig({ effort: resolvedEffort });
110
- }
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
+ }
111
170
 
112
- const gateway = new ParallAgentGateway({
113
- accountId: agentUserId,
114
- client,
115
- ws,
116
- connectionLabel: resolvedWsUrl,
117
- config: {
118
- parall_url: config.apiUrl,
119
- api_key: config.apiKey,
120
- org_id: config.orgId,
121
- },
122
- agentUserId,
123
- runtimeType: "claude-code",
124
- runtimeKey,
125
- runtimeRef: {
126
- hostname: os.hostname(),
127
- pid: process.pid,
128
- workspace_dir: config.workspaceDir,
129
- claude_home: config.claudeHome,
130
- },
131
- dispatchAdapter: adapter,
132
- log: agentLog,
133
- shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
134
- contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
135
- stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
136
- onConfigUpdate: async () => {
137
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
138
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
139
- const updated = await configMgr.fetch();
140
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
141
- adapter.updateConfig({
142
- model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
143
- effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
144
- });
145
- },
146
- onSessionReady: async () => {
147
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
148
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
149
- const updated = await configMgr.fetch();
150
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
151
- adapter.updateConfig({
152
- model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
153
- effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
154
- });
155
- },
156
- // Long-lived `claude` subprocesses outlive a single dispatch, so the
157
- // gateway's shuttingDown flag alone does not tear them down. Piggyback on
158
- // onBeforeDisconnect to close stdin and SIGTERM any survivors after
159
- // in-flight drains finish.
160
- onBeforeDisconnect: () => adapter.shutdown(),
161
- onNewSession: async () => {
162
- sessionManager.clearMainSession();
163
- await adapter.shutdown();
164
- },
165
- });
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
+ });
166
235
 
167
- const abortController = new AbortController();
168
- const abort = () => abortController.abort();
169
- process.on("SIGINT", abort);
170
- process.on("SIGTERM", abort);
236
+ const abortController = new AbortController();
237
+ const abort = () => abortController.abort();
238
+ process.on('SIGINT', abort);
239
+ process.on('SIGTERM', abort);
171
240
 
172
- try {
173
- agentLog.info(`starting self-hosted Claude runtime for ${me.display_name} (${agentUserId})`);
174
- 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
+ }
175
248
  } finally {
176
- process.off("SIGINT", abort);
177
- process.off("SIGTERM", abort);
249
+ await telemetry.shutdown();
178
250
  }
179
251
  }
180
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
  };