switchroom 0.16.22 → 0.16.24

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.
Files changed (118) hide show
  1. package/dist/cli/self-improve-apply-guard-pretool.mjs +6 -0
  2. package/dist/cli/skill-validate-pretool.mjs +2865 -48
  3. package/dist/cli/switchroom.js +338 -92
  4. package/dist/host-control/main.js +1 -1
  5. package/package.json +5 -5
  6. package/telegram-plugin/answer-stream.ts +13 -43
  7. package/telegram-plugin/auth-snapshot-format.ts +27 -30
  8. package/telegram-plugin/auto-fallback-fleet.ts +6 -11
  9. package/telegram-plugin/card-format.ts +28 -25
  10. package/telegram-plugin/credits-watch.ts +5 -10
  11. package/telegram-plugin/dist/gateway/gateway.js +1670 -1892
  12. package/telegram-plugin/draft-stream.ts +4 -4
  13. package/telegram-plugin/format.ts +101 -662
  14. package/telegram-plugin/gateway/approval-callback.ts +2 -3
  15. package/telegram-plugin/gateway/approval-card.test.ts +17 -4
  16. package/telegram-plugin/gateway/approval-card.ts +16 -6
  17. package/telegram-plugin/gateway/approvals-commands.ts +18 -24
  18. package/telegram-plugin/gateway/auth-command.ts +74 -74
  19. package/telegram-plugin/gateway/auth-line.ts +5 -15
  20. package/telegram-plugin/gateway/boot-card.ts +20 -22
  21. package/telegram-plugin/gateway/boot-version.ts +3 -2
  22. package/telegram-plugin/gateway/config-approval-handler.test.ts +35 -33
  23. package/telegram-plugin/gateway/config-approval-handler.ts +24 -24
  24. package/telegram-plugin/gateway/config-snapshot.ts +9 -9
  25. package/telegram-plugin/gateway/diff-preview-card.test.ts +8 -8
  26. package/telegram-plugin/gateway/diff-preview-card.ts +2 -5
  27. package/telegram-plugin/gateway/disconnect-flush.ts +0 -4
  28. package/telegram-plugin/gateway/effort-command.ts +17 -17
  29. package/telegram-plugin/gateway/folder-picker-handler.test.ts +8 -2
  30. package/telegram-plugin/gateway/folder-picker-handler.ts +3 -4
  31. package/telegram-plugin/gateway/gateway.ts +751 -624
  32. package/telegram-plugin/gateway/inject-handler.test.ts +15 -13
  33. package/telegram-plugin/gateway/inject-handler.ts +5 -5
  34. package/telegram-plugin/gateway/ipc-protocol.ts +33 -1
  35. package/telegram-plugin/gateway/ipc-server.ts +28 -0
  36. package/telegram-plugin/gateway/linear-activity.ts +16 -14
  37. package/telegram-plugin/gateway/linear-setup.ts +1 -1
  38. package/telegram-plugin/gateway/model-command.ts +25 -25
  39. package/telegram-plugin/gateway/oversize-card-body.ts +6 -7
  40. package/telegram-plugin/gateway/skill-proposal-card.ts +167 -0
  41. package/telegram-plugin/inline-keyboard-callbacks.ts +19 -13
  42. package/telegram-plugin/issues-card.ts +6 -7
  43. package/telegram-plugin/model-unavailable.ts +8 -12
  44. package/telegram-plugin/operator-events-history.ts +1 -1
  45. package/telegram-plugin/operator-events.ts +24 -28
  46. package/telegram-plugin/package.json +1 -1
  47. package/telegram-plugin/pending-work-progress.ts +36 -36
  48. package/telegram-plugin/permission-title.ts +36 -20
  49. package/telegram-plugin/pty-partial-handler.ts +5 -13
  50. package/telegram-plugin/quota-check.ts +5 -5
  51. package/telegram-plugin/quota-watch.ts +13 -18
  52. package/telegram-plugin/recent-outbound-dedup.ts +5 -5
  53. package/telegram-plugin/registry/turns-schema.ts +44 -2
  54. package/telegram-plugin/retry-api-call.ts +15 -7
  55. package/telegram-plugin/rich-send.ts +57 -0
  56. package/telegram-plugin/secret-detect/vault-error.test.ts +6 -6
  57. package/telegram-plugin/secret-detect/vault-error.ts +29 -22
  58. package/telegram-plugin/shared/bot-runtime.ts +23 -6
  59. package/telegram-plugin/silence-poke.ts +26 -69
  60. package/telegram-plugin/slot-banner-driver.ts +9 -6
  61. package/telegram-plugin/slot-banner.ts +5 -8
  62. package/telegram-plugin/steering.ts +0 -4
  63. package/telegram-plugin/stream-controller.ts +59 -62
  64. package/telegram-plugin/stream-reply-handler.ts +40 -97
  65. package/telegram-plugin/subagent-watcher.ts +2 -2
  66. package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +5 -2
  67. package/telegram-plugin/tests/answer-stream.test.ts +54 -63
  68. package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
  69. package/telegram-plugin/tests/auth-command-vernacular.test.ts +3 -2
  70. package/telegram-plugin/tests/auth-snapshot-format.test.ts +19 -18
  71. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +15 -13
  72. package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +27 -12
  73. package/telegram-plugin/tests/boot-card-render.test.ts +59 -48
  74. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  75. package/telegram-plugin/tests/bot-api.harness.ts +23 -1
  76. package/telegram-plugin/tests/bot-runtime.test.ts +23 -18
  77. package/telegram-plugin/tests/card-format.test.ts +6 -4
  78. package/telegram-plugin/tests/config-snapshot.test.ts +1 -1
  79. package/telegram-plugin/tests/credits-watch.test.ts +5 -5
  80. package/telegram-plugin/tests/fake-bot-api.ts +58 -4
  81. package/telegram-plugin/tests/finalize-callback.test.ts +11 -9
  82. package/telegram-plugin/tests/foreground-nesting.test.ts +1 -1
  83. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +3 -13
  84. package/telegram-plugin/tests/issues-card.test.ts +15 -12
  85. package/telegram-plugin/tests/linear-agent-activity.test.ts +8 -5
  86. package/telegram-plugin/tests/model-command.test.ts +2 -2
  87. package/telegram-plugin/tests/model-unavailable.test.ts +13 -13
  88. package/telegram-plugin/tests/multi-turn-continuity.test.ts +6 -10
  89. package/telegram-plugin/tests/operator-events.test.ts +7 -9
  90. package/telegram-plugin/tests/pending-work-progress.test.ts +20 -21
  91. package/telegram-plugin/tests/permission-title.test.ts +45 -41
  92. package/telegram-plugin/tests/pty-partial-handler.test.ts +8 -8
  93. package/telegram-plugin/tests/quota-check.test.ts +3 -3
  94. package/telegram-plugin/tests/quota-watch.test.ts +8 -4
  95. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +4 -3
  96. package/telegram-plugin/tests/silence-poke.test.ts +75 -112
  97. package/telegram-plugin/tests/single-mode-stream-reply.test.ts +137 -0
  98. package/telegram-plugin/tests/skill-proposal-card.test.ts +103 -0
  99. package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +36 -24
  100. package/telegram-plugin/tests/slot-banner.test.ts +9 -6
  101. package/telegram-plugin/tests/status-accent.test.ts +29 -32
  102. package/telegram-plugin/tests/{stream-controller-html-fallback.test.ts → stream-controller-parse-fallback.test.ts} +40 -42
  103. package/telegram-plugin/tests/stream-controller.test.ts +63 -52
  104. package/telegram-plugin/tests/stream-reply-error-paths.test.ts +43 -38
  105. package/telegram-plugin/tests/stream-reply-handler.test.ts +122 -249
  106. package/telegram-plugin/tests/streaming-e2e.test.ts +35 -30
  107. package/telegram-plugin/tests/streaming-orchestration.test.ts +29 -28
  108. package/telegram-plugin/tests/telegram-format.test.ts +120 -1083
  109. package/telegram-plugin/tests/tool-activity-summary.test.ts +144 -145
  110. package/telegram-plugin/tests/welcome-text.test.ts +72 -65
  111. package/telegram-plugin/tests/worker-activity-feed.test.ts +119 -137
  112. package/telegram-plugin/text-voice-scrub.ts +8 -11
  113. package/telegram-plugin/tool-activity-summary.ts +29 -29
  114. package/telegram-plugin/welcome-text.ts +82 -83
  115. package/telegram-plugin/worker-activity-feed.ts +2 -3
  116. package/telegram-plugin/html-sanitize.ts +0 -244
  117. package/telegram-plugin/tests/html-sanitize.test.ts +0 -146
  118. package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -162
