@evan_zhong/browser-attach 0.22.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # 架构与设计边界
2
2
 
3
- 当前实现为 **0.22.0**。本文说明 BrowserAttach 的组件、控制模型与设计取舍;工具参数见 [使用指南](USAGE.md)。
3
+ 当前实现为 **0.22.1**。本文说明 BrowserAttach 的组件、控制模型与设计取舍;工具参数见 [使用指南](USAGE.md)。
4
4
 
5
5
  ## 组件与数据流
6
6
 
@@ -1,9 +1,18 @@
1
1
  # 验证报告
2
2
 
3
- 当前版本 **0.22.0**(2026-10-24)。浏览器验证使用本地 fixture 和临时 profile;测试链路在各项下说明。
3
+ 当前版本 **0.22.1**(2026-10-25)。浏览器验证使用本地 fixture 和临时 profile;测试链路在各项下说明。
4
4
 
5
5
  ## 当前版本
6
6
 
7
+ - `npm test`:246 项通过。新增隔离世界选择器诊断经 CDP 异常对象读取、单步/read_text/wait/batch 传递及 frame 路径底层原因保留;诊断读取失败保留原异常并清理 wrapper。补充准备/派发超时分类、派发后不沿用准备诊断、fill 值不匹配保留未知结果且不回显字段值。后续新增的 click 目标变化原因断言另通过 15 项指针回归。
8
+ - Chromium 156、Edge 155 headless 完整 stdio MCP `live-smoke`:各 267 次工具调用、29 项检查通过。新增真实 CSS/XPath 语法错误、CSS/AX role 歧义、缺失 selector、read_text/wait/batch 错误原因经过扩展 → Hub → MCP 的检查;原生 fill 同步格式化返回 value_not_retained、input_dispatched=true 及 post_action,真实 input 事件仅 1 次。保留上传、对话框、OOPIF、日志、后台输入与大 AX 树回归。
9
+ - Edge 首次与 Chromium 并行执行时,原有遮挡检查在 500 ms 准备预算内超时,未派发输入;未修改该检查预算,独立复跑完整脚本通过。
10
+ - `npm run test:package`:包外安装与 stdio MCP 通过,23 个工具、60 个发布文件,无配对密钥。未增加工具、参数、依赖或扩展权限,版本按诊断修复递增至 0.22.1。
11
+ - 本机部署:协调升级因现有 MCP 连接拒绝后,使用项目 stop/start 重启;Hub 为 0.22.1 / ready,已配对 Chrome、Edge 均为 0.22.1。稳定扩展目录 `F:\.nosis\browser-attach\extension` 的 42 个文件已核对:manifest 版本一致,其余非生成文件 SHA-256 一致。
12
+ - 当前边界:原因码不推断业务成功或服务端拒绝,也不授权重放;文档/上下文销毁导致异常诊断无法读取时保留原错误,diagnostics 可能缺失。已有 MCP 客户端需重连,重新发现和接管标签。
13
+
14
+ ## 0.22.0 验证
15
+
7
16
  - `npm test`:243 项通过。新增非活动标签的单次左键单击(ref / selector / viewport / document 坐标)仍取得可信回执且不激活标签;hover、拖拽、其他按键、双击与带修饰键的点击仍在派发前以 `TAB_NOT_READY` 拒绝;派发前的就绪检查允许左键单击期间的标签切换,仍拒绝视图冻结或目标丢弃,`page_state` 带 `tab_active`。
8
17
  - Chromium 156、Edge 155 headless:`interaction-smoke --regressions` 各 35 个场景通过。后台 ref / selector / viewport / document / offset 各 3 次(每浏览器 15 次,合计 30 次)均 `delivery_verified` 且标签激活与窗口焦点状态不变;连续两次后台 batch、后台回执丢失(1 次效果、不重放)、后台遮挡(未派发)、受限动作组、settle 期间切换标签等场景通过。
