@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,217 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { wrapCdpError } from "./error-utils.js";
|
|
3
|
+
import { getProHooks } from "../hooks/pro-hooks.js";
|
|
4
|
+
/**
|
|
5
|
+
* Detects top-level const/let/class declarations and wraps the expression in
|
|
6
|
+
* an IIFE to avoid "Identifier has already been declared" errors across
|
|
7
|
+
* repeated Runtime.evaluate calls (which share the global scope).
|
|
8
|
+
*
|
|
9
|
+
* The last ExpressionStatement is automatically returned so callers still
|
|
10
|
+
* get the evaluation result.
|
|
11
|
+
*/
|
|
12
|
+
export function wrapInIIFE(expression) {
|
|
13
|
+
// Quick check: does the code need IIFE wrapping?
|
|
14
|
+
// - const/let/class declarations (would collide across repeated evaluate calls)
|
|
15
|
+
// - top-level return statements (illegal outside function body)
|
|
16
|
+
// - top-level await (FR-H3: illegal outside async function)
|
|
17
|
+
const hasDeclarations = /^[ \t]*(const|let|class)\s/m.test(expression);
|
|
18
|
+
const hasTopLevelReturn = /^[ \t]*return\s/m.test(expression);
|
|
19
|
+
const hasTopLevelAwait = /\bawait\b/.test(expression);
|
|
20
|
+
if (!hasDeclarations && !hasTopLevelReturn && !hasTopLevelAwait)
|
|
21
|
+
return expression;
|
|
22
|
+
// Already wrapped in an IIFE? Don't double-wrap.
|
|
23
|
+
const trimmed = expression.trim();
|
|
24
|
+
if (/^\([\s\S]*\)\s*\(\s*\)\s*;?\s*$/.test(trimmed))
|
|
25
|
+
return expression;
|
|
26
|
+
// FR-H3: Use async IIFE when code contains await
|
|
27
|
+
const asyncPrefix = hasTopLevelAwait ? "async " : "";
|
|
28
|
+
// If code already has explicit return statements, just wrap in IIFE — don't insert return.
|
|
29
|
+
if (hasTopLevelReturn) {
|
|
30
|
+
return `(${asyncPrefix}() => {\n${expression}\n})()`;
|
|
31
|
+
}
|
|
32
|
+
// Insert `return` before the last expression statement so the IIFE
|
|
33
|
+
// returns its value (arrow function block bodies don't auto-return).
|
|
34
|
+
const lines = expression.split("\n");
|
|
35
|
+
// FR-001: Use bracket-depth tracking to find the START of the last multi-line expression.
|
|
36
|
+
// Walk backwards from the last line, tracking depth of () {} [].
|
|
37
|
+
// When depth reaches 0, we've found the start of the expression.
|
|
38
|
+
let depth = 0;
|
|
39
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
40
|
+
const line = lines[i].trim();
|
|
41
|
+
if (!line || line.startsWith("//"))
|
|
42
|
+
continue;
|
|
43
|
+
// Count brackets right-to-left (we're scanning backwards)
|
|
44
|
+
for (let c = line.length - 1; c >= 0; c--) {
|
|
45
|
+
const ch = line[c];
|
|
46
|
+
if (ch === ")" || ch === "}" || ch === "]")
|
|
47
|
+
depth++;
|
|
48
|
+
else if (ch === "(" || ch === "{" || ch === "[")
|
|
49
|
+
depth--;
|
|
50
|
+
}
|
|
51
|
+
// depth > 0 means we're still inside a multi-line expression — keep walking up
|
|
52
|
+
if (depth > 0)
|
|
53
|
+
continue;
|
|
54
|
+
// Found the start of the last complete expression/statement.
|
|
55
|
+
if (/^(const|let|var|if|for|while|switch|try|throw|return|class|function)\b/.test(line)) {
|
|
56
|
+
// It's a statement keyword — check for trailing expression after last ;
|
|
57
|
+
const lastSemi = line.lastIndexOf(";");
|
|
58
|
+
if (lastSemi >= 0 && lastSemi < line.length - 1) {
|
|
59
|
+
const trailing = line.substring(lastSemi + 1).trim();
|
|
60
|
+
if (trailing && !/^(const|let|var|if|for|while|switch|try|throw|return|class|function)\b/.test(trailing)) {
|
|
61
|
+
const indent = lines[i].match(/^(\s*)/)?.[1] ?? "";
|
|
62
|
+
lines[i] = indent + line.substring(0, lastSemi + 1);
|
|
63
|
+
lines.splice(i + 1, 0, indent + "return " + trailing);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
// Pure expression — prepend return
|
|
69
|
+
const indent = lines[i].match(/^(\s*)/)?.[1] ?? "";
|
|
70
|
+
lines[i] = indent + "return " + lines[i].trimStart();
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return `(${asyncPrefix}() => {\n${lines.join("\n")}\n})()`;
|
|
74
|
+
}
|
|
75
|
+
export const evaluateSchema = z.object({
|
|
76
|
+
expression: z.string().describe("JavaScript code to execute in the page context"),
|
|
77
|
+
await_promise: z
|
|
78
|
+
.boolean()
|
|
79
|
+
.optional()
|
|
80
|
+
.default(true)
|
|
81
|
+
.describe("Whether to await Promise results"),
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* FR-024: Detect common evaluate anti-patterns where a dedicated tool would be better.
|
|
85
|
+
* Returns a hint string appended to the evaluate result, or null if no pattern matched.
|
|
86
|
+
* Intent: "what you did isn't wrong, but there's a more reliable tool for this".
|
|
87
|
+
*
|
|
88
|
+
* Design: be specific enough to avoid false positives on legitimate DOM work
|
|
89
|
+
* (e.g. `document.querySelector('.card').style.width = '200px'` is NOT an
|
|
90
|
+
* anti-pattern — it's a style edit that no other tool covers).
|
|
91
|
+
*/
|
|
92
|
+
export function detectEvaluateAntiPattern(expression) {
|
|
93
|
+
const hints = [];
|
|
94
|
+
// Pattern 1A: Bulk element discovery via querySelectorAll / getElementsByXxx.
|
|
95
|
+
// This is read_page's core job — stable refs survive, selectors don't.
|
|
96
|
+
const bulkDiscovery = /\bdocument\.(querySelectorAll|getElementsByTagName|getElementsByClassName|getElementsByName)\b/.test(expression);
|
|
97
|
+
// Pattern 1B: querySelector with a bare interactive tag selector (e.g. 'button', 'input').
|
|
98
|
+
// Someone using tag selectors is almost always discovering interactive elements.
|
|
99
|
+
const querySelectorForTag = /\bdocument\.querySelector\s*\(\s*['"`](button|input|select|textarea|form|label|a)\b/i.test(expression);
|
|
100
|
+
// Pattern 1C: query-then-interact — get element by ID/selector, then use it as an interactive target
|
|
101
|
+
// (click/focus/submit/value/checked/selectedIndex). This is click/type/fill_form territory.
|
|
102
|
+
// Allowed through: .style.*, .classList.*, .dataset.*, .scrollTop — those are handled elsewhere.
|
|
103
|
+
const queryThenInteract = /\bdocument\.(querySelector|getElementById)\s*\([^)]*\)\s*(?:\?\.)?\s*\.(click\s*\(|focus\s*\(|blur\s*\(|submit\s*\(|value\b|checked\b|selectedIndex\b|disabled\b|selected\b)/.test(expression);
|
|
104
|
+
if (bulkDiscovery || querySelectorForTag || queryThenInteract) {
|
|
105
|
+
hints.push("Interactive elements (buttons, links, inputs) are already surfaced as stable refs by read_page. Try click(ref: 'eN') or fill_form(fields: [...]) instead of DOM queries — refs survive layout changes, selectors don't.");
|
|
106
|
+
}
|
|
107
|
+
// Pattern 2: Reading innerText/textContent to extract visible text.
|
|
108
|
+
// filter:'all' on a subtree ref exposes the same text without a second round-trip.
|
|
109
|
+
// Require a leading dot so that string literals mentioning the word don't trigger.
|
|
110
|
+
if (/[.?]\s*(innerText|textContent)\b(?!\s*=)/.test(expression)) {
|
|
111
|
+
hints.push("Reading .innerText/.textContent? The a11y tree already contains visible text. Try read_page(ref: 'eN', filter: 'all') — table cells, static codes, paragraphs all show up with stable refs.");
|
|
112
|
+
}
|
|
113
|
+
// Pattern 3: Inspecting function source via .toString() on a Tests.* / test harness function.
|
|
114
|
+
// This is usually "the LLM is reverse-engineering the test instead of reading the UI".
|
|
115
|
+
if (/\b(Tests?|Benchmark|Spec)\b[\w.]*\.toString\s*\(\s*\)/.test(expression) ||
|
|
116
|
+
/\bfunction[\s\S]{0,80}\.toString\s*\(\s*\)/.test(expression)) {
|
|
117
|
+
hints.push("Reading test/function source via .toString()? The visible UI usually has the task description (e.g. .test-desc text). Try read_page(ref, filter:'all') first — don't debug the test harness.");
|
|
118
|
+
}
|
|
119
|
+
// Pattern 4: Scrolling via element.scrollIntoView() or container.scrollTop = N.
|
|
120
|
+
// The scroll tool handles both patterns with ref-based targeting and smooth fallback.
|
|
121
|
+
if (/\.scrollIntoView\s*\(/.test(expression) ||
|
|
122
|
+
/\.scrollTop\s*=\s*\d/.test(expression)) {
|
|
123
|
+
hints.push("Scrolling via JS? The scroll tool supports ref/selector and container scrolling — scroll(ref: 'eN') or scroll(container_ref: 'eN', direction: 'down').");
|
|
124
|
+
}
|
|
125
|
+
// Pattern 5: Dispatching click events via .click() or dispatchEvent(new MouseEvent).
|
|
126
|
+
// The click tool fires the full CDP pointer event chain which works with widgets
|
|
127
|
+
// that only listen to pointerdown/mousedown.
|
|
128
|
+
if (/\.click\s*\(\s*\)/.test(expression) ||
|
|
129
|
+
/dispatchEvent\s*\(\s*new\s+(Mouse|Pointer)Event/.test(expression)) {
|
|
130
|
+
hints.push("Dispatching click via JS? The click tool fires the full CDP pointer chain (pointerdown → mousedown → pointerup → mouseup → click), which works with custom widgets that DOM .click() silently skips.");
|
|
131
|
+
}
|
|
132
|
+
if (hints.length === 0)
|
|
133
|
+
return null;
|
|
134
|
+
return "\n\nTip: " + hints.join("\n\nTip: ");
|
|
135
|
+
}
|
|
136
|
+
export async function evaluateHandler(params, cdpClient, sessionId) {
|
|
137
|
+
const start = performance.now();
|
|
138
|
+
try {
|
|
139
|
+
// --- Execute expression ---
|
|
140
|
+
const wrappedExpression = wrapInIIFE(params.expression);
|
|
141
|
+
const cdpResult = await cdpClient.send("Runtime.evaluate", {
|
|
142
|
+
expression: wrappedExpression,
|
|
143
|
+
returnByValue: true,
|
|
144
|
+
awaitPromise: params.await_promise,
|
|
145
|
+
}, sessionId);
|
|
146
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
147
|
+
// Check for JS exception
|
|
148
|
+
if (cdpResult.exceptionDetails) {
|
|
149
|
+
const details = cdpResult.exceptionDetails;
|
|
150
|
+
const message = details.exception?.description || details.text || "Unknown JavaScript error";
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: message }],
|
|
153
|
+
isError: true,
|
|
154
|
+
_meta: { elapsedMs, method: "evaluate" },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Extract result value
|
|
158
|
+
const resultValue = cdpResult.result;
|
|
159
|
+
let text;
|
|
160
|
+
if (resultValue.type === "undefined") {
|
|
161
|
+
text = "undefined";
|
|
162
|
+
}
|
|
163
|
+
else if (resultValue.value === undefined) {
|
|
164
|
+
// Non-serializable result (e.g. DOM nodes) — returnByValue couldn't serialize
|
|
165
|
+
const desc = resultValue.description || resultValue.className || resultValue.subtype || resultValue.type;
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: `Result not serializable: ${desc}` }],
|
|
168
|
+
isError: true,
|
|
169
|
+
_meta: { elapsedMs, method: "evaluate" },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
text = JSON.stringify(resultValue.value);
|
|
174
|
+
}
|
|
175
|
+
// FR-024: Detect evaluate anti-patterns and append actionable hints so the
|
|
176
|
+
// LLM learns better defaults over time. The result stays correct — this is
|
|
177
|
+
// a "what you did isn't wrong, but there's a better tool" nudge.
|
|
178
|
+
const antiPatternHint = detectEvaluateAntiPattern(params.expression);
|
|
179
|
+
const textWithHint = antiPatternHint ? text + antiPatternHint : text;
|
|
180
|
+
const baseResult = {
|
|
181
|
+
content: [{ type: "text", text: textWithHint }],
|
|
182
|
+
_meta: {
|
|
183
|
+
elapsedMs: Math.round(performance.now() - start),
|
|
184
|
+
method: "evaluate",
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
// Story 15.2: Visual Feedback (Geometry-Diff + Clip-Screenshot) is a
|
|
188
|
+
// Pro-Feature. The Pro-Repo registers `enhanceEvaluateResult` via
|
|
189
|
+
// `registerProHooks(...)` to inject geometry + screenshot data. When
|
|
190
|
+
// no Pro-Repo is loaded, the plain text result is returned unchanged.
|
|
191
|
+
//
|
|
192
|
+
// Code-Review M2: The hook is defensively wrapped in try/catch. Any
|
|
193
|
+
// exception (sync throw or rejected Promise) falls back to `baseResult`
|
|
194
|
+
// so that a buggy Pro-Repo cannot crash the evaluate tool.
|
|
195
|
+
const hooks = getProHooks();
|
|
196
|
+
if (hooks.enhanceEvaluateResult) {
|
|
197
|
+
try {
|
|
198
|
+
return await hooks.enhanceEvaluateResult(params.expression, baseResult, {
|
|
199
|
+
cdpClient,
|
|
200
|
+
sessionId,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return baseResult;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return baseResult;
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: wrapCdpError(err, "evaluate") }],
|
|
213
|
+
isError: true,
|
|
214
|
+
_meta: { elapsedMs, method: "evaluate" },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CdpClient } from "../cdp/cdp-client.js";
|
|
3
|
+
import type { SessionManager } from "../cdp/session-manager.js";
|
|
4
|
+
import type { ToolResponse } from "../types.js";
|
|
5
|
+
export declare const fileUploadSchema: z.ZodObject<{
|
|
6
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
7
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
8
|
+
path: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
path: string | string[];
|
|
11
|
+
ref?: string | undefined;
|
|
12
|
+
selector?: string | undefined;
|
|
13
|
+
}, {
|
|
14
|
+
path: string | string[];
|
|
15
|
+
ref?: string | undefined;
|
|
16
|
+
selector?: string | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export type FileUploadParams = z.infer<typeof fileUploadSchema>;
|
|
19
|
+
export declare function formatFileSize(bytes: number): string;
|
|
20
|
+
export declare function fileUploadHandler(params: FileUploadParams, cdpClient: CdpClient, sessionId?: string, sessionManager?: SessionManager): Promise<ToolResponse>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, isAbsolute } from "node:path";
|
|
4
|
+
import { resolveElement, buildRefNotFoundError, RefNotFoundError } from "./element-utils.js";
|
|
5
|
+
import { wrapCdpError } from "./error-utils.js";
|
|
6
|
+
// --- Schema (Task 1.1) ---
|
|
7
|
+
export const fileUploadSchema = z.object({
|
|
8
|
+
ref: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("A11y-Tree element ref (e.g. 'e8') — preferred over selector"),
|
|
12
|
+
selector: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("CSS selector (e.g. 'input[type=file]') — fallback when ref is not available"),
|
|
16
|
+
path: z
|
|
17
|
+
.union([z.string(), z.array(z.string()).min(1)])
|
|
18
|
+
.describe("Absolute file path(s) to upload. String for single file, array for multiple files."),
|
|
19
|
+
});
|
|
20
|
+
// --- Helpers (Task 1.3) ---
|
|
21
|
+
export function formatFileSize(bytes) {
|
|
22
|
+
if (bytes < 1024)
|
|
23
|
+
return `${bytes} B`;
|
|
24
|
+
if (bytes < 1024 * 1024)
|
|
25
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
26
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
27
|
+
}
|
|
28
|
+
// --- Main handler (Task 1.2) ---
|
|
29
|
+
export async function fileUploadHandler(params, cdpClient, sessionId, sessionManager) {
|
|
30
|
+
const start = performance.now();
|
|
31
|
+
// Step 1: Validate ref/selector — at least one must be provided
|
|
32
|
+
if (!params.ref && !params.selector) {
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: "file_upload requires either 'ref' or 'selector' to identify the target element",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
isError: true,
|
|
41
|
+
_meta: { elapsedMs: 0, method: "file_upload" },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Step 2: Normalize paths — string or array → always string[]
|
|
45
|
+
const filePaths = Array.isArray(params.path) ? params.path : [params.path];
|
|
46
|
+
// Step 2b: Validate all paths are absolute
|
|
47
|
+
for (const filePath of filePaths) {
|
|
48
|
+
if (!isAbsolute(filePath)) {
|
|
49
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `Relativer Pfad nicht erlaubt: "${filePath}". Bitte absoluten Pfad verwenden (z.B. /Users/…/datei.pdf).`,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
isError: true,
|
|
58
|
+
_meta: { elapsedMs, method: "file_upload" },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Step 3: Validate all file paths exist BEFORE any CDP calls
|
|
63
|
+
for (const filePath of filePaths) {
|
|
64
|
+
if (!existsSync(filePath)) {
|
|
65
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: `Datei nicht gefunden: ${filePath}`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
isError: true,
|
|
74
|
+
_meta: { elapsedMs, method: "file_upload" },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
// Step 4: Resolve element (ref preferred over selector, with OOPIF routing)
|
|
80
|
+
const target = params.ref ? { ref: params.ref } : { selector: params.selector };
|
|
81
|
+
const element = await resolveElement(cdpClient, sessionId, target, sessionManager);
|
|
82
|
+
const targetSession = element.resolvedSessionId;
|
|
83
|
+
// Step 5: File-Input validation — check if element is <input type="file">
|
|
84
|
+
const tagCheck = await cdpClient.send("Runtime.callFunctionOn", {
|
|
85
|
+
objectId: element.objectId,
|
|
86
|
+
functionDeclaration: `function() {
|
|
87
|
+
return this.tagName + '|' + this.type;
|
|
88
|
+
}`,
|
|
89
|
+
returnByValue: true,
|
|
90
|
+
}, targetSession);
|
|
91
|
+
const [tagName, inputType] = tagCheck.result.value.split("|");
|
|
92
|
+
if (tagName !== "INPUT" || inputType !== "file") {
|
|
93
|
+
// Search for nearest file input in DOM for helpful error message
|
|
94
|
+
const hint = await cdpClient.send("Runtime.evaluate", {
|
|
95
|
+
expression: `(() => {
|
|
96
|
+
const fi = document.querySelector('input[type=file]');
|
|
97
|
+
if (!fi) return '';
|
|
98
|
+
return fi.name || fi.id || fi.getAttribute('aria-label') || 'unnamed';
|
|
99
|
+
})()`,
|
|
100
|
+
returnByValue: true,
|
|
101
|
+
}, targetSession);
|
|
102
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
103
|
+
let errorText = `Element ${params.ref ?? params.selector} ist kein File-Input (${tagName.toLowerCase()} ${inputType || ""}).`;
|
|
104
|
+
if (hint.result.value) {
|
|
105
|
+
errorText += ` Naechstes File-Input im DOM: ${hint.result.value}`;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: errorText.trimEnd() }],
|
|
109
|
+
isError: true,
|
|
110
|
+
_meta: { elapsedMs, method: "file_upload" },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Step 6: Multiple-validation — reject multiple files when input doesn't have multiple attribute
|
|
114
|
+
if (filePaths.length > 1) {
|
|
115
|
+
const multiCheck = await cdpClient.send("Runtime.callFunctionOn", {
|
|
116
|
+
objectId: element.objectId,
|
|
117
|
+
functionDeclaration: "function() { return this.multiple; }",
|
|
118
|
+
returnByValue: true,
|
|
119
|
+
}, targetSession);
|
|
120
|
+
if (!multiCheck.result.value) {
|
|
121
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: "File-Input akzeptiert keine mehrfachen Dateien (multiple nicht gesetzt). Nur eine Datei hochladen.",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
isError: true,
|
|
130
|
+
_meta: { elapsedMs, method: "file_upload" },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Step 7: Execute upload via CDP DOM.setFileInputFiles
|
|
135
|
+
await cdpClient.send("DOM.setFileInputFiles", { files: filePaths, backendNodeId: element.backendNodeId }, targetSession);
|
|
136
|
+
// Step 8: Get file sizes and build success response
|
|
137
|
+
const fileDetails = filePaths.map((fp) => {
|
|
138
|
+
const size = statSync(fp).size;
|
|
139
|
+
return `- ${basename(fp)} (${formatFileSize(size)})`;
|
|
140
|
+
});
|
|
141
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
142
|
+
const displayName = element.name
|
|
143
|
+
? `${element.role} '${element.name}'`
|
|
144
|
+
: (params.ref ?? params.selector);
|
|
145
|
+
const fileWord = filePaths.length === 1 ? "file" : "files";
|
|
146
|
+
return {
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: `Uploaded ${filePaths.length} ${fileWord} to ${displayName}:\n${fileDetails.join("\n")}`,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
_meta: { elapsedMs, method: "file_upload" },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
// RefNotFoundError — contextual error with "did you mean?" suggestion
|
|
158
|
+
if (err instanceof RefNotFoundError && params.ref) {
|
|
159
|
+
const errorText = buildRefNotFoundError(params.ref);
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: errorText }],
|
|
162
|
+
isError: true,
|
|
163
|
+
_meta: { elapsedMs: 0, method: "file_upload" },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// CDP connection errors — wrap for user-friendly message (consistent with other tools)
|
|
167
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text: wrapCdpError(err, "file_upload") }],
|
|
170
|
+
isError: true,
|
|
171
|
+
_meta: { elapsedMs, method: "file_upload" },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CdpClient } from "../cdp/cdp-client.js";
|
|
3
|
+
import type { SessionManager } from "../cdp/session-manager.js";
|
|
4
|
+
import type { ToolResponse } from "../types.js";
|
|
5
|
+
export declare const fillFormSchema: z.ZodObject<{
|
|
6
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
7
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
8
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
9
|
+
value: z.ZodUnion<[z.ZodString, z.ZodBoolean, z.ZodNumber]>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
value: string | number | boolean;
|
|
12
|
+
ref?: string | undefined;
|
|
13
|
+
selector?: string | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
value: string | number | boolean;
|
|
16
|
+
ref?: string | undefined;
|
|
17
|
+
selector?: string | undefined;
|
|
18
|
+
}>, "many">;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
fields: {
|
|
21
|
+
value: string | number | boolean;
|
|
22
|
+
ref?: string | undefined;
|
|
23
|
+
selector?: string | undefined;
|
|
24
|
+
}[];
|
|
25
|
+
}, {
|
|
26
|
+
fields: {
|
|
27
|
+
value: string | number | boolean;
|
|
28
|
+
ref?: string | undefined;
|
|
29
|
+
selector?: string | undefined;
|
|
30
|
+
}[];
|
|
31
|
+
}>;
|
|
32
|
+
export type FillFormParams = z.infer<typeof fillFormSchema>;
|
|
33
|
+
/**
|
|
34
|
+
* Story 16.5: Optional human-type callback injected via the `enhanceTool`
|
|
35
|
+
* Pro-Hook. Used to replace the raw `Input.insertText` with a realistic
|
|
36
|
+
* per-character typing sequence from the Pro-Repo Human Touch module.
|
|
37
|
+
*/
|
|
38
|
+
export type HumanTypeFn = (cdpClient: CdpClient, sessionId: string, text: string) => Promise<void>;
|
|
39
|
+
export declare function fillFormHandler(params: FillFormParams, cdpClient: CdpClient, sessionId?: string, sessionManager?: SessionManager): Promise<ToolResponse>;
|