libretto 0.5.5 → 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 +39 -26
  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 +17 -13
  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 -1
  46. package/dist/shared/workflow/workflow.js +16 -9
  47. package/package.json +17 -16
  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 +3 -3
  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 +45 -26
  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 +16 -16
  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 -13
  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
@@ -1,29 +1,56 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { type AiConfig, readAiConfig } from "./ai-config.js";
3
+ import { type AiConfig, 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
8
  type Provider,
9
- } from "../../shared/llm/client.js";
9
+ } from "./resolve-model.js";
10
10
 
11
- const DEFAULT_SNAPSHOT_MODELS = {
11
+ // ── Default models ──────────────────────────────────────────────────────────
12
+
13
+ export const DEFAULT_SNAPSHOT_MODELS = {
12
14
  openai: "openai/gpt-5.4",
13
15
  anthropic: "anthropic/claude-sonnet-4-6",
14
16
  google: "google/gemini-3-flash-preview",
15
- vertex: "vertex/gemini-2.5-pro",
17
+ vertex: "vertex/gemini-2.5-flash",
16
18
  } as const satisfies Record<Provider, string>;
17
19
 
20
+ // ── Source detection ────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Detect which specific env var provides credentials for a provider.
24
+ * Returns the env var name (e.g. "OPENAI_API_KEY", "GEMINI_API_KEY"),
25
+ * or null if no credential is found.
26
+ */
27
+ function detectProviderEnvVar(
28
+ provider: Provider,
29
+ env: NodeJS.ProcessEnv = process.env,
30
+ ): string | null {
31
+ switch (provider) {
32
+ case "openai":
33
+ return env.OPENAI_API_KEY?.trim() ? "OPENAI_API_KEY" : null;
34
+ case "anthropic":
35
+ return env.ANTHROPIC_API_KEY?.trim() ? "ANTHROPIC_API_KEY" : null;
36
+ case "google":
37
+ if (env.GEMINI_API_KEY?.trim()) return "GEMINI_API_KEY";
38
+ if (env.GOOGLE_GENERATIVE_AI_API_KEY?.trim())
39
+ return "GOOGLE_GENERATIVE_AI_API_KEY";
40
+ return null;
41
+ case "vertex":
42
+ if (env.GOOGLE_CLOUD_PROJECT?.trim()) return "GOOGLE_CLOUD_PROJECT";
43
+ if (env.GCLOUD_PROJECT?.trim()) return "GCLOUD_PROJECT";
44
+ return null;
45
+ }
46
+ }
47
+
48
+ // ── Snapshot model resolution ───────────────────────────────────────────────
49
+
18
50
  export type SnapshotApiModelSelection = {
19
51
  model: string;
20
52
  provider: Provider;
21
- source:
22
- | "config"
23
- | "env:auto-openai"
24
- | "env:auto-anthropic"
25
- | "env:auto-google"
26
- | "env:auto-vertex";
53
+ source: "config" | `env:${string}`;
27
54
  };
28
55
 
29
56
  export class SnapshotApiUnavailableError extends Error {
@@ -67,7 +94,7 @@ function noSnapshotApiConfiguredMessage(): string {
67
94
  return [
68
95
  "Failed to analyze snapshot because no snapshot analyzer is configured.",
69
96
  `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()}\`.`,
70
- "For more info, run `npx libretto init`.",
97
+ "For more info, run `npx libretto setup`.",
71
98
  ].join(" ");
72
99
  }
73
100
 
@@ -81,10 +108,12 @@ function missingProviderSnapshotMessage(
81
108
  return [
82
109
  `Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
83
110
  providerSetupSentence(selection.provider),
84
- "For more info, run `npx libretto init`.",
111
+ "For more info, run `npx libretto setup`.",
85
112
  ].join(" ");
86
113
  }
87
114
 
115
+ // ── Dotenv loading ──────────────────────────────────────────────────────────
116
+
88
117
  function readWorktreeEnvPath(): string | null {
89
118
  const gitPath = join(REPO_ROOT, ".git");
90
119
  if (!existsSync(gitPath)) return null;
@@ -165,6 +194,8 @@ export function parseDotEnvAssignment(
165
194
  return { key, value };
166
195
  }
167
196
 
197
+ // ── Model resolution ────────────────────────────────────────────────────────
198
+
168
199
  function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
169
200
  const providersInPriorityOrder: Provider[] = [
170
201
  "openai",
@@ -174,11 +205,12 @@ function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
174
205
  ];
175
206
 
176
207
  for (const provider of providersInPriorityOrder) {
177
- if (!hasProviderCredentials(provider)) continue;
208
+ const envVar = detectProviderEnvVar(provider);
209
+ if (!envVar) continue;
178
210
  return {
179
211
  model: DEFAULT_SNAPSHOT_MODELS[provider],
180
212
  provider,
181
- source: `env:auto-${provider}` as SnapshotApiModelSelection["source"],
213
+ source: `env:${envVar}`,
182
214
  };
183
215
  }
184
216
 
@@ -229,3 +261,111 @@ export function resolveSnapshotApiModelOrThrow(
229
261
  export function isSnapshotApiUnavailableError(error: unknown): boolean {
230
262
  return error instanceof SnapshotApiUnavailableError;
231
263
  }
264
+
265
+ // ── AI setup status ─────────────────────────────────────────────────────────
266
+
267
+ /**
268
+ * Workspace AI setup health states.
269
+ *
270
+ * - `ready`: a usable model was resolved and the matching provider has credentials.
271
+ * - `configured-missing-credentials`: config pins a provider whose credentials are absent.
272
+ * - `invalid-config`: `.libretto/config.json` exists but fails schema validation.
273
+ * - `unconfigured`: no config and no env credentials detected.
274
+ */
275
+ export type AiSetupStatus =
276
+ | {
277
+ kind: "ready";
278
+ model: string;
279
+ provider: Provider;
280
+ source: "config" | `env:${string}`;
281
+ }
282
+ | {
283
+ kind: "configured-missing-credentials";
284
+ model: string;
285
+ provider: Provider;
286
+ }
287
+ | { kind: "invalid-config"; message: string }
288
+ | { kind: "unconfigured" };
289
+
290
+ /**
291
+ * Read AI config without throwing on invalid files.
292
+ * Returns the config or an error message.
293
+ */
294
+ function readAiConfigSafely(
295
+ configPath: string,
296
+ ): { ok: true; config: AiConfig | null } | { ok: false; message: string } {
297
+ try {
298
+ return { ok: true, config: readAiConfig(configPath) };
299
+ } catch (err) {
300
+ return {
301
+ ok: false,
302
+ message: err instanceof Error ? err.message : String(err),
303
+ };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Resolve the workspace's current AI setup health.
309
+ *
310
+ * Uses the existing config reader and snapshot model resolver, but wraps
311
+ * them to distinguish broken states (invalid config, missing credentials)
312
+ * that the throwing APIs collapse into errors.
313
+ *
314
+ * 1. If config read throws → `invalid-config`.
315
+ * 2. If config has an `ai` block → check credentials for that provider.
316
+ * 3. If no config or no `ai` block → auto-detect from env via existing resolver.
317
+ */
318
+ export function resolveAiSetupStatus(
319
+ configPath: string = LIBRETTO_CONFIG_PATH,
320
+ ): AiSetupStatus {
321
+ loadSnapshotEnv();
322
+
323
+ const configResult = readAiConfigSafely(configPath);
324
+
325
+ if (!configResult.ok) {
326
+ return { kind: "invalid-config", message: configResult.message };
327
+ }
328
+
329
+ // Config exists with an ai block — use it directly to check credentials
330
+ if (configResult.config) {
331
+ let selection: SnapshotApiModelSelection | null;
332
+ try {
333
+ selection = resolveSnapshotApiModel(configResult.config);
334
+ } catch (err) {
335
+ return {
336
+ kind: "invalid-config",
337
+ message: err instanceof Error ? err.message : String(err),
338
+ };
339
+ }
340
+ if (!selection) {
341
+ // Should not happen when config has a model, but handle gracefully
342
+ return { kind: "unconfigured" };
343
+ }
344
+ if (hasProviderCredentials(selection.provider)) {
345
+ return {
346
+ kind: "ready",
347
+ model: selection.model,
348
+ provider: selection.provider,
349
+ source: selection.source,
350
+ };
351
+ }
352
+ return {
353
+ kind: "configured-missing-credentials",
354
+ model: selection.model,
355
+ provider: selection.provider,
356
+ };
357
+ }
358
+
359
+ // No ai config — fall back to env auto-detect via existing resolver
360
+ const envSelection = resolveSnapshotApiModel(null);
361
+ if (envSelection && hasProviderCredentials(envSelection.provider)) {
362
+ return {
363
+ kind: "ready",
364
+ model: envSelection.model,
365
+ provider: envSelection.provider,
366
+ source: envSelection.source,
367
+ };
368
+ }
369
+
370
+ return { kind: "unconfigured" };
371
+ }
@@ -8,7 +8,8 @@
8
8
 
9
9
  import { readFileSync } from "node:fs";
10
10
  import type { LoggerApi } from "../../shared/logger/index.js";
11
- import { createLLMClient } from "../../shared/llm/client.js";
11
+ import { generateObject } from "ai";
12
+ import { resolveModel } from "./resolve-model.js";
12
13
  import {
13
14
  InterpretResultSchema,
14
15
  buildInlinePromptSelection,
@@ -17,8 +18,8 @@ import {
17
18
  type InterpretResult,
18
19
  type InterpretArgs,
19
20
  } from "./snapshot-analyzer.js";
20
- import { readAiConfig, type AiConfig } from "./ai-config.js";
21
- import { resolveSnapshotApiModelOrThrow } from "./snapshot-api-config.js";
21
+ import { readAiConfig, type AiConfig } from "./config.js";
22
+ import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
22
23
 
23
24
  export async function runApiInterpret(
24
25
  args: InterpretArgs,
@@ -64,9 +65,10 @@ export async function runApiInterpret(
64
65
  const imageMimeType = getMimeType(args.pngPath);
65
66
  const imageBytes = Buffer.from(imageBase64, "base64");
66
67
 
67
- const client = createLLMClient(selection.model);
68
+ const model = await resolveModel(selection.model);
68
69
 
69
- const result = await client.generateObjectFromMessages({
70
+ const { object: result } = await generateObject({
71
+ model,
70
72
  schema: InterpretResultSchema,
71
73
  messages: [
72
74
  {
@@ -12,6 +12,7 @@ import { createRequire } from "node:module";
12
12
  import { createServer } from "node:net";
13
13
  import { spawn } from "node:child_process";
14
14
  import type { LoggerApi } from "../../shared/logger/index.js";
15
+ import type { SessionAccessMode } from "../../shared/state/index.js";
15
16
  import {
16
17
  filterSemanticClasses,
17
18
  INTERACTIVE_ROLE_NAMES,
@@ -25,10 +26,11 @@ import {
25
26
  getSessionNetworkLogPath,
26
27
  PROFILES_DIR,
27
28
  } from "./context.js";
28
- import { readLibrettoConfig } from "./ai-config.js";
29
+ import { readLibrettoConfig } from "./config.js";
29
30
  import {
30
31
  assertSessionAvailableForStart,
31
32
  clearSessionState,
33
+ isPidRunning,
32
34
  listSessionsWithStateFile,
33
35
  readSessionStateOrThrow,
34
36
  logFileForSession,
@@ -398,13 +400,24 @@ export async function runOpen(
398
400
  headed: boolean,
399
401
  session: string,
400
402
  logger: LoggerApi,
401
- options?: { viewport?: { width: number; height: number } },
403
+ options?: {
404
+ viewport?: { width: number; height: number };
405
+ accessMode?: SessionAccessMode;
406
+ },
402
407
  ): Promise<void> {
403
408
  const parsedUrl = normalizeUrl(rawUrl);
404
409
  const url = parsedUrl.href;
405
410
  const viewport = resolveViewport(options?.viewport, logger);
411
+ const accessMode = options?.accessMode ?? "write-access";
406
412
  const windowPosition = headed ? resolveWindowPosition(logger) : undefined;
407
- logger.info("open-start", { url, headed, session, viewport, windowPosition });
413
+ logger.info("open-start", {
414
+ url,
415
+ headed,
416
+ session,
417
+ viewport,
418
+ windowPosition,
419
+ accessMode,
420
+ });
408
421
  assertSessionAvailableForStart(session, logger);
409
422
 
410
423
  const port = await pickFreePort();
@@ -674,6 +687,7 @@ await new Promise(() => {});
674
687
  session,
675
688
  startedAt: new Date().toISOString(),
676
689
  status: "active",
690
+ mode: accessMode,
677
691
  viewport,
678
692
  },
679
693
  logger,
@@ -819,15 +833,6 @@ function waitForCloseSignalWindow(ms: number): Promise<void> {
819
833
  return new Promise((r) => setTimeout(r, ms));
820
834
  }
821
835
 
822
- function isPidRunning(pid: number): boolean {
823
- try {
824
- process.kill(pid, 0);
825
- return true;
826
- } catch {
827
- return false;
828
- }
829
- }
830
-
831
836
  function sendSignalToProcessGroupOrPid(
832
837
  pid: number,
833
838
  signal: NodeJS.Signals,
@@ -993,8 +998,9 @@ export async function runConnect(
993
998
  cdpUrl: string,
994
999
  session: string,
995
1000
  logger: LoggerApi,
1001
+ accessMode: SessionAccessMode = "write-access",
996
1002
  ): Promise<void> {
997
- logger.info("connect-start", { cdpUrl, session });
1003
+ logger.info("connect-start", { cdpUrl, session, accessMode });
998
1004
  assertSessionAvailableForStart(session, logger);
999
1005
 
1000
1006
  let parsedUrl: URL;
@@ -1005,18 +1011,22 @@ export async function runConnect(
1005
1011
  [
1006
1012
  `Invalid CDP URL: ${cdpUrl}`,
1007
1013
  ``,
1008
- `Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
1014
+ `Expected an HTTP or WebSocket URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
1009
1015
  ` libretto connect http://127.0.0.1:9222`,
1010
1016
  ` libretto connect http://remote-host:9222`,
1011
1017
  ` libretto connect http://remote-host:9222/devtools/browser/<id>`,
1018
+ ` libretto connect ws://remote-host:9222/devtools/browser/<id>`,
1019
+ ` libretto connect wss://remote-host/cdp-endpoint`,
1012
1020
  ].join("\n"),
1013
1021
  );
1014
1022
  }
1015
1023
 
1016
1024
  const endpoint = parsedUrl.href;
1025
+ const isWebSocket =
1026
+ parsedUrl.protocol === "ws:" || parsedUrl.protocol === "wss:";
1017
1027
  const port = parsedUrl.port
1018
1028
  ? Number(parsedUrl.port)
1019
- : parsedUrl.protocol === "https:"
1029
+ : parsedUrl.protocol === "https:" || parsedUrl.protocol === "wss:"
1020
1030
  ? 443
1021
1031
  : 80;
1022
1032
 
@@ -1024,17 +1034,25 @@ export async function runConnect(
1024
1034
  `Connecting to CDP endpoint at ${endpoint} (session: ${session})...`,
1025
1035
  );
1026
1036
 
1027
- // Verify the CDP endpoint is reachable
1028
- const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
1029
- try {
1030
- const resp = await fetch(versionUrl);
1031
- const versionInfo = await resp.json();
1032
- logger.info("connect-version-ok", { versionUrl, versionInfo });
1033
- } catch (err) {
1034
- logger.error("connect-version-failed", { versionUrl, error: err });
1035
- throw new Error(
1036
- `Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`,
1037
- );
1037
+ // Verify the CDP endpoint is reachable (HTTP only — WebSocket
1038
+ // endpoints are validated by the Playwright connect call below).
1039
+ if (!isWebSocket) {
1040
+ const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
1041
+ try {
1042
+ const resp = await fetch(versionUrl);
1043
+ const versionInfo = await resp.json();
1044
+ logger.info("connect-version-ok", { versionUrl, versionInfo });
1045
+ } catch (err) {
1046
+ logger.error("connect-version-failed", { versionUrl, error: err });
1047
+ throw new Error(
1048
+ `Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`,
1049
+ );
1050
+ }
1051
+ } else {
1052
+ logger.info("connect-skip-version-check", {
1053
+ reason: "WebSocket-only endpoint, skipping HTTP version check",
1054
+ endpoint,
1055
+ });
1038
1056
  }
1039
1057
 
1040
1058
  // Connect via CDP using the full endpoint URL
@@ -1061,6 +1079,7 @@ export async function runConnect(
1061
1079
  session,
1062
1080
  startedAt: new Date().toISOString(),
1063
1081
  status: "active",
1082
+ mode: accessMode,
1064
1083
  },
1065
1084
  logger,
1066
1085
  );
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { z } from "zod";
4
+ import { SessionAccessModeSchema } from "../../shared/state/index.js";
4
5
  import { LIBRETTO_CONFIG_PATH } from "./context.js";
5
6
 
6
7
  export const CURRENT_CONFIG_VERSION = 1;
@@ -41,34 +42,11 @@ export const LibrettoConfigSchema = z
41
42
  ai: AiConfigSchema.optional(),
42
43
  viewport: ViewportConfigSchema.optional(),
43
44
  windowPosition: WindowPositionConfigSchema.optional(),
45
+ sessionMode: SessionAccessModeSchema.optional(),
44
46
  })
