libretto 0.5.1 → 0.5.3-experimental.0

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 (45) hide show
  1. package/README.md +10 -5
  2. package/dist/cli/commands/execution.js +38 -12
  3. package/dist/cli/commands/init.js +4 -21
  4. package/dist/cli/core/ai-config.js +12 -2
  5. package/dist/cli/core/browser.js +75 -8
  6. package/dist/cli/core/session-telemetry.js +429 -172
  7. package/dist/cli/core/telemetry.js +10 -2
  8. package/dist/cli/framework/simple-cli.js +4 -0
  9. package/dist/cli/workers/run-integration-runtime.js +18 -41
  10. package/dist/cli/workers/run-integration-worker-protocol.js +2 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +6 -0
  13. package/dist/shared/condense-dom/condense-dom.js +11 -56
  14. package/dist/shared/dom-semantics.d.ts +8 -0
  15. package/dist/shared/dom-semantics.js +69 -0
  16. package/dist/shared/run/browser.js +40 -1
  17. package/dist/shared/visualization/ghost-cursor.js +17 -4
  18. package/dist/shared/workflow/workflow.d.ts +14 -3
  19. package/dist/shared/workflow/workflow.js +50 -3
  20. package/package.json +7 -4
  21. package/scripts/check-skills-sync.mjs +1 -1
  22. package/scripts/generate-changelog.ts +132 -0
  23. package/scripts/skills-libretto.mjs +1 -1
  24. package/scripts/sync-skills.mjs +1 -1
  25. package/skills/libretto/SKILL.md +54 -38
  26. package/skills/libretto/references/action-logs.md +101 -0
  27. package/skills/libretto/references/auth-profiles.md +1 -2
  28. package/skills/libretto/references/code-generation-rules.md +10 -6
  29. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  30. package/src/cli/commands/execution.ts +39 -11
  31. package/src/cli/commands/init.ts +5 -24
  32. package/src/cli/core/ai-config.ts +12 -1
  33. package/src/cli/core/browser.ts +82 -8
  34. package/src/cli/core/session-telemetry.ts +431 -190
  35. package/src/cli/core/telemetry.ts +23 -1
  36. package/src/cli/framework/simple-cli.ts +5 -0
  37. package/src/cli/workers/run-integration-runtime.ts +24 -52
  38. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  39. package/src/index.ts +4 -0
  40. package/src/shared/condense-dom/condense-dom.ts +12 -64
  41. package/src/shared/dom-semantics.ts +68 -0
  42. package/src/shared/run/browser.ts +53 -0
  43. package/src/shared/visualization/ghost-cursor.ts +22 -4
  44. package/src/shared/workflow/workflow.ts +88 -2
  45. package/scripts/prepare-release.sh +0 -97
@@ -17,8 +17,8 @@ import {
17
17
  loadSnapshotEnv,
18
18
  resolveSnapshotApiModel,
19
19
  } from "../core/snapshot-api-config.js";
20
- import { SimpleCLI } from "../framework/simple-cli.js";
21
20
  import { hasProviderCredentials } from "../../shared/llm/client.js";
21
+ import { SimpleCLI } from "../framework/simple-cli.js";
22
22
 
23
23
  type ProviderChoice = {
24
24
  key: string;
@@ -66,16 +66,6 @@ function promptUser(
66
66
  });
67
67
  }
68
68
 
