@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.
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/build/cache/a11y-tree.d.ts +252 -0
- package/build/cache/a11y-tree.js +1956 -0
- package/build/cache/index.d.ts +8 -0
- package/build/cache/index.js +4 -0
- package/build/cache/selector-cache.d.ts +47 -0
- package/build/cache/selector-cache.js +119 -0
- package/build/cache/session-defaults.d.ts +27 -0
- package/build/cache/session-defaults.js +130 -0
- package/build/cache/tab-state-cache.d.ts +39 -0
- package/build/cache/tab-state-cache.js +171 -0
- package/build/cdp/cdp-client.d.ts +25 -0
- package/build/cdp/cdp-client.js +146 -0
- package/build/cdp/chrome-launcher.d.ts +85 -0
- package/build/cdp/chrome-launcher.js +502 -0
- package/build/cdp/console-collector.d.ts +53 -0
- package/build/cdp/console-collector.js +147 -0
- package/build/cdp/debug.d.ts +1 -0
- package/build/cdp/debug.js +6 -0
- package/build/cdp/dialog-handler.d.ts +54 -0
- package/build/cdp/dialog-handler.js +129 -0
- package/build/cdp/dom-watcher.d.ts +45 -0
- package/build/cdp/dom-watcher.js +195 -0
- package/build/cdp/emulation.d.ts +12 -0
- package/build/cdp/emulation.js +17 -0
- package/build/cdp/index.d.ts +11 -0
- package/build/cdp/index.js +6 -0
- package/build/cdp/network-collector.d.ts +77 -0
- package/build/cdp/network-collector.js +257 -0
- package/build/cdp/protocol.d.ts +20 -0
- package/build/cdp/protocol.js +1 -0
- package/build/cdp/session-manager.d.ts +62 -0
- package/build/cdp/session-manager.js +205 -0
- package/build/cdp/settle.d.ts +16 -0
- package/build/cdp/settle.js +71 -0
- package/build/cli/license-commands.d.ts +19 -0
- package/build/cli/license-commands.js +199 -0
- package/build/cli/top-level-commands.d.ts +49 -0
- package/build/cli/top-level-commands.js +222 -0
- package/build/hooks/index.d.ts +2 -0
- package/build/hooks/index.js +1 -0
- package/build/hooks/pro-hooks.d.ts +126 -0
- package/build/hooks/pro-hooks.js +17 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +86 -0
- package/build/license/free-tier-config.d.ts +14 -0
- package/build/license/free-tier-config.js +18 -0
- package/build/license/index.d.ts +4 -0
- package/build/license/index.js +2 -0
- package/build/license/license-status.d.ts +15 -0
- package/build/license/license-status.js +9 -0
- package/build/overlay/session-overlay.d.ts +22 -0
- package/build/overlay/session-overlay.js +372 -0
- package/build/plan/index.d.ts +7 -0
- package/build/plan/index.js +4 -0
- package/build/plan/plan-conditions.d.ts +12 -0
- package/build/plan/plan-conditions.js +242 -0
- package/build/plan/plan-executor.d.ts +49 -0
- package/build/plan/plan-executor.js +259 -0
- package/build/plan/plan-state-store.d.ts +24 -0
- package/build/plan/plan-state-store.js +43 -0
- package/build/plan/plan-variables.d.ts +16 -0
- package/build/plan/plan-variables.js +71 -0
- package/build/registry.d.ts +124 -0
- package/build/registry.js +884 -0
- package/build/server.d.ts +1 -0
- package/build/server.js +245 -0
- package/build/tools/click.d.ts +34 -0
- package/build/tools/click.js +293 -0
- package/build/tools/configure-session.d.ts +15 -0
- package/build/tools/configure-session.js +45 -0
- package/build/tools/console-logs.d.ts +18 -0
- package/build/tools/console-logs.js +44 -0
- package/build/tools/dom-snapshot.d.ts +13 -0
- package/build/tools/dom-snapshot.js +259 -0
- package/build/tools/element-utils.d.ts +23 -0
- package/build/tools/element-utils.js +133 -0
- package/build/tools/error-utils.d.ts +8 -0
- package/build/tools/error-utils.js +27 -0
- package/build/tools/evaluate.d.ts +34 -0
- package/build/tools/evaluate.js +217 -0
- package/build/tools/file-upload.d.ts +20 -0
- package/build/tools/file-upload.js +174 -0
- package/build/tools/fill-form.d.ts +39 -0
- package/build/tools/fill-form.js +256 -0
- package/build/tools/handle-dialog.d.ts +15 -0
- package/build/tools/handle-dialog.js +48 -0
- package/build/tools/index.d.ts +35 -0
- package/build/tools/index.js +18 -0
- package/build/tools/navigate.d.ts +18 -0
- package/build/tools/navigate.js +111 -0
- package/build/tools/network-monitor.d.ts +18 -0
- package/build/tools/network-monitor.js +66 -0
- package/build/tools/observe.d.ts +44 -0
- package/build/tools/observe.js +339 -0
- package/build/tools/press-key.d.ts +33 -0
- package/build/tools/press-key.js +155 -0
- package/build/tools/read-page.d.ts +22 -0
- package/build/tools/read-page.js +100 -0
- package/build/tools/run-plan.d.ts +205 -0
- package/build/tools/run-plan.js +215 -0
- package/build/tools/screenshot.d.ts +16 -0
- package/build/tools/screenshot.js +283 -0
- package/build/tools/scroll.d.ts +28 -0
- package/build/tools/scroll.js +143 -0
- package/build/tools/switch-tab.d.ts +26 -0
- package/build/tools/switch-tab.js +355 -0
- package/build/tools/tab-status.d.ts +7 -0
- package/build/tools/tab-status.js +50 -0
- package/build/tools/type.d.ts +31 -0
- package/build/tools/type.js +247 -0
- package/build/tools/virtual-desk.d.ts +7 -0
- package/build/tools/virtual-desk.js +108 -0
- package/build/tools/visual-constants.d.ts +3 -0
- package/build/tools/visual-constants.js +10 -0
- package/build/tools/wait-for.d.ts +26 -0
- package/build/tools/wait-for.js +323 -0
- package/build/transport/index.d.ts +3 -0
- package/build/transport/index.js +2 -0
- package/build/transport/pipe-transport.d.ts +18 -0
- package/build/transport/pipe-transport.js +63 -0
- package/build/transport/transport.d.ts +8 -0
- package/build/transport/transport.js +1 -0
- package/build/transport/websocket-transport.d.ts +22 -0
- package/build/transport/websocket-transport.js +200 -0
- package/build/types.d.ts +21 -0
- package/build/types.js +1 -0
- 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,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 @@
|
|
|
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
|
+
}
|
package/build/types.d.ts
ADDED
|
@@ -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 {};
|