@owloops/browserbird 1.5.9 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { $ as touchSession, A as SYSTEM_CRON_PREFIX, B as listCronJobs, C as failStaleJobs, D as listJobs, E as hasPendingCronJob, F as deleteOldCronRuns, G as createSession, H as setCronJobEnabled, I as ensureSystemCronJob, J as getSession, K as deleteOldSessions, L as getCronJob, M as createCronJob, N as createCronRun, O as retryAllFailedJobs, P as deleteCronJob, Q as listSessions, R as getEnabledCronJobs, S as failJob, T as getJobStatus, U as updateCronJob, V as listFlights, W as updateCronJobStatus, X as getSessionMessages, Y as getSessionCount, Z as getSessionTokenStats, _ as clearJobs, a as getSetting, at as optimizeDatabase, b as deleteJob, c as getUserCount, d as getRecentLogs, et as updateSessionProviderId, f as insertLog, g as claimNextJob, h as logMessage, i as createUser, it as openDatabase, j as completeCronRun, k as retryJob, l as setSetting, m as getMessageStats, n as resolveDbPath, nt as closeDatabase, o as getUserByEmail, ot as resolveByUid, p as deleteOldMessages, q as findSession, r as resolveDbPathFromArgv, rt as getDb, s as getUserById, st as logger, tt as shortUid, u as deleteOldLogs, v as completeJob, w as getJobStats, x as deleteOldJobs, y as createJob, z as getFlightStats } from "./db-BNF1vZIm.mjs";
2
1
  import { createRequire } from "node:module";
3
2
  import { parseArgs, styleText } from "node:util";
4
- import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
4
  import { dirname, extname, join, resolve } from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
6
  import { createHmac, randomBytes, scrypt, timingSafeEqual } from "node:crypto";
7
7
  import { connect } from "node:net";
8
8
  import { execFileSync, spawn } from "node:child_process";
@@ -21,6 +21,76 @@ const COMMANDS = {
21
21
  DOCTOR: "doctor"
22
22
  };
23
23
 
