getbotons 0.0.0-stage → 0.1.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 (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +505 -2
  3. package/installer/admin.mjs +6 -0
  4. package/installer/botons.mjs +430 -0
  5. package/installer/brain-clis.mjs +28 -0
  6. package/installer/linger.mjs +39 -0
  7. package/installer/output.mjs +27 -0
  8. package/installer/revision.mjs +30 -0
  9. package/installer/shell-path.mjs +66 -0
  10. package/installer/source-lock.json +2891 -0
  11. package/package.json +26 -4
  12. package/payload/LICENSE +21 -0
  13. package/payload/SECURITY.md +33 -0
  14. package/payload/installer/admin.mjs +6 -0
  15. package/payload/installer/botons.mjs +430 -0
  16. package/payload/installer/brain-clis.mjs +28 -0
  17. package/payload/installer/linger.mjs +39 -0
  18. package/payload/installer/output.mjs +27 -0
  19. package/payload/installer/revision.mjs +30 -0
  20. package/payload/installer/shell-path.mjs +66 -0
  21. package/payload/installer/source-lock.json +2891 -0
  22. package/payload/npm-shrinkwrap.json +51 -0
  23. package/payload/package.json +13 -0
  24. package/payload/scripts/backup.ts +47 -0
  25. package/payload/scripts/botons-backup.service +15 -0
  26. package/payload/scripts/botons-backup.timer +11 -0
  27. package/payload/scripts/botons.service +36 -0
  28. package/payload/scripts/check-browser.ts +90 -0
  29. package/payload/scripts/doctor.ts +790 -0
  30. package/payload/scripts/install-backup-timer.ts +52 -0
  31. package/payload/scripts/install-service.ts +206 -0
  32. package/payload/scripts/login.ts +17 -0
  33. package/payload/scripts/migrate-names.ts +47 -0
  34. package/payload/scripts/pair.ts +163 -0
  35. package/payload/scripts/restore.ts +108 -0
  36. package/payload/server/bin/botons +4 -0
  37. package/payload/server/bin/legacy/minion +4 -0
  38. package/payload/server/bin/legacy/tots +4 -0
  39. package/payload/server/src/acting.ts +16 -0
  40. package/payload/server/src/api-keys.ts +544 -0
  41. package/payload/server/src/api.ts +1484 -0
  42. package/payload/server/src/app.ts +546 -0
  43. package/payload/server/src/approvals.ts +873 -0
  44. package/payload/server/src/ask-rules.ts +181 -0
  45. package/payload/server/src/audit.ts +158 -0
  46. package/payload/server/src/auth.ts +497 -0
  47. package/payload/server/src/backup.ts +382 -0
  48. package/payload/server/src/brain-login.ts +75 -0
  49. package/payload/server/src/browser-cert.ts +67 -0
  50. package/payload/server/src/browser-handoff.ts +119 -0
  51. package/payload/server/src/browser-handover.ts +175 -0
  52. package/payload/server/src/browser-policy.ts +210 -0
  53. package/payload/server/src/browser-proxy.ts +580 -0
  54. package/payload/server/src/browser-request.ts +335 -0
  55. package/payload/server/src/browser-screenshot.ts +56 -0
  56. package/payload/server/src/browser-send.ts +155 -0
  57. package/payload/server/src/browser.ts +2164 -0
  58. package/payload/server/src/callback.ts +218 -0
  59. package/payload/server/src/capabilities.ts +90 -0
  60. package/payload/server/src/cards.ts +243 -0
  61. package/payload/server/src/cli/approve-mcp.ts +119 -0
  62. package/payload/server/src/cli/botons.ts +629 -0
  63. package/payload/server/src/cli/browser-mcp.ts +168 -0
  64. package/payload/server/src/cli/workspace-file.ts +78 -0
  65. package/payload/server/src/cli-homes.ts +38 -0
  66. package/payload/server/src/cli-install.ts +104 -0
  67. package/payload/server/src/cli-updates.ts +112 -0
  68. package/payload/server/src/codex-config-repair.ts +80 -0
  69. package/payload/server/src/config.ts +473 -0
  70. package/payload/server/src/connector-access.ts +158 -0
  71. package/payload/server/src/connector-launch.ts +203 -0
  72. package/payload/server/src/connectors.ts +947 -0
  73. package/payload/server/src/disk-usage.ts +62 -0
  74. package/payload/server/src/engine.ts +2328 -0
  75. package/payload/server/src/env-init.ts +5 -0
  76. package/payload/server/src/events.ts +46 -0
  77. package/payload/server/src/files.ts +528 -0
  78. package/payload/server/src/github-access.ts +52 -0
  79. package/payload/server/src/health.ts +117 -0
  80. package/payload/server/src/held-dir.ts +96 -0
  81. package/payload/server/src/hooks.ts +141 -0
  82. package/payload/server/src/http.ts +207 -0
  83. package/payload/server/src/index.ts +139 -0
  84. package/payload/server/src/install-type.ts +11 -0
  85. package/payload/server/src/instructions.ts +204 -0
  86. package/payload/server/src/isolation.ts +234 -0
  87. package/payload/server/src/legacy-upgrade.ts +668 -0
  88. package/payload/server/src/limits.ts +44 -0
  89. package/payload/server/src/lock.ts +75 -0
  90. package/payload/server/src/log.ts +137 -0
  91. package/payload/server/src/managed-clis.ts +65 -0
  92. package/payload/server/src/mcp-client.ts +229 -0
  93. package/payload/server/src/notify.ts +193 -0
  94. package/payload/server/src/oauth-connections.ts +650 -0
  95. package/payload/server/src/oauth.ts +311 -0
  96. package/payload/server/src/outbound.ts +142 -0
  97. package/payload/server/src/portable.ts +1669 -0
  98. package/payload/server/src/prompt.ts +676 -0
  99. package/payload/server/src/providers/api-brain.ts +612 -0
  100. package/payload/server/src/providers/api-history.ts +131 -0
  101. package/payload/server/src/providers/api-tools.ts +481 -0
  102. package/payload/server/src/providers/api-wire.ts +690 -0
  103. package/payload/server/src/providers/claude-cli.ts +620 -0
  104. package/payload/server/src/providers/cli-check.ts +265 -0
  105. package/payload/server/src/providers/cli-turn.ts +20 -0
  106. package/payload/server/src/providers/codex-cli.ts +552 -0
  107. package/payload/server/src/providers/codex-workspace.ts +61 -0
  108. package/payload/server/src/providers/failures.ts +143 -0
  109. package/payload/server/src/providers/index.ts +131 -0
  110. package/payload/server/src/providers/mcp-stdio.ts +147 -0
  111. package/payload/server/src/providers/mock.ts +94 -0
  112. package/payload/server/src/providers/process.ts +268 -0
  113. package/payload/server/src/providers/readiness.ts +43 -0
  114. package/payload/server/src/providers/sse.ts +73 -0
  115. package/payload/server/src/providers/types.ts +173 -0
  116. package/payload/server/src/rates.ts +128 -0
  117. package/payload/server/src/reactions.ts +266 -0
  118. package/payload/server/src/recent-logs.ts +489 -0
  119. package/payload/server/src/rehearsal.ts +593 -0
  120. package/payload/server/src/rehearsals.ts +303 -0
  121. package/payload/server/src/replay.ts +165 -0
  122. package/payload/server/src/sandbox.ts +767 -0
  123. package/payload/server/src/schedule.ts +214 -0
  124. package/payload/server/src/scheduler.ts +24 -0
  125. package/payload/server/src/schema.ts +882 -0
  126. package/payload/server/src/secrets.ts +46 -0
  127. package/payload/server/src/setup.ts +356 -0
  128. package/payload/server/src/skill-packages.ts +363 -0
  129. package/payload/server/src/skills.ts +527 -0
  130. package/payload/server/src/spend.ts +255 -0
  131. package/payload/server/src/ssrf.ts +126 -0
  132. package/payload/server/src/starter-gate.ts +168 -0
  133. package/payload/server/src/starters.ts +610 -0
  134. package/payload/server/src/storage.ts +32 -0
  135. package/payload/server/src/store.ts +3087 -0
  136. package/payload/server/src/tailnet.ts +132 -0
  137. package/payload/server/src/templates.ts +454 -0
  138. package/payload/server/src/trash.ts +772 -0
  139. package/payload/server/src/util.ts +97 -0
  140. package/payload/server/src/webhooks.ts +216 -0
  141. package/payload/server/src/work-feed.ts +473 -0
  142. package/payload/shared/app-version.d.mts +1 -0
  143. package/payload/shared/app-version.mjs +3 -0
  144. package/payload/shared/boton.ts +323 -0
  145. package/payload/shared/brand.ts +107 -0
  146. package/payload/shared/cards.ts +427 -0
  147. package/payload/shared/effort.ts +91 -0
  148. package/payload/shared/files.ts +104 -0
  149. package/payload/shared/handoff.ts +97 -0
  150. package/payload/shared/install-commands.ts +7 -0
  151. package/payload/shared/legacy-names.d.mts +35 -0
  152. package/payload/shared/legacy-names.mjs +244 -0
  153. package/payload/shared/live-view.ts +94 -0
  154. package/payload/shared/look-critter.ts +189 -0
  155. package/payload/shared/look-tater.ts +127 -0
  156. package/payload/shared/look.ts +200 -0
  157. package/payload/shared/reactions.ts +43 -0
  158. package/payload/shared/seeded.ts +34 -0
  159. package/payload/shared/services.ts +90 -0
  160. package/payload/shared/types.ts +3016 -0
  161. package/payload/shared/util.ts +68 -0
  162. package/payload/shared/versions.ts +44 -0
  163. package/payload/shared/wake.ts +164 -0
  164. package/payload/web/dist/apple-touch-icon.png +0 -0
  165. package/payload/web/dist/assets/Icons-nytVAa9g.js +1 -0
  166. package/payload/web/dist/assets/NewBotDialog-DXr2XhDP.js +1 -0
  167. package/payload/web/dist/assets/PairScreen-CMcJO7cQ.js +1 -0
  168. package/payload/web/dist/assets/Panel-Bvqz3I3m.js +4 -0
  169. package/payload/web/dist/assets/ReactionPicker-B0SKsjiy.js +1 -0
  170. package/payload/web/dist/assets/SettingRows-CvLomtuo.js +4 -0
  171. package/payload/web/dist/assets/SettingsDialog-CQ41LV-P.js +8 -0
  172. package/payload/web/dist/assets/Setup-C-3P7Mtn.js +1 -0
  173. package/payload/web/dist/assets/Skills-DXMH69kH.js +1 -0
  174. package/payload/web/dist/assets/StarterSetup-Ce2V1_Cs.js +5 -0
  175. package/payload/web/dist/assets/Steps-CKtB3GAM.js +1 -0
  176. package/payload/web/dist/assets/emoji-catalog-DfpPz8qw.js +540 -0
  177. package/payload/web/dist/assets/index-BEU7FDxV.css +1 -0
  178. package/payload/web/dist/assets/index-Dhwn-rf0.js +3 -0
  179. package/payload/web/dist/assets/react-C1iE8mnY.js +9 -0
  180. package/payload/web/dist/assets/reactions-rw0YeCmA.js +1 -0
  181. package/payload/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  182. package/payload/web/dist/assets/util-Cx5feSJu.js +1 -0
  183. package/payload/web/dist/assets/vendor-B3qdcjGo.js +14 -0
  184. package/payload/web/dist/favicon.svg +1 -0
  185. package/payload/web/dist/icon-192.png +0 -0
  186. package/payload/web/dist/icon-512.png +0 -0
  187. package/payload/web/dist/icon.svg +1 -0
  188. package/payload/web/dist/index.html +67 -0
  189. package/payload/web/dist/manifest.webmanifest +12 -0
  190. package/shared/legacy-names.d.mts +35 -0
  191. package/shared/legacy-names.mjs +244 -0
@@ -0,0 +1,629 @@
1
+ import { VERSION } from '../../../shared/app-version.mjs';
2
+ // `botons`: what a Boton runs from its shell. $BOTON_TOKEN is issued for one turn and reaches only this Boton's own things.
3
+ // Its deprecated names run it too, from server/bin/legacy (see shared/legacy-names.mjs).
4
+ import '../env-init.ts';
5
+ import { resolve } from 'node:path';
6
+ import { parseArgs } from 'node:util';
7
+ import type { Approval, ApprovalRequestResult, AskResult, ConnectorRequestResult, Memory, Message, BotonConnector, NotifyResult, Routine, RoutineRun, ShareResult, Skill, TeamMember, WebhookFilter } from '../../../shared/types.ts';
8
+ import { durationText, isPastOneOff } from '../../../shared/util.ts';
9
+ import { fmtTime } from '../util.ts';
10
+ import { serveApprovals } from './approve-mcp.ts';
11
+ import { serveBrowser } from './browser-mcp.ts';
12
+ import { readWorkspaceFile, WorkspaceFileError } from './workspace-file.ts';
13
+
14
+ const API = (process.env.BOTONS_API ?? 'http://127.0.0.1:7777').replace(/\/$/, '');
15
+ const BOT = process.env.BOTON_BOT_ID ?? '';
16
+ const TOKEN = process.env.BOTON_TOKEN ?? '';
17
+ const WORKSPACE = process.env.BOTON_WORKSPACE ?? '';
18
+
19
+ const HELP = `botons — manage your routines and memory, use skills, notify your owner, share files, react to your owner's messages, ask your owner for approval, and ask the other Botons
20
+
21
+ botons routine list
22
+ botons routine show <id>
23
+ botons routine create --name <name> --instruction <text> [--schedule <when>] [--webhook] [--paused] [--timezone <tz>]
24
+ [--notify auto|always|failures|never] [--source <sender>] [--filter <filter>] [--skill <name>]
25
+ [--model <id>] [--effort <level>] [--session chat|own] [--time-limit <duration>]
26
+ botons routine update <id> [--name ..] [--instruction ..] [--schedule <when> | --no-schedule]
27
+ [--pause | --resume] [--webhook | --no-webhook] [--rotate-key] [--timezone <tz>]
28
+ [--notify auto|always|failures|never] [--source <sender>] [--filter <filter> | --no-filter]
29
+ [--skill <name> | --no-skill] [--model <id> | --no-model]
30
+ [--effort <level> | --no-effort] [--session chat|own]
31
+ [--time-limit <duration> | --no-time-limit]
32
+ botons routine delete <id>
33
+ botons routine run <id>
34
+ botons routine test <id> [--payload <json> | --payload @<file>]
35
+ botons memory list
36
+ botons memory add <text>
37
+ botons memory forget <id>
38
+ botons skill list
39
+ botons skill show <name>
40
+ botons skill create --name <name> --description <text> (--body <text> | --file <SKILL.md>)
41
+ botons skill update <name> [--description <text>] [--body <text> | --file <SKILL.md>]
42
+ botons notify <text> [--priority high]
43
+ botons share <path> [--title <text>]
44
+ botons react <message-id> <emoji>
45
+ botons unreact <message-id>
46
+ botons approval request --title <text> [--details <text>] [--then <text>] [--action <kind>] [--expires <duration>]
47
+ botons approval list [--pending]
48
+ botons approval show <id>
49
+ botons approval cancel <id>
50
+ botons connector list
51
+ botons connector request <id>
52
+ botons card post (--card <json> | --file <card.json>) [--expires <duration>]
53
+ botons card show <id>
54
+ botons team
55
+ botons ask <name or id> <text>
56
+
57
+ Schedules: cron ("0 8 * * 1-5"), "every 30m", "every 2h", "daily at 8:00", "weekdays at 7:30am",
58
+ "in 20m", or "at 2026-11-05 09:00".
59
+ --notify: which runs push to your owner's phone. auto (default): failures and \`botons notify\`;
60
+ always: also every result that isn't [quiet]; failures: only failures; never: nothing.
61
+ --source: who calls the webhook: generic (callers send the key; the default), github, linear, slack
62
+ or sentry (each delivery must carry the sender's signature; your owner saves Linear's, Slack's and
63
+ Sentry's signing secrets in the app, and GitHub uses the key as its secret).
64
+ \`routine test\` runs it as its webhook would, with a sample payload (JSON or text, or @file to read
65
+ one from your workspace), as a dry run: it doesn't change the routine's last status and pushes nothing.
66
+ --filter: run only for some deliveries, e.g. "header:X-GitHub-Event=push,pull_request" or
67
+ "field:action=opened" (a JSON body field, dot path); others are skipped.
68
+ --model: the model the routine's runs use (e.g. haiku for a frequent, simple check); --no-model (or
69
+ --model "") goes back to your own model.
70
+ --effort: how hard the routine's runs think (e.g. low for a frequent, simple check). The levels are your
71
+ provider's (Claude Code: low, medium, high, xhigh, max; Codex: the ones its model lists, such as low,
72
+ medium, high, xhigh); another one is refused with the list. --no-effort (or --effort "") goes back to
73
+ your own effort. Your context block says what this turn runs on.
74
+ --session: chat (default) runs it in your conversation with your owner; own runs it in a session of
75
+ its own, so frequent runs and their payloads stay out of the chat's context (your chat turns still see
76
+ its latest results). Its results are posted in the chat either way.
77
+ --time-limit: how long one run may take once it starts (e.g. 10m, 1h, 90s) before it is stopped and
78
+ fails as timed out (not tried again by itself); at most the server's limit, which is also the default.
79
+ --no-time-limit goes back to the server's limit.
80
+ \`botons notify\` posts a chip in your chat and pushes the text (keep it to one short line) to your
81
+ owner's phone. \`botons share\` posts a chip in your chat that opens a file from your workspace
82
+ (a report, a chart, an export); the path is relative to the current folder, or absolute.
83
+ \`botons react\` puts one emoji on one of your owner's own messages in your chat (your context block lists
84
+ the ids of the messages your turn answers); \`unreact\` takes yours off. One reaction per message at most,
85
+ never on your own messages, and never in place of an answer a question or request needs. A reaction pushes
86
+ nothing and is never an approval.
87
+ \`botons ask\` posts your message in another Boton's chat, signed as you, and returns at once;
88
+ its reply comes back to you later as a new message.
89
+ Skills are procedures in the library your owner shares with every Boton. Save one when you have
90
+ worked out how to do something that will come up again: a name like weekly-report (lowercase words
91
+ and hyphens), a description that says what it does and when to use it, and the steps as Markdown.
92
+ --file reads a SKILL.md in your workspace (name and description can come from its frontmatter).
93
+ A skill you save is yours: only you can use it until your owner approves it for the other Botons,
94
+ and you can change it until then. --skill on a routine makes every run follow that skill.
95
+ \`botons approval request\` asks your owner before you do something risky: they get a card in your
96
+ chat (and a push) to allow or deny it. Don't wait for the answer and don't do the thing: end your
97
+ turn. You get a new turn with the decision and your --then text (what to do once allowed; write it
98
+ so it stands on its own). --action names the kind of action ("git push"), so your owner can always
99
+ allow it; then such a request is allowed at once and you go ahead in the same turn. --expires: how
100
+ long it waits (default 24h, at most 7d).
101
+ \`botons connector list\` shows your connectors and the ones your owner lets you ask for; \`request\`
102
+ asks for one by id: it is yours from your next turn, at once or once your owner allows it.
103
+ \`botons card post\` shows your owner a card in the chat: data only, drawn by the app as plain text.
104
+ The card is JSON: {"v":1,"title":"…","blocks":[…]} with up to 20 blocks, each one of
105
+ {"type":"text","text":"…"} {"type":"status","tone":"info|success|warning|error|working","text":"…"}
106
+ {"type":"progress","label":"…","value":3,"max":10} {"type":"table","columns":["…"],"rows":[["…",1]]}
107
+ {"type":"file","path":"<a file in your workspace>"} {"type":"link","url":"https://…","label":"…"}
108
+ and at most one question: {"type":"choice","id":"pick","prompt":"…","options":[{"id":"a","label":"…"},…]},
109
+ {"type":"form","id":"f","fields":[{"id":"name","label":"…","kind":"text|textarea|number|select|checkbox"}]}
110
+ or {"type":"connector","connectorId":"<one from \`botons connector list\` you may ask for>"}. No HTML,
111
+ Markdown or scripts; unknown fields are refused. Never ask for a password, key, token or code on a card:
112
+ its answer goes into the chat. Don't wait for the answer: end your turn. You get a new turn with it
113
+ (a connector card: with the connector, if your owner gives it). --expires: default 24h, at most 7d.
114
+ Add --json to any command for JSON output.`;
115
+
116
+ class CliError extends Error {}
117
+
118
+ async function call<T>(method: string, path: string, body?: unknown): Promise<T> {
119
+ let res: Response;
120
+ try {
121
+ res = await fetch(`${API}${path}`, {
122
+ method,
123
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${TOKEN}` },
124
+ body: body === undefined ? undefined : JSON.stringify(body),
125
+ });
126
+ } catch (e) {
127
+ throw new CliError(`Can't reach the Botons server at ${API}: ${(e as Error).message}`);
128
+ }
129
+ const text = await res.text();
130
+ let data: any = null;
131
+ try {
132
+ data = text ? JSON.parse(text) : null;
133
+ } catch {}
134
+ if (res.status === 401) {
135
+ throw new CliError('The Botons server did not accept this turn\'s token. It only works during the turn it was issued for; run `botons` from within a turn.');
136
+ }
137
+ if (!res.ok) throw new CliError(data?.error ?? `HTTP ${res.status}`);
138
+ return data as T;
139
+ }
140
+
141
+ const when = (iso: string | null, tz: string) => (iso ? fmtTime(iso, tz) : '—');
142
+
143
+ function routineLine(r: Routine): string {
144
+ const done = !r.enabled && isPastOneOff(r.schedule);
145
+ const bits = [r.scheduleText ?? 'no schedule', done ? 'done' : r.enabled ? 'active' : 'paused'];
146
+ if (r.enabled && r.nextRunAt) bits.push(`next ${when(r.nextRunAt, r.timezone)}`);
147
+ if (r.webhook) bits.push(r.webhook.source === 'generic' ? 'webhook' : `webhook from ${r.webhook.source}`);
148
+ if (r.notify !== 'auto') bits.push(`notify ${r.notify}`);
149
+ if (r.skill) bits.push(`skill ${r.skill}`);
150
+ if (r.model) bits.push(`model ${r.model}`);
151
+ if (r.effort) bits.push(`effort ${r.effort}`);
152
+ if (r.session === 'own') bits.push('own session');
153
+ if (r.timeLimitMs) bits.push(`time limit ${durationText(r.timeLimitMs)}`);
154
+ if (r.lastStatus) bits.push(`last ${r.lastStatus}`);
155
+ return `${r.id} ${r.name} (${bits.join(', ')})`;
156
+ }
157
+
158
+ /** "90s", "10m", "1.5h" (a bare number is minutes), in ms. */
159
+ function parseTimeLimit(raw: string): number {
160
+ const m = /^\s*(\d+(?:\.\d+)?)\s*(s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?)?\s*$/i.exec(raw);
161
+ if (!m) throw new CliError('--time-limit must be a duration such as 10m, 1h or 90s.');
162
+ const unit = (m[2] ?? 'm')[0].toLowerCase();
163
+ return Math.round(Number(m[1]) * (unit === 's' ? 1000 : unit === 'h' ? 3_600_000 : 60_000));
164
+ }
165
+
166
+ function parseFilterFlag(raw: string): WebhookFilter {
167
+ const m = /^\s*(header|field)\s*:\s*([^=]+?)\s*=\s*(.+)$/.exec(raw);
168
+ if (!m) throw new CliError('--filter must look like "header:X-GitHub-Event=push,pull_request" or "field:action=opened".');
169
+ return { kind: m[1] as WebhookFilter['kind'], name: m[2], values: m[3].split(',').map((v) => v.trim()).filter(Boolean) };
170
+ }
171
+
172
+ function filterLine(f: WebhookFilter): string {
173
+ return ` Runs only when ${f.kind === 'header' ? `header ${f.name}` : `body field ${f.name}`} is ${f.values.join(' or ')}; other deliveries are skipped.`;
174
+ }
175
+
176
+ async function ownRoutine(id: string): Promise<Routine> {
177
+ const r = await call<Routine>('GET', `/api/routines/${encodeURIComponent(id)}`);
178
+ if (r.botId !== BOT) throw new CliError(`Routine ${id} belongs to another boton`);
179
+ return r;
180
+ }
181
+
182
+ async function main(argv: string[]): Promise<string | object> {
183
+ if (argv.length === 1 && argv[0] === '--version') return VERSION;
184
+ const { values: o, positionals: pos } = parseArgs({
185
+ args: argv,
186
+ allowPositionals: true,
187
+ options: {
188
+ name: { type: 'string' },
189
+ instruction: { type: 'string' },
190
+ prompt: { type: 'string' },
191
+ schedule: { type: 'string' },
192
+ 'no-schedule': { type: 'boolean' },
193
+ timezone: { type: 'string' },
194
+ webhook: { type: 'boolean' },
195
+ 'no-webhook': { type: 'boolean' },
196
+ 'rotate-key': { type: 'boolean' },
197
+ pause: { type: 'boolean' },
198
+ paused: { type: 'boolean' },
199
+ resume: { type: 'boolean' },
200
+ notify: { type: 'string' },
201
+ source: { type: 'string' },
202
+ filter: { type: 'string' },
203
+ 'no-filter': { type: 'boolean' },
204
+ payload: { type: 'string' },
205
+ model: { type: 'string' },
206
+ 'no-model': { type: 'boolean' },
207
+ effort: { type: 'string' },
208
+ 'no-effort': { type: 'boolean' },
209
+ session: { type: 'string' },
210
+ 'time-limit': { type: 'string' },
211
+ 'no-time-limit': { type: 'boolean' },
212
+ priority: { type: 'string' },
213
+ title: { type: 'string' },
214
+ skill: { type: 'string' },
215
+ 'no-skill': { type: 'boolean' },
216
+ description: { type: 'string' },
217
+ body: { type: 'string' },
218
+ file: { type: 'string' },
219
+ details: { type: 'string' },
220
+ then: { type: 'string' },
221
+ action: { type: 'string' },
222
+ expires: { type: 'string' },
223
+ card: { type: 'string' },
224
+ pending: { type: 'boolean' },
225
+ json: { type: 'boolean' },
226
+ help: { type: 'boolean', short: 'h' },
227
+ },
228
+ });
229
+ const [area, action, ...rest] = pos;
230
+ if (o.help || !area || area === 'help') return HELP;
231
+ if (!BOT || !TOKEN) throw new CliError('BOTON_BOT_ID or BOTON_TOKEN is not set; this tool is meant to be run by a Boton, during a turn.');
232
+ const instruction = o.instruction ?? o.prompt;
233
+ const skillFile = () => {
234
+ if (o.file === undefined) return null;
235
+ const file = parseSkillFile(readWorkspaceFile(o.file, WORKSPACE));
236
+ if (o.body !== undefined) throw new CliError('Give --body or --file, not both.');
237
+ return file;
238
+ };
239
+
240
+ if (area === 'routine' || area === 'routines') {
241
+ switch (action ?? 'list') {
242
+ case 'list': {
243
+ const list = await call<Routine[]>('GET', `/api/bots/${BOT}/routines`);
244
+ if (o.json) return list;
245
+ return list.length ? list.map(routineLine).join('\n') : 'No routines yet.';
246
+ }
247
+ case 'show': {
248
+ const r = await ownRoutine(need(rest[0], 'routine id'));
249
+ const runs = await call<RoutineRun[]>('GET', `/api/routines/${r.id}/runs`);
250
+ if (o.json) return { ...r, runs };
251
+ const lines = [routineLine(r), '', 'Instruction:', r.instruction];
252
+ if (r.webhook) {
253
+ // The server leaves the key out during a sandboxed webhook run.
254
+ const { source } = r.webhook;
255
+ const key = r.webhook.key || '(key hidden during a webhook run)';
256
+ if (source === 'generic') lines.push('', 'Webhook:', ` POST ${r.webhook.url}`, ` Authorization: Bearer ${key}`);
257
+ else {
258
+ lines.push('', `Webhook (sent by ${source}; only deliveries with a valid ${source} signature run):`, ` POST ${r.webhook.url}`, ` Key: ${key}`);
259
+ if (source === 'github') lines.push(r.webhook.hasSecret ? ' GitHub signs with the signing secret your owner saved.' : ' GitHub signs with the key: paste it into the webhook\'s Secret field.');
260
+ else if (!r.webhook.hasSecret) lines.push(` No ${source} signing secret is saved yet, so deliveries are refused: your owner adds it in the app (routine editor).`);
261
+ }
262
+ if (r.webhook.filter) lines.push(filterLine(r.webhook.filter));
263
+ if (r.webhook.callbackUrl) lines.push(` Each finished run's result is POSTed to ${r.webhook.callbackUrl}, signed with the key (X-Boton-Signature).`);
264
+ lines.push(r.webhookWorkspace ? ' Its runs get Workspace access (sandboxed), whatever your own access is.' : ' Its runs get your own access.');
265
+ if (r.webhook.key && source === 'generic') lines.push(` curl -X POST '${r.webhook.url}' -H 'Authorization: Bearer ${r.webhook.key}' -H 'Content-Type: application/json' -d '{"hello":"world"}'`);
266
+ }
267
+ lines.push('', 'Recent runs:');
268
+ if (!runs.length) lines.push(' none');
269
+ for (const run of runs.slice(0, 5)) {
270
+ const status = run.quiet ? 'quiet' : run.status;
271
+ const said = run.error ?? (run.quiet ? run.reply : null);
272
+ lines.push(` ${run.id} ${run.trigger} ${status} ${when(run.startedAt, r.timezone)}${said ? ` — ${said}` : ''}`);
273
+ }
274
+ return lines.join('\n');
275
+ }
276
+ case 'create':
277
+ case 'add': {
278
+ const r = await call<Routine>('POST', `/api/bots/${BOT}/routines`, {
279
+ name: need(o.name, '--name'),
280
+ instruction: need(instruction, '--instruction'),
281
+ schedule: o.schedule ?? null,
282
+ timezone: o.timezone,
283
+ webhook: !!o.webhook,
284
+ enabled: !(o.paused || o.pause),
285
+ notify: o.notify,
286
+ webhookSource: o.source,
287
+ webhookFilter: o.filter === undefined ? undefined : parseFilterFlag(o.filter),
288
+ skill: o.skill,
289
+ model: o['no-model'] ? null : o.model,
290
+ effort: o['no-effort'] ? null : o.effort,
291
+ session: o.session,
292
+ timeLimitMs: o['time-limit'] === undefined ? undefined : parseTimeLimit(o['time-limit']),
293
+ });
294
+ if (o.json) return r;
295
+ let out = `Created ${routineLine(r)}`;
296
+ if (r.webhook) out += `\nWebhook: POST ${r.webhook.url} (Authorization: Bearer ${r.webhook.key})`;
297
+ if (!r.schedule && !r.webhook) out += '\nNote: no schedule or webhook, so it only runs when triggered with `botons routine run`.';
298
+ return out;
299
+ }
300
+ case 'update':
301
+ case 'edit': {
302
+ const r0 = await ownRoutine(need(rest[0], 'routine id'));
303
+ const patch: Record<string, unknown> = {};
304
+ if (o.name !== undefined) patch.name = o.name;
305
+ if (instruction !== undefined) patch.instruction = instruction;
306
+ if (o.schedule !== undefined) patch.schedule = o.schedule;
307
+ if (o['no-schedule']) patch.schedule = null;
308
+ if (o.timezone !== undefined) patch.timezone = o.timezone;
309
+ if (o.pause || o.paused) patch.enabled = false;
310
+ if (o.resume) patch.enabled = true;
311
+ if (o.webhook) patch.webhook = true;
312
+ if (o['no-webhook']) patch.webhook = false;
313
+ if (o['rotate-key']) patch.rotateKey = true;
314
+ if (o.notify !== undefined) patch.notify = o.notify;
315
+ if (o.source !== undefined) patch.webhookSource = o.source;
316
+ if (o.filter !== undefined) patch.webhookFilter = parseFilterFlag(o.filter);
317
+ if (o['no-filter']) patch.webhookFilter = null;
318
+ if (o.skill !== undefined) patch.skill = o.skill;
319
+ if (o['no-skill']) patch.skill = null;
320
+ if (o.model !== undefined) patch.model = o.model.trim() || null;
321
+ if (o['no-model']) patch.model = null;
322
+ if (o.effort !== undefined) patch.effort = o.effort.trim() || null;
323
+ if (o['no-effort']) patch.effort = null;
324
+ if (o.session !== undefined) patch.session = o.session;
325
+ if (o['time-limit'] !== undefined) patch.timeLimitMs = parseTimeLimit(o['time-limit']);
326
+ if (o['no-time-limit']) patch.timeLimitMs = null;
327
+ if (!Object.keys(patch).length) throw new CliError('Nothing to update. See `botons help`.');
328
+ const r = await call<Routine>('PATCH', `/api/routines/${r0.id}`, patch);
329
+ if (o.json) return r;
330
+ let out = `Updated ${routineLine(r)}`;
331
+ if (r.webhook && (o.webhook || o['rotate-key'])) out += `\nWebhook: POST ${r.webhook.url} (Authorization: Bearer ${r.webhook.key})`;
332
+ return out;
333
+ }
334
+ case 'delete':
335
+ case 'remove':
336
+ case 'rm': {
337
+ const r = await ownRoutine(need(rest[0], 'routine id'));
338
+ await call('DELETE', `/api/routines/${r.id}`);
339
+ return o.json ? { ok: true } : `Deleted ${r.id} (${r.name}).`;
340
+ }
341
+ case 'run': {
342
+ const r = await ownRoutine(need(rest[0], 'routine id'));
343
+ const run = await call<RoutineRun>('POST', `/api/routines/${r.id}/run`);
344
+ return o.json ? run : `Queued run ${run.id} of "${r.name}". It starts after your current reply and its result is posted in the chat.`;
345
+ }
346
+ case 'test': {
347
+ const r = await ownRoutine(need(rest[0], 'routine id'));
348
+ // At most 1 MiB, as a webhook's; read from the workspace only, since a test run shows the Boton its payload.
349
+ const payload = o.payload?.startsWith('@')
350
+ ? readWorkspaceFile(o.payload.slice(1), WORKSPACE, 1024 * 1024, { flag: '--payload @file', instead: 'pass the payload itself with --payload' })
351
+ : o.payload;
352
+ const run = await call<RoutineRun>('POST', `/api/routines/${r.id}/run`, { test: true, ...(payload === undefined ? {} : { payload }) });
353
+ if (o.json) return run;
354
+ return `Queued test run ${run.id} of "${r.name}"${run.payloadBytes ? ` with a ${run.payloadBytes}-byte payload` : ''}. It starts after your current reply; it runs as a dry run, and its result is posted in the chat.`;
355
+ }
356
+ default:
357
+ throw new CliError(`Unknown routine command "${action}". See \`botons help\`.`);
358
+ }
359
+ }
360
+
361
+ if (area === 'memory' || area === 'memories') {
362
+ switch (action ?? 'list') {
363
+ case 'list': {
364
+ const list = await call<Memory[]>('GET', `/api/bots/${BOT}/memories`);
365
+ if (o.json) return list;
366
+ return list.length ? list.map((m) => `${m.id} ${m.text}`).join('\n') : 'No memories yet.';
367
+ }
368
+ case 'add':
369
+ case 'save': {
370
+ const text = need(rest.join(' ').trim() || undefined, 'memory text');
371
+ const m = await call<Memory>('POST', `/api/bots/${BOT}/memories`, { text });
372
+ return o.json ? m : `Saved ${m.id}.`;
373
+ }
374
+ case 'forget':
375
+ case 'delete':
376
+ case 'rm': {
377
+ const id = need(rest[0], 'memory id');
378
+ await call('DELETE', `/api/memories/${encodeURIComponent(id)}`);
379
+ return o.json ? { ok: true } : `Forgot ${id}.`;
380
+ }
381
+ default:
382
+ throw new CliError(`Unknown memory command "${action}". See \`botons help\`.`);
383
+ }
384
+ }
385
+ if (area === 'skill' || area === 'skills') {
386
+ switch (action ?? 'list') {
387
+ case 'list': {
388
+ const list = await call<Skill[]>('GET', `/api/bots/${BOT}/skills`);
389
+ if (o.json) return list;
390
+ const by = (s: Skill) => (s.pending ? ' (yours; only you can use it until your owner approves it)' : s.createdBy.kind === 'boton' ? ` (written by ${s.createdBy.name})` : '');
391
+ return list.length ? list.map((s) => `${s.name} ${s.description}${by(s)}`).join('\n') : 'No skills yet.';
392
+ }
393
+ case 'show':
394
+ case 'read': {
395
+ const s = await call<Skill>('GET', `/api/bots/${BOT}/skills/${encodeURIComponent(need(rest[0], 'skill name'))}`);
396
+ if (o.json) return s;
397
+ return `# ${s.name}\n${s.description}\n\n${s.body}`;
398
+ }
399
+ case 'create':
400
+ case 'add':
401
+ case 'save': {
402
+ const file = skillFile();
403
+ const s = await call<Skill>('POST', `/api/bots/${BOT}/skills`, {
404
+ name: need(o.name ?? file?.name, '--name'),
405
+ description: need(o.description ?? file?.description, '--description'),
406
+ body: need(o.body ?? file?.body, '--body or --file'),
407
+ });
408
+ return o.json ? s : `Saved the skill ${s.name}. Only you can use it until your owner approves it for the other Botons.`;
409
+ }
410
+ case 'update':
411
+ case 'edit': {
412
+ const name = need(rest[0], 'skill name');
413
+ const file = skillFile();
414
+ const patch: Record<string, unknown> = {};
415
+ if (o.description !== undefined) patch.description = o.description;
416
+ else if (file?.description) patch.description = file.description;
417
+ if (o.body !== undefined) patch.body = o.body;
418
+ else if (file) patch.body = file.body;
419
+ if (!Object.keys(patch).length) throw new CliError('Nothing to update. See `botons help`.');
420
+ const s = await call<Skill>('PATCH', `/api/bots/${BOT}/skills/${encodeURIComponent(name)}`, patch);
421
+ return o.json ? s : `Updated the skill ${s.name}.`;
422
+ }
423
+ default:
424
+ throw new CliError(`Unknown skill command "${action}". See \`botons help\`.`);
425
+ }
426
+ }
427
+ if (area === 'notify') {
428
+ const text = need(pos.slice(1).join(' ').trim() || undefined, 'notification text');
429
+ const priority = o.priority ?? 'default';
430
+ if (priority !== 'default' && priority !== 'high') throw new CliError('--priority must be "high" (or "default").');
431
+ const out = await call<NotifyResult>('POST', `/api/bots/${BOT}/notify`, { text, priority });
432
+ if (o.json) return out;
433
+ const failed = out.deliveries.filter((d) => !d.ok);
434
+ if (out.skipped) return `Posted in your chat; no push sent: ${out.skipped}.`;
435
+ if (!failed.length) return 'Sent. Your owner got a push, and it is posted in your chat.';
436
+ return `Posted in your chat, but the push failed (${failed.map((d) => d.error).join('; ')}).`;
437
+ }
438
+ if (area === 'share') {
439
+ const path = need(action, 'file path');
440
+ if (rest.length) throw new CliError('Share one file at a time (quote a path with spaces). See `botons help`.');
441
+ const out = await call<ShareResult>('POST', `/api/bots/${BOT}/share`, { path: resolve(process.cwd(), path), title: o.title });
442
+ if (o.json) return out;
443
+ return `Shared ${out.file.path}. Your owner sees a chip in the chat that opens it.`;
444
+ }
445
+ if (area === 'react') {
446
+ const id = need(action, 'message id');
447
+ const emoji = need(rest.join(' ').trim() || undefined, 'emoji');
448
+ const out = await call<Message>('POST', `/api/bots/${BOT}/messages/${encodeURIComponent(id)}/reactions`, { emoji });
449
+ if (o.json) return out;
450
+ return `Reacted ${out.reactions?.find((r) => r.by === 'boton')?.emoji ?? emoji} to ${id}. Your owner sees it on their message (no push).`;
451
+ }
452
+ if (area === 'unreact') {
453
+ const id = need(action, 'message id');
454
+ if (rest.length) throw new CliError('Unreact takes only the message id: you have one reaction per message at most.');
455
+ const out = await call<Message>('DELETE', `/api/bots/${BOT}/messages/${encodeURIComponent(id)}/reactions`);
456
+ return o.json ? out : `Took your reaction off ${id}.`;
457
+ }
458
+ if (area === 'approval' || area === 'approvals') {
459
+ switch (action ?? 'list') {
460
+ case 'request':
461
+ case 'ask': {
462
+ const out = await call<ApprovalRequestResult>('POST', `/api/bots/${BOT}/approvals`, {
463
+ title: need(o.title ?? (rest.join(' ').trim() || undefined), '--title'),
464
+ details: o.details,
465
+ followUp: o.then,
466
+ action: o.action,
467
+ expiresIn: o.expires,
468
+ });
469
+ if (o.json) return out;
470
+ const a = out.approval;
471
+ if (a.status === 'allowed') return `Allowed at once (${a.id}): your owner always allows "${a.action}". Go ahead now, in this turn.`;
472
+ const again = out.duplicate ? 'You already asked this; it is still waiting. ' : '';
473
+ return `${again}Asked your owner to approve "${a.title}" (${a.id}); it expires ${when(a.expiresAt, Intl.DateTimeFormat().resolvedOptions().timeZone)}. Don't do it now: end your turn. You get a new turn with the decision${a.followUp ? ' and your --then text' : ''}.`;
474
+ }
475
+ case 'list': {
476
+ const list = await call<Approval[]>('GET', `/api/bots/${BOT}/approvals?status=${o.pending ? 'pending' : 'all'}`);
477
+ if (o.json) return list;
478
+ return list.length ? list.slice(0, 30).map(approvalLine).join('\n') : o.pending ? 'No approval requests waiting.' : 'No approval requests yet.';
479
+ }
480
+ case 'show': {
481
+ const a = await call<Approval>('GET', `/api/approvals/${encodeURIComponent(need(rest[0], 'approval id'))}`);
482
+ if (o.json) return a;
483
+ const lines = [approvalLine(a)];
484
+ if (a.details) lines.push('', 'Details:', a.details);
485
+ if (a.followUp) lines.push('', 'Then:', a.followUp);
486
+ if (a.note) lines.push('', `Your owner's note: ${a.note}`);
487
+ return lines.join('\n');
488
+ }
489
+ case 'cancel':
490
+ case 'withdraw': {
491
+ const a = await call<Approval>('POST', `/api/approvals/${encodeURIComponent(need(rest[0], 'approval id'))}/cancel`);
492
+ return o.json ? a : `Withdrew ${a.id} ("${a.title}").`;
493
+ }
494
+ default:
495
+ throw new CliError(`Unknown approval command "${action}". See \`botons help\`.`);
496
+ }
497
+ }
498
+ if (area === 'card' || area === 'cards') {
499
+ switch (action) {
500
+ case 'post': {
501
+ if ((o.card === undefined) === (o.file === undefined)) throw new CliError('Give the card as --card <json> or --file <card.json> (a file in your workspace). See `botons help`.');
502
+ const raw = o.card ?? readWorkspaceFile(o.file!, WORKSPACE, 64 * 1024, { flag: '--file', instead: 'pass the card itself with --card' });
503
+ let card: unknown;
504
+ try {
505
+ card = JSON.parse(raw);
506
+ } catch (e) {
507
+ throw new CliError(`The card is not valid JSON: ${(e as Error).message}`);
508
+ }
509
+ const out = await call<{ message: Message; duplicate: boolean }>('POST', `/api/bots/${BOT}/cards`, { card, ...(o.expires ? { expiresIn: o.expires } : {}) });
510
+ if (o.json) return out;
511
+ const c = out.message.meta?.card;
512
+ if (c?.status === 'shown') return `Posted the card "${c.descriptor.title}" (${out.message.id}) in your chat.`;
513
+ const again = out.duplicate ? 'You already posted this card; it is still waiting. ' : '';
514
+ return `${again}Posted the card "${c?.descriptor.title}" (${out.message.id}); it waits for your owner until ${when(c?.expiresAt ?? null, Intl.DateTimeFormat().resolvedOptions().timeZone)}. Don't wait for it: end your turn. You get a new turn with the answer.`;
515
+ }
516
+ case 'show': {
517
+ const m = await call<Message>('GET', `/api/cards/${encodeURIComponent(need(rest[0], 'card id'))}`);
518
+ if (o.json) return m;
519
+ const c = m.meta?.card;
520
+ if (!c) throw new CliError('That is not a card.');
521
+ const lines = [`${m.id} ${c.descriptor.title} (${c.status})`];
522
+ if (c.reason) lines.push(`Closed: ${c.reason}`);
523
+ if (c.answer) lines.push(`Answered ${c.answer.at}: ${c.answer.action}${c.answer.option ? ` ${c.answer.option.label}` : ''}${c.answer.result ? ` (${c.answer.result})` : ''}`);
524
+ return lines.join('\n');
525
+ }
526
+ default:
527
+ throw new CliError(`Unknown card command "${action ?? ''}". See \`botons help\`.`);
528
+ }
529
+ }
530
+ if (area === 'connector' || area === 'connectors') {
531
+ switch (action ?? 'list') {
532
+ case 'list': {
533
+ const list = await call<BotonConnector[]>('GET', `/api/bots/${BOT}/connectors`);
534
+ if (o.json) return list;
535
+ if (!list.length) return 'You have no connectors, and none are available on request.';
536
+ return list
537
+ .map((c) => `${c.id} ${c.name} (${c.state === 'yours' ? `yours${c.off ? ', turned off by your owner' : c.usable ? '' : ', not working now'}` : `ask for it: ${c.grant === 'immediate' ? 'granted at once' : 'your owner approves'}`})`)
538
+ .join('\n');
539
+ }
540
+ case 'request':
541
+ case 'ask': {
542
+ const out = await call<ConnectorRequestResult>('POST', `/api/bots/${BOT}/connector-requests`, { connector: need(rest[0], 'connector id (see `botons connector list`)') });
543
+ if (o.json) return out;
544
+ if (out.outcome === 'already') return `${out.connector.name} is already one of your connectors.`;
545
+ if (out.outcome === 'granted') return `${out.connector.name} is yours now; its tools come with your next turn, which starts after this one. End this turn, and carry on with it then.`;
546
+ return `Asked your owner for ${out.connector.name} (${out.approval?.id}). Don't wait for it: end your turn. You get a new turn with the decision.`;
547
+ }
548
+ default:
549
+ throw new CliError(`Unknown connector command "${action}". See \`botons help\`.`);
550
+ }
551
+ }
552
+ if (area === 'team') {
553
+ const team = await call<TeamMember[]>('GET', `/api/bots/${BOT}/team`);
554
+ if (o.json) return team;
555
+ return team.map(teamLine).join('\n');
556
+ }
557
+ if (area === 'ask') {
558
+ const who = need(action, 'who to ask (a name from `botons team`)');
559
+ const text = need(rest.join(' ').trim() || undefined, 'what to ask');
560
+ const target = pick(await call<TeamMember[]>('GET', `/api/bots/${BOT}/team`), who);
561
+ if (target.self) throw new CliError("You can't ask yourself. Do the work in this turn instead.");
562
+ const out = await call<AskResult>('POST', `/api/bots/${encodeURIComponent(target.id)}/asks`, { text });
563
+ if (o.json) return out;
564
+ const later = target.paused ? `${target.name} is paused, so it answers once it is resumed` : `${target.name} answers when it gets to it`;
565
+ return `Asked ${target.name}. ${later}; the reply comes back to you as a new message. Don't wait for it: finish your turn.`;
566
+ }
567
+ throw new CliError(`Unknown command "${area}". See \`botons help\`.`);
568
+ }
569
+
570
+ function approvalLine(a: Approval): string {
571
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
572
+ const status =
573
+ a.status === 'pending'
574
+ ? `waiting, expires ${when(a.expiresAt, tz)}`
575
+ : a.auto
576
+ ? `allowed at once (always allows "${a.action}")`
577
+ : `${a.status === 'cancelled' ? 'withdrawn' : a.status}${a.always ? ` (always allows "${a.action}")` : ''} ${when(a.decidedAt, tz)}`;
578
+ return `${a.id} ${a.title} (${status})`;
579
+ }
580
+
581
+ function teamLine(t: TeamMember): string {
582
+ const bits = [t.state === 'idle' ? 'idle' : t.state === 'queued' ? 'up next' : 'working'];
583
+ if (t.paused) bits.push('paused');
584
+ if (t.primary) bits.push('primary');
585
+ if (t.self) bits.push('you');
586
+ return `${t.id} ${t.name}${t.title ? ` — ${t.title}` : ''} (${bits.join(', ')})`;
587
+ }
588
+
589
+ function pick(team: TeamMember[], who: string): TeamMember {
590
+ const byId = team.find((t) => t.id === who);
591
+ if (byId) return byId;
592
+ const named = team.filter((t) => t.name.toLowerCase() === who.trim().toLowerCase());
593
+ if (named.length === 1) return named[0];
594
+ if (named.length > 1) throw new CliError(`More than one Boton is called ${who}; use an id:\n${named.map(teamLine).join('\n')}`);
595
+ throw new CliError(`No Boton called ${who}. The team:\n${team.map(teamLine).join('\n')}`);
596
+ }
597
+
598
+ /** A SKILL.md: optional frontmatter with name: and description: lines, then the body. */
599
+ function parseSkillFile(text: string): { name?: string; description?: string; body: string } {
600
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text.replace(/^\uFEFF/, ''));
601
+ if (!m) return { body: text.trim() };
602
+ const fields: Record<string, string> = {};
603
+ for (const line of m[1].split(/\r?\n/)) {
604
+ const kv = /^([A-Za-z_-]+):\s*(.*)$/.exec(line);
605
+ if (kv) fields[kv[1].toLowerCase()] = kv[2].trim().replace(/^(["'])(.*)\1$/, '$2');
606
+ }
607
+ return { name: fields.name || undefined, description: fields.description || undefined, body: m[2].trim() };
608
+ }
609
+
610
+ function need<T>(v: T | undefined, what: string): T {
611
+ if (v === undefined || v === '') throw new CliError(`Missing ${what}. See \`botons help\`.`);
612
+ return v;
613
+ }
614
+
615
+ if (process.argv[2] === 'mcp') {
616
+ // Claude Code runs this for a turn with Ask first rules; stdout is the MCP channel.
617
+ await serveApprovals({ api: API, bot: BOT, token: TOKEN, pollMs: Number(process.env.BOTON_APPROVAL_POLL_MS) || undefined });
618
+ } else if (process.argv[2] === 'browser-mcp') {
619
+ // Both CLIs start this for a Boton that has the Botons browser connector; stdout is the MCP channel.
620
+ await serveBrowser({ api: API, bot: BOT, token: TOKEN });
621
+ } else {
622
+ try {
623
+ const out = await main(process.argv.slice(2));
624
+ console.log(typeof out === 'string' ? out : JSON.stringify(out, null, 2));
625
+ } catch (e) {
626
+ console.error(`botons: ${e instanceof CliError || e instanceof WorkspaceFileError ? e.message : (e as Error).stack}`);
627
+ process.exit(1);
628
+ }
629
+ }