libretto 0.6.19 → 0.6.21

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.
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.19"
7
+ version: "0.6.21"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -39,12 +39,13 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
39
39
 
40
40
  ## Setup
41
41
 
42
- - Use `libretto setup` for first-time workspace onboarding. It installs Chromium and syncs skills.
43
- - Use `libretto status` to inspect open sessions without triggering setup.
42
+ - Use the package manager convention for the target project. The examples use `npx libretto`; pnpm, yarn, and bun projects should use their equivalent package-manager execution form.
43
+ - Use `npx libretto setup` for first-time workspace onboarding. It installs Chromium and syncs skills.
44
+ - Use `npx libretto status` to inspect open sessions without triggering setup.
44
45
 
45
46
  ## Experiments
46
47
 
47
- - Use `libretto experiments` to list internal feature flags and `libretto experiments describe <name>` for usage notes when an experiment is enabled.
48
+ - Use `npx libretto experiments` to list internal feature flags and `npx libretto experiments describe <name>` for usage notes when an experiment is enabled.
48
49
 
49
50
  ## Working Rules
50
51
 
@@ -83,9 +84,9 @@ npx libretto open https://example.com --session debug-example
83
84
  - Pass `--read-only` if the connected session must stay inspection-only from the start.
84
85
 
85
86
  ```bash
86
- libretto connect http://127.0.0.1:9222 --session my-session
87
- libretto connect http://127.0.0.1:9222 --read-only --session readonly-session
88
- libretto connect http://127.0.0.1:9223 --session another-session
87
+ npx libretto connect http://127.0.0.1:9222 --session my-session
88
+ npx libretto connect http://127.0.0.1:9222 --read-only --session readonly-session
89
+ npx libretto connect http://127.0.0.1:9223 --session another-session
89
90
  ```
90
91
 
91
92
  ### `session-mode`
@@ -96,7 +97,7 @@ libretto connect http://127.0.0.1:9223 --session another-session
96
97
  - Pass `--read-only` or `--write-access` to override the config default for a single command.
97
98
 
98
99
  ```bash
99
- libretto session-mode --session my-session
100
+ npx libretto session-mode --session my-session
100
101
  ```
101
102
 
102
103
  ### `snapshot`
@@ -108,9 +109,9 @@ libretto session-mode --session my-session
108
109
  - Use it before guessing at selectors, after workflow failures, and whenever the visible page state is unclear.
109
110
 
110
111
  ```bash
111
- libretto snapshot --session debug-example
112
- libretto snapshot <ref> --session debug-example
113
- libretto snapshot --session debug-example --page <page-id>
112
+ npx libretto snapshot --session debug-example
113
+ npx libretto snapshot <ref> --session debug-example
114
+ npx libretto snapshot --session debug-example --page <page-id>
114
115
  ```
115
116
 
116
117
  ### `exec`
@@ -126,10 +127,10 @@ libretto snapshot --session debug-example --page <page-id>
126
127
  - After successful mutations, `exec` prints page-change diffs from compact snapshots.
127
128
 
128
129
  ```bash
129
- libretto exec "await page.url()"
130
- libretto exec "await page.locator('button:has-text(\"Continue\")').click()"
131
- echo "async function textOf(selector) { return await page.locator(selector).textContent(); }" | libretto exec - --session debug-example
132
- libretto exec --session debug-example "await textOf('h1')"
130
+ npx libretto exec "await page.url()"
131
+ npx libretto exec "await page.locator('button:has-text(\"Continue\")').click()"
132
+ echo "async function textOf(selector) { return await page.locator(selector).textContent(); }" | npx libretto exec - --session debug-example
133
+ npx libretto exec --session debug-example "await textOf('h1')"
133
134
  ```
134
135
 
135
136
  ### `pages`
@@ -138,8 +139,8 @@ libretto exec --session debug-example "await textOf('h1')"
138
139
  - If `exec` or `snapshot` complains about multiple pages, list page ids first and then pass `--page`.
139
140
 
140
141
  ```bash
141
- libretto pages --session debug-example
142
- libretto exec --session debug-example --page <page-id> "await page.url()"
142
+ npx libretto pages --session debug-example
143
+ npx libretto exec --session debug-example --page <page-id> "await page.url()"
143
144
  ```
144
145
 
145
146
  ### `run`