24
+ //#endregion
25
+ //#region src/core/logger.ts
26
+ /**
27
+ * @fileoverview Structured logger. Respects NO_COLOR.
28
+ *
29
+ * Stream routing by mode:
30
+ * - CLI mode (default): all logs to stderr, keeping stdout clean for data output.
31
+ * - Daemon mode: errors/warnings to stderr, everything else to stdout.
32
+ * Cloud platforms (Railway, Fly, etc.) treat stderr lines as errors, so this
33
+ * prevents info lines from showing up red.
34
+ *
35
+ * Call `logger.setMode('daemon')` once at startup to switch.
36
+ */
37
+ function shouldUseColor$1() {
38
+ if (process.env["NO_COLOR"] !== void 0) return false;
39
+ if (process.env["TERM"] === "dumb") return false;
40
+ if (process.argv.includes("--no-color")) return false;
41
+ return process.stdout.isTTY === true || process.stderr.isTTY === true;
42
+ }
43
+ const useColor$1 = shouldUseColor$1();
44
+ function style(format, text) {
45
+ if (!useColor$1) return text;
46
+ return styleText(format, text);
47
+ }
48
+ const LOG_LEVELS = {
49
+ ERROR: 0,
50
+ WARN: 1,
51
+ INFO: 2,
52
+ DEBUG: 3
53
+ };
54
+ let currentLevel = LOG_LEVELS.INFO;
55
+ let daemonMode = false;
56
+ function timestamp() {
57
+ return (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
58
+ }
59
+ function out(prefix, message) {
60
+ (daemonMode ? process.stdout : process.stderr).write(`${style("dim", timestamp())} ${prefix} ${message}\n`);
61
+ }
62
+ function err(prefix, message) {
63
+ process.stderr.write(`${style("dim", timestamp())} ${prefix} ${message}\n`);
64
+ }
65
+ const logger = {
66
+ error(message) {
67
+ if (currentLevel >= LOG_LEVELS.ERROR) err(style("red", "[error]"), message);
68
+ },
69
+ warn(message) {
70
+ if (currentLevel >= LOG_LEVELS.WARN) err(style("yellow", "[warn]"), message);
71
+ },
72
+ info(message) {
73
+ if (currentLevel >= LOG_LEVELS.INFO) out(style("blue", "[info]"), message);
74
+ },
75
+ debug(message) {
76
+ if (currentLevel >= LOG_LEVELS.DEBUG) out(style("dim", "[debug]"), message);
77
+ },
78
+ success(message) {
79
+ if (currentLevel >= LOG_LEVELS.INFO) out(style("green", "[ok]"), message);
80
+ },
81
+ setLevel(level) {
82
+ currentLevel = {
83
+ error: LOG_LEVELS.ERROR,
84
+ warn: LOG_LEVELS.WARN,
85
+ info: LOG_LEVELS.INFO,
86
+ debug: LOG_LEVELS.DEBUG
87
+ }[level] ?? LOG_LEVELS.INFO;
88
+ },
89
+ setMode(mode) {
90
+ daemonMode = mode === "daemon";
91
+ }
92
+ };
93
+
24
94
  //#endregion
25
95
  //#region src/core/utils.ts
26
96
  /** @fileoverview Shared utilities: formatting, time ranges, and CLI table output. */
@@ -122,8 +192,8 @@ function unknownSubcommand(subcommand, command, validCommands) {
122
192
  /** @fileoverview ASCII banner displayed on daemon startup and in help text. */
123
193
  const pkg = createRequire(import.meta.url)("../package.json");
124
194
  const buildInfo = [];
125
- buildInfo.push(`commit: ${"079abaeb36f4cadf0afba256b58885e1e8280174".substring(0, 7)}`);
126
- buildInfo.push(`built: 2026-03-18T11:14:06+04:00`);
195
+ buildInfo.push(`commit: ${"8468e2519ab927c9f810bc3af7d127829a93c83e".substring(0, 7)}`);
196
+ buildInfo.push(`built: 2026-03-19T13:57:16+04:00`);
127
197
  const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
128
198
  const VERSION = `browserbird ${pkg.version}${buildString}`;
129
199
  const BIRD = [
@@ -346,6 +416,329 @@ function loadDotEnv(envPath) {
346
416
  }
347
417
  }
348
418
 
419
+ //#endregion
420
+ //#region src/db/core.ts
421
+ /** @fileoverview SQLite database lifecycle, migrations, and query utilities. */
422
+ const DEFAULT_PER_PAGE = 15;
423
+ const MAX_PER_PAGE = 100;
424
+ /**
425
+ * Parses a sort string into an SQL ORDER BY clause.
426
+ * Each token is a column name optionally prefixed with `-` for DESC.
427
+ * Only columns present in `allowedColumns` are included.
428
+ */
429
+ function parseSort(raw, allowedColumns, fallback) {
430
+ if (!raw) return fallback;
431
+ const parts = [];
432
+ for (const token of raw.split(",")) {
433
+ const trimmed = token.trim();
434
+ if (!trimmed) continue;
435
+ const desc = trimmed.startsWith("-");
436
+ const col = desc ? trimmed.slice(1) : trimmed;
437
+ if (allowedColumns.has(col)) parts.push(`${col} ${desc ? "DESC" : "ASC"}`);
438
+ }
439
+ return parts.length > 0 ? parts.join(", ") : fallback;
440
+ }
441
+ /**
442
+ * Builds a parenthesized OR clause for LIKE-based search across columns.
443
+ * Returns empty sql/params when the search term is empty.
444
+ */
445
+ function buildSearchClause(term, columns) {
446
+ if (!term || columns.length === 0) return {
447
+ sql: "",
448
+ params: []
449
+ };
450
+ const like = `%${term}%`;
451
+ return {
452
+ sql: `(${columns.map((c) => `${c} LIKE ?`).join(" OR ")})`,
453
+ params: columns.map(() => like)
454
+ };
455
+ }
456
+ function paginate(table, page, perPage, whereOrOptions, params, orderBy) {
457
+ let where;
458
+ let allParams;
459
+ let resolvedOrderBy;
460
+ if (typeof whereOrOptions === "object" && whereOrOptions !== null) {
461
+ const opts = whereOrOptions;
462
+ const conditions = [];
463
+ allParams = [...opts.params ?? []];
464
+ if (opts.where) conditions.push(opts.where);
465
+ if (opts.search && opts.searchColumns && opts.searchColumns.length > 0) {
466
+ const sc = buildSearchClause(opts.search, opts.searchColumns);
467
+ if (sc.sql) {
468
+ conditions.push(sc.sql);
469
+ allParams.push(...sc.params);
470
+ }
471
+ }
472
+ where = conditions.join(" AND ");
473
+ resolvedOrderBy = parseSort(opts.sort, opts.allowedSortColumns ?? /* @__PURE__ */ new Set(), opts.defaultSort ?? "created_at DESC");
474
+ } else {
475
+ where = whereOrOptions ?? "";
476
+ allParams = params ?? [];
477
+ resolvedOrderBy = orderBy ?? "created_at DESC";
478
+ }
479
+ const pp = Math.min(Math.max(perPage, 1), MAX_PER_PAGE);
480
+ const p = Math.max(page, 1);
481
+ const offset = (p - 1) * pp;
482
+ const countSql = `SELECT COUNT(*) as count FROM ${table}${where ? ` WHERE ${where}` : ""}`;
483
+ const totalItems = getDb().prepare(countSql).get(...allParams).count;
484
+ const totalPages = Math.max(Math.ceil(totalItems / pp), 1);
485
+ const dataSql = `SELECT * FROM ${table}${where ? ` WHERE ${where}` : ""} ORDER BY ${resolvedOrderBy} LIMIT ? OFFSET ?`;
486
+ return {
487
+ items: getDb().prepare(dataSql).all(...allParams, pp, offset),
488
+ page: p,
489
+ perPage: pp,
490
+ totalItems,
491
+ totalPages
492
+ };
493
+ }
494
+ /**
495
+ * Versioned migration registry. Each entry runs once, in order.
496
+ * PRAGMA user_version tracks which migrations have been applied.
497
+ * All DDL uses IF NOT EXISTS for idempotency.
498
+ */
499
+ const MIGRATIONS = [
500
+ {
501
+ name: "initial schema",
502
+ up(d) {
503
+ d.exec(`
504
+ CREATE TABLE IF NOT EXISTS users (
505
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
506
+ email TEXT UNIQUE NOT NULL COLLATE NOCASE,
507
+ password_hash TEXT NOT NULL,
508
+ token_key TEXT NOT NULL,
509
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
510
+ );
511
+
512
+ CREATE TABLE IF NOT EXISTS settings (
513
+ key TEXT PRIMARY KEY,
514
+ value TEXT NOT NULL
515
+ );
516
+
517
+ CREATE TABLE IF NOT EXISTS sessions (
518
+ uid TEXT PRIMARY KEY,
519
+ channel_id TEXT NOT NULL,
520
+ thread_id TEXT,
521
+ agent_id TEXT NOT NULL DEFAULT 'default',
522
+ provider_session_id TEXT NOT NULL,
523
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
524
+ last_active TEXT NOT NULL DEFAULT (datetime('now')),
525
+ message_count INTEGER NOT NULL DEFAULT 0,
526
+ UNIQUE(channel_id, thread_id)
527
+ );
528
+
529
+ CREATE TABLE IF NOT EXISTS cron_jobs (
530
+ uid TEXT PRIMARY KEY,
531
+ name TEXT NOT NULL,
532
+ agent_id TEXT NOT NULL DEFAULT 'default',
533
+ schedule TEXT NOT NULL,
534
+ prompt TEXT NOT NULL,
535
+ target_channel_id TEXT,
536
+ active_hours_start TEXT,
537
+ active_hours_end TEXT,
538
+ enabled INTEGER NOT NULL DEFAULT 1,
539
+ failure_count INTEGER NOT NULL DEFAULT 0,
540
+ last_run TEXT,
541
+ last_status TEXT,
542
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
543
+ );
544
+
545
+ CREATE TABLE IF NOT EXISTS cron_runs (
546
+ uid TEXT PRIMARY KEY,
547
+ job_uid TEXT NOT NULL REFERENCES cron_jobs(uid),
548
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
549
+ finished_at TEXT,
550
+ status TEXT NOT NULL DEFAULT 'running',
551
+ result TEXT,
552
+ error TEXT
553
+ );
554
+
555
+ CREATE TABLE IF NOT EXISTS messages (
556
+ id INTEGER PRIMARY KEY,
557
+ channel_id TEXT NOT NULL,
558
+ thread_id TEXT,
559
+ user_id TEXT NOT NULL,
560
+ direction TEXT NOT NULL CHECK(direction IN ('in', 'out')),
561
+ content TEXT,
562
+ tokens_in INTEGER,
563
+ tokens_out INTEGER,
564
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
565
+ );
566
+
567
+ CREATE TABLE IF NOT EXISTS jobs (
568
+ id INTEGER PRIMARY KEY,
569
+ name TEXT NOT NULL,
570
+ payload TEXT,
571
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed')),
572
+ priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('high', 'normal', 'low')),
573
+ attempts INTEGER NOT NULL DEFAULT 0,
574
+ max_attempts INTEGER NOT NULL DEFAULT 1,
575
+ timeout INTEGER NOT NULL DEFAULT 1800,
576
+ cron_job_uid TEXT REFERENCES cron_jobs(uid),
577
+ run_at TEXT,
578
+ started_at TEXT,
579
+ completed_at TEXT,
580
+ result TEXT,
581
+ error TEXT,
582
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
583
+ );
584
+
585
+ CREATE TABLE IF NOT EXISTS logs (
586
+ id INTEGER PRIMARY KEY,
587
+ level TEXT NOT NULL DEFAULT 'info' CHECK(level IN ('debug', 'info', 'warn', 'error')),
588
+ source TEXT NOT NULL,
589
+ message TEXT NOT NULL,
590
+ channel_id TEXT,
591
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
592
+ );
593
+
594
+ CREATE INDEX IF NOT EXISTS idx_sessions_channel_thread
595
+ ON sessions(channel_id, thread_id);
596
+ CREATE INDEX IF NOT EXISTS idx_sessions_last_active
597
+ ON sessions(last_active);
598
+ CREATE INDEX IF NOT EXISTS idx_messages_channel_thread
599
+ ON messages(channel_id, thread_id);
600
+ CREATE INDEX IF NOT EXISTS idx_messages_created_at
601
+ ON messages(created_at DESC);
602
+ CREATE INDEX IF NOT EXISTS idx_cron_runs_job_uid
603
+ ON cron_runs(job_uid, started_at DESC);
604
+ CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled
605
+ ON cron_jobs(enabled);
606
+ CREATE INDEX IF NOT EXISTS idx_jobs_poll
607
+ ON jobs(status, priority, run_at, created_at);
608
+ CREATE INDEX IF NOT EXISTS idx_jobs_cron_job_uid
609
+ ON jobs(cron_job_uid);
610
+ CREATE INDEX IF NOT EXISTS idx_jobs_stale
611
+ ON jobs(status, started_at);
612
+ CREATE INDEX IF NOT EXISTS idx_logs_created_at
613
+ ON logs(created_at DESC);
614
+ CREATE INDEX IF NOT EXISTS idx_logs_level_source
615
+ ON logs(level, source, created_at DESC);
616
+ `);
617
+ }
618
+ },
619
+ {
620
+ name: "browser lock",
621
+ up(d) {
622
+ d.exec(`
623
+ CREATE TABLE IF NOT EXISTS browser_lock (
624
+ id INTEGER PRIMARY KEY CHECK(id = 1),
625
+ holder TEXT NOT NULL,
626
+ acquired_at TEXT NOT NULL DEFAULT (datetime('now'))
627
+ );
628
+ `);
629
+ }
630
+ },
631
+ {
632
+ name: "feedback",
633
+ up(d) {
634
+ d.exec(`
635
+ CREATE TABLE IF NOT EXISTS feedback (
636
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
637
+ channel_id TEXT NOT NULL,
638
+ thread_id TEXT,
639
+ message_ts TEXT,
640
+ user_id TEXT NOT NULL,
641
+ value TEXT NOT NULL CHECK(value IN ('good', 'bad')),
642
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
643
+ UNIQUE(channel_id, message_ts, user_id)
644
+ );
645
+ CREATE INDEX IF NOT EXISTS idx_feedback_channel_thread
646
+ ON feedback(channel_id, thread_id);
647
+ `);
648
+ }
649
+ }
650
+ ];
651
+ let db = null;
652
+ function getSchemaVersion(d) {
653
+ return d.prepare("PRAGMA user_version").get().user_version;
654
+ }
655
+ function setSchemaVersion(d, version) {
656
+ d.exec(`PRAGMA user_version = ${version}`);
657
+ }
658
+ /**
659
+ * Runs pending migrations inside a transaction.
660
+ * Safe to call on every startup; already-applied migrations are skipped.
661
+ */
662
+ function migrate(d) {
663
+ const current = getSchemaVersion(d);
664
+ const target = MIGRATIONS.length;
665
+ if (current >= target) return;
666
+ for (let i = current; i < target; i++) {
667
+ const migration = MIGRATIONS[i];
668
+ logger.info(`migration ${i + 1}/${target}: ${migration.name}`);
669
+ d.exec("BEGIN");
670
+ try {
671
+ migration.up(d);
672
+ setSchemaVersion(d, i + 1);
673
+ d.exec("COMMIT");
674
+ } catch (err) {
675
+ d.exec("ROLLBACK");
676
+ throw new Error(`migration "${migration.name}" failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
677
+ }
678
+ }
679
+ }
680
+ /**
681
+ * Opens (or creates) the SQLite database at the given path.
682
+ * Configures WAL mode, runs pending migrations.
683
+ */
684
+ function openDatabase(dbPath) {
685
+ mkdirSync(dirname(dbPath), { recursive: true });
686
+ db = new DatabaseSync(dbPath);
687
+ db.exec("PRAGMA journal_mode = WAL");
688
+ db.exec("PRAGMA synchronous = NORMAL");
689
+ db.exec("PRAGMA foreign_keys = ON");
690
+ db.exec("PRAGMA busy_timeout = 5000");
691
+ migrate(db);
692
+ logger.debug(`database opened at ${dbPath}`);
693
+ }
694
+ function closeDatabase() {
695
+ if (db) {
696
+ db.close();
697
+ db = null;
698
+ logger.debug("database closed");
699
+ }
700
+ }
701
+ function getDb() {
702
+ if (!db) throw new Error("Database not initialized. Call openDatabase() first.");
703
+ return db;
704
+ }
705
+ /** Runs WAL checkpoint and query planner optimization. Safe to call periodically. */
706
+ function optimizeDatabase() {
707
+ const d = getDb();
708
+ d.exec("PRAGMA wal_checkpoint(TRUNCATE)");
709
+ d.exec("PRAGMA optimize");
710
+ }
711
+ /**
712
+ * Resolves a row by UID or UID prefix, like git's short SHA resolution.
713
+ * Exact match first (fast path), then prefix scan via LIKE.
714
+ */
715
+ function resolveByUid(table, uidPrefix) {
716
+ const input = uidPrefix.toLowerCase();
717
+ const d = getDb();
718
+ const exact = d.prepare(`SELECT * FROM ${table} WHERE uid = ?`).get(input);
719
+ if (exact) return { row: exact };
720
+ const rows = d.prepare(`SELECT * FROM ${table} WHERE uid LIKE ? LIMIT 2`).all(`${input}%`);
721
+ if (rows.length === 0) return void 0;
722
+ if (rows.length > 1) return {
723
+ ambiguous: true,
724
+ count: d.prepare(`SELECT COUNT(*) as count FROM ${table} WHERE uid LIKE ?`).get(`${input}%`).count
725
+ };
726
+ return { row: rows[0] };
727
+ }
728
+ /** Wraps a function in BEGIN/COMMIT with automatic ROLLBACK on error. */
729
+ function transaction(fn) {
730
+ const d = getDb();
731
+ d.exec("BEGIN IMMEDIATE");
732
+ try {
733
+ const result = fn();
734
+ d.exec("COMMIT");
735
+ return result;
736
+ } catch (err) {
737
+ d.exec("ROLLBACK");
738
+ throw err;
739
+ }
740
+ }
741
+
349
742
  //#endregion
350
743
  //#region src/browser/lock.ts
351
744
  /** @fileoverview SQLite-based browser lock for persistent browser mode. */
@@ -390,6 +783,586 @@ function clearBrowserLock() {
390
783
  getDb().prepare("DELETE FROM browser_lock WHERE id = 1").run();
391
784
  }
392
785
 
786
+ //#endregion
787
+ //#region src/core/uid.ts
788
+ /** @fileoverview Prefixed short ID generation (PocketBase/Motebase pattern). */
789
+ const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
790
+ const UID_PREFIX = {
791
+ bird: "br_",
792
+ flight: "fl_",
793
+ session: "ss_"
794
+ };
795
+ function generateUid(prefix) {
796
+ const bytes = randomBytes(15);
797
+ let id = prefix;
798
+ for (let i = 0; i < 15; i++) id += ALPHABET[bytes[i] % 36];
799
+ return id;
800
+ }
801
+ function shortUid(uid) {
802
+ const i = uid.indexOf("_");
803
+ if (i === -1) return uid.slice(0, 10);
804
+ return uid.slice(0, i + 1 + 7);
805
+ }
806
+
807
+ //#endregion
808
+ //#region src/db/sessions.ts
809
+ function findSession(channelId, threadId) {
810
+ return getDb().prepare("SELECT * FROM sessions WHERE channel_id = ? AND thread_id IS ?").get(channelId, threadId);
811
+ }
812
+ function createSession(channelId, threadId, agentId, providerSessionId) {
813
+ const uid = generateUid(UID_PREFIX.session);
814
+ return getDb().prepare(`INSERT INTO sessions (uid, channel_id, thread_id, agent_id, provider_session_id)
815
+ VALUES (?, ?, ?, ?, ?)
816
+ RETURNING *`).get(uid, channelId, threadId, agentId, providerSessionId);
817
+ }
818
+ function touchSession(uid, messageCountDelta = 1) {
819
+ getDb().prepare(`UPDATE sessions SET last_active = datetime('now'), message_count = message_count + ? WHERE uid = ?`).run(messageCountDelta, uid);
820
+ }
821
+ const SESSION_SORT_COLUMNS = new Set([
822
+ "uid",
823
+ "channel_id",
824
+ "agent_id",
825
+ "message_count",
826
+ "last_active",
827
+ "created_at"
828
+ ]);
829
+ const SESSION_SEARCH_COLUMNS = [
830
+ "channel_id",
831
+ "thread_id",
832
+ "agent_id"
833
+ ];
834
+ function listSessions(page = 1, perPage = DEFAULT_PER_PAGE, sort, search) {
835
+ return paginate("sessions", page, perPage, {
836
+ defaultSort: "last_active DESC",
837
+ sort,
838
+ search,
839
+ allowedSortColumns: SESSION_SORT_COLUMNS,
840
+ searchColumns: SESSION_SEARCH_COLUMNS
841
+ });
842
+ }
843
+ function getSession(uid) {
844
+ return getDb().prepare("SELECT * FROM sessions WHERE uid = ?").get(uid);
845
+ }
846
+ const MESSAGE_SORT_COLUMNS = new Set([
847
+ "id",
848
+ "created_at",
849
+ "direction",
850
+ "user_id"
851
+ ]);
852
+ const MESSAGE_SEARCH_COLUMNS = ["content", "user_id"];
853
+ function getSessionMessages(channelId, threadId, page = 1, perPage = DEFAULT_PER_PAGE, sort, search) {
854
+ const pp = Math.min(Math.max(perPage, 1), MAX_PER_PAGE);
855
+ const p = Math.max(page, 1);
856
+ const offset = (p - 1) * pp;
857
+ const conditions = ["channel_id = ? AND thread_id IS ?"];
858
+ const allParams = [channelId, threadId];
859
+ if (search) {
860
+ const sc = buildSearchClause(search, MESSAGE_SEARCH_COLUMNS);
861
+ if (sc.sql) {
862
+ conditions.push(sc.sql);
863
+ allParams.push(...sc.params);
864
+ }
865
+ }
866
+ const where = conditions.join(" AND ");
867
+ const orderBy = parseSort(sort, MESSAGE_SORT_COLUMNS, "created_at ASC, id ASC");
868
+ const totalItems = getDb().prepare(`SELECT COUNT(*) as count FROM messages WHERE ${where}`).get(...allParams).count;
869
+ const totalPages = Math.max(Math.ceil(totalItems / pp), 1);
870
+ return {
871
+ items: getDb().prepare(`SELECT * FROM messages
872
+ WHERE ${where}
873
+ ORDER BY ${orderBy}
874
+ LIMIT ? OFFSET ?`).all(...allParams, pp, offset),
875
+ page: p,
876
+ perPage: pp,
877
+ totalItems,
878
+ totalPages
879
+ };
880
+ }
881
+ function getSessionTokenStats(channelId, threadId) {
882
+ return getDb().prepare(`SELECT COALESCE(SUM(tokens_in), 0) as totalTokensIn,
883
+ COALESCE(SUM(tokens_out), 0) as totalTokensOut
884
+ FROM messages
885
+ WHERE channel_id = ? AND thread_id IS ?`).get(channelId, threadId);
886
+ }
887
+ function getSessionCount() {
888
+ return getDb().prepare("SELECT COUNT(*) as count FROM sessions").get().count;
889
+ }
890
+ function deleteOldSessions(retentionDays) {
891
+ const result = getDb().prepare(`DELETE FROM sessions WHERE last_active < datetime('now', ? || ' days')`).run(`-${retentionDays}`);
892
+ return Number(result.changes);
893
+ }
894
+ function updateSessionProviderId(uid, providerSessionId) {
895
+ getDb().prepare("UPDATE sessions SET provider_session_id = ? WHERE uid = ?").run(providerSessionId, uid);
896
+ }
897
+
898
+ //#endregion
899
+ //#region src/db/birds.ts
900
+ const SYSTEM_CRON_PREFIX = "__bb_";
901
+ const CRON_SORT_COLUMNS = new Set([
902
+ "uid",
903
+ "name",
904
+ "schedule",
905
+ "agent_id",
906
+ "enabled",
907
+ "last_run",
908
+ "created_at"
909
+ ]);
910
+ const CRON_SEARCH_COLUMNS = [
911
+ "uid",
912
+ "name",
913
+ "prompt",
914
+ "schedule"
915
+ ];
916
+ function listCronJobs(page = 1, perPage = DEFAULT_PER_PAGE, includeSystem = false, sort, search) {
917
+ return paginate("cron_jobs", page, perPage, {
918
+ where: includeSystem ? "" : `name NOT LIKE '${SYSTEM_CRON_PREFIX}%'`,
919
+ defaultSort: "created_at ASC",
920
+ sort,
921
+ search,
922
+ allowedSortColumns: CRON_SORT_COLUMNS,
923
+ searchColumns: CRON_SEARCH_COLUMNS
924
+ });
925
+ }
926
+ function getEnabledCronJobs() {
927
+ return getDb().prepare("SELECT * FROM cron_jobs WHERE enabled = 1 ORDER BY created_at").all();
928
+ }
929
+ function createCronJob(name, schedule, prompt, targetChannelId, agentId, activeHoursStart, activeHoursEnd) {
930
+ const uid = generateUid(UID_PREFIX.bird);
931
+ return getDb().prepare(`INSERT INTO cron_jobs (uid, name, schedule, prompt, target_channel_id, agent_id, active_hours_start, active_hours_end)
932
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
933
+ RETURNING *`).get(uid, name, schedule, prompt, targetChannelId ?? null, agentId ?? "default", activeHoursStart ?? null, activeHoursEnd ?? null);
934
+ }
935
+ function updateCronJobStatus(jobUid, status, failureCount) {
936
+ getDb().prepare(`UPDATE cron_jobs SET last_run = datetime('now'), last_status = ?, failure_count = ? WHERE uid = ?`).run(status, failureCount, jobUid);
937
+ }
938
+ function getCronJob(jobUid) {
939
+ return getDb().prepare("SELECT * FROM cron_jobs WHERE uid = ?").get(jobUid);
940
+ }
941
+ function setCronJobEnabled(jobUid, enabled) {
942
+ const result = getDb().prepare("UPDATE cron_jobs SET enabled = ? WHERE uid = ?").run(enabled ? 1 : 0, jobUid);
943
+ return Number(result.changes) > 0;
944
+ }
945
+ function updateCronJob(jobUid, fields) {
946
+ const sets = [];
947
+ const params = [];
948
+ if (fields.name !== void 0) {
949
+ sets.push("name = ?");
950
+ params.push(fields.name);
951
+ }
952
+ if (fields.schedule !== void 0) {
953
+ sets.push("schedule = ?");
954
+ params.push(fields.schedule);
955
+ }
956
+ if (fields.prompt !== void 0) {
957
+ sets.push("prompt = ?");
958
+ params.push(fields.prompt);
959
+ }
960
+ if (fields.targetChannelId !== void 0) {
961
+ sets.push("target_channel_id = ?");
962
+ params.push(fields.targetChannelId);
963
+ }
964
+ if (fields.agentId !== void 0) {
965
+ sets.push("agent_id = ?");
966
+ params.push(fields.agentId);
967
+ }
968
+ if (fields.activeHoursStart !== void 0) {
969
+ sets.push("active_hours_start = ?");
970
+ params.push(fields.activeHoursStart);
971
+ }
972
+ if (fields.activeHoursEnd !== void 0) {
973
+ sets.push("active_hours_end = ?");
974
+ params.push(fields.activeHoursEnd);
975
+ }
976
+ if (sets.length === 0) return getCronJob(jobUid);
977
+ params.push(jobUid);
978
+ return getDb().prepare(`UPDATE cron_jobs SET ${sets.join(", ")} WHERE uid = ? RETURNING *`).get(...params);
979
+ }
980
+ function deleteCronJob(jobUid) {
981
+ const d = getDb();
982
+ d.exec("BEGIN");
983
+ try {
984
+ d.prepare("DELETE FROM cron_runs WHERE job_uid = ?").run(jobUid);
985
+ d.prepare("UPDATE jobs SET cron_job_uid = NULL WHERE cron_job_uid = ?").run(jobUid);
986
+ const result = d.prepare("DELETE FROM cron_jobs WHERE uid = ?").run(jobUid);
987
+ d.exec("COMMIT");
988
+ return Number(result.changes) > 0;
989
+ } catch (err) {
990
+ d.exec("ROLLBACK");
991
+ throw err;
992
+ }
993
+ }
994
+ const FLIGHT_SORT_COLUMNS = new Set([
995
+ "uid",
996
+ "started_at",
997
+ "finished_at",
998
+ "status",
999
+ "bird_name"
1000
+ ]);
1001
+ const FLIGHT_SORT_MAP = { bird_name: "j.name" };
1002
+ const FLIGHT_SEARCH_COLUMNS = [
1003
+ "r.uid",
1004
+ "r.job_uid",
1005
+ "j.uid",
1006
+ "j.name",
1007
+ "r.error",
1008
+ "r.result"
1009
+ ];
1010
+ function listFlights(page = 1, perPage = DEFAULT_PER_PAGE, filters = {}, sort, search) {
1011
+ const pp = Math.min(Math.max(perPage, 1), MAX_PER_PAGE);
1012
+ const p = Math.max(page, 1);
1013
+ const offset = (p - 1) * pp;
1014
+ const conditions = [];
1015
+ const params = [];
1016
+ if (!filters.system) conditions.push(`j.name NOT LIKE '${SYSTEM_CRON_PREFIX}%'`);
1017
+ if (filters.birdUid != null) {
1018
+ conditions.push("r.job_uid = ?");
1019
+ params.push(filters.birdUid);
1020
+ }
1021
+ if (filters.status) {
1022
+ conditions.push("r.status = ?");
1023
+ params.push(filters.status);
1024
+ }
1025
+ if (search) {
1026
+ const sc = buildSearchClause(search, FLIGHT_SEARCH_COLUMNS);
1027
+ if (sc.sql) {
1028
+ conditions.push(sc.sql);
1029
+ params.push(...sc.params);
1030
+ }
1031
+ }
1032
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1033
+ let orderBy = parseSort(sort, FLIGHT_SORT_COLUMNS, "r.started_at DESC");
1034
+ for (const [key, qualified] of Object.entries(FLIGHT_SORT_MAP)) orderBy = orderBy.replaceAll(key, qualified);
1035
+ orderBy = orderBy.replace(/(?<![a-z.])\b(uid|started_at|finished_at|status)\b/g, "r.$1");
1036
+ const totalItems = getDb().prepare(`SELECT COUNT(*) as count FROM cron_runs r JOIN cron_jobs j ON j.uid = r.job_uid ${where}`).get(...params).count;
1037
+ const totalPages = Math.max(Math.ceil(totalItems / pp), 1);
1038
+ return {
1039
+ items: getDb().prepare(`SELECT r.uid, r.job_uid, j.uid as bird_uid, j.name as bird_name,
1040
+ r.started_at, r.finished_at, r.status, r.result, r.error
1041
+ FROM cron_runs r
1042
+ JOIN cron_jobs j ON j.uid = r.job_uid
1043
+ ${where}
1044
+ ORDER BY ${orderBy}
1045
+ LIMIT ? OFFSET ?`).all(...params, pp, offset),
1046
+ page: p,
1047
+ perPage: pp,
1048
+ totalItems,
1049
+ totalPages
1050
+ };
1051
+ }
1052
+ function createCronRun(jobUid) {
1053
+ const uid = generateUid(UID_PREFIX.flight);
1054
+ return getDb().prepare("INSERT INTO cron_runs (uid, job_uid) VALUES (?, ?) RETURNING *").get(uid, jobUid);
1055
+ }
1056
+ function completeCronRun(runUid, status, result, error) {
1057
+ getDb().prepare(`UPDATE cron_runs SET finished_at = datetime('now'), status = ?, result = ?, error = ? WHERE uid = ?`).run(status, result ?? null, error ?? null, runUid);
1058
+ }
1059
+ function getFlightStats() {
1060
+ const rows = getDb().prepare(`SELECT r.status, COUNT(*) as count
1061
+ FROM cron_runs r
1062
+ JOIN cron_jobs j ON j.uid = r.job_uid
1063
+ WHERE j.name NOT LIKE '${SYSTEM_CRON_PREFIX}%'
1064
+ GROUP BY r.status`).all();
1065
+ const stats = {
1066
+ running: 0,
1067
+ completed: 0,
1068
+ failed: 0,
1069
+ total: 0
1070
+ };
1071
+ for (const row of rows) {
1072
+ if (row.status === "running") stats.running = row.count;
1073
+ else if (row.status === "success") stats.completed = row.count;
1074
+ else if (row.status === "error") stats.failed = row.count;
1075
+ stats.total += row.count;
1076
+ }
1077
+ return stats;
1078
+ }
1079
+ function deleteOldCronRuns(retentionDays) {
1080
+ const result = getDb().prepare(`DELETE FROM cron_runs WHERE started_at < datetime('now', ? || ' days')`).run(`-${retentionDays}`);
1081
+ return Number(result.changes);
1082
+ }
1083
+ function ensureSystemCronJob(name, schedule, prompt) {
1084
+ if (getDb().prepare("SELECT uid FROM cron_jobs WHERE name = ?").get(name)) return;
1085
+ const uid = generateUid(UID_PREFIX.bird);
1086
+ getDb().prepare(`INSERT INTO cron_jobs (uid, name, schedule, prompt, agent_id) VALUES (?, ?, ?, ?, 'system')`).run(uid, name, schedule, prompt);
1087
+ }
1088
+
1089
+ //#endregion
1090
+ //#region src/db/jobs.ts
1091
+ function createJob(options) {
1092
+ const payload = options.payload != null ? JSON.stringify(options.payload) : null;
1093
+ const priority = options.priority ?? "normal";
1094
+ const maxAttempts = options.maxAttempts ?? 1;
1095
+ const timeout = options.timeout ?? 1800;
1096
+ const cronJobUid = options.cronJobUid ?? null;
1097
+ if (options.delaySeconds) return getDb().prepare(`INSERT INTO jobs (name, payload, priority, max_attempts, timeout, cron_job_uid, run_at)
1098
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now', '+' || ? || ' seconds'))
1099
+ RETURNING *`).get(options.name, payload, priority, maxAttempts, timeout, cronJobUid, options.delaySeconds);
1100
+ return getDb().prepare(`INSERT INTO jobs (name, payload, priority, max_attempts, timeout, cron_job_uid)
1101
+ VALUES (?, ?, ?, ?, ?, ?)
1102
+ RETURNING *`).get(options.name, payload, priority, maxAttempts, timeout, cronJobUid);
1103
+ }
1104
+ /**
1105
+ * Atomically claims the next pending job for processing.
1106
+ * Uses IMMEDIATE transaction to prevent race conditions.
1107
+ * Priority order: high > normal > low, then by creation time.
1108
+ */
1109
+ function claimNextJob() {
1110
+ return transaction(() => {
1111
+ const row = getDb().prepare(`SELECT * FROM jobs
1112
+ WHERE status = 'pending' AND (run_at IS NULL OR run_at <= datetime('now'))
1113
+ ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 WHEN 'low' THEN 2 END, created_at ASC
1114
+ LIMIT 1`).get();
1115
+ if (!row) return void 0;
1116
+ getDb().prepare(`UPDATE jobs SET status = 'running', started_at = datetime('now'), attempts = attempts + 1
1117
+ WHERE id = ?`).run(row.id);
1118
+ return {
1119
+ ...row,
1120
+ status: "running",
1121
+ attempts: row.attempts + 1
1122
+ };
1123
+ });
1124
+ }
1125
+ function getJobStatus(jobId) {
1126
+ return getDb().prepare("SELECT status FROM jobs WHERE id = ?").get(jobId)?.status;
1127
+ }
1128
+ function completeJob(jobId, result) {
1129
+ getDb().prepare(`UPDATE jobs SET status = 'completed', completed_at = datetime('now'), result = ?
1130
+ WHERE id = ?`).run(result ?? null, jobId);
1131
+ }
1132
+ function failJob(jobId, error) {
1133
+ const job = getDb().prepare("SELECT attempts, max_attempts FROM jobs WHERE id = ?").get(jobId);
1134
+ if (!job) return;
1135
+ if (job.attempts < job.max_attempts) {
1136
+ const delaySeconds = job.attempts * job.attempts;
1137
+ getDb().prepare(`UPDATE jobs SET status = 'pending', error = ?,
1138
+ run_at = datetime('now', '+' || ? || ' seconds')
1139
+ WHERE id = ?`).run(error, delaySeconds, jobId);
1140
+ } else getDb().prepare(`UPDATE jobs SET status = 'failed', completed_at = datetime('now'), error = ?
1141
+ WHERE id = ?`).run(error, jobId);
1142
+ }
1143
+ /** Marks running jobs past their timeout as failed and cascades to linked cron_runs/cron_jobs. */
1144
+ function failStaleJobs() {
1145
+ const d = getDb();
1146
+ const staleRows = d.prepare(`SELECT id, cron_job_uid FROM jobs
1147
+ WHERE status = 'running'
1148
+ AND started_at < datetime('now', '-' || timeout || ' seconds')`).all();
1149
+ if (staleRows.length === 0) return 0;
1150
+ const updateJob = d.prepare(`UPDATE jobs SET status = 'failed', error = 'timeout', completed_at = datetime('now')
1151
+ WHERE id = ?`);
1152
+ const updateRun = d.prepare(`UPDATE cron_runs SET status = 'error', error = 'timeout', finished_at = datetime('now')
1153
+ WHERE job_uid = ? AND status = 'running'`);
1154
+ const updateBird = d.prepare(`UPDATE cron_jobs SET last_status = 'failed', failure_count = failure_count + 1
1155
+ WHERE uid = ?`);
1156
+ for (const row of staleRows) {
1157
+ updateJob.run(row.id);
1158
+ if (row.cron_job_uid != null) {
1159
+ updateRun.run(row.cron_job_uid);
1160
+ updateBird.run(row.cron_job_uid);
1161
+ }
1162
+ }
1163
+ return staleRows.length;
1164
+ }
1165
+ function deleteOldJobs(retentionDays) {
1166
+ const stmt = getDb().prepare(`DELETE FROM jobs WHERE status IN ('completed', 'failed')
1167
+ AND completed_at < datetime('now', ? || ' days')`);
1168
+ return Number(stmt.run(`-${retentionDays}`).changes);
1169
+ }
1170
+ const JOB_SORT_COLUMNS = new Set([
1171
+ "id",
1172
+ "name",
1173
+ "status",
1174
+ "priority",
1175
+ "created_at",
1176
+ "started_at"
1177
+ ]);
1178
+ const JOB_SEARCH_COLUMNS = ["name", "error"];
1179
+ function listJobs(page = 1, perPage = DEFAULT_PER_PAGE, filters = {}, sort, search) {
1180
+ const conditions = [];
1181
+ const params = [];
1182
+ if (filters.status) {
1183
+ conditions.push("status = ?");
1184
+ params.push(filters.status);
1185
+ }
1186
+ if (filters.cronJobUid != null) {
1187
+ conditions.push("cron_job_uid = ?");
1188
+ params.push(filters.cronJobUid);
1189
+ }
1190
+ if (filters.name) {
1191
+ conditions.push("name LIKE ?");
1192
+ params.push(`%${filters.name}%`);
1193
+ }
1194
+ return paginate("jobs", page, perPage, {
1195
+ where: conditions.join(" AND "),
1196
+ params,
1197
+ defaultSort: "created_at DESC",
1198
+ sort,
1199
+ search,
1200
+ allowedSortColumns: JOB_SORT_COLUMNS,
1201
+ searchColumns: JOB_SEARCH_COLUMNS
1202
+ });
1203
+ }
1204
+ function getJobStats() {
1205
+ const rows = getDb().prepare("SELECT status, COUNT(*) as count FROM jobs GROUP BY status").all();
1206
+ const stats = {
1207
+ pending: 0,
1208
+ running: 0,
1209
+ completed: 0,
1210
+ failed: 0,
1211
+ total: 0
1212
+ };
1213
+ for (const row of rows) {
1214
+ if (row.status in stats) stats[row.status] = row.count;
1215
+ stats.total += row.count;
1216
+ }
1217
+ return stats;
1218
+ }
1219
+ function retryJob(jobId) {
1220
+ const result = getDb().prepare(`UPDATE jobs SET status = 'pending', attempts = 0, error = NULL, result = NULL,
1221
+ run_at = NULL, started_at = NULL, completed_at = NULL
1222
+ WHERE id = ? AND status = 'failed'`).run(jobId);
1223
+ return Number(result.changes) > 0;
1224
+ }
1225
+ function retryAllFailedJobs() {
1226
+ const result = getDb().prepare(`UPDATE jobs SET status = 'pending', attempts = 0, error = NULL, result = NULL,
1227
+ run_at = NULL, started_at = NULL, completed_at = NULL
1228
+ WHERE status = 'failed'`).run();
1229
+ return Number(result.changes);
1230
+ }
1231
+ function deleteJob(jobId) {
1232
+ const result = getDb().prepare("DELETE FROM jobs WHERE id = ?").run(jobId);
1233
+ return Number(result.changes) > 0;
1234
+ }
1235
+ function clearJobs(status) {
1236
+ const result = getDb().prepare("DELETE FROM jobs WHERE status = ?").run(status);
1237
+ return Number(result.changes);
1238
+ }
1239
+ /** Returns true if the given cron job has a pending or running job in the queue. */
1240
+ function hasPendingCronJob(cronJobUid) {
1241
+ return getDb().prepare(`SELECT 1 FROM jobs WHERE cron_job_uid = ? AND status IN ('pending', 'running') LIMIT 1`).get(cronJobUid) != null;
1242
+ }
1243
+
1244
+ //#endregion
1245
+ //#region src/db/messages.ts
1246
+ /** @fileoverview Message logging: message audit trail and token tracking. */
1247
+ function logMessage(channelId, threadId, userId, direction, content, tokensIn, tokensOut) {
1248
+ getDb().prepare(`INSERT INTO messages (channel_id, thread_id, user_id, direction, content, tokens_in, tokens_out)
1249
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(channelId, threadId ?? null, userId, direction, content ?? null, tokensIn ?? null, tokensOut ?? null);
1250
+ }
1251
+ function getLastInboundMessage(channelId, threadId) {
1252
+ const row = getDb().prepare(`SELECT content, created_at FROM messages
1253
+ WHERE channel_id = ? AND thread_id IS ? AND direction = 'in' AND content IS NOT NULL
1254
+ ORDER BY created_at DESC LIMIT 1`).get(channelId, threadId);
1255
+ if (!row) return void 0;
1256
+ return {
1257
+ content: row.content,
1258
+ timestamp: row.created_at
1259
+ };
1260
+ }
1261
+ function deleteOldMessages(retentionDays) {
1262
+ const result = getDb().prepare(`DELETE FROM messages WHERE created_at < datetime('now', ? || ' days')`).run(`-${retentionDays}`);
1263
+ return Number(result.changes);
1264
+ }
1265
+ function getMessageStats() {
1266
+ return getDb().prepare(`SELECT
1267
+ COUNT(*) as totalMessages,
1268
+ COALESCE(SUM(tokens_in), 0) as totalTokensIn,
1269
+ COALESCE(SUM(tokens_out), 0) as totalTokensOut
1270
+ FROM messages`).get();
1271
+ }
1272
+
1273
+ //#endregion
1274
+ //#region src/db/logs.ts
1275
+ function insertLog(level, source, message, channelId) {
1276
+ getDb().prepare(`INSERT INTO logs (level, source, message, channel_id) VALUES (?, ?, ?, ?)`).run(level, source, message, channelId ?? null);
1277
+ }
1278
+ const LOG_SORT_COLUMNS = new Set([
1279
+ "id",
1280
+ "level",
1281
+ "source",
1282
+ "created_at"
1283
+ ]);
1284
+ const LOG_SEARCH_COLUMNS = ["message", "source"];
1285
+ function getRecentLogs(page = 1, perPage = DEFAULT_PER_PAGE, level, source, sort, search) {
1286
+ const conditions = [];
1287
+ const params = [];
1288
+ if (level) {
1289
+ conditions.push("level = ?");
1290
+ params.push(level);
1291
+ }
1292
+ if (source) {
1293
+ conditions.push("source = ?");
1294
+ params.push(source);
1295
+ }
1296
+ return paginate("logs", page, perPage, {
1297
+ where: conditions.join(" AND "),
1298
+ params,
1299
+ defaultSort: "created_at DESC",
1300
+ sort,
1301
+ search,
1302
+ allowedSortColumns: LOG_SORT_COLUMNS,
1303
+ searchColumns: LOG_SEARCH_COLUMNS
1304
+ });
1305
+ }
1306
+ function deleteOldLogs(retentionDays) {
1307
+ const stmt = getDb().prepare(`DELETE FROM logs WHERE created_at < datetime('now', ? || ' days')`);
1308
+ return Number(stmt.run(`-${retentionDays}`).changes);
1309
+ }
1310
+
1311
+ //#endregion
1312
+ //#region src/db/auth.ts
1313
+ /** @fileoverview User and settings persistence for the auth system. */
1314
+ function getUserCount() {
1315
+ return getDb().prepare("SELECT COUNT(*) as count FROM users").get().count;
1316
+ }
1317
+ function getUserByEmail(email) {
1318
+ return getDb().prepare("SELECT * FROM users WHERE email = ?").get(email);
1319
+ }
1320
+ function getUserById(id) {
1321
+ return getDb().prepare("SELECT * FROM users WHERE id = ?").get(id);
1322
+ }
1323
+ function createUser(email, passwordHash, tokenKey) {
1324
+ getDb().prepare("INSERT INTO users (email, password_hash, token_key) VALUES (?, ?, ?)").run(email, passwordHash, tokenKey);
1325
+ return getUserByEmail(email);
1326
+ }
1327
+ function getSetting(key) {
1328
+ return getDb().prepare("SELECT value FROM settings WHERE key = ?").get(key)?.value;
1329
+ }
1330
+ function setSetting(key, value) {
1331
+ getDb().prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run(key, value);
1332
+ }
1333
+
1334
+ //#endregion
1335
+ //#region src/db/feedback.ts
1336
+ /** @fileoverview Response feedback persistence (thumbs up/down). */
1337
+ function insertFeedback(channelId, threadId, messageTs, userId, value) {
1338
+ getDb().prepare(`INSERT OR REPLACE INTO feedback (channel_id, thread_id, message_ts, user_id, value)
1339
+ VALUES (?, ?, ?, ?, ?)`).run(channelId, threadId ?? null, messageTs ?? null, userId, value);
1340
+ }
1341
+
1342
+ //#endregion
1343
+ //#region src/db/path.ts
1344
+ /** @fileoverview Database path resolution: CLI flag, env var, or default. */
1345
+ const DEFAULT_DB_PATH = resolve(".browserbird", "browserbird.db");
1346
+ /**
1347
+ * Resolves the database file path.
1348
+ * Priority: explicit value > BROWSERBIRD_DB env var > default.
1349
+ */
1350
+ function resolveDbPath(explicit) {
1351
+ if (explicit) return resolve(explicit);
1352
+ const envValue = process.env["BROWSERBIRD_DB"];
1353
+ if (envValue) return resolve(envValue);
1354
+ return DEFAULT_DB_PATH;
1355
+ }
1356
+ /**
1357
+ * Extracts --db value from a raw argv array, then resolves.
1358
+ * Used by CLI command handlers that receive argv directly.
1359
+ */
1360
+ function resolveDbPathFromArgv(argv) {
1361
+ const idx = argv.indexOf("--db");
1362
+ if (idx !== -1 && idx + 1 < argv.length) return resolveDbPath(argv[idx + 1]);
1363
+ return resolveDbPath();
1364
+ }
1365
+
393
1366
  //#endregion
394
1367
  //#region src/server/auth.ts
395
1368
  /** @fileoverview Password hashing, token signing, and verification using node:crypto. */
@@ -2141,11 +3114,7 @@ function buildCommand(options) {
2141
3114
  env
2142
3115
  };
2143
3116
  }
2144
- /**
2145
- * Parses a single line of stream-json output into zero or more StreamEvents.
2146
- * Only extracts text, images, completion, and error events. Tool use/result
2147
- * events are internal to the agent and not surfaced to the channel layer.
2148
- */
3117
+ /** Parses a single line of stream-json output into zero or more StreamEvents. */
2149
3118
  function parseStreamLine(line) {
2150
3119
  const trimmed = line.trim();
2151
3120
  if (!trimmed || !trimmed.startsWith("{")) return [];
@@ -2166,7 +3135,7 @@ function parseStreamLine(line) {
2166
3135
  }];
2167
3136
  return [];
2168
3137
  case "assistant": return parseAssistantContent(parsed);
2169
- case "user": return extractImages(parsed);
3138
+ case "user": return parseUserContent(parsed);
2170
3139
  case "result": {
2171
3140
  const usage = parsed["usage"];
2172
3141
  return [{
@@ -2216,23 +3185,36 @@ function parseAssistantContent(parsed) {
2216
3185
  type: "text_delta",
2217
3186
  delta: b["text"]
2218
3187
  });
2219
- else if (b["type"] === "tool_use" && typeof b["name"] === "string") events.push({
2220
- type: "tool_use",
2221
- toolName: b["name"]
2222
- });
3188
+ else if (b["type"] === "tool_use" && typeof b["name"] === "string") {
3189
+ const input = b["input"];
3190
+ events.push({
3191
+ type: "tool_use",
3192
+ toolName: b["name"],
3193
+ toolCallId: typeof b["id"] === "string" ? b["id"] : void 0,
3194
+ details: extractToolDetails(b["name"], input)
3195
+ });
3196
+ }
2223
3197
  }
2224
3198
  return events;
2225
3199
  }
2226
- function extractImages(parsed) {
3200
+ function parseUserContent(parsed) {
2227
3201
  const msg = parsed["message"];
2228
3202
  if (!msg || typeof msg !== "object") return [];
2229
3203
  const content = msg["content"];
2230
3204
  if (!Array.isArray(content)) return [];
3205
+ const events = [];
2231
3206
  const images = [];
2232
3207
  for (const block of content) {
2233
3208
  if (!block || typeof block !== "object") continue;
2234
3209
  const b = block;
2235
3210
  if (b["type"] !== "tool_result") continue;
3211
+ const toolUseId = b["tool_use_id"];
3212
+ if (typeof toolUseId === "string") events.push({
3213
+ type: "tool_result",
3214
+ toolCallId: toolUseId,
3215
+ isError: b["is_error"] === true,
3216
+ output: extractToolOutput(b["content"])
3217
+ });
2236
3218
  const inner = b["content"];
2237
3219
  if (!Array.isArray(inner)) continue;
2238
3220
  for (const item of inner) {
@@ -2247,11 +3229,45 @@ function extractImages(parsed) {
2247
3229
  });
2248
3230
  }
2249
3231
  }
2250
- if (images.length > 0) return [{
3232
+ if (images.length > 0) events.push({
2251
3233
  type: "tool_images",
2252
3234
  images
2253
- }];
2254
- return [];
3235
+ });
3236
+ return events;
3237
+ }
3238
+ const MAX_DETAIL_LENGTH = 120;
3239
+ function truncate$1(text, max) {
3240
+ return text.length > max ? text.slice(0, max - 3) + "..." : text;
3241
+ }
3242
+ function extractToolDetails(toolName, input) {
3243
+ if (!input) return void 0;
3244
+ if (toolName === "Bash") {
3245
+ const desc = input["description"];
3246
+ const cmd = input["command"];
3247
+ return truncate$1(desc || cmd || "", MAX_DETAIL_LENGTH) || void 0;
3248
+ }
3249
+ if (toolName === "Read" || toolName === "Edit" || toolName === "Write") return input["file_path"] || void 0;
3250
+ if (toolName === "Grep") {
3251
+ const pattern = input["pattern"];
3252
+ const path = input["path"];
3253
+ if (!pattern) return void 0;
3254
+ return path ? `${pattern} in ${path}` : pattern;
3255
+ }
3256
+ if (toolName === "Glob") return input["pattern"] || void 0;
3257
+ if (toolName === "WebSearch") return input["query"] || void 0;
3258
+ if (toolName === "WebFetch") return input["url"] || void 0;
3259
+ if (toolName.startsWith("mcp__playwright__")) {
3260
+ const url = input["url"];
3261
+ if (url) return url;
3262
+ }
3263
+ }
3264
+ function extractToolOutput(content) {
3265
+ if (typeof content === "string") {
3266
+ const lines = content.split("\n").filter((l) => l.trim());
3267
+ if (lines.length === 0) return void 0;
3268
+ if (lines.length === 1) return truncate$1(lines[0], MAX_DETAIL_LENGTH);
3269
+ return `${lines.length} lines of output`;
3270
+ }
2255
3271
  }
2256
3272
 
2257
3273
  //#endregion
@@ -2456,10 +3472,11 @@ function header(text) {
2456
3472
  text: plain(text)
2457
3473
  };
2458
3474
  }
2459
- function section(text) {
3475
+ function section(text, opts) {
2460
3476
  return {
2461
3477
  type: "section",
2462
- text: mrkdwn(text)
3478
+ text: mrkdwn(text),
3479
+ ...opts?.expand ? { expand: true } : {}
2463
3480
  };
2464
3481
  }
2465
3482
  function fields(...pairs) {
@@ -2477,6 +3494,23 @@ function context(text) {
2477
3494
  elements: [mrkdwn(text)]
2478
3495
  };
2479
3496
  }
3497
+ function feedbackButtons() {
3498
+ return {
3499
+ type: "context_actions",
3500
+ elements: [{
3501
+ type: "feedback_buttons",
3502
+ action_id: "response_feedback",
3503
+ positive_button: {
3504
+ text: plain("Good"),
3505
+ value: "good"
3506
+ },
3507
+ negative_button: {
3508
+ text: plain("Bad"),
3509
+ value: "bad"
3510
+ }
3511
+ }]
3512
+ };
3513
+ }
2480
3514
  const SUBTYPE_LABELS = {
2481
3515
  error_max_turns: "Warning: Hit turn limit",
2482
3516
  error_max_budget_usd: "Warning: Hit budget limit",
@@ -2498,7 +3532,12 @@ function completionFooterBlocks(completion, hasError, birdName, userId) {
2498
3532
  parts.push(formatDuration(completion.durationMs));
2499
3533
  parts.push(`${completion.numTurns} turn${completion.numTurns === 1 ? "" : "s"}`);
2500
3534
  if (birdName) parts.push(birdName);
2501
- return [divider(), context(parts.join(" | "))];
3535
+ return [
3536
+ divider(),
3537
+ feedbackButtons(),
3538
+ context(parts.join(" | ")),
3539
+ context("BrowserBird can hallucinate and may be inaccurate.")
3540
+ ];
2502
3541
  }
2503
3542
  /**
2504
3543
  * Standalone completion card for cron/bird results posted to a channel
@@ -2509,7 +3548,7 @@ function sessionCompleteBlocks(completion, summary, birdName, userId) {
2509
3548
  const blocks = [header(completion.subtype === "success" ? "Session Complete" : "Session Ended"), fields(["Status", statusText], ["Duration", formatDuration(completion.durationMs)], ["Turns", String(completion.numTurns)])];
2510
3549
  if (summary) {
2511
3550
  const trimmed = summary.length > 2800 ? summary.slice(0, 2800) + "..." : summary;
2512
- blocks.push(section(`*Summary:*\n${trimmed}`));
3551
+ blocks.push(section(`*Summary:*\n${trimmed}`, { expand: true }));
2513
3552
  }
2514
3553
  const contextParts = [];
2515
3554
  if (birdName) contextParts.push(`Bird: *${birdName}*`);
@@ -2700,6 +3739,31 @@ function statusBlocks(opts) {
2700
3739
  if (opts.runningBirds && opts.runningBirds.length > 0) result.push(section(`*In flight:* ${opts.runningBirds.join(", ")}`));
2701
3740
  return result;
2702
3741
  }
3742
+ function homeTabView(opts) {
3743
+ const blocks = [
3744
+ header(opts.agentName),
3745
+ section(opts.description),
3746
+ divider(),
3747
+ header("Scheduled Birds")
3748
+ ];
3749
+ if (opts.birds.length === 0) blocks.push(section("No birds configured. Use `/bird create` to get started."));
3750
+ else {
3751
+ const lines = opts.birds.map((b) => {
3752
+ return `[${b.enabled ? "on" : "off"}] *${b.name}* \`${b.schedule}\``;
3753
+ });
3754
+ blocks.push(section(lines.join("\n")));
3755
+ }
3756
+ blocks.push(divider(), header("Quick Commands"), section([
3757
+ "`/bird list` -- show all birds",
3758
+ "`/bird create` -- create a new bird",
3759
+ "`/bird fly <name>` -- trigger a bird now",
3760
+ "`/bird status` -- check system status"
3761
+ ].join("\n")), divider(), context(`${opts.activeSessions}/${opts.maxConcurrent} active sessions`));
3762
+ return {
3763
+ type: "home",
3764
+ blocks
3765
+ };
3766
+ }
2703
3767
  function truncate(text, maxLength) {
2704
3768
  if (text.length <= maxLength) return text;
2705
3769
  return text.slice(0, maxLength) + "...";
@@ -2793,7 +3857,11 @@ function startScheduler(getConfig, signal, deps) {
2793
3857
  return "completed (no output)";
2794
3858
  }
2795
3859
  if (payload.channelId && deps?.postToSlack) {
2796
- await deps.postToSlack(payload.channelId, result);
3860
+ const ts = await deps.postToSlack(payload.channelId, result);
3861
+ if (ts && deps.setThreadTitle) {
3862
+ const title = `${agent.name}: ${payload.prompt.slice(0, 60)}`;
3863
+ deps.setThreadTitle(payload.channelId, ts, title).catch(() => {});
3864
+ }
2797
3865
  if (completion) {
2798
3866
  const blocks = sessionCompleteBlocks(completion, void 0, agent.name);
2799
3867
  const fallback = `Bird ${agent.name} completed: ${completion.numTurns} turns`;
@@ -2950,6 +4018,23 @@ function toolStatusText(toolName) {
2950
4018
  if (toolName === "Edit" || toolName === "Write") return "is writing code...";
2951
4019
  return "is working...";
2952
4020
  }
4021
+ function toolTaskTitle(toolName) {
4022
+ if (toolName.startsWith(BROWSER_TOOL_PREFIX)) return `Browser: ${toolName.slice(17)}`;
4023
+ if (toolName.startsWith("mcp__")) {
4024
+ const parts = toolName.slice(5).split("__");
4025
+ return `${parts[0] ?? "mcp"}: ${parts.slice(1).join("__") || "call"}`;
4026
+ }
4027
+ switch (toolName) {
4028
+ case "Bash": return "Running command";
4029
+ case "Read": return "Reading file";
4030
+ case "Grep": return "Searching code";
4031
+ case "Glob": return "Finding files";
4032
+ case "Edit": return "Editing file";
4033
+ case "Write": return "Writing file";
4034
+ case "Agent": return "Running sub-agent";
4035
+ default: return toolName;
4036
+ }
4037
+ }
2953
4038
  function createHandler(client, getConfig, signal, getTeamId, getChannelNameToId) {
2954
4039
  const locks = /* @__PURE__ */ new Map();
2955
4040
  let activeSpawns = 0;
@@ -2984,6 +4069,8 @@ function createHandler(client, getConfig, signal, getTeamId, getChannelNameToId)
2984
4069
  let hasError = false;
2985
4070
  let timedOut = false;
2986
4071
  let timedOutMs = 0;
4072
+ const activeTasks = /* @__PURE__ */ new Map();
4073
+ let toolCount = 0;
2987
4074
  function isStreamExpired(err) {
2988
4075
  const msg = err instanceof Error ? err.message : String(err);
2989
4076
  return msg.includes("not_in_streaming_state") || msg.includes("streaming");
@@ -3033,7 +4120,40 @@ function createHandler(client, getConfig, signal, getTeamId, getChannelNameToId)
3033
4120
  meta.onToolUse?.(event.toolName);
3034
4121
  lastStatus = toolStatusText(event.toolName);
3035
4122
  client.setStatus?.(channelId, threadTs, lastStatus).catch(() => {});
4123
+ if (event.toolCallId !== void 0) {
4124
+ toolCount++;
4125
+ activeTasks.set(event.toolCallId, event.toolName);
4126
+ const title = toolTaskTitle(event.toolName);
4127
+ const chunks = [];
4128
+ if (toolCount === 1) chunks.push({
4129
+ type: "plan_update",
4130
+ title: "Working on it"
4131
+ });
4132
+ chunks.push({
4133
+ type: "task_update",
4134
+ id: event.toolCallId,
4135
+ title,
4136
+ status: "in_progress",
4137
+ ...event.details ? { details: event.details } : {}
4138
+ });
4139
+ await safeAppend({ chunks });
4140
+ }
4141
+ break;
4142
+ case "tool_result": {
4143
+ const toolName = activeTasks.get(event.toolCallId);
4144
+ if (toolName) {
4145
+ activeTasks.delete(event.toolCallId);
4146
+ const title = toolTaskTitle(toolName);
4147
+ await safeAppend({ chunks: [{
4148
+ type: "task_update",
4149
+ id: event.toolCallId,
4150
+ title,
4151
+ status: event.isError ? "error" : "complete",
4152
+ ...event.output ? { output: event.output } : {}
4153
+ }] });
4154
+ }
3036
4155
  break;
4156
+ }
3037
4157
  case "completion":
3038
4158
  completion = event;
3039
4159
  logger.info(`completion [${event.subtype}]: ${event.tokensIn}in/${event.tokensOut}out, $${event.costUsd.toFixed(4)}, ${event.numTurns} turns`);
@@ -3059,6 +4179,21 @@ function createHandler(client, getConfig, signal, getTeamId, getChannelNameToId)
3059
4179
  }
3060
4180
  clearInterval(statusTimer);
3061
4181
  client.setStatus?.(channelId, threadTs, "").catch(() => {});
4182
+ if (activeTasks.size > 0) {
4183
+ const staleChunks = [];
4184
+ for (const [id, toolName] of activeTasks) staleChunks.push({
4185
+ type: "task_update",
4186
+ id,
4187
+ title: toolTaskTitle(toolName),
4188
+ status: "error"
4189
+ });
4190
+ await safeAppend({ chunks: staleChunks });
4191
+ activeTasks.clear();
4192
+ }
4193
+ if (toolCount > 0) await safeAppend({ chunks: [{
4194
+ type: "plan_update",
4195
+ title: hasError ? "Finished with errors" : `Completed (${toolCount} ${toolCount === 1 ? "step" : "steps"})`
4196
+ }] });
3062
4197
  if (timedOut) {
3063
4198
  await safeStop({});
3064
4199
  const blocks = sessionTimeoutBlocks(timedOutMs, { sessionUid });
@@ -3428,7 +4563,7 @@ var SlackChannelClient = class {
3428
4563
  thread_ts: opts.threadTs,
3429
4564
  recipient_team_id: opts.teamId,
3430
4565
  recipient_user_id: opts.userId,
3431
- task_display_mode: "timeline",
4566
+ task_display_mode: "plan",
3432
4567
  buffer_size: 128
3433
4568
  });
3434
4569
  }
@@ -3446,11 +4581,12 @@ var SlackChannelClient = class {
3446
4581
  title
3447
4582
  });
3448
4583
  }
3449
- async setSuggestedPrompts(channelId, threadTs, prompts) {
4584
+ async setSuggestedPrompts(channelId, threadTs, prompts, title) {
3450
4585
  await this.web.assistant.threads.setSuggestedPrompts({
3451
4586
  channel_id: channelId,
3452
4587
  thread_ts: threadTs,
3453
- prompts
4588
+ prompts,
4589
+ ...title ? { title } : {}
3454
4590
  });
3455
4591
  }
3456
4592
  };
@@ -3572,6 +4708,20 @@ function createSlackChannel(getConfig, signal) {
3572
4708
  }
3573
4709
  coalescer.push(channelId, threadTs, userId, text, messageTs);
3574
4710
  });
