libretto 0.6.12 → 0.6.14

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 (82) hide show
  1. package/README.md +3 -8
  2. package/README.template.md +3 -8
  3. package/dist/cli/cli.js +0 -23
  4. package/dist/cli/commands/auth.js +24 -33
  5. package/dist/cli/commands/billing.js +3 -5
  6. package/dist/cli/commands/browser.js +4 -13
  7. package/dist/cli/commands/deploy.js +54 -45
  8. package/dist/cli/commands/execution.js +6 -3
  9. package/dist/cli/commands/experiments.js +1 -1
  10. package/dist/cli/commands/setup.js +2 -295
  11. package/dist/cli/commands/shared.js +1 -1
  12. package/dist/cli/commands/snapshot.js +10 -100
  13. package/dist/cli/commands/status.js +2 -42
  14. package/dist/cli/core/auth-fetch.js +11 -6
  15. package/dist/cli/core/browser.js +13 -8
  16. package/dist/cli/core/config.js +3 -6
  17. package/dist/cli/core/daemon/daemon.js +88 -74
  18. package/dist/cli/core/daemon/exec-repl.js +133 -0
  19. package/dist/cli/core/daemon/exec.js +6 -21
  20. package/dist/cli/core/daemon/ipc.js +47 -4
  21. package/dist/cli/core/daemon/ipc.spec.js +21 -0
  22. package/dist/cli/core/daemon/snapshot.js +2 -29
  23. package/dist/cli/core/exec-compiler.js +8 -3
  24. package/dist/cli/core/experiments.js +1 -28
  25. package/dist/cli/core/providers/index.js +13 -4
  26. package/dist/cli/core/providers/libretto-cloud.js +178 -26
  27. package/dist/cli/index.js +0 -2
  28. package/dist/cli/router.js +9 -6
  29. package/dist/shared/instrumentation/instrument.js +4 -4
  30. package/dist/shared/ipc/socket-transport.d.ts +2 -1
  31. package/dist/shared/ipc/socket-transport.js +16 -5
  32. package/dist/shared/ipc/socket-transport.spec.js +5 -0
  33. package/docs/releasing.md +8 -6
  34. package/package.json +3 -2
  35. package/skills/libretto/SKILL.md +49 -47
  36. package/skills/libretto/references/code-generation-rules.md +6 -0
  37. package/skills/libretto/references/configuration-file-reference.md +14 -12
  38. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  39. package/skills/libretto/references/site-security-review.md +6 -6
  40. package/skills/libretto-readonly/SKILL.md +2 -9
  41. package/src/cli/cli.ts +0 -24
  42. package/src/cli/commands/auth.ts +24 -33
  43. package/src/cli/commands/billing.ts +3 -5
  44. package/src/cli/commands/browser.ts +6 -16
  45. package/src/cli/commands/deploy.ts +55 -49
  46. package/src/cli/commands/execution.ts +6 -3
  47. package/src/cli/commands/experiments.ts +1 -1
  48. package/src/cli/commands/setup.ts +2 -381
  49. package/src/cli/commands/shared.ts +1 -1
  50. package/src/cli/commands/snapshot.ts +9 -137
  51. package/src/cli/commands/status.ts +2 -50
  52. package/src/cli/core/auth-fetch.ts +9 -4
  53. package/src/cli/core/browser.ts +15 -8
  54. package/src/cli/core/config.ts +3 -6
  55. package/src/cli/core/daemon/daemon.ts +106 -76
  56. package/src/cli/core/daemon/exec-repl.ts +189 -0
  57. package/src/cli/core/daemon/exec.ts +8 -43
  58. package/src/cli/core/daemon/ipc.spec.ts +27 -0
  59. package/src/cli/core/daemon/ipc.ts +81 -23
  60. package/src/cli/core/daemon/snapshot.ts +1 -43
  61. package/src/cli/core/exec-compiler.ts +8 -3
  62. package/src/cli/core/experiments.ts +9 -38
  63. package/src/cli/core/providers/index.ts +17 -4
  64. package/src/cli/core/providers/libretto-cloud.ts +224 -36
  65. package/src/cli/core/resolve-model.ts +5 -0
  66. package/src/cli/core/workflow-runtime.ts +1 -0
  67. package/src/cli/index.ts +0 -1
  68. package/src/cli/router.ts +9 -6
  69. package/src/shared/instrumentation/instrument.ts +4 -4
  70. package/src/shared/ipc/socket-transport.spec.ts +6 -0
  71. package/src/shared/ipc/socket-transport.ts +20 -5
  72. package/dist/cli/commands/ai.js +0 -110
  73. package/dist/cli/core/ai-model.js +0 -195
  74. package/dist/cli/core/api-snapshot-analyzer.js +0 -86
  75. package/dist/cli/core/snapshot-analyzer.js +0 -667
  76. package/dist/cli/framework/simple-cli.js +0 -880
  77. package/scripts/summarize-evals.mjs +0 -135
  78. package/src/cli/commands/ai.ts +0 -144
  79. package/src/cli/core/ai-model.ts +0 -301
  80. package/src/cli/core/api-snapshot-analyzer.ts +0 -110
  81. package/src/cli/core/snapshot-analyzer.ts +0 -856
  82. package/src/cli/framework/simple-cli.ts +0 -1459
