@mearl/native-host 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,8 @@ mearl-native-host [options]
45
45
  --daemon 独立 CDP daemon
46
46
  --cdp-port <port> 指定 CDP 端口
47
47
  --cdp-user-data-dir <dir> 指定 Chrome Profile
48
+ --agentbay-session-id <id> 连接 AgentBay CDP Session
49
+ --agentbay-idle-release-minutes <minutes> AgentBay 空闲回收时间(内部参数)
48
50
  --browser-id <id> 指定浏览器注册 ID
49
51
  --browser-name <name> 指定浏览器显示名称
50
52
  --version, -v 查看版本
@@ -63,6 +65,15 @@ Chrome Extension
63
65
 
64
66
  `browser_launch` 会为每个托管 Chrome 启动一个带独立 CDP 配置的 native-host 子进程,使多个 headless/Profile/TDBank 账号实例可以同时被现有浏览器注册表寻址。设计细节见 [托管浏览器与 TDBank 多账号设计](../../docs/managed-browsers.md)。
65
67
 
68
+ AgentBay 会话创建时使用官方 `LifecyclePolicy`:默认 30 分钟没有 Mearl 指令时由本地宿主执行优雅关闭,120 分钟达到云端最大运行时长时由 AgentBay 强制释放。优雅关闭会先同步持久化 Browser Context,再删除 Session 和本地注册记录;执行中的 Mearl action 不计为空闲。正常任务仍应主动调用 `browser_close`,超时只用于异常退出兜底。
69
+
70
+ 运维环境可覆盖默认值;两项均为正整数分钟,空闲时间至少为 3,最大运行时长不得小于空闲时间:
71
+
72
+ ```bash
73
+ MEARL_AGENTBAY_IDLE_RELEASE_MINUTES=30
74
+ MEARL_AGENTBAY_MAX_RUNTIME_MINUTES=120
75
+ ```
76
+
66
77
  ## 侧边栏 Agent Chat
67
78
 
68
79
  扩展侧边栏通过 Native Messaging 调用 native-host,再按用户选择启动持久的 Codex app-server 或 Qoder ACP 进程。两种 Provider 都支持流式消息、思考与工具进度、中断和审批,并复用各自 CLI 已保存的本地登录态。