4711
+ const DEFAULT_PROMPTS = [
4712
+ {
4713
+ title: "Browse a website",
4714
+ message: "Browse https://example.com and summarize it"
4715
+ },
4716
+ {
4717
+ title: "Run a command",
4718
+ message: "List files in the current directory"
4719
+ },
4720
+ {
4721
+ title: "Help me code",
4722
+ message: "Help me write a function that..."
4723
+ }
4724
+ ];
3575
4725
  socketClient.on("assistant_thread_started", async ({ ack, event }) => {
3576
4726
  await ack();
3577
4727
  if (!event) return;
@@ -3580,23 +4730,54 @@ function createSlackChannel(getConfig, signal) {
3580
4730
  const channelId = threadInfo["channel_id"];
3581
4731
  const threadTs = threadInfo["thread_ts"];
3582
4732
  if (!channelId || !threadTs) return;
3583
- channelClient.setSuggestedPrompts(channelId, threadTs, [
3584
- {
3585
- title: "Browse a website",
3586
- message: "Browse https://example.com and summarize it"
3587
- },
3588
- {
3589
- title: "Run a command",
3590
- message: "List files in the current directory"
3591
- },
3592
- {
3593
- title: "Help me code",
3594
- message: "Help me write a function that..."
3595
- }
3596
- ]).catch(() => {});
4733
+ const agent = matchAgent(channelId, getConfig().agents, channelNameToId);
4734
+ const prompts = agent?.suggestedPrompts ?? DEFAULT_PROMPTS;
4735
+ const promptTitle = `What can ${agent?.name ?? "BrowserBird"} help with?`;
4736
+ channelClient.setSuggestedPrompts?.(channelId, threadTs, prompts, promptTitle).catch(() => {});
3597
4737
  });
3598
- socketClient.on("assistant_thread_context_changed", async ({ ack }) => {
4738
+ socketClient.on("assistant_thread_context_changed", async ({ ack, event }) => {
3599
4739
  await ack();
4740
+ if (!event) return;
4741
+ const threadInfo = event["assistant_thread"];
4742
+ if (!threadInfo) return;
4743
+ const channelId = threadInfo["channel_id"];
4744
+ const threadTs = threadInfo["thread_ts"];
4745
+ if (!channelId || !threadTs) return;
4746
+ const contextChannelId = threadInfo["context"]?.["channel_id"];
4747
+ const config = getConfig();
4748
+ const agent = matchAgent(contextChannelId ?? channelId, config.agents, channelNameToId);
4749
+ const prompts = agent?.suggestedPrompts ?? DEFAULT_PROMPTS;
4750
+ const promptTitle = `What can ${agent?.name ?? "BrowserBird"} help with?`;
4751
+ channelClient.setSuggestedPrompts?.(channelId, threadTs, prompts, promptTitle).catch(() => {});
4752
+ });
4753
+ socketClient.on("app_home_opened", async ({ ack, event }) => {
4754
+ await ack();
4755
+ if (!event) return;
4756
+ const userId = event["user"];
4757
+ if (!userId) return;
4758
+ if (event["tab"] !== "home") return;
4759
+ try {
4760
+ const config = getConfig();
4761
+ const agent = config.agents[0];
4762
+ const birds = listCronJobs(1, 20).items.map((b) => ({
4763
+ name: b.name,
4764
+ schedule: b.schedule,
4765
+ enabled: b.enabled === 1
4766
+ }));
4767
+ const view = homeTabView({
4768
+ agentName: agent?.name ?? "BrowserBird",
4769
+ description: agent?.systemPrompt ?? "A self-hosted AI assistant with a real browser and a scheduler.",
4770
+ birds,
4771
+ activeSessions: handler.activeCount(),
4772
+ maxConcurrent: config.sessions.maxConcurrent
4773
+ });
4774
+ await webClient.views.publish({
4775
+ user_id: userId,
4776
+ view
4777
+ });
4778
+ } catch (err) {
4779
+ logger.warn(`home tab error: ${err instanceof Error ? err.message : String(err)}`);
4780
+ }
3600
4781
  });
3601
4782
  let connected = false;
3602
4783
  const statusProvider = {
@@ -3634,6 +4815,16 @@ function createSlackChannel(getConfig, signal) {
3634
4815
  if (!sessionUid) continue;
3635
4816
  await handleSessionRetry(sessionUid, user ?? "unknown", handler);
3636
4817
  }
4818
+ if (actionId === "response_feedback") {
4819
+ const value = action["value"];
4820
+ if (value === "good" || value === "bad") {
4821
+ const message = body["message"];
4822
+ const threadTs = message?.["thread_ts"];
4823
+ const messageTs = message?.["ts"];
4824
+ insertFeedback(channel, threadTs, messageTs, user ?? "unknown", value);
4825
+ logger.info(`feedback: ${value} from ${user} in ${channel}`);
4826
+ }
4827
+ }
3637
4828
  }
3638
4829
  }
3639
4830
  });
