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
@@ -19,16 +19,18 @@ import {
19
19
  getSessionNetworkLogPath,
20
20
  PROFILES_DIR
21
21
  } from "./context.js";
22
- import { readLibrettoConfig } from "./ai-config.js";
22
+ import { readLibrettoConfig } from "./config.js";
23
23
  import {
24
24
  assertSessionAvailableForStart,
25
25
  clearSessionState,
26
+ isPidRunning,
26
27
  listSessionsWithStateFile,
27
28
  readSessionStateOrThrow,
28
29
  logFileForSession,
29
30
  readSessionState,
30
31
  writeSessionState
31
32
  } from "./session.js";
33
+ import { getCloudProviderApi } from "./providers/index.js";
32
34
  import { installSessionTelemetry } from "./session-telemetry.js";
33
35
  const CLOSE_WAIT_MS = 1500;
34
36
  const FORCE_CLOSE_WAIT_MS = 300;
@@ -178,6 +180,11 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
178
180
  endpoint,
179
181
  pid: state.pid
180
182
  });
183
+ if (state.provider) {
184
+ throw new Error(
185
+ `Could not connect to ${state.provider.name} session for "${session}" at ${endpoint}. The remote session may still be active. Try again, or close with: libretto close --session ${session}`
186
+ );
187
+ }
181
188
  if (state.pid == null || !isPidRunning(state.pid)) {
182
189
  clearSessionState(session, logger);
183
190
  throw new Error(
@@ -293,8 +300,16 @@ async function runOpen(rawUrl, headed, session, logger, options) {
293
300
  const parsedUrl = normalizeUrl(rawUrl);
294
301
  const url = parsedUrl.href;
295
302
  const viewport = resolveViewport(options?.viewport, logger);
303
+ const accessMode = options?.accessMode ?? "write-access";
296
304
  const windowPosition = headed ? resolveWindowPosition(logger) : void 0;
297
- logger.info("open-start", { url, headed, session, viewport, windowPosition });
305
+ logger.info("open-start", {
306
+ url,
307
+ headed,
308
+ session,
309
+ viewport,
310
+ windowPosition,
311
+ accessMode
312
+ });
298
313
  assertSessionAvailableForStart(session, logger);
299
314
  const port = await pickFreePort();
300
315
  const runLogPath = logFileForSession(session);
@@ -523,6 +538,7 @@ await new Promise(() => {});
523
538
  session,
524
539
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
525
540
  status: "active",
541
+ mode: accessMode,
526
542
  viewport
527
543
  },
528
544
  logger
@@ -549,6 +565,86 @@ await new Promise(() => {});
549
565
  `Failed to connect to browser after ${Math.ceil(cdpStartupTimeoutMs / 1e3)}s. Check startup logs: ${runLogPath}`
550
566
  );
551
567
  }
