openmausbot 0.1.60 → 0.1.62

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.
Files changed (31) hide show
  1. package/README.md +71 -8
  2. package/dist/assets/index-BSI5e_fY.js +264 -0
  3. package/dist/assets/{index-BIzf_lw1.js → index-DTeYtYoC.js} +1 -1
  4. package/dist/index.html +1 -1
  5. package/dist-server/container-mcp.js +16 -1
  6. package/dist-server/drivers/agents-proxy.js +8 -4
  7. package/dist-server/index.js +463 -134
  8. package/dist-server/openmausbot.js +33585 -14506
  9. package/dist-server/pair-cli.js +33585 -14506
  10. package/dist-server/server/browser-bundle-release.js +56 -0
  11. package/dist-server/server/browser-engine-release.js +13 -5
  12. package/dist-server/server/browser-engine.js +114 -15
  13. package/dist-server/server/cli-api-setup.js +144 -0
  14. package/dist-server/server/cli-phone-setup.js +130 -0
  15. package/dist-server/server/cli-prompts.js +185 -0
  16. package/dist-server/server/cli-setup.js +303 -0
  17. package/dist-server/server/cli.js +375 -70
  18. package/dist-server/server/config.js +25 -1
  19. package/dist-server/server/default-model-selection.js +21 -0
  20. package/dist-server/server/drivers/agents-proxy.js +13 -4
  21. package/dist-server/server/drivers/claude.js +26 -1
  22. package/dist-server/server/drivers/openai-compat.js +4 -2
  23. package/dist-server/server/index.js +117 -80
  24. package/dist-server/server/recall-disclosure.js +45 -0
  25. package/dist-server/server/request-auth.js +6 -0
  26. package/dist-server/server/screen-frame-gate.js +36 -1
  27. package/dist-server/server/screen-frame-source.js +57 -0
  28. package/dist-server/server/sessions.js +52 -4
  29. package/dist-server/vps-container-mcp.js +16 -1
  30. package/package.json +1 -1
  31. package/dist/assets/index-Tha1umgl.js +0 -264
