libretto 0.2.0 → 0.2.2

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 (69) hide show
  1. package/README.md +128 -126
  2. package/dist/cli/cli.js +2 -0
  3. package/dist/cli/commands/browser.js +4 -1
  4. package/dist/cli/commands/execution.js +21 -6
  5. package/dist/cli/commands/logs.js +36 -8
  6. package/dist/cli/commands/snapshot.js +14 -7
  7. package/dist/cli/core/browser.js +89 -253
  8. package/dist/cli/core/session-telemetry.js +491 -0
  9. package/dist/cli/core/telemetry.js +18 -6
  10. package/dist/cli/workers/run-integration-runtime.js +19 -1
  11. package/dist/index.cjs +2 -6
  12. package/dist/index.d.cts +2 -5
  13. package/dist/index.d.ts +2 -5
  14. package/dist/index.js +2 -5
  15. package/dist/runtime/download/download.d.cts +2 -2
  16. package/dist/runtime/download/download.d.ts +2 -2
  17. package/dist/runtime/extract/extract.cjs +2 -1
  18. package/dist/runtime/extract/extract.d.cts +5 -5
  19. package/dist/runtime/extract/extract.d.ts +5 -5
  20. package/dist/runtime/extract/extract.js +2 -1
  21. package/dist/runtime/network/network.d.cts +6 -6
  22. package/dist/runtime/network/network.d.ts +6 -6
  23. package/dist/runtime/recovery/agent.cjs +12 -7
  24. package/dist/runtime/recovery/agent.d.cts +2 -2
  25. package/dist/runtime/recovery/agent.d.ts +2 -2
  26. package/dist/runtime/recovery/agent.js +12 -7
  27. package/dist/runtime/recovery/errors.cjs +8 -6
  28. package/dist/runtime/recovery/errors.d.cts +2 -2
  29. package/dist/runtime/recovery/errors.d.ts +2 -2
  30. package/dist/runtime/recovery/errors.js +8 -6
  31. package/dist/runtime/recovery/recovery.cjs +5 -3
  32. package/dist/runtime/recovery/recovery.d.cts +2 -2
  33. package/dist/runtime/recovery/recovery.d.ts +2 -2
  34. package/dist/runtime/recovery/recovery.js +5 -3
  35. package/dist/shared/instrumentation/instrument.d.cts +2 -2
  36. package/dist/shared/instrumentation/instrument.d.ts +2 -2
  37. package/dist/shared/llm/types.d.cts +5 -5
  38. package/dist/shared/llm/types.d.ts +5 -5
  39. package/dist/shared/logger/index.cjs +2 -0
  40. package/dist/shared/logger/index.d.cts +1 -1
  41. package/dist/shared/logger/index.d.ts +1 -1
  42. package/dist/shared/logger/index.js +2 -1
  43. package/dist/shared/logger/logger.cjs +15 -2
  44. package/dist/shared/logger/logger.d.cts +13 -1
  45. package/dist/shared/logger/logger.d.ts +13 -1
  46. package/dist/shared/logger/logger.js +13 -1
  47. package/dist/shared/state/session-state.d.cts +2 -2
  48. package/dist/shared/state/session-state.d.ts +2 -2
  49. package/package.json +15 -11
  50. package/scripts/postinstall.mjs +48 -0
  51. package/skill/SKILL.md +438 -0
  52. package/skill/code-generation-rules.md +190 -0
  53. package/skill/integration-approach-selection.md +174 -0
  54. package/dist/runtime/step/index.cjs +0 -31
  55. package/dist/runtime/step/index.d.cts +0 -7
  56. package/dist/runtime/step/index.d.ts +0 -7
  57. package/dist/runtime/step/index.js +0 -6
  58. package/dist/runtime/step/runner.cjs +0 -208
  59. package/dist/runtime/step/runner.d.cts +0 -16
  60. package/dist/runtime/step/runner.d.ts +0 -16
  61. package/dist/runtime/step/runner.js +0 -187
  62. package/dist/runtime/step/step.cjs +0 -67
  63. package/dist/runtime/step/step.d.cts +0 -23
  64. package/dist/runtime/step/step.d.ts +0 -23
  65. package/dist/runtime/step/step.js +0 -43
  66. package/dist/runtime/step/types.cjs +0 -16
  67. package/dist/runtime/step/types.d.cts +0 -72
  68. package/dist/runtime/step/types.d.ts +0 -72
  69. package/dist/runtime/step/types.js +0 -0
