libretto 0.6.24 → 0.6.26

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 (63) hide show
  1. package/README.md +9 -1
  2. package/README.template.md +9 -1
  3. package/dist/cli/commands/browser.js +17 -10
  4. package/dist/cli/commands/cloud-credentials.js +70 -0
  5. package/dist/cli/commands/deploy.js +24 -2
  6. package/dist/cli/commands/execution.js +9 -30
  7. package/dist/cli/commands/import-chrome-profiles.js +46 -0
  8. package/dist/cli/commands/profiles.js +71 -0
  9. package/dist/cli/commands/shared.js +1 -3
  10. package/dist/cli/core/browser.js +89 -75
  11. package/dist/cli/core/daemon/daemon.js +47 -35
  12. package/dist/cli/core/daemon/ipc.js +3 -0
  13. package/dist/cli/core/deploy-artifact.js +85 -22
  14. package/dist/cli/core/profiles.js +47 -0
  15. package/dist/cli/core/prompt.js +9 -0
  16. package/dist/cli/core/providers/libretto-cloud.js +6 -2
  17. package/dist/cli/core/session-logs.js +325 -0
  18. package/dist/cli/core/telemetry.js +110 -311
  19. package/dist/cli/core/workflow-runner/runner.js +65 -0
  20. package/dist/cli/router.js +9 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +12 -0
  23. package/dist/shared/workflow/auth-profile-name.d.ts +3 -0
  24. package/dist/shared/workflow/auth-profile-name.js +29 -0
  25. package/dist/shared/workflow/auth-profile-state.d.ts +20 -0
  26. package/dist/shared/workflow/auth-profile-state.js +105 -0
  27. package/dist/shared/workflow/authenticate.d.ts +17 -0
  28. package/dist/shared/workflow/authenticate.js +37 -0
  29. package/dist/shared/workflow/credentials.d.ts +5 -0
  30. package/dist/shared/workflow/credentials.js +68 -0
  31. package/dist/shared/workflow/workflow.d.ts +16 -1
  32. package/dist/shared/workflow/workflow.js +56 -4
  33. package/package.json +1 -1
  34. package/skills/libretto/SKILL.md +3 -4
  35. package/skills/libretto/references/auth-profiles.md +61 -11
  36. package/skills/libretto/references/code-generation-rules.md +31 -1
  37. package/skills/libretto-readonly/SKILL.md +1 -1
  38. package/src/cli/commands/browser.ts +19 -11
  39. package/src/cli/commands/cloud-credentials.ts +82 -0
  40. package/src/cli/commands/deploy.ts +41 -2
  41. package/src/cli/commands/execution.ts +10 -31
  42. package/src/cli/commands/import-chrome-profiles.ts +46 -0
  43. package/src/cli/commands/profiles.ts +90 -0
  44. package/src/cli/commands/shared.ts +4 -8
  45. package/src/cli/core/browser.ts +102 -91
  46. package/src/cli/core/daemon/config.ts +4 -1
  47. package/src/cli/core/daemon/daemon.ts +52 -44
  48. package/src/cli/core/daemon/ipc.ts +15 -0
  49. package/src/cli/core/deploy-artifact.ts +131 -32
  50. package/src/cli/core/profiles.ts +53 -0
  51. package/src/cli/core/prompt.ts +15 -0
  52. package/src/cli/core/providers/libretto-cloud.ts +6 -2
  53. package/src/cli/core/providers/types.ts +4 -1
  54. package/src/cli/core/session-logs.ts +445 -0
  55. package/src/cli/core/telemetry.ts +142 -413
  56. package/src/cli/core/workflow-runner/runner.ts +86 -1
  57. package/src/cli/router.ts +8 -0
  58. package/src/index.ts +10 -0
  59. package/src/shared/workflow/auth-profile-name.ts +27 -0
  60. package/src/shared/workflow/auth-profile-state.ts +144 -0
  61. package/src/shared/workflow/authenticate.ts +63 -0
  62. package/src/shared/workflow/credentials.ts +91 -0
  63. package/src/shared/workflow/workflow.ts +89 -4
