@nordbyte/nordrelay 0.3.1 → 0.4.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.
Files changed (48) hide show
  1. package/.env.example +45 -2
  2. package/README.md +204 -30
  3. package/dist/agent-activity.js +300 -0
  4. package/dist/agent-adapter.js +17 -30
  5. package/dist/agent-factory.js +27 -0
  6. package/dist/agent.js +123 -9
  7. package/dist/artifacts.js +1 -1
  8. package/dist/audit-log.js +1 -1
  9. package/dist/bot-ui.js +1 -1
  10. package/dist/bot.js +328 -159
  11. package/dist/claude-code-auth.js +121 -0
  12. package/dist/claude-code-cli.js +19 -0
  13. package/dist/claude-code-launch.js +73 -0
  14. package/dist/claude-code-session.js +660 -0
  15. package/dist/claude-code-state.js +590 -0
  16. package/dist/codex-session.js +12 -1
  17. package/dist/config.js +113 -9
  18. package/dist/hermes-api.js +150 -0
  19. package/dist/hermes-auth.js +96 -0
  20. package/dist/hermes-cli.js +19 -0
  21. package/dist/hermes-launch.js +57 -0
  22. package/dist/hermes-session.js +477 -0
  23. package/dist/hermes-state.js +609 -0
  24. package/dist/index.js +51 -8
  25. package/dist/openclaw-auth.js +27 -0
  26. package/dist/openclaw-cli.js +19 -0
  27. package/dist/openclaw-gateway.js +285 -0
  28. package/dist/openclaw-launch.js +65 -0
  29. package/dist/openclaw-session.js +549 -0
  30. package/dist/openclaw-state.js +409 -0
  31. package/dist/operations.js +83 -2
  32. package/dist/pi-auth.js +59 -0
  33. package/dist/pi-launch.js +61 -0
  34. package/dist/pi-rpc.js +18 -0
  35. package/dist/pi-session.js +103 -15
  36. package/dist/pi-state.js +253 -0
  37. package/dist/relay-runtime.js +673 -51
  38. package/dist/session-format.js +28 -18
  39. package/dist/session-registry.js +40 -15
  40. package/dist/settings-service.js +35 -4
  41. package/dist/web-dashboard-ui.js +18 -0
  42. package/dist/web-dashboard.js +329 -47
  43. package/package.json +8 -3
  44. package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
  45. package/plugins/nordrelay/commands/remote.md +2 -2
  46. package/plugins/nordrelay/scripts/nordrelay.mjs +131 -3
  47. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
  48. package/CHANGELOG.md +0 -26