@@ -164,7 +164,7 @@ export async function handleApprovalCallback(
164
164
  const newBody =
165
165
  `${icon} ${displayMode}` +
166
166
  (granted
167
- ? ` · /approvals revoke <code>${decision_id}</code>`
167
+ ? ` · /approvals revoke \`${decision_id}\``
168
168
  : "");
169
169
 
170
170
  const postTapKeyboard = granted && result.scope
@@ -172,8 +172,7 @@ export async function handleApprovalCallback(
172
172
  : undefined;
173
173
 
174
174
  try {
175
- await ctx.editMessageText(newBody, {
176
- parse_mode: "HTML",
175
+ await ctx.editMessageText({ markdown: newBody }, {
177
176
  reply_markup: postTapKeyboard,
178
177
  });
179
178
  } catch {
@@ -44,15 +44,28 @@ describe("approval card", () => {
44
44
  expect(datas).toContain("apv:a3f1b9c2a3f1b9c2a3f1b9c2a3f1b9c2:ttl:1h");
45
45
  });
46
46
 
47
- it("escapes HTML metacharacters in agent and scope", () => {
47
+ it("passes < > & literally in agent and scope (markdown, #2669)", () => {
48
48
  const card = buildApprovalCard({
49
49
  request_id: "deadbeefdeadbeefdeadbeefdeadbeef",
50
50
  agent: "<script>",
51
51
  scope_humanized: "a&b",
52
52
  });
53
- expect(card.text).not.toContain("<script>");
54
- expect(card.text).toContain("&lt;script&gt;");
55
- expect(card.text).toContain("a&amp;b");
53
+ // < > & are literal in rich markdown. The agent is bolded (no specials
54
+ // here to escape) and the scope rides verbatim in a `code span`.
55
+ expect(card.text).toContain("**<script>**");
56
+ expect(card.text).toContain("`a&b`");
57
+ });
58
+
59
+ it("keeps a scope with underscores literal inside the code span (no \\_ leak, #2669)", () => {
60
+ // Regression: escaping inside the code span produced visible backslashes
61
+ // like `secret:OPENAI\_API\_KEY`. The scope must render verbatim.
62
+ const card = buildApprovalCard({
63
+ request_id: "deadbeefdeadbeefdeadbeefdeadbeef",
64
+ agent: "k",
65
+ scope_humanized: "secret:OPENAI_API_KEY",
66
+ });
67
+ expect(card.text).toContain("`secret:OPENAI_API_KEY`");
68
+ expect(card.text).not.toContain("\\_");
56
69
  });
57
70
  });
58
71
 
@@ -37,8 +37,12 @@ export interface BuiltApprovalCard {
37
37
  */
38
38
  export function buildApprovalCard(opts: ApprovalCardOptions): BuiltApprovalCard {
39
39
  const lines: string[] = [];
40
- lines.push(`🔐 <b>${escapeHtml(opts.agent)}</b> wants approval`);
41
- lines.push(`<code>${escapeHtml(opts.scope_humanized)}</code>`);
40
+ lines.push(`🔐 **${escapeHtml(opts.agent)}** wants approval`);
41
+ // The scope rides in a `code span` — content there is LITERAL, so it must
42
+ // NOT be markdown-escaped (escaping would leak visible backslashes, e.g.
43
+ // `secret:OPENAI\_API\_KEY`). Only an embedded backtick could break out of
44
+ // the span, so defuse that one char with a zero-width space (#2669).
45
+ lines.push(`\`${codeSpanSafe(opts.scope_humanized)}\``);
42
46
  if (opts.why && opts.why.trim().length > 0) {
43
47
  lines.push("");
44
48
  lines.push(escapeHtml(opts.why.trim()));
@@ -120,8 +124,14 @@ export function ttlMsFromToken(token: string): number | null {
120
124
  }
121
125
 
122
126
  function escapeHtml(s: string): string {
123
- return s
124
- .replace(/&/g, "&amp;")
125
- .replace(/</g, "&lt;")
126
- .replace(/>/g, "&gt;");
127
+ return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
128
+ }
129
+
130
+ /**
131
+ * Make a string safe to interpolate INSIDE a `code span` without escaping
132
+ * (content there is literal). The only character that can prematurely close
133
+ * the span is a backtick, so split it with a zero-width space.
134
+ */
135
+ function codeSpanSafe(s: string): string {
136
+ return s.replace(/`/g, "`​");
127
137
  }
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { Bot, Context } from "grammy";
17
+ import { richMessage } from "../rich-send.js";
17
18
  import {
18
19
  approvalList,
19
20
  approvalRevoke,
@@ -52,12 +53,11 @@ export function registerApprovalsCommands(
52
53
  return;
53
54
  }
54
55
  if (decisions.length === 0) {
55
- await ctx.reply(
56
+ await ctx.replyWithRichMessage(richMessage(
56
57
  agentFilter
57
- ? `No active approvals for <code>${escapeHtml(agentFilter)}</code>.`
58
+ ? `No active approvals for \`${agentFilter}\`.`
58
59
  : "No active approvals.",
59
- { parse_mode: "HTML" },
60
- );
60
+ ));
61
61
  return;
62
62
  }
63
63
  // Top-level summary by agent, mirroring the GitHub-style permissions UI
@@ -65,7 +65,7 @@ export function registerApprovalsCommands(
65
65
  const byAgent = new Map<string, number>();
66
66
  for (const d of decisions) byAgent.set(d.agent_unit, (byAgent.get(d.agent_unit) ?? 0) + 1);
67
67
  const summary = Array.from(byAgent.entries())
68
- .map(([a, n]) => `• <b>${escapeHtml(a)}</b>: ${n}`)
68
+ .map(([a, n]) => `• **${escapeHtml(a)}**: ${n}`)
69
69
  .join("\n");
70
70
  const detail = decisions
71
71
  .slice(0, 20)
@@ -75,17 +75,15 @@ export function registerApprovalsCommands(
75
75
  ? "always"
76
76
  : `until ${new Date(d.ttl_expires_at).toISOString().slice(0, 16).replace("T", " ")}`;
77
77
  return (
78
- `<code>${escapeHtml(d.id.slice(0, 8))}</code> ` +
78
+ `\`${d.id.slice(0, 8)}\` ` +
79
79
  `${escapeHtml(d.agent_unit)} → ` +
80
- `<code>${escapeHtml(d.scope)}</code> ` +
80
+ `\`${d.scope}\` ` +
81
81
  `(${escapeHtml(d.action)}, ${ttl}) ` +
82
82
  `· /approvals revoke ${escapeHtml(d.id)}`
83
83
  );
84
84
  })
85
85
  .join("\n");
86
- await ctx.reply(`<b>Active approvals</b>\n\n${summary}\n\n${detail}`, {
87
- parse_mode: "HTML",
88
- });
86
+ await ctx.replyWithRichMessage(richMessage(`**Active approvals**\n\n${summary}\n\n${detail}`));
89
87
  return;
90
88
  }
91
89
 
@@ -93,9 +91,7 @@ export function registerApprovalsCommands(
93
91
  if (sub === "revoke") {
94
92
  const id = args[1];
95
93
  if (!id) {
96
- await ctx.reply("Usage: <code>/approvals revoke &lt;id&gt;</code>", {
97
- parse_mode: "HTML",
98
- });
94
+ await ctx.replyWithRichMessage(richMessage("Usage: `/approvals revoke <id>`"));
99
95
  return;
100
96
  }
101
97
  const actor = ctx.from?.id?.toString() ?? "unknown";
@@ -104,23 +100,21 @@ export function registerApprovalsCommands(
104
100
  await ctx.reply("Approval kernel unreachable.");
105
101
  return;
106
102
  }
107
- await ctx.reply(
108
- ok ? `Revoked <code>${escapeHtml(id)}</code>.` : `No such active decision <code>${escapeHtml(id)}</code>.`,
109
- { parse_mode: "HTML" },
110
- );
103
+ await ctx.replyWithRichMessage(richMessage(
104
+ ok ? `Revoked \`${id}\`.` : `No such active decision \`${id}\`.`,
105
+ ));
111
106
  return;
112
107
  }
113
108
 
114
109
  // /approvals add and /approvals stats — TODO Phase 1.5
115
- await ctx.reply(
116
- `Unknown subcommand <code>${escapeHtml(sub)}</code>. ` +
117
- `Use <code>/approvals list</code> or <code>/approvals revoke &lt;id&gt;</code>. ` +
118
- `(<code>add</code> and <code>stats</code> are coming in a follow-up.)`,
119
- { parse_mode: "HTML" },
120
- );
110
+ await ctx.replyWithRichMessage(richMessage(
111
+ `Unknown subcommand \`${sub}\`. ` +
112
+ `Use \`/approvals list\` or \`/approvals revoke <id>\`. ` +
113
+ `(\`add\` and \`stats\` are coming in a follow-up.)`,
114
+ ));
121
115
  });
122
116
  }
123
117
 
124
118
  function escapeHtml(s: string): string {
125
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
119
+ return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
126
120
  }
@@ -95,7 +95,7 @@ export function validateAuthAddLabel(label: string): string | null {
95
95
  return 'Label cannot contain path separators.'
96
96
  }
97
97
  if (!LABEL_RE.test(label)) {
98
- return 'Label must match <code>[A-Za-z0-9._@+-]+</code> (letters, digits, dot, underscore, dash, @, +).'
98
+ return 'Label must match \`[A-Za-z0-9._@+-]+\` (letters, digits, dot, underscore, dash, @, +).'
99
99
  }
100
100
  return null
101
101
  }
@@ -158,7 +158,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
158
158
  if (tail.length > 0) {
159
159
  return {
160
160
  kind: 'help',
161
- reason: `Unknown <code>rm</code> modifier: <code>${escapeHtml(tail)}</code>. Use <code>/auth rm &lt;label&gt; confirm</code> to confirm.`,
161
+ reason: `Unknown \`rm\` modifier: \`${escapeHtml(tail)}\`. Use \`/auth rm <label> confirm\` to confirm.`,
162
162
  }
163
163
  }
164
164
  return { kind: 'rm-prompt', label }
@@ -180,7 +180,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
180
180
  if (sub !== 'override') {
181
181
  return {
182
182
  kind: 'help',
183
- reason: `Unknown <code>agent</code> subcommand: <code>${escapeHtml(sub || '(none)')}</code>. Try <code>/auth agent override &lt;agent&gt; &lt;label|clear&gt;</code>.`,
183
+ reason: `Unknown \`agent\` subcommand: \`${escapeHtml(sub || '(none)')}\`. Try \`/auth agent override <agent> <label|clear>\`.`,
184
184
  }
185
185
  }
186
186
  const agent = parts[2]
@@ -188,7 +188,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
188
188
  if (!agent || !target) {
189
189
  return {
190
190
  kind: 'help',
191
- reason: 'Usage: /auth agent override &lt;agent&gt; &lt;label|clear&gt;',
191
+ reason: 'Usage: /auth agent override <agent> <label|clear>',
192
192
  }
193
193
  }
194
194
  if (target.toLowerCase() === 'clear') {
@@ -201,7 +201,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
201
201
  case 'help':
202
202
  return { kind: 'help' }
203
203
  default:
204
- return { kind: 'help', reason: `Unknown verb: <code>${escapeHtml(verb)}</code>` }
204
+ return { kind: 'help', reason: `Unknown verb: \`${escapeHtml(verb)}\`` }
205
205
  }
206
206
  }
207
207
 
@@ -351,19 +351,19 @@ export async function handleAuthCommand(
351
351
  const reason = parsed.reason ? `${parsed.reason}\n\n` : ''
352
352
  return {
353
353
  text:
354
- `${reason}<b>/auth</b> — verbs (mirror of <code>switchroom auth</code>):\n` +
355
- ` <code>/auth</code> — show fleet snapshot (alias of <code>show</code>)\n` +
356
- ` <code>/auth show</code> — show fleet snapshot\n` +
357
- ` <code>/auth show &lt;agent&gt;</code> — show one agent's effective account + mirror state\n` +
358
- ` <code>/auth list</code> — alias of <code>/auth show</code>\n` +
359
- ` <code>/auth use &lt;label&gt;</code> — admin: swap the fleet to &lt;label&gt;\n` +
360
- ` <code>/auth rotate</code> — admin: cycle to next non-exhausted fallback\n` +
361
- ` <code>/auth add &lt;label&gt;</code> — admin: OAuth-add a new account from chat\n` +
362
- ` <code>/auth cancel</code> — abort an <code>/auth add</code> in progress\n` +
363
- ` <code>/auth rm &lt;label&gt;</code> — admin: remove an account (two-step confirm)\n` +
364
- ` <code>/auth refresh [&lt;label&gt;]</code> — admin: force a refresh tick\n` +
365
- ` <code>/auth agent override &lt;agent&gt; &lt;label|clear&gt;</code> — admin: per-agent account override\n` +
366
- ` <code>/auth help</code> — this list`,
354
+ `${reason}**/auth** — verbs (mirror of \`switchroom auth\`):\n` +
355
+ ` \`/auth\` — show fleet snapshot (alias of \`show\`)\n` +
356
+ ` \`/auth show\` — show fleet snapshot\n` +
357
+ ` \`/auth show <agent>\` — show one agent's effective account + mirror state\n` +
358
+ ` \`/auth list\` — alias of \`/auth show\`\n` +
359
+ ` \`/auth use <label>\` — admin: swap the fleet to <label>\n` +
360
+ ` \`/auth rotate\` — admin: cycle to next non-exhausted fallback\n` +
361
+ ` \`/auth add <label>\` — admin: OAuth-add a new account from chat\n` +
362
+ ` \`/auth cancel\` — abort an \`/auth add\` in progress\n` +
363
+ ` \`/auth rm <label>\` — admin: remove an account (two-step confirm)\n` +
364
+ ` \`/auth refresh [<label>]\` — admin: force a refresh tick\n` +
365
+ ` \`/auth agent override <agent> <label|clear>\` — admin: per-agent account override\n` +
366
+ ` \`/auth help\` — this list`,
367
367
  html: true,
368
368
  }
369
369
  }
@@ -417,7 +417,7 @@ export async function handleAuthCommand(
417
417
  }
418
418
  } catch (err) {
419
419
  return {
420
- text: `<b>/auth show failed:</b> ${escapeHtml((err as Error)?.message ?? String(err))}`,
420
+ text: `**/auth show failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
421
421
  html: true,
422
422
  }
423
423
  }
@@ -432,15 +432,15 @@ export async function handleAuthCommand(
432
432
  if (!agent) {
433
433
  return {
434
434
  text:
435
- `<b>/auth show:</b> no agent named <code>${escapeHtml(agentName)}</code> in broker view.\n` +
436
- `Run <code>/auth show</code> for the fleet snapshot.`,
435
+ `**/auth show:** no agent named \`${escapeHtml(agentName)}\` in broker view.\n` +
436
+ `Run \`/auth show\` for the fleet snapshot.`,
437
437
  html: true,
438
438
  }
439
439
  }
440
440
  return { text: renderAgentDetail(state, agent), html: true }
441
441
  } catch (err) {
442
442
  return {
443
- text: `<b>/auth show failed:</b> ${escapeHtml((err as Error)?.message ?? String(err))}`,
443
+ text: `**/auth show failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
444
444
  html: true,
445
445
  }
446
446
  }
@@ -450,10 +450,10 @@ export async function handleAuthCommand(
450
450
  if (!isAdmin(ctx)) {
451
451
  return {
452
452
  text:
453
- `<b>Not authorized.</b> <code>/auth ${parsed.kind}</code> is admin-only.\n` +
454
- `Set <code>admin: true</code> on this agent in switchroom.yaml to unlock ` +
455
- `(the same flag that gates <code>/agents</code>, <code>/restart</code>, ` +
456
- `<code>/update</code> etc.).`,
453
+ `**Not authorized.** \`/auth ${parsed.kind}\` is admin-only.\n` +
454
+ `Set \`admin: true\` on this agent in switchroom.yaml to unlock ` +
455
+ `(the same flag that gates \`/agents\`, \`/restart\`, ` +
456
+ `\`/update\` etc.).`,
457
457
  html: true,
458
458
  }
459
459
  }
@@ -466,7 +466,7 @@ export async function handleAuthCommand(
466
466
  if (parsed.kind === 'add' || parsed.kind === 'cancel') {
467
467
  return {
468
468
  text:
469
- `<b>/auth ${parsed.kind} not routed.</b> Internal error — gateway should dispatch this verb directly. Report this.`,
469
+ `**/auth ${parsed.kind} not routed.** Internal error — gateway should dispatch this verb directly. Report this.`,
470
470
  html: true,
471
471
  }
472
472
  }
@@ -476,13 +476,13 @@ export async function handleAuthCommand(
476
476
  const result = await ctx.client.setActive(parsed.label)
477
477
  return {
478
478
  text:
479
- `<b>Active account →</b> <code>${escapeHtml(result.active)}</code>\n` +
479
+ `**Active account →** \`${escapeHtml(result.active)}\`\n` +
480
480
  `Re-mirrored credentials for ${result.fanned.length} agent${result.fanned.length === 1 ? '' : 's'}.`,
481
481
  html: true,
482
482
  }
483
483
  } catch (err) {
484
484
  return {
485
- text: `<b>/auth use failed:</b> ${escapeHtml((err as Error)?.message ?? String(err))}`,
485
+ text: `**/auth use failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
486
486
  html: true,
487
487
  }
488
488
  }
@@ -495,8 +495,8 @@ export async function handleAuthCommand(
495
495
  if (!nextLabel) {
496
496
  return {
497
497
  text:
498
- `<b>/auth rotate</b> — no eligible target.\n` +
499
- `Either every account in <code>fallback_order</code> is exhausted, ` +
498
+ `**/auth rotate** — no eligible target.\n` +
499
+ `Either every account in \`fallback_order\` is exhausted, ` +
500
500
  `or no fallback order is configured.`,
501
501
  html: true,
502
502
  }
@@ -504,13 +504,13 @@ export async function handleAuthCommand(
504
504
  const result = await ctx.client.setActive(nextLabel)
505
505
  return {
506
506
  text:
507
- `<b>Rotated:</b> active → <code>${escapeHtml(result.active)}</code>\n` +
507
+ `**Rotated:** active → \`${escapeHtml(result.active)}\`\n` +
508
508
  `Re-mirrored credentials for ${result.fanned.length} agent${result.fanned.length === 1 ? '' : 's'}.`,
509
509
  html: true,
510
510
  }
511
511
  } catch (err) {
512
512
  return {
513
- text: `<b>/auth rotate failed:</b> ${escapeHtml((err as Error)?.message ?? String(err))}`,
513
+ text: `**/auth rotate failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
514
514
  html: true,
515
515
  }
516
516
  }
@@ -525,7 +525,7 @@ export async function handleAuthCommand(
525
525
  state = await ctx.client.listState()
526
526
  } catch (err) {
527
527
  return {
528
- text: `<b>/auth rm failed:</b> ${escapeHtml((err as Error)?.message ?? String(err))}`,
528
+ text: `**/auth rm failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
529
529
  html: true,
530
530
  }
531
531
  }
@@ -533,16 +533,16 @@ export async function handleAuthCommand(
533
533
  if (!exists) {
534
534
  return {
535
535
  text:
536
- `<b>/auth rm:</b> no account named <code>${escapeHtml(parsed.label)}</code>. ` +
537
- `Run <code>/auth show</code> for the current list.`,
536
+ `**/auth rm:** no account named \`${escapeHtml(parsed.label)}\`. ` +
537
+ `Run \`/auth show\` for the current list.`,
538
538
  html: true,
539
539
  }
540
540
  }
541
541
  if (state.active === parsed.label) {
542
542
  return {
543
543
  text:
544
- `<b>/auth rm refused.</b> <code>${escapeHtml(parsed.label)}</code> is the fleet active. ` +
545
- `Switch with <code>/auth use &lt;other&gt;</code> or <code>/auth rotate</code> first.`,
544
+ `**/auth rm refused.** \`${escapeHtml(parsed.label)}\` is the fleet active. ` +
545
+ `Switch with \`/auth use <other>\` or \`/auth rotate\` first.`,
546
546
  html: true,
547
547
  }
548
548
  }
@@ -560,9 +560,9 @@ export async function handleAuthCommand(
560
560
  }
561
561
  return {
562
562
  text:
563
- `<b>⚠ /auth rm</b> — about to remove <code>${escapeHtml(parsed.label)}</code> from the broker.\n` +
564
- `The fleet active is unchanged. Any agent override pointing at <code>${escapeHtml(parsed.label)}</code> will stop working.\n\n` +
565
- `Send <code>/auth rm ${escapeHtml(parsed.label)} confirm</code> within ${Math.round(
563
+ `**⚠ /auth rm** — about to remove \`${escapeHtml(parsed.label)}\` from the broker.\n` +
564
+ `The fleet active is unchanged. Any agent override pointing at \`${escapeHtml(parsed.label)}\` will stop working.\n\n` +
565
+ `Send \`/auth rm ${escapeHtml(parsed.label)} confirm\` within ${Math.round(
566
566
  AUTH_RM_CONFIRM_TTL_MS / 1000,
567
567
  )}s to proceed.`,
568
568
  html: true,
@@ -578,8 +578,8 @@ export async function handleAuthCommand(
578
578
  }
579
579
  return {
580
580
  text:
581
- `<b>/auth rm:</b> no pending confirm for <code>${escapeHtml(parsed.label)}</code> (expired or not started). ` +
582
- `Send <code>/auth rm ${escapeHtml(parsed.label)}</code> first.`,
581
+ `**/auth rm:** no pending confirm for \`${escapeHtml(parsed.label)}\` (expired or not started). ` +
582
+ `Send \`/auth rm ${escapeHtml(parsed.label)}\` first.`,
583
583
  html: true,
584
584
  }
585
585
  }
@@ -589,12 +589,12 @@ export async function handleAuthCommand(
589
589
  try {
590
590
  const data = await ctx.client.rmAccount(parsed.label)
591
591
  return {
592
- text: `<b>Removed</b> <code>${escapeHtml(data.label)}</code> from the broker.`,
592
+ text: `**Removed** \`${escapeHtml(data.label)}\` from the broker.`,
593
593
  html: true,
594
594
  }
595
595
  } catch (err) {
596
596
  return {
597
- text: `<b>/auth rm failed:</b> ${escapeHtml((err as Error)?.message ?? String(err))}`,
597
+ text: `**/auth rm failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
598
598
  html: true,
599
599
  }
600
600
  }
@@ -609,12 +609,12 @@ export async function handleAuthCommand(
609
609
  if (parsed.label && targets.length === 0) {
610
610
  return {
611
611
  text:
612
- `<b>/auth refresh:</b> no account named <code>${escapeHtml(parsed.label)}</code>.`,
612
+ `**/auth refresh:** no account named \`${escapeHtml(parsed.label)}\`.`,
613
613
  html: true,
614
614
  }
615
615
  }
616
616
  if (targets.length === 0) {
617
- return { text: `<b>/auth refresh:</b> no accounts to refresh.`, html: true }
617
+ return { text: `**/auth refresh:** no accounts to refresh.`, html: true }
618
618
  }
619
619
  const oldByLabel = new Map(state.accounts.map((a) => [a.label, a.expiresAt]))
620
620
  const rows: string[][] = [['ACCOUNT', 'OLD EXPIRY', 'NEW EXPIRY']]
@@ -635,18 +635,18 @@ export async function handleAuthCommand(
635
635
  }
636
636
  const head =
637
637
  targets.length === 1
638
- ? `<b>Refreshed</b> <code>${escapeHtml(targets[0]!)}</code>`
639
- : `<b>Refreshed</b> ${rows.length - 1}/${targets.length} account${targets.length === 1 ? '' : 's'}`
638
+ ? `**Refreshed** \`${escapeHtml(targets[0]!)}\``
639
+ : `**Refreshed** ${rows.length - 1}/${targets.length} account${targets.length === 1 ? '' : 's'}`
640
640
  const table = rows.length > 1
641
- ? `\n<pre>${alignTable(rows)}</pre>`
641
+ ? "\n```\n" + alignTable(rows) + "\n```"
642
642
  : ''
643
643
  const failBlock = failures.length > 0
644
- ? `\n<b>Failures:</b>\n${failures.map((f) => ` ${f}`).join('\n')}`
644
+ ? `\n**Failures:**\n${failures.map((f) => ` ${f}`).join('\n')}`
645
645
  : ''
646
646
  return { text: head + table + failBlock, html: true }
647
647
  } catch (err) {
648
648
  return {
649
- text: `<b>/auth refresh failed:</b> ${escapeHtml((err as Error)?.message ?? String(err))}`,
649
+ text: `**/auth refresh failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
650
650
  html: true,
651
651
  }
652
652
  }
@@ -657,13 +657,13 @@ export async function handleAuthCommand(
657
657
  const data = await ctx.client.setOverride(parsed.agent, parsed.label)
658
658
  return {
659
659
  text:
660
- `<b>Override set.</b> <code>${escapeHtml(data.agent)}</code> is now pinned to ` +
661
- `<code>${escapeHtml(data.account ?? parsed.label)}</code>.`,
660
+ `**Override set.** \`${escapeHtml(data.agent)}\` is now pinned to ` +
661
+ `\`${escapeHtml(data.account ?? parsed.label)}\`.`,
662
662
  html: true,
663
663
  }
664
664
  } catch (err) {
665
665
  return {
666
- text: `<b>/auth agent override failed:</b> ${escapeHtml((err as Error)?.message ?? String(err))}`,
666
+ text: `**/auth agent override failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
667
667
  html: true,
668
668
  }
669
669
  }
@@ -674,13 +674,13 @@ export async function handleAuthCommand(
674
674
  const data = await ctx.client.setOverride(parsed.agent, null)
675
675
  return {
676
676
  text:
677
- `<b>Override cleared</b> on <code>${escapeHtml(data.agent)}</code> ` +
677
+ `**Override cleared** on \`${escapeHtml(data.agent)}\` ` +
678
678
  `— back to fleet active.`,
679
679
  html: true,
680
680
  }
681
681
  } catch (err) {
682
682
  return {
683
- text: `<b>/auth agent override failed:</b> ${escapeHtml((err as Error)?.message ?? String(err))}`,
683
+ text: `**/auth agent override failed:** ${escapeHtml((err as Error)?.message ?? String(err))}`,
684
684
  html: true,
685
685
  }
686
686
  }
@@ -690,7 +690,7 @@ export async function handleAuthCommand(
690
690
  const _exhaustive: never = parsed
691
691
  void _exhaustive
692
692
  return {
693
- text: `<b>/auth:</b> unhandled verb. Report this.`,
693
+ text: `**/auth:** unhandled verb. Report this.`,
694
694
  html: true,
695
695
  }
696
696
  }
@@ -800,37 +800,37 @@ export function renderShowText(
800
800
  }),
801
801
  )
802
802
  } else {
803
- lines.push('<b>Auth — fleet snapshot</b>')
803
+ lines.push('**Auth — fleet snapshot**')
804
804
  if (state.accounts.length > 0) {
805
805
  lines.push('')
806
- lines.push('<b>Accounts</b>')
807
- lines.push('<pre>')
806
+ lines.push('**Accounts**')
807
+ lines.push('```')
808
808
  lines.push(formatAccountsTable(state, now, opts.demo ?? false))
809
- lines.push('</pre>')
809
+ lines.push('```')
810
810
  }
811
811
  }
812
812
 
813
813
  // Agents table
814
814
  if (state.agents.length > 0) {
815
- lines.push('<b>Agents</b>')
816
- lines.push('<pre>')
815
+ lines.push('**Agents**')
816
+ lines.push('```')
817
817
  lines.push(formatAgentsTable(state, opts.demo ?? false))
818
- lines.push('</pre>')
818
+ lines.push('```')
819
819
  }
820
820
 
821
821
  // Consumers table — only when there are any (typical case: hindsight).
822
822
  if (state.consumers.length > 0) {
823
- lines.push('<b>Consumers</b>')
824
- lines.push('<pre>')
823
+ lines.push('**Consumers**')
824
+ lines.push('```')
825
825
  lines.push(formatConsumersTable(state, now, opts.demo ?? false))
826
- lines.push('</pre>')
826
+ lines.push('```')
827
827
  }
828
828
 
829
829
  // Discovery hint — operators on a quota-walled fleet need to know
830
830
  // `/auth add` exists so they can add a fresh account without an
831
831
  // LLM in the loop. Keep it short; the help text has the full menu.
832
832
  lines.push(
833
- '<i>Add a new Anthropic account: <code>/auth add &lt;label&gt;</code> (admin)</i>',
833
+ '_Add a new Anthropic account: \`/auth add <label>\` (admin)_',
834
834
  )
835
835
 
836
836
  return lines.join('\n')
@@ -909,10 +909,10 @@ export function renderAgentDetail(
909
909
  now: number = Date.now(),
910
910
  ): string {
911
911
  const lines: string[] = []
912
- lines.push(`<b>${escapeHtml(agent.name)}</b>`)
912
+ lines.push(`**${escapeHtml(agent.name)}**`)
913
913
  const source = agent.override ? 'override' : 'fleet-active'
914
914
  lines.push(
915
- `Active account: <code>${escapeHtml(agent.account)}</code> (${source})`,
915
+ `Active account: \`${escapeHtml(agent.account)}\` (${source})`,
916
916
  )
917
917
  const acct = state.accounts.find((a) => a.label === agent.account)
918
918
  if (acct) {
@@ -928,11 +928,11 @@ export function renderAgentDetail(
928
928
  acct.exhausted_until != null && acct.exhausted_until > now
929
929
  ? formatRelativeMs(acct.exhausted_until - now)
930
930
  : '—'
931
- lines.push(`<i>Quota: exhausted · resets in ${resetRel}</i>`)
931
+ lines.push(`_Quota: exhausted · resets in ${resetRel}_`)
932
932
  }
933
933
  if (typeof acct.threshold_violations === 'number' && acct.threshold_violations > 0) {
934
934
  lines.push(
935
- `<i>Threshold violations: ${acct.threshold_violations} — claude refreshed under the broker's feet</i>`,
935
+ `_Threshold violations: ${acct.threshold_violations} — claude refreshed under the broker's feet_`,
936
936
  )
937
937
  }
938
938
  }
@@ -956,7 +956,7 @@ function formatConsumersTable(state: ListStateData, now: number, demo = false):
956
956
  // ─── Plain-text helpers ────────────────────────────────────────────────────
957
957
 
958
958
  function escapeHtml(s: string): string {
959
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
959
+ return s.replace(/([\\`*_~=\[\]|])/g, '\\$1')
960
960
  }
961
961
 
962
962
  function formatRelativeMs(ms: number): string {
@@ -22,16 +22,6 @@ import type { ListStateData, AccountState } from '../../src/auth/broker/client.j
22
22
 
23
23
  export type { ListStateData, AccountState }
24
24
 
25
- // Local HTML-escape (mirrors the helper formerly co-located in
26
- // auth-dashboard.ts so we keep the same escaping discipline without
27
- // pulling in a heavier util).
28
- function escapeHtml(s: string): string {
29
- return s
30
- .replace(/&/g, '&amp;')
31
- .replace(/</g, '&lt;')
32
- .replace(/>/g, '&gt;')
33
- }
34
-
35
25
  /** Format a duration in ms as a short relative string ("1h 22m", "12s"). */
36
26
  function formatRelativeMs(ms: number): string {
37
27
  if (ms <= 0) return '0s'
@@ -55,9 +45,9 @@ export function formatAuthQuotaLine(acc: AccountState, now: number = Date.now())
55
45
  if (acc.exhausted) {
56
46
  const until = acc.exhausted_until
57
47
  if (until != null && until > now) {
58
- return `<i>exhausted · resets in ${formatRelativeMs(until - now)}</i>`
48
+ return `_exhausted · resets in ${formatRelativeMs(until - now)}_`
59
49
  }
60
- return `<i>exhausted</i>`
50
+ return `_exhausted_`
61
51
  }
62
52
  return null
63
53
  }
@@ -110,14 +100,14 @@ export function renderAuthLine(
110
100
 
111
101
  const byLabel = new Map(state.accounts.map((a) => [a.label, a]))
112
102
  const rows: string[] = []
113
- rows.push(`<b>Accounts (${state.accounts.length})</b>`)
103
+ rows.push(`**Accounts (${state.accounts.length})**`)
114
104
  for (const label of order) {
115
105
  const acc = byLabel.get(label)
116
106
  if (!acc) continue
117
107
  const marker = label === activeLabel ? '▶' : '↳'
118
- const labelHtml = `<code>${escapeHtml(acc.label)}</code>`
108
+ const labelMd = `\`${acc.label}\``
119
109
  const quotaLine = formatAuthQuotaLine(acc, now)
120
- rows.push(quotaLine ? `${marker} ${labelHtml} ${quotaLine}` : `${marker} ${labelHtml}`)
110
+ rows.push(quotaLine ? `${marker} ${labelMd} ${quotaLine}` : `${marker} ${labelMd}`)
121
111
  }
122
112
  return rows
123
113
  }