libretto 0.2.3 → 0.2.5
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 +78 -184
- package/dist/cli/cli.js +8 -2
- package/dist/cli/commands/browser.js +26 -3
- package/dist/cli/commands/execution.js +50 -11
- package/dist/cli/commands/init.js +95 -0
- package/dist/cli/core/browser.js +131 -6
- package/dist/cli/core/context.js +5 -0
- package/dist/cli/core/session.js +13 -13
- package/dist/cli/workers/run-integration-runtime.js +64 -59
- package/dist/cli/workers/run-integration-worker-protocol.js +12 -0
- package/dist/cli/workers/run-integration-worker.js +13 -30
- package/dist/index.cjs +5 -12
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +5 -15
- package/dist/shared/debug/index.cjs +4 -6
- package/dist/shared/debug/index.d.cts +1 -2
- package/dist/shared/debug/index.d.ts +1 -2
- package/dist/shared/debug/index.js +3 -8
- package/dist/shared/debug/pause.cjs +58 -24
- package/dist/shared/debug/pause.d.cts +13 -20
- package/dist/shared/debug/pause.d.ts +13 -20
- package/dist/shared/debug/pause.js +46 -21
- package/dist/shared/llm/ai-sdk-adapter.cjs +67 -0
- package/dist/shared/llm/ai-sdk-adapter.d.cts +22 -0
- package/dist/shared/llm/ai-sdk-adapter.d.ts +22 -0
- package/dist/shared/llm/ai-sdk-adapter.js +43 -0
- package/dist/shared/llm/index.cjs +5 -2
- package/dist/shared/llm/index.d.cts +2 -0
- package/dist/shared/llm/index.d.ts +2 -0
- package/dist/shared/llm/index.js +3 -1
- package/dist/shared/llm/types.d.cts +32 -0
- package/dist/shared/llm/types.d.ts +32 -0
- package/dist/shared/run/api.cjs +0 -7
- package/dist/shared/run/api.d.cts +0 -1
- package/dist/shared/run/api.d.ts +0 -1
- package/dist/shared/run/api.js +0 -8
- package/dist/shared/workflow/workflow.d.cts +11 -24
- package/dist/shared/workflow/workflow.d.ts +11 -24
- package/package.json +4 -10
- package/skill/SKILL.md +18 -5
- package/skill/code-generation-rules.md +7 -10
package/README.md
CHANGED
|
@@ -1,231 +1,125 @@
|
|
|
1
1
|
# Libretto
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
3
|
+
AI-powered browser automation library and CLI built on Playwright.
|
|
15
4
|
|
|
16
5
|
## Installation
|
|
17
6
|
|
|
18
7
|
```bash
|
|
19
8
|
pnpm add libretto playwright zod
|
|
9
|
+
npx libretto init
|
|
20
10
|
```
|
|
21
11
|
|
|
22
|
-
`
|
|
12
|
+
> **pnpm users:** add `onlyBuiltDependencies` to your `package.json` to allow
|
|
13
|
+
> Playwright's postinstall script to run:
|
|
14
|
+
>
|
|
15
|
+
> ```jsonc
|
|
16
|
+
> // package.json
|
|
17
|
+
> {
|
|
18
|
+
> "pnpm": {
|
|
19
|
+
> "onlyBuiltDependencies": ["playwright"]
|
|
20
|
+
> }
|
|
21
|
+
> }
|
|
22
|
+
> ```
|
|
23
23
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
26
|
-
|
|
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
|
|
26
|
+
### 1. Configure your LLM
|
|
44
27
|
|
|
45
|
-
|
|
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.
|
|
28
|
+
The easiest way is to use the built-in Vercel AI SDK adapter with any compatible provider:
|
|
50
29
|
|
|
51
30
|
```typescript
|
|
52
|
-
import {
|
|
31
|
+
import { createLLMClientFromModel } from "libretto/llm";
|
|
32
|
+
import { openai } from "@ai-sdk/openai";
|
|
53
33
|
|
|
54
|
-
|
|
55
|
-
await page.click('button[type="submit"]');
|
|
56
|
-
}, undefined, llmClient);
|
|
34
|
+
const llmClient = createLLMClientFromModel(openai("gpt-4o"));
|
|
57
35
|
```
|
|
58
36
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Runs a multi-step vision-based recovery agent that takes screenshots and executes browser actions (click, type, scroll, etc.) to resolve obstacles.
|
|
37
|
+
Or use any other provider:
|
|
62
38
|
|
|
63
39
|
```typescript
|
|
64
|
-
import {
|
|
65
|
-
|
|
66
|
-
await executeRecoveryAgent(
|
|
67
|
-
page,
|
|
68
|
-
"Close the cookie consent banner",
|
|
69
|
-
undefined,
|
|
70
|
-
llmClient,
|
|
71
|
-
);
|
|
72
|
-
```
|
|
40
|
+
import { createLLMClientFromModel } from "libretto";
|
|
41
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
73
42
|
|
|
74
|
-
|
|
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
|
-
}
|
|
43
|
+
const llmClient = createLLMClientFromModel(anthropic("claude-sonnet-4-20250514"));
|
|
89
44
|
```
|
|
90
45
|
|
|
91
|
-
|
|
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");
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
#### `downloadAndSave(page, selector, options?)`
|
|
150
|
-
|
|
151
|
-
Same as `downloadViaClick` but also writes the file to disk.
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
import { downloadAndSave } from "libretto";
|
|
155
|
-
|
|
156
|
-
const { savedTo } = await downloadAndSave(page, "#export-csv", {
|
|
157
|
-
savePath: "./exports/report.csv",
|
|
158
|
-
});
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## LLM Client Interface
|
|
162
|
-
|
|
163
|
-
Provide your own implementation backed by any LLM provider:
|
|
46
|
+
You can also implement the `LLMClient` interface directly for full control:
|
|
164
47
|
|
|
165
48
|
```typescript
|
|
166
49
|
import type { LLMClient } from "libretto";
|
|
167
50
|
|
|
168
|
-
const
|
|
51
|
+
const llmClient: LLMClient = {
|
|
169
52
|
async generateObject({ prompt, schema, temperature }) {
|
|
170
53
|
// Call your LLM, return parsed + validated result
|
|
171
54
|
},
|
|
172
55
|
async generateObjectFromMessages({ messages, schema, temperature }) {
|
|
173
|
-
// Call your LLM with message history
|
|
56
|
+
// Call your LLM with message history (may include images)
|
|
174
57
|
},
|
|
175
58
|
};
|
|
176
59
|
```
|
|
177
60
|
|
|
178
|
-
|
|
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:
|
|
61
|
+
### 2. Write a workflow
|
|
183
62
|
|
|
184
63
|
```typescript
|
|
185
|
-
import {
|
|
64
|
+
import { workflow } from "libretto";
|
|
65
|
+
import { z } from "zod";
|
|
186
66
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
67
|
+
export default workflow({
|
|
68
|
+
name: "extract-product",
|
|
69
|
+
schema: z.object({ url: z.string() }),
|
|
70
|
+
handler: async (ctx) => {
|
|
71
|
+
const page = ctx.page;
|
|
72
|
+
await page.goto(ctx.params.url);
|
|
190
73
|
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
74
|
+
const data = await ctx.extract({
|
|
75
|
+
instruction: "Extract the product name and price",
|
|
76
|
+
schema: z.object({ name: z.string(), price: z.number() }),
|
|
77
|
+
});
|
|
195
78
|
|
|
196
|
-
|
|
79
|
+
return data;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
197
83
|
|
|
198
|
-
|
|
84
|
+
### 3. Run it
|
|
199
85
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 |
|
|
86
|
+
```bash
|
|
87
|
+
npx libretto run ./workflows/extract-product.ts extractProduct \
|
|
88
|
+
--params '{"url": "https://example.com/product"}'
|
|
89
|
+
```
|
|
215
90
|
|
|
216
|
-
##
|
|
91
|
+
## CLI Commands
|
|
217
92
|
|
|
218
|
-
|
|
93
|
+
```
|
|
94
|
+
npx libretto init # Copy skills, install Playwright Chromium
|
|
95
|
+
npx libretto open <url> # Launch browser and open URL
|
|
96
|
+
npx libretto run <file> <export> # Run a workflow
|
|
97
|
+
npx libretto ai configure <preset> # Configure AI runtime (codex, claude, gemini)
|
|
98
|
+
npx libretto snapshot # Capture page screenshot + HTML
|
|
99
|
+
npx libretto exec <code> # Execute Playwright code
|
|
100
|
+
```
|
|
219
101
|
|
|
220
|
-
|
|
221
|
-
| --------------------- | --------------------------------------------------- |
|
|
222
|
-
| `LIBRETTO_DEBUG` | Enable debug mode |
|
|
223
|
-
| `LIBRETTO_DRY_RUN` | Enable dry-run mode (defaults to `true` in development) |
|
|
102
|
+
Run `npx libretto help` for the full list.
|
|
224
103
|
|
|
225
|
-
##
|
|
104
|
+
## Module Exports
|
|
226
105
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
106
|
+
| Import | Contents |
|
|
107
|
+
| -------------------------- | ------------------------------------------------------------- |
|
|
108
|
+
| `libretto` | Everything |
|
|
109
|
+
| `libretto/llm` | `LLMClient` type, `createLLMClient`, `createLLMClientFromModel` |
|
|
110
|
+
| `libretto/recovery` | `attemptWithRecovery`, `executeRecoveryAgent`, `detectSubmissionError` |
|
|
111
|
+
| `libretto/extract` | `extractFromPage` |
|
|
112
|
+
| `libretto/network` | `pageRequest` |
|
|
113
|
+
| `libretto/download` | `downloadViaClick`, `downloadAndSave` |
|
|
114
|
+
| `libretto/logger` | `Logger`, `defaultLogger`, sinks |
|
|
115
|
+
| `libretto/debug` | `debugPause` |
|
|
116
|
+
| `libretto/config` | `isDryRun`, `isDebugMode`, `shouldPauseBeforeMutation` |
|
|
117
|
+
| `libretto/instrumentation` | `instrumentPage`, `installInstrumentation` |
|
|
118
|
+
| `libretto/visualization` | Ghost cursor and highlight helpers |
|
|
119
|
+
| `libretto/run` | `launchBrowser` |
|
|
120
|
+
| `libretto/state` | Session state serialization and parsing |
|
|
121
|
+
|
|
122
|
+
## Links
|
|
123
|
+
|
|
124
|
+
- [GitHub](https://github.com/saffron-health/libretto)
|
|
125
|
+
- [Issues](https://github.com/saffron-health/libretto/issues)
|
package/dist/cli/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { registerAICommands } from "./commands/ai.js";
|
|
|
4
4
|
import { registerBrowserCommands } from "./commands/browser.js";
|
|
5
5
|
import { registerExecutionCommands } from "./commands/execution.js";
|
|
6
6
|
import { registerLogCommands } from "./commands/logs.js";
|
|
7
|
+
import { registerInitCommand } from "./commands/init.js";
|
|
7
8
|
import { registerSnapshotCommands } from "./commands/snapshot.js";
|
|
8
9
|
import {
|
|
9
10
|
closeLogger,
|
|
@@ -26,6 +27,7 @@ const CLI_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
26
27
|
"pages",
|
|
27
28
|
"resume",
|
|
28
29
|
"close",
|
|
30
|
+
"init",
|
|
29
31
|
"--help",
|
|
30
32
|
"-h",
|
|
31
33
|
"help"
|
|
@@ -34,9 +36,10 @@ function printUsage() {
|
|
|
34
36
|
console.log(`Usage: libretto-cli <command> [--session <name>]
|
|
35
37
|
|
|
36
38
|
Commands:
|
|
39
|
+
init [--skip-browsers] Initialize libretto (copy skills, install browsers)
|
|
37
40
|
open <url> [--headless] Launch browser and open URL (headed by default)
|
|
38
41
|
Automatically loads saved profile if available
|
|
39
|
-
run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless]
|
|
42
|
+
run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless] Run an exported Libretto workflow from a file
|
|
40
43
|
ai configure [preset] [-- <command prefix...>] Configure AI runtime for analysis commands
|
|
41
44
|
save <url|domain> Save current browser session (cookies, localStorage, etc.)
|
|
42
45
|
exec <code> [--visualize] Execute Playwright typescript code (--visualize enables ghost cursor + highlight)
|
|
@@ -45,7 +48,7 @@ Commands:
|
|
|
45
48
|
actions [--last N] [--filter regex] [--action TYPE] [--source SOURCE] [--clear] View captured actions
|
|
46
49
|
pages List open pages in the active session
|
|
47
50
|
resume Resume a paused workflow in the active session
|
|
48
|
-
close
|
|
51
|
+
close [--all] [--force] Close the browser for the session, or all tracked sessions with --all
|
|
49
52
|
|
|
50
53
|
Options:
|
|
51
54
|
--session <name> Use a named session (default: "default")
|
|
@@ -68,6 +71,8 @@ Examples:
|
|
|
68
71
|
libretto-cli snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
69
72
|
libretto-cli resume --session default
|
|
70
73
|
libretto-cli close
|
|
74
|
+
libretto-cli close --all
|
|
75
|
+
libretto-cli close --all --force
|
|
71
76
|
|
|
72
77
|
# Multiple sessions
|
|
73
78
|
libretto-cli open https://site1.com --session test1
|
|
@@ -160,6 +165,7 @@ function createParser(logger) {
|
|
|
160
165
|
parser = registerLogCommands(parser);
|
|
161
166
|
parser = registerAICommands(parser);
|
|
162
167
|
parser = registerSnapshotCommands(parser, logger);
|
|
168
|
+
parser = registerInitCommand(parser);
|
|
163
169
|
parser = parser.command("help", "Show usage", () => {
|
|
164
170
|
}, () => {
|
|
165
171
|
printUsage();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runClose as runCloseWithLogger,
|
|
3
|
+
runCloseAll as runCloseAllWithLogger,
|
|
3
4
|
runOpen,
|
|
4
5
|
runPages,
|
|
5
6
|
runSave
|
|
@@ -44,9 +45,31 @@ function registerBrowserCommands(yargs, logger) {
|
|
|
44
45
|
}
|
|
45
46
|
).command("pages", "List open pages in the session", (cmd) => cmd, async (argv) => {
|
|
46
47
|
await runPages(String(argv.session), logger);
|
|
47
|
-
}).command(
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
}).command(
|
|
49
|
+
"close",
|
|
50
|
+
"Close the browser",
|
|
51
|
+
(cmd) => cmd.option("all", {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
default: false,
|
|
54
|
+
describe: "Close all tracked sessions in this workspace"
|
|
55
|
+
}).option("force", {
|
|
56
|
+
type: "boolean",
|
|
57
|
+
default: false,
|
|
58
|
+
describe: "Force kill sessions that ignore SIGTERM (requires --all)"
|
|
59
|
+
}),
|
|
60
|
+
async (argv) => {
|
|
61
|
+
const closeAll = Boolean(argv.all);
|
|
62
|
+
const force = Boolean(argv.force);
|
|
63
|
+
if (force && !closeAll) {
|
|
64
|
+
throw new Error("Usage: libretto-cli close --all [--force]");
|
|
65
|
+
}
|
|
66
|
+
if (closeAll) {
|
|
67
|
+
await runCloseAllWithLogger(logger, { force });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await runCloseWithLogger(String(argv.session), logger);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
50
73
|
}
|
|
51
74
|
async function runClose(session) {
|
|
52
75
|
await withSessionLogger(session, async (logger) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
import * as moduleBuiltin from "node:module";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { installInstrumentation } from "../../shared/instrumentation/index.js";
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
11
11
|
import {
|
|
12
12
|
assertSessionAvailableForStart,
|
|
13
|
+
clearSessionState,
|
|
14
|
+
readSessionState,
|
|
13
15
|
readSessionStateOrThrow,
|
|
14
16
|
setSessionStatus
|
|
15
17
|
} from "../core/session.js";
|
|
@@ -170,6 +172,35 @@ function isProcessRunning(pid) {
|
|
|
170
172
|
return false;
|
|
171
173
|
}
|
|
172
174
|
}
|
|
175
|
+
async function stopExistingFailedRunSession(session, logger) {
|
|
176
|
+
const existingState = readSessionState(session, logger);
|
|
177
|
+
if (!existingState || existingState.status !== "failed") {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
logger.info("run-release-existing-failed-session", {
|
|
181
|
+
session,
|
|
182
|
+
pid: existingState.pid,
|
|
183
|
+
port: existingState.port
|
|
184
|
+
});
|
|
185
|
+
clearSessionState(session, logger);
|
|
186
|
+
const stopDeadline = Date.now() + 3e3;
|
|
187
|
+
while (isProcessRunning(existingState.pid) && Date.now() < stopDeadline) {
|
|
188
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
189
|
+
}
|
|
190
|
+
if (isProcessRunning(existingState.pid)) {
|
|
191
|
+
logger.warn("run-release-existing-failed-session-timeout", {
|
|
192
|
+
session,
|
|
193
|
+
pid: existingState.pid
|
|
194
|
+
});
|
|
195
|
+
console.warn(
|
|
196
|
+
`Existing failed workflow process for session "${session}" (pid ${existingState.pid}) is still shutting down; continuing.`
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
console.log(
|
|
201
|
+
`Closed existing failed workflow process for session "${session}" (pid ${existingState.pid}).`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
173
204
|
function readJsonFileIfExists(path) {
|
|
174
205
|
if (!existsSync(path)) return null;
|
|
175
206
|
try {
|
|
@@ -246,7 +277,7 @@ async function runResume(session, logger) {
|
|
|
246
277
|
} = getPauseSignalPaths(session);
|
|
247
278
|
if (!existsSync(pausedSignalPath)) {
|
|
248
279
|
throw new Error(
|
|
249
|
-
`Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call
|
|
280
|
+
`Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call pause() first.`
|
|
250
281
|
);
|
|
251
282
|
}
|
|
252
283
|
if (!isProcessRunning(state.pid)) {
|
|
@@ -297,6 +328,7 @@ async function runResume(session, logger) {
|
|
|
297
328
|
console.log("Workflow paused.");
|
|
298
329
|
}
|
|
299
330
|
async function runIntegrationFromFile(args, logger) {
|
|
331
|
+
await stopExistingFailedRunSession(args.session, logger);
|
|
300
332
|
assertSessionAvailableForStart(args.session, logger);
|
|
301
333
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
302
334
|
clearSignalIfExists(signalPaths.pausedSignalPath);
|
|
@@ -308,12 +340,11 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
308
340
|
new URL("../workers/run-integration-worker.js", import.meta.url)
|
|
309
341
|
);
|
|
310
342
|
const payload = JSON.stringify(args);
|
|
311
|
-
const worker =
|
|
343
|
+
const worker = spawn(process.execPath, [workerEntryPath, payload], {
|
|
312
344
|
detached: true,
|
|
313
|
-
stdio:
|
|
345
|
+
stdio: "ignore",
|
|
314
346
|
env: process.env
|
|
315
347
|
});
|
|
316
|
-
worker.disconnect();
|
|
317
348
|
worker.unref();
|
|
318
349
|
const outcome = await waitForWorkflowOutcome({
|
|
319
350
|
session: args.session,
|
|
@@ -326,7 +357,10 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
326
357
|
}
|
|
327
358
|
if (outcome.status === "failed") {
|
|
328
359
|
setSessionStatus(args.session, "failed", logger);
|
|
329
|
-
throw new Error(
|
|
360
|
+
throw new Error(
|
|
361
|
+
`${outcome.message ?? "Workflow failed during run."}
|
|
362
|
+
Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-run the workflow.`
|
|
363
|
+
);
|
|
330
364
|
}
|
|
331
365
|
if (outcome.status === "exited") {
|
|
332
366
|
setSessionStatus(args.session, "exited", logger);
|
|
@@ -360,11 +394,17 @@ function registerExecutionCommands(yargs, logger) {
|
|
|
360
394
|
).command(
|
|
361
395
|
"run [integrationFile] [integrationExport]",
|
|
362
396
|
"Run an exported Libretto workflow from a file",
|
|
363
|
-
(cmd) => cmd.option("params", { type: "string" }).option("params-file", { type: "string" }).option("headed", { type: "boolean", default: false }).option("headless", { type: "boolean", default: false }).option("
|
|
397
|
+
(cmd) => cmd.option("params", { type: "string" }).option("params-file", { type: "string" }).option("headed", { type: "boolean", default: false }).option("headless", { type: "boolean", default: false }).option("auth-profile", { type: "string", describe: "Domain for local auth profile (e.g. apps.example.com)" }),
|
|
364
398
|
async (argv) => {
|
|
365
|
-
const usage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless]
|
|
399
|
+
const usage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless]";
|
|
366
400
|
const integrationPath = argv.integrationFile;
|
|
367
401
|
const exportName = argv.integrationExport;
|
|
402
|
+
const legacyDebug = argv.debug;
|
|
403
|
+
if (legacyDebug !== void 0) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
"The --debug flag has been removed. Run the command without --debug."
|
|
406
|
+
);
|
|
407
|
+
}
|
|
368
408
|
if (!integrationPath || !exportName) {
|
|
369
409
|
throw new Error(usage);
|
|
370
410
|
}
|
|
@@ -397,15 +437,14 @@ function registerExecutionCommands(yargs, logger) {
|
|
|
397
437
|
throw new Error("Cannot pass both --headed and --headless.");
|
|
398
438
|
}
|
|
399
439
|
const headlessMode = hasHeadedFlag ? false : hasHeadlessFlag ? true : void 0;
|
|
400
|
-
const
|
|
401
|
-
const debugMode = debugFlag !== void 0 ? debugFlag : process.env.LIBRETTO_DEBUG === "true";
|
|
440
|
+
const authProfileDomain = argv["auth-profile"];
|
|
402
441
|
await runIntegrationFromFile({
|
|
403
442
|
integrationPath,
|
|
404
443
|
exportName,
|
|
405
444
|
session,
|
|
406
445
|
params,
|
|
407
446
|
headless: headlessMode ?? false,
|
|
408
|
-
|
|
447
|
+
authProfileDomain
|
|
409
448
|
}, logger);
|
|
410
449
|
}
|
|
411
450
|
).command(
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, cpSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { REPO_ROOT } from "../core/context.js";
|
|
6
|
+
function getSkillSourceDir() {
|
|
7
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkgRoot = join(thisDir, "..", "..", "..");
|
|
9
|
+
const skillDir = join(pkgRoot, "skill");
|
|
10
|
+
if (existsSync(skillDir)) return skillDir;
|
|
11
|
+
const skillsDir = join(pkgRoot, "skills");
|
|
12
|
+
if (existsSync(skillsDir)) return skillsDir;
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Could not find skill/ or skills/ directory in the libretto package."
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
function copySkills() {
|
|
18
|
+
const src = getSkillSourceDir();
|
|
19
|
+
const files = readdirSync(src);
|
|
20
|
+
if (files.length === 0) {
|
|
21
|
+
console.log(" No skill files found to copy.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const targets = [
|
|
25
|
+
join(REPO_ROOT, ".agents", "skills", "libretto"),
|
|
26
|
+
join(REPO_ROOT, ".claude", "skills", "libretto")
|
|
27
|
+
];
|
|
28
|
+
for (const target of targets) {
|
|
29
|
+
mkdirSync(target, { recursive: true });
|
|
30
|
+
cpSync(src, target, { recursive: true });
|
|
31
|
+
console.log(` \u2713 Copied skill files to ${target}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function installBrowsers() {
|
|
35
|
+
console.log("\nInstalling Playwright Chromium...");
|
|
36
|
+
const result = spawnSync("npx", ["playwright", "install", "chromium"], {
|
|
37
|
+
stdio: "inherit",
|
|
38
|
+
shell: true
|
|
39
|
+
});
|
|
40
|
+
if (result.status === 0) {
|
|
41
|
+
console.log(" \u2713 Playwright Chromium installed");
|
|
42
|
+
} else {
|
|
43
|
+
console.error(
|
|
44
|
+
" \u2717 Failed to install Playwright Chromium. Run manually: npx playwright install chromium"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function checkSnapshotLLM() {
|
|
49
|
+
const hasAnyCreds = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
|
|
50
|
+
console.log("\nSnapshot LLM configuration:");
|
|
51
|
+
if (hasAnyCreds) {
|
|
52
|
+
console.log(" \u2713 LLM credentials detected");
|
|
53
|
+
} else {
|
|
54
|
+
console.log(" \u2717 No LLM credentials found.");
|
|
55
|
+
console.log(" Set one of the following environment variables:");
|
|
56
|
+
console.log(" GOOGLE_CLOUD_PROJECT (for Vertex AI / Gemini)");
|
|
57
|
+
console.log(" ANTHROPIC_API_KEY (for Claude)");
|
|
58
|
+
console.log(" OPENAI_API_KEY (for GPT)");
|
|
59
|
+
console.log(
|
|
60
|
+
" Then configure via: npx libretto ai configure <preset>"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function registerInitCommand(yargs) {
|
|
65
|
+
return yargs.command(
|
|
66
|
+
"init",
|
|
67
|
+
"Initialize libretto in the current project",
|
|
68
|
+
(cmd) => cmd.option("skip-browsers", {
|
|
69
|
+
type: "boolean",
|
|
70
|
+
default: false,
|
|
71
|
+
describe: "Skip Playwright Chromium installation"
|
|
72
|
+
}),
|
|
73
|
+
(argv) => {
|
|
74
|
+
console.log("Initializing libretto...\n");
|
|
75
|
+
console.log("Copying skill files...");
|
|
76
|
+
try {
|
|
77
|
+
copySkills();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(
|
|
80
|
+
` \u2717 ${err instanceof Error ? err.message : String(err)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (!argv["skip-browsers"]) {
|
|
84
|
+
installBrowsers();
|
|
85
|
+
} else {
|
|
86
|
+
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
87
|
+
}
|
|
88
|
+
checkSnapshotLLM();
|
|
89
|
+
console.log("\n\u2713 libretto init complete");
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
registerInitCommand
|
|
95
|
+
};
|