@attson/atwebpilot-mcp 0.0.73 → 0.0.75

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
@@ -17,11 +17,11 @@ Claude Code:
17
17
  **不需要手填端口。** 从 Plan 33 起是配对流程:
18
18
 
19
19
  1. 会话启动时**不绑任何端口** —— 不碰网页的会话零副作用。
20
- 2. AI 第一次调 `list_tabs` / `browser_*` 时才绑一个空闲端口,尝试用系统默认浏览器打开配对页,并立即在 MCP 进度消息中显示配对 URL。
21
- 3. 你在浏览器里点「允许」,扩展即接入;正在等待的调用会自动继续,无需告诉 AI 授权成功或手动重试。
20
+ 2. AI 第一次调 `list_tabs` / `pairing_status` 时才绑一个空闲端口,尝试用系统默认浏览器打开配对页,并立即返回 `pairing_required + pair_url`,不阻塞等待。
21
+ 3. 你在浏览器里点「允许」,扩展即接入;AI 用轻量 `pairing_status` 确认连接后再读取 tab,不重复阻塞式调用。
22
22
  4. 之后本机的会话都免确认(信任是安装级的);端口能复用时连配对页都不会弹。
23
23
 
24
- 首次调用默认等待授权 90 秒。若系统未弹出浏览器,可直接打开进度消息中的 URL;拒绝或超时也会返回明确错误和配对页地址。授权成功只以扩展实际建立连接并发送 `HELLO` 为准。
24
+ 若系统未弹出浏览器,可直接打开返回结果中的 `pair_url`。授权成功只以扩展实际建立连接并发送 `HELLO` 为准;打开配对页本身不代表已连接。
25
25
 
26
26
  多个 Codex / Claude Code 会话可以同时接入同一个浏览器,各自一条连接。可在扩展设置 →
27
27
  Coordinator 查看已接入的会话、单独断开,或撤销信任。
@@ -48,9 +48,9 @@ listen 端口的能力,所以方向只能如此。这也是配对页存在的
48
48
 
49
49
  ## 工具面
50
50
 
51
- - 控制面 4 个:`list_tabs / open_session / close_session / get_quota`
51
+ - 控制面 5 个:`pairing_status / list_tabs / open_session / close_session / get_quota`
52
52
  - 发现 1 个:`browser_discoverTools` —— 已知工具时直接传 `enable: [...]`,已知分组时直接传 `enableGroups: ["export", ...]`;只有两者都未知时才不带参数读取目录。启用后会加入本进程的 `tools/list`(发送 `tools/list_changed`),并直接返回完整 schema。
53
- - 执行面默认 **core 31 个** `browser_*`:浏览 / 采集 / 填表 / 导航 / 截图 闭环所需的工具。其余 20 个由 AI 按需 `discoverTools` 拉取,用户不用配置。
53
+ - 执行面在浏览器连接前不进入 `tools/list`;连接确认后加载默认 **core 31 个** `browser_*`。其余 20 个由 AI 按需 `discoverTools` 拉取,用户不用配置。
54
54
  - 不暴露 `askUser`(MCP 会话没有人在侧边栏应答)和 `attachTab` / `detachTab`(目标 tab 已由 `open_session` 绑定)。
55
55
 
56
56
  MCP 层的描述是精简英文(≈2.5k tokens for core),侧边栏内置 LLM 仍用中文长描述;两者共享同一份 `TOOL_DEFS`,靠 `mcp` 字段区分。
@@ -65,8 +65,8 @@ hover / uploadFile / scroll / waitFor`);`browser_storage` 也不会列出,
65
65
 
66
66
  | 值 | 效果 |
67
67
  |---|---|
68
- | `core`(默认) | 31 个核心工具 + `browser_discoverTools`,其余按需发现 |
69
- | `full` | 一开始就全部列出(51 个),适合不想让 AI 多一步发现的用户 |
68
+ | `core`(默认) | 连接前仅控制面;连接后 31 个核心工具 + `browser_discoverTools` |
69
+ | `full` | 连接前仍仅控制面;连接后列出全部 51 个工具 |
70
70
 
71
71
  无法识别的值(包括已移除的 `parity`)按 `core` 处理,并往 stderr 打一条提示。
72
72
 
@@ -77,12 +77,14 @@ hover / uploadFile / scroll / waitFor`);`browser_storage` 也不会列出,
77
77
  ```bash
78
78
  pnpm analyze:sessions
79
79
  pnpm analyze:sessions -- --since 7d --clients claude --format json
80
+ pnpm analyze:sessions -- --since 24h --clients codex --latest-session
80
81
  ```
81
82
 
82
83
  分析器只读扫描 `~/.claude/projects` 与 `~/.codex/sessions` 中最近 30 天的
83
- JSONL,识别真实 AtWebPilot 工具调用及重复验证、固定等待、重复截图、可批量填表等
84
+ JSONL,识别真实 AtWebPilot 工具调用及重复验证、固定等待、重复截图、可批量填表、配对超时与无状态重试等
84
85
  模式。输出只包含聚合计数、参数字段名与不可逆指纹;不会输出或保存 prompt、参数值、
85
- 工具结果正文、凭证或绝对会话路径。使用 `--help` 查看目录覆盖等选项。
86
+ 工具结果正文、凭证或绝对会话路径。Codex 嵌套调用会关联 wait cell 的耗时、错误以及
87
+ cached/uncached token;使用 `--latest-session` 只看最近一条真实使用记录。
86
88
 
