libretto 0.6.9 → 0.6.11

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 (60) hide show
  1. package/dist/cli/cli.js +2 -0
  2. package/dist/cli/commands/auth.js +535 -0
  3. package/dist/cli/commands/billing.js +74 -0
  4. package/dist/cli/commands/browser.js +8 -3
  5. package/dist/cli/commands/deploy.js +2 -7
  6. package/dist/cli/commands/execution.js +99 -136
  7. package/dist/cli/commands/snapshot.js +38 -126
  8. package/dist/cli/core/ai-model.js +0 -3
  9. package/dist/cli/core/auth-fetch.js +195 -0
  10. package/dist/cli/core/auth-storage.js +52 -0
  11. package/dist/cli/core/browser.js +128 -202
  12. package/dist/cli/core/daemon/config.js +6 -0
  13. package/dist/cli/core/daemon/daemon.js +298 -0
  14. package/dist/cli/core/daemon/exec.js +86 -0
  15. package/dist/cli/core/daemon/index.js +16 -0
  16. package/dist/cli/core/daemon/ipc.js +171 -0
  17. package/dist/cli/core/daemon/pages.js +15 -0
  18. package/dist/cli/core/daemon/snapshot.js +86 -0
  19. package/dist/cli/core/daemon/spawn.js +90 -0
  20. package/dist/cli/core/exec-compiler.js +111 -0
  21. package/dist/cli/core/prompt.js +72 -0
  22. package/dist/cli/core/providers/libretto-cloud.js +2 -6
  23. package/dist/cli/core/readonly-exec.js +1 -1
  24. package/dist/cli/router.js +4 -0
  25. package/dist/cli/workers/run-integration-runtime.js +0 -5
  26. package/dist/shared/state/session-state.d.ts +1 -0
  27. package/dist/shared/state/session-state.js +2 -1
  28. package/docs/browser-automation-approaches.md +435 -0
  29. package/docs/releasing.md +117 -0
  30. package/package.json +4 -3
  31. package/skills/libretto/SKILL.md +14 -1
  32. package/skills/libretto-readonly/SKILL.md +1 -1
  33. package/src/cli/cli.ts +2 -0
  34. package/src/cli/commands/auth.ts +787 -0
  35. package/src/cli/commands/billing.ts +133 -0
  36. package/src/cli/commands/browser.ts +8 -2
  37. package/src/cli/commands/deploy.ts +2 -7
  38. package/src/cli/commands/execution.ts +126 -186
  39. package/src/cli/commands/snapshot.ts +46 -143
  40. package/src/cli/core/ai-model.ts +4 -5
  41. package/src/cli/core/auth-fetch.ts +283 -0
  42. package/src/cli/core/auth-storage.ts +102 -0
  43. package/src/cli/core/browser.ts +159 -242
  44. package/src/cli/core/daemon/config.ts +46 -0
  45. package/src/cli/core/daemon/daemon.ts +429 -0
  46. package/src/cli/core/daemon/exec.ts +128 -0
  47. package/src/cli/core/daemon/index.ts +24 -0
  48. package/src/cli/core/daemon/ipc.ts +294 -0
  49. package/src/cli/core/daemon/pages.ts +21 -0
  50. package/src/cli/core/daemon/snapshot.ts +114 -0
  51. package/src/cli/core/daemon/spawn.ts +171 -0
  52. package/src/cli/core/exec-compiler.ts +169 -0
  53. package/src/cli/core/prompt.ts +94 -0
  54. package/src/cli/core/providers/libretto-cloud.ts +2 -6
  55. package/src/cli/core/readonly-exec.ts +2 -1
  56. package/src/cli/router.ts +4 -0
  57. package/src/cli/workers/run-integration-runtime.ts +0 -6
  58. package/src/shared/state/session-state.ts +1 -0
  59. package/dist/cli/core/browser-daemon.js +0 -122
  60. package/src/cli/core/browser-daemon.ts +0 -198
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Hosted-platform billing commands. Stripe is the source of truth
3
+ * for the plan catalog and is also where every tenant — including
4
+ * Free — has a live Subscription. The Stripe Customer Portal is the
5
+ * single management UI: it shows the user's current plan and lets
6
+ * them switch between any of the configured Subscription Update
7
+ * products (Free / Pro / Team).
8
+ *
9
+ * libretto experimental billing portal → Stripe Customer Portal
10
+ * libretto experimental billing status → plan + usage + period end
11
+ *
12
+ * `libretto init` is unchanged. New tenants start on Free automatically
13
+ * (with a real Stripe Customer + Free Subscription created at signup).
14
+ *
15
+ * Auth: requires a session cookie (or LIBRETTO_API_KEY).
16
+ */
17
+
18
+ import { SimpleCLI } from "../framework/simple-cli.js";
19
+ import {
20
+ NOT_AUTHENTICATED_MESSAGE,
21
+ orpcCall,
22
+ pickCredential,
23
+ resolveApiUrl,
24
+ } from "../core/auth-fetch.js";
25
+ import { readAuthState } from "../core/auth-storage.js";
26
+
27
+ // Marketing-site URL — used for BAA requests and Enterprise contact.
28
+ const CONTACT_URL = "https://libretto.sh";
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Types — mirrored from api/src/routes/billing/subscription.ts
32
+ // ---------------------------------------------------------------------------
33
+
34
+ type SubscriptionResponse = {
35
+ plan: string;
36
+ status: string;
37
+ currentPeriodEnd: string | null;
38
+ cancelAtPeriodEnd: boolean;
39
+ browserHoursUsedThisPeriod: number;
40
+ browserHoursLimit: number | null;
41
+ };
42
+
43
+ type OpenPlansPageResponse = {
44
+ url: string;
45
+ };
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Shared helpers
49
+ // ---------------------------------------------------------------------------
50
+
51
+ async function requireAuth(): Promise<{ apiUrl: string; credential: ReturnType<typeof pickCredential> }> {
52
+ const stored = await readAuthState();
53
+ const apiUrl = resolveApiUrl(stored);
54
+ const credential = pickCredential(stored);
55
+ if (credential.source === "none") {
56
+ throw new Error(NOT_AUTHENTICATED_MESSAGE);
57
+ }
58
+ return { apiUrl, credential };
59
+ }
60
+
61
+ function formatLimit(limit: number | null): string {
62
+ return limit === null ? "∞" : String(limit);
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // portal: always opens the Stripe Customer Portal — every tenant has a
67
+ // live Stripe Subscription (Free or paid) so the portal always has
68
+ // something to show. It's where users see their current plan and switch
69
+ // to another. We DON'T branch on plan / status here.
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export const billingPortalCommand = SimpleCLI.command({
73
+ description: "Open the libretto plans page (current plan + switch options)",
74
+ experimental: true,
75
+ })
76
+ .handle(async () => {
77
+ const { apiUrl, credential } = await requireAuth();
78
+ const { url } = await orpcCall<OpenPlansPageResponse>({
79
+ apiUrl,
80
+ path: "/v1/billing/openPlansPage",
81
+ credential,
82
+ });
83
+ console.log("Open this URL in your browser to choose or change your plan:");
84
+ console.log(` ${url}`);
85
+ console.log();
86
+ console.log(
87
+ "(Shows all tiers with features, your current plan, and a Manage payment / invoices link.)",
88
+ );
89
+ console.log(
90
+ `For a BAA or Enterprise pricing, contact us at ${CONTACT_URL}.`,
91
+ );
92
+ });
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // status
96
+ // ---------------------------------------------------------------------------
97
+
98
+ export const billingStatusCommand = SimpleCLI.command({
99
+ description: "Print the current plan, status, and browser-hour usage",
100
+ experimental: true,
101
+ })
102
+ .handle(async () => {
103
+ const { apiUrl, credential } = await requireAuth();
104
+ const sub = await orpcCall<SubscriptionResponse>({
105
+ apiUrl,
106
+ path: "/v1/billing/subscription",
107
+ credential,
108
+ });
109
+
110
+ const used = sub.browserHoursUsedThisPeriod.toFixed(2);
111
+ const limit = formatLimit(sub.browserHoursLimit);
112
+
113
+ console.log(`Plan: ${sub.plan} (${sub.status})`);
114
+ console.log(`Usage: ${used} / ${limit} browser hours this period`);
115
+ if (sub.currentPeriodEnd) {
116
+ console.log(`Period: ends ${sub.currentPeriodEnd.slice(0, 10)}`);
117
+ }
118
+ if (sub.cancelAtPeriodEnd) {
119
+ console.log("Note: cancellation scheduled at period end.");
120
+ }
121
+ });
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Group export
125
+ // ---------------------------------------------------------------------------
126
+
127
+ export const billingCommands = SimpleCLI.group({
128
+ description: "Hosted-platform subscription + usage commands",
129
+ routes: {
130
+ portal: billingPortalCommand,
131
+ status: billingStatusCommand,
132
+ },
133
+ });
@@ -80,6 +80,10 @@ export const openInput = SimpleCLI.input({
80
80
  name: "write-access",
81
81
  help: "Create the session in write-access mode (overrides config default)",
82
82
  }),
83
+ authProfile: SimpleCLI.option(z.string().optional(), {
84
+ name: "auth-profile",
85
+ help: "Override the domain used for auth profile lookup (e.g. use login.example.com's profile when opening app.example.com)",
86
+ }),
83
87
  viewport: SimpleCLI.option(z.string().optional(), {
84
88
  help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
85
89
  }),
@@ -91,7 +95,7 @@ export const openInput = SimpleCLI.input({
91
95
  })
92
96
  .refine(
93
97
  (input) => Boolean(input.url),
94
- `Usage: libretto open <url> [--headless] [--read-only|--write-access] [--viewport WxH] [--session <name>]`,
98
+ `Usage: libretto open <url> [--headless] [--read-only|--write-access] [--auth-profile <domain>] [--viewport WxH] [--session <name>]`,
95
99
  )
96
100
  .refine(
97
101
  (input) => !(input.headed && input.headless),
@@ -103,7 +107,8 @@ export const openInput = SimpleCLI.input({
103
107
  );
104
108
 
105
109
  export const openCommand = SimpleCLI.command({
106
- description: "Launch browser and open URL (headed by default)",
110
+ description:
111
+ "Launch browser and open URL (headed by default). Automatically loads a saved auth profile for the URL's domain if one exists.",
107
112
  })
108
113
  .input(openInput)
109
114
  .use(withAutoSession())
@@ -120,6 +125,7 @@ export const openCommand = SimpleCLI.command({
120
125
  input.readOnly,
121
126
  input.writeAccess,
122
127
  ),
128
+ authProfileDomain: input.authProfile,
123
129
  });
124
130
  } else {
125
131
  const provider = getCloudProviderApi(providerName);
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { z } from "zod";
3
+ import { HOSTED_API_URL } from "../core/auth-fetch.js";
3
4
  import { buildHostedDeployTarball } from "../core/deploy-artifact.js";
4
5
  import { SimpleCLI } from "../framework/simple-cli.js";
5
6
 
@@ -19,21 +20,15 @@ function generateDeploymentName(): string {
19
20
  }
20
21
 
21
22
  function getConfig() {
22
- const apiUrl = process.env.LIBRETTO_API_URL;
23
23
  const apiKey = process.env.LIBRETTO_API_KEY;
24
24
 
25
- if (!apiUrl) {
26
- throw new Error(
27
- "LIBRETTO_API_URL environment variable is required.",
28
- );
29
- }
30
25
  if (!apiKey) {
31
26
  throw new Error(
32
27
  "LIBRETTO_API_KEY environment variable is required.",
33
28
  );
34
29
  }
35
30
 
36
- return { apiUrl: apiUrl.replace(/\/$/, ""), apiKey };
31
+ return { apiUrl: HOSTED_API_URL, apiKey };
37
32
  }
38
33
 
39
34
  async function postJson(
@@ -17,18 +17,24 @@ import {
17
17
  assertSessionAllowsCommand,
18
18
  clearSessionState,
19
19
  readSessionState,
20
+ readSessionStateOrThrow,
20
21
  setSessionStatus,
21
22
  type SessionState,
22
23
  } from "../core/session.js";
23
24
  import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
25
+ import { readLibrettoConfig } from "../core/config.js";
26
+ import { resolveProviderName, getCloudProviderApi } from "../core/providers/index.js";
27
+ import {
28
+ compileExecFunction,
29
+ stripEmptyCatchHandlers,
30
+ } from "../core/exec-compiler.js";
31
+ import { DaemonClient } from "../core/daemon/index.js";
32
+ import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
24
33
  import {
25
34
  readActionLog,
26
35
  readNetworkLog,
27
36
  wrapPageForActionLogging,
28
37
  } from "../core/telemetry.js";
29
- import { readLibrettoConfig } from "../core/config.js";
30
- import { resolveProviderName, getCloudProviderApi } from "../core/providers/index.js";
31
- import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
32
38
  import type { RunIntegrationWorkerRequest } from "../workers/run-integration-worker-protocol.js";
33
39
  import { SimpleCLI } from "../framework/simple-cli.js";
34
40
  import {
@@ -38,173 +44,85 @@ import {
38
44
  withRequiredSession,
39
45
  } from "./shared.js";
40
46
 
41
- type ExecFunction = (...args: unknown[]) => Promise<unknown>;
42
47
  type RunIntegrationCommandRequest = RunIntegrationWorkerRequest & {
43
48
  tsconfigPath?: string;
44
49
  };
45
50
  type ExecMode = "exec" | "readonly-exec";
46
51
 
47
- type StripTypeScriptTypesFn = (
48
- code: string,
49
- options?: { mode?: "strip" | "transform" },
50
- ) => string;
51
-
52
- const stripTypeScriptTypes = (
53
- moduleBuiltin as { stripTypeScriptTypes?: StripTypeScriptTypesFn }
54
- ).stripTypeScriptTypes;
55
52
  const require = moduleBuiltin.createRequire(import.meta.url);
56
53
  const tsxCliPath = require.resolve("tsx/cli");
57
54
 
58
- function withSuppressedStripTypeScriptWarning<T>(action: () => T): T {
59
- type EmitWarningFn = (...args: unknown[]) => void;
60
- const mutableProcess = process as unknown as { emitWarning: EmitWarningFn };
61
- const originalEmitWarning = mutableProcess.emitWarning;
62
-
63
- mutableProcess.emitWarning = (...args: unknown[]) => {
64
- const warning = args[0];
65
- const typeOrOptions = args[1];
66
- const warningMessage =
67
- typeof warning === "string"
68
- ? warning
69
- : warning instanceof Error
70
- ? warning.message
71
- : "";
72
- const warningType =
73
- typeof typeOrOptions === "string"
74
- ? typeOrOptions
75
- : typeof typeOrOptions === "object" &&
76
- typeOrOptions !== null &&
77
- "type" in typeOrOptions &&
78
- typeof (typeOrOptions as { type?: unknown }).type === "string"
79
- ? ((typeOrOptions as { type?: string }).type ?? "")
80
- : "";
81
-
82
- if (
83
- warningType === "ExperimentalWarning" &&
84
- warningMessage.includes("stripTypeScriptTypes")
85
- ) {
86
- return;
87
- }
88
- originalEmitWarning(...args);
89
- };
90
-
91
- try {
92
- return action();
93
- } finally {
94
- mutableProcess.emitWarning = originalEmitWarning;
55
+ function writeDaemonExecOutput(output?: { stdout: string; stderr: string }) {
56
+ if (output?.stdout) {
57
+ process.stdout.write(output.stdout);
58
+ }
59
+ if (output?.stderr) {
60
+ process.stderr.write(output.stderr);
95
61
  }
96
62
  }
97
63
 
98
- function compileTypeScriptExecFunction(
64
+ async function execViaDaemon(
99
65
  code: string,
100
- helperNames: string[],
101
- ): ExecFunction | null {
102
- if (!stripTypeScriptTypes) return null;
66
+ session: string,
67
+ daemonSocketPath: string,
68
+ logger: LoggerApi,
69
+ options: {
70
+ visualize?: boolean;
71
+ pageId?: string;
72
+ mode?: ExecMode;
73
+ },
74
+ ): Promise<void> {
75
+ const mode = options.mode ?? "exec";
76
+ const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
77
+ if (strippedCount > 0) {
78
+ console.log("(Stripped `.catch(() => {})` — letting errors bubble up)");
79
+ }
80
+ logger.info(`${mode}-start`, {
81
+ session,
82
+ codeLength: cleanedCode.length,
83
+ codePreview: cleanedCode.slice(0, 200),
84
+ visualize: options.visualize,
85
+ pageId: options.pageId,
86
+ via: "daemon",
87
+ });
103
88
 
104
- const wrappedSource = `(async function __librettoExec(${helperNames.join(", ")}) {\n${code}\n})`;
105
- const jsSource = withSuppressedStripTypeScriptWarning(() =>
106
- stripTypeScriptTypes(wrappedSource, { mode: "strip" }),
107
- );
108
- const createFunction = new Function(
109
- `return ${jsSource}`,
110
- ) as () => ExecFunction;
111
- return createFunction();
112
- }
89
+ const client = new DaemonClient(daemonSocketPath);
90
+
91
+ const response =
92
+ mode === "exec"
93
+ ? await client.exec({
94
+ code: cleanedCode,
95
+ pageId: options.pageId,
96
+ visualize: options.visualize,
97
+ })
98
+ : await client.readonlyExec({
99
+ code: cleanedCode,
100
+ pageId: options.pageId,
101
+ });
102
+
103
+ if (!response.ok) {
104
+ writeDaemonExecOutput(response.output);
105
+ throw new Error(response.message);
106
+ }
113
107
 
114
- function compileExecFunction(
115
- code: string,
116
- helperNames: string[],
117
- ): ExecFunction {
118
- const typeStripped = compileTypeScriptExecFunction(code, helperNames);
119
- if (typeStripped) return typeStripped;
120
-
121
- const AsyncFunction = Object.getPrototypeOf(async function () {})
122
- .constructor as new (...args: string[]) => ExecFunction;
123
- return new AsyncFunction(...helperNames, code);
124
- }
108
+ const { result, output } = response.data;
109
+ writeDaemonExecOutput(output);
125
110
 
126
- /**
127
- * Strip `.catch(() => {})` / `?.catch(() => {})` from executable code,
128
- * skipping occurrences inside string literals (single, double, backtick)
129
- * and single-line / multi-line comments so we never corrupt non-code text.
130
- */
131
- function stripEmptyCatchHandlers(code: string): {
132
- cleaned: string;
133
- strippedCount: number;
134
- } {
135
- const catchRe = /\??\s*\.catch\(\s*\(\)\s*=>\s*\{\s*\}\s*\)/g;
136
- let strippedCount = 0;
137
- let result = "";
138
- let i = 0;
139
-
140
- while (i < code.length) {
141
- // Single-line comment
142
- if (code[i] === "/" && code[i + 1] === "/") {
143
- const end = code.indexOf("\n", i);
144
- const slice = end === -1 ? code.slice(i) : code.slice(i, end + 1);
145
- result += slice;
146
- i += slice.length;
147
- continue;
148
- }
149
- // Multi-line comment
150
- if (code[i] === "/" && code[i + 1] === "*") {
151
- const end = code.indexOf("*/", i + 2);
152
- const slice = end === -1 ? code.slice(i) : code.slice(i, end + 2);
153
- result += slice;
154
- i += slice.length;
155
- continue;
156
- }
157
- // String literals
158
- if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
159
- const quote = code[i];
160
- let j = i + 1;
161
- while (j < code.length) {
162
- if (code[j] === "\\" && quote !== "`") {
163
- j += 2;
164
- continue;
165
- }
166
- if (code[j] === "\\" && quote === "`") {
167
- j += 2;
168
- continue;
169
- }
170
- if (code[j] === quote) {
171
- j++;
172
- break;
173
- }
174
- // Template literal interpolation — skip nested braces
175
- if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
176
- let depth = 1;
177
- j += 2;
178
- while (j < code.length && depth > 0) {
179
- if (code[j] === "{") depth++;
180
- else if (code[j] === "}") depth--;
181
- j++;
182
- }
183
- continue;
184
- }
185
- j++;
186
- }
187
- result += code.slice(i, j);
188
- i = j;
189
- continue;
190
- }
191
- // Try to match the catch pattern at the current position
192
- catchRe.lastIndex = i;
193
- const match = catchRe.exec(code);
194
- if (match && match.index === i) {
195
- strippedCount++;
196
- i += match[0].length;
197
- continue;
198
- }
199
- // Regular character
200
- result += code[i];
201
- i++;
111
+ logger.info(`${mode}-success`, {
112
+ session,
113
+ hasResult: result !== undefined,
114
+ via: "daemon",
115
+ });
116
+ if (result !== undefined) {
117
+ console.log(
118
+ typeof result === "string" ? result : JSON.stringify(result, null, 2),
119
+ );
120
+ } else {
121
+ console.log("Executed successfully");
202
122
  }
203
-
204
- return { cleaned: result, strippedCount };
205
123
  }
206
124
 
207
- async function runExec(
125
+ async function execViaCdpFallback(
208
126
  code: string,
209
127
  session: string,
210
128
  logger: LoggerApi,
@@ -212,7 +130,7 @@ async function runExec(
212
130
  visualize?: boolean;
213
131
  pageId?: string;
214
132
  mode?: ExecMode;
215
- } = {},
133
+ },
216
134
  ): Promise<void> {
217
135
  const visualize = options.visualize ?? false;
218
136
  const pageId = options.pageId;
@@ -227,7 +145,9 @@ async function runExec(
227
145
  codePreview: cleanedCode.slice(0, 200),
228
146
  visualize,
229
147
  pageId,
148
+ via: "cdp-fallback",
230
149
  });
150
+
231
151
  const {
232
152
  browser,
233
153
  context,
@@ -235,7 +155,6 @@ async function runExec(
235
155
  pageId: resolvedPageId,
236
156
  } = await connect(session, logger, 10000, {
237
157
  pageId,
238
- requireSinglePage: true,
239
158
  });
240
159
 
241
160
  const STALL_THRESHOLD_MS = 60_000;
@@ -251,6 +170,7 @@ async function runExec(
251
170
  session,
252
171
  silenceMs,
253
172
  codePreview: cleanedCode.slice(0, 200),
173
+ via: "cdp-fallback",
254
174
  });
255
175
  console.warn(
256
176
  `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1000)}s — ${mode} may be hung (code: ${cleanedCode.slice(0, 100)}...)`,
@@ -264,6 +184,7 @@ async function runExec(
264
184
  session,
265
185
  duration: Date.now() - execStartTs,
266
186
  codePreview: cleanedCode.slice(0, 200),
187
+ via: "cdp-fallback",
267
188
  });
268
189
  };
269
190
  process.on("SIGINT", sigintHandler);
@@ -277,24 +198,24 @@ async function runExec(
277
198
  }
278
199
 
279
200
  try {
201
+ const execState: Record<string, unknown> = {};
280
202
  const helpers =
281
203
  mode === "readonly-exec"
282
204
  ? createReadonlyExecHelpers(page, { onActivity })
283
- : (() => {
284
- const execState: Record<string, unknown> = {};
285
-
286
- const networkLog = (
205
+ : {
206
+ page,
207
+ context,
208
+ state: execState,
209
+ browser,
210
+ networkLog: (
287
211
  opts: {
288
212
  last?: number;
289
213
  filter?: string;
290
214
  method?: string;
291
215
  pageId?: string;
292
216
  } = {},
293
- ) => {
294
- return readNetworkLog(session, opts);
295
- };
296
-
297
- const actionLog = (
217
+ ) => readNetworkLog(session, opts),
218
+ actionLog: (
298
219
  opts: {
299
220
  last?: number;
300
221
  filter?: string;
@@ -302,33 +223,25 @@ async function runExec(
302
223
  source?: string;
303
224
  pageId?: string;
304
225
  } = {},
305
- ) => {
306
- return readActionLog(session, opts);
307
- };
308
-
309
- return {
310
- page,
311
- context,
312
- state: execState,
313
- browser,
314
- networkLog,
315
- actionLog,
316
- console,
317
- setTimeout,
318
- setInterval,
319
- clearTimeout,
320
- clearInterval,
321
- fetch,
322
- URL,
323
- Buffer,
324
- };
325
- })();
226
+ ) => readActionLog(session, opts),
227
+ console,
228
+ setTimeout,
229
+ setInterval,
230
+ clearTimeout,
231
+ clearInterval,
232
+ fetch,
233
+ URL,
234
+ Buffer,
235
+ };
326
236
 
327
237
  const helperNames = Object.keys(helpers);
328
238
  const fn = compileExecFunction(cleanedCode, helperNames);
329
-
330
239
  const result = await fn(...Object.values(helpers));
331
- logger.info(`${mode}-success`, { session, hasResult: result !== undefined });
240
+ logger.info(`${mode}-success`, {
241
+ session,
242
+ hasResult: result !== undefined,
243
+ via: "cdp-fallback",
244
+ });
332
245
  if (result !== undefined) {
333
246
  console.log(
334
247
  typeof result === "string" ? result : JSON.stringify(result, null, 2),
@@ -341,6 +254,7 @@ async function runExec(
341
254
  error: err,
342
255
  session,
343
256
  codePreview: cleanedCode.slice(0, 200),
257
+ via: "cdp-fallback",
344
258
  });
345
259
  throw err;
346
260
  } finally {
@@ -350,6 +264,32 @@ async function runExec(
350
264
  }
351
265
  }
352
266
 
267
+ async function runExec(
268
+ code: string,
269
+ session: string,
270
+ logger: LoggerApi,
271
+ options: {
272
+ visualize?: boolean;
273
+ pageId?: string;
274
+ mode?: ExecMode;
275
+ } = {},
276
+ ): Promise<void> {
277
+ const state = readSessionStateOrThrow(session);
278
+ if (!state.daemonSocketPath) {
279
+ // Compatibility fallback for failed runs created before `run` became
280
+ // daemon-backed: those session states can have a live CDP endpoint/port but
281
+ // no daemon socket. Keep `exec` inspection working until such sessions are
282
+ // gone. Context: https://www.notion.so/Make-libretto-run-daemon-backed-for-failed-workflow-inspection-352ac9fb35f181c1b7d3f08c0a735e9d
283
+ logger.warn(`${options.mode ?? "exec"}-daemon-socket-missing-cdp-fallback`, {
284
+ session,
285
+ hasCdpEndpoint: Boolean(state.cdpEndpoint),
286
+ port: state.port,
287
+ });
288
+ return execViaCdpFallback(code, session, logger, options);
289
+ }
290
+ return execViaDaemon(code, session, state.daemonSocketPath, logger, options);
291
+ }
292
+
353
293
  function parseJsonArg(label: string, raw: string): unknown {
354
294
  try {
355
295
  return JSON.parse(raw);