@owloops/browserbird 1.2.4 → 1.2.6
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/{db-DAXqDqAz.mjs → db-C9ESgb0d.mjs} +12 -1
- package/dist/index.mjs +88 -44
- package/package.json +1 -1
|
@@ -290,6 +290,17 @@ const MIGRATIONS = [{
|
|
|
290
290
|
ON logs(level, source, created_at DESC);
|
|
291
291
|
`);
|
|
292
292
|
}
|
|
293
|
+
}, {
|
|
294
|
+
name: "browser lock",
|
|
295
|
+
up(d) {
|
|
296
|
+
d.exec(`
|
|
297
|
+
CREATE TABLE IF NOT EXISTS browser_lock (
|
|
298
|
+
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
299
|
+
holder TEXT NOT NULL,
|
|
300
|
+
acquired_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
301
|
+
);
|
|
302
|
+
`);
|
|
303
|
+
}
|
|
293
304
|
}];
|
|
294
305
|
let db = null;
|
|
295
306
|
function getSchemaVersion(d) {
|
|
@@ -1025,4 +1036,4 @@ var db_exports = /* @__PURE__ */ __exportAll({
|
|
|
1025
1036
|
});
|
|
1026
1037
|
|
|
1027
1038
|
//#endregion
|
|
1028
|
-
export { updateSessionProviderId as $, completeCronRun as A, listFlights as B, failStaleJobs as C, retryAllFailedJobs as D, listJobs as E, ensureSystemCronJob as F, deleteStaleSessions as G, updateCronJob as H, getCronJob as I, getSessionCount as J, findSession as K, getEnabledCronJobs as L, createCronRun as M, deleteCronJob as N, retryJob as O, deleteOldCronRuns as P, touchSession as Q, getFlightStats as R, failJob as S, hasPendingCronJob as T, updateCronJobStatus as U, setCronJobEnabled as V, createSession as W, getSessionTokenStats as X, getSessionMessages as Y, listSessions as Z, clearJobs as _, getSetting as a,
|
|
1039
|
+
export { updateSessionProviderId as $, completeCronRun as A, listFlights as B, failStaleJobs as C, retryAllFailedJobs as D, listJobs as E, ensureSystemCronJob as F, deleteStaleSessions as G, updateCronJob as H, getCronJob as I, getSessionCount as J, findSession as K, getEnabledCronJobs as L, createCronRun as M, deleteCronJob as N, retryJob as O, deleteOldCronRuns as P, touchSession as Q, getFlightStats as R, failJob as S, hasPendingCronJob as T, updateCronJobStatus as U, setCronJobEnabled as V, createSession as W, getSessionTokenStats as X, getSessionMessages as Y, listSessions as Z, clearJobs as _, getSetting as a, resolveByUid as at, deleteJob as b, getUserCount as c, getRecentLogs as d, shortUid as et, insertLog as f, claimNextJob as g, logMessage as h, createUser as i, optimizeDatabase as it, createCronJob as j, SYSTEM_CRON_PREFIX as k, setSetting as l, getMessageStats as m, resolveDbPath as n, getDb as nt, getUserByEmail as o, logger as ot, deleteOldMessages as p, getSession as q, resolveDbPathFromArgv as r, openDatabase as rt, getUserById as s, db_exports as t, closeDatabase as tt, deleteOldLogs as u, completeJob as v, getJobStats as w, deleteOldJobs as x, createJob as y, listCronJobs as z };
|
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
|
|
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";
|
|
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: ${"
|
|
126
|
-
buildInfo.push(`built: 2026-03-
|
|
125
|
+
buildInfo.push(`commit: ${"536f400d5d95ecfb7467a216235dcdd8e7d9573f".substring(0, 7)}`);
|
|
126
|
+
buildInfo.push(`built: 2026-03-05T15:03:39+04:00`);
|
|
127
127
|
const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
|
|
128
128
|
const VERSION = `browserbird ${pkg.version}${buildString}`;
|
|
129
129
|
const BIRD = [
|
|
@@ -248,8 +248,7 @@ function validateConfig(config) {
|
|
|
248
248
|
if (!Array.isArray(agent.channels) || agent.channels.length === 0) throw new Error(`agent "${agent.id}": "channels" must be a non-empty array`);
|
|
249
249
|
if (agent.fallbackModel && agent.fallbackModel === agent.model) throw new Error(`agent "${agent.id}": fallbackModel cannot be the same as model ("${agent.model}")`);
|
|
250
250
|
}
|
|
251
|
-
|
|
252
|
-
if (config.browser.enabled && browserMode === "persistent" && config.sessions.maxConcurrent > 1) logger.warn("persistent browser mode with maxConcurrent > 1 will cause lock contention; use \"isolated\" or set maxConcurrent to 1");
|
|
251
|
+
if (config.browser.enabled && getBrowserMode() === "persistent" && config.sessions.maxConcurrent > 1) logger.warn("persistent browser mode with maxConcurrent > 1 will cause lock contention; use \"isolated\" or set maxConcurrent to 1");
|
|
253
252
|
}
|
|
254
253
|
/**
|
|
255
254
|
* Reads and merges JSON config with DEFAULTS but skips env: resolution.
|
|
@@ -313,6 +312,10 @@ function ensureMcpConfig(config, configDir) {
|
|
|
313
312
|
config.browser.mcpConfigPath = mcpPath;
|
|
314
313
|
logger.info(`generated mcp config at ${mcpPath} (host: ${host})`);
|
|
315
314
|
}
|
|
315
|
+
/** Returns the browser mode from BROWSER_MODE env var, defaulting to 'persistent'. */
|
|
316
|
+
function getBrowserMode() {
|
|
317
|
+
return process.env["BROWSER_MODE"] ?? "persistent";
|
|
318
|
+
}
|
|
316
319
|
/** Atomic write: writes to a .tmp file then renames over the target. */
|
|
317
320
|
function saveConfig(configPath, data) {
|
|
318
321
|
const tmp = configPath + ".tmp";
|
|
@@ -908,7 +911,7 @@ function sanitizeConfig(config) {
|
|
|
908
911
|
birds: config.birds,
|
|
909
912
|
browser: {
|
|
910
913
|
enabled: config.browser.enabled,
|
|
911
|
-
mode:
|
|
914
|
+
mode: getBrowserMode(),
|
|
912
915
|
novncHost: config.browser.novncHost,
|
|
913
916
|
vncPort: config.browser.vncPort,
|
|
914
917
|
novncPort: config.browser.novncPort
|
|
@@ -2103,6 +2106,7 @@ function splitLines(buffer, chunk) {
|
|
|
2103
2106
|
|
|
2104
2107
|
//#endregion
|
|
2105
2108
|
//#region src/provider/claude.ts
|
|
2109
|
+
/** @fileoverview Claude Code CLI provider: arg building and stream-json parsing. */
|
|
2106
2110
|
function buildCommand$1(options) {
|
|
2107
2111
|
const { message, sessionId, agent, mcpConfigPath } = options;
|
|
2108
2112
|
const args = [
|
|
@@ -2123,10 +2127,12 @@ function buildCommand$1(options) {
|
|
|
2123
2127
|
args.push("--dangerously-skip-permissions");
|
|
2124
2128
|
const oauthToken = process.env["CLAUDE_CODE_OAUTH_TOKEN"];
|
|
2125
2129
|
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
2130
|
+
const env = oauthToken ? { CLAUDE_CODE_OAUTH_TOKEN: oauthToken } : apiKey ? { ANTHROPIC_API_KEY: apiKey } : {};
|
|
2131
|
+
env["CLAUDE_CONFIG_DIR"] = resolve(".browserbird", "claude");
|
|
2126
2132
|
return {
|
|
2127
2133
|
binary: "claude",
|
|
2128
2134
|
args,
|
|
2129
|
-
env
|
|
2135
|
+
env
|
|
2130
2136
|
};
|
|
2131
2137
|
}
|
|
2132
2138
|
/**
|
|
@@ -2861,6 +2867,29 @@ function truncate(text, maxLength) {
|
|
|
2861
2867
|
return text.slice(0, maxLength) + "...";
|
|
2862
2868
|
}
|
|
2863
2869
|
|
|
2870
|
+
//#endregion
|
|
2871
|
+
//#region src/browser/lock.ts
|
|
2872
|
+
/** @fileoverview SQLite-based browser lock for persistent browser mode. */
|
|
2873
|
+
/**
|
|
2874
|
+
* Attempts to acquire the browser lock for a given holder.
|
|
2875
|
+
* Uses a single conditional UPSERT: inserts if absent, overwrites if stale.
|
|
2876
|
+
* Returns false when an active (non-stale) lock is held by someone else.
|
|
2877
|
+
*/
|
|
2878
|
+
function acquireBrowserLock(holder, timeoutMs) {
|
|
2879
|
+
const timeoutSeconds = Math.ceil(timeoutMs / 1e3);
|
|
2880
|
+
const result = getDb().prepare(`INSERT INTO browser_lock (id, holder, acquired_at)
|
|
2881
|
+
VALUES (1, ?, datetime('now'))
|
|
2882
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2883
|
+
holder = excluded.holder,
|
|
2884
|
+
acquired_at = excluded.acquired_at
|
|
2885
|
+
WHERE acquired_at <= datetime('now', '-' || ? || ' seconds')`).run(holder, timeoutSeconds);
|
|
2886
|
+
return Number(result.changes) > 0;
|
|
2887
|
+
}
|
|
2888
|
+
/** Releases the browser lock only if the caller is the current holder. */
|
|
2889
|
+
function releaseBrowserLock(holder) {
|
|
2890
|
+
getDb().prepare("DELETE FROM browser_lock WHERE id = 1 AND holder = ?").run(holder);
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2864
2893
|
//#endregion
|
|
2865
2894
|
//#region src/cron/scheduler.ts
|
|
2866
2895
|
const TICK_INTERVAL_MS = 6e4;
|
|
@@ -2898,40 +2927,48 @@ function startScheduler(config, signal, deps) {
|
|
|
2898
2927
|
const payload = raw;
|
|
2899
2928
|
const agent = config.agents.find((a) => a.id === payload.agentId);
|
|
2900
2929
|
if (!agent) throw new Error(`agent "${payload.agentId}" not found`);
|
|
2901
|
-
const
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
mcpConfigPath: config.browser.mcpConfigPath
|
|
2905
|
-
}, signal);
|
|
2906
|
-
if (payload.channelId) logMessage(payload.channelId, null, agent.id, "in", payload.prompt);
|
|
2907
|
-
let result = "";
|
|
2908
|
-
let completion;
|
|
2909
|
-
for await (const event of events) if (event.type === "text_delta") result += redact(event.delta);
|
|
2910
|
-
else if (event.type === "completion") completion = event;
|
|
2911
|
-
else if (event.type === "rate_limit") logger.debug(`bird ${shortUid(payload.cronJobUid)} rate limit window resets ${(/* @__PURE__ */ new Date(event.resetsAt * 1e3)).toISOString()}`);
|
|
2912
|
-
else if (event.type === "error") {
|
|
2913
|
-
const safeError = redact(event.error);
|
|
2914
|
-
if (payload.channelId && deps?.postToSlack) {
|
|
2915
|
-
const blocks = sessionErrorBlocks(safeError, { birdName: agent.name });
|
|
2916
|
-
await deps.postToSlack(payload.channelId, `Bird failed: ${safeError}`, { blocks });
|
|
2917
|
-
}
|
|
2918
|
-
throw new Error(safeError);
|
|
2919
|
-
}
|
|
2920
|
-
if (completion && payload.channelId) logMessage(payload.channelId, null, agent.id, "out", result || void 0, completion.tokensIn, completion.tokensOut);
|
|
2921
|
-
if (!result) {
|
|
2922
|
-
logger.info(`bird ${shortUid(payload.cronJobUid)} completed (no output)`);
|
|
2923
|
-
return "completed (no output)";
|
|
2930
|
+
const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
|
|
2931
|
+
if (needsBrowserLock) {
|
|
2932
|
+
if (!acquireBrowserLock(payload.cronJobUid, config.sessions.processTimeoutMs)) throw new Error("browser is locked by another session");
|
|
2924
2933
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2934
|
+
try {
|
|
2935
|
+
const { events } = spawnProvider(agent.provider, {
|
|
2936
|
+
message: payload.prompt,
|
|
2937
|
+
agent,
|
|
2938
|
+
mcpConfigPath: config.browser.mcpConfigPath
|
|
2939
|
+
}, signal);
|
|
2940
|
+
if (payload.channelId) logMessage(payload.channelId, null, agent.id, "in", payload.prompt);
|
|
2941
|
+
let result = "";
|
|
2942
|
+
let completion;
|
|
2943
|
+
for await (const event of events) if (event.type === "text_delta") result += redact(event.delta);
|
|
2944
|
+
else if (event.type === "completion") completion = event;
|
|
2945
|
+
else if (event.type === "rate_limit") logger.debug(`bird ${shortUid(payload.cronJobUid)} rate limit window resets ${(/* @__PURE__ */ new Date(event.resetsAt * 1e3)).toISOString()}`);
|
|
2946
|
+
else if (event.type === "error") {
|
|
2947
|
+
const safeError = redact(event.error);
|
|
2948
|
+
if (payload.channelId && deps?.postToSlack) {
|
|
2949
|
+
const blocks = sessionErrorBlocks(safeError, { birdName: agent.name });
|
|
2950
|
+
await deps.postToSlack(payload.channelId, `Bird failed: ${safeError}`, { blocks });
|
|
2951
|
+
}
|
|
2952
|
+
throw new Error(safeError);
|
|
2953
|
+
}
|
|
2954
|
+
if (completion && payload.channelId) logMessage(payload.channelId, null, agent.id, "out", result || void 0, completion.tokensIn, completion.tokensOut);
|
|
2955
|
+
if (!result) {
|
|
2956
|
+
logger.info(`bird ${shortUid(payload.cronJobUid)} completed (no output)`);
|
|
2957
|
+
return "completed (no output)";
|
|
2931
2958
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2959
|
+
if (payload.channelId && deps?.postToSlack) {
|
|
2960
|
+
await deps.postToSlack(payload.channelId, result);
|
|
2961
|
+
if (completion) {
|
|
2962
|
+
const blocks = sessionCompleteBlocks(completion, void 0, agent.name);
|
|
2963
|
+
const fallback = `Bird ${agent.name} completed: ${completion.numTurns} turns`;
|
|
2964
|
+
await deps.postToSlack(payload.channelId, fallback, { blocks });
|
|
2965
|
+
}
|
|
2966
|
+
logger.info(`bird ${shortUid(payload.cronJobUid)} result posted to ${payload.channelId}`);
|
|
2967
|
+
} else logger.info(`bird ${shortUid(payload.cronJobUid)} completed (${result.length} chars)`);
|
|
2968
|
+
return result;
|
|
2969
|
+
} finally {
|
|
2970
|
+
if (needsBrowserLock) releaseBrowserLock(payload.cronJobUid);
|
|
2971
|
+
}
|
|
2935
2972
|
});
|
|
2936
2973
|
registerHandler("system_cron_run", (raw) => {
|
|
2937
2974
|
const payload = raw;
|
|
@@ -3162,6 +3199,12 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3162
3199
|
logger.warn("max concurrent sessions reached");
|
|
3163
3200
|
return;
|
|
3164
3201
|
}
|
|
3202
|
+
const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
|
|
3203
|
+
if (needsBrowserLock && !acquireBrowserLock(key, config.sessions.processTimeoutMs)) {
|
|
3204
|
+
await client.postMessage(channelId, threadTs, "The browser is in use by another session. Your message will be processed when it finishes.");
|
|
3205
|
+
lock.queue.push(dispatch);
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3165
3208
|
lock.processing = true;
|
|
3166
3209
|
activeSpawns++;
|
|
3167
3210
|
let sessionUid;
|
|
@@ -3202,6 +3245,7 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3202
3245
|
await client.postMessage(channelId, threadTs, `Something went wrong: ${errMsg}`, { blocks });
|
|
3203
3246
|
} catch {}
|
|
3204
3247
|
} finally {
|
|
3248
|
+
if (needsBrowserLock) releaseBrowserLock(key);
|
|
3205
3249
|
activeSpawns--;
|
|
3206
3250
|
lock.processing = false;
|
|
3207
3251
|
lock.killCurrent = null;
|
|
@@ -3737,7 +3781,7 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
|
|
|
3737
3781
|
logger.warn("bird_create submission missing required fields");
|
|
3738
3782
|
return;
|
|
3739
3783
|
}
|
|
3740
|
-
const { createCronJob, setCronJobEnabled } = await import("./db-
|
|
3784
|
+
const { createCronJob, setCronJobEnabled } = await import("./db-C9ESgb0d.mjs").then((n) => n.t);
|
|
3741
3785
|
const bird = createCronJob(name, schedule, prompt, channelId || void 0, "default", defaultTimezone);
|
|
3742
3786
|
if (enabledValue !== "enabled") setCronJobEnabled(bird.uid, false);
|
|
3743
3787
|
await webClient.chat.postMessage({
|
|
@@ -3751,7 +3795,7 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
|
|
|
3751
3795
|
}
|
|
3752
3796
|
async function handleSessionRetry(sessionUid, channelId, userId, config, handler) {
|
|
3753
3797
|
try {
|
|
3754
|
-
const { getSession, getLastInboundMessage } = await import("./db-
|
|
3798
|
+
const { getSession, getLastInboundMessage } = await import("./db-C9ESgb0d.mjs").then((n) => n.t);
|
|
3755
3799
|
const session = getSession(sessionUid);
|
|
3756
3800
|
if (!session) {
|
|
3757
3801
|
logger.warn(`retry: session ${sessionUid} not found`);
|
|
@@ -3857,7 +3901,7 @@ async function startDaemon(options) {
|
|
|
3857
3901
|
logger.success("browserbird orchestrator started");
|
|
3858
3902
|
logger.info(`agents: ${config.agents.map((a) => a.id).join(", ")}`);
|
|
3859
3903
|
logger.info(`max concurrent sessions: ${config.sessions.maxConcurrent}`);
|
|
3860
|
-
if (config.browser.enabled) logger.info(`browser mode: ${
|
|
3904
|
+
if (config.browser.enabled) logger.info(`browser mode: ${getBrowserMode()}`);
|
|
3861
3905
|
};
|
|
3862
3906
|
const onLaunch = async () => {
|
|
3863
3907
|
loadDotEnv(envPath);
|
|
@@ -4622,7 +4666,7 @@ function handleConfig(argv) {
|
|
|
4622
4666
|
allowPositionals: false,
|
|
4623
4667
|
strict: false
|
|
4624
4668
|
});
|
|
4625
|
-
printConfig(values.config);
|
|
4669
|
+
printConfig(values.config ?? process.env["BROWSERBIRD_CONFIG"]);
|
|
4626
4670
|
}
|
|
4627
4671
|
function printConfig(configPath) {
|
|
4628
4672
|
const config = loadRawConfig(configPath);
|
|
@@ -4652,7 +4696,7 @@ function printConfig(configPath) {
|
|
|
4652
4696
|
console.log(`\n${c("cyan", "browser:")}`);
|
|
4653
4697
|
console.log(` ${c("dim", "enabled:")} ${config.browser.enabled ? "yes" : "no"}`);
|
|
4654
4698
|
if (config.browser.enabled) {
|
|
4655
|
-
console.log(` ${c("dim", "mode:")} ${
|
|
4699
|
+
console.log(` ${c("dim", "mode:")} ${getBrowserMode()}`);
|
|
4656
4700
|
console.log(` ${c("dim", "vnc port:")} ${config.browser.vncPort}`);
|
|
4657
4701
|
console.log(` ${c("dim", "novnc port:")} ${config.browser.novncPort}`);
|
|
4658
4702
|
}
|