libretto 0.5.6 → 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 (35) hide show
  1. package/dist/cli/commands/browser.js +31 -6
  2. package/dist/cli/commands/execution.js +54 -15
  3. package/dist/cli/commands/setup.js +78 -64
  4. package/dist/cli/commands/status.js +1 -1
  5. package/dist/cli/core/browser.js +163 -10
  6. package/dist/cli/core/config.js +1 -0
  7. package/dist/cli/core/providers/browserbase.js +53 -0
  8. package/dist/cli/core/providers/index.js +48 -0
  9. package/dist/cli/core/providers/kernel.js +46 -0
  10. package/dist/cli/core/providers/libretto-cloud.js +58 -0
  11. package/dist/cli/core/providers/types.js +0 -0
  12. package/dist/cli/core/session.js +9 -0
  13. package/dist/cli/workers/run-integration-runtime.js +3 -1
  14. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  15. package/dist/shared/run/browser.d.ts +6 -1
  16. package/dist/shared/run/browser.js +39 -1
  17. package/dist/shared/state/session-state.d.ts +11 -1
  18. package/dist/shared/state/session-state.js +9 -2
  19. package/package.json +1 -1
  20. package/src/cli/commands/browser.ts +35 -7
  21. package/src/cli/commands/execution.ts +54 -14
  22. package/src/cli/commands/setup.ts +81 -64
  23. package/src/cli/commands/status.ts +3 -1
  24. package/src/cli/core/browser.ts +197 -9
  25. package/src/cli/core/config.ts +1 -0
  26. package/src/cli/core/providers/browserbase.ts +57 -0
  27. package/src/cli/core/providers/index.ts +62 -0
  28. package/src/cli/core/providers/kernel.ts +49 -0
  29. package/src/cli/core/providers/libretto-cloud.ts +61 -0
  30. package/src/cli/core/providers/types.ts +9 -0
  31. package/src/cli/core/session.ts +13 -0
  32. package/src/cli/workers/run-integration-runtime.ts +2 -0
  33. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  34. package/src/shared/run/browser.ts +45 -0
  35. package/src/shared/state/session-state.ts +7 -0
@@ -37,6 +37,8 @@ import {
37
37
  readSessionState,
38
38
  writeSessionState,
39
39
  } from "./session.js";
40
+ import type { ProviderApi } from "./providers/types.js";
41
+ import { getCloudProviderApi } from "./providers/index.js";
40
42
  import { installSessionTelemetry } from "./session-telemetry.js";
41
43
 
42
44
  const CLOSE_WAIT_MS = 1_500;
@@ -258,6 +260,16 @@ export async function connect(
258
260
  endpoint,
259
261
  pid: state.pid,
260
262
  });
263
+ // Provider sessions have no local PID to check liveness.
264
+ // Don't destroy the remote session on a transient failure —
265
+ // let the user retry or explicitly close.
266
+ if (state.provider) {
267
+ throw new Error(
268
+ `Could not connect to ${state.provider.name} session for "${session}" at ${endpoint}. ` +
269
+ `The remote session may still be active. Try again, or close with: libretto close --session ${session}`,
270
+ );
271
+ }
272
+
261
273
  if (state.pid == null || !isPidRunning(state.pid)) {
262
274
  clearSessionState(session, logger);
263
275
  throw new Error(
@@ -717,6 +729,106 @@ await new Promise(() => {});
717
729
  );
718
730
  }
719
731
 
