libretto 0.3.0 → 0.3.2

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