libretto 0.4.2 → 0.5.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/cli.js +20 -19
- package/dist/cli/commands/ai.js +1 -1
- package/dist/cli/commands/browser.js +3 -3
- package/dist/cli/commands/execution.js +3 -3
- package/dist/cli/commands/init.js +30 -21
- package/dist/cli/commands/logs.js +1 -1
- package/dist/cli/commands/snapshot.js +6 -1
- package/dist/cli/core/ai-config.js +62 -12
- package/dist/cli/core/browser.js +11 -6
- package/dist/cli/core/context.js +4 -18
- package/dist/cli/core/session.js +2 -2
- package/dist/cli/core/snapshot-analyzer.js +2 -2
- package/dist/cli/core/snapshot-api-config.js +46 -5
- package/dist/cli/router.js +1 -1
- package/dist/cli/workers/run-integration-runtime.js +2 -4
- package/dist/shared/debug/index.d.ts +1 -1
- package/dist/shared/debug/index.js +2 -3
- package/dist/shared/debug/pause.d.ts +2 -6
- package/dist/shared/debug/pause.js +27 -20
- package/dist/shared/llm/client.js +5 -5
- package/dist/shared/paths/paths.js +2 -1
- package/dist/shared/paths/repo-root.d.ts +3 -0
- package/dist/shared/paths/repo-root.js +24 -0
- package/package.json +8 -8
- package/scripts/postinstall.mjs +50 -0
- package/skills/libretto/SKILL.md +93 -404
- package/skills/libretto/references/auth-profiles.md +30 -0
- package/skills/libretto/references/pages-and-page-targeting.md +29 -0
- package/skills/libretto/references/reverse-engineering-network-requests.md +39 -0
- package/skills/libretto/references/user-action-log.md +31 -0
- package/src/cli/cli.ts +173 -0
- package/src/cli/commands/ai.ts +35 -0
- package/src/cli/commands/browser.ts +165 -0
- package/src/cli/commands/execution.ts +691 -0
- package/src/cli/commands/init.ts +327 -0
- package/src/cli/commands/logs.ts +128 -0
- package/src/cli/commands/shared.ts +70 -0
- package/src/cli/commands/snapshot.ts +327 -0
- package/src/cli/core/ai-config.ts +255 -0
- package/src/cli/core/api-snapshot-analyzer.ts +97 -0
- package/src/cli/core/browser.ts +839 -0
- package/src/cli/core/context.ts +122 -0
- package/src/cli/core/pause-signals.ts +35 -0
- package/src/cli/core/session-telemetry.ts +553 -0
- package/src/cli/core/session.ts +209 -0
- package/src/cli/core/snapshot-analyzer.ts +875 -0
- package/src/cli/core/snapshot-api-config.ts +236 -0
- package/src/cli/core/telemetry.ts +446 -0
- package/src/cli/framework/simple-cli.ts +1273 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/router.ts +28 -0
- package/src/cli/workers/run-integration-runtime.ts +311 -0
- package/src/cli/workers/run-integration-worker-protocol.ts +14 -0
- package/src/cli/workers/run-integration-worker.ts +75 -0
- package/src/index.ts +120 -0
- package/src/runtime/download/download.ts +100 -0
- package/src/runtime/download/index.ts +7 -0
- package/src/runtime/extract/extract.ts +92 -0
- package/src/runtime/extract/index.ts +1 -0
- package/src/runtime/network/index.ts +5 -0
- package/src/runtime/network/network.ts +113 -0
- package/src/runtime/recovery/agent.ts +256 -0
- package/src/runtime/recovery/errors.ts +152 -0
- package/src/runtime/recovery/index.ts +7 -0
- package/src/runtime/recovery/recovery.ts +50 -0
- package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +243 -115
- package/src/shared/config/config.ts +22 -0
- package/src/shared/config/index.ts +5 -0
- package/src/shared/debug/index.ts +1 -0
- package/src/shared/debug/pause.ts +85 -0
- package/src/shared/instrumentation/errors.ts +82 -0
- package/src/shared/instrumentation/index.ts +9 -0
- package/src/shared/instrumentation/instrument.ts +276 -0
- package/src/shared/llm/ai-sdk-adapter.ts +78 -0
- package/src/shared/llm/client.ts +217 -0
- package/src/shared/llm/index.ts +3 -0
- package/src/shared/llm/types.ts +63 -0
- package/src/shared/logger/index.ts +6 -0
- package/src/shared/logger/logger.ts +352 -0
- package/src/shared/logger/sinks.ts +144 -0
- package/src/shared/paths/paths.ts +109 -0
- package/src/shared/paths/repo-root.ts +27 -0
- package/src/shared/run/api.ts +2 -0
- package/src/shared/run/browser.ts +98 -0
- package/src/shared/state/index.ts +11 -0
- package/src/shared/state/session-state.ts +74 -0
- package/src/shared/visualization/ghost-cursor.ts +200 -0
- package/src/shared/visualization/highlight.ts +146 -0
- package/src/shared/visualization/index.ts +18 -0
- package/src/shared/workflow/workflow.ts +42 -0
- package/dist/index.cjs +0 -144
- package/dist/index.d.cts +0 -21
- package/dist/runtime/download/download.cjs +0 -70
- package/dist/runtime/download/download.d.cts +0 -35
- package/dist/runtime/download/index.cjs +0 -30
- package/dist/runtime/download/index.d.cts +0 -3
- package/dist/runtime/extract/extract.cjs +0 -88
- package/dist/runtime/extract/extract.d.cts +0 -23
- package/dist/runtime/extract/index.cjs +0 -28
- package/dist/runtime/extract/index.d.cts +0 -5
- package/dist/runtime/network/index.cjs +0 -28
- package/dist/runtime/network/index.d.cts +0 -4
- package/dist/runtime/network/network.cjs +0 -91
- package/dist/runtime/network/network.d.cts +0 -28
- package/dist/runtime/recovery/agent.cjs +0 -223
- package/dist/runtime/recovery/agent.d.cts +0 -13
- package/dist/runtime/recovery/errors.cjs +0 -124
- package/dist/runtime/recovery/errors.d.cts +0 -31
- package/dist/runtime/recovery/index.cjs +0 -34
- package/dist/runtime/recovery/index.d.cts +0 -7
- package/dist/runtime/recovery/recovery.cjs +0 -55
- package/dist/runtime/recovery/recovery.d.cts +0 -12
- package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
- package/dist/shared/config/config.cjs +0 -44
- package/dist/shared/config/config.d.cts +0 -10
- package/dist/shared/config/index.cjs +0 -32
- package/dist/shared/config/index.d.cts +0 -1
- package/dist/shared/debug/index.cjs +0 -30
- package/dist/shared/debug/index.d.cts +0 -1
- package/dist/shared/debug/pause.cjs +0 -90
- package/dist/shared/debug/pause.d.cts +0 -16
- package/dist/shared/instrumentation/errors.cjs +0 -81
- package/dist/shared/instrumentation/errors.d.cts +0 -12
- package/dist/shared/instrumentation/index.cjs +0 -35
- package/dist/shared/instrumentation/index.d.cts +0 -6
- package/dist/shared/instrumentation/instrument.cjs +0 -206
- package/dist/shared/instrumentation/instrument.d.cts +0 -32
- package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
- package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
- package/dist/shared/llm/client.cjs +0 -218
- package/dist/shared/llm/client.d.cts +0 -13
- package/dist/shared/llm/index.cjs +0 -31
- package/dist/shared/llm/index.d.cts +0 -5
- package/dist/shared/llm/types.cjs +0 -16
- package/dist/shared/llm/types.d.cts +0 -67
- package/dist/shared/logger/index.cjs +0 -37
- package/dist/shared/logger/index.d.cts +0 -2
- package/dist/shared/logger/logger.cjs +0 -232
- package/dist/shared/logger/logger.d.cts +0 -86
- package/dist/shared/logger/sinks.cjs +0 -160
- package/dist/shared/logger/sinks.d.cts +0 -9
- package/dist/shared/paths/paths.cjs +0 -104
- package/dist/shared/paths/paths.d.cts +0 -10
- package/dist/shared/run/api.cjs +0 -28
- package/dist/shared/run/api.d.cts +0 -2
- package/dist/shared/run/browser.cjs +0 -98
- package/dist/shared/run/browser.d.cts +0 -22
- package/dist/shared/state/index.cjs +0 -38
- package/dist/shared/state/index.d.cts +0 -2
- package/dist/shared/state/session-state.cjs +0 -92
- package/dist/shared/state/session-state.d.cts +0 -40
- package/dist/shared/visualization/ghost-cursor.cjs +0 -174
- package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
- package/dist/shared/visualization/highlight.cjs +0 -134
- package/dist/shared/visualization/highlight.d.cts +0 -22
- package/dist/shared/visualization/index.cjs +0 -45
- package/dist/shared/visualization/index.d.cts +0 -3
- package/dist/shared/workflow/workflow.cjs +0 -47
- package/dist/shared/workflow/workflow.d.cts +0 -21
- package/skills/libretto/code-generation-rules.md +0 -223
- package/skills/libretto/integration-approach-selection.md +0 -174
package/dist/cli/cli.js
CHANGED
|
@@ -16,30 +16,30 @@ Options:
|
|
|
16
16
|
Built-in sessions: default, dev-server, browser-agent
|
|
17
17
|
|
|
18
18
|
Examples:
|
|
19
|
-
libretto
|
|
19
|
+
libretto open https://linkedin.com
|
|
20
20
|
|
|
21
21
|
# ... manually log in ...
|
|
22
|
-
libretto
|
|
22
|
+
libretto save linkedin.com
|
|
23
23
|
# Next time you open linkedin.com, you'll be logged in automatically
|
|
24
24
|
|
|
25
|
-
libretto
|
|
26
|
-
libretto
|
|
27
|
-
libretto
|
|
28
|
-
libretto
|
|
29
|
-
libretto
|
|
30
|
-
libretto
|
|
31
|
-
libretto
|
|
32
|
-
libretto
|
|
33
|
-
libretto
|
|
34
|
-
libretto
|
|
35
|
-
libretto
|
|
36
|
-
libretto
|
|
37
|
-
libretto
|
|
25
|
+
libretto exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
|
|
26
|
+
libretto exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
|
|
27
|
+
libretto ai configure openai
|
|
28
|
+
libretto ai configure anthropic
|
|
29
|
+
libretto ai configure gemini
|
|
30
|
+
libretto ai configure vertex
|
|
31
|
+
libretto ai configure openai/gpt-4o
|
|
32
|
+
libretto snapshot
|
|
33
|
+
libretto snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
34
|
+
libretto resume --session default
|
|
35
|
+
libretto close
|
|
36
|
+
libretto close --all
|
|
37
|
+
libretto close --all --force
|
|
38
38
|
|
|
39
39
|
# Multiple sessions
|
|
40
|
-
libretto
|
|
41
|
-
libretto
|
|
42
|
-
libretto
|
|
40
|
+
libretto open https://site1.com --session test1
|
|
41
|
+
libretto open https://site2.com --session test2
|
|
42
|
+
libretto exec "return await page.title()" --session test1
|
|
43
43
|
|
|
44
44
|
Available in exec:
|
|
45
45
|
page, context, state, browser, networkLog, actionLog
|
|
@@ -93,7 +93,8 @@ function validateLegacySessionArg(rawArgs) {
|
|
|
93
93
|
if (value === void 0) return;
|
|
94
94
|
if (value === null) {
|
|
95
95
|
throw new Error(
|
|
96
|
-
|
|
96
|
+
`Usage: libretto <command> [--session <name>]
|
|
97
|
+
Missing or invalid --session value.`
|
|
97
98
|
);
|
|
98
99
|
}
|
|
99
100
|
validateSessionName(value);
|
package/dist/cli/commands/ai.js
CHANGED
|
@@ -47,7 +47,7 @@ const openInput = SimpleCLI.input({
|
|
|
47
47
|
}
|
|
48
48
|
}).refine(
|
|
49
49
|
(input) => Boolean(input.url),
|
|
50
|
-
|
|
50
|
+
`Usage: libretto open <url> [--headless] [--viewport WxH] [--session <name>]`
|
|
51
51
|
).refine(
|
|
52
52
|
(input) => !(input.headed && input.headless),
|
|
53
53
|
"Cannot pass both --headed and --headless."
|
|
@@ -73,7 +73,7 @@ const saveInput = SimpleCLI.input({
|
|
|
73
73
|
}
|
|
74
74
|
}).refine(
|
|
75
75
|
(input) => Boolean(input.urlOrDomain),
|
|
76
|
-
|
|
76
|
+
`Usage: libretto save <url|domain> [--session <name>]`
|
|
77
77
|
);
|
|
78
78
|
function createSaveCommand(logger) {
|
|
79
79
|
return SimpleCLI.command({
|
|
@@ -108,7 +108,7 @@ function createCloseCommand(logger) {
|
|
|
108
108
|
description: "Close the browser"
|
|
109
109
|
}).input(closeInput).use(resolveSessionMiddleware).handle(async ({ input, ctx }) => {
|
|
110
110
|
if (input.force && !input.all) {
|
|
111
|
-
throw new Error(
|
|
111
|
+
throw new Error(`Usage: libretto close --all [--force]`);
|
|
112
112
|
}
|
|
113
113
|
if (input.all) {
|
|
114
114
|
await runCloseAllWithLogger(logger, { force: input.force });
|
|
@@ -292,7 +292,7 @@ async function runResume(session, logger, sessionState) {
|
|
|
292
292
|
} = getPauseSignalPaths(session);
|
|
293
293
|
if (!existsSync(pausedSignalPath)) {
|
|
294
294
|
throw new Error(
|
|
295
|
-
`Session "${session}" is not paused. Run "libretto
|
|
295
|
+
`Session "${session}" is not paused. Run "libretto run ... --session ${session}" and call pause("${session}") first.`
|
|
296
296
|
);
|
|
297
297
|
}
|
|
298
298
|
if (!isProcessRunning(sessionState.pid)) {
|
|
@@ -414,7 +414,7 @@ const execInput = SimpleCLI.input({
|
|
|
414
414
|
}
|
|
415
415
|
}).refine(
|
|
416
416
|
(input) => input.codeParts.length > 0,
|
|
417
|
-
|
|
417
|
+
`Usage: libretto exec <code> [--session <name>] [--visualize]`
|
|
418
418
|
);
|
|
419
419
|
function createExecCommand(logger) {
|
|
420
420
|
return SimpleCLI.command({
|
|
@@ -429,7 +429,7 @@ function createExecCommand(logger) {
|
|
|
429
429
|
);
|
|
430
430
|
});
|
|
431
431
|
}
|
|
432
|
-
const runUsage =
|
|
432
|
+
const runUsage = `Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]`;
|
|
433
433
|
const runInput = SimpleCLI.input({
|
|
434
434
|
positionals: [
|
|
435
435
|
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { appendFileSync, cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { readAiConfig } from "../core/ai-config.js";
|
|
7
7
|
import { REPO_ROOT } from "../core/context.js";
|
|
@@ -60,6 +60,17 @@ function safeReadAiConfig() {
|
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
function printInvalidAiConfigWarning() {
|
|
64
|
+
try {
|
|
65
|
+
readAiConfig();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
+
console.log(" ! Existing AI config is invalid:");
|
|
69
|
+
for (const line of message.split("\n")) {
|
|
70
|
+
console.log(` ${line}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
63
74
|
function printSnapshotApiStatus() {
|
|
64
75
|
const config = safeReadAiConfig();
|
|
65
76
|
const selection = resolveSnapshotApiModel(config);
|
|
@@ -69,6 +80,7 @@ function printSnapshotApiStatus() {
|
|
|
69
80
|
" Libretto uses direct API calls for snapshot analysis when supported credentials are available."
|
|
70
81
|
);
|
|
71
82
|
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
83
|
+
printInvalidAiConfigWarning();
|
|
72
84
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
73
85
|
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
74
86
|
console.log(" Snapshot objectives will use the API analyzer by default.");
|
|
@@ -84,7 +96,7 @@ function printSnapshotApiStatus() {
|
|
|
84
96
|
" GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
|
|
85
97
|
);
|
|
86
98
|
console.log(
|
|
87
|
-
" Or run `npx libretto ai configure
|
|
99
|
+
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
88
100
|
);
|
|
89
101
|
console.log(" Run `npx libretto init` interactively to set up credentials.");
|
|
90
102
|
}
|
|
@@ -95,6 +107,7 @@ async function runInteractiveApiSetup() {
|
|
|
95
107
|
console.log("\nSnapshot analysis setup:");
|
|
96
108
|
console.log(" Libretto uses direct API calls for snapshot analysis.");
|
|
97
109
|
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
110
|
+
printInvalidAiConfigWarning();
|
|
98
111
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
99
112
|
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
100
113
|
console.log(" Snapshot objectives will use the API analyzer by default.");
|
|
@@ -119,7 +132,7 @@ async function runInteractiveApiSetup() {
|
|
|
119
132
|
console.log(" ANTHROPIC_API_KEY=...");
|
|
120
133
|
console.log(" GEMINI_API_KEY=...");
|
|
121
134
|
console.log(
|
|
122
|
-
" Or run `npx libretto ai configure
|
|
135
|
+
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
123
136
|
);
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
@@ -196,27 +209,21 @@ function getPackageSkillsDir() {
|
|
|
196
209
|
}
|
|
197
210
|
throw new Error("Could not locate libretto skill files in package");
|
|
198
211
|
}
|
|
212
|
+
function detectAgentDirs(root) {
|
|
213
|
+
const dirs = [];
|
|
214
|
+
if (existsSync(join(root, ".agents"))) dirs.push(join(root, ".agents"));
|
|
215
|
+
if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
|
|
216
|
+
return dirs;
|
|
217
|
+
}
|
|
199
218
|
async function copySkills() {
|
|
200
|
-
const
|
|
201
|
-
const agentDirs = [];
|
|
202
|
-
if (existsSync(join(cwd, ".agents"))) {
|
|
203
|
-
agentDirs.push({
|
|
204
|
-
name: ".agents",
|
|
205
|
-
skillDest: join(cwd, ".agents", "skills", "libretto")
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
if (existsSync(join(cwd, ".claude"))) {
|
|
209
|
-
agentDirs.push({
|
|
210
|
-
name: ".claude",
|
|
211
|
-
skillDest: join(cwd, ".claude", "skills", "libretto")
|
|
212
|
-
});
|
|
213
|
-
}
|
|
219
|
+
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
214
220
|
if (agentDirs.length === 0) {
|
|
215
|
-
console.log("\nSkills: No .agents/ or .claude/ directory found \u2014 skipping
|
|
221
|
+
console.log("\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping.");
|
|
216
222
|
return;
|
|
217
223
|
}
|
|
218
|
-
const
|
|
219
|
-
const
|
|
224
|
+
const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
|
|
225
|
+
const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
|
|
226
|
+
const existing = destinations.filter((d) => existsSync(d));
|
|
220
227
|
const verb = existing.length > 0 ? "Overwrite" : "Install";
|
|
221
228
|
const proceed = await askYesNo(`
|
|
222
229
|
${verb} libretto skills in ${dirNames}?`);
|
|
@@ -231,7 +238,9 @@ ${verb} libretto skills in ${dirNames}?`);
|
|
|
231
238
|
console.error(` \u2717 ${e instanceof Error ? e.message : String(e)}`);
|
|
232
239
|
return;
|
|
233
240
|
}
|
|
234
|
-
for (
|
|
241
|
+
for (let i = 0; i < agentDirs.length; i++) {
|
|
242
|
+
const skillDest = destinations[i];
|
|
243
|
+
const name = basename(agentDirs[i]);
|
|
235
244
|
if (existsSync(skillDest)) {
|
|
236
245
|
rmSync(skillDest, { recursive: true });
|
|
237
246
|
}
|
|
@@ -26,7 +26,7 @@ async function resolvePageId(session, pageId) {
|
|
|
26
26
|
const foundPage = pages.find((page) => page.id === pageId);
|
|
27
27
|
if (!foundPage) {
|
|
28
28
|
throw new Error(
|
|
29
|
-
`Page "${pageId}" was not found in session "${session}". Run "libretto
|
|
29
|
+
`Page "${pageId}" was not found in session "${session}". Run "libretto pages --session ${session}" to list ids.`
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
return pageId;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./shared.js";
|
|
14
14
|
import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
|
|
15
15
|
import { readAiConfig } from "../core/ai-config.js";
|
|
16
|
+
import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
|
|
16
17
|
const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
|
|
17
18
|
const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
|
|
18
19
|
function generateSnapshotRunId() {
|
|
@@ -191,6 +192,10 @@ async function runSnapshot(session, logger, pageId, objective, context) {
|
|
|
191
192
|
"Couldn't run analysis: --objective is required when providing --context."
|
|
192
193
|
);
|
|
193
194
|
}
|
|
195
|
+
const configuredAi = normalizedObjective ? readAiConfig() : null;
|
|
196
|
+
if (normalizedObjective) {
|
|
197
|
+
resolveSnapshotApiModelOrThrow(configuredAi);
|
|
198
|
+
}
|
|
194
199
|
const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
|
|
195
200
|
session,
|
|
196
201
|
logger,
|
|
@@ -212,7 +217,7 @@ async function runSnapshot(session, logger, pageId, objective, context) {
|
|
|
212
217
|
htmlPath,
|
|
213
218
|
condensedHtmlPath
|
|
214
219
|
};
|
|
215
|
-
await runApiInterpret(interpretArgs, logger,
|
|
220
|
+
await runApiInterpret(interpretArgs, logger, configuredAi);
|
|
216
221
|
}
|
|
217
222
|
const snapshotInput = SimpleCLI.input({
|
|
218
223
|
positionals: [],
|
|
@@ -6,7 +6,7 @@ const CURRENT_CONFIG_VERSION = 1;
|
|
|
6
6
|
const AiConfigSchema = z.object({
|
|
7
7
|
model: z.string().min(1),
|
|
8
8
|
updatedAt: z.string()
|
|
9
|
-
})
|
|
9
|
+
});
|
|
10
10
|
const ViewportConfigSchema = z.object({
|
|
11
11
|
width: z.number().int().min(1),
|
|
12
12
|
height: z.number().int().min(1)
|
|
@@ -19,22 +19,68 @@ const LibrettoConfigSchema = z.object({
|
|
|
19
19
|
const DEFAULT_MODELS = {
|
|
20
20
|
openai: "openai/gpt-5.4",
|
|
21
21
|
anthropic: "anthropic/claude-sonnet-4-6",
|
|
22
|
-
gemini: "google/gemini-
|
|
23
|
-
google: "google/gemini-2.5-flash",
|
|
22
|
+
gemini: "google/gemini-3-flash-preview",
|
|
24
23
|
vertex: "vertex/gemini-2.5-pro"
|
|
25
24
|
};
|
|
26
|
-
const
|
|
27
|
-
|
|
25
|
+
const PROVIDER_ALIASES = {
|
|
26
|
+
claude: DEFAULT_MODELS.anthropic,
|
|
27
|
+
google: DEFAULT_MODELS.gemini
|
|
28
|
+
};
|
|
29
|
+
const CONFIGURE_PROVIDERS = ["openai", "anthropic", "gemini", "vertex"];
|
|
30
|
+
function formatConfigureProviders(separator = " | ") {
|
|
31
|
+
return CONFIGURE_PROVIDERS.join(separator);
|
|
32
|
+
}
|
|
33
|
+
function formatConfigIssues(error) {
|
|
34
|
+
return error.issues.map((issue) => ` - ${issue.path.join(".") || "root"}: ${issue.message}`).join("\n");
|
|
35
|
+
}
|
|
36
|
+
function formatExpectedConfigExample() {
|
|
37
|
+
return JSON.stringify(
|
|
38
|
+
{
|
|
39
|
+
version: CURRENT_CONFIG_VERSION,
|
|
40
|
+
ai: {
|
|
41
|
+
model: "openai/gpt-5.4",
|
|
42
|
+
updatedAt: "2026-01-01T00:00:00.000Z"
|
|
43
|
+
},
|
|
44
|
+
viewport: {
|
|
45
|
+
width: 1280,
|
|
46
|
+
height: 800
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
null,
|
|
50
|
+
2
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function invalidConfigError(configPath, detail) {
|
|
28
54
|
return new Error(
|
|
29
|
-
|
|
55
|
+
[
|
|
56
|
+
`AI config is invalid at ${configPath}.`,
|
|
57
|
+
detail ? `Problems:
|
|
58
|
+
${detail}` : null,
|
|
59
|
+
"Expected config example:",
|
|
60
|
+
formatExpectedConfigExample(),
|
|
61
|
+
"Notes:",
|
|
62
|
+
' - "ai" and "viewport" are optional.',
|
|
63
|
+
' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
|
|
64
|
+
"Fix the file to match this shape, or delete it and rerun:",
|
|
65
|
+
` npx libretto ai configure ${formatConfigureProviders()}`
|
|
66
|
+
].filter(Boolean).join("\n")
|
|
30
67
|
);
|
|
31
68
|
}
|
|
32
69
|
function parseConfig(raw, configPath) {
|
|
70
|
+
let parsedJson;
|
|
33
71
|
try {
|
|
34
|
-
|
|
35
|
-
} catch {
|
|
36
|
-
throw invalidConfigError(
|
|
72
|
+
parsedJson = JSON.parse(raw);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw invalidConfigError(
|
|
75
|
+
configPath,
|
|
76
|
+
` - root: Invalid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const parsed = LibrettoConfigSchema.safeParse(parsedJson);
|
|
80
|
+
if (!parsed.success) {
|
|
81
|
+
throw invalidConfigError(configPath, formatConfigIssues(parsed.error));
|
|
37
82
|
}
|
|
83
|
+
return parsed.data;
|
|
38
84
|
}
|
|
39
85
|
function readLibrettoConfig(configPath = LIBRETTO_CONFIG_PATH) {
|
|
40
86
|
if (!existsSync(configPath)) {
|
|
@@ -88,7 +134,8 @@ function resolveModelFromInput(input) {
|
|
|
88
134
|
const trimmed = input.trim();
|
|
89
135
|
if (!trimmed) return null;
|
|
90
136
|
if (trimmed.includes("/")) return trimmed;
|
|
91
|
-
|
|
137
|
+
const normalized = trimmed.toLowerCase();
|
|
138
|
+
return DEFAULT_MODELS[normalized] ?? PROVIDER_ALIASES[normalized] ?? null;
|
|
92
139
|
}
|
|
93
140
|
function runAiConfigure(input, options = {}) {
|
|
94
141
|
const configureCommandName = options.configureCommandName ?? "npx libretto ai configure";
|
|
@@ -97,7 +144,10 @@ function runAiConfigure(input, options = {}) {
|
|
|
97
144
|
if (!presetArg && !input.clear) {
|
|
98
145
|
const config2 = readAiConfig(configPath);
|
|
99
146
|
if (!config2) {
|
|
100
|
-
console.log(
|
|
147
|
+
console.log(
|
|
148
|
+
`No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
|
|
149
|
+
);
|
|
150
|
+
console.log("Provider credentials still come from your shell or .env file.");
|
|
101
151
|
return;
|
|
102
152
|
}
|
|
103
153
|
printAiConfig(config2, configPath);
|
|
@@ -120,7 +170,7 @@ function runAiConfigure(input, options = {}) {
|
|
|
120
170
|
${configureCommandName} --clear`
|
|
121
171
|
);
|
|
122
172
|
throw new Error(
|
|
123
|
-
`Invalid provider or model. Use one of: ${
|
|
173
|
+
`Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`
|
|
124
174
|
);
|
|
125
175
|
}
|
|
126
176
|
const config = writeAiConfig(model, configPath);
|
package/dist/cli/core/browser.js
CHANGED
|
@@ -142,9 +142,14 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
|
|
|
142
142
|
port: state.port,
|
|
143
143
|
pid: state.pid
|
|
144
144
|
});
|
|
145
|
-
|
|
145
|
+
if (!isPidRunning(state.pid)) {
|
|
146
|
+
clearSessionState(session, logger);
|
|
147
|
+
throw new Error(
|
|
148
|
+
`No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
146
151
|
throw new Error(
|
|
147
|
-
`
|
|
152
|
+
`Could not connect to the browser for session "${session}" at http://127.0.0.1:${state.port}, but the session process (pid ${state.pid}) is still running. Try the command again, or close and reopen the session if it stays stuck.`
|
|
148
153
|
);
|
|
149
154
|
}
|
|
150
155
|
const contexts = browser.contexts();
|
|
@@ -170,14 +175,14 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
|
|
|
170
175
|
}
|
|
171
176
|
if (options?.requireSinglePage && !options.pageId && pages.length > 1) {
|
|
172
177
|
throw new Error(
|
|
173
|
-
`Multiple pages are open in session "${session}". Pass --page <id> to target a page (run "libretto
|
|
178
|
+
`Multiple pages are open in session "${session}". Pass --page <id> to target a page (run "libretto pages --session ${session}" to list ids).`
|
|
174
179
|
);
|
|
175
180
|
}
|
|
176
181
|
const pageRefs = await resolvePageReferences(pages);
|
|
177
182
|
const pageRef = options?.pageId ? pageRefs.find((ref) => ref.id === options.pageId) ?? null : pageRefs[pageRefs.length - 1];
|
|
178
183
|
if (!pageRef) {
|
|
179
184
|
throw new Error(
|
|
180
|
-
`Page "${options?.pageId}" was not found in session "${session}". Run "libretto
|
|
185
|
+
`Page "${options?.pageId}" was not found in session "${session}". Run "libretto pages --session ${session}" to list ids.`
|
|
181
186
|
);
|
|
182
187
|
}
|
|
183
188
|
const page = pageRef.page;
|
|
@@ -293,7 +298,7 @@ function childLog(level, event, data = {}) {
|
|
|
293
298
|
timestamp: new Date().toISOString(),
|
|
294
299
|
id: Math.random().toString(36).slice(2, 10),
|
|
295
300
|
level,
|
|
296
|
-
scope: 'libretto
|
|
301
|
+
scope: 'libretto.child',
|
|
297
302
|
event,
|
|
298
303
|
data,
|
|
299
304
|
});
|
|
@@ -606,7 +611,7 @@ async function runCloseAll(logger, options) {
|
|
|
606
611
|
[
|
|
607
612
|
`Failed to close ${survivors.length} session(s) gracefully: ${formatSessionList(survivors)}.`,
|
|
608
613
|
`Closed ${closed} session(s).`,
|
|
609
|
-
|
|
614
|
+
`Retry with: libretto close --all --force`
|
|
610
615
|
].join("\n")
|
|
611
616
|
);
|
|
612
617
|
}
|
package/dist/cli/core/context.js
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
import { Logger, createFileLogSink } from "../../shared/logger/index.js";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { cwd } from "node:process";
|
|
4
2
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolveLibrettoRepoRoot } from "../../shared/paths/repo-root.js";
|
|
6
5
|
import { validateSessionName } from "./session.js";
|
|
7
|
-
|
|
8
|
-
const override = process.env.LIBRETTO_REPO_ROOT?.trim();
|
|
9
|
-
if (override) {
|
|
10
|
-
return resolve(override);
|
|
11
|
-
}
|
|
12
|
-
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
13
|
-
encoding: "utf-8"
|
|
14
|
-
});
|
|
15
|
-
if (result.status === 0 && result.stdout) {
|
|
16
|
-
return result.stdout.trim();
|
|
17
|
-
}
|
|
18
|
-
return cwd();
|
|
19
|
-
}
|
|
20
|
-
const REPO_ROOT = getRepoRoot();
|
|
6
|
+
const REPO_ROOT = resolveLibrettoRepoRoot();
|
|
21
7
|
const LIBRETTO_CONFIG_DIR = join(REPO_ROOT, ".libretto");
|
|
22
8
|
const LIBRETTO_CONFIG_PATH = join(LIBRETTO_CONFIG_DIR, "config.json");
|
|
23
9
|
const PROFILES_DIR = join(LIBRETTO_CONFIG_DIR, "profiles");
|
|
@@ -63,7 +49,7 @@ function createLoggerForSession(session) {
|
|
|
63
49
|
const sessionDir = getSessionDir(session);
|
|
64
50
|
mkdirSync(sessionDir, { recursive: true });
|
|
65
51
|
const logFilePath = getSessionLogsPath(session);
|
|
66
|
-
return new Logger(["libretto
|
|
52
|
+
return new Logger(["libretto"], [createFileLogSink({ filePath: logFilePath })]);
|
|
67
53
|
}
|
|
68
54
|
async function closeLogger(logger) {
|
|
69
55
|
if (!logger) return;
|
package/dist/cli/core/session.js
CHANGED
|
@@ -93,7 +93,7 @@ function throwSessionNotFoundError(session) {
|
|
|
93
93
|
}
|
|
94
94
|
lines.push("");
|
|
95
95
|
lines.push("Start one with:");
|
|
96
|
-
lines.push(` libretto
|
|
96
|
+
lines.push(` libretto open <url> --session ${session}`);
|
|
97
97
|
throw new Error(lines.join("\n"));
|
|
98
98
|
}
|
|
99
99
|
function assertSessionStateExistsOrThrow(session) {
|
|
@@ -161,7 +161,7 @@ function assertSessionAvailableForStart(session, logger) {
|
|
|
161
161
|
}
|
|
162
162
|
const endpoint = `http://127.0.0.1:${existingState.port}`;
|
|
163
163
|
throw new Error(
|
|
164
|
-
`Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with: libretto
|
|
164
|
+
`Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with: libretto close --session ${session}`
|
|
165
165
|
);
|
|
166
166
|
}
|
|
167
167
|
export {
|
|
@@ -76,7 +76,7 @@ ${stripAnsi(result.stderr).trim() || stripAnsi(result.stdout).trim() || "No erro
|
|
|
76
76
|
}
|
|
77
77
|
class CodexUserCodingAgent extends UserCodingAgent {
|
|
78
78
|
async analyzeSnapshot(prompt, pngPath, logger) {
|
|
79
|
-
const tempDir = mkdtempSync(join(tmpdir(), "libretto-
|
|
79
|
+
const tempDir = mkdtempSync(join(tmpdir(), "libretto-analyzer-"));
|
|
80
80
|
const outputPath = join(
|
|
81
81
|
tempDir,
|
|
82
82
|
`snapshot-analyzer-${Date.now()}-${Math.random().toString(36).slice(2)}.json`
|
|
@@ -171,7 +171,7 @@ async function runExternalCommand(command, args, logger, stdinText) {
|
|
|
171
171
|
if (error.code === "ENOENT") {
|
|
172
172
|
reject(
|
|
173
173
|
new Error(
|
|
174
|
-
`Command not found: ${command}. Configure AI with 'libretto
|
|
174
|
+
`Command not found: ${command}. Configure AI with 'libretto ai configure'.`
|
|
175
175
|
)
|
|
176
176
|
);
|
|
177
177
|
return;
|
|
@@ -3,16 +3,15 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
readAiConfig
|
|
5
5
|
} from "./ai-config.js";
|
|
6
|
-
import { REPO_ROOT } from "./context.js";
|
|
6
|
+
import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
|
|
7
7
|
import {
|
|
8
8
|
hasProviderCredentials,
|
|
9
|
-
missingProviderCredentialsMessage,
|
|
10
9
|
parseModel
|
|
11
10
|
} from "../../shared/llm/client.js";
|
|
12
11
|
const DEFAULT_SNAPSHOT_MODELS = {
|
|
13
12
|
openai: "openai/gpt-5.4",
|
|
14
13
|
anthropic: "anthropic/claude-sonnet-4-6",
|
|
15
|
-
google: "google/gemini-
|
|
14
|
+
google: "google/gemini-3-flash-preview",
|
|
16
15
|
vertex: "vertex/gemini-2.5-pro"
|
|
17
16
|
};
|
|
18
17
|
class SnapshotApiUnavailableError extends Error {
|
|
@@ -21,6 +20,48 @@ class SnapshotApiUnavailableError extends Error {
|
|
|
21
20
|
this.name = "SnapshotApiUnavailableError";
|
|
22
21
|
}
|
|
23
22
|
}
|
|
23
|
+
function providerSetupSentence(provider) {
|
|
24
|
+
switch (provider) {
|
|
25
|
+
case "openai":
|
|
26
|
+
return "Add OPENAI_API_KEY to .env or as a shell environment variable.";
|
|
27
|
+
case "anthropic":
|
|
28
|
+
return "Add ANTHROPIC_API_KEY to .env or as a shell environment variable.";
|
|
29
|
+
case "google":
|
|
30
|
+
return "Add GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY to .env or as a shell environment variable.";
|
|
31
|
+
case "vertex":
|
|
32
|
+
return "Add GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT to .env or as a shell environment variable, and make sure application default credentials are configured.";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function defaultModelCommandLine() {
|
|
36
|
+
return "npx libretto ai configure openai | anthropic | gemini | vertex";
|
|
37
|
+
}
|
|
38
|
+
function providerMissingCredentialSummary(provider) {
|
|
39
|
+
switch (provider) {
|
|
40
|
+
case "openai":
|
|
41
|
+
return "OPENAI_API_KEY is missing";
|
|
42
|
+
case "anthropic":
|
|
43
|
+
return "ANTHROPIC_API_KEY is missing";
|
|
44
|
+
case "google":
|
|
45
|
+
return "GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are missing";
|
|
46
|
+
case "vertex":
|
|
47
|
+
return "GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT are missing";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function noSnapshotApiConfiguredMessage() {
|
|
51
|
+
return [
|
|
52
|
+
"Failed to analyze snapshot because no snapshot analyzer is configured.",
|
|
53
|
+
`Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, or GOOGLE_CLOUD_PROJECT to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
|
|
54
|
+
"For more info, run `npx libretto init`."
|
|
55
|
+
].join(" ");
|
|
56
|
+
}
|
|
57
|
+
function missingProviderSnapshotMessage(selection) {
|
|
58
|
+
const configuredSource = selection.source === "config" ? ` in ${LIBRETTO_CONFIG_PATH}` : " from process env or .env";
|
|
59
|
+
return [
|
|
60
|
+
`Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
|
|
61
|
+
providerSetupSentence(selection.provider),
|
|
62
|
+
"For more info, run `npx libretto init`."
|
|
63
|
+
].join(" ");
|
|
64
|
+
}
|
|
24
65
|
function readWorktreeEnvPath() {
|
|
25
66
|
const gitPath = join(REPO_ROOT, ".git");
|
|
26
67
|
if (!existsSync(gitPath)) return null;
|
|
@@ -114,12 +155,12 @@ function resolveSnapshotApiModelOrThrow(config = readAiConfig()) {
|
|
|
114
155
|
const selection = resolveSnapshotApiModel(config);
|
|
115
156
|
if (!selection) {
|
|
116
157
|
throw new SnapshotApiUnavailableError(
|
|
117
|
-
|
|
158
|
+
noSnapshotApiConfiguredMessage()
|
|
118
159
|
);
|
|
119
160
|
}
|
|
120
161
|
if (!hasProviderCredentials(selection.provider)) {
|
|
121
162
|
throw new SnapshotApiUnavailableError(
|
|
122
|
-
|
|
163
|
+
missingProviderSnapshotMessage(selection)
|
|
123
164
|
);
|
|
124
165
|
}
|
|
125
166
|
return selection;
|
package/dist/cli/router.js
CHANGED
|
@@ -17,7 +17,7 @@ function buildCLIRoutes(logger) {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
function createCLIApp(logger) {
|
|
20
|
-
return SimpleCLI.define("libretto
|
|
20
|
+
return SimpleCLI.define("libretto", buildCLIRoutes(logger), {
|
|
21
21
|
globalNamed: {
|
|
22
22
|
session: sessionOption()
|
|
23
23
|
}
|
|
@@ -6,7 +6,6 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
import {
|
|
7
7
|
launchBrowser
|
|
8
8
|
} from "../../index.js";
|
|
9
|
-
import { setSessionForPause } from "../../shared/debug/pause.js";
|
|
10
9
|
import { parseSessionStateContent } from "../../shared/state/index.js";
|
|
11
10
|
import { getProfilePath, normalizeDomain } from "../core/browser.js";
|
|
12
11
|
import {
|
|
@@ -78,9 +77,9 @@ function getMissingLocalAuthProfileError(args) {
|
|
|
78
77
|
`Local auth profile not found for domain "${normalizedDomain}".`,
|
|
79
78
|
`Expected profile file: ${args.profilePath}`,
|
|
80
79
|
"To create it:",
|
|
81
|
-
` 1. libretto
|
|
80
|
+
` 1. libretto open https://${normalizedDomain} --headed --session ${args.session}`,
|
|
82
81
|
" 2. Log in manually in the browser window.",
|
|
83
|
-
` 3. libretto
|
|
82
|
+
` 3. libretto save ${normalizedDomain} --session ${args.session}`
|
|
84
83
|
].join("\n");
|
|
85
84
|
}
|
|
86
85
|
function getAbsoluteIntegrationPath(integrationPath) {
|
|
@@ -181,7 +180,6 @@ async function runIntegrationInternal(args, options) {
|
|
|
181
180
|
appendFileSync(networkLogPath, JSON.stringify(entry) + "\n");
|
|
182
181
|
}
|
|
183
182
|
});
|
|
184
|
-
setSessionForPause(args.session);
|
|
185
183
|
const workflowContext = {
|
|
186
184
|
logger: integrationLogger,
|
|
187
185
|
page: browserSession.page,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { pause
|
|
1
|
+
export { pause } from './pause.js';
|