@@ -0,0 +1,47 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { PROFILES_DIR } from "./context.js";
5
+ import { normalizeProfileName } from "../../shared/workflow/auth-profile-name.js";
6
+ import { normalizeProfileName as normalizeProfileName2 } from "../../shared/workflow/auth-profile-name.js";
7
+ function getProfilePath(profileName) {
8
+ return join(PROFILES_DIR, `${normalizeProfileName(profileName)}.json`);
9
+ }
10
+ function hasProfile(profileName) {
11
+ return existsSync(getProfilePath(profileName));
12
+ }
13
+ function readProfile(profileName) {
14
+ const profilePath = getProfilePath(profileName);
15
+ const parsed = JSON.parse(readFileSync(profilePath, "utf8"));
16
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
17
+ throw new Error(`Saved auth profile "${profileName}" is not a JSON object.`);
18
+ }
19
+ return parsed;
20
+ }
21
+ async function writeProfile(profileName, profile) {
22
+ const profilePath = getProfilePath(profileName);
23
+ await mkdir(dirname(profilePath), { recursive: true });
24
+ await writeFile(profilePath, JSON.stringify(profile, null, 2), "utf8");
25
+ return profilePath;
26
+ }
27
+ function formatMissingLocalAuthProfileMessage(args) {
28
+ return [
29
+ `Local auth profile not found: "${args.profileName}".`,
30
+ `Expected profile file: ${args.profilePath}`,
31
+ "To create it locally:",
32
+ ` 1. libretto open <site-url> --headed --session ${args.session}`,
33
+ " 2. Log in manually in the browser window.",
34
+ ` 3. libretto save ${args.profileName} --session ${args.session} --sites <site>`,
35
+ "Or import site-scoped state from Chrome with:",
36
+ ` libretto import-chrome-profiles ${args.profileName} --cdp-url <url> --sites <site>`,
37
+ "Local profile files are not uploaded to cloud profiles."
38
+ ].join("\n");
39
+ }
40
+ export {
41
+ formatMissingLocalAuthProfileMessage,
42
+ getProfilePath,
43
+ hasProfile,
44
+ normalizeProfileName2 as normalizeProfileName,
45
+ readProfile,
46
+ writeProfile
47
+ };
@@ -13,6 +13,14 @@ async function prompt(question, opts = {}) {
13
13
  rl.close();
14
14
  }
15
15
  }
16
+ async function promptConfirm(question, opts = {}) {
17
+ const defaultValue = opts.defaultValue ?? false;
18
+ if (!stdin.isTTY) return defaultValue;
19
+ const suffix = defaultValue ? "[Y/n]" : "[y/N]";
20
+ const answer = (await prompt(`${question} ${suffix}`)).trim().toLowerCase();
21
+ if (answer.length === 0) return defaultValue;
22
+ return answer === "y" || answer === "yes";
23
+ }
16
24
  const CTRL_C = "";
17
25
  const CR = "\r";
18
26
  const LF = "\n";
@@ -67,6 +75,7 @@ function slugify(name) {
67
75
  }
68
76
  export {
69
77
  prompt,
78
+ promptConfirm,
70
79
  promptPassword,
71
80
  slugify
72
81
  };
@@ -10,7 +10,7 @@ function createLibrettoCloudProvider() {
10
10
  );
11
11
  const endpoint = resolveHostedApiUrl();
