libretto 0.5.3-experimental.5 → 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 +114 -37
- package/README.template.md +160 -0
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/deploy.js +148 -0
- package/dist/cli/commands/execution.js +218 -96
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/deploy-artifact.js +687 -0
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +144 -43
- package/dist/cli/router.js +16 -21
- package/dist/cli/workers/run-integration-runtime.js +25 -45
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +13 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +18 -10
- package/dist/shared/workflow/workflow.js +50 -5
- package/package.json +14 -6
- package/scripts/generate-changelog.ts +132 -0
- package/scripts/postinstall.mjs +4 -3
- package/scripts/skills-libretto.mjs +2 -88
- package/scripts/summarize-evals.mjs +32 -10
- package/skills/libretto/SKILL.md +132 -62
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +176 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/deploy.ts +198 -0
- package/src/cli/commands/execution.ts +251 -111
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/deploy-artifact.ts +938 -0
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +281 -98
- package/src/cli/router.ts +15 -21
- package/src/cli/workers/run-integration-runtime.ts +35 -57
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +77 -67
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +107 -30
- package/scripts/check-skills-sync.mjs +0 -23
- package/scripts/prepare-release.sh +0 -97
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -75
- package/skills/libretto/references/user-action-log.md +0 -31
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
3
11
|
import { spawnSync } from "node:child_process";
|
|
4
12
|
import { basename, dirname, join } from "node:path";
|
|
5
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -9,8 +17,8 @@ import {
|
|
|
9
17
|
loadSnapshotEnv,
|
|
10
18
|
resolveSnapshotApiModel,
|
|
11
19
|
} from "../core/snapshot-api-config.js";
|
|
12
|
-
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
20
|
import { hasProviderCredentials } from "../../shared/llm/client.js";
|
|
21
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
14
22
|
|
|
15
23
|
type ProviderChoice = {
|
|
16
24
|
key: string;
|
|
@@ -42,7 +50,8 @@ const PROVIDER_CHOICES: ProviderChoice[] = [
|
|
|
42
50
|
key: "4",
|
|
43
51
|
label: "Google Vertex AI",
|
|
44
52
|
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
45
|
-
envHint:
|
|
53
|
+
envHint:
|
|
54
|
+
"Requires gcloud auth application-default login and a GCP project ID",
|
|
46
55
|
},
|
|
47
56
|
];
|
|
48
57
|
|
|
@@ -57,16 +66,6 @@ function promptUser(
|
|
|
57
66
|
});
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
function askYesNo(question: string): Promise<boolean> {
|
|
61
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
62
|
-
return new Promise((resolve) => {
|
|
63
|
-
rl.question(`${question} (y/N) `, (answer) => {
|
|
64
|
-
rl.close();
|
|
65
|
-
resolve(answer.trim().toLowerCase() === "y");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
69
|
function safeReadAiConfig(): ReturnType<typeof readAiConfig> {
|
|
71
70
|
try {
|
|
72
71
|
return readAiConfig();
|
|
@@ -101,7 +100,9 @@ function printSnapshotApiStatus(): void {
|
|
|
101
100
|
|
|
102
101
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
103
102
|
console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
|
|
104
|
-
console.log(
|
|
103
|
+
console.log(
|
|
104
|
+
" Snapshot objectives will use the API analyzer by default.",
|
|
105
|
+
);
|
|
105
106
|
console.log(" No further action required.");
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
@@ -117,7 +118,9 @@ function printSnapshotApiStatus(): void {
|
|
|
117
118
|
console.log(
|
|
118
119
|
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model.",
|
|
119
120
|
);
|
|
120
|
-
console.log(
|
|
121
|
+
console.log(
|
|
122
|
+
" Run `npx libretto init` interactively to set up credentials.",
|
|
123
|
+
);
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
async function runInteractiveApiSetup(): Promise<void> {
|
|
@@ -132,7 +135,9 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
132
135
|
|
|
133
136
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
134
137
|
console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
|
|
135
|
-
console.log(
|
|
138
|
+
console.log(
|
|
139
|
+
" Snapshot objectives will use the API analyzer by default.",
|
|
140
|
+
);
|
|
136
141
|
return;
|
|
137
142
|
}
|
|
138
143
|
|
|
@@ -144,7 +149,9 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
144
149
|
});
|
|
145
150
|
|
|
146
151
|
try {
|
|
147
|
-
console.log(
|
|
152
|
+
console.log(
|
|
153
|
+
" Which API provider would you like to use for snapshot analysis?\n",
|
|
154
|
+
);
|
|
148
155
|
for (const choice of PROVIDER_CHOICES) {
|
|
149
156
|
console.log(` ${choice.key}) ${choice.label}`);
|
|
150
157
|
}
|
|
@@ -153,7 +160,9 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
153
160
|
const answer = await promptUser(rl, " Choice: ");
|
|
154
161
|
|
|
155
162
|
if (answer.toLowerCase() === "s" || !answer) {
|
|
156
|
-
console.log(
|
|
163
|
+
console.log(
|
|
164
|
+
"\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.",
|
|
165
|
+
);
|
|
157
166
|
console.log(" Or add credentials directly to your .env file:");
|
|
158
167
|
console.log(" OPENAI_API_KEY=...");
|
|
159
168
|
console.log(" ANTHROPIC_API_KEY=...");
|
|
@@ -251,26 +260,17 @@ function detectAgentDirs(root: string): string[] {
|
|
|
251
260
|
return dirs;
|
|
252
261
|
}
|
|
253
262
|
|
|
254
|
-
|
|
263
|
+
function copySkills(): void {
|
|
255
264
|
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
256
265
|
|
|
257
266
|
if (agentDirs.length === 0) {
|
|
258
|
-
console.log(
|
|
267
|
+
console.log(
|
|
268
|
+
"\nSkills: No .agents/ or .claude/ directory found in repo root — skipping.",
|
|
269
|
+
);
|
|
259
270
|
return;
|
|
260
271
|
}
|
|
261
272
|
|
|
262
273
|
const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
|
|
263
|
-
const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
|
|
264
|
-
// Say "Overwrite" if skills already exist in ANY target dir — skills must
|
|
265
|
-
// be identical across coding agents, so we always copy to all of them.
|
|
266
|
-
const existing = destinations.filter((d) => existsSync(d));
|
|
267
|
-
const verb = existing.length > 0 ? "Overwrite" : "Install";
|
|
268
|
-
|
|
269
|
-
const proceed = await askYesNo(`\n${verb} libretto skills in ${dirNames}?`);
|
|
270
|
-
if (!proceed) {
|
|
271
|
-
console.log(" Skipping skill copy.");
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
274
|
|
|
275
275
|
let sourceDir: string;
|
|
276
276
|
try {
|
|
@@ -289,7 +289,9 @@ async function copySkills(): Promise<void> {
|
|
|
289
289
|
}
|
|
290
290
|
cpSync(sourceDir, skillDest, { recursive: true });
|
|
291
291
|
const fileCount = readdirSync(skillDest).length;
|
|
292
|
-
console.log(
|
|
292
|
+
console.log(
|
|
293
|
+
` ✓ Copied ${fileCount} skill files to ${name}/skills/libretto/`,
|
|
294
|
+
);
|
|
293
295
|
}
|
|
294
296
|
}
|
|
295
297
|
|
|
@@ -316,10 +318,12 @@ export const initCommand = SimpleCLI.command({
|
|
|
316
318
|
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
317
319
|
}
|
|
318
320
|
|
|
321
|
+
copySkills();
|
|
322
|
+
|
|
319
323
|
if (process.stdin.isTTY) {
|
|
320
|
-
await copySkills();
|
|
321
324
|
await runInteractiveApiSetup();
|
|
322
325
|
} else {
|
|
326
|
+
loadSnapshotEnv();
|
|
323
327
|
printSnapshotApiStatus();
|
|
324
328
|
}
|
|
325
329
|
|
package/src/cli/commands/logs.ts
CHANGED
|
@@ -12,13 +12,15 @@ import {
|
|
|
12
12
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
13
|
import {
|
|
14
14
|
integerOption,
|
|
15
|
-
loadSessionStateMiddleware,
|
|
16
15
|
pageOption,
|
|
17
|
-
resolveSessionMiddleware,
|
|
18
16
|
sessionOption,
|
|
17
|
+
withRequiredSession,
|
|
19
18
|
} from "./shared.js";
|
|
20
19
|
|
|
21
|
-
async function resolvePageId(
|
|
20
|
+
async function resolvePageId(
|
|
21
|
+
session: string,
|
|
22
|
+
pageId?: string,
|
|
23
|
+
): Promise<string | undefined> {
|
|
22
24
|
if (!pageId) return undefined;
|
|
23
25
|
const pages = await withSessionLogger(session, async (logger) =>
|
|
24
26
|
listOpenPages(session, logger),
|
|
@@ -48,8 +50,7 @@ export const networkCommand = SimpleCLI.command({
|
|
|
48
50
|
description: "View captured network requests",
|
|
49
51
|
})
|
|
50
52
|
.input(networkInput)
|
|
51
|
-
.use(
|
|
52
|
-
.use(loadSessionStateMiddleware)
|
|
53
|
+
.use(withRequiredSession())
|
|
53
54
|
.handle(async ({ input, ctx }) => {
|
|
54
55
|
if (input.clear) {
|
|
55
56
|
clearNetworkLog(ctx.session);
|
|
@@ -93,8 +94,7 @@ export const actionsCommand = SimpleCLI.command({
|
|
|
93
94
|
description: "View captured actions",
|
|
94
95
|
})
|
|
95
96
|
.input(actionsInput)
|
|
96
|
-
.use(
|
|
97
|
-
.use(loadSessionStateMiddleware)
|
|
97
|
+
.use(withRequiredSession())
|
|
98
98
|
.handle(async ({ input, ctx }) => {
|
|
99
99
|
if (input.clear) {
|
|
100
100
|
clearActionLog(ctx.session);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
3
|
+
import { createLoggerForSession } from "../core/context.js";
|
|
2
4
|
import {
|
|
3
|
-
|
|
5
|
+
generateSessionName,
|
|
4
6
|
readSessionStateOrThrow,
|
|
5
7
|
type SessionState,
|
|
6
8
|
validateSessionName,
|
|
@@ -10,21 +12,8 @@ import {
|
|
|
10
12
|
type SimpleCLIMiddleware,
|
|
11
13
|
} from "../framework/simple-cli.js";
|
|
12
14
|
|
|
13
|
-
export function
|
|
14
|
-
return z.string().
|
|
15
|
-
try {
|
|
16
|
-
validateSessionName(value);
|
|
17
|
-
} catch (err) {
|
|
18
|
-
ctx.addIssue({
|
|
19
|
-
code: z.ZodIssueCode.custom,
|
|
20
|
-
message: err instanceof Error ? err.message : String(err),
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function sessionOption(help = "Use a named session") {
|
|
27
|
-
return SimpleCLI.option(createSessionSchema(), { help });
|
|
15
|
+
export function sessionOption(help = "Session name") {
|
|
16
|
+
return SimpleCLI.option(z.string().optional(), { help });
|
|
28
17
|
}
|
|
29
18
|
|
|
30
19
|
export function pageOption(help = "Target a specific page id") {
|
|
@@ -35,36 +24,46 @@ export function integerOption(help?: string) {
|
|
|
35
24
|
return SimpleCLI.option(z.coerce.number().int().optional(), { help });
|
|
36
25
|
}
|
|
37
26
|
|
|
38
|
-
export type SessionInput = {
|
|
39
|
-
session: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
27
|
export type SessionContext = {
|
|
43
28
|
session: string;
|
|
29
|
+
logger: LoggerApi;
|
|
44
30
|
};
|
|
45
31
|
|
|
46
32
|
export type SessionStateContext = SessionContext & {
|
|
47
33
|
sessionState: SessionState;
|
|
48
34
|
};
|
|
49
35
|
|
|
50
|
-
export
|
|
51
|
-
|
|
36
|
+
export function withRequiredSession(): SimpleCLIMiddleware<
|
|
37
|
+
{ session?: string },
|
|
52
38
|
{},
|
|
53
|
-
|
|
54
|
-
>
|
|
55
|
-
return {
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
SessionStateContext
|
|
40
|
+
> {
|
|
41
|
+
return async ({ input, ctx }) => {
|
|
42
|
+
if (!input.session) {
|
|
43
|
+
throw new Error("Missing required option --session.");
|
|
44
|
+
}
|
|
45
|
+
validateSessionName(input.session);
|
|
46
|
+
const logger = createLoggerForSession(input.session);
|
|
47
|
+
return {
|
|
48
|
+
...ctx,
|
|
49
|
+
session: input.session,
|
|
50
|
+
logger,
|
|
51
|
+
sessionState: readSessionStateOrThrow(input.session),
|
|
52
|
+
};
|
|
58
53
|
};
|
|
59
|
-
}
|
|
54
|
+
}
|
|
60
55
|
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
>
|
|
66
|
-
return {
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
export function withAutoSession(): SimpleCLIMiddleware<
|
|
57
|
+
{ session?: string },
|
|
58
|
+
{},
|
|
59
|
+
SessionContext
|
|
60
|
+
> {
|
|
61
|
+
return async ({ input, ctx }) => {
|
|
62
|
+
const session = input.session ?? generateSessionName();
|
|
63
|
+
if (input.session) {
|
|
64
|
+
validateSessionName(input.session);
|
|
65
|
+
}
|
|
66
|
+
const logger = createLoggerForSession(session);
|
|
67
|
+
return { ...ctx, session, logger };
|
|
69
68
|
};
|
|
70
|
-
}
|
|
69
|
+
}
|
|
@@ -10,17 +10,11 @@ import {
|
|
|
10
10
|
type ScreenshotPair,
|
|
11
11
|
} from "../core/snapshot-analyzer.js";
|
|
12
12
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
|
-
import {
|
|
14
|
-
loadSessionStateMiddleware,
|
|
15
|
-
pageOption,
|
|
16
|
-
resolveSessionMiddleware,
|
|
17
|
-
sessionOption,
|
|
18
|
-
} from "./shared.js";
|
|
13
|
+
import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
|
|
19
14
|
import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
|
|
20
15
|
import { readAiConfig } from "../core/ai-config.js";
|
|
21
16
|
import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
|
|
22
17
|
|
|
23
|
-
const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
|
|
24
18
|
const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 } as const;
|
|
25
19
|
|
|
26
20
|
function generateSnapshotRunId(): string {
|
|
@@ -38,28 +32,28 @@ function isZeroViewport(value: number | null): boolean {
|
|
|
38
32
|
return typeof value === "number" && value <= 0;
|
|
39
33
|
}
|
|
40
34
|
|
|
41
|
-
function shouldForceSnapshotViewport(
|
|
35
|
+
function shouldForceSnapshotViewport(
|
|
36
|
+
metrics: SnapshotViewportMetrics,
|
|
37
|
+
): boolean {
|
|
42
38
|
return (
|
|
43
|
-
isZeroViewport(metrics.configuredWidth)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
isZeroViewport(metrics.configuredWidth) ||
|
|
40
|
+
isZeroViewport(metrics.configuredHeight) ||
|
|
41
|
+
isZeroViewport(metrics.innerWidth) ||
|
|
42
|
+
isZeroViewport(metrics.innerHeight)
|
|
47
43
|
);
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
function isZeroWidthScreenshotError(error: unknown): boolean {
|
|
51
47
|
return (
|
|
52
|
-
error instanceof Error
|
|
53
|
-
|
|
48
|
+
error instanceof Error &&
|
|
49
|
+
error.message.includes("Cannot take screenshot with 0 width")
|
|
54
50
|
);
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
async function readSnapshotViewportMetrics(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
},
|
|
62
|
-
): Promise<SnapshotViewportMetrics> {
|
|
53
|
+
async function readSnapshotViewportMetrics(page: {
|
|
54
|
+
viewportSize(): { width: number; height: number } | null;
|
|
55
|
+
evaluate<T>(pageFunction: () => T | Promise<T>): Promise<T>;
|
|
56
|
+
}): Promise<SnapshotViewportMetrics> {
|
|
63
57
|
const configuredViewport = page.viewportSize();
|
|
64
58
|
let innerWidth: number | null = null;
|
|
65
59
|
let innerHeight: number | null = null;
|
|
@@ -161,6 +155,12 @@ async function captureScreenshot(
|
|
|
161
155
|
const htmlPath = `${snapshotRunDir}/page.html`;
|
|
162
156
|
const condensedHtmlPath = `${snapshotRunDir}/page.condensed.html`;
|
|
163
157
|
|
|
158
|
+
const RENDER_SETTLE_TIMEOUT_MS = 10_000;
|
|
159
|
+
await Promise.race([
|
|
160
|
+
page.waitForLoadState("networkidle").catch(() => {}),
|
|
161
|
+
new Promise((resolve) => setTimeout(resolve, RENDER_SETTLE_TIMEOUT_MS)),
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
164
|
const restoreViewport = resolveSnapshotViewport(session, logger);
|
|
165
165
|
const viewportMetrics = await readSnapshotViewportMetrics(page);
|
|
166
166
|
logger.info("screenshot-viewport-metrics", {
|
|
@@ -249,22 +249,15 @@ async function captureScreenshot(
|
|
|
249
249
|
async function runSnapshot(
|
|
250
250
|
session: string,
|
|
251
251
|
logger: LoggerApi,
|
|
252
|
-
pageId
|
|
253
|
-
objective
|
|
254
|
-
context
|
|
252
|
+
pageId: string | undefined,
|
|
253
|
+
objective: string,
|
|
254
|
+
context: string,
|
|
255
255
|
): Promise<void> {
|
|
256
|
-
const normalizedObjective = objective
|
|
257
|
-
const normalizedContext = context
|
|
258
|
-
if (!normalizedObjective && normalizedContext) {
|
|
259
|
-
throw new Error(
|
|
260
|
-
"Couldn't run analysis: --objective is required when providing --context.",
|
|
261
|
-
);
|
|
262
|
-
}
|
|
256
|
+
const normalizedObjective = objective.trim();
|
|
257
|
+
const normalizedContext = context.trim();
|
|
263
258
|
|
|
264
|
-
const configuredAi =
|
|
265
|
-
|
|
266
|
-
resolveSnapshotApiModelOrThrow(configuredAi);
|
|
267
|
-
}
|
|
259
|
+
const configuredAi = readAiConfig();
|
|
260
|
+
resolveSnapshotApiModelOrThrow(configuredAi);
|
|
268
261
|
|
|
269
262
|
const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
|
|
270
263
|
session,
|
|
@@ -277,15 +270,10 @@ async function runSnapshot(
|
|
|
277
270
|
console.log(` HTML: ${htmlPath}`);
|
|
278
271
|
console.log(` Condensed HTML: ${condensedHtmlPath}`);
|
|
279
272
|
|
|
280
|
-
if (!normalizedObjective) {
|
|
281
|
-
console.log("Use --objective flag to analyze snapshots.");
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
273
|
const interpretArgs: InterpretArgs = {
|
|
286
274
|
objective: normalizedObjective,
|
|
287
275
|
session,
|
|
288
|
-
context: normalizedContext
|
|
276
|
+
context: normalizedContext,
|
|
289
277
|
pngPath,
|
|
290
278
|
htmlPath,
|
|
291
279
|
condensedHtmlPath,
|
|
@@ -303,25 +291,22 @@ export const snapshotInput = SimpleCLI.input({
|
|
|
303
291
|
named: {
|
|
304
292
|
session: sessionOption(),
|
|
305
293
|
page: pageOption(),
|
|
306
|
-
objective: SimpleCLI.option(z.string()
|
|
307
|
-
context: SimpleCLI.option(z.string()
|
|
294
|
+
objective: SimpleCLI.option(z.string()),
|
|
295
|
+
context: SimpleCLI.option(z.string()),
|
|
308
296
|
},
|
|
309
297
|
});
|
|
310
298
|
|
|
311
|
-
export
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
);
|
|
326
|
-
});
|
|
327
|
-
}
|
|
299
|
+
export const snapshotCommand = SimpleCLI.command({
|
|
300
|
+
description: "Capture PNG + HTML and analyze with --objective and --context",
|
|
301
|
+
})
|
|
302
|
+
.input(snapshotInput)
|
|
303
|
+
.use(withRequiredSession())
|
|
304
|
+
.handle(async ({ input, ctx }) => {
|
|
305
|
+
await runSnapshot(
|
|
306
|
+
ctx.session,
|
|
307
|
+
ctx.logger,
|
|
308
|
+
input.page,
|
|
309
|
+
input.objective,
|
|
310
|
+
input.context,
|
|
311
|
+
);
|
|
312
|
+
});
|
|
@@ -29,11 +29,18 @@ export const ViewportConfigSchema = z.object({
|
|
|
29
29
|
});
|
|
30
30
|
export type ViewportConfig = z.infer<typeof ViewportConfigSchema>;
|
|
31
31
|
|
|
32
|
+
export const WindowPositionConfigSchema = z.object({
|
|
33
|
+
x: z.number().int(),
|
|
34
|
+
y: z.number().int(),
|
|
35
|
+
});
|
|
36
|
+
export type WindowPositionConfig = z.infer<typeof WindowPositionConfigSchema>;
|
|
37
|
+
|
|
32
38
|
export const LibrettoConfigSchema = z
|
|
33
39
|
.object({
|
|
34
40
|
version: z.literal(CURRENT_CONFIG_VERSION),
|
|
35
41
|
ai: AiConfigSchema.optional(),
|
|
36
42
|
viewport: ViewportConfigSchema.optional(),
|
|
43
|
+
windowPosition: WindowPositionConfigSchema.optional(),
|
|
37
44
|
})
|
|
38
45
|
.passthrough();
|
|
39
46
|
export type LibrettoConfig = z.infer<typeof LibrettoConfigSchema>;
|
|
@@ -51,7 +58,12 @@ const PROVIDER_ALIASES: Record<string, string> = {
|
|
|
51
58
|
google: DEFAULT_MODELS.gemini,
|
|
52
59
|
};
|
|
53
60
|
|
|
54
|
-
const CONFIGURE_PROVIDERS = [
|
|
61
|
+
const CONFIGURE_PROVIDERS = [
|
|
62
|
+
"openai",
|
|
63
|
+
"anthropic",
|
|
64
|
+
"gemini",
|
|
65
|
+
"vertex",
|
|
66
|
+
] as const;
|
|
55
67
|
|
|
56
68
|
function formatConfigureProviders(separator = " | "): string {
|
|
57
69
|
return CONFIGURE_PROVIDERS.join(separator);
|
|
@@ -75,6 +87,10 @@ function formatExpectedConfigExample(): string {
|
|
|
75
87
|
width: 1280,
|
|
76
88
|
height: 800,
|
|
77
89
|
},
|
|
90
|
+
windowPosition: {
|
|
91
|
+
x: 1600,
|
|
92
|
+
y: 120,
|
|
93
|
+
},
|
|
78
94
|
},
|
|
79
95
|
null,
|
|
80
96
|
2,
|
|
@@ -89,11 +105,13 @@ function invalidConfigError(configPath: string, detail?: string): Error {
|
|
|
89
105
|
"Expected config example:",
|
|
90
106
|
formatExpectedConfigExample(),
|
|
91
107
|
"Notes:",
|
|
92
|
-
' - "ai" and "
|
|
108
|
+
' - "ai", "viewport", and "windowPosition" are optional.',
|
|
93
109
|
' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
|
|
94
110
|
"Fix the file to match this shape, or delete it and rerun:",
|
|
95
111
|
` npx libretto ai configure ${formatConfigureProviders()}`,
|
|
96
|
-
]
|
|
112
|
+
]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join("\n"),
|
|
97
115
|
);
|
|
98
116
|
}
|
|
99
117
|
|
|
@@ -220,7 +238,9 @@ export function runAiConfigure(
|
|
|
220
238
|
console.log(
|
|
221
239
|
`No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
|
|
222
240
|
);
|
|
223
|
-
console.log(
|
|
241
|
+
console.log(
|
|
242
|
+
"Provider credentials still come from your shell or .env file.",
|
|
243
|
+
);
|
|
224
244
|
return;
|
|
225
245
|
}
|
|
226
246
|
printAiConfig(config, configPath);
|
|
@@ -10,7 +10,6 @@ import { readFileSync } from "node:fs";
|
|
|
10
10
|
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
11
11
|
import { createLLMClient } from "../../shared/llm/client.js";
|
|
12
12
|
import {
|
|
13
|
-
formatInterpretationOutput,
|
|
14
13
|
InterpretResultSchema,
|
|
15
14
|
buildInlinePromptSelection,
|
|
16
15
|
getMimeType,
|
|
@@ -19,9 +18,7 @@ import {
|
|
|
19
18
|
type InterpretArgs,
|
|
20
19
|
} from "./snapshot-analyzer.js";
|
|
21
20
|
import { readAiConfig, type AiConfig } from "./ai-config.js";
|
|
22
|
-
import {
|
|
23
|
-
resolveSnapshotApiModelOrThrow,
|
|
24
|
-
} from "./snapshot-api-config.js";
|
|
21
|
+
import { resolveSnapshotApiModelOrThrow } from "./snapshot-api-config.js";
|
|
25
22
|
|
|
26
23
|
export async function runApiInterpret(
|
|
27
24
|
args: InterpretArgs,
|
|
@@ -52,7 +49,8 @@ export async function runApiInterpret(
|
|
|
52
49
|
logger.info("api-interpret-dom-selection", {
|
|
53
50
|
configuredModel: promptSelection.stats.configuredModel,
|
|
54
51
|
fullDomEstimatedTokens: promptSelection.stats.fullDomEstimatedTokens,
|
|
55
|
-
condensedDomEstimatedTokens:
|
|
52
|
+
condensedDomEstimatedTokens:
|
|
53
|
+
promptSelection.stats.condensedDomEstimatedTokens,
|
|
56
54
|
contextWindowTokens: promptSelection.budget.contextWindowTokens,
|
|
57
55
|
promptBudgetTokens: promptSelection.budget.promptBudgetTokens,
|
|
58
56
|
selectedDom: promptSelection.domSource,
|
|
@@ -93,5 +91,18 @@ export async function runApiInterpret(
|
|
|
93
91
|
answer: parsed.answer.slice(0, 200),
|
|
94
92
|
});
|
|
95
93
|
|
|
96
|
-
console.log(
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log("Analysis:");
|
|
96
|
+
console.log(parsed.answer);
|
|
97
|
+
if (parsed.selectors.length > 0) {
|
|
98
|
+
console.log("");
|
|
99
|
+
console.log("Selectors:");
|
|
100
|
+
parsed.selectors.forEach((selector, index) => {
|
|
101
|
+
console.log(` ${index + 1}. ${selector.label}: ${selector.selector}`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (parsed.notes?.trim()) {
|
|
105
|
+
console.log("");
|
|
106
|
+
console.log(`Notes: ${parsed.notes.trim()}`);
|
|
107
|
+
}
|
|
97
108
|
}
|