9
18
  - Chromium 156 `interaction-smoke` 默认场景 17 项通过,覆盖前台、后台 ref 与坐标、遮挡窗口、缩放、hover 偏移、闭合 Shadow DOM 与导航。
package/docs/USAGE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # BrowserAttach 使用指南
2
2
 
3
- 适用于 **0.22.0**:Node.js 22+、Chrome / Edge 125+、23 个 MCP 工具。安装、配对与升级见 [README](../README.md),宿主配置见 [Agent 集成](AGENT-INTEGRATION.md),内部机制见 [架构说明](ARCHITECTURE.md)。
3
+ 适用于 **0.22.1**:Node.js 22+、Chrome / Edge 125+、23 个 MCP 工具。安装、配对与升级见 [README](../README.md),宿主配置见 [Agent 集成](AGENT-INTEGRATION.md),内部机制见 [架构说明](ARCHITECTURE.md)。
4
4
 
5
5
  本文供按需查阅,不会自动通过 MCP 发送给 agent。MCP 初始化 `instructions` 提供通用规则,工具 description 与参数 schema 提供调用说明;宿主应把 instructions 纳入模型上下文。
6
6
 
@@ -71,6 +71,20 @@
71
71
 
72
72
  这些核验不代替业务检查。下拉过滤文字不等于选项已提交,字段值不等于框架状态已保存;按任务观察选中项、成功提示或业务接口。
73
73
 
74
+ 定位与输入失败可按 `diagnostics.reason` 区分,无须解析英文错误文案:
75
+
76
+ | reason | 含义 |
77
+ |---|---|
78
+ | `selector_not_found` | 准备预算内未找到 selector/nth 目标,或直接读取时目标不存在 |
79
+ | `selector_ambiguous` | CSS/XPath/label/placeholder/AX role 匹配不唯一;附有界候选与匹配总数 |
80
+ | `selector_invalid` / `selector_not_element` | 选择器解析失败 / 结果不是元素 |
81
+ | `value_not_retained` | fill 后实际文本与请求不一致;不回显请求值或实际值,不推断是格式化还是输入被吞 |
82
+ | `delivery_unverified` | 可信输入事件回执未在回执预算内完成 |
83
+ | `target_changed` | click/hover 派发期间的目标核验被拒绝,可能已有前序输入送达 |
84
+ | `preparation_timeout` / `input_timeout` | 无更具体准备原因的超时 / 已进入派发阶段后操作超时 |
85
+
86
+ 已有 `stale_ref`、`frame_locator`、可操作性及遮挡原因保持各自语义。frame 路径查询失败仍以 `frame_locator` 为 reason,可在 cause 和 selector_diagnostics 中保留底层选择器原因。定位辅助脚本抛出的诊断通过 CDP 单独读取;文档已销毁或无法读取时保留原错误,诊断可能缺失。派发后超时不会沿用准备阶段的定位/可操作性原因。上述原因不是重放许可;结合 `input_dispatched` 与 `post_action` 判断下一步,不推断服务端业务拒绝。
87
+
74
88
  ### 结果未知时
75
89
 
76
90
  超时、回执丢失或派发中断可能发生在动作生效之后。先用快照、截图或日志观察,再决定下一步;不要自动重放或换模式补发。导航和异步 JavaScript 也可能在超时后继续。