12
12
  return {
13
- async createSession() {
13
+ async createSession(options) {
14
14
  const browserSessionTimeoutSeconds = readPositiveNumberEnv(
15
15
  "LIBRETTO_TIMEOUT_SECONDS",
16
16
  DEFAULT_BROWSER_SESSION_TIMEOUT_SECONDS
@@ -22,7 +22,11 @@ function createLibrettoCloudProvider() {
22
22
  "Content-Type": "application/json"
23
23
  },
24
24
  body: JSON.stringify({
25
- json: { timeout_seconds: browserSessionTimeoutSeconds }
25
+ json: {
26
+ timeout_seconds: browserSessionTimeoutSeconds,
27
+ profile_name: options?.authProfileName,
28
+ profile_persist: options?.authProfilePersist
29
+ }
26
30
  })
27
31
  });
28
32
  if (!resp.ok) {
@@ -0,0 +1,325 @@
1
+ import {
2
+ appendFileSync,
3
+ existsSync,
4
+ readFileSync
5
+ } from "node:fs";
6
+ import {
7
+ getSessionActionsLogPath,
8
+ getSessionNetworkLogPath
9
+ } from "./context.js";
10
+ import { assertSessionStateExistsOrThrow } from "./session.js";
11
+ function readNetworkLog(session, opts = {}) {
12
+ assertSessionStateExistsOrThrow(session);
13
+ const logPath = getSessionNetworkLogPath(session);
14
+ if (!existsSync(logPath)) return [];
15
+ const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
16
+ let entries = lines.map(
17
+ (line) => JSON.parse(line)
18
+ );
19
+ if (opts.method) {
20
+ const m = opts.method.toUpperCase();
21
+ entries = entries.filter((e) => e.method === m);
22
+ }
23
+ if (opts.filter) {
24
+ const re = new RegExp(opts.filter, "i");
25
+ entries = entries.filter((e) => re.test(e.url));
26
+ }
27
+ if (opts.pageId) {
28
+ entries = entries.filter((e) => e.pageId === opts.pageId);
29
+ }
30
+ const last = opts.last ?? 20;
31
+ if (entries.length > last) {
32
+ entries = entries.slice(-last);
33
+ }
34
+ return entries;
35
+ }
36
+ function parentLogAction(session, entry) {
37
+ try {
38
+ const record = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
39
+ appendFileSync(
40
+ getSessionActionsLogPath(session),
41
+ JSON.stringify(record) + "\n"
42
+ );
43
+ } catch {
44
+ }
45
+ }
46
+ function readActionLog(session, opts = {}) {
47
+ assertSessionStateExistsOrThrow(session);
48
+ const logPath = getSessionActionsLogPath(session);
49
+ if (!existsSync(logPath)) return [];
50
+ const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
51
+ let entries = lines.map(
52
+ (line) => JSON.parse(line)
53
+ );
54
+ if (opts.action) {
55
+ const a = opts.action.toLowerCase();
56
+ entries = entries.filter((e) => e.action === a);
57
+ }
58
+ if (opts.source) {
59
+ const s = opts.source.toLowerCase();
60
+ entries = entries.filter((e) => e.source === s);
61
+ }
62
+ if (opts.filter) {
63
+ const re = new RegExp(opts.filter, "i");
64
+ entries = entries.filter(
65
+ (e) => re.test(e.action) || re.test(e.selector || "") || re.test(e.bestSemanticSelector || "") || re.test(e.targetSelector || "") || re.test((e.ancestorSelectors || []).join(" ")) || re.test(e.nearbyText || "") || re.test((e.composedPath || []).join(" ")) || re.test(e.value || "") || re.test(e.url || "")
66
+ );
67
+ }
68
+ if (opts.pageId) {
69
+ entries = entries.filter((e) => e.pageId === opts.pageId);
70
+ }
71
+ const last = opts.last ?? 20;
72
+ if (entries.length > last) {
73
+ entries = entries.slice(-last);
74
+ }
75
+ return entries;
76
+ }
77
+ const LOCATOR_ACTION_METHODS = [
78
+ "click",
79
+ "dblclick",
80
+ "fill",
81
+ "type",
82
+ "press",
83
+ "check",
84
+ "uncheck",
85
+ "selectOption",
86
+ "hover",
87
+ "focus",
88
+ "scrollIntoViewIfNeeded",
89
+ "waitFor",
90
+ "innerHTML",
91
+ "innerText",
92
+ "textContent",
93
+ "inputValue",
94
+ "isChecked",
95
+ "isDisabled",
96
+ "isEditable",
97
+ "isEnabled",
98
+ "isHidden",
99
+ "isVisible",
100
+ "count",
101
+ "boundingBox",
102
+ "screenshot",
103
+ "evaluate",
104
+ "evaluateAll",
105
+ "evaluateHandle",
106
+ "getAttribute",
107
+ "dispatchEvent",
108
+ "setInputFiles",
109
+ "selectText",
110
+ "dragTo",
111
+ "highlight",
112
+ "tap"
113
+ ];
114
+ const LOCATOR_RETURNING_METHODS = [
115
+ "first",
116
+ "last",
117
+ "locator",
118
+ "getByRole",
119
+ "getByText",
120
+ "getByLabel",
121
+ "getByPlaceholder",
122
+ "getByAltText",
123
+ "getByTitle",
124
+ "getByTestId",
125
+ "filter",
126
+ "and",
127
+ "or"
128
+ ];
129
+ function formatHint(method, args) {
130
+ const formatted = args.map((a) => JSON.stringify(a)).join(", ");
131
+ return `${method}(${formatted})`;
132
+ }
133
+ function wrapLocator(locator, hint, session, page, pageId, onActivity) {
134
+ if (locator.__librettoActionLogged) return locator;
135
+ locator.__librettoActionLogged = true;
136
+ for (const actMethod of LOCATOR_ACTION_METHODS) {
137
+ if (typeof locator[actMethod] !== "function") continue;
138
+ const origAct = locator[actMethod].bind(locator);
139
+ locator[actMethod] = async (...actArgs) => {
140
+ const start = Date.now();
141
+ try {
142
+ await page.evaluate(() => {
143
+ window.__btApiActionInProgress = true;
144
+ });
145
+ } catch {
146
+ }
147
+ try {
148
+ const result = await origAct(...actArgs);
149
+ parentLogAction(session, {
150
+ pageId,
151
+ action: actMethod,
152
+ source: "agent",
153
+ selector: hint,
154
+ value: actArgs[0] !== void 0 ? String(actArgs[0]).slice(0, 100) : void 0,
155
+ duration: Date.now() - start,
156
+ success: true
157
+ });
158
+ onActivity?.();
159
+ return result;
160
+ } catch (err) {
161
+ parentLogAction(session, {
162
+ pageId,
163
+ action: actMethod,
164
+ source: "agent",
165
+ selector: hint,
166
+ duration: Date.now() - start,
167
+ success: false,
168
+ error: err.message
169
+ });
170
+ onActivity?.();
171
+ throw err;
172
+ } finally {
173
+ try {
174
+ await page.evaluate(() => {
175
+ window.__btApiActionInProgress = false;
176
+ });
177
+ } catch {
178
+ }
179
+ }
180
+ };
181
+ }
182
+ for (const method of LOCATOR_RETURNING_METHODS) {
183
+ if (typeof locator[method] !== "function") continue;
184
+ const origMethod = locator[method].bind(locator);
185
+ locator[method] = (...args) => {
186
+ const child = origMethod(...args);
187
+ const childHint = args.length > 0 ? `${hint}.${formatHint(method, args)}` : `${hint}.${method}()`;
188
+ return wrapLocator(child, childHint, session, page, pageId, onActivity);
189
+ };
190
+ }
191
+ if (typeof locator.nth === "function") {
192
+ const origNth = locator.nth.bind(locator);
193
+ locator.nth = (index) => {
194
+ const child = origNth(index);
195
+ const childHint = `${hint}.nth(${index})`;
196
+ return wrapLocator(child, childHint, session, page, pageId, onActivity);
197
+ };
198
+ }
199
+ if (typeof locator.all === "function") {
200
+ const origAll = locator.all.bind(locator);
201
+ locator.all = async () => {
202
+ const items = await origAll();
203
+ return items.map((item, i) => {
204
+ const childHint = `${hint}.all()[${i}]`;
205
+ return wrapLocator(item, childHint, session, page, pageId, onActivity);
206
+ });
207
+ };
208
+ }
209
+ return locator;
210
+ }
211
+ function wrapPageForActionLogging(page, session, pageId, onActivity) {
212
+ const PAGE_ACTIONS = [
213
+ "click",
214
+ "dblclick",
215
+ "fill",
216
+ "type",
217
+ "press",
218
+ "check",
219
+ "uncheck",
220
+ "selectOption",
221
+ "hover",
222
+ "focus"
223
+ ];
224
+ const NAV_ACTIONS = ["goto", "reload", "goBack", "goForward"];
225
+ for (const method of PAGE_ACTIONS) {
226
+ const orig = page[method].bind(page);
227
+ page[method] = async (...args) => {
228
+ const start = Date.now();
229
+ try {
230
+ await page.evaluate(() => {
231
+ window.__btApiActionInProgress = true;
232
+ });
233
+ } catch {
234
+ }
235
+ try {
236
+ const result = await orig(...args);
237
+ parentLogAction(session, {
238
+ pageId,
239
+ action: method,
240
+ source: "agent",
241
+ selector: typeof args[0] === "string" ? args[0] : void 0,
242
+ value: args[1] !== void 0 ? String(args[1]).slice(0, 100) : void 0,
243
+ duration: Date.now() - start,
244
+ success: true
245
+ });
246
+ onActivity?.();
247
+ return result;
248
+ } catch (err) {
249
+ parentLogAction(session, {
250
+ pageId,
251
+ action: method,
252
+ source: "agent",
253
+ selector: typeof args[0] === "string" ? args[0] : void 0,
254
+ duration: Date.now() - start,
255
+ success: false,
256
+ error: err.message
257
+ });
258
+ onActivity?.();
259
+ throw err;
260
+ } finally {
261
+ try {
262
+ await page.evaluate(() => {
263
+ window.__btApiActionInProgress = false;
264
+ });
265
+ } catch {
266
+ }
267
+ }
268
+ };
269
+ }
270
+ for (const method of NAV_ACTIONS) {
271
+ const orig = page[method].bind(page);
272
+ page[method] = async (...args) => {
273
+ const start = Date.now();
274
+ try {
275
+ const result = await orig(...args);
276
+ parentLogAction(session, {
277
+ pageId,
278
+ action: method,
279
+ source: "agent",
280
+ url: typeof args[0] === "string" ? args[0] : page.url(),
281
+ duration: Date.now() - start,
282
+ success: true
283
+ });
284
+ onActivity?.();
285
+ return result;
286
+ } catch (err) {
287
+ parentLogAction(session, {
288
+ pageId,
289
+ action: method,
290
+ source: "agent",
291
+ url: typeof args[0] === "string" ? args[0] : void 0,
292
+ duration: Date.now() - start,
293
+ success: false,
294
+ error: err.message
295
+ });
296
+ onActivity?.();
297
+ throw err;
298
+ }
299
+ };
300
+ }
301
+ const LOCATOR_FACTORIES = [
302
+ "locator",
303
+ "getByRole",
304
+ "getByText",
305
+ "getByLabel",
306
+ "getByPlaceholder",
307
+ "getByAltText",
308
+ "getByTitle",
309
+ "getByTestId"
310
+ ];
311
+ for (const factory of LOCATOR_FACTORIES) {
312
+ const orig = page[factory].bind(page);
313
+ page[factory] = (...factoryArgs) => {
314
+ const locator = orig(...factoryArgs);
315
+ const hint = formatHint(factory, factoryArgs);
316
+ return wrapLocator(locator, hint, session, page, pageId, onActivity);
317
+ };
318
+ }
319
+ }
320
+ export {
321
+ parentLogAction,
322
+ readActionLog,
323
+ readNetworkLog,
324
+ wrapPageForActionLogging
325
+ };