@@ -6,9 +6,9 @@ var __export = (target, all2) => {
6
6
 
7
7
  // server/index.ts
8
8
  import { createHash as createHash25, randomBytes as randomBytes7, randomUUID as randomUUID17, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
9
- import { existsSync as existsSync25, readFileSync as readFileSync38, rmSync as rmSync6, unlinkSync as unlinkSync9 } from "node:fs";
9
+ import { existsSync as existsSync25, readFileSync as readFileSync38, rmSync as rmSync7, unlinkSync as unlinkSync9 } from "node:fs";
10
10
  import { createServer as createServer3 } from "node:http";
11
- import { extname as extname5, join as join49 } from "node:path";
11
+ import { extname as extname5, join as join50 } from "node:path";
12
12
 
13
13
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
14
14
  var external_exports = {};
@@ -15651,6 +15651,15 @@ import { readFileSync as readFileSync3, mkdirSync, existsSync as existsSync2, re
15651
15651
  import { homedir as homedir2 } from "node:os";
15652
15652
  import { join as join3 } from "node:path";
15653
15653
 
15654
+ // server/contracts.ts
15655
+ var EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
15656
+ function isEffortLevel(value) {
15657
+ return typeof value === "string" && EFFORT_LEVELS.includes(value);
15658
+ }
15659
+ var eventCounter = 0;
15660
+ var newEventId = () => `ev-${Date.now().toString(36)}-${(eventCounter++).toString(36)}`;
15661
+ var newId = () => crypto.randomUUID();
15662
+
15654
15663
  // server/mcp-registry.ts
15655
15664
  var MAX_MCP_SERVERS = 20;
15656
15665
  var MAX_ARGS = 64;
@@ -15933,7 +15942,19 @@ var instanceConfigSchema = external_exports.object({
15933
15942
  config: external_exports.json().optional()
15934
15943
  });
15935
15944
  var instanceConfigMapSchema = external_exports.record(external_exports.string(), instanceConfigSchema);
15945
+ var defaultModelSelectionSchema = external_exports.object({
15946
+ instanceId: external_exports.string().trim().min(1),
15947
+ model: external_exports.string().trim().min(1),
15948
+ effort: external_exports.enum(EFFORT_LEVELS).optional()
15949
+ });
15936
15950
  var appConfigSchema = external_exports.object({
15951
+ defaultModelSelection: defaultModelSelectionSchema.optional(),
15952
+ /** CLI-only launch preferences. Never enable remote access implicitly. */
15953
+ cliStartup: external_exports.object({
15954
+ access: external_exports.enum(["local", "tunnel", "tailscale", "public-url"]),
15955
+ publicUrl: external_exports.string().url().optional(),
15956
+ phone: external_exports.enum(["ios", "android"]).optional()
15957
+ }).optional(),
15937
15958
  xai: external_exports.object({ key: optionalText, url: optionalText }).optional(),
15938
15959
  /** `model` seeds the default selection; `provider` pins an OpenRouter
15939
15960
  * upstream (e.g. "fireworks"). Both are non-secret and optional. */
@@ -15970,7 +15991,7 @@ var appConfigSchema = external_exports.object({
15970
15991
  var storedAppConfigSchema = appConfigSchema.extend({
15971
15992
  browserProfiles: storedBrowserProfilesSchema.optional()
15972
15993
  });
15973
- var appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true });
15994
+ var appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true });
15974
15995
  var jsonObjectSchema = external_exports.record(external_exports.string(), external_exports.json());
15975
15996
  function browserProfilePartitionId(profile) {
15976
15997
  return profile.partitionId ?? profile.id;
@@ -16170,6 +16191,10 @@ function saveConfig(patch) {
16170
16191
  }
16171
16192
  if (checkedPatch.vps !== void 0) disk.vps = normalizeVpsConfig(checkedPatch.vps);
16172
16193
  if (checkedPatch.language !== void 0) disk.language = checkedPatch.language;
16194
+ if (checkedPatch.defaultModelSelection !== void 0) {
16195
+ disk.defaultModelSelection = checkedPatch.defaultModelSelection;
16196
+ }
16197
+ if (checkedPatch.cliStartup !== void 0) disk.cliStartup = checkedPatch.cliStartup;
16173
16198
  if (checkedPatch.mcpServers !== void 0) {
16174
16199
  disk.mcpServers = jsonObjectSchema.parse(checkedPatch.mcpServers);
16175
16200
  }
@@ -16289,6 +16314,7 @@ function instanceConfigs(cfg2) {
16289
16314
  const current2 = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
16290
16315
  const merged = { ...current2 };
16291
16316
  for (const [k, v] of Object.entries(defaults)) {
16317
+ if (k === "provider" && typeof merged[k] === "string") continue;
16292
16318
  if (typeof merged[k] !== "string" || !merged[k].trim()) merged[k] = v;
16293
16319
  }
16294
16320
  entry.config = merged;
@@ -28129,15 +28155,6 @@ function buildNotification(kind, bot, threadId, detail, extra) {
28129
28155
  return notification;
28130
28156
  }
28131
28157
 
28132
- // server/contracts.ts
28133
- var EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
28134
- function isEffortLevel(value) {
28135
- return typeof value === "string" && EFFORT_LEVELS.includes(value);
28136
- }
28137
- var eventCounter = 0;
28138
- var newEventId = () => `ev-${Date.now().toString(36)}-${(eventCounter++).toString(36)}`;
28139
- var newId = () => crypto.randomUUID();
28140
-
28141
28158
  // server/drivers/retry.ts
28142
28159
  var RETRY_MAX_ATTEMPTS = 3;
28143
28160
  var BACKOFF_BASE_MS = [1e3, 3e3, 8e3];
@@ -30848,6 +30865,10 @@ function claudeSignedIn(cli, env, run3 = execCli) {
30848
30865
  });
30849
30866
  });
30850
30867
  }
30868
+ function claudeAuthFailure(frame, text6) {
30869
+ if (frame.is_api_error_message !== true && typeof frame.error !== "string") return false;
30870
+ return frame.error === "authentication_failed" || classifyError({ text: text6 }).reason === "auth";
30871
+ }
30851
30872
  function claudeEnvironment(model, source = process.env) {
30852
30873
  const env = { ...source, PATH: augmentedPath(), NPM_CONFIG_LOGLEVEL: "error" };
30853
30874
  delete env.CLAUDECODE;
@@ -31562,6 +31583,11 @@ var ClaudeDriver = {
31562
31583
  case "assistant": {
31563
31584
  const msg = o.message ?? {};
31564
31585
  const text6 = firstText(msg.content);
31586
+ if (claudeAuthFailure(o, text6)) {
31587
+ if (session.turn) session.turn.authFailed = true;
31588
+ emit({ ...base2(threadId, currentTurnId()), type: "runtime.error", message: text6, setup: true });
31589
+ break;
31590
+ }
31565
31591
  if (text6.trim()) {
31566
31592
  if (!session.turn?.sawStreamDelta) {
31567
31593
  emit({ ...base2(threadId, currentTurnId()), type: "content.delta", streamKind: "assistant_text", delta: text6 });
@@ -31596,7 +31622,7 @@ var ClaudeDriver = {
31596
31622
  if (o.origin?.kind === "task-notification") break;
31597
31623
  settle(
31598
31624
  o.is_error !== true,
31599
- o.stop_reason ?? o.terminal_reason ?? null,
31625
+ session.turn?.authFailed ? "auth_required" : o.stop_reason ?? o.terminal_reason ?? null,
31600
31626
  o.total_cost_usd ?? null,
31601
31627
  o.usage ? {
31602
31628
  input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
@@ -41630,7 +41656,9 @@ function decodeConfig5(raw) {
41630
41656
  apiKeyEnv: typeof config2.apiKeyEnv === "string" && config2.apiKeyEnv ? config2.apiKeyEnv : "OPENAI_COMPAT_API_KEY",
41631
41657
  key: typeof config2.key === "string" && config2.key ? config2.key : void 0,
41632
41658
  model: typeof config2.model === "string" && config2.model ? config2.model : process.env.OPENAI_COMPAT_MODEL || void 0,
41633
- provider: typeof config2.provider === "string" && config2.provider ? config2.provider : process.env.OPENAI_COMPAT_PROVIDER || void 0
41659
+ // An explicit empty override disables inherited routing for an isolated
41660
+ // connection (CLI setup uses this). Absent still inherits the global pin.
41661
+ provider: typeof config2.provider === "string" ? config2.provider || void 0 : process.env.OPENAI_COMPAT_PROVIDER || void 0
41634
41662
  };
41635
41663
  }
41636
41664
  var OpenAICompatDriver = {
@@ -44165,6 +44193,26 @@ function mirrorActivity(bus2, from, channel, name2, ok, notify2) {
44165
44193
  markMirrored(bus2, channel, notify2);
44166
44194
  }
44167
44195
 
44196
+ // server/recall-disclosure.ts
44197
+ var announced = /* @__PURE__ */ new Map();
44198
+ function claimRecallCrossings(roomThreadId, sources) {
44199
+ const seen = announced.get(roomThreadId) ?? /* @__PURE__ */ new Set();
44200
+ const threadIds = [];
44201
+ let count = 0;
44202
+ for (const threadId of sources) {
44203
+ if (seen.has(threadId)) continue;
44204
+ if (!threadIds.includes(threadId)) threadIds.push(threadId);
44205
+ count += 1;
44206
+ }
44207
+ if (!threadIds.length) return { threadIds: [], count: 0 };
44208
+ for (const threadId of threadIds) seen.add(threadId);
44209
+ announced.set(roomThreadId, seen);
44210
+ return { threadIds, count };
44211
+ }
44212
+ function recallCrossingLabel(botName, count) {
44213
+ return `${botName} recalled ${count} message${count === 1 ? "" : "s"} from its private chat with you`;
44214
+ }
44215
+
44168
44216
  // server/replies.ts
44169
44217
  var MAX_REPLY_EXCERPT = 900;
44170
44218
  function replyExcerpt(text6, limit = MAX_REPLY_EXCERPT) {
@@ -45207,6 +45255,22 @@ var ProviderRegistry = class {
45207
45255
  }
45208
45256
  };
45209
45257
 
45258
+ // server/default-model-selection.ts
45259
+ function selectDefaultModelSelection(instances, preferred) {
45260
+ if (preferred) {
45261
+ const instance = instances.find((candidate) => candidate.instanceId === preferred.instanceId);
45262
+ if (instance?.snapshot.state !== "available" || instance.snapshot.authenticated === false || !(instance.models.default === preferred.model || instance.models.options.some((model) => model.id === preferred.model))) {
45263
+ return { instanceId: "", model: "" };
45264
+ }
45265
+ const selection = { ...preferred };
45266
+ if (selection.effort && !instance.capabilities?.effortLevels?.includes(selection.effort)) delete selection.effort;
45267
+ return selection;
45268
+ }
45269
+ const available = instances.filter((instance) => instance.snapshot.state === "available");
45270
+ const pick2 = available.find((instance) => instance.driverKind === "claudeAgent") ?? available[0];
45271
+ return { instanceId: pick2?.instanceId ?? "", model: pick2?.models.default ?? "" };
45272
+ }
45273
+
45210
45274
  // server/room-post-budget.ts
45211
45275
  var SENDER_WINDOW_MS = 6e4;
45212
45276
  var SENDER_MAX = 10;
@@ -49596,8 +49660,12 @@ var CalendarCallManager = class {
49596
49660
  // server/browser-engine.ts
49597
49661
  import { spawn as spawn4 } from "node:child_process";
49598
49662
  import { createHash as createHash16, randomBytes as randomBytes4, randomUUID as randomUUID12 } from "node:crypto";
49599
- import { chmodSync as chmodSync5, existsSync as existsSync18, mkdirSync as mkdirSync20, readFileSync as readFileSync31, renameSync as renameSync5, statSync as statSync8, writeFileSync as writeFileSync15 } from "node:fs";
49600
- import { delimiter as delimiter4, join as join40, resolve as resolve10 } from "node:path";
49663
+ import { chmodSync as chmodSync5, existsSync as existsSync18, mkdirSync as mkdirSync20, readFileSync as readFileSync31, renameSync as renameSync5, rmSync as rmSync6, statSync as statSync8, writeFileSync as writeFileSync15 } from "node:fs";
49664
+ import { tmpdir as tmpdir6 } from "node:os";
49665
+ import { delimiter as delimiter4, join as join41, resolve as resolve10 } from "node:path";
49666
+
49667
+ // server/browser-bundle-release.ts
49668
+ import { join as join40 } from "node:path";
49601
49669
 
49602
49670
  // server/browser-engine-release.ts
49603
49671
  var AGENT_BROWSER_VERSION = "0.36.0";
@@ -49628,17 +49696,77 @@ var RELEASES2 = /* @__PURE__ */ new Map([
49628
49696
  ],
49629
49697
  [
49630
49698
  "win32-x64",
49631
- { target: "win32-x64", asset: "agent-browser-win32-x64.exe", sha256: "412ff72737a109e93f5304b0ff76c988fb6f1f451d0fc7e010577922bcc20ff3", bytes: 13837312 }
49699
+ {
49700
+ target: "win32-x64",
49701
+ version: "0.36.0-omb.1",
49702
+ asset: "agent-browser-win32-x64-0.36.0-omb.1.exe",
49703
+ url: "https://github.com/milind-soni/OpenMausBot/releases/download/browser-engine-v0.36.0-omb.1/agent-browser-win32-x64-0.36.0-omb.1.exe",
49704
+ sha256: "33bee834f6a6072ec8688b0914726e0262874d758f69f27e8baf7eaac6b5ed15",
49705
+ bytes: 13806080
49706
+ }
49632
49707
  ]
49633
49708
  ]);
49634
49709
  function agentBrowserReleaseUrl(asset) {
49635
- return `https://github.com/vercel-labs/agent-browser/releases/download/v${AGENT_BROWSER_VERSION}/${asset.asset}`;
49710
+ return asset.url ?? `https://github.com/vercel-labs/agent-browser/releases/download/v${AGENT_BROWSER_VERSION}/${asset.asset}`;
49711
+ }
49712
+ function agentBrowserReleaseVersion(asset) {
49713
+ return asset?.version ?? AGENT_BROWSER_VERSION;
49636
49714
  }
49637
49715
  function resolveAgentBrowserReleaseAsset(platform = process.platform, arch = process.arch, musl = false) {
49638
49716
  const key3 = platform === "linux" && musl ? `linux-musl-${arch}` : `${platform}-${arch}`;
49639
49717
  return RELEASES2.get(key3) ?? null;
49640
49718
  }
49641
49719
 
49720
+ // server/browser-bundle-release.ts
49721
+ var CHROME_VERSION = "152.0.7977.82";
49722
+ var CHROME_ASSETS = {
49723
+ "darwin-arm64": { platform: "mac-arm64", bytes: 97760996, sha256: "1615f063c894aa824fd55c89f8f05e9904f63cce0c95d0cbce7394d77884fba5", executableSha256: "ac18537fb2bc2b72b0440893b6ea5d4ef3bacc6220b1e9a1693c73f8b1045fbe" },
49724
+ "darwin-x64": { platform: "mac-x64", bytes: 102895824, sha256: "c903707292c6aaed7c6e572c0bb5e6645454e45d9e0e6db27e876b18e9165f31", executableSha256: "2faefd55d4e47764956e31e338d362a1e794037fa772a426ffd510f122f33f19" },
49725
+ "linux-x64": { platform: "linux64", bytes: 119454769, sha256: "0ca12ea26b502a83e32db334a17883c315348845efc071d908de7a6d94a97eff", executableSha256: "1301a5024fef1d58c4cfbde5d44794cb18c5b2cbbe4f3c64f81cc4eaa683c45f" },
49726
+ "win32-x64": { platform: "win64", bytes: 119768220, sha256: "86fb1fa7fbbda65f6f1572062c358803e61a6e6d9489e6619b37a61d609fa845", executableSha256: "ec0744f041cb6c35c439c85a5ac42d77b507838cd59cac005e25114025336c98" }
49727
+ };
49728
+ function browserBundleSpec(target) {
49729
+ if (!Object.hasOwn(CHROME_ASSETS, target)) throw new Error(`Unsupported desktop browser target: ${target}`);
49730
+ const pinned = CHROME_ASSETS[target];
49731
+ const [platform, arch] = target.split("-");
49732
+ const engine = resolveAgentBrowserReleaseAsset(platform, arch);
49733
+ const suffix = platform === "win32" ? ".exe" : "";
49734
+ const directory = `chrome-headless-shell-${pinned.platform}`;
49735
+ return {
49736
+ schemaVersion: 1,
49737
+ target,
49738
+ engine: {
49739
+ version: agentBrowserReleaseVersion(engine),
49740
+ asset: engine.asset,
49741
+ url: agentBrowserReleaseUrl(engine),
49742
+ bytes: engine.bytes,
49743
+ sha256: engine.sha256,
49744
+ executable: `agent-browser${suffix}`
49745
+ },
49746
+ chrome: {
49747
+ version: CHROME_VERSION,
49748
+ asset: `${directory}.zip`,
49749
+ url: `https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/${pinned.platform}/${directory}.zip`,
49750
+ bytes: pinned.bytes,
49751
+ sha256: pinned.sha256,
49752
+ executableSha256: pinned.executableSha256,
49753
+ executable: `chrome/${directory}/chrome-headless-shell${suffix}`,
49754
+ license: `chrome/${directory}/LICENSE.headless_shell`,
49755
+ about: `chrome/${directory}/ABOUT`
49756
+ }
49757
+ };
49758
+ }
49759
+ function browserBundlePaths(bundleDirectory, target) {
49760
+ const spec = browserBundleSpec(target);
49761
+ return {
49762
+ directory: bundleDirectory,
49763
+ manifest: join40(bundleDirectory, "manifest.json"),
49764
+ engine: join40(bundleDirectory, spec.engine.executable),
49765
+ chrome: join40(bundleDirectory, spec.chrome.executable),
49766
+ licenses: join40(bundleDirectory, "licenses")
49767
+ };
49768
+ }
49769
+
49642
49770
  // server/browser-engine.ts
49643
49771
  var ENGINE_DIR = "tools/agent-browser";
49644
49772
  var KEY_FILE = "browser-engine-key";
@@ -49649,8 +49777,9 @@ function executableName(platform = process.platform) {
49649
49777
  function isMusl(platform = process.platform, exists = existsSync18) {
49650
49778
  return platform === "linux" && (exists("/lib/ld-musl-x86_64.so.1") || exists("/lib/ld-musl-aarch64.so.1"));
49651
49779
  }
49652
- function pinnedBinaryPath(dataDir = DATA_DIR, platform = process.platform) {
49653
- return join40(dataDir, ENGINE_DIR, AGENT_BROWSER_VERSION, executableName(platform));
49780
+ function pinnedBinaryPath(dataDir = DATA_DIR, platform = process.platform, arch = process.arch) {
49781
+ const version2 = agentBrowserReleaseVersion(resolveAgentBrowserReleaseAsset(platform, arch));
49782
+ return join41(dataDir, ENGINE_DIR, version2, executableName(platform));
49654
49783
  }
49655
49784
  function onPath(env, platform, exists) {
49656
49785
  const pathValue = platform === "win32" ? Object.entries(env).findLast(([key3]) => key3.toUpperCase() === "PATH")?.[1] : env.PATH;
@@ -49662,13 +49791,27 @@ function onPath(env, platform, exists) {
49662
49791
  }
49663
49792
  return null;
49664
49793
  }
49794
+ function packagedBrowser(options) {
49795
+ const resources = (options.env ?? process.env).OMB_RESOURCES_PATH;
49796
+ if (!resources) return null;
49797
+ try {
49798
+ return browserBundlePaths(join41(resolve10(resources), "browser-engine"), `${options.platform ?? process.platform}-${options.arch ?? process.arch}`);
49799
+ } catch {
49800
+ return null;
49801
+ }
49802
+ }
49803
+ function completePackage(bundle, exists) {
49804
+ return [bundle.manifest, bundle.engine, bundle.chrome, bundle.licenses].every(exists);
49805
+ }
49665
49806
  function resolveAgentBrowserBinary(options = {}) {
49666
49807
  const env = options.env ?? process.env;
49667
49808
  const platform = options.platform ?? process.platform;
49668
49809
  const exists = options.exists ?? existsSync18;
49669
49810
  const override = env.OMB_AGENT_BROWSER_PATH?.trim();
49670
49811
  if (override) return resolve10(override) && exists(resolve10(override)) ? resolve10(override) : null;
49671
- const pinned = pinnedBinaryPath(options.dataDir, platform);
49812
+ const bundle = packagedBrowser(options);
49813
+ if (bundle && exists(bundle.directory)) return completePackage(bundle, exists) ? bundle.engine : null;
49814
+ const pinned = pinnedBinaryPath(options.dataDir, platform, options.arch);
49672
49815
  if (exists(pinned)) return pinned;
49673
49816
  return onPath(env, platform, exists);
49674
49817
  }
@@ -49676,11 +49819,11 @@ async function installAgentBrowserBinary(options = {}) {
49676
49819
  const platform = options.platform ?? process.platform;
49677
49820
  const asset = options.asset ?? resolveAgentBrowserReleaseAsset(platform, options.arch ?? process.arch, options.musl ?? isMusl(platform));
49678
49821
  if (!asset) throw new Error(`agent-browser publishes no build for ${platform}-${options.arch ?? process.arch}.`);
49679
- const destination = pinnedBinaryPath(options.dataDir, platform);
49680
- const directory = join40(destination, "..");
49822
+ const destination = pinnedBinaryPath(options.dataDir, platform, options.arch);
49823
+ const directory = join41(destination, "..");
49681
49824
  mkdirSync20(directory, { recursive: true, mode: 448 });
49682
49825
  const url2 = agentBrowserReleaseUrl(asset);
49683
- options.log?.(`downloading agent-browser ${AGENT_BROWSER_VERSION} (${Math.round(asset.bytes / 1024 / 1024)} MB, digest pinned)`);
49826
+ options.log?.(`downloading agent-browser ${agentBrowserReleaseVersion(asset)} (${Math.round(asset.bytes / 1024 / 1024)} MB, digest pinned)`);
49684
49827
  const controller = new AbortController();
49685
49828
  const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS2);
49686
49829
  timer.unref?.();
@@ -49702,6 +49845,11 @@ async function installAgentBrowserBinary(options = {}) {
49702
49845
  return destination;
49703
49846
  }
49704
49847
  function ensureChrome(binaryPath, options = {}) {
49848
+ const bundle = packagedBrowser(options);
49849
+ if (!options.withDeps && bundle && resolve10(binaryPath) === bundle.engine && completePackage(bundle, existsSync18)) {
49850
+ options.log?.("agent-browser: the bundled browser is ready; no download needed");
49851
+ return Promise.resolve();
49852
+ }
49705
49853
  const args = ["install", ...options.withDeps ? ["--with-deps"] : []];
49706
49854
  return new Promise((done, fail2) => {
49707
49855
  const child = spawn4(binaryPath, args, { env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
@@ -49724,7 +49872,7 @@ function ensureChrome(binaryPath, options = {}) {
49724
49872
  });
49725
49873
  }
49726
49874
  function browserEngineEncryptionKey(dataDir = DATA_DIR) {
49727
- const file2 = join40(dataDir, KEY_FILE);
49875
+ const file2 = join41(dataDir, KEY_FILE);
49728
49876
  try {
49729
49877
  const existing = readFileSync31(file2, "utf8").trim();
49730
49878
  if (/^[0-9a-f]{64}$/u.test(existing)) return existing;
@@ -49738,14 +49886,28 @@ function browserEngineEncryptionKey(dataDir = DATA_DIR) {
49738
49886
  }
49739
49887
  function browserEngineStatus(options = {}) {
49740
49888
  const binaryPath = resolveAgentBrowserBinary(options);
49741
- if (binaryPath) return { kind: "ready", binaryPath, version: AGENT_BROWSER_VERSION };
49889
+ const bundle = packagedBrowser(options);
49890
+ if (binaryPath) {
49891
+ const platform2 = options.platform ?? process.platform;
49892
+ const managed = binaryPath === bundle?.engine || binaryPath === pinnedBinaryPath(options.dataDir, platform2, options.arch);
49893
+ const version2 = managed ? agentBrowserReleaseVersion(resolveAgentBrowserReleaseAsset(platform2, options.arch)) : AGENT_BROWSER_VERSION;
49894
+ return { kind: "ready", binaryPath, version: version2 };
49895
+ }
49896
+ if (bundle && (options.exists ?? existsSync18)(bundle.directory)) {
49897
+ return { kind: "unavailable", reason: "The desktop browser bundle is incomplete. Reinstall or update OpenMausBot to repair it.", installable: false };
49898
+ }
49742
49899
  const platform = options.platform ?? process.platform;
49743
- const asset = resolveAgentBrowserReleaseAsset(platform, process.arch, isMusl(platform));
49744
- return asset ? { kind: "unavailable", reason: "agent-browser is not installed on this machine yet", installable: true } : { kind: "unavailable", reason: `agent-browser publishes no build for ${platform}-${process.arch}`, installable: false };
49900
+ const arch = options.arch ?? process.arch;
49901
+ const asset = resolveAgentBrowserReleaseAsset(platform, arch, isMusl(platform));
49902
+ return asset ? { kind: "unavailable", reason: "agent-browser is not installed on this machine yet", installable: true } : { kind: "unavailable", reason: `agent-browser publishes no build for ${platform}-${arch}`, installable: false };
49745
49903
  }
49746
49904
  function agentBrowserIntegration(input) {
49747
49905
  const env = {
49748
49906
  AGENT_BROWSER_SESSION: input.session,
49907
+ // The MCP server invokes child CLI commands without forwarding its own
49908
+ // global flags. The environment keeps page-provided tools disabled in
49909
+ // those commands too, and avoids changing browser launch settings later.
49910
+ AGENT_BROWSER_NO_WEBMCP: "1",
49749
49911
  // This is a restore *name*, not a boolean. "1" would give every bot
49750
49912
  // the same saved cookies despite using different daemon sessions.
49751
49913
  AGENT_BROWSER_RESTORE: input.session,
@@ -49753,10 +49915,53 @@ function agentBrowserIntegration(input) {
49753
49915
  AGENT_BROWSER_ENCRYPTION_KEY: input.encryptionKey
49754
49916
  };
49755
49917
  if (input.headless !== false) env.AGENT_BROWSER_HEADLESS = "1";
49756
- const path = (input.env ?? process.env).PATH;
49757
- if (path) env.PATH = path;
49918
+ const sourceEnv = input.env ?? process.env;
49919
+ for (const name2 of ["PATH", "AGENT_BROWSER_EXECUTABLE_PATH"]) {
49920
+ if (sourceEnv[name2]) env[name2] = sourceEnv[name2];
49921
+ }
49922
+ const bundle = packagedBrowser({ env: sourceEnv });
49923
+ if (!env.AGENT_BROWSER_EXECUTABLE_PATH && bundle && resolve10(input.binaryPath) === bundle.engine && completePackage(bundle, existsSync18)) {
49924
+ env.AGENT_BROWSER_EXECUTABLE_PATH = bundle.chrome;
49925
+ }
49758
49926
  return { command: input.binaryPath, args: ["mcp", "--tools", "core", "--no-webmcp"], env };
49759
49927
  }
49928
+ var FRAME_TIMEOUT_MS = 1e4;
49929
+ function agentBrowserFrame(input) {
49930
+ const file2 = join41(tmpdir6(), `openmausbot-browser-${randomUUID12()}.png`);
49931
+ return new Promise((settle, fail2) => {
49932
+ const child = spawn4(input.binaryPath, ["screenshot", file2], {
49933
+ env: { ...process.env, ...input.env },
49934
+ stdio: ["ignore", "ignore", "pipe"],
49935
+ windowsHide: true
49936
+ });
49937
+ let stderr = "";
49938
+ child.stderr?.on("data", (chunk) => {
49939
+ if (stderr.length < 2e3) stderr += String(chunk);
49940
+ });
49941
+ const timer = setTimeout(() => {
49942
+ child.kill("SIGKILL");
49943
+ fail2(new Error("the browser did not return a picture in time"));
49944
+ }, input.timeoutMs ?? FRAME_TIMEOUT_MS);
49945
+ const done = (error51) => {
49946
+ clearTimeout(timer);
49947
+ try {
49948
+ if (error51) {
49949
+ fail2(error51);
49950
+ return;
49951
+ }
49952
+ settle({ png: readFileSync31(file2).toString("base64"), format: "png" });
49953
+ } catch {
49954
+ fail2(new Error("the browser reported a picture it did not write"));
49955
+ } finally {
49956
+ rmSync6(file2, { force: true });
49957
+ }
49958
+ };
49959
+ child.on("error", (error51) => done(error51 instanceof Error ? error51 : new Error(String(error51))));
49960
+ child.on("close", (code) => {
49961
+ done(code === 0 ? null : new Error(`the browser could not be pictured${stderr.trim() ? `: ${stderr.trim().slice(0, 200)}` : ""}`));
49962
+ });
49963
+ });
49964
+ }
49760
49965
  function browserSessionId(botId, partitionId) {
49761
49966
  if (partitionId === "guest") return `guest-${randomUUID12()}`;
49762
49967
  const raw = partitionId || `bot-${botId}`;
@@ -49811,6 +50016,49 @@ async function captureOutsideHumanControl(control, capture) {
49811
50016
  return { png, mime: format === "jpeg" ? "image/jpeg" : "image/png" };
49812
50017
  }
49813
50018
 
50019
+ // server/screen-frame-source.ts
50020
+ function createScreenFrameSource(input) {
50021
+ let current2 = null;
50022
+ let lastAt = 0;
50023
+ let attemptedSurface;
50024
+ let last = null;
50025
+ let lastSurface;
50026
+ const selectedSurface = () => input.captures[source.surface] ? source.surface : input.captures.computer ? "computer" : "browser";
50027
+ const source = {
50028
+ surface: input.captures.computer ? "computer" : "browser",
50029
+ get last() {
50030
+ return lastSurface === selectedSurface() ? last : null;
50031
+ },
50032
+ async capture(fresh = false) {
50033
+ while (current2) await current2;
50034
+ if (input.control().held) return;
50035
+ const surface = selectedSurface();
50036
+ if (!fresh && attemptedSurface === surface && Date.now() - lastAt < input.minGapMs) return;
50037
+ attemptedSurface = surface;
50038
+ current2 = (async () => {
50039
+ try {
50040
+ const capture = input.captures[surface];
50041
+ if (!capture) return;
50042
+ const frame = await captureOutsideHumanControl(input.control, capture);
50043
+ if (!frame || surface !== selectedSurface()) return;
50044
+ last = frame;
50045
+ lastSurface = surface;
50046
+ input.onFrame(frame);
50047
+ } catch {
50048
+ } finally {
50049
+ lastAt = Date.now();
50050
+ }
50051
+ })();
50052
+ try {
50053
+ await current2;
50054
+ } finally {
50055
+ current2 = null;
50056
+ }
50057
+ }
50058
+ };
50059
+ return source;
50060
+ }
50061
+
49814
50062
  // server/screen-frame-gate.ts
49815
50063
  import { createHash as createHash17 } from "node:crypto";
49816
50064
  var SCREEN_TOUCHING_TOOLS = /* @__PURE__ */ new Set([
@@ -49824,7 +50072,8 @@ var SCREEN_TOUCHING_TOOLS = /* @__PURE__ */ new Set([
49824
50072
  "open_url",
49825
50073
  "browser_click",
49826
50074
  "browser_fill",
49827
- // built-in browser
50075
+ // built-in browser: the Electron surface's names, kept because a bot on an
50076
+ // older remote harness can still call them
49828
50077
  "browser_navigate",
49829
50078
  "browser_type",
49830
50079
  "browser_press",
@@ -49835,6 +50084,20 @@ var SCREEN_TOUCHING_TOOLS = /* @__PURE__ */ new Set([
49835
50084
  "browser_back",
49836
50085
  "browser_forward",
49837
50086
  "browser_screenshot",
50087
+ // built-in browser: agent-browser's names. The engine swap replaced the
50088
+ // Electron surface without renaming these here, so every browser turn
50089
+ // stopped counting as screen work and no page ever reached the transcript.
50090
+ // Read-only tools stay out by the same rule as everywhere else in this
50091
+ // list: agent_browser_snapshot, _read and _get_text return text, and the
50092
+ // waits change nothing the action before them did not already count.
50093
+ "agent_browser_open",
50094
+ "agent_browser_click",
50095
+ "agent_browser_fill",
50096
+ "agent_browser_type",
50097
+ "agent_browser_press",
50098
+ "agent_browser_select",
50099
+ "agent_browser_check",
50100
+ "agent_browser_screenshot",
49838
50101
  // Cua Driver (local Mac, Local VM, VPS)
49839
50102
  "double_click",
49840
50103
  "right_click",
@@ -49855,6 +50118,14 @@ function screenFrameHash(png) {
49855
50118
  function settledFrameIsNews(shownFrameHash, png) {
49856
50119
  return shownFrameHash === void 0 || screenFrameHash(png) !== shownFrameHash;
49857
50120
  }
50121
+ function screenSurfaceForTool(toolName) {
50122
+ const name2 = toolName.toLowerCase();
50123
+ if (name2.startsWith("mcp__computer__") || name2.startsWith("computer_")) return "computer";
50124
+ if (name2.startsWith("mcp__browser__")) return "browser";
50125
+ const bare = name2.replace(/^mcp__.+?__/, "");
50126
+ if (bare === "browser_click" || bare === "browser_fill") return "computer";
50127
+ return bare.startsWith("agent_browser_") || bare.startsWith("browser_") ? "browser" : "computer";
50128
+ }
49858
50129
 
49859
50130
  // server/routine-requests.ts
49860
50131
  import { createHash as createHash18 } from "node:crypto";
@@ -50862,13 +51133,13 @@ function profileRevision(bot) {
50862
51133
  import { appendFile as appendFile2 } from "node:fs/promises";
50863
51134
  import { createHash as createHash20, randomUUID as randomUUID13 } from "node:crypto";
50864
51135
  import { closeSync as closeSync8, existsSync as existsSync19, fstatSync as fstatSync3, mkdirSync as mkdirSync21, openSync as openSync8, readSync as readSync3 } from "node:fs";
50865
- import { join as join41 } from "node:path";
51136
+ import { join as join42 } from "node:path";
50866
51137
  var FILE_NAME2 = "history.ndjson";
50867
51138
  var ONE_LINER = 200;
50868
51139
  var MAX_HISTORY_READ_BYTES = 8 * 1024 * 1024;
50869
51140
  var writeQueues2 = /* @__PURE__ */ new Map();
50870
51141
  function historyFile(botId) {
50871
- return join41(botFolder(botId), FILE_NAME2);
51142
+ return join42(botFolder(botId), FILE_NAME2);
50872
51143
  }
50873
51144
  function oneLiner(value) {
50874
51145
  return value.replace(/\s+/g, " ").trim().slice(0, ONE_LINER);
@@ -51265,8 +51536,8 @@ function parseBotDirectory(value) {
51265
51536
  }
51266
51537
  async function readBounded(response, maxBytes) {
51267
51538
  const oversized = () => new Error("The bot directory response is too large");
51268
- const announced = Number(response.headers.get("content-length") ?? 0);
51269
- if (announced > maxBytes) throw oversized();
51539
+ const announced2 = Number(response.headers.get("content-length") ?? 0);
51540
+ if (announced2 > maxBytes) throw oversized();
51270
51541
  if (!response.body) {
51271
51542
  const raw = await response.text();
51272
51543
  if (Buffer.byteLength(raw) > maxBytes) throw oversized();
@@ -51368,7 +51639,7 @@ function matchDirectoryBots(profile, bots, limit = 5) {
51368
51639
 
51369
51640
  // server/project-scout.ts
51370
51641
  import { readFileSync as readFileSync32, readdirSync as readdirSync6, statSync as statSync9 } from "node:fs";
51371
- import { basename as basename5, join as join42 } from "node:path";
51642
+ import { basename as basename5, join as join43 } from "node:path";
51372
51643
 
51373
51644
  // shared/import-name.ts
51374
51645
  function takeImportName(value, taken, maxLength = 100) {
@@ -51572,7 +51843,7 @@ function isDir(path) {
51572
51843
  }
51573
51844
  }
51574
51845
  function packageJson(cwd) {
51575
- const raw = readText2(join42(cwd, "package.json"));
51846
+ const raw = readText2(join43(cwd, "package.json"));
51576
51847
  if (!raw) return { deps: /* @__PURE__ */ new Set() };
51577
51848
  try {
51578
51849
  const parsed = JSON.parse(raw);
@@ -51595,7 +51866,7 @@ function packageJson(cwd) {
51595
51866
  }
51596
51867
  }
51597
51868
  function readme(cwd) {
51598
- const raw = readText2(join42(cwd, "README.md")) ?? readText2(join42(cwd, "readme.md")) ?? readText2(join42(cwd, "README"));
51869
+ const raw = readText2(join43(cwd, "README.md")) ?? readText2(join43(cwd, "readme.md")) ?? readText2(join43(cwd, "README"));
51599
51870
  if (!raw) return {};
51600
51871
  let title;
51601
51872
  let summary;
@@ -51669,12 +51940,12 @@ function scoutProject(cwd) {
51669
51940
  const pkg = packageJson(cwd);
51670
51941
  const md = readme(cwd);
51671
51942
  const entries = new Set(listNames(cwd));
51672
- const pythonDeps = `${readText2(join42(cwd, "requirements.txt")) ?? ""}
51673
- ${readText2(join42(cwd, "pyproject.toml")) ?? ""}`.toLowerCase();
51943
+ const pythonDeps = `${readText2(join43(cwd, "requirements.txt")) ?? ""}
51944
+ ${readText2(join43(cwd, "pyproject.toml")) ?? ""}`.toLowerCase();
51674
51945
  const hasDep = (dep) => pkg.deps.has(dep) || pythonDeps.length > 1 && pythonDeps.includes(dep);
51675
51946
  const pathEvidence = (path) => {
51676
- if (path.includes("/")) return isDir(join42(cwd, path)) && listNames(join42(cwd, path)).length > 0 ? `${path}/` : null;
51677
- if (entries.has(path)) return isDir(join42(cwd, path)) ? `${path}/` : path;
51947
+ if (path.includes("/")) return isDir(join43(cwd, path)) && listNames(join43(cwd, path)).length > 0 ? `${path}/` : null;
51948
+ if (entries.has(path)) return isDir(join43(cwd, path)) ? `${path}/` : path;
51678
51949
  return null;
51679
51950
  };
51680
51951
  const signals = [];
@@ -51687,7 +51958,7 @@ ${readText2(join42(cwd, "pyproject.toml")) ?? ""}`.toLowerCase();
51687
51958
  }
51688
51959
  if (detector.role === "docs" && evidence.length > 0) {
51689
51960
  const hasMkdocs = evidence.includes("mkdocs.yml");
51690
- const hasMarkdown = listNames(join42(cwd, "docs")).some((name2) => name2.endsWith(".md"));
51961
+ const hasMarkdown = listNames(join43(cwd, "docs")).some((name2) => name2.endsWith(".md"));
51691
51962
  if (!hasMkdocs && !hasMarkdown) continue;
51692
51963
  }
51693
51964
  if (evidence.length > 0) signals.push({ role: detector.role, evidence: evidence.slice(0, 6) });
@@ -52182,8 +52453,8 @@ async function fetchJson(url2, maxBytes, fetcher) {
52182
52453
  const error51 = Object.assign(new Error(`GitHub returned HTTP ${response.status}`), { status: response.status });
52183
52454
  throw error51;
52184
52455
  }
52185
- const announced = Number(response.headers.get("content-length") ?? 0);
52186
- if (announced > maxBytes) throw new Error("The remote team file is too large");
52456
+ const announced2 = Number(response.headers.get("content-length") ?? 0);
52457
+ if (announced2 > maxBytes) throw new Error("The remote team file is too large");
52187
52458
  const raw = await response.text();
52188
52459
  if (Buffer.byteLength(raw) > maxBytes) throw new Error("The remote team file is too large");
52189
52460
  try {
@@ -52199,8 +52470,8 @@ async function fetchText2(url2, maxBytes, fetcher) {
52199
52470
  signal: AbortSignal.timeout(1e4)
52200
52471
  });
52201
52472
  if (!response.ok) throw Object.assign(new Error(`GitHub returned HTTP ${response.status}`), { status: response.status });
52202
- const announced = Number(response.headers.get("content-length") ?? 0);
52203
- if (announced > maxBytes) throw new Error("The remote team file is too large");
52473
+ const announced2 = Number(response.headers.get("content-length") ?? 0);
52474
+ if (announced2 > maxBytes) throw new Error("The remote team file is too large");
52204
52475
  const raw = await response.text();
52205
52476
  if (Buffer.byteLength(raw) > maxBytes) throw new Error("The remote team file is too large");
52206
52477
  return raw;
@@ -52277,7 +52548,7 @@ async function fetchGithubTeam(input, fetcher = fetch) {
52277
52548
 
52278
52549
  // server/thread-events.ts
52279
52550
  import { closeSync as closeSync9, fstatSync as fstatSync4, openSync as openSync9, readSync as readSync4 } from "node:fs";
52280
- import { join as join43 } from "node:path";
52551
+ import { join as join44 } from "node:path";
52281
52552
  var DEFAULT_LIMIT = 300;
52282
52553
  var MAX_LIMIT = 2e3;
52283
52554
  var READ_CHUNK = 64 * 1024;
@@ -52411,8 +52682,8 @@ function readThreadEvents(input) {
52411
52682
  assertThreadId(threadId);
52412
52683
  const requested = input.limit ?? DEFAULT_LIMIT;
52413
52684
  const limit = Number.isFinite(requested) ? Math.max(1, Math.min(Math.trunc(requested), MAX_LIMIT)) : DEFAULT_LIMIT;
52414
- const runtime = readRecentLines(join43(eventsDir, `${threadId}.ndjson`), limit, isRuntimeEvent);
52415
- const native = readRecentLines(join43(nativeDir, `${threadId}.ndjson`), limit, isNativeRecord);
52685
+ const runtime = readRecentLines(join44(eventsDir, `${threadId}.ndjson`), limit, isRuntimeEvent);
52686
+ const native = readRecentLines(join44(nativeDir, `${threadId}.ndjson`), limit, isNativeRecord);
52416
52687
  const merged = [
52417
52688
  ...runtime.lines.map((data) => ({ kind: "runtime", at: data.createdAt, data })),
52418
52689
  ...native.lines.map((data) => ({ kind: "native", at: data.at, data }))
@@ -52589,7 +52860,7 @@ function memberTurnSelection(selection) {
52589
52860
  // server/webhooks.ts
52590
52861
  import { createHash as createHash21, randomBytes as randomBytes5, randomUUID as randomUUID14, timingSafeEqual } from "node:crypto";
52591
52862
  import { mkdirSync as mkdirSync22, readFileSync as readFileSync33 } from "node:fs";
52592
- import { dirname as dirname13, join as join44 } from "node:path";
52863
+ import { dirname as dirname13, join as join45 } from "node:path";
52593
52864
  var MAX_DELIVERIES = 2e3;
52594
52865
  var MAX_ATTEMPTS = 2e3;
52595
52866
  var MAX_EVENT_CHARS = 48e3;
@@ -52776,7 +53047,7 @@ var WebhookManager = class {
52776
53047
  rate = /* @__PURE__ */ new Map();
52777
53048
  constructor(options) {
52778
53049
  this.options = options;
52779
- this.file = options.file ?? join44(DATA_DIR, "webhooks.json");
53050
+ this.file = options.file ?? join45(DATA_DIR, "webhooks.json");
52780
53051
  this.now = options.now ?? Date.now;
52781
53052
  try {
52782
53053
  const parsed = webhookFileSchema.safeParse(parseJson(readFileSync33(this.file, "utf8")));
@@ -53040,7 +53311,7 @@ var WebhookManager = class {
53040
53311
 
53041
53312
  // server/skill-library.ts
53042
53313
  import { existsSync as existsSync20, readFileSync as readFileSync34, readdirSync as readdirSync7 } from "node:fs";
53043
- import { basename as basename6, join as join45 } from "node:path";
53314
+ import { basename as basename6, join as join46 } from "node:path";
53044
53315
  var SAFE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
53045
53316
  function strings(value) {
53046
53317
  return Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim()) ? value.map((item) => item.trim()) : null;
@@ -53069,19 +53340,19 @@ function parseSkillManifest(value, directory) {
53069
53340
  };
53070
53341
  }
53071
53342
  function loadSkillDirectory(directory) {
53072
- const manifestPath2 = join45(directory, "manifest.json");
53073
- const skillPath = join45(directory, "SKILL.md");
53343
+ const manifestPath2 = join46(directory, "manifest.json");
53344
+ const skillPath = join46(directory, "SKILL.md");
53074
53345
  if (!existsSync20(manifestPath2) || !existsSync20(skillPath)) return null;
53075
53346
  const manifest = parseSkillManifest(JSON.parse(readFileSync34(manifestPath2, "utf8")), directory);
53076
53347
  const instructions = readFileSync34(skillPath, "utf8").trim();
53077
53348
  if (!instructions.startsWith("---")) throw new Error(`${skillPath} has no skill frontmatter`);
53078
53349
  return { manifest, instructions, directory };
53079
53350
  }
53080
- function loadBundledSkills(root = process.env.OMB_SKILLS_DIR || join45(process.cwd(), "skills")) {
53351
+ function loadBundledSkills(root = process.env.OMB_SKILLS_DIR || join46(process.cwd(), "skills")) {
53081
53352
  if (!existsSync20(root)) return [];
53082
53353
  const skills = [];
53083
53354
  for (const name2 of readdirSync7(root).sort()) {
53084
- const directory = join45(root, name2);
53355
+ const directory = join46(root, name2);
53085
53356
  const skill = loadSkillDirectory(directory);
53086
53357
  if (skill) skills.push(skill);
53087
53358
  }
@@ -53098,7 +53369,7 @@ function loadUserSkills(root) {
53098
53369
  const skills = [];
53099
53370
  for (const name2 of names) {
53100
53371
  try {
53101
- const skill = loadSkillDirectory(join45(root, name2));
53372
+ const skill = loadSkillDirectory(join46(root, name2));
53102
53373
  if (skill) skills.push(skill);
53103
53374
  } catch {
53104
53375
  }
@@ -53802,7 +54073,7 @@ function createGracefulShutdown({
53802
54073
  import { createHash as createHash22, randomUUID as randomUUID15 } from "node:crypto";
53803
54074
  import { existsSync as existsSync21, linkSync as linkSync4, mkdirSync as mkdirSync23, readFileSync as readFileSync35, renameSync as renameSync6, unlinkSync as unlinkSync8, writeFileSync as writeFileSync16 } from "node:fs";
53804
54075
  import { hostname as hostname4 } from "node:os";
53805
- import { join as join46 } from "node:path";
54076
+ import { join as join47 } from "node:path";
53806
54077
  var LEASE_NAME = "openmausbot-server.lease";
53807
54078
  var DELEGATED_CHILD_DIR = ".openmausbot-server-child";
53808
54079
  var CHILD_LEASE_ENV = "OPENMAUSBOT_INTERNAL_DATA_DIR_LEASE";
@@ -53961,10 +54232,10 @@ function prepareDataDir(dataDir, legacyDataDir) {
53961
54232
  } catch (error51) {
53962
54233
  throw leaseError("OpenMausBot cannot create its data directory.", error51);
53963
54234
  }
53964
- return join46(dataDir, LEASE_NAME);
54235
+ return join47(dataDir, LEASE_NAME);
53965
54236
  }
53966
54237
  function assertNoLiveDelegatedChild(dataDir) {
53967
- const childLeasePath = join46(dataDir, DELEGATED_CHILD_DIR, LEASE_NAME);
54238
+ const childLeasePath = join47(dataDir, DELEGATED_CHILD_DIR, LEASE_NAME);
53968
54239
  const child = readOwner(childLeasePath);
53969
54240
  if (!child) return;
53970
54241
  if (child.host !== hostname4()) {
@@ -54006,13 +54277,13 @@ function validateChildDelegation(dataDir, encoded) {
54006
54277
  const capability = parseCapability(encoded);
54007
54278
  const invalid = () => leaseError("The OpenMausBot desktop lease delegation is invalid; refusing to start to protect its state.");
54008
54279
  if (!capability) throw invalid();
54009
- const parentLeasePath = join46(dataDir, LEASE_NAME);
54280
+ const parentLeasePath = join47(dataDir, LEASE_NAME);
54010
54281
  const matchesLiveParent = (owner2) => Boolean(owner2 && owner2.pid === capability.pid && owner2.token === capability.token && owner2.host === hostname4() && processIsAlive2(owner2.pid));
54011
54282
  if (!matchesLiveParent(readOwner(parentLeasePath))) {
54012
54283
  throw invalid();
54013
54284
  }
54014
54285
  const childLease = acquireDataDirLeaseInternal(
54015
- join46(dataDir, DELEGATED_CHILD_DIR),
54286
+ join47(dataDir, DELEGATED_CHILD_DIR),
54016
54287
  { guardDelegatedChild: false }
54017
54288
  );
54018
54289
  if (!matchesLiveParent(readOwner(parentLeasePath))) {
@@ -54111,14 +54382,14 @@ function acquireDataDirLeaseForProcess(dataDir, environment = process.env) {
54111
54382
 
54112
54383
  // server/enterprise.ts
54113
54384
  import { existsSync as existsSync22 } from "node:fs";
54114
- import { join as join47 } from "node:path";
54385
+ import { join as join48 } from "node:path";
54115
54386
  import { pathToFileURL } from "node:url";
54116
54387
  var layerSchema = external_exports.object({
54117
54388
  customer: external_exports.string().min(1),
54118
54389
  features: external_exports.array(external_exports.string().min(1)),
54119
54390
  expiresAt: external_exports.string().nullable()
54120
54391
  });
54121
- var DEFAULT_DIR = join47(SERVER_ROOT, "..", "enterprise");
54392
+ var DEFAULT_DIR = join48(SERVER_ROOT, "..", "enterprise");
54122
54393
  var current = { edition: "oss", features: [] };
54123
54394
  function oss(notice) {
54124
54395
  current = notice ? { edition: "oss", features: [], notice } : { edition: "oss", features: [] };
@@ -54127,7 +54398,7 @@ function oss(notice) {
54127
54398
  async function loadEnterpriseLayer(options = {}) {
54128
54399
  const dir = options.dir ?? process.env.OMB_ENTERPRISE_DIR ?? DEFAULT_DIR;
54129
54400
  const licenseKey = options.licenseKey ?? process.env.OMB_LICENSE_KEY;
54130
- const entry = [join47(dir, "server", "index.ts"), join47(dir, "server", "index.js")].find((candidate) => existsSync22(candidate));
54401
+ const entry = [join48(dir, "server", "index.ts"), join48(dir, "server", "index.js")].find((candidate) => existsSync22(candidate));
54131
54402
  if (!entry) {
54132
54403
  return oss(
54133
54404
  licenseKey ? `OMB_LICENSE_KEY is set but no enterprise layer exists at ${dir}` : void 0
@@ -54559,6 +54830,7 @@ function resolveRequestAuth(req, options) {
54559
54830
  if (!session.scopes.includes(needed)) {
54560
54831
  return deny(403, `forbidden: this session lacks the ${needed} scope`);
54561
54832
  }
54833
+ if (via !== "ticket") options.sessions.renew(session.id);
54562
54834
  return { auth: { kind: "session", session, via, scopes: session.scopes }, status: 401, error: "" };
54563
54835
  }
54564
54836
  const proxied = isProxied(req);
@@ -54594,7 +54866,17 @@ var SCOPES = ["admin", "client"];
54594
54866
  var PAIRING_CODE_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
54595
54867
  var PAIRING_CODE_LENGTH = 12;
54596
54868
  var PAIRING_CODE_TTL_MS = 5 * 6e4;
54597
- var SESSION_TTL_MS = 30 * 24 * 60 * 6e4;
54869
+ function daysMs(value, fallbackDays) {
54870
+ const days = Number(value);
54871
+ const ok = Number.isInteger(days) && days >= 1 && days <= 3650;
54872
+ return (ok ? days : fallbackDays) * 24 * 60 * 6e4;
54873
+ }
54874
+ var SESSION_TTL_MS = daysMs(process.env.OMB_SESSION_TTL_DAYS, 30);
54875
+ var SESSION_MAX_AGE_MS = daysMs(process.env.OMB_SESSION_MAX_DAYS, 180);
54876
+ var SESSION_RENEW_WHEN_LEFT_MS = SESSION_TTL_MS / 2;
54877
+ function cookieMaxAgeSeconds(session, now = Date.now()) {
54878
+ return Math.max(1, Math.floor((session.expiresAt - now) / 1e3));
54879
+ }
54598
54880
  var STREAM_TICKET_TTL_MS = 5 * 6e4;
54599
54881
  var LOCKOUT = { failures: 10, windowMs: 6e4, lockMs: 6e4 };
54600
54882
  var EXCHANGE_REPLAY_MS = 6e4;
@@ -54793,7 +55075,9 @@ var SessionRegistry = class {
54793
55075
  scopes: [...pairing.scopes],
54794
55076
  createdAt: now,
54795
55077
  lastSeenAt: now,
54796
- expiresAt: now + SESSION_TTL_MS
55078
+ // The absolute cap applies from the first term, so a TTL configured
55079
+ // longer than the cap does not hand out a session the cap forbids.
55080
+ expiresAt: now + Math.min(SESSION_TTL_MS, SESSION_MAX_AGE_MS)
54797
55081
  };
54798
55082
  this.sessions.push(record2);
54799
55083
  this.lastSeenWrites.set(record2.id, now);
@@ -54817,6 +55101,25 @@ var SessionRegistry = class {
54817
55101
  }
54818
55102
  return record2;
54819
55103
  }
55104
+ /** Sliding expiry. Called by the request gate once a request has passed
55105
+ * the origin and scope checks (so a rejected request never extends
55106
+ * anything): a still-valid session with half its term or less left is
55107
+ * renewed for the full term, capped at SESSION_MAX_AGE_MS from pairing.
55108
+ * Nothing expired is revived. Writes at most once per half-term, so it
55109
+ * adds nothing to the last-seen traffic. Returns whether it renewed. */
55110
+ renew(sessionId) {
55111
+ const record2 = this.sessions.find((s) => s.id === sessionId);
55112
+ const now = this.now();
55113
+ if (!record2 || record2.expiresAt <= now) return false;
55114
+ if (record2.expiresAt - now > SESSION_RENEW_WHEN_LEFT_MS) return false;
55115
+ const next = Math.min(now + SESSION_TTL_MS, record2.createdAt + SESSION_MAX_AGE_MS);
55116
+ if (next <= record2.expiresAt) return false;
55117
+ record2.expiresAt = next;
55118
+ record2.lastSeenAt = now;
55119
+ this.lastSeenWrites.set(record2.id, now);
55120
+ this.persist();
55121
+ return true;
55122
+ }
54820
55123
  /** Still valid right now (prunes expiry first). */
54821
55124
  isLive(sessionId) {
54822
55125
  this.prune();
@@ -54859,7 +55162,7 @@ var SessionRegistry = class {
54859
55162
 
54860
55163
  // server/brand.ts
54861
55164
  import { existsSync as existsSync24, readFileSync as readFileSync37 } from "node:fs";
54862
- import { join as join48 } from "node:path";
55165
+ import { join as join49 } from "node:path";
54863
55166
  var HEX_COLOUR = /^#[0-9a-fA-F]{6}$/;
54864
55167
  var MAX_LOGO_CHARS = 3e5;
54865
55168
  var brandSchema = external_exports.object({
@@ -54876,7 +55179,7 @@ var brandSchema = external_exports.object({
54876
55179
  }).strict();
54877
55180
  var DEFAULT_BRAND = { name: "OpenMausBot" };
54878
55181
  function brandFile() {
54879
- return process.env.OMB_BRAND_FILE || join48(DATA_DIR, "brand.json");
55182
+ return process.env.OMB_BRAND_FILE || join49(DATA_DIR, "brand.json");
54880
55183
  }
54881
55184
  function loadBrand(options = {}) {
54882
55185
  const file2 = options.file ?? brandFile();
@@ -57372,7 +57675,7 @@ function releaseDataDirLeaseAtExit() {
57372
57675
  }
57373
57676
  process.once("exit", releaseDataDirLeaseAtExit);
57374
57677
  var ENVIRONMENT_ID = loadEnvironmentId(DATA_DIR);
57375
- var sessions = new SessionRegistry({ file: join49(DATA_DIR, "sessions.json") });
57678
+ var sessions = new SessionRegistry({ file: join50(DATA_DIR, "sessions.json") });
57376
57679
  var SESSION_COOKIE = sessionCookieName(PORT, ENVIRONMENT_ID);
57377
57680
  var DESKTOP_MANAGED = process.env.OMB_DESKTOP_PARENT === "1";
57378
57681
  var desktopMutationToken = DESKTOP_MANAGED ? "" : void 0;
@@ -57382,7 +57685,7 @@ var cfg = loadConfig();
57382
57685
  var registry2 = new ProviderRegistry(BUILT_IN_DRIVERS);
57383
57686
  await registry2.load(instanceConfigs(cfg));
57384
57687
  var bundledSkills = loadBundledSkills();
57385
- var availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join49(DATA_DIR, "skills")));
57688
+ var availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join50(DATA_DIR, "skills")));
57386
57689
  var utilityParentPort = process.parentPort;
57387
57690
  function postDesktopPrivateMessage(message3) {
57388
57691
  if (!utilityParentPort) return false;
@@ -57408,7 +57711,7 @@ function applyDesktopMutationTokenMessage(raw) {
57408
57711
  return true;
57409
57712
  }
57410
57713
  var browserCleanup = new BrowserCleanupCoordinator({
57411
- file: join49(DATA_DIR, "browser-cleanups.json"),
57714
+ file: join50(DATA_DIR, "browser-cleanups.json"),
57412
57715
  send: (request) => {
57413
57716
  const status = browserEngineStatus();
57414
57717
  const sessions2 = request.type === "openmausbot:browser-bot-deleted" && request.botId ? [browserSessionId(request.botId, "")] : request.partitionId ? [browserSessionId("", request.partitionId)] : [];
@@ -57765,10 +58068,7 @@ function askBotAndWait(targetBotId, message3, depth, fromBotId) {
57765
58068
  });
57766
58069
  }
57767
58070
  async function defaultSelection() {
57768
- const described = await registry2.describe();
57769
- const available = described.filter((d) => d.snapshot.state === "available");
57770
- const pick2 = available.find((d) => d.driverKind === "claudeAgent") ?? available[0];
57771
- return { instanceId: pick2?.instanceId ?? "", model: pick2?.models.default ?? "" };
58071
+ return selectDefaultModelSelection(await registry2.describe(), cfg.defaultModelSelection);
57772
58072
  }
57773
58073
  function checkedModelSelection(raw, current2, requireAvailableModel = false) {
57774
58074
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
@@ -58538,12 +58838,16 @@ function broadcast(payload) {
58538
58838
  data: ${JSON.stringify({ ...payload, seq: seq2 })}
58539
58839
 
58540
58840
  `;
58541
- replayBuffer.push({ seq: seq2, kind, frame: kind === "screen" ? null : frame });
58841
+ const clientFrame = kind === "config" ? `id: ${STREAM_ID}:${seq2}
58842
+ data: ${JSON.stringify({ ...configForAccess(payload, false), seq: seq2 })}
58843
+
58844
+ ` : frame;
58845
+ replayBuffer.push({ seq: seq2, kind, frame: kind === "screen" ? null : frame, clientFrame: kind === "screen" ? null : clientFrame });
58542
58846
  if (replayBuffer.length > REPLAY_MAX) replayBuffer.shift();
58543
58847
  for (const client of [...sseClients]) {
58544
58848
  if (!wants(client, kind)) continue;
58545
58849
  try {
58546
- client.res.write(frame);
58850
+ client.res.write(client.admin ? frame : clientFrame);
58547
58851
  } catch {
58548
58852
  sseClients.delete(client);
58549
58853
  }
@@ -59042,7 +59346,7 @@ bus.subscribe((event) => {
59042
59346
  if (bot) {
59043
59347
  const touches = screenTouchingTool(toolName);
59044
59348
  if (touches || /computer|screenshot|click|type_text|press_key|scroll|open_url|wait_for|browser_/i.test(toolName)) {
59045
- pokeScreenPoller(bot.id, touches);
59349
+ pokeScreenPoller(bot.id, touches, screenSurfaceForTool(toolName));
59046
59350
  }
59047
59351
  }
59048
59352
  }
@@ -59606,45 +59910,29 @@ function drainQueuedSends() {
59606
59910
  var screenPollers = /* @__PURE__ */ new Map();
59607
59911
  var SCREEN_POLL_MS = 6e3;
59608
59912
  var SCREEN_MIN_GAP_MS = 3e3;
59609
- function startScreenPoller(botId, capture, { screenIsTheWork = false } = {}) {
59913
+ function startScreenPoller(botId, captures, { screenIsTheWork = false } = {}) {
59914
+ if (!captures.computer && !captures.browser) return;
59610
59915
  if (screenPollers.has(botId)) return;
59611
- let current2 = null;
59612
- let lastAt = 0;
59613
- const entry = {
59916
+ const entry = Object.assign(createScreenFrameSource({
59917
+ captures,
59918
+ control: () => ({
59919
+ held: computerControl.snapshot(botId).held,
59920
+ revision: computerControlRevision.get(botId) ?? 0
59921
+ }),
59922
+ onFrame: (frame) => broadcast({ kind: "screen", botId, ...frame }),
59923
+ minGapMs: SCREEN_MIN_GAP_MS
59924
+ }), {
59614
59925
  timer: null,
59615
- capture: () => {
59616
- if (computerControl.snapshot(botId).held) return Promise.resolve();
59617
- if (!current2 && Date.now() - lastAt < SCREEN_MIN_GAP_MS) return Promise.resolve();
59618
- current2 ??= (async () => {
59619
- try {
59620
- const frame = await captureOutsideHumanControl(
59621
- () => ({
59622
- held: computerControl.snapshot(botId).held,
59623
- revision: computerControlRevision.get(botId) ?? 0
59624
- }),
59625
- capture
59626
- );
59627
- if (!frame) return;
59628
- entry.last = frame;
59629
- broadcast({ kind: "screen", botId, ...frame });
59630
- } catch {
59631
- } finally {
59632
- lastAt = Date.now();
59633
- current2 = null;
59634
- }
59635
- })();
59636
- return current2;
59637
- },
59638
- last: null,
59639
59926
  touched: screenIsTheWork
59640
- };
59927
+ });
59641
59928
  entry.timer = setInterval(() => void entry.capture(), SCREEN_POLL_MS);
59642
59929
  screenPollers.set(botId, entry);
59643
59930
  }
59644
- function pokeScreenPoller(botId, touches) {
59931
+ function pokeScreenPoller(botId, touches, surface) {
59645
59932
  const entry = screenPollers.get(botId);
59646
59933
  if (!entry) return;
59647
59934
  if (touches) entry.touched = true;
59935
+ if (touches && surface) entry.surface = surface;
59648
59936
  void entry.capture();
59649
59937
  }
59650
59938
  function stopScreenPoller(botId) {
@@ -59666,7 +59954,7 @@ async function finalScreenFrame(botId, threadId) {
59666
59954
  if (entry.timer) clearInterval(entry.timer);
59667
59955
  screenPollers.delete(botId);
59668
59956
  if (!entry.touched) return null;
59669
- await entry.capture();
59957
+ await entry.capture(true);
59670
59958
  const frame = entry.last;
59671
59959
  if (!frame || !settledFrameIsNews(shownScreenHash(botId, threadId), frame.png)) return null;
59672
59960
  settledScreenHashes.set(botId, screenFrameHash(frame.png));
@@ -59837,6 +60125,7 @@ async function startTurn(botId, text6, opts) {
59837
60125
  }
59838
60126
  const wants2 = plan.computer;
59839
60127
  let previewCapture = null;
60128
+ let browserCapture = null;
59840
60129
  let computerKind = null;
59841
60130
  let autoVpsProblem = null;
59842
60131
  if (wants2 === "vm") {
@@ -60005,6 +60294,10 @@ async function startTurn(botId, text6, opts) {
60005
60294
  const selectedProfile = liveBot.browserProfile;
60006
60295
  browser = browserIntegration(bot.id, selectedProfile);
60007
60296
  if (browser) integrations.browser = browser.integration;
60297
+ if (browser) {
60298
+ const frame = { binaryPath: browser.integration.command, env: browser.integration.env };
60299
+ browserCapture = () => agentBrowserFrame(frame);
60300
+ }
60008
60301
  }
60009
60302
  await pendingCancelledProviderHandshakes.waitForClear(threadId);
60010
60303
  if (!markDirectTurnDispatching(bot.id, dispatchClaimId, threadId)) {
@@ -60066,8 +60359,12 @@ async function startTurn(botId, text6, opts) {
60066
60359
  if (!isExternalContextMarker(task2.lastInstanceId) || task2.lastInstanceId === externalContextMarker) {
60067
60360
  store.markTaskDispatched(bot.id, threadId, instanceId);
60068
60361
  }
60069
- if (previewCapture && store.bot(bot.id)?.busy) {
60070
- startScreenPoller(bot.id, previewCapture, { screenIsTheWork: instance.driverKind === "boxAgent" });
60362
+ if ((previewCapture || browserCapture) && store.bot(bot.id)?.busy) {
60363
+ startScreenPoller(
60364
+ bot.id,
60365
+ { ...previewCapture ? { computer: previewCapture } : {}, ...browserCapture ? { browser: browserCapture } : {} },
60366
+ { screenIsTheWork: instance.driverKind === "boxAgent" }
60367
+ );
60071
60368
  }
60072
60369
  if (store.bot(bot.id)?.busy !== true) {
60073
60370
  revokeInternalCapabilityGeneration(threadId, dispatchClaimId);
@@ -62286,6 +62583,15 @@ function configStatus() {
62286
62583
  browserProfiles: cfg.browserProfiles ?? []
62287
62584
  };
62288
62585
  }
62586
+ function configForAccess(status, admin) {
62587
+ if (admin) return status;
62588
+ return {
62589
+ ...status,
62590
+ vps: { configured: status.vps.configured, sshAlias: "" },
62591
+ profile: { name: status.profile.name, email: "" },
62592
+ browserProfiles: status.browserProfiles.map((profile) => Object.fromEntries(Object.entries(profile).filter(([key3]) => key3 !== "partitionId")))
62593
+ };
62594
+ }
62289
62595
  function mcpServerResponse() {
62290
62596
  return { servers: listMcpServers(cfg.mcpServers) };
62291
62597
  }
@@ -62332,7 +62638,7 @@ var claudeUpdatesInFlight = /* @__PURE__ */ new Set();
62332
62638
  function serveStatic(res, path) {
62333
62639
  if (!STATIC_DIR) return false;
62334
62640
  const safe = path === "/" ? "/index.html" : path.replace(/\.\./g, "");
62335
- const file2 = join49(STATIC_DIR, safe);
62641
+ const file2 = join50(STATIC_DIR, safe);
62336
62642
  try {
62337
62643
  const data = readFileSync38(file2);
62338
62644
  res.writeHead(200, { "content-type": MIME[extname5(file2)] ?? "application/octet-stream" });
@@ -62340,7 +62646,7 @@ function serveStatic(res, path) {
62340
62646
  return true;
62341
62647
  } catch {
62342
62648
  try {
62343
- const data = readFileSync38(join49(STATIC_DIR, "index.html"));
62649
+ const data = readFileSync38(join50(STATIC_DIR, "index.html"));
62344
62650
  res.writeHead(200, { "content-type": "text/html" });
62345
62651
  res.end(data);
62346
62652
  return true;
@@ -62388,7 +62694,12 @@ function readBody(req, limit = 1e6) {
62388
62694
  });
62389
62695
  }
62390
62696
  var handleRequest = async (req, res) => {
62391
- const url2 = new URL(req.url ?? "/", `http://localhost:${PORT}`);
62697
+ let url2;
62698
+ try {
62699
+ url2 = new URL(req.url ?? "/", `http://localhost:${PORT}`);
62700
+ } catch {
62701
+ return json3(res, 400, { error: "invalid request URL" });
62702
+ }
62392
62703
  const path = url2.pathname;
62393
62704
  const method = req.method ?? "GET";
62394
62705
  let m = null;
@@ -62414,7 +62725,7 @@ var handleRequest = async (req, res) => {
62414
62725
  const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED });
62415
62726
  if (wantsCookie) {
62416
62727
  const secure = requestOrigin(req)?.startsWith("https://") === true;
62417
- res.setHeader("set-cookie", serializeSessionCookie(SESSION_COOKIE, result.token, { secure, maxAgeSeconds: SESSION_TTL_MS / 1e3 }));
62728
+ res.setHeader("set-cookie", serializeSessionCookie(SESSION_COOKIE, result.token, { secure, maxAgeSeconds: cookieMaxAgeSeconds(result.session) }));
62418
62729
  return json3(res, 200, { session: result.session, environment });
62419
62730
  }
62420
62731
  return json3(res, 200, { token: result.token, session: result.session, environment });
@@ -62427,6 +62738,13 @@ var handleRequest = async (req, res) => {
62427
62738
  loopbackMutationToken: desktopMutationToken,
62428
62739
  companionMutationToken
62429
62740
  });
62741
+ if (gate.auth?.kind === "session" && gate.auth.via === "cookie") {
62742
+ const presented = parseCookies(req.headers.cookie).get(SESSION_COOKIE);
62743
+ if (presented) {
62744
+ const secure = requestOrigin(req)?.startsWith("https://") === true;
62745
+ res.setHeader("set-cookie", serializeSessionCookie(SESSION_COOKIE, presented, { secure, maxAgeSeconds: cookieMaxAgeSeconds(gate.auth.session) }));
62746
+ }
62747
+ }
62430
62748
  if (method === "GET" && path === "/api/health" && !gate.auth) {
62431
62749
  return json3(res, 200, { app: "openmausbot" });
62432
62750
  }
@@ -62472,7 +62790,7 @@ var handleRequest = async (req, res) => {
62472
62790
  hint: base2 ? null : "this server has no public address to put in a link: set OMB_PUBLIC_URL, or open /pair on the address you use and type the code"
62473
62791
  });
62474
62792
  }
62475
- if (method === "GET" && path === "/api/auth/pairing") return json3(res, 200, { pairings: sessions.openPairings() });
62793
+ if (method === "GET" && path === "/api/auth/pairing") return json3(res, 200, { pairings: sessions.openPairings(), publicUrl: PUBLIC_URL });
62476
62794
  m = path.match(/^\/api\/auth\/pairing\/([\w-]+)$/);
62477
62795
  if (m && method === "DELETE") {
62478
62796
  const cancelled = sessions.cancelPairing(m[1]);
@@ -62697,6 +63015,16 @@ var handleRequest = async (req, res) => {
62697
63015
  });
62698
63016
  return json3(res, 201, proposed);
62699
63017
  }
63018
+ const discloseRecall = (bot, roomThreadId, sourceThreadIds) => {
63019
+ const crossing = claimRecallCrossings(roomThreadId, sourceThreadIds);
63020
+ if (!crossing.count) return;
63021
+ store.appendMessage(roomThreadId, {
63022
+ role: "bot",
63023
+ kind: "activity",
63024
+ from: { botId: bot.id, name: bot.name, color: bot.color },
63025
+ tool: { name: recallCrossingLabel(bot.name, crossing.count), ok: true }
63026
+ });
63027
+ };
62700
63028
  if (method === "GET" && path === "/api/internal/session-search") {
62701
63029
  const fromBotId = String(url2.searchParams.get("fromBotId") ?? "");
62702
63030
  const from = store.bot(fromBotId);
@@ -62710,11 +63038,16 @@ var handleRequest = async (req, res) => {
62710
63038
  const rawLimit = Number(url2.searchParams.get("limit"));
62711
63039
  const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.trunc(rawLimit), 25) : 12;
62712
63040
  const ownThreads = [.../* @__PURE__ */ new Set([from.threadId, ...(from.tasks ?? []).map((task2) => task2.threadId)])];
63041
+ const inRoom = Boolean(store.groupByThread(fromThreadId));
62713
63042
  const hits = recallMessages(q, ownThreads, limit).map((hit) => ({
62714
63043
  ...hit,
62715
63044
  task: store.taskByThread(from.id, hit.threadId)?.title,
62716
- current: hit.threadId === fromThreadId
63045
+ current: hit.threadId === fromThreadId,
63046
+ crossed: inRoom && hit.threadId !== fromThreadId
62717
63047
  }));
63048
+ if (inRoom) {
63049
+ discloseRecall(from, fromThreadId, hits.filter((hit) => hit.crossed).map((hit) => hit.threadId));
63050
+ }
62718
63051
  return json3(res, 200, { hits });
62719
63052
  }
62720
63053
  if (method === "GET" && path === "/api/internal/session-read") {
@@ -62731,8 +63064,12 @@ var handleRequest = async (req, res) => {
62731
63064
  const own3 = threadId === from.threadId || Boolean(store.taskByThread(from.id, threadId));
62732
63065
  const message3 = own3 ? readMessageText(threadId, messageId) : null;
62733
63066
  if (!message3) return json3(res, 404, { error: "no such message in your conversations" });
63067
+ const readInRoom = Boolean(store.groupByThread(fromThreadId));
63068
+ const readCrossed = readInRoom && threadId !== fromThreadId;
63069
+ if (readCrossed) discloseRecall(from, fromThreadId, [threadId]);
62734
63070
  return json3(res, 200, {
62735
63071
  ...message3,
63072
+ crossed: readCrossed,
62736
63073
  text: message3.text.length > SESSION_READ_MAX_CHARS ? `${message3.text.slice(0, SESSION_READ_MAX_CHARS)}\u2026` : message3.text,
62737
63074
  task: store.taskByThread(from.id, threadId)?.title
62738
63075
  });
@@ -63421,7 +63758,7 @@ var handleRequest = async (req, res) => {
63421
63758
  return webhooks.remove(webhookMatch[1]) ? json3(res, 200, { ok: true }) : json3(res, 404, { error: "no such webhook" });
63422
63759
  }
63423
63760
  if (method === "GET" && path === "/api/events") {
63424
- const client = { res, screens: url2.searchParams.get("screens") !== "off" };
63761
+ const client = { res, admin: auth.scopes.includes("admin"), screens: url2.searchParams.get("screens") !== "off" };
63425
63762
  if (auth.kind === "session") client.sessionId = auth.session.id;
63426
63763
  res.writeHead(200, {
63427
63764
  "content-type": "text/event-stream",
@@ -63447,7 +63784,8 @@ var handleRequest = async (req, res) => {
63447
63784
  );
63448
63785
  if (resumed) {
63449
63786
  for (const buffered of replayBuffer) {
63450
- if (buffered.seq > since && buffered.frame && wants(client, buffered.kind)) res.write(buffered.frame);
63787
+ const frame = client.admin ? buffered.frame : buffered.clientFrame;
63788
+ if (buffered.seq > since && frame && wants(client, buffered.kind)) res.write(frame);
63451
63789
  }
63452
63790
  }
63453
63791
  sseClients.add(client);
@@ -64288,7 +64626,7 @@ data: ${JSON.stringify({ kind: "ping" })}
64288
64626
  for (const threadId of threadIds) {
64289
64627
  for (const dir of [EVENTS_DIR, NATIVE_DIR]) {
64290
64628
  try {
64291
- unlinkSync9(join49(dir, `${threadId}.ndjson`));
64629
+ unlinkSync9(join50(dir, `${threadId}.ndjson`));
64292
64630
  } catch {
64293
64631
  }
64294
64632
  }
@@ -65010,7 +65348,7 @@ data: ${JSON.stringify({ kind: "ping" })}
65010
65348
  }
65011
65349
  for (const dir of [EVENTS_DIR, NATIVE_DIR]) {
65012
65350
  try {
65013
- unlinkSync9(join49(dir, `${bot.threadId}.ndjson`));
65351
+ unlinkSync9(join50(dir, `${bot.threadId}.ndjson`));
65014
65352
  } catch {
65015
65353
  }
65016
65354
  }
@@ -66099,16 +66437,7 @@ data: ${JSON.stringify({ kind: "ping" })}
66099
66437
  }
66100
66438
  }
66101
66439
  if (method === "GET" && path === "/api/config") {
66102
- const status = configStatus();
66103
- if (auth.kind === "session" && !auth.scopes.includes("admin")) {
66104
- return json3(res, 200, {
66105
- ...status,
66106
- vps: { configured: status.vps.configured, sshAlias: "" },
66107
- profile: { name: status.profile.name, email: "" },
66108
- browserProfiles: status.browserProfiles.map((profile) => Object.fromEntries(Object.entries(profile).filter(([key3]) => key3 !== "partitionId")))
66109
- });
66110
- }
66111
- return json3(res, 200, status);
66440
+ return json3(res, 200, configForAccess(configStatus(), auth.scopes.includes("admin")));
66112
66441
  }
66113
66442
  if ((method === "PUT" || method === "PATCH") && path === "/api/config") {
66114
66443
  const body = await readBody(req);
@@ -66767,7 +67096,7 @@ server.listen(PORT, "127.0.0.1", () => {
66767
67096
  var TUNNEL_SOCKET = process.env.OMB_TUNNEL_SOCKET?.trim() || null;
66768
67097
  var tunnelListener = null;
66769
67098
  if (TUNNEL_SOCKET) {
66770
- if (process.platform !== "win32") rmSync6(TUNNEL_SOCKET, { force: true });
67099
+ if (process.platform !== "win32") rmSync7(TUNNEL_SOCKET, { force: true });
66771
67100
  tunnelListener = createServer3(handleRequest);
66772
67101
  tunnelListener.listen(TUNNEL_SOCKET, () => {
66773
67102
  console.log(`openmausbot tunnel listener on ${TUNNEL_SOCKET}`);