libretto 0.5.2 → 0.5.3-experimental.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/README.md +3 -2
- package/dist/cli/commands/deploy.js +162 -0
- package/dist/cli/commands/execution.js +38 -12
- package/dist/cli/framework/simple-cli.js +6 -0
- package/dist/cli/router.js +3 -1
- package/dist/cli/workers/run-integration-runtime.js +18 -41
- package/dist/cli/workers/run-integration-worker-protocol.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -0
- package/dist/shared/workflow/workflow.d.ts +14 -3
- package/dist/shared/workflow/workflow.js +50 -3
- package/package.json +7 -4
- package/scripts/check-skills-sync.mjs +1 -1
- package/scripts/generate-changelog.ts +132 -0
- package/scripts/skills-libretto.mjs +1 -1
- package/scripts/sync-skills.mjs +1 -1
- package/skills/libretto/SKILL.md +4 -2
- package/skills/libretto/references/code-generation-rules.md +6 -4
- package/src/cli/commands/deploy.ts +209 -0
- package/src/cli/commands/execution.ts +39 -11
- package/src/cli/framework/simple-cli.ts +9 -0
- package/src/cli/router.ts +2 -0
- package/src/cli/workers/run-integration-runtime.ts +24 -52
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
- package/src/index.ts +4 -0
- package/src/shared/workflow/workflow.ts +88 -2
- package/scripts/prepare-release.sh +0 -97
package/README.md
CHANGED
|
@@ -63,8 +63,9 @@ You can also use Libretto directly from the command line. All commands accept `-
|
|
|
63
63
|
npx libretto init # interactive; run yourself, not through an agent
|
|
64
64
|
npx libretto open <url> # launch browser and open a URL (headed by default)
|
|
65
65
|
npx libretto snapshot --objective "..." --context "..." # capture PNG + HTML and analyze with an LLM
|
|
66
|
-
npx libretto exec "<code>" # execute Playwright TypeScript against the open page
|
|
67
|
-
npx libretto
|
|
66
|
+
npx libretto exec "<code>" # execute Playwright TypeScript against the open page (single quoted argument)
|
|
67
|
+
echo "<code>" | npx libretto exec - # intentionally read Playwright TypeScript from stdin
|
|
68
|
+
npx libretto run <file> <workflowName> # run an exported workflow from a file
|
|
68
69
|
npx libretto resume # resume a paused workflow
|
|
69
70
|
npx libretto network # view captured network requests
|
|
70
71
|
npx libretto actions # view captured user/agent actions
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
11
|
+
function getConfig() {
|
|
12
|
+
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
13
|
+
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
14
|
+
if (!apiUrl) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"LIBRETTO_API_URL environment variable is required."
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"LIBRETTO_API_KEY environment variable is required."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return { apiUrl: apiUrl.replace(/\/$/, ""), apiKey };
|
|
25
|
+
}
|
|
26
|
+
async function postJson(apiUrl, apiKey, path, input = {}) {
|
|
27
|
+
return fetch(`${apiUrl}${path}`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"x-api-key": apiKey,
|
|
31
|
+
"Content-Type": "application/json"
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ json: input })
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function buildSourceTarball(sourceDir) {
|
|
37
|
+
const absSourceDir = resolve(sourceDir);
|
|
38
|
+
const pkgJsonPath = join(absSourceDir, "package.json");
|
|
39
|
+
try {
|
|
40
|
+
readFileSync(pkgJsonPath, "utf8");
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`No package.json found in ${absSourceDir}. Deploy source must contain a package.json.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const dir = join(tmpdir(), `libretto-deploy-${Date.now()}`);
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
cpSync(absSourceDir, dir, { recursive: true });
|
|
49
|
+
const tarPath = join(dir, "source.tar.gz");
|
|
50
|
+
execSync(
|
|
51
|
+
`tar czf "${tarPath}" --exclude=source.tar.gz --exclude=node_modules --exclude=.git -C "${dir}" .`
|
|
52
|
+
);
|
|
53
|
+
return readFileSync(tarPath).toString("base64");
|
|
54
|
+
}
|
|
55
|
+
async function pollDeployment(apiUrl, apiKey, deploymentId, pollIntervalMs, maxWaitMs) {
|
|
56
|
+
const start = Date.now();
|
|
57
|
+
let status = "building";
|
|
58
|
+
let deployment;
|
|
59
|
+
while (status === "building" && Date.now() - start < maxWaitMs) {
|
|
60
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
61
|
+
const res = await postJson(apiUrl, apiKey, "/v1/deployments/get", {
|
|
62
|
+
id: deploymentId
|
|
63
|
+
});
|
|
64
|
+
const body = await res.json();
|
|
65
|
+
if (res.status !== 200) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Failed to get deployment status (${res.status}): ${JSON.stringify(body)}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
status = body.json.status;
|
|
71
|
+
deployment = body.json;
|
|
72
|
+
process.stdout.write(".");
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
if (!deployment) {
|
|
76
|
+
throw new Error("Deployment timed out before receiving a status update.");
|
|
77
|
+
}
|
|
78
|
+
return deployment;
|
|
79
|
+
}
|
|
80
|
+
const deployInput = SimpleCLI.input({
|
|
81
|
+
positionals: [
|
|
82
|
+
SimpleCLI.positional("sourceDir", z.string().default("."), {
|
|
83
|
+
help: "Path to source directory (default: current directory)"
|
|
84
|
+
})
|
|
85
|
+
],
|
|
86
|
+
named: {
|
|
87
|
+
name: SimpleCLI.option(z.string(), {
|
|
88
|
+
help: "Deployment name"
|
|
89
|
+
}),
|
|
90
|
+
description: SimpleCLI.option(z.string().optional(), {
|
|
91
|
+
help: "Deployment description"
|
|
92
|
+
}),
|
|
93
|
+
entryPoint: SimpleCLI.option(z.string().optional(), {
|
|
94
|
+
name: "entry-point",
|
|
95
|
+
help: "Entry point file (default: index.ts)"
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const deployCommand = SimpleCLI.command({
|
|
100
|
+
description: "[experimental] Deploy workflows to the hosted platform",
|
|
101
|
+
experimental: true
|
|
102
|
+
}).input(deployInput).handle(async ({ input }) => {
|
|
103
|
+
const { apiUrl, apiKey } = getConfig();
|
|
104
|
+
console.log(`Packaging source from ${resolve(input.sourceDir)}...`);
|
|
105
|
+
const source = buildSourceTarball(input.sourceDir);
|
|
106
|
+
const createPayload = {
|
|
107
|
+
name: input.name,
|
|
108
|
+
source
|
|
109
|
+
};
|
|
110
|
+
if (input.description) createPayload.description = input.description;
|
|
111
|
+
if (input.entryPoint) createPayload.entry_point = input.entryPoint;
|
|
112
|
+
console.log("Uploading deployment...");
|
|
113
|
+
const res = await postJson(
|
|
114
|
+
apiUrl,
|
|
115
|
+
apiKey,
|
|
116
|
+
"/v1/deployments/create",
|
|
117
|
+
createPayload
|
|
118
|
+
);
|
|
119
|
+
const body = await res.json();
|
|
120
|
+
if (res.status !== 200) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Failed to create deployment (${res.status}): ${JSON.stringify(body)}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const { deployment_id, name, version, status } = body.json;
|
|
126
|
+
console.log(
|
|
127
|
+
`Deployment created: ${name} v${version} (${deployment_id})`
|
|
128
|
+
);
|
|
129
|
+
console.log(`Status: ${status}`);
|
|
130
|
+
if (status === "building") {
|
|
131
|
+
process.stdout.write("Waiting for build");
|
|
132
|
+
const deployment = await pollDeployment(
|
|
133
|
+
apiUrl,
|
|
134
|
+
apiKey,
|
|
135
|
+
deployment_id,
|
|
136
|
+
1e4,
|
|
137
|
+
5 * 60 * 1e3
|
|
138
|
+
);
|
|
139
|
+
if (deployment.status === "failed") {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Build failed: ${deployment.build_error ?? "unknown error"}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (deployment.status === "ready") {
|
|
145
|
+
console.log(`Build complete.`);
|
|
146
|
+
if (deployment.workflows?.length) {
|
|
147
|
+
console.log(
|
|
148
|
+
`Workflows: ${deployment.workflows.join(", ")}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
console.log(
|
|
153
|
+
`Build still in progress (timed out waiting). Check status with deployment ID: ${deployment_id}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return deployment_id;
|
|
158
|
+
});
|
|
159
|
+
export {
|
|
160
|
+
deployCommand,
|
|
161
|
+
deployInput
|
|
162
|
+
};
|
|
@@ -445,9 +445,10 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
445
445
|
);
|
|
446
446
|
const payload = JSON.stringify({
|
|
447
447
|
integrationPath: args.integrationPath,
|
|
448
|
-
|
|
448
|
+
workflowName: args.workflowName,
|
|
449
449
|
session: args.session,
|
|
450
450
|
params: args.params,
|
|
451
|
+
credentials: args.credentials,
|
|
451
452
|
headless: args.headless,
|
|
452
453
|
visualize: args.visualize,
|
|
453
454
|
authProfileDomain: args.authProfileDomain,
|
|
@@ -496,11 +497,19 @@ Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-ru
|
|
|
496
497
|
setSessionStatus(args.session, "completed", logger);
|
|
497
498
|
console.log("Integration completed.");
|
|
498
499
|
}
|
|
500
|
+
function readStdinSync() {
|
|
501
|
+
if (process.stdin.isTTY === true) return null;
|
|
502
|
+
try {
|
|
503
|
+
const content = readFileSync(0, "utf8");
|
|
504
|
+
return content.trim().length > 0 ? content : null;
|
|
505
|
+
} catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
499
509
|
const execInput = SimpleCLI.input({
|
|
500
510
|
positionals: [
|
|
501
|
-
SimpleCLI.positional("
|
|
502
|
-
help: "Playwright TypeScript code to execute"
|
|
503
|
-
variadic: true
|
|
511
|
+
SimpleCLI.positional("code", z.string().optional(), {
|
|
512
|
+
help: "Playwright TypeScript code to execute"
|
|
504
513
|
})
|
|
505
514
|
],
|
|
506
515
|
named: {
|
|
@@ -511,28 +520,36 @@ const execInput = SimpleCLI.input({
|
|
|
511
520
|
page: pageOption()
|
|
512
521
|
}
|
|
513
522
|
}).refine(
|
|
514
|
-
(input) => input.
|
|
515
|
-
`Usage: libretto exec <code
|
|
523
|
+
(input) => input.code !== void 0,
|
|
524
|
+
`Usage: libretto exec <code|-> [--session <name>] [--visualize]
|
|
525
|
+
echo '<code>' | libretto exec - [--session <name>] [--visualize]`
|
|
516
526
|
);
|
|
517
527
|
const execCommand = SimpleCLI.command({
|
|
518
528
|
description: "Execute Playwright TypeScript code"
|
|
519
529
|
}).input(execInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
530
|
+
const code = input.code;
|
|
531
|
+
const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
|
|
532
|
+
if (codeFromArgsOrStdin === null) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
"Missing stdin input for `exec -`. Pipe Playwright code into stdin."
|
|
535
|
+
);
|
|
536
|
+
}
|
|
520
537
|
await runExec(
|
|
521
|
-
|
|
538
|
+
codeFromArgsOrStdin,
|
|
522
539
|
ctx.session,
|
|
523
540
|
ctx.logger,
|
|
524
541
|
input.visualize,
|
|
525
542
|
input.page
|
|
526
543
|
);
|
|
527
544
|
});
|
|
528
|
-
const runUsage = `Usage: libretto run <integrationFile> <
|
|
545
|
+
const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--credentials <json>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
|
|
529
546
|
const runInput = SimpleCLI.input({
|
|
530
547
|
positionals: [
|
|
531
548
|
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
532
549
|
help: "Path to the integration file"
|
|
533
550
|
}),
|
|
534
|
-
SimpleCLI.positional("
|
|
535
|
-
help: "
|
|
551
|
+
SimpleCLI.positional("workflowName", z.string().optional(), {
|
|
552
|
+
help: "Workflow name to run (from workflow(name, handler))"
|
|
536
553
|
})
|
|
537
554
|
],
|
|
538
555
|
named: {
|
|
@@ -544,6 +561,9 @@ const runInput = SimpleCLI.input({
|
|
|
544
561
|
name: "params-file",
|
|
545
562
|
help: "Path to a JSON params file"
|
|
546
563
|
}),
|
|
564
|
+
credentials: SimpleCLI.option(z.string().optional(), {
|
|
565
|
+
help: "Inline JSON credentials passed to ctx.credentials"
|
|
566
|
+
}),
|
|
547
567
|
tsconfig: SimpleCLI.option(z.string().optional(), {
|
|
548
568
|
help: "Path to a tsconfig used for workflow module resolution"
|
|
549
569
|
}),
|
|
@@ -562,7 +582,7 @@ const runInput = SimpleCLI.input({
|
|
|
562
582
|
})
|
|
563
583
|
}
|
|
564
584
|
}).refine(
|
|
565
|
-
(input) => Boolean(input.integrationFile && input.
|
|
585
|
+
(input) => Boolean(input.integrationFile && input.workflowName),
|
|
566
586
|
runUsage
|
|
567
587
|
).refine(
|
|
568
588
|
(input) => !(input.params && input.paramsFile),
|
|
@@ -594,6 +614,11 @@ const runCommand = SimpleCLI.command({
|
|
|
594
614
|
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
595
615
|
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
596
616
|
const params = resolveRunParams(input.params, input.paramsFile);
|
|
617
|
+
const rawCredentials = input.credentials ? parseJsonArg("--credentials", input.credentials) : void 0;
|
|
618
|
+
if (rawCredentials !== void 0 && (typeof rawCredentials !== "object" || rawCredentials === null || Array.isArray(rawCredentials))) {
|
|
619
|
+
throw new Error(`--credentials must be a JSON object (e.g., '{"key": "value"}').`);
|
|
620
|
+
}
|
|
621
|
+
const credentials = rawCredentials;
|
|
597
622
|
const headlessMode = input.headed ? false : input.headless ? true : void 0;
|
|
598
623
|
const visualize = !input.noVisualize;
|
|
599
624
|
const viewport = resolveViewport(
|
|
@@ -603,9 +628,10 @@ const runCommand = SimpleCLI.command({
|
|
|
603
628
|
await runIntegrationFromFile(
|
|
604
629
|
{
|
|
605
630
|
integrationPath: input.integrationFile,
|
|
606
|
-
|
|
631
|
+
workflowName: input.workflowName,
|
|
607
632
|
session: ctx.session,
|
|
608
633
|
params,
|
|
634
|
+
credentials,
|
|
609
635
|
tsconfigPath: input.tsconfig,
|
|
610
636
|
headless: headlessMode ?? false,
|
|
611
637
|
visualize,
|
|
@@ -270,6 +270,10 @@ class SimpleCLIApp {
|
|
|
270
270
|
named["--"] = args.slice(index + 1);
|
|
271
271
|
break;
|
|
272
272
|
}
|
|
273
|
+
if (arg === "-") {
|
|
274
|
+
positionals.push(arg);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
273
277
|
if (arg.startsWith("--")) {
|
|
274
278
|
const [rawName, inlineValue] = splitNamedArg(arg.slice(2));
|
|
275
279
|
const namedEntry = namedSpecs.get(rawName);
|
|
@@ -489,6 +493,7 @@ class SimpleCLIApp {
|
|
|
489
493
|
continue;
|
|
490
494
|
}
|
|
491
495
|
const command2 = this.findCommandByPath(routeEntry.path);
|
|
496
|
+
if (command2?.experimental) continue;
|
|
492
497
|
entries.push({
|
|
493
498
|
label: token,
|
|
494
499
|
description: command2?.description
|
|
@@ -633,6 +638,7 @@ function resolveRouteTree(routes, parentPath = [], parentMiddlewares = []) {
|
|
|
633
638
|
routeKey: pathToRouteKey(path),
|
|
634
639
|
path,
|
|
635
640
|
description: command2.config.description,
|
|
641
|
+
experimental: command2.config.experimental,
|
|
636
642
|
input: command2.input,
|
|
637
643
|
middlewares: mergeInheritedMiddlewares(
|
|
638
644
|
parentMiddlewares,
|
package/dist/cli/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { aiCommands } from "./commands/ai.js";
|
|
2
2
|
import { browserCommands } from "./commands/browser.js";
|
|
3
|
+
import { deployCommand } from "./commands/deploy.js";
|
|
3
4
|
import { executionCommands } from "./commands/execution.js";
|
|
4
5
|
import { initCommand } from "./commands/init.js";
|
|
5
6
|
import { logCommands } from "./commands/logs.js";
|
|
@@ -11,7 +12,8 @@ const cliRoutes = {
|
|
|
11
12
|
...logCommands,
|
|
12
13
|
ai: aiCommands,
|
|
13
14
|
init: initCommand,
|
|
14
|
-
snapshot: snapshotCommand
|
|
15
|
+
snapshot: snapshotCommand,
|
|
16
|
+
deploy: deployCommand
|
|
15
17
|
};
|
|
16
18
|
function createCLIApp() {
|
|
17
19
|
return SimpleCLI.define("libretto", cliRoutes);
|
|
@@ -4,6 +4,8 @@ import { cwd } from "node:process";
|
|
|
4
4
|
import { isAbsolute, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import {
|
|
7
|
+
getWorkflowFromModuleExports,
|
|
8
|
+
getWorkflowsFromModuleExports,
|
|
7
9
|
instrumentContext,
|
|
8
10
|
launchBrowser
|
|
9
11
|
} from "../../index.js";
|
|
@@ -19,7 +21,6 @@ import {
|
|
|
19
21
|
removeSignalIfExists
|
|
20
22
|
} from "../core/pause-signals.js";
|
|
21
23
|
import { installSessionTelemetry } from "../core/session-telemetry.js";
|
|
22
|
-
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
23
24
|
const FAILURE_HOLD_POLL_INTERVAL_MS = 250;
|
|
24
25
|
const TSCONFIG_HINT = "TypeScript compilation failed. Pass --tsconfig <path> to run against a specific tsconfig.";
|
|
25
26
|
function isTsxCompileError(error) {
|
|
@@ -67,11 +68,6 @@ async function waitForFailureSessionRelease(args) {
|
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
function isLoadedLibrettoWorkflow(value) {
|
|
71
|
-
if (!value || typeof value !== "object") return false;
|
|
72
|
-
const candidate = value;
|
|
73
|
-
return candidate[LIBRETTO_WORKFLOW_BRAND] === true && typeof candidate.run === "function";
|
|
74
|
-
}
|
|
75
71
|
function resolveLocalAuthProfilePath(domain) {
|
|
76
72
|
return getProfilePath(normalizeDomain(domain));
|
|
77
73
|
}
|
|
@@ -93,7 +89,7 @@ function getAbsoluteIntegrationPath(integrationPath) {
|
|
|
93
89
|
}
|
|
94
90
|
return absolutePath;
|
|
95
91
|
}
|
|
96
|
-
async function
|
|
92
|
+
async function loadWorkflowByName(absolutePath, workflowName) {
|
|
97
93
|
let loadedModule;
|
|
98
94
|
try {
|
|
99
95
|
loadedModule = await import(pathToFileURL(absolutePath).href);
|
|
@@ -105,37 +101,17 @@ ${TSCONFIG_HINT}` : "";
|
|
|
105
101
|
`Failed to import integration module at ${absolutePath}: ${message}${compileHint}`
|
|
106
102
|
);
|
|
107
103
|
}
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
const detail = availableExports.length > 0 ? ` Available exports: ${availableExports.join(", ")}` : " The module has no exports.";
|
|
112
|
-
throw new Error(
|
|
113
|
-
`Export "${exportName}" was not found in ${absolutePath}.${detail}`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
if (!isLoadedLibrettoWorkflow(targetExport)) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
[
|
|
119
|
-
`Export "${exportName}" in ${absolutePath} is not a valid Libretto workflow.`,
|
|
120
|
-
"",
|
|
121
|
-
'A workflow must be created using the workflow() function from "libretto":',
|
|
122
|
-
"",
|
|
123
|
-
' import { workflow } from "libretto";',
|
|
124
|
-
"",
|
|
125
|
-
` export const ${exportName} = workflow<InputType, OutputType>(`,
|
|
126
|
-
" async (ctx, input) => {",
|
|
127
|
-
" // ctx.session \u2014 libretto session name",
|
|
128
|
-
" // ctx.page \u2014 Playwright Page instance",
|
|
129
|
-
" // ctx.logger \u2014 MinimalLogger",
|
|
130
|
-
" // ctx.services \u2014 injected dependencies (generic, default {})",
|
|
131
|
-
" // input \u2014 JSON-serializable input matching InputType",
|
|
132
|
-
" return output; // must match OutputType",
|
|
133
|
-
" },",
|
|
134
|
-
" );"
|
|
135
|
-
].join("\n")
|
|
136
|
-
);
|
|
104
|
+
const workflow = getWorkflowFromModuleExports(loadedModule, workflowName);
|
|
105
|
+
if (workflow) {
|
|
106
|
+
return workflow;
|
|
137
107
|
}
|
|
138
|
-
|
|
108
|
+
const availableWorkflows = getWorkflowsFromModuleExports(loadedModule).map(
|
|
109
|
+
(candidate) => candidate.name
|
|
110
|
+
);
|
|
111
|
+
const detail = availableWorkflows.length > 0 ? ` Available workflows: ${availableWorkflows.join(", ")}` : ' No workflows found in this file. Export a workflow() instance from "libretto" directly or via `export const workflows = { ... }`.';
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Workflow "${workflowName}" not found in ${absolutePath}.${detail}`
|
|
114
|
+
);
|
|
139
115
|
}
|
|
140
116
|
async function installHeadedWorkflowVisualization(args) {
|
|
141
117
|
await (args.instrument ?? instrumentContext)(args.context, {
|
|
@@ -146,7 +122,7 @@ async function installHeadedWorkflowVisualization(args) {
|
|
|
146
122
|
async function runIntegrationInternal(args, options) {
|
|
147
123
|
const { logger } = options;
|
|
148
124
|
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
149
|
-
const workflow = await
|
|
125
|
+
const workflow = await loadWorkflowByName(absolutePath, args.workflowName);
|
|
150
126
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
151
127
|
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
152
128
|
await removeSignalIfExists(signalPaths.resumeSignalPath);
|
|
@@ -154,11 +130,11 @@ async function runIntegrationInternal(args, options) {
|
|
|
154
130
|
await removeSignalIfExists(signalPaths.failedSignalPath);
|
|
155
131
|
const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
|
|
156
132
|
console.log(
|
|
157
|
-
`Running
|
|
133
|
+
`Running workflow "${args.workflowName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`
|
|
158
134
|
);
|
|
159
135
|
const integrationLogger = logger.withScope("integration-run", {
|
|
160
136
|
integrationPath: absolutePath,
|
|
161
|
-
|
|
137
|
+
workflowName: args.workflowName,
|
|
162
138
|
session: args.session
|
|
163
139
|
});
|
|
164
140
|
const authProfileDomain = args.authProfileDomain;
|
|
@@ -201,7 +177,8 @@ async function runIntegrationInternal(args, options) {
|
|
|
201
177
|
session: args.session,
|
|
202
178
|
logger: integrationLogger,
|
|
203
179
|
page: browserSession.page,
|
|
204
|
-
services: {}
|
|
180
|
+
services: {},
|
|
181
|
+
credentials: args.credentials
|
|
205
182
|
};
|
|
206
183
|
try {
|
|
207
184
|
try {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
const RunIntegrationWorkerRequestSchema = z.object({
|
|
3
3
|
integrationPath: z.string().min(1),
|
|
4
|
-
|
|
4
|
+
workflowName: z.string().min(1),
|
|
5
5
|
session: z.string().min(1),
|
|
6
6
|
params: z.unknown(),
|
|
7
|
+
credentials: z.record(z.string(), z.unknown()).optional(),
|
|
7
8
|
headless: z.boolean(),
|
|
8
9
|
visualize: z.boolean().default(true),
|
|
9
10
|
authProfileDomain: z.string().optional(),
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { InstrumentationOptions, InstrumentedPage, installInstrumentation, instr
|
|
|
14
14
|
export { GhostCursorOptions, ensureGhostCursor, ghostClick, hideGhostCursor, moveGhostCursor } from './shared/visualization/ghost-cursor.js';
|
|
15
15
|
export { HighlightOptions, clearHighlights, ensureHighlightLayer, showHighlight } from './shared/visualization/highlight.js';
|
|
16
16
|
export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './shared/run/browser.js';
|
|
17
|
-
export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, workflow } from './shared/workflow/workflow.js';
|
|
17
|
+
export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow } from './shared/workflow/workflow.js';
|
|
18
18
|
import 'zod';
|
|
19
19
|
import 'ai';
|
|
20
20
|
import 'playwright';
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,9 @@ import {
|
|
|
54
54
|
launchBrowser
|
|
55
55
|
} from "./shared/run/api.js";
|
|
56
56
|
import {
|
|
57
|
+
getWorkflowFromModuleExports,
|
|
58
|
+
getWorkflowsFromModuleExports,
|
|
59
|
+
isLibrettoWorkflow,
|
|
57
60
|
LibrettoWorkflow,
|
|
58
61
|
LIBRETTO_WORKFLOW_BRAND,
|
|
59
62
|
workflow
|
|
@@ -92,11 +95,14 @@ export {
|
|
|
92
95
|
ensureHighlightLayer,
|
|
93
96
|
executeRecoveryAgent,
|
|
94
97
|
extractFromPage,
|
|
98
|
+
getWorkflowFromModuleExports,
|
|
99
|
+
getWorkflowsFromModuleExports,
|
|
95
100
|
ghostClick,
|
|
96
101
|
hideGhostCursor,
|
|
97
102
|
installInstrumentation,
|
|
98
103
|
instrumentContext,
|
|
99
104
|
instrumentPage,
|
|
105
|
+
isLibrettoWorkflow,
|
|
100
106
|
jsonlConsoleSink,
|
|
101
107
|
launchBrowser,
|
|
102
108
|
moveGhostCursor,
|
|
@@ -7,14 +7,25 @@ type LibrettoWorkflowContext<S = {}> = {
|
|
|
7
7
|
page: Page;
|
|
8
8
|
logger: MinimalLogger;
|
|
9
9
|
services: S;
|
|
10
|
+
credentials?: Record<string, unknown>;
|
|
10
11
|
};
|
|
11
12
|
type LibrettoWorkflowHandler<Input = unknown, Output = unknown, S = {}> = (ctx: LibrettoWorkflowContext<S>, input: Input) => Promise<Output>;
|
|
12
13
|
declare class LibrettoWorkflow<Input = unknown, Output = unknown, S = {}> {
|
|
13
14
|
readonly [LIBRETTO_WORKFLOW_BRAND] = true;
|
|
15
|
+
readonly name: string;
|
|
14
16
|
private readonly handler;
|
|
15
|
-
constructor(handler: LibrettoWorkflowHandler<Input, Output, S>);
|
|
17
|
+
constructor(name: string, handler: LibrettoWorkflowHandler<Input, Output, S>);
|
|
16
18
|
run(ctx: LibrettoWorkflowContext<S>, input: Input): Promise<Output>;
|
|
17
19
|
}
|
|
18
|
-
|
|
20
|
+
type ExportedLibrettoWorkflow = {
|
|
21
|
+
readonly [LIBRETTO_WORKFLOW_BRAND]: true;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
type WorkflowModuleExports = Record<string, unknown>;
|
|
26
|
+
declare function isLibrettoWorkflow(value: unknown): value is ExportedLibrettoWorkflow;
|
|
27
|
+
declare function getWorkflowsFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow[];
|
|
28
|
+
declare function getWorkflowFromModuleExports(moduleExports: WorkflowModuleExports, workflowName: string): ExportedLibrettoWorkflow | null;
|
|
29
|
+
declare function workflow<Input = unknown, Output = unknown, S = {}>(name: string, handler: LibrettoWorkflowHandler<Input, Output, S>): LibrettoWorkflow<Input, Output, S>;
|
|
19
30
|
|
|
20
|
-
export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, workflow };
|
|
31
|
+
export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow };
|
|
@@ -1,19 +1,66 @@
|
|
|
1
1
|
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
2
2
|
class LibrettoWorkflow {
|
|
3
3
|
[LIBRETTO_WORKFLOW_BRAND] = true;
|
|
4
|
+
name;
|
|
4
5
|
handler;
|
|
5
|
-
constructor(handler) {
|
|
6
|
+
constructor(name, handler) {
|
|
7
|
+
this.name = name;
|
|
6
8
|
this.handler = handler;
|
|
7
9
|
}
|
|
8
10
|
async run(ctx, input) {
|
|
9
11
|
return this.handler(ctx, input);
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
+
function isLibrettoWorkflow(value) {
|
|
15
|
+
if (!value || typeof value !== "object") return false;
|
|
16
|
+
const candidate = value;
|
|
17
|
+
return candidate[LIBRETTO_WORKFLOW_BRAND] === true && typeof candidate.name === "string" && typeof candidate.run === "function";
|
|
18
|
+
}
|
|
19
|
+
function addWorkflowOrThrow(workflowsByName, value) {
|
|
20
|
+
if (!isLibrettoWorkflow(value)) return;
|
|
21
|
+
const existing = workflowsByName.get(value.name);
|
|
22
|
+
if (existing && existing !== value) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Duplicate workflow name: "${value.name}". Each workflow() call must use a unique name.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
workflowsByName.set(value.name, value);
|
|
28
|
+
}
|
|
29
|
+
function getWorkflowsFromModuleExports(moduleExports) {
|
|
30
|
+
const workflowsByName = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const [exportName, value] of Object.entries(moduleExports)) {
|
|
32
|
+
if (exportName === "workflows" && value && typeof value === "object") {
|
|
33
|
+
if (isLibrettoWorkflow(value)) {
|
|
34
|
+
addWorkflowOrThrow(workflowsByName, value);
|
|
35
|
+
} else {
|
|
36
|
+
for (const nestedValue of Object.values(
|
|
37
|
+
value
|
|
38
|
+
)) {
|
|
39
|
+
addWorkflowOrThrow(workflowsByName, nestedValue);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
addWorkflowOrThrow(workflowsByName, value);
|
|
45
|
+
}
|
|
46
|
+
return [...workflowsByName.values()];
|
|
47
|
+
}
|
|
48
|
+
function getWorkflowFromModuleExports(moduleExports, workflowName) {
|
|
49
|
+
for (const workflow2 of getWorkflowsFromModuleExports(moduleExports)) {
|
|
50
|
+
if (workflow2.name === workflowName) {
|
|
51
|
+
return workflow2;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function workflow(name, handler) {
|
|
57
|
+
return new LibrettoWorkflow(name, handler);
|
|
14
58
|
}
|
|
15
59
|
export {
|
|
16
60
|
LIBRETTO_WORKFLOW_BRAND,
|
|
17
61
|
LibrettoWorkflow,
|
|
62
|
+
getWorkflowFromModuleExports,
|
|
63
|
+
getWorkflowsFromModuleExports,
|
|
64
|
+
isLibrettoWorkflow,
|
|
18
65
|
workflow
|
|
19
66
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3-experimental.1",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,11 +36,9 @@
|
|
|
36
36
|
"build": "tsup --config tsup.config.ts",
|
|
37
37
|
"type-check": "tsc --noEmit",
|
|
38
38
|
"test": "pnpm run build && vitest run",
|
|
39
|
-
"eval": "pnpm run build && vitest run --config vitest.evals.config.ts",
|
|
40
|
-
"benchmark": "pnpm run build && tsx benchmarks/run.ts",
|
|
41
39
|
"test:watch": "vitest",
|
|
42
40
|
"cli": "node dist/index.js",
|
|
43
|
-
"
|
|
41
|
+
"generate-changelog": "tsx scripts/generate-changelog.ts",
|
|
44
42
|
"prepack": "pnpm run build"
|
|
45
43
|
},
|
|
46
44
|
"peerDependencies": {
|
|
@@ -69,8 +67,13 @@
|
|
|
69
67
|
"@ai-sdk/google-vertex": "^4.0.80",
|
|
70
68
|
"@ai-sdk/openai": "^3.0.41",
|
|
71
69
|
"@anthropic-ai/claude-agent-sdk": "^0.2.75",
|
|
70
|
+
"@mariozechner/pi-agent-core": "^0.62.0",
|
|
71
|
+
"@mariozechner/pi-ai": "^0.62.0",
|
|
72
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
73
|
+
"@sinclair/typebox": "^0.34.48",
|
|
72
74
|
"@types/node": "^25.5.0",
|
|
73
75
|
"glimpseui": "^0.5.1",
|
|
76
|
+
"google-auth-library": "^10.6.1",
|
|
74
77
|
"openai": "^6.29.0",
|
|
75
78
|
"tsup": "^8.5.1",
|
|
76
79
|
"typescript": "^5.9.3",
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { compareSkillDirs, SKILL_DIRS } from "./skills-libretto.mjs";
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const repoRoot = join(__dirname, "..");
|
|
9
|
+
const repoRoot = join(__dirname, "..", "..", "..");
|
|
10
10
|
const result = compareSkillDirs(repoRoot);
|
|
11
11
|
|
|
12
12
|
if (result.ok) {
|