@michael-joseph-miller/ant-bot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +267 -0
  4. package/dist/browser-OHRD7YI3.js +524 -0
  5. package/dist/browser-OHRD7YI3.js.map +7 -0
  6. package/dist/bundled-46DUK5PG.js +133 -0
  7. package/dist/bundled-46DUK5PG.js.map +7 -0
  8. package/dist/chunk-7BOHBPB2.js +287 -0
  9. package/dist/chunk-7BOHBPB2.js.map +7 -0
  10. package/dist/chunk-AHAON6J7.js +24 -0
  11. package/dist/chunk-AHAON6J7.js.map +7 -0
  12. package/dist/chunk-DYIJTUMY.js +321 -0
  13. package/dist/chunk-DYIJTUMY.js.map +7 -0
  14. package/dist/chunk-KSQOVWP5.js +310 -0
  15. package/dist/chunk-KSQOVWP5.js.map +7 -0
  16. package/dist/chunk-Z2YT2PZN.js +64 -0
  17. package/dist/chunk-Z2YT2PZN.js.map +7 -0
  18. package/dist/index.js +1562 -0
  19. package/dist/index.js.map +7 -0
  20. package/dist/install-IY2M3OUQ.js +36 -0
  21. package/dist/install-IY2M3OUQ.js.map +7 -0
  22. package/dist/plugin-POMHVGD4.js +53 -0
  23. package/dist/plugin-POMHVGD4.js.map +7 -0
  24. package/dist/scheduler-Q7OHNGP6.js +274 -0
  25. package/dist/scheduler-Q7OHNGP6.js.map +7 -0
  26. package/dist/server.js +3016 -0
  27. package/dist/server.js.map +7 -0
  28. package/dist/skills-66WRX64H.js +17 -0
  29. package/dist/skills-66WRX64H.js.map +7 -0
  30. package/dist/skills-spec.js +172 -0
  31. package/dist/skills-spec.js.map +7 -0
  32. package/dist/tools-P5537ASX.js +148 -0
  33. package/dist/tools-P5537ASX.js.map +7 -0
  34. package/package.json +51 -0
  35. package/skills/README.md +69 -0
  36. package/skills/SPEC.md +274 -0
  37. package/skills/bug-repro/SKILL.md +53 -0
  38. package/skills/deep-research/SKILL.md +451 -0
  39. package/skills/deep-research/references/V6_1_improvements.md +112 -0
  40. package/skills/deep-research/references/completeness_review_checklist.md +25 -0
  41. package/skills/deep-research/references/counter_review_team_guide.md +181 -0
  42. package/skills/deep-research/references/enterprise_analysis_frameworks.md +135 -0
  43. package/skills/deep-research/references/enterprise_mode.md +99 -0
  44. package/skills/deep-research/references/enterprise_quality_checklist.md +160 -0
  45. package/skills/deep-research/references/enterprise_research_methodology.md +164 -0
  46. package/skills/deep-research/references/formatting_rules.md +31 -0
  47. package/skills/deep-research/references/quality_gates.md +77 -0
  48. package/skills/deep-research/references/report_template_v6.md +82 -0
  49. package/skills/deep-research/references/research_notes_format.md +147 -0
  50. package/skills/deep-research/references/research_plan_checklist.md +26 -0
  51. package/skills/deep-research/references/research_report_template.md +49 -0
  52. package/skills/deep-research/references/source_accessibility_policy.md +179 -0
  53. package/skills/deep-research/references/source_quality_rubric.md +28 -0
  54. package/skills/deep-research/references/subagent_prompt.md +116 -0
  55. package/skills/inbox-digest/SKILL.md +51 -0
  56. package/skills/skill-author/SKILL.md +92 -0
  57. package/skills/weekly-report/SKILL.md +49 -0
  58. package/web/dist/assets/index-BLQ8rPiN.js +130 -0
  59. package/web/dist/assets/index-IEIkG_jd.css +2 -0
  60. package/web/dist/index.html +13 -0