@@ -17,6 +17,7 @@ import {
17
17
  readSessionState,
18
18
  writeSessionState
19
19
  } from "./session.js";
20
+ import { installSessionTelemetry } from "./session-telemetry.js";
20
21
  async function pickFreePort() {
21
22
  return await new Promise((resolve2, reject) => {
22
23
  const server = createServer();
@@ -76,6 +77,10 @@ async function tryConnectToPort(port, logger, timeoutMs = 5e3) {
76
77
  return null;
77
78
  }
78
79
  }
80
+ function isOperationalPage(page) {
81
+ const url = page.url();
82
+ return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
83
+ }
79
84
  function disconnectBrowser(browser, logger, session) {
80
85
  logger.info("cdp-disconnect", { session });
81
86
  try {
@@ -84,7 +89,46 @@ function disconnectBrowser(browser, logger, session) {
84
89
  logger.warn("cdp-disconnect-already-closed", { error: err });
85
90
  }
86
91
  }
87
- async function connect(session, logger, timeoutMs = 1e4) {
92
+ function resolveOperationalPages(browser) {
93
+ return browser.contexts().flatMap((context) => context.pages()).filter(isOperationalPage);
94
+ }
95
+ async function resolvePageId(page) {
96
+ const cdpSession = await page.context().newCDPSession(page);
97
+ try {
98
+ const targetInfo = await cdpSession.send("Target.getTargetInfo");
99
+ const targetId = targetInfo?.targetInfo?.targetId;
100
+ if (typeof targetId !== "string" || targetId.length === 0) {
101
+ throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
102
+ }
103
+ return targetId;
104
+ } finally {
105
+ await cdpSession.detach();
106
+ }
107
+ }
108
+ async function resolvePageReferences(pages) {
109
+ const refs = await Promise.all(
110
+ pages.map(async (page) => {
111
+ const id = await resolvePageId(page);
112
+ return { id, page };
113
+ })
114
+ );
115
+ return refs;
116
+ }
117
+ async function listOpenPages(session, logger) {
118
+ const { browser, page: activePage } = await connect(session, logger);
119
+ try {
120
+ const pages = browser.contexts().flatMap((ctx) => ctx.pages()).filter(isOperationalPage);
121
+ const pageRefs = await resolvePageReferences(pages);
122
+ return pageRefs.map(({ id, page }) => ({
123
+ id,
124
+ url: page.url(),
125
+ active: page === activePage
126
+ }));
127
+ } finally {
128
+ disconnectBrowser(browser, logger, session);
129
+ }
130
+ }
131
+ async function connect(session, logger, timeoutMs = 1e4, options) {
88
132
  logger.info("connect", { session, timeoutMs });
89
133
  const state = readSessionStateOrThrow(session);
90
134
  const browser = await tryConnectToPort(state.port, logger, timeoutMs);
@@ -106,10 +150,7 @@ async function connect(session, logger, timeoutMs = 1e4) {
106
150
  throw new Error("No browser context found.");
107
151
  }
108
152
  const allPages = contexts.flatMap((c) => c.pages());
109
- const pages = allPages.filter((p) => {
110
- const url = p.url();
111
- return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
112
- });
153
+ const pages = resolveOperationalPages(browser);
113
154
  logger.info("connect-pages", {
114
155
  session,
115
156
  totalPages: allPages.length,
@@ -123,7 +164,19 @@ async function connect(session, logger, timeoutMs = 1e4) {
123
164
  });
124
165
  throw new Error("No pages found.");
125
166
  }
126
- const page = pages[pages.length - 1];
167
+ if (options?.requireSinglePage && !options.pageId && pages.length > 1) {
168
+ throw new Error(
169
+ `Multiple pages are open in session "${session}". Pass --page <id> to target a page (run "libretto-cli pages --session ${session}" to list ids).`
170
+ );
171
+ }
172
+ const pageRefs = await resolvePageReferences(pages);
173
+ const pageRef = options?.pageId ? pageRefs.find((ref) => ref.id === options.pageId) ?? null : pageRefs[pageRefs.length - 1];
174
+ if (!pageRef) {
175
+ throw new Error(
176
+ `Page "${options?.pageId}" was not found in session "${session}". Run "libretto-cli pages --session ${session}" to list ids.`
177
+ );
178
+ }
179
+ const page = pageRef.page;
127
180
  const context = page.context();
128
181
  page.on("close", () => {
129
182
  logger.error("page-closed-during-command", {
@@ -145,7 +198,20 @@ async function connect(session, logger, timeoutMs = 1e4) {
145
198
  });
146
199
  });
147
200
  logger.info("connect-success", { session, pageUrl: page.url() });
148
- return { browser, context, page };
201
+ return { browser, context, page, pageId: pageRef.id };
202
+ }
203
+ async function runPages(session, logger) {
204
+ logger.info("pages-start", { session });
205
+ const pageSummaries = await listOpenPages(session, logger);
206
+ if (pageSummaries.length === 0) {
207
+ console.log("No pages found.");
208
+ return;
209
+ }
210
+ console.log("Open pages:");
211
+ pageSummaries.forEach((pageSummary) => {
212
+ const activeSuffix = pageSummary.active ? " active=true" : "";
213
+ console.log(` id=${pageSummary.id} url=${pageSummary.url}${activeSuffix}`);
214
+ });
149
215
  }
150
216
  async function runOpen(rawUrl, headed, session, logger) {
151
217
  const url = normalizeUrl(rawUrl);
@@ -181,43 +247,21 @@ async function runOpen(rawUrl, headed, session, logger) {
181
247
  const launcherCode = `
182
248
  import { chromium } from 'playwright';
183
249
  import { appendFileSync, mkdirSync } from 'node:fs';
250
+ import { dirname } from 'node:path';
184
251
 
185
252
  const LOG_FILE = '${escapedLogPath}';
186
253
  const NETWORK_LOG = '${escapedNetworkLogPath}';
187
254
  const ACTIONS_LOG = '${escapedActionsLogPath}';
188
- mkdirSync(NETWORK_LOG.replace(/\\/[^\\/]+$/, ''), { recursive: true });
255
+ mkdirSync(dirname(NETWORK_LOG), { recursive: true });
189
256
 
190
- const STATIC_EXT_RE = /\\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\\?|$)/i;
191
- async function logNetworkResponse(response) {
192
- try {
193
- const req = response.request();
194
- const url = req.url();
195
- if (STATIC_EXT_RE.test(url) || url.startsWith('chrome-extension://')) return;
196
- let responseBody = null;
197
- try {
198
- const buf = await response.body();
199
- responseBody = buf.toString('utf-8');
200
- } catch {}
201
- const entry = JSON.stringify({
202
- ts: new Date().toISOString(),
203
- method: req.method(),
204
- url,
205
- status: response.status(),
206
- contentType: response.headers()['content-type'] || null,
207
- postData: req.method() === 'POST' || req.method() === 'PUT' || req.method() === 'PATCH'
208
- ? (req.postData() || '').substring(0, 2000)
209
- : undefined,
210
- responseBody,
211
- });
212
- appendFileSync(NETWORK_LOG, entry + '\\n');
213
- } catch {}
214
- }
257
+ ${installSessionTelemetry.toString()}
215
258
 
216
259
  function logAction(entry) {
217
- try {
218
- const record = { ts: new Date().toISOString(), ...entry };
219
- appendFileSync(ACTIONS_LOG, JSON.stringify(record) + '\\n');
220
- } catch {}
260
+ appendFileSync(ACTIONS_LOG, JSON.stringify(entry) + '\\n');
261
+ }
262
+
263
+ function logNetwork(entry) {
264
+ appendFileSync(NETWORK_LOG, JSON.stringify(entry) + '\\n');
221
265
  }
222
266
 
223
267
  function childLog(level, event, data = {}) {
@@ -234,185 +278,6 @@ function childLog(level, event, data = {}) {
234
278
  } catch {}
235
279
  }
236
280
 
237
- async function setupActionTracking(p) {
238
- await p.exposeFunction('__btActionLog', (jsonStr) => {
239
- try { logAction({ ...JSON.parse(jsonStr), source: 'user' }); } catch {}
240
- });
241
-
242
- await p.addInitScript(() => {
243
- if (window.__btDomListenersInstalled) return;
244
- window.__btDomListenersInstalled = true;
245
-
246
- function identify(el) {
247
- if (!el || !el.tagName) return '';
248
- var tid = el.getAttribute('data-testid');
249
- if (tid) return '[data-testid="' + tid + '"]';
250
- var role = el.getAttribute('role') || '';
251
- var id = el.id;
252
- if (role && id) return role + '#' + id;
253
- var name = el.getAttribute('aria-label') || (el.textContent || '').trim().slice(0, 30) || '';
254
- if (role && name) return role + ' "' + name + '"';
255
- var tag = el.tagName.toLowerCase();
256
- var cls = el.className && typeof el.className === 'string' ? '.' + el.className.trim().split(/\\s+/).slice(0, 2).join('.') : '';
257
- return tag + cls;
258
- }
259
-
260
- var clickTimer = null;
261
- var pendingClick = null;
262
-
263
- document.addEventListener('click', function(e) {
264
- if (window.__btApiActionInProgress) return;
265
- var target = e.target;
266
- var sel = identify(target);
267
- if (target.type === 'checkbox') {
268
- if (typeof window.__btActionLog === 'function') {
269
- window.__btActionLog(JSON.stringify({ action: target.checked ? 'check' : 'uncheck', selector: sel, success: true }));
270
- }
271
- return;
272
- }
273
- pendingClick = { selector: sel };
274
- if (clickTimer) clearTimeout(clickTimer);
275
- clickTimer = setTimeout(function() {
276
- if (pendingClick && typeof window.__btActionLog === 'function') {
277
- window.__btActionLog(JSON.stringify({ action: 'click', selector: pendingClick.selector, success: true }));
278
- }
279
- pendingClick = null;
280
- clickTimer = null;
281
- }, 200);
282
- }, true);
283
-
284
- document.addEventListener('dblclick', function(e) {
285
- if (window.__btApiActionInProgress) return;
286
- if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; pendingClick = null; }
287
- var sel = identify(e.target);
288
- if (typeof window.__btActionLog === 'function') {
289
- window.__btActionLog(JSON.stringify({ action: 'dblclick', selector: sel, success: true }));
290
- }
291
- }, true);
292
-
293
- var inputTimers = new WeakMap();
294
- document.addEventListener('input', function(e) {
295
- if (window.__btApiActionInProgress) return;
296
- var target = e.target;
297
- var sel = identify(target);
298
- if (target.tagName === 'SELECT') {
299
- if (typeof window.__btActionLog === 'function') {
300
- window.__btActionLog(JSON.stringify({ action: 'selectOption', selector: sel, value: target.value, success: true }));
301
- }
302
- return;
303
- }
304
- var existing = inputTimers.get(target);
305
- if (existing) clearTimeout(existing);
306
- inputTimers.set(target, setTimeout(function() {
307
- inputTimers.delete(target);
308
- if (typeof window.__btActionLog === 'function') {
309
- window.__btActionLog(JSON.stringify({ action: 'fill', selector: sel, value: (target.value || '').slice(0, 100), success: true }));
310
- }
311
- }, 500));
312
- }, true);
313
-
314
- var SPECIAL_KEYS = ['Enter','Escape','Tab','Backspace','Delete','ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Home','End','PageUp','PageDown','F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12'];
315
- document.addEventListener('keydown', function(e) {
316
- if (window.__btApiActionInProgress) return;
317
- var isShortcut = e.ctrlKey || e.metaKey || e.altKey;
318
- if (!isShortcut && SPECIAL_KEYS.indexOf(e.key) === -1) return;
319
- var sel = identify(e.target);
320
- var keyDesc = (e.ctrlKey ? 'Ctrl+' : '') + (e.metaKey ? 'Meta+' : '') + (e.altKey ? 'Alt+' : '') + (e.shiftKey ? 'Shift+' : '') + e.key;
321
- if (typeof window.__btActionLog === 'function') {
322
- window.__btActionLog(JSON.stringify({ action: 'press', selector: sel, value: keyDesc, success: true }));
323
- }
324
- }, true);
325
-
326
- var scrollTimer = null;
327
- document.addEventListener('scroll', function() {
328
- if (window.__btApiActionInProgress) return;
329
- if (scrollTimer) clearTimeout(scrollTimer);
330
- scrollTimer = setTimeout(function() {
331
- scrollTimer = null;
332
- if (typeof window.__btActionLog === 'function') {
333
- window.__btActionLog(JSON.stringify({ action: 'scroll', selector: 'document', value: 'y=' + window.scrollY, success: true }));
334
- }
335
- }, 300);
336
- }, true);
337
- });
338
-
339
- var PAGE_ACTIONS = ['click', 'dblclick', 'fill', 'type', 'press', 'check', 'uncheck', 'selectOption', 'hover', 'focus'];
340
- var NAV_ACTIONS = ['goto', 'reload', 'goBack', 'goForward'];
341
-
342
- for (var m of PAGE_ACTIONS) {
343
- (function(method) {
344
- var orig = p[method].bind(p);
345
- p[method] = async function() {
346
- var args = Array.from(arguments);
347
- var start = Date.now();
348
- try { await p.evaluate(function() { window.__btApiActionInProgress = true; }); } catch {}
349
- try {
350
- var result = await orig.apply(null, args);
351
- logAction({ action: method, source: 'agent', selector: typeof args[0] === 'string' ? args[0] : undefined, value: args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined, duration: Date.now() - start, success: true });
352
- return result;
353
- } catch (err) {
354
- logAction({ action: method, source: 'agent', selector: typeof args[0] === 'string' ? args[0] : undefined, duration: Date.now() - start, success: false, error: err.message });
355
- throw err;
356
- } finally {
357
- try { await p.evaluate(function() { window.__btApiActionInProgress = false; }); } catch {}
358
- }
359
- };
360
- })(m);
361
- }
362
-
363
- for (var m of NAV_ACTIONS) {
364
- (function(method) {
365
- var orig = p[method].bind(p);
366
- p[method] = async function() {
367
- var args = Array.from(arguments);
368
- var start = Date.now();
369
- try {
370
- var result = await orig.apply(null, args);
371
- logAction({ action: method, source: 'agent', url: typeof args[0] === 'string' ? args[0] : p.url(), duration: Date.now() - start, success: true });
372
- return result;
373
- } catch (err) {
374
- logAction({ action: method, source: 'agent', url: typeof args[0] === 'string' ? args[0] : undefined, duration: Date.now() - start, success: false, error: err.message });
375
- throw err;
376
- }
377
- };
378
- })(m);
379
- }
380
-
381
- var LOCATOR_FACTORIES = ['locator', 'getByRole', 'getByText', 'getByLabel', 'getByPlaceholder', 'getByAltText', 'getByTitle', 'getByTestId'];
382
- for (var f of LOCATOR_FACTORIES) {
383
- (function(factory) {
384
- var orig = p[factory].bind(p);
385
- p[factory] = function() {
386
- var args = Array.from(arguments);
387
- var locator = orig.apply(null, args);
388
- var hint = factory + '(' + args.map(function(a) { return typeof a === 'string' ? a : JSON.stringify(a); }).join(', ') + ')';
389
- for (var am of PAGE_ACTIONS) {
390
- (function(actMethod) {
391
- if (typeof locator[actMethod] !== 'function') return;
392
- var origAct = locator[actMethod].bind(locator);
393
- locator[actMethod] = async function() {
394
- var actArgs = Array.from(arguments);
395
- var start = Date.now();
396
- try { await p.evaluate(function() { window.__btApiActionInProgress = true; }); } catch {}
397
- try {
398
- var result = await origAct.apply(null, actArgs);
399
- logAction({ action: actMethod, source: 'agent', selector: hint, value: actArgs[0] !== undefined ? String(actArgs[0]).slice(0, 100) : undefined, duration: Date.now() - start, success: true });
400
- return result;
401
- } catch (err) {
402
- logAction({ action: actMethod, source: 'agent', selector: hint, duration: Date.now() - start, success: false, error: err.message });
403
- throw err;
404
- } finally {
405
- try { await p.evaluate(function() { window.__btApiActionInProgress = false; }); } catch {}
406
- }
407
- };
408
- })(am);
409
- }
410
- return locator;
411
- };
412
- })(f);
413
- }
414
- }
415
-
416
281
  const browser = await chromium.launch({
417
282
  headless: ${!headed},
418
283
  args: ['--disable-blink-features=AutomationControlled', '--remote-debugging-port=${port}', '--remote-debugging-address=127.0.0.1', '--no-focus-on-check'],
@@ -432,43 +297,12 @@ const page = await context.newPage();
432
297
  page.setDefaultTimeout(30000);
433
298
  page.setDefaultNavigationTimeout(45000);
434
299
 
435
- await setupActionTracking(page);
436
-
437
- page.on('crash', () => childLog('error', 'page-crash', { url: page.url() }));
438
- page.on('close', () => childLog('warn', 'page-close', { url: page.url(), trace: new Error('page-close-trace').stack }));
439
- page.on('pageerror', (err) => childLog('error', 'page-error', { message: err.message, stack: err.stack }));
440
- page.on('console', (msg) => {
441
- if (msg.type() === 'error' || msg.type() === 'warning') {
442
- childLog(msg.type() === 'error' ? 'error' : 'warn', 'console-' + msg.type(), { text: msg.text(), url: page.url() });
443
- }
444
- });
445
- page.on('framenavigated', (frame) => {
446
- if (frame === page.mainFrame()) {
447
- childLog('info', 'page-navigated', { url: frame.url() });
448
- logAction({ action: 'navigate', source: 'agent', url: frame.url(), success: true });
449
- }
450
- });
451
- page.on('requestfailed', (req) => {
452
- const failure = req.failure();
453
- childLog('warn', 'request-failed', { url: req.url(), method: req.method(), errorText: failure?.errorText });
454
- });
455
- page.on('response', logNetworkResponse);
456
- page.on('popup', (popup) => logAction({ action: 'popup', source: 'agent', url: popup.url(), success: true }));
457
- page.on('dialog', (dialog) => logAction({ action: 'dialog', source: 'agent', value: dialog.type() + ': ' + dialog.message().slice(0, 500), success: true }));
458
-
459
- context.on('page', async (newPage) => {
460
- childLog('info', 'new-page-created', { url: newPage.url() });
461
- newPage.on('crash', () => childLog('error', 'page-crash', { url: newPage.url() }));
462
- newPage.on('close', () => childLog('info', 'page-close', { url: newPage.url(), trace: new Error('page-close-trace').stack }));
463
- newPage.on('response', logNetworkResponse);
464
- newPage.on('popup', (popup) => logAction({ action: 'popup', source: 'agent', url: popup.url(), success: true }));
465
- newPage.on('dialog', (dialog) => logAction({ action: 'dialog', source: 'agent', value: dialog.type() + ': ' + dialog.message().slice(0, 500), success: true }));
466
- newPage.on('framenavigated', (frame) => {
467
- if (frame === newPage.mainFrame()) logAction({ action: 'navigate', source: 'agent', url: frame.url(), success: true });
468
- });
469
- try { await setupActionTracking(newPage); } catch (err) {
470
- childLog('warn', 'action-tracking-setup-failed', { url: newPage.url(), error: err.message });
471
- }
300
+ await installSessionTelemetry({
301
+ context,
302
+ initialPage: page,
303
+ includeUserDomActions: true,
304
+ logAction,
305
+ logNetwork,
472
306
  });
473
307
 
474
308
 
@@ -678,10 +512,12 @@ export {
678
512
  getProfilePath,
679
513
  getScreenshotBaseName,
680
514
  hasProfile,
515
+ listOpenPages,
681
516
  normalizeDomain,
682
517
  normalizeUrl,
683
518
  resolvePath,
684
519
  runClose,
685
520
  runOpen,
521
+ runPages,
686
522
  runSave
687
523
  };