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,516 @@
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
+ type AiSetupStatus,
22
+ DEFAULT_SNAPSHOT_MODELS,
23
+ loadSnapshotEnv,
24
+ resolveAiSetupStatus,
25
+ } from "../core/ai-model.js";
26
+ import type { Provider } from "../core/resolve-model.js";
27
+ import { SimpleCLI } from "../framework/simple-cli.js";
28
+
29
+ export type ProviderChoice = {
30
+ key: string;
31
+ label: string;
32
+ provider: Provider;
33
+ envVar: string;
34
+ envHint: string;
35
+ };
36
+
37
+ export const PROVIDER_CHOICES: ProviderChoice[] = [
38
+ {
39
+ key: "1",
40
+ label: "OpenAI",
41
+ provider: "openai",
42
+ envVar: "OPENAI_API_KEY",
43
+ envHint: "Get your key at https://platform.openai.com/api-keys",
44
+ },
45
+ {
46
+ key: "2",
47
+ label: "Anthropic",
48
+ provider: "anthropic",
49
+ envVar: "ANTHROPIC_API_KEY",
50
+ envHint: "Get your key at https://console.anthropic.com/settings/keys",
51
+ },
52
+ {
53
+ key: "3",
54
+ label: "Google Gemini",
55
+ provider: "google",
56
+ envVar: "GEMINI_API_KEY",
57
+ envHint: "Get your key at https://aistudio.google.com/apikey",
58
+ },
59
+ {
60
+ key: "4",
61
+ label: "Google Vertex AI",
62
+ provider: "vertex",
63
+ envVar: "GOOGLE_CLOUD_PROJECT",
64
+ envHint:
65
+ "Requires `gcloud auth application-default login` and a GCP project ID",
66
+ },
67
+ ];
68
+
69
+ function promptUser(
70
+ rl: ReturnType<typeof createInterface>,
71
+ question: string,
72
+ ): Promise<string> {
73
+ return new Promise((resolve) => {
74
+ rl.question(question, (answer) => {
75
+ resolve(answer.trim());
76
+ });
77
+ });
78
+ }
79
+
80
+ /** Map provider to a human-readable label for status messages. */
81
+ function providerLabel(provider: Provider): string {
82
+ const choice = PROVIDER_CHOICES.find((c) => c.provider === provider);
83
+ return choice?.label ?? provider;
84
+ }
85
+
86
+ /** Extract the env var name from source like "env:GOOGLE_CLOUD_PROJECT". */
87
+ function sourceEnvVar(source: string): string | null {
88
+ if (source.startsWith("env:")) return source.slice(4);
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * If the workspace has usable credentials but no pinned model in config,
94
+ * write the resolved default model to `.libretto/config.json`.
95
+ */
96
+ function ensurePinnedDefaultModel(
97
+ status: AiSetupStatus & { kind: "ready" },
98
+ ): AiSetupStatus & { kind: "ready" } {
99
+ if (status.source !== "config") {
100
+ writeAiConfig(status.model);
101
+ return { ...status, source: "config" as const };
102
+ }
103
+ return status;
104
+ }
105
+
106
+ function printHealthySummary(status: AiSetupStatus & { kind: "ready" }): void {
107
+ const envVar = sourceEnvVar(status.source);
108
+ if (envVar) {
109
+ console.log(
110
+ `✓ Detected ${envVar}. Using ${providerLabel(status.provider)}.`,
111
+ );
112
+ } else {
113
+ console.log(`✓ Using ${providerLabel(status.provider)} (${status.model}).`);
114
+ }
115
+ console.log(
116
+ "To change: npx libretto ai configure openai | anthropic | gemini | vertex",
117
+ );
118
+ }
119
+
120
+ function printInvalidAiConfigWarning(status: AiSetupStatus): void {
121
+ if (status.kind !== "invalid-config") return;
122
+ console.log("! Existing AI config is invalid:");
123
+ for (const line of status.message.split("\n")) {
124
+ console.log(` ${line}`);
125
+ }
126
+ }
127
+
128
+ // ── Repair plan helpers (exported for testing) ──────────────────────────────
129
+
130
+ export type RepairChoice =
131
+ | "enter-matching-credential"
132
+ | "switch-provider"
133
+ | "skip";
134
+
135
+ export type RepairPlan =
136
+ | {
137
+ kind: "repair-missing-credentials";
138
+ provider: Provider;
139
+ model: string;
140
+ envVar: string;
141
+ choices: RepairChoice[];
142
+ }
143
+ | { kind: "repair-invalid-config"; message: string }
144
+ | { kind: "no-repair-needed" };
145
+
146
+ /**
147
+ * Determine what repair action setup should take for the current AI status.
148
+ * Pure function — no I/O, no prompts.
149
+ */
150
+ export function buildRepairPlan(status: AiSetupStatus): RepairPlan {
151
+ if (status.kind === "configured-missing-credentials") {
152
+ const choice = PROVIDER_CHOICES.find((c) => c.provider === status.provider);
153
+ return {
154
+ kind: "repair-missing-credentials",
155
+ provider: status.provider,
156
+ model: status.model,
157
+ envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
158
+ choices: ["enter-matching-credential", "switch-provider", "skip"],
159
+ };
160
+ }
161
+ if (status.kind === "invalid-config") {
162
+ return { kind: "repair-invalid-config", message: status.message };
163
+ }
164
+ return { kind: "no-repair-needed" };
165
+ }
166
+
167
+ /**
168
+ * Format a provider-specific explanation for missing credentials.
169
+ */
170
+ export function formatMissingCredentialsMessage(
171
+ plan: RepairPlan & { kind: "repair-missing-credentials" },
172
+ ): string {
173
+ return `✗ ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`;
174
+ }
175
+
176
+ function printSnapshotApiStatus(): boolean {
177
+ const status = resolveAiSetupStatus();
178
+
179
+ console.log(
180
+ "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
181
+ );
182
+
183
+ if (status.kind === "ready") {
184
+ console.log();
185
+ printHealthySummary(status);
186
+ ensurePinnedDefaultModel(status);
187
+ return true;
188
+ }
189
+
190
+ // Provider-specific missing-credentials message
191
+ const plan = buildRepairPlan(status);
192
+ if (plan.kind === "repair-missing-credentials") {
193
+ console.log();
194
+ console.log(formatMissingCredentialsMessage(plan));
195
+ console.log(
196
+ ` To fix: add ${plan.envVar} to .env, or run \`npx libretto setup\` interactively to repair.`,
197
+ );
198
+ return false;
199
+ }
200
+
201
+ if (plan.kind === "repair-invalid-config") {
202
+ printInvalidAiConfigWarning(status);
203
+ console.log(" Run `npx libretto setup` interactively to reconfigure.");
204
+ return false;
205
+ }
206
+
207
+ console.log();
208
+ console.log("✗ No snapshot API credentials detected.");
209
+ console.log(" Add one provider to .env:");
210
+ console.log(" OPENAI_API_KEY=...");
211
+ console.log(" ANTHROPIC_API_KEY=...");
212
+ console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
213
+ console.log(
214
+ " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex",
215
+ );
216
+ console.log(
217
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model.",
218
+ );
219
+ console.log(
220
+ " Run `npx libretto setup` interactively to set up credentials.",
221
+ );
222
+ return false;
223
+ }
224
+
225
+ /**
226
+ * Write an env var to the .env file and update process.env.
227
+ */
228
+ function writeEnvVar(envVar: string, value: string, envPath: string): void {
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.
287
+ * Returns true if a provider was successfully configured.
288
+ */
289
+ async function promptProviderSelection(
290
+ rl: ReturnType<typeof createInterface>,
291
+ envPath: string,
292
+ ): Promise<boolean> {
293
+ console.log(
294
+ "Which model provider would you like to use for snapshot analysis?\n",
295
+ );
296
+ for (const choice of PROVIDER_CHOICES) {
297
+ console.log(` ${choice.key}) ${choice.label}`);
298
+ }
299
+ console.log(" s) Skip for now\n");
300
+
301
+ const answer = await promptUser(rl, "Choice: ");
302
+
303
+ if (answer.toLowerCase() === "s" || !answer) {
304
+ printSkipMessage();
305
+ return false;
306
+ }
307
+
308
+ const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
309
+ if (!selected) {
310
+ console.log(`\nUnknown choice "${answer}". Skipping API setup.`);
311
+ return false;
312
+ }
313
+
314
+ return promptForCredential(rl, selected, envPath);
315
+ }
316
+
317
+ function printSkipMessage(): void {
318
+ console.log(
319
+ "\nSkipped. You can set up API credentials later by rerunning `npx libretto setup`.",
320
+ );
321
+ console.log("Or add credentials directly to your .env file:");
322
+ console.log(" OPENAI_API_KEY=...");
323
+ console.log(" ANTHROPIC_API_KEY=...");
324
+ console.log(" GEMINI_API_KEY=...");
325
+ console.log(
326
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model.",
327
+ );
328
+ }
329
+
330
+ async function runInteractiveApiSetup(): Promise<void> {
331
+ const status = resolveAiSetupStatus();
332
+ const envPath = join(REPO_ROOT, ".env");
333
+
334
+ console.log(
335
+ "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
336
+ );
337
+
338
+ if (status.kind === "ready") {
339
+ console.log();
340
+ printHealthySummary(status);
341
+ ensurePinnedDefaultModel(status);
342
+ return;
343
+ }
344
+
345
+ const plan = buildRepairPlan(status);
346
+
347
+ const rl = createInterface({
348
+ input: process.stdin,
349
+ output: process.stdout,
350
+ });
351
+
352
+ try {
353
+ // ── Repair: configured provider with missing credentials ──
354
+ if (plan.kind === "repair-missing-credentials") {
355
+ console.log(formatMissingCredentialsMessage(plan));
356
+ console.log("");
357
+ console.log("How would you like to fix this?\n");
358
+ console.log(` 1) Enter ${plan.envVar}`);
359
+ console.log(" 2) Switch to a different provider");
360
+ console.log(" s) Skip for now\n");
361
+
362
+ const answer = await promptUser(rl, "Choice: ");
363
+
364
+ if (answer === "1") {
365
+ const matchingChoice = PROVIDER_CHOICES.find(
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);
376
+ return;
377
+ }
378
+
379
+ // skip or empty
380
+ printSkipMessage();
381
+ return;
382
+ }
383
+
384
+ // ── Repair: invalid config → let user pick a provider ──
385
+ if (plan.kind === "repair-invalid-config") {
386
+ printInvalidAiConfigWarning(status);
387
+ console.log(
388
+ "\nWould you like to reconfigure with a fresh provider selection?\n",
389
+ );
390
+ await promptProviderSelection(rl, envPath);
391
+ return;
392
+ }
393
+
394
+ // ── Unconfigured: standard first-run flow ──
395
+ console.log("✗ No snapshot API credentials detected.\n");
396
+ await promptProviderSelection(rl, envPath);
397
+ } finally {
398
+ rl.close();
399
+ }
400
+ }
401
+
402
+ function installBrowsers(): void {
403
+ console.log("Installing Playwright Chromium...");
404
+ const result = spawnSync("npx", ["playwright", "install", "chromium"], {
405
+ stdio: "inherit",
406
+ shell: true,
407
+ });
408
+ if (result.status === 0) {
409
+ console.log("✓ Playwright Chromium installed");
410
+ } else {
411
+ console.error(
412
+ "✗ Failed to install Playwright Chromium. Run manually: npx playwright install chromium",
413
+ );
414
+ }
415
+ }
416
+
417
+ function getPackageSkillsRoot(): string {
418
+ const thisFile = fileURLToPath(import.meta.url);
419
+ // Walk up from dist/cli/commands/ to package root
420
+ let dir = dirname(thisFile);
421
+ while (dir !== dirname(dir)) {
422
+ if (existsSync(join(dir, "skills", "libretto"))) {
423
+ return join(dir, "skills");
424
+ }
425
+ dir = dirname(dir);
426
+ }
427
+ throw new Error("Could not locate libretto skill files in package");
428
+ }
429
+
430
+ /**
431
+ * Auto-detect .agents/ and .claude/ directories at a given root path.
432
+ */
433
+ function detectAgentDirs(root: string): string[] {
434
+ const dirs: string[] = [];
435
+ if (existsSync(join(root, ".agents"))) dirs.push(join(root, ".agents"));
436
+ if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
437
+ return dirs;
438
+ }
439
+
440
+ function copySkills(): void {
441
+ const agentDirs = detectAgentDirs(REPO_ROOT);
442
+
443
+ if (agentDirs.length === 0) {
444
+ return;
445
+ }
446
+
447
+ let skillsRoot: string;
448
+ try {
449
+ skillsRoot = getPackageSkillsRoot();
450
+ } catch (e) {
451
+ console.error(`✗ ${e instanceof Error ? e.message : String(e)}`);
452
+ return;
453
+ }
454
+
455
+ const skillNames = readdirSync(skillsRoot, { withFileTypes: true })
456
+ .filter((entry) => entry.isDirectory())
457
+ .map((entry) => entry.name)
458
+ .sort();
459
+
460
+ for (const agentDir of agentDirs) {
461
+ const agentName = basename(agentDir);
462
+
463
+ for (const skillName of skillNames) {
464
+ const sourceDir = join(skillsRoot, skillName);
465
+ const skillDest = join(agentDir, "skills", skillName);
466
+ if (existsSync(skillDest)) {
467
+ rmSync(skillDest, { recursive: true });
468
+ }
469
+ cpSync(sourceDir, skillDest, { recursive: true });
470
+ const fileCount = readdirSync(skillDest).length;
471
+ console.log(
472
+ `✓ Copied ${fileCount} skill files to ${agentName}/skills/${skillName}/`,
473
+ );
474
+ }
475
+ }
476
+ }
477
+
478
+ export const setupInput = SimpleCLI.input({
479
+ positionals: [],
480
+ named: {
481
+ skipBrowsers: SimpleCLI.flag({
482
+ name: "skip-browsers",
483
+ help: "Skip Playwright Chromium installation",
484
+ }),
485
+ },
486
+ });
487
+
488
+ export const setupCommand = SimpleCLI.command({
489
+ description: "Set up libretto in the current project",
490
+ })
491
+ .input(setupInput)
492
+ .handle(async ({ input }) => {
493
+ ensureLibrettoSetup();
494
+
495
+ if (!input.skipBrowsers) {
496
+ installBrowsers();
497
+ } else {
498
+ console.log("Skipping browser installation (--skip-browsers)");
499
+ }
500
+
501
+ copySkills();
502
+
503
+ if (process.stdin.isTTY) {
504
+ await runInteractiveApiSetup();
505
+ } else {
506
+ const ready = printSnapshotApiStatus();
507
+ if (!ready) {
508
+ console.log(
509
+ "\nIf you're an agent, request the user to run `npx libretto setup`.",
510
+ );
511
+ }
512
+ }
513
+
514
+ console.log(`\nConfig set up at ${LIBRETTO_CONFIG_PATH}`);
515
+ console.log("\n✓ libretto setup complete");
516
+ });
@@ -12,8 +12,8 @@ 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 { readAiConfig } from "../core/ai-config.js";
16
- import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
15
+ import { readAiConfig } from "../core/config.js";
16
+ import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
17
17
 
18
18
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 } as const;
19
19
 
@@ -0,0 +1,79 @@
1
+ import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
2
+ import { type AiSetupStatus, resolveAiSetupStatus } from "../core/ai-model.js";
3
+ import { listRunningSessions, type SessionState } from "../core/session.js";
4
+ import { SimpleCLI } from "../framework/simple-cli.js";
5
+
6
+ // ── AI status printing ──────────────────────────────────────────────────────
7
+
8
+ function printAiStatus(status: AiSetupStatus): void {
9
+ console.log("AI configuration:");
10
+
11
+ switch (status.kind) {
12
+ case "ready":
13
+ console.log(` ✓ Model: ${status.model}`);
14
+ if (status.source === "config") {
15
+ console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
16
+ } else {
17
+ console.log(` Source: ${status.source}`);
18
+ }
19
+ console.log(
20
+ " To change: npx libretto ai configure openai | anthropic | gemini | vertex",
21
+ );
22
+ break;
23
+
24
+ case "configured-missing-credentials":
25
+ console.log(
26
+ ` ✗ ${status.provider} is configured (model: ${status.model}), but credentials are missing.`,
27
+ );
28
+ console.log(" Run `npx libretto setup` to repair.");
29
+ break;
30
+
31
+ case "invalid-config":
32
+ console.log(" ✗ Config is invalid:");
33
+ for (const line of status.message.split("\n")) {
34
+ console.log(` ${line}`);
35
+ }
36
+ console.log(" Run `npx libretto setup` to reconfigure.");
37
+ break;
38
+
39
+ case "unconfigured":
40
+ console.log(" ✗ No AI model configured.");
41
+ console.log(
42
+ " Run `npx libretto setup` or `npx libretto ai configure` to set up.",
43
+ );
44
+ break;
45
+ }
46
+ }
47
+
48
+ // ── Session status printing ─────────────────────────────────────────────────
49
+
50
+ function printOpenSessions(sessions: SessionState[]): void {
51
+ console.log("\nOpen sessions:");
52
+
53
+ if (sessions.length === 0) {
54
+ console.log(" No open sessions.");
55
+ return;
56
+ }
57
+
58
+ for (const session of sessions) {
59
+ const statusLabel = session.status ? ` [${session.status}]` : "";
60
+ const endpoint = session.provider
61
+ ? `${session.provider.name} (${session.cdpEndpoint})`
62
+ : `http://127.0.0.1:${session.port}`;
63
+ console.log(` ${session.session}${statusLabel} — ${endpoint}`);
64
+ }
65
+ }
66
+
67
+ // ── Command ─────────────────────────────────────────────────────────────────
68
+
69
+ export const statusCommand = SimpleCLI.command({
70
+ description: "Show workspace status: AI configuration and open sessions",
71
+ })
72
+ .input(SimpleCLI.input({ positionals: [], named: {} }))
73
+ .handle(async () => {
74
+ const aiStatus = resolveAiSetupStatus();
75
+ printAiStatus(aiStatus);
76
+
77
+ const sessions = listRunningSessions();
78
+ printOpenSessions(sessions);
79
+ });