grok-telegram-bot 2.3.0 → 2.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.
- package/.env.example +26 -0
- package/CHANGELOG.md +55 -0
- package/package.json +1 -1
- package/scripts/analyze-jsonl.ts +33 -0
- package/scripts/delayed-restart.ps1 +29 -0
- package/scripts/probe-exit-response-shape.py +77 -0
- package/scripts/probe-plan-exit.py +60 -0
- package/scripts/probe-plan-exit2.py +48 -0
- package/scripts/probe-plan-fields.py +41 -0
- package/scripts/probe-plan-fields2.py +58 -0
- package/scripts/probe-plan-response-path.py +48 -0
- package/scripts/sample-claude-tooluse.ts +21 -0
- package/scripts/sample-kiro-events.ts +31 -0
- package/scripts/smoke-exit-plan.ts +274 -0
- package/scripts/smoke-exit-shapes.ts +252 -0
- package/scripts/smoke-import.mjs +82 -0
- package/scripts/smoke-import.ts +73 -0
- package/src/app/accounts.ts +84 -0
- package/src/app/instance-lock.ts +6 -0
- package/src/app/types.ts +19 -2
- package/src/app/updater.ts +17 -6
- package/src/app/usage.ts +204 -7
- package/src/bot/account-rotator.ts +71 -2
- package/src/bot/bot.ts +36 -0
- package/src/bot/chat-controller.ts +35 -0
- package/src/bot/commands.ts +2 -0
- package/src/bot/complexity-gate.ts +69 -0
- package/src/bot/deps.ts +19 -0
- package/src/bot/handlers/accounts.ts +55 -5
- package/src/bot/handlers/import-session.ts +290 -0
- package/src/bot/handlers/menu.ts +17 -38
- package/src/bot/handlers/message.ts +1 -0
- package/src/bot/handlers/running.ts +35 -5
- package/src/bot/handlers/session-card.ts +12 -0
- package/src/bot/handlers/sessions.ts +14 -3
- package/src/bot/handlers/usage.ts +118 -16
- package/src/bot/menu/keyboard.ts +5 -4
- package/src/bot/menu/status-panel.ts +19 -6
- package/src/bot/prompt-content.ts +4 -0
- package/src/bot/reauth-controller.ts +2 -2
- package/src/bot/session-fork.ts +11 -0
- package/src/bot/session-runtime.ts +831 -64
- package/src/bot/suggestions.ts +429 -0
- package/src/config.ts +41 -0
- package/src/grok/client.ts +106 -20
- package/src/grok/plan-approval.ts +72 -0
- package/src/grok/session-log.ts +16 -0
- package/src/grok/types.ts +21 -2
- package/src/import/build-import.ts +132 -0
- package/src/import/history-readers.ts +681 -0
- package/src/import/list-running.ts +100 -0
- package/src/import/sources.ts +78 -0
- package/src/index.ts +179 -24
- package/src/render/diff.ts +11 -2
- package/src/render/file-summary.ts +31 -1
- package/src/render/markdown.ts +293 -35
- package/src/render/plan.ts +127 -0
- package/src/render/session-comment.ts +261 -0
- package/src/render/tool-call-detail.ts +400 -19
- package/src/render/tool-call-merge.ts +115 -0
- package/src/render/tool-call.ts +405 -142
- package/src/render/truncate.ts +85 -0
- package/src/service/windows.ts +14 -2
- package/src/sessions/history.ts +57 -0
- package/src/sessions/store.ts +3 -0
- package/src/sessions/types.ts +5 -0
- package/src/stream/streamer.ts +73 -9
- package/src/tasks/runner.ts +4 -3
|
@@ -1,24 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tool-call-detail.ts
|
|
3
3
|
*
|
|
4
|
-
* Rich detail extractors
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Each function returns a RAW markdown string (code blocks, etc.) appended after
|
|
9
|
-
* the tool-call icon + title line.
|
|
4
|
+
* Rich detail extractors + tool identity resolution for ACP tool updates.
|
|
5
|
+
* Maps Grok tool names (read_file, run_terminal_command, grep, …) to display
|
|
6
|
+
* kinds and pulls path / command / query / offset args from many raw shapes.
|
|
10
7
|
*/
|
|
11
|
-
import type
|
|
8
|
+
import { contentText, type SessionUpdate, type ToolCallContent } from "../grok/types.js";
|
|
12
9
|
|
|
13
10
|
/** Max chars to show for search queries, command previews, etc. */
|
|
14
11
|
export const PREVIEW_MAX = 600;
|
|
15
12
|
/** Max chars for file content preview on write/create. */
|
|
16
13
|
export const CONTENT_PREVIEW_MAX = 1000;
|
|
14
|
+
/** Max chars for terminal / tool result bodies (display-only middle truncate). */
|
|
15
|
+
export const OUTPUT_PREVIEW_MAX = 3500;
|
|
16
|
+
/** Max lines for multi-line tool outputs before middle-omission. */
|
|
17
|
+
export const OUTPUT_PREVIEW_LINES = 80;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Canonical display kinds used by the Telegram renderer.
|
|
21
|
+
* (ACP often reports kind "other" — we re-derive from the tool name.)
|
|
22
|
+
*/
|
|
23
|
+
export type ToolDisplayKind =
|
|
24
|
+
| "read"
|
|
25
|
+
| "edit"
|
|
26
|
+
| "write"
|
|
27
|
+
| "create"
|
|
28
|
+
| "execute"
|
|
29
|
+
| "search"
|
|
30
|
+
| "delete"
|
|
31
|
+
| "move"
|
|
32
|
+
| "rename"
|
|
33
|
+
| "fetch"
|
|
34
|
+
| "web_search"
|
|
35
|
+
| "web_fetch"
|
|
36
|
+
| "list"
|
|
37
|
+
| "todo"
|
|
38
|
+
| "think"
|
|
39
|
+
| "image"
|
|
40
|
+
| "mcp"
|
|
41
|
+
| "other";
|
|
42
|
+
|
|
43
|
+
export interface ToolIdentity {
|
|
44
|
+
/** Canonical kind for icons + formatters. */
|
|
45
|
+
kind: ToolDisplayKind;
|
|
46
|
+
/** Raw tool name when known (read_file, grep, server__tool, …). */
|
|
47
|
+
toolName: string;
|
|
48
|
+
/** True when this is a real namespaced MCP call. */
|
|
49
|
+
isMcp: boolean;
|
|
50
|
+
mcpServer?: string;
|
|
51
|
+
mcpMethod?: string;
|
|
52
|
+
}
|
|
17
53
|
|
|
18
|
-
/** Normalize a
|
|
54
|
+
/** Normalize a loose ACP kind string (aliases only — no name inference). */
|
|
19
55
|
export function normalizeKind(kind: string | undefined): string {
|
|
20
56
|
const k = (kind || "other").toLowerCase().trim();
|
|
21
|
-
// Map common variants to canonical kinds.
|
|
22
57
|
const ALIASES: Record<string, string> = {
|
|
23
58
|
bash: "execute",
|
|
24
59
|
shell: "execute",
|
|
@@ -28,8 +63,8 @@ export function normalizeKind(kind: string | undefined): string {
|
|
|
28
63
|
glob: "search",
|
|
29
64
|
find: "search",
|
|
30
65
|
ripgrep: "search",
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
web_search: "web_search",
|
|
67
|
+
web_fetch: "fetch",
|
|
33
68
|
url: "fetch",
|
|
34
69
|
http: "fetch",
|
|
35
70
|
request: "fetch",
|
|
@@ -37,18 +72,223 @@ export function normalizeKind(kind: string | undefined): string {
|
|
|
37
72
|
copy: "move",
|
|
38
73
|
mkdir: "create",
|
|
39
74
|
touch: "create",
|
|
75
|
+
list: "list",
|
|
76
|
+
ls: "list",
|
|
40
77
|
};
|
|
41
78
|
return ALIASES[k] ?? k;
|
|
42
79
|
}
|
|
43
80
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Resolve display identity from ACP kind + title + rawInput.
|
|
83
|
+
* Prefer the real tool name (read_file / grep / …) over a vague ACP `other`.
|
|
84
|
+
*/
|
|
85
|
+
export function resolveToolIdentity(u: SessionUpdate, raw: Record<string, unknown>): ToolIdentity {
|
|
86
|
+
const toolName = extractToolName(u, raw);
|
|
87
|
+
const lower = toolName.toLowerCase();
|
|
88
|
+
const acpKind = normalizeKind(u.kind);
|
|
89
|
+
|
|
90
|
+
// True MCP only when namespaced (server__tool, @server/tool, …).
|
|
91
|
+
const mcp = parseMcpName(toolName);
|
|
92
|
+
if (mcp) {
|
|
93
|
+
// Still map known methods (e.g. filesystem__read_file) to a display kind.
|
|
94
|
+
const methodKind = kindFromToolName(mcp.method);
|
|
95
|
+
const kind: ToolDisplayKind =
|
|
96
|
+
methodKind && methodKind !== "other" ? methodKind : "mcp";
|
|
97
|
+
return {
|
|
98
|
+
kind,
|
|
99
|
+
toolName,
|
|
100
|
+
isMcp: true,
|
|
101
|
+
mcpServer: mcp.server,
|
|
102
|
+
mcpMethod: mcp.method,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const fromName = kindFromToolName(toolName) || kindFromTitle(u.title);
|
|
107
|
+
// ACP kinds that are specific win; "other"/empty defer to the name.
|
|
108
|
+
if (acpKind && acpKind !== "other" && acpKind !== "") {
|
|
109
|
+
// Name is more specific for Grok tools (kind=other often, or wrong kind).
|
|
110
|
+
if (fromName && fromName !== "other" && (acpKind === "other" || looksLikeGrokToolName(lower))) {
|
|
111
|
+
return { kind: fromName, toolName, isMcp: false };
|
|
112
|
+
}
|
|
113
|
+
return { kind: (acpKind as ToolDisplayKind) || "other", toolName, isMcp: false };
|
|
114
|
+
}
|
|
115
|
+
return { kind: fromName ?? "other", toolName, isMcp: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Extract the tool name from update fields / raw input / title. */
|
|
119
|
+
export function extractToolName(u: SessionUpdate, raw: Record<string, unknown>): string {
|
|
120
|
+
// ACP top-level name (RFD) and common Grok extensions first.
|
|
121
|
+
const top =
|
|
122
|
+
strOf(u.name) ||
|
|
123
|
+
strOf(u.toolName) ||
|
|
124
|
+
strOf((u as { tool_name?: unknown }).tool_name);
|
|
125
|
+
if (top) return top.trim();
|
|
126
|
+
|
|
127
|
+
const explicit =
|
|
128
|
+
strOf(raw.tool_name) ||
|
|
129
|
+
strOf(raw.toolName) ||
|
|
130
|
+
strOf(raw.name) ||
|
|
131
|
+
strOf(raw.tool) ||
|
|
132
|
+
strOf(raw.function) ||
|
|
133
|
+
strOf(raw.function_name);
|
|
134
|
+
if (explicit) return explicit.trim();
|
|
135
|
+
const t = (u.title || "").trim();
|
|
136
|
+
// Title that looks like an identifier (read_file, run_terminal_command).
|
|
137
|
+
if (t && /^[@a-zA-Z0-9._/-]+$/.test(t) && !t.includes(" ") && !/^tool[_ ]?call$/i.test(t)) {
|
|
138
|
+
return t;
|
|
139
|
+
}
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Map a tool name (or MCP method) to a display kind. */
|
|
144
|
+
export function kindFromToolName(name: string): ToolDisplayKind | undefined {
|
|
145
|
+
if (!name) return undefined;
|
|
146
|
+
const n = name.toLowerCase().replace(/[\s-]+/g, "_");
|
|
147
|
+
|
|
148
|
+
// Exact / suffix matches for Grok Build + common agent tools.
|
|
149
|
+
if (
|
|
150
|
+
/^(read_file|read|fs_read|file_read|view|open_file|cat)$/.test(n) ||
|
|
151
|
+
n.endsWith("_read") ||
|
|
152
|
+
n.includes("read_file")
|
|
153
|
+
) {
|
|
154
|
+
return "read";
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
/^(write|write_file|fs_write|create_file|create)$/.test(n) ||
|
|
158
|
+
n.includes("write_file") ||
|
|
159
|
+
n === "write"
|
|
160
|
+
) {
|
|
161
|
+
return "write";
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
/^(search_replace|str_replace|string_replace|edit|edit_file|apply_patch|multi_edit|smart_edit)$/.test(n) ||
|
|
165
|
+
n.includes("replace") ||
|
|
166
|
+
n.includes("search_replace")
|
|
167
|
+
) {
|
|
168
|
+
return "edit";
|
|
169
|
+
}
|
|
170
|
+
if (/^(delete|delete_file|rm|unlink|remove_file)$/.test(n) || n.includes("delete_file")) {
|
|
171
|
+
return "delete";
|
|
172
|
+
}
|
|
173
|
+
if (/^(move|rename|mv|copy|cp)$/.test(n)) {
|
|
174
|
+
return n.includes("rename") ? "rename" : "move";
|
|
175
|
+
}
|
|
176
|
+
if (
|
|
177
|
+
/^(run_terminal_command|run_terminal|shell|bash|execute|execute_bash|command|terminal)$/.test(n) ||
|
|
178
|
+
n.includes("terminal") ||
|
|
179
|
+
n.includes("shell") ||
|
|
180
|
+
n.startsWith("run_")
|
|
181
|
+
) {
|
|
182
|
+
return "execute";
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
/^(grep|glob|search|rg|smart_grep|smart_glob|find_files|codebase_search)$/.test(n) ||
|
|
186
|
+
n.includes("grep") ||
|
|
187
|
+
n.includes("glob") ||
|
|
188
|
+
n.endsWith("_search")
|
|
189
|
+
) {
|
|
190
|
+
// web_search handled below
|
|
191
|
+
if (n.includes("web")) return "web_search";
|
|
192
|
+
return "search";
|
|
193
|
+
}
|
|
194
|
+
if (/^(list_dir|list|ls|listdir|list_directory|read_dir)$/.test(n) || n.includes("list_dir")) {
|
|
195
|
+
return "list";
|
|
196
|
+
}
|
|
197
|
+
if (/^(web_search|websearch)$/.test(n) || (n.includes("web") && n.includes("search"))) {
|
|
198
|
+
return "web_search";
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
/^(web_fetch|webfetch|open_page|browse|fetch_url|http_request)$/.test(n) ||
|
|
202
|
+
n.includes("web_fetch") ||
|
|
203
|
+
n.includes("open_page")
|
|
204
|
+
) {
|
|
205
|
+
return "web_fetch";
|
|
206
|
+
}
|
|
207
|
+
if (/^(todo_write|todo_list|todowrite|todoread)$/.test(n) || n.includes("todo")) {
|
|
208
|
+
return "todo";
|
|
209
|
+
}
|
|
210
|
+
if (
|
|
211
|
+
/^(spawn_subagent|task|delegate|subagent|think|thinking)$/.test(n) ||
|
|
212
|
+
n.includes("subagent") ||
|
|
213
|
+
n.includes("spawn")
|
|
214
|
+
) {
|
|
215
|
+
return "think";
|
|
216
|
+
}
|
|
217
|
+
if (/image|video|imagine|nano.?banana|screenshot/.test(n)) {
|
|
218
|
+
return "image";
|
|
219
|
+
}
|
|
220
|
+
// Broad regex fallbacks (same spirit as models.toolKind).
|
|
221
|
+
if (/write|edit|create|apply|patch|str_replace|delete|move|mkdir/.test(n) && !/read/.test(n)) {
|
|
222
|
+
if (/delete|rm|unlink/.test(n)) return "delete";
|
|
223
|
+
if (/move|rename/.test(n)) return "move";
|
|
224
|
+
if (/write|create|mkdir|touch/.test(n)) return "write";
|
|
225
|
+
return "edit";
|
|
226
|
+
}
|
|
227
|
+
if (/read|view|open|cat|stat/.test(n)) return "read";
|
|
228
|
+
if (/search|grep|find|glob/.test(n)) return "search";
|
|
229
|
+
if (/bash|shell|exec|run|command|terminal|process/.test(n)) return "execute";
|
|
230
|
+
if (/fetch|http|curl|browse/.test(n)) return "fetch";
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function kindFromTitle(title: string | undefined): ToolDisplayKind | undefined {
|
|
235
|
+
if (!title) return undefined;
|
|
236
|
+
const t = title.toLowerCase();
|
|
237
|
+
if (/^read\b|reading\b/.test(t)) return "read";
|
|
238
|
+
if (/^edit\b|^write\b|^create\b|edited|writing/.test(t)) return "edit";
|
|
239
|
+
if (/^run\b|^exec\b|command|shell|bash|terminal/.test(t)) return "execute";
|
|
240
|
+
if (/search|grep|glob|find/.test(t)) return "search";
|
|
241
|
+
if (/delete|remove/.test(t)) return "delete";
|
|
242
|
+
if (/fetch|http|url/.test(t)) return "fetch";
|
|
243
|
+
if (/web search/.test(t)) return "web_search";
|
|
244
|
+
if (/list|directory|folder/.test(t)) return "list";
|
|
245
|
+
return kindFromToolName(title.replace(/\s+/g, "_"));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function looksLikeGrokToolName(n: string): boolean {
|
|
249
|
+
return (
|
|
250
|
+
n.includes("_") ||
|
|
251
|
+
/^(read|write|grep|glob|shell|bash|edit|search|list|todo|fetch|run)/i.test(n)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Parse server__method / @server/method MCP-style names. */
|
|
256
|
+
export function parseMcpName(name: string): { server: string; method: string } | undefined {
|
|
257
|
+
if (!name) return undefined;
|
|
258
|
+
const patterns = [
|
|
259
|
+
/^@([a-z0-9._-]+)[/_]{1,3}(.+)$/i,
|
|
260
|
+
/^([a-z0-9.-]+)___(.+)$/i,
|
|
261
|
+
/^([a-z0-9.-]+)__(.+)$/i,
|
|
262
|
+
/^([a-z0-9.-]+)\/([a-z0-9_.-]+)$/i,
|
|
263
|
+
];
|
|
264
|
+
for (const re of patterns) {
|
|
265
|
+
const m = re.exec(name);
|
|
266
|
+
if (m) return { server: m[1]!, method: m[2]! };
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Extract the primary file path from a tool-call raw input (+ locations). */
|
|
272
|
+
export function extractPath(raw: Record<string, unknown>, u?: SessionUpdate): string {
|
|
273
|
+
const fromLoc = u?.locations?.find((l) => l.path)?.path;
|
|
46
274
|
return (
|
|
275
|
+
strOf(fromLoc) ||
|
|
47
276
|
strOf(raw.path) ||
|
|
48
277
|
strOf(raw.file_path) ||
|
|
278
|
+
strOf(raw.filePath) ||
|
|
279
|
+
strOf(raw.target_file) ||
|
|
280
|
+
strOf(raw.targetFile) ||
|
|
281
|
+
strOf(raw.target_path) ||
|
|
282
|
+
strOf(raw.targetPath) ||
|
|
49
283
|
strOf(raw.filename) ||
|
|
50
284
|
strOf(raw.file) ||
|
|
51
|
-
strOf(raw.
|
|
285
|
+
strOf(raw.uri) ||
|
|
286
|
+
// Nested args (some MCP wrappers).
|
|
287
|
+
strOf(nested(raw, "arguments", "path")) ||
|
|
288
|
+
strOf(nested(raw, "arguments", "file_path")) ||
|
|
289
|
+
strOf(nested(raw, "arguments", "target_file")) ||
|
|
290
|
+
strOf(nested(raw, "input", "path")) ||
|
|
291
|
+
strOf(nested(raw, "input", "target_file")) ||
|
|
52
292
|
""
|
|
53
293
|
);
|
|
54
294
|
}
|
|
@@ -77,6 +317,8 @@ export function extractSearchQuery(raw: Record<string, unknown>): string {
|
|
|
77
317
|
strOf(raw.glob) ||
|
|
78
318
|
strOf(raw.term) ||
|
|
79
319
|
strOf(raw.q) ||
|
|
320
|
+
strOf(nested(raw, "arguments", "pattern")) ||
|
|
321
|
+
strOf(nested(raw, "arguments", "query")) ||
|
|
80
322
|
""
|
|
81
323
|
);
|
|
82
324
|
}
|
|
@@ -90,6 +332,8 @@ export function extractSearchPath(raw: Record<string, unknown>): string {
|
|
|
90
332
|
strOf(raw.scope) ||
|
|
91
333
|
strOf(raw.cwd) ||
|
|
92
334
|
strOf(raw.folder) ||
|
|
335
|
+
strOf(raw.target_directory) ||
|
|
336
|
+
strOf(raw.targetDirectory) ||
|
|
93
337
|
""
|
|
94
338
|
);
|
|
95
339
|
}
|
|
@@ -101,13 +345,21 @@ export function extractUrl(raw: Record<string, unknown>): string {
|
|
|
101
345
|
strOf(raw.uri) ||
|
|
102
346
|
strOf(raw.link) ||
|
|
103
347
|
strOf(raw.endpoint) ||
|
|
348
|
+
strOf(nested(raw, "arguments", "url")) ||
|
|
104
349
|
""
|
|
105
350
|
);
|
|
106
351
|
}
|
|
107
352
|
|
|
108
353
|
/** Extract command string from an execute/shell call. */
|
|
109
354
|
export function extractCommand(raw: Record<string, unknown>): string {
|
|
110
|
-
return
|
|
355
|
+
return (
|
|
356
|
+
strOf(raw.command) ||
|
|
357
|
+
strOf(raw.cmd) ||
|
|
358
|
+
strOf(raw.shell_command) ||
|
|
359
|
+
strOf(raw.shellCommand) ||
|
|
360
|
+
strOf(nested(raw, "arguments", "command")) ||
|
|
361
|
+
""
|
|
362
|
+
);
|
|
111
363
|
}
|
|
112
364
|
|
|
113
365
|
/** Extract file content for write/create operations. */
|
|
@@ -117,26 +369,140 @@ export function extractContent(raw: Record<string, unknown>): string {
|
|
|
117
369
|
strOf(raw.file_text) ||
|
|
118
370
|
strOf(raw.text) ||
|
|
119
371
|
strOf(raw.data) ||
|
|
372
|
+
strOf(raw.new_string) ||
|
|
373
|
+
strOf(raw.newString) ||
|
|
120
374
|
""
|
|
121
375
|
);
|
|
122
376
|
}
|
|
123
377
|
|
|
378
|
+
/** Read-range / paging args for display. */
|
|
379
|
+
export function extractReadRange(raw: Record<string, unknown>): {
|
|
380
|
+
offset?: string;
|
|
381
|
+
limit?: string;
|
|
382
|
+
startLine?: string;
|
|
383
|
+
pages?: string;
|
|
384
|
+
} {
|
|
385
|
+
const offset = strOf(raw.offset) || strOf(raw.start) || numStr(raw.offset);
|
|
386
|
+
const limit = strOf(raw.limit) || strOf(raw.count) || numStr(raw.limit);
|
|
387
|
+
const startLine =
|
|
388
|
+
strOf(raw.start_line) ||
|
|
389
|
+
strOf(raw.startLine) ||
|
|
390
|
+
strOf(raw.line) ||
|
|
391
|
+
numStr(raw.start_line) ||
|
|
392
|
+
numStr(raw.line);
|
|
393
|
+
const pages = strOf(raw.pages) || strOf(raw.page);
|
|
394
|
+
const out: { offset?: string; limit?: string; startLine?: string; pages?: string } = {};
|
|
395
|
+
if (offset) out.offset = offset;
|
|
396
|
+
if (limit) out.limit = limit;
|
|
397
|
+
if (startLine) out.startLine = startLine;
|
|
398
|
+
if (pages) out.pages = pages;
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
|
|
124
402
|
/** Extract include/exclude filters from a search call. */
|
|
125
403
|
export function extractFilters(raw: Record<string, unknown>): { include?: string; exclude?: string } {
|
|
126
|
-
const include =
|
|
127
|
-
|
|
404
|
+
const include =
|
|
405
|
+
strOf(raw.include) || strOf(raw.glob) || strOf(raw.file_pattern) || strOf(raw.type) || strOf(raw.glob_pattern);
|
|
406
|
+
const exclude = strOf(raw.exclude) || strOf(raw.ignore) || strOf(raw.exclude_pattern);
|
|
128
407
|
const out: { include?: string; exclude?: string } = {};
|
|
129
408
|
if (include) out.include = include;
|
|
130
409
|
if (exclude) out.exclude = exclude;
|
|
131
410
|
return out;
|
|
132
411
|
}
|
|
133
412
|
|
|
134
|
-
/**
|
|
413
|
+
/**
|
|
414
|
+
* Pretty-print non-empty raw args for "other" / MCP tools so nothing important
|
|
415
|
+
* is hidden. Skips meta keys and huge blobs (middle-truncated later by caller).
|
|
416
|
+
*/
|
|
417
|
+
export function formatArgLines(raw: Record<string, unknown>, maxLines = 24): string {
|
|
418
|
+
const SKIP = new Set([
|
|
419
|
+
"tool_name",
|
|
420
|
+
"toolName",
|
|
421
|
+
"name",
|
|
422
|
+
"tool",
|
|
423
|
+
"type",
|
|
424
|
+
"_meta",
|
|
425
|
+
"function",
|
|
426
|
+
"function_name",
|
|
427
|
+
]);
|
|
428
|
+
const lines: string[] = [];
|
|
429
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
430
|
+
if (SKIP.has(key)) continue;
|
|
431
|
+
if (val === undefined || val === null || val === "") continue;
|
|
432
|
+
let s: string;
|
|
433
|
+
if (typeof val === "string") s = val;
|
|
434
|
+
else if (typeof val === "number" || typeof val === "boolean") s = String(val);
|
|
435
|
+
else {
|
|
436
|
+
try {
|
|
437
|
+
s = JSON.stringify(val);
|
|
438
|
+
} catch {
|
|
439
|
+
s = String(val);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (s.length > 240) s = s.slice(0, 239) + "\u2026";
|
|
443
|
+
lines.push(`${key}: ${s}`);
|
|
444
|
+
if (lines.length >= maxLines) {
|
|
445
|
+
lines.push("\u2026");
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return lines.join("\n");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Truncate text to max chars with ellipsis (head-only). */
|
|
135
453
|
export function truncate(text: string, max: number): string {
|
|
136
454
|
if (text.length <= max) return text;
|
|
137
455
|
return text.slice(0, max - 1) + "\u2026";
|
|
138
456
|
}
|
|
139
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Extract human-readable tool result text from ACP content blocks / content.
|
|
460
|
+
* Used so completed execute/search/fetch calls show their output in Telegram
|
|
461
|
+
* (display-truncated later — full results still live in the agent session).
|
|
462
|
+
*/
|
|
463
|
+
export function extractToolOutput(u: SessionUpdate): string {
|
|
464
|
+
const parts: string[] = [];
|
|
465
|
+
const push = (v: unknown): void => {
|
|
466
|
+
if (typeof v === "string" && v.trim()) parts.push(v);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
for (const b of collectContent(u)) {
|
|
470
|
+
if (b.type === "diff") continue; // handled separately as a unified diff
|
|
471
|
+
const nested = b.content;
|
|
472
|
+
if (nested && typeof nested === "object") {
|
|
473
|
+
push((nested as { text?: unknown }).text);
|
|
474
|
+
const data = (nested as { data?: unknown }).data;
|
|
475
|
+
if (typeof data === "string") push(data);
|
|
476
|
+
}
|
|
477
|
+
const extra = b as Record<string, unknown>;
|
|
478
|
+
push(extra.text);
|
|
479
|
+
push(extra.output);
|
|
480
|
+
push(extra.stdout);
|
|
481
|
+
push(extra.stderr);
|
|
482
|
+
if (typeof extra.data === "string") push(extra.data);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
push(contentText(u.content));
|
|
486
|
+
const rawOut = (u as { rawOutput?: unknown }).rawOutput;
|
|
487
|
+
if (rawOut && typeof rawOut === "object") {
|
|
488
|
+
const ro = rawOut as Record<string, unknown>;
|
|
489
|
+
push(ro.output);
|
|
490
|
+
push(ro.stdout);
|
|
491
|
+
push(ro.stderr);
|
|
492
|
+
push(ro.text);
|
|
493
|
+
push(ro.message);
|
|
494
|
+
} else if (typeof rawOut === "string") {
|
|
495
|
+
push(rawOut);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const out: string[] = [];
|
|
499
|
+
for (const p of parts) {
|
|
500
|
+
if (out[out.length - 1] === p) continue;
|
|
501
|
+
out.push(p);
|
|
502
|
+
}
|
|
503
|
+
return out.join("\n").trim();
|
|
504
|
+
}
|
|
505
|
+
|
|
140
506
|
/** Collect all content blocks (diffs, text, etc.) from a tool update. */
|
|
141
507
|
export function collectContent(u: SessionUpdate): ToolCallContent[] {
|
|
142
508
|
const out: ToolCallContent[] = [];
|
|
@@ -156,6 +522,9 @@ export function gatherPaths(u: SessionUpdate, raw: Record<string, unknown>): str
|
|
|
156
522
|
add(raw.file_path);
|
|
157
523
|
add(raw.filename);
|
|
158
524
|
add(raw.file);
|
|
525
|
+
add(raw.target_file);
|
|
526
|
+
add(raw.targetFile);
|
|
527
|
+
add(raw.target_path);
|
|
159
528
|
if (Array.isArray(raw.operations)) {
|
|
160
529
|
for (const op of raw.operations) {
|
|
161
530
|
if (op && typeof op === "object") add((op as Record<string, unknown>).path);
|
|
@@ -165,6 +534,18 @@ export function gatherPaths(u: SessionUpdate, raw: Record<string, unknown>): str
|
|
|
165
534
|
return out;
|
|
166
535
|
}
|
|
167
536
|
|
|
537
|
+
function nested(raw: Record<string, unknown>, a: string, b: string): unknown {
|
|
538
|
+
const o = raw[a];
|
|
539
|
+
if (o && typeof o === "object" && !Array.isArray(o)) {
|
|
540
|
+
return (o as Record<string, unknown>)[b];
|
|
541
|
+
}
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
168
545
|
function strOf(v: unknown): string {
|
|
169
546
|
return typeof v === "string" ? v : "";
|
|
170
|
-
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function numStr(v: unknown): string {
|
|
550
|
+
return typeof v === "number" && Number.isFinite(v) ? String(v) : "";
|
|
551
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge ACP tool_call + tool_call_update patches by toolCallId.
|
|
3
|
+
*
|
|
4
|
+
* Grok often sends a rich first notification (title/kind/rawInput/locations),
|
|
5
|
+
* then a completed update with only { toolCallId, status }. Formatting the
|
|
6
|
+
* update alone produces the useless "Tool call ✓" line — always format the
|
|
7
|
+
* merged snapshot instead.
|
|
8
|
+
*/
|
|
9
|
+
import type { SessionUpdate, ToolCallContent } from "../grok/types.js";
|
|
10
|
+
|
|
11
|
+
export type ToolSnapshot = SessionUpdate & {
|
|
12
|
+
toolCallId?: string;
|
|
13
|
+
rawInput?: Record<string, unknown>;
|
|
14
|
+
locations?: Array<{ path?: string; line?: number }>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Merge a new patch onto a previous snapshot (patch semantics: only set fields replace). */
|
|
18
|
+
export function mergeToolSnapshot(prev: ToolSnapshot | undefined, patch: SessionUpdate): ToolSnapshot {
|
|
19
|
+
const base: ToolSnapshot = prev ? { ...prev } : { sessionUpdate: patch.sessionUpdate };
|
|
20
|
+
// Always keep latest sessionUpdate type for debugging; display uses fields not type.
|
|
21
|
+
base.sessionUpdate = patch.sessionUpdate || base.sessionUpdate;
|
|
22
|
+
|
|
23
|
+
if (patch.toolCallId) base.toolCallId = patch.toolCallId;
|
|
24
|
+
if (patch.title != null && String(patch.title).trim() && !isGenericTitle(String(patch.title))) {
|
|
25
|
+
base.title = patch.title;
|
|
26
|
+
} else if (patch.title != null && !base.title) {
|
|
27
|
+
base.title = patch.title;
|
|
28
|
+
}
|
|
29
|
+
if (patch.name != null && String(patch.name).trim()) base.name = patch.name;
|
|
30
|
+
if (patch.toolName != null && String(patch.toolName).trim()) base.toolName = patch.toolName;
|
|
31
|
+
if (patch.kind != null && String(patch.kind).trim() && String(patch.kind).toLowerCase() !== "other") {
|
|
32
|
+
base.kind = patch.kind;
|
|
33
|
+
} else if (patch.kind != null && !base.kind) {
|
|
34
|
+
base.kind = patch.kind;
|
|
35
|
+
}
|
|
36
|
+
if (patch.status != null) base.status = patch.status;
|
|
37
|
+
|
|
38
|
+
if (patch.rawInput && typeof patch.rawInput === "object") {
|
|
39
|
+
base.rawInput = { ...(base.rawInput || {}), ...normalizeRawKeys(patch.rawInput as Record<string, unknown>) };
|
|
40
|
+
}
|
|
41
|
+
// snake_case wire variants
|
|
42
|
+
const snakeIn = (patch as { raw_input?: unknown }).raw_input;
|
|
43
|
+
if (snakeIn && typeof snakeIn === "object" && !Array.isArray(snakeIn)) {
|
|
44
|
+
base.rawInput = {
|
|
45
|
+
...(base.rawInput || {}),
|
|
46
|
+
...normalizeRawKeys(snakeIn as Record<string, unknown>),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (patch.rawOutput !== undefined) base.rawOutput = patch.rawOutput;
|
|
51
|
+
const snakeOut = (patch as { raw_output?: unknown }).raw_output;
|
|
52
|
+
if (snakeOut !== undefined) base.rawOutput = snakeOut;
|
|
53
|
+
|
|
54
|
+
if (Array.isArray(patch.locations) && patch.locations.length) {
|
|
55
|
+
base.locations = patch.locations;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Content: prefer non-empty arrays
|
|
59
|
+
if (Array.isArray(patch.content_blocks) && patch.content_blocks.length) {
|
|
60
|
+
base.content_blocks = patch.content_blocks as ToolCallContent[];
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(patch.content) && (patch.content as unknown[]).length) {
|
|
63
|
+
base.content = patch.content;
|
|
64
|
+
// Mirror into content_blocks for older formatters.
|
|
65
|
+
if (!base.content_blocks?.length) {
|
|
66
|
+
base.content_blocks = patch.content as ToolCallContent[];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Lift path from locations into rawInput for path extractors.
|
|
71
|
+
if (base.locations?.[0]?.path) {
|
|
72
|
+
base.rawInput = base.rawInput || {};
|
|
73
|
+
if (!base.rawInput.path && !base.rawInput.target_file && !base.rawInput.file_path) {
|
|
74
|
+
base.rawInput.path = base.locations[0].path;
|
|
75
|
+
}
|
|
76
|
+
if (base.locations[0].line != null && base.rawInput.start_line == null) {
|
|
77
|
+
base.rawInput.start_line = base.locations[0].line;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return base;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** True when a title is useless alone (Grok default). */
|
|
85
|
+
export function isGenericTitle(title: string): boolean {
|
|
86
|
+
const t = title.trim().toLowerCase();
|
|
87
|
+
return !t || t === "tool call" || t === "tool" || t === "other" || t === "tool_call";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether the snapshot has enough info to show a meaningful card. */
|
|
91
|
+
export function snapshotHasDetail(s: ToolSnapshot): boolean {
|
|
92
|
+
if (s.name || s.toolName) return true;
|
|
93
|
+
if (s.title && !isGenericTitle(String(s.title))) return true;
|
|
94
|
+
if (s.kind && String(s.kind).toLowerCase() !== "other") return true;
|
|
95
|
+
const raw = s.rawInput || {};
|
|
96
|
+
if (Object.keys(raw).length > 0) return true;
|
|
97
|
+
if (s.locations?.some((l) => l.path)) return true;
|
|
98
|
+
if (Array.isArray(s.content_blocks) && s.content_blocks.length > 0) return true;
|
|
99
|
+
if (Array.isArray(s.content) && (s.content as unknown[]).length > 0) return true;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeRawKeys(raw: Record<string, unknown>): Record<string, unknown> {
|
|
104
|
+
// Flatten nested arguments once so path/command extractors see them.
|
|
105
|
+
const out: Record<string, unknown> = { ...raw };
|
|
106
|
+
for (const key of ["arguments", "input", "args", "parameters"]) {
|
|
107
|
+
const nested = raw[key];
|
|
108
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
109
|
+
for (const [k, v] of Object.entries(nested as Record<string, unknown>)) {
|
|
110
|
+
if (out[k] === undefined) out[k] = v;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|