libretto 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -17
- package/bin/libretto.mjs +18 -0
- package/dist/cli/cli.js +201 -0
- package/dist/cli/commands/ai.js +21 -0
- package/dist/cli/commands/browser.js +56 -0
- package/dist/cli/commands/execution.js +407 -0
- package/dist/cli/commands/logs.js +65 -0
- package/dist/cli/commands/snapshot.js +99 -0
- package/dist/cli/core/ai-config.js +149 -0
- package/dist/cli/core/browser.js +687 -0
- package/dist/cli/core/context.js +113 -0
- package/dist/cli/core/pause-signals.js +29 -0
- package/dist/cli/core/session.js +183 -0
- package/dist/cli/core/snapshot-analyzer.js +492 -0
- package/dist/cli/core/telemetry.js +350 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/workers/run-integration-runtime.js +204 -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 +127 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +110 -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 +87 -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 +63 -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 +218 -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 +194 -0
- package/dist/runtime/recovery/errors.cjs +122 -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 +98 -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 +53 -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 +29 -0
- package/dist/runtime/step/index.cjs +31 -0
- package/dist/runtime/step/index.d.cts +7 -0
- package/dist/runtime/step/index.d.ts +7 -0
- package/dist/runtime/step/index.js +6 -0
- package/dist/runtime/step/runner.cjs +208 -0
- package/dist/runtime/step/runner.d.cts +16 -0
- package/dist/runtime/step/runner.d.ts +16 -0
- package/dist/runtime/step/runner.js +187 -0
- package/dist/runtime/step/step.cjs +67 -0
- package/dist/runtime/step/step.d.cts +23 -0
- package/dist/runtime/step/step.d.ts +23 -0
- package/dist/runtime/step/step.js +43 -0
- package/dist/runtime/step/types.cjs +16 -0
- package/dist/runtime/step/types.d.cts +72 -0
- package/dist/runtime/step/types.d.ts +72 -0
- package/dist/runtime/step/types.js +0 -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 +35 -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 +12 -0
- package/dist/shared/logger/logger.cjs +200 -0
- package/dist/shared/logger/logger.d.cts +70 -0
- package/dist/shared/logger/logger.d.ts +70 -0
- package/dist/shared/logger/logger.js +176 -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 +123 -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 -168
- 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 -171
- 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,229 @@
|
|
|
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
|
+
- **Step-based workflows** — Define automation as named steps with built-in error handling and recovery
|
|
8
|
+
- **AI-powered recovery** — Vision-based agent that automatically detects and dismisses popups or obstacles using an LLM
|
|
9
|
+
- **Structured data extraction** — Extract typed data from web pages using AI vision + Zod schemas
|
|
10
|
+
- **Error detection** — Classify form/submission errors against known patterns
|
|
11
|
+
- **Debug bundles** — On failure, captures screenshots, DOM, logs, and step history for investigation
|
|
12
|
+
- **Dry-run mode** — Run workflows in simulation without side effects
|
|
13
|
+
- **Pluggable LLM** — Bring your own LLM provider (Claude, GPT, etc.) via a simple interface
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
4
16
|
|
|
5
17
|
```bash
|
|
6
|
-
libretto
|
|
18
|
+
pnpm add libretto playwright zod
|
|
7
19
|
```
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
`playwright` and `zod` are peer dependencies.
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { chromium } from "playwright";
|
|
27
|
+
import { step, createRunner } from "libretto";
|
|
28
|
+
|
|
29
|
+
const runner = createRunner({
|
|
30
|
+
llmClient: myLLMClient, // optional — enables AI recovery & extraction
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const steps = [
|
|
34
|
+
step("navigate", async ({ page, logger }) => {
|
|
35
|
+
await page.goto("https://example.com/login");
|
|
36
|
+
logger.info("navigated to login page");
|
|
37
|
+
}),
|
|
38
|
+
|
|
39
|
+
step("login", async ({ page }) => {
|
|
40
|
+
await page.fill("#email", "user@example.com");
|
|
41
|
+
await page.fill("#password", "secret");
|
|
42
|
+
await page.click('button[type="submit"]');
|
|
43
|
+
await page.waitForURL("**/dashboard");
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
step("scrape-data", async ({ page, logger }) => {
|
|
47
|
+
const title = await page.textContent("h1");
|
|
48
|
+
logger.info("page title", { title });
|
|
49
|
+
}),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const browser = await chromium.launch();
|
|
53
|
+
const page = await browser.newPage();
|
|
54
|
+
await runner.run(page, steps);
|
|
55
|
+
await browser.close();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Core Concepts
|
|
59
|
+
|
|
60
|
+
### Steps
|
|
61
|
+
|
|
62
|
+
A step is a named unit of work. Create one with the `step()` factory:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
step("step-name", async ({ page, logger, config }) => {
|
|
66
|
+
// page: Playwright Page instance
|
|
67
|
+
// logger: scoped logger for this step
|
|
68
|
+
// config: { dryRun, debug, logDir }
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Step Options
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
step("submit-form", handler, {
|
|
76
|
+
dryRun: "skip", // "skip" (default) | "execute" | "simulate"
|
|
77
|
+
simulate: async ({ logger }) => {
|
|
78
|
+
logger.info("simulated form submission");
|
|
79
|
+
},
|
|
80
|
+
recovery: {
|
|
81
|
+
"session-expired": async ({ page, logger }) => {
|
|
82
|
+
await page.click("#re-login");
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **`dryRun`** — Controls behavior when the runner is in dry-run mode:
|
|
89
|
+
- `"skip"` — Skip the step entirely
|
|
90
|
+
- `"execute"` — Run normally even in dry-run mode
|
|
91
|
+
- `"simulate"` — Call the `simulate` function instead
|
|
92
|
+
- **`recovery`** — Named recovery handlers tried after AI recovery fails
|
|
93
|
+
|
|
94
|
+
### Extending Steps
|
|
95
|
+
|
|
96
|
+
Use `step.extend()` to create a step factory with shared recovery handlers:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const myStep = step.extend({
|
|
100
|
+
recovery: {
|
|
101
|
+
"cookie-banner": async ({ page }) => {
|
|
102
|
+
await page.click("#accept-cookies");
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Every step created with myStep inherits the cookie-banner recovery
|
|
108
|
+
myStep("checkout", async ({ page }) => { /* ... */ });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Runner
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { createRunner } from "libretto";
|
|
115
|
+
|
|
116
|
+
const runner = createRunner({
|
|
117
|
+
llmClient, // optional — enables AI recovery & extraction
|
|
118
|
+
dryRun: false, // run in dry-run mode
|
|
119
|
+
debug: false, // enable debug mode
|
|
120
|
+
logDir: "./logs", // defaults to .libretto/sessions/<sessionName>/logs
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await runner.run(page, steps);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The runner executes steps sequentially. For each step it:
|
|
127
|
+
1. Captures a start screenshot
|
|
128
|
+
2. Runs the handler with automatic popup recovery (if `llmClient` provided)
|
|
129
|
+
3. Falls back to custom recovery handlers on failure
|
|
130
|
+
4. Generates a debug bundle if all recovery fails
|
|
131
|
+
5. Captures an end screenshot
|
|
132
|
+
|
|
133
|
+
## LLM Client Interface
|
|
134
|
+
|
|
135
|
+
Provide your own implementation backed by any LLM provider:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import type { LLMClient } from "libretto";
|
|
139
|
+
|
|
140
|
+
const myLLMClient: LLMClient = {
|
|
141
|
+
async generateObject({ prompt, schema, temperature }) {
|
|
142
|
+
// Call your LLM, return parsed + validated result
|
|
143
|
+
},
|
|
144
|
+
async generateObjectFromMessages({ messages, schema, temperature }) {
|
|
145
|
+
// Call your LLM with message history, return parsed + validated result
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Data Extraction
|
|
151
|
+
|
|
152
|
+
Extract structured data from a page using AI vision:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { extractFromPage } from "libretto/extract";
|
|
156
|
+
import { z } from "zod";
|
|
157
|
+
|
|
158
|
+
const result = await extractFromPage(page, llmClient, {
|
|
159
|
+
prompt: "Extract the product name and price from this page",
|
|
160
|
+
schema: z.object({
|
|
161
|
+
name: z.string(),
|
|
162
|
+
price: z.number(),
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
// result is typed as { name: string; price: number }
|
|
13
166
|
```
|
|
14
167
|
|
|
15
|
-
|
|
168
|
+
## Error Detection
|
|
169
|
+
|
|
170
|
+
Detect and classify form submission errors:
|
|
16
171
|
|
|
172
|
+
```typescript
|
|
173
|
+
import { detectSubmissionError } from "libretto/recovery";
|
|
17
174
|
|
|
18
|
-
|
|
19
|
-
|
|
175
|
+
const error = await detectSubmissionError(page, llmClient, [
|
|
176
|
+
{ name: "duplicate-entry", description: "Record already exists" },
|
|
177
|
+
{ name: "invalid-field", description: "A form field has a validation error" },
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
if (error) {
|
|
181
|
+
console.log(error.name, error.details);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
20
184
|
|
|
21
|
-
|
|
185
|
+
## Logging
|
|
22
186
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
187
|
+
```typescript
|
|
188
|
+
import { Logger, createFileLogSink, prettyConsoleSink } from "libretto/logger";
|
|
189
|
+
|
|
190
|
+
const logger = new Logger()
|
|
191
|
+
.withSink(createFileLogSink({ filePath: "./app.log" }))
|
|
192
|
+
.withSink(prettyConsoleSink);
|
|
193
|
+
|
|
194
|
+
const scoped = logger.withScope("auth");
|
|
195
|
+
scoped.info("login attempt", { user: "alice" });
|
|
196
|
+
scoped.error("login failed", { reason: "bad password" });
|
|
197
|
+
```
|
|
27
198
|
|
|
28
|
-
|
|
199
|
+
## Module Exports
|
|
29
200
|
|
|
30
|
-
|
|
201
|
+
Libretto provides granular imports:
|
|
31
202
|
|
|
32
|
-
|
|
203
|
+
| Import | Contents |
|
|
204
|
+
| ------------------------ | --------------------------------------------- |
|
|
205
|
+
| `libretto` | Everything |
|
|
206
|
+
| `libretto/step` | `step`, `createRunner` |
|
|
207
|
+
| `libretto/logger` | `Logger`, sinks |
|
|
208
|
+
| `libretto/recovery` | `attemptWithRecovery`, `detectSubmissionError` |
|
|
209
|
+
| `libretto/extract` | `extractFromPage` |
|
|
210
|
+
| `libretto/network` | `pageRequest` |
|
|
211
|
+
| `libretto/debug` | `debugPause` |
|
|
212
|
+
| `libretto/config` | `isDryRun`, `isDebugMode`, etc. |
|
|
213
|
+
|
|
214
|
+
## Configuration
|
|
215
|
+
|
|
216
|
+
Runtime flags can be set via runner config or environment variables:
|
|
217
|
+
|
|
218
|
+
| Env Variable | Effect |
|
|
219
|
+
| --------------------- | ------------------------ |
|
|
220
|
+
| `LIBRETTO_DEBUG` | Enable debug mode |
|
|
221
|
+
| `LIBRETTO_DRY_RUN` | Enable dry-run mode |
|
|
222
|
+
|
|
223
|
+
## Development
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
pnpm install
|
|
227
|
+
pnpm build # compile to dist/
|
|
228
|
+
pnpm type-check # typecheck without emitting
|
|
33
229
|
```
|
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,201 @@
|
|
|
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
|
+
"resume",
|
|
27
|
+
"close",
|
|
28
|
+
"--help",
|
|
29
|
+
"-h",
|
|
30
|
+
"help"
|
|
31
|
+
]);
|
|
32
|
+
function printUsage() {
|
|
33
|
+
console.log(`Usage: libretto-cli <command> [--session <name>]
|
|
34
|
+
|
|
35
|
+
Commands:
|
|
36
|
+
open <url> [--headless] Launch browser and open URL (headed by default)
|
|
37
|
+
Automatically loads saved profile if available
|
|
38
|
+
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)
|
|
39
|
+
ai configure [preset] [-- <command prefix...>] Configure AI runtime for analysis commands
|
|
40
|
+
save <url|domain> Save current browser session (cookies, localStorage, etc.)
|
|
41
|
+
exec <code> [--visualize] Execute Playwright typescript code (--visualize enables ghost cursor + highlight)
|
|
42
|
+
snapshot [--objective <text> --context <text>] Capture PNG + HTML; analyze when objective is provided (context optional)
|
|
43
|
+
network [--last N] [--filter regex] [--method M] [--clear] View captured network requests
|
|
44
|
+
actions [--last N] [--filter regex] [--action TYPE] [--source SOURCE] [--clear] View captured actions
|
|
45
|
+
resume Resume a paused workflow in the active session
|
|
46
|
+
close Close the browser
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--session <name> Use a named session (default: "default")
|
|
50
|
+
Built-in sessions: default, dev-server, browser-agent
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
libretto-cli open https://linkedin.com
|
|
54
|
+
|
|
55
|
+
# ... manually log in ...
|
|
56
|
+
libretto-cli save linkedin.com
|
|
57
|
+
# Next time you open linkedin.com, you'll be logged in automatically
|
|
58
|
+
|
|
59
|
+
libretto-cli exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
|
|
60
|
+
libretto-cli exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
|
|
61
|
+
libretto-cli ai configure codex
|
|
62
|
+
libretto-cli ai configure claude
|
|
63
|
+
libretto-cli ai configure gemini
|
|
64
|
+
libretto-cli ai configure <codex|claude|gemini> -- <command prefix...>
|
|
65
|
+
libretto-cli snapshot
|
|
66
|
+
libretto-cli snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
67
|
+
libretto-cli resume --session default
|
|
68
|
+
libretto-cli close
|
|
69
|
+
|
|
70
|
+
# Multiple sessions
|
|
71
|
+
libretto-cli open https://site1.com --session test1
|
|
72
|
+
libretto-cli open https://site2.com --session test2
|
|
73
|
+
libretto-cli exec "return await page.title()" --session test1
|
|
74
|
+
|
|
75
|
+
Available in exec:
|
|
76
|
+
page, context, state, browser, networkLog, actionLog
|
|
77
|
+
|
|
78
|
+
Profiles:
|
|
79
|
+
Profiles are saved to .libretto/profiles/<domain>.json (git-ignored)
|
|
80
|
+
They persist cookies, localStorage, and session data across browser launches.
|
|
81
|
+
Local profiles are machine-local and are not shared with other users/environments.
|
|
82
|
+
Sessions can expire; if run fails auth, log in again and re-save the profile.
|
|
83
|
+
|
|
84
|
+
Sessions:
|
|
85
|
+
Session state is stored in .libretto/sessions/<session>/state.json
|
|
86
|
+
CLI logs are stored in .libretto/sessions/<session>/logs.jsonl
|
|
87
|
+
Each session runs an isolated browser instance on a dynamic port.
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
function filterSessionArgs(args) {
|
|
91
|
+
const result = [];
|
|
92
|
+
for (let i = 0; i < args.length; i++) {
|
|
93
|
+
if (args[i] === "--session") {
|
|
94
|
+
i++;
|
|
95
|
+
} else {
|
|
96
|
+
result.push(args[i]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
function parseSessionForLog(rawArgs) {
|
|
102
|
+
const idx = rawArgs.indexOf("--session");
|
|
103
|
+
if (idx < 0) return SESSION_DEFAULT;
|
|
104
|
+
const value = rawArgs[idx + 1];
|
|
105
|
+
if (!value || value.startsWith("--") || CLI_COMMANDS.has(value)) {
|
|
106
|
+
return SESSION_DEFAULT;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
validateSessionName(value);
|
|
110
|
+
return value;
|
|
111
|
+
} catch {
|
|
112
|
+
return SESSION_DEFAULT;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function validateLegacySessionArg(rawArgs) {
|
|
116
|
+
const idx = rawArgs.indexOf("--session");
|
|
117
|
+
if (idx < 0) return;
|
|
118
|
+
const value = rawArgs[idx + 1];
|
|
119
|
+
if (!value || value.startsWith("--") || CLI_COMMANDS.has(value)) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"Usage: libretto-cli <command> [--session <name>]\nMissing or invalid --session value."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
validateSessionName(value);
|
|
125
|
+
}
|
|
126
|
+
function initializeLogger(rawArgs) {
|
|
127
|
+
const sessionForLog = parseSessionForLog(rawArgs);
|
|
128
|
+
const logger = createLoggerForSession(sessionForLog);
|
|
129
|
+
logger.info("cli-start", {
|
|
130
|
+
args: rawArgs,
|
|
131
|
+
cwd: process.cwd(),
|
|
132
|
+
session: sessionForLog
|
|
133
|
+
});
|
|
134
|
+
return logger;
|
|
135
|
+
}
|
|
136
|
+
async function withCliLogger(rawArgs, run) {
|
|
137
|
+
const logger = initializeLogger(rawArgs);
|
|
138
|
+
try {
|
|
139
|
+
return await run(logger);
|
|
140
|
+
} finally {
|
|
141
|
+
await closeLogger(logger);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function createParser(logger) {
|
|
145
|
+
let parser = yargs(hideBin(process.argv)).scriptName("libretto-cli").parserConfiguration({ "populate--": true }).option("session", {
|
|
146
|
+
type: "string",
|
|
147
|
+
default: SESSION_DEFAULT,
|
|
148
|
+
describe: "Use a named session",
|
|
149
|
+
global: true
|
|
150
|
+
}).middleware((argv) => {
|
|
151
|
+
validateSessionName(String(argv.session));
|
|
152
|
+
}).exitProcess(false).help(false).version(false).fail((msg, err) => {
|
|
153
|
+
if (err) throw err;
|
|
154
|
+
throw new Error(msg || "Command failed");
|
|
155
|
+
});
|
|
156
|
+
parser = registerBrowserCommands(parser, logger);
|
|
157
|
+
parser = registerExecutionCommands(parser, logger);
|
|
158
|
+
parser = registerLogCommands(parser);
|
|
159
|
+
parser = registerAICommands(parser);
|
|
160
|
+
parser = registerSnapshotCommands(parser, logger);
|
|
161
|
+
parser = parser.command("help", "Show usage", () => {
|
|
162
|
+
}, () => {
|
|
163
|
+
printUsage();
|
|
164
|
+
});
|
|
165
|
+
return parser;
|
|
166
|
+
}
|
|
167
|
+
async function runLibrettoCLI() {
|
|
168
|
+
const rawArgs = process.argv.slice(2);
|
|
169
|
+
let exitCode = 0;
|
|
170
|
+
ensureLibrettoSetup();
|
|
171
|
+
await withCliLogger(rawArgs, async (logger) => {
|
|
172
|
+
try {
|
|
173
|
+
validateLegacySessionArg(rawArgs);
|
|
174
|
+
const args = filterSessionArgs(rawArgs);
|
|
175
|
+
const command = args[0];
|
|
176
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
177
|
+
printUsage();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!CLI_COMMANDS.has(command)) {
|
|
181
|
+
console.error(`Unknown command: ${command}
|
|
182
|
+
`);
|
|
183
|
+
printUsage();
|
|
184
|
+
exitCode = 1;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const parser = createParser(logger);
|
|
188
|
+
logger.info("cli-command", { command, args });
|
|
189
|
+
await parser.parseAsync();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
logger.error("cli-error", { error: err, args: rawArgs });
|
|
192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
193
|
+
console.error(message);
|
|
194
|
+
exitCode = 1;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
process.exit(exitCode);
|
|
198
|
+
}
|
|
199
|
+
export {
|
|
200
|
+
runLibrettoCLI
|
|
201
|
+
};
|
|
@@ -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,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runClose as runCloseWithLogger,
|
|
3
|
+
runOpen,
|
|
4
|
+
runSave
|
|
5
|
+
} from "../core/browser.js";
|
|
6
|
+
import { withSessionLogger } from "../core/context.js";
|
|
7
|
+
function registerBrowserCommands(yargs, logger) {
|
|
8
|
+
return yargs.command(
|
|
9
|
+
"open [url]",
|
|
10
|
+
"Launch browser and open URL (headed by default)",
|
|
11
|
+
(cmd) => cmd.option("headed", {
|
|
12
|
+
type: "boolean",
|
|
13
|
+
default: false
|
|
14
|
+
}).option("headless", {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
default: false
|
|
17
|
+
}),
|
|
18
|
+
async (argv) => {
|
|
19
|
+
const hasHeadedFlag = Boolean(argv.headed);
|
|
20
|
+
const hasHeadlessFlag = Boolean(argv.headless);
|
|
21
|
+
if (hasHeadedFlag && hasHeadlessFlag) {
|
|
22
|
+
throw new Error("Cannot pass both --headed and --headless.");
|
|
23
|
+
}
|
|
24
|
+
const headed = hasHeadedFlag || !hasHeadlessFlag;
|
|
25
|
+
const url = argv.url;
|
|
26
|
+
if (!url) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Usage: libretto-cli open <url> [--headless] [--session <name>]"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
await runOpen(url, headed, String(argv.session), logger);
|
|
32
|
+
}
|
|
33
|
+
).command(
|
|
34
|
+
"save [urlOrDomain]",
|
|
35
|
+
"Save current browser session",
|
|
36
|
+
(cmd) => cmd,
|
|
37
|
+
async (argv) => {
|
|
38
|
+
const urlOrDomain = argv.urlOrDomain;
|
|
39
|
+
if (!urlOrDomain) {
|
|
40
|
+
throw new Error("Usage: libretto-cli save <url|domain> [--session <name>]");
|
|
41
|
+
}
|
|
42
|
+
await runSave(urlOrDomain, String(argv.session), logger);
|
|
43
|
+
}
|
|
44
|
+
).command("close", "Close the browser", (cmd) => cmd, async (argv) => {
|
|
45
|
+
await runCloseWithLogger(String(argv.session), logger);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function runClose(session) {
|
|
49
|
+
await withSessionLogger(session, async (logger) => {
|
|
50
|
+
await runCloseWithLogger(session, logger);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
registerBrowserCommands,
|
|
55
|
+
runClose
|
|
56
|
+
};
|