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.
- package/.env.example +5 -0
- package/CHANGELOG.md +86 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/scripts/_push-branch.mjs +89 -0
- package/scripts/_release-version.mjs +87 -0
- package/src/app/accounts.ts +49 -12
- package/src/app/auth-service.ts +4 -1
- package/src/bot/account-rotator.ts +79 -52
- package/src/bot/bot.ts +4 -2
- package/src/bot/handlers/accounts.ts +22 -5
- package/src/bot/permission-service.ts +59 -15
- package/src/bot/reauth-controller.ts +3 -3
- package/src/bot/session-runtime.ts +50 -30
- package/src/config.ts +9 -0
- package/src/grok/client.ts +72 -17
- package/src/render/tool-call-detail.ts +170 -0
- package/src/render/tool-call.ts +259 -99
|
@@ -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
|
+
}
|
package/src/render/tool-call.ts
CHANGED
|
@@ -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.
|
|
4
|
-
*
|
|
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
|
|
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
|
|
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 ?
|
|
60
|
+
const tail = status ? " " + status : "";
|
|
39
61
|
|
|
40
|
-
// Skill load
|
|
41
|
-
|
|
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
|
|
65
|
+
if (skill) return "\u{1F4DA} **Loaded skill: " + skill + "**" + tail;
|
|
45
66
|
}
|
|
46
67
|
|
|
47
|
-
// MCP / extension tool call
|
|
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 ?
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
105
|
+
// ---- helpers for code fences (avoid backtick-in-template-literal issues) ----
|
|
59
106
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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 =
|
|
69
|
-
out +=
|
|
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([
|
|
84
|
-
|
|
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
|
|
309
|
+
/** Namespaced MCP tool-name shapes - [, server, method]. */
|
|
87
310
|
const MCP_NS = [
|
|
88
|
-
/^@([a-z0-9._-]+)[/_]{1,3}(.+)$/i,
|
|
89
|
-
/^([a-z0-9.-]+)___(.+)$/i,
|
|
90
|
-
/^([a-z0-9.-]+)__(.+)$/i,
|
|
91
|
-
/^([a-z0-9.-]+)\/(.+)$/i,
|
|
92
|
-
/^([a-z0-9-]+)\.(.+)$/i,
|
|
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
|
-
|
|
135
|
-
|
|
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
|
+
}
|