libretto 0.1.5 → 0.2.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 +215 -17
- package/bin/libretto.mjs +18 -0
- package/dist/cli/cli.js +203 -0
- package/dist/cli/commands/ai.js +21 -0
- package/dist/cli/commands/browser.js +59 -0
- package/dist/cli/commands/execution.js +422 -0
- package/dist/cli/commands/logs.js +93 -0
- package/dist/cli/commands/snapshot.js +106 -0
- package/dist/cli/core/ai-config.js +149 -0
- package/dist/cli/core/browser.js +523 -0
- package/dist/cli/core/context.js +113 -0
- package/dist/cli/core/pause-signals.js +29 -0
- package/dist/cli/core/session-telemetry.js +491 -0
- package/dist/cli/core/session.js +183 -0
- package/dist/cli/core/snapshot-analyzer.js +492 -0
- package/dist/cli/core/telemetry.js +362 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/workers/run-integration-runtime.js +222 -0
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -0
- package/dist/cli/workers/run-integration-worker.js +83 -0
- package/dist/index.cjs +123 -0
- package/dist/index.d.cts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +107 -0
- package/dist/runtime/download/download.cjs +70 -0
- package/dist/runtime/download/download.d.cts +35 -0
- package/dist/runtime/download/download.d.ts +35 -0
- package/dist/runtime/download/download.js +45 -0
- package/dist/runtime/download/index.cjs +30 -0
- package/dist/runtime/download/index.d.cts +3 -0
- package/dist/runtime/download/index.d.ts +3 -0
- package/dist/runtime/download/index.js +8 -0
- package/dist/runtime/extract/extract.cjs +88 -0
- package/dist/runtime/extract/extract.d.cts +23 -0
- package/dist/runtime/extract/extract.d.ts +23 -0
- package/dist/runtime/extract/extract.js +64 -0
- package/dist/runtime/extract/index.cjs +28 -0
- package/dist/runtime/extract/index.d.cts +5 -0
- package/dist/runtime/extract/index.d.ts +5 -0
- package/dist/runtime/extract/index.js +4 -0
- package/dist/runtime/network/index.cjs +28 -0
- package/dist/runtime/network/index.d.cts +4 -0
- package/dist/runtime/network/index.d.ts +4 -0
- package/dist/runtime/network/index.js +6 -0
- package/dist/runtime/network/network.cjs +91 -0
- package/dist/runtime/network/network.d.cts +28 -0
- package/dist/runtime/network/network.d.ts +28 -0
- package/dist/runtime/network/network.js +67 -0
- package/dist/runtime/recovery/agent.cjs +223 -0
- package/dist/runtime/recovery/agent.d.cts +13 -0
- package/dist/runtime/recovery/agent.d.ts +13 -0
- package/dist/runtime/recovery/agent.js +199 -0
- package/dist/runtime/recovery/errors.cjs +124 -0
- package/dist/runtime/recovery/errors.d.cts +31 -0
- package/dist/runtime/recovery/errors.d.ts +31 -0
- package/dist/runtime/recovery/errors.js +100 -0
- package/dist/runtime/recovery/index.cjs +34 -0
- package/dist/runtime/recovery/index.d.cts +7 -0
- package/dist/runtime/recovery/index.d.ts +7 -0
- package/dist/runtime/recovery/index.js +10 -0
- package/dist/runtime/recovery/recovery.cjs +55 -0
- package/dist/runtime/recovery/recovery.d.cts +12 -0
- package/dist/runtime/recovery/recovery.d.ts +12 -0
- package/dist/runtime/recovery/recovery.js +31 -0
- package/dist/shared/config/config.cjs +44 -0
- package/dist/shared/config/config.d.cts +10 -0
- package/dist/shared/config/config.d.ts +10 -0
- package/dist/shared/config/config.js +18 -0
- package/dist/shared/config/index.cjs +32 -0
- package/dist/shared/config/index.d.cts +1 -0
- package/dist/shared/config/index.d.ts +1 -0
- package/dist/shared/config/index.js +10 -0
- package/dist/shared/debug/index.cjs +32 -0
- package/dist/shared/debug/index.d.cts +2 -0
- package/dist/shared/debug/index.d.ts +2 -0
- package/dist/shared/debug/index.js +10 -0
- package/dist/shared/debug/pause.cjs +56 -0
- package/dist/shared/debug/pause.d.cts +23 -0
- package/dist/shared/debug/pause.d.ts +23 -0
- package/dist/shared/debug/pause.js +30 -0
- package/dist/shared/instrumentation/errors.cjs +81 -0
- package/dist/shared/instrumentation/errors.d.cts +12 -0
- package/dist/shared/instrumentation/errors.d.ts +12 -0
- package/dist/shared/instrumentation/errors.js +57 -0
- package/dist/shared/instrumentation/index.cjs +35 -0
- package/dist/shared/instrumentation/index.d.cts +6 -0
- package/dist/shared/instrumentation/index.d.ts +6 -0
- package/dist/shared/instrumentation/index.js +12 -0
- package/dist/shared/instrumentation/instrument.cjs +206 -0
- package/dist/shared/instrumentation/instrument.d.cts +32 -0
- package/dist/shared/instrumentation/instrument.d.ts +32 -0
- package/dist/shared/instrumentation/instrument.js +190 -0
- package/dist/shared/llm/client.cjs +139 -0
- package/dist/shared/llm/client.d.cts +6 -0
- package/dist/shared/llm/client.d.ts +6 -0
- package/dist/shared/llm/client.js +115 -0
- package/dist/shared/llm/index.cjs +28 -0
- package/dist/shared/llm/index.d.cts +3 -0
- package/dist/shared/llm/index.d.ts +3 -0
- package/dist/shared/llm/index.js +4 -0
- package/dist/shared/llm/types.cjs +16 -0
- package/dist/shared/llm/types.d.cts +34 -0
- package/dist/shared/llm/types.d.ts +34 -0
- package/dist/shared/llm/types.js +0 -0
- package/dist/shared/logger/index.cjs +37 -0
- package/dist/shared/logger/index.d.cts +2 -0
- package/dist/shared/logger/index.d.ts +2 -0
- package/dist/shared/logger/index.js +13 -0
- package/dist/shared/logger/logger.cjs +213 -0
- package/dist/shared/logger/logger.d.cts +82 -0
- package/dist/shared/logger/logger.d.ts +82 -0
- package/dist/shared/logger/logger.js +188 -0
- package/dist/shared/logger/sinks.cjs +160 -0
- package/dist/shared/logger/sinks.d.cts +9 -0
- package/dist/shared/logger/sinks.d.ts +9 -0
- package/dist/shared/logger/sinks.js +124 -0
- package/dist/shared/paths/paths.cjs +104 -0
- package/dist/shared/paths/paths.d.cts +10 -0
- package/dist/shared/paths/paths.d.ts +10 -0
- package/dist/shared/paths/paths.js +73 -0
- package/dist/shared/run/api.cjs +35 -0
- package/dist/shared/run/api.d.cts +3 -0
- package/dist/shared/run/api.d.ts +3 -0
- package/dist/shared/run/api.js +12 -0
- package/dist/shared/run/browser.cjs +98 -0
- package/dist/shared/run/browser.d.cts +22 -0
- package/dist/shared/run/browser.d.ts +22 -0
- package/dist/shared/run/browser.js +74 -0
- package/dist/shared/state/index.cjs +38 -0
- package/dist/shared/state/index.d.cts +2 -0
- package/dist/shared/state/index.d.ts +2 -0
- package/dist/shared/state/index.js +16 -0
- package/dist/shared/state/session-state.cjs +85 -0
- package/dist/shared/state/session-state.d.cts +34 -0
- package/dist/shared/state/session-state.d.ts +34 -0
- package/dist/shared/state/session-state.js +56 -0
- package/dist/shared/visualization/ghost-cursor.cjs +174 -0
- package/dist/shared/visualization/ghost-cursor.d.cts +37 -0
- package/dist/shared/visualization/ghost-cursor.d.ts +37 -0
- package/dist/shared/visualization/ghost-cursor.js +145 -0
- package/dist/shared/visualization/highlight.cjs +134 -0
- package/dist/shared/visualization/highlight.d.cts +22 -0
- package/dist/shared/visualization/highlight.d.ts +22 -0
- package/dist/shared/visualization/highlight.js +108 -0
- package/dist/shared/visualization/index.cjs +45 -0
- package/dist/shared/visualization/index.d.cts +3 -0
- package/dist/shared/visualization/index.d.ts +3 -0
- package/dist/shared/visualization/index.js +24 -0
- package/dist/shared/workflow/workflow.cjs +47 -0
- package/dist/shared/workflow/workflow.d.cts +33 -0
- package/dist/shared/workflow/workflow.d.ts +33 -0
- package/dist/shared/workflow/workflow.js +21 -0
- package/package.json +125 -26
- package/.npmignore +0 -2
- package/bin/libretto +0 -31
- package/lib/connect.js +0 -34
- package/lib/export.js +0 -224
- package/lib/import.js +0 -166
- package/lib/index.js +0 -8
- package/lib/log.js +0 -9
- package/lib/validate.js +0 -20
- package/makefile +0 -8
- package/src/connect.coffee +0 -25
- package/src/export.coffee +0 -222
- package/src/import.coffee +0 -166
- package/src/index.coffee +0 -3
- package/src/log.coffee +0 -3
- package/src/validate.coffee +0 -10
package/README.md
CHANGED
|
@@ -1,33 +1,231 @@
|
|
|
1
|
-
Libretto
|
|
1
|
+
# Libretto
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A TypeScript library for browser automation with AI-powered recovery and data extraction. Built on Playwright.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **AI-powered recovery** — Vision-based agent that automatically detects and dismisses popups or obstacles using an LLM
|
|
8
|
+
- **Structured data extraction** — Extract typed data from web pages using AI vision + Zod schemas
|
|
9
|
+
- **Error detection** — Classify form/submission errors against known patterns
|
|
10
|
+
- **In-browser network requests** — Execute authenticated fetch calls inside the page context with optional Zod validation
|
|
11
|
+
- **File downloads** — Trigger and intercept file downloads via click, with optional save-to-disk
|
|
12
|
+
- **Dry-run mode** — Skip mutations in development without side effects
|
|
13
|
+
- **Pluggable LLM** — Bring your own LLM provider (Claude, GPT, etc.) via a simple interface
|
|
14
|
+
- **Pluggable logging** — All runtime functions accept an optional logger; defaults to console output
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
4
17
|
|
|
5
18
|
```bash
|
|
6
|
-
libretto
|
|
19
|
+
pnpm add libretto playwright zod
|
|
7
20
|
```
|
|
8
21
|
|
|
9
|
-
|
|
22
|
+
`playwright` and `zod` are peer dependencies.
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { chromium } from "playwright";
|
|
28
|
+
import { extractFromPage, attemptWithRecovery } from "libretto";
|
|
29
|
+
|
|
30
|
+
const browser = await chromium.launch();
|
|
31
|
+
const page = await browser.newPage();
|
|
32
|
+
|
|
33
|
+
await page.goto("https://example.com/login");
|
|
34
|
+
await page.fill("#email", "user@example.com");
|
|
35
|
+
await page.fill("#password", "secret");
|
|
36
|
+
|
|
37
|
+
// Automatically retry with AI popup recovery on failure
|
|
38
|
+
await attemptWithRecovery(page, () => page.click('button[type="submit"]'));
|
|
39
|
+
|
|
40
|
+
await browser.close();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Runtime Functions
|
|
44
|
+
|
|
45
|
+
### Recovery
|
|
46
|
+
|
|
47
|
+
#### `attemptWithRecovery(page, fn, logger?, llmClient?)`
|
|
48
|
+
|
|
49
|
+
Executes a function and, if it fails, uses AI vision to detect and dismiss popups before retrying once.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { attemptWithRecovery } from "libretto";
|
|
53
|
+
|
|
54
|
+
await attemptWithRecovery(page, async () => {
|
|
55
|
+
await page.click('button[type="submit"]');
|
|
56
|
+
}, undefined, llmClient);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### `executeRecoveryAgent(page, instruction, logger?, llmClient?)`
|
|
60
|
+
|
|
61
|
+
Runs a multi-step vision-based recovery agent that takes screenshots and executes browser actions (click, type, scroll, etc.) to resolve obstacles.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { executeRecoveryAgent } from "libretto";
|
|
65
|
+
|
|
66
|
+
await executeRecoveryAgent(
|
|
67
|
+
page,
|
|
68
|
+
"Close the cookie consent banner",
|
|
69
|
+
undefined,
|
|
70
|
+
llmClient,
|
|
71
|
+
);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### `detectSubmissionError(page, error, logContext, llmClient, knownErrors?, logger?)`
|
|
75
|
+
|
|
76
|
+
Uses a screenshot + LLM vision to detect if an error occurred during a form submission. Matches against provided known error patterns.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { detectSubmissionError } from "libretto";
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await page.click("#submit");
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const result = await detectSubmissionError(page, error, "checkout", llmClient, [
|
|
85
|
+
{ id: "duplicate", errorPatterns: ["already exists"], userMessage: "Duplicate entry" },
|
|
86
|
+
]);
|
|
87
|
+
console.log(result.errorId, result.message);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Data Extraction
|
|
92
|
+
|
|
93
|
+
#### `extractFromPage(options)`
|
|
94
|
+
|
|
95
|
+
Extract structured data from a page using AI vision + a Zod schema.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { extractFromPage } from "libretto";
|
|
99
|
+
import { z } from "zod";
|
|
100
|
+
|
|
101
|
+
const result = await extractFromPage({
|
|
102
|
+
page,
|
|
103
|
+
llmClient,
|
|
104
|
+
instruction: "Extract the product name and price",
|
|
105
|
+
schema: z.object({
|
|
106
|
+
name: z.string(),
|
|
107
|
+
price: z.number(),
|
|
108
|
+
}),
|
|
109
|
+
selector: ".product-card", // optional — scopes to a specific element
|
|
110
|
+
});
|
|
111
|
+
// result is typed as { name: string; price: number }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Network
|
|
115
|
+
|
|
116
|
+
#### `pageRequest(page, config, options?)`
|
|
117
|
+
|
|
118
|
+
Executes a fetch call inside the browser context via `page.evaluate()`, inheriting the page's cookies and auth state. Supports optional Zod validation.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { pageRequest } from "libretto";
|
|
122
|
+
import { z } from "zod";
|
|
123
|
+
|
|
124
|
+
const data = await pageRequest(
|
|
125
|
+
page,
|
|
126
|
+
{
|
|
127
|
+
url: "https://example.com/api/profile",
|
|
128
|
+
method: "GET",
|
|
129
|
+
responseType: "json",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
schema: z.object({ name: z.string(), email: z.string() }),
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Downloads
|
|
138
|
+
|
|
139
|
+
#### `downloadViaClick(page, selector, options?)`
|
|
140
|
+
|
|
141
|
+
Triggers a file download by clicking a DOM element and intercepts the result.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { downloadViaClick } from "libretto";
|
|
145
|
+
|
|
146
|
+
const { buffer, filename } = await downloadViaClick(page, "#download-btn");
|
|
13
147
|
```
|
|
14
148
|
|
|
15
|
-
|
|
149
|
+
#### `downloadAndSave(page, selector, options?)`
|
|
150
|
+
|
|
151
|
+
Same as `downloadViaClick` but also writes the file to disk.
|
|
16
152
|
|
|
153
|
+
```typescript
|
|
154
|
+
import { downloadAndSave } from "libretto";
|
|
17
155
|
|
|
18
|
-
|
|
19
|
-
|
|
156
|
+
const { savedTo } = await downloadAndSave(page, "#export-csv", {
|
|
157
|
+
savePath: "./exports/report.csv",
|
|
158
|
+
});
|
|
159
|
+
```
|
|
20
160
|
|
|
21
|
-
|
|
161
|
+
## LLM Client Interface
|
|
22
162
|
|
|
23
|
-
|
|
24
|
-
before(function(next) {
|
|
25
|
-
exec("./node_modules/.bin/libretto import " + __dirname + "/fixtures/scenario1/* --database=app-testing", next);
|
|
26
|
-
});
|
|
163
|
+
Provide your own implementation backed by any LLM provider:
|
|
27
164
|
|
|
28
|
-
|
|
165
|
+
```typescript
|
|
166
|
+
import type { LLMClient } from "libretto";
|
|
29
167
|
|
|
30
|
-
|
|
168
|
+
const myLLMClient: LLMClient = {
|
|
169
|
+
async generateObject({ prompt, schema, temperature }) {
|
|
170
|
+
// Call your LLM, return parsed + validated result
|
|
171
|
+
},
|
|
172
|
+
async generateObjectFromMessages({ messages, schema, temperature }) {
|
|
173
|
+
// Call your LLM with message history, return parsed + validated result
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
```
|
|
31
177
|
|
|
32
|
-
|
|
178
|
+
## Logging
|
|
179
|
+
|
|
180
|
+
All runtime functions accept an optional `logger` parameter. When omitted, output goes to `console.log` with `[INFO]`, `[WARN]`, `[ERROR]` prefixes.
|
|
181
|
+
|
|
182
|
+
For structured logging, use the built-in `Logger` class:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { Logger, createFileLogSink, prettyConsoleSink } from "libretto";
|
|
186
|
+
|
|
187
|
+
const logger = new Logger()
|
|
188
|
+
.withSink(createFileLogSink({ filePath: "./app.log" }))
|
|
189
|
+
.withSink(prettyConsoleSink);
|
|
190
|
+
|
|
191
|
+
const scoped = logger.withScope("auth");
|
|
192
|
+
scoped.info("login attempt", { user: "alice" });
|
|
193
|
+
scoped.error("login failed", { reason: "bad password" });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Module Exports
|
|
197
|
+
|
|
198
|
+
Libretto provides granular imports:
|
|
199
|
+
|
|
200
|
+
| Import | Contents |
|
|
201
|
+
| ------------------------ | --------------------------------------------------------- |
|
|
202
|
+
| `libretto` | Everything |
|
|
203
|
+
| `libretto/logger` | `Logger`, `defaultLogger`, sinks |
|
|
204
|
+
| `libretto/recovery` | `attemptWithRecovery`, `executeRecoveryAgent`, `detectSubmissionError` |
|
|
205
|
+
| `libretto/extract` | `extractFromPage` |
|
|
206
|
+
| `libretto/network` | `pageRequest` |
|
|
207
|
+
| `libretto/download` | `downloadViaClick`, `downloadAndSave` |
|
|
208
|
+
| `libretto/debug` | `debugPause` |
|
|
209
|
+
| `libretto/config` | `isDryRun`, `isDebugMode`, `shouldPauseBeforeMutation` |
|
|
210
|
+
| `libretto/instrumentation` | `instrumentPage`, `installInstrumentation` |
|
|
211
|
+
| `libretto/visualization` | Ghost cursor and highlight helpers |
|
|
212
|
+
| `libretto/run` | `launchBrowser` |
|
|
213
|
+
| `libretto/state` | Session state serialization and parsing |
|
|
214
|
+
| `libretto/llm` | `LLMClient` type |
|
|
215
|
+
|
|
216
|
+
## Configuration
|
|
217
|
+
|
|
218
|
+
Runtime flags via environment variables:
|
|
219
|
+
|
|
220
|
+
| Env Variable | Effect |
|
|
221
|
+
| --------------------- | --------------------------------------------------- |
|
|
222
|
+
| `LIBRETTO_DEBUG` | Enable debug mode |
|
|
223
|
+
| `LIBRETTO_DRY_RUN` | Enable dry-run mode (defaults to `true` in development) |
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
pnpm install
|
|
229
|
+
pnpm build # compile to dist/
|
|
230
|
+
pnpm type-check # typecheck without emitting
|
|
33
231
|
```
|
package/bin/libretto.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const cliRoot = join(__dirname, "..");
|
|
9
|
+
const distEntry = join(cliRoot, "dist", "cli", "index.js");
|
|
10
|
+
|
|
11
|
+
if (!existsSync(distEntry)) {
|
|
12
|
+
// Build from the libretto monorepo root (builds core then CLI in order)
|
|
13
|
+
const monorepoRoot = join(cliRoot, "..", "..");
|
|
14
|
+
console.error("[libretto] dist not found, building...");
|
|
15
|
+
execSync("pnpm build", { cwd: monorepoRoot, stdio: "inherit" });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await import(distEntry);
|
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import yargs from "yargs";
|
|
2
|
+
import { hideBin } from "yargs/helpers";
|
|
3
|
+
import { registerAICommands } from "./commands/ai.js";
|
|
4
|
+
import { registerBrowserCommands } from "./commands/browser.js";
|
|
5
|
+
import { registerExecutionCommands } from "./commands/execution.js";
|
|
6
|
+
import { registerLogCommands } from "./commands/logs.js";
|
|
7
|
+
import { registerSnapshotCommands } from "./commands/snapshot.js";
|
|
8
|
+
import {
|
|
9
|
+
closeLogger,
|
|
10
|
+
createLoggerForSession,
|
|
11
|
+
ensureLibrettoSetup
|
|
12
|
+
} from "./core/context.js";
|
|
13
|
+
import {
|
|
14
|
+
SESSION_DEFAULT,
|
|
15
|
+
validateSessionName
|
|
16
|
+
} from "./core/session.js";
|
|
17
|
+
const CLI_COMMANDS = /* @__PURE__ */ new Set([
|
|
18
|
+
"open",
|
|
19
|
+
"run",
|
|
20
|
+
"ai",
|
|
21
|
+
"save",
|
|
22
|
+
"exec",
|
|
23
|
+
"snapshot",
|
|
24
|
+
"network",
|
|
25
|
+
"actions",
|
|
26
|
+
"pages",
|
|
27
|
+
"resume",
|
|
28
|
+
"close",
|
|
29
|
+
"--help",
|
|
30
|
+
"-h",
|
|
31
|
+
"help"
|
|
32
|
+
]);
|
|
33
|
+
function printUsage() {
|
|
34
|
+
console.log(`Usage: libretto-cli <command> [--session <name>]
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
open <url> [--headless] Launch browser and open URL (headed by default)
|
|
38
|
+
Automatically loads saved profile if available
|
|
39
|
+
run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless] [--debug] Run an exported Libretto workflow from a file; pass --debug to enable pause-on-failure debugging (or --no-debug to disable)
|
|
40
|
+
ai configure [preset] [-- <command prefix...>] Configure AI runtime for analysis commands
|
|
41
|
+
save <url|domain> Save current browser session (cookies, localStorage, etc.)
|
|
42
|
+
exec <code> [--visualize] Execute Playwright typescript code (--visualize enables ghost cursor + highlight)
|
|
43
|
+
snapshot [--objective <text> --context <text>] Capture PNG + HTML; analyze when objective is provided (context optional)
|
|
44
|
+
network [--last N] [--filter regex] [--method M] [--clear] View captured network requests
|
|
45
|
+
actions [--last N] [--filter regex] [--action TYPE] [--source SOURCE] [--clear] View captured actions
|
|
46
|
+
pages List open pages in the active session
|
|
47
|
+
resume Resume a paused workflow in the active session
|
|
48
|
+
close Close the browser
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--session <name> Use a named session (default: "default")
|
|
52
|
+
Built-in sessions: default, dev-server, browser-agent
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
libretto-cli open https://linkedin.com
|
|
56
|
+
|
|
57
|
+
# ... manually log in ...
|
|
58
|
+
libretto-cli save linkedin.com
|
|
59
|
+
# Next time you open linkedin.com, you'll be logged in automatically
|
|
60
|
+
|
|
61
|
+
libretto-cli exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
|
|
62
|
+
libretto-cli exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
|
|
63
|
+
libretto-cli ai configure codex
|
|
64
|
+
libretto-cli ai configure claude
|
|
65
|
+
libretto-cli ai configure gemini
|
|
66
|
+
libretto-cli ai configure <codex|claude|gemini> -- <command prefix...>
|
|
67
|
+
libretto-cli snapshot
|
|
68
|
+
libretto-cli snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
69
|
+
libretto-cli resume --session default
|
|
70
|
+
libretto-cli close
|
|
71
|
+
|
|
72
|
+
# Multiple sessions
|
|
73
|
+
libretto-cli open https://site1.com --session test1
|
|
74
|
+
libretto-cli open https://site2.com --session test2
|
|
75
|
+
libretto-cli exec "return await page.title()" --session test1
|
|
76
|
+
|
|
77
|
+
Available in exec:
|
|
78
|
+
page, context, state, browser, networkLog, actionLog
|
|
79
|
+
|
|
80
|
+
Profiles:
|
|
81
|
+
Profiles are saved to .libretto/profiles/<domain>.json (git-ignored)
|
|
82
|
+
They persist cookies, localStorage, and session data across browser launches.
|
|
83
|
+
Local profiles are machine-local and are not shared with other users/environments.
|
|
84
|
+
Sessions can expire; if run fails auth, log in again and re-save the profile.
|
|
85
|
+
|
|
86
|
+
Sessions:
|
|
87
|
+
Session state is stored in .libretto/sessions/<session>/state.json
|
|
88
|
+
CLI logs are stored in .libretto/sessions/<session>/logs.jsonl
|
|
89
|
+
Each session runs an isolated browser instance on a dynamic port.
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
function filterSessionArgs(args) {
|
|
93
|
+
const result = [];
|
|
94
|
+
for (let i = 0; i < args.length; i++) {
|
|
95
|
+
if (args[i] === "--session") {
|
|
96
|
+
i++;
|
|
97
|
+
} else {
|
|
98
|
+
result.push(args[i]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function parseSessionForLog(rawArgs) {
|
|
104
|
+
const idx = rawArgs.indexOf("--session");
|
|
105
|
+
if (idx < 0) return SESSION_DEFAULT;
|
|
106
|
+
const value = rawArgs[idx + 1];
|
|
107
|
+
if (!value || value.startsWith("--") || CLI_COMMANDS.has(value)) {
|
|
108
|
+
return SESSION_DEFAULT;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
validateSessionName(value);
|
|
112
|
+
return value;
|
|
113
|
+
} catch {
|
|
114
|
+
return SESSION_DEFAULT;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function validateLegacySessionArg(rawArgs) {
|
|
118
|
+
const idx = rawArgs.indexOf("--session");
|
|
119
|
+
if (idx < 0) return;
|
|
120
|
+
const value = rawArgs[idx + 1];
|
|
121
|
+
if (!value || value.startsWith("--") || CLI_COMMANDS.has(value)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"Usage: libretto-cli <command> [--session <name>]\nMissing or invalid --session value."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
validateSessionName(value);
|
|
127
|
+
}
|
|
128
|
+
function initializeLogger(rawArgs) {
|
|
129
|
+
const sessionForLog = parseSessionForLog(rawArgs);
|
|
130
|
+
const logger = createLoggerForSession(sessionForLog);
|
|
131
|
+
logger.info("cli-start", {
|
|
132
|
+
args: rawArgs,
|
|
133
|
+
cwd: process.cwd(),
|
|
134
|
+
session: sessionForLog
|
|
135
|
+
});
|
|
136
|
+
return logger;
|
|
137
|
+
}
|
|
138
|
+
async function withCliLogger(rawArgs, run) {
|
|
139
|
+
const logger = initializeLogger(rawArgs);
|
|
140
|
+
try {
|
|
141
|
+
return await run(logger);
|
|
142
|
+
} finally {
|
|
143
|
+
await closeLogger(logger);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function createParser(logger) {
|
|
147
|
+
let parser = yargs(hideBin(process.argv)).scriptName("libretto-cli").parserConfiguration({ "populate--": true }).option("session", {
|
|
148
|
+
type: "string",
|
|
149
|
+
default: SESSION_DEFAULT,
|
|
150
|
+
describe: "Use a named session",
|
|
151
|
+
global: true
|
|
152
|
+
}).middleware((argv) => {
|
|
153
|
+
validateSessionName(String(argv.session));
|
|
154
|
+
}).exitProcess(false).help(false).version(false).fail((msg, err) => {
|
|
155
|
+
if (err) throw err;
|
|
156
|
+
throw new Error(msg || "Command failed");
|
|
157
|
+
});
|
|
158
|
+
parser = registerBrowserCommands(parser, logger);
|
|
159
|
+
parser = registerExecutionCommands(parser, logger);
|
|
160
|
+
parser = registerLogCommands(parser);
|
|
161
|
+
parser = registerAICommands(parser);
|
|
162
|
+
parser = registerSnapshotCommands(parser, logger);
|
|
163
|
+
parser = parser.command("help", "Show usage", () => {
|
|
164
|
+
}, () => {
|
|
165
|
+
printUsage();
|
|
166
|
+
});
|
|
167
|
+
return parser;
|
|
168
|
+
}
|
|
169
|
+
async function runLibrettoCLI() {
|
|
170
|
+
const rawArgs = process.argv.slice(2);
|
|
171
|
+
let exitCode = 0;
|
|
172
|
+
ensureLibrettoSetup();
|
|
173
|
+
await withCliLogger(rawArgs, async (logger) => {
|
|
174
|
+
try {
|
|
175
|
+
validateLegacySessionArg(rawArgs);
|
|
176
|
+
const args = filterSessionArgs(rawArgs);
|
|
177
|
+
const command = args[0];
|
|
178
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
179
|
+
printUsage();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!CLI_COMMANDS.has(command)) {
|
|
183
|
+
console.error(`Unknown command: ${command}
|
|
184
|
+
`);
|
|
185
|
+
printUsage();
|
|
186
|
+
exitCode = 1;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const parser = createParser(logger);
|
|
190
|
+
logger.info("cli-command", { command, args });
|
|
191
|
+
await parser.parseAsync();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
logger.error("cli-error", { error: err, args: rawArgs });
|
|
194
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
195
|
+
console.error(message);
|
|
196
|
+
exitCode = 1;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
process.exit(exitCode);
|
|
200
|
+
}
|
|
201
|
+
export {
|
|
202
|
+
runLibrettoCLI
|
|
203
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runAiConfigure } from "../core/ai-config.js";
|
|
2
|
+
function registerAICommands(yargs) {
|
|
3
|
+
return yargs.command(
|
|
4
|
+
"ai configure [preset]",
|
|
5
|
+
"Configure AI runtime",
|
|
6
|
+
(cmd) => cmd.option("clear", { type: "boolean", default: false }),
|
|
7
|
+
(argv) => {
|
|
8
|
+
const customPrefix = Array.isArray(argv["--"]) ? argv["--"] : [];
|
|
9
|
+
runAiConfigure({
|
|
10
|
+
clear: Boolean(argv.clear),
|
|
11
|
+
preset: argv.preset,
|
|
12
|
+
customPrefix
|
|
13
|
+
}, {
|
|
14
|
+
configureCommandName: "libretto-cli ai configure"
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
registerAICommands
|
|
21
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runClose as runCloseWithLogger,
|
|
3
|
+
runOpen,
|
|
4
|
+
runPages,
|
|
5
|
+
runSave
|
|
6
|
+
} from "../core/browser.js";
|
|
7
|
+
import { withSessionLogger } from "../core/context.js";
|
|
8
|
+
function registerBrowserCommands(yargs, logger) {
|
|
9
|
+
return yargs.command(
|
|
10
|
+
"open [url]",
|
|
11
|
+
"Launch browser and open URL (headed by default)",
|
|
12
|
+
(cmd) => cmd.option("headed", {
|
|
13
|
+
type: "boolean",
|
|
14
|
+
default: false
|
|
15
|
+
}).option("headless", {
|
|
16
|
+
type: "boolean",
|
|
17
|
+
default: false
|
|
18
|
+
}),
|
|
19
|
+
async (argv) => {
|
|
20
|
+
const hasHeadedFlag = Boolean(argv.headed);
|
|
21
|
+
const hasHeadlessFlag = Boolean(argv.headless);
|
|
22
|
+
if (hasHeadedFlag && hasHeadlessFlag) {
|
|
23
|
+
throw new Error("Cannot pass both --headed and --headless.");
|
|
24
|
+
}
|
|
25
|
+
const headed = hasHeadedFlag || !hasHeadlessFlag;
|
|
26
|
+
const url = argv.url;
|
|
27
|
+
if (!url) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Usage: libretto-cli open <url> [--headless] [--session <name>]"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
await runOpen(url, headed, String(argv.session), logger);
|
|
33
|
+
}
|
|
34
|
+
).command(
|
|
35
|
+
"save [urlOrDomain]",
|
|
36
|
+
"Save current browser session",
|
|
37
|
+
(cmd) => cmd,
|
|
38
|
+
async (argv) => {
|
|
39
|
+
const urlOrDomain = argv.urlOrDomain;
|
|
40
|
+
if (!urlOrDomain) {
|
|
41
|
+
throw new Error("Usage: libretto-cli save <url|domain> [--session <name>]");
|
|
42
|
+
}
|
|
43
|
+
await runSave(urlOrDomain, String(argv.session), logger);
|
|
44
|
+
}
|
|
45
|
+
).command("pages", "List open pages in the session", (cmd) => cmd, async (argv) => {
|
|
46
|
+
await runPages(String(argv.session), logger);
|
|
47
|
+
}).command("close", "Close the browser", (cmd) => cmd, async (argv) => {
|
|
48
|
+
await runCloseWithLogger(String(argv.session), logger);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function runClose(session) {
|
|
52
|
+
await withSessionLogger(session, async (logger) => {
|
|
53
|
+
await runCloseWithLogger(session, logger);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
registerBrowserCommands,
|
|
58
|
+
runClose
|
|
59
|
+
};
|