switchroom 0.14.8 → 0.14.9
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 +357 -357
- 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 +194 -321
- package/telegram-plugin/dist/server.js +160 -160
- package/telegram-plugin/gateway/gateway.ts +41 -97
- package/telegram-plugin/tests/tool-activity-summary.test.ts +0 -216
- package/telegram-plugin/tool-activity-summary.ts +18 -197
|
@@ -1,205 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool-activity
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Tool-activity feed — a Claude-Code-style live list of what the agent
|
|
3
|
+
* is doing this turn, rendered into ONE Telegram message that edits in
|
|
4
|
+
* place and clears the moment the model's real reply lands.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Each non-surface tool gets a human-friendly, present-tense line
|
|
7
|
+
* ("Reading CLAUDE.md", "Searching memory", "Running a command"); the
|
|
8
|
+
* feed renders them chronologically (oldest first, newest = the
|
|
9
|
+
* in-progress step), consecutive duplicates collapsed, capped to the
|
|
10
|
+
* most recent MIRROR_MAX_LINES with a "+N earlier" header.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* - One Telegram message per phase (low signal-to-noise vs N
|
|
19
|
-
* messages on a heavy turn)
|
|
20
|
-
* - The user sees ACCUMULATED work in a glanceable form, not a flood
|
|
21
|
-
* - Plays nicely with the existing answer-lane stream that handles
|
|
22
|
-
* the actual reply text
|
|
23
|
-
*
|
|
24
|
-
* Tracking shape: per-turn counters keyed by `verb` (the action class
|
|
25
|
-
* derived from tool name). One counter per verb so the summary line
|
|
26
|
-
* collapses neatly regardless of which specific Read/Bash/WebSearch
|
|
27
|
-
* the model chose. `register()` increments the counter; `formatSummary()`
|
|
28
|
-
* renders the current state.
|
|
12
|
+
* Two append entrypoints feed the same `lines: string[]` accumulator:
|
|
13
|
+
* - `appendActivityLabel` — for a pre-computed label from the
|
|
14
|
+
* real-time PreToolUse sidecar (`tool_label` event). This is the
|
|
15
|
+
* gateway's live driver: it fires at tool-call time regardless of
|
|
16
|
+
* when claude flushes the transcript, so it stays deterministic on
|
|
17
|
+
* fast/clustered-tool turns.
|
|
18
|
+
* - `appendActivityLine` — derives the label from a tool_use's name +
|
|
19
|
+
* input via `describeToolUse` (used where the raw tool_use is the
|
|
20
|
+
* only signal available).
|
|
29
21
|
*/
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
const WRITE_VERBS = new Set(["wrote", "created", "edited"]);
|
|
33
|
-
|
|
34
|
-
export type ActivityVerb =
|
|
35
|
-
| "read"
|
|
36
|
-
| "edited"
|
|
37
|
-
| "created"
|
|
38
|
-
| "ran"
|
|
39
|
-
| "searched"
|
|
40
|
-
| "fetched"
|
|
41
|
-
| "dispatched"
|
|
42
|
-
| "noted"
|
|
43
|
-
| "saved" // memory-retain class (hindsight, etc.) — distinct from "noted" (TodoWrite)
|
|
44
|
-
| "used"; // generic fallback
|
|
45
|
-
|
|
46
|
-
/** Object form so `register()` can mutate; pure functions inside the
|
|
47
|
-
* module work against this shape (easier to unit-test than a Map). */
|
|
48
|
-
export interface ActivityState {
|
|
49
|
-
counts: Partial<Record<ActivityVerb, number>>;
|
|
50
|
-
/** Order verbs were first observed this turn. The summary renders in
|
|
51
|
-
* this order so the line reads as a chronological natural-language
|
|
52
|
-
* account: "edited a file, read a file, ran a command" matches the
|
|
53
|
-
* agent's actual sequence of actions. Stable — once a verb is added
|
|
54
|
-
* to this list, it never moves. */
|
|
55
|
-
order: ActivityVerb[];
|
|
56
|
-
/** First non-trivial tool name observed this turn (for telemetry / future
|
|
57
|
-
* "what kicked this off" forensic). Not used in the rendered summary. */
|
|
58
|
-
firstToolName: string | null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function makeEmptyActivityState(): ActivityState {
|
|
62
|
-
return { counts: {}, order: [], firstToolName: null };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Map a tool name → verb. Mirrors the existing `tool-intent-surface.ts`
|
|
66
|
-
* verb table but in past tense. Tools that don't map (or surface tools
|
|
67
|
-
* like reply/stream_reply) return null — the caller skips them. */
|
|
68
|
-
export function verbForTool(toolName: string): ActivityVerb | null {
|
|
69
|
-
if (!toolName) return null;
|
|
70
|
-
// Lazy match on the server segment so names containing underscores
|
|
71
|
-
// (e.g. `mcp__claude_ai_Gmail__search`) parse as
|
|
72
|
-
// server="claude_ai_Gmail", tool="search"
|
|
73
|
-
// instead of the prior `[^_]+` which stopped at the first inner `_`.
|
|
74
|
-
const mcpMatch = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
75
|
-
// Skip user-facing Telegram-plugin tools entirely — those ARE the
|
|
76
|
-
// surface, never to be summarised.
|
|
77
|
-
if (mcpMatch && mcpMatch[1] === "switchroom-telegram") return null;
|
|
78
|
-
|
|
79
|
-
// MCP allowlist — map common MCP tools to specific verbs so the summary
|
|
80
|
-
// reads as "Searched memory" or "Read 2 files" instead of the generic
|
|
81
|
-
// fallback "Used 2 tools". Tools NOT on this list fall through to the
|
|
82
|
-
// generic "used" verb, which is still better than nothing for one-offs
|
|
83
|
-
// but hurts on heavy MCP turns. Mirrors the label table in
|
|
84
|
-
// `telegram-plugin/hooks/tool-label-pretool.mjs` — keep them in sync.
|
|
85
|
-
if (mcpMatch) {
|
|
86
|
-
// Case-insensitive match — claude.ai prefixes use mixed-case
|
|
87
|
-
// server names ("claude_ai_Gmail", "claude_ai_Google_Drive") so we
|
|
88
|
-
// lowercase both sides before comparing.
|
|
89
|
-
const server = mcpMatch[1].toLowerCase();
|
|
90
|
-
const mcpTool = mcpMatch[2].toLowerCase();
|
|
91
|
-
if (server === "hindsight") {
|
|
92
|
-
if (mcpTool === "recall" || mcpTool === "reflect") return "searched";
|
|
93
|
-
if (mcpTool === "retain" || mcpTool === "update_memory" || mcpTool === "sync_retain") return "saved";
|
|
94
|
-
}
|
|
95
|
-
if (server === "google-workspace" || server === "claude_ai_google_drive" || server === "claude_ai_gmail" || server === "claude_ai_google_calendar") {
|
|
96
|
-
if (/^(search|list|query|read|get|fetch|download)/i.test(mcpTool)) return "searched";
|
|
97
|
-
if (/^(create|update|write|send|move|copy|duplicate)/i.test(mcpTool)) return "edited";
|
|
98
|
-
}
|
|
99
|
-
if (server === "notion" || server === "claude_ai_notion") {
|
|
100
|
-
// claude.ai Notion exposes tools as `notion-search`, `notion-update-page`,
|
|
101
|
-
// etc. Strip the redundant `notion-` prefix before matching the verb.
|
|
102
|
-
const action = mcpTool.replace(/^notion-/, "");
|
|
103
|
-
if (/^(search|fetch|query|get|read)/i.test(action)) return "searched";
|
|
104
|
-
if (/^(create|update|move|duplicate|comment)/i.test(action)) return "edited";
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const suffix = (mcpMatch ? mcpMatch[2] : toolName).toLowerCase();
|
|
109
|
-
switch (suffix) {
|
|
110
|
-
case "read":
|
|
111
|
-
return "read";
|
|
112
|
-
case "write":
|
|
113
|
-
return "created";
|
|
114
|
-
case "edit":
|
|
115
|
-
case "multiedit":
|
|
116
|
-
case "notebookedit":
|
|
117
|
-
return "edited";
|
|
118
|
-
case "bash":
|
|
119
|
-
case "bashoutput":
|
|
120
|
-
case "killshell":
|
|
121
|
-
return "ran";
|
|
122
|
-
case "websearch":
|
|
123
|
-
case "grep":
|
|
124
|
-
case "glob":
|
|
125
|
-
return "searched";
|
|
126
|
-
case "webfetch":
|
|
127
|
-
return "fetched";
|
|
128
|
-
case "task":
|
|
129
|
-
case "agent":
|
|
130
|
-
return "dispatched";
|
|
131
|
-
case "todowrite":
|
|
132
|
-
case "todoread":
|
|
133
|
-
return "noted";
|
|
134
|
-
default:
|
|
135
|
-
return "used";
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Mutates `state` to record one tool_use of `toolName`. Returns true
|
|
140
|
-
* iff the activity state changed (so the caller knows to refresh the
|
|
141
|
-
* rendered summary). */
|
|
142
|
-
export function register(state: ActivityState, toolName: string): boolean {
|
|
143
|
-
const verb = verbForTool(toolName);
|
|
144
|
-
if (!verb) return false;
|
|
145
|
-
if (state.firstToolName == null) state.firstToolName = toolName;
|
|
146
|
-
const prior = state.counts[verb] ?? 0;
|
|
147
|
-
if (prior === 0) state.order.push(verb);
|
|
148
|
-
state.counts[verb] = prior + 1;
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
interface VerbPhrase {
|
|
153
|
-
singular: string;
|
|
154
|
-
plural: string;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const VERB_PHRASE: Record<ActivityVerb, VerbPhrase> = {
|
|
158
|
-
read: { singular: "read a file", plural: "read $N files" },
|
|
159
|
-
edited: { singular: "edited a file", plural: "edited $N files" },
|
|
160
|
-
created: { singular: "created a file", plural: "created $N files" },
|
|
161
|
-
ran: { singular: "ran a command", plural: "ran $N commands" },
|
|
162
|
-
searched: { singular: "ran a search", plural: "ran $N searches" },
|
|
163
|
-
fetched: { singular: "fetched a URL", plural: "fetched $N URLs" },
|
|
164
|
-
dispatched: { singular: "dispatched a sub-agent", plural: "dispatched $N sub-agents" },
|
|
165
|
-
noted: { singular: "updated the todo list", plural: "updated the todo list ($N edits)" },
|
|
166
|
-
saved: { singular: "saved a memory", plural: "saved $N memories" },
|
|
167
|
-
used: { singular: "used a tool", plural: "used $N tools" },
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
/** Render the activity state as a single natural-language line.
|
|
171
|
-
* Verbs are rendered in `state.order` — first-occurrence order — so
|
|
172
|
-
* the line reads chronologically ("edited a file, read a file, ran
|
|
173
|
-
* a command" mirrors the agent's actual action sequence). Returns
|
|
174
|
-
* null when the state is empty (nothing to show yet). */
|
|
175
|
-
export function formatSummary(state: ActivityState): string | null {
|
|
176
|
-
const phrases: string[] = [];
|
|
177
|
-
for (const verb of state.order) {
|
|
178
|
-
const n = state.counts[verb] ?? 0;
|
|
179
|
-
if (n <= 0) continue;
|
|
180
|
-
const p = VERB_PHRASE[verb];
|
|
181
|
-
phrases.push(n === 1 ? p.singular : p.plural.replace("$N", String(n)));
|
|
182
|
-
}
|
|
183
|
-
if (phrases.length === 0) return null;
|
|
184
|
-
// Capitalize first letter so the sentence reads as a statement.
|
|
185
|
-
const sentence = phrases.join(", ");
|
|
186
|
-
return sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Convenience: ergonomic full pipeline for callers that just want
|
|
190
|
-
* "given the new tool name and prior state, give me the updated rendered
|
|
191
|
-
* text or null if nothing changed". Returns null when the tool is a
|
|
192
|
-
* surface tool / no-op (so the caller can skip the Telegram edit). */
|
|
193
|
-
export function registerAndRender(
|
|
194
|
-
state: ActivityState,
|
|
195
|
-
toolName: string,
|
|
196
|
-
): string | null {
|
|
197
|
-
const changed = register(state, toolName);
|
|
198
|
-
if (!changed) return null;
|
|
199
|
-
return formatSummary(state);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ─── Friendly per-tool rendering (draft-mirror, RFC draft-mirror-preview) ───
|
|
23
|
+
// ─── Friendly per-tool rendering ────────────────────────────────────────────
|
|
203
24
|
//
|
|
204
25
|
// Claude Code's own UI reads human-friendly because the model AUTHORS the
|
|
205
26
|
// descriptive text inside each tool_use.input — verified against a real
|