@@ -6,7 +6,7 @@ export function deadline(timeout_ms = 5000, phase = 'input', parentEnd = Infinit
6
6
  let dispatched = false, waiting, diagnostics;
7
7
  function error() {
8
8
  const result = timeoutError();
9
- if (diagnostics) result.diagnostics = diagnostics;
9
+ if (diagnostics && !dispatched) result.diagnostics = diagnostics;
10
10
  return result;
11
11
  }
12
12
  function timeoutError() {
@@ -19,6 +19,7 @@ export function deadline(timeout_ms = 5000, phase = 'input', parentEnd = Infinit
19
19
  : `Target did not become actionable within ${timeout_ms} ms${waiting ? `; waiting for ${waiting}` : ''}; no input was dispatched.`);
20
20
  result.input_dispatched = dispatched;
21
21
  result.outcome_unknown = dispatched;
22
+ result.diagnostics = { reason: dispatched ? 'input_timeout' : 'preparation_timeout' };
22
23
  return result;
23
24
  }
24
25
  async function run(operation) {
@@ -91,6 +92,8 @@ export function inspectTarget(editable) {
91
92
  export async function delivery(operation, ms = 1250) {
92
93
  let timer;
93
94
  try { return await Promise.race([operation, new Promise((resolve, reject) => {
94
- timer = setTimeout(() => reject(Object.assign(new Error('Native input has no verified receipt. Outcome is unknown. Browser UI may be capturing input; press Esc to dismiss a native menu, then observe. Never automatically replay or fall back.'), { outcome_unknown: true })), ms);
95
+ timer = setTimeout(() => reject(Object.assign(new Error('Native input has no verified receipt. Outcome is unknown. Browser UI may be capturing input; press Esc to dismiss a native menu, then observe. Never automatically replay or fall back.'), {
96
+ outcome_unknown: true, diagnostics: { reason: 'delivery_unverified' },
97
+ })), ms);
95
98
  })]); } finally { clearTimeout(timer); }
96
99
  }
@@ -14,7 +14,9 @@ export function createClick(cdp, value, { prepare, assertReady }) {
14
14
  try {
15
15
  const receipt = JSON.parse(params.payload);
16
16
  if (receipt.matched === true) operation.resolve();
17
- else operation.reject(Object.assign(new Error('Click target changed or became covered during mouse dispatch. Further incorrect target input was blocked; outcome is unknown. Observe again before retrying.'), { outcome_unknown: true }));
17
+ else operation.reject(Object.assign(new Error('Click target changed or became covered during mouse dispatch. Further incorrect target input was blocked; outcome is unknown. Observe again before retrying.'), {
18
+ outcome_unknown: true, diagnostics: { reason: 'target_changed' },
19
+ }));
18
20
  } catch (error) { operation.reject(error); }
19
21
  }
20
22
  function cancel(tabId) { pending.get(tabId)?.reject(Object.assign(new Error('Tab detached during click; outcome is unknown. Observe before retrying.'), { outcome_unknown: true })); }
@@ -11,3 +11,21 @@ export function errorFromData(data) {
11
11
  for (const key of fields) if (data[key] !== undefined) error[key] = data[key];
12
12
  return error;
13
13
  }
14
+
15
+ // CDP exception descriptions omit custom properties on errors thrown by our
16
+ // isolated-world helpers. Read their diagnostics only on the failure path.
17
+ export async function checkedValue(result, send, value) {
18
+ if (!result.exceptionDetails) return value(result);
19
+ let diagnostics;
20
+ const objectId = result.exceptionDetails.exception?.objectId;
21
+ if (objectId) {
22
+ try {
23
+ const details = await send('Runtime.callFunctionOn', { objectId, returnByValue: true,
24
+ functionDeclaration: 'function() { return Object.getOwnPropertyDescriptor(this, "diagnostics")?.value; }' });
25
+ if (!details.exceptionDetails) diagnostics = details.result?.value;
26
+ } catch {}
27
+ finally { await send('Runtime.releaseObject', { objectId }).catch(() => {}); }
28
+ }
29
+ try { return value(result); }
30
+ catch (error) { if (typeof diagnostics?.reason === 'string') error.diagnostics = diagnostics; throw error; }
31
+ }
@@ -127,7 +127,9 @@ export function createInput(cdp, value, sessions) {
127
127
  if (mode === 'native') await send('Input.insertText', { text: args.text });
128
128
  const matches = value(await send('Runtime.callFunctionOn', { objectId: node.objectId, returnByValue: true,
129
129
  functionDeclaration: verifyFill.toString(), arguments: [{ value: args.text }] }));
130
- if (matches !== true) throw Object.assign(new Error(`Fill did not retain the requested text. Input may have been swallowed, focus may have moved, or the page may have reformatted it. Outcome is unknown. ${BUSY}`), { outcome_unknown: true });
130
+ if (matches !== true) throw Object.assign(new Error(`Fill did not retain the requested text. Input may have been swallowed, focus may have moved, or the page may have reformatted it. Outcome is unknown. ${BUSY}`), {
131
+ outcome_unknown: true, diagnostics: { reason: 'value_not_retained', frame_id: node.frameId },
132
+ });
131
133
  return { filled: true, method: mode, value_verified: true, ...(mode === 'dom' ? { events_trusted: false } : {}), application_result: 'not_checked', observe_again: true };
132
134
  }
133
135
 
@@ -1,3 +1,5 @@
1
+ import { checkedValue } from './errors.js';
2
+
1
3
  // Selectors are reusable queries, never identities inferred from old AX text.
2
4
  export function targetOptions(args, { coordinates = false, focused = false } = {}) {
3
5
  const hasRef = args.ref != null, hasSelector = args.selector != null;
@@ -19,19 +21,26 @@ export function targetOptions(args, { coordinates = false, focused = false } = {
19
21
 
20
22
  export function queryElement(selector, type = 'css', nth, waitForMatch = false) {
21
23
  if (nth != null && (!Number.isSafeInteger(nth) || nth < 0)) throw new Error('nth must be a nonnegative safe integer (zero-based)');
24
+ const failure = (message, reason, detail = {}) => Object.assign(new Error(message), {
25
+ diagnostics: { reason, selector_type: type, ...detail },
26
+ });
22
27
  let element, count, at;
23
- if (type === 'xpath') {
24
- const result = document.evaluate(selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
25
- count = result.snapshotLength; at = index => result.snapshotItem(index);
26
- } else {
27
- const normalize = text => text.replace(/\s+/g, ' ').trim();
28
- const elements = type === 'label'
29
- ? Array.from(document.querySelectorAll('input,textarea,select,button,meter,output,progress')).filter(element =>
30
- Array.from(element.labels || []).some(label => normalize(label.textContent || '') === normalize(selector)))
31
- : type === 'placeholder'
32
- ? Array.from(document.querySelectorAll('[placeholder]')).filter(element => element.getAttribute('placeholder') === selector)
33
- : document.querySelectorAll(selector);
34
- count = elements.length; at = index => elements[index];
28
+ try {
29
+ if (type === 'xpath') {
30
+ const result = document.evaluate(selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
31
+ count = result.snapshotLength; at = index => result.snapshotItem(index);
32
+ } else {
33
+ const normalize = text => text.replace(/\s+/g, ' ').trim();
34
+ const elements = type === 'label'
35
+ ? Array.from(document.querySelectorAll('input,textarea,select,button,meter,output,progress')).filter(element =>
36
+ Array.from(element.labels || []).some(label => normalize(label.textContent || '') === normalize(selector)))
37
+ : type === 'placeholder'
38
+ ? Array.from(document.querySelectorAll('[placeholder]')).filter(element => element.getAttribute('placeholder') === selector)
39
+ : document.querySelectorAll(selector);
40
+ count = elements.length; at = index => elements[index];
41
+ }
42
+ } catch (error) {
43
+ throw failure(error.message, 'selector_invalid');
35
44
  }
36
45
  if (waitForMatch && (count === 0 || (nth != null && nth >= count))) return null;
37
46
  if (nth == null && count !== 1) {
@@ -43,11 +52,12 @@ export function queryElement(selector, type = 'css', nth, waitForMatch = false)
43
52
  disabled: node.matches(':disabled') || !!node.closest('[inert], [aria-disabled="true"]'),
44
53
  visible: rect.width > 0 && rect.height > 0 && style.visibility === 'visible' };
45
54
  });
46
- throw new Error(`Selector must match exactly one element; matched ${count}. Scope the selector or provide an explicit zero-based nth. Candidates: ${JSON.stringify(candidates)}${count > candidates.length ? ` (${count - candidates.length} more omitted)` : ''}`);
55
+ throw failure(`Selector must match exactly one element; matched ${count}. Scope the selector or provide an explicit zero-based nth. Candidates: ${JSON.stringify(candidates)}${count > candidates.length ? ` (${count - candidates.length} more omitted)` : ''}`,
56
+ count ? 'selector_ambiguous' : 'selector_not_found', { matched: count, candidates, omitted_candidates: Math.max(0, count - candidates.length) });
47
57
  }
48
- if (nth != null && nth >= count) throw new Error(`Selector nth=${nth} is out of range; matched ${count}`);
58
+ if (nth != null && nth >= count) throw failure(`Selector nth=${nth} is out of range; matched ${count}`, 'selector_not_found', { matched: count, nth });
49
59
  element = at(nth ?? 0);
50
- if (element.nodeType !== 1) throw new Error('Selector must resolve to an element');
60
+ if (element.nodeType !== 1) throw failure('Selector must resolve to an element', 'selector_not_element');
51
61
  return element;
52
62
  }
53
63
 
@@ -66,22 +76,27 @@ export async function resolveSelector(cdp, value, sessions, tabId, args, budget,
66
76
  } else {
67
77
  const result = await send('Runtime.evaluate', { contextId: context.executionContextId, objectGroup: 'browser-attach',
68
78
  expression: `(${queryElement.toString()})(${JSON.stringify(args.selector)},${JSON.stringify(args.selector_type ?? 'css')},${args.nth ?? 'undefined'},true)` });
69
- value(result);
79
+ await checkedValue(result, send, value);
70
80
  if (result.result?.subtype !== 'null') {
71
- if (!result.result?.objectId) throw new Error('Selector did not resolve to an element');
81
+ if (!result.result?.objectId) throw Object.assign(new Error('Selector did not resolve to an element'), {
82
+ diagnostics: { reason: 'selector_not_element', frame_id: frameId },
83
+ });
72
84
  const { node } = await send('DOM.describeNode', { objectId: result.result.objectId });
73
85
  backendNodeId = node.backendNodeId;
74
86
  }
75
87
  }
76
88
  if (backendNodeId != null) break;
77
89
  if (!wait) return null;
78
- budget.describe?.(`selector ${JSON.stringify(args.selector)}${args.name == null ? '' : ` name=${JSON.stringify(args.name)}`}${args.nth == null ? '' : ` nth=${args.nth}`} to appear; current=no matching element`);
90
+ budget.describe?.(`selector ${JSON.stringify(args.selector)}${args.name == null ? '' : ` name=${JSON.stringify(args.name)}`}${args.nth == null ? '' : ` nth=${args.nth}`} to appear; current=no matching element`,
91
+ { reason: 'selector_not_found', selector_type: args.selector_type ?? 'css', frame_id: frameId, ...(args.nth == null ? {} : { nth: args.nth }) });
79
92
  await budget.run(() => new Promise(resolve => setTimeout(resolve, 20)));
80
93
  }
81
94
  // Hit-test refs resolve in the default world too. CDP forbids passing object
82
95
  // arguments between worlds, even when both wrappers refer to the same node.
83
96
  const resolved = await send('DOM.resolveNode', { backendNodeId, objectGroup: 'browser-attach' });
84
- if (!resolved.object?.objectId) throw new Error('Selector target detached before resolution; no input was dispatched');
97
+ if (!resolved.object?.objectId) throw Object.assign(new Error('Selector target detached before resolution; no input was dispatched'), {
98
+ diagnostics: { reason: 'element_detached', frame_id: frameId },
99
+ });
85
100
  return { target, frameId, objectId: resolved.object.objectId, backendNodeId };
86
101
  }
87
102
 
@@ -98,7 +113,13 @@ export function matchRole(nodes, args) {
98
113
  for (let i = (node.childIds?.length || 0) - 1; i >= 0; i--) pending.push(byId.get(node.childIds[i]));
99
114
  }
100
115
  const candidates = [...matches.values()];
101
- if (args.nth == null && candidates.length > 1) throw new Error(`Role selector must match exactly one element; matched ${candidates.length}. Provide name or an explicit zero-based nth. Candidates: ${JSON.stringify(candidates.slice(0, 10).map((node, nth) => ({ nth, role: node.role?.value, name: String(node.name?.value || '').slice(0, 120) })))}`);
116
+ if (args.nth == null && candidates.length > 1) {
117
+ const preview = candidates.slice(0, 10).map((node, nth) => ({ nth, role: node.role?.value, name: String(node.name?.value || '').slice(0, 120) }));
118
+ throw Object.assign(new Error(`Role selector must match exactly one element; matched ${candidates.length}. Provide name or an explicit zero-based nth. Candidates: ${JSON.stringify(preview)}`), {
119
+ diagnostics: { reason: 'selector_ambiguous', selector_type: 'role', matched: candidates.length,
120
+ candidates: preview, omitted_candidates: Math.max(0, candidates.length - preview.length) },
121
+ });
122
+ }
102
123
  return candidates[args.nth ?? 0] || null;
103
124
  }
104
125
 
@@ -116,7 +137,7 @@ async function resolveFrameOnce(cdp, value, sessions, tabId, args, budget) {
116
137
  const frames = await budget.run(() => sessions.frames(tabId));
117
138
  const failure = (error, scope = {}) => {
118
139
  const candidates = frames.slice(0, 10).map(frame => ({ frame_id: frame.id, parent_frame_id: frame.parentId ?? null, url: frame.url }));
119
- error.diagnostics = { reason: 'frame_locator', ...scope, candidates, omitted_candidates: Math.max(0, frames.length - candidates.length) };
140
+ error.diagnostics = { reason: 'frame_locator', ...(error.diagnostics ? { cause: error.diagnostics.reason, selector_diagnostics: error.diagnostics } : {}), ...scope, candidates, omitted_candidates: Math.max(0, frames.length - candidates.length) };
120
141
  error.message += `; frame scope=${JSON.stringify(scope)}; current frames=${JSON.stringify(candidates)}${frames.length > candidates.length ? ` (${frames.length - candidates.length} more omitted)` : ''}`;
121
142
  return error;
122
143
  };
@@ -134,7 +155,7 @@ async function resolveFrameOnce(cdp, value, sessions, tabId, args, budget) {
134
155
  const send = (method, params) => budget.run(() => cdp(target, method, params));
135
156
  const { executionContextId } = await send('Page.createIsolatedWorld', { frameId, worldName: 'browser-attach-locator' });
136
157
  const result = await send('Runtime.evaluate', { contextId: executionContextId, expression: `(${queryElement.toString()})(${JSON.stringify(selector)},"css")`, objectGroup: 'browser-attach-frame' });
137
- value(result);
158
+ await checkedValue(result, send, value);
138
159
  try {
139
160
  const { node } = await send('DOM.describeNode', { objectId: result.result.objectId });
140
161
  if (!['IFRAME', 'FRAME'].includes(node.nodeName)) throw new Error('frame.path must resolve to iframe/frame elements');
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "BrowserAttach",
4
- "version": "0.22.0",
4
+ "version": "0.22.1",
5
5
  "description": "Connect existing Chrome and Edge tabs to a locally paired MCP client.",
6
6
  "minimum_chrome_version": "125",
7
7
  "permissions": [
package/extension/text.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { queryElement, targetOptions } from './locator.js';
2
+ import { checkedValue } from './errors.js';
2
3
 
3
4
  export async function readText(cdp, value, tabId, args) {
4
5
  const max = args.max_chars ?? 20000, offset = args.offset ?? 0, mode = args.mode ?? 'visible';
@@ -12,8 +13,9 @@ export async function readText(cdp, value, tabId, args) {
12
13
  const options = { selector: args.selector, selector_type: args.selector_type, nth: args.nth, mode, offset, max_chars: max };
13
14
  if (args.node) return value(await cdp(tabId, 'Runtime.callFunctionOn', { objectId: args.node.objectId, returnByValue: true,
14
15
  functionDeclaration: `function(args) { return (${extractText.toString()})(args, () => this); }`, arguments: [{ value: options }] }));
15
- return value(await cdp(tabId, 'Runtime.evaluate', { returnByValue: true, ...(args.context_id != null ? { contextId: args.context_id } : {}),
16
- expression: `(${extractText.toString()})(${JSON.stringify(options)},${queryElement.toString()})` }));
16
+ const send = (method, params) => cdp(tabId, method, params);
17
+ return await checkedValue(await send('Runtime.evaluate', { returnByValue: true, ...(args.context_id != null ? { contextId: args.context_id } : {}),
18
+ expression: `(${extractText.toString()})(${JSON.stringify(options)},${queryElement.toString()})` }), send, value);
17
19
  }
18
20
 
19
21
  export function extractText(args, query) {
package/extension/wait.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { deadline, inspectTarget } from './actionability.js';
2
2
  import { queryElement, targetOptions, resolveSelector, resolveFrame } from './locator.js';
3
+ import { checkedValue } from './errors.js';
3
4
 
4
5
  export const WAIT_CONDITIONS = ['attached', 'absent', 'visible', 'actionable', 'text', 'value', 'checked', 'selected', 'expanded', 'url', 'network_idle'];
5
6
 
@@ -46,8 +47,9 @@ export async function waitFor(cdp, value, sessions, network, pointer, tabId, arg
46
47
  }
47
48
  } else {
48
49
  const { executionContextId } = await budget.run(() => cdp(target, 'Page.createIsolatedWorld', { frameId, worldName: 'browser-attach-locator' }));
49
- current = value(await budget.run(() => cdp(target, 'Runtime.evaluate', { contextId: executionContextId, returnByValue: true,
50
- expression: `(${inspectWait.toString()})((${queryElement.toString()}),(${inspectTarget.toString()}),${JSON.stringify(args)})` })));
50
+ const send = (method, params) => budget.run(() => cdp(target, method, params));
51
+ current = await checkedValue(await send('Runtime.evaluate', { contextId: executionContextId, returnByValue: true,
52
+ expression: `(${inspectWait.toString()})((${queryElement.toString()}),(${inspectTarget.toString()}),${JSON.stringify(args)})` }), send, value);
51
53
  }
52
54
  const stable = current?.ready && previous?.ready && JSON.stringify(current) === JSON.stringify(previous);
53
55
  if (current?.satisfied && (args.condition !== 'actionable' || stable)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evan_zhong/browser-attach",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "MCP for existing signed-in Chrome and Edge tabs through a locally paired extension",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/config.mjs CHANGED
@@ -15,7 +15,7 @@ export const DATA_DIR = configuredHome
15
15
  : process.platform === 'win32' ? join(homedir(), '.browser-attach')
16
16
  : join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'browser-attach');
17
17
  export const DEFAULT_CONFIG = join(DATA_DIR, 'config.json');
18
- export const VERSION = '0.22.0';
18
+ export const VERSION = '0.22.1';
19
19
 
20
20
  export async function loadConfig(path = DEFAULT_CONFIG) {
21
21
  let config;