@@ -0,0 +1,660 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { CLAUDE_CODE_AGENT_CAPABILITIES, CLAUDE_CODE_EFFORT_LEVELS, } from "./agent.js";
5
+ import { resolveClaudeCodeCli } from "./claude-code-cli.js";
6
+ import { claudeCodeProfileAsLaunchProfile, findClaudeCodeLaunchProfile, listClaudeCodeLaunchProfiles, } from "./claude-code-launch.js";
7
+ import { getClaudeCodeSession, listClaudeCodeSessions, listClaudeCodeWorkspaces, } from "./claude-code-state.js";
8
+ export class ClaudeCodeSessionService {
9
+ config;
10
+ cliPath;
11
+ currentWorkspace;
12
+ currentThreadId = null;
13
+ currentModel;
14
+ currentEffort;
15
+ currentLaunchProfile;
16
+ cachedModels = [];
17
+ cachedUsage;
18
+ processing = false;
19
+ abortController = null;
20
+ currentQuery = null;
21
+ lastStateRefreshAt = 0;
22
+ static STATE_CACHE_TTL_MS = 5_000;
23
+ constructor(config) {
24
+ this.config = config;
25
+ this.cliPath = resolveClaudeCodeCli(process.env, config.claudeCodeCliPath).path;
26
+ this.currentWorkspace = config.workspace;
27
+ this.currentModel = config.claudeCodeDefaultModel;
28
+ this.currentEffort = normalizeClaudeCodeEffort(config.claudeCodeDefaultEffort);
29
+ this.currentLaunchProfile = findClaudeCodeLaunchProfile(config.claudeCodeDefaultLaunchProfileId, config.enableUnsafeLaunchProfiles);
30
+ this.cachedModels = defaultClaudeCodeModels(this.currentModel);
31
+ }
32
+ static async create(config, options) {
33
+ const service = new ClaudeCodeSessionService(config);
34
+ service.currentWorkspace = options?.workspace ?? config.workspace;
35
+ service.currentModel = options?.model ?? config.claudeCodeDefaultModel;
36
+ service.currentEffort = normalizeClaudeCodeEffort(options?.reasoningEffort ?? config.claudeCodeDefaultEffort);
37
+ service.currentLaunchProfile = findClaudeCodeLaunchProfile(options?.launchProfileId ?? config.claudeCodeDefaultLaunchProfileId, config.enableUnsafeLaunchProfiles);
38
+ await service.refreshModels().catch(() => { });
39
+ if (options?.resumeThreadId) {
40
+ await service.resumeThread(options.resumeThreadId);
41
+ return service;
42
+ }
43
+ if (!options?.deferThreadStart) {
44
+ await service.newThread(service.currentWorkspace, service.currentModel);
45
+ }
46
+ return service;
47
+ }
48
+ getInfo() {
49
+ this.refreshFromState();
50
+ return {
51
+ agentId: "claude-code",
52
+ agentLabel: "Claude Code",
53
+ threadId: this.currentThreadId,
54
+ workspace: this.currentWorkspace,
55
+ model: this.currentModel,
56
+ reasoningEffort: this.currentEffort,
57
+ launchProfileId: this.currentLaunchProfile.id,
58
+ launchProfileLabel: this.currentLaunchProfile.label,
59
+ launchProfileBehavior: this.currentLaunchProfile.behavior,
60
+ sandboxMode: "host",
61
+ approvalPolicy: this.currentLaunchProfile.permissionMode,
62
+ fastMode: false,
63
+ unsafeLaunch: this.currentLaunchProfile.unsafe,
64
+ sessionUsage: this.cachedUsage,
65
+ sessionPath: this.currentThreadId ? this.getRecord(this.currentThreadId)?.sessionPath : undefined,
66
+ capabilities: CLAUDE_CODE_AGENT_CAPABILITIES,
67
+ };
68
+ }
69
+ isProcessing() {
70
+ return this.processing;
71
+ }
72
+ getActiveThreadId() {
73
+ return this.currentThreadId;
74
+ }
75
+ hasActiveThread() {
76
+ return Boolean(this.currentThreadId);
77
+ }
78
+ getCurrentWorkspace() {
79
+ return this.currentWorkspace;
80
+ }
81
+ async prompt(input, callbacks) {
82
+ if (this.processing) {
83
+ throw new Error("A Claude Code turn is already in progress");
84
+ }
85
+ const prompt = await this.buildPrompt(input);
86
+ const abortController = new AbortController();
87
+ const stream = query({
88
+ prompt,
89
+ options: this.queryOptions(abortController),
90
+ });
91
+ const openTools = new Map();
92
+ const contentBlockTools = new Map();
93
+ let streamedOutput = "";
94
+ let assistantOutputHandled = false;
95
+ let didEnd = false;
96
+ let toolCounter = 0;
97
+ this.processing = true;
98
+ this.abortController = abortController;
99
+ this.currentQuery = stream;
100
+ try {
101
+ for await (const message of stream) {
102
+ const sessionId = sessionIdOf(message);
103
+ if (sessionId) {
104
+ this.currentThreadId = sessionId;
105
+ }
106
+ const type = stringValue(message.type);
107
+ if (type === "system") {
108
+ this.handleSystemMessage(message);
109
+ this.handleTaskSystemMessage(message, callbacks, openTools);
110
+ continue;
111
+ }
112
+ if (type === "stream_event") {
113
+ const result = handleStreamEvent(message, callbacks, contentBlockTools, openTools, this.currentThreadId ?? "claude", ++toolCounter);
114
+ toolCounter = result.toolCounter;
115
+ if (result.delta) {
116
+ streamedOutput += result.delta;
117
+ }
118
+ continue;
119
+ }
120
+ if (type === "assistant") {
121
+ const handledText = handleAssistantMessage(message, callbacks, openTools, streamedOutput);
122
+ assistantOutputHandled = assistantOutputHandled || handledText;
123
+ continue;
124
+ }
125
+ if (type === "tool_progress") {
126
+ handleToolProgress(message, callbacks, openTools);
127
+ continue;
128
+ }
129
+ if (type === "tool_use_summary") {
130
+ handleToolSummary(message, callbacks, openTools);
131
+ continue;
132
+ }
133
+ if (type === "result") {
134
+ const text = this.handleResultMessage(message, callbacks);
135
+ if (text && !streamedOutput && !assistantOutputHandled) {
136
+ callbacks.onTextDelta(text);
137
+ }
138
+ didEnd = true;
139
+ callbacks.onAgentEnd();
140
+ continue;
141
+ }
142
+ if (type === "rate_limit_event") {
143
+ const info = objectValue(message.rate_limit_info);
144
+ const toolId = `claude-rate-limit-${this.currentThreadId ?? "session"}`;
145
+ callbacks.onToolStart("rate_limit", toolId);
146
+ callbacks.onToolUpdate(toolId, stringifyPreview(info) ?? "Rate limit update");
147
+ callbacks.onToolEnd(toolId, false);
148
+ }
149
+ }
150
+ if (!didEnd) {
151
+ callbacks.onAgentEnd();
152
+ }
153
+ this.refreshFromState({ force: true });
154
+ }
155
+ catch (error) {
156
+ if (isAbortError(error)) {
157
+ throw new Error("Claude Code run was aborted");
158
+ }
159
+ throw error;
160
+ }
161
+ finally {
162
+ for (const tool of openTools.values()) {
163
+ callbacks.onToolEnd(tool.id, false);
164
+ }
165
+ this.currentQuery?.close();
166
+ this.currentQuery = null;
167
+ this.abortController = null;
168
+ this.processing = false;
169
+ }
170
+ }
171
+ async abort() {
172
+ await this.currentQuery?.interrupt().catch(() => { });
173
+ this.abortController?.abort();
174
+ this.currentQuery?.close();
175
+ this.processing = false;
176
+ }
177
+ async newThread(workspace, model) {
178
+ this.ensureIdle("start a new Claude Code session");
179
+ this.currentWorkspace = workspace ?? this.currentWorkspace;
180
+ if (model) {
181
+ this.currentModel = model;
182
+ }
183
+ this.currentThreadId = null;
184
+ this.cachedUsage = undefined;
185
+ this.lastStateRefreshAt = Date.now();
186
+ return this.getInfo();
187
+ }
188
+ async resumeThread(threadId) {
189
+ this.ensureIdle("resume Claude Code session");
190
+ const record = this.getRecord(threadId);
191
+ if (record) {
192
+ this.applyRecord(record);
193
+ }
194
+ else {
195
+ this.currentThreadId = threadId.trim();
196
+ }
197
+ return this.getInfo();
198
+ }
199
+ async switchSession(threadId) {
200
+ this.ensureIdle("switch Claude Code session");
201
+ const record = this.getRecord(threadId);
202
+ if (!record) {
203
+ throw new Error(`Unknown Claude Code session: ${threadId}`);
204
+ }
205
+ this.applyRecord(record);
206
+ this.lastStateRefreshAt = Date.now();
207
+ return this.getInfo();
208
+ }
209
+ listAllSessions(limit) {
210
+ return listClaudeCodeSessions(limit ?? 20, this.stateOptions());
211
+ }
212
+ listWorkspaces() {
213
+ const workspaces = new Set(listClaudeCodeWorkspaces(this.stateOptions()));
214
+ workspaces.add(this.currentWorkspace);
215
+ workspaces.add(this.config.workspace);
216
+ return [...workspaces].sort((left, right) => left.localeCompare(right));
217
+ }
218
+ async refreshModels() {
219
+ this.cachedModels = defaultClaudeCodeModels(this.currentModel);
220
+ }
221
+ listModels() {
222
+ const models = [...this.cachedModels];
223
+ if (this.currentModel && !models.some((model) => model.slug === this.currentModel)) {
224
+ models.unshift({ slug: this.currentModel, displayName: this.currentModel, supportsThinking: true, supportsImages: true });
225
+ }
226
+ return models;
227
+ }
228
+ listLaunchProfiles() {
229
+ return listClaudeCodeLaunchProfiles(this.config.enableUnsafeLaunchProfiles);
230
+ }
231
+ getSessionRecord(threadId) {
232
+ return this.getRecord(threadId);
233
+ }
234
+ setModel(slug) {
235
+ this.currentModel = slug;
236
+ return slug;
237
+ }
238
+ setModelForCurrentSession(slug) {
239
+ this.ensureIdle("change Claude Code model");
240
+ this.currentModel = slug;
241
+ return { value: slug, appliedToActiveThread: Boolean(this.currentThreadId) };
242
+ }
243
+ setReasoningEffort(effort) {
244
+ this.currentEffort = normalizeClaudeCodeEffort(effort);
245
+ }
246
+ setReasoningEffortForCurrentSession(effort) {
247
+ this.ensureIdle("change Claude Code effort");
248
+ const value = normalizeClaudeCodeEffort(effort);
249
+ if (!value) {
250
+ throw new Error("Claude Code effort is empty");
251
+ }
252
+ this.currentEffort = value;
253
+ return { value, appliedToActiveThread: Boolean(this.currentThreadId) };
254
+ }
255
+ setLaunchProfile(profileId) {
256
+ this.ensureIdle("change Claude Code profile");
257
+ this.currentLaunchProfile = findClaudeCodeLaunchProfile(profileId, this.config.enableUnsafeLaunchProfiles);
258
+ return claudeCodeProfileAsLaunchProfile(this.currentLaunchProfile);
259
+ }
260
+ setFastMode() {
261
+ throw new Error("Fast mode is only supported by Codex sessions");
262
+ }
263
+ getSelectedLaunchProfile() {
264
+ return claudeCodeProfileAsLaunchProfile(this.currentLaunchProfile);
265
+ }
266
+ syncFromAgentState() {
267
+ const before = this.getInfo();
268
+ this.refreshFromState({ force: true });
269
+ const after = this.getInfo();
270
+ const changedFields = [];
271
+ if (before.threadId !== after.threadId)
272
+ changedFields.push("thread");
273
+ if (before.workspace !== after.workspace)
274
+ changedFields.push("workspace");
275
+ if (before.model !== after.model)
276
+ changedFields.push("model");
277
+ if (before.reasoningEffort !== after.reasoningEffort)
278
+ changedFields.push("effort");
279
+ return {
280
+ threadId: this.currentThreadId,
281
+ changed: changedFields.length > 0,
282
+ reattached: false,
283
+ changedFields,
284
+ info: after,
285
+ };
286
+ }
287
+ handback() {
288
+ const threadId = this.currentThreadId;
289
+ const workspace = this.currentWorkspace;
290
+ this.currentQuery?.close();
291
+ this.currentThreadId = null;
292
+ return {
293
+ threadId,
294
+ workspace,
295
+ command: threadId
296
+ ? `cd ${shellQuote(workspace)} && ${shellQuote(this.cliPath ?? "claude")} --resume ${shellQuote(threadId)}`
297
+ : undefined,
298
+ label: "Claude Code CLI",
299
+ };
300
+ }
301
+ dispose() {
302
+ this.currentQuery?.close();
303
+ this.abortController?.abort();
304
+ this.processing = false;
305
+ }
306
+ queryOptions(abortController) {
307
+ const options = {
308
+ abortController,
309
+ cwd: this.currentWorkspace,
310
+ includePartialMessages: true,
311
+ includeHookEvents: true,
312
+ promptSuggestions: true,
313
+ agentProgressSummaries: true,
314
+ maxTurns: this.config.claudeCodeMaxTurns,
315
+ resume: this.currentThreadId ?? undefined,
316
+ model: this.currentModel,
317
+ permissionMode: this.currentLaunchProfile.permissionMode,
318
+ allowDangerouslySkipPermissions: this.currentLaunchProfile.allowDangerouslySkipPermissions,
319
+ pathToClaudeCodeExecutable: this.cliPath,
320
+ env: {
321
+ ...process.env,
322
+ CLAUDE_AGENT_SDK_CLIENT_APP: `nordrelay/${process.env.npm_package_version ?? "local"}`,
323
+ },
324
+ };
325
+ if (this.currentLaunchProfile.tools !== undefined) {
326
+ options.tools = this.currentLaunchProfile.tools;
327
+ }
328
+ if (this.currentLaunchProfile.allowedTools) {
329
+ options.allowedTools = this.currentLaunchProfile.allowedTools;
330
+ }
331
+ if (this.currentLaunchProfile.disallowedTools) {
332
+ options.disallowedTools = this.currentLaunchProfile.disallowedTools;
333
+ }
334
+ if (this.currentLaunchProfile.instructions && this.currentLaunchProfile.permissionMode === "plan") {
335
+ options.planModeInstructions = this.currentLaunchProfile.instructions;
336
+ }
337
+ if (this.currentEffort === "off") {
338
+ options.thinking = { type: "disabled" };
339
+ }
340
+ else if (this.currentEffort && isClaudeEffortLevel(this.currentEffort)) {
341
+ options.effort = this.currentEffort;
342
+ options.thinking = { type: "adaptive", display: "summarized" };
343
+ }
344
+ return options;
345
+ }
346
+ handleSystemMessage(message) {
347
+ const object = message;
348
+ const subtype = stringValue(object.subtype);
349
+ if (subtype === "init") {
350
+ this.currentWorkspace = stringValue(object.cwd) ?? this.currentWorkspace;
351
+ this.currentModel = stringValue(object.model) ?? this.currentModel;
352
+ }
353
+ }
354
+ handleTaskSystemMessage(message, callbacks, openTools) {
355
+ const object = message;
356
+ const subtype = stringValue(object.subtype);
357
+ if (subtype === "task_started") {
358
+ const taskId = stringValue(object.task_id) ?? `task-${openTools.size + 1}`;
359
+ const toolName = stringValue(object.task_type) ?? stringValue(object.workflow_name) ?? "task";
360
+ const tool = { id: taskId, name: toolName };
361
+ openTools.set(taskId, tool);
362
+ callbacks.onToolStart(toolName, taskId);
363
+ const description = stringValue(object.description) ?? stringValue(object.prompt);
364
+ if (description)
365
+ callbacks.onToolUpdate(taskId, description);
366
+ return;
367
+ }
368
+ if (subtype === "task_progress") {
369
+ const taskId = stringValue(object.task_id);
370
+ if (taskId) {
371
+ const tool = openTools.get(taskId) ?? { id: taskId, name: stringValue(object.last_tool_name) ?? "task" };
372
+ openTools.set(taskId, tool);
373
+ callbacks.onToolUpdate(taskId, stringValue(object.summary) ?? stringValue(object.description) ?? "Task progress");
374
+ }
375
+ return;
376
+ }
377
+ if (subtype === "task_notification") {
378
+ const taskId = stringValue(object.task_id);
379
+ if (taskId) {
380
+ const tool = openTools.get(taskId) ?? { id: taskId, name: "task" };
381
+ callbacks.onToolUpdate(tool.id, stringValue(object.summary) ?? stringValue(object.output_file) ?? "Task completed");
382
+ callbacks.onToolEnd(tool.id, stringValue(object.status) === "failed");
383
+ openTools.delete(tool.id);
384
+ }
385
+ return;
386
+ }
387
+ if (subtype === "permission_denied") {
388
+ const toolId = stringValue(object.tool_use_id) ?? `permission-${openTools.size + 1}`;
389
+ const toolName = stringValue(object.tool_name) ?? "permission";
390
+ callbacks.onToolStart(toolName, toolId);
391
+ callbacks.onToolUpdate(toolId, stringValue(object.message) ?? stringValue(object.decision_reason) ?? "Permission denied");
392
+ callbacks.onToolEnd(toolId, true);
393
+ }
394
+ }
395
+ handleResultMessage(message, callbacks) {
396
+ const object = message;
397
+ const usage = objectValue(object.usage);
398
+ const input = numberValue(usage?.input_tokens) ?? numberValue(usage?.inputTokens) ?? 0;
399
+ const output = numberValue(usage?.output_tokens) ?? numberValue(usage?.outputTokens) ?? 0;
400
+ const cacheRead = numberValue(usage?.cache_read_input_tokens) ?? numberValue(usage?.cacheReadInputTokens) ?? numberValue(usage?.cache_read_tokens) ?? 0;
401
+ const cacheWrite = numberValue(usage?.cache_creation_input_tokens) ?? numberValue(usage?.cacheCreationInputTokens) ?? numberValue(usage?.cache_write_tokens) ?? 0;
402
+ const total = input + output + cacheRead + cacheWrite;
403
+ if (total > 0) {
404
+ this.cachedUsage = {
405
+ input,
406
+ output,
407
+ cacheRead,
408
+ cacheWrite,
409
+ total,
410
+ cost: numberValue(object.total_cost_usd) ?? undefined,
411
+ };
412
+ callbacks.onTurnComplete?.({
413
+ inputTokens: input,
414
+ cachedInputTokens: cacheRead,
415
+ outputTokens: output,
416
+ });
417
+ }
418
+ const errors = arrayValue(object.errors).map(stringValue).filter((value) => Boolean(value));
419
+ if (errors.length > 0) {
420
+ const toolId = `claude-result-${this.currentThreadId ?? "session"}`;
421
+ callbacks.onToolStart("result_error", toolId);
422
+ callbacks.onToolUpdate(toolId, errors.join("\n"));
423
+ callbacks.onToolEnd(toolId, true);
424
+ }
425
+ return stringValue(object.result);
426
+ }
427
+ refreshFromState(options = {}) {
428
+ if (!this.currentThreadId) {
429
+ return;
430
+ }
431
+ const now = Date.now();
432
+ if (!options.force && now - this.lastStateRefreshAt < ClaudeCodeSessionService.STATE_CACHE_TTL_MS) {
433
+ return;
434
+ }
435
+ this.lastStateRefreshAt = now;
436
+ const record = this.getRecord(this.currentThreadId);
437
+ if (record) {
438
+ this.applyRecord(record);
439
+ }
440
+ }
441
+ applyRecord(record) {
442
+ this.currentThreadId = record.id;
443
+ this.currentWorkspace = record.cwd || this.currentWorkspace;
444
+ this.currentModel = record.model ?? this.currentModel;
445
+ this.currentEffort = normalizeClaudeCodeEffort(record.reasoningEffort ?? this.currentEffort);
446
+ this.cachedUsage = record.usage ?? this.cachedUsage;
447
+ }
448
+ getRecord(threadId) {
449
+ return getClaudeCodeSession(threadId, this.stateOptions());
450
+ }
451
+ stateOptions() {
452
+ return {
453
+ configDir: this.config.claudeCodeConfigDir,
454
+ workspace: this.currentWorkspace || this.config.workspace,
455
+ };
456
+ }
457
+ ensureIdle(action) {
458
+ if (this.processing) {
459
+ throw new Error(`Cannot ${action} while a turn is in progress`);
460
+ }
461
+ }
462
+ async buildPrompt(input) {
463
+ if (typeof input === "string") {
464
+ return input;
465
+ }
466
+ const parts = [input.stagedFileInstructions, input.text].filter((part) => Boolean(part?.trim()));
467
+ const imagePaths = input.imagePaths ?? [];
468
+ if (imagePaths.length > 0) {
469
+ const attachments = await Promise.all(imagePaths.map(async (imagePath) => {
470
+ const data = await readFile(imagePath);
471
+ return `- ${imagePath} (${mimeTypeForImage(imagePath)}, ${data.byteLength} bytes)`;
472
+ }));
473
+ parts.push([
474
+ "Attached image files are available on disk. Inspect them if needed:",
475
+ ...attachments,
476
+ ].join("\n"));
477
+ }
478
+ return parts.join("\n\n").trim() || "Please inspect the attached file(s).";
479
+ }
480
+ }
481
+ function handleStreamEvent(message, callbacks, contentBlockTools, openTools, fallbackId, toolCounter) {
482
+ const event = objectValue(message.event);
483
+ const eventType = stringValue(event?.type);
484
+ if (eventType === "content_block_delta") {
485
+ const delta = objectValue(event?.delta);
486
+ const text = stringValue(delta?.text) ?? stringValue(delta?.partial_json) ?? "";
487
+ if (text && stringValue(delta?.type) !== "input_json_delta") {
488
+ callbacks.onTextDelta(text);
489
+ return { delta: text, toolCounter };
490
+ }
491
+ const index = numberValue(event?.index);
492
+ if (index !== null && text) {
493
+ const tool = contentBlockTools.get(index);
494
+ if (tool)
495
+ callbacks.onToolUpdate(tool.id, text);
496
+ }
497
+ return { delta: "", toolCounter };
498
+ }
499
+ if (eventType === "content_block_start") {
500
+ const index = numberValue(event?.index);
501
+ const block = objectValue(event?.content_block);
502
+ if (stringValue(block?.type) === "tool_use") {
503
+ const toolName = stringValue(block?.name) ?? "tool";
504
+ const toolId = stringValue(block?.id) ?? `${fallbackId}-${toolName}-${toolCounter}`;
505
+ const tool = { id: toolId, name: toolName };
506
+ if (index !== null)
507
+ contentBlockTools.set(index, tool);
508
+ openTools.set(tool.id, tool);
509
+ callbacks.onToolStart(tool.name, tool.id);
510
+ }
511
+ return { delta: "", toolCounter };
512
+ }
513
+ if (eventType === "content_block_stop") {
514
+ const index = numberValue(event?.index);
515
+ const tool = index !== null ? contentBlockTools.get(index) : undefined;
516
+ if (tool) {
517
+ contentBlockTools.delete(index);
518
+ callbacks.onToolEnd(tool.id, false);
519
+ openTools.delete(tool.id);
520
+ }
521
+ }
522
+ return { delta: "", toolCounter };
523
+ }
524
+ function handleAssistantMessage(message, callbacks, openTools, alreadyStreamedText) {
525
+ const root = message;
526
+ const assistant = objectValue(root.message);
527
+ const content = arrayValue(assistant?.content);
528
+ let text = "";
529
+ for (const part of content) {
530
+ const block = objectValue(part);
531
+ if (!block)
532
+ continue;
533
+ const blockType = stringValue(block.type);
534
+ if (blockType === "text") {
535
+ text += stringValue(block.text) ?? "";
536
+ }
537
+ else if (blockType === "tool_use") {
538
+ const toolName = stringValue(block.name) ?? "tool";
539
+ const toolId = stringValue(block.id) ?? `${toolName}-${openTools.size + 1}`;
540
+ if (!openTools.has(toolId)) {
541
+ const tool = { id: toolId, name: toolName };
542
+ openTools.set(toolId, tool);
543
+ callbacks.onToolStart(toolName, toolId);
544
+ }
545
+ const preview = stringifyPreview(block.input);
546
+ if (preview)
547
+ callbacks.onToolUpdate(toolId, preview);
548
+ }
549
+ }
550
+ const delta = text && !alreadyStreamedText ? text : "";
551
+ if (delta) {
552
+ callbacks.onTextDelta(delta);
553
+ return true;
554
+ }
555
+ return false;
556
+ }
557
+ function handleToolProgress(message, callbacks, openTools) {
558
+ const object = message;
559
+ const toolId = stringValue(object.tool_use_id);
560
+ if (!toolId)
561
+ return;
562
+ const toolName = stringValue(object.tool_name) ?? "tool";
563
+ if (!openTools.has(toolId)) {
564
+ openTools.set(toolId, { id: toolId, name: toolName });
565
+ callbacks.onToolStart(toolName, toolId);
566
+ }
567
+ const elapsed = numberValue(object.elapsed_time_seconds);
568
+ callbacks.onToolUpdate(toolId, elapsed !== null ? `${toolName} running for ${Math.round(elapsed)}s` : `${toolName} running`);
569
+ }
570
+ function handleToolSummary(message, callbacks, openTools) {
571
+ const object = message;
572
+ const summary = stringValue(object.summary);
573
+ for (const toolId of arrayValue(object.preceding_tool_use_ids).map(stringValue).filter((value) => Boolean(value))) {
574
+ const tool = openTools.get(toolId);
575
+ if (summary)
576
+ callbacks.onToolUpdate(toolId, summary);
577
+ if (tool) {
578
+ callbacks.onToolEnd(tool.id, false);
579
+ openTools.delete(tool.id);
580
+ }
581
+ }
582
+ }
583
+ function defaultClaudeCodeModels(currentModel) {
584
+ const models = [
585
+ { slug: "sonnet", displayName: "sonnet", supportsThinking: true, supportsImages: true },
586
+ { slug: "opus", displayName: "opus", supportsThinking: true, supportsImages: true },
587
+ { slug: "haiku", displayName: "haiku", supportsThinking: true, supportsImages: true },
588
+ { slug: "claude-sonnet-4-6", displayName: "claude-sonnet-4-6", supportsThinking: true, supportsImages: true },
589
+ { slug: "claude-opus-4-7", displayName: "claude-opus-4-7", supportsThinking: true, supportsImages: true },
590
+ { slug: "claude-3-5-haiku-latest", displayName: "claude-3-5-haiku-latest", supportsThinking: false, supportsImages: true },
591
+ ];
592
+ if (currentModel && !models.some((model) => model.slug === currentModel)) {
593
+ models.unshift({ slug: currentModel, displayName: currentModel, supportsThinking: true, supportsImages: true });
594
+ }
595
+ return models;
596
+ }
597
+ function normalizeClaudeCodeEffort(value) {
598
+ if (!value) {
599
+ return undefined;
600
+ }
601
+ if (CLAUDE_CODE_EFFORT_LEVELS.includes(value)) {
602
+ return value;
603
+ }
604
+ throw new Error(`Unsupported Claude Code effort: ${value}`);
605
+ }
606
+ function isClaudeEffortLevel(value) {
607
+ return value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
608
+ }
609
+ function sessionIdOf(message) {
610
+ return stringValue(message.session_id) ?? stringValue(message.sessionId);
611
+ }
612
+ function objectValue(value) {
613
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
614
+ }
615
+ function arrayValue(value) {
616
+ return Array.isArray(value) ? value : [];
617
+ }
618
+ function stringValue(value) {
619
+ if (typeof value === "string" && value.trim()) {
620
+ return value;
621
+ }
622
+ if (typeof value === "number" && Number.isFinite(value)) {
623
+ return String(value);
624
+ }
625
+ return null;
626
+ }
627
+ function numberValue(value) {
628
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
629
+ }
630
+ function stringifyPreview(value) {
631
+ if (value === undefined || value === null) {
632
+ return null;
633
+ }
634
+ if (typeof value === "string") {
635
+ return value.trim() || null;
636
+ }
637
+ try {
638
+ const text = JSON.stringify(value);
639
+ return text.length <= 700 ? text : `${text.slice(0, 697)}...`;
640
+ }
641
+ catch {
642
+ return null;
643
+ }
644
+ }
645
+ function isAbortError(error) {
646
+ return error instanceof Error && (error.name === "AbortError" || error.message.toLowerCase().includes("abort"));
647
+ }
648
+ function mimeTypeForImage(filePath) {
649
+ const extension = path.extname(filePath).toLowerCase();
650
+ if (extension === ".png")
651
+ return "image/png";
652
+ if (extension === ".webp")
653
+ return "image/webp";
654
+ if (extension === ".gif")
655
+ return "image/gif";
656
+ return "image/jpeg";
657
+ }
658
+ function shellQuote(value) {
659
+ return `'${value.replace(/'/g, `'\\''`)}'`;
660
+ }