@owloops/browserbird 1.2.10 → 1.3.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # BrowserBird
6
6
 
7
- Self-hosted AI agent for Slack with a real browser, a scheduler, and a web dashboard.
7
+ Self-hosted AI agent orchestrator with a real browser, a cron scheduler, and a web dashboard.
8
8
 
9
9
  [![License: FSL-1.1-MIT](https://img.shields.io/badge/license-FSL--1.1--MIT-blue?style=flat-square)](LICENSE)
10
10
  [![npm version](https://img.shields.io/npm/v/@owloops/browserbird?style=flat-square)](https://www.npmjs.com/package/@owloops/browserbird)
@@ -25,7 +25,7 @@ Self-hosted AI agent for Slack with a real browser, a scheduler, and a web dashb
25
25
  </tr>
26
26
  </table>
27
27
 
28
- Talk to an AI agent in Slack threads. It can browse the web with a real Chromium browser you can watch live through VNC, run scheduled tasks on a cron, and keep persistent sessions across conversations. BrowserBird is the orchestration layer; the agent CLI ([claude](https://docs.anthropic.com/en/docs/claude-code/overview), [opencode](https://github.com/anomalyco/opencode)) handles reasoning, memory, tools, and sub-agents.
28
+ Schedule AI agents to run on a cron, browse the web with a real Chromium browser you can watch live through VNC, and manage everything from a web dashboard or the CLI. Optionally connect Slack for conversational threads and slash commands. BrowserBird is the orchestration layer; the agent CLI ([claude](https://docs.anthropic.com/en/docs/claude-code/overview), [opencode](https://github.com/anomalyco/opencode)) handles reasoning, memory, tools, and sub-agents.
29
29
 
30
30
  Built by [Owloops](https://github.com/Owloops), building browser automation tools since 2020.
31
31
 
@@ -46,7 +46,7 @@ These are starting points. Every bird has a full AI agent (Claude Code or openco
46
46
 
47
47
  ## Installation
48
48
 
49
- On first run, open the web UI and complete the onboarding wizard. It walks through Slack tokens, agent config, and API keys.
49
+ On first run, open the web UI and complete the onboarding wizard. It walks through agent config, API keys, and optional integrations (Slack, browser).
50
50
 
51
51
  ### Docker (recommended)
52
52
 
@@ -63,7 +63,7 @@ The browser runs in **persistent** mode by default: logins and cookies are saved
63
63
 
64
64
  [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/browserbird-1)
65
65
 
66
- ## Slack
66
+ ## Slack (Optional)
67
67
 
68
68
  [![Create Slack App](https://img.shields.io/badge/Slack-Create_App-4A154B?style=for-the-badge&logo=slack&logoColor=white)](https://api.slack.com/apps?new_app=1&manifest_json=%7B%22display_information%22%3A%7B%22name%22%3A%22BrowserBird%22%2C%22description%22%3A%22A%20self-hosted%20AI%20assistant%20in%20Slack%2C%20with%20a%20real%20browser%20and%20a%20scheduler.%22%2C%22background_color%22%3A%22%231a1a2e%22%7D%2C%22features%22%3A%7B%22assistant_view%22%3A%7B%22assistant_description%22%3A%22A%20self-hosted%20AI%20assistant%20in%20Slack%2C%20with%20a%20real%20browser%20and%20a%20scheduler.%22%7D%2C%22app_home%22%3A%7B%22home_tab_enabled%22%3Atrue%2C%22messages_tab_enabled%22%3Atrue%2C%22messages_tab_read_only_enabled%22%3Afalse%7D%2C%22bot_user%22%3A%7B%22display_name%22%3A%22BrowserBird%22%2C%22always_online%22%3Atrue%7D%2C%22slash_commands%22%3A%5B%7B%22command%22%3A%22%2Fbird%22%2C%22description%22%3A%22Manage%20BrowserBird%20birds%22%2C%22usage_hint%22%3A%22list%20%7C%20fly%20%7C%20logs%20%7C%20enable%20%7C%20disable%20%7C%20create%20%7C%20status%22%2C%22should_escape%22%3Afalse%7D%5D%7D%2C%22oauth_config%22%3A%7B%22scopes%22%3A%7B%22bot%22%3A%5B%22app_mentions%3Aread%22%2C%22assistant%3Awrite%22%2C%22channels%3Ahistory%22%2C%22channels%3Aread%22%2C%22chat%3Awrite%22%2C%22files%3Aread%22%2C%22files%3Awrite%22%2C%22groups%3Ahistory%22%2C%22groups%3Aread%22%2C%22im%3Ahistory%22%2C%22im%3Aread%22%2C%22im%3Awrite%22%2C%22mpim%3Ahistory%22%2C%22mpim%3Aread%22%2C%22reactions%3Aread%22%2C%22reactions%3Awrite%22%2C%22users%3Aread%22%2C%22commands%22%5D%7D%7D%2C%22settings%22%3A%7B%22event_subscriptions%22%3A%7B%22bot_events%22%3A%5B%22app_mention%22%2C%22assistant_thread_context_changed%22%2C%22assistant_thread_started%22%2C%22message.channels%22%2C%22message.groups%22%2C%22message.im%22%2C%22message.mpim%22%5D%7D%2C%22interactivity%22%3A%7B%22is_enabled%22%3Atrue%7D%2C%22org_deploy_enabled%22%3Afalse%2C%22socket_mode_enabled%22%3Atrue%2C%22token_rotation_enabled%22%3Afalse%7D%7D)
69
69
 
@@ -114,7 +114,7 @@ The top-level `timezone` field (IANA format, default `"UTC"`) is used for cron s
114
114
  }
115
115
  ```
116
116
 
117
- - `botToken`, `appToken`: Required. Bot user OAuth token and app-level token for Socket Mode
117
+ - `botToken`, `appToken`: Optional. Bot user OAuth token and app-level token for Socket Mode. Required only for Slack integration
118
118
  - `requireMention`: Only respond in channels when `@mentioned`; DMs always respond
119
119
  - `coalesce.debounceMs`: Wait N ms after last message before dispatching (groups rapid messages)
120
120
  - `coalesce.bypassDms`: Skip debouncing for DMs
@@ -247,8 +247,8 @@ Authentication is handled via the web UI. On first visit, you create an account.
247
247
 
248
248
  | Variable | Description |
249
249
  | ------------------------- | ------------------------------------------------------------------------------------------------ |
250
- | `SLACK_BOT_TOKEN` | Bot user OAuth token |
251
- | `SLACK_APP_TOKEN` | App-level token for Socket Mode |
250
+ | `SLACK_BOT_TOKEN` | Bot user OAuth token (optional, for Slack integration) |
251
+ | `SLACK_APP_TOKEN` | App-level token for Socket Mode (optional, for Slack integration) |
252
252
  | `ANTHROPIC_API_KEY` | Anthropic API key (pay-per-token). Used by both claude and opencode providers |
253
253
  | `CLAUDE_CODE_OAUTH_TOKEN` | OAuth token for claude provider only (uses your Claude Pro/Max subscription) |
254
254
  | `BROWSER_MODE` | `persistent` (default) or `isolated`. Requires container restart |
@@ -295,6 +295,16 @@ options:
295
295
  run 'browserbird <command> --help' for command-specific options.
296
296
  ```
297
297
 
298
+ ### Standalone CLI Workflow
299
+
300
+ BrowserBird works without Slack. Create a bird, trigger it, and check results from the terminal:
301
+
302
+ ```bash
303
+ browserbird birds add --schedule "0 9 * * *" --prompt "Check Hacker News for AI news and summarize"
304
+ browserbird birds fly <name>
305
+ browserbird birds flights <name>
306
+ ```
307
+
298
308
  ## Web UI
299
309
 
300
310
  Runs at `http://localhost:18800` by default.
package/dist/index.mjs CHANGED
@@ -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: ${"bddda226a98294fa5e272a0f50b3e8f52dab935f".substring(0, 7)}`);
126
- buildInfo.push(`built: 2026-03-09T14:46:24+04:00`);
125
+ buildInfo.push(`commit: ${"b06564aa22755ababe5972e05230d453baa632e9".substring(0, 7)}`);
126
+ buildInfo.push(`built: 2026-03-09T18:52:55+04:00`);
127
127
  const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
128
128
  const VERSION = `browserbird ${pkg.version}${buildString}`;
129
129
  const BIRD = [
@@ -267,31 +267,6 @@ function loadRawConfig(configPath) {
267
267
  return deepMerge(DEFAULTS, parsed);
268
268
  }
269
269
  /**
270
- * Checks whether both Slack tokens are present and resolvable.
271
- * Literal strings must be non-empty; `"env:VAR"` references must point to a set env var.
272
- */
273
- function hasSlackTokens(configPath) {
274
- const filePath = configPath ?? resolve("browserbird.json");
275
- if (!existsSync(filePath)) return false;
276
- let parsed;
277
- try {
278
- parsed = JSON.parse(readFileSync(filePath, "utf-8"));
279
- } catch {
280
- return false;
281
- }
282
- const slack = parsed["slack"];
283
- if (!slack) return false;
284
- return isTokenResolvable(slack["botToken"]) && isTokenResolvable(slack["appToken"]);
285
- }
286
- function isTokenResolvable(value) {
287
- if (typeof value !== "string" || !value) return false;
288
- if (value.startsWith("env:")) {
289
- const envKey = value.slice(4);
290
- return !!process.env[envKey];
291
- }
292
- return true;
293
- }
294
- /**
295
270
  * When the browser is enabled but no explicit mcpConfigPath is set, generates
296
271
  * a Playwright MCP config file using novncHost as the SSE server hostname.
297
272
  * Writes to `<configDir>/mcp.json` and mutates `config.browser.mcpConfigPath`.
@@ -925,6 +900,7 @@ function sanitizeConfig(config) {
925
900
  model: a.model,
926
901
  fallbackModel: a.fallbackModel ?? null,
927
902
  maxTurns: a.maxTurns,
903
+ processTimeoutMs: a.processTimeoutMs ?? null,
928
904
  systemPrompt: a.systemPrompt,
929
905
  channels: a.channels
930
906
  })),
@@ -1231,7 +1207,7 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1231
1207
  broadcastSSE("invalidate", { resource: "secrets" });
1232
1208
  json(res, {
1233
1209
  success: true,
1234
- requiresRestart: true
1210
+ requiresRestart: false
1235
1211
  });
1236
1212
  } catch (err) {
1237
1213
  jsonError(res, `Failed to save Slack tokens: ${err instanceof Error ? err.message : String(err)}`, 500);
@@ -2534,7 +2510,7 @@ function cleanEnv() {
2534
2510
  function spawnProvider(provider, options, signal) {
2535
2511
  const mod = PROVIDERS[provider];
2536
2512
  const cmd = mod.buildCommand(options);
2537
- const timeoutMs = options.agent.processTimeoutMs ?? 3e5;
2513
+ const timeoutMs = options.agent.processTimeoutMs ?? options.globalTimeoutMs ?? 3e5;
2538
2514
  logger.debug(`spawning: ${cmd.binary} ${cmd.args.join(" ")} (timeout: ${timeoutMs}ms)`);
2539
2515
  const baseEnv = cleanEnv();
2540
2516
  if (cmd.env) for (const [k, v] of Object.entries(cmd.env)) if (v === "") delete baseEnv[k];
@@ -2982,7 +2958,8 @@ function startScheduler(config, signal, deps) {
2982
2958
  message: payload.prompt,
2983
2959
  agent,
2984
2960
  mcpConfigPath: config.browser.mcpConfigPath,
2985
- timezone: config.timezone
2961
+ timezone: config.timezone,
2962
+ globalTimeoutMs: config.sessions.processTimeoutMs
2986
2963
  }, signal);
2987
2964
  if (payload.channelId) logMessage(payload.channelId, null, agent.id, "in", payload.prompt);
2988
2965
  let result = "";
@@ -3298,7 +3275,8 @@ function createHandler(client, config, signal, getTeamId) {
3298
3275
  sessionId: existingSessionId,
3299
3276
  agent,
3300
3277
  mcpConfigPath: config.browser.mcpConfigPath,
3301
- timezone: config.timezone
3278
+ timezone: config.timezone,
3279
+ globalTimeoutMs: config.sessions.processTimeoutMs
3302
3280
  }, signal);
3303
3281
  lock.killCurrent = kill;
3304
3282
  client.setStatus?.(channelId, threadTs, "is thinking...").catch(() => {});
@@ -3942,14 +3920,6 @@ function setupShutdown() {
3942
3920
  process.on("SIGINT", () => shutdown("SIGINT"));
3943
3921
  process.on("SIGTERM", () => shutdown("SIGTERM"));
3944
3922
  }
3945
- const stubDeps = {
3946
- slackConnected: () => false,
3947
- activeProcessCount: () => 0,
3948
- serviceHealth: () => ({
3949
- agent: { available: false },
3950
- browser: { connected: false }
3951
- })
3952
- };
3953
3923
  async function startDaemon(options) {
3954
3924
  setupShutdown();
3955
3925
  logger.setMode("daemon");
@@ -3962,56 +3932,68 @@ async function startDaemon(options) {
3962
3932
  clearBrowserLock();
3963
3933
  startWorker(controller.signal);
3964
3934
  loadDotEnv(envPath);
3965
- let currentConfig = loadRawConfig(configPath);
3935
+ let currentConfig;
3966
3936
  let slackHandle = null;
3967
- let setupMode = true;
3937
+ let schedulerStarted = false;
3938
+ let slackStarted = false;
3939
+ let healthStarted = false;
3940
+ try {
3941
+ currentConfig = loadConfig(configPath);
3942
+ ensureMcpConfig(currentConfig, configDir);
3943
+ } catch (err) {
3944
+ logger.warn(`config validation failed: ${err instanceof Error ? err.message : String(err)}`);
3945
+ currentConfig = loadRawConfig(configPath);
3946
+ }
3968
3947
  const getConfig = () => currentConfig;
3969
- const getDeps = () => {
3970
- if (setupMode) return stubDeps;
3971
- return {
3972
- slackConnected: () => slackHandle?.isConnected() ?? false,
3973
- activeProcessCount: () => slackHandle?.activeCount() ?? 0,
3974
- serviceHealth: () => getServiceHealth(currentConfig)
3975
- };
3976
- };
3977
- const startFull = (config) => {
3948
+ const getDeps = () => ({
3949
+ slackConnected: () => slackHandle?.isConnected() ?? false,
3950
+ activeProcessCount: () => slackHandle?.activeCount() ?? 0,
3951
+ serviceHealth: () => getServiceHealth(currentConfig)
3952
+ });
3953
+ let activated = false;
3954
+ const activateLayers = (config) => {
3978
3955
  currentConfig = config;
3979
- setupMode = false;
3980
- logger.info("connecting to slack...");
3981
- slackHandle = createSlackChannel(config, controller.signal);
3982
- logger.info("starting scheduler...");
3983
- startScheduler(config, controller.signal, { postToSlack: (channel, text, opts) => slackHandle.postMessage(channel, text, opts) });
3984
- slackHandle.start().catch((err) => {
3985
- logger.error(`slack failed to start: ${err instanceof Error ? err.message : String(err)}`);
3986
- });
3987
- startHealthChecks(getConfig, controller.signal);
3988
- logger.success("browserbird orchestrator started");
3989
- logger.info(`agents: ${config.agents.map((a) => a.id).join(", ")}`);
3990
- logger.info(`max concurrent sessions: ${config.sessions.maxConcurrent}`);
3991
- if (config.browser.enabled) logger.info(`browser mode: ${getBrowserMode()}`);
3956
+ if (!schedulerStarted && config.agents.length > 0) {
3957
+ logger.info("starting scheduler...");
3958
+ startScheduler(config, controller.signal, { postToSlack: (channel, text, opts) => slackHandle ? slackHandle.postMessage(channel, text, opts) : Promise.resolve() });
3959
+ schedulerStarted = true;
3960
+ }
3961
+ if (!healthStarted) {
3962
+ startHealthChecks(getConfig, controller.signal);
3963
+ healthStarted = true;
3964
+ }
3965
+ if (!slackStarted && config.slack.botToken && config.slack.appToken) {
3966
+ logger.info("connecting to slack...");
3967
+ slackHandle = createSlackChannel(config, controller.signal);
3968
+ slackHandle.start().catch((err) => {
3969
+ logger.error(`slack failed to start: ${err instanceof Error ? err.message : String(err)}`);
3970
+ });
3971
+ slackStarted = true;
3972
+ }
3973
+ if (!activated) {
3974
+ logger.success("browserbird orchestrator started");
3975
+ logger.info(`agents: ${config.agents.map((a) => a.id).join(", ") || "none"}`);
3976
+ if (!slackStarted) logger.info("slack: not configured");
3977
+ logger.info(`max concurrent sessions: ${config.sessions.maxConcurrent}`);
3978
+ if (config.browser.enabled) logger.info(`browser mode: ${getBrowserMode()}`);
3979
+ activated = true;
3980
+ }
3992
3981
  };
3993
3982
  const onLaunch = async () => {
3994
3983
  loadDotEnv(envPath);
3995
3984
  const config = loadConfig(configPath);
3996
3985
  ensureMcpConfig(config, configDir);
3997
- if (!config.slack.botToken || !config.slack.appToken) throw new Error("Slack tokens are required to launch");
3998
- startFull(config);
3986
+ activateLayers(config);
3999
3987
  };
4000
3988
  const reloadConfig = () => {
4001
3989
  loadDotEnv(envPath);
4002
- currentConfig = loadConfig(configPath);
4003
- ensureMcpConfig(currentConfig, configDir);
3990
+ const config = loadConfig(configPath);
3991
+ ensureMcpConfig(config, configDir);
3992
+ activateLayers(config);
4004
3993
  logger.info("config reloaded");
4005
3994
  };
4006
- if (hasSlackTokens(configPath)) {
4007
- currentConfig = loadConfig(configPath);
4008
- ensureMcpConfig(currentConfig, configDir);
4009
- setSetting("onboarding_completed", "true");
4010
- startFull(currentConfig);
4011
- } else {
4012
- setSetting("onboarding_completed", "");
4013
- logger.info("starting in setup mode (onboarding not completed)");
4014
- }
3995
+ if (currentConfig.agents.length > 0) activateLayers(currentConfig);
3996
+ else logger.info("no agents configured");
4015
3997
  let webServer = null;
4016
3998
  const webConfig = getConfig();
4017
3999
  if (webConfig.web.enabled) {
@@ -4024,7 +4006,6 @@ async function startDaemon(options) {
4024
4006
  });
4025
4007
  await webServer.start();
4026
4008
  }
4027
- if (setupMode) logger.info("waiting for onboarding to complete via web UI");
4028
4009
  await new Promise((resolvePromise) => {
4029
4010
  controller.signal.addEventListener("abort", () => {
4030
4011
  resolvePromise();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@owloops/browserbird",
3
- "version": "1.2.10",
4
- "description": "Self-hosted AI agent for Slack with a real browser, a scheduler, and a web dashboard",
3
+ "version": "1.3.0",
4
+ "description": "AI agent orchestrator with a real browser, a cron scheduler, and a web dashboard",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "browserbird": "./bin/browserbird"
@@ -28,12 +28,14 @@
28
28
  "prepublishOnly": "npm run lint && npm run format:check && npm run typecheck && npm run build && cd web && npm ci && npm run build"
29
29
  },
30
30
  "keywords": [
31
- "slack",
32
- "claude",
33
- "claude-code",
34
- "orchestrator",
35
31
  "cli",
36
- "bot"
32
+ "ai-agent",
33
+ "orchestrator",
34
+ "browser-automation",
35
+ "scheduler",
36
+ "cron",
37
+ "claude",
38
+ "playwright"
37
39
  ],
38
40
  "author": "Owloops",
39
41
  "license": "FSL-1.1-MIT",