package/dist/server.js ADDED
@@ -0,0 +1,3016 @@
1
+ import { createRequire as __antbotCreateRequire } from 'node:module';
2
+ const require = __antbotCreateRequire(import.meta.url);
3
+ import {
4
+ ApprovalDecisionRequest,
5
+ CreateBotRequest,
6
+ CreateRoutineRequest,
7
+ CreateRuleRequest,
8
+ CreateSkillRequest,
9
+ CreateThreadRequest,
10
+ LIMITS,
11
+ LIMIT_ERROR,
12
+ LimitError,
13
+ PostMessageRequest,
14
+ SettingsPatchSchema,
15
+ SettingsSchema,
16
+ UpdateBotRequest
17
+ } from "./chunk-DYIJTUMY.js";
18
+ import {
19
+ findWebDist,
20
+ nodeLocateDeps,
21
+ readPackageVersion
22
+ } from "./chunk-Z2YT2PZN.js";
23
+ import {
24
+ describeSkillSource
25
+ } from "./chunk-7BOHBPB2.js";
26
+ import {
27
+ newId,
28
+ now,
29
+ slugify
30
+ } from "./chunk-KSQOVWP5.js";
31
+ import {
32
+ logger
33
+ } from "./chunk-AHAON6J7.js";
34
+
35
+ // packages/server/src/api/server.ts
36
+ import path11 from "node:path";
37
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
38
+ import { createRequire } from "node:module";
39
+ import Fastify from "fastify";
40
+ import cors from "@fastify/cors";
41
+ import multipart from "@fastify/multipart";
42
+ import websocket from "@fastify/websocket";
43
+ import fastifyStatic from "@fastify/static";
44
+
45
+ // packages/server/src/app.ts
46
+ import path8 from "node:path";
47
+
48
+ // packages/server/src/db/db.ts
49
+ import Database from "better-sqlite3";
50
+ import fs2 from "node:fs";
51
+ import path2 from "node:path";
52
+
53
+ // packages/server/src/db/migrations.ts
54
+ import fs from "node:fs";
55
+ import path from "node:path";
56
+
57
+ // packages/server/src/db/schema.ts
58
+ var SCHEMA_SQL = `
59
+ CREATE TABLE IF NOT EXISTS bots (
60
+ id TEXT PRIMARY KEY, slug TEXT UNIQUE NOT NULL, name TEXT NOT NULL,
61
+ title TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
62
+ avatar_emoji TEXT NOT NULL DEFAULT '\u{1F916}', model_tier TEXT NOT NULL DEFAULT 'sonnet',
63
+ pinned INTEGER NOT NULL DEFAULT 0, hidden INTEGER NOT NULL DEFAULT 0,
64
+ notifications INTEGER NOT NULL DEFAULT 1, session_id TEXT,
65
+ state TEXT NOT NULL DEFAULT 'idle', attention TEXT NOT NULL DEFAULT 'none',
66
+ thread_id TEXT, created_at INTEGER NOT NULL, deleted_at INTEGER
67
+ );
68
+ CREATE TABLE IF NOT EXISTS threads (
69
+ id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK(kind IN ('dm','group')),
70
+ title TEXT NOT NULL DEFAULT '', member_bot_ids TEXT NOT NULL DEFAULT '[]',
71
+ pinned INTEGER NOT NULL DEFAULT 0, hidden INTEGER NOT NULL DEFAULT 0,
72
+ last_read_at INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL
73
+ );
74
+ CREATE TABLE IF NOT EXISTS messages (
75
+ id TEXT PRIMARY KEY, thread_id TEXT NOT NULL,
76
+ author_kind TEXT NOT NULL CHECK(author_kind IN ('user','bot','system')),
77
+ author_bot_id TEXT, reply_to_id TEXT, content_md TEXT NOT NULL DEFAULT '',
78
+ cards TEXT NOT NULL DEFAULT '[]', streaming INTEGER NOT NULL DEFAULT 0,
79
+ created_at INTEGER NOT NULL
80
+ );
81
+ CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id, created_at);
82
+ CREATE TABLE IF NOT EXISTS attachments (
83
+ id TEXT PRIMARY KEY, message_id TEXT, path TEXT NOT NULL, name TEXT NOT NULL,
84
+ mime TEXT NOT NULL, bytes INTEGER NOT NULL, created_at INTEGER NOT NULL
85
+ );
86
+ CREATE TABLE IF NOT EXISTS approvals (
87
+ id TEXT PRIMARY KEY, bot_id TEXT NOT NULL, thread_id TEXT NOT NULL,
88
+ tool_name TEXT NOT NULL, input_summary TEXT NOT NULL, raw_input TEXT NOT NULL DEFAULT 'null',
89
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','allowed','denied','expired')),
90
+ decided_by TEXT CHECK(decided_by IN ('user','rule','auto_review')),
91
+ reason TEXT NOT NULL DEFAULT '', rule_id TEXT,
92
+ created_at INTEGER NOT NULL, decided_at INTEGER
93
+ );
94
+ CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status);
95
+ CREATE TABLE IF NOT EXISTS rules (
96
+ id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK(kind IN ('require','allow')),
97
+ tool_pattern TEXT NOT NULL DEFAULT '*', input_pattern TEXT NOT NULL DEFAULT '',
98
+ scope_note TEXT NOT NULL DEFAULT '', builtin INTEGER NOT NULL DEFAULT 0,
99
+ enabled INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL
100
+ );
101
+ CREATE TABLE IF NOT EXISTS skills (
102
+ id TEXT PRIMARY KEY, slug TEXT UNIQUE NOT NULL, name TEXT NOT NULL,
103
+ description TEXT NOT NULL DEFAULT '', path TEXT NOT NULL,
104
+ source TEXT NOT NULL DEFAULT 'user' CHECK(source IN ('user','taught','imported')),
105
+ created_at INTEGER NOT NULL
106
+ );
107
+ CREATE TABLE IF NOT EXISTS bot_skills (
108
+ bot_id TEXT NOT NULL, skill_id TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1,
109
+ PRIMARY KEY (bot_id, skill_id)
110
+ );
111
+ CREATE TABLE IF NOT EXISTS routines (
112
+ id TEXT PRIMARY KEY, bot_id TEXT NOT NULL, name TEXT NOT NULL,
113
+ cron_expr TEXT NOT NULL, timezone TEXT NOT NULL DEFAULT 'UTC',
114
+ instruction_md TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1,
115
+ last_run_at INTEGER, next_run_at INTEGER, created_at INTEGER NOT NULL
116
+ );
117
+ CREATE INDEX IF NOT EXISTS idx_routines_bot ON routines(bot_id);
118
+ CREATE TABLE IF NOT EXISTS routine_runs (
119
+ id TEXT PRIMARY KEY, routine_id TEXT NOT NULL, started_at INTEGER NOT NULL,
120
+ finished_at INTEGER, status TEXT NOT NULL CHECK(status IN ('running','ok','failed','interrupted')),
121
+ summary TEXT NOT NULL DEFAULT '', thread_id TEXT, is_test INTEGER NOT NULL DEFAULT 0
122
+ );
123
+ CREATE INDEX IF NOT EXISTS idx_runs_routine ON routine_runs(routine_id, started_at DESC);
124
+ CREATE TABLE IF NOT EXISTS mailbox (
125
+ id TEXT PRIMARY KEY, from_bot_id TEXT NOT NULL, to_bot_id TEXT NOT NULL,
126
+ content_md TEXT NOT NULL, hops INTEGER NOT NULL DEFAULT 1,
127
+ delivered INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL
128
+ );
129
+ CREATE TABLE IF NOT EXISTS usage (
130
+ id TEXT PRIMARY KEY, bot_id TEXT NOT NULL, turn_id TEXT NOT NULL, model TEXT NOT NULL,
131
+ input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
132
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0, cost_estimate REAL NOT NULL DEFAULT 0,
133
+ created_at INTEGER NOT NULL
134
+ );
135
+ CREATE INDEX IF NOT EXISTS idx_usage_created ON usage(created_at);
136
+ CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
137
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
138
+ content_md, content='messages', content_rowid='rowid'
139
+ );
140
+ CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
141
+ INSERT INTO messages_fts(rowid, content_md) VALUES (new.rowid, new.content_md);
142
+ END;
143
+ CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
144
+ INSERT INTO messages_fts(messages_fts, rowid, content_md) VALUES('delete', old.rowid, old.content_md);
145
+ END;
146
+ CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
147
+ INSERT INTO messages_fts(messages_fts, rowid, content_md) VALUES('delete', old.rowid, old.content_md);
148
+ INSERT INTO messages_fts(rowid, content_md) VALUES (new.rowid, new.content_md);
149
+ END;
150
+ `;
151
+
152
+ // packages/server/src/db/migrations.ts
153
+ var MigrationError = class extends Error {
154
+ constructor(code, message) {
155
+ super(message);
156
+ this.code = code;
157
+ this.name = "MigrationError";
158
+ }
159
+ code;
160
+ };
161
+ var BASELINE_VERSION = 1;
162
+ var BASELINE_SENTINEL_TABLE = "bots";
163
+ var MIGRATIONS = [
164
+ { version: BASELINE_VERSION, name: "baseline", up: SCHEMA_SQL }
165
+ ];
166
+ var SCHEMA_VERSION_SQL = `
167
+ CREATE TABLE IF NOT EXISTS schema_version (
168
+ version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at INTEGER NOT NULL
169
+ );
170
+ `;
171
+ function detectBaselineAdoption(hasLedgerRows, hasBaselineTable) {
172
+ return !hasLedgerRows && hasBaselineTable;
173
+ }
174
+ function planMigrations(currentVersion, migrations) {
175
+ const sorted = [...migrations].sort((a, b2) => a.version - b2.version);
176
+ let prev = 0;
177
+ for (const m of sorted) {
178
+ if (!Number.isInteger(m.version) || m.version < 1) {
179
+ throw new MigrationError("MIGRATION_ORDER", `migration "${m.name}" has invalid version ${m.version}`);
180
+ }
181
+ if (m.version === prev) {
182
+ throw new MigrationError("MIGRATION_ORDER", `duplicate migration version ${m.version}`);
183
+ }
184
+ prev = m.version;
185
+ }
186
+ const latest = sorted.length ? sorted[sorted.length - 1].version : 0;
187
+ if (currentVersion > latest) {
188
+ throw new MigrationError(
189
+ "MIGRATION_DOWNGRADE",
190
+ `database is at schema version ${currentVersion} but this build only knows up to ${latest}. Upgrade ant-bot (npm i -g @michael-joseph-miller/ant-bot) rather than downgrading the database.`
191
+ );
192
+ }
193
+ return sorted.filter((m) => m.version > currentVersion);
194
+ }
195
+ function readCurrentVersion(db) {
196
+ db.exec(SCHEMA_VERSION_SQL);
197
+ const row = db.prepare(`SELECT MAX(version) v FROM schema_version`).get();
198
+ if (row.v !== null) return { version: row.v, adopted: false };
199
+ const sentinel = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(BASELINE_SENTINEL_TABLE);
200
+ const adopted = detectBaselineAdoption(false, Boolean(sentinel));
201
+ return { version: adopted ? BASELINE_VERSION : 0, adopted };
202
+ }
203
+ function snapshot(db, backupsDir, toVersion, now2) {
204
+ fs.mkdirSync(backupsDir, { recursive: true });
205
+ const stamp = new Date(now2()).toISOString().replace(/[:.]/g, "-");
206
+ const dest = path.join(backupsDir, `antbot-pre-v${toVersion}-${stamp}.db`);
207
+ db.prepare(`VACUUM INTO ?`).run(dest);
208
+ return dest;
209
+ }
210
+ function migrate(db, opts = {}) {
211
+ const migrations = opts.migrations ?? MIGRATIONS;
212
+ const now2 = opts.now ?? Date.now;
213
+ const { version: from, adopted } = readCurrentVersion(db);
214
+ const record = db.prepare(
215
+ `INSERT OR IGNORE INTO schema_version (version, name, applied_at) VALUES (?, ?, ?)`
216
+ );
217
+ if (adopted) {
218
+ const baseline = migrations.find((m) => m.version === BASELINE_VERSION);
219
+ record.run(BASELINE_VERSION, baseline?.name ?? "baseline", now2());
220
+ }
221
+ const pending = planMigrations(from, migrations);
222
+ if (pending.length === 0) return { from, to: from, applied: [] };
223
+ const target = pending[pending.length - 1].version;
224
+ let backupPath;
225
+ if (opts.backupsDir && from > 0) {
226
+ backupPath = snapshot(db, opts.backupsDir, target, now2);
227
+ }
228
+ const applied = [];
229
+ for (const m of pending) {
230
+ const run = db.transaction(() => {
231
+ db.exec(m.up);
232
+ record.run(m.version, m.name, now2());
233
+ });
234
+ try {
235
+ run();
236
+ } catch (err) {
237
+ throw new MigrationError(
238
+ "MIGRATION_FAILED",
239
+ `migration ${m.version} (${m.name}) failed: ${err.message}` + (backupPath ? `. The pre-migration database was saved to ${backupPath}` : "")
240
+ );
241
+ }
242
+ applied.push({ version: m.version, name: m.name });
243
+ }
244
+ return { from, to: target, applied, backupPath };
245
+ }
246
+
247
+ // packages/server/src/db/db.ts
248
+ var log = logger("db");
249
+ function openDb(file, opts = {}) {
250
+ if (file !== ":memory:") fs2.mkdirSync(path2.dirname(file), { recursive: true });
251
+ const db = new Database(file);
252
+ db.pragma("journal_mode = WAL");
253
+ db.pragma("foreign_keys = ON");
254
+ db.pragma("busy_timeout = 5000");
255
+ const backupsDir = file === ":memory:" ? void 0 : opts.backupsDir ?? path2.join(path2.dirname(file), "backups");
256
+ const result = migrate(db, { backupsDir });
257
+ if (result.from > 0 && result.applied.length) {
258
+ log.info(
259
+ `schema ${result.from} -> ${result.to}: ${result.applied.map((a) => a.name).join(", ")}` + (result.backupPath ? ` (snapshot: ${result.backupPath})` : "")
260
+ );
261
+ }
262
+ return db;
263
+ }
264
+
265
+ // packages/server/src/db/store.ts
266
+ var b = (v) => v === 1 || v === true;
267
+ var i = (v, d = false) => v ?? d ? 1 : 0;
268
+ var toBot = (r) => ({
269
+ id: r.id,
270
+ slug: r.slug,
271
+ name: r.name,
272
+ title: r.title,
273
+ description: r.description,
274
+ avatarEmoji: r.avatar_emoji,
275
+ modelTier: r.model_tier,
276
+ pinned: b(r.pinned),
277
+ hidden: b(r.hidden),
278
+ notifications: b(r.notifications),
279
+ sessionId: r.session_id,
280
+ state: r.state,
281
+ attention: r.attention,
282
+ threadId: r.thread_id,
283
+ createdAt: r.created_at,
284
+ deletedAt: r.deleted_at
285
+ });
286
+ var toThread = (r) => ({
287
+ id: r.id,
288
+ kind: r.kind,
289
+ title: r.title,
290
+ memberBotIds: JSON.parse(r.member_bot_ids),
291
+ pinned: b(r.pinned),
292
+ hidden: b(r.hidden),
293
+ lastReadAt: r.last_read_at,
294
+ createdAt: r.created_at
295
+ });
296
+ var toMessage = (r) => ({
297
+ id: r.id,
298
+ threadId: r.thread_id,
299
+ authorKind: r.author_kind,
300
+ authorBotId: r.author_bot_id,
301
+ replyToId: r.reply_to_id,
302
+ contentMd: r.content_md,
303
+ cards: JSON.parse(r.cards),
304
+ streaming: b(r.streaming),
305
+ createdAt: r.created_at
306
+ });
307
+ var toApproval = (r) => ({
308
+ id: r.id,
309
+ botId: r.bot_id,
310
+ threadId: r.thread_id,
311
+ toolName: r.tool_name,
312
+ inputSummary: r.input_summary,
313
+ rawInput: JSON.parse(r.raw_input),
314
+ status: r.status,
315
+ decidedBy: r.decided_by,
316
+ reason: r.reason,
317
+ ruleId: r.rule_id,
318
+ createdAt: r.created_at,
319
+ decidedAt: r.decided_at
320
+ });
321
+ var toRule = (r) => ({
322
+ id: r.id,
323
+ kind: r.kind,
324
+ toolPattern: r.tool_pattern,
325
+ inputPattern: r.input_pattern,
326
+ scopeNote: r.scope_note,
327
+ builtin: b(r.builtin),
328
+ enabled: b(r.enabled),
329
+ createdAt: r.created_at
330
+ });
331
+ var toSkill = (r) => ({
332
+ id: r.id,
333
+ slug: r.slug,
334
+ name: r.name,
335
+ description: r.description,
336
+ path: r.path,
337
+ source: r.source,
338
+ createdAt: r.created_at
339
+ });
340
+ var toRoutine = (r) => ({
341
+ id: r.id,
342
+ botId: r.bot_id,
343
+ name: r.name,
344
+ cronExpr: r.cron_expr,
345
+ timezone: r.timezone,
346
+ instructionMd: r.instruction_md,
347
+ enabled: b(r.enabled),
348
+ lastRunAt: r.last_run_at,
349
+ nextRunAt: r.next_run_at,
350
+ createdAt: r.created_at
351
+ });
352
+ var toRun = (r) => ({
353
+ id: r.id,
354
+ routineId: r.routine_id,
355
+ startedAt: r.started_at,
356
+ finishedAt: r.finished_at,
357
+ status: r.status,
358
+ summary: r.summary,
359
+ threadId: r.thread_id,
360
+ isTest: b(r.is_test)
361
+ });
362
+ var toAttachment = (r) => ({
363
+ id: r.id,
364
+ messageId: r.message_id,
365
+ path: r.path,
366
+ name: r.name,
367
+ mime: r.mime,
368
+ bytes: r.bytes,
369
+ createdAt: r.created_at
370
+ });
371
+ var toMail = (r) => ({
372
+ id: r.id,
373
+ fromBotId: r.from_bot_id,
374
+ toBotId: r.to_bot_id,
375
+ contentMd: r.content_md,
376
+ hops: r.hops,
377
+ delivered: b(r.delivered),
378
+ createdAt: r.created_at
379
+ });
380
+ var toUsage = (r) => ({
381
+ id: r.id,
382
+ botId: r.bot_id,
383
+ turnId: r.turn_id,
384
+ model: r.model,
385
+ inputTokens: r.input_tokens,
386
+ outputTokens: r.output_tokens,
387
+ cacheReadTokens: r.cache_read_tokens,
388
+ costEstimate: r.cost_estimate,
389
+ createdAt: r.created_at
390
+ });
391
+ var Store = class {
392
+ constructor(db) {
393
+ this.db = db;
394
+ }
395
+ db;
396
+ /* ---- bots ---- */
397
+ countBotsAndGroups() {
398
+ const bots = this.db.prepare(`SELECT COUNT(*) c FROM bots WHERE deleted_at IS NULL`).get();
399
+ const groups = this.db.prepare(`SELECT COUNT(*) c FROM threads WHERE kind='group'`).get();
400
+ return bots.c + groups.c;
401
+ }
402
+ createBot(input) {
403
+ if (this.countBotsAndGroups() >= LIMITS.MAX_BOTS_AND_GROUPS)
404
+ throw new LimitError(LIMIT_ERROR.TOO_MANY_BOTS, `Limit of ${LIMITS.MAX_BOTS_AND_GROUPS} bots and groups reached`);
405
+ const existing = new Set(
406
+ this.db.prepare(`SELECT slug FROM bots`).all().map((r) => r.slug)
407
+ );
408
+ const id = newId();
409
+ const thread = this.createThread({ kind: "dm", title: input.name, memberBotIds: [id] });
410
+ this.db.prepare(
411
+ `INSERT INTO bots (id,slug,name,title,description,avatar_emoji,model_tier,pinned,hidden,notifications,session_id,state,attention,thread_id,created_at)
412
+ VALUES (@id,@slug,@name,@title,@description,@avatar_emoji,@model_tier,0,0,1,NULL,'idle','none',@thread_id,@created_at)`
413
+ ).run({
414
+ id,
415
+ slug: slugify(input.name, existing),
416
+ name: input.name,
417
+ title: input.title ?? "",
418
+ description: input.description ?? "",
419
+ avatar_emoji: input.avatarEmoji ?? "\u{1F916}",
420
+ model_tier: input.modelTier ?? "sonnet",
421
+ thread_id: thread.id,
422
+ created_at: now()
423
+ });
424
+ return this.getBot(id);
425
+ }
426
+ getBot(id) {
427
+ const r = this.db.prepare(`SELECT * FROM bots WHERE id=? AND deleted_at IS NULL`).get(id);
428
+ return r ? toBot(r) : null;
429
+ }
430
+ getBotBySlug(slug) {
431
+ const r = this.db.prepare(`SELECT * FROM bots WHERE slug=? AND deleted_at IS NULL`).get(slug);
432
+ return r ? toBot(r) : null;
433
+ }
434
+ listBots(includeHidden = true) {
435
+ const rows = this.db.prepare(`SELECT * FROM bots WHERE deleted_at IS NULL ${includeHidden ? "" : "AND hidden=0"} ORDER BY pinned DESC, created_at ASC`).all();
436
+ return rows.map(toBot);
437
+ }
438
+ updateBot(id, patch) {
439
+ const cur = this.getBot(id);
440
+ if (!cur) return null;
441
+ const m = {
442
+ name: patch.name ?? cur.name,
443
+ title: patch.title ?? cur.title,
444
+ description: patch.description ?? cur.description,
445
+ avatar_emoji: patch.avatarEmoji ?? cur.avatarEmoji,
446
+ model_tier: patch.modelTier ?? cur.modelTier,
447
+ pinned: i(patch.pinned, cur.pinned),
448
+ hidden: i(patch.hidden, cur.hidden),
449
+ notifications: i(patch.notifications, cur.notifications),
450
+ session_id: patch.sessionId !== void 0 ? patch.sessionId : cur.sessionId,
451
+ state: patch.state ?? cur.state,
452
+ attention: patch.attention ?? cur.attention,
453
+ id
454
+ };
455
+ this.db.prepare(
456
+ `UPDATE bots SET name=@name,title=@title,description=@description,avatar_emoji=@avatar_emoji,
457
+ model_tier=@model_tier,pinned=@pinned,hidden=@hidden,notifications=@notifications,
458
+ session_id=@session_id,state=@state,attention=@attention WHERE id=@id`
459
+ ).run(m);
460
+ return this.getBot(id);
461
+ }
462
+ deleteBot(id) {
463
+ const bot = this.getBot(id);
464
+ if (!bot) return;
465
+ this.db.prepare(`UPDATE bots SET deleted_at=? WHERE id=?`).run(now(), id);
466
+ this.db.prepare(`DELETE FROM routines WHERE bot_id=?`).run(id);
467
+ if (bot.threadId) this.db.prepare(`DELETE FROM threads WHERE id=?`).run(bot.threadId);
468
+ }
469
+ duplicateBot(id) {
470
+ const src = this.getBot(id);
471
+ if (!src) return null;
472
+ const copy = this.createBot({
473
+ name: `${src.name} copy`,
474
+ title: src.title,
475
+ description: src.description,
476
+ avatarEmoji: src.avatarEmoji,
477
+ modelTier: src.modelTier
478
+ });
479
+ for (const bs of this.db.prepare(`SELECT * FROM bot_skills WHERE bot_id=?`).all(id))
480
+ this.db.prepare(`INSERT OR REPLACE INTO bot_skills (bot_id,skill_id,enabled) VALUES (?,?,?)`).run(copy.id, bs.skill_id, bs.enabled);
481
+ for (const r of this.listRoutines(id))
482
+ this.createRoutine({ botId: copy.id, name: r.name, cronExpr: r.cronExpr, timezone: r.timezone, instructionMd: r.instructionMd, enabled: false });
483
+ return copy;
484
+ }
485
+ /* ---- threads & messages ---- */
486
+ createThread(input) {
487
+ if (input.kind === "group") {
488
+ const n = input.memberBotIds.length;
489
+ if (n < LIMITS.MIN_GROUP_MEMBERS || n > LIMITS.MAX_GROUP_MEMBERS)
490
+ throw new LimitError(LIMIT_ERROR.GROUP_SIZE, `A group needs ${LIMITS.MIN_GROUP_MEMBERS}\u2013${LIMITS.MAX_GROUP_MEMBERS} bots (got ${n})`);
491
+ if (this.countBotsAndGroups() >= LIMITS.MAX_BOTS_AND_GROUPS)
492
+ throw new LimitError(LIMIT_ERROR.TOO_MANY_BOTS, `Limit of ${LIMITS.MAX_BOTS_AND_GROUPS} bots and groups reached`);
493
+ }
494
+ const id = newId();
495
+ this.db.prepare(
496
+ `INSERT INTO threads (id,kind,title,member_bot_ids,pinned,hidden,last_read_at,created_at)
497
+ VALUES (?,?,?,?,0,0,0,?)`
498
+ ).run(id, input.kind, input.title ?? "", JSON.stringify(input.memberBotIds), now());
499
+ return this.getThread(id);
500
+ }
501
+ getThread(id) {
502
+ const r = this.db.prepare(`SELECT * FROM threads WHERE id=?`).get(id);
503
+ return r ? toThread(r) : null;
504
+ }
505
+ listThreads(kind) {
506
+ const rows = kind ? this.db.prepare(`SELECT * FROM threads WHERE kind=? ORDER BY created_at ASC`).all(kind) : this.db.prepare(`SELECT * FROM threads ORDER BY created_at ASC`).all();
507
+ return rows.map(toThread);
508
+ }
509
+ updateThread(id, patch) {
510
+ const cur = this.getThread(id);
511
+ if (!cur) return null;
512
+ this.db.prepare(
513
+ `UPDATE threads SET title=?, member_bot_ids=?, pinned=?, hidden=?, last_read_at=? WHERE id=?`
514
+ ).run(
515
+ patch.title ?? cur.title,
516
+ JSON.stringify(patch.memberBotIds ?? cur.memberBotIds),
517
+ i(patch.pinned, cur.pinned),
518
+ i(patch.hidden, cur.hidden),
519
+ patch.lastReadAt ?? cur.lastReadAt,
520
+ id
521
+ );
522
+ return this.getThread(id);
523
+ }
524
+ deleteThread(id) {
525
+ this.db.prepare(`DELETE FROM threads WHERE id=?`).run(id);
526
+ this.db.prepare(`DELETE FROM messages WHERE thread_id=?`).run(id);
527
+ }
528
+ createMessage(input) {
529
+ const id = newId();
530
+ this.db.prepare(
531
+ `INSERT INTO messages (id,thread_id,author_kind,author_bot_id,reply_to_id,content_md,cards,streaming,created_at)
532
+ VALUES (?,?,?,?,?,?,?,?,?)`
533
+ ).run(
534
+ id,
535
+ input.threadId,
536
+ input.authorKind,
537
+ input.authorBotId ?? null,
538
+ input.replyToId ?? null,
539
+ input.contentMd ?? "",
540
+ JSON.stringify(input.cards ?? []),
541
+ i(input.streaming),
542
+ now()
543
+ );
544
+ return this.getMessage(id);
545
+ }
546
+ getMessage(id) {
547
+ const r = this.db.prepare(`SELECT * FROM messages WHERE id=?`).get(id);
548
+ return r ? toMessage(r) : null;
549
+ }
550
+ listMessages(threadId, limit = 500) {
551
+ const rows = this.db.prepare(`SELECT * FROM messages WHERE thread_id=? ORDER BY created_at ASC LIMIT ?`).all(threadId, limit);
552
+ return rows.map(toMessage);
553
+ }
554
+ updateMessage(id, patch) {
555
+ const cur = this.getMessage(id);
556
+ if (!cur) return null;
557
+ this.db.prepare(`UPDATE messages SET content_md=?, cards=?, streaming=? WHERE id=?`).run(
558
+ patch.contentMd ?? cur.contentMd,
559
+ JSON.stringify(patch.cards ?? cur.cards),
560
+ i(patch.streaming, cur.streaming),
561
+ id
562
+ );
563
+ return this.getMessage(id);
564
+ }
565
+ appendCard(id, card) {
566
+ const cur = this.getMessage(id);
567
+ if (!cur) return -1;
568
+ const cards = [...cur.cards, card];
569
+ this.db.prepare(`UPDATE messages SET cards=? WHERE id=?`).run(JSON.stringify(cards), id);
570
+ return cards.length - 1;
571
+ }
572
+ updateCard(id, index, card) {
573
+ const cur = this.getMessage(id);
574
+ if (!cur || !cur.cards[index]) return;
575
+ const cards = [...cur.cards];
576
+ cards[index] = card;
577
+ this.db.prepare(`UPDATE messages SET cards=? WHERE id=?`).run(JSON.stringify(cards), id);
578
+ }
579
+ lastMessageAt(threadId) {
580
+ const r = this.db.prepare(`SELECT MAX(created_at) m FROM messages WHERE thread_id=?`).get(threadId);
581
+ return r?.m ?? 0;
582
+ }
583
+ /* ---- attachments ---- */
584
+ createAttachment(a) {
585
+ const isVideo = a.mime.startsWith("video/");
586
+ const cap = isVideo ? LIMITS.MAX_VIDEO_ATTACHMENT_BYTES : LIMITS.MAX_ATTACHMENT_BYTES;
587
+ if (a.bytes > cap)
588
+ throw new LimitError(LIMIT_ERROR.ATTACHMENT_TOO_LARGE, `${a.name} is ${(a.bytes / 1048576).toFixed(1)} MB; limit is ${cap / 1048576} MB`);
589
+ const id = newId();
590
+ this.db.prepare(
591
+ `INSERT INTO attachments (id,message_id,path,name,mime,bytes,created_at) VALUES (?,?,?,?,?,?,?)`
592
+ ).run(id, a.messageId ?? null, a.path, a.name, a.mime, a.bytes, now());
593
+ return toAttachment(this.db.prepare(`SELECT * FROM attachments WHERE id=?`).get(id));
594
+ }
595
+ getAttachment(id) {
596
+ const r = this.db.prepare(`SELECT * FROM attachments WHERE id=?`).get(id);
597
+ return r ? toAttachment(r) : null;
598
+ }
599
+ attachToMessage(ids, messageId) {
600
+ if (ids.length > LIMITS.MAX_ATTACHMENTS_PER_MESSAGE)
601
+ throw new LimitError(LIMIT_ERROR.TOO_MANY_ATTACHMENTS, `At most ${LIMITS.MAX_ATTACHMENTS_PER_MESSAGE} attachments per message`);
602
+ const stmt = this.db.prepare(`UPDATE attachments SET message_id=? WHERE id=?`);
603
+ for (const id of ids) stmt.run(messageId, id);
604
+ }
605
+ listAttachmentsForMessage(messageId) {
606
+ return this.db.prepare(`SELECT * FROM attachments WHERE message_id=?`).all(messageId).map(toAttachment);
607
+ }
608
+ /* ---- approvals ---- */
609
+ createApproval(a) {
610
+ const id = newId();
611
+ this.db.prepare(
612
+ `INSERT INTO approvals (id,bot_id,thread_id,tool_name,input_summary,raw_input,status,reason,created_at)
613
+ VALUES (?,?,?,?,?,?, 'pending', ?, ?)`
614
+ ).run(id, a.botId, a.threadId, a.toolName, a.inputSummary, JSON.stringify(a.rawInput ?? null), a.reason ?? "", now());
615
+ return this.getApproval(id);
616
+ }
617
+ getApproval(id) {
618
+ const r = this.db.prepare(`SELECT * FROM approvals WHERE id=?`).get(id);
619
+ return r ? toApproval(r) : null;
620
+ }
621
+ listPendingApprovals() {
622
+ return this.db.prepare(`SELECT * FROM approvals WHERE status='pending' ORDER BY created_at ASC`).all().map(toApproval);
623
+ }
624
+ resolveApproval(id, status, decidedBy, ruleId, reason) {
625
+ const cur = this.getApproval(id);
626
+ if (!cur) return null;
627
+ this.db.prepare(`UPDATE approvals SET status=?, decided_by=?, rule_id=?, reason=?, decided_at=? WHERE id=?`).run(status, decidedBy, ruleId ?? cur.ruleId, reason ?? cur.reason, now(), id);
628
+ return this.getApproval(id);
629
+ }
630
+ /* ---- rules ---- */
631
+ createRule(r) {
632
+ const id = newId();
633
+ this.db.prepare(
634
+ `INSERT INTO rules (id,kind,tool_pattern,input_pattern,scope_note,builtin,enabled,created_at) VALUES (?,?,?,?,?,?,1,?)`
635
+ ).run(id, r.kind, r.toolPattern, r.inputPattern ?? "", r.scopeNote ?? "", i(r.builtin), now());
636
+ return this.getRule(id);
637
+ }
638
+ getRule(id) {
639
+ const r = this.db.prepare(`SELECT * FROM rules WHERE id=?`).get(id);
640
+ return r ? toRule(r) : null;
641
+ }
642
+ listRules(onlyEnabled = false) {
643
+ return this.db.prepare(`SELECT * FROM rules ${onlyEnabled ? "WHERE enabled=1" : ""} ORDER BY kind ASC, created_at ASC`).all().map(toRule);
644
+ }
645
+ setRuleEnabled(id, enabled) {
646
+ this.db.prepare(`UPDATE rules SET enabled=? WHERE id=?`).run(i(enabled), id);
647
+ }
648
+ deleteRule(id) {
649
+ this.db.prepare(`DELETE FROM rules WHERE id=? AND builtin=0`).run(id);
650
+ }
651
+ /* ---- skills ---- */
652
+ createSkill(s) {
653
+ const id = newId();
654
+ this.db.prepare(
655
+ `INSERT OR REPLACE INTO skills (id,slug,name,description,path,source,created_at) VALUES (?,?,?,?,?,?,?)`
656
+ ).run(id, s.slug, s.name, s.description ?? "", s.path, s.source ?? "user", now());
657
+ return this.getSkillBySlug(s.slug);
658
+ }
659
+ getSkill(id) {
660
+ const r = this.db.prepare(`SELECT * FROM skills WHERE id=?`).get(id);
661
+ return r ? toSkill(r) : null;
662
+ }
663
+ getSkillBySlug(slug) {
664
+ const r = this.db.prepare(`SELECT * FROM skills WHERE slug=?`).get(slug);
665
+ return r ? toSkill(r) : null;
666
+ }
667
+ listSkills() {
668
+ return this.db.prepare(`SELECT * FROM skills ORDER BY name ASC`).all().map(toSkill);
669
+ }
670
+ /**
671
+ * Update a skill's metadata in place, keeping its id so bot assignments survive a
672
+ * re-install. Returns null if the skill is gone.
673
+ */
674
+ updateSkill(id, patch) {
675
+ const existing = this.getSkill(id);
676
+ if (!existing) return null;
677
+ this.db.prepare(`UPDATE skills SET name=?, description=?, path=? WHERE id=?`).run(
678
+ patch.name ?? existing.name,
679
+ patch.description ?? existing.description,
680
+ patch.path ?? existing.path,
681
+ id
682
+ );
683
+ return this.getSkill(id);
684
+ }
685
+ deleteSkill(id) {
686
+ this.db.prepare(`DELETE FROM skills WHERE id=?`).run(id);
687
+ this.db.prepare(`DELETE FROM bot_skills WHERE skill_id=?`).run(id);
688
+ }
689
+ setBotSkills(botId, skillIds) {
690
+ this.db.prepare(`DELETE FROM bot_skills WHERE bot_id=?`).run(botId);
691
+ const stmt = this.db.prepare(`INSERT OR REPLACE INTO bot_skills (bot_id,skill_id,enabled) VALUES (?,?,1)`);
692
+ for (const s of skillIds) stmt.run(botId, s);
693
+ }
694
+ listBotSkills(botId) {
695
+ return this.db.prepare(
696
+ `SELECT s.* FROM skills s JOIN bot_skills bs ON bs.skill_id=s.id WHERE bs.bot_id=? AND bs.enabled=1`
697
+ ).all(botId).map(toSkill);
698
+ }
699
+ /* ---- routines ---- */
700
+ createRoutine(r) {
701
+ const count = this.db.prepare(`SELECT COUNT(*) c FROM routines WHERE bot_id=?`).get(r.botId).c;
702
+ if (count >= LIMITS.MAX_ROUTINES_PER_BOT)
703
+ throw new LimitError(LIMIT_ERROR.TOO_MANY_ROUTINES, `A bot can own at most ${LIMITS.MAX_ROUTINES_PER_BOT} routines`);
704
+ const id = newId();
705
+ this.db.prepare(
706
+ `INSERT INTO routines (id,bot_id,name,cron_expr,timezone,instruction_md,enabled,created_at) VALUES (?,?,?,?,?,?,?,?)`
707
+ ).run(id, r.botId, r.name, r.cronExpr, r.timezone ?? "UTC", r.instructionMd, i(r.enabled, true), now());
708
+ return this.getRoutine(id);
709
+ }
710
+ getRoutine(id) {
711
+ const r = this.db.prepare(`SELECT * FROM routines WHERE id=?`).get(id);
712
+ return r ? toRoutine(r) : null;
713
+ }
714
+ listRoutines(botId) {
715
+ const rows = botId ? this.db.prepare(`SELECT * FROM routines WHERE bot_id=? ORDER BY created_at ASC`).all(botId) : this.db.prepare(`SELECT * FROM routines ORDER BY created_at ASC`).all();
716
+ return rows.map(toRoutine);
717
+ }
718
+ updateRoutine(id, patch) {
719
+ const cur = this.getRoutine(id);
720
+ if (!cur) return null;
721
+ this.db.prepare(
722
+ `UPDATE routines SET name=?,cron_expr=?,timezone=?,instruction_md=?,enabled=?,last_run_at=?,next_run_at=? WHERE id=?`
723
+ ).run(
724
+ patch.name ?? cur.name,
725
+ patch.cronExpr ?? cur.cronExpr,
726
+ patch.timezone ?? cur.timezone,
727
+ patch.instructionMd ?? cur.instructionMd,
728
+ i(patch.enabled, cur.enabled),
729
+ patch.lastRunAt !== void 0 ? patch.lastRunAt : cur.lastRunAt,
730
+ patch.nextRunAt !== void 0 ? patch.nextRunAt : cur.nextRunAt,
731
+ id
732
+ );
733
+ return this.getRoutine(id);
734
+ }
735
+ deleteRoutine(id) {
736
+ this.db.prepare(`DELETE FROM routines WHERE id=?`).run(id);
737
+ this.db.prepare(`DELETE FROM routine_runs WHERE routine_id=?`).run(id);
738
+ }
739
+ startRun(routineId, isTest = false, threadId) {
740
+ const id = newId();
741
+ this.db.prepare(
742
+ `INSERT INTO routine_runs (id,routine_id,started_at,status,summary,thread_id,is_test) VALUES (?,?,?,'running','',?,?)`
743
+ ).run(id, routineId, now(), threadId ?? null, i(isTest));
744
+ return this.getRun(id);
745
+ }
746
+ finishRun(id, status, summary) {
747
+ this.db.prepare(`UPDATE routine_runs SET finished_at=?, status=?, summary=? WHERE id=?`).run(now(), status, summary, id);
748
+ const run = this.getRun(id);
749
+ if (run) this.pruneRuns(run.routineId);
750
+ return run;
751
+ }
752
+ getRun(id) {
753
+ const r = this.db.prepare(`SELECT * FROM routine_runs WHERE id=?`).get(id);
754
+ return r ? toRun(r) : null;
755
+ }
756
+ listRuns(routineId) {
757
+ return this.db.prepare(
758
+ `SELECT * FROM routine_runs WHERE routine_id=? ORDER BY started_at DESC LIMIT ?`
759
+ ).all(routineId, LIMITS.ROUTINE_RUNS_RETAINED).map(toRun);
760
+ }
761
+ /** Keep only the N most recent run records (outline §13). */
762
+ pruneRuns(routineId) {
763
+ this.db.prepare(
764
+ `DELETE FROM routine_runs WHERE routine_id=? AND id NOT IN
765
+ (SELECT id FROM routine_runs WHERE routine_id=? ORDER BY started_at DESC LIMIT ?)`
766
+ ).run(routineId, routineId, LIMITS.ROUTINE_RUNS_RETAINED);
767
+ }
768
+ /* ---- mailbox ---- */
769
+ createMail(m) {
770
+ const hops = m.hops ?? 1;
771
+ if (hops > LIMITS.MAX_BOT_TO_BOT_HOPS)
772
+ throw new LimitError(LIMIT_ERROR.HOP_LIMIT, `Bot-to-bot hop limit (${LIMITS.MAX_BOT_TO_BOT_HOPS}) reached; a human must take the next step`);
773
+ const id = newId();
774
+ this.db.prepare(
775
+ `INSERT INTO mailbox (id,from_bot_id,to_bot_id,content_md,hops,delivered,created_at) VALUES (?,?,?,?,?,0,?)`
776
+ ).run(id, m.fromBotId, m.toBotId, m.contentMd, hops, now());
777
+ return toMail(this.db.prepare(`SELECT * FROM mailbox WHERE id=?`).get(id));
778
+ }
779
+ markDelivered(id) {
780
+ this.db.prepare(`UPDATE mailbox SET delivered=1 WHERE id=?`).run(id);
781
+ }
782
+ listMail(toBotId, onlyUndelivered = true) {
783
+ return this.db.prepare(
784
+ `SELECT * FROM mailbox WHERE to_bot_id=? ${onlyUndelivered ? "AND delivered=0" : ""} ORDER BY created_at ASC`
785
+ ).all(toBotId).map(toMail);
786
+ }
787
+ /* ---- usage ---- */
788
+ recordUsage(u) {
789
+ const id = newId();
790
+ this.db.prepare(
791
+ `INSERT INTO usage (id,bot_id,turn_id,model,input_tokens,output_tokens,cache_read_tokens,cost_estimate,created_at)
792
+ VALUES (?,?,?,?,?,?,?,?,?)`
793
+ ).run(id, u.botId, u.turnId, u.model, u.inputTokens, u.outputTokens, u.cacheReadTokens, u.costEstimate, now());
794
+ return toUsage(this.db.prepare(`SELECT * FROM usage WHERE id=?`).get(id));
795
+ }
796
+ listUsage(sinceMs = 0) {
797
+ return this.db.prepare(`SELECT * FROM usage WHERE created_at>=? ORDER BY created_at DESC`).all(sinceMs).map(toUsage);
798
+ }
799
+ tokensToday() {
800
+ const start = /* @__PURE__ */ new Date();
801
+ start.setHours(0, 0, 0, 0);
802
+ const r = this.db.prepare(
803
+ `SELECT COALESCE(SUM(input_tokens+output_tokens),0) t FROM usage WHERE created_at>=?`
804
+ ).get(start.getTime());
805
+ return r.t;
806
+ }
807
+ /* ---- settings ---- */
808
+ getSettings() {
809
+ const rows = this.db.prepare(`SELECT * FROM settings`).all();
810
+ const obj = {};
811
+ for (const r of rows) obj[r.key] = JSON.parse(r.value);
812
+ return SettingsSchema.parse(obj);
813
+ }
814
+ patchSettings(patch) {
815
+ const stmt = this.db.prepare(`INSERT OR REPLACE INTO settings (key,value) VALUES (?,?)`);
816
+ for (const [k, v] of Object.entries(patch)) if (v !== void 0) stmt.run(k, JSON.stringify(v));
817
+ return this.getSettings();
818
+ }
819
+ /* ---- search ---- */
820
+ searchMessages(q, limit = 40) {
821
+ if (!q.trim()) return [];
822
+ const escaped = `"${q.replace(/"/g, '""')}"`;
823
+ try {
824
+ const rows = this.db.prepare(
825
+ `SELECT m.* FROM messages_fts f JOIN messages m ON m.rowid=f.rowid
826
+ WHERE messages_fts MATCH ? ORDER BY rank LIMIT ?`
827
+ ).all(escaped, limit);
828
+ return rows.map(toMessage);
829
+ } catch {
830
+ const rows = this.db.prepare(
831
+ `SELECT * FROM messages WHERE content_md LIKE ? ORDER BY created_at DESC LIMIT ?`
832
+ ).all(`%${q}%`, limit);
833
+ return rows.map(toMessage);
834
+ }
835
+ }
836
+ };
837
+
838
+ // packages/server/src/util/bus.ts
839
+ import { EventEmitter } from "node:events";
840
+ var EventBus = class {
841
+ emitter = new EventEmitter();
842
+ seq = 0;
843
+ ring = [];
844
+ ringMax = 500;
845
+ constructor() {
846
+ this.emitter.setMaxListeners(200);
847
+ }
848
+ publish(e) {
849
+ const full = { ...e, seq: ++this.seq };
850
+ this.ring.push(full);
851
+ if (this.ring.length > this.ringMax) this.ring.shift();
852
+ this.emitter.emit("event", full);
853
+ return full;
854
+ }
855
+ subscribe(fn) {
856
+ this.emitter.on("event", fn);
857
+ return () => this.emitter.off("event", fn);
858
+ }
859
+ /** Replay events after a given seq (client reconnect). */
860
+ since(seq) {
861
+ return this.ring.filter((e) => e.seq > seq);
862
+ }
863
+ get currentSeq() {
864
+ return this.seq;
865
+ }
866
+ };
867
+
868
+ // packages/server/src/permissions/rules.ts
869
+ var log2 = logger("rules");
870
+ function globToRegExp(glob) {
871
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
872
+ return new RegExp(`^${escaped}$`, "i");
873
+ }
874
+ function serializeInput(input) {
875
+ if (input == null) return "";
876
+ if (typeof input === "string") return input;
877
+ try {
878
+ return JSON.stringify(input);
879
+ } catch {
880
+ return String(input);
881
+ }
882
+ }
883
+ function buildMatchText(input) {
884
+ const lines = [];
885
+ const walk = (v, depth) => {
886
+ if (depth > 6) return;
887
+ if (typeof v === "string") lines.push(v);
888
+ else if (Array.isArray(v)) v.forEach((x) => walk(x, depth + 1));
889
+ else if (v && typeof v === "object") Object.values(v).forEach((x) => walk(x, depth + 1));
890
+ else if (typeof v === "number" || typeof v === "boolean") lines.push(String(v));
891
+ };
892
+ walk(input, 0);
893
+ const serialized = serializeInput(input);
894
+ if (serialized && !lines.includes(serialized)) lines.push(serialized);
895
+ return lines.join("\n");
896
+ }
897
+ function toolNameAliases(toolName) {
898
+ const m = /^mcp__.+?__(.+)$/.exec(toolName);
899
+ return m?.[1] ? [toolName, m[1]] : [toolName];
900
+ }
901
+ function ruleMatches(rule, toolName, inputText) {
902
+ if (!rule.enabled) return false;
903
+ const pattern = globToRegExp(rule.toolPattern);
904
+ if (!toolNameAliases(toolName).some((n) => pattern.test(n))) return false;
905
+ if (rule.inputPattern) {
906
+ let re;
907
+ try {
908
+ re = new RegExp(rule.inputPattern, "im");
909
+ } catch {
910
+ return false;
911
+ }
912
+ if (!re.test(inputText)) return false;
913
+ }
914
+ return true;
915
+ }
916
+ function evaluateRules(rules, toolName, input) {
917
+ const inputText = buildMatchText(input);
918
+ const required = rules.find((r) => r.kind === "require" && ruleMatches(r, toolName, inputText));
919
+ if (required) return { kind: "require", rule: required };
920
+ const allowed = rules.find((r) => r.kind === "allow" && ruleMatches(r, toolName, inputText));
921
+ if (allowed) return { kind: "allow", rule: allowed };
922
+ return { kind: "none" };
923
+ }
924
+ var BUILTIN_RULES = [
925
+ { kind: "require", toolPattern: "Bash", inputPattern: "\\bsudo\\b|\\bdoas\\b|\\bsu\\s+-", scopeNote: "Privilege escalation", builtin: true, enabled: true },
926
+ { kind: "require", toolPattern: "Bash", inputPattern: "rm\\s+(-[a-zA-Z]*\\s+)*-?[rf]|shred\\b|mkfs\\b|dd\\s+if=", scopeNote: "Destructive filesystem command", builtin: true, enabled: true },
927
+ { kind: "require", toolPattern: "Bash", inputPattern: "curl[^|]*\\|\\s*(ba)?sh|wget[^|]*\\|\\s*(ba)?sh", scopeNote: "Piping a download into a shell", builtin: true, enabled: true },
928
+ { kind: "require", toolPattern: "Bash", inputPattern: "\\b(npm|pnpm|yarn|pip|pip3|gem|cargo|apt|apt-get|dnf|brew|go)\\s+(i|install|add|get)\\b", scopeNote: "Package install", builtin: true, enabled: true },
929
+ { kind: "require", toolPattern: "Bash", inputPattern: "git\\s+(push|remote\\s+add)|gh\\s+(pr|release|repo)\\s+(create|merge)", scopeNote: "Publishing to a remote", builtin: true, enabled: true },
930
+ { kind: "require", toolPattern: "Bash", inputPattern: "\\b(mail|sendmail|mutt|msmtp)\\b", scopeNote: "Sending mail", builtin: true, enabled: true },
931
+ { kind: "require", toolPattern: "Bash", inputPattern: "curl[^\\n]*(-X\\s*(POST|PUT|PATCH|DELETE)|--data|-d\\s)", scopeNote: "Outbound write request", builtin: true, enabled: true },
932
+ { kind: "require", toolPattern: "WebFetch", inputPattern: ".", scopeNote: "Fetching an external URL", builtin: true, enabled: true },
933
+ { kind: "require", toolPattern: "browser_click", inputPattern: "(buy|purchase|checkout|pay|order|subscribe|confirm|delete|send|publish)", scopeNote: "Consequential click", builtin: true, enabled: true },
934
+ { kind: "require", toolPattern: "browser_type", inputPattern: "(password|passwd|secret|token|api[_-]?key|ssn|credit\\s*card)", scopeNote: "Typing a credential \u2014 use takeover instead", builtin: true, enabled: true },
935
+ { kind: "require", toolPattern: "send_to_bot", inputPattern: "", scopeNote: "Handing work to another bot", builtin: false, enabled: false },
936
+ { kind: "require", toolPattern: "install_skill", inputPattern: "", scopeNote: "Installing a skill from an external source", builtin: true, enabled: true },
937
+ { kind: "require", toolPattern: "remove_skill", inputPattern: "", scopeNote: "Uninstalling a skill", builtin: true, enabled: true },
938
+ { kind: "allow", toolPattern: "Read", inputPattern: "", scopeNote: "Reading files is safe", builtin: true, enabled: true },
939
+ { kind: "allow", toolPattern: "Glob", inputPattern: "", scopeNote: "Listing files is safe", builtin: true, enabled: true },
940
+ { kind: "allow", toolPattern: "Grep", inputPattern: "", scopeNote: "Searching files is safe", builtin: true, enabled: true },
941
+ { kind: "allow", toolPattern: "TodoWrite", inputPattern: "", scopeNote: "Planning scratchpad", builtin: true, enabled: true },
942
+ { kind: "allow", toolPattern: "Bash", inputPattern: "^\\s*(git\\s+(status|diff|log|show|branch)|ls|pwd|cat|head|tail|wc|echo|date|which|grep|find|rg)\\b", scopeNote: "Read-only shell inspection", builtin: true, enabled: true }
943
+ ];
944
+ function seedBuiltinRules(store) {
945
+ const key = (r) => `${r.kind}\0${r.toolPattern}\0${r.inputPattern}`;
946
+ const existing = new Set(store.listRules().map(key));
947
+ const added = [];
948
+ for (const r of BUILTIN_RULES) {
949
+ if (existing.has(key(r))) continue;
950
+ const rule = store.createRule(r);
951
+ if (!r.enabled) store.setRuleEnabled(rule.id, false);
952
+ added.push(r.toolPattern);
953
+ }
954
+ if (added.length) log2.info(`seeded ${added.length} builtin permission rule(s): ${added.join(", ")}`);
955
+ }
956
+
957
+ // packages/server/src/permissions/local.ts
958
+ import path3 from "node:path";
959
+ var HOME_ISH = /(^|[\s"'=:])(~|\$HOME)\//;
960
+ var URL_RE = /\b[a-z][a-z0-9+.-]*:\/\/\S+/gi;
961
+ function extractPaths(text) {
962
+ const out = [];
963
+ const cleaned = text.replace(URL_RE, " ");
964
+ for (const m of cleaned.matchAll(/(?<![\w\-.:/])((?:~|\.{1,2}\/|\.\.(?=\s|$)|\/)[^\s"';|&)]*)/g)) {
965
+ const p = m[1];
966
+ if (p && p.length > 1) out.push(p);
967
+ }
968
+ return out;
969
+ }
970
+ function assessLocalReach(toolName, input, workspace) {
971
+ const o = input ?? {};
972
+ const str = (k) => typeof o[k] === "string" ? o[k] : "";
973
+ const ws = path3.resolve(workspace);
974
+ const insideWorkspace = (p) => {
975
+ if (p.startsWith("~")) return false;
976
+ const abs = path3.isAbsolute(p) ? path3.resolve(p) : path3.resolve(ws, p);
977
+ const rel = path3.relative(ws, abs);
978
+ return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
979
+ };
980
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit" || toolName === "NotebookEdit") {
981
+ const p = str("file_path") || str("notebook_path");
982
+ if (p && !insideWorkspace(p)) return { reaches: true, evidence: p };
983
+ return { reaches: false, evidence: "" };
984
+ }
985
+ if (toolName === "Bash") {
986
+ const cmd = str("command");
987
+ if (HOME_ISH.test(cmd)) {
988
+ const m = cmd.match(HOME_ISH);
989
+ return { reaches: true, evidence: m ? cmd.slice(Math.max(0, m.index ?? 0), (m.index ?? 0) + 60).trim() : "~" };
990
+ }
991
+ for (const p of extractPaths(cmd)) {
992
+ if (!insideWorkspace(p)) return { reaches: true, evidence: p };
993
+ }
994
+ return { reaches: false, evidence: "" };
995
+ }
996
+ return { reaches: false, evidence: "" };
997
+ }
998
+ function localDecision(policy, reach) {
999
+ if (!reach.reaches) return { action: "ignore" };
1000
+ if (policy === "always") return { action: "ignore" };
1001
+ if (policy === "never")
1002
+ return {
1003
+ action: "deny",
1004
+ reason: `Blocked: this touches ${reach.evidence}, which is outside the shared workspace. "Execution on local computer" is set to Never in Settings.`
1005
+ };
1006
+ return { action: "require", reason: `Touches ${reach.evidence}, outside the shared workspace (your own machine)` };
1007
+ }
1008
+
1009
+ // packages/server/src/permissions/gateway.ts
1010
+ var log3 = logger("gateway");
1011
+ function summarize(namespacedToolName, input) {
1012
+ const o = input ?? {};
1013
+ const s = (k) => typeof o[k] === "string" ? o[k] : "";
1014
+ const aliases = toolNameAliases(namespacedToolName);
1015
+ const toolName = aliases[aliases.length - 1];
1016
+ switch (toolName) {
1017
+ case "Bash":
1018
+ return `Run: ${s("command").slice(0, 200)}`;
1019
+ case "Write":
1020
+ return `Write file ${s("file_path")}`;
1021
+ case "Edit":
1022
+ return `Edit file ${s("file_path")}`;
1023
+ case "Read":
1024
+ return `Read file ${s("file_path")}`;
1025
+ case "WebFetch":
1026
+ return `Fetch ${s("url")}`;
1027
+ case "send_to_bot":
1028
+ return `Hand work to @${s("bot_slug")}`;
1029
+ // Scope is the whole decision here: one named skill, or every skill in a repository.
1030
+ case "install_skill":
1031
+ return describeSkillSource(s("source"));
1032
+ case "remove_skill":
1033
+ return `Uninstall the skill "${s("slug")}"`;
1034
+ default: {
1035
+ if (toolName.startsWith("browser_")) {
1036
+ const bits = [s("url"), s("selector"), s("text")].filter(Boolean).join(" ");
1037
+ return `${toolName.replace("browser_", "Browser ")}: ${bits}`.trim();
1038
+ }
1039
+ const flat = serializeInput(input);
1040
+ return `${toolName}: ${flat.slice(0, 180)}`;
1041
+ }
1042
+ }
1043
+ }
1044
+ var PermissionGateway = class {
1045
+ constructor(store, bus, autoReviewer) {
1046
+ this.store = store;
1047
+ this.bus = bus;
1048
+ this.autoReviewer = autoReviewer;
1049
+ }
1050
+ store;
1051
+ bus;
1052
+ autoReviewer;
1053
+ pending = /* @__PURE__ */ new Map();
1054
+ /**
1055
+ * Decide whether a proposed tool call may run.
1056
+ * Order: deterministic rules → auto review (advisory) → human approval card.
1057
+ * A matching `require` rule can never be satisfied by auto review (outline §9).
1058
+ */
1059
+ async check(args) {
1060
+ const { toolName, input, settings } = args;
1061
+ const rules = this.store.listRules(true);
1062
+ const decision = evaluateRules(rules, toolName, input);
1063
+ const local = localDecision(settings.localExecution, assessLocalReach(toolName, input, args.workspace));
1064
+ if (local.action === "deny") {
1065
+ return { behavior: "deny", message: local.reason, via: "rule" };
1066
+ }
1067
+ if (local.action === "require" && decision.kind !== "require") {
1068
+ return this.askHuman({ ...args, reason: local.reason });
1069
+ }
1070
+ if (decision.kind === "allow") {
1071
+ log3.debug(`allow by rule ${decision.rule.id}: ${toolName}`);
1072
+ return { behavior: "allow", reason: decision.rule.scopeNote || "Matched an allow rule", via: "rule" };
1073
+ }
1074
+ const requiredBy = decision.kind === "require" ? decision.rule : null;
1075
+ if (!requiredBy && settings.autoReviewEnabled && this.autoReviewer) {
1076
+ try {
1077
+ const verdict = await this.autoReviewer.classify(toolName, input, args.botDescription);
1078
+ if (verdict.verdict === "allow_ok")
1079
+ return { behavior: "allow", reason: verdict.reason, via: "auto_review" };
1080
+ if (verdict.verdict === "deny_suggested")
1081
+ return this.askHuman({ ...args, reason: `Auto review flagged this: ${verdict.reason}` });
1082
+ return this.askHuman({ ...args, reason: verdict.reason });
1083
+ } catch (err) {
1084
+ log3.warn("auto review failed; falling back to human approval", err);
1085
+ }
1086
+ }
1087
+ return this.askHuman({
1088
+ ...args,
1089
+ reason: requiredBy ? requiredBy.scopeNote || "Matched a require-approval rule" : "No rule covers this action",
1090
+ ruleId: requiredBy?.id ?? null
1091
+ });
1092
+ }
1093
+ askHuman(args) {
1094
+ const approval = this.store.createApproval({
1095
+ botId: args.botId,
1096
+ threadId: args.threadId,
1097
+ toolName: args.toolName,
1098
+ inputSummary: summarize(args.toolName, args.input),
1099
+ rawInput: args.input,
1100
+ reason: args.reason
1101
+ });
1102
+ if (args.ruleId) this.store.db.prepare(`UPDATE approvals SET rule_id=? WHERE id=?`).run(args.ruleId, approval.id);
1103
+ const fresh = this.store.getApproval(approval.id);
1104
+ args.onPending?.(fresh);
1105
+ this.bus.publish({
1106
+ type: "approval.pending",
1107
+ threadId: args.threadId,
1108
+ botId: args.botId,
1109
+ approval: fresh
1110
+ });
1111
+ return new Promise((resolve) => {
1112
+ const timer = setTimeout(() => {
1113
+ this.pending.delete(approval.id);
1114
+ this.store.resolveApproval(approval.id, "expired", "user", null, "No response in time");
1115
+ this.bus.publish({
1116
+ type: "approval.resolved",
1117
+ threadId: args.threadId,
1118
+ botId: args.botId,
1119
+ approval: this.store.getApproval(approval.id)
1120
+ });
1121
+ resolve({ behavior: "deny", message: "Approval request expired without a decision.", via: "timeout" });
1122
+ }, LIMITS.APPROVAL_TIMEOUT_MS);
1123
+ this.pending.set(approval.id, { approvalId: approval.id, resolve, timer });
1124
+ args.signal?.addEventListener("abort", () => {
1125
+ const p = this.pending.get(approval.id);
1126
+ if (!p) return;
1127
+ clearTimeout(p.timer);
1128
+ this.pending.delete(approval.id);
1129
+ this.store.resolveApproval(approval.id, "denied", "user", null, "Turn interrupted");
1130
+ resolve({ behavior: "deny", message: "Interrupted.", via: "user" });
1131
+ });
1132
+ });
1133
+ }
1134
+ /** Resolve a pending approval from the UI. Returns the updated row. */
1135
+ decide(approvalId, decision, alwaysRule) {
1136
+ const p = this.pending.get(approvalId);
1137
+ const existing = this.store.getApproval(approvalId);
1138
+ if (!existing) return null;
1139
+ if (existing.status !== "pending") return existing;
1140
+ let ruleId = null;
1141
+ if (decision === "allow" && alwaysRule) {
1142
+ const rule = this.store.createRule({
1143
+ kind: "allow",
1144
+ toolPattern: alwaysRule.toolPattern,
1145
+ inputPattern: alwaysRule.inputPattern ?? "",
1146
+ scopeNote: alwaysRule.scopeNote ?? "Saved from an approval"
1147
+ });
1148
+ ruleId = rule.id;
1149
+ }
1150
+ const updated = this.store.resolveApproval(
1151
+ approvalId,
1152
+ decision === "allow" ? "allowed" : "denied",
1153
+ "user",
1154
+ ruleId,
1155
+ decision === "allow" ? "Approved by you" : "Denied by you"
1156
+ );
1157
+ this.bus.publish({
1158
+ type: "approval.resolved",
1159
+ threadId: existing.threadId,
1160
+ botId: existing.botId,
1161
+ approval: updated
1162
+ });
1163
+ if (p) {
1164
+ clearTimeout(p.timer);
1165
+ this.pending.delete(approvalId);
1166
+ p.resolve(
1167
+ decision === "allow" ? { behavior: "allow", reason: "Approved by you", via: "user" } : { behavior: "deny", message: "You denied this action.", via: "user" }
1168
+ );
1169
+ }
1170
+ return updated;
1171
+ }
1172
+ hasPending(approvalId) {
1173
+ return this.pending.has(approvalId);
1174
+ }
1175
+ /** Cancel any pending approvals for a bot (used when a turn is interrupted). */
1176
+ cancelForBot(botId) {
1177
+ for (const [id, p] of [...this.pending]) {
1178
+ const a = this.store.getApproval(id);
1179
+ if (a?.botId !== botId) continue;
1180
+ clearTimeout(p.timer);
1181
+ this.pending.delete(id);
1182
+ this.store.resolveApproval(id, "denied", "user", null, "Turn interrupted");
1183
+ p.resolve({ behavior: "deny", message: "Interrupted.", via: "user" });
1184
+ }
1185
+ }
1186
+ };
1187
+
1188
+ // packages/server/src/permissions/autoreview.ts
1189
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
1190
+
1191
+ // packages/server/src/agent/session.ts
1192
+ import { query } from "@anthropic-ai/claude-agent-sdk";
1193
+ var log4 = logger("agent");
1194
+ function resolveModel(tier) {
1195
+ return tier;
1196
+ }
1197
+ function buildEnv(settings, base = process.env) {
1198
+ const env = { ...base };
1199
+ if (settings.billingMode !== "api") {
1200
+ delete env.ANTHROPIC_API_KEY;
1201
+ delete env.ANTHROPIC_AUTH_TOKEN;
1202
+ }
1203
+ return env;
1204
+ }
1205
+ async function* runTurn(req) {
1206
+ const abort = req.abortController ?? new AbortController();
1207
+ const options = {
1208
+ model: resolveModel(req.modelTier),
1209
+ systemPrompt: { type: "preset", preset: "claude_code", append: req.systemPrompt },
1210
+ cwd: req.cwd,
1211
+ additionalDirectories: req.additionalDirectories,
1212
+ abortController: abort,
1213
+ includePartialMessages: true,
1214
+ permissionMode: "default",
1215
+ canUseTool: req.canUseTool,
1216
+ mcpServers: req.mcpServers,
1217
+ env: buildEnv(req.settings),
1218
+ // Do not inherit the user's own Claude Code project settings into bot turns.
1219
+ settingSources: [],
1220
+ maxTurns: 60
1221
+ };
1222
+ if (req.resumeSessionId) options.resume = req.resumeSessionId;
1223
+ if (req.skillPluginPath) {
1224
+ options.plugins = [{ type: "local", path: req.skillPluginPath }];
1225
+ options.skills = req.enabledSkills ?? [];
1226
+ }
1227
+ let q;
1228
+ try {
1229
+ q = query({ prompt: req.prompt, options });
1230
+ } catch (err) {
1231
+ yield { kind: "error", message: err instanceof Error ? err.message : String(err) };
1232
+ return;
1233
+ }
1234
+ const seenToolIds = /* @__PURE__ */ new Set();
1235
+ try {
1236
+ for await (const msg of q) {
1237
+ const m = msg;
1238
+ switch (m.type) {
1239
+ case "system":
1240
+ if (m.subtype === "init" && m.session_id) yield { kind: "session", sessionId: m.session_id };
1241
+ break;
1242
+ case "stream_event": {
1243
+ const ev = m.event;
1244
+ if (ev?.type === "content_block_delta" && ev.delta?.type === "text_delta" && ev.delta.text)
1245
+ yield { kind: "text", text: ev.delta.text };
1246
+ break;
1247
+ }
1248
+ case "assistant": {
1249
+ for (const block of m.message?.content ?? []) {
1250
+ if (block.type === "tool_use" && !seenToolIds.has(block.id)) {
1251
+ seenToolIds.add(block.id);
1252
+ yield { kind: "tool_start", toolName: block.name, toolInput: block.input, toolUseId: block.id };
1253
+ }
1254
+ }
1255
+ break;
1256
+ }
1257
+ case "user": {
1258
+ for (const block of m.message?.content ?? []) {
1259
+ if (block.type === "tool_result") {
1260
+ const content = Array.isArray(block.content) ? block.content.map((c) => typeof c?.text === "string" ? c.text : "").join("\n") : typeof block.content === "string" ? block.content : "";
1261
+ yield {
1262
+ kind: "tool_result",
1263
+ toolUseId: block.tool_use_id,
1264
+ result: content.slice(0, 4e3),
1265
+ isError: Boolean(block.is_error)
1266
+ };
1267
+ }
1268
+ }
1269
+ break;
1270
+ }
1271
+ case "result": {
1272
+ const u = m.usage ?? {};
1273
+ yield {
1274
+ kind: "done",
1275
+ sessionId: m.session_id,
1276
+ text: typeof m.result === "string" ? m.result : void 0,
1277
+ isError: m.subtype !== "success",
1278
+ usage: {
1279
+ model: m.modelUsage ? Object.keys(m.modelUsage)[0] ?? resolveModel(req.modelTier) : resolveModel(req.modelTier),
1280
+ inputTokens: u.input_tokens ?? 0,
1281
+ outputTokens: u.output_tokens ?? 0,
1282
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
1283
+ costUsd: m.total_cost_usd ?? 0
1284
+ }
1285
+ };
1286
+ break;
1287
+ }
1288
+ default:
1289
+ break;
1290
+ }
1291
+ }
1292
+ } catch (err) {
1293
+ if (abort.signal.aborted) {
1294
+ yield { kind: "error", message: "Interrupted." };
1295
+ return;
1296
+ }
1297
+ log4.error("turn failed", err);
1298
+ yield { kind: "error", message: err instanceof Error ? err.message : String(err) };
1299
+ }
1300
+ }
1301
+
1302
+ // packages/server/src/permissions/autoreview.ts
1303
+ var log5 = logger("autoreview");
1304
+ var SYSTEM = `You are the automated action reviewer for a local AI-teammate system.
1305
+ You judge ONE proposed tool call and answer with a single JSON object.
1306
+
1307
+ Answer "allow_ok" only when the action is clearly routine, reversible and
1308
+ low-consequence \u2014 reading files, inspecting state, searching, navigating to a
1309
+ normal public page, writing inside the bot's own workspace.
1310
+
1311
+ Answer "needs_human" when the action is consequential or ambiguous: sending or
1312
+ publishing anything, contacting a person, spending money, changing permissions,
1313
+ touching production, deleting or overwriting data outside the workspace,
1314
+ installing software, or anything whose blast radius you cannot determine.
1315
+
1316
+ Answer "deny_suggested" when the action looks actively unsafe or like an attempt
1317
+ to exfiltrate credentials.
1318
+
1319
+ You are advisory only and you are NOT the last line of defence. When in doubt,
1320
+ choose needs_human.
1321
+
1322
+ Reply with ONLY: {"verdict":"allow_ok|needs_human|deny_suggested","reason":"<12 words max>"}`;
1323
+ var HaikuAutoReviewer = class {
1324
+ constructor(getSettings, cwd) {
1325
+ this.getSettings = getSettings;
1326
+ this.cwd = cwd;
1327
+ }
1328
+ getSettings;
1329
+ cwd;
1330
+ async classify(toolName, input, botDescription) {
1331
+ const prompt = `Bot's standing job description:
1332
+ ${botDescription.slice(0, 800) || "(none)"}
1333
+
1334
+ Proposed tool call:
1335
+ tool: ${toolName}
1336
+ summary: ${summarize(toolName, input)}
1337
+ raw input: ${JSON.stringify(input).slice(0, 1500)}`;
1338
+ const q = query2({
1339
+ prompt,
1340
+ options: {
1341
+ model: "haiku",
1342
+ systemPrompt: SYSTEM,
1343
+ cwd: this.cwd,
1344
+ settingSources: [],
1345
+ env: buildEnv(this.getSettings()),
1346
+ maxTurns: 1,
1347
+ allowedTools: [],
1348
+ permissionMode: "default"
1349
+ }
1350
+ });
1351
+ let out = "";
1352
+ for await (const m of q) {
1353
+ const msg = m;
1354
+ if (msg.type === "result" && typeof msg.result === "string") out = msg.result;
1355
+ }
1356
+ return parseVerdict(out);
1357
+ }
1358
+ };
1359
+ function parseVerdict(text) {
1360
+ const fallback = { verdict: "needs_human", reason: "Reviewer output was unreadable" };
1361
+ if (!text) return fallback;
1362
+ const match = text.match(/\{[\s\S]*\}/);
1363
+ if (!match) return fallback;
1364
+ try {
1365
+ const o = JSON.parse(match[0]);
1366
+ if (o.verdict === "allow_ok" || o.verdict === "needs_human" || o.verdict === "deny_suggested")
1367
+ return { verdict: o.verdict, reason: String(o.reason ?? "").slice(0, 200) };
1368
+ return fallback;
1369
+ } catch {
1370
+ return fallback;
1371
+ }
1372
+ }
1373
+ var NullAutoReviewer = class {
1374
+ async classify() {
1375
+ return { verdict: "needs_human", reason: "Auto review disabled" };
1376
+ }
1377
+ };
1378
+ function makeAutoReviewer(getSettings, cwd) {
1379
+ try {
1380
+ return new HaikuAutoReviewer(getSettings, cwd);
1381
+ } catch (err) {
1382
+ log5.warn("falling back to null reviewer", err);
1383
+ return new NullAutoReviewer();
1384
+ }
1385
+ }
1386
+
1387
+ // packages/server/src/bots/manager.ts
1388
+ import path5 from "node:path";
1389
+ import fs4 from "node:fs";
1390
+ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
1391
+ import { z } from "zod";
1392
+
1393
+ // packages/server/src/memory/memory.ts
1394
+ import fs3 from "node:fs";
1395
+ import path4 from "node:path";
1396
+ function memoryDir(workspace, slug) {
1397
+ return path4.join(workspace, "bots", slug, "memory");
1398
+ }
1399
+ function ensureMemoryDir(workspace, slug) {
1400
+ const dir = memoryDir(workspace, slug);
1401
+ fs3.mkdirSync(dir, { recursive: true });
1402
+ return dir;
1403
+ }
1404
+ function readMemory(workspace, slug) {
1405
+ const dir = memoryDir(workspace, slug);
1406
+ if (!fs3.existsSync(dir)) return [];
1407
+ return fs3.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((name) => ({ name, content: fs3.readFileSync(path4.join(dir, name), "utf8") }));
1408
+ }
1409
+ function writeMemory(workspace, slug, name, content) {
1410
+ const dir = ensureMemoryDir(workspace, slug);
1411
+ const safe = name.replace(/[^a-zA-Z0-9._-]/g, "-");
1412
+ fs3.writeFileSync(path4.join(dir, safe.endsWith(".md") ? safe : `${safe}.md`), content);
1413
+ }
1414
+ function deleteMemory(workspace, slug, name) {
1415
+ const f = path4.join(memoryDir(workspace, slug), name.replace(/[^a-zA-Z0-9._-]/g, "-"));
1416
+ if (fs3.existsSync(f)) fs3.unlinkSync(f);
1417
+ }
1418
+ function renderMemoryBlock(files) {
1419
+ if (!files.length) return "";
1420
+ const body = files.map((f) => `### ${f.name}
1421
+ ${f.content.trim()}`).join("\n\n");
1422
+ return `
1423
+
1424
+ ## Your memory
1425
+ Stable preferences and facts you recorded earlier. Memory is a working
1426
+ note, not an authoritative source \u2014 re-check the source system before any
1427
+ consequential decision.
1428
+
1429
+ ${body}`;
1430
+ }
1431
+
1432
+ // packages/server/src/bots/prompt.ts
1433
+ function buildSystemPrompt(ctx) {
1434
+ const { bot, workspace } = ctx;
1435
+ const parts = [];
1436
+ parts.push(`You are **${bot.name}**${bot.title ? `, ${bot.title}` : ""} \u2014 a persistent AI teammate in ant-bot.
1437
+
1438
+ You are not a general chat assistant. You own a job and finish work end to end,
1439
+ then report back with evidence. You keep working across turns; your files,
1440
+ memory and browser sessions persist.`);
1441
+ if (bot.description.trim()) {
1442
+ parts.push(`## Your standing job description
1443
+ These are durable rules for how you work. They outrank any single message.
1444
+
1445
+ ${bot.description.trim()}`);
1446
+ }
1447
+ parts.push(`## Your computer
1448
+ You share one computer with every other bot on this account.
1449
+ - Shared workspace: \`${workspace}\` \u2014 keep durable project files here.
1450
+ - Your own folder: \`${workspace}/bots/${bot.slug}/\`
1451
+ - Files, logins and installed tools are visible to all bots. Bots are **not** a
1452
+ security boundary. Do not store anything here another bot should not see.`);
1453
+ const mem = renderMemoryBlock(readMemory(workspace, bot.slug));
1454
+ if (mem) parts.push(mem.trim());
1455
+ if (ctx.skills.length) {
1456
+ parts.push(`## Your skills
1457
+ ${ctx.skills.map((s) => `- **${s.name}** (${s.slug}): ${s.description}`).join("\n")}
1458
+ Read the skill file before following it.`);
1459
+ }
1460
+ const others = ctx.roster.filter((r) => r.slug !== bot.slug);
1461
+ if (others.length) {
1462
+ parts.push(`## Your teammates
1463
+ ${others.map((r) => `- @${r.slug} \u2014 ${r.name}${r.title ? `, ${r.title}` : ""}`).join("\n")}
1464
+
1465
+ Use the \`send_to_bot\` tool to hand work to a teammate when the job genuinely
1466
+ belongs to their role. Keep one owner per stage \u2014 do not fan the same task out
1467
+ to several bots at once.`);
1468
+ }
1469
+ if (ctx.isGroup) {
1470
+ parts.push(`## Group conversation
1471
+ You are in a group chat. Other bots can see and reply to these messages.
1472
+ Answer only when the request is yours to own, or when you were @-mentioned.
1473
+ Say who should take the next step. Keep replies short \u2014 the humans are reading.`);
1474
+ }
1475
+ parts.push(`## How to work
1476
+ - Lead with the result. Put evidence \u2014 links, file paths, quoted output \u2014 under it.
1477
+ - Separate: facts found, assumptions, actions taken, actions awaiting approval,
1478
+ open questions.
1479
+ - Write deliverables as real files in the workspace, not as walls of chat text.
1480
+ - Never guess at a credential. If a step needs a password, 2FA code or CAPTCHA,
1481
+ stop and ask the human to take over the computer.
1482
+ - Some actions pause for the human's approval. That is normal \u2014 propose the
1483
+ action and wait. Approval covers only the proposed step; it does not undo
1484
+ anything you already did.`);
1485
+ return parts.join("\n\n");
1486
+ }
1487
+
1488
+ // packages/server/src/bots/manager.ts
1489
+ var log6 = logger("bots");
1490
+ var BotManager = class {
1491
+ constructor(deps) {
1492
+ this.deps = deps;
1493
+ }
1494
+ deps;
1495
+ queue = [];
1496
+ running = /* @__PURE__ */ new Map();
1497
+ draining = false;
1498
+ get runningCount() {
1499
+ return this.running.size;
1500
+ }
1501
+ get queuedCount() {
1502
+ return this.queue.length;
1503
+ }
1504
+ isBusy(botId) {
1505
+ return this.running.has(botId) || this.queue.some((j) => j.botId === botId);
1506
+ }
1507
+ /** Enqueue a turn. Interactive work sorts ahead of routine work. */
1508
+ enqueue(job) {
1509
+ const full = {
1510
+ ...job,
1511
+ id: newId(),
1512
+ priority: job.priority ?? (job.origin === "routine" ? 10 : 0)
1513
+ };
1514
+ this.queue.push(full);
1515
+ this.queue.sort((a, b2) => a.priority - b2.priority);
1516
+ this.setState(full.botId, "queued");
1517
+ void this.drain();
1518
+ return full;
1519
+ }
1520
+ async drain() {
1521
+ if (this.draining) return;
1522
+ this.draining = true;
1523
+ try {
1524
+ const max = this.deps.getSettings().maxConcurrentSessions ?? LIMITS.DEFAULT_MAX_CONCURRENT_SESSIONS;
1525
+ while (this.queue.length && this.running.size < max) {
1526
+ const idx = this.queue.findIndex((j) => !this.running.has(j.botId));
1527
+ if (idx === -1) break;
1528
+ const [job] = this.queue.splice(idx, 1);
1529
+ void this.execute(job);
1530
+ }
1531
+ } finally {
1532
+ this.draining = false;
1533
+ }
1534
+ }
1535
+ setState(botId, state, attention) {
1536
+ const bot = this.deps.store.updateBot(botId, { state, ...attention ? { attention } : {} });
1537
+ if (!bot) return;
1538
+ this.deps.bus.publish({
1539
+ type: "bot.state",
1540
+ botId,
1541
+ threadId: bot.threadId,
1542
+ state: bot.state,
1543
+ attention: bot.attention
1544
+ });
1545
+ }
1546
+ interrupt(botId) {
1547
+ const r = this.running.get(botId);
1548
+ this.queue = this.queue.filter((j) => j.botId !== botId);
1549
+ this.deps.gateway.cancelForBot(botId);
1550
+ if (!r) {
1551
+ this.setState(botId, "idle");
1552
+ return false;
1553
+ }
1554
+ r.abort.abort();
1555
+ return true;
1556
+ }
1557
+ /** Custom tools exposed to every bot turn: handoff, memory, secrets-safe helpers. */
1558
+ buildToolServer(bot, threadId, hops) {
1559
+ const { store, workspace } = this.deps;
1560
+ return createSdkMcpServer({
1561
+ name: "antbot",
1562
+ version: "1.0.0",
1563
+ tools: [
1564
+ tool(
1565
+ "send_to_bot",
1566
+ "Hand work to another bot on this account. The recipient wakes, handles the request in its own thread, and can reply later. Use when the job genuinely belongs to that role.",
1567
+ { bot_slug: z.string().describe('slug of the teammate, e.g. "writer"'), message: z.string().describe("the request, with all context they need") },
1568
+ async (args) => {
1569
+ const target = store.getBotBySlug(args.bot_slug);
1570
+ if (!target) return { content: [{ type: "text", text: `No bot with slug "${args.bot_slug}". Use one of: ${store.listBots().map((b2) => b2.slug).join(", ")}` }] };
1571
+ if (target.id === bot.id) return { content: [{ type: "text", text: "You cannot hand work to yourthis." }] };
1572
+ if (hops + 1 > LIMITS.MAX_BOT_TO_BOT_HOPS)
1573
+ return { content: [{ type: "text", text: `Hop limit of ${LIMITS.MAX_BOT_TO_BOT_HOPS} reached. Stop and report back to the human instead.` }] };
1574
+ store.createMail({ fromBotId: bot.id, toBotId: target.id, contentMd: args.message, hops: hops + 1 });
1575
+ this.postSystemCard(threadId, bot.id, { type: "handoff", fromBotId: bot.id, toBotId: target.id, note: args.message.slice(0, 300) });
1576
+ this.enqueue({
1577
+ botId: target.id,
1578
+ threadId: target.threadId,
1579
+ origin: "bot",
1580
+ hops: hops + 1,
1581
+ prompt: `**Handoff from @${bot.slug} (${bot.name}):**
1582
+
1583
+ ${args.message}
1584
+
1585
+ ---
1586
+ Handle this, then reply. If it belongs to someone else, say so rather than guessing.`
1587
+ });
1588
+ return { content: [{ type: "text", text: `Handed to @${target.slug}. They will pick it up and reply in their own thread.` }] };
1589
+ }
1590
+ ),
1591
+ tool(
1592
+ "remember",
1593
+ "Save a durable preference or fact to your memory so it survives future turns. Use only for stable things, never for changing data.",
1594
+ { title: z.string().describe("short kebab-case file name"), note: z.string().describe("the fact, in markdown") },
1595
+ async (args) => {
1596
+ const dir = ensureMemoryDir(workspace, bot.slug);
1597
+ const file = path5.join(dir, `${args.title.replace(/[^a-zA-Z0-9._-]/g, "-")}.md`);
1598
+ fs4.writeFileSync(file, args.note);
1599
+ return { content: [{ type: "text", text: `Saved to memory: ${path5.basename(file)}` }] };
1600
+ }
1601
+ ),
1602
+ tool(
1603
+ "list_skills",
1604
+ "List every skill installed on this account, with its slug. Check here before installing something that may already be present, and to find the exact slug to pass to remove_skill.",
1605
+ {},
1606
+ async () => {
1607
+ const skills = this.deps.listSkills?.() ?? [];
1608
+ if (!skills.length)
1609
+ return { content: [{ type: "text", text: "No skills are installed." }] };
1610
+ const lines = skills.map((sk) => `- ${sk.slug} \u2014 ${sk.name}${sk.description ? `: ${sk.description}` : ""}`);
1611
+ return { content: [{ type: "text", text: `${skills.length} skill(s) installed:
1612
+ ${lines.join("\n")}` }] };
1613
+ }
1614
+ ),
1615
+ tool(
1616
+ "install_skill",
1617
+ "Install a skill so you (and other bots) can use it. This always stops for the human's approval first, because a skill is instructions that will steer future work and may ship scripts. Check list_skills before installing something you already have.\nInstall the NARROWEST source that covers the request. Accepted forms:\n github.com/owner/repo/tree/<ref>/<dir> one skill inside a repository \u2014 prefer this\n https://host/path/SKILL.md one skill, direct link\n ./path/to/skill a local directory\n owner/repo, github.com/owner/repo EVERY skill in the repository\nA bare owner/repo pointing at a collection installs all of it. If the human asked for one named skill, point at that skill's directory instead; if you only have a link to the repo root, call this anyway and the error will list what is inside so you can narrow it.",
1618
+ {
1619
+ source: z.string().describe("prefer owner/repo/tree/<ref>/<dir> or a /SKILL.md link; owner/repo installs the whole repository"),
1620
+ reason: z.string().describe("why you need this skill for the task at hand"),
1621
+ install_all: z.boolean().optional().describe("set true only when the human has asked for every skill in a multi-skill source")
1622
+ },
1623
+ async (args) => {
1624
+ if (!this.deps.installSkill)
1625
+ return { content: [{ type: "text", text: "Skill installation is unavailable on this server." }] };
1626
+ try {
1627
+ const installed = await this.deps.installSkill(args.source, { allowMultiple: args.install_all === true });
1628
+ if (!installed.length)
1629
+ return { content: [{ type: "text", text: `No skill found at "${args.source}".` }] };
1630
+ const names = installed.map((i2) => i2.name).join(", ");
1631
+ const scripts = installed.flatMap((i2) => i2.executables);
1632
+ const note = scripts.length ? ` Note for the human: this shipped ${scripts.length} script(s): ${scripts.join(", ")}.` : "";
1633
+ return {
1634
+ content: [{
1635
+ type: "text",
1636
+ text: `Installed: ${names}.${note} A skill must still be assigned to you in Bot settings before you can invoke it \u2014 ask the human to enable it, then retry.`
1637
+ }]
1638
+ };
1639
+ } catch (err) {
1640
+ const e = err;
1641
+ if (e.name === "MultipleSkillsError" && Array.isArray(e.names)) {
1642
+ const listed = e.names.slice(0, 40).join(", ");
1643
+ const more = e.names.length > 40 ? `, and ${e.names.length - 40} more` : "";
1644
+ return {
1645
+ content: [{
1646
+ type: "text",
1647
+ text: `Nothing was installed. "${args.source}" holds ${e.names.length} skills: ${listed}${more}.
1648
+ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\/\//, "").replace(/\/$/, "")}/tree/main/<skill-directory>. Only pass install_all if the human has asked for all of them.`
1649
+ }]
1650
+ };
1651
+ }
1652
+ return { content: [{ type: "text", text: `Install failed: ${e.message}` }] };
1653
+ }
1654
+ }
1655
+ ),
1656
+ tool(
1657
+ "remove_skill",
1658
+ "Uninstall a skill by slug \u2014 deletes its directory and its registration together. Use this rather than deleting skill directories with Bash, which would leave the registry pointing at files that no longer exist. Stops for the human's approval.",
1659
+ {
1660
+ slug: z.string().describe("the skill slug, exactly as list_skills reports it"),
1661
+ reason: z.string().describe("why this skill should be removed")
1662
+ },
1663
+ async (args) => {
1664
+ if (!this.deps.removeSkill)
1665
+ return { content: [{ type: "text", text: "Skill removal is unavailable on this server." }] };
1666
+ try {
1667
+ const res = await this.deps.removeSkill(args.slug);
1668
+ if (!res.removed) {
1669
+ const known = (this.deps.listSkills?.() ?? []).map((sk) => sk.slug).join(", ");
1670
+ return { content: [{ type: "text", text: `No skill with slug "${args.slug}". Installed: ${known || "none"}.` }] };
1671
+ }
1672
+ return { content: [{ type: "text", text: `Removed "${args.slug}"${res.name ? ` (${res.name})` : ""}.` }] };
1673
+ } catch (err) {
1674
+ return { content: [{ type: "text", text: `Remove failed: ${err.message}` }] };
1675
+ }
1676
+ }
1677
+ ),
1678
+ tool(
1679
+ "request_secret",
1680
+ "Ask the human for a secret value (API key, token). The value goes straight to the keychain and is never shown to you. Never ask for passwords in chat \u2014 ask for computer takeover instead.",
1681
+ { name: z.string().describe("identifier, e.g. STRIPE_API_KEY"), reason: z.string() },
1682
+ async (args) => {
1683
+ this.deps.bus.publish({ type: "secret.request", botId: bot.id, threadId, requestId: newId(), name: args.name, reason: args.reason });
1684
+ return { content: [{ type: "text", text: `Asked the human for "${args.name}". It will be injected into your environment as that variable name; you will never see the value. Continue once they confirm.` }] };
1685
+ }
1686
+ )
1687
+ ]
1688
+ });
1689
+ }
1690
+ postSystemCard(threadId, botId, card) {
1691
+ const msg = this.deps.store.createMessage({ threadId, authorKind: "system", authorBotId: botId, cards: [card] });
1692
+ this.deps.bus.publish({ type: "message.created", threadId, botId, message: msg });
1693
+ }
1694
+ async execute(job) {
1695
+ const { store, bus, gateway, workspace, getSettings } = this.deps;
1696
+ const bot = store.getBot(job.botId);
1697
+ if (!bot) return;
1698
+ const abort = new AbortController();
1699
+ this.running.set(job.botId, { job, abort });
1700
+ this.setState(job.botId, "running", "none");
1701
+ const settings = getSettings();
1702
+ const msg = store.createMessage({
1703
+ threadId: job.threadId,
1704
+ authorKind: "bot",
1705
+ authorBotId: bot.id,
1706
+ contentMd: "",
1707
+ streaming: true
1708
+ });
1709
+ bus.publish({ type: "message.created", threadId: job.threadId, botId: bot.id, message: msg });
1710
+ const thread = store.getThread(job.threadId);
1711
+ const isGroup = thread?.kind === "group";
1712
+ const botDir = path5.join(workspace, "bots", bot.slug);
1713
+ fs4.mkdirSync(botDir, { recursive: true });
1714
+ const botSkills = store.listBotSkills(bot.id);
1715
+ const systemPrompt = buildSystemPrompt({
1716
+ bot,
1717
+ workspace,
1718
+ skills: botSkills,
1719
+ roster: store.listBots().map((x) => ({ slug: x.slug, name: x.name, title: x.title })),
1720
+ isGroup
1721
+ });
1722
+ let text = "";
1723
+ let ok = true;
1724
+ let errorMessage = "";
1725
+ const toolCards = /* @__PURE__ */ new Map();
1726
+ const mcpServers = { antbot: this.buildToolServer(bot, job.threadId, job.hops) };
1727
+ const browser = this.deps.browserTools?.(bot.id);
1728
+ if (browser) mcpServers.browser = browser;
1729
+ try {
1730
+ for await (const ev of runTurn({
1731
+ prompt: job.prompt,
1732
+ resumeSessionId: bot.sessionId,
1733
+ modelTier: bot.modelTier,
1734
+ systemPrompt,
1735
+ cwd: workspace,
1736
+ settings,
1737
+ abortController: abort,
1738
+ mcpServers,
1739
+ skillPluginPath: this.deps.skillPluginPath?.(),
1740
+ // Skill names come from SKILL.md frontmatter, which is what the SDK matches on.
1741
+ enabledSkills: botSkills.map((s) => s.name),
1742
+ canUseTool: async (toolName, input) => {
1743
+ this.setState(job.botId, "waiting_approval", "needs_attention");
1744
+ const d = await gateway.check({
1745
+ botId: bot.id,
1746
+ threadId: job.threadId,
1747
+ toolName,
1748
+ input,
1749
+ botDescription: bot.description,
1750
+ settings,
1751
+ signal: abort.signal,
1752
+ workspace,
1753
+ // Render the approval inline in the transcript so the human can act on it
1754
+ // where the work is happening, not only in a global queue.
1755
+ onPending: (approval) => {
1756
+ const card = { type: "approval", approvalId: approval.id };
1757
+ const idx = store.appendCard(msg.id, card);
1758
+ bus.publish({
1759
+ type: "message.card",
1760
+ threadId: job.threadId,
1761
+ botId: bot.id,
1762
+ messageId: msg.id,
1763
+ card,
1764
+ cardIndex: idx
1765
+ });
1766
+ }
1767
+ });
1768
+ if (!abort.signal.aborted) this.setState(job.botId, "running");
1769
+ return d.behavior === "allow" ? { behavior: "allow", updatedInput: input } : { behavior: "deny", message: d.message };
1770
+ }
1771
+ })) {
1772
+ await this.applyEvent(ev, { job, bot, msgId: msg.id, toolCards });
1773
+ if (ev.kind === "text" && ev.text) text += ev.text;
1774
+ if (ev.kind === "done") {
1775
+ if (ev.text && !text.trim()) text = ev.text;
1776
+ ok = !ev.isError;
1777
+ }
1778
+ if (ev.kind === "error") {
1779
+ ok = false;
1780
+ errorMessage = ev.message ?? "Unknown error";
1781
+ }
1782
+ }
1783
+ } catch (err) {
1784
+ ok = false;
1785
+ errorMessage = err instanceof Error ? err.message : String(err);
1786
+ log6.error("turn crashed", err);
1787
+ } finally {
1788
+ this.running.delete(job.botId);
1789
+ }
1790
+ if (errorMessage) {
1791
+ const idx = store.appendCard(msg.id, { type: "error", message: errorMessage });
1792
+ bus.publish({ type: "message.card", threadId: job.threadId, botId: bot.id, messageId: msg.id, card: { type: "error", message: errorMessage }, cardIndex: idx });
1793
+ }
1794
+ const final = text.trim();
1795
+ store.updateMessage(msg.id, { contentMd: final, streaming: false });
1796
+ bus.publish({ type: "message.done", threadId: job.threadId, botId: bot.id, messageId: msg.id, contentMd: final });
1797
+ const interrupted = abort.signal.aborted;
1798
+ this.setState(job.botId, interrupted ? "interrupted" : "idle", "unread");
1799
+ if (interrupted) this.setState(job.botId, "idle", "unread");
1800
+ job.onDone?.(final || errorMessage, ok && !interrupted);
1801
+ void this.drain();
1802
+ }
1803
+ async applyEvent(ev, ctx) {
1804
+ const { store, bus } = this.deps;
1805
+ const { job, bot, msgId, toolCards } = ctx;
1806
+ switch (ev.kind) {
1807
+ case "session":
1808
+ if (ev.sessionId) store.updateBot(bot.id, { sessionId: ev.sessionId });
1809
+ break;
1810
+ case "text":
1811
+ if (ev.text) {
1812
+ const cur = store.getMessage(msgId);
1813
+ store.updateMessage(msgId, { contentMd: (cur?.contentMd ?? "") + ev.text });
1814
+ bus.publish({ type: "message.delta", threadId: job.threadId, botId: bot.id, messageId: msgId, delta: ev.text });
1815
+ }
1816
+ break;
1817
+ case "tool_start": {
1818
+ const card = {
1819
+ type: "tool",
1820
+ toolName: ev.toolName ?? "tool",
1821
+ summary: summarizeTool(ev.toolName ?? "", ev.toolInput),
1822
+ input: ev.toolInput,
1823
+ status: "running"
1824
+ };
1825
+ const idx = store.appendCard(msgId, card);
1826
+ if (ev.toolUseId) toolCards.set(ev.toolUseId, idx);
1827
+ bus.publish({ type: "message.card", threadId: job.threadId, botId: bot.id, messageId: msgId, card, cardIndex: idx });
1828
+ break;
1829
+ }
1830
+ case "tool_result": {
1831
+ const idx = ev.toolUseId ? toolCards.get(ev.toolUseId) : void 0;
1832
+ if (idx === void 0) break;
1833
+ const cur = store.getMessage(msgId);
1834
+ const existing = cur?.cards[idx];
1835
+ if (!existing || existing.type !== "tool") break;
1836
+ const denied = ev.isError && /denied|approval/i.test(ev.result ?? "");
1837
+ const card = {
1838
+ ...existing,
1839
+ status: denied ? "denied" : ev.isError ? "error" : "ok",
1840
+ result: (ev.result ?? "").slice(0, 2e3)
1841
+ };
1842
+ store.updateCard(msgId, idx, card);
1843
+ bus.publish({ type: "message.card", threadId: job.threadId, botId: bot.id, messageId: msgId, card, cardIndex: idx });
1844
+ break;
1845
+ }
1846
+ case "done":
1847
+ if (ev.sessionId) store.updateBot(bot.id, { sessionId: ev.sessionId });
1848
+ if (ev.usage) {
1849
+ store.recordUsage({
1850
+ botId: bot.id,
1851
+ turnId: job.id,
1852
+ model: ev.usage.model,
1853
+ inputTokens: ev.usage.inputTokens,
1854
+ outputTokens: ev.usage.outputTokens,
1855
+ cacheReadTokens: ev.usage.cacheReadTokens,
1856
+ costEstimate: ev.usage.costUsd
1857
+ });
1858
+ bus.publish({
1859
+ type: "usage.tick",
1860
+ threadId: job.threadId,
1861
+ botId: bot.id,
1862
+ inputTokens: ev.usage.inputTokens,
1863
+ outputTokens: ev.usage.outputTokens,
1864
+ model: ev.usage.model
1865
+ });
1866
+ }
1867
+ break;
1868
+ default:
1869
+ break;
1870
+ }
1871
+ }
1872
+ };
1873
+ function summarizeTool(name, input) {
1874
+ const o = input ?? {};
1875
+ const s = (k) => typeof o[k] === "string" ? o[k] : "";
1876
+ if (name === "Bash") return s("command").slice(0, 160);
1877
+ if (name === "Read" || name === "Write" || name === "Edit") return s("file_path");
1878
+ if (name === "WebFetch") return s("url");
1879
+ if (name.includes("send_to_bot")) return `\u2192 @${s("bot_slug")}`;
1880
+ if (name.includes("install_skill")) return describeSkillSource(s("source"));
1881
+ if (name.includes("remove_skill")) return `remove skill: ${s("slug")}`;
1882
+ if (name.includes("list_skills")) return "list installed skills";
1883
+ if (name.includes("remember")) return `memory: ${s("title")}`;
1884
+ if (name.startsWith("browser_") || name.includes("browser")) return [s("url"), s("selector"), s("text")].filter(Boolean).join(" ").slice(0, 160);
1885
+ const j = JSON.stringify(input ?? {});
1886
+ return j.length > 160 ? `${j.slice(0, 160)}\u2026` : j;
1887
+ }
1888
+
1889
+ // packages/server/src/config/config.ts
1890
+ import fs6 from "node:fs";
1891
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
1892
+
1893
+ // packages/server/src/config/paths.ts
1894
+ import os from "node:os";
1895
+ import path6 from "node:path";
1896
+ import fs5 from "node:fs";
1897
+ function resolvePaths(root) {
1898
+ const base = root ?? process.env.ANTBOT_HOME ?? path6.join(os.homedir(), ".ant-bot");
1899
+ return {
1900
+ root: base,
1901
+ db: path6.join(base, "antbot.db"),
1902
+ config: path6.join(base, "config.toml"),
1903
+ workspace: path6.join(base, "workspace"),
1904
+ attachments: path6.join(base, "attachments"),
1905
+ skills: path6.join(base, "skills"),
1906
+ browserProfile: path6.join(base, "browser-profile"),
1907
+ backups: path6.join(base, "backups"),
1908
+ secrets: path6.join(base, "secrets.json"),
1909
+ logs: path6.join(base, "logs")
1910
+ };
1911
+ }
1912
+ function ensureDirs(p) {
1913
+ for (const d of [
1914
+ p.root,
1915
+ p.workspace,
1916
+ p.attachments,
1917
+ p.skills,
1918
+ p.backups,
1919
+ p.logs,
1920
+ path6.join(p.workspace, "projects"),
1921
+ path6.join(p.workspace, "bots")
1922
+ ]) {
1923
+ fs5.mkdirSync(d, { recursive: true });
1924
+ }
1925
+ }
1926
+
1927
+ // packages/server/src/config/config.ts
1928
+ var DEFAULT_PORT = 4780;
1929
+ var DEFAULT_HOST = "127.0.0.1";
1930
+ function loadConfig(root) {
1931
+ const paths = resolvePaths(root);
1932
+ ensureDirs(paths);
1933
+ let raw = {};
1934
+ if (fs6.existsSync(paths.config)) {
1935
+ try {
1936
+ raw = parseToml(fs6.readFileSync(paths.config, "utf8"));
1937
+ } catch {
1938
+ raw = {};
1939
+ }
1940
+ }
1941
+ const server = raw.server ?? {};
1942
+ const settings = SettingsSchema.parse({
1943
+ ...typeof raw.settings === "object" && raw.settings ? raw.settings : {},
1944
+ timezone: raw.settings?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC"
1945
+ });
1946
+ const cfg = {
1947
+ paths,
1948
+ settings,
1949
+ port: Number(process.env.ANTBOT_PORT ?? server.port ?? DEFAULT_PORT),
1950
+ host: String(server.host ?? DEFAULT_HOST)
1951
+ };
1952
+ if (!fs6.existsSync(paths.config)) writeConfig(cfg);
1953
+ return cfg;
1954
+ }
1955
+ function writeConfig(cfg) {
1956
+ fs6.writeFileSync(
1957
+ cfg.paths.config,
1958
+ stringifyToml({ server: { port: cfg.port, host: cfg.host }, settings: cfg.settings })
1959
+ );
1960
+ }
1961
+
1962
+ // packages/server/src/permissions/secrets.ts
1963
+ import fs7 from "node:fs";
1964
+ import path7 from "node:path";
1965
+ import crypto from "node:crypto";
1966
+ import { execFile } from "node:child_process";
1967
+ import { promisify } from "node:util";
1968
+ var exec = promisify(execFile);
1969
+ var log7 = logger("secrets");
1970
+ var SERVICE = "ant-bot";
1971
+ var SecretToolBackend = class {
1972
+ name = "libsecret (secret-tool)";
1973
+ async set(key, value) {
1974
+ await new Promise((resolve, reject) => {
1975
+ const p = execFile(
1976
+ "secret-tool",
1977
+ ["store", "--label", `${SERVICE}: ${key}`, "service", SERVICE, "account", key],
1978
+ (err) => err ? reject(err) : resolve()
1979
+ );
1980
+ p.stdin?.end(value);
1981
+ });
1982
+ }
1983
+ async get(key) {
1984
+ try {
1985
+ const { stdout } = await exec("secret-tool", ["lookup", "service", SERVICE, "account", key]);
1986
+ return stdout;
1987
+ } catch {
1988
+ return null;
1989
+ }
1990
+ }
1991
+ async delete(key) {
1992
+ try {
1993
+ await exec("secret-tool", ["clear", "service", SERVICE, "account", key]);
1994
+ } catch {
1995
+ }
1996
+ }
1997
+ async list() {
1998
+ return [];
1999
+ }
2000
+ // secret-tool has no reliable enumeration
2001
+ };
2002
+ var MacKeychainBackend = class {
2003
+ name = "macOS Keychain";
2004
+ async set(key, value) {
2005
+ await exec("security", ["add-generic-password", "-U", "-s", SERVICE, "-a", key, "-w", value]);
2006
+ }
2007
+ async get(key) {
2008
+ try {
2009
+ const { stdout } = await exec("security", ["find-generic-password", "-s", SERVICE, "-a", key, "-w"]);
2010
+ return stdout.replace(/\n$/, "");
2011
+ } catch {
2012
+ return null;
2013
+ }
2014
+ }
2015
+ async delete(key) {
2016
+ try {
2017
+ await exec("security", ["delete-generic-password", "-s", SERVICE, "-a", key]);
2018
+ } catch {
2019
+ }
2020
+ }
2021
+ async list() {
2022
+ return [];
2023
+ }
2024
+ };
2025
+ var EncryptedFileBackend = class {
2026
+ constructor(file) {
2027
+ this.file = file;
2028
+ this.keyFile = `${file}.key`;
2029
+ }
2030
+ file;
2031
+ name = "encrypted file (weaker than a system keychain)";
2032
+ keyFile;
2033
+ key() {
2034
+ if (!fs7.existsSync(this.keyFile)) {
2035
+ fs7.mkdirSync(path7.dirname(this.keyFile), { recursive: true });
2036
+ fs7.writeFileSync(this.keyFile, crypto.randomBytes(32), { mode: 384 });
2037
+ }
2038
+ return fs7.readFileSync(this.keyFile);
2039
+ }
2040
+ read() {
2041
+ if (!fs7.existsSync(this.file)) return {};
2042
+ try {
2043
+ const raw = JSON.parse(fs7.readFileSync(this.file, "utf8"));
2044
+ const key = this.key();
2045
+ const out = {};
2046
+ for (const [k, v] of Object.entries(raw)) {
2047
+ const d = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(v.iv, "base64"));
2048
+ d.setAuthTag(Buffer.from(v.tag, "base64"));
2049
+ out[k] = Buffer.concat([d.update(Buffer.from(v.data, "base64")), d.final()]).toString("utf8");
2050
+ }
2051
+ return out;
2052
+ } catch {
2053
+ return {};
2054
+ }
2055
+ }
2056
+ write(values) {
2057
+ const key = this.key();
2058
+ const out = {};
2059
+ for (const [k, v] of Object.entries(values)) {
2060
+ const iv = crypto.randomBytes(12);
2061
+ const c = crypto.createCipheriv("aes-256-gcm", key, iv);
2062
+ const data = Buffer.concat([c.update(v, "utf8"), c.final()]);
2063
+ out[k] = { iv: iv.toString("base64"), tag: c.getAuthTag().toString("base64"), data: data.toString("base64") };
2064
+ }
2065
+ fs7.mkdirSync(path7.dirname(this.file), { recursive: true });
2066
+ fs7.writeFileSync(this.file, JSON.stringify(out), { mode: 384 });
2067
+ }
2068
+ async set(key, value) {
2069
+ const v = this.read();
2070
+ v[key] = value;
2071
+ this.write(v);
2072
+ }
2073
+ async get(key) {
2074
+ return this.read()[key] ?? null;
2075
+ }
2076
+ async delete(key) {
2077
+ const v = this.read();
2078
+ delete v[key];
2079
+ this.write(v);
2080
+ }
2081
+ async list() {
2082
+ return Object.keys(this.read());
2083
+ }
2084
+ };
2085
+ async function pickBackend(fallbackFile) {
2086
+ const has = async (bin, args) => {
2087
+ try {
2088
+ await exec(bin, args);
2089
+ return true;
2090
+ } catch (err) {
2091
+ return err.code !== "ENOENT";
2092
+ }
2093
+ };
2094
+ if (process.platform === "darwin" && await has("security", ["-h"])) return new MacKeychainBackend();
2095
+ if (process.platform === "linux" && await has("secret-tool", ["--version"])) return new SecretToolBackend();
2096
+ log7.warn("no system keychain available; using the encrypted-file fallback");
2097
+ return new EncryptedFileBackend(fallbackFile);
2098
+ }
2099
+ var SecretsService = class {
2100
+ constructor(backend, indexFile) {
2101
+ this.backend = backend;
2102
+ this.indexFile = indexFile;
2103
+ if (fs7.existsSync(indexFile)) {
2104
+ try {
2105
+ this.names = new Set(JSON.parse(fs7.readFileSync(indexFile, "utf8")));
2106
+ } catch {
2107
+ }
2108
+ }
2109
+ }
2110
+ backend;
2111
+ indexFile;
2112
+ names = /* @__PURE__ */ new Set();
2113
+ persist() {
2114
+ fs7.mkdirSync(path7.dirname(this.indexFile), { recursive: true });
2115
+ fs7.writeFileSync(this.indexFile, JSON.stringify([...this.names]), { mode: 384 });
2116
+ }
2117
+ get backendName() {
2118
+ return this.backend.name;
2119
+ }
2120
+ async set(name, value) {
2121
+ await this.backend.set(name, value);
2122
+ this.names.add(name);
2123
+ this.persist();
2124
+ }
2125
+ async remove(name) {
2126
+ await this.backend.delete(name);
2127
+ this.names.delete(name);
2128
+ this.persist();
2129
+ }
2130
+ /** Names only — values are never returned to the API surface. */
2131
+ list() {
2132
+ return [...this.names];
2133
+ }
2134
+ /** Build an env overlay for a tool subprocess. Values never touch the transcript. */
2135
+ async envOverlay() {
2136
+ const out = {};
2137
+ for (const n of this.names) {
2138
+ const v = await this.backend.get(n);
2139
+ if (v !== null) out[n] = v;
2140
+ }
2141
+ return out;
2142
+ }
2143
+ };
2144
+
2145
+ // packages/server/src/app.ts
2146
+ var log8 = logger("app");
2147
+ async function optionalImport(name, load) {
2148
+ try {
2149
+ return await load();
2150
+ } catch (err) {
2151
+ log8.warn(`${name} module could not be loaded`, err.message);
2152
+ return null;
2153
+ }
2154
+ }
2155
+ async function createApp(opts = {}) {
2156
+ const cfg = loadConfig(opts.root);
2157
+ const db = openDb(cfg.paths.db, { backupsDir: cfg.paths.backups });
2158
+ const store = new Store(db);
2159
+ const persisted = store.getSettings();
2160
+ const settingsCount = store.db.prepare(`SELECT COUNT(*) c FROM settings`).get();
2161
+ if (!settingsCount.c) {
2162
+ store.patchSettings(cfg.settings);
2163
+ }
2164
+ const getSettings = () => store.getSettings();
2165
+ void persisted;
2166
+ seedBuiltinRules(store);
2167
+ const bus = new EventBus();
2168
+ const reviewer = opts.withAgent === false ? new NullAutoReviewer() : makeAutoReviewer(getSettings, cfg.paths.workspace);
2169
+ const gateway = new PermissionGateway(store, bus, reviewer);
2170
+ const app = {
2171
+ cfg,
2172
+ db,
2173
+ store,
2174
+ bus,
2175
+ gateway,
2176
+ getSettings,
2177
+ manager: void 0,
2178
+ lastUserActivity: { at: Date.now() },
2179
+ shutdown: async () => {
2180
+ }
2181
+ };
2182
+ app.manager = new BotManager({
2183
+ store,
2184
+ bus,
2185
+ gateway,
2186
+ workspace: cfg.paths.workspace,
2187
+ getSettings,
2188
+ skillPluginPath: () => app.skillPluginPath,
2189
+ installSkill: async (source, opts2) => {
2190
+ if (!app.skills?.installFromSource) throw new Error("Skill installation is unavailable.");
2191
+ const installed = await app.skills.installFromSource(source, opts2 ?? {});
2192
+ return installed.map((i2) => ({
2193
+ name: i2.skill.name,
2194
+ executables: i2.executables
2195
+ }));
2196
+ },
2197
+ listSkills: () => store.listSkills().map((sk) => ({ slug: sk.slug, name: sk.name, description: sk.description })),
2198
+ // Routed through SkillStore so the directory and the registration go together — a bot
2199
+ // deleting directories with Bash is what leaves the registry pointing at nothing.
2200
+ removeSkill: async (slug) => {
2201
+ if (!app.skills?.deleteSkill) throw new Error("Skill removal is unavailable.");
2202
+ const skill = store.getSkillBySlug(slug);
2203
+ if (!skill) return { removed: false };
2204
+ app.skills.deleteSkill(skill.id);
2205
+ return { removed: true, name: skill.name };
2206
+ },
2207
+ browserTools: (botId) => {
2208
+ if (!app.browser?.toolServerFor) return void 0;
2209
+ try {
2210
+ return app.browser.toolServerFor(botId);
2211
+ } catch {
2212
+ return void 0;
2213
+ }
2214
+ }
2215
+ });
2216
+ try {
2217
+ app.secrets = new SecretsService(
2218
+ await pickBackend(cfg.paths.secrets),
2219
+ `${cfg.paths.secrets}.index`
2220
+ );
2221
+ log8.info(`secrets backend: ${app.secrets.backendName}`);
2222
+ } catch (err) {
2223
+ log8.warn("secrets backend unavailable", err.message);
2224
+ }
2225
+ await wireSkills(app);
2226
+ await wireBrowser(app);
2227
+ await wireScheduler(app);
2228
+ app.shutdown = async () => {
2229
+ try {
2230
+ app.scheduler?.stop?.();
2231
+ } catch {
2232
+ }
2233
+ try {
2234
+ await app.browser?.shutdown?.();
2235
+ } catch {
2236
+ }
2237
+ try {
2238
+ db.close();
2239
+ } catch {
2240
+ }
2241
+ };
2242
+ return app;
2243
+ }
2244
+ async function wireSkills(app) {
2245
+ try {
2246
+ const mod = await optionalImport("skills", () => import("./skills-66WRX64H.js"));
2247
+ const pluginMod = await optionalImport("skill plugin", () => import("./plugin-POMHVGD4.js"));
2248
+ const Ctor = mod?.SkillStore ?? mod?.default;
2249
+ if (!Ctor) return void log8.warn("skills subsystem unavailable: no SkillStore export");
2250
+ const pluginRoot = app.cfg.paths.skills;
2251
+ if (pluginMod?.ensureSkillPlugin) {
2252
+ pluginMod.ensureSkillPlugin(pluginRoot);
2253
+ const moved = pluginMod.migrateLegacyLayout?.(pluginRoot) ?? [];
2254
+ if (moved.length) log8.info(`migrated ${moved.length} skill(s) into the plugin layout: ${moved.join(", ")}`);
2255
+ app.skillPluginPath = pluginRoot;
2256
+ }
2257
+ const filesDir = pluginMod?.skillFilesDir?.(pluginRoot) ?? pluginRoot;
2258
+ app.skills = new Ctor(app.store, filesDir);
2259
+ const bundledMod = await optionalImport("bundled skills", () => import("./bundled-46DUK5PG.js"));
2260
+ if (bundledMod?.syncBundledSkills) {
2261
+ try {
2262
+ const decisions = bundledMod.syncBundledSkills(filesDir);
2263
+ const took = (action) => decisions.filter((d) => d.action === action).map((d) => d.slug);
2264
+ const installed = took("install");
2265
+ const updated = took("update");
2266
+ const kept = [...took("skip-modified"), ...took("skip-foreign")];
2267
+ if (installed.length) log8.info(`installed ${installed.length} bundled skill(s): ${installed.join(", ")}`);
2268
+ if (updated.length) log8.info(`updated ${updated.length} bundled skill(s): ${updated.join(", ")}`);
2269
+ if (kept.length) log8.info(`left ${kept.length} locally-modified skill(s) alone: ${kept.join(", ")}`);
2270
+ const written = [...installed, ...updated, ...took("adopt")];
2271
+ const renamed = app.skills?.refreshFromDisk?.(written) ?? [];
2272
+ if (renamed.length) log8.info(`refreshed metadata for ${renamed.length} skill(s): ${renamed.join(", ")}`);
2273
+ } catch (e) {
2274
+ log8.warn("bundled skills not synced", e.message);
2275
+ }
2276
+ }
2277
+ app.skills.syncFromDisk?.();
2278
+ const fixed = app.skills.reconcile?.();
2279
+ if (fixed?.repaired.length) log8.info(`repaired ${fixed.repaired.length} skill path(s): ${fixed.repaired.join(", ")}`);
2280
+ if (fixed?.removed.length) log8.info(`dropped ${fixed.removed.length} skill row(s) with no files on disk`);
2281
+ log8.info(`skills ready (${app.store.listSkills().length} registered)`);
2282
+ } catch (err) {
2283
+ log8.warn("skills subsystem unavailable", err.message);
2284
+ }
2285
+ }
2286
+ async function wireBrowser(app) {
2287
+ try {
2288
+ const mod = await optionalImport("browser", () => import("./browser-OHRD7YI3.js"));
2289
+ const Ctor = mod?.BrowserService ?? mod?.default;
2290
+ if (!Ctor) return void log8.warn("browser subsystem unavailable: no BrowserService export");
2291
+ const svc = new Ctor({ profileDir: app.cfg.paths.browserProfile, bus: app.bus, headless: true });
2292
+ let toolsMod = null;
2293
+ toolsMod = await optionalImport("browser tools", () => import("./tools-P5537ASX.js"));
2294
+ const cache = /* @__PURE__ */ new Map();
2295
+ svc.toolServerFor = (botId) => {
2296
+ if (!toolsMod?.createBrowserToolServer) return void 0;
2297
+ let s = cache.get(botId);
2298
+ if (!s) {
2299
+ s = toolsMod.createBrowserToolServer(svc, botId, app.cfg.paths.workspace);
2300
+ cache.set(botId, s);
2301
+ }
2302
+ return s;
2303
+ };
2304
+ app.browser = svc;
2305
+ log8.info("browser computer service ready");
2306
+ } catch (err) {
2307
+ log8.warn("browser subsystem unavailable", err.message);
2308
+ }
2309
+ }
2310
+ async function wireScheduler(app) {
2311
+ try {
2312
+ const mod = await optionalImport("scheduler", () => import("./scheduler-Q7OHNGP6.js"));
2313
+ const Ctor = mod?.Scheduler ?? mod?.default;
2314
+ if (!Ctor) return void log8.warn("scheduler subsystem unavailable: no Scheduler export");
2315
+ app.scheduler = new Ctor({
2316
+ store: app.store,
2317
+ bus: app.bus,
2318
+ manager: app.manager,
2319
+ getSettings: app.getSettings
2320
+ });
2321
+ app.scheduler.start?.();
2322
+ log8.info(`scheduler started (${app.store.listRoutines().filter((r) => r.enabled).length} active routines)`);
2323
+ } catch (err) {
2324
+ log8.warn("scheduler subsystem unavailable", err.message);
2325
+ }
2326
+ }
2327
+ function drainMailbox(app) {
2328
+ let n = 0;
2329
+ for (const bot of app.store.listBots()) {
2330
+ for (const m of app.store.listMail(bot.id)) {
2331
+ const from = app.store.getBot(m.fromBotId);
2332
+ app.manager.enqueue({
2333
+ botId: bot.id,
2334
+ threadId: bot.threadId,
2335
+ origin: "bot",
2336
+ hops: m.hops,
2337
+ prompt: `**Handoff from @${from?.slug ?? "unknown"}:**
2338
+
2339
+ ${m.contentMd}`
2340
+ });
2341
+ app.store.markDelivered(m.id);
2342
+ n++;
2343
+ }
2344
+ }
2345
+ return n;
2346
+ }
2347
+ function workspaceRelative(root, p) {
2348
+ const resolved = path8.resolve(root, p);
2349
+ const rel = path8.relative(root, resolved);
2350
+ if (rel.startsWith("..") || path8.isAbsolute(rel)) return null;
2351
+ return resolved;
2352
+ }
2353
+
2354
+ // packages/server/src/bots/groups.ts
2355
+ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
2356
+ var log9 = logger("groups");
2357
+ async function routeGroupMessage(args) {
2358
+ const { text, members, mentionBotIds, mentionEveryone, settings, cwd } = args;
2359
+ if (mentionEveryone) return members;
2360
+ if (mentionBotIds?.length) {
2361
+ const picked = members.filter((m) => mentionBotIds.includes(m.id));
2362
+ if (picked.length) return picked;
2363
+ }
2364
+ const inline = members.filter((m) => new RegExp(`@${m.slug}\\b`, "i").test(text));
2365
+ if (inline.length) return inline;
2366
+ try {
2367
+ const roster = members.map((m) => `- ${m.slug}: ${m.name}${m.title ? `, ${m.title}` : ""}. ${m.description.slice(0, 200)}`).join("\n");
2368
+ const q = query3({
2369
+ prompt: `Team:
2370
+ ${roster}
2371
+
2372
+ Message:
2373
+ ${text.slice(0, 2e3)}
2374
+
2375
+ Which single teammate should own this? Reply with only the slug.`,
2376
+ options: {
2377
+ model: "haiku",
2378
+ systemPrompt: "You route work to the right teammate. Reply with exactly one slug from the list and nothing else.",
2379
+ cwd,
2380
+ settingSources: [],
2381
+ env: buildEnv(settings),
2382
+ maxTurns: 1,
2383
+ allowedTools: []
2384
+ }
2385
+ });
2386
+ let out = "";
2387
+ for await (const m of q) {
2388
+ const msg = m;
2389
+ if (msg.type === "result" && typeof msg.result === "string") out = msg.result;
2390
+ }
2391
+ const slug = out.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
2392
+ const found = members.find((m) => m.slug === slug);
2393
+ if (found) return [found];
2394
+ } catch (err) {
2395
+ log9.warn("group router failed; defaulting to first member", err);
2396
+ }
2397
+ return members.slice(0, 1);
2398
+ }
2399
+ function parseMentions(text, members) {
2400
+ const everyone = /@everyone\b/i.test(text);
2401
+ const botIds = members.filter((m) => new RegExp(`@${m.slug}\\b`, "i").test(text)).map((m) => m.id);
2402
+ return { botIds, everyone };
2403
+ }
2404
+
2405
+ // packages/server/src/api/routes-core.ts
2406
+ import fs8 from "node:fs";
2407
+ import path9 from "node:path";
2408
+ import { fileURLToPath } from "node:url";
2409
+ var SERVER_VERSION = readPackageVersion(
2410
+ path9.dirname(fileURLToPath(import.meta.url)),
2411
+ (p) => fs8.existsSync(p),
2412
+ (p) => fs8.readFileSync(p, "utf8")
2413
+ );
2414
+ function registerCoreRoutes(f, app) {
2415
+ const { store, bus, manager } = app;
2416
+ f.get("/api/health", async () => ({
2417
+ ok: true,
2418
+ seq: bus.currentSeq,
2419
+ version: SERVER_VERSION,
2420
+ bots: store.listBots().length
2421
+ }));
2422
+ f.get(
2423
+ "/api/bots",
2424
+ async () => store.listBots().map((bot) => ({
2425
+ bot,
2426
+ thread: bot.threadId ? store.getThread(bot.threadId) : null,
2427
+ lastMessageAt: bot.threadId ? store.lastMessageAt(bot.threadId) : 0
2428
+ }))
2429
+ );
2430
+ f.post("/api/bots", async (req, reply) => {
2431
+ const parsed = CreateBotRequest.safeParse(req.body);
2432
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? "Invalid body" });
2433
+ try {
2434
+ return store.createBot(parsed.data);
2435
+ } catch (err) {
2436
+ if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
2437
+ throw err;
2438
+ }
2439
+ });
2440
+ f.get("/api/bots/:id", async (req, reply) => {
2441
+ const bot = store.getBot(req.params.id);
2442
+ return bot ?? reply.code(404).send({ error: "No such bot" });
2443
+ });
2444
+ f.patch("/api/bots/:id", async (req, reply) => {
2445
+ const parsed = UpdateBotRequest.safeParse(req.body);
2446
+ if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
2447
+ const bot = store.updateBot(req.params.id, parsed.data);
2448
+ if (!bot) return reply.code(404).send({ error: "No such bot" });
2449
+ bus.publish({ type: "bot.state", botId: bot.id, threadId: bot.threadId, state: bot.state, attention: bot.attention });
2450
+ return bot;
2451
+ });
2452
+ f.delete("/api/bots/:id", async (req, reply) => {
2453
+ if (!store.getBot(req.params.id)) return reply.code(404).send({ error: "No such bot" });
2454
+ manager.interrupt(req.params.id);
2455
+ store.deleteBot(req.params.id);
2456
+ return { ok: true };
2457
+ });
2458
+ f.post("/api/bots/:id/duplicate", async (req, reply) => {
2459
+ try {
2460
+ const copy = store.duplicateBot(req.params.id);
2461
+ return copy ?? reply.code(404).send({ error: "No such bot" });
2462
+ } catch (err) {
2463
+ if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
2464
+ throw err;
2465
+ }
2466
+ });
2467
+ f.post("/api/bots/:id/stop", async (req) => ({
2468
+ stopped: manager.interrupt(req.params.id)
2469
+ }));
2470
+ f.get("/api/bots/:id/memory", async (req, reply) => {
2471
+ const bot = store.getBot(req.params.id);
2472
+ if (!bot) return reply.code(404).send({ error: "No such bot" });
2473
+ return readMemory(app.cfg.paths.workspace, bot.slug);
2474
+ });
2475
+ f.put("/api/bots/:id/memory", async (req, reply) => {
2476
+ const bot = store.getBot(req.params.id);
2477
+ if (!bot) return reply.code(404).send({ error: "No such bot" });
2478
+ const { name, content } = req.body ?? {};
2479
+ if (!name || typeof content !== "string") return reply.code(400).send({ error: "name and content are required" });
2480
+ writeMemory(app.cfg.paths.workspace, bot.slug, name, content);
2481
+ return { ok: true };
2482
+ });
2483
+ f.delete("/api/bots/:id/memory/:name", async (req, reply) => {
2484
+ const bot = store.getBot(req.params.id);
2485
+ if (!bot) return reply.code(404).send({ error: "No such bot" });
2486
+ deleteMemory(app.cfg.paths.workspace, bot.slug, req.params.name);
2487
+ return { ok: true };
2488
+ });
2489
+ f.get("/api/bots/:id/skills", async (req, reply) => {
2490
+ if (!store.getBot(req.params.id)) return reply.code(404).send({ error: "No such bot" });
2491
+ return store.listBotSkills(req.params.id);
2492
+ });
2493
+ f.put("/api/bots/:id/skills", async (req, reply) => {
2494
+ if (!store.getBot(req.params.id)) return reply.code(404).send({ error: "No such bot" });
2495
+ store.setBotSkills(req.params.id, req.body?.skillIds ?? []);
2496
+ return { ok: true };
2497
+ });
2498
+ f.get("/api/threads", async () => store.listThreads());
2499
+ f.post("/api/threads", async (req, reply) => {
2500
+ const parsed = CreateThreadRequest.safeParse(req.body);
2501
+ if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
2502
+ try {
2503
+ const members = parsed.data.memberBotIds.map((id) => store.getBot(id)).filter(Boolean);
2504
+ if (members.length !== parsed.data.memberBotIds.length)
2505
+ return reply.code(400).send({ error: "One or more bots do not exist" });
2506
+ const title = parsed.data.title || members.map((m) => m.name).join(", ");
2507
+ return store.createThread({ ...parsed.data, title });
2508
+ } catch (err) {
2509
+ if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
2510
+ throw err;
2511
+ }
2512
+ });
2513
+ f.get("/api/threads/:id", async (req, reply) => {
2514
+ const thread = store.getThread(req.params.id);
2515
+ if (!thread) return reply.code(404).send({ error: "No such thread" });
2516
+ const payload = { thread, messages: store.listMessages(thread.id) };
2517
+ return payload;
2518
+ });
2519
+ f.delete("/api/threads/:id", async (req) => {
2520
+ store.deleteThread(req.params.id);
2521
+ return { ok: true };
2522
+ });
2523
+ f.post("/api/threads/:id/read", async (req, reply) => {
2524
+ const thread = store.getThread(req.params.id);
2525
+ if (!thread) return reply.code(404).send({ error: "No such thread" });
2526
+ store.updateThread(thread.id, { lastReadAt: Date.now() });
2527
+ for (const id of thread.memberBotIds) {
2528
+ const bot = store.getBot(id);
2529
+ if (bot && bot.attention !== "needs_attention") {
2530
+ store.updateBot(id, { attention: "none" });
2531
+ bus.publish({ type: "bot.state", botId: id, threadId: thread.id, state: bot.state, attention: "none" });
2532
+ }
2533
+ }
2534
+ return { ok: true };
2535
+ });
2536
+ f.post("/api/threads/:id/messages", async (req, reply) => {
2537
+ const parsed = PostMessageRequest.safeParse(req.body);
2538
+ if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
2539
+ const thread = store.getThread(req.params.id);
2540
+ if (!thread) return reply.code(404).send({ error: "No such thread" });
2541
+ app.lastUserActivity.at = Date.now();
2542
+ const msg = store.createMessage({
2543
+ threadId: thread.id,
2544
+ authorKind: "user",
2545
+ contentMd: parsed.data.contentMd,
2546
+ replyToId: parsed.data.replyToId ?? null
2547
+ });
2548
+ if (parsed.data.attachmentIds?.length) {
2549
+ try {
2550
+ store.attachToMessage(parsed.data.attachmentIds, msg.id);
2551
+ } catch (err) {
2552
+ if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
2553
+ throw err;
2554
+ }
2555
+ }
2556
+ bus.publish({ type: "message.created", threadId: thread.id, botId: null, message: store.getMessage(msg.id) });
2557
+ const attachments = store.listAttachmentsForMessage(msg.id);
2558
+ const attachNote = attachments.length ? `
2559
+
2560
+ Attached files (read them from disk):
2561
+ ${attachments.map((a) => `- ${a.name} \u2192 ${a.path}`).join("\n")}` : "";
2562
+ const prompt = parsed.data.contentMd + attachNote;
2563
+ const members = thread.memberBotIds.map((id) => store.getBot(id)).filter(Boolean);
2564
+ if (!members.length) return msg;
2565
+ if (thread.kind === "dm") {
2566
+ manager.enqueue({ botId: members[0].id, threadId: thread.id, prompt, origin: "user", hops: 0 });
2567
+ } else {
2568
+ const inline = parseMentions(parsed.data.contentMd, members);
2569
+ const targets = await routeGroupMessage({
2570
+ text: parsed.data.contentMd,
2571
+ members,
2572
+ mentionBotIds: parsed.data.mentionBotIds ?? inline.botIds,
2573
+ mentionEveryone: parsed.data.mentionEveryone ?? inline.everyone,
2574
+ settings: app.getSettings(),
2575
+ cwd: app.cfg.paths.workspace
2576
+ });
2577
+ for (const t of targets)
2578
+ manager.enqueue({ botId: t.id, threadId: thread.id, prompt, origin: "user", hops: 0 });
2579
+ }
2580
+ return msg;
2581
+ });
2582
+ }
2583
+
2584
+ // packages/server/src/api/routes-ops.ts
2585
+ import fs9 from "node:fs";
2586
+ import path10 from "node:path";
2587
+ function registerOpsRoutes(f, app) {
2588
+ const { store, gateway, bus } = app;
2589
+ f.get("/api/approvals", async () => store.listPendingApprovals());
2590
+ f.post("/api/approvals/:id", async (req, reply) => {
2591
+ const parsed = ApprovalDecisionRequest.safeParse(req.body);
2592
+ if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
2593
+ const updated = gateway.decide(req.params.id, parsed.data.decision, parsed.data.alwaysRule);
2594
+ return updated ?? reply.code(404).send({ error: "No such approval" });
2595
+ });
2596
+ f.get("/api/rules", async () => store.listRules());
2597
+ f.post("/api/rules", async (req, reply) => {
2598
+ const parsed = CreateRuleRequest.safeParse(req.body);
2599
+ if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
2600
+ try {
2601
+ new RegExp(parsed.data.inputPattern || "");
2602
+ } catch {
2603
+ return reply.code(400).send({ error: "inputPattern is not a valid regular expression" });
2604
+ }
2605
+ return store.createRule(parsed.data);
2606
+ });
2607
+ f.patch("/api/rules/:id", async (req, reply) => {
2608
+ const rule = store.getRule(req.params.id);
2609
+ if (!rule) return reply.code(404).send({ error: "No such rule" });
2610
+ store.setRuleEnabled(rule.id, Boolean(req.body?.enabled));
2611
+ return store.getRule(rule.id);
2612
+ });
2613
+ f.delete("/api/rules/:id", async (req, reply) => {
2614
+ const rule = store.getRule(req.params.id);
2615
+ if (!rule) return reply.code(404).send({ error: "No such rule" });
2616
+ if (rule.builtin) return reply.code(400).send({ error: "Built-in rules cannot be deleted; disable it instead." });
2617
+ store.deleteRule(rule.id);
2618
+ return { ok: true };
2619
+ });
2620
+ f.get("/api/skills", async () => store.listSkills());
2621
+ f.post("/api/skills", async (req, reply) => {
2622
+ const parsed = CreateSkillRequest.safeParse(req.body);
2623
+ if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
2624
+ if (!app.skills?.writeSkill) return reply.code(503).send({ error: "Skills subsystem unavailable" });
2625
+ return app.skills.writeSkill(parsed.data);
2626
+ });
2627
+ f.post("/api/skills/install", async (req, reply) => {
2628
+ const source = (req.body?.source ?? "").trim();
2629
+ if (!source) return reply.code(400).send({ error: 'A "source" is required' });
2630
+ if (!app.skills?.installFromSource) return reply.code(503).send({ error: "Skills subsystem unavailable" });
2631
+ try {
2632
+ const installed = await app.skills.installFromSource(source, { allowMultiple: true });
2633
+ return {
2634
+ installed: installed.map((i2) => ({
2635
+ skill: i2.skill,
2636
+ executables: i2.executables,
2637
+ manifest: i2.manifestText,
2638
+ replaced: i2.replaced
2639
+ }))
2640
+ };
2641
+ } catch (err) {
2642
+ return reply.code(400).send({ error: err.message });
2643
+ }
2644
+ });
2645
+ f.get("/api/skills/:id", async (req, reply) => {
2646
+ const skill = store.getSkill(req.params.id);
2647
+ if (!skill) return reply.code(404).send({ error: "No such skill" });
2648
+ if (app.skills?.readSkill) return app.skills.readSkill(skill.id);
2649
+ return { ...skill, bodyMd: "" };
2650
+ });
2651
+ f.delete("/api/skills/:id", async (req, reply) => {
2652
+ const skill = store.getSkill(req.params.id);
2653
+ if (!skill) return reply.code(404).send({ error: "No such skill" });
2654
+ if (app.skills?.deleteSkill) app.skills.deleteSkill(skill.id);
2655
+ else store.deleteSkill(skill.id);
2656
+ return { ok: true };
2657
+ });
2658
+ f.get("/api/secrets", async () => ({
2659
+ backend: app.secrets?.backendName ?? "unavailable",
2660
+ names: app.secrets?.list() ?? []
2661
+ }));
2662
+ f.post("/api/secrets", async (req, reply) => {
2663
+ if (!app.secrets) return reply.code(503).send({ error: "Secrets backend unavailable" });
2664
+ const name = (req.body?.name ?? "").trim();
2665
+ const value = req.body?.value ?? "";
2666
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
2667
+ return reply.code(400).send({ error: "Name must be a valid environment-variable identifier" });
2668
+ if (!value) return reply.code(400).send({ error: "A value is required" });
2669
+ await app.secrets.set(name, value);
2670
+ return { ok: true, names: app.secrets.list() };
2671
+ });
2672
+ f.delete("/api/secrets/:name", async (req, reply) => {
2673
+ if (!app.secrets) return reply.code(503).send({ error: "Secrets backend unavailable" });
2674
+ await app.secrets.remove(req.params.name);
2675
+ return { ok: true, names: app.secrets.list() };
2676
+ });
2677
+ f.get("/api/routines", async (req) => store.listRoutines(req.query.botId));
2678
+ f.post("/api/routines", async (req, reply) => {
2679
+ const parsed = CreateRoutineRequest.safeParse(req.body);
2680
+ if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
2681
+ if (!store.getBot(parsed.data.botId)) return reply.code(400).send({ error: "No such bot" });
2682
+ try {
2683
+ const routine = store.createRoutine({ ...parsed.data, timezone: parsed.data.timezone ?? app.getSettings().timezone });
2684
+ app.scheduler?.reload?.(routine.id);
2685
+ return routine;
2686
+ } catch (err) {
2687
+ if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
2688
+ throw err;
2689
+ }
2690
+ });
2691
+ f.patch("/api/routines/:id", async (req, reply) => {
2692
+ const routine = store.updateRoutine(req.params.id, req.body ?? {});
2693
+ if (!routine) return reply.code(404).send({ error: "No such routine" });
2694
+ app.scheduler?.reload?.(routine.id);
2695
+ return routine;
2696
+ });
2697
+ f.delete("/api/routines/:id", async (req, reply) => {
2698
+ if (!store.getRoutine(req.params.id)) return reply.code(404).send({ error: "No such routine" });
2699
+ store.deleteRoutine(req.params.id);
2700
+ app.scheduler?.reload?.(req.params.id);
2701
+ return { ok: true };
2702
+ });
2703
+ f.get("/api/routines/:id/runs", async (req) => store.listRuns(req.params.id));
2704
+ f.post("/api/routines/:id/test-run", async (req, reply) => {
2705
+ const routine = store.getRoutine(req.params.id);
2706
+ if (!routine) return reply.code(404).send({ error: "No such routine" });
2707
+ if (!app.scheduler?.testRun) return reply.code(503).send({ error: "Scheduler unavailable" });
2708
+ const runId = await app.scheduler.testRun(routine.id);
2709
+ return { runId };
2710
+ });
2711
+ f.post("/api/attachments", async (req, reply) => {
2712
+ const anyReq = req;
2713
+ if (typeof anyReq.file !== "function") return reply.code(400).send({ error: "Expected a multipart upload" });
2714
+ const part = await anyReq.file();
2715
+ if (!part) return reply.code(400).send({ error: "No file in request" });
2716
+ const buf = await part.toBuffer();
2717
+ const safe = String(part.filename ?? "file").replace(/[^a-zA-Z0-9._-]/g, "_");
2718
+ const dest = path10.join(app.cfg.paths.attachments, `${Date.now()}-${safe}`);
2719
+ fs9.mkdirSync(path10.dirname(dest), { recursive: true });
2720
+ fs9.writeFileSync(dest, buf);
2721
+ try {
2722
+ return store.createAttachment({
2723
+ messageId: null,
2724
+ path: dest,
2725
+ name: safe,
2726
+ mime: part.mimetype ?? "application/octet-stream",
2727
+ bytes: buf.byteLength
2728
+ });
2729
+ } catch (err) {
2730
+ fs9.unlinkSync(dest);
2731
+ if (err instanceof LimitError) return reply.code(413).send({ error: err.message, code: err.code });
2732
+ throw err;
2733
+ }
2734
+ });
2735
+ f.get("/api/attachments/:id", async (req, reply) => {
2736
+ const a = store.getAttachment(req.params.id);
2737
+ if (!a || !fs9.existsSync(a.path)) return reply.code(404).send({ error: "No such attachment" });
2738
+ return reply.type(a.mime).send(fs9.createReadStream(a.path));
2739
+ });
2740
+ f.get("/api/usage", async () => {
2741
+ const rows = store.listUsage(0);
2742
+ const totals = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
2743
+ const byBot = /* @__PURE__ */ new Map();
2744
+ const byDay = /* @__PURE__ */ new Map();
2745
+ const byModel = /* @__PURE__ */ new Map();
2746
+ for (const r of rows) {
2747
+ totals.inputTokens += r.inputTokens;
2748
+ totals.outputTokens += r.outputTokens;
2749
+ totals.cacheReadTokens += r.cacheReadTokens;
2750
+ const day = new Date(r.createdAt).toISOString().slice(0, 10);
2751
+ for (const [map, key] of [[byBot, r.botId], [byDay, day], [byModel, r.model]]) {
2752
+ const cur = map.get(key) ?? { inputTokens: 0, outputTokens: 0 };
2753
+ cur.inputTokens += r.inputTokens;
2754
+ cur.outputTokens += r.outputTokens;
2755
+ map.set(key, cur);
2756
+ }
2757
+ }
2758
+ return {
2759
+ totals,
2760
+ byBot: [...byBot].map(([botId, v]) => ({ botId, botName: store.getBot(botId)?.name ?? "deleted", ...v })),
2761
+ byDay: [...byDay].map(([day, v]) => ({ day, ...v })).sort((a, b2) => a.day.localeCompare(b2.day)),
2762
+ byModel: [...byModel].map(([model, v]) => ({ model, ...v }))
2763
+ };
2764
+ });
2765
+ f.get("/api/search", async (req) => {
2766
+ const q = (req.query.q ?? "").trim();
2767
+ if (!q) return [];
2768
+ const out = [];
2769
+ for (const b2 of store.listBots()) {
2770
+ if (b2.name.toLowerCase().includes(q.toLowerCase()) || b2.title.toLowerCase().includes(q.toLowerCase()))
2771
+ out.push({ kind: "bot", id: b2.id, threadId: b2.threadId, botId: b2.id, title: b2.name, snippet: b2.title || b2.description.slice(0, 120), createdAt: b2.createdAt });
2772
+ }
2773
+ for (const m of store.searchMessages(q)) {
2774
+ const idx = m.contentMd.toLowerCase().indexOf(q.toLowerCase());
2775
+ const start = Math.max(0, idx - 60);
2776
+ out.push({
2777
+ kind: "message",
2778
+ id: m.id,
2779
+ threadId: m.threadId,
2780
+ botId: m.authorBotId,
2781
+ title: m.authorKind === "user" ? "You" : store.getBot(m.authorBotId ?? "")?.name ?? "Bot",
2782
+ snippet: `${start > 0 ? "\u2026" : ""}${m.contentMd.slice(start, start + 180)}`,
2783
+ createdAt: m.createdAt
2784
+ });
2785
+ }
2786
+ for (const r of store.listRoutines()) {
2787
+ if (r.name.toLowerCase().includes(q.toLowerCase()))
2788
+ out.push({ kind: "routine", id: r.id, threadId: null, botId: r.botId, title: r.name, snippet: r.cronExpr, createdAt: r.createdAt });
2789
+ }
2790
+ return out.slice(0, 50);
2791
+ });
2792
+ f.get("/api/settings", async () => store.getSettings());
2793
+ f.patch("/api/settings", async (req, reply) => {
2794
+ const parsed = SettingsPatchSchema.safeParse(req.body);
2795
+ if (!parsed.success) return reply.code(400).send({ error: "Invalid settings" });
2796
+ const next = store.patchSettings(parsed.data);
2797
+ app.scheduler?.syncAll?.();
2798
+ bus.publish({ type: "notify", botId: null, threadId: null, title: "Settings updated", body: "", level: "info" });
2799
+ return next;
2800
+ });
2801
+ f.get("/api/workspace/tree", async (req, reply) => {
2802
+ const root = app.cfg.paths.workspace;
2803
+ const target = workspaceRelative(root, req.query.path ?? ".");
2804
+ if (!target) return reply.code(400).send({ error: "Path is outside the workspace" });
2805
+ if (!fs9.existsSync(target)) return [];
2806
+ return fs9.readdirSync(target, { withFileTypes: true }).map((d) => {
2807
+ const full = path10.join(target, d.name);
2808
+ let bytes = 0;
2809
+ try {
2810
+ bytes = d.isFile() ? fs9.statSync(full).size : 0;
2811
+ } catch {
2812
+ }
2813
+ return { name: d.name, path: path10.relative(root, full), dir: d.isDirectory(), bytes };
2814
+ }).sort((a, b2) => a.dir === b2.dir ? a.name.localeCompare(b2.name) : a.dir ? -1 : 1);
2815
+ });
2816
+ f.get("/api/workspace/file", async (req, reply) => {
2817
+ const root = app.cfg.paths.workspace;
2818
+ const target = workspaceRelative(root, req.query.path ?? "");
2819
+ if (!target || !fs9.existsSync(target) || !fs9.statSync(target).isFile())
2820
+ return reply.code(404).send({ error: "No such file" });
2821
+ const ext = path10.extname(target).toLowerCase();
2822
+ const mime = {
2823
+ ".md": "text/markdown",
2824
+ ".txt": "text/plain",
2825
+ ".json": "application/json",
2826
+ ".csv": "text/csv",
2827
+ ".png": "image/png",
2828
+ ".jpg": "image/jpeg",
2829
+ ".jpeg": "image/jpeg",
2830
+ ".gif": "image/gif",
2831
+ ".svg": "image/svg+xml",
2832
+ ".pdf": "application/pdf",
2833
+ ".html": "text/html"
2834
+ };
2835
+ return reply.type(mime[ext] ?? "application/octet-stream").send(fs9.createReadStream(target));
2836
+ });
2837
+ f.get("/api/computer/status", async () => {
2838
+ if (!app.browser?.status) return { available: false, reason: "Browser service not built", mode: "host", headless: true, pages: [] };
2839
+ try {
2840
+ return await app.browser.status();
2841
+ } catch (err) {
2842
+ return { available: false, reason: err.message, mode: "host", headless: true, pages: [] };
2843
+ }
2844
+ });
2845
+ f.post("/api/computer/takeover", async (req, reply) => {
2846
+ if (!app.browser?.takeOver) return reply.code(503).send({ error: "Browser service unavailable" });
2847
+ return app.browser.takeOver(req.body?.botId ?? "shared");
2848
+ });
2849
+ f.delete("/api/computer/takeover", async (req, reply) => {
2850
+ if (!app.browser?.returnControl) return reply.code(503).send({ error: "Browser service unavailable" });
2851
+ await app.browser.returnControl(req.body?.botId ?? "shared");
2852
+ return { ok: true };
2853
+ });
2854
+ }
2855
+
2856
+ // packages/server/src/api/server.ts
2857
+ var log10 = logger("server");
2858
+ var require_ = createRequire(import.meta.url);
2859
+ function resolveWebDist() {
2860
+ return findWebDist(
2861
+ nodeLocateDeps(path11.dirname(fileURLToPath2(import.meta.url)), (spec) => {
2862
+ try {
2863
+ return require_.resolve(spec);
2864
+ } catch {
2865
+ return null;
2866
+ }
2867
+ })
2868
+ );
2869
+ }
2870
+ async function startServer(opts = {}) {
2871
+ const app = await createApp({ root: opts.root, withAgent: opts.withAgent });
2872
+ const fastify = Fastify({ logger: false, bodyLimit: LIMITS.MAX_VIDEO_ATTACHMENT_BYTES });
2873
+ fastify.addHook("onRequest", (req, _reply, done) => {
2874
+ const len = req.headers["content-length"];
2875
+ const ct = req.headers["content-type"];
2876
+ if ((len === "0" || len === void 0) && ct?.includes("application/json")) {
2877
+ delete req.headers["content-type"];
2878
+ }
2879
+ done();
2880
+ });
2881
+ await fastify.register(cors, { origin: true });
2882
+ await fastify.register(multipart, {
2883
+ limits: { fileSize: LIMITS.MAX_VIDEO_ATTACHMENT_BYTES, files: LIMITS.MAX_ATTACHMENTS_PER_MESSAGE }
2884
+ });
2885
+ await fastify.register(websocket);
2886
+ registerCoreRoutes(fastify, app);
2887
+ registerOpsRoutes(fastify, app);
2888
+ fastify.register(async (scope) => {
2889
+ scope.get("/api/events", { websocket: true }, (socket) => {
2890
+ const send = (payload) => {
2891
+ try {
2892
+ if (socket.readyState === 1) socket.send(JSON.stringify(payload));
2893
+ } catch {
2894
+ }
2895
+ };
2896
+ const unsubscribe = app.bus.subscribe(send);
2897
+ send({ type: "hello", seq: app.bus.currentSeq, threadId: null, botId: null });
2898
+ socket.on("message", (raw) => {
2899
+ try {
2900
+ const msg = JSON.parse(raw.toString());
2901
+ if (msg.type === "resume" && typeof msg.seq === "number")
2902
+ for (const e of app.bus.since(msg.seq)) send(e);
2903
+ } catch {
2904
+ }
2905
+ });
2906
+ socket.on("close", unsubscribe);
2907
+ socket.on("error", unsubscribe);
2908
+ });
2909
+ scope.get("/api/computer/screencast/:botId", { websocket: true }, async (socket, req) => {
2910
+ const botId = req.params.botId;
2911
+ if (!app.browser?.startScreencast) {
2912
+ try {
2913
+ socket.send(JSON.stringify({ type: "error", message: "Browser service unavailable" }));
2914
+ } catch {
2915
+ }
2916
+ socket.close();
2917
+ return;
2918
+ }
2919
+ let stop;
2920
+ try {
2921
+ stop = await app.browser.startScreencast(botId, (data, w, h) => {
2922
+ try {
2923
+ if (socket.readyState === 1) socket.send(JSON.stringify({ type: "frame", data, w, h }));
2924
+ } catch {
2925
+ }
2926
+ });
2927
+ } catch (err) {
2928
+ try {
2929
+ socket.send(JSON.stringify({ type: "error", message: err.message }));
2930
+ } catch {
2931
+ }
2932
+ socket.close();
2933
+ return;
2934
+ }
2935
+ socket.on("close", () => stop?.());
2936
+ socket.on("error", () => stop?.());
2937
+ });
2938
+ });
2939
+ if (opts.serveStatic !== false) {
2940
+ const dist = resolveWebDist();
2941
+ if (dist) {
2942
+ await fastify.register(fastifyStatic, { root: dist, prefix: "/" });
2943
+ fastify.setNotFoundHandler((req, reply) => {
2944
+ if (req.url.startsWith("/api")) return reply.code(404).send({ error: "Not found" });
2945
+ return reply.sendFile("index.html");
2946
+ });
2947
+ log10.info(`serving UI from ${dist}`);
2948
+ } else {
2949
+ log10.warn("web UI not built \u2014 run `pnpm --filter @antbot/web build`");
2950
+ fastify.setNotFoundHandler((req, reply) => {
2951
+ if (req.url.startsWith("/api")) return reply.code(404).send({ error: "Not found" });
2952
+ return reply.type("text/html").send(
2953
+ `<!doctype html><meta charset=utf-8><title>ant-bot</title>
2954
+ <body style="font-family:system-ui;background:#0b0d10;color:#e6e8eb;padding:3rem">
2955
+ <h1>ant-bot daemon is running</h1>
2956
+ <p>The web UI has not been built yet. Run:</p>
2957
+ <pre style="background:#151922;padding:1rem;border-radius:8px">pnpm --filter @antbot/web build</pre>
2958
+ <p>The API is live at <code>/api/health</code>.</p>`
2959
+ );
2960
+ });
2961
+ }
2962
+ }
2963
+ fastify.setErrorHandler((err, _req, reply) => {
2964
+ log10.error("request failed", err);
2965
+ const e = err;
2966
+ const code = e.statusCode && e.statusCode >= 400 ? e.statusCode : 500;
2967
+ reply.code(code).send({ error: e.message ?? "Internal error" });
2968
+ });
2969
+ const port = opts.port ?? app.cfg.port;
2970
+ const host = opts.host ?? app.cfg.host;
2971
+ await fastify.listen({ port, host });
2972
+ const url = `http://${host}:${port}`;
2973
+ const delivered = drainMailbox(app);
2974
+ if (delivered) log10.info(`redelivered ${delivered} queued handoff message(s)`);
2975
+ const stale = app.store.listBots().filter((b2) => b2.state === "running" || b2.state === "queued");
2976
+ for (const b2 of stale) app.store.updateBot(b2.id, { state: "idle" });
2977
+ app.db.prepare(`UPDATE messages SET streaming=0 WHERE streaming=1`).run();
2978
+ app.db.prepare(`UPDATE approvals SET status='expired', reason='Daemon restarted' WHERE status='pending'`).run();
2979
+ app.db.prepare(`UPDATE routine_runs SET status='interrupted', finished_at=? WHERE status='running'`).run(Date.now());
2980
+ log10.info(`ant-bot listening on ${url}`);
2981
+ return {
2982
+ fastify,
2983
+ app,
2984
+ url,
2985
+ close: async () => {
2986
+ await fastify.close();
2987
+ await app.shutdown();
2988
+ }
2989
+ };
2990
+ }
2991
+ export {
2992
+ BUILTIN_RULES,
2993
+ BotManager,
2994
+ DEFAULT_HOST,
2995
+ DEFAULT_PORT,
2996
+ EventBus,
2997
+ PermissionGateway,
2998
+ Store,
2999
+ buildEnv,
3000
+ buildSystemPrompt,
3001
+ createApp,
3002
+ ensureDirs,
3003
+ evaluateRules,
3004
+ globToRegExp,
3005
+ loadConfig,
3006
+ openDb,
3007
+ parseVerdict,
3008
+ resolveModel,
3009
+ resolvePaths,
3010
+ ruleMatches,
3011
+ runTurn,
3012
+ serializeInput,
3013
+ startServer,
3014
+ summarize
3015
+ };
3016
+ //# sourceMappingURL=server.js.map