grok-telegram-bot 2.0.0 → 2.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,170 @@
1
+ /**
2
+ * tool-call-detail.ts
3
+ *
4
+ * Rich detail extractors for specific tool kinds: search queries, file reads,
5
+ * URLs, writes/creates content previews, move/rename source+dest, delete paths,
6
+ * web search queries, and MCP argument previews.
7
+ *
8
+ * Each function returns a RAW markdown string (code blocks, etc.) appended after
9
+ * the tool-call icon + title line.
10
+ */
11
+ import type { SessionUpdate, ToolCallContent } from "../grok/types.js";
12
+
13
+ /** Max chars to show for search queries, command previews, etc. */
14
+ export const PREVIEW_MAX = 600;
15
+ /** Max chars for file content preview on write/create. */
16
+ export const CONTENT_PREVIEW_MAX = 1000;
17
+
18
+ /** Normalize a tool-call kind string to a canonical lowercase value. */
19
+ export function normalizeKind(kind: string | undefined): string {
20
+ const k = (kind || "other").toLowerCase().trim();
21
+ // Map common variants to canonical kinds.
22
+ const ALIASES: Record<string, string> = {
23
+ bash: "execute",
24
+ shell: "execute",
25
+ command: "execute",
26
+ terminal: "execute",
27
+ grep: "search",
28
+ glob: "search",
29
+ find: "search",
30
+ ripgrep: "search",
31
+ "web_search": "web_search",
32
+ "web_fetch": "fetch",
33
+ url: "fetch",
34
+ http: "fetch",
35
+ request: "fetch",
36
+ rename: "move",
37
+ copy: "move",
38
+ mkdir: "create",
39
+ touch: "create",
40
+ };
41
+ return ALIASES[k] ?? k;
42
+ }
43
+
44
+ /** Extract the primary file path from a tool-call raw input. */
45
+ export function extractPath(raw: Record<string, unknown>): string {
46
+ return (
47
+ strOf(raw.path) ||
48
+ strOf(raw.file_path) ||
49
+ strOf(raw.filename) ||
50
+ strOf(raw.file) ||
51
+ strOf(raw.filePath) ||
52
+ ""
53
+ );
54
+ }
55
+
56
+ /** Extract a secondary/destination path (for moves, renames, copies). */
57
+ export function extractDestPath(raw: Record<string, unknown>): string {
58
+ return (
59
+ strOf(raw.new_path) ||
60
+ strOf(raw.newPath) ||
61
+ strOf(raw.destination) ||
62
+ strOf(raw.dest) ||
63
+ strOf(raw.to) ||
64
+ strOf(raw.target_path) ||
65
+ strOf(raw.targetPath) ||
66
+ ""
67
+ );
68
+ }
69
+
70
+ /** Extract search query / pattern from various raw input shapes. */
71
+ export function extractSearchQuery(raw: Record<string, unknown>): string {
72
+ return (
73
+ strOf(raw.pattern) ||
74
+ strOf(raw.query) ||
75
+ strOf(raw.search) ||
76
+ strOf(raw.regex) ||
77
+ strOf(raw.glob) ||
78
+ strOf(raw.term) ||
79
+ strOf(raw.q) ||
80
+ ""
81
+ );
82
+ }
83
+
84
+ /** Extract the search path/scope if present. */
85
+ export function extractSearchPath(raw: Record<string, unknown>): string {
86
+ return (
87
+ strOf(raw.path) ||
88
+ strOf(raw.directory) ||
89
+ strOf(raw.dir) ||
90
+ strOf(raw.scope) ||
91
+ strOf(raw.cwd) ||
92
+ strOf(raw.folder) ||
93
+ ""
94
+ );
95
+ }
96
+
97
+ /** Extract a URL from a fetch/web request. */
98
+ export function extractUrl(raw: Record<string, unknown>): string {
99
+ return (
100
+ strOf(raw.url) ||
101
+ strOf(raw.uri) ||
102
+ strOf(raw.link) ||
103
+ strOf(raw.endpoint) ||
104
+ ""
105
+ );
106
+ }
107
+
108
+ /** Extract command string from an execute/shell call. */
109
+ export function extractCommand(raw: Record<string, unknown>): string {
110
+ return strOf(raw.command) || strOf(raw.cmd) || strOf(raw.shell_command) || "";
111
+ }
112
+
113
+ /** Extract file content for write/create operations. */
114
+ export function extractContent(raw: Record<string, unknown>): string {
115
+ return (
116
+ strOf(raw.content) ||
117
+ strOf(raw.file_text) ||
118
+ strOf(raw.text) ||
119
+ strOf(raw.data) ||
120
+ ""
121
+ );
122
+ }
123
+
124
+ /** Extract include/exclude filters from a search call. */
125
+ export function extractFilters(raw: Record<string, unknown>): { include?: string; exclude?: string } {
126
+ const include = strOf(raw.include) || strOf(raw.glob) || strOf(raw.file_pattern) || strOf(raw.type);
127
+ const exclude = strOf(raw.exclude) || strOf(raw.ignore);
128
+ const out: { include?: string; exclude?: string } = {};
129
+ if (include) out.include = include;
130
+ if (exclude) out.exclude = exclude;
131
+ return out;
132
+ }
133
+
134
+ /** Truncate text to max chars with ellipsis. */
135
+ export function truncate(text: string, max: number): string {
136
+ if (text.length <= max) return text;
137
+ return text.slice(0, max - 1) + "\u2026";
138
+ }
139
+
140
+ /** Collect all content blocks (diffs, text, etc.) from a tool update. */
141
+ export function collectContent(u: SessionUpdate): ToolCallContent[] {
142
+ const out: ToolCallContent[] = [];
143
+ if (Array.isArray(u.content_blocks)) out.push(...u.content_blocks);
144
+ const content = (u as unknown as { content?: unknown }).content;
145
+ if (Array.isArray(content)) out.push(...(content as ToolCallContent[]));
146
+ return out;
147
+ }
148
+
149
+ /** Collect every file path referenced by a tool call. */
150
+ export function gatherPaths(u: SessionUpdate, raw: Record<string, unknown>): string[] {
151
+ const out: string[] = [];
152
+ const add = (v: unknown): void => {
153
+ if (typeof v === "string" && v) out.push(v);
154
+ };
155
+ add(raw.path);
156
+ add(raw.file_path);
157
+ add(raw.filename);
158
+ add(raw.file);
159
+ if (Array.isArray(raw.operations)) {
160
+ for (const op of raw.operations) {
161
+ if (op && typeof op === "object") add((op as Record<string, unknown>).path);
162
+ }
163
+ }
164
+ for (const b of collectContent(u)) add(b.path);
165
+ return out;
166
+ }
167
+
168
+ function strOf(v: unknown): string {
169
+ return typeof v === "string" ? v : "";
170
+ }
@@ -1,19 +1,41 @@
1
- /**
1
+ /**
2
2
  * Format ACP tool-call updates into clear, RAW markdown blocks so they read
3
- * distinctly from the agent's prose and thinking. Commands appear in a `bash`
4
- * block, file edits as a `diff` block.
3
+ * distinctly from the agent's prose and thinking. Each tool kind gets its own
4
+ * rich detail: commands in bash blocks, diffs in diff blocks, search queries,
5
+ * file paths, URLs, content previews, move/rename source+dest, delete paths.
5
6
  */