87
89
  ### 从 0.0.70 及更早版本迁移
88
90
 
package/dist/index.js CHANGED
@@ -895,7 +895,9 @@ var Coordinator = class {
895
895
  return session;
896
896
  }
897
897
  closeSession(id) {
898
+ const session = this.sessions.get(id);
898
899
  this.sessions.close(id);
900
+ if (session) this.notifySessionClosed(session);
899
901
  }
900
902
  // === Tool calls ===
901
903
  validateCall(input) {
@@ -920,8 +922,22 @@ var Coordinator = class {
920
922
  // === Periodic housekeeping ===
921
923
  tick() {
922
924
  const expired_sessions = this.sessions.tick();
925
+ for (const id of expired_sessions) {
926
+ const session = this.sessions.get(id);
927
+ if (session) this.notifySessionClosed(session);
928
+ }
923
929
  return { expired_sessions };
924
930
  }
931
+ notifySessionClosed(session) {
932
+ void this.hub.send(session.worker_id, {
933
+ type: "CLOSE_SESSION",
934
+ nonce: this.idGen.next("nonce"),
935
+ ts: this.clock.now(),
936
+ protocol_version: PROTOCOL_VERSION,
937
+ session_id: session.id
938
+ }).catch(() => {
939
+ });
940
+ }
925
941
  };
926
942
 
927
943
  // src/mcp-server.ts
@@ -930,14 +946,19 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprot
930
946
 
931
947
  // src/control-tools.ts
932
948
  var CONTROL_TOOLS = [
949
+ {
950
+ name: "pairing_status",
951
+ description: "Start or check browser pairing without waiting. Returns connected or pairing_required with the pair URL.",
952
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
953
+ },
933
954
  {
934
955
  name: "list_tabs",
935
- description: "\u5217\u51FA\u6D4F\u89C8\u5668\u53EF\u7528\u6807\u7B7E\u9875\uFF1A[{tab_id,url,title}]\u3002\u82E5\u6269\u5C55\u5C1A\u672A\u8FDE\u63A5\uFF0C\u9996\u6B21\u8C03\u7528\u4F1A\u5C1D\u8BD5\u81EA\u52A8\u6253\u5F00\u914D\u5BF9\u9875\u5E76\u7B49\u5F85\u6388\u6743\uFF08\u6700\u591A 90 \u79D2\uFF09\uFF0C\u7B49\u5F85\u671F\u95F4\u4F1A\u901A\u77E5\u5B9E\u9645\u914D\u5BF9 URL\u3002\u5148\u8C03\u5B83\u62FF tab_id\u3002",
956
+ description: "List browser tabs. If pairing is needed, opens the pair page and immediately returns pairing_required with its URL; wait for user approval, then call pairing_status instead of retrying this tool.",
936
957
  inputSchema: { type: "object", properties: {}, additionalProperties: false }
937
958
  },
938
959
  {
939
960
  name: "open_session",
940
- description: "\u4E3A\u67D0\u4E2A tab \u5F00\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u8FD4\u56DE session_id\uFF1B\u540E\u7EED browser_* \u5DE5\u5177\u90FD\u5E26\u8FD9\u4E2A session_id\u3002capabilities \u7701\u7565=\u6388\u4E88\u5168\u90E8\u80FD\u529B\u3002",
961
+ description: "Open a session for a tab from list_tabs. Returns immediately with PAIRING_REQUIRED if the browser is not connected.",
941
962
  inputSchema: {
942
963
  type: "object",
943
964
  required: ["tab_id"],
@@ -2264,16 +2285,46 @@ function locate(source, offset) {
2264
2285
  }
2265
2286
 
2266
2287
  // src/handlers.ts
2267
- async function handleListTabs(deps, onPairingRequired) {
2288
+ function singleWorkerId(c, pairUrl) {
2289
+ const workers = c.workers.list();
2290
+ if (workers.length === 0) {
2291
+ throw new Error(
2292
+ "PAIRING_REQUIRED: \u6D4F\u89C8\u5668\u5C1A\u672A\u8FDE\u63A5\u3002" + (pairUrl ? `\u8BF7\u5728\u6D4F\u89C8\u5668\u4E2D\u5B8C\u6210\u914D\u5BF9\uFF1A${pairUrl}` : "\u8BF7\u5148\u8C03\u7528 pairing_status \u83B7\u53D6\u914D\u5BF9\u9875\u3002")
2293
+ );
2294
+ }
2295
+ 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");
2296
+ return workers[0].id;
2297
+ }
2298
+ async function handlePairingStatus(deps) {
2268
2299
  const { coordinator } = await deps.ensure();
2269
- const workerId = await deps.waitForWorker(void 0, onPairingRequired);
2270
- const w = coordinator.workers.get(workerId);
2271
- if (!w) throw new Error(`worker ${workerId} disconnected while opening browser context`);
2272
- return { tabs: w.available_tabs };
2300
+ const workers = coordinator.workers.list();
2301
+ if (workers.length > 1) {
2302
+ 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");
2303
+ }
2304
+ if (workers.length === 1) {
2305
+ return { status: "connected", tab_count: workers[0].available_tabs.length };
2306
+ }
2307
+ const pairUrl = deps.pairUrl();
2308
+ if (!pairUrl) throw new Error("PAIRING_REQUIRED: \u914D\u5BF9\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u4F46\u914D\u5BF9\u9875\u5C1A\u4E0D\u53EF\u7528");
2309
+ return { status: "pairing_required", pair_url: pairUrl };
2310
+ }
2311
+ async function handleListTabs(deps) {
2312
+ const { coordinator } = await deps.ensure();
2313
+ const workers = coordinator.workers.list();
2314
+ if (workers.length > 1) {
2315
+ 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");
2316
+ }
2317
+ const worker = workers[0];
2318
+ if (!worker) {
2319
+ const pairUrl = deps.pairUrl();
2320
+ if (!pairUrl) throw new Error("PAIRING_REQUIRED: \u914D\u5BF9\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u4F46\u914D\u5BF9\u9875\u5C1A\u4E0D\u53EF\u7528");
2321
+ return { status: "pairing_required", pair_url: pairUrl, tabs: [] };
2322
+ }
2323
+ return { status: "connected", tabs: worker.available_tabs };
2273
2324
  }
2274
- async function handleOpenSession(deps, args, onPairingRequired) {
2325
+ async function handleOpenSession(deps, args) {
2275
2326
  const { coordinator } = await deps.ensure();
2276
- const worker_id = await deps.waitForWorker(void 0, onPairingRequired);
2327
+ const worker_id = singleWorkerId(coordinator, deps.pairUrl());
2277
2328
  const tab_id = String(args.tab_id);
2278
2329
  const requested = Array.isArray(args.capabilities) ? args.capabilities.map(String).filter(isCapability) : [];
2279
2330
  const scope = new Set(requested.length ? requested : CAPABILITIES);
@@ -2365,6 +2416,37 @@ import { dirname, join } from "path";
2365
2416
  import { fileURLToPath } from "url";
2366
2417
  var SKILL_NAME = "atwebpilot-browser";
2367
2418
  var SKILL_DESCRIPTION = "Strategy + scenarios + safety rails for driving the AtWebPilot browser extension via MCP.";
2419
+ var TOPICS = {
2420
+ quickstart: `# AtWebPilot quickstart
2421
+
2422
+ 1. Call list_tabs once. If it returns pairing_required, show pair_url and wait for the user to approve.
2423
+ 2. After approval, call pairing_status. Do not retry list_tabs while status is unchanged.
2424
+ 3. Call list_tabs when connected, choose a tab, then open_session with its tab_id.
2425
+ 4. For interaction use browser_getPageInfo then browser_takeSnapshot; for broad reading use browser_createPageIndex.
2426
+ 5. Prefer purpose-built tools and verified results. Use browser_discoverTools only for capabilities outside the connected core surface.
2427
+ 6. Close the session when finished.
2428
+ `,
2429
+ pairing: `# AtWebPilot pairing
2430
+
2431
+ list_tabs and pairing_status never wait for browser authorization. A pairing_required result includes pair_url. Show that URL once and wait for the user to approve in the browser. Then call pairing_status; only call list_tabs after it reports connected. Never poll list_tabs or inspect the pair-page HTML.
2432
+ `,
2433
+ reading: `# AtWebPilot reading
2434
+
2435
+ Call browser_getPageInfo first. For articles, products, tables, and forms, use browser_createPageIndex, then browser_extractPageFields or browser_searchPageIndex, and read only the relevant block with browser_readPageBlock. Avoid browser_extractText on body and full-DOM reads. Use targeted screenshots only when visual evidence matters.
2436
+ `,
2437
+ interaction: `# AtWebPilot interaction
2438
+
2439
+ Refresh browser_takeSnapshot before each major action. Prefer uid-based browser_clickByUid/browser_fillByUid, and use browser_fillForm for independent fields. Use condition-based browser_waitFor rather than fixed delays. Keep submit, upload, storage, and credentialed network actions as explicit reviewed steps.
2440
+ `,
2441
+ visual: `# AtWebPilot visual validation
2442
+
2443
+ Use browser_resize and trust its actualViewport/verified result. Capture one browser_screenshot after the page settles; target a selector or page-index block and lower scale when detail permits. Do not follow browser_resize or browser_screenshot with browser_runJS that only re-reads viewport dimensions.
2444
+ `,
2445
+ debugging: `# AtWebPilot debugging
2446
+
2447
+ Read browser_consoleMessages first, then browser_networkRequests summaries. Arm recorder bodies only when needed, reproduce once, and fetch one browser_networkRequestDetail. Poll incrementally with sinceId. Check backend and degradedReason before treating missing events as proof.
2448
+ `
2449
+ };
2368
2450
  var cached = null;
2369
2451
  function locateSkillFile() {
2370
2452
  let here = dirname(fileURLToPath(import.meta.url));
@@ -2379,7 +2461,13 @@ function locateSkillFile() {
2379
2461
  }
2380
2462
  return join(dirname(fileURLToPath(import.meta.url)), "../../../skill/SKILL.md");
2381
2463
  }
2382
- function readSkillBundle() {
2464
+ function readSkillBundle(rawTopic = "quickstart") {
2465
+ const topic = rawTopic == null ? "quickstart" : String(rawTopic);
2466
+ if (topic !== "full") {
2467
+ const content = TOPICS[topic];
2468
+ if (!content) throw new Error(`Unknown skill topic: ${topic}`);
2469
+ return { name: SKILL_NAME, description: SKILL_DESCRIPTION, content };
2470
+ }
2383
2471
  if (cached == null) {
2384
2472
  try {
2385
2473
  cached = readFileSync(locateSkillFile(), "utf-8");
@@ -2396,15 +2484,29 @@ ${SKILL_DESCRIPTION}
2396
2484
  }
2397
2485
  var SKILL_TOOL = {
2398
2486
  name: "atwebpilot_skill_read",
2399
- description: "Return the `atwebpilot-browser` skill bundle \u2014 a recommended tool-usage flow, scenarios, and safety rails for driving AtWebPilot's browser tools.",
2400
- inputSchema: { type: "object", properties: {}, required: [] }
2487
+ description: "Return concise AtWebPilot guidance by topic. Defaults to quickstart; request full only when detailed reference is necessary.",
2488
+ inputSchema: {
2489
+ type: "object",
2490
+ properties: {
2491
+ topic: {
2492
+ type: "string",
2493
+ enum: ["quickstart", "pairing", "reading", "interaction", "visual", "debugging", "full"],
2494
+ description: "Guidance section; quickstart is the token-efficient default"
2495
+ }
2496
+ },
2497
+ required: [],
2498
+ additionalProperties: false
2499
+ }
2401
2500
  };
2402
2501
 
2403
2502
  // src/mcp-server.ts
2404
2503
  var ALL_BROWSER_TOOLS = generateBrowserTools("full");
2405
2504
  var BROWSER_BY_NAME = new Map(ALL_BROWSER_TOOLS.map((t) => [t.name, t]));
2406
2505
  function createToolState(mode) {
2407
- return { advertised: new Set(generateBrowserTools(mode).map((t) => t.name)) };
2506
+ return {
2507
+ advertised: new Set(generateBrowserTools(mode).map((t) => t.name)),
2508
+ browserSurfaceVisible: false
2509
+ };
2408
2510
  }
2409
2511
  var DEFAULT_STATE = createToolState(readToolMode(process.env));
2410
2512
  var LEGACY_TOOLS = [
@@ -2438,7 +2540,7 @@ function workerToolSupport(deps) {
2438
2540
  }
2439
2541
  function buildToolList(deps, state = DEFAULT_STATE) {
2440
2542
  const supported = workerToolSupport(deps);
2441
- const browser = ALL_BROWSER_TOOLS.filter((t) => state.advertised.has(t.name)).filter((t) => !supported || t.builtinTools.every((b) => supported.has(b)));
2543
+ const browser = state.browserSurfaceVisible || supported ? ALL_BROWSER_TOOLS.filter((t) => state.advertised.has(t.name)).filter((t) => !supported || t.builtinTools.every((b) => supported.has(b))) : [];
2442
2544
  return [
2443
2545
  { name: SKILL_TOOL.name, description: SKILL_TOOL.description, inputSchema: SKILL_TOOL.inputSchema },
2444
2546
  ...CONTROL_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
@@ -2462,10 +2564,10 @@ function toolResult(gen, data) {
2462
2564
  return { content };
2463
2565
  }
2464
2566
  var fail2 = (message) => ({ content: [{ type: "text", text: message }], isError: true });
2465
- async function dispatchCall(deps, name, args, onPairingRequired, state = DEFAULT_STATE, onListChanged) {
2567
+ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChanged) {
2466
2568
  try {
2467
2569
  if (name === SKILL_TOOL.name) {
2468
- const bundle = readSkillBundle();
2570
+ const bundle = readSkillBundle(args.topic);
2469
2571
  return { content: [{ type: "text", text: bundle.content }] };
2470
2572
  }
2471
2573
  if (name === DISCOVER_TOOL.name) {
@@ -2479,8 +2581,21 @@ async function dispatchCall(deps, name, args, onPairingRequired, state = DEFAULT
2479
2581
  const { changed: _c, ...body } = r;
2480
2582
  return ok(body);
2481
2583
  }
2482
- if (name === "list_tabs") return ok(await handleListTabs(deps, onPairingRequired));
2483
- if (name === "open_session") return ok(await handleOpenSession(deps, args, onPairingRequired));
2584
+ if (name === "pairing_status") {
2585
+ const result = await handlePairingStatus(deps);
2586
+ if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
2587
+ return ok(result);
2588
+ }
2589
+ if (name === "list_tabs") {
2590
+ const result = await handleListTabs(deps);
2591
+ if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
2592
+ return ok(result);
2593
+ }
2594
+ if (name === "open_session") {
2595
+ const result = await handleOpenSession(deps, args);
2596
+ await revealBrowserSurface(state, onListChanged);
2597
+ return ok(result);
2598
+ }
2484
2599
  if (name === "close_session") return ok(await handleCloseSession(deps, args));
2485
2600
  if (name === "get_quota") return ok(await handleGetQuota(deps, args));
2486
2601
  const gen = BROWSER_BY_NAME.get(name);
@@ -2490,33 +2605,19 @@ async function dispatchCall(deps, name, args, onPairingRequired, state = DEFAULT
2490
2605
  return fail2(e instanceof Error ? e.message : String(e));
2491
2606
  }
2492
2607
  }
2608
+ async function revealBrowserSurface(state, onListChanged) {
2609
+ if (state.browserSurfaceVisible) return;
2610
+ state.browserSurfaceVisible = true;
2611
+ await onListChanged?.();
2612
+ }
2493
2613
  function createMcpServer(deps, state = DEFAULT_STATE) {
2494
2614
  const server = new Server(
2495
2615
  { name: "atwebpilot-mcp", version: "0.1.0" },
2496
2616
  { capabilities: { tools: { listChanged: true }, logging: {} } }
2497
2617
  );
2498
2618
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList(deps, state) }));
2499
- server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
2619
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
2500
2620
  const args = req.params.arguments ?? {};
2501
- const onPairingRequired = async (url) => {
2502
- const message = `\u7B49\u5F85\u6D4F\u89C8\u5668\u6388\u6743\uFF08\u6700\u591A 90 \u79D2\uFF09\u3002\u914D\u5BF9\u9875\uFF1A${url}\u3002\u5DF2\u5C1D\u8BD5\u81EA\u52A8\u6253\u5F00\uFF1B\u82E5\u672A\u5F39\u51FA\uFF0C\u8BF7\u624B\u52A8\u6253\u5F00\u3002`;
2503
- try {
2504
- const progressToken = extra._meta?.progressToken;
2505
- if (progressToken != null) {
2506
- await extra.sendNotification({
2507
- method: "notifications/progress",
2508
- params: { progressToken, progress: 0, total: 1, message }
2509
- });
2510
- } else {
2511
- await server.sendLoggingMessage({ level: "info", logger: "atwebpilot", data: message });
2512
- }
2513
- } catch (error) {
2514
- console.error(
2515
- "[atwebpilot-mcp] failed to report pairing URL:",
2516
- error instanceof Error ? error.message : String(error)
2517
- );
2518
- }
2519
- };
2520
2621
  const onListChanged = async () => {
2521
2622
  try {
2522
2623
  await server.sendToolListChanged();
@@ -2524,7 +2625,7 @@ function createMcpServer(deps, state = DEFAULT_STATE) {
2524
2625
  console.error("[atwebpilot-mcp] failed to send tools/list_changed:", error instanceof Error ? error.message : String(error));
2525
2626
  }
2526
2627
  };
2527
- return dispatchCall(deps, req.params.name, args, onPairingRequired, state, onListChanged);
2628
+ return dispatchCall(deps, req.params.name, args, state, onListChanged);
2528
2629
  });
2529
2630
  return server;
2530
2631
  }
@@ -3011,6 +3112,7 @@ function processInfo(cwd = process.cwd()) {
3011
3112
 
3012
3113
  // src/ensure-hub.ts
3013
3114
  var DEFAULT_WORKER_WAIT_TIMEOUT_MS = 9e4;
3115
+ var SESSION_SWEEP_INTERVAL_MS = 3e4;
3014
3116
  function createHubEnsurer(d) {
3015
3117
  let bundle = null;
3016
3118
  let inflight = null;
@@ -3019,6 +3121,7 @@ function createHubEnsurer(d) {
3019
3121
  let opening = null;
3020
3122
  let hubIsClosed = false;
3021
3123
  let pendingDenial = false;
3124
+ let sessionSweepTimer = null;
3022
3125
  let workerWait = null;
3023
3126
  const info = d.processInfo ?? processInfo();
3024
3127
  function connectedWorkerId(coordinator) {
@@ -3054,6 +3157,10 @@ function createHubEnsurer(d) {
3054
3157
  }
3055
3158
  function hubClosed() {
3056
3159
  hubIsClosed = true;
3160
+ if (sessionSweepTimer) {
3161
+ clearInterval(sessionSweepTimer);
3162
+ sessionSweepTimer = null;
3163
+ }
3057
3164
  if (workerWait) {
3058
3165
  finishWorkerWait({ error: new Error("\u6D4F\u89C8\u5668\u914D\u5BF9\u670D\u52A1\u5DF2\u5173\u95ED\u3002") });
3059
3166
  }
@@ -3082,6 +3189,8 @@ function createHubEnsurer(d) {
3082
3189
  }
3083
3190
  saveLastPort(port, d.identityDir);
3084
3191
  const coordinator = new Coordinator({ hub, clock: d.clock, idGen: d.idGen });
3192
+ sessionSweepTimer = setInterval(() => coordinator.tick(), SESSION_SWEEP_INTERVAL_MS);
3193
+ sessionSweepTimer.unref?.();
3085
3194
  installWire(hub, coordinator, d.clock, () => notifyWorkerReady(coordinator));
3086
3195
  pairUrlValue = `http://127.0.0.1:${port}/pair`;
3087
3196
  return { coordinator, hub, port };
@@ -0,0 +1,261 @@
1
+ # atwebpilot-browser skill
2
+
3
+ A reusable strategy for agents that drive AtWebPilot's browser extension via
4
+ MCP. Inspired by AIPex's [aipex-browser](https://github.com/AIPexStudio/AIPex)
5
+ pattern: tool usage flow, common scenarios, safety rails — all in one bundle
6
+ so an agent doesn't have to discover them from scratch.
7
+
8
+ ## Capabilities you have
9
+
10
+ When this skill is loaded, you can drive any open tab through these tools
11
+ (exposed by `@attson/atwebpilot-mcp`):
12
+
13
+ ### Control plane
14
+
15
+ - `pairing_status` — start/check pairing without waiting. When it reports
16
+ `pairing_required`, show `pair_url` once and wait for browser approval.
17
+ - `list_tabs` — enumerate Chrome tabs you can operate on. Entries carry `busy`
18
+ and `busy_label` when **another** Claude Code session is already driving that
19
+ tab, and `mine` for tabs you hold. This is advisory — nothing blocks you — but
20
+ contending for a tab another agent is typing into is rarely what you want.
21
+ Open your own with `browser_openTab` instead.
22
+ - `open_session(tabId)` — pin a tab so subsequent calls target it
23
+ - `close_session(sessionId)` — release the tab
24
+ - `get_quota(sessionId)` — see remaining requests this minute
25
+
26
+ ### Browser-side built-ins (per session)
27
+
28
+ All of these are prefixed `browser_` over MCP, e.g. `browser_takeSnapshot`.
29
+
30
+ **Core — advertised after the browser connects (31):**
31
+
32
+ | Class | Tools |
33
+ |---|---|
34
+ | page state(safe) | `takeSnapshot`, `findElements`, `getPageInfo`, `extractText` |
35
+ | page index(safe) | `createPageIndex`, `searchPageIndex`, `readPageBlock`, `extractPageFields` |
36
+ | interaction(caution) | `clickByUid`, `click`, `fillByUid`, `fillInput`, `fillForm`, `selectOption`, `setCheckbox`, `hover`, `pressKey`, `drag`, `drop`(files ⇒ dangerous), `uploadFile`(dangerous) |
37
+ | navigation / tabs | `navigate`(`action` 取 `back` / `forward` / `reload` / `goto`), `listTabs`, `openTab`, `closeTab`, `resize`, `scroll` |
38
+ | observation | `screenshot`, `waitFor`, `runJS`(static-scanned), `consoleMessages`, `networkRequests` |
39
+
40
+ **Discoverable — enable directly with `browser_discoverTools`:**
41
+
42
+ | Group | Tools |
43
+ |---|---|
44
+ | `export` | `downloadImage`, `downloadSpreadsheet`(real `.xlsx`, multi-sheet) |
45
+ | `network` | `httpRequest`(`withCredentials` ⇒ dangerous), `networkRequestDetail`(dangerous), `recorderConfig`, `handleDialog` |
46
+ | `storage` | `storage`(`op` 取 `get` / `set`; dangerous) |
47
+ | `browser-data` | `searchBookmarks`, `searchHistory` |
48
+ | `inspect` | `inspectElement`, `highlight`(`text` or `selector`/`uid`), `getValue`, `extractFormState` |
49
+ | `legacy-dom` | `snapshotDOM`, `querySelector`, `querySelectorAll`, `extractImages`, `focus` |
50
+ | `form` | `submitForm`(dangerous) |
51
+ | `tabs` | `switchToTab`(只在用户明确要求把 Chrome 切到前台时使用) |
52
+
53
+ `askUser`, `attachTab` and `detachTab` are **not** exposed: an MCP session has
54
+ no human at the side panel, and its tab is already bound by `open_session`.
55
+
56
+ ### Getting more tools
57
+
58
+ The default list is deliberately small. When a task needs something outside
59
+ core — exporting a spreadsheet, calling an API with cookies, reading
60
+ localStorage, inspecting a request body, searching history, debugging layout —
61
+ do this, in order:
62
+
63
+ 1. If you know the tool, enable it directly with
64
+ `browser_discoverTools({ enable: ["browser_downloadSpreadsheet", ...] })`.
65
+ If you know only the group, use
66
+ `browser_discoverTools({ enableGroups: ["export"] })`.
67
+ 2. Call `browser_discoverTools({})` only when neither the tool nor group is
68
+ known. Enabled tools join `tools/list` (a `tools/list_changed` notification
69
+ is sent), and the response carries their full schemas for immediate use.
70
+
71
+ **Do not** rebuild these capabilities with `runJS` (`fetch`, CSV blobs,
72
+ `localStorage[...]`). It is slower, loses schema validation, and usually trips
73
+ the dangerous-tier review that the purpose-built tool would have avoided.
74
+
75
+ `ATWEBPILOT_MCP_TOOLS=full` advertises everything from the start for users who
76
+ prefer to pay the context cost once.
77
+
78
+ ## Recommended flow
79
+
80
+ 1. **先建立连接**:调一次 `list_tabs`;若返回 `pairing_required`,展示 `pair_url` 并等待用户批准。批准后只调 `pairing_status`,它报告 `connected` 后再调 `list_tabs`,不要轮询 `list_tabs`。
81
+ 2. **探查先于操作**:每次进入新页面,先 `getPageInfo` 确认位置,再 `takeSnapshot`(要点击/填表)或 `createPageIndex`(要读内容/抽字段);`snapshotDOM` / `querySelector` 属于 legacy-dom 组,只在需要分析整页结构时通过 `browser_discoverTools` 启用。
82
+ 3. **最少充分验证**:危险或不可逆操作保持单步;多个独立字段用 `fillForm`。工具已返回 `verified` 或明确后置状态时,不要再用 `runJS` 重复验证。
83
+ 4. **dangerous 工具会被人工审核**:调用前用 `extractText` 给用户看上下文,让审批更顺。
84
+ 5. **跑不动了就停下来问**:候选不唯一、缺关键信息、需要二次确认时,把问题写在回复里交给用户,不要瞎猜(MCP 会话没有 `askUser`)。
85
+ 6. **完成后给一个简洁的总结**:用户希望看到「做了 N 步,最终结果 X」,不希望看流水账。
86
+
87
+ ## Tool usage notes (moved here from the tool descriptions)
88
+
89
+ - `snapshotDOM`: `{}` (maxDepth 3) / `{ root: '.main', maxDepth: 5 }` / `{ maxDepth: 8 }`. Prefer `takeSnapshot` when you will click or fill afterwards.
90
+ - `querySelector` / `querySelectorAll`: `{ selector: 'button[type=submit]' }`, `{ selector: '.comment-item', limit: 50 }`.
91
+ - `extractText`: `{ selector: 'h1', single: true }`, `{ selector: 'article p' }`. Never `body` — use `createPageIndex`.
92
+ - `extractImages`: `{}` for the page, `{ root: '.product-gallery' }` for a region.
93
+ - `scroll`: `{ to: 'bottom', max: 5 }` for lazy loading; `{ to: 'bottom', max: 10, untilSelector: '.item:nth-child(20)' }` to stop once content appears.
94
+ - `waitFor`: `{ ms: 500 }` or `{ selector: '.lazy-loaded', timeoutMs: 8000 }`.
95
+ - `fillInput`: `{ selector: 'input[name=email]', value: 'a@b.c' }`; `clear: false` appends.
96
+ - `fillForm`: `{ fields: [{ selector: 'input[name=name]', value: '张三' }, { uid: 'el_5', value: 'mushroom' }] }` — much cheaper than repeated `fillInput`.
97
+ - `pressKey`: `{ selector: 'input[name=q]', key: 'Enter' }` submits a form-less search; `{ key: 'Escape' }` closes a modal.
98
+ - `navigate`: `{ action: 'back' }`, `{ action: 'goto', url: 'https://example.com/page' }`.
99
+ - `switchToTab`: all tools already target the session-bound tab. Discover and call this only when the user explicitly asks to bring Chrome and that tab to the foreground; never use it as setup for click, wait, runJS, or screenshot.
100
+ - `resize`: returns measured `actualViewport`, `devicePixelRatio`, and `verified`; do not follow it with a viewport-only `runJS` probe.
101
+ - `screenshot`: the image block is followed by compact capture metadata (backend, dimensions and target when available); use that instead of probing the viewport again.
102
+ - `httpRequest`: `{ url: '.../api/comments?page=2' }` (no cookies) vs `{ url, withCredentials: true }` (cookied, reviewed).
103
+ - `consoleMessages`: `{ level: 'error', limit: 50 }`; incremental polling with `{ sinceId }`.
104
+ - `networkRequestDetail`: arm bodies first with `recorderConfig({ bodies: true })` in main-world mode.
105
+ - `drop`: `{ selector: '#dropzone', files: [{ name: 'a.csv', mimeType: 'text/csv', base64: '...' }] }`.
106
+ - `downloadSpreadsheet`: `{ filename: 'items', sheets: [{ name: 'Sheet1', rows: [{ title: 'A', price: 12 }], columns: [{ key: 'title', header: '标题' }, { key: 'price' }] }] }`.
107
+ - `storage`: `{ op: 'get', store: 'local', key: 'token' }`, `{ op: 'set', store: 'session', key: 'k', value: '{"a":1}' }`.
108
+ - `highlight`: `{ text: 'Checkout' }` or `{ selector: '#pay' }` / `{ uid: 'el_3' }`; visual only, 3 s by default.
109
+
110
+ ## Scenarios
111
+
112
+ ### 总结此页
113
+
114
+ ```
115
+ createPageIndex({})
116
+ extractText({ selector: "main, article, .content" })
117
+ → 文本总结
118
+ ```
119
+
120
+ ### 填表 + 提交
121
+
122
+ `submitForm` 属于 form 组,先 `browser_discoverTools({ enable: ["browser_submitForm"] })` 启用。
123
+
124
+ ```
125
+ takeSnapshot() // 找输入框
126
+ fillInput({ selector: "#name", value: "张三" })
127
+ setCheckbox({ selector: "#agree", checked: true })
128
+ selectOption({ selector: "#city", value: "北京" })
129
+ → 在回复中向用户确认「确认提交吗?」
130
+ → submitForm() // dangerous,用户确认后执行
131
+ ```
132
+
133
+ ### 翻页采集
134
+
135
+ `snapshotDOM` 属于 legacy-dom 组,先 `browser_discoverTools({ enable: ["browser_snapshotDOM"] })` 启用。
136
+
137
+ ```
138
+ snapshotDOM({ selector: "[data-pagination]" }) // 找翻页结构
139
+ extractText({ selector: ".item", multiple: true })
140
+ → 累积到内存
141
+ → click({ selector: ".next" })
142
+ waitFor({ selector: ".item:nth-child(N+1)" })
143
+ → 重复
144
+ ```
145
+
146
+ ### 跨 tab 协作
147
+
148
+ ```
149
+ listTabs()
150
+ openTab({ url: "https://example.com" }) // 自动 attach
151
+ → 在新 tab 操作
152
+ ```
153
+
154
+ ## Safety / quota rails
155
+
156
+ - **Quota**:默认 60 请求/分钟。超过会被拒绝,到 60s 自动恢复。先 `get_quota` 看额度。
157
+ - **Dangerous gating**:用户已在 sidepanel 配置了授权模式(read / default / trust / yolo),你不需要预判,调就是了,会被拦下来或自动通过。
158
+ - **超时**:默认每步 30s。慢操作(大页面 snapshot、长等待)记得显式传 `timeoutMs`。
159
+
160
+ ## Don'ts
161
+
162
+ - ❌ 不要在 `runJS` 里直接读 cookie / localStorage(会被静态扫描归 dangerous,每次问审批)
163
+ - ❌ 不要循环调 `snapshotDOM`——单次抓全树用 `maxDepth` 而不是多次
164
+ - ❌ 不要在回复里频繁向用户确认(每次都要用户手动响应)
165
+ - ❌ 不要无视 `attachedTabs`——跨 tab 操作前确认目标 tab 未被其他会话占用(`list_tabs` 的 `busy`)
166
+
167
+ ## When to use
168
+
169
+ - 用户要你「在网页上做某事」、「抓某站点数据」、「填某表单」
170
+ - 用户给了一个 URL 让你看
171
+ - 用户问「这页面在讲什么」
172
+
173
+ ## When NOT to use
174
+
175
+ - 纯文本任务(写代码、改文件)不需要浏览器
176
+ - 用户明确说「不要打开浏览器」
177
+ - 网页需要复杂登录但用户没在浏览器里登录过
178
+
179
+ ## Reference
180
+
181
+ - README: https://github.com/attson/atwebpilot
182
+ - 工具完整 schema 由 mcp-server 的 `tools/list` 暴露
183
+
184
+ ## Observing a page: console, network, dialogs
185
+
186
+ These read from a recorder that is running on the page already, so you can ask
187
+ about things that happened *before* you called.
188
+
189
+ **Two backends, and you must check which one you got.** Every result carries
190
+ `backend`:
191
+
192
+ - `main-world` (default) — patches installed in the page's own realm. Cannot
193
+ see messages emitted before the patches installed, browser-level CORS/CSP
194
+ errors, or requests made by a service worker.
195
+ - `cdp` — `chrome.debugger`, opt-in from the extension settings. Sees all of
196
+ the above plus real response bodies.
197
+
198
+ If a result carries `degradedReason`, a CDP session was expected but is gone
199
+ (DevTools opened, or another extension took the tab). Treat the data as
200
+ incomplete rather than concluding the page did nothing. If it carries
201
+ `disabled`, the recorder is not on that page at all.
202
+
203
+ **Response bodies are off by default.** They cost memory on every page the user
204
+ visits, so arm them first and then reproduce:
205
+
206
+ ```
207
+ browser_recorderConfig({ bodies: true }) → arm
208
+ … trigger the request …
209
+ browser_networkRequests({ urlPattern: "/api/" })
210
+ browser_networkRequestDetail({ id: <id from above> })
211
+ ```
212
+
213
+ Calling `networkRequestDetail` without arming returns metadata plus
214
+ `bodyUnavailable` explaining the fix. Note this tool is **dangerous**: response
215
+ headers routinely carry `Authorization`, `Set-Cookie`, and bearer tokens.
216
+
217
+ **`handleDialog` is a pre-set policy on the default backend, not a reaction.**
218
+ `alert`, `confirm`, and `prompt` are synchronous, so a patched implementation
219
+ cannot pause and ask you. Declare what should happen *before* triggering the
220
+ dialog:
221
+
222
+ ```
223
+ browser_handleDialog({ accept: true, promptText: "yes", scope: "all" })
224
+ … click the thing that opens the dialog …
225
+ browser_handleDialog({ accept: true }) → also returns the recorded dialogs
226
+ ```
227
+
228
+ Under the CDP backend the dialog genuinely suspends, and a `handleDialog` call
229
+ answers the pending one immediately.
230
+
231
+ Until you arm it, dialogs behave exactly as they would without the extension —
232
+ the recorder never silently changes page behaviour.
233
+
234
+ ## Debugging flow
235
+
236
+ When a page misbehaves, this ordering costs the least context:
237
+
238
+ 1. `browser_consoleMessages({ level: "error" })` — cheap, and already buffered.
239
+ 2. `browser_networkRequests({ status: 500 })` or `{ urlPattern: "/api/" }` —
240
+ summaries only.
241
+ 3. `browser_recorderConfig({ bodies: true })`, reproduce, then
242
+ `browser_networkRequestDetail({ id })` for the one request that matters.
243
+
244
+ Poll incrementally with `sinceId` rather than re-reading the whole buffer. If a
245
+ result reports `dropped > 0`, the ring overflowed and you are seeing a window,
246
+ not the whole history.
247
+
248
+ ## Multiple sessions share one browser
249
+
250
+ Several Claude Code sessions can be attached to the same browser at once, each
251
+ through its own connection. Two consequences worth knowing:
252
+
253
+ - **Pairing is non-blocking.** `list_tabs` lazily binds the port and opens a
254
+ confirmation page, then immediately returns `pairing_required` with
255
+ `pair_url`. Ask the user to click Allow. After they confirm, call
256
+ `pairing_status`; call `list_tabs` again only when it reports `connected`.
257
+ Never inspect the pair-page HTML or retry `list_tabs` while state is unchanged.
258
+ Once approved, later sessions on that machine connect silently.
259
+ - **Pick a free tab.** `list_tabs` marks tabs held by other sessions as `busy`.
260
+ Prefer an unowned tab, or open your own — two agents typing into one form
261
+ produces failures that look like your own tool calls not working.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attson/atwebpilot-mcp",
3
- "version": "0.0.73",
3
+ "version": "0.0.75",
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",
@@ -26,7 +26,7 @@
26
26
  "scripts": {
27
27
  "typecheck": "tsc --noEmit",
28
28
  "test": "vitest run",
29
- "build": "tsup",
29
+ "build": "tsup && node scripts/copy-skill.mjs",
30
30
  "start": "tsx src/index.ts",
31
31
  "analyze:sessions": "tsx src/session-analysis/cli.ts"
32
32
  },