45
47
  .passthrough();
46
48
  export type LibrettoConfig = z.infer<typeof LibrettoConfigSchema>;
47
49
 
48
- /** Default models for each provider shorthand accepted by `ai configure`. */
49
- const DEFAULT_MODELS: Record<string, string> = {
50
- openai: "openai/gpt-5.4",
51
- anthropic: "anthropic/claude-sonnet-4-6",
52
- gemini: "google/gemini-3-flash-preview",
53
- vertex: "vertex/gemini-2.5-pro",
54
- };
55
-
56
- const PROVIDER_ALIASES: Record<string, string> = {
57
- claude: DEFAULT_MODELS.anthropic,
58
- google: DEFAULT_MODELS.gemini,
59
- };
60
-
61
- const CONFIGURE_PROVIDERS = [
62
- "openai",
63
- "anthropic",
64
- "gemini",
65
- "vertex",
66
- ] as const;
67
-
68
- function formatConfigureProviders(separator = " | "): string {
69
- return CONFIGURE_PROVIDERS.join(separator);
70
- }
71
-
72
50
  function formatConfigIssues(error: z.ZodError): string {
73
51
  return error.issues
74
52
  .map((issue) => ` - ${issue.path.join(".") || "root"}: ${issue.message}`)
@@ -91,6 +69,7 @@ function formatExpectedConfigExample(): string {
91
69
  x: 1600,
92
70
  y: 120,
93
71
  },
72
+ sessionMode: "write-access",
94
73
  },
