trawl-devices-agent 0.1.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/auth.js +23 -0
  4. package/dist/auth.js.map +1 -0
  5. package/dist/codegen.js +41 -0
  6. package/dist/codegen.js.map +1 -0
  7. package/dist/control.js +106 -0
  8. package/dist/control.js.map +1 -0
  9. package/dist/devices.js +69 -0
  10. package/dist/devices.js.map +1 -0
  11. package/dist/guide.js +14 -0
  12. package/dist/guide.js.map +1 -0
  13. package/dist/index.js +40 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/interpolate.js +20 -0
  16. package/dist/interpolate.js.map +1 -0
  17. package/dist/mask.js +19 -0
  18. package/dist/mask.js.map +1 -0
  19. package/dist/recorder.js +99 -0
  20. package/dist/recorder.js.map +1 -0
  21. package/dist/recorderInject.js +165 -0
  22. package/dist/recorderInject.js.map +1 -0
  23. package/dist/retention.js +21 -0
  24. package/dist/retention.js.map +1 -0
  25. package/dist/routes.js +140 -0
  26. package/dist/routes.js.map +1 -0
  27. package/dist/runner.js +373 -0
  28. package/dist/runner.js.map +1 -0
  29. package/dist/sandbox.js +80 -0
  30. package/dist/sandbox.js.map +1 -0
  31. package/dist/server.js +100 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/sessions.js +85 -0
  34. package/dist/sessions.js.map +1 -0
  35. package/dist/steps.js +27 -0
  36. package/dist/steps.js.map +1 -0
  37. package/dist/targets.js +68 -0
  38. package/dist/targets.js.map +1 -0
  39. package/dist/traffic.js +116 -0
  40. package/dist/traffic.js.map +1 -0
  41. package/dist/transform.js +110 -0
  42. package/dist/transform.js.map +1 -0
  43. package/dist/types.js +12 -0
  44. package/dist/types.js.map +1 -0
  45. package/dist/version.js +5 -0
  46. package/dist/version.js.map +1 -0
  47. package/dist/workspace.js +50 -0
  48. package/dist/workspace.js.map +1 -0
  49. package/package.json +50 -0
  50. package/skills/writing-device-scripts/SKILL.md +59 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 legostin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # trawl-devices-agent
