libretto 0.6.7 → 0.6.9
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 +26 -74
- package/README.template.md +26 -74
- package/dist/cli/commands/execution.js +13 -1
- package/dist/cli/commands/setup.js +12 -4
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/core/ai-model.js +12 -4
- package/dist/cli/core/browser-daemon.js +122 -0
- package/dist/cli/core/browser.js +54 -180
- package/dist/cli/core/config.js +1 -1
- package/dist/cli/core/providers/browserbase.js +1 -0
- package/dist/cli/core/providers/kernel.js +1 -0
- package/dist/cli/core/providers/libretto-cloud.js +9 -4
- package/dist/cli/core/resolve-model.js +20 -2
- package/dist/cli/workers/run-integration-runtime.js +3 -0
- package/dist/shared/dom-semantics.js +0 -1
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +14 -3
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/execution.ts +13 -1
- package/src/cli/commands/setup.ts +11 -3
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/core/ai-model.ts +10 -2
- package/src/cli/core/browser-daemon.ts +198 -0
- package/src/cli/core/browser.ts +50 -190
- package/src/cli/core/config.ts +1 -1
- package/src/cli/core/providers/browserbase.ts +1 -0
- package/src/cli/core/providers/kernel.ts +1 -0
- package/src/cli/core/providers/libretto-cloud.ts +18 -5
- package/src/cli/core/providers/types.ts +12 -1
- package/src/cli/core/resolve-model.ts +20 -2
- package/src/cli/workers/run-integration-runtime.ts +10 -0
- package/src/shared/dom-semantics.ts +0 -1
package/README.md
CHANGED
|
@@ -7,11 +7,6 @@
|
|
|
7
7
|
[](https://github.com/saffron-health/libretto/discussions)
|
|
8
8
|
[](https://discord.gg/NYrG56hVDt)
|
|
9
9
|
|
|
10
|
-
- Website: [libretto.sh](https://libretto.sh)
|
|
11
|
-
- Repository: [github.com/saffron-health/libretto](https://github.com/saffron-health/libretto)
|
|
12
|
-
- Docs: [libretto.sh/docs](https://libretto.sh/docs)
|
|
13
|
-
- Discord: [discord.gg/NYrG56hVDt](https://discord.gg/NYrG56hVDt)
|
|
14
|
-
|
|
15
10
|
Libretto is a toolkit for building robust web integrations. It gives your coding agent a live browser and a token-efficient CLI to:
|
|
16
11
|
|
|
17
12
|
- Inspect live pages with minimal context overhead
|
|
@@ -23,6 +18,13 @@ We at [Saffron Health](https://saffron.health) built Libretto to help us maintai
|
|
|
23
18
|
|
|
24
19
|
https://github.com/user-attachments/assets/9b9a0ab3-5133-4b20-b3be-459943349d18
|
|
25
20
|
|
|
21
|
+
### Quick Links
|
|
22
|
+
|
|
23
|
+
- Website: [libretto.sh](https://libretto.sh)
|
|
24
|
+
- Docs: [libretto.sh/docs](https://libretto.sh/docs)
|
|
25
|
+
- Repository: [github.com/saffron-health/libretto](https://github.com/saffron-health/libretto)
|
|
26
|
+
- Discord: [discord.gg/NYrG56hVDt](https://discord.gg/NYrG56hVDt)
|
|
27
|
+
|
|
26
28
|
## Installation
|
|
27
29
|
|
|
28
30
|
```bash
|
|
@@ -75,86 +77,29 @@ Agents can use Libretto to reproduce the failure, pause the workflow at any poin
|
|
|
75
77
|
You can also use Libretto directly from the command line. All commands accept `--session <name>` to target a specific session.
|
|
76
78
|
|
|
77
79
|
```bash
|
|
78
|
-
npx libretto
|
|
79
|
-
npx libretto
|
|
80
|
-
npx libretto
|
|
81
|
-
npx libretto snapshot --objective "..." --context "..." # capture PNG + HTML and analyze with an LLM
|
|
82
|
-
npx libretto exec "<code>" # execute Playwright TypeScript against the open page (single quoted argument)
|
|
83
|
-
echo "<code>" | npx libretto exec - # intentionally read Playwright TypeScript from stdin
|
|
84
|
-
npx libretto run <file> # run the file's default-exported workflow
|
|
85
|
-
npx libretto resume # resume a paused workflow
|
|
86
|
-
npx libretto pages # list open pages in the session
|
|
87
|
-
npx libretto save <domain> # save browser session (cookies, localStorage) for reuse
|
|
80
|
+
npx libretto open <url> # launch browser and open a URL
|
|
81
|
+
npx libretto snapshot --objective "..." # capture PNG + HTML and analyze with an LLM
|
|
82
|
+
npx libretto exec "<code>" # execute Playwright TypeScript against the open page
|
|
88
83
|
npx libretto close # close the browser
|
|
89
|
-
npx libretto ai configure <provider> # manually change snapshot analysis model
|
|
90
|
-
npx libretto status # show AI config and open sessions
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Configuration
|
|
94
|
-
|
|
95
|
-
All Libretto state lives in a `.libretto/` directory at your project root. Configuration is stored in `.libretto/config.json`.
|
|
96
|
-
|
|
97
|
-
### Config file
|
|
98
|
-
|
|
99
|
-
`.libretto/config.json` controls snapshot analysis and viewport settings:
|
|
100
|
-
|
|
101
|
-
```json
|
|
102
|
-
{
|
|
103
|
-
"version": 1,
|
|
104
|
-
"ai": {
|
|
105
|
-
"model": "openai/gpt-5.4",
|
|
106
|
-
"updatedAt": "2026-01-01T00:00:00.000Z"
|
|
107
|
-
},
|
|
108
|
-
"viewport": { "width": 1280, "height": 800 }
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
The `ai` field configures which model Libretto uses for snapshot analysis — extracting selectors, identifying interactive elements, or diagnosing why a step failed. This keeps heavy visual context out of your coding agent's context window. Snapshot analysis is required.
|
|
113
|
-
|
|
114
|
-
`npx libretto setup` automatically pins the default model for the first provider whose credentials it finds. To explicitly change the provider or model afterward:
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
npx libretto ai configure <openai | anthropic | gemini | vertex>
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
To inspect the current configuration without changing anything:
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
npx libretto status
|
|
124
84
|
```
|
|
125
85
|
|
|
126
|
-
|
|
86
|
+
Run `npx libretto help` for the full list of commands.
|
|
127
87
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
### Sessions
|
|
131
|
-
|
|
132
|
-
Each Libretto session gets its own directory under `.libretto/sessions/<name>/` containing runtime state. Sessions are git-ignored.
|
|
133
|
-
|
|
134
|
-
- `state.json` — session metadata (debug port, PID, status)
|
|
135
|
-
- `logs.jsonl` — structured session logs
|
|
136
|
-
- `network.jsonl` — captured network requests
|
|
137
|
-
- `actions.jsonl` — recorded user actions
|
|
138
|
-
- `snapshots/` — screenshot PNGs and HTML snapshots
|
|
139
|
-
|
|
140
|
-
### Profiles
|
|
88
|
+
## Configuration
|
|
141
89
|
|
|
142
|
-
|
|
90
|
+
All Libretto state lives in a `.libretto/` directory at your project root. See the [configuration docs](https://libretto.sh/docs/configuration) for details on config files, sessions, and profiles.
|
|
143
91
|
|
|
144
|
-
## Community
|
|
92
|
+
## Join the Community
|
|
145
93
|
|
|
146
|
-
|
|
94
|
+
Join our Discord to connect with other developers, get help, and share what you've built:
|
|
147
95
|
|
|
148
|
-
|
|
149
|
-
- **[Ideas](https://github.com/saffron-health/libretto/discussions/categories/ideas)** — Suggest new features or improvements
|
|
150
|
-
- **[Show and tell](https://github.com/saffron-health/libretto/discussions/categories/show-and-tell)** — Share your workflows and automations
|
|
151
|
-
- **[General](https://github.com/saffron-health/libretto/discussions/categories/general)** — Chat about anything Libretto-related
|
|
96
|
+
[](https://discord.gg/NYrG56hVDt)
|
|
152
97
|
|
|
153
|
-
Found a bug?
|
|
98
|
+
For longer-form threads, head to [GitHub Discussions](https://github.com/saffron-health/libretto/discussions). Found a bug? [Open an issue](https://github.com/saffron-health/libretto/issues/new).
|
|
154
99
|
|
|
155
|
-
##
|
|
100
|
+
## License
|
|
156
101
|
|
|
157
|
-
|
|
102
|
+
[MIT License](LICENSE) — use it freely in commercial and open-source projects.
|
|
158
103
|
|
|
159
104
|
## Development
|
|
160
105
|
|
|
@@ -179,3 +124,10 @@ Source layout:
|
|
|
179
124
|
Run `pnpm sync:mirrors` after editing `README.template.md` or anything under `skills/libretto/`.
|
|
180
125
|
|
|
181
126
|
To check that generated READMEs, skill mirrors, and skill version metadata are in sync without fixing them, run `pnpm check:mirrors`. To release, run `pnpm prepare-release`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
> [!NOTE]
|
|
131
|
+
> This is an early-stage project under active development. APIs may change before version 1.0. We recommend pinning to specific versions in production.
|
|
132
|
+
|
|
133
|
+
Built by the team at [Saffron Health](https://saffron.health).
|
package/README.template.md
CHANGED
|
@@ -5,11 +5,6 @@
|
|
|
5
5
|
[](https://github.com/saffron-health/libretto/discussions)
|
|
6
6
|
[](https://discord.gg/NYrG56hVDt)
|
|
7
7
|
|
|
8
|
-
- Website: [libretto.sh](https://libretto.sh)
|
|
9
|
-
- Repository: [github.com/saffron-health/libretto](https://github.com/saffron-health/libretto)
|
|
10
|
-
- Docs: [libretto.sh/docs](https://libretto.sh/docs)
|
|
11
|
-
- Discord: [discord.gg/NYrG56hVDt](https://discord.gg/NYrG56hVDt)
|
|
12
|
-
|
|
13
8
|
Libretto is a toolkit for building robust web integrations. It gives your coding agent a live browser and a token-efficient CLI to:
|
|
14
9
|
|
|
15
10
|
- Inspect live pages with minimal context overhead
|
|
@@ -21,6 +16,13 @@ We at [Saffron Health](https://saffron.health) built Libretto to help us maintai
|
|
|
21
16
|
|
|
22
17
|
https://github.com/user-attachments/assets/9b9a0ab3-5133-4b20-b3be-459943349d18
|
|
23
18
|
|
|
19
|
+
### Quick Links
|
|
20
|
+
|
|
21
|
+
- Website: [libretto.sh](https://libretto.sh)
|
|
22
|
+
- Docs: [libretto.sh/docs](https://libretto.sh/docs)
|
|
23
|
+
- Repository: [github.com/saffron-health/libretto](https://github.com/saffron-health/libretto)
|
|
24
|
+
- Discord: [discord.gg/NYrG56hVDt](https://discord.gg/NYrG56hVDt)
|
|
25
|
+
|
|
24
26
|
## Installation
|
|
25
27
|
|
|
26
28
|
```bash
|
|
@@ -73,86 +75,29 @@ Agents can use Libretto to reproduce the failure, pause the workflow at any poin
|
|
|
73
75
|
You can also use Libretto directly from the command line. All commands accept `--session <name>` to target a specific session.
|
|
74
76
|
|
|
75
77
|
```bash
|
|
76
|
-
npx libretto
|
|
77
|
-
npx libretto
|
|
78
|
-
npx libretto
|
|
79
|
-
npx libretto snapshot --objective "..." --context "..." # capture PNG + HTML and analyze with an LLM
|
|
80
|
-
npx libretto exec "<code>" # execute Playwright TypeScript against the open page (single quoted argument)
|
|
81
|
-
echo "<code>" | npx libretto exec - # intentionally read Playwright TypeScript from stdin
|
|
82
|
-
npx libretto run <file> # run the file's default-exported workflow
|
|
83
|
-
npx libretto resume # resume a paused workflow
|
|
84
|
-
npx libretto pages # list open pages in the session
|
|
85
|
-
npx libretto save <domain> # save browser session (cookies, localStorage) for reuse
|
|
78
|
+
npx libretto open <url> # launch browser and open a URL
|
|
79
|
+
npx libretto snapshot --objective "..." # capture PNG + HTML and analyze with an LLM
|
|
80
|
+
npx libretto exec "<code>" # execute Playwright TypeScript against the open page
|
|
86
81
|
npx libretto close # close the browser
|
|
87
|
-
npx libretto ai configure <provider> # manually change snapshot analysis model
|
|
88
|
-
npx libretto status # show AI config and open sessions
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## Configuration
|
|
92
|
-
|
|
93
|
-
All Libretto state lives in a `.libretto/` directory at your project root. Configuration is stored in `.libretto/config.json`.
|
|
94
|
-
|
|
95
|
-
### Config file
|
|
96
|
-
|
|
97
|
-
`.libretto/config.json` controls snapshot analysis and viewport settings:
|
|
98
|
-
|
|
99
|
-
```json
|
|
100
|
-
{
|
|
101
|
-
"version": 1,
|
|
102
|
-
"ai": {
|
|
103
|
-
"model": "openai/gpt-5.4",
|
|
104
|
-
"updatedAt": "2026-01-01T00:00:00.000Z"
|
|
105
|
-
},
|
|
106
|
-
"viewport": { "width": 1280, "height": 800 }
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
The `ai` field configures which model Libretto uses for snapshot analysis — extracting selectors, identifying interactive elements, or diagnosing why a step failed. This keeps heavy visual context out of your coding agent's context window. Snapshot analysis is required.
|
|
111
|
-
|
|
112
|
-
`npx libretto setup` automatically pins the default model for the first provider whose credentials it finds. To explicitly change the provider or model afterward:
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
npx libretto ai configure <openai | anthropic | gemini | vertex>
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
To inspect the current configuration without changing anything:
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
npx libretto status
|
|
122
82
|
```
|
|
123
83
|
|
|
124
|
-
|
|
84
|
+
Run `npx libretto help` for the full list of commands.
|
|
125
85
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
### Sessions
|
|
129
|
-
|
|
130
|
-
Each Libretto session gets its own directory under `.libretto/sessions/<name>/` containing runtime state. Sessions are git-ignored.
|
|
131
|
-
|
|
132
|
-
- `state.json` — session metadata (debug port, PID, status)
|
|
133
|
-
- `logs.jsonl` — structured session logs
|
|
134
|
-
- `network.jsonl` — captured network requests
|
|
135
|
-
- `actions.jsonl` — recorded user actions
|
|
136
|
-
- `snapshots/` — screenshot PNGs and HTML snapshots
|
|
137
|
-
|
|
138
|
-
### Profiles
|
|
86
|
+
## Configuration
|
|
139
87
|
|
|
140
|
-
|
|
88
|
+
All Libretto state lives in a `.libretto/` directory at your project root. See the [configuration docs](https://libretto.sh/docs/configuration) for details on config files, sessions, and profiles.
|
|
141
89
|
|
|
142
|
-
## Community
|
|
90
|
+
## Join the Community
|
|
143
91
|
|
|
144
|
-
|
|
92
|
+
Join our Discord to connect with other developers, get help, and share what you've built:
|
|
145
93
|
|
|
146
|
-
|
|
147
|
-
- **[Ideas](https://github.com/saffron-health/libretto/discussions/categories/ideas)** — Suggest new features or improvements
|
|
148
|
-
- **[Show and tell](https://github.com/saffron-health/libretto/discussions/categories/show-and-tell)** — Share your workflows and automations
|
|
149
|
-
- **[General](https://github.com/saffron-health/libretto/discussions/categories/general)** — Chat about anything Libretto-related
|
|
94
|
+
[](https://discord.gg/NYrG56hVDt)
|
|
150
95
|
|
|
151
|
-
Found a bug?
|
|
96
|
+
For longer-form threads, head to [GitHub Discussions](https://github.com/saffron-health/libretto/discussions). Found a bug? [Open an issue](https://github.com/saffron-health/libretto/issues/new).
|
|
152
97
|
|
|
153
|
-
##
|
|
98
|
+
## License
|
|
154
99
|
|
|
155
|
-
|
|
100
|
+
[MIT License](LICENSE) — use it freely in commercial and open-source projects.
|
|
156
101
|
|
|
157
102
|
## Development
|
|
158
103
|
|
|
@@ -177,3 +122,10 @@ Source layout:
|
|
|
177
122
|
Run `pnpm sync:mirrors` after editing `{{LIBRETTO_PATH_PREFIX}}README.template.md` or anything under `{{LIBRETTO_PATH_PREFIX}}skills/libretto/`.
|
|
178
123
|
|
|
179
124
|
To check that generated READMEs, skill mirrors, and skill version metadata are in sync without fixing them, run `pnpm check:mirrors`. To release, run `pnpm prepare-release`.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
> [!NOTE]
|
|
129
|
+
> This is an early-stage project under active development. APIs may change before version 1.0. We recommend pinning to specific versions in production.
|
|
130
|
+
|
|
131
|
+
Built by the team at [Saffron Health](https://saffron.health).
|
|
@@ -687,6 +687,15 @@ const runCommand = SimpleCLI.command({
|
|
|
687
687
|
`Creating ${providerName} browser session (session: ${ctx.session})...`
|
|
688
688
|
);
|
|
689
689
|
const providerSession = await provider.createSession();
|
|
690
|
+
ctx.logger.info("run-provider-session-created", {
|
|
691
|
+
provider: providerName,
|
|
692
|
+
sessionId: providerSession.sessionId,
|
|
693
|
+
cdpEndpoint: providerSession.cdpEndpoint,
|
|
694
|
+
liveViewUrl: providerSession.liveViewUrl
|
|
695
|
+
});
|
|
696
|
+
if (providerSession.liveViewUrl) {
|
|
697
|
+
console.log(`View live session: ${providerSession.liveViewUrl}`);
|
|
698
|
+
}
|
|
690
699
|
console.log(`Connecting to ${providerName} browser...`);
|
|
691
700
|
cdpEndpoint = providerSession.cdpEndpoint;
|
|
692
701
|
providerInfo = {
|
|
@@ -714,7 +723,10 @@ const runCommand = SimpleCLI.command({
|
|
|
714
723
|
} finally {
|
|
715
724
|
if (provider && providerInfo) {
|
|
716
725
|
try {
|
|
717
|
-
await provider.closeSession(providerInfo.sessionId);
|
|
726
|
+
const result = await provider.closeSession(providerInfo.sessionId);
|
|
727
|
+
if (result.replayUrl) {
|
|
728
|
+
console.log(`View recording: ${result.replayUrl}`);
|
|
729
|
+
}
|
|
718
730
|
} catch (cleanupErr) {
|
|
719
731
|
console.error(
|
|
720
732
|
`Failed to clean up ${providerInfo.name} session ${providerInfo.sessionId}:`,
|
|
@@ -23,7 +23,8 @@ const PROVIDER_SDK_PACKAGES = {
|
|
|
23
23
|
openai: "@ai-sdk/openai",
|
|
24
24
|
anthropic: "@ai-sdk/anthropic",
|
|
25
25
|
google: "@ai-sdk/google",
|
|
26
|
-
vertex: "@ai-sdk/google-vertex"
|
|
26
|
+
vertex: "@ai-sdk/google-vertex",
|
|
27
|
+
openrouter: "@ai-sdk/openai"
|
|
27
28
|
};
|
|
28
29
|
function detectPackageManager() {
|
|
29
30
|
if (existsSync(join(REPO_ROOT, "pnpm-lock.yaml"))) return "pnpm";
|
|
@@ -102,6 +103,13 @@ const PROVIDER_CHOICES = [
|
|
|
102
103
|
provider: "vertex",
|
|
103
104
|
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
104
105
|
envHint: "Requires `gcloud auth application-default login` and a GCP project ID"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: "5",
|
|
109
|
+
label: "OpenRouter",
|
|
110
|
+
provider: "openrouter",
|
|
111
|
+
envVar: "OPENROUTER_API_KEY",
|
|
112
|
+
envHint: "Get your key at https://openrouter.ai/settings/keys"
|
|
105
113
|
}
|
|
106
114
|
];
|
|
107
115
|
function promptUser(rl, question) {
|
|
@@ -136,7 +144,7 @@ function printHealthySummary(status) {
|
|
|
136
144
|
console.log(`\u2713 Using ${providerLabel(status.provider)} (${status.model}).`);
|
|
137
145
|
}
|
|
138
146
|
console.log(
|
|
139
|
-
"To change: npx libretto ai configure openai | anthropic | gemini | vertex"
|
|
147
|
+
"To change: npx libretto ai configure openai | anthropic | gemini | vertex | openrouter"
|
|
140
148
|
);
|
|
141
149
|
}
|
|
142
150
|
function printInvalidAiConfigWarning(status) {
|
|
@@ -200,7 +208,7 @@ function printSnapshotApiStatus() {
|
|
|
200
208
|
" GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
|
|
201
209
|
);
|
|
202
210
|
console.log(
|
|
203
|
-
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
211
|
+
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex | openrouter` to set a specific model."
|
|
204
212
|
);
|
|
205
213
|
console.log(
|
|
206
214
|
" Run `npx libretto setup` interactively to set up credentials."
|
|
@@ -245,7 +253,7 @@ function printSkipMessage() {
|
|
|
245
253
|
console.log(" ANTHROPIC_API_KEY=...");
|
|
246
254
|
console.log(" GEMINI_API_KEY=...");
|
|
247
255
|
console.log(
|
|
248
|
-
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
256
|
+
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex | openrouter` to set a specific model."
|
|
249
257
|
);
|
|
250
258
|
}
|
|
251
259
|
async function runInteractiveApiSetup() {
|
|
@@ -13,7 +13,7 @@ function printAiStatus(status) {
|
|
|
13
13
|
console.log(` Source: ${status.source}`);
|
|
14
14
|
}
|
|
15
15
|
console.log(
|
|
16
|
-
" To change: npx libretto ai configure openai | anthropic | gemini | vertex"
|
|
16
|
+
" To change: npx libretto ai configure openai | anthropic | gemini | vertex | openrouter"
|
|
17
17
|
);
|
|
18
18
|
break;
|
|
19
19
|
case "configured-missing-credentials":
|
|
@@ -10,7 +10,8 @@ const DEFAULT_SNAPSHOT_MODELS = {
|
|
|
10
10
|
openai: "openai/gpt-5.4",
|
|
11
11
|
anthropic: "anthropic/claude-sonnet-4-6",
|
|
12
12
|
google: "google/gemini-3-flash-preview",
|
|
13
|
-
vertex: "vertex/gemini-2.5-flash"
|
|
13
|
+
vertex: "vertex/gemini-2.5-flash",
|
|
14
|
+
openrouter: "openrouter/free"
|
|
14
15
|
};
|
|
15
16
|
function detectProviderEnvVar(provider, env = process.env) {
|
|
16
17
|
switch (provider) {
|
|
@@ -27,6 +28,8 @@ function detectProviderEnvVar(provider, env = process.env) {
|
|
|
27
28
|
if (env.GOOGLE_CLOUD_PROJECT?.trim()) return "GOOGLE_CLOUD_PROJECT";
|
|
28
29
|
if (env.GCLOUD_PROJECT?.trim()) return "GCLOUD_PROJECT";
|
|
29
30
|
return null;
|
|
31
|
+
case "openrouter":
|
|
32
|
+
return env.OPENROUTER_API_KEY?.trim() ? "OPENROUTER_API_KEY" : null;
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
class SnapshotApiUnavailableError extends Error {
|
|
@@ -45,10 +48,12 @@ function providerSetupSentence(provider) {
|
|
|
45
48
|
return "Add GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY to .env or as a shell environment variable.";
|
|
46
49
|
case "vertex":
|
|
47
50
|
return "Add GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT to .env or as a shell environment variable, and make sure application default credentials are configured.";
|
|
51
|
+
case "openrouter":
|
|
52
|
+
return "Add OPENROUTER_API_KEY to .env or as a shell environment variable.";
|
|
48
53
|
}
|
|
49
54
|
}
|
|
50
55
|
function defaultModelCommandLine() {
|
|
51
|
-
return "npx libretto ai configure openai | anthropic | gemini | vertex";
|
|
56
|
+
return "npx libretto ai configure openai | anthropic | gemini | vertex | openrouter";
|
|
52
57
|
}
|
|
53
58
|
function providerMissingCredentialSummary(provider) {
|
|
54
59
|
switch (provider) {
|
|
@@ -60,12 +65,14 @@ function providerMissingCredentialSummary(provider) {
|
|
|
60
65
|
return "GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are missing";
|
|
61
66
|
case "vertex":
|
|
62
67
|
return "GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT are missing";
|
|
68
|
+
case "openrouter":
|
|
69
|
+
return "OPENROUTER_API_KEY is missing";
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
72
|
function noSnapshotApiConfiguredMessage() {
|
|
66
73
|
return [
|
|
67
74
|
"Failed to analyze snapshot because no snapshot analyzer is configured.",
|
|
68
|
-
`Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, or
|
|
75
|
+
`Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, GOOGLE_CLOUD_PROJECT, or OPENROUTER_API_KEY to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
|
|
69
76
|
"For more info, run `npx libretto setup`."
|
|
70
77
|
].join(" ");
|
|
71
78
|
}
|
|
@@ -82,7 +89,8 @@ function inferAutoSnapshotModel() {
|
|
|
82
89
|
"openai",
|
|
83
90
|
"anthropic",
|
|
84
91
|
"google",
|
|
85
|
-
"vertex"
|
|
92
|
+
"vertex",
|
|
93
|
+
"openrouter"
|
|
86
94
|
];
|
|
87
95
|
for (const provider of providersInPriorityOrder) {
|
|
88
96
|
const envVar = detectProviderEnvVar(provider);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { mkdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
4
|
+
import { installSessionTelemetry } from "./session-telemetry.js";
|
|
5
|
+
import {
|
|
6
|
+
getSessionDir,
|
|
7
|
+
getSessionLogsPath,
|
|
8
|
+
getSessionNetworkLogPath,
|
|
9
|
+
getSessionActionsLogPath,
|
|
10
|
+
getSessionStatePath
|
|
11
|
+
} from "./context.js";
|
|
12
|
+
const config = JSON.parse(process.argv[2]);
|
|
13
|
+
const sessionDir = getSessionDir(config.session);
|
|
14
|
+
await mkdir(sessionDir, { recursive: true });
|
|
15
|
+
const logFile = getSessionLogsPath(config.session);
|
|
16
|
+
const networkLogFile = getSessionNetworkLogPath(config.session);
|
|
17
|
+
const actionsLogFile = getSessionActionsLogPath(config.session);
|
|
18
|
+
function childLog(level, event, data = {}) {
|
|
19
|
+
const entry = JSON.stringify({
|
|
20
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
21
|
+
id: Math.random().toString(36).slice(2, 10),
|
|
22
|
+
level,
|
|
23
|
+
scope: "libretto.child",
|
|
24
|
+
event,
|
|
25
|
+
data
|
|
26
|
+
});
|
|
27
|
+
appendFileSync(logFile, entry + "\n");
|
|
28
|
+
}
|
|
29
|
+
function logAction(entry) {
|
|
30
|
+
appendFileSync(actionsLogFile, JSON.stringify(entry) + "\n");
|
|
31
|
+
}
|
|
32
|
+
function logNetwork(entry) {
|
|
33
|
+
appendFileSync(networkLogFile, JSON.stringify(entry) + "\n");
|
|
34
|
+
}
|
|
35
|
+
const windowPositionArg = config.windowPosition ? `--window-position=${config.windowPosition.x},${config.windowPosition.y}` : void 0;
|
|
36
|
+
const launchArgs = [
|
|
37
|
+
"--disable-blink-features=AutomationControlled",
|
|
38
|
+
`--remote-debugging-port=${config.port}`,
|
|
39
|
+
"--remote-debugging-address=127.0.0.1",
|
|
40
|
+
"--no-focus-on-check",
|
|
41
|
+
...windowPositionArg ? [windowPositionArg] : []
|
|
42
|
+
];
|
|
43
|
+
const browser = await chromium.launch({
|
|
44
|
+
headless: !config.headed,
|
|
45
|
+
args: launchArgs
|
|
46
|
+
});
|
|
47
|
+
async function cleanupSessionState() {
|
|
48
|
+
const sessionStatePath = getSessionStatePath(config.session);
|
|
49
|
+
try {
|
|
50
|
+
await unlink(sessionStatePath);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.code !== "ENOENT") throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let shuttingDown = false;
|
|
56
|
+
let wakeDaemon;
|
|
57
|
+
const sleepPromise = new Promise((resolve) => {
|
|
58
|
+
wakeDaemon = resolve;
|
|
59
|
+
});
|
|
60
|
+
async function shutdown(reason, closeBrowser) {
|
|
61
|
+
if (shuttingDown) return;
|
|
62
|
+
shuttingDown = true;
|
|
63
|
+
try {
|
|
64
|
+
childLog("info", reason, { port: config.port });
|
|
65
|
+
await cleanupSessionState();
|
|
66
|
+
if (closeBrowser) await browser.close();
|
|
67
|
+
} finally {
|
|
68
|
+
wakeDaemon();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
browser.on("disconnected", () => {
|
|
72
|
+
void shutdown("browser-disconnected-exiting", false);
|
|
73
|
+
});
|
|
74
|
+
const context = await browser.newContext({
|
|
75
|
+
...config.storageStatePath ? { storageState: config.storageStatePath } : {},
|
|
76
|
+
viewport: {
|
|
77
|
+
width: config.viewport.width,
|
|
78
|
+
height: config.viewport.height
|
|
79
|
+
},
|
|
80
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
|
81
|
+
});
|
|
82
|
+
const page = await context.newPage();
|
|
83
|
+
page.setDefaultTimeout(3e4);
|
|
84
|
+
page.setDefaultNavigationTimeout(45e3);
|
|
85
|
+
await installSessionTelemetry({
|
|
86
|
+
context,
|
|
87
|
+
initialPage: page,
|
|
88
|
+
includeUserDomActions: true,
|
|
89
|
+
logAction,
|
|
90
|
+
logNetwork
|
|
91
|
+
});
|
|
92
|
+
await page.goto(config.url);
|
|
93
|
+
process.on("SIGTERM", () => {
|
|
94
|
+
void shutdown("child-sigterm", true);
|
|
95
|
+
});
|
|
96
|
+
process.on("SIGINT", () => {
|
|
97
|
+
void shutdown("child-sigint", true);
|
|
98
|
+
});
|
|
99
|
+
process.on("uncaughtException", (err) => {
|
|
100
|
+
childLog("error", "uncaught-exception", {
|
|
101
|
+
message: err.message,
|
|
102
|
+
stack: err.stack
|
|
103
|
+
});
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
106
|
+
process.on("unhandledRejection", (reason) => {
|
|
107
|
+
childLog("warn", "unhandled-rejection", { reason: String(reason) });
|
|
108
|
+
});
|
|
109
|
+
process.on("exit", (code) => {
|
|
110
|
+
childLog("info", "child-exit", {
|
|
111
|
+
code,
|
|
112
|
+
pid: process.pid,
|
|
113
|
+
port: config.port
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
childLog("info", "child-launched", {
|
|
117
|
+
port: config.port,
|
|
118
|
+
pid: process.pid,
|
|
119
|
+
session: config.session
|
|
120
|
+
});
|
|
121
|
+
await sleepPromise;
|
|
122
|
+
process.exit(0);
|