libretto 0.6.9 → 0.6.11

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 (60) hide show
  1. package/dist/cli/cli.js +2 -0
  2. package/dist/cli/commands/auth.js +535 -0
  3. package/dist/cli/commands/billing.js +74 -0
  4. package/dist/cli/commands/browser.js +8 -3
  5. package/dist/cli/commands/deploy.js +2 -7
  6. package/dist/cli/commands/execution.js +99 -136
  7. package/dist/cli/commands/snapshot.js +38 -126
  8. package/dist/cli/core/ai-model.js +0 -3
  9. package/dist/cli/core/auth-fetch.js +195 -0
  10. package/dist/cli/core/auth-storage.js +52 -0
  11. package/dist/cli/core/browser.js +128 -202
  12. package/dist/cli/core/daemon/config.js +6 -0
  13. package/dist/cli/core/daemon/daemon.js +298 -0
  14. package/dist/cli/core/daemon/exec.js +86 -0
  15. package/dist/cli/core/daemon/index.js +16 -0
  16. package/dist/cli/core/daemon/ipc.js +171 -0
  17. package/dist/cli/core/daemon/pages.js +15 -0
  18. package/dist/cli/core/daemon/snapshot.js +86 -0
  19. package/dist/cli/core/daemon/spawn.js +90 -0
  20. package/dist/cli/core/exec-compiler.js +111 -0
  21. package/dist/cli/core/prompt.js +72 -0
  22. package/dist/cli/core/providers/libretto-cloud.js +2 -6
  23. package/dist/cli/core/readonly-exec.js +1 -1
  24. package/dist/cli/router.js +4 -0
  25. package/dist/cli/workers/run-integration-runtime.js +0 -5
  26. package/dist/shared/state/session-state.d.ts +1 -0
  27. package/dist/shared/state/session-state.js +2 -1
  28. package/docs/browser-automation-approaches.md +435 -0
  29. package/docs/releasing.md +117 -0
  30. package/package.json +4 -3
  31. package/skills/libretto/SKILL.md +14 -1
  32. package/skills/libretto-readonly/SKILL.md +1 -1
  33. package/src/cli/cli.ts +2 -0
  34. package/src/cli/commands/auth.ts +787 -0
  35. package/src/cli/commands/billing.ts +133 -0
  36. package/src/cli/commands/browser.ts +8 -2
  37. package/src/cli/commands/deploy.ts +2 -7
  38. package/src/cli/commands/execution.ts +126 -186
  39. package/src/cli/commands/snapshot.ts +46 -143
  40. package/src/cli/core/ai-model.ts +4 -5
  41. package/src/cli/core/auth-fetch.ts +283 -0
  42. package/src/cli/core/auth-storage.ts +102 -0
  43. package/src/cli/core/browser.ts +159 -242
  44. package/src/cli/core/daemon/config.ts +46 -0
  45. package/src/cli/core/daemon/daemon.ts +429 -0
  46. package/src/cli/core/daemon/exec.ts +128 -0
  47. package/src/cli/core/daemon/index.ts +24 -0
  48. package/src/cli/core/daemon/ipc.ts +294 -0
  49. package/src/cli/core/daemon/pages.ts +21 -0
  50. package/src/cli/core/daemon/snapshot.ts +114 -0
  51. package/src/cli/core/daemon/spawn.ts +171 -0
  52. package/src/cli/core/exec-compiler.ts +169 -0
  53. package/src/cli/core/prompt.ts +94 -0
  54. package/src/cli/core/providers/libretto-cloud.ts +2 -6
  55. package/src/cli/core/readonly-exec.ts +2 -1
  56. package/src/cli/router.ts +4 -0
  57. package/src/cli/workers/run-integration-runtime.ts +0 -6
  58. package/src/shared/state/session-state.ts +1 -0
  59. package/dist/cli/core/browser-daemon.js +0 -122
  60. package/src/cli/core/browser-daemon.ts +0 -198
@@ -16,17 +16,23 @@ import {
16
16
  assertSessionAllowsCommand,
17
17
  clearSessionState,
18
18
  readSessionState,
19
+ readSessionStateOrThrow,
19
20
  setSessionStatus
20
21
  } from "../core/session.js";
21
22
  import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
