switchroom 0.15.1 → 0.15.3
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 +1 -0
- package/dist/auth-broker/index.js +80 -13
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +1784 -1427
- package/dist/cli/ui/index.html +67 -1
- package/dist/host-control/main.js +5 -1
- package/dist/vault/approvals/kernel-server.js +1 -0
- package/dist/vault/broker/server.js +2 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +33 -0
- package/profiles/default/CLAUDE.md.hbs +27 -0
- package/telegram-plugin/dist/gateway/gateway.js +576 -16
- package/telegram-plugin/gateway/gateway.ts +135 -4
- package/telegram-plugin/gateway/model-command.ts +368 -0
- package/telegram-plugin/tests/model-command.test.ts +349 -0
- package/telegram-plugin/uat/scenarios/jtbd-model-command-dm.test.ts +93 -0
- package/telegram-plugin/welcome-text.ts +7 -1
|
@@ -24194,6 +24194,7 @@ var init_schema = __esm(() => {
|
|
|
24194
24194
|
dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
|
|
24195
24195
|
network_isolation: NetworkIsolationSchema,
|
|
24196
24196
|
admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently \u2014 " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
|
|
24197
|
+
root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet \u2014 read any agent's logs, " + "docker exec into peers, edit host files \u2014 instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent \u2014 it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
|
|
24197
24198
|
settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only \u2014 prefer the typed fields when they exist."),
|
|
24198
24199
|
claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
|
|
24199
24200
|
cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
|
|
@@ -43042,6 +43043,7 @@ var TELEGRAM_MENU_COMMANDS = [
|
|
|
43042
43043
|
{ command: "version", description: "Show versions + running agent health" },
|
|
43043
43044
|
{ command: "logs", description: "Show recent agent logs" },
|
|
43044
43045
|
{ command: "inject", description: "Inject a Claude Code slash command (e.g. /cost)" },
|
|
43046
|
+
{ command: "model", description: "Show or switch the Claude model" },
|
|
43045
43047
|
{ command: "doctor", description: "Health check (deps, services, MCP)" },
|
|
43046
43048
|
{ command: "usage", description: "Pro/Max plan quota (5h + 7d windows)" },
|
|
43047
43049
|
{ command: "vault", description: "Manage vault secrets + capability grants" },
|
|
@@ -43081,6 +43083,8 @@ function switchroomHelpText(agentName3) {
|
|
|
43081
43083
|
`<code>/auth list [agent]</code> \u2014 list account slots and health`,
|
|
43082
43084
|
`<code>/auth use [agent] <slot></code> \u2014 switch active slot and restart`,
|
|
43083
43085
|
`<code>/auth rm [agent] <slot> [--force]</code> \u2014 remove a slot`,
|
|
43086
|
+
`<code>/model</code> \u2014 show the configured Claude model`,
|
|
43087
|
+
`<code>/model <name></code> \u2014 switch the live session's model (opus \u00b7 sonnet \u00b7 haiku or a full id; until restart)`,
|
|
43084
43088
|
`<code>/topics</code> \u2014 topic-to-agent mappings`,
|
|
43085
43089
|
`<code>/permissions [agent]</code> \u2014 show agent permissions`,
|
|
43086
43090
|
`<code>/grant <tool></code> \u2014 grant a tool permission`,
|
|
@@ -44324,6 +44328,10 @@ async function fetchQuota2(opts) {
|
|
|
44324
44328
|
}
|
|
44325
44329
|
return parsed;
|
|
44326
44330
|
}
|
|
44331
|
+
function formatQuotaLine2(q) {
|
|
44332
|
+
const fmt = (n) => `${Math.round(n)}%`;
|
|
44333
|
+
return `${fmt(q.fiveHourUtilizationPct)} / 5h \u00b7 ${fmt(q.sevenDayUtilizationPct)} / 7d`;
|
|
44334
|
+
}
|
|
44327
44335
|
function formatResetRelative2(target, now = new Date) {
|
|
44328
44336
|
if (!target)
|
|
44329
44337
|
return "\u2014";
|
|
@@ -44395,6 +44403,22 @@ var TOKEN_VALIDITY_MS2 = 365 * 24 * 60 * 60000;
|
|
|
44395
44403
|
// ../src/agents/inject.ts
|
|
44396
44404
|
import { execFile, execFileSync } from "node:child_process";
|
|
44397
44405
|
import { promisify } from "node:util";
|
|
44406
|
+
|
|
44407
|
+
// ../src/agents/pane-lock.ts
|
|
44408
|
+
var tails = new Map;
|
|
44409
|
+
function withPaneLock(key, fn) {
|
|
44410
|
+
const tail = tails.get(key) ?? Promise.resolve();
|
|
44411
|
+
const run2 = tail.then(() => fn());
|
|
44412
|
+
const next = run2.then(() => {}, () => {});
|
|
44413
|
+
tails.set(key, next);
|
|
44414
|
+
next.then(() => {
|
|
44415
|
+
if (tails.get(key) === next)
|
|
44416
|
+
tails.delete(key);
|
|
44417
|
+
});
|
|
44418
|
+
return run2;
|
|
44419
|
+
}
|
|
44420
|
+
|
|
44421
|
+
// ../src/agents/inject.ts
|
|
44398
44422
|
var execFileAsync = promisify(execFile);
|
|
44399
44423
|
var INJECT_COMMANDS = new Map([
|
|
44400
44424
|
["/cost", { description: "Show session cost", expectsOutput: true }],
|
|
@@ -44544,13 +44568,13 @@ async function injectSlashCommand(agentName3, command, opts = {}) {
|
|
|
44544
44568
|
const session = opts.sessionName ?? agentName3;
|
|
44545
44569
|
const settleMs = opts.settleMs ?? 2000;
|
|
44546
44570
|
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
44547
|
-
return injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
|
|
44571
|
+
return withPaneLock(`${socket}:${session}`, () => injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
|
|
44548
44572
|
socket,
|
|
44549
44573
|
session,
|
|
44550
44574
|
command: command.trim(),
|
|
44551
44575
|
settleMs,
|
|
44552
44576
|
timeoutMs
|
|
44553
|
-
});
|
|
44577
|
+
}));
|
|
44554
44578
|
}
|
|
44555
44579
|
async function injectSlashCommandWith(runner, args) {
|
|
44556
44580
|
const { socket, session, command, settleMs, timeoutMs } = args;
|
|
@@ -44681,6 +44705,34 @@ var INJECT_BLOCKED2 = new Map([
|
|
|
44681
44705
|
["/quit", { reason: "would kill the agent process" }]
|
|
44682
44706
|
]);
|
|
44683
44707
|
var INJECT_BLOCKLIST2 = new Set(INJECT_BLOCKED2.keys());
|
|
44708
|
+
function makeTmuxRunner2(tmuxBin) {
|
|
44709
|
+
return {
|
|
44710
|
+
capture(socket, session) {
|
|
44711
|
+
try {
|
|
44712
|
+
return execFileSync2(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
44713
|
+
} catch {
|
|
44714
|
+
return null;
|
|
44715
|
+
}
|
|
44716
|
+
},
|
|
44717
|
+
send(socket, session, args) {
|
|
44718
|
+
const [subcmd, ...rest] = args;
|
|
44719
|
+
const flagEnd = rest.findIndex((a) => !a.startsWith("-"));
|
|
44720
|
+
const flagsBeforeKeys = flagEnd === -1 ? rest : rest.slice(0, flagEnd);
|
|
44721
|
+
const keys = flagEnd === -1 ? [] : rest.slice(flagEnd);
|
|
44722
|
+
execFileSync2(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
|
|
44723
|
+
},
|
|
44724
|
+
hasSession(socket, session) {
|
|
44725
|
+
try {
|
|
44726
|
+
execFileSync2(tmuxBin, ["-L", socket, "has-session", "-t", session], {
|
|
44727
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
44728
|
+
});
|
|
44729
|
+
return true;
|
|
44730
|
+
} catch {
|
|
44731
|
+
return false;
|
|
44732
|
+
}
|
|
44733
|
+
}
|
|
44734
|
+
};
|
|
44735
|
+
}
|
|
44684
44736
|
|
|
44685
44737
|
// stream-reply-handler.ts
|
|
44686
44738
|
function buildAccentHeader2(accent) {
|
|
@@ -44811,6 +44863,430 @@ Allowed: <code>${deps.escapeHtml(allow)}</code>`, { html: true });
|
|
|
44811
44863
|
await deps.reply(ctx, finalText, { html: true, accent });
|
|
44812
44864
|
}
|
|
44813
44865
|
|
|
44866
|
+
// ../src/agents/model-picker.ts
|
|
44867
|
+
function labelTag(label) {
|
|
44868
|
+
let h = 2166136261;
|
|
44869
|
+
for (const ch of label) {
|
|
44870
|
+
h ^= ch.codePointAt(0);
|
|
44871
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
44872
|
+
}
|
|
44873
|
+
return h.toString(16).padStart(8, "0");
|
|
44874
|
+
}
|
|
44875
|
+
|
|
44876
|
+
// gateway/model-command.ts
|
|
44877
|
+
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "default"];
|
|
44878
|
+
var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,99}$/;
|
|
44879
|
+
function isValidModelArg(arg) {
|
|
44880
|
+
return MODEL_ARG_RE.test(arg);
|
|
44881
|
+
}
|
|
44882
|
+
function parseModelCommand(text) {
|
|
44883
|
+
const m = text.match(/^\/model(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
|
|
44884
|
+
if (!m)
|
|
44885
|
+
return null;
|
|
44886
|
+
const rest = (m[1] ?? "").trim();
|
|
44887
|
+
if (rest.length === 0)
|
|
44888
|
+
return { kind: "show" };
|
|
44889
|
+
const parts = rest.split(/\s+/);
|
|
44890
|
+
if (parts.length > 1) {
|
|
44891
|
+
return { kind: "help", reason: "model takes a single argument" };
|
|
44892
|
+
}
|
|
44893
|
+
const arg = parts[0];
|
|
44894
|
+
if (arg.toLowerCase() === "help")
|
|
44895
|
+
return { kind: "help" };
|
|
44896
|
+
if (!isValidModelArg(arg)) {
|
|
44897
|
+
return { kind: "help", reason: `not a valid model name: ${arg}` };
|
|
44898
|
+
}
|
|
44899
|
+
return { kind: "set", model: arg };
|
|
44900
|
+
}
|
|
44901
|
+
var PERSIST_NOTE = "<i>Session-only \u2014 lasts until restart. To persist, set <code>model:</code> in switchroom.yaml and restart.</i>";
|
|
44902
|
+
function helpText2(deps, reason) {
|
|
44903
|
+
const lines = [];
|
|
44904
|
+
if (reason)
|
|
44905
|
+
lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
|
|
44906
|
+
lines.push("<b>/model</b> \u2014 show or switch the Claude model", "<code>/model</code> \u2014 show the configured model", `<code>/model <name></code> \u2014 switch the live session (${MODEL_ALIASES.map((a) => `<code>${a}</code>`).join(" \u00b7 ")} or a full model id)`, PERSIST_NOTE);
|
|
44907
|
+
return { text: lines.join(`
|
|
44908
|
+
`), html: true };
|
|
44909
|
+
}
|
|
44910
|
+
async function handleModelCommand(parsed, deps) {
|
|
44911
|
+
if (parsed.kind === "help")
|
|
44912
|
+
return helpText2(deps, parsed.reason);
|
|
44913
|
+
if (parsed.kind === "show") {
|
|
44914
|
+
const configured = deps.getConfiguredModel();
|
|
44915
|
+
const shown = configured && configured.length > 0 ? configured : "default";
|
|
44916
|
+
return {
|
|
44917
|
+
text: [
|
|
44918
|
+
`<b>Model \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`,
|
|
44919
|
+
`Configured: <code>${deps.escapeHtml(shown)}</code>`,
|
|
44920
|
+
`Switch the live session: ${MODEL_ALIASES.map((a) => `<code>/model ${a}</code>`).join(" \u00b7 ")}`,
|
|
44921
|
+
"or <code>/model <full-model-id></code>",
|
|
44922
|
+
PERSIST_NOTE
|
|
44923
|
+
].join(`
|
|
44924
|
+
`),
|
|
44925
|
+
html: true
|
|
44926
|
+
};
|
|
44927
|
+
}
|
|
44928
|
+
if (!isValidModelArg(parsed.model)) {
|
|
44929
|
+
return helpText2(deps, `not a valid model name: ${parsed.model}`);
|
|
44930
|
+
}
|
|
44931
|
+
const verbHtml = `<code>/model ${deps.escapeHtml(parsed.model)}</code>`;
|
|
44932
|
+
let result;
|
|
44933
|
+
try {
|
|
44934
|
+
result = await deps.inject(deps.getAgentName(), `/model ${parsed.model}`);
|
|
44935
|
+
} catch (err) {
|
|
44936
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
44937
|
+
return {
|
|
44938
|
+
text: `\u274c ${verbHtml} \u2014 inject failed: ${deps.escapeHtml(msg)}`,
|
|
44939
|
+
html: true
|
|
44940
|
+
};
|
|
44941
|
+
}
|
|
44942
|
+
if (result.outcome === "ok") {
|
|
44943
|
+
return {
|
|
44944
|
+
text: [
|
|
44945
|
+
`${verbHtml}`,
|
|
44946
|
+
deps.preBlock(result.output),
|
|
44947
|
+
...result.truncated ? ["<i>truncated</i>"] : [],
|
|
44948
|
+
PERSIST_NOTE
|
|
44949
|
+
].join(`
|
|
44950
|
+
`),
|
|
44951
|
+
html: true
|
|
44952
|
+
};
|
|
44953
|
+
}
|
|
44954
|
+
if (result.outcome === "ok_no_output") {
|
|
44955
|
+
return {
|
|
44956
|
+
text: [
|
|
44957
|
+
`${verbHtml} \u2014 sent, but no response captured. The agent may be mid-turn; check <code>/inject /status</code> to confirm the active model.`,
|
|
44958
|
+
PERSIST_NOTE
|
|
44959
|
+
].join(`
|
|
44960
|
+
`),
|
|
44961
|
+
html: true
|
|
44962
|
+
};
|
|
44963
|
+
}
|
|
44964
|
+
if (result.errorCode === "session_missing") {
|
|
44965
|
+
return {
|
|
44966
|
+
text: "\u274c tmux session not found \u2014 the agent must be running under the tmux supervisor (the default). Remove <code>experimental.legacy_pty: true</code> if set.",
|
|
44967
|
+
html: true
|
|
44968
|
+
};
|
|
44969
|
+
}
|
|
44970
|
+
return {
|
|
44971
|
+
text: `\u274c ${verbHtml} \u2014 ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`,
|
|
44972
|
+
html: true
|
|
44973
|
+
};
|
|
44974
|
+
}
|
|
44975
|
+
var MODEL_CALLBACK_PREFIX = "mdl:";
|
|
44976
|
+
var MODEL_CALLBACK_SELECT = "mdl:s:";
|
|
44977
|
+
var MODEL_CALLBACK_REFRESH = "mdl:r";
|
|
44978
|
+
function modelSelectCallbackData(label) {
|
|
44979
|
+
return `${MODEL_CALLBACK_SELECT}${labelTag(label)}`;
|
|
44980
|
+
}
|
|
44981
|
+
function busyReply(deps) {
|
|
44982
|
+
return {
|
|
44983
|
+
text: "\u23f3 The agent is mid-turn \u2014 the model picker needs an idle prompt. Try again in a moment.",
|
|
44984
|
+
html: true
|
|
44985
|
+
};
|
|
44986
|
+
}
|
|
44987
|
+
function menuKeyboard(options) {
|
|
44988
|
+
const rows = options.map((o) => [
|
|
44989
|
+
{
|
|
44990
|
+
text: o.current ? `\u2705 ${o.label}` : o.label,
|
|
44991
|
+
callback_data: modelSelectCallbackData(o.label)
|
|
44992
|
+
}
|
|
44993
|
+
]);
|
|
44994
|
+
rows.push([{ text: "\uD83D\uDD04 Refresh", callback_data: MODEL_CALLBACK_REFRESH }]);
|
|
44995
|
+
return rows;
|
|
44996
|
+
}
|
|
44997
|
+
async function buildModelMenu(deps) {
|
|
44998
|
+
if (deps.isBusy())
|
|
44999
|
+
return busyReply(deps);
|
|
45000
|
+
const [discovered, quota] = await Promise.all([
|
|
45001
|
+
deps.discover(deps.getAgentName()),
|
|
45002
|
+
deps.getQuotaBrief().catch(() => null)
|
|
45003
|
+
]);
|
|
45004
|
+
if (!discovered.ok) {
|
|
45005
|
+
const v1 = await handleModelCommand({ kind: "show" }, deps);
|
|
45006
|
+
return {
|
|
45007
|
+
text: [`<i>(picker unavailable: ${deps.escapeHtml(discovered.reason)})</i>`, v1.text].join(`
|
|
45008
|
+
`),
|
|
45009
|
+
html: true
|
|
45010
|
+
};
|
|
45011
|
+
}
|
|
45012
|
+
const current = discovered.options.find((o) => o.current);
|
|
45013
|
+
const lines = [`<b>Model \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`];
|
|
45014
|
+
if (discovered.dismissFailed) {
|
|
45015
|
+
lines.push("\u26a0\ufe0f <i>The picker may still be open on the agent pane \u2014 check it before switching.</i>");
|
|
45016
|
+
}
|
|
45017
|
+
if (current) {
|
|
45018
|
+
const detail = current.detail ? ` \u00b7 ${deps.escapeHtml(current.detail)}` : "";
|
|
45019
|
+
lines.push(`Now: <b>${deps.escapeHtml(current.label)}</b>${detail}`);
|
|
45020
|
+
} else {
|
|
45021
|
+
lines.push("Now: <i>unknown (no \u2714 row in picker)</i>");
|
|
45022
|
+
}
|
|
45023
|
+
if (quota)
|
|
45024
|
+
lines.push(`Quota: ${deps.escapeHtml(quota)}`);
|
|
45025
|
+
lines.push("", "Tap to switch (applies to the live session):");
|
|
45026
|
+
lines.push(PERSIST_NOTE);
|
|
45027
|
+
return { text: lines.join(`
|
|
45028
|
+
`), html: true, keyboard: menuKeyboard(discovered.options) };
|
|
45029
|
+
}
|
|
45030
|
+
async function handleModelMenuCallback(data, deps) {
|
|
45031
|
+
if (data === MODEL_CALLBACK_REFRESH) {
|
|
45032
|
+
return { answer: "Refreshed", reply: await buildModelMenu(deps) };
|
|
45033
|
+
}
|
|
45034
|
+
if (!data.startsWith(MODEL_CALLBACK_SELECT)) {
|
|
45035
|
+
return { answer: "Unknown action", reply: await buildModelMenu(deps) };
|
|
45036
|
+
}
|
|
45037
|
+
if (deps.isBusy()) {
|
|
45038
|
+
return { answer: "Agent is mid-turn \u2014 try again shortly", reply: busyReply(deps) };
|
|
45039
|
+
}
|
|
45040
|
+
const tag = data.slice(MODEL_CALLBACK_SELECT.length);
|
|
45041
|
+
const discovered = await deps.discover(deps.getAgentName());
|
|
45042
|
+
if (!discovered.ok) {
|
|
45043
|
+
return {
|
|
45044
|
+
answer: "Picker unavailable",
|
|
45045
|
+
reply: {
|
|
45046
|
+
text: `\u274c Could not open the model picker: ${deps.escapeHtml(discovered.reason)}`,
|
|
45047
|
+
html: true
|
|
45048
|
+
}
|
|
45049
|
+
};
|
|
45050
|
+
}
|
|
45051
|
+
const target = discovered.options.find((o) => labelTag(o.label) === tag);
|
|
45052
|
+
if (!target) {
|
|
45053
|
+
const fresh2 = await buildModelMenu(deps);
|
|
45054
|
+
return { answer: "Model list changed \u2014 menu refreshed", reply: fresh2 };
|
|
45055
|
+
}
|
|
45056
|
+
if (target.current) {
|
|
45057
|
+
const fresh2 = await buildModelMenu(deps);
|
|
45058
|
+
return { answer: `Already on ${target.label}`, reply: fresh2 };
|
|
45059
|
+
}
|
|
45060
|
+
const result = await deps.select(deps.getAgentName(), target.label);
|
|
45061
|
+
if (!result.ok) {
|
|
45062
|
+
return {
|
|
45063
|
+
answer: "Switch failed",
|
|
45064
|
+
reply: {
|
|
45065
|
+
text: `\u274c Switch to <b>${deps.escapeHtml(target.label)}</b> failed: ${deps.escapeHtml(result.reason)}`,
|
|
45066
|
+
html: true
|
|
45067
|
+
}
|
|
45068
|
+
};
|
|
45069
|
+
}
|
|
45070
|
+
const fresh = await buildModelMenu(deps);
|
|
45071
|
+
const confirmed = {
|
|
45072
|
+
text: [`\u2705 ${deps.escapeHtml(result.confirmation)}`, "", fresh.text].join(`
|
|
45073
|
+
`),
|
|
45074
|
+
html: true,
|
|
45075
|
+
...fresh.keyboard ? { keyboard: fresh.keyboard } : {}
|
|
45076
|
+
};
|
|
45077
|
+
return { answer: result.confirmation, reply: confirmed };
|
|
45078
|
+
}
|
|
45079
|
+
|
|
45080
|
+
// ../src/agents/model-picker.ts
|
|
45081
|
+
var HEADER_RE = /Select model/;
|
|
45082
|
+
var OPTION_RE = /^\s*(\u276f)?\s*(\d+)\.\s+(.*)$/;
|
|
45083
|
+
var FOOTER_RE = /Esc to cancel/i;
|
|
45084
|
+
var EFFORT_RE = /^\s*\u25cb\s/;
|
|
45085
|
+
function parseModelPicker(pane) {
|
|
45086
|
+
const lines = pane.split(`
|
|
45087
|
+
`);
|
|
45088
|
+
let headerIdx = -1;
|
|
45089
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
45090
|
+
if (HEADER_RE.test(lines[i])) {
|
|
45091
|
+
headerIdx = i;
|
|
45092
|
+
break;
|
|
45093
|
+
}
|
|
45094
|
+
}
|
|
45095
|
+
if (headerIdx < 0)
|
|
45096
|
+
return null;
|
|
45097
|
+
const options = [];
|
|
45098
|
+
let cursorIndex = -1;
|
|
45099
|
+
let footerSeen = false;
|
|
45100
|
+
for (let i = headerIdx + 1;i < lines.length; i++) {
|
|
45101
|
+
const raw = lines[i];
|
|
45102
|
+
if (FOOTER_RE.test(raw)) {
|
|
45103
|
+
footerSeen = true;
|
|
45104
|
+
break;
|
|
45105
|
+
}
|
|
45106
|
+
if (EFFORT_RE.test(raw))
|
|
45107
|
+
continue;
|
|
45108
|
+
const m = OPTION_RE.exec(raw);
|
|
45109
|
+
if (m) {
|
|
45110
|
+
const index = Number(m[2]);
|
|
45111
|
+
const rest = m[3].trimEnd();
|
|
45112
|
+
const gap = rest.search(/\s{2,}/);
|
|
45113
|
+
let label = (gap >= 0 ? rest.slice(0, gap) : rest).trim();
|
|
45114
|
+
const detail = gap >= 0 ? rest.slice(gap).trim() : "";
|
|
45115
|
+
let current = false;
|
|
45116
|
+
if (/[\u2714\u2713]$/.test(label)) {
|
|
45117
|
+
current = true;
|
|
45118
|
+
label = label.replace(/\s*[\u2714\u2713]$/, "").trim();
|
|
45119
|
+
}
|
|
45120
|
+
if (label.length === 0)
|
|
45121
|
+
continue;
|
|
45122
|
+
options.push({ index, label, detail, current });
|
|
45123
|
+
if (m[1])
|
|
45124
|
+
cursorIndex = index;
|
|
45125
|
+
continue;
|
|
45126
|
+
}
|
|
45127
|
+
if (options.length > 0 && /^\s{4,}\S/.test(raw)) {
|
|
45128
|
+
const last = options[options.length - 1];
|
|
45129
|
+
last.detail = `${last.detail} ${raw.trim()}`.trim();
|
|
45130
|
+
continue;
|
|
45131
|
+
}
|
|
45132
|
+
}
|
|
45133
|
+
if (options.length === 0)
|
|
45134
|
+
return null;
|
|
45135
|
+
return { options, cursorIndex, footerSeen };
|
|
45136
|
+
}
|
|
45137
|
+
var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
45138
|
+
function makeIo(agentName3, opts) {
|
|
45139
|
+
return {
|
|
45140
|
+
runner: opts._runner ?? makeTmuxRunner2(opts.tmuxBin ?? "tmux"),
|
|
45141
|
+
socket: opts.socketName ?? `switchroom-${agentName3}`,
|
|
45142
|
+
session: opts.sessionName ?? agentName3,
|
|
45143
|
+
stepMs: opts.stepMs ?? 600,
|
|
45144
|
+
timeoutMs: opts.timeoutMs ?? 1e4,
|
|
45145
|
+
sleep: opts._sleep ?? realSleep,
|
|
45146
|
+
log: opts._log ?? ((line) => process.stderr.write(`${line}
|
|
45147
|
+
`)),
|
|
45148
|
+
startedAt: Date.now()
|
|
45149
|
+
};
|
|
45150
|
+
}
|
|
45151
|
+
async function dismissOrWarn(io, verb) {
|
|
45152
|
+
const dismissed = await dismissPicker(io);
|
|
45153
|
+
if (!dismissed) {
|
|
45154
|
+
io.log(`model-picker: ${verb}: picker may still be open after Esc retries ` + `(socket=${io.socket} session=${io.session}) \u2014 pane needs eyes`);
|
|
45155
|
+
}
|
|
45156
|
+
return dismissed;
|
|
45157
|
+
}
|
|
45158
|
+
function expired(io) {
|
|
45159
|
+
return Date.now() - io.startedAt >= io.timeoutMs;
|
|
45160
|
+
}
|
|
45161
|
+
function sendLiteral(io, text) {
|
|
45162
|
+
io.runner.send(io.socket, io.session, ["send-keys", "-l", text]);
|
|
45163
|
+
}
|
|
45164
|
+
function sendKey(io, key) {
|
|
45165
|
+
io.runner.send(io.socket, io.session, ["send-keys", key]);
|
|
45166
|
+
}
|
|
45167
|
+
async function openPicker(io) {
|
|
45168
|
+
sendLiteral(io, "/model");
|
|
45169
|
+
sendKey(io, "Enter");
|
|
45170
|
+
for (;; ) {
|
|
45171
|
+
await io.sleep(io.stepMs);
|
|
45172
|
+
const pane = io.runner.capture(io.socket, io.session) ?? "";
|
|
45173
|
+
const parsed = parseModelPicker(pane);
|
|
45174
|
+
if (parsed?.footerSeen)
|
|
45175
|
+
return parsed;
|
|
45176
|
+
if (expired(io))
|
|
45177
|
+
return parsed;
|
|
45178
|
+
}
|
|
45179
|
+
}
|
|
45180
|
+
async function dismissPicker(io) {
|
|
45181
|
+
for (let attempt = 0;attempt < 2; attempt++) {
|
|
45182
|
+
try {
|
|
45183
|
+
sendKey(io, "Escape");
|
|
45184
|
+
} catch {
|
|
45185
|
+
return false;
|
|
45186
|
+
}
|
|
45187
|
+
await io.sleep(io.stepMs);
|
|
45188
|
+
const pane = io.runner.capture(io.socket, io.session) ?? "";
|
|
45189
|
+
const parsed = parseModelPicker(pane);
|
|
45190
|
+
if (!parsed || !parsed.footerSeen)
|
|
45191
|
+
return true;
|
|
45192
|
+
const tail = pane.slice(pane.lastIndexOf("Esc to cancel"));
|
|
45193
|
+
if (tail.split(`
|
|
45194
|
+
`).slice(1).some((l) => l.trim().length > 0))
|
|
45195
|
+
return true;
|
|
45196
|
+
}
|
|
45197
|
+
return false;
|
|
45198
|
+
}
|
|
45199
|
+
async function discoverModels(agentName3, opts = {}) {
|
|
45200
|
+
const io = makeIo(agentName3, opts);
|
|
45201
|
+
if (!io.runner.hasSession(io.socket, io.session)) {
|
|
45202
|
+
return { ok: false, reason: "tmux session not found" };
|
|
45203
|
+
}
|
|
45204
|
+
return withPaneLock(`${io.socket}:${io.session}`, async () => {
|
|
45205
|
+
io.startedAt = Date.now();
|
|
45206
|
+
let parsed = null;
|
|
45207
|
+
let dismissed = true;
|
|
45208
|
+
try {
|
|
45209
|
+
parsed = await openPicker(io);
|
|
45210
|
+
} finally {
|
|
45211
|
+
dismissed = await dismissOrWarn(io, "discover");
|
|
45212
|
+
}
|
|
45213
|
+
const tail = dismissed ? {} : { dismissFailed: true };
|
|
45214
|
+
if (!parsed || !parsed.footerSeen) {
|
|
45215
|
+
return {
|
|
45216
|
+
ok: false,
|
|
45217
|
+
reason: "picker did not render \u2014 agent may be mid-turn or the CLI changed its /model UI",
|
|
45218
|
+
...tail
|
|
45219
|
+
};
|
|
45220
|
+
}
|
|
45221
|
+
const current = parsed.options.find((o) => o.current) ?? null;
|
|
45222
|
+
return { ok: true, options: parsed.options, currentLabel: current?.label ?? null, ...tail };
|
|
45223
|
+
});
|
|
45224
|
+
}
|
|
45225
|
+
async function selectModel(agentName3, targetLabel, opts = {}) {
|
|
45226
|
+
const io = makeIo(agentName3, opts);
|
|
45227
|
+
if (!io.runner.hasSession(io.socket, io.session)) {
|
|
45228
|
+
return { ok: false, reason: "tmux session not found" };
|
|
45229
|
+
}
|
|
45230
|
+
return withPaneLock(`${io.socket}:${io.session}`, async () => {
|
|
45231
|
+
io.startedAt = Date.now();
|
|
45232
|
+
let selected = false;
|
|
45233
|
+
try {
|
|
45234
|
+
const parsed = await openPicker(io);
|
|
45235
|
+
if (!parsed || !parsed.footerSeen) {
|
|
45236
|
+
return { ok: false, reason: "picker did not render \u2014 agent may be mid-turn" };
|
|
45237
|
+
}
|
|
45238
|
+
const target = parsed.options.find((o) => o.label === targetLabel);
|
|
45239
|
+
if (!target) {
|
|
45240
|
+
const have = parsed.options.map((o) => o.label).join(", ");
|
|
45241
|
+
return { ok: false, reason: `option "${targetLabel}" not offered (have: ${have})` };
|
|
45242
|
+
}
|
|
45243
|
+
if (parsed.cursorIndex < 0) {
|
|
45244
|
+
return { ok: false, reason: "cursor marker not visible \u2014 refusing blind navigation" };
|
|
45245
|
+
}
|
|
45246
|
+
const delta = target.index - parsed.cursorIndex;
|
|
45247
|
+
const key = delta > 0 ? "Down" : "Up";
|
|
45248
|
+
for (let i = 0;i < Math.abs(delta); i++) {
|
|
45249
|
+
if (expired(io))
|
|
45250
|
+
return { ok: false, reason: "timed out navigating picker" };
|
|
45251
|
+
sendKey(io, key);
|
|
45252
|
+
await io.sleep(Math.min(io.stepMs, 250));
|
|
45253
|
+
}
|
|
45254
|
+
await io.sleep(io.stepMs);
|
|
45255
|
+
const verifyPane = io.runner.capture(io.socket, io.session) ?? "";
|
|
45256
|
+
const verify = parseModelPicker(verifyPane);
|
|
45257
|
+
const atRow = verify?.options.find((o) => o.index === verify.cursorIndex);
|
|
45258
|
+
if (!verify || !atRow || atRow.label !== targetLabel) {
|
|
45259
|
+
return {
|
|
45260
|
+
ok: false,
|
|
45261
|
+
reason: `cursor verification failed (on "${atRow?.label ?? "?"}", wanted "${targetLabel}")`
|
|
45262
|
+
};
|
|
45263
|
+
}
|
|
45264
|
+
sendLiteral(io, "s");
|
|
45265
|
+
selected = true;
|
|
45266
|
+
await io.sleep(io.stepMs);
|
|
45267
|
+
const after = io.runner.capture(io.socket, io.session) ?? "";
|
|
45268
|
+
const confirmation = extractConfirmation(after) ?? `Switched to ${targetLabel} (session)`;
|
|
45269
|
+
return { ok: true, confirmation };
|
|
45270
|
+
} catch (err) {
|
|
45271
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
45272
|
+
} finally {
|
|
45273
|
+
if (!selected) {
|
|
45274
|
+
await dismissOrWarn(io, "select");
|
|
45275
|
+
}
|
|
45276
|
+
}
|
|
45277
|
+
});
|
|
45278
|
+
}
|
|
45279
|
+
function extractConfirmation(pane) {
|
|
45280
|
+
const lines = pane.split(`
|
|
45281
|
+
`);
|
|
45282
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
45283
|
+
const t = lines[i].replace(/^\s*\u23bf\s*/, "").trim();
|
|
45284
|
+
if (/^(Set model to|Kept model as|Switched to)/i.test(t))
|
|
45285
|
+
return t;
|
|
45286
|
+
}
|
|
45287
|
+
return null;
|
|
45288
|
+
}
|
|
45289
|
+
|
|
44814
45290
|
// ../src/config/loader.ts
|
|
44815
45291
|
init_dist();
|
|
44816
45292
|
init_zod();
|
|
@@ -50872,7 +51348,7 @@ function startSubagentWatcher(config) {
|
|
|
50872
51348
|
watch
|
|
50873
51349
|
};
|
|
50874
51350
|
const registry = new Map;
|
|
50875
|
-
const
|
|
51351
|
+
const tails2 = new Map;
|
|
50876
51352
|
const dirWatchers = new Map;
|
|
50877
51353
|
const knownFiles = new Set;
|
|
50878
51354
|
const pendingCloses = new Map;
|
|
@@ -50918,7 +51394,7 @@ function startSubagentWatcher(config) {
|
|
|
50918
51394
|
hasEmittedStart: false,
|
|
50919
51395
|
watcher: null
|
|
50920
51396
|
};
|
|
50921
|
-
|
|
51397
|
+
tails2.set(agentId, tail);
|
|
50922
51398
|
readSubTail(entry, tail, n, (desc) => {
|
|
50923
51399
|
log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`);
|
|
50924
51400
|
}, fs2, log, db2, parentStateDir, config.onUnstall, undefined, config.onProgress);
|
|
@@ -50956,7 +51432,7 @@ function startSubagentWatcher(config) {
|
|
|
50956
51432
|
if (stopped)
|
|
50957
51433
|
return;
|
|
50958
51434
|
const entry2 = registry.get(agentId);
|
|
50959
|
-
const t =
|
|
51435
|
+
const t = tails2.get(agentId);
|
|
50960
51436
|
if (!entry2 || !t)
|
|
50961
51437
|
return;
|
|
50962
51438
|
readSubTail(entry2, t, nowFn(), (desc) => {
|
|
@@ -51025,14 +51501,14 @@ function startSubagentWatcher(config) {
|
|
|
51025
51501
|
pendingCloses.set(agentId, handle);
|
|
51026
51502
|
}
|
|
51027
51503
|
function cleanupTerminalAgent(agentId) {
|
|
51028
|
-
const tail =
|
|
51504
|
+
const tail = tails2.get(agentId);
|
|
51029
51505
|
if (tail?.watcher) {
|
|
51030
51506
|
try {
|
|
51031
51507
|
tail.watcher.close();
|
|
51032
51508
|
} catch {}
|
|
51033
51509
|
tail.watcher = null;
|
|
51034
51510
|
}
|
|
51035
|
-
|
|
51511
|
+
tails2.delete(agentId);
|
|
51036
51512
|
const entry = registry.get(agentId);
|
|
51037
51513
|
if (entry?.filePath) {
|
|
51038
51514
|
knownFiles.delete(entry.filePath);
|
|
@@ -51202,7 +51678,7 @@ function startSubagentWatcher(config) {
|
|
|
51202
51678
|
for (const [agentId, entry] of registry) {
|
|
51203
51679
|
if (entry.state !== "running")
|
|
51204
51680
|
continue;
|
|
51205
|
-
const tail =
|
|
51681
|
+
const tail = tails2.get(agentId);
|
|
51206
51682
|
if (!tail)
|
|
51207
51683
|
continue;
|
|
51208
51684
|
readSubTail(entry, tail, n, (desc) => {
|
|
@@ -51244,7 +51720,7 @@ function startSubagentWatcher(config) {
|
|
|
51244
51720
|
} catch {}
|
|
51245
51721
|
}
|
|
51246
51722
|
dirWatchers.clear();
|
|
51247
|
-
for (const tail of
|
|
51723
|
+
for (const tail of tails2.values()) {
|
|
51248
51724
|
if (tail.watcher) {
|
|
51249
51725
|
try {
|
|
51250
51726
|
tail.watcher.close();
|
|
@@ -51252,7 +51728,7 @@ function startSubagentWatcher(config) {
|
|
|
51252
51728
|
tail.watcher = null;
|
|
51253
51729
|
}
|
|
51254
51730
|
}
|
|
51255
|
-
|
|
51731
|
+
tails2.clear();
|
|
51256
51732
|
registry.clear();
|
|
51257
51733
|
knownFiles.clear();
|
|
51258
51734
|
terminatedAgentIds.clear();
|
|
@@ -53078,10 +53554,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
53078
53554
|
}
|
|
53079
53555
|
|
|
53080
53556
|
// ../src/build-info.ts
|
|
53081
|
-
var VERSION = "0.
|
|
53082
|
-
var COMMIT_SHA = "
|
|
53083
|
-
var COMMIT_DATE = "2026-06-
|
|
53084
|
-
var LATEST_PR =
|
|
53557
|
+
var VERSION = "0.15.3";
|
|
53558
|
+
var COMMIT_SHA = "af652154";
|
|
53559
|
+
var COMMIT_DATE = "2026-06-10T09:15:23Z";
|
|
53560
|
+
var LATEST_PR = 2266;
|
|
53085
53561
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
53086
53562
|
|
|
53087
53563
|
// gateway/boot-version.ts
|
|
@@ -60725,6 +61201,62 @@ bot.command("inject", async (ctx) => {
|
|
|
60725
61201
|
formatOutput: formatSwitchroomOutput
|
|
60726
61202
|
});
|
|
60727
61203
|
});
|
|
61204
|
+
function buildModelDeps() {
|
|
61205
|
+
return {
|
|
61206
|
+
discover: (a) => discoverModels(a),
|
|
61207
|
+
select: (a, label) => selectModel(a, label),
|
|
61208
|
+
isBusy: () => currentTurn !== null,
|
|
61209
|
+
getAgentName: getMyAgentName,
|
|
61210
|
+
getQuotaBrief: async () => {
|
|
61211
|
+
try {
|
|
61212
|
+
const probed = await probeQuotaForBootCard(getMyAgentName(), 4000);
|
|
61213
|
+
if (probed?.ok)
|
|
61214
|
+
return formatQuotaLine2(probed.data);
|
|
61215
|
+
} catch {}
|
|
61216
|
+
try {
|
|
61217
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
61218
|
+
if (agentDir) {
|
|
61219
|
+
const local = await fetchQuota2({ claudeConfigDir: join35(agentDir, ".claude") });
|
|
61220
|
+
if (local.ok)
|
|
61221
|
+
return formatQuotaLine2(local.data);
|
|
61222
|
+
}
|
|
61223
|
+
} catch {}
|
|
61224
|
+
return null;
|
|
61225
|
+
},
|
|
61226
|
+
inject: injectSlashCommand,
|
|
61227
|
+
getConfiguredModel: () => {
|
|
61228
|
+
const data = switchroomExecJson(["agent", "list"]);
|
|
61229
|
+
return data?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
61230
|
+
},
|
|
61231
|
+
escapeHtml: escapeHtmlForTg,
|
|
61232
|
+
preBlock
|
|
61233
|
+
};
|
|
61234
|
+
}
|
|
61235
|
+
function modelMenuReplyMarkup(reply) {
|
|
61236
|
+
if (!reply.keyboard)
|
|
61237
|
+
return;
|
|
61238
|
+
const kb = new import_grammy9.InlineKeyboard;
|
|
61239
|
+
for (const row of reply.keyboard) {
|
|
61240
|
+
for (const btn of row)
|
|
61241
|
+
kb.text(btn.text, btn.callback_data);
|
|
61242
|
+
kb.row();
|
|
61243
|
+
}
|
|
61244
|
+
return kb;
|
|
61245
|
+
}
|
|
61246
|
+
bot.command("model", async (ctx) => {
|
|
61247
|
+
if (!isAuthorizedSender(ctx))
|
|
61248
|
+
return;
|
|
61249
|
+
const text = ctx.message?.text ?? ctx.channelPost?.text ?? "";
|
|
61250
|
+
const parsed = parseModelCommand(text) ?? { kind: "show" };
|
|
61251
|
+
const deps = buildModelDeps();
|
|
61252
|
+
if (parsed.kind === "show" && process.env.SWITCHROOM_MODEL_MENU !== "0") {
|
|
61253
|
+
const menu = await buildModelMenu(deps);
|
|
61254
|
+
await switchroomReply(ctx, menu.text, { html: true, reply_markup: modelMenuReplyMarkup(menu) });
|
|
61255
|
+
return;
|
|
61256
|
+
}
|
|
61257
|
+
const reply = await handleModelCommand(parsed, deps);
|
|
61258
|
+
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
61259
|
+
});
|
|
60728
61260
|
bot.command("agentstart", async (ctx) => {
|
|
60729
61261
|
if (!isAuthorizedSender(ctx))
|
|
60730
61262
|
return;
|
|
@@ -61604,7 +62136,7 @@ bot.command("connect", async (ctx) => {
|
|
|
61604
62136
|
try {
|
|
61605
62137
|
const cfg = loadConfig2();
|
|
61606
62138
|
const me = cfg?.agents?.[getMyAgentName()];
|
|
61607
|
-
isAdmin2 = me?.admin === true;
|
|
62139
|
+
isAdmin2 = me?.admin === true || me?.root === true;
|
|
61608
62140
|
} catch {}
|
|
61609
62141
|
if (!isAuthAdmin({ isAdmin: isAdmin2 })) {
|
|
61610
62142
|
await switchroomReply(ctx, `<b>Not authorized.</b> <code>/connect</code> requires this agent to have <code>admin: true</code> in switchroom.yaml.`, { html: true });
|
|
@@ -61693,7 +62225,7 @@ bot.command("auth", async (ctx) => {
|
|
|
61693
62225
|
try {
|
|
61694
62226
|
const cfg = loadConfig2();
|
|
61695
62227
|
const me = cfg?.agents?.[currentAgent];
|
|
61696
|
-
isAdmin2 = me?.admin === true;
|
|
62228
|
+
isAdmin2 = me?.admin === true || me?.root === true;
|
|
61697
62229
|
} catch {}
|
|
61698
62230
|
const chatId = String(ctx.chat?.id ?? "");
|
|
61699
62231
|
if (parsed.kind === "add" || parsed.kind === "cancel") {
|
|
@@ -63452,6 +63984,34 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
63452
63984
|
await handleAuthDashboardCallback(ctx);
|
|
63453
63985
|
return;
|
|
63454
63986
|
}
|
|
63987
|
+
if (data.startsWith(MODEL_CALLBACK_PREFIX)) {
|
|
63988
|
+
const access2 = loadAccess();
|
|
63989
|
+
const senderId2 = String(ctx.from?.id ?? "");
|
|
63990
|
+
if (!access2.allowFrom.includes(senderId2)) {
|
|
63991
|
+
await ctx.answerCallbackQuery({ text: "Not authorized." });
|
|
63992
|
+
return;
|
|
63993
|
+
}
|
|
63994
|
+
if (process.env.SWITCHROOM_MODEL_MENU === "0") {
|
|
63995
|
+
await ctx.answerCallbackQuery({ text: "Model menu is disabled (SWITCHROOM_MODEL_MENU=0)." }).catch(() => {});
|
|
63996
|
+
await ctx.editMessageText("Model menu is disabled on this agent. Use <code>/model <name></code>.", {
|
|
63997
|
+
parse_mode: "HTML",
|
|
63998
|
+
reply_markup: { inline_keyboard: [] }
|
|
63999
|
+
}).catch(() => {});
|
|
64000
|
+
return;
|
|
64001
|
+
}
|
|
64002
|
+
await ctx.answerCallbackQuery({ text: "Working\u2026" }).catch(() => {});
|
|
64003
|
+
try {
|
|
64004
|
+
const outcome = await handleModelMenuCallback(data, buildModelDeps());
|
|
64005
|
+
await ctx.editMessageText(outcome.reply.text, {
|
|
64006
|
+
parse_mode: "HTML",
|
|
64007
|
+
reply_markup: modelMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] }
|
|
64008
|
+
}).catch(() => {});
|
|
64009
|
+
} catch (err) {
|
|
64010
|
+
process.stderr.write(`telegram gateway: model-menu callback failed: ${err?.message ?? String(err)}
|
|
64011
|
+
`);
|
|
64012
|
+
}
|
|
64013
|
+
return;
|
|
64014
|
+
}
|
|
63455
64015
|
if (data.startsWith("cn:")) {
|
|
63456
64016
|
const access2 = loadAccess();
|
|
63457
64017
|
const senderId2 = String(ctx.from?.id ?? "");
|