69
- function askYesNo(question: string): Promise<boolean> {
70
- const rl = createInterface({ input: process.stdin, output: process.stdout });
71
- return new Promise((resolve) => {
72
- rl.question(`${question} (y/N) `, (answer) => {
73
- rl.close();
74
- resolve(answer.trim().toLowerCase() === "y");
75
- });
76
- });
77
- }
78
-
79
69
  function safeReadAiConfig(): ReturnType<typeof readAiConfig> {
80
70
  try {
81
71
  return readAiConfig();
@@ -270,7 +260,7 @@ function detectAgentDirs(root: string): string[] {
270
260
  return dirs;
271
261
  }
272
262
 
273
- async function copySkills(): Promise<void> {
263
+ function copySkills(): void {
274
264
  const agentDirs = detectAgentDirs(REPO_ROOT);
275
265
 
276
266
  if (agentDirs.length === 0) {
@@ -281,17 +271,6 @@ async function copySkills(): Promise<void> {
281
271
  }
282
272
 
283
273
  const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
284
- const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
285
- // Say "Overwrite" if skills already exist in ANY target dir — skills must
286
- // be identical across coding agents, so we always copy to all of them.
287
- const existing = destinations.filter((d) => existsSync(d));
288
- const verb = existing.length > 0 ? "Overwrite" : "Install";
289
-
290
- const proceed = await askYesNo(`\n${verb} libretto skills in ${dirNames}?`);
291
- if (!proceed) {
292
- console.log(" Skipping skill copy.");
293
- return;
294
- }
295
274
 
296
275
  let sourceDir: string;
297
276
  try {
@@ -339,10 +318,12 @@ export const initCommand = SimpleCLI.command({
339
318
  console.log("\nSkipping browser installation (--skip-browsers)");
340
319
  }
341
320
 
321
+ copySkills();
322
+
342
323
  if (process.stdin.isTTY) {
343
- await copySkills();
344
324
  await runInteractiveApiSetup();
345
325
  } else {
326
+ loadSnapshotEnv();
346
327
  printSnapshotApiStatus();
347
328
  }
348
329
 
@@ -29,11 +29,18 @@ export const ViewportConfigSchema = z.object({
29
29
  });
30
30
  export type ViewportConfig = z.infer<typeof ViewportConfigSchema>;
31
31
 
32
+ export const WindowPositionConfigSchema = z.object({
33
+ x: z.number().int(),
34
+ y: z.number().int(),
35
+ });
36
+ export type WindowPositionConfig = z.infer<typeof WindowPositionConfigSchema>;
37
+
32
38
  export const LibrettoConfigSchema = z
33
39
  .object({
34
40
  version: z.literal(CURRENT_CONFIG_VERSION),
35
41
  ai: AiConfigSchema.optional(),
36
42
  viewport: ViewportConfigSchema.optional(),
43
+ windowPosition: WindowPositionConfigSchema.optional(),
37
44
  })
38
45
  .passthrough();
39
46
  export type LibrettoConfig = z.infer<typeof LibrettoConfigSchema>;
@@ -80,6 +87,10 @@ function formatExpectedConfigExample(): string {
80
87
  width: 1280,
81
88
  height: 800,
82
89
  },
90
+ windowPosition: {
91
+ x: 1600,
92
+ y: 120,
93
+ },
83
94
  },
84
95
  null,
85
96
  2,
@@ -94,7 +105,7 @@ function invalidConfigError(configPath: string, detail?: string): Error {
94
105
  "Expected config example:",
95
106
  formatExpectedConfigExample(),
96
107
  "Notes:",
97
- ' - "ai" and "viewport" are optional.',
108
+ ' - "ai", "viewport", and "windowPosition" are optional.',
98
109
  ' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
99
110
  "Fix the file to match this shape, or delete it and rerun:",
100
111
  ` npx libretto ai configure ${formatConfigureProviders()}`,
@@ -12,6 +12,14 @@ import { createRequire } from "node:module";
12
12
  import { createServer } from "node:net";
13
13
  import { spawn } from "node:child_process";
14
14
  import type { LoggerApi } from "../../shared/logger/index.js";
15
+ import {
16
+ filterSemanticClasses,
17
+ INTERACTIVE_ROLE_NAMES,
18
+ INTERACTIVE_TAG_NAMES,
19
+ isObfuscatedClass,
20
+ TEST_ATTRIBUTE_NAMES,
21
+ TRUSTED_ATTRIBUTE_NAMES,
22
+ } from "../../shared/dom-semantics.js";
15
23
  import {
16
24
  getSessionActionsLogPath,
17
25
  getSessionNetworkLogPath,
@@ -330,6 +338,20 @@ export function resolveViewport(
330
338
  return DEFAULT_VIEWPORT;
331
339
  }
332
340
 
341
+ function resolveWindowPosition(
342
+ logger: LoggerApi,
343
+ ): { x: number; y: number } | undefined {
344
+ const config = readLibrettoConfig();
345
+ if (config.windowPosition) {
346
+ logger.info("window-position-source", {
347
+ source: "config",
348
+ windowPosition: config.windowPosition,
349
+ });
350
+ return config.windowPosition;
351
+ }
352
+ return undefined;
353
+ }
354
+
333
355
  export async function runOpen(
334
356
  rawUrl: string,
335
357
  headed: boolean,
@@ -339,7 +361,8 @@ export async function runOpen(
339
361
  ): Promise<void> {
340
362
  const url = normalizeUrl(rawUrl);
341
363
  const viewport = resolveViewport(options?.viewport, logger);
342
- logger.info("open-start", { url, headed, session, viewport });
364
+ const windowPosition = headed ? resolveWindowPosition(logger) : undefined;
365
+ logger.info("open-start", { url, headed, session, viewport, windowPosition });
343
366
  assertSessionAvailableForStart(session, logger);
344
367
 
345
368
  const port = await pickFreePort();
@@ -382,6 +405,49 @@ export async function runOpen(
382
405
  const escapedActionsLogPath = actionsLogPath
383
406
  .replace(/\\/g, "\\\\")
384
407
  .replace(/'/g, "\\'");
408
+ const windowPositionArg = windowPosition
409
+ ? `, '--window-position=${windowPosition.x},${windowPosition.y}'`
410
+ : "";
411
+ const windowBoundsSetupCode = windowPosition
412
+ ? `
413
+ const requestedWindowBounds = { left: ${windowPosition.x}, top: ${windowPosition.y}, windowState: 'normal' };
414
+ const pageCdp = await context.newCDPSession(page);
415
+ let browserCdp;
416
+ try {
417
+ const targetInfo = await pageCdp.send('Target.getTargetInfo');
418
+ const targetId = targetInfo?.targetInfo?.targetId;
419
+ browserCdp = await browser.newBrowserCDPSession();
420
+ const windowResult = await browserCdp.send(
421
+ 'Browser.getWindowForTarget',
422
+ targetId ? { targetId } : {},
423
+ );
424
+ await browserCdp.send('Browser.setWindowBounds', {
425
+ windowId: windowResult.windowId,
426
+ bounds: requestedWindowBounds,
427
+ });
428
+ await new Promise((resolve) => setTimeout(resolve, 250));
429
+ const actualWindow = await browserCdp.send('Browser.getWindowBounds', {
430
+ windowId: windowResult.windowId,
431
+ });
432
+ childLog('info', 'window-bounds-set', {
433
+ windowId: windowResult.windowId,
434
+ requestedBounds: requestedWindowBounds,
435
+ actualBounds: actualWindow.bounds,
436
+ });
437
+ } catch (error) {
438
+ childLog('warn', 'window-bounds-set-failed', {
439
+ requestedBounds: requestedWindowBounds,
440
+ message: error instanceof Error ? error.message : String(error),
441
+ stack: error instanceof Error ? error.stack : undefined,
442
+ });
443
+ } finally {
444
+ await pageCdp.detach().catch(() => {});
445
+ if (browserCdp) {
446
+ await browserCdp.detach().catch(() => {});
447
+ }
448
+ }
449
+ `
450
+ : "";
385
451
 
386
452
  const launcherCode = `
387
453
  import { chromium } from 'playwright';
@@ -394,14 +460,21 @@ const ACTIONS_LOG = '${escapedActionsLogPath}';
394
460
  mkdirSync(dirname(NETWORK_LOG), { recursive: true });
395
461
 
396
462
  // tsx/esbuild may emit __name() wrappers in Function#toString output.
397
- const __name = (target, value) =>
398
- Object.defineProperty(target, 'name', { value, configurable: true });
463
+ const __name = (target, value) =>
464
+ Object.defineProperty(target, 'name', { value, configurable: true });
399
465
 
400
- ${installSessionTelemetry.toString()}
466
+ const TEST_ATTRIBUTE_NAMES = ${JSON.stringify([...TEST_ATTRIBUTE_NAMES])};
467
+ const TRUSTED_ATTRIBUTE_NAMES = ${JSON.stringify([...TRUSTED_ATTRIBUTE_NAMES])};
468
+ const INTERACTIVE_TAG_NAMES = ${JSON.stringify([...INTERACTIVE_TAG_NAMES])};
469
+ const INTERACTIVE_ROLE_NAMES = ${JSON.stringify([...INTERACTIVE_ROLE_NAMES])};
470
+ const filterSemanticClasses = ${filterSemanticClasses.toString()};
471
+ const isObfuscatedClass = ${isObfuscatedClass.toString()};
401
472
 
402
- function logAction(entry) {
403
- appendFileSync(ACTIONS_LOG, JSON.stringify(entry) + '\\n');
404
- }
473
+ ${installSessionTelemetry.toString()}
474
+
475
+ function logAction(entry) {
476
+ appendFileSync(ACTIONS_LOG, JSON.stringify(entry) + '\\n');
477
+ }
405
478
 
406
479
  function logNetwork(entry) {
407
480
  appendFileSync(NETWORK_LOG, JSON.stringify(entry) + '\\n');
@@ -423,7 +496,7 @@ function childLog(level, event, data = {}) {
423
496
 
424
497
  const browser = await chromium.launch({
425
498
  headless: ${!headed},
426
- args: ['--disable-blink-features=AutomationControlled', '--remote-debugging-port=${port}', '--remote-debugging-address=127.0.0.1', '--no-focus-on-check'],
499
+ args: ['--disable-blink-features=AutomationControlled', '--remote-debugging-port=${port}', '--remote-debugging-address=127.0.0.1', '--no-focus-on-check'${windowPositionArg}],
427
500
  });
428
501
 
429
502
  browser.on('disconnected', () => {
@@ -437,6 +510,7 @@ const context = await browser.newContext({
437
510
  });
438
511
 
439
512
  const page = await context.newPage();
513
+ ${windowBoundsSetupCode}
440
514
  page.setDefaultTimeout(30000);
441
515
  page.setDefaultNavigationTimeout(45000);
442
516