libretto 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -125
- package/dist/cli/cli.js +298 -0
- package/dist/cli/commands/ai.js +21 -0
- package/dist/cli/commands/browser.js +103 -0
- package/dist/cli/commands/execution.js +490 -0
- package/dist/cli/commands/init.js +166 -0
- package/dist/cli/commands/logs.js +93 -0
- package/dist/cli/commands/snapshot.js +217 -0
- package/dist/cli/core/ai-config.js +156 -0
- package/dist/cli/core/browser.js +669 -0
- package/dist/cli/core/context.js +117 -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 +570 -0
- package/dist/cli/core/telemetry.js +362 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/workers/run-integration-runtime.js +234 -0
- package/dist/cli/workers/run-integration-worker-protocol.js +12 -0
- package/dist/cli/workers/run-integration-worker.js +67 -0
- package/dist/index.cjs +144 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +114 -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 +30 -0
- package/dist/shared/debug/index.d.cts +1 -0
- package/dist/shared/debug/index.d.ts +1 -0
- package/dist/shared/debug/index.js +5 -0
- package/dist/shared/debug/pause.cjs +90 -0
- package/dist/shared/debug/pause.d.cts +16 -0
- package/dist/shared/debug/pause.d.ts +16 -0
- package/dist/shared/debug/pause.js +55 -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/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/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 +31 -0
- package/dist/shared/llm/index.d.cts +5 -0
- package/dist/shared/llm/index.d.ts +5 -0
- package/dist/shared/llm/index.js +6 -0
- package/dist/shared/llm/types.cjs +16 -0
- package/dist/shared/llm/types.d.cts +66 -0
- package/dist/shared/llm/types.d.ts +66 -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 +232 -0
- package/dist/shared/logger/logger.d.cts +86 -0
- package/dist/shared/logger/logger.d.ts +86 -0
- package/dist/shared/logger/logger.js +207 -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 +28 -0
- package/dist/shared/run/api.d.cts +2 -0
- package/dist/shared/run/api.d.ts +2 -0
- package/dist/shared/run/api.js +4 -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 +92 -0
- package/dist/shared/state/session-state.d.cts +40 -0
- package/dist/shared/state/session-state.d.ts +40 -0
- package/dist/shared/state/session-state.js +62 -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 +21 -0
- package/dist/shared/workflow/workflow.d.ts +21 -0
- package/dist/shared/workflow/workflow.js +21 -0
- package/package.json +37 -95
- package/bin/libretto.mjs +0 -18
- package/scripts/postinstall.mjs +0 -48
- /package/{skill → .agents/skills/libretto}/SKILL.md +0 -0
- /package/{skill → .agents/skills/libretto}/code-generation-rules.md +0 -0
- /package/{skill → .agents/skills/libretto}/integration-approach-selection.md +0 -0
package/README.md
CHANGED
|
@@ -1,156 +1,82 @@
|
|
|
1
1
|
# Libretto
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Libretto gives your coding agent superpowers for building, debugging, and maintaining browser RPA integrations.
|
|
4
|
+
|
|
5
|
+
It is designed for engineering teams that automate workflows in web apps and want to move from brittle browser-only scripts to faster, more reliable network-first integrations.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
|
|
9
|
-
npx libretto init
|
|
10
|
+
npm install --save-dev libretto
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
> `libretto` and `playwright` to allow their postinstall scripts to run
|
|
14
|
-
> (libretto's postinstall copies skill files and installs Playwright Chromium):
|
|
15
|
-
>
|
|
16
|
-
> ```jsonc
|
|
17
|
-
> // package.json
|
|
18
|
-
> {
|
|
19
|
-
> "pnpm": {
|
|
20
|
-
> "onlyBuiltDependencies": ["libretto", "playwright"]
|
|
21
|
-
> }
|
|
22
|
-
> }
|
|
23
|
-
> ```
|
|
24
|
-
>
|
|
25
|
-
> If the postinstall was skipped (e.g., `libretto` wasn't in the allowlist),
|
|
26
|
-
> run `npx libretto init` manually after install to complete setup.
|
|
27
|
-
|
|
28
|
-
## Quick Start
|
|
29
|
-
|
|
30
|
-
### 1. Configure your LLM
|
|
31
|
-
|
|
32
|
-
The easiest way is to use the built-in Vercel AI SDK adapter with any compatible provider:
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
import { createLLMClientFromModel } from "libretto/llm";
|
|
36
|
-
import { openai } from "@ai-sdk/openai";
|
|
37
|
-
|
|
38
|
-
const llmClient = createLLMClientFromModel(openai("gpt-4o"));
|
|
39
|
-
```
|
|
13
|
+
Chromium is downloaded automatically via a `postinstall` script. If postinstall scripts are disabled (e.g. `--ignore-scripts`, common in monorepos), run init manually:
|
|
40
14
|
|
|
41
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npx libretto init
|
|
17
|
+
```
|
|
42
18
|
|
|
43
|
-
|
|
44
|
-
import { createLLMClientFromModel } from "libretto";
|
|
45
|
-
import { anthropic } from "@ai-sdk/anthropic";
|
|
19
|
+
This installs the Chromium browser binary and optionally configures an AI subagent (Gemini, Claude, or Codex) that can analyze page snapshots without consuming the coding agent's context window.
|
|
46
20
|
|
|
47
|
-
|
|
48
|
-
```
|
|
21
|
+
## Usage
|
|
49
22
|
|
|
50
|
-
|
|
23
|
+
Libretto is usually used through prompts with the Libretto skill.
|
|
51
24
|
|
|
52
|
-
|
|
53
|
-
import type { LLMClient } from "libretto";
|
|
25
|
+
### One-shot script generation
|
|
54
26
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// Call your LLM, return parsed + validated result
|
|
58
|
-
},
|
|
59
|
-
async generateObjectFromMessages({ messages, schema, temperature }) {
|
|
60
|
-
// Call your LLM with message history (may include images)
|
|
61
|
-
},
|
|
62
|
-
};
|
|
27
|
+
```text
|
|
28
|
+
Use the Libretto skill. Go on LinkedIn and scrape the first 10 posts for content, who posted it, the number of reactions, the first 25 comments, and the first 25 reposts.
|
|
63
29
|
```
|
|
64
30
|
|
|
65
|
-
###
|
|
31
|
+
### Interactive script building
|
|
66
32
|
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
|
|
33
|
+
```text
|
|
34
|
+
Use the Libretto skill. Let's interactively build a script to scrape scheduling info from the eClinicalWorks EHR.
|
|
35
|
+
```
|
|
70
36
|
|
|
71
|
-
|
|
72
|
-
name: "extract-product",
|
|
73
|
-
schema: z.object({ url: z.string() }),
|
|
74
|
-
handler: async (ctx) => {
|
|
75
|
-
const page = ctx.page;
|
|
76
|
-
await page.goto(ctx.params.url);
|
|
37
|
+
### Convert browser automation to network requests
|
|
77
38
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
});
|
|
39
|
+
```text
|
|
40
|
+
We have a browser script at ./integration.ts that automates going to Hacker News and getting the first 10 posts. Convert it to direct network scripts instead. Use the Libretto skill.
|
|
41
|
+
```
|
|
82
42
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
43
|
+
### Fix broken integrations
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
We have a browser script at ./integration.ts that is supposed to go to Availity and perform an eligibility check for a patient. But I'm getting a broken selector error when I run it. Fix it. Use the Libretto skill.
|
|
86
47
|
```
|
|
87
48
|
|
|
88
|
-
|
|
49
|
+
You can also run workflows directly from the CLI:
|
|
89
50
|
|
|
90
51
|
```bash
|
|
91
|
-
npx libretto
|
|
92
|
-
|
|
52
|
+
npx libretto help
|
|
53
|
+
npx libretto run ./integration.ts main
|
|
93
54
|
```
|
|
94
55
|
|
|
95
|
-
##
|
|
56
|
+
## The `.libretto/` directory
|
|
96
57
|
|
|
97
|
-
|
|
98
|
-
npx libretto init # Copy skills, install Playwright Chromium
|
|
99
|
-
npx libretto open <url> # Launch browser and open URL
|
|
100
|
-
npx libretto run <file> <export> # Run a workflow
|
|
101
|
-
npx libretto ai configure <preset> # Configure AI runtime (codex, claude, gemini)
|
|
102
|
-
npx libretto snapshot # Capture page screenshot + HTML
|
|
103
|
-
npx libretto exec <code> # Execute Playwright code
|
|
104
|
-
```
|
|
58
|
+
Libretto stores local runtime state in a `.libretto/` directory at your project root. Sensitive directories (`sessions/` and `profiles/`) are automatically git-ignored via `.libretto/.gitignore`.
|
|
105
59
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
| `libretto/recovery` | `attemptWithRecovery`, `executeRecoveryAgent`, `detectSubmissionError` |
|
|
115
|
-
| `libretto/extract` | `extractFromPage` |
|
|
116
|
-
| `libretto/network` | `pageRequest` |
|
|
117
|
-
| `libretto/download` | `downloadViaClick`, `downloadAndSave` |
|
|
118
|
-
| `libretto/logger` | `Logger`, `defaultLogger`, sinks |
|
|
119
|
-
| `libretto/debug` | `debugPause` |
|
|
120
|
-
| `libretto/config` | `isDryRun`, `isDebugMode`, `shouldPauseBeforeMutation` |
|
|
121
|
-
| `libretto/instrumentation` | `instrumentPage`, `installInstrumentation` |
|
|
122
|
-
| `libretto/visualization` | Ghost cursor and highlight helpers |
|
|
123
|
-
| `libretto/run` | `launchBrowser` |
|
|
124
|
-
| `libretto/state` | Session state serialization and parsing |
|
|
125
|
-
|
|
126
|
-
## Using Recovery Helpers
|
|
127
|
-
|
|
128
|
-
The recovery module (`libretto/recovery`) provides `detectSubmissionError` and
|
|
129
|
-
`executeRecoveryAgent` for handling form submission errors. Both accept an
|
|
130
|
-
`LLMClient` — create one with `createLLMClientFromModel` and pass it directly:
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
import { detectSubmissionError, executeRecoveryAgent } from "libretto/recovery";
|
|
134
|
-
import { createLLMClientFromModel } from "libretto/llm";
|
|
135
|
-
import { openai } from "@ai-sdk/openai";
|
|
136
|
-
|
|
137
|
-
const llmClient = createLLMClientFromModel(openai("gpt-4o"));
|
|
138
|
-
|
|
139
|
-
// Detect if a submission produced an error
|
|
140
|
-
const error = await detectSubmissionError(
|
|
141
|
-
page, submissionError, "eligibility check failed", llmClient, knownErrors, logger,
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
// Or run the full recovery agent to retry with corrections
|
|
145
|
-
const result = await executeRecoveryAgent(
|
|
146
|
-
page, error, llmClient, recoveryOptions, logger,
|
|
147
|
-
);
|
|
148
|
-
```
|
|
60
|
+
- **`profiles/<domain>.json`** — Saved browser sessions (cookies, localStorage) for authenticated sites. Created via `npx libretto save <domain>`. Machine-local and never committed.
|
|
61
|
+
- **`sessions/<name>/`** — Per-session runtime state:
|
|
62
|
+
- `state.json` — Session metadata (debug port, PID, status)
|
|
63
|
+
- `logs.jsonl` — Structured session logs
|
|
64
|
+
- `network.jsonl` — Captured network requests (URLs, methods, headers, response status)
|
|
65
|
+
- `actions.jsonl` — Recorded user actions (clicks, fills, navigations)
|
|
66
|
+
- `snapshots/` — Screenshot PNGs and HTML snapshots captured via `npx libretto snapshot`
|
|
67
|
+
- **`ai.json`** — AI runtime configuration set via `npx libretto ai configure`.
|
|
149
68
|
|
|
150
|
-
|
|
151
|
-
Vercel AI SDK provider into the `LLMClient` interface that recovery helpers expect.
|
|
69
|
+
## Authors
|
|
152
70
|
|
|
153
|
-
|
|
71
|
+
Maintained by the team at [Saffron Health](https://saffron.health).
|
|
154
72
|
|
|
155
|
-
|
|
156
|
-
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
For local development in this repository:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pnpm i
|
|
79
|
+
pnpm build
|
|
80
|
+
pnpm type-check
|
|
81
|
+
pnpm test
|
|
82
|
+
```
|
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
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 { registerInitCommand } from "./commands/init.js";
|
|
8
|
+
import { registerSnapshotCommands } from "./commands/snapshot.js";
|
|
9
|
+
import {
|
|
10
|
+
closeLogger,
|
|
11
|
+
createLoggerForSession,
|
|
12
|
+
ensureLibrettoSetup
|
|
13
|
+
} from "./core/context.js";
|
|
14
|
+
import {
|
|
15
|
+
listSessionsWithStateFile,
|
|
16
|
+
validateSessionName
|
|
17
|
+
} from "./core/session.js";
|
|
18
|
+
const AUTO_SESSION_COMMANDS = /* @__PURE__ */ new Set(["open", "run"]);
|
|
19
|
+
const SESSION_OPTIONAL_COMMANDS = /* @__PURE__ */ new Set(["help", "--help", "-h", "init", "ai"]);
|
|
20
|
+
const CLI_COMMANDS = /* @__PURE__ */ new Set([
|
|
21
|
+
"open",
|
|
22
|
+
"run",
|
|
23
|
+
"ai",
|
|
24
|
+
"save",
|
|
25
|
+
"exec",
|
|
26
|
+
"snapshot",
|
|
27
|
+
"network",
|
|
28
|
+
"actions",
|
|
29
|
+
"pages",
|
|
30
|
+
"resume",
|
|
31
|
+
"close",
|
|
32
|
+
"init",
|
|
33
|
+
"--help",
|
|
34
|
+
"-h",
|
|
35
|
+
"help"
|
|
36
|
+
]);
|
|
37
|
+
function printUsage() {
|
|
38
|
+
console.log(`Usage: libretto-cli <command> [--session <name>]
|
|
39
|
+
|
|
40
|
+
Commands:
|
|
41
|
+
init [--skip-browsers] Initialize libretto (install browsers, check AI setup)
|
|
42
|
+
open <url> [--headless] Launch browser and open URL (headed by default)
|
|
43
|
+
Automatically loads saved profile if available
|
|
44
|
+
run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] Run an exported Libretto workflow from a file
|
|
45
|
+
ai configure [preset] [-- <command prefix...>] Configure AI runtime for analysis commands
|
|
46
|
+
save <url|domain> Save current browser session (cookies, localStorage, etc.)
|
|
47
|
+
exec <code> [--visualize] Execute Playwright typescript code (--visualize enables ghost cursor + highlight)
|
|
48
|
+
snapshot [--objective <text> --context <text>] Capture PNG + HTML; analyze when objective is provided (context optional)
|
|
49
|
+
network [--last N] [--filter regex] [--method M] [--clear] View captured network requests
|
|
50
|
+
actions [--last N] [--filter regex] [--action TYPE] [--source SOURCE] [--clear] View captured actions
|
|
51
|
+
pages List open pages in the active session
|
|
52
|
+
resume Resume a paused workflow in the active session
|
|
53
|
+
close [--all] [--force] Close the browser for the session, or all tracked sessions with --all
|
|
54
|
+
|
|
55
|
+
Options:
|
|
56
|
+
--session <name> Use a named session
|
|
57
|
+
If omitted for open/run, a session id is auto-generated
|
|
58
|
+
All other stateful commands require --session
|
|
59
|
+
Built-in sessions: default, dev-server, browser-agent
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
libretto-cli open https://linkedin.com
|
|
63
|
+
|
|
64
|
+
# ... manually log in ...
|
|
65
|
+
libretto-cli save linkedin.com
|
|
66
|
+
# Next time you open linkedin.com, you'll be logged in automatically
|
|
67
|
+
|
|
68
|
+
libretto-cli exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
|
|
69
|
+
libretto-cli exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
|
|
70
|
+
libretto-cli ai configure codex
|
|
71
|
+
libretto-cli ai configure claude
|
|
72
|
+
libretto-cli ai configure gemini
|
|
73
|
+
libretto-cli ai configure <codex|claude|gemini> -- <command prefix...>
|
|
74
|
+
libretto-cli snapshot
|
|
75
|
+
libretto-cli snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
76
|
+
libretto-cli resume --session default
|
|
77
|
+
libretto-cli close
|
|
78
|
+
libretto-cli close --all
|
|
79
|
+
libretto-cli close --all --force
|
|
80
|
+
|
|
81
|
+
# Multiple sessions
|
|
82
|
+
libretto-cli open https://site1.com --session test1
|
|
83
|
+
libretto-cli open https://site2.com --session test2
|
|
84
|
+
libretto-cli exec "return await page.title()" --session test1
|
|
85
|
+
|
|
86
|
+
Available in exec:
|
|
87
|
+
page, context, state, browser, networkLog, actionLog
|
|
88
|
+
|
|
89
|
+
Profiles:
|
|
90
|
+
Profiles are saved to .libretto/profiles/<domain>.json (git-ignored)
|
|
91
|
+
They persist cookies, localStorage, and session data across browser launches.
|
|
92
|
+
Local profiles are machine-local and are not shared with other users/environments.
|
|
93
|
+
Sessions can expire; if run fails auth, log in again and re-save the profile.
|
|
94
|
+
|
|
95
|
+
Sessions:
|
|
96
|
+
Session state is stored in .libretto/sessions/<session>/state.json
|
|
97
|
+
CLI logs are stored in .libretto/sessions/<session>/logs.jsonl
|
|
98
|
+
Each session runs an isolated browser instance on a dynamic port.
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
function filterSessionArgs(args) {
|
|
102
|
+
const result = [];
|
|
103
|
+
for (let i = 0; i < args.length; i++) {
|
|
104
|
+
if (args[i] === "--session") {
|
|
105
|
+
i++;
|
|
106
|
+
} else {
|
|
107
|
+
result.push(args[i]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function parseSessionForLog(rawArgs) {
|
|
113
|
+
const idx = rawArgs.indexOf("--session");
|
|
114
|
+
if (idx < 0) return null;
|
|
115
|
+
const value = rawArgs[idx + 1];
|
|
116
|
+
if (!value || value.startsWith("--") || CLI_COMMANDS.has(value)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
validateSessionName(value);
|
|
121
|
+
return value;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function hasExplicitSession(rawArgs) {
|
|
127
|
+
return rawArgs.includes("--session");
|
|
128
|
+
}
|
|
129
|
+
function randomSessionId() {
|
|
130
|
+
const digits = Math.floor(Math.random() * 1e4).toString().padStart(4, "0");
|
|
131
|
+
return `ses-${digits}`;
|
|
132
|
+
}
|
|
133
|
+
function generateSessionId() {
|
|
134
|
+
const activeSessions = new Set(listSessionsWithStateFile());
|
|
135
|
+
for (let attempt = 0; attempt < 1e4; attempt += 1) {
|
|
136
|
+
const candidate = randomSessionId();
|
|
137
|
+
if (!activeSessions.has(candidate)) {
|
|
138
|
+
return candidate;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Could not generate an available session id. Close an existing session and try again."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
function hasExecCodeArg(filteredArgs) {
|
|
146
|
+
for (let i = 1; i < filteredArgs.length; i += 1) {
|
|
147
|
+
const token = filteredArgs[i];
|
|
148
|
+
if (!token) continue;
|
|
149
|
+
if (token === "--") {
|
|
150
|
+
return filteredArgs.length > i + 1;
|
|
151
|
+
}
|
|
152
|
+
if (token === "--visualize") {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (token === "--page") {
|
|
156
|
+
const maybeValue = filteredArgs[i + 1];
|
|
157
|
+
if (maybeValue && !maybeValue.startsWith("--")) {
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (token.startsWith("--page=")) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (token.startsWith("-")) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
function commandNeedsSession(command, rawArgs, filteredArgs) {
|
|
173
|
+
if (AUTO_SESSION_COMMANDS.has(command)) return false;
|
|
174
|
+
if (SESSION_OPTIONAL_COMMANDS.has(command)) return false;
|
|
175
|
+
if (command === "close" && rawArgs.includes("--all")) return false;
|
|
176
|
+
if (command === "close" && rawArgs.includes("--force")) return false;
|
|
177
|
+
if (command === "exec" && !hasExecCodeArg(filteredArgs)) return false;
|
|
178
|
+
if (command === "save" && filteredArgs.length <= 1) return false;
|
|
179
|
+
if (!CLI_COMMANDS.has(command)) return false;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
function resolveSessionArgs(rawArgs) {
|
|
183
|
+
const filtered = filterSessionArgs(rawArgs);
|
|
184
|
+
const command = filtered[0];
|
|
185
|
+
const explicitSession = parseSessionForLog(rawArgs);
|
|
186
|
+
if (!command) {
|
|
187
|
+
return {
|
|
188
|
+
args: rawArgs,
|
|
189
|
+
generatedSession: null,
|
|
190
|
+
resolvedSession: explicitSession
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (hasExplicitSession(rawArgs)) {
|
|
194
|
+
return {
|
|
195
|
+
args: rawArgs,
|
|
196
|
+
generatedSession: null,
|
|
197
|
+
resolvedSession: explicitSession
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (!AUTO_SESSION_COMMANDS.has(command)) {
|
|
201
|
+
return {
|
|
202
|
+
args: rawArgs,
|
|
203
|
+
generatedSession: null,
|
|
204
|
+
resolvedSession: null
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const generatedSession = generateSessionId();
|
|
208
|
+
return {
|
|
209
|
+
args: [...rawArgs, "--session", generatedSession],
|
|
210
|
+
generatedSession,
|
|
211
|
+
resolvedSession: generatedSession
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function createParser(logger) {
|
|
215
|
+
let parser = yargs(hideBin(process.argv)).scriptName("libretto-cli").parserConfiguration({ "populate--": true }).option("session", {
|
|
216
|
+
type: "string",
|
|
217
|
+
describe: "Use a named session",
|
|
218
|
+
global: true,
|
|
219
|
+
requiresArg: true
|
|
220
|
+
}).middleware((argv) => {
|
|
221
|
+
if (argv.session !== void 0) {
|
|
222
|
+
validateSessionName(String(argv.session));
|
|
223
|
+
}
|
|
224
|
+
}).exitProcess(false).help(false).version(false).fail((msg, err) => {
|
|
225
|
+
if (err) throw err;
|
|
226
|
+
throw new Error(msg || "Command failed");
|
|
227
|
+
});
|
|
228
|
+
parser = registerBrowserCommands(parser, logger);
|
|
229
|
+
parser = registerExecutionCommands(parser, logger);
|
|
230
|
+
parser = registerLogCommands(parser);
|
|
231
|
+
parser = registerAICommands(parser);
|
|
232
|
+
parser = registerSnapshotCommands(parser, logger);
|
|
233
|
+
parser = registerInitCommand(parser);
|
|
234
|
+
parser = parser.command("help", "Show usage", () => {
|
|
235
|
+
}, () => {
|
|
236
|
+
printUsage();
|
|
237
|
+
});
|
|
238
|
+
return parser;
|
|
239
|
+
}
|
|
240
|
+
async function runLibrettoCLI() {
|
|
241
|
+
const rawArgs = process.argv.slice(2);
|
|
242
|
+
let exitCode = 0;
|
|
243
|
+
let effectiveArgs = rawArgs;
|
|
244
|
+
let generatedSession = null;
|
|
245
|
+
let resolvedSession = null;
|
|
246
|
+
({
|
|
247
|
+
args: effectiveArgs,
|
|
248
|
+
generatedSession,
|
|
249
|
+
resolvedSession
|
|
250
|
+
} = resolveSessionArgs(rawArgs));
|
|
251
|
+
ensureLibrettoSetup();
|
|
252
|
+
const args = filterSessionArgs(effectiveArgs);
|
|
253
|
+
const command = args[0];
|
|
254
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
255
|
+
printUsage();
|
|
256
|
+
process.exit(exitCode);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (!CLI_COMMANDS.has(command)) {
|
|
260
|
+
console.error(`Unknown command: ${command}
|
|
261
|
+
`);
|
|
262
|
+
printUsage();
|
|
263
|
+
process.exit(1);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!hasExplicitSession(effectiveArgs) && commandNeedsSession(command, effectiveArgs, args)) {
|
|
267
|
+
console.error(
|
|
268
|
+
[
|
|
269
|
+
`Missing required --session for "${command}".`,
|
|
270
|
+
"Pass --session <name>, or use open/run without --session to auto-create one."
|
|
271
|
+
].join("\n")
|
|
272
|
+
);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const sessionForLogger = resolvedSession ?? "cli";
|
|
277
|
+
const logger = createLoggerForSession(sessionForLogger);
|
|
278
|
+
try {
|
|
279
|
+
const parser = createParser(logger);
|
|
280
|
+
await parser.parseAsync(effectiveArgs);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
logger.error("cli-error", {
|
|
283
|
+
error: err,
|
|
284
|
+
args: rawArgs,
|
|
285
|
+
effectiveArgs,
|
|
286
|
+
generatedSession
|
|
287
|
+
});
|
|
288
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
289
|
+
console.error(message);
|
|
290
|
+
exitCode = 1;
|
|
291
|
+
} finally {
|
|
292
|
+
await closeLogger(logger);
|
|
293
|
+
}
|
|
294
|
+
process.exit(exitCode);
|
|
295
|
+
}
|
|
296
|
+
export {
|
|
297
|
+
runLibrettoCLI
|
|
298
|
+
};
|
|
@@ -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,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runClose as runCloseWithLogger,
|
|
3
|
+
runCloseAll as runCloseAllWithLogger,
|
|
4
|
+
runOpen,
|
|
5
|
+
runPages,
|
|
6
|
+
runSave
|
|
7
|
+
} from "../core/browser.js";
|
|
8
|
+
import { withSessionLogger } from "../core/context.js";
|
|
9
|
+
function registerBrowserCommands(yargs, logger) {
|
|
10
|
+
return yargs.command(
|
|
11
|
+
"open [url]",
|
|
12
|
+
"Launch browser and open URL (headed by default)",
|
|
13
|
+
(cmd) => cmd.option("headed", {
|
|
14
|
+
type: "boolean",
|
|
15
|
+
default: false
|
|
16
|
+
}).option("headless", {
|
|
17
|
+
type: "boolean",
|
|
18
|
+
default: false
|
|
19
|
+
}).option("viewport", {
|
|
20
|
+
type: "string",
|
|
21
|
+
describe: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)"
|
|
22
|
+
}),
|
|
23
|
+
async (argv) => {
|
|
24
|
+
const hasHeadedFlag = Boolean(argv.headed);
|
|
25
|
+
const hasHeadlessFlag = Boolean(argv.headless);
|
|
26
|
+
if (hasHeadedFlag && hasHeadlessFlag) {
|
|
27
|
+
throw new Error("Cannot pass both --headed and --headless.");
|
|
28
|
+
}
|
|
29
|
+
const headed = hasHeadedFlag || !hasHeadlessFlag;
|
|
30
|
+
const url = argv.url;
|
|
31
|
+
if (!url) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"Usage: libretto-cli open <url> [--headless] [--viewport WxH] [--session <name>]"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const viewportArg = argv.viewport;
|
|
37
|
+
let viewport;
|
|
38
|
+
if (viewportArg) {
|
|
39
|
+
const match = viewportArg.match(/^(\d+)x(\d+)$/i);
|
|
40
|
+
if (!match) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Invalid --viewport format. Expected WIDTHxHEIGHT (e.g. 1920x1080)."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const w = Number(match[1]);
|
|
46
|
+
const h = Number(match[2]);
|
|
47
|
+
if (w < 1 || h < 1) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"Invalid --viewport dimensions. Width and height must be at least 1."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
viewport = { width: w, height: h };
|
|
53
|
+
}
|
|
54
|
+
await runOpen(url, headed, String(argv.session), logger, { viewport });
|
|
55
|
+
}
|
|
56
|
+
).command(
|
|
57
|
+
"save [urlOrDomain]",
|
|
58
|
+
"Save current browser session",
|
|
59
|
+
(cmd) => cmd,
|
|
60
|
+
async (argv) => {
|
|
61
|
+
const urlOrDomain = argv.urlOrDomain;
|
|
62
|
+
if (!urlOrDomain) {
|
|
63
|
+
throw new Error("Usage: libretto-cli save <url|domain> [--session <name>]");
|
|
64
|
+
}
|
|
65
|
+
await runSave(urlOrDomain, String(argv.session), logger);
|
|
66
|
+
}
|
|
67
|
+
).command("pages", "List open pages in the session", (cmd) => cmd, async (argv) => {
|
|
68
|
+
await runPages(String(argv.session), logger);
|
|
69
|
+
}).command(
|
|
70
|
+
"close",
|
|
71
|
+
"Close the browser",
|
|
72
|
+
(cmd) => cmd.option("all", {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
default: false,
|
|
75
|
+
describe: "Close all tracked sessions in this workspace"
|
|
76
|
+
}).option("force", {
|
|
77
|
+
type: "boolean",
|
|
78
|
+
default: false,
|
|
79
|
+
describe: "Force kill sessions that ignore SIGTERM (requires --all)"
|
|
80
|
+
}),
|
|
81
|
+
async (argv) => {
|
|
82
|
+
const closeAll = Boolean(argv.all);
|
|
83
|
+
const force = Boolean(argv.force);
|
|
84
|
+
if (force && !closeAll) {
|
|
85
|
+
throw new Error("Usage: libretto-cli close --all [--force]");
|
|
86
|
+
}
|
|
87
|
+
if (closeAll) {
|
|
88
|
+
await runCloseAllWithLogger(logger, { force });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
await runCloseWithLogger(String(argv.session), logger);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
async function runClose(session) {
|
|
96
|
+
await withSessionLogger(session, async (logger) => {
|
|
97
|
+
await runCloseWithLogger(session, logger);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
registerBrowserCommands,
|
|
102
|
+
runClose
|
|
103
|
+
};
|