libretto 0.6.28 → 0.6.30-experimental-runtime-libretto.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/dist/cli/commands/execution.js +24 -10
- package/dist/cli/core/browser.js +1 -0
- package/dist/cli/core/daemon/daemon.js +2 -0
- package/dist/cli/core/deploy-artifact.js +130 -10
- package/dist/shared/run/browser.js +3 -32
- package/dist/shared/run/window-position.d.ts +9 -0
- package/dist/shared/run/window-position.js +36 -0
- package/dist/shared/workflow/authenticate.d.ts +2 -2
- package/dist/shared/workflow/authenticate.js +4 -4
- package/docs/releasing.md +1 -1
- package/package.json +19 -18
- package/skills/libretto/SKILL.md +10 -4
- package/skills/libretto/references/code-generation-rules.md +2 -9
- package/skills/libretto/references/site-security-review.md +7 -6
- package/skills/libretto/references/website-authentication.md +73 -0
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/execution.ts +38 -12
- package/src/cli/core/browser.ts +1 -1
- package/src/cli/core/daemon/daemon.ts +2 -0
- package/src/cli/core/deploy-artifact.ts +145 -10
- package/src/shared/run/browser.ts +5 -44
- package/src/shared/run/window-position.ts +49 -0
- package/src/shared/workflow/authenticate.ts +6 -6
- package/skills/libretto/references/auth-profiles.md +0 -79
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
connect,
|
|
7
7
|
disconnectBrowser,
|
|
8
8
|
runClose,
|
|
9
|
+
resolveWindowPosition,
|
|
9
10
|
resolveViewport
|
|
10
11
|
} from "../core/browser.js";
|
|
11
12
|
import { parseViewportArg } from "./browser.js";
|
|
@@ -20,7 +21,9 @@ import {
|
|
|
20
21
|
writeSessionState
|
|
21
22
|
} from "../core/session.js";
|
|
22
23
|
import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
readLibrettoConfig
|
|
26
|
+
} from "../core/config.js";
|
|
24
27
|
import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
|
|
25
28
|
import {
|
|
26
29
|
getProviderStartupTimeoutMs,
|
|
@@ -51,6 +54,20 @@ import {
|
|
|
51
54
|
withRequiredSession
|
|
52
55
|
} from "./shared.js";
|
|
53
56
|
const require2 = moduleBuiltin.createRequire(import.meta.url);
|
|
57
|
+
function createRunBrowserConfig(args) {
|
|
58
|
+
if (args.providerName) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "provider",
|
|
61
|
+
providerName: args.providerName
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
kind: "launch",
|
|
66
|
+
headed: !args.headless,
|
|
67
|
+
viewport: args.viewport ?? { width: 1366, height: 768 },
|
|
68
|
+
...!args.headless && args.windowPosition ? { windowPosition: args.windowPosition } : {}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
54
71
|
function writeDaemonExecOutput(output) {
|
|
55
72
|
if (output?.stdout) {
|
|
56
73
|
process.stdout.write(output.stdout);
|
|
@@ -427,14 +444,7 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
427
444
|
config: {
|
|
428
445
|
session: args.session,
|
|
429
446
|
experiments: args.experiments,
|
|
430
|
-
browser: args
|
|
431
|
-
kind: "provider",
|
|
432
|
-
providerName: args.providerName
|
|
433
|
-
} : {
|
|
434
|
-
kind: "launch",
|
|
435
|
-
headed: !args.headless,
|
|
436
|
-
viewport: args.viewport ?? { width: 1366, height: 768 }
|
|
437
|
-
},
|
|
447
|
+
browser: createRunBrowserConfig(args),
|
|
438
448
|
workflow: {
|
|
439
449
|
integrationPath: absoluteIntegrationPath,
|
|
440
450
|
params: args.params,
|
|
@@ -690,15 +700,18 @@ const runCommand = SimpleCLI.command({
|
|
|
690
700
|
});
|
|
691
701
|
console.log(`Connecting to ${providerName} browser...`);
|
|
692
702
|
}
|
|
703
|
+
const headless = daemonProviderName ? true : headlessMode ?? false;
|
|
704
|
+
const windowPosition = headless ? void 0 : resolveWindowPosition(ctx.logger);
|
|
693
705
|
await runIntegrationFromFile(
|
|
694
706
|
{
|
|
695
707
|
integrationPath: input.integrationFile,
|
|
696
708
|
session: ctx.session,
|
|
697
709
|
params,
|
|
698
710
|
tsconfigPath: input.tsconfig,
|
|
699
|
-
headless
|
|
711
|
+
headless,
|
|
700
712
|
visualize,
|
|
701
713
|
viewport,
|
|
714
|
+
windowPosition,
|
|
702
715
|
accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access",
|
|
703
716
|
providerName: daemonProviderName,
|
|
704
717
|
stayOpenOnSuccess: input.stayOpenOnSuccess,
|
|
@@ -725,6 +738,7 @@ const executionCommands = {
|
|
|
725
738
|
resume: resumeCommand
|
|
726
739
|
};
|
|
727
740
|
export {
|
|
741
|
+
createRunBrowserConfig,
|
|
728
742
|
execCommand,
|
|
729
743
|
execInput,
|
|
730
744
|
executionCommands,
|
package/dist/cli/core/browser.js
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
import { WorkflowController } from "../workflow-runner/runner.js";
|
|
50
50
|
import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
|
|
51
51
|
import { captureAuthProfileStorageState } from "../../../shared/workflow/auth-profile-state.js";
|
|
52
|
+
import { applyWindowPosition } from "../../../shared/run/window-position.js";
|
|
52
53
|
function isOperationalPage(page) {
|
|
53
54
|
const url = page.url();
|
|
54
55
|
return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
|
|
@@ -266,6 +267,7 @@ class BrowserDaemon {
|
|
|
266
267
|
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
|
267
268
|
});
|
|
268
269
|
const page = await context.newPage();
|
|
270
|
+
await applyWindowPosition(browser, context, page, config.windowPosition);
|
|
269
271
|
page.setDefaultTimeout(3e4);
|
|
270
272
|
page.setDefaultNavigationTimeout(45e3);
|
|
271
273
|
const daemon = await BrowserDaemon.initialize({
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
} from "../../shared/workflow/workflow.js";
|
|
23
23
|
import { normalizeCredentialNames } from "../../shared/workflow/credentials.js";
|
|
24
24
|
const DEFAULT_RUNTIME_EXTERNALS = [
|
|
25
|
-
"libretto",
|
|
26
25
|
"playwright",
|
|
27
26
|
"playwright-core",
|
|
28
27
|
"chromium-bidi"
|
|
@@ -408,14 +407,58 @@ function resolveDependencyVersion(sourceDir, packageName, fallbackVersion) {
|
|
|
408
407
|
`Unable to determine a version for external package "${packageName}". Add it to your package.json or remove it from --external.`
|
|
409
408
|
);
|
|
410
409
|
}
|
|
410
|
+
function resolveLibrettoDependencyVersion(sourceDir, packageName) {
|
|
411
|
+
try {
|
|
412
|
+
const librettoManifestPath = require2.resolve("libretto/package.json", {
|
|
413
|
+
paths: [sourceDir]
|
|
414
|
+
});
|
|
415
|
+
const version2 = readDependencyVersionFromManifest(
|
|
416
|
+
readPackageManifest(librettoManifestPath),
|
|
417
|
+
packageName
|
|
418
|
+
);
|
|
419
|
+
if (version2) {
|
|
420
|
+
return version2;
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
const version = readDependencyVersionFromManifest(
|
|
425
|
+
readPackageManifest(join(CURRENT_LIBRETTO_PACKAGE_DIR, "package.json")),
|
|
426
|
+
packageName
|
|
427
|
+
);
|
|
428
|
+
if (version) {
|
|
429
|
+
return version;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const manifestPath = require2.resolve(`${packageName}/package.json`, {
|
|
433
|
+
paths: [CURRENT_LIBRETTO_PACKAGE_DIR]
|
|
434
|
+
});
|
|
435
|
+
return readPackageManifest(manifestPath).version ?? null;
|
|
436
|
+
} catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
411
440
|
function writeDeployManifest(args) {
|
|
441
|
+
const dependencyPackages = [
|
|
442
|
+
.../* @__PURE__ */ new Set([
|
|
443
|
+
...BUILT_IN_MANIFEST_DEPENDENCIES,
|
|
444
|
+
...args.runtimeExternals.filter(
|
|
445
|
+
(packageName) => resolveLibrettoDependencyVersion(args.sourceDir, packageName)
|
|
446
|
+
),
|
|
447
|
+
...args.additionalExternals
|
|
448
|
+
])
|
|
449
|
+
];
|
|
412
450
|
const dependencies = Object.fromEntries(
|
|
413
|
-
|
|
414
|
-
(packageName)
|
|
451
|
+
dependencyPackages.map((packageName) => {
|
|
452
|
+
const fallbackVersion = packageName === "libretto" ? args.librettoDependency : resolveLibrettoDependencyVersion(args.sourceDir, packageName) ?? void 0;
|
|
453
|
+
return [
|
|
415
454
|
packageName,
|
|
416
|
-
packageName === "libretto" ? args.librettoDependency : resolveDependencyVersion(
|
|
417
|
-
|
|
418
|
-
|
|
455
|
+
packageName === "libretto" ? args.librettoDependency : resolveDependencyVersion(
|
|
456
|
+
args.sourceDir,
|
|
457
|
+
packageName,
|
|
458
|
+
fallbackVersion
|
|
459
|
+
)
|
|
460
|
+
];
|
|
461
|
+
})
|
|
419
462
|
);
|
|
420
463
|
writeFileSync(
|
|
421
464
|
join(args.outputDir, "package.json"),
|
|
@@ -649,7 +692,7 @@ function createBootstrapSource(args) {
|
|
|
649
692
|
authProfileRefresh: workflow.authProfileRefresh
|
|
650
693
|
})});`
|
|
651
694
|
).join("\n");
|
|
652
|
-
return `import { createRequire } from "node:module";
|
|
695
|
+
return `import { createRequire, Module } from "node:module";
|
|
653
696
|
import { existsSync, writeFileSync } from "node:fs";
|
|
654
697
|
import { tmpdir } from "node:os";
|
|
655
698
|
import { join } from "node:path";
|
|
@@ -665,6 +708,45 @@ const BUNDLE_FILENAME = join(
|
|
|
665
708
|
const nativeRequire = createRequire(
|
|
666
709
|
join(tmpdir(), ${JSON.stringify("libretto-deploy-bootstrap.cjs")}),
|
|
667
710
|
);
|
|
711
|
+
const packageRequire = createRequire(import.meta.url || __filename);
|
|
712
|
+
|
|
713
|
+
function isBarePackageSpecifier(specifier) {
|
|
714
|
+
return (
|
|
715
|
+
!specifier.startsWith(".") &&
|
|
716
|
+
!specifier.startsWith("/") &&
|
|
717
|
+
!specifier.startsWith("node:") &&
|
|
718
|
+
!/^[A-Za-z]:[\\\\/]/.test(specifier)
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function loadImplementation() {
|
|
723
|
+
const originalRequire = Module.prototype.require;
|
|
724
|
+
function patchedRequire(specifier) {
|
|
725
|
+
try {
|
|
726
|
+
return originalRequire.call(this, specifier);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
if (
|
|
729
|
+
error?.code === "MODULE_NOT_FOUND" &&
|
|
730
|
+
isBarePackageSpecifier(specifier)
|
|
731
|
+
) {
|
|
732
|
+
Module.prototype.require = originalRequire;
|
|
733
|
+
try {
|
|
734
|
+
return packageRequire(specifier);
|
|
735
|
+
} finally {
|
|
736
|
+
Module.prototype.require = patchedRequire;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
Module.prototype.require = patchedRequire;
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
return nativeRequire(ensureBundleFile());
|
|
746
|
+
} finally {
|
|
747
|
+
Module.prototype.require = originalRequire;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
668
750
|
|
|
669
751
|
function ensureBundleFile() {
|
|
670
752
|
if (!existsSync(BUNDLE_FILENAME)) {
|
|
@@ -679,7 +761,7 @@ function ensureBundleFile() {
|
|
|
679
761
|
|
|
680
762
|
function createWorkflowProxy(workflowName, metadata) {
|
|
681
763
|
const handler = async (ctx, input) => {
|
|
682
|
-
const impl =
|
|
764
|
+
const impl = loadImplementation();
|
|
683
765
|
const target = getWorkflowFromModuleExports(impl, workflowName);
|
|
684
766
|
if (!target || typeof target.run !== "function") {
|
|
685
767
|
throw new Error(
|
|
@@ -716,6 +798,11 @@ ${exportLines}
|
|
|
716
798
|
async function writeBundledDeployEntrypoint(args) {
|
|
717
799
|
try {
|
|
718
800
|
const unresolvedWorkspaceImports = /* @__PURE__ */ new Map();
|
|
801
|
+
const discoveryExternalPackages = /* @__PURE__ */ new Set([
|
|
802
|
+
...args.externalPackages,
|
|
803
|
+
"libretto"
|
|
804
|
+
]);
|
|
805
|
+
const unresolvedDiscoveryWorkspaceImports = /* @__PURE__ */ new Map();
|
|
719
806
|
const implementationBuild = await build({
|
|
720
807
|
absWorkingDir: args.absSourceDir,
|
|
721
808
|
bundle: true,
|
|
@@ -736,6 +823,25 @@ async function writeBundledDeployEntrypoint(args) {
|
|
|
736
823
|
target: "node20",
|
|
737
824
|
write: false
|
|
738
825
|
});
|
|
826
|
+
const discoveryBuild = await build({
|
|
827
|
+
absWorkingDir: args.absSourceDir,
|
|
828
|
+
bundle: true,
|
|
829
|
+
entryPoints: [args.absEntryPoint],
|
|
830
|
+
external: [...discoveryExternalPackages],
|
|
831
|
+
format: "cjs",
|
|
832
|
+
outfile: "prebundled-discovery.cjs",
|
|
833
|
+
platform: "node",
|
|
834
|
+
plugins: [
|
|
835
|
+
workspaceSourcePlugin(
|
|
836
|
+
args.workspacePackages,
|
|
837
|
+
discoveryExternalPackages,
|
|
838
|
+
unresolvedDiscoveryWorkspaceImports
|
|
839
|
+
)
|
|
840
|
+
],
|
|
841
|
+
splitting: false,
|
|
842
|
+
target: "node20",
|
|
843
|
+
write: false
|
|
844
|
+
});
|
|
739
845
|
const [unresolvedImport] = unresolvedWorkspaceImports;
|
|
740
846
|
if (unresolvedImport) {
|
|
741
847
|
const [importPath, packageDir] = unresolvedImport;
|
|
@@ -743,6 +849,13 @@ async function writeBundledDeployEntrypoint(args) {
|
|
|
743
849
|
`Unable to resolve workspace import "${importPath}" from ${packageDir}.`
|
|
744
850
|
);
|
|
745
851
|
}
|
|
852
|
+
const [unresolvedDiscoveryImport] = unresolvedDiscoveryWorkspaceImports;
|
|
853
|
+
if (unresolvedDiscoveryImport) {
|
|
854
|
+
const [importPath, packageDir] = unresolvedDiscoveryImport;
|
|
855
|
+
throw new Error(
|
|
856
|
+
`Unable to resolve workspace import "${importPath}" from ${packageDir}.`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
746
859
|
const bundledImplementation = implementationBuild.outputFiles?.find(
|
|
747
860
|
(file) => file.path.endsWith("prebundled.cjs")
|
|
748
861
|
);
|
|
@@ -751,11 +864,17 @@ async function writeBundledDeployEntrypoint(args) {
|
|
|
751
864
|
"Bundler did not produce a deployment implementation file."
|
|
752
865
|
);
|
|
753
866
|
}
|
|
867
|
+
const bundledDiscovery = discoveryBuild.outputFiles?.find(
|
|
868
|
+
(file) => file.path.endsWith("prebundled-discovery.cjs")
|
|
869
|
+
);
|
|
870
|
+
if (!bundledDiscovery) {
|
|
871
|
+
throw new Error("Bundler did not produce a deployment discovery file.");
|
|
872
|
+
}
|
|
754
873
|
const workflows = discoverBundledWorkflows({
|
|
755
874
|
absEntryPoint: args.absEntryPoint,
|
|
756
875
|
absSourceDir: args.absSourceDir,
|
|
757
|
-
bundleBuffer: Buffer.from(
|
|
758
|
-
externalPackages:
|
|
876
|
+
bundleBuffer: Buffer.from(bundledDiscovery.contents),
|
|
877
|
+
externalPackages: discoveryExternalPackages
|
|
759
878
|
});
|
|
760
879
|
writeFileSync(
|
|
761
880
|
join(args.outputDir, "index.js"),
|
|
@@ -825,6 +944,7 @@ async function createHostedDeployPackage(args) {
|
|
|
825
944
|
deploymentName: args.deploymentName,
|
|
826
945
|
librettoDependency,
|
|
827
946
|
outputDir,
|
|
947
|
+
runtimeExternals: [...DEFAULT_RUNTIME_EXTERNALS],
|
|
828
948
|
sourceDir: absSourceDir
|
|
829
949
|
});
|
|
830
950
|
writeDeployMetadata({ outputDir, workflows: workflowsWithShareableSource });
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
SessionStateFileSchema
|
|
10
10
|
} from "../state/session-state.js";
|
|
11
11
|
import { readLibrettoConfig } from "../../cli/core/config.js";
|
|
12
|
+
import {
|
|
13
|
+
applyWindowPosition
|
|
14
|
+
} from "./window-position.js";
|
|
12
15
|
async function pickFreePort() {
|
|
13
16
|
return await new Promise((resolve, reject) => {
|
|
14
17
|
const server = createServer();
|
|
@@ -27,38 +30,6 @@ async function pickFreePort() {
|
|
|
27
30
|
function resolveWindowPosition() {
|
|
28
31
|
return readLibrettoConfig().windowPosition;
|
|
29
32
|
}
|
|
30
|
-
async function applyWindowPosition(browser, context, page, windowPosition) {
|
|
31
|
-
if (!windowPosition) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const requestedBounds = {
|
|
35
|
-
left: windowPosition.x,
|
|
36
|
-
top: windowPosition.y,
|
|
37
|
-
windowState: "normal"
|
|
38
|
-
};
|
|
39
|
-
const pageCdp = await context.newCDPSession(page);
|
|
40
|
-
let browserCdp;
|
|
41
|
-
try {
|
|
42
|
-
const targetInfo = await pageCdp.send("Target.getTargetInfo");
|
|
43
|
-
const targetId = targetInfo.targetInfo?.targetId;
|
|
44
|
-
browserCdp = await browser.newBrowserCDPSession();
|
|
45
|
-
const windowResult = await browserCdp.send(
|
|
46
|
-
"Browser.getWindowForTarget",
|
|
47
|
-
targetId ? { targetId } : {}
|
|
48
|
-
);
|
|
49
|
-
await browserCdp.send("Browser.setWindowBounds", {
|
|
50
|
-
windowId: windowResult.windowId,
|
|
51
|
-
bounds: requestedBounds
|
|
52
|
-
});
|
|
53
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
54
|
-
} catch {
|
|
55
|
-
} finally {
|
|
56
|
-
await pageCdp.detach().catch(() => {
|
|
57
|
-
});
|
|
58
|
-
await browserCdp?.detach().catch(() => {
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
33
|
async function launchBrowser({
|
|
63
34
|
sessionName,
|
|
64
35
|
headless = false,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Browser, BrowserContext, Page } from 'playwright';
|
|
2
|
+
|
|
3
|
+
type WindowPosition = {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
};
|
|
7
|
+
declare function applyWindowPosition(browser: Browser, context: BrowserContext, page: Page, windowPosition: WindowPosition | undefined): Promise<void>;
|
|
8
|
+
|
|
9
|
+
export { type WindowPosition, applyWindowPosition };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
async function applyWindowPosition(browser, context, page, windowPosition) {
|
|
2
|
+
if (!windowPosition) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
const requestedBounds = {
|
|
6
|
+
left: windowPosition.x,
|
|
7
|
+
top: windowPosition.y,
|
|
8
|
+
windowState: "normal"
|
|
9
|
+
};
|
|
10
|
+
let pageCdp;
|
|
11
|
+
let browserCdp;
|
|
12
|
+
try {
|
|
13
|
+
pageCdp = await context.newCDPSession(page);
|
|
14
|
+
const targetInfo = await pageCdp.send("Target.getTargetInfo");
|
|
15
|
+
const targetId = targetInfo.targetInfo?.targetId;
|
|
16
|
+
browserCdp = await browser.newBrowserCDPSession();
|
|
17
|
+
const windowResult = await browserCdp.send(
|
|
18
|
+
"Browser.getWindowForTarget",
|
|
19
|
+
targetId ? { targetId } : {}
|
|
20
|
+
);
|
|
21
|
+
await browserCdp.send("Browser.setWindowBounds", {
|
|
22
|
+
windowId: windowResult.windowId,
|
|
23
|
+
bounds: requestedBounds
|
|
24
|
+
});
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
26
|
+
} catch {
|
|
27
|
+
} finally {
|
|
28
|
+
await pageCdp?.detach().catch(() => {
|
|
29
|
+
});
|
|
30
|
+
await browserCdp?.detach().catch(() => {
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
applyWindowPosition
|
|
36
|
+
};
|
|
@@ -5,8 +5,8 @@ import '../../runtime/recovery/page-fallbacks.js';
|
|
|
5
5
|
import 'ai';
|
|
6
6
|
|
|
7
7
|
type LibrettoAuthenticateOptions = {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
isSignedIn: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
|
|
9
|
+
signIn: (ctx: LibrettoWorkflowContext, credentials: Record<string, string>) => Promise<void> | void;
|
|
10
10
|
credentials?: Record<string, unknown>;
|
|
11
11
|
envPrefix?: string;
|
|
12
12
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
async function librettoAuthenticate(ctx, options) {
|
|
2
|
-
if (await options.
|
|
2
|
+
if (await options.isSignedIn(ctx)) {
|
|
3
3
|
return { usedProfile: true };
|
|
4
4
|
}
|
|
5
5
|
const credentials = normalizeCredentials(
|
|
6
6
|
options.credentials ?? readCredentialsFromEnv(options.envPrefix)
|
|
7
7
|
);
|
|
8
|
-
await options.
|
|
9
|
-
if (!await options.
|
|
10
|
-
throw new Error("
|
|
8
|
+
await options.signIn(ctx, credentials);
|
|
9
|
+
if (!await options.isSignedIn(ctx)) {
|
|
10
|
+
throw new Error("Sign-in completed, but the session is still not signed in.");
|
|
11
11
|
}
|
|
12
12
|
return { usedProfile: false };
|
|
13
13
|
}
|
package/docs/releasing.md
CHANGED
|
@@ -66,7 +66,7 @@ The root `scripts/prepare-release.sh` script does the following:
|
|
|
66
66
|
1. Checks that the working tree is clean.
|
|
67
67
|
2. Updates local `main` from `origin/main`.
|
|
68
68
|
3. Runs `pnpm install --frozen-lockfile`, `pnpm --filter libretto type-check`, and `pnpm --filter libretto test`.
|
|
69
|
-
4. Checks whether `packages/affordance` changed since the current Libretto release tag. If
|
|
69
|
+
4. Checks whether `packages/affordance` changed since the current Libretto release tag, excluding `packages/affordance/package.json` version-only changes. If source/package contents changed, the script runs Affordance type-check/tests and bumps `packages/affordance/package.json` by one patch version when the current Affordance version is already published to npm.
|
|
70
70
|
5. Bumps the version in `packages/libretto/package.json`.
|
|
71
71
|
6. Creates a release branch.
|
|
72
72
|
7. Commits the version bump.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.30-experimental-runtime-libretto.0",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://libretto.sh",
|
|
@@ -33,6 +33,21 @@
|
|
|
33
33
|
"default": "./dist/index.js"
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
|
|
38
|
+
"check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
|
|
39
|
+
"sync-skills": "pnpm run sync:mirrors",
|
|
40
|
+
"check:skills": "pnpm run check:mirrors",
|
|
41
|
+
"build": "tsup --config tsup.config.ts",
|
|
42
|
+
"lint": "lintcn lint --tsconfig tsconfig.json",
|
|
43
|
+
"type-check": "tsc --noEmit",
|
|
44
|
+
"test": "turbo run test:vitest --filter=libretto --log-order=grouped",
|
|
45
|
+
"test:vitest": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"cli": "node dist/index.js",
|
|
48
|
+
"generate-changelog": "tsx scripts/generate-changelog.ts",
|
|
49
|
+
"prepack": "pnpm run build"
|
|
50
|
+
},
|
|
36
51
|
"peerDependencies": {
|
|
37
52
|
"@ai-sdk/google": "^3.0.51",
|
|
38
53
|
"@ai-sdk/google-vertex": "^4.0.80"
|
|
@@ -65,25 +80,11 @@
|
|
|
65
80
|
"dependencies": {
|
|
66
81
|
"@ai-sdk/anthropic": "^3.0.66",
|
|
67
82
|
"@ai-sdk/openai": "^3.0.66",
|
|
83
|
+
"affordance": "workspace:^",
|
|
68
84
|
"ai": "^6.0.116",
|
|
69
85
|
"esbuild": "^0.27.0",
|
|
70
86
|
"playwright": "^1.58.2",
|
|
71
87
|
"tsx": "^4.21.0",
|
|
72
|
-
"zod": "^4.3.6"
|
|
73
|
-
"affordance": "^0.2.1"
|
|
74
|
-
},
|
|
75
|
-
"scripts": {
|
|
76
|
-
"sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
|
|
77
|
-
"check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
|
|
78
|
-
"sync-skills": "pnpm run sync:mirrors",
|
|
79
|
-
"check:skills": "pnpm run check:mirrors",
|
|
80
|
-
"build": "tsup --config tsup.config.ts",
|
|
81
|
-
"lint": "lintcn lint --tsconfig tsconfig.json",
|
|
82
|
-
"type-check": "tsc --noEmit",
|
|
83
|
-
"test": "turbo run test:vitest --filter=libretto --log-order=grouped",
|
|
84
|
-
"test:vitest": "vitest run",
|
|
85
|
-
"test:watch": "vitest",
|
|
86
|
-
"cli": "node dist/index.js",
|
|
87
|
-
"generate-changelog": "tsx scripts/generate-changelog.ts"
|
|
88
|
+
"zod": "^4.3.6"
|
|
88
89
|
}
|
|
89
|
-
}
|
|
90
|
+
}
|
package/skills/libretto/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: saffron-health
|
|
7
|
-
version: "0.6.
|
|
7
|
+
version: "0.6.29"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## How Libretto Works
|
|
@@ -51,7 +51,11 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
|
|
|
51
51
|
- Do not treat visibility as interactivity. If an element will not act, inspect blockers before retrying.
|
|
52
52
|
- Defer repo/code review until you begin generating code, unless the user explicitly asks for it earlier.
|
|
53
53
|
- Read and follow guidelines in `references/code-generation-rules.md` before generating or editing production workflow code.
|
|
54
|
-
-
|
|
54
|
+
- For authenticated workflows, manual login is discovery only. After the user logs in, read only the sign-in action logs and identify the required credentials; if credentials are unclear, ask before writing code.
|
|
55
|
+
- Add missing blank `LIBRETTO_CLOUD_<secret_name>=` entries without overwriting populated values. If any required credential is blank, stop and ask the user to fill it; until then, do not inspect logged-in pages, read authenticated network bodies, write workflow code, open validation sessions, or continue discovery.
|
|
56
|
+
- Authenticated workflows must implement `librettoAuthenticate` with declared credentials before validation. Use a reusable `*_totp_secret` credential for authenticator-app MFA, not a one-time `otp_code`; text and email verification codes are not supported for fully automated sign-in.
|
|
57
|
+
- Read `references/website-authentication.md` when you need `librettoAuthenticate` examples or auth-profile details.
|
|
58
|
+
- Validation requires a successful clean `run` on a fresh, unauthenticated session with confirmation of the actual returned output, not just process success. Use the same headed or headless mode that the workflow run is already using.
|
|
55
59
|
- After validation, always show the user: (1) the output/results from the validation run, and (2) the same command so they can re-run it themselves. Include any `--params`, `--headed`, or `--headless` flags the workflow needs.
|
|
56
60
|
- Treat exploration sessions as disposable unless the user explicitly wants one kept open.
|
|
57
61
|
- Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
|
|
@@ -232,13 +236,15 @@ Key fields: `id` (incrementing request id), `ts` (ISO timestamp), `pageId` (page
|
|
|
232
236
|
[Context: The user wants to build a new browser workflow and does not yet know the page structure]
|
|
233
237
|
Assistant: I'll inspect the real site first if needed, but before I finish I'll create `target-workflow.ts` so the task produces reusable automation code.
|
|
234
238
|
Assistant: [Runs `npx libretto open https://target.example.com --headed`]
|
|
239
|
+
Assistant: [If sign-in is required, follows the authenticated workflow rules before continuing]
|
|
235
240
|
Assistant: [Reads `references/site-security-review.md` before choosing between passive network inspection, direct browser fetch calls, and Playwright-first automation]
|
|
236
241
|
Assistant: [Runs `npx libretto snapshot --session <session>`]
|
|
237
242
|
Assistant: [Uses `snapshot` and `exec` as needed to understand the site and decide the implementation path]
|
|
238
243
|
Assistant: [Reads `references/code-generation-rules.md` before writing production workflow code]
|
|
239
244
|
Assistant: I found the working path. I'll now create the workflow file and verify it.
|
|
240
245
|
Assistant: [Creates or edits `target-workflow.ts` following `references/code-generation-rules.md`]
|
|
241
|
-
Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` to
|
|
246
|
+
Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` on a fresh, unauthenticated session to verify both sign-in and workflow behavior]
|
|
247
|
+
Assistant: [If sign-in validation passes and a reusable session is useful, adds an auth profile and reruns validation]
|
|
242
248
|
Assistant: Validation passed. Here are the results:
|
|
243
249
|
[Shows the output/results from the validation run]
|
|
244
250
|
To run it again, use: npx libretto run ./target-workflow.ts --params '{"status":"open"}'
|
|
@@ -270,7 +276,7 @@ To run it again, use: npx libretto run ./integration.ts
|
|
|
270
276
|
- Read `references/configuration-file-reference.md` when you need to inspect or change `.libretto/config.json` for viewport or session defaults.
|
|
271
277
|
- Read `references/site-security-review.md` before reviewing the site's security posture and deciding whether to lead with network requests, passive interception, or Playwright DOM automation on a new site.
|
|
272
278
|
- Read `references/code-generation-rules.md` before writing or editing production workflow files.
|
|
273
|
-
- Read `references/
|
|
279
|
+
- Read `references/website-authentication.md` when website sign-in implementation or auth-profile behavior is relevant.
|
|
274
280
|
- Read `references/pages-and-page-targeting.md` when a session has multiple open pages or you need `--page`.
|
|
275
281
|
- Read `references/action-logs.md` for full action log field descriptions and user-vs-agent event semantics.
|
|
276
282
|
- If the workflow code is deployed to the Libretto Cloud platform and you need to reference its API docs, fetch [https://libretto.sh/docs/llms.txt](https://libretto.sh/docs/llms.txt) and follow the relevant page links.
|
|
@@ -49,7 +49,7 @@ Key points:
|
|
|
49
49
|
- After validation is complete and the workflow is confirmed working end to end, remove all `pause()` calls and pause-only workflow params unless the user explicitly says to keep them.
|
|
50
50
|
- The browser is launched and closed automatically by the CLI. Do not launch or close it in the handler.
|
|
51
51
|
|
|
52
|
-
## Workflow Credentials And
|
|
52
|
+
## Workflow Credentials And Authentication
|
|
53
53
|
|
|
54
54
|
Declare `credentials` for runtime secrets instead of putting them in the Zod input schema or `--params`. Only declared names are injected into `input.credentials`; local runs read matching `LIBRETTO_CLOUD_` variables from `.env`, and hosted runs read matching Libretto Cloud credentials. For example, `LIBRETTO_CLOUD_OPENAI_API_KEY` becomes `input.credentials.openai_api_key`.
|
|
55
55
|
|
|
@@ -70,14 +70,7 @@ export default workflow("sentimentWorkflow", {
|
|
|
70
70
|
});
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
For
|
|
74
|
-
|
|
75
|
-
- `authProfile: { name, refresh: true }`
|
|
76
|
-
- `librettoAuthenticate(...)` fallback login logic using declared credentials such as `credentials: ["username", "password"]`
|
|
77
|
-
|
|
78
|
-
After generating or validating a credentialed workflow, include the local `.env` variable names the user must set, and never print secret values.
|
|
79
|
-
|
|
80
|
-
The fallback should validate the signed-in state, log in when validation fails, and validate again before continuing. Follow `references/auth-profiles.md` for profile naming, refresh behavior, hosted profile behavior, and the fallback-login pattern.
|
|
73
|
+
For website workflows that require signing in, build working sign-in logic with `librettoAuthenticate` and verify it from a clean signed-out browser before adding an auth profile. Follow `references/website-authentication.md`.
|
|
81
74
|
|
|
82
75
|
## Workflow Error Handling
|
|
83
76
|
|
|
@@ -43,7 +43,7 @@ General guidance: determine whether the site has bot protection and roughly how
|
|
|
43
43
|
|
|
44
44
|
### Probe 2: Fetch and XHR Interception
|
|
45
45
|
|
|
46
|
-
Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`.
|
|
46
|
+
Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`. Patching is a caution signal, not an automatic blocker. Browser-context network requests are usually fine when the target endpoint is already called by the site with fetch/XHR and there is no strong bot-protection evidence.
|
|
47
47
|
|
|
48
48
|
```js
|
|
49
49
|
window.fetch.toString()
|
|
@@ -52,7 +52,7 @@ Object.getOwnPropertyDescriptor(window, 'fetch')
|
|
|
52
52
|
window.fetch.hasOwnProperty('prototype')
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Important:
|
|
55
|
+
Important: ordinary app instrumentation and Libretto's own page-stability tracking can also wrap fetch/XHR. Treat browser fetch as risky only when the wrapper appears security-related, obfuscated, tied to bot-protection scripts, or likely to inspect call stacks. Some `Proxy` wrappers still stringify as `"[native code]"`, so these checks are only heuristics.
|
|
56
56
|
|
|
57
57
|
## Choosing a Data Capture Strategy
|
|
58
58
|
|
|
@@ -66,7 +66,7 @@ When to prioritize this:
|
|
|
66
66
|
|
|
67
67
|
- The target endpoint is normally called by the site with fetch/XHR
|
|
68
68
|
- No enterprise bot protection is detected
|
|
69
|
-
-
|
|
69
|
+
- No security-related fetch/XHR interception is detected, or the observed wrapper appears to be ordinary app instrumentation
|
|
70
70
|
- The API responses are parseable and useful
|
|
71
71
|
- You need data that requires many API calls (deep pagination, bulk queries) where driving the UI would be slow
|
|
72
72
|
|
|
@@ -83,7 +83,7 @@ Listen to network responses that the browser naturally makes as you navigate. Yo
|
|
|
83
83
|
When to prioritize this:
|
|
84
84
|
|
|
85
85
|
- Enterprise bot protection is detected
|
|
86
|
-
-
|
|
86
|
+
- Security-related fetch/XHR interception is detected
|
|
87
87
|
- The site's normal UI flow triggers API calls that return the data you need
|
|
88
88
|
- You want to minimize detection risk as much as possible
|
|
89
89
|
|
|
@@ -112,8 +112,9 @@ Trade-off: it is slower, more fragile against DOM changes, and you only get data
|
|
|
112
112
|
|
|
113
113
|
| Site Profile | Primary Strategy | Supplement With |
|
|
114
114
|
| --- | --- | --- |
|
|
115
|
-
| No bot protection, fetch/XHR endpoint,
|
|
116
|
-
| No bot protection,
|
|
115
|
+
| No bot protection, fetch/XHR endpoint, no security-related interception | A (`page.evaluate(fetch)`) | Playwright for navigation/auth |
|
|
116
|
+
| No bot protection, harmless app instrumentation, fetch/XHR endpoint | A (`page.evaluate(fetch)`) | B (`page.on('response', ...)`) if requests fail |
|
|
117
|
+
| No bot protection, security-related interception or endpoint is not fetch/XHR | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
|
|
117
118
|
| Bot protection detected | B (`page.on('response', ...)`) | Playwright for navigation; cautious use of `page.evaluate(fetch)` only if needed |
|
|
118
119
|
| Server-rendered content (no API calls) | C (DOM extraction) | Playwright for all interaction |
|
|
119
120
|
|