switchroom 0.13.65 → 0.14.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/dist/agent-scheduler/index.js +80 -80
- package/dist/auth-broker/index.js +80 -80
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/notion-write-pretool.mjs +82 -82
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +832 -637
- package/dist/host-control/main.js +148 -148
- package/dist/vault/approvals/kernel-server.js +82 -82
- package/dist/vault/broker/server.js +83 -83
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +112 -112
- package/telegram-plugin/dist/gateway/gateway.js +300 -195
- package/telegram-plugin/dist/server.js +160 -160
- package/telegram-plugin/gateway/gateway.ts +35 -22
- package/telegram-plugin/tests/tool-activity-summary.test.ts +66 -0
- package/telegram-plugin/tool-activity-summary.ts +137 -0
|
@@ -57,6 +57,7 @@ import { allocateDraftId } from '../draft-transport.js'
|
|
|
57
57
|
import {
|
|
58
58
|
makeEmptyActivityState,
|
|
59
59
|
registerAndRender,
|
|
60
|
+
describeToolUse,
|
|
60
61
|
type ActivityState,
|
|
61
62
|
} from '../tool-activity-summary.js'
|
|
62
63
|
import { toolLabel } from '../tool-labels.js'
|
|
@@ -6837,7 +6838,12 @@ async function drainActivitySummary(turn: CurrentTurn): Promise<void> {
|
|
|
6837
6838
|
while (turn.activityPendingRender !== turn.activityLastSentRender) {
|
|
6838
6839
|
const target = turn.activityPendingRender
|
|
6839
6840
|
if (target == null) break
|
|
6840
|
-
|
|
6841
|
+
// Escape before wrapping in <i> + parse_mode HTML. The legacy
|
|
6842
|
+
// verb-count summaries were safe ASCII, but the draft-mirror's
|
|
6843
|
+
// describeToolUse content (file names, Bash descriptions, search
|
|
6844
|
+
// queries) can contain <, >, & — which would break HTML parsing
|
|
6845
|
+
// and surface literal tags (the exact #1942 bug class).
|
|
6846
|
+
const html = `<i>${escapeHtmlForTg(target)}</i>`
|
|
6841
6847
|
const chat = turn.sessionChatId
|
|
6842
6848
|
const thread = turn.sessionThreadId
|
|
6843
6849
|
// sendMessageDraft doesn't support forum threads.
|
|
@@ -7130,14 +7136,21 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
7130
7136
|
// exactly once at a time and re-running until pending matches
|
|
7131
7137
|
// the last-sent. Captures `turn` so a late drain after turn-swap
|
|
7132
7138
|
// can't corrupt the next turn's atom.
|
|
7133
|
-
// DRAFT_MIRROR (RFC draft-mirror-preview
|
|
7134
|
-
//
|
|
7135
|
-
//
|
|
7136
|
-
//
|
|
7137
|
-
// the
|
|
7138
|
-
//
|
|
7139
|
-
|
|
7140
|
-
|
|
7139
|
+
// DRAFT_MIRROR (RFC draft-mirror-preview): render each tool_use as a
|
|
7140
|
+
// human-friendly line in the live preview, using the model-authored
|
|
7141
|
+
// descriptive field (Bash.description, Read/Edit file basename,
|
|
7142
|
+
// hindsight→"Searching memory", etc. — see describeToolUse). Latest
|
|
7143
|
+
// action wins (the draft shows "doing X" live), clears on reply.
|
|
7144
|
+
// Never surfaces raw shell/query syntax — option A, uniform across
|
|
7145
|
+
// code + non-code agents.
|
|
7146
|
+
//
|
|
7147
|
+
// Flag OFF (default): the legacy generic verb-count summary
|
|
7148
|
+
// ("Ran 5 commands") via registerAndRender — byte-identical to
|
|
7149
|
+
// pre-draft-mirror behavior.
|
|
7150
|
+
if (!turn.replyCalled && !isTelegramSurfaceTool(name)) {
|
|
7151
|
+
const rendered = DRAFT_MIRROR_ENABLED
|
|
7152
|
+
? describeToolUse(name, ev.input)
|
|
7153
|
+
: registerAndRender(turn.toolActivity, name)
|
|
7141
7154
|
if (rendered != null) {
|
|
7142
7155
|
turn.activityPendingRender = rendered
|
|
7143
7156
|
if (turn.activityInFlight == null) {
|
|
@@ -7185,19 +7198,19 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
7185
7198
|
isPrivateChat: turn.isDm,
|
|
7186
7199
|
threadId: turn.sessionThreadId,
|
|
7187
7200
|
// Transport selection:
|
|
7188
|
-
// -
|
|
7189
|
-
//
|
|
7190
|
-
//
|
|
7191
|
-
//
|
|
7192
|
-
//
|
|
7193
|
-
//
|
|
7194
|
-
//
|
|
7195
|
-
//
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
+
// #869-Phase1 visible-answer-stream: omit the draft API so
|
|
7202
|
+
// the lane edits a user-visible chat-timeline message
|
|
7203
|
+
// (minInitialChars:1 opens it on the first chunk). The
|
|
7204
|
+
// draft-mirror does NOT touch this lane — the canary proved
|
|
7205
|
+
// the model emits almost no interstitial assistant.text
|
|
7206
|
+
// (it thinks→tool→reply), so routing it to the draft just
|
|
7207
|
+
// emptied the preview. The draft-mirror instead renders the
|
|
7208
|
+
// tool_use stream (case 'tool_use' above) where the real
|
|
7209
|
+
// signal lives. assistant.text keeps its visible-message
|
|
7210
|
+
// home; the reply tool stays the canonical answer.
|
|
7211
|
+
...(ANSWER_STREAM_VISIBLE_ENABLED
|
|
7212
|
+
? { minInitialChars: 1 }
|
|
7213
|
+
: { sendMessageDraft: sendMessageDraftFn }),
|
|
7201
7214
|
// #1075: route through robustApiCall so flood-wait,
|
|
7202
7215
|
// benign-400, and THREAD_NOT_FOUND are handled uniformly
|
|
7203
7216
|
// instead of crashing the answer-stream loop on a deleted
|
|
@@ -5,8 +5,74 @@ import {
|
|
|
5
5
|
formatSummary,
|
|
6
6
|
registerAndRender,
|
|
7
7
|
verbForTool,
|
|
8
|
+
describeToolUse,
|
|
8
9
|
} from "../tool-activity-summary.js";
|
|
9
10
|
|
|
11
|
+
describe("describeToolUse — friendly per-tool rendering (draft-mirror)", () => {
|
|
12
|
+
it("Bash uses the model-authored description verbatim, never the command", () => {
|
|
13
|
+
expect(
|
|
14
|
+
describeToolUse("Bash", { command: "ls -la /tmp", description: "List workspace" }),
|
|
15
|
+
).toBe("List workspace");
|
|
16
|
+
// No description → safe generic, still never the raw command.
|
|
17
|
+
expect(describeToolUse("Bash", { command: "grep -r foo ." })).toBe("Running a command");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("Read/Edit/Write render the file basename, not the full path", () => {
|
|
21
|
+
expect(describeToolUse("Read", { file_path: "/home/ken/code/switchroom/gateway.ts" })).toBe(
|
|
22
|
+
"Reading gateway.ts",
|
|
23
|
+
);
|
|
24
|
+
expect(describeToolUse("Edit", { file_path: "/a/b/CLAUDE.md" })).toBe("Editing CLAUDE.md");
|
|
25
|
+
expect(describeToolUse("Write", { file_path: "notes.txt" })).toBe("Writing notes.txt");
|
|
26
|
+
expect(describeToolUse("Read", {})).toBe("Reading a file");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("Grep/Glob show the pattern; WebFetch shows the hostname", () => {
|
|
30
|
+
expect(describeToolUse("Grep", { pattern: "TODO" })).toBe("Searching for TODO");
|
|
31
|
+
expect(describeToolUse("WebFetch", { url: "https://www.example.com/path?q=1" })).toBe(
|
|
32
|
+
"Reading example.com",
|
|
33
|
+
);
|
|
34
|
+
expect(describeToolUse("WebSearch", { query: "best running shoes" })).toBe(
|
|
35
|
+
"Searching the web for best running shoes",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("Task/Agent surface the sub-agent task description", () => {
|
|
40
|
+
expect(describeToolUse("Task", { description: "Review the migration" })).toBe(
|
|
41
|
+
"Delegating: Review the migration",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("domain MCP tools render human-meaningful labels (no jargon)", () => {
|
|
46
|
+
expect(describeToolUse("mcp__hindsight__reflect", { query: "x" })).toBe("Searching memory");
|
|
47
|
+
expect(describeToolUse("mcp__hindsight__retain", {})).toBe("Saving to memory");
|
|
48
|
+
expect(describeToolUse("mcp__claude_ai_Google_Calendar__list_events", {})).toBe(
|
|
49
|
+
"Checking your calendar",
|
|
50
|
+
);
|
|
51
|
+
expect(describeToolUse("mcp__claude_ai_Gmail__search", {})).toBe("Checking your email");
|
|
52
|
+
expect(describeToolUse("mcp__claude_ai_Google_Drive__search_files", {})).toBe(
|
|
53
|
+
"Looking through your files",
|
|
54
|
+
);
|
|
55
|
+
expect(describeToolUse("mcp__claude_ai_Notion__notion-search", {})).toBe("Checking your notes");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("surface tools (reply/stream_reply) return null — never mirrored", () => {
|
|
59
|
+
expect(describeToolUse("mcp__switchroom-telegram__reply", { text: "hi" })).toBeNull();
|
|
60
|
+
expect(describeToolUse("mcp__switchroom-telegram__stream_reply", {})).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("unknown MCP tool prefers a model-authored field, else humanizes the name", () => {
|
|
64
|
+
expect(describeToolUse("mcp__acme__do_thing", { description: "Fetched the report" })).toBe(
|
|
65
|
+
"Fetched the report",
|
|
66
|
+
);
|
|
67
|
+
expect(describeToolUse("mcp__acme__do_thing", {})).toBe("Using do thing");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("unknown built-in falls back to a generic working line, never raw syntax", () => {
|
|
71
|
+
expect(describeToolUse("SomeFutureTool", {})).toBe("Working…");
|
|
72
|
+
expect(describeToolUse("", {})).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
10
76
|
describe("verbForTool — tool name → past-tense verb", () => {
|
|
11
77
|
it("maps standard CLI tools to readable verbs", () => {
|
|
12
78
|
expect(verbForTool("Read")).toBe("read");
|
|
@@ -198,3 +198,140 @@ export function registerAndRender(
|
|
|
198
198
|
if (!changed) return null;
|
|
199
199
|
return formatSummary(state);
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
// ─── Friendly per-tool rendering (draft-mirror, RFC draft-mirror-preview) ───
|
|
203
|
+
//
|
|
204
|
+
// Claude Code's own UI reads human-friendly because the model AUTHORS the
|
|
205
|
+
// descriptive text inside each tool_use.input — verified against a real
|
|
206
|
+
// session JSONL (1360 Bash calls etc.):
|
|
207
|
+
// Bash → input.description ("Get CLAUDE.md size and recent history")
|
|
208
|
+
// Read → input.file_path (basename → "Reading CLAUDE.md")
|
|
209
|
+
// Edit/Write → input.file_path (basename)
|
|
210
|
+
// Grep/Glob → input.pattern
|
|
211
|
+
// Task/Agent → input.description (the sub-agent's task)
|
|
212
|
+
// WebFetch → input.url (hostname → "Reading example.com")
|
|
213
|
+
// hindsight → friendly label ("Searching memory")
|
|
214
|
+
// There is never a raw `grep`/`jq`/`ls` to surface — only the model's own
|
|
215
|
+
// plain-English description or a domain label. This is the signal the
|
|
216
|
+
// draft-mirror renders (option A: uniform across code + non-code agents).
|
|
217
|
+
|
|
218
|
+
/** Strip a path to its basename for display. */
|
|
219
|
+
function baseName(p: unknown): string | null {
|
|
220
|
+
if (typeof p !== "string" || p.length === 0) return null;
|
|
221
|
+
const parts = p.split("/").filter(Boolean);
|
|
222
|
+
return parts.length > 0 ? parts[parts.length - 1] : p;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Extract a bare hostname from a URL for display (no scheme/path). */
|
|
226
|
+
function hostName(u: unknown): string | null {
|
|
227
|
+
if (typeof u !== "string" || u.length === 0) return null;
|
|
228
|
+
try {
|
|
229
|
+
return new URL(u).hostname.replace(/^www\./, "");
|
|
230
|
+
} catch {
|
|
231
|
+
return u.replace(/^https?:\/\//, "").split("/")[0] || null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function clip(s: unknown, n: number): string | null {
|
|
236
|
+
if (typeof s !== "string") return null;
|
|
237
|
+
const t = s.trim();
|
|
238
|
+
if (t.length === 0) return null;
|
|
239
|
+
return t.length > n ? t.slice(0, n - 1) + "…" : t;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Render a single tool_use into a human-friendly, present-tense activity
|
|
244
|
+
* line for the live draft preview — or null when the tool should NOT be
|
|
245
|
+
* surfaced (the Telegram-plugin surface tools, which ARE the conversation).
|
|
246
|
+
*
|
|
247
|
+
* Leads with the model-authored descriptive field per the map above; falls
|
|
248
|
+
* back to a domain label, then to a humanized tool name. Never emits raw
|
|
249
|
+
* shell/query syntax.
|
|
250
|
+
*/
|
|
251
|
+
export function describeToolUse(
|
|
252
|
+
toolName: string,
|
|
253
|
+
input: Record<string, unknown> | undefined,
|
|
254
|
+
): string | null {
|
|
255
|
+
if (!toolName) return null;
|
|
256
|
+
const inp = input ?? {};
|
|
257
|
+
|
|
258
|
+
const mcpMatch = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
259
|
+
if (mcpMatch) {
|
|
260
|
+
const server = mcpMatch[1].toLowerCase();
|
|
261
|
+
const tool = mcpMatch[2].toLowerCase();
|
|
262
|
+
// Surface tools ARE the conversation — never mirror them.
|
|
263
|
+
if (server === "switchroom-telegram") return null;
|
|
264
|
+
if (server === "hindsight") {
|
|
265
|
+
if (tool === "recall" || tool === "reflect") return "Searching memory";
|
|
266
|
+
if (tool === "retain" || tool === "update_memory" || tool === "sync_retain")
|
|
267
|
+
return "Saving to memory";
|
|
268
|
+
return "Working with memory";
|
|
269
|
+
}
|
|
270
|
+
if (
|
|
271
|
+
server === "google-workspace" ||
|
|
272
|
+
server === "claude_ai_google_calendar"
|
|
273
|
+
) {
|
|
274
|
+
return "Checking your calendar";
|
|
275
|
+
}
|
|
276
|
+
if (server === "claude_ai_gmail") return "Checking your email";
|
|
277
|
+
if (server === "claude_ai_google_drive") return "Looking through your files";
|
|
278
|
+
if (server === "notion" || server === "claude_ai_notion") {
|
|
279
|
+
return "Checking your notes";
|
|
280
|
+
}
|
|
281
|
+
// Unknown MCP tool: prefer a model-authored field, else a humanized name.
|
|
282
|
+
const desc = clip(inp.description, 60) ?? clip(inp.query, 50) ?? clip(inp.title, 50);
|
|
283
|
+
if (desc) return desc;
|
|
284
|
+
return "Using " + tool.replace(/[-_]+/g, " ");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
switch (toolName) {
|
|
288
|
+
case "Bash": {
|
|
289
|
+
// The model writes a plain-English description for every command.
|
|
290
|
+
return clip(inp.description, 70) ?? "Running a command";
|
|
291
|
+
}
|
|
292
|
+
case "BashOutput":
|
|
293
|
+
case "KillShell":
|
|
294
|
+
return "Managing a background command";
|
|
295
|
+
case "Read": {
|
|
296
|
+
const f = baseName(inp.file_path);
|
|
297
|
+
return f ? `Reading ${f}` : "Reading a file";
|
|
298
|
+
}
|
|
299
|
+
case "Edit":
|
|
300
|
+
case "MultiEdit":
|
|
301
|
+
case "NotebookEdit": {
|
|
302
|
+
const f = baseName(inp.file_path) ?? baseName(inp.notebook_path);
|
|
303
|
+
return f ? `Editing ${f}` : "Editing a file";
|
|
304
|
+
}
|
|
305
|
+
case "Write": {
|
|
306
|
+
const f = baseName(inp.file_path);
|
|
307
|
+
return f ? `Writing ${f}` : "Writing a file";
|
|
308
|
+
}
|
|
309
|
+
case "Grep":
|
|
310
|
+
case "Glob": {
|
|
311
|
+
const p = clip(inp.pattern, 40);
|
|
312
|
+
return p ? `Searching for ${p}` : "Searching files";
|
|
313
|
+
}
|
|
314
|
+
case "WebFetch": {
|
|
315
|
+
const h = hostName(inp.url);
|
|
316
|
+
return h ? `Reading ${h}` : "Reading a web page";
|
|
317
|
+
}
|
|
318
|
+
case "WebSearch": {
|
|
319
|
+
const q = clip(inp.query, 50);
|
|
320
|
+
return q ? `Searching the web for ${q}` : "Searching the web";
|
|
321
|
+
}
|
|
322
|
+
case "Task":
|
|
323
|
+
case "Agent": {
|
|
324
|
+
const d = clip(inp.description, 60);
|
|
325
|
+
return d ? `Delegating: ${d}` : "Delegating to a sub-agent";
|
|
326
|
+
}
|
|
327
|
+
case "TodoWrite":
|
|
328
|
+
case "TaskCreate":
|
|
329
|
+
case "TaskUpdate":
|
|
330
|
+
case "TaskList":
|
|
331
|
+
return "Updating the plan";
|
|
332
|
+
case "ToolSearch":
|
|
333
|
+
return "Finding the right tool";
|
|
334
|
+
default:
|
|
335
|
+
return "Working…";
|
|
336
|
+
}
|
|
337
|
+
}
|