6
- import type { SessionUpdate, ToolCallContent } from "../grok/types.js";
7
+ import type { SessionUpdate } from "../grok/types.js";
7
8
  import { renderUnifiedDiff } from "./diff.js";
9
+ import {
10
+ normalizeKind,
11
+ extractPath,
12
+ extractDestPath,
13
+ extractSearchQuery,
14
+ extractSearchPath,
15
+ extractUrl,
16
+ extractCommand,
17
+ extractContent,
18
+ extractFilters,
19
+ truncate,
20
+ collectContent,
21
+ gatherPaths,
22
+ PREVIEW_MAX,
23
+ CONTENT_PREVIEW_MAX,
24
+ } from "./tool-call-detail.js";
8
25
 
9
26
  const KIND_ICON: Record<string, string> = {
10
27
  read: "\u{1F4D6}",
11
28
  edit: "\u270F\uFE0F",
29
+ write: "\u{1F4DD}",
30
+ create: "\u{1F4DD}",
12
31
  execute: "\u{1F4BB}",
13
32
  search: "\u{1F50E}",
14
33
  delete: "\u{1F5D1}\uFE0F",
15
34
  move: "\u{1F4E6}",
35
+ rename: "\u{1F4E6}",
16
36
  fetch: "\u{1F310}",
37
+ web_search: "\u{1F310}",
38
+ web_fetch: "\u{1F310}",
17
39
  think: "\u{1F4AD}",
18
40
  other: "\u{1F527}",
19
41
  };
