libretto 0.6.9 → 0.6.11
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/dist/cli/cli.js +2 -0
- package/dist/cli/commands/auth.js +535 -0
- package/dist/cli/commands/billing.js +74 -0
- package/dist/cli/commands/browser.js +8 -3
- package/dist/cli/commands/deploy.js +2 -7
- package/dist/cli/commands/execution.js +99 -136
- package/dist/cli/commands/snapshot.js +38 -126
- package/dist/cli/core/ai-model.js +0 -3
- package/dist/cli/core/auth-fetch.js +195 -0
- package/dist/cli/core/auth-storage.js +52 -0
- package/dist/cli/core/browser.js +128 -202
- package/dist/cli/core/daemon/config.js +6 -0
- package/dist/cli/core/daemon/daemon.js +298 -0
- package/dist/cli/core/daemon/exec.js +86 -0
- package/dist/cli/core/daemon/index.js +16 -0
- package/dist/cli/core/daemon/ipc.js +171 -0
- package/dist/cli/core/daemon/pages.js +15 -0
- package/dist/cli/core/daemon/snapshot.js +86 -0
- package/dist/cli/core/daemon/spawn.js +90 -0
- package/dist/cli/core/exec-compiler.js +111 -0
- package/dist/cli/core/prompt.js +72 -0
- package/dist/cli/core/providers/libretto-cloud.js +2 -6
- package/dist/cli/core/readonly-exec.js +1 -1
- package/dist/cli/router.js +4 -0
- package/dist/cli/workers/run-integration-runtime.js +0 -5
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +2 -1
- package/docs/browser-automation-approaches.md +435 -0
- package/docs/releasing.md +117 -0
- package/package.json +4 -3
- package/skills/libretto/SKILL.md +14 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +2 -0
- package/src/cli/commands/auth.ts +787 -0
- package/src/cli/commands/billing.ts +133 -0
- package/src/cli/commands/browser.ts +8 -2
- package/src/cli/commands/deploy.ts +2 -7
- package/src/cli/commands/execution.ts +126 -186
- package/src/cli/commands/snapshot.ts +46 -143
- package/src/cli/core/ai-model.ts +4 -5
- package/src/cli/core/auth-fetch.ts +283 -0
- package/src/cli/core/auth-storage.ts +102 -0
- package/src/cli/core/browser.ts +159 -242
- package/src/cli/core/daemon/config.ts +46 -0
- package/src/cli/core/daemon/daemon.ts +429 -0
- package/src/cli/core/daemon/exec.ts +128 -0
- package/src/cli/core/daemon/index.ts +24 -0
- package/src/cli/core/daemon/ipc.ts +294 -0
- package/src/cli/core/daemon/pages.ts +21 -0
- package/src/cli/core/daemon/snapshot.ts +114 -0
- package/src/cli/core/daemon/spawn.ts +171 -0
- package/src/cli/core/exec-compiler.ts +169 -0
- package/src/cli/core/prompt.ts +94 -0
- package/src/cli/core/providers/libretto-cloud.ts +2 -6
- package/src/cli/core/readonly-exec.ts +2 -1
- package/src/cli/router.ts +4 -0
- package/src/cli/workers/run-integration-runtime.ts +0 -6
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/browser-daemon.js +0 -122
- package/src/cli/core/browser-daemon.ts +0 -198
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { readAuthState, writeAuthState } from "./auth-storage.js";
|
|
2
|
+
const HOSTED_API_URL = "https://api.libretto.sh";
|
|
3
|
+
const NOT_AUTHENTICATED_MESSAGE = [
|
|
4
|
+
"Not authenticated.",
|
|
5
|
+
" \u2022 Cookie expired or never set: run `libretto experimental auth login` to refresh it.",
|
|
6
|
+
" \u2022 Or set LIBRETTO_API_KEY in your .env (issue one with `libretto experimental auth api-key issue --label <label>` after logging in)."
|
|
7
|
+
].join("\n");
|
|
8
|
+
function pickCredential(state) {
|
|
9
|
+
const envKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
10
|
+
if (envKey) return { source: "env-api-key", apiKey: envKey };
|
|
11
|
+
if (state?.session?.cookie) {
|
|
12
|
+
return { source: "cookie", cookie: state.session.cookie };
|
|
13
|
+
}
|
|
14
|
+
return { source: "none" };
|
|
15
|
+
}
|
|
16
|
+
function resolveApiUrl(_state) {
|
|
17
|
+
return HOSTED_API_URL;
|
|
18
|
+
}
|
|
19
|
+
async function authFetch(options) {
|
|
20
|
+
const headers = {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
// Better Auth's CSRF middleware rejects state-changing requests
|
|
23
|
+
// ("/api/auth/*" POSTs like api-key/create, organization/invite-member,
|
|
24
|
+
// sign-in/email, etc.) when there's no Origin header. Browsers send
|
|
25
|
+
// this automatically; node:fetch does not. Sending the apiUrl as the
|
|
26
|
+
// Origin matches Better Auth's trustedOrigins default (which includes
|
|
27
|
+
// baseURL), so the check passes for our own service.
|
|
28
|
+
Origin: options.apiUrl
|
|
29
|
+
};
|
|
30
|
+
if (!options.unauthenticated) {
|
|
31
|
+
const credential = options.credential ?? pickCredential(await readAuthState());
|
|
32
|
+
if (credential.source === "env-api-key") {
|
|
33
|
+
headers["x-api-key"] = credential.apiKey;
|
|
34
|
+
} else if (credential.source === "cookie") {
|
|
35
|
+
headers["cookie"] = credential.cookie;
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error(NOT_AUTHENTICATED_MESSAGE);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const response = await fetch(`${options.apiUrl}${options.path}`, {
|
|
41
|
+
method: options.method ?? "POST",
|
|
42
|
+
headers,
|
|
43
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
44
|
+
});
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
class ApiCallError extends Error {
|
|
48
|
+
status;
|
|
49
|
+
code;
|
|
50
|
+
data;
|
|
51
|
+
path;
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
super(opts.message);
|
|
54
|
+
this.name = "ApiCallError";
|
|
55
|
+
this.status = opts.status;
|
|
56
|
+
this.code = opts.code;
|
|
57
|
+
this.data = opts.data;
|
|
58
|
+
this.path = opts.path;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function orpcCall(opts) {
|
|
62
|
+
const response = await authFetch({
|
|
63
|
+
apiUrl: opts.apiUrl,
|
|
64
|
+
method: "POST",
|
|
65
|
+
path: opts.path,
|
|
66
|
+
body: { json: opts.input ?? {} },
|
|
67
|
+
unauthenticated: opts.unauthenticated,
|
|
68
|
+
credential: opts.credential
|
|
69
|
+
});
|
|
70
|
+
const text = await response.text();
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = text.length === 0 ? {} : JSON.parse(text);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new ApiCallError({
|
|
76
|
+
message: `Unexpected non-JSON response from ${opts.path} (${response.status}): ${text.slice(0, 200)}`,
|
|
77
|
+
status: response.status,
|
|
78
|
+
code: null,
|
|
79
|
+
data: null,
|
|
80
|
+
path: opts.path
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const message = extractErrorMessage(parsed) ?? `${opts.path} failed (${response.status})`;
|
|
85
|
+
throw new ApiCallError({
|
|
86
|
+
message,
|
|
87
|
+
status: response.status,
|
|
88
|
+
code: extractErrorCode(parsed),
|
|
89
|
+
data: extractErrorData(parsed),
|
|
90
|
+
path: opts.path
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const json = parsed.json;
|
|
94
|
+
if (json === void 0) {
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
return json;
|
|
98
|
+
}
|
|
99
|
+
async function betterAuthCall(opts) {
|
|
100
|
+
const response = await authFetch({
|
|
101
|
+
apiUrl: opts.apiUrl,
|
|
102
|
+
method: opts.method ?? "POST",
|
|
103
|
+
path: opts.path,
|
|
104
|
+
body: opts.input,
|
|
105
|
+
unauthenticated: opts.unauthenticated,
|
|
106
|
+
credential: opts.credential
|
|
107
|
+
});
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = text.length === 0 ? {} : JSON.parse(text);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Unexpected non-JSON response from ${opts.path} (${response.status}): ${text.slice(0, 200)}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const message = extractErrorMessage(parsed) ?? `${opts.path} failed (${response.status})`;
|
|
119
|
+
throw new ApiCallError({
|
|
120
|
+
message,
|
|
121
|
+
status: response.status,
|
|
122
|
+
code: extractErrorCode(parsed),
|
|
123
|
+
data: extractErrorData(parsed),
|
|
124
|
+
path: opts.path
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const setCookie = readSetCookies(response);
|
|
128
|
+
return { data: parsed, setCookie };
|
|
129
|
+
}
|
|
130
|
+
function readSetCookies(response) {
|
|
131
|
+
const headers = response.headers;
|
|
132
|
+
if (typeof headers.getSetCookie === "function") {
|
|
133
|
+
return headers.getSetCookie();
|
|
134
|
+
}
|
|
135
|
+
const single = response.headers.get("set-cookie");
|
|
136
|
+
return single ? [single] : [];
|
|
137
|
+
}
|
|
138
|
+
function extractErrorMessage(body) {
|
|
139
|
+
if (!body || typeof body !== "object") return null;
|
|
140
|
+
const record = body;
|
|
141
|
+
if (typeof record.message === "string") return record.message;
|
|
142
|
+
if (record.json && typeof record.json === "object" && typeof record.json.message === "string") {
|
|
143
|
+
return record.json.message;
|
|
144
|
+
}
|
|
145
|
+
if (record.error && typeof record.error === "object") {
|
|
146
|
+
const errMsg = record.error.message;
|
|
147
|
+
if (typeof errMsg === "string") return errMsg;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function extractErrorCode(body) {
|
|
152
|
+
if (!body || typeof body !== "object") return null;
|
|
153
|
+
const record = body;
|
|
154
|
+
if (typeof record.code === "string") return record.code;
|
|
155
|
+
if (record.json && typeof record.json === "object" && typeof record.json.code === "string") {
|
|
156
|
+
return record.json.code;
|
|
157
|
+
}
|
|
158
|
+
if (record.error && typeof record.error === "object") {
|
|
159
|
+
const code = record.error.code;
|
|
160
|
+
if (typeof code === "string") return code;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
function extractErrorData(body) {
|
|
165
|
+
if (!body || typeof body !== "object") return null;
|
|
166
|
+
const record = body;
|
|
167
|
+
if (record.data !== void 0) return record.data;
|
|
168
|
+
if (record.json && typeof record.json === "object") {
|
|
169
|
+
const inner = record.json.data;
|
|
170
|
+
if (inner !== void 0) return inner;
|
|
171
|
+
}
|
|
172
|
+
if (record.error && typeof record.error === "object") {
|
|
173
|
+
const inner = record.error.data;
|
|
174
|
+
if (inner !== void 0) return inner;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
async function ensureAuthState(apiUrl) {
|
|
179
|
+
const existing = await readAuthState();
|
|
180
|
+
if (existing && existing.apiUrl === apiUrl) return existing;
|
|
181
|
+
const next = existing ? { ...existing, apiUrl } : { apiUrl, session: null };
|
|
182
|
+
await writeAuthState(next);
|
|
183
|
+
return next;
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
ApiCallError,
|
|
187
|
+
HOSTED_API_URL,
|
|
188
|
+
NOT_AUTHENTICATED_MESSAGE,
|
|
189
|
+
authFetch,
|
|
190
|
+
betterAuthCall,
|
|
191
|
+
ensureAuthState,
|
|
192
|
+
orpcCall,
|
|
193
|
+
pickCredential,
|
|
194
|
+
resolveApiUrl
|
|
195
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const FILE_NAME = "auth.json";
|
|
5
|
+
function authDir() {
|
|
6
|
+
return join(homedir(), ".libretto");
|
|
7
|
+
}
|
|
8
|
+
function authPath() {
|
|
9
|
+
return join(authDir(), FILE_NAME);
|
|
10
|
+
}
|
|
11
|
+
async function readAuthState() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await fs.readFile(authPath(), "utf8");
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (!parsed.apiUrl || typeof parsed.apiUrl !== "string") return null;
|
|
16
|
+
return {
|
|
17
|
+
apiUrl: parsed.apiUrl,
|
|
18
|
+
session: parsed.session ?? null
|
|
19
|
+
};
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === "ENOENT") return null;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function writeAuthState(state) {
|
|
26
|
+
await fs.mkdir(authDir(), { recursive: true, mode: 448 });
|
|
27
|
+
const payload = JSON.stringify(state, null, 2);
|
|
28
|
+
const target = authPath();
|
|
29
|
+
const tmp = `${target}.tmp`;
|
|
30
|
+
await fs.writeFile(tmp, payload, { mode: 384 });
|
|
31
|
+
await fs.rename(tmp, target);
|
|
32
|
+
}
|
|
33
|
+
async function clearAuthState() {
|
|
34
|
+
try {
|
|
35
|
+
await fs.unlink(authPath());
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code !== "ENOENT") throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function setCookieToCookieHeader(setCookie) {
|
|
41
|
+
return setCookie.map((entry) => entry.split(";")[0]?.trim()).filter((pair) => Boolean(pair && pair.includes("="))).join("; ");
|
|
42
|
+
}
|
|
43
|
+
function authStatePath() {
|
|
44
|
+
return authPath();
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
authStatePath,
|
|
48
|
+
clearAuthState,
|
|
49
|
+
readAuthState,
|
|
50
|
+
setCookieToCookieHeader,
|
|
51
|
+
writeAuthState
|
|
52
|
+
};
|