libretto 0.5.5 → 0.6.0

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 (110) hide show
  1. package/README.md +23 -10
  2. package/README.template.md +23 -10
  3. package/dist/cli/cli.js +10 -0
  4. package/dist/cli/commands/ai.js +77 -2
  5. package/dist/cli/commands/browser.js +98 -8
  6. package/dist/cli/commands/execution.js +152 -56
  7. package/dist/cli/commands/setup.js +390 -0
  8. package/dist/cli/commands/snapshot.js +2 -2
  9. package/dist/cli/commands/status.js +62 -0
  10. package/dist/cli/core/{snapshot-api-config.js → ai-model.js} +81 -7
  11. package/dist/cli/core/api-snapshot-analyzer.js +7 -5
  12. package/dist/cli/core/browser.js +202 -36
  13. package/dist/cli/core/{ai-config.js → config.js} +14 -79
  14. package/dist/cli/core/context.js +1 -25
  15. package/dist/cli/core/deploy-artifact.js +121 -61
  16. package/dist/cli/core/providers/browserbase.js +53 -0
  17. package/dist/cli/core/providers/index.js +48 -0
  18. package/dist/cli/core/providers/kernel.js +46 -0
  19. package/dist/cli/core/providers/libretto-cloud.js +58 -0
  20. package/dist/cli/core/readonly-exec.js +231 -0
  21. package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
  22. package/dist/cli/core/session.js +53 -0
  23. package/dist/cli/core/skill-version.js +73 -0
  24. package/dist/cli/core/telemetry.js +1 -54
  25. package/dist/cli/index.js +1 -7
  26. package/dist/cli/router.js +4 -4
  27. package/dist/cli/workers/run-integration-runtime.js +19 -13
  28. package/dist/cli/workers/run-integration-worker-protocol.js +5 -2
  29. package/dist/index.d.ts +2 -4
  30. package/dist/index.js +2 -2
  31. package/dist/runtime/extract/extract.d.ts +2 -2
  32. package/dist/runtime/extract/extract.js +4 -2
  33. package/dist/runtime/extract/index.d.ts +1 -1
  34. package/dist/runtime/recovery/agent.d.ts +2 -3
  35. package/dist/runtime/recovery/agent.js +5 -3
  36. package/dist/runtime/recovery/errors.d.ts +2 -3
  37. package/dist/runtime/recovery/errors.js +4 -2
  38. package/dist/runtime/recovery/index.d.ts +1 -2
  39. package/dist/runtime/recovery/recovery.d.ts +2 -3
  40. package/dist/runtime/recovery/recovery.js +3 -3
  41. package/dist/shared/debug/pause.js +4 -21
  42. package/dist/shared/run/api.d.ts +2 -0
  43. package/dist/shared/run/browser.d.ts +9 -1
  44. package/dist/shared/run/browser.js +43 -3
  45. package/dist/shared/state/index.d.ts +1 -1
  46. package/dist/shared/state/index.js +2 -0
  47. package/dist/shared/state/session-state.d.ts +20 -1
  48. package/dist/shared/state/session-state.js +12 -2
  49. package/dist/shared/workflow/workflow.d.ts +2 -1
  50. package/dist/shared/workflow/workflow.js +16 -9
  51. package/package.json +17 -16
  52. package/scripts/postinstall.mjs +13 -11
  53. package/scripts/skills-libretto.mjs +14 -4
  54. package/skills/AGENTS.md +11 -0
  55. package/skills/libretto/SKILL.md +30 -9
  56. package/skills/libretto/references/auth-profiles.md +1 -1
  57. package/skills/libretto/references/code-generation-rules.md +3 -3
  58. package/skills/libretto/references/configuration-file-reference.md +11 -6
  59. package/skills/libretto-readonly/SKILL.md +95 -0
  60. package/src/cli/cli.ts +10 -0
  61. package/src/cli/commands/ai.ts +111 -1
  62. package/src/cli/commands/browser.ts +111 -9
  63. package/src/cli/commands/execution.ts +181 -74
  64. package/src/cli/commands/setup.ts +516 -0
  65. package/src/cli/commands/snapshot.ts +2 -2
  66. package/src/cli/commands/status.ts +79 -0
  67. package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
  68. package/src/cli/core/api-snapshot-analyzer.ts +7 -5
  69. package/src/cli/core/browser.ts +242 -35
  70. package/src/cli/core/{ai-config.ts → config.ts} +14 -108
  71. package/src/cli/core/context.ts +1 -45
  72. package/src/cli/core/deploy-artifact.ts +141 -71
  73. package/src/cli/core/providers/browserbase.ts +57 -0
  74. package/src/cli/core/providers/index.ts +62 -0
  75. package/src/cli/core/providers/kernel.ts +49 -0
  76. package/src/cli/core/providers/libretto-cloud.ts +61 -0
  77. package/src/cli/core/providers/types.ts +9 -0
  78. package/src/cli/core/readonly-exec.ts +284 -0
  79. package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
  80. package/src/cli/core/session.ts +75 -2
  81. package/src/cli/core/skill-version.ts +93 -0
  82. package/src/cli/core/telemetry.ts +0 -52
  83. package/src/cli/index.ts +0 -6
  84. package/src/cli/router.ts +4 -4
  85. package/src/cli/workers/run-integration-runtime.ts +18 -16
  86. package/src/cli/workers/run-integration-worker-protocol.ts +4 -1
  87. package/src/index.ts +1 -7
  88. package/src/runtime/extract/extract.ts +6 -5
  89. package/src/runtime/recovery/agent.ts +5 -4
  90. package/src/runtime/recovery/errors.ts +4 -3
  91. package/src/runtime/recovery/recovery.ts +4 -4
  92. package/src/shared/debug/pause.ts +4 -23
  93. package/src/shared/run/browser.ts +50 -1
  94. package/src/shared/state/index.ts +2 -0
  95. package/src/shared/state/session-state.ts +10 -0
  96. package/src/shared/workflow/workflow.ts +24 -13
  97. package/dist/cli/commands/init.js +0 -286
  98. package/dist/cli/commands/logs.js +0 -117
  99. package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
  100. package/dist/shared/llm/ai-sdk-adapter.js +0 -49
  101. package/dist/shared/llm/client.d.ts +0 -13
  102. package/dist/shared/llm/index.d.ts +0 -5
  103. package/dist/shared/llm/index.js +0 -6
  104. package/dist/shared/llm/types.d.ts +0 -67
  105. package/src/cli/commands/init.ts +0 -331
  106. package/src/cli/commands/logs.ts +0 -128
  107. package/src/shared/llm/ai-sdk-adapter.ts +0 -81
  108. package/src/shared/llm/index.ts +0 -3
  109. package/src/shared/llm/types.ts +0 -63
  110. /package/dist/{shared/llm → cli/core/providers}/types.js +0 -0
