@nordbyte/nordrelay 0.3.1 → 0.4.1

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 (55) hide show
  1. package/.env.example +45 -2
  2. package/README.md +221 -35
  3. package/dist/access-control.js +3 -0
  4. package/dist/agent-activity.js +300 -0
  5. package/dist/agent-adapter.js +17 -30
  6. package/dist/agent-factory.js +27 -0
  7. package/dist/agent-feature-matrix.js +42 -0
  8. package/dist/agent-updates.js +294 -0
  9. package/dist/agent.js +123 -9
  10. package/dist/artifacts.js +1 -1
  11. package/dist/audit-log.js +1 -1
  12. package/dist/bot-ui.js +1 -1
  13. package/dist/bot.js +483 -354
  14. package/dist/channel-actions.js +372 -0
  15. package/dist/claude-code-auth.js +121 -0
  16. package/dist/claude-code-cli.js +19 -0
  17. package/dist/claude-code-launch.js +73 -0
  18. package/dist/claude-code-session.js +660 -0
  19. package/dist/claude-code-state.js +590 -0
  20. package/dist/codex-session.js +12 -1
  21. package/dist/config.js +113 -9
  22. package/dist/hermes-api.js +150 -0
  23. package/dist/hermes-auth.js +96 -0
  24. package/dist/hermes-cli.js +19 -0
  25. package/dist/hermes-launch.js +57 -0
  26. package/dist/hermes-session.js +477 -0
  27. package/dist/hermes-state.js +609 -0
  28. package/dist/index.js +51 -8
  29. package/dist/openclaw-auth.js +27 -0
  30. package/dist/openclaw-cli.js +19 -0
  31. package/dist/openclaw-gateway.js +285 -0
  32. package/dist/openclaw-launch.js +65 -0
  33. package/dist/openclaw-session.js +549 -0
  34. package/dist/openclaw-state.js +409 -0
  35. package/dist/operations.js +115 -9
  36. package/dist/pi-auth.js +59 -0
  37. package/dist/pi-launch.js +61 -0
  38. package/dist/pi-rpc.js +18 -0
  39. package/dist/pi-session.js +103 -15
  40. package/dist/pi-state.js +253 -0
  41. package/dist/relay-runtime.js +798 -72
  42. package/dist/session-format.js +98 -19
  43. package/dist/session-registry.js +40 -15
  44. package/dist/settings-service.js +35 -4
  45. package/dist/web-dashboard-assets.js +2 -0
  46. package/dist/web-dashboard-client.js +275 -0
  47. package/dist/web-dashboard-style.js +9 -0
  48. package/dist/web-dashboard-ui.js +18 -0
  49. package/dist/web-dashboard.js +296 -196
  50. package/package.json +8 -3
  51. package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
  52. package/plugins/nordrelay/commands/remote.md +2 -2
  53. package/plugins/nordrelay/scripts/nordrelay.mjs +187 -12
  54. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
  55. package/CHANGELOG.md +0 -26
