libretto 0.5.5 → 0.6.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/README.md +23 -10
- package/README.template.md +23 -10
- package/dist/cli/cli.js +10 -0
- package/dist/cli/commands/ai.js +77 -2
- package/dist/cli/commands/browser.js +98 -8
- package/dist/cli/commands/execution.js +152 -56
- package/dist/cli/commands/setup.js +390 -0
- package/dist/cli/commands/snapshot.js +2 -2
- package/dist/cli/commands/status.js +62 -0
- package/dist/cli/core/{snapshot-api-config.js → ai-model.js} +81 -7
- package/dist/cli/core/api-snapshot-analyzer.js +7 -5
- package/dist/cli/core/browser.js +202 -36
- package/dist/cli/core/{ai-config.js → config.js} +14 -79
- package/dist/cli/core/context.js +1 -25
- package/dist/cli/core/deploy-artifact.js +121 -61
- package/dist/cli/core/providers/browserbase.js +53 -0
- package/dist/cli/core/providers/index.js +48 -0
- package/dist/cli/core/providers/kernel.js +46 -0
- package/dist/cli/core/providers/libretto-cloud.js +58 -0
- package/dist/cli/core/readonly-exec.js +231 -0
- package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
- package/dist/cli/core/session.js +53 -0
- package/dist/cli/core/skill-version.js +73 -0
- package/dist/cli/core/telemetry.js +1 -54
- package/dist/cli/index.js +1 -7
- package/dist/cli/router.js +4 -4
- package/dist/cli/workers/run-integration-runtime.js +19 -13
- package/dist/cli/workers/run-integration-worker-protocol.js +5 -2
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -2
- package/dist/runtime/extract/extract.d.ts +2 -2
- package/dist/runtime/extract/extract.js +4 -2
- package/dist/runtime/extract/index.d.ts +1 -1
- package/dist/runtime/recovery/agent.d.ts +2 -3
- package/dist/runtime/recovery/agent.js +5 -3
- package/dist/runtime/recovery/errors.d.ts +2 -3
- package/dist/runtime/recovery/errors.js +4 -2
- package/dist/runtime/recovery/index.d.ts +1 -2
- package/dist/runtime/recovery/recovery.d.ts +2 -3
- package/dist/runtime/recovery/recovery.js +3 -3
- package/dist/shared/debug/pause.js +4 -21
- package/dist/shared/run/api.d.ts +2 -0
- package/dist/shared/run/browser.d.ts +9 -1
- package/dist/shared/run/browser.js +43 -3
- package/dist/shared/state/index.d.ts +1 -1
- package/dist/shared/state/index.js +2 -0
- package/dist/shared/state/session-state.d.ts +20 -1
- package/dist/shared/state/session-state.js +12 -2
- package/dist/shared/workflow/workflow.d.ts +2 -1
- package/dist/shared/workflow/workflow.js +16 -9
- package/package.json +17 -16
- package/scripts/postinstall.mjs +13 -11
- package/scripts/skills-libretto.mjs +14 -4
- package/skills/AGENTS.md +11 -0
- package/skills/libretto/SKILL.md +30 -9
- package/skills/libretto/references/auth-profiles.md +1 -1
- package/skills/libretto/references/code-generation-rules.md +3 -3
- package/skills/libretto/references/configuration-file-reference.md +11 -6
- package/skills/libretto-readonly/SKILL.md +95 -0
- package/src/cli/cli.ts +10 -0
- package/src/cli/commands/ai.ts +111 -1
- package/src/cli/commands/browser.ts +111 -9
- package/src/cli/commands/execution.ts +181 -74
- package/src/cli/commands/setup.ts +516 -0
- package/src/cli/commands/snapshot.ts +2 -2
- package/src/cli/commands/status.ts +79 -0
- package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
- package/src/cli/core/api-snapshot-analyzer.ts +7 -5
- package/src/cli/core/browser.ts +242 -35
- package/src/cli/core/{ai-config.ts → config.ts} +14 -108
- package/src/cli/core/context.ts +1 -45
- package/src/cli/core/deploy-artifact.ts +141 -71
- package/src/cli/core/providers/browserbase.ts +57 -0
- package/src/cli/core/providers/index.ts +62 -0
- package/src/cli/core/providers/kernel.ts +49 -0
- package/src/cli/core/providers/libretto-cloud.ts +61 -0
- package/src/cli/core/providers/types.ts +9 -0
- package/src/cli/core/readonly-exec.ts +284 -0
- package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
- package/src/cli/core/session.ts +75 -2
- package/src/cli/core/skill-version.ts +93 -0
- package/src/cli/core/telemetry.ts +0 -52
- package/src/cli/index.ts +0 -6
- package/src/cli/router.ts +4 -4
- package/src/cli/workers/run-integration-runtime.ts +18 -16
- package/src/cli/workers/run-integration-worker-protocol.ts +4 -1
- package/src/index.ts +1 -7
- package/src/runtime/extract/extract.ts +6 -5
- package/src/runtime/recovery/agent.ts +5 -4
- package/src/runtime/recovery/errors.ts +4 -3
- package/src/runtime/recovery/recovery.ts +4 -4
- package/src/shared/debug/pause.ts +4 -23
- package/src/shared/run/browser.ts +50 -1
- package/src/shared/state/index.ts +2 -0
- package/src/shared/state/session-state.ts +10 -0
- package/src/shared/workflow/workflow.ts +24 -13
- package/dist/cli/commands/init.js +0 -286
- package/dist/cli/commands/logs.js +0 -117
- package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
- package/dist/shared/llm/ai-sdk-adapter.js +0 -49
- package/dist/shared/llm/client.d.ts +0 -13
- package/dist/shared/llm/index.d.ts +0 -5
- package/dist/shared/llm/index.js +0 -6
- package/dist/shared/llm/types.d.ts +0 -67
- package/src/cli/commands/init.ts +0 -331
- package/src/cli/commands/logs.ts +0 -128
- package/src/shared/llm/ai-sdk-adapter.ts +0 -81
- package/src/shared/llm/index.ts +0 -3
- package/src/shared/llm/types.ts +0 -63
- /package/dist/{shared/llm → cli/core/providers}/types.js +0 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { basename, dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { writeAiConfig } from "../core/config.js";
|
|
15
|
+
import {
|
|
16
|
+
ensureLibrettoSetup,
|
|
17
|
+
LIBRETTO_CONFIG_PATH,
|
|
18
|
+
REPO_ROOT
|
|
19
|
+
} from "../core/context.js";
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_SNAPSHOT_MODELS,
|
|
22
|
+
loadSnapshotEnv,
|
|
23
|
+
resolveAiSetupStatus
|
|
24
|
+
} from "../core/ai-model.js";
|
|
25
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
26
|
+
const PROVIDER_CHOICES = [
|
|
27
|
+
{
|
|
28
|
+
key: "1",
|
|
29
|
+
label: "OpenAI",
|
|
30
|
+
provider: "openai",
|
|
31
|
+
envVar: "OPENAI_API_KEY",
|
|
32
|
+
envHint: "Get your key at https://platform.openai.com/api-keys"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "2",
|
|
36
|
+
label: "Anthropic",
|
|
37
|
+
provider: "anthropic",
|
|
38
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
39
|
+
envHint: "Get your key at https://console.anthropic.com/settings/keys"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "3",
|
|
43
|
+
label: "Google Gemini",
|
|
44
|
+
provider: "google",
|
|
45
|
+
envVar: "GEMINI_API_KEY",
|
|
46
|
+
envHint: "Get your key at https://aistudio.google.com/apikey"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "4",
|
|
50
|
+
label: "Google Vertex AI",
|
|
51
|
+
provider: "vertex",
|
|
52
|
+
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
53
|
+
envHint: "Requires `gcloud auth application-default login` and a GCP project ID"
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
function promptUser(rl, question) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
rl.question(question, (answer) => {
|
|
59
|
+
resolve(answer.trim());
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function providerLabel(provider) {
|
|
64
|
+
const choice = PROVIDER_CHOICES.find((c) => c.provider === provider);
|
|
65
|
+
return choice?.label ?? provider;
|
|
66
|
+
}
|
|
67
|
+
function sourceEnvVar(source) {
|
|
68
|
+
if (source.startsWith("env:")) return source.slice(4);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function ensurePinnedDefaultModel(status) {
|
|
72
|
+
if (status.source !== "config") {
|
|
73
|
+
writeAiConfig(status.model);
|
|
74
|
+
return { ...status, source: "config" };
|
|
75
|
+
}
|
|
76
|
+
return status;
|
|
77
|
+
}
|
|
78
|
+
function printHealthySummary(status) {
|
|
79
|
+
const envVar = sourceEnvVar(status.source);
|
|
80
|
+
if (envVar) {
|
|
81
|
+
console.log(
|
|
82
|
+
`\u2713 Detected ${envVar}. Using ${providerLabel(status.provider)}.`
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`\u2713 Using ${providerLabel(status.provider)} (${status.model}).`);
|
|
86
|
+
}
|
|
87
|
+
console.log(
|
|
88
|
+
"To change: npx libretto ai configure openai | anthropic | gemini | vertex"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function printInvalidAiConfigWarning(status) {
|
|
92
|
+
if (status.kind !== "invalid-config") return;
|
|
93
|
+
console.log("! Existing AI config is invalid:");
|
|
94
|
+
for (const line of status.message.split("\n")) {
|
|
95
|
+
console.log(` ${line}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function buildRepairPlan(status) {
|
|
99
|
+
if (status.kind === "configured-missing-credentials") {
|
|
100
|
+
const choice = PROVIDER_CHOICES.find((c) => c.provider === status.provider);
|
|
101
|
+
return {
|
|
102
|
+
kind: "repair-missing-credentials",
|
|
103
|
+
provider: status.provider,
|
|
104
|
+
model: status.model,
|
|
105
|
+
envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
|
|
106
|
+
choices: ["enter-matching-credential", "switch-provider", "skip"]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (status.kind === "invalid-config") {
|
|
110
|
+
return { kind: "repair-invalid-config", message: status.message };
|
|
111
|
+
}
|
|
112
|
+
return { kind: "no-repair-needed" };
|
|
113
|
+
}
|
|
114
|
+
function formatMissingCredentialsMessage(plan) {
|
|
115
|
+
return `\u2717 ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`;
|
|
116
|
+
}
|
|
117
|
+
function printSnapshotApiStatus() {
|
|
118
|
+
const status = resolveAiSetupStatus();
|
|
119
|
+
console.log(
|
|
120
|
+
"\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables."
|
|
121
|
+
);
|
|
122
|
+
if (status.kind === "ready") {
|
|
123
|
+
console.log();
|
|
124
|
+
printHealthySummary(status);
|
|
125
|
+
ensurePinnedDefaultModel(status);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
const plan = buildRepairPlan(status);
|
|
129
|
+
if (plan.kind === "repair-missing-credentials") {
|
|
130
|
+
console.log();
|
|
131
|
+
console.log(formatMissingCredentialsMessage(plan));
|
|
132
|
+
console.log(
|
|
133
|
+
` To fix: add ${plan.envVar} to .env, or run \`npx libretto setup\` interactively to repair.`
|
|
134
|
+
);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (plan.kind === "repair-invalid-config") {
|
|
138
|
+
printInvalidAiConfigWarning(status);
|
|
139
|
+
console.log(" Run `npx libretto setup` interactively to reconfigure.");
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
console.log();
|
|
143
|
+
console.log("\u2717 No snapshot API credentials detected.");
|
|
144
|
+
console.log(" Add one provider to .env:");
|
|
145
|
+
console.log(" OPENAI_API_KEY=...");
|
|
146
|
+
console.log(" ANTHROPIC_API_KEY=...");
|
|
147
|
+
console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
|
|
148
|
+
console.log(
|
|
149
|
+
" GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
|
|
150
|
+
);
|
|
151
|
+
console.log(
|
|
152
|
+
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
153
|
+
);
|
|
154
|
+
console.log(
|
|
155
|
+
" Run `npx libretto setup` interactively to set up credentials."
|
|
156
|
+
);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
function writeEnvVar(envVar, value, envPath) {
|
|
160
|
+
let envContent = "";
|
|
161
|
+
if (existsSync(envPath)) {
|
|
162
|
+
envContent = readFileSync(envPath, "utf-8");
|
|
163
|
+
}
|
|
164
|
+
const envLine = `${envVar}=${value}`;
|
|
165
|
+
if (envContent.includes(`${envVar}=`)) {
|
|
166
|
+
const updated = envContent.replace(
|
|
167
|
+
new RegExp(`^${envVar}=.*$`, "m"),
|
|
168
|
+
() => envLine
|
|
169
|
+
);
|
|
170
|
+
writeFileSync(envPath, updated);
|
|
171
|
+
console.log(`
|
|
172
|
+
\u2713 Updated ${envVar} in ${envPath}`);
|
|
173
|
+
} else {
|
|
174
|
+
const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
|
|
175
|
+
appendFileSync(envPath, `${separator}${envLine}
|
|
176
|
+
`);
|
|
177
|
+
console.log(`
|
|
178
|
+
\u2713 Added ${envVar} to ${envPath}`);
|
|
179
|
+
}
|
|
180
|
+
process.env[envVar] = value;
|
|
181
|
+
}
|
|
182
|
+
async function promptForCredential(rl, choice, envPath, modelOverride) {
|
|
183
|
+
console.log(`
|
|
184
|
+
${choice.label} selected.`);
|
|
185
|
+
console.log(`${choice.envHint}
|
|
186
|
+
`);
|
|
187
|
+
const apiKeyValue = await promptUser(rl, `Enter your ${choice.envVar}: `);
|
|
188
|
+
if (!apiKeyValue) {
|
|
189
|
+
console.log("\nNo value entered. Skipping API key setup.");
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
writeEnvVar(choice.envVar, apiKeyValue, envPath);
|
|
193
|
+
loadSnapshotEnv();
|
|
194
|
+
const model = modelOverride ?? DEFAULT_SNAPSHOT_MODELS[choice.provider];
|
|
195
|
+
writeAiConfig(model);
|
|
196
|
+
console.log(`\u2713 Snapshot API ready: ${model}`);
|
|
197
|
+
console.log(
|
|
198
|
+
"To change: npx libretto ai configure openai | anthropic | gemini | vertex"
|
|
199
|
+
);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
async function promptProviderSelection(rl, envPath) {
|
|
203
|
+
console.log(
|
|
204
|
+
"Which model provider would you like to use for snapshot analysis?\n"
|
|
205
|
+
);
|
|
206
|
+
for (const choice of PROVIDER_CHOICES) {
|
|
207
|
+
console.log(` ${choice.key}) ${choice.label}`);
|
|
208
|
+
}
|
|
209
|
+
console.log(" s) Skip for now\n");
|
|
210
|
+
const answer = await promptUser(rl, "Choice: ");
|
|
211
|
+
if (answer.toLowerCase() === "s" || !answer) {
|
|
212
|
+
printSkipMessage();
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
|
|
216
|
+
if (!selected) {
|
|
217
|
+
console.log(`
|
|
218
|
+
Unknown choice "${answer}". Skipping API setup.`);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return promptForCredential(rl, selected, envPath);
|
|
222
|
+
}
|
|
223
|
+
function printSkipMessage() {
|
|
224
|
+
console.log(
|
|
225
|
+
"\nSkipped. You can set up API credentials later by rerunning `npx libretto setup`."
|
|
226
|
+
);
|
|
227
|
+
console.log("Or add credentials directly to your .env file:");
|
|
228
|
+
console.log(" OPENAI_API_KEY=...");
|
|
229
|
+
console.log(" ANTHROPIC_API_KEY=...");
|
|
230
|
+
console.log(" GEMINI_API_KEY=...");
|
|
231
|
+
console.log(
|
|
232
|
+
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
async function runInteractiveApiSetup() {
|
|
236
|
+
const status = resolveAiSetupStatus();
|
|
237
|
+
const envPath = join(REPO_ROOT, ".env");
|
|
238
|
+
console.log(
|
|
239
|
+
"\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables."
|
|
240
|
+
);
|
|
241
|
+
if (status.kind === "ready") {
|
|
242
|
+
console.log();
|
|
243
|
+
printHealthySummary(status);
|
|
244
|
+
ensurePinnedDefaultModel(status);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const plan = buildRepairPlan(status);
|
|
248
|
+
const rl = createInterface({
|
|
249
|
+
input: process.stdin,
|
|
250
|
+
output: process.stdout
|
|
251
|
+
});
|
|
252
|
+
try {
|
|
253
|
+
if (plan.kind === "repair-missing-credentials") {
|
|
254
|
+
console.log(formatMissingCredentialsMessage(plan));
|
|
255
|
+
console.log("");
|
|
256
|
+
console.log("How would you like to fix this?\n");
|
|
257
|
+
console.log(` 1) Enter ${plan.envVar}`);
|
|
258
|
+
console.log(" 2) Switch to a different provider");
|
|
259
|
+
console.log(" s) Skip for now\n");
|
|
260
|
+
const answer = await promptUser(rl, "Choice: ");
|
|
261
|
+
if (answer === "1") {
|
|
262
|
+
const matchingChoice = PROVIDER_CHOICES.find(
|
|
263
|
+
(c) => c.provider === plan.provider
|
|
264
|
+
);
|
|
265
|
+
if (matchingChoice) {
|
|
266
|
+
await promptForCredential(rl, matchingChoice, envPath, plan.model);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (answer === "2") {
|
|
271
|
+
await promptProviderSelection(rl, envPath);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
printSkipMessage();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (plan.kind === "repair-invalid-config") {
|
|
278
|
+
printInvalidAiConfigWarning(status);
|
|
279
|
+
console.log(
|
|
280
|
+
"\nWould you like to reconfigure with a fresh provider selection?\n"
|
|
281
|
+
);
|
|
282
|
+
await promptProviderSelection(rl, envPath);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
console.log("\u2717 No snapshot API credentials detected.\n");
|
|
286
|
+
await promptProviderSelection(rl, envPath);
|
|
287
|
+
} finally {
|
|
288
|
+
rl.close();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function installBrowsers() {
|
|
292
|
+
console.log("Installing Playwright Chromium...");
|
|
293
|
+
const result = spawnSync("npx", ["playwright", "install", "chromium"], {
|
|
294
|
+
stdio: "inherit",
|
|
295
|
+
shell: true
|
|
296
|
+
});
|
|
297
|
+
if (result.status === 0) {
|
|
298
|
+
console.log("\u2713 Playwright Chromium installed");
|
|
299
|
+
} else {
|
|
300
|
+
console.error(
|
|
301
|
+
"\u2717 Failed to install Playwright Chromium. Run manually: npx playwright install chromium"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function getPackageSkillsRoot() {
|
|
306
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
307
|
+
let dir = dirname(thisFile);
|
|
308
|
+
while (dir !== dirname(dir)) {
|
|
309
|
+
if (existsSync(join(dir, "skills", "libretto"))) {
|
|
310
|
+
return join(dir, "skills");
|
|
311
|
+
}
|
|
312
|
+
dir = dirname(dir);
|
|
313
|
+
}
|
|
314
|
+
throw new Error("Could not locate libretto skill files in package");
|
|
315
|
+
}
|
|
316
|
+
function detectAgentDirs(root) {
|
|
317
|
+
const dirs = [];
|
|
318
|
+
if (existsSync(join(root, ".agents"))) dirs.push(join(root, ".agents"));
|
|
319
|
+
if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
|
|
320
|
+
return dirs;
|
|
321
|
+
}
|
|
322
|
+
function copySkills() {
|
|
323
|
+
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
324
|
+
if (agentDirs.length === 0) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
let skillsRoot;
|
|
328
|
+
try {
|
|
329
|
+
skillsRoot = getPackageSkillsRoot();
|
|
330
|
+
} catch (e) {
|
|
331
|
+
console.error(`\u2717 ${e instanceof Error ? e.message : String(e)}`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const skillNames = readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
335
|
+
for (const agentDir of agentDirs) {
|
|
336
|
+
const agentName = basename(agentDir);
|
|
337
|
+
for (const skillName of skillNames) {
|
|
338
|
+
const sourceDir = join(skillsRoot, skillName);
|
|
339
|
+
const skillDest = join(agentDir, "skills", skillName);
|
|
340
|
+
if (existsSync(skillDest)) {
|
|
341
|
+
rmSync(skillDest, { recursive: true });
|
|
342
|
+
}
|
|
343
|
+
cpSync(sourceDir, skillDest, { recursive: true });
|
|
344
|
+
const fileCount = readdirSync(skillDest).length;
|
|
345
|
+
console.log(
|
|
346
|
+
`\u2713 Copied ${fileCount} skill files to ${agentName}/skills/${skillName}/`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const setupInput = SimpleCLI.input({
|
|
352
|
+
positionals: [],
|
|
353
|
+
named: {
|
|
354
|
+
skipBrowsers: SimpleCLI.flag({
|
|
355
|
+
name: "skip-browsers",
|
|
356
|
+
help: "Skip Playwright Chromium installation"
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
const setupCommand = SimpleCLI.command({
|
|
361
|
+
description: "Set up libretto in the current project"
|
|
362
|
+
}).input(setupInput).handle(async ({ input }) => {
|
|
363
|
+
ensureLibrettoSetup();
|
|
364
|
+
if (!input.skipBrowsers) {
|
|
365
|
+
installBrowsers();
|
|
366
|
+
} else {
|
|
367
|
+
console.log("Skipping browser installation (--skip-browsers)");
|
|
368
|
+
}
|
|
369
|
+
copySkills();
|
|
370
|
+
if (process.stdin.isTTY) {
|
|
371
|
+
await runInteractiveApiSetup();
|
|
372
|
+
} else {
|
|
373
|
+
const ready = printSnapshotApiStatus();
|
|
374
|
+
if (!ready) {
|
|
375
|
+
console.log(
|
|
376
|
+
"\nIf you're an agent, request the user to run `npx libretto setup`."
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
console.log(`
|
|
381
|
+
Config set up at ${LIBRETTO_CONFIG_PATH}`);
|
|
382
|
+
console.log("\n\u2713 libretto setup complete");
|
|
383
|
+
});
|
|
384
|
+
export {
|
|
385
|
+
PROVIDER_CHOICES,
|
|
386
|
+
buildRepairPlan,
|
|
387
|
+
formatMissingCredentialsMessage,
|
|
388
|
+
setupCommand,
|
|
389
|
+
setupInput
|
|
390
|
+
};
|
|
@@ -7,8 +7,8 @@ import { readSessionState } from "../core/session.js";
|
|
|
7
7
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
8
8
|
import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
|
|
9
9
|
import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
|
|
10
|
-
import { readAiConfig } from "../core/
|
|
11
|
-
import { resolveSnapshotApiModelOrThrow } from "../core/
|
|
10
|
+
import { readAiConfig } from "../core/config.js";
|
|
11
|
+
import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
|
|
12
12
|
const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
|
|
13
13
|
function generateSnapshotRunId() {
|
|
14
14
|
return `snapshot-${Date.now()}`;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
|
|
2
|
+
import { resolveAiSetupStatus } from "../core/ai-model.js";
|
|
3
|
+
import { listRunningSessions } from "../core/session.js";
|
|
4
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
5
|
+
function printAiStatus(status) {
|
|
6
|
+
console.log("AI configuration:");
|
|
7
|
+
switch (status.kind) {
|
|
8
|
+
case "ready":
|
|
9
|
+
console.log(` \u2713 Model: ${status.model}`);
|
|
10
|
+
if (status.source === "config") {
|
|
11
|
+
console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
|
|
12
|
+
} else {
|
|
13
|
+
console.log(` Source: ${status.source}`);
|
|
14
|
+
}
|
|
15
|
+
console.log(
|
|
16
|
+
" To change: npx libretto ai configure openai | anthropic | gemini | vertex"
|
|
17
|
+
);
|
|
18
|
+
break;
|
|
19
|
+
case "configured-missing-credentials":
|
|
20
|
+
console.log(
|
|
21
|
+
` \u2717 ${status.provider} is configured (model: ${status.model}), but credentials are missing.`
|
|
22
|
+
);
|
|
23
|
+
console.log(" Run `npx libretto setup` to repair.");
|
|
24
|
+
break;
|
|
25
|
+
case "invalid-config":
|
|
26
|
+
console.log(" \u2717 Config is invalid:");
|
|
27
|
+
for (const line of status.message.split("\n")) {
|
|
28
|
+
console.log(` ${line}`);
|
|
29
|
+
}
|
|
30
|
+
console.log(" Run `npx libretto setup` to reconfigure.");
|
|
31
|
+
break;
|
|
32
|
+
case "unconfigured":
|
|
33
|
+
console.log(" \u2717 No AI model configured.");
|
|
34
|
+
console.log(
|
|
35
|
+
" Run `npx libretto setup` or `npx libretto ai configure` to set up."
|
|
36
|
+
);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function printOpenSessions(sessions) {
|
|
41
|
+
console.log("\nOpen sessions:");
|
|
42
|
+
if (sessions.length === 0) {
|
|
43
|
+
console.log(" No open sessions.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const session of sessions) {
|
|
47
|
+
const statusLabel = session.status ? ` [${session.status}]` : "";
|
|
48
|
+
const endpoint = session.provider ? `${session.provider.name} (${session.cdpEndpoint})` : `http://127.0.0.1:${session.port}`;
|
|
49
|
+
console.log(` ${session.session}${statusLabel} \u2014 ${endpoint}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const statusCommand = SimpleCLI.command({
|
|
53
|
+
description: "Show workspace status: AI configuration and open sessions"
|
|
54
|
+
}).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
|
|
55
|
+
const aiStatus = resolveAiSetupStatus();
|
|
56
|
+
printAiStatus(aiStatus);
|
|
57
|
+
const sessions = listRunningSessions();
|
|
58
|
+
printOpenSessions(sessions);
|
|
59
|
+
});
|
|
60
|
+
export {
|
|
61
|
+
statusCommand
|
|
62
|
+
};
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { readAiConfig } from "./
|
|
3
|
+
import { readAiConfig } from "./config.js";
|
|
4
4
|
import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
|
|
5
5
|
import {
|
|
6
6
|
hasProviderCredentials,
|
|
7
7
|
parseModel
|
|
8
|
-
} from "
|
|
8
|
+
} from "./resolve-model.js";
|
|
9
9
|
const DEFAULT_SNAPSHOT_MODELS = {
|
|
10
10
|
openai: "openai/gpt-5.4",
|
|
11
11
|
anthropic: "anthropic/claude-sonnet-4-6",
|
|
12
12
|
google: "google/gemini-3-flash-preview",
|
|
13
|
-
vertex: "vertex/gemini-2.5-
|
|
13
|
+
vertex: "vertex/gemini-2.5-flash"
|
|
14
14
|
};
|
|
15
|
+
function detectProviderEnvVar(provider, env = process.env) {
|
|
16
|
+
switch (provider) {
|
|
17
|
+
case "openai":
|
|
18
|
+
return env.OPENAI_API_KEY?.trim() ? "OPENAI_API_KEY" : null;
|
|
19
|
+
case "anthropic":
|
|
20
|
+
return env.ANTHROPIC_API_KEY?.trim() ? "ANTHROPIC_API_KEY" : null;
|
|
21
|
+
case "google":
|
|
22
|
+
if (env.GEMINI_API_KEY?.trim()) return "GEMINI_API_KEY";
|
|
23
|
+
if (env.GOOGLE_GENERATIVE_AI_API_KEY?.trim())
|
|
24
|
+
return "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
25
|
+
return null;
|
|
26
|
+
case "vertex":
|
|
27
|
+
if (env.GOOGLE_CLOUD_PROJECT?.trim()) return "GOOGLE_CLOUD_PROJECT";
|
|
28
|
+
if (env.GCLOUD_PROJECT?.trim()) return "GCLOUD_PROJECT";
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
15
32
|
class SnapshotApiUnavailableError extends Error {
|
|
16
33
|
constructor(message) {
|
|
17
34
|
super(message);
|
|
@@ -49,7 +66,7 @@ function noSnapshotApiConfiguredMessage() {
|
|
|
49
66
|
return [
|
|
50
67
|
"Failed to analyze snapshot because no snapshot analyzer is configured.",
|
|
51
68
|
`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()}\`.`,
|
|
52
|
-
"For more info, run `npx libretto
|
|
69
|
+
"For more info, run `npx libretto setup`."
|
|
53
70
|
].join(" ");
|
|
54
71
|
}
|
|
55
72
|
function missingProviderSnapshotMessage(selection) {
|
|
@@ -57,7 +74,7 @@ function missingProviderSnapshotMessage(selection) {
|
|
|
57
74
|
return [
|
|
58
75
|
`Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
|
|
59
76
|
providerSetupSentence(selection.provider),
|
|
60
|
-
"For more info, run `npx libretto
|
|
77
|
+
"For more info, run `npx libretto setup`."
|
|
61
78
|
].join(" ");
|
|
62
79
|
}
|
|
63
80
|
function readWorktreeEnvPath() {
|
|
@@ -128,11 +145,12 @@ function inferAutoSnapshotModel() {
|
|
|
128
145
|
"vertex"
|
|
129
146
|
];
|
|
130
147
|
for (const provider of providersInPriorityOrder) {
|
|
131
|
-
|
|
148
|
+
const envVar = detectProviderEnvVar(provider);
|
|
149
|
+
if (!envVar) continue;
|
|
132
150
|
return {
|
|
133
151
|
model: DEFAULT_SNAPSHOT_MODELS[provider],
|
|
134
152
|
provider,
|
|
135
|
-
source: `env
|
|
153
|
+
source: `env:${envVar}`
|
|
136
154
|
};
|
|
137
155
|
}
|
|
138
156
|
return null;
|
|
@@ -164,11 +182,67 @@ function resolveSnapshotApiModelOrThrow(config = readAiConfig()) {
|
|
|
164
182
|
function isSnapshotApiUnavailableError(error) {
|
|
165
183
|
return error instanceof SnapshotApiUnavailableError;
|
|
166
184
|
}
|
|
185
|
+
function readAiConfigSafely(configPath) {
|
|
186
|
+
try {
|
|
187
|
+
return { ok: true, config: readAiConfig(configPath) };
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
message: err instanceof Error ? err.message : String(err)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function resolveAiSetupStatus(configPath = LIBRETTO_CONFIG_PATH) {
|
|
196
|
+
loadSnapshotEnv();
|
|
197
|
+
const configResult = readAiConfigSafely(configPath);
|
|
198
|
+
if (!configResult.ok) {
|
|
199
|
+
return { kind: "invalid-config", message: configResult.message };
|
|
200
|
+
}
|
|
201
|
+
if (configResult.config) {
|
|
202
|
+
let selection;
|
|
203
|
+
try {
|
|
204
|
+
selection = resolveSnapshotApiModel(configResult.config);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return {
|
|
207
|
+
kind: "invalid-config",
|
|
208
|
+
message: err instanceof Error ? err.message : String(err)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (!selection) {
|
|
212
|
+
return { kind: "unconfigured" };
|
|
213
|
+
}
|
|
214
|
+
if (hasProviderCredentials(selection.provider)) {
|
|
215
|
+
return {
|
|
216
|
+
kind: "ready",
|
|
217
|
+
model: selection.model,
|
|
218
|
+
provider: selection.provider,
|
|
219
|
+
source: selection.source
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
kind: "configured-missing-credentials",
|
|
224
|
+
model: selection.model,
|
|
225
|
+
provider: selection.provider
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const envSelection = resolveSnapshotApiModel(null);
|
|
229
|
+
if (envSelection && hasProviderCredentials(envSelection.provider)) {
|
|
230
|
+
return {
|
|
231
|
+
kind: "ready",
|
|
232
|
+
model: envSelection.model,
|
|
233
|
+
provider: envSelection.provider,
|
|
234
|
+
source: envSelection.source
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return { kind: "unconfigured" };
|
|
238
|
+
}
|
|
167
239
|
export {
|
|
240
|
+
DEFAULT_SNAPSHOT_MODELS,
|
|
168
241
|
SnapshotApiUnavailableError,
|
|
169
242
|
isSnapshotApiUnavailableError,
|
|
170
243
|
loadSnapshotEnv,
|
|
171
244
|
parseDotEnvAssignment,
|
|
245
|
+
resolveAiSetupStatus,
|
|
172
246
|
resolveSnapshotApiModel,
|
|
173
247
|
resolveSnapshotApiModelOrThrow
|
|
174
248
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { generateObject } from "ai";
|
|
3
|
+
import { resolveModel } from "./resolve-model.js";
|
|
3
4
|
import {
|
|
4
5
|
InterpretResultSchema,
|
|
5
6
|
buildInlinePromptSelection,
|
|
6
7
|
getMimeType,
|
|
7
8
|
readFileAsBase64
|
|
8
9
|
} from "./snapshot-analyzer.js";
|
|
9
|
-
import { readAiConfig } from "./
|
|
10
|
-
import { resolveSnapshotApiModelOrThrow } from "./
|
|
10
|
+
import { readAiConfig } from "./config.js";
|
|
11
|
+
import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
|
|
11
12
|
async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
|
|
12
13
|
const selection = resolveSnapshotApiModelOrThrow(configuredAi);
|
|
13
14
|
logger.info("api-interpret-start", {
|
|
@@ -41,8 +42,9 @@ async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
|
|
|
41
42
|
const imageBase64 = readFileAsBase64(args.pngPath);
|
|
42
43
|
const imageMimeType = getMimeType(args.pngPath);
|
|
43
44
|
const imageBytes = Buffer.from(imageBase64, "base64");
|
|
44
|
-
const
|
|
45
|
-
const result = await
|
|
45
|
+
const model = await resolveModel(selection.model);
|
|
46
|
+
const { object: result } = await generateObject({
|
|
47
|
+
model,
|
|
46
48
|
schema: InterpretResultSchema,
|
|
47
49
|
messages: [
|
|
48
50
|
{
|