libretto 0.6.7-experimental.0 → 0.6.8
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 +27 -69
- package/README.template.md +27 -69
- package/dist/cli/commands/setup.js +12 -4
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/core/ai-model.js +17 -70
- package/dist/cli/core/browser-daemon.js +122 -0
- package/dist/cli/core/browser.js +31 -176
- package/dist/cli/core/config.js +1 -1
- package/dist/cli/core/providers/libretto-cloud.js +5 -3
- package/dist/cli/core/resolve-model.js +20 -2
- package/dist/cli/workers/run-integration-runtime.js +5 -2
- package/dist/shared/dom-semantics.js +0 -1
- package/dist/shared/env/load-env.d.ts +9 -4
- package/dist/shared/env/load-env.js +57 -36
- package/package.json +1 -1
- package/scripts/generate-changelog.ts +9 -6
- package/skills/libretto/SKILL.md +14 -3
- package/skills/libretto/references/configuration-file-reference.md +1 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/setup.ts +11 -3
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/core/ai-model.ts +17 -89
- package/src/cli/core/browser-daemon.ts +198 -0
- package/src/cli/core/browser.ts +27 -187
- package/src/cli/core/config.ts +1 -1
- package/src/cli/core/providers/libretto-cloud.ts +8 -5
- package/src/cli/core/resolve-model.ts +20 -2
- package/src/cli/workers/run-integration-runtime.ts +12 -2
- package/src/shared/dom-semantics.ts +0 -1
- package/src/shared/env/load-env.ts +75 -53
|
@@ -13,6 +13,8 @@ export function createLibrettoCloudProvider(): ProviderApi {
|
|
|
13
13
|
);
|
|
14
14
|
const endpoint = apiUrl.replace(/\/$/, "");
|
|
15
15
|
|
|
16
|
+
// The Libretto Cloud API is an oRPC RPCHandler, not plain REST, so inputs
|
|
17
|
+
// must be wrapped as { json: ... } and outputs arrive the same way.
|
|
16
18
|
return {
|
|
17
19
|
async createSession() {
|
|
18
20
|
const timeoutSeconds = Number(
|
|
@@ -24,7 +26,9 @@ export function createLibrettoCloudProvider(): ProviderApi {
|
|
|
24
26
|
"x-api-key": apiKey,
|
|
25
27
|
"Content-Type": "application/json",
|
|
26
28
|
},
|
|
27
|
-
body: JSON.stringify({
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
json: { timeout_seconds: timeoutSeconds },
|
|
31
|
+
}),
|
|
28
32
|
});
|
|
29
33
|
if (!resp.ok) {
|
|
30
34
|
const body = await resp.text();
|
|
@@ -32,9 +36,8 @@ export function createLibrettoCloudProvider(): ProviderApi {
|
|
|
32
36
|
`Libretto Cloud API error (${resp.status}): ${body}`,
|
|
33
37
|
);
|
|
34
38
|
}
|
|
35
|
-
const json = (await resp.json()) as {
|
|
36
|
-
session_id: string;
|
|
37
|
-
cdp_url: string;
|
|
39
|
+
const { json } = (await resp.json()) as {
|
|
40
|
+
json: { session_id: string; cdp_url: string };
|
|
38
41
|
};
|
|
39
42
|
return {
|
|
40
43
|
sessionId: json.session_id,
|
|
@@ -48,7 +51,7 @@ export function createLibrettoCloudProvider(): ProviderApi {
|
|
|
48
51
|
"x-api-key": apiKey,
|
|
49
52
|
"Content-Type": "application/json",
|
|
50
53
|
},
|
|
51
|
-
body: JSON.stringify({ session_id: sessionId }),
|
|
54
|
+
body: JSON.stringify({ json: { session_id: sessionId } }),
|
|
52
55
|
});
|
|
53
56
|
if (!resp.ok) {
|
|
54
57
|
const body = await resp.text();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LanguageModel } from "ai";
|
|
2
2
|
|
|
3
|
-
export type Provider = "google" | "vertex" | "anthropic" | "openai";
|
|
3
|
+
export type Provider = "google" | "vertex" | "anthropic" | "openai" | "openrouter";
|
|
4
4
|
|
|
5
5
|
const GEMINI_API_KEY_ENV_VARS = [
|
|
6
6
|
"GEMINI_API_KEY",
|
|
@@ -19,6 +19,7 @@ const SUPPORTED_PROVIDER_ALIASES = {
|
|
|
19
19
|
anthropic: "anthropic",
|
|
20
20
|
codex: "openai",
|
|
21
21
|
openai: "openai",
|
|
22
|
+
openrouter: "openrouter",
|
|
22
23
|
} as const satisfies Record<string, Provider>;
|
|
23
24
|
|
|
24
25
|
function readFirstEnvValue(
|
|
@@ -51,7 +52,7 @@ export function parseModel(model: string): {
|
|
|
51
52
|
|
|
52
53
|
if (!provider) {
|
|
53
54
|
throw new Error(
|
|
54
|
-
`Unsupported provider "${providerInput}". Supported providers: openai/codex, anthropic, google (Gemini API), and
|
|
55
|
+
`Unsupported provider "${providerInput}". Supported providers: openai/codex, anthropic, google (Gemini API), vertex, and openrouter.`,
|
|
55
56
|
);
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -71,6 +72,8 @@ export function hasProviderCredentials(
|
|
|
71
72
|
return Boolean(env.ANTHROPIC_API_KEY?.trim());
|
|
72
73
|
case "openai":
|
|
73
74
|
return Boolean(env.OPENAI_API_KEY?.trim());
|
|
75
|
+
case "openrouter":
|
|
76
|
+
return Boolean(env.OPENROUTER_API_KEY?.trim());
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -86,6 +89,9 @@ export function missingProviderCredentialsMessage(provider: Provider): string {
|
|
|
86
89
|
case "openai": {
|
|
87
90
|
return "OpenAI API key is missing. Set OPENAI_API_KEY.";
|
|
88
91
|
}
|
|
92
|
+
case "openrouter": {
|
|
93
|
+
return "OpenRouter API key is missing. Set OPENROUTER_API_KEY.";
|
|
94
|
+
}
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -133,6 +139,18 @@ async function getProviderModel(
|
|
|
133
139
|
const openai = createOpenAI({ apiKey });
|
|
134
140
|
return openai(modelId);
|
|
135
141
|
}
|
|
142
|
+
case "openrouter": {
|
|
143
|
+
const apiKey = process.env.OPENROUTER_API_KEY?.trim();
|
|
144
|
+
if (!apiKey) {
|
|
145
|
+
throw new Error(missingProviderCredentialsMessage(provider));
|
|
146
|
+
}
|
|
147
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
148
|
+
const openrouter = createOpenAI({
|
|
149
|
+
apiKey,
|
|
150
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
151
|
+
});
|
|
152
|
+
return openrouter(modelId);
|
|
153
|
+
}
|
|
136
154
|
}
|
|
137
155
|
}
|
|
138
156
|
|
|
@@ -4,7 +4,7 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { cwd } from "node:process";
|
|
5
5
|
import { isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import { loadEnv } from "../../shared/env/load-env.js";
|
|
8
8
|
import {
|
|
9
9
|
getDefaultWorkflowFromModuleExports,
|
|
10
10
|
getWorkflowsFromModuleExports,
|
|
@@ -196,7 +196,7 @@ async function runIntegrationInternal(
|
|
|
196
196
|
const { logger } = options;
|
|
197
197
|
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
198
198
|
|
|
199
|
-
const envPath =
|
|
199
|
+
const envPath = loadEnv();
|
|
200
200
|
if (envPath) {
|
|
201
201
|
logger.info("loaded-env", { path: envPath });
|
|
202
202
|
}
|
|
@@ -269,6 +269,16 @@ async function runIntegrationInternal(
|
|
|
269
269
|
},
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
+
// tsx/esbuild injects __name() wrappers when keepNames is true. Playwright
|
|
273
|
+
// serializes callbacks via Function#toString() into the browser context which
|
|
274
|
+
// lacks __name, causing ReferenceError. Inject a no-op polyfill into every page.
|
|
275
|
+
await browserSession.context.addInitScript(() => {
|
|
276
|
+
(globalThis as Record<string, unknown>).__name = (
|
|
277
|
+
target: unknown,
|
|
278
|
+
value: string,
|
|
279
|
+
) => Object.defineProperty(target as object, "name", { value, configurable: true });
|
|
280
|
+
});
|
|
281
|
+
|
|
272
282
|
const workflowContext: LibrettoWorkflowContext = {
|
|
273
283
|
session: args.session,
|
|
274
284
|
page: browserSession.page,
|
|
@@ -58,7 +58,6 @@ export function isObfuscatedClass(cls: string): boolean {
|
|
|
58
58
|
if (cls.length > 80) return true;
|
|
59
59
|
if (/^_?[0-9a-f]{6,}$/i.test(cls)) return true;
|
|
60
60
|
if (/^[a-z]+_[0-9a-f]{4,}$/i.test(cls)) return true;
|
|
61
|
-
if (/^[a-z]{1,2}[0-9]{2,}$/i.test(cls)) return true;
|
|
62
61
|
|
|
63
62
|
const digits = (cls.match(/[0-9]/g) || []).length;
|
|
64
63
|
const letters = (cls.match(/[a-zA-Z]/g) || []).length;
|
|
@@ -1,69 +1,91 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { resolveLibrettoRepoRoot } from "../paths/repo-root.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
const REPO_ROOT = resolveLibrettoRepoRoot();
|
|
6
|
+
|
|
7
|
+
export function parseDotEnvAssignment(
|
|
8
|
+
line: string,
|
|
9
|
+
): { key: string; value: string } | null {
|
|
10
|
+
const trimmed = line.trim();
|
|
11
|
+
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
12
|
+
|
|
13
|
+
const withoutExport = trimmed.startsWith("export ")
|
|
14
|
+
? trimmed.slice("export ".length).trimStart()
|
|
15
|
+
: trimmed;
|
|
16
|
+
const eqIdx = withoutExport.indexOf("=");
|
|
17
|
+
if (eqIdx < 1) return null;
|
|
18
|
+
|
|
19
|
+
const key = withoutExport.slice(0, eqIdx).trim();
|
|
20
|
+
if (!key) return null;
|
|
21
|
+
|
|
22
|
+
const rawValue = withoutExport.slice(eqIdx + 1).trimStart();
|
|
23
|
+
if (!rawValue) {
|
|
24
|
+
return { key, value: "" };
|
|
16
25
|
}
|
|
17
|
-
}
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*/
|
|
24
|
-
function parseEnvFile(content: string): Record<string, string> {
|
|
25
|
-
const vars: Record<string, string> = {};
|
|
26
|
-
for (const raw of content.split("\n")) {
|
|
27
|
-
const line = raw.trim();
|
|
28
|
-
if (!line || line.startsWith("#")) continue;
|
|
29
|
-
const withoutExport = line.startsWith("export ")
|
|
30
|
-
? line.slice("export ".length).trimStart()
|
|
31
|
-
: line;
|
|
32
|
-
const eqIndex = withoutExport.indexOf("=");
|
|
33
|
-
if (eqIndex === -1) continue;
|
|
34
|
-
const key = withoutExport.slice(0, eqIndex).trim();
|
|
35
|
-
let value = withoutExport.slice(eqIndex + 1).trim();
|
|
36
|
-
// Strip matching quotes
|
|
37
|
-
if (
|
|
38
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
39
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
40
|
-
) {
|
|
41
|
-
value = value.slice(1, -1);
|
|
42
|
-
} else {
|
|
43
|
-
// Strip inline comments from unquoted values
|
|
44
|
-
const commentIndex = value.search(/\s#/);
|
|
45
|
-
if (commentIndex >= 0) {
|
|
46
|
-
value = value.slice(0, commentIndex).trimEnd();
|
|
47
|
-
}
|
|
27
|
+
if (rawValue.startsWith('"')) {
|
|
28
|
+
const closeIdx = rawValue.indexOf('"', 1);
|
|
29
|
+
if (closeIdx > 0) {
|
|
30
|
+
return { key, value: rawValue.slice(1, closeIdx) };
|
|
48
31
|
}
|
|
49
|
-
|
|
32
|
+
return { key, value: rawValue.slice(1) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (rawValue.startsWith("'")) {
|
|
36
|
+
const closeIdx = rawValue.indexOf("'", 1);
|
|
37
|
+
if (closeIdx > 0) {
|
|
38
|
+
return { key, value: rawValue.slice(1, closeIdx) };
|
|
39
|
+
}
|
|
40
|
+
return { key, value: rawValue.slice(1) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const inlineCommentIndex = rawValue.search(/\s#/);
|
|
44
|
+
const value =
|
|
45
|
+
inlineCommentIndex >= 0
|
|
46
|
+
? rawValue.slice(0, inlineCommentIndex).trimEnd()
|
|
47
|
+
: rawValue.trim();
|
|
48
|
+
return { key, value };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readWorktreeEnvPath(): string | null {
|
|
52
|
+
const gitPath = join(REPO_ROOT, ".git");
|
|
53
|
+
if (!existsSync(gitPath)) return null;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const gitPointer = readFileSync(gitPath, "utf-8").trim();
|
|
57
|
+
const match = gitPointer.match(/^gitdir:\s*(.+)$/i);
|
|
58
|
+
if (!match?.[1]) return null;
|
|
59
|
+
const worktreeGitDir = resolve(REPO_ROOT, match[1].trim());
|
|
60
|
+
const commonGitDir = resolve(worktreeGitDir, "..", "..");
|
|
61
|
+
return join(dirname(commonGitDir), ".env");
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
50
64
|
}
|
|
51
|
-
return vars;
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
/**
|
|
55
|
-
* Load the
|
|
56
|
-
* Existing
|
|
68
|
+
* Load the `.env` file at the repository root into `process.env`.
|
|
69
|
+
* Existing values are never overridden.
|
|
70
|
+
* Respects `LIBRETTO_DISABLE_DOTENV=1` to skip loading entirely.
|
|
57
71
|
* Returns the path of the loaded `.env`, or `null` if none was found.
|
|
58
72
|
*/
|
|
59
|
-
export function
|
|
60
|
-
|
|
73
|
+
export function loadEnv(): string | null {
|
|
74
|
+
if (process.env.LIBRETTO_DISABLE_DOTENV?.trim() === "1") return null;
|
|
75
|
+
|
|
76
|
+
const envPathCandidates = [
|
|
77
|
+
join(REPO_ROOT, ".env"),
|
|
78
|
+
readWorktreeEnvPath(),
|
|
79
|
+
].filter((value): value is string => Boolean(value));
|
|
80
|
+
|
|
81
|
+
const envPath = envPathCandidates.find((candidate) => existsSync(candidate));
|
|
61
82
|
if (!envPath) return null;
|
|
62
83
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
84
|
+
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
85
|
+
const parsed = parseDotEnvAssignment(line);
|
|
86
|
+
if (!parsed) continue;
|
|
87
|
+
if (!(parsed.key in process.env)) {
|
|
88
|
+
process.env[parsed.key] = parsed.value;
|
|
67
89
|
}
|
|
68
90
|
}
|
|
69
91
|
return envPath;
|