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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/auth.js +23 -0
- package/dist/auth.js.map +1 -0
- package/dist/codegen.js +41 -0
- package/dist/codegen.js.map +1 -0
- package/dist/control.js +106 -0
- package/dist/control.js.map +1 -0
- package/dist/devices.js +69 -0
- package/dist/devices.js.map +1 -0
- package/dist/guide.js +14 -0
- package/dist/guide.js.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolate.js +20 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/mask.js +19 -0
- package/dist/mask.js.map +1 -0
- package/dist/recorder.js +99 -0
- package/dist/recorder.js.map +1 -0
- package/dist/recorderInject.js +165 -0
- package/dist/recorderInject.js.map +1 -0
- package/dist/retention.js +21 -0
- package/dist/retention.js.map +1 -0
- package/dist/routes.js +140 -0
- package/dist/routes.js.map +1 -0
- package/dist/runner.js +373 -0
- package/dist/runner.js.map +1 -0
- package/dist/sandbox.js +80 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.js +100 -0
- package/dist/server.js.map +1 -0
- package/dist/sessions.js +85 -0
- package/dist/sessions.js.map +1 -0
- package/dist/steps.js +27 -0
- package/dist/steps.js.map +1 -0
- package/dist/targets.js +68 -0
- package/dist/targets.js.map +1 -0
- package/dist/traffic.js +116 -0
- package/dist/traffic.js.map +1 -0
- package/dist/transform.js +110 -0
- package/dist/transform.js.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace.js +50 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +50 -0
- 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
|
package/dist/auth.js.map
ADDED
|
@@ -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"}
|
package/dist/codegen.js
ADDED
|
@@ -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":"AAAA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGrD,MAAM,KAAK,GAAG,4BAA4B,CAAC;AAC3C,MAAM,KAAK,GAAG,CAAC,CAAS,EAAU,EAAE,CAClC,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC;AAE7E,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,WAAW,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC;IACnD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IAClF,IAAI,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,WAAW,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACjF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACxE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;aAC7D,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC;aAClC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACvE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;AAMzC,4FAA4F;AAC5F,MAAM,UAAU,QAAQ,CAAC,KAAmB,EAAE,UAA2B,EAAE;IACzE,MAAM,IAAI,GAAG,CAAC,CAAa,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACnF,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAEjE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,OAAO,CAAC,MAAM;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACxD,IAAI,KAAK,CAAC,MAAM;QAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7B,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC;AACtD,CAAC"}
|
package/dist/control.js
ADDED
|
@@ -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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"}
|
package/dist/devices.js
ADDED
|
@@ -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
|
package/dist/mask.js.map
ADDED
|
@@ -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"}
|
package/dist/recorder.js
ADDED
|
@@ -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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"}
|