libretto 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -10
- package/README.template.md +23 -10
- package/dist/cli/cli.js +10 -0
- package/dist/cli/commands/ai.js +77 -2
- package/dist/cli/commands/browser.js +98 -8
- package/dist/cli/commands/execution.js +152 -56
- package/dist/cli/commands/setup.js +390 -0
- package/dist/cli/commands/snapshot.js +2 -2
- package/dist/cli/commands/status.js +62 -0
- package/dist/cli/core/{snapshot-api-config.js → ai-model.js} +81 -7
- package/dist/cli/core/api-snapshot-analyzer.js +7 -5
- package/dist/cli/core/browser.js +202 -36
- package/dist/cli/core/{ai-config.js → config.js} +14 -79
- package/dist/cli/core/context.js +1 -25
- package/dist/cli/core/deploy-artifact.js +121 -61
- 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/readonly-exec.js +231 -0
- package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
- package/dist/cli/core/session.js +53 -0
- package/dist/cli/core/skill-version.js +73 -0
- package/dist/cli/core/telemetry.js +1 -54
- package/dist/cli/index.js +1 -7
- package/dist/cli/router.js +4 -4
- package/dist/cli/workers/run-integration-runtime.js +19 -13
- package/dist/cli/workers/run-integration-worker-protocol.js +5 -2
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -2
- package/dist/runtime/extract/extract.d.ts +2 -2
- package/dist/runtime/extract/extract.js +4 -2
- package/dist/runtime/extract/index.d.ts +1 -1
- package/dist/runtime/recovery/agent.d.ts +2 -3
- package/dist/runtime/recovery/agent.js +5 -3
- package/dist/runtime/recovery/errors.d.ts +2 -3
- package/dist/runtime/recovery/errors.js +4 -2
- package/dist/runtime/recovery/index.d.ts +1 -2
- package/dist/runtime/recovery/recovery.d.ts +2 -3
- package/dist/runtime/recovery/recovery.js +3 -3
- package/dist/shared/debug/pause.js +4 -21
- package/dist/shared/run/api.d.ts +2 -0
- package/dist/shared/run/browser.d.ts +9 -1
- package/dist/shared/run/browser.js +43 -3
- package/dist/shared/state/index.d.ts +1 -1
- package/dist/shared/state/index.js +2 -0
- package/dist/shared/state/session-state.d.ts +20 -1
- package/dist/shared/state/session-state.js +12 -2
- package/dist/shared/workflow/workflow.d.ts +2 -1
- package/dist/shared/workflow/workflow.js +16 -9
- package/package.json +17 -16
- package/scripts/postinstall.mjs +13 -11
- package/scripts/skills-libretto.mjs +14 -4
- package/skills/AGENTS.md +11 -0
- package/skills/libretto/SKILL.md +30 -9
- package/skills/libretto/references/auth-profiles.md +1 -1
- package/skills/libretto/references/code-generation-rules.md +3 -3
- package/skills/libretto/references/configuration-file-reference.md +11 -6
- package/skills/libretto-readonly/SKILL.md +95 -0
- package/src/cli/cli.ts +10 -0
- package/src/cli/commands/ai.ts +111 -1
- package/src/cli/commands/browser.ts +111 -9
- package/src/cli/commands/execution.ts +181 -74
- package/src/cli/commands/setup.ts +516 -0
- package/src/cli/commands/snapshot.ts +2 -2
- package/src/cli/commands/status.ts +79 -0
- package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
- package/src/cli/core/api-snapshot-analyzer.ts +7 -5
- package/src/cli/core/browser.ts +242 -35
- package/src/cli/core/{ai-config.ts → config.ts} +14 -108
- package/src/cli/core/context.ts +1 -45
- package/src/cli/core/deploy-artifact.ts +141 -71
- 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/readonly-exec.ts +284 -0
- package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
- package/src/cli/core/session.ts +75 -2
- package/src/cli/core/skill-version.ts +93 -0
- package/src/cli/core/telemetry.ts +0 -52
- package/src/cli/index.ts +0 -6
- package/src/cli/router.ts +4 -4
- package/src/cli/workers/run-integration-runtime.ts +18 -16
- package/src/cli/workers/run-integration-worker-protocol.ts +4 -1
- package/src/index.ts +1 -7
- package/src/runtime/extract/extract.ts +6 -5
- package/src/runtime/recovery/agent.ts +5 -4
- package/src/runtime/recovery/errors.ts +4 -3
- package/src/runtime/recovery/recovery.ts +4 -4
- package/src/shared/debug/pause.ts +4 -23
- package/src/shared/run/browser.ts +50 -1
- package/src/shared/state/index.ts +2 -0
- package/src/shared/state/session-state.ts +10 -0
- package/src/shared/workflow/workflow.ts +24 -13
- package/dist/cli/commands/init.js +0 -286
- package/dist/cli/commands/logs.js +0 -117
- package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
- package/dist/shared/llm/ai-sdk-adapter.js +0 -49
- package/dist/shared/llm/client.d.ts +0 -13
- package/dist/shared/llm/index.d.ts +0 -5
- package/dist/shared/llm/index.js +0 -6
- package/dist/shared/llm/types.d.ts +0 -67
- package/src/cli/commands/init.ts +0 -331
- package/src/cli/commands/logs.ts +0 -128
- package/src/shared/llm/ai-sdk-adapter.ts +0 -81
- package/src/shared/llm/index.ts +0 -3
- package/src/shared/llm/types.ts +0 -63
- /package/dist/{shared/llm → cli/core/providers}/types.js +0 -0
|
@@ -10,11 +10,16 @@ import {
|
|
|
10
10
|
rmSync,
|
|
11
11
|
writeFileSync
|
|
12
12
|
} from "node:fs";
|
|
13
|
+
import { createRequire, Module } from "node:module";
|
|
13
14
|
import { tmpdir } from "node:os";
|
|
14
15
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
import { gzipSync } from "node:zlib";
|
|
17
18
|
import { build } from "esbuild";
|
|
19
|
+
import {
|
|
20
|
+
getWorkflowsFromModuleExports,
|
|
21
|
+
LIBRETTO_WORKFLOW_BRAND
|
|
22
|
+
} from "../../shared/workflow/workflow.js";
|
|
18
23
|
const DEFAULT_RUNTIME_EXTERNALS = [
|
|
19
24
|
"libretto",
|
|
20
25
|
"playwright",
|
|
@@ -43,6 +48,7 @@ const CURRENT_LIBRETTO_VERSION = readCurrentLibrettoVersion();
|
|
|
43
48
|
const CURRENT_LIBRETTO_PACKAGE_DIR = fileURLToPath(
|
|
44
49
|
new URL("../../..", import.meta.url)
|
|
45
50
|
);
|
|
51
|
+
const require2 = createRequire(import.meta.url);
|
|
46
52
|
function readCurrentLibrettoVersion() {
|
|
47
53
|
const packageJsonPath = fileURLToPath(
|
|
48
54
|
new URL("../../../package.json", import.meta.url)
|
|
@@ -464,36 +470,114 @@ function formatBuildError(error) {
|
|
|
464
470
|
return `${location} ${entry.text ?? error.message}`;
|
|
465
471
|
}).join("\n");
|
|
466
472
|
}
|
|
467
|
-
function
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
)) {
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
473
|
+
function getGeneratedWorkflowExportName(index) {
|
|
474
|
+
return `workflow_${index}`;
|
|
475
|
+
}
|
|
476
|
+
function getPackageNameFromImportPath(importPath) {
|
|
477
|
+
if (importPath.startsWith("@")) {
|
|
478
|
+
return importPath.split("/").slice(0, 2).join("/");
|
|
479
|
+
}
|
|
480
|
+
return importPath.split("/")[0] ?? importPath;
|
|
481
|
+
}
|
|
482
|
+
function createExternalDiscoveryStub() {
|
|
483
|
+
const stub = (() => createExternalDiscoveryStub());
|
|
484
|
+
return new Proxy(stub, {
|
|
485
|
+
apply: () => createExternalDiscoveryStub(),
|
|
486
|
+
construct: () => createExternalDiscoveryStub(),
|
|
487
|
+
get: (_target, property) => {
|
|
488
|
+
if (property === "__esModule") {
|
|
489
|
+
return true;
|
|
480
490
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
);
|
|
484
|
-
if (aliasMatch?.[2]) {
|
|
485
|
-
exportNames.add(aliasMatch[2]);
|
|
486
|
-
continue;
|
|
491
|
+
if (property === "default") {
|
|
492
|
+
return createExternalDiscoveryStub();
|
|
487
493
|
}
|
|
488
|
-
if (
|
|
489
|
-
|
|
494
|
+
if (property === Symbol.toPrimitive) {
|
|
495
|
+
return () => "";
|
|
490
496
|
}
|
|
497
|
+
return createExternalDiscoveryStub();
|
|
491
498
|
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
function createDiscoveryLibrettoModule(workflowNames) {
|
|
502
|
+
const moduleShape = {
|
|
503
|
+
LIBRETTO_WORKFLOW_BRAND,
|
|
504
|
+
workflow: (name) => {
|
|
505
|
+
workflowNames.add(name);
|
|
506
|
+
return {
|
|
507
|
+
[LIBRETTO_WORKFLOW_BRAND]: true,
|
|
508
|
+
name,
|
|
509
|
+
async run() {
|
|
510
|
+
return void 0;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
return new Proxy(moduleShape, {
|
|
516
|
+
get(target, property) {
|
|
517
|
+
if (property in target) {
|
|
518
|
+
return target[property];
|
|
519
|
+
}
|
|
520
|
+
if (property === "__esModule") {
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
return createExternalDiscoveryStub();
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
function discoverBundledWorkflowNames(args) {
|
|
528
|
+
const discoveryPath = join(
|
|
529
|
+
args.absSourceDir,
|
|
530
|
+
`.libretto-deploy-discovery-${process.pid}-${Date.now()}.cjs`
|
|
531
|
+
);
|
|
532
|
+
const originalRequire = Module.prototype.require;
|
|
533
|
+
const workflowNames = /* @__PURE__ */ new Set();
|
|
534
|
+
const discoveryLibrettoModule = createDiscoveryLibrettoModule(workflowNames);
|
|
535
|
+
let loadedModuleExports = null;
|
|
536
|
+
try {
|
|
537
|
+
writeFileSync(discoveryPath, args.bundleBuffer);
|
|
538
|
+
Module.prototype.require = function patchedRequire(id) {
|
|
539
|
+
const packageName = getPackageNameFromImportPath(id);
|
|
540
|
+
if (packageName === "libretto") {
|
|
541
|
+
return discoveryLibrettoModule;
|
|
542
|
+
}
|
|
543
|
+
if (packageName !== "libretto" && args.externalPackages.has(packageName)) {
|
|
544
|
+
return createExternalDiscoveryStub();
|
|
545
|
+
}
|
|
546
|
+
return originalRequire.call(this, id);
|
|
547
|
+
};
|
|
548
|
+
loadedModuleExports = require2(discoveryPath);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`Failed to evaluate deploy entry point ${args.absEntryPoint} while discovering workflows.
|
|
552
|
+
${formatBuildError(error)}`
|
|
553
|
+
);
|
|
554
|
+
} finally {
|
|
555
|
+
Module.prototype.require = originalRequire;
|
|
556
|
+
delete require2.cache?.[discoveryPath];
|
|
557
|
+
rmSync(discoveryPath, { force: true });
|
|
492
558
|
}
|
|
493
|
-
|
|
494
|
-
|
|
559
|
+
const discoveredWorkflowNames = [...workflowNames].sort(
|
|
560
|
+
(left, right) => left.localeCompare(right)
|
|
561
|
+
);
|
|
562
|
+
if (discoveredWorkflowNames.length === 0) {
|
|
563
|
+
throw new Error(
|
|
564
|
+
`No workflows were found in ${args.absEntryPoint}. Import the workflow files you want to deploy from the entry point, or export a workflow directly from it.`
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
const exportedWorkflowNames = new Set(
|
|
568
|
+
getWorkflowsFromModuleExports(loadedModuleExports ?? {}).map(
|
|
569
|
+
(workflow) => workflow.name
|
|
570
|
+
)
|
|
571
|
+
);
|
|
572
|
+
const nonExportedWorkflowNames = discoveredWorkflowNames.filter(
|
|
573
|
+
(name) => !exportedWorkflowNames.has(name)
|
|
574
|
+
);
|
|
575
|
+
if (nonExportedWorkflowNames.length > 0) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`Workflows discovered in ${args.absEntryPoint} must be exported from the deploy entry point. Re-export them from the entry point or export them through a \`workflows\` object. Non-exported workflows: ${nonExportedWorkflowNames.join(", ")}`
|
|
578
|
+
);
|
|
495
579
|
}
|
|
496
|
-
return
|
|
580
|
+
return discoveredWorkflowNames;
|
|
497
581
|
}
|
|
498
582
|
function createBootstrapSource(args) {
|
|
499
583
|
const bundleHash = createHash("sha256").update(args.bundleBuffer).digest("hex").slice(0, 16);
|
|
@@ -501,17 +585,15 @@ function createBootstrapSource(args) {
|
|
|
501
585
|
"base64"
|
|
502
586
|
);
|
|
503
587
|
const outputPrefix = `${normalizePackageName(args.deploymentName)}-`;
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
(name) => `export const ${name} = createWorkflowProxy(${JSON.stringify(name)});`
|
|
588
|
+
const exportLines = args.workflowNames.map(
|
|
589
|
+
(name, index) => `export const ${getGeneratedWorkflowExportName(index)} = createWorkflowProxy(${JSON.stringify(name)});`
|
|
507
590
|
).join("\n");
|
|
508
|
-
const defaultExportLine = hasDefaultExport ? 'export default createWorkflowProxy("default");' : "";
|
|
509
591
|
return `import { createRequire } from "node:module";
|
|
510
592
|
import { existsSync, writeFileSync } from "node:fs";
|
|
511
593
|
import { tmpdir } from "node:os";
|
|
512
594
|
import { join } from "node:path";
|
|
513
595
|
import { gunzipSync } from "node:zlib";
|
|
514
|
-
import { workflow } from "libretto";
|
|
596
|
+
import { getWorkflowFromModuleExports, workflow } from "libretto";
|
|
515
597
|
|
|
516
598
|
const BUNDLE_HASH = ${JSON.stringify(bundleHash)};
|
|
517
599
|
const BUNDLE_GZIP_BASE64 = ${JSON.stringify(bundleBase64)};
|
|
@@ -534,13 +616,13 @@ function ensureBundleFile() {
|
|
|
534
616
|
return BUNDLE_FILENAME;
|
|
535
617
|
}
|
|
536
618
|
|
|
537
|
-
function createWorkflowProxy(
|
|
538
|
-
return workflow(
|
|
619
|
+
function createWorkflowProxy(workflowName) {
|
|
620
|
+
return workflow(workflowName, async (ctx, input) => {
|
|
539
621
|
const impl = nativeRequire(ensureBundleFile());
|
|
540
|
-
const target = impl
|
|
622
|
+
const target = getWorkflowFromModuleExports(impl, workflowName);
|
|
541
623
|
if (!target || typeof target.run !== "function") {
|
|
542
624
|
throw new Error(
|
|
543
|
-
\`Expected workflow
|
|
625
|
+
\`Expected exported workflow "\${workflowName}" to be available in the bundled deployment implementation.\`,
|
|
544
626
|
);
|
|
545
627
|
}
|
|
546
628
|
return await target.run(ctx, input);
|
|
@@ -548,7 +630,6 @@ function createWorkflowProxy(exportName) {
|
|
|
548
630
|
}
|
|
549
631
|
|
|
550
632
|
${exportLines}
|
|
551
|
-
${defaultExportLine}
|
|
552
633
|
`;
|
|
553
634
|
}
|
|
554
635
|
async function writeBundledDeployEntrypoint(args) {
|
|
@@ -576,39 +657,18 @@ async function writeBundledDeployEntrypoint(args) {
|
|
|
576
657
|
"Bundler did not produce a deployment implementation file."
|
|
577
658
|
);
|
|
578
659
|
}
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
format: "esm",
|
|
585
|
-
outfile: "entry-exports.js",
|
|
586
|
-
platform: "node",
|
|
587
|
-
plugins: [
|
|
588
|
-
workspaceSourcePlugin(args.workspacePackages, args.externalPackages)
|
|
589
|
-
],
|
|
590
|
-
splitting: false,
|
|
591
|
-
target: "node20",
|
|
592
|
-
write: false
|
|
660
|
+
const workflowNames = discoverBundledWorkflowNames({
|
|
661
|
+
absEntryPoint: args.absEntryPoint,
|
|
662
|
+
absSourceDir: args.absSourceDir,
|
|
663
|
+
bundleBuffer: Buffer.from(bundledImplementation.contents),
|
|
664
|
+
externalPackages: args.externalPackages
|
|
593
665
|
});
|
|
594
|
-
const bundledExports = exportBuild.outputFiles?.find(
|
|
595
|
-
(file) => file.path.endsWith("entry-exports.js")
|
|
596
|
-
);
|
|
597
|
-
if (!bundledExports) {
|
|
598
|
-
throw new Error("Bundler did not produce an export analysis file.");
|
|
599
|
-
}
|
|
600
|
-
const exportNames = extractExportNamesFromEsmBundle(bundledExports.text);
|
|
601
|
-
if (exportNames.length === 0) {
|
|
602
|
-
throw new Error(
|
|
603
|
-
`No named exports were found in ${args.absEntryPoint}. Hosted deploy expects the entry point to export one or more workflows.`
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
666
|
writeFileSync(
|
|
607
667
|
join(args.outputDir, "index.js"),
|
|
608
668
|
createBootstrapSource({
|
|
609
669
|
bundleBuffer: Buffer.from(bundledImplementation.contents),
|
|
610
670
|
deploymentName: args.deploymentName,
|
|
611
|
-
|
|
671
|
+
workflowNames
|
|
612
672
|
})
|
|
613
673
|
);
|
|
614
674
|
} catch (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
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const PAGE_READ_METHODS = /* @__PURE__ */ new Set([
|
|
2
|
+
"url",
|
|
3
|
+
"title",
|
|
4
|
+
"content",
|
|
5
|
+
"pageErrors",
|
|
6
|
+
"viewportSize",
|
|
7
|
+
"waitForLoadState",
|
|
8
|
+
"waitForRequest",
|
|
9
|
+
"waitForResponse",
|
|
10
|
+
"waitForURL"
|
|
11
|
+
]);
|
|
12
|
+
const PAGE_LOCATOR_FACTORY_METHODS = /* @__PURE__ */ new Set([
|
|
13
|
+
"locator",
|
|
14
|
+
"getByRole",
|
|
15
|
+
"getByText",
|
|
16
|
+
"getByLabel",
|
|
17
|
+
"getByPlaceholder",
|
|
18
|
+
"getByAltText",
|
|
19
|
+
"getByTitle",
|
|
20
|
+
"getByTestId"
|
|
21
|
+
]);
|
|
22
|
+
const PAGE_ALLOWED_PROPERTIES = /* @__PURE__ */ new Set([]);
|
|
23
|
+
const LOCATOR_READ_METHODS = /* @__PURE__ */ new Set([
|
|
24
|
+
"textContent",
|
|
25
|
+
"innerText",
|
|
26
|
+
"allTextContents",
|
|
27
|
+
"allInnerTexts",
|
|
28
|
+
"ariaSnapshot",
|
|
29
|
+
"boundingBox",
|
|
30
|
+
"count",
|
|
31
|
+
"getAttribute",
|
|
32
|
+
"inputValue",
|
|
33
|
+
"isChecked",
|
|
34
|
+
"isDisabled",
|
|
35
|
+
"isEditable",
|
|
36
|
+
"isEnabled",
|
|
37
|
+
"isVisible",
|
|
38
|
+
"isHidden",
|
|
39
|
+
"waitFor"
|
|
40
|
+
]);
|
|
41
|
+
const LOCATOR_FACTORY_METHODS = /* @__PURE__ */ new Set([
|
|
42
|
+
"locator",
|
|
43
|
+
"getByRole",
|
|
44
|
+
"getByText",
|
|
45
|
+
"getByLabel",
|
|
46
|
+
"getByPlaceholder",
|
|
47
|
+
"getByAltText",
|
|
48
|
+
"getByTitle",
|
|
49
|
+
"getByTestId",
|
|
50
|
+
"filter",
|
|
51
|
+
"and",
|
|
52
|
+
"or",
|
|
53
|
+
"first",
|
|
54
|
+
"last",
|
|
55
|
+
"nth"
|
|
56
|
+
]);
|
|
57
|
+
const LOCATOR_COLLECTION_FACTORY_METHODS = /* @__PURE__ */ new Set(["all"]);
|
|
58
|
+
const LOCATOR_SCROLL_METHODS = /* @__PURE__ */ new Set(["scrollIntoViewIfNeeded"]);
|
|
59
|
+
const LOCATOR_ALLOWED_PROPERTIES = /* @__PURE__ */ new Set([]);
|
|
60
|
+
const readonlyPageCache = /* @__PURE__ */ new WeakMap();
|
|
61
|
+
const readonlyLocatorCache = /* @__PURE__ */ new WeakMap();
|
|
62
|
+
function markActivity(onActivity) {
|
|
63
|
+
onActivity?.();
|
|
64
|
+
}
|
|
65
|
+
class ReadonlyExecDeniedError extends Error {
|
|
66
|
+
constructor(message) {
|
|
67
|
+
super(`ReadonlyExecDenied: ${message}`);
|
|
68
|
+
this.name = "ReadonlyExecDenied";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function denyOperation(targetName, method) {
|
|
72
|
+
throw new ReadonlyExecDeniedError(
|
|
73
|
+
`${targetName}.${method} is blocked in readonly-exec`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function wrapLocatorForReadonlyExec(locator, options = {}) {
|
|
77
|
+
const cached = readonlyLocatorCache.get(locator);
|
|
78
|
+
if (cached) return cached;
|
|
79
|
+
const proxy = new Proxy(locator, {
|
|
80
|
+
get(target, prop, receiver) {
|
|
81
|
+
if (typeof prop !== "string") {
|
|
82
|
+
return Reflect.get(target, prop, receiver);
|
|
83
|
+
}
|
|
84
|
+
const value = Reflect.get(target, prop, target);
|
|
85
|
+
if (typeof value !== "function") {
|
|
86
|
+
if (LOCATOR_ALLOWED_PROPERTIES.has(prop)) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
return denyOperation("locator", prop);
|
|
90
|
+
}
|
|
91
|
+
if (LOCATOR_READ_METHODS.has(prop)) {
|
|
92
|
+
return (...args) => {
|
|
93
|
+
const result = value.apply(target, args);
|
|
94
|
+
markActivity(options.onActivity);
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (LOCATOR_FACTORY_METHODS.has(prop)) {
|
|
99
|
+
return (...args) => {
|
|
100
|
+
const nextLocator = value.apply(target, args);
|
|
101
|
+
markActivity(options.onActivity);
|
|
102
|
+
return wrapLocatorForReadonlyExec(nextLocator, options);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (LOCATOR_COLLECTION_FACTORY_METHODS.has(prop)) {
|
|
106
|
+
return async (...args) => {
|
|
107
|
+
const locators = await value.apply(target, args);
|
|
108
|
+
markActivity(options.onActivity);
|
|
109
|
+
return locators.map(
|
|
110
|
+
(locator2) => wrapLocatorForReadonlyExec(locator2, options)
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (LOCATOR_SCROLL_METHODS.has(prop)) {
|
|
115
|
+
return async (...args) => {
|
|
116
|
+
await value.apply(target, args);
|
|
117
|
+
markActivity(options.onActivity);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return (..._args) => denyOperation("locator", prop);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
readonlyLocatorCache.set(locator, proxy);
|
|
124
|
+
return proxy;
|
|
125
|
+
}
|
|
126
|
+
function wrapPageForReadonlyExec(page, options = {}) {
|
|
127
|
+
const cached = readonlyPageCache.get(page);
|
|
128
|
+
if (cached) return cached;
|
|
129
|
+
const proxy = new Proxy(page, {
|
|
130
|
+
get(target, prop, receiver) {
|
|
131
|
+
if (typeof prop !== "string") {
|
|
132
|
+
return Reflect.get(target, prop, receiver);
|
|
133
|
+
}
|
|
134
|
+
const value = Reflect.get(target, prop, target);
|
|
135
|
+
if (typeof value !== "function") {
|
|
136
|
+
if (PAGE_ALLOWED_PROPERTIES.has(prop)) {
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
return denyOperation("page", prop);
|
|
140
|
+
}
|
|
141
|
+
if (PAGE_READ_METHODS.has(prop)) {
|
|
142
|
+
return (...args) => {
|
|
143
|
+
const result = value.apply(target, args);
|
|
144
|
+
markActivity(options.onActivity);
|
|
145
|
+
return result;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (PAGE_LOCATOR_FACTORY_METHODS.has(prop)) {
|
|
149
|
+
return (...args) => {
|
|
150
|
+
const locator = value.apply(target, args);
|
|
151
|
+
markActivity(options.onActivity);
|
|
152
|
+
return wrapLocatorForReadonlyExec(locator, options);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return (..._args) => denyOperation("page", prop);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
readonlyPageCache.set(page, proxy);
|
|
159
|
+
return proxy;
|
|
160
|
+
}
|
|
161
|
+
function resolveRequestMethod(input, init) {
|
|
162
|
+
const requestMethod = typeof Request !== "undefined" && input instanceof Request ? input.method : void 0;
|
|
163
|
+
return (init?.method ?? requestMethod ?? "GET").toUpperCase();
|
|
164
|
+
}
|
|
165
|
+
function assertReadonlyRequestBodyAllowed(input, init) {
|
|
166
|
+
if (init?.body !== void 0) {
|
|
167
|
+
throw new ReadonlyExecDeniedError(
|
|
168
|
+
"request bodies are blocked in readonly-exec"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (typeof Request !== "undefined" && input instanceof Request && input.body !== null) {
|
|
172
|
+
throw new ReadonlyExecDeniedError(
|
|
173
|
+
"request bodies are blocked in readonly-exec"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function createReadonlyExecHelpers(page, options = {}) {
|
|
178
|
+
const readonlyPage = wrapPageForReadonlyExec(page, options);
|
|
179
|
+
const execState = {};
|
|
180
|
+
return {
|
|
181
|
+
page: readonlyPage,
|
|
182
|
+
state: execState,
|
|
183
|
+
// Playwright has no native viewport scroll method — only locator.scrollIntoViewIfNeeded().
|
|
184
|
+
// Arbitrary scrolling requires page.evaluate(), which is blocked by the readonly proxy
|
|
185
|
+
// since it can run arbitrary code. This helper calls evaluate on the raw (unwrapped) page,
|
|
186
|
+
// scoped to just window.scrollBy.
|
|
187
|
+
scrollBy: async (deltaX, deltaY) => {
|
|
188
|
+
await page.evaluate(
|
|
189
|
+
([x, y]) => {
|
|
190
|
+
window.scrollBy(x, y);
|
|
191
|
+
},
|
|
192
|
+
[deltaX, deltaY]
|
|
193
|
+
);
|
|
194
|
+
markActivity(options.onActivity);
|
|
195
|
+
},
|
|
196
|
+
get: async (input, init) => {
|
|
197
|
+
const method = resolveRequestMethod(input, init);
|
|
198
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
199
|
+
throw new ReadonlyExecDeniedError(
|
|
200
|
+
`${method} requests are blocked in readonly-exec`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
assertReadonlyRequestBodyAllowed(input, init);
|
|
204
|
+
markActivity(options.onActivity);
|
|
205
|
+
return await fetch(input, {
|
|
206
|
+
...init,
|
|
207
|
+
method
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
// Shadows the global Node.js fetch to prevent unrestricted HTTP access.
|
|
211
|
+
// Without this, agent code would fall through to the global fetch (POST, PUT, DELETE, etc.).
|
|
212
|
+
fetch: () => {
|
|
213
|
+
throw new ReadonlyExecDeniedError(
|
|
214
|
+
"fetch is blocked in readonly-exec; use get() instead"
|
|
215
|
+
);
|
|
216
|
+
},
|
|
217
|
+
console,
|
|
218
|
+
setTimeout,
|
|
219
|
+
setInterval,
|
|
220
|
+
clearTimeout,
|
|
221
|
+
clearInterval,
|
|
222
|
+
URL,
|
|
223
|
+
Buffer
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export {
|
|
227
|
+
ReadonlyExecDeniedError,
|
|
228
|
+
createReadonlyExecHelpers,
|
|
229
|
+
wrapLocatorForReadonlyExec,
|
|
230
|
+
wrapPageForReadonlyExec
|
|
231
|
+
};
|