@owloops/browserbird 1.4.15 → 1.4.17

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,7 +1,7 @@
1
1
  import { $ as updateSessionProviderId, A as completeCronRun, B as listFlights, C as failStaleJobs, D as retryAllFailedJobs, E as listJobs, F as ensureSystemCronJob, G as deleteStaleSessions, H as updateCronJob, I as getCronJob, J as getSessionCount, K as findSession, L as getEnabledCronJobs, M as createCronRun, N as deleteCronJob, O as retryJob, P as deleteOldCronRuns, Q as touchSession, R as getFlightStats, S as failJob, T as hasPendingCronJob, U as updateCronJobStatus, V as setCronJobEnabled, W as createSession, X as getSessionTokenStats, Y as getSessionMessages, Z as listSessions, _ as clearJobs, a as getSetting, at as resolveByUid, b as deleteJob, c as getUserCount, d as getRecentLogs, et as shortUid, f as insertLog, g as claimNextJob, h as logMessage, i as createUser, it as optimizeDatabase, j as createCronJob, k as SYSTEM_CRON_PREFIX, l as setSetting, m as getMessageStats, n as resolveDbPath, nt as getDb, o as getUserByEmail, ot as logger, p as deleteOldMessages, q as getSession, r as resolveDbPathFromArgv, rt as openDatabase, s as getUserById, tt as closeDatabase, u as deleteOldLogs, v as completeJob, w as getJobStats, x as deleteOldJobs, y as createJob, z as listCronJobs } from "./db-CyQcrilg.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { parseArgs, styleText } from "node:util";
4
- import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
4
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
5
  import { dirname, extname, join, resolve } from "node:path";
6
6
  import { createHmac, randomBytes, scrypt, timingSafeEqual } from "node:crypto";
7
7
  import { connect } from "node:net";
