switchroom 0.15.18 → 0.15.20
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 +521 -336
- package/package.json +1 -1
- package/profiles/_shared/agent-self-service.md.hbs +24 -15
- package/telegram-plugin/dist/gateway/gateway.js +304 -6
- package/telegram-plugin/gateway/effort-command.ts +272 -0
- package/telegram-plugin/gateway/gateway.ts +199 -2
- package/telegram-plugin/gateway/grant-restart.ts +30 -0
- package/telegram-plugin/tests/effort-command.test.ts +191 -0
- package/telegram-plugin/tests/grant-restart.test.ts +38 -0
- package/telegram-plugin/welcome-text.ts +7 -1
package/package.json
CHANGED
|
@@ -38,22 +38,31 @@ tools is to let you do the edit yourself.
|
|
|
38
38
|
derived from the entry content is assigned.
|
|
39
39
|
|
|
40
40
|
**Mind the cost — pick the cheapest tier that does the job:**
|
|
41
|
-
- *Default (no `model`)* —
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
(summarise a feed, format a
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
- *Default (no `model`)* — a **frequent** cron (every ≤60min) is auto-routed
|
|
42
|
+
to a **cheap, minimal-context cron session** (Tier 1: a fresh Sonnet that
|
|
43
|
+
still shares your memory + tools but drops your accumulated conversation
|
|
44
|
+
context). A **daily/weekly** cron defaults to a **full turn in your live
|
|
45
|
+
session** (Tier 2: your model, your whole context). So routine frequent
|
|
46
|
+
checks are already cheap without you doing anything.
|
|
47
|
+
- *`model: "sonnet"` / `context: "fresh"`* — force the cheap Tier-1 session
|
|
48
|
+
even for a **daily/weekly** self-contained job (summarise a feed, format a
|
|
49
|
+
digest) — overrides the cadence default. *`context: "agent"`* does the
|
|
50
|
+
opposite: pins a fire to your full live session when it genuinely needs your
|
|
51
|
+
accumulated conversation context (this always wins).
|
|
52
|
+
- *"Post/send a FIXED thing on a schedule"* (a set reminder message, a webhook
|
|
53
|
+
ping — text fully determined, no thinking) — ask the **operator** for a
|
|
54
|
+
**`kind: action`**: it runs **model-free, zero tokens** (no session at all),
|
|
55
|
+
posting your fixed/templated text or firing a fixed request. The cheapest
|
|
56
|
+
tier there is.
|
|
51
57
|
- *"Only act when X changes"* — don't poll with a frequent prompt cron (every
|
|
52
|
-
fire is a wasted turn when nothing changed). Ask the **operator**
|
|
53
|
-
|
|
54
|
-
reaction-triggered work, **`reaction_dispatch`** (an
|
|
55
|
-
you instantly — zero polling).
|
|
56
|
-
|
|
58
|
+
fire is a wasted turn when nothing changed). Ask the **operator** for a
|
|
59
|
+
**`kind: poll`** (model-free check, e.g. a webpage/API — only a *change*
|
|
60
|
+
wakes you) or, for reaction-triggered work, **`reaction_dispatch`** (an
|
|
61
|
+
emoji reaction wakes you instantly — zero polling).
|
|
62
|
+
- `kind: action` / `kind: poll` / `reaction_dispatch` need an operator config
|
|
63
|
+
commit (egress / identity gates), so **request** them rather than authoring
|
|
64
|
+
them yourself — you can only self-author plain prompt crons (plus the
|
|
65
|
+
`model`/`context` tier hints).
|
|
57
66
|
|
|
58
67
|
- **`schedule_remove(name | cron_hash)`** — delete by `name` (the slug from
|
|
59
68
|
add) or by 12-hex `cron_hash` (shown in `cron_list` output). Both
|
|
@@ -43144,6 +43144,7 @@ var TELEGRAM_MENU_COMMANDS = [
|
|
|
43144
43144
|
{ command: "logs", description: "Show recent agent logs" },
|
|
43145
43145
|
{ command: "inject", description: "Inject a Claude Code slash command (e.g. /cost)" },
|
|
43146
43146
|
{ command: "model", description: "Show or switch the Claude model" },
|
|
43147
|
+
{ command: "effort", description: "Show or switch the reasoning effort" },
|
|
43147
43148
|
{ command: "doctor", description: "Health check (deps, services, MCP)" },
|
|
43148
43149
|
{ command: "usage", description: "Pro/Max plan quota (5h + 7d windows)" },
|
|
43149
43150
|
{ command: "vault", description: "Manage vault secrets + capability grants" },
|
|
@@ -43176,6 +43177,7 @@ function switchroomHelpText(agentName3) {
|
|
|
43176
43177
|
`<code>/update</code> \u2014 dry-run plan; <code>/update apply</code> \u2014 actually pull images, reconcile, restart`,
|
|
43177
43178
|
`<code>/restart [name|all]</code> \u2014 bounce agent (drains in-flight turn by default)`,
|
|
43178
43179
|
`<code>/version</code> \u2014 show versions + running agent health summary`,
|
|
43180
|
+
`<code>/whoami</code> \u2014 this agent's sandbox: tools, MCP, vault key-names, powers`,
|
|
43179
43181
|
``,
|
|
43180
43182
|
`<b>Auth & config</b>`,
|
|
43181
43183
|
`<code>/auth</code> \u2014 auth status or actions`,
|
|
@@ -43185,6 +43187,7 @@ function switchroomHelpText(agentName3) {
|
|
|
43185
43187
|
`<code>/auth rm [agent] <slot> [--force]</code> \u2014 remove a slot`,
|
|
43186
43188
|
`<code>/model</code> \u2014 show the configured Claude model`,
|
|
43187
43189
|
`<code>/model <name></code> \u2014 switch the live session's model (opus \u00b7 sonnet \u00b7 haiku or a full id; until restart)`,
|
|
43190
|
+
`<code>/effort</code> \u2014 show or switch reasoning effort (low \u00b7 medium \u00b7 high \u00b7 xhigh \u00b7 max; until restart)`,
|
|
43188
43191
|
`<code>/topics</code> \u2014 topic-to-agent mappings`,
|
|
43189
43192
|
`<code>/permissions [agent]</code> \u2014 show agent permissions`,
|
|
43190
43193
|
`<code>/grant <tool></code> \u2014 grant a tool permission`,
|
|
@@ -44527,6 +44530,7 @@ var INJECT_COMMANDS = new Map([
|
|
|
44527
44530
|
["/hooks", { description: "List configured hooks", expectsOutput: true }],
|
|
44528
44531
|
["/memory", { description: "Open memory picker", expectsOutput: true }],
|
|
44529
44532
|
["/model", { description: "Open model picker", expectsOutput: true }],
|
|
44533
|
+
["/effort", { description: "Set reasoning effort", expectsOutput: true }],
|
|
44530
44534
|
[
|
|
44531
44535
|
"/clear",
|
|
44532
44536
|
{ description: "Clear session screen", expectsOutput: false }
|
|
@@ -44784,6 +44788,7 @@ var INJECT_COMMANDS2 = new Map([
|
|
|
44784
44788
|
["/hooks", { description: "List configured hooks", expectsOutput: true }],
|
|
44785
44789
|
["/memory", { description: "Open memory picker", expectsOutput: true }],
|
|
44786
44790
|
["/model", { description: "Open model picker", expectsOutput: true }],
|
|
44791
|
+
["/effort", { description: "Set reasoning effort", expectsOutput: true }],
|
|
44787
44792
|
[
|
|
44788
44793
|
"/clear",
|
|
44789
44794
|
{ description: "Clear session screen", expectsOutput: false }
|
|
@@ -45392,6 +45397,161 @@ function extractConfirmation(pane) {
|
|
|
45392
45397
|
return null;
|
|
45393
45398
|
}
|
|
45394
45399
|
|
|
45400
|
+
// gateway/effort-command.ts
|
|
45401
|
+
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
45402
|
+
function isValidEffortArg(arg) {
|
|
45403
|
+
return EFFORT_LEVELS.includes(arg.toLowerCase());
|
|
45404
|
+
}
|
|
45405
|
+
function parseEffortCommand(text) {
|
|
45406
|
+
const m = text.match(/^\/effort(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
|
|
45407
|
+
if (!m)
|
|
45408
|
+
return null;
|
|
45409
|
+
const rest = (m[1] ?? "").trim();
|
|
45410
|
+
if (rest.length === 0)
|
|
45411
|
+
return { kind: "show" };
|
|
45412
|
+
const parts = rest.split(/\s+/);
|
|
45413
|
+
if (parts.length > 1) {
|
|
45414
|
+
return { kind: "help", reason: "effort takes a single level" };
|
|
45415
|
+
}
|
|
45416
|
+
const arg = parts[0];
|
|
45417
|
+
if (arg.toLowerCase() === "help")
|
|
45418
|
+
return { kind: "help" };
|
|
45419
|
+
if (!isValidEffortArg(arg)) {
|
|
45420
|
+
return { kind: "help", reason: `not a valid effort level: ${arg}` };
|
|
45421
|
+
}
|
|
45422
|
+
return { kind: "set", level: arg.toLowerCase() };
|
|
45423
|
+
}
|
|
45424
|
+
var PERSIST_NOTE2 = "<i>Session-only \u2014 reverts to the configured default on restart. To change the default, set <code>thinking_effort:</code> in switchroom.yaml and restart.</i>";
|
|
45425
|
+
var LEVELS_INLINE = EFFORT_LEVELS.map((l) => `<code>${l}</code>`).join(" \u00b7 ");
|
|
45426
|
+
function helpText3(deps, reason) {
|
|
45427
|
+
const lines = [];
|
|
45428
|
+
if (reason)
|
|
45429
|
+
lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
|
|
45430
|
+
lines.push("<b>/effort</b> \u2014 show or switch the reasoning effort (faster\u2192smarter)", "<code>/effort</code> \u2014 show the configured effort + a tap menu", `<code>/effort <level></code> \u2014 switch the live session (${LEVELS_INLINE})`, PERSIST_NOTE2);
|
|
45431
|
+
return { text: lines.join(`
|
|
45432
|
+
`), html: true };
|
|
45433
|
+
}
|
|
45434
|
+
async function handleEffortCommand(parsed, deps) {
|
|
45435
|
+
if (parsed.kind === "help")
|
|
45436
|
+
return helpText3(deps, parsed.reason);
|
|
45437
|
+
if (parsed.kind === "show") {
|
|
45438
|
+
const configured = deps.getConfiguredEffort();
|
|
45439
|
+
const shown = configured && configured.length > 0 ? configured : "low";
|
|
45440
|
+
return {
|
|
45441
|
+
text: [
|
|
45442
|
+
`<b>Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`,
|
|
45443
|
+
`Configured default: <code>${deps.escapeHtml(shown)}</code>`,
|
|
45444
|
+
`Switch the live session: ${EFFORT_LEVELS.map((l) => `<code>/effort ${l}</code>`).join(" \u00b7 ")}`,
|
|
45445
|
+
PERSIST_NOTE2
|
|
45446
|
+
].join(`
|
|
45447
|
+
`),
|
|
45448
|
+
html: true
|
|
45449
|
+
};
|
|
45450
|
+
}
|
|
45451
|
+
if (!isValidEffortArg(parsed.level)) {
|
|
45452
|
+
return helpText3(deps, `not a valid effort level: ${parsed.level}`);
|
|
45453
|
+
}
|
|
45454
|
+
const verbHtml = `<code>/effort ${deps.escapeHtml(parsed.level)}</code>`;
|
|
45455
|
+
let result;
|
|
45456
|
+
try {
|
|
45457
|
+
result = await deps.inject(deps.getAgentName(), `/effort ${parsed.level}`);
|
|
45458
|
+
} catch (err) {
|
|
45459
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
45460
|
+
return { text: `\u274c ${verbHtml} \u2014 inject failed: ${deps.escapeHtml(msg)}`, html: true };
|
|
45461
|
+
}
|
|
45462
|
+
if (result.outcome === "ok") {
|
|
45463
|
+
return {
|
|
45464
|
+
text: [
|
|
45465
|
+
`${verbHtml}`,
|
|
45466
|
+
deps.preBlock(result.output),
|
|
45467
|
+
...result.truncated ? ["<i>truncated</i>"] : [],
|
|
45468
|
+
PERSIST_NOTE2
|
|
45469
|
+
].join(`
|
|
45470
|
+
`),
|
|
45471
|
+
html: true
|
|
45472
|
+
};
|
|
45473
|
+
}
|
|
45474
|
+
if (result.outcome === "ok_no_output") {
|
|
45475
|
+
return {
|
|
45476
|
+
text: [
|
|
45477
|
+
`${verbHtml} \u2014 sent, but no response captured. The agent may be mid-turn; check <code>/inject /status</code> to confirm the active effort.`,
|
|
45478
|
+
PERSIST_NOTE2
|
|
45479
|
+
].join(`
|
|
45480
|
+
`),
|
|
45481
|
+
html: true
|
|
45482
|
+
};
|
|
45483
|
+
}
|
|
45484
|
+
if (result.errorCode === "session_missing") {
|
|
45485
|
+
return {
|
|
45486
|
+
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.",
|
|
45487
|
+
html: true
|
|
45488
|
+
};
|
|
45489
|
+
}
|
|
45490
|
+
return {
|
|
45491
|
+
text: `\u274c ${verbHtml} \u2014 ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`,
|
|
45492
|
+
html: true
|
|
45493
|
+
};
|
|
45494
|
+
}
|
|
45495
|
+
var EFFORT_CALLBACK_PREFIX = "eff:";
|
|
45496
|
+
var EFFORT_CALLBACK_SELECT = "eff:s:";
|
|
45497
|
+
function effortSelectCallbackData(level) {
|
|
45498
|
+
return `${EFFORT_CALLBACK_SELECT}${level}`;
|
|
45499
|
+
}
|
|
45500
|
+
function menuKeyboard2(highlight) {
|
|
45501
|
+
return [
|
|
45502
|
+
EFFORT_LEVELS.map((l) => ({
|
|
45503
|
+
text: l === highlight ? `\u2705 ${l}` : l,
|
|
45504
|
+
callback_data: effortSelectCallbackData(l)
|
|
45505
|
+
}))
|
|
45506
|
+
];
|
|
45507
|
+
}
|
|
45508
|
+
function buildEffortMenu(deps, highlight) {
|
|
45509
|
+
const configured = deps.getConfiguredEffort() || "low";
|
|
45510
|
+
const live = highlight ?? configured;
|
|
45511
|
+
return {
|
|
45512
|
+
text: [
|
|
45513
|
+
`<b>Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`,
|
|
45514
|
+
`Default: <code>${deps.escapeHtml(configured)}</code> \u00b7 faster \u2192 smarter: ${LEVELS_INLINE}`,
|
|
45515
|
+
"Tap to switch the live session:",
|
|
45516
|
+
PERSIST_NOTE2
|
|
45517
|
+
].join(`
|
|
45518
|
+
`),
|
|
45519
|
+
html: true,
|
|
45520
|
+
keyboard: menuKeyboard2(live)
|
|
45521
|
+
};
|
|
45522
|
+
}
|
|
45523
|
+
async function handleEffortMenuCallback(data, deps) {
|
|
45524
|
+
if (!data.startsWith(EFFORT_CALLBACK_SELECT)) {
|
|
45525
|
+
return { reply: buildEffortMenu(deps) };
|
|
45526
|
+
}
|
|
45527
|
+
const level = data.slice(EFFORT_CALLBACK_SELECT.length);
|
|
45528
|
+
if (!isValidEffortArg(level)) {
|
|
45529
|
+
return { reply: buildEffortMenu(deps) };
|
|
45530
|
+
}
|
|
45531
|
+
let banner;
|
|
45532
|
+
let selected;
|
|
45533
|
+
try {
|
|
45534
|
+
const result = await deps.inject(deps.getAgentName(), `/effort ${level}`);
|
|
45535
|
+
if (result.outcome === "ok" || result.outcome === "ok_no_output") {
|
|
45536
|
+
banner = `\u2705 Effort \u2192 <code>${deps.escapeHtml(level)}</code> for this session`;
|
|
45537
|
+
selected = level;
|
|
45538
|
+
} else if (result.errorCode === "session_missing") {
|
|
45539
|
+
banner = "\u274c tmux session not found \u2014 is the agent running under the supervisor?";
|
|
45540
|
+
} else {
|
|
45541
|
+
banner = `\u274c couldn't set effort: ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`;
|
|
45542
|
+
}
|
|
45543
|
+
} catch (err) {
|
|
45544
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
45545
|
+
banner = `\u274c inject failed: ${deps.escapeHtml(msg)}`;
|
|
45546
|
+
}
|
|
45547
|
+
const menu = buildEffortMenu(deps, selected);
|
|
45548
|
+
return {
|
|
45549
|
+
reply: { ...menu, text: `${banner}
|
|
45550
|
+
${menu.text}` },
|
|
45551
|
+
selectedEffort: selected
|
|
45552
|
+
};
|
|
45553
|
+
}
|
|
45554
|
+
|
|
45395
45555
|
// ../src/config/loader.ts
|
|
45396
45556
|
init_dist();
|
|
45397
45557
|
init_zod();
|
|
@@ -53487,6 +53647,15 @@ function readBashCommand(inputPreview) {
|
|
|
53487
53647
|
}
|
|
53488
53648
|
}
|
|
53489
53649
|
|
|
53650
|
+
// gateway/grant-restart.ts
|
|
53651
|
+
function grantRestartDecision(opts) {
|
|
53652
|
+
if ((opts.killSwitch ?? "") === "0")
|
|
53653
|
+
return "disabled";
|
|
53654
|
+
if (!opts.selfAgent || opts.selfAgent !== opts.agentName)
|
|
53655
|
+
return "disabled";
|
|
53656
|
+
return opts.turnInFlight ? "deferred" : "now";
|
|
53657
|
+
}
|
|
53658
|
+
|
|
53490
53659
|
// permission-diff.ts
|
|
53491
53660
|
var TARGET_HEADER_A = "--- a/switchroom.yaml";
|
|
53492
53661
|
var TARGET_HEADER_B = "+++ b/switchroom.yaml";
|
|
@@ -54158,11 +54327,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
54158
54327
|
}
|
|
54159
54328
|
|
|
54160
54329
|
// ../src/build-info.ts
|
|
54161
|
-
var VERSION = "0.15.
|
|
54162
|
-
var COMMIT_SHA = "
|
|
54163
|
-
var COMMIT_DATE = "2026-06-
|
|
54330
|
+
var VERSION = "0.15.20";
|
|
54331
|
+
var COMMIT_SHA = "0b63ab9e";
|
|
54332
|
+
var COMMIT_DATE = "2026-06-14T10:58:14+10:00";
|
|
54164
54333
|
var LATEST_PR = null;
|
|
54165
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
54334
|
+
var COMMITS_AHEAD_OF_TAG = 5;
|
|
54166
54335
|
|
|
54167
54336
|
// gateway/boot-version.ts
|
|
54168
54337
|
function formatRelativeAgo(iso) {
|
|
@@ -61613,6 +61782,26 @@ async function sweepBeforeSelfRestart() {
|
|
|
61613
61782
|
`);
|
|
61614
61783
|
}
|
|
61615
61784
|
}
|
|
61785
|
+
function scheduleGrantRestart(agentName3, chatId, threadId, reason) {
|
|
61786
|
+
const decision = grantRestartDecision({
|
|
61787
|
+
killSwitch: process.env.SWITCHROOM_AUTORESTART_ON_GRANT,
|
|
61788
|
+
selfAgent: process.env.SWITCHROOM_AGENT_NAME,
|
|
61789
|
+
agentName: agentName3,
|
|
61790
|
+
turnInFlight: turnInFlightForGate()
|
|
61791
|
+
});
|
|
61792
|
+
if (decision === "disabled")
|
|
61793
|
+
return decision;
|
|
61794
|
+
if (chatId != null) {
|
|
61795
|
+
writeRestartMarker({ chat_id: String(chatId), thread_id: threadId ?? null, ack_message_id: null, ts: Date.now() });
|
|
61796
|
+
}
|
|
61797
|
+
stampUserRestartReason(reason);
|
|
61798
|
+
if (decision === "deferred") {
|
|
61799
|
+
pendingRestarts.set(agentName3, Date.now());
|
|
61800
|
+
} else {
|
|
61801
|
+
sweepBeforeSelfRestart().finally(() => triggerSelfRestart(agentName3, reason, 1500));
|
|
61802
|
+
}
|
|
61803
|
+
return decision;
|
|
61804
|
+
}
|
|
61616
61805
|
function formatAuthOutputForTelegram(output) {
|
|
61617
61806
|
const trimmed = stripAnsi2(output).trim();
|
|
61618
61807
|
const url = trimmed.match(/https:\/\/\S+/)?.[0] ?? null;
|
|
@@ -62143,6 +62332,43 @@ bot.command("model", async (ctx) => {
|
|
|
62143
62332
|
const reply = await handleModelCommand(parsed, deps);
|
|
62144
62333
|
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
62145
62334
|
});
|
|
62335
|
+
function buildEffortDeps() {
|
|
62336
|
+
return {
|
|
62337
|
+
inject: injectSlashCommand,
|
|
62338
|
+
getAgentName: getMyAgentName,
|
|
62339
|
+
getConfiguredEffort: () => {
|
|
62340
|
+
const data = switchroomExecJson(["agent", "list"]);
|
|
62341
|
+
return data?.agents?.find((a) => a.name === getMyAgentName())?.thinking_effort ?? null;
|
|
62342
|
+
},
|
|
62343
|
+
escapeHtml: escapeHtmlForTg,
|
|
62344
|
+
preBlock
|
|
62345
|
+
};
|
|
62346
|
+
}
|
|
62347
|
+
function effortMenuReplyMarkup(reply) {
|
|
62348
|
+
if (!reply.keyboard)
|
|
62349
|
+
return;
|
|
62350
|
+
const kb = new import_grammy9.InlineKeyboard;
|
|
62351
|
+
for (const row of reply.keyboard) {
|
|
62352
|
+
for (const btn of row)
|
|
62353
|
+
kb.text(btn.text, btn.callback_data);
|
|
62354
|
+
kb.row();
|
|
62355
|
+
}
|
|
62356
|
+
return kb;
|
|
62357
|
+
}
|
|
62358
|
+
bot.command("effort", async (ctx) => {
|
|
62359
|
+
if (!isAuthorizedSender(ctx))
|
|
62360
|
+
return;
|
|
62361
|
+
const text = ctx.message?.text ?? ctx.channelPost?.text ?? "";
|
|
62362
|
+
const parsed = parseEffortCommand(text) ?? { kind: "show" };
|
|
62363
|
+
const deps = buildEffortDeps();
|
|
62364
|
+
if (parsed.kind === "show") {
|
|
62365
|
+
const menu = buildEffortMenu(deps);
|
|
62366
|
+
await switchroomReply(ctx, menu.text, { html: true, reply_markup: effortMenuReplyMarkup(menu) });
|
|
62367
|
+
return;
|
|
62368
|
+
}
|
|
62369
|
+
const reply = await handleEffortCommand(parsed, deps);
|
|
62370
|
+
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
62371
|
+
});
|
|
62146
62372
|
bot.command("agentstart", async (ctx) => {
|
|
62147
62373
|
if (!isAuthorizedSender(ctx))
|
|
62148
62374
|
return;
|
|
@@ -64855,6 +65081,56 @@ bot.command("version", async (ctx) => {
|
|
|
64855
65081
|
${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
|
|
64856
65082
|
}
|
|
64857
65083
|
});
|
|
65084
|
+
bot.command("whoami", async (ctx) => {
|
|
65085
|
+
if (!isAuthorizedSender(ctx))
|
|
65086
|
+
return;
|
|
65087
|
+
try {
|
|
65088
|
+
let raw;
|
|
65089
|
+
try {
|
|
65090
|
+
raw = switchroomExecCombined(["config", "whoami"], 1e4);
|
|
65091
|
+
} catch (err) {
|
|
65092
|
+
raw = err.stdout ?? err.message ?? "whoami failed";
|
|
65093
|
+
}
|
|
65094
|
+
const trimmed = stripAnsi2(raw).trim();
|
|
65095
|
+
let card;
|
|
65096
|
+
try {
|
|
65097
|
+
card = formatWhoamiCard(JSON.parse(trimmed.split(`
|
|
65098
|
+
`).pop() ?? trimmed));
|
|
65099
|
+
} catch {
|
|
65100
|
+
card = preBlock(formatSwitchroomOutput(trimmed || "whoami: no output"));
|
|
65101
|
+
}
|
|
65102
|
+
await switchroomReply(ctx, card, { html: true });
|
|
65103
|
+
} catch (err) {
|
|
65104
|
+
await switchroomReply(ctx, `<b>whoami failed:</b>
|
|
65105
|
+
${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
|
|
65106
|
+
}
|
|
65107
|
+
});
|
|
65108
|
+
function formatWhoamiCard(v) {
|
|
65109
|
+
const esc = escapeHtmlForTg;
|
|
65110
|
+
const yn = (b) => b ? "\u2713" : "\u2717";
|
|
65111
|
+
const lines = [];
|
|
65112
|
+
lines.push(`\uD83D\uDC64 <b>${esc(v.name ?? "?")}</b> \xB7 ${esc(v.tier ?? "standard")}`);
|
|
65113
|
+
if (v.persona)
|
|
65114
|
+
lines.push(esc(v.persona));
|
|
65115
|
+
if (v.model)
|
|
65116
|
+
lines.push(`Model: ${esc(v.model)}`);
|
|
65117
|
+
const allow = v.tools?.allow ?? [];
|
|
65118
|
+
lines.push(`Tools: ${allow.length ? esc(allow.slice(0, 8).join(", ")) + (allow.length > 8 ? ` \u2026(+${allow.length - 8})` : "") : "\u2014"}`);
|
|
65119
|
+
if ((v.tools?.deny ?? []).length)
|
|
65120
|
+
lines.push(`Denied: ${esc(v.tools.deny.join(", "))}`);
|
|
65121
|
+
if ((v.mcpServers ?? []).length)
|
|
65122
|
+
lines.push(`MCP: ${esc(v.mcpServers.join(", "))}`);
|
|
65123
|
+
if ((v.skills ?? []).length)
|
|
65124
|
+
lines.push(`Skills: ${esc(v.skills.join(", "))}`);
|
|
65125
|
+
if ((v.vault ?? []).length) {
|
|
65126
|
+
lines.push(`Vault keys (names only): ${v.vault.map((k) => `${esc(k.key)} ${yn(k.readable)}`).join(", ")}`);
|
|
65127
|
+
}
|
|
65128
|
+
const p = v.powers ?? {};
|
|
65129
|
+
lines.push(`Powers: admin ${yn(p.admin)} \xB7 root ${yn(p.root)} \xB7 config-edit ${yn(p.configEdit)} \xB7 cross-agent verbs ${yn(p.crossAgentHostVerbs)}`);
|
|
65130
|
+
lines.push(`Schedule: ${v.scheduleCount ?? 0} cron \xB7 Memory: ${esc(v.memoryBackend ?? "none")}`);
|
|
65131
|
+
return lines.join(`
|
|
65132
|
+
`);
|
|
65133
|
+
}
|
|
64858
65134
|
bot.command("commands", async (ctx) => {
|
|
64859
65135
|
if (!isAuthorizedSender(ctx))
|
|
64860
65136
|
return;
|
|
@@ -64870,6 +65146,26 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
64870
65146
|
await handleAuthDashboardCallback(ctx);
|
|
64871
65147
|
return;
|
|
64872
65148
|
}
|
|
65149
|
+
if (data.startsWith(EFFORT_CALLBACK_PREFIX)) {
|
|
65150
|
+
const access2 = loadAccess();
|
|
65151
|
+
const senderId2 = String(ctx.from?.id ?? "");
|
|
65152
|
+
if (!access2.allowFrom.includes(senderId2)) {
|
|
65153
|
+
await ctx.answerCallbackQuery({ text: "Not authorized." });
|
|
65154
|
+
return;
|
|
65155
|
+
}
|
|
65156
|
+
await ctx.answerCallbackQuery({ text: "Setting effort\u2026" }).catch(() => {});
|
|
65157
|
+
try {
|
|
65158
|
+
const outcome = await handleEffortMenuCallback(data, buildEffortDeps());
|
|
65159
|
+
await ctx.editMessageText(outcome.reply.text, {
|
|
65160
|
+
parse_mode: "HTML",
|
|
65161
|
+
reply_markup: effortMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] }
|
|
65162
|
+
}).catch(() => {});
|
|
65163
|
+
} catch (err) {
|
|
65164
|
+
process.stderr.write(`telegram gateway: effort-menu callback failed: ${err?.message ?? String(err)}
|
|
65165
|
+
`);
|
|
65166
|
+
}
|
|
65167
|
+
return;
|
|
65168
|
+
}
|
|
64873
65169
|
if (data.startsWith(MODEL_CALLBACK_PREFIX)) {
|
|
64874
65170
|
const access2 = loadAccess();
|
|
64875
65171
|
const senderId2 = String(ctx.from?.id ?? "");
|
|
@@ -65299,10 +65595,12 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
65299
65595
|
}
|
|
65300
65596
|
const ok = durable;
|
|
65301
65597
|
const legacyNote = legacy && durable;
|
|
65302
|
-
const
|
|
65598
|
+
const restartScheduled = ok && !legacy && scheduleGrantRestart(agentName3, ctx.chat?.id, ctx.callbackQuery?.message?.message_thread_id, `always-allow: ${grantPhrase}`) !== "disabled";
|
|
65599
|
+
const liveSuffix = restartScheduled ? " \u2014 applying now (restarting to take effect)" : "";
|
|
65600
|
+
const ackText2 = ok ? legacyNote ? `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking (legacy path).` : `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking.${liveSuffix}` : editLockHint ? `\u26A0\uFE0F Allowed for now \u2014 config edits are locked. Enable hostd.config_edit_enabled.` : `\u26A0\uFE0F Allowed for now, but "always" did NOT save \u2014 it will ask again after restart. Check gateway log.`;
|
|
65303
65601
|
const sourceMsg = ctx.callbackQuery?.message;
|
|
65304
65602
|
const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
|
|
65305
|
-
const editLabel = ok ? legacyNote ? `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking (legacy path); restart agent for full effect` : `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking; restart agent for full effect` : editLockHint ? `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> Config edits are locked; enable <code>hostd.config_edit_enabled</code>.` : `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> It will ask again after restart. Check gateway log.`;
|
|
65603
|
+
const editLabel = ok ? legacyNote ? `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking (legacy path); restart agent for full effect` : restartScheduled ? `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking \u2014 applying now (restarting to take effect).` : `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking; restart agent for full effect` : editLockHint ? `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> Config edits are locked; enable <code>hostd.config_edit_enabled</code>.` : `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> It will ask again after restart. Check gateway log.`;
|
|
65306
65604
|
await finalizeCallback(ctx, {
|
|
65307
65605
|
ackText: ackText2.slice(0, 200),
|
|
65308
65606
|
newText: baseText2 ? `${baseText2}
|