libretto 0.6.8 → 0.6.10

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 (65) hide show
  1. package/dist/cli/cli.js +2 -0
  2. package/dist/cli/commands/auth.js +535 -0
  3. package/dist/cli/commands/billing.js +74 -0
  4. package/dist/cli/commands/browser.js +8 -3
  5. package/dist/cli/commands/deploy.js +2 -7
  6. package/dist/cli/commands/execution.js +112 -137
  7. package/dist/cli/commands/snapshot.js +38 -126
  8. package/dist/cli/core/ai-model.js +0 -3
  9. package/dist/cli/core/auth-fetch.js +195 -0
  10. package/dist/cli/core/auth-storage.js +52 -0
  11. package/dist/cli/core/browser.js +151 -206
  12. package/dist/cli/core/daemon/config.js +6 -0
  13. package/dist/cli/core/daemon/daemon.js +298 -0
  14. package/dist/cli/core/daemon/exec.js +86 -0
  15. package/dist/cli/core/daemon/index.js +16 -0
  16. package/dist/cli/core/daemon/ipc.js +171 -0
  17. package/dist/cli/core/daemon/pages.js +15 -0
  18. package/dist/cli/core/daemon/snapshot.js +86 -0
  19. package/dist/cli/core/daemon/spawn.js +90 -0
  20. package/dist/cli/core/exec-compiler.js +111 -0
  21. package/dist/cli/core/prompt.js +72 -0
  22. package/dist/cli/core/providers/browserbase.js +1 -0
  23. package/dist/cli/core/providers/kernel.js +1 -0
  24. package/dist/cli/core/providers/libretto-cloud.js +6 -7
  25. package/dist/cli/core/readonly-exec.js +1 -1
  26. package/dist/cli/router.js +4 -0
  27. package/dist/cli/workers/run-integration-runtime.js +0 -5
  28. package/dist/shared/state/session-state.d.ts +1 -0
  29. package/dist/shared/state/session-state.js +2 -1
  30. package/docs/browser-automation-approaches.md +435 -0
  31. package/docs/releasing.md +117 -0
  32. package/package.json +4 -3
  33. package/skills/libretto/SKILL.md +14 -1
  34. package/skills/libretto-readonly/SKILL.md +1 -1
  35. package/src/cli/cli.ts +2 -0
  36. package/src/cli/commands/auth.ts +787 -0
  37. package/src/cli/commands/billing.ts +133 -0
  38. package/src/cli/commands/browser.ts +8 -2
  39. package/src/cli/commands/deploy.ts +2 -7
  40. package/src/cli/commands/execution.ts +139 -187
  41. package/src/cli/commands/snapshot.ts +46 -143
  42. package/src/cli/core/ai-model.ts +4 -5
  43. package/src/cli/core/auth-fetch.ts +283 -0
  44. package/src/cli/core/auth-storage.ts +102 -0
  45. package/src/cli/core/browser.ts +182 -245
  46. package/src/cli/core/daemon/config.ts +46 -0
  47. package/src/cli/core/daemon/daemon.ts +429 -0
  48. package/src/cli/core/daemon/exec.ts +128 -0
  49. package/src/cli/core/daemon/index.ts +24 -0
  50. package/src/cli/core/daemon/ipc.ts +294 -0
  51. package/src/cli/core/daemon/pages.ts +21 -0
  52. package/src/cli/core/daemon/snapshot.ts +114 -0
  53. package/src/cli/core/daemon/spawn.ts +171 -0
  54. package/src/cli/core/exec-compiler.ts +169 -0
  55. package/src/cli/core/prompt.ts +94 -0
  56. package/src/cli/core/providers/browserbase.ts +1 -0
  57. package/src/cli/core/providers/kernel.ts +1 -0
  58. package/src/cli/core/providers/libretto-cloud.ts +13 -7
  59. package/src/cli/core/providers/types.ts +12 -1
  60. package/src/cli/core/readonly-exec.ts +2 -1
  61. package/src/cli/router.ts +4 -0
  62. package/src/cli/workers/run-integration-runtime.ts +0 -6
  63. package/src/shared/state/session-state.ts +1 -0
  64. package/dist/cli/core/browser-daemon.js +0 -122
  65. 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
+ };