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.
- package/LICENSE +21 -0
- package/README.md +505 -2
- package/installer/admin.mjs +6 -0
- package/installer/botons.mjs +430 -0
- package/installer/brain-clis.mjs +28 -0
- package/installer/linger.mjs +39 -0
- package/installer/output.mjs +27 -0
- package/installer/revision.mjs +30 -0
- package/installer/shell-path.mjs +66 -0
- package/installer/source-lock.json +2891 -0
- package/package.json +26 -4
- package/payload/LICENSE +21 -0
- package/payload/SECURITY.md +33 -0
- package/payload/installer/admin.mjs +6 -0
- package/payload/installer/botons.mjs +430 -0
- package/payload/installer/brain-clis.mjs +28 -0
- package/payload/installer/linger.mjs +39 -0
- package/payload/installer/output.mjs +27 -0
- package/payload/installer/revision.mjs +30 -0
- package/payload/installer/shell-path.mjs +66 -0
- package/payload/installer/source-lock.json +2891 -0
- package/payload/npm-shrinkwrap.json +51 -0
- package/payload/package.json +13 -0
- package/payload/scripts/backup.ts +47 -0
- package/payload/scripts/botons-backup.service +15 -0
- package/payload/scripts/botons-backup.timer +11 -0
- package/payload/scripts/botons.service +36 -0
- package/payload/scripts/check-browser.ts +90 -0
- package/payload/scripts/doctor.ts +790 -0
- package/payload/scripts/install-backup-timer.ts +52 -0
- package/payload/scripts/install-service.ts +206 -0
- package/payload/scripts/login.ts +17 -0
- package/payload/scripts/migrate-names.ts +47 -0
- package/payload/scripts/pair.ts +163 -0
- package/payload/scripts/restore.ts +108 -0
- package/payload/server/bin/botons +4 -0
- package/payload/server/bin/legacy/minion +4 -0
- package/payload/server/bin/legacy/tots +4 -0
- package/payload/server/src/acting.ts +16 -0
- package/payload/server/src/api-keys.ts +544 -0
- package/payload/server/src/api.ts +1484 -0
- package/payload/server/src/app.ts +546 -0
- package/payload/server/src/approvals.ts +873 -0
- package/payload/server/src/ask-rules.ts +181 -0
- package/payload/server/src/audit.ts +158 -0
- package/payload/server/src/auth.ts +497 -0
- package/payload/server/src/backup.ts +382 -0
- package/payload/server/src/brain-login.ts +75 -0
- package/payload/server/src/browser-cert.ts +67 -0
- package/payload/server/src/browser-handoff.ts +119 -0
- package/payload/server/src/browser-handover.ts +175 -0
- package/payload/server/src/browser-policy.ts +210 -0
- package/payload/server/src/browser-proxy.ts +580 -0
- package/payload/server/src/browser-request.ts +335 -0
- package/payload/server/src/browser-screenshot.ts +56 -0
- package/payload/server/src/browser-send.ts +155 -0
- package/payload/server/src/browser.ts +2164 -0
- package/payload/server/src/callback.ts +218 -0
- package/payload/server/src/capabilities.ts +90 -0
- package/payload/server/src/cards.ts +243 -0
- package/payload/server/src/cli/approve-mcp.ts +119 -0
- package/payload/server/src/cli/botons.ts +629 -0
- package/payload/server/src/cli/browser-mcp.ts +168 -0
- package/payload/server/src/cli/workspace-file.ts +78 -0
- package/payload/server/src/cli-homes.ts +38 -0
- package/payload/server/src/cli-install.ts +104 -0
- package/payload/server/src/cli-updates.ts +112 -0
- package/payload/server/src/codex-config-repair.ts +80 -0
- package/payload/server/src/config.ts +473 -0
- package/payload/server/src/connector-access.ts +158 -0
- package/payload/server/src/connector-launch.ts +203 -0
- package/payload/server/src/connectors.ts +947 -0
- package/payload/server/src/disk-usage.ts +62 -0
- package/payload/server/src/engine.ts +2328 -0
- package/payload/server/src/env-init.ts +5 -0
- package/payload/server/src/events.ts +46 -0
- package/payload/server/src/files.ts +528 -0
- package/payload/server/src/github-access.ts +52 -0
- package/payload/server/src/health.ts +117 -0
- package/payload/server/src/held-dir.ts +96 -0
- package/payload/server/src/hooks.ts +141 -0
- package/payload/server/src/http.ts +207 -0
- package/payload/server/src/index.ts +139 -0
- package/payload/server/src/install-type.ts +11 -0
- package/payload/server/src/instructions.ts +204 -0
- package/payload/server/src/isolation.ts +234 -0
- package/payload/server/src/legacy-upgrade.ts +668 -0
- package/payload/server/src/limits.ts +44 -0
- package/payload/server/src/lock.ts +75 -0
- package/payload/server/src/log.ts +137 -0
- package/payload/server/src/managed-clis.ts +65 -0
- package/payload/server/src/mcp-client.ts +229 -0
- package/payload/server/src/notify.ts +193 -0
- package/payload/server/src/oauth-connections.ts +650 -0
- package/payload/server/src/oauth.ts +311 -0
- package/payload/server/src/outbound.ts +142 -0
- package/payload/server/src/portable.ts +1669 -0
- package/payload/server/src/prompt.ts +676 -0
- package/payload/server/src/providers/api-brain.ts +612 -0
- package/payload/server/src/providers/api-history.ts +131 -0
- package/payload/server/src/providers/api-tools.ts +481 -0
- package/payload/server/src/providers/api-wire.ts +690 -0
- package/payload/server/src/providers/claude-cli.ts +620 -0
- package/payload/server/src/providers/cli-check.ts +265 -0
- package/payload/server/src/providers/cli-turn.ts +20 -0
- package/payload/server/src/providers/codex-cli.ts +552 -0
- package/payload/server/src/providers/codex-workspace.ts +61 -0
- package/payload/server/src/providers/failures.ts +143 -0
- package/payload/server/src/providers/index.ts +131 -0
- package/payload/server/src/providers/mcp-stdio.ts +147 -0
- package/payload/server/src/providers/mock.ts +94 -0
- package/payload/server/src/providers/process.ts +268 -0
- package/payload/server/src/providers/readiness.ts +43 -0
- package/payload/server/src/providers/sse.ts +73 -0
- package/payload/server/src/providers/types.ts +173 -0
- package/payload/server/src/rates.ts +128 -0
- package/payload/server/src/reactions.ts +266 -0
- package/payload/server/src/recent-logs.ts +489 -0
- package/payload/server/src/rehearsal.ts +593 -0
- package/payload/server/src/rehearsals.ts +303 -0
- package/payload/server/src/replay.ts +165 -0
- package/payload/server/src/sandbox.ts +767 -0
- package/payload/server/src/schedule.ts +214 -0
- package/payload/server/src/scheduler.ts +24 -0
- package/payload/server/src/schema.ts +882 -0
- package/payload/server/src/secrets.ts +46 -0
- package/payload/server/src/setup.ts +356 -0
- package/payload/server/src/skill-packages.ts +363 -0
- package/payload/server/src/skills.ts +527 -0
- package/payload/server/src/spend.ts +255 -0
- package/payload/server/src/ssrf.ts +126 -0
- package/payload/server/src/starter-gate.ts +168 -0
- package/payload/server/src/starters.ts +610 -0
- package/payload/server/src/storage.ts +32 -0
- package/payload/server/src/store.ts +3087 -0
- package/payload/server/src/tailnet.ts +132 -0
- package/payload/server/src/templates.ts +454 -0
- package/payload/server/src/trash.ts +772 -0
- package/payload/server/src/util.ts +97 -0
- package/payload/server/src/webhooks.ts +216 -0
- package/payload/server/src/work-feed.ts +473 -0
- package/payload/shared/app-version.d.mts +1 -0
- package/payload/shared/app-version.mjs +3 -0
- package/payload/shared/boton.ts +323 -0
- package/payload/shared/brand.ts +107 -0
- package/payload/shared/cards.ts +427 -0
- package/payload/shared/effort.ts +91 -0
- package/payload/shared/files.ts +104 -0
- package/payload/shared/handoff.ts +97 -0
- package/payload/shared/install-commands.ts +7 -0
- package/payload/shared/legacy-names.d.mts +35 -0
- package/payload/shared/legacy-names.mjs +244 -0
- package/payload/shared/live-view.ts +94 -0
- package/payload/shared/look-critter.ts +189 -0
- package/payload/shared/look-tater.ts +127 -0
- package/payload/shared/look.ts +200 -0
- package/payload/shared/reactions.ts +43 -0
- package/payload/shared/seeded.ts +34 -0
- package/payload/shared/services.ts +90 -0
- package/payload/shared/types.ts +3016 -0
- package/payload/shared/util.ts +68 -0
- package/payload/shared/versions.ts +44 -0
- package/payload/shared/wake.ts +164 -0
- package/payload/web/dist/apple-touch-icon.png +0 -0
- package/payload/web/dist/assets/Icons-nytVAa9g.js +1 -0
- package/payload/web/dist/assets/NewBotDialog-DXr2XhDP.js +1 -0
- package/payload/web/dist/assets/PairScreen-CMcJO7cQ.js +1 -0
- package/payload/web/dist/assets/Panel-Bvqz3I3m.js +4 -0
- package/payload/web/dist/assets/ReactionPicker-B0SKsjiy.js +1 -0
- package/payload/web/dist/assets/SettingRows-CvLomtuo.js +4 -0
- package/payload/web/dist/assets/SettingsDialog-CQ41LV-P.js +8 -0
- package/payload/web/dist/assets/Setup-C-3P7Mtn.js +1 -0
- package/payload/web/dist/assets/Skills-DXMH69kH.js +1 -0
- package/payload/web/dist/assets/StarterSetup-Ce2V1_Cs.js +5 -0
- package/payload/web/dist/assets/Steps-CKtB3GAM.js +1 -0
- package/payload/web/dist/assets/emoji-catalog-DfpPz8qw.js +540 -0
- package/payload/web/dist/assets/index-BEU7FDxV.css +1 -0
- package/payload/web/dist/assets/index-Dhwn-rf0.js +3 -0
- package/payload/web/dist/assets/react-C1iE8mnY.js +9 -0
- package/payload/web/dist/assets/reactions-rw0YeCmA.js +1 -0
- package/payload/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/payload/web/dist/assets/util-Cx5feSJu.js +1 -0
- package/payload/web/dist/assets/vendor-B3qdcjGo.js +14 -0
- package/payload/web/dist/favicon.svg +1 -0
- package/payload/web/dist/icon-192.png +0 -0
- package/payload/web/dist/icon-512.png +0 -0
- package/payload/web/dist/icon.svg +1 -0
- package/payload/web/dist/index.html +67 -0
- package/payload/web/dist/manifest.webmanifest +12 -0
- package/shared/legacy-names.d.mts +35 -0
- package/shared/legacy-names.mjs +244 -0
|
@@ -0,0 +1,3016 @@
|
|
|
1
|
+
// API contract shared by the server and the web app.
|
|
2
|
+
|
|
3
|
+
import type { TranscriptCard } from './cards.ts';
|
|
4
|
+
import type { BrowserHandoff } from './handoff.ts';
|
|
5
|
+
import type { CliCopy } from './versions.ts';
|
|
6
|
+
|
|
7
|
+
export type AccessLevel = 'workspace' | 'host';
|
|
8
|
+
|
|
9
|
+
export type BotonShape = 'round' | 'pill' | 'key';
|
|
10
|
+
export type BotonShell = 'snow' | 'steel' | 'graphite' | 'sun' | 'signal' | 'cobalt' | 'violet' | 'teal';
|
|
11
|
+
export type BotonGlow = 'cyan' | 'pink' | 'lime' | 'amber' | 'violet' | 'ice';
|
|
12
|
+
export type BotonEyes = 'dots' | 'rings' | 'bars' | 'happy' | 'wink' | 'lidded';
|
|
13
|
+
export type BotonBrows = 'none' | 'soft' | 'straight' | 'arched' | 'bold';
|
|
14
|
+
export type BotonMouth = 'smile' | 'grin' | 'smirk' | 'o' | 'calm' | 'wave';
|
|
15
|
+
export type BotonTop = 'none' | 'antenna' | 'twin' | 'dish' | 'coil' | 'fin' | 'beacon' | 'halo';
|
|
16
|
+
export type BotonEars = 'none' | 'bolts' | 'pods' | 'fins' | 'knobs';
|
|
17
|
+
export type BotonVisor = 'none' | 'goggles' | 'monocle' | 'band';
|
|
18
|
+
export type BotonPanel = 'none' | 'holes' | 'grill' | 'leds' | 'dial' | 'switch';
|
|
19
|
+
export type BotonArms = 'none' | 'nubs' | 'wave' | 'cheer';
|
|
20
|
+
export type BotonBackdrop = 'mist' | 'ice' | 'mint' | 'lilac' | 'blush' | 'night' | 'plum' | 'grid';
|
|
21
|
+
|
|
22
|
+
/** A Boton's look: a button-bot built from fixed parts (see shared/look.ts for the values, shared/boton.ts for the drawing). */
|
|
23
|
+
export interface BotonLook {
|
|
24
|
+
/** The button: round, a wide pill, or a keycap. */
|
|
25
|
+
shape: BotonShape;
|
|
26
|
+
/** Body color. */
|
|
27
|
+
shell: BotonShell;
|
|
28
|
+
/** The light of its face and rim. */
|
|
29
|
+
glow: BotonGlow;
|
|
30
|
+
eyes: BotonEyes;
|
|
31
|
+
brows: BotonBrows;
|
|
32
|
+
mouth: BotonMouth;
|
|
33
|
+
/** On top: antennas and more. */
|
|
34
|
+
top: BotonTop;
|
|
35
|
+
ears: BotonEars;
|
|
36
|
+
/** Over the display. */
|
|
37
|
+
visor: BotonVisor;
|
|
38
|
+
/** Under the display. */
|
|
39
|
+
panel: BotonPanel;
|
|
40
|
+
arms: BotonArms;
|
|
41
|
+
backdrop: BotonBackdrop;
|
|
42
|
+
cheeks: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What an avatar shows through its face and parts. `asleep`: no working brain yet. */
|
|
46
|
+
export type AvatarState = 'idle' | 'working' | 'approval' | 'error' | 'asleep';
|
|
47
|
+
|
|
48
|
+
export interface BotStatus {
|
|
49
|
+
state: 'idle' | 'queued' | 'working';
|
|
50
|
+
activity: string | null;
|
|
51
|
+
since: string | null;
|
|
52
|
+
draft?: TurnDraft | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TurnDraft {
|
|
56
|
+
turnId: string;
|
|
57
|
+
text: string;
|
|
58
|
+
at: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type StepKind = 'command' | 'read' | 'edit' | 'search' | 'web' | 'tool';
|
|
62
|
+
|
|
63
|
+
export interface TurnStep {
|
|
64
|
+
kind: StepKind;
|
|
65
|
+
label: string;
|
|
66
|
+
detail: string;
|
|
67
|
+
at: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface TurnSteps {
|
|
71
|
+
turnId: string;
|
|
72
|
+
startedAt: string;
|
|
73
|
+
outcome: TurnOutcome;
|
|
74
|
+
/** Only the first 200 steps are kept, so `steps` can be shorter. */
|
|
75
|
+
total: number;
|
|
76
|
+
steps: TurnStep[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface Bot {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
title: string;
|
|
83
|
+
instructions: string;
|
|
84
|
+
look: BotonLook;
|
|
85
|
+
provider: string;
|
|
86
|
+
/** "": the provider's default. */
|
|
87
|
+
model: string;
|
|
88
|
+
/** How hard it thinks (a level of shared/effort.ts for its provider); "": the provider's default. */
|
|
89
|
+
effort: string;
|
|
90
|
+
access: AccessLevel;
|
|
91
|
+
/** Its sidebar group's id; null: ungrouped. */
|
|
92
|
+
groupId: string | null;
|
|
93
|
+
/** That group's name; "": ungrouped. */
|
|
94
|
+
group: string;
|
|
95
|
+
pinned: boolean;
|
|
96
|
+
timezone: string;
|
|
97
|
+
workspace: string;
|
|
98
|
+
createdAt: string;
|
|
99
|
+
updatedAt: string;
|
|
100
|
+
lastMessage: { role: MessageRole; text: string; createdAt: string; from?: string } | null;
|
|
101
|
+
unread: number;
|
|
102
|
+
status: BotStatus;
|
|
103
|
+
paused: boolean;
|
|
104
|
+
connections: string[];
|
|
105
|
+
/** null: every skill in the library. */
|
|
106
|
+
skills: string[] | null;
|
|
107
|
+
alwaysAllow: string[];
|
|
108
|
+
askFirst: string[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type MessageRole = 'user' | 'bot' | 'event' | 'error';
|
|
112
|
+
|
|
113
|
+
export type MessageKind =
|
|
114
|
+
| 'routine.created'
|
|
115
|
+
| 'routine.updated'
|
|
116
|
+
| 'routine.paused'
|
|
117
|
+
| 'routine.resumed'
|
|
118
|
+
| 'routine.deleted'
|
|
119
|
+
| 'routine.run'
|
|
120
|
+
| 'routine.rate_limited'
|
|
121
|
+
| 'routine.model_reset'
|
|
122
|
+
| 'effort.reset'
|
|
123
|
+
| 'provider.switched'
|
|
124
|
+
| 'provider.summary'
|
|
125
|
+
| 'memory.saved'
|
|
126
|
+
| 'memory.forgotten'
|
|
127
|
+
| 'session.reset'
|
|
128
|
+
| 'notify.sent'
|
|
129
|
+
| 'turn.stopped'
|
|
130
|
+
| 'turn.interrupted'
|
|
131
|
+
| 'bot.paused'
|
|
132
|
+
| 'bot.resumed'
|
|
133
|
+
| 'usage.limit'
|
|
134
|
+
| 'file.shared'
|
|
135
|
+
| 'instructions.restored'
|
|
136
|
+
| 'ask'
|
|
137
|
+
| 'ask.reply'
|
|
138
|
+
| 'ask.sent'
|
|
139
|
+
| 'ask.refused'
|
|
140
|
+
| 'bot.duplicated'
|
|
141
|
+
| 'bot.from_template'
|
|
142
|
+
| 'bot.imported'
|
|
143
|
+
| 'skill.created'
|
|
144
|
+
| 'skill.updated'
|
|
145
|
+
| 'approval.requested'
|
|
146
|
+
| 'approval.decided'
|
|
147
|
+
| 'approval.auto'
|
|
148
|
+
| 'approval.expired'
|
|
149
|
+
| 'approval.cancelled'
|
|
150
|
+
| 'connector.granted'
|
|
151
|
+
| 'connector.reconnect'
|
|
152
|
+
| 'work.event'
|
|
153
|
+
| 'card.posted'
|
|
154
|
+
| 'card.answered'
|
|
155
|
+
| 'reaction.followup'
|
|
156
|
+
| 'browser.handoff'
|
|
157
|
+
| 'browser.handed_back';
|
|
158
|
+
|
|
159
|
+
export interface MessageMeta {
|
|
160
|
+
kind?: MessageKind;
|
|
161
|
+
routineId?: string;
|
|
162
|
+
routineName?: string;
|
|
163
|
+
runId?: string;
|
|
164
|
+
trigger?: RunTrigger;
|
|
165
|
+
memoryText?: string;
|
|
166
|
+
notifyText?: string;
|
|
167
|
+
priority?: NotifyPriority;
|
|
168
|
+
skillName?: string;
|
|
169
|
+
attempt?: number;
|
|
170
|
+
retryAt?: string;
|
|
171
|
+
durationMs?: number;
|
|
172
|
+
provider?: string;
|
|
173
|
+
/** provider.summary: the name of the provider whose session wrote it, as the server had it then. */
|
|
174
|
+
providerLabel?: string;
|
|
175
|
+
model?: string;
|
|
176
|
+
/** The effort level the turn asked its CLI for (not necessarily the one that ran: no CLI reports that); absent: none, the CLI's default. */
|
|
177
|
+
effort?: string;
|
|
178
|
+
usage?: TokenUsage;
|
|
179
|
+
turnId?: string;
|
|
180
|
+
steps?: number;
|
|
181
|
+
staleContext?: boolean;
|
|
182
|
+
attachments?: FileRef[];
|
|
183
|
+
file?: FileRef & { title?: string };
|
|
184
|
+
/** Taken from the asking Boton's turn token, never from what it says. */
|
|
185
|
+
fromBotId?: string;
|
|
186
|
+
fromBotName?: string;
|
|
187
|
+
toBotId?: string;
|
|
188
|
+
toBotName?: string;
|
|
189
|
+
askText?: string;
|
|
190
|
+
threadId?: string;
|
|
191
|
+
hop?: number;
|
|
192
|
+
askId?: string;
|
|
193
|
+
sourceBotId?: string;
|
|
194
|
+
sourceName?: string;
|
|
195
|
+
templateId?: string;
|
|
196
|
+
copiedRoutines?: number;
|
|
197
|
+
copiedMemories?: number;
|
|
198
|
+
approvalId?: string;
|
|
199
|
+
approvalTitle?: string;
|
|
200
|
+
approvalAction?: string;
|
|
201
|
+
decision?: 'allowed' | 'denied';
|
|
202
|
+
note?: string;
|
|
203
|
+
connectorId?: string;
|
|
204
|
+
/** A work update from a connector's feed: only values the server checked. */
|
|
205
|
+
work?: WorkEvent;
|
|
206
|
+
/** provider.switched: what the server noted at the switch, for the first turn of the new provider's session. */
|
|
207
|
+
handoff?: ProviderHandoff;
|
|
208
|
+
/** provider.switched: a turn on the new provider has run since, so the note is not given again. */
|
|
209
|
+
handoffDone?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* The owner pinned this message (POST /api/bots/:id/messages/:messageId/pin): every new session is given it,
|
|
212
|
+
* whatever its age (a long one as an excerpt: its start and end). Only that route sets it, and starting the chat
|
|
213
|
+
* fresh takes it off.
|
|
214
|
+
*/
|
|
215
|
+
pinned?: { at: string };
|
|
216
|
+
/** card.posted: the card a Boton posted, checked by the server, and how it stands. */
|
|
217
|
+
card?: TranscriptCard;
|
|
218
|
+
/** card.answered: the card (its message id) this answers. */
|
|
219
|
+
cardId?: string;
|
|
220
|
+
/** browser.handoff: the Boton's request for the owner to take its browser over, and how it stands (shared/handoff.ts). */
|
|
221
|
+
browserHandoff?: BrowserHandoff;
|
|
222
|
+
/** browser.handed_back: the handoff (its message id) the owner handed back. */
|
|
223
|
+
handoffId?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Left out of the transcript: an automatic wake-up (see shared/wake.ts) whose turn ended [quiet], or what that turn
|
|
226
|
+
* did in the chat (a memory saved, a file shared...). The server sets it when the turn ends, and reads it from the
|
|
227
|
+
* turn records for chat history from before that (without writing it). The message is still returned here, and the
|
|
228
|
+
* turn's history keeps the note.
|
|
229
|
+
*/
|
|
230
|
+
quiet?: boolean;
|
|
231
|
+
/** The quiet turn a marked message belongs to: its steps hold the note. */
|
|
232
|
+
quietTurn?: string;
|
|
233
|
+
/**
|
|
234
|
+
* An event of what a Boton does (TURN_ACTIVITY in shared/wake.ts: memory, a shared file, a notification, an ask, its
|
|
235
|
+
* routines and skills): the turn it did it in, or null when it wasn't a turn (the owner, the server). Missing on older
|
|
236
|
+
* messages, which then always show: nothing else says whether a turn or the owner did it.
|
|
237
|
+
*/
|
|
238
|
+
byTurn?: string | null;
|
|
239
|
+
/** reaction.followup: the Boton's message the owner reacted to, and with which emoji. */
|
|
240
|
+
reactionTo?: string;
|
|
241
|
+
emoji?: string;
|
|
242
|
+
/** reaction.followup: the owner took the reaction off before the follow-up turn started, so none ran. */
|
|
243
|
+
withdrawn?: boolean;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A provider and the model a Boton asks it for ("": its default). */
|
|
247
|
+
export interface BrainRef {
|
|
248
|
+
provider: string;
|
|
249
|
+
/** The provider's name, as the server had it at the time. */
|
|
250
|
+
label: string;
|
|
251
|
+
model: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* What the server noted when a Boton moved to another provider: taken once, at the switch, from what Botons
|
|
256
|
+
* stores (never from the old provider's session), and bounded. The first turn of the new session starts with it.
|
|
257
|
+
*/
|
|
258
|
+
export interface ProviderHandoff {
|
|
259
|
+
from: BrainRef;
|
|
260
|
+
to: BrainRef;
|
|
261
|
+
at: string;
|
|
262
|
+
/** Whether a session on the old provider was there to lose. */
|
|
263
|
+
sessionEnded: boolean;
|
|
264
|
+
/** A turn was underway on the old provider at the move: it finished there, and its session was not kept. */
|
|
265
|
+
working?: boolean;
|
|
266
|
+
kept: { memories: number; routines: number };
|
|
267
|
+
/** Its approval requests still waiting for the owner. */
|
|
268
|
+
waiting: { id: string; title: string }[];
|
|
269
|
+
/** Runs queued or running at the switch. */
|
|
270
|
+
runs: { routineId: string; routineName: string; status: 'queued' | 'running' }[];
|
|
271
|
+
/** Scheduled runs due within a day. */
|
|
272
|
+
dueSoon: { routineId: string; routineName: string; at: string }[];
|
|
273
|
+
/** The owner's latest decisions on its requests. */
|
|
274
|
+
decisions: { title: string; decision: 'allowed' | 'denied'; at: string }[];
|
|
275
|
+
/**
|
|
276
|
+
* The provider.summary message the old session wrote at the owner's request just before the move: the old agent's
|
|
277
|
+
* own words, which the server bounds but doesn't check. Absent when none was asked for.
|
|
278
|
+
*/
|
|
279
|
+
summary?: { messageId: string };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Whether the move can first ask the old session for a short summary of its open work: one more turn there, with no
|
|
284
|
+
* tools, at the owner's choice. Never offered when there is no session to ask, or it can't run now.
|
|
285
|
+
*/
|
|
286
|
+
export interface SwitchSummaryOffer {
|
|
287
|
+
available: boolean;
|
|
288
|
+
/** Why it isn't (null when it is). */
|
|
289
|
+
reason: string | null;
|
|
290
|
+
/** Sent back as `switchConfirmation`, with `switchSummary: true`, to move with the summary. */
|
|
291
|
+
confirmation: string | null;
|
|
292
|
+
/** What the extra turn may take: its time limit, and the summary kept at most (longer is cut at a sentence). */
|
|
293
|
+
timeoutMs: number;
|
|
294
|
+
maxChars: number;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface ProviderSwitchInput {
|
|
298
|
+
provider: string;
|
|
299
|
+
/** As the update will send it: absent, the Boton's current model. */
|
|
300
|
+
model?: string;
|
|
301
|
+
/** As the update will send it: absent, the Boton's level, kept if the new provider and model have it. */
|
|
302
|
+
effort?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** What moving a Boton to another provider keeps and loses, before it happens. */
|
|
306
|
+
export interface ProviderSwitchPreview {
|
|
307
|
+
from: BrainRef & { effort: string };
|
|
308
|
+
to: BrainRef & { effort: string };
|
|
309
|
+
/**
|
|
310
|
+
* Whether the update must carry `confirmation`: a session on the old provider would end (the chat's, or a
|
|
311
|
+
* routine's own). Without one there is nothing to lose, and the switch needs no confirming.
|
|
312
|
+
*/
|
|
313
|
+
required: boolean;
|
|
314
|
+
/**
|
|
315
|
+
* Sent back as `switchConfirmation` with the update. It stands for everything else in this preview, for this
|
|
316
|
+
* Boton: if any of it differs when the update comes (a session, a turn starting or ending, a memory, routine or pin,
|
|
317
|
+
* queued runs, the new provider's readiness or a usage limit), the update is refused and the owner reviews again.
|
|
318
|
+
*/
|
|
319
|
+
confirmation: string | null;
|
|
320
|
+
/** The sessions the move ends: saved ones, and the one a turn underway on the old provider is starting or carrying on. */
|
|
321
|
+
sessions: { chat: boolean; routines: { id: string; name: string }[] };
|
|
322
|
+
/**
|
|
323
|
+
* A turn is underway on the old provider (setting up, or running). It finishes there: what it does still happens
|
|
324
|
+
* and its reply is kept, but its session isn't. Queued runs run on the new one.
|
|
325
|
+
*/
|
|
326
|
+
working: boolean;
|
|
327
|
+
queuedRuns: number;
|
|
328
|
+
/**
|
|
329
|
+
* What the new session is given of the chat, sized to one budget it shares with the handoff note and the summary.
|
|
330
|
+
* The budget bounds the replayed text only, not the instructions and context every session starts with. Sizes are
|
|
331
|
+
* estimated tokens, counted as UTF-8 bytes: never fewer than the real count. Pinned messages always fit
|
|
332
|
+
* (at most pinLimit of them, each bounded); then the latest messages, newest first, until the budget is spent.
|
|
333
|
+
* Messages over messageLimit are given as excerpts (their start and end), not summarized.
|
|
334
|
+
*/
|
|
335
|
+
replay: { budget: number; messageLimit: number; pinned: number; pinLimit: number };
|
|
336
|
+
/**
|
|
337
|
+
* What the replay would give now, without a summary (which takes some of the budget when there is one). An estimate
|
|
338
|
+
* for the owner, not bound by the confirmation: the chat moves on before the turn.
|
|
339
|
+
*/
|
|
340
|
+
replayNow: ReplayEstimate;
|
|
341
|
+
kept: { memories: number; routines: number };
|
|
342
|
+
/** The Boton's level goes back to the default because the new provider or model lacks it (not the update's own pick). */
|
|
343
|
+
effortReset: boolean;
|
|
344
|
+
/** Routines whose own model is the old provider's: they go back to the Boton's model. */
|
|
345
|
+
routineModels: { id: string; name: string; model: string }[];
|
|
346
|
+
/** Routines whose own effort the new provider or model lacks: they go back to the Boton's. */
|
|
347
|
+
routineEfforts: { id: string; name: string; effort: string }[];
|
|
348
|
+
/** Its connectors the new provider can't run. */
|
|
349
|
+
connectors: { id: string; name: string }[];
|
|
350
|
+
/**
|
|
351
|
+
* What the server knows of the new provider: ready only when its last check found it usable; unknown before any
|
|
352
|
+
* check. Never a promise that a turn will work.
|
|
353
|
+
*/
|
|
354
|
+
readiness: { state: 'ready' | 'unavailable' | 'unknown'; reason: string | null; usageLimitUntil: string | null };
|
|
355
|
+
/** The old provider is out of usage until then: the server noted it, and switching is the owner's choice. */
|
|
356
|
+
oldUsageLimitUntil: string | null;
|
|
357
|
+
summary: SwitchSummaryOffer;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** What a replay would give of the chat, as counts: see ProviderSwitchPreview.replay. */
|
|
361
|
+
export interface ReplayEstimate {
|
|
362
|
+
/** Estimated tokens used of the budget. */
|
|
363
|
+
used: number;
|
|
364
|
+
messages: number;
|
|
365
|
+
routines: number;
|
|
366
|
+
pinned: number;
|
|
367
|
+
excerpts: number;
|
|
368
|
+
/** Older messages since the reset are left out. */
|
|
369
|
+
olderLeftOut: boolean;
|
|
370
|
+
/** The oldest message given, pins apart (null: none). */
|
|
371
|
+
since: string | null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** POST /api/bots/:id/messages/:messageId/pin. */
|
|
375
|
+
export interface PinInput {
|
|
376
|
+
pinned: boolean;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export type WorkEventKind = 'finished' | 'needs_input' | 'failed' | 'cancelled' | 'delegated_ended';
|
|
380
|
+
|
|
381
|
+
export interface WorkEvent {
|
|
382
|
+
connectorId: string;
|
|
383
|
+
jobId: string;
|
|
384
|
+
kind: WorkEventKind;
|
|
385
|
+
state: string | null;
|
|
386
|
+
/** t3-thread: or https: only, from the connector's own job data. */
|
|
387
|
+
link: string | null;
|
|
388
|
+
/** The feed's id for the event it came from (its cursor). */
|
|
389
|
+
eventId?: string;
|
|
390
|
+
/**
|
|
391
|
+
* The job's title from the connector's own job data, cleaned to one short line: shown to the owner only, never in
|
|
392
|
+
* anything a Boton reads.
|
|
393
|
+
*/
|
|
394
|
+
title?: string;
|
|
395
|
+
/** Not known to be any Boton's: delivered to the primary Boton by the connector's setting. */
|
|
396
|
+
unowned?: boolean;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export interface FileRef {
|
|
400
|
+
name: string;
|
|
401
|
+
/** Relative to the Boton's workspace, "/"-separated. */
|
|
402
|
+
path: string;
|
|
403
|
+
size: number;
|
|
404
|
+
type: string;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export interface FileEntry {
|
|
408
|
+
name: string;
|
|
409
|
+
path: string;
|
|
410
|
+
isDir: boolean;
|
|
411
|
+
size: number;
|
|
412
|
+
mtime: string;
|
|
413
|
+
/** In the inbox only: kept by the owner, so neither the daily cleanup nor Clear inbox moves it to the trash. */
|
|
414
|
+
pinned?: boolean;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Why something is in the trash: the owner deleted it, the daily inbox cleanup moved it, or Clear inbox did. */
|
|
418
|
+
export type TrashReason = 'deleted' | 'inbox_cleanup' | 'inbox_cleared';
|
|
419
|
+
|
|
420
|
+
/** Where a trash item came from, and so where Restore puts it: a Boton's workspace, or the shared folder. */
|
|
421
|
+
export type TrashOrigin = 'workspace' | 'shared';
|
|
422
|
+
|
|
423
|
+
/** Something moved to the server's trash (BOTONS_HOME/trash), kept for TRASH_KEEP_DAYS, then deleted for good. */
|
|
424
|
+
export interface TrashItem {
|
|
425
|
+
id: string;
|
|
426
|
+
origin: TrashOrigin;
|
|
427
|
+
/** The Boton whose workspace it was in; empty for the shared folder. */
|
|
428
|
+
botId: string;
|
|
429
|
+
/** The Boton's name when it was deleted; empty for the shared folder. */
|
|
430
|
+
botName: string;
|
|
431
|
+
/** Where it was, relative to the Boton's workspace (or the shared folder), "/"-separated. */
|
|
432
|
+
path: string;
|
|
433
|
+
name: string;
|
|
434
|
+
isDir: boolean;
|
|
435
|
+
/** Bytes, files in a folder added up. */
|
|
436
|
+
size: number;
|
|
437
|
+
deletedAt: string;
|
|
438
|
+
/** When it is deleted for good. */
|
|
439
|
+
purgeAt: string;
|
|
440
|
+
reason: TrashReason;
|
|
441
|
+
/** The Boton itself was deleted since: it can only be deleted forever. */
|
|
442
|
+
botGone: boolean;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export interface DeleteFilesInput {
|
|
446
|
+
/** Workspace (or shared folder) paths, as the listing gives them. */
|
|
447
|
+
paths: string[];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export interface TrashResult {
|
|
451
|
+
trashed: TrashItem[];
|
|
452
|
+
/** Clear inbox: how many items it left because they are kept (pinned). */
|
|
453
|
+
kept?: number;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export interface RestoreResult {
|
|
457
|
+
origin: TrashOrigin;
|
|
458
|
+
/** Where it is now, relative to the workspace (or the shared folder): its old path, with -2, -3… when that was taken, or in the nearest folder on the way when its own can't be made again. */
|
|
459
|
+
path: string;
|
|
460
|
+
renamed: boolean;
|
|
461
|
+
/** Its own folder couldn't be made again (a file in the way, or a link out), so it is in the nearest one that could. */
|
|
462
|
+
folderGone: boolean;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export interface InboxPinInput {
|
|
466
|
+
/** inbox/<name>: a file or folder directly in the inbox. */
|
|
467
|
+
path: string;
|
|
468
|
+
pinned: boolean;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Bytes a Boton's files take: the workspace (inbox included), its inbox, and its items in the trash. */
|
|
472
|
+
export interface BotStorage {
|
|
473
|
+
botId: string;
|
|
474
|
+
name: string;
|
|
475
|
+
workspaceBytes: number;
|
|
476
|
+
inboxBytes: number;
|
|
477
|
+
trashBytes: number;
|
|
478
|
+
trashItems: number;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export interface StorageUsage {
|
|
482
|
+
bots: BotStorage[];
|
|
483
|
+
sharedBytes: number;
|
|
484
|
+
/** The shared folder's items in the trash. */
|
|
485
|
+
sharedTrashBytes: number;
|
|
486
|
+
sharedTrashItems: number;
|
|
487
|
+
/** Every item in the trash, of every Boton (deleted ones too) and the shared folder. */
|
|
488
|
+
trashBytes: number;
|
|
489
|
+
/** A folder was too big to count in full: the sizes are at least these. */
|
|
490
|
+
partial: boolean;
|
|
491
|
+
/** Inbox files older than this many days go to the trash each day; null: never. */
|
|
492
|
+
inboxCleanupDays: number | null;
|
|
493
|
+
trashKeepDays: number;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export interface FilePreviewInput {
|
|
497
|
+
path: string;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** An HTML file's sandboxed preview page. `url` holds a key for that one file; null `expiresAt`: no key (auth off). `size`: the file's, when the link was made. */
|
|
501
|
+
export interface FilePreviewLink {
|
|
502
|
+
url: string;
|
|
503
|
+
expiresAt: string | null;
|
|
504
|
+
size: number;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export interface FileListing {
|
|
508
|
+
path: string;
|
|
509
|
+
entries: FileEntry[];
|
|
510
|
+
truncated: boolean;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export interface SendMessageInput {
|
|
514
|
+
text?: string;
|
|
515
|
+
attachments?: { path: string }[];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export interface ShareInput {
|
|
519
|
+
path: string;
|
|
520
|
+
title?: string;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export interface ShareResult {
|
|
524
|
+
message: Message;
|
|
525
|
+
file: FileRef;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** GET /api/bots/:id/background: what the Boton did on its own lately (routine turns and automatic wake-ups). */
|
|
529
|
+
export interface BackgroundActivity {
|
|
530
|
+
/** Routine turns and wake-ups in the last hour. */
|
|
531
|
+
lastHour: number;
|
|
532
|
+
/** How many of those ended [quiet]: nothing in the chat. */
|
|
533
|
+
quietLastHour: number;
|
|
534
|
+
/** The latest one, whenever it was; null if it never had one. */
|
|
535
|
+
lastAt: string | null;
|
|
536
|
+
/** The last day's, newest first (at most 100): what the chat's background line opens. */
|
|
537
|
+
recent: BackgroundItem[];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** One routine turn or automatic wake-up, for the Boton's background activity list. */
|
|
541
|
+
export interface BackgroundItem {
|
|
542
|
+
at: string;
|
|
543
|
+
/** What woke it, as the chat labels it: never an id. */
|
|
544
|
+
label: string;
|
|
545
|
+
/** May name ids. */
|
|
546
|
+
detail: string;
|
|
547
|
+
quiet: boolean;
|
|
548
|
+
/** A quiet turn's note ('' for none), or null when not quiet. */
|
|
549
|
+
note: string | null;
|
|
550
|
+
/** The turn's outcome (ok, error, running, interrupted...), or null when no turn ran yet. */
|
|
551
|
+
outcome: string | null;
|
|
552
|
+
/** The message whose steps show the turn (the wake-up when quiet, else the reply), or null. */
|
|
553
|
+
stepsMessageId: string | null;
|
|
554
|
+
/** How many steps the turn kept (a quiet one's note is one). */
|
|
555
|
+
steps: number;
|
|
556
|
+
/** A routine run's id, for its run steps. */
|
|
557
|
+
runId: string | null;
|
|
558
|
+
routineId: string | null;
|
|
559
|
+
warn: boolean;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export interface Message {
|
|
563
|
+
id: string;
|
|
564
|
+
botId: string;
|
|
565
|
+
role: MessageRole;
|
|
566
|
+
text: string;
|
|
567
|
+
meta: MessageMeta | null;
|
|
568
|
+
createdAt: string;
|
|
569
|
+
/** Emoji reactions on it, oldest first; absent when it has none. */
|
|
570
|
+
reactions?: MessageReaction[];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Who reacted: the owner (from a paired device) or the Boton whose chat it is. */
|
|
574
|
+
export type ReactionActor = 'owner' | 'boton';
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* One emoji on a message. The owner reacts to the Boton's messages, the Boton to the owner's (one emoji at most
|
|
578
|
+
* per message). A reaction is feedback only: it never decides an approval or a card.
|
|
579
|
+
*/
|
|
580
|
+
export interface MessageReaction {
|
|
581
|
+
emoji: string;
|
|
582
|
+
by: ReactionActor;
|
|
583
|
+
at: string;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** POST /api/bots/:id/messages/:messageId/reactions. */
|
|
587
|
+
export interface ReactInput {
|
|
588
|
+
emoji: string;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* GET /api/reactions/export: every reaction, as a file of its own (apart from any backup). POST /api/reactions/import
|
|
593
|
+
* puts one back: all of it or nothing if any entry is wrong. Each message the file names that is here gets exactly
|
|
594
|
+
* the file's reactions; a message that isn't here is skipped. Importing never starts a turn, and the Boton isn't
|
|
595
|
+
* told of imported reactions as new feedback.
|
|
596
|
+
*/
|
|
597
|
+
export interface ReactionTransfer {
|
|
598
|
+
format: 'botons-reactions';
|
|
599
|
+
version: 1;
|
|
600
|
+
/** When it was exported; not read on import. */
|
|
601
|
+
exportedAt?: string;
|
|
602
|
+
reactions: ReactionTransferEntry[];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export interface ReactionTransferEntry {
|
|
606
|
+
messageId: string;
|
|
607
|
+
botId: string;
|
|
608
|
+
by: ReactionActor;
|
|
609
|
+
emoji: string;
|
|
610
|
+
at: string;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export interface ReactionImportResult {
|
|
614
|
+
/** Reactions now on the messages the file named. */
|
|
615
|
+
imported: number;
|
|
616
|
+
/** Message ids in the file that are no message here (or not in the chat the file says). */
|
|
617
|
+
skipped: string[];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export interface Routine {
|
|
621
|
+
id: string;
|
|
622
|
+
botId: string;
|
|
623
|
+
name: string;
|
|
624
|
+
instruction: string;
|
|
625
|
+
/** Normalized ("cron 0 8 * * 1-5", "every 30m", "once <iso>"), or null for webhook/manual only. */
|
|
626
|
+
schedule: string | null;
|
|
627
|
+
scheduleText: string | null;
|
|
628
|
+
timezone: string;
|
|
629
|
+
webhook: RoutineWebhook | null;
|
|
630
|
+
enabled: boolean;
|
|
631
|
+
nextRunAt: string | null;
|
|
632
|
+
lastRunAt: string | null;
|
|
633
|
+
lastStatus: RunStatus | null;
|
|
634
|
+
notify: NotifyPolicy;
|
|
635
|
+
/** Webhook runs get Workspace access (sandboxed): their payload comes from outside. */
|
|
636
|
+
webhookWorkspace: boolean;
|
|
637
|
+
skill: string | null;
|
|
638
|
+
/** null: the Boton's own model. */
|
|
639
|
+
model: string | null;
|
|
640
|
+
/** null: the Boton's own effort. */
|
|
641
|
+
effort: string | null;
|
|
642
|
+
session: RoutineSession;
|
|
643
|
+
/**
|
|
644
|
+
* How many of the chat's latest messages each webhook run is given (0: none, at most WEBHOOK_HISTORY_MAX), in a
|
|
645
|
+
* session that ends with the run: a webhook run never carries on the chat's session, whatever `session` says. Only
|
|
646
|
+
* the owner sets it above 0.
|
|
647
|
+
*/
|
|
648
|
+
webhookHistory: number;
|
|
649
|
+
/**
|
|
650
|
+
* How long one attempt at a run may take, in ms, from the moment its agent process starts (time queued or setting up
|
|
651
|
+
* connectors doesn't count), before it is stopped and the run fails as timed out. null: the server's limit
|
|
652
|
+
* (ServerInfo.turnTimeoutMs), which also caps this one.
|
|
653
|
+
*/
|
|
654
|
+
timeLimitMs: number | null;
|
|
655
|
+
createdAt: string;
|
|
656
|
+
updatedAt: string;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export type WebhookSource = 'generic' | 'github' | 'linear' | 'slack' | 'sentry';
|
|
660
|
+
|
|
661
|
+
export interface WebhookFilter {
|
|
662
|
+
kind: 'header' | 'field';
|
|
663
|
+
name: string;
|
|
664
|
+
values: string[];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export interface RoutineWebhook {
|
|
668
|
+
url: string;
|
|
669
|
+
/** "" when read with the token of a sandboxed webhook run. */
|
|
670
|
+
key: string;
|
|
671
|
+
source: WebhookSource;
|
|
672
|
+
/** The secret itself is write-only. */
|
|
673
|
+
hasSecret: boolean;
|
|
674
|
+
filter: WebhookFilter | null;
|
|
675
|
+
callbackUrl: string | null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export type RoutineSession = 'chat' | 'own';
|
|
679
|
+
|
|
680
|
+
export type NotifyPolicy = 'auto' | 'always' | 'failures' | 'never';
|
|
681
|
+
export type NotifyPriority = 'default' | 'high';
|
|
682
|
+
|
|
683
|
+
export type RunTrigger = 'schedule' | 'webhook' | 'manual' | 'test';
|
|
684
|
+
export type RunStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'skipped';
|
|
685
|
+
|
|
686
|
+
export interface RoutineRun {
|
|
687
|
+
id: string;
|
|
688
|
+
routineId: string;
|
|
689
|
+
botId: string;
|
|
690
|
+
trigger: RunTrigger;
|
|
691
|
+
status: RunStatus;
|
|
692
|
+
error: string | null;
|
|
693
|
+
payloadPreview: string | null;
|
|
694
|
+
payloadBytes: number;
|
|
695
|
+
headers: Record<string, string> | null;
|
|
696
|
+
reply: string | null;
|
|
697
|
+
quiet: boolean;
|
|
698
|
+
meta: RunMeta | null;
|
|
699
|
+
startedAt: string;
|
|
700
|
+
finishedAt: string | null;
|
|
701
|
+
attempts: number;
|
|
702
|
+
lastError: string | null;
|
|
703
|
+
retryAt: string | null;
|
|
704
|
+
usage: TokenUsage | null;
|
|
705
|
+
model: string | null;
|
|
706
|
+
steps: number;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** A run the owner may want to act on, across every Boton and routine. */
|
|
710
|
+
export interface AttentionRun extends RoutineRun {
|
|
711
|
+
botName: string;
|
|
712
|
+
routineName: string;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** GET /api/runs/attention: failed runs nobody retried yet, and runs waiting to try again by themselves. */
|
|
716
|
+
export interface RunsAttention {
|
|
717
|
+
/** Failed runs that finished since then are listed; older ones are left to each routine's run history. */
|
|
718
|
+
since: string;
|
|
719
|
+
/** Newest first. Not test runs (the owner watched those), runs already retried, nor runs the owner cancelled. */
|
|
720
|
+
failed: AttentionRun[];
|
|
721
|
+
/** Queued runs held after a network error or for a usage limit, soonest first. */
|
|
722
|
+
held: AttentionRun[];
|
|
723
|
+
/** Finished since then, their result not delivered to the routine's callback URL after every retry; newest first. */
|
|
724
|
+
callbacks: AttentionRun[];
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export interface TokenUsage {
|
|
728
|
+
/** Excludes cache reads and writes. */
|
|
729
|
+
inputTokens: number;
|
|
730
|
+
outputTokens: number;
|
|
731
|
+
cacheReadTokens: number;
|
|
732
|
+
cacheWriteTokens: number;
|
|
733
|
+
costUsd?: number;
|
|
734
|
+
/**
|
|
735
|
+
* An API brain's turns: how `costUsd` was found. `estimate`: Botons' own sum of the tokens the API reported times
|
|
736
|
+
* the rates in server/src/rates.ts, which is not the provider's bill. Absent for a CLI's own figure.
|
|
737
|
+
*/
|
|
738
|
+
costBasis?: 'estimate';
|
|
739
|
+
/**
|
|
740
|
+
* Part of `costUsd` that is a hold, not a count: a request whose answer never fully came (the connection dropped
|
|
741
|
+
* after it was sent) is counted at the most it could have cost, since the provider may have billed it.
|
|
742
|
+
*/
|
|
743
|
+
costHeldUsd?: number;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export type TurnOutcome = 'running' | 'ok' | 'error' | 'timeout' | 'stopped' | 'interrupted';
|
|
747
|
+
|
|
748
|
+
export type TurnTrigger = 'chat' | RunTrigger;
|
|
749
|
+
|
|
750
|
+
export interface UsageReport {
|
|
751
|
+
since: string;
|
|
752
|
+
until: string;
|
|
753
|
+
group: 'bot' | 'routine';
|
|
754
|
+
rows: UsageRow[];
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export interface UsageRow extends TokenUsage {
|
|
758
|
+
botId: string;
|
|
759
|
+
botName: string;
|
|
760
|
+
routineId?: string | null;
|
|
761
|
+
routineName?: string | null;
|
|
762
|
+
turns: number;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export interface RunPayload {
|
|
766
|
+
runId: string;
|
|
767
|
+
payload: string | null;
|
|
768
|
+
headers: Record<string, string> | null;
|
|
769
|
+
bytes: number;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export interface RunMeta {
|
|
773
|
+
notifications?: NotifyDelivery[];
|
|
774
|
+
callback?: CallbackDelivery;
|
|
775
|
+
/** This run retries a failed one, with its trigger, payload and headers. */
|
|
776
|
+
retryOf?: { runId: string; error: string | null };
|
|
777
|
+
/** The run the owner started to retry this failed one. */
|
|
778
|
+
retriedAs?: string;
|
|
779
|
+
/**
|
|
780
|
+
* The owner ended this run (Cancel, Stop or a pause), so it is failed but needs nothing more: it is not retried by
|
|
781
|
+
* itself, pushes nothing and is not listed to check. `started`: an attempt at it ran (this one, or an earlier one that
|
|
782
|
+
* failed and was held to try again), so it may have partly run.
|
|
783
|
+
*/
|
|
784
|
+
cancelled?: { at: string; started: boolean };
|
|
785
|
+
/**
|
|
786
|
+
* Stopped because it ran past its time limit (`limitMs`) before anything else ended it: failed, not retried by itself,
|
|
787
|
+
* and (unlike a cancelled run) listed for the owner to check, since it may have partly run.
|
|
788
|
+
*/
|
|
789
|
+
timedOut?: { at: string; limitMs: number };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export interface CallbackDelivery {
|
|
793
|
+
url: string;
|
|
794
|
+
ok: boolean;
|
|
795
|
+
status: number | null;
|
|
796
|
+
error: string | null;
|
|
797
|
+
attempts: number;
|
|
798
|
+
at: string;
|
|
799
|
+
/** Being sent (ok is false until it is). One still pending at a start was cut off, and is sent again. */
|
|
800
|
+
pending?: boolean;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export interface RunCallback {
|
|
804
|
+
runId: string;
|
|
805
|
+
routineId: string;
|
|
806
|
+
botId: string;
|
|
807
|
+
trigger: RunTrigger;
|
|
808
|
+
status: RunStatus;
|
|
809
|
+
quiet: boolean;
|
|
810
|
+
reply: string | null;
|
|
811
|
+
error: string | null;
|
|
812
|
+
startedAt: string;
|
|
813
|
+
finishedAt: string | null;
|
|
814
|
+
attempts: number;
|
|
815
|
+
/** Failed because the owner ended it (see RunMeta.cancelled). */
|
|
816
|
+
cancelled: boolean;
|
|
817
|
+
/** Failed because it ran past its time limit (see RunMeta.timedOut). */
|
|
818
|
+
timedOut: boolean;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export interface WebhookRunStatus {
|
|
822
|
+
ok: boolean;
|
|
823
|
+
runId: string;
|
|
824
|
+
status: RunStatus;
|
|
825
|
+
trigger: RunTrigger;
|
|
826
|
+
reply: string | null;
|
|
827
|
+
error: string | null;
|
|
828
|
+
startedAt: string;
|
|
829
|
+
finishedAt: string | null;
|
|
830
|
+
attempts: number;
|
|
831
|
+
/** The run that retries this one, once it failed and the owner retried it. */
|
|
832
|
+
retriedAs: string | null;
|
|
833
|
+
/** Failed because the owner ended it (see RunMeta.cancelled). */
|
|
834
|
+
cancelled: boolean;
|
|
835
|
+
/** Failed because it ran past its time limit (see RunMeta.timedOut). */
|
|
836
|
+
timedOut: boolean;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export interface RunRoutineInput {
|
|
840
|
+
payload?: unknown;
|
|
841
|
+
test?: boolean;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export interface NotifyDelivery {
|
|
845
|
+
channel: 'ntfy';
|
|
846
|
+
reason: 'failure' | 'result' | 'notify' | 'approval' | 'test';
|
|
847
|
+
ok: boolean;
|
|
848
|
+
status: number | null;
|
|
849
|
+
error: string | null;
|
|
850
|
+
at: string;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export interface Memory {
|
|
854
|
+
id: string;
|
|
855
|
+
botId: string;
|
|
856
|
+
text: string;
|
|
857
|
+
createdAt: string;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export interface ProviderModel {
|
|
861
|
+
id: string;
|
|
862
|
+
label: string;
|
|
863
|
+
/** The effort levels the provider lists for this model; absent when it doesn't say. */
|
|
864
|
+
efforts?: string[];
|
|
865
|
+
/** The level it runs at when none is set, when the provider says. */
|
|
866
|
+
defaultEffort?: string;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Whether a provider can be told how hard to think, and how. */
|
|
870
|
+
export interface ProviderEffortInfo {
|
|
871
|
+
/** Weakest first; empty when the provider has no effort setting. */
|
|
872
|
+
levels: ProviderModel[];
|
|
873
|
+
/** Why the control is off, when `levels` is empty. */
|
|
874
|
+
reason: string | null;
|
|
875
|
+
/** How it reaches the CLI, e.g. "Claude Code’s --effort". */
|
|
876
|
+
via: string | null;
|
|
877
|
+
/** What runs when no level is set. */
|
|
878
|
+
defaultText: string;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* How ready a brain is to run turns, from the server's own check of its CLI and the usage limits its turns hit.
|
|
883
|
+
* `ready` only when a recent check found it installed and confirmed it signed in; anything the check could not tell
|
|
884
|
+
* (not run yet, too old, no answer, a sign-in status it could not read) is `unknown`.
|
|
885
|
+
*/
|
|
886
|
+
export type BrainState = 'ready' | 'unknown' | 'not-installed' | 'signed-out' | 'unavailable' | 'usage-limited';
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Why a brain is not ready, as a fixed word (server/src/providers/readiness.ts): never the CLI's output, a path or a
|
|
890
|
+
* token. `not-found`: no such command for the server. `not-runnable`: there, but it could not be started.
|
|
891
|
+
* `incompatible-version`: the installed CLI cannot safely run Botons turns.
|
|
892
|
+
* `version-failed`: `--version` exited with an error. `login-unreadable`: installed, but its sign-in status could not be
|
|
893
|
+
* read. `outdated`: the last check is too old to go by. `clock-changed`: the last check is stamped later than the
|
|
894
|
+
* server's clock now (it was set back), so how old it is can't be told.
|
|
895
|
+
*/
|
|
896
|
+
export type BrainCheckProblem =
|
|
897
|
+
| 'not-checked'
|
|
898
|
+
| 'outdated'
|
|
899
|
+
| 'clock-changed'
|
|
900
|
+
| 'timed-out'
|
|
901
|
+
| 'not-found'
|
|
902
|
+
| 'not-runnable'
|
|
903
|
+
| 'incompatible-version'
|
|
904
|
+
| 'version-failed'
|
|
905
|
+
| 'login-unreadable'
|
|
906
|
+
| 'signed-out'
|
|
907
|
+
| 'usage-limit'
|
|
908
|
+
| 'unavailable';
|
|
909
|
+
|
|
910
|
+
export interface BrainReadiness {
|
|
911
|
+
state: BrainState;
|
|
912
|
+
problem: BrainCheckProblem | null;
|
|
913
|
+
/** When the check this comes from ran; null before any. */
|
|
914
|
+
checkedAt: string | null;
|
|
915
|
+
/** Its turns hit a usage limit: held until then. */
|
|
916
|
+
usageLimitUntil: string | null;
|
|
917
|
+
/**
|
|
918
|
+
* Which run of the server answered: a new random id each time it starts, never a clock. Two answers with different
|
|
919
|
+
* boots can't be ordered by `generation`; the later request's is the newer.
|
|
920
|
+
*/
|
|
921
|
+
boot: string;
|
|
922
|
+
/**
|
|
923
|
+
* Higher for each check of this brain the server finishes in this boot: of two answers from the same boot, the
|
|
924
|
+
* higher one is from the newer check. 0 for a brain the server doesn't check.
|
|
925
|
+
*/
|
|
926
|
+
generation: number;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export interface ProviderInfo {
|
|
930
|
+
/** Offline Workspace helper preflight; independent of sign-in and Full computer access. */
|
|
931
|
+
workspaceProblem?: string | null;
|
|
932
|
+
id: string;
|
|
933
|
+
label: string;
|
|
934
|
+
kind: 'cli' | 'api';
|
|
935
|
+
available: boolean;
|
|
936
|
+
reason: string | null;
|
|
937
|
+
installed: boolean;
|
|
938
|
+
version: string | null;
|
|
939
|
+
/** A CLI brain's copy: Botons' own or the system's, and the version of another copy a terminal here runs. */
|
|
940
|
+
copy?: CliCopy;
|
|
941
|
+
loggedIn: boolean | null;
|
|
942
|
+
checkedAt: string | null;
|
|
943
|
+
lastSuccessAt: string | null;
|
|
944
|
+
lastError: { at: string; message: string } | null;
|
|
945
|
+
models: ProviderModel[];
|
|
946
|
+
defaultModel: string;
|
|
947
|
+
effort: ProviderEffortInfo;
|
|
948
|
+
askFirst?: boolean;
|
|
949
|
+
usageLimit?: { until: string; reason: string } | null;
|
|
950
|
+
readiness: BrainReadiness;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ------------------------------------------------------------------ API keys
|
|
954
|
+
|
|
955
|
+
/** The model APIs whose keys the owner can give Botons. */
|
|
956
|
+
export type ApiKeyProvider = 'anthropic' | 'openai';
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Where the key Botons would use comes from: one saved in Settings (kept in the database, like OAuth tokens), else
|
|
960
|
+
* the variable in the server's environment, else none. A saved key always wins over the environment.
|
|
961
|
+
*/
|
|
962
|
+
export type ApiKeySource = 'saved' | 'environment' | 'none';
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* How a test went, in fixed words; never the provider's own error text. `invalid`: the provider refused the key
|
|
966
|
+
* (401). `forbidden`: the key is known but may not list models (403). `rate_limited`: 429. `unavailable`: the
|
|
967
|
+
* provider answered 5xx or said it was overloaded. `unreachable`: no answer (network, DNS, TLS, time limit).
|
|
968
|
+
* `unexpected`: another status, a redirect, or an answer Botons couldn't read.
|
|
969
|
+
*/
|
|
970
|
+
export type ApiKeyTestOutcome = 'ok' | 'invalid' | 'forbidden' | 'rate_limited' | 'unavailable' | 'unreachable' | 'unexpected';
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* What the provider's model list says about one capability: `yes` or `no` only when the list itself has a field for
|
|
974
|
+
* it; otherwise `unknown`. Botons never infers a capability from a model's or provider's name.
|
|
975
|
+
*/
|
|
976
|
+
export type CapabilityEvidence = 'yes' | 'no' | 'unknown';
|
|
977
|
+
|
|
978
|
+
export interface ApiModelCapabilities {
|
|
979
|
+
chat: CapabilityEvidence;
|
|
980
|
+
tools: CapabilityEvidence;
|
|
981
|
+
vision: CapabilityEvidence;
|
|
982
|
+
realtimeVoice: CapabilityEvidence;
|
|
983
|
+
speechToText: CapabilityEvidence;
|
|
984
|
+
textToSpeech: CapabilityEvidence;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** One model a key can use, as the provider's model list gave it, kept only in its expected shape. */
|
|
988
|
+
export interface ApiModel {
|
|
989
|
+
id: string;
|
|
990
|
+
/** Only when it is plain text of the expected shape; else null. */
|
|
991
|
+
displayName: string | null;
|
|
992
|
+
/** Anthropic: active, deprecated or retired; null when the list doesn't say. */
|
|
993
|
+
lifecycle: 'active' | 'deprecated' | 'retired' | null;
|
|
994
|
+
capabilities: ApiModelCapabilities;
|
|
995
|
+
/** The effort levels the list says it takes; null when the list doesn't say. */
|
|
996
|
+
effort: string[] | null;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** One test of a key: listing its models, read-only, with no prompt sent and nothing generated. */
|
|
1000
|
+
export interface ApiKeyTest {
|
|
1001
|
+
provider: ApiKeyProvider;
|
|
1002
|
+
/** The key this test was of: the saved one at `generation`, or the environment's. */
|
|
1003
|
+
source: Exclude<ApiKeySource, 'none'>;
|
|
1004
|
+
generation: number;
|
|
1005
|
+
outcome: ApiKeyTestOutcome;
|
|
1006
|
+
/** The provider's HTTP status, when it answered. */
|
|
1007
|
+
httpStatus: number | null;
|
|
1008
|
+
checkedAt: string;
|
|
1009
|
+
durationMs: number;
|
|
1010
|
+
/** With `ok`: the models in their expected shape, newest first as the provider lists them. */
|
|
1011
|
+
models: ApiModel[];
|
|
1012
|
+
/** Models the list held that were left out: an id not of the expected shape, or past the limit. */
|
|
1013
|
+
omitted: number;
|
|
1014
|
+
/** The list had more pages than Botons reads. */
|
|
1015
|
+
truncated: boolean;
|
|
1016
|
+
/** False when the key changed while the test ran: its result says nothing about the key there is now. */
|
|
1017
|
+
current: boolean;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
export interface ApiKeyState {
|
|
1021
|
+
provider: ApiKeyProvider;
|
|
1022
|
+
label: string;
|
|
1023
|
+
/** The server environment variable used when no key is saved. */
|
|
1024
|
+
envVar: string;
|
|
1025
|
+
source: ApiKeySource;
|
|
1026
|
+
saved: boolean;
|
|
1027
|
+
savedAt: string | null;
|
|
1028
|
+
/** The variable is set in the server's environment (read at start). */
|
|
1029
|
+
environment: boolean;
|
|
1030
|
+
/** Bumped by every save and removal of the saved key; send it back with the next change. */
|
|
1031
|
+
generation: number;
|
|
1032
|
+
/** The newest test of the key there is now; null if it hasn't been tested. */
|
|
1033
|
+
test: ApiKeyTest | null;
|
|
1034
|
+
testing: boolean;
|
|
1035
|
+
/** What a test sends, in the owner's words. */
|
|
1036
|
+
testDescription: string;
|
|
1037
|
+
/** Whether Botons can run on this key now (the provider id they pick), or why not. */
|
|
1038
|
+
brain: { available: boolean; provider: string; reason: string | null };
|
|
1039
|
+
/**
|
|
1040
|
+
* What this key's own turns showed, per model, since the key was saved (or the server started, for the
|
|
1041
|
+
* environment's): evidence from real use, never from a model's name. Paid probes are never sent to find out.
|
|
1042
|
+
*/
|
|
1043
|
+
observed: ApiModelObservation[];
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/** When a turn on this key first showed a model doing each thing; absent means never seen, which is not "no". */
|
|
1047
|
+
export interface ApiModelObservation {
|
|
1048
|
+
model: string;
|
|
1049
|
+
/** A turn finished with a reply. */
|
|
1050
|
+
chat?: string;
|
|
1051
|
+
/** A turn's tool call was answered and the model carried on. */
|
|
1052
|
+
tools?: string;
|
|
1053
|
+
/** A turn with an image in it finished. */
|
|
1054
|
+
vision?: string;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** Spending caps on a Boton's API-key turns, in US dollars of Botons' own estimate (see TokenUsage.costBasis). */
|
|
1058
|
+
export interface SpendingView {
|
|
1059
|
+
botId: string;
|
|
1060
|
+
/** At most this much a day (from midnight in the Boton's time zone); null for no cap. */
|
|
1061
|
+
dailyCapUsd: number | null;
|
|
1062
|
+
/** Today's estimate so far, holds and running requests' reservations included. */
|
|
1063
|
+
spentTodayUsd: number;
|
|
1064
|
+
/** Today's API-key turns with no known price (a model without a rate): with a cap set, turns are refused. */
|
|
1065
|
+
unpricedToday: number;
|
|
1066
|
+
dayStartedAt: string;
|
|
1067
|
+
routines: { id: string; name: string; perRunCapUsd: number | null }[];
|
|
1068
|
+
/** How the caps are kept, in the owner's words. */
|
|
1069
|
+
rules: string;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export interface SpendingInput {
|
|
1073
|
+
dailyCapUsd?: number | null;
|
|
1074
|
+
/** By routine id; null removes its cap. */
|
|
1075
|
+
routines?: Record<string, number | null>;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export interface ApiKeysView {
|
|
1079
|
+
providers: ApiKeyState[];
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** Save (or replace) a provider's key. `generation` is the one the page last saw: a key changed since is not overwritten. */
|
|
1083
|
+
export interface SaveApiKeyInput {
|
|
1084
|
+
key: string;
|
|
1085
|
+
generation: number;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
export interface RemoveApiKeyInput {
|
|
1089
|
+
generation: number;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
export type HealthCheck = 'database' | 'scheduler' | 'providers' | 'draining';
|
|
1093
|
+
|
|
1094
|
+
export interface Health {
|
|
1095
|
+
ok: boolean;
|
|
1096
|
+
build: string | null;
|
|
1097
|
+
failing: HealthCheck[];
|
|
1098
|
+
/** Only for paired devices. */
|
|
1099
|
+
details?: HealthDetails;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
export interface HealthDetails {
|
|
1103
|
+
database: { ok: boolean; error: string | null };
|
|
1104
|
+
scheduler: { ok: boolean; lastTickAt: string | null; intervalMs: number };
|
|
1105
|
+
providers: ProviderInfo[];
|
|
1106
|
+
draining: boolean;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** One backup folder in BOTONS_HOME/backups, as its manifest describes it. */
|
|
1110
|
+
export interface BackupSummary {
|
|
1111
|
+
/** The folder's name: when the backup was made, in UTC. */
|
|
1112
|
+
name: string;
|
|
1113
|
+
createdAt: string;
|
|
1114
|
+
/** Null when the manifest is missing or unreadable. */
|
|
1115
|
+
schemaVersion: number | null;
|
|
1116
|
+
/** workspaces/, shared/ and templates/ were archived with the database. */
|
|
1117
|
+
workspaces: boolean;
|
|
1118
|
+
/** The archive could not be made, so only the database was kept (npm run backup prints why). */
|
|
1119
|
+
workspacesFailed: boolean;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/** What the paired owner sees of backups. No paths, file contents or error text. */
|
|
1123
|
+
export interface BackupStatus {
|
|
1124
|
+
/** Newest first, at most BACKUPS_LISTED (server/src/backup.ts). */
|
|
1125
|
+
backups: BackupSummary[];
|
|
1126
|
+
/** All complete backups in the folder. */
|
|
1127
|
+
total: number;
|
|
1128
|
+
/** Backups the timer keeps; older ones are deleted. */
|
|
1129
|
+
keep: number;
|
|
1130
|
+
/** The newest is older than this, or there is none while the database exists. */
|
|
1131
|
+
staleAfterHours: number;
|
|
1132
|
+
stale: boolean;
|
|
1133
|
+
/** Backups that stopped part-way (`<time>.partial`), deleted after a day. */
|
|
1134
|
+
partial: number;
|
|
1135
|
+
/** The backups folder could not be listed (not a folder, or no permission); the doctor on the server says more. */
|
|
1136
|
+
unreadable: boolean;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/** Why a push did not go through, as a fixed word (server/src/notify.ts): never the error text. */
|
|
1140
|
+
export type PushProblem = 'refused' | 'rejected' | 'redirected' | 'server-error' | 'unreachable' | 'timeout';
|
|
1141
|
+
|
|
1142
|
+
/** How pushes have gone since the server started. */
|
|
1143
|
+
export interface PushStatus {
|
|
1144
|
+
/** `unset`: no ntfy server or topic is saved. `off`: saved, but push is turned off. */
|
|
1145
|
+
state: 'unset' | 'off' | 'on';
|
|
1146
|
+
/** The newest attempt, a test or a real push; null when none since `since`. */
|
|
1147
|
+
last: { at: string; ok: boolean; reason: NotifyDelivery['reason']; status: number | null; problem: PushProblem | null } | null;
|
|
1148
|
+
lastSuccessAt: string | null;
|
|
1149
|
+
/** Attempts that failed since the last one that went through. */
|
|
1150
|
+
failuresInARow: number;
|
|
1151
|
+
/** When counting started: the server's start, or the last change to the ntfy server, topic or token. */
|
|
1152
|
+
since: string;
|
|
1153
|
+
/** Counting restarted because the ntfy server, topic or token changed: older attempts said nothing about these. */
|
|
1154
|
+
sinceSettingsChange: boolean;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** Room left where the data lives (BOTONS_HOME). Sizes in bytes; no paths. */
|
|
1158
|
+
export interface StorageStatus {
|
|
1159
|
+
/** The free space could not be read; the other fields are then 0. */
|
|
1160
|
+
unreadable: boolean;
|
|
1161
|
+
freeBytes: number;
|
|
1162
|
+
totalBytes: number;
|
|
1163
|
+
/** The database with its write-ahead log. */
|
|
1164
|
+
databaseBytes: number;
|
|
1165
|
+
/** Free space is below `lowBelowBytes`, or below 5% of the disk. */
|
|
1166
|
+
low: boolean;
|
|
1167
|
+
lowBelowBytes: number;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/** Why the database check failed, as a fixed word (server/src/health.ts): never the error text. */
|
|
1171
|
+
export type DatabaseProblem = 'busy' | 'corrupt' | 'read-only' | 'disk-full' | 'cannot-open' | 'io-error' | 'other';
|
|
1172
|
+
/** Why a brain is not ready, as a fixed word: never the CLI's output. */
|
|
1173
|
+
export type BrainProblem = 'not-installed' | 'signed-out' | 'unavailable';
|
|
1174
|
+
|
|
1175
|
+
/** One brain as Settings → Health shows it: states and times, a version only in a known shape, no CLI output. */
|
|
1176
|
+
export interface OperationsBrain {
|
|
1177
|
+
id: string;
|
|
1178
|
+
label: string;
|
|
1179
|
+
available: boolean;
|
|
1180
|
+
installed: boolean;
|
|
1181
|
+
loggedIn: boolean | null;
|
|
1182
|
+
/** Only a version number found in `--version`'s output ("2.1.3"), else null. */
|
|
1183
|
+
version: string | null;
|
|
1184
|
+
problem: BrainProblem | null;
|
|
1185
|
+
lastSuccessAt: string | null;
|
|
1186
|
+
/** When its last turn failed, without the error. */
|
|
1187
|
+
lastErrorAt: string | null;
|
|
1188
|
+
/** It hit a usage limit and waits until then. */
|
|
1189
|
+
usageLimitUntil: string | null;
|
|
1190
|
+
readiness: BrainReadiness;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/** The health check as the owner's view carries it: GET /api/health's checks with fixed words in place of error text. */
|
|
1194
|
+
export interface OperationsHealth {
|
|
1195
|
+
ok: boolean;
|
|
1196
|
+
/** Only a commit sha (hex), else null. */
|
|
1197
|
+
build: string | null;
|
|
1198
|
+
failing: HealthCheck[];
|
|
1199
|
+
details: {
|
|
1200
|
+
database: { ok: boolean; problem: DatabaseProblem | null };
|
|
1201
|
+
scheduler: HealthDetails['scheduler'];
|
|
1202
|
+
providers: OperationsBrain[];
|
|
1203
|
+
draining: boolean;
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Why a restore rehearsal failed, as a fixed word (server/src/rehearsal.ts): never the error text.
|
|
1209
|
+
* `interrupted`: the server stopped or restarted while it ran. `failed`: something the rehearsal did not expect.
|
|
1210
|
+
*/
|
|
1211
|
+
export type RehearsalProblem =
|
|
1212
|
+
| 'not-found'
|
|
1213
|
+
| 'not-a-backup'
|
|
1214
|
+
| 'database-too-large'
|
|
1215
|
+
| 'no-space'
|
|
1216
|
+
| 'damaged-database'
|
|
1217
|
+
| 'newer-schema'
|
|
1218
|
+
| 'older-schema'
|
|
1219
|
+
| 'upgrade-failed'
|
|
1220
|
+
| 'damaged-archive'
|
|
1221
|
+
| 'unsafe-archive'
|
|
1222
|
+
| 'archive-too-large'
|
|
1223
|
+
| 'timed-out'
|
|
1224
|
+
| 'interrupted'
|
|
1225
|
+
| 'failed';
|
|
1226
|
+
|
|
1227
|
+
/** `checked`: read through and every member checked. `none`: the backup holds the database only. `not-checked`: it stopped first. */
|
|
1228
|
+
export type RehearsalWorkspaces = 'checked' | 'none' | 'not-checked';
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* The last restore rehearsal: a backup copied to a private temporary folder, checked, upgraded there to this server's
|
|
1232
|
+
* schema, its workspaces archive read through, then deleted. Nothing in the data folder changes. Only times, fixed
|
|
1233
|
+
* words, counts and versions: no paths, file names or error text.
|
|
1234
|
+
*/
|
|
1235
|
+
export interface RestoreRehearsal {
|
|
1236
|
+
/** The backup folder's name (its UTC time). */
|
|
1237
|
+
backup: string;
|
|
1238
|
+
startedAt: string;
|
|
1239
|
+
/** Null while it runs. */
|
|
1240
|
+
finishedAt: string | null;
|
|
1241
|
+
outcome: 'running' | 'passed' | 'failed';
|
|
1242
|
+
problem: RehearsalProblem | null;
|
|
1243
|
+
/** The schema the backup's database is at, read from the copy; null when it could not be read. */
|
|
1244
|
+
fromSchema: number | null;
|
|
1245
|
+
/** The schema the copy was upgraded to (or already at): this server's when it passed. */
|
|
1246
|
+
toSchema: number | null;
|
|
1247
|
+
/** Whether each table holds as many rows as the backup's manifest says; null when it says nothing. */
|
|
1248
|
+
matchesManifest: boolean | null;
|
|
1249
|
+
workspaces: RehearsalWorkspaces;
|
|
1250
|
+
/** What the workspaces archive holds; null unless it was read through. */
|
|
1251
|
+
files: number | null;
|
|
1252
|
+
folders: number | null;
|
|
1253
|
+
bytes: number | null;
|
|
1254
|
+
/** The server that ran it: a release version and build only in their expected shapes, else null. */
|
|
1255
|
+
ranBy: { version: string | null; build: string | null; schema: number };
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/** POST /api/backups/rehearse: a backup folder's name, or "latest" (the default). */
|
|
1259
|
+
export interface RehearseRestoreInput {
|
|
1260
|
+
backup?: string;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
export interface RehearsalStatus {
|
|
1264
|
+
/** The newest rehearsal, running or finished; null when none has run on this data folder. */
|
|
1265
|
+
last: RestoreRehearsal | null;
|
|
1266
|
+
/** Rehearsals allowed per hour. */
|
|
1267
|
+
limitPerHour: number;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/** GET /api/operations: the owner's read-only view of whether the server works and is backed up. No error text, paths or addresses. */
|
|
1271
|
+
export interface OperationsStatus {
|
|
1272
|
+
health: OperationsHealth;
|
|
1273
|
+
/** The database schema this server runs (SCHEMA_VERSION). */
|
|
1274
|
+
schema: number;
|
|
1275
|
+
backups: BackupStatus;
|
|
1276
|
+
push: PushStatus;
|
|
1277
|
+
storage: StorageStatus;
|
|
1278
|
+
/** Which commands the recovery steps show: the Docker image's, or a checkout's. */
|
|
1279
|
+
install: import('./install-commands.ts').InstallType;
|
|
1280
|
+
/** The backup schemas this server's restore takes, oldest first: its own, and the one before (upgraded at the next start). */
|
|
1281
|
+
restorableSchemas: number[];
|
|
1282
|
+
rehearsal: RehearsalStatus;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
export type RecentLogLevel = 'info' | 'warn' | 'error';
|
|
1286
|
+
/** Only ids of the server's own shape, enum words, booleans and bounded whole numbers: never text from a log line. */
|
|
1287
|
+
export type RecentLogField = string | number | boolean;
|
|
1288
|
+
/** What the owner can do about an event, for events with a known next step. */
|
|
1289
|
+
export type RecentLogHelp = 'reconnect' | 'connector' | 'push' | 'tailscale' | 'usage-limit' | 'brain' | 'run' | 'bug' | 'auth-off';
|
|
1290
|
+
|
|
1291
|
+
/** One server event as Settings keeps it (server/src/recent-logs.ts): selected from a log line, never copied from it. */
|
|
1292
|
+
export interface RecentLogEntry {
|
|
1293
|
+
/** Counts up from the server's start; a restart starts again at 1. */
|
|
1294
|
+
seq: number;
|
|
1295
|
+
ts: string;
|
|
1296
|
+
level: RecentLogLevel;
|
|
1297
|
+
/** A fixed code for a known log message, or `unclassified` for any other. */
|
|
1298
|
+
event: string;
|
|
1299
|
+
/** Written in the server's source for the event, never taken from the line. */
|
|
1300
|
+
label: string;
|
|
1301
|
+
help: RecentLogHelp | null;
|
|
1302
|
+
/** The allowlisted fields whose values passed their checks; `errorCategory` stands for any thrown error. */
|
|
1303
|
+
fields: Record<string, RecentLogField>;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
export interface RecentLogs {
|
|
1307
|
+
entries: RecentLogEntry[];
|
|
1308
|
+
/** Older lines match the filters too: ask again with `before` the last entry's seq. */
|
|
1309
|
+
more: boolean;
|
|
1310
|
+
/** When this server process started keeping lines: a restart empties the view. */
|
|
1311
|
+
since: string;
|
|
1312
|
+
/** The oldest line still kept, or null when none is. */
|
|
1313
|
+
oldestAt: string | null;
|
|
1314
|
+
/** Lines let go since `since` to stay within the limits. */
|
|
1315
|
+
dropped: number;
|
|
1316
|
+
kept: number;
|
|
1317
|
+
/** Of the kept lines, whatever the filters. */
|
|
1318
|
+
counts: Record<RecentLogLevel, number>;
|
|
1319
|
+
bytes: number;
|
|
1320
|
+
limits: { entries: number; bytes: number; page: number };
|
|
1321
|
+
/** BOTONS_LOG_LEVEL: less severe lines are neither written nor kept. */
|
|
1322
|
+
threshold: RecentLogLevel;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
export interface Settings {
|
|
1326
|
+
ownerName: string;
|
|
1327
|
+
timezone: string;
|
|
1328
|
+
primaryBotId: string | null;
|
|
1329
|
+
ntfy: NtfySettings;
|
|
1330
|
+
paused: boolean;
|
|
1331
|
+
/**
|
|
1332
|
+
* Reactions of the owner's on a Boton's message that start a short follow-up turn (each at most once per message);
|
|
1333
|
+
* any other reaction only reaches the Boton as feedback on its next turn. Default: 👎 alone.
|
|
1334
|
+
*/
|
|
1335
|
+
reactionFollowUps: string[];
|
|
1336
|
+
/** Each day, inbox files untouched this many days go to the trash (kept ones stay); null: off. Default 30. */
|
|
1337
|
+
inboxCleanupDays: number | null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
export interface NtfySettings {
|
|
1341
|
+
enabled: boolean;
|
|
1342
|
+
server: string;
|
|
1343
|
+
topic: string;
|
|
1344
|
+
/** The token itself is write-only. */
|
|
1345
|
+
hasToken: boolean;
|
|
1346
|
+
preview: boolean;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
export interface UpdateSettingsInput {
|
|
1350
|
+
ownerName?: string;
|
|
1351
|
+
timezone?: string;
|
|
1352
|
+
primaryBotId?: string | null;
|
|
1353
|
+
ntfy?: {
|
|
1354
|
+
enabled?: boolean;
|
|
1355
|
+
server?: string;
|
|
1356
|
+
topic?: string;
|
|
1357
|
+
/** "" or null removes the saved one. */
|
|
1358
|
+
token?: string | null;
|
|
1359
|
+
preview?: boolean;
|
|
1360
|
+
};
|
|
1361
|
+
/** Emoji, at most REACTION_FOLLOW_UP_MAX; [] for none. */
|
|
1362
|
+
reactionFollowUps?: string[];
|
|
1363
|
+
/** One of INBOX_CLEANUP_CHOICES, or null for off. */
|
|
1364
|
+
inboxCleanupDays?: number | null;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
export interface NotifyInput {
|
|
1368
|
+
text: string;
|
|
1369
|
+
priority?: NotifyPriority;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export interface NotifyResult {
|
|
1373
|
+
message: Message;
|
|
1374
|
+
deliveries: NotifyDelivery[];
|
|
1375
|
+
skipped: string | null;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
export interface TeamMember {
|
|
1379
|
+
id: string;
|
|
1380
|
+
name: string;
|
|
1381
|
+
title: string;
|
|
1382
|
+
state: BotStatus['state'];
|
|
1383
|
+
paused: boolean;
|
|
1384
|
+
primary: boolean;
|
|
1385
|
+
self: boolean;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
export interface AskInput {
|
|
1389
|
+
text: string;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
export interface AskResult {
|
|
1393
|
+
message: Message;
|
|
1394
|
+
threadId: string;
|
|
1395
|
+
hop: number;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
export type ApprovalStatus = 'pending' | 'allowed' | 'denied' | 'expired' | 'cancelled';
|
|
1399
|
+
|
|
1400
|
+
export type ApprovalKind = 'action' | 'skill' | 'connector';
|
|
1401
|
+
|
|
1402
|
+
/** The turn that asked, as the server knew it (never from what the Boton says). */
|
|
1403
|
+
export interface ApprovalOrigin {
|
|
1404
|
+
trigger: TurnTrigger;
|
|
1405
|
+
turnId: string | null;
|
|
1406
|
+
routineId: string | null;
|
|
1407
|
+
routineName: string | null;
|
|
1408
|
+
runId: string | null;
|
|
1409
|
+
sandboxed: boolean;
|
|
1410
|
+
askedBy: string[];
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
export interface Approval {
|
|
1414
|
+
id: string;
|
|
1415
|
+
botId: string;
|
|
1416
|
+
kind: ApprovalKind;
|
|
1417
|
+
title: string;
|
|
1418
|
+
details: string;
|
|
1419
|
+
followUp: string;
|
|
1420
|
+
action: string | null;
|
|
1421
|
+
skillName: string | null;
|
|
1422
|
+
/** A connector request: which connector (its id), as the Boton asked for it. */
|
|
1423
|
+
connectorId: string | null;
|
|
1424
|
+
status: ApprovalStatus;
|
|
1425
|
+
always: boolean;
|
|
1426
|
+
auto: boolean;
|
|
1427
|
+
note: string | null;
|
|
1428
|
+
from: ApprovalOrigin;
|
|
1429
|
+
createdAt: string;
|
|
1430
|
+
expiresAt: string | null;
|
|
1431
|
+
decidedAt: string | null;
|
|
1432
|
+
messageId: string | null;
|
|
1433
|
+
notifications: NotifyDelivery[];
|
|
1434
|
+
tool: ApprovalToolCall | null;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
export interface ApprovalToolCall {
|
|
1438
|
+
name: string;
|
|
1439
|
+
/** The card's title and details are made from this, never from what the Boton says. */
|
|
1440
|
+
input: unknown;
|
|
1441
|
+
rule: string | null;
|
|
1442
|
+
waitUntil: string;
|
|
1443
|
+
inTurn: boolean;
|
|
1444
|
+
usedAt: string | null;
|
|
1445
|
+
/**
|
|
1446
|
+
* A request the Boton's managed browser holds at the network boundary (not a CLI's tool call): allowing it sends
|
|
1447
|
+
* exactly the request on the card once, while the browser still holds it; `usedAt` is when it was sent. Never
|
|
1448
|
+
* always-allowed, and never carried over to another turn.
|
|
1449
|
+
*/
|
|
1450
|
+
browser: boolean;
|
|
1451
|
+
/**
|
|
1452
|
+
* A browser request's send, once allowed and spent: `sending` until it is known (a server stopped mid-send leaves it
|
|
1453
|
+
* so, which means unknown), `sent` (the site answered), `not-sent` (it failed before it left) or `unknown` (written,
|
|
1454
|
+
* then the connection broke before an answer: the site may have acted on it, so it is never retried). Else null.
|
|
1455
|
+
*/
|
|
1456
|
+
delivery: 'sending' | 'sent' | 'not-sent' | 'unknown' | null;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
export interface ToolCallInput {
|
|
1460
|
+
toolName: string;
|
|
1461
|
+
input: unknown;
|
|
1462
|
+
/** The CLI's id for the call, for the log. */
|
|
1463
|
+
toolUseId?: string;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
export interface ToolCallVerdict {
|
|
1467
|
+
behavior: 'allow' | 'deny' | 'wait';
|
|
1468
|
+
message: string | null;
|
|
1469
|
+
approval: Approval | null;
|
|
1470
|
+
/** With 'wait': this call's own id on the card, for GET /api/approvals/:id/tool-call?waiter=. Only it gets the allow. */
|
|
1471
|
+
waiter?: string;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
export interface ApprovalRequestInput {
|
|
1475
|
+
title: string;
|
|
1476
|
+
details?: string;
|
|
1477
|
+
followUp?: string;
|
|
1478
|
+
action?: string;
|
|
1479
|
+
expiresIn?: string;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
export interface ApprovalRequestResult {
|
|
1483
|
+
approval: Approval;
|
|
1484
|
+
duplicate: boolean;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
export interface ApprovalDecisionInput {
|
|
1488
|
+
decision: 'allow' | 'deny';
|
|
1489
|
+
always?: boolean;
|
|
1490
|
+
note?: string;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
export interface CancelRunsResult {
|
|
1494
|
+
cancelledRuns: number;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
export interface StopInput {
|
|
1498
|
+
clearQueue?: boolean;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
export interface StopResult {
|
|
1502
|
+
stopped: boolean;
|
|
1503
|
+
cancelledRuns: number;
|
|
1504
|
+
queuedRuns: number;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
export interface ServerInfo {
|
|
1508
|
+
install?: import('./install-commands.ts').InstallType;
|
|
1509
|
+
version: string;
|
|
1510
|
+
/** A different value in a later `hello` event means the server was upgraded. */
|
|
1511
|
+
build: string | null;
|
|
1512
|
+
hostname: string;
|
|
1513
|
+
urls: string[];
|
|
1514
|
+
publicUrl: string;
|
|
1515
|
+
hooksUrl: string | null;
|
|
1516
|
+
/** The longest one agent turn may run, in ms (BOTONS_TURN_TIMEOUT_MS): a routine's time limit defaults to it and can't exceed it. */
|
|
1517
|
+
turnTimeoutMs: number;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
export interface AuthStatus {
|
|
1521
|
+
/** False only with BOTONS_AUTH=off. */
|
|
1522
|
+
required: boolean;
|
|
1523
|
+
caller: 'device' | 'boton' | 'none';
|
|
1524
|
+
deviceId: string | null;
|
|
1525
|
+
botId: string | null;
|
|
1526
|
+
urlKeys: UrlKeys | null;
|
|
1527
|
+
ownerPairing: { login: string; node: string } | null;
|
|
1528
|
+
hostname: string;
|
|
1529
|
+
/** False until the first device pairs: that device claims the server for its owner. */
|
|
1530
|
+
claimed: boolean;
|
|
1531
|
+
/** What to run on the server's machine to manage pairing (`npm run pair`, or its Docker form). */
|
|
1532
|
+
pairCommand: string;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* Which networks may reach the app's port. A second line behind the listeners (BOTONS_HOST, the
|
|
1537
|
+
* published port) and auth: requests from other networks are refused before anything else runs.
|
|
1538
|
+
* It narrows, never widens: reaching the server from further away also needs host changes.
|
|
1539
|
+
*/
|
|
1540
|
+
export type AccessMode = 'local' | 'tailscale' | 'lan' | 'public';
|
|
1541
|
+
|
|
1542
|
+
export interface AccessModeOption {
|
|
1543
|
+
mode: AccessMode;
|
|
1544
|
+
label: string;
|
|
1545
|
+
/** Which connections this mode accepts, as the server is set up now. */
|
|
1546
|
+
summary: string;
|
|
1547
|
+
/** What the mode can't see (forwarders, a container's network): available in Details. */
|
|
1548
|
+
limits: string[];
|
|
1549
|
+
/** False: it can't be chosen as the server is set up now; `reason` says why. */
|
|
1550
|
+
available: boolean;
|
|
1551
|
+
reason: string | null;
|
|
1552
|
+
/** What the owner does on the host first (none for local); Botons never does these itself. */
|
|
1553
|
+
steps: string[];
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
export interface SetupWarning {
|
|
1557
|
+
level: 'error' | 'warn' | 'info';
|
|
1558
|
+
text: string;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
export interface SetupStatus {
|
|
1562
|
+
owner: {
|
|
1563
|
+
name: string;
|
|
1564
|
+
/** When the first device paired, and its label; null on a server paired before claims were recorded. */
|
|
1565
|
+
claimedAt: string | null;
|
|
1566
|
+
claimedBy: string | null;
|
|
1567
|
+
devices: number;
|
|
1568
|
+
};
|
|
1569
|
+
/** The first-run setup was finished or skipped (or the server already had Botons). */
|
|
1570
|
+
completed: boolean;
|
|
1571
|
+
completedAt: string | null;
|
|
1572
|
+
/** null: never chosen, so no network is refused beyond what the listeners allow. */
|
|
1573
|
+
accessMode: AccessMode | null;
|
|
1574
|
+
recommendedMode: AccessMode | null;
|
|
1575
|
+
modes: AccessModeOption[];
|
|
1576
|
+
exposure: {
|
|
1577
|
+
/** Where the app listens (host:port). */
|
|
1578
|
+
listeners: string[];
|
|
1579
|
+
/** The webhooks-only listener, if any (BOTONS_HOOKS_PORT). */
|
|
1580
|
+
hooksListeners: string[];
|
|
1581
|
+
publicUrl: string;
|
|
1582
|
+
https: boolean;
|
|
1583
|
+
authRequired: boolean;
|
|
1584
|
+
tailscale: boolean;
|
|
1585
|
+
container: boolean;
|
|
1586
|
+
};
|
|
1587
|
+
warnings: SetupWarning[];
|
|
1588
|
+
botons: number;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
export interface UpdateSetupInput {
|
|
1592
|
+
/** null forgets the choice (no network refused beyond the listeners). */
|
|
1593
|
+
accessMode?: AccessMode | null;
|
|
1594
|
+
/** true: the first-run setup is done (or skipped); it is not shown again. */
|
|
1595
|
+
complete?: boolean;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
export type AuditActorKind = 'device' | 'boton' | 'host' | 'system';
|
|
1599
|
+
|
|
1600
|
+
export type AuditAction =
|
|
1601
|
+
| 'device.paired'
|
|
1602
|
+
| 'device.revoked'
|
|
1603
|
+
| 'devices.revoked_all'
|
|
1604
|
+
| 'pairing_request.approved'
|
|
1605
|
+
| 'access_mode.changed'
|
|
1606
|
+
| 'setup.completed'
|
|
1607
|
+
| 'settings.notifications_changed'
|
|
1608
|
+
| 'notifications.tested'
|
|
1609
|
+
| 'backup.rehearsed'
|
|
1610
|
+
| 'boton.created'
|
|
1611
|
+
| 'boton.deleted'
|
|
1612
|
+
| 'boton.access_changed'
|
|
1613
|
+
| 'boton.spending_changed'
|
|
1614
|
+
| 'boton.permissions_changed'
|
|
1615
|
+
| 'routine.permissions_changed'
|
|
1616
|
+
| 'routine.starter_activated'
|
|
1617
|
+
| 'run.retried'
|
|
1618
|
+
| 'run.cancelled'
|
|
1619
|
+
| 'run.callback_resent'
|
|
1620
|
+
| 'approval.decided'
|
|
1621
|
+
| 'card.answered'
|
|
1622
|
+
| 'skill.approved'
|
|
1623
|
+
| 'skill.installed'
|
|
1624
|
+
| 'skill.restored'
|
|
1625
|
+
| 'skill.enabled'
|
|
1626
|
+
| 'skill.disabled'
|
|
1627
|
+
| 'connector.policy_changed'
|
|
1628
|
+
| 'connector.granted'
|
|
1629
|
+
| 'connector.tested'
|
|
1630
|
+
| 'connector.disabled'
|
|
1631
|
+
| 'connector.enabled'
|
|
1632
|
+
| 'mcp_server.created'
|
|
1633
|
+
| 'mcp_server.changed'
|
|
1634
|
+
| 'mcp_server.deleted'
|
|
1635
|
+
| 'oauth.created'
|
|
1636
|
+
| 'oauth.deleted'
|
|
1637
|
+
| 'oauth.signin_started'
|
|
1638
|
+
| 'oauth.connected'
|
|
1639
|
+
| 'brain.cli_install_started'
|
|
1640
|
+
| 'brain.cli_install_succeeded'
|
|
1641
|
+
| 'brain.cli_install_failed'
|
|
1642
|
+
| 'brain.signin_started'
|
|
1643
|
+
| 'brain.signin_cancelled'
|
|
1644
|
+
| 'brain.signin_import_confirmed'
|
|
1645
|
+
| 'brain.signin_import_completed'
|
|
1646
|
+
| 'brain.config_repair_confirmed'
|
|
1647
|
+
| 'brain.config_repair_completed'
|
|
1648
|
+
| 'brain.config_repair_failed'
|
|
1649
|
+
| 'api_key.saved'
|
|
1650
|
+
| 'api_key.removed'
|
|
1651
|
+
| 'api_key.tested'
|
|
1652
|
+
| 'browser.sites_changed'
|
|
1653
|
+
| 'browser.stopped'
|
|
1654
|
+
| 'browser.handover_started'
|
|
1655
|
+
| 'browser.handover_ended'
|
|
1656
|
+
| 'config.exported'
|
|
1657
|
+
| 'config.imported'
|
|
1658
|
+
| 'files.trashed'
|
|
1659
|
+
| 'files.restored'
|
|
1660
|
+
| 'files.purged'
|
|
1661
|
+
| 'inbox.cleared'
|
|
1662
|
+
| 'inbox.cleaned_up';
|
|
1663
|
+
|
|
1664
|
+
/** One consequential action. Never holds a token, code, secret, URL query or message text. */
|
|
1665
|
+
export interface AuditEvent {
|
|
1666
|
+
seq: number;
|
|
1667
|
+
at: string;
|
|
1668
|
+
actor: { kind: AuditActorKind; id: string | null; label: string };
|
|
1669
|
+
action: AuditAction;
|
|
1670
|
+
target: { kind: string; id: string; label: string } | null;
|
|
1671
|
+
details: Record<string, string | number | boolean | null | string[]>;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/** What one Boton may do, in the owner's words: for Settings and for review, not enforcement. */
|
|
1675
|
+
export interface BotonCapabilities {
|
|
1676
|
+
botId: string;
|
|
1677
|
+
access: AccessLevel;
|
|
1678
|
+
paused: boolean;
|
|
1679
|
+
/** One sentence each, most consequential first. */
|
|
1680
|
+
summary: string[];
|
|
1681
|
+
connectors: { id: string; name: string; usable: boolean; off: boolean }[];
|
|
1682
|
+
/** Connectors it may ask for, and how a request is granted. */
|
|
1683
|
+
requestable: { id: string; name: string; grant: ConnectorGrant }[];
|
|
1684
|
+
/** null: every skill in the library. */
|
|
1685
|
+
skills: string[] | null;
|
|
1686
|
+
/**
|
|
1687
|
+
* The saved Ask first rules as the engine applies them: `enforced` pause a tool call, `alwaysAllowed` pause
|
|
1688
|
+
* only webhook runs ("Always allow" covers them), `inactive` do nothing because the brain doesn't support them.
|
|
1689
|
+
*/
|
|
1690
|
+
askFirst: { enforced: string[]; alwaysAllowed: string[]; inactive: string[] };
|
|
1691
|
+
alwaysAllow: string[];
|
|
1692
|
+
routines: { id: string; name: string; enabled: boolean; schedule: boolean; webhook: boolean; webhookFullAccess: boolean; callback: boolean }[];
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
/** `?key=` values for what a browser loads by URL and can't send a header with, each good for its GET routes only. */
|
|
1696
|
+
export interface UrlKeys {
|
|
1697
|
+
events: string;
|
|
1698
|
+
files: string;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
export interface Device {
|
|
1702
|
+
id: string;
|
|
1703
|
+
label: string;
|
|
1704
|
+
createdAt: string;
|
|
1705
|
+
lastUsedAt: string | null;
|
|
1706
|
+
current: boolean;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/** With `secret`: the request the owner approved. With neither: the owner's own Tailscale device. A `code` alone is refused. */
|
|
1710
|
+
export interface PairInput {
|
|
1711
|
+
secret?: string;
|
|
1712
|
+
code?: string;
|
|
1713
|
+
label?: string;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
export interface PairResult {
|
|
1717
|
+
device: Device;
|
|
1718
|
+
token: string;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* A device's request to pair. The device keeps `secret` (it alone redeems the approval) and shows `code`,
|
|
1723
|
+
* which the owner approves on the server's machine (`approveCommand`) or on a paired device.
|
|
1724
|
+
*/
|
|
1725
|
+
export interface PairingRequest {
|
|
1726
|
+
secret: string;
|
|
1727
|
+
code: string;
|
|
1728
|
+
expiresAt: string;
|
|
1729
|
+
approveCommand: string;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
export interface ApprovePairingInput {
|
|
1733
|
+
code: string;
|
|
1734
|
+
/** Names the device; beats the device's own guess. */
|
|
1735
|
+
label?: string;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
export interface ApprovedPairing {
|
|
1739
|
+
label: string;
|
|
1740
|
+
createdAt: string;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/** Every connector is Botons' own: a server in its library, or one signed in to with OAuth. */
|
|
1744
|
+
export type ConnectorSource = 'library' | 'oauth';
|
|
1745
|
+
|
|
1746
|
+
export type ConnectorStatus = 'ok' | 'needs-auth';
|
|
1747
|
+
|
|
1748
|
+
/** Never carries secrets. */
|
|
1749
|
+
export interface Connector {
|
|
1750
|
+
id: string;
|
|
1751
|
+
name: string;
|
|
1752
|
+
source: ConnectorSource;
|
|
1753
|
+
providers: string[];
|
|
1754
|
+
transport: 'stdio' | 'http';
|
|
1755
|
+
target: string;
|
|
1756
|
+
status: ConnectorStatus;
|
|
1757
|
+
note: string | null;
|
|
1758
|
+
/** Botons that don't have it may ask for it (`botons connector request`). */
|
|
1759
|
+
onRequest: boolean;
|
|
1760
|
+
/** How such a request is granted: at once, or on an approval card. */
|
|
1761
|
+
grant: ConnectorGrant;
|
|
1762
|
+
/** What `status` rests on, in the owner's words: being listed or set up is not the same as its tools loading in a turn. */
|
|
1763
|
+
basis: string;
|
|
1764
|
+
/** The owner can test it from the server (POST /api/connectors/:id/test): Botons reaches it itself and lists its tools. */
|
|
1765
|
+
testable: boolean;
|
|
1766
|
+
/** The last test since the server started, if any. */
|
|
1767
|
+
lastTest: ConnectorTest | null;
|
|
1768
|
+
/** Built into Botons (the managed browser): run by the server itself, not edited, tested or asked for. */
|
|
1769
|
+
managed?: boolean;
|
|
1770
|
+
/**
|
|
1771
|
+
* False: the owner turned it off. Its setup, sign-in and grants are kept, but no turn gets it, no Boton may ask for
|
|
1772
|
+
* it and its work feed isn't read, until it is turned on again. The managed browser is always on here (it is given
|
|
1773
|
+
* and stopped in each Boton's settings).
|
|
1774
|
+
*/
|
|
1775
|
+
enabled: boolean;
|
|
1776
|
+
/** When and why it was turned off; null while it is on. */
|
|
1777
|
+
off: ConnectorOff | null;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
export interface ConnectorOff {
|
|
1781
|
+
at: string;
|
|
1782
|
+
/** The owner's own words, if any (at most 200 characters). */
|
|
1783
|
+
reason: string;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* An owner's test of one connector. reached: Botons' server started an MCP session and listed its tools; failed:
|
|
1788
|
+
* it tried and couldn't; untestable: the server can't check this one itself (`scope` says what would).
|
|
1789
|
+
*/
|
|
1790
|
+
export interface ConnectorTest {
|
|
1791
|
+
connectorId: string;
|
|
1792
|
+
at: string;
|
|
1793
|
+
outcome: 'reached' | 'failed' | 'untestable';
|
|
1794
|
+
/** What the test covered and what it doesn't prove. */
|
|
1795
|
+
scope: string;
|
|
1796
|
+
/** Why it failed or wasn't tested; when reached, a warning about what the token can see (GitHub's private repos). */
|
|
1797
|
+
problem: string | null;
|
|
1798
|
+
toolCount: number;
|
|
1799
|
+
/** Tool names, as the server listed them (at most 100). */
|
|
1800
|
+
tools: string[];
|
|
1801
|
+
durationMs: number;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
export type ConnectorGrant = 'approval' | 'immediate';
|
|
1805
|
+
|
|
1806
|
+
export interface ConnectorPolicyInput {
|
|
1807
|
+
onRequest?: boolean;
|
|
1808
|
+
grant?: ConnectorGrant;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* One Boton as one connector sees it. has: it is in the Boton's connectors, so its turns get it; mayAsk: it doesn't
|
|
1813
|
+
* have it but may ask for it (`botons connector request`, granted as the connector's policy says); brainCanUse: the
|
|
1814
|
+
* Boton's brain takes connectors at all. withoutReason: why its last turn went without it, if it did.
|
|
1815
|
+
*/
|
|
1816
|
+
export interface ConnectorBotonAccess {
|
|
1817
|
+
botId: string;
|
|
1818
|
+
name: string;
|
|
1819
|
+
provider: string;
|
|
1820
|
+
has: boolean;
|
|
1821
|
+
mayAsk: boolean;
|
|
1822
|
+
brainCanUse: boolean;
|
|
1823
|
+
withoutReason: string | null;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/** Which Botons may use one connector, exactly, and what was done to it and its grants (newest first). */
|
|
1827
|
+
export interface ConnectorAccess {
|
|
1828
|
+
connectorId: string;
|
|
1829
|
+
name: string;
|
|
1830
|
+
/** The managed browser: given only in each Boton's settings, so this view is read-only for it. */
|
|
1831
|
+
managed: boolean;
|
|
1832
|
+
onRequest: boolean;
|
|
1833
|
+
grant: ConnectorGrant;
|
|
1834
|
+
enabled: boolean;
|
|
1835
|
+
off: ConnectorOff | null;
|
|
1836
|
+
botons: ConnectorBotonAccess[];
|
|
1837
|
+
/**
|
|
1838
|
+
* Botons with a turn running now that was given it. Turning it off doesn't reach into that turn (its CLI already
|
|
1839
|
+
* has the server, and an OAuth token can't be taken back from it), so each keeps it until that turn ends. `turnId`
|
|
1840
|
+
* names that one turn: a later turn of the same Boton is another one.
|
|
1841
|
+
*/
|
|
1842
|
+
running: { botId: string; name: string; turnId: string }[];
|
|
1843
|
+
history: AuditEvent[];
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Turn a connector off or on. `expected` is what the owner was shown: whether it was on, which Botons had it, and the
|
|
1848
|
+
* turns running with it (their `turnId`s). If it is on or off otherwise now, other Botons have it, or a turn they
|
|
1849
|
+
* weren't shown runs with it now, nothing is changed and the server answers 409.
|
|
1850
|
+
*/
|
|
1851
|
+
export interface ConnectorEnabledInput {
|
|
1852
|
+
enabled: boolean;
|
|
1853
|
+
reason?: string;
|
|
1854
|
+
expected: { enabled: boolean; botIds: string[]; running: string[] };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* The Botons that should have the connector, exactly. `expected` is the list the owner was shown (the Botons that
|
|
1859
|
+
* had it): if that changed meanwhile, nothing is changed and the server answers 409.
|
|
1860
|
+
*/
|
|
1861
|
+
export interface ConnectorAccessInput {
|
|
1862
|
+
botIds: string[];
|
|
1863
|
+
expected: string[];
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/** What a Boton sees of a connector: never its URL, headers or tokens. */
|
|
1867
|
+
export interface BotonConnector {
|
|
1868
|
+
id: string;
|
|
1869
|
+
name: string;
|
|
1870
|
+
/** yours: in this Boton's connectors; available: it may ask for it. */
|
|
1871
|
+
state: 'yours' | 'available';
|
|
1872
|
+
grant: ConnectorGrant | null;
|
|
1873
|
+
usable: boolean;
|
|
1874
|
+
/** The owner turned it off: it stays this Boton's, but its turns go without it until it is on again. */
|
|
1875
|
+
off: boolean;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
export interface ConnectorRequestResult {
|
|
1879
|
+
connector: BotonConnector;
|
|
1880
|
+
/** granted: it is the Boton's from its next turn; pending: on an approval card; already: it had it. */
|
|
1881
|
+
outcome: 'granted' | 'pending' | 'already';
|
|
1882
|
+
approval: Approval | null;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
export type OAuthStatus = 'new' | 'connected' | 'reconnect';
|
|
1886
|
+
|
|
1887
|
+
/** A remote MCP server signed in to with OAuth. Tokens never leave the server. */
|
|
1888
|
+
export interface OAuthConnection {
|
|
1889
|
+
id: string;
|
|
1890
|
+
/** Its connector id is "oauth:<name>". */
|
|
1891
|
+
name: string;
|
|
1892
|
+
url: string;
|
|
1893
|
+
status: OAuthStatus;
|
|
1894
|
+
statusReason: string | null;
|
|
1895
|
+
issuer: string | null;
|
|
1896
|
+
scope: string | null;
|
|
1897
|
+
connectedAt: string | null;
|
|
1898
|
+
refreshedAt: string | null;
|
|
1899
|
+
/** The last refresh failed but the grant may still work (network, server error); it is tried again. */
|
|
1900
|
+
refreshError: string | null;
|
|
1901
|
+
keepConnected: boolean;
|
|
1902
|
+
nextKeepAliveAt: string | null;
|
|
1903
|
+
/** The server has a compatible work_feed tool, which Botons polls. */
|
|
1904
|
+
feed: boolean;
|
|
1905
|
+
feedCheckedAt: string | null;
|
|
1906
|
+
feedError: string | null;
|
|
1907
|
+
/** Where updates on work no Boton is known to have started go. */
|
|
1908
|
+
unknownJobs: 'nowhere' | 'primary';
|
|
1909
|
+
createdAt: string;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
export interface OAuthConnectionInput {
|
|
1913
|
+
name?: string;
|
|
1914
|
+
url?: string;
|
|
1915
|
+
keepConnected?: boolean;
|
|
1916
|
+
unknownJobs?: 'nowhere' | 'primary';
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
export interface OAuthConnectStart {
|
|
1920
|
+
/** Open it in the browser: the authorization server's sign-in page. */
|
|
1921
|
+
authorizeUrl: string;
|
|
1922
|
+
expiresAt: string;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
export interface ConnectorCatalog {
|
|
1926
|
+
connectors: Connector[];
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
/**
|
|
1930
|
+
* Something the owner should know about one of a Boton's connectors. removed: it was a connector from the owner's
|
|
1931
|
+
* own Claude Code or Codex setup, which Botons no longer uses, and was taken off the Boton; it stays until the
|
|
1932
|
+
* owner dismisses it or gives the Boton `offer`. skipped: the Boton's last turn went without it, for `reason`.
|
|
1933
|
+
*/
|
|
1934
|
+
export interface ConnectorNotice {
|
|
1935
|
+
botId: string;
|
|
1936
|
+
connectorId: string;
|
|
1937
|
+
kind: 'removed' | 'skipped';
|
|
1938
|
+
name: string;
|
|
1939
|
+
/** Where it pointed (no query or credentials), if known. */
|
|
1940
|
+
target: string;
|
|
1941
|
+
reason: string;
|
|
1942
|
+
at: string;
|
|
1943
|
+
/** A Botons connector with the same name or address, which the owner may give the Boton instead. */
|
|
1944
|
+
offer: { id: string; name: string; target: string } | null;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/** Environment and header values are write-only: only their names come back. */
|
|
1948
|
+
export interface McpServer {
|
|
1949
|
+
name: string;
|
|
1950
|
+
transport: 'stdio' | 'http';
|
|
1951
|
+
command: string;
|
|
1952
|
+
args: string[];
|
|
1953
|
+
url: string;
|
|
1954
|
+
envKeys: string[];
|
|
1955
|
+
headerKeys: string[];
|
|
1956
|
+
createdAt: string;
|
|
1957
|
+
updatedAt: string;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
export interface McpServerInput {
|
|
1961
|
+
name?: string;
|
|
1962
|
+
transport?: 'stdio' | 'http';
|
|
1963
|
+
command?: string;
|
|
1964
|
+
args?: string[];
|
|
1965
|
+
url?: string;
|
|
1966
|
+
/** null removes a value; names left out keep theirs. */
|
|
1967
|
+
env?: Record<string, string | null>;
|
|
1968
|
+
headers?: Record<string, string | null>;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
export interface SchedulePreview {
|
|
1972
|
+
ok: boolean;
|
|
1973
|
+
error: string | null;
|
|
1974
|
+
schedule: string | null;
|
|
1975
|
+
text: string | null;
|
|
1976
|
+
nextRuns: string[];
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
export interface CreateBotInput {
|
|
1980
|
+
name: string;
|
|
1981
|
+
title?: string;
|
|
1982
|
+
instructions?: string;
|
|
1983
|
+
look?: BotonLook;
|
|
1984
|
+
provider?: string;
|
|
1985
|
+
model?: string;
|
|
1986
|
+
/** "": the provider's default. */
|
|
1987
|
+
effort?: string;
|
|
1988
|
+
access?: AccessLevel;
|
|
1989
|
+
/** A sidebar group by id; null or "": ungrouped. Not with `group`. */
|
|
1990
|
+
groupId?: string | null;
|
|
1991
|
+
/** A sidebar group by name (any case), made if there is none; "": ungrouped. Not with `groupId`. */
|
|
1992
|
+
group?: string;
|
|
1993
|
+
/** Pinning puts it last among the pinned; to place it, POST /api/sidebar/bots/:id/pin. */
|
|
1994
|
+
pinned?: boolean;
|
|
1995
|
+
timezone?: string;
|
|
1996
|
+
connections?: string[];
|
|
1997
|
+
skills?: string[] | null;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
export interface NewBotInput extends CreateBotInput {
|
|
2001
|
+
/** Applied atomically with creation; null means no daily cap. */
|
|
2002
|
+
dailyCapUsd?: number | null;
|
|
2003
|
+
templateId?: string;
|
|
2004
|
+
startRoutines?: boolean;
|
|
2005
|
+
/**
|
|
2006
|
+
* A starter template (one with `needs`) is installed only as the owner reviewed it: `reviewKey` from
|
|
2007
|
+
* POST /api/templates/:id/review for the same brain, access, timezone and `starterConnectors`. Its connectors are
|
|
2008
|
+
* the owner's choices here (slot to connector id, or null to go without an optional one), never the template's,
|
|
2009
|
+
* and its routines start paused.
|
|
2010
|
+
*/
|
|
2011
|
+
reviewKey?: string;
|
|
2012
|
+
starterConnectors?: Record<string, string | null>;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
export type UpdateBotInput = Partial<CreateBotInput> & {
|
|
2016
|
+
alwaysAllow?: string[];
|
|
2017
|
+
askFirst?: string[];
|
|
2018
|
+
};
|
|
2019
|
+
|
|
2020
|
+
/** A Boton update as sent: moving it to another provider that would end a session needs the preview's confirmation. */
|
|
2021
|
+
/**
|
|
2022
|
+
* `switchSummary`: ask the old session for a summary before the move (its preview's `summary.confirmation` then goes in
|
|
2023
|
+
* `switchConfirmation`); the move waits for it, and is refused, changing nothing, if it can't be written.
|
|
2024
|
+
*/
|
|
2025
|
+
export type UpdateBotRequest = UpdateBotInput & { switchConfirmation?: string; switchSummary?: boolean };
|
|
2026
|
+
|
|
2027
|
+
export interface DuplicateBotInput {
|
|
2028
|
+
name?: string;
|
|
2029
|
+
withRoutines?: boolean;
|
|
2030
|
+
withMemories?: boolean;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
export interface TemplateRoutine {
|
|
2034
|
+
name: string;
|
|
2035
|
+
instruction: string;
|
|
2036
|
+
schedule: string | null;
|
|
2037
|
+
scheduleText: string | null;
|
|
2038
|
+
timezone: string | null;
|
|
2039
|
+
webhook: boolean;
|
|
2040
|
+
notify: NotifyPolicy;
|
|
2041
|
+
webhookWorkspace: boolean;
|
|
2042
|
+
webhookSource: WebhookSource;
|
|
2043
|
+
webhookFilter: WebhookFilter | null;
|
|
2044
|
+
skill: string | null;
|
|
2045
|
+
model: string | null;
|
|
2046
|
+
effort: string | null;
|
|
2047
|
+
session: RoutineSession;
|
|
2048
|
+
/** null: the server's limit. */
|
|
2049
|
+
timeLimitMs: number | null;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/** Nulls leave the New Boton form's own value. */
|
|
2053
|
+
export interface TemplateBot {
|
|
2054
|
+
name: string;
|
|
2055
|
+
title: string;
|
|
2056
|
+
instructions: string;
|
|
2057
|
+
look: BotonLook | null;
|
|
2058
|
+
provider: string | null;
|
|
2059
|
+
model: string;
|
|
2060
|
+
effort: string;
|
|
2061
|
+
access: AccessLevel;
|
|
2062
|
+
group: string;
|
|
2063
|
+
timezone: string | null;
|
|
2064
|
+
connections: string[];
|
|
2065
|
+
skills: string[] | null;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
export interface BotTemplate {
|
|
2069
|
+
id: string;
|
|
2070
|
+
name: string;
|
|
2071
|
+
description: string;
|
|
2072
|
+
builtin: boolean;
|
|
2073
|
+
bot: TemplateBot;
|
|
2074
|
+
routines: TemplateRoutine[];
|
|
2075
|
+
createdAt: string | null;
|
|
2076
|
+
/** A starter's declared needs, reviewed by the owner before it is installed; null for a saved template. */
|
|
2077
|
+
needs: StarterNeeds | null;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
/**
|
|
2081
|
+
* What a starter says it needs, in the owner's words. A declaration only: nothing here is granted. The owner picks
|
|
2082
|
+
* the brain, the access and which of their own connectors (if any) fills each slot, and reviews exactly that.
|
|
2083
|
+
*/
|
|
2084
|
+
export interface StarterNeeds {
|
|
2085
|
+
/** Part of the review key: a starter whose declaration changes must be reviewed again. */
|
|
2086
|
+
version: number;
|
|
2087
|
+
brain: {
|
|
2088
|
+
/** Its work needs connectors, so the brain must take them. */
|
|
2089
|
+
connectors: boolean;
|
|
2090
|
+
text: string;
|
|
2091
|
+
};
|
|
2092
|
+
/** The access it asks for; the owner may choose another, which the review says. */
|
|
2093
|
+
access: AccessLevel;
|
|
2094
|
+
accessWhy: string;
|
|
2095
|
+
connectors: StarterConnectorNeed[];
|
|
2096
|
+
/** When it runs, in words; the routines' own schedules are in `routines`. */
|
|
2097
|
+
schedule: string;
|
|
2098
|
+
output: { where: 'chat' | 'workspace'; folder: string | null; text: string };
|
|
2099
|
+
/** What happens when a run fails. */
|
|
2100
|
+
failure: string;
|
|
2101
|
+
/** A webhook routine's sample payload for its test run. */
|
|
2102
|
+
samplePayload: string | null;
|
|
2103
|
+
/** What no check here can verify (the services themselves). */
|
|
2104
|
+
notChecked: string[];
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
/** A connector the starter's work uses, filled by one of the owner's connectors they choose (or none, if optional). */
|
|
2108
|
+
export interface StarterConnectorNeed {
|
|
2109
|
+
slot: string;
|
|
2110
|
+
label: string;
|
|
2111
|
+
purpose: string;
|
|
2112
|
+
required: boolean;
|
|
2113
|
+
/** Words matched against the owner's connector names and addresses to suggest one; never a connector id. */
|
|
2114
|
+
hints: string[];
|
|
2115
|
+
/** How the starter's instructions use it. The grant itself is the whole connector, which the review says. */
|
|
2116
|
+
uses: 'read' | 'read-write';
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
export interface TemplateReviewInput {
|
|
2120
|
+
provider: string;
|
|
2121
|
+
access: AccessLevel;
|
|
2122
|
+
timezone?: string;
|
|
2123
|
+
/** Slot to connector id, or null for none. */
|
|
2124
|
+
connectors?: Record<string, string | null>;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
export type ReviewItemState = 'ok' | 'warning' | 'blocked';
|
|
2128
|
+
|
|
2129
|
+
/** One connector the owner could put in a slot. */
|
|
2130
|
+
export interface StarterConnectorCandidate {
|
|
2131
|
+
id: string;
|
|
2132
|
+
name: string;
|
|
2133
|
+
/** Its name or address matches one of the slot's hints. */
|
|
2134
|
+
suggested: boolean;
|
|
2135
|
+
/** Why it can't be used for this Boton now, if it can't. */
|
|
2136
|
+
problem: string | null;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
export interface TemplateReviewSlot {
|
|
2140
|
+
slot: string;
|
|
2141
|
+
label: string;
|
|
2142
|
+
purpose: string;
|
|
2143
|
+
required: boolean;
|
|
2144
|
+
uses: 'read' | 'read-write';
|
|
2145
|
+
chosen: { id: string; name: string } | null;
|
|
2146
|
+
state: ReviewItemState;
|
|
2147
|
+
text: string;
|
|
2148
|
+
candidates: StarterConnectorCandidate[];
|
|
2149
|
+
/** What to do when nothing fits (no connector, or none this brain can use). */
|
|
2150
|
+
guidance: string | null;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/**
|
|
2154
|
+
* Exactly what installing a starter grants, for the owner to confirm. `reviewKey` binds the confirmation to it: an
|
|
2155
|
+
* install sent with a key for anything else (another brain, access, connector, or the connector's state, or the
|
|
2156
|
+
* starter itself, changed since) is refused with 409.
|
|
2157
|
+
*/
|
|
2158
|
+
export interface TemplateReview {
|
|
2159
|
+
templateId: string;
|
|
2160
|
+
brain: { id: string; label: string; ready: boolean; takesConnectors: boolean; text: string; state: ReviewItemState };
|
|
2161
|
+
access: { level: AccessLevel; asked: AccessLevel; text: string; state: ReviewItemState };
|
|
2162
|
+
slots: TemplateReviewSlot[];
|
|
2163
|
+
skills: string[];
|
|
2164
|
+
routines: { name: string; scheduleText: string | null; timezone: string; webhook: boolean; notify: NotifyPolicy; text: string }[];
|
|
2165
|
+
output: string;
|
|
2166
|
+
failure: string;
|
|
2167
|
+
/** Problems that stop the install, in words. */
|
|
2168
|
+
blocked: string[];
|
|
2169
|
+
/** Everything it grants, one line each: the summary the owner confirms. */
|
|
2170
|
+
grants: string[];
|
|
2171
|
+
reviewKey: string;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
/** One line of a starter's setup check. `unverified`: nothing here can tell; the test run or the service says. */
|
|
2175
|
+
export interface SetupCheckItem {
|
|
2176
|
+
key: string;
|
|
2177
|
+
label: string;
|
|
2178
|
+
state: 'ok' | 'problem' | 'unverified';
|
|
2179
|
+
text: string;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* A starter's setup check on a Boton installed from it: computed on the server from what it saved at the install
|
|
2184
|
+
* (the starter, the owner's slot choices), the Boton's settings, the connectors' last known state and the brain's
|
|
2185
|
+
* last check. It runs nothing and reaches nothing outside this server.
|
|
2186
|
+
*/
|
|
2187
|
+
export interface SetupCheck {
|
|
2188
|
+
botId: string;
|
|
2189
|
+
templateId: string;
|
|
2190
|
+
at: string;
|
|
2191
|
+
/** The owner's slot choices at the install, as the server saved them. */
|
|
2192
|
+
slots: Record<string, string | null>;
|
|
2193
|
+
items: SetupCheckItem[];
|
|
2194
|
+
ready: boolean;
|
|
2195
|
+
/** Each routine's turning on, as the server decides it (POST /api/routines/:id/activate). */
|
|
2196
|
+
routines: StarterRoutineGate[];
|
|
2197
|
+
/** What it did and didn't cover, in words. */
|
|
2198
|
+
scope: string;
|
|
2199
|
+
notChecked: string[];
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* A Boton installed from a starter, as the server keeps it. Each of its routines waits on the owner's decision until
|
|
2204
|
+
* the owner turns it on with POST /api/routines/:id/activate: no other path (a PATCH, the `botons` CLI, the Routines
|
|
2205
|
+
* panel, a copy of the Boton) can turn it on before that.
|
|
2206
|
+
*/
|
|
2207
|
+
export interface StarterInstall {
|
|
2208
|
+
botId: string;
|
|
2209
|
+
templateId: string;
|
|
2210
|
+
templateName: string;
|
|
2211
|
+
slots: Record<string, string | null>;
|
|
2212
|
+
installedAt: string;
|
|
2213
|
+
/** Its routines still waiting on the owner's decision. */
|
|
2214
|
+
pending: string[];
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/** Whether a starter's routine can be turned on now, and why. */
|
|
2218
|
+
export interface StarterRoutineGate {
|
|
2219
|
+
routineId: string;
|
|
2220
|
+
/** Still waiting on the owner's decision; false once the owner turned it on (from then on an ordinary routine). */
|
|
2221
|
+
pending: boolean;
|
|
2222
|
+
/** A test run of its current settings finished without an error, and none is running: turning it on needs no override. */
|
|
2223
|
+
tested: boolean;
|
|
2224
|
+
/** A test run of it is queued or running: nothing turns it on until that ends. */
|
|
2225
|
+
testing: boolean;
|
|
2226
|
+
/** The latest test run, if any. */
|
|
2227
|
+
runId: string | null;
|
|
2228
|
+
text: string;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* The owner turns a starter's routine on. Without `override` only when its latest test run finished without an error
|
|
2233
|
+
* on exactly its current settings (the Boton's, the routine's, its connectors' and skills') and no test run is under
|
|
2234
|
+
* way. `override: true` turns it on without that, and is audited as such; it still waits for a test run under way.
|
|
2235
|
+
*/
|
|
2236
|
+
export interface StarterActivationInput {
|
|
2237
|
+
override?: boolean;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
export interface SaveTemplateInput {
|
|
2241
|
+
fromBotId: string;
|
|
2242
|
+
name?: string;
|
|
2243
|
+
description?: string;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
export interface CreateRoutineInput {
|
|
2247
|
+
name: string;
|
|
2248
|
+
instruction: string;
|
|
2249
|
+
schedule?: string | null;
|
|
2250
|
+
timezone?: string;
|
|
2251
|
+
webhook?: boolean;
|
|
2252
|
+
enabled?: boolean;
|
|
2253
|
+
notify?: NotifyPolicy;
|
|
2254
|
+
webhookWorkspace?: boolean;
|
|
2255
|
+
webhookSource?: WebhookSource;
|
|
2256
|
+
webhookSecret?: string | null;
|
|
2257
|
+
webhookFilter?: WebhookFilter | null;
|
|
2258
|
+
callbackUrl?: string | null;
|
|
2259
|
+
skill?: string | null;
|
|
2260
|
+
model?: string | null;
|
|
2261
|
+
/** null or absent: the Boton's effort. */
|
|
2262
|
+
effort?: string | null;
|
|
2263
|
+
session?: RoutineSession;
|
|
2264
|
+
/** 0 to WEBHOOK_HISTORY_MAX; above 0 only from the owner (see Routine.webhookHistory). */
|
|
2265
|
+
webhookHistory?: number;
|
|
2266
|
+
/** In ms, at least 1 second and at most the server's limit; null or absent: the server's limit. */
|
|
2267
|
+
timeLimitMs?: number | null;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
export interface UpdateRoutineInput {
|
|
2271
|
+
name?: string;
|
|
2272
|
+
instruction?: string;
|
|
2273
|
+
schedule?: string | null;
|
|
2274
|
+
timezone?: string;
|
|
2275
|
+
webhook?: boolean;
|
|
2276
|
+
rotateKey?: boolean;
|
|
2277
|
+
enabled?: boolean;
|
|
2278
|
+
notify?: NotifyPolicy;
|
|
2279
|
+
webhookWorkspace?: boolean;
|
|
2280
|
+
webhookSource?: WebhookSource;
|
|
2281
|
+
/** "" or null removes the saved one. */
|
|
2282
|
+
webhookSecret?: string | null;
|
|
2283
|
+
webhookFilter?: WebhookFilter | null;
|
|
2284
|
+
callbackUrl?: string | null;
|
|
2285
|
+
skill?: string | null;
|
|
2286
|
+
/** null or "": the Boton's model. */
|
|
2287
|
+
model?: string | null;
|
|
2288
|
+
/** null or "": the Boton's effort. */
|
|
2289
|
+
effort?: string | null;
|
|
2290
|
+
session?: RoutineSession;
|
|
2291
|
+
/** 0 to WEBHOOK_HISTORY_MAX; above 0 only from the owner (see Routine.webhookHistory). */
|
|
2292
|
+
webhookHistory?: number;
|
|
2293
|
+
/** null: back to the server's limit. */
|
|
2294
|
+
timeLimitMs?: number | null;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
export type SkillAuthor = { kind: 'owner' } | { kind: 'boton'; botId: string; name: string };
|
|
2298
|
+
|
|
2299
|
+
export interface Skill {
|
|
2300
|
+
name: string;
|
|
2301
|
+
description: string;
|
|
2302
|
+
body: string;
|
|
2303
|
+
createdBy: SkillAuthor;
|
|
2304
|
+
updatedBy: SkillAuthor;
|
|
2305
|
+
createdAt: string;
|
|
2306
|
+
updatedAt: string;
|
|
2307
|
+
/** A hash of description and body: approving names it, so a Boton can't swap the body between the owner reading and approving. */
|
|
2308
|
+
version: string;
|
|
2309
|
+
/** Not approved yet: only the Boton that wrote it (updatedBy) sees and uses it. */
|
|
2310
|
+
pending: boolean;
|
|
2311
|
+
/** Turned off by the owner: no Boton sees or follows it, and routines that follow it run without it. */
|
|
2312
|
+
disabled: boolean;
|
|
2313
|
+
/** The package it was installed from, or null for one written here. */
|
|
2314
|
+
source: SkillSource | null;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
/**
|
|
2318
|
+
* Where an installed skill came from: a folder in the skill packages folder (BOTONS_HOME/skill-packages). Only its
|
|
2319
|
+
* SKILL.md's text is installed, as it was when the owner reviewed it; nothing in the folder is ever run.
|
|
2320
|
+
*/
|
|
2321
|
+
export interface SkillSource {
|
|
2322
|
+
kind: 'folder';
|
|
2323
|
+
folder: string;
|
|
2324
|
+
/** The package's own `version:` from SKILL.md, as written there: a label, not checked. */
|
|
2325
|
+
label: string | null;
|
|
2326
|
+
/** Skill.version of the text installed from it. The skill was changed here since when this isn't Skill.version. */
|
|
2327
|
+
installed: string;
|
|
2328
|
+
installedAt: string;
|
|
2329
|
+
/** SKILL.md fields Botons doesn't use (like allowed-tools), as read: listed for the owner, never granted. */
|
|
2330
|
+
fields: Record<string, string>;
|
|
2331
|
+
/** The other files its folder held (SkillPackage.otherFiles), as reviewed: never installed. */
|
|
2332
|
+
files: string[];
|
|
2333
|
+
/** SkillPackage.fingerprint of the package as reviewed and installed: the package is current only while it matches. */
|
|
2334
|
+
reviewed: string;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
/** A package in the skill packages folder, read and checked; nothing installs from one with problems. */
|
|
2338
|
+
export interface SkillPackage {
|
|
2339
|
+
folder: string;
|
|
2340
|
+
/**
|
|
2341
|
+
* new: no skill has its name. current: the installed skill is what it holds, and the package is as it was reviewed.
|
|
2342
|
+
* update: it holds a text that wasn't installed, or its details (version label, fields, other files) changed since
|
|
2343
|
+
* it was. changed: it is as it was installed, but the skill was changed here since. conflict: a skill with its name
|
|
2344
|
+
* was not installed from it. invalid: it has problems.
|
|
2345
|
+
*/
|
|
2346
|
+
state: 'new' | 'current' | 'update' | 'changed' | 'conflict' | 'invalid';
|
|
2347
|
+
problems: string[];
|
|
2348
|
+
name: string | null;
|
|
2349
|
+
description: string | null;
|
|
2350
|
+
body: string | null;
|
|
2351
|
+
label: string | null;
|
|
2352
|
+
/** The same hash as Skill.version, of its description and body; null when it can't be read. */
|
|
2353
|
+
version: string | null;
|
|
2354
|
+
/** A hash of all of it the owner reviews: description, procedure, version label, fields and other files; null when it can't be read. */
|
|
2355
|
+
fingerprint: string | null;
|
|
2356
|
+
fields: Record<string, string>;
|
|
2357
|
+
/** Files in the folder besides SKILL.md: never installed, read or run. */
|
|
2358
|
+
otherFiles: string[];
|
|
2359
|
+
/** What it asks for or points at (fields Botons doesn't grant, sites, Boton commands), one line each. */
|
|
2360
|
+
asks: string[];
|
|
2361
|
+
/** Of `asks`, the ones the installed skill doesn't have: all of them for a new skill. */
|
|
2362
|
+
newAsks: string[];
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/** A package with what installing it changes: the lines of its description, procedure and details against the skill's. */
|
|
2366
|
+
export interface SkillPackageReview extends SkillPackage {
|
|
2367
|
+
installed: Skill | null;
|
|
2368
|
+
/** `details`: its version label, fields and other files, one line each, against those the skill was installed with. */
|
|
2369
|
+
diff: { description: DiffLine[]; body: DiffLine[]; details: DiffLine[] };
|
|
2370
|
+
/** Who would use it: every Boton, as for SkillAccess. */
|
|
2371
|
+
botons: SkillBotonAccess[];
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
export interface DiffLine {
|
|
2375
|
+
op: 'same' | 'add' | 'remove';
|
|
2376
|
+
text: string;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
export interface SkillPackagesList {
|
|
2380
|
+
/** The folder on the server it reads, for the owner to put package folders in. */
|
|
2381
|
+
dir: string;
|
|
2382
|
+
exists: boolean;
|
|
2383
|
+
packages: SkillPackage[];
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* Install or update a skill from its package, exactly as reviewed: `version` and `fingerprint` are the package's,
|
|
2388
|
+
* `current` the installed skill's (null for a new one), `accept` the package's newAsks, and `botIds`/`expected` who
|
|
2389
|
+
* gets it, as for SkillAccessInput. Anything that changed since is refused with 409 and nothing changes.
|
|
2390
|
+
*/
|
|
2391
|
+
export interface InstallSkillInput {
|
|
2392
|
+
version: string;
|
|
2393
|
+
fingerprint: string;
|
|
2394
|
+
current: string | null;
|
|
2395
|
+
accept: string[];
|
|
2396
|
+
botIds: string[];
|
|
2397
|
+
expected: string[];
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
export interface SkillBotonAccess {
|
|
2401
|
+
botId: string;
|
|
2402
|
+
name: string;
|
|
2403
|
+
has: boolean;
|
|
2404
|
+
/** Its skills are "every skill, including new ones": taking one away lists every other skill for it instead. */
|
|
2405
|
+
every: boolean;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
export interface SkillAccess {
|
|
2409
|
+
name: string;
|
|
2410
|
+
botons: SkillBotonAccess[];
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/** The Botons that should have the skill, exactly; `expected` is who had it as shown (409 if that changed). */
|
|
2414
|
+
export interface SkillAccessInput {
|
|
2415
|
+
botIds: string[];
|
|
2416
|
+
expected: string[];
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/** An earlier text of a skill, kept when it was replaced (newest first, a few per skill). */
|
|
2420
|
+
export interface SkillHistoryEntry {
|
|
2421
|
+
id: number;
|
|
2422
|
+
version: string;
|
|
2423
|
+
description: string;
|
|
2424
|
+
body: string;
|
|
2425
|
+
source: SkillSource | null;
|
|
2426
|
+
replacedAt: string;
|
|
2427
|
+
replacedBy: SkillAuthor;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
export interface SkillDetails {
|
|
2431
|
+
skill: Skill;
|
|
2432
|
+
access: SkillAccess;
|
|
2433
|
+
history: SkillHistoryEntry[];
|
|
2434
|
+
audit: AuditEvent[];
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
export interface CreateSkillInput {
|
|
2438
|
+
name: string;
|
|
2439
|
+
description: string;
|
|
2440
|
+
body: string;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
export interface UpdateSkillInput {
|
|
2444
|
+
description?: string;
|
|
2445
|
+
body?: string;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// ------------------------------------------------------------------ the sidebar
|
|
2449
|
+
|
|
2450
|
+
/**
|
|
2451
|
+
* How the owner arranges Botons in the sidebar, the same on every device: GET /api/sidebar, and `sidebar.updated`
|
|
2452
|
+
* with the whole layout after every change. Every Boton is in exactly one of `groups[].botIds` and
|
|
2453
|
+
* `ungrouped.botIds`, in order; pinning is apart from that: `pinned.botIds` lists the pinned ones in their own order,
|
|
2454
|
+
* and each stays in its group too, where unpinning leaves it.
|
|
2455
|
+
*/
|
|
2456
|
+
export interface SidebarLayout {
|
|
2457
|
+
pinned: SidebarSection;
|
|
2458
|
+
groups: SidebarGroup[];
|
|
2459
|
+
ungrouped: SidebarSection;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
export interface SidebarSection {
|
|
2463
|
+
collapsed: boolean;
|
|
2464
|
+
botIds: string[];
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
export interface SidebarGroup extends SidebarSection {
|
|
2468
|
+
id: string;
|
|
2469
|
+
name: string;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/** The sections that aren't groups. */
|
|
2473
|
+
export type SidebarSectionKey = 'pinned' | 'ungrouped';
|
|
2474
|
+
|
|
2475
|
+
/**
|
|
2476
|
+
* `index`, everywhere: the place in the list it moves within or into, counted once it is there (0: first). Missing
|
|
2477
|
+
* or past the end: last.
|
|
2478
|
+
*/
|
|
2479
|
+
export interface CreateSidebarGroupInput {
|
|
2480
|
+
name: string;
|
|
2481
|
+
index?: number;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
/** POST /api/sidebar/groups answers with the new group's id and the layout. */
|
|
2485
|
+
export interface CreatedSidebarGroup {
|
|
2486
|
+
groupId: string;
|
|
2487
|
+
layout: SidebarLayout;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
export interface UpdateSidebarGroupInput {
|
|
2491
|
+
name?: string;
|
|
2492
|
+
collapsed?: boolean;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
export interface MoveSidebarGroupInput {
|
|
2496
|
+
index?: number;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
/** groupId null: ungrouped. */
|
|
2500
|
+
export interface MoveSidebarBotInput {
|
|
2501
|
+
groupId: string | null;
|
|
2502
|
+
index?: number;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
export interface PinSidebarBotInput {
|
|
2506
|
+
pinned: boolean;
|
|
2507
|
+
index?: number;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
export interface UpdateSidebarSectionInput {
|
|
2511
|
+
collapsed: boolean;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/**
|
|
2515
|
+
* The layout as a file of its own, apart from backups: GET /api/sidebar/export gives it, and POST
|
|
2516
|
+
* /api/sidebar/import puts it back, all of it or (on any problem in the file) none of it. Botons are by id: an id
|
|
2517
|
+
* with no Boton here is skipped and listed in the answer; a Boton the file doesn't mention keeps its pin and goes
|
|
2518
|
+
* last among the ungrouped. Groups are by name (matched as group names are, in any case): a group of the same name
|
|
2519
|
+
* keeps its id, the others are made, and a group the file doesn't have is deleted (its Botons are placed as above).
|
|
2520
|
+
*/
|
|
2521
|
+
export interface SidebarTransfer {
|
|
2522
|
+
format: 'botons-sidebar';
|
|
2523
|
+
version: 1;
|
|
2524
|
+
/** When it was exported; not read on import. */
|
|
2525
|
+
exportedAt?: string;
|
|
2526
|
+
pinned: SidebarSection;
|
|
2527
|
+
groups: SidebarTransferGroup[];
|
|
2528
|
+
ungrouped: SidebarSection;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
export interface SidebarTransferGroup extends SidebarSection {
|
|
2532
|
+
name: string;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
/** POST /api/sidebar/import answers with the layout and the ids in the file that are no Boton here. */
|
|
2536
|
+
export interface SidebarImportResult {
|
|
2537
|
+
layout: SidebarLayout;
|
|
2538
|
+
skipped: string[];
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
/*
|
|
2542
|
+
* A portable configuration (format "botons-config"): the owner's deliberate setup, to move to another server or
|
|
2543
|
+
* rebuild one: Botons and their routines, skills, connectors, settings and the sidebar. Never a secret, chat,
|
|
2544
|
+
* memory, workspace, token, session or sign-in: a full backup (npm run backup) holds those. Its version is its own,
|
|
2545
|
+
* apart from the database's schema. Botons are named in it by `ref`, which only the file's other parts use.
|
|
2546
|
+
*/
|
|
2547
|
+
export interface PortableConfig {
|
|
2548
|
+
format: 'botons-config';
|
|
2549
|
+
/** This format's own version, apart from the database schema. Older files are upgraded as they are read: a version-1 file gets no templates and no reaction setting, a version-2 file no spending caps. */
|
|
2550
|
+
version: 5;
|
|
2551
|
+
exportedAt: string;
|
|
2552
|
+
/** The Botons that made it; read for the preview only. */
|
|
2553
|
+
source: { app: string; schema: number };
|
|
2554
|
+
settings: PortableSettings;
|
|
2555
|
+
botons: PortableBoton[];
|
|
2556
|
+
skills: PortableSkill[];
|
|
2557
|
+
connectors: PortableConnector[];
|
|
2558
|
+
/** As GET /api/sidebar/export has it, with Boton refs in place of Boton ids. */
|
|
2559
|
+
sidebar: SidebarTransfer;
|
|
2560
|
+
/** The owner's saved templates (never the built-in ones). */
|
|
2561
|
+
templates: PortableTemplate[];
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
/** A saved template: a New Boton form filled in, with its routines. Nothing in it runs or grants anything until the owner makes a Boton from it. */
|
|
2565
|
+
export interface PortableTemplate {
|
|
2566
|
+
ref: string;
|
|
2567
|
+
name: string;
|
|
2568
|
+
description: string;
|
|
2569
|
+
bot: TemplateBot;
|
|
2570
|
+
routines: Omit<TemplateRoutine, 'scheduleText'>[];
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
export interface PortableSettings {
|
|
2574
|
+
ownerName: string;
|
|
2575
|
+
timezone: string;
|
|
2576
|
+
/** A Boton's ref, or null. */
|
|
2577
|
+
primaryBoton: string | null;
|
|
2578
|
+
ntfy: { enabled: boolean; server: string; topic: string; preview: boolean; tokenLeftOut: boolean };
|
|
2579
|
+
/** Settings.reactionFollowUps; null when the file doesn't say (a version-1 file), which keeps this server's. */
|
|
2580
|
+
reactionFollowUps: string[] | null;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
export interface PortableBoton {
|
|
2584
|
+
ref: string;
|
|
2585
|
+
name: string;
|
|
2586
|
+
title: string;
|
|
2587
|
+
instructions: string;
|
|
2588
|
+
look: BotonLook;
|
|
2589
|
+
provider: string;
|
|
2590
|
+
model: string;
|
|
2591
|
+
effort: string;
|
|
2592
|
+
access: AccessLevel;
|
|
2593
|
+
timezone: string;
|
|
2594
|
+
paused: boolean;
|
|
2595
|
+
/** Connector ids ("lib:<name>", "oauth:<name>", the managed browser's). */
|
|
2596
|
+
connections: string[];
|
|
2597
|
+
/** Skill names, or null for every skill. */
|
|
2598
|
+
skills: string[] | null;
|
|
2599
|
+
askFirst: string[];
|
|
2600
|
+
alwaysAllow: string[];
|
|
2601
|
+
/** The managed browser's sites. */
|
|
2602
|
+
browserSites: string[];
|
|
2603
|
+
/** Its spending cap a day on API-key turns (SpendingView.dailyCapUsd); null for none (and in files before version 3). */
|
|
2604
|
+
dailyCapUsd: number | null;
|
|
2605
|
+
routines: PortableRoutine[];
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
export interface PortableRoutine extends Omit<TemplateRoutine, 'scheduleText'> {
|
|
2609
|
+
/** Whether it was on where it was exported: an imported routine is always off until the owner turns it on. */
|
|
2610
|
+
enabled: boolean;
|
|
2611
|
+
/** It had a signing secret (webhookSecret) or a callback URL, which the file never holds. */
|
|
2612
|
+
webhookSecretLeftOut: boolean;
|
|
2613
|
+
callbackLeftOut: boolean;
|
|
2614
|
+
/** Its spending cap a run (SpendingView.routines); null for none (and in files before version 3). */
|
|
2615
|
+
perRunCapUsd: number | null;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
export interface PortableSkill {
|
|
2619
|
+
name: string;
|
|
2620
|
+
description: string;
|
|
2621
|
+
body: string;
|
|
2622
|
+
disabled: boolean;
|
|
2623
|
+
/** Who changed it last: the owner, or a Boton (by ref, null if that Boton isn't in the file), whose skill waits for approval. */
|
|
2624
|
+
author: { kind: 'owner' } | { kind: 'boton'; ref: string | null; name: string };
|
|
2625
|
+
source: SkillSource | null;
|
|
2626
|
+
/** Earlier texts, newest first. */
|
|
2627
|
+
history: { description: string; body: string; source: SkillSource | null; replacedAt: string }[];
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
interface PortableConnectorBase {
|
|
2631
|
+
id: string;
|
|
2632
|
+
name: string;
|
|
2633
|
+
onRequest: boolean;
|
|
2634
|
+
grant: ConnectorGrant;
|
|
2635
|
+
enabled: boolean;
|
|
2636
|
+
/** What the export left out of it (a URL's query, an argument that looked like a credential). */
|
|
2637
|
+
leftOut: string[];
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
export type PortableConnector =
|
|
2641
|
+
| (PortableConnectorBase & {
|
|
2642
|
+
kind: 'library';
|
|
2643
|
+
transport: 'stdio' | 'http';
|
|
2644
|
+
command: string;
|
|
2645
|
+
args: string[];
|
|
2646
|
+
url: string;
|
|
2647
|
+
/** The names its secrets had; never their values. */
|
|
2648
|
+
envKeys: string[];
|
|
2649
|
+
headerKeys: string[];
|
|
2650
|
+
})
|
|
2651
|
+
| (PortableConnectorBase & { kind: 'oauth'; url: string; keepConnected: boolean; unknownJobs: 'nowhere' | 'primary' });
|
|
2652
|
+
|
|
2653
|
+
/** GET /api/config/export: the file, and what to check before keeping or sharing it. */
|
|
2654
|
+
export interface PortableExport {
|
|
2655
|
+
file: PortableConfig;
|
|
2656
|
+
/** Where something left out of the file was (a connector's secrets, a URL query): placeholders stand there. */
|
|
2657
|
+
leftOut: string[];
|
|
2658
|
+
/** Text the owner wrote that holds something shaped like a credential: the file has it as written. */
|
|
2659
|
+
textWarnings: string[];
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
export type PortableBotonAction = 'create' | 'skip';
|
|
2663
|
+
export type PortableSkillAction = 'create' | 'replace' | 'keep' | 'skip';
|
|
2664
|
+
export type PortableConnectorAction = 'create' | 'keep' | 'skip';
|
|
2665
|
+
export type PortableSidebarMode = 'replace' | 'merge' | 'skip';
|
|
2666
|
+
export type PortableTemplateAction = 'create' | 'skip';
|
|
2667
|
+
export type PortableCapsChoice = 'file' | 'none';
|
|
2668
|
+
|
|
2669
|
+
/** What the owner chose in the preview; anything left out takes the preview's default. */
|
|
2670
|
+
export interface PortableImportChoices {
|
|
2671
|
+
/**
|
|
2672
|
+
* `caps`: "file" (the default) gives the new Boton and its routines the file's spending caps; "none" leaves them
|
|
2673
|
+
* all out, which only the owner chooses. No choice raises a cap or touches one a Boton here already has.
|
|
2674
|
+
*/
|
|
2675
|
+
botons?: Record<string, { action?: PortableBotonAction; provider?: string; fullAccess?: boolean; alwaysAllow?: boolean; caps?: PortableCapsChoice }>;
|
|
2676
|
+
skills?: Record<string, PortableSkillAction>;
|
|
2677
|
+
connectors?: Record<string, PortableConnectorAction>;
|
|
2678
|
+
/** By template ref. */
|
|
2679
|
+
templates?: Record<string, PortableTemplateAction>;
|
|
2680
|
+
settings?: boolean;
|
|
2681
|
+
sidebar?: PortableSidebarMode;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
export interface PortableImportInput {
|
|
2685
|
+
file: unknown;
|
|
2686
|
+
choices?: PortableImportChoices;
|
|
2687
|
+
/** PortableImportPreview.fingerprint of the preview the owner confirmed (import only). */
|
|
2688
|
+
fingerprint?: string;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
/** Something to do by hand after an import: a secret to enter, a sign-in, a routine to turn on. */
|
|
2692
|
+
export interface PortableReconnect {
|
|
2693
|
+
kind: 'connector-secret' | 'connector-sign-in' | 'connector-off' | 'connector-left-out' | 'webhook' | 'routine-off' | 'ntfy-token' | 'brain';
|
|
2694
|
+
target: string;
|
|
2695
|
+
text: string;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
export interface PortableBotonPlan {
|
|
2699
|
+
ref: string;
|
|
2700
|
+
name: string;
|
|
2701
|
+
action: PortableBotonAction;
|
|
2702
|
+
/** An existing Boton has this name. */
|
|
2703
|
+
conflict: boolean;
|
|
2704
|
+
provider: { file: string; imported: string; available: boolean; options: string[] };
|
|
2705
|
+
model: { file: string; imported: string };
|
|
2706
|
+
effort: { file: string; imported: string };
|
|
2707
|
+
access: { file: AccessLevel; imported: AccessLevel };
|
|
2708
|
+
alwaysAllow: { file: string[]; imported: string[] };
|
|
2709
|
+
askFirst: string[];
|
|
2710
|
+
paused: boolean;
|
|
2711
|
+
/** `live`: the imported ones already set up and on here, which the Boton can use at once with this server's secrets or sign-in. */
|
|
2712
|
+
connections: { imported: string[]; live: string[]; dropped: { id: string; why: string }[] };
|
|
2713
|
+
skills: { imported: string[] | null; dropped: { name: string; why: string }[] };
|
|
2714
|
+
browserSites: { imported: string[]; dropped: string[] };
|
|
2715
|
+
/**
|
|
2716
|
+
* Its spending caps as the file has them and as they will be here (null: no cap). `applies`: its brain here is an
|
|
2717
|
+
* API key, the only turns caps count. `unpriced`: a capped chat or routine model has no known price here, so those turns
|
|
2718
|
+
* are refused until it is changed. Spending counted where it was exported is never in the file: here it starts at $0.
|
|
2719
|
+
*/
|
|
2720
|
+
caps: { choice: PortableCapsChoice; daily: { file: number | null; imported: number | null }; applies: boolean; unpriced: boolean };
|
|
2721
|
+
routines: {
|
|
2722
|
+
name: string;
|
|
2723
|
+
perRunCap: { file: number | null; imported: number | null };
|
|
2724
|
+
/** How its schedule reads, or null for none. */
|
|
2725
|
+
schedule: string | null;
|
|
2726
|
+
webhook: boolean;
|
|
2727
|
+
fullAccessWebhook: { file: boolean; imported: boolean };
|
|
2728
|
+
wasEnabled: boolean;
|
|
2729
|
+
skill: { file: string | null; imported: string | null };
|
|
2730
|
+
model: { file: string | null; imported: string | null };
|
|
2731
|
+
effort: { file: string | null; imported: string | null };
|
|
2732
|
+
}[];
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
export interface PortableImportPreview {
|
|
2736
|
+
source: { format: 'botons-config'; version: number; app: string; schema: number; exportedAt: string };
|
|
2737
|
+
/** The file's version when it was older than this server's and was upgraded to read it; null otherwise. */
|
|
2738
|
+
upgradedFrom: number | null;
|
|
2739
|
+
/** The choices in effect, defaults filled in. */
|
|
2740
|
+
choices: Required<PortableImportChoices>;
|
|
2741
|
+
/** `notes`: what is kept as this server has it, and why. */
|
|
2742
|
+
settings: { apply: boolean; changes: { field: string; from: string; to: string }[]; notes: string[] };
|
|
2743
|
+
botons: PortableBotonPlan[];
|
|
2744
|
+
skills: { name: string; action: PortableSkillAction; exists: boolean; sameText: boolean; disabled: boolean; pending: boolean; source: string | null; history: number; asks: string[] }[];
|
|
2745
|
+
connectors: {
|
|
2746
|
+
id: string;
|
|
2747
|
+
kind: 'library' | 'oauth';
|
|
2748
|
+
name: string;
|
|
2749
|
+
action: PortableConnectorAction;
|
|
2750
|
+
exists: boolean;
|
|
2751
|
+
target: string;
|
|
2752
|
+
/** A command that would run in the turns of Botons that have it. */
|
|
2753
|
+
runsCommand: boolean;
|
|
2754
|
+
secrets: string[];
|
|
2755
|
+
leftOut: string[];
|
|
2756
|
+
onRequest: boolean;
|
|
2757
|
+
grant: ConnectorGrant;
|
|
2758
|
+
}[];
|
|
2759
|
+
sidebar: { mode: PortableSidebarMode; groups: string[] };
|
|
2760
|
+
/** How the file's spending caps arrive, in the owner's words. */
|
|
2761
|
+
spending: { notes: string[] };
|
|
2762
|
+
templates: {
|
|
2763
|
+
ref: string;
|
|
2764
|
+
name: string;
|
|
2765
|
+
action: PortableTemplateAction;
|
|
2766
|
+
/** A saved template here has this name and the same content. */
|
|
2767
|
+
same: boolean;
|
|
2768
|
+
routines: number;
|
|
2769
|
+
/** What the file had that the template here won't: Full computer access, Full access webhook runs, connectors or skills this server won't have, a brain it lacks. */
|
|
2770
|
+
narrowed: string[];
|
|
2771
|
+
}[];
|
|
2772
|
+
textWarnings: string[];
|
|
2773
|
+
reconnect: PortableReconnect[];
|
|
2774
|
+
/** Send it back with the import: it is refused if anything shown here changed since. */
|
|
2775
|
+
fingerprint: string;
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
export interface PortableImportResult {
|
|
2779
|
+
/** `caps`: the spending caps it was made with (routines by their new ids). */
|
|
2780
|
+
botons: { ref: string; botId: string; name: string; caps: { dailyCapUsd: number | null; routines: { id: string; name: string; perRunCapUsd: number }[] } }[];
|
|
2781
|
+
skills: string[];
|
|
2782
|
+
connectors: string[];
|
|
2783
|
+
/** Names of the templates made. */
|
|
2784
|
+
templates: string[];
|
|
2785
|
+
settings: boolean;
|
|
2786
|
+
sidebar: PortableSidebarMode;
|
|
2787
|
+
reconnect: PortableReconnect[];
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
/** GET /api/events: one JSON object per `data:` line; `hello` comes first on every connection. */
|
|
2791
|
+
export type ServerEvent =
|
|
2792
|
+
| { type: 'hello'; serverTime: string; build: string | null }
|
|
2793
|
+
| { type: 'ping'; serverTime: string }
|
|
2794
|
+
| { type: 'bot.upserted'; bot: Bot }
|
|
2795
|
+
| { type: 'bot.deleted'; botId: string }
|
|
2796
|
+
| { type: 'bot.status'; botId: string; status: BotStatus }
|
|
2797
|
+
| { type: 'message.created'; message: Message }
|
|
2798
|
+
/** A message's meta or reactions changed (pinned or unpinned, a card's state, a reaction added or taken off): its text never does. */
|
|
2799
|
+
| { type: 'message.updated'; message: Message }
|
|
2800
|
+
| { type: 'message.draft'; botId: string; draft: TurnDraft }
|
|
2801
|
+
| { type: 'routine.upserted'; routine: Routine }
|
|
2802
|
+
| { type: 'routine.deleted'; routineId: string; botId: string }
|
|
2803
|
+
| { type: 'run.upserted'; run: RoutineRun }
|
|
2804
|
+
| { type: 'memory.upserted'; memory: Memory }
|
|
2805
|
+
| { type: 'memory.deleted'; memoryId: string; botId: string }
|
|
2806
|
+
| { type: 'settings.updated'; settings: Settings }
|
|
2807
|
+
| { type: 'skill.upserted'; skill: Skill }
|
|
2808
|
+
| { type: 'skill.deleted'; name: string }
|
|
2809
|
+
| { type: 'approval.upserted'; approval: Approval }
|
|
2810
|
+
| { type: 'oauth.upserted'; connection: OAuthConnection }
|
|
2811
|
+
| { type: 'oauth.deleted'; id: string }
|
|
2812
|
+
| { type: 'sidebar.updated'; layout: SidebarLayout }
|
|
2813
|
+
| { type: 'api_keys.updated'; view: ApiKeysView };
|
|
2814
|
+
|
|
2815
|
+
// ------------------------------------------------------------------ the managed browser
|
|
2816
|
+
|
|
2817
|
+
/**
|
|
2818
|
+
* The sites one Boton's managed browser may load, set by the owner. Nothing is reachable until a site is listed.
|
|
2819
|
+
* "example.com" also covers its subdomains; "host:port" names one address, and is the only way to name a loopback
|
|
2820
|
+
* or private one; "*" is any public site, Full computer Botons only. The browser itself is on for a Boton when the
|
|
2821
|
+
* owner gives it the Botons browser connector (BROWSER_CONNECTOR in its connectors).
|
|
2822
|
+
*/
|
|
2823
|
+
export interface BrowserPolicy {
|
|
2824
|
+
sites: string[];
|
|
2825
|
+
updatedAt: string | null;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
export interface BrowserPolicyInput {
|
|
2829
|
+
sites?: string[];
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
export interface BrowserStatus {
|
|
2833
|
+
botId: string;
|
|
2834
|
+
/** The connector that turns it on: in the Boton's connectors, its turns get the browser tools. */
|
|
2835
|
+
connectorId: string;
|
|
2836
|
+
/** It has the connector, and a brain that can use connectors. */
|
|
2837
|
+
enabled: boolean;
|
|
2838
|
+
policy: BrowserPolicy;
|
|
2839
|
+
/** Whether this server can run a browser at all; `why` says what is missing when it can't. */
|
|
2840
|
+
available: boolean;
|
|
2841
|
+
why: string | null;
|
|
2842
|
+
/** The Chromium it runs, when available. */
|
|
2843
|
+
executable: string | null;
|
|
2844
|
+
/** Chromium's own sandbox is on (BOTONS_BROWSER_SANDBOX=0 turns it off, where a host can't start it). */
|
|
2845
|
+
sandbox: boolean;
|
|
2846
|
+
headed: boolean;
|
|
2847
|
+
/** Its browser is open now: only during one of its turns. */
|
|
2848
|
+
running: boolean;
|
|
2849
|
+
/** Whether a Workspace Boton's limits apply to its sites. */
|
|
2850
|
+
workspace: boolean;
|
|
2851
|
+
/** What the browser can do, in the owner's words: it reads, and sends nothing without the owner's card. */
|
|
2852
|
+
capability: string;
|
|
2853
|
+
/** Desktop control (screen, mouse and keyboard of the host) is not offered; why. */
|
|
2854
|
+
desktop: string;
|
|
2855
|
+
/** The owner has the browser now (to sign it in, say): the Boton can't use it until they finish. */
|
|
2856
|
+
handover: BrowserHandover | null;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
/**
|
|
2860
|
+
* The owner driving a Boton's browser themselves, from one paired device: started and finished only by the owner,
|
|
2861
|
+
* never by a Boton. While it lasts the Boton's browser tools are refused; the profile (and any sign-in) stays theirs.
|
|
2862
|
+
*/
|
|
2863
|
+
export interface BrowserHandover {
|
|
2864
|
+
/** Sent with each of the owner's inputs, so one meant for a handover that has ended never reaches the next. */
|
|
2865
|
+
id: string;
|
|
2866
|
+
startedAt: string;
|
|
2867
|
+
/** The device that drives it: the only one that gets its view and sends it input (any paired device can end it). */
|
|
2868
|
+
byThisDevice: boolean;
|
|
2869
|
+
deviceLabel: string;
|
|
2870
|
+
/** Where its page is now (origin and path only), once one is open. */
|
|
2871
|
+
page: { url: string; title: string } | null;
|
|
2872
|
+
/** When it ends on its own if the owner sends nothing more. */
|
|
2873
|
+
idleEndsAt: string;
|
|
2874
|
+
/** The page's size now (CSS pixels): points are sent in these. */
|
|
2875
|
+
viewport: { width: number; height: number };
|
|
2876
|
+
/** The Boton's page to go back to (origin and path only; the server opens it in full), or null. */
|
|
2877
|
+
resume: string | null;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
/** What the owner's device asks for as it takes the browser over: its view's size (CSS pixels), for the page to fit. */
|
|
2881
|
+
export interface BrowserHandoverStart {
|
|
2882
|
+
width?: number;
|
|
2883
|
+
height?: number;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
/** The keys the owner can press in a handover (HANDOVER_KEYS in shared/util.ts): never a shortcut with a modifier. */
|
|
2887
|
+
export type HandoverKey = 'Enter' | 'Tab' | 'Shift+Tab' | 'Backspace' | 'Delete' | 'Escape' | 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'Home' | 'End' | 'PageUp' | 'PageDown' | 'Space';
|
|
2888
|
+
|
|
2889
|
+
/** One of the owner's inputs in a handover: fixed actions only (no script, no page DOM, no cookies). */
|
|
2890
|
+
/** Held with a key in a handover: Control only for editing shortcuts, and Meta is Control (a Mac's shortcuts). */
|
|
2891
|
+
export type HandoverMod = 'Shift' | 'Control' | 'Meta';
|
|
2892
|
+
|
|
2893
|
+
/** One of the owner's inputs in a handover. Points are in the page's CSS pixels. */
|
|
2894
|
+
export type BrowserHandoverInput = { handover: string } & (
|
|
2895
|
+
| { action: 'open'; url: string }
|
|
2896
|
+
/** The Boton's own page as it handed over (the server keeps its full address; the owner asks for it). */
|
|
2897
|
+
| { action: 'resume' }
|
|
2898
|
+
| { action: 'click'; x: number; y: number; count?: number }
|
|
2899
|
+
| { action: 'down' | 'up'; x: number; y: number; button: 'left' | 'right' | 'middle'; count?: number }
|
|
2900
|
+
| { action: 'move'; x: number; y: number }
|
|
2901
|
+
| { action: 'wheel'; x: number; y: number; dx: number; dy: number }
|
|
2902
|
+
/** Typed into whatever has focus. Never kept, logged or shown again by Botons. */
|
|
2903
|
+
| { action: 'type'; text: string }
|
|
2904
|
+
/** The owner's own clipboard text, put into whatever has focus (new lines kept). Never kept, logged or shown again. */
|
|
2905
|
+
| { action: 'paste'; text: string }
|
|
2906
|
+
/** The owner's selection in the page, given back to this device only (never from a password field). */
|
|
2907
|
+
| { action: 'copy' }
|
|
2908
|
+
/** A named key, or a letter with Control for an editing shortcut. */
|
|
2909
|
+
| { action: 'key'; key: HandoverKey | string; mods?: HandoverMod[] }
|
|
2910
|
+
| { action: 'scroll'; dy: number }
|
|
2911
|
+
| { action: 'back' }
|
|
2912
|
+
| { action: 'forward' }
|
|
2913
|
+
| { action: 'reload' }
|
|
2914
|
+
/** The owner's view changed size: the page follows it (within bounds). */
|
|
2915
|
+
| { action: 'resize'; width: number; height: number }
|
|
2916
|
+
);
|
|
2917
|
+
|
|
2918
|
+
export interface BrowserHandoverResult {
|
|
2919
|
+
outcome: 'done' | 'refused';
|
|
2920
|
+
message: string;
|
|
2921
|
+
page: { url: string; title: string } | null;
|
|
2922
|
+
/** copy: the selected text, for this device's clipboard. */
|
|
2923
|
+
copied?: string;
|
|
2924
|
+
/** click, up: whether what has focus now takes text (so a phone shows its keyboard). Only true or false, never what it holds. */
|
|
2925
|
+
editable?: boolean;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/**
|
|
2929
|
+
* What the live view's stream says besides its frames (one JSON message each, see BrowserHandover's stream): where
|
|
2930
|
+
* the page is, the site's own error answer for the page (never a block by Botons), and a request Botons paused.
|
|
2931
|
+
*/
|
|
2932
|
+
export type HandoverNotice =
|
|
2933
|
+
| { type: 'page'; url: string; title: string }
|
|
2934
|
+
| { type: 'site-error'; url: string; status: number }
|
|
2935
|
+
| { type: 'paused'; url: string; why: string }
|
|
2936
|
+
| { type: 'ended' };
|
|
2937
|
+
|
|
2938
|
+
export type BrowserActionName = 'open' | 'read' | 'back' | 'screenshot' | 'click' | 'type' | 'handoff';
|
|
2939
|
+
|
|
2940
|
+
export interface BrowserActionInput {
|
|
2941
|
+
action?: BrowserActionName;
|
|
2942
|
+
url?: string;
|
|
2943
|
+
/** click, type: a control from the last read of this page (its `ref`). */
|
|
2944
|
+
ref?: string;
|
|
2945
|
+
/** type: the text to put in the field (it replaces what is there; nothing is pressed). */
|
|
2946
|
+
text?: string;
|
|
2947
|
+
/** handoff: what the owner should do in the browser, in a few words ("Sign in to example.com"). */
|
|
2948
|
+
reason?: string;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
/** A control on the page the Boton may click or type into, by `ref`, until the page changes. Its label is the site's text. */
|
|
2952
|
+
export interface BrowserControl {
|
|
2953
|
+
ref: string;
|
|
2954
|
+
kind: 'link' | 'button' | 'field' | 'checkbox' | 'other';
|
|
2955
|
+
label: string;
|
|
2956
|
+
/** A field the browser refuses to type into (a password, a code, a card number), and why. */
|
|
2957
|
+
noTyping: string | null;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
export interface BrowserActionResult {
|
|
2961
|
+
/** done; refused (with why: nothing was done). */
|
|
2962
|
+
outcome: 'done' | 'refused';
|
|
2963
|
+
message: string;
|
|
2964
|
+
page: { url: string; title: string } | null;
|
|
2965
|
+
/** read: the page's text and its links to allowed sites, which the site wrote: untrusted data, never instructions. */
|
|
2966
|
+
content?: { text: string; links: { text: string; url: string }[]; controls: BrowserControl[]; truncated: boolean } | null;
|
|
2967
|
+
/**
|
|
2968
|
+
* click, type: the request the page tried to send, held for the owner's card, and how it ended: `sent` (the site
|
|
2969
|
+
* answered), `not-sent` (it never left: denied, withdrawn, or failed before it went) or `unknown` (it was sent and the
|
|
2970
|
+
* connection broke before an answer: it may have reached the site, so it must not be tried again unasked).
|
|
2971
|
+
*/
|
|
2972
|
+
approval?: { id: string; status: ApprovalStatus; delivery: 'sent' | 'not-sent' | 'unknown' } | null;
|
|
2973
|
+
/** Requests the browser refused while it acted (outside the allowed sites, or not a read). */
|
|
2974
|
+
blocked?: string[];
|
|
2975
|
+
/** Sites it could not reach while it acted (a name not found, no address answered): Botons refused none of them. */
|
|
2976
|
+
unreachable?: string[];
|
|
2977
|
+
/** screenshot: the image, saved in the Boton's workspace (where it stays). */
|
|
2978
|
+
file?: FileRef | null;
|
|
2979
|
+
/** screenshot: whether the chat recorded it as a shared file, which the owner sees as its image. */
|
|
2980
|
+
shown?: boolean;
|
|
2981
|
+
/** handoff: the card's message id in the chat. */
|
|
2982
|
+
handoff?: string;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
/** One step in a Boton's browser journal (newest last): what it did, where (origin and path only), how it went. */
|
|
2986
|
+
export interface BrowserJournalEntry {
|
|
2987
|
+
at: string;
|
|
2988
|
+
action: BrowserActionName | 'blocked' | 'unreachable' | 'download' | 'stopped' | 'sent' | 'handover';
|
|
2989
|
+
url: string;
|
|
2990
|
+
outcome: 'done' | 'refused';
|
|
2991
|
+
detail: string;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
/** Metadata only: neither the linked file nor its contents are sent to the browser. */
|
|
2995
|
+
export interface CodexConfigRepair {
|
|
2996
|
+
id: string;
|
|
2997
|
+
path: string;
|
|
2998
|
+
target: string | null;
|
|
2999
|
+
backup: string | null;
|
|
3000
|
+
token: string | null;
|
|
3001
|
+
problem: string | null;
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
export interface CodexConfigRepairResult extends CodexConfigRepair {
|
|
3005
|
+
/** Replacement succeeded, but recording its completion failed. */
|
|
3006
|
+
warning?: string;
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
export interface BrainCliInstallReview { provider: 'codex-cli' | 'claude-cli'; command: string; bin: string; available: boolean; reason: string | null }
|
|
3010
|
+
|
|
3011
|
+
export interface BrainReleaseStatus {
|
|
3012
|
+
installs?: BrainCliInstallReview[];
|
|
3013
|
+
installing?: boolean;
|
|
3014
|
+
checks: Partial<Record<'claude-cli' | 'codex-cli' | 'botons', { latest: string | null; checkedAt: number; failed: boolean }>>;
|
|
3015
|
+
botonsVersion: string | null;
|
|
3016
|
+
}
|