libretto 0.6.12 → 0.6.14

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 (82) hide show
  1. package/README.md +3 -8
  2. package/README.template.md +3 -8
  3. package/dist/cli/cli.js +0 -23
  4. package/dist/cli/commands/auth.js +24 -33
  5. package/dist/cli/commands/billing.js +3 -5
  6. package/dist/cli/commands/browser.js +4 -13
  7. package/dist/cli/commands/deploy.js +54 -45
  8. package/dist/cli/commands/execution.js +6 -3
  9. package/dist/cli/commands/experiments.js +1 -1
  10. package/dist/cli/commands/setup.js +2 -295
  11. package/dist/cli/commands/shared.js +1 -1
  12. package/dist/cli/commands/snapshot.js +10 -100
  13. package/dist/cli/commands/status.js +2 -42
  14. package/dist/cli/core/auth-fetch.js +11 -6
  15. package/dist/cli/core/browser.js +13 -8
  16. package/dist/cli/core/config.js +3 -6
  17. package/dist/cli/core/daemon/daemon.js +88 -74
  18. package/dist/cli/core/daemon/exec-repl.js +133 -0
  19. package/dist/cli/core/daemon/exec.js +6 -21
  20. package/dist/cli/core/daemon/ipc.js +47 -4
  21. package/dist/cli/core/daemon/ipc.spec.js +21 -0
  22. package/dist/cli/core/daemon/snapshot.js +2 -29
  23. package/dist/cli/core/exec-compiler.js +8 -3
  24. package/dist/cli/core/experiments.js +1 -28
  25. package/dist/cli/core/providers/index.js +13 -4
  26. package/dist/cli/core/providers/libretto-cloud.js +178 -26
  27. package/dist/cli/index.js +0 -2
  28. package/dist/cli/router.js +9 -6
  29. package/dist/shared/instrumentation/instrument.js +4 -4
  30. package/dist/shared/ipc/socket-transport.d.ts +2 -1
  31. package/dist/shared/ipc/socket-transport.js +16 -5
  32. package/dist/shared/ipc/socket-transport.spec.js +5 -0
  33. package/docs/releasing.md +8 -6
  34. package/package.json +3 -2
  35. package/skills/libretto/SKILL.md +49 -47
  36. package/skills/libretto/references/code-generation-rules.md +6 -0
  37. package/skills/libretto/references/configuration-file-reference.md +14 -12
  38. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  39. package/skills/libretto/references/site-security-review.md +6 -6
  40. package/skills/libretto-readonly/SKILL.md +2 -9
  41. package/src/cli/cli.ts +0 -24
  42. package/src/cli/commands/auth.ts +24 -33
  43. package/src/cli/commands/billing.ts +3 -5
  44. package/src/cli/commands/browser.ts +6 -16
  45. package/src/cli/commands/deploy.ts +55 -49
  46. package/src/cli/commands/execution.ts +6 -3
  47. package/src/cli/commands/experiments.ts +1 -1
  48. package/src/cli/commands/setup.ts +2 -381
  49. package/src/cli/commands/shared.ts +1 -1
  50. package/src/cli/commands/snapshot.ts +9 -137
  51. package/src/cli/commands/status.ts +2 -50
  52. package/src/cli/core/auth-fetch.ts +9 -4
  53. package/src/cli/core/browser.ts +15 -8
  54. package/src/cli/core/config.ts +3 -6
  55. package/src/cli/core/daemon/daemon.ts +106 -76
  56. package/src/cli/core/daemon/exec-repl.ts +189 -0
  57. package/src/cli/core/daemon/exec.ts +8 -43
  58. package/src/cli/core/daemon/ipc.spec.ts +27 -0
  59. package/src/cli/core/daemon/ipc.ts +81 -23
  60. package/src/cli/core/daemon/snapshot.ts +1 -43
  61. package/src/cli/core/exec-compiler.ts +8 -3
  62. package/src/cli/core/experiments.ts +9 -38
  63. package/src/cli/core/providers/index.ts +17 -4
  64. package/src/cli/core/providers/libretto-cloud.ts +224 -36
  65. package/src/cli/core/resolve-model.ts +5 -0
  66. package/src/cli/core/workflow-runtime.ts +1 -0
  67. package/src/cli/index.ts +0 -1
  68. package/src/cli/router.ts +9 -6
  69. package/src/shared/instrumentation/instrument.ts +4 -4
  70. package/src/shared/ipc/socket-transport.spec.ts +6 -0
  71. package/src/shared/ipc/socket-transport.ts +20 -5
  72. package/dist/cli/commands/ai.js +0 -110
  73. package/dist/cli/core/ai-model.js +0 -195
  74. package/dist/cli/core/api-snapshot-analyzer.js +0 -86
  75. package/dist/cli/core/snapshot-analyzer.js +0 -667
  76. package/dist/cli/framework/simple-cli.js +0 -880
  77. package/scripts/summarize-evals.mjs +0 -135
  78. package/src/cli/commands/ai.ts +0 -144
  79. package/src/cli/core/ai-model.ts +0 -301
  80. package/src/cli/core/api-snapshot-analyzer.ts +0 -110
  81. package/src/cli/core/snapshot-analyzer.ts +0 -856
  82. package/src/cli/framework/simple-cli.ts +0 -1459
