@owloops/browserbird 1.2.5 → 1.2.7

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.
@@ -209,7 +209,6 @@ const MIGRATIONS = [{
209
209
  target_channel_id TEXT,
210
210
  active_hours_start TEXT,
211
211
  active_hours_end TEXT,
212
- timezone TEXT DEFAULT 'UTC',
213
212
  enabled INTEGER NOT NULL DEFAULT 1,
214
213
  failure_count INTEGER NOT NULL DEFAULT 0,
215
214
  last_run TEXT,
@@ -536,11 +535,11 @@ function listCronJobs(page = 1, perPage = DEFAULT_PER_PAGE, includeSystem = fals
536
535
  function getEnabledCronJobs() {
537
536
  return getDb().prepare("SELECT * FROM cron_jobs WHERE enabled = 1 ORDER BY created_at").all();
538
537
  }
539
- function createCronJob(name, schedule, prompt, targetChannelId, agentId, timezone, activeHoursStart, activeHoursEnd) {
538
+ function createCronJob(name, schedule, prompt, targetChannelId, agentId, activeHoursStart, activeHoursEnd) {
540
539
  const uid = generateUid(UID_PREFIX.bird);
541
- return getDb().prepare(`INSERT INTO cron_jobs (uid, name, schedule, prompt, target_channel_id, agent_id, timezone, active_hours_start, active_hours_end)
542
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
543
- RETURNING *`).get(uid, name, schedule, prompt, targetChannelId ?? null, agentId ?? "default", timezone ?? "UTC", activeHoursStart ?? null, activeHoursEnd ?? null);
540
+ return getDb().prepare(`INSERT INTO cron_jobs (uid, name, schedule, prompt, target_channel_id, agent_id, active_hours_start, active_hours_end)
541
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
542
+ RETURNING *`).get(uid, name, schedule, prompt, targetChannelId ?? null, agentId ?? "default", activeHoursStart ?? null, activeHoursEnd ?? null);
544
543
  }
545
544
  function updateCronJobStatus(jobUid, status, failureCount) {
546
545
  getDb().prepare(`UPDATE cron_jobs SET last_run = datetime('now'), last_status = ?, failure_count = ? WHERE uid = ?`).run(status, failureCount, jobUid);
@@ -575,10 +574,6 @@ function updateCronJob(jobUid, fields) {
575
574
  sets.push("agent_id = ?");
576
575
  params.push(fields.agentId);
577
576
  }
578
- if (fields.timezone !== void 0) {
579
- sets.push("timezone = ?");
580
- params.push(fields.timezone);
581
- }
582
577
  if (fields.activeHoursStart !== void 0) {
583
578
  sets.push("active_hours_start = ?");
584
579
  params.push(fields.activeHoursStart);
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
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-C9ESgb0d.mjs";
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-Da2zSpkY.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { parseArgs, styleText } from "node:util";
4
4
  import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -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: ${"6c46374ad2f83536a49768a46c02b8a043b1304a".substring(0, 7)}`);
126
- buildInfo.push(`built: 2026-03-05T12:20:55+04:00`);
125
+ buildInfo.push(`commit: ${"febb9bb9f678ef12c57880b663dcc875288f1950".substring(0, 7)}`);
126
+ buildInfo.push(`built: 2026-03-06T10:34:46+04:00`);
127
127
  const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
128
128
  const VERSION = `browserbird ${pkg.version}${buildString}`;
129
129
  const BIRD = [
@@ -374,6 +374,37 @@ function loadDotEnv(envPath) {
374
374
  }
375
375
  }
376
376
 
377
+ //#endregion
378
+ //#region src/browser/lock.ts
379
+ /** @fileoverview SQLite-based browser lock for persistent browser mode. */
380
+ /**
381
+ * Attempts to acquire the browser lock for a given holder.
382
+ * Uses a single conditional UPSERT: inserts if absent, overwrites if stale.
383
+ * Returns false when an active (non-stale) lock is held by someone else.
384
+ */
385
+ function acquireBrowserLock(holder, timeoutMs) {
386
+ const timeoutSeconds = Math.ceil(timeoutMs / 1e3);
387
+ const result = getDb().prepare(`INSERT INTO browser_lock (id, holder, acquired_at)
388
+ VALUES (1, ?, datetime('now'))
389
+ ON CONFLICT(id) DO UPDATE SET
390
+ holder = excluded.holder,
391
+ acquired_at = excluded.acquired_at
392
+ WHERE acquired_at <= datetime('now', '-' || ? || ' seconds')`).run(holder, timeoutSeconds);
393
+ return Number(result.changes) > 0;
394
+ }
395
+ /** Refreshes the lock timestamp to prevent staleness during long operations. */
396
+ function refreshBrowserLock(holder) {
397
+ getDb().prepare(`UPDATE browser_lock SET acquired_at = datetime('now') WHERE id = 1 AND holder = ?`).run(holder);
398
+ }
399
+ /** Releases the browser lock only if the caller is the current holder. */
400
+ function releaseBrowserLock(holder) {
401
+ getDb().prepare("DELETE FROM browser_lock WHERE id = 1 AND holder = ?").run(holder);
402
+ }
403
+ /** Clears any browser lock unconditionally. Called on startup before any sessions exist. */
404
+ function clearBrowserLock() {
405
+ getDb().prepare("DELETE FROM browser_lock WHERE id = 1").run();
406
+ }
407
+
377
408
  //#endregion
378
409
  //#region src/server/auth.ts
379
410
  /** @fileoverview Password hashing, token signing, and verification using node:crypto. */
@@ -1353,7 +1384,7 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1353
1384
  for (const bird of getEnabledCronJobs()) {
1354
1385
  if (bird.name.startsWith(SYSTEM_CRON_PREFIX)) continue;
1355
1386
  try {
1356
- const next = nextCronMatch(parseCron(bird.schedule), now, bird.timezone);
1387
+ const next = nextCronMatch(parseCron(bird.schedule), now, getConfig().timezone);
1357
1388
  if (next) upcoming.push({
1358
1389
  uid: bird.uid,
1359
1390
  name: bird.name,
@@ -1417,7 +1448,7 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1417
1448
  jsonError(res, "\"prompt\" is required", 400);
1418
1449
  return;
1419
1450
  }
1420
- const job = createCronJob(deriveBirdName(body.prompt), body.schedule.trim(), body.prompt.trim(), body.channel?.trim() || void 0, body.agent?.trim() || void 0, body.timezone?.trim() || getConfig().timezone, body.activeHoursStart?.trim() || void 0, body.activeHoursEnd?.trim() || void 0);
1451
+ const job = createCronJob(deriveBirdName(body.prompt), body.schedule.trim(), body.prompt.trim(), body.channel?.trim() || void 0, body.agent?.trim() || void 0, body.activeHoursStart?.trim() || void 0, body.activeHoursEnd?.trim() || void 0);
1421
1452
  broadcastSSE("invalidate", { resource: "birds" });
1422
1453
  json(res, job, 201);
1423
1454
  }
@@ -1441,7 +1472,6 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
1441
1472
  name: body.prompt ? deriveBirdName(body.prompt) : void 0,
1442
1473
  targetChannelId: body.channel !== void 0 ? body.channel?.trim() || null : void 0,
1443
1474
  agentId: body.agent?.trim() || void 0,
1444
- timezone: body.timezone?.trim() || void 0,
1445
1475
  activeHoursStart: body.activeHoursStart !== void 0 ? body.activeHoursStart?.trim() || null : void 0,
1446
1476
  activeHoursEnd: body.activeHoursEnd !== void 0 ? body.activeHoursEnd?.trim() || null : void 0
1447
1477
  });
@@ -2106,6 +2136,7 @@ function splitLines(buffer, chunk) {
2106
2136
 
2107
2137
  //#endregion
2108
2138
  //#region src/provider/claude.ts
2139
+ /** @fileoverview Claude Code CLI provider: arg building and stream-json parsing. */
2109
2140
  function buildCommand$1(options) {
2110
2141
  const { message, sessionId, agent, mcpConfigPath } = options;
2111
2142
  const args = [
@@ -2120,16 +2151,21 @@ function buildCommand$1(options) {
2120
2151
  String(agent.maxTurns)
2121
2152
  ];
2122
2153
  if (sessionId) args.push("--resume", sessionId);
2123
- if (agent.systemPrompt) args.push("--append-system-prompt", agent.systemPrompt);
2154
+ const systemParts = [];
2155
+ if (agent.systemPrompt) systemParts.push(agent.systemPrompt);
2156
+ if (options.timezone) systemParts.push(`System timezone: ${options.timezone}. All cron expressions and scheduled times use this timezone.`);
2157
+ if (systemParts.length > 0) args.push("--append-system-prompt", systemParts.join(" "));
2124
2158
  if (mcpConfigPath) args.push("--mcp-config", mcpConfigPath);
2125
2159
  if (agent.fallbackModel) args.push("--fallback-model", agent.fallbackModel);
2126
2160
  args.push("--dangerously-skip-permissions");
2127
2161
  const oauthToken = process.env["CLAUDE_CODE_OAUTH_TOKEN"];
2128
2162
  const apiKey = process.env["ANTHROPIC_API_KEY"];
2163
+ const env = oauthToken ? { CLAUDE_CODE_OAUTH_TOKEN: oauthToken } : apiKey ? { ANTHROPIC_API_KEY: apiKey } : {};
2164
+ env["CLAUDE_CONFIG_DIR"] = resolve(".browserbird", "claude");
2129
2165
  return {
2130
2166
  binary: "claude",
2131
2167
  args,
2132
- env: oauthToken ? { CLAUDE_CODE_OAUTH_TOKEN: oauthToken } : apiKey ? { ANTHROPIC_API_KEY: apiKey } : {}
2168
+ env
2133
2169
  };
2134
2170
  }
2135
2171
  /**
@@ -2207,6 +2243,10 @@ function parseAssistantContent(parsed) {
2207
2243
  type: "text_delta",
2208
2244
  delta: b["text"]
2209
2245
  });
2246
+ else if (b["type"] === "tool_use" && typeof b["name"] === "string") events.push({
2247
+ type: "tool_use",
2248
+ toolName: b["name"]
2249
+ });
2210
2250
  }
2211
2251
  return events;
2212
2252
  }
@@ -2312,7 +2352,10 @@ function ensureWorkspace(mcpConfigPath, systemPrompt) {
2312
2352
  */
2313
2353
  function buildCommand(options) {
2314
2354
  const { message, sessionId, agent, mcpConfigPath } = options;
2315
- ensureWorkspace(mcpConfigPath, agent.systemPrompt);
2355
+ const systemParts = [];
2356
+ if (agent.systemPrompt) systemParts.push(agent.systemPrompt);
2357
+ if (options.timezone) systemParts.push(`System timezone: ${options.timezone}. All cron expressions and scheduled times use this timezone.`);
2358
+ ensureWorkspace(mcpConfigPath, systemParts.join(" ") || void 0);
2316
2359
  const args = [
2317
2360
  "run",
2318
2361
  "--format",
@@ -2425,6 +2468,14 @@ function parseStreamLine(line) {
2425
2468
  accumulators.delete(sid);
2426
2469
  return [completion];
2427
2470
  }
2471
+ case "tool_use": {
2472
+ const toolName = typeof part?.["tool"] === "string" && part["tool"] || "";
2473
+ if (toolName) return [{
2474
+ type: "tool_use",
2475
+ toolName
2476
+ }];
2477
+ return [];
2478
+ }
2428
2479
  case "error": {
2429
2480
  const err = parsed["error"];
2430
2481
  const data = err?.["data"];
@@ -2864,31 +2915,10 @@ function truncate(text, maxLength) {
2864
2915
  return text.slice(0, maxLength) + "...";
2865
2916
  }
2866
2917
 
2867
- //#endregion
2868
- //#region src/browser/lock.ts
2869
- /** @fileoverview SQLite-based browser lock for persistent browser mode. */
2870
- /**
2871
- * Attempts to acquire the browser lock for a given holder.
2872
- * Uses a single conditional UPSERT: inserts if absent, overwrites if stale.
2873
- * Returns false when an active (non-stale) lock is held by someone else.
2874
- */
2875
- function acquireBrowserLock(holder, timeoutMs) {
2876
- const timeoutSeconds = Math.ceil(timeoutMs / 1e3);
2877
- const result = getDb().prepare(`INSERT INTO browser_lock (id, holder, acquired_at)
2878
- VALUES (1, ?, datetime('now'))
2879
- ON CONFLICT(id) DO UPDATE SET
2880
- holder = excluded.holder,
2881
- acquired_at = excluded.acquired_at
2882
- WHERE acquired_at <= datetime('now', '-' || ? || ' seconds')`).run(holder, timeoutSeconds);
2883
- return Number(result.changes) > 0;
2884
- }
2885
- /** Releases the browser lock only if the caller is the current holder. */
2886
- function releaseBrowserLock(holder) {
2887
- getDb().prepare("DELETE FROM browser_lock WHERE id = 1 AND holder = ?").run(holder);
2888
- }
2889
-
2890
2918
  //#endregion
2891
2919
  //#region src/cron/scheduler.ts
2920
+ const BROWSER_TOOL_PREFIX$1 = "mcp__playwright__";
2921
+ const LOCK_HEARTBEAT_MS$1 = 3e4;
2892
2922
  const TICK_INTERVAL_MS = 6e4;
2893
2923
  const MAX_SCHEDULE_ERRORS = 3;
2894
2924
  const systemHandlers = /* @__PURE__ */ new Map();
@@ -2925,19 +2955,26 @@ function startScheduler(config, signal, deps) {
2925
2955
  const agent = config.agents.find((a) => a.id === payload.agentId);
2926
2956
  if (!agent) throw new Error(`agent "${payload.agentId}" not found`);
2927
2957
  const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
2928
- if (needsBrowserLock) {
2929
- if (!acquireBrowserLock(payload.cronJobUid, config.sessions.processTimeoutMs)) throw new Error("browser is locked by another session");
2930
- }
2958
+ let browserLockAcquired = false;
2959
+ let heartbeatTimer;
2931
2960
  try {
2932
2961
  const { events } = spawnProvider(agent.provider, {
2933
2962
  message: payload.prompt,
2934
2963
  agent,
2935
- mcpConfigPath: config.browser.mcpConfigPath
2964
+ mcpConfigPath: config.browser.mcpConfigPath,
2965
+ timezone: config.timezone
2936
2966
  }, signal);
2937
2967
  if (payload.channelId) logMessage(payload.channelId, null, agent.id, "in", payload.prompt);
2938
2968
  let result = "";
2939
2969
  let completion;
2940
- for await (const event of events) if (event.type === "text_delta") result += redact(event.delta);
2970
+ for await (const event of events) if (event.type === "tool_use") {
2971
+ if (needsBrowserLock && !browserLockAcquired && event.toolName.startsWith(BROWSER_TOOL_PREFIX$1)) {
2972
+ if (!acquireBrowserLock(payload.cronJobUid, config.sessions.processTimeoutMs)) throw new Error("browser is locked by another session");
2973
+ browserLockAcquired = true;
2974
+ heartbeatTimer = setInterval(() => refreshBrowserLock(payload.cronJobUid), LOCK_HEARTBEAT_MS$1);
2975
+ logger.info(`browser lock acquired lazily for bird ${shortUid(payload.cronJobUid)}`);
2976
+ }
2977
+ } else if (event.type === "text_delta") result += redact(event.delta);
2941
2978
  else if (event.type === "completion") completion = event;
2942
2979
  else if (event.type === "rate_limit") logger.debug(`bird ${shortUid(payload.cronJobUid)} rate limit window resets ${(/* @__PURE__ */ new Date(event.resetsAt * 1e3)).toISOString()}`);
2943
2980
  else if (event.type === "error") {
@@ -2964,7 +3001,8 @@ function startScheduler(config, signal, deps) {
2964
3001
  } else logger.info(`bird ${shortUid(payload.cronJobUid)} completed (${result.length} chars)`);
2965
3002
  return result;
2966
3003
  } finally {
2967
- if (needsBrowserLock) releaseBrowserLock(payload.cronJobUid);
3004
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
3005
+ if (browserLockAcquired) releaseBrowserLock(payload.cronJobUid);
2968
3006
  }
2969
3007
  });
2970
3008
  registerHandler("system_cron_run", (raw) => {
@@ -2997,8 +3035,8 @@ function startScheduler(config, signal, deps) {
2997
3035
  } else logger.warn(`bird ${shortUid(job.uid)}: invalid expression "${job.schedule}" (attempt ${count}/${MAX_SCHEDULE_ERRORS})`);
2998
3036
  continue;
2999
3037
  }
3000
- if (!matchesCron(schedule, now, job.timezone)) continue;
3001
- if (!isWithinActiveHours(job.active_hours_start, job.active_hours_end, now, job.timezone)) {
3038
+ if (!matchesCron(schedule, now, config.timezone)) continue;
3039
+ if (!isWithinActiveHours(job.active_hours_start, job.active_hours_end, now, config.timezone)) {
3002
3040
  logger.debug(`bird ${shortUid(job.uid)} skipped: outside active hours`);
3003
3041
  continue;
3004
3042
  }
@@ -3098,6 +3136,8 @@ function createCoalescer(config, onDispatch) {
3098
3136
 
3099
3137
  //#endregion
3100
3138
  //#region src/channel/handler.ts
3139
+ const BROWSER_TOOL_PREFIX = "mcp__playwright__";
3140
+ const LOCK_HEARTBEAT_MS = 3e4;
3101
3141
  function createHandler(client, config, signal, getTeamId) {
3102
3142
  const locks = /* @__PURE__ */ new Map();
3103
3143
  let activeSpawns = 0;
@@ -3145,6 +3185,9 @@ function createHandler(client, config, signal, getTeamId) {
3145
3185
  case "tool_images":
3146
3186
  await uploadImages(event.images, channelId, threadTs);
3147
3187
  break;
3188
+ case "tool_use":
3189
+ meta.onToolUse?.(event.toolName);
3190
+ break;
3148
3191
  case "completion":
3149
3192
  completion = event;
3150
3193
  logger.info(`completion [${event.subtype}]: ${event.tokensIn}in/${event.tokensOut}out, $${event.costUsd.toFixed(4)}, ${event.numTurns} turns`);
@@ -3197,11 +3240,8 @@ function createHandler(client, config, signal, getTeamId) {
3197
3240
  return;
3198
3241
  }
3199
3242
  const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
3200
- if (needsBrowserLock && !acquireBrowserLock(key, config.sessions.processTimeoutMs)) {
3201
- await client.postMessage(channelId, threadTs, "The browser is in use by another session. Your message will be processed when it finishes.");
3202
- lock.queue.push(dispatch);
3203
- return;
3204
- }
3243
+ let browserLockAcquired = false;
3244
+ let heartbeatTimer;
3205
3245
  lock.processing = true;
3206
3246
  activeSpawns++;
3207
3247
  let sessionUid;
@@ -3224,7 +3264,8 @@ function createHandler(client, config, signal, getTeamId) {
3224
3264
  message: prompt,
3225
3265
  sessionId: existingSessionId,
3226
3266
  agent,
3227
- mcpConfigPath: config.browser.mcpConfigPath
3267
+ mcpConfigPath: config.browser.mcpConfigPath,
3268
+ timezone: config.timezone
3228
3269
  }, signal);
3229
3270
  lock.killCurrent = kill;
3230
3271
  client.setStatus?.(channelId, threadTs, "is thinking...").catch(() => {});
@@ -3232,7 +3273,22 @@ function createHandler(client, config, signal, getTeamId) {
3232
3273
  const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
3233
3274
  client.setTitle?.(channelId, threadTs, title).catch(() => {});
3234
3275
  }
3235
- await streamToChannel(events, channelId, threadTs, session.uid, getTeamId(), userId, { birdName: agent.name });
3276
+ const onToolUse = (toolName) => {
3277
+ if (!needsBrowserLock || browserLockAcquired) return;
3278
+ if (!toolName.startsWith(BROWSER_TOOL_PREFIX)) return;
3279
+ if (acquireBrowserLock(key, config.sessions.processTimeoutMs)) {
3280
+ browserLockAcquired = true;
3281
+ heartbeatTimer = setInterval(() => refreshBrowserLock(key), LOCK_HEARTBEAT_MS);
3282
+ logger.info(`browser lock acquired lazily for ${key} (tool: ${toolName})`);
3283
+ } else {
3284
+ logger.warn(`browser lock unavailable for ${key} (tool: ${toolName})`);
3285
+ client.postMessage(channelId, threadTs, "The browser is in use by another session.").catch(() => {});
3286
+ }
3287
+ };
3288
+ await streamToChannel(events, channelId, threadTs, session.uid, getTeamId(), userId, {
3289
+ birdName: agent.name,
3290
+ onToolUse
3291
+ });
3236
3292
  } catch (err) {
3237
3293
  const errMsg = err instanceof Error ? err.message : String(err);
3238
3294
  logger.error(`handler error: ${errMsg}`);
@@ -3242,7 +3298,8 @@ function createHandler(client, config, signal, getTeamId) {
3242
3298
  await client.postMessage(channelId, threadTs, `Something went wrong: ${errMsg}`, { blocks });
3243
3299
  } catch {}
3244
3300
  } finally {
3245
- if (needsBrowserLock) releaseBrowserLock(key);
3301
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
3302
+ if (browserLockAcquired) releaseBrowserLock(key);
3246
3303
  activeSpawns--;
3247
3304
  lock.processing = false;
3248
3305
  lock.killCurrent = null;
@@ -3653,7 +3710,7 @@ function createSlackChannel(config, signal) {
3653
3710
  const interactionType = body["type"];
3654
3711
  if (interactionType === "view_submission") {
3655
3712
  const view = body["view"];
3656
- if (view?.["callback_id"] === "bird_create") await handleBirdCreateSubmission(view, webClient, config.timezone);
3713
+ if (view?.["callback_id"] === "bird_create") await handleBirdCreateSubmission(view, webClient);
3657
3714
  }
3658
3715
  if (interactionType === "block_actions") {
3659
3716
  const actionsArr = body["actions"];
@@ -3766,7 +3823,7 @@ function createSlackChannel(config, signal) {
3766
3823
  postMessage
3767
3824
  };
3768
3825
  }
3769
- async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
3826
+ async function handleBirdCreateSubmission(view, webClient) {
3770
3827
  try {
3771
3828
  const stateValues = view["state"]?.["values"] ?? {};
3772
3829
  const name = stateValues["bird_name"]?.["name_input"]?.["value"] ?? "";
@@ -3778,8 +3835,8 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
3778
3835
  logger.warn("bird_create submission missing required fields");
3779
3836
  return;
3780
3837
  }
3781
- const { createCronJob, setCronJobEnabled } = await import("./db-C9ESgb0d.mjs").then((n) => n.t);
3782
- const bird = createCronJob(name, schedule, prompt, channelId || void 0, "default", defaultTimezone);
3838
+ const { createCronJob, setCronJobEnabled } = await import("./db-Da2zSpkY.mjs").then((n) => n.t);
3839
+ const bird = createCronJob(name, schedule, prompt, channelId || void 0, "default");
3783
3840
  if (enabledValue !== "enabled") setCronJobEnabled(bird.uid, false);
3784
3841
  await webClient.chat.postMessage({
3785
3842
  channel: channelId || "general",
@@ -3792,7 +3849,7 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
3792
3849
  }
3793
3850
  async function handleSessionRetry(sessionUid, channelId, userId, config, handler) {
3794
3851
  try {
3795
- const { getSession, getLastInboundMessage } = await import("./db-C9ESgb0d.mjs").then((n) => n.t);
3852
+ const { getSession, getLastInboundMessage } = await import("./db-Da2zSpkY.mjs").then((n) => n.t);
3796
3853
  const session = getSession(sessionUid);
3797
3854
  if (!session) {
3798
3855
  logger.warn(`retry: session ${sessionUid} not found`);
@@ -3869,6 +3926,7 @@ async function startDaemon(options) {
3869
3926
  const configDir = dirname(configPath);
3870
3927
  const envPath = resolve(configDir, ".env");
3871
3928
  openDatabase(resolveDbPath(options.flags.db));
3929
+ clearBrowserLock();
3872
3930
  startWorker(controller.signal);
3873
3931
  loadDotEnv(envPath);
3874
3932
  let currentConfig = loadConfig(configPath);
@@ -4099,7 +4157,6 @@ ${c("dim", "options:")}
4099
4157
  ${c("yellow", "--agent")} <id> target agent id
4100
4158
  ${c("yellow", "--schedule")} <expr> cron schedule expression
4101
4159
  ${c("yellow", "--prompt")} <text> prompt text
4102
- ${c("yellow", "--timezone")} <tz> IANA timezone (default: UTC)
4103
4160
  ${c("yellow", "--active-hours")} <range> restrict runs to a time window (e.g. "09:00-17:00")
4104
4161
  ${c("yellow", "--limit")} <n> number of flights to show (default: 10)
4105
4162
  ${c("yellow", "--json")} output as JSON (with list, flights)
@@ -4147,7 +4204,6 @@ function handleBirds(argv) {
4147
4204
  agent: { type: "string" },
4148
4205
  schedule: { type: "string" },
4149
4206
  prompt: { type: "string" },
4150
- timezone: { type: "string" },
4151
4207
  "active-hours": { type: "string" },
4152
4208
  limit: { type: "string" },
4153
4209
  json: {
@@ -4221,7 +4277,7 @@ function handleBirds(argv) {
4221
4277
  activeStart = parsed.start;
4222
4278
  activeEnd = parsed.end;
4223
4279
  }
4224
- const job = createCronJob(deriveBirdName(prompt), schedule, prompt, values.channel, values.agent, values.timezone, activeStart, activeEnd);
4280
+ const job = createCronJob(deriveBirdName(prompt), schedule, prompt, values.channel, values.agent, activeStart, activeEnd);
4225
4281
  logger.success(`bird ${shortUid(job.uid)} created: "${schedule}"`);
4226
4282
  process.stderr.write(c("dim", ` hint: run 'browserbird birds fly ${shortUid(job.uid)}' to trigger it now`) + "\n");
4227
4283
  break;
@@ -4229,7 +4285,7 @@ function handleBirds(argv) {
4229
4285
  case "edit": {
4230
4286
  const uidPrefix = positionals[0];
4231
4287
  if (!uidPrefix) {
4232
- logger.error("usage: browserbird birds edit <uid> [--schedule <expr>] [--prompt <text>] [--channel <id>] [--agent <id>] [--timezone <tz>] [--active-hours <range>]");
4288
+ logger.error("usage: browserbird birds edit <uid> [--schedule <expr>] [--prompt <text>] [--channel <id>] [--agent <id>] [--active-hours <range>]");
4233
4289
  process.exitCode = 1;
4234
4290
  return;
4235
4291
  }
@@ -4239,7 +4295,6 @@ function handleBirds(argv) {
4239
4295
  const agent = values.agent;
4240
4296
  const schedule = values.schedule;
4241
4297
  const prompt = values.prompt;
4242
- const timezone = values.timezone;
4243
4298
  const editActiveHoursRaw = values["active-hours"];
4244
4299
  let editActiveStart;
4245
4300
  let editActiveEnd;
@@ -4256,8 +4311,8 @@ function handleBirds(argv) {
4256
4311
  editActiveStart = parsed.start;
4257
4312
  editActiveEnd = parsed.end;
4258
4313
  }
4259
- if (!schedule && !prompt && !channel && !agent && !timezone && editActiveStart === void 0) {
4260
- logger.error("provide at least one of: --schedule, --prompt, --channel, --agent, --timezone, --active-hours");
4314
+ if (!schedule && !prompt && !channel && !agent && editActiveStart === void 0) {
4315
+ logger.error("provide at least one of: --schedule, --prompt, --channel, --agent, --active-hours");
4261
4316
  process.exitCode = 1;
4262
4317
  return;
4263
4318
  }
@@ -4267,7 +4322,6 @@ function handleBirds(argv) {
4267
4322
  name: prompt ? deriveBirdName(prompt) : void 0,
4268
4323
  targetChannelId: channel !== void 0 ? channel || null : void 0,
4269
4324
  agentId: agent,
4270
- timezone,
4271
4325
  activeHoursStart: editActiveStart,
4272
4326
  activeHoursEnd: editActiveEnd
4273
4327
  })) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owloops/browserbird",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Self-hosted AI agent for Slack with a real browser, a scheduler, and a web dashboard",
5
5
  "type": "module",
6
6
  "bin": {