@silbercue/chrome 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/build/cache/a11y-tree.d.ts +252 -0
  4. package/build/cache/a11y-tree.js +1956 -0
  5. package/build/cache/index.d.ts +8 -0
  6. package/build/cache/index.js +4 -0
  7. package/build/cache/selector-cache.d.ts +47 -0
  8. package/build/cache/selector-cache.js +119 -0
  9. package/build/cache/session-defaults.d.ts +27 -0
  10. package/build/cache/session-defaults.js +130 -0
  11. package/build/cache/tab-state-cache.d.ts +39 -0
  12. package/build/cache/tab-state-cache.js +171 -0
  13. package/build/cdp/cdp-client.d.ts +25 -0
  14. package/build/cdp/cdp-client.js +146 -0
  15. package/build/cdp/chrome-launcher.d.ts +85 -0
  16. package/build/cdp/chrome-launcher.js +502 -0
  17. package/build/cdp/console-collector.d.ts +53 -0
  18. package/build/cdp/console-collector.js +147 -0
  19. package/build/cdp/debug.d.ts +1 -0
  20. package/build/cdp/debug.js +6 -0
  21. package/build/cdp/dialog-handler.d.ts +54 -0
  22. package/build/cdp/dialog-handler.js +129 -0
  23. package/build/cdp/dom-watcher.d.ts +45 -0
  24. package/build/cdp/dom-watcher.js +195 -0
  25. package/build/cdp/emulation.d.ts +12 -0
  26. package/build/cdp/emulation.js +17 -0
  27. package/build/cdp/index.d.ts +11 -0
  28. package/build/cdp/index.js +6 -0
  29. package/build/cdp/network-collector.d.ts +77 -0
  30. package/build/cdp/network-collector.js +257 -0
  31. package/build/cdp/protocol.d.ts +20 -0
  32. package/build/cdp/protocol.js +1 -0
  33. package/build/cdp/session-manager.d.ts +62 -0
  34. package/build/cdp/session-manager.js +205 -0
  35. package/build/cdp/settle.d.ts +16 -0
  36. package/build/cdp/settle.js +71 -0
  37. package/build/cli/license-commands.d.ts +19 -0
  38. package/build/cli/license-commands.js +199 -0
  39. package/build/cli/top-level-commands.d.ts +49 -0
  40. package/build/cli/top-level-commands.js +222 -0
  41. package/build/hooks/index.d.ts +2 -0
  42. package/build/hooks/index.js +1 -0
  43. package/build/hooks/pro-hooks.d.ts +126 -0
  44. package/build/hooks/pro-hooks.js +17 -0
  45. package/build/index.d.ts +4 -0
  46. package/build/index.js +86 -0
  47. package/build/license/free-tier-config.d.ts +14 -0
  48. package/build/license/free-tier-config.js +18 -0
  49. package/build/license/index.d.ts +4 -0
  50. package/build/license/index.js +2 -0
  51. package/build/license/license-status.d.ts +15 -0
  52. package/build/license/license-status.js +9 -0
  53. package/build/overlay/session-overlay.d.ts +22 -0
  54. package/build/overlay/session-overlay.js +372 -0
  55. package/build/plan/index.d.ts +7 -0
  56. package/build/plan/index.js +4 -0
  57. package/build/plan/plan-conditions.d.ts +12 -0
  58. package/build/plan/plan-conditions.js +242 -0
  59. package/build/plan/plan-executor.d.ts +49 -0
  60. package/build/plan/plan-executor.js +259 -0
  61. package/build/plan/plan-state-store.d.ts +24 -0
  62. package/build/plan/plan-state-store.js +43 -0
  63. package/build/plan/plan-variables.d.ts +16 -0
  64. package/build/plan/plan-variables.js +71 -0
  65. package/build/registry.d.ts +124 -0
  66. package/build/registry.js +884 -0
  67. package/build/server.d.ts +1 -0
  68. package/build/server.js +245 -0
  69. package/build/tools/click.d.ts +34 -0
  70. package/build/tools/click.js +293 -0
  71. package/build/tools/configure-session.d.ts +15 -0
  72. package/build/tools/configure-session.js +45 -0
  73. package/build/tools/console-logs.d.ts +18 -0
  74. package/build/tools/console-logs.js +44 -0
  75. package/build/tools/dom-snapshot.d.ts +13 -0
  76. package/build/tools/dom-snapshot.js +259 -0
  77. package/build/tools/element-utils.d.ts +23 -0
  78. package/build/tools/element-utils.js +133 -0
  79. package/build/tools/error-utils.d.ts +8 -0
  80. package/build/tools/error-utils.js +27 -0
  81. package/build/tools/evaluate.d.ts +34 -0
  82. package/build/tools/evaluate.js +217 -0
  83. package/build/tools/file-upload.d.ts +20 -0
  84. package/build/tools/file-upload.js +174 -0
  85. package/build/tools/fill-form.d.ts +39 -0
  86. package/build/tools/fill-form.js +256 -0
  87. package/build/tools/handle-dialog.d.ts +15 -0
  88. package/build/tools/handle-dialog.js +48 -0
  89. package/build/tools/index.d.ts +35 -0
  90. package/build/tools/index.js +18 -0
  91. package/build/tools/navigate.d.ts +18 -0
  92. package/build/tools/navigate.js +111 -0
  93. package/build/tools/network-monitor.d.ts +18 -0
  94. package/build/tools/network-monitor.js +66 -0
  95. package/build/tools/observe.d.ts +44 -0
  96. package/build/tools/observe.js +339 -0
  97. package/build/tools/press-key.d.ts +33 -0
  98. package/build/tools/press-key.js +155 -0
  99. package/build/tools/read-page.d.ts +22 -0
  100. package/build/tools/read-page.js +100 -0
  101. package/build/tools/run-plan.d.ts +205 -0
  102. package/build/tools/run-plan.js +215 -0
  103. package/build/tools/screenshot.d.ts +16 -0
  104. package/build/tools/screenshot.js +283 -0
  105. package/build/tools/scroll.d.ts +28 -0
  106. package/build/tools/scroll.js +143 -0
  107. package/build/tools/switch-tab.d.ts +26 -0
  108. package/build/tools/switch-tab.js +355 -0
  109. package/build/tools/tab-status.d.ts +7 -0
  110. package/build/tools/tab-status.js +50 -0
  111. package/build/tools/type.d.ts +31 -0
  112. package/build/tools/type.js +247 -0
  113. package/build/tools/virtual-desk.d.ts +7 -0
  114. package/build/tools/virtual-desk.js +108 -0
  115. package/build/tools/visual-constants.d.ts +3 -0
  116. package/build/tools/visual-constants.js +10 -0
  117. package/build/tools/wait-for.d.ts +26 -0
  118. package/build/tools/wait-for.js +323 -0
  119. package/build/transport/index.d.ts +3 -0
  120. package/build/transport/index.js +2 -0
  121. package/build/transport/pipe-transport.d.ts +18 -0
  122. package/build/transport/pipe-transport.js +63 -0
  123. package/build/transport/transport.d.ts +8 -0
  124. package/build/transport/transport.js +1 -0
  125. package/build/transport/websocket-transport.d.ts +22 -0
  126. package/build/transport/websocket-transport.js +200 -0
  127. package/build/types.d.ts +21 -0
  128. package/build/types.js +1 -0
  129. package/package.json +62 -0