@@ -63,7 +63,8 @@ import {
63
63
  } from "../browser.js";
64
64
  import { handlePages } from "./pages.js";
65
65
  import { handleExec, handleReadonlyExec } from "./exec.js";
66
- import { handleCompactSnapshot, handleSnapshot } from "./snapshot.js";
66
+ import { DaemonExecRepl } from "./exec-repl.js";
67
+ import { handleCompactSnapshot } from "./snapshot.js";
67
68
  import { librettoCommand } from "../../../shared/package-manager.js";
68
69
  import type { Snapshot } from "../../../shared/snapshot/types.js";
69
70
  import { snapshot } from "../../../shared/snapshot/capture-snapshot.js";
@@ -82,7 +83,7 @@ import {
82
83
  } from "./config.js";
83
84
  import type { Experiments } from "../experiments.js";
84
85
  import { getCloudProviderApi } from "../providers/index.js";
85
- import type { ProviderApi } from "../providers/types.js";
86
+ import type { ProviderApi, ProviderSession } from "../providers/types.js";
86
87
  import {
87
88
  getAbsoluteIntegrationPath,
88
89
  loadDefaultWorkflow,
@@ -160,7 +161,7 @@ const REQUEST_TIMEOUT_MS = 60_000;
160
161
 
161
162
  class BrowserDaemon {
162
163
  readonly logger: LoggerApi;
163
- private readonly execState: Record<string, unknown> = {};
164
+ private readonly execRepl: DaemonExecRepl;
164
165
  private readonly pageById = new Map<string, Page>();
165
166
  private readonly shutdownHandlers: ShutdownHandler[] = [];
166
167
  private readonly connectedClis = new Set<IpcPeer<DaemonToCliApi>>();
@@ -183,6 +184,10 @@ class BrowserDaemon {
183
184
  },
184
185
  ) {
185
186
  this.logger = logger.withScope("child");
187
+ this.execRepl = new DaemonExecRepl({
188
+ browser: this.browser,
189
+ context: this.context,
190
+ });
186
191
  }
187
192
 
188
193
  private trackPage(page: Page): string {
@@ -230,6 +235,7 @@ class BrowserDaemon {
230
235
  name: string;
231
236
  sessionId: string;
232
237
  };
238
+ beforeReady?: () => void;
233
239
  }): Promise<BrowserDaemon> {
234
240
  const {
235
241
  session,
@@ -242,6 +248,7 @@ class BrowserDaemon {
242
248
  navigateUrl,
243
249
  readyProvider,
244
250
  providerSession,
251
+ beforeReady,
245
252
  } = args;
246
253
 
247
254
  await mkdir(getSessionDir(session), { recursive: true });
@@ -271,9 +278,7 @@ class BrowserDaemon {
271
278
  });
272
279
  }
273
280
 
274
- if (experiments["compact-snapshot-format"]) {
275
- await context.addInitScript(installPageStabilityWaiter);
276
- }
281
+ await context.addInitScript(installPageStabilityWaiter);
277
282
 
278
283
  // IPC server — typed handlers are attached per client connection so one
279
284
  // daemon lifetime can serve multiple CLI invocations.
