libretto 0.6.24 → 0.6.25

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 +83 -313
  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 +105 -422
  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,445 @@
1
+ import {
2
+ appendFileSync,
3
+ existsSync,
4
+ readFileSync,
5
+ } from "node:fs";
6
+ import type { Page } from "playwright";
7
+ import {
8
+ getSessionActionsLogPath,
9
+ getSessionNetworkLogPath,
10
+ } from "./context.js";
11
+ import { assertSessionStateExistsOrThrow } from "./session.js";
12
+
13
+ export type NetworkLogEntry = {
14
+ id?: number;
15
+ ts: string;
16
+ pageId?: string;
17
+ method: string;
18
+ url: string;
19
+ resourceType?: string;
20
+ status: number | null;
21
+ statusText?: string | null;
22
+ contentType: string | null;
23
+ requestHeaders?: Record<string, string> | null;
24
+ responseHeaders?: Record<string, string> | null;
25
+ requestBodyPreview?: string | null;
26
+ requestBodyPath?: string | null;
27
+ requestBodyBytes?: number | null;
28
+ requestBodyTruncated?: boolean;
29
+ requestBodyOmittedReason?: string | null;
30
+ responseBodyPreview?: string | null;
31
+ responseBodyPath?: string | null;
32
+ responseBodyBytes?: number | null;
33
+ responseBodyTruncated?: boolean;
34
+ responseBodyOmittedReason?: string | null;
35
+ errorText?: string | null;
36
+ postData?: string;
37
+ responseBody?: string | null;
38
+ size?: number | null;
39
+ durationMs?: number | null;
40
+ };
41
+
42
+ export function readNetworkLog(
43
+ session: string,
44
+ opts: {
45
+ last?: number;
46
+ filter?: string;
47
+ method?: string;
48
+ pageId?: string;
49
+ } = {},
50
+ ): NetworkLogEntry[] {
51
+ assertSessionStateExistsOrThrow(session);
52
+ const logPath = getSessionNetworkLogPath(session);
53
+ if (!existsSync(logPath)) return [];
54
+
55
+ const lines = readFileSync(logPath, "utf-8")
56
+ .trim()
57
+ .split("\n")
58
+ .filter(Boolean);
59
+ let entries: NetworkLogEntry[] = lines.map(
60
+ (line) => JSON.parse(line) as NetworkLogEntry,
61
+ );
62
+
63
+ if (opts.method) {
64
+ const m = opts.method.toUpperCase();
65
+ entries = entries.filter((e) => e.method === m);
66
+ }
67
+ if (opts.filter) {
68
+ const re = new RegExp(opts.filter, "i");
69
+ entries = entries.filter((e) => re.test(e.url));
70
+ }
71
+ if (opts.pageId) {
72
+ entries = entries.filter((e) => e.pageId === opts.pageId);
73
+ }
74
+
75
+ const last = opts.last ?? 20;
76
+ if (entries.length > last) {
77
+ entries = entries.slice(-last);
78
+ }
79
+
80
+ return entries;
81
+ }
82
+
83
+ export type ActionLogEntry = {
84
+ ts: string;
85
+ pageId?: string;
86
+ action: string;
87
+ source: "user" | "agent";
88
+ selector?: string;
89
+ bestSemanticSelector?: string;
90
+ targetSelector?: string;
91
+ ancestorSelectors?: string[];
92
+ nearbyText?: string;
93
+ composedPath?: string[];
94
+ coordinates?: {
95
+ x: number;
96
+ y: number;
97
+ };
98
+ value?: string;
99
+ url?: string;
100
+ duration?: number;
101
+ success: boolean;
102
+ error?: string;
103
+ };
104
+
105
+ export function parentLogAction(
106
+ session: string,
107
+ entry: Record<string, unknown>,
108
+ ): void {
109
+ try {
110
+ const record = { ts: new Date().toISOString(), ...entry };
111
+ appendFileSync(
112
+ getSessionActionsLogPath(session),
113
+ JSON.stringify(record) + "\n",
114
+ );
115
+ } catch {}
116
+ }
117
+
118
+ export function readActionLog(
119
+ session: string,
120
+ opts: {
121
+ last?: number;
122
+ filter?: string;
123
+ action?: string;
124
+ source?: string;
125
+ pageId?: string;
126
+ } = {},
127
+ ): ActionLogEntry[] {
128
+ assertSessionStateExistsOrThrow(session);
129
+ const logPath = getSessionActionsLogPath(session);
130
+ if (!existsSync(logPath)) return [];
131
+
132
+ const lines = readFileSync(logPath, "utf-8")
133
+ .trim()
134
+ .split("\n")
135
+ .filter(Boolean);
136
+ let entries: ActionLogEntry[] = lines.map(
137
+ (line) => JSON.parse(line) as ActionLogEntry,
138
+ );
139
+
140
+ if (opts.action) {
141
+ const a = opts.action.toLowerCase();
142
+ entries = entries.filter((e) => e.action === a);
143
+ }
144
+ if (opts.source) {
145
+ const s = opts.source.toLowerCase();
146
+ entries = entries.filter((e) => e.source === s);
147
+ }
148
+ if (opts.filter) {
149
+ const re = new RegExp(opts.filter, "i");
150
+ entries = entries.filter(
151
+ (e) =>
152
+ re.test(e.action) ||
153
+ re.test(e.selector || "") ||
154
+ re.test(e.bestSemanticSelector || "") ||
155
+ re.test(e.targetSelector || "") ||
156
+ re.test((e.ancestorSelectors || []).join(" ")) ||
157
+ re.test(e.nearbyText || "") ||
158
+ re.test((e.composedPath || []).join(" ")) ||
159
+ re.test(e.value || "") ||
160
+ re.test(e.url || ""),
161
+ );
162
+ }
163
+ if (opts.pageId) {
164
+ entries = entries.filter((e) => e.pageId === opts.pageId);
165
+ }
166
+
167
+ const last = opts.last ?? 20;
168
+ if (entries.length > last) {
169
+ entries = entries.slice(-last);
170
+ }
171
+
172
+ return entries;
173
+ }
174
+
175
+ const LOCATOR_ACTION_METHODS = [
176
+ "click",
177
+ "dblclick",
178
+ "fill",
179
+ "type",
180
+ "press",
181
+ "check",
182
+ "uncheck",
183
+ "selectOption",
184
+ "hover",
185
+ "focus",
186
+ "scrollIntoViewIfNeeded",
187
+ "waitFor",
188
+ "innerHTML",
189
+ "innerText",
190
+ "textContent",
191
+ "inputValue",
192
+ "isChecked",
193
+ "isDisabled",
194
+ "isEditable",
195
+ "isEnabled",
196
+ "isHidden",
197
+ "isVisible",
198
+ "count",
199
+ "boundingBox",
200
+ "screenshot",
201
+ "evaluate",
202
+ "evaluateAll",
203
+ "evaluateHandle",
204
+ "getAttribute",
205
+ "dispatchEvent",
206
+ "setInputFiles",
207
+ "selectText",
208
+ "dragTo",
209
+ "highlight",
210
+ "tap",
211
+ ] as const;
212
+
213
+ const LOCATOR_RETURNING_METHODS = [
214
+ "first",
215
+ "last",
216
+ "locator",
217
+ "getByRole",
218
+ "getByText",
219
+ "getByLabel",
220
+ "getByPlaceholder",
221
+ "getByAltText",
222
+ "getByTitle",
223
+ "getByTestId",
224
+ "filter",
225
+ "and",
226
+ "or",
227
+ ] as const;
228
+
229
+ function formatHint(method: string, args: any[]): string {
230
+ const formatted = args.map((a: any) => JSON.stringify(a)).join(", ");
231
+ return `${method}(${formatted})`;
232
+ }
233
+
234
+ function wrapLocator(
235
+ locator: any,
236
+ hint: string,
237
+ session: string,
238
+ page: Page,
239
+ pageId?: string,
240
+ onActivity?: () => void,
241
+ ): any {
242
+ if (locator.__librettoActionLogged) return locator;
243
+ locator.__librettoActionLogged = true;
244
+
245
+ for (const actMethod of LOCATOR_ACTION_METHODS) {
246
+ if (typeof locator[actMethod] !== "function") continue;
247
+ const origAct = locator[actMethod].bind(locator);
248
+ locator[actMethod] = async (...actArgs: any[]) => {
249
+ const start = Date.now();
250
+ try {
251
+ await page.evaluate(() => {
252
+ (window as any).__btApiActionInProgress = true;
253
+ });
254
+ } catch {}
255
+ try {
256
+ const result = await origAct(...actArgs);
257
+ parentLogAction(session, {
258
+ pageId,
259
+ action: actMethod,
260
+ source: "agent",
261
+ selector: hint,
262
+ value:
263
+ actArgs[0] !== undefined
264
+ ? String(actArgs[0]).slice(0, 100)
265
+ : undefined,
266
+ duration: Date.now() - start,
267
+ success: true,
268
+ });
269
+ onActivity?.();
270
+ return result;
271
+ } catch (err: any) {
272
+ parentLogAction(session, {
273
+ pageId,
274
+ action: actMethod,
275
+ source: "agent",
276
+ selector: hint,
277
+ duration: Date.now() - start,
278
+ success: false,
279
+ error: err.message,
280
+ });
281
+ onActivity?.();
282
+ throw err;
283
+ } finally {
284
+ try {
285
+ await page.evaluate(() => {
286
+ (window as any).__btApiActionInProgress = false;
287
+ });
288
+ } catch {}
289
+ }
290
+ };
291
+ }
292
+
293
+ for (const method of LOCATOR_RETURNING_METHODS) {
294
+ if (typeof locator[method] !== "function") continue;
295
+ const origMethod = locator[method].bind(locator);
296
+ locator[method] = (...args: any[]) => {
297
+ const child = origMethod(...args);
298
+ const childHint =
299
+ args.length > 0
300
+ ? `${hint}.${formatHint(method, args)}`
301
+ : `${hint}.${method}()`;
302
+ return wrapLocator(child, childHint, session, page, pageId, onActivity);
303
+ };
304
+ }
305
+
306
+ if (typeof locator.nth === "function") {
307
+ const origNth = locator.nth.bind(locator);
308
+ locator.nth = (index: number) => {
309
+ const child = origNth(index);
310
+ const childHint = `${hint}.nth(${index})`;
311
+ return wrapLocator(child, childHint, session, page, pageId, onActivity);
312
+ };
313
+ }
314
+
315
+ if (typeof locator.all === "function") {
316
+ const origAll = locator.all.bind(locator);
317
+ locator.all = async () => {
318
+ const items: any[] = await origAll();
319
+ return items.map((item: any, i: number) => {
320
+ const childHint = `${hint}.all()[${i}]`;
321
+ return wrapLocator(item, childHint, session, page, pageId, onActivity);
322
+ });
323
+ };
324
+ }
325
+
326
+ return locator;
327
+ }
328
+
329
+ export function wrapPageForActionLogging(
330
+ page: Page,
331
+ session: string,
332
+ pageId?: string,
333
+ onActivity?: () => void,
334
+ ): void {
335
+ const PAGE_ACTIONS = [
336
+ "click",
337
+ "dblclick",
338
+ "fill",
339
+ "type",
340
+ "press",
341
+ "check",
342
+ "uncheck",
343
+ "selectOption",
344
+ "hover",
345
+ "focus",
346
+ ] as const;
347
+ const NAV_ACTIONS = ["goto", "reload", "goBack", "goForward"] as const;
348
+
349
+ for (const method of PAGE_ACTIONS) {
350
+ const orig = (page as any)[method].bind(page);
351
+ (page as any)[method] = async (...args: any[]) => {
352
+ const start = Date.now();
353
+ try {
354
+ await page.evaluate(() => {
355
+ (window as any).__btApiActionInProgress = true;
356
+ });
357
+ } catch {}
358
+ try {
359
+ const result = await orig(...args);
360
+ parentLogAction(session, {
361
+ pageId,
362
+ action: method,
363
+ source: "agent",
364
+ selector: typeof args[0] === "string" ? args[0] : undefined,
365
+ value:
366
+ args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
367
+ duration: Date.now() - start,
368
+ success: true,
369
+ });
370
+ onActivity?.();
371
+ return result;
372
+ } catch (err: any) {
373
+ parentLogAction(session, {
374
+ pageId,
375
+ action: method,
376
+ source: "agent",
377
+ selector: typeof args[0] === "string" ? args[0] : undefined,
378
+ duration: Date.now() - start,
379
+ success: false,
380
+ error: err.message,
381
+ });
382
+ onActivity?.();
383
+ throw err;
384
+ } finally {
385
+ try {
386
+ await page.evaluate(() => {
387
+ (window as any).__btApiActionInProgress = false;
388
+ });
389
+ } catch {}
390
+ }
391
+ };
392
+ }
393
+
394
+ for (const method of NAV_ACTIONS) {
395
+ const orig = (page as any)[method].bind(page);
396
+ (page as any)[method] = async (...args: any[]) => {
397
+ const start = Date.now();
398
+ try {
399
+ const result = await orig(...args);
400
+ parentLogAction(session, {
401
+ pageId,
402
+ action: method,
403
+ source: "agent",
404
+ url: typeof args[0] === "string" ? args[0] : page.url(),
405
+ duration: Date.now() - start,
406
+ success: true,
407
+ });
408
+ onActivity?.();
409
+ return result;
410
+ } catch (err: any) {
411
+ parentLogAction(session, {
412
+ pageId,
413
+ action: method,
414
+ source: "agent",
415
+ url: typeof args[0] === "string" ? args[0] : undefined,
416
+ duration: Date.now() - start,
417
+ success: false,
418
+ error: err.message,
419
+ });
420
+ onActivity?.();
421
+ throw err;
422
+ }
423
+ };
424
+ }
425
+
426
+ const LOCATOR_FACTORIES = [
427
+ "locator",
428
+ "getByRole",
429
+ "getByText",
430
+ "getByLabel",
431
+ "getByPlaceholder",
432
+ "getByAltText",
433
+ "getByTitle",
434
+ "getByTestId",
435
+ ] as const;
436
+
437
+ for (const factory of LOCATOR_FACTORIES) {
438
+ const orig = (page as any)[factory].bind(page);
439
+ (page as any)[factory] = (...factoryArgs: any[]) => {
440
+ const locator = orig(...factoryArgs);
441
+ const hint = formatHint(factory, factoryArgs);
442
+ return wrapLocator(locator, hint, session, page, pageId, onActivity);
443
+ };
444
+ }
445
+ }