@owloops/browserbird 1.4.14 → 1.4.16

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: ${"b38241676b0cfb906f92f85c02adcfc544353f75".substring(0, 7)}`);
126
- buildInfo.push(`built: 2026-03-12T16:47:28+04:00`);
125
+ buildInfo.push(`commit: ${"b807d937b0d78743f9de1d49acfe9cc32165fe4f".substring(0, 7)}`);
126
+ buildInfo.push(`built: 2026-03-15T01:33:22+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
  }
@@ -1562,7 +1549,6 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1562
1549
  json(res, {
1563
1550
  agent: {
1564
1551
  name: defaultAgent.name,
1565
- provider: defaultAgent.provider,
1566
1552
  model: defaultAgent.model,
1567
1553
  systemPrompt: defaultAgent.systemPrompt,
1568
1554
  maxTurns: defaultAgent.maxTurns,
@@ -1658,10 +1644,6 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1658
1644
  jsonError(res, "\"name\" is required", 400);
1659
1645
  return;
1660
1646
  }
1661
- if (!body.provider || typeof body.provider !== "string") {
1662
- jsonError(res, "\"provider\" is required", 400);
1663
- return;
1664
- }
1665
1647
  if (!body.model || typeof body.model !== "string") {
1666
1648
  jsonError(res, "\"model\" is required", 400);
1667
1649
  return;
@@ -1671,7 +1653,6 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1671
1653
  raw["agents"] = [{
1672
1654
  id: "default",
1673
1655
  name: body.name.trim(),
1674
- provider: body.provider.trim(),
1675
1656
  model: body.model.trim(),
1676
1657
  maxTurns: body.maxTurns ?? DEFAULTS.agents[0].maxTurns,
1677
1658
  systemPrompt: body.systemPrompt?.trim() ?? DEFAULTS.agents[0].systemPrompt,
@@ -2067,16 +2048,20 @@ function startWorker(signal) {
2067
2048
  * Matches an incoming message to the correct agent based on channel config.
2068
2049
  * Agents are checked in order; first match wins.
2069
2050
  * A wildcard `"*"` in the agent's channels array matches everything.
2051
+ * The optional `nameToId` map resolves human-readable channel names to Slack IDs.
2070
2052
  */
2071
- function matchAgent(channelId, agents) {
2072
- for (const agent of agents) for (const pattern of agent.channels) if (pattern === "*" || pattern === channelId) return agent;
2053
+ function matchAgent(channelId, agents, nameToId) {
2054
+ for (const agent of agents) for (const pattern of agent.channels) {
2055
+ if (pattern === "*" || pattern === channelId) return agent;
2056
+ if (nameToId?.get(pattern) === channelId) return agent;
2057
+ }
2073
2058
  }
2074
2059
  /**
2075
2060
  * Looks up or creates a session for the given Slack thread.
2076
2061
  * Returns the session row and whether it was newly created.
2077
2062
  */
2078
- function resolveSession(channelId, threadTs, config) {
2079
- const agent = matchAgent(channelId, config.agents);
2063
+ function resolveSession(channelId, threadTs, config, nameToId) {
2064
+ const agent = matchAgent(channelId, config.agents, nameToId);
2080
2065
  if (!agent) {
2081
2066
  logger.warn(`no agent matched for channel ${channelId}`);
2082
2067
  return null;
@@ -2112,21 +2097,10 @@ function expireStaleSessions(ttlHours) {
2112
2097
  return deleted;
2113
2098
  }
2114
2099
 
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
2100
  //#endregion
2127
2101
  //#region src/provider/claude.ts
2128
2102
  /** @fileoverview Claude Code CLI provider: arg building and stream-json parsing. */
2129
- function buildCommand$1(options) {
2103
+ function buildCommand(options) {
2130
2104
  const { message, sessionId, agent, mcpConfigPath } = options;
2131
2105
  const args = [
2132
2106
  "-p",
@@ -2162,7 +2136,7 @@ function buildCommand$1(options) {
2162
2136
  * Only extracts text, images, completion, and error events. Tool use/result
2163
2137
  * events are internal to the agent and not surfaced to the channel layer.
2164
2138
  */
2165
- function parseStreamLine$1(line) {
2139
+ function parseStreamLine(line) {
2166
2140
  const trimmed = line.trim();
2167
2141
  if (!trimmed || !trimmed.startsWith("{")) return [];
2168
2142
  let parsed;
@@ -2269,226 +2243,22 @@ function extractImages(parsed) {
2269
2243
  }];
2270
2244
  return [];
2271
2245
  }
2272
- const claude = {
2273
- buildCommand: buildCommand$1,
2274
- parseStreamLine: parseStreamLine$1
2275
- };
2276
2246
 
2277
2247
  //#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
- }
2248
+ //#region src/provider/stream.ts
2387
2249
  /**
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)
2250
+ * Splits a raw data chunk into lines, handling partial lines across chunks.
2251
+ * Returns [completeLines, remainingPartial].
2396
2252
  */
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
- }
2253
+ function splitLines(buffer, chunk) {
2254
+ const lines = (buffer + chunk).split("\n");
2255
+ return [lines, lines.pop() ?? ""];
2478
2256
  }
2479
- const opencode = {
2480
- buildCommand,
2481
- parseStreamLine
2482
- };
2483
2257
 
2484
2258
  //#endregion
2485
2259
  //#region src/provider/spawn.ts
2486
- /** @fileoverview Spawn a CLI provider as a subprocess. */
2260
+ /** @fileoverview Spawn the Claude CLI as a subprocess with streaming output. */
2487
2261
  const SIGKILL_GRACE_MS = 5e3;
2488
- const PROVIDERS = {
2489
- claude,
2490
- opencode
2491
- };
2492
2262
  /** Sends SIGTERM, then SIGKILL after a grace period if the process is still alive. */
2493
2263
  function gracefulKill(proc) {
2494
2264
  if (!proc.pid || proc.killed) return;
@@ -2517,12 +2287,11 @@ function cleanEnv() {
2517
2287
  return env;
2518
2288
  }
2519
2289
  /**
2520
- * Spawns a provider CLI with streaming output.
2290
+ * Spawns the Claude CLI with streaming output.
2521
2291
  * Returns an async iterable of parsed stream events and a kill handle.
2522
2292
  */
2523
- function spawnProvider(provider, options, signal) {
2524
- const mod = PROVIDERS[provider];
2525
- const cmd = mod.buildCommand(options);
2293
+ function spawnProvider(options, signal) {
2294
+ const cmd = buildCommand(options);
2526
2295
  const timeoutMs = options.agent.processTimeoutMs ?? options.globalTimeoutMs ?? 3e5;
2527
2296
  logger.debug(`spawning: ${cmd.binary} ${cmd.args.join(" ")} (timeout: ${timeoutMs}ms)`);
2528
2297
  const baseEnv = cleanEnv();
@@ -2552,10 +2321,10 @@ function spawnProvider(provider, options, signal) {
2552
2321
  async function* iterate() {
2553
2322
  let buffer = "";
2554
2323
  try {
2555
- yield* parseStdout(proc, mod, buffer, (b) => {
2324
+ yield* parseStdout(proc, buffer, (b) => {
2556
2325
  buffer = b;
2557
2326
  });
2558
- if (buffer.trim()) yield* mod.parseStreamLine(buffer);
2327
+ if (buffer.trim()) yield* parseStreamLine(buffer);
2559
2328
  if (timedOut) yield {
2560
2329
  type: "timeout",
2561
2330
  timeoutMs
@@ -2571,7 +2340,7 @@ function spawnProvider(provider, options, signal) {
2571
2340
  kill: () => gracefulKill(proc)
2572
2341
  };
2573
2342
  }
2574
- async function* parseStdout(proc, mod, buffer, setBuffer) {
2343
+ async function* parseStdout(proc, buffer, setBuffer) {
2575
2344
  const pending = [];
2576
2345
  let done = false;
2577
2346
  let error = null;
@@ -2600,7 +2369,7 @@ async function* parseStdout(proc, mod, buffer, setBuffer) {
2600
2369
  const [lines, remaining] = splitLines(buffer, data);
2601
2370
  buffer = remaining;
2602
2371
  setBuffer(buffer);
2603
- for (const line of lines) yield* mod.parseStreamLine(line);
2372
+ for (const line of lines) yield* parseStreamLine(line);
2604
2373
  }
2605
2374
  }
2606
2375
  if (error) yield {
@@ -2967,7 +2736,7 @@ function startScheduler(getConfig, signal, deps) {
2967
2736
  const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
2968
2737
  let browserLock = null;
2969
2738
  try {
2970
- const { events } = spawnProvider(agent.provider, {
2739
+ const { events } = spawnProvider({
2971
2740
  message: payload.prompt,
2972
2741
  agent,
2973
2742
  mcpConfigPath: config.browser.mcpConfigPath,
@@ -3146,7 +2915,7 @@ function createCoalescer(config, onDispatch) {
3146
2915
  //#endregion
3147
2916
  //#region src/channel/handler.ts
3148
2917
  const BROWSER_TOOL_PREFIX = "mcp__playwright__";
3149
- function createHandler(client, getConfig, signal, getTeamId) {
2918
+ function createHandler(client, getConfig, signal, getTeamId, getChannelNameToId) {
3150
2919
  const locks = /* @__PURE__ */ new Map();
3151
2920
  let activeSpawns = 0;
3152
2921
  function getLock(key) {
@@ -3292,7 +3061,7 @@ function createHandler(client, getConfig, signal, getTeamId) {
3292
3061
  activeSpawns++;
3293
3062
  let sessionUid;
3294
3063
  try {
3295
- const resolved = resolveSession(channelId, threadTs, config);
3064
+ const resolved = resolveSession(channelId, threadTs, config, getChannelNameToId?.());
3296
3065
  if (!resolved) {
3297
3066
  const blocks = noAgentBlocks(channelId);
3298
3067
  await client.postMessage(channelId, threadTs, "No agent configured for this channel.", { blocks });
@@ -3305,10 +3074,9 @@ function createHandler(client, getConfig, signal, getTeamId) {
3305
3074
  broadcastSSE("invalidate", { resource: "sessions" });
3306
3075
  const prompt = formatPrompt(messages);
3307
3076
  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, {
3077
+ const { events, kill } = spawnProvider({
3310
3078
  message: prompt,
3311
- sessionId: existingSessionId,
3079
+ sessionId: isNew ? void 0 : session.provider_session_id || void 0,
3312
3080
  agent,
3313
3081
  mcpConfigPath: config.browser.mcpConfigPath,
3314
3082
  timezone: config.timezone,
@@ -3653,7 +3421,7 @@ function createSlackChannel(getConfig, signal) {
3653
3421
  });
3654
3422
  const webClient = new WebClient(initConfig.slack.botToken);
3655
3423
  const channelClient = new SlackChannelClient(webClient);
3656
- const handler = createHandler(channelClient, getConfig, signal, () => teamId);
3424
+ const handler = createHandler(channelClient, getConfig, signal, () => teamId, () => channelNameToId);
3657
3425
  const coalescer = createCoalescer(initConfig.slack.coalesce, (dispatch) => {
3658
3426
  handler.handle(dispatch).catch(logDispatchError);
3659
3427
  });
@@ -3685,7 +3453,7 @@ function createSlackChannel(getConfig, signal) {
3685
3453
  const isDm = event["channel_type"] === "im";
3686
3454
  if (!isDm && config.slack.requireMention) return;
3687
3455
  const channelId = event["channel"];
3688
- if (!isChannelAllowed(channelId, config.slack.channels)) return;
3456
+ if (!isChannelAllowed(channelId, config.slack.channels, channelNameToId)) return;
3689
3457
  if (!isDm && isQuietHours(config.slack.quietHours)) return;
3690
3458
  const threadTs = event["thread_ts"] ?? event["ts"];
3691
3459
  const userId = event["user"] ?? "unknown";
@@ -3709,7 +3477,7 @@ function createSlackChannel(getConfig, signal) {
3709
3477
  if (isDuplicate(body)) return;
3710
3478
  const config = getConfig();
3711
3479
  const channelId = event["channel"];
3712
- if (!isChannelAllowed(channelId, config.slack.channels)) return;
3480
+ if (!isChannelAllowed(channelId, config.slack.channels, channelNameToId)) return;
3713
3481
  if (isQuietHours(config.slack.quietHours)) return;
3714
3482
  const messageTs = event["ts"];
3715
3483
  if (!messageTs) return;
@@ -3779,7 +3547,7 @@ function createSlackChannel(getConfig, signal) {
3779
3547
  if (!raw?.startsWith("retry:")) continue;
3780
3548
  const sessionUid = raw.slice(6);
3781
3549
  if (!sessionUid) continue;
3782
- await handleSessionRetry(sessionUid, channel, user ?? "unknown", getConfig(), handler);
3550
+ await handleSessionRetry(sessionUid, user ?? "unknown", handler);
3783
3551
  }
3784
3552
  }
3785
3553
  }
@@ -3804,15 +3572,18 @@ function createSlackChannel(getConfig, signal) {
3804
3572
  logger.info("reset reconnection back-off counter");
3805
3573
  }
3806
3574
  });
3575
+ /** Rebuilt on connect and config reload via resolveChannelNames(). */
3576
+ const channelNameToId = /* @__PURE__ */ new Map();
3807
3577
  async function resolveChannelNames() {
3578
+ const target = getConfig();
3808
3579
  const namesToResolve = /* @__PURE__ */ new Set();
3809
3580
  function collectNames(channels) {
3810
3581
  for (const ch of channels) if (ch !== "*" && !ch.startsWith("C") && !ch.startsWith("D") && !ch.startsWith("G")) namesToResolve.add(ch);
3811
3582
  }
3812
- collectNames(initConfig.slack.channels);
3813
- for (const agent of initConfig.agents) collectNames(agent.channels);
3583
+ collectNames(target.slack.channels);
3584
+ for (const agent of target.agents) collectNames(agent.channels);
3814
3585
  if (namesToResolve.size === 0) return;
3815
- const nameToId = /* @__PURE__ */ new Map();
3586
+ channelNameToId.clear();
3816
3587
  try {
3817
3588
  let cursor;
3818
3589
  do {
@@ -3822,26 +3593,17 @@ function createSlackChannel(getConfig, signal) {
3822
3593
  exclude_archived: true,
3823
3594
  cursor
3824
3595
  });
3825
- for (const ch of result.channels ?? []) if (ch.name && ch.id && namesToResolve.has(ch.name)) nameToId.set(ch.name, ch.id);
3596
+ for (const ch of result.channels ?? []) if (ch.name && ch.id && namesToResolve.has(ch.name)) {
3597
+ channelNameToId.set(ch.name, ch.id);
3598
+ logger.info(`resolved channel "${ch.name}" -> ${ch.id}`);
3599
+ }
3826
3600
  cursor = result.response_metadata?.next_cursor || void 0;
3827
3601
  } while (cursor);
3828
3602
  } catch (err) {
3829
3603
  logger.warn(`failed to resolve channel names: ${err instanceof Error ? err.message : String(err)}`);
3830
3604
  return;
3831
3605
  }
3832
- function resolveList(channels, label) {
3833
- return channels.map((ch) => {
3834
- const resolved = nameToId.get(ch);
3835
- if (resolved) {
3836
- logger.info(`${label}: resolved channel "${ch}" -> ${resolved}`);
3837
- return resolved;
3838
- }
3839
- if (namesToResolve.has(ch)) logger.warn(`${label}: channel "${ch}" not found in workspace`);
3840
- return ch;
3841
- });
3842
- }
3843
- initConfig.slack.channels = resolveList(initConfig.slack.channels, "slack");
3844
- for (const agent of initConfig.agents) agent.channels = resolveList(agent.channels, `agent "${agent.id}"`);
3606
+ for (const name of namesToResolve) if (!channelNameToId.has(name)) logger.warn(`channel "${name}" not found in workspace`);
3845
3607
  }
3846
3608
  async function start() {
3847
3609
  const authResult = await webClient.auth.test();
@@ -3875,7 +3637,8 @@ function createSlackChannel(getConfig, signal) {
3875
3637
  stop,
3876
3638
  isConnected,
3877
3639
  activeCount: () => handler.activeCount(),
3878
- postMessage
3640
+ postMessage,
3641
+ resolveChannelNames
3879
3642
  };
3880
3643
  }
3881
3644
  async function handleBirdCreateSubmission(view, webClient) {
@@ -3902,7 +3665,7 @@ async function handleBirdCreateSubmission(view, webClient) {
3902
3665
  logger.error(`bird_create submission error: ${err instanceof Error ? err.message : String(err)}`);
3903
3666
  }
3904
3667
  }
3905
- async function handleSessionRetry(sessionUid, channelId, userId, config, handler) {
3668
+ async function handleSessionRetry(sessionUid, userId, handler) {
3906
3669
  try {
3907
3670
  const { getSession, getLastInboundMessage } = await import("./db-CyQcrilg.mjs").then((n) => n.t);
3908
3671
  const session = getSession(sessionUid);
@@ -3929,9 +3692,13 @@ async function handleSessionRetry(sessionUid, channelId, userId, config, handler
3929
3692
  logger.error(`retry error: ${err instanceof Error ? err.message : String(err)}`);
3930
3693
  }
3931
3694
  }
3932
- function isChannelAllowed(channelId, channels) {
3695
+ function isChannelAllowed(channelId, channels, nameToId) {
3933
3696
  if (channels.includes("*")) return true;
3934
- return channels.includes(channelId);
3697
+ for (const ch of channels) {
3698
+ if (ch === channelId) return true;
3699
+ if (nameToId.get(ch) === channelId) return true;
3700
+ }
3701
+ return false;
3935
3702
  }
3936
3703
  function isQuietHours(quietHours) {
3937
3704
  if (!quietHours.enabled) return false;
@@ -4037,6 +3804,9 @@ async function startDaemon(options) {
4037
3804
  const config = loadConfig(configPath);
4038
3805
  ensureMcpConfig(config, configDir);
4039
3806
  activateLayers(config);
3807
+ if (slackHandle) slackHandle.resolveChannelNames().catch((err) => {
3808
+ logger.warn(`failed to resolve channel names on reload: ${err instanceof Error ? err.message : String(err)}`);
3809
+ });
4040
3810
  logger.info("config reloaded");
4041
3811
  };
4042
3812
  if (currentConfig.agents.length > 0) activateLayers(currentConfig);
@@ -4788,7 +4558,6 @@ function printConfig(configPath) {
4788
4558
  console.log(`\n${c("cyan", "agents:")}`);
4789
4559
  for (const a of config.agents) {
4790
4560
  console.log(` ${c("cyan", a.id)} (${a.name})`);
4791
- console.log(` ${c("dim", "provider:")} ${a.provider}`);
4792
4561
  console.log(` ${c("dim", "model:")} ${a.model}`);
4793
4562
  console.log(` ${c("dim", "max turns:")} ${a.maxTurns}`);
4794
4563
  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.14",
3
+ "version": "1.4.16",
4
4
  "description": "AI agent orchestrator with a real browser, a cron scheduler, and a web dashboard",
5
5
  "type": "module",
6
6
  "bin": {