switchroom 0.8.1 → 0.10.0

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 (105) hide show
  1. package/README.md +49 -57
  2. package/bin/timezone-hook.sh +9 -7
  3. package/dist/agent-scheduler/index.js +285 -45
  4. package/dist/auth-broker/index.js +13932 -0
  5. package/dist/cli/switchroom.js +15931 -12778
  6. package/dist/host-control/main.js +582 -43
  7. package/dist/vault/approvals/kernel-server.js +276 -47
  8. package/dist/vault/broker/server.js +333 -69
  9. package/examples/minimal.yaml +63 -0
  10. package/examples/personal-google-workspace-mcp/.env.example +34 -0
  11. package/examples/personal-google-workspace-mcp/README.md +194 -0
  12. package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
  13. package/examples/switchroom.yaml +220 -0
  14. package/package.json +6 -4
  15. package/profiles/_base/start.sh.hbs +3 -3
  16. package/profiles/_shared/agent-self-service.md.hbs +126 -0
  17. package/profiles/default/CLAUDE.md +10 -0
  18. package/profiles/default/CLAUDE.md.hbs +16 -0
  19. package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
  20. package/skills/buildkite-agent-runtime/SKILL.md +44 -11
  21. package/skills/buildkite-api/SKILL.md +31 -8
  22. package/skills/buildkite-cli/SKILL.md +27 -9
  23. package/skills/buildkite-migration/SKILL.md +22 -9
  24. package/skills/buildkite-pipelines/SKILL.md +26 -9
  25. package/skills/buildkite-secure-delivery/SKILL.md +23 -9
  26. package/skills/buildkite-test-engine/SKILL.md +25 -8
  27. package/skills/docx/SKILL.md +1 -1
  28. package/skills/file-bug/SKILL.md +34 -6
  29. package/skills/humanizer/SKILL.md +15 -0
  30. package/skills/humanizer-calibrate/SKILL.md +7 -1
  31. package/skills/mcp-builder/SKILL.md +1 -1
  32. package/skills/pdf/SKILL.md +1 -1
  33. package/skills/pptx/SKILL.md +1 -1
  34. package/skills/skill-creator/SKILL.md +21 -1
  35. package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  36. package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
  37. package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
  38. package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
  39. package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
  40. package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  41. package/skills/switchroom-cli/SKILL.md +63 -64
  42. package/skills/switchroom-health/SKILL.md +23 -10
  43. package/skills/switchroom-install/SKILL.md +3 -3
  44. package/skills/switchroom-manage/SKILL.md +26 -19
  45. package/skills/switchroom-runtime/SKILL.md +67 -15
  46. package/skills/switchroom-status/SKILL.md +26 -1
  47. package/skills/telegram-test-harness/SKILL.md +3 -0
  48. package/skills/webapp-testing/SKILL.md +31 -1
  49. package/skills/xlsx/SKILL.md +1 -1
  50. package/telegram-plugin/admin-commands/index.ts +7 -5
  51. package/telegram-plugin/dist/gateway/gateway.js +13042 -12844
  52. package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
  53. package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
  54. package/telegram-plugin/gateway/auth-command.ts +794 -0
  55. package/telegram-plugin/gateway/auth-line.ts +123 -0
  56. package/telegram-plugin/gateway/boot-card.ts +22 -36
  57. package/telegram-plugin/gateway/boot-probes.ts +3 -3
  58. package/telegram-plugin/gateway/gateway.ts +313 -798
  59. package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
  60. package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
  61. package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
  62. package/telegram-plugin/permission-title.ts +56 -0
  63. package/telegram-plugin/quota-check.ts +19 -41
  64. package/telegram-plugin/scripts/build.mjs +0 -1
  65. package/telegram-plugin/shared/bot-runtime.ts +5 -4
  66. package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
  67. package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
  68. package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
  69. package/telegram-plugin/tests/boot-probes.test.ts +11 -4
  70. package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
  71. package/telegram-plugin/tests/permission-title.test.ts +31 -0
  72. package/telegram-plugin/tests/quota-check.test.ts +5 -35
  73. package/telegram-plugin/uat/SETUP.md +31 -1
  74. package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
  75. package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
  76. package/telegram-plugin/uat/runners/report.ts +150 -0
  77. package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
  78. package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
  79. package/telegram-plugin/uat/runners/scorer.ts +106 -0
  80. package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
  81. package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
  82. package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +7 -1
  83. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +7 -1
  84. package/telegram-plugin/auth-dashboard.ts +0 -1104
  85. package/telegram-plugin/auth-slot-parser.ts +0 -497
  86. package/telegram-plugin/dist/foreman/foreman.js +0 -31358
  87. package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
  88. package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
  89. package/telegram-plugin/foreman/foreman.ts +0 -1165
  90. package/telegram-plugin/foreman/setup-flow.ts +0 -345
  91. package/telegram-plugin/foreman/setup-state.ts +0 -239
  92. package/telegram-plugin/foreman/state.ts +0 -203
  93. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
  94. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
  95. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
  96. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
  97. package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
  98. package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
  99. package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
  100. package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
  101. package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
  102. package/telegram-plugin/tests/foreman-state.test.ts +0 -164
  103. package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
  104. package/telegram-plugin/tests/setup-flow.test.ts +0 -510
  105. package/telegram-plugin/tests/setup-state.test.ts +0 -146