@@ -122,8 +122,8 @@ function unknownSubcommand(subcommand, command, validCommands) {
122
122
  /** @fileoverview ASCII banner displayed on daemon startup and in help text. */
123
123
  const pkg = createRequire(import.meta.url)("../package.json");
124
124
  const buildInfo = [];
125
- buildInfo.push(`commit: ${"d1793a3eaee8e6d63526c28dcdca1e3598d384a9".substring(0, 7)}`);
126
- buildInfo.push(`built: 2026-03-13T00:29:35+04:00`);
125
+ buildInfo.push(`commit: ${"4e976792ac1f38e4ad5ba619163daa52cc482ae1".substring(0, 7)}`);
126
+ buildInfo.push(`built: 2026-03-15T22:25:47+04:00`);
127
127
  const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
128
128
  const VERSION = `browserbird ${pkg.version}${buildString}`;
129
129
  const BIRD = [
@@ -138,7 +138,6 @@ const BANNER = BIRD;
138
138
  //#endregion
139
139
  //#region src/config.ts
140
140
  /** @fileoverview Configuration loading from JSON with env: variable resolution. */
141
- const VALID_PROVIDERS$1 = new Set(["claude", "opencode"]);
142
141
  const DEFAULTS = {
143
142
  timezone: "UTC",
144
143
  slack: {
@@ -160,7 +159,6 @@ const DEFAULTS = {
160
159
  agents: [{
161
160
  id: "default",
162
161
  name: "BrowserBird",
163
- provider: "claude",
164
162
  model: "sonnet",
165
163
  maxTurns: 50,
166
164
  systemPrompt: "You are responding in a Slack workspace. Be concise, helpful, and natural.",
@@ -243,7 +241,6 @@ function validateConfig(config) {
243
241
  if (!Array.isArray(config.agents) || config.agents.length === 0) throw new Error("at least one agent must be configured");
244
242
  for (const agent of config.agents) {
245
243
  if (!agent.id || !agent.name) throw new Error("each agent must have an \"id\" and \"name\"");
246
- if (!VALID_PROVIDERS$1.has(agent.provider)) throw new Error(`agent "${agent.id}": unknown provider "${agent.provider}" (expected: ${[...VALID_PROVIDERS$1].join(", ")})`);
247
244
  if (!agent.model) throw new Error(`agent "${agent.id}": "model" is required`);
248
245
  if (!Array.isArray(agent.channels) || agent.channels.length === 0) throw new Error(`agent "${agent.id}": "channels" must be a non-empty array`);
249
246
  if (agent.fallbackModel && agent.fallbackModel === agent.model) throw new Error(`agent "${agent.id}": fallbackModel cannot be the same as model ("${agent.model}")`);
@@ -617,7 +614,7 @@ function c(format, text) {
617
614
  const DOCTOR_HELP = `
618
615
  ${c("cyan", "usage:")} browserbird doctor
619
616
 
620
- check system dependencies (agent clis, node.js).
617
+ check system dependencies (agent cli, node.js).
621
618
  `.trim();
622
619
  function checkCli(binary, versionArgs) {
623
620
  try {
@@ -643,7 +640,6 @@ function checkCli(binary, versionArgs) {
643
640
  function checkDoctor() {
644
641
  return {
645
642
  claude: checkCli("claude", ["--version"]),
646
- opencode: checkCli("opencode", ["--version"]),
647
643
  node: process.version
648
644
  };
649
645
  }
@@ -656,11 +652,6 @@ function handleDoctor() {
656
652
  logger.error("claude cli: not found");
657
653
  process.stderr.write(" install: npm install -g @anthropic-ai/claude-code\n");
658
654
  }
659
- if (result.opencode.available) logger.success(`opencode cli: ${result.opencode.version}`);
660
- else {
661
- logger.warn("opencode cli: not found (optional)");
662
- process.stderr.write(" install: npm install -g opencode\n");
663
- }
664
655
  logger.success(`node.js: ${result.node}`);
665
656
  }
666
657
 
@@ -674,11 +665,10 @@ let agentAvailable = false;
674
665
  let agentCheckedAt = 0;
675
666
  let browserConnected = false;
676
667
  let browserCheckPending = false;
677
- function refreshAgent(config) {
668
+ function refreshAgent() {
678
669
  const now = Date.now();
679
670
  if (now - agentCheckedAt < AGENT_CHECK_INTERVAL_MS) return;
680
- const result = checkDoctor();
681
- agentAvailable = [...new Set(config.agents.map((a) => a.provider))].some((p) => result[p]?.available === true);
671
+ agentAvailable = checkDoctor().claude.available;
682
672
  agentCheckedAt = now;
683
673
  }
684
674
  function probeBrowser(host, port) {
@@ -710,14 +700,14 @@ function refreshBrowser(config) {
710
700
  });
711
701
  }
712
702
  function getServiceHealth(config) {
713
- refreshAgent(config);
703
+ refreshAgent();
714
704
  return {
715
705
  agent: { available: agentAvailable },
716
706
  browser: { connected: config.browser.enabled ? browserConnected : false }
717
707
  };
718
708
  }
719
709
  function startHealthChecks(getConfig, signal) {
720
- refreshAgent(getConfig());
710
+ refreshAgent();
721
711
  refreshBrowser(getConfig());
722
712
  const timer = setInterval(() => {
723
713
  refreshBrowser(getConfig());
@@ -875,7 +865,6 @@ function resolveBirdParam(params, res) {
875
865
  }
876
866
  return result.row;
877
867
  }
878
- const VALID_PROVIDERS = new Set(["claude", "opencode"]);
879
868
  function maskSecret(value) {
880
869
  if (!value) return {
881
870
  set: false,
@@ -909,7 +898,6 @@ function sanitizeConfig(config) {
909
898
  agents: config.agents.map((a) => ({
910
899
  id: a.id,
911
900
  name: a.name,
912
- provider: a.provider,
913
901
  model: a.model,
914
902
  fallbackModel: a.fallbackModel ?? null,
915
903
  maxTurns: a.maxTurns,
@@ -949,7 +937,6 @@ function validateConfigPatch(body) {
949
937
  for (const a of agents) {
950
938
  if (!a["id"] || typeof a["id"] !== "string") return "Each agent must have a string \"id\"";
951
939
  if (!a["name"] || typeof a["name"] !== "string") return "Each agent must have a string \"name\"";
952
- if (!a["provider"] || !VALID_PROVIDERS.has(a["provider"])) return `Agent "${a["id"]}": invalid provider (expected: ${[...VALID_PROVIDERS].join(", ")})`;
953
940
  if (!a["model"] || typeof a["model"] !== "string") return `Agent "${a["id"]}": "model" is required`;
954
941
  if (!Array.isArray(a["channels"]) || a["channels"].length === 0) return `Agent "${a["id"]}": "channels" must be a non-empty array`;
955
942
  }
@@ -1557,21 +1544,21 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1557
1544
  pattern: pathToRegex("/api/onboarding/defaults"),
1558
1545
  handler(_req, res) {
1559
1546
  const doctor = checkDoctor();
1560
- const defaultAgent = DEFAULTS.agents[0];
1561
- const novncHost = !!process.env["RAILWAY_ENVIRONMENT_NAME"] ? "browserbird-vm.railway.internal" : DEFAULTS.browser.novncHost;
1547
+ const config = getConfig();
1548
+ const defaultAgent = config.agents[0] ?? DEFAULTS.agents[0];
1549
+ const novncHost = !!process.env["RAILWAY_ENVIRONMENT_NAME"] ? "browserbird-vm.railway.internal" : config.browser.novncHost || DEFAULTS.browser.novncHost;
1562
1550
  json(res, {
1563
1551
  agent: {
1564
1552
  name: defaultAgent.name,
1565
- provider: defaultAgent.provider,
1566
1553
  model: defaultAgent.model,
1567
1554
  systemPrompt: defaultAgent.systemPrompt,
1568
1555
  maxTurns: defaultAgent.maxTurns,
1569
1556
  channels: defaultAgent.channels
1570
1557
  },
1571
1558
  browser: {
1572
- enabled: DEFAULTS.browser.enabled,
1559
+ enabled: config.browser.enabled,
1573
1560
  novncHost,
1574
- novncPort: DEFAULTS.browser.novncPort
1561
+ novncPort: config.browser.novncPort
1575
1562
  },
1576
1563
  doctor
1577
1564
  });
@@ -1658,10 +1645,6 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1658
1645
  jsonError(res, "\"name\" is required", 400);
1659
1646
  return;
1660
1647
  }
1661
- if (!body.provider || typeof body.provider !== "string") {
1662
- jsonError(res, "\"provider\" is required", 400);
1663
- return;
1664
- }
1665
1648
  if (!body.model || typeof body.model !== "string") {
1666
1649
  jsonError(res, "\"model\" is required", 400);
1667
1650
  return;
@@ -1671,7 +1654,6 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1671
1654
  raw["agents"] = [{
1672
1655
  id: "default",
1673
1656
  name: body.name.trim(),
1674
- provider: body.provider.trim(),
1675
1657
  model: body.model.trim(),
1676
1658
  maxTurns: body.maxTurns ?? DEFAULTS.agents[0].maxTurns,
1677
1659
  systemPrompt: body.systemPrompt?.trim() ?? DEFAULTS.agents[0].systemPrompt,
@@ -2067,16 +2049,20 @@ function startWorker(signal) {
2067
2049
  * Matches an incoming message to the correct agent based on channel config.
2068
2050
  * Agents are checked in order; first match wins.
2069
2051
  * A wildcard `"*"` in the agent's channels array matches everything.
2052
+ * The optional `nameToId` map resolves human-readable channel names to Slack IDs.
2070
2053
  */
2071
- function matchAgent(channelId, agents) {
2072
- for (const agent of agents) for (const pattern of agent.channels) if (pattern === "*" || pattern === channelId) return agent;
2054
+ function matchAgent(channelId, agents, nameToId) {
2055
+ for (const agent of agents) for (const pattern of agent.channels) {
2056
+ if (pattern === "*" || pattern === channelId) return agent;
2057
+ if (nameToId?.get(pattern) === channelId) return agent;
2058
+ }
2073
2059
  }
2074
2060
  /**
2075
2061
  * Looks up or creates a session for the given Slack thread.
2076
2062
  * Returns the session row and whether it was newly created.
2077
2063
  */
2078
- function resolveSession(channelId, threadTs, config) {
2079
- const agent = matchAgent(channelId, config.agents);
2064
+ function resolveSession(channelId, threadTs, config, nameToId) {
2065
+ const agent = matchAgent(channelId, config.agents, nameToId);
2080
2066
  if (!agent) {
2081
2067
  logger.warn(`no agent matched for channel ${channelId}`);
2082
2068
  return null;
@@ -2112,21 +2098,10 @@ function expireStaleSessions(ttlHours) {
2112
2098
  return deleted;
2113
2099
  }
2114
2100
 
2115
- //#endregion
2116
- //#region src/provider/stream.ts
2117
- /**
2118
- * Splits a raw data chunk into lines, handling partial lines across chunks.
2119
- * Returns [completeLines, remainingPartial].
2120
- */
2121
- function splitLines(buffer, chunk) {
2122
- const lines = (buffer + chunk).split("\n");
2123
- return [lines, lines.pop() ?? ""];
2124
- }
2125
-
2126
2101
  //#endregion
2127
2102
  //#region src/provider/claude.ts
2128
2103
  /** @fileoverview Claude Code CLI provider: arg building and stream-json parsing. */
2129
- function buildCommand$1(options) {
2104
+ function buildCommand(options) {
2130
2105
  const { message, sessionId, agent, mcpConfigPath } = options;
2131
2106
  const args = [
2132
2107
  "-p",
@@ -2162,7 +2137,7 @@ function buildCommand$1(options) {
2162
2137
  * Only extracts text, images, completion, and error events. Tool use/result
2163
2138
  * events are internal to the agent and not surfaced to the channel layer.
2164
2139
  */
2165
- function parseStreamLine$1(line) {
2140
+ function parseStreamLine(line) {
2166
2141
  const trimmed = line.trim();
2167
2142
  if (!trimmed || !trimmed.startsWith("{")) return [];
2168
2143
  let parsed;
@@ -2269,226 +2244,22 @@ function extractImages(parsed) {
2269
2244
  }];
2270
2245
  return [];
2271
2246
  }
2272
- const claude = {
2273
- buildCommand: buildCommand$1,
2274
- parseStreamLine: parseStreamLine$1
2275
- };
2276
2247
 
2277
2248
  //#endregion
2278
- //#region src/provider/opencode.ts
2279
- /** @fileoverview OpenCode CLI provider: arg building, workspace setup, and JSON stream parsing. */
2280
- const WORKSPACE_DIR = resolve(".browserbird", "opencode");
2281
- /**
2282
- * Translates a Claude-format MCP config into an opencode-format config.
2283
- *
2284
- * Claude format: { mcpServers: { name: { type: "sse", url: "..." } } }
2285
- * OpenCode format: { mcp: { name: { type: "remote", url: "..." } } }
2286
- */
2287
- function translateMcpConfig(claudeConfig) {
2288
- const servers = claudeConfig["mcpServers"] ?? {};
2289
- const result = {};
2290
- for (const [name, server] of Object.entries(servers)) {
2291
- const serverType = server["type"];
2292
- const url = server["url"];
2293
- const command = server["command"];
2294
- const args = server["args"];
2295
- if (serverType === "sse" || serverType === "streamable-http") {
2296
- if (url) result[name] = {
2297
- type: "remote",
2298
- url
2299
- };
2300
- } else if (serverType === "stdio") {
2301
- if (command) {
2302
- const entry = {
2303
- type: "local",
2304
- command: args ? [command, ...args] : [command]
2305
- };
2306
- const env = server["env"];
2307
- if (env) entry["environment"] = env;
2308
- result[name] = entry;
2309
- }
2310
- }
2311
- }
2312
- return result;
2313
- }
2314
- /**
2315
- * Ensures the opencode workspace directory exists with the right config files.
2316
- * Writes opencode.json (MCP servers) and .opencode/agent/browserbird.md (system prompt).
2317
- */
2318
- function ensureWorkspace(mcpConfigPath, systemPrompt) {
2319
- mkdirSync(resolve(WORKSPACE_DIR, ".opencode", "agent"), { recursive: true });
2320
- const config = {};
2321
- if (mcpConfigPath) try {
2322
- const raw = readFileSync(resolve(mcpConfigPath), "utf-8");
2323
- const mcp = translateMcpConfig(JSON.parse(raw));
2324
- if (Object.keys(mcp).length > 0) config["mcp"] = mcp;
2325
- } catch (err) {
2326
- logger.warn(`opencode: failed to read MCP config at ${mcpConfigPath}: ${err instanceof Error ? err.message : String(err)}`);
2327
- }
2328
- writeFileSync(resolve(WORKSPACE_DIR, "opencode.json"), JSON.stringify(config, null, 2) + "\n");
2329
- if (systemPrompt) {
2330
- const agentMd = `---\nmode: primary\n---\n\n${systemPrompt}\n`;
2331
- writeFileSync(resolve(WORKSPACE_DIR, ".opencode", "agent", "browserbird.md"), agentMd);
2332
- }
2333
- const agentsMd = resolve("AGENTS.md");
2334
- if (existsSync(agentsMd)) copyFileSync(agentsMd, resolve(WORKSPACE_DIR, "AGENTS.md"));
2335
- }
2336
- /**
2337
- * Builds the `opencode run` command for a given agent config.
2338
- *
2339
- * @remarks `fallbackModel` is not yet supported by opencode.
2340
- * @see https://github.com/anomalyco/opencode/issues/7602
2341
- */
2342
- function buildCommand(options) {
2343
- const { message, sessionId, agent, mcpConfigPath } = options;
2344
- const systemParts = [];
2345
- if (agent.systemPrompt) systemParts.push(agent.systemPrompt);
2346
- if (options.timezone) systemParts.push(`System timezone: ${options.timezone}. All cron expressions and scheduled times use this timezone.`);
2347
- ensureWorkspace(mcpConfigPath, systemParts.join(" ") || void 0);
2348
- const args = [
2349
- "run",
2350
- "--format",
2351
- "json",
2352
- "-m",
2353
- agent.model
2354
- ];
2355
- if (agent.systemPrompt) args.push("--agent", "browserbird");
2356
- if (sessionId) args.push("--session", sessionId);
2357
- args.push(message);
2358
- const env = {};
2359
- const apiKey = process.env["ANTHROPIC_API_KEY"];
2360
- if (apiKey) env["ANTHROPIC_API_KEY"] = apiKey;
2361
- const openRouterKey = process.env["OPENROUTER_API_KEY"];
2362
- if (openRouterKey) env["OPENROUTER_API_KEY"] = openRouterKey;
2363
- const openAiKey = process.env["OPENAI_API_KEY"];
2364
- if (openAiKey) env["OPENAI_API_KEY"] = openAiKey;
2365
- const geminiKey = process.env["GEMINI_API_KEY"];
2366
- if (geminiKey) env["GEMINI_API_KEY"] = geminiKey;
2367
- return {
2368
- binary: "opencode",
2369
- args,
2370
- cwd: WORKSPACE_DIR,
2371
- env
2372
- };
2373
- }
2374
- /** Per-session metric accumulators. Concurrent sessions each get their own entry. */
2375
- const accumulators = /* @__PURE__ */ new Map();
2376
- function accumulateStep(sessionId, part) {
2377
- const acc = accumulators.get(sessionId);
2378
- acc.stepCount++;
2379
- const tokens = part["tokens"];
2380
- const cache = tokens?.["cache"];
2381
- acc.tokensIn += tokens?.["input"] ?? 0;
2382
- acc.tokensOut += tokens?.["output"] ?? 0;
2383
- acc.cacheWrite += cache?.["write"] ?? 0;
2384
- acc.cacheRead += cache?.["read"] ?? 0;
2385
- acc.cost += part["cost"] ?? 0;
2386
- }
2249
+ //#region src/provider/stream.ts
2387
2250
  /**
2388
- * Parses a single line of opencode JSON output into zero or more StreamEvents.
2389
- *
2390
- * OpenCode emits these event types:
2391
- * step_start -> init (first one carries sessionID)
2392
- * text -> text_delta
2393
- * tool_use -> ignored (internal to the agent)
2394
- * step_finish -> accumulates tokens/cost; final one (reason "stop") emits completion
2395
- * error -> error (connection/auth failures)
2251
+ * Splits a raw data chunk into lines, handling partial lines across chunks.
2252
+ * Returns [completeLines, remainingPartial].
2396
2253
  */
2397
- function parseStreamLine(line) {
2398
- const trimmed = line.trim();
2399
- if (!trimmed || !trimmed.startsWith("{")) return [];
2400
- let parsed;
2401
- try {
2402
- parsed = JSON.parse(trimmed);
2403
- } catch {
2404
- return [];
2405
- }
2406
- const eventType = parsed["type"];
2407
- if (!eventType) return [];
2408
- const part = parsed["part"];
2409
- const timestamp = parsed["timestamp"] ?? 0;
2410
- switch (eventType) {
2411
- case "step_start":
2412
- if (part && typeof part["sessionID"] === "string") {
2413
- const sid = part["sessionID"];
2414
- if (!accumulators.has(sid)) accumulators.set(sid, {
2415
- startTimestamp: timestamp,
2416
- stepCount: 0,
2417
- tokensIn: 0,
2418
- tokensOut: 0,
2419
- cacheWrite: 0,
2420
- cacheRead: 0,
2421
- cost: 0
2422
- });
2423
- return [{
2424
- type: "init",
2425
- sessionId: sid,
2426
- model: ""
2427
- }];
2428
- }
2429
- return [];
2430
- case "text":
2431
- if (part && typeof part["text"] === "string") return [{
2432
- type: "text_delta",
2433
- delta: part["text"]
2434
- }];
2435
- return [];
2436
- case "step_finish": {
2437
- if (!part) return [];
2438
- const sid = part["sessionID"] ?? "";
2439
- accumulateStep(sid, part);
2440
- if (part["reason"] !== "stop") return [];
2441
- const acc = accumulators.get(sid);
2442
- const durationMs = timestamp > acc.startTimestamp ? timestamp - acc.startTimestamp : 0;
2443
- const completion = {
2444
- type: "completion",
2445
- subtype: "success",
2446
- result: "",
2447
- sessionId: sid,
2448
- isError: false,
2449
- tokensIn: acc.tokensIn,
2450
- tokensOut: acc.tokensOut,
2451
- cacheCreationTokens: acc.cacheWrite,
2452
- cacheReadTokens: acc.cacheRead,
2453
- costUsd: acc.cost,
2454
- durationMs,
2455
- numTurns: acc.stepCount
2456
- };
2457
- accumulators.delete(sid);
2458
- return [completion];
2459
- }
2460
- case "tool_use": {
2461
- const toolName = typeof part?.["tool"] === "string" && part["tool"] || "";
2462
- if (toolName) return [{
2463
- type: "tool_use",
2464
- toolName
2465
- }];
2466
- return [];
2467
- }
2468
- case "error": {
2469
- const err = parsed["error"];
2470
- const data = err?.["data"];
2471
- return [{
2472
- type: "error",
2473
- error: typeof data?.["message"] === "string" && data["message"] || typeof parsed["message"] === "string" && parsed["message"] || typeof err?.["name"] === "string" && err["name"] || JSON.stringify(parsed)
2474
- }];
2475
- }
2476
- default: return [];
2477
- }
2254
+ function splitLines(buffer, chunk) {
2255
+ const lines = (buffer + chunk).split("\n");
2256
+ return [lines, lines.pop() ?? ""];
2478
2257
  }
2479
- const opencode = {
2480
- buildCommand,
2481
- parseStreamLine
2482
- };
2483
2258
 
2484
2259
  //#endregion
2485
2260
  //#region src/provider/spawn.ts
2486
- /** @fileoverview Spawn a CLI provider as a subprocess. */
2261
+ /** @fileoverview Spawn the Claude CLI as a subprocess with streaming output. */
2487
2262
  const SIGKILL_GRACE_MS = 5e3;
2488
- const PROVIDERS = {
2489
- claude,
2490
- opencode
2491
- };
2492
2263
  /** Sends SIGTERM, then SIGKILL after a grace period if the process is still alive. */
2493
2264
  function gracefulKill(proc) {
2494
2265
  if (!proc.pid || proc.killed) return;
@@ -2517,12 +2288,11 @@ function cleanEnv() {
2517
2288
  return env;
2518
2289
  }
2519
2290
  /**
2520
- * Spawns a provider CLI with streaming output.
2291
+ * Spawns the Claude CLI with streaming output.
2521
2292
  * Returns an async iterable of parsed stream events and a kill handle.
2522
2293
  */
2523
- function spawnProvider(provider, options, signal) {
2524
- const mod = PROVIDERS[provider];
2525
- const cmd = mod.buildCommand(options);
2294
+ function spawnProvider(options, signal) {
2295
+ const cmd = buildCommand(options);
2526
2296
  const timeoutMs = options.agent.processTimeoutMs ?? options.globalTimeoutMs ?? 3e5;
2527
2297
  logger.debug(`spawning: ${cmd.binary} ${cmd.args.join(" ")} (timeout: ${timeoutMs}ms)`);
2528
2298
  const baseEnv = cleanEnv();
@@ -2552,10 +2322,10 @@ function spawnProvider(provider, options, signal) {
2552
2322
  async function* iterate() {
2553
2323
  let buffer = "";
2554
2324
  try {
2555
- yield* parseStdout(proc, mod, buffer, (b) => {
2325
+ yield* parseStdout(proc, buffer, (b) => {
2556
2326
  buffer = b;
2557
2327
  });
2558
- if (buffer.trim()) yield* mod.parseStreamLine(buffer);
2328
+ if (buffer.trim()) yield* parseStreamLine(buffer);
2559
2329
  if (timedOut) yield {
2560
2330
  type: "timeout",
2561
2331
  timeoutMs
@@ -2571,7 +2341,7 @@ function spawnProvider(provider, options, signal) {
2571
2341
  kill: () => gracefulKill(proc)
2572
2342
  };
2573
2343
  }
2574
- async function* parseStdout(proc, mod, buffer, setBuffer) {
2344
+ async function* parseStdout(proc, buffer, setBuffer) {
2575
2345
  const pending = [];
2576
2346
  let done = false;
2577
2347
  let error = null;
@@ -2600,7 +2370,7 @@ async function* parseStdout(proc, mod, buffer, setBuffer) {
2600
2370
  const [lines, remaining] = splitLines(buffer, data);
2601
2371
  buffer = remaining;
2602
2372
  setBuffer(buffer);
2603
- for (const line of lines) yield* mod.parseStreamLine(line);
2373
+ for (const line of lines) yield* parseStreamLine(line);
2604
2374
  }
2605
2375
  }
2606
2376
  if (error) yield {
@@ -2967,7 +2737,7 @@ function startScheduler(getConfig, signal, deps) {
2967
2737
  const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
2968
2738
  let browserLock = null;
2969
2739
  try {
2970
- const { events } = spawnProvider(agent.provider, {
2740
+ const { events } = spawnProvider({
2971
2741
  message: payload.prompt,
2972
2742
  agent,
2973
2743
  mcpConfigPath: config.browser.mcpConfigPath,
@@ -3146,7 +2916,7 @@ function createCoalescer(config, onDispatch) {
3146
2916
  //#endregion
3147
2917
  //#region src/channel/handler.ts
3148
2918
  const BROWSER_TOOL_PREFIX = "mcp__playwright__";
3149
- function createHandler(client, getConfig, signal, getTeamId) {
2919
+ function createHandler(client, getConfig, signal, getTeamId, getChannelNameToId) {
3150
2920
  const locks = /* @__PURE__ */ new Map();
3151
2921
  let activeSpawns = 0;
3152
2922
  function getLock(key) {
@@ -3292,7 +3062,7 @@ function createHandler(client, getConfig, signal, getTeamId) {
3292
3062
  activeSpawns++;
3293
3063
  let sessionUid;
3294
3064
  try {
3295
- const resolved = resolveSession(channelId, threadTs, config);
3065
+ const resolved = resolveSession(channelId, threadTs, config, getChannelNameToId?.());
3296
3066
  if (!resolved) {
3297
3067
  const blocks = noAgentBlocks(channelId);
3298
3068
  await client.postMessage(channelId, threadTs, "No agent configured for this channel.", { blocks });
@@ -3305,10 +3075,9 @@ function createHandler(client, getConfig, signal, getTeamId) {
3305
3075
  broadcastSSE("invalidate", { resource: "sessions" });
3306
3076
  const prompt = formatPrompt(messages);
3307
3077
  const userId = messages[messages.length - 1].userId;
3308
- const existingSessionId = isNew ? void 0 : session.provider_session_id || void 0;
3309
- const { events, kill } = spawnProvider(agent.provider, {
3078
+ const { events, kill } = spawnProvider({
3310
3079
  message: prompt,
3311
- sessionId: existingSessionId,
3080
+ sessionId: isNew ? void 0 : session.provider_session_id || void 0,
3312
3081
  agent,
3313
3082
  mcpConfigPath: config.browser.mcpConfigPath,
3314
3083
  timezone: config.timezone,
@@ -3653,7 +3422,7 @@ function createSlackChannel(getConfig, signal) {
3653
3422
  });
3654
3423
  const webClient = new WebClient(initConfig.slack.botToken);
3655
3424
  const channelClient = new SlackChannelClient(webClient);
3656
- const handler = createHandler(channelClient, getConfig, signal, () => teamId);
3425
+ const handler = createHandler(channelClient, getConfig, signal, () => teamId, () => channelNameToId);
3657
3426
  const coalescer = createCoalescer(initConfig.slack.coalesce, (dispatch) => {
3658
3427
  handler.handle(dispatch).catch(logDispatchError);
3659
3428
  });
@@ -3685,7 +3454,7 @@ function createSlackChannel(getConfig, signal) {
3685
3454
  const isDm = event["channel_type"] === "im";
3686
3455
  if (!isDm && config.slack.requireMention) return;
3687
3456
  const channelId = event["channel"];
3688
- if (!isChannelAllowed(channelId, config.slack.channels)) return;
3457
+ if (!isChannelAllowed(channelId, config.slack.channels, channelNameToId)) return;
3689
3458
  if (!isDm && isQuietHours(config.slack.quietHours)) return;
3690
3459
  const threadTs = event["thread_ts"] ?? event["ts"];
3691
3460
  const userId = event["user"] ?? "unknown";
@@ -3709,7 +3478,7 @@ function createSlackChannel(getConfig, signal) {
3709
3478
  if (isDuplicate(body)) return;
3710
3479
  const config = getConfig();
3711
3480
  const channelId = event["channel"];
3712
- if (!isChannelAllowed(channelId, config.slack.channels)) return;
3481
+ if (!isChannelAllowed(channelId, config.slack.channels, channelNameToId)) return;
3713
3482
  if (isQuietHours(config.slack.quietHours)) return;
3714
3483
  const messageTs = event["ts"];
3715
3484
  if (!messageTs) return;
@@ -3804,6 +3573,8 @@ function createSlackChannel(getConfig, signal) {
3804
3573
  logger.info("reset reconnection back-off counter");
3805
3574
  }
3806
3575
  });
3576
+ /** Rebuilt on connect and config reload via resolveChannelNames(). */
3577
+ const channelNameToId = /* @__PURE__ */ new Map();
3807
3578
  async function resolveChannelNames() {
3808
3579
  const target = getConfig();
3809
3580
  const namesToResolve = /* @__PURE__ */ new Set();
@@ -3813,7 +3584,7 @@ function createSlackChannel(getConfig, signal) {
3813
3584
  collectNames(target.slack.channels);
3814
3585
  for (const agent of target.agents) collectNames(agent.channels);
3815
3586
  if (namesToResolve.size === 0) return;
3816
- const nameToId = /* @__PURE__ */ new Map();
3587
+ channelNameToId.clear();
3817
3588
  try {
3818
3589
  let cursor;
3819
3590
  do {
@@ -3823,26 +3594,17 @@ function createSlackChannel(getConfig, signal) {
3823
3594
  exclude_archived: true,
3824
3595
  cursor
3825
3596
  });
3826
- for (const ch of result.channels ?? []) if (ch.name && ch.id && namesToResolve.has(ch.name)) nameToId.set(ch.name, ch.id);
3597
+ for (const ch of result.channels ?? []) if (ch.name && ch.id && namesToResolve.has(ch.name)) {
3598
+ channelNameToId.set(ch.name, ch.id);
3599
+ logger.info(`resolved channel "${ch.name}" -> ${ch.id}`);
3600
+ }
3827
3601
  cursor = result.response_metadata?.next_cursor || void 0;
3828
3602
  } while (cursor);
3829
3603
  } catch (err) {
3830
3604
  logger.warn(`failed to resolve channel names: ${err instanceof Error ? err.message : String(err)}`);
3831
3605
  return;
3832
3606
  }
3833
- function resolveList(channels, label) {
3834
- return channels.map((ch) => {
3835
- const resolved = nameToId.get(ch);
3836
- if (resolved) {
3837
- logger.info(`${label}: resolved channel "${ch}" -> ${resolved}`);
3838
- return resolved;
3839
- }
3840
- if (namesToResolve.has(ch)) logger.warn(`${label}: channel "${ch}" not found in workspace`);
3841
- return ch;
3842
- });
3843
- }
3844
- target.slack.channels = resolveList(target.slack.channels, "slack");
3845
- for (const agent of target.agents) agent.channels = resolveList(agent.channels, `agent "${agent.id}"`);
3607
+ for (const name of namesToResolve) if (!channelNameToId.has(name)) logger.warn(`channel "${name}" not found in workspace`);
3846
3608
  }
3847
3609
  async function start() {
3848
3610
  const authResult = await webClient.auth.test();
@@ -3931,9 +3693,13 @@ async function handleSessionRetry(sessionUid, userId, handler) {
3931
3693
  logger.error(`retry error: ${err instanceof Error ? err.message : String(err)}`);
3932
3694
  }
3933
3695
  }
3934
- function isChannelAllowed(channelId, channels) {
3696
+ function isChannelAllowed(channelId, channels, nameToId) {
3935
3697
  if (channels.includes("*")) return true;
3936
- return channels.includes(channelId);
3698
+ for (const ch of channels) {
3699
+ if (ch === channelId) return true;
3700
+ if (nameToId.get(ch) === channelId) return true;
3701
+ }
3702
+ return false;
3937
3703
  }
3938
3704
  function isQuietHours(quietHours) {
3939
3705
  if (!quietHours.enabled) return false;
@@ -4793,7 +4559,6 @@ function printConfig(configPath) {
4793
4559
  console.log(`\n${c("cyan", "agents:")}`);
4794
4560
  for (const a of config.agents) {
4795
4561
  console.log(` ${c("cyan", a.id)} (${a.name})`);
4796
- console.log(` ${c("dim", "provider:")} ${a.provider}`);
4797
4562
  console.log(` ${c("dim", "model:")} ${a.model}`);
4798
4563
  console.log(` ${c("dim", "max turns:")} ${a.maxTurns}`);
4799
4564
  console.log(` ${c("dim", "channels:")} ${a.channels.join(", ") || "*"}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owloops/browserbird",
3
- "version": "1.4.15",
3
+ "version": "1.4.17",
4
4
  "description": "AI agent orchestrator with a real browser, a cron scheduler, and a web dashboard",
5
5
  "type": "module",
6
6
  "bin": {