@@ -150,14 +151,14 @@ libretto exec --session debug-example --page <page-id> "await page.url()"
150
151
  - Pass `--read-only` if the preserved session should come back locked for follow-up terminal inspection after the workflow run.
151
152
  - If the workflow fails, Libretto keeps the browser open. Inspect the failed state with `snapshot` and `exec` before editing code.
152
153
  - Insert `await pause(session)` statements in the workflow file when you need to stop at specific states for interactive debugging, like breakpoints in the browser flow.
153
- - If the workflow pauses, resume it with `libretto resume --session <name>`.
154
+ - If the workflow pauses, resume it with `npx libretto resume --session <name>`.
154
155
  - Re-run the same workflow after each fix to verify the browser behavior end to end.
155
156
 
156
157
  ```bash
157
- libretto run ./integration.ts --params '{"status":"open"}'
158
- libretto run ./integration.ts --read-only
159
- libretto run ./integration.ts --stay-open-on-success
160
- libretto run ./integration.ts --auth-profile app.example.com
158
+ npx libretto run ./integration.ts --params '{"status":"open"}'
159
+ npx libretto run ./integration.ts --read-only
160
+ npx libretto run ./integration.ts --stay-open-on-success
161
+ npx libretto run ./integration.ts --auth-profile app.example.com
161
162
  ```
162
163
 
163
164
  ### `resume`
@@ -168,7 +169,7 @@ libretto run ./integration.ts --auth-profile app.example.com
168
169
  - Keep resuming the same session until the workflow completes or pauses again.
169
170
 
170
171
  ```bash
171
- libretto resume --session debug-example
172
+ npx libretto resume --session debug-example
172
173
  ```
173
174
 
174
175
  ### `save`
@@ -176,7 +177,7 @@ libretto resume --session debug-example
176
177
  - Use `save` only when the user explicitly asks to save or reuse authenticated browser state.
177
178
 
178
179
  ```bash
179
- libretto save app.example.com
180
+ npx libretto save app.example.com
180
181
  ```
181
182
 
182
183
  ### `close`
@@ -185,8 +186,8 @@ libretto save app.example.com
185
186
  - `close --all` is available for workspace cleanup.
186
187
 
187
188
  ```bash
188
- libretto close --session debug-example
189
- libretto close --all
189
+ npx libretto close --session debug-example
190
+ npx libretto close --all
190
191
  ```
191
192
 
192
193
  ## Session Logs
