agentel 0.2.6 → 0.3.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,367 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+
5
+ const PROVIDER = "grok";
6
+
7
+ function parseGrokUpdatesFile(file, options = {}) {
8
+ let text;
9
+ try {
10
+ text = fs.readFileSync(file, "utf8");
11
+ } catch {
12
+ return null;
13
+ }
14
+ return parseGrokUpdatesText(text, options);
15
+ }
16
+
17
+ // Grok Build session dirs hold updates.jsonl: ACP (Agent Client Protocol)
18
+ // JSON-RPC session/update notifications. Message content streams in as
19
+ // chunked entries (user_message_chunk / agent_message_chunk /
20
+ // agent_thought_chunk) that must be concatenated; the only token telemetry is
21
+ // a cumulative _meta.totalTokens counter that can rewind during streaming, so
22
+ // the session total is the maximum observed value.
23
+ function parseGrokUpdatesText(text, options = {}) {
24
+ const lines = String(text || "").split(/\r?\n/);
25
+ const fallbackTime = toIso(options.fallbackTime) || new Date().toISOString();
26
+ const messages = [];
27
+ const models = new Set();
28
+ let sessionId = "";
29
+ let maxTotalTokens = 0;
30
+ let currentModel = "";
31
+ let pending = null;
32
+ let index = 0;
33
+
34
+ const flush = () => {
35
+ if (!pending) return;
36
+ const content = pending.texts.join("").trim();
37
+ const thinking = pending.thinking.join("").trim();
38
+ if (!content && !thinking && !pending.toolCalls.length) {
39
+ pending = null;
40
+ return;
41
+ }
42
+ messages.push({
43
+ role: pending.role,
44
+ content,
45
+ timestamp: pending.timestamp,
46
+ metadata: compactObject({
47
+ provider: PROVIDER,
48
+ model: pending.role === "assistant" ? pending.model || undefined : undefined,
49
+ thinking: thinking || undefined,
50
+ toolCalls: pending.toolCalls.length ? pending.toolCalls : undefined
51
+ })
52
+ });
53
+ pending = null;
54
+ };
55
+
56
+ for (const line of lines) {
57
+ if (!line.trim()) continue;
58
+ const record = parseJson(line);
59
+ if (!record || typeof record !== "object") continue;
60
+ const params = record.params && typeof record.params === "object" ? record.params : record;
61
+ const update = params.update && typeof params.update === "object" ? params.update : null;
62
+ if (!update) continue;
63
+ const meta = { ...(params._meta && typeof params._meta === "object" ? params._meta : {}), ...(update._meta && typeof update._meta === "object" ? update._meta : {}) };
64
+ sessionId ||= firstString(params.sessionId, params.session_id);
65
+ const timestamp =
66
+ toIso(meta.agentTimestampMs) || toIso(params.timestamp) || toIso(record.timestamp) || toIso(record.ts) || offsetTimestamp(fallbackTime, index);
67
+ index++;
68
+ const totalTokens = Number(meta.totalTokens);
69
+ if (Number.isFinite(totalTokens) && totalTokens > maxTotalTokens) maxTotalTokens = totalTokens;
70
+ const model = firstString(meta.modelId, params.modelId, params.model_id, params.model);
71
+ if (model) {
72
+ models.add(model);
73
+ currentModel = model;
74
+ }
75
+ const kind = String(update.sessionUpdate || update.session_update || "");
76
+ if (kind === "user_message_chunk") {
77
+ if (!pending || pending.role !== "user") {
78
+ flush();
79
+ pending = { role: "user", texts: [], thinking: [], toolCalls: [], timestamp, model: "" };
80
+ }
81
+ pending.texts.push(grokContentText(update));
82
+ continue;
83
+ }
84
+ if (kind === "agent_message_chunk" || kind === "agent_thought_chunk") {
85
+ if (!pending || pending.role !== "assistant") {
86
+ flush();
87
+ pending = { role: "assistant", texts: [], thinking: [], toolCalls: [], timestamp, model: currentModel };
88
+ }
89
+ pending.model = currentModel || pending.model;
90
+ (kind === "agent_thought_chunk" ? pending.thinking : pending.texts).push(grokContentText(update));
91
+ continue;
92
+ }
93
+ if (kind === "tool_call") {
94
+ if (!pending || pending.role !== "assistant") {
95
+ flush();
96
+ pending = { role: "assistant", texts: [], thinking: [], toolCalls: [], timestamp, model: currentModel };
97
+ }
98
+ const call = grokToolCall(update);
99
+ if (call) pending.toolCalls.push(call);
100
+ continue;
101
+ }
102
+ if (kind === "tool_call_update") {
103
+ const status = firstString(update.status);
104
+ const output = grokToolOutput(update);
105
+ if ((status === "completed" || status === "failed") || output) {
106
+ flush();
107
+ const toolResult = grokToolResult(update, output);
108
+ if (toolResult) {
109
+ messages.push({ role: "tool", content: toolResult.output, timestamp, metadata: { provider: PROVIDER, toolResult } });
110
+ }
111
+ }
112
+ continue;
113
+ }
114
+ if (kind === "plan") {
115
+ flush();
116
+ const planText = grokPlanText(update);
117
+ if (planText) {
118
+ messages.push({
119
+ role: "system",
120
+ content: planText,
121
+ timestamp,
122
+ metadata: { provider: PROVIDER, providerGenerated: true, contextKind: "plan", eventType: "grok-plan-update" }
123
+ });
124
+ }
125
+ continue;
126
+ }
127
+ if (kind === "current_mode_update") {
128
+ flush();
129
+ const mode = firstString(update.currentModeId, update.current_mode_id, update.modeId, update.mode);
130
+ if (mode) {
131
+ messages.push({
132
+ role: "system",
133
+ content: `Mode changed to ${mode}`,
134
+ timestamp,
135
+ metadata: { provider: PROVIDER, providerGenerated: true, contextKind: "mode_change", eventType: "grok-mode-update" }
136
+ });
137
+ }
138
+ continue;
139
+ }
140
+ // available_commands_update, hook_execution, subagent_spawned/finished
141
+ // (linkage comes from subagents/<id>/meta.json instead), and unknown
142
+ // kinds: ignore (product is young and the vocabulary is still growing).
143
+ }
144
+ flush();
145
+ if (!sessionId && !messages.length) return null;
146
+ return {
147
+ sessionId,
148
+ messages,
149
+ models: [...models],
150
+ maxTotalTokens,
151
+ startedAt: messages[0]?.timestamp || fallbackTime,
152
+ endedAt: messages[messages.length - 1]?.timestamp || fallbackTime
153
+ };
154
+ }
155
+
156
+ // summary.json + signals.json sidecars carry the per-session rollup.
157
+ function grokSessionSummary({ summary, signals, models, maxTotalTokens }) {
158
+ const totalTokens = positiveNumber(maxTotalTokens);
159
+ const signalModels = Array.isArray(signals?.modelsUsed) ? signals.modelsUsed.filter(Boolean) : [];
160
+ const allModels = [...new Set([...(models || []), ...signalModels, firstString(summary?.current_model_id, summary?.model_id, signals?.primaryModelId)].filter(Boolean))];
161
+ return compactObject({
162
+ usage: totalTokens ? { totalTokens, authoritativeTotalTokens: true, source: "grok-updates-total-tokens" } : undefined,
163
+ modelUsage: allModels.length ? allModels.map((model) => ({ model, source: "grok-session" })) : undefined,
164
+ grokBuild: compactObject({
165
+ turnCount: positiveNumber(signals?.turnCount),
166
+ contextTokensUsed: positiveNumber(signals?.contextTokensUsed),
167
+ contextWindowTokens: positiveNumber(signals?.contextWindowTokens),
168
+ totalTokensBeforeCompaction: positiveNumber(signals?.totalTokensBeforeCompaction),
169
+ sessionDurationSeconds: positiveNumber(signals?.sessionDurationSeconds),
170
+ primaryModelId: firstString(signals?.primaryModelId) || undefined,
171
+ createdAt: toIso(summary?.created_at) || undefined,
172
+ updatedAt: toIso(summary?.updated_at) || undefined
173
+ })
174
+ });
175
+ }
176
+
177
+ // Session dirs are grouped under percent-encoded absolute cwd paths,
178
+ // e.g. ~/.grok/sessions/%2FUsers%2Fme%2Fproject/<session-id>/.
179
+ function decodeGrokCwd(name) {
180
+ try {
181
+ const decoded = decodeURIComponent(String(name || ""));
182
+ return decoded.startsWith("/") || /^[A-Za-z]:[\\/]/.test(decoded) ? decoded : "";
183
+ } catch {
184
+ return "";
185
+ }
186
+ }
187
+
188
+ function grokContentText(update) {
189
+ const content = update.content;
190
+ if (typeof content === "string") return content;
191
+ if (Array.isArray(content)) return content.map(grokBlockText).filter(Boolean).join("");
192
+ if (content && typeof content === "object") return grokBlockText(content);
193
+ return firstString(update.text, update.chunk);
194
+ }
195
+
196
+ function grokBlockText(block) {
197
+ if (typeof block === "string") return block;
198
+ if (!block || typeof block !== "object") return "";
199
+ if (typeof block.text === "string") return block.text;
200
+ if (block.content) return grokContentText({ content: block.content });
201
+ return "";
202
+ }
203
+
204
+ function grokToolCall(update) {
205
+ const id = firstString(update.toolCallId, update.tool_call_id);
206
+ const title = firstString(update.title, update.name);
207
+ if (!id && !title) return null;
208
+ const args = update.rawInput && typeof update.rawInput === "object" ? update.rawInput : undefined;
209
+ const summary = firstString(title, summarizeToolArguments(args));
210
+ return compactObject({
211
+ id: id || undefined,
212
+ name: firstString(update.name, update.kind, title, "tool"),
213
+ displayName: title || toolDisplayName(firstString(update.kind, "tool")),
214
+ rawCategory: "tool_call",
215
+ category: grokToolCategory(firstString(update.kind, title)),
216
+ title: title || undefined,
217
+ status: firstString(update.status, "tool_call"),
218
+ argument: summary,
219
+ rawInputSummary: summary,
220
+ inputPreview: summary,
221
+ target: grokToolLocation(update) || undefined,
222
+ arguments: args,
223
+ provider: PROVIDER
224
+ });
225
+ }
226
+
227
+ function grokToolResult(update, output) {
228
+ const id = firstString(update.toolCallId, update.tool_call_id);
229
+ if (!id && !output) return null;
230
+ const isError = firstString(update.status) === "failed";
231
+ const lineCount = output ? output.split(/\r?\n/).length : 0;
232
+ return compactObject({
233
+ provider: PROVIDER,
234
+ id: id || undefined,
235
+ kind: "Tool result",
236
+ title: isError ? "Tool error" : "Tool result",
237
+ rawCategory: "tool_call_update",
238
+ category: grokToolCategory(firstString(update.kind, update.title)),
239
+ summary: firstLine(output),
240
+ output: output || "",
241
+ lineCount: lineCount || undefined,
242
+ collapsed: lineCount > 18 || undefined,
243
+ status: isError ? "error" : "completed"
244
+ });
245
+ }
246
+
247
+ function grokToolOutput(update) {
248
+ const fromContent = grokContentText(update);
249
+ if (fromContent) return fromContent;
250
+ const raw = update.rawOutput ?? update.raw_output;
251
+ if (typeof raw === "string") return raw.trim();
252
+ if (raw && typeof raw === "object") return JSON.stringify(raw);
253
+ return "";
254
+ }
255
+
256
+ function grokPlanText(update) {
257
+ const entries = Array.isArray(update.entries) ? update.entries : [];
258
+ if (!entries.length) return grokContentText(update);
259
+ return entries
260
+ .map((entry) => {
261
+ const status = firstString(entry?.status);
262
+ const content = firstString(entry?.content, entry?.title);
263
+ if (!content) return "";
264
+ return status ? `[${status}] ${content}` : content;
265
+ })
266
+ .filter(Boolean)
267
+ .join("\n");
268
+ }
269
+
270
+ function grokToolLocation(update) {
271
+ const locations = Array.isArray(update.locations) ? update.locations : [];
272
+ return firstString(locations[0]?.path, locations[0]?.uri);
273
+ }
274
+
275
+ function grokToolCategory(kind) {
276
+ const text = String(kind || "");
277
+ if (/think/i.test(text)) return "status";
278
+ if (/execute|shell|bash|command/i.test(text)) return "shell";
279
+ if (/fetch|web|url|search(?!_files)/i.test(text)) return "web";
280
+ if (/edit|delete|move|write|patch/i.test(text)) return "edit";
281
+ if (/read|view/i.test(text)) return "read";
282
+ if (/glob|grep|find/i.test(text)) return "search";
283
+ if (/agent|task|subagent/i.test(text)) return "task";
284
+ return "tool";
285
+ }
286
+
287
+ function summarizeToolArguments(value) {
288
+ if (value == null) return "";
289
+ if (typeof value === "string") return value.slice(0, 240);
290
+ if (typeof value !== "object") return String(value).slice(0, 240);
291
+ for (const key of ["command", "prompt", "query", "pattern", "path", "file_path", "url"]) {
292
+ if (value[key]) return String(value[key]).slice(0, 240);
293
+ }
294
+ return Object.entries(value)
295
+ .slice(0, 3)
296
+ .map(([key, item]) => `${key}: ${typeof item === "string" ? item : JSON.stringify(item)}`)
297
+ .join(", ")
298
+ .slice(0, 240);
299
+ }
300
+
301
+ function toolDisplayName(value) {
302
+ const text = String(value || "tool").trim();
303
+ return text
304
+ .split(/_+|(?=[A-Z])/g)
305
+ .filter(Boolean)
306
+ .map((part) => part.replace(/(^|[-\s])([a-z])/g, (_, prefix, char) => `${prefix}${char.toUpperCase()}`))
307
+ .join(" ");
308
+ }
309
+
310
+ function firstLine(value) {
311
+ return String(value || "").split(/\r?\n/).find((line) => line.trim())?.trim() || "";
312
+ }
313
+
314
+ function offsetTimestamp(base, index) {
315
+ const date = new Date(base);
316
+ if (Number.isNaN(date.valueOf())) return base;
317
+ return new Date(date.valueOf() + index * 1000).toISOString();
318
+ }
319
+
320
+ function toIso(value) {
321
+ if (!value && value !== 0) return "";
322
+ if (typeof value === "number") {
323
+ const ms = value > 1e12 ? value : value * 1000;
324
+ const date = new Date(ms);
325
+ return Number.isNaN(date.valueOf()) ? "" : date.toISOString();
326
+ }
327
+ const date = new Date(value);
328
+ return Number.isNaN(date.valueOf()) ? "" : date.toISOString();
329
+ }
330
+
331
+ function parseJson(text) {
332
+ if (typeof text !== "string" || !text.trim()) return null;
333
+ try {
334
+ return JSON.parse(text);
335
+ } catch {
336
+ return null;
337
+ }
338
+ }
339
+
340
+ function firstString(...values) {
341
+ for (const value of values) {
342
+ if (typeof value === "string" && value.trim()) return value.trim();
343
+ }
344
+ return "";
345
+ }
346
+
347
+ function positiveNumber(value) {
348
+ const number = Number(value);
349
+ return Number.isFinite(number) && number > 0 ? number : undefined;
350
+ }
351
+
352
+ function compactObject(value) {
353
+ if (!value || typeof value !== "object") return value;
354
+ const result = {};
355
+ for (const [key, item] of Object.entries(value)) {
356
+ if (item === undefined || item === null || item === "") continue;
357
+ result[key] = item;
358
+ }
359
+ return Object.keys(result).length ? result : undefined;
360
+ }
361
+
362
+ module.exports = {
363
+ decodeGrokCwd,
364
+ grokSessionSummary,
365
+ parseGrokUpdatesFile,
366
+ parseGrokUpdatesText
367
+ };