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.
- package/README.md +49 -57
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +285 -45
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +15931 -12778
- package/dist/host-control/main.js +582 -43
- package/dist/vault/approvals/kernel-server.js +276 -47
- package/dist/vault/broker/server.js +333 -69
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +6 -4
- package/profiles/_base/start.sh.hbs +3 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/default/CLAUDE.md +10 -0
- package/profiles/default/CLAUDE.md.hbs +16 -0
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +67 -15
- package/skills/switchroom-status/SKILL.md +26 -1
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/dist/gateway/gateway.js +13042 -12844
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +22 -36
- package/telegram-plugin/gateway/boot-probes.ts +3 -3
- package/telegram-plugin/gateway/gateway.ts +313 -798
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-probes.test.ts +11 -4
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/uat/SETUP.md +31 -1
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +7 -1
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +7 -1
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/dist/foreman/foreman.js +0 -31358
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- 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, '&')
|
|
494
|
-
.replace(/</g, '<')
|
|
495
|
-
.replace(/>/g, '>')
|
|
496
|
-
.replace(/"/g, '"');
|
|
497
|
-
}
|