@@ -3711,11 +4902,14 @@ function createSlackChannel(getConfig, signal) {
3711
4902
  return connected;
3712
4903
  }
3713
4904
  async function postMessage(channel, text, opts) {
3714
- await webClient.chat.postMessage({
4905
+ return (await webClient.chat.postMessage({
3715
4906
  channel,
3716
4907
  text,
3717
4908
  ...opts?.blocks ? { blocks: opts.blocks } : {}
3718
- });
4909
+ })).ts ?? "";
4910
+ }
4911
+ async function setTitle(channelId, threadTs, title) {
4912
+ await channelClient.setTitle(channelId, threadTs, title);
3719
4913
  }
3720
4914
  return {
3721
4915
  start,
@@ -3723,6 +4917,7 @@ function createSlackChannel(getConfig, signal) {
3723
4917
  isConnected,
3724
4918
  activeCount: () => handler.activeCount(),
3725
4919
  postMessage,
4920
+ setTitle,
3726
4921
  resolveChannelNames
3727
4922
  };
3728
4923
  }
@@ -3738,7 +4933,6 @@ async function handleBirdCreateSubmission(view, webClient) {
3738
4933
  logger.warn("bird_create submission missing required fields");
3739
4934
  return;
3740
4935
  }
3741
- const { createCronJob, setCronJobEnabled } = await import("./db-BNF1vZIm.mjs").then((n) => n.t);
3742
4936
  const bird = createCronJob(name, schedule, prompt, channelId || void 0, "default");
