claude4arc 0.5.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.
@@ -0,0 +1,353 @@
1
+ import { Task } from "./task.js";
2
+
3
+ const SELECTOR_START = /^(?:@|ref=|text=|role=|css=|xpath=|loc=|#|\.|\[)/;
4
+
5
+ const number = (value, fallback) => (value === undefined || value === "" ? fallback : Number(value));
6
+
7
+ function receiptText(receipt) {
8
+ if (!receipt || typeof receipt !== "object") return null;
9
+ const parts = [];
10
+ for (const popup of receipt.popups ?? []) parts.push(`popup ${popup.label}${popup.via ? ` (${popup.via})` : ""}`);
11
+ if (receipt.navigated) parts.push(`navigated → ${receipt.navigated}`);
12
+ for (const blocked of receipt.blocked ?? []) {
13
+ parts.push(blocked.kind === "clipboard" ? `copied (kept off the user's clipboard): ${JSON.stringify(blocked.detail)}` : `blocked ${blocked.kind}: ${blocked.detail}`);
14
+ }
15
+ for (const download of receipt.downloads ?? []) parts.push(`download started: ${download} (run "wait download" for the file path)`);
16
+ if (receipt.disabled) parts.push(`note: ${receipt.target} is disabled; the click may have done nothing`);
17
+ if (receipt.warning) parts.push(`warning: ${receipt.warning}`);
18
+ if (receipt.dialog) parts.push(`dialog ${receipt.dialog.type}: "${receipt.dialog.message}"`);
19
+ for (const dialog of receipt.dialogs ?? []) {
20
+ const outcome = dialog.type === "alert" ? "shown" : dialog.accepted ? "accepted" : "dismissed (to accept: accept -- <action>)";
21
+ parts.push(`${dialog.type} "${dialog.message}" ${outcome}`);
22
+ }
23
+ if (receipt.warning) parts.push(receipt.warning);
24
+ return parts.length ? parts.join("\n") : null;
25
+ }
26
+
27
+ function tabLine(tab) {
28
+ return `${tab.label ?? "-"} ${tab.tabId}${tab.active ? "*" : ""} ${tab.title?.slice(0, 50) ?? ""} | ${tab.url}`;
29
+ }
30
+
31
+ const POINT = /^(\d+(?:\.\d+)?),(\d+(?:\.\d+)?)$/;
32
+
33
+ const pointOf = (value) => {
34
+ const match = POINT.exec(value ?? "");
35
+ return match ? { x: Number(match[1]), y: Number(match[2]) } : null;
36
+ };
37
+
38
+ const COMMANDS = {
39
+ goto: async ({ page }, [url]) => {
40
+ const result = await page.goto(url);
41
+ return `${result.title} | ${result.url}`;
42
+ },
43
+ snap: ({ page }, args) => {
44
+ const [mode, ...rest] = args;
45
+ if (mode === "diff") return page.snapshot({ diff: true });
46
+ if (mode === "full") return page.snapshot({ scope: "full_page", root: rest.join(" ").trim() || undefined });
47
+ const root = args.join(" ").trim();
48
+ return page.snapshot(root ? { root } : {});
49
+ },
50
+ diff: ({ page }) => page.snapshot({ diff: true }),
51
+ find: ({ page }, words) => page.find(words.join(" ")),
52
+ table: ({ page }, args) => page.table(args.join(" ").trim() || undefined),
53
+ links: ({ page }, words) => page.links(words.join(" ")),
54
+ seek: ({ page }, args) => {
55
+ const container = args.length === 2 && SELECTOR_START.test(args[1]) ? args.pop() : undefined;
56
+ return page.seek(args.join(" "), { container });
57
+ },
58
+ text: async ({ page }, args) => {
59
+ const maxChars = /^\d+$/.test(args.at(-1) ?? "") ? Number(args.pop()) : 8000;
60
+ if (args[0] === "all") return (await page.text({ maxChars, all: true })) || "(no text)";
61
+ const selector = args.join(" ").trim();
62
+ if (!selector) return (await page.text({ maxChars })) || "(no text)";
63
+ try {
64
+ return (await page.text({ maxChars, selector })) || "(no text)";
65
+ } catch (error) {
66
+ if (!/^Could not find /.test(error.message)) throw error;
67
+ return `(${selector} matched nothing; main text follows)\n${(await page.text({ maxChars: Math.max(maxChars, 3000) })) || "(no text)"}`;
68
+ }
69
+ },
70
+ section: async ({ page }, args) => {
71
+ const maxChars = /^\d+$/.test(args.at(-1) ?? "") && args.length > 1 ? Number(args.pop()) : 4000;
72
+ return page.section(args.join(" "), { maxChars });
73
+ },
74
+ click: async ({ page }, [selector]) => {
75
+ const point = pointOf(selector);
76
+ if (!point) return receiptText(await page.click(selector));
77
+ await page.mouse.click(point.x, point.y, { label: `click ${selector}` });
78
+ return null;
79
+ },
80
+ dblclick: async ({ page }, [selector]) => {
81
+ const point = pointOf(selector);
82
+ if (!point) return receiptText(await page.dblclick(selector));
83
+ await page.mouse.dblclick(point.x, point.y, { label: `double-click ${selector}` });
84
+ return null;
85
+ },
86
+ hover: async ({ page }, [selector]) => {
87
+ const point = pointOf(selector);
88
+ if (point) await page.mouse.move(point.x, point.y, { steps: 3, label: `hover ${selector}` });
89
+ else await page.hover(selector);
90
+ return null;
91
+ },
92
+ fill: async ({ page }, [selector, ...value]) => receiptText(await page.fill(selector, value.join(" "))),
93
+ drag: async ({ page }, [source, target]) => {
94
+ if (!source || !target) throw new Error("Usage: drag <source> <target> (selectors, refs, or x,y points)");
95
+ const from = pointOf(source);
96
+ const to = pointOf(target);
97
+ if (from && to) {
98
+ await page.mouse.move(from.x, from.y, { label: `drag from ${source}` });
99
+ await page.mouse.down();
100
+ await page.mouse.move(to.x, to.y, { steps: 12, label: `drag to ${target}` });
101
+ await page.mouse.up();
102
+ return null;
103
+ }
104
+ await page.dragAndDrop(source, target);
105
+ return null;
106
+ },
107
+ type: async ({ page }, text) => {
108
+ await page.keyboard.type(text.join(" "));
109
+ return null;
110
+ },
111
+ insert: async ({ page }, text) => {
112
+ await page.keyboard.insertText(text.join(" "));
113
+ return null;
114
+ },
115
+ press: async ({ page }, args) => receiptText(args.length > 1 ? await page.press(args[0], args[1]) : await page.press(args[0])),
116
+ select: async ({ page }, [selector, ...values]) => (await page.selectOption(selector, values.length > 1 ? values : values[0])).join(", "),
117
+ check: async ({ page }, [selector]) => receiptText(await page.check(selector)),
118
+ uncheck: async ({ page }, [selector]) => receiptText(await page.uncheck(selector)),
119
+ upload: async ({ page }, [selector, ...files]) => {
120
+ await page.setInputFiles(selector, files);
121
+ return null;
122
+ },
123
+ scroll: async ({ page }, args) => {
124
+ const numbers = args.filter((arg) => /^-?\d+$/.test(arg));
125
+ const selector = args.filter((arg) => !/^-?\d+$/.test(arg)).join(" ").trim() || undefined;
126
+ const position = await page.scroll(number(numbers[0], 600), { deltaX: number(numbers[1], 0), selector });
127
+ const end = position.scrollY + position.view >= position.height - 2 ? " (end)" : position.scrollY === 0 ? " (top)" : "";
128
+ return `${position.scroller} y=${position.scrollY}/${Math.max(0, position.height - position.view)}${end}`;
129
+ },
130
+ wait: async ({ page }, [target, ...rest]) => {
131
+ const forget = rest.includes("forget");
132
+ const timeout = rest.find((arg) => /^\d+$/.test(arg));
133
+ const options = { timeout: number(timeout, 15_000) };
134
+ if (target === "download") {
135
+ const file = await page.waitForDownload({ timeout: number(timeout, 60_000), forget });
136
+ return `download complete: ${file.path} (${file.bytes ?? "?"} bytes)`;
137
+ }
138
+ if (/^\d+$/.test(target)) await page.waitForTimeout(Number(target));
139
+ else if (target.startsWith("url:")) await page.waitForURL(target.slice(4), options);
140
+ else if (target.startsWith("gone:")) await page.waitForSelector(target.slice(5), { ...options, state: "hidden" });
141
+ else await page.waitForSelector(target, options);
142
+ return null;
143
+ },
144
+ back: async ({ page }) => page.goBack(),
145
+ forward: async ({ page }) => page.goForward(),
146
+ reload: async ({ page }) => {
147
+ await page.reload();
148
+ return null;
149
+ },
150
+ eval: async ({ page }, code) => {
151
+ const frame = /^@\d+(\.\d+)*$/.test(code[0] ?? "") && code.length > 1 ? code.shift() : undefined;
152
+ const value = await page.evaluate(code.join(" "), undefined, { frame });
153
+ if (value === undefined) return "undefined";
154
+ if (value === "") return '""';
155
+ return typeof value === "string" ? value : JSON.stringify(value);
156
+ },
157
+ shot: ({ page }, args) =>
158
+ page.screenshot({ fullPage: args.includes("full"), path: args.find((arg) => arg.includes("/")) }),
159
+ front: async ({ page }) => {
160
+ await page.bringToFront();
161
+ return null;
162
+ },
163
+ accept: async ({ page }, text) => ((await page.acceptDialog(text.length ? text.join(" ") : undefined)) === "armed" ? null : "accepted"),
164
+ dismiss: async ({ page }) => ((await page.dismissDialog()) === "armed" ? null : "dismissed"),
165
+ url: ({ page }) => page.url(),
166
+ title: ({ page }) => page.title(),
167
+ info: async ({ page }) => JSON.stringify(await page.info()),
168
+ close: async ({ page }) => {
169
+ await page.close();
170
+ return null;
171
+ },
172
+ open: async ({ task }, [url]) => {
173
+ const page = await task.newPage({ url });
174
+ return `${page.label} | ${await page.title()} | ${await page.url()}`;
175
+ },
176
+ use: async ({ task }, [label]) => {
177
+ await task.use(label);
178
+ return null;
179
+ },
180
+ pages: async ({ task }) => (await task.tabs()).filter((tab) => tab.label).map(tabLine).join("\n"),
181
+ tabs: async ({ task }) => (await task.tabs()).map(tabLine).join("\n"),
182
+ adopt: async ({ task }, [tabId]) => {
183
+ const target = tabId ? Number(tabId) : await task.userTab();
184
+ const page = await task.adopt(target);
185
+ return `${page.label} | ${await page.title()} | ${await page.url()}`;
186
+ },
187
+ finish: async ({ task }, keep) => {
188
+ const result = await task.finish({ keep });
189
+ return `closed ${result.closed.join(",") || "-"} kept ${result.kept.join(",") || "-"} released ${result.released.join(",") || "-"}`;
190
+ },
191
+ };
192
+
193
+ const ALIASES = { snapshot: "snap", go: "goto", screenshot: "shot", evaluate: "eval", key: "press" };
194
+
195
+ const TASK_COMMANDS = new Set(["open", "use", "pages", "tabs", "adopt", "finish"]);
196
+
197
+ export const COMMAND_NAMES = Object.keys(COMMANDS);
198
+
199
+ export function splitChain(args) {
200
+ const steps = [[]];
201
+ for (const arg of args) {
202
+ if (arg === "--") steps.push([]);
203
+ else steps.at(-1).push(arg);
204
+ }
205
+ return steps.filter((step) => step.length);
206
+ }
207
+
208
+ export async function runCommands(bridge, target, args) {
209
+ const snapAfter = args.at(-1) === "-s";
210
+ const steps = splitChain(snapAfter ? args.slice(0, -1) : args);
211
+ const [idText, labelText] = target.split(":");
212
+ const task = await Task.open(bridge, Number(idText));
213
+ if (labelText) await task.use(labelText);
214
+ return executeSteps(task, steps, snapAfter);
215
+ }
216
+
217
+ async function executeSteps(task, steps, snapAfter) {
218
+ const output = [];
219
+ let finished = false;
220
+ for (const [rawName, ...rest] of steps) {
221
+ const name = ALIASES[rawName] ?? rawName;
222
+ const handler = COMMANDS[name];
223
+ if (!handler) throw new Error(`Unknown command "${rawName}". Commands: ${COMMAND_NAMES.join(", ")}`);
224
+ const context = { task, page: TASK_COMMANDS.has(name) ? null : task.page() };
225
+ let result;
226
+ try {
227
+ result = await handler(context, rest);
228
+ } catch (error) {
229
+ if (!context.page || !/Detached while handling command|Debugger is not attached|Session with given id not found|Cannot find context with specified id/i.test(error.message)) throw error;
230
+ context.page._resetScopes();
231
+ await new Promise((resolve) => setTimeout(resolve, 150));
232
+ result = await handler(context, rest);
233
+ }
234
+ if (result !== null && result !== undefined && result !== "") output.push(String(result));
235
+ if (name === "finish") finished = true;
236
+ else if (context.page) await context.page._enforceBlocklist();
237
+ }
238
+ if (!finished) output.push(...(await task._settleDialogs()));
239
+ if (snapAfter && !finished) {
240
+ const page = task.page();
241
+ await page.settle();
242
+ output.push(await page.snapshot({ diff: true }));
243
+ }
244
+ return output.length ? output.join("\n") : "ok";
245
+ }
246
+
247
+ export async function createTask(bridge, args) {
248
+ const snapAfter = args.at(-1) === "-s";
249
+ const rest = snapAfter ? args.slice(0, -1) : args;
250
+ const split = rest.indexOf("--");
251
+ const head = split >= 0 ? rest.slice(0, split) : rest;
252
+ const chain = split >= 0 ? splitChain(rest.slice(split + 1)) : [];
253
+ const [url, ...nameParts] = head;
254
+ const name = nameParts.join(" ") || (url ? url.replace(/^https?:\/\//, "").split("/")[0] : "task");
255
+ const task = await Task.open(bridge, name, { url });
256
+ const page = task.page();
257
+ const lines = [`task ${task.spaceId} ${page.label} | ${await page.title()} | ${await page.url()}`];
258
+ if (chain.length) {
259
+ if (snapAfter) await page.snapshot().catch(() => {});
260
+ lines.push(await executeSteps(task, chain, snapAfter));
261
+ } else if (snapAfter && url) {
262
+ lines.push(await page.snapshot());
263
+ }
264
+ return lines.join("\n");
265
+ }
266
+
267
+ export function shellWords(line) {
268
+ const words = [];
269
+ let index = 0;
270
+ const escapes = { n: "\n", t: "\t", r: "\r", "\\": "\\", "'": "'", '"': '"' };
271
+ while (index < line.length) {
272
+ while (/\s/.test(line[index] ?? "")) index++;
273
+ if (index >= line.length) break;
274
+ let word = "";
275
+ while (index < line.length && !/\s/.test(line[index])) {
276
+ const char = line[index];
277
+ if (char === "$" && line[index + 1] === "'") {
278
+ index += 2;
279
+ while (index < line.length && line[index] !== "'") {
280
+ if (line[index] === "\\") {
281
+ word += escapes[line[index + 1]] ?? `\\${line[index + 1]}`;
282
+ index += 2;
283
+ } else word += line[index++];
284
+ }
285
+ index++;
286
+ } else if (char === "'") {
287
+ const end = line.indexOf("'", index + 1);
288
+ if (end < 0) throw new Error(`Unclosed ' in: ${line}`);
289
+ word += line.slice(index + 1, end);
290
+ index = end + 1;
291
+ } else if (char === '"') {
292
+ index++;
293
+ while (index < line.length && line[index] !== '"') {
294
+ if (line[index] === "\\" && /["\\$`]/.test(line[index + 1] ?? "")) {
295
+ word += line[index + 1];
296
+ index += 2;
297
+ } else word += line[index++];
298
+ }
299
+ if (index >= line.length) throw new Error(`Unclosed " in: ${line}`);
300
+ index++;
301
+ } else if (char === "\\") {
302
+ word += line[index + 1] ?? "";
303
+ index += 2;
304
+ } else {
305
+ word += char;
306
+ index++;
307
+ }
308
+ }
309
+ words.push(word);
310
+ }
311
+ return words;
312
+ }
313
+
314
+ const LABEL = /^([A-Za-z][\w.-]{0,15}):\s+(.*)$/;
315
+
316
+ export function batchLabel(line, number) {
317
+ return line.match(LABEL)?.[1] ?? String(number + 1);
318
+ }
319
+
320
+ export function parseBatchLine(line) {
321
+ const labelled = line.match(LABEL);
322
+ const args = shellWords(labelled ? labelled[2] : line);
323
+ const snapAfter = args.at(-1) === "-s";
324
+ return { steps: splitChain(snapAfter ? args.slice(0, -1) : args), snapAfter };
325
+ }
326
+
327
+ export async function runBatch(bridge, script, { taskId, keep = false, write }) {
328
+ const lines = script.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
329
+ if (!lines.length) throw new Error("No batch lines given. Pipe one command chain per line on stdin.");
330
+ const task = taskId ? await Task.open(bridge, Number(taskId)) : await Task.open(bridge, "batch", { url: "about:blank" });
331
+ write(`task ${task.spaceId}`);
332
+ let failures = 0;
333
+ let finished = false;
334
+ for (const [number, line] of lines.entries()) {
335
+ const label = batchLabel(line, number);
336
+ const start = Date.now();
337
+ let body;
338
+ let ok = true;
339
+ try {
340
+ const { steps, snapAfter } = parseBatchLine(line);
341
+ if (steps.some(([name]) => name === "finish")) finished = true;
342
+ body = await executeSteps(task, steps, snapAfter);
343
+ } catch (error) {
344
+ ok = false;
345
+ failures++;
346
+ body = `Error: ${error.message}`;
347
+ }
348
+ const end = Date.now();
349
+ write(`== ${label} ${ok ? "ok" : "FAILED"} ${start}-${end} (${end - start} ms)\n${body}`);
350
+ }
351
+ if (!keep && !finished) write(`== finish\n${await executeSteps(task, [["finish"]], false)}`);
352
+ return failures;
353
+ }
package/lib/config.js ADDED
@@ -0,0 +1,22 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { STATE_DIR } from "./paths.js";
4
+
5
+ export const CONFIG_PATH = path.join(STATE_DIR, "config.json");
6
+
7
+ export function readConfig() {
8
+ try {
9
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
10
+ } catch {
11
+ return {};
12
+ }
13
+ }
14
+
15
+ export function updateConfig(changes) {
16
+ fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
17
+ const config = { ...readConfig(), ...changes };
18
+ const temp = `${CONFIG_PATH}.${process.pid}.tmp`;
19
+ fs.writeFileSync(temp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
20
+ fs.renameSync(temp, CONFIG_PATH);
21
+ return config;
22
+ }
package/lib/dnd.js ADDED
@@ -0,0 +1,60 @@
1
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2
+ const SETTLE_MS = 120;
3
+
4
+ function between(from, to, fraction) {
5
+ return { x: from.x + (to.x - from.x) * fraction, y: from.y + (to.y - from.y) * fraction };
6
+ }
7
+
8
+ function eventWaiter(page, waitForEvent) {
9
+ if (waitForEvent) return waitForEvent;
10
+ if (typeof page._waitForCdpEvent === "function") return (method, options) => page._waitForCdpEvent(method, options);
11
+ throw new Error("html5Drag needs page._waitForCdpEvent or a waitForEvent option.");
12
+ }
13
+
14
+ async function looksDraggable(page, point) {
15
+ return page
16
+ .evaluate(
17
+ ({ x, y }) => Boolean(document.elementFromPoint(x, y)?.closest("[draggable=\"true\"], a[href], img")),
18
+ point,
19
+ )
20
+ .catch(() => false);
21
+ }
22
+
23
+ export async function html5Drag(page, from, to, { steps = 8, timeout = 3000, draggable, labels = {}, waitForEvent } = {}) {
24
+ const wait = eventWaiter(page, waitForEvent);
25
+ const expectDrag = draggable ?? (await looksDraggable(page, from));
26
+ await page.cdp("Input.setInterceptDrags", { enabled: true });
27
+ let data = null;
28
+ const intercepted = Promise.resolve()
29
+ .then(() => wait("Input.dragIntercepted", { timeout }))
30
+ .then(
31
+ (params) => {
32
+ data = params.data;
33
+ },
34
+ () => {},
35
+ );
36
+ try {
37
+ await page.mouse.move(from.x, from.y, { label: labels.from ?? "drag" });
38
+ await page.mouse.down();
39
+ let current = from;
40
+ for (let step = 1; step <= steps && !data; step++) {
41
+ current = between(from, to, step / steps);
42
+ await page.mouse.move(current.x, current.y, { label: false });
43
+ }
44
+ if (!data) await Promise.race([intercepted, sleep(expectDrag ? timeout : SETTLE_MS)]);
45
+ if (data) {
46
+ await page.cdp("Input.dispatchDragEvent", { type: "dragEnter", x: current.x, y: current.y, data });
47
+ const overSteps = Math.max(1, Math.ceil(steps / 2));
48
+ for (let step = 1; step <= overSteps; step++) {
49
+ const point = between(current, to, step / overSteps);
50
+ await page.cdp("Input.dispatchDragEvent", { type: "dragOver", x: point.x, y: point.y, data });
51
+ }
52
+ await page.cdp("Input.dispatchDragEvent", { type: "drop", x: to.x, y: to.y, data });
53
+ }
54
+ await page.mouse.move(to.x, to.y, { label: labels.to ?? "drop" });
55
+ await page.mouse.up();
56
+ return { intercepted: Boolean(data) };
57
+ } finally {
58
+ await page.cdp("Input.setInterceptDrags", { enabled: false }).catch(() => {});
59
+ }
60
+ }