@owloops/browserbird 1.2.11 → 1.3.1

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: ${"a5c784646077ea93f574e3557dbcb4126981c868".substring(0, 7)}`);
126
- buildInfo.push(`built: 2026-03-09T15:23:20+04:00`);
125
+ buildInfo.push(`commit: ${"7a38fde28b18642c0e6dbfe7c7221777ed34033b".substring(0, 7)}`);
126
+ buildInfo.push(`built: 2026-03-10T11:08:01+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`.
@@ -1232,7 +1207,7 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1232
1207
  broadcastSSE("invalidate", { resource: "secrets" });
1233
1208
  json(res, {
1234
1209
  success: true,
1235
- requiresRestart: true
1210
+ requiresRestart: false
1236
1211
  });
1237
1212
  } catch (err) {
1238
1213
  jsonError(res, `Failed to save Slack tokens: ${err instanceof Error ? err.message : String(err)}`, 500);
@@ -3945,14 +3920,6 @@ function setupShutdown() {
3945
3920
  process.on("SIGINT", () => shutdown("SIGINT"));
3946
3921
  process.on("SIGTERM", () => shutdown("SIGTERM"));
3947
3922
  }
3948
- const stubDeps = {
3949
- slackConnected: () => false,
3950
- activeProcessCount: () => 0,
3951
- serviceHealth: () => ({
3952
- agent: { available: false },
3953
- browser: { connected: false }
3954
- })
3955
- };
3956
3923
  async function startDaemon(options) {
3957
3924
  setupShutdown();
3958
3925
  logger.setMode("daemon");
@@ -3965,56 +3932,68 @@ async function startDaemon(options) {
3965
3932
  clearBrowserLock();
3966
3933
  startWorker(controller.signal);
3967
3934
  loadDotEnv(envPath);
3968
- let currentConfig = loadRawConfig(configPath);
3935
+ let currentConfig;
3969
3936
  let slackHandle = null;
3970
- 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
+ }
3971
3947
  const getConfig = () => currentConfig;
3972
- const getDeps = () => {
3973
- if (setupMode) return stubDeps;
3974
- return {
3975
- slackConnected: () => slackHandle?.isConnected() ?? false,
3976
- activeProcessCount: () => slackHandle?.activeCount() ?? 0,
3977
- serviceHealth: () => getServiceHealth(currentConfig)
3978
- };
3979
- };
3980
- 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) => {
3981
3955
  currentConfig = config;
3982
- setupMode = false;
3983
- logger.info("connecting to slack...");
3984
- slackHandle = createSlackChannel(config, controller.signal);
3985
- logger.info("starting scheduler...");
3986
- startScheduler(config, controller.signal, { postToSlack: (channel, text, opts) => slackHandle.postMessage(channel, text, opts) });
3987
- slackHandle.start().catch((err) => {
3988
- logger.error(`slack failed to start: ${err instanceof Error ? err.message : String(err)}`);
3989
- });
3990
- startHealthChecks(getConfig, controller.signal);
3991
- logger.success("browserbird orchestrator started");
3992
- logger.info(`agents: ${config.agents.map((a) => a.id).join(", ")}`);
3993
- logger.info(`max concurrent sessions: ${config.sessions.maxConcurrent}`);
3994
- 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
+ }
3995
3981
  };
3996
3982
  const onLaunch = async () => {
3997
3983
  loadDotEnv(envPath);
3998
3984
  const config = loadConfig(configPath);
3999
3985
  ensureMcpConfig(config, configDir);
4000
- if (!config.slack.botToken || !config.slack.appToken) throw new Error("Slack tokens are required to launch");
4001
- startFull(config);
3986
+ activateLayers(config);
4002
3987
  };
4003
3988
  const reloadConfig = () => {
4004
3989
  loadDotEnv(envPath);
4005
- currentConfig = loadConfig(configPath);
4006
- ensureMcpConfig(currentConfig, configDir);
3990
+ const config = loadConfig(configPath);
3991
+ ensureMcpConfig(config, configDir);
3992
+ activateLayers(config);
4007
3993
  logger.info("config reloaded");
4008
3994
  };
4009
- if (hasSlackTokens(configPath)) {
4010
- currentConfig = loadConfig(configPath);
4011
- ensureMcpConfig(currentConfig, configDir);
4012
- setSetting("onboarding_completed", "true");
4013
- startFull(currentConfig);
4014
- } else {
4015
- setSetting("onboarding_completed", "");
4016
- logger.info("starting in setup mode (onboarding not completed)");
4017
- }
3995
+ if (currentConfig.agents.length > 0) activateLayers(currentConfig);
3996
+ else logger.info("no agents configured");
4018
3997
  let webServer = null;
4019
3998
  const webConfig = getConfig();
4020
3999
  if (webConfig.web.enabled) {
@@ -4027,7 +4006,6 @@ async function startDaemon(options) {
4027
4006
  });
4028
4007
  await webServer.start();
4029
4008
  }
4030
- if (setupMode) logger.info("waiting for onboarding to complete via web UI");
4031
4009
  await new Promise((resolvePromise) => {
4032
4010
  controller.signal.addEventListener("abort", () => {
4033
4011
  resolvePromise();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@owloops/browserbird",
3
- "version": "1.2.11",
4
- "description": "Self-hosted AI agent for Slack with a real browser, a scheduler, and a web dashboard",
3
+ "version": "1.3.1",
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",