@@ -32,77 +54,275 @@ export interface ToolFormatOptions {
32
54
 
33
55
  /** Returns a RAW markdown block describing the tool call, or "" to skip. */
34
56
  export function formatToolCall(u: SessionUpdate, opts: ToolFormatOptions): string {
35
- const kind = (u.kind || "other").toLowerCase();
57
+ const kind = normalizeKind(u.kind);
36
58
  const raw = (u.rawInput || {}) as Record<string, unknown>;
37
59
  const status = u.status ? (STATUS_ICON[u.status] ?? "") : "";
38
- const tail = status ? ` ${status}` : "";
60
+ const tail = status ? " " + status : "";
39
61
 
40
- // Skill load — reading a `.../skills/<name>/SKILL.md`. Don't treat edits/
41
- // deletes of a SKILL.md (skill authoring) as a "load".
42
- if (kind !== "edit" && kind !== "delete" && kind !== "move") {
62
+ // Skill load
63
+ if (kind !== "edit" && kind !== "delete" && kind !== "move" && kind !== "write" && kind !== "create") {
43
64
  const skill = detectSkill(u, raw);
44
- if (skill) return `\u{1F4DA} **Loaded skill: ${skill}**${tail}`;
65
+ if (skill) return "\u{1F4DA} **Loaded skill: " + skill + "**" + tail;
45
66
  }
46
67
 
47
- // MCP / extension tool call → "Call MCP <server>: <method>" (or "Call MCP:
48
- // <tool>" when the call carries no server name).
68
+ // MCP / extension tool call
49
69
  const mcp = detectMcp(u, raw, kind);
50
70
  if (mcp) {
51
- const label = mcp.server ? `Call MCP ${mcp.server}: ${mcp.method}` : `Call MCP: ${mcp.method}`;
52
- return `\u{1F9E9} **${label}**${tail}`;
71
+ const label = mcp.server ? "Call MCP " + mcp.server + ": " + mcp.method : "Call MCP: " + mcp.method;
72
+ let out = "\u{1F9E9} **" + label + "**" + tail;
73
+ const argPreview = mcpArgPreview(raw);
74
+ if (argPreview) out += "\n" + fence(argPreview) + "\n";
75
+ return out;
53
76
  }
54
77
 
55
- const icon = KIND_ICON[kind] ?? KIND_ICON.other;
56
- const title = u.title || titleFromRaw(kind, raw);
78
+ switch (kind) {
79
+ case "execute":
80
+ return formatExecute(raw, tail);
81
+ case "edit":
82
+ return formatEdit(u, raw, tail, opts);
83
+ case "write":
84
+ case "create":
85
+ return formatWrite(kind, raw, tail);
86
+ case "read":
87
+ return formatRead(raw, tail);
88
+ case "search":
89
+ return formatSearch(raw, tail);
90
+ case "delete":
91
+ return formatDelete(raw, tail);
92
+ case "move":
93
+ case "rename":
94
+ return formatMove(kind, raw, tail);
95
+ case "fetch":
96
+ case "web_fetch":
97
+ return formatFetch(raw, tail);
98
+ case "web_search":
99
+ return formatWebSearch(raw, tail);
100
+ default:
101
+ return formatGeneric(u, raw, tail, kind);
102
+ }
103
+ }
57
104
 
58
- let out = `${icon} **${title}**${tail}`;
105
+ // ---- helpers for code fences (avoid backtick-in-template-literal issues) ----
59
106
 
60
- if (kind === "execute") {
61
- const cmd = strOf(raw.command ?? raw.cmd);
62
- if (cmd) out += "\n```bash\n" + cmd + "\n```";
63
- }
107
+ /** Wrap text in a fenced code block with optional language. */
108
+ function fence(text: string, lang?: string): string {
109
+ const marker = "```";
110
+ return marker + (lang || "") + "\n" + text + "\n" + marker;
111
+ }
112
+
113
+ // ---- per-kind formatters ----
64
114
 
65
- if (kind === "edit" && opts.showDiffs) {
115
+ function formatExecute(raw: Record<string, unknown>, tail: string): string {
116
+ const cmd = extractCommand(raw);
117
+ const cwd = strOf(raw.cwd);
118
+ const title = "Run command" + (cwd ? " in " + truncate(cwd, 80) : "");
119
+ let out = "\u{1F4BB} **" + title + "**" + tail;
120
+ if (cmd) out += "\n" + fence(truncate(cmd, PREVIEW_MAX), "bash") + "\n";
121
+ return out;
122
+ }
123
+
124
+ function formatEdit(u: SessionUpdate, raw: Record<string, unknown>, tail: string, opts: ToolFormatOptions): string {
125
+ const path = extractPath(raw);
126
+ const title = "Edit " + (path || "file");
127
+ let out = "\u270F\uFE0F **" + title + "**" + tail;
128
+ if (opts.showDiffs) {
66
129
  const diff = buildEditDiff(u, raw, opts.diffMaxLines);
67
130
  if (diff && diff.block) {
68
- const stat = `${diff.added > 0 ? "+" + diff.added : ""}${diff.removed > 0 ? " -" + diff.removed : ""}`.trim();
69
- out += `${stat ? ` (${stat})` : ""}\n${diff.block}`;
131
+ const stat = (diff.added > 0 ? "+" + diff.added : "") + (diff.removed > 0 ? " -" + diff.removed : "");
132
+ out += (stat.trim() ? " (" + stat.trim() + ")" : "") + "\n" + diff.block;
70
133
  }
71
134
  }
135
+ return out;
136
+ }
137
+
138
+ function formatWrite(kind: string, raw: Record<string, unknown>, tail: string): string {
139
+ const path = extractPath(raw);
140
+ const verb = kind === "create" ? "Create" : "Write";
141
+ let out = "\u{1F4DD} **" + verb + " " + (path || "file") + "**" + tail;
142
+ const content = extractContent(raw);
143
+ if (content) {
144
+ out += "\n" + fence(truncate(content, CONTENT_PREVIEW_MAX), detectLang(path)) + "\n";
145
+ }
146
+ return out;
147
+ }
72
148
 
149
+ function formatRead(raw: Record<string, unknown>, tail: string): string {
150
+ const path = extractPath(raw);
151
+ const lines = strOf(raw.start_line) || strOf(raw.line);
152
+ const offset = strOf(raw.offset);
153
+ const limit = strOf(raw.limit);
154
+ let title = "Read " + (path || "file");
155
+ const parts: string[] = [];
156
+ if (lines) parts.push("line " + lines);
157
+ if (offset) parts.push("offset " + offset);
158
+ if (limit) parts.push("limit " + limit);
159
+ if (parts.length) title += " (" + parts.join(", ") + ")";
160
+ return "\u{1F4D6} **" + title + "**" + tail;
161
+ }
162
+
163
+ function formatSearch(raw: Record<string, unknown>, tail: string): string {
164
+ const query = extractSearchQuery(raw);
165
+ const path = extractSearchPath(raw);
166
+ const filters = extractFilters(raw);
167
+ let title = "Search";
168
+ if (query) title += ": " + truncate(query, 120);
169
+ else if (path) title += " " + path;
170
+ let out = "\u{1F50E} **" + title + "**" + tail;
171
+ if (path && !query.includes(path)) out += "\n \u{1F4C2} in: " + truncate(path, 100);
172
+ if (filters.include) out += "\n \u{1F4C1} include: " + filters.include;
173
+ if (filters.exclude) out += "\n \u{1F6AB} exclude: " + filters.exclude;
174
+ if (raw.case_sensitive !== undefined)
175
+ out += "\n case-sensitive: " + (raw.case_sensitive ? "yes" : "no");
73
176
  return out;
74
177
  }
75
178
 
179
+ function formatDelete(raw: Record<string, unknown>, tail: string): string {
180
+ const path = extractPath(raw);
181
+ return "\u{1F5D1}\uFE0F **Delete " + (path || "file") + "**" + tail;
182
+ }
183
+
184
+ function formatMove(kind: string, raw: Record<string, unknown>, tail: string): string {
185
+ const src = extractPath(raw);
186
+ const dst = extractDestPath(raw);
187
+ const verb = kind === "rename" ? "Rename" : "Move";
188
+ if (src && dst) {
189
+ return "\u{1F4E6} **" + verb + "**" + tail + "\n \u{1F4C4} " + truncate(src, 100) + "\n \u27A1\uFE0F " + truncate(dst, 100);
190
+ }
191
+ return "\u{1F4E6} **" + verb + " " + (src || dst || "file") + "**" + tail;
192
+ }
193
+
194
+ function formatFetch(raw: Record<string, unknown>, tail: string): string {
195
+ const url = extractUrl(raw);
196
+ const method = strOf(raw.method) || strOf(raw.verb) || "GET";
197
+ let title = "Fetch URL";
198
+ if (url) title = "Fetch " + truncate(url, 200);
199
+ let out = "\u{1F310} **" + title + "**" + tail;
200
+ if (method && method !== "GET") out += "\n method: " + method;
201
+ const headers = raw.headers;
202
+ if (headers && typeof headers === "object") {
203
+ const hs = JSON.stringify(headers);
204
+ if (hs !== "{}") out += "\n headers: " + truncate(hs, 200);
205
+ }
206
+ const body = strOf(raw.body) || strOf(raw.data);
207
+ if (body) out += "\n body: " + truncate(body, 200);
208
+ return out;
209
+ }
210
+
211
+ function formatWebSearch(raw: Record<string, unknown>, tail: string): string {
212
+ const query = extractSearchQuery(raw) || extractUrl(raw);
213
+ const count = strOf(raw.count) || strOf(raw.num) || strOf(raw.num_results);
214
+ let title = "Web search";
215
+ if (query) title += ": " + truncate(query, 150);
216
+ let out = "\u{1F310} **" + title + "**" + tail;
217
+ if (count) out += "\n results: " + count;
218
+ return out;
219
+ }
220
+
221
+ function formatGeneric(u: SessionUpdate, raw: Record<string, unknown>, tail: string, kind: string): string {
222
+ const icon = KIND_ICON[kind] ?? KIND_ICON.other;
223
+ const path = extractPath(raw);
224
+ const title = u.title || (path ? capitalize(kind) + " " + path : capitalize(kind));
225
+ let out = icon + " **" + title + "**" + tail;
226
+ const desc = strOf(raw.description) || strOf(raw.message) || strOf(raw.prompt);
227
+ if (desc) out += "\n " + truncate(desc, 300);
228
+ return out;
229
+ }
230
+
231
+ // ---- diff building ----
232
+
233
+ function buildEditDiff(u: SessionUpdate, raw: Record<string, unknown>, maxLines: number) {
234
+ const blocks = collectContent(u);
235
+ const diffBlock = blocks.find((b) => b.type === "diff");
236
+ if (diffBlock) {
237
+ return renderUnifiedDiff({
238
+ path: strOf(diffBlock.path) || strOf(raw.path) || "file",
239
+ oldText: typeof diffBlock.oldText === "string" ? diffBlock.oldText : "",
240
+ newText: typeof diffBlock.newText === "string" ? diffBlock.newText : "",
241
+ maxLines,
242
+ });
243
+ }
244
+ const oldStr = strOf(raw.old_str) || strOf(raw.oldStr) || strOf(raw.old_string) || strOf(raw.find);
245
+ const newStr = strOf(raw.new_str) || strOf(raw.newStr) || strOf(raw.new_string) || strOf(raw.replace);
246
+ if (oldStr || newStr) {
247
+ return renderUnifiedDiff({ path: strOf(raw.path) || "file", oldText: oldStr, newText: newStr, maxLines });
248
+ }
249
+ const content = strOf(raw.file_text) || strOf(raw.content) || strOf(raw.text);
250
+ if (content) {
251
+ return renderUnifiedDiff({ path: strOf(raw.path) || "file", oldText: "", newText: content, maxLines });
252
+ }
253
+ return undefined;
254
+ }
255
+
256
+ // ---- language detection ----
257
+
258
+ function detectLang(path: string): string {
259
+ const ext = (path.split(".").pop() || "").toLowerCase();
260
+ const MAP: Record<string, string> = {
261
+ ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx",
262
+ py: "python", go: "go", rs: "rust", java: "java",
263
+ c: "c", cpp: "cpp", h: "c", hpp: "cpp",
264
+ cs: "csharp", rb: "ruby", php: "php", swift: "swift",
265
+ kt: "kotlin", scala: "scala", sh: "bash", bash: "bash",
266
+ sql: "sql", html: "html", css: "css", scss: "scss",
267
+ json: "json", yaml: "yaml", yml: "yaml", xml: "xml",
268
+ md: "markdown", toml: "toml", ini: "ini", cfg: "ini",
269
+ vue: "vue", svelte: "svelte", dart: "dart", lua: "lua",
270
+ r: "r", pl: "perl", ps1: "powershell",
271
+ };
272
+ return MAP[ext] || "";
273
+ }
274
+
275
+ // ---- MCP helpers ----
276
+
277
+ /** Compact one-line-per-key preview of an MCP call's arguments. */
278
+ function mcpArgPreview(raw: Record<string, unknown>): string {
279
+ const SKIP = new Set(["tool_name", "toolName", "name", "tool", "type", "_meta"]);
280
+ const lines: string[] = [];
281
+ for (const [key, val] of Object.entries(raw)) {
282
+ if (SKIP.has(key)) continue;
283
+ let s: string;
284
+ if (typeof val === "string") s = val;
285
+ else if (typeof val === "number" || typeof val === "boolean") s = String(val);
286
+ else {
287
+ try { s = JSON.stringify(val); } catch { s = String(val); }
288
+ }
289
+ if (s.length > 200) s = s.slice(0, 199) + "\u2026";
290
+ lines.push(key + ": " + s);
291
+ }
292
+ return truncate(lines.join("\n"), PREVIEW_MAX);
293
+ }
294
+
76
295
  /** Built-in Grok tools that must never be labelled as MCP calls. */
77
296
  const BUILTIN_TOOLS = new Set([
78
297
  "read", "write", "shell", "grep", "glob", "web_fetch", "web_search", "fs_read",
79
298
  "fs_write", "fs_replace", "fs_search", "execute_bash", "report_issue", "use_aws",
80
299
  "todo_list", "introspect", "knowledge", "thinking", "summary", "subagent",
300
+ "edit", "create", "delete", "move", "rename", "execute", "search", "fetch",
81
301
  ]);
82
302
  /** Tool kinds that are first-class file/shell operations (never MCP). */
83
- const FILE_KINDS = new Set(["read", "edit", "execute", "search", "delete", "move"]);
84
- /** `.../skills/<name>/SKILL.md` the signature of loading a skill. */
303
+ const FILE_KINDS = new Set([
304
+ "read", "edit", "execute", "search", "delete", "move", "write", "create",
305
+ "rename", "fetch", "web_fetch", "web_search",
306
+ ]);
307
+ /** `.../skills/<name>/SKILL.md` - the signature of loading a skill. */
85
308
  const SKILL_RE = /[\\/]skills[\\/]([^\\/]+)[\\/]SKILL\.md$/i;
86
- /** Namespaced MCP tool-name shapes [, server, method]. */
309
+ /** Namespaced MCP tool-name shapes - [, server, method]. */
87
310
  const MCP_NS = [
88
- /^@([a-z0-9._-]+)[/_]{1,3}(.+)$/i, // @server/method · @server___method
89
- /^([a-z0-9.-]+)___(.+)$/i, // server___method
90
- /^([a-z0-9.-]+)__(.+)$/i, // server__method
91
- /^([a-z0-9.-]+)\/(.+)$/i, // server/method
92
- /^([a-z0-9-]+)\.(.+)$/i, // server.method
311
+ /^@([a-z0-9._-]+)[/_]{1,3}(.+)$/i,
312
+ /^([a-z0-9.-]+)___(.+)$/i,
313
+ /^([a-z0-9.-]+)__(.+)$/i,
314
+ /^([a-z0-9.-]+)\/(.+)$/i,
315
+ /^([a-z0-9-]+)\.(.+)$/i,
93
316
  ];
94
317
 
95
- /** The skill name if this tool call loads a `SKILL.md`, else undefined. */
96
318
  function detectSkill(u: SessionUpdate, raw: Record<string, unknown>): string | undefined {
97
319
  for (const p of gatherPaths(u, raw)) {
98
320
  const m = SKILL_RE.exec(p);
99
- if (m) return m[1];
321
+ if (m) return m[1]!;
100
322
  }
101
323
  return undefined;
102
324
  }
103
325
 
104
- /** The MCP server + method this call targets, if it looks like an MCP/external
105
- * tool. Built-in file/shell tools return undefined. */
106
326
  function detectMcp(
107
327
  u: SessionUpdate,
108
328
  raw: Record<string, unknown>,
@@ -114,83 +334,23 @@ function detectMcp(
114
334
  const m = re.exec(name);
115
335
  if (m) return { server: m[1]!, method: m[2]! };
116
336
  }
117
- // Bare external tool: not a built-in, and not a file/shell operation.
118
337
  if (!BUILTIN_TOOLS.has(name.toLowerCase()) && !FILE_KINDS.has(kind)) {
119
338
  return { method: name };
120
339
  }
121
340
  return undefined;
122
341
  }
123
342
 
124
- /** Best-effort tool name from the raw input or a tool-name-like title. */
125
343
  function mcpToolName(u: SessionUpdate, raw: Record<string, unknown>): string {
126
344
  const explicit = strOf(raw.tool_name) || strOf(raw.toolName) || strOf(raw.name) || strOf(raw.tool);
127
345
  if (explicit) return explicit;
128
346
  const t = (u.title || "").trim();
129
- // Use the title only when it reads like a tool identifier (no spaces, not a
130
- // "file:line" read title like "SKILL.md:1").
131
347
  return /^[@a-z0-9._/-]+$/i.test(t) && !t.includes(":") ? t : "";
132
348
  }
133
349
 
134
- /** Collect every file path referenced by a tool call (incl. nested ops/diffs). */
135
- function gatherPaths(u: SessionUpdate, raw: Record<string, unknown>): string[] {
136
- const out: string[] = [];
137
- const add = (v: unknown): void => {
138
- if (typeof v === "string" && v) out.push(v);
139
- };
140
- add(raw.path);
141
- add(raw.file_path);
142
- add(raw.filename);
143
- add(raw.file);
144
- if (Array.isArray(raw.operations)) {
145
- for (const op of raw.operations) {
146
- if (op && typeof op === "object") add((op as Record<string, unknown>).path);
147
- }
148
- }
149
- for (const b of collectContent(u)) add(b.path);
150
- return out;
151
- }
152
-
153
- function buildEditDiff(u: SessionUpdate, raw: Record<string, unknown>, maxLines: number) {
154
- const blocks = collectContent(u);
155
- const diffBlock = blocks.find((b) => b.type === "diff");
156
- if (diffBlock) {
157
- return renderUnifiedDiff({
158
- path: strOf(diffBlock.path) || strOf(raw.path) || "file",
159
- oldText: typeof diffBlock.oldText === "string" ? diffBlock.oldText : "",
160
- newText: typeof diffBlock.newText === "string" ? diffBlock.newText : "",
161
- maxLines,
162
- });
163
- }
164
- const oldStr = strOf(raw.old_str ?? raw.oldStr);
165
- const newStr = strOf(raw.new_str ?? raw.newStr);
166
- if (oldStr || newStr) {
167
- return renderUnifiedDiff({ path: strOf(raw.path) || "file", oldText: oldStr, newText: newStr, maxLines });
168
- }
169
- const content = strOf(raw.file_text ?? raw.content ?? raw.text);
170
- if (content) {
171
- return renderUnifiedDiff({ path: strOf(raw.path) || "file", oldText: "", newText: content, maxLines });
172
- }
173
- return undefined;
174
- }
175
-
176
- function titleFromRaw(kind: string, raw: Record<string, unknown>): string {
177
- const path = strOf(raw.path ?? raw.file_path ?? raw.filename);
178
- if (path) return `${capitalize(kind)} ${path}`;
179
- return capitalize(kind);
180
- }
181
-
182
- function collectContent(u: SessionUpdate): ToolCallContent[] {
183
- const out: ToolCallContent[] = [];
184
- if (Array.isArray(u.content_blocks)) out.push(...u.content_blocks);
185
- const content = (u as unknown as { content?: unknown }).content;
186
- if (Array.isArray(content)) out.push(...(content as ToolCallContent[]));
187
- return out;
350
+ function capitalize(s: string): string {
351
+ return s.length ? s[0]!.toUpperCase() + s.slice(1) : s;
188
352
  }
189
353
 
190
354
  function strOf(v: unknown): string {
191
355
  return typeof v === "string" ? v : "";
192
- }
193
-
194
- function capitalize(s: string): string {
195
- return s.length ? s[0]!.toUpperCase() + s.slice(1) : s;
196
- }
356
+ }