@@ -0,0 +1,884 @@
1
+ import { evaluateSchema, evaluateHandler } from "./tools/evaluate.js";
2
+ import { navigateSchema, navigateHandler } from "./tools/navigate.js";
3
+ import { readPageSchema, readPageHandler } from "./tools/read-page.js";
4
+ import { screenshotSchema, screenshotHandler } from "./tools/screenshot.js";
5
+ import { waitForSchema, waitForHandler } from "./tools/wait-for.js";
6
+ import { clickSchema, clickHandler } from "./tools/click.js";
7
+ import { typeSchema, typeHandler } from "./tools/type.js";
8
+ import { tabStatusHandler } from "./tools/tab-status.js";
9
+ import { switchTabSchema, switchTabHandler } from "./tools/switch-tab.js";
10
+ import { virtualDeskHandler } from "./tools/virtual-desk.js";
11
+ import { runPlanSchema, runPlanHandler } from "./tools/run-plan.js";
12
+ import { domSnapshotSchema, domSnapshotHandler } from "./tools/dom-snapshot.js";
13
+ import { handleDialogSchema, handleDialogHandler } from "./tools/handle-dialog.js";
14
+ import { fileUploadSchema, fileUploadHandler } from "./tools/file-upload.js";
15
+ import { fillFormSchema, fillFormHandler } from "./tools/fill-form.js";
16
+ import { consoleLogsSchema, consoleLogsHandler } from "./tools/console-logs.js";
17
+ import { networkMonitorSchema, networkMonitorHandler } from "./tools/network-monitor.js";
18
+ import { configureSessionSchema, configureSessionHandler } from "./tools/configure-session.js";
19
+ import { pressKeySchema, pressKeyHandler } from "./tools/press-key.js";
20
+ import { scrollSchema, scrollHandler } from "./tools/scroll.js";
21
+ import { observeSchema, observeHandler } from "./tools/observe.js";
22
+ import { updateOverlayStatus, getToolLabel, setLastElapsed, showClickIndicator } from "./overlay/session-overlay.js";
23
+ import { PlanStateStore } from "./plan/plan-state-store.js";
24
+ import { FreeTierLicenseStatus } from "./license/license-status.js";
25
+ import { loadFreeTierConfig } from "./license/free-tier-config.js";
26
+ import { z } from "zod";
27
+ import { getProHooks, registerProHooks, proFeatureError } from "./hooks/pro-hooks.js";
28
+ import { a11yTree, A11yTreeProcessor } from "./cache/a11y-tree.js";
29
+ /**
30
+ * Story 16.4: Konvertiert ein JSON-Schema-Literal in eine Zod Raw Shape,
31
+ * damit der MCP-SDK `server.tool()` Aufruf den Schema-Check besteht.
32
+ *
33
+ * Unterstuetzte Typen:
34
+ * - `string` (inkl. `enum` → `z.enum`)
35
+ * - `boolean`
36
+ * - `number`
37
+ * - `integer` → `z.number().int()`
38
+ * - `array` (mit `items.type` string|number|boolean|object — verschachtelt)
39
+ * - `object` (rekursiv ueber `properties`/`required`)
40
+ * - Type-Arrays wie `["string", "null"]` → `z.union([...])`
41
+ * - Unbekannte Typen → `z.unknown()` als Fallback
42
+ *
43
+ * Default-Handling: Wenn ein Feld einen `default` hat, wird ausschliesslich
44
+ * `.default(value)` angewendet. Zod behandelt Felder mit Default in einem
45
+ * `z.object()` automatisch als optional (wenn der Input `undefined` ist,
46
+ * wird der Default eingesetzt). Erst wenn KEIN Default existiert UND das
47
+ * Feld nicht in `required` steht, wird `.optional()` angehaengt.
48
+ */
49
+ export function jsonSchemaToZodShape(schema) {
50
+ const properties = (schema.properties ?? {});
51
+ const required = new Set((schema.required ?? []));
52
+ const shape = {};
53
+ for (const [key, prop] of Object.entries(properties)) {
54
+ let zodType = jsonSchemaPropToZod(prop);
55
+ if (prop.description) {
56
+ zodType = zodType.describe(prop.description);
57
+ }
58
+ if (prop.default !== undefined) {
59
+ // Default impliziert in Zod automatisch optional — kein zusaetzliches
60
+ // .optional() noetig (waere semantisch falsch: .default().optional()
61
+ // veraendert den Output-Typ zu T | undefined).
62
+ zodType = zodType.default(prop.default);
63
+ }
64
+ else if (!required.has(key)) {
65
+ zodType = zodType.optional();
66
+ }
67
+ shape[key] = zodType;
68
+ }
69
+ return shape;
70
+ }
71
+ /**
72
+ * Story 16.4: Konvertiert ein einzelnes JSON-Schema-Property in einen Zod-Typ.
73
+ * Wird sowohl von `jsonSchemaToZodShape` als auch rekursiv von sich selbst
74
+ * (fuer `array.items` und `object.properties`) aufgerufen.
75
+ */
76
+ function jsonSchemaPropToZod(prop) {
77
+ const propType = prop.type;
78
+ // Type-Array (z.B. `["string", "null"]`) → Union
79
+ if (Array.isArray(propType)) {
80
+ const variants = propType.map((t) => jsonSchemaPropToZod({ ...prop, type: t }));
81
+ if (variants.length === 0)
82
+ return z.unknown();
83
+ if (variants.length === 1)
84
+ return variants[0];
85
+ return z.union(variants);
86
+ }
87
+ if (propType === "null") {
88
+ return z.null();
89
+ }
90
+ if (propType === "string") {
91
+ const enumValues = prop.enum;
92
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
93
+ return z.enum(enumValues);
94
+ }
95
+ return z.string();
96
+ }
97
+ if (propType === "boolean") {
98
+ return z.boolean();
99
+ }
100
+ if (propType === "number") {
101
+ return z.number();
102
+ }
103
+ if (propType === "integer") {
104
+ return z.number().int();
105
+ }
106
+ if (propType === "array") {
107
+ const items = prop.items;
108
+ if (items && typeof items === "object") {
109
+ return z.array(jsonSchemaPropToZod(items));
110
+ }
111
+ return z.array(z.unknown());
112
+ }
113
+ if (propType === "object") {
114
+ // Rekursiver Aufruf fuer verschachtelte Objekte
115
+ const nestedShape = jsonSchemaToZodShape(prop);
116
+ return z.object(nestedShape);
117
+ }
118
+ return z.unknown();
119
+ }
120
+ export class ToolRegistry {
121
+ server;
122
+ cdpClient;
123
+ _tabStateCache;
124
+ _sessionId;
125
+ _handlers = new Map();
126
+ planStateStore = new PlanStateStore();
127
+ /**
128
+ * Story 15.2: Delegate for `registerTool()` — set during `registerAll()`
129
+ * so it has access to the wrap() closure (dialog injection, response_bytes,
130
+ * session defaults). Pro-Repo calls `registerTool()` from within
131
+ * `registerProTools`, which runs inside `registerAll()` after this delegate
132
+ * is installed.
133
+ */
134
+ _registerProToolDelegate = null;
135
+ /**
136
+ * Story 15.2: Public method exposed via `ToolRegistryPublic`. The Pro-Repo
137
+ * uses this from within the `registerProTools` hook to register extra
138
+ * MCP tools (e.g. inspect_element).
139
+ *
140
+ * Lifecycle: The delegate is installed at the start of `registerAll()`
141
+ * (before `registerProTools` is invoked) and cleared at the end of
142
+ * `registerAll()`. Calling `registerTool()` outside this window throws.
143
+ */
144
+ registerTool(name, description, schema, handler) {
145
+ if (!this._registerProToolDelegate) {
146
+ throw new Error("ToolRegistry.registerTool() can only be called during registerAll() / registerProTools");
147
+ }
148
+ this._registerProToolDelegate(name, description, schema, handler);
149
+ }
150
+ _getConnectionStatus = null;
151
+ _sessionManager;
152
+ _dialogHandler;
153
+ _licenseStatus;
154
+ _freeTierConfig;
155
+ _consoleCollector;
156
+ _networkCollector;
157
+ _sessionDefaults;
158
+ // Story 13a.2: Callback to wait for Accessibility.nodesUpdated event
159
+ _waitForAXChange = null;
160
+ // FR-H: Track whether the LLM has checked browser context before acting
161
+ _contextChecked = false;
162
+ constructor(server, cdpClient, sessionId, _tabStateCache, getConnectionStatus, sessionManager, dialogHandler, licenseStatus, freeTierConfig, consoleCollector, networkCollector, sessionDefaults, waitForAXChange) {
163
+ this.server = server;
164
+ this.cdpClient = cdpClient;
165
+ this._tabStateCache = _tabStateCache;
166
+ this._sessionId = sessionId;
167
+ this._getConnectionStatus = getConnectionStatus ?? null;
168
+ this._sessionManager = sessionManager;
169
+ this._dialogHandler = dialogHandler;
170
+ this._licenseStatus = licenseStatus ?? new FreeTierLicenseStatus();
171
+ this._freeTierConfig = freeTierConfig ?? loadFreeTierConfig();
172
+ this._consoleCollector = consoleCollector;
173
+ this._networkCollector = networkCollector;
174
+ this._sessionDefaults = sessionDefaults;
175
+ this._waitForAXChange = waitForAXChange ?? null;
176
+ }
177
+ get sessionId() {
178
+ return this._sessionId;
179
+ }
180
+ /** Story 16.4: Public getter fuer OOPIF SessionManager (ToolRegistryPublic). */
181
+ get sessionManager() {
182
+ return this._sessionManager;
183
+ }
184
+ updateSession(sessionId) {
185
+ this._sessionId = sessionId;
186
+ }
187
+ /** Swap the CDP client and session after a successful reconnect */
188
+ updateClient(cdpClient, sessionId) {
189
+ this.cdpClient = cdpClient;
190
+ this._sessionId = sessionId;
191
+ }
192
+ /** Get current connection status (connected, reconnecting, disconnected) */
193
+ get connectionStatus() {
194
+ return this._getConnectionStatus?.() ?? "connected";
195
+ }
196
+ async executeTool(name, params, sessionIdOverride) {
197
+ const handler = this._handlers.get(name);
198
+ if (!handler) {
199
+ const content = [{ type: "text", text: `Unknown tool: ${name}` }];
200
+ return {
201
+ content,
202
+ isError: true,
203
+ _meta: { elapsedMs: 0, method: name, response_bytes: Buffer.byteLength(JSON.stringify(content), 'utf8') },
204
+ };
205
+ }
206
+ // Story 7.3: Track call and resolve session defaults for run_plan path
207
+ // Skip trackCall/resolveParams for meta-tools (H2 fix)
208
+ let resolvedParams = params;
209
+ let suggestionText;
210
+ if (this._sessionDefaults && name !== "configure_session") {
211
+ this._sessionDefaults.trackCall(name, params);
212
+ // H1 fix: Read suggestions immediately after trackCall (atomic with tracking)
213
+ const suggestions = this._sessionDefaults.getSuggestions();
214
+ if (suggestions.length > 0) {
215
+ const s = suggestions[0];
216
+ suggestionText = `${s.param} '${s.value}' wurde ${s.count}x verwendet — setze als Default mit configure_session`;
217
+ }
218
+ resolvedParams = this._sessionDefaults.resolveParams(name, params);
219
+ }
220
+ // Story 16.5: enhanceTool hook (Operator/Human Touch) — Pro-Repo injiziert
221
+ // Callback-Funktionen (z.B. humanMouseMove) in params. Sync-Hook, kein await.
222
+ {
223
+ const hooks = getProHooks();
224
+ const enhanced = hooks.enhanceTool?.(name, resolvedParams);
225
+ if (enhanced) {
226
+ resolvedParams = enhanced;
227
+ }
228
+ }
229
+ const result = await handler(resolvedParams, sessionIdOverride);
230
+ this._injectDialogNotifications(result);
231
+ // Story 15.3: Ambient Page Context — delegated to Pro-Repo via onToolResult hook
232
+ await this._runOnToolResultHook(result, name);
233
+ // Story 7.3: Inject auto-promote suggestion into _meta
234
+ if (suggestionText && result._meta) {
235
+ result._meta.suggestion = suggestionText;
236
+ }
237
+ // Story 12.1: Inject response_bytes into _meta
238
+ if (result._meta) {
239
+ const responseBytes = Buffer.byteLength(JSON.stringify(result.content ?? []), 'utf8');
240
+ result._meta.response_bytes = responseBytes;
241
+ // Story 12.2: Inject estimated_tokens for text-heavy tools
242
+ const method = result._meta.method;
243
+ if (method === "read_page" || method === "dom_snapshot") {
244
+ result._meta.estimated_tokens = Math.ceil(responseBytes / 4);
245
+ }
246
+ }
247
+ return result;
248
+ }
249
+ /**
250
+ * Story 15.3: Invokes the `onToolResult` Pro-Hook (if registered) to enrich
251
+ * the tool response with ambient context (DOM diffs, compact snapshots).
252
+ *
253
+ * The Free-Repo itself no longer contains any ambient-context orchestration —
254
+ * the 3-stage click analysis (classifyRef → waitForAXChange → diffSnapshots →
255
+ * formatDomDiff) now lives in the Pro-Repo hook implementation.
256
+ *
257
+ * Responsibilities that stay in the Free-Repo:
258
+ * - FR-007: `a11yTree.reset()` on navigate — called BEFORE the hook so
259
+ * stale refs never leak to the next tool call, even if the hook is
260
+ * not registered.
261
+ * - Error-guard: the hook is NOT invoked when `result.isError` is true,
262
+ * preserving the pre-15.3 semantics.
263
+ *
264
+ * After the hook returns, the enhanced response is merged via
265
+ * `Object.assign(result, enhanced)` so the original `_meta` reference
266
+ * remains stable — later code-paths (response_bytes, estimated_tokens,
267
+ * suggestion injection) mutate `result._meta` in place. If the hook
268
+ * returns a new `_meta` object, the original reference is restored to
269
+ * prevent downstream mutations from writing to a detached object.
270
+ */
271
+ async _runOnToolResultHook(result, name) {
272
+ // FR-007: Navigate invalidates all refs — reset immediately so next
273
+ // tool gets clear stale-error even if no hook is registered.
274
+ if (name === "navigate") {
275
+ a11yTree.reset();
276
+ }
277
+ if (result.isError)
278
+ return;
279
+ const hooks = getProHooks();
280
+ if (!hooks.onToolResult)
281
+ return;
282
+ // Story 15.3 (AC #5): Build a unified `a11yTree` facade that exposes both
283
+ // the instance methods (classifyRef, getSnapshotMap, refreshPrecomputed, …)
284
+ // AND the static diff/format methods (diffSnapshots, formatDomDiff), so
285
+ // the Pro-Repo can drive the full 3-stage analysis through a single
286
+ // `context.a11yTree` object. The legacy `a11yTreeDiffs` field is kept
287
+ // for backward compatibility.
288
+ const a11yTreeFacade = {
289
+ classifyRef: (ref) => a11yTree.classifyRef(ref),
290
+ getSnapshotMap: () => a11yTree.getSnapshotMap(),
291
+ getCompactSnapshot: (maxTokens) => a11yTree.getCompactSnapshot(maxTokens),
292
+ refreshPrecomputed: (client, sessionId, manager) => a11yTree.refreshPrecomputed(client, sessionId, manager),
293
+ reset: () => a11yTree.reset(),
294
+ get currentUrl() {
295
+ return a11yTree.currentUrl;
296
+ },
297
+ diffSnapshots: A11yTreeProcessor.diffSnapshots,
298
+ formatDomDiff: A11yTreeProcessor.formatDomDiff,
299
+ };
300
+ // M1 fix: Save the original `_meta` reference BEFORE invoking the hook.
301
+ // If the hook returns a new `_meta` object, downstream code-paths would
302
+ // otherwise mutate a detached object (response_bytes, estimated_tokens,
303
+ // suggestion injection all assume `result._meta` is the original reference).
304
+ const originalMeta = result._meta;
305
+ const enhanced = await hooks.onToolResult(name, result, {
306
+ a11yTree: a11yTreeFacade,
307
+ a11yTreeDiffs: A11yTreeProcessor,
308
+ waitForAXChange: this._waitForAXChange ?? undefined,
309
+ cdpClient: this.cdpClient,
310
+ sessionId: this._sessionId,
311
+ sessionManager: this._sessionManager,
312
+ });
313
+ // Merge enhanced fields into the original result object so the `_meta`
314
+ // reference stays stable for downstream mutations. If the hook returned
315
+ // a new object (enhanced !== result), we still Object.assign to copy the
316
+ // content, then restore the original _meta reference.
317
+ if (enhanced && enhanced !== result) {
318
+ Object.assign(result, enhanced);
319
+ result._meta = originalMeta;
320
+ }
321
+ }
322
+ /**
323
+ * Story 6.1: Inject pending dialog notifications into any tool response.
324
+ * Called from both executeTool() (run_plan path) and server.tool() callbacks (direct MCP path).
325
+ */
326
+ _injectDialogNotifications(result) {
327
+ const dialogs = this._dialogHandler?.consumeNotifications();
328
+ if (dialogs && dialogs.length > 0) {
329
+ const dialogText = dialogs
330
+ .map((d) => `[dialog] ${d.type}: "${d.message}"`)
331
+ .join("\n");
332
+ result.content.push({ type: "text", text: dialogText });
333
+ }
334
+ }
335
+ /**
336
+ * Wrap a tool handler so dialog notifications are injected into its response.
337
+ * This ensures notifications reach the LLM regardless of whether the tool
338
+ * is called via the direct MCP path (server.tool) or via executeTool (run_plan).
339
+ */
340
+ _wrapWithDialogInjection(handler) {
341
+ return async (params) => {
342
+ const result = await handler(params);
343
+ this._injectDialogNotifications(result);
344
+ return result;
345
+ };
346
+ }
347
+ /**
348
+ * Story 9.5: Wrap a tool handler with a Pro feature gate check.
349
+ * If a featureGate hook is registered and returns { allowed: false },
350
+ * the tool is blocked with an isError response.
351
+ * When no hook is registered, the tool executes normally.
352
+ */
353
+ wrapWithGate(toolName, fn, hooks) {
354
+ return async (params) => {
355
+ const gate = hooks.featureGate?.(toolName);
356
+ if (gate && !gate.allowed) {
357
+ if (gate.message) {
358
+ return {
359
+ content: [{ type: "text", text: gate.message }],
360
+ isError: true,
361
+ _meta: { elapsedMs: 0, method: toolName },
362
+ };
363
+ }
364
+ return proFeatureError(toolName);
365
+ }
366
+ return fn(params);
367
+ };
368
+ }
369
+ registerAll() {
370
+ // Story 9.5: Read Pro hooks once at startup
371
+ const hooks = getProHooks();
372
+ // Story 9.6: Register default feature gate for Pro-only tools
373
+ // Pro-Repo can override by calling registerProHooks() before startServer()
374
+ if (!hooks.featureGate) {
375
+ const licenseStatus = this._licenseStatus;
376
+ registerProHooks({
377
+ ...hooks,
378
+ featureGate: (toolName) => {
379
+ const gatedTools = ["dom_snapshot", "switch_tab", "virtual_desk"];
380
+ if (gatedTools.includes(toolName) && !licenseStatus.isPro()) {
381
+ return { allowed: false };
382
+ }
383
+ return { allowed: true };
384
+ },
385
+ });
386
+ }
387
+ // Re-read hooks after potential registration
388
+ const finalHooks = getProHooks();
389
+ // Story 6.1 (C1): All server.tool() callbacks are wrapped with dialog notification
390
+ // injection so that pending dialogs reach the LLM regardless of call path
391
+ // (direct MCP call vs executeTool/run_plan).
392
+ // Story 7.3: Extended wrap to include session defaults tracking, resolution, and suggestion injection.
393
+ const sessionDefaults = this._sessionDefaults;
394
+ // Story 12.1: Helper to inject response_bytes into _meta
395
+ // Story 12.2: Also injects estimated_tokens for read_page and dom_snapshot
396
+ const injectResponseBytes = (result) => {
397
+ if (result._meta) {
398
+ const responseBytes = Buffer.byteLength(JSON.stringify(result.content ?? []), 'utf8');
399
+ result._meta.response_bytes = responseBytes;
400
+ const method = result._meta.method;
401
+ if (method === "read_page" || method === "dom_snapshot") {
402
+ result._meta.estimated_tokens = Math.ceil(responseBytes / 4);
403
+ }
404
+ }
405
+ };
406
+ // Session overlay: show status before tool, clear after (with elapsed time)
407
+ const overlayBefore = async (name) => {
408
+ await updateOverlayStatus(this.cdpClient, this._sessionId, getToolLabel(name));
409
+ };
410
+ const overlayAfter = async (elapsedMs, meta) => {
411
+ if (elapsedMs !== undefined && elapsedMs > 0)
412
+ setLastElapsed(elapsedMs);
413
+ // Show click indicator at click position
414
+ if (meta?.clickX !== undefined && meta?.clickY !== undefined) {
415
+ showClickIndicator(this.cdpClient, this._sessionId, meta.clickX, meta.clickY);
416
+ }
417
+ await updateOverlayStatus(this.cdpClient, this._sessionId, "");
418
+ };
419
+ const wrap = (fn, toolName) => {
420
+ const dialogWrapped = this._wrapWithDialogInjection(fn);
421
+ if (!sessionDefaults) {
422
+ // Story 12.1: Inject response_bytes even without sessionDefaults
423
+ return async (params) => {
424
+ const name = toolName ?? "unknown";
425
+ await overlayBefore(name);
426
+ let elapsed;
427
+ let meta;
428
+ try {
429
+ // Story 16.5: enhanceTool hook (Operator/Human Touch) — Pro-Repo
430
+ // injiziert Callback-Funktionen in params. Sync-Hook.
431
+ let effectiveParams = params;
432
+ const enhanceHooks = getProHooks();
433
+ const enhanced = enhanceHooks.enhanceTool?.(name, params);
434
+ if (enhanced) {
435
+ effectiveParams = enhanced;
436
+ }
437
+ const result = await dialogWrapped(effectiveParams);
438
+ elapsed = result._meta?.elapsedMs;
439
+ meta = result._meta;
440
+ // Story 15.3: Ambient Page Context — delegated to Pro-Repo via onToolResult hook
441
+ await this._runOnToolResultHook(result, name);
442
+ injectResponseBytes(result);
443
+ return result;
444
+ }
445
+ finally {
446
+ await overlayAfter(elapsed, meta);
447
+ }
448
+ };
449
+ }
450
+ return async (params) => {
451
+ const name = toolName ?? "unknown";
452
+ await overlayBefore(name);
453
+ let elapsed;
454
+ let meta;
455
+ try {
456
+ // H2 fix: Skip trackCall/resolveParams for meta-tools
457
+ if (name === "configure_session") {
458
+ const result = await dialogWrapped(params);
459
+ injectResponseBytes(result);
460
+ return result;
461
+ }
462
+ // Track call for auto-promote analysis
463
+ sessionDefaults.trackCall(name, params);
464
+ // H1 fix: Read suggestions immediately after trackCall (atomic with tracking)
465
+ let suggestionText;
466
+ const suggestions = sessionDefaults.getSuggestions();
467
+ if (suggestions.length > 0) {
468
+ const s = suggestions[0];
469
+ suggestionText = `${s.param} '${s.value}' wurde ${s.count}x verwendet — setze als Default mit configure_session`;
470
+ }
471
+ // Resolve defaults into params
472
+ let resolvedParams = sessionDefaults.resolveParams(name, params);
473
+ // Story 16.5: enhanceTool hook (Operator/Human Touch) — Pro-Repo
474
+ // injiziert Callback-Funktionen in params. Sync-Hook, kein await.
475
+ const enhanceHooks = getProHooks();
476
+ const enhanced = enhanceHooks.enhanceTool?.(name, resolvedParams);
477
+ if (enhanced) {
478
+ resolvedParams = enhanced;
479
+ }
480
+ const result = await dialogWrapped(resolvedParams);
481
+ // Story 15.3: Ambient Page Context — delegated to Pro-Repo via onToolResult hook
482
+ await this._runOnToolResultHook(result, name);
483
+ // Inject auto-promote suggestion into _meta
484
+ if (suggestionText && result._meta) {
485
+ result._meta.suggestion = suggestionText;
486
+ }
487
+ // Story 12.1: Inject response_bytes into _meta
488
+ // Story 12.2: Inject estimated_tokens for text-heavy tools
489
+ if (result._meta) {
490
+ const responseBytes = Buffer.byteLength(JSON.stringify(result.content ?? []), 'utf8');
491
+ result._meta.response_bytes = responseBytes;
492
+ const method = result._meta.method;
493
+ if (method === "read_page" || method === "dom_snapshot") {
494
+ result._meta.estimated_tokens = Math.ceil(responseBytes / 4);
495
+ }
496
+ }
497
+ elapsed = result._meta?.elapsedMs;
498
+ meta = result._meta;
499
+ return result;
500
+ }
501
+ finally {
502
+ await overlayAfter(elapsed, meta);
503
+ }
504
+ };
505
+ };
506
+ // Story 15.2: Install the registerTool delegate so the Pro-Repo can
507
+ // register extra MCP tools from within its `registerProTools` hook.
508
+ // Must be set BEFORE `finalHooks.registerProTools?.(this)` is called.
509
+ this._registerProToolDelegate = (name, description, schema, handler) => {
510
+ // Register in the internal handlers map (for executeTool / run_plan)
511
+ this._handlers.set(name, handler);
512
+ // Story 16.4: Pro-Repo liefert JSON-Schema-Literale (kein zod).
513
+ // MCP SDK erwartet Zod — konvertieren wir hier in der Free-Repo-Schicht.
514
+ const zodShape = jsonSchemaToZodShape(schema);
515
+ // Register with the MCP server (for tools/list). Reuse the same
516
+ // `wrap()` closure as Free-Tools so Pro-Tools inherit the same
517
+ // cross-cutting concerns (dialog injection, response_bytes,
518
+ // session-defaults, overlay status).
519
+ this.server.tool(name, description, zodShape, wrap(async (params) => handler(params), name));
520
+ };
521
+ // Story 15.2: Let the Pro-Repo register its tools BEFORE the Free-Tools
522
+ // so that `tools/list` is deterministic.
523
+ //
524
+ // AC #8: When the Pro-Repo does NOT call `registerProTools`, the Pro-only
525
+ // tools (e.g. `inspect_element`) are simply NOT registered — they do not
526
+ // appear in `tools/list` at all. If an LLM still attempts to invoke them,
527
+ // the MCP server returns a standard "Unknown tool" error. This is cleaner
528
+ // than maintaining a fake stub that clutters the tool list in the free tier.
529
+ finalHooks.registerProTools?.(this);
530
+ // Tool order matters for LLM selection (Positional Bias — BiasBusters
531
+ // arXiv:2510.00307). High-priority workflow tools come first; evaluate
532
+ // is deliberately last so it is NOT the default for text/element tasks.
533
+ //
534
+ // Order: orientation → reading → interaction → tab-mgmt → timing →
535
+ // visual → special → debug → meta → evaluate (last resort).
536
+ // --- 1. Orientation ---
537
+ this.server.tool("virtual_desk", "PRIMARY orientation tool — call first in every new session, after reconnect, or when unsure. Lists all tabs with IDs, URLs, state. Use returned IDs with switch_tab(tab: '<id>') instead of opening duplicates via navigate. Cheap, call liberally.", {}, wrap(this.wrapWithGate("virtual_desk", async (params) => {
538
+ this._contextChecked = true;
539
+ return virtualDeskHandler(params, this.cdpClient, this.sessionId, this._tabStateCache, this.connectionStatus);
540
+ }, finalHooks), "virtual_desk"));
541
+ // --- 2. Reading ---
542
+ this.server.tool("read_page", "PRIMARY tool for page understanding — call after navigate/switch_tab before any interaction. Returns accessibility tree with stable refs (e.g. 'e5') that you pass to click/type/fill_form. Use this to read visible text too — not evaluate/querySelector. Default filter:'interactive' hides static text; for cells/paragraphs/labels call read_page(ref: 'eN', filter: 'all'). ~10-30x cheaper than screenshot.", {
543
+ depth: readPageSchema.shape.depth,
544
+ ref: readPageSchema.shape.ref,
545
+ filter: readPageSchema.shape.filter,
546
+ max_tokens: readPageSchema.shape.max_tokens,
547
+ }, wrap(async (params) => {
548
+ return readPageHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
549
+ }, "read_page"));
550
+ // --- 3. Interaction (click/type/fill_form/press_key/scroll) ---
551
+ this.server.tool("click", "Click an element by ref, CSS selector, or viewport coordinates. Dispatches real CDP mouse events (mouseMoved/mousePressed/mouseReleased). For canvas or pixel-precise targets, use x+y coordinates instead of ref. If the click opens a new tab, the response reports it automatically. The response already includes the DOM diff (NEW/REMOVED/CHANGED lines) — inspect those changes for success/failure signals instead of following up with evaluate to re-check state.", {
552
+ ref: clickSchema.shape.ref,
553
+ selector: clickSchema.shape.selector,
554
+ text: clickSchema.shape.text,
555
+ x: clickSchema.shape.x,
556
+ y: clickSchema.shape.y,
557
+ }, wrap(async (params) => {
558
+ return clickHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
559
+ }, "click"));
560
+ this.server.tool("type", "Type text into an input field identified by ref or CSS selector. For multiple fields in the same form, prefer fill_form — it handles text inputs, <select>, checkbox, and radio in one round-trip and is more reliable than N separate type calls. For special keys (Enter, Escape, Tab, arrows) or shortcuts (Ctrl+K), use press_key instead.", {
561
+ ref: typeSchema.shape.ref,
562
+ selector: typeSchema.shape.selector,
563
+ text: typeSchema.shape.text,
564
+ clear: typeSchema.shape.clear,
565
+ }, wrap(async (params) => {
566
+ return typeHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
567
+ }, "type"));
568
+ // Story 6.3: fill_form — fill complete forms with one call
569
+ this.server.tool("fill_form", "Fill a complete form with one call — the preferred way to submit any form with 2+ fields. Each field needs ref or CSS selector plus value. Supports text inputs, <select> (by value or visible label), checkboxes (boolean), and radio buttons. Use this INSTEAD of multiple type calls or evaluate-setting select.value: one round-trip, partial errors do not abort, each field reports its own status.", {
570
+ fields: fillFormSchema.shape.fields,
571
+ }, wrap(async (params) => {
572
+ return fillFormHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
573
+ }, "fill_form"));
574
+ // FR-C: press_key — real CDP keyboard events (not JS dispatchEvent)
575
+ this.server.tool("press_key", "Press a keyboard key or shortcut. Optionally focus an element first via ref/selector. Use for Enter, Escape, Tab, arrows, shortcuts (Ctrl+K).", {
576
+ key: pressKeySchema.shape.key,
577
+ ref: pressKeySchema.shape.ref,
578
+ selector: pressKeySchema.shape.selector,
579
+ modifiers: pressKeySchema.shape.modifiers,
580
+ }, wrap(async (params) => {
581
+ return pressKeyHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
582
+ }, "press_key"));
583
+ // FR-F: scroll — scroll page or element into view
584
+ this.server.tool("scroll", "Scroll the page, a container, or an element into view. Use ref/selector to scroll an element into the viewport. Use container_ref/container_selector + direction to scroll inside a specific container (e.g. sidebar, modal body).", {
585
+ ref: scrollSchema.shape.ref,
586
+ selector: scrollSchema.shape.selector,
587
+ container_ref: scrollSchema.shape.container_ref,
588
+ container_selector: scrollSchema.shape.container_selector,
589
+ direction: scrollSchema.shape.direction,
590
+ amount: scrollSchema.shape.amount,
591
+ }, wrap(async (params) => {
592
+ return scrollHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
593
+ }, "scroll"));
594
+ // --- 4. Tab management (navigate/switch_tab/tab_status) ---
595
+ this.server.tool("navigate", "Navigate the ACTIVE tab to a URL (or action:'back'). Waits for settle. WARNING: overwrites the user's active tab — always call virtual_desk FIRST to check what's open; if the right tab exists, use switch_tab instead. First call per session is auto-redirected to virtual_desk.", {
596
+ url: navigateSchema.shape.url,
597
+ action: navigateSchema.shape.action,
598
+ settle_ms: navigateSchema.shape.settle_ms,
599
+ }, wrap(async (params) => {
600
+ // FR-H: If virtual_desk hasn't been called yet, run it instead of navigating.
601
+ // This prevents overwriting the user's active tab blindly.
602
+ if (!this._contextChecked) {
603
+ this._contextChecked = true;
604
+ const vdResult = await virtualDeskHandler({}, this.cdpClient, this.sessionId, this._tabStateCache, this.connectionStatus);
605
+ const tabList = vdResult.content?.[0]?.type === "text" ? vdResult.content[0].text : "";
606
+ return {
607
+ content: [{ type: "text", text: `Navigation blocked — virtual_desk was not called yet. Here are your open tabs:\n\n${tabList}\n\nUse switch_tab(tab: "<id>") to go to an existing tab, or call navigate again to open a new page.` }],
608
+ _meta: { elapsedMs: vdResult._meta?.elapsedMs ?? 0, method: "navigate", intercepted: true },
609
+ };
610
+ }
611
+ return navigateHandler(params, this.cdpClient, this.sessionId);
612
+ }, "navigate"));
613
+ this.server.tool("switch_tab", "Open a new tab, switch to an existing tab by ID (from virtual_desk), or close a tab. Prefer 'open' over navigate when you don't want to touch the user's active tab.", {
614
+ action: switchTabSchema.shape.action,
615
+ url: switchTabSchema.shape.url,
616
+ tab: switchTabSchema.shape.tab,
617
+ }, wrap(this.wrapWithGate("switch_tab", async (params) => {
618
+ return switchTabHandler(params, this.cdpClient, this.sessionId, this._tabStateCache, (newSessionId) => {
619
+ this.updateSession(newSessionId);
620
+ }, this._sessionManager);
621
+ }, finalHooks), "switch_tab"));
622
+ this.server.tool("tab_status", "Active tab's cached URL/title/ready/errors for quick sanity checks mid-workflow ('did my click navigate?'). For tab discovery: use virtual_desk. For page content: use read_page.", {}, wrap(async (params) => {
623
+ this._contextChecked = true;
624
+ return tabStatusHandler(params, this.cdpClient, this.sessionId, this._tabStateCache, this.connectionStatus);
625
+ }, "tab_status"));
626
+ // --- 5. Timing (wait_for/observe) ---
627
+ this.server.tool("wait_for", "Wait for a condition: element visible, network idle, or JS expression true", {
628
+ condition: waitForSchema.shape.condition,
629
+ selector: waitForSchema.shape.selector,
630
+ expression: waitForSchema.shape.expression,
631
+ timeout: waitForSchema.shape.timeout,
632
+ }, wrap(async (params) => {
633
+ return waitForHandler(params, this.cdpClient, this.sessionId);
634
+ }, "wait_for"));
635
+ // FR-009: observe — passively watch DOM changes
636
+ this.server.tool("observe", "Watch an element for changes over time — use this INSTEAD of writing MutationObserver/setInterval/setTimeout code in evaluate. Two modes: (1) collect — watch for 'duration' ms, return all text/attribute changes (e.g. collect 3 values that appear one after another). (2) until — wait for a condition, then optionally click immediately (e.g. click Capture when counter hits 8). Use click_first to trigger the action that causes changes (observer is set up BEFORE the click, so nothing is missed).", {
637
+ selector: observeSchema.shape.selector,
638
+ duration: observeSchema.shape.duration,
639
+ until: observeSchema.shape.until,
640
+ then_click: observeSchema.shape.then_click,
641
+ click_first: observeSchema.shape.click_first,
642
+ collect: observeSchema.shape.collect,
643
+ interval: observeSchema.shape.interval,
644
+ timeout: observeSchema.shape.timeout,
645
+ }, wrap(async (params) => {
646
+ return observeHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
647
+ }, "observe"));
648
+ // --- 6. Visual (screenshot/dom_snapshot — last resort for visual tasks) ---
649
+ this.server.tool("screenshot", "Capture a WebP image of the page (max 800px, <100KB). You CANNOT use screenshots as input for click/type — use read_page for element refs. Only use for visual verification, canvas pages, or explicit user requests. ~10-30x more tokens than read_page.", {
650
+ full_page: screenshotSchema.shape.full_page,
651
+ som: screenshotSchema.shape.som,
652
+ }, wrap(async (params) => {
653
+ // Check for minimized window before taking screenshot
654
+ const activeTarget = this._tabStateCache.activeTargetId;
655
+ if (activeTarget) {
656
+ try {
657
+ const { bounds } = await this.cdpClient.send("Browser.getWindowForTarget", { targetId: activeTarget });
658
+ if (bounds.windowState === "minimized") {
659
+ return {
660
+ content: [{ type: "text", text: "Warning: Window is minimized — screenshot may be empty or stale. Use switch_tab to bring the window to foreground first, or call Browser.setWindowBounds to restore it." }],
661
+ isError: true,
662
+ _meta: { elapsedMs: 0, method: "screenshot", windowMinimized: true },
663
+ };
664
+ }
665
+ }
666
+ catch {
667
+ /* best-effort — proceed with screenshot */
668
+ }
669
+ }
670
+ const result = await screenshotHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
671
+ // Preventive hint: screenshots cannot drive click/type — steer back to read_page
672
+ if (!result.isError && result.content?.length > 0) {
673
+ const somHint = params.som
674
+ ? " SoM labels match read_page refs — pass them to click/type directly."
675
+ : " Add som: true to overlay numbered ref labels matching read_page.";
676
+ result.content.push({ type: "text", text: `Reminder: screenshots cannot be used as input for click/type — you need refs from read_page for any interaction.${somHint}` });
677
+ }
678
+ return result;
679
+ }, "screenshot"));
680
+ this.server.tool("dom_snapshot", "Structured layout data: bounding boxes, computed styles, paint order, colors. Refs match read_page. Use ONLY for spatial questions read_page cannot answer (is A above B? what color?). For element discovery or text: use read_page. For pure visual verification: use screenshot.", {
681
+ ref: domSnapshotSchema.shape.ref,
682
+ }, wrap(this.wrapWithGate("dom_snapshot", async (params) => {
683
+ return domSnapshotHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
684
+ }, finalHooks), "dom_snapshot"));
685
+ // --- 7. Special interactions (handle_dialog/file_upload) ---
686
+ // Story 6.1: handle_dialog — configure dialog handling before triggering actions
687
+ // H3 fix: Route through wrap for default-resolution and suggestion-injection
688
+ if (this._dialogHandler) {
689
+ this.server.tool("handle_dialog", "Configure how the browser handles JavaScript dialogs (alerts, confirms, prompts). Pre-configure before triggering actions, or check dialog status.", {
690
+ action: handleDialogSchema.shape.action,
691
+ text: handleDialogSchema.shape.text,
692
+ }, wrap(async (params) => {
693
+ return handleDialogHandler(params, this._dialogHandler);
694
+ }, "handle_dialog"));
695
+ }
696
+ // Story 6.2: file_upload — upload files to file input elements
697
+ this.server.tool("file_upload", "Upload file(s) to a file input element. Provide ref or CSS selector to identify the <input type='file'>, and absolute path(s) to the file(s).", {
698
+ ref: fileUploadSchema.shape.ref,
699
+ selector: fileUploadSchema.shape.selector,
700
+ path: fileUploadSchema.shape.path,
701
+ }, wrap(async (params) => {
702
+ return fileUploadHandler(params, this.cdpClient, this.sessionId, this._sessionManager);
703
+ }, "file_upload"));
704
+ // --- 8. Debugging (console_logs/network_monitor) ---
705
+ // Story 7.1: console_logs — retrieve and filter console output
706
+ if (this._consoleCollector) {
707
+ this.server.tool("console_logs", "Retrieve collected browser console logs. Filter by level (info/warning/error/debug) and/or regex pattern. Optionally clear the buffer after reading.", {
708
+ level: consoleLogsSchema.shape.level,
709
+ pattern: consoleLogsSchema.shape.pattern,
710
+ clear: consoleLogsSchema.shape.clear,
711
+ }, wrap(async (params) => {
712
+ return consoleLogsHandler(params, this._consoleCollector);
713
+ }, "console_logs"));
714
+ }
715
+ // Story 7.2: network_monitor — start/stop/get network request monitoring
716
+ if (this._networkCollector) {
717
+ this.server.tool("network_monitor", "Monitor network requests: start recording, retrieve recorded requests (with optional filter/pattern), or stop and return all collected data.", {
718
+ action: networkMonitorSchema.shape.action,
719
+ filter: networkMonitorSchema.shape.filter,
720
+ pattern: networkMonitorSchema.shape.pattern,
721
+ }, wrap(async (params) => {
722
+ return networkMonitorHandler(params, this._networkCollector);
723
+ }, "network_monitor"));
724
+ }
725
+ // --- 9. Meta (configure_session/run_plan) ---
726
+ // Story 7.3: configure_session — set session defaults and auto-promote
727
+ if (this._sessionDefaults) {
728
+ this.server.tool("configure_session", "View/set session defaults for recurring parameters (tab, timeout, etc.). Without params: show current defaults and auto-promote suggestions. With autoPromote: true: apply all suggestions.", {
729
+ defaults: configureSessionSchema.shape.defaults,
730
+ autoPromote: configureSessionSchema.shape.autoPromote,
731
+ }, wrap(async (params) => {
732
+ return configureSessionHandler(params, this._sessionDefaults);
733
+ }, "configure_session"));
734
+ }
735
+ this.server.tool("run_plan", "Execute a sequential plan of tool steps server-side. Supports variables ($varName), conditions (if), saveAs, error strategies (abort/continue/screenshot), suspend/resume. Parallel tab execution via parallel: [{ tab, steps }] is a Pro-Feature - requires Pro license.", {
736
+ steps: runPlanSchema.shape.steps,
737
+ parallel: runPlanSchema.shape.parallel,
738
+ use_operator: runPlanSchema.shape.use_operator,
739
+ resume: runPlanSchema.shape.resume,
740
+ }, wrap(async (params) => {
741
+ const result = await runPlanHandler(params, this, {
742
+ cdpClient: this.cdpClient,
743
+ sessionId: this._sessionId,
744
+ sessionManager: this._sessionManager,
745
+ }, this.planStateStore, this._licenseStatus, this._freeTierConfig);
746
+ // Convert SuspendedPlanResponse to ToolResponse for MCP transport
747
+ if ("status" in result && result.status === "suspended") {
748
+ const suspended = result;
749
+ const content = [
750
+ { type: "text", text: JSON.stringify({
751
+ status: suspended.status,
752
+ planId: suspended.planId,
753
+ question: suspended.question,
754
+ completedSteps: suspended.completedSteps,
755
+ }) },
756
+ ];
757
+ if (suspended.screenshot) {
758
+ content.push({
759
+ type: "image",
760
+ data: suspended.screenshot,
761
+ mimeType: "image/webp",
762
+ });
763
+ }
764
+ return { content, _meta: suspended._meta };
765
+ }
766
+ return result;
767
+ }, "run_plan"));
768
+ // --- 10. Last resort: evaluate (intentionally registered last so LLMs
769
+ // don't default to it for text/element tasks — Positional Bias fix) ---
770
+ this.server.tool("evaluate", "Execute JavaScript in the browser page context and return the result. Use this to COMPUTE values or trigger side effects no other tool covers — NOT to discover elements. If you're using querySelector/getElementById/innerText to find interactive elements or read visible text, prefer read_page (stable refs survive DOM changes, selectors don't) or fill_form. Common anti-patterns that evaluate will detect and hint you about: DOM-queried buttons/inputs, .innerText/.textContent reads, .click()/.scrollIntoView(), Tests.*.toString() introspection. Scope is shared between calls — top-level const/let/class are auto-wrapped in IIFE. If/else blocks may return undefined — use ternary (a ? b : c) or explicit return.", {
771
+ expression: evaluateSchema.shape.expression,
772
+ await_promise: evaluateSchema.shape.await_promise,
773
+ }, wrap(async (params) => {
774
+ return evaluateHandler(params, this.cdpClient, this.sessionId);
775
+ }, "evaluate"));
776
+ // Register tool handlers for executeTool dispatch
777
+ // IMPORTANT: run_plan is NOT registered here to prevent recursive invocation
778
+ // Story 7.6: All session-aware handlers accept sessionIdOverride for parallel tab execution.
779
+ // When sessionIdOverride is provided, it is used INSTEAD of this.sessionId.
780
+ // This avoids Race-Conditions when multiple tab groups run in parallel.
781
+ this._handlers.set("evaluate", async (params, sessionIdOverride) => {
782
+ return evaluateHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId);
783
+ });
784
+ this._handlers.set("navigate", async (params, sessionIdOverride) => {
785
+ return navigateHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId);
786
+ });
787
+ this._handlers.set("read_page", async (params, sessionIdOverride) => {
788
+ return readPageHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._sessionManager);
789
+ });
790
+ this._handlers.set("screenshot", async (params, sessionIdOverride) => {
791
+ return screenshotHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._sessionManager);
792
+ });
793
+ this._handlers.set("wait_for", async (params, sessionIdOverride) => {
794
+ return waitForHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId);
795
+ });
796
+ this._handlers.set("observe", async (params, sessionIdOverride) => {
797
+ return observeHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._sessionManager);
798
+ });
799
+ this._handlers.set("click", async (params, sessionIdOverride) => {
800
+ return clickHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._sessionManager);
801
+ });
802
+ this._handlers.set("type", async (params, sessionIdOverride) => {
803
+ return typeHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._sessionManager);
804
+ });
805
+ this._handlers.set("tab_status", async (params, sessionIdOverride) => {
806
+ return tabStatusHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._tabStateCache, this.connectionStatus);
807
+ });
808
+ this._handlers.set("switch_tab", async (params, sessionIdOverride) => {
809
+ // Story 9.9: Pro-Feature-Gate must fire BEFORE the parallel check
810
+ const switchGate = finalHooks.featureGate?.("switch_tab");
811
+ if (switchGate && !switchGate.allowed) {
812
+ if (switchGate.message) {
813
+ return { content: [{ type: "text", text: switchGate.message }], isError: true, _meta: { elapsedMs: 0, method: "switch_tab" } };
814
+ }
815
+ return proFeatureError("switch_tab");
816
+ }
817
+ // H3 fix: switch_tab in parallel context would mutate the global session — block it
818
+ if (sessionIdOverride) {
819
+ return {
820
+ content: [{ type: "text", text: "switch_tab ist in parallelen Plan-Gruppen nicht erlaubt — jede Gruppe operiert auf ihrem eigenen Tab" }],
821
+ isError: true,
822
+ _meta: { elapsedMs: 0, method: "switch_tab" },
823
+ };
824
+ }
825
+ return switchTabHandler(params, this.cdpClient, this.sessionId, this._tabStateCache, (newSessionId) => {
826
+ this.updateSession(newSessionId);
827
+ }, this._sessionManager);
828
+ });
829
+ this._handlers.set("virtual_desk", async (params, sessionIdOverride) => {
830
+ // Story 9.9: Pro-Feature-Gate for virtual_desk
831
+ const vdGate = finalHooks.featureGate?.("virtual_desk");
832
+ if (vdGate && !vdGate.allowed) {
833
+ if (vdGate.message) {
834
+ return { content: [{ type: "text", text: vdGate.message }], isError: true, _meta: { elapsedMs: 0, method: "virtual_desk" } };
835
+ }
836
+ return proFeatureError("virtual_desk");
837
+ }
838
+ return virtualDeskHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._tabStateCache, this.connectionStatus);
839
+ });
840
+ this._handlers.set("dom_snapshot", async (params, sessionIdOverride) => {
841
+ // C1 fix: dom_snapshot must use sessionIdOverride for parallel tab execution
842
+ const gate = finalHooks.featureGate?.("dom_snapshot");
843
+ if (gate && !gate.allowed) {
844
+ if (gate.message) {
845
+ return { content: [{ type: "text", text: gate.message }], isError: true, _meta: { elapsedMs: 0, method: "dom_snapshot" } };
846
+ }
847
+ return proFeatureError("dom_snapshot");
848
+ }
849
+ return domSnapshotHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._sessionManager);
850
+ });
851
+ if (this._dialogHandler) {
852
+ this._handlers.set("handle_dialog", async (params, _sessionIdOverride) => {
853
+ // C2 fix: accept sessionIdOverride for parallel-context compatibility
854
+ return handleDialogHandler(params, this._dialogHandler);
855
+ });
856
+ }
857
+ this._handlers.set("file_upload", async (params, sessionIdOverride) => {
858
+ return fileUploadHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._sessionManager);
859
+ });
860
+ this._handlers.set("fill_form", async (params, sessionIdOverride) => {
861
+ return fillFormHandler(params, this.cdpClient, sessionIdOverride ?? this.sessionId, this._sessionManager);
862
+ });
863
+ if (this._consoleCollector) {
864
+ this._handlers.set("console_logs", async (params) => {
865
+ return consoleLogsHandler(params, this._consoleCollector);
866
+ });
867
+ }
868
+ if (this._networkCollector) {
869
+ this._handlers.set("network_monitor", async (params) => {
870
+ return networkMonitorHandler(params, this._networkCollector);
871
+ });
872
+ }
873
+ if (this._sessionDefaults) {
874
+ this._handlers.set("configure_session", async (params) => {
875
+ return configureSessionHandler(params, this._sessionDefaults);
876
+ });
877
+ }
878
+ // Story 15.2 / H2: Clear the Pro-Tool registration delegate now that
879
+ // `registerAll()` is done. Any subsequent `registerTool()` call (after
880
+ // the setup phase) will throw — preventing non-deterministic late
881
+ // registrations from corrupting `tools/list`.
882
+ this._registerProToolDelegate = null;
883
+ }
884
+ }