switchroom 0.13.60 → 0.13.62
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 +460 -361
- package/dist/host-control/main.js +148 -148
- package/dist/vault/approvals/kernel-server.js +82 -82
- package/dist/vault/broker/server.js +98 -83
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +23 -0
- package/telegram-plugin/answer-stream.ts +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +112 -112
- package/telegram-plugin/dist/gateway/gateway.js +221 -193
- package/telegram-plugin/dist/server.js +160 -160
- package/telegram-plugin/gateway/gateway.ts +2 -2
- package/telegram-plugin/silence-poke.ts +16 -0
- package/telegram-plugin/tests/silence-poke.test.ts +37 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +34 -6
- package/telegram-plugin/tool-activity-summary.ts +37 -1
|
@@ -638,7 +638,7 @@ const GRAMMY_VERSION: string = (() => {
|
|
|
638
638
|
}
|
|
639
639
|
})()
|
|
640
640
|
const sendMessageDraftFn: (
|
|
641
|
-
(chatId: string, draftId: number, text: string, params?: { message_thread_id?: number }) => Promise<unknown>
|
|
641
|
+
(chatId: string, draftId: number, text: string, params?: { message_thread_id?: number; parse_mode?: 'HTML' }) => Promise<unknown>
|
|
642
642
|
) | undefined =
|
|
643
643
|
typeof _rawSendMessageDraft === 'function'
|
|
644
644
|
? (chatId, draftId, text, params) =>
|
|
@@ -6828,7 +6828,7 @@ async function drainActivitySummary(turn: CurrentTurn): Promise<void> {
|
|
|
6828
6828
|
turn.activityDraftId = allocateDraftId()
|
|
6829
6829
|
}
|
|
6830
6830
|
const draftId = turn.activityDraftId
|
|
6831
|
-
await sendMessageDraftFn!(chat, draftId, html,
|
|
6831
|
+
await sendMessageDraftFn!(chat, draftId, html, { parse_mode: 'HTML' })
|
|
6832
6832
|
} else if (turn.activityMessageId == null) {
|
|
6833
6833
|
const sent = await robustApiCall(
|
|
6834
6834
|
() => bot.api.sendMessage(chat, html, {
|
|
@@ -433,6 +433,15 @@ export function formatFrameworkFallbackText(
|
|
|
433
433
|
// running Grep "foo" for 4m (no update from agent in 5 min)
|
|
434
434
|
// running Grep "foo" + 2 more (4m) (no update from agent in 5 min)
|
|
435
435
|
// running Grep (no label) for 4m (no update from agent in 5 min)
|
|
436
|
+
//
|
|
437
|
+
// Raw MCP tool names (`mcp__server__tool`) are technical identifiers
|
|
438
|
+
// and look like a leak when surfaced to a user. When the tool name
|
|
439
|
+
// matches that shape AND a human-friendly label is available, drop
|
|
440
|
+
// the raw name and lead with the label instead:
|
|
441
|
+
// Searching memory for 4m (no update from agent in 5 min)
|
|
442
|
+
// Built-in tool names (Grep, Read, Bash) stay as-is — they ARE
|
|
443
|
+
// human-readable, and the label is supplementary detail (e.g. the
|
|
444
|
+
// search pattern) that reads naturally after the verb.
|
|
436
445
|
if (inFlightTools.length > 0) {
|
|
437
446
|
const longest = inFlightTools[0]!
|
|
438
447
|
const dur = formatDurationShort(longest.durationMs)
|
|
@@ -442,6 +451,13 @@ export function formatFrameworkFallbackText(
|
|
|
442
451
|
const more = inFlightTools.length > 1
|
|
443
452
|
? ` + ${inFlightTools.length - 1} more`
|
|
444
453
|
: ''
|
|
454
|
+
const isMcpRawName = /^mcp__/.test(longest.name)
|
|
455
|
+
if (isMcpRawName && labelTail !== '') {
|
|
456
|
+
// Label-only: "Searching memory for 4m (…)". Drop the raw
|
|
457
|
+
// `mcp__server__tool` and the leading "running" because the
|
|
458
|
+
// label already reads as a gerund phrase.
|
|
459
|
+
return `${truncateLabel(longest.label!)}${more} for ${dur} ${suffix}`
|
|
460
|
+
}
|
|
445
461
|
return `running ${longest.name}${labelTail}${more} for ${dur} ${suffix}`
|
|
446
462
|
}
|
|
447
463
|
return fallbackKind === 'thinking'
|
|
@@ -533,6 +533,43 @@ describe('silence-poke — #1292 tool-aware framework fallback', () => {
|
|
|
533
533
|
)
|
|
534
534
|
})
|
|
535
535
|
|
|
536
|
+
it('raw mcp__ tool name with a human label drops the technical name and leads with the label', () => {
|
|
537
|
+
// mcp__hindsight__reflect is the internal MCP identifier — looks
|
|
538
|
+
// like a leak when surfaced to a user. The label table emits
|
|
539
|
+
// "Searching memory" for it (see hooks/tool-label-pretool.mjs);
|
|
540
|
+
// the fallback message should lead with the label, not concatenate
|
|
541
|
+
// both.
|
|
542
|
+
const text = formatFrameworkFallbackText('working', 305_000, [
|
|
543
|
+
{ name: 'mcp__hindsight__reflect', label: 'Searching memory', durationMs: 305_000 },
|
|
544
|
+
])
|
|
545
|
+
expect(text).toBe(
|
|
546
|
+
'Searching memory for 5m (no update from agent in 5 min)',
|
|
547
|
+
)
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('raw mcp__ tool name with NO label falls back to the bare name (no leak-but-no-better-option)', () => {
|
|
551
|
+
// If the label table doesn't recognise an MCP tool, we have nothing
|
|
552
|
+
// better to show than the raw name. Better honest-ugly than silent.
|
|
553
|
+
const text = formatFrameworkFallbackText('working', 305_000, [
|
|
554
|
+
{ name: 'mcp__some-third-party__do_thing', label: null, durationMs: 305_000 },
|
|
555
|
+
])
|
|
556
|
+
expect(text).toBe(
|
|
557
|
+
'running mcp__some-third-party__do_thing for 5m (no update from agent in 5 min)',
|
|
558
|
+
)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('built-in tool (Grep) with a label keeps the prior "running Name label" shape — name is already human-readable', () => {
|
|
562
|
+
// Regression guard: don't accidentally drop the built-in tool name
|
|
563
|
+
// when generalising the MCP rule. "Grep" is human-readable; the
|
|
564
|
+
// label ("foo") is supplementary detail like the search pattern.
|
|
565
|
+
const text = formatFrameworkFallbackText('working', 305_000, [
|
|
566
|
+
{ name: 'Grep', label: 'foo', durationMs: 305_000 },
|
|
567
|
+
])
|
|
568
|
+
expect(text).toBe(
|
|
569
|
+
'running Grep foo for 5m (no update from agent in 5 min)',
|
|
570
|
+
)
|
|
571
|
+
})
|
|
572
|
+
|
|
536
573
|
it('empty inFlightTools falls back to the base "still working" wording', () => {
|
|
537
574
|
expect(
|
|
538
575
|
formatFrameworkFallbackText('working', 305_000, []),
|
|
@@ -32,9 +32,25 @@ describe("verbForTool — tool name → past-tense verb", () => {
|
|
|
32
32
|
expect(verbForTool("mcp__switchroom-telegram__react")).toBeNull();
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
it("
|
|
36
|
-
|
|
37
|
-
expect(verbForTool("
|
|
35
|
+
it("maps recognised MCP tools (hindsight, google-workspace, notion) to specific verbs", () => {
|
|
36
|
+
// hindsight: recall/reflect → searched, retain/update_memory → saved
|
|
37
|
+
expect(verbForTool("mcp__hindsight__reflect")).toBe("searched");
|
|
38
|
+
expect(verbForTool("mcp__hindsight__recall")).toBe("searched");
|
|
39
|
+
expect(verbForTool("mcp__hindsight__retain")).toBe("saved");
|
|
40
|
+
expect(verbForTool("mcp__hindsight__update_memory")).toBe("saved");
|
|
41
|
+
// google-workspace / claude.ai variants: read-shaped → searched, write-shaped → edited
|
|
42
|
+
expect(verbForTool("mcp__google-workspace__list_files")).toBe("searched");
|
|
43
|
+
expect(verbForTool("mcp__claude_ai_Gmail__search_messages")).toBe("searched");
|
|
44
|
+
expect(verbForTool("mcp__google-workspace__create_file")).toBe("edited");
|
|
45
|
+
expect(verbForTool("mcp__claude_ai_Google_Drive__download_file_content")).toBe("searched");
|
|
46
|
+
// notion: query/get → searched, create/update → edited
|
|
47
|
+
expect(verbForTool("mcp__notion__query_database")).toBe("searched");
|
|
48
|
+
expect(verbForTool("mcp__claude_ai_Notion__notion-search")).toBe("searched");
|
|
49
|
+
expect(verbForTool("mcp__claude_ai_Notion__notion-update-page")).toBe("edited");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns 'used' for genuinely unknown MCP / future tools (generic fallback)", () => {
|
|
53
|
+
expect(verbForTool("mcp__random-third-party__do_thing")).toBe("used");
|
|
38
54
|
expect(verbForTool("SomeFutureUnknownTool")).toBe("used");
|
|
39
55
|
});
|
|
40
56
|
|
|
@@ -112,14 +128,26 @@ describe("register + formatSummary — Claude Code-style summary", () => {
|
|
|
112
128
|
expect(formatSummary(s)).toBeNull(); // nothing tracked
|
|
113
129
|
});
|
|
114
130
|
|
|
115
|
-
it("includes generic 'used' for unknown MCP tools", () => {
|
|
131
|
+
it("includes generic 'used' for genuinely-unknown MCP tools (fallback)", () => {
|
|
116
132
|
const s = makeEmptyActivityState();
|
|
117
|
-
register(s, "
|
|
133
|
+
register(s, "mcp__random-third-party__do_thing");
|
|
118
134
|
expect(formatSummary(s)).toBe("Used a tool");
|
|
119
|
-
register(s, "
|
|
135
|
+
register(s, "mcp__another-unknown-server__something_else");
|
|
120
136
|
expect(formatSummary(s)).toBe("Used 2 tools");
|
|
121
137
|
});
|
|
122
138
|
|
|
139
|
+
it("maps recognised MCP tools to natural-language summaries (no generic 'Used N tools')", () => {
|
|
140
|
+
// hindsight search shows up as 'searched' (memory)
|
|
141
|
+
const s = makeEmptyActivityState();
|
|
142
|
+
register(s, "mcp__hindsight__reflect");
|
|
143
|
+
expect(formatSummary(s)).toBe("Ran a search");
|
|
144
|
+
register(s, "mcp__hindsight__reflect");
|
|
145
|
+
expect(formatSummary(s)).toBe("Ran 2 searches");
|
|
146
|
+
// hindsight retain shows up as 'saved a memory'
|
|
147
|
+
register(s, "mcp__hindsight__retain");
|
|
148
|
+
expect(formatSummary(s)).toBe("Ran 2 searches, saved a memory");
|
|
149
|
+
});
|
|
150
|
+
|
|
123
151
|
it("tracks firstToolName for forensic / telemetry use", () => {
|
|
124
152
|
const s = makeEmptyActivityState();
|
|
125
153
|
register(s, "Read");
|
|
@@ -40,6 +40,7 @@ export type ActivityVerb =
|
|
|
40
40
|
| "fetched"
|
|
41
41
|
| "dispatched"
|
|
42
42
|
| "noted"
|
|
43
|
+
| "saved" // memory-retain class (hindsight, etc.) — distinct from "noted" (TodoWrite)
|
|
43
44
|
| "used"; // generic fallback
|
|
44
45
|
|
|
45
46
|
/** Object form so `register()` can mutate; pure functions inside the
|
|
@@ -66,10 +67,44 @@ export function makeEmptyActivityState(): ActivityState {
|
|
|
66
67
|
* like reply/stream_reply) return null — the caller skips them. */
|
|
67
68
|
export function verbForTool(toolName: string): ActivityVerb | null {
|
|
68
69
|
if (!toolName) return null;
|
|
69
|
-
|
|
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);
|
|
70
75
|
// Skip user-facing Telegram-plugin tools entirely — those ARE the
|
|
71
76
|
// surface, never to be summarised.
|
|
72
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
|
+
|
|
73
108
|
const suffix = (mcpMatch ? mcpMatch[2] : toolName).toLowerCase();
|
|
74
109
|
switch (suffix) {
|
|
75
110
|
case "read":
|
|
@@ -128,6 +163,7 @@ const VERB_PHRASE: Record<ActivityVerb, VerbPhrase> = {
|
|
|
128
163
|
fetched: { singular: "fetched a URL", plural: "fetched $N URLs" },
|
|
129
164
|
dispatched: { singular: "dispatched a sub-agent", plural: "dispatched $N sub-agents" },
|
|
130
165
|
noted: { singular: "updated the todo list", plural: "updated the todo list ($N edits)" },
|
|
166
|
+
saved: { singular: "saved a memory", plural: "saved $N memories" },
|
|
131
167
|
used: { singular: "used a tool", plural: "used $N tools" },
|
|
132
168
|
};
|
|
133
169
|
|