libretto 0.5.0 → 0.5.2
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.
- package/README.md +109 -35
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/execution.js +199 -86
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +24 -5
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +6 -2
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +15 -15
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +132 -54
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +210 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/execution.ts +233 -102
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +144 -77
- package/src/cli/router.ts +13 -21
- package/src/cli/workers/run-integration-runtime.ts +36 -9
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +73 -66
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +19 -25
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
- package/skills/libretto/references/user-action-log.md +0 -31
package/src/cli/core/session.ts
CHANGED
|
@@ -23,9 +23,17 @@ import {
|
|
|
23
23
|
|
|
24
24
|
const SESSION_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
25
25
|
|
|
26
|
-
export const SESSION_DEFAULT = "default";
|
|
27
26
|
export const SESSION_DEV_SERVER = "dev-server";
|
|
28
27
|
export const SESSION_BROWSER_AGENT = "browser-agent";
|
|
28
|
+
|
|
29
|
+
export function generateSessionName(): string {
|
|
30
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
31
|
+
let id = "";
|
|
32
|
+
for (let i = 0; i < 4; i++) {
|
|
33
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
34
|
+
}
|
|
35
|
+
return `ses-${id}`;
|
|
36
|
+
}
|
|
29
37
|
export { SESSION_STATE_VERSION };
|
|
30
38
|
export type { SessionStatus, SessionState };
|
|
31
39
|
|
|
@@ -136,7 +144,10 @@ export function readSessionStateOrThrow(session: string): SessionState {
|
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
try {
|
|
139
|
-
return parseSessionStateContent(
|
|
147
|
+
return parseSessionStateContent(
|
|
148
|
+
readFileSync(stateFile, "utf-8"),
|
|
149
|
+
stateFile,
|
|
150
|
+
);
|
|
140
151
|
} catch (err) {
|
|
141
152
|
throw new Error(
|
|
142
153
|
`Could not read session state for "${session}": ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -186,10 +197,13 @@ export function setSessionStatus(
|
|
|
186
197
|
const state = readSessionState(session, logger);
|
|
187
198
|
if (!state) return;
|
|
188
199
|
if (state.status === status) return;
|
|
189
|
-
writeSessionState(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
200
|
+
writeSessionState(
|
|
201
|
+
{
|
|
202
|
+
...state,
|
|
203
|
+
status,
|
|
204
|
+
},
|
|
205
|
+
logger,
|
|
206
|
+
);
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
export function assertSessionAvailableForStart(
|
|
@@ -198,7 +212,7 @@ export function assertSessionAvailableForStart(
|
|
|
198
212
|
): void {
|
|
199
213
|
const existingState = readSessionState(session, logger);
|
|
200
214
|
if (!existingState) return;
|
|
201
|
-
if (!isPidRunning(existingState.pid)) {
|
|
215
|
+
if (existingState.pid == null || !isPidRunning(existingState.pid)) {
|
|
202
216
|
setSessionStatus(session, "exited", logger);
|
|
203
217
|
return;
|
|
204
218
|
}
|
|
@@ -8,15 +8,10 @@
|
|
|
8
8
|
* to the CLI-agent approach if needed.
|
|
9
9
|
*
|
|
10
10
|
* Shared types and utilities (InterpretResultSchema, buildInlinePromptSelection,
|
|
11
|
-
*
|
|
11
|
+
* etc.) are still actively used by the API analyzer.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
existsSync,
|
|
16
|
-
mkdtempSync,
|
|
17
|
-
readFileSync,
|
|
18
|
-
rmSync,
|
|
19
|
-
} from "node:fs";
|
|
14
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
20
15
|
import { extname, isAbsolute, join, resolve } from "node:path";
|
|
21
16
|
import { spawn } from "node:child_process";
|
|
22
17
|
import { tmpdir } from "node:os";
|
|
@@ -147,7 +142,12 @@ abstract class UserCodingAgent {
|
|
|
147
142
|
logger: LoggerApi,
|
|
148
143
|
stdinText?: string,
|
|
149
144
|
): Promise<ExternalCommandResult> {
|
|
150
|
-
const result = await runExternalCommand(
|
|
145
|
+
const result = await runExternalCommand(
|
|
146
|
+
this.command,
|
|
147
|
+
args,
|
|
148
|
+
logger,
|
|
149
|
+
stdinText,
|
|
150
|
+
);
|
|
151
151
|
if (result.exitCode !== 0) {
|
|
152
152
|
throw new Error(
|
|
153
153
|
`Analyzer command failed (${[this.command, ...args].join(" ")}).\n${stripAnsi(result.stderr).trim() || stripAnsi(result.stdout).trim() || "No error output."}`,
|
|
@@ -586,17 +586,18 @@ function estimateTokensFromChars(chars: number): number {
|
|
|
586
586
|
return Math.ceil(chars / 4);
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
-
function inferContextWindowTokens(
|
|
590
|
-
|
|
591
|
-
|
|
589
|
+
function inferContextWindowTokens(model: string): {
|
|
590
|
+
contextWindowTokens: number;
|
|
591
|
+
source: string;
|
|
592
|
+
} {
|
|
592
593
|
const normalized = model.trim().toLowerCase();
|
|
593
594
|
if (normalized.includes("claude")) {
|
|
594
595
|
return { contextWindowTokens: 200_000, source: "model:claude" };
|
|
595
596
|
}
|
|
596
597
|
if (
|
|
597
|
-
normalized.includes("gpt-5")
|
|
598
|
-
|
|
599
|
-
|
|
598
|
+
normalized.includes("gpt-5") ||
|
|
599
|
+
normalized.includes("o3") ||
|
|
600
|
+
normalized.includes("o4")
|
|
600
601
|
) {
|
|
601
602
|
return { contextWindowTokens: 200_000, source: "model:openai" };
|
|
602
603
|
}
|
|
@@ -699,7 +700,9 @@ export function buildInlinePromptSelection(
|
|
|
699
700
|
fullDomChars: fullHtmlContent.length,
|
|
700
701
|
fullDomEstimatedTokens: estimateTokensFromChars(fullHtmlContent.length),
|
|
701
702
|
condensedDomChars: condensedHtmlContent.length,
|
|
702
|
-
condensedDomEstimatedTokens: estimateTokensFromChars(
|
|
703
|
+
condensedDomEstimatedTokens: estimateTokensFromChars(
|
|
704
|
+
condensedHtmlContent.length,
|
|
705
|
+
),
|
|
703
706
|
configuredModel: model,
|
|
704
707
|
};
|
|
705
708
|
|
|
@@ -740,8 +743,7 @@ export function buildInlinePromptSelection(
|
|
|
740
743
|
false,
|
|
741
744
|
);
|
|
742
745
|
if (fullCandidate.promptEstimatedTokens <= budget.promptBudgetTokens) {
|
|
743
|
-
const selectionReason =
|
|
744
|
-
`Full DOM fits within the estimated prompt budget (~${fullCandidate.promptEstimatedTokens.toLocaleString()} <= ${budget.promptBudgetTokens.toLocaleString()} tokens), so the analyzer receives the uncondensed page HTML.`;
|
|
746
|
+
const selectionReason = `Full DOM fits within the estimated prompt budget (~${fullCandidate.promptEstimatedTokens.toLocaleString()} <= ${budget.promptBudgetTokens.toLocaleString()} tokens), so the analyzer receives the uncondensed page HTML.`;
|
|
745
747
|
const prompt = buildInlineHtmlPrompt(args, {
|
|
746
748
|
htmlContent: fullHtmlContent,
|
|
747
749
|
domLabel: "full DOM",
|
|
@@ -784,7 +786,10 @@ export function buildInlinePromptSelection(
|
|
|
784
786
|
2_000,
|
|
785
787
|
budget.promptBudgetTokens - estimateTokensFromChars(basePrompt.length),
|
|
786
788
|
);
|
|
787
|
-
const truncatedHtml = truncateText(
|
|
789
|
+
const truncatedHtml = truncateText(
|
|
790
|
+
condensedHtmlContent,
|
|
791
|
+
availableHtmlTokens * 4,
|
|
792
|
+
);
|
|
788
793
|
|
|
789
794
|
return buildCandidate(
|
|
790
795
|
"condensed",
|
|
@@ -794,31 +799,6 @@ export function buildInlinePromptSelection(
|
|
|
794
799
|
);
|
|
795
800
|
}
|
|
796
801
|
|
|
797
|
-
export function formatInterpretationOutput(
|
|
798
|
-
parsed: InterpretResult,
|
|
799
|
-
header: string = "Interpretation:",
|
|
800
|
-
): string {
|
|
801
|
-
const outputLines: string[] = [];
|
|
802
|
-
outputLines.push(header);
|
|
803
|
-
outputLines.push(`Answer: ${parsed.answer}`);
|
|
804
|
-
outputLines.push("");
|
|
805
|
-
if (parsed.selectors.length === 0) {
|
|
806
|
-
outputLines.push("Selectors: none found.");
|
|
807
|
-
} else {
|
|
808
|
-
outputLines.push("Selectors:");
|
|
809
|
-
parsed.selectors.forEach((selector, index) => {
|
|
810
|
-
outputLines.push(` ${index + 1}. ${selector.label}`);
|
|
811
|
-
outputLines.push(` selector: ${selector.selector}`);
|
|
812
|
-
outputLines.push(` rationale: ${selector.rationale}`);
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
if (parsed.notes && parsed.notes.trim()) {
|
|
816
|
-
outputLines.push("");
|
|
817
|
-
outputLines.push(`Notes: ${parsed.notes.trim()}`);
|
|
818
|
-
}
|
|
819
|
-
return outputLines.join("\n");
|
|
820
|
-
}
|
|
821
|
-
|
|
822
802
|
export async function runInterpret(
|
|
823
803
|
args: InterpretArgs,
|
|
824
804
|
logger: LoggerApi,
|
|
@@ -860,14 +840,14 @@ export async function runInterpret(
|
|
|
860
840
|
// re-enabled, the caller must supply a valid provider/model-id string.
|
|
861
841
|
throw new Error(
|
|
862
842
|
"The CLI-agent snapshot analysis path is not active. " +
|
|
863
|
-
|
|
864
|
-
|
|
843
|
+
"Update your config to the current format with `npx libretto ai configure <provider>`, " +
|
|
844
|
+
"or set API credentials in .env for direct API analysis.",
|
|
865
845
|
);
|
|
866
846
|
|
|
867
847
|
// Preserved for reference — to re-enable, remove the throw above and:
|
|
868
848
|
// const selection = buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent, model);
|
|
869
849
|
// const parsed = await configuredAgent.analyzeSnapshot(selection.prompt, pngPath, logger);
|
|
870
|
-
// console.log(
|
|
850
|
+
// console.log(parsed.answer);
|
|
871
851
|
}
|
|
872
852
|
|
|
873
853
|
export function canAnalyzeSnapshots(): boolean {
|
|
@@ -1,191 +1,188 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
type AiConfig,
|
|
5
|
-
readAiConfig,
|
|
6
|
-
} from "./ai-config.js";
|
|
3
|
+
import { type AiConfig, readAiConfig } from "./ai-config.js";
|
|
7
4
|
import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
|
|
8
5
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
hasProviderCredentials,
|
|
7
|
+
parseModel,
|
|
8
|
+
type Provider,
|
|
12
9
|
} from "../../shared/llm/client.js";
|
|
13
10
|
|
|
14
11
|
const DEFAULT_SNAPSHOT_MODELS = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
openai: "openai/gpt-5.4",
|
|
13
|
+
anthropic: "anthropic/claude-sonnet-4-6",
|
|
14
|
+
google: "google/gemini-3-flash-preview",
|
|
15
|
+
vertex: "vertex/gemini-2.5-pro",
|
|
19
16
|
} as const satisfies Record<Provider, string>;
|
|
20
17
|
|
|
21
18
|
export type SnapshotApiModelSelection = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
model: string;
|
|
20
|
+
provider: Provider;
|
|
21
|
+
source:
|
|
22
|
+
| "config"
|
|
23
|
+
| "env:auto-openai"
|
|
24
|
+
| "env:auto-anthropic"
|
|
25
|
+
| "env:auto-google"
|
|
26
|
+
| "env:auto-vertex";
|
|
30
27
|
};
|
|
31
28
|
|
|
32
29
|
export class SnapshotApiUnavailableError extends Error {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
constructor(message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "SnapshotApiUnavailableError";
|
|
33
|
+
}
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
function providerSetupSentence(provider: Provider): string {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
switch (provider) {
|
|
38
|
+
case "openai":
|
|
39
|
+
return "Add OPENAI_API_KEY to .env or as a shell environment variable.";
|
|
40
|
+
case "anthropic":
|
|
41
|
+
return "Add ANTHROPIC_API_KEY to .env or as a shell environment variable.";
|
|
42
|
+
case "google":
|
|
43
|
+
return "Add GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY to .env or as a shell environment variable.";
|
|
44
|
+
case "vertex":
|
|
45
|
+
return "Add GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT to .env or as a shell environment variable, and make sure application default credentials are configured.";
|
|
46
|
+
}
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
function defaultModelCommandLine(): string {
|
|
53
|
-
|
|
50
|
+
return "npx libretto ai configure openai | anthropic | gemini | vertex";
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
function providerMissingCredentialSummary(provider: Provider): string {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
switch (provider) {
|
|
55
|
+
case "openai":
|
|
56
|
+
return "OPENAI_API_KEY is missing";
|
|
57
|
+
case "anthropic":
|
|
58
|
+
return "ANTHROPIC_API_KEY is missing";
|
|
59
|
+
case "google":
|
|
60
|
+
return "GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are missing";
|
|
61
|
+
case "vertex":
|
|
62
|
+
return "GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT are missing";
|
|
63
|
+
}
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
function noSnapshotApiConfiguredMessage(): string {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
return [
|
|
68
|
+
"Failed to analyze snapshot because no snapshot analyzer is configured.",
|
|
69
|
+
`Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, or GOOGLE_CLOUD_PROJECT to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
|
|
70
|
+
"For more info, run `npx libretto init`.",
|
|
71
|
+
].join(" ");
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
function missingProviderSnapshotMessage(
|
|
78
|
-
|
|
75
|
+
selection: SnapshotApiModelSelection,
|
|
79
76
|
): string {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
const configuredSource =
|
|
78
|
+
selection.source === "config"
|
|
79
|
+
? ` in ${LIBRETTO_CONFIG_PATH}`
|
|
80
|
+
: " from process env or .env";
|
|
81
|
+
return [
|
|
82
|
+
`Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
|
|
83
|
+
providerSetupSentence(selection.provider),
|
|
84
|
+
"For more info, run `npx libretto init`.",
|
|
85
|
+
].join(" ");
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
function readWorktreeEnvPath(): string | null {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
89
|
+
const gitPath = join(REPO_ROOT, ".git");
|
|
90
|
+
if (!existsSync(gitPath)) return null;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const gitPointer = readFileSync(gitPath, "utf-8").trim();
|
|
94
|
+
const match = gitPointer.match(/^gitdir:\s*(.+)$/i);
|
|
95
|
+
if (!match?.[1]) return null;
|
|
96
|
+
const worktreeGitDir = resolve(REPO_ROOT, match[1].trim());
|
|
97
|
+
const commonGitDir = resolve(worktreeGitDir, "..", "..");
|
|
98
|
+
return join(dirname(commonGitDir), ".env");
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
export function loadSnapshotEnv(): void {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
105
|
+
if (process.env.LIBRETTO_DISABLE_DOTENV?.trim() === "1") return;
|
|
106
|
+
|
|
107
|
+
const envPathCandidates = [
|
|
108
|
+
join(REPO_ROOT, ".env"),
|
|
109
|
+
readWorktreeEnvPath(),
|
|
110
|
+
].filter((value): value is string => Boolean(value));
|
|
111
|
+
|
|
112
|
+
const envPath = envPathCandidates.find((candidate) => existsSync(candidate));
|
|
113
|
+
if (!envPath) return;
|
|
114
|
+
|
|
115
|
+
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
116
|
+
const parsed = parseDotEnvAssignment(line);
|
|
117
|
+
if (!parsed) continue;
|
|
118
|
+
if (!(parsed.key in process.env)) {
|
|
119
|
+
process.env[parsed.key] = parsed.value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
125
122
|
}
|
|
126
123
|
|
|
127
124
|
export function parseDotEnvAssignment(
|
|
128
|
-
|
|
125
|
+
line: string,
|
|
129
126
|
): { key: string; value: string } | null {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
129
|
+
|
|
130
|
+
const withoutExport = trimmed.startsWith("export ")
|
|
131
|
+
? trimmed.slice("export ".length).trimStart()
|
|
132
|
+
: trimmed;
|
|
133
|
+
const eqIdx = withoutExport.indexOf("=");
|
|
134
|
+
if (eqIdx < 1) return null;
|
|
135
|
+
|
|
136
|
+
const key = withoutExport.slice(0, eqIdx).trim();
|
|
137
|
+
if (!key) return null;
|
|
138
|
+
|
|
139
|
+
const rawValue = withoutExport.slice(eqIdx + 1).trimStart();
|
|
140
|
+
if (!rawValue) {
|
|
141
|
+
return { key, value: "" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (rawValue.startsWith('"')) {
|
|
145
|
+
const closeIdx = rawValue.indexOf('"', 1);
|
|
146
|
+
if (closeIdx > 0) {
|
|
147
|
+
return { key, value: rawValue.slice(1, closeIdx) };
|
|
148
|
+
}
|
|
149
|
+
return { key, value: rawValue.slice(1) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (rawValue.startsWith("'")) {
|
|
153
|
+
const closeIdx = rawValue.indexOf("'", 1);
|
|
154
|
+
if (closeIdx > 0) {
|
|
155
|
+
return { key, value: rawValue.slice(1, closeIdx) };
|
|
156
|
+
}
|
|
157
|
+
return { key, value: rawValue.slice(1) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const inlineCommentIndex = rawValue.search(/\s#/);
|
|
161
|
+
const value =
|
|
162
|
+
inlineCommentIndex >= 0
|
|
163
|
+
? rawValue.slice(0, inlineCommentIndex).trimEnd()
|
|
164
|
+
: rawValue.trim();
|
|
165
|
+
return { key, value };
|
|
169
166
|
}
|
|
170
167
|
|
|
171
168
|
function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
169
|
+
const providersInPriorityOrder: Provider[] = [
|
|
170
|
+
"openai",
|
|
171
|
+
"anthropic",
|
|
172
|
+
"google",
|
|
173
|
+
"vertex",
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
for (const provider of providersInPriorityOrder) {
|
|
177
|
+
if (!hasProviderCredentials(provider)) continue;
|
|
178
|
+
return {
|
|
179
|
+
model: DEFAULT_SNAPSHOT_MODELS[provider],
|
|
180
|
+
provider,
|
|
181
|
+
source: `env:auto-${provider}` as SnapshotApiModelSelection["source"],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return null;
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
/**
|
|
@@ -196,41 +193,39 @@ function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
|
|
|
196
193
|
* 2. Auto-detect from available API credentials in env
|
|
197
194
|
*/
|
|
198
195
|
export function resolveSnapshotApiModel(
|
|
199
|
-
|
|
196
|
+
config: AiConfig | null = readAiConfig(),
|
|
200
197
|
): SnapshotApiModelSelection | null {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
198
|
+
loadSnapshotEnv();
|
|
199
|
+
|
|
200
|
+
if (config?.model) {
|
|
201
|
+
const { provider } = parseModel(config.model);
|
|
202
|
+
return {
|
|
203
|
+
model: config.model,
|
|
204
|
+
provider,
|
|
205
|
+
source: "config",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return inferAutoSnapshotModel();
|
|
213
210
|
}
|
|
214
211
|
|
|
215
212
|
export function resolveSnapshotApiModelOrThrow(
|
|
216
|
-
|
|
213
|
+
config: AiConfig | null = readAiConfig(),
|
|
217
214
|
): SnapshotApiModelSelection {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return selection;
|
|
215
|
+
const selection = resolveSnapshotApiModel(config);
|
|
216
|
+
if (!selection) {
|
|
217
|
+
throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!hasProviderCredentials(selection.provider)) {
|
|
221
|
+
throw new SnapshotApiUnavailableError(
|
|
222
|
+
missingProviderSnapshotMessage(selection),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return selection;
|
|
232
227
|
}
|
|
233
228
|
|
|
234
229
|
export function isSnapshotApiUnavailableError(error: unknown): boolean {
|
|
235
|
-
|
|
230
|
+
return error instanceof SnapshotApiUnavailableError;
|
|
236
231
|
}
|