@@ -9,6 +9,7 @@ export interface AgentBayBrowserRecord extends DaemonRecord {
9
9
  name: string;
10
10
  sessionId: string;
11
11
  imageId?: string;
12
+ userAgentMode?: 'default' | 'desktop';
12
13
  persistent?: boolean;
13
14
  contextId?: string;
14
15
  contextName?: string;
@@ -93,12 +94,21 @@ export interface AgentBayBrowserSummary {
93
94
  copiedCookies?: AgentBayBrowserRecord['copiedCookies'];
94
95
  running: boolean;
95
96
  }
97
+ export interface AgentBayBrowserLaunchResult extends AgentBayBrowserSummary {
98
+ created: boolean;
99
+ reused: boolean;
100
+ }
96
101
  export interface AgentBayBrowserManagerDeps {
97
102
  nativeHostScript: string;
98
103
  exportCookies: (domains: string[]) => Promise<TransferableCookie[]>;
99
104
  getBrowserUserAgent: () => Promise<string>;
100
105
  log: (...args: any[]) => void;
101
106
  createAgentBayClient?: (apiKey: string) => Promise<AgentBayClientLike>;
107
+ connectCdp?: ConnectAgentBayCdp;
108
+ findBrowserRecord?: (selector: string) => {
109
+ record: AgentBayBrowserRecord;
110
+ running: boolean;
111
+ } | null;
102
112
  persistBrowserRecord?: (record: AgentBayBrowserRecord) => void;
103
113
  resolveApiKey?: () => string;
104
114
  }
@@ -124,7 +134,8 @@ export declare function getAgentBayAccessUrl(sessionId: string, dependencies?: P
124
134
  export declare function resolveAgentBayEndpoint(sessionId: string): Promise<string>;
125
135
  export declare function keepAgentBaySessionAlive(sessionId: string): Promise<void>;
126
136
  export declare function initializeAgentBayBrowserSession(session: Pick<AgentBaySessionLike, 'browser'>, input: Pick<BrowserLaunchParams, 'url'>, cookies: TransferableCookie[], options?: AgentBayBrowserSessionInitOptions): Promise<number>;
127
- export declare function launchAgentBayBrowser(input: BrowserLaunchParams, deps: AgentBayBrowserManagerDeps): Promise<AgentBayBrowserSummary>;
137
+ export declare function reuseAgentBayBrowserSession(session: Pick<AgentBaySessionLike, 'browser'>, input: Pick<BrowserLaunchParams, 'url'>, cookies: TransferableCookie[], options?: Pick<AgentBayBrowserSessionInitOptions, 'connectCdp'>): Promise<number>;
138
+ export declare function launchAgentBayBrowser(input: BrowserLaunchParams, deps: AgentBayBrowserManagerDeps): Promise<AgentBayBrowserLaunchResult>;
128
139
  export declare function releaseAgentBayBrowserResources(record: AgentBayBrowserRecord, agentBay: AgentBayClientLike, options?: {
129
140
  deleteProfile?: boolean;
130
141
  persistRecord?: (record: AgentBayBrowserRecord) => void;
@@ -0,0 +1,38 @@
1
+ export declare const MIN_AGENTBAY_IDLE_RELEASE_MINUTES = 3;
2
+ export declare const DEFAULT_AGENTBAY_IDLE_RELEASE_MINUTES = 30;
3
+ export declare const DEFAULT_AGENTBAY_MAX_RUNTIME_MINUTES = 120;
4
+ export declare const DEFAULT_AGENTBAY_LIFECYCLE_CHECK_INTERVAL_MS = 60000;
5
+ export interface AgentBayLifecycleConfig {
6
+ idleReleaseTimeout: number;
7
+ maxRuntime: number;
8
+ }
9
+ interface AgentBaySessionWatchdogOptions {
10
+ idleTimeoutMs: number;
11
+ checkIntervalMs?: number;
12
+ now?: () => number;
13
+ keepAlive: () => Promise<void>;
14
+ close: () => Promise<void>;
15
+ log: (...args: unknown[]) => void;
16
+ }
17
+ export declare function resolveAgentBayLifecycleConfig(env?: NodeJS.ProcessEnv): AgentBayLifecycleConfig;
18
+ export declare class AgentBaySessionWatchdog {
19
+ private readonly idleTimeoutMs;
20
+ private readonly checkIntervalMs;
21
+ private readonly now;
22
+ private readonly keepAlive;
23
+ private readonly close;
24
+ private readonly log;
25
+ private activeRequests;
26
+ private lastActivityAt;
27
+ private timer;
28
+ private checking;
29
+ private closing;
30
+ private stopped;
31
+ constructor(options: AgentBaySessionWatchdogOptions);
32
+ start(): void;
33
+ stop(): void;
34
+ beginActivity(): boolean;
35
+ endActivity(): void;
36
+ checkNow(): Promise<void>;
37
+ }
38
+ export {};
@@ -4,7 +4,7 @@
4
4
  * Contains: HTTP helper (nodeFetch), cookie utilities, page-load waiters,
5
5
  * viewport helpers, and serialized function sources for page injection.
6
6
  */
7
- import { type PageClickPointerType } from '@mearl/browser-core';
7
+ import { type PageClickPointerType, type PageDragPoint, type PageDragPointerType } from '@mearl/browser-core';
8
8
  import type { PageSession } from './cdp-session.js';
9
9
  export declare const TYPE_INTO_ELEMENT_SRC: string;
10
10
  export declare const CLICK_ELEMENT_VIA_JS_SRC: string;
@@ -61,4 +61,7 @@ export declare function toCdpInputPoint(session: PageSession, x: number, y: numb
61
61
  y: number;
62
62
  }>;
63
63
  export declare function dispatchCdpClick(session: PageSession, x: number, y: number, pointerType: PageClickPointerType): Promise<void>;
64
+ export declare function dispatchCdpDrag(session: PageSession, from: PageDragPoint, to: PageDragPoint, pointerType: PageDragPointerType, durationMs: number): Promise<{
65
+ steps: number;
66
+ }>;
64
67
  export declare function resolveBackendNodeObjectId(session: PageSession, backendNodeId: number): Promise<string>;
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Covers: click, type, scroll, eval, press, wait, navigate, upload, device emulation.
5
5
  */
6
- import { type PageClickParams, type PageEvalParams, type PageHoverParams, type PageNavigateParams, type PagePressParams, type PageScrollParams, type PageTypeParams, type PageUploadParams, type PageWaitParams, type SetDeviceEmulationParams } from '@mearl/browser-core';
6
+ import { type PageClickParams, type PageDragParams, type PageDragResultData, type PageEvalParams, type PageHoverParams, type PageNavigateParams, type PagePressParams, type PageScrollParams, type PageTypeParams, type PageUploadParams, type PageWaitParams, type SetDeviceEmulationParams } from '@mearl/browser-core';
7
7
  import type { PageSession } from './cdp-session.js';
8
8
  export declare function cdpPageClick(session: PageSession, data: PageClickParams): Promise<any>;
9
+ export declare function cdpPageDrag(session: PageSession, data: PageDragParams): Promise<PageDragResultData>;
9
10
  export declare function cdpPageType(session: PageSession, data: PageTypeParams): Promise<any>;
10
11
  export declare function cdpPageScroll(session: PageSession, data: PageScrollParams): Promise<any>;
11
12
  export declare function cdpPageEval(session: PageSession, data: PageEvalParams): Promise<any>;
package/dist/index.js CHANGED
@@ -2068,6 +2068,83 @@ function getClickCoordsFromNode() {
2068
2068
  };
2069
2069
  }
2070
2070
 
2071
+ // ../browser-core/dist/pageDragging.js
2072
+ var PAGE_DRAG_DEFAULT_DURATION_MS = 400;
2073
+ var PAGE_DRAG_MAX_DURATION_MS = 5e3;
2074
+ var PAGE_DRAG_STEP_INTERVAL_MS = 25;
2075
+ var PAGE_DRAG_MIN_STEPS = 2;
2076
+ var PAGE_DRAG_MAX_STEPS = 60;
2077
+ function isPageDragPoint(value) {
2078
+ if (!value || typeof value !== "object")
2079
+ return false;
2080
+ const point = value;
2081
+ return Number.isFinite(point.x) && Number.isFinite(point.y) && point.x >= 0 && point.y >= 0;
2082
+ }
2083
+ function resolveEndpoint(value, name) {
2084
+ if (typeof value === "string" && value.trim().length > 0)
2085
+ return value.trim();
2086
+ if (isPageDragPoint(value))
2087
+ return { x: value.x, y: value.y };
2088
+ throw new Error(`page_drag: "${name}" must be a non-empty CSS/@ref selector or { x, y } viewport point.`);
2089
+ }
2090
+ function resolvePageDragInput(data) {
2091
+ if (!data || typeof data !== "object") {
2092
+ throw new Error("page_drag: payload is required");
2093
+ }
2094
+ if (data.dragMode !== void 0 && !["auto", "mouse", "touch"].includes(data.dragMode)) {
2095
+ throw new Error('page_drag: "dragMode" must be one of auto, mouse, touch');
2096
+ }
2097
+ const durationMs = data.durationMs ?? PAGE_DRAG_DEFAULT_DURATION_MS;
2098
+ if (!Number.isFinite(durationMs) || durationMs < 0 || durationMs > PAGE_DRAG_MAX_DURATION_MS) {
2099
+ throw new Error(`page_drag: "durationMs" must be between 0 and ${PAGE_DRAG_MAX_DURATION_MS}.`);
2100
+ }
2101
+ return {
2102
+ from: resolveEndpoint(data.from, "from"),
2103
+ to: resolveEndpoint(data.to, "to"),
2104
+ dragMode: data.dragMode ?? "auto",
2105
+ durationMs: Math.round(durationMs)
2106
+ };
2107
+ }
2108
+ function buildPageDragPath(from, to, durationMs) {
2109
+ const steps = Math.max(PAGE_DRAG_MIN_STEPS, Math.min(PAGE_DRAG_MAX_STEPS, Math.ceil(durationMs / PAGE_DRAG_STEP_INTERVAL_MS)));
2110
+ const points = Array.from({ length: steps }, (_, index) => {
2111
+ const ratio = (index + 1) / steps;
2112
+ return {
2113
+ x: from.x + (to.x - from.x) * ratio,
2114
+ y: from.y + (to.y - from.y) * ratio
2115
+ };
2116
+ });
2117
+ return { points, intervalMs: durationMs / steps };
2118
+ }
2119
+ function getDragCoordsFromNode() {
2120
+ const el = this.nodeType === Node.TEXT_NODE ? this.parentElement : this;
2121
+ if (!el || typeof el.getBoundingClientRect !== "function")
2122
+ return null;
2123
+ const rect = el.getBoundingClientRect();
2124
+ const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
2125
+ const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
2126
+ if (rect.width <= 0 || rect.height <= 0) {
2127
+ return { x: 0, y: 0, tag: "", text: "", error: "Element has no visible bounds" };
2128
+ }
2129
+ if (rect.right <= 0 || rect.bottom <= 0 || rect.left >= viewportWidth || rect.top >= viewportHeight) {
2130
+ return {
2131
+ x: 0,
2132
+ y: 0,
2133
+ tag: "",
2134
+ text: "",
2135
+ error: "Element is outside the current viewport; scroll it into view before dragging"
2136
+ };
2137
+ }
2138
+ const x = Math.min(Math.max(rect.left + rect.width / 2, 0), Math.max(viewportWidth - 1, 0));
2139
+ const y = Math.min(Math.max(rect.top + rect.height / 2, 0), Math.max(viewportHeight - 1, 0));
2140
+ return {
2141
+ x,
2142
+ y,
2143
+ tag: el.tagName?.toLowerCase() ?? "",
2144
+ text: (el.textContent ?? "").trim().slice(0, 100)
2145
+ };
2146
+ }
2147
+
2071
2148
  // ../browser-core/dist/axSnapshot.js
2072
2149
  var PAGE_SNAPSHOT_QUERY_MAX_MATCHES = 100;
2073
2150
  var PAGE_SNAPSHOT_QUERY_MAX_BYTES = 256 * 1024;
@@ -3372,6 +3449,7 @@ var SEND_REQUEST_MAX_TOTAL_FILE_BYTES = 40 * 1024 * 1024;
3372
3449
  var SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES = 7 * 1024 * 1024;
3373
3450
  var PAGE_ACT_ACTIONS = [
3374
3451
  "page_click",
3452
+ "page_drag",
3375
3453
  "page_type",
3376
3454
  "page_hover",
3377
3455
  "page_scroll",
@@ -3402,6 +3480,7 @@ var BROWSER_COMMAND_ACTIONS = {
3402
3480
  tab_close: true,
3403
3481
  tab_list: true,
3404
3482
  page_click: true,
3483
+ page_drag: true,
3405
3484
  page_type: true,
3406
3485
  page_scroll: true,
3407
3486
  page_hover: true,
@@ -3442,6 +3521,7 @@ var EXTENSION_BROWSER_ACTIONS = {
3442
3521
  tab_close: true,
3443
3522
  tab_list: true,
3444
3523
  page_click: true,
3524
+ page_drag: true,
3445
3525
  page_type: true,
3446
3526
  page_scroll: true,
3447
3527
  page_hover: true,
@@ -4339,6 +4419,12 @@ function resolveSetCookieTarget(currentUrl, input) {
4339
4419
  }
4340
4420
 
4341
4421
  // ../browser-core/dist/controlledFavicon.js
4422
+ function normalizeControlSource(source) {
4423
+ return source === "remote" || source === "cloud" ? "remote" : "local";
4424
+ }
4425
+ function toControlSourceWireValue(source) {
4426
+ return source === "remote" ? "cloud" : "local";
4427
+ }
4342
4428
  var BASE_ICON = `
4343
4429
  <rect width="128" height="128" rx="28" fill="#3273E8"/>
4344
4430
  <path d="M36 90V40L64 68L92 40V90" stroke="#FFFFFF" stroke-width="14"
@@ -4348,7 +4434,7 @@ var SOURCE_BADGES = {
4348
4434
  local: `
4349
4435
  <circle cx="104" cy="104" r="21" fill="#16A34A" stroke="#FFFFFF" stroke-width="6"/>
4350
4436
  `,
4351
- cloud: `
4437
+ remote: `
4352
4438
  <circle cx="104" cy="104" r="21" fill="#FF7A00" stroke="#FFFFFF" stroke-width="6"/>
4353
4439
  `
4354
4440
  };
@@ -5949,6 +6035,85 @@ async function dispatchCdpClick(session, x, y, pointerType) {
5949
6035
  clickCount: 1
5950
6036
  });
5951
6037
  }
6038
+ async function dispatchCdpDrag(session, from, to, pointerType, durationMs) {
6039
+ const [start, end] = await Promise.all([
6040
+ toCdpInputPoint(session, from.x, from.y),
6041
+ toCdpInputPoint(session, to.x, to.y)
6042
+ ]);
6043
+ const path15 = buildPageDragPath(start, end, durationMs);
6044
+ const waitForNextPoint = async () => {
6045
+ if (path15.intervalMs <= 0) return;
6046
+ await new Promise((resolve2) => setTimeout(resolve2, path15.intervalMs));
6047
+ };
6048
+ if (pointerType === "touch") {
6049
+ await sendCdpInputCommand(session, "Input.dispatchTouchEvent", {
6050
+ type: "touchStart",
6051
+ touchPoints: [start]
6052
+ });
6053
+ let primaryError2;
6054
+ try {
6055
+ for (const point of path15.points) {
6056
+ await waitForNextPoint();
6057
+ await sendCdpInputCommand(session, "Input.dispatchTouchEvent", {
6058
+ type: "touchMove",
6059
+ touchPoints: [point]
6060
+ });
6061
+ }
6062
+ } catch (error48) {
6063
+ primaryError2 = error48;
6064
+ throw error48;
6065
+ } finally {
6066
+ await sendCdpInputCommand(session, "Input.dispatchTouchEvent", {
6067
+ type: "touchEnd",
6068
+ touchPoints: []
6069
+ }).catch((error48) => {
6070
+ if (primaryError2 === void 0) throw error48;
6071
+ });
6072
+ }
6073
+ return { steps: path15.points.length };
6074
+ }
6075
+ await sendCdpInputCommand(session, "Input.dispatchMouseEvent", {
6076
+ type: "mouseMoved",
6077
+ x: start.x,
6078
+ y: start.y
6079
+ });
6080
+ await sendCdpInputCommand(session, "Input.dispatchMouseEvent", {
6081
+ type: "mousePressed",
6082
+ x: start.x,
6083
+ y: start.y,
6084
+ button: "left",
6085
+ buttons: 1,
6086
+ clickCount: 1
6087
+ });
6088
+ let primaryError;
6089
+ try {
6090
+ for (const point of path15.points) {
6091
+ await waitForNextPoint();
6092
+ await sendCdpInputCommand(session, "Input.dispatchMouseEvent", {
6093
+ type: "mouseMoved",
6094
+ x: point.x,
6095
+ y: point.y,
6096
+ button: "left",
6097
+ buttons: 1
6098
+ });
6099
+ }
6100
+ } catch (error48) {
6101
+ primaryError = error48;
6102
+ throw error48;
6103
+ } finally {
6104
+ await sendCdpInputCommand(session, "Input.dispatchMouseEvent", {
6105
+ type: "mouseReleased",
6106
+ x: end.x,
6107
+ y: end.y,
6108
+ button: "left",
6109
+ buttons: 0,
6110
+ clickCount: 1
6111
+ }).catch((error48) => {
6112
+ if (primaryError === void 0) throw error48;
6113
+ });
6114
+ }
6115
+ return { steps: path15.points.length };
6116
+ }
5952
6117
  async function sendCdpInputCommand(session, method, params) {
5953
6118
  try {
5954
6119
  await session.client.sendCommand(method, params, PAGE_CLICK_INPUT_TIMEOUT_MS);
@@ -7940,6 +8105,7 @@ registerActions([
7940
8105
  ]);
7941
8106
 
7942
8107
  // src/cdp-backend/interaction-handlers.ts
8108
+ var GET_DRAG_COORDS_FROM_NODE_SRC = getDragCoordsFromNode.toString();
7943
8109
  function resolveSnapshotRef(session, ref) {
7944
8110
  const entry = resolvePageRef(session, ref);
7945
8111
  if (!entry) {
@@ -8251,6 +8417,102 @@ ${candidates}`
8251
8417
  )
8252
8418
  );
8253
8419
  }
8420
+ async function scrollDragSourceIntoView(session, endpoint) {
8421
+ if (typeof endpoint !== "string") return;
8422
+ validatePageElementSelector(endpoint, "page_drag from");
8423
+ if (endpoint.startsWith("@e")) {
8424
+ const entry = resolveSnapshotRef(session, endpoint);
8425
+ const objectId = await resolveBackendNodeObjectId(session, entry.backendNodeId);
8426
+ await session.client.sendCommand("Runtime.callFunctionOn", {
8427
+ objectId,
8428
+ functionDeclaration: `function() {
8429
+ const el = this.nodeType === Node.TEXT_NODE ? this.parentElement : this;
8430
+ if (!el) return false;
8431
+ el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
8432
+ return true;
8433
+ }`,
8434
+ returnByValue: true
8435
+ });
8436
+ return;
8437
+ }
8438
+ const result = await session.client.sendCommand("Runtime.evaluate", {
8439
+ expression: `(() => {
8440
+ const selector = ${JSON.stringify(endpoint)};
8441
+ const el = document.querySelector(selector);
8442
+ if (!el) return { error: 'Element not found: ' + selector };
8443
+ el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
8444
+ return { scrolled: true };
8445
+ })()`,
8446
+ returnByValue: true
8447
+ });
8448
+ const value = result.result?.value;
8449
+ if (value?.error) throw new Error(`page_drag: ${value.error}`);
8450
+ }
8451
+ function unwrapDragElementCoords(value, endpoint) {
8452
+ if (!value) throw new Error(`page_drag: cannot resolve element coordinates for "${endpoint}"`);
8453
+ if (value.error) throw new Error(`page_drag: ${value.error}: "${endpoint}"`);
8454
+ return value;
8455
+ }
8456
+ async function resolveDragEndpoint(session, endpoint) {
8457
+ if (typeof endpoint !== "string") {
8458
+ return { requested: endpoint, point: endpoint };
8459
+ }
8460
+ validatePageElementSelector(endpoint, "page_drag endpoint");
8461
+ let coords;
8462
+ if (endpoint.startsWith("@e")) {
8463
+ const entry = resolveSnapshotRef(session, endpoint);
8464
+ const objectId = await resolveBackendNodeObjectId(session, entry.backendNodeId);
8465
+ const result = await session.client.sendCommand("Runtime.callFunctionOn", {
8466
+ objectId,
8467
+ functionDeclaration: GET_DRAG_COORDS_FROM_NODE_SRC,
8468
+ returnByValue: true
8469
+ });
8470
+ coords = unwrapDragElementCoords(result.result?.value, endpoint);
8471
+ } else {
8472
+ const result = await session.client.sendCommand("Runtime.evaluate", {
8473
+ expression: `(() => {
8474
+ const selector = ${JSON.stringify(endpoint)};
8475
+ const el = document.querySelector(selector);
8476
+ if (!el) return { error: 'Element not found: ' + selector };
8477
+ return (${GET_DRAG_COORDS_FROM_NODE_SRC}).call(el);
8478
+ })()`,
8479
+ returnByValue: true
8480
+ });
8481
+ coords = unwrapDragElementCoords(result.result?.value, endpoint);
8482
+ }
8483
+ return {
8484
+ requested: endpoint,
8485
+ point: { x: coords.x, y: coords.y },
8486
+ tag: coords.tag,
8487
+ text: coords.text
8488
+ };
8489
+ }
8490
+ async function cdpPageDrag(session, data) {
8491
+ const input = resolvePageDragInput(data);
8492
+ await scrollDragSourceIntoView(session, input.from);
8493
+ const from = await resolveDragEndpoint(session, input.from);
8494
+ const to = await resolveDragEndpoint(session, input.to);
8495
+ if (Math.hypot(to.point.x - from.point.x, to.point.y - from.point.y) < 1) {
8496
+ throw new Error("page_drag: resolved endpoints must be at least 1 viewport pixel apart");
8497
+ }
8498
+ const pointerType = input.dragMode === "auto" ? await prefersTouchInput(session) ? "touch" : "mouse" : input.dragMode;
8499
+ const { steps } = await dispatchCdpDrag(
8500
+ session,
8501
+ from.point,
8502
+ to.point,
8503
+ pointerType,
8504
+ input.durationMs
8505
+ );
8506
+ return {
8507
+ dragged: true,
8508
+ from,
8509
+ to,
8510
+ dispatchMode: "trusted-input",
8511
+ pointerType,
8512
+ durationMs: input.durationMs,
8513
+ steps
8514
+ };
8515
+ }
8254
8516
  async function cdpPageType(session, data) {
8255
8517
  const { selector, text, clearFirst = true } = data;
8256
8518
  if (!selector || typeof selector !== "string") {
@@ -8571,6 +8833,7 @@ async function cdpPageHover(session, data) {
8571
8833
  }
8572
8834
  registerActions([
8573
8835
  { name: "page_click", session: "page", handler: cdpPageClick },
8836
+ { name: "page_drag", session: "page", handler: cdpPageDrag },
8574
8837
  { name: "page_type", session: "page", handler: cdpPageType },
8575
8838
  { name: "page_hover", session: "page", handler: cdpPageHover },
8576
8839
  { name: "page_scroll", session: "page", handler: cdpPageScroll },
@@ -9373,6 +9636,12 @@ function acquireLaunchLock(browserId) {
9373
9636
  };
9374
9637
  }
9375
9638
  async function launchManagedBrowser(input, deps) {
9639
+ if (input.reuse !== void 0 && input.reuse !== "never" && input.reuse !== "prefer" && input.reuse !== "require") {
9640
+ throw new Error('reuse must be "never", "prefer", or "require"');
9641
+ }
9642
+ if (input.reuse === "prefer" || input.reuse === "require") {
9643
+ throw new Error("reuse is currently supported only by the AgentBay browser provider");
9644
+ }
9376
9645
  if (input.imageId !== void 0) {
9377
9646
  throw new Error("imageId is only supported by the AgentBay browser provider");
9378
9647
  }
@@ -9477,7 +9746,11 @@ async function launchManagedBrowser(input, deps) {
9477
9746
  recordWritten = true;
9478
9747
  await waitForBrowserRegistration(browserId, hostPid);
9479
9748
  deps.log(`managed browser launched: ${browserId}, chrome=${chromePid}, host=${hostPid}`);
9480
- return browserSummary(record2, true);
9749
+ return {
9750
+ ...browserSummary(record2, true),
9751
+ created: true,
9752
+ reused: false
9753
+ };
9481
9754
  } catch (error48) {
9482
9755
  session?.close();
9483
9756
  if (hostPid) await stopProcess(hostPid, `--browser-id ${browserId}`);
@@ -9513,6 +9786,127 @@ import { spawn as spawn4 } from "node:child_process";
9513
9786
  import { homedir as homedir2 } from "node:os";
9514
9787
  import { dirname as dirname3, join as join4, parse, resolve } from "node:path";
9515
9788
  import { fileURLToPath } from "node:url";
9789
+
9790
+ // src/agentbay-lifecycle.ts
9791
+ var MIN_AGENTBAY_IDLE_RELEASE_MINUTES = 3;
9792
+ var DEFAULT_AGENTBAY_IDLE_RELEASE_MINUTES = 30;
9793
+ var DEFAULT_AGENTBAY_MAX_RUNTIME_MINUTES = 120;
9794
+ var DEFAULT_AGENTBAY_LIFECYCLE_CHECK_INTERVAL_MS = 6e4;
9795
+ function positiveIntegerMinutes(value, name, fallback) {
9796
+ if (value === void 0 || !value.trim()) return fallback;
9797
+ const minutes = Number(value);
9798
+ if (!Number.isInteger(minutes) || minutes <= 0) {
9799
+ throw new Error(`${name} must be a positive integer in minutes`);
9800
+ }
9801
+ return minutes;
9802
+ }
9803
+ function resolveAgentBayLifecycleConfig(env = process.env) {
9804
+ const idleReleaseTimeout = positiveIntegerMinutes(
9805
+ env.MEARL_AGENTBAY_IDLE_RELEASE_MINUTES,
9806
+ "MEARL_AGENTBAY_IDLE_RELEASE_MINUTES",
9807
+ DEFAULT_AGENTBAY_IDLE_RELEASE_MINUTES
9808
+ );
9809
+ const maxRuntime = positiveIntegerMinutes(
9810
+ env.MEARL_AGENTBAY_MAX_RUNTIME_MINUTES,
9811
+ "MEARL_AGENTBAY_MAX_RUNTIME_MINUTES",
9812
+ DEFAULT_AGENTBAY_MAX_RUNTIME_MINUTES
9813
+ );
9814
+ if (idleReleaseTimeout < MIN_AGENTBAY_IDLE_RELEASE_MINUTES) {
9815
+ throw new Error(
9816
+ `MEARL_AGENTBAY_IDLE_RELEASE_MINUTES must be at least ${MIN_AGENTBAY_IDLE_RELEASE_MINUTES}`
9817
+ );
9818
+ }
9819
+ if (maxRuntime < idleReleaseTimeout) {
9820
+ throw new Error(
9821
+ "MEARL_AGENTBAY_MAX_RUNTIME_MINUTES must be greater than or equal to MEARL_AGENTBAY_IDLE_RELEASE_MINUTES"
9822
+ );
9823
+ }
9824
+ return { idleReleaseTimeout, maxRuntime };
9825
+ }
9826
+ var AgentBaySessionWatchdog = class {
9827
+ idleTimeoutMs;
9828
+ checkIntervalMs;
9829
+ now;
9830
+ keepAlive;
9831
+ close;
9832
+ log;
9833
+ activeRequests = 0;
9834
+ lastActivityAt;
9835
+ timer = null;
9836
+ checking = false;
9837
+ closing = false;
9838
+ stopped = false;
9839
+ constructor(options) {
9840
+ if (!Number.isFinite(options.idleTimeoutMs) || options.idleTimeoutMs <= 0) {
9841
+ throw new Error("AgentBay idle timeout must be positive");
9842
+ }
9843
+ this.idleTimeoutMs = options.idleTimeoutMs;
9844
+ this.checkIntervalMs = options.checkIntervalMs ?? DEFAULT_AGENTBAY_LIFECYCLE_CHECK_INTERVAL_MS;
9845
+ if (!Number.isFinite(this.checkIntervalMs) || this.checkIntervalMs <= 0) {
9846
+ throw new Error("AgentBay lifecycle check interval must be positive");
9847
+ }
9848
+ this.now = options.now ?? Date.now;
9849
+ this.keepAlive = options.keepAlive;
9850
+ this.close = options.close;
9851
+ this.log = options.log;
9852
+ this.lastActivityAt = this.now();
9853
+ }
9854
+ start() {
9855
+ if (this.timer || this.stopped) return;
9856
+ this.timer = setInterval(() => void this.checkNow(), this.checkIntervalMs);
9857
+ this.timer.unref();
9858
+ }
9859
+ stop() {
9860
+ this.stopped = true;
9861
+ if (this.timer) {
9862
+ clearInterval(this.timer);
9863
+ this.timer = null;
9864
+ }
9865
+ }
9866
+ beginActivity() {
9867
+ if (this.stopped || this.closing) return false;
9868
+ this.activeRequests += 1;
9869
+ this.lastActivityAt = this.now();
9870
+ return true;
9871
+ }
9872
+ endActivity() {
9873
+ if (this.activeRequests > 0) this.activeRequests -= 1;
9874
+ this.lastActivityAt = this.now();
9875
+ }
9876
+ async checkNow() {
9877
+ if (this.stopped || this.checking || this.closing) return;
9878
+ this.checking = true;
9879
+ try {
9880
+ const idleForMs = this.now() - this.lastActivityAt;
9881
+ if (this.activeRequests > 0 || idleForMs < this.idleTimeoutMs) {
9882
+ try {
9883
+ await this.keepAlive();
9884
+ } catch (error48) {
9885
+ this.log(
9886
+ "AgentBay keepalive failed:",
9887
+ error48 instanceof Error ? error48.message : String(error48)
9888
+ );
9889
+ }
9890
+ return;
9891
+ }
9892
+ this.closing = true;
9893
+ this.log(`AgentBay browser reached the idle timeout after ${idleForMs}ms; closing`);
9894
+ try {
9895
+ await this.close();
9896
+ } catch (error48) {
9897
+ this.closing = false;
9898
+ this.log(
9899
+ "AgentBay idle close failed:",
9900
+ error48 instanceof Error ? error48.message : String(error48)
9901
+ );
9902
+ }
9903
+ } finally {
9904
+ this.checking = false;
9905
+ }
9906
+ }
9907
+ };
9908
+
9909
+ // src/agentbay-browser.ts
9516
9910
  var AGENTBAY_BROWSERS_SUBDIR = "agentbay-browsers";
9517
9911
  var DEFAULT_AGENTBAY_IMAGE_ID = "imgc-0aae4rgl8kvagdv1j";
9518
9912
  var sdkPromise = null;
@@ -9589,6 +9983,24 @@ function validateAgentBayInput(input) {
9589
9983
  throw new Error("TDBank login parameters are only supported by the local browser provider");
9590
9984
  }
9591
9985
  }
9986
+ function resolveReuseMode(value) {
9987
+ if (value === void 0) return "never";
9988
+ if (value === "never" || value === "prefer" || value === "require") return value;
9989
+ throw new Error('reuse must be "never", "prefer", or "require"');
9990
+ }
9991
+ function validateReusableAgentBayConfiguration(input, record2) {
9992
+ if (input.imageId !== void 0 && resolveAgentBayImageId(input.imageId) !== record2.imageId) {
9993
+ throw new Error(`Cannot change imageId while reusing AgentBay browser: ${record2.browserId}`);
9994
+ }
9995
+ if (input.persistent !== void 0 && input.persistent !== (record2.persistent === true)) {
9996
+ throw new Error(`Cannot change persistent while reusing AgentBay browser: ${record2.browserId}`);
9997
+ }
9998
+ if (input.userAgentMode !== void 0 && record2.userAgentMode !== void 0 && input.userAgentMode !== record2.userAgentMode) {
9999
+ throw new Error(
10000
+ `Cannot change userAgentMode while reusing AgentBay browser: ${record2.browserId}`
10001
+ );
10002
+ }
10003
+ }
9592
10004
  function resolveAgentBayImageId(value) {
9593
10005
  if (value === void 0) return DEFAULT_AGENTBAY_IMAGE_ID;
9594
10006
  const imageId = typeof value === "string" ? value.trim() : "";
@@ -9647,11 +10059,14 @@ function listRecords2() {
9647
10059
  }
9648
10060
  return records;
9649
10061
  }
9650
- function findRecord(selector) {
10062
+ function findRecordWithStatus(selector) {
9651
10063
  const normalized = selector.trim().toLowerCase();
9652
10064
  return listRecords2().find(
9653
10065
  ({ record: record2 }) => record2.browserId.toLowerCase() === normalized || record2.name.toLowerCase() === normalized
9654
- )?.record ?? null;
10066
+ ) ?? null;
10067
+ }
10068
+ function findRecord(selector) {
10069
+ return findRecordWithStatus(selector)?.record ?? null;
9655
10070
  }
9656
10071
  function hasAgentBayBrowser(selector) {
9657
10072
  return findRecord(selector) !== null;
@@ -9723,13 +10138,9 @@ async function keepAgentBaySessionAlive(sessionId) {
9723
10138
  const result = await session.keepAlive();
9724
10139
  if (result.success === false) throw new Error(result.errorMessage || "AgentBay keepalive failed");
9725
10140
  }
9726
- async function initializeAgentBayBrowserSession(session, input, cookies, options = {}) {
9727
- const initialized = await session.browser.initializeAsync(
9728
- options.userAgent ? { userAgent: options.userAgent } : {}
9729
- );
9730
- if (!initialized) throw new Error("AgentBay browser initialization failed");
10141
+ async function configureAgentBayBrowserSession(session, input, cookies, connectCdp = (url2) => CdpClient.connect(url2)) {
9731
10142
  const endpoint = await session.browser.getEndpointUrl();
9732
- const cdp = await (options.connectCdp ?? ((url2) => CdpClient.connect(url2)))(endpoint);
10143
+ const cdp = await connectCdp(endpoint);
9733
10144
  try {
9734
10145
  await cdp.sendCommand("Browser.getVersion", {}, 1e4);
9735
10146
  const copiedCookieCount = await cdpImportCookiesWithClient(cdp, cookies);
@@ -9741,32 +10152,76 @@ async function initializeAgentBayBrowserSession(session, input, cookies, options
9741
10152
  cdp.close();
9742
10153
  }
9743
10154
  }
10155
+ async function initializeAgentBayBrowserSession(session, input, cookies, options = {}) {
10156
+ const initialized = await session.browser.initializeAsync(
10157
+ options.userAgent ? { userAgent: options.userAgent } : {}
10158
+ );
10159
+ if (!initialized) throw new Error("AgentBay browser initialization failed");
10160
+ return configureAgentBayBrowserSession(session, input, cookies, options.connectCdp);
10161
+ }
10162
+ async function reuseAgentBayBrowserSession(session, input, cookies, options = {}) {
10163
+ return configureAgentBayBrowserSession(session, input, cookies, options.connectCdp);
10164
+ }
9744
10165
  async function launchAgentBayBrowser(input, deps) {
9745
10166
  const name = validateName(input.name);
9746
10167
  const browserId = agentBayBrowserId(name);
9747
10168
  validateAgentBayInput(input);
10169
+ const reuseMode = resolveReuseMode(input.reuse);
9748
10170
  const imageId = resolveAgentBayImageId(input.imageId);
9749
10171
  const persistent = input.persistent === true;
9750
10172
  const userAgentMode = resolveUserAgentMode(input.userAgentMode);
9751
10173
  const copyCookieDomains = input.copyCookieDomains === void 0 ? [] : normalizeCookieDomains(input.copyCookieDomains);
9752
10174
  const apiKey = (deps.resolveApiKey ?? resolveAgentBayApiKey)();
10175
+ const lifecycle = resolveAgentBayLifecycleConfig();
9753
10176
  const releaseLock = acquireLaunchLock(browserId);
9754
10177
  const persistBrowserRecord = deps.persistBrowserRecord ?? ((record2) => writeRecord(keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, record2.browserId), record2));
10178
+ const findBrowserRecord = deps.findBrowserRecord ?? findRecordWithStatus;
9755
10179
  let session = null;
9756
10180
  let context;
9757
10181
  let hostPid = 0;
9758
10182
  let recordWritten = false;
9759
10183
  try {
9760
- if (findRecord(browserId)) throw new Error(`AgentBay browser already exists: ${browserId}`);
10184
+ const existing = findBrowserRecord(browserId);
10185
+ if (existing) {
10186
+ if (reuseMode === "never") {
10187
+ throw new Error(`AgentBay browser already exists: ${browserId}`);
10188
+ }
10189
+ if (!existing.running) {
10190
+ throw new Error(`AgentBay browser is not running and cannot be reused: ${browserId}`);
10191
+ }
10192
+ validateReusableAgentBayConfiguration(input, existing.record);
10193
+ const cookies2 = copyCookieDomains.length > 0 ? await deps.exportCookies(copyCookieDomains) : [];
10194
+ const agentBay2 = await (deps.createAgentBayClient ?? createClient)(apiKey);
10195
+ const existingSession = await requireSessionFromClient(agentBay2, existing.record.sessionId);
10196
+ const copiedCookieCount2 = await reuseAgentBayBrowserSession(existingSession, input, cookies2, {
10197
+ connectCdp: deps.connectCdp
10198
+ });
10199
+ const record3 = {
10200
+ ...existing.record,
10201
+ ...copyCookieDomains.length > 0 ? { copiedCookies: { domains: copyCookieDomains, count: copiedCookieCount2 } } : {}
10202
+ };
10203
+ if (copyCookieDomains.length > 0) persistBrowserRecord(record3);
10204
+ deps.log(`AgentBay browser reused: ${browserId}, host=${record3.pid}`);
10205
+ return {
10206
+ ...browserSummary2(record3, true),
10207
+ created: false,
10208
+ reused: true
10209
+ };
10210
+ }
10211
+ if (reuseMode === "require") {
10212
+ throw new Error(`AgentBay browser is not running and reuse=require: ${browserId}`);
10213
+ }
9761
10214
  const [cookies, userAgent] = await Promise.all([
9762
10215
  copyCookieDomains.length > 0 ? deps.exportCookies(copyCookieDomains) : Promise.resolve([]),
9763
10216
  userAgentMode === "desktop" ? deps.getBrowserUserAgent().then(normalizeDesktopUserAgent) : Promise.resolve(void 0)
9764
10217
  ]);
9765
10218
  const agentBay = await (deps.createAgentBayClient ?? createClient)(apiKey);
9766
10219
  context = persistent ? await resolvePersistentContext(agentBay, browserId) : void 0;
10220
+ const { LifecyclePolicy } = await loadSdk();
9767
10221
  const created = await agentBay.create({
9768
10222
  imageId,
9769
10223
  framework: "mearl",
10224
+ lifecyclePolicy: new LifecyclePolicy(lifecycle),
9770
10225
  ...context ? {
9771
10226
  browserContext: {
9772
10227
  contextId: context.id,
@@ -9780,7 +10235,8 @@ async function launchAgentBayBrowser(input, deps) {
9780
10235
  }
9781
10236
  session = created.session;
9782
10237
  const copiedCookieCount = await initializeAgentBayBrowserSession(session, input, cookies, {
9783
- userAgent
10238
+ userAgent,
10239
+ connectCdp: deps.connectCdp
9784
10240
  });
9785
10241
  const host = spawn4(
9786
10242
  process.execPath,
@@ -9792,7 +10248,9 @@ async function launchAgentBayBrowser(input, deps) {
9792
10248
  "--browser-id",
9793
10249
  browserId,
9794
10250
  "--browser-name",
9795
- name
10251
+ name,
10252
+ "--agentbay-idle-release-minutes",
10253
+ String(lifecycle.idleReleaseTimeout)
9796
10254
  ],
9797
10255
  {
9798
10256
  detached: true,
@@ -9813,6 +10271,7 @@ async function launchAgentBayBrowser(input, deps) {
9813
10271
  name,
9814
10272
  sessionId: session.sessionId,
9815
10273
  imageId,
10274
+ userAgentMode,
9816
10275
  persistent,
9817
10276
  ...context ? { contextId: context.id, contextName: context.name } : {},
9818
10277
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -9822,7 +10281,11 @@ async function launchAgentBayBrowser(input, deps) {
9822
10281
  recordWritten = true;
9823
10282
  await waitForBrowserRegistration(browserId, hostPid);
9824
10283
  deps.log(`AgentBay browser launched: ${browserId}, host=${hostPid}`);
9825
- return browserSummary2(record2, true);
10284
+ return {
10285
+ ...browserSummary2(record2, true),
10286
+ created: true,
10287
+ reused: false
10288
+ };
9826
10289
  } catch (error48) {
9827
10290
  if (hostPid) await stopProcess(hostPid, `--browser-id ${browserId}`);
9828
10291
  if (recordWritten) {
@@ -9846,6 +10309,7 @@ async function launchAgentBayBrowser(input, deps) {
9846
10309
  name,
9847
10310
  sessionId: session.sessionId,
9848
10311
  imageId,
10312
+ userAgentMode,
9849
10313
  persistent,
9850
10314
  ...context ? { contextId: context.id, contextName: context.name } : {},
9851
10315
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -9940,31 +10404,38 @@ async function releaseAgentBayBrowserResources(record2, agentBay, options = {})
9940
10404
  };
9941
10405
  }
9942
10406
  async function closeAgentBayBrowser(selector, options = {}) {
9943
- const record2 = findRecord(selector);
10407
+ let record2 = findRecord(selector);
9944
10408
  if (!record2) throw new Error(`AgentBay browser not found: ${selector}`);
9945
10409
  const agentBay = await createClient(resolveAgentBayApiKey());
9946
- const released = await releaseAgentBayBrowserResources(record2, agentBay, {
9947
- ...options,
9948
- persistRecord: (updatedRecord) => writeRecord(
9949
- keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, updatedRecord.browserId),
9950
- updatedRecord
9951
- )
9952
- });
9953
- const closingOwnHost = record2.pid === process.pid;
9954
- if (!closingOwnHost) await stopProcess(record2.pid, `--browser-id ${record2.browserId}`);
9955
- removeRecord(keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, record2.browserId));
9956
- removeRecord(keyedConfigFile("browsers", record2.browserId));
9957
- if (closingOwnHost) {
9958
- const timer = setTimeout(() => process.kill(process.pid, "SIGTERM"), 250);
9959
- timer.unref();
10410
+ const releaseLock = acquireLaunchLock(record2.browserId);
10411
+ try {
10412
+ record2 = findRecord(record2.browserId);
10413
+ if (!record2) throw new Error(`AgentBay browser not found: ${selector}`);
10414
+ const released = await releaseAgentBayBrowserResources(record2, agentBay, {
10415
+ ...options,
10416
+ persistRecord: (updatedRecord) => writeRecord(
10417
+ keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, updatedRecord.browserId),
10418
+ updatedRecord
10419
+ )
10420
+ });
10421
+ const closingOwnHost = record2.pid === process.pid;
10422
+ if (!closingOwnHost) await stopProcess(record2.pid, `--browser-id ${record2.browserId}`);
10423
+ removeRecord(keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, record2.browserId));
10424
+ removeRecord(keyedConfigFile("browsers", record2.browserId));
10425
+ if (closingOwnHost) {
10426
+ const timer = setTimeout(() => process.kill(process.pid, "SIGTERM"), 250);
10427
+ timer.unref();
10428
+ }
10429
+ return {
10430
+ browserId: record2.browserId,
10431
+ provider: "agentbay",
10432
+ closed: true,
10433
+ ...released,
10434
+ ...record2.persistent === true ? { profileDeleted: options.deleteProfile === true } : { profileDeleted: true }
10435
+ };
10436
+ } finally {
10437
+ releaseLock();
9960
10438
  }
9961
- return {
9962
- browserId: record2.browserId,
9963
- provider: "agentbay",
9964
- closed: true,
9965
- ...released,
9966
- ...record2.persistent === true ? { profileDeleted: options.deleteProfile === true } : { profileDeleted: true }
9967
- };
9968
10439
  }
9969
10440
 
9970
10441
  // src/browser-registry.ts
@@ -10163,7 +10634,7 @@ import os2 from "node:os";
10163
10634
  import { randomUUID as randomUUID4 } from "node:crypto";
10164
10635
 
10165
10636
  // src/version.ts
10166
- var NATIVE_HOST_VERSION = true ? "2.6.0" : "unknown";
10637
+ var NATIVE_HOST_VERSION = true ? "2.7.0" : "unknown";
10167
10638
 
10168
10639
  // src/analytics.ts
10169
10640
  var MEASUREMENT_ID = "G-W84LJDZ6PH";
@@ -30153,6 +30624,7 @@ var AgentSkillManager = class {
30153
30624
  "--agent",
30154
30625
  providerId,
30155
30626
  scope === "global" ? "--global" : "--project",
30627
+ "--full-depth",
30156
30628
  "--yes"
30157
30629
  ];
30158
30630
  const output = await this.run(executable, args, cwd);
@@ -30229,7 +30701,7 @@ function createExtensionRequestMessage(id, action, data, trustedInternal = false
30229
30701
  id,
30230
30702
  action,
30231
30703
  data,
30232
- controlSource,
30704
+ controlSource: toControlSourceWireValue(controlSource),
30233
30705
  ...trustedInternal ? { trustedInternal: true } : {}
30234
30706
  };
30235
30707
  }
@@ -30654,9 +31126,22 @@ function positivePort(value, fallback) {
30654
31126
  }
30655
31127
  return port;
30656
31128
  }
31129
+ function agentBayIdleReleaseMinutes(value) {
31130
+ if (value === void 0) return void 0;
31131
+ const minutes = Number(value);
31132
+ if (!Number.isInteger(minutes) || minutes < MIN_AGENTBAY_IDLE_RELEASE_MINUTES) {
31133
+ throw new Error(
31134
+ `Invalid --agentbay-idle-release-minutes: ${value}. Expected an integer of at least ${MIN_AGENTBAY_IDLE_RELEASE_MINUTES}.`
31135
+ );
31136
+ }
31137
+ return minutes;
31138
+ }
30657
31139
  var CDP_PORT = positivePort(cliOption("--cdp-port"), 9222);
30658
31140
  var CDP_USER_DATA_DIR = cliOption("--cdp-user-data-dir");
30659
31141
  var AGENTBAY_SESSION_ID = cliOption("--agentbay-session-id");
31142
+ var AGENTBAY_IDLE_RELEASE_MINUTES = agentBayIdleReleaseMinutes(
31143
+ cliOption("--agentbay-idle-release-minutes")
31144
+ );
30660
31145
  var CDP_BROWSER_ID = cliOption("--browser-id") ?? "cdp";
30661
31146
  var CDP_BROWSER_NAME = cliOption("--browser-name") ?? "CDP";
30662
31147
  if (cliArg === "--init" || cliArg === "--uninstall") {
@@ -30684,6 +31169,8 @@ Options:
30684
31169
  --cdp-port <port> CDP port for daemon mode (default 9222)
30685
31170
  --cdp-user-data-dir <dir> Read DevToolsActivePort from this Chrome profile
30686
31171
  --agentbay-session-id <id> Connect to an AgentBay browser session over CDP
31172
+ --agentbay-idle-release-minutes <minutes>
31173
+ Close an idle AgentBay browser (minimum 3 minutes)
30687
31174
  --browser-id <id> Registry id for this CDP browser
30688
31175
  --browser-name <name> Human-readable registry name
30689
31176
  --version, -v Show version
@@ -30808,7 +31295,7 @@ function buildVersionWarning(clientVersion) {
30808
31295
  versionList,
30809
31296
  "",
30810
31297
  "This may cause unexpected behavior. To update:",
30811
- " npm install -g @mearl/native-host@latest @mearl/client@latest",
31298
+ " npx @mearl/setup update --local",
30812
31299
  ` Browser extension: ${EXTENSION_INSTALL_URL}`
30813
31300
  ].join("\n");
30814
31301
  }
@@ -30818,6 +31305,7 @@ var socketServer = null;
30818
31305
  var legacyServer = null;
30819
31306
  var inventoryExitTimer = null;
30820
31307
  var cdpRegistrationTimer = null;
31308
+ var agentBaySessionWatchdog = null;
30821
31309
  function scheduleInventoryExit() {
30822
31310
  if (inventoryExitTimer) clearTimeout(inventoryExitTimer);
30823
31311
  inventoryExitTimer = setTimeout(() => cleanupAndExit(0), INVENTORY_IDLE_EXIT_MS);
@@ -31085,7 +31573,7 @@ function toTimeoutMs(action, data) {
31085
31573
  }
31086
31574
  return DEFAULT_TIMEOUT_MS;
31087
31575
  }
31088
- async function handleSocketLine(socket, line) {
31576
+ async function handleSocketRequest(socket, line) {
31089
31577
  let request;
31090
31578
  try {
31091
31579
  request = JSON.parse(line);
@@ -31096,7 +31584,7 @@ async function handleSocketLine(socket, line) {
31096
31584
  return;
31097
31585
  }
31098
31586
  const { id = "", action, data = {}, version: clientVersion } = request;
31099
- const controlSource = request.controlSource === "cloud" ? "cloud" : "local";
31587
+ const controlSource = normalizeControlSource(request.controlSource);
31100
31588
  if (action === "start_cloud_connector") {
31101
31589
  const serverUrl = data?.serverUrl;
31102
31590
  if (!serverUrl || typeof serverUrl !== "string") {
@@ -31386,6 +31874,33 @@ async function handleSocketLine(socket, line) {
31386
31874
  socket.write(JSON.stringify({ id, success: false, error: message }) + "\n");
31387
31875
  }
31388
31876
  }
31877
+ async function handleSocketLine(socket, line) {
31878
+ const watchdog = agentBaySessionWatchdog;
31879
+ if (!watchdog) {
31880
+ await handleSocketRequest(socket, line);
31881
+ return;
31882
+ }
31883
+ if (!watchdog.beginActivity()) {
31884
+ let id = "";
31885
+ try {
31886
+ id = JSON.parse(line)?.id ?? "";
31887
+ } catch {
31888
+ }
31889
+ socket.write(
31890
+ JSON.stringify({
31891
+ id,
31892
+ success: false,
31893
+ error: "AgentBay browser is closing after reaching its idle timeout"
31894
+ }) + "\n"
31895
+ );
31896
+ return;
31897
+ }
31898
+ try {
31899
+ await handleSocketRequest(socket, line);
31900
+ } finally {
31901
+ watchdog.endActivity();
31902
+ }
31903
+ }
31389
31904
  async function handleViaCdp(action, data, _timeoutMs, controlSource) {
31390
31905
  if (action === "set_rule" || action === "get_rules") {
31391
31906
  throw new Error(
@@ -31544,6 +32059,8 @@ function cleanupAndExit(code) {
31544
32059
  clearInterval(cdpRegistrationTimer);
31545
32060
  cdpRegistrationTimer = null;
31546
32061
  }
32062
+ agentBaySessionWatchdog?.stop();
32063
+ agentBaySessionWatchdog = null;
31547
32064
  cdpSession?.close();
31548
32065
  agentChatManager?.dispose();
31549
32066
  agentChatManager = null;
@@ -31630,15 +32147,17 @@ async function main() {
31630
32147
  await monitorDaemonBrowser();
31631
32148
  }
31632
32149
  if (AGENTBAY_SESSION_ID) {
31633
- const keepAliveTimer = setInterval(() => {
31634
- keepAgentBaySessionAlive(AGENTBAY_SESSION_ID).catch((error48) => {
31635
- log(
31636
- `AgentBay keepalive failed for ${AGENTBAY_SESSION_ID}:`,
31637
- error48 instanceof Error ? error48.message : String(error48)
31638
- );
31639
- });
31640
- }, 6e4);
31641
- keepAliveTimer.unref();
32150
+ const idleReleaseMinutes = AGENTBAY_IDLE_RELEASE_MINUTES ?? resolveAgentBayLifecycleConfig().idleReleaseTimeout;
32151
+ agentBaySessionWatchdog = new AgentBaySessionWatchdog({
32152
+ idleTimeoutMs: idleReleaseMinutes * 6e4,
32153
+ keepAlive: () => keepAgentBaySessionAlive(AGENTBAY_SESSION_ID),
32154
+ close: async () => {
32155
+ await closeAgentBayBrowser(CDP_BROWSER_ID);
32156
+ },
32157
+ log
32158
+ });
32159
+ agentBaySessionWatchdog.start();
32160
+ log(`AgentBay idle close enabled for ${AGENTBAY_SESSION_ID}: ${idleReleaseMinutes} minutes`);
31642
32161
  }
31643
32162
  process.on("SIGINT", () => cleanupAndExit(0));
31644
32163
  process.on("SIGTERM", () => cleanupAndExit(0));
@@ -1,8 +1,9 @@
1
+ import { type ControlSource, type ControlSourceWireValue } from '@mearl/browser-core';
1
2
  export declare function isInternalExtensionAction(action: unknown): boolean;
2
- export declare function createExtensionRequestMessage(id: string, action: string, data: any, trustedInternal?: boolean, controlSource?: 'local' | 'cloud'): {
3
+ export declare function createExtensionRequestMessage(id: string, action: string, data: any, trustedInternal?: boolean, controlSource?: ControlSource): {
3
4
  id: string;
4
5
  action: string;
5
6
  data: any;
6
- controlSource: 'local' | 'cloud';
7
+ controlSource: ControlSourceWireValue;
7
8
  trustedInternal?: true;
8
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/native-host",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Native Messaging Host for Mearl — bridges Chrome Extension and local socket server",
5
5
  "type": "module",
6
6
  "main": "dist/install.js",
@@ -37,9 +37,9 @@
37
37
  "@types/ws": "^8.18.1",
38
38
  "esbuild": "^0.25.2",
39
39
  "typescript": "^5.8.3",
40
- "@mearl/browser-core": "2.6.0",
41
- "@mearl/daemon-core": "2.6.0",
42
- "@mearl/setup": "2.6.0"
40
+ "@mearl/browser-core": "2.7.0",
41
+ "@mearl/daemon-core": "2.7.0",
42
+ "@mearl/setup": "2.7.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "rm -rf dist/ && node build.mjs",