@@ -2,7 +2,6 @@ import {
2
2
  appendFileSync,
3
3
  existsSync,
4
4
  readFileSync,
5
- writeFileSync,
6
5
  } from "node:fs";
7
6
  import type { Page } from "playwright";
8
7
  import {
@@ -65,31 +64,6 @@ export function readNetworkLog(
65
64
  return entries;
66
65
  }
67
66
 
68
- export function formatNetworkEntry(e: NetworkLogEntry): string {
69
- const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
70
- const duration = e.durationMs != null ? `${e.durationMs}ms` : "?ms";
71
- const size = e.size != null ? `${e.size}B` : "";
72
- const parts = [
73
- `[${time}]`,
74
- `${e.status}`,
75
- `${e.method.padEnd(6)}`,
76
- e.url,
77
- duration,
78
- size,
79
- ].filter(Boolean);
80
- let line = parts.join(" ");
81
- if (e.postData) {
82
- line += `\n body: ${e.postData.substring(0, 120)}${e.postData.length > 120 ? "..." : ""}`;
83
- }
84
- return line;
85
- }
86
-
87
- export function clearNetworkLog(session: string): void {
88
- assertSessionStateExistsOrThrow(session);
89
- const logPath = getSessionNetworkLogPath(session);
90
- writeFileSync(logPath, "");
91
- }
92
-
93
67
  export type ActionLogEntry = {
94
68
  ts: string;
95
69
  pageId?: string;
@@ -182,32 +156,6 @@ export function readActionLog(
182
156
  return entries;
183
157
  }
184
158
 
185
- export function formatActionEntry(e: ActionLogEntry): string {
186
- const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
187
- const src = e.source.toUpperCase().padEnd(5);
188
- const displaySelector = e.bestSemanticSelector || e.selector;
189
- const parts = [`[${time}]`, `[${src}]`, e.action];
190
- if (displaySelector) parts.push(displaySelector);
191
- if (e.targetSelector && e.targetSelector !== displaySelector) {
192
- parts.push(`target=${e.targetSelector}`);
193
- }
194
- if (e.nearbyText) parts.push(`text="${e.nearbyText}"`);
195
- if (e.coordinates) {
196
- parts.push(`@(${e.coordinates.x},${e.coordinates.y})`);
197
- }
198
- if (e.value) parts.push(`"${e.value}"`);
199
- if (e.url) parts.push(e.url);
200
- if (e.duration != null) parts.push(`${e.duration}ms`);
201
- if (!e.success) parts.push(`ERROR: ${e.error || "unknown"}`);
202
- return parts.join(" ");
203
- }
204
-
205
- export function clearActionLog(session: string): void {
206
- assertSessionStateExistsOrThrow(session);
207
- const logPath = getSessionActionsLogPath(session);
208
- writeFileSync(logPath, "");
209
- }
210
-
211
159
  const LOCATOR_ACTION_METHODS = [
212
160
  "click",
213
161
  "dblclick",
package/src/cli/index.ts CHANGED
@@ -1,13 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { runLibrettoCLI } from "./cli.js";
3
- import {
4
- maybeConfigureLLMClientFactoryFromEnv,
5
- setLLMClientFactory,
6
- } from "./core/context.js";
7
3
 
8
- export { setLLMClientFactory };
9
4
  export { runClose } from "./commands/browser.js";
10
5
  export { runLibrettoCLI };
11
6
 
12
- maybeConfigureLLMClientFactoryFromEnv();
13
7
  void runLibrettoCLI();
package/src/cli/router.ts CHANGED
@@ -2,8 +2,8 @@ import { aiCommands } from "./commands/ai.js";
2
2
  import { browserCommands } from "./commands/browser.js";
3
3
  import { deployCommand } from "./commands/deploy.js";
4
4
  import { executionCommands } from "./commands/execution.js";
5
- import { initCommand } from "./commands/init.js";
6
- import { logCommands } from "./commands/logs.js";
5
+ import { setupCommand } from "./commands/setup.js";
6
+ import { statusCommand } from "./commands/status.js";
7
7
  import { snapshotCommand } from "./commands/snapshot.js";
8
8
  import { SimpleCLI } from "./framework/simple-cli.js";
9
9
 
@@ -11,9 +11,9 @@ export const cliRoutes = {
11
11
  ...browserCommands,
12
12
  deploy: deployCommand,
13
13
  ...executionCommands,
14
- ...logCommands,
15
14
  ai: aiCommands,
16
- init: initCommand,
15
+ setup: setupCommand,
16
+ status: statusCommand,
17
17
  snapshot: snapshotCommand,
18
18
  };
19
19
 
@@ -5,7 +5,7 @@ import { cwd } from "node:process";
5
5
  import { isAbsolute, resolve } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import {
8
- getWorkflowFromModuleExports,
8
+ getDefaultWorkflowFromModuleExports,
9
9
  getWorkflowsFromModuleExports,
10
10
  instrumentContext,
11
11
  launchBrowser,
@@ -138,9 +138,8 @@ function getAbsoluteIntegrationPath(integrationPath: string): string {
138
138
  return absolutePath;
139
139
  }
140
140
 
141
- async function loadWorkflowByName(
141
+ async function loadDefaultWorkflow(
142
142
  absolutePath: string,
143
- workflowName: string,
144
143
  ): Promise<LoadedLibrettoWorkflow> {
145
144
  let loadedModule: Record<string, unknown>;
146
145
  try {
@@ -156,22 +155,23 @@ async function loadWorkflowByName(
156
155
  );
157
156
  }
158
157
 
159
- const workflow = getWorkflowFromModuleExports(loadedModule, workflowName);
160
- if (workflow) {
161
- return workflow as LoadedLibrettoWorkflow;
158
+ const defaultWorkflow = getDefaultWorkflowFromModuleExports(loadedModule);
159
+ if (defaultWorkflow) {
160
+ return defaultWorkflow as LoadedLibrettoWorkflow;
162
161
  }
163
162
 
164
- const availableWorkflows = getWorkflowsFromModuleExports(loadedModule).map(
163
+ const availableWorkflowNames = getWorkflowsFromModuleExports(loadedModule).map(
165
164
  (candidate) => candidate.name,
166
165
  );
167
166
 
168
- const detail =
169
- availableWorkflows.length > 0
170
- ? ` Available workflows: ${availableWorkflows.join(", ")}`
171
- : ' No workflows found in this file. Export a workflow() instance from "libretto" directly or via `export const workflows = { ... }`.';
167
+ if (availableWorkflowNames.length === 0) {
168
+ throw new Error(
169
+ `No default-exported workflow found in ${absolutePath}. Export the workflow with \`export default workflow("name", handler)\`.`,
170
+ );
171
+ }
172
172
 
173
173
  throw new Error(
174
- `Workflow "${workflowName}" not found in ${absolutePath}.${detail}`,
174
+ `No default-exported workflow found in ${absolutePath}. libretto run only uses the file's default export. Available named workflows: ${availableWorkflowNames.join(", ")}`,
175
175
  );
176
176
  }
177
177
 
@@ -194,7 +194,7 @@ async function runIntegrationInternal(
194
194
  ): Promise<RunIntegrationOutcome> {
195
195
  const { logger } = options;
196
196
  const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
197
- const workflow = await loadWorkflowByName(absolutePath, args.workflowName);
197
+ const workflow = await loadDefaultWorkflow(absolutePath);
198
198
  const signalPaths = getPauseSignalPaths(args.session);
199
199
  await removeSignalIfExists(signalPaths.pausedSignalPath);
200
200
  await removeSignalIfExists(signalPaths.resumeSignalPath);
@@ -203,12 +203,12 @@ async function runIntegrationInternal(
203
203
  const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
204
204
 
205
205
  console.log(
206
- `Running workflow "${args.workflowName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
206
+ `Running workflow "${workflow.name}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
207
207
  );
208
208
 
209
209
  const integrationLogger = logger.withScope("integration-run", {
210
210
  integrationPath: absolutePath,
211
- workflowName: args.workflowName,
211
+ workflowName: workflow.name,
212
212
  session: args.session,
213
213
  });
214
214
 
@@ -238,6 +238,9 @@ async function runIntegrationInternal(
238
238
  headless: args.headless,
239
239
  storageStatePath,
240
240
  viewport: args.viewport,
241
+ accessMode: args.accessMode,
242
+ cdpEndpoint: args.cdpEndpoint,
243
+ provider: args.provider,
241
244
  });
242
245
  if (!args.headless && args.visualize !== false) {
243
246
  await installHeadedWorkflowVisualization({
@@ -295,7 +298,6 @@ async function runIntegrationInternal(
295
298
  JSON.stringify({ completedAt: new Date().toISOString() }, null, 2),
296
299
  "utf8",
297
300
  );
298
- console.log("Integration completed.");
299
301
  return { status: "completed" };
300
302
  } finally {
301
303
  restoreStdout();
@@ -1,14 +1,17 @@
1
1
  import { z } from "zod";
2
+ import { SessionAccessModeSchema } from "../../shared/state/index.js";
2
3
 
3
4
  export const RunIntegrationWorkerRequestSchema = z.object({
4
5
  integrationPath: z.string().min(1),
5
- workflowName: z.string().min(1),
6
6
  session: z.string().min(1),
7
7
  params: z.unknown(),
8
8
  headless: z.boolean(),
9
9
  visualize: z.boolean().default(true),
10
10
  authProfileDomain: z.string().optional(),
11
11
  viewport: z.object({ width: z.number(), height: z.number() }).optional(),
12
+ accessMode: SessionAccessModeSchema.default("write-access"),
13
+ cdpEndpoint: z.string().optional(),
14
+ provider: z.object({ name: z.string(), sessionId: z.string() }).optional(),
12
15
  });
13
16
 
14
17
  export type RunIntegrationWorkerRequest = z.infer<
package/src/index.ts CHANGED
@@ -16,13 +16,6 @@ export {
16
16
  jsonlConsoleSink,
17
17
  } from "./shared/logger/sinks.js";
18
18
 
19
- // LLM client interface
20
- export type {
21
- LLMClient,
22
- Message,
23
- MessageContentPart,
24
- } from "./shared/llm/types.js";
25
- export { createLLMClientFromModel } from "./shared/llm/ai-sdk-adapter.js";
26
19
  export {
27
20
  SESSION_STATE_VERSION,
28
21
  SessionStatusSchema,
@@ -102,6 +95,7 @@ export {
102
95
 
103
96
  // Workflow helpers
104
97
  export {
98
+ getDefaultWorkflowFromModuleExports,
105
99
  getWorkflowFromModuleExports,
106
100
  getWorkflowsFromModuleExports,
107
101
  isLibrettoWorkflow,
@@ -4,13 +4,13 @@ import {
4
4
  type MinimalLogger,
5
5
  defaultLogger,
6
6
  } from "../../shared/logger/logger.js";
7
- import type { LLMClient } from "../../shared/llm/types.js";
7
+ import { generateObject, type LanguageModel } from "ai";
8
8
 
9
9
  export type ExtractOptions<T extends z.ZodType> = {
10
10
  page: Page;
11
11
  instruction: string;
12
12
  schema: T;
13
- llmClient: LLMClient;
13
+ model: LanguageModel;
14
14
  logger?: MinimalLogger;
15
15
  /** Optional CSS selector to scope extraction to a specific element. */
16
16
  selector?: string;
@@ -31,7 +31,7 @@ export async function extractFromPage<T extends z.ZodType>(
31
31
  schema,
32
32
  selector,
33
33
  logger = defaultLogger,
34
- llmClient,
34
+ model,
35
35
  } = options;
36
36
 
37
37
  let screenshot: string;
@@ -79,7 +79,8 @@ ${domContent ? `Here is the HTML content for additional context:\n<html>\n${domC
79
79
 
80
80
  Extract the requested information from the screenshot and return it in the specified format. Be precise and only extract what is visible.`;
81
81
 
82
- const result = await llmClient.generateObjectFromMessages({
82
+ const { object: result } = await generateObject({
83
+ model,
83
84
  schema,
84
85
  messages: [
85
86
  {
@@ -98,5 +99,5 @@ Extract the requested information from the screenshot and return it in the speci
98
99
  instruction: instruction.slice(0, 100),
99
100
  });
100
101
 
101
- return result;
102
+ return result as z.infer<T>;
102
103
  }
@@ -3,7 +3,7 @@ import {
3
3
  type MinimalLogger,
4
4
  defaultLogger,
5
5
  } from "../../shared/logger/logger.js";
6
- import type { LLMClient } from "../../shared/llm/types.js";
6
+ import { generateObject, type LanguageModel } from "ai";
7
7
 
8
8
  type BrowserAction =
9
9
  | { type: "click"; x: number; y: number; button?: string }
@@ -183,9 +183,9 @@ export async function executeRecoveryAgent(
183
183
  page: Page,
184
184
  instruction: string,
185
185
  logger?: MinimalLogger,
186
- llmClient?: LLMClient,
186
+ model?: LanguageModel,
187
187
  ): Promise<void> {
188
- if (!llmClient) {
188
+ if (!model) {
189
189
  return;
190
190
  }
191
191
  const log = logger ?? defaultLogger;
@@ -213,7 +213,8 @@ export async function executeRecoveryAgent(
213
213
 
214
214
  const maxSteps = 3;
215
215
  for (let step = 1; step <= maxSteps; step++) {
216
- const result = await llmClient.generateObjectFromMessages({
216
+ const { object: result } = await generateObject({
217
+ model,
217
218
  schema: recoveryActionSchema,
218
219
  messages: [
219
220
  {
@@ -3,7 +3,7 @@ import {
3
3
  type MinimalLogger,
4
4
  defaultLogger,
5
5
  } from "../../shared/logger/logger.js";
6
- import type { LLMClient } from "../../shared/llm/types.js";
6
+ import { generateObject, type LanguageModel } from "ai";
7
7
  import { z } from "zod";
8
8
 
9
9
  /**
@@ -47,7 +47,7 @@ export async function detectSubmissionError(
47
47
  page: Page,
48
48
  error: unknown,
49
49
  logContext: string,
50
- llmClient: LLMClient,
50
+ model: LanguageModel,
51
51
  knownErrors: KnownSubmissionError[] = [],
52
52
  logger?: MinimalLogger,
53
53
  ): Promise<DetectedSubmissionError> {
@@ -109,7 +109,8 @@ IMPORTANT:
109
109
 
110
110
  ${domSnapshot ? `<dom_snapshot>\n${domSnapshot}\n</dom_snapshot>` : ""}`;
111
111
 
112
- const result = await llmClient.generateObjectFromMessages({
112
+ const { object: result } = await generateObject({
113
+ model,
113
114
  schema: detectSubmissionErrorSchema,
114
115
  messages: [
115
116
  {
@@ -3,7 +3,7 @@ import {
3
3
  type MinimalLogger,
4
4
  defaultLogger,
5
5
  } from "../../shared/logger/logger.js";
6
- import type { LLMClient } from "../../shared/llm/types.js";
6
+ import type { LanguageModel } from "ai";
7
7
  import { executeRecoveryAgent } from "./agent.js";
8
8
 
9
9
  /**
@@ -14,7 +14,7 @@ export async function attemptWithRecovery<T>(
14
14
  page: Page,
15
15
  fn: () => Promise<T>,
16
16
  logger?: MinimalLogger,
17
- llmClient?: LLMClient,
17
+ model?: LanguageModel,
18
18
  ): Promise<T> {
19
19
  const log = logger ?? defaultLogger;
20
20
  try {
@@ -33,7 +33,7 @@ export async function attemptWithRecovery<T>(
33
33
  throw error;
34
34
  }
35
35
 
36
- if (!llmClient) {
36
+ if (!model) {
37
37
  throw error;
38
38
  }
39
39
 
@@ -45,7 +45,7 @@ export async function attemptWithRecovery<T>(
45
45
  page,
46
46
  "Look at the page to see if there is a popup blocking the screen. If so, close the popup.",
47
47
  log,
48
- llmClient,
48
+ model,
49
49
  );
50
50
 
51
51
  return await fn();
@@ -5,35 +5,16 @@ import {
5
5
  getPauseSignalPaths,
6
6
  removeSignalIfExists,
7
7
  } from "../../cli/core/pause-signals.js";
8
- import {
9
- listSessionsWithStateFile,
10
- readSessionState,
11
- } from "../../cli/core/session.js";
12
-
13
- function isPidRunning(pid: number): boolean {
14
- try {
15
- process.kill(pid, 0);
16
- return true;
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
- function getRunningSessions(): string[] {
23
- return listSessionsWithStateFile().filter((candidate) => {
24
- const state = readSessionState(candidate);
25
- return state !== null && state.pid != null && isPidRunning(state.pid);
26
- });
27
- }
8
+ import { listRunningSessions } from "../../cli/core/session.js";
28
9
 
29
10
  function throwMissingSessionError(): never {
30
- const runningSessions = getRunningSessions();
11
+ const runningSessions = listRunningSessions();
31
12
  const lines = ["pause(session) requires a non-empty session ID."];
32
13
 
33
14
  if (runningSessions.length > 0) {
34
15
  lines.push("", "Running sessions:");
35
- for (const runningSession of runningSessions) {
36
- lines.push(` ${runningSession}`);
16
+ for (const s of runningSessions) {
17
+ lines.push(` ${s.session}`);
37
18
  }
38
19
  }
39
20
 
@@ -9,9 +9,10 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { ensureLibrettoSessionStatePath } from "../paths/paths.js";
10
10
  import {
11
11
  SESSION_STATE_VERSION,
12
+ type SessionAccessMode,
12
13
  SessionStateFileSchema,
13
14
  } from "../state/session-state.js";
14
- import { readLibrettoConfig } from "../../cli/core/ai-config.js";
15
+ import { readLibrettoConfig } from "../../cli/core/config.js";
15
16
 
16
17
  async function pickFreePort(): Promise<number> {
17
18
  return await new Promise((resolve, reject) => {
@@ -34,6 +35,9 @@ export type LaunchBrowserArgs = {
34
35
  headless?: boolean;
35
36
  viewport?: { width: number; height: number };
36
37
  storageStatePath?: string;
38
+ accessMode?: SessionAccessMode;
39
+ cdpEndpoint?: string;
40
+ provider?: { name: string; sessionId: string };
37
41
  };
38
42
 
39
43
  export type BrowserSession = {
@@ -97,7 +101,51 @@ export async function launchBrowser({
97
101
  headless = false,
98
102
  viewport = { width: 1366, height: 768 },
99
103
  storageStatePath,
104
+ accessMode = "write-access",
105
+ cdpEndpoint,
106
+ provider,
100
107
  }: LaunchBrowserArgs): Promise<BrowserSession> {
108
+ // Cloud/remote mode: connect to an existing CDP endpoint instead of launching locally
109
+ if (cdpEndpoint) {
110
+ const browser = await chromium.connectOverCDP(cdpEndpoint);
111
+ const context =
112
+ browser.contexts()[0] ?? (await browser.newContext({ viewport }));
113
+ const page = context.pages()[0] ?? (await context.newPage());
114
+ page.setDefaultTimeout(30_000);
115
+ page.setDefaultNavigationTimeout(45_000);
116
+
117
+ const metadataPath = ensureLibrettoSessionStatePath(sessionName);
118
+ writeFileSync(
119
+ metadataPath,
120
+ JSON.stringify(
121
+ {
122
+ version: SESSION_STATE_VERSION,
123
+ session: sessionName,
124
+ port: 0,
125
+ cdpEndpoint,
126
+ pid: process.pid,
127
+ startedAt: new Date().toISOString(),
128
+ status: "active",
129
+ mode: accessMode,
130
+ ...(provider ? { provider } : {}),
131
+ },
132
+ null,
133
+ 2,
134
+ ),
135
+ );
136
+
137
+ return {
138
+ browser,
139
+ context,
140
+ page,
141
+ debugPort: 0,
142
+ metadataPath,
143
+ close: async () => {
144
+ await browser.close();
145
+ },
146
+ };
147
+ }
148
+
101
149
  const debugPort = await pickFreePort();
102
150
  const windowPosition = headless ? undefined : resolveWindowPosition();
103
151
  const browser = await chromium.launch({
@@ -141,6 +189,7 @@ export async function launchBrowser({
141
189
  pid: process.pid,
142
190
  startedAt: new Date().toISOString(),
143
191
  status: "active",
192
+ mode: accessMode,
144
193
  },
145
194
  null,
146
195
  2,
@@ -1,10 +1,12 @@
1
1
  export {
2
+ SessionAccessModeSchema,
2
3
  SESSION_STATE_VERSION,
3
4
  SessionStatusSchema,
4
5
  SessionStateFileSchema,
5
6
  parseSessionStateData,
6
7
  parseSessionStateContent,
7
8
  serializeSessionState,
9
+ type SessionAccessMode,
8
10
  type SessionStatus,
9
11
  type SessionState,
10
12
  type SessionStateFile,
@@ -8,12 +8,19 @@ export const SessionStatusSchema = z.enum([
8
8
  "completed",
9
9
  "failed",
10
10
  "exited",
11
+ "cleanup-failed",
11
12
  ]);
13
+ export const SessionAccessModeSchema = z.enum(["read-only", "write-access"]);
12
14
  export const SessionViewportSchema = z.object({
13
15
  width: z.number().int().min(1),
14
16
  height: z.number().int().min(1),
15
17
  });
16
18
 
19
+ export const ProviderStateSchema = z.object({
20
+ name: z.string(),
21
+ sessionId: z.string(),
22
+ });
23
+
17
24
  export const SessionStateFileSchema = z.object({
18
25
  version: z.literal(SESSION_STATE_VERSION),
19
26
  port: z.number().int().min(0).max(65535),
@@ -22,10 +29,13 @@ export const SessionStateFileSchema = z.object({
22
29
  session: z.string().min(1),
23
30
  startedAt: z.string().datetime({ offset: true }),
24
31
  status: SessionStatusSchema.optional(),
32
+ mode: SessionAccessModeSchema.default("write-access"),
25
33
  viewport: SessionViewportSchema.optional(),
34
+ provider: ProviderStateSchema.optional(),
26
35
  });
27
36
 
28
37
  export type SessionStatus = z.infer<typeof SessionStatusSchema>;
38
+ export type SessionAccessMode = z.infer<typeof SessionAccessModeSchema>;
29
39
  export type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
30
40
  export type SessionState = Omit<SessionStateFile, "version">;
31
41
 
@@ -17,10 +17,7 @@ export class LibrettoWorkflow<Input = unknown, Output = unknown> {
17
17
  public readonly name: string;
18
18
  private readonly handler: LibrettoWorkflowHandler<Input, Output>;
19
19
 
20
- constructor(
21
- name: string,
22
- handler: LibrettoWorkflowHandler<Input, Output>,
23
- ) {
20
+ constructor(name: string, handler: LibrettoWorkflowHandler<Input, Output>) {
24
21
  this.name = name;
25
22
  this.handler = handler;
26
23
  }
@@ -70,31 +67,45 @@ function addWorkflowOrThrow(
70
67
  workflowsByName.set(value.name, value);
71
68
  }
72
69
 
70
+ function collectWorkflowsOrThrow(
71
+ values: Iterable<unknown>,
72
+ ): ExportedLibrettoWorkflow[] {
73
+ const workflowsByName = new Map<string, ExportedLibrettoWorkflow>();
74
+
75
+ for (const value of values) {
76
+ addWorkflowOrThrow(workflowsByName, value);
77
+ }
78
+
79
+ return [...workflowsByName.values()];
80
+ }
81
+
73
82
  export function getWorkflowsFromModuleExports(
74
83
  moduleExports: WorkflowModuleExports,
75
84
  ): ExportedLibrettoWorkflow[] {
76
- const workflowsByName = new Map<string, ExportedLibrettoWorkflow>();
85
+ const discoveredValues: unknown[] = [];
77
86
 
78
87
  for (const [exportName, value] of Object.entries(moduleExports)) {
79
88
  if (exportName === "workflows" && value && typeof value === "object") {
80
89
  // Support both `export const workflows = workflow(...)` and
81
90
  // `export const workflows = { myWorkflow }`.
82
91
  if (isLibrettoWorkflow(value)) {
83
- addWorkflowOrThrow(workflowsByName, value);
92
+ discoveredValues.push(value);
84
93
  } else {
85
- for (const nestedValue of Object.values(
86
- value as Record<string, unknown>,
87
- )) {
88
- addWorkflowOrThrow(workflowsByName, nestedValue);
89
- }
94
+ discoveredValues.push(...Object.values(value as Record<string, unknown>));
90
95
  }
91
96
  continue;
92
97
  }
93
98
 
94
- addWorkflowOrThrow(workflowsByName, value);
99
+ discoveredValues.push(value);
95
100
  }
96
101
 
97
- return [...workflowsByName.values()];
102
+ return collectWorkflowsOrThrow(discoveredValues);
103
+ }
104
+
105
+ export function getDefaultWorkflowFromModuleExports(
106
+ moduleExports: WorkflowModuleExports,
107
+ ): ExportedLibrettoWorkflow | null {
108
+ return isLibrettoWorkflow(moduleExports.default) ? moduleExports.default : null;
98
109
  }
99
110
 
100
111
  export function getWorkflowFromModuleExports(