3743
4937
  if (enabledValue !== "enabled") setCronJobEnabled(bird.uid, false);
3744
4938
  await webClient.chat.postMessage({
@@ -3752,7 +4946,6 @@ async function handleBirdCreateSubmission(view, webClient) {
3752
4946
  }
3753
4947
  async function handleSessionRetry(sessionUid, userId, handler) {
3754
4948
  try {
3755
- const { getSession, getLastInboundMessage } = await import("./db-BNF1vZIm.mjs").then((n) => n.t);
3756
4949
  const session = getSession(sessionUid);
3757
4950
  if (!session) {
3758
4951
  logger.warn(`retry: session ${sessionUid} not found`);
@@ -3851,7 +5044,10 @@ async function startDaemon(options) {
3851
5044
  currentConfig = config;
3852
5045
  if (!schedulerStarted && config.agents.length > 0) {
3853
5046
  logger.info("starting scheduler...");
3854
- startScheduler(getConfig, controller.signal, { postToSlack: (channel, text, opts) => slackHandle ? slackHandle.postMessage(channel, text, opts) : Promise.resolve() });
5047
+ startScheduler(getConfig, controller.signal, {
5048
+ postToSlack: (channel, text, opts) => slackHandle ? slackHandle.postMessage(channel, text, opts) : Promise.resolve(""),
5049
+ setThreadTitle: (channelId, threadTs, title) => slackHandle ? slackHandle.setTitle(channelId, threadTs, title) : Promise.resolve()
5050
+ });
3855
5051
  schedulerStarted = true;
3856
5052
  }
3857
5053
  if (!healthStarted) {