libretto 0.2.0 → 0.2.2
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/README.md +128 -126
- package/dist/cli/cli.js +2 -0
- package/dist/cli/commands/browser.js +4 -1
- package/dist/cli/commands/execution.js +21 -6
- package/dist/cli/commands/logs.js +36 -8
- package/dist/cli/commands/snapshot.js +14 -7
- package/dist/cli/core/browser.js +89 -253
- package/dist/cli/core/session-telemetry.js +491 -0
- package/dist/cli/core/telemetry.js +18 -6
- package/dist/cli/workers/run-integration-runtime.js +19 -1
- package/dist/index.cjs +2 -6
- package/dist/index.d.cts +2 -5
- package/dist/index.d.ts +2 -5
- package/dist/index.js +2 -5
- package/dist/runtime/download/download.d.cts +2 -2
- package/dist/runtime/download/download.d.ts +2 -2
- package/dist/runtime/extract/extract.cjs +2 -1
- package/dist/runtime/extract/extract.d.cts +5 -5
- package/dist/runtime/extract/extract.d.ts +5 -5
- package/dist/runtime/extract/extract.js +2 -1
- package/dist/runtime/network/network.d.cts +6 -6
- package/dist/runtime/network/network.d.ts +6 -6
- package/dist/runtime/recovery/agent.cjs +12 -7
- package/dist/runtime/recovery/agent.d.cts +2 -2
- package/dist/runtime/recovery/agent.d.ts +2 -2
- package/dist/runtime/recovery/agent.js +12 -7
- package/dist/runtime/recovery/errors.cjs +8 -6
- package/dist/runtime/recovery/errors.d.cts +2 -2
- package/dist/runtime/recovery/errors.d.ts +2 -2
- package/dist/runtime/recovery/errors.js +8 -6
- package/dist/runtime/recovery/recovery.cjs +5 -3
- package/dist/runtime/recovery/recovery.d.cts +2 -2
- package/dist/runtime/recovery/recovery.d.ts +2 -2
- package/dist/runtime/recovery/recovery.js +5 -3
- package/dist/shared/instrumentation/instrument.d.cts +2 -2
- package/dist/shared/instrumentation/instrument.d.ts +2 -2
- package/dist/shared/llm/types.d.cts +5 -5
- package/dist/shared/llm/types.d.ts +5 -5
- package/dist/shared/logger/index.cjs +2 -0
- package/dist/shared/logger/index.d.cts +1 -1
- package/dist/shared/logger/index.d.ts +1 -1
- package/dist/shared/logger/index.js +2 -1
- package/dist/shared/logger/logger.cjs +15 -2
- package/dist/shared/logger/logger.d.cts +13 -1
- package/dist/shared/logger/logger.d.ts +13 -1
- package/dist/shared/logger/logger.js +13 -1
- package/dist/shared/state/session-state.d.cts +2 -2
- package/dist/shared/state/session-state.d.ts +2 -2
- package/package.json +15 -11
- package/scripts/postinstall.mjs +48 -0
- package/skill/SKILL.md +438 -0
- package/skill/code-generation-rules.md +190 -0
- package/skill/integration-approach-selection.md +174 -0
- package/dist/runtime/step/index.cjs +0 -31
- package/dist/runtime/step/index.d.cts +0 -7
- package/dist/runtime/step/index.d.ts +0 -7
- package/dist/runtime/step/index.js +0 -6
- package/dist/runtime/step/runner.cjs +0 -208
- package/dist/runtime/step/runner.d.cts +0 -16
- package/dist/runtime/step/runner.d.ts +0 -16
- package/dist/runtime/step/runner.js +0 -187
- package/dist/runtime/step/step.cjs +0 -67
- package/dist/runtime/step/step.d.cts +0 -23
- package/dist/runtime/step/step.d.ts +0 -23
- package/dist/runtime/step/step.js +0 -43
- package/dist/runtime/step/types.cjs +0 -16
- package/dist/runtime/step/types.d.cts +0 -72
- package/dist/runtime/step/types.d.ts +0 -72
- package/dist/runtime/step/types.js +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Integration Approach Selection Guide
|
|
2
|
+
|
|
3
|
+
**Purpose:** You are connected to a live Chrome session on a target website. Your job is to probe the site for bot detection measures, assess its security posture, and determine the best integration strategy for data extraction. All strategies use Playwright for browser control — the question is what to **prioritize** for data capture: in-browser fetch calls, passive network interception, or DOM extraction.
|
|
4
|
+
|
|
5
|
+
After completing the probes below, produce a **Site Assessment Summary** (see the output format at the end of this document).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Probing the Site
|
|
10
|
+
|
|
11
|
+
Run these probes to build a picture of the site's detection posture. The examples below are starting points — use your judgment to investigate further based on what you find. Sites may use detection methods not listed here.
|
|
12
|
+
|
|
13
|
+
### Probe 1: Bot Protection Services & Security Signals
|
|
14
|
+
|
|
15
|
+
Look for signs that the site uses bot protection — either a third-party service or custom detection. There is no complete list of indicators; these are common examples.
|
|
16
|
+
|
|
17
|
+
**Cookies to look for (examples, not exhaustive):**
|
|
18
|
+
|
|
19
|
+
| Cookie Pattern | Associated Service |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `_abck` | Akamai Bot Manager |
|
|
22
|
+
| `_px*` | PerimeterX (HUMAN) |
|
|
23
|
+
| `datadome` | DataDome |
|
|
24
|
+
| `cf_clearance` | Cloudflare |
|
|
25
|
+
| `_imp_apg_r_*` | Shape Security (F5) |
|
|
26
|
+
| `x-kpsdk-*` | Kasada |
|
|
27
|
+
|
|
28
|
+
But don't just check this list. Examine **all** cookies on the page — look for any cookies with obfuscated names, telemetry-related prefixes, or values that look like fingerprint hashes or encrypted tokens. Unknown security cookies are still security cookies.
|
|
29
|
+
|
|
30
|
+
**Global variables to check (examples):**
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// Known telemetry globals — but probe broadly, not just these
|
|
34
|
+
window._pxAppId // PerimeterX
|
|
35
|
+
window.bmak // Akamai
|
|
36
|
+
window.ddjskey // DataDome
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Also examine the page's scripts: look at the first `<script>` tags in the document source, check what external domains scripts load from (e.g., `*.akamaized.net`, `*.perimeterx.net`, `*.datadome.co`, `*.kasada.io`). Bot protection scripts are typically injected before any application code.
|
|
40
|
+
|
|
41
|
+
**Challenge pages:**
|
|
42
|
+
|
|
43
|
+
Check if the page is showing a challenge or interstitial instead of real content — "Checking your browser...", CAPTCHA iframes, blank pages with only a spinner. These indicate active bot protection that has already been triggered.
|
|
44
|
+
|
|
45
|
+
**General guidance:** The goal is to determine whether the site has bot protection and roughly how aggressive it is. Don't limit yourself to known signatures — look at the overall page behavior, unusual scripts, and anything that seems like security telemetry.
|
|
46
|
+
|
|
47
|
+
### Probe 2: Fetch / XHR Interception
|
|
48
|
+
|
|
49
|
+
Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`. If it has, making your own fetch calls from `page.evaluate()` is risky because the site can inspect call stacks and detect calls that don't originate from its own code.
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
// Check if fetch has been wrapped
|
|
53
|
+
window.fetch.toString()
|
|
54
|
+
// Native: "function fetch() { [native code] }"
|
|
55
|
+
// Patched: shows actual JavaScript source
|
|
56
|
+
|
|
57
|
+
// Check XMLHttpRequest
|
|
58
|
+
XMLHttpRequest.prototype.open.toString()
|
|
59
|
+
|
|
60
|
+
// Check property descriptors for tampering
|
|
61
|
+
Object.getOwnPropertyDescriptor(window, 'fetch')
|
|
62
|
+
// Normal: { value: ƒ, writable: true, enumerable: true, configurable: true }
|
|
63
|
+
|
|
64
|
+
// Proxy-based wrapping is harder to detect — native fetch has no prototype
|
|
65
|
+
window.fetch.hasOwnProperty('prototype') // true may indicate a Proxy wrapper
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Important:** Some sites use `Proxy` to wrap fetch, which makes `toString()` still return `"[native code]"`. The prototype check is a heuristic, not definitive. If you see any sign of fetch interception, treat it as patched.
|
|
69
|
+
|
|
70
|
+
### Probe 3: Behavioral Monitoring
|
|
71
|
+
|
|
72
|
+
Look for signs that the site collects behavioral telemetry (mouse movements, keystrokes, scroll patterns). Heavy monitoring means you should use natural, human-like interaction patterns when driving the UI.
|
|
73
|
+
|
|
74
|
+
Things to check:
|
|
75
|
+
- Unusually large numbers of event listeners on `document` or `body` for `mousemove`, `keydown`, `scroll`, `touchstart`, `click`
|
|
76
|
+
- Known telemetry collection scripts
|
|
77
|
+
- `MutationObserver` instances watching the DOM for injected elements
|
|
78
|
+
- `requestAnimationFrame` loops that aren't tied to visible animations
|
|
79
|
+
|
|
80
|
+
If you're in a DevTools context, `getEventListeners(document)` is the quickest way to assess this. Otherwise, use heuristics — heavy behavioral monitoring usually correlates with enterprise bot protection from Probe 1.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Choosing a Data Capture Strategy
|
|
85
|
+
|
|
86
|
+
Every integration uses Playwright to control the browser. The question is what to **prioritize** for getting data out. In practice, most integrations use a mix — you'll always need some Playwright interaction for navigation, login flows, cookie consent, etc. The strategies below describe what to lean on for the core data extraction.
|
|
87
|
+
|
|
88
|
+
### Strategy A: Prioritize `page.evaluate(fetch(...))`
|
|
89
|
+
|
|
90
|
+
Make fetch calls directly from within the browser's JavaScript context. The requests share the browser's TLS fingerprint, cookies, and origin — they look identical to requests the site's own JS would make.
|
|
91
|
+
|
|
92
|
+
**When to prioritize this:**
|
|
93
|
+
- No enterprise bot protection detected
|
|
94
|
+
- `fetch` is NOT monkey-patched
|
|
95
|
+
- You've identified API endpoints that return the data you need
|
|
96
|
+
- You need data that requires many API calls (deep pagination, bulk queries) where driving the UI would be slow
|
|
97
|
+
|
|
98
|
+
**Why:** Maximum control and efficiency. You call exactly the endpoints you want with the parameters you want, skip UI rendering, and get structured JSON back. On sites without aggressive detection, this is the fastest and cleanest approach.
|
|
99
|
+
|
|
100
|
+
**Risk:** If the site monitors fetch call stacks (Layer 4 detection), your calls will be flagged because they don't originate from the site's bundled code. This is uncommon but exists on high-security sites.
|
|
101
|
+
|
|
102
|
+
**You'll still use Playwright for:** Initial navigation, login/auth flows, cookie consent, and any UI interactions needed to establish session state before making fetch calls.
|
|
103
|
+
|
|
104
|
+
### Strategy B: Prioritize `page.on('response', ...)` (Passive Interception)
|
|
105
|
+
|
|
106
|
+
Listen to network responses that the browser naturally makes as you navigate. You don't make any extra requests — you capture data flowing through the site's own API calls.
|
|
107
|
+
|
|
108
|
+
**When to prioritize this:**
|
|
109
|
+
- Enterprise bot protection is detected
|
|
110
|
+
- `fetch` IS monkey-patched
|
|
111
|
+
- The site's normal UI flow triggers API calls that return the data you need
|
|
112
|
+
- You want to minimize detection risk as much as possible
|
|
113
|
+
|
|
114
|
+
**Why:** Zero additional network risk. The requests that happen are the ones the site's own code triggers. You're just listening. No anomalous call stacks, no unexpected request patterns, no extra fetch calls for monitoring to flag.
|
|
115
|
+
|
|
116
|
+
**Trade-off:** You only get data the page naturally loads. If you need page 50 of results, you have to click "next" 49 times via Playwright. You must set up listeners before the navigation that triggers the requests.
|
|
117
|
+
|
|
118
|
+
**You'll still use Playwright for:** All navigation and interaction to trigger the data-bearing API calls, plus any data that isn't available via intercepted responses (DOM-only content).
|
|
119
|
+
|
|
120
|
+
### Strategy C: Prioritize Playwright DOM Extraction
|
|
121
|
+
|
|
122
|
+
Extract data directly from the rendered page using selectors and `page.evaluate()` to read DOM content.
|
|
123
|
+
|
|
124
|
+
**When to prioritize this:**
|
|
125
|
+
- Data is server-rendered (no JSON API calls observed)
|
|
126
|
+
- The site doesn't expose the data you need via any API
|
|
127
|
+
- You need visual/layout information that only exists in the DOM
|
|
128
|
+
- As a fallback when Strategies A and B can't get specific pieces of data
|
|
129
|
+
|
|
130
|
+
**Why:** Works regardless of the site's API architecture. If the data is visible on the page, you can extract it.
|
|
131
|
+
|
|
132
|
+
**Trade-off:** Slower, fragile against DOM changes, and you only get data the UI renders (which may be less than what API responses contain).
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Decision Summary
|
|
137
|
+
|
|
138
|
+
| Site Profile | Primary Strategy | Supplement With |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| No bot protection, fetch not patched | **A** (`page.evaluate(fetch)`) | Playwright for navigation/auth |
|
|
141
|
+
| No bot protection, fetch IS patched | **B** (`page.onResponse`) | Playwright for navigation; DOM extraction as fallback |
|
|
142
|
+
| Bot protection detected, fetch not patched | **B** (`page.onResponse`) | Playwright for all navigation; cautious use of `page.evaluate(fetch)` only if needed |
|
|
143
|
+
| Bot protection detected, fetch IS patched | **B** (`page.onResponse`) | Playwright for all navigation; DOM extraction as fallback |
|
|
144
|
+
| Server-rendered content (no API calls) | **C** (DOM extraction) | Playwright for all interaction |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Output: Site Assessment Summary
|
|
149
|
+
|
|
150
|
+
After running the probes, produce a summary in this format. **Do NOT include a final strategy recommendation.** The security assessment determines what's *safe to use*, not what will *work*. Present this to the user for input, then use the safe approaches as you build the integration — adapting if specific endpoints don't work as expected (see "Handling Approach Mismatches" in SKILL.md).
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
## Site Assessment: [site URL]
|
|
154
|
+
|
|
155
|
+
### Bot Detection Profile
|
|
156
|
+
- **Enterprise bot protection:** [None detected / Detected — describe what you found (service name if identifiable, cookies, scripts, telemetry globals)]
|
|
157
|
+
- **Fetch/XHR interception:** [Native (not patched) / Patched — describe what you found]
|
|
158
|
+
- **Behavioral monitoring:** [None detected / Light / Heavy — describe indicators]
|
|
159
|
+
- **Challenge pages:** [None / Present — describe type (CAPTCHA, interstitial, etc.)]
|
|
160
|
+
- **Overall security posture:** [None / Low / Moderate / High / Very High]
|
|
161
|
+
|
|
162
|
+
### API Surface
|
|
163
|
+
- **API calls observed:** [List key endpoints discovered, or "None — content appears server-rendered"]
|
|
164
|
+
- **Data format:** [JSON / GraphQL / HTML fragments / Other — note if any responses use proprietary/binary formats]
|
|
165
|
+
- **Pagination:** [Describe how pagination works if applicable]
|
|
166
|
+
|
|
167
|
+
### Safe Approaches
|
|
168
|
+
- **`page.evaluate(fetch(...))`:** [Safe / Unsafe — brief rationale based on fetch patching, bot detection, etc.]
|
|
169
|
+
- **`page.on('response', ...)`:** [Viable / Not viable — note if response formats are parseable or proprietary]
|
|
170
|
+
- **DOM extraction:** [Always available as fallback]
|
|
171
|
+
- **Interaction notes:** [any behavioral precautions — natural mouse movements, typing delays, etc.]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Important:** This assessment tells you which tools are in your toolbox. Present it to the user, get their input, then start building the integration using the safe approaches.
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
var step_exports = {};
|
|
20
|
-
__export(step_exports, {
|
|
21
|
-
createRunner: () => import_runner.createRunner,
|
|
22
|
-
step: () => import_step.step
|
|
23
|
-
});
|
|
24
|
-
module.exports = __toCommonJS(step_exports);
|
|
25
|
-
var import_step = require("./step.js");
|
|
26
|
-
var import_runner = require("./runner.js");
|
|
27
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
28
|
-
0 && (module.exports = {
|
|
29
|
-
createRunner,
|
|
30
|
-
step
|
|
31
|
-
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { step } from './step.cjs';
|
|
2
|
-
export { Runner, createRunner } from './runner.cjs';
|
|
3
|
-
export { DebugBundle, RecoveryHandler, RunnerConfig, Step, StepContext, StepHandler, StepHistoryEntry, StepOptions } from './types.cjs';
|
|
4
|
-
import 'playwright';
|
|
5
|
-
import '../../shared/logger/logger.cjs';
|
|
6
|
-
import '../../shared/llm/types.cjs';
|
|
7
|
-
import 'zod';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { step } from './step.js';
|
|
2
|
-
export { Runner, createRunner } from './runner.js';
|
|
3
|
-
export { DebugBundle, RecoveryHandler, RunnerConfig, Step, StepContext, StepHandler, StepHistoryEntry, StepOptions } from './types.js';
|
|
4
|
-
import 'playwright';
|
|
5
|
-
import '../../shared/logger/logger.js';
|
|
6
|
-
import '../../shared/llm/types.js';
|
|
7
|
-
import 'zod';
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
var runner_exports = {};
|
|
20
|
-
__export(runner_exports, {
|
|
21
|
-
createRunner: () => createRunner
|
|
22
|
-
});
|
|
23
|
-
module.exports = __toCommonJS(runner_exports);
|
|
24
|
-
var import_node_fs = require("node:fs");
|
|
25
|
-
var import_node_path = require("node:path");
|
|
26
|
-
var import_logger = require("../../shared/logger/logger.js");
|
|
27
|
-
var import_sinks = require("../../shared/logger/sinks.js");
|
|
28
|
-
var import_config = require("../../shared/config/config.js");
|
|
29
|
-
var import_pause = require("../../shared/debug/pause.js");
|
|
30
|
-
var import_recovery = require("../recovery/recovery.js");
|
|
31
|
-
var import_paths = require("../../shared/paths/paths.js");
|
|
32
|
-
function createRunner(config = {}) {
|
|
33
|
-
const {
|
|
34
|
-
llmClient,
|
|
35
|
-
dryRun: dryRunOption,
|
|
36
|
-
debug: debugOption,
|
|
37
|
-
sessionName = "libretto",
|
|
38
|
-
logDir: configuredLogDir
|
|
39
|
-
} = config;
|
|
40
|
-
const dryRun = dryRunOption ?? (0, import_config.isDryRun)();
|
|
41
|
-
const debug = debugOption ?? false;
|
|
42
|
-
const logDir = configuredLogDir ?? (0, import_paths.ensureLibrettoRunnerLogDir)(sessionName);
|
|
43
|
-
return {
|
|
44
|
-
run: async (page, steps) => {
|
|
45
|
-
(0, import_node_fs.mkdirSync)(logDir, { recursive: true });
|
|
46
|
-
const logPath = (0, import_paths.getRunnerLogPathForDir)(logDir);
|
|
47
|
-
const logger = new import_logger.Logger().withSink((0, import_sinks.createFileLogSink)({ filePath: logPath })).withSink(import_sinks.prettyConsoleSink);
|
|
48
|
-
const stepHistory = [];
|
|
49
|
-
logger.info("runner:start", {
|
|
50
|
-
totalSteps: steps.length,
|
|
51
|
-
dryRun,
|
|
52
|
-
debug,
|
|
53
|
-
logDir
|
|
54
|
-
});
|
|
55
|
-
for (const step of steps) {
|
|
56
|
-
const stepLogger = logger.withScope(`step:${step.name}`);
|
|
57
|
-
const startTime = Date.now();
|
|
58
|
-
if (dryRun && step.options.dryRun !== "execute") {
|
|
59
|
-
if (step.options.dryRun === "skip") {
|
|
60
|
-
stepLogger.info("skipped (dry-run)");
|
|
61
|
-
stepHistory.push({
|
|
62
|
-
name: step.name,
|
|
63
|
-
status: "skipped",
|
|
64
|
-
duration: 0
|
|
65
|
-
});
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (step.options.dryRun === "simulate" && step.options.simulate) {
|
|
69
|
-
stepLogger.info("simulating (dry-run)");
|
|
70
|
-
try {
|
|
71
|
-
await step.options.simulate({ logger: stepLogger });
|
|
72
|
-
} catch (simError) {
|
|
73
|
-
stepLogger.warn("simulate failed", { error: simError });
|
|
74
|
-
}
|
|
75
|
-
stepHistory.push({
|
|
76
|
-
name: step.name,
|
|
77
|
-
status: "simulated",
|
|
78
|
-
duration: Date.now() - startTime
|
|
79
|
-
});
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
stepLogger.info("skipped (dry-run, no simulate fn)");
|
|
83
|
-
stepHistory.push({
|
|
84
|
-
name: step.name,
|
|
85
|
-
status: "skipped",
|
|
86
|
-
duration: 0
|
|
87
|
-
});
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
await captureScreenshot(page, (0, import_node_path.join)(logDir, `${step.name}-start.png`), stepLogger);
|
|
91
|
-
stepLogger.info("start");
|
|
92
|
-
try {
|
|
93
|
-
await (0, import_recovery.attemptWithRecovery)(
|
|
94
|
-
page,
|
|
95
|
-
() => step.handler({ page, logger: stepLogger, config: { dryRun, debug, logDir } }),
|
|
96
|
-
stepLogger,
|
|
97
|
-
llmClient
|
|
98
|
-
);
|
|
99
|
-
stepHistory.push({
|
|
100
|
-
name: step.name,
|
|
101
|
-
status: "completed",
|
|
102
|
-
duration: Date.now() - startTime
|
|
103
|
-
});
|
|
104
|
-
stepLogger.info("end", { status: "completed", duration: Date.now() - startTime });
|
|
105
|
-
} catch (firstError) {
|
|
106
|
-
let recovered = false;
|
|
107
|
-
const customRecovery = step.options.recovery ?? {};
|
|
108
|
-
for (const [recoveryName, recoveryHandler] of Object.entries(customRecovery)) {
|
|
109
|
-
try {
|
|
110
|
-
stepLogger.info(`trying custom recovery: ${recoveryName}`);
|
|
111
|
-
await recoveryHandler({ page, logger: stepLogger });
|
|
112
|
-
await step.handler({ page, logger: stepLogger, config: { dryRun, debug, logDir } });
|
|
113
|
-
recovered = true;
|
|
114
|
-
stepHistory.push({
|
|
115
|
-
name: step.name,
|
|
116
|
-
status: "completed",
|
|
117
|
-
duration: Date.now() - startTime
|
|
118
|
-
});
|
|
119
|
-
stepLogger.info("end", {
|
|
120
|
-
status: "completed",
|
|
121
|
-
recoveredBy: recoveryName,
|
|
122
|
-
duration: Date.now() - startTime
|
|
123
|
-
});
|
|
124
|
-
break;
|
|
125
|
-
} catch {
|
|
126
|
-
stepLogger.warn(`custom recovery "${recoveryName}" failed`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (!recovered) {
|
|
130
|
-
stepHistory.push({
|
|
131
|
-
name: step.name,
|
|
132
|
-
status: "failed",
|
|
133
|
-
duration: Date.now() - startTime
|
|
134
|
-
});
|
|
135
|
-
const bundle = await generateDebugBundle(
|
|
136
|
-
page,
|
|
137
|
-
step.name,
|
|
138
|
-
firstError,
|
|
139
|
-
logDir,
|
|
140
|
-
logPath,
|
|
141
|
-
stepHistory,
|
|
142
|
-
stepLogger
|
|
143
|
-
);
|
|
144
|
-
stepLogger.info("step:debug-bundle", { path: bundle.bundlePath });
|
|
145
|
-
if (debug) {
|
|
146
|
-
await (0, import_pause.debugPause)({
|
|
147
|
-
page,
|
|
148
|
-
session: sessionName
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
throw firstError;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
await captureScreenshot(page, (0, import_node_path.join)(logDir, `${step.name}-end.png`), stepLogger);
|
|
155
|
-
}
|
|
156
|
-
logger.info("runner:complete", {
|
|
157
|
-
totalSteps: steps.length,
|
|
158
|
-
completed: stepHistory.filter((s) => s.status === "completed").length,
|
|
159
|
-
skipped: stepHistory.filter((s) => s.status === "skipped").length,
|
|
160
|
-
simulated: stepHistory.filter((s) => s.status === "simulated").length
|
|
161
|
-
});
|
|
162
|
-
await logger.close();
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
async function captureScreenshot(page, filePath, logger) {
|
|
167
|
-
try {
|
|
168
|
-
const buffer = await page.screenshot({ fullPage: false, timeout: 5e3 });
|
|
169
|
-
(0, import_node_fs.writeFileSync)(filePath, buffer);
|
|
170
|
-
} catch (err) {
|
|
171
|
-
logger.warn("Failed to capture screenshot", {
|
|
172
|
-
path: filePath,
|
|
173
|
-
error: err instanceof Error ? err.message : String(err)
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
async function generateDebugBundle(page, stepName, error, logDir, logPath, stepHistory, logger) {
|
|
178
|
-
const screenshotPath = (0, import_node_path.join)(logDir, `${stepName}-error.png`);
|
|
179
|
-
const domPath = (0, import_node_path.join)(logDir, `${stepName}-error.html`);
|
|
180
|
-
const bundlePath = (0, import_node_path.join)(logDir, `${stepName}-debug-bundle.json`);
|
|
181
|
-
await captureScreenshot(page, screenshotPath, logger);
|
|
182
|
-
try {
|
|
183
|
-
const html = await page.content();
|
|
184
|
-
(0, import_node_fs.writeFileSync)(domPath, html);
|
|
185
|
-
} catch (domErr) {
|
|
186
|
-
logger.warn("Failed to capture DOM for debug bundle", {
|
|
187
|
-
error: domErr instanceof Error ? domErr.message : String(domErr)
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
const pageUrl = page.isClosed() ? "" : page.url();
|
|
191
|
-
const bundle = {
|
|
192
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
-
step: stepName,
|
|
194
|
-
error: error instanceof Error ? error.message : String(error),
|
|
195
|
-
stacktrace: error instanceof Error ? error.stack ?? "" : "",
|
|
196
|
-
screenshotPath,
|
|
197
|
-
domPath,
|
|
198
|
-
logPath,
|
|
199
|
-
stepHistory,
|
|
200
|
-
pageUrl
|
|
201
|
-
};
|
|
202
|
-
(0, import_node_fs.writeFileSync)(bundlePath, JSON.stringify(bundle, null, 2));
|
|
203
|
-
return { bundlePath };
|
|
204
|
-
}
|
|
205
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
206
|
-
0 && (module.exports = {
|
|
207
|
-
createRunner
|
|
208
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Page } from 'playwright';
|
|
2
|
-
import { Step, RunnerConfig } from './types.cjs';
|
|
3
|
-
import '../../shared/logger/logger.cjs';
|
|
4
|
-
import '../../shared/llm/types.cjs';
|
|
5
|
-
import 'zod';
|
|
6
|
-
|
|
7
|
-
type Runner = {
|
|
8
|
-
run: (page: Page, steps: Step[]) => Promise<void>;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Creates a step runner that executes a sequence of steps with logging,
|
|
12
|
-
* recovery, dry-run support, and debug bundle generation.
|
|
13
|
-
*/
|
|
14
|
-
declare function createRunner(config?: RunnerConfig): Runner;
|
|
15
|
-
|
|
16
|
-
export { type Runner, createRunner };
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Page } from 'playwright';
|
|
2
|
-
import { Step, RunnerConfig } from './types.js';
|
|
3
|
-
import '../../shared/logger/logger.js';
|
|
4
|
-
import '../../shared/llm/types.js';
|
|
5
|
-
import 'zod';
|
|
6
|
-
|
|
7
|
-
type Runner = {
|
|
8
|
-
run: (page: Page, steps: Step[]) => Promise<void>;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Creates a step runner that executes a sequence of steps with logging,
|
|
12
|
-
* recovery, dry-run support, and debug bundle generation.
|
|
13
|
-
*/
|
|
14
|
-
declare function createRunner(config?: RunnerConfig): Runner;
|
|
15
|
-
|
|
16
|
-
export { type Runner, createRunner };
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { Logger } from "../../shared/logger/logger.js";
|
|
4
|
-
import { createFileLogSink, prettyConsoleSink } from "../../shared/logger/sinks.js";
|
|
5
|
-
import { isDryRun } from "../../shared/config/config.js";
|
|
6
|
-
import { debugPause } from "../../shared/debug/pause.js";
|
|
7
|
-
import { attemptWithRecovery } from "../recovery/recovery.js";
|
|
8
|
-
import {
|
|
9
|
-
ensureLibrettoRunnerLogDir,
|
|
10
|
-
getRunnerLogPathForDir
|
|
11
|
-
} from "../../shared/paths/paths.js";
|
|
12
|
-
function createRunner(config = {}) {
|
|
13
|
-
const {
|
|
14
|
-
llmClient,
|
|
15
|
-
dryRun: dryRunOption,
|
|
16
|
-
debug: debugOption,
|
|
17
|
-
sessionName = "libretto",
|
|
18
|
-
logDir: configuredLogDir
|
|
19
|
-
} = config;
|
|
20
|
-
const dryRun = dryRunOption ?? isDryRun();
|
|
21
|
-
const debug = debugOption ?? false;
|
|
22
|
-
const logDir = configuredLogDir ?? ensureLibrettoRunnerLogDir(sessionName);
|
|
23
|
-
return {
|
|
24
|
-
run: async (page, steps) => {
|
|
25
|
-
mkdirSync(logDir, { recursive: true });
|
|
26
|
-
const logPath = getRunnerLogPathForDir(logDir);
|
|
27
|
-
const logger = new Logger().withSink(createFileLogSink({ filePath: logPath })).withSink(prettyConsoleSink);
|
|
28
|
-
const stepHistory = [];
|
|
29
|
-
logger.info("runner:start", {
|
|
30
|
-
totalSteps: steps.length,
|
|
31
|
-
dryRun,
|
|
32
|
-
debug,
|
|
33
|
-
logDir
|
|
34
|
-
});
|
|
35
|
-
for (const step of steps) {
|
|
36
|
-
const stepLogger = logger.withScope(`step:${step.name}`);
|
|
37
|
-
const startTime = Date.now();
|
|
38
|
-
if (dryRun && step.options.dryRun !== "execute") {
|
|
39
|
-
if (step.options.dryRun === "skip") {
|
|
40
|
-
stepLogger.info("skipped (dry-run)");
|
|
41
|
-
stepHistory.push({
|
|
42
|
-
name: step.name,
|
|
43
|
-
status: "skipped",
|
|
44
|
-
duration: 0
|
|
45
|
-
});
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (step.options.dryRun === "simulate" && step.options.simulate) {
|
|
49
|
-
stepLogger.info("simulating (dry-run)");
|
|
50
|
-
try {
|
|
51
|
-
await step.options.simulate({ logger: stepLogger });
|
|
52
|
-
} catch (simError) {
|
|
53
|
-
stepLogger.warn("simulate failed", { error: simError });
|
|
54
|
-
}
|
|
55
|
-
stepHistory.push({
|
|
56
|
-
name: step.name,
|
|
57
|
-
status: "simulated",
|
|
58
|
-
duration: Date.now() - startTime
|
|
59
|
-
});
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
stepLogger.info("skipped (dry-run, no simulate fn)");
|
|
63
|
-
stepHistory.push({
|
|
64
|
-
name: step.name,
|
|
65
|
-
status: "skipped",
|
|
66
|
-
duration: 0
|
|
67
|
-
});
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
await captureScreenshot(page, join(logDir, `${step.name}-start.png`), stepLogger);
|
|
71
|
-
stepLogger.info("start");
|
|
72
|
-
try {
|
|
73
|
-
await attemptWithRecovery(
|
|
74
|
-
page,
|
|
75
|
-
() => step.handler({ page, logger: stepLogger, config: { dryRun, debug, logDir } }),
|
|
76
|
-
stepLogger,
|
|
77
|
-
llmClient
|
|
78
|
-
);
|
|
79
|
-
stepHistory.push({
|
|
80
|
-
name: step.name,
|
|
81
|
-
status: "completed",
|
|
82
|
-
duration: Date.now() - startTime
|
|
83
|
-
});
|
|
84
|
-
stepLogger.info("end", { status: "completed", duration: Date.now() - startTime });
|
|
85
|
-
} catch (firstError) {
|
|
86
|
-
let recovered = false;
|
|
87
|
-
const customRecovery = step.options.recovery ?? {};
|
|
88
|
-
for (const [recoveryName, recoveryHandler] of Object.entries(customRecovery)) {
|
|
89
|
-
try {
|
|
90
|
-
stepLogger.info(`trying custom recovery: ${recoveryName}`);
|
|
91
|
-
await recoveryHandler({ page, logger: stepLogger });
|
|
92
|
-
await step.handler({ page, logger: stepLogger, config: { dryRun, debug, logDir } });
|
|
93
|
-
recovered = true;
|
|
94
|
-
stepHistory.push({
|
|
95
|
-
name: step.name,
|
|
96
|
-
status: "completed",
|
|
97
|
-
duration: Date.now() - startTime
|
|
98
|
-
});
|
|
99
|
-
stepLogger.info("end", {
|
|
100
|
-
status: "completed",
|
|
101
|
-
recoveredBy: recoveryName,
|
|
102
|
-
duration: Date.now() - startTime
|
|
103
|
-
});
|
|
104
|
-
break;
|
|
105
|
-
} catch {
|
|
106
|
-
stepLogger.warn(`custom recovery "${recoveryName}" failed`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (!recovered) {
|
|
110
|
-
stepHistory.push({
|
|
111
|
-
name: step.name,
|
|
112
|
-
status: "failed",
|
|
113
|
-
duration: Date.now() - startTime
|
|
114
|
-
});
|
|
115
|
-
const bundle = await generateDebugBundle(
|
|
116
|
-
page,
|
|
117
|
-
step.name,
|
|
118
|
-
firstError,
|
|
119
|
-
logDir,
|
|
120
|
-
logPath,
|
|
121
|
-
stepHistory,
|
|
122
|
-
stepLogger
|
|
123
|
-
);
|
|
124
|
-
stepLogger.info("step:debug-bundle", { path: bundle.bundlePath });
|
|
125
|
-
if (debug) {
|
|
126
|
-
await debugPause({
|
|
127
|
-
page,
|
|
128
|
-
session: sessionName
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
throw firstError;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
await captureScreenshot(page, join(logDir, `${step.name}-end.png`), stepLogger);
|
|
135
|
-
}
|
|
136
|
-
logger.info("runner:complete", {
|
|
137
|
-
totalSteps: steps.length,
|
|
138
|
-
completed: stepHistory.filter((s) => s.status === "completed").length,
|
|
139
|
-
skipped: stepHistory.filter((s) => s.status === "skipped").length,
|
|
140
|
-
simulated: stepHistory.filter((s) => s.status === "simulated").length
|
|
141
|
-
});
|
|
142
|
-
await logger.close();
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
async function captureScreenshot(page, filePath, logger) {
|
|
147
|
-
try {
|
|
148
|
-
const buffer = await page.screenshot({ fullPage: false, timeout: 5e3 });
|
|
149
|
-
writeFileSync(filePath, buffer);
|
|
150
|
-
} catch (err) {
|
|
151
|
-
logger.warn("Failed to capture screenshot", {
|
|
152
|
-
path: filePath,
|
|
153
|
-
error: err instanceof Error ? err.message : String(err)
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
async function generateDebugBundle(page, stepName, error, logDir, logPath, stepHistory, logger) {
|
|
158
|
-
const screenshotPath = join(logDir, `${stepName}-error.png`);
|
|
159
|
-
const domPath = join(logDir, `${stepName}-error.html`);
|
|
160
|
-
const bundlePath = join(logDir, `${stepName}-debug-bundle.json`);
|
|
161
|
-
await captureScreenshot(page, screenshotPath, logger);
|
|
162
|
-
try {
|
|
163
|
-
const html = await page.content();
|
|
164
|
-
writeFileSync(domPath, html);
|
|
165
|
-
} catch (domErr) {
|
|
166
|
-
logger.warn("Failed to capture DOM for debug bundle", {
|
|
167
|
-
error: domErr instanceof Error ? domErr.message : String(domErr)
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
const pageUrl = page.isClosed() ? "" : page.url();
|
|
171
|
-
const bundle = {
|
|
172
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
173
|
-
step: stepName,
|
|
174
|
-
error: error instanceof Error ? error.message : String(error),
|
|
175
|
-
stacktrace: error instanceof Error ? error.stack ?? "" : "",
|
|
176
|
-
screenshotPath,
|
|
177
|
-
domPath,
|
|
178
|
-
logPath,
|
|
179
|
-
stepHistory,
|
|
180
|
-
pageUrl
|
|
181
|
-
};
|
|
182
|
-
writeFileSync(bundlePath, JSON.stringify(bundle, null, 2));
|
|
183
|
-
return { bundlePath };
|
|
184
|
-
}
|
|
185
|
-
export {
|
|
186
|
-
createRunner
|
|
187
|
-
};
|