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
@@ -1,135 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { readdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { basename, join, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- function usage() {
8
- console.error(
9
- "Usage: node scripts/summarize-evals.mjs <score-dir> <summary-json-path>",
10
- );
11
- }
12
-
13
- function normalizeFailureRecord(failure) {
14
- return {
15
- criterion: String(failure?.criterion ?? "").trim(),
16
- reason: String(failure?.reason ?? "").trim(),
17
- };
18
- }
19
-
20
- function normalizeRecord(record) {
21
- const failures = Array.isArray(record?.failures)
22
- ? record.failures
23
- .map(normalizeFailureRecord)
24
- .filter(
25
- (failure) =>
26
- failure.criterion.length > 0 && failure.reason.length > 0,
27
- )
28
- : [];
29
-
30
- return {
31
- name: String(record?.name ?? "").trim(),
32
- passed: Number(record?.passed ?? 0),
33
- total: Number(record?.total ?? 0),
34
- percent: Number(record?.percent ?? 0),
35
- failures,
36
- };
37
- }
38
-
39
- export function loadScoreRecords(scoreDirArg) {
40
- const scoreDir = resolve(scoreDirArg);
41
- return readdirSync(scoreDir, { withFileTypes: true })
42
- .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
43
- .map((entry) =>
44
- JSON.parse(readFileSync(join(scoreDir, entry.name), "utf8")),
45
- )
46
- .map(normalizeRecord)
47
- .sort((a, b) => String(a.name).localeCompare(String(b.name)));
48
- }
49
-
50
- export function buildSummary(records) {
51
- const passed = records.reduce(
52
- (sum, record) => sum + Number(record.passed || 0),
53
- 0,
54
- );
55
- const total = records.reduce(
56
- (sum, record) => sum + Number(record.total || 0),
57
- 0,
58
- );
59
- const percent = total > 0 ? Number(((passed / total) * 100).toFixed(2)) : 0;
60
- const failingRecords = records.filter((record) => record.failures.length > 0);
61
-
62
- return {
63
- generatedAt: new Date().toISOString(),
64
- recordCount: records.length,
65
- passed,
66
- total,
67
- percent,
68
- failingRecordCount: failingRecords.length,
69
- records,
70
- };
71
- }
72
-
73
- export function buildMarkdown(summary, summaryPathArg) {
74
- const lines = [
75
- "# Eval Summary",
76
- "",
77
- `- Overall score: \`${summary.percent}%\``,
78
- `- Passed criteria: \`${summary.passed}/${summary.total}\``,
79
- `- Recorded score entries: \`${summary.recordCount}\``,
80
- `- Failed evals: \`${summary.failingRecordCount}\``,
81
- `- Summary file: \`${basename(summaryPathArg)}\``,
82
- ];
83
-
84
- if (summary.records.length > 0) {
85
- lines.push("", "## Breakdown", "");
86
- for (const record of summary.records) {
87
- const status = record.failures.length > 0 ? "fail" : "pass";
88
- lines.push(
89
- `- ${status} \`${record.name}\`: \`${record.percent}%\` (${record.passed}/${record.total})`,
90
- );
91
- }
92
- }
93
-
94
- if (summary.failingRecordCount > 0) {
95
- lines.push("", "## Failed Evals", "");
96
- for (const record of summary.records.filter(
97
- (candidate) => candidate.failures.length > 0,
98
- )) {
99
- lines.push(`### \`${record.name}\``);
100
- lines.push("");
101
- lines.push(
102
- `- Score: \`${record.percent}%\` (${record.passed}/${record.total})`,
103
- );
104
- for (const failure of record.failures) {
105
- lines.push(`- ${failure.criterion}: ${failure.reason}`);
106
- }
107
- lines.push("");
108
- }
109
- }
110
-
111
- return `${lines.join("\n").trimEnd()}\n`;
112
- }
113
-
114
- function main(argv) {
115
- const [, , scoreDirArg, summaryPathArg] = argv;
116
-
117
- if (!scoreDirArg || !summaryPathArg) {
118
- usage();
119
- process.exit(1);
120
- }
121
-
122
- const summaryPath = resolve(summaryPathArg);
123
- const records = loadScoreRecords(scoreDirArg);
124
- const summary = buildSummary(records);
125
-
126
- writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
127
- process.stdout.write(buildMarkdown(summary, summaryPath));
128
- }
129
-
130
- if (
131
- process.argv[1] &&
132
- resolve(process.argv[1]) === fileURLToPath(import.meta.url)
133
- ) {
134
- main(process.argv);
135
- }
@@ -1,144 +0,0 @@
1
- import { z } from "zod";
2
- import {
3
- CURRENT_CONFIG_VERSION,
4
- readSnapshotModel,
5
- writeSnapshotModel,
6
- clearSnapshotModel,
7
- } from "../core/config.js";
8
- import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
9
- import { DEFAULT_SNAPSHOT_MODELS } from "../core/ai-model.js";
10
- import { librettoCommand } from "../../shared/package-manager.js";
11
- import { SimpleCLI } from "../framework/simple-cli.js";
12
-
13
- const PROVIDER_ALIASES: Record<string, string> = {
14
- claude: DEFAULT_SNAPSHOT_MODELS.anthropic,
15
- gemini: DEFAULT_SNAPSHOT_MODELS.google,
16
- google: DEFAULT_SNAPSHOT_MODELS.google,
17
- };
18
-
19
- const CONFIGURE_PROVIDERS = [
20
- "openai",
21
- "anthropic",
22
- "gemini",
23
- "vertex",
24
- ] as const;
25
-
26
- function formatConfigureProviders(separator = " | "): string {
27
- return CONFIGURE_PROVIDERS.join(separator);
28
- }
29
-
30
- function printSnapshotModelConfig(model: string, configPath: string): void {
31
- console.log(`Snapshot model: ${model}`);
32
- console.log(`Config file: ${configPath}`);
33
- }
34
-
35
- /**
36
- * Resolve the model string from a `ai configure` argument.
37
- * Accepts a provider shorthand ("openai", "anthropic", "gemini", "vertex")
38
- * or a full provider/model-id string ("openai/gpt-4o", "anthropic/claude-sonnet-4-6").
39
- */
40
- function resolveModelFromInput(input: string): string | null {
41
- const trimmed = input.trim();
42
- if (!trimmed) return null;
43
-
44
- // Full model string (contains a slash)
45
- if (trimmed.includes("/")) return trimmed;
46
-
47
- // Provider shorthand
48
- const normalized = trimmed.toLowerCase();
49
- return (
50
- (DEFAULT_SNAPSHOT_MODELS as Record<string, string>)[normalized] ??
51
- PROVIDER_ALIASES[normalized] ??
52
- null
53
- );
54
- }
55
-
56
- export function runAiConfigure(
57
- input: {
58
- preset?: string;
59
- clear?: boolean;
60
- },
61
- options: {
62
- configureCommandName?: string;
63
- configPath?: string;
64
- } = {},
65
- ): void {
66
- const configureCommandName =
67
- options.configureCommandName ?? librettoCommand("ai configure");
68
- const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
69
-
70
- const presetArg = input.preset?.trim();
71
-
72
- if (!presetArg && !input.clear) {
73
- const model = readSnapshotModel(configPath);
74
- if (!model) {
75
- console.log(
76
- `No snapshot model set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
77
- );
78
- console.log(
79
- "Provider credentials still come from your shell or .env file.",
80
- );
81
- return;
82
- }
83
- printSnapshotModelConfig(model, configPath);
84
- return;
85
- }
86
-
87
- if (input.clear) {
88
- const removed = clearSnapshotModel(configPath);
89
- if (removed) {
90
- console.log(`Cleared snapshot model config: ${configPath}`);
91
- } else {
92
- console.log("No snapshot model was set.");
93
- }
94
- return;
95
- }
96
-
97
- const model = resolveModelFromInput(presetArg!);
98
- if (!model) {
99
- console.log(
100
- `Usage: ${configureCommandName} <${CONFIGURE_PROVIDERS.join("|")}|provider/model-id>\n` +
101
- ` ${configureCommandName}\n` +
102
- ` ${configureCommandName} --clear`,
103
- );
104
- throw new Error(
105
- `Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`,
106
- );
107
- }
108
-
109
- writeSnapshotModel(model, configPath);
110
- console.log("Snapshot model saved.");
111
- printSnapshotModelConfig(model, configPath);
112
- }
113
-
114
- export const aiConfigureInput = SimpleCLI.input({
115
- positionals: [
116
- SimpleCLI.positional("preset", z.string().optional(), {
117
- help: "Provider shorthand or provider/model-id",
118
- }),
119
- ],
120
- named: {
121
- clear: SimpleCLI.flag({ help: "Clear existing AI config" }),
122
- },
123
- });
124
-
125
- export const aiCommands = SimpleCLI.group({
126
- description: "AI commands",
127
- routes: {
128
- configure: SimpleCLI.command({
129
- description: "Configure AI runtime",
130
- })
131
- .input(aiConfigureInput)
132
- .handle(async ({ input }) => {
133
- runAiConfigure(
134
- {
135
- clear: input.clear,
136
- preset: input.preset,
137
- },
138
- {
139
- configureCommandName: librettoCommand("ai configure"),
140
- },
141
- );
142
- }),
143
- },
144
- });
@@ -1,301 +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
- type Provider,
8
- } from "./resolve-model.js";
9
-
10
- // Re-export so existing consumers (e.g. tests) don't break.
11
- export { parseDotEnvAssignment } from "../../shared/env/load-env.js";
12
-
13
- // ── Default models ──────────────────────────────────────────────────────────
14
-
15
- export const DEFAULT_SNAPSHOT_MODELS = {
16
- openai: "openai/gpt-5.4",
17
- anthropic: "anthropic/claude-sonnet-4-6",
18
- google: "google/gemini-3-flash-preview",
19
- vertex: "vertex/gemini-2.5-flash",
20
- openrouter: "openrouter/free",
21
- } as const satisfies Record<Provider, string>;
22
-
23
- // ── Source detection ────────────────────────────────────────────────────────
24
-
25
- /**
26
- * Detect which specific env var provides credentials for a provider.
27
- * Returns the env var name (e.g. "OPENAI_API_KEY", "GEMINI_API_KEY"),
28
- * or null if no credential is found.
29
- */
30
- function detectProviderEnvVar(
31
- provider: Provider,
32
- env: NodeJS.ProcessEnv = process.env,
33
- ): string | null {
34
- switch (provider) {
35
- case "openai":
36
- return env.OPENAI_API_KEY?.trim() ? "OPENAI_API_KEY" : null;
37
- case "anthropic":
38
- return env.ANTHROPIC_API_KEY?.trim() ? "ANTHROPIC_API_KEY" : null;
39
- case "google":
40
- if (env.GEMINI_API_KEY?.trim()) return "GEMINI_API_KEY";
41
- if (env.GOOGLE_GENERATIVE_AI_API_KEY?.trim())
42
- return "GOOGLE_GENERATIVE_AI_API_KEY";
43
- return null;
44
- case "vertex":
45
- if (env.GOOGLE_CLOUD_PROJECT?.trim()) return "GOOGLE_CLOUD_PROJECT";
46
- if (env.GCLOUD_PROJECT?.trim()) return "GCLOUD_PROJECT";
47
- return null;
48
- case "openrouter":
49
- return env.OPENROUTER_API_KEY?.trim() ? "OPENROUTER_API_KEY" : null;
50
- }
51
- }
52
-
53
- // ── Snapshot model resolution ───────────────────────────────────────────────
54
-
55
- export type SnapshotApiModelSelection = {
56
- model: string;
57
- provider: Provider;
58
- source: "config" | `env:${string}`;
59
- };
60
-
61
- export class SnapshotApiUnavailableError extends Error {
62
- constructor(message: string) {
63
- super(message);
64
- this.name = "SnapshotApiUnavailableError";
65
- }
66
- }
67
-
68
- function providerSetupSentence(provider: Provider): string {
69
- switch (provider) {
70
- case "openai":
71
- return "Add OPENAI_API_KEY to .env or as a shell environment variable.";
72
- case "anthropic":
73
- return "Add ANTHROPIC_API_KEY to .env or as a shell environment variable.";
74
- case "google":
75
- return "Add GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY to .env or as a shell environment variable.";
76
- case "vertex":
77
- return "Add GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT to .env or as a shell environment variable, and make sure application default credentials are configured.";
78
- case "openrouter":
79
- return "Add OPENROUTER_API_KEY to .env or as a shell environment variable.";
80
- }
81
- }
82
-
83
- function defaultModelCommandLine(): string {
84
- return librettoCommand(
85
- "ai configure openai | anthropic | gemini | vertex | openrouter",
86
- );
87
- }
88
-
89
- function providerMissingCredentialSummary(provider: Provider): string {
90
- switch (provider) {
91
- case "openai":
92
- return "OPENAI_API_KEY is missing";
93
- case "anthropic":
94
- return "ANTHROPIC_API_KEY is missing";
95
- case "google":
96
- return "GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are missing";
97
- case "vertex":
98
- return "GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT are missing";
99
- case "openrouter":
100
- return "OPENROUTER_API_KEY is missing";
101
- }
102
- }
103
-
104
- function noSnapshotApiConfiguredMessage(): string {
105
- return [
106
- "Failed to analyze snapshot because no snapshot analyzer is configured.",
107
- `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()}\`.`,
108
- `For more info, run \`${librettoCommand("setup")}\`.`,
109
- ].join(" ");
110
- }
111
-
112
- function missingProviderSnapshotMessage(
113
- selection: SnapshotApiModelSelection,
114
- ): string {
115
- const configuredSource =
116
- selection.source === "config"
117
- ? ` in ${LIBRETTO_CONFIG_PATH}`
118
- : " from process env or .env";
119
- return [
120
- `Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
121
- providerSetupSentence(selection.provider),
122
- `For more info, run \`${librettoCommand("setup")}\`.`,
123
- ].join(" ");
124
- }
125
-
126
- // ── Model resolution ────────────────────────────────────────────────────────
127
-
128
- function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
129
- const providersInPriorityOrder: Provider[] = [
130
- "openai",
131
- "anthropic",
132
- "google",
133
- "vertex",
134
- "openrouter",
135
- ];
136
-
137
- for (const provider of providersInPriorityOrder) {
138
- const envVar = detectProviderEnvVar(provider);
139
- if (!envVar) continue;
140
- return {
141
- model: DEFAULT_SNAPSHOT_MODELS[provider],
142
- provider,
143
- source: `env:${envVar}`,
144
- };
145
- }
146
-
147
- return null;
148
- }
149
-
150
- /**
151
- * Resolve which API model to use for snapshot analysis.
152
- *
153
- * Environment variables are loaded by the CLI entrypoint (`runLibrettoCLI` in
154
- * `cli.ts`) before this resolver runs. Keep dotenv loading centralized there so
155
- * model resolution and browser provider resolution share the same env setup.
156
- *
157
- * Priority:
158
- * 1. snapshotModel from .libretto/config.json (set via `ai configure`)
159
- * 2. Auto-detect from available API credentials in env
160
- */
161
- export function resolveSnapshotApiModel(
162
- snapshotModel: string | null = readSnapshotModel(),
163
- ): SnapshotApiModelSelection | null {
164
- if (snapshotModel) {
165
- const { provider } = parseModel(snapshotModel);
166
- return {
167
- model: snapshotModel,
168
- provider,
169
- source: "config",
170
- };
171
- }
172
-
173
- return inferAutoSnapshotModel();
174
- }
175
-
176
- export function resolveSnapshotApiModelOrThrow(
177
- snapshotModel: string | null = readSnapshotModel(),
178
- ): SnapshotApiModelSelection {
179
- const selection = resolveSnapshotApiModel(snapshotModel);
180
- if (!selection) {
181
- throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
182
- }
183
-
184
- if (!hasProviderCredentials(selection.provider)) {
185
- throw new SnapshotApiUnavailableError(
186
- missingProviderSnapshotMessage(selection),
187
- );
188
- }
189
-
190
- return selection;
191
- }
192
-
193
- export function isSnapshotApiUnavailableError(error: unknown): boolean {
194
- return error instanceof SnapshotApiUnavailableError;
195
- }
196
-
197
- // ── AI setup status ─────────────────────────────────────────────────────────
198
-
199
- /**
200
- * Workspace AI setup health states.
201
- *
202
- * - `ready`: a usable model was resolved and the matching provider has credentials.
203
- * - `configured-missing-credentials`: config pins a provider whose credentials are absent.
204
- * - `invalid-config`: `.libretto/config.json` exists but fails schema validation.
205
- * - `unconfigured`: no config and no env credentials detected.
206
- */
207
- export type AiSetupStatus =
208
- | {
209
- kind: "ready";
210
- model: string;
211
- provider: Provider;
212
- source: "config" | `env:${string}`;
213
- }
214
- | {
215
- kind: "configured-missing-credentials";
216
- model: string;
217
- provider: Provider;
218
- }
219
- | { kind: "invalid-config"; message: string }
220
- | { kind: "unconfigured" };
221
-
222
- /**
223
- * Read snapshot model without throwing on invalid files.
224
- * Returns the model string or an error message.
225
- */
226
- function readSnapshotModelSafely(
227
- configPath: string,
228
- ): { ok: true; model: string | null } | { ok: false; message: string } {
229
- try {
230
- return { ok: true, model: readSnapshotModel(configPath) };
231
- } catch (err) {
232
- return {
233
- ok: false,
234
- message: err instanceof Error ? err.message : String(err),
235
- };
236
- }
237
- }
238
-
239
- /**
240
- * Resolve the workspace's current AI setup health.
241
- *
242
- * Uses the existing config reader and snapshot model resolver, but wraps
243
- * them to distinguish broken states (invalid config, missing credentials)
244
- * that the throwing APIs collapse into errors.
245
- *
246
- * 1. If config read throws → `invalid-config`.
247
- * 2. If config has a `snapshotModel` → check credentials for that provider.
248
- * 3. If no `snapshotModel` → auto-detect from env via existing resolver.
249
- */
250
- export function resolveAiSetupStatus(
251
- configPath: string = LIBRETTO_CONFIG_PATH,
252
- ): AiSetupStatus {
253
- const result = readSnapshotModelSafely(configPath);
254
-
255
- if (!result.ok) {
256
- return { kind: "invalid-config", message: result.message };
257
- }
258
-
259
- // Config has a snapshotModel — use it directly to check credentials
260
- if (result.model) {
261
- let selection: SnapshotApiModelSelection | null;
262
- try {
263
- selection = resolveSnapshotApiModel(result.model);
264
- } catch (err) {
265
- return {
266
- kind: "invalid-config",
267
- message: err instanceof Error ? err.message : String(err),
268
- };
269
- }
270
- if (!selection) {
271
- // Should not happen when config has a model, but handle gracefully
272
- return { kind: "unconfigured" };
273
- }
274
- if (hasProviderCredentials(selection.provider)) {
275
- return {
276
- kind: "ready",
277
- model: selection.model,
278
- provider: selection.provider,
279
- source: selection.source,
280
- };
281
- }
282
- return {
283
- kind: "configured-missing-credentials",
284
- model: selection.model,
285
- provider: selection.provider,
286
- };
287
- }
288
-
289
- // No snapshotModel — fall back to env auto-detect via existing resolver
290
- const envSelection = resolveSnapshotApiModel(null);
291
- if (envSelection && hasProviderCredentials(envSelection.provider)) {
292
- return {
293
- kind: "ready",
294
- model: envSelection.model,
295
- provider: envSelection.provider,
296
- source: envSelection.source,
297
- };
298
- }
299
-
300
- return { kind: "unconfigured" };
301
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * API-based snapshot analyzer.
3
- *
4
- * Sends the DOM snapshot (condensed or full depending on sizing) and screenshot
5
- * directly to a supported API provider via the Vercel AI SDK, without spawning
6
- * a CLI process.
7
- */
8
-
9
- import { readFileSync } from "node:fs";
10
- import type { LoggerApi } from "../../shared/logger/index.js";
11
- import { generateObject } from "ai";
12
- import { resolveModel } from "./resolve-model.js";
13
- import {
14
- InterpretResultSchema,
15
- buildInlinePromptSelection,
16
- getMimeType,
17
- readFileAsBase64,
18
- type InterpretResult,
19
- type InterpretArgs,
20
- } from "./snapshot-analyzer.js";
21
- import { readSnapshotModel } from "./config.js";
22
- import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
23
-
24
- export async function runApiInterpret(
25
- args: InterpretArgs,
26
- logger: LoggerApi,
27
- snapshotModel: string | null = readSnapshotModel(),
28
- ): Promise<void> {
29
- const selection = resolveSnapshotApiModelOrThrow(snapshotModel);
30
-
31
- logger.info("api-interpret-start", {
32
- objective: args.objective,
33
- pngPath: args.pngPath,
34
- htmlPath: args.htmlPath,
35
- condensedHtmlPath: args.condensedHtmlPath,
36
- model: selection.model,
37
- modelSource: selection.source,
38
- });
39
-
40
- const fullHtmlContent = readFileSync(args.htmlPath, "utf-8");
41
- const condensedHtmlContent = readFileSync(args.condensedHtmlPath, "utf-8");
42
-
43
- const promptSelection = buildInlinePromptSelection(
44
- args,
45
- fullHtmlContent,
46
- condensedHtmlContent,
47
- selection.model,
48
- );
49
-
50
- logger.info("api-interpret-dom-selection", {
51
- configuredModel: promptSelection.stats.configuredModel,
52
- fullDomEstimatedTokens: promptSelection.stats.fullDomEstimatedTokens,
53
- condensedDomEstimatedTokens:
54
- promptSelection.stats.condensedDomEstimatedTokens,
55
- contextWindowTokens: promptSelection.budget.contextWindowTokens,
56
- promptBudgetTokens: promptSelection.budget.promptBudgetTokens,
57
- selectedDom: promptSelection.domSource,
58
- selectedHtmlEstimatedTokens: promptSelection.htmlEstimatedTokens,
59
- selectedPromptEstimatedTokens: promptSelection.promptEstimatedTokens,
60
- selectionReason: promptSelection.selectionReason,
61
- truncated: promptSelection.truncated,
62
- });
63
-
64
- const imageBase64 = readFileAsBase64(args.pngPath);
65
- const imageMimeType = getMimeType(args.pngPath);
66
- const imageBytes = Buffer.from(imageBase64, "base64");
67
-
68
- const model = await resolveModel(selection.model);
69
-
70
- const { object: result } = await generateObject({
71
- model,
72
- schema: InterpretResultSchema,
73
- messages: [
74
- {
75
- role: "user",
76
- content: [
77
- { type: "text", text: promptSelection.prompt },
78
- {
79
- type: "image",
80
- image: imageBytes,
81
- mediaType: imageMimeType,
82
- },
83
- ],
84
- },
85
- ],
86
- temperature: 0.1,
87
- });
88
-
89
- const parsed: InterpretResult = InterpretResultSchema.parse(result);
90
-
91
- logger.info("api-interpret-success", {
92
- selectorCount: parsed.selectors.length,
93
- answer: parsed.answer.slice(0, 200),
94
- });
95
-
96
- console.log("");
97
- console.log("Analysis:");
98
- console.log(parsed.answer);
99
- if (parsed.selectors.length > 0) {
100
- console.log("");
101
- console.log("Selectors:");
102
- parsed.selectors.forEach((selector, index) => {
103
- console.log(` ${index + 1}. ${selector.label}: ${selector.selector}`);
104
- });
105
- }
106
- if (parsed.notes?.trim()) {
107
- console.log("");
108
- console.log(`Notes: ${parsed.notes.trim()}`);
109
- }
110
- }