@@ -295,19 +300,15 @@ class BrowserDaemon {
295
300
  wrapPageForActionLogging(p, session);
296
301
  daemon.trackPage(p);
297
302
  }
298
- if (experiments["compact-snapshot-format"]) {
299
- await Promise.all(
300
- initialPages.map((initialPage) =>
301
- daemon.installCompactSnapshotWaiter(initialPage),
302
- ),
303
- );
304
- }
303
+ await Promise.all(
304
+ initialPages.map((initialPage) =>
305
+ daemon.installCompactSnapshotWaiter(initialPage),
306
+ ),
307
+ );
305
308
  context.on("page", (newPage) => {
306
309
  wrapPageForActionLogging(newPage, session);
307
310
  daemon.trackPage(newPage);
308
- if (experiments["compact-snapshot-format"]) {
309
- void daemon.installCompactSnapshotWaiter(newPage);
310
- }
311
+ void daemon.installCompactSnapshotWaiter(newPage);
311
312
  });
312
313
 
313
314
  // Navigate after telemetry is installed (so we capture the initial
@@ -352,6 +353,7 @@ class BrowserDaemon {
352
353
  });
353
354
 
354
355
  await listenOnIpcSocket(ipcServer, socketPath);
356
+ beforeReady?.();
355
357
  process.send?.({ type: "ready", socketPath, provider: readyProvider });
356
358
  daemon.logger.info("ipc-server-listening", { socketPath });
357
359
 
@@ -477,8 +479,13 @@ class BrowserDaemon {
477
479
  }): Promise<BrowserDaemon> {
478
480
  const { session, browser: config } = args;
479
481
  const provider = getCloudProviderApi(config.providerName);
480
- const providerSession = await provider.createSession();
482
+ let providerSession: ProviderSession | undefined;
483
+ const startupCleanup = createProviderStartupCleanup({
484
+ provider,
485
+ getProviderSession: () => providerSession,
486
+ });
481
487
  try {
488
+ providerSession = await provider.createSession();
482
489
  const browser = await chromium.connectOverCDP(
483
490
  providerSession.cdpEndpoint,
484
491
  );
@@ -512,6 +519,7 @@ class BrowserDaemon {
512
519
  name: config.providerName,
513
520
  sessionId: providerSession.sessionId,
514
521
  },
522
+ beforeReady: startupCleanup.dispose,
515
523
  });
516
524
 
