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.
Files changed (110) hide show
  1. package/README.md +23 -10
  2. package/README.template.md +23 -10
  3. package/dist/cli/cli.js +10 -0
  4. package/dist/cli/commands/ai.js +77 -2
  5. package/dist/cli/commands/browser.js +98 -8
  6. package/dist/cli/commands/execution.js +152 -56
  7. package/dist/cli/commands/setup.js +390 -0
  8. package/dist/cli/commands/snapshot.js +2 -2
  9. package/dist/cli/commands/status.js +62 -0
  10. package/dist/cli/core/{snapshot-api-config.js → ai-model.js} +81 -7
  11. package/dist/cli/core/api-snapshot-analyzer.js +7 -5
  12. package/dist/cli/core/browser.js +202 -36
  13. package/dist/cli/core/{ai-config.js → config.js} +14 -79
  14. package/dist/cli/core/context.js +1 -25
  15. package/dist/cli/core/deploy-artifact.js +121 -61
  16. package/dist/cli/core/providers/browserbase.js +53 -0
  17. package/dist/cli/core/providers/index.js +48 -0
  18. package/dist/cli/core/providers/kernel.js +46 -0
  19. package/dist/cli/core/providers/libretto-cloud.js +58 -0
  20. package/dist/cli/core/readonly-exec.js +231 -0
  21. package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
  22. package/dist/cli/core/session.js +53 -0
  23. package/dist/cli/core/skill-version.js +73 -0
  24. package/dist/cli/core/telemetry.js +1 -54
  25. package/dist/cli/index.js +1 -7
  26. package/dist/cli/router.js +4 -4
  27. package/dist/cli/workers/run-integration-runtime.js +19 -13
  28. package/dist/cli/workers/run-integration-worker-protocol.js +5 -2
  29. package/dist/index.d.ts +2 -4
  30. package/dist/index.js +2 -2
  31. package/dist/runtime/extract/extract.d.ts +2 -2
  32. package/dist/runtime/extract/extract.js +4 -2
  33. package/dist/runtime/extract/index.d.ts +1 -1
  34. package/dist/runtime/recovery/agent.d.ts +2 -3
  35. package/dist/runtime/recovery/agent.js +5 -3
  36. package/dist/runtime/recovery/errors.d.ts +2 -3
  37. package/dist/runtime/recovery/errors.js +4 -2
  38. package/dist/runtime/recovery/index.d.ts +1 -2
  39. package/dist/runtime/recovery/recovery.d.ts +2 -3
  40. package/dist/runtime/recovery/recovery.js +3 -3
  41. package/dist/shared/debug/pause.js +4 -21
  42. package/dist/shared/run/api.d.ts +2 -0
  43. package/dist/shared/run/browser.d.ts +9 -1
  44. package/dist/shared/run/browser.js +43 -3
  45. package/dist/shared/state/index.d.ts +1 -1
  46. package/dist/shared/state/index.js +2 -0
  47. package/dist/shared/state/session-state.d.ts +20 -1
  48. package/dist/shared/state/session-state.js +12 -2
  49. package/dist/shared/workflow/workflow.d.ts +2 -1
  50. package/dist/shared/workflow/workflow.js +16 -9
  51. package/package.json +17 -16
  52. package/scripts/postinstall.mjs +13 -11
  53. package/scripts/skills-libretto.mjs +14 -4
  54. package/skills/AGENTS.md +11 -0
  55. package/skills/libretto/SKILL.md +30 -9
  56. package/skills/libretto/references/auth-profiles.md +1 -1
  57. package/skills/libretto/references/code-generation-rules.md +3 -3
  58. package/skills/libretto/references/configuration-file-reference.md +11 -6
  59. package/skills/libretto-readonly/SKILL.md +95 -0
  60. package/src/cli/cli.ts +10 -0
  61. package/src/cli/commands/ai.ts +111 -1
  62. package/src/cli/commands/browser.ts +111 -9
  63. package/src/cli/commands/execution.ts +181 -74
  64. package/src/cli/commands/setup.ts +516 -0
  65. package/src/cli/commands/snapshot.ts +2 -2
  66. package/src/cli/commands/status.ts +79 -0
  67. package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
  68. package/src/cli/core/api-snapshot-analyzer.ts +7 -5
  69. package/src/cli/core/browser.ts +242 -35
  70. package/src/cli/core/{ai-config.ts → config.ts} +14 -108
  71. package/src/cli/core/context.ts +1 -45
  72. package/src/cli/core/deploy-artifact.ts +141 -71
  73. package/src/cli/core/providers/browserbase.ts +57 -0
  74. package/src/cli/core/providers/index.ts +62 -0
  75. package/src/cli/core/providers/kernel.ts +49 -0
  76. package/src/cli/core/providers/libretto-cloud.ts +61 -0
  77. package/src/cli/core/providers/types.ts +9 -0
  78. package/src/cli/core/readonly-exec.ts +284 -0
  79. package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
  80. package/src/cli/core/session.ts +75 -2
  81. package/src/cli/core/skill-version.ts +93 -0
  82. package/src/cli/core/telemetry.ts +0 -52
  83. package/src/cli/index.ts +0 -6
  84. package/src/cli/router.ts +4 -4
  85. package/src/cli/workers/run-integration-runtime.ts +18 -16
  86. package/src/cli/workers/run-integration-worker-protocol.ts +4 -1
  87. package/src/index.ts +1 -7
  88. package/src/runtime/extract/extract.ts +6 -5
  89. package/src/runtime/recovery/agent.ts +5 -4
  90. package/src/runtime/recovery/errors.ts +4 -3
  91. package/src/runtime/recovery/recovery.ts +4 -4
  92. package/src/shared/debug/pause.ts +4 -23
  93. package/src/shared/run/browser.ts +50 -1
  94. package/src/shared/state/index.ts +2 -0
  95. package/src/shared/state/session-state.ts +10 -0
  96. package/src/shared/workflow/workflow.ts +24 -13
  97. package/dist/cli/commands/init.js +0 -286
  98. package/dist/cli/commands/logs.js +0 -117
  99. package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
  100. package/dist/shared/llm/ai-sdk-adapter.js +0 -49
  101. package/dist/shared/llm/client.d.ts +0 -13
  102. package/dist/shared/llm/index.d.ts +0 -5
  103. package/dist/shared/llm/index.js +0 -6
  104. package/dist/shared/llm/types.d.ts +0 -67
  105. package/src/cli/commands/init.ts +0 -331
  106. package/src/cli/commands/logs.ts +0 -128
  107. package/src/shared/llm/ai-sdk-adapter.ts +0 -81
  108. package/src/shared/llm/index.ts +0 -3
  109. package/src/shared/llm/types.ts +0 -63
  110. /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/ai-config.js";
11
- import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
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 "./ai-config.js";
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 "../../shared/llm/client.js";
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-pro"
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 init`."
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 init`."
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
- if (!hasProviderCredentials(provider)) continue;
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:auto-${provider}`
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 { createLLMClient } from "../../shared/llm/client.js";
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 "./ai-config.js";
10
- import { resolveSnapshotApiModelOrThrow } from "./snapshot-api-config.js";
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 client = createLLMClient(selection.model);
45
- const result = await client.generateObjectFromMessages({
45
+ const model = await resolveModel(selection.model);
46
+ const { object: result } = await generateObject({
47
+ model,
46
48
  schema: InterpretResultSchema,
47
49
  messages: [
48
50
  {