libretto 0.5.4 → 0.5.6

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 (101) 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 +71 -6
  6. package/dist/cli/commands/execution.js +101 -44
  7. package/dist/cli/commands/setup.js +376 -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 +81 -42
  13. package/dist/cli/core/{ai-config.js → config.js} +13 -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/readonly-exec.js +231 -0
  17. package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
  18. package/dist/cli/core/session.js +44 -0
  19. package/dist/cli/core/skill-version.js +73 -0
  20. package/dist/cli/core/telemetry.js +1 -54
  21. package/dist/cli/index.js +1 -7
  22. package/dist/cli/router.js +4 -4
  23. package/dist/cli/workers/run-integration-runtime.js +29 -25
  24. package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
  25. package/dist/index.d.ts +2 -4
  26. package/dist/index.js +2 -2
  27. package/dist/runtime/extract/extract.d.ts +2 -2
  28. package/dist/runtime/extract/extract.js +4 -2
  29. package/dist/runtime/extract/index.d.ts +1 -1
  30. package/dist/runtime/recovery/agent.d.ts +2 -3
  31. package/dist/runtime/recovery/agent.js +5 -3
  32. package/dist/runtime/recovery/errors.d.ts +2 -3
  33. package/dist/runtime/recovery/errors.js +4 -2
  34. package/dist/runtime/recovery/index.d.ts +1 -2
  35. package/dist/runtime/recovery/recovery.d.ts +2 -3
  36. package/dist/runtime/recovery/recovery.js +3 -3
  37. package/dist/shared/debug/pause.js +4 -21
  38. package/dist/shared/run/api.d.ts +2 -0
  39. package/dist/shared/run/browser.d.ts +4 -1
  40. package/dist/shared/run/browser.js +5 -3
  41. package/dist/shared/state/index.d.ts +1 -1
  42. package/dist/shared/state/index.js +2 -0
  43. package/dist/shared/state/session-state.d.ts +10 -1
  44. package/dist/shared/state/session-state.js +3 -0
  45. package/dist/shared/workflow/workflow.d.ts +2 -3
  46. package/dist/shared/workflow/workflow.js +16 -9
  47. package/package.json +3 -4
  48. package/scripts/postinstall.mjs +13 -11
  49. package/scripts/skills-libretto.mjs +14 -4
  50. package/skills/AGENTS.md +11 -0
  51. package/skills/libretto/SKILL.md +30 -9
  52. package/skills/libretto/references/auth-profiles.md +1 -1
  53. package/skills/libretto/references/code-generation-rules.md +6 -6
  54. package/skills/libretto/references/configuration-file-reference.md +11 -6
  55. package/skills/libretto-readonly/SKILL.md +95 -0
  56. package/src/cli/cli.ts +10 -0
  57. package/src/cli/commands/ai.ts +111 -1
  58. package/src/cli/commands/browser.ts +81 -7
  59. package/src/cli/commands/execution.ts +128 -61
  60. package/src/cli/commands/setup.ts +499 -0
  61. package/src/cli/commands/snapshot.ts +2 -2
  62. package/src/cli/commands/status.ts +77 -0
  63. package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
  64. package/src/cli/core/api-snapshot-analyzer.ts +7 -5
  65. package/src/cli/core/browser.ts +107 -45
  66. package/src/cli/core/{ai-config.ts → config.ts} +13 -108
  67. package/src/cli/core/context.ts +1 -45
  68. package/src/cli/core/deploy-artifact.ts +141 -71
  69. package/src/cli/core/readonly-exec.ts +284 -0
  70. package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
  71. package/src/cli/core/session.ts +62 -2
  72. package/src/cli/core/skill-version.ts +93 -0
  73. package/src/cli/core/telemetry.ts +0 -52
  74. package/src/cli/index.ts +0 -6
  75. package/src/cli/router.ts +4 -4
  76. package/src/cli/workers/run-integration-runtime.ts +36 -31
  77. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  78. package/src/index.ts +1 -7
  79. package/src/runtime/extract/extract.ts +6 -5
  80. package/src/runtime/recovery/agent.ts +5 -4
  81. package/src/runtime/recovery/errors.ts +4 -3
  82. package/src/runtime/recovery/recovery.ts +4 -4
  83. package/src/shared/debug/pause.ts +4 -23
  84. package/src/shared/run/browser.ts +5 -1
  85. package/src/shared/state/index.ts +2 -0
  86. package/src/shared/state/session-state.ts +3 -0
  87. package/src/shared/workflow/workflow.ts +24 -15
  88. package/dist/cli/commands/init.js +0 -286
  89. package/dist/cli/commands/logs.js +0 -117
  90. package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
  91. package/dist/shared/llm/ai-sdk-adapter.js +0 -49
  92. package/dist/shared/llm/client.d.ts +0 -13
  93. package/dist/shared/llm/index.d.ts +0 -5
  94. package/dist/shared/llm/index.js +0 -6
  95. package/dist/shared/llm/types.d.ts +0 -67
  96. package/dist/shared/llm/types.js +0 -0
  97. package/src/cli/commands/init.ts +0 -331
  98. package/src/cli/commands/logs.ts +0 -128
  99. package/src/shared/llm/ai-sdk-adapter.ts +0 -81
  100. package/src/shared/llm/index.ts +0 -3
  101. package/src/shared/llm/types.ts +0 -63