@@ -234,17 +235,17 @@ Key fields: `id` (incrementing request id), `ts` (ISO timestamp), `pageId` (page
234
235
  <example>
235
236
  [Context: The user wants to build a new browser workflow and does not yet know the page structure]
236
237
  Assistant: I'll inspect the real site first if needed, but before I finish I'll create `target-workflow.ts` so the task produces reusable automation code.
237
- Assistant: [Runs `libretto open https://target.example.com --headed`]
238
+ Assistant: [Runs `npx libretto open https://target.example.com --headed`]
238
239
  Assistant: [Reads `references/site-security-review.md` before choosing between passive network inspection, direct browser fetch calls, and Playwright-first automation]
239
- Assistant: [Runs `libretto snapshot --session <session>`]
240
+ Assistant: [Runs `npx libretto snapshot --session <session>`]
240
241
  Assistant: [Uses `snapshot` and `exec` as needed to understand the site and decide the implementation path]
241
242
  Assistant: [Reads `references/code-generation-rules.md` before writing production workflow code]
242
243
  Assistant: I found the working path. I'll now create the workflow file and verify it.
243
244
  Assistant: [Creates or edits `target-workflow.ts` following `references/code-generation-rules.md`]
244
- Assistant: [Runs `libretto run ./target-workflow.ts --params '{"status":"open"}'` to validate]
245
+ Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` to validate]
245
246
  Assistant: Validation passed. Here are the results:
246
247
  [Shows the output/results from the validation run]
247
- To run it again, use: libretto run ./target-workflow.ts --params '{"status":"open"}'
248
+ To run it again, use: npx libretto run ./target-workflow.ts --params '{"status":"open"}'
248
249
  </example>
249
250
  ```
250
251
 
@@ -254,17 +255,17 @@ To run it again, use: libretto run ./target-workflow.ts --params '{"status":"ope
254
255
  <example>
255
256
  [Context: The user has an existing Libretto workflow that is failing]
256
257
  Assistant: I'll reproduce the failure first so we can inspect the exact browser state it leaves behind.
257
- Assistant: [Runs `libretto run ./integration.ts --session debug-flow`]
258
+ Assistant: [Runs `npx libretto run ./integration.ts --session debug-flow`]
258
259
  Assistant: The workflow failed and Libretto kept the browser open. I'll inspect the page state before changing code.
259
- Assistant: [Runs `libretto snapshot --session debug-flow`]
260
- Assistant: [Runs `libretto exec --session debug-flow "...focused inspection or prototype..."`]
260
+ Assistant: [Runs `npx libretto snapshot --session debug-flow`]
261
+ Assistant: [Runs `npx libretto exec --session debug-flow "...focused inspection or prototype..."`]
261
262
  Assistant: [Reads `references/code-generation-rules.md` before patching the workflow file]
262
263
  Assistant: I found the issue. I'll patch the workflow code and verify.
263
264
  Assistant: [Edits `integration.ts` following `references/code-generation-rules.md`]
264
- Assistant: [Runs `libretto run ./integration.ts` to validate the fix]
265
+ Assistant: [Runs `npx libretto run ./integration.ts` to validate the fix]
265
266
  Assistant: Fix verified. Here are the results:
266
267
  [Shows the output/results from the validation run]
267
- To run it again, use: libretto run ./integration.ts
268
+ To run it again, use: npx libretto run ./integration.ts
268
269
  </example>
269
270
  ```
270
271
 
@@ -17,9 +17,9 @@ Use this reference only when the user explicitly asks to save or reuse local aut
17
17
  ## Commands
18
18
 
19
19
  ```bash
20
- libretto open https://app.example.com --headed
21
- libretto save app.example.com
22
- libretto run ./integration.ts --auth-profile app.example.com
20
+ npx libretto open https://app.example.com --headed
21
+ npx libretto save app.example.com
22
+ npx libretto run ./integration.ts --auth-profile app.example.com
23
23
  ```
24
24
 
25
25
  ## Notes
@@ -6,7 +6,7 @@ Follow the user's existing codebase conventions, abstractions, and patterns when
6
6
 
7
7
  ## Workflow File Structure
8
8
 
9
- Generated files must default-export a `workflow()` instance so they can be run via `libretto run <file>`. Workflows declare their input and output shapes as Zod schemas, which both type the handler and validate runtime input.
9
+ Generated files must default-export a `workflow()` instance so they can be run via `npx libretto run <file>`. Workflows declare their input and output shapes as Zod schemas, which both type the handler and validate runtime input.
10
10
 
11
11
  Add `zod` (`^4.0.0`) to the workflow's `package.json` dependencies. Then import `workflow` from `"libretto"` and `z` from `"zod"`:
12
12
 
@@ -42,7 +42,7 @@ Key points:
42
42
 
43
43
  - `workflow(name, { input, output }, handler)` takes a unique workflow name, a pair of Zod schemas describing input and output, and the async handler. The handler's `input` parameter is inferred from the input schema — do not redeclare it with a separate `type Input = ...`.
44
44
  - At run time, Libretto validates `input` against `inputSchema` before calling the handler. Invalid input throws a clear error listing each failing field; the workflow handler never sees malformed input.
45
- - `libretto run ./file.ts` executes the file's default-exported workflow, so always use `export default workflow(...)`.
45
+ - `npx libretto run ./file.ts` executes the file's default-exported workflow, so always use `export default workflow(...)`.
46
46
  - `ctx` provides `session` and `page`. Use `console.log`/`console.warn`/`console.error` for logging — the runtime wraps these with structured metadata automatically.
47
47
  - `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI, then gets parsed through `inputSchema`.
48
48
  - Use `await pause(ctx.session)` (or `await pause(session)`) to pause the workflow for debugging. It is a no-op in production.
@@ -12,8 +12,8 @@ Use this reference when you need to inspect or change workspace configuration fo
12
12
 
13
13
  Libretto reads workspace config from `.libretto/config.json`.
14
14
 
15
- - The file is created by `libretto setup` during first-time onboarding.
16
- - Use `libretto status` to inspect open sessions without changing anything.
15
+ - The file is created by `npx libretto setup` during first-time onboarding.
16
+ - Use `npx libretto status` to inspect open sessions without changing anything.
17
17
  - For first-time setup instructions, follow the main `SKILL.md` flow instead of expanding this reference.
18
18
 
19
19
  ## Supported Settings
@@ -42,17 +42,17 @@ Example:
42
42
  ## Common Commands
43
43
 
44
44
  ```bash
45
- libretto setup # first-time onboarding
46
- libretto status # inspect open sessions
47
- libretto open https://example.com --provider kernel
48
- libretto run ./integration.ts --provider browserbase
49
- libretto open https://example.com --provider steel
50
- libretto open https://example.com --viewport 1440x900
51
- libretto run ./integration.ts --viewport 1440x900
45
+ npx libretto setup # first-time onboarding
46
+ npx libretto status # inspect open sessions
47
+ npx libretto open https://example.com --provider kernel
48
+ npx libretto run ./integration.ts --provider browserbase
49
+ npx libretto open https://example.com --provider steel
50
+ npx libretto open https://example.com --viewport 1440x900
51
+ npx libretto run ./integration.ts --viewport 1440x900
52
52
  ```
53
53
 
54
54
  ## Notes
55
55
 
56
56
  - If you want a persistent default provider for the workspace, add `provider` to `.libretto/config.json` instead of repeating `--provider` on every command.
57
57
  - If you want a persistent default viewport for the workspace, add `viewport` to `.libretto/config.json` instead of repeating `--viewport` on every command.
58
- - Run `libretto status` at any time to check open sessions.
58
+ - Run `npx libretto status` at any time to check open sessions.
@@ -17,9 +17,9 @@ Use this reference when a Libretto session has multiple open pages and you need
17
17
  ## Commands
18
18
 
19
19
  ```bash
20
- libretto pages --session debug-flow
21
- libretto exec --session debug-flow --page <page-id> "return await page.url()"
22
- libretto snapshot --session debug-flow --page <page-id>
20
+ npx libretto pages --session debug-flow
21
+ npx libretto exec --session debug-flow --page <page-id> "return await page.url()"
22
+ npx libretto snapshot --session debug-flow --page <page-id>
23
23
  ```
24
24
 
25
25
  ## Notes
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.19"
7
+ version: "0.6.21"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
package/src/cli/cli.ts CHANGED
@@ -14,22 +14,6 @@ function printSetupAudit(): void {
14
14
  warnIfLibrettoVersionsDiffer();
15
15
  }
16
16
 
17
- function isPackageManagerExec(env: NodeJS.ProcessEnv = process.env): boolean {
18
- return env.npm_command === "exec";
19
- }
20
-
21
- function warnIfPackageManagerExec(): void {
22
- if (!isPackageManagerExec()) return;
23
-
24
- console.error(
25
- [
26
- "Warning: running Libretto through a package manager is deprecated and will be removed in a future release.",
27
- "Install the native command instead:",
28
- " curl -fsSL https://libretto.sh/install.sh | bash",
29
- ].join("\n"),
30
- );
31
- }
32
-
33
17
  function isRootHelpRequest(rawArgs: readonly string[]): boolean {
34
18
  if (rawArgs.length === 0) return true;
35
19
  return rawArgs[0] === "help" && rawArgs.length === 1;
@@ -55,7 +39,6 @@ export async function runLibrettoCLI(): Promise<void> {
55
39
  const rawArgs = process.argv.slice(2);
56
40
  let exitCode = 0;
57
41
  loadEnv();
58
- warnIfPackageManagerExec();
59
42
  ensureLibrettoSetup();
60
43
  const app = createCLIApp();
61
44
 
@@ -125,6 +125,10 @@ export const deployInput = SimpleCLI.input({
125
125
  name: "entry-point",
126
126
  help: "Entry point file (default: index.ts)",
127
127
  }),
128
+ autoRepair: SimpleCLI.flag({
129
+ name: "auto-repair",
130
+ help: "Route failed jobs for this deployment to autofix",
131
+ }),
128
132
  external: SimpleCLI.option(
129
133
  z
130
134
  .string()
@@ -167,6 +171,7 @@ export const deployCommand = SimpleCLI.command({
167
171
  entry_point: entryPoint,
168
172
  };
169
173
  if (input.description) createPayload.description = input.description;
174
+ if (input.autoRepair) createPayload.auto_repair = true;
170
175
 
171
176
  console.log("Uploading deployment...");
172
177
  const body = await orpcCall<DeploymentResponse["json"]>({
@@ -1,14 +1,26 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { SimpleCLI } from "affordance";
5
-
6
- const UPDATE_COMMAND = "curl -fsSL https://libretto.sh/install.sh | bash";
6
+ import { REPO_ROOT } from "../core/context.js";
7
+ import {
8
+ detectProjectPackageManager,
9
+ installCommand,
10
+ type PackageManager,
11
+ } from "../../shared/package-manager.js";
7
12
 
8
13
  type PackageManifest = {
9
14
  version?: string;
10
15
  };
11
16
 
17
+ function packageInstallCommand(
18
+ packageManager: PackageManager,
19
+ packageSpec: string,
20
+ ): string {
21
+ return `${installCommand(packageManager)} ${packageSpec}`;
22
+ }
23
+
12
24
  function readCurrentCliVersion(): string {
13
25
  const packageJsonPath = fileURLToPath(
14
26
  new URL("../../../package.json", import.meta.url),
@@ -26,6 +38,23 @@ function readCurrentCliVersion(): string {
26
38
  return manifest.version;
27
39
  }
28
40
 
41
+ function readPackageVersion(packageJsonPath: string): string | null {
42
+ try {
43
+ const manifest = JSON.parse(
44
+ readFileSync(packageJsonPath, "utf8"),
45
+ ) as PackageManifest;
46
+ return manifest.version?.trim() || null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function readLocalPackageVersion(): string | null {
53
+ return readPackageVersion(
54
+ join(REPO_ROOT, "node_modules", "libretto", "package.json"),
55
+ );
56
+ }
57
+
29
58
  function readLatestNpmVersion(): string {
30
59
  const result = spawnSync("npm", ["view", "libretto@latest", "version"], {
31
60
  encoding: "utf8",
@@ -83,16 +112,17 @@ export const updateInput = SimpleCLI.input({
83
112
  function formatUpdateFailure(
84
113
  status: number | null,
85
114
  signal: string | null,
115
+ updateCommand: string,
86
116
  ): string {
87
117
  const knownState =
88
118
  status === null
89
- ? `installer was interrupted${signal ? ` by ${signal}` : ""}.`
90
- : `installer exited with status ${status}.`;
119
+ ? `package update was interrupted${signal ? ` by ${signal}` : ""}.`
120
+ : `package update exited with status ${status}.`;
91
121
 
92
122
  return [
93
123
  "Error: failed to update Libretto to the latest version.",
94
124
  `Known state: ${knownState}`,
95
- `Try: ${UPDATE_COMMAND}`,
125
+ `Try: ${updateCommand}`,
96
126
  "Help: libretto help update",
97
127
  ].join("\n");
98
128
  }
@@ -102,48 +132,56 @@ export const updateCommand = SimpleCLI.command({
102
132
  })
103
133
  .input(updateInput)
104
134
  .handle(async ({ input }) => {
135
+ const packageManager = detectProjectPackageManager();
136
+ const updateCommand = packageInstallCommand(packageManager, "libretto@latest");
137
+
105
138
  if (input.dryRun) {
106
139
  console.log("Update command:");
107
- console.log(` ${UPDATE_COMMAND}`);
140
+ console.log(` ${updateCommand}`);
108
141
  console.log("No changes made.");
109
142
  return;
110
143
  }
111
144
 
112
145
  const currentVersion = readCurrentCliVersion();
146
+ const localPackageVersion = readLocalPackageVersion();
147
+ const installedVersion = localPackageVersion ?? currentVersion;
113
148
  const latestVersion = readLatestNpmVersion();
114
- console.log(`Current version: ${currentVersion}`);
149
+ console.log(`Current version: ${installedVersion}`);
115
150
  console.log(`Latest version: ${latestVersion}`);
116
151
 
117
- if (currentVersion === latestVersion) {
118
- console.log(`Libretto is already up to date (${currentVersion}).`);
152
+ if (localPackageVersion && installedVersion === latestVersion) {
153
+ console.log(`Libretto is already up to date (${installedVersion}).`);
119
154
  console.log("No further action required.");
120
155
  return;
121
156
  }
122
157
 
123
- console.log("Updating Libretto to latest...");
124
- const result = spawnSync("bash", ["-lc", UPDATE_COMMAND], {
158
+ if (!localPackageVersion) {
159
+ console.log("Local package: not installed");
160
+ }
161
+
162
+ console.log("Updating local Libretto package to latest...");
163
+ const result = spawnSync(updateCommand, {
125
164
  stdio: "inherit",
126
- env: {
127
- ...process.env,
128
- LIBRETTO_VERSION: "latest",
129
- },
165
+ shell: true,
130
166
  });
131
167
 
132
168
  if (result.error) {
133
169
  throw new Error(
134
170
  [
135
- "Error: failed to start the Libretto installer.",
171
+ "Error: failed to start the Libretto package update.",
136
172
  `Known state: ${result.error.message}`,
137
- `Try: ${UPDATE_COMMAND}`,
173
+ `Try: ${updateCommand}`,
138
174
  "Help: libretto help update",
139
175
  ].join("\n"),
140
176
  );
141
177
  }
142
178
 
143
179
  if (result.status !== 0) {
144
- throw new Error(formatUpdateFailure(result.status, result.signal));
180
+ throw new Error(
181
+ formatUpdateFailure(result.status, result.signal, updateCommand),
182
+ );
145
183
  }
146
184
 
147
- console.log("Libretto updated to latest.");
185
+ console.log("Local Libretto package updated to latest.");
148
186
  console.log("No further action required.");
149
187
  });
@@ -89,6 +89,7 @@ import {
89
89
  loadDefaultWorkflow,
90
90
  } from "../workflow-runtime.js";
91
91
  import { WorkflowController } from "../workflow-runner/runner.js";
92
+ import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
92
93
 
93
94
  function isOperationalPage(page: Page): boolean {
94
95
  const url = page.url();
@@ -941,6 +942,7 @@ async function main(): Promise<void> {
941
942
  loadedWorkflow = await loadDefaultWorkflow(
942
943
  getAbsoluteIntegrationPath(config.workflow.integrationPath),
943
944
  );
945
+ validateWorkflowInput(loadedWorkflow, config.workflow.params ?? {});
944
946
  } catch (error) {
945
947
  throw new UserFacingStartupError(
946
948
  error instanceof Error ? error.message : String(error),
@@ -6,7 +6,6 @@ type CloudSessionResponse = {
6
6
  status: string;
7
7
  cdp_url: string | null;
8
8
  live_view_url: string | null;
9
- recording_url: string | null;
10
9
  };
11
10
 
12
11
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
@@ -77,8 +76,11 @@ export function createLibrettoCloudProvider(): ProviderApi {
77
76
  };
78
77
  },
79
78
  async closeSession(sessionId) {
80
- const json = await closeCloudSession(endpoint, apiKey, sessionId);
81
- return { replayUrl: json.replay_url ?? undefined };
79
+ await closeCloudSession(endpoint, apiKey, sessionId);
80
+ const replayUrl = await getCloudRecordingUrl(endpoint, apiKey, sessionId).catch(
81
+ () => undefined,
82
+ );
83
+ return { replayUrl };
82
84
  },
83
85
  };
84
86
  }
@@ -172,7 +174,7 @@ async function closeCloudSession(
172
174
  endpoint: string,
173
175
  apiKey: string,
174
176
  sessionId: string,
175
- ): Promise<{ replay_url: string | null }> {
177
+ ): Promise<void> {
176
178
  const resp = await fetch(`${endpoint}/v1/sessions/close`, {
177
179
  method: "POST",
178
180
  headers: {
@@ -187,10 +189,31 @@ async function closeCloudSession(
187
189
  `Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`,
188
190
  );
189
191
  }
192
+ }
193
+
194
+ async function getCloudRecordingUrl(
195
+ endpoint: string,
196
+ apiKey: string,
197
+ sessionId: string,
198
+ ): Promise<string | undefined> {
199
+ const resp = await fetch(`${endpoint}/v1/recordings/get`, {
200
+ method: "POST",
201
+ headers: {
202
+ "x-api-key": apiKey,
203
+ "Content-Type": "application/json",
204
+ },
205
+ body: JSON.stringify({ json: { session_id: sessionId } }),
206
+ });
207
+ if (!resp.ok) {
208
+ const body = await resp.text();
209
+ throw new Error(
210
+ `Libretto Cloud API error reading recording for session ${sessionId} (${resp.status}): ${body}`,
211
+ );
212
+ }
190
213
  const { json } = (await resp.json()) as {
191
- json: { replay_url: string | null };
214
+ json: { recording_url: string | null };
192
215
  };
193
- return json;
216
+ return json.recording_url ?? undefined;
194
217
  }
195
218
 
196
219
  function createStartupSessionCleanup(