517
525
  daemon.logger.info("child-provider-connected", {
@@ -524,7 +532,10 @@ class BrowserDaemon {
524
532
 
525
533
  return daemon;
526
534
  } catch (error) {
527
- await provider.closeSession(providerSession.sessionId);
535
+ startupCleanup.dispose();
536
+ if (providerSession) {
537
+ await provider.closeSession(providerSession.sessionId);
538
+ }
528
539
  throw error;
529
540
  }
530
541
  }
@@ -659,40 +670,23 @@ class BrowserDaemon {
659
670
  private async runSnapshot(
660
671
  args: Parameters<CliToDaemonApi["snapshot"]>[0],
661
672
  ): Promise<ReturnType<CliToDaemonApi["snapshot"]>> {
662
- if (args.mode === "compact") {
663
- if (!this.experiments["compact-snapshot-format"]) {
664
- throw new Error(
665
- `The compact-snapshot-format experiment is not enabled for session "${this.session}". ` +
666
- `Close and reopen the session after running ${librettoCommand("experiments enable compact-snapshot-format")}.`,
667
- );
668
- }
669
- const targetPage = this.resolveTargetPage(args.pageId);
670
- const result = await this.withRequestTimeout(() =>
671
- handleCompactSnapshot(
672
- targetPage,
673
- this.session,
674
- this.logger,
675
- {
676
- pageId: args.pageId,
677
- cachedSnapshot: this.latestCompactSnapshotByPage.get(targetPage),
678
- useCachedSnapshot: args.useCachedSnapshot,
679
- },
680
- ),
681
- );
682
- if (!args.useCachedSnapshot) {
683
- this.latestCompactSnapshotByPage.set(targetPage, result.snapshot);
684
- }
685
- return result;
686
- }
687
-
688
- return this.withRequestTimeout(() =>
689
- handleSnapshot(
690
- this.resolveTargetPage(args.pageId),
673
+ const targetPage = this.resolveTargetPage(args.pageId);
674
+ const result = await this.withRequestTimeout(() =>
675
+ handleCompactSnapshot(
676
+ targetPage,
691
677
  this.session,
692
678
  this.logger,
693
- args.pageId,
679
+ {
680
+ pageId: args.pageId,
681
+ cachedSnapshot: this.latestCompactSnapshotByPage.get(targetPage),
682
+ useCachedSnapshot: args.useCachedSnapshot,
683
+ },
694
684
  ),
695
685
  );
686
+ if (!args.useCachedSnapshot) {
687
+ this.latestCompactSnapshotByPage.set(targetPage, result.snapshot);
688
+ }
689
+ return result;
696
690
  }
697
691
 
698
692
  private async withRequestTimeout<T>(
@@ -720,26 +714,7 @@ class BrowserDaemon {
720
714
  private async runExec(
721
715
  args: Parameters<CliToDaemonApi["exec"]>[0],
722
716
  ): Promise<DaemonExecResult> {
723
- if (this.experiments["compact-snapshot-format"]) {
724
- return this.runCompactExec(args);
725
- }
726
-
727
- try {
728
- const data = await this.withRequestTimeout(() =>
729
- handleExec(
730
- this.resolveTargetPage(args.pageId),
731
- args.code,
732
- this.context,
733
- this.browser,
734
- this.execState,
735
- this.session,
736
- args.visualize,
737
- ),
738
- );
739
- return { ok: true, data };
740
- } catch (error) {
741
- return this.createExecErrorResult(error);
742
- }
717
+ return this.runCompactExec(args);
743
718
  }
744
719
 
745
720
  private async runCompactExec(
@@ -755,10 +730,7 @@ class BrowserDaemon {
755
730
  const result = await handleExec(
756
731
  page,
757
732
  args.code,
758
- this.context,
759
- this.browser,
760
- this.execState,
761
- this.session,
733
+ this.execRepl,
762
734
  args.visualize,
763
735
  );
764
736
 
@@ -831,17 +803,17 @@ class BrowserDaemon {
831
803
  context: this.context,
832
804
  logger: this.logger,
833
805
  onLog: (event) => {
834
- this.broadcast("workflowOutput", event);
806
+ void this.broadcast("workflowOutput", event);
835
807
  },
836
808
  onOutcome: (outcome) => {
837
809
  if (outcome.state === "paused") {
838
- this.broadcast("workflowPaused", {
810
+ void this.broadcast("workflowPaused", {
839
811
  pausedAt: outcome.pausedAt,
840
812
  url: outcome.url,
841
813
  });
842
814
  return;
843
815
  }
844
- this.broadcast(
816
+ void this.broadcast(
845
817
  "workflowFinished",
846
818
  outcome.result === "completed"
847
819
  ? { result: "completed", completedAt: outcome.completedAt }
@@ -898,6 +870,62 @@ class BrowserDaemon {
898
870
  }
899
871
  }
900
872
 
873
+ function createProviderStartupCleanup(args: {
874
+ provider: ProviderApi;
875
+ getProviderSession: () => ProviderSession | undefined;
876
+ }): { dispose: () => void } {
877
+ let disposed = false;
878
+ let fallbackExit: NodeJS.Timeout | undefined;
879
+
880
+ const requestClose = (reason: string): void => {
881
+ if (disposed) return;
882
+ disposed = true;
883
+ // Startup cancellation should preserve the signal-style exit code even if
884
+ // provider.createSession() later rejects through the normal startup path.
885
+ process.exitCode = reason === "received SIGINT" ? 130 : 1;
886
+ const providerSession = args.getProviderSession();
887
+ if (!providerSession) {
888
+ // If cancellation lands while createSession() is still awaiting provider
889
+ // capacity, provider-specific signal handlers may still need a tick to
890
+ // close a queued session id that this daemon has not received yet.
891
+ fallbackExit = setTimeout(() => {
892
+ process.exit(process.exitCode);
893
+ }, 5_000);
894
+ fallbackExit.unref();
895
+ return;
896
+ }
897
+
898
+ void args.provider
899
+ .closeSession(providerSession.sessionId)
900
+ .catch(() => {})
901
+ .finally(() => {
902
+ process.exit(reason === "received SIGINT" ? 130 : 1);
903
+ });
904
+ };
905
+
906
+ const onDisconnect = (): void => requestClose("parent command disconnected");
907
+ const onSigint = (): void => requestClose("received SIGINT");
908
+ const onSigterm = (): void => requestClose("received SIGTERM");
909
+
910
+ // These handlers only cover the pre-ready window. Once the daemon is ready,
911
+ // normal shutdown owns provider cleanup through BrowserDaemon.shutdown().
912
+ if (typeof process.send === "function") {
913
+ process.once("disconnect", onDisconnect);
914
+ }
915
+ process.once("SIGINT", onSigint);
916
+ process.once("SIGTERM", onSigterm);
917
+
918
+ return {
919
+ dispose: () => {
920
+ disposed = true;
921
+ if (fallbackExit) clearTimeout(fallbackExit);
922
+ process.off("disconnect", onDisconnect);
923
+ process.off("SIGINT", onSigint);
924
+ process.off("SIGTERM", onSigterm);
925
+ },
926
+ };
927
+ }
928
+
901
929
  // ── Main ───────────────────────────────────────────────────────────────
902
930
 
903
931
  async function main(): Promise<void> {
@@ -999,7 +1027,9 @@ function reportStartupError(error: unknown): never {
999
1027
  message: error.message,
1000
1028
  });
1001
1029
  }
1002
- process.exit(1);
1030
+ process.exit(
1031
+ process.exitCode && process.exitCode !== 0 ? process.exitCode : 1,
1032
+ );
1003
1033
  }
1004
1034
 
1005
1035
  try {
@@ -0,0 +1,189 @@
1
+ import * as repl from "node:repl";
2
+ import { PassThrough } from "node:stream";
3
+ import { format, formatWithOptions, type InspectOptions } from "node:util";
4
+ import { stripTypeScriptExecCode } from "../exec-compiler.js";
5
+
6
+ const PROMPT = "__LIBRETTO_EXEC_REPL_READY__";
7
+ const TOP_LEVEL_RETURN_HINT =
8
+ "Hint: top-level return isn't supported because exec is a REPL-style environment. Use the expression value instead, for example: await page.title()";
9
+ const NO_RESULT = Symbol("NO_RESULT");
10
+
11
+ type ReplOutput = {
12
+ stdout: string;
13
+ stderr: string;
14
+ };
15
+
16
+ export type DaemonExecReplResult = {
17
+ ok: true;
18
+ result: unknown;
19
+ output: ReplOutput;
20
+ };
21
+
22
+ export type DaemonExecReplFailure = {
23
+ ok: false;
24
+ error: Error;
25
+ output: ReplOutput;
26
+ };
27
+
28
+ export type DaemonExecReplResponse = DaemonExecReplResult | DaemonExecReplFailure;
29
+
30
+ type ActiveEval = {
31
+ output: string;
32
+ consoleOutput: ReplOutput;
33
+ resolve: (value: DaemonExecReplResponse) => void;
34
+ };
35
+
36
+ function getErrorMessage(error: unknown): string {
37
+ if (error instanceof Error) return error.message;
38
+ if (
39
+ typeof error === "object" &&
40
+ error !== null &&
41
+ "message" in error &&
42
+ typeof error.message === "string"
43
+ ) {
44
+ return error.message;
45
+ }
46
+ return String(error);
47
+ }
48
+
49
+ function isTopLevelReturnError(error: unknown): boolean {
50
+ const message = getErrorMessage(error);
51
+ return (
52
+ message.includes("Illegal return statement") ||
53
+ message.includes("Return statement is not allowed here")
54
+ );
55
+ }
56
+
57
+ function isErrorLike(value: unknown): boolean {
58
+ return (
59
+ value instanceof Error ||
60
+ (typeof value === "object" &&
61
+ value !== null &&
62
+ "name" in value &&
63
+ "message" in value &&
64
+ typeof value.name === "string" &&
65
+ typeof value.message === "string")
66
+ );
67
+ }
68
+
69
+ function toError(value: unknown): Error {
70
+ return value instanceof Error ? value : new Error(getErrorMessage(value));
71
+ }
72
+
73
+ function appendTopLevelReturnHint(error: unknown): Error {
74
+ const message = getErrorMessage(error);
75
+ if (message.includes(TOP_LEVEL_RETURN_HINT)) {
76
+ return error instanceof Error ? error : new Error(message);
77
+ }
78
+ return new SyntaxError(`${message}\n\n${TOP_LEVEL_RETURN_HINT}`);
79
+ }
80
+
81
+ function createBufferedConsole(): { console: Console; output: ReplOutput } {
82
+ const output: ReplOutput = { stdout: "", stderr: "" };
83
+ const writeStdout = (...args: unknown[]) => {
84
+ output.stdout += `${format(...args)}\n`;
85
+ };
86
+ const writeStderr = (...args: unknown[]) => {
87
+ output.stderr += `${format(...args)}\n`;
88
+ };
89
+
90
+ const bufferedConsole = {
91
+ ...globalThis.console,
92
+ log: writeStdout,
93
+ info: writeStdout,
94
+ debug: writeStdout,
95
+ dir: (value?: unknown, options?: InspectOptions) => {
96
+ output.stdout += `${formatWithOptions(options ?? {}, value)}\n`;
97
+ },
98
+ warn: writeStderr,
99
+ error: writeStderr,
100
+ } satisfies Console;
101
+
102
+ return { console: bufferedConsole, output };
103
+ }
104
+
105
+ export class DaemonExecRepl {
106
+ private readonly replServer: repl.REPLServer;
107
+ private readonly input = new PassThrough();
108
+ private readonly output = new PassThrough();
109
+ private readyResolve: (() => void) | undefined;
110
+ private readonly ready: Promise<void>;
111
+ private activeEval: ActiveEval | undefined;
112
+ private lastResult: unknown = NO_RESULT;
113
+
114
+ constructor(globals: Record<string, unknown>) {
115
+ this.ready = new Promise((resolve) => {
116
+ this.readyResolve = resolve;
117
+ });
118
+ this.output.on("data", (chunk: Buffer | string) => {
119
+ this.handleOutput(String(chunk));
120
+ });
121
+ this.replServer = repl.start({
122
+ prompt: PROMPT,
123
+ input: this.input,
124
+ output: this.output,
125
+ terminal: true,
126
+ useGlobal: false,
127
+ writer: (value: unknown) => {
128
+ this.lastResult = value;
129
+ return "";
130
+ },
131
+ });
132
+ Object.assign(this.replServer.context, globals);
133
+ }
134
+
135
+ async run(
136
+ code: string,
137
+ globals: Record<string, unknown>,
138
+ ): Promise<DaemonExecReplResponse> {
139
+ Object.assign(this.replServer.context, globals);
140
+
141
+ let jsCode: string;
142
+ try {
143
+ jsCode = stripTypeScriptExecCode(code);
144
+ } catch (error) {
145
+ return {
146
+ ok: false,
147
+ error: isTopLevelReturnError(error)
148
+ ? appendTopLevelReturnHint(error)
149
+ : toError(error),
150
+ output: { stdout: "", stderr: "" },
151
+ };
152
+ }
153
+
154
+ await this.ready;
155
+ const buffered = createBufferedConsole();
156
+ Object.assign(this.replServer.context, { console: buffered.console });
157
+ return await new Promise<DaemonExecReplResponse>((resolve) => {
158
+ this.activeEval = { output: "", consoleOutput: buffered.output, resolve };
159
+ this.lastResult = NO_RESULT;
160
+ this.input.write(`.editor\n${jsCode}\n\x04`);
161
+ });
162
+ }
163
+
164
+ private handleOutput(chunk: string): void {
165
+ const active = this.activeEval;
166
+ if (!active) {
167
+ if (chunk.includes(PROMPT)) {
168
+ this.readyResolve?.();
169
+ this.readyResolve = undefined;
170
+ }
171
+ return;
172
+ }
173
+
174
+ active.output += chunk;
175
+ if (!active.output.includes(PROMPT)) return;
176
+
177
+ this.activeEval = undefined;
178
+ const result = this.lastResult === NO_RESULT ? undefined : this.lastResult;
179
+ const output = active.consoleOutput;
180
+ if (isErrorLike(result)) {
181
+ const error = isTopLevelReturnError(result)
182
+ ? appendTopLevelReturnHint(result)
183
+ : toError(result);
184
+ active.resolve({ ok: false, error, output });
185
+ return;
186
+ }
187
+ active.resolve({ ok: true, result, output });
188
+ }
189
+ }
@@ -1,9 +1,9 @@
1
- import type { Browser, BrowserContext, Page } from "playwright";
1
+ import type { Page } from "playwright";
2
2
  import { format, formatWithOptions, type InspectOptions } from "node:util";
3
3
  import { installInstrumentation } from "../../../shared/instrumentation/index.js";
4
4
  import { compileExecFunction } from "../exec-compiler.js";
5
5
  import { createReadonlyExecHelpers } from "../readonly-exec.js";
6
- import { readNetworkLog, readActionLog } from "../telemetry.js";
6
+ import type { DaemonExecRepl } from "./exec-repl.js";
7
7
 
8
8
  type ExecOutput = {
9
9
  stdout: string;
@@ -52,58 +52,23 @@ function createBufferedConsole(): { console: Console; output: ExecOutput } {
52
52
  export async function handleExec(
53
53
  targetPage: Page,
54
54
  code: string,
55
- context: BrowserContext,
56
- browser: Browser,
57
- execState: Record<string, unknown>,
58
- session: string,
55
+ execRepl: DaemonExecRepl,
59
56
  visualize?: boolean,
60
57
  ): Promise<ExecResponse> {
61
- const buffered = createBufferedConsole();
62
-
63
58
  if (visualize) {
64
59
  await installInstrumentation(targetPage, { visualize: true });
65
60
  }
66
61
 
67
- const networkLog = (
68
- opts: {
69
- last?: number;
70
- filter?: string;
71
- method?: string;
72
- pageId?: string;
73
- } = {},
74
- ) => readNetworkLog(session, opts);
75
-
76
- const actionLog = (
77
- opts: {
78
- last?: number;
79
- filter?: string;
80
- action?: string;
81
- source?: string;
82
- pageId?: string;
83
- } = {},
84
- ) => readActionLog(session, opts);
85
-
86
62
  const helpers = {
87
63
  page: targetPage,
88
- context,
89
- browser,
90
- state: execState,
91
- console: buffered.console,
92
- networkLog,
93
- actionLog,
64
+ frame: targetPage.mainFrame(),
94
65
  };
95
66
 
96
- const helperNames = Object.keys(helpers);
97
- const fn = compileExecFunction(code, helperNames);
98
- try {
99
- const result = await fn(...Object.values(helpers));
100
- return { result, output: buffered.output };
101
- } catch (error) {
102
- throw new DaemonExecError(
103
- error instanceof Error ? error.message : String(error),
104
- buffered.output,
105
- );
67
+ const result = await execRepl.run(code, helpers);
68
+ if (!result.ok) {
69
+ throw new DaemonExecError(result.error.message, result.output);
106
70
  }
71
+ return { result: result.result, output: result.output };
107
72
  }
108
73
 
109
74
  export async function handleReadonlyExec(
@@ -0,0 +1,27 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { getDaemonSocketPath } from "./ipc.js";
3
+
4
+ describe("daemon IPC endpoint paths", () => {
5
+ test("uses a Windows named pipe path on Windows", () => {
6
+ const socketPath = getDaemonSocketPath("windows-session", "win32");
7
+
8
+ expect(socketPath).toMatch(/^\\\\\.\\pipe\\libretto-[a-f0-9]{12}$/);
9
+ expect(socketPath).not.toContain("/tmp/");
10
+ expect(socketPath).not.toContain(".sock");
11
+ });
12
+
13
+ test("uses a short Unix socket path on Unix-like platforms", () => {
14
+ const socketPath = getDaemonSocketPath("unix-session", "linux");
15
+
16
+ expect(socketPath).toMatch(/^\/tmp\/libretto-.+-[a-f0-9]{12}\.sock$/);
17
+ });
18
+
19
+ test("keeps daemon IPC endpoints deterministic per session", () => {
20
+ const firstPath = getDaemonSocketPath("stable-session", "linux");
21
+ const secondPath = getDaemonSocketPath("stable-session", "linux");
22
+ const otherPath = getDaemonSocketPath("other-session", "linux");
23
+
24
+ expect(secondPath).toBe(firstPath);
25
+ expect(otherPath).not.toBe(firstPath);
26
+ });
27
+ });