@poolzin/pool-bot 2026.3.22 → 2026.3.24

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 (159) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/bindings-store.js +209 -0
  4. package/dist/acp/control-plane/runtime-cache.js +54 -0
  5. package/dist/acp/control-plane/runtime-options.js +215 -0
  6. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  7. package/dist/acp/policy.js +52 -0
  8. package/dist/acp/runtime/errors.js +47 -0
  9. package/dist/acp/runtime/registry.js +86 -0
  10. package/dist/acp/runtime/types.js +1 -0
  11. package/dist/acp/translator.js +97 -0
  12. package/dist/agents/btw.js +280 -0
  13. package/dist/agents/failover-error.js +145 -47
  14. package/dist/agents/fast-mode.js +24 -0
  15. package/dist/agents/live-model-errors.js +23 -0
  16. package/dist/agents/model-auth-env-vars.js +44 -0
  17. package/dist/agents/model-auth-markers.js +69 -0
  18. package/dist/agents/models-config.providers.discovery.js +180 -0
  19. package/dist/agents/models-config.providers.static.js +480 -0
  20. package/dist/auto-reply/reply/typing-policy.js +15 -0
  21. package/dist/browser/browser-profile-manager.js +319 -0
  22. package/dist/browser/cdp-proxy-bypass.js +129 -0
  23. package/dist/browser/cdp-timeouts.js +41 -0
  24. package/dist/browser/chrome-extension-validator.js +406 -0
  25. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  26. package/dist/browser/chrome-mcp.js +421 -0
  27. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  28. package/dist/browser/errors.js +67 -0
  29. package/dist/browser/form-fields.js +22 -0
  30. package/dist/browser/output-atomic.js +44 -0
  31. package/dist/browser/profile-capabilities.js +47 -0
  32. package/dist/browser/safe-filename.js +25 -0
  33. package/dist/browser/snapshot-roles.js +60 -0
  34. package/dist/build-info.json +3 -3
  35. package/dist/channels/account-snapshot-fields.js +176 -0
  36. package/dist/channels/draft-stream-controls.js +89 -0
  37. package/dist/channels/inbound-debounce-policy.js +28 -0
  38. package/dist/channels/typing-lifecycle.js +39 -0
  39. package/dist/cli/program/command-registry.js +52 -0
  40. package/dist/commands/agent-binding.js +123 -0
  41. package/dist/commands/agents.commands.bind.js +280 -0
  42. package/dist/commands/backup-shared.js +186 -0
  43. package/dist/commands/backup-verify.js +236 -0
  44. package/dist/commands/backup.js +166 -0
  45. package/dist/commands/channel-account-context.js +15 -0
  46. package/dist/commands/channel-account.js +190 -0
  47. package/dist/commands/gateway-install-token.js +117 -0
  48. package/dist/commands/oauth-tls-preflight.js +121 -0
  49. package/dist/commands/ollama-setup.js +402 -0
  50. package/dist/commands/security-owner-only.js +86 -0
  51. package/dist/commands/self-hosted-provider-setup.js +207 -0
  52. package/dist/commands/session-store-targets.js +12 -0
  53. package/dist/commands/sessions-cleanup.js +97 -0
  54. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  55. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  56. package/dist/control-ui/index.html +1 -1
  57. package/dist/cron/cron-filters.js +150 -0
  58. package/dist/cron/heartbeat-policy.js +26 -0
  59. package/dist/gateway/device-pairing-security.js +197 -0
  60. package/dist/gateway/event-deduplication.js +167 -0
  61. package/dist/gateway/hooks-mapping.js +46 -7
  62. package/dist/gateway/run-tracker.js +253 -0
  63. package/dist/gateway/server-methods/nodes.js +14 -0
  64. package/dist/gateway/websocket-preauth-security.js +188 -0
  65. package/dist/hooks/module-loader.js +28 -0
  66. package/dist/infra/agent-command-binding.js +144 -0
  67. package/dist/infra/backup.js +328 -0
  68. package/dist/infra/channel-account-context.js +173 -0
  69. package/dist/infra/errors.js +53 -13
  70. package/dist/infra/exec-approvals-security.js +217 -0
  71. package/dist/infra/security/command-analyzer.js +257 -0
  72. package/dist/infra/session-cleanup.js +143 -0
  73. package/dist/plugins/loader.js +16 -8
  74. package/dist/security/external-content.js +51 -1
  75. package/dist/sessions/session-costs.js +228 -0
  76. package/dist/shared/param-key.js +16 -0
  77. package/dist/shared/poll-params.js +58 -0
  78. package/dist/shared/polls.js +55 -0
  79. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  80. package/docs/FEATURES.md +523 -0
  81. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  82. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  83. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  84. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  85. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  86. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  87. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  88. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  89. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  90. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  91. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  92. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  93. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  94. package/docs/PHASE-7-SUMMARY.md +144 -0
  95. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  96. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  97. package/docs/README.md +116 -0
  98. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  99. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  100. package/docs/channels/googlechat.md +235 -206
  101. package/docs/channels/irc.md +332 -0
  102. package/docs/channels/nostr.md +255 -168
  103. package/docs/components/command-palette.md +166 -0
  104. package/docs/components/login-gate.md +219 -0
  105. package/docs/getting-started/installation.md +191 -0
  106. package/docs/getting-started/introduction.md +120 -0
  107. package/docs/improvements/USAGE-GUIDE.md +359 -0
  108. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  109. package/docs/reference/deadcode-detection.md +72 -0
  110. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  111. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  112. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  113. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  114. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  115. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  116. package/extensions/googlechat/package.json +11 -28
  117. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  118. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  119. package/extensions/googlechat/src/index.ts +14 -0
  120. package/extensions/irc/node_modules/.bin/tsc +21 -0
  121. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  122. package/extensions/irc/node_modules/.bin/vitest +21 -0
  123. package/extensions/irc/package.json +16 -8
  124. package/extensions/irc/src/index.ts +14 -0
  125. package/extensions/irc/src/irc-channel.test.ts +43 -0
  126. package/extensions/irc/src/irc-channel.ts +191 -0
  127. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  128. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  129. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  130. package/extensions/keyed-async-queue/package.json +20 -0
  131. package/extensions/keyed-async-queue/src/index.ts +14 -0
  132. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  133. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  134. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  135. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  136. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  137. package/extensions/memory-core/package.json +11 -8
  138. package/extensions/memory-core/src/index.ts +14 -0
  139. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  140. package/extensions/memory-core/src/memory-manager.ts +186 -0
  141. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  142. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  143. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  144. package/extensions/nostr/package.json +15 -24
  145. package/extensions/nostr/src/index.ts +14 -0
  146. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  147. package/extensions/nostr/src/nostr-channel.ts +228 -0
  148. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  149. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  150. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  151. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  152. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  153. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  154. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  155. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  156. package/package.json +2 -1
  157. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  158. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  159. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -0,0 +1,421 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
