libretto 0.4.4 → 0.5.1
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 +106 -36
- package/dist/cli/cli.js +39 -113
- package/dist/cli/commands/ai.js +1 -1
- package/dist/cli/commands/browser.js +87 -60
- package/dist/cli/commands/execution.js +201 -88
- package/dist/cli/commands/init.js +30 -8
- package/dist/cli/commands/logs.js +5 -6
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +9 -2
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +141 -33
- package/dist/cli/core/context.js +7 -18
- package/dist/cli/core/session-telemetry.js +5 -2
- package/dist/cli/core/session.js +23 -10
- package/dist/cli/core/snapshot-analyzer.js +16 -33
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +10 -2
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +26 -7
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +6 -13
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/paths/paths.js +2 -1
- package/dist/shared/paths/repo-root.d.ts +3 -0
- package/dist/shared/paths/repo-root.js +24 -0
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +7 -2
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +19 -10
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +11 -8
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +26 -17
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +130 -377
- package/skills/libretto/references/auth-profiles.md +30 -0
- package/skills/libretto/{code-generation-rules.md → references/code-generation-rules.md} +27 -42
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +29 -0
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +86 -0
- package/src/cli/commands/ai.ts +35 -0
- package/src/cli/commands/browser.ts +189 -0
- package/src/cli/commands/execution.ts +822 -0
- package/src/cli/commands/init.ts +350 -0
- package/src/cli/commands/logs.ts +128 -0
- package/src/cli/commands/shared.ts +69 -0
- package/src/cli/commands/snapshot.ts +312 -0
- package/src/cli/core/ai-config.ts +264 -0
- package/src/cli/core/api-snapshot-analyzer.ts +108 -0
- package/src/cli/core/browser.ts +976 -0
- package/src/cli/core/context.ts +127 -0
- package/src/cli/core/pause-signals.ts +35 -0
- package/src/cli/core/session-telemetry.ts +564 -0
- package/src/cli/core/session.ts +223 -0
- package/src/cli/core/snapshot-analyzer.ts +855 -0
- package/src/cli/core/snapshot-api-config.ts +231 -0
- package/src/cli/core/telemetry.ts +459 -0
- package/src/cli/framework/simple-cli.ts +1340 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/router.ts +20 -0
- package/src/cli/workers/run-integration-runtime.ts +338 -0
- package/src/cli/workers/run-integration-worker-protocol.ts +16 -0
- package/src/cli/workers/run-integration-worker.ts +72 -0
- package/src/index.ts +127 -0
- package/src/runtime/download/download.ts +104 -0
- package/src/runtime/download/index.ts +7 -0
- package/src/runtime/extract/extract.ts +102 -0
- package/src/runtime/extract/index.ts +1 -0
- package/src/runtime/network/index.ts +5 -0
- package/src/runtime/network/network.ts +119 -0
- package/{dist/runtime/recovery/agent.cjs → src/runtime/recovery/agent.ts} +114 -76
- package/src/runtime/recovery/errors.ts +155 -0
- package/src/runtime/recovery/index.ts +7 -0
- package/src/runtime/recovery/recovery.ts +53 -0
- package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +249 -124
- package/src/shared/config/config.ts +3 -0
- package/src/shared/config/index.ts +0 -0
- package/src/shared/debug/index.ts +1 -0
- package/src/shared/debug/pause.ts +91 -0
- package/src/shared/instrumentation/errors.ts +84 -0
- package/src/shared/instrumentation/index.ts +9 -0
- package/src/shared/instrumentation/instrument.ts +406 -0
- package/src/shared/llm/ai-sdk-adapter.ts +81 -0
- package/{dist/shared/llm/client.cjs → src/shared/llm/client.ts} +86 -80
- package/src/shared/llm/index.ts +3 -0
- package/src/shared/llm/types.ts +63 -0
- package/src/shared/logger/index.ts +13 -0
- package/src/shared/logger/logger.ts +358 -0
- package/src/shared/logger/sinks.ts +148 -0
- package/src/shared/paths/paths.ts +110 -0
- package/src/shared/paths/repo-root.ts +27 -0
- package/src/shared/run/api.ts +6 -0
- package/src/shared/run/browser.ts +107 -0
- package/src/shared/state/index.ts +11 -0
- package/src/shared/state/session-state.ts +77 -0
- package/src/shared/visualization/ghost-cursor.ts +213 -0
- package/src/shared/visualization/highlight.ts +149 -0
- package/src/shared/visualization/index.ts +18 -0
- package/src/shared/workflow/workflow.ts +36 -0
- package/dist/index.cjs +0 -144
- package/dist/index.d.cts +0 -21
- package/dist/runtime/download/download.cjs +0 -70
- package/dist/runtime/download/download.d.cts +0 -35
- package/dist/runtime/download/index.cjs +0 -30
- package/dist/runtime/download/index.d.cts +0 -3
- package/dist/runtime/extract/extract.cjs +0 -88
- package/dist/runtime/extract/extract.d.cts +0 -23
- package/dist/runtime/extract/index.cjs +0 -28
- package/dist/runtime/extract/index.d.cts +0 -5
- package/dist/runtime/network/index.cjs +0 -28
- package/dist/runtime/network/index.d.cts +0 -4
- package/dist/runtime/network/network.cjs +0 -91
- package/dist/runtime/network/network.d.cts +0 -28
- package/dist/runtime/recovery/agent.d.cts +0 -13
- package/dist/runtime/recovery/errors.cjs +0 -124
- package/dist/runtime/recovery/errors.d.cts +0 -31
- package/dist/runtime/recovery/index.cjs +0 -34
- package/dist/runtime/recovery/index.d.cts +0 -7
- package/dist/runtime/recovery/recovery.cjs +0 -55
- package/dist/runtime/recovery/recovery.d.cts +0 -12
- package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
- package/dist/shared/config/config.cjs +0 -44
- package/dist/shared/config/config.d.cts +0 -10
- package/dist/shared/config/index.cjs +0 -32
- package/dist/shared/config/index.d.cts +0 -1
- package/dist/shared/debug/index.cjs +0 -28
- package/dist/shared/debug/index.d.cts +0 -1
- package/dist/shared/debug/pause.cjs +0 -86
- package/dist/shared/debug/pause.d.cts +0 -12
- package/dist/shared/instrumentation/errors.cjs +0 -81
- package/dist/shared/instrumentation/errors.d.cts +0 -12
- package/dist/shared/instrumentation/index.cjs +0 -35
- package/dist/shared/instrumentation/index.d.cts +0 -6
- package/dist/shared/instrumentation/instrument.cjs +0 -206
- package/dist/shared/instrumentation/instrument.d.cts +0 -32
- package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
- package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
- package/dist/shared/llm/client.d.cts +0 -13
- package/dist/shared/llm/index.cjs +0 -31
- package/dist/shared/llm/index.d.cts +0 -5
- package/dist/shared/llm/types.cjs +0 -16
- package/dist/shared/llm/types.d.cts +0 -67
- package/dist/shared/logger/index.cjs +0 -37
- package/dist/shared/logger/index.d.cts +0 -2
- package/dist/shared/logger/logger.cjs +0 -232
- package/dist/shared/logger/logger.d.cts +0 -86
- package/dist/shared/logger/sinks.cjs +0 -160
- package/dist/shared/logger/sinks.d.cts +0 -9
- package/dist/shared/paths/paths.cjs +0 -104
- package/dist/shared/paths/paths.d.cts +0 -10
- package/dist/shared/run/api.cjs +0 -28
- package/dist/shared/run/api.d.cts +0 -2
- package/dist/shared/run/browser.cjs +0 -98
- package/dist/shared/run/browser.d.cts +0 -22
- package/dist/shared/state/index.cjs +0 -38
- package/dist/shared/state/index.d.cts +0 -2
- package/dist/shared/state/session-state.cjs +0 -92
- package/dist/shared/state/session-state.d.cts +0 -40
- package/dist/shared/visualization/ghost-cursor.cjs +0 -174
- package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
- package/dist/shared/visualization/highlight.cjs +0 -134
- package/dist/shared/visualization/highlight.d.cts +0 -22
- package/dist/shared/visualization/index.cjs +0 -45
- package/dist/shared/visualization/index.d.cts +0 -3
- package/dist/shared/workflow/workflow.cjs +0 -47
- package/dist/shared/workflow/workflow.d.cts +0 -21
- package/skills/libretto/integration-approach-selection.md +0 -174
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
These rules apply when generating production TypeScript files from interactive browser sessions. Read this file before writing any production code.
|
|
4
4
|
|
|
5
|
+
Follow the user's existing codebase conventions, abstractions, and patterns whenever possible. Do not introduce a new style unless the codebase does not already have a suitable one.
|
|
6
|
+
|
|
5
7
|
## Workflow File Structure
|
|
6
8
|
|
|
7
9
|
Generated files must export a `workflow()` instance so they can be run via `npx libretto run <file> <exportName>`. Import `workflow` and its types from `"libretto"`:
|
|
@@ -21,27 +23,27 @@ type Output = {
|
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
export const myWorkflow = workflow<Input, Output>(
|
|
24
|
-
{},
|
|
25
26
|
async (ctx, input): Promise<Output> => {
|
|
26
|
-
const { page } = ctx;
|
|
27
|
+
const { session, page, logger } = ctx;
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
logger.info("workflow-start", { session, query: input.query });
|
|
29
30
|
await page.goto("https://example.com");
|
|
30
|
-
|
|
31
|
+
await pause(session);
|
|
31
32
|
|
|
32
33
|
return { results: [] };
|
|
33
34
|
},
|
|
34
35
|
);
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
Key points:
|
|
38
39
|
|
|
39
40
|
- The named export (e.g., `myWorkflow`) is what you pass as the second arg to `npx libretto run ./file.ts myWorkflow`
|
|
40
|
-
- `
|
|
41
|
+
- `workflow(handler)` returns a branded workflow object with a `.run(ctx, input)` method. The CLI expects that contract.
|
|
42
|
+
- `ctx` provides `session`, `page`, `logger`, and `services` (generic, default `{}`)
|
|
41
43
|
- `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI
|
|
42
44
|
- If the site requires a saved login session, pass `--auth-profile <domain>` to the CLI (created via `npx libretto save <domain>`)
|
|
43
|
-
- Use `await pause()` (
|
|
44
|
-
- The browser is launched and closed automatically by the CLI
|
|
45
|
+
- Use `await pause(ctx.session)` (or `await pause(session)`) to pause the workflow for debugging. It is a no-op in production.
|
|
46
|
+
- The browser is launched and closed automatically by the CLI. Do not launch or close it in the handler.
|
|
45
47
|
|
|
46
48
|
## Passing Application Dependencies via Services
|
|
47
49
|
|
|
@@ -55,7 +57,6 @@ import { type Transaction } from "./db";
|
|
|
55
57
|
type MyServices = { tx?: Transaction };
|
|
56
58
|
|
|
57
59
|
export const myWorkflow = workflow<Input, Output, MyServices>(
|
|
58
|
-
{},
|
|
59
60
|
async (ctx, input) => {
|
|
60
61
|
if (ctx.services.tx) {
|
|
61
62
|
await ctx.services.tx.insert(/* ... */);
|
|
@@ -71,7 +72,7 @@ In production, the caller passes services when invoking `.run()`:
|
|
|
71
72
|
|
|
72
73
|
```typescript
|
|
73
74
|
await myWorkflow.run(
|
|
74
|
-
{ page, logger, services: { tx } },
|
|
75
|
+
{ session: "debug-flow", page, logger, services: { tx } },
|
|
75
76
|
input,
|
|
76
77
|
);
|
|
77
78
|
```
|
|
@@ -85,22 +86,6 @@ Generated code must use Playwright locator APIs for all DOM interactions. Do not
|
|
|
85
86
|
|
|
86
87
|
During the interactive `exec` phase, `page.evaluate` is fine for quick prototyping. In generated production code, translate those patterns into Playwright locators.
|
|
87
88
|
|
|
88
|
-
### Translation Table
|
|
89
|
-
|
|
90
|
-
| Operation | Interactive (`exec`) | Production file |
|
|
91
|
-
| ---------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
92
|
-
| Click | `page.evaluate(() => document.getElementById('x').click())` | `page.locator('#x').click()` |
|
|
93
|
-
| Check state | `page.evaluate(() => el.checked)` | `page.locator('#x').isChecked()` |
|
|
94
|
-
| Read text | `page.evaluate(() => el.textContent)` | `page.locator('#x').textContent()` |
|
|
95
|
-
| Read all text | `querySelectorAll(...).map(e => e.textContent)` | `page.locator('.items').allTextContents()` |
|
|
96
|
-
| Element position | `el.getBoundingClientRect()` | `page.locator('#x').boundingBox()` |
|
|
97
|
-
| Inline styles | `el.style.top` | `page.locator('#x').getAttribute('style')` |
|
|
98
|
-
| Count elements | `querySelectorAll(...).length` | `page.locator('.items').count()` |
|
|
99
|
-
| Select dropdown | `selectEl.value = '...'` | `page.locator('select').selectOption('...')` |
|
|
100
|
-
| Iterate elements | `querySelectorAll(...).forEach(...)` | `const items = await locator.all(); for (const item of items) { ... }` |
|
|
101
|
-
| Scoped query | `parent.querySelector('.child')` | `parentLocator.locator('.child').textContent()` |
|
|
102
|
-
| Batch extraction | `querySelectorAll('.item').forEach(e => { ... })` | `for (const item of await locator.all()) { const text = await item.locator('.text').textContent(); ... }` |
|
|
103
|
-
|
|
104
89
|
### Anti-Patterns
|
|
105
90
|
|
|
106
91
|
These patterns come up frequently during interactive sessions and should not carry over into production code:
|
|
@@ -116,10 +101,10 @@ const data = await page.evaluate(`(() => {
|
|
|
116
101
|
})()`);
|
|
117
102
|
|
|
118
103
|
// DO — Playwright locators with a loop
|
|
119
|
-
const posts = await page.locator(
|
|
104
|
+
const posts = await page.locator(".post").all();
|
|
120
105
|
for (const post of posts) {
|
|
121
|
-
const name = await post.locator(
|
|
122
|
-
const content = await post.locator(
|
|
106
|
+
const name = await post.locator(".name").textContent();
|
|
107
|
+
const content = await post.locator(".content").textContent();
|
|
123
108
|
}
|
|
124
109
|
```
|
|
125
110
|
|
|
@@ -128,13 +113,13 @@ for (const post of posts) {
|
|
|
128
113
|
const count = await el.evaluate(`(el) => el.querySelectorAll('.item').length`);
|
|
129
114
|
|
|
130
115
|
// DO
|
|
131
|
-
const count = await el.locator(
|
|
116
|
+
const count = await el.locator(".item").count();
|
|
132
117
|
```
|
|
133
118
|
|
|
134
119
|
```typescript
|
|
135
120
|
// DON'T — evaluate to read scoped text
|
|
136
121
|
const text = await post.evaluate(
|
|
137
|
-
`(el) => el.querySelector('[data-view-name="foo"]')?.textContent
|
|
122
|
+
`(el) => el.querySelector('[data-view-name="foo"]')?.textContent`,
|
|
138
123
|
);
|
|
139
124
|
|
|
140
125
|
// DO
|
|
@@ -145,9 +130,9 @@ const text = await post.locator('[data-view-name="foo"]').textContent();
|
|
|
145
130
|
|
|
146
131
|
Use `page.evaluate()` only for operations that have no Playwright locator equivalent:
|
|
147
132
|
|
|
148
|
-
1.
|
|
149
|
-
2.
|
|
150
|
-
3.
|
|
133
|
+
1. Browser-native APIs: `getComputedStyle()`, `window.*` globals, `document.cookie`, scroll position
|
|
134
|
+
2. In-browser `fetch()` calls: making HTTP requests from the browser context
|
|
135
|
+
3. Parsing operations: using `DOMParser` to parse HTML/XML strings inside the browser
|
|
151
136
|
|
|
152
137
|
A quick test: if the evaluate body contains `querySelector`, `querySelectorAll`, `textContent`, `click()`, `getAttribute()`, or iterates DOM elements, it should be rewritten with Playwright locators.
|
|
153
138
|
|
|
@@ -198,23 +183,23 @@ class ApiClient {
|
|
|
198
183
|
}
|
|
199
184
|
```
|
|
200
185
|
|
|
201
|
-
One method per endpoint. No try
|
|
186
|
+
One method per endpoint. No try/catch in API methods. Let errors propagate to the orchestrator. Parse XML/HTML inside `page.evaluate()` with `DOMParser`. Use string expressions for `page.evaluate()` to avoid DOM type errors.
|
|
202
187
|
|
|
203
188
|
## Comments
|
|
204
189
|
|
|
205
|
-
Add comments throughout generated code to explain what each logical block is doing. Comments should describe
|
|
190
|
+
Add comments throughout generated code to explain what each logical block is doing. Comments should describe intent, not restate the code. Group related actions under a single comment rather than commenting every line.
|
|
206
191
|
|
|
207
192
|
```typescript
|
|
208
193
|
// Log in with credentials
|
|
209
|
-
await page.locator(
|
|
210
|
-
await page.locator(
|
|
211
|
-
await page.locator(
|
|
194
|
+
await page.locator("#username").fill(user);
|
|
195
|
+
await page.locator("#password").fill(pass);
|
|
196
|
+
await page.locator("#login").click();
|
|
212
197
|
|
|
213
198
|
// Extract author and content from each feed post
|
|
214
|
-
const posts = await page.locator(
|
|
199
|
+
const posts = await page.locator(".post").all();
|
|
215
200
|
for (const post of posts) {
|
|
216
|
-
const name = await post.locator(
|
|
217
|
-
const content = await post.locator(
|
|
201
|
+
const name = await post.locator(".name").textContent();
|
|
202
|
+
const content = await post.locator(".content").textContent();
|
|
218
203
|
}
|
|
219
204
|
```
|
|
220
205
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Configuration File Reference
|
|
2
|
+
|
|
3
|
+
Use this reference when you need to inspect or change the workspace configuration that powers `snapshot` analysis and default viewport behavior.
|
|
4
|
+
|
|
5
|
+
## When to Use This
|
|
6
|
+
|
|
7
|
+
- You want to confirm which AI model `snapshot` will use.
|
|
8
|
+
- You want to understand where Libretto stores workspace-level settings.
|
|
9
|
+
- You want a persistent default viewport for `open` or `run`.
|
|
10
|
+
|
|
11
|
+
## File Location
|
|
12
|
+
|
|
13
|
+
Libretto reads workspace config from `.libretto/config.json`.
|
|
14
|
+
|
|
15
|
+
- The file is usually created or updated by `npx libretto ai configure ...`.
|
|
16
|
+
- API credentials still come from your shell environment or `.env`. The config file stores the selected model, not the secret itself.
|
|
17
|
+
- For first-time setup instructions, follow the main `SKILL.md` flow instead of expanding this reference.
|
|
18
|
+
|
|
19
|
+
## Supported Settings
|
|
20
|
+
|
|
21
|
+
- `ai.model` selects the configured analysis model for `snapshot`.
|
|
22
|
+
- `viewport` is an optional top-level setting used by `open` and `run` when you do not pass `--viewport`.
|
|
23
|
+
- Viewport precedence is: CLI `--viewport`, then `.libretto/config.json`, then the default `1366x768`.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"version": 1,
|
|
30
|
+
"ai": {
|
|
31
|
+
"model": "openai/gpt-5.4",
|
|
32
|
+
"updatedAt": "2026-01-01T00:00:00.000Z"
|
|
33
|
+
},
|
|
34
|
+
"viewport": {
|
|
35
|
+
"width": 1280,
|
|
36
|
+
"height": 800
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Common Commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx libretto init
|
|
45
|
+
npx libretto ai configure openai
|
|
46
|
+
npx libretto open https://app.example.com --viewport 1440x900
|
|
47
|
+
npx libretto run ./integration.ts main --viewport 1440x900
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Notes
|
|
51
|
+
|
|
52
|
+
- If you want a persistent default viewport for the workspace, add `viewport` to `.libretto/config.json` instead of repeating `--viewport` on every command.
|
|
53
|
+
- If `snapshot` analysis is not configured yet, return to the setup steps in the main `SKILL.md` flow.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Pages and Page Targeting
|
|
2
|
+
|
|
3
|
+
Use this reference when a Libretto session has multiple open pages and you need to inspect or target the right one.
|
|
4
|
+
|
|
5
|
+
## When to Use This
|
|
6
|
+
|
|
7
|
+
- The workflow opens a popup, new tab, or secondary page.
|
|
8
|
+
- `exec` or `snapshot` fails because more than one page is open.
|
|
9
|
+
- You are not sure which page in the session holds the relevant state.
|
|
10
|
+
|
|
11
|
+
## Workflow
|
|
12
|
+
|
|
13
|
+
- List the open pages in the session.
|
|
14
|
+
- Identify the page you want by URL.
|
|
15
|
+
- Re-run the command against that page.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx libretto pages --session debug-flow
|
|
21
|
+
npx libretto exec --session debug-flow --page <page-id> "return await page.url()"
|
|
22
|
+
npx libretto snapshot --session debug-flow --page <page-id> --objective "Find the active form"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Notes
|
|
26
|
+
|
|
27
|
+
- A session can contain more than one page.
|
|
28
|
+
- When multiple pages are open, think about page targeting first before debugging selectors.
|
|
29
|
+
- Use `pages` to resolve the correct page id, then pass `--page` to `exec`, `snapshot`, `network`, or `actions` when needed.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Site Security Review
|
|
2
|
+
|
|
3
|
+
Purpose: You are connected to a live Chrome session on a target website. Your job is to review the site's bot-detection and security posture before committing to an integration strategy. Probe for bot protection, fetch interception, and challenge flows, then use that review to decide which integration approaches are safe and which one to try first for this site.
|
|
4
|
+
|
|
5
|
+
After completing the probes below, produce a Site Assessment Summary (see the output format at the end of this document).
|
|
6
|
+
|
|
7
|
+
## Probing the Site
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Probe 1: Bot Protection Services and Security Signals
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
Cookies to look for (examples, not exhaustive):
|
|
16
|
+
|
|
17
|
+
| Cookie Pattern | Associated Service |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `_abck` | Akamai Bot Manager |
|
|
20
|
+
| `_px*` | PerimeterX (HUMAN) |
|
|
21
|
+
| `datadome` | DataDome |
|
|
22
|
+
| `cf_clearance` | Cloudflare |
|
|
23
|
+
| `_imp_apg_r_*` | Shape Security (F5) |
|
|
24
|
+
| `x-kpsdk-*` | Kasada |
|
|
25
|
+
|
|
26
|
+
But do not just check this list. Examine all cookies on the page. Look for cookies with obfuscated names, telemetry-related prefixes, or values that look like fingerprint hashes or encrypted tokens. Unknown security cookies are still security cookies.
|
|
27
|
+
|
|
28
|
+
Global variables to check (examples):
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
window._pxAppId
|
|
32
|
+
window.bmak
|
|
33
|
+
window.ddjskey
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Also examine the page's scripts. Look at the first `<script>` tags in the document source, and check what external domains scripts load from (for example `*.akamaized.net`, `*.perimeterx.net`, `*.datadome.co`, `*.kasada.io`). Bot protection scripts are typically injected before application code.
|
|
37
|
+
|
|
38
|
+
Challenge pages:
|
|
39
|
+
|
|
40
|
+
Check if the page is showing a challenge or interstitial instead of real content: "Checking your browser...", CAPTCHA iframes, or blank pages with only a spinner. These indicate active bot protection that has already been triggered.
|
|
41
|
+
|
|
42
|
+
General guidance: determine whether the site has bot protection and roughly how aggressive it is. Do not limit yourself to known signatures. Look at overall page behavior, unusual scripts, and anything that seems like security telemetry.
|
|
43
|
+
|
|
44
|
+
### Probe 2: Fetch and XHR Interception
|
|
45
|
+
|
|
46
|
+
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 do not originate from its own code.
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
window.fetch.toString()
|
|
50
|
+
XMLHttpRequest.prototype.open.toString()
|
|
51
|
+
Object.getOwnPropertyDescriptor(window, 'fetch')
|
|
52
|
+
window.fetch.hasOwnProperty('prototype')
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
## Choosing a Data Capture Strategy
|
|
58
|
+
|
|
59
|
+
Use the review above to decide what is safe to prioritize. Every integration uses Playwright to control the browser. The question is what to lean on for data capture: direct browser fetch calls, passive network interception, or DOM extraction. In practice, many integrations mix approaches, but the site-security review should tell you which approach is safest to try first.
|
|
60
|
+
|
|
61
|
+
### Strategy A: Prioritize `page.evaluate(fetch(...))`
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
When to prioritize this:
|
|
66
|
+
|
|
67
|
+
- No enterprise bot protection is detected
|
|
68
|
+
- `fetch` is not monkey-patched
|
|
69
|
+
- The API responses are parseable and useful
|
|
70
|
+
- You need data that requires many API calls (deep pagination, bulk queries) where driving the UI would be slow
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
Risk: if the site monitors fetch call stacks, your calls may be flagged because they do not originate from the site's bundled code. This is uncommon but exists on high-security sites.
|
|
75
|
+
|
|
76
|
+
You will still use Playwright for initial navigation, login/auth flows, cookie consent, and any UI interactions needed to establish session state before making fetch calls.
|
|
77
|
+
|
|
78
|
+
### Strategy B: Prioritize `page.on('response', ...)`
|
|
79
|
+
|
|
80
|
+
Listen to network responses that the browser naturally makes as you navigate. You do not make any extra requests. You capture data flowing through the site's own API calls.
|
|
81
|
+
|
|
82
|
+
When to prioritize this:
|
|
83
|
+
|
|
84
|
+
- Enterprise bot protection is detected
|
|
85
|
+
- `fetch` is monkey-patched
|
|
86
|
+
- The site's normal UI flow triggers API calls that return the data you need
|
|
87
|
+
- You want to minimize detection risk as much as possible
|
|
88
|
+
|
|
89
|
+
Why: zero additional network risk. The requests that happen are the ones the site's own code triggers. You are just listening. No anomalous call stacks, no unexpected request patterns, no extra fetch calls for monitoring to flag.
|
|
90
|
+
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
You will still use Playwright for all navigation and interaction to trigger the data-bearing API calls, plus any data that is not available via intercepted responses.
|
|
94
|
+
|
|
95
|
+
### Strategy C: Prioritize Playwright DOM Extraction
|
|
96
|
+
|
|
97
|
+
Extract data directly from the rendered page using selectors and `page.evaluate()` to read DOM content.
|
|
98
|
+
|
|
99
|
+
When to prioritize this:
|
|
100
|
+
|
|
101
|
+
- Data is server-rendered and no useful JSON API calls are observed
|
|
102
|
+
- The site does not expose the data you need via any API
|
|
103
|
+
- You need visual or layout information that only exists in the DOM
|
|
104
|
+
- As a fallback when Strategies A and B cannot get specific pieces of data
|
|
105
|
+
|
|
106
|
+
Why: it works regardless of the site's API architecture. If the data is visible on the page, you can extract it.
|
|
107
|
+
|
|
108
|
+
Trade-off: it is slower, more fragile against DOM changes, and you only get data the UI renders.
|
|
109
|
+
|
|
110
|
+
## Decision Summary
|
|
111
|
+
|
|
112
|
+
| Site Profile | Primary Strategy | Supplement With |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| No bot protection, fetch not patched | A (`page.evaluate(fetch)`) | Playwright for navigation/auth |
|
|
115
|
+
| No bot protection, fetch is patched | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
|
|
116
|
+
| Bot protection detected, fetch not patched | B (`page.on('response', ...)`) | Playwright for navigation; cautious use of `page.evaluate(fetch)` only if needed |
|
|
117
|
+
| Bot protection detected, fetch is patched | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
|
|
118
|
+
| Server-rendered content (no API calls) | C (DOM extraction) | Playwright for all interaction |
|
|
119
|
+
|
|
120
|
+
## Output: Site Assessment Summary
|
|
121
|
+
|
|
122
|
+
After running the probes, produce a summary in this format. This assessment tells you what is safe to try first, not what will definitely work for every endpoint.
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
## Site Assessment: [site URL]
|
|
126
|
+
|
|
127
|
+
### Bot Detection Profile
|
|
128
|
+
- Enterprise bot protection: [None detected / Detected — describe what you found]
|
|
129
|
+
- Fetch/XHR interception: [Native (not patched) / Patched — describe what you found]
|
|
130
|
+
- Challenge pages: [None / Present — describe type]
|
|
131
|
+
- Overall security posture: [None / Low / Moderate / High / Very High]
|
|
132
|
+
|
|
133
|
+
### API Surface
|
|
134
|
+
- API calls observed: [List key endpoints discovered, or "None — content appears server-rendered"]
|
|
135
|
+
- Data format: [JSON / GraphQL / HTML fragments / Other]
|
|
136
|
+
- Pagination: [Describe how pagination works if applicable]
|
|
137
|
+
|
|
138
|
+
### Safe Approaches
|
|
139
|
+
- `page.evaluate(fetch(...))`: [Safe / Unsafe — brief rationale]
|
|
140
|
+
- `page.on('response', ...)`: [Viable / Not viable — note if response formats are parseable]
|
|
141
|
+
- DOM extraction: [Always available as fallback]
|
|
142
|
+
- Interaction notes: [any behavioral precautions]
|
|
143
|
+
```
|
package/src/cli/cli.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ensureLibrettoSetup } from "./core/context.js";
|
|
2
|
+
import { createCLIApp } from "./router.js";
|
|
3
|
+
|
|
4
|
+
function renderUsage(app: ReturnType<typeof createCLIApp>): string {
|
|
5
|
+
return `${app.renderHelp()}
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
--session <name> Use a named session (auto-generated for open/run if omitted)
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
libretto open https://linkedin.com
|
|
12
|
+
|
|
13
|
+
# ... manually log in ...
|
|
14
|
+
libretto save linkedin.com
|
|
15
|
+
# Next time you open linkedin.com, you'll be logged in automatically
|
|
16
|
+
|
|
17
|
+
libretto exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
|
|
18
|
+
libretto exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
|
|
19
|
+
libretto ai configure openai
|
|
20
|
+
libretto ai configure anthropic
|
|
21
|
+
libretto ai configure gemini
|
|
22
|
+
libretto ai configure vertex
|
|
23
|
+
libretto ai configure openai/gpt-4o
|
|
24
|
+
libretto snapshot
|
|
25
|
+
libretto snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
26
|
+
libretto resume --session my-session
|
|
27
|
+
libretto close
|
|
28
|
+
libretto close --all
|
|
29
|
+
libretto close --all --force
|
|
30
|
+
|
|
31
|
+
# Multiple sessions
|
|
32
|
+
libretto open https://site1.com --session test1
|
|
33
|
+
libretto open https://site2.com --session test2
|
|
34
|
+
libretto exec "return await page.title()" --session test1
|
|
35
|
+
|
|
36
|
+
Available in exec:
|
|
37
|
+
page, context, state, browser, networkLog, actionLog
|
|
38
|
+
|
|
39
|
+
Profiles:
|
|
40
|
+
Profiles are saved to .libretto/profiles/<domain>.json (git-ignored)
|
|
41
|
+
They persist cookies, localStorage, and session data across browser launches.
|
|
42
|
+
Local profiles are machine-local and are not shared with other users/environments.
|
|
43
|
+
Sessions can expire; if run fails auth, log in again and re-save the profile.
|
|
44
|
+
|
|
45
|
+
Sessions:
|
|
46
|
+
Session state is stored in .libretto/sessions/<session>/state.json
|
|
47
|
+
CLI logs are stored in .libretto/sessions/<session>/logs.jsonl
|
|
48
|
+
Each session runs an isolated browser instance on a dynamic port.
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isRootHelpRequest(rawArgs: readonly string[]): boolean {
|
|
53
|
+
if (rawArgs.length === 0) return true;
|
|
54
|
+
if (rawArgs[0] === "--help" || rawArgs[0] === "-h") return true;
|
|
55
|
+
return rawArgs[0] === "help" && rawArgs.length === 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runLibrettoCLI(): Promise<void> {
|
|
59
|
+
const rawArgs = process.argv.slice(2);
|
|
60
|
+
let exitCode = 0;
|
|
61
|
+
ensureLibrettoSetup();
|
|
62
|
+
const app = createCLIApp();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (isRootHelpRequest(rawArgs)) {
|
|
66
|
+
console.log(renderUsage(app));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const result = await app.run(rawArgs);
|
|
71
|
+
if (typeof result === "string") {
|
|
72
|
+
console.log(result);
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
if (message.startsWith("Unknown command: ")) {
|
|
77
|
+
console.error(`${message}\n`);
|
|
78
|
+
console.log(renderUsage(app));
|
|
79
|
+
} else {
|
|
80
|
+
console.error(message);
|
|
81
|
+
}
|
|
82
|
+
exitCode = 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.exit(exitCode);
|
|
86
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runAiConfigure } from "../core/ai-config.js";
|
|
3
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
4
|
+
|
|
5
|
+
export const aiConfigureInput = SimpleCLI.input({
|
|
6
|
+
positionals: [
|
|
7
|
+
SimpleCLI.positional("preset", z.string().optional(), {
|
|
8
|
+
help: "Provider shorthand or provider/model-id",
|
|
9
|
+
}),
|
|
10
|
+
],
|
|
11
|
+
named: {
|
|
12
|
+
clear: SimpleCLI.flag({ help: "Clear existing AI config" }),
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const aiCommands = SimpleCLI.group({
|
|
17
|
+
description: "AI commands",
|
|
18
|
+
routes: {
|
|
19
|
+
configure: SimpleCLI.command({
|
|
20
|
+
description: "Configure AI runtime",
|
|
21
|
+
})
|
|
22
|
+
.input(aiConfigureInput)
|
|
23
|
+
.handle(async ({ input }) => {
|
|
24
|
+
runAiConfigure(
|
|
25
|
+
{
|
|
26
|
+
clear: input.clear,
|
|
27
|
+
preset: input.preset,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
configureCommandName: `libretto ai configure`,
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
});
|