23
+ import { readLibrettoConfig } from "../core/config.js";
24
+ import { resolveProviderName, getCloudProviderApi } from "../core/providers/index.js";
25
+ import {
26
+ compileExecFunction,
27
+ stripEmptyCatchHandlers
28
+ } from "../core/exec-compiler.js";
29
+ import { DaemonClient } from "../core/daemon/index.js";
30
+ import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
22
31
  import {
23
32
  readActionLog,
24
33
  readNetworkLog,
25
34
  wrapPageForActionLogging
26
35
  } from "../core/telemetry.js";
27
- import { readLibrettoConfig } from "../core/config.js";
28
- import { resolveProviderName, getCloudProviderApi } from "../core/providers/index.js";
29
- import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
30
36
  import { SimpleCLI } from "../framework/simple-cli.js";
31
37
  import {
32
38
  pageOption,
@@ -34,113 +40,59 @@ import {
34
40
  withAutoSession,
35
41
  withRequiredSession
36
42
  } from "./shared.js";
37
- const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
38
43
  const require2 = moduleBuiltin.createRequire(import.meta.url);
39
44
  const tsxCliPath = require2.resolve("tsx/cli");
40
- function withSuppressedStripTypeScriptWarning(action) {
41
- const mutableProcess = process;
42
- const originalEmitWarning = mutableProcess.emitWarning;
43
- mutableProcess.emitWarning = (...args) => {
44
- const warning = args[0];
45
- const typeOrOptions = args[1];
46
- const warningMessage = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
47
- const warningType = typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type ?? "" : "";
48
- if (warningType === "ExperimentalWarning" && warningMessage.includes("stripTypeScriptTypes")) {
49
- return;
50
- }
51
- originalEmitWarning(...args);
52
- };
53
- try {
54
- return action();
55
- } finally {
56
- mutableProcess.emitWarning = originalEmitWarning;
45
+ function writeDaemonExecOutput(output) {
46
+ if (output?.stdout) {
47
+ process.stdout.write(output.stdout);
48
+ }
49
+ if (output?.stderr) {
50
+ process.stderr.write(output.stderr);
57
51
  }
58
52
  }
59
- function compileTypeScriptExecFunction(code, helperNames) {
60
- if (!stripTypeScriptTypes) return null;
61
- const wrappedSource = `(async function __librettoExec(${helperNames.join(", ")}) {
62
- ${code}
63
- })`;
64
- const jsSource = withSuppressedStripTypeScriptWarning(
65
- () => stripTypeScriptTypes(wrappedSource, { mode: "strip" })
66
- );
67
- const createFunction = new Function(
68
- `return ${jsSource}`
69
- );
70
- return createFunction();
71
- }
72
- function compileExecFunction(code, helperNames) {
73
- const typeStripped = compileTypeScriptExecFunction(code, helperNames);
74
- if (typeStripped) return typeStripped;
75
- const AsyncFunction = Object.getPrototypeOf(async function() {
76
- }).constructor;
77
- return new AsyncFunction(...helperNames, code);
78
- }
79
- function stripEmptyCatchHandlers(code) {
80
- const catchRe = /\??\s*\.catch\(\s*\(\)\s*=>\s*\{\s*\}\s*\)/g;
81
- let strippedCount = 0;
82
- let result = "";
83
- let i = 0;
84
- while (i < code.length) {
85
- if (code[i] === "/" && code[i + 1] === "/") {
86
- const end = code.indexOf("\n", i);
87
- const slice = end === -1 ? code.slice(i) : code.slice(i, end + 1);
88
- result += slice;
89
- i += slice.length;
90
- continue;
91
- }
92
- if (code[i] === "/" && code[i + 1] === "*") {
93
- const end = code.indexOf("*/", i + 2);
94
- const slice = end === -1 ? code.slice(i) : code.slice(i, end + 2);
95
- result += slice;
96
- i += slice.length;
97
- continue;
98
- }
99
- if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
100
- const quote = code[i];
101
- let j = i + 1;
102
- while (j < code.length) {
103
- if (code[j] === "\\" && quote !== "`") {
104
- j += 2;
105
- continue;
106
- }
107
- if (code[j] === "\\" && quote === "`") {
108
- j += 2;
109
- continue;
110
- }
111
- if (code[j] === quote) {
112
- j++;
113
- break;
114
- }
115
- if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
116
- let depth = 1;
117
- j += 2;
118
- while (j < code.length && depth > 0) {
119
- if (code[j] === "{") depth++;
120
- else if (code[j] === "}") depth--;
121
- j++;
122
- }
123
- continue;
124
- }
125
- j++;
126
- }
127
- result += code.slice(i, j);
128
- i = j;
129
- continue;
130
- }
131
- catchRe.lastIndex = i;
132
- const match = catchRe.exec(code);
133
- if (match && match.index === i) {
134
- strippedCount++;
135
- i += match[0].length;
136
- continue;
137
- }
138
- result += code[i];
139
- i++;
53
+ async function execViaDaemon(code, session, daemonSocketPath, logger, options) {
54
+ const mode = options.mode ?? "exec";
55
+ const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
56
+ if (strippedCount > 0) {
57
+ console.log("(Stripped `.catch(() => {})` \u2014 letting errors bubble up)");
58
+ }
59
+ logger.info(`${mode}-start`, {
60
+ session,
61
+ codeLength: cleanedCode.length,
62
+ codePreview: cleanedCode.slice(0, 200),
63
+ visualize: options.visualize,
64
+ pageId: options.pageId,
65
+ via: "daemon"
66
+ });
67
+ const client = new DaemonClient(daemonSocketPath);
68
+ const response = mode === "exec" ? await client.exec({
69
+ code: cleanedCode,
70
+ pageId: options.pageId,
71
+ visualize: options.visualize
72
+ }) : await client.readonlyExec({
73
+ code: cleanedCode,
74
+ pageId: options.pageId
75
+ });
76
+ if (!response.ok) {
77
+ writeDaemonExecOutput(response.output);
78
+ throw new Error(response.message);
79
+ }
80
+ const { result, output } = response.data;
81
+ writeDaemonExecOutput(output);
82
+ logger.info(`${mode}-success`, {
83
+ session,
84
+ hasResult: result !== void 0,
85
+ via: "daemon"
86
+ });
87
+ if (result !== void 0) {
88
+ console.log(
89
+ typeof result === "string" ? result : JSON.stringify(result, null, 2)
90
+ );
91
+ } else {
92
+ console.log("Executed successfully");
140
93
  }
141
- return { cleaned: result, strippedCount };
142
94
  }
143
- async function runExec(code, session, logger, options = {}) {
95
+ async function execViaCdpFallback(code, session, logger, options) {
144
96
  const visualize = options.visualize ?? false;
145
97
  const pageId = options.pageId;
146
98
  const mode = options.mode ?? "exec";
@@ -153,7 +105,8 @@ async function runExec(code, session, logger, options = {}) {
153
105
  codeLength: cleanedCode.length,
154
106
  codePreview: cleanedCode.slice(0, 200),
155
107
  visualize,
156
- pageId
108
+ pageId,
109
+ via: "cdp-fallback"
157
110
  });
158
111
  const {
159
112
  browser,
@@ -161,8 +114,7 @@ async function runExec(code, session, logger, options = {}) {
161
114
  page,
162
115
  pageId: resolvedPageId
163
116
  } = await connect(session, logger, 1e4, {
164
- pageId,
165
- requireSinglePage: true
117
+ pageId
166
118
  });
167
119
  const STALL_THRESHOLD_MS = 6e4;
168
120
  let lastActivityTs = Date.now();
@@ -175,7 +127,8 @@ async function runExec(code, session, logger, options = {}) {
175
127
  logger.warn(`${mode}-stall-warning`, {
176
128
  session,
177
129
  silenceMs,
178
- codePreview: cleanedCode.slice(0, 200)
130
+ codePreview: cleanedCode.slice(0, 200),
131
+ via: "cdp-fallback"
179
132
  });
180
133
  console.warn(
181
134
  `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 ${mode} may be hung (code: ${cleanedCode.slice(0, 100)}...)`
@@ -187,7 +140,8 @@ async function runExec(code, session, logger, options = {}) {
187
140
  logger.info(`${mode}-interrupted`, {
188
141
  session,
189
142
  duration: Date.now() - execStartTs,
190
- codePreview: cleanedCode.slice(0, 200)
143
+ codePreview: cleanedCode.slice(0, 200),
144
+ via: "cdp-fallback"
191
145
  });
192
146
  };
193
147
  process.on("SIGINT", sigintHandler);
@@ -198,35 +152,31 @@ async function runExec(code, session, logger, options = {}) {
198
152
  await installInstrumentation(page, { visualize: true, logger });
199
153
  }
200
154
  try {
201
- const helpers = mode === "readonly-exec" ? createReadonlyExecHelpers(page, { onActivity }) : (() => {
202
- const execState = {};
203
- const networkLog = (opts = {}) => {
204
- return readNetworkLog(session, opts);
205
- };
206
- const actionLog = (opts = {}) => {
207
- return readActionLog(session, opts);
208
- };
209
- return {
210
- page,
211
- context,
212
- state: execState,
213
- browser,
214
- networkLog,
215
- actionLog,
216
- console,
217
- setTimeout,
218
- setInterval,
219
- clearTimeout,
220
- clearInterval,
221
- fetch,
222
- URL,
223
- Buffer
224
- };
225
- })();
155
+ const execState = {};
156
+ const helpers = mode === "readonly-exec" ? createReadonlyExecHelpers(page, { onActivity }) : {
157
+ page,
158
+ context,
159
+ state: execState,
160
+ browser,
161
+ networkLog: (opts = {}) => readNetworkLog(session, opts),
162
+ actionLog: (opts = {}) => readActionLog(session, opts),
163
+ console,
164
+ setTimeout,
165
+ setInterval,
166
+ clearTimeout,
167
+ clearInterval,
168
+ fetch,
169
+ URL,
170
+ Buffer
171
+ };
226
172
  const helperNames = Object.keys(helpers);
227
173
  const fn = compileExecFunction(cleanedCode, helperNames);
228
174
  const result = await fn(...Object.values(helpers));
229
- logger.info(`${mode}-success`, { session, hasResult: result !== void 0 });
175
+ logger.info(`${mode}-success`, {
176
+ session,
177
+ hasResult: result !== void 0,
178
+ via: "cdp-fallback"
179
+ });
230
180
  if (result !== void 0) {
231
181
  console.log(
232
182
  typeof result === "string" ? result : JSON.stringify(result, null, 2)
@@ -238,7 +188,8 @@ async function runExec(code, session, logger, options = {}) {
238
188
  logger.error(`${mode}-error`, {
239
189
  error: err,
240
190
  session,
241
- codePreview: cleanedCode.slice(0, 200)
191
+ codePreview: cleanedCode.slice(0, 200),
192
+ via: "cdp-fallback"
242
193
  });
243
194
  throw err;
244
195
  } finally {
@@ -247,6 +198,18 @@ async function runExec(code, session, logger, options = {}) {
247
198
  disconnectBrowser(browser, logger, session);
248
199
  }
249
200
  }
201
+ async function runExec(code, session, logger, options = {}) {
202
+ const state = readSessionStateOrThrow(session);
203
+ if (!state.daemonSocketPath) {
204
+ logger.warn(`${options.mode ?? "exec"}-daemon-socket-missing-cdp-fallback`, {
205
+ session,
206
+ hasCdpEndpoint: Boolean(state.cdpEndpoint),
207
+ port: state.port
208
+ });
209
+ return execViaCdpFallback(code, session, logger, options);
210
+ }
211
+ return execViaDaemon(code, session, state.daemonSocketPath, logger, options);
212
+ }
250
213
  function parseJsonArg(label, raw) {
251
214
  try {
252
215
  return JSON.parse(raw);
@@ -1,7 +1,5 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { z } from "zod";
3
- import { connect, disconnectBrowser } from "../core/browser.js";
4
- import { getSessionSnapshotRunDir } from "../core/context.js";
5
3
  import { condenseDom } from "../../shared/condense-dom/condense-dom.js";
6
4
  import { readSessionState } from "../core/session.js";
7
5
  import { SimpleCLI } from "../framework/simple-cli.js";
@@ -9,10 +7,8 @@ import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
9
7
  import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
10
8
  import { readSnapshotModel } from "../core/config.js";
11
9
  import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
10
+ import { DaemonClient } from "../core/daemon/index.js";
12
11
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
13
- function generateSnapshotRunId() {
14
- return `snapshot-${Date.now()}`;
15
- }
16
12
  function isZeroViewport(value) {
17
13
  return typeof value === "number" && value <= 0;
18
14
  }
@@ -67,133 +63,42 @@ async function forceSnapshotViewport(page, viewport, logger, session, pageId, re
67
63
  viewport
68
64
  });
69
65
  }
70
- async function captureScreenshot(session, logger, pageId) {
71
- logger.info("screenshot-start", { session, pageId });
72
- const snapshotRunId = generateSnapshotRunId();
73
- const snapshotRunDir = getSessionSnapshotRunDir(session, snapshotRunId);
74
- mkdirSync(snapshotRunDir, { recursive: true });
75
- const { browser, page } = await connect(session, logger, 1e4, {
76
- pageId,
77
- requireSinglePage: true
78
- });
79
- try {
80
- let title = null;
81
- try {
82
- title = await page.title();
83
- } catch (error) {
84
- logger.warn("screenshot-title-read-failed", {
85
- session,
86
- pageId,
87
- error
88
- });
89
- }
90
- let pageUrl = null;
91
- try {
92
- pageUrl = page.url();
93
- } catch (error) {
94
- logger.warn("screenshot-url-read-failed", {
95
- session,
96
- pageId,
97
- error
98
- });
99
- }
100
- const pngPath = `${snapshotRunDir}/page.png`;
101
- const htmlPath = `${snapshotRunDir}/page.html`;
102
- const condensedHtmlPath = `${snapshotRunDir}/page.condensed.html`;
103
- const RENDER_SETTLE_TIMEOUT_MS = 1e4;
104
- await Promise.race([
105
- page.waitForLoadState("networkidle").catch(() => {
106
- }),
107
- new Promise((resolve) => setTimeout(resolve, RENDER_SETTLE_TIMEOUT_MS))
108
- ]);
109
- const restoreViewport = resolveSnapshotViewport(session, logger);
110
- const viewportMetrics = await readSnapshotViewportMetrics(page);
111
- logger.info("screenshot-viewport-metrics", {
112
- session,
113
- pageId,
114
- restoreViewport,
115
- ...viewportMetrics
116
- });
117
- await forceSnapshotViewport(
118
- page,
119
- restoreViewport,
120
- logger,
121
- session,
122
- pageId,
123
- shouldForceSnapshotViewport(viewportMetrics) ? "preflight-invalid-viewport" : "preflight-normalize-viewport"
124
- );
125
- try {
126
- await page.screenshot({ path: pngPath });
127
- } catch (error) {
128
- if (!isZeroWidthScreenshotError(error)) {
129
- throw error;
130
- }
131
- await forceSnapshotViewport(
132
- page,
133
- restoreViewport,
134
- logger,
135
- session,
136
- pageId,
137
- "retry-after-zero-width-screenshot-error"
138
- );
139
- await page.screenshot({ path: pngPath });
140
- }
141
- const htmlContent = await page.content();
142
- const fs = await import("node:fs/promises");
143
- await fs.writeFile(htmlPath, htmlContent);
144
- const condenseResult = condenseDom(htmlContent);
145
- await fs.writeFile(condensedHtmlPath, condenseResult.html);
146
- logger.info("screenshot-success", {
147
- session,
148
- pageUrl,
149
- title,
150
- pngPath,
151
- htmlPath,
152
- condensedHtmlPath,
153
- snapshotRunId,
154
- domCondenseStats: {
155
- originalLength: condenseResult.originalLength,
156
- condensedLength: condenseResult.condensedLength,
157
- reductions: condenseResult.reductions
158
- }
159
- });
160
- return { pngPath, htmlPath, condensedHtmlPath, baseName: snapshotRunId };
161
- } catch (err) {
162
- let pageAlive = false;
163
- let browserConnected = false;
164
- try {
165
- browserConnected = browser.isConnected();
166
- pageAlive = !page.isClosed();
167
- } catch {
66
+ async function captureSnapshot(session, logger, daemonSocketPath, pageId) {
67
+ logger.info("snapshot-via-daemon", { session, pageId });
68
+ const client = new DaemonClient(daemonSocketPath);
69
+ const { pngPath, htmlPath, snapshotRunId, pageUrl, title } = await client.snapshot({ pageId });
70
+ const htmlContent = readFileSync(htmlPath, "utf8");
71
+ const condenseResult = condenseDom(htmlContent);
72
+ const condensedHtmlPath = htmlPath.replace(/\.html$/, ".condensed.html");
73
+ writeFileSync(condensedHtmlPath, condenseResult.html);
74
+ logger.info("snapshot-daemon-success", {
75
+ session,
76
+ pageUrl,
77
+ title,
78
+ pngPath,
79
+ htmlPath,
80
+ condensedHtmlPath,
81
+ snapshotRunId,
82
+ domCondenseStats: {
83
+ originalLength: condenseResult.originalLength,
84
+ condensedLength: condenseResult.condensedLength,
85
+ reductions: condenseResult.reductions
168
86
  }
169
- logger.error("screenshot-error", {
170
- error: err,
171
- session,
172
- pageAlive,
173
- browserConnected,
174
- pageUrl: (() => {
175
- try {
176
- return page.url();
177
- } catch {
178
- return null;
179
- }
180
- })()
181
- });
182
- throw err;
183
- } finally {
184
- disconnectBrowser(browser, logger, session);
185
- }
87
+ });
88
+ return { pngPath, htmlPath, condensedHtmlPath, baseName: snapshotRunId };
186
89
  }
187
90
  async function runSnapshot(session, logger, pageId, objective, context) {
188
91
  const normalizedObjective = objective.trim();
189
92
  const normalizedContext = context.trim();
190
93
  const snapshotModel = readSnapshotModel();
191
94
  resolveSnapshotApiModelOrThrow(snapshotModel);
192
- const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
193
- session,
194
- logger,
195
- pageId
196
- );
95
+ const state = readSessionState(session, logger);
96
+ if (!state?.daemonSocketPath) {
97
+ throw new Error(
98
+ `Session "${session}" has no daemon socket. The browser daemon may have crashed. Close and reopen the session: libretto close --session ${session}`
99
+ );
100
+ }
101
+ const { pngPath, htmlPath, condensedHtmlPath } = await captureSnapshot(session, logger, state.daemonSocketPath, pageId);
197
102
  console.log("Screenshot saved:");
198
103
  console.log(` PNG: ${pngPath}`);
199
104
  console.log(` HTML: ${htmlPath}`);
@@ -229,6 +134,13 @@ const snapshotCommand = SimpleCLI.command({
229
134
  );
230
135
  });
231
136
  export {
137
+ FALLBACK_SNAPSHOT_VIEWPORT,
138
+ forceSnapshotViewport,
139
+ isZeroViewport,
140
+ isZeroWidthScreenshotError,
141
+ readSnapshotViewportMetrics,
142
+ resolveSnapshotViewport,
143
+ shouldForceSnapshotViewport,
232
144
  snapshotCommand,
233
145
  snapshotInput
234
146
  };
@@ -4,7 +4,6 @@ import {
4
4
  hasProviderCredentials,
5
5
  parseModel
6
6
  } from "./resolve-model.js";
7
- import { loadEnv } from "../../shared/env/load-env.js";
8
7
  import { parseDotEnvAssignment } from "../../shared/env/load-env.js";
9
8
  const DEFAULT_SNAPSHOT_MODELS = {
10
9
  openai: "openai/gpt-5.4",
@@ -104,7 +103,6 @@ function inferAutoSnapshotModel() {
104
103
  return null;
105
104
  }
106
105
  function resolveSnapshotApiModel(snapshotModel = readSnapshotModel()) {
107
- loadEnv();
108
106
  if (snapshotModel) {
109
107
  const { provider } = parseModel(snapshotModel);
110
108
  return {
@@ -141,7 +139,6 @@ function readSnapshotModelSafely(configPath) {
141
139
  }
142
140
  }
143
141
  function resolveAiSetupStatus(configPath = LIBRETTO_CONFIG_PATH) {
144
- loadEnv();
145
142
  const result = readSnapshotModelSafely(configPath);
146
143
  if (!result.ok) {
147
144
  return { kind: "invalid-config", message: result.message };