libretto 0.5.6 → 0.6.1
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.
- package/dist/cli/commands/browser.js +31 -6
- package/dist/cli/commands/execution.js +54 -15
- package/dist/cli/commands/setup.js +78 -64
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/core/browser.js +163 -10
- package/dist/cli/core/config.js +1 -0
- package/dist/cli/core/providers/browserbase.js +53 -0
- package/dist/cli/core/providers/index.js +48 -0
- package/dist/cli/core/providers/kernel.js +46 -0
- package/dist/cli/core/providers/libretto-cloud.js +58 -0
- package/dist/cli/core/providers/types.js +0 -0
- package/dist/cli/core/session.js +9 -0
- package/dist/cli/workers/run-integration-runtime.js +3 -1
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/shared/run/browser.d.ts +6 -1
- package/dist/shared/run/browser.js +39 -1
- package/dist/shared/state/session-state.d.ts +11 -1
- package/dist/shared/state/session-state.js +9 -2
- package/package.json +1 -1
- package/skills/AGENTS.md +1 -0
- package/skills/libretto/SKILL.md +1 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/browser.ts +35 -7
- package/src/cli/commands/execution.ts +54 -14
- package/src/cli/commands/setup.ts +81 -64
- package/src/cli/commands/status.ts +3 -1
- package/src/cli/core/browser.ts +197 -9
- package/src/cli/core/config.ts +1 -0
- package/src/cli/core/providers/browserbase.ts +57 -0
- package/src/cli/core/providers/index.ts +62 -0
- package/src/cli/core/providers/kernel.ts +49 -0
- package/src/cli/core/providers/libretto-cloud.ts +61 -0
- package/src/cli/core/providers/types.ts +9 -0
- package/src/cli/core/session.ts +13 -0
- package/src/cli/workers/run-integration-runtime.ts +2 -0
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/shared/run/browser.ts +45 -0
- package/src/shared/state/session-state.ts +7 -0
package/dist/cli/core/browser.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
readSessionState,
|
|
31
31
|
writeSessionState
|
|
32
32
|
} from "./session.js";
|
|
33
|
+
import { getCloudProviderApi } from "./providers/index.js";
|
|
33
34
|
import { installSessionTelemetry } from "./session-telemetry.js";
|
|
34
35
|
const CLOSE_WAIT_MS = 1500;
|
|
35
36
|
const FORCE_CLOSE_WAIT_MS = 300;
|
|
@@ -179,6 +180,11 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
|
|
|
179
180
|
endpoint,
|
|
180
181
|
pid: state.pid
|
|
181
182
|
});
|
|
183
|
+
if (state.provider) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Could not connect to ${state.provider.name} session for "${session}" at ${endpoint}. The remote session may still be active. Try again, or close with: libretto close --session ${session}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
182
188
|
if (state.pid == null || !isPidRunning(state.pid)) {
|
|
183
189
|
clearSessionState(session, logger);
|
|
184
190
|
throw new Error(
|
|
@@ -559,6 +565,86 @@ await new Promise(() => {});
|
|
|
559
565
|
`Failed to connect to browser after ${Math.ceil(cdpStartupTimeoutMs / 1e3)}s. Check startup logs: ${runLogPath}`
|
|
560
566
|
);
|
|
561
567
|
}
|
|
568
|
+
async function runOpenWithProvider(rawUrl, providerName, provider, session, logger, accessMode = "write-access") {
|
|
569
|
+
const parsedUrl = normalizeUrl(rawUrl);
|
|
570
|
+
const url = parsedUrl.href;
|
|
571
|
+
logger.info("open-provider-start", { url, provider: providerName, session });
|
|
572
|
+
console.log(
|
|
573
|
+
`Creating ${providerName} browser session (session: ${session})...`
|
|
574
|
+
);
|
|
575
|
+
const providerSession = await provider.createSession();
|
|
576
|
+
logger.info("open-provider-session-created", {
|
|
577
|
+
provider: providerName,
|
|
578
|
+
sessionId: providerSession.sessionId,
|
|
579
|
+
cdpEndpoint: providerSession.cdpEndpoint
|
|
580
|
+
});
|
|
581
|
+
console.log(`Connecting to ${providerName} browser...`);
|
|
582
|
+
let browser = null;
|
|
583
|
+
try {
|
|
584
|
+
browser = await tryConnectToCDP(
|
|
585
|
+
providerSession.cdpEndpoint,
|
|
586
|
+
logger,
|
|
587
|
+
3e4
|
|
588
|
+
);
|
|
589
|
+
if (!browser) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
`Could not connect to ${providerName} browser at ${providerSession.cdpEndpoint}. The remote session was created but CDP connection failed.`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const contexts = browser.contexts();
|
|
595
|
+
let page;
|
|
596
|
+
if (contexts.length > 0 && contexts[0].pages().length > 0) {
|
|
597
|
+
page = contexts[0].pages()[0];
|
|
598
|
+
} else {
|
|
599
|
+
const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
|
|
600
|
+
page = await context.newPage();
|
|
601
|
+
}
|
|
602
|
+
await page.goto(url);
|
|
603
|
+
logger.info("open-provider-navigated", { url, session });
|
|
604
|
+
writeSessionState(
|
|
605
|
+
{
|
|
606
|
+
port: 0,
|
|
607
|
+
cdpEndpoint: providerSession.cdpEndpoint,
|
|
608
|
+
session,
|
|
609
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
610
|
+
status: "active",
|
|
611
|
+
mode: accessMode,
|
|
612
|
+
provider: {
|
|
613
|
+
name: providerName,
|
|
614
|
+
sessionId: providerSession.sessionId
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
logger
|
|
618
|
+
);
|
|
619
|
+
disconnectBrowser(browser, logger, session);
|
|
620
|
+
} catch (err) {
|
|
621
|
+
if (browser) {
|
|
622
|
+
disconnectBrowser(browser, logger, session);
|
|
623
|
+
}
|
|
624
|
+
logger.warn("open-provider-cleanup-after-error", {
|
|
625
|
+
provider: providerName,
|
|
626
|
+
sessionId: providerSession.sessionId,
|
|
627
|
+
error: err
|
|
628
|
+
});
|
|
629
|
+
try {
|
|
630
|
+
await provider.closeSession(providerSession.sessionId);
|
|
631
|
+
} catch (cleanupErr) {
|
|
632
|
+
logger.warn("open-provider-cleanup-failed", {
|
|
633
|
+
provider: providerName,
|
|
634
|
+
sessionId: providerSession.sessionId,
|
|
635
|
+
error: cleanupErr
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
throw err;
|
|
639
|
+
}
|
|
640
|
+
logger.info("open-provider-success", {
|
|
641
|
+
url,
|
|
642
|
+
provider: providerName,
|
|
643
|
+
session,
|
|
644
|
+
sessionId: providerSession.sessionId
|
|
645
|
+
});
|
|
646
|
+
console.log(`Browser open (${providerName}): ${url}`);
|
|
647
|
+
}
|
|
562
648
|
async function runSave(urlOrDomain, session, logger) {
|
|
563
649
|
logger.info("save-start", { urlOrDomain, session });
|
|
564
650
|
const { browser, context, page } = await connect(session, logger);
|
|
@@ -631,10 +717,33 @@ async function runClose(session, logger) {
|
|
|
631
717
|
console.log(`No browser running for session "${session}".`);
|
|
632
718
|
return;
|
|
633
719
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
720
|
+
if (state.provider) {
|
|
721
|
+
logger.info("close-provider", {
|
|
722
|
+
session,
|
|
723
|
+
provider: state.provider.name,
|
|
724
|
+
sessionId: state.provider.sessionId
|
|
725
|
+
});
|
|
726
|
+
try {
|
|
727
|
+
const provider = getCloudProviderApi(state.provider.name);
|
|
728
|
+
await provider.closeSession(state.provider.sessionId);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
logger.warn("close-provider-error", {
|
|
731
|
+
session,
|
|
732
|
+
provider: state.provider.name,
|
|
733
|
+
sessionId: state.provider.sessionId,
|
|
734
|
+
error: err
|
|
735
|
+
});
|
|
736
|
+
writeSessionState({ ...state, status: "cleanup-failed" }, logger);
|
|
737
|
+
throw new Error(
|
|
738
|
+
`Failed to close remote ${state.provider.name} session "${state.provider.sessionId}" for session "${session}". State preserved with status "cleanup-failed". Retry with: libretto close --session ${session}`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
} else {
|
|
742
|
+
logger.info("close-killing", { session, pid: state.pid, port: state.port });
|
|
743
|
+
if (state.pid != null) {
|
|
744
|
+
sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
|
|
745
|
+
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
746
|
+
}
|
|
638
747
|
}
|
|
639
748
|
clearSessionState(session, logger);
|
|
640
749
|
logger.info("close-success", { session });
|
|
@@ -676,14 +785,16 @@ function resolveClosableSessions(logger) {
|
|
|
676
785
|
closable.push({
|
|
677
786
|
session,
|
|
678
787
|
pid: state.pid,
|
|
679
|
-
port: state.port
|
|
788
|
+
port: state.port,
|
|
789
|
+
provider: state.provider
|
|
680
790
|
});
|
|
681
791
|
}
|
|
682
792
|
return { closable, clearedUnreadableStates };
|
|
683
793
|
}
|
|
684
|
-
function clearStoppedSessionStates(sessions, logger) {
|
|
794
|
+
function clearStoppedSessionStates(sessions, logger, skip) {
|
|
685
795
|
let cleared = 0;
|
|
686
796
|
for (const session of sessions) {
|
|
797
|
+
if (skip?.has(session.session)) continue;
|
|
687
798
|
if (session.pid == null || !isPidRunning(session.pid)) {
|
|
688
799
|
clearSessionState(session.session, logger);
|
|
689
800
|
cleared += 1;
|
|
@@ -704,7 +815,34 @@ async function runCloseAll(logger, options) {
|
|
|
704
815
|
console.log("No browser sessions found.");
|
|
705
816
|
return;
|
|
706
817
|
}
|
|
818
|
+
const failedProviderSessions = /* @__PURE__ */ new Set();
|
|
819
|
+
for (const target of closable) {
|
|
820
|
+
if (target.provider) {
|
|
821
|
+
logger.info("close-all-provider", {
|
|
822
|
+
session: target.session,
|
|
823
|
+
provider: target.provider.name,
|
|
824
|
+
sessionId: target.provider.sessionId
|
|
825
|
+
});
|
|
826
|
+
try {
|
|
827
|
+
const provider = getCloudProviderApi(target.provider.name);
|
|
828
|
+
await provider.closeSession(target.provider.sessionId);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
logger.warn("close-all-provider-error", {
|
|
831
|
+
session: target.session,
|
|
832
|
+
provider: target.provider.name,
|
|
833
|
+
sessionId: target.provider.sessionId,
|
|
834
|
+
error: err
|
|
835
|
+
});
|
|
836
|
+
failedProviderSessions.add(target.session);
|
|
837
|
+
const state = readSessionState(target.session, logger);
|
|
838
|
+
if (state) {
|
|
839
|
+
writeSessionState({ ...state, status: "cleanup-failed" }, logger);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
707
844
|
for (const target of closable) {
|
|
845
|
+
if (target.provider) continue;
|
|
708
846
|
logger.info("close-all-sigterm", {
|
|
709
847
|
session: target.session,
|
|
710
848
|
pid: target.pid,
|
|
@@ -724,7 +862,11 @@ async function runCloseAll(logger, options) {
|
|
|
724
862
|
(target) => target.pid != null && isPidRunning(target.pid)
|
|
725
863
|
);
|
|
726
864
|
if (survivors.length > 0 && !force) {
|
|
727
|
-
const closed = clearStoppedSessionStates(
|
|
865
|
+
const closed = clearStoppedSessionStates(
|
|
866
|
+
closable,
|
|
867
|
+
logger,
|
|
868
|
+
failedProviderSessions
|
|
869
|
+
);
|
|
728
870
|
throw new Error(
|
|
729
871
|
[
|
|
730
872
|
`Failed to close ${survivors.length} session(s) gracefully: ${formatSessionList(survivors)}.`,
|
|
@@ -755,7 +897,11 @@ async function runCloseAll(logger, options) {
|
|
|
755
897
|
(target) => target.pid != null && isPidRunning(target.pid)
|
|
756
898
|
);
|
|
757
899
|
if (survivors.length > 0) {
|
|
758
|
-
const closed = clearStoppedSessionStates(
|
|
900
|
+
const closed = clearStoppedSessionStates(
|
|
901
|
+
closable,
|
|
902
|
+
logger,
|
|
903
|
+
failedProviderSessions
|
|
904
|
+
);
|
|
759
905
|
throw new Error(
|
|
760
906
|
[
|
|
761
907
|
`Failed to force-close ${survivors.length} session(s): ${formatSessionList(survivors)}.`,
|
|
@@ -764,13 +910,19 @@ async function runCloseAll(logger, options) {
|
|
|
764
910
|
);
|
|
765
911
|
}
|
|
766
912
|
}
|
|
767
|
-
clearStoppedSessionStates(closable, logger);
|
|
913
|
+
clearStoppedSessionStates(closable, logger, failedProviderSessions);
|
|
914
|
+
if (failedProviderSessions.size > 0) {
|
|
915
|
+
console.log(
|
|
916
|
+
`Warning: ${failedProviderSessions.size} provider session(s) failed remote cleanup and were preserved with status "cleanup-failed".`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
768
919
|
if (clearedUnreadableStates > 0) {
|
|
769
920
|
console.log(
|
|
770
921
|
`Cleared ${clearedUnreadableStates} unreadable session state file(s).`
|
|
771
922
|
);
|
|
772
923
|
}
|
|
773
|
-
|
|
924
|
+
const closedCount = closable.length - failedProviderSessions.size;
|
|
925
|
+
console.log(`Closed ${closedCount} session(s).`);
|
|
774
926
|
if (forceKilled > 0) {
|
|
775
927
|
console.log(`Force-killed ${forceKilled} session(s).`);
|
|
776
928
|
}
|
|
@@ -878,6 +1030,7 @@ export {
|
|
|
878
1030
|
runCloseAll,
|
|
879
1031
|
runConnect,
|
|
880
1032
|
runOpen,
|
|
1033
|
+
runOpenWithProvider,
|
|
881
1034
|
runPages,
|
|
882
1035
|
runSave
|
|
883
1036
|
};
|
package/dist/cli/core/config.js
CHANGED
|
@@ -21,6 +21,7 @@ const LibrettoConfigSchema = z.object({
|
|
|
21
21
|
ai: AiConfigSchema.optional(),
|
|
22
22
|
viewport: ViewportConfigSchema.optional(),
|
|
23
23
|
windowPosition: WindowPositionConfigSchema.optional(),
|
|
24
|
+
provider: z.string().optional(),
|
|
24
25
|
sessionMode: SessionAccessModeSchema.optional()
|
|
25
26
|
}).passthrough();
|
|
26
27
|
function formatConfigIssues(error) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
function createBrowserbaseProvider() {
|
|
2
|
+
const apiKey = process.env.BROWSERBASE_API_KEY;
|
|
3
|
+
if (!apiKey)
|
|
4
|
+
throw new Error(
|
|
5
|
+
"BROWSERBASE_API_KEY is required for Browserbase provider."
|
|
6
|
+
);
|
|
7
|
+
const projectId = process.env.BROWSERBASE_PROJECT_ID;
|
|
8
|
+
if (!projectId)
|
|
9
|
+
throw new Error(
|
|
10
|
+
"BROWSERBASE_PROJECT_ID is required for Browserbase provider."
|
|
11
|
+
);
|
|
12
|
+
const endpoint = process.env.BROWSERBASE_ENDPOINT ?? "https://api.browserbase.com";
|
|
13
|
+
return {
|
|
14
|
+
async createSession() {
|
|
15
|
+
const resp = await fetch(`${endpoint}/v1/sessions`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"X-BB-API-Key": apiKey,
|
|
19
|
+
"Content-Type": "application/json"
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({ projectId })
|
|
22
|
+
});
|
|
23
|
+
if (!resp.ok) {
|
|
24
|
+
const body = await resp.text();
|
|
25
|
+
throw new Error(`Browserbase API error (${resp.status}): ${body}`);
|
|
26
|
+
}
|
|
27
|
+
const json = await resp.json();
|
|
28
|
+
return {
|
|
29
|
+
sessionId: json.id,
|
|
30
|
+
cdpEndpoint: json.connectUrl
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
async closeSession(sessionId) {
|
|
34
|
+
const resp = await fetch(`${endpoint}/v1/sessions/${sessionId}`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"X-BB-API-Key": apiKey,
|
|
38
|
+
"Content-Type": "application/json"
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ status: "REQUEST_RELEASE" })
|
|
41
|
+
});
|
|
42
|
+
if (!resp.ok) {
|
|
43
|
+
const body = await resp.text();
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Browserbase API error closing session ${sessionId} (${resp.status}): ${body}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
createBrowserbaseProvider
|
|
53
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
const VALID_PROVIDERS = /* @__PURE__ */ new Set(["local", "kernel", "browserbase", "libretto-cloud"]);
|
|
6
|
+
function assertValidProviderName(value, source) {
|
|
7
|
+
if (!VALID_PROVIDERS.has(value)) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
`Invalid provider "${value}" from ${source}. Valid providers: ${[...VALID_PROVIDERS].join(", ")}`
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function resolveProviderName(cliFlag) {
|
|
15
|
+
if (cliFlag) {
|
|
16
|
+
return assertValidProviderName(cliFlag, "--provider flag");
|
|
17
|
+
}
|
|
18
|
+
const envVar = process.env.LIBRETTO_PROVIDER;
|
|
19
|
+
if (envVar) {
|
|
20
|
+
return assertValidProviderName(envVar, "LIBRETTO_PROVIDER env var");
|
|
21
|
+
}
|
|
22
|
+
const config = readLibrettoConfig();
|
|
23
|
+
if (config.provider) {
|
|
24
|
+
return assertValidProviderName(config.provider, "config file");
|
|
25
|
+
}
|
|
26
|
+
return "local";
|
|
27
|
+
}
|
|
28
|
+
function getCloudProviderApi(name) {
|
|
29
|
+
switch (name) {
|
|
30
|
+
case "kernel":
|
|
31
|
+
return createKernelProvider();
|
|
32
|
+
case "browserbase":
|
|
33
|
+
return createBrowserbaseProvider();
|
|
34
|
+
case "libretto-cloud":
|
|
35
|
+
console.warn(
|
|
36
|
+
"Note: The libretto-cloud provider is in alpha."
|
|
37
|
+
);
|
|
38
|
+
return createLibrettoCloudProvider();
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Unknown provider "${name}". Valid cloud providers: kernel, browserbase`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
getCloudProviderApi,
|
|
47
|
+
resolveProviderName
|
|
48
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function createKernelProvider() {
|
|
2
|
+
const apiKey = process.env.KERNEL_API_KEY;
|
|
3
|
+
if (!apiKey)
|
|
4
|
+
throw new Error("KERNEL_API_KEY is required for Kernel provider.");
|
|
5
|
+
const endpoint = process.env.KERNEL_ENDPOINT ?? "https://api.onkernel.com";
|
|
6
|
+
return {
|
|
7
|
+
async createSession() {
|
|
8
|
+
const resp = await fetch(`${endpoint}/browsers`, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
Authorization: `Bearer ${apiKey}`,
|
|
12
|
+
"Content-Type": "application/json"
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
headless: process.env.KERNEL_HEADLESS !== "false",
|
|
16
|
+
stealth: process.env.KERNEL_STEALTH === "true",
|
|
17
|
+
timeout_seconds: Number(process.env.KERNEL_TIMEOUT_SECONDS ?? 300)
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
if (!resp.ok) {
|
|
21
|
+
const body = await resp.text();
|
|
22
|
+
throw new Error(`Kernel API error (${resp.status}): ${body}`);
|
|
23
|
+
}
|
|
24
|
+
const json = await resp.json();
|
|
25
|
+
return {
|
|
26
|
+
sessionId: json.session_id,
|
|
27
|
+
cdpEndpoint: json.cdp_ws_url
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
async closeSession(sessionId) {
|
|
31
|
+
const resp = await fetch(`${endpoint}/browsers/${sessionId}`, {
|
|
32
|
+
method: "DELETE",
|
|
33
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
34
|
+
});
|
|
35
|
+
if (!resp.ok) {
|
|
36
|
+
const body = await resp.text();
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Kernel API error closing session ${sessionId} (${resp.status}): ${body}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
createKernelProvider
|
|
46
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function createLibrettoCloudProvider() {
|
|
2
|
+
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
3
|
+
if (!apiKey)
|
|
4
|
+
throw new Error(
|
|
5
|
+
"LIBRETTO_API_KEY is required for the Libretto Cloud provider."
|
|
6
|
+
);
|
|
7
|
+
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
8
|
+
if (!apiUrl)
|
|
9
|
+
throw new Error(
|
|
10
|
+
"LIBRETTO_API_URL is required for the Libretto Cloud provider."
|
|
11
|
+
);
|
|
12
|
+
const endpoint = apiUrl.replace(/\/$/, "");
|
|
13
|
+
return {
|
|
14
|
+
async createSession() {
|
|
15
|
+
const timeoutSeconds = Number(
|
|
16
|
+
process.env.LIBRETTO_TIMEOUT_SECONDS ?? 7200
|
|
17
|
+
);
|
|
18
|
+
const resp = await fetch(`${endpoint}/v1/sessions/create`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"x-api-key": apiKey,
|
|
22
|
+
"Content-Type": "application/json"
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({ timeout_seconds: timeoutSeconds })
|
|
25
|
+
});
|
|
26
|
+
if (!resp.ok) {
|
|
27
|
+
const body = await resp.text();
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Libretto Cloud API error (${resp.status}): ${body}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const json = await resp.json();
|
|
33
|
+
return {
|
|
34
|
+
sessionId: json.session_id,
|
|
35
|
+
cdpEndpoint: json.cdp_url
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
async closeSession(sessionId) {
|
|
39
|
+
const resp = await fetch(`${endpoint}/v1/sessions/close`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"x-api-key": apiKey,
|
|
43
|
+
"Content-Type": "application/json"
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({ session_id: sessionId })
|
|
46
|
+
});
|
|
47
|
+
if (!resp.ok) {
|
|
48
|
+
const body = await resp.text();
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
createLibrettoCloudProvider
|
|
58
|
+
};
|
|
File without changes
|
package/dist/cli/core/session.js
CHANGED
|
@@ -95,6 +95,10 @@ function listRunningSessions() {
|
|
|
95
95
|
for (const name of sessions) {
|
|
96
96
|
const state = readSessionState(name);
|
|
97
97
|
if (!state) continue;
|
|
98
|
+
if (state.provider) {
|
|
99
|
+
running.push(state);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
98
102
|
if (state.pid == null || !isPidRunning(state.pid)) continue;
|
|
99
103
|
running.push(state);
|
|
100
104
|
}
|
|
@@ -207,6 +211,11 @@ function setSessionStatus(session, status, logger) {
|
|
|
207
211
|
function assertSessionAvailableForStart(session, logger) {
|
|
208
212
|
const existingState = readSessionState(session, logger);
|
|
209
213
|
if (!existingState) return;
|
|
214
|
+
if (existingState.provider && existingState.cdpEndpoint) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Session "${session}" is already open via ${existingState.provider.name} provider. Close it first with: libretto close --session ${session}`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
210
219
|
if (existingState.pid == null || !isPidRunning(existingState.pid)) {
|
|
211
220
|
setSessionStatus(session, "exited", logger);
|
|
212
221
|
return;
|
|
@@ -158,7 +158,9 @@ async function runIntegrationInternal(args, options) {
|
|
|
158
158
|
headless: args.headless,
|
|
159
159
|
storageStatePath,
|
|
160
160
|
viewport: args.viewport,
|
|
161
|
-
accessMode: args.accessMode
|
|
161
|
+
accessMode: args.accessMode,
|
|
162
|
+
cdpEndpoint: args.cdpEndpoint,
|
|
163
|
+
provider: args.provider
|
|
162
164
|
});
|
|
163
165
|
if (!args.headless && args.visualize !== false) {
|
|
164
166
|
await installHeadedWorkflowVisualization({
|
|
@@ -8,7 +8,9 @@ const RunIntegrationWorkerRequestSchema = z.object({
|
|
|
8
8
|
visualize: z.boolean().default(true),
|
|
9
9
|
authProfileDomain: z.string().optional(),
|
|
10
10
|
viewport: z.object({ width: z.number(), height: z.number() }).optional(),
|
|
11
|
-
accessMode: SessionAccessModeSchema.default("write-access")
|
|
11
|
+
accessMode: SessionAccessModeSchema.default("write-access"),
|
|
12
|
+
cdpEndpoint: z.string().optional(),
|
|
13
|
+
provider: z.object({ name: z.string(), sessionId: z.string() }).optional()
|
|
12
14
|
});
|
|
13
15
|
export {
|
|
14
16
|
RunIntegrationWorkerRequestSchema
|
|
@@ -11,6 +11,11 @@ type LaunchBrowserArgs = {
|
|
|
11
11
|
};
|
|
12
12
|
storageStatePath?: string;
|
|
13
13
|
accessMode?: SessionAccessMode;
|
|
14
|
+
cdpEndpoint?: string;
|
|
15
|
+
provider?: {
|
|
16
|
+
name: string;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
};
|
|
14
19
|
};
|
|
15
20
|
type BrowserSession = {
|
|
16
21
|
browser: Browser;
|
|
@@ -20,6 +25,6 @@ type BrowserSession = {
|
|
|
20
25
|
metadataPath: string;
|
|
21
26
|
close: () => Promise<void>;
|
|
22
27
|
};
|
|
23
|
-
declare function launchBrowser({ sessionName, headless, viewport, storageStatePath, accessMode, }: LaunchBrowserArgs): Promise<BrowserSession>;
|
|
28
|
+
declare function launchBrowser({ sessionName, headless, viewport, storageStatePath, accessMode, cdpEndpoint, provider, }: LaunchBrowserArgs): Promise<BrowserSession>;
|
|
24
29
|
|
|
25
30
|
export { type BrowserSession, type LaunchBrowserArgs, launchBrowser };
|
|
@@ -64,8 +64,46 @@ async function launchBrowser({
|
|
|
64
64
|
headless = false,
|
|
65
65
|
viewport = { width: 1366, height: 768 },
|
|
66
66
|
storageStatePath,
|
|
67
|
-
accessMode = "write-access"
|
|
67
|
+
accessMode = "write-access",
|
|
68
|
+
cdpEndpoint,
|
|
69
|
+
provider
|
|
68
70
|
}) {
|
|
71
|
+
if (cdpEndpoint) {
|
|
72
|
+
const browser2 = await chromium.connectOverCDP(cdpEndpoint);
|
|
73
|
+
const context2 = browser2.contexts()[0] ?? await browser2.newContext({ viewport });
|
|
74
|
+
const page2 = context2.pages()[0] ?? await context2.newPage();
|
|
75
|
+
page2.setDefaultTimeout(3e4);
|
|
76
|
+
page2.setDefaultNavigationTimeout(45e3);
|
|
77
|
+
const metadataPath2 = ensureLibrettoSessionStatePath(sessionName);
|
|
78
|
+
writeFileSync(
|
|
79
|
+
metadataPath2,
|
|
80
|
+
JSON.stringify(
|
|
81
|
+
{
|
|
82
|
+
version: SESSION_STATE_VERSION,
|
|
83
|
+
session: sessionName,
|
|
84
|
+
port: 0,
|
|
85
|
+
cdpEndpoint,
|
|
86
|
+
pid: process.pid,
|
|
87
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
88
|
+
status: "active",
|
|
89
|
+
mode: accessMode,
|
|
90
|
+
...provider ? { provider } : {}
|
|
91
|
+
},
|
|
92
|
+
null,
|
|
93
|
+
2
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
return {
|
|
97
|
+
browser: browser2,
|
|
98
|
+
context: context2,
|
|
99
|
+
page: page2,
|
|
100
|
+
debugPort: 0,
|
|
101
|
+
metadataPath: metadataPath2,
|
|
102
|
+
close: async () => {
|
|
103
|
+
await browser2.close();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
69
107
|
const debugPort = await pickFreePort();
|
|
70
108
|
const windowPosition = headless ? void 0 : resolveWindowPosition();
|
|
71
109
|
const browser = await chromium.launch({
|
|
@@ -7,6 +7,7 @@ declare const SessionStatusSchema: z.ZodEnum<{
|
|
|
7
7
|
completed: "completed";
|
|
8
8
|
failed: "failed";
|
|
9
9
|
exited: "exited";
|
|
10
|
+
"cleanup-failed": "cleanup-failed";
|
|
10
11
|
}>;
|
|
11
12
|
declare const SessionAccessModeSchema: z.ZodEnum<{
|
|
12
13
|
"read-only": "read-only";
|
|
@@ -16,6 +17,10 @@ declare const SessionViewportSchema: z.ZodObject<{
|
|
|
16
17
|
width: z.ZodNumber;
|
|
17
18
|
height: z.ZodNumber;
|
|
18
19
|
}, z.core.$strip>;
|
|
20
|
+
declare const ProviderStateSchema: z.ZodObject<{
|
|
21
|
+
name: z.ZodString;
|
|
22
|
+
sessionId: z.ZodString;
|
|
23
|
+
}, z.core.$strip>;
|
|
19
24
|
declare const SessionStateFileSchema: z.ZodObject<{
|
|
20
25
|
version: z.ZodLiteral<1>;
|
|
21
26
|
port: z.ZodNumber;
|
|
@@ -29,6 +34,7 @@ declare const SessionStateFileSchema: z.ZodObject<{
|
|
|
29
34
|
completed: "completed";
|
|
30
35
|
failed: "failed";
|
|
31
36
|
exited: "exited";
|
|
37
|
+
"cleanup-failed": "cleanup-failed";
|
|
32
38
|
}>>;
|
|
33
39
|
mode: z.ZodDefault<z.ZodEnum<{
|
|
34
40
|
"read-only": "read-only";
|
|
@@ -38,6 +44,10 @@ declare const SessionStateFileSchema: z.ZodObject<{
|
|
|
38
44
|
width: z.ZodNumber;
|
|
39
45
|
height: z.ZodNumber;
|
|
40
46
|
}, z.core.$strip>>;
|
|
47
|
+
provider: z.ZodOptional<z.ZodObject<{
|
|
48
|
+
name: z.ZodString;
|
|
49
|
+
sessionId: z.ZodString;
|
|
50
|
+
}, z.core.$strip>>;
|
|
41
51
|
}, z.core.$strip>;
|
|
42
52
|
type SessionStatus = z.infer<typeof SessionStatusSchema>;
|
|
43
53
|
type SessionAccessMode = z.infer<typeof SessionAccessModeSchema>;
|
|
@@ -47,4 +57,4 @@ declare function parseSessionStateData(rawState: unknown, source: string): Sessi
|
|
|
47
57
|
declare function parseSessionStateContent(content: string, source: string): SessionState;
|
|
48
58
|
declare function serializeSessionState(state: SessionState): SessionStateFile;
|
|
49
59
|
|
|
50
|
-
export { SESSION_STATE_VERSION, type SessionAccessMode, SessionAccessModeSchema, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
|
|
60
|
+
export { ProviderStateSchema, SESSION_STATE_VERSION, type SessionAccessMode, SessionAccessModeSchema, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
|
|
@@ -5,13 +5,18 @@ const SessionStatusSchema = z.enum([
|
|
|
5
5
|
"paused",
|
|
6
6
|
"completed",
|
|
7
7
|
"failed",
|
|
8
|
-
"exited"
|
|
8
|
+
"exited",
|
|
9
|
+
"cleanup-failed"
|
|
9
10
|
]);
|
|
10
11
|
const SessionAccessModeSchema = z.enum(["read-only", "write-access"]);
|
|
11
12
|
const SessionViewportSchema = z.object({
|
|
12
13
|
width: z.number().int().min(1),
|
|
13
14
|
height: z.number().int().min(1)
|
|
14
15
|
});
|
|
16
|
+
const ProviderStateSchema = z.object({
|
|
17
|
+
name: z.string(),
|
|
18
|
+
sessionId: z.string()
|
|
19
|
+
});
|
|
15
20
|
const SessionStateFileSchema = z.object({
|
|
16
21
|
version: z.literal(SESSION_STATE_VERSION),
|
|
17
22
|
port: z.number().int().min(0).max(65535),
|
|
@@ -21,7 +26,8 @@ const SessionStateFileSchema = z.object({
|
|
|
21
26
|
startedAt: z.string().datetime({ offset: true }),
|
|
22
27
|
status: SessionStatusSchema.optional(),
|
|
23
28
|
mode: SessionAccessModeSchema.default("write-access"),
|
|
24
|
-
viewport: SessionViewportSchema.optional()
|
|
29
|
+
viewport: SessionViewportSchema.optional(),
|
|
30
|
+
provider: ProviderStateSchema.optional()
|
|
25
31
|
});
|
|
26
32
|
function formatIssues(error) {
|
|
27
33
|
return error.issues.map((issue) => {
|
|
@@ -57,6 +63,7 @@ function serializeSessionState(state) {
|
|
|
57
63
|
});
|
|
58
64
|
}
|
|
59
65
|
export {
|
|
66
|
+
ProviderStateSchema,
|
|
60
67
|
SESSION_STATE_VERSION,
|
|
61
68
|
SessionAccessModeSchema,
|
|
62
69
|
SessionStateFileSchema,
|
package/package.json
CHANGED
package/skills/AGENTS.md
CHANGED