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.
- package/README.md +3 -8
- package/README.template.md +3 -8
- package/dist/cli/cli.js +0 -23
- package/dist/cli/commands/auth.js +24 -33
- package/dist/cli/commands/billing.js +3 -5
- package/dist/cli/commands/browser.js +4 -13
- package/dist/cli/commands/deploy.js +54 -45
- package/dist/cli/commands/execution.js +6 -3
- package/dist/cli/commands/experiments.js +1 -1
- package/dist/cli/commands/setup.js +2 -295
- package/dist/cli/commands/shared.js +1 -1
- package/dist/cli/commands/snapshot.js +10 -100
- package/dist/cli/commands/status.js +2 -42
- package/dist/cli/core/auth-fetch.js +11 -6
- package/dist/cli/core/browser.js +13 -8
- package/dist/cli/core/config.js +3 -6
- package/dist/cli/core/daemon/daemon.js +88 -74
- package/dist/cli/core/daemon/exec-repl.js +133 -0
- package/dist/cli/core/daemon/exec.js +6 -21
- package/dist/cli/core/daemon/ipc.js +47 -4
- package/dist/cli/core/daemon/ipc.spec.js +21 -0
- package/dist/cli/core/daemon/snapshot.js +2 -29
- package/dist/cli/core/exec-compiler.js +8 -3
- package/dist/cli/core/experiments.js +1 -28
- package/dist/cli/core/providers/index.js +13 -4
- package/dist/cli/core/providers/libretto-cloud.js +178 -26
- package/dist/cli/index.js +0 -2
- package/dist/cli/router.js +9 -6
- package/dist/shared/instrumentation/instrument.js +4 -4
- package/dist/shared/ipc/socket-transport.d.ts +2 -1
- package/dist/shared/ipc/socket-transport.js +16 -5
- package/dist/shared/ipc/socket-transport.spec.js +5 -0
- package/docs/releasing.md +8 -6
- package/package.json +3 -2
- package/skills/libretto/SKILL.md +49 -47
- package/skills/libretto/references/code-generation-rules.md +6 -0
- package/skills/libretto/references/configuration-file-reference.md +14 -12
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +6 -6
- package/skills/libretto-readonly/SKILL.md +2 -9
- package/src/cli/cli.ts +0 -24
- package/src/cli/commands/auth.ts +24 -33
- package/src/cli/commands/billing.ts +3 -5
- package/src/cli/commands/browser.ts +6 -16
- package/src/cli/commands/deploy.ts +55 -49
- package/src/cli/commands/execution.ts +6 -3
- package/src/cli/commands/experiments.ts +1 -1
- package/src/cli/commands/setup.ts +2 -381
- package/src/cli/commands/shared.ts +1 -1
- package/src/cli/commands/snapshot.ts +9 -137
- package/src/cli/commands/status.ts +2 -50
- package/src/cli/core/auth-fetch.ts +9 -4
- package/src/cli/core/browser.ts +15 -8
- package/src/cli/core/config.ts +3 -6
- package/src/cli/core/daemon/daemon.ts +106 -76
- package/src/cli/core/daemon/exec-repl.ts +189 -0
- package/src/cli/core/daemon/exec.ts +8 -43
- package/src/cli/core/daemon/ipc.spec.ts +27 -0
- package/src/cli/core/daemon/ipc.ts +81 -23
- package/src/cli/core/daemon/snapshot.ts +1 -43
- package/src/cli/core/exec-compiler.ts +8 -3
- package/src/cli/core/experiments.ts +9 -38
- package/src/cli/core/providers/index.ts +17 -4
- package/src/cli/core/providers/libretto-cloud.ts +224 -36
- package/src/cli/core/resolve-model.ts +5 -0
- package/src/cli/core/workflow-runtime.ts +1 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/router.ts +9 -6
- package/src/shared/instrumentation/instrument.ts +4 -4
- package/src/shared/ipc/socket-transport.spec.ts +6 -0
- package/src/shared/ipc/socket-transport.ts +20 -5
- package/dist/cli/commands/ai.js +0 -110
- package/dist/cli/core/ai-model.js +0 -195
- package/dist/cli/core/api-snapshot-analyzer.js +0 -86
- package/dist/cli/core/snapshot-analyzer.js +0 -667
- package/dist/cli/framework/simple-cli.js +0 -880
- package/scripts/summarize-evals.mjs +0 -135
- package/src/cli/commands/ai.ts +0 -144
- package/src/cli/core/ai-model.ts +0 -301
- package/src/cli/core/api-snapshot-analyzer.ts +0 -110
- package/src/cli/core/snapshot-analyzer.ts +0 -856
- package/src/cli/framework/simple-cli.ts +0 -1459
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "../core/browser.js";
|
|
12
12
|
import { resolveProviderName } from "../core/providers/index.js";
|
|
13
13
|
import { readLibrettoConfig } from "../core/config.js";
|
|
14
|
-
import { createLoggerForSession
|
|
14
|
+
import { createLoggerForSession } from "../core/context.js";
|
|
15
15
|
import { librettoCommand } from "../../shared/package-manager.js";
|
|
16
16
|
import {
|
|
17
17
|
type SessionAccessMode,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
validateSessionName,
|
|
21
21
|
} from "../core/session.js";
|
|
22
22
|
import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
|
|
23
|
-
import { SimpleCLI } from "
|
|
23
|
+
import { SimpleCLI } from "affordance";
|
|
24
24
|
import {
|
|
25
25
|
sessionOption,
|
|
26
26
|
withAutoSession,
|
|
@@ -63,8 +63,8 @@ function resolveRequestedSessionMode(
|
|
|
63
63
|
|
|
64
64
|
export const openInput = SimpleCLI.input({
|
|
65
65
|
positionals: [
|
|
66
|
-
SimpleCLI.positional("url", z.string().
|
|
67
|
-
help: "URL to open",
|
|
66
|
+
SimpleCLI.positional("url", z.string().default("about:blank"), {
|
|
67
|
+
help: "URL to open (defaults to about:blank)",
|
|
68
68
|
}),
|
|
69
69
|
],
|
|
70
70
|
named: {
|
|
@@ -92,10 +92,6 @@ export const openInput = SimpleCLI.input({
|
|
|
92
92
|
}),
|
|
93
93
|
},
|
|
94
94
|
})
|
|
95
|
-
.refine(
|
|
96
|
-
(input) => Boolean(input.url),
|
|
97
|
-
`Usage: ${librettoCommand("open <url> [--headless] [--read-only|--write-access] [--auth-profile <domain>] [--viewport WxH] [--session <name>]")}`,
|
|
98
|
-
)
|
|
99
95
|
.refine(
|
|
100
96
|
(input) => !(input.headed && input.headless),
|
|
101
97
|
"Cannot pass both --headed and --headless.",
|
|
@@ -119,7 +115,7 @@ export const openCommand = SimpleCLI.command({
|
|
|
119
115
|
if (providerName === "local") {
|
|
120
116
|
const headed = input.headed || !input.headless;
|
|
121
117
|
const viewport = parseViewportArg(input.viewport);
|
|
122
|
-
await runOpen(input.url
|
|
118
|
+
await runOpen(input.url, headed, ctx.session, ctx.logger, {
|
|
123
119
|
viewport,
|
|
124
120
|
accessMode: resolveRequestedSessionMode(
|
|
125
121
|
input.readOnly,
|
|
@@ -130,7 +126,7 @@ export const openCommand = SimpleCLI.command({
|
|
|
130
126
|
});
|
|
131
127
|
} else {
|
|
132
128
|
await runOpenWithProvider(
|
|
133
|
-
input.url
|
|
129
|
+
input.url,
|
|
134
130
|
providerName,
|
|
135
131
|
ctx.session,
|
|
136
132
|
ctx.logger,
|
|
@@ -295,9 +291,3 @@ export const browserCommands = {
|
|
|
295
291
|
"session-mode": sessionModeCommand,
|
|
296
292
|
close: closeCommand,
|
|
297
293
|
};
|
|
298
|
-
|
|
299
|
-
export async function runClose(session: string): Promise<void> {
|
|
300
|
-
await withSessionLogger(session, async (logger) => {
|
|
301
|
-
await runCloseWithLogger(session, logger);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
orpcCall,
|
|
5
|
+
resolveApiUrl,
|
|
6
|
+
} from "../core/auth-fetch.js";
|
|
4
7
|
import { buildHostedDeployTarball } from "../core/deploy-artifact.js";
|
|
5
|
-
import {
|
|
8
|
+
import { readAuthState } from "../core/auth-storage.js";
|
|
9
|
+
import { SimpleCLI } from "affordance";
|
|
6
10
|
|
|
7
11
|
type DeploymentStatus = "building" | "ready" | "failed";
|
|
8
12
|
|
|
@@ -19,37 +23,50 @@ function generateDeploymentName(): string {
|
|
|
19
23
|
return `deploy-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
function
|
|
23
|
-
|
|
26
|
+
function deployApiKeyRequiredMessage(hasStoredSession: boolean): string {
|
|
27
|
+
if (hasStoredSession) {
|
|
28
|
+
return [
|
|
29
|
+
"LIBRETTO_API_KEY is required to deploy to Libretto Cloud.",
|
|
30
|
+
"You are logged in locally, but deploy endpoints require API-key auth.",
|
|
31
|
+
" • Generate a key: run `libretto cloud auth api-key issue --label <label>`.",
|
|
32
|
+
" • Add it to your project .env file: `LIBRETTO_API_KEY=<issued-key>`.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return [
|
|
37
|
+
"LIBRETTO_API_KEY is required to deploy to Libretto Cloud.",
|
|
38
|
+
"No local cloud session was found.",
|
|
39
|
+
" • New account: run `libretto cloud auth signup`, then verify your email.",
|
|
40
|
+
" • Existing account: run `libretto cloud auth login`.",
|
|
41
|
+
" • Generate a key: run `libretto cloud auth api-key issue --label <label>`.",
|
|
42
|
+
" • Add it to your project .env file: `LIBRETTO_API_KEY=<issued-key>`.",
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function requireDeployApiKey() {
|
|
47
|
+
const apiKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
24
48
|
|
|
25
49
|
if (!apiKey) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
"LIBRETTO_API_KEY environment variable is required.",
|
|
28
|
-
);
|
|
50
|
+
throw new Error(deployApiKeyRequiredMessage(await hasStoredCloudSession()));
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
apiUrl: resolveApiUrl(null),
|
|
55
|
+
credential: { source: "env-api-key" as const, apiKey },
|
|
56
|
+
};
|
|
32
57
|
}
|
|
33
58
|
|
|
34
|
-
async function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return fetch(`${apiUrl}${path}`, {
|
|
41
|
-
method: "POST",
|
|
42
|
-
headers: {
|
|
43
|
-
"x-api-key": apiKey,
|
|
44
|
-
"Content-Type": "application/json",
|
|
45
|
-
},
|
|
46
|
-
body: JSON.stringify({ json: input }),
|
|
47
|
-
});
|
|
59
|
+
async function hasStoredCloudSession(): Promise<boolean> {
|
|
60
|
+
try {
|
|
61
|
+
return Boolean((await readAuthState())?.session);
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
async function pollDeployment(
|
|
51
68
|
apiUrl: string,
|
|
52
|
-
apiKey: string,
|
|
69
|
+
credential: { source: "env-api-key"; apiKey: string },
|
|
53
70
|
deploymentId: string,
|
|
54
71
|
pollIntervalMs: number,
|
|
55
72
|
maxWaitMs: number,
|
|
@@ -68,18 +85,14 @@ async function pollDeployment(
|
|
|
68
85
|
|
|
69
86
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
deployment = await orpcCall<DeploymentResponse["json"]>({
|
|
89
|
+
apiUrl,
|
|
90
|
+
path: "/v1/deployments/sync",
|
|
91
|
+
input: { id: deploymentId },
|
|
92
|
+
credential,
|
|
73
93
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
throw new Error(
|
|
77
|
-
`Failed to sync deployment status (${res.status}): ${JSON.stringify(body)}`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
status = body.json.status;
|
|
81
|
-
workflows = body.json.workflows;
|
|
82
|
-
deployment = body.json;
|
|
94
|
+
status = deployment.status;
|
|
95
|
+
workflows = deployment.workflows;
|
|
83
96
|
if (status === "ready" && readyAt === null) readyAt = Date.now();
|
|
84
97
|
process.stdout.write(".");
|
|
85
98
|
}
|
|
@@ -132,11 +145,10 @@ export const deployInput = SimpleCLI.input({
|
|
|
132
145
|
|
|
133
146
|
export const deployCommand = SimpleCLI.command({
|
|
134
147
|
description: "Deploy workflows to the hosted platform",
|
|
135
|
-
experimental: true,
|
|
136
148
|
})
|
|
137
149
|
.input(deployInput)
|
|
138
150
|
.handle(async ({ input }) => {
|
|
139
|
-
const { apiUrl,
|
|
151
|
+
const { apiUrl, credential } = await requireDeployApiKey();
|
|
140
152
|
const deploymentName = generateDeploymentName();
|
|
141
153
|
|
|
142
154
|
// Hosted deploy uploads a generated artifact with a deploy entrypoint and
|
|
@@ -157,20 +169,14 @@ export const deployCommand = SimpleCLI.command({
|
|
|
157
169
|
if (input.description) createPayload.description = input.description;
|
|
158
170
|
|
|
159
171
|
console.log("Uploading deployment...");
|
|
160
|
-
const
|
|
172
|
+
const body = await orpcCall<DeploymentResponse["json"]>({
|
|
161
173
|
apiUrl,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
);
|
|
166
|
-
const body = (await res.json()) as DeploymentResponse;
|
|
167
|
-
if (res.status !== 200) {
|
|
168
|
-
throw new Error(
|
|
169
|
-
`Failed to create deployment (${res.status}): ${JSON.stringify(body)}`,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
174
|
+
path: "/v1/deployments/create",
|
|
175
|
+
input: createPayload,
|
|
176
|
+
credential,
|
|
177
|
+
});
|
|
172
178
|
|
|
173
|
-
const { deployment_id, status } = body
|
|
179
|
+
const { deployment_id, status } = body;
|
|
174
180
|
console.log(`Deployment created: ${deployment_id}`);
|
|
175
181
|
console.log(`Status: ${status}`);
|
|
176
182
|
|
|
@@ -178,7 +184,7 @@ export const deployCommand = SimpleCLI.command({
|
|
|
178
184
|
process.stdout.write("Waiting for build");
|
|
179
185
|
const deployment = await pollDeployment(
|
|
180
186
|
apiUrl,
|
|
181
|
-
|
|
187
|
+
credential,
|
|
182
188
|
deployment_id,
|
|
183
189
|
10_000,
|
|
184
190
|
5 * 60 * 1000,
|
|
@@ -29,7 +29,10 @@ import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
|
|
|
29
29
|
import { readLibrettoConfig } from "../core/config.js";
|
|
30
30
|
import { librettoCommand } from "../../shared/package-manager.js";
|
|
31
31
|
import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
getProviderStartupTimeoutMs,
|
|
34
|
+
resolveProviderName,
|
|
35
|
+
} from "../core/providers/index.js";
|
|
33
36
|
import { getAbsoluteIntegrationPath } from "../core/workflow-runtime.js";
|
|
34
37
|
import {
|
|
35
38
|
compileExecFunction,
|
|
@@ -49,7 +52,7 @@ import {
|
|
|
49
52
|
} from "../core/telemetry.js";
|
|
50
53
|
import type { SessionAccessMode } from "../../shared/state/index.js";
|
|
51
54
|
import type { Experiments } from "../core/experiments.js";
|
|
52
|
-
import { SimpleCLI } from "
|
|
55
|
+
import { SimpleCLI } from "affordance";
|
|
53
56
|
import {
|
|
54
57
|
pageOption,
|
|
55
58
|
sessionOption,
|
|
@@ -609,7 +612,7 @@ async function runIntegrationFromFile(
|
|
|
609
612
|
},
|
|
610
613
|
logger,
|
|
611
614
|
logPath: runLogPath,
|
|
612
|
-
startupTimeoutMs:
|
|
615
|
+
startupTimeoutMs: getProviderStartupTimeoutMs(args.providerName),
|
|
613
616
|
handlers,
|
|
614
617
|
});
|
|
615
618
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type ExperimentName,
|
|
9
9
|
type Experiments,
|
|
10
10
|
} from "../core/experiments.js";
|
|
11
|
-
import { SimpleCLI } from "
|
|
11
|
+
import { SimpleCLI } from "affordance";
|
|
12
12
|
|
|
13
13
|
const experimentNames = Object.keys(EXPERIMENTS) as ExperimentName[];
|
|
14
14
|
|
|
@@ -1,382 +1,14 @@
|
|
|
1
|
-
import { createInterface } from "node:readline";
|
|
2
1
|
import { cpSync, existsSync, readdirSync, rmSync } from "node:fs";
|
|
3
2
|
import { spawnSync } from "node:child_process";
|
|
4
3
|
import { basename, dirname, join } from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { writeSnapshotModel } from "../core/config.js";
|
|
7
5
|
import {
|
|
8
6
|
ensureLibrettoSetup,
|
|
9
7
|
LIBRETTO_CONFIG_PATH,
|
|
10
8
|
REPO_ROOT,
|
|
11
9
|
} from "../core/context.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
DEFAULT_SNAPSHOT_MODELS,
|
|
15
|
-
resolveAiSetupStatus,
|
|
16
|
-
} from "../core/ai-model.js";
|
|
17
|
-
import {
|
|
18
|
-
detectProjectPackageManager,
|
|
19
|
-
installCommand,
|
|
20
|
-
librettoCommand,
|
|
21
|
-
} from "../../shared/package-manager.js";
|
|
22
|
-
import type { Provider } from "../core/resolve-model.js";
|
|
23
|
-
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
24
|
-
|
|
25
|
-
const PROVIDER_SDK_PACKAGES: Record<Provider, string> = {
|
|
26
|
-
openai: "@ai-sdk/openai",
|
|
27
|
-
anthropic: "@ai-sdk/anthropic",
|
|
28
|
-
google: "@ai-sdk/google",
|
|
29
|
-
vertex: "@ai-sdk/google-vertex",
|
|
30
|
-
openrouter: "@ai-sdk/openai",
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function isSdkInstalled(sdkPackage: string): boolean {
|
|
34
|
-
try {
|
|
35
|
-
const result = spawnSync("node", ["-e", `require.resolve("${sdkPackage}")`], {
|
|
36
|
-
cwd: REPO_ROOT,
|
|
37
|
-
stdio: "pipe",
|
|
38
|
-
});
|
|
39
|
-
return result.status === 0;
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function installSdkIfNeeded(provider: Provider): void {
|
|
46
|
-
const sdkPackage = PROVIDER_SDK_PACKAGES[provider];
|
|
47
|
-
if (isSdkInstalled(sdkPackage)) return;
|
|
48
|
-
|
|
49
|
-
const pkgManager = detectProjectPackageManager();
|
|
50
|
-
const cmd = installCommand(pkgManager);
|
|
51
|
-
console.log(`\nInstalling ${sdkPackage}...`);
|
|
52
|
-
const result = spawnSync(cmd, [sdkPackage], {
|
|
53
|
-
cwd: REPO_ROOT,
|
|
54
|
-
stdio: "inherit",
|
|
55
|
-
shell: true,
|
|
56
|
-
});
|
|
57
|
-
if (result.status === 0) {
|
|
58
|
-
console.log(`✓ Installed ${sdkPackage}`);
|
|
59
|
-
} else {
|
|
60
|
-
console.error(
|
|
61
|
-
`✗ Failed to install ${sdkPackage}. Install it manually: ${cmd} ${sdkPackage}`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type ProviderChoice = {
|
|
67
|
-
key: string;
|
|
68
|
-
label: string;
|
|
69
|
-
provider: Provider;
|
|
70
|
-
envVar: string;
|
|
71
|
-
envHint: string;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export const PROVIDER_CHOICES: ProviderChoice[] = [
|
|
75
|
-
{
|
|
76
|
-
key: "1",
|
|
77
|
-
label: "OpenAI",
|
|
78
|
-
provider: "openai",
|
|
79
|
-
envVar: "OPENAI_API_KEY",
|
|
80
|
-
envHint: "Get your key at https://platform.openai.com/api-keys",
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
key: "2",
|
|
84
|
-
label: "Anthropic",
|
|
85
|
-
provider: "anthropic",
|
|
86
|
-
envVar: "ANTHROPIC_API_KEY",
|
|
87
|
-
envHint: "Get your key at https://console.anthropic.com/settings/keys",
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
key: "3",
|
|
91
|
-
label: "Google Gemini",
|
|
92
|
-
provider: "google",
|
|
93
|
-
envVar: "GEMINI_API_KEY",
|
|
94
|
-
envHint: "Get your key at https://aistudio.google.com/apikey",
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
key: "4",
|
|
98
|
-
label: "Google Vertex AI",
|
|
99
|
-
provider: "vertex",
|
|
100
|
-
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
101
|
-
envHint:
|
|
102
|
-
"Requires `gcloud auth application-default login` and a GCP project ID",
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
key: "5",
|
|
106
|
-
label: "OpenRouter",
|
|
107
|
-
provider: "openrouter",
|
|
108
|
-
envVar: "OPENROUTER_API_KEY",
|
|
109
|
-
envHint: "Get your key at https://openrouter.ai/settings/keys",
|
|
110
|
-
},
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
function promptUser(
|
|
114
|
-
rl: ReturnType<typeof createInterface>,
|
|
115
|
-
question: string,
|
|
116
|
-
): Promise<string> {
|
|
117
|
-
return new Promise((resolve) => {
|
|
118
|
-
rl.question(question, (answer) => {
|
|
119
|
-
resolve(answer.trim());
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Map provider to a human-readable label for status messages. */
|
|
125
|
-
function providerLabel(provider: Provider): string {
|
|
126
|
-
const choice = PROVIDER_CHOICES.find((c) => c.provider === provider);
|
|
127
|
-
return choice?.label ?? provider;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Extract the env var name from source like "env:GOOGLE_CLOUD_PROJECT". */
|
|
131
|
-
function sourceEnvVar(source: string): string | null {
|
|
132
|
-
if (source.startsWith("env:")) return source.slice(4);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* If the workspace has usable credentials but no pinned model in config,
|
|
138
|
-
* write the resolved default model to `.libretto/config.json`.
|
|
139
|
-
*/
|
|
140
|
-
function ensurePinnedDefaultModel(
|
|
141
|
-
status: AiSetupStatus & { kind: "ready" },
|
|
142
|
-
): AiSetupStatus & { kind: "ready" } {
|
|
143
|
-
if (status.source !== "config") {
|
|
144
|
-
writeSnapshotModel(status.model);
|
|
145
|
-
return { ...status, source: "config" as const };
|
|
146
|
-
}
|
|
147
|
-
return status;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function printHealthySummary(status: AiSetupStatus & { kind: "ready" }): void {
|
|
151
|
-
const envVar = sourceEnvVar(status.source);
|
|
152
|
-
if (envVar) {
|
|
153
|
-
console.log(
|
|
154
|
-
`✓ Detected ${envVar}. Using ${providerLabel(status.provider)}.`,
|
|
155
|
-
);
|
|
156
|
-
} else {
|
|
157
|
-
console.log(`✓ Using ${providerLabel(status.provider)} (${status.model}).`);
|
|
158
|
-
}
|
|
159
|
-
console.log(
|
|
160
|
-
`To change: ${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function printInvalidAiConfigWarning(status: AiSetupStatus): void {
|
|
165
|
-
if (status.kind !== "invalid-config") return;
|
|
166
|
-
console.log("! Existing AI config is invalid:");
|
|
167
|
-
for (const line of status.message.split("\n")) {
|
|
168
|
-
console.log(` ${line}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ── Repair plan helpers (exported for testing) ──────────────────────────────
|
|
173
|
-
|
|
174
|
-
export type RepairChoice = "switch-provider" | "skip";
|
|
175
|
-
|
|
176
|
-
export type RepairPlan =
|
|
177
|
-
| {
|
|
178
|
-
kind: "repair-missing-credentials";
|
|
179
|
-
provider: Provider;
|
|
180
|
-
model: string;
|
|
181
|
-
envVar: string;
|
|
182
|
-
choices: RepairChoice[];
|
|
183
|
-
}
|
|
184
|
-
| { kind: "repair-invalid-config"; message: string }
|
|
185
|
-
| { kind: "no-repair-needed" };
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Determine what repair action setup should take for the current AI status.
|
|
189
|
-
* Pure function — no I/O, no prompts.
|
|
190
|
-
*/
|
|
191
|
-
export function buildRepairPlan(status: AiSetupStatus): RepairPlan {
|
|
192
|
-
if (status.kind === "configured-missing-credentials") {
|
|
193
|
-
const choice = PROVIDER_CHOICES.find((c) => c.provider === status.provider);
|
|
194
|
-
return {
|
|
195
|
-
kind: "repair-missing-credentials",
|
|
196
|
-
provider: status.provider,
|
|
197
|
-
model: status.model,
|
|
198
|
-
envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
|
|
199
|
-
choices: ["switch-provider", "skip"],
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
if (status.kind === "invalid-config") {
|
|
203
|
-
return { kind: "repair-invalid-config", message: status.message };
|
|
204
|
-
}
|
|
205
|
-
return { kind: "no-repair-needed" };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Format a provider-specific explanation for missing credentials.
|
|
210
|
-
*/
|
|
211
|
-
export function formatMissingCredentialsMessage(
|
|
212
|
-
plan: RepairPlan & { kind: "repair-missing-credentials" },
|
|
213
|
-
): string {
|
|
214
|
-
return `✗ ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function printSnapshotApiStatus(): boolean {
|
|
218
|
-
const status = resolveAiSetupStatus();
|
|
219
|
-
|
|
220
|
-
console.log(
|
|
221
|
-
"\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
if (status.kind === "ready") {
|
|
225
|
-
console.log();
|
|
226
|
-
printHealthySummary(status);
|
|
227
|
-
ensurePinnedDefaultModel(status);
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Provider-specific missing-credentials message
|
|
232
|
-
const plan = buildRepairPlan(status);
|
|
233
|
-
if (plan.kind === "repair-missing-credentials") {
|
|
234
|
-
console.log();
|
|
235
|
-
console.log(formatMissingCredentialsMessage(plan));
|
|
236
|
-
console.log(
|
|
237
|
-
` To fix: add ${plan.envVar} to .env, or run \`${librettoCommand("setup")}\` interactively to repair.`,
|
|
238
|
-
);
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (plan.kind === "repair-invalid-config") {
|
|
243
|
-
printInvalidAiConfigWarning(status);
|
|
244
|
-
console.log(
|
|
245
|
-
` Run \`${librettoCommand("setup")}\` interactively to reconfigure.`,
|
|
246
|
-
);
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
console.log();
|
|
251
|
-
console.log("✗ No snapshot API credentials detected.");
|
|
252
|
-
console.log(" Add one provider to .env:");
|
|
253
|
-
console.log(" OPENAI_API_KEY=...");
|
|
254
|
-
console.log(" ANTHROPIC_API_KEY=...");
|
|
255
|
-
console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
|
|
256
|
-
console.log(
|
|
257
|
-
" GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex",
|
|
258
|
-
);
|
|
259
|
-
console.log(
|
|
260
|
-
` Or run \`${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}\` to set a specific model.`,
|
|
261
|
-
);
|
|
262
|
-
console.log(
|
|
263
|
-
` Run \`${librettoCommand("setup")}\` interactively to set up credentials.`,
|
|
264
|
-
);
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Run the full provider selection menu.
|
|
270
|
-
* Pins the selected provider's default model to config and prints
|
|
271
|
-
* instructions for the user to add the credential to .env themselves.
|
|
272
|
-
* Returns true if a provider was successfully configured.
|
|
273
|
-
*/
|
|
274
|
-
async function promptProviderSelection(
|
|
275
|
-
rl: ReturnType<typeof createInterface>,
|
|
276
|
-
): Promise<boolean> {
|
|
277
|
-
console.log(
|
|
278
|
-
"Which model provider would you like to use for snapshot analysis?\n",
|
|
279
|
-
);
|
|
280
|
-
for (const choice of PROVIDER_CHOICES) {
|
|
281
|
-
console.log(` ${choice.key}) ${choice.label}`);
|
|
282
|
-
}
|
|
283
|
-
console.log(" s) Skip for now\n");
|
|
284
|
-
|
|
285
|
-
const answer = await promptUser(rl, "Choice: ");
|
|
286
|
-
|
|
287
|
-
if (answer.toLowerCase() === "s" || !answer) {
|
|
288
|
-
printSkipMessage();
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
|
|
293
|
-
if (!selected) {
|
|
294
|
-
console.log(`\nUnknown choice "${answer}". Skipping API setup.`);
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const model = DEFAULT_SNAPSHOT_MODELS[selected.provider];
|
|
299
|
-
writeSnapshotModel(model);
|
|
300
|
-
console.log(`\n✓ ${selected.label} selected (model: ${model}).`);
|
|
301
|
-
console.log(`\nAdd ${selected.envVar} to your .env file:`);
|
|
302
|
-
console.log(` ${selected.envHint}`);
|
|
303
|
-
installSdkIfNeeded(selected.provider);
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function printSkipMessage(): void {
|
|
308
|
-
console.log(
|
|
309
|
-
`\nSkipped. You can set up API credentials later by rerunning \`${librettoCommand("setup")}\`.`,
|
|
310
|
-
);
|
|
311
|
-
console.log("Or add credentials directly to your .env file:");
|
|
312
|
-
console.log(" OPENAI_API_KEY=...");
|
|
313
|
-
console.log(" ANTHROPIC_API_KEY=...");
|
|
314
|
-
console.log(" GEMINI_API_KEY=...");
|
|
315
|
-
console.log(
|
|
316
|
-
` Or run \`${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}\` to set a specific model.`,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async function runInteractiveApiSetup(): Promise<void> {
|
|
321
|
-
const status = resolveAiSetupStatus();
|
|
322
|
-
|
|
323
|
-
console.log(
|
|
324
|
-
"\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
if (status.kind === "ready") {
|
|
328
|
-
console.log();
|
|
329
|
-
printHealthySummary(status);
|
|
330
|
-
ensurePinnedDefaultModel(status);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const plan = buildRepairPlan(status);
|
|
335
|
-
|
|
336
|
-
const rl = createInterface({
|
|
337
|
-
input: process.stdin,
|
|
338
|
-
output: process.stdout,
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
// ── Repair: configured provider with missing credentials ──
|
|
343
|
-
if (plan.kind === "repair-missing-credentials") {
|
|
344
|
-
console.log(formatMissingCredentialsMessage(plan));
|
|
345
|
-
console.log(`\nAdd ${plan.envVar} to your .env file to fix this.`);
|
|
346
|
-
console.log("");
|
|
347
|
-
console.log("Or switch to a different provider:\n");
|
|
348
|
-
console.log(" 1) Switch to a different provider");
|
|
349
|
-
console.log(" s) Skip for now\n");
|
|
350
|
-
|
|
351
|
-
const answer = await promptUser(rl, "Choice: ");
|
|
352
|
-
|
|
353
|
-
if (answer === "1") {
|
|
354
|
-
await promptProviderSelection(rl);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// skip or empty
|
|
359
|
-
printSkipMessage();
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// ── Repair: invalid config → let user pick a provider ──
|
|
364
|
-
if (plan.kind === "repair-invalid-config") {
|
|
365
|
-
printInvalidAiConfigWarning(status);
|
|
366
|
-
console.log(
|
|
367
|
-
"\nWould you like to reconfigure with a fresh provider selection?\n",
|
|
368
|
-
);
|
|
369
|
-
await promptProviderSelection(rl);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ── Unconfigured: standard first-run flow ──
|
|
374
|
-
console.log("✗ No snapshot API credentials detected.\n");
|
|
375
|
-
await promptProviderSelection(rl);
|
|
376
|
-
} finally {
|
|
377
|
-
rl.close();
|
|
378
|
-
}
|
|
379
|
-
}
|
|
10
|
+
import { librettoCommand } from "../../shared/package-manager.js";
|
|
11
|
+
import { SimpleCLI } from "affordance";
|
|
380
12
|
|
|
381
13
|
function installBrowsers(): void {
|
|
382
14
|
console.log("Installing Playwright Chromium...");
|
|
@@ -486,17 +118,6 @@ export const setupCommand = SimpleCLI.command({
|
|
|
486
118
|
|
|
487
119
|
copySkills();
|
|
488
120
|
|
|
489
|
-
if (process.stdin.isTTY) {
|
|
490
|
-
await runInteractiveApiSetup();
|
|
491
|
-
} else {
|
|
492
|
-
const ready = printSnapshotApiStatus();
|
|
493
|
-
if (!ready) {
|
|
494
|
-
console.log(
|
|
495
|
-
`\nIf you're an agent, request the user to run \`${librettoCommand("setup")}\`.`,
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
121
|
console.log(`\nConfig set up at ${LIBRETTO_CONFIG_PATH}`);
|
|
501
122
|
console.log("\n✓ libretto setup complete");
|
|
502
123
|
});
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type SimpleCLIMiddlewareArgs,
|
|
15
15
|
type SimpleCLIContext,
|
|
16
16
|
type SimpleCLIMiddleware,
|
|
17
|
-
} from "
|
|
17
|
+
} from "affordance";
|
|
18
18
|
|
|
19
19
|
export function sessionOption(help = "Session name") {
|
|
20
20
|
return SimpleCLI.option(z.string().optional(), { help });
|