switchroom 0.13.61 → 0.13.63
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/cli/switchroom.js +108 -5
- package/dist/vault/broker/server.js +15 -0
- 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/gateway/gateway.js +34 -6
- 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
package/dist/cli/switchroom.js
CHANGED
|
@@ -23466,6 +23466,15 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23466
23466
|
} catch {}
|
|
23467
23467
|
lines.push(` - ${homePrefix}/.switchroom-config/agents/${a.name}/personal-skills:${homePrefix}/.switchroom-config/agents/${a.name}/personal-skills:rw`);
|
|
23468
23468
|
}
|
|
23469
|
+
if (existsSync12(`${hostHomeForChecks}/.switchroom/bin/webkite`)) {
|
|
23470
|
+
lines.push(` - ${homePrefix}/.switchroom/bin/webkite:/usr/local/bin/webkite:ro`);
|
|
23471
|
+
}
|
|
23472
|
+
if (existsSync12(`${hostHomeForChecks}/.cloakbrowser`)) {
|
|
23473
|
+
lines.push(` - ${homePrefix}/.cloakbrowser:/state/agent/home/.cloakbrowser:ro`);
|
|
23474
|
+
}
|
|
23475
|
+
if (existsSync12(`${hostHomeForChecks}/.switchroom/webkite/config.toml`)) {
|
|
23476
|
+
lines.push(` - ${homePrefix}/.switchroom/webkite/config.toml:/state/agent/home/.config/webkite/config.toml:ro`);
|
|
23477
|
+
}
|
|
23469
23478
|
if (bundledSkillsPoolDir && existsSync12(bundledSkillsPoolDir) && !bundledSkillsPoolDir.startsWith(`${hostHomeForChecks}/.switchroom/skills`)) {
|
|
23470
23479
|
lines.push(` - ${bundledSkillsPoolDir}:${bundledSkillsPoolDir}:ro`);
|
|
23471
23480
|
}
|
|
@@ -27779,6 +27788,13 @@ var init_via_claude = __esm(() => {
|
|
|
27779
27788
|
});
|
|
27780
27789
|
|
|
27781
27790
|
// src/vault/broker/acl.ts
|
|
27791
|
+
function isWebkiteCredentialKeyForAgent(agentConfig, key) {
|
|
27792
|
+
if (!WEBKITE_VAULT_KEYS.has(key))
|
|
27793
|
+
return false;
|
|
27794
|
+
if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
|
|
27795
|
+
return false;
|
|
27796
|
+
return true;
|
|
27797
|
+
}
|
|
27782
27798
|
function parseCronUnit(unitName) {
|
|
27783
27799
|
const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
|
|
27784
27800
|
if (!m)
|
|
@@ -27868,6 +27884,9 @@ function checkAclByAgent(config, agentName, key) {
|
|
|
27868
27884
|
if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
|
|
27869
27885
|
return { allow: true };
|
|
27870
27886
|
}
|
|
27887
|
+
if (isWebkiteCredentialKeyForAgent(agentConfig, key)) {
|
|
27888
|
+
return { allow: true };
|
|
27889
|
+
}
|
|
27871
27890
|
const agentBot = agentConfig.bot_token;
|
|
27872
27891
|
const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
|
|
27873
27892
|
if (typeof botRef === "string" && botRef.startsWith("vault:")) {
|
|
@@ -27941,7 +27960,14 @@ function checkGoogleAccountAcl(config, agentName, account, key) {
|
|
|
27941
27960
|
}
|
|
27942
27961
|
return { allow: true };
|
|
27943
27962
|
}
|
|
27944
|
-
var
|
|
27963
|
+
var WEBKITE_VAULT_KEYS;
|
|
27964
|
+
var init_acl = __esm(() => {
|
|
27965
|
+
WEBKITE_VAULT_KEYS = new Set([
|
|
27966
|
+
"webkite/cloudflare-account-id",
|
|
27967
|
+
"webkite/cloudflare-api-token",
|
|
27968
|
+
"webkite/firecrawl-api-key"
|
|
27969
|
+
]);
|
|
27970
|
+
});
|
|
27945
27971
|
|
|
27946
27972
|
// src/util/audit-hashchain.ts
|
|
27947
27973
|
import { createHash as createHash7 } from "node:crypto";
|
|
@@ -49057,8 +49083,8 @@ var {
|
|
|
49057
49083
|
} = import__.default;
|
|
49058
49084
|
|
|
49059
49085
|
// src/build-info.ts
|
|
49060
|
-
var VERSION = "0.13.
|
|
49061
|
-
var COMMIT_SHA = "
|
|
49086
|
+
var VERSION = "0.13.63";
|
|
49087
|
+
var COMMIT_SHA = "9aaa5939";
|
|
49062
49088
|
|
|
49063
49089
|
// src/cli/agent.ts
|
|
49064
49090
|
init_source();
|
|
@@ -49695,6 +49721,42 @@ When the user asks you to forget something ("forget that", "delete X", "drop wha
|
|
|
49695
49721
|
When the user asks "what do you know about X / me", "what do you remember about Y", or any memory audit, use \`reflect\` to synthesize an answer across the bank. Return it as honest prose, not a raw dump. If the bank has little on the topic, say so.
|
|
49696
49722
|
|
|
49697
49723
|
Don't wait for a slash command. Don't ask permission. Memory work is table stakes, like a colleague who takes notes and remembers.`;
|
|
49724
|
+
var WEB_FETCH_GUIDANCE = `## Fetching from the web
|
|
49725
|
+
|
|
49726
|
+
The native \`WebFetch\` and \`WebSearch\` tools are disabled in this
|
|
49727
|
+
fleet. Use the \`webkite_*\` MCP tools instead \u2014 they're already
|
|
49728
|
+
wired up and authenticated.
|
|
49729
|
+
|
|
49730
|
+
The 12 tools webkite exposes cover what you actually need:
|
|
49731
|
+
|
|
49732
|
+
- \`webkite_read\` \u2014 fetch a URL and get clean article text or
|
|
49733
|
+
markdown. The everyday "read this page" tool. Pass several URLs
|
|
49734
|
+
to read them in one call.
|
|
49735
|
+
- \`webkite_search\` \u2014 web search; returns URLs + snippets. Use
|
|
49736
|
+
before \`webkite_read\` when you don't already have the URL.
|
|
49737
|
+
- \`webkite_extract\` \u2014 pull specific fields from one page via a
|
|
49738
|
+
JSON Schema you supply. Use when you want structured data, not
|
|
49739
|
+
prose.
|
|
49740
|
+
- \`webkite_crawl\` \u2014 walk a site from a seed URL. Higher-cost than
|
|
49741
|
+
\`read\` or \`map\` \u2014 reserve for "harvest everything under this
|
|
49742
|
+
section."
|
|
49743
|
+
- \`webkite_map\` \u2014 list a site's URLs from its sitemap, no page
|
|
49744
|
+
content. Cheap. Use when you only need URLs.
|
|
49745
|
+
- \`webkite_clip\` \u2014 save a page snapshot (Obsidian, file, or
|
|
49746
|
+
screenshot).
|
|
49747
|
+
- session / cookie / diagnostics helpers.
|
|
49748
|
+
|
|
49749
|
+
**Why this exists:** the fleet ships with a stealth Chromium
|
|
49750
|
+
(cloakbrowser) baked in for bot-gated sites the native fetcher can't
|
|
49751
|
+
touch, plus Cloudflare/Firecrawl fallback when local render fails.
|
|
49752
|
+
\`webkite_*\` Just Works \u2014 don't try to shell out to \`curl\` or
|
|
49753
|
+
\`wget\` as a fallback; if webkite can't fetch a URL, neither can
|
|
49754
|
+
they.
|
|
49755
|
+
|
|
49756
|
+
If you genuinely need WebFetch back for one agent (e.g. a workflow
|
|
49757
|
+
that depends on its specific output shape), set \`mcp_servers.webkite:
|
|
49758
|
+
false\` in that agent's switchroom.yaml block \u2014 webkite goes away
|
|
49759
|
+
and the native tools come back together.`;
|
|
49698
49760
|
function renderFleetInvariants() {
|
|
49699
49761
|
return [
|
|
49700
49762
|
"<!--",
|
|
@@ -49717,6 +49779,8 @@ function renderFleetInvariants() {
|
|
|
49717
49779
|
TELEGRAM_GUIDANCE,
|
|
49718
49780
|
"",
|
|
49719
49781
|
MEMORY_GUIDANCE,
|
|
49782
|
+
"",
|
|
49783
|
+
WEB_FETCH_GUIDANCE,
|
|
49720
49784
|
""
|
|
49721
49785
|
].join(`
|
|
49722
49786
|
`);
|
|
@@ -49888,6 +49952,22 @@ var DEFAULT_READ_ONLY_PREAPPROVED_TOOLS = [
|
|
|
49888
49952
|
"ExitPlanMode",
|
|
49889
49953
|
"Skill"
|
|
49890
49954
|
];
|
|
49955
|
+
var WEBKITE_FLEET_DENY_TOOLS = ["WebFetch", "WebSearch"];
|
|
49956
|
+
var WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite";
|
|
49957
|
+
function webkiteBinaryAvailable() {
|
|
49958
|
+
const override = process.env.SWITCHROOM_WEBKITE_BINARY;
|
|
49959
|
+
if (override !== undefined && override !== "") {
|
|
49960
|
+
return existsSync11(override);
|
|
49961
|
+
}
|
|
49962
|
+
return existsSync11(join8(homedir4(), ".switchroom", "bin", "webkite")) || existsSync11(WEBKITE_BINARY_CONTAINER_PATH);
|
|
49963
|
+
}
|
|
49964
|
+
function webkiteDenyForAgent(agentConfig) {
|
|
49965
|
+
if (agentConfig.mcp_servers?.["webkite"] === false)
|
|
49966
|
+
return [];
|
|
49967
|
+
if (!webkiteBinaryAvailable())
|
|
49968
|
+
return [];
|
|
49969
|
+
return WEBKITE_FLEET_DENY_TOOLS;
|
|
49970
|
+
}
|
|
49891
49971
|
var SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-4-6";
|
|
49892
49972
|
var SWITCHROOM_DEFAULT_THINKING_EFFORT = "low";
|
|
49893
49973
|
function dedupe2(items) {
|
|
@@ -50455,7 +50535,10 @@ function buildWorkspaceContext(args) {
|
|
|
50455
50535
|
user: agentConfig.user,
|
|
50456
50536
|
agentConfig,
|
|
50457
50537
|
tools,
|
|
50458
|
-
toolsDeny:
|
|
50538
|
+
toolsDeny: dedupe2([
|
|
50539
|
+
...tools.deny ?? [],
|
|
50540
|
+
...webkiteDenyForAgent(agentConfig)
|
|
50541
|
+
]),
|
|
50459
50542
|
permissionAllow,
|
|
50460
50543
|
defaultModeAcceptEdits: hasAllWildcard,
|
|
50461
50544
|
memory: agentConfig.memory,
|
|
@@ -50602,6 +50685,17 @@ function resolveNotionMcpEntry(agentName, agentConfig, switchroomConfig) {
|
|
|
50602
50685
|
};
|
|
50603
50686
|
return entry;
|
|
50604
50687
|
}
|
|
50688
|
+
function resolveWebkiteMcpEntry(_agentName, agentConfig, _switchroomConfig) {
|
|
50689
|
+
if ((agentConfig.mcp_servers ?? {})["webkite"] === false)
|
|
50690
|
+
return null;
|
|
50691
|
+
return {
|
|
50692
|
+
key: "webkite",
|
|
50693
|
+
value: {
|
|
50694
|
+
command: "webkite",
|
|
50695
|
+
args: ["mcp"]
|
|
50696
|
+
}
|
|
50697
|
+
};
|
|
50698
|
+
}
|
|
50605
50699
|
var INTEGRATION_MCP_RESOLVERS = [
|
|
50606
50700
|
{
|
|
50607
50701
|
label: "Google Workspace",
|
|
@@ -50620,6 +50714,12 @@ var INTEGRATION_MCP_RESOLVERS = [
|
|
|
50620
50714
|
emitKey: "notion",
|
|
50621
50715
|
retractionKey: "notion",
|
|
50622
50716
|
resolve: resolveNotionMcpEntry
|
|
50717
|
+
},
|
|
50718
|
+
{
|
|
50719
|
+
label: "Webkite",
|
|
50720
|
+
emitKey: "webkite",
|
|
50721
|
+
retractionKey: "webkite",
|
|
50722
|
+
resolve: resolveWebkiteMcpEntry
|
|
50623
50723
|
}
|
|
50624
50724
|
];
|
|
50625
50725
|
function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchroomConfig, userIdOverride, switchroomConfigPath) {
|
|
@@ -51443,7 +51543,10 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
51443
51543
|
...AGENT_CONFIG_MCP_TOOLS,
|
|
51444
51544
|
...HOSTD_MCP_TOOLS
|
|
51445
51545
|
]);
|
|
51446
|
-
const desiredDeny =
|
|
51546
|
+
const desiredDeny = dedupe2([
|
|
51547
|
+
...tools.deny ?? [],
|
|
51548
|
+
...webkiteDenyForAgent(agentConfig)
|
|
51549
|
+
]);
|
|
51447
51550
|
let topicId = agentConfig.topic_id;
|
|
51448
51551
|
if (topicId === undefined) {
|
|
51449
51552
|
try {
|
|
@@ -13133,6 +13133,18 @@ function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
|
|
|
13133
13133
|
}
|
|
13134
13134
|
|
|
13135
13135
|
// src/vault/broker/acl.ts
|
|
13136
|
+
var WEBKITE_VAULT_KEYS = new Set([
|
|
13137
|
+
"webkite/cloudflare-account-id",
|
|
13138
|
+
"webkite/cloudflare-api-token",
|
|
13139
|
+
"webkite/firecrawl-api-key"
|
|
13140
|
+
]);
|
|
13141
|
+
function isWebkiteCredentialKeyForAgent(agentConfig, key) {
|
|
13142
|
+
if (!WEBKITE_VAULT_KEYS.has(key))
|
|
13143
|
+
return false;
|
|
13144
|
+
if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
|
|
13145
|
+
return false;
|
|
13146
|
+
return true;
|
|
13147
|
+
}
|
|
13136
13148
|
function parseCronUnit(unitName) {
|
|
13137
13149
|
const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
|
|
13138
13150
|
if (!m)
|
|
@@ -13222,6 +13234,9 @@ function checkAclByAgent(config, agentName, key) {
|
|
|
13222
13234
|
if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
|
|
13223
13235
|
return { allow: true };
|
|
13224
13236
|
}
|
|
13237
|
+
if (isWebkiteCredentialKeyForAgent(agentConfig, key)) {
|
|
13238
|
+
return { allow: true };
|
|
13239
|
+
}
|
|
13225
13240
|
const agentBot = agentConfig.bot_token;
|
|
13226
13241
|
const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
|
|
13227
13242
|
if (typeof botRef === "string" && botRef.startsWith("vault:")) {
|
package/package.json
CHANGED
|
@@ -598,6 +598,29 @@ if [ -d "$SR_FLEET_DIR" ]; then
|
|
|
598
598
|
SR_FLEET_ARG="--add-dir $SR_FLEET_DIR"
|
|
599
599
|
fi
|
|
600
600
|
|
|
601
|
+
# Webkite credential pre-auth — fetch Cloudflare + Firecrawl creds from
|
|
602
|
+
# the vault-broker at boot and export into the agent process env so
|
|
603
|
+
# `webkite mcp` (spawned by claude with sanitized env per .mcp.json)
|
|
604
|
+
# inherits them via the agent's env on `exec claude`. Best-effort: a
|
|
605
|
+
# missing key falls webkite back to cloakbrowser-only (no cloud render),
|
|
606
|
+
# which still works for most sites. The broker enforces the per-key
|
|
607
|
+
# ACL — if the operator hasn't `--allow`ed this agent on the key, the
|
|
608
|
+
# get returns empty and webkite stays cloakbrowser-only.
|
|
609
|
+
if command -v switchroom >/dev/null 2>&1; then
|
|
610
|
+
for sr_wk_pair in \
|
|
611
|
+
"CLOUDFLARE_ACCOUNT_ID:webkite/cloudflare-account-id" \
|
|
612
|
+
"CLOUDFLARE_API_TOKEN:webkite/cloudflare-api-token" \
|
|
613
|
+
"FIRECRAWL_API_KEY:webkite/firecrawl-api-key"; do
|
|
614
|
+
sr_wk_env="${sr_wk_pair%%:*}"
|
|
615
|
+
sr_wk_key="${sr_wk_pair##*:}"
|
|
616
|
+
sr_wk_val="$(switchroom vault get "$sr_wk_key" 2>/dev/null || true)"
|
|
617
|
+
if [ -n "$sr_wk_val" ]; then
|
|
618
|
+
export "$sr_wk_env=$sr_wk_val"
|
|
619
|
+
fi
|
|
620
|
+
done
|
|
621
|
+
unset sr_wk_pair sr_wk_env sr_wk_key sr_wk_val
|
|
622
|
+
fi
|
|
623
|
+
|
|
601
624
|
{{#if useSwitchroomPlugin}}
|
|
602
625
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
603
626
|
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
@@ -82,7 +82,7 @@ export interface AnswerStreamConfig {
|
|
|
82
82
|
chatId: string,
|
|
83
83
|
draftId: number,
|
|
84
84
|
text: string,
|
|
85
|
-
params?: { message_thread_id?: number },
|
|
85
|
+
params?: { message_thread_id?: number; parse_mode?: 'HTML' },
|
|
86
86
|
) => Promise<unknown>
|
|
87
87
|
sendMessage: (
|
|
88
88
|
chatId: string,
|
|
@@ -31585,9 +31585,32 @@ function makeEmptyActivityState() {
|
|
|
31585
31585
|
function verbForTool(toolName) {
|
|
31586
31586
|
if (!toolName)
|
|
31587
31587
|
return null;
|
|
31588
|
-
const mcpMatch = /^mcp__(
|
|
31588
|
+
const mcpMatch = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
31589
31589
|
if (mcpMatch && mcpMatch[1] === "switchroom-telegram")
|
|
31590
31590
|
return null;
|
|
31591
|
+
if (mcpMatch) {
|
|
31592
|
+
const server = mcpMatch[1].toLowerCase();
|
|
31593
|
+
const mcpTool = mcpMatch[2].toLowerCase();
|
|
31594
|
+
if (server === "hindsight") {
|
|
31595
|
+
if (mcpTool === "recall" || mcpTool === "reflect")
|
|
31596
|
+
return "searched";
|
|
31597
|
+
if (mcpTool === "retain" || mcpTool === "update_memory" || mcpTool === "sync_retain")
|
|
31598
|
+
return "saved";
|
|
31599
|
+
}
|
|
31600
|
+
if (server === "google-workspace" || server === "claude_ai_google_drive" || server === "claude_ai_gmail" || server === "claude_ai_google_calendar") {
|
|
31601
|
+
if (/^(search|list|query|read|get|fetch|download)/i.test(mcpTool))
|
|
31602
|
+
return "searched";
|
|
31603
|
+
if (/^(create|update|write|send|move|copy|duplicate)/i.test(mcpTool))
|
|
31604
|
+
return "edited";
|
|
31605
|
+
}
|
|
31606
|
+
if (server === "notion" || server === "claude_ai_notion") {
|
|
31607
|
+
const action = mcpTool.replace(/^notion-/, "");
|
|
31608
|
+
if (/^(search|fetch|query|get|read)/i.test(action))
|
|
31609
|
+
return "searched";
|
|
31610
|
+
if (/^(create|update|move|duplicate|comment)/i.test(action))
|
|
31611
|
+
return "edited";
|
|
31612
|
+
}
|
|
31613
|
+
}
|
|
31591
31614
|
const suffix = (mcpMatch ? mcpMatch[2] : toolName).toLowerCase();
|
|
31592
31615
|
switch (suffix) {
|
|
31593
31616
|
case "read":
|
|
@@ -31639,6 +31662,7 @@ var VERB_PHRASE = {
|
|
|
31639
31662
|
fetched: { singular: "fetched a URL", plural: "fetched $N URLs" },
|
|
31640
31663
|
dispatched: { singular: "dispatched a sub-agent", plural: "dispatched $N sub-agents" },
|
|
31641
31664
|
noted: { singular: "updated the todo list", plural: "updated the todo list ($N edits)" },
|
|
31665
|
+
saved: { singular: "saved a memory", plural: "saved $N memories" },
|
|
31642
31666
|
used: { singular: "used a tool", plural: "used $N tools" }
|
|
31643
31667
|
};
|
|
31644
31668
|
function formatSummary(state) {
|
|
@@ -37961,6 +37985,10 @@ function formatFrameworkFallbackText(fallbackKind, silenceMs, inFlightTools = []
|
|
|
37961
37985
|
const dur = formatDurationShort(longest.durationMs);
|
|
37962
37986
|
const labelTail = longest.label && longest.label.length > 0 ? ` ${truncateLabel(longest.label)}` : "";
|
|
37963
37987
|
const more = inFlightTools.length > 1 ? ` + ${inFlightTools.length - 1} more` : "";
|
|
37988
|
+
const isMcpRawName = /^mcp__/.test(longest.name);
|
|
37989
|
+
if (isMcpRawName && labelTail !== "") {
|
|
37990
|
+
return `${truncateLabel(longest.label)}${more} for ${dur} ${suffix}`;
|
|
37991
|
+
}
|
|
37964
37992
|
return `running ${longest.name}${labelTail}${more} for ${dur} ${suffix}`;
|
|
37965
37993
|
}
|
|
37966
37994
|
return fallbackKind === "thinking" ? `still thinking\u2026 ${suffix}` : `still working\u2026 ${suffix}`;
|
|
@@ -49688,10 +49716,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
49688
49716
|
}
|
|
49689
49717
|
|
|
49690
49718
|
// ../src/build-info.ts
|
|
49691
|
-
var VERSION = "0.13.
|
|
49692
|
-
var COMMIT_SHA = "
|
|
49693
|
-
var COMMIT_DATE = "2026-05-
|
|
49694
|
-
var LATEST_PR =
|
|
49719
|
+
var VERSION = "0.13.63";
|
|
49720
|
+
var COMMIT_SHA = "9aaa5939";
|
|
49721
|
+
var COMMIT_DATE = "2026-05-28T03:28:57Z";
|
|
49722
|
+
var LATEST_PR = 1945;
|
|
49695
49723
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
49696
49724
|
|
|
49697
49725
|
// gateway/boot-version.ts
|
|
@@ -53669,7 +53697,7 @@ async function drainActivitySummary(turn) {
|
|
|
53669
53697
|
turn.activityDraftId = allocateDraftId();
|
|
53670
53698
|
}
|
|
53671
53699
|
const draftId = turn.activityDraftId;
|
|
53672
|
-
await sendMessageDraftFn(chat, draftId, html,
|
|
53700
|
+
await sendMessageDraftFn(chat, draftId, html, { parse_mode: "HTML" });
|
|
53673
53701
|
} else if (turn.activityMessageId == null) {
|
|
53674
53702
|
const sent = await robustApiCall(() => bot.api.sendMessage(chat, html, {
|
|
53675
53703
|
...thread != null ? { message_thread_id: thread } : {},
|
|
@@ -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
|
|