@attson/atwebpilot-mcp 0.0.66 → 0.0.68

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 (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +293 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,9 +18,11 @@ Claude Code:
18
18
 
19
19
  1. 会话启动时**不绑任何端口** —— 不碰网页的会话零副作用。
20
20
  2. AI 第一次调 `list_tabs` / `browser_*` 时才绑一个空闲端口,并自动打开配对页。
21
- 3. 你在浏览器里点「允许」,扩展即接入,重试那次调用即可。
21
+ 3. 你在浏览器里点「允许」,扩展即接入;正在等待的调用会自动继续,无需告诉 AI 授权成功或手动重试。
22
22
  4. 之后本机的会话都免确认(信任是安装级的);端口能复用时连配对页都不会弹。
23
23
 
24
+ 首次调用默认等待授权 90 秒。拒绝或超时会返回明确错误和配对页地址;授权成功只以扩展实际建立连接并发送 `HELLO` 为准。
25
+
24
26
  多个 Codex / Claude Code 会话可以同时接入同一个浏览器,各自一条连接。可在扩展设置 →
25
27
  Coordinator 查看已接入的会话、单独断开,或撤销信任。
26
28
 
package/dist/index.js CHANGED
@@ -212,11 +212,18 @@ var ChatSessionEventSchema = z3.discriminatedUnion("type", [
212
212
  ]);
213
213
 
214
214
  // ../shared/src/protocol/messages.ts
215
- var StepSchema = z4.object({
216
- kind: z4.literal("tool"),
217
- tool: z4.string(),
218
- args: z4.unknown()
219
- });
215
+ var ProtocolStepSchema = z4.discriminatedUnion("kind", [
216
+ z4.object({
217
+ kind: z4.literal("tool"),
218
+ tool: z4.string(),
219
+ args: z4.unknown()
220
+ }),
221
+ z4.object({
222
+ kind: z4.literal("js"),
223
+ source: z4.string(),
224
+ timeoutMs: z4.number().int().positive().optional()
225
+ })
226
+ ]);
220
227
  var DerivedTabFields = {
221
228
  /** true when the receiving connection owns this tab */
222
229
  mine: z4.boolean().optional(),
@@ -330,7 +337,7 @@ var ExecSchema = z4.object({
330
337
  req_id: z4.string(),
331
338
  session_id: z4.string(),
332
339
  tab_id: z4.string(),
333
- step: StepSchema
340
+ step: ProtocolStepSchema
334
341
  });
335
342
  var CloseSessionSchema = z4.object({
336
343
  ...EnvelopeFields,
@@ -687,6 +694,7 @@ function capabilityForTool(tool, opts) {
687
694
  case "searchPageIndex":
688
695
  case "readPageBlock":
689
696
  case "extractPageFields":
697
+ case "inspectElement":
690
698
  return "read:dom";
691
699
  case "extractImages":
692
700
  return "read:image";
@@ -998,6 +1006,25 @@ var TOOL_DEFS = [
998
1006
  required: ["selector"]
999
1007
  }
1000
1008
  },
1009
+ {
1010
+ name: "inspectElement",
1011
+ description: "[FAST\xB7READ\xB7LAYOUT] \u8BFB\u53D6\u5143\u7D20\u7684\u8FD0\u884C\u65F6\u5E03\u5C40\u771F\u76F8\uFF1Acomputed style\u3001viewport/document rect\u3001\u53EF\u89C1\u6027\u3001\u7956\u5148\u94FE\u4E0E shadow root \u4FE1\u606F\u3002\n\u8C03\u8BD5 display/box-sizing/position/overflow/z-index\u3001\u5F39\u7A97\u6302\u8F7D\u70B9\u6216\u7236\u7EA7\u5E03\u5C40\u65F6\u4F18\u5148\u7528\u5B83\uFF0C\u4E0D\u8981\u4E3A\u8FD9\u4E9B\u53EA\u8BFB\u4FE1\u606F\u8C03\u7528 runJS\u3002",
1012
+ input_schema: {
1013
+ type: "object",
1014
+ properties: {
1015
+ selector: { type: "string", description: "CSS selector\uFF1B\u4E0E uid \u4E8C\u9009\u4E00" },
1016
+ uid: { type: "string", description: "takeSnapshot/findElements \u8FD4\u56DE\u7684 uid\uFF1B\u4E0E selector \u4E8C\u9009\u4E00" },
1017
+ ancestorDepth: { type: "integer", default: 5, minimum: 0, maximum: 10 },
1018
+ styleProperties: {
1019
+ type: "array",
1020
+ items: { type: "string" },
1021
+ maxItems: 50,
1022
+ description: "\u8981\u8BFB\u53D6\u7684 CSS \u5C5E\u6027\uFF1B\u7701\u7565\u65F6\u8FD4\u56DE\u5E38\u7528\u5E03\u5C40\u5C5E\u6027"
1023
+ },
1024
+ tabId: TAB_ID_FIELD
1025
+ }
1026
+ }
1027
+ },
1001
1028
  {
1002
1029
  name: "extractText",
1003
1030
  description: "[FAST\xB7TARGETED] \u63D0\u53D6\u9009\u62E9\u5668\u547D\u4E2D\u5143\u7D20\u7684\u6587\u672C\u3002single=true \u8FD4\u56DE\u5B57\u7B26\u4E32\uFF0C\u5426\u5219\u8FD4\u56DE\u6570\u7EC4\u3002\n\u53EA\u7528\u4E8E\u660E\u786E\u7684\u5C0F\u8303\u56F4 selector\uFF1B\u666E\u901A\u7F51\u9875\u7406\u89E3/\u5B57\u6BB5\u63D0\u53D6\u4E0D\u8981\u7528 extractText({selector:'body'})\uFF0C\u5148\u7528 createPageIndex + extractPageFields/searchPageIndex\u3002\n\n\u793A\u4F8B\uFF1A\n- \u63D0\u53D6\u6807\u9898\uFF1A{ selector: 'h1', single: true }\n- \u63D0\u53D6\u6240\u6709\u6BB5\u843D\uFF1A{ selector: 'article p' }",
@@ -1290,6 +1317,7 @@ var TOOL_DEFS = [
1290
1317
  type: "object",
1291
1318
  properties: {
1292
1319
  source: { type: "string", description: "async function body" },
1320
+ timeoutMs: { type: "integer", minimum: 1, maximum: 25e3, default: 25e3 },
1293
1321
  tabId: TAB_ID_FIELD
1294
1322
  },
1295
1323
  required: ["source"]
@@ -1856,25 +1884,125 @@ function readToolMode(env) {
1856
1884
  return "full";
1857
1885
  }
1858
1886
 
1859
- // src/handlers.ts
1860
- function singleWorkerId(c, pairUrl) {
1861
- const workers = c.workers.list();
1862
- if (workers.length === 0) {
1863
- throw new Error(
1864
- "\u6CA1\u6709\u6D4F\u89C8\u5668\u8FDE\u5165\u3002\u5DF2\u4E3A\u4F60\u6253\u5F00\u914D\u5BF9\u9875" + (pairUrl ? ` ${pairUrl}` : "") + "\uFF0C\u5728\u6D4F\u89C8\u5668\u91CC\u786E\u8BA4\u540E\u91CD\u8BD5\u672C\u6B21\u8C03\u7528\u3002\uFF08\u5982\u679C\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E0D\u662F\u88C5\u4E86\u6269\u5C55\u7684\u90A3\u4E2A\uFF0C\u8BF7\u628A\u8BE5\u5730\u5740\u7C98\u8D34\u8FC7\u53BB\u6253\u5F00\uFF09"
1865
- );
1887
+ // ../shared/src/static-scan.ts
1888
+ var RULES = [
1889
+ {
1890
+ rule: "uses-document-cookie",
1891
+ severity: "dangerous",
1892
+ message: "\u8BFB\u53D6/\u5199\u5165 cookie",
1893
+ pattern: /\bdocument\s*\.\s*cookie\b/g
1894
+ },
1895
+ {
1896
+ rule: "uses-eval",
1897
+ severity: "dangerous",
1898
+ message: "eval() \u6267\u884C\u52A8\u6001\u4EE3\u7801",
1899
+ pattern: /\beval\s*\(/g
1900
+ },
1901
+ {
1902
+ rule: "uses-new-function",
1903
+ severity: "dangerous",
1904
+ message: "new Function \u6267\u884C\u52A8\u6001\u4EE3\u7801",
1905
+ pattern: /\bnew\s+Function\s*\(/g
1906
+ },
1907
+ {
1908
+ rule: "uses-chrome-api",
1909
+ severity: "dangerous",
1910
+ message: "\u5C1D\u8BD5\u8BBF\u95EE\u6269\u5C55 API",
1911
+ pattern: /\b(chrome|browser)\s*\.\s*[a-zA-Z_$]/g
1912
+ },
1913
+ {
1914
+ rule: "uses-fetch",
1915
+ severity: "caution",
1916
+ message: "\u53D1\u8D77\u7F51\u7EDC\u8BF7\u6C42 (fetch)",
1917
+ pattern: /\bfetch\s*\(/g
1918
+ },
1919
+ {
1920
+ rule: "uses-xhr",
1921
+ severity: "caution",
1922
+ message: "\u53D1\u8D77\u7F51\u7EDC\u8BF7\u6C42 (XMLHttpRequest)",
1923
+ pattern: /\bnew\s+XMLHttpRequest\b|\bXMLHttpRequest\s*\(/g
1924
+ },
1925
+ {
1926
+ rule: "uses-send-beacon",
1927
+ severity: "caution",
1928
+ message: "navigator.sendBeacon",
1929
+ pattern: /navigator\s*\.\s*sendBeacon\b/g
1930
+ },
1931
+ {
1932
+ rule: "uses-storage",
1933
+ severity: "caution",
1934
+ message: "\u8BFB/\u5199 localStorage / sessionStorage",
1935
+ pattern: /\b(local|session)Storage\b/g
1936
+ },
1937
+ {
1938
+ rule: "uses-indexed-db",
1939
+ severity: "caution",
1940
+ message: "\u8BFB/\u5199 IndexedDB",
1941
+ pattern: /\bindexedDB\b/g
1942
+ },
1943
+ {
1944
+ rule: "uses-mutation-observer",
1945
+ severity: "info",
1946
+ message: "MutationObserver",
1947
+ pattern: /\bMutationObserver\b/g
1948
+ }
1949
+ ];
1950
+ function runStaticScan(source) {
1951
+ const out = [];
1952
+ for (const r of RULES) {
1953
+ const matches = collectMatches(source, r.pattern);
1954
+ if (matches.length > 0) {
1955
+ out.push({
1956
+ rule: r.rule,
1957
+ severity: r.severity,
1958
+ message: r.message,
1959
+ matches
1960
+ });
1961
+ }
1962
+ }
1963
+ return out;
1964
+ }
1965
+ function highestSeverity(findings) {
1966
+ if (findings.some((f) => f.severity === "dangerous")) return "dangerous";
1967
+ if (findings.some((f) => f.severity === "caution")) return "caution";
1968
+ return "info";
1969
+ }
1970
+ function collectMatches(source, pattern) {
1971
+ const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
1972
+ const out = [];
1973
+ let m;
1974
+ while ((m = re.exec(source)) !== null) {
1975
+ const { line, col } = locate(source, m.index);
1976
+ out.push({ line, col, text: m[0] });
1977
+ if (m.index === re.lastIndex) re.lastIndex++;
1978
+ }
1979
+ return out;
1980
+ }
1981
+ function locate(source, offset) {
1982
+ let line = 1;
1983
+ let col = 1;
1984
+ for (let i = 0; i < offset; i++) {
1985
+ if (source.charCodeAt(i) === 10) {
1986
+ line++;
1987
+ col = 1;
1988
+ } else {
1989
+ col++;
1990
+ }
1866
1991
  }
1867
- if (workers.length > 1) throw new Error("\u68C0\u6D4B\u5230\u591A\u4E2A\u6D4F\u89C8\u5668\u8FDE\u5165\uFF1Bv1 \u4EC5\u652F\u6301\u5355 worker\uFF0C\u8BF7\u53EA\u4FDD\u7559\u4E00\u4E2A\u8FDE\u63A5");
1868
- return workers[0].id;
1992
+ return { line, col };
1869
1993
  }
1994
+
1995
+ // src/handlers.ts
1870
1996
  async function handleListTabs(deps) {
1871
1997
  const { coordinator } = await deps.ensure();
1872
- const w = coordinator.workers.get(singleWorkerId(coordinator, deps.pairUrl()));
1998
+ const workerId = await deps.waitForWorker();
1999
+ const w = coordinator.workers.get(workerId);
2000
+ if (!w) throw new Error(`worker ${workerId} disconnected while opening browser context`);
1873
2001
  return { tabs: w.available_tabs };
1874
2002
  }
1875
2003
  async function handleOpenSession(deps, args) {
1876
2004
  const { coordinator } = await deps.ensure();
1877
- const worker_id = singleWorkerId(coordinator, deps.pairUrl());
2005
+ const worker_id = await deps.waitForWorker();
1878
2006
  const tab_id = String(args.tab_id);
1879
2007
  const requested = Array.isArray(args.capabilities) ? args.capabilities.map(String).filter(isCapability) : [];
1880
2008
  const scope = new Set(requested.length ? requested : CAPABILITIES);
@@ -1902,18 +2030,25 @@ async function handleBrowserTool(deps, gen, args) {
1902
2030
  const session_id = String(args.session_id);
1903
2031
  const session = coordinator.sessions.get(session_id);
1904
2032
  if (!session) throw new Error(`session ${session_id} not found`);
1905
- const { session_id: _omit, ...toolArgs } = args;
2033
+ const { session_id: _omit, ...suppliedToolArgs } = args;
2034
+ const toolArgs = { ...suppliedToolArgs };
1906
2035
  const tool = gen.builtinTool;
1907
2036
  if (gen.stepKind === "js") {
1908
2037
  const source = String(toolArgs.source ?? "");
1909
2038
  if (!source) throw new Error("browser_runJS: source required");
1910
- const v = coordinator.validateCall({ session_id, kind: "runJS", unsafe: true });
2039
+ const unsafe = highestSeverity(runStaticScan(source)) === "dangerous";
2040
+ const v = coordinator.validateCall({ session_id, kind: "runJS", unsafe });
1911
2041
  if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
1912
2042
  coordinator.recordCall(session_id, v.dangerous);
2043
+ const requestedTimeout = toolArgs.timeoutMs;
1913
2044
  const jsResult = await hub.exec(session.worker_id, {
1914
2045
  session_id,
1915
2046
  tab_id: session.tab_id,
1916
- step: { kind: "js", source }
2047
+ step: {
2048
+ kind: "js",
2049
+ source,
2050
+ ...typeof requestedTimeout === "number" && Number.isInteger(requestedTimeout) && requestedTimeout > 0 ? { timeoutMs: requestedTimeout } : {}
2051
+ }
1917
2052
  });
1918
2053
  if (!jsResult.ok) {
1919
2054
  throw new Error(
@@ -1922,6 +2057,13 @@ async function handleBrowserTool(deps, gen, args) {
1922
2057
  }
1923
2058
  return jsResult.return ?? null;
1924
2059
  }
2060
+ if (gen.builtinTool === "switchToTab" || gen.builtinTool === "closeTab") {
2061
+ const boundTabId = Number.parseInt(session.tab_id, 10);
2062
+ if (!Number.isFinite(boundTabId)) {
2063
+ throw new Error(`InvalidArgs: session tab_id "${session.tab_id}" is not numeric`);
2064
+ }
2065
+ toolArgs.tabId = boundTabId;
2066
+ }
1925
2067
  const override = NON_BUILTIN_CAPABILITY[gen.builtinTool];
1926
2068
  if (override) {
1927
2069
  const v = coordinator.validateCall({ session_id, kind: "capability", capability: override });
@@ -2068,7 +2210,9 @@ function createMcpServer(deps) {
2068
2210
 
2069
2211
  // src/loopback-ws-hub.ts
2070
2212
  import { WebSocketServer, WebSocket } from "ws";
2071
- import { createServer } from "http";
2213
+ import {
2214
+ createServer
2215
+ } from "http";
2072
2216
 
2073
2217
  // ../shared/src/pairing/types.ts
2074
2218
  var GRACEFUL_CLOSE_CODE = 4e3;
@@ -2106,6 +2250,17 @@ function renderPairPage(payload) {
2106
2250
  el.textContent = e.data.trusted ? "\u5DF2\u4FE1\u4EFB\uFF0C\u8FDE\u63A5\u4E2D\u2026" : "\u5DF2\u8FDE\u63A5";
2107
2251
  setTimeout(function () { window.close(); }, 1200);
2108
2252
  } else if (e.data.reason === "denied") {
2253
+ fetch("/pair/decision", {
2254
+ method: "POST",
2255
+ headers: { "content-type": "application/json" },
2256
+ body: JSON.stringify({
2257
+ v: 1,
2258
+ decision: "denied",
2259
+ sessionId: payload.sessionId,
2260
+ installId: payload.installId,
2261
+ secret: payload.secret
2262
+ })
2263
+ }).catch(function () {});
2109
2264
  el.textContent = "\u5DF2\u62D2\u7EDD\u3002\u53EF\u4EE5\u5173\u95ED\u672C\u9875\u3002";
2110
2265
  } else {
2111
2266
  el.textContent = "\u8FDE\u63A5\u6269\u5C55\u5931\u8D25\u3002\u8BF7\u5237\u65B0\u9875\u9762\u91CD\u8BD5\uFF0C\u6216\u68C0\u67E5\u6269\u5C55\u662F\u5426\u5DF2\u542F\u7528\u3002";
@@ -2177,12 +2332,17 @@ var LoopbackWSHub = class {
2177
2332
  execTimeoutMs;
2178
2333
  keepaliveIntervalMs;
2179
2334
  keepaliveTimers = /* @__PURE__ */ new Map();
2335
+ closedNotified = false;
2180
2336
  ready() {
2181
2337
  return this.readyPromise;
2182
2338
  }
2183
2339
  /** Serves the pairing page; everything else is a 404. */
2184
2340
  onHttpRequest(req, res) {
2185
2341
  const path = (req.url ?? "").split("?")[0];
2342
+ if (path === "/pair/decision" && req.method === "POST") {
2343
+ void this.onPairDecision(req, res);
2344
+ return;
2345
+ }
2186
2346
  if (path !== "/pair") {
2187
2347
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
2188
2348
  res.end("not found");
@@ -2200,6 +2360,29 @@ var LoopbackWSHub = class {
2200
2360
  });
2201
2361
  res.end(renderPairPage(payload));
2202
2362
  }
2363
+ async onPairDecision(req, res) {
2364
+ try {
2365
+ let body = "";
2366
+ for await (const chunk of req) {
2367
+ body += chunk.toString();
2368
+ if (body.length > 4096) throw new Error("pairing decision too large");
2369
+ }
2370
+ const decision = JSON.parse(body);
2371
+ const payload = this.opts.pairPayload?.();
2372
+ const matches = payload && decision.v === 1 && decision.decision === "denied" && decision.sessionId === payload.sessionId && decision.installId === payload.installId && decision.secret === payload.secret;
2373
+ if (!matches) {
2374
+ res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
2375
+ res.end("pairing decision rejected");
2376
+ return;
2377
+ }
2378
+ this.opts.onPairingDenied?.();
2379
+ res.writeHead(204);
2380
+ res.end();
2381
+ } catch {
2382
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
2383
+ res.end("invalid pairing decision");
2384
+ }
2385
+ }
2203
2386
  /**
2204
2387
  * Tells connected extensions this shutdown is deliberate, so they drop the
2205
2388
  * endpoint instead of retrying it for the rest of the browser session.
@@ -2215,6 +2398,13 @@ var LoopbackWSHub = class {
2215
2398
  await this.close();
2216
2399
  }
2217
2400
  close() {
2401
+ if (!this.closedNotified) {
2402
+ this.closedNotified = true;
2403
+ try {
2404
+ this.opts.onClosed?.();
2405
+ } catch {
2406
+ }
2407
+ }
2218
2408
  for (const p of this.pending.values()) {
2219
2409
  clearTimeout(p.timer);
2220
2410
  p.reject(new Error("hub closing"));
@@ -2374,7 +2564,7 @@ var LoopbackWSHub = class {
2374
2564
  req_id,
2375
2565
  session_id: params.session_id,
2376
2566
  tab_id: params.tab_id,
2377
- step: { kind: "tool", tool: params.step.tool, args: params.step.args }
2567
+ step: params.step
2378
2568
  });
2379
2569
  if (this.byWorker.get(worker_id) !== socket) {
2380
2570
  clearTimeout(timer);
@@ -2422,12 +2612,13 @@ function helloToWorker(h, now) {
2422
2612
  last_heartbeat_at: now
2423
2613
  };
2424
2614
  }
2425
- function installWire(hub, coordinator, clock) {
2615
+ function installWire(hub, coordinator, clock, onWorkerReady) {
2426
2616
  hub.onMessage((worker_id, msg) => {
2427
2617
  switch (msg.type) {
2428
2618
  case "HELLO":
2429
2619
  coordinator.unregisterWorker(msg.worker_id);
2430
2620
  coordinator.registerWorker(helloToWorker(msg, clock.now()));
2621
+ onWorkerReady?.(msg.worker_id);
2431
2622
  break;
2432
2623
  case "PING":
2433
2624
  coordinator.heartbeatWorker(worker_id);
@@ -2498,12 +2689,53 @@ function processInfo(cwd = process.cwd()) {
2498
2689
  }
2499
2690
 
2500
2691
  // src/ensure-hub.ts
2692
+ var DEFAULT_WORKER_WAIT_TIMEOUT_MS = 9e4;
2501
2693
  function createHubEnsurer(d) {
2502
2694
  let bundle = null;
2503
2695
  let inflight = null;
2504
2696
  let pairUrlValue = null;
2505
2697
  let opened = false;
2698
+ let hubIsClosed = false;
2699
+ let pendingDenial = false;
2700
+ let workerWait = null;
2506
2701
  const info = d.processInfo ?? processInfo();
2702
+ function connectedWorkerId(coordinator) {
2703
+ const workers = coordinator.workers.list();
2704
+ if (workers.length === 0) return null;
2705
+ if (workers.length > 1) {
2706
+ throw new Error("\u68C0\u6D4B\u5230\u591A\u4E2A\u6D4F\u89C8\u5668\u8FDE\u5165\uFF1Bv1 \u4EC5\u652F\u6301\u5355 worker\uFF0C\u8BF7\u53EA\u4FDD\u7559\u4E00\u4E2A\u8FDE\u63A5");
2707
+ }
2708
+ return workers[0].id;
2709
+ }
2710
+ function finishWorkerWait(result) {
2711
+ const pending = workerWait;
2712
+ if (!pending) return;
2713
+ workerWait = null;
2714
+ clearTimeout(pending.timer);
2715
+ if ("workerId" in result) pending.resolve(result.workerId);
2716
+ else pending.reject(result.error);
2717
+ }
2718
+ function notifyWorkerReady(coordinator) {
2719
+ try {
2720
+ const workerId = connectedWorkerId(coordinator);
2721
+ if (workerId) finishWorkerWait({ workerId });
2722
+ } catch (error) {
2723
+ finishWorkerWait({ error: error instanceof Error ? error : new Error(String(error)) });
2724
+ }
2725
+ }
2726
+ function pairingDenied() {
2727
+ const error = new Error(
2728
+ "\u7528\u6237\u62D2\u7EDD\u4E86\u6D4F\u89C8\u5668\u914D\u5BF9\u6388\u6743\u3002" + (pairUrlValue ? `\u914D\u5BF9\u9875\uFF1A${pairUrlValue}` : "")
2729
+ );
2730
+ if (workerWait) finishWorkerWait({ error });
2731
+ else pendingDenial = true;
2732
+ }
2733
+ function hubClosed() {
2734
+ hubIsClosed = true;
2735
+ if (workerWait) {
2736
+ finishWorkerWait({ error: new Error("\u6D4F\u89C8\u5668\u914D\u5BF9\u670D\u52A1\u5DF2\u5173\u95ED\u3002") });
2737
+ }
2738
+ }
2507
2739
  async function bind() {
2508
2740
  const identity = loadOrCreateIdentity(d.identityDir);
2509
2741
  const preferred = d.explicitPort ?? loadLastPort(d.identityDir) ?? 0;
@@ -2528,11 +2760,17 @@ function createHubEnsurer(d) {
2528
2760
  }
2529
2761
  saveLastPort(port, d.identityDir);
2530
2762
  const coordinator = new Coordinator({ hub, clock: d.clock, idGen: d.idGen });
2531
- installWire(hub, coordinator, d.clock);
2763
+ installWire(hub, coordinator, d.clock, () => notifyWorkerReady(coordinator));
2532
2764
  pairUrlValue = `http://127.0.0.1:${port}/pair`;
2533
2765
  return { coordinator, hub, port };
2534
2766
  function common() {
2535
- return { token: d.token, clock: d.clock, idGen: d.idGen };
2767
+ return {
2768
+ token: d.token,
2769
+ clock: d.clock,
2770
+ idGen: d.idGen,
2771
+ onPairingDenied: pairingDenied,
2772
+ onClosed: hubClosed
2773
+ };
2536
2774
  }
2537
2775
  }
2538
2776
  async function ensure() {
@@ -2548,10 +2786,40 @@ function createHubEnsurer(d) {
2548
2786
  }
2549
2787
  return b;
2550
2788
  }
2789
+ async function waitForWorker(timeoutMs = DEFAULT_WORKER_WAIT_TIMEOUT_MS) {
2790
+ const { coordinator } = await ensure();
2791
+ if (hubIsClosed) throw new Error("\u6D4F\u89C8\u5668\u914D\u5BF9\u670D\u52A1\u5DF2\u5173\u95ED\u3002");
2792
+ const connected = connectedWorkerId(coordinator);
2793
+ if (connected) return connected;
2794
+ if (pendingDenial) {
2795
+ pendingDenial = false;
2796
+ throw new Error(
2797
+ "\u7528\u6237\u62D2\u7EDD\u4E86\u6D4F\u89C8\u5668\u914D\u5BF9\u6388\u6743\u3002" + (pairUrlValue ? `\u914D\u5BF9\u9875\uFF1A${pairUrlValue}` : "")
2798
+ );
2799
+ }
2800
+ if (workerWait) return workerWait.promise;
2801
+ let resolve;
2802
+ let reject;
2803
+ const promise = new Promise((res, rej) => {
2804
+ resolve = res;
2805
+ reject = rej;
2806
+ });
2807
+ const timer = setTimeout(() => {
2808
+ finishWorkerWait({
2809
+ error: new Error(
2810
+ `\u7B49\u5F85\u6D4F\u89C8\u5668\u6388\u6743\u8D85\u65F6\uFF08${Math.ceil(timeoutMs / 1e3)} \u79D2\uFF09\u3002` + (pairUrlValue ? `\u8BF7\u5728\u5DF2\u6253\u5F00\u7684\u914D\u5BF9\u9875 ${pairUrlValue} \u5B8C\u6210\u6388\u6743\u540E\u91CD\u8BD5\u3002` : "")
2811
+ )
2812
+ });
2813
+ }, timeoutMs);
2814
+ timer.unref?.();
2815
+ workerWait = { promise, resolve, reject, timer };
2816
+ return promise;
2817
+ }
2551
2818
  return {
2552
2819
  ensure,
2553
2820
  peek: () => bundle,
2554
2821
  pairUrl: () => pairUrlValue,
2822
+ waitForWorker,
2555
2823
  bound: () => bundle != null
2556
2824
  };
2557
2825
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attson/atwebpilot-mcp",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
4
4
  "license": "Apache-2.0",
5
5
  "description": "MCP server for the atwebpilot browser extension - let Codex or Claude Code drive your browser.",
6
6
  "homepage": "https://github.com/attson/atwebpilot#readme",