732
+ export async function runOpenWithProvider(
733
+ rawUrl: string,
734
+ providerName: string,
735
+ provider: ProviderApi,
736
+ session: string,
737
+ logger: LoggerApi,
738
+ accessMode: SessionAccessMode = "write-access",
739
+ ): Promise<void> {
740
+ const parsedUrl = normalizeUrl(rawUrl);
741
+ const url = parsedUrl.href;
742
+ logger.info("open-provider-start", { url, provider: providerName, session });
743
+
744
+ console.log(
745
+ `Creating ${providerName} browser session (session: ${session})...`,
746
+ );
747
+
748
+ const providerSession = await provider.createSession();
749
+ logger.info("open-provider-session-created", {
750
+ provider: providerName,
751
+ sessionId: providerSession.sessionId,
752
+ cdpEndpoint: providerSession.cdpEndpoint,
753
+ });
754
+
755
+ console.log(`Connecting to ${providerName} browser...`);
756
+
757
+ let browser: Browser | null = null;
758
+ try {
759
+ browser = await tryConnectToCDP(
760
+ providerSession.cdpEndpoint,
761
+ logger,
762
+ 30_000,
763
+ );
764
+ if (!browser) {
765
+ throw new Error(
766
+ `Could not connect to ${providerName} browser at ${providerSession.cdpEndpoint}. The remote session was created but CDP connection failed.`,
767
+ );
768
+ }
769
+
770
+ const contexts = browser.contexts();
771
+ let page: Page;
772
+ if (contexts.length > 0 && contexts[0].pages().length > 0) {
773
+ page = contexts[0].pages()[0];
774
+ } else {
775
+ const context =
776
+ contexts.length > 0 ? contexts[0] : await browser.newContext();
777
+ page = await context.newPage();
778
+ }
779
+
780
+ await page.goto(url);
781
+ logger.info("open-provider-navigated", { url, session });
782
+
783
+ // Cloud sessions have no local port. Reconnection uses cdpEndpoint directly.
784
+ writeSessionState(
785
+ {
786
+ port: 0,
787
+ cdpEndpoint: providerSession.cdpEndpoint,
788
+ session,
789
+ startedAt: new Date().toISOString(),
790
+ status: "active",
791
+ mode: accessMode,
792
+ provider: {
793
+ name: providerName,
794
+ sessionId: providerSession.sessionId,
795
+ },
796
+ },
797
+ logger,
798
+ );
799
+
800
+ disconnectBrowser(browser, logger, session);
801
+ } catch (err) {
802
+ if (browser) {
803
+ disconnectBrowser(browser, logger, session);
804
+ }
805
+ // Clean up the remote session so it doesn't leak
806
+ logger.warn("open-provider-cleanup-after-error", {
807
+ provider: providerName,
808
+ sessionId: providerSession.sessionId,
809
+ error: err,
810
+ });
811
+ try {
812
+ await provider.closeSession(providerSession.sessionId);
813
+ } catch (cleanupErr) {
814
+ logger.warn("open-provider-cleanup-failed", {
815
+ provider: providerName,
816
+ sessionId: providerSession.sessionId,
817
+ error: cleanupErr,
818
+ });
819
+ }
820
+ throw err;
821
+ }
822
+
823
+ logger.info("open-provider-success", {
824
+ url,
825
+ provider: providerName,
826
+ session,
827
+ sessionId: providerSession.sessionId,
828
+ });
829
+ console.log(`Browser open (${providerName}): ${url}`);
830
+ }
831
+
720
832
  export async function runSave(
721
833
  urlOrDomain: string,
722
834
  session: string,
@@ -811,11 +923,37 @@ export async function runClose(
811
923
  return;
812
924
  }
813
925
 
814
- logger.info("close-killing", { session, pid: state.pid, port: state.port });
815
-
816
- if (state.pid != null) {
817
- sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
818
- await waitForCloseSignalWindow(CLOSE_WAIT_MS);
926
+ if (state.provider) {
927
+ // Cloud provider session — close via provider API, no local pid to kill
928
+ logger.info("close-provider", {
929
+ session,
930
+ provider: state.provider.name,
931
+ sessionId: state.provider.sessionId,
932
+ });
933
+ try {
934
+ const provider = getCloudProviderApi(state.provider.name);
935
+ await provider.closeSession(state.provider.sessionId);
936
+ } catch (err) {
937
+ logger.warn("close-provider-error", {
938
+ session,
939
+ provider: state.provider.name,
940
+ sessionId: state.provider.sessionId,
941
+ error: err,
942
+ });
943
+ // Preserve state with cleanup-failed status so the user can retry.
944
+ // The provider.sessionId is retained for manual or future cleanup.
945
+ writeSessionState({ ...state, status: "cleanup-failed" }, logger);
946
+ throw new Error(
947
+ `Failed to close remote ${state.provider.name} session "${state.provider.sessionId}" for session "${session}". ` +
948
+ `State preserved with status "cleanup-failed". Retry with: libretto close --session ${session}`,
949
+ );
950
+ }
951
+ } else {
952
+ logger.info("close-killing", { session, pid: state.pid, port: state.port });
953
+ if (state.pid != null) {
954
+ sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
955
+ await waitForCloseSignalWindow(CLOSE_WAIT_MS);
956
+ }
819
957
  }
820
958
 
821
959
  clearSessionState(session, logger);
@@ -827,6 +965,7 @@ type ClosableSession = {
827
965
  session: string;
828
966
  pid?: number;
829
967
  port: number;
968
+ provider?: { name: string; sessionId: string };
830
969
  };
831
970
 
832
971
  function waitForCloseSignalWindow(ms: number): Promise<void> {
@@ -879,6 +1018,7 @@ function resolveClosableSessions(logger: LoggerApi): {
879
1018
  session,
880
1019
  pid: state.pid,
881
1020
  port: state.port,
1021
+ provider: state.provider,
882
1022
  });
883
1023
  }
884
1024
 
@@ -888,9 +1028,11 @@ function resolveClosableSessions(logger: LoggerApi): {
888
1028
  function clearStoppedSessionStates(
889
1029
  sessions: ReadonlyArray<ClosableSession>,
890
1030
  logger: LoggerApi,
1031
+ skip?: ReadonlySet<string>,
891
1032
  ): number {
892
1033
  let cleared = 0;
893
1034
  for (const session of sessions) {
1035
+ if (skip?.has(session.session)) continue;
894
1036
  if (session.pid == null || !isPidRunning(session.pid)) {
895
1037
  clearSessionState(session.session, logger);
896
1038
  cleared += 1;
@@ -916,7 +1058,38 @@ export async function runCloseAll(
916
1058
  return;
917
1059
  }
918
1060
 
1061
+ // Close provider sessions via their APIs
1062
+ const failedProviderSessions = new Set<string>();
1063
+ for (const target of closable) {
1064
+ if (target.provider) {
1065
+ logger.info("close-all-provider", {
1066
+ session: target.session,
1067
+ provider: target.provider.name,
1068
+ sessionId: target.provider.sessionId,
1069
+ });
1070
+ try {
1071
+ const provider = getCloudProviderApi(target.provider.name);
1072
+ await provider.closeSession(target.provider.sessionId);
1073
+ } catch (err) {
1074
+ logger.warn("close-all-provider-error", {
1075
+ session: target.session,
1076
+ provider: target.provider.name,
1077
+ sessionId: target.provider.sessionId,
1078
+ error: err,
1079
+ });
1080
+ failedProviderSessions.add(target.session);
1081
+ // Mark as cleanup-failed, preserving provider.sessionId for retry
1082
+ const state = readSessionState(target.session, logger);
1083
+ if (state) {
1084
+ writeSessionState({ ...state, status: "cleanup-failed" }, logger);
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+
1090
+ // Send SIGTERM to local sessions
919
1091
  for (const target of closable) {
1092
+ if (target.provider) continue; // already handled above
920
1093
  logger.info("close-all-sigterm", {
921
1094
  session: target.session,
922
1095
  pid: target.pid,
@@ -938,7 +1111,11 @@ export async function runCloseAll(
938
1111
  (target) => target.pid != null && isPidRunning(target.pid),
939
1112
  );
940
1113
  if (survivors.length > 0 && !force) {
941
- const closed = clearStoppedSessionStates(closable, logger);
1114
+ const closed = clearStoppedSessionStates(
1115
+ closable,
1116
+ logger,
1117
+ failedProviderSessions,
1118
+ );
942
1119
 
943
1120
  throw new Error(
944
1121
  [
@@ -971,7 +1148,11 @@ export async function runCloseAll(
971
1148
  (target) => target.pid != null && isPidRunning(target.pid),
972
1149
  );
973
1150
  if (survivors.length > 0) {
974
- const closed = clearStoppedSessionStates(closable, logger);
1151
+ const closed = clearStoppedSessionStates(
1152
+ closable,
1153
+ logger,
1154
+ failedProviderSessions,
1155
+ );
975
1156
  throw new Error(
976
1157
  [
977
1158
  `Failed to force-close ${survivors.length} session(s): ${formatSessionList(survivors)}.`,
@@ -981,14 +1162,21 @@ export async function runCloseAll(
981
1162
  }
982
1163
  }
983
1164
 
984
- clearStoppedSessionStates(closable, logger);
1165
+ clearStoppedSessionStates(closable, logger, failedProviderSessions);
1166
+
1167
+ if (failedProviderSessions.size > 0) {
1168
+ console.log(
1169
+ `Warning: ${failedProviderSessions.size} provider session(s) failed remote cleanup and were preserved with status "cleanup-failed".`,
1170
+ );
1171
+ }
985
1172
 
986
1173
  if (clearedUnreadableStates > 0) {
987
1174
  console.log(
988
1175
  `Cleared ${clearedUnreadableStates} unreadable session state file(s).`,
989
1176
  );
990
1177
  }
991
- console.log(`Closed ${closable.length} session(s).`);
1178
+ const closedCount = closable.length - failedProviderSessions.size;
1179
+ console.log(`Closed ${closedCount} session(s).`);
992
1180
  if (forceKilled > 0) {
993
1181
  console.log(`Force-killed ${forceKilled} session(s).`);
994
1182
  }
@@ -42,6 +42,7 @@ export const LibrettoConfigSchema = z
42
42
  ai: AiConfigSchema.optional(),
43
43
  viewport: ViewportConfigSchema.optional(),
44
44
  windowPosition: WindowPositionConfigSchema.optional(),
45
+ provider: z.string().optional(),
45
46
  sessionMode: SessionAccessModeSchema.optional(),
46
47
  })
47
48
  .passthrough();
@@ -0,0 +1,57 @@
1
+ import type { ProviderApi } from "./types.js";
2
+
3
+ export function createBrowserbaseProvider(): ProviderApi {
4
+ const apiKey = process.env.BROWSERBASE_API_KEY;
5
+ if (!apiKey)
6
+ throw new Error(
7
+ "BROWSERBASE_API_KEY is required for Browserbase provider.",
8
+ );
9
+ const projectId = process.env.BROWSERBASE_PROJECT_ID;
10
+ if (!projectId)
11
+ throw new Error(
12
+ "BROWSERBASE_PROJECT_ID is required for Browserbase provider.",
13
+ );
14
+ const endpoint =
15
+ process.env.BROWSERBASE_ENDPOINT ?? "https://api.browserbase.com";
16
+
17
+ return {
18
+ async createSession() {
19
+ const resp = await fetch(`${endpoint}/v1/sessions`, {
20
+ method: "POST",
21
+ headers: {
22
+ "X-BB-API-Key": apiKey,
23
+ "Content-Type": "application/json",
24
+ },
25
+ body: JSON.stringify({ projectId }),
26
+ });
27
+ if (!resp.ok) {
28
+ const body = await resp.text();
29
+ throw new Error(`Browserbase API error (${resp.status}): ${body}`);
30
+ }
31
+ const json = (await resp.json()) as {
32
+ id: string;
33
+ connectUrl: string;
34
+ };
35
+ return {
36
+ sessionId: json.id,
37
+ cdpEndpoint: json.connectUrl,
38
+ };
39
+ },
40
+ async closeSession(sessionId) {
41
+ const resp = await fetch(`${endpoint}/v1/sessions/${sessionId}`, {
42
+ method: "POST",
43
+ headers: {
44
+ "X-BB-API-Key": apiKey,
45
+ "Content-Type": "application/json",
46
+ },
47
+ body: JSON.stringify({ status: "REQUEST_RELEASE" }),
48
+ });
49
+ if (!resp.ok) {
50
+ const body = await resp.text();
51
+ throw new Error(
52
+ `Browserbase API error closing session ${sessionId} (${resp.status}): ${body}`,
53
+ );
54
+ }
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,62 @@
1
+ import { readLibrettoConfig } from "../config.js";
2
+ import { createBrowserbaseProvider } from "./browserbase.js";
3
+ import { createKernelProvider } from "./kernel.js";
4
+ import { createLibrettoCloudProvider } from "./libretto-cloud.js";
5
+ import type { ProviderApi } from "./types.js";
6
+
7
+ const VALID_PROVIDERS = new Set(["local", "kernel", "browserbase", "libretto-cloud"] as const);
8
+ export type ProviderName =
9
+ typeof VALID_PROVIDERS extends Set<infer T> ? T : never;
10
+
11
+ function assertValidProviderName(value: string, source: string): ProviderName {
12
+ if (!VALID_PROVIDERS.has(value as ProviderName)) {
13
+ throw new Error(
14
+ `Invalid provider "${value}" from ${source}. Valid providers: ${[...VALID_PROVIDERS].join(", ")}`,
15
+ );
16
+ }
17
+ return value as ProviderName;
18
+ }
19
+
20
+ /**
21
+ * Resolve which provider to use.
22
+ * Precedence: CLI flag > LIBRETTO_PROVIDER env var > config file > "local" default.
23
+ */
24
+ export function resolveProviderName(cliFlag?: string): ProviderName {
25
+ if (cliFlag) {
26
+ return assertValidProviderName(cliFlag, "--provider flag");
27
+ }
28
+
29
+ const envVar = process.env.LIBRETTO_PROVIDER;
30
+ if (envVar) {
31
+ return assertValidProviderName(envVar, "LIBRETTO_PROVIDER env var");
32
+ }
33
+
34
+ const config = readLibrettoConfig();
35
+ if (config.provider) {
36
+ return assertValidProviderName(config.provider, "config file");
37
+ }
38
+
39
+ return "local";
40
+ }
41
+
42
+ /**
43
+ * Get a ProviderApi instance for a cloud provider.
44
+ * Only call this for non-"local" providers.
45
+ */
46
+ export function getCloudProviderApi(name: string): ProviderApi {
47
+ switch (name) {
48
+ case "kernel":
49
+ return createKernelProvider();
50
+ case "browserbase":
51
+ return createBrowserbaseProvider();
52
+ case "libretto-cloud":
53
+ console.warn(
54
+ "Note: The libretto-cloud provider is in alpha.",
55
+ );
56
+ return createLibrettoCloudProvider();
57
+ default:
58
+ throw new Error(
59
+ `Unknown provider "${name}". Valid cloud providers: kernel, browserbase`,
60
+ );
61
+ }
62
+ }
@@ -0,0 +1,49 @@
1
+ import type { ProviderApi } from "./types.js";
2
+
3
+ export function createKernelProvider(): ProviderApi {
4
+ const apiKey = process.env.KERNEL_API_KEY;
5
+ if (!apiKey)
6
+ throw new Error("KERNEL_API_KEY is required for Kernel provider.");
7
+ const endpoint = process.env.KERNEL_ENDPOINT ?? "https://api.onkernel.com";
8
+
9
+ return {
10
+ async createSession() {
11
+ const resp = await fetch(`${endpoint}/browsers`, {
12
+ method: "POST",
13
+ headers: {
14
+ Authorization: `Bearer ${apiKey}`,
15
+ "Content-Type": "application/json",
16
+ },
17
+ body: JSON.stringify({
18
+ headless: process.env.KERNEL_HEADLESS !== "false",
19
+ stealth: process.env.KERNEL_STEALTH === "true",
20
+ timeout_seconds: Number(process.env.KERNEL_TIMEOUT_SECONDS ?? 300),
21
+ }),
22
+ });
23
+ if (!resp.ok) {
24
+ const body = await resp.text();
25
+ throw new Error(`Kernel API error (${resp.status}): ${body}`);
26
+ }
27
+ const json = (await resp.json()) as {
28
+ session_id: string;
29
+ cdp_ws_url: string;
30
+ };
31
+ return {
32
+ sessionId: json.session_id,
33
+ cdpEndpoint: json.cdp_ws_url,
34
+ };
35
+ },
36
+ async closeSession(sessionId) {
37
+ const resp = await fetch(`${endpoint}/browsers/${sessionId}`, {
38
+ method: "DELETE",
39
+ headers: { Authorization: `Bearer ${apiKey}` },
40
+ });
41
+ if (!resp.ok) {
42
+ const body = await resp.text();
43
+ throw new Error(
44
+ `Kernel API error closing session ${sessionId} (${resp.status}): ${body}`,
45
+ );
46
+ }
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,61 @@
1
+ import type { ProviderApi } from "./types.js";
2
+
3
+ export function createLibrettoCloudProvider(): ProviderApi {
4
+ const apiKey = process.env.LIBRETTO_API_KEY;
5
+ if (!apiKey)
6
+ throw new Error(
7
+ "LIBRETTO_API_KEY is required for the Libretto Cloud provider.",
8
+ );
9
+ const apiUrl = process.env.LIBRETTO_API_URL;
10
+ if (!apiUrl)
11
+ throw new Error(
12
+ "LIBRETTO_API_URL is required for the Libretto Cloud provider.",
13
+ );
14
+ const endpoint = apiUrl.replace(/\/$/, "");
15
+
16
+ return {
17
+ async createSession() {
18
+ const timeoutSeconds = Number(
19
+ process.env.LIBRETTO_TIMEOUT_SECONDS ?? 7200,
20
+ );
21
+ const resp = await fetch(`${endpoint}/v1/sessions/create`, {
22
+ method: "POST",
23
+ headers: {
24
+ "x-api-key": apiKey,
25
+ "Content-Type": "application/json",
26
+ },
27
+ body: JSON.stringify({ timeout_seconds: timeoutSeconds }),
28
+ });
29
+ if (!resp.ok) {
30
+ const body = await resp.text();
31
+ throw new Error(
32
+ `Libretto Cloud API error (${resp.status}): ${body}`,
33
+ );
34
+ }
35
+ const json = (await resp.json()) as {
36
+ session_id: string;
37
+ cdp_url: string;
38
+ };
39
+ return {
40
+ sessionId: json.session_id,
41
+ cdpEndpoint: json.cdp_url,
42
+ };
43
+ },
44
+ async closeSession(sessionId) {
45
+ const resp = await fetch(`${endpoint}/v1/sessions/close`, {
46
+ method: "POST",
47
+ headers: {
48
+ "x-api-key": apiKey,
49
+ "Content-Type": "application/json",
50
+ },
51
+ body: JSON.stringify({ session_id: sessionId }),
52
+ });
53
+ if (!resp.ok) {
54
+ const body = await resp.text();
55
+ throw new Error(
56
+ `Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`,
57
+ );
58
+ }
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,9 @@
1
+ export type ProviderSession = {
2
+ sessionId: string; // remote session id for cleanup
3
+ cdpEndpoint: string; // CDP WebSocket URL
4
+ };
5
+
6
+ export type ProviderApi = {
7
+ createSession(): Promise<ProviderSession>;
8
+ closeSession(sessionId: string): Promise<void>;
9
+ };
@@ -129,6 +129,10 @@ export function listRunningSessions(): SessionState[] {
129
129
  for (const name of sessions) {
130
130
  const state = readSessionState(name);
131
131
  if (!state) continue;
132
+ if (state.provider) {
133
+ running.push(state);
134
+ continue;
135
+ }
132
136
  if (state.pid == null || !isPidRunning(state.pid)) continue;
133
137
  running.push(state);
134
138
  }
@@ -272,6 +276,15 @@ export function assertSessionAvailableForStart(
272
276
  ): void {
273
277
  const existingState = readSessionState(session, logger);
274
278
  if (!existingState) return;
279
+
280
+ // Cloud provider sessions have no local PID — treat them as active
281
+ // if they have a provider field with a cdpEndpoint.
282
+ if (existingState.provider && existingState.cdpEndpoint) {
283
+ throw new Error(
284
+ `Session "${session}" is already open via ${existingState.provider.name} provider. Close it first with: libretto close --session ${session}`,
285
+ );
286
+ }
287
+
275
288
  if (existingState.pid == null || !isPidRunning(existingState.pid)) {
276
289
  setSessionStatus(session, "exited", logger);
277
290
  return;
@@ -239,6 +239,8 @@ async function runIntegrationInternal(
239
239
  storageStatePath,
240
240
  viewport: args.viewport,
241
241
  accessMode: args.accessMode,
242
+ cdpEndpoint: args.cdpEndpoint,
243
+ provider: args.provider,
242
244
  });
243
245
  if (!args.headless && args.visualize !== false) {
244
246
  await installHeadedWorkflowVisualization({
@@ -10,6 +10,8 @@ export const RunIntegrationWorkerRequestSchema = z.object({
10
10
  authProfileDomain: z.string().optional(),
11
11
  viewport: z.object({ width: z.number(), height: z.number() }).optional(),
12
12
  accessMode: SessionAccessModeSchema.default("write-access"),
13
+ cdpEndpoint: z.string().optional(),
14
+ provider: z.object({ name: z.string(), sessionId: z.string() }).optional(),
13
15
  });
14
16
 
15
17
  export type RunIntegrationWorkerRequest = z.infer<
@@ -36,6 +36,8 @@ export type LaunchBrowserArgs = {
36
36
  viewport?: { width: number; height: number };
37
37
  storageStatePath?: string;
38
38
  accessMode?: SessionAccessMode;
39
+ cdpEndpoint?: string;
40
+ provider?: { name: string; sessionId: string };
39
41
  };
40
42
 
41
43
  export type BrowserSession = {
@@ -100,7 +102,50 @@ export async function launchBrowser({
100
102
  viewport = { width: 1366, height: 768 },
101
103
  storageStatePath,
102
104
  accessMode = "write-access",
105
+ cdpEndpoint,
106
+ provider,
103
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
+
104
149
  const debugPort = await pickFreePort();
105
150
  const windowPosition = headless ? undefined : resolveWindowPosition();
106
151
  const browser = await chromium.launch({
@@ -8,6 +8,7 @@ export const SessionStatusSchema = z.enum([
8
8
  "completed",
9
9
  "failed",
10
10
  "exited",
11
+ "cleanup-failed",
11
12
  ]);
12
13
  export const SessionAccessModeSchema = z.enum(["read-only", "write-access"]);
13
14
  export const SessionViewportSchema = z.object({
@@ -15,6 +16,11 @@ export const SessionViewportSchema = z.object({
15
16
  height: z.number().int().min(1),
16
17
  });
17
18
 
19
+ export const ProviderStateSchema = z.object({
20
+ name: z.string(),
21
+ sessionId: z.string(),
22
+ });
23
+
18
24
  export const SessionStateFileSchema = z.object({
19
25
  version: z.literal(SESSION_STATE_VERSION),
20
26
  port: z.number().int().min(0).max(65535),
@@ -25,6 +31,7 @@ export const SessionStateFileSchema = z.object({
25
31
  status: SessionStatusSchema.optional(),
26
32
  mode: SessionAccessModeSchema.default("write-access"),
27
33
  viewport: SessionViewportSchema.optional(),
34
+ provider: ProviderStateSchema.optional(),
28
35
  });
29
36
 
30
37
  export type SessionStatus = z.infer<typeof SessionStatusSchema>;