@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,323 @@
1
+ import { z } from "zod";
2
+ import { settle } from "../cdp/settle.js";
3
+ import { a11yTree } from "../cache/a11y-tree.js";
4
+ import { wrapCdpError } from "./error-utils.js";
5
+ // --- Schema (Task 1) ---
6
+ export const waitForSchema = z.object({
7
+ condition: z
8
+ .enum(["element", "network_idle", "js"])
9
+ .describe("What to wait for: element visibility, network idle, or JS expression returning true"),
10
+ selector: z
11
+ .string()
12
+ .optional()
13
+ .describe("CSS selector or element ref (e.g. 'e5') — required when condition is 'element'"),
14
+ expression: z
15
+ .string()
16
+ .optional()
17
+ .describe("JavaScript expression that should evaluate to true — required when condition is 'js'"),
18
+ timeout: z
19
+ .number()
20
+ .optional()
21
+ .default(10000)
22
+ .describe("Maximum wait time in milliseconds (default: 10000)"),
23
+ });
24
+ // --- Constants ---
25
+ const POLL_INTERVAL_MS = 200;
26
+ // --- Delay helper ---
27
+ function delay(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+ async function waitForElement(cdpClient, sessionId, selector, timeout) {
31
+ const start = performance.now();
32
+ const deadline = start + timeout;
33
+ const isRef = /^e\d+$/.test(selector);
34
+ while (performance.now() < deadline) {
35
+ try {
36
+ let found = false;
37
+ if (isRef) {
38
+ // Ref path
39
+ const backendNodeId = a11yTree.resolveRef(selector);
40
+ if (backendNodeId !== undefined) {
41
+ // Resolve backendNodeId → objectId via DOM.resolveNode
42
+ const { object } = await cdpClient.send("DOM.resolveNode", { backendNodeId }, sessionId);
43
+ // Check visibility via callFunctionOn
44
+ const { result } = await cdpClient.send("Runtime.callFunctionOn", {
45
+ objectId: object.objectId,
46
+ functionDeclaration: "function() { const r = this.getBoundingClientRect(); return r.width > 0 && r.height > 0; }",
47
+ returnByValue: true,
48
+ }, sessionId);
49
+ found = result.value === true;
50
+ }
51
+ // If resolveRef returns undefined, ref not in cache yet — keep polling
52
+ }
53
+ else {
54
+ // CSS path
55
+ const checkExpression = `(() => {
56
+ const el = document.querySelector(${JSON.stringify(selector)});
57
+ if (!el) return false;
58
+ const rect = el.getBoundingClientRect();
59
+ return rect.width > 0 && rect.height > 0;
60
+ })()`;
61
+ const evalResult = await cdpClient.send("Runtime.evaluate", { expression: checkExpression, returnByValue: true }, sessionId);
62
+ found = evalResult.result.value === true;
63
+ }
64
+ if (found) {
65
+ return { found: true, elapsedMs: Math.round(performance.now() - start) };
66
+ }
67
+ }
68
+ catch {
69
+ // CDP error during polling (e.g. element removed) — swallow and continue
70
+ // Transport errors will propagate from the outer try/catch in the handler
71
+ }
72
+ const remaining = deadline - performance.now();
73
+ if (remaining <= 0)
74
+ break;
75
+ await delay(Math.min(POLL_INTERVAL_MS, remaining));
76
+ }
77
+ return { found: false, elapsedMs: Math.round(performance.now() - start) };
78
+ }
79
+ async function waitForNetworkIdle(cdpClient, sessionId, timeout) {
80
+ // Get main frame ID
81
+ const frameTree = await cdpClient.send("Page.getFrameTree", {}, sessionId);
82
+ const frameId = frameTree.frameTree.frame.id;
83
+ const settleResult = await settle({
84
+ cdpClient,
85
+ sessionId,
86
+ frameId,
87
+ settleMs: 500,
88
+ timeoutMs: timeout,
89
+ });
90
+ return {
91
+ settled: settleResult.settled,
92
+ signal: settleResult.signal,
93
+ elapsedMs: settleResult.elapsedMs,
94
+ };
95
+ }
96
+ async function waitForJs(cdpClient, sessionId, expression, timeout) {
97
+ const start = performance.now();
98
+ const deadline = start + timeout;
99
+ let lastValue = undefined;
100
+ while (performance.now() < deadline) {
101
+ try {
102
+ const result = await cdpClient.send("Runtime.evaluate", {
103
+ expression,
104
+ returnByValue: true,
105
+ awaitPromise: false,
106
+ }, sessionId);
107
+ if (!result.exceptionDetails) {
108
+ lastValue = result.result.value;
109
+ if (result.result.value === true) {
110
+ return { met: true, elapsedMs: Math.round(performance.now() - start), lastValue };
111
+ }
112
+ }
113
+ // Exception in expression — swallow and keep polling
114
+ }
115
+ catch {
116
+ // CDP error — swallow and keep polling
117
+ }
118
+ const remaining = deadline - performance.now();
119
+ if (remaining <= 0)
120
+ break;
121
+ await delay(Math.min(POLL_INTERVAL_MS, remaining));
122
+ }
123
+ return { met: false, elapsedMs: Math.round(performance.now() - start), lastValue };
124
+ }
125
+ // --- Element timeout diagnostics (FR-H7) ---
126
+ /**
127
+ * After an element wait_for timeout, check if the element exists in DOM.
128
+ * Returns a diagnostic string helping the LLM understand why the wait failed.
129
+ */
130
+ async function elementTimeoutDiagnostic(cdpClient, sessionId, selector) {
131
+ const isRef = /^e\d+$/.test(selector);
132
+ if (isRef) {
133
+ const backendNodeId = a11yTree.resolveRef(selector);
134
+ if (backendNodeId === undefined) {
135
+ return "\nDebug: Ref not found in cache — page may have changed. Call read_page to get fresh refs.";
136
+ }
137
+ return "\nDebug: Ref exists in cache but element has zero size (hidden or not rendered).";
138
+ }
139
+ // CSS selector path
140
+ try {
141
+ const result = await cdpClient.send("Runtime.evaluate", {
142
+ expression: `(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return { exists: false, hidden: false, tag: "" }; const r = el.getBoundingClientRect(); return { exists: true, hidden: r.width === 0 || r.height === 0, tag: el.tagName.toLowerCase() }; })()`,
143
+ returnByValue: true,
144
+ }, sessionId);
145
+ const v = result.result.value;
146
+ if (!v.exists) {
147
+ return `\nDebug: querySelector('${selector}') returned null — element not in DOM.`;
148
+ }
149
+ if (v.hidden) {
150
+ return `\nDebug: <${v.tag}> exists but has zero size (display: none or collapsed). A preceding action may be needed to reveal it.`;
151
+ }
152
+ return `\nDebug: <${v.tag}> exists with size > 0 but visibility check failed.`;
153
+ }
154
+ catch {
155
+ return "";
156
+ }
157
+ }
158
+ // --- JS timeout diagnostics (FR-006) ---
159
+ /**
160
+ * Extract the first CSS selector from a querySelector/getElementById call in a JS expression.
161
+ * Returns the CSS selector string, or null if none found.
162
+ */
163
+ export function extractSelector(expression) {
164
+ // Match querySelector('...') or querySelector("...")
165
+ const qsMatch = expression.match(/querySelector\(\s*(['"])(.*?)\1\s*\)/);
166
+ if (qsMatch)
167
+ return qsMatch[2];
168
+ // Match getElementById('...') or getElementById("...")
169
+ const idMatch = expression.match(/getElementById\(\s*(['"])(.*?)\1\s*\)/);
170
+ if (idMatch)
171
+ return `#${idMatch[2]}`;
172
+ return null;
173
+ }
174
+ /**
175
+ * After a JS wait_for timeout, check if the extracted selector's element exists in the DOM.
176
+ * Returns a diagnostic line or empty string if no selector was found.
177
+ */
178
+ async function jsTimeoutDiagnostic(cdpClient, sessionId, expression) {
179
+ const selector = extractSelector(expression);
180
+ if (!selector)
181
+ return "";
182
+ try {
183
+ const result = await cdpClient.send("Runtime.evaluate", {
184
+ expression: `document.querySelector(${JSON.stringify(selector)}) !== null`,
185
+ returnByValue: true,
186
+ }, sessionId);
187
+ if (result.result.value === true) {
188
+ return `\nDebug: Element exists but condition not met (content may still be loading).`;
189
+ }
190
+ return `\nDebug: querySelector('${selector}') returned null — element not found in DOM.`;
191
+ }
192
+ catch {
193
+ // If CDP call fails, skip diagnostics
194
+ return "";
195
+ }
196
+ }
197
+ // --- Main handler (Task 5) ---
198
+ export async function waitForHandler(params, cdpClient, sessionId) {
199
+ const start = performance.now();
200
+ // Validation (Task 1.4)
201
+ if (params.condition === "element" && (!params.selector || params.selector.trim() === "")) {
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: "wait_for condition 'element' requires a 'selector' parameter (CSS selector or ref like 'e5')",
207
+ },
208
+ ],
209
+ isError: true,
210
+ _meta: { elapsedMs: 0, method: "wait_for" },
211
+ };
212
+ }
213
+ if (params.condition === "js" && (!params.expression || params.expression.trim() === "")) {
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: "wait_for condition 'js' requires an 'expression' parameter",
219
+ },
220
+ ],
221
+ isError: true,
222
+ _meta: { elapsedMs: 0, method: "wait_for" },
223
+ };
224
+ }
225
+ try {
226
+ switch (params.condition) {
227
+ case "element": {
228
+ const result = await waitForElement(cdpClient, sessionId, params.selector, params.timeout);
229
+ if (result.found) {
230
+ return {
231
+ content: [
232
+ {
233
+ type: "text",
234
+ text: `Condition 'element' met after ${result.elapsedMs}ms — selector: ${params.selector}`,
235
+ },
236
+ ],
237
+ _meta: { elapsedMs: result.elapsedMs, method: "wait_for", condition: "element" },
238
+ };
239
+ }
240
+ // FR-H7: Append diagnostic info on timeout
241
+ const diagnostic = await elementTimeoutDiagnostic(cdpClient, sessionId, params.selector);
242
+ return {
243
+ content: [
244
+ {
245
+ type: "text",
246
+ text: `Timeout after ${params.timeout}ms waiting for element '${params.selector}' to become visible${diagnostic}`,
247
+ },
248
+ ],
249
+ isError: true,
250
+ _meta: { elapsedMs: result.elapsedMs, method: "wait_for", condition: "element" },
251
+ };
252
+ }
253
+ case "network_idle": {
254
+ const result = await waitForNetworkIdle(cdpClient, sessionId, params.timeout);
255
+ if (result.settled) {
256
+ return {
257
+ content: [
258
+ {
259
+ type: "text",
260
+ text: `Condition 'network_idle' met after ${result.elapsedMs}ms`,
261
+ },
262
+ ],
263
+ _meta: {
264
+ elapsedMs: result.elapsedMs,
265
+ method: "wait_for",
266
+ condition: "network_idle",
267
+ settleSignal: result.signal,
268
+ },
269
+ };
270
+ }
271
+ return {
272
+ content: [
273
+ {
274
+ type: "text",
275
+ text: `Timeout after ${params.timeout}ms waiting for network idle (signal: ${result.signal})`,
276
+ },
277
+ ],
278
+ isError: true,
279
+ _meta: {
280
+ elapsedMs: result.elapsedMs,
281
+ method: "wait_for",
282
+ condition: "network_idle",
283
+ settleSignal: result.signal,
284
+ },
285
+ };
286
+ }
287
+ case "js": {
288
+ const result = await waitForJs(cdpClient, sessionId, params.expression, params.timeout);
289
+ if (result.met) {
290
+ return {
291
+ content: [
292
+ {
293
+ type: "text",
294
+ text: `Condition 'js' met after ${result.elapsedMs}ms`,
295
+ },
296
+ ],
297
+ _meta: { elapsedMs: result.elapsedMs, method: "wait_for", condition: "js" },
298
+ };
299
+ }
300
+ // FR-006: Append diagnostic info when a querySelector/getElementById is detected
301
+ const diagnostic = await jsTimeoutDiagnostic(cdpClient, sessionId, params.expression);
302
+ return {
303
+ content: [
304
+ {
305
+ type: "text",
306
+ text: `Timeout after ${params.timeout}ms waiting for JS expression to return true. Last evaluation returned: ${JSON.stringify(result.lastValue)}${diagnostic}`,
307
+ },
308
+ ],
309
+ isError: true,
310
+ _meta: { elapsedMs: result.elapsedMs, method: "wait_for", condition: "js" },
311
+ };
312
+ }
313
+ }
314
+ }
315
+ catch (err) {
316
+ const elapsedMs = Math.round(performance.now() - start);
317
+ return {
318
+ content: [{ type: "text", text: wrapCdpError(err, "wait_for") }],
319
+ isError: true,
320
+ _meta: { elapsedMs, method: "wait_for" },
321
+ };
322
+ }
323
+ }
@@ -0,0 +1,3 @@
1
+ export type { CdpTransport } from "./transport.js";
2
+ export { PipeTransport } from "./pipe-transport.js";
3
+ export { WebSocketTransport } from "./websocket-transport.js";
@@ -0,0 +1,2 @@
1
+ export { PipeTransport } from "./pipe-transport.js";
2
+ export { WebSocketTransport } from "./websocket-transport.js";
@@ -0,0 +1,18 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import type { CdpTransport } from "./transport.js";
3
+ export declare class PipeTransport implements CdpTransport {
4
+ private readonly readable;
5
+ private readonly writable;
6
+ private _connected;
7
+ private _buffer;
8
+ private _messageCallback;
9
+ private _errorCallback;
10
+ private _closeCallback;
11
+ constructor(readable: Readable, writable: Writable);
12
+ get connected(): boolean;
13
+ send(message: string): boolean;
14
+ onMessage(cb: (message: string) => void): void;
15
+ onError(cb: (error: Error) => void): void;
16
+ onClose(cb: () => void): void;
17
+ close(): Promise<void>;
18
+ }
@@ -0,0 +1,63 @@
1
+ export class PipeTransport {
2
+ readable;
3
+ writable;
4
+ _connected = true;
5
+ _buffer = "";
6
+ _messageCallback = null;
7
+ _errorCallback = null;
8
+ _closeCallback = null;
9
+ constructor(readable, writable) {
10
+ this.readable = readable;
11
+ this.writable = writable;
12
+ this.readable.on("data", (chunk) => {
13
+ this._buffer += chunk.toString();
14
+ const parts = this._buffer.split("\0");
15
+ this._buffer = parts.pop();
16
+ for (const part of parts) {
17
+ if (part.length > 0 && this._messageCallback) {
18
+ this._messageCallback(part);
19
+ }
20
+ }
21
+ });
22
+ this.readable.on("error", (err) => {
23
+ this._errorCallback?.(err);
24
+ });
25
+ this.writable.on("error", (err) => {
26
+ this._errorCallback?.(err);
27
+ });
28
+ this.readable.on("close", () => {
29
+ if (!this._connected)
30
+ return;
31
+ this._connected = false;
32
+ this._closeCallback?.();
33
+ });
34
+ this.writable.on("close", () => {
35
+ if (!this._connected)
36
+ return;
37
+ this._connected = false;
38
+ this._closeCallback?.();
39
+ });
40
+ }
41
+ get connected() {
42
+ return this._connected;
43
+ }
44
+ send(message) {
45
+ if (!this._connected)
46
+ return false;
47
+ return this.writable.write(message + "\0");
48
+ }
49
+ onMessage(cb) {
50
+ this._messageCallback = cb;
51
+ }
52
+ onError(cb) {
53
+ this._errorCallback = cb;
54
+ }
55
+ onClose(cb) {
56
+ this._closeCallback = cb;
57
+ }
58
+ async close() {
59
+ this._connected = false;
60
+ this.readable.destroy();
61
+ this.writable.end();
62
+ }
63
+ }
@@ -0,0 +1,8 @@
1
+ export interface CdpTransport {
2
+ send(message: string): boolean;
3
+ onMessage(cb: (message: string) => void): void;
4
+ onError(cb: (error: Error) => void): void;
5
+ onClose(cb: () => void): void;
6
+ close(): Promise<void>;
7
+ readonly connected: boolean;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { CdpTransport } from "./transport.js";
2
+ export declare class WebSocketTransport implements CdpTransport {
3
+ private readonly socket;
4
+ private _connected;
5
+ private _messageCallback;
6
+ private _errorCallback;
7
+ private _closeCallback;
8
+ private _recvBuffer;
9
+ private constructor();
10
+ static connect(url: string, options?: {
11
+ timeoutMs?: number;
12
+ }): Promise<WebSocketTransport>;
13
+ get connected(): boolean;
14
+ send(message: string): boolean;
15
+ onMessage(cb: (message: string) => void): void;
16
+ onError(cb: (error: Error) => void): void;
17
+ onClose(cb: () => void): void;
18
+ close(): Promise<void>;
19
+ private _onData;
20
+ private _decodeFrame;
21
+ private _encodeFrame;
22
+ }
@@ -0,0 +1,200 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { request as httpRequest } from "node:http";
3
+ const OPCODES = {
4
+ TEXT: 0x1,
5
+ CLOSE: 0x8,
6
+ PING: 0x9,
7
+ PONG: 0xa,
8
+ };
9
+ export class WebSocketTransport {
10
+ socket;
11
+ _connected = true;
12
+ _messageCallback = null;
13
+ _errorCallback = null;
14
+ _closeCallback = null;
15
+ _recvBuffer = Buffer.alloc(0);
16
+ constructor(socket) {
17
+ this.socket = socket;
18
+ this.socket.on("data", (chunk) => this._onData(chunk));
19
+ this.socket.on("error", (err) => this._errorCallback?.(err));
20
+ this.socket.on("close", () => {
21
+ this._connected = false;
22
+ this._closeCallback?.();
23
+ });
24
+ }
25
+ static async connect(url, options) {
26
+ const parsed = new URL(url);
27
+ const key = randomBytes(16).toString("base64");
28
+ const timeoutMs = options?.timeoutMs ?? 30_000;
29
+ return new Promise((resolve, reject) => {
30
+ let settled = false;
31
+ const timer = setTimeout(() => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ req.destroy();
36
+ reject(new Error(`WebSocket connect timed out after ${timeoutMs}ms`));
37
+ }, timeoutMs);
38
+ const req = httpRequest({
39
+ hostname: parsed.hostname,
40
+ port: parsed.port || 80,
41
+ path: parsed.pathname + parsed.search,
42
+ headers: {
43
+ Upgrade: "websocket",
44
+ Connection: "Upgrade",
45
+ "Sec-WebSocket-Key": key,
46
+ "Sec-WebSocket-Version": "13",
47
+ },
48
+ });
49
+ req.on("upgrade", (_res, socket) => {
50
+ if (settled) {
51
+ socket.destroy();
52
+ return;
53
+ }
54
+ clearTimeout(timer);
55
+ settled = true;
56
+ // BUG-003: Accept validation permanently skipped.
57
+ // Node 22 undici 6.21.1 has a confirmed bug where Sec-WebSocket-Accept hashes
58
+ // mismatch between client and server — affects both native WebSocket and custom
59
+ // implementations. Safe to skip: Chrome DevTools is a trusted localhost endpoint.
60
+ resolve(new WebSocketTransport(socket));
61
+ });
62
+ req.on("response", (res) => {
63
+ if (settled)
64
+ return;
65
+ clearTimeout(timer);
66
+ settled = true;
67
+ reject(new Error(`WebSocket handshake failed: server returned HTTP ${res.statusCode}`));
68
+ req.destroy();
69
+ });
70
+ req.on("error", (err) => {
71
+ if (settled)
72
+ return;
73
+ clearTimeout(timer);
74
+ settled = true;
75
+ reject(err);
76
+ });
77
+ req.end();
78
+ });
79
+ }
80
+ get connected() {
81
+ return this._connected;
82
+ }
83
+ send(message) {
84
+ if (!this._connected)
85
+ return false;
86
+ const payload = Buffer.from(message, "utf-8");
87
+ const frame = this._encodeFrame(OPCODES.TEXT, payload);
88
+ return this.socket.write(frame);
89
+ }
90
+ onMessage(cb) {
91
+ this._messageCallback = cb;
92
+ }
93
+ onError(cb) {
94
+ this._errorCallback = cb;
95
+ }
96
+ onClose(cb) {
97
+ this._closeCallback = cb;
98
+ }
99
+ async close() {
100
+ if (!this._connected)
101
+ return;
102
+ this._connected = false;
103
+ const closeFrame = this._encodeFrame(OPCODES.CLOSE, Buffer.alloc(0));
104
+ this.socket.write(closeFrame);
105
+ this.socket.end();
106
+ }
107
+ _onData(chunk) {
108
+ this._recvBuffer = Buffer.concat([this._recvBuffer, chunk]);
109
+ while (this._recvBuffer.length >= 2) {
110
+ const result = this._decodeFrame(this._recvBuffer);
111
+ if (!result)
112
+ break;
113
+ const { opcode, payload, bytesConsumed } = result;
114
+ this._recvBuffer = this._recvBuffer.subarray(bytesConsumed);
115
+ switch (opcode) {
116
+ case OPCODES.TEXT:
117
+ this._messageCallback?.(payload.toString("utf-8"));
118
+ break;
119
+ case OPCODES.CLOSE:
120
+ this._connected = false;
121
+ // Send close frame back
122
+ this.socket.write(this._encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
123
+ this.socket.end();
124
+ break;
125
+ case OPCODES.PING:
126
+ this.socket.write(this._encodeFrame(OPCODES.PONG, payload));
127
+ break;
128
+ case OPCODES.PONG:
129
+ // Ignore unsolicited pongs
130
+ break;
131
+ }
132
+ }
133
+ }
134
+ _decodeFrame(buf) {
135
+ if (buf.length < 2)
136
+ return null;
137
+ const opcode = buf[0] & 0x0f;
138
+ const masked = (buf[1] & 0x80) !== 0;
139
+ let payloadLength = buf[1] & 0x7f;
140
+ let offset = 2;
141
+ if (payloadLength === 126) {
142
+ if (buf.length < 4)
143
+ return null;
144
+ payloadLength = buf.readUInt16BE(2);
145
+ offset = 4;
146
+ }
147
+ else if (payloadLength === 127) {
148
+ if (buf.length < 10)
149
+ return null;
150
+ // Read as BigInt, but CDP messages won't exceed Number.MAX_SAFE_INTEGER
151
+ payloadLength = Number(buf.readBigUInt64BE(2));
152
+ offset = 10;
153
+ }
154
+ if (masked) {
155
+ if (buf.length < offset + 4 + payloadLength)
156
+ return null;
157
+ const maskKey = buf.subarray(offset, offset + 4);
158
+ offset += 4;
159
+ const payload = Buffer.alloc(payloadLength);
160
+ for (let i = 0; i < payloadLength; i++) {
161
+ payload[i] = buf[offset + i] ^ maskKey[i % 4];
162
+ }
163
+ return { opcode, payload, bytesConsumed: offset + payloadLength };
164
+ }
165
+ if (buf.length < offset + payloadLength)
166
+ return null;
167
+ const payload = buf.subarray(offset, offset + payloadLength);
168
+ return { opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLength };
169
+ }
170
+ _encodeFrame(opcode, payload) {
171
+ const mask = randomBytes(4);
172
+ const payloadLength = payload.length;
173
+ let header;
174
+ if (payloadLength < 126) {
175
+ header = Buffer.alloc(6);
176
+ header[0] = 0x80 | opcode; // FIN + opcode
177
+ header[1] = 0x80 | payloadLength; // MASK + length
178
+ mask.copy(header, 2);
179
+ }
180
+ else if (payloadLength < 65536) {
181
+ header = Buffer.alloc(8);
182
+ header[0] = 0x80 | opcode;
183
+ header[1] = 0x80 | 126;
184
+ header.writeUInt16BE(payloadLength, 2);
185
+ mask.copy(header, 4);
186
+ }
187
+ else {
188
+ header = Buffer.alloc(14);
189
+ header[0] = 0x80 | opcode;
190
+ header[1] = 0x80 | 127;
191
+ header.writeBigUInt64BE(BigInt(payloadLength), 2);
192
+ mask.copy(header, 10);
193
+ }
194
+ const maskedPayload = Buffer.alloc(payloadLength);
195
+ for (let i = 0; i < payloadLength; i++) {
196
+ maskedPayload[i] = payload[i] ^ mask[i % 4];
197
+ }
198
+ return Buffer.concat([header, maskedPayload]);
199
+ }
200
+ }
@@ -0,0 +1,21 @@
1
+ export interface ToolMeta {
2
+ [key: string]: unknown;
3
+ elapsedMs: number;
4
+ method: string;
5
+ }
6
+ export type ToolContentBlock = {
7
+ type: "text";
8
+ text: string;
9
+ } | {
10
+ type: "image";
11
+ data: string;
12
+ mimeType: string;
13
+ };
14
+ export interface ToolResponse {
15
+ [key: string]: unknown;
16
+ content: Array<ToolContentBlock>;
17
+ isError?: boolean;
18
+ _meta?: ToolMeta;
19
+ }
20
+ export type ConnectionStatus = "connected" | "reconnecting" | "disconnected";
21
+ export type TransportType = "pipe" | "websocket";
package/build/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};