2
+
3
+ Local sidecar for the Trawl **devices** plugin: records browser actions into a
4
+ declarative JS DSL and replays them with Playwright.
5
+
6
+ ## Run
7
+
8
+ ```sh
9
+ npx trawl-devices-agent@latest --workspace=/path/to/repo --port=8787
10
+ ```
11
+
12
+ It prints the port and the bearer token; paste the token into the plugin once.
13
+ The token also lives in `~/.trawl-devices/agent.json` (mode 0600).
14
+
15
+ Install a browser once: `npx playwright install chromium`.
16
+
17
+ Flags: `--workspace` (default: cwd), `--port` (8787, next free port if taken),
18
+ `--proxy-port` (8080, Trawl's proxy), `--keep-runs` (50).
19
+
20
+ ## Workspace layout
21
+
22
+ ```
23
+ <workspace>/
24
+ devices.json # device registry, commit it
25
+ scripts/*.js # scenarios, commit them
26
+ runs/<runId>/ # report.json, trace.zip, *.png — gitignore this
27
+ ```
28
+
29
+ ## Writing scripts
30
+
31
+ See `skills/writing-device-scripts/SKILL.md` — the same text the agent serves at
32
+ `GET /guide` and that can be symlinked into `~/.claude/skills/`.
33
+
34
+ ## Security model
35
+
36
+ The agent listens on `127.0.0.1` only and refuses any request that lacks the
37
+ bearer token, carries an `Origin` header, or arrives with an unexpected `Host`
38
+ header. `GET /health` answers without a token so the plugin can detect a running
39
+ agent, and returns only the agent version in that case.
40
+
41
+ **Scripts are not sandboxed against malice.** `node:vm` keeps `require`,
42
+ `process` and `fetch` out of scope so accidents stay contained, but a script runs
43
+ with your privileges. Run only scripts you wrote or reviewed — the same trust
44
+ model as Trawl plugins.
45
+
46
+ Secrets passed by the plugin are masked in reports, logs and API responses.
47
+ Playwright traces cannot be masked, so a device that uses `secret()` should keep
48
+ `trace` at `on-failure` (the default) rather than `always`.
49
+
50
+ ## API
51
+
52
+ | Method | Path | Purpose |
53
+ | --- | --- | --- |
54
+ | GET | `/health` | version, DSL version, known steps |
55
+ | GET/POST | `/devices` | list / upsert devices |
56
+ | POST/GET/DELETE | `/sessions` | start, list, stop browser sessions |
57
+ | GET/POST | `/scripts…` | list, read, write, validate scripts |
58
+ | POST/GET/DELETE | `/runs…` | start, poll, cancel runs |
59
+ | POST/GET | `/record…` | start, inspect, stop a recording |
60
+ | POST | `/control/snapshot`, `/control/do` | live browser control |
61
+ | GET | `/guide` | the DSL reference |
package/dist/auth.js ADDED
@@ -0,0 +1,23 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { randomBytes } from "node:crypto";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ export const configDir = () => path.join(os.homedir(), ".trawl-devices");
6
+ /** Read the bearer token, creating a 0600 file with a fresh one if absent. */
7
+ export async function loadOrCreateToken(dir = configDir()) {
8
+ const file = path.join(dir, "agent.json");
9
+ try {
10
+ const parsed = JSON.parse(await fs.readFile(file, "utf8"));
11
+ if (typeof parsed?.token === "string" && parsed.token.length >= 32)
12
+ return parsed.token;
13
+ }
14
+ catch {
15
+ // missing or corrupt — fall through and mint a new one
16
+ }
17
+ const token = randomBytes(24).toString("base64url"); // 32 chars
18
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
19
+ await fs.writeFile(file, JSON.stringify({ token }, null, 2) + "\n", { mode: 0o600 });
20
+ await fs.chmod(file, 0o600);
21
+ return token;
22
+ }
23
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,CAAC,MAAM,SAAS,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,gBAAgB,CAAC,CAAC;AAEjF,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAc,SAAS,EAAE;IAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,IAAI,OAAO,MAAM,EAAE,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE;YAAE,OAAO,MAAM,CAAC,KAAK,CAAC;IAC1F,CAAC;IAAC,MAAM,CAAC;QACP,uDAAuD;IACzD,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW;IAChE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACtD,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACrF,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC5B,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { isRegExp, isRegexSpec } from "./targets.js";
2
+ const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
3
+ const quote = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
4
+ export function formatArg(value) {
5
+ if (value === null)
6
+ return "null";
7
+ if (value === undefined)
8
+ return "undefined";
9
+ if (typeof value === "string")
10
+ return quote(value);
11
+ if (typeof value === "number" || typeof value === "boolean")
12
+ return String(value);
13
+ if (isRegExp(value))
14
+ return String(value);
15
+ if (isRegexSpec(value))
16
+ return `/${value.__regex.source}/${value.__regex.flags}`;
17
+ if (Array.isArray(value))
18
+ return `[${value.map(formatArg).join(", ")}]`;
19
+ if (typeof value === "object") {
20
+ const entries = Object.entries(value)
21
+ .filter(([, v]) => v !== undefined)
22
+ .map(([k, v]) => `${IDENT.test(k) ? k : quote(k)}: ${formatArg(v)}`);
23
+ return entries.length ? `{ ${entries.join(", ")} }` : "{}";
24
+ }
25
+ return "null";
26
+ }
27
+ const SETUP = new Set(["device", "use"]);
28
+ /** Steps → a runnable DSL script. Setup calls are grouped and separated by a blank line. */
29
+ export function generate(steps, options = {}) {
30
+ const line = (s) => `${s.action}(${s.args.map(formatArg).join(", ")})`;
31
+ const setup = steps.filter((s) => SETUP.has(s.action)).map(line);
32
+ const body = steps.filter((s) => !SETUP.has(s.action)).map(line);
33
+ const chunks = [];
34
+ if (options.header)
35
+ chunks.push(`// ${options.header}`);
36
+ if (setup.length)
37
+ chunks.push(setup.join("\n"), "");
38
+ chunks.push(body.join("\n"));
39
+ return chunks.join("\n").replace(/\n+$/, "") + "\n";
40
+ }
41
+ //# sourceMappingURL=codegen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codegen.js","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ import { AgentError } from "./types.js";
2
+ import { toLocator } from "./targets.js";
3
+ const REF_ATTR = "data-trawl-ref";
4
+ /** Interactive/labelled nodes, each tagged with a stable ref for the next action. */
5
+ export async function snapshot(page) {
6
+ return page.evaluate((refAttr) => {
7
+ const SELECTOR = "a[href],button,input,select,textarea,[role],[tabindex]:not([tabindex='-1']),h1,h2,h3,label";
8
+ const implicitRole = (el) => {
9
+ const explicit = el.getAttribute("role");
10
+ if (explicit)
11
+ return explicit;
12
+ const tag = el.tagName.toLowerCase();
13
+ if (tag === "button")
14
+ return "button";
15
+ if (tag === "a")
16
+ return "link";
17
+ if (tag === "select")
18
+ return "combobox";
19
+ if (tag === "textarea")
20
+ return "textbox";
21
+ if (tag === "label")
22
+ return "label";
23
+ if (/^h[1-6]$/.test(tag))
24
+ return "heading";
25
+ if (tag === "input") {
26
+ const type = (el.getAttribute("type") ?? "text").toLowerCase();
27
+ if (type === "checkbox")
28
+ return "checkbox";
29
+ if (type === "radio")
30
+ return "radio";
31
+ if (type === "submit" || type === "button")
32
+ return "button";
33
+ return "textbox";
34
+ }
35
+ return tag;
36
+ };
37
+ let counter = 0;
38
+ const out = [];
39
+ for (const el of Array.from(document.querySelectorAll(SELECTOR))) {
40
+ const ref = `e${++counter}`;
41
+ el.setAttribute(refAttr, ref);
42
+ const input = el;
43
+ const labelled = input.labels?.[0]?.textContent?.trim();
44
+ const rect = el.getBoundingClientRect();
45
+ out.push({
46
+ ref,
47
+ role: implicitRole(el),
48
+ name: (el.getAttribute("aria-label") ?? labelled ?? el.textContent ?? "").trim().slice(0, 80),
49
+ ...(labelled ? { label: labelled } : {}),
50
+ tag: el.tagName.toLowerCase(),
51
+ ...(input.value !== undefined ? { value: String(input.value).slice(0, 80) } : {}),
52
+ visible: rect.width > 0 && rect.height > 0,
53
+ });
54
+ }
55
+ return out;
56
+ }, REF_ATTR);
57
+ }
58
+ /** Perform one action by ref or by declarative target, and report what changed. */
59
+ export async function performAction(page, input) {
60
+ const consoleErrors = [];
61
+ const onConsole = (msg) => {
62
+ if (msg.type() === "error")
63
+ consoleErrors.push(msg.text());
64
+ };
65
+ page.on("console", onConsole);
66
+ try {
67
+ if (input.action === "goto") {
68
+ await page.goto(input.value ?? "about:blank");
69
+ }
70
+ else if (input.action !== "screenshot") {
71
+ const locator = input.ref ? page.locator(`[${REF_ATTR}="${input.ref}"]`) : toLocator(page, input.target ?? {});
72
+ if (input.ref && (await locator.count()) === 0) {
73
+ throw new AgentError("script", `unknown ref: ${input.ref}`);
74
+ }
75
+ switch (input.action) {
76
+ case "click":
77
+ await locator.click({ timeout: 10_000 });
78
+ break;
79
+ case "fill":
80
+ await locator.fill(input.value ?? "", { timeout: 10_000 });
81
+ break;
82
+ case "check":
83
+ await locator.check({ timeout: 10_000 });
84
+ break;
85
+ case "uncheck":
86
+ await locator.uncheck({ timeout: 10_000 });
87
+ break;
88
+ case "select":
89
+ await locator.selectOption(input.value ?? "", { timeout: 10_000 });
90
+ break;
91
+ case "hover":
92
+ await locator.hover({ timeout: 10_000 });
93
+ break;
94
+ case "press":
95
+ await locator.press(input.value ?? "Enter", { timeout: 10_000 });
96
+ break;
97
+ }
98
+ }
99
+ await page.waitForTimeout(150);
100
+ return { url: page.url(), title: await page.title(), consoleErrors };
101
+ }
102
+ finally {
103
+ page.off("console", onConsole);
104
+ }
105
+ }
106
+ //# sourceMappingURL=control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control.js","sourceRoot":"","sources":["../src/control.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { AgentError } from "./types.js";
4
+ import { resolveInWorkspace } from "./workspace.js";
5
+ export const DEVICE_DEFAULTS = {
6
+ type: "browser",
7
+ browser: "chromium",
8
+ headless: false,
9
+ viewport: { width: 1280, height: 800 },
10
+ proxy: { mode: "trawl" },
11
+ ignoreHTTPSErrors: true,
12
+ trace: "on-failure",
13
+ video: false,
14
+ capabilities: ["record", "run", "live", "traffic"],
15
+ };
16
+ const BROWSERS = ["chromium", "firefox", "webkit"];
17
+ const TRACES = ["off", "on-failure", "always"];
18
+ const PROXY_MODES = ["trawl", "none", "custom"];
19
+ /** Fill defaults and reject malformed input. Throws AgentError("script"). */
20
+ export function validateDevice(input) {
21
+ const raw = (input ?? {});
22
+ const fail = (msg) => {
23
+ throw new AgentError("script", `invalid device: ${msg}`);
24
+ };
25
+ if (typeof raw.id !== "string" || !/^[a-zA-Z0-9._-]+$/.test(raw.id))
26
+ fail("id must match [a-zA-Z0-9._-]+");
27
+ if (typeof raw.name !== "string" || !raw.name.trim())
28
+ fail("name is required");
29
+ const merged = { ...DEVICE_DEFAULTS, ...raw };
30
+ if (!BROWSERS.includes(merged.browser))
31
+ fail(`browser must be one of ${BROWSERS.join(", ")}`);
32
+ if (!TRACES.includes(merged.trace))
33
+ fail(`trace must be one of ${TRACES.join(", ")}`);
34
+ if (!PROXY_MODES.includes(merged.proxy?.mode))
35
+ fail(`proxy.mode must be one of ${PROXY_MODES.join(", ")}`);
36
+ if (merged.proxy.mode === "custom" && !merged.proxy.url)
37
+ fail("proxy.url is required when proxy.mode is custom");
38
+ merged.type = "browser";
39
+ return merged;
40
+ }
41
+ const registryPath = (root) => resolveInWorkspace(root, "devices.json");
42
+ export async function loadDevices(root) {
43
+ try {
44
+ const parsed = JSON.parse(await fs.readFile(registryPath(root), "utf8"));
45
+ return Array.isArray(parsed?.devices) ? parsed.devices.map(validateDevice) : [];
46
+ }
47
+ catch (err) {
48
+ if (err.code === "ENOENT")
49
+ return [];
50
+ throw err;
51
+ }
52
+ }
53
+ export async function saveDevice(root, patch) {
54
+ const device = validateDevice(patch);
55
+ const devices = await loadDevices(root);
56
+ const next = devices.filter((d) => d.id !== device.id).concat(device);
57
+ next.sort((a, b) => a.id.localeCompare(b.id));
58
+ const file = registryPath(root);
59
+ await fs.mkdir(path.dirname(file), { recursive: true });
60
+ await fs.writeFile(file, JSON.stringify({ devices: next }, null, 2) + "\n", "utf8");
61
+ return device;
62
+ }
63
+ export async function getDevice(root, id) {
64
+ const found = (await loadDevices(root)).find((d) => d.id === id);
65
+ if (!found)
66
+ throw new AgentError("device", `unknown device: ${id}`);
67
+ return found;
68
+ }
69
+ //# sourceMappingURL=devices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devices.js","sourceRoot":"","sources":["../src/devices.ts"],"names":[],"mappings":"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"}
package/dist/guide.js ADDED
@@ -0,0 +1,14 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * Resolved relative to this module, so it works from src/ under vitest and from
6
+ * dist/ in the published package — `skills/` sits at the package root in both.
7
+ */
8
+ const GUIDE_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills/writing-device-scripts/SKILL.md");
9
+ /** The DSL reference, frontmatter stripped. Single source shared with the Claude Code skill. */
10
+ export async function readGuide() {
11
+ const raw = await fs.readFile(GUIDE_PATH, "utf8");
12
+ return raw.replace(/^---\n[\s\S]*?\n---\n/, "").trim() + "\n";
13
+ }
14
+ //# sourceMappingURL=guide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guide.js","sourceRoot":"","sources":["../src/guide.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAC1B,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAC5C,IAAI,EACJ,wCAAwC,CACzC,CAAC;AAEF,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAClD,OAAO,GAAG,CAAC,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC;AAChE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from "./server.js";
3
+ import { loadOrCreateToken } from "./auth.js";
4
+ import { ensureWorkspace } from "./workspace.js";
5
+ import { pruneRuns } from "./retention.js";
6
+ import { AGENT_VERSION, DSL_VERSION } from "./version.js";
7
+ import { STEP_NAMES } from "./steps.js";
8
+ import { buildRoutes } from "./routes.js";
9
+ const arg = (name, fallback) => {
10
+ const hit = process.argv.find((a) => a.startsWith(`--${name}=`));
11
+ return hit ? hit.slice(name.length + 3) : fallback;
12
+ };
13
+ const workspace = arg("workspace", process.cwd());
14
+ const wanted = Number(arg("port", "8787"));
15
+ const trawlProxyPort = Number(arg("proxy-port", "8080"));
16
+ const listen = (server, candidate) => new Promise((resolve, reject) => {
17
+ server.once("error", (err) => (err.code === "EADDRINUSE" ? resolve(-1) : reject(err)));
18
+ server.listen(candidate, "127.0.0.1", () => resolve(candidate));
19
+ });
20
+ const token = await loadOrCreateToken();
21
+ await ensureWorkspace(workspace);
22
+ const pruned = await pruneRuns(workspace, Number(arg("keep-runs", "50")));
23
+ if (pruned.length)
24
+ console.log(`pruned ${pruned.length} old run(s)`);
25
+ let port = -1;
26
+ for (let candidate = wanted; candidate < wanted + 20 && port < 0; candidate++) {
27
+ const server = createServer({
28
+ token,
29
+ getPort: () => port,
30
+ routes: buildRoutes({ workspace, trawlProxyPort }),
31
+ health: () => ({ dsl: DSL_VERSION, steps: [...STEP_NAMES], workspace }),
32
+ });
33
+ port = await listen(server, candidate);
34
+ }
35
+ if (port < 0)
36
+ throw new Error(`no free port in ${wanted}..${wanted + 20}`);
37
+ console.log(`trawl-devices-agent ${AGENT_VERSION} listening on http://127.0.0.1:${port}`);
38
+ console.log(`workspace: ${workspace}`);
39
+ console.log(`token: ${token}`);
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { AgentError } from "./types.js";
2
+ const PATTERN = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
3
+ /** Replace {{VAR}} from `env`, deeply. Unknown variables are a script error. */
4
+ export function interpolate(value, env) {
5
+ if (typeof value === "string") {
6
+ return value.replace(PATTERN, (_match, name) => {
7
+ const replacement = env[name];
8
+ if (replacement === undefined)
9
+ throw new AgentError("script", `unknown variable: ${name}`);
10
+ return replacement;
11
+ });
12
+ }
13
+ if (Array.isArray(value))
14
+ return value.map((v) => interpolate(v, env));
15
+ if (value && typeof value === "object" && Object.prototype.toString.call(value) !== "[object RegExp]") {
16
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, interpolate(v, env)]));
17
+ }
18
+ return value;
19
+ }
20
+ //# sourceMappingURL=interpolate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interpolate.js","sourceRoot":"","sources":["../src/interpolate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,OAAO,GAAG,yCAAyC,CAAC;AAE1D,gFAAgF;AAChF,MAAM,UAAU,WAAW,CAAC,KAAc,EAAE,GAA2B;IACrE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,EAAE;YACrD,MAAM,WAAW,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,WAAW,KAAK,SAAS;gBAAE,MAAM,IAAI,UAAU,CAAC,QAAQ,EAAE,qBAAqB,IAAI,EAAE,CAAC,CAAC;YAC3F,OAAO,WAAW,CAAC;QACrB,CAAC,CAAC,CAAC;IACL,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACvE,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,iBAAiB,EAAE,CAAC;QACtG,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACxH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
package/dist/mask.js ADDED
@@ -0,0 +1,19 @@
1
+ /** Replaces every secret value with *** in any string reachable from `value`. */
2
+ export function makeMasker(secrets) {
3
+ const values = Object.values(secrets)
4
+ .filter((v) => typeof v === "string" && v.length >= 4)
5
+ .sort((a, b) => b.length - a.length);
6
+ const maskString = (input) => values.reduce((acc, secret) => acc.split(secret).join("***"), input);
7
+ const walk = (value) => {
8
+ if (typeof value === "string")
9
+ return maskString(value);
10
+ if (Array.isArray(value))
11
+ return value.map(walk);
12
+ if (value && typeof value === "object" && Object.prototype.toString.call(value) !== "[object RegExp]") {
13
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, walk(v)]));
14
+ }
15
+ return value;
16
+ };
17
+ return values.length ? walk : (value) => value;
18
+ }
19
+ //# sourceMappingURL=mask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,UAAU,UAAU,CAAC,OAA+B;IACxD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;SAClC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;SACrD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IAEvC,MAAM,UAAU,GAAG,CAAC,KAAa,EAAU,EAAE,CAC3C,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;IAEvE,MAAM,IAAI,GAAG,CAAC,KAAc,EAAW,EAAE;QACvC,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;QACxD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,iBAAiB,EAAE,CAAC;YACtG,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5G,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,KAAK,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,99 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { AgentError } from "./types.js";
3
+ import { toLocator } from "./targets.js";
4
+ import { generate } from "./codegen.js";
5
+ import { writeScript } from "./workspace.js";
6
+ import { RECORDER_SOURCE } from "./recorderInject.js";
7
+ const MARK = "data-trawl-rec-el";
8
+ export class RecorderStore {
9
+ deps;
10
+ recordings = new Map();
11
+ constructor(deps) {
12
+ this.deps = deps;
13
+ }
14
+ get(id) {
15
+ const found = this.recordings.get(id);
16
+ if (!found)
17
+ throw new AgentError("agent", `unknown recording: ${id}`);
18
+ return found;
19
+ }
20
+ async start(sessionId, url) {
21
+ const session = this.deps.sessions.get(sessionId);
22
+ const recording = {
23
+ id: `rec_${randomBytes(4).toString("hex")}`,
24
+ sessionId,
25
+ steps: [],
26
+ warnings: [],
27
+ startedAt: Date.now(),
28
+ };
29
+ await session.context.exposeBinding("__trawlRec", async ({ page }, raw) => {
30
+ try {
31
+ const target = await this.pickTarget(page, raw.candidates, recording);
32
+ recording.steps.push({
33
+ index: recording.steps.length,
34
+ action: raw.action,
35
+ args: [target, ...raw.args],
36
+ });
37
+ }
38
+ catch (err) {
39
+ recording.warnings.push(`dropped ${raw.action}: ${err.message}`);
40
+ }
41
+ });
42
+ await session.context.addInitScript(RECORDER_SOURCE);
43
+ this.deps.sessions.setState(sessionId, "recording");
44
+ if (url)
45
+ await session.page.goto(url);
46
+ else
47
+ await session.page.reload();
48
+ this.recordings.set(recording.id, recording);
49
+ return recording;
50
+ }
51
+ /**
52
+ * Playwright's own locator engine is the arbiter: the first candidate that
53
+ * resolves to exactly one element, and to *the* marked element, wins.
54
+ */
55
+ async pickTarget(page, candidates, recording) {
56
+ for (const candidate of candidates) {
57
+ try {
58
+ const locator = toLocator(page, candidate);
59
+ if ((await locator.count()) !== 1)
60
+ continue;
61
+ const handle = (await locator.elementHandle({ timeout: 1000 }));
62
+ if (!handle)
63
+ continue;
64
+ const isMarked = await handle.evaluate((el, mark) => el.hasAttribute(mark), MARK);
65
+ await handle.dispose();
66
+ if (isMarked)
67
+ return candidate;
68
+ }
69
+ catch {
70
+ // try the next candidate
71
+ }
72
+ }
73
+ const fallback = candidates.at(-1);
74
+ if (!fallback)
75
+ throw new AgentError("script", "no candidate target");
76
+ recording.warnings.push(`no verified target; fell back to ${JSON.stringify(fallback)}`);
77
+ return fallback;
78
+ }
79
+ async stop(id, options) {
80
+ const recording = this.get(id);
81
+ const session = this.deps.sessions.get(recording.sessionId);
82
+ await session.page
83
+ .evaluate(() => window.__trawlRecorderControl?.("flush"))
84
+ .catch(() => { });
85
+ await session.page.waitForTimeout(150);
86
+ this.recordings.delete(id);
87
+ this.deps.sessions.setState(recording.sessionId, "idle");
88
+ const code = generate(recording.steps, { header: "recorded by trawl-devices-agent" });
89
+ if (options.saveAs)
90
+ await writeScript(this.deps.workspace, options.saveAs, code);
91
+ return {
92
+ steps: recording.steps,
93
+ code,
94
+ warnings: recording.warnings,
95
+ ...(options.saveAs ? { scriptPath: options.saveAs } : {}),
96
+ };
97
+ }
98
+ }
99
+ //# sourceMappingURL=recorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recorder.js","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"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"}