agentel 0.2.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.
@@ -0,0 +1,795 @@
1
+ "use strict";
2
+
3
+ const { toIso } = require("../archive");
4
+
5
+ const PROVIDER = "gemini_cli";
6
+
7
+ function parseGeminiCliJsonl(text, options = {}) {
8
+ const events = [];
9
+ for (const line of String(text || "").split(/\r?\n/)) {
10
+ if (!line.trim()) continue;
11
+ try {
12
+ events.push(JSON.parse(line));
13
+ } catch {
14
+ // Ignore malformed partial lines in append-only history files.
15
+ }
16
+ }
17
+ return parseGeminiCliEvents(events, options);
18
+ }
19
+
20
+ function parseGeminiCliJson(data, options = {}) {
21
+ return parseGeminiCliEvents(geminiEventList(data), { ...options, root: data });
22
+ }
23
+
24
+ function parseGeminiCliEvents(events, options = {}) {
25
+ const root = options.root && typeof options.root === "object" ? options.root : {};
26
+ const normalizedEvents = coalesceGeminiIncrementalEvents(events);
27
+ const fallbackTime = toIso(options.fallbackTime) || new Date(0).toISOString();
28
+ const context = {
29
+ model: firstString(options.model, geminiModel(root)),
30
+ lastTimestamp: fallbackTime
31
+ };
32
+ let sessionId = firstString(options.sessionId, geminiSessionId(root));
33
+ let title = firstString(options.title, geminiTitle(root));
34
+ let cwd = firstString(options.cwd, geminiCwd(root));
35
+ const messages = [];
36
+
37
+ for (let index = 0; index < normalizedEvents.length; index++) {
38
+ const event = normalizedEvents[index];
39
+ if (!event || typeof event !== "object") continue;
40
+ context.model = firstString(geminiModel(event), context.model);
41
+ sessionId ||= geminiSessionId(event);
42
+ title ||= geminiTitle(event);
43
+ cwd ||= geminiCwd(event);
44
+ const timestamp = eventTimestamp(event, fallbackTime, index);
45
+ context.lastTimestamp = timestamp;
46
+ const usage = geminiUsage(event);
47
+
48
+ const checkpoint = geminiCheckpointMessage(event, timestamp);
49
+ if (checkpoint) {
50
+ messages.push(checkpoint);
51
+ continue;
52
+ }
53
+
54
+ const extracted = geminiMessagesFromEvent(event, { ...context, timestamp, usage });
55
+ if (extracted.length) {
56
+ messages.push(...extracted);
57
+ continue;
58
+ }
59
+
60
+ if (usage) applyUsageToLastAssistant(messages, usage);
61
+ }
62
+
63
+ const sorted = messages
64
+ .map((message, index) => ({ ...message, _ordinal: index }))
65
+ .sort((a, b) => {
66
+ const byTime = String(a.timestamp || "").localeCompare(String(b.timestamp || ""));
67
+ return byTime || a._ordinal - b._ordinal;
68
+ })
69
+ .map(({ _ordinal, ...message }) => message);
70
+
71
+ return {
72
+ sessionId,
73
+ title,
74
+ cwd,
75
+ model: context.model,
76
+ messages: sorted,
77
+ startedAt: sorted[0]?.timestamp || "",
78
+ endedAt: sorted[sorted.length - 1]?.timestamp || ""
79
+ };
80
+ }
81
+
82
+ function geminiEventList(data) {
83
+ if (Array.isArray(data)) return data;
84
+ if (!data || typeof data !== "object") return [];
85
+ const candidates = [
86
+ data.history,
87
+ data.messages,
88
+ data.turns,
89
+ data.events,
90
+ data.records,
91
+ data.contents,
92
+ data.conversation?.messages,
93
+ data.conversation?.turns,
94
+ data.session?.messages,
95
+ data.session?.history,
96
+ data.data?.messages,
97
+ data.data?.history
98
+ ];
99
+ const arrays = candidates.filter((item) => Array.isArray(item));
100
+ if (arrays.length) return arrays.flat();
101
+ return [data];
102
+ }
103
+
104
+ function coalesceGeminiIncrementalEvents(events) {
105
+ const output = [];
106
+ const indexes = new Map();
107
+ for (const event of events || []) {
108
+ if (!event || typeof event !== "object") {
109
+ output.push(event);
110
+ continue;
111
+ }
112
+ const id = geminiEventId(event);
113
+ if (!id) {
114
+ output.push(event);
115
+ continue;
116
+ }
117
+ const existingIndex = indexes.get(id);
118
+ if (existingIndex === undefined) {
119
+ indexes.set(id, output.length);
120
+ output.push(event);
121
+ continue;
122
+ }
123
+ output[existingIndex] = mergeGeminiEvent(output[existingIndex], event);
124
+ }
125
+ return output;
126
+ }
127
+
128
+ function mergeGeminiEvent(previous, next) {
129
+ const merged = { ...(previous && typeof previous === "object" ? previous : {}) };
130
+ for (const [key, value] of Object.entries(next || {})) {
131
+ if (value === undefined || value === null) continue;
132
+ if (typeof value === "string" && !value && merged[key]) continue;
133
+ if (Array.isArray(value)) {
134
+ if (value.length || !Array.isArray(merged[key])) merged[key] = value;
135
+ continue;
136
+ }
137
+ if (value && typeof value === "object" && merged[key] && typeof merged[key] === "object" && !Array.isArray(merged[key])) {
138
+ merged[key] = mergeGeminiEvent(merged[key], value);
139
+ continue;
140
+ }
141
+ merged[key] = value;
142
+ }
143
+ return merged;
144
+ }
145
+
146
+ function geminiMessagesFromEvent(event, context = {}) {
147
+ const messages = [];
148
+ const directCallNodes = directToolCallNodes(event);
149
+ const directCalls = directCallNodes.map((node) => normalizeGeminiToolCall(node, { event, context })).filter(Boolean);
150
+ if (directCalls.length) {
151
+ messages.push({
152
+ role: "assistant",
153
+ content: textFromGeminiParts(partsFromContent(event)),
154
+ timestamp: context.timestamp,
155
+ metadata: compactMetadata({
156
+ provider: PROVIDER,
157
+ eventType: eventType(event),
158
+ providerMessageId: geminiEventId(event),
159
+ model: context.model,
160
+ usage: context.usage,
161
+ thoughts: geminiThoughts(event),
162
+ toolCalls: directCalls
163
+ })
164
+ });
165
+ messages.push(...toolResultsFromToolCallNodes(directCallNodes, event, context));
166
+ }
167
+
168
+ const directResults = directToolResults(event, context);
169
+ if (directResults.length) messages.push(...directResults);
170
+ if (messages.length) return messages;
171
+
172
+ for (const node of contentNodesFromEvent(event)) {
173
+ const role = normalizeGeminiRole(firstString(node.role, node.author?.role, event.role, event.sender, event.type, event.kind));
174
+ const parts = partsFromContent(node);
175
+ const toolResults = toolResultsFromParts(parts, event, context);
176
+ if (role === "tool" || toolResults.length) {
177
+ messages.push(...toolResults);
178
+ continue;
179
+ }
180
+
181
+ const content = textFromGeminiParts(parts);
182
+ const toolCalls = toolCallsFromParts(parts, event, context);
183
+ if (!role && !content && !toolCalls.length) continue;
184
+ const messageRole = role || (toolCalls.length ? "assistant" : "");
185
+ if (!messageRole) continue;
186
+ messages.push({
187
+ role: messageRole,
188
+ content,
189
+ timestamp: timestampForNode(node, event, context.timestamp),
190
+ metadata: compactMetadata({
191
+ provider: PROVIDER,
192
+ eventType: eventType(event),
193
+ providerMessageId: geminiEventId(event),
194
+ model: messageRole === "assistant" ? context.model : undefined,
195
+ usage: messageRole === "assistant" ? context.usage : undefined,
196
+ thoughts: messageRole === "assistant" ? geminiThoughts(event) : undefined,
197
+ toolCalls: toolCalls.length ? toolCalls : undefined
198
+ })
199
+ });
200
+ }
201
+ return messages;
202
+ }
203
+
204
+ function contentNodesFromEvent(event) {
205
+ const nodes = [];
206
+ const add = (node) => {
207
+ if (node && typeof node === "object" && looksLikeGeminiContent(node)) nodes.push(node);
208
+ };
209
+ add(event);
210
+ add(event.message);
211
+ add(event.content);
212
+ add(event.item);
213
+ add(event.payload);
214
+ add(event.data);
215
+ add(event.response?.content);
216
+ if (Array.isArray(event.candidates)) {
217
+ for (const candidate of event.candidates) add(candidate.content || candidate.message || candidate);
218
+ }
219
+ if (Array.isArray(event.response?.candidates)) {
220
+ for (const candidate of event.response.candidates) add(candidate.content || candidate.message || candidate);
221
+ }
222
+ return uniqueObjects(nodes);
223
+ }
224
+
225
+ function looksLikeGeminiContent(node) {
226
+ if (!node || typeof node !== "object") return false;
227
+ if (Array.isArray(node.parts)) return true;
228
+ if (Array.isArray(node.content)) return true;
229
+ if (Array.isArray(node.content?.parts)) return true;
230
+ if (typeof node.text === "string" || typeof node.message === "string") return true;
231
+ if (typeof node.content === "string" && normalizeGeminiRole(firstString(node.role, node.type, node.kind))) return true;
232
+ if (node.functionCall || node.function_call || node.functionResponse || node.function_response) return true;
233
+ if (node.executableCode || node.executable_code || node.codeExecutionResult || node.code_execution_result) return true;
234
+ return Boolean(node.role && (node.content || node.parts || node.text));
235
+ }
236
+
237
+ function partsFromContent(node) {
238
+ if (!node || typeof node !== "object") return [];
239
+ const value = node.parts ?? node.content?.parts ?? node.message?.parts ?? node.content;
240
+ if (Array.isArray(value)) return value;
241
+ if (value && typeof value === "object") return [value];
242
+ if (typeof value === "string") return [{ text: value }];
243
+ if (typeof node.text === "string") return [{ text: node.text }];
244
+ if (typeof node.message === "string") return [{ text: node.message }];
245
+ return [node];
246
+ }
247
+
248
+ function directToolCalls(event, context) {
249
+ return directToolCallNodes(event).map((node) => normalizeGeminiToolCall(node, { event, context })).filter(Boolean);
250
+ }
251
+
252
+ function directToolCallNodes(event) {
253
+ const nodes = []
254
+ .concat(asArray(event.tool_calls))
255
+ .concat(asArray(event.toolCalls))
256
+ .concat(asArray(event.functionCalls))
257
+ .concat(asArray(event.function_calls));
258
+ if (event.functionCall || event.function_call) nodes.push(event.functionCall || event.function_call);
259
+ if (event.toolCall || event.tool_call) nodes.push(event.toolCall || event.tool_call);
260
+ if (event.type && /function[_-]?call|tool[_-]?call|tool[_-]?use/i.test(event.type)) nodes.push(event);
261
+ return nodes.filter((node) => node && typeof node === "object");
262
+ }
263
+
264
+ function directToolResults(event, context) {
265
+ const nodes = []
266
+ .concat(asArray(event.tool_results))
267
+ .concat(asArray(event.toolResults))
268
+ .concat(asArray(event.functionResponses))
269
+ .concat(asArray(event.function_responses));
270
+ if (event.functionResponse || event.function_response) nodes.push(event.functionResponse || event.function_response);
271
+ if (event.toolResult || event.tool_result) nodes.push(event.toolResult || event.tool_result);
272
+ if (event.type && /function[_-]?response|function[_-]?call[_-]?output|tool[_-]?(result|output)/i.test(event.type)) nodes.push(event);
273
+ return nodes.map((node) => toolResultMessage(normalizeGeminiToolResult(node, { event, context }), context.timestamp, event)).filter(Boolean);
274
+ }
275
+
276
+ function toolResultsFromToolCallNodes(nodes, event, context) {
277
+ const messages = [];
278
+ for (const node of nodes) {
279
+ const resultNodes = []
280
+ .concat(asArray(node.result))
281
+ .concat(asArray(node.results))
282
+ .concat(asArray(node.toolResult))
283
+ .concat(asArray(node.tool_result))
284
+ .concat(asArray(node.functionResponse))
285
+ .concat(asArray(node.function_response));
286
+ if (!resultNodes.length && firstString(node.resultDisplay, node.output)) {
287
+ resultNodes.push({ response: { output: firstString(node.resultDisplay, node.output) } });
288
+ }
289
+ for (const resultNode of resultNodes) {
290
+ const unwrapped = unwrapGeminiToolResult(resultNode);
291
+ const normalized = normalizeGeminiToolResult(
292
+ {
293
+ ...unwrapped,
294
+ id: firstString(unwrapped.id, unwrapped.callId, unwrapped.call_id, node.id, node.callId, node.call_id),
295
+ name: firstString(unwrapped.name, unwrapped.toolName, unwrapped.tool_name, node.name, node.toolName, node.tool_name),
296
+ status: firstString(unwrapped.status, node.status),
297
+ response: unwrapped.response ?? unwrapped.output ?? unwrapped.result ?? unwrapped.content ?? unwrapped.text
298
+ },
299
+ { event, context, part: node }
300
+ );
301
+ const timestamp = toIso(unwrapped.timestamp || node.timestamp) || context.timestamp;
302
+ const message = toolResultMessage(normalized, timestamp, event);
303
+ if (message) messages.push(message);
304
+ }
305
+ }
306
+ return messages;
307
+ }
308
+
309
+ function unwrapGeminiToolResult(value) {
310
+ if (!value || typeof value !== "object") return { response: { output: value } };
311
+ return value.functionResponse || value.function_response || value.toolResult || value.tool_result || value;
312
+ }
313
+
314
+ function toolCallsFromParts(parts, event, context) {
315
+ const calls = [];
316
+ for (const part of parts) {
317
+ const call =
318
+ part?.functionCall ||
319
+ part?.function_call ||
320
+ part?.toolCall ||
321
+ part?.tool_call ||
322
+ (part?.executableCode || part?.executable_code ? executableCodeCall(part) : null);
323
+ const normalized = normalizeGeminiToolCall(call, { event, context, part });
324
+ if (normalized) calls.push(normalized);
325
+ }
326
+ return calls;
327
+ }
328
+
329
+ function toolResultsFromParts(parts, event, context) {
330
+ const results = [];
331
+ for (const part of parts) {
332
+ const result =
333
+ part?.functionResponse ||
334
+ part?.function_response ||
335
+ part?.toolResult ||
336
+ part?.tool_result ||
337
+ (part?.codeExecutionResult || part?.code_execution_result ? codeExecutionResult(part) : null);
338
+ const normalized = normalizeGeminiToolResult(result, { event, context, part });
339
+ if (normalized) results.push(toolResultMessage(normalized, context.timestamp, event));
340
+ }
341
+ return results;
342
+ }
343
+
344
+ function normalizeGeminiToolCall(node, { event, part } = {}) {
345
+ if (!node || typeof node !== "object") return null;
346
+ const name = firstString(
347
+ node.name,
348
+ node.toolName,
349
+ node.tool_name,
350
+ node.function?.name,
351
+ event?.toolName,
352
+ event?.tool_name,
353
+ node.language === "bash" ? "run_shell_command" : "",
354
+ node.code ? "code_execution" : ""
355
+ );
356
+ if (!name) return null;
357
+ const args = parseToolArgsValue(
358
+ node.args ??
359
+ node.arguments ??
360
+ node.input ??
361
+ node.parameters ??
362
+ node.function?.arguments ??
363
+ (node.code ? { language: node.language, code: node.code } : undefined)
364
+ );
365
+ const summary = summarizeToolArguments(args);
366
+ return {
367
+ id: firstString(node.id, node.callId, node.call_id, event?.callId, event?.call_id, part?.id) || undefined,
368
+ name,
369
+ displayName: firstString(node.displayName, node.display_name, toolDisplayName(name)),
370
+ rawCategory: firstString(eventType(event), node.type, node.kind, "function_call"),
371
+ category: toolCategory(name, eventType(event)),
372
+ title: firstString(node.title, node.label, node.description, node.displayName, node.display_name, toolDisplayName(name)),
373
+ status: firstString(node.status, event?.status, "tool_call"),
374
+ argument: summary,
375
+ rawInputSummary: summary,
376
+ inputPreview: summary,
377
+ target: toolTarget(args) || undefined,
378
+ arguments: args && typeof args === "object" && !Array.isArray(args) ? args : undefined,
379
+ provider: PROVIDER
380
+ };
381
+ }
382
+
383
+ function normalizeGeminiToolResult(node, { event, part } = {}) {
384
+ if (!node || typeof node !== "object") return null;
385
+ const name = firstString(
386
+ node.name,
387
+ node.toolName,
388
+ node.tool_name,
389
+ node.function?.name,
390
+ event?.toolName,
391
+ event?.tool_name,
392
+ event?.name,
393
+ node.outcome ? "code_execution" : "",
394
+ "Tool output"
395
+ );
396
+ const output = toolOutputText(
397
+ node.response ??
398
+ node.output ??
399
+ node.result ??
400
+ node.content ??
401
+ node.text ??
402
+ node.stdout ??
403
+ node.stderr ??
404
+ node.error ??
405
+ (node.outcome ? { outcome: node.outcome, output: node.output } : undefined)
406
+ );
407
+ if (!output) return null;
408
+ const category = toolCategory(name, eventType(event));
409
+ const lineCount = output.split(/\r?\n/).length;
410
+ return {
411
+ provider: PROVIDER,
412
+ id: firstString(node.id, node.callId, node.call_id, event?.callId, event?.call_id, part?.id) || undefined,
413
+ kind: toolDisplayName(name),
414
+ title: node.error || node.stderr || /error|fail/i.test(String(node.outcome || "")) ? "Tool error" : "Tool result",
415
+ rawCategory: firstString(eventType(event), node.type, node.kind, "function_response"),
416
+ category,
417
+ categoryLabel: toolDisplayName(category),
418
+ summary: firstLine(output),
419
+ output,
420
+ lineCount,
421
+ collapsed: lineCount > 18,
422
+ status: firstString(node.status, event?.status, node.outcome, "completed"),
423
+ exitCode: numberValue(
424
+ node.exit_code ?? node.exitCode ?? node.response?.exit_code ?? node.response?.exitCode ?? node.output?.exit_code ?? node.output?.exitCode
425
+ )
426
+ };
427
+ }
428
+
429
+ function toolResultMessage(toolResult, timestamp, event) {
430
+ if (!toolResult) return null;
431
+ return {
432
+ role: "tool",
433
+ content: toolResult.output,
434
+ timestamp,
435
+ metadata: compactMetadata({
436
+ provider: PROVIDER,
437
+ eventType: toolResult.rawCategory,
438
+ providerMessageId: geminiEventId(event),
439
+ toolResult
440
+ })
441
+ };
442
+ }
443
+
444
+ function executableCodeCall(part) {
445
+ const value = part.executableCode || part.executable_code;
446
+ if (!value || typeof value !== "object") return null;
447
+ return {
448
+ name: value.language === "bash" || value.language === "shell" ? "run_shell_command" : "code_execution",
449
+ args: { language: value.language, code: value.code }
450
+ };
451
+ }
452
+
453
+ function codeExecutionResult(part) {
454
+ const value = part.codeExecutionResult || part.code_execution_result;
455
+ if (!value || typeof value !== "object") return null;
456
+ return {
457
+ name: "code_execution",
458
+ response: {
459
+ outcome: value.outcome,
460
+ output: value.output
461
+ }
462
+ };
463
+ }
464
+
465
+ function textFromGeminiParts(parts) {
466
+ return (parts || [])
467
+ .map((part) => {
468
+ if (typeof part === "string") return part;
469
+ if (!part || typeof part !== "object") return "";
470
+ if (typeof part.text === "string") return part.text;
471
+ if (typeof part.content === "string") return part.content;
472
+ if (part.inlineData || part.inline_data || part.fileData || part.file_data) return attachmentLabel(part);
473
+ return "";
474
+ })
475
+ .filter(Boolean)
476
+ .join("\n")
477
+ .trim();
478
+ }
479
+
480
+ function attachmentLabel(part) {
481
+ const data = part.inlineData || part.inline_data || part.fileData || part.file_data || {};
482
+ const mime = firstString(data.mimeType, data.mime_type);
483
+ const uri = firstString(data.fileUri, data.file_uri, data.uri);
484
+ if (uri && mime) return `[Attachment: ${mime} ${uri}]`;
485
+ if (uri) return `[Attachment: ${uri}]`;
486
+ if (mime) return `[Attachment: ${mime}]`;
487
+ return "[Attachment]";
488
+ }
489
+
490
+ function geminiCheckpointMessage(event, timestamp) {
491
+ const type = eventType(event);
492
+ if (!/(checkpoint|snapshot|restore)/i.test(type) && !event.checkpoint && !event.checkpointId && !event.checkpoint_id) return null;
493
+ const checkpoint = event.checkpoint && typeof event.checkpoint === "object" ? event.checkpoint : event;
494
+ const checkpointId = firstString(checkpoint.id, checkpoint.checkpointId, checkpoint.checkpoint_id, event.checkpointId, event.checkpoint_id);
495
+ const files = asArray(checkpoint.files || checkpoint.changedFiles || checkpoint.changed_files).filter(Boolean);
496
+ const label = /restore/i.test(type) ? "Checkpoint restored" : "Checkpoint saved";
497
+ return {
498
+ role: "system",
499
+ content: files.length ? `${label}: ${files.join(", ")}` : label,
500
+ timestamp,
501
+ metadata: compactMetadata({
502
+ provider: PROVIDER,
503
+ eventType: type || "checkpoint",
504
+ providerGenerated: true,
505
+ contextKind: "checkpoint",
506
+ contextSource: "gemini-checkpoint-event",
507
+ checkpointId,
508
+ files: files.length ? files : undefined,
509
+ gitSha: firstString(checkpoint.gitSha, checkpoint.git_sha, checkpoint.sha, checkpoint.commit)
510
+ })
511
+ };
512
+ }
513
+
514
+ function geminiUsage(value) {
515
+ if (!value || typeof value !== "object") return null;
516
+ const usage =
517
+ value.usageMetadata ||
518
+ value.usage_metadata ||
519
+ value.usage ||
520
+ value.tokens ||
521
+ value.tokenUsage ||
522
+ value.token_usage ||
523
+ value.response?.usageMetadata ||
524
+ value.response?.usage_metadata ||
525
+ value.response?.tokens;
526
+ if (!usage || typeof usage !== "object") return null;
527
+ const inputTokens = numberValue(
528
+ usage.promptTokenCount ?? usage.prompt_token_count ?? usage.inputTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? usage.input
529
+ );
530
+ const outputTokens = numberValue(
531
+ usage.candidatesTokenCount ??
532
+ usage.candidates_token_count ??
533
+ usage.outputTokens ??
534
+ usage.output_tokens ??
535
+ usage.completion_tokens ??
536
+ usage.output
537
+ );
538
+ const totalTokens = numberValue(usage.totalTokenCount ?? usage.total_token_count ?? usage.totalTokens ?? usage.total_tokens ?? usage.total);
539
+ const thoughtsTokens = numberValue(usage.thoughtsTokenCount ?? usage.thoughts_token_count ?? usage.thoughts);
540
+ const cacheReadTokens = numberValue(usage.cachedContentTokenCount ?? usage.cached_content_token_count ?? usage.cacheReadTokens ?? usage.cached);
541
+ const toolUsePromptTokens = numberValue(usage.toolUsePromptTokenCount ?? usage.tool_use_prompt_token_count ?? usage.tool);
542
+ const normalized = compactMetadata({
543
+ inputTokens,
544
+ outputTokens,
545
+ totalTokens,
546
+ thoughtsTokens,
547
+ cacheReadTokens,
548
+ toolUsePromptTokens
549
+ });
550
+ return Object.keys(normalized).length ? normalized : null;
551
+ }
552
+
553
+ function geminiThoughts(value) {
554
+ const thoughts = asArray(value?.thoughts || value?.reasoning || value?.thinking)
555
+ .map((thought) => {
556
+ if (!thought || typeof thought !== "object") return null;
557
+ return compactMetadata({
558
+ subject: firstString(thought.subject, thought.title, thought.name),
559
+ description: firstString(thought.description, thought.summary, thought.text, thought.content),
560
+ timestamp: toIso(thought.timestamp || thought.createdAt || thought.created_at)
561
+ });
562
+ })
563
+ .filter((thought) => thought && Object.keys(thought).length);
564
+ return thoughts.length ? thoughts : undefined;
565
+ }
566
+
567
+ function applyUsageToLastAssistant(messages, usage) {
568
+ if (!usage) return;
569
+ const target = [...messages].reverse().find((message) => message.role === "assistant");
570
+ if (!target) return;
571
+ target.metadata = { ...(target.metadata || {}), usage };
572
+ }
573
+
574
+ function geminiSessionId(value) {
575
+ return firstString(
576
+ value?.sessionId,
577
+ value?.session_id,
578
+ value?.conversationId,
579
+ value?.conversation_id,
580
+ value?.chatId,
581
+ value?.chat_id,
582
+ value?.id,
583
+ value?.session?.id,
584
+ value?.metadata?.sessionId,
585
+ value?.metadata?.session_id
586
+ );
587
+ }
588
+
589
+ function geminiTitle(value) {
590
+ return firstString(value?.title, value?.name, value?.summary, value?.metadata?.title, value?.session?.title);
591
+ }
592
+
593
+ function geminiCwd(value) {
594
+ return firstString(
595
+ value?.cwd,
596
+ value?.workingDirectory,
597
+ value?.working_directory,
598
+ value?.workspace,
599
+ value?.workspaceFolder,
600
+ value?.projectRoot,
601
+ value?.project_root,
602
+ value?.context?.cwd,
603
+ value?.metadata?.cwd,
604
+ value?.session?.cwd
605
+ );
606
+ }
607
+
608
+ function geminiModel(value) {
609
+ return firstString(
610
+ value?.model,
611
+ value?.modelName,
612
+ value?.model_name,
613
+ value?.modelVersion,
614
+ value?.model_version,
615
+ value?.metadata?.model,
616
+ value?.response?.model
617
+ );
618
+ }
619
+
620
+ function eventTimestamp(event, fallbackTime, index) {
621
+ const explicit = toIso(
622
+ event.timestamp ||
623
+ event.createdAt ||
624
+ event.created_at ||
625
+ event.time ||
626
+ event.startTime ||
627
+ event.start_time ||
628
+ event.metadata?.timestamp
629
+ );
630
+ if (explicit) return explicit;
631
+ return new Date(new Date(fallbackTime).getTime() + index).toISOString();
632
+ }
633
+
634
+ function timestampForNode(node, event, fallback) {
635
+ return toIso(node.timestamp || node.createdAt || node.created_at || event.timestamp || event.createdAt || event.created_at) || fallback;
636
+ }
637
+
638
+ function eventType(event) {
639
+ return firstString(event?.type, event?.kind, event?.eventType, event?.event_type, event?.name);
640
+ }
641
+
642
+ function geminiEventId(event) {
643
+ return firstString(event?.id, event?.messageId, event?.message_id, event?.eventId, event?.event_id);
644
+ }
645
+
646
+ function normalizeGeminiRole(role) {
647
+ const value = String(role || "").toLowerCase();
648
+ if (value === "model" || value === "assistant" || value === "gemini") return "assistant";
649
+ if (value === "human" || value === "user") return "user";
650
+ if (value === "function" || value === "tool" || value === "functionresponse") return "tool";
651
+ if (value === "system") return "system";
652
+ if (value.includes("model")) return "assistant";
653
+ if (value.includes("assistant")) return "assistant";
654
+ if (value.includes("user")) return "user";
655
+ if (value.includes("tool") || value.includes("function_response")) return "tool";
656
+ return "";
657
+ }
658
+
659
+ function toolCategory(name, type = "") {
660
+ const text = `${name || ""} ${type || ""}`;
661
+ if (/shell|bash|command|terminal|exec|npm|yarn|pnpm/i.test(text)) return "shell";
662
+ if (/web|fetch|browser|url|http/i.test(text)) return "web";
663
+ if (/edit|write|patch|replace|insert|update/i.test(text)) return "edit";
664
+ if (/read|open|view|cat|ls|list/i.test(text)) return "read";
665
+ if (/grep|glob|find|search|rg/i.test(text)) return "search";
666
+ if (/mcp/i.test(text)) return "mcp";
667
+ return /function/i.test(type) ? "function" : "tool";
668
+ }
669
+
670
+ function parseToolArgsValue(value) {
671
+ if (value == null) return null;
672
+ if (typeof value === "string") {
673
+ try {
674
+ return JSON.parse(value);
675
+ } catch {
676
+ return value;
677
+ }
678
+ }
679
+ return value;
680
+ }
681
+
682
+ function summarizeToolArguments(value) {
683
+ if (value == null) return "";
684
+ if (typeof value === "string") return value.slice(0, 240);
685
+ if (typeof value !== "object") return String(value).slice(0, 240);
686
+ for (const key of ["query", "pattern", "command", "cmd", "code", "path", "file", "file_path", "dir_path"]) {
687
+ if (value[key]) return String(value[key]).slice(0, 240);
688
+ }
689
+ return Object.entries(value)
690
+ .slice(0, 3)
691
+ .map(([key, item]) => `${key}: ${typeof item === "string" ? item : JSON.stringify(item)}`)
692
+ .join(", ")
693
+ .slice(0, 240);
694
+ }
695
+
696
+ function toolOutputText(value) {
697
+ if (value == null) return "";
698
+ if (typeof value === "string") {
699
+ const trimmed = value.trim();
700
+ if (!trimmed) return "";
701
+ try {
702
+ const parsed = JSON.parse(trimmed);
703
+ const nested = toolOutputText(parsed.output ?? parsed.content ?? parsed.result ?? parsed.text ?? parsed.stdout ?? parsed.stderr);
704
+ if (nested) return nested;
705
+ } catch {
706
+ // Keep raw text.
707
+ }
708
+ return trimmed.replace(/^Output:\s*\n?/i, "").trim();
709
+ }
710
+ if (Array.isArray(value)) return value.map(toolOutputText).filter(Boolean).join("\n");
711
+ if (typeof value === "object") {
712
+ if (value.functionResponse || value.function_response) return toolOutputText(value.functionResponse || value.function_response);
713
+ if (value.toolResult || value.tool_result) return toolOutputText(value.toolResult || value.tool_result);
714
+ const preferred = value.output ?? value.stdout ?? value.stderr ?? value.error ?? value.result ?? value.content ?? value.text ?? value.message;
715
+ if (preferred != null) return toolOutputText(preferred);
716
+ return JSON.stringify(value);
717
+ }
718
+ return String(value);
719
+ }
720
+
721
+ function toolTarget(args) {
722
+ if (!args || typeof args !== "object") return "";
723
+ return firstString(
724
+ args.path,
725
+ args.file,
726
+ args.filename,
727
+ args.file_path,
728
+ args.filePath,
729
+ args.dir_path,
730
+ args.dirPath,
731
+ args.directory,
732
+ args.folder,
733
+ args.cwd,
734
+ args.root,
735
+ args.url
736
+ );
737
+ }
738
+
739
+ function firstLine(value) {
740
+ return String(value || "").split(/\r?\n/).find((line) => line.trim())?.trim() || "";
741
+ }
742
+
743
+ function toolDisplayName(value) {
744
+ const text = String(value || "tool").trim();
745
+ return text
746
+ .split(/_+|(?=[A-Z])/g)
747
+ .filter(Boolean)
748
+ .map((part) => part.replace(/(^|[-\s])([a-z])/g, (_, prefix, char) => `${prefix}${char.toUpperCase()}`))
749
+ .join(" ");
750
+ }
751
+
752
+ function firstString(...values) {
753
+ for (const value of values) {
754
+ if (typeof value === "string" && value.trim()) return value.trim();
755
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
756
+ }
757
+ return "";
758
+ }
759
+
760
+ function numberValue(value) {
761
+ const number = Number(value);
762
+ return Number.isFinite(number) ? number : undefined;
763
+ }
764
+
765
+ function asArray(value) {
766
+ if (Array.isArray(value)) return value;
767
+ return value == null ? [] : [value];
768
+ }
769
+
770
+ function compactMetadata(value) {
771
+ const output = {};
772
+ for (const [key, item] of Object.entries(value || {})) {
773
+ if (item === undefined || item === null || item === "") continue;
774
+ if (Array.isArray(item) && !item.length) continue;
775
+ output[key] = item;
776
+ }
777
+ return output;
778
+ }
779
+
780
+ function uniqueObjects(values) {
781
+ const seen = new Set();
782
+ const result = [];
783
+ for (const value of values) {
784
+ if (seen.has(value)) continue;
785
+ seen.add(value);
786
+ result.push(value);
787
+ }
788
+ return result;
789
+ }
790
+
791
+ module.exports = {
792
+ parseGeminiCliEvents,
793
+ parseGeminiCliJson,
794
+ parseGeminiCliJsonl
795
+ };