@@ -0,0 +1,549 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { OPENCLAW_AGENT_CAPABILITIES, OPENCLAW_THINKING_LEVELS, } from "./agent.js";
6
+ import { OpenClawGatewayClient, extractOpenClawOutputText } from "./openclaw-gateway.js";
7
+ import { findOpenClawLaunchProfile, listOpenClawLaunchProfiles, openClawProfileAsLaunchProfile, } from "./openclaw-launch.js";
8
+ import { resolveOpenClawCli } from "./openclaw-cli.js";
9
+ import { getOpenClawSession, listOpenClawSessions, listOpenClawWorkspaces, } from "./openclaw-state.js";
10
+ export class OpenClawSessionService {
11
+ config;
12
+ cliPath;
13
+ currentWorkspace;
14
+ currentThreadId = null;
15
+ currentModel;
16
+ currentThinking;
17
+ currentLaunchProfile;
18
+ currentOpenClawAgentId;
19
+ cachedModels = [];
20
+ cachedUsage;
21
+ processing = false;
22
+ abortController = null;
23
+ currentGateway = null;
24
+ currentRunId = null;
25
+ modelsLoadedAt = 0;
26
+ lastStateRefreshAt = 0;
27
+ static MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
28
+ static STATE_CACHE_TTL_MS = 5_000;
29
+ constructor(config) {
30
+ this.config = config;
31
+ this.cliPath = resolveOpenClawCli(process.env, config.openClawCliPath).path;
32
+ this.currentWorkspace = config.workspace;
33
+ this.currentModel = config.openClawDefaultModel;
34
+ this.currentThinking = config.openClawDefaultThinking;
35
+ this.currentLaunchProfile = findOpenClawLaunchProfile(config.openClawDefaultLaunchProfileId);
36
+ this.currentOpenClawAgentId = config.openClawAgentId;
37
+ }
38
+ static async create(config, options) {
39
+ const service = new OpenClawSessionService(config);
40
+ service.currentWorkspace = options?.workspace ?? config.workspace;
41
+ service.currentModel = options?.model ?? config.openClawDefaultModel;
42
+ service.currentThinking = normalizeOpenClawThinking(options?.reasoningEffort ?? config.openClawDefaultThinking);
43
+ service.currentLaunchProfile = findOpenClawLaunchProfile(options?.launchProfileId ?? config.openClawDefaultLaunchProfileId);
44
+ await service.refreshModels().catch(() => { });
45
+ if (options?.resumeThreadId) {
46
+ await service.resumeThread(options.resumeThreadId);
47
+ return service;
48
+ }
49
+ if (!options?.deferThreadStart) {
50
+ await service.newThread(service.currentWorkspace, service.currentModel);
51
+ }
52
+ return service;
53
+ }
54
+ getInfo() {
55
+ this.refreshFromState();
56
+ return {
57
+ agentId: "openclaw",
58
+ agentLabel: "OpenClaw",
59
+ threadId: this.currentThreadId,
60
+ workspace: this.currentWorkspace,
61
+ model: this.currentModel,
62
+ reasoningEffort: this.currentThinking,
63
+ launchProfileId: this.currentLaunchProfile.id,
64
+ launchProfileLabel: this.currentLaunchProfile.label,
65
+ launchProfileBehavior: this.currentLaunchProfile.behavior,
66
+ sandboxMode: "host",
67
+ approvalPolicy: "never",
68
+ fastMode: false,
69
+ unsafeLaunch: this.currentLaunchProfile.unsafe,
70
+ sessionUsage: this.cachedUsage,
71
+ capabilities: OPENCLAW_AGENT_CAPABILITIES,
72
+ };
73
+ }
74
+ isProcessing() {
75
+ return this.processing;
76
+ }
77
+ getActiveThreadId() {
78
+ return this.currentThreadId;
79
+ }
80
+ hasActiveThread() {
81
+ return Boolean(this.currentThreadId);
82
+ }
83
+ getCurrentWorkspace() {
84
+ return this.currentWorkspace;
85
+ }
86
+ async prompt(input, callbacks) {
87
+ if (this.processing) {
88
+ throw new Error("An OpenClaw turn is already in progress");
89
+ }
90
+ await this.ensureSessionStarted();
91
+ const threadId = this.currentThreadId;
92
+ const prompt = await this.buildPrompt(input);
93
+ const attachments = await this.buildAttachments(input);
94
+ const abortController = new AbortController();
95
+ const gateway = this.createGatewayClient();
96
+ const openTools = new Map();
97
+ let streamedOutput = "";
98
+ let didEnd = false;
99
+ let toolCounter = 0;
100
+ this.processing = true;
101
+ this.abortController = abortController;
102
+ this.currentGateway = gateway;
103
+ try {
104
+ const result = await gateway.runAgent({
105
+ message: prompt,
106
+ sessionId: threadId,
107
+ agentId: this.currentOpenClawAgentId,
108
+ model: this.currentModel,
109
+ thinking: this.currentThinking,
110
+ workspace: this.currentWorkspace,
111
+ local: this.currentLaunchProfile.local,
112
+ deliver: this.currentLaunchProfile.deliver,
113
+ instructions: this.currentLaunchProfile.instructions,
114
+ attachments,
115
+ onRunId: (runId) => {
116
+ this.currentRunId = runId;
117
+ },
118
+ }, (event) => {
119
+ const eventName = stringValue(event.event) ?? stringValue(event.type);
120
+ const payload = objectValue(event.payload) ?? event;
121
+ switch (eventName) {
122
+ case "agent.delta":
123
+ case "message.delta":
124
+ case "session.message.delta": {
125
+ const delta = stringValue(payload.delta) ?? stringValue(payload.text_delta) ?? stringValue(payload.text);
126
+ if (delta) {
127
+ streamedOutput += delta;
128
+ callbacks.onTextDelta(delta);
129
+ }
130
+ break;
131
+ }
132
+ case "session.message":
133
+ case "agent.message": {
134
+ const text = extractOpenClawOutputText(payload);
135
+ const delta = computeTextDelta(streamedOutput, text ?? "");
136
+ if (delta) {
137
+ streamedOutput += delta;
138
+ callbacks.onTextDelta(delta);
139
+ }
140
+ break;
141
+ }
142
+ case "session.tool":
143
+ case "tool.started":
144
+ case "tool.start": {
145
+ const toolName = stringValue(payload.toolName) ?? stringValue(payload.tool_name) ?? stringValue(payload.tool) ?? "tool";
146
+ const status = stringValue(payload.status);
147
+ const toolCallId = stringValue(payload.toolCallId) ?? stringValue(payload.tool_call_id);
148
+ if (status && /complete|finish|done/i.test(status)) {
149
+ const openTool = openTools.get(toolName)?.shift();
150
+ if (openTool)
151
+ callbacks.onToolEnd(openTool.id, Boolean(payload.error));
152
+ break;
153
+ }
154
+ toolCounter += 1;
155
+ const toolId = toolCallId ?? `${threadId}-${toolName}-${toolCounter}`;
156
+ const openTool = { id: toolId, name: toolName };
157
+ const tools = openTools.get(toolName) ?? [];
158
+ tools.push(openTool);
159
+ openTools.set(toolName, tools);
160
+ callbacks.onToolStart(toolName, toolId);
161
+ const preview = stringValue(payload.preview) ?? stringValue(payload.text);
162
+ if (preview)
163
+ callbacks.onToolUpdate(toolId, preview);
164
+ break;
165
+ }
166
+ case "tool.completed":
167
+ case "tool.finished": {
168
+ const toolName = stringValue(payload.toolName) ?? stringValue(payload.tool_name) ?? stringValue(payload.tool) ?? "tool";
169
+ const openTool = openTools.get(toolName)?.shift();
170
+ if (openTool)
171
+ callbacks.onToolEnd(openTool.id, Boolean(payload.error));
172
+ break;
173
+ }
174
+ case "agent.completed":
175
+ case "run.completed": {
176
+ const finalText = extractOpenClawOutputText(payload);
177
+ const delta = computeTextDelta(streamedOutput, finalText ?? "");
178
+ if (delta)
179
+ callbacks.onTextDelta(delta);
180
+ const usage = objectValue(payload.usage);
181
+ callbacks.onTurnComplete?.({
182
+ inputTokens: numberValue(usage?.inputTokens) ?? numberValue(usage?.input_tokens) ?? 0,
183
+ cachedInputTokens: numberValue(usage?.cachedInputTokens) ?? numberValue(usage?.cached_input_tokens) ?? 0,
184
+ outputTokens: numberValue(usage?.outputTokens) ?? numberValue(usage?.output_tokens) ?? 0,
185
+ });
186
+ didEnd = true;
187
+ callbacks.onAgentEnd();
188
+ break;
189
+ }
190
+ default:
191
+ break;
192
+ }
193
+ }, abortController.signal);
194
+ this.currentRunId = result.runId;
195
+ if (result.text && !streamedOutput.trim()) {
196
+ callbacks.onTextDelta(result.text);
197
+ }
198
+ this.cachedUsage = usageFromGatewayResult(result.usage ?? result.payload.usage);
199
+ if (!didEnd) {
200
+ callbacks.onAgentEnd();
201
+ }
202
+ this.refreshFromState({ force: true });
203
+ }
204
+ catch (error) {
205
+ if (isAbortError(error)) {
206
+ throw new Error("OpenClaw run was aborted");
207
+ }
208
+ throw error;
209
+ }
210
+ finally {
211
+ this.currentRunId = null;
212
+ this.currentGateway = null;
213
+ this.abortController = null;
214
+ this.processing = false;
215
+ gateway.close();
216
+ }
217
+ }
218
+ async abort() {
219
+ if (this.currentRunId && this.currentGateway) {
220
+ await this.currentGateway.cancelRun(this.currentRunId).catch(() => { });
221
+ }
222
+ this.abortController?.abort();
223
+ this.processing = false;
224
+ }
225
+ async newThread(workspace, model) {
226
+ this.ensureIdle("start a new OpenClaw session");
227
+ this.currentWorkspace = workspace ?? this.currentWorkspace;
228
+ if (model) {
229
+ this.currentModel = model;
230
+ }
231
+ this.currentThreadId = createOpenClawSessionId();
232
+ this.cachedUsage = undefined;
233
+ this.lastStateRefreshAt = Date.now();
234
+ return this.getInfo();
235
+ }
236
+ async resumeThread(threadId) {
237
+ return this.switchSession(threadId);
238
+ }
239
+ async switchSession(threadId) {
240
+ this.ensureIdle("switch OpenClaw session");
241
+ const record = this.getRecord(threadId);
242
+ if (!record) {
243
+ throw new Error(`Unknown OpenClaw session: ${threadId}`);
244
+ }
245
+ this.applyRecord(record);
246
+ this.lastStateRefreshAt = Date.now();
247
+ return this.getInfo();
248
+ }
249
+ listAllSessions(limit) {
250
+ return listOpenClawSessions(limit ?? 20, this.stateOptions());
251
+ }
252
+ listWorkspaces() {
253
+ const workspaces = new Set(listOpenClawWorkspaces(this.stateOptions()));
254
+ workspaces.add(this.currentWorkspace);
255
+ workspaces.add(this.config.workspace);
256
+ return [...workspaces].sort((left, right) => left.localeCompare(right));
257
+ }
258
+ async refreshModels(options = {}) {
259
+ const now = Date.now();
260
+ if (!options.force &&
261
+ this.cachedModels.length > 0 &&
262
+ now - this.modelsLoadedAt < OpenClawSessionService.MODEL_CACHE_TTL_MS) {
263
+ return;
264
+ }
265
+ const gatewayModels = await this.refreshModelsFromGateway().catch(() => []);
266
+ const cliModels = gatewayModels.length > 0 ? [] : this.refreshModelsFromCli();
267
+ this.cachedModels = gatewayModels.length > 0 ? gatewayModels : cliModels;
268
+ this.modelsLoadedAt = now;
269
+ }
270
+ listModels() {
271
+ const models = [...this.cachedModels];
272
+ if (this.currentModel && !models.some((model) => model.slug === this.currentModel)) {
273
+ models.unshift({ slug: this.currentModel, displayName: this.currentModel, supportsThinking: true, supportsImages: true });
274
+ }
275
+ if (models.length === 0) {
276
+ models.push({ slug: "openclaw/default", displayName: "openclaw/default", supportsThinking: true, supportsImages: true });
277
+ }
278
+ return models;
279
+ }
280
+ listLaunchProfiles() {
281
+ return listOpenClawLaunchProfiles();
282
+ }
283
+ getSessionRecord(threadId) {
284
+ return this.getRecord(threadId);
285
+ }
286
+ setModel(slug) {
287
+ this.currentModel = slug;
288
+ return slug;
289
+ }
290
+ setModelForCurrentSession(slug) {
291
+ this.ensureIdle("change OpenClaw model");
292
+ this.currentModel = slug;
293
+ return { value: slug, appliedToActiveThread: Boolean(this.currentThreadId) };
294
+ }
295
+ setReasoningEffort(effort) {
296
+ this.currentThinking = normalizeOpenClawThinking(effort);
297
+ }
298
+ setReasoningEffortForCurrentSession(effort) {
299
+ this.ensureIdle("change OpenClaw thinking");
300
+ const value = normalizeOpenClawThinking(effort);
301
+ if (!value) {
302
+ throw new Error("OpenClaw thinking level is empty");
303
+ }
304
+ this.currentThinking = value;
305
+ return { value, appliedToActiveThread: Boolean(this.currentThreadId) };
306
+ }
307
+ setLaunchProfile(profileId) {
308
+ this.ensureIdle("change OpenClaw profile");
309
+ this.currentLaunchProfile = findOpenClawLaunchProfile(profileId);
310
+ return openClawProfileAsLaunchProfile(this.currentLaunchProfile);
311
+ }
312
+ setFastMode() {
313
+ throw new Error("Fast mode is only supported by Codex sessions");
314
+ }
315
+ getSelectedLaunchProfile() {
316
+ return openClawProfileAsLaunchProfile(this.currentLaunchProfile);
317
+ }
318
+ syncFromAgentState() {
319
+ const before = this.getInfo();
320
+ this.refreshFromState({ force: true });
321
+ const after = this.getInfo();
322
+ const changedFields = [];
323
+ if (before.model !== after.model)
324
+ changedFields.push("model");
325
+ if (before.reasoningEffort !== after.reasoningEffort)
326
+ changedFields.push("thinking");
327
+ if (before.workspace !== after.workspace)
328
+ changedFields.push("workspace");
329
+ return {
330
+ threadId: this.currentThreadId,
331
+ changed: changedFields.length > 0,
332
+ reattached: false,
333
+ changedFields,
334
+ info: after,
335
+ };
336
+ }
337
+ handback() {
338
+ const threadId = this.currentThreadId;
339
+ const workspace = this.currentWorkspace;
340
+ this.currentThreadId = null;
341
+ return {
342
+ threadId,
343
+ workspace,
344
+ command: threadId
345
+ ? `cd ${shellQuote(workspace)} && ${shellQuote(this.cliPath ?? "openclaw")} agent --agent ${shellQuote(this.currentOpenClawAgentId)} --session-id ${shellQuote(threadId)} --message ${shellQuote("<your next message>")}`
346
+ : undefined,
347
+ label: "OpenClaw CLI",
348
+ };
349
+ }
350
+ dispose() {
351
+ this.abortController?.abort();
352
+ this.currentGateway?.close();
353
+ this.processing = false;
354
+ this.currentRunId = null;
355
+ }
356
+ async ensureSessionStarted() {
357
+ if (!this.currentThreadId) {
358
+ await this.newThread(this.currentWorkspace, this.currentModel);
359
+ }
360
+ }
361
+ ensureIdle(action) {
362
+ if (this.processing) {
363
+ throw new Error(`Cannot ${action} while a turn is in progress`);
364
+ }
365
+ }
366
+ createGatewayClient() {
367
+ return new OpenClawGatewayClient({
368
+ url: this.config.openClawGatewayUrl,
369
+ token: this.config.openClawGatewayToken,
370
+ password: this.config.openClawGatewayPassword,
371
+ timeoutMs: 15_000,
372
+ });
373
+ }
374
+ async refreshModelsFromGateway() {
375
+ const gateway = this.createGatewayClient();
376
+ try {
377
+ const response = await gateway.listModels({ agent: this.currentOpenClawAgentId });
378
+ return parseModelsPayload(response);
379
+ }
380
+ finally {
381
+ gateway.close();
382
+ }
383
+ }
384
+ refreshModelsFromCli() {
385
+ if (!this.cliPath) {
386
+ return [];
387
+ }
388
+ const result = spawnSync(this.cliPath, ["models", "list", "--json"], {
389
+ encoding: "utf8",
390
+ timeout: 5000,
391
+ windowsHide: true,
392
+ });
393
+ if (result.error || result.status !== 0) {
394
+ return [];
395
+ }
396
+ try {
397
+ return parseModelsPayload(JSON.parse(result.stdout.trim() || "{}"));
398
+ }
399
+ catch {
400
+ return [];
401
+ }
402
+ }
403
+ refreshFromState(options = {}) {
404
+ if (!this.currentThreadId) {
405
+ return;
406
+ }
407
+ const now = Date.now();
408
+ if (!options.force && now - this.lastStateRefreshAt < OpenClawSessionService.STATE_CACHE_TTL_MS) {
409
+ return;
410
+ }
411
+ this.lastStateRefreshAt = now;
412
+ const record = this.getRecord(this.currentThreadId);
413
+ if (record) {
414
+ this.applyRecord(record);
415
+ }
416
+ }
417
+ applyRecord(record) {
418
+ this.currentThreadId = record.id;
419
+ this.currentWorkspace = record.cwd || this.currentWorkspace;
420
+ this.currentModel = record.model ?? this.currentModel;
421
+ this.currentThinking = normalizeOpenClawThinking(record.reasoningEffort ?? this.currentThinking);
422
+ this.currentOpenClawAgentId = record.openClawAgentId ?? this.currentOpenClawAgentId;
423
+ this.cachedUsage = record.usage;
424
+ }
425
+ getRecord(threadId) {
426
+ return getOpenClawSession(threadId, this.stateOptions());
427
+ }
428
+ stateOptions() {
429
+ return {
430
+ cliPath: this.cliPath,
431
+ openClawHome: this.config.openClawHome,
432
+ stateDir: this.config.openClawStateDir,
433
+ workspace: this.currentWorkspace || this.config.workspace,
434
+ openClawAgentId: this.currentOpenClawAgentId,
435
+ };
436
+ }
437
+ async buildPrompt(input) {
438
+ if (typeof input === "string") {
439
+ return this.withInstructions(input);
440
+ }
441
+ const textParts = [input.stagedFileInstructions, input.text].filter((part) => Boolean(part?.trim()));
442
+ if ((input.imagePaths?.length ?? 0) > 0) {
443
+ textParts.push(`Attached images: ${(input.imagePaths ?? []).join(", ")}`);
444
+ }
445
+ return this.withInstructions(textParts.join("\n\n").trim() || "Please inspect the attached file(s).");
446
+ }
447
+ withInstructions(prompt) {
448
+ const parts = [
449
+ this.currentLaunchProfile.instructions,
450
+ this.currentThinking ? `Use OpenClaw thinking level "${this.currentThinking}" when the configured provider supports it.` : undefined,
451
+ prompt,
452
+ ].filter((part) => Boolean(part?.trim()));
453
+ return parts.join("\n\n");
454
+ }
455
+ async buildAttachments(input) {
456
+ if (typeof input === "string") {
457
+ return [];
458
+ }
459
+ const attachments = [];
460
+ for (const imagePath of input.imagePaths ?? []) {
461
+ attachments.push({
462
+ type: "image",
463
+ path: imagePath,
464
+ mimeType: mimeTypeForImage(imagePath),
465
+ data: (await readFile(imagePath)).toString("base64"),
466
+ });
467
+ }
468
+ return attachments;
469
+ }
470
+ }
471
+ function parseModelsPayload(payload) {
472
+ const object = objectValue(payload);
473
+ const rows = arrayValue(object?.models ?? object?.data ?? payload);
474
+ const models = [];
475
+ for (const row of rows) {
476
+ const item = objectValue(row);
477
+ const slug = stringValue(item?.id) ?? stringValue(item?.slug) ?? stringValue(item?.model) ?? stringValue(row);
478
+ if (!slug)
479
+ continue;
480
+ models.push({
481
+ slug,
482
+ displayName: stringValue(item?.name) ?? stringValue(item?.displayName) ?? slug,
483
+ contextWindow: numberValue(item?.contextWindow) ?? numberValue(item?.context_window) ?? undefined,
484
+ maxInputTokens: numberValue(item?.maxInputTokens) ?? numberValue(item?.max_input_tokens) ?? undefined,
485
+ maxOutputTokens: numberValue(item?.maxOutputTokens) ?? numberValue(item?.max_output_tokens) ?? undefined,
486
+ supportsThinking: booleanValue(item?.supportsThinking) ?? true,
487
+ supportsImages: booleanValue(item?.supportsImages) ?? true,
488
+ });
489
+ }
490
+ return models;
491
+ }
492
+ function normalizeOpenClawThinking(value) {
493
+ if (!value) {
494
+ return undefined;
495
+ }
496
+ if (OPENCLAW_THINKING_LEVELS.includes(value)) {
497
+ return value;
498
+ }
499
+ throw new Error(`Unsupported OpenClaw thinking level: ${value}`);
500
+ }
501
+ function createOpenClawSessionId() {
502
+ return `nordrelay-openclaw-${randomUUID().replace(/-/g, "").slice(0, 12)}`;
503
+ }
504
+ function usageFromGatewayResult(value) {
505
+ const usage = objectValue(value);
506
+ if (!usage) {
507
+ return undefined;
508
+ }
509
+ const input = numberValue(usage.input) ?? numberValue(usage.inputTokens) ?? numberValue(usage.input_tokens) ?? 0;
510
+ const output = numberValue(usage.output) ?? numberValue(usage.outputTokens) ?? numberValue(usage.output_tokens) ?? 0;
511
+ const cacheRead = numberValue(usage.cacheRead) ?? numberValue(usage.cache_read_tokens) ?? 0;
512
+ const cacheWrite = numberValue(usage.cacheWrite) ?? numberValue(usage.cache_write_tokens) ?? 0;
513
+ const total = input + output + cacheRead + cacheWrite;
514
+ return total > 0 ? { input, output, cacheRead, cacheWrite, total, cost: numberValue(usage.cost) ?? undefined } : undefined;
515
+ }
516
+ function computeTextDelta(previous, next) {
517
+ return next.startsWith(previous) ? next.slice(previous.length) : next;
518
+ }
519
+ function mimeTypeForImage(filePath) {
520
+ const extension = path.extname(filePath).toLowerCase();
521
+ if (extension === ".png")
522
+ return "image/png";
523
+ if (extension === ".webp")
524
+ return "image/webp";
525
+ if (extension === ".gif")
526
+ return "image/gif";
527
+ return "image/jpeg";
528
+ }
529
+ function isAbortError(error) {
530
+ return error instanceof Error && (error.name === "AbortError" || error.message.toLowerCase().includes("abort"));
531
+ }
532
+ function objectValue(value) {
533
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
534
+ }
535
+ function arrayValue(value) {
536
+ return Array.isArray(value) ? value : [];
537
+ }
538
+ function stringValue(value) {
539
+ return typeof value === "string" && value.trim() ? value : null;
540
+ }
541
+ function numberValue(value) {
542
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
543
+ }
544
+ function booleanValue(value) {
545
+ return typeof value === "boolean" ? value : null;
546
+ }
547
+ function shellQuote(value) {
548
+ return `'${value.replace(/'/g, `'\\''`)}'`;
549
+ }