grok-telegram-bot 2.3.1 → 2.5.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 +64 -2
- package/CHANGELOG.md +156 -1
- package/README.md +58 -15
- package/docs/GROUP.md +225 -0
- package/docs/INSTALL.md +3 -0
- package/package.json +1 -1
- package/src/app/accounts.ts +84 -0
- package/src/app/instance-lock.ts +6 -0
- package/src/app/lifetime-flag.ts +20 -0
- package/src/app/settings-store.ts +47 -8
- package/src/app/types.ts +30 -2
- package/src/app/updater.ts +38 -6
- package/src/app/usage.ts +204 -7
- package/src/bot/account-rotator.ts +10 -0
- package/src/bot/auth.ts +96 -15
- package/src/bot/bot.ts +154 -11
- package/src/bot/chat-controller.ts +82 -13
- package/src/bot/commands.ts +69 -27
- package/src/bot/complexity-gate.ts +69 -0
- package/src/bot/deps.ts +22 -0
- package/src/bot/group-memory.ts +159 -0
- package/src/bot/handlers/accounts.ts +58 -1
- package/src/bot/handlers/control.ts +85 -32
- package/src/bot/handlers/document.ts +31 -4
- package/src/bot/handlers/forum.ts +207 -0
- package/src/bot/handlers/import-session.ts +290 -0
- package/src/bot/handlers/menu.ts +102 -61
- package/src/bot/handlers/message.ts +102 -21
- package/src/bot/handlers/photo.ts +123 -16
- package/src/bot/handlers/running.ts +172 -16
- package/src/bot/handlers/session-card.ts +20 -0
- package/src/bot/handlers/sessions.ts +76 -15
- package/src/bot/handlers/usage.ts +118 -16
- package/src/bot/handlers/voice.ts +52 -7
- package/src/bot/image-return.ts +8 -5
- package/src/bot/menu/ephemeral.ts +13 -3
- package/src/bot/menu/keyboard.ts +54 -14
- package/src/bot/menu/refresh.ts +3 -1
- package/src/bot/menu/status-panel.ts +25 -6
- package/src/bot/permission-service.ts +19 -0
- package/src/bot/prompt-anchor.ts +300 -0
- package/src/bot/prompt-content.ts +7 -0
- package/src/bot/registry.ts +94 -1
- package/src/bot/scope.ts +94 -0
- package/src/bot/session-fork.ts +11 -0
- package/src/bot/session-runtime.ts +1254 -83
- package/src/bot/suggestions.ts +489 -0
- package/src/bot/telegram-actions.ts +440 -0
- package/src/bot/telegram-bots.ts +495 -0
- package/src/bot/telegram-io.ts +94 -10
- package/src/cli.ts +2 -0
- package/src/config.ts +242 -2
- package/src/forum/bind-path.ts +146 -0
- package/src/forum/manager.ts +651 -0
- package/src/forum/project-icon.ts +142 -0
- package/src/forum/thread.ts +16 -0
- package/src/forum/topic-store.ts +114 -0
- package/src/forum/types.ts +29 -0
- package/src/grok/client.ts +214 -37
- 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 +315 -30
- package/src/projects/manager.ts +16 -3
- package/src/render/chunk.ts +17 -10
- package/src/render/diff.ts +11 -2
- package/src/render/file-summary.ts +31 -1
- package/src/render/hashtags.ts +5 -1
- package/src/render/markdown.ts +293 -35
- package/src/render/plan.ts +127 -0
- package/src/render/session-comment.ts +318 -0
- package/src/render/telegram-bridge.ts +360 -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 +444 -162
- package/src/render/truncate.ts +85 -0
- package/src/service/platform.ts +44 -7
- package/src/service/windows.ts +30 -6
- package/src/sessions/history.ts +98 -0
- package/src/sessions/process.ts +7 -0
- package/src/sessions/store.ts +3 -0
- package/src/sessions/types.ts +5 -0
- package/src/stream/streamer.ts +90 -15
- package/src/tasks/runner.ts +4 -3
package/src/render/tool-call.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* Format ACP tool-call updates into clear, RAW markdown blocks so they read
|
|
3
3
|
* distinctly from the agent's prose and thinking. Each tool kind gets its own
|
|
4
4
|
* rich detail: commands in bash blocks, diffs in diff blocks, search queries,
|
|
5
|
-
* file paths, URLs, content previews,
|
|
5
|
+
* file paths (with offset/limit), URLs, content previews, MCP args.
|
|
6
|
+
*
|
|
7
|
+
* Grok often reports kind "other" with title/name `read_file` / `grep` / etc.
|
|
8
|
+
* {@link resolveToolIdentity} maps those to real display kinds so users never
|
|
9
|
+
* see a bare "Other" or a false "Call MCP: read_file".
|
|
6
10
|
*/
|
|
7
11
|
import type { SessionUpdate } from "../grok/types.js";
|
|
8
12
|
import { renderUnifiedDiff } from "./diff.js";
|
|
9
13
|
import {
|
|
10
|
-
|
|
14
|
+
resolveToolIdentity,
|
|
11
15
|
extractPath,
|
|
12
16
|
extractDestPath,
|
|
13
17
|
extractSearchQuery,
|
|
@@ -16,28 +20,40 @@ import {
|
|
|
16
20
|
extractCommand,
|
|
17
21
|
extractContent,
|
|
18
22
|
extractFilters,
|
|
23
|
+
extractReadRange,
|
|
24
|
+
extractToolOutput,
|
|
25
|
+
extractToolName,
|
|
26
|
+
formatArgLines,
|
|
19
27
|
truncate,
|
|
20
28
|
collectContent,
|
|
21
29
|
gatherPaths,
|
|
22
30
|
PREVIEW_MAX,
|
|
23
31
|
CONTENT_PREVIEW_MAX,
|
|
32
|
+
OUTPUT_PREVIEW_MAX,
|
|
33
|
+
OUTPUT_PREVIEW_LINES,
|
|
34
|
+
type ToolDisplayKind,
|
|
24
35
|
} from "./tool-call-detail.js";
|
|
36
|
+
import { formatLiveTerminalOutput, truncateMiddle, truncateMiddleLines } from "./truncate.js";
|
|
25
37
|
|
|
26
38
|
const KIND_ICON: Record<string, string> = {
|
|
27
|
-
read: "\u{1F4D6}",
|
|
28
|
-
edit: "\u270F\uFE0F",
|
|
29
|
-
write: "\u{1F4DD}",
|
|
39
|
+
read: "\u{1F4D6}", // 📖
|
|
40
|
+
edit: "\u270F\uFE0F", // ✏️
|
|
41
|
+
write: "\u{1F4DD}", // 📝
|
|
30
42
|
create: "\u{1F4DD}",
|
|
31
|
-
execute: "\u{1F4BB}",
|
|
32
|
-
search: "\u{1F50E}",
|
|
33
|
-
delete: "\u{1F5D1}\uFE0F",
|
|
34
|
-
move: "\u{1F4E6}",
|
|
43
|
+
execute: "\u{1F4BB}", // 💻
|
|
44
|
+
search: "\u{1F50E}", // 🔎
|
|
45
|
+
delete: "\u{1F5D1}\uFE0F", // 🗑️
|
|
46
|
+
move: "\u{1F4E6}", // 📦
|
|
35
47
|
rename: "\u{1F4E6}",
|
|
36
|
-
fetch: "\u{1F310}",
|
|
48
|
+
fetch: "\u{1F310}", // 🌐
|
|
37
49
|
web_search: "\u{1F310}",
|
|
38
50
|
web_fetch: "\u{1F310}",
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
list: "\u{1F4C1}", // 📁
|
|
52
|
+
todo: "\u2705", // ✅
|
|
53
|
+
think: "\u{1F4AD}", // 💭
|
|
54
|
+
image: "\u{1F5BC}\uFE0F", // 🖼️
|
|
55
|
+
mcp: "\u{1F9E9}", // 🧩
|
|
56
|
+
other: "\u{1F527}", // 🔧
|
|
41
57
|
};
|
|
42
58
|
|
|
43
59
|
const STATUS_ICON: Record<string, string> = {
|
|
@@ -54,39 +70,74 @@ export interface ToolFormatOptions {
|
|
|
54
70
|
|
|
55
71
|
/** Returns a RAW markdown block describing the tool call, or "" to skip. */
|
|
56
72
|
export function formatToolCall(u: SessionUpdate, opts: ToolFormatOptions): string {
|
|
57
|
-
const
|
|
58
|
-
|
|
73
|
+
const raw = flattenRawInput(u);
|
|
74
|
+
// Inject path from ACP locations when rawInput is thin.
|
|
75
|
+
if (u.locations?.[0]?.path && !raw.path && !raw.target_file && !raw.file_path) {
|
|
76
|
+
raw.path = u.locations[0].path;
|
|
77
|
+
if (u.locations[0].line != null && raw.start_line == null) raw.start_line = u.locations[0].line;
|
|
78
|
+
}
|
|
79
|
+
const id = resolveToolIdentity(u, raw);
|
|
59
80
|
const status = u.status ? (STATUS_ICON[u.status] ?? "") : "";
|
|
60
81
|
const tail = status ? " " + status : "";
|
|
61
82
|
|
|
62
|
-
//
|
|
63
|
-
|
|
83
|
+
// Plan mode enter/exit — surface clearly (exit failures used to look opaque).
|
|
84
|
+
const planCard = formatPlanModeTool(u, raw, id.toolName, tail);
|
|
85
|
+
if (planCard) return planCard;
|
|
86
|
+
|
|
87
|
+
// Skill load (SKILL.md path) — not a normal edit.
|
|
88
|
+
if (id.kind !== "edit" && id.kind !== "delete" && id.kind !== "move" && id.kind !== "write" && id.kind !== "create") {
|
|
64
89
|
const skill = detectSkill(u, raw);
|
|
65
90
|
if (skill) return "\u{1F4DA} **Loaded skill: " + skill + "**" + tail;
|
|
66
91
|
}
|
|
67
92
|
|
|
68
|
-
// MCP
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
93
|
+
// Real MCP (namespaced) — badge + rich method-specific detail when possible.
|
|
94
|
+
if (id.isMcp) {
|
|
95
|
+
const label = id.mcpServer
|
|
96
|
+
? `\u{1F9E9} **MCP ${id.mcpServer} \u00B7 ${id.mcpMethod || id.toolName}**${tail}`
|
|
97
|
+
: `\u{1F9E9} **MCP: ${id.mcpMethod || id.toolName}**${tail}`;
|
|
98
|
+
// Prefer rich formatter for known methods (read/search/…); keep MCP header.
|
|
99
|
+
if (id.kind !== "mcp" && id.kind !== "other") {
|
|
100
|
+
const body = formatByKind(id.kind, u, raw, "", opts, id.mcpMethod || id.toolName);
|
|
101
|
+
return label + "\n" + body;
|
|
102
|
+
}
|
|
103
|
+
let out = label;
|
|
104
|
+
const args = formatArgLines(raw);
|
|
105
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
106
|
+
const result = extractToolOutput(u);
|
|
107
|
+
if (result) {
|
|
108
|
+
out +=
|
|
109
|
+
"\n**Output:**\n" +
|
|
110
|
+
fence(truncateMiddleLines(result, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) +
|
|
111
|
+
"\n";
|
|
112
|
+
}
|
|
75
113
|
return out;
|
|
76
114
|
}
|
|
77
115
|
|
|
116
|
+
return formatByKind(id.kind, u, raw, tail, opts, id.toolName);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatByKind(
|
|
120
|
+
kind: ToolDisplayKind | string,
|
|
121
|
+
u: SessionUpdate,
|
|
122
|
+
raw: Record<string, unknown>,
|
|
123
|
+
tail: string,
|
|
124
|
+
opts: ToolFormatOptions,
|
|
125
|
+
toolName: string,
|
|
126
|
+
): string {
|
|
78
127
|
switch (kind) {
|
|
79
128
|
case "execute":
|
|
80
|
-
return formatExecute(raw, tail);
|
|
129
|
+
return formatExecute(u, raw, tail, toolName);
|
|
81
130
|
case "edit":
|
|
82
|
-
return formatEdit(u, raw, tail, opts);
|
|
131
|
+
return formatEdit(u, raw, tail, opts, toolName);
|
|
83
132
|
case "write":
|
|
84
133
|
case "create":
|
|
85
|
-
return formatWrite(kind, raw, tail);
|
|
134
|
+
return formatWrite(u, kind, raw, tail);
|
|
86
135
|
case "read":
|
|
87
|
-
return formatRead(raw, tail);
|
|
136
|
+
return formatRead(u, raw, tail, toolName);
|
|
137
|
+
case "list":
|
|
138
|
+
return formatList(u, raw, tail, toolName);
|
|
88
139
|
case "search":
|
|
89
|
-
return formatSearch(raw, tail);
|
|
140
|
+
return formatSearch(u, raw, tail, toolName);
|
|
90
141
|
case "delete":
|
|
91
142
|
return formatDelete(raw, tail);
|
|
92
143
|
case "move":
|
|
@@ -94,37 +145,106 @@ export function formatToolCall(u: SessionUpdate, opts: ToolFormatOptions): strin
|
|
|
94
145
|
return formatMove(kind, raw, tail);
|
|
95
146
|
case "fetch":
|
|
96
147
|
case "web_fetch":
|
|
97
|
-
return formatFetch(raw, tail);
|
|
148
|
+
return formatFetch(u, raw, tail);
|
|
98
149
|
case "web_search":
|
|
99
|
-
return formatWebSearch(raw, tail);
|
|
150
|
+
return formatWebSearch(u, raw, tail);
|
|
151
|
+
case "todo":
|
|
152
|
+
return formatTodo(raw, tail, toolName);
|
|
153
|
+
case "think":
|
|
154
|
+
return formatThink(raw, tail, toolName, u);
|
|
155
|
+
case "image":
|
|
156
|
+
return formatImage(raw, tail, toolName);
|
|
100
157
|
default:
|
|
101
|
-
return formatGeneric(u, raw, tail, kind);
|
|
158
|
+
return formatGeneric(u, raw, tail, kind, toolName);
|
|
102
159
|
}
|
|
103
160
|
}
|
|
104
161
|
|
|
105
|
-
// ---- helpers for code fences
|
|
162
|
+
// ---- helpers for code fences / path labels ----
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* File paths in **bold** break Telegram MarkdownV2 (Windows `\`, dots, long
|
|
166
|
+
* session paths). Always put paths in inline code; keep only the verb bold.
|
|
167
|
+
* ✏️ **Edit** `C:\Users\…\plan.md`
|
|
168
|
+
*/
|
|
169
|
+
function pathCode(path: string | undefined | null, fallback = "file"): string {
|
|
170
|
+
const p = (path || "").trim() || fallback;
|
|
171
|
+
// Inline code cannot contain raw backticks unescaped in our MD pipeline;
|
|
172
|
+
// replace rare ` in paths so the span stays closed.
|
|
173
|
+
return "`" + p.replace(/`/g, "'") + "`";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** `**Edit** \`path\`` style header fragment (no leading emoji). */
|
|
177
|
+
function boldVerbPath(verb: string, path: string | undefined | null, fallback = "file"): string {
|
|
178
|
+
return "**" + verb + "** " + pathCode(path, fallback);
|
|
179
|
+
}
|
|
106
180
|
|
|
107
|
-
/**
|
|
181
|
+
/** Fence that lengthens itself when the body contains backticks (avoids MD break). */
|
|
108
182
|
function fence(text: string, lang?: string): string {
|
|
109
|
-
|
|
183
|
+
let tickLen = 3;
|
|
184
|
+
const runs = text.match(/`+/g);
|
|
185
|
+
if (runs) {
|
|
186
|
+
const max = Math.max(...runs.map((r) => r.length));
|
|
187
|
+
if (max >= tickLen) tickLen = max + 1;
|
|
188
|
+
}
|
|
189
|
+
const marker = "`".repeat(tickLen);
|
|
110
190
|
return marker + (lang || "") + "\n" + text + "\n" + marker;
|
|
111
191
|
}
|
|
112
192
|
|
|
193
|
+
/** Merge rawInput with nested arguments/input objects Grok sometimes uses. */
|
|
194
|
+
function flattenRawInput(u: SessionUpdate): Record<string, unknown> {
|
|
195
|
+
const raw = { ...((u.rawInput || {}) as Record<string, unknown>) };
|
|
196
|
+
for (const key of ["arguments", "input", "args", "parameters"]) {
|
|
197
|
+
const nested = raw[key];
|
|
198
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
199
|
+
for (const [k, v] of Object.entries(nested as Record<string, unknown>)) {
|
|
200
|
+
if (raw[k] === undefined) raw[k] = v;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return raw;
|
|
205
|
+
}
|
|
206
|
+
|
|
113
207
|
// ---- per-kind formatters ----
|
|
114
208
|
|
|
115
|
-
function formatExecute(
|
|
209
|
+
function formatExecute(
|
|
210
|
+
u: SessionUpdate,
|
|
211
|
+
raw: Record<string, unknown>,
|
|
212
|
+
tail: string,
|
|
213
|
+
toolName: string,
|
|
214
|
+
): string {
|
|
116
215
|
const cmd = extractCommand(raw);
|
|
117
|
-
const cwd = strOf(raw.cwd);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
216
|
+
const cwd = strOf(raw.cwd) || strOf(raw.working_directory) || strOf(raw.workingDirectory);
|
|
217
|
+
let out = "\u{1F4BB} **Run command**" + tail;
|
|
218
|
+
if (cwd) out += " in " + pathCode(truncate(cwd, 80));
|
|
219
|
+
if (toolName && toolName !== "execute" && toolName !== "shell" && toolName !== "bash") {
|
|
220
|
+
out += `\n tool: \`${toolName}\``;
|
|
221
|
+
}
|
|
222
|
+
if (cmd) out += "\n" + fence(truncateMiddle(cmd, PREVIEW_MAX), "bash") + "\n";
|
|
223
|
+
else {
|
|
224
|
+
// No command field — dump args so the user still sees what ran.
|
|
225
|
+
const args = formatArgLines(raw);
|
|
226
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
227
|
+
}
|
|
228
|
+
// Display: first output line + live tail (one block, updated in place via upsert).
|
|
229
|
+
// Full stdout remains in the agent session / merged tool snapshot.
|
|
230
|
+
const result = extractToolOutput(u);
|
|
231
|
+
if (result) {
|
|
232
|
+
const live = formatLiveTerminalOutput(result, 12, OUTPUT_PREVIEW_MAX);
|
|
233
|
+
out += "\n**Output:**\n" + fence(live) + "\n";
|
|
234
|
+
}
|
|
121
235
|
return out;
|
|
122
236
|
}
|
|
123
237
|
|
|
124
|
-
function formatEdit(
|
|
238
|
+
function formatEdit(
|
|
239
|
+
u: SessionUpdate,
|
|
240
|
+
raw: Record<string, unknown>,
|
|
241
|
+
tail: string,
|
|
242
|
+
opts: ToolFormatOptions,
|
|
243
|
+
toolName: string,
|
|
244
|
+
): string {
|
|
125
245
|
const path = extractPath(raw);
|
|
126
|
-
|
|
127
|
-
|
|
246
|
+
let out = "\u270F\uFE0F " + boldVerbPath("Edit", path) + tail;
|
|
247
|
+
if (toolName && !/edit|replace/i.test(toolName)) out += `\n tool: \`${toolName}\``;
|
|
128
248
|
if (opts.showDiffs) {
|
|
129
249
|
const diff = buildEditDiff(u, raw, opts.diffMaxLines);
|
|
130
250
|
if (diff && diff.block) {
|
|
@@ -135,50 +255,105 @@ function formatEdit(u: SessionUpdate, raw: Record<string, unknown>, tail: string
|
|
|
135
255
|
return out;
|
|
136
256
|
}
|
|
137
257
|
|
|
138
|
-
function formatWrite(kind: string, raw: Record<string, unknown>, tail: string): string {
|
|
258
|
+
function formatWrite(u: SessionUpdate, kind: string, raw: Record<string, unknown>, tail: string): string {
|
|
139
259
|
const path = extractPath(raw);
|
|
140
260
|
const verb = kind === "create" ? "Create" : "Write";
|
|
141
|
-
let out = "\u{1F4DD}
|
|
142
|
-
const content = extractContent(raw);
|
|
261
|
+
let out = "\u{1F4DD} " + boldVerbPath(verb, path) + tail;
|
|
262
|
+
const content = extractContent(raw) || extractToolOutput(u);
|
|
143
263
|
if (content) {
|
|
144
|
-
out += "\n" + fence(
|
|
264
|
+
out += "\n" + fence(truncateMiddleLines(content, OUTPUT_PREVIEW_LINES, CONTENT_PREVIEW_MAX), detectLang(path)) + "\n";
|
|
145
265
|
}
|
|
146
266
|
return out;
|
|
147
267
|
}
|
|
148
268
|
|
|
149
|
-
function formatRead(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
269
|
+
function formatRead(
|
|
270
|
+
u: SessionUpdate,
|
|
271
|
+
raw: Record<string, unknown>,
|
|
272
|
+
tail: string,
|
|
273
|
+
toolName: string,
|
|
274
|
+
): string {
|
|
275
|
+
const path = extractPath(raw, u);
|
|
276
|
+
const range = extractReadRange(raw);
|
|
155
277
|
const parts: string[] = [];
|
|
156
|
-
if (
|
|
157
|
-
if (offset) parts.push("offset " + offset);
|
|
158
|
-
if (limit) parts.push("limit " + limit);
|
|
159
|
-
if (
|
|
160
|
-
|
|
278
|
+
if (range.startLine) parts.push("line " + range.startLine);
|
|
279
|
+
if (range.offset) parts.push("offset " + range.offset);
|
|
280
|
+
if (range.limit) parts.push("limit " + range.limit);
|
|
281
|
+
if (range.pages) parts.push("pages " + range.pages);
|
|
282
|
+
let out = "\u{1F4D6} " + boldVerbPath("Read", path) + tail;
|
|
283
|
+
if (parts.length) out += " (" + parts.join(", ") + ")";
|
|
284
|
+
if (toolName && toolName !== "read" && toolName !== "read_file") {
|
|
285
|
+
out += `\n tool: \`${toolName}\``;
|
|
286
|
+
}
|
|
287
|
+
// Extra range detail lines for clarity when many fields present.
|
|
288
|
+
if (parts.length >= 2) {
|
|
289
|
+
out += "\n " + parts.join(" \u00B7 ");
|
|
290
|
+
}
|
|
291
|
+
const body = extractToolOutput(u);
|
|
292
|
+
if (body) {
|
|
293
|
+
out += "\n" + fence(truncateMiddleLines(body, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX), detectLang(path)) + "\n";
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
161
296
|
}
|
|
162
297
|
|
|
163
|
-
function
|
|
298
|
+
function formatList(
|
|
299
|
+
u: SessionUpdate,
|
|
300
|
+
raw: Record<string, unknown>,
|
|
301
|
+
tail: string,
|
|
302
|
+
toolName: string,
|
|
303
|
+
): string {
|
|
304
|
+
const path =
|
|
305
|
+
extractPath(raw) ||
|
|
306
|
+
extractSearchPath(raw) ||
|
|
307
|
+
strOf(raw.target_directory) ||
|
|
308
|
+
strOf(raw.targetDirectory) ||
|
|
309
|
+
".";
|
|
310
|
+
let out = "\u{1F4C1} " + boldVerbPath("List", truncate(path, 120), ".") + tail;
|
|
311
|
+
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
312
|
+
const filters = extractFilters(raw);
|
|
313
|
+
if (filters.include) out += "\n include: `" + filters.include.replace(/`/g, "'") + "`";
|
|
314
|
+
if (filters.exclude) out += "\n exclude: `" + filters.exclude.replace(/`/g, "'") + "`";
|
|
315
|
+
const body = extractToolOutput(u);
|
|
316
|
+
if (body) {
|
|
317
|
+
out += "\n" + fence(truncateMiddleLines(body, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function formatSearch(
|
|
323
|
+
u: SessionUpdate,
|
|
324
|
+
raw: Record<string, unknown>,
|
|
325
|
+
tail: string,
|
|
326
|
+
toolName: string,
|
|
327
|
+
): string {
|
|
164
328
|
const query = extractSearchQuery(raw);
|
|
165
329
|
const path = extractSearchPath(raw);
|
|
166
330
|
const filters = extractFilters(raw);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (path
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
331
|
+
const isGlob = /glob/i.test(toolName) || (!!filters.include && !query);
|
|
332
|
+
const verb = isGlob ? "Glob" : "Search";
|
|
333
|
+
let out = "\u{1F50E} **" + verb + "**" + tail;
|
|
334
|
+
if (query) out += " " + pathCode(truncate(query, 120), "query");
|
|
335
|
+
else if (path) out += " " + pathCode(truncate(path, 120));
|
|
336
|
+
if (toolName && toolName !== "search" && toolName !== "grep") {
|
|
337
|
+
out += `\n tool: \`${toolName}\``;
|
|
338
|
+
}
|
|
339
|
+
if (path && query) out += "\n \u{1F4C2} in: " + pathCode(truncate(path, 100));
|
|
340
|
+
if (filters.include) out += "\n \u{1F4C1} include: " + pathCode(filters.include);
|
|
341
|
+
if (filters.exclude) out += "\n \u{1F6AB} exclude: " + pathCode(filters.exclude);
|
|
342
|
+
if (raw.case_sensitive !== undefined) {
|
|
175
343
|
out += "\n case-sensitive: " + (raw.case_sensitive ? "yes" : "no");
|
|
344
|
+
}
|
|
345
|
+
if (raw.multiline !== undefined) out += "\n multiline: " + (raw.multiline ? "yes" : "no");
|
|
346
|
+
if (raw.type) out += "\n type: " + String(raw.type);
|
|
347
|
+
const hits = extractToolOutput(u);
|
|
348
|
+
if (hits) {
|
|
349
|
+
out += "\n" + fence(truncateMiddleLines(hits, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
350
|
+
}
|
|
176
351
|
return out;
|
|
177
352
|
}
|
|
178
353
|
|
|
179
354
|
function formatDelete(raw: Record<string, unknown>, tail: string): string {
|
|
180
355
|
const path = extractPath(raw);
|
|
181
|
-
return "\u{1F5D1}\uFE0F
|
|
356
|
+
return "\u{1F5D1}\uFE0F " + boldVerbPath("Delete", path) + tail;
|
|
182
357
|
}
|
|
183
358
|
|
|
184
359
|
function formatMove(kind: string, raw: Record<string, unknown>, tail: string): string {
|
|
@@ -186,17 +361,26 @@ function formatMove(kind: string, raw: Record<string, unknown>, tail: string): s
|
|
|
186
361
|
const dst = extractDestPath(raw);
|
|
187
362
|
const verb = kind === "rename" ? "Rename" : "Move";
|
|
188
363
|
if (src && dst) {
|
|
189
|
-
return
|
|
364
|
+
return (
|
|
365
|
+
"\u{1F4E6} **" +
|
|
366
|
+
verb +
|
|
367
|
+
"**" +
|
|
368
|
+
tail +
|
|
369
|
+
"\n \u{1F4C4} " +
|
|
370
|
+
pathCode(truncate(src, 100)) +
|
|
371
|
+
"\n \u27A1\uFE0F " +
|
|
372
|
+
pathCode(truncate(dst, 100))
|
|
373
|
+
);
|
|
190
374
|
}
|
|
191
|
-
return "\u{1F4E6}
|
|
375
|
+
return "\u{1F4E6} " + boldVerbPath(verb, src || dst) + tail;
|
|
192
376
|
}
|
|
193
377
|
|
|
194
|
-
function formatFetch(raw: Record<string, unknown>, tail: string): string {
|
|
378
|
+
function formatFetch(u: SessionUpdate, raw: Record<string, unknown>, tail: string): string {
|
|
195
379
|
const url = extractUrl(raw);
|
|
196
380
|
const method = strOf(raw.method) || strOf(raw.verb) || "GET";
|
|
197
|
-
let
|
|
198
|
-
|
|
199
|
-
|
|
381
|
+
let out = url
|
|
382
|
+
? "\u{1F310} **Fetch** " + pathCode(truncate(url, 200), "URL") + tail
|
|
383
|
+
: "\u{1F310} **Fetch URL**" + tail;
|
|
200
384
|
if (method && method !== "GET") out += "\n method: " + method;
|
|
201
385
|
const headers = raw.headers;
|
|
202
386
|
if (headers && typeof headers === "object") {
|
|
@@ -205,26 +389,146 @@ function formatFetch(raw: Record<string, unknown>, tail: string): string {
|
|
|
205
389
|
}
|
|
206
390
|
const body = strOf(raw.body) || strOf(raw.data);
|
|
207
391
|
if (body) out += "\n body: " + truncate(body, 200);
|
|
392
|
+
const result = extractToolOutput(u);
|
|
393
|
+
if (result) {
|
|
394
|
+
out += "\n" + fence(truncateMiddleLines(result, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
395
|
+
}
|
|
208
396
|
return out;
|
|
209
397
|
}
|
|
210
398
|
|
|
211
|
-
function formatWebSearch(raw: Record<string, unknown>, tail: string): string {
|
|
399
|
+
function formatWebSearch(u: SessionUpdate, raw: Record<string, unknown>, tail: string): string {
|
|
212
400
|
const query = extractSearchQuery(raw) || extractUrl(raw);
|
|
213
|
-
const count = strOf(raw.count) || strOf(raw.num) || strOf(raw.num_results);
|
|
214
|
-
let
|
|
215
|
-
if (query)
|
|
216
|
-
let out = "\u{1F310} **" + title + "**" + tail;
|
|
401
|
+
const count = strOf(raw.count) || strOf(raw.num) || strOf(raw.num_results) || numStr(raw.num_results);
|
|
402
|
+
let out = "\u{1F310} **Web search**" + tail;
|
|
403
|
+
if (query) out += " " + pathCode(truncate(query, 150), "query");
|
|
217
404
|
if (count) out += "\n results: " + count;
|
|
405
|
+
const result = extractToolOutput(u);
|
|
406
|
+
if (result) {
|
|
407
|
+
out += "\n" + fence(truncateMiddleLines(result, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function formatTodo(raw: Record<string, unknown>, tail: string, toolName: string): string {
|
|
413
|
+
let out = "\u2705 **Todos**" + tail;
|
|
414
|
+
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
415
|
+
const args = formatArgLines(raw);
|
|
416
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function formatThink(
|
|
421
|
+
raw: Record<string, unknown>,
|
|
422
|
+
tail: string,
|
|
423
|
+
toolName: string,
|
|
424
|
+
u: SessionUpdate,
|
|
425
|
+
): string {
|
|
426
|
+
const desc =
|
|
427
|
+
strOf(raw.description) ||
|
|
428
|
+
strOf(raw.prompt) ||
|
|
429
|
+
strOf(raw.message) ||
|
|
430
|
+
strOf(raw.task) ||
|
|
431
|
+
u.title ||
|
|
432
|
+
toolName ||
|
|
433
|
+
"subagent";
|
|
434
|
+
let out = "\u{1F4AD} **Delegate / think**" + tail + "\n " + truncate(desc, 300);
|
|
435
|
+
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
436
|
+
const args = formatArgLines(raw);
|
|
437
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function formatImage(raw: Record<string, unknown>, tail: string, toolName: string): string {
|
|
442
|
+
const path = extractPath(raw) || strOf(raw.image) || strOf(raw.output);
|
|
443
|
+
let out = "\u{1F5BC}\uFE0F **Image**" + tail;
|
|
444
|
+
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
445
|
+
if (path) out += "\n " + pathCode(truncate(path, 200));
|
|
446
|
+
const prompt = strOf(raw.prompt);
|
|
447
|
+
if (prompt) out += "\n prompt: " + truncate(prompt, 200);
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Enter/exit plan mode tools — show status + failure reason (e.g. client
|
|
453
|
+
* disconnected when the bridge did not answer the plan-approval reverse-request).
|
|
454
|
+
*/
|
|
455
|
+
function formatPlanModeTool(
|
|
456
|
+
u: SessionUpdate,
|
|
457
|
+
raw: Record<string, unknown>,
|
|
458
|
+
toolName: string,
|
|
459
|
+
tail: string,
|
|
460
|
+
): string | undefined {
|
|
461
|
+
const name = (toolName || u.title || "").toLowerCase();
|
|
462
|
+
const variant = String(raw.variant ?? raw.action ?? "").toLowerCase();
|
|
463
|
+
const isEnter =
|
|
464
|
+
name.includes("enter_plan") ||
|
|
465
|
+
name === "plan: enter" ||
|
|
466
|
+
variant.includes("enterplan");
|
|
467
|
+
const isExit =
|
|
468
|
+
name.includes("exit_plan") ||
|
|
469
|
+
name === "plan: exit" ||
|
|
470
|
+
variant.includes("exitplan");
|
|
471
|
+
if (!isEnter && !isExit) return undefined;
|
|
472
|
+
|
|
473
|
+
const icon = isExit ? "\u{1F4CB}" : "\u{1F4D1}";
|
|
474
|
+
const label = isExit ? "Plan: Exit" : "Plan: Enter";
|
|
475
|
+
let out = `${icon} **${label}**${tail}`;
|
|
476
|
+
if (variant) out += `\n variant: \`${String(raw.variant ?? raw.action)}\``;
|
|
477
|
+
const result = extractToolOutput(u);
|
|
478
|
+
if (result) {
|
|
479
|
+
// Prefer a short, visible failure/success reason over a huge dump.
|
|
480
|
+
const short = truncateMiddle(result.trim(), 500);
|
|
481
|
+
out += "\n" + fence(short) + "\n";
|
|
482
|
+
} else if ((u.status || "").toLowerCase() === "failed") {
|
|
483
|
+
out += "\n \u274C Plan approval failed (bridge should auto-approve exit).";
|
|
484
|
+
}
|
|
218
485
|
return out;
|
|
219
486
|
}
|
|
220
487
|
|
|
221
|
-
function formatGeneric(
|
|
488
|
+
function formatGeneric(
|
|
489
|
+
u: SessionUpdate,
|
|
490
|
+
raw: Record<string, unknown>,
|
|
491
|
+
tail: string,
|
|
492
|
+
kind: string,
|
|
493
|
+
toolName: string,
|
|
494
|
+
): string {
|
|
222
495
|
const icon = KIND_ICON[kind] ?? KIND_ICON.other;
|
|
223
496
|
const path = extractPath(raw);
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
497
|
+
const cmd = extractCommand(raw);
|
|
498
|
+
const query = extractSearchQuery(raw);
|
|
499
|
+
// Never show a bare "Other" / "Tool call" — prefer real tool name. Paths/queries
|
|
500
|
+
// go in inline code (not bold) so Windows paths cannot break MarkdownV2.
|
|
501
|
+
let label =
|
|
502
|
+
(toolName && !/^tool[_ ]?call$/i.test(toolName) ? toolName : "") ||
|
|
503
|
+
(u.title && !/^other$/i.test(u.title.trim()) && !/^tool[_ ]?call$/i.test(u.title.trim())
|
|
504
|
+
? u.title.trim()
|
|
505
|
+
: "") ||
|
|
506
|
+
(path ? capitalize(kind !== "other" ? kind : "use") : "") ||
|
|
507
|
+
(cmd ? "Run command" : "") ||
|
|
508
|
+
(query ? "Search" : "") ||
|
|
509
|
+
(kind && kind !== "other" ? capitalize(kind) : "") ||
|
|
510
|
+
(toolName || "Tool");
|
|
511
|
+
if (/^other$/i.test(label) || /^tool[_ ]?call$/i.test(label)) {
|
|
512
|
+
label = toolName && !/^tool[_ ]?call$/i.test(toolName) ? toolName : "Tool";
|
|
513
|
+
}
|
|
514
|
+
// Titles that accidentally embed a path stay short: drop path from bold label.
|
|
515
|
+
if (path && label.includes(path)) {
|
|
516
|
+
label = label.replace(path, "").replace(/\s+/g, " ").trim() || capitalize(kind !== "other" ? kind : "Tool");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let out = icon + " **" + label + "**" + tail;
|
|
520
|
+
if (toolName && toolName !== label) out += `\n tool: \`${toolName}\``;
|
|
521
|
+
if (path) out += "\n \u{1F4C4} " + pathCode(truncate(path, 120));
|
|
522
|
+
if (cmd) out += "\n" + fence(truncateMiddle(cmd, PREVIEW_MAX), "bash") + "\n";
|
|
523
|
+
else if (query) out += "\n query: " + pathCode(truncate(query, 150), "query");
|
|
524
|
+
else if (!path && !cmd) {
|
|
525
|
+
const args = formatArgLines(raw);
|
|
526
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
527
|
+
}
|
|
528
|
+
const result = extractToolOutput(u);
|
|
529
|
+
if (result) {
|
|
530
|
+
out += "\n" + fence(truncateMiddleLines(result, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
531
|
+
}
|
|
228
532
|
return out;
|
|
229
533
|
}
|
|
230
534
|
|
|
@@ -235,7 +539,7 @@ function buildEditDiff(u: SessionUpdate, raw: Record<string, unknown>, maxLines:
|
|
|
235
539
|
const diffBlock = blocks.find((b) => b.type === "diff");
|
|
236
540
|
if (diffBlock) {
|
|
237
541
|
return renderUnifiedDiff({
|
|
238
|
-
path: strOf(diffBlock.path) ||
|
|
542
|
+
path: strOf(diffBlock.path) || extractPath(raw) || "file",
|
|
239
543
|
oldText: typeof diffBlock.oldText === "string" ? diffBlock.oldText : "",
|
|
240
544
|
newText: typeof diffBlock.newText === "string" ? diffBlock.newText : "",
|
|
241
545
|
maxLines,
|
|
@@ -244,11 +548,16 @@ function buildEditDiff(u: SessionUpdate, raw: Record<string, unknown>, maxLines:
|
|
|
244
548
|
const oldStr = strOf(raw.old_str) || strOf(raw.oldStr) || strOf(raw.old_string) || strOf(raw.find);
|
|
245
549
|
const newStr = strOf(raw.new_str) || strOf(raw.newStr) || strOf(raw.new_string) || strOf(raw.replace);
|
|
246
550
|
if (oldStr || newStr) {
|
|
247
|
-
return renderUnifiedDiff({
|
|
551
|
+
return renderUnifiedDiff({
|
|
552
|
+
path: extractPath(raw) || "file",
|
|
553
|
+
oldText: oldStr,
|
|
554
|
+
newText: newStr,
|
|
555
|
+
maxLines,
|
|
556
|
+
});
|
|
248
557
|
}
|
|
249
558
|
const content = strOf(raw.file_text) || strOf(raw.content) || strOf(raw.text);
|
|
250
559
|
if (content) {
|
|
251
|
-
return renderUnifiedDiff({ path:
|
|
560
|
+
return renderUnifiedDiff({ path: extractPath(raw) || "file", oldText: "", newText: content, maxLines });
|
|
252
561
|
}
|
|
253
562
|
return undefined;
|
|
254
563
|
}
|
|
@@ -258,62 +567,52 @@ function buildEditDiff(u: SessionUpdate, raw: Record<string, unknown>, maxLines:
|
|
|
258
567
|
function detectLang(path: string): string {
|
|
259
568
|
const ext = (path.split(".").pop() || "").toLowerCase();
|
|
260
569
|
const MAP: Record<string, string> = {
|
|
261
|
-
ts: "typescript",
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
570
|
+
ts: "typescript",
|
|
571
|
+
tsx: "tsx",
|
|
572
|
+
js: "javascript",
|
|
573
|
+
jsx: "jsx",
|
|
574
|
+
py: "python",
|
|
575
|
+
go: "go",
|
|
576
|
+
rs: "rust",
|
|
577
|
+
java: "java",
|
|
578
|
+
c: "c",
|
|
579
|
+
cpp: "cpp",
|
|
580
|
+
h: "c",
|
|
581
|
+
hpp: "cpp",
|
|
582
|
+
cs: "csharp",
|
|
583
|
+
rb: "ruby",
|
|
584
|
+
php: "php",
|
|
585
|
+
swift: "swift",
|
|
586
|
+
kt: "kotlin",
|
|
587
|
+
scala: "scala",
|
|
588
|
+
sh: "bash",
|
|
589
|
+
bash: "bash",
|
|
590
|
+
sql: "sql",
|
|
591
|
+
html: "html",
|
|
592
|
+
css: "css",
|
|
593
|
+
scss: "scss",
|
|
594
|
+
json: "json",
|
|
595
|
+
yaml: "yaml",
|
|
596
|
+
yml: "yaml",
|
|
597
|
+
xml: "xml",
|
|
598
|
+
md: "markdown",
|
|
599
|
+
toml: "toml",
|
|
600
|
+
ini: "ini",
|
|
601
|
+
cfg: "ini",
|
|
602
|
+
vue: "vue",
|
|
603
|
+
svelte: "svelte",
|
|
604
|
+
dart: "dart",
|
|
605
|
+
lua: "lua",
|
|
606
|
+
r: "r",
|
|
607
|
+
pl: "perl",
|
|
608
|
+
ps1: "powershell",
|
|
271
609
|
};
|
|
272
610
|
return MAP[ext] || "";
|
|
273
611
|
}
|
|
274
612
|
|
|
275
|
-
// ----
|
|
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
|
-
}
|
|
613
|
+
// ---- skill ----
|
|
294
614
|
|
|
295
|
-
/** Built-in Grok tools that must never be labelled as MCP calls. */
|
|
296
|
-
const BUILTIN_TOOLS = new Set([
|
|
297
|
-
"read", "write", "shell", "grep", "glob", "web_fetch", "web_search", "fs_read",
|
|
298
|
-
"fs_write", "fs_replace", "fs_search", "execute_bash", "report_issue", "use_aws",
|
|
299
|
-
"todo_list", "introspect", "knowledge", "thinking", "summary", "subagent",
|
|
300
|
-
"edit", "create", "delete", "move", "rename", "execute", "search", "fetch",
|
|
301
|
-
]);
|
|
302
|
-
/** Tool kinds that are first-class file/shell operations (never MCP). */
|
|
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. */
|
|
308
615
|
const SKILL_RE = /[\\/]skills[\\/]([^\\/]+)[\\/]SKILL\.md$/i;
|
|
309
|
-
/** Namespaced MCP tool-name shapes - [, server, method]. */
|
|
310
|
-
const MCP_NS = [
|
|
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,
|
|
316
|
-
];
|
|
317
616
|
|
|
318
617
|
function detectSkill(u: SessionUpdate, raw: Record<string, unknown>): string | undefined {
|
|
319
618
|
for (const p of gatherPaths(u, raw)) {
|
|
@@ -323,34 +622,17 @@ function detectSkill(u: SessionUpdate, raw: Record<string, unknown>): string | u
|
|
|
323
622
|
return undefined;
|
|
324
623
|
}
|
|
325
624
|
|
|
326
|
-
function detectMcp(
|
|
327
|
-
u: SessionUpdate,
|
|
328
|
-
raw: Record<string, unknown>,
|
|
329
|
-
kind: string,
|
|
330
|
-
): { server?: string; method: string } | undefined {
|
|
331
|
-
const name = mcpToolName(u, raw);
|
|
332
|
-
if (!name) return undefined;
|
|
333
|
-
for (const re of MCP_NS) {
|
|
334
|
-
const m = re.exec(name);
|
|
335
|
-
if (m) return { server: m[1]!, method: m[2]! };
|
|
336
|
-
}
|
|
337
|
-
if (!BUILTIN_TOOLS.has(name.toLowerCase()) && !FILE_KINDS.has(kind)) {
|
|
338
|
-
return { method: name };
|
|
339
|
-
}
|
|
340
|
-
return undefined;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function mcpToolName(u: SessionUpdate, raw: Record<string, unknown>): string {
|
|
344
|
-
const explicit = strOf(raw.tool_name) || strOf(raw.toolName) || strOf(raw.name) || strOf(raw.tool);
|
|
345
|
-
if (explicit) return explicit;
|
|
346
|
-
const t = (u.title || "").trim();
|
|
347
|
-
return /^[@a-z0-9._/-]+$/i.test(t) && !t.includes(":") ? t : "";
|
|
348
|
-
}
|
|
349
|
-
|
|
350
625
|
function capitalize(s: string): string {
|
|
351
626
|
return s.length ? s[0]!.toUpperCase() + s.slice(1) : s;
|
|
352
627
|
}
|
|
353
628
|
|
|
354
629
|
function strOf(v: unknown): string {
|
|
355
630
|
return typeof v === "string" ? v : "";
|
|
356
|
-
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function numStr(v: unknown): string {
|
|
634
|
+
return typeof v === "number" && Number.isFinite(v) ? String(v) : "";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Re-export for tests / step-from-tool helpers.
|
|
638
|
+
export { extractToolName, resolveToolIdentity };
|