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 CHANGED
@@ -5,6 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/libretto)](https://www.npmjs.com/package/libretto)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
7
  [![GitHub Discussions](https://img.shields.io/github/discussions/saffron-health/libretto)](https://github.com/saffron-health/libretto/discussions)
8
+ [![Discord](https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white)](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 setup # interactive first-run onboarding; run yourself, not through an agent
73
- npx libretto status # check AI config health and open sessions
74
- npx libretto open <url> # launch browser and open a URL (headed by default)
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
- To inspect the current configuration without changing anything:
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
- ### Sessions
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
- Profiles save browser sessions (cookies, localStorage) so you can reuse authenticated state across runs. They are stored in `.libretto/profiles/<domain>.json`, created via `npx libretto save <domain>`. Profiles are machine-local and git-ignored.
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
- Have a question, idea, or want to share what you've built? Join the conversation on [GitHub Discussions](https://github.com/saffron-health/libretto/discussions).
94
+ Join our Discord to connect with other developers, get help, and share what you've built:
141
95
 
142
- - **[Q&A](https://github.com/saffron-health/libretto/discussions/categories/q-a)** — Ask questions and get help
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
+ [![Discord](https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/NYrG56hVDt)
146
97
 
147
- Found a bug? Please [open an issue](https://github.com/saffron-health/libretto/issues/new).
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
- ## Authors
100
+ ## License
150
101
 
151
- Maintained by the team at [Saffron Health](https://saffron.health).
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).
@@ -3,6 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/libretto)](https://www.npmjs.com/package/libretto)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
  [![GitHub Discussions](https://img.shields.io/github/discussions/saffron-health/libretto)](https://github.com/saffron-health/libretto/discussions)
6
+ [![Discord](https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white)](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 setup # interactive first-run onboarding; run yourself, not through an agent
71
- npx libretto status # check AI config health and open sessions
72
- npx libretto open <url> # launch browser and open a URL (headed by default)
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
- To inspect the current configuration without changing anything:
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
- ### Sessions
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
- Profiles save browser sessions (cookies, localStorage) so you can reuse authenticated state across runs. They are stored in `.libretto/profiles/<domain>.json`, created via `npx libretto save <domain>`. Profiles are machine-local and git-ignored.
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
- Have a question, idea, or want to share what you've built? Join the conversation on [GitHub Discussions](https://github.com/saffron-health/libretto/discussions).
92
+ Join our Discord to connect with other developers, get help, and share what you've built:
139
93
 
140
- - **[Q&A](https://github.com/saffron-health/libretto/discussions/categories/q-a)** — Ask questions and get help
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
+ [![Discord](https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/NYrG56hVDt)
144
95
 
145
- Found a bug? Please [open an issue](https://github.com/saffron-health/libretto/issues/new).
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
- ## Authors
98
+ ## License
148
99
 
149
- Maintained by the team at [Saffron Health](https://saffron.health).
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, REPO_ROOT } from "./context.js";
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 GOOGLE_CLOUD_PROJECT to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
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
- loadSnapshotEnv();
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
- loadSnapshotEnv();
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);