@@ -0,0 +1,499 @@
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
+ /**
81
+ * If the workspace has usable credentials but no pinned model in config,
82
+ * write the resolved default model to `.libretto/config.json`.
83
+ */
84
+ function ensurePinnedDefaultModel(
85
+ status: AiSetupStatus & { kind: "ready" },
86
+ ): AiSetupStatus & { kind: "ready" } {
87
+ if (status.source !== "config") {
88
+ writeAiConfig(status.model);
89
+ return { ...status, source: "config" as const };
90
+ }
91
+ return status;
92
+ }
93
+
94
+ function printHealthySummary(status: AiSetupStatus & { kind: "ready" }): void {
95
+ console.log(` ✓ Model: ${status.model}`);
96
+ console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
97
+ console.log(
98
+ " To change: npx libretto ai configure openai | anthropic | gemini | vertex",
99
+ );
100
+ }
101
+
102
+ function printInvalidAiConfigWarning(status: AiSetupStatus): void {
103
+ if (status.kind !== "invalid-config") return;
104
+ console.log(" ! Existing AI config is invalid:");
105
+ for (const line of status.message.split("\n")) {
106
+ console.log(` ${line}`);
107
+ }
108
+ }
109
+
110
+ // ── Repair plan helpers (exported for testing) ──────────────────────────────
111
+
112
+ export type RepairChoice =
113
+ | "enter-matching-credential"
114
+ | "switch-provider"
115
+ | "skip";
116
+
117
+ export type RepairPlan =
118
+ | {
119
+ kind: "repair-missing-credentials";
120
+ provider: Provider;
121
+ model: string;
122
+ envVar: string;
123
+ choices: RepairChoice[];
124
+ }
125
+ | { kind: "repair-invalid-config"; message: string }
126
+ | { kind: "no-repair-needed" };
127
+
128
+ /**
129
+ * Determine what repair action setup should take for the current AI status.
130
+ * Pure function — no I/O, no prompts.
131
+ */
132
+ export function buildRepairPlan(status: AiSetupStatus): RepairPlan {
133
+ if (status.kind === "configured-missing-credentials") {
134
+ const choice = PROVIDER_CHOICES.find((c) => c.provider === status.provider);
135
+ return {
136
+ kind: "repair-missing-credentials",
137
+ provider: status.provider,
138
+ model: status.model,
139
+ envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
140
+ choices: ["enter-matching-credential", "switch-provider", "skip"],
141
+ };
142
+ }
143
+ if (status.kind === "invalid-config") {
144
+ return { kind: "repair-invalid-config", message: status.message };
145
+ }
146
+ return { kind: "no-repair-needed" };
147
+ }
148
+
149
+ /**
150
+ * Format a provider-specific explanation for missing credentials.
151
+ */
152
+ export function formatMissingCredentialsMessage(
153
+ plan: RepairPlan & { kind: "repair-missing-credentials" },
154
+ ): string {
155
+ return [
156
+ ` ✗ ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`,
157
+ ].join("\n");
158
+ }
159
+
160
+ function printSnapshotApiStatus(): boolean {
161
+ const status = resolveAiSetupStatus();
162
+ const envPath = join(REPO_ROOT, ".env");
163
+
164
+ console.log("\nSnapshot analysis:");
165
+ console.log(
166
+ " Libretto uses direct API calls for snapshot analysis when supported credentials are available.",
167
+ );
168
+ console.log(` Credentials are loaded from process env and ${envPath}.`);
169
+
170
+ if (status.kind === "ready") {
171
+ const pinned = ensurePinnedDefaultModel(status);
172
+ printHealthySummary(pinned);
173
+ return true;
174
+ }
175
+
176
+ // Provider-specific missing-credentials message
177
+ const plan = buildRepairPlan(status);
178
+ if (plan.kind === "repair-missing-credentials") {
179
+ console.log(formatMissingCredentialsMessage(plan));
180
+ console.log(
181
+ ` To fix: add ${plan.envVar} to .env, or run \`npx libretto setup\` interactively to repair.`,
182
+ );
183
+ return false;
184
+ }
185
+
186
+ if (plan.kind === "repair-invalid-config") {
187
+ printInvalidAiConfigWarning(status);
188
+ console.log(" Run `npx libretto setup` interactively to reconfigure.");
189
+ return false;
190
+ }
191
+
192
+ console.log(" ✗ No snapshot API credentials detected.");
193
+ console.log(" Add one provider to .env:");
194
+ console.log(" OPENAI_API_KEY=...");
195
+ console.log(" ANTHROPIC_API_KEY=...");
196
+ console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
197
+ console.log(
198
+ " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex",
199
+ );
200
+ console.log(
201
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model.",
202
+ );
203
+ console.log(
204
+ " Run `npx libretto setup` interactively to set up credentials.",
205
+ );
206
+ return false;
207
+ }
208
+
209
+ /**
210
+ * Write an env var to the .env file and update process.env.
211
+ */
212
+ function writeEnvVar(envVar: string, value: string, envPath: string): void {
213
+ let envContent = "";
214
+ if (existsSync(envPath)) {
215
+ envContent = readFileSync(envPath, "utf-8");
216
+ }
217
+
218
+ const envLine = `${envVar}=${value}`;
219
+ if (envContent.includes(`${envVar}=`)) {
220
+ const updated = envContent.replace(
221
+ new RegExp(`^${envVar}=.*$`, "m"),
222
+ () => envLine,
223
+ );
224
+ writeFileSync(envPath, updated);
225
+ console.log(`\n ✓ Updated ${envVar} in ${envPath}`);
226
+ } else {
227
+ const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
228
+ appendFileSync(envPath, `${separator}${envLine}\n`);
229
+ console.log(`\n ✓ Added ${envVar} to ${envPath}`);
230
+ }
231
+
232
+ process.env[envVar] = value;
233
+ }
234
+
235
+ /**
236
+ * Prompt the user to enter a credential for a specific provider and pin its model.
237
+ * When modelOverride is provided (e.g. during repair), preserves the existing model
238
+ * instead of resetting to the provider default.
239
+ * Returns true if credential was entered successfully.
240
+ */
241
+ async function promptForCredential(
242
+ rl: ReturnType<typeof createInterface>,
243
+ choice: ProviderChoice,
244
+ envPath: string,
245
+ modelOverride?: string,
246
+ ): Promise<boolean> {
247
+ console.log(`\n ${choice.label} selected.`);
248
+ console.log(` ${choice.envHint}\n`);
249
+
250
+ const apiKeyValue = await promptUser(rl, ` Enter your ${choice.envVar}: `);
251
+
252
+ if (!apiKeyValue) {
253
+ console.log("\n No value entered. Skipping API key setup.");
254
+ return false;
255
+ }
256
+
257
+ writeEnvVar(choice.envVar, apiKeyValue, envPath);
258
+ loadSnapshotEnv();
259
+
260
+ const model = modelOverride ?? DEFAULT_SNAPSHOT_MODELS[choice.provider];
261
+ writeAiConfig(model);
262
+ console.log(` ✓ Snapshot API ready: ${model}`);
263
+ return true;
264
+ }
265
+
266
+ /**
267
+ * Run the full provider selection menu and credential entry.
268
+ * Returns true if a provider was successfully configured.
269
+ */
270
+ async function promptProviderSelection(
271
+ rl: ReturnType<typeof createInterface>,
272
+ envPath: string,
273
+ ): Promise<boolean> {
274
+ console.log(
275
+ " Which API provider would you like to use for snapshot analysis?\n",
276
+ );
277
+ for (const choice of PROVIDER_CHOICES) {
278
+ console.log(` ${choice.key}) ${choice.label}`);
279
+ }
280
+ console.log(" s) Skip for now\n");
281
+
282
+ const answer = await promptUser(rl, " Choice: ");
283
+
284
+ if (answer.toLowerCase() === "s" || !answer) {
285
+ printSkipMessage();
286
+ return false;
287
+ }
288
+
289
+ const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
290
+ if (!selected) {
291
+ console.log(`\n Unknown choice "${answer}". Skipping API setup.`);
292
+ return false;
293
+ }
294
+
295
+ return promptForCredential(rl, selected, envPath);
296
+ }
297
+
298
+ function printSkipMessage(): void {
299
+ console.log(
300
+ "\n Skipped. You can set up API credentials later by rerunning `npx libretto setup`.",
301
+ );
302
+ console.log(" Or add credentials directly to your .env file:");
303
+ console.log(" OPENAI_API_KEY=...");
304
+ console.log(" ANTHROPIC_API_KEY=...");
305
+ console.log(" GEMINI_API_KEY=...");
306
+ console.log(
307
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model.",
308
+ );
309
+ }
310
+
311
+ async function runInteractiveApiSetup(): Promise<void> {
312
+ const status = resolveAiSetupStatus();
313
+ const envPath = join(REPO_ROOT, ".env");
314
+
315
+ console.log("\nSnapshot analysis setup:");
316
+ console.log(" Libretto uses direct API calls for snapshot analysis.");
317
+ console.log(` Credentials are loaded from process env and ${envPath}.`);
318
+
319
+ if (status.kind === "ready") {
320
+ const pinned = ensurePinnedDefaultModel(status);
321
+ printHealthySummary(pinned);
322
+ return;
323
+ }
324
+
325
+ const plan = buildRepairPlan(status);
326
+
327
+ const rl = createInterface({
328
+ input: process.stdin,
329
+ output: process.stdout,
330
+ });
331
+
332
+ try {
333
+ // ── Repair: configured provider with missing credentials ──
334
+ if (plan.kind === "repair-missing-credentials") {
335
+ console.log(formatMissingCredentialsMessage(plan));
336
+ console.log("");
337
+ console.log(" How would you like to fix this?\n");
338
+ console.log(` 1) Enter ${plan.envVar}`);
339
+ console.log(" 2) Switch to a different provider");
340
+ console.log(" s) Skip for now\n");
341
+
342
+ const answer = await promptUser(rl, " Choice: ");
343
+
344
+ if (answer === "1") {
345
+ const matchingChoice = PROVIDER_CHOICES.find(
346
+ (c) => c.provider === plan.provider,
347
+ );
348
+ if (matchingChoice) {
349
+ await promptForCredential(rl, matchingChoice, envPath, plan.model);
350
+ }
351
+ return;
352
+ }
353
+
354
+ if (answer === "2") {
355
+ await promptProviderSelection(rl, envPath);
356
+ return;
357
+ }
358
+
359
+ // skip or empty
360
+ printSkipMessage();
361
+ return;
362
+ }
363
+
364
+ // ── Repair: invalid config → let user pick a provider ──
365
+ if (plan.kind === "repair-invalid-config") {
366
+ printInvalidAiConfigWarning(status);
367
+ console.log(
368
+ "\n Would you like to reconfigure with a fresh provider selection?\n",
369
+ );
370
+ await promptProviderSelection(rl, envPath);
371
+ return;
372
+ }
373
+
374
+ // ── Unconfigured: standard first-run flow ──
375
+ console.log(" ✗ No snapshot API credentials detected.\n");
376
+ await promptProviderSelection(rl, envPath);
377
+ } finally {
378
+ rl.close();
379
+ }
380
+ }
381
+
382
+ function installBrowsers(): void {
383
+ console.log("\nInstalling Playwright Chromium...");
384
+ const result = spawnSync("npx", ["playwright", "install", "chromium"], {
385
+ stdio: "inherit",
386
+ shell: true,
387
+ });
388
+ if (result.status === 0) {
389
+ console.log(" ✓ Playwright Chromium installed");
390
+ } else {
391
+ console.error(
392
+ " ✗ Failed to install Playwright Chromium. Run manually: npx playwright install chromium",
393
+ );
394
+ }
395
+ }
396
+
397
+ function getPackageSkillsRoot(): string {
398
+ const thisFile = fileURLToPath(import.meta.url);
399
+ // Walk up from dist/cli/commands/ to package root
400
+ let dir = dirname(thisFile);
401
+ while (dir !== dirname(dir)) {
402
+ if (existsSync(join(dir, "skills", "libretto"))) {
403
+ return join(dir, "skills");
404
+ }
405
+ dir = dirname(dir);
406
+ }
407
+ throw new Error("Could not locate libretto skill files in package");
408
+ }
409
+
410
+ /**
411
+ * Auto-detect .agents/ and .claude/ directories at a given root path.
412
+ */
413
+ function detectAgentDirs(root: string): string[] {
414
+ const dirs: string[] = [];
415
+ if (existsSync(join(root, ".agents"))) dirs.push(join(root, ".agents"));
416
+ if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
417
+ return dirs;
418
+ }
419
+
420
+ function copySkills(): void {
421
+ const agentDirs = detectAgentDirs(REPO_ROOT);
422
+
423
+ if (agentDirs.length === 0) {
424
+ console.log(
425
+ "\nSkills: No .agents/ or .claude/ directory found in repo root — skipping.",
426
+ );
427
+ return;
428
+ }
429
+
430
+ let skillsRoot: string;
431
+ try {
432
+ skillsRoot = getPackageSkillsRoot();
433
+ } catch (e) {
434
+ console.error(` ✗ ${e instanceof Error ? e.message : String(e)}`);
435
+ return;
436
+ }
437
+
438
+ const skillNames = readdirSync(skillsRoot, { withFileTypes: true })
439
+ .filter((entry) => entry.isDirectory())
440
+ .map((entry) => entry.name)
441
+ .sort();
442
+
443
+ for (const agentDir of agentDirs) {
444
+ const agentName = basename(agentDir);
445
+
446
+ for (const skillName of skillNames) {
447
+ const sourceDir = join(skillsRoot, skillName);
448
+ const skillDest = join(agentDir, "skills", skillName);
449
+ if (existsSync(skillDest)) {
450
+ rmSync(skillDest, { recursive: true });
451
+ }
452
+ cpSync(sourceDir, skillDest, { recursive: true });
453
+ const fileCount = readdirSync(skillDest).length;
454
+ console.log(
455
+ ` ✓ Copied ${fileCount} skill files to ${agentName}/skills/${skillName}/`,
456
+ );
457
+ }
458
+ }
459
+ }
460
+
461
+ export const setupInput = SimpleCLI.input({
462
+ positionals: [],
463
+ named: {
464
+ skipBrowsers: SimpleCLI.flag({
465
+ name: "skip-browsers",
466
+ help: "Skip Playwright Chromium installation",
467
+ }),
468
+ },
469
+ });
470
+
471
+ export const setupCommand = SimpleCLI.command({
472
+ description: "Set up libretto in the current project",
473
+ })
474
+ .input(setupInput)
475
+ .handle(async ({ input }) => {
476
+ console.log("Setting up libretto...\n");
477
+ ensureLibrettoSetup();
478
+
479
+ if (!input.skipBrowsers) {
480
+ installBrowsers();
481
+ } else {
482
+ console.log("\nSkipping browser installation (--skip-browsers)");
483
+ }
484
+
485
+ copySkills();
486
+
487
+ if (process.stdin.isTTY) {
488
+ await runInteractiveApiSetup();
489
+ } else {
490
+ const ready = printSnapshotApiStatus();
491
+ if (!ready) {
492
+ console.log(
493
+ "\nIf you're an agent, request the user to run `npx libretto setup`.",
494
+ );
495
+ }
496
+ }
497
+
498
+ console.log("\n✓ libretto setup complete");
499
+ });
@@ -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,77 @@
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 = `http://127.0.0.1:${session.port}`;
61
+ console.log(` ${session.session}${statusLabel} — ${endpoint}`);
62
+ }
63
+ }
64
+
65
+ // ── Command ─────────────────────────────────────────────────────────────────
66
+
67
+ export const statusCommand = SimpleCLI.command({
68
+ description: "Show workspace status: AI configuration and open sessions",
69
+ })
70
+ .input(SimpleCLI.input({ positionals: [], named: {} }))
71
+ .handle(async () => {
72
+ const aiStatus = resolveAiSetupStatus();
73
+ printAiStatus(aiStatus);
74
+
75
+ const sessions = listRunningSessions();
76
+ printOpenSessions(sessions);
77
+ });