libretto 0.6.2 → 0.6.4
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 +28 -49
- package/dist/cli/commands/ai.js +15 -16
- package/dist/cli/commands/browser.js +6 -2
- package/dist/cli/commands/deploy.js +16 -3
- package/dist/cli/commands/setup.js +20 -69
- package/dist/cli/commands/snapshot.js +4 -4
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/core/ai-model.js +14 -14
- package/dist/cli/core/api-snapshot-analyzer.js +3 -3
- package/dist/cli/core/config.js +17 -29
- package/dist/cli/framework/simple-cli.js +6 -3
- package/package.json +2 -1
- package/skills/libretto/SKILL.md +1 -1
- package/skills/libretto/references/configuration-file-reference.md +3 -6
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +29 -48
- package/src/cli/commands/ai.ts +15 -17
- package/src/cli/commands/browser.ts +6 -2
- package/src/cli/commands/deploy.ts +18 -3
- package/src/cli/commands/setup.ts +19 -91
- package/src/cli/commands/snapshot.ts +4 -4
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/core/ai-model.ts +22 -22
- package/src/cli/core/api-snapshot-analyzer.ts +3 -3
- package/src/cli/core/config.ts +15 -40
- package/src/cli/framework/simple-cli.ts +6 -3
|
@@ -19,7 +19,7 @@ Libretto reads workspace config from `.libretto/config.json`.
|
|
|
19
19
|
|
|
20
20
|
## Supported Settings
|
|
21
21
|
|
|
22
|
-
- `
|
|
22
|
+
- `snapshotModel` selects the configured analysis model for `snapshot`.
|
|
23
23
|
- `viewport` is an optional top-level setting used by `open` and `run` when you do not pass `--viewport`.
|
|
24
24
|
- Viewport precedence is: CLI `--viewport`, then `.libretto/config.json`, then the default `1366x768`.
|
|
25
25
|
- `sessionMode` sets the default session access mode for new sessions created by `open`, `connect`, and `run`. Must be `"read-only"` or `"write-access"`. When omitted, defaults to `"write-access"`. Pass `--read-only` or `--write-access` to `open`, `connect`, or `run` to override when creating a session.
|
|
@@ -29,10 +29,7 @@ Example:
|
|
|
29
29
|
```json
|
|
30
30
|
{
|
|
31
31
|
"version": 1,
|
|
32
|
-
"
|
|
33
|
-
"model": "openai/gpt-5.4",
|
|
34
|
-
"updatedAt": "2026-01-01T00:00:00.000Z"
|
|
35
|
-
},
|
|
32
|
+
"snapshotModel": "openai/gpt-5.4",
|
|
36
33
|
"viewport": {
|
|
37
34
|
"width": 1280,
|
|
38
35
|
"height": 800
|
|
@@ -47,7 +44,7 @@ Example:
|
|
|
47
44
|
npx libretto setup # first-time onboarding, auto-pins default model
|
|
48
45
|
npx libretto status # inspect AI config and open sessions
|
|
49
46
|
npx libretto ai configure openai # explicitly change provider/model
|
|
50
|
-
npx libretto open https://
|
|
47
|
+
npx libretto open https://example.com --viewport 1440x900
|
|
51
48
|
npx libretto run ./integration.ts --viewport 1440x900
|
|
52
49
|
```
|
|
53
50
|
|
package/src/cli/cli.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { resolveAiSetupStatus } from "./core/ai-model.js";
|
|
1
2
|
import { ensureLibrettoSetup } from "./core/context.js";
|
|
2
3
|
import { createCLIApp } from "./router.js";
|
|
4
|
+
import { warnIfInstalledSkillOutOfDate } from "./core/skill-version.js";
|
|
3
5
|
|
|
4
6
|
function renderUsage(app: ReturnType<typeof createCLIApp>): string {
|
|
5
7
|
return `${app.renderHelp()}
|
|
@@ -7,56 +9,34 @@ function renderUsage(app: ReturnType<typeof createCLIApp>): string {
|
|
|
7
9
|
Options:
|
|
8
10
|
--session <name> Use a named session (auto-generated for open/run if omitted)
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# ... manually log in ...
|
|
14
|
-
libretto save linkedin.com
|
|
15
|
-
# Next time you open linkedin.com, you'll be logged in automatically
|
|
16
|
-
|
|
17
|
-
libretto exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
|
|
18
|
-
libretto exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
|
|
19
|
-
libretto readonly-exec "return await page.title()" --session test1
|
|
20
|
-
libretto connect http://127.0.0.1:9222 --read-only --session test1
|
|
21
|
-
libretto run ./integration.ts --read-only --session test1
|
|
22
|
-
libretto status
|
|
23
|
-
libretto ai configure openai
|
|
24
|
-
libretto ai configure anthropic
|
|
25
|
-
libretto ai configure gemini
|
|
26
|
-
libretto ai configure vertex
|
|
27
|
-
libretto ai configure openai/gpt-4o
|
|
28
|
-
libretto snapshot
|
|
29
|
-
libretto snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
30
|
-
libretto resume --session my-session
|
|
31
|
-
libretto close
|
|
32
|
-
libretto close --all
|
|
33
|
-
libretto close --all --force
|
|
34
|
-
|
|
35
|
-
# Multiple sessions
|
|
36
|
-
libretto open https://site1.com --session test1
|
|
37
|
-
libretto open https://site2.com --session test2
|
|
38
|
-
libretto exec "return await page.title()" --session test1
|
|
39
|
-
|
|
40
|
-
Available in exec:
|
|
41
|
-
page, context, state, browser, networkLog, actionLog
|
|
42
|
-
|
|
43
|
-
Available in readonly-exec:
|
|
44
|
-
page, state, snapshot, scrollBy, get
|
|
12
|
+
Docs (agent-friendly): https://libretto.sh/docs
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
45
15
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
They persist cookies, localStorage, and session data across browser launches.
|
|
49
|
-
Local profiles are machine-local and are not shared with other users/environments.
|
|
50
|
-
Sessions can expire; if run fails auth, log in again and re-save the profile.
|
|
16
|
+
function printSetupAudit(): void {
|
|
17
|
+
warnIfInstalledSkillOutOfDate();
|
|
51
18
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
19
|
+
const status = resolveAiSetupStatus();
|
|
20
|
+
switch (status.kind) {
|
|
21
|
+
case "ready":
|
|
22
|
+
console.log(`✓ Snapshot model: ${status.model}`);
|
|
23
|
+
break;
|
|
24
|
+
case "configured-missing-credentials":
|
|
25
|
+
console.log(
|
|
26
|
+
`✗ ${status.provider} configured (model: ${status.model}), but credentials are missing. Run \`npx libretto setup\` to repair.`,
|
|
27
|
+
);
|
|
28
|
+
break;
|
|
29
|
+
case "invalid-config":
|
|
30
|
+
console.log(
|
|
31
|
+
`✗ AI config is invalid. Run \`npx libretto setup\` to reconfigure.`,
|
|
32
|
+
);
|
|
33
|
+
break;
|
|
34
|
+
case "unconfigured":
|
|
35
|
+
console.log(
|
|
36
|
+
`✗ No AI model configured. Run \`npx libretto setup\` or \`npx libretto ai configure\` to set up.`,
|
|
37
|
+
);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
60
40
|
}
|
|
61
41
|
|
|
62
42
|
function isRootHelpRequest(rawArgs: readonly string[]): boolean {
|
|
@@ -74,6 +54,7 @@ export async function runLibrettoCLI(): Promise<void> {
|
|
|
74
54
|
try {
|
|
75
55
|
if (isRootHelpRequest(rawArgs)) {
|
|
76
56
|
console.log(renderUsage(app));
|
|
57
|
+
printSetupAudit();
|
|
77
58
|
return;
|
|
78
59
|
}
|
|
79
60
|
|
package/src/cli/commands/ai.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import {
|
|
3
3
|
CURRENT_CONFIG_VERSION,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type AiConfig,
|
|
4
|
+
readSnapshotModel,
|
|
5
|
+
writeSnapshotModel,
|
|
6
|
+
clearSnapshotModel,
|
|
8
7
|
} from "../core/config.js";
|
|
9
8
|
import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
|
|
10
9
|
import { DEFAULT_SNAPSHOT_MODELS } from "../core/ai-model.js";
|
|
@@ -27,10 +26,9 @@ function formatConfigureProviders(separator = " | "): string {
|
|
|
27
26
|
return CONFIGURE_PROVIDERS.join(separator);
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
function
|
|
31
|
-
console.log(`
|
|
29
|
+
function printSnapshotModelConfig(model: string, configPath: string): void {
|
|
30
|
+
console.log(`Snapshot model: ${model}`);
|
|
32
31
|
console.log(`Config file: ${configPath}`);
|
|
33
|
-
console.log(`Updated at: ${config.updatedAt}`);
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
/**
|
|
@@ -71,26 +69,26 @@ export function runAiConfigure(
|
|
|
71
69
|
const presetArg = input.preset?.trim();
|
|
72
70
|
|
|
73
71
|
if (!presetArg && !input.clear) {
|
|
74
|
-
const
|
|
75
|
-
if (!
|
|
72
|
+
const model = readSnapshotModel(configPath);
|
|
73
|
+
if (!model) {
|
|
76
74
|
console.log(
|
|
77
|
-
`No
|
|
75
|
+
`No snapshot model set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
|
|
78
76
|
);
|
|
79
77
|
console.log(
|
|
80
78
|
"Provider credentials still come from your shell or .env file.",
|
|
81
79
|
);
|
|
82
80
|
return;
|
|
83
81
|
}
|
|
84
|
-
|
|
82
|
+
printSnapshotModelConfig(model, configPath);
|
|
85
83
|
return;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
if (input.clear) {
|
|
89
|
-
const removed =
|
|
87
|
+
const removed = clearSnapshotModel(configPath);
|
|
90
88
|
if (removed) {
|
|
91
|
-
console.log(`Cleared
|
|
89
|
+
console.log(`Cleared snapshot model config: ${configPath}`);
|
|
92
90
|
} else {
|
|
93
|
-
console.log("No
|
|
91
|
+
console.log("No snapshot model was set.");
|
|
94
92
|
}
|
|
95
93
|
return;
|
|
96
94
|
}
|
|
@@ -107,9 +105,9 @@ export function runAiConfigure(
|
|
|
107
105
|
);
|
|
108
106
|
}
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
console.log("
|
|
112
|
-
|
|
108
|
+
writeSnapshotModel(model, configPath);
|
|
109
|
+
console.log("Snapshot model saved.");
|
|
110
|
+
printSnapshotModelConfig(model, configPath);
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
export const aiConfigureInput = SimpleCLI.input({
|
|
@@ -242,7 +242,11 @@ export const sessionModeCommand = SimpleCLI.command({
|
|
|
242
242
|
});
|
|
243
243
|
|
|
244
244
|
export const closeInput = SimpleCLI.input({
|
|
245
|
-
positionals: [
|
|
245
|
+
positionals: [
|
|
246
|
+
SimpleCLI.positional("session", z.string().optional(), {
|
|
247
|
+
help: "Session name to close",
|
|
248
|
+
}),
|
|
249
|
+
],
|
|
246
250
|
named: {
|
|
247
251
|
session: sessionOption(),
|
|
248
252
|
all: SimpleCLI.flag({
|
|
@@ -254,7 +258,7 @@ export const closeInput = SimpleCLI.input({
|
|
|
254
258
|
},
|
|
255
259
|
}).refine(
|
|
256
260
|
(input) => input.all || input.session,
|
|
257
|
-
`Usage: libretto close
|
|
261
|
+
`Usage: libretto close <session>\nUsage: libretto close --all [--force]`,
|
|
258
262
|
);
|
|
259
263
|
|
|
260
264
|
export const closeCommand = SimpleCLI.command({
|
|
@@ -60,23 +60,32 @@ async function pollDeployment(
|
|
|
60
60
|
maxWaitMs: number,
|
|
61
61
|
): Promise<DeploymentResponse["json"]> {
|
|
62
62
|
const start = Date.now();
|
|
63
|
+
const workflowWaitMs = 60_000;
|
|
63
64
|
let status: DeploymentStatus = "building";
|
|
65
|
+
let workflows: string[] | null | undefined = null;
|
|
66
|
+
let readyAt: number | null = null;
|
|
64
67
|
let deployment: DeploymentResponse["json"] | undefined;
|
|
65
68
|
|
|
66
|
-
while (
|
|
69
|
+
while (Date.now() - start < maxWaitMs) {
|
|
70
|
+
if (status !== "building" && status !== "ready") break;
|
|
71
|
+
if (status === "ready" && workflows?.length) break;
|
|
72
|
+
if (status === "ready" && readyAt && Date.now() - readyAt > workflowWaitMs) break;
|
|
73
|
+
|
|
67
74
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
68
75
|
|
|
69
|
-
const res = await postJson(apiUrl, apiKey, "/v1/deployments/
|
|
76
|
+
const res = await postJson(apiUrl, apiKey, "/v1/deployments/sync", {
|
|
70
77
|
id: deploymentId,
|
|
71
78
|
});
|
|
72
79
|
const body = (await res.json()) as DeploymentResponse;
|
|
73
80
|
if (res.status !== 200) {
|
|
74
81
|
throw new Error(
|
|
75
|
-
`Failed to
|
|
82
|
+
`Failed to sync deployment status (${res.status}): ${JSON.stringify(body)}`,
|
|
76
83
|
);
|
|
77
84
|
}
|
|
78
85
|
status = body.json.status;
|
|
86
|
+
workflows = body.json.workflows;
|
|
79
87
|
deployment = body.json;
|
|
88
|
+
if (status === "ready" && readyAt === null) readyAt = Date.now();
|
|
80
89
|
process.stdout.write(".");
|
|
81
90
|
}
|
|
82
91
|
console.log();
|
|
@@ -85,6 +94,12 @@ async function pollDeployment(
|
|
|
85
94
|
throw new Error("Deployment timed out before receiving a status update.");
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
if (status === "ready" && !workflows?.length) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Build completed but workflow discovery failed due to a server-side error. Please redeploy.",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
return deployment;
|
|
89
104
|
}
|
|
90
105
|
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import {
|
|
3
|
-
appendFileSync,
|
|
4
3
|
cpSync,
|
|
5
4
|
existsSync,
|
|
6
5
|
readdirSync,
|
|
7
|
-
readFileSync,
|
|
8
6
|
rmSync,
|
|
9
|
-
writeFileSync,
|
|
10
7
|
} from "node:fs";
|
|
11
8
|
import { spawnSync } from "node:child_process";
|
|
12
9
|
import { basename, dirname, join } from "node:path";
|
|
13
10
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import {
|
|
11
|
+
import { writeSnapshotModel } from "../core/config.js";
|
|
15
12
|
import {
|
|
16
13
|
ensureLibrettoSetup,
|
|
17
14
|
LIBRETTO_CONFIG_PATH,
|
|
@@ -20,7 +17,6 @@ import {
|
|
|
20
17
|
import {
|
|
21
18
|
type AiSetupStatus,
|
|
22
19
|
DEFAULT_SNAPSHOT_MODELS,
|
|
23
|
-
loadSnapshotEnv,
|
|
24
20
|
resolveAiSetupStatus,
|
|
25
21
|
} from "../core/ai-model.js";
|
|
26
22
|
import type { Provider } from "../core/resolve-model.js";
|
|
@@ -97,7 +93,7 @@ function ensurePinnedDefaultModel(
|
|
|
97
93
|
status: AiSetupStatus & { kind: "ready" },
|
|
98
94
|
): AiSetupStatus & { kind: "ready" } {
|
|
99
95
|
if (status.source !== "config") {
|
|
100
|
-
|
|
96
|
+
writeSnapshotModel(status.model);
|
|
101
97
|
return { ...status, source: "config" as const };
|
|
102
98
|
}
|
|
103
99
|
return status;
|
|
@@ -127,10 +123,7 @@ function printInvalidAiConfigWarning(status: AiSetupStatus): void {
|
|
|
127
123
|
|
|
128
124
|
// ── Repair plan helpers (exported for testing) ──────────────────────────────
|
|
129
125
|
|
|
130
|
-
export type RepairChoice =
|
|
131
|
-
| "enter-matching-credential"
|
|
132
|
-
| "switch-provider"
|
|
133
|
-
| "skip";
|
|
126
|
+
export type RepairChoice = "switch-provider" | "skip";
|
|
134
127
|
|
|
135
128
|
export type RepairPlan =
|
|
136
129
|
| {
|
|
@@ -155,7 +148,7 @@ export function buildRepairPlan(status: AiSetupStatus): RepairPlan {
|
|
|
155
148
|
provider: status.provider,
|
|
156
149
|
model: status.model,
|
|
157
150
|
envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
|
|
158
|
-
choices: ["
|
|
151
|
+
choices: ["switch-provider", "skip"],
|
|
159
152
|
};
|
|
160
153
|
}
|
|
161
154
|
if (status.kind === "invalid-config") {
|
|
@@ -223,72 +216,13 @@ function printSnapshotApiStatus(): boolean {
|
|
|
223
216
|
}
|
|
224
217
|
|
|
225
218
|
/**
|
|
226
|
-
*
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
let envContent = "";
|
|
230
|
-
if (existsSync(envPath)) {
|
|
231
|
-
envContent = readFileSync(envPath, "utf-8");
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const envLine = `${envVar}=${value}`;
|
|
235
|
-
if (envContent.includes(`${envVar}=`)) {
|
|
236
|
-
const updated = envContent.replace(
|
|
237
|
-
new RegExp(`^${envVar}=.*$`, "m"),
|
|
238
|
-
() => envLine,
|
|
239
|
-
);
|
|
240
|
-
writeFileSync(envPath, updated);
|
|
241
|
-
console.log(`\n✓ Updated ${envVar} in ${envPath}`);
|
|
242
|
-
} else {
|
|
243
|
-
const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
|
|
244
|
-
appendFileSync(envPath, `${separator}${envLine}\n`);
|
|
245
|
-
console.log(`\n✓ Added ${envVar} to ${envPath}`);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
process.env[envVar] = value;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Prompt the user to enter a credential for a specific provider and pin its model.
|
|
253
|
-
* When modelOverride is provided (e.g. during repair), preserves the existing model
|
|
254
|
-
* instead of resetting to the provider default.
|
|
255
|
-
* Returns true if credential was entered successfully.
|
|
256
|
-
*/
|
|
257
|
-
async function promptForCredential(
|
|
258
|
-
rl: ReturnType<typeof createInterface>,
|
|
259
|
-
choice: ProviderChoice,
|
|
260
|
-
envPath: string,
|
|
261
|
-
modelOverride?: string,
|
|
262
|
-
): Promise<boolean> {
|
|
263
|
-
console.log(`\n${choice.label} selected.`);
|
|
264
|
-
console.log(`${choice.envHint}\n`);
|
|
265
|
-
|
|
266
|
-
const apiKeyValue = await promptUser(rl, `Enter your ${choice.envVar}: `);
|
|
267
|
-
|
|
268
|
-
if (!apiKeyValue) {
|
|
269
|
-
console.log("\nNo value entered. Skipping API key setup.");
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
writeEnvVar(choice.envVar, apiKeyValue, envPath);
|
|
274
|
-
loadSnapshotEnv();
|
|
275
|
-
|
|
276
|
-
const model = modelOverride ?? DEFAULT_SNAPSHOT_MODELS[choice.provider];
|
|
277
|
-
writeAiConfig(model);
|
|
278
|
-
console.log(`✓ Snapshot API ready: ${model}`);
|
|
279
|
-
console.log(
|
|
280
|
-
"To change: npx libretto ai configure openai | anthropic | gemini | vertex",
|
|
281
|
-
);
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Run the full provider selection menu and credential entry.
|
|
219
|
+
* Run the full provider selection menu.
|
|
220
|
+
* Pins the selected provider's default model to config and prints
|
|
221
|
+
* instructions for the user to add the credential to .env themselves.
|
|
287
222
|
* Returns true if a provider was successfully configured.
|
|
288
223
|
*/
|
|
289
224
|
async function promptProviderSelection(
|
|
290
225
|
rl: ReturnType<typeof createInterface>,
|
|
291
|
-
envPath: string,
|
|
292
226
|
): Promise<boolean> {
|
|
293
227
|
console.log(
|
|
294
228
|
"Which model provider would you like to use for snapshot analysis?\n",
|
|
@@ -311,7 +245,12 @@ async function promptProviderSelection(
|
|
|
311
245
|
return false;
|
|
312
246
|
}
|
|
313
247
|
|
|
314
|
-
|
|
248
|
+
const model = DEFAULT_SNAPSHOT_MODELS[selected.provider];
|
|
249
|
+
writeSnapshotModel(model);
|
|
250
|
+
console.log(`\n✓ ${selected.label} selected (model: ${model}).`);
|
|
251
|
+
console.log(`\nAdd ${selected.envVar} to your .env file:`);
|
|
252
|
+
console.log(` ${selected.envHint}`);
|
|
253
|
+
return true;
|
|
315
254
|
}
|
|
316
255
|
|
|
317
256
|
function printSkipMessage(): void {
|
|
@@ -329,7 +268,6 @@ function printSkipMessage(): void {
|
|
|
329
268
|
|
|
330
269
|
async function runInteractiveApiSetup(): Promise<void> {
|
|
331
270
|
const status = resolveAiSetupStatus();
|
|
332
|
-
const envPath = join(REPO_ROOT, ".env");
|
|
333
271
|
|
|
334
272
|
console.log(
|
|
335
273
|
"\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
|
|
@@ -353,26 +291,16 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
353
291
|
// ── Repair: configured provider with missing credentials ──
|
|
354
292
|
if (plan.kind === "repair-missing-credentials") {
|
|
355
293
|
console.log(formatMissingCredentialsMessage(plan));
|
|
294
|
+
console.log(`\nAdd ${plan.envVar} to your .env file to fix this.`);
|
|
356
295
|
console.log("");
|
|
357
|
-
console.log("
|
|
358
|
-
console.log(
|
|
359
|
-
console.log(" 2) Switch to a different provider");
|
|
296
|
+
console.log("Or switch to a different provider:\n");
|
|
297
|
+
console.log(" 1) Switch to a different provider");
|
|
360
298
|
console.log(" s) Skip for now\n");
|
|
361
299
|
|
|
362
300
|
const answer = await promptUser(rl, "Choice: ");
|
|
363
301
|
|
|
364
302
|
if (answer === "1") {
|
|
365
|
-
|
|
366
|
-
(c) => c.provider === plan.provider,
|
|
367
|
-
);
|
|
368
|
-
if (matchingChoice) {
|
|
369
|
-
await promptForCredential(rl, matchingChoice, envPath, plan.model);
|
|
370
|
-
}
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (answer === "2") {
|
|
375
|
-
await promptProviderSelection(rl, envPath);
|
|
303
|
+
await promptProviderSelection(rl);
|
|
376
304
|
return;
|
|
377
305
|
}
|
|
378
306
|
|
|
@@ -387,13 +315,13 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
387
315
|
console.log(
|
|
388
316
|
"\nWould you like to reconfigure with a fresh provider selection?\n",
|
|
389
317
|
);
|
|
390
|
-
await promptProviderSelection(rl
|
|
318
|
+
await promptProviderSelection(rl);
|
|
391
319
|
return;
|
|
392
320
|
}
|
|
393
321
|
|
|
394
322
|
// ── Unconfigured: standard first-run flow ──
|
|
395
323
|
console.log("✗ No snapshot API credentials detected.\n");
|
|
396
|
-
await promptProviderSelection(rl
|
|
324
|
+
await promptProviderSelection(rl);
|
|
397
325
|
} finally {
|
|
398
326
|
rl.close();
|
|
399
327
|
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
13
|
import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
|
|
14
14
|
import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
|
|
15
|
-
import {
|
|
15
|
+
import { readSnapshotModel } from "../core/config.js";
|
|
16
16
|
import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
|
|
17
17
|
|
|
18
18
|
const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 } as const;
|
|
@@ -256,8 +256,8 @@ async function runSnapshot(
|
|
|
256
256
|
const normalizedObjective = objective.trim();
|
|
257
257
|
const normalizedContext = context.trim();
|
|
258
258
|
|
|
259
|
-
const
|
|
260
|
-
resolveSnapshotApiModelOrThrow(
|
|
259
|
+
const snapshotModel = readSnapshotModel();
|
|
260
|
+
resolveSnapshotApiModelOrThrow(snapshotModel);
|
|
261
261
|
|
|
262
262
|
const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
|
|
263
263
|
session,
|
|
@@ -283,7 +283,7 @@ async function runSnapshot(
|
|
|
283
283
|
// The legacy CLI-agent path (spawning codex/claude/gemini as a subprocess) is preserved
|
|
284
284
|
// in snapshot-analyzer.ts — to switch back, replace this call with:
|
|
285
285
|
// await runInterpret(interpretArgs, logger);
|
|
286
|
-
await runApiInterpret(interpretArgs, logger,
|
|
286
|
+
await runApiInterpret(interpretArgs, logger, snapshotModel);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
export const snapshotInput = SimpleCLI.input({
|
|
@@ -10,7 +10,7 @@ function printAiStatus(status: AiSetupStatus): void {
|
|
|
10
10
|
|
|
11
11
|
switch (status.kind) {
|
|
12
12
|
case "ready":
|
|
13
|
-
console.log(` ✓
|
|
13
|
+
console.log(` ✓ Snapshot model: ${status.model}`);
|
|
14
14
|
if (status.source === "config") {
|
|
15
15
|
console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
|
|
16
16
|
} else {
|
package/src/cli/core/ai-model.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { readSnapshotModel } from "./config.js";
|
|
4
4
|
import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
|
|
5
5
|
import {
|
|
6
6
|
hasProviderCredentials,
|
|
@@ -221,18 +221,18 @@ function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
|
|
|
221
221
|
* Resolve which API model to use for snapshot analysis.
|
|
222
222
|
*
|
|
223
223
|
* Priority:
|
|
224
|
-
* 1.
|
|
224
|
+
* 1. snapshotModel from .libretto/config.json (set via `ai configure`)
|
|
225
225
|
* 2. Auto-detect from available API credentials in env
|
|
226
226
|
*/
|
|
227
227
|
export function resolveSnapshotApiModel(
|
|
228
|
-
|
|
228
|
+
snapshotModel: string | null = readSnapshotModel(),
|
|
229
229
|
): SnapshotApiModelSelection | null {
|
|
230
230
|
loadSnapshotEnv();
|
|
231
231
|
|
|
232
|
-
if (
|
|
233
|
-
const { provider } = parseModel(
|
|
232
|
+
if (snapshotModel) {
|
|
233
|
+
const { provider } = parseModel(snapshotModel);
|
|
234
234
|
return {
|
|
235
|
-
model:
|
|
235
|
+
model: snapshotModel,
|
|
236
236
|
provider,
|
|
237
237
|
source: "config",
|
|
238
238
|
};
|
|
@@ -242,9 +242,9 @@ export function resolveSnapshotApiModel(
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
export function resolveSnapshotApiModelOrThrow(
|
|
245
|
-
|
|
245
|
+
snapshotModel: string | null = readSnapshotModel(),
|
|
246
246
|
): SnapshotApiModelSelection {
|
|
247
|
-
const selection = resolveSnapshotApiModel(
|
|
247
|
+
const selection = resolveSnapshotApiModel(snapshotModel);
|
|
248
248
|
if (!selection) {
|
|
249
249
|
throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
|
|
250
250
|
}
|
|
@@ -288,14 +288,14 @@ export type AiSetupStatus =
|
|
|
288
288
|
| { kind: "unconfigured" };
|
|
289
289
|
|
|
290
290
|
/**
|
|
291
|
-
* Read
|
|
292
|
-
* Returns the
|
|
291
|
+
* Read snapshot model without throwing on invalid files.
|
|
292
|
+
* Returns the model string or an error message.
|
|
293
293
|
*/
|
|
294
|
-
function
|
|
294
|
+
function readSnapshotModelSafely(
|
|
295
295
|
configPath: string,
|
|
296
|
-
): { ok: true;
|
|
296
|
+
): { ok: true; model: string | null } | { ok: false; message: string } {
|
|
297
297
|
try {
|
|
298
|
-
return { ok: true,
|
|
298
|
+
return { ok: true, model: readSnapshotModel(configPath) };
|
|
299
299
|
} catch (err) {
|
|
300
300
|
return {
|
|
301
301
|
ok: false,
|
|
@@ -312,25 +312,25 @@ function readAiConfigSafely(
|
|
|
312
312
|
* that the throwing APIs collapse into errors.
|
|
313
313
|
*
|
|
314
314
|
* 1. If config read throws → `invalid-config`.
|
|
315
|
-
* 2. If config has
|
|
316
|
-
* 3. If no
|
|
315
|
+
* 2. If config has a `snapshotModel` → check credentials for that provider.
|
|
316
|
+
* 3. If no `snapshotModel` → auto-detect from env via existing resolver.
|
|
317
317
|
*/
|
|
318
318
|
export function resolveAiSetupStatus(
|
|
319
319
|
configPath: string = LIBRETTO_CONFIG_PATH,
|
|
320
320
|
): AiSetupStatus {
|
|
321
321
|
loadSnapshotEnv();
|
|
322
322
|
|
|
323
|
-
const
|
|
323
|
+
const result = readSnapshotModelSafely(configPath);
|
|
324
324
|
|
|
325
|
-
if (!
|
|
326
|
-
return { kind: "invalid-config", message:
|
|
325
|
+
if (!result.ok) {
|
|
326
|
+
return { kind: "invalid-config", message: result.message };
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
// Config
|
|
330
|
-
if (
|
|
329
|
+
// Config has a snapshotModel — use it directly to check credentials
|
|
330
|
+
if (result.model) {
|
|
331
331
|
let selection: SnapshotApiModelSelection | null;
|
|
332
332
|
try {
|
|
333
|
-
selection = resolveSnapshotApiModel(
|
|
333
|
+
selection = resolveSnapshotApiModel(result.model);
|
|
334
334
|
} catch (err) {
|
|
335
335
|
return {
|
|
336
336
|
kind: "invalid-config",
|
|
@@ -356,7 +356,7 @@ export function resolveAiSetupStatus(
|
|
|
356
356
|
};
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
// No
|
|
359
|
+
// No snapshotModel — fall back to env auto-detect via existing resolver
|
|
360
360
|
const envSelection = resolveSnapshotApiModel(null);
|
|
361
361
|
if (envSelection && hasProviderCredentials(envSelection.provider)) {
|
|
362
362
|
return {
|
|
@@ -18,15 +18,15 @@ import {
|
|
|
18
18
|
type InterpretResult,
|
|
19
19
|
type InterpretArgs,
|
|
20
20
|
} from "./snapshot-analyzer.js";
|
|
21
|
-
import {
|
|
21
|
+
import { readSnapshotModel } from "./config.js";
|
|
22
22
|
import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
|
|
23
23
|
|
|
24
24
|
export async function runApiInterpret(
|
|
25
25
|
args: InterpretArgs,
|
|
26
26
|
logger: LoggerApi,
|
|
27
|
-
|
|
27
|
+
snapshotModel: string | null = readSnapshotModel(),
|
|
28
28
|
): Promise<void> {
|
|
29
|
-
const selection = resolveSnapshotApiModelOrThrow(
|
|
29
|
+
const selection = resolveSnapshotApiModelOrThrow(snapshotModel);
|
|
30
30
|
|
|
31
31
|
logger.info("api-interpret-start", {
|
|
32
32
|
objective: args.objective,
|