libretto 0.5.3-experimental.6 → 0.5.3
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 +7 -2
- package/README.template.md +160 -0
- package/dist/cli/commands/deploy.js +1 -1
- package/dist/cli/commands/execution.js +1 -11
- package/dist/cli/framework/simple-cli.js +96 -21
- package/dist/cli/router.js +2 -2
- package/dist/cli/workers/run-integration-runtime.js +1 -2
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -1
- package/dist/shared/workflow/workflow.d.ts +0 -1
- package/package.json +5 -3
- package/scripts/skills-libretto.mjs +1 -89
- package/skills/libretto/SKILL.md +1 -1
- package/skills/libretto/references/code-generation-rules.md +1 -1
- package/src/cli/commands/deploy.ts +1 -1
- package/src/cli/commands/execution.ts +1 -13
- package/src/cli/framework/simple-cli.ts +130 -23
- package/src/cli/router.ts +1 -1
- package/src/cli/workers/run-integration-runtime.ts +0 -1
- package/src/cli/workers/run-integration-worker-protocol.ts +0 -1
- package/src/shared/workflow/workflow.ts +0 -1
- package/scripts/check-skills-sync.mjs +0 -25
- package/scripts/sync-skills.mjs +0 -12
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- Generated from packages/libretto/README.template.md by `pnpm sync:mirrors`. Do not edit directly. -->
|
|
2
|
+
|
|
1
3
|
# Libretto
|
|
2
4
|
|
|
3
5
|
[](https://www.npmjs.com/package/libretto)
|
|
@@ -152,6 +154,9 @@ Source layout:
|
|
|
152
154
|
- `src/runtime/` — browser runtime (network, recovery, downloads, extraction)
|
|
153
155
|
- `src/shared/` — shared utilities (config, LLM client, logging, state)
|
|
154
156
|
- `test/` — test files (`*.spec.ts`)
|
|
155
|
-
- `
|
|
157
|
+
- `README.template.md` — source of truth for the repo and package READMEs
|
|
158
|
+
- `skills/libretto/` — source of truth for the Libretto skill
|
|
159
|
+
|
|
160
|
+
Run `pnpm sync:mirrors` after editing `README.template.md` or anything under `skills/libretto/`. `pnpm i` also resyncs the skill mirrors through `postinstall`.
|
|
156
161
|
|
|
157
|
-
To check that skill mirrors are in sync without fixing them, run `pnpm check:
|
|
162
|
+
To check that generated READMEs, skill mirrors, and skill version metadata are in sync without fixing them, run `pnpm check:mirrors`. To release, run `pnpm prepare-release`.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Libretto
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/libretto)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/saffron-health/libretto/discussions)
|
|
6
|
+
|
|
7
|
+
Libretto is a toolkit for building robust web integrations. It gives your coding agent a live browser and a token-efficient CLI to:
|
|
8
|
+
|
|
9
|
+
- Inspect live pages with minimal context overhead
|
|
10
|
+
- Capture network traffic to reverse-engineer site APIs
|
|
11
|
+
- Record user actions and replay them as automation scripts
|
|
12
|
+
- Debug broken workflows interactively against the real site
|
|
13
|
+
|
|
14
|
+
We at [Saffron Health](https://saffron.health) built Libretto to help us maintain our browser integrations to common healthcare software. We're open-sourcing it so other teams have an easier time doing the same thing.
|
|
15
|
+
|
|
16
|
+
https://github.com/user-attachments/assets/9b9a0ab3-5133-4b20-b3be-459943349d18
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install libretto
|
|
22
|
+
|
|
23
|
+
# Install skill, download Chromium if not already installed, configure snapshot analysis
|
|
24
|
+
npx libretto init
|
|
25
|
+
|
|
26
|
+
# Configure or change the snapshot analysis model (see Configuration section below). `npx libretto init` sets this up the first time.
|
|
27
|
+
npx libretto ai configure <openai | anthropic | gemini | vertex>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Use cases
|
|
31
|
+
|
|
32
|
+
Libretto is designed to be used as a skill through your coding agent. Here are some example prompts:
|
|
33
|
+
|
|
34
|
+
### One-shot script generation
|
|
35
|
+
|
|
36
|
+
> Use the Libretto skill. Go on LinkedIn and scrape the first 10 posts for content, who posted it, the number of reactions, the first 25 comments, and the first 25 reposts.
|
|
37
|
+
|
|
38
|
+
Your coding agent will open a window for you to log into LinkedIn, and then automatically start exploring.
|
|
39
|
+
|
|
40
|
+
### Interactive script building
|
|
41
|
+
|
|
42
|
+
> I'm gonna show you a workflow in the eclinicalworks EHR to get a patient's primary insurance ID. Use libretto skill to turn it into a playwright script that takes patient name and dob as input to get back the insurance ID. URL is ...
|
|
43
|
+
|
|
44
|
+
Libretto can read your actions you perform in the browser, so you can perform a workflow, then ask it to use your actions to rebuild the workflow.
|
|
45
|
+
|
|
46
|
+
### Convert browser automation to network requests
|
|
47
|
+
|
|
48
|
+
> We have a browser script at ./integration.ts that automates going to Hacker News and getting the first 10 posts. Convert it to direct network scripts instead. Use the Libretto skill.
|
|
49
|
+
|
|
50
|
+
Libretto can read network requests from the browser, which it can use to reverse engineer the API and create a script that directly calls those requests. Directly making API calls is faster, and more reliable, than UI automation. You can also ask Libretto to conduct a security analysis which analyzes the requests for common security cookies, so you can understand whether a network request approach will be safe.
|
|
51
|
+
|
|
52
|
+
### Fix broken integrations
|
|
53
|
+
|
|
54
|
+
> We have a browser script at ./integration.ts that is supposed to go to Availity and perform an eligibility check for a patient. But I'm getting a broken selector error when I run it. Fix it. Use the Libretto skill.
|
|
55
|
+
|
|
56
|
+
Agents can use Libretto to reproduce the failure, pause the workflow at any point, inspect the live page, and fix issues, all autonomously.
|
|
57
|
+
|
|
58
|
+
### CLI usage
|
|
59
|
+
|
|
60
|
+
You can also use Libretto directly from the command line. All commands accept `--session <name>` to target a specific session.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx libretto init # interactive; run yourself, not through an agent
|
|
64
|
+
npx libretto open <url> # launch browser and open a URL (headed by default)
|
|
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 (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
|
|
69
|
+
npx libretto resume # resume a paused workflow
|
|
70
|
+
npx libretto network # view captured network requests
|
|
71
|
+
npx libretto actions # view captured user/agent actions
|
|
72
|
+
npx libretto pages # list open pages in the session
|
|
73
|
+
npx libretto save <domain> # save browser session (cookies, localStorage) for reuse
|
|
74
|
+
npx libretto close # close the browser
|
|
75
|
+
npx libretto ai configure <provider> # configure snapshot analysis model
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
All Libretto state lives in a `.libretto/` directory at your project root. Configuration is stored in `.libretto/config.json`.
|
|
81
|
+
|
|
82
|
+
### Config file
|
|
83
|
+
|
|
84
|
+
`.libretto/config.json` controls snapshot analysis and viewport settings:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"version": 1,
|
|
89
|
+
"ai": {
|
|
90
|
+
"model": "openai/gpt-5.4",
|
|
91
|
+
"updatedAt": "2026-01-01T00:00:00.000Z"
|
|
92
|
+
},
|
|
93
|
+
"viewport": { "width": 1280, "height": 800 }
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `ai` field configures which model Libretto uses for snapshot analysis — extracting selectors, identifying interactive elements, or diagnosing why a step failed. This keeps heavy visual context out of your coding agent's context window. Snapshot analysis is required.
|
|
98
|
+
|
|
99
|
+
The easiest way to set the model is through the CLI:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx libretto ai configure <openai | anthropic | gemini | vertex>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Provider credentials are read from environment variables or a `.env` file at your project root: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` / `GOOGLE_GENERATIVE_AI_API_KEY`, or `GOOGLE_CLOUD_PROJECT` for Vertex.
|
|
106
|
+
|
|
107
|
+
The `viewport` field sets the default browser viewport size. Both fields are optional.
|
|
108
|
+
|
|
109
|
+
### Sessions
|
|
110
|
+
|
|
111
|
+
Each Libretto session gets its own directory under `.libretto/sessions/<name>/` containing runtime state. Sessions are git-ignored.
|
|
112
|
+
|
|
113
|
+
- `state.json` — session metadata (debug port, PID, status)
|
|
114
|
+
- `logs.jsonl` — structured session logs
|
|
115
|
+
- `network.jsonl` — captured network requests
|
|
116
|
+
- `actions.jsonl` — recorded user actions
|
|
117
|
+
- `snapshots/` — screenshot PNGs and HTML snapshots
|
|
118
|
+
|
|
119
|
+
### Profiles
|
|
120
|
+
|
|
121
|
+
Profiles save browser sessions (cookies, localStorage) so you can reuse authenticated state across runs. They are stored in `.libretto/profiles/<domain>.json`, created via `npx libretto save <domain>`. Profiles are machine-local and git-ignored.
|
|
122
|
+
|
|
123
|
+
## Community
|
|
124
|
+
|
|
125
|
+
Have a question, idea, or want to share what you've built? Join the conversation on [GitHub Discussions](https://github.com/saffron-health/libretto/discussions).
|
|
126
|
+
|
|
127
|
+
- **[Q&A](https://github.com/saffron-health/libretto/discussions/categories/q-a)** — Ask questions and get help
|
|
128
|
+
- **[Ideas](https://github.com/saffron-health/libretto/discussions/categories/ideas)** — Suggest new features or improvements
|
|
129
|
+
- **[Show and tell](https://github.com/saffron-health/libretto/discussions/categories/show-and-tell)** — Share your workflows and automations
|
|
130
|
+
- **[General](https://github.com/saffron-health/libretto/discussions/categories/general)** — Chat about anything Libretto-related
|
|
131
|
+
|
|
132
|
+
Found a bug? Please [open an issue](https://github.com/saffron-health/libretto/issues/new).
|
|
133
|
+
|
|
134
|
+
## Authors
|
|
135
|
+
|
|
136
|
+
Maintained by the team at [Saffron Health](https://saffron.health).
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
For local development in this repository:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pnpm i
|
|
144
|
+
pnpm build
|
|
145
|
+
pnpm type-check
|
|
146
|
+
pnpm test
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Source layout:
|
|
150
|
+
|
|
151
|
+
- `{{LIBRETTO_PATH_PREFIX}}src/cli/` — CLI commands
|
|
152
|
+
- `{{LIBRETTO_PATH_PREFIX}}src/runtime/` — browser runtime (network, recovery, downloads, extraction)
|
|
153
|
+
- `{{LIBRETTO_PATH_PREFIX}}src/shared/` — shared utilities (config, LLM client, logging, state)
|
|
154
|
+
- `{{LIBRETTO_PATH_PREFIX}}test/` — test files (`*.spec.ts`)
|
|
155
|
+
- `{{LIBRETTO_PATH_PREFIX}}README.template.md` — source of truth for the repo and package READMEs
|
|
156
|
+
- `{{LIBRETTO_PATH_PREFIX}}skills/libretto/` — source of truth for the Libretto skill
|
|
157
|
+
|
|
158
|
+
Run `pnpm sync:mirrors` after editing `{{LIBRETTO_PATH_PREFIX}}README.template.md` or anything under `{{LIBRETTO_PATH_PREFIX}}skills/libretto/`. `pnpm i` also resyncs the skill mirrors through `postinstall`.
|
|
159
|
+
|
|
160
|
+
To check that generated READMEs, skill mirrors, and skill version metadata are in sync without fixing them, run `pnpm check:mirrors`. To release, run `pnpm prepare-release`.
|
|
@@ -80,7 +80,7 @@ const deployInput = SimpleCLI.input({
|
|
|
80
80
|
}
|
|
81
81
|
});
|
|
82
82
|
const deployCommand = SimpleCLI.command({
|
|
83
|
-
description: "
|
|
83
|
+
description: "Deploy workflows to the hosted platform",
|
|
84
84
|
experimental: true
|
|
85
85
|
}).input(deployInput).handle(async ({ input }) => {
|
|
86
86
|
const { apiUrl, apiKey } = getConfig();
|
|
@@ -448,7 +448,6 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
448
448
|
workflowName: args.workflowName,
|
|
449
449
|
session: args.session,
|
|
450
450
|
params: args.params,
|
|
451
|
-
credentials: args.credentials,
|
|
452
451
|
headless: args.headless,
|
|
453
452
|
visualize: args.visualize,
|
|
454
453
|
authProfileDomain: args.authProfileDomain,
|
|
@@ -542,7 +541,7 @@ const execCommand = SimpleCLI.command({
|
|
|
542
541
|
input.page
|
|
543
542
|
);
|
|
544
543
|
});
|
|
545
|
-
const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--
|
|
544
|
+
const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
|
|
546
545
|
const runInput = SimpleCLI.input({
|
|
547
546
|
positionals: [
|
|
548
547
|
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
@@ -561,9 +560,6 @@ const runInput = SimpleCLI.input({
|
|
|
561
560
|
name: "params-file",
|
|
562
561
|
help: "Path to a JSON params file"
|
|
563
562
|
}),
|
|
564
|
-
credentials: SimpleCLI.option(z.string().optional(), {
|
|
565
|
-
help: "Inline JSON credentials passed to ctx.credentials"
|
|
566
|
-
}),
|
|
567
563
|
tsconfig: SimpleCLI.option(z.string().optional(), {
|
|
568
564
|
help: "Path to a tsconfig used for workflow module resolution"
|
|
569
565
|
}),
|
|
@@ -614,11 +610,6 @@ const runCommand = SimpleCLI.command({
|
|
|
614
610
|
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
615
611
|
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
616
612
|
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;
|
|
622
613
|
const headlessMode = input.headed ? false : input.headless ? true : void 0;
|
|
623
614
|
const visualize = !input.noVisualize;
|
|
624
615
|
const viewport = resolveViewport(
|
|
@@ -631,7 +622,6 @@ const runCommand = SimpleCLI.command({
|
|
|
631
622
|
workflowName: input.workflowName,
|
|
632
623
|
session: ctx.session,
|
|
633
624
|
params,
|
|
634
|
-
credentials,
|
|
635
625
|
tsconfigPath: input.tsconfig,
|
|
636
626
|
headless: headlessMode ?? false,
|
|
637
627
|
visualize,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
const EXPERIMENTAL_COMMAND_PREFIX = "experimental";
|
|
3
|
+
const EXPERIMENTAL_GROUP_DESCRIPTION = "Experimental commands";
|
|
2
4
|
function toCamelCase(input2) {
|
|
3
5
|
return input2.replace(
|
|
4
6
|
/-([a-zA-Z0-9])/g,
|
|
@@ -420,7 +422,7 @@ class SimpleCLIApp {
|
|
|
420
422
|
}
|
|
421
423
|
renderRootHelp() {
|
|
422
424
|
const lines = [`Usage: ${this.name} <command>`, "", "Commands:"];
|
|
423
|
-
for (const entry of this.
|
|
425
|
+
for (const entry of this.getRootHelpEntries()) {
|
|
424
426
|
lines.push(formatListEntry(entry.label, entry.description));
|
|
425
427
|
}
|
|
426
428
|
return lines.join("\n");
|
|
@@ -493,7 +495,6 @@ class SimpleCLIApp {
|
|
|
493
495
|
continue;
|
|
494
496
|
}
|
|
495
497
|
const command2 = this.findCommandByPath(routeEntry.path);
|
|
496
|
-
if (command2?.experimental) continue;
|
|
497
498
|
entries.push({
|
|
498
499
|
label: token,
|
|
499
500
|
description: command2?.description
|
|
@@ -501,6 +502,41 @@ class SimpleCLIApp {
|
|
|
501
502
|
}
|
|
502
503
|
return entries;
|
|
503
504
|
}
|
|
505
|
+
getRootHelpEntries() {
|
|
506
|
+
return this.getImmediateRouteEntries([]).filter((entry) => {
|
|
507
|
+
const token = entry.label.replace(/\s+<subcommand>$/, "");
|
|
508
|
+
const group2 = this.findGroupByPath([token]);
|
|
509
|
+
if (!group2) {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
if (token === EXPERIMENTAL_COMMAND_PREFIX) {
|
|
513
|
+
return this.groupHasExperimentalCommand(group2.path);
|
|
514
|
+
}
|
|
515
|
+
return this.groupHasVisibleNonExperimentalCommand(group2.path);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
groupHasVisibleNonExperimentalCommand(path) {
|
|
519
|
+
for (const routeEntry of this.routeEntries) {
|
|
520
|
+
if (routeEntry.kind !== "command") continue;
|
|
521
|
+
if (!pathStartsWith(routeEntry.path, path)) continue;
|
|
522
|
+
const command2 = this.findCommandByPath(routeEntry.path);
|
|
523
|
+
if (command2 && !command2.experimental) {
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
groupHasExperimentalCommand(path) {
|
|
530
|
+
for (const routeEntry of this.routeEntries) {
|
|
531
|
+
if (routeEntry.kind !== "command") continue;
|
|
532
|
+
if (!pathStartsWith(routeEntry.path, path)) continue;
|
|
533
|
+
const command2 = this.findCommandByPath(routeEntry.path);
|
|
534
|
+
if (command2?.experimental) {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
504
540
|
findBestMatchingCommand(args) {
|
|
505
541
|
let bestMatch = null;
|
|
506
542
|
for (const command2 of this.resolvedCommands.values()) {
|
|
@@ -600,31 +636,38 @@ function validateRequiredNamedArgs(definitions, named) {
|
|
|
600
636
|
throw new Error(`Missing required option --${flagName}.`);
|
|
601
637
|
}
|
|
602
638
|
}
|
|
603
|
-
function resolveRouteTree(routes
|
|
639
|
+
function resolveRouteTree(routes) {
|
|
640
|
+
const collected = collectRouteTree(routes);
|
|
641
|
+
const { groups, routeEntries } = buildResolvedRouteEntries(
|
|
642
|
+
collected.commands,
|
|
643
|
+
collected.groupDescriptions
|
|
644
|
+
);
|
|
645
|
+
return {
|
|
646
|
+
commands: collected.commands,
|
|
647
|
+
groups,
|
|
648
|
+
routeEntries
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function collectRouteTree(routes, parentPath = [], parentMiddlewares = []) {
|
|
604
652
|
const resolved = {
|
|
605
653
|
commands: [],
|
|
606
|
-
|
|
607
|
-
routeEntries: []
|
|
654
|
+
groupDescriptions: /* @__PURE__ */ new Map()
|
|
608
655
|
};
|
|
609
656
|
for (const [token, routeValue] of Object.entries(routes)) {
|
|
610
657
|
if (isGroup(routeValue)) {
|
|
611
658
|
const groupPath = [...parentPath, token];
|
|
612
|
-
resolved.
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
resolved.routeEntries.push({
|
|
618
|
-
kind: "group",
|
|
619
|
-
path: groupPath
|
|
620
|
-
});
|
|
621
|
-
const nested = resolveRouteTree(routeValue.routes, groupPath, [
|
|
659
|
+
resolved.groupDescriptions.set(
|
|
660
|
+
pathToRouteKey(groupPath),
|
|
661
|
+
routeValue.description
|
|
662
|
+
);
|
|
663
|
+
const nested = collectRouteTree(routeValue.routes, groupPath, [
|
|
622
664
|
...parentMiddlewares,
|
|
623
665
|
...routeValue.middlewares
|
|
624
666
|
]);
|
|
625
667
|
resolved.commands.push(...nested.commands);
|
|
626
|
-
|
|
627
|
-
|
|
668
|
+
for (const [routeKey, description] of nested.groupDescriptions) {
|
|
669
|
+
resolved.groupDescriptions.set(routeKey, description);
|
|
670
|
+
}
|
|
628
671
|
continue;
|
|
629
672
|
}
|
|
630
673
|
const command2 = routeValue.getDefinition();
|
|
@@ -633,7 +676,8 @@ function resolveRouteTree(routes, parentPath = [], parentMiddlewares = []) {
|
|
|
633
676
|
`Command "${[...parentPath, token].join(" ")}" is missing a handler.`
|
|
634
677
|
);
|
|
635
678
|
}
|
|
636
|
-
const
|
|
679
|
+
const rawPath = [...parentPath, token];
|
|
680
|
+
const path = command2.config.experimental ? [EXPERIMENTAL_COMMAND_PREFIX, ...rawPath] : rawPath;
|
|
637
681
|
resolved.commands.push({
|
|
638
682
|
routeKey: pathToRouteKey(path),
|
|
639
683
|
path,
|
|
@@ -646,12 +690,43 @@ function resolveRouteTree(routes, parentPath = [], parentMiddlewares = []) {
|
|
|
646
690
|
),
|
|
647
691
|
handler: command2.handler
|
|
648
692
|
});
|
|
649
|
-
|
|
693
|
+
}
|
|
694
|
+
return resolved;
|
|
695
|
+
}
|
|
696
|
+
function buildResolvedRouteEntries(commands, groupDescriptions) {
|
|
697
|
+
const groups = /* @__PURE__ */ new Map();
|
|
698
|
+
const routeEntries = [];
|
|
699
|
+
for (const command2 of commands) {
|
|
700
|
+
for (let depth = 1; depth < command2.path.length; depth += 1) {
|
|
701
|
+
const path = command2.path.slice(0, depth);
|
|
702
|
+
const routeKey = pathToRouteKey(path);
|
|
703
|
+
if (groups.has(routeKey)) continue;
|
|
704
|
+
groups.set(routeKey, {
|
|
705
|
+
routeKey,
|
|
706
|
+
path,
|
|
707
|
+
description: resolveGroupDescription(path, groupDescriptions)
|
|
708
|
+
});
|
|
709
|
+
routeEntries.push({
|
|
710
|
+
kind: "group",
|
|
711
|
+
path
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
routeEntries.push({
|
|
650
715
|
kind: "command",
|
|
651
|
-
path
|
|
716
|
+
path: command2.path
|
|
652
717
|
});
|
|
653
718
|
}
|
|
654
|
-
return
|
|
719
|
+
return {
|
|
720
|
+
groups: [...groups.values()],
|
|
721
|
+
routeEntries
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function resolveGroupDescription(path, groupDescriptions) {
|
|
725
|
+
if (path.length === 1 && path[0] === EXPERIMENTAL_COMMAND_PREFIX) {
|
|
726
|
+
return EXPERIMENTAL_GROUP_DESCRIPTION;
|
|
727
|
+
}
|
|
728
|
+
const originalPath = path[0] === EXPERIMENTAL_COMMAND_PREFIX ? path.slice(1) : path;
|
|
729
|
+
return groupDescriptions.get(pathToRouteKey(originalPath));
|
|
655
730
|
}
|
|
656
731
|
function mergeInheritedMiddlewares(parentMiddlewares, commandMiddlewares) {
|
|
657
732
|
if (parentMiddlewares.length === 0) {
|
package/dist/cli/router.js
CHANGED
|
@@ -8,12 +8,12 @@ import { snapshotCommand } from "./commands/snapshot.js";
|
|
|
8
8
|
import { SimpleCLI } from "./framework/simple-cli.js";
|
|
9
9
|
const cliRoutes = {
|
|
10
10
|
...browserCommands,
|
|
11
|
+
deploy: deployCommand,
|
|
11
12
|
...executionCommands,
|
|
12
13
|
...logCommands,
|
|
13
14
|
ai: aiCommands,
|
|
14
15
|
init: initCommand,
|
|
15
|
-
snapshot: snapshotCommand
|
|
16
|
-
deploy: deployCommand
|
|
16
|
+
snapshot: snapshotCommand
|
|
17
17
|
};
|
|
18
18
|
function createCLIApp() {
|
|
19
19
|
return SimpleCLI.define("libretto", cliRoutes);
|
|
@@ -176,8 +176,7 @@ async function runIntegrationInternal(args, options) {
|
|
|
176
176
|
const workflowContext = {
|
|
177
177
|
session: args.session,
|
|
178
178
|
logger: integrationLogger,
|
|
179
|
-
page: browserSession.page
|
|
180
|
-
credentials: args.credentials
|
|
179
|
+
page: browserSession.page
|
|
181
180
|
};
|
|
182
181
|
try {
|
|
183
182
|
try {
|
|
@@ -4,7 +4,6 @@ const RunIntegrationWorkerRequestSchema = z.object({
|
|
|
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(),
|
|
8
7
|
headless: z.boolean(),
|
|
9
8
|
visualize: z.boolean().default(true),
|
|
10
9
|
authProfileDomain: z.string().optional(),
|
|
@@ -6,7 +6,6 @@ type LibrettoWorkflowContext = {
|
|
|
6
6
|
session: string;
|
|
7
7
|
page: Page;
|
|
8
8
|
logger: MinimalLogger;
|
|
9
|
-
credentials?: Record<string, unknown>;
|
|
10
9
|
};
|
|
11
10
|
type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (ctx: LibrettoWorkflowContext, input: Input) => Promise<Output>;
|
|
12
11
|
declare class LibrettoWorkflow<Input = unknown, Output = unknown> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.5.3
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"postinstall": "node scripts/postinstall.mjs",
|
|
34
|
-
"sync
|
|
35
|
-
"check:
|
|
34
|
+
"sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
|
|
35
|
+
"check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
|
|
36
|
+
"sync-skills": "pnpm run sync:mirrors",
|
|
37
|
+
"check:skills": "pnpm run check:mirrors",
|
|
36
38
|
"build": "tsup --config tsup.config.ts",
|
|
37
39
|
"type-check": "tsc --noEmit",
|
|
38
40
|
"test": "pnpm run build && vitest run",
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
cpSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
readdirSync,
|
|
9
|
-
rmSync,
|
|
10
|
-
} from "node:fs";
|
|
11
|
-
import { relative, resolve, join } from "node:path";
|
|
3
|
+
import { cpSync, mkdirSync, rmSync } from "node:fs";
|
|
12
4
|
|
|
13
5
|
export const SKILL_DIRS = [
|
|
14
6
|
"packages/libretto/skills/libretto",
|
|
@@ -16,88 +8,8 @@ export const SKILL_DIRS = [
|
|
|
16
8
|
".claude/skills/libretto",
|
|
17
9
|
];
|
|
18
10
|
|
|
19
|
-
function walkFiles(dir, baseDir = dir) {
|
|
20
|
-
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
|
|
21
|
-
a.name.localeCompare(b.name),
|
|
22
|
-
);
|
|
23
|
-
const files = [];
|
|
24
|
-
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
const fullPath = join(dir, entry.name);
|
|
27
|
-
if (entry.isDirectory()) {
|
|
28
|
-
files.push(...walkFiles(fullPath, baseDir));
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (entry.isFile()) files.push(relative(baseDir, fullPath));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return files;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
11
|
export function syncSkillDir(sourceDir, destDir) {
|
|
38
12
|
rmSync(destDir, { recursive: true, force: true });
|
|
39
13
|
mkdirSync(destDir, { recursive: true });
|
|
40
14
|
cpSync(sourceDir, destDir, { recursive: true });
|
|
41
15
|
}
|
|
42
|
-
|
|
43
|
-
export function syncRepoSkills(repoRoot) {
|
|
44
|
-
const sourceDir = resolve(repoRoot, "skills/libretto");
|
|
45
|
-
for (const dir of SKILL_DIRS.slice(1)) {
|
|
46
|
-
syncSkillDir(sourceDir, resolve(repoRoot, dir));
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function compareSkillDirs(repoRoot) {
|
|
51
|
-
const roots = SKILL_DIRS.map((dir) => ({
|
|
52
|
-
label: dir,
|
|
53
|
-
absPath: resolve(repoRoot, dir),
|
|
54
|
-
}));
|
|
55
|
-
const missing = roots.filter(({ absPath }) => !existsSync(absPath));
|
|
56
|
-
const mismatches = [];
|
|
57
|
-
|
|
58
|
-
if (missing.length > 0) {
|
|
59
|
-
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
issues: missing.map(({ label }) => `missing directory: ${label}`),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const expectedFiles = walkFiles(roots[0].absPath);
|
|
66
|
-
const expectedFileSet = new Set(expectedFiles);
|
|
67
|
-
|
|
68
|
-
for (const root of roots.slice(1)) {
|
|
69
|
-
const actualFiles = walkFiles(root.absPath);
|
|
70
|
-
const actualFileSet = new Set(actualFiles);
|
|
71
|
-
|
|
72
|
-
for (const file of expectedFiles) {
|
|
73
|
-
if (!actualFileSet.has(file)) {
|
|
74
|
-
mismatches.push(`${root.label} is missing file: ${file}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const file of actualFiles) {
|
|
79
|
-
if (!expectedFileSet.has(file)) {
|
|
80
|
-
mismatches.push(`${root.label} has unexpected file: ${file}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
for (const file of expectedFiles) {
|
|
86
|
-
const expectedContent = readFileSync(join(roots[0].absPath, file));
|
|
87
|
-
for (const root of roots.slice(1)) {
|
|
88
|
-
const targetPath = join(root.absPath, file);
|
|
89
|
-
if (!existsSync(targetPath)) continue;
|
|
90
|
-
const actualContent = readFileSync(targetPath);
|
|
91
|
-
if (!expectedContent.equals(actualContent)) {
|
|
92
|
-
mismatches.push(
|
|
93
|
-
`${root.label} differs from ${roots[0].label}: ${file}`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
ok: mismatches.length === 0,
|
|
101
|
-
issues: mismatches,
|
|
102
|
-
};
|
|
103
|
-
}
|
package/skills/libretto/SKILL.md
CHANGED
|
@@ -40,7 +40,7 @@ Key points:
|
|
|
40
40
|
|
|
41
41
|
- `workflow(name, handler)` takes a unique workflow name and returns the workflow object that Libretto can run.
|
|
42
42
|
- `npx libretto run ./file.ts myWorkflow` resolves `myWorkflow` from the workflows exported by `./file.ts`, so export or re-export the workflow from that file directly or through a `workflows` object, and make sure the run argument matches the name passed to `workflow("myWorkflow", ...)`.
|
|
43
|
-
- `ctx` provides `session`, `page`,
|
|
43
|
+
- `ctx` provides `session`, `page`, and `logger`
|
|
44
44
|
- `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI
|
|
45
45
|
- Use `await pause(ctx.session)` (or `await pause(session)`) to pause the workflow for debugging. It is a no-op in production.
|
|
46
46
|
- 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.
|
|
@@ -121,7 +121,7 @@ export const deployInput = SimpleCLI.input({
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
export const deployCommand = SimpleCLI.command({
|
|
124
|
-
description: "
|
|
124
|
+
description: "Deploy workflows to the hosted platform",
|
|
125
125
|
experimental: true,
|
|
126
126
|
})
|
|
127
127
|
.input(deployInput)
|
|
@@ -608,7 +608,6 @@ async function runIntegrationFromFile(
|
|
|
608
608
|
workflowName: args.workflowName,
|
|
609
609
|
session: args.session,
|
|
610
610
|
params: args.params,
|
|
611
|
-
credentials: args.credentials,
|
|
612
611
|
headless: args.headless,
|
|
613
612
|
visualize: args.visualize,
|
|
614
613
|
authProfileDomain: args.authProfileDomain,
|
|
@@ -707,7 +706,7 @@ export const execCommand = SimpleCLI.command({
|
|
|
707
706
|
);
|
|
708
707
|
});
|
|
709
708
|
|
|
710
|
-
const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--
|
|
709
|
+
const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
|
|
711
710
|
|
|
712
711
|
export const runInput = SimpleCLI.input({
|
|
713
712
|
positionals: [
|
|
@@ -727,9 +726,6 @@ export const runInput = SimpleCLI.input({
|
|
|
727
726
|
name: "params-file",
|
|
728
727
|
help: "Path to a JSON params file",
|
|
729
728
|
}),
|
|
730
|
-
credentials: SimpleCLI.option(z.string().optional(), {
|
|
731
|
-
help: "Inline JSON credentials passed to ctx.credentials",
|
|
732
|
-
}),
|
|
733
729
|
tsconfig: SimpleCLI.option(z.string().optional(), {
|
|
734
730
|
help: "Path to a tsconfig used for workflow module resolution",
|
|
735
731
|
}),
|
|
@@ -792,13 +788,6 @@ export const runCommand = SimpleCLI.command({
|
|
|
792
788
|
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
793
789
|
|
|
794
790
|
const params = resolveRunParams(input.params, input.paramsFile);
|
|
795
|
-
const rawCredentials = input.credentials
|
|
796
|
-
? parseJsonArg("--credentials", input.credentials)
|
|
797
|
-
: undefined;
|
|
798
|
-
if (rawCredentials !== undefined && (typeof rawCredentials !== "object" || rawCredentials === null || Array.isArray(rawCredentials))) {
|
|
799
|
-
throw new Error("--credentials must be a JSON object (e.g., '{\"key\": \"value\"}').");
|
|
800
|
-
}
|
|
801
|
-
const credentials = rawCredentials as Record<string, unknown> | undefined;
|
|
802
791
|
const headlessMode = input.headed
|
|
803
792
|
? false
|
|
804
793
|
: input.headless
|
|
@@ -816,7 +805,6 @@ export const runCommand = SimpleCLI.command({
|
|
|
816
805
|
workflowName: input.workflowName!,
|
|
817
806
|
session: ctx.session,
|
|
818
807
|
params,
|
|
819
|
-
credentials,
|
|
820
808
|
tsconfigPath: input.tsconfig,
|
|
821
809
|
headless: headlessMode ?? false,
|
|
822
810
|
visualize,
|
|
@@ -129,6 +129,11 @@ type InternalResolvedRouteEntry = {
|
|
|
129
129
|
path: readonly string[];
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
type CollectedRouteTreeResult = {
|
|
133
|
+
commands: InternalResolvedCommand[];
|
|
134
|
+
groupDescriptions: Map<string, string | undefined>;
|
|
135
|
+
};
|
|
136
|
+
|
|
132
137
|
type ResolveRouteTreeResult = {
|
|
133
138
|
commands: InternalResolvedCommand[];
|
|
134
139
|
groups: InternalResolvedGroup[];
|
|
@@ -150,6 +155,9 @@ type ExtractedGlobalArgs = {
|
|
|
150
155
|
named: Readonly<Record<string, unknown>>;
|
|
151
156
|
};
|
|
152
157
|
|
|
158
|
+
const EXPERIMENTAL_COMMAND_PREFIX = "experimental";
|
|
159
|
+
const EXPERIMENTAL_GROUP_DESCRIPTION = "Experimental commands";
|
|
160
|
+
|
|
153
161
|
function toCamelCase(input: string): string {
|
|
154
162
|
return input.replace(/-([a-zA-Z0-9])/g, (_match, letter: string) =>
|
|
155
163
|
letter.toUpperCase(),
|
|
@@ -761,7 +769,7 @@ export class SimpleCLIApp {
|
|
|
761
769
|
|
|
762
770
|
private renderRootHelp(): string {
|
|
763
771
|
const lines = [`Usage: ${this.name} <command>`, "", "Commands:"];
|
|
764
|
-
for (const entry of this.
|
|
772
|
+
for (const entry of this.getRootHelpEntries()) {
|
|
765
773
|
lines.push(formatListEntry(entry.label, entry.description));
|
|
766
774
|
}
|
|
767
775
|
return lines.join("\n");
|
|
@@ -860,7 +868,6 @@ export class SimpleCLIApp {
|
|
|
860
868
|
}
|
|
861
869
|
|
|
862
870
|
const command = this.findCommandByPath(routeEntry.path);
|
|
863
|
-
if (command?.experimental) continue;
|
|
864
871
|
entries.push({
|
|
865
872
|
label: token,
|
|
866
873
|
description: command?.description,
|
|
@@ -870,6 +877,49 @@ export class SimpleCLIApp {
|
|
|
870
877
|
return entries;
|
|
871
878
|
}
|
|
872
879
|
|
|
880
|
+
private getRootHelpEntries(): Array<{
|
|
881
|
+
label: string;
|
|
882
|
+
description?: string;
|
|
883
|
+
}> {
|
|
884
|
+
return this.getImmediateRouteEntries([]).filter((entry) => {
|
|
885
|
+
const token = entry.label.replace(/\s+<subcommand>$/, "");
|
|
886
|
+
const group = this.findGroupByPath([token]);
|
|
887
|
+
if (!group) {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
if (token === EXPERIMENTAL_COMMAND_PREFIX) {
|
|
891
|
+
return this.groupHasExperimentalCommand(group.path);
|
|
892
|
+
}
|
|
893
|
+
return this.groupHasVisibleNonExperimentalCommand(group.path);
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private groupHasVisibleNonExperimentalCommand(path: readonly string[]): boolean {
|
|
898
|
+
for (const routeEntry of this.routeEntries) {
|
|
899
|
+
if (routeEntry.kind !== "command") continue;
|
|
900
|
+
if (!pathStartsWith(routeEntry.path, path)) continue;
|
|
901
|
+
const command = this.findCommandByPath(routeEntry.path);
|
|
902
|
+
if (command && !command.experimental) {
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private groupHasExperimentalCommand(path: readonly string[]): boolean {
|
|
911
|
+
for (const routeEntry of this.routeEntries) {
|
|
912
|
+
if (routeEntry.kind !== "command") continue;
|
|
913
|
+
if (!pathStartsWith(routeEntry.path, path)) continue;
|
|
914
|
+
const command = this.findCommandByPath(routeEntry.path);
|
|
915
|
+
if (command?.experimental) {
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
|
|
873
923
|
private findBestMatchingCommand(
|
|
874
924
|
args: readonly string[],
|
|
875
925
|
): InternalResolvedCommand | null {
|
|
@@ -1034,37 +1084,46 @@ function validateRequiredNamedArgs(
|
|
|
1034
1084
|
}
|
|
1035
1085
|
}
|
|
1036
1086
|
|
|
1037
|
-
function resolveRouteTree(
|
|
1087
|
+
function resolveRouteTree(routes: SimpleCLIRouteTree<any>): ResolveRouteTreeResult {
|
|
1088
|
+
const collected = collectRouteTree(routes);
|
|
1089
|
+
const { groups, routeEntries } = buildResolvedRouteEntries(
|
|
1090
|
+
collected.commands,
|
|
1091
|
+
collected.groupDescriptions,
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
commands: collected.commands,
|
|
1096
|
+
groups,
|
|
1097
|
+
routeEntries,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function collectRouteTree(
|
|
1038
1102
|
routes: SimpleCLIRouteTree<any>,
|
|
1039
1103
|
parentPath: readonly string[] = [],
|
|
1040
1104
|
parentMiddlewares: readonly AnySimpleCLIMiddleware[] = [],
|
|
1041
|
-
):
|
|
1042
|
-
const resolved:
|
|
1105
|
+
): CollectedRouteTreeResult {
|
|
1106
|
+
const resolved: CollectedRouteTreeResult = {
|
|
1043
1107
|
commands: [],
|
|
1044
|
-
|
|
1045
|
-
routeEntries: [],
|
|
1108
|
+
groupDescriptions: new Map<string, string | undefined>(),
|
|
1046
1109
|
};
|
|
1047
1110
|
|
|
1048
1111
|
for (const [token, routeValue] of Object.entries(routes)) {
|
|
1049
1112
|
if (isGroup(routeValue)) {
|
|
1050
1113
|
const groupPath = [...parentPath, token];
|
|
1051
|
-
resolved.
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
});
|
|
1056
|
-
resolved.routeEntries.push({
|
|
1057
|
-
kind: "group",
|
|
1058
|
-
path: groupPath,
|
|
1059
|
-
});
|
|
1114
|
+
resolved.groupDescriptions.set(
|
|
1115
|
+
pathToRouteKey(groupPath),
|
|
1116
|
+
routeValue.description,
|
|
1117
|
+
);
|
|
1060
1118
|
|
|
1061
|
-
const nested =
|
|
1119
|
+
const nested = collectRouteTree(routeValue.routes, groupPath, [
|
|
1062
1120
|
...parentMiddlewares,
|
|
1063
1121
|
...routeValue.middlewares,
|
|
1064
1122
|
]);
|
|
1065
1123
|
resolved.commands.push(...nested.commands);
|
|
1066
|
-
|
|
1067
|
-
|
|
1124
|
+
for (const [routeKey, description] of nested.groupDescriptions) {
|
|
1125
|
+
resolved.groupDescriptions.set(routeKey, description);
|
|
1126
|
+
}
|
|
1068
1127
|
continue;
|
|
1069
1128
|
}
|
|
1070
1129
|
|
|
@@ -1075,7 +1134,10 @@ function resolveRouteTree(
|
|
|
1075
1134
|
);
|
|
1076
1135
|
}
|
|
1077
1136
|
|
|
1078
|
-
const
|
|
1137
|
+
const rawPath = [...parentPath, token];
|
|
1138
|
+
const path = command.config.experimental
|
|
1139
|
+
? [EXPERIMENTAL_COMMAND_PREFIX, ...rawPath]
|
|
1140
|
+
: rawPath;
|
|
1079
1141
|
resolved.commands.push({
|
|
1080
1142
|
routeKey: pathToRouteKey(path),
|
|
1081
1143
|
path,
|
|
@@ -1092,13 +1154,58 @@ function resolveRouteTree(
|
|
|
1092
1154
|
unknown
|
|
1093
1155
|
>,
|
|
1094
1156
|
});
|
|
1095
|
-
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return resolved;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function buildResolvedRouteEntries(
|
|
1163
|
+
commands: readonly InternalResolvedCommand[],
|
|
1164
|
+
groupDescriptions: ReadonlyMap<string, string | undefined>,
|
|
1165
|
+
): Pick<ResolveRouteTreeResult, "groups" | "routeEntries"> {
|
|
1166
|
+
const groups = new Map<string, InternalResolvedGroup>();
|
|
1167
|
+
const routeEntries: InternalResolvedRouteEntry[] = [];
|
|
1168
|
+
|
|
1169
|
+
for (const command of commands) {
|
|
1170
|
+
for (let depth = 1; depth < command.path.length; depth += 1) {
|
|
1171
|
+
const path = command.path.slice(0, depth);
|
|
1172
|
+
const routeKey = pathToRouteKey(path);
|
|
1173
|
+
if (groups.has(routeKey)) continue;
|
|
1174
|
+
|
|
1175
|
+
groups.set(routeKey, {
|
|
1176
|
+
routeKey,
|
|
1177
|
+
path,
|
|
1178
|
+
description: resolveGroupDescription(path, groupDescriptions),
|
|
1179
|
+
});
|
|
1180
|
+
routeEntries.push({
|
|
1181
|
+
kind: "group",
|
|
1182
|
+
path,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
routeEntries.push({
|
|
1096
1187
|
kind: "command",
|
|
1097
|
-
path,
|
|
1188
|
+
path: command.path,
|
|
1098
1189
|
});
|
|
1099
1190
|
}
|
|
1100
1191
|
|
|
1101
|
-
return
|
|
1192
|
+
return {
|
|
1193
|
+
groups: [...groups.values()],
|
|
1194
|
+
routeEntries,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function resolveGroupDescription(
|
|
1199
|
+
path: readonly string[],
|
|
1200
|
+
groupDescriptions: ReadonlyMap<string, string | undefined>,
|
|
1201
|
+
): string | undefined {
|
|
1202
|
+
if (path.length === 1 && path[0] === EXPERIMENTAL_COMMAND_PREFIX) {
|
|
1203
|
+
return EXPERIMENTAL_GROUP_DESCRIPTION;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const originalPath =
|
|
1207
|
+
path[0] === EXPERIMENTAL_COMMAND_PREFIX ? path.slice(1) : path;
|
|
1208
|
+
return groupDescriptions.get(pathToRouteKey(originalPath));
|
|
1102
1209
|
}
|
|
1103
1210
|
|
|
1104
1211
|
function mergeInheritedMiddlewares(
|
package/src/cli/router.ts
CHANGED
|
@@ -9,12 +9,12 @@ import { SimpleCLI } from "./framework/simple-cli.js";
|
|
|
9
9
|
|
|
10
10
|
export const cliRoutes = {
|
|
11
11
|
...browserCommands,
|
|
12
|
+
deploy: deployCommand,
|
|
12
13
|
...executionCommands,
|
|
13
14
|
...logCommands,
|
|
14
15
|
ai: aiCommands,
|
|
15
16
|
init: initCommand,
|
|
16
17
|
snapshot: snapshotCommand,
|
|
17
|
-
deploy: deployCommand,
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export function createCLIApp() {
|
|
@@ -5,7 +5,6 @@ export const RunIntegrationWorkerRequestSchema = z.object({
|
|
|
5
5
|
workflowName: z.string().min(1),
|
|
6
6
|
session: z.string().min(1),
|
|
7
7
|
params: z.unknown(),
|
|
8
|
-
credentials: z.record(z.string(), z.unknown()).optional(),
|
|
9
8
|
headless: z.boolean(),
|
|
10
9
|
visualize: z.boolean().default(true),
|
|
11
10
|
authProfileDomain: z.string().optional(),
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
import { compareSkillDirs, SKILL_DIRS } from "./skills-libretto.mjs";
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const repoRoot = join(__dirname, "..", "..", "..");
|
|
10
|
-
const result = compareSkillDirs(repoRoot);
|
|
11
|
-
|
|
12
|
-
if (result.ok) {
|
|
13
|
-
console.log(
|
|
14
|
-
`libretto: verified identical skill mirrors across ${SKILL_DIRS.join(", ")}`,
|
|
15
|
-
);
|
|
16
|
-
process.exit(0);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
console.error("libretto: skill directories must be identical:");
|
|
20
|
-
for (const issue of result.issues) {
|
|
21
|
-
console.error(`- ${issue}`);
|
|
22
|
-
}
|
|
23
|
-
console.error("");
|
|
24
|
-
console.error("Run `pnpm i` to resync the mirrors in this repository.");
|
|
25
|
-
process.exit(1);
|
package/scripts/sync-skills.mjs
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
import { SKILL_DIRS, syncRepoSkills } from "./skills-libretto.mjs";
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const repoRoot = join(__dirname, "..", "..", "..");
|
|
10
|
-
|
|
11
|
-
syncRepoSkills(repoRoot);
|
|
12
|
-
console.log(`libretto: synced skill mirrors across ${SKILL_DIRS.join(", ")}`);
|