95
74
  null,
96
75
  2,
@@ -105,10 +84,10 @@ function invalidConfigError(configPath: string, detail?: string): Error {
105
84
  "Expected config example:",
106
85
  formatExpectedConfigExample(),
107
86
  "Notes:",
108
- ' - "ai", "viewport", and "windowPosition" are optional.',
87
+ ' - "ai", "viewport", "windowPosition", and "sessionMode" are optional.',
109
88
  ' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
110
89
  "Fix the file to match this shape, or delete it and rerun:",
111
- ` npx libretto ai configure ${formatConfigureProviders()}`,
90
+ ` npx libretto ai configure openai | anthropic | gemini | vertex`,
112
91
  ]
113
92
  .filter(Boolean)
114
93
  .join("\n"),
@@ -162,7 +141,14 @@ export function writeAiConfig(
162
141
  model: string,
163
142
  configPath: string = LIBRETTO_CONFIG_PATH,
164
143
  ): AiConfig {
165
- const librettoConfig = readLibrettoConfig(configPath);
144
+ let librettoConfig: LibrettoConfig;
145
+ try {
146
+ librettoConfig = readLibrettoConfig(configPath);
147
+ } catch {
148
+ // Existing config is malformed — start fresh so repair flows can
149
+ // overwrite a broken file instead of throwing.
150
+ librettoConfig = { version: CURRENT_CONFIG_VERSION };
151
+ }
166
152
  const ai = AiConfigSchema.parse({
167
153
  model,
168
154
  updatedAt: new Date().toISOString(),
@@ -192,84 +178,3 @@ export function clearAiConfig(
192
178
  );
193
179
  return true;
194
180
  }
195
-
196
- function printAiConfig(config: AiConfig, configPath: string): void {
197
- console.log(`Model: ${config.model}`);
198
- console.log(`Config file: ${configPath}`);
199
- console.log(`Updated at: ${config.updatedAt}`);
200
- }
201
-
202
- /**
203
- * Resolve the model string from a `ai configure` argument.
204
- * Accepts a provider shorthand ("openai", "anthropic", "gemini", "vertex")
205
- * or a full provider/model-id string ("openai/gpt-4o", "anthropic/claude-sonnet-4-6").
206
- */
207
- function resolveModelFromInput(input: string): string | null {
208
- const trimmed = input.trim();
209
- if (!trimmed) return null;
210
-
211
- // Full model string (contains a slash)
212
- if (trimmed.includes("/")) return trimmed;
213
-
214
- // Provider shorthand
215
- const normalized = trimmed.toLowerCase();
216
- return DEFAULT_MODELS[normalized] ?? PROVIDER_ALIASES[normalized] ?? null;
217
- }
218
-
219
- export function runAiConfigure(
220
- input: {
221
- preset?: string;
222
- clear?: boolean;
223
- },
224
- options: {
225
- configureCommandName?: string;
226
- configPath?: string;
227
- } = {},
228
- ): void {
229
- const configureCommandName =
230
- options.configureCommandName ?? "npx libretto ai configure";
231
- const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
232
-
233
- const presetArg = input.preset?.trim();
234
-
235
- if (!presetArg && !input.clear) {
236
- const config = readAiConfig(configPath);
237
- if (!config) {
238
- console.log(
239
- `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
240
- );
241
- console.log(
242
- "Provider credentials still come from your shell or .env file.",
243
- );
244
- return;
245
- }
246
- printAiConfig(config, configPath);
247
- return;
248
- }
249
-
250
- if (input.clear) {
251
- const removed = clearAiConfig(configPath);
252
- if (removed) {
253
- console.log(`Cleared AI config: ${configPath}`);
254
- } else {
255
- console.log("No AI config was set.");
256
- }
257
- return;
258
- }
259
-
260
- const model = resolveModelFromInput(presetArg!);
261
- if (!model) {
262
- console.log(
263
- `Usage: ${configureCommandName} <${CONFIGURE_PROVIDERS.join("|")}|provider/model-id>\n` +
264
- ` ${configureCommandName}\n` +
265
- ` ${configureCommandName} --clear`,
266
- );
267
- throw new Error(
268
- `Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`,
269
- );
270
- }
271
-
272
- const config = writeAiConfig(model, configPath);
273
- console.log("AI config saved.");
274
- printAiConfig(config, configPath);
275
- }
@@ -1,6 +1,4 @@
1
1
  import { Logger, createFileLogSink } from "../../shared/logger/index.js";
2
- import type { LLMClient } from "../../shared/llm/index.js";
3
- import type { LoggerApi } from "../../shared/logger/index.js";
4
2
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
3
  import { join } from "node:path";
6
4
  import { resolveLibrettoRepoRoot } from "../../shared/paths/repo-root.js";
@@ -72,13 +70,6 @@ export function createLoggerForSession(session: string): Logger {
72
70
  );
73
71
  }
74
72
 
75
- export async function closeLogger(
76
- logger: Logger | null | undefined,
77
- ): Promise<void> {
78
- if (!logger) return;
79
- await logger.close();
80
- }
81
-
82
73
  export async function withSessionLogger<T>(
83
74
  session: string,
84
75
  run: (logger: Logger) => Promise<T>,
@@ -87,41 +78,6 @@ export async function withSessionLogger<T>(
87
78
  try {
88
79
  return await run(logger);
89
80
  } finally {
90
- await closeLogger(logger);
81
+ await logger.close();
91
82
  }
92
83
  }
93
-
94
- let llmClientFactory:
95
- | ((logger: LoggerApi, model: string) => Promise<LLMClient>)
96
- | null = null;
97
-
98
- export function setLLMClientFactory(
99
- factory: (logger: LoggerApi, model: string) => Promise<LLMClient>,
100
- ): void {
101
- llmClientFactory = factory;
102
- }
103
-
104
- export function getLLMClientFactory():
105
- | ((logger: LoggerApi, model: string) => Promise<LLMClient>)
106
- | null {
107
- return llmClientFactory;
108
- }
109
-
110
- export function maybeConfigureLLMClientFactoryFromEnv(): void {
111
- if (llmClientFactory) return;
112
-
113
- const hasAnyCreds =
114
- process.env.GOOGLE_CLOUD_PROJECT ||
115
- process.env.GCLOUD_PROJECT ||
116
- process.env.ANTHROPIC_API_KEY ||
117
- process.env.OPENAI_API_KEY ||
118
- process.env.GEMINI_API_KEY ||
119
- process.env.GOOGLE_GENERATIVE_AI_API_KEY;
120
-
121
- if (!hasAnyCreds) return;
122
-
123
- setLLMClientFactory(async (_logger, model) => {
124
- const { createLLMClient } = await import("../../shared/llm/index.js");
125
- return createLLMClient(model);
126
- });
127
- }