568
+ async function runOpenWithProvider(rawUrl, providerName, provider, session, logger, accessMode = "write-access") {
569
+ const parsedUrl = normalizeUrl(rawUrl);
570
+ const url = parsedUrl.href;
571
+ logger.info("open-provider-start", { url, provider: providerName, session });
572
+ console.log(
573
+ `Creating ${providerName} browser session (session: ${session})...`
574
+ );
575
+ const providerSession = await provider.createSession();
576
+ logger.info("open-provider-session-created", {
577
+ provider: providerName,
578
+ sessionId: providerSession.sessionId,
579
+ cdpEndpoint: providerSession.cdpEndpoint
580
+ });
581
+ console.log(`Connecting to ${providerName} browser...`);
582
+ let browser = null;
583
+ try {
584
+ browser = await tryConnectToCDP(
585
+ providerSession.cdpEndpoint,
586
+ logger,
587
+ 3e4
588
+ );
589
+ if (!browser) {
590
+ throw new Error(
591
+ `Could not connect to ${providerName} browser at ${providerSession.cdpEndpoint}. The remote session was created but CDP connection failed.`
592
+ );
593
+ }
594
+ const contexts = browser.contexts();
595
+ let page;
596
+ if (contexts.length > 0 && contexts[0].pages().length > 0) {
597
+ page = contexts[0].pages()[0];
598
+ } else {
599
+ const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
600
+ page = await context.newPage();
601
+ }
602
+ await page.goto(url);
603
+ logger.info("open-provider-navigated", { url, session });
604
+ writeSessionState(
605
+ {
606
+ port: 0,
607
+ cdpEndpoint: providerSession.cdpEndpoint,
608
+ session,
609
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
610
+ status: "active",
611
+ mode: accessMode,
612
+ provider: {
613
+ name: providerName,
614
+ sessionId: providerSession.sessionId
615
+ }
616
+ },
617
+ logger
618
+ );
619
+ disconnectBrowser(browser, logger, session);
620
+ } catch (err) {
621
+ if (browser) {
622
+ disconnectBrowser(browser, logger, session);
623
+ }
624
+ logger.warn("open-provider-cleanup-after-error", {
625
+ provider: providerName,
626
+ sessionId: providerSession.sessionId,
627
+ error: err
628
+ });
629
+ try {
630
+ await provider.closeSession(providerSession.sessionId);
631
+ } catch (cleanupErr) {
632
+ logger.warn("open-provider-cleanup-failed", {
633
+ provider: providerName,
634
+ sessionId: providerSession.sessionId,
635
+ error: cleanupErr
636
+ });
637
+ }
638
+ throw err;
639
+ }
640
+ logger.info("open-provider-success", {
641
+ url,
642
+ provider: providerName,
643
+ session,
644
+ sessionId: providerSession.sessionId
645
+ });
646
+ console.log(`Browser open (${providerName}): ${url}`);
647
+ }
552
648
  async function runSave(urlOrDomain, session, logger) {
553
649
  logger.info("save-start", { urlOrDomain, session });
554
650
  const { browser, context, page } = await connect(session, logger);
@@ -621,10 +717,33 @@ async function runClose(session, logger) {
621
717
  console.log(`No browser running for session "${session}".`);
622
718
  return;
623
719
  }
624
- logger.info("close-killing", { session, pid: state.pid, port: state.port });
625
- if (state.pid != null) {
626
- sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
627
- await waitForCloseSignalWindow(CLOSE_WAIT_MS);
720
+ if (state.provider) {
721
+ logger.info("close-provider", {
722
+ session,
723
+ provider: state.provider.name,
724
+ sessionId: state.provider.sessionId
725
+ });
726
+ try {
727
+ const provider = getCloudProviderApi(state.provider.name);
728
+ await provider.closeSession(state.provider.sessionId);
729
+ } catch (err) {
730
+ logger.warn("close-provider-error", {
731
+ session,
732
+ provider: state.provider.name,
733
+ sessionId: state.provider.sessionId,
734
+ error: err
735
+ });
736
+ writeSessionState({ ...state, status: "cleanup-failed" }, logger);
737
+ throw new Error(
738
+ `Failed to close remote ${state.provider.name} session "${state.provider.sessionId}" for session "${session}". State preserved with status "cleanup-failed". Retry with: libretto close --session ${session}`
739
+ );
740
+ }
741
+ } else {
742
+ logger.info("close-killing", { session, pid: state.pid, port: state.port });
743
+ if (state.pid != null) {
744
+ sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
745
+ await waitForCloseSignalWindow(CLOSE_WAIT_MS);
746
+ }
628
747
  }
629
748
  clearSessionState(session, logger);
630
749
  logger.info("close-success", { session });
@@ -633,14 +752,6 @@ async function runClose(session, logger) {
633
752
  function waitForCloseSignalWindow(ms) {
634
753
  return new Promise((r) => setTimeout(r, ms));
635
754
  }
636
- function isPidRunning(pid) {
637
- try {
638
- process.kill(pid, 0);
639
- return true;
640
- } catch {
641
- return false;
642
- }
643
- }
644
755
  function sendSignalToProcessGroupOrPid(pid, signal, logger, session) {
645
756
  try {
646
757
  process.kill(pid, signal);
@@ -674,14 +785,16 @@ function resolveClosableSessions(logger) {
674
785
  closable.push({
675
786
  session,
676
787
  pid: state.pid,
677
- port: state.port
788
+ port: state.port,
789
+ provider: state.provider
678
790
  });
679
791
  }
680
792
  return { closable, clearedUnreadableStates };
681
793
  }
682
- function clearStoppedSessionStates(sessions, logger) {
794
+ function clearStoppedSessionStates(sessions, logger, skip) {
683
795
  let cleared = 0;
684
796
  for (const session of sessions) {
797
+ if (skip?.has(session.session)) continue;
685
798
  if (session.pid == null || !isPidRunning(session.pid)) {
686
799
  clearSessionState(session.session, logger);
687
800
  cleared += 1;
@@ -702,7 +815,34 @@ async function runCloseAll(logger, options) {
702
815
  console.log("No browser sessions found.");
703
816
  return;
704
817
  }
818
+ const failedProviderSessions = /* @__PURE__ */ new Set();
705
819
  for (const target of closable) {
820
+ if (target.provider) {
821
+ logger.info("close-all-provider", {
822
+ session: target.session,
823
+ provider: target.provider.name,
824
+ sessionId: target.provider.sessionId
825
+ });
826
+ try {
827
+ const provider = getCloudProviderApi(target.provider.name);
828
+ await provider.closeSession(target.provider.sessionId);
829
+ } catch (err) {
830
+ logger.warn("close-all-provider-error", {
831
+ session: target.session,
832
+ provider: target.provider.name,
833
+ sessionId: target.provider.sessionId,
834
+ error: err
835
+ });
836
+ failedProviderSessions.add(target.session);
837
+ const state = readSessionState(target.session, logger);
838
+ if (state) {
839
+ writeSessionState({ ...state, status: "cleanup-failed" }, logger);
840
+ }
841
+ }
842
+ }
843
+ }
844
+ for (const target of closable) {
845
+ if (target.provider) continue;
706
846
  logger.info("close-all-sigterm", {
707
847
  session: target.session,
708
848
  pid: target.pid,
@@ -722,7 +862,11 @@ async function runCloseAll(logger, options) {
722
862
  (target) => target.pid != null && isPidRunning(target.pid)
723
863
  );
724
864
  if (survivors.length > 0 && !force) {
725
- const closed = clearStoppedSessionStates(closable, logger);
865
+ const closed = clearStoppedSessionStates(
866
+ closable,
867
+ logger,
868
+ failedProviderSessions
869
+ );
726
870
  throw new Error(
727
871
  [
728
872
  `Failed to close ${survivors.length} session(s) gracefully: ${formatSessionList(survivors)}.`,
@@ -753,7 +897,11 @@ async function runCloseAll(logger, options) {
753
897
  (target) => target.pid != null && isPidRunning(target.pid)
754
898
  );
755
899
  if (survivors.length > 0) {
756
- const closed = clearStoppedSessionStates(closable, logger);
900
+ const closed = clearStoppedSessionStates(
901
+ closable,
902
+ logger,
903
+ failedProviderSessions
904
+ );
757
905
  throw new Error(
758
906
  [
759
907
  `Failed to force-close ${survivors.length} session(s): ${formatSessionList(survivors)}.`,
@@ -762,19 +910,25 @@ async function runCloseAll(logger, options) {
762
910
  );
763
911
  }
764
912
  }
765
- clearStoppedSessionStates(closable, logger);
913
+ clearStoppedSessionStates(closable, logger, failedProviderSessions);
914
+ if (failedProviderSessions.size > 0) {
915
+ console.log(
916
+ `Warning: ${failedProviderSessions.size} provider session(s) failed remote cleanup and were preserved with status "cleanup-failed".`
917
+ );
918
+ }
766
919
  if (clearedUnreadableStates > 0) {
767
920
  console.log(
768
921
  `Cleared ${clearedUnreadableStates} unreadable session state file(s).`
769
922
  );
770
923
  }
771
- console.log(`Closed ${closable.length} session(s).`);
924
+ const closedCount = closable.length - failedProviderSessions.size;
925
+ console.log(`Closed ${closedCount} session(s).`);
772
926
  if (forceKilled > 0) {
773
927
  console.log(`Force-killed ${forceKilled} session(s).`);
774
928
  }
775
929
  }
776
- async function runConnect(cdpUrl, session, logger) {
777
- logger.info("connect-start", { cdpUrl, session });
930
+ async function runConnect(cdpUrl, session, logger, accessMode = "write-access") {
931
+ logger.info("connect-start", { cdpUrl, session, accessMode });
778
932
  assertSessionAvailableForStart(session, logger);
779
933
  let parsedUrl;
780
934
  try {
@@ -784,28 +938,38 @@ async function runConnect(cdpUrl, session, logger) {
784
938
  [
785
939
  `Invalid CDP URL: ${cdpUrl}`,
786
940
  ``,
787
- `Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
941
+ `Expected an HTTP or WebSocket URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
788
942
  ` libretto connect http://127.0.0.1:9222`,
789
943
  ` libretto connect http://remote-host:9222`,
790
- ` libretto connect http://remote-host:9222/devtools/browser/<id>`
944
+ ` libretto connect http://remote-host:9222/devtools/browser/<id>`,
945
+ ` libretto connect ws://remote-host:9222/devtools/browser/<id>`,
946
+ ` libretto connect wss://remote-host/cdp-endpoint`
791
947
  ].join("\n")
792
948
  );
793
949
  }
794
950
  const endpoint = parsedUrl.href;
795
- const port = parsedUrl.port ? Number(parsedUrl.port) : parsedUrl.protocol === "https:" ? 443 : 80;
951
+ const isWebSocket = parsedUrl.protocol === "ws:" || parsedUrl.protocol === "wss:";
952
+ const port = parsedUrl.port ? Number(parsedUrl.port) : parsedUrl.protocol === "https:" || parsedUrl.protocol === "wss:" ? 443 : 80;
796
953
  console.log(
797
954
  `Connecting to CDP endpoint at ${endpoint} (session: ${session})...`
798
955
  );
799
- const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
800
- try {
801
- const resp = await fetch(versionUrl);
802
- const versionInfo = await resp.json();
803
- logger.info("connect-version-ok", { versionUrl, versionInfo });
804
- } catch (err) {
805
- logger.error("connect-version-failed", { versionUrl, error: err });
806
- throw new Error(
807
- `Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`
808
- );
956
+ if (!isWebSocket) {
957
+ const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
958
+ try {
959
+ const resp = await fetch(versionUrl);
960
+ const versionInfo = await resp.json();
961
+ logger.info("connect-version-ok", { versionUrl, versionInfo });
962
+ } catch (err) {
963
+ logger.error("connect-version-failed", { versionUrl, error: err });
964
+ throw new Error(
965
+ `Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`
966
+ );
967
+ }
968
+ } else {
969
+ logger.info("connect-skip-version-check", {
970
+ reason: "WebSocket-only endpoint, skipping HTTP version check",
971
+ endpoint
972
+ });
809
973
  }
810
974
  const browser = await tryConnectToCDP(endpoint, logger, 1e4);
811
975
  if (!browser) {
@@ -826,7 +990,8 @@ async function runConnect(cdpUrl, session, logger) {
826
990
  cdpEndpoint: endpoint,
827
991
  session,
828
992
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
829
- status: "active"
993
+ status: "active",
994
+ mode: accessMode
830
995
  },
831
996
  logger
832
997
  );
@@ -865,6 +1030,7 @@ export {
865
1030
  runCloseAll,
866
1031
  runConnect,
867
1032
  runOpen,
1033
+ runOpenWithProvider,
868
1034
  runPages,
869
1035
  runSave
870
1036
  };
@@ -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
  const CURRENT_CONFIG_VERSION = 1;
6
7
  const AiConfigSchema = z.object({
@@ -19,27 +20,10 @@ const LibrettoConfigSchema = z.object({
19
20
  version: z.literal(CURRENT_CONFIG_VERSION),
20
21
  ai: AiConfigSchema.optional(),
21
22
  viewport: ViewportConfigSchema.optional(),
22
- windowPosition: WindowPositionConfigSchema.optional()
23
+ windowPosition: WindowPositionConfigSchema.optional(),
24
+ provider: z.string().optional(),
25
+ sessionMode: SessionAccessModeSchema.optional()
23
26
  }).passthrough();
24
- const DEFAULT_MODELS = {
25
- openai: "openai/gpt-5.4",
26
- anthropic: "anthropic/claude-sonnet-4-6",
27
- gemini: "google/gemini-3-flash-preview",
28
- vertex: "vertex/gemini-2.5-pro"
29
- };
30
- const PROVIDER_ALIASES = {
31
- claude: DEFAULT_MODELS.anthropic,
32
- google: DEFAULT_MODELS.gemini
33
- };
34
- const CONFIGURE_PROVIDERS = [
35
- "openai",
36
- "anthropic",
37
- "gemini",
38
- "vertex"
39
- ];
40
- function formatConfigureProviders(separator = " | ") {
41
- return CONFIGURE_PROVIDERS.join(separator);
42
- }
43
27
  function formatConfigIssues(error) {
44
28
  return error.issues.map((issue) => ` - ${issue.path.join(".") || "root"}: ${issue.message}`).join("\n");
45
29
  }
@@ -58,7 +42,8 @@ function formatExpectedConfigExample() {
58
42
  windowPosition: {
59
43
  x: 1600,
60
44
  y: 120
61
- }
45
+ },
46
+ sessionMode: "write-access"
62
47
  },
63
48
  null,
64
49
  2
@@ -73,10 +58,10 @@ ${detail}` : null,
73
58
  "Expected config example:",
74
59
  formatExpectedConfigExample(),
75
60
  "Notes:",
76
- ' - "ai", "viewport", and "windowPosition" are optional.',
61
+ ' - "ai", "viewport", "windowPosition", and "sessionMode" are optional.',
77
62
  ' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
78
63
  "Fix the file to match this shape, or delete it and rerun:",
79
- ` npx libretto ai configure ${formatConfigureProviders()}`
64
+ ` npx libretto ai configure openai | anthropic | gemini | vertex`
80
65
  ].filter(Boolean).join("\n")
81
66
  );
82
67
  }
@@ -112,7 +97,12 @@ function readAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
112
97
  return readLibrettoConfig(configPath).ai ?? null;
113
98
  }
114
99
  function writeAiConfig(model, configPath = LIBRETTO_CONFIG_PATH) {
115
- const librettoConfig = readLibrettoConfig(configPath);
100
+ let librettoConfig;
101
+ try {
102
+ librettoConfig = readLibrettoConfig(configPath);
103
+ } catch {
104
+ librettoConfig = { version: CURRENT_CONFIG_VERSION };
105
+ }
116
106
  const ai = AiConfigSchema.parse({
117
107
  model,
118
108
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -139,60 +129,6 @@ function clearAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
139
129
  );
140
130
  return true;
141
131
  }
142
- function printAiConfig(config, configPath) {
143
- console.log(`Model: ${config.model}`);
144
- console.log(`Config file: ${configPath}`);
145
- console.log(`Updated at: ${config.updatedAt}`);
146
- }
147
- function resolveModelFromInput(input) {
148
- const trimmed = input.trim();
149
- if (!trimmed) return null;
150
- if (trimmed.includes("/")) return trimmed;
151
- const normalized = trimmed.toLowerCase();
152
- return DEFAULT_MODELS[normalized] ?? PROVIDER_ALIASES[normalized] ?? null;
153
- }
154
- function runAiConfigure(input, options = {}) {
155
- const configureCommandName = options.configureCommandName ?? "npx libretto ai configure";
156
- const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
157
- const presetArg = input.preset?.trim();
158
- if (!presetArg && !input.clear) {
159
- const config2 = readAiConfig(configPath);
160
- if (!config2) {
161
- console.log(
162
- `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
163
- );
164
- console.log(
165
- "Provider credentials still come from your shell or .env file."
166
- );
167
- return;
168
- }
169
- printAiConfig(config2, configPath);
170
- return;
171
- }
172
- if (input.clear) {
173
- const removed = clearAiConfig(configPath);
174
- if (removed) {
175
- console.log(`Cleared AI config: ${configPath}`);
176
- } else {
177
- console.log("No AI config was set.");
178
- }
179
- return;
180
- }
181
- const model = resolveModelFromInput(presetArg);
182
- if (!model) {
183
- console.log(
184
- `Usage: ${configureCommandName} <${CONFIGURE_PROVIDERS.join("|")}|provider/model-id>
185
- ${configureCommandName}
186
- ${configureCommandName} --clear`
187
- );
188
- throw new Error(
189
- `Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`
190
- );
191
- }
192
- const config = writeAiConfig(model, configPath);
193
- console.log("AI config saved.");
194
- printAiConfig(config, configPath);
195
- }
196
132
  export {
197
133
  AiConfigSchema,
198
134
  CURRENT_CONFIG_VERSION,
@@ -202,7 +138,6 @@ export {
202
138
  clearAiConfig,
203
139
  readAiConfig,
204
140
  readLibrettoConfig,
205
- runAiConfigure,
206
141
  writeAiConfig,
207
142
  writeLibrettoConfig
208
143
  };
@@ -54,34 +54,14 @@ function createLoggerForSession(session) {
54
54
  [createFileLogSink({ filePath: logFilePath })]
55
55
  );
56
56
  }
57
- async function closeLogger(logger) {
58
- if (!logger) return;
59
- await logger.close();
60
- }
61
57
  async function withSessionLogger(session, run) {
62
58
  const logger = createLoggerForSession(session);
63
59
  try {
64
60
  return await run(logger);
65
61
  } finally {
66
- await closeLogger(logger);
62
+ await logger.close();
67
63
  }
68
64
  }
69
- let llmClientFactory = null;
70
- function setLLMClientFactory(factory) {
71
- llmClientFactory = factory;
72
- }
73
- function getLLMClientFactory() {
74
- return llmClientFactory;
75
- }
76
- function maybeConfigureLLMClientFactoryFromEnv() {
77
- if (llmClientFactory) return;
78
- const hasAnyCreds = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
79
- if (!hasAnyCreds) return;
80
- setLLMClientFactory(async (_logger, model) => {
81
- const { createLLMClient } = await import("../../shared/llm/index.js");
82
- return createLLMClient(model);
83
- });
84
- }
85
65
  export {
86
66
  LIBRETTO_CONFIG_DIR,
87
67
  LIBRETTO_CONFIG_PATH,
@@ -89,10 +69,8 @@ export {
89
69
  LIBRETTO_SESSIONS_DIR,
90
70
  PROFILES_DIR,
91
71
  REPO_ROOT,
92
- closeLogger,
93
72
  createLoggerForSession,
94
73
  ensureLibrettoSetup,
95
- getLLMClientFactory,
96
74
  getSessionActionsLogPath,
97
75
  getSessionDir,
98
76
  getSessionLogsPath,
@@ -100,7 +78,5 @@ export {
100
78
  getSessionSnapshotRunDir,
101
79
  getSessionSnapshotsDir,
102
80
  getSessionStatePath,
103
- maybeConfigureLLMClientFactoryFromEnv,
104
- setLLMClientFactory,
105
81
  withSessionLogger
106
82
  };