@@ -105,6 +105,7 @@ async function getProviderModel(
105
105
  if (!apiKey) {
106
106
  throw new Error(missingProviderCredentialsMessage(provider));
107
107
  }
108
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
108
109
  const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
109
110
  const google = createGoogleGenerativeAI({ apiKey });
110
111
  return google(modelId);
@@ -114,6 +115,7 @@ async function getProviderModel(
114
115
  if (!project) {
115
116
  throw new Error(missingProviderCredentialsMessage(provider));
116
117
  }
118
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
117
119
  const { createVertex } = await import("@ai-sdk/google-vertex");
118
120
  const vertex = createVertex({
119
121
  project,
@@ -126,6 +128,7 @@ async function getProviderModel(
126
128
  if (!apiKey) {
127
129
  throw new Error(missingProviderCredentialsMessage(provider));
128
130
  }
131
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
129
132
  const { createAnthropic } = await import("@ai-sdk/anthropic");
130
133
  const anthropic = createAnthropic({ apiKey });
131
134
  return anthropic(modelId);
@@ -135,6 +138,7 @@ async function getProviderModel(
135
138
  if (!apiKey) {
136
139
  throw new Error(missingProviderCredentialsMessage(provider));
137
140
  }
141
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
138
142
  const { createOpenAI } = await import("@ai-sdk/openai");
139
143
  const openai = createOpenAI({ apiKey });
140
144
  return openai(modelId);
@@ -144,6 +148,7 @@ async function getProviderModel(
144
148
  if (!apiKey) {
145
149
  throw new Error(missingProviderCredentialsMessage(provider));
146
150
  }
151
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
147
152
  const { createOpenAI } = await import("@ai-sdk/openai");
148
153
  const openrouter = createOpenAI({
149
154
  apiKey,
@@ -37,6 +37,7 @@ export async function loadDefaultWorkflow(
37
37
  ): Promise<ExportedLibrettoWorkflow> {
38
38
  let loadedModule: Record<string, unknown>;
39
39
  try {
40
+ // @lintc-ignore Human-approved: user workflow files must be loaded dynamically from the CLI argument.
40
41
  loadedModule = (await import(pathToFileURL(absolutePath).href)) as Record<
41
42
  string,
42
43
  unknown
package/src/cli/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { runLibrettoCLI } from "./cli.js";
3
3
 
4
- export { runClose } from "./commands/browser.js";
5
4
  export { runLibrettoCLI };
6
5
 
7
6
  void runLibrettoCLI();
package/src/cli/router.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { aiCommands } from "./commands/ai.js";
2
1
  import { authCommands } from "./commands/auth.js";
3
2
  import { billingCommands } from "./commands/billing.js";
4
3
  import { browserCommands } from "./commands/browser.js";
@@ -9,16 +8,20 @@ import { setupCommand } from "./commands/setup.js";
9
8
  import { statusCommand } from "./commands/status.js";
10
9
  import { snapshotCommand } from "./commands/snapshot.js";
11
10
  import { librettoCommand } from "../shared/package-manager.js";
12
- import { SimpleCLI } from "./framework/simple-cli.js";
11
+ import { SimpleCLI } from "affordance";
13
12
 
14
13
  export const cliRoutes = {
15
14
  ...browserCommands,
16
- deploy: deployCommand,
15
+ cloud: SimpleCLI.group({
16
+ description: "Libretto Cloud commands",
17
+ routes: {
18
+ deploy: deployCommand,
19
+ auth: authCommands,
20
+ billing: billingCommands,
21
+ },
22
+ }),
17
23
  experiments: experimentsCommand,
18
24
  ...executionCommands,
19
- ai: aiCommands,
20
- auth: authCommands,
21
- billing: billingCommands,
22
25
  setup: setupCommand,
23
26
  status: statusCommand,
24
27
  snapshot: snapshotCommand,
@@ -131,12 +131,12 @@ function wrapLocatorActions(
131
131
  try {
132
132
  const result = await orig(...args);
133
133
  if (opts.visualize) {
134
- enqueue(page, () => visualizeAfterAction(page));
134
+ void enqueue(page, () => visualizeAfterAction(page));
135
135
  }
136
136
  return result;
137
137
  } catch (err: any) {
138
138
  if (opts.visualize) {
139
- enqueue(page, () => visualizeAfterAction(page));
139
+ void enqueue(page, () => visualizeAfterAction(page));
140
140
  }
141
141
  // Enrich timeout errors for pointer actions
142
142
  if (POINTER_ACTIONS.has(method) && isTimeoutError(err)) {
@@ -323,12 +323,12 @@ export async function installInstrumentation(
323
323
  try {
324
324
  const result = await orig(...args);
325
325
  if (visualize) {
326
- enqueue(page, () => visualizeAfterAction(page));
326
+ void enqueue(page, () => visualizeAfterAction(page));
327
327
  }
328
328
  return result;
329
329
  } catch (err: any) {
330
330
  if (visualize) {
331
- enqueue(page, () => visualizeAfterAction(page));
331
+ void enqueue(page, () => visualizeAfterAction(page));
332
332
  }
333
333
  if (
334
334
  POINTER_ACTIONS.has(method) &&
@@ -6,6 +6,7 @@ import { expect, test as base } from "vitest";
6
6
  import { createIpcPeer, type IpcPeer } from "./ipc.js";
7
7
  import {
8
8
  connectToIpcSocket,
9
+ isWindowsNamedPipePath,
9
10
  listenForIpcConnections,
10
11
  } from "./socket-transport.js";
11
12
 
@@ -64,6 +65,11 @@ test("sends concurrent calls over one socket", async ({ socketPath }) => {
64
65
  await expect(stat(socketPath)).rejects.toThrow();
65
66
  });
66
67
 
68
+ test("recognizes Windows named pipe paths", () => {
69
+ expect(isWindowsNamedPipePath("\\\\.\\pipe\\libretto-abc123")).toBe(true);
70
+ expect(isWindowsNamedPipePath("/tmp/libretto-501-abc123.sock")).toBe(false);
71
+ });
72
+
67
73
  test("rejects pending calls when the socket closes", async ({ socketPath }) => {
68
74
  const serverPeers: Array<IpcPeer<ClientApi>> = [];
69
75
  const server = await listenForIpcConnections(socketPath, (transport) => {
@@ -1,4 +1,4 @@
1
- import { rm } from "node:fs/promises";
1
+ import { mkdir, rm } from "node:fs/promises";
2
2
  import {
3
3
  createServer,
4
4
  createConnection,
@@ -6,7 +6,6 @@ import {
6
6
  type Socket,
7
7
  } from "node:net";
8
8
  import { dirname } from "node:path";
9
- import { mkdir } from "node:fs/promises";
10
9
  import type { IpcProtocolMessage, IpcTransport } from "./ipc.js";
11
10
 
12
11
  function createJsonSocketTransport(
@@ -109,13 +108,12 @@ export async function listenOnIpcSocket(
109
108
  server: Server,
110
109
  socketPath: string,
111
110
  ): Promise<void> {
112
- await mkdir(dirname(socketPath), { recursive: true });
113
- await rm(socketPath, { force: true });
111
+ await prepareIpcSocketPath(socketPath);
114
112
 
115
113
  const originalClose = server.close.bind(server);
116
114
  server.close = ((callback?: (error?: Error) => void) => {
117
115
  return originalClose((error?: Error) => {
118
- void rm(socketPath, { force: true }).finally(() => callback?.(error));
116
+ void removeStaleSocketFile(socketPath).finally(() => callback?.(error));
119
117
  });
120
118
  }) as Server["close"];
121
119
 
@@ -135,6 +133,23 @@ export async function listenOnIpcSocket(
135
133
  });
136
134
  }
137
135
 
136
+ export function isWindowsNamedPipePath(socketPath: string): boolean {
137
+ return socketPath.startsWith("\\\\.\\pipe\\");
138
+ }
139
+
140
+ async function prepareIpcSocketPath(socketPath: string): Promise<void> {
141
+ if (isWindowsNamedPipePath(socketPath)) return;
142
+
143
+ await mkdir(dirname(socketPath), { recursive: true });
144
+ await removeStaleSocketFile(socketPath);
145
+ }
146
+
147
+ async function removeStaleSocketFile(socketPath: string): Promise<void> {
148
+ if (isWindowsNamedPipePath(socketPath)) return;
149
+
150
+ await rm(socketPath, { force: true });
151
+ }
152
+
138
153
  async function connectSocket(socketPath: string): Promise<Socket> {
139
154
  const socket = createConnection(socketPath);
140
155
 
@@ -1,110 +0,0 @@
1
- import { z } from "zod";
2
- import {
3
- readSnapshotModel,
4
- writeSnapshotModel,
5
- clearSnapshotModel
6
- } from "../core/config.js";
7
- import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
8
- import { DEFAULT_SNAPSHOT_MODELS } from "../core/ai-model.js";
9
- import { librettoCommand } from "../../shared/package-manager.js";
10
- import { SimpleCLI } from "../framework/simple-cli.js";
11
- const PROVIDER_ALIASES = {
12
- claude: DEFAULT_SNAPSHOT_MODELS.anthropic,
13
- gemini: DEFAULT_SNAPSHOT_MODELS.google,
14
- google: DEFAULT_SNAPSHOT_MODELS.google
15
- };
16
- const CONFIGURE_PROVIDERS = [
17
- "openai",
18
- "anthropic",
19
- "gemini",
20
- "vertex"
21
- ];
22
- function formatConfigureProviders(separator = " | ") {
23
- return CONFIGURE_PROVIDERS.join(separator);
24
- }
25
- function printSnapshotModelConfig(model, configPath) {
26
- console.log(`Snapshot model: ${model}`);
27
- console.log(`Config file: ${configPath}`);
28
- }
29
- function resolveModelFromInput(input) {
30
- const trimmed = input.trim();
31
- if (!trimmed) return null;
32
- if (trimmed.includes("/")) return trimmed;
33
- const normalized = trimmed.toLowerCase();
34
- return DEFAULT_SNAPSHOT_MODELS[normalized] ?? PROVIDER_ALIASES[normalized] ?? null;
35
- }
36
- function runAiConfigure(input, options = {}) {
37
- const configureCommandName = options.configureCommandName ?? librettoCommand("ai configure");
38
- const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
39
- const presetArg = input.preset?.trim();
40
- if (!presetArg && !input.clear) {
41
- const model2 = readSnapshotModel(configPath);
42
- if (!model2) {
43
- console.log(
44
- `No snapshot model set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
45
- );
46
- console.log(
47
- "Provider credentials still come from your shell or .env file."
48
- );
49
- return;
50
- }
51
- printSnapshotModelConfig(model2, configPath);
52
- return;
53
- }
54
- if (input.clear) {
55
- const removed = clearSnapshotModel(configPath);
56
- if (removed) {
57
- console.log(`Cleared snapshot model config: ${configPath}`);
58
- } else {
59
- console.log("No snapshot model was set.");
60
- }
61
- return;
62
- }
63
- const model = resolveModelFromInput(presetArg);
64
- if (!model) {
65
- console.log(
66
- `Usage: ${configureCommandName} <${CONFIGURE_PROVIDERS.join("|")}|provider/model-id>
67
- ${configureCommandName}
68
- ${configureCommandName} --clear`
69
- );
70
- throw new Error(
71
- `Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`
72
- );
73
- }
74
- writeSnapshotModel(model, configPath);
75
- console.log("Snapshot model saved.");
76
- printSnapshotModelConfig(model, configPath);
77
- }
78
- const aiConfigureInput = SimpleCLI.input({
79
- positionals: [
80
- SimpleCLI.positional("preset", z.string().optional(), {
81
- help: "Provider shorthand or provider/model-id"
82
- })
83
- ],
84
- named: {
85
- clear: SimpleCLI.flag({ help: "Clear existing AI config" })
86
- }
87
- });
88
- const aiCommands = SimpleCLI.group({
89
- description: "AI commands",
90
- routes: {
91
- configure: SimpleCLI.command({
92
- description: "Configure AI runtime"
93
- }).input(aiConfigureInput).handle(async ({ input }) => {
94
- runAiConfigure(
95
- {
96
- clear: input.clear,
97
- preset: input.preset
98
- },
99
- {
100
- configureCommandName: librettoCommand("ai configure")
101
- }
102
- );
103
- })
104
- }
105
- });
106
- export {
107
- aiCommands,
108
- aiConfigureInput,
109
- runAiConfigure
110
- };
@@ -1,195 +0,0 @@
1
- import { readSnapshotModel } from "./config.js";
2
- import { LIBRETTO_CONFIG_PATH } from "./context.js";
3
- import { librettoCommand } from "../../shared/package-manager.js";
4
- import {
5
- hasProviderCredentials,
6
- parseModel
7
- } from "./resolve-model.js";
8
- import { parseDotEnvAssignment } from "../../shared/env/load-env.js";
9
- const DEFAULT_SNAPSHOT_MODELS = {
10
- openai: "openai/gpt-5.4",
11
- anthropic: "anthropic/claude-sonnet-4-6",
12
- google: "google/gemini-3-flash-preview",
13
- vertex: "vertex/gemini-2.5-flash",
14
- openrouter: "openrouter/free"
15
- };
16
- function detectProviderEnvVar(provider, env = process.env) {
17
- switch (provider) {
18
- case "openai":
19
- return env.OPENAI_API_KEY?.trim() ? "OPENAI_API_KEY" : null;
20
- case "anthropic":
21
- return env.ANTHROPIC_API_KEY?.trim() ? "ANTHROPIC_API_KEY" : null;
22
- case "google":
23
- if (env.GEMINI_API_KEY?.trim()) return "GEMINI_API_KEY";
24
- if (env.GOOGLE_GENERATIVE_AI_API_KEY?.trim())
25
- return "GOOGLE_GENERATIVE_AI_API_KEY";
26
- return null;
27
- case "vertex":
28
- if (env.GOOGLE_CLOUD_PROJECT?.trim()) return "GOOGLE_CLOUD_PROJECT";
29
- if (env.GCLOUD_PROJECT?.trim()) return "GCLOUD_PROJECT";
30
- return null;
31
- case "openrouter":
32
- return env.OPENROUTER_API_KEY?.trim() ? "OPENROUTER_API_KEY" : null;
33
- }
34
- }
35
- class SnapshotApiUnavailableError extends Error {
36
- constructor(message) {
37
- super(message);
38
- this.name = "SnapshotApiUnavailableError";
39
- }
40
- }
41
- function providerSetupSentence(provider) {
42
- switch (provider) {
43
- case "openai":
44
- return "Add OPENAI_API_KEY to .env or as a shell environment variable.";
45
- case "anthropic":
46
- return "Add ANTHROPIC_API_KEY to .env or as a shell environment variable.";
47
- case "google":
48
- return "Add GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY to .env or as a shell environment variable.";
49
- case "vertex":
50
- return "Add GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT to .env or as a shell environment variable, and make sure application default credentials are configured.";
51
- case "openrouter":
52
- return "Add OPENROUTER_API_KEY to .env or as a shell environment variable.";
53
- }
54
- }
55
- function defaultModelCommandLine() {
56
- return librettoCommand(
57
- "ai configure openai | anthropic | gemini | vertex | openrouter"
58
- );
59
- }
60
- function providerMissingCredentialSummary(provider) {
61
- switch (provider) {
62
- case "openai":
63
- return "OPENAI_API_KEY is missing";
64
- case "anthropic":
65
- return "ANTHROPIC_API_KEY is missing";
66
- case "google":
67
- return "GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are missing";
68
- case "vertex":
69
- return "GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT are missing";
70
- case "openrouter":
71
- return "OPENROUTER_API_KEY is missing";
72
- }
73
- }
74
- function noSnapshotApiConfiguredMessage() {
75
- return [
76
- "Failed to analyze snapshot because no snapshot analyzer is configured.",
77
- `Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, GOOGLE_CLOUD_PROJECT, or OPENROUTER_API_KEY to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
78
- `For more info, run \`${librettoCommand("setup")}\`.`
79
- ].join(" ");
80
- }
81
- function missingProviderSnapshotMessage(selection) {
82
- const configuredSource = selection.source === "config" ? ` in ${LIBRETTO_CONFIG_PATH}` : " from process env or .env";
83
- return [
84
- `Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
85
- providerSetupSentence(selection.provider),
86
- `For more info, run \`${librettoCommand("setup")}\`.`
87
- ].join(" ");
88
- }
89
- function inferAutoSnapshotModel() {
90
- const providersInPriorityOrder = [
91
- "openai",
92
- "anthropic",
93
- "google",
94
- "vertex",
95
- "openrouter"
96
- ];
97
- for (const provider of providersInPriorityOrder) {
98
- const envVar = detectProviderEnvVar(provider);
99
- if (!envVar) continue;
100
- return {
101
- model: DEFAULT_SNAPSHOT_MODELS[provider],
102
- provider,
103
- source: `env:${envVar}`
104
- };
105
- }
106
- return null;
107
- }
108
- function resolveSnapshotApiModel(snapshotModel = readSnapshotModel()) {
109
- if (snapshotModel) {
110
- const { provider } = parseModel(snapshotModel);
111
- return {
112
- model: snapshotModel,
113
- provider,
114
- source: "config"
115
- };
116
- }
117
- return inferAutoSnapshotModel();
118
- }
119
- function resolveSnapshotApiModelOrThrow(snapshotModel = readSnapshotModel()) {
120
- const selection = resolveSnapshotApiModel(snapshotModel);
121
- if (!selection) {
122
- throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
123
- }
124
- if (!hasProviderCredentials(selection.provider)) {
125
- throw new SnapshotApiUnavailableError(
126
- missingProviderSnapshotMessage(selection)
127
- );
128
- }
129
- return selection;
130
- }
131
- function isSnapshotApiUnavailableError(error) {
132
- return error instanceof SnapshotApiUnavailableError;
133
- }
134
- function readSnapshotModelSafely(configPath) {
135
- try {
136
- return { ok: true, model: readSnapshotModel(configPath) };
137
- } catch (err) {
138
- return {
139
- ok: false,
140
- message: err instanceof Error ? err.message : String(err)
141
- };
142
- }
143
- }
144
- function resolveAiSetupStatus(configPath = LIBRETTO_CONFIG_PATH) {
145
- const result = readSnapshotModelSafely(configPath);
146
- if (!result.ok) {
147
- return { kind: "invalid-config", message: result.message };
148
- }
149
- if (result.model) {
150
- let selection;
151
- try {
152
- selection = resolveSnapshotApiModel(result.model);
153
- } catch (err) {
154
- return {
155
- kind: "invalid-config",
156
- message: err instanceof Error ? err.message : String(err)
157
- };
158
- }
159
- if (!selection) {
160
- return { kind: "unconfigured" };
161
- }
162
- if (hasProviderCredentials(selection.provider)) {
163
- return {
164
- kind: "ready",
165
- model: selection.model,
166
- provider: selection.provider,
167
- source: selection.source
168
- };
169
- }
170
- return {
171
- kind: "configured-missing-credentials",
172
- model: selection.model,
173
- provider: selection.provider
174
- };
175
- }
176
- const envSelection = resolveSnapshotApiModel(null);
177
- if (envSelection && hasProviderCredentials(envSelection.provider)) {
178
- return {
179
- kind: "ready",
180
- model: envSelection.model,
181
- provider: envSelection.provider,
182
- source: envSelection.source
183
- };
184
- }
185
- return { kind: "unconfigured" };
186
- }
187
- export {
188
- DEFAULT_SNAPSHOT_MODELS,
189
- SnapshotApiUnavailableError,
190
- isSnapshotApiUnavailableError,
191
- parseDotEnvAssignment,
192
- resolveAiSetupStatus,
193
- resolveSnapshotApiModel,
194
- resolveSnapshotApiModelOrThrow
195
- };
@@ -1,86 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { generateObject } from "ai";
3
- import { resolveModel } from "./resolve-model.js";
4
- import {
5
- InterpretResultSchema,
6
- buildInlinePromptSelection,
7
- getMimeType,
8
- readFileAsBase64
9
- } from "./snapshot-analyzer.js";
10
- import { readSnapshotModel } from "./config.js";
11
- import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
12
- async function runApiInterpret(args, logger, snapshotModel = readSnapshotModel()) {
13
- const selection = resolveSnapshotApiModelOrThrow(snapshotModel);
14
- logger.info("api-interpret-start", {
15
- objective: args.objective,
16
- pngPath: args.pngPath,
17
- htmlPath: args.htmlPath,
18
- condensedHtmlPath: args.condensedHtmlPath,
19
- model: selection.model,
20
- modelSource: selection.source
21
- });
22
- const fullHtmlContent = readFileSync(args.htmlPath, "utf-8");
23
- const condensedHtmlContent = readFileSync(args.condensedHtmlPath, "utf-8");
24
- const promptSelection = buildInlinePromptSelection(
25
- args,
26
- fullHtmlContent,
27
- condensedHtmlContent,
28
- selection.model
29
- );
30
- logger.info("api-interpret-dom-selection", {
31
- configuredModel: promptSelection.stats.configuredModel,
32
- fullDomEstimatedTokens: promptSelection.stats.fullDomEstimatedTokens,
33
- condensedDomEstimatedTokens: promptSelection.stats.condensedDomEstimatedTokens,
34
- contextWindowTokens: promptSelection.budget.contextWindowTokens,
35
- promptBudgetTokens: promptSelection.budget.promptBudgetTokens,
36
- selectedDom: promptSelection.domSource,
37
- selectedHtmlEstimatedTokens: promptSelection.htmlEstimatedTokens,
38
- selectedPromptEstimatedTokens: promptSelection.promptEstimatedTokens,
39
- selectionReason: promptSelection.selectionReason,
40
- truncated: promptSelection.truncated
41
- });
42
- const imageBase64 = readFileAsBase64(args.pngPath);
43
- const imageMimeType = getMimeType(args.pngPath);
44
- const imageBytes = Buffer.from(imageBase64, "base64");
45
- const model = await resolveModel(selection.model);
46
- const { object: result } = await generateObject({
47
- model,
48
- schema: InterpretResultSchema,
49
- messages: [
50
- {
51
- role: "user",
52
- content: [
53
- { type: "text", text: promptSelection.prompt },
54
- {
55
- type: "image",
56
- image: imageBytes,
57
- mediaType: imageMimeType
58
- }
59
- ]
60
- }
61
- ],
62
- temperature: 0.1
63
- });
64
- const parsed = InterpretResultSchema.parse(result);
65
- logger.info("api-interpret-success", {
66
- selectorCount: parsed.selectors.length,
67
- answer: parsed.answer.slice(0, 200)
68
- });
69
- console.log("");
70
- console.log("Analysis:");
71
- console.log(parsed.answer);
72
- if (parsed.selectors.length > 0) {
73
- console.log("");
74
- console.log("Selectors:");
75
- parsed.selectors.forEach((selector, index) => {
76
- console.log(` ${index + 1}. ${selector.label}: ${selector.selector}`);
77
- });
78
- }
79
- if (parsed.notes?.trim()) {
80
- console.log("");
81
- console.log(`Notes: ${parsed.notes.trim()}`);
82
- }
83
- }
84
- export {
85
- runApiInterpret
86
- };