@@ -1,497 +0,0 @@
1
- /**
2
- * Pure logic for the `/auth` slot-management sub-verbs (add/use/list/rm).
3
- *
4
- * Lives outside gateway.ts + server.ts so it's unit-testable without
5
- * spinning up a grammy bot. The gateway/server command handlers call
6
- * `parseAuthSubCommand` to turn a raw /auth argv into a dispatch plan
7
- * (switchroom CLI args + label + optional post-action hook), then
8
- * handle that plan via their existing runSwitchroomCommand pipeline.
9
- */
10
-
11
- /** Pattern used by slot names throughout switchroom. Matches the shape
12
- * used by `addAccountStart` and slot-dir naming in src/auth/accounts.ts. */
13
- const SLOT_NAME_RE = /^[a-zA-Z0-9_-]{1,32}$/;
14
-
15
- export function assertSafeSlotName(slot: string): void {
16
- if (!SLOT_NAME_RE.test(slot)) {
17
- throw new Error(`invalid slot name: ${slot}`);
18
- }
19
- }
20
-
21
- /** Pattern used by global account labels — matches validateAccountLabel
22
- * in src/auth/account-store.ts. Allows email-shaped labels
23
- * (`pixsoul@gmail.com`) and gmail-tag forms (`ken+work@example.com`).
24
- * Excludes `:` (callback_data separator), path separators, shell
25
- * metas, and whitespace. Max 64 chars. Keep in sync with `LABEL_RE`
26
- * in account-store.ts and `isSafeAccountLabel` in auth-dashboard.ts. */
27
- const ACCOUNT_LABEL_RE = /^[A-Za-z0-9._@+-]{1,64}$/;
28
-
29
- export function assertSafeAccountLabel(label: string): void {
30
- if (label === '.' || label === '..') {
31
- throw new Error(`invalid account label: ${label}`);
32
- }
33
- if (!ACCOUNT_LABEL_RE.test(label)) {
34
- throw new Error(`invalid account label: ${label}`);
35
- }
36
- }
37
-
38
- /** Agent-name check mirrored from gateway.ts so the parser doesn't
39
- * need to import gateway.ts (which has top-level side effects). */
40
- const AGENT_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
41
- export function assertSafeAgentNameForParser(name: string): void {
42
- if (name !== 'all' && !AGENT_NAME_RE.test(name)) {
43
- throw new Error(`invalid agent name: ${name}`);
44
- }
45
- }
46
-
47
- export type AuthIntent =
48
- | { kind: 'login' | 'reauth' | 'link'; agent: string; label: string; cliArgs: string[]; registerReauth: boolean }
49
- | { kind: 'code'; agent: string; code: string; label: string; cliArgs: string[] }
50
- | { kind: 'cancel'; agent: string; label: string; cliArgs: string[] }
51
- | { kind: 'status'; label: string; cliArgs: string[] }
52
- | { kind: 'add'; agent: string; slot?: string; label: string; cliArgs: string[] }
53
- | { kind: 'use'; agent: string; slot: string; force: boolean; label: string; cliArgs: string[]; restartAgentAfter: true }
54
- | { kind: 'list'; agent: string; label: string; cliArgs: string[] }
55
- | { kind: 'rm'; agent: string; slot: string; force: boolean; label: string; cliArgs: string[] }
56
- // ── New account-shaped verbs (see reference/share-auth-across-the-fleet.md) ──
57
- | { kind: 'account-add'; account: string; fromAgent: string; label: string; cliArgs: string[] }
58
- | { kind: 'account-list'; label: string; cliArgs: string[] }
59
- | { kind: 'account-rm'; account: string; label: string; cliArgs: string[] }
60
- | { kind: 'account-rename'; oldAccount: string; newAccount: string; label: string; cliArgs: string[] }
61
- | { kind: 'enable'; account: string; agents: string[]; label: string; cliArgs: string[]; restartAgentsAfter: true }
62
- | { kind: 'disable'; account: string; agents: string[]; label: string; cliArgs: string[] }
63
- | { kind: 'share'; account: string; fromAgent: string; label: string; cliArgs: string[]; restartAgentsAfter: true }
64
- | { kind: 'usage'; message: string }
65
- | { kind: 'error'; message: string };
66
-
67
- export const AUTH_VERBS = [
68
- 'login', 'reauth', 'link',
69
- 'code', 'cancel', 'status',
70
- 'add', 'use', 'list', 'rm',
71
- // New account-shaped verbs
72
- 'account', 'enable', 'disable', 'share',
73
- ] as const;
74
-
75
- /** Help/usage string shown for unknown subcommands. Keep wording close
76
- * to the previous inline usage so the help-text asserting tests
77
- * naturally catch drift. */
78
- export function usageText(): string {
79
- return [
80
- 'Usage:',
81
- '/auth — status dashboard',
82
- '',
83
- 'Per-agent (legacy slot model):',
84
- '/auth login [agent] — start OAuth for agent',
85
- '/auth reauth [agent] — re-auth from scratch',
86
- '/auth code [agent] <browser-code> — finish OAuth flow',
87
- '/auth cancel [agent] — cancel pending flow',
88
- '/auth add [agent] [--slot <name>] — add another slot',
89
- '/auth use [agent] <slot> [--force] — switch active slot',
90
- '/auth list [agent] — list slots',
91
- '/auth rm [agent] <slot> [--force] — remove a slot',
92
- '',
93
- 'Anthropic accounts (shared across agents):',
94
- '/auth account add <label> [--from-agent <name>] — promote slot to global account',
95
- '/auth account list — accounts + agents using each',
96
- '/auth account rm <label> — remove (refused if enabled)',
97
- '/auth account rename <old> <new> — rename account + rewrite agents.<name>.auth.accounts lists',
98
- '/auth enable <label> [agents...|all] — wire account to agent(s); "all" = every agent',
99
- '/auth disable <label> [agents...|all] — unwire account from agent(s); "all" = every agent',
100
- '/auth share <label> [--from-agent <name>] — account add + enable on every agent in one step',
101
- ].join('\n');
102
- }
103
-
104
- /**
105
- * Turn raw /auth argv into a dispatch intent.
106
- *
107
- * `parts` is the whitespace-split tail of the /auth command (no leading
108
- * "/auth"). `currentAgent` is the agent this gateway process represents.
109
- * Missing agent arg defaults to `currentAgent` so single-agent setups
110
- * Just Work without typing the name.
111
- */
112
- export function parseAuthSubCommand(
113
- parts: string[],
114
- currentAgent: string,
115
- ): AuthIntent {
116
- const sub = (parts[0] ?? 'status').toLowerCase();
117
-
118
- // Existing verbs — kept here so both gateway.ts and server.ts can
119
- // route them through a single source of truth once they migrate.
120
- if (sub === 'login' || sub === 'reauth' || sub === 'link') {
121
- const agent = parts[1] ?? currentAgent;
122
- try { assertSafeAgentNameForParser(agent); }
123
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
124
- return {
125
- kind: sub,
126
- agent,
127
- label: `auth ${sub} ${agent}`,
128
- cliArgs: ['auth', sub, agent],
129
- registerReauth: sub === 'reauth' || sub === 'login',
130
- };
131
- }
132
-
133
- if (sub === 'code') {
134
- let agent = currentAgent; let code = '';
135
- if (parts.length >= 3) { agent = parts[1]; code = parts.slice(2).join(' '); }
136
- else if (parts.length === 2) { code = parts[1]; }
137
- if (!code) return { kind: 'usage', message: 'Usage: /auth code [agent] <browser-code>' };
138
- try { assertSafeAgentNameForParser(agent); }
139
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
140
- return { kind: 'code', agent, code, label: `auth code ${agent}`, cliArgs: ['auth', 'code', agent, code] };
141
- }
142
-
143
- if (sub === 'cancel') {
144
- const agent = parts[1] ?? currentAgent;
145
- try { assertSafeAgentNameForParser(agent); }
146
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
147
- return { kind: 'cancel', agent, label: `auth cancel ${agent}`, cliArgs: ['auth', 'cancel', agent] };
148
- }
149
-
150
- if (sub === 'status') {
151
- return { kind: 'status', label: 'auth status', cliArgs: ['auth', 'status'] };
152
- }
153
-
154
- // --- New slot-management verbs ---
155
-
156
- if (sub === 'add') {
157
- // /auth add [agent] [--slot <name>]
158
- const rest = parts.slice(1);
159
- const { flags, positional } = splitFlags(rest, ['--slot']);
160
- const agent = positional[0] ?? currentAgent;
161
- // splitFlags returns `string | true | undefined` for value flags
162
- // (true when the flag is present without a value). For `--slot` we
163
- // expect a string value; reject the bare-flag form.
164
- const rawSlot = flags['--slot'];
165
- const slot = typeof rawSlot === 'string' ? rawSlot : undefined;
166
- try { assertSafeAgentNameForParser(agent); }
167
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
168
- if (slot !== undefined) {
169
- try { assertSafeSlotName(slot); }
170
- catch { return { kind: 'error', message: 'Invalid slot name. Use [A-Za-z0-9_-], 1-32 chars.' }; }
171
- }
172
- const cliArgs = ['auth', 'add', agent];
173
- if (slot) cliArgs.push('--slot', slot);
174
- return { kind: 'add', agent, slot, label: `auth add ${agent}`, cliArgs };
175
- }
176
-
177
- if (sub === 'use') {
178
- // /auth use [agent] <slot> [--force]
179
- const rest = parts.slice(1);
180
- const { flags, positional } = splitFlags(rest, []);
181
- if (positional.length === 0) {
182
- return { kind: 'usage', message: 'Usage: /auth use [agent] <slot> [--force]' };
183
- }
184
- const [agent, slot] = positional.length === 1
185
- ? [currentAgent, positional[0]]
186
- : [positional[0], positional[1]];
187
- try { assertSafeAgentNameForParser(agent); }
188
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
189
- try { assertSafeSlotName(slot); }
190
- catch { return { kind: 'error', message: 'Invalid slot name. Use [A-Za-z0-9_-], 1-32 chars.' }; }
191
- return {
192
- kind: 'use', agent, slot,
193
- force: flags['--force'] === true,
194
- label: `auth use ${agent} ${slot}`,
195
- cliArgs: ['auth', 'use', agent, slot],
196
- restartAgentAfter: true,
197
- };
198
- }
199
-
200
- if (sub === 'list') {
201
- const agent = parts[1] ?? currentAgent;
202
- try { assertSafeAgentNameForParser(agent); }
203
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
204
- return {
205
- kind: 'list', agent,
206
- label: `auth list ${agent}`,
207
- cliArgs: ['auth', 'list', agent, '--json'],
208
- };
209
- }
210
-
211
- if (sub === 'rm') {
212
- // /auth rm [agent] <slot> [--force]
213
- const rest = parts.slice(1);
214
- const { flags, positional } = splitFlags(rest, ['--force']);
215
- if (positional.length === 0) {
216
- return { kind: 'usage', message: 'Usage: /auth rm [agent] <slot> [--force]' };
217
- }
218
- const [agent, slot] = positional.length === 1
219
- ? [currentAgent, positional[0]]
220
- : [positional[0], positional[1]];
221
- try { assertSafeAgentNameForParser(agent); }
222
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
223
- try { assertSafeSlotName(slot); }
224
- catch { return { kind: 'error', message: 'Invalid slot name. Use [A-Za-z0-9_-], 1-32 chars.' }; }
225
- const force = flags['--force'] === true;
226
- return {
227
- kind: 'rm', agent, slot, force,
228
- label: `auth rm ${agent} ${slot}`,
229
- cliArgs: ['auth', 'rm', agent, slot],
230
- };
231
- }
232
-
233
- // --- Account-shaped verbs (see reference/share-auth-across-the-fleet.md) ---
234
-
235
- if (sub === 'account') {
236
- const accountSub = (parts[1] ?? 'list').toLowerCase();
237
-
238
- if (accountSub === 'add') {
239
- // /auth account add <label> [--from-agent <name>]
240
- // Default --from-agent to the current agent — that's the common case
241
- // for a Telegram-only operator who just /auth login'd this agent.
242
- const rest = parts.slice(2);
243
- const { flags, positional } = splitFlags(rest, ['--from-agent']);
244
- const account = positional[0];
245
- if (!account) {
246
- return {
247
- kind: 'usage',
248
- message: 'Usage: /auth account add <label> [--from-agent <name>]',
249
- };
250
- }
251
- try { assertSafeAccountLabel(account); }
252
- catch { return { kind: 'error', message: 'Invalid account label. Use [A-Za-z0-9._@+-], 1-64 chars (email shape OK).' }; }
253
- const fromAgentRaw = flags['--from-agent'];
254
- const fromAgent = typeof fromAgentRaw === 'string' ? fromAgentRaw : currentAgent;
255
- try { assertSafeAgentNameForParser(fromAgent); }
256
- catch { return { kind: 'error', message: 'Invalid --from-agent value.' }; }
257
- return {
258
- kind: 'account-add',
259
- account,
260
- fromAgent,
261
- label: `auth account add ${account}`,
262
- cliArgs: ['auth', 'account', 'add', account, '--from-agent', fromAgent],
263
- };
264
- }
265
-
266
- if (accountSub === 'list') {
267
- return {
268
- kind: 'account-list',
269
- label: 'auth account list',
270
- cliArgs: ['auth', 'account', 'list'],
271
- };
272
- }
273
-
274
- if (accountSub === 'rm') {
275
- // /auth account rm <label>
276
- const account = parts[2];
277
- if (!account) {
278
- return { kind: 'usage', message: 'Usage: /auth account rm <label>' };
279
- }
280
- try { assertSafeAccountLabel(account); }
281
- catch { return { kind: 'error', message: 'Invalid account label.' }; }
282
- return {
283
- kind: 'account-rm',
284
- account,
285
- label: `auth account rm ${account}`,
286
- cliArgs: ['auth', 'account', 'rm', account],
287
- };
288
- }
289
-
290
- if (accountSub === 'rename') {
291
- // /auth account rename <oldLabel> <newLabel>
292
- const oldAccount = parts[2];
293
- const newAccount = parts[3];
294
- if (!oldAccount || !newAccount) {
295
- return {
296
- kind: 'usage',
297
- message: 'Usage: /auth account rename <oldLabel> <newLabel>',
298
- };
299
- }
300
- try { assertSafeAccountLabel(oldAccount); assertSafeAccountLabel(newAccount); }
301
- catch { return { kind: 'error', message: 'Invalid account label.' }; }
302
- if (oldAccount === newAccount) {
303
- return { kind: 'error', message: `Account "${oldAccount}" already has that name — nothing to do.` };
304
- }
305
- return {
306
- kind: 'account-rename',
307
- oldAccount,
308
- newAccount,
309
- label: `auth account rename ${oldAccount} ${newAccount}`,
310
- cliArgs: ['auth', 'account', 'rename', oldAccount, newAccount],
311
- };
312
- }
313
-
314
- return {
315
- kind: 'usage',
316
- message: 'Usage: /auth account add | list | rm | rename (see /auth)',
317
- };
318
- }
319
-
320
- if (sub === 'enable') {
321
- // /auth enable <label> [agents...] — defaults to the current agent.
322
- const rest = parts.slice(1);
323
- const account = rest[0];
324
- if (!account) {
325
- return { kind: 'usage', message: 'Usage: /auth enable <label> [agents...]' };
326
- }
327
- try { assertSafeAccountLabel(account); }
328
- catch { return { kind: 'error', message: 'Invalid account label.' }; }
329
- const agents = rest.slice(1);
330
- if (agents.length === 0) agents.push(currentAgent);
331
- for (const a of agents) {
332
- try { assertSafeAgentNameForParser(a); }
333
- catch { return { kind: 'error', message: `Invalid agent name: ${a}` }; }
334
- }
335
- return {
336
- kind: 'enable',
337
- account,
338
- agents,
339
- label: `auth enable ${account} ${agents.join(' ')}`,
340
- cliArgs: ['auth', 'enable', account, ...agents],
341
- restartAgentsAfter: true,
342
- };
343
- }
344
-
345
- if (sub === 'disable') {
346
- // /auth disable <label> [agents...] — defaults to the current agent.
347
- const rest = parts.slice(1);
348
- const account = rest[0];
349
- if (!account) {
350
- return { kind: 'usage', message: 'Usage: /auth disable <label> [agents...]' };
351
- }
352
- try { assertSafeAccountLabel(account); }
353
- catch { return { kind: 'error', message: 'Invalid account label.' }; }
354
- const agents = rest.slice(1);
355
- if (agents.length === 0) agents.push(currentAgent);
356
- for (const a of agents) {
357
- try { assertSafeAgentNameForParser(a); }
358
- catch { return { kind: 'error', message: `Invalid agent name: ${a}` }; }
359
- }
360
- return {
361
- kind: 'disable',
362
- account,
363
- agents,
364
- label: `auth disable ${account} ${agents.join(' ')}`,
365
- cliArgs: ['auth', 'disable', account, ...agents],
366
- };
367
- }
368
-
369
- if (sub === 'share') {
370
- // /auth share <label> [--from-agent <name>] — one-shot: account add + enable
371
- // on every agent. Defaults --from-agent to the current agent (same shape as
372
- // /auth account add).
373
- const rest = parts.slice(1);
374
- const { flags, positional } = splitFlags(rest, ['--from-agent']);
375
- const account = positional[0];
376
- if (!account) {
377
- return { kind: 'usage', message: 'Usage: /auth share <label> [--from-agent <name>]' };
378
- }
379
- try { assertSafeAccountLabel(account); }
380
- catch { return { kind: 'error', message: 'Invalid account label. Use [A-Za-z0-9._@+-], 1-64 chars (email shape OK).' }; }
381
- const fromAgentRaw = flags['--from-agent'];
382
- const fromAgent = typeof fromAgentRaw === 'string' ? fromAgentRaw : currentAgent;
383
- try { assertSafeAgentNameForParser(fromAgent); }
384
- catch { return { kind: 'error', message: 'Invalid --from-agent value.' }; }
385
- return {
386
- kind: 'share',
387
- account,
388
- fromAgent,
389
- label: `auth share ${account}`,
390
- cliArgs: ['auth', 'share', account, '--from-agent', fromAgent],
391
- restartAgentsAfter: true,
392
- };
393
- }
394
-
395
- return { kind: 'usage', message: usageText() };
396
- }
397
-
398
- /** Helper to split --flag [value]? from positional args.
399
- * Value-taking flags are passed in `valueFlags`; bare flags (like
400
- * --force) show up in `flags` as boolean true.*/
401
- export function splitFlags(
402
- parts: string[],
403
- valueFlags: string[],
404
- ): { flags: Record<string, string | true>; positional: string[] } {
405
- const flags: Record<string, string | true> = {};
406
- const positional: string[] = [];
407
- const valueSet = new Set(valueFlags);
408
- for (let i = 0; i < parts.length; i++) {
409
- const p = parts[i];
410
- if (p.startsWith('--')) {
411
- if (valueSet.has(p)) {
412
- const next = parts[i + 1];
413
- if (next !== undefined && !next.startsWith('--')) { flags[p] = next; i++; }
414
- else flags[p] = true;
415
- } else {
416
- flags[p] = true;
417
- }
418
- } else {
419
- positional.push(p);
420
- }
421
- }
422
- return { flags, positional };
423
- }
424
-
425
- /** Active + total slot accounting for the rm safety check.
426
- * Returned from the CLI's --json shape (see src/cli/auth.ts `list`). */
427
- export type SlotListingFromCli = {
428
- agent: string;
429
- slots: Array<{
430
- slot: string;
431
- active: boolean;
432
- health: string;
433
- expires_at: number | null;
434
- quota_exhausted_until: number | null;
435
- }>;
436
- };
437
-
438
- /** Check whether a /auth rm is safe. Returns `null` if safe, or an error
439
- * message if the slot is the only/active slot without --force. */
440
- export function checkRemoveSafety(
441
- listing: SlotListingFromCli,
442
- targetSlot: string,
443
- force: boolean,
444
- ): string | null {
445
- if (force) return null;
446
- if (listing.slots.length <= 1) {
447
- return `Refusing to remove the only account slot. Add another with /auth add ${listing.agent}, or pass --force to proceed.`;
448
- }
449
- const target = listing.slots.find(s => s.slot === targetSlot);
450
- if (!target) return null; // CLI will error with its own message
451
- if (target.active) {
452
- return `Refusing to remove the active slot "${targetSlot}". Switch first with /auth use ${listing.agent} <other-slot>, or pass --force.`;
453
- }
454
- return null;
455
- }
456
-
457
- /** Format the /auth list CLI --json output as a Telegram HTML block. */
458
- export function formatSlotList(listing: SlotListingFromCli): string {
459
- if (!listing.slots || listing.slots.length === 0) {
460
- return `<i>No slots for <b>${escapeMini(listing.agent)}</b>. Add one with /auth add ${escapeMini(listing.agent)}.</i>`;
461
- }
462
- const lines = [`<b>Slots for ${escapeMini(listing.agent)}</b>`];
463
- for (const s of listing.slots) {
464
- const active = s.active ? '● ' : ' ';
465
- const name = `<code>${escapeMini(s.slot)}</code>`;
466
- const health = healthIcon(s.health) + ' ' + s.health;
467
- let tail = '';
468
- if (s.health === 'quota-exhausted' && s.quota_exhausted_until) {
469
- const mins = Math.max(0, Math.round((s.quota_exhausted_until - Date.now()) / 60_000));
470
- tail = ` · resets in ~${mins}m`;
471
- } else if (s.health === 'expired') {
472
- tail = ' · run /auth reauth';
473
- }
474
- lines.push(`${active}${name} ${health}${tail}`);
475
- }
476
- return lines.join('\n');
477
- }
478
-
479
- function healthIcon(health: string): string {
480
- switch (health) {
481
- case 'healthy': return '✓';
482
- case 'quota-exhausted': return '⚠️';
483
- case 'expired': return '⌛';
484
- case 'missing': return '✗';
485
- default: return '·';
486
- }
487
- }
488
-
489
- /** Tiny HTML escaper — mirrored from welcome-text.ts so this module
490
- * stays dependency-free and testable in isolation. */
491
- function escapeMini(text: string): string {
492
- return text
493
- .replace(/&/g, '&amp;')
494
- .replace(/</g, '&lt;')
495
- .replace(/>/g, '&gt;')
496
- .replace(/"/g, '&quot;');
497
- }