+ import { BrowserProfileUnavailableError, BrowserTabNotFoundError } from "./errors.js";
8
+ const DEFAULT_CHROME_MCP_COMMAND = "npx";
9
+ const DEFAULT_CHROME_MCP_ARGS = [
10
+ "-y",
11
+ "chrome-devtools-mcp@latest",
12
+ "--autoConnect",
13
+ // Direct chrome-devtools-mcp launches do not enable structuredContent by default.
14
+ "--experimentalStructuredContent",
15
+ "--experimental-page-id-routing",
16
+ ];
17
+ const sessions = new Map();
18
+ const pendingSessions = new Map();
19
+ let sessionFactory = null;
20
+ const browserProfiles = new Map();
21
+ /**
22
+ * Register a browser profile for isolated sessions
23
+ */
24
+ export function registerBrowserProfile(config) {
25
+ browserProfiles.set(config.name, config);
26
+ }
27
+ /**
28
+ * Get registered browser profile
29
+ */
30
+ export function getBrowserProfile(name) {
31
+ return browserProfiles.get(name);
32
+ }
33
+ /**
34
+ * List all registered browser profiles
35
+ */
36
+ export function listBrowserProfiles() {
37
+ return Array.from(browserProfiles.values());
38
+ }
39
+ function asRecord(value) {
40
+ return value && typeof value === "object" && !Array.isArray(value)
41
+ ? value
42
+ : null;
43
+ }
44
+ function asPages(value) {
45
+ if (!Array.isArray(value)) {
46
+ return [];
47
+ }
48
+ const out = [];
49
+ for (const entry of value) {
50
+ const record = asRecord(entry);
51
+ if (!record || typeof record.id !== "number") {
52
+ continue;
53
+ }
54
+ out.push({
55
+ id: record.id,
56
+ url: typeof record.url === "string" ? record.url : undefined,
57
+ selected: record.selected === true,
58
+ });
59
+ }
60
+ return out;
61
+ }
62
+ function parsePageId(targetId) {
63
+ const parsed = Number.parseInt(targetId.trim(), 10);
64
+ if (!Number.isFinite(parsed)) {
65
+ throw new BrowserTabNotFoundError();
66
+ }
67
+ return parsed;
68
+ }
69
+ function toBrowserTabs(pages) {
70
+ return pages.map((page) => ({
71
+ targetId: String(page.id),
72
+ title: "",
73
+ url: page.url ?? "",
74
+ type: "page",
75
+ }));
76
+ }
77
+ function extractStructuredContent(result) {
78
+ return asRecord(result.structuredContent) ?? {};
79
+ }
80
+ function extractTextContent(result) {
81
+ const content = Array.isArray(result.content) ? result.content : [];
82
+ return content
83
+ .map((entry) => {
84
+ const record = asRecord(entry);
85
+ return record && typeof record.text === "string" ? record.text : "";
86
+ })
87
+ .filter(Boolean);
88
+ }
89
+ function extractTextPages(result) {
90
+ const pages = [];
91
+ for (const block of extractTextContent(result)) {
92
+ for (const line of block.split(/\r?\n/)) {
93
+ const match = line.match(/^\s*(\d+):\s+(.+?)(?:\s+\[(selected)\])?\s*$/i);
94
+ if (!match) {
95
+ continue;
96
+ }
97
+ pages.push({
98
+ id: Number.parseInt(match[1] ?? "", 10),
99
+ url: match[2]?.trim() || undefined,
100
+ selected: Boolean(match[3]),
101
+ });
102
+ }
103
+ }
104
+ return pages;
105
+ }
106
+ function extractStructuredPages(result) {
107
+ const structured = asPages(extractStructuredContent(result).pages);
108
+ return structured.length > 0 ? structured : extractTextPages(result);
109
+ }
110
+ function extractSnapshot(result) {
111
+ const structured = extractStructuredContent(result);
112
+ const snapshot = asRecord(structured.snapshot);
113
+ if (!snapshot) {
114
+ throw new Error("Chrome MCP snapshot response was missing structured snapshot data.");
115
+ }
116
+ return snapshot;
117
+ }
118
+ function extractJsonBlock(text) {
119
+ const match = text.match(/```json\s*([\s\S]*?)\s*```/i);
120
+ const raw = match?.[1]?.trim() || text.trim();
121
+ return raw ? JSON.parse(raw) : null;
122
+ }
123
+ function extractMessageText(result) {
124
+ const message = extractStructuredContent(result).message;
125
+ if (typeof message === "string" && message.trim()) {
126
+ return message;
127
+ }
128
+ const blocks = extractTextContent(result);
129
+ return blocks.find((block) => block.trim()) ?? "";
130
+ }
131
+ function extractToolErrorMessage(result, name) {
132
+ const message = extractMessageText(result).trim();
133
+ return message || `Chrome MCP tool "${name}" failed.`;
134
+ }
135
+ function extractJsonMessage(result) {
136
+ const candidates = [extractMessageText(result), ...extractTextContent(result)].filter((text) => text.trim());
137
+ let lastError;
138
+ for (const candidate of candidates) {
139
+ try {
140
+ return extractJsonBlock(candidate);
141
+ }
142
+ catch (err) {
143
+ lastError = err;
144
+ }
145
+ }
146
+ if (lastError) {
147
+ throw lastError;
148
+ }
149
+ return null;
150
+ }
151
+ async function createRealSession(profileName) {
152
+ const transport = new StdioClientTransport({
153
+ command: DEFAULT_CHROME_MCP_COMMAND,
154
+ args: DEFAULT_CHROME_MCP_ARGS,
155
+ stderr: "pipe",
156
+ });
157
+ const client = new Client({
158
+ name: "openclaw-browser",
159
+ version: "0.0.0",
160
+ }, {});
161
+ const ready = (async () => {
162
+ try {
163
+ await client.connect(transport);
164
+ const tools = await client.listTools();
165
+ if (!tools.tools.some((tool) => tool.name === "list_pages")) {
166
+ throw new Error("Chrome MCP server did not expose the expected navigation tools.");
167
+ }
168
+ }
169
+ catch (err) {
170
+ await client.close().catch(() => { });
171
+ throw new BrowserProfileUnavailableError(`Chrome MCP existing-session attach failed for profile "${profileName}". ` +
172
+ `Make sure Chrome (v146+) is running. ` +
173
+ `Details: ${String(err)}`);
174
+ }
175
+ })();
176
+ return {
177
+ client,
178
+ transport,
179
+ ready,
180
+ };
181
+ }
182
+ async function getSession(profileName) {
183
+ let session = sessions.get(profileName);
184
+ if (session && session.transport.pid === null) {
185
+ sessions.delete(profileName);
186
+ session = undefined;
187
+ }
188
+ if (!session) {
189
+ let pending = pendingSessions.get(profileName);
190
+ if (!pending) {
191
+ pending = (async () => {
192
+ const created = await (sessionFactory ?? createRealSession)(profileName);
193
+ sessions.set(profileName, created);
194
+ return created;
195
+ })();
196
+ pendingSessions.set(profileName, pending);
197
+ }
198
+ try {
199
+ session = await pending;
200
+ }
201
+ finally {
202
+ if (pendingSessions.get(profileName) === pending) {
203
+ pendingSessions.delete(profileName);
204
+ }
205
+ }
206
+ }
207
+ try {
208
+ await session.ready;
209
+ return session;
210
+ }
211
+ catch (err) {
212
+ const current = sessions.get(profileName);
213
+ if (current?.transport === session.transport) {
214
+ sessions.delete(profileName);
215
+ }
216
+ throw err;
217
+ }
218
+ }
219
+ async function callTool(profileName, name, args = {}) {
220
+ const session = await getSession(profileName);
221
+ let result;
222
+ try {
223
+ result = (await session.client.callTool({
224
+ name,
225
+ arguments: args,
226
+ }));
227
+ }
228
+ catch (err) {
229
+ // Transport/connection error — tear down session so it reconnects on next call
230
+ sessions.delete(profileName);
231
+ await session.client.close().catch(() => { });
232
+ throw err;
233
+ }
234
+ // Tool-level errors (element not found, script error, etc.) don't indicate a
235
+ // broken connection — don't tear down the session for these.
236
+ if (result.isError) {
237
+ throw new Error(extractToolErrorMessage(result, name));
238
+ }
239
+ return result;
240
+ }
241
+ async function withTempFile(fn) {
242
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-mcp-"));
243
+ const filePath = path.join(dir, randomUUID());
244
+ try {
245
+ return await fn(filePath);
246
+ }
247
+ finally {
248
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
249
+ }
250
+ }
251
+ async function findPageById(profileName, pageId) {
252
+ const pages = await listChromeMcpPages(profileName);
253
+ const page = pages.find((entry) => entry.id === pageId);
254
+ if (!page) {
255
+ throw new BrowserTabNotFoundError();
256
+ }
257
+ return page;
258
+ }
259
+ export async function ensureChromeMcpAvailable(profileName) {
260
+ await getSession(profileName);
261
+ }
262
+ export function getChromeMcpPid(profileName) {
263
+ return sessions.get(profileName)?.transport.pid ?? null;
264
+ }
265
+ export async function closeChromeMcpSession(profileName) {
266
+ pendingSessions.delete(profileName);
267
+ const session = sessions.get(profileName);
268
+ if (!session) {
269
+ return false;
270
+ }
271
+ sessions.delete(profileName);
272
+ await session.client.close().catch(() => { });
273
+ return true;
274
+ }
275
+ export async function stopAllChromeMcpSessions() {
276
+ const names = [...sessions.keys()];
277
+ for (const name of names) {
278
+ await closeChromeMcpSession(name).catch(() => { });
279
+ }
280
+ }
281
+ export async function listChromeMcpPages(profileName) {
282
+ const result = await callTool(profileName, "list_pages");
283
+ return extractStructuredPages(result);
284
+ }
285
+ export async function listChromeMcpTabs(profileName) {
286
+ return toBrowserTabs(await listChromeMcpPages(profileName));
287
+ }
288
+ export async function openChromeMcpTab(profileName, url) {
289
+ const result = await callTool(profileName, "new_page", { url });
290
+ const pages = extractStructuredPages(result);
291
+ const chosen = pages.find((page) => page.selected) ?? pages.at(-1);
292
+ if (!chosen) {
293
+ throw new Error("Chrome MCP did not return the created page.");
294
+ }
295
+ return {
296
+ targetId: String(chosen.id),
297
+ title: "",
298
+ url: chosen.url ?? url,
299
+ type: "page",
300
+ };
301
+ }
302
+ export async function focusChromeMcpTab(profileName, targetId) {
303
+ await callTool(profileName, "select_page", {
304
+ pageId: parsePageId(targetId),
305
+ bringToFront: true,
306
+ });
307
+ }
308
+ export async function closeChromeMcpTab(profileName, targetId) {
309
+ await callTool(profileName, "close_page", { pageId: parsePageId(targetId) });
310
+ }
311
+ export async function navigateChromeMcpPage(params) {
312
+ await callTool(params.profileName, "navigate_page", {
313
+ pageId: parsePageId(params.targetId),
314
+ type: "url",
315
+ url: params.url,
316
+ ...(typeof params.timeoutMs === "number" ? { timeout: params.timeoutMs } : {}),
317
+ });
318
+ const page = await findPageById(params.profileName, parsePageId(params.targetId));
319
+ return { url: page.url ?? params.url };
320
+ }
321
+ export async function takeChromeMcpSnapshot(params) {
322
+ const result = await callTool(params.profileName, "take_snapshot", {
323
+ pageId: parsePageId(params.targetId),
324
+ });
325
+ return extractSnapshot(result);
326
+ }
327
+ export async function takeChromeMcpScreenshot(params) {
328
+ return await withTempFile(async (filePath) => {
329
+ await callTool(params.profileName, "take_screenshot", {
330
+ pageId: parsePageId(params.targetId),
331
+ filePath,
332
+ format: params.format ?? "png",
333
+ ...(params.uid ? { uid: params.uid } : {}),
334
+ ...(params.fullPage ? { fullPage: true } : {}),
335
+ });
336
+ return await fs.readFile(filePath);
337
+ });
338
+ }
339
+ export async function clickChromeMcpElement(params) {
340
+ await callTool(params.profileName, "click", {
341
+ pageId: parsePageId(params.targetId),
342
+ uid: params.uid,
343
+ ...(params.doubleClick ? { dblClick: true } : {}),
344
+ });
345
+ }
346
+ export async function fillChromeMcpElement(params) {
347
+ await callTool(params.profileName, "fill", {
348
+ pageId: parsePageId(params.targetId),
349
+ uid: params.uid,
350
+ value: params.value,
351
+ });
352
+ }
353
+ export async function fillChromeMcpForm(params) {
354
+ await callTool(params.profileName, "fill_form", {
355
+ pageId: parsePageId(params.targetId),
356
+ elements: params.elements,
357
+ });
358
+ }
359
+ export async function hoverChromeMcpElement(params) {
360
+ await callTool(params.profileName, "hover", {
361
+ pageId: parsePageId(params.targetId),
362
+ uid: params.uid,
363
+ });
364
+ }
365
+ export async function dragChromeMcpElement(params) {
366
+ await callTool(params.profileName, "drag", {
367
+ pageId: parsePageId(params.targetId),
368
+ from_uid: params.fromUid,
369
+ to_uid: params.toUid,
370
+ });
371
+ }
372
+ export async function uploadChromeMcpFile(params) {
373
+ await callTool(params.profileName, "upload_file", {
374
+ pageId: parsePageId(params.targetId),
375
+ uid: params.uid,
376
+ filePath: params.filePath,
377
+ });
378
+ }
379
+ export async function pressChromeMcpKey(params) {
380
+ await callTool(params.profileName, "press_key", {
381
+ pageId: parsePageId(params.targetId),
382
+ key: params.key,
383
+ });
384
+ }
385
+ export async function resizeChromeMcpPage(params) {
386
+ await callTool(params.profileName, "resize_page", {
387
+ pageId: parsePageId(params.targetId),
388
+ width: params.width,
389
+ height: params.height,
390
+ });
391
+ }
392
+ export async function handleChromeMcpDialog(params) {
393
+ await callTool(params.profileName, "handle_dialog", {
394
+ pageId: parsePageId(params.targetId),
395
+ action: params.action,
396
+ ...(params.promptText ? { promptText: params.promptText } : {}),
397
+ });
398
+ }
399
+ export async function evaluateChromeMcpScript(params) {
400
+ const result = await callTool(params.profileName, "evaluate_script", {
401
+ pageId: parsePageId(params.targetId),
402
+ function: params.fn,
403
+ ...(params.args?.length ? { args: params.args } : {}),
404
+ });
405
+ return extractJsonMessage(result);
406
+ }
407
+ export async function waitForChromeMcpText(params) {
408
+ await callTool(params.profileName, "wait_for", {
409
+ pageId: parsePageId(params.targetId),
410
+ text: params.text,
411
+ ...(typeof params.timeoutMs === "number" ? { timeout: params.timeoutMs } : {}),
412
+ });
413
+ }
414
+ export function setChromeMcpSessionFactoryForTest(factory) {
415
+ sessionFactory = factory;
416
+ }
417
+ export async function resetChromeMcpSessionsForTest() {
418
+ sessionFactory = null;
419
+ pendingSessions.clear();
420
+ await stopAllChromeMcpSessions();
421
+ }
@@ -0,0 +1,133 @@
1
+ import { getRoleSnapshotStats, } from "./pw-role-snapshot.js";
2
+ import { CONTENT_ROLES, INTERACTIVE_ROLES, STRUCTURAL_ROLES } from "./snapshot-roles.js";
3
+ function normalizeRole(node) {
4
+ const role = typeof node.role === "string" ? node.role.trim().toLowerCase() : "";
5
+ return role || "generic";
6
+ }
7
+ function normalizeString(value) {
8
+ if (typeof value === "string") {
9
+ const trimmed = value.trim();
10
+ return trimmed || undefined;
11
+ }
12
+ if (typeof value === "number" || typeof value === "boolean") {
13
+ return String(value);
14
+ }
15
+ return undefined;
16
+ }
17
+ function escapeQuoted(value) {
18
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
19
+ }
20
+ function shouldIncludeNode(params) {
21
+ if (params.options?.interactive && !INTERACTIVE_ROLES.has(params.role)) {
22
+ return false;
23
+ }
24
+ if (params.options?.compact && STRUCTURAL_ROLES.has(params.role) && !params.name) {
25
+ return false;
26
+ }
27
+ return true;
28
+ }
29
+ function shouldCreateRef(role, name) {
30
+ return INTERACTIVE_ROLES.has(role) || (CONTENT_ROLES.has(role) && Boolean(name));
31
+ }
32
+ function createDuplicateTracker() {
33
+ return {
34
+ counts: new Map(),
35
+ keysByRef: new Map(),
36
+ duplicates: new Set(),
37
+ };
38
+ }
39
+ function registerRef(tracker, ref, role, name) {
40
+ const key = `${role}:${name ?? ""}`;
41
+ const count = tracker.counts.get(key) ?? 0;
42
+ tracker.counts.set(key, count + 1);
43
+ tracker.keysByRef.set(ref, key);
44
+ if (count > 0) {
45
+ tracker.duplicates.add(key);
46
+ return count;
47
+ }
48
+ return undefined;
49
+ }
50
+ export function flattenChromeMcpSnapshotToAriaNodes(root, limit = 500) {
51
+ const boundedLimit = Math.max(1, Math.min(2000, Math.floor(limit)));
52
+ const out = [];
53
+ const visit = (node, depth) => {
54
+ if (out.length >= boundedLimit) {
55
+ return;
56
+ }
57
+ const ref = normalizeString(node.id);
58
+ if (ref) {
59
+ out.push({
60
+ ref,
61
+ role: normalizeRole(node),
62
+ name: normalizeString(node.name) ?? "",
63
+ value: normalizeString(node.value),
64
+ description: normalizeString(node.description),
65
+ depth,
66
+ });
67
+ }
68
+ for (const child of node.children ?? []) {
69
+ visit(child, depth + 1);
70
+ if (out.length >= boundedLimit) {
71
+ return;
72
+ }
73
+ }
74
+ };
75
+ visit(root, 0);
76
+ return out;
77
+ }
78
+ export function buildAiSnapshotFromChromeMcpSnapshot(params) {
79
+ const refs = {};
80
+ const tracker = createDuplicateTracker();
81
+ const lines = [];
82
+ const visit = (node, depth) => {
83
+ const role = normalizeRole(node);
84
+ const name = normalizeString(node.name);
85
+ const value = normalizeString(node.value);
86
+ const description = normalizeString(node.description);
87
+ const maxDepth = params.options?.maxDepth;
88
+ if (maxDepth !== undefined && depth > maxDepth) {
89
+ return;
90
+ }
91
+ const includeNode = shouldIncludeNode({ role, name, options: params.options });
92
+ if (includeNode) {
93
+ let line = `${" ".repeat(depth)}- ${role}`;
94
+ if (name) {
95
+ line += ` "${escapeQuoted(name)}"`;
96
+ }
97
+ const ref = normalizeString(node.id);
98
+ if (ref && shouldCreateRef(role, name)) {
99
+ const nth = registerRef(tracker, ref, role, name);
100
+ refs[ref] = nth === undefined ? { role, name } : { role, name, nth };
101
+ line += ` [ref=${ref}]`;
102
+ }
103
+ if (value) {
104
+ line += ` value="${escapeQuoted(value)}"`;
105
+ }
106
+ if (description) {
107
+ line += ` description="${escapeQuoted(description)}"`;
108
+ }
109
+ lines.push(line);
110
+ }
111
+ for (const child of node.children ?? []) {
112
+ visit(child, depth + 1);
113
+ }
114
+ };
115
+ visit(params.root, 0);
116
+ for (const [ref, data] of Object.entries(refs)) {
117
+ const key = tracker.keysByRef.get(ref);
118
+ if (key && !tracker.duplicates.has(key)) {
119
+ delete data.nth;
120
+ }
121
+ }
122
+ let snapshot = lines.join("\n");
123
+ let truncated = false;
124
+ const maxChars = typeof params.maxChars === "number" && Number.isFinite(params.maxChars) && params.maxChars > 0
125
+ ? Math.floor(params.maxChars)
126
+ : undefined;
127
+ if (maxChars && snapshot.length > maxChars) {
128
+ snapshot = `${snapshot.slice(0, maxChars)}\n\n[...TRUNCATED - page too large]`;
129
+ truncated = true;
130
+ }
131
+ const stats = getRoleSnapshotStats(snapshot, refs);
132
+ return truncated ? { snapshot, truncated, refs, stats } : { snapshot, refs, stats };
133
+ }
@@ -0,0 +1,67 @@
1
+ import { SsrFBlockedError } from "../infra/net/ssrf.js";
2
+ import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js";
3
+ export class BrowserError extends Error {
4
+ status;
5
+ constructor(message, status = 500, options) {
6
+ super(message, options);
7
+ this.name = new.target.name;
8
+ this.status = status;
9
+ }
10
+ }
11
+ export class BrowserValidationError extends BrowserError {
12
+ constructor(message, options) {
13
+ super(message, 400, options);
14
+ }
15
+ }
16
+ export class BrowserConfigurationError extends BrowserError {
17
+ constructor(message, options) {
18
+ super(message, 400, options);
19
+ }
20
+ }
21
+ export class BrowserTargetAmbiguousError extends BrowserError {
22
+ constructor(message = "ambiguous target id prefix", options) {
23
+ super(message, 409, options);
24
+ }
25
+ }
26
+ export class BrowserTabNotFoundError extends BrowserError {
27
+ constructor(message = "tab not found", options) {
28
+ super(message, 404, options);
29
+ }
30
+ }
31
+ export class BrowserProfileNotFoundError extends BrowserError {
32
+ constructor(message, options) {
33
+ super(message, 404, options);
34
+ }
35
+ }
36
+ export class BrowserConflictError extends BrowserError {
37
+ constructor(message, options) {
38
+ super(message, 409, options);
39
+ }
40
+ }
41
+ export class BrowserResetUnsupportedError extends BrowserError {
42
+ constructor(message, options) {
43
+ super(message, 400, options);
44
+ }
45
+ }
46
+ export class BrowserProfileUnavailableError extends BrowserError {
47
+ constructor(message, options) {
48
+ super(message, 409, options);
49
+ }
50
+ }
51
+ export class BrowserResourceExhaustedError extends BrowserError {
52
+ constructor(message, options) {
53
+ super(message, 507, options);
54
+ }
55
+ }
56
+ export function toBrowserErrorResponse(err) {
57
+ if (err instanceof BrowserError) {
58
+ return { status: err.status, message: err.message };
59
+ }
60
+ if (err instanceof SsrFBlockedError) {
61
+ return { status: 400, message: err.message };
62
+ }
63
+ if (err instanceof InvalidBrowserNavigationUrlError) {
64
+ return { status: 400, message: err.message };
65
+ }
66
+ return null;
67
+ }
@@ -0,0 +1,22 @@
1
+ export const DEFAULT_FILL_FIELD_TYPE = "text";
2
+ export function normalizeBrowserFormFieldRef(value) {
3
+ return typeof value === "string" ? value.trim() : "";
4
+ }
5
+ export function normalizeBrowserFormFieldType(value) {
6
+ const type = typeof value === "string" ? value.trim() : "";
7
+ return type || DEFAULT_FILL_FIELD_TYPE;
8
+ }
9
+ export function normalizeBrowserFormFieldValue(value) {
10
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
11
+ ? value
12
+ : undefined;
13
+ }
14
+ export function normalizeBrowserFormField(record) {
15
+ const ref = normalizeBrowserFormFieldRef(record.ref);
16
+ if (!ref) {
17
+ return null;
18
+ }
19
+ const type = normalizeBrowserFormFieldType(record.type);
20
+ const value = normalizeBrowserFormFieldValue(record.value);
21
+ return value === undefined ? { ref, type } : { ref, type, value };
22
+ }
@@ -0,0 +1,44 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
5
+ import { sanitizeUntrustedFileName } from "./safe-filename.js";
6
+ function buildSiblingTempPath(targetPath) {
7
+ const id = crypto.randomUUID();
8
+ const safeTail = sanitizeUntrustedFileName(path.basename(targetPath), "output.bin");
9
+ return path.join(path.dirname(targetPath), `.poolbot-output-${id}-${safeTail}.part`);
10
+ }
11
+ export async function writeViaSiblingTempPath(params) {
12
+ const rootDir = await fs
13
+ .realpath(path.resolve(params.rootDir))
14
+ .catch(() => path.resolve(params.rootDir));
15
+ const requestedTargetPath = path.resolve(params.targetPath);
16
+ const targetPath = await fs
17
+ .realpath(path.dirname(requestedTargetPath))
18
+ .then((realDir) => path.join(realDir, path.basename(requestedTargetPath)))
19
+ .catch(() => requestedTargetPath);
20
+ const relativeTargetPath = path.relative(rootDir, targetPath);
21
+ if (!relativeTargetPath ||
22
+ relativeTargetPath === ".." ||
23
+ relativeTargetPath.startsWith(`..${path.sep}`) ||
24
+ path.isAbsolute(relativeTargetPath)) {
25
+ throw new Error("Target path is outside the allowed root");
26
+ }
27
+ const tempPath = buildSiblingTempPath(targetPath);
28
+ let renameSucceeded = false;
29
+ try {
30
+ await params.writeTemp(tempPath);
31
+ await writeFileFromPathWithinRoot({
32
+ rootDir,
33
+ relativePath: relativeTargetPath,
34
+ sourcePath: tempPath,
35
+ mkdir: false,
36
+ });
37
+ renameSucceeded = true;
38
+ }
39
+ finally {
40
+ if (!renameSucceeded) {
41
+ await fs.rm(tempPath, { force: true }).catch(() => { });
42
+ }
43
+ }
44
+ }