switchroom 0.16.14 → 0.16.15
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 +224 -4
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +14 -5
- package/telegram-plugin/gateway/model-command.ts +22 -1
- package/telegram-plugin/permission-title.ts +1 -0
- package/telegram-plugin/tests/model-command.test.ts +7 -1
package/dist/cli/switchroom.js
CHANGED
|
@@ -15589,6 +15589,10 @@ var init_helpers = __esm(() => {
|
|
|
15589
15589
|
function isKnownCheapModel(model) {
|
|
15590
15590
|
return model !== undefined && CHEAP_MODEL_RE.test(model);
|
|
15591
15591
|
}
|
|
15592
|
+
function isCheapCronEnabled(env2 = process.env) {
|
|
15593
|
+
const v = (env2.SWITCHROOM_CHEAP_CRON ?? "").toLowerCase();
|
|
15594
|
+
return !(v === "0" || v === "false" || v === "off");
|
|
15595
|
+
}
|
|
15592
15596
|
function resolveCronModel(model) {
|
|
15593
15597
|
return isKnownCheapModel(model) ? model : DEFAULT_CRON_MODEL;
|
|
15594
15598
|
}
|
|
@@ -50753,6 +50757,10 @@ function dispatchTool(name, args) {
|
|
|
50753
50757
|
cliArgs = buildArgs(["cron", "list"], args);
|
|
50754
50758
|
parseMode = "json";
|
|
50755
50759
|
break;
|
|
50760
|
+
case "cron_doctor":
|
|
50761
|
+
cliArgs = buildArgs(["cron", "doctor"], args);
|
|
50762
|
+
parseMode = "json";
|
|
50763
|
+
break;
|
|
50756
50764
|
case "skill_list":
|
|
50757
50765
|
cliArgs = buildArgs(["skill", "list"], args);
|
|
50758
50766
|
parseMode = "json";
|
|
@@ -50970,7 +50978,17 @@ var init_server3 = __esm(() => {
|
|
|
50970
50978
|
},
|
|
50971
50979
|
{
|
|
50972
50980
|
name: "cron_list",
|
|
50973
|
-
description: "List the agent's scheduled cron entries (schedule array) as JSON.",
|
|
50981
|
+
description: "List the agent's scheduled cron entries (schedule array) as JSON. " + "Each entry is self-describing: `source` (base-config vs overlay), " + "`file` (switchroom.yaml or the schedule.d path), resolved " + "`tier`/`context`, and a `duplicate_of` back-reference when another " + "entry shares the same cron expression. Original entry fields are " + "preserved.",
|
|
50982
|
+
inputSchema: {
|
|
50983
|
+
type: "object",
|
|
50984
|
+
properties: {
|
|
50985
|
+
agent: { type: "string" }
|
|
50986
|
+
}
|
|
50987
|
+
}
|
|
50988
|
+
},
|
|
50989
|
+
{
|
|
50990
|
+
name: "cron_doctor",
|
|
50991
|
+
description: "Read-only cron health report: duplicate cron expressions (the " + "double-fire hazard), base-config-vs-overlay name conflicts, and " + "entries missing a resolved tier/context. Returns " + "{agent, entry_count, healthy, findings[]}. Call this BEFORE adding " + "a cron, or when a schedule misbehaves (firing twice, removing the " + "wrong entry).",
|
|
50974
50992
|
inputSchema: {
|
|
50975
50993
|
type: "object",
|
|
50976
50994
|
properties: {
|
|
@@ -51674,8 +51692,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
51674
51692
|
import { dirname, join } from "node:path";
|
|
51675
51693
|
|
|
51676
51694
|
// src/build-info.ts
|
|
51677
|
-
var VERSION = "0.16.
|
|
51678
|
-
var COMMIT_SHA = "
|
|
51695
|
+
var VERSION = "0.16.15";
|
|
51696
|
+
var COMMIT_SHA = "a9c59169";
|
|
51679
51697
|
|
|
51680
51698
|
// src/cli/resolve-version.ts
|
|
51681
51699
|
function readPackageVersion() {
|
|
@@ -51828,6 +51846,138 @@ import {
|
|
|
51828
51846
|
appendFileSync,
|
|
51829
51847
|
readFileSync as readFileSync5
|
|
51830
51848
|
} from "node:fs";
|
|
51849
|
+
|
|
51850
|
+
// src/scheduler/cron-introspect.ts
|
|
51851
|
+
init_overlay_loader();
|
|
51852
|
+
init_cron_routing();
|
|
51853
|
+
function normalizeCronExpr(expr) {
|
|
51854
|
+
if (typeof expr !== "string")
|
|
51855
|
+
return null;
|
|
51856
|
+
const t = expr.trim();
|
|
51857
|
+
if (t === "")
|
|
51858
|
+
return null;
|
|
51859
|
+
return t.replace(/\s+/g, " ");
|
|
51860
|
+
}
|
|
51861
|
+
function attributeEntry(raw) {
|
|
51862
|
+
const entry = raw ?? {};
|
|
51863
|
+
const isOverlay = entry[OVERLAY_SOURCE] === true;
|
|
51864
|
+
const overlayTitle = entry[OVERLAY_TITLE];
|
|
51865
|
+
const explicitName = typeof entry.name === "string" && entry.name.trim() !== "" ? entry.name : null;
|
|
51866
|
+
if (isOverlay) {
|
|
51867
|
+
const title = typeof overlayTitle === "string" && overlayTitle.trim() !== "" ? overlayTitle : explicitName;
|
|
51868
|
+
const cron = typeof entry.cron === "string" ? entry.cron : "";
|
|
51869
|
+
const prompt = typeof entry.prompt === "string" ? entry.prompt : undefined;
|
|
51870
|
+
const file = `schedule.d/cron-${cronUnitHash(cron, prompt)}.yaml`;
|
|
51871
|
+
return { entry, source: "overlay", file, name: title };
|
|
51872
|
+
}
|
|
51873
|
+
return { entry, source: "base-config", file: "switchroom.yaml", name: explicitName };
|
|
51874
|
+
}
|
|
51875
|
+
function resolveTierContext(entry, cheapCronEnabled) {
|
|
51876
|
+
const routing = resolveCronRouting({ kind: entry.kind, model: entry.model, context: entry.context }, { cheapCronEnabled });
|
|
51877
|
+
const ctx = routing.tier === "poll" || routing.tier === "action" ? null : routing.session === "cron" ? "fresh" : "agent";
|
|
51878
|
+
return { tier: routing.tier, context: ctx };
|
|
51879
|
+
}
|
|
51880
|
+
function buildCronList(schedule, opts) {
|
|
51881
|
+
const raws = (schedule ?? []).map(attributeEntry);
|
|
51882
|
+
const byExpr = new Map;
|
|
51883
|
+
raws.forEach((r, i) => {
|
|
51884
|
+
const expr = normalizeCronExpr(r.entry.cron);
|
|
51885
|
+
if (expr === null)
|
|
51886
|
+
return;
|
|
51887
|
+
const arr = byExpr.get(expr);
|
|
51888
|
+
if (arr)
|
|
51889
|
+
arr.push(i);
|
|
51890
|
+
else
|
|
51891
|
+
byExpr.set(expr, [i]);
|
|
51892
|
+
});
|
|
51893
|
+
return raws.map((r, i) => {
|
|
51894
|
+
const { tier, context } = resolveTierContext(r.entry, opts.cheapCronEnabled);
|
|
51895
|
+
const expr = normalizeCronExpr(r.entry.cron);
|
|
51896
|
+
const out = {
|
|
51897
|
+
...r.entry,
|
|
51898
|
+
name: r.name,
|
|
51899
|
+
source: r.source,
|
|
51900
|
+
file: r.file,
|
|
51901
|
+
tier,
|
|
51902
|
+
context
|
|
51903
|
+
};
|
|
51904
|
+
if (expr !== null) {
|
|
51905
|
+
const siblings = (byExpr.get(expr) ?? []).filter((j) => j !== i);
|
|
51906
|
+
if (siblings.length > 0) {
|
|
51907
|
+
out.duplicate_of = siblings.map((j) => ({ name: raws[j].name, file: raws[j].file }));
|
|
51908
|
+
}
|
|
51909
|
+
}
|
|
51910
|
+
return out;
|
|
51911
|
+
});
|
|
51912
|
+
}
|
|
51913
|
+
function cronDoctor(agent, schedule, opts) {
|
|
51914
|
+
const raws = (schedule ?? []).map(attributeEntry);
|
|
51915
|
+
const findings = [];
|
|
51916
|
+
const byExpr = new Map;
|
|
51917
|
+
for (const r of raws) {
|
|
51918
|
+
const expr = normalizeCronExpr(r.entry.cron);
|
|
51919
|
+
if (expr === null)
|
|
51920
|
+
continue;
|
|
51921
|
+
const arr = byExpr.get(expr);
|
|
51922
|
+
if (arr)
|
|
51923
|
+
arr.push(r);
|
|
51924
|
+
else
|
|
51925
|
+
byExpr.set(expr, [r]);
|
|
51926
|
+
}
|
|
51927
|
+
for (const [expr, group] of byExpr) {
|
|
51928
|
+
if (group.length > 1) {
|
|
51929
|
+
findings.push({
|
|
51930
|
+
kind: "duplicate_cron",
|
|
51931
|
+
severity: "error",
|
|
51932
|
+
message: `${group.length} entries share cron expression "${expr}" \u2014 they will ` + `all fire together (double-fire). Remove the redundant one(s).`,
|
|
51933
|
+
cron: expr,
|
|
51934
|
+
entries: group.map((r) => ({ name: r.name, file: r.file }))
|
|
51935
|
+
});
|
|
51936
|
+
}
|
|
51937
|
+
}
|
|
51938
|
+
const byName = new Map;
|
|
51939
|
+
for (const r of raws) {
|
|
51940
|
+
if (r.name === null)
|
|
51941
|
+
continue;
|
|
51942
|
+
const arr = byName.get(r.name);
|
|
51943
|
+
if (arr)
|
|
51944
|
+
arr.push(r);
|
|
51945
|
+
else
|
|
51946
|
+
byName.set(r.name, [r]);
|
|
51947
|
+
}
|
|
51948
|
+
for (const [name, group] of byName) {
|
|
51949
|
+
const sources = new Set(group.map((r) => r.source));
|
|
51950
|
+
if (group.length > 1 && sources.size > 1) {
|
|
51951
|
+
findings.push({
|
|
51952
|
+
kind: "name_conflict",
|
|
51953
|
+
severity: "warn",
|
|
51954
|
+
message: `name "${name}" is used by both a base-config and an overlay entry \u2014 ` + `removing by name is ambiguous.`,
|
|
51955
|
+
conflicting_name: name,
|
|
51956
|
+
entries: group.map((r) => ({ name: r.name, file: r.file }))
|
|
51957
|
+
});
|
|
51958
|
+
}
|
|
51959
|
+
}
|
|
51960
|
+
for (const r of raws) {
|
|
51961
|
+
const expr = normalizeCronExpr(r.entry.cron);
|
|
51962
|
+
if (expr === null) {
|
|
51963
|
+
findings.push({
|
|
51964
|
+
kind: "missing_tier",
|
|
51965
|
+
severity: "warn",
|
|
51966
|
+
message: `entry ${r.name ? `"${r.name}"` : "(unnamed)"} has no valid cron ` + `expression, so its tier/context cannot be resolved.`,
|
|
51967
|
+
entries: [{ name: r.name, file: r.file }]
|
|
51968
|
+
});
|
|
51969
|
+
}
|
|
51970
|
+
}
|
|
51971
|
+
return {
|
|
51972
|
+
agent,
|
|
51973
|
+
entry_count: raws.length,
|
|
51974
|
+
findings,
|
|
51975
|
+
healthy: findings.length === 0
|
|
51976
|
+
};
|
|
51977
|
+
}
|
|
51978
|
+
|
|
51979
|
+
// src/cli/agent-config.ts
|
|
51980
|
+
init_cron_routing();
|
|
51831
51981
|
var AUDIT_ROOT = join3(homedir2(), ".switchroom", "audit");
|
|
51832
51982
|
function auditPathFor(agent) {
|
|
51833
51983
|
return join3(AUDIT_ROOT, agent, "agent-config.jsonl");
|
|
@@ -52068,7 +52218,10 @@ function registerAgentConfigCommands(program2) {
|
|
|
52068
52218
|
const cfg = getConfig(program2);
|
|
52069
52219
|
try {
|
|
52070
52220
|
const slice = getAgentSlice(cfg, agent);
|
|
52071
|
-
const
|
|
52221
|
+
const rows = buildCronList(slice.schedule ?? [], {
|
|
52222
|
+
cheapCronEnabled: isCheapCronEnabled()
|
|
52223
|
+
});
|
|
52224
|
+
const tasks = stripSecretValues(rows);
|
|
52072
52225
|
process.stdout.write(JSON.stringify(tasks) + `
|
|
52073
52226
|
`);
|
|
52074
52227
|
appendAudit(agent, "cron.list", { ...opts }, 0);
|
|
@@ -52079,6 +52232,32 @@ function registerAgentConfigCommands(program2) {
|
|
|
52079
52232
|
process.exit(1);
|
|
52080
52233
|
}
|
|
52081
52234
|
}));
|
|
52235
|
+
cron.command("doctor").description("Report cron health: duplicate cron expressions, base-vs-overlay " + "name conflicts, and entries missing a resolved tier/context. " + "Read-only.").option("--agent <name>", "Target agent (defaults to $SWITCHROOM_AGENT_NAME)").action(withConfigError(async (opts) => {
|
|
52236
|
+
let agent;
|
|
52237
|
+
try {
|
|
52238
|
+
agent = resolveTargetAgent(opts.agent);
|
|
52239
|
+
} catch (err) {
|
|
52240
|
+
process.stderr.write(`${err.message}
|
|
52241
|
+
`);
|
|
52242
|
+
appendAudit(opts.agent ?? "<unknown>", "cron.doctor", { ...opts }, 7);
|
|
52243
|
+
process.exit(7);
|
|
52244
|
+
}
|
|
52245
|
+
const cfg = getConfig(program2);
|
|
52246
|
+
try {
|
|
52247
|
+
const slice = getAgentSlice(cfg, agent);
|
|
52248
|
+
const report = cronDoctor(agent, slice.schedule ?? [], {
|
|
52249
|
+
cheapCronEnabled: isCheapCronEnabled()
|
|
52250
|
+
});
|
|
52251
|
+
process.stdout.write(JSON.stringify(report) + `
|
|
52252
|
+
`);
|
|
52253
|
+
appendAudit(agent, "cron.doctor", { ...opts }, 0);
|
|
52254
|
+
} catch (err) {
|
|
52255
|
+
process.stderr.write(`${err.message}
|
|
52256
|
+
`);
|
|
52257
|
+
appendAudit(agent, "cron.doctor", { ...opts }, 1);
|
|
52258
|
+
process.exit(1);
|
|
52259
|
+
}
|
|
52260
|
+
}));
|
|
52082
52261
|
const skill = program2.commands.find((c) => c.name() === "skill") ?? program2.command("skill").description("Read-only access to an agent's skill list");
|
|
52083
52262
|
skill.command("list").description("List the agent's configured skills as JSON").option("--agent <name>", "Target agent (defaults to $SWITCHROOM_AGENT_NAME)").action(withConfigError(async (opts) => {
|
|
52084
52263
|
let agent;
|
|
@@ -87819,6 +87998,7 @@ function exitCodeFor(code) {
|
|
|
87819
87998
|
switch (code) {
|
|
87820
87999
|
case "E_OVERLAY_SECRETS_REQUIRES_APPROVAL":
|
|
87821
88000
|
case "E_CRON_TOO_FREQUENT":
|
|
88001
|
+
case "E_CRON_DUPLICATE":
|
|
87822
88002
|
case "E_QUOTA_EXCEEDED":
|
|
87823
88003
|
case "E_WRITE_REQUIRES_RECREATE":
|
|
87824
88004
|
case "E_SLUG_COLLISION":
|
|
@@ -87834,6 +88014,24 @@ function exitCodeFor(code) {
|
|
|
87834
88014
|
return 10;
|
|
87835
88015
|
}
|
|
87836
88016
|
}
|
|
88017
|
+
function parseOverlayCronAndName(e) {
|
|
88018
|
+
let cron;
|
|
88019
|
+
let name;
|
|
88020
|
+
const headerMatch = e.raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m);
|
|
88021
|
+
if (headerMatch)
|
|
88022
|
+
name = headerMatch[1];
|
|
88023
|
+
try {
|
|
88024
|
+
const doc = import_yaml21.parse(e.raw);
|
|
88025
|
+
const first = doc?.schedule?.[0];
|
|
88026
|
+
if (first) {
|
|
88027
|
+
if (typeof first.cron === "string")
|
|
88028
|
+
cron = first.cron;
|
|
88029
|
+
if (!name && typeof first.name === "string")
|
|
88030
|
+
name = first.name;
|
|
88031
|
+
}
|
|
88032
|
+
} catch {}
|
|
88033
|
+
return { cron, name };
|
|
88034
|
+
}
|
|
87837
88035
|
function scheduleAdd(opts) {
|
|
87838
88036
|
let agent;
|
|
87839
88037
|
try {
|
|
@@ -87903,6 +88101,28 @@ function scheduleAdd(opts) {
|
|
|
87903
88101
|
meta: { current: existing.length }
|
|
87904
88102
|
};
|
|
87905
88103
|
}
|
|
88104
|
+
const wantExpr = normalizeCronExpr(opts.cronExpr);
|
|
88105
|
+
for (const e of existing) {
|
|
88106
|
+
const parsed = parseOverlayCronAndName(e);
|
|
88107
|
+
if (opts.name && parsed.name && parsed.name === opts.name) {
|
|
88108
|
+
return {
|
|
88109
|
+
ok: false,
|
|
88110
|
+
code: "E_CRON_DUPLICATE",
|
|
88111
|
+
message: `an overlay cron named "${opts.name}" already exists ` + `(${e.slug}.yaml) \u2014 remove it first or choose a different name`,
|
|
88112
|
+
exit: 9,
|
|
88113
|
+
meta: { conflicting_slug: e.slug, conflicting_name: parsed.name }
|
|
88114
|
+
};
|
|
88115
|
+
}
|
|
88116
|
+
if (wantExpr && parsed.cron && normalizeCronExpr(parsed.cron) === wantExpr) {
|
|
88117
|
+
return {
|
|
88118
|
+
ok: false,
|
|
88119
|
+
code: "E_CRON_DUPLICATE",
|
|
88120
|
+
message: `an overlay cron with expression "${wantExpr}" already exists ` + `(${e.slug}.yaml${parsed.name ? `, name "${parsed.name}"` : ""}) \u2014 ` + `adding another would double-fire. Remove the existing one first.`,
|
|
88121
|
+
exit: 9,
|
|
88122
|
+
meta: { conflicting_slug: e.slug, conflicting_cron: wantExpr }
|
|
88123
|
+
};
|
|
88124
|
+
}
|
|
88125
|
+
}
|
|
87906
88126
|
const hash2 = cronUnitHash(opts.cronExpr, opts.prompt);
|
|
87907
88127
|
const slug = `cron-${hash2}`;
|
|
87908
88128
|
let priorContent = null;
|
|
@@ -22587,7 +22587,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:f
|
|
|
22587
22587
|
import { dirname as dirname4, join as join2 } from "node:path";
|
|
22588
22588
|
|
|
22589
22589
|
// src/build-info.ts
|
|
22590
|
-
var VERSION = "0.16.
|
|
22590
|
+
var VERSION = "0.16.15";
|
|
22591
22591
|
|
|
22592
22592
|
// src/cli/resolve-version.ts
|
|
22593
22593
|
function readPackageVersion() {
|
package/package.json
CHANGED
|
@@ -46272,7 +46272,7 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
46272
46272
|
`).map((l) => l.trim()).find((l) => /set model|switched/i.test(l)) ?? `Switched to ${friendlyName} (session)`;
|
|
46273
46273
|
return {
|
|
46274
46274
|
answer: confirmation,
|
|
46275
|
-
reply: await
|
|
46275
|
+
reply: await menuWithBannerStatic(deps, `\u2705 ${deps.escapeHtml(confirmation)}`),
|
|
46276
46276
|
selectedModel: srName
|
|
46277
46277
|
};
|
|
46278
46278
|
}
|
|
@@ -46334,6 +46334,14 @@ async function menuWithBanner(deps, banner) {
|
|
|
46334
46334
|
...fresh.keyboard ? { keyboard: fresh.keyboard } : {}
|
|
46335
46335
|
};
|
|
46336
46336
|
}
|
|
46337
|
+
async function menuWithBannerStatic(deps, banner) {
|
|
46338
|
+
const v1 = await handleModelCommand({ kind: "show" }, deps);
|
|
46339
|
+
return {
|
|
46340
|
+
text: [banner, "", v1.text].join(`
|
|
46341
|
+
`),
|
|
46342
|
+
html: true
|
|
46343
|
+
};
|
|
46344
|
+
}
|
|
46337
46345
|
|
|
46338
46346
|
// ../src/agents/model-picker.ts
|
|
46339
46347
|
var HEADER_RE = /Select model/;
|
|
@@ -54753,6 +54761,7 @@ var ARG_SUMMARY_LINE_MAX = 180;
|
|
|
54753
54761
|
var MCP_TOOL_DESCRIPTIONS = {
|
|
54754
54762
|
"mcp__agent-config__config_get": "Read its own merged config",
|
|
54755
54763
|
"mcp__agent-config__cron_list": "List its own scheduled tasks",
|
|
54764
|
+
"mcp__agent-config__cron_doctor": "Health-check its own cron schedule",
|
|
54756
54765
|
"mcp__agent-config__skill_list": "List its own installed skills",
|
|
54757
54766
|
"mcp__agent-config__audit_tail": "Read its own recent tool-call audit log",
|
|
54758
54767
|
"mcp__agent-config__peers_list": "List the other agents on this instance",
|
|
@@ -56064,10 +56073,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
56064
56073
|
}
|
|
56065
56074
|
|
|
56066
56075
|
// ../src/build-info.ts
|
|
56067
|
-
var VERSION = "0.16.
|
|
56068
|
-
var COMMIT_SHA = "
|
|
56069
|
-
var COMMIT_DATE = "2026-06-
|
|
56070
|
-
var LATEST_PR =
|
|
56076
|
+
var VERSION = "0.16.15";
|
|
56077
|
+
var COMMIT_SHA = "a9c59169";
|
|
56078
|
+
var COMMIT_DATE = "2026-06-28T22:48:22Z";
|
|
56079
|
+
var LATEST_PR = 2643;
|
|
56071
56080
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
56072
56081
|
|
|
56073
56082
|
// gateway/boot-version.ts
|
|
@@ -560,7 +560,11 @@ export async function handleModelMenuCallback(
|
|
|
560
560
|
.find((l) => /set model|switched/i.test(l)) ?? `Switched to ${friendlyName} (session)`
|
|
561
561
|
return {
|
|
562
562
|
answer: confirmation,
|
|
563
|
-
|
|
563
|
+
// Use the static (no-discover) path — after a text-inject the picker
|
|
564
|
+
// is in flux and discover() reliably fails, producing a spurious
|
|
565
|
+
// "(picker unavailable)" line that reads as an error when the switch
|
|
566
|
+
// actually succeeded.
|
|
567
|
+
reply: await menuWithBannerStatic(deps, `✅ ${deps.escapeHtml(confirmation)}`),
|
|
564
568
|
selectedModel: srName,
|
|
565
569
|
}
|
|
566
570
|
}
|
|
@@ -683,3 +687,20 @@ async function menuWithBanner(
|
|
|
683
687
|
...(fresh.keyboard ? { keyboard: fresh.keyboard } : {}),
|
|
684
688
|
}
|
|
685
689
|
}
|
|
690
|
+
|
|
691
|
+
// Static variant — skips discover() entirely and uses the v1 text path.
|
|
692
|
+
// Use after a text-inject where the picker state is inherently uncertain:
|
|
693
|
+
// discover() reliably fails immediately post-inject, producing a spurious
|
|
694
|
+
// "(picker unavailable)" warning that reads as an error.
|
|
695
|
+
async function menuWithBannerStatic(
|
|
696
|
+
deps: ModelMenuDeps & ModelCommandDeps,
|
|
697
|
+
banner: string,
|
|
698
|
+
): Promise<ModelMenuReply> {
|
|
699
|
+
const v1 = await handleModelCommand({ kind: 'show' }, deps)
|
|
700
|
+
return {
|
|
701
|
+
text: [banner, '', v1.text].join('\n'),
|
|
702
|
+
html: true,
|
|
703
|
+
// No keyboard — picker state is unknown after a text-inject; operator
|
|
704
|
+
// can tap /model to get a fresh interactive menu.
|
|
705
|
+
}
|
|
706
|
+
}
|
|
@@ -46,6 +46,7 @@ const MCP_TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
|
46
46
|
// agent-config — every agent's self-service surface (#1163, #1215)
|
|
47
47
|
"mcp__agent-config__config_get": "Read its own merged config",
|
|
48
48
|
"mcp__agent-config__cron_list": "List its own scheduled tasks",
|
|
49
|
+
"mcp__agent-config__cron_doctor": "Health-check its own cron schedule",
|
|
49
50
|
"mcp__agent-config__skill_list": "List its own installed skills",
|
|
50
51
|
"mcp__agent-config__audit_tail": "Read its own recent tool-call audit log",
|
|
51
52
|
"mcp__agent-config__peers_list": "List the other agents on this instance",
|
|
@@ -713,7 +713,13 @@ describe("handleModelMenuCallback — sr-* selection", () => {
|
|
|
713
713
|
expect(calls.select).toHaveLength(0);
|
|
714
714
|
expect(out.answer).toContain("Set model to sonnet");
|
|
715
715
|
expect(out.selectedModel).toBe("sr-gemini-2.5-pro");
|
|
716
|
-
|
|
716
|
+
// No keyboard on success: static reply path skips discover() to avoid the
|
|
717
|
+
// spurious "(picker unavailable)" line that discover() reliably produces
|
|
718
|
+
// immediately after an inject. Operator taps /model for a fresh menu.
|
|
719
|
+
expect(out.reply.keyboard).toBeUndefined();
|
|
720
|
+
// Banner present and text doesn't contain picker-unavailable noise
|
|
721
|
+
expect(out.reply.text).toContain('✅');
|
|
722
|
+
expect(out.reply.text).not.toContain('picker unavailable');
|
|
717
723
|
});
|
|
718
724
|
|
|
719
725
|
it("sr-* tap while busy returns toast-only with no inject", async () => {
|