libretto 0.6.7-experimental.0 → 0.6.8
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 +27 -69
- package/README.template.md +27 -69
- package/dist/cli/commands/setup.js +12 -4
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/core/ai-model.js +17 -70
- package/dist/cli/core/browser-daemon.js +122 -0
- package/dist/cli/core/browser.js +31 -176
- package/dist/cli/core/config.js +1 -1
- package/dist/cli/core/providers/libretto-cloud.js +5 -3
- package/dist/cli/core/resolve-model.js +20 -2
- package/dist/cli/workers/run-integration-runtime.js +5 -2
- package/dist/shared/dom-semantics.js +0 -1
- package/dist/shared/env/load-env.d.ts +9 -4
- package/dist/shared/env/load-env.js +57 -36
- package/package.json +1 -1
- package/scripts/generate-changelog.ts +9 -6
- package/skills/libretto/SKILL.md +14 -3
- package/skills/libretto/references/configuration-file-reference.md +1 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/setup.ts +11 -3
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/core/ai-model.ts +17 -89
- package/src/cli/core/browser-daemon.ts +198 -0
- package/src/cli/core/browser.ts +27 -187
- package/src/cli/core/config.ts +1 -1
- package/src/cli/core/providers/libretto-cloud.ts +8 -5
- package/src/cli/core/resolve-model.ts +20 -2
- package/src/cli/workers/run-integration-runtime.ts +12 -2
- package/src/shared/dom-semantics.ts +0 -1
- package/src/shared/env/load-env.ts +75 -53
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/libretto)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://github.com/saffron-health/libretto/discussions)
|
|
8
|
+
[](https://discord.gg/NYrG56hVDt)
|
|
8
9
|
|
|
9
10
|
Libretto is a toolkit for building robust web integrations. It gives your coding agent a live browser and a token-efficient CLI to:
|
|
10
11
|
|
|
@@ -17,6 +18,13 @@ We at [Saffron Health](https://saffron.health) built Libretto to help us maintai
|
|
|
17
18
|
|
|
18
19
|
https://github.com/user-attachments/assets/9b9a0ab3-5133-4b20-b3be-459943349d18
|
|
19
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
|
+
|
|
20
28
|
## Installation
|
|
21
29
|
|
|
22
30
|
```bash
|
|
@@ -69,86 +77,29 @@ Agents can use Libretto to reproduce the failure, pause the workflow at any poin
|
|
|
69
77
|
You can also use Libretto directly from the command line. All commands accept `--session <name>` to target a specific session.
|
|
70
78
|
|
|
71
79
|
```bash
|
|
72
|
-
npx libretto
|
|
73
|
-
npx libretto
|
|
74
|
-
npx libretto
|
|
75
|
-
npx libretto snapshot --objective "..." --context "..." # capture PNG + HTML and analyze with an LLM
|
|
76
|
-
npx libretto exec "<code>" # execute Playwright TypeScript against the open page (single quoted argument)
|
|
77
|
-
echo "<code>" | npx libretto exec - # intentionally read Playwright TypeScript from stdin
|
|
78
|
-
npx libretto run <file> # run the file's default-exported workflow
|
|
79
|
-
npx libretto resume # resume a paused workflow
|
|
80
|
-
npx libretto pages # list open pages in the session
|
|
81
|
-
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
|
|
82
83
|
npx libretto close # close the browser
|
|
83
|
-
npx libretto ai configure <provider> # manually change snapshot analysis model
|
|
84
|
-
npx libretto status # show AI config and open sessions
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Configuration
|
|
88
|
-
|
|
89
|
-
All Libretto state lives in a `.libretto/` directory at your project root. Configuration is stored in `.libretto/config.json`.
|
|
90
|
-
|
|
91
|
-
### Config file
|
|
92
|
-
|
|
93
|
-
`.libretto/config.json` controls snapshot analysis and viewport settings:
|
|
94
|
-
|
|
95
|
-
```json
|
|
96
|
-
{
|
|
97
|
-
"version": 1,
|
|
98
|
-
"ai": {
|
|
99
|
-
"model": "openai/gpt-5.4",
|
|
100
|
-
"updatedAt": "2026-01-01T00:00:00.000Z"
|
|
101
|
-
},
|
|
102
|
-
"viewport": { "width": 1280, "height": 800 }
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
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.
|
|
107
|
-
|
|
108
|
-
`npx libretto setup` automatically pins the default model for the first provider whose credentials it finds. To explicitly change the provider or model afterward:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
npx libretto ai configure <openai | anthropic | gemini | vertex>
|
|
112
84
|
```
|
|
113
85
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
npx libretto status
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Provider credentials are read from environment variables or a `.env` file at your project root: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` / `GOOGLE_GENERATIVE_AI_API_KEY`, or `GOOGLE_CLOUD_PROJECT` for Vertex.
|
|
121
|
-
|
|
122
|
-
The `viewport` field sets the default browser viewport size. Both fields are optional.
|
|
86
|
+
Run `npx libretto help` for the full list of commands.
|
|
123
87
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Each Libretto session gets its own directory under `.libretto/sessions/<name>/` containing runtime state. Sessions are git-ignored.
|
|
127
|
-
|
|
128
|
-
- `state.json` — session metadata (debug port, PID, status)
|
|
129
|
-
- `logs.jsonl` — structured session logs
|
|
130
|
-
- `network.jsonl` — captured network requests
|
|
131
|
-
- `actions.jsonl` — recorded user actions
|
|
132
|
-
- `snapshots/` — screenshot PNGs and HTML snapshots
|
|
133
|
-
|
|
134
|
-
### Profiles
|
|
88
|
+
## Configuration
|
|
135
89
|
|
|
136
|
-
|
|
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.
|
|
137
91
|
|
|
138
|
-
## Community
|
|
92
|
+
## Join the Community
|
|
139
93
|
|
|
140
|
-
|
|
94
|
+
Join our Discord to connect with other developers, get help, and share what you've built:
|
|
141
95
|
|
|
142
|
-
|
|
143
|
-
- **[Ideas](https://github.com/saffron-health/libretto/discussions/categories/ideas)** — Suggest new features or improvements
|
|
144
|
-
- **[Show and tell](https://github.com/saffron-health/libretto/discussions/categories/show-and-tell)** — Share your workflows and automations
|
|
145
|
-
- **[General](https://github.com/saffron-health/libretto/discussions/categories/general)** — Chat about anything Libretto-related
|
|
96
|
+
[](https://discord.gg/NYrG56hVDt)
|
|
146
97
|
|
|
147
|
-
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).
|
|
148
99
|
|
|
149
|
-
##
|
|
100
|
+
## License
|
|
150
101
|
|
|
151
|
-
|
|
102
|
+
[MIT License](LICENSE) — use it freely in commercial and open-source projects.
|
|
152
103
|
|
|
153
104
|
## Development
|
|
154
105
|
|
|
@@ -173,3 +124,10 @@ Source layout:
|
|
|
173
124
|
Run `pnpm sync:mirrors` after editing `README.template.md` or anything under `skills/libretto/`.
|
|
174
125
|
|
|
175
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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/libretto)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://github.com/saffron-health/libretto/discussions)
|
|
6
|
+
[](https://discord.gg/NYrG56hVDt)
|
|
6
7
|
|
|
7
8
|
Libretto is a toolkit for building robust web integrations. It gives your coding agent a live browser and a token-efficient CLI to:
|
|
8
9
|
|
|
@@ -15,6 +16,13 @@ We at [Saffron Health](https://saffron.health) built Libretto to help us maintai
|
|
|
15
16
|
|
|
16
17
|
https://github.com/user-attachments/assets/9b9a0ab3-5133-4b20-b3be-459943349d18
|
|
17
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
|
+
|
|
18
26
|
## Installation
|
|
19
27
|
|
|
20
28
|
```bash
|
|
@@ -67,86 +75,29 @@ Agents can use Libretto to reproduce the failure, pause the workflow at any poin
|
|
|
67
75
|
You can also use Libretto directly from the command line. All commands accept `--session <name>` to target a specific session.
|
|
68
76
|
|
|
69
77
|
```bash
|
|
70
|
-
npx libretto
|
|
71
|
-
npx libretto
|
|
72
|
-
npx libretto
|
|
73
|
-
npx libretto snapshot --objective "..." --context "..." # capture PNG + HTML and analyze with an LLM
|
|
74
|
-
npx libretto exec "<code>" # execute Playwright TypeScript against the open page (single quoted argument)
|
|
75
|
-
echo "<code>" | npx libretto exec - # intentionally read Playwright TypeScript from stdin
|
|
76
|
-
npx libretto run <file> # run the file's default-exported workflow
|
|
77
|
-
npx libretto resume # resume a paused workflow
|
|
78
|
-
npx libretto pages # list open pages in the session
|
|
79
|
-
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
|
|
80
81
|
npx libretto close # close the browser
|
|
81
|
-
npx libretto ai configure <provider> # manually change snapshot analysis model
|
|
82
|
-
npx libretto status # show AI config and open sessions
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Configuration
|
|
86
|
-
|
|
87
|
-
All Libretto state lives in a `.libretto/` directory at your project root. Configuration is stored in `.libretto/config.json`.
|
|
88
|
-
|
|
89
|
-
### Config file
|
|
90
|
-
|
|
91
|
-
`.libretto/config.json` controls snapshot analysis and viewport settings:
|
|
92
|
-
|
|
93
|
-
```json
|
|
94
|
-
{
|
|
95
|
-
"version": 1,
|
|
96
|
-
"ai": {
|
|
97
|
-
"model": "openai/gpt-5.4",
|
|
98
|
-
"updatedAt": "2026-01-01T00:00:00.000Z"
|
|
99
|
-
},
|
|
100
|
-
"viewport": { "width": 1280, "height": 800 }
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
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.
|
|
105
|
-
|
|
106
|
-
`npx libretto setup` automatically pins the default model for the first provider whose credentials it finds. To explicitly change the provider or model afterward:
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
npx libretto ai configure <openai | anthropic | gemini | vertex>
|
|
110
82
|
```
|
|
111
83
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
npx libretto status
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Provider credentials are read from environment variables or a `.env` file at your project root: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` / `GOOGLE_GENERATIVE_AI_API_KEY`, or `GOOGLE_CLOUD_PROJECT` for Vertex.
|
|
119
|
-
|
|
120
|
-
The `viewport` field sets the default browser viewport size. Both fields are optional.
|
|
84
|
+
Run `npx libretto help` for the full list of commands.
|
|
121
85
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Each Libretto session gets its own directory under `.libretto/sessions/<name>/` containing runtime state. Sessions are git-ignored.
|
|
125
|
-
|
|
126
|
-
- `state.json` — session metadata (debug port, PID, status)
|
|
127
|
-
- `logs.jsonl` — structured session logs
|
|
128
|
-
- `network.jsonl` — captured network requests
|
|
129
|
-
- `actions.jsonl` — recorded user actions
|
|
130
|
-
- `snapshots/` — screenshot PNGs and HTML snapshots
|
|
131
|
-
|
|
132
|
-
### Profiles
|
|
86
|
+
## Configuration
|
|
133
87
|
|
|
134
|
-
|
|
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.
|
|
135
89
|
|
|
136
|
-
## Community
|
|
90
|
+
## Join the Community
|
|
137
91
|
|
|
138
|
-
|
|
92
|
+
Join our Discord to connect with other developers, get help, and share what you've built:
|
|
139
93
|
|
|
140
|
-
|
|
141
|
-
- **[Ideas](https://github.com/saffron-health/libretto/discussions/categories/ideas)** — Suggest new features or improvements
|
|
142
|
-
- **[Show and tell](https://github.com/saffron-health/libretto/discussions/categories/show-and-tell)** — Share your workflows and automations
|
|
143
|
-
- **[General](https://github.com/saffron-health/libretto/discussions/categories/general)** — Chat about anything Libretto-related
|
|
94
|
+
[](https://discord.gg/NYrG56hVDt)
|
|
144
95
|
|
|
145
|
-
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).
|
|
146
97
|
|
|
147
|
-
##
|
|
98
|
+
## License
|
|
148
99
|
|
|
149
|
-
|
|
100
|
+
[MIT License](LICENSE) — use it freely in commercial and open-source projects.
|
|
150
101
|
|
|
151
102
|
## Development
|
|
152
103
|
|
|
@@ -171,3 +122,10 @@ Source layout:
|
|
|
171
122
|
Run `pnpm sync:mirrors` after editing `{{LIBRETTO_PATH_PREFIX}}README.template.md` or anything under `{{LIBRETTO_PATH_PREFIX}}skills/libretto/`.
|
|
172
123
|
|
|
173
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).
|
|
@@ -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":
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
1
|
import { readSnapshotModel } from "./config.js";
|
|
4
|
-
import { LIBRETTO_CONFIG_PATH
|
|
2
|
+
import { LIBRETTO_CONFIG_PATH } from "./context.js";
|
|
5
3
|
import {
|
|
6
4
|
hasProviderCredentials,
|
|
7
5
|
parseModel
|
|
8
6
|
} from "./resolve-model.js";
|
|
7
|
+
import { loadEnv } from "../../shared/env/load-env.js";
|
|
8
|
+
import { parseDotEnvAssignment } from "../../shared/env/load-env.js";
|
|
9
9
|
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
|
}
|
|
@@ -77,72 +84,13 @@ function missingProviderSnapshotMessage(selection) {
|
|
|
77
84
|
"For more info, run `npx libretto setup`."
|
|
78
85
|
].join(" ");
|
|
79
86
|
}
|
|
80
|
-
function readWorktreeEnvPath() {
|
|
81
|
-
const gitPath = join(REPO_ROOT, ".git");
|
|
82
|
-
if (!existsSync(gitPath)) return null;
|
|
83
|
-
try {
|
|
84
|
-
const gitPointer = readFileSync(gitPath, "utf-8").trim();
|
|
85
|
-
const match = gitPointer.match(/^gitdir:\s*(.+)$/i);
|
|
86
|
-
if (!match?.[1]) return null;
|
|
87
|
-
const worktreeGitDir = resolve(REPO_ROOT, match[1].trim());
|
|
88
|
-
const commonGitDir = resolve(worktreeGitDir, "..", "..");
|
|
89
|
-
return join(dirname(commonGitDir), ".env");
|
|
90
|
-
} catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function loadSnapshotEnv() {
|
|
95
|
-
if (process.env.LIBRETTO_DISABLE_DOTENV?.trim() === "1") return;
|
|
96
|
-
const envPathCandidates = [
|
|
97
|
-
join(REPO_ROOT, ".env"),
|
|
98
|
-
readWorktreeEnvPath()
|
|
99
|
-
].filter((value) => Boolean(value));
|
|
100
|
-
const envPath = envPathCandidates.find((candidate) => existsSync(candidate));
|
|
101
|
-
if (!envPath) return;
|
|
102
|
-
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
103
|
-
const parsed = parseDotEnvAssignment(line);
|
|
104
|
-
if (!parsed) continue;
|
|
105
|
-
if (!(parsed.key in process.env)) {
|
|
106
|
-
process.env[parsed.key] = parsed.value;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function parseDotEnvAssignment(line) {
|
|
111
|
-
const trimmed = line.trim();
|
|
112
|
-
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
113
|
-
const withoutExport = trimmed.startsWith("export ") ? trimmed.slice("export ".length).trimStart() : trimmed;
|
|
114
|
-
const eqIdx = withoutExport.indexOf("=");
|
|
115
|
-
if (eqIdx < 1) return null;
|
|
116
|
-
const key = withoutExport.slice(0, eqIdx).trim();
|
|
117
|
-
if (!key) return null;
|
|
118
|
-
const rawValue = withoutExport.slice(eqIdx + 1).trimStart();
|
|
119
|
-
if (!rawValue) {
|
|
120
|
-
return { key, value: "" };
|
|
121
|
-
}
|
|
122
|
-
if (rawValue.startsWith('"')) {
|
|
123
|
-
const closeIdx = rawValue.indexOf('"', 1);
|
|
124
|
-
if (closeIdx > 0) {
|
|
125
|
-
return { key, value: rawValue.slice(1, closeIdx) };
|
|
126
|
-
}
|
|
127
|
-
return { key, value: rawValue.slice(1) };
|
|
128
|
-
}
|
|
129
|
-
if (rawValue.startsWith("'")) {
|
|
130
|
-
const closeIdx = rawValue.indexOf("'", 1);
|
|
131
|
-
if (closeIdx > 0) {
|
|
132
|
-
return { key, value: rawValue.slice(1, closeIdx) };
|
|
133
|
-
}
|
|
134
|
-
return { key, value: rawValue.slice(1) };
|
|
135
|
-
}
|
|
136
|
-
const inlineCommentIndex = rawValue.search(/\s#/);
|
|
137
|
-
const value = inlineCommentIndex >= 0 ? rawValue.slice(0, inlineCommentIndex).trimEnd() : rawValue.trim();
|
|
138
|
-
return { key, value };
|
|
139
|
-
}
|
|
140
87
|
function inferAutoSnapshotModel() {
|
|
141
88
|
const providersInPriorityOrder = [
|
|
142
89
|
"openai",
|
|
143
90
|
"anthropic",
|
|
144
91
|
"google",
|
|
145
|
-
"vertex"
|
|
92
|
+
"vertex",
|
|
93
|
+
"openrouter"
|
|
146
94
|
];
|
|
147
95
|
for (const provider of providersInPriorityOrder) {
|
|
148
96
|
const envVar = detectProviderEnvVar(provider);
|
|
@@ -156,7 +104,7 @@ function inferAutoSnapshotModel() {
|
|
|
156
104
|
return null;
|
|
157
105
|
}
|
|
158
106
|
function resolveSnapshotApiModel(snapshotModel = readSnapshotModel()) {
|
|
159
|
-
|
|
107
|
+
loadEnv();
|
|
160
108
|
if (snapshotModel) {
|
|
161
109
|
const { provider } = parseModel(snapshotModel);
|
|
162
110
|
return {
|
|
@@ -193,7 +141,7 @@ function readSnapshotModelSafely(configPath) {
|
|
|
193
141
|
}
|
|
194
142
|
}
|
|
195
143
|
function resolveAiSetupStatus(configPath = LIBRETTO_CONFIG_PATH) {
|
|
196
|
-
|
|
144
|
+
loadEnv();
|
|
197
145
|
const result = readSnapshotModelSafely(configPath);
|
|
198
146
|
if (!result.ok) {
|
|
199
147
|
return { kind: "invalid-config", message: result.message };
|
|
@@ -240,7 +188,6 @@ export {
|
|
|
240
188
|
DEFAULT_SNAPSHOT_MODELS,
|
|
241
189
|
SnapshotApiUnavailableError,
|
|
242
190
|
isSnapshotApiUnavailableError,
|
|
243
|
-
loadSnapshotEnv,
|
|
244
191
|
parseDotEnvAssignment,
|
|
245
192
|
resolveAiSetupStatus,
|
|
246
193
|
resolveSnapshotApiModel,
|
|
@@ -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);
|