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,882 @@
|
|
|
1
|
+
import { serverCommand } from './install-type.ts';
|
|
2
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { lookFor } from '../../shared/look.ts';
|
|
4
|
+
import { taterLookFor } from '../../shared/look-tater.ts';
|
|
5
|
+
import { newId } from './util.ts';
|
|
6
|
+
import { RENAMED } from '../../shared/legacy-names.mjs';
|
|
7
|
+
|
|
8
|
+
export const SCHEMA_VERSION = 35;
|
|
9
|
+
|
|
10
|
+
// Approvals is rebuilt by the upgrade from 20 (a check constraint can't be altered), so it is kept apart.
|
|
11
|
+
const APPROVALS = `create table approvals (
|
|
12
|
+
id text primary key,
|
|
13
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
14
|
+
kind text not null check (kind in ('action', 'skill', 'connector')),
|
|
15
|
+
title text not null,
|
|
16
|
+
details text not null default '',
|
|
17
|
+
follow_up text not null default '',
|
|
18
|
+
action text,
|
|
19
|
+
skill_name text,
|
|
20
|
+
status text not null check (status in ('pending', 'allowed', 'denied', 'expired', 'cancelled')),
|
|
21
|
+
always integer not null default 0,
|
|
22
|
+
auto integer not null default 0,
|
|
23
|
+
note text,
|
|
24
|
+
trigger text not null,
|
|
25
|
+
turn_id text,
|
|
26
|
+
routine_id text,
|
|
27
|
+
routine_name text,
|
|
28
|
+
run_id text,
|
|
29
|
+
sandboxed integer not null default 0,
|
|
30
|
+
asked_by text not null default '[]',
|
|
31
|
+
message_id text,
|
|
32
|
+
decided_by text,
|
|
33
|
+
notifications text not null default '[]',
|
|
34
|
+
created_at text not null,
|
|
35
|
+
expires_at text,
|
|
36
|
+
decided_at text,
|
|
37
|
+
tool_name text,
|
|
38
|
+
tool_input text,
|
|
39
|
+
tool_key text,
|
|
40
|
+
tool_rule text,
|
|
41
|
+
wait_until text,
|
|
42
|
+
in_turn integer not null default 0,
|
|
43
|
+
used_at text,
|
|
44
|
+
skill_version text,
|
|
45
|
+
connector_id text,
|
|
46
|
+
connector_generation integer
|
|
47
|
+
);
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const APPROVAL_INDEXES = `create index approvals_bot on approvals(bot_id, created_at);
|
|
51
|
+
create index approvals_pending on approvals(status, expires_at) where status = 'pending';
|
|
52
|
+
create index approvals_tool on approvals(bot_id, tool_key) where tool_key is not null;
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
/** New in 21: OAuth connectors, which connectors Botons may ask for, and work started through a connector. */
|
|
56
|
+
const CONNECTORS_21 = `
|
|
57
|
+
create table oauth_connections (
|
|
58
|
+
id text primary key,
|
|
59
|
+
name text not null unique,
|
|
60
|
+
url text not null,
|
|
61
|
+
status text not null check (status in ('new', 'connected', 'reconnect')),
|
|
62
|
+
status_reason text,
|
|
63
|
+
generation integer not null default 0,
|
|
64
|
+
resource text not null default '',
|
|
65
|
+
issuer text not null default '',
|
|
66
|
+
as_metadata text not null default '{}',
|
|
67
|
+
client_id text not null default '',
|
|
68
|
+
client_for text not null default '',
|
|
69
|
+
scope text not null default '',
|
|
70
|
+
access_token text,
|
|
71
|
+
access_expires_at text,
|
|
72
|
+
access_lifetime_ms integer,
|
|
73
|
+
refresh_token text,
|
|
74
|
+
refreshed_at text,
|
|
75
|
+
refresh_error text,
|
|
76
|
+
refresh_error_kind text,
|
|
77
|
+
refresh_retry_at text,
|
|
78
|
+
refresh_started_at text,
|
|
79
|
+
keep_connected integer not null default 1,
|
|
80
|
+
keepalive_at text,
|
|
81
|
+
notified_generation integer,
|
|
82
|
+
unknown_jobs text not null default 'nowhere' check (unknown_jobs in ('nowhere', 'primary')),
|
|
83
|
+
feed integer not null default 0,
|
|
84
|
+
feed_cursor text,
|
|
85
|
+
feed_ready integer not null default 0,
|
|
86
|
+
feed_checked_at text,
|
|
87
|
+
feed_error text,
|
|
88
|
+
created_at text not null,
|
|
89
|
+
updated_at text not null
|
|
90
|
+
);
|
|
91
|
+
create table oauth_flows (
|
|
92
|
+
state_hash text primary key,
|
|
93
|
+
connection_id text not null references oauth_connections(id) on delete cascade,
|
|
94
|
+
device_id text not null,
|
|
95
|
+
generation integer not null,
|
|
96
|
+
issuer text not null,
|
|
97
|
+
resource text not null,
|
|
98
|
+
metadata text not null,
|
|
99
|
+
scope text not null default '',
|
|
100
|
+
client_id text not null,
|
|
101
|
+
redirect_uri text not null,
|
|
102
|
+
code_verifier text not null,
|
|
103
|
+
return_to text not null,
|
|
104
|
+
created_at text not null,
|
|
105
|
+
expires_at text not null
|
|
106
|
+
);
|
|
107
|
+
create index oauth_flows_expiry on oauth_flows(expires_at);
|
|
108
|
+
create table connector_policies (
|
|
109
|
+
connector_id text primary key,
|
|
110
|
+
on_request integer not null default 0,
|
|
111
|
+
grant_mode text not null default 'approval' check (grant_mode in ('approval', 'immediate')),
|
|
112
|
+
generation integer not null default 1,
|
|
113
|
+
updated_at text not null
|
|
114
|
+
);
|
|
115
|
+
create table work_jobs (
|
|
116
|
+
connection_id text not null references oauth_connections(id) on delete cascade,
|
|
117
|
+
job_id text not null,
|
|
118
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
119
|
+
call_id text,
|
|
120
|
+
created_at text not null,
|
|
121
|
+
primary key (connection_id, job_id)
|
|
122
|
+
);
|
|
123
|
+
create table work_feed_seen (
|
|
124
|
+
connection_id text not null references oauth_connections(id) on delete cascade,
|
|
125
|
+
key text not null,
|
|
126
|
+
value text not null default '',
|
|
127
|
+
at text not null,
|
|
128
|
+
primary key (connection_id, key)
|
|
129
|
+
);
|
|
130
|
+
create index work_feed_seen_at on work_feed_seen(connection_id, at);
|
|
131
|
+
create table work_feed_held (
|
|
132
|
+
connection_id text not null references oauth_connections(id) on delete cascade,
|
|
133
|
+
event_id text not null,
|
|
134
|
+
job_id text not null,
|
|
135
|
+
event text not null,
|
|
136
|
+
held_at text not null,
|
|
137
|
+
primary key (connection_id, event_id)
|
|
138
|
+
);
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* New in 22: the audit log of consequential actions (pairing, access, setup, approvals, connectors).
|
|
143
|
+
* Rows name who did what to which thing; they never hold a token, code, secret or message text.
|
|
144
|
+
*/
|
|
145
|
+
const AUDIT_EVENTS = `
|
|
146
|
+
create table audit_events (
|
|
147
|
+
seq integer primary key autoincrement,
|
|
148
|
+
at text not null,
|
|
149
|
+
actor_kind text not null check (actor_kind in ('device', 'boton', 'host', 'system')),
|
|
150
|
+
actor_id text,
|
|
151
|
+
actor_label text not null default '',
|
|
152
|
+
action text not null,
|
|
153
|
+
target_kind text,
|
|
154
|
+
target_id text,
|
|
155
|
+
target_label text not null default '',
|
|
156
|
+
details text not null default '{}'
|
|
157
|
+
);
|
|
158
|
+
create index audit_events_at on audit_events(at);
|
|
159
|
+
`;
|
|
160
|
+
const AUDIT_22 = AUDIT_EVENTS;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* New in 22: a device asking to pair. It holds the request's secret (only its hash is here) and shows
|
|
164
|
+
* the code; the owner approves the code on the server's machine or on a paired device, and only then can
|
|
165
|
+
* the holder of the secret turn it into a device token. A code alone never pairs anything.
|
|
166
|
+
*/
|
|
167
|
+
const PAIRING_22 = `
|
|
168
|
+
create table pairing_requests (
|
|
169
|
+
secret_hash text primary key,
|
|
170
|
+
code_hash text not null unique,
|
|
171
|
+
label text not null default '',
|
|
172
|
+
created_at text not null,
|
|
173
|
+
expires_at text not null,
|
|
174
|
+
approved_at text,
|
|
175
|
+
approved_label text
|
|
176
|
+
);
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* New in 23: what the owner should know about a Boton's connectors, one row per Boton and connector id
|
|
181
|
+
* (ConnectorNotice in shared/types.ts). The Connectors class owns it.
|
|
182
|
+
*/
|
|
183
|
+
const NOTICES_23 = `
|
|
184
|
+
create table connector_notices (
|
|
185
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
186
|
+
connector_id text not null,
|
|
187
|
+
kind text not null check (kind in ('removed', 'skipped')),
|
|
188
|
+
name text not null,
|
|
189
|
+
target text not null default '',
|
|
190
|
+
reason text not null,
|
|
191
|
+
at text not null,
|
|
192
|
+
primary key (bot_id, connector_id)
|
|
193
|
+
);
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A sidebar group name as it is compared: two names are the same group when their keys are equal. Unicode case
|
|
198
|
+
* folding in JavaScript, after NFC (so "Équipe", "ÉQUIPE" and a decomposed "équipe" are one group), because SQLite's
|
|
199
|
+
* nocase folds ASCII only. The upgrade, the lookups and the unique index (on name_key) all use it.
|
|
200
|
+
*/
|
|
201
|
+
export function sidebarGroupKey(name: string): string {
|
|
202
|
+
return name.normalize('NFC').toLowerCase().toUpperCase().toLowerCase().normalize('NFC');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* New in 27: the owner's sidebar groups, in their order, each collapsed or not, names unique by sidebarGroupKey. A
|
|
207
|
+
* Boton is in at most one (bots.group_id; none is ungrouped) at bots.position among its section, and pinned ones
|
|
208
|
+
* are ordered by bots.pin_position. Whether Pinned and Ungrouped are collapsed are settings.
|
|
209
|
+
*/
|
|
210
|
+
const SIDEBAR_27 = `create table sidebar_groups (
|
|
211
|
+
id text primary key,
|
|
212
|
+
name text not null,
|
|
213
|
+
name_key text not null,
|
|
214
|
+
position integer not null,
|
|
215
|
+
collapsed integer not null default 0,
|
|
216
|
+
created_at text not null,
|
|
217
|
+
updated_at text not null
|
|
218
|
+
);
|
|
219
|
+
create unique index sidebar_groups_name on sidebar_groups(name_key);
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* New in 28: the texts a skill had before it was replaced, so the owner can put one back (a few per skill; skills.ts
|
|
224
|
+
* keeps them).
|
|
225
|
+
*/
|
|
226
|
+
const SKILL_HISTORY_28 = `
|
|
227
|
+
create table skill_history (
|
|
228
|
+
id integer primary key autoincrement,
|
|
229
|
+
name text not null,
|
|
230
|
+
version text not null,
|
|
231
|
+
description text not null,
|
|
232
|
+
body text not null,
|
|
233
|
+
source text,
|
|
234
|
+
replaced_at text not null,
|
|
235
|
+
replaced_by text not null
|
|
236
|
+
);
|
|
237
|
+
create index skill_history_name on skill_history (name, id);
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A message's meta as SQL, read as JSON only when it is: like parseJson, a meta that isn't JSON reads as none, so it
|
|
242
|
+
* can't fail a write, an index or a query (json_extract throws on malformed text).
|
|
243
|
+
*/
|
|
244
|
+
export const MESSAGE_META = 'case when json_valid(meta) then meta end';
|
|
245
|
+
/** A pinned message (store.ts's setMessagePinned): only the owner's and the Boton's messages carry a pin. */
|
|
246
|
+
export const PINNED_MESSAGE = `json_extract(${MESSAGE_META}, '$.pinned') is not null and role in ('user', 'bot')`;
|
|
247
|
+
/** A message about a routine (its run's output, or an event about it): the replay reads these apart from the chat. */
|
|
248
|
+
export const ROUTINE_MESSAGE = `json_extract(${MESSAGE_META}, '$.routineId') is not null`;
|
|
249
|
+
/** Any other message: the chat a replay reads, newest first, apart from routine messages. */
|
|
250
|
+
export const CHAT_MESSAGE = `json_extract(${MESSAGE_META}, '$.routineId') is null`;
|
|
251
|
+
/** The event store.ts's resetContext adds as it starts the chat fresh: every message after it is since the reset. */
|
|
252
|
+
export const RESET_EVENT = `json_extract(${MESSAGE_META}, '$.kind') = 'session.reset' and role = 'event'`;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* New in 29: a chat's pins, its routine messages and the rest, each in seq order, so a new session's replay, the
|
|
256
|
+
* pins' reads and a reset read only their own rows rather than scanning past many others for a few (routine output
|
|
257
|
+
* can far outnumber the chat, and pins are a handful); and its resets by time, so a read since a reset starts at its
|
|
258
|
+
* event rather than reading every message before it to find none newer. SQLite uses a partial index only for a query
|
|
259
|
+
* whose WHERE repeats its condition, so store.ts builds its queries from these same constants.
|
|
260
|
+
*/
|
|
261
|
+
const MESSAGE_INDEXES_29 = `create index messages_pinned on messages(bot_id, seq) where ${PINNED_MESSAGE};
|
|
262
|
+
create index messages_routine on messages(bot_id, seq) where ${ROUTINE_MESSAGE};
|
|
263
|
+
create index messages_chat on messages(bot_id, seq) where ${CHAT_MESSAGE};
|
|
264
|
+
create index messages_reset on messages(bot_id, created_at) where ${RESET_EVENT};
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* New in 30: emoji reactions on chat messages. The owner reacts to the Boton's messages and the Boton to the
|
|
269
|
+
* owner's, each emoji at most once per message by each, and the Boton with one emoji at most. told_at is when one of
|
|
270
|
+
* the Boton's turns was given an owner's reaction as feedback (null: not yet). reaction_followups keeps which
|
|
271
|
+
* reactions on which message started a follow-up turn, so taking one off and adding it again starts none.
|
|
272
|
+
*/
|
|
273
|
+
const MESSAGE_REACTIONS = `create table message_reactions (
|
|
274
|
+
message_id text not null references messages(id) on delete cascade,
|
|
275
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
276
|
+
actor text not null check (actor in ('owner', 'boton')),
|
|
277
|
+
emoji text not null,
|
|
278
|
+
created_at text not null,
|
|
279
|
+
told_at text,
|
|
280
|
+
primary key (message_id, actor, emoji)
|
|
281
|
+
);
|
|
282
|
+
create unique index message_reactions_boton on message_reactions(message_id) where actor = 'boton';
|
|
283
|
+
create index message_reactions_untold on message_reactions(bot_id, created_at) where actor = 'owner' and told_at is null;
|
|
284
|
+
`;
|
|
285
|
+
const REACTIONS_30 = `${MESSAGE_REACTIONS}create table reaction_followups (
|
|
286
|
+
message_id text not null references messages(id) on delete cascade,
|
|
287
|
+
emoji text not null,
|
|
288
|
+
at text not null,
|
|
289
|
+
primary key (message_id, emoji)
|
|
290
|
+
);
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* New in 31: the account of API-key requests (spend.ts), one row per request to a model, for the spending caps. `at`
|
|
295
|
+
* is when it was reserved, just before it was sent: the day it counts on. `state`: reserved (sent or about to be),
|
|
296
|
+
* settled (`cost_usd` is what its reported tokens cost), held (its tokens are unknown: it counts at `reserved_usd`) or
|
|
297
|
+
* released (refused before anything was generated). `reserved_usd` null: a model with no known price.
|
|
298
|
+
*/
|
|
299
|
+
const API_REQUESTS_31 = `create table api_requests (
|
|
300
|
+
id text primary key,
|
|
301
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
302
|
+
turn_id text not null,
|
|
303
|
+
run_id text,
|
|
304
|
+
routine_id text,
|
|
305
|
+
provider text not null,
|
|
306
|
+
model text not null,
|
|
307
|
+
at text not null,
|
|
308
|
+
reserved_usd real,
|
|
309
|
+
cost_usd real,
|
|
310
|
+
state text not null check (state in ('reserved', 'settled', 'held', 'released')),
|
|
311
|
+
ended_at text
|
|
312
|
+
);
|
|
313
|
+
create index api_requests_bot on api_requests(bot_id, at);
|
|
314
|
+
create index api_requests_run on api_requests(run_id) where run_id is not null;
|
|
315
|
+
`;
|
|
316
|
+
|
|
317
|
+
const AUTH_TOKENS = `create table auth_tokens (
|
|
318
|
+
id text primary key,
|
|
319
|
+
hash text not null unique,
|
|
320
|
+
kind text not null check (kind in ('device', 'boton')),
|
|
321
|
+
bot_id text references bots(id) on delete cascade,
|
|
322
|
+
label text not null default '',
|
|
323
|
+
created_at text not null,
|
|
324
|
+
last_used_at text,
|
|
325
|
+
check ((kind = 'boton') = (bot_id is not null))
|
|
326
|
+
);
|
|
327
|
+
create index auth_tokens_bot on auth_tokens(bot_id);
|
|
328
|
+
`;
|
|
329
|
+
|
|
330
|
+
const SCHEMA = `
|
|
331
|
+
create table settings (
|
|
332
|
+
key text primary key,
|
|
333
|
+
value text not null
|
|
334
|
+
);
|
|
335
|
+
create table bots (
|
|
336
|
+
id text primary key,
|
|
337
|
+
name text not null,
|
|
338
|
+
title text not null default '',
|
|
339
|
+
instructions text not null default '',
|
|
340
|
+
look text not null,
|
|
341
|
+
provider text not null,
|
|
342
|
+
model text not null default '',
|
|
343
|
+
access text not null default 'workspace',
|
|
344
|
+
pinned integer not null default 0,
|
|
345
|
+
timezone text not null,
|
|
346
|
+
workspace text not null,
|
|
347
|
+
session_id text,
|
|
348
|
+
session_key text,
|
|
349
|
+
context_since text,
|
|
350
|
+
created_at text not null,
|
|
351
|
+
updated_at text not null,
|
|
352
|
+
last_activity_at text not null,
|
|
353
|
+
read_seq integer not null default 0,
|
|
354
|
+
paused integer not null default 0,
|
|
355
|
+
connections text not null default '[]',
|
|
356
|
+
skills text,
|
|
357
|
+
always_allow text not null default '[]',
|
|
358
|
+
ask_first text not null default '[]'
|
|
359
|
+
, effort text not null default '', group_id text references sidebar_groups(id) on delete set null, position integer not null default 0, pin_position integer not null default 0, previous_look text, tater_look text);
|
|
360
|
+
create table messages (
|
|
361
|
+
seq integer primary key autoincrement,
|
|
362
|
+
id text not null unique,
|
|
363
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
364
|
+
role text not null,
|
|
365
|
+
text text not null,
|
|
366
|
+
meta text,
|
|
367
|
+
pending integer not null default 0,
|
|
368
|
+
created_at text not null,
|
|
369
|
+
cut_off integer not null default 0
|
|
370
|
+
);
|
|
371
|
+
create index messages_bot on messages(bot_id, seq);
|
|
372
|
+
create index messages_pending on messages(bot_id, pending) where pending = 1;
|
|
373
|
+
create table routines (
|
|
374
|
+
id text primary key,
|
|
375
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
376
|
+
name text not null,
|
|
377
|
+
instruction text not null,
|
|
378
|
+
schedule text,
|
|
379
|
+
timezone text not null,
|
|
380
|
+
webhook_key text,
|
|
381
|
+
enabled integer not null default 1,
|
|
382
|
+
next_run_at text,
|
|
383
|
+
last_run_at text,
|
|
384
|
+
last_status text,
|
|
385
|
+
created_at text not null,
|
|
386
|
+
updated_at text not null,
|
|
387
|
+
notify text not null default 'auto',
|
|
388
|
+
webhook_workspace integer not null default 1,
|
|
389
|
+
webhook_source text not null default 'generic',
|
|
390
|
+
webhook_secret text,
|
|
391
|
+
webhook_filter text,
|
|
392
|
+
callback_url text,
|
|
393
|
+
skill text,
|
|
394
|
+
model text,
|
|
395
|
+
session text not null default 'chat',
|
|
396
|
+
session_id text,
|
|
397
|
+
session_key text
|
|
398
|
+
, time_limit_ms integer, effort text, webhook_history integer not null default 0);
|
|
399
|
+
create index routines_due on routines(enabled, next_run_at);
|
|
400
|
+
create table routine_runs (
|
|
401
|
+
seq integer primary key autoincrement,
|
|
402
|
+
id text not null unique,
|
|
403
|
+
routine_id text not null references routines(id) on delete cascade,
|
|
404
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
405
|
+
trigger text not null,
|
|
406
|
+
status text not null,
|
|
407
|
+
error text,
|
|
408
|
+
payload text,
|
|
409
|
+
reply text,
|
|
410
|
+
started_at text not null,
|
|
411
|
+
finished_at text,
|
|
412
|
+
quiet integer not null default 0,
|
|
413
|
+
meta text,
|
|
414
|
+
attempts integer not null default 0,
|
|
415
|
+
last_error text,
|
|
416
|
+
retry_at text,
|
|
417
|
+
held_for text,
|
|
418
|
+
headers text
|
|
419
|
+
);
|
|
420
|
+
create index runs_routine on routine_runs(routine_id, seq);
|
|
421
|
+
create table memories (
|
|
422
|
+
id text primary key,
|
|
423
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
424
|
+
text text not null,
|
|
425
|
+
created_at text not null
|
|
426
|
+
);
|
|
427
|
+
${AUTH_TOKENS}create table provider_cooldowns (
|
|
428
|
+
provider text primary key,
|
|
429
|
+
until text not null,
|
|
430
|
+
reason text not null
|
|
431
|
+
);
|
|
432
|
+
create table webhook_deliveries (
|
|
433
|
+
routine_id text not null references routines(id) on delete cascade,
|
|
434
|
+
delivery_id text not null,
|
|
435
|
+
run_id text not null,
|
|
436
|
+
created_at text not null,
|
|
437
|
+
primary key (routine_id, delivery_id)
|
|
438
|
+
);
|
|
439
|
+
create index webhook_deliveries_created on webhook_deliveries(created_at);
|
|
440
|
+
create table mcp_servers (
|
|
441
|
+
name text primary key,
|
|
442
|
+
transport text not null check (transport in ('stdio', 'http')),
|
|
443
|
+
command text not null default '',
|
|
444
|
+
args text not null default '[]',
|
|
445
|
+
url text not null default '',
|
|
446
|
+
env text not null default '{}',
|
|
447
|
+
headers text not null default '{}',
|
|
448
|
+
created_at text not null,
|
|
449
|
+
updated_at text not null
|
|
450
|
+
);
|
|
451
|
+
create table skills (
|
|
452
|
+
name text primary key,
|
|
453
|
+
description text not null,
|
|
454
|
+
body text not null,
|
|
455
|
+
created_by text not null,
|
|
456
|
+
updated_by text not null,
|
|
457
|
+
created_at text not null,
|
|
458
|
+
updated_at text not null
|
|
459
|
+
, source text, disabled integer not null default 0);
|
|
460
|
+
create table turns (
|
|
461
|
+
id text primary key,
|
|
462
|
+
bot_id text not null references bots(id) on delete cascade,
|
|
463
|
+
routine_id text,
|
|
464
|
+
run_id text,
|
|
465
|
+
message_id text,
|
|
466
|
+
trigger text not null,
|
|
467
|
+
provider text not null,
|
|
468
|
+
requested_model text not null default '',
|
|
469
|
+
model text,
|
|
470
|
+
started_at text not null,
|
|
471
|
+
duration_ms integer,
|
|
472
|
+
outcome text not null,
|
|
473
|
+
error_kind text,
|
|
474
|
+
input_tokens integer not null default 0,
|
|
475
|
+
output_tokens integer not null default 0,
|
|
476
|
+
cache_read_tokens integer not null default 0,
|
|
477
|
+
cache_write_tokens integer not null default 0,
|
|
478
|
+
cost_usd real,
|
|
479
|
+
num_turns integer,
|
|
480
|
+
replayed_transcript integer not null default 0,
|
|
481
|
+
step_count integer not null default 0
|
|
482
|
+
);
|
|
483
|
+
create index turns_started on turns(started_at);
|
|
484
|
+
create index turns_bot on turns(bot_id, started_at);
|
|
485
|
+
create index turns_run on turns(run_id) where run_id is not null;
|
|
486
|
+
create index turns_message on turns(message_id) where message_id is not null;
|
|
487
|
+
create table turn_steps (
|
|
488
|
+
turn_id text not null references turns(id) on delete cascade,
|
|
489
|
+
idx integer not null,
|
|
490
|
+
kind text not null,
|
|
491
|
+
label text not null,
|
|
492
|
+
detail text not null,
|
|
493
|
+
at text not null,
|
|
494
|
+
primary key (turn_id, idx)
|
|
495
|
+
);
|
|
496
|
+
${APPROVALS}${APPROVAL_INDEXES}${CONNECTORS_21}${AUDIT_22}${PAIRING_22}${NOTICES_23}${SIDEBAR_27}${SKILL_HISTORY_28}${MESSAGE_INDEXES_29}${REACTIONS_30}${API_REQUESTS_31}`;
|
|
497
|
+
|
|
498
|
+
/** The columns approvals had at 20, all copied as they are. */
|
|
499
|
+
const APPROVAL_COLUMNS_20 =
|
|
500
|
+
'id, bot_id, kind, title, details, follow_up, action, skill_name, status, always, auto, note, trigger, turn_id, routine_id, routine_name, run_id, sandboxed, asked_by, message_id, decided_by, notifications, created_at, expires_at, decided_at, tool_name, tool_input, tool_key, tool_rule, wait_until, in_turn, used_at, skill_version';
|
|
501
|
+
|
|
502
|
+
/** 20 to 21 in place, inside openSchema's transaction: approvals may be connector requests now, and the new tables. */
|
|
503
|
+
function upgradeFrom20(db: DatabaseSync): void {
|
|
504
|
+
db.exec(`alter table approvals rename to approvals_20;
|
|
505
|
+
${APPROVALS}
|
|
506
|
+
insert into approvals (${APPROVAL_COLUMNS_20}) select ${APPROVAL_COLUMNS_20} from approvals_20 order by rowid;
|
|
507
|
+
drop table approvals_20;
|
|
508
|
+
${APPROVAL_INDEXES}${CONNECTORS_21}
|
|
509
|
+
pragma user_version = 21;`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* 21 to 22 in place, inside openSchema's transaction: new tables, and pairing_codes is dropped. Pairing takes
|
|
514
|
+
* the owner's approval of a request, since a bearer code pairs whoever types it (a Boton that saw one in chat
|
|
515
|
+
* included); any code in it expires within minutes anyway. Every other row stays as it is.
|
|
516
|
+
*/
|
|
517
|
+
function upgradeFrom21(db: DatabaseSync): void {
|
|
518
|
+
db.exec(`${AUDIT_22}${PAIRING_22}
|
|
519
|
+
drop table pairing_codes;
|
|
520
|
+
pragma user_version = 22;`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Connector ids from a provider CLI's own MCP setup, which Botons no longer uses. */
|
|
524
|
+
const CLI_CONNECTOR = /^(claude|codex):(.+)$/;
|
|
525
|
+
const CLI_NAME: Record<string, string> = { claude: 'Claude Code', codex: 'Codex' };
|
|
526
|
+
|
|
527
|
+
/** Where a server from a CLI's setup pointed, for the owner: an address without query or credentials, or a command's name. */
|
|
528
|
+
function cliTarget(config: unknown): string {
|
|
529
|
+
const c = config && typeof config === 'object' ? (config as Record<string, unknown>) : {};
|
|
530
|
+
if (typeof c.url === 'string') {
|
|
531
|
+
try {
|
|
532
|
+
const u = new URL(c.url);
|
|
533
|
+
return `${u.protocol}//${u.host}${u.pathname}`;
|
|
534
|
+
} catch {
|
|
535
|
+
return '';
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return typeof c.command === 'string' ? (c.command.split('/').pop() ?? '') : '';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* 22 to 23 in place, inside openSchema's transaction. Every connector is Botons' own now, so a Boton's
|
|
543
|
+
* connectors from a CLI's own MCP setup ("claude:<name>", "codex:<name>") come off it, and each becomes a
|
|
544
|
+
* "removed" notice the owner sees in the Boton's settings, with where it pointed, so nothing is dropped
|
|
545
|
+
* unreported. Their request policies go, a pending request for one is withdrawn, and the saved lookups of
|
|
546
|
+
* the CLIs' servers (discovered_connectors) are dropped: nothing reads a CLI's MCP setup any more.
|
|
547
|
+
*/
|
|
548
|
+
function upgradeFrom22(db: DatabaseSync): void {
|
|
549
|
+
db.exec(NOTICES_23);
|
|
550
|
+
const targets = new Map<string, string>();
|
|
551
|
+
for (const r of db.prepare('select provider, servers from discovered_connectors').all()) {
|
|
552
|
+
const cli = String(r.provider).replace(/-cli$/, '');
|
|
553
|
+
let servers: unknown = [];
|
|
554
|
+
try {
|
|
555
|
+
servers = JSON.parse(String(r.servers));
|
|
556
|
+
} catch {}
|
|
557
|
+
if (!Array.isArray(servers)) continue;
|
|
558
|
+
for (const x of servers) if (x && typeof x.name === 'string') targets.set(`${cli}:${x.name}`, cliTarget(x.config));
|
|
559
|
+
}
|
|
560
|
+
const now = new Date().toISOString();
|
|
561
|
+
const notice = db.prepare("insert or ignore into connector_notices (bot_id, connector_id, kind, name, target, reason, at) values (?, ?, 'removed', ?, ?, ?, ?)");
|
|
562
|
+
const save = db.prepare('update bots set connections = ? where id = ?');
|
|
563
|
+
for (const b of db.prepare('select id, connections from bots order by rowid').all()) {
|
|
564
|
+
let ids: unknown = [];
|
|
565
|
+
try {
|
|
566
|
+
ids = JSON.parse(String(b.connections));
|
|
567
|
+
} catch {}
|
|
568
|
+
if (!Array.isArray(ids)) continue;
|
|
569
|
+
const kept = ids.filter((id) => !(typeof id === 'string' && CLI_CONNECTOR.test(id)));
|
|
570
|
+
if (kept.length === ids.length) continue;
|
|
571
|
+
for (const id of ids) {
|
|
572
|
+
const m = typeof id === 'string' ? CLI_CONNECTOR.exec(id) : null;
|
|
573
|
+
if (!m) continue;
|
|
574
|
+
notice.run(b.id, id, m[2], targets.get(id) ?? '', `Botons no longer uses MCP servers from ${CLI_NAME[m[1]]}'s own settings, so it was taken off this Boton.`, now);
|
|
575
|
+
}
|
|
576
|
+
save.run(JSON.stringify(kept), b.id);
|
|
577
|
+
}
|
|
578
|
+
db.prepare("update approvals set status = 'cancelled', decided_at = ? where kind = 'connector' and status = 'pending' and (connector_id like 'claude:%' or connector_id like 'codex:%')").run(now);
|
|
579
|
+
db.exec(`delete from connector_policies where connector_id like 'claude:%' or connector_id like 'codex:%';
|
|
580
|
+
drop table discovered_connectors;
|
|
581
|
+
pragma user_version = 23;`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 23 to 24 in place, inside openSchema's transaction: a routine may have its own time limit for a run. Every routine
|
|
586
|
+
* starts without one, so each keeps the server's limit (BOTONS_TURN_TIMEOUT_MS) as before; every row stays as it is.
|
|
587
|
+
* The column is written in SCHEMA as this "add column" leaves it, so a new database gets the same schema.
|
|
588
|
+
*/
|
|
589
|
+
function upgradeFrom23(db: DatabaseSync): void {
|
|
590
|
+
db.exec(`alter table routines add column time_limit_ms integer;
|
|
591
|
+
pragma user_version = 24;`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* 24 to 25 in place, inside openSchema's transaction: the version marks a new kind of look, which a release that
|
|
596
|
+
* draws capsules cannot read. Looks themselves change in 31 to 32, which reads a capsule as well.
|
|
597
|
+
*/
|
|
598
|
+
function upgradeFrom24(db: DatabaseSync): void {
|
|
599
|
+
db.exec('pragma user_version = 25;');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* 25 to 26 in place, inside openSchema's transaction: a Boton may set how hard its agent CLI thinks, and a routine
|
|
604
|
+
* may set its own. Every Boton starts at "" (the provider's default) and every routine at null (the Boton's), so
|
|
605
|
+
* each turn runs as before; every row stays as it is. The columns are written in SCHEMA as these "add column"s leave
|
|
606
|
+
* them, so a new database gets the same schema.
|
|
607
|
+
*/
|
|
608
|
+
function upgradeFrom25(db: DatabaseSync): void {
|
|
609
|
+
db.exec(`alter table bots add column effort text not null default '';
|
|
610
|
+
alter table routines add column effort text;
|
|
611
|
+
pragma user_version = 26;`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* 26 to 27 in place, inside openSchema's transaction: groups in the sidebar become their own rows, with an id that
|
|
616
|
+
* stays when they are renamed, an order and a collapsed state, and Botons keep an order there. Each group name in
|
|
617
|
+
* use becomes a group (names with the same sidebarGroupKey become one, spelled as the first in code-point order), the
|
|
618
|
+
* groups ordered by name, each group's Botons and the pinned ones by latest activity: the order the sidebar shows
|
|
619
|
+
* them in. bots.group_name goes; every other row stays as it is. The columns are written in SCHEMA as these
|
|
620
|
+
* "alter table"s leave them, so a new database gets the same schema.
|
|
621
|
+
*/
|
|
622
|
+
function upgradeFrom26(db: DatabaseSync): void {
|
|
623
|
+
db.exec(`${SIDEBAR_27}alter table bots add column group_id text references sidebar_groups(id) on delete set null;
|
|
624
|
+
alter table bots add column position integer not null default 0;
|
|
625
|
+
alter table bots add column pin_position integer not null default 0;`);
|
|
626
|
+
const now = new Date().toISOString();
|
|
627
|
+
const names = new Map<string, string>();
|
|
628
|
+
for (const r of db.prepare("select distinct group_name from bots where group_name != '' order by group_name").all()) {
|
|
629
|
+
const name = String(r.group_name);
|
|
630
|
+
if (!names.has(sidebarGroupKey(name))) names.set(sidebarGroupKey(name), name);
|
|
631
|
+
}
|
|
632
|
+
const sorted = [...names.values()].sort((a, b) => a.localeCompare(b));
|
|
633
|
+
const add = db.prepare('insert into sidebar_groups (id, name, name_key, position, collapsed, created_at, updated_at) values (?, ?, ?, ?, 0, ?, ?)');
|
|
634
|
+
const ids = new Map<string, string>();
|
|
635
|
+
sorted.forEach((name, i) => {
|
|
636
|
+
const id = newId('grp');
|
|
637
|
+
ids.set(sidebarGroupKey(name), id);
|
|
638
|
+
add.run(id, name, sidebarGroupKey(name), i, now, now);
|
|
639
|
+
});
|
|
640
|
+
const place = db.prepare('update bots set group_id = ?, position = ? where id = ?');
|
|
641
|
+
const next = new Map<string | null, number>();
|
|
642
|
+
for (const b of db.prepare('select id, group_name from bots order by last_activity_at desc, rowid').all()) {
|
|
643
|
+
const group = String(b.group_name) ? ids.get(sidebarGroupKey(String(b.group_name)))! : null;
|
|
644
|
+
const n = next.get(group) ?? 0;
|
|
645
|
+
next.set(group, n + 1);
|
|
646
|
+
place.run(group, n, b.id);
|
|
647
|
+
}
|
|
648
|
+
const pin = db.prepare('update bots set pin_position = ? where id = ?');
|
|
649
|
+
db.prepare('select id from bots where pinned = 1 order by last_activity_at desc, rowid')
|
|
650
|
+
.all()
|
|
651
|
+
.forEach((b, i) => pin.run(i, b.id));
|
|
652
|
+
db.exec(`alter table bots drop column group_name;
|
|
653
|
+
pragma user_version = 27;`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 27 to 28 in place, inside openSchema's transaction: a skill may come from a package (its source) and may be turned
|
|
658
|
+
* off, and replaced texts are kept. Every skill starts written here (no source) and on, so each Boton sees the skills
|
|
659
|
+
* it saw before; every row stays as it is. The columns are written in SCHEMA as these "add column"s leave them, so a
|
|
660
|
+
* new database gets the same schema.
|
|
661
|
+
*/
|
|
662
|
+
function upgradeFrom27(db: DatabaseSync): void {
|
|
663
|
+
db.exec(`alter table skills add column source text;
|
|
664
|
+
alter table skills add column disabled integer not null default 0;
|
|
665
|
+
${SKILL_HISTORY_28}pragma user_version = 28;`);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* 28 to 29 in place, inside openSchema's transaction: the messages' indexes for the replay. Rows are only
|
|
670
|
+
* read, so every message, pin, session and setting stays as it was; a meta that isn't JSON reads as none.
|
|
671
|
+
*/
|
|
672
|
+
function upgradeFrom28(db: DatabaseSync): void {
|
|
673
|
+
db.exec(`${MESSAGE_INDEXES_29}pragma user_version = 29;`);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* 29 to 30 in place, inside openSchema's transaction: the reactions' tables, empty. Every message, pin and setting
|
|
678
|
+
* stays as it was.
|
|
679
|
+
*/
|
|
680
|
+
function upgradeFrom29(db: DatabaseSync): void {
|
|
681
|
+
db.exec(`${REACTIONS_30}pragma user_version = 30;`);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* 30 to 31 in place, inside openSchema's transaction: the account of API-key requests. A turn on an API key already
|
|
686
|
+
* recorded becomes one settled row at its start, at the cost it recorded (or, with tokens and no cost, one with no
|
|
687
|
+
* known price), so today's spending and a run's stay what they were. Every other row stays as it was.
|
|
688
|
+
*/
|
|
689
|
+
function upgradeFrom30(db: DatabaseSync): void {
|
|
690
|
+
db.exec(`${API_REQUESTS_31}insert into api_requests (id, bot_id, turn_id, run_id, routine_id, provider, model, at, reserved_usd, cost_usd, state, ended_at)
|
|
691
|
+
select 'req_' || t.id, t.bot_id, t.id, t.run_id, t.routine_id, t.provider, coalesce(t.model, t.requested_model), t.started_at, t.cost_usd, t.cost_usd, 'settled', t.started_at
|
|
692
|
+
from turns t join bots b on b.id = t.bot_id
|
|
693
|
+
where t.provider in ('anthropic-api', 'openai-api') and (t.cost_usd is not null or t.input_tokens + t.output_tokens + t.cache_read_tokens + t.cache_write_tokens > 0);
|
|
694
|
+
pragma user_version = 31;`);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* 31 to 32 in place, inside openSchema's transaction: every Boton's look becomes a tater look. A Critter (or a capsule)
|
|
699
|
+
* maps to its closest tater look (taterLookFor in shared/look-tater.ts), the same on every machine; a look that can't
|
|
700
|
+
* be read gets one seeded by the Boton's id. The look it had is kept, as it was, in previous_look, so nothing is lost and the
|
|
701
|
+
* change can be undone. Only those two columns change: ids, names, sessions, grants, connectors, routines and every
|
|
702
|
+
* other row stay as they are.
|
|
703
|
+
*/
|
|
704
|
+
function upgradeFrom31(db: DatabaseSync): void {
|
|
705
|
+
db.exec('alter table bots add column previous_look text;');
|
|
706
|
+
const save = db.prepare('update bots set look = ?, previous_look = ? where id = ?');
|
|
707
|
+
for (const b of db.prepare('select id, look from bots order by rowid').all()) {
|
|
708
|
+
let stored: unknown = null;
|
|
709
|
+
try {
|
|
710
|
+
stored = JSON.parse(String(b.look));
|
|
711
|
+
} catch {}
|
|
712
|
+
save.run(JSON.stringify(taterLookFor(stored, String(b.id))), String(b.look), b.id);
|
|
713
|
+
}
|
|
714
|
+
db.exec('pragma user_version = 32;');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* 32 to 33 in place, inside openSchema's transaction: every Boton's look becomes a Boton. A tater look maps to its
|
|
719
|
+
* closest Boton (lookFor in shared/look.ts), field by field and the same on every machine; a look that is already a
|
|
720
|
+
* Boton stays as it is, and one that can't be read gets a Boton seeded by the Boton's id. The look it had is kept, as
|
|
721
|
+
* it was, in tater_look, so nothing is lost and the change can be undone (previous_look keeps what it holds). Only
|
|
722
|
+
* those two columns change: ids, names, sessions, grants, connectors, routines and every other row stay as they are.
|
|
723
|
+
*/
|
|
724
|
+
function upgradeFrom32(db: DatabaseSync): void {
|
|
725
|
+
db.exec('alter table bots add column tater_look text;');
|
|
726
|
+
const save = db.prepare('update bots set look = ?, tater_look = ? where id = ?');
|
|
727
|
+
for (const b of db.prepare('select id, look from bots order by rowid').all()) {
|
|
728
|
+
let stored: unknown = null;
|
|
729
|
+
try {
|
|
730
|
+
stored = JSON.parse(String(b.look));
|
|
731
|
+
} catch {}
|
|
732
|
+
save.run(JSON.stringify(lookFor(stored, String(b.id))), String(b.look), b.id);
|
|
733
|
+
}
|
|
734
|
+
db.exec('pragma user_version = 33;');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 33 to 34 in place, inside openSchema's transaction: each routine's webhook_history, how many of the chat's latest
|
|
739
|
+
* messages its webhook runs are given, starts at 0 (none) whatever its session: sharing the chat with a payload from
|
|
740
|
+
* outside is the owner's to turn on. Every other column and row stays as it was.
|
|
741
|
+
*/
|
|
742
|
+
function upgradeFrom33(db: DatabaseSync): void {
|
|
743
|
+
db.exec(`alter table routines add column webhook_history integer not null default 0;
|
|
744
|
+
pragma user_version = 34;`);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* 34 to 35 in place, inside openSchema's transaction: the names stored under the app's earlier name take its
|
|
749
|
+
* current one (shared/legacy-names.mjs). An actor's kind becomes "boton" in the audit log, reactions and turn
|
|
750
|
+
* tokens, and of a skill's author; the audit log's actions, target kinds and detail keys follow; and the managed browser's connector id is
|
|
751
|
+
* lib:botons_browser wherever a connector id is kept. The three tables whose CHECKs name the kind are rebuilt with
|
|
752
|
+
* the same rows, under the same definitions a new database gets. Nothing else changes.
|
|
753
|
+
*/
|
|
754
|
+
function upgradeFrom34(db: DatabaseSync): void {
|
|
755
|
+
const old = RENAMED.boton;
|
|
756
|
+
const kind = (col: string) => `case ${col} when '${old}' then 'boton' else ${col} end`;
|
|
757
|
+
db.exec(`drop index audit_events_at;
|
|
758
|
+
alter table audit_events rename to audit_events_34;
|
|
759
|
+
${AUDIT_EVENTS}
|
|
760
|
+
insert into audit_events (seq, at, actor_kind, actor_id, actor_label, action, target_kind, target_id, target_label, details)
|
|
761
|
+
select seq, at, ${kind('actor_kind')}, actor_id, actor_label,
|
|
762
|
+
case when action like '${old}.%' then 'boton.' || substr(action, ${old.length + 2}) else replace(action, '.${old}s_changed', '.botons_changed') end,
|
|
763
|
+
${kind('target_kind')}, target_id, target_label, replace(replace(details, '"${old}":', '"boton":'), '"${old}s":', '"botons":')
|
|
764
|
+
from audit_events_34 order by seq;
|
|
765
|
+
delete from sqlite_sequence where name = 'audit_events';
|
|
766
|
+
insert into sqlite_sequence (name, seq) select 'audit_events', seq from sqlite_sequence where name = 'audit_events_34';
|
|
767
|
+
drop table audit_events_34;
|
|
768
|
+
drop index if exists message_reactions_${old};
|
|
769
|
+
drop index if exists message_reactions_boton;
|
|
770
|
+
drop index message_reactions_untold;
|
|
771
|
+
alter table message_reactions rename to message_reactions_34;
|
|
772
|
+
${MESSAGE_REACTIONS}
|
|
773
|
+
insert into message_reactions (message_id, bot_id, actor, emoji, created_at, told_at)
|
|
774
|
+
select message_id, bot_id, ${kind('actor')}, emoji, created_at, told_at from message_reactions_34 order by rowid;
|
|
775
|
+
drop table message_reactions_34;
|
|
776
|
+
drop index auth_tokens_bot;
|
|
777
|
+
alter table auth_tokens rename to auth_tokens_34;
|
|
778
|
+
${AUTH_TOKENS}
|
|
779
|
+
insert into auth_tokens (id, hash, kind, bot_id, label, created_at, last_used_at)
|
|
780
|
+
select id, hash, ${kind('kind')}, bot_id, label, created_at, last_used_at from auth_tokens_34 order by rowid;
|
|
781
|
+
drop table auth_tokens_34;`);
|
|
782
|
+
// Who wrote a skill (or a text it replaced), kept as JSON with the author's kind.
|
|
783
|
+
const author = (col: string) => `${col} = replace(${col}, '"kind":"${old}"', '"kind":"boton"')`;
|
|
784
|
+
db.exec(`update skills set ${author('created_by')}, ${author('updated_by')};
|
|
785
|
+
update skill_history set ${author('replaced_by')};`);
|
|
786
|
+
// A connector id, wherever one is kept, from `from` to `to`: grants, policies, notices, approvals and whether it is off.
|
|
787
|
+
const renameConnector = (from: string, to: string) => {
|
|
788
|
+
for (const table of ['approvals', 'connector_policies', 'connector_notices']) db.prepare(`update ${table} set connector_id = ? where connector_id = ?`).run(to, from);
|
|
789
|
+
db.prepare('update bots set connections = replace(connections, ?, ?) where instr(connections, ?) > 0').run(JSON.stringify(from), JSON.stringify(to), JSON.stringify(from));
|
|
790
|
+
db.prepare('update settings set key = ? where key = ?').run(`connector_off:${to}`, `connector_off:${from}`);
|
|
791
|
+
};
|
|
792
|
+
// An id nothing refers to yet: no connector, grant, policy, notice, approval or off switch has it.
|
|
793
|
+
const taken = (id: string) =>
|
|
794
|
+
!!db.prepare('select 1 from mcp_servers where name = ?').get(id.slice(4)) ||
|
|
795
|
+
['approvals', 'connector_policies', 'connector_notices'].some((t) => db.prepare(`select 1 from ${t} where connector_id = ?`).get(id)) ||
|
|
796
|
+
!!db.prepare('select 1 from bots where instr(connections, ?) > 0').get(JSON.stringify(id)) ||
|
|
797
|
+
!!db.prepare('select 1 from settings where key = ?').get(`connector_off:${id}`);
|
|
798
|
+
// A connector of the owner's own that already has the browser's new name takes a free one first, with every grant,
|
|
799
|
+
// policy, notice and off switch of it, so none of them ever reads as the browser's.
|
|
800
|
+
if (db.prepare("select 1 from mcp_servers where name = 'botons_browser'").get()) {
|
|
801
|
+
let n = 2;
|
|
802
|
+
while (taken(`lib:botons_browser_${n}`)) n++;
|
|
803
|
+
renameConnector('lib:botons_browser', `lib:botons_browser_${n}`);
|
|
804
|
+
db.prepare("update mcp_servers set name = ? where name = 'botons_browser'").run(`botons_browser_${n}`);
|
|
805
|
+
}
|
|
806
|
+
renameConnector(`lib:${RENAMED.botons_browser}`, 'lib:botons_browser');
|
|
807
|
+
db.exec('pragma user_version = 35;');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** Each step takes the database one version up; openSchema runs them in order from the version it finds. */
|
|
811
|
+
const UPGRADES: Record<number, (db: DatabaseSync) => void> = {
|
|
812
|
+
20: upgradeFrom20,
|
|
813
|
+
21: upgradeFrom21,
|
|
814
|
+
22: upgradeFrom22,
|
|
815
|
+
23: upgradeFrom23,
|
|
816
|
+
24: upgradeFrom24,
|
|
817
|
+
25: upgradeFrom25,
|
|
818
|
+
26: upgradeFrom26,
|
|
819
|
+
27: upgradeFrom27,
|
|
820
|
+
28: upgradeFrom28,
|
|
821
|
+
29: upgradeFrom29,
|
|
822
|
+
30: upgradeFrom30,
|
|
823
|
+
31: upgradeFrom31,
|
|
824
|
+
32: upgradeFrom32,
|
|
825
|
+
33: upgradeFrom33,
|
|
826
|
+
34: upgradeFrom34,
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
/** The older versions openSchema upgrades in place. */
|
|
830
|
+
export const UPGRADABLE_VERSIONS: readonly number[] = Object.keys(UPGRADES).map(Number);
|
|
831
|
+
|
|
832
|
+
/** The database is not one this code can open. Restarting will not help. */
|
|
833
|
+
export class SchemaError extends Error {}
|
|
834
|
+
|
|
835
|
+
const userVersion = (db: DatabaseSync) => Number(db.prepare('pragma user_version').get()!.user_version);
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Use a database at SCHEMA_VERSION as it is, or create the schema in an empty one; refuse anything else. An older
|
|
839
|
+
* version it can upgrade is upgraded in place, after `beforeUpgrade` (a backup) has returned: it runs under the
|
|
840
|
+
* write lock, so nothing changes between the two, and if it throws the database is left as it was.
|
|
841
|
+
*/
|
|
842
|
+
export function openSchema(db: DatabaseSync, opts: { beforeUpgrade?: (version: number) => void } = {}): void {
|
|
843
|
+
if (userVersion(db) === SCHEMA_VERSION) return;
|
|
844
|
+
db.exec('begin immediate');
|
|
845
|
+
try {
|
|
846
|
+
// Read again under the lock: another process may have created it meanwhile.
|
|
847
|
+
const version = userVersion(db);
|
|
848
|
+
if (version === 0 && !db.prepare('select 1 from sqlite_master limit 1').get()) db.exec(`${SCHEMA}pragma user_version = ${SCHEMA_VERSION};`);
|
|
849
|
+
else if (UPGRADES[version]) {
|
|
850
|
+
try {
|
|
851
|
+
opts.beforeUpgrade?.(version);
|
|
852
|
+
} catch (err) {
|
|
853
|
+
throw new SchemaError(`The database is at schema version ${version} and needs upgrading to ${SCHEMA_VERSION}, but backing it up first failed (${(err as Error).message}), so it was left as it was. Fix that (or run \`${serverCommand('backup')}\` to see why) and start again.`);
|
|
854
|
+
}
|
|
855
|
+
for (let v = version; v < SCHEMA_VERSION; v++) {
|
|
856
|
+
try {
|
|
857
|
+
UPGRADES[v](db);
|
|
858
|
+
} catch (err) {
|
|
859
|
+
throw new SchemaError(`The database is at schema version ${version}, but upgrading it to ${SCHEMA_VERSION} failed (${(err as Error).message}), so it was left as it was. Restore a backup (see "Backups" in the README).`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
else if (version !== SCHEMA_VERSION) throw new SchemaError(refusal(version));
|
|
864
|
+
db.exec('commit');
|
|
865
|
+
} catch (err) {
|
|
866
|
+
if (db.isTransaction) db.exec('rollback');
|
|
867
|
+
throw err;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function refusal(version: number): string {
|
|
872
|
+
if (version > SCHEMA_VERSION) {
|
|
873
|
+
return (
|
|
874
|
+
`The database is at schema version ${version}, but this version of Botons only knows versions up to ${SCHEMA_VERSION}. ` +
|
|
875
|
+
'It was upgraded by a newer Botons. Run that newer version, or restore a backup taken before the upgrade (see "Backups" in the README).'
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return (
|
|
879
|
+
`The database ${version ? `is at schema version ${version}` : 'has no schema version'}, but this version of Botons only opens schema version ${SCHEMA_VERSION} (or ${Object.keys(UPGRADES).join(' or ')}, which it upgrades) or an empty database. ` +
|
|
880
|
+
`Restore a backup at version ${SCHEMA_VERSION} (see "Backups" in the README), or set BOTONS_HOME to an empty folder to start over.`
|
|
881
|
+
);
|
|
882
|
+
}
|