libretto 0.5.0 → 0.5.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 (122) hide show
  1. package/README.md +109 -35
  2. package/dist/cli/cli.js +22 -97
  3. package/dist/cli/commands/browser.js +86 -59
  4. package/dist/cli/commands/execution.js +199 -86
  5. package/dist/cli/commands/init.js +34 -29
  6. package/dist/cli/commands/logs.js +4 -5
  7. package/dist/cli/commands/shared.js +30 -29
  8. package/dist/cli/commands/snapshot.js +26 -39
  9. package/dist/cli/core/ai-config.js +21 -4
  10. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  11. package/dist/cli/core/browser.js +207 -37
  12. package/dist/cli/core/context.js +4 -1
  13. package/dist/cli/core/session-telemetry.js +434 -174
  14. package/dist/cli/core/session.js +21 -8
  15. package/dist/cli/core/snapshot-analyzer.js +14 -31
  16. package/dist/cli/core/snapshot-api-config.js +2 -6
  17. package/dist/cli/core/telemetry.js +20 -4
  18. package/dist/cli/framework/simple-cli.js +45 -25
  19. package/dist/cli/router.js +14 -21
  20. package/dist/cli/workers/run-integration-runtime.js +24 -5
  21. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  22. package/dist/cli/workers/run-integration-worker.js +1 -4
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.js +7 -10
  25. package/dist/runtime/download/download.js +5 -1
  26. package/dist/runtime/extract/extract.js +11 -2
  27. package/dist/runtime/network/network.js +8 -1
  28. package/dist/runtime/recovery/agent.js +6 -2
  29. package/dist/runtime/recovery/errors.js +3 -1
  30. package/dist/runtime/recovery/recovery.js +3 -1
  31. package/dist/shared/condense-dom/condense-dom.js +17 -69
  32. package/dist/shared/config/config.d.ts +1 -9
  33. package/dist/shared/config/config.js +0 -18
  34. package/dist/shared/config/index.d.ts +2 -1
  35. package/dist/shared/config/index.js +0 -10
  36. package/dist/shared/debug/pause.js +9 -3
  37. package/dist/shared/dom-semantics.d.ts +8 -0
  38. package/dist/shared/dom-semantics.js +69 -0
  39. package/dist/shared/instrumentation/instrument.js +101 -5
  40. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  41. package/dist/shared/llm/client.js +3 -1
  42. package/dist/shared/logger/index.js +4 -1
  43. package/dist/shared/run/api.js +3 -1
  44. package/dist/shared/run/browser.js +47 -3
  45. package/dist/shared/state/session-state.d.ts +2 -1
  46. package/dist/shared/state/session-state.js +5 -2
  47. package/dist/shared/visualization/ghost-cursor.js +36 -14
  48. package/dist/shared/visualization/highlight.js +9 -6
  49. package/dist/shared/workflow/workflow.d.ts +4 -5
  50. package/dist/shared/workflow/workflow.js +3 -5
  51. package/package.json +6 -2
  52. package/scripts/check-skills-sync.mjs +25 -0
  53. package/scripts/compare-eval-summary.mjs +47 -0
  54. package/scripts/postinstall.mjs +15 -15
  55. package/scripts/prepare-release.sh +97 -0
  56. package/scripts/skills-libretto.mjs +103 -0
  57. package/scripts/summarize-evals.mjs +135 -0
  58. package/scripts/sync-skills.mjs +12 -0
  59. package/skills/libretto/SKILL.md +132 -54
  60. package/skills/libretto/references/action-logs.md +101 -0
  61. package/skills/libretto/references/auth-profiles.md +1 -2
  62. package/skills/libretto/references/code-generation-rules.md +210 -0
  63. package/skills/libretto/references/configuration-file-reference.md +53 -0
  64. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  65. package/skills/libretto/references/site-security-review.md +143 -0
  66. package/src/cli/cli.ts +23 -110
  67. package/src/cli/commands/browser.ts +94 -70
  68. package/src/cli/commands/execution.ts +233 -102
  69. package/src/cli/commands/init.ts +37 -33
  70. package/src/cli/commands/logs.ts +7 -7
  71. package/src/cli/commands/shared.ts +36 -37
  72. package/src/cli/commands/snapshot.ts +44 -59
  73. package/src/cli/core/ai-config.ts +24 -4
  74. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  75. package/src/cli/core/browser.ts +260 -49
  76. package/src/cli/core/context.ts +7 -2
  77. package/src/cli/core/session-telemetry.ts +449 -197
  78. package/src/cli/core/session.ts +21 -7
  79. package/src/cli/core/snapshot-analyzer.ts +26 -46
  80. package/src/cli/core/snapshot-api-config.ts +170 -175
  81. package/src/cli/core/telemetry.ts +39 -4
  82. package/src/cli/framework/simple-cli.ts +144 -77
  83. package/src/cli/router.ts +13 -21
  84. package/src/cli/workers/run-integration-runtime.ts +36 -9
  85. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  86. package/src/cli/workers/run-integration-worker.ts +1 -4
  87. package/src/index.ts +73 -66
  88. package/src/runtime/download/download.ts +62 -58
  89. package/src/runtime/download/index.ts +5 -5
  90. package/src/runtime/extract/extract.ts +71 -61
  91. package/src/runtime/network/index.ts +3 -3
  92. package/src/runtime/network/network.ts +99 -93
  93. package/src/runtime/recovery/agent.ts +217 -212
  94. package/src/runtime/recovery/errors.ts +107 -104
  95. package/src/runtime/recovery/index.ts +3 -3
  96. package/src/runtime/recovery/recovery.ts +38 -35
  97. package/src/shared/condense-dom/condense-dom.ts +27 -82
  98. package/src/shared/config/config.ts +0 -19
  99. package/src/shared/config/index.ts +0 -5
  100. package/src/shared/debug/pause.ts +57 -51
  101. package/src/shared/dom-semantics.ts +68 -0
  102. package/src/shared/instrumentation/errors.ts +64 -62
  103. package/src/shared/instrumentation/index.ts +5 -5
  104. package/src/shared/instrumentation/instrument.ts +339 -209
  105. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  106. package/src/shared/llm/client.ts +181 -174
  107. package/src/shared/llm/types.ts +39 -39
  108. package/src/shared/logger/index.ts +11 -4
  109. package/src/shared/logger/logger.ts +312 -306
  110. package/src/shared/logger/sinks.ts +118 -114
  111. package/src/shared/paths/paths.ts +50 -49
  112. package/src/shared/paths/repo-root.ts +17 -17
  113. package/src/shared/run/api.ts +5 -1
  114. package/src/shared/run/browser.ts +65 -3
  115. package/src/shared/state/index.ts +9 -9
  116. package/src/shared/state/session-state.ts +46 -43
  117. package/src/shared/visualization/ghost-cursor.ts +180 -149
  118. package/src/shared/visualization/highlight.ts +89 -86
  119. package/src/shared/visualization/index.ts +13 -13
  120. package/src/shared/workflow/workflow.ts +19 -25
  121. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
  122. package/skills/libretto/references/user-action-log.md +0 -31
@@ -3,142 +3,146 @@ import * as path from "node:path";
3
3
  import type { LoggerSink } from "./logger.js";
4
4
 
5
5
  export function createFileLogSink({
6
- filePath,
6
+ filePath,
7
7
  }: {
8
- filePath: string;
8
+ filePath: string;
9
9
  }): LoggerSink {
10
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
10
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
11
11
 
12
- const writeStream = fs.createWriteStream(filePath, { flags: "a" });
12
+ const writeStream = fs.createWriteStream(filePath, { flags: "a" });
13
13
 
14
- return {
15
- write: ({ id, scope, level, event, data, options }) => {
16
- if (writeStream.destroyed || writeStream.writableEnded) {
17
- return;
18
- }
19
- const timestamp = options?.timestamp || new Date();
14
+ return {
15
+ write: ({ id, scope, level, event, data, options }) => {
16
+ if (writeStream.destroyed || writeStream.writableEnded) {
17
+ return;
18
+ }
19
+ const timestamp = options?.timestamp || new Date();
20
20
 
21
- const logEntry = {
22
- timestamp: timestamp.toISOString(),
23
- id,
24
- level,
25
- scope,
26
- event,
27
- data,
28
- };
21
+ const logEntry = {
22
+ timestamp: timestamp.toISOString(),
23
+ id,
24
+ level,
25
+ scope,
26
+ event,
27
+ data,
28
+ };
29
29
 
30
- const jsonLine = JSON.stringify(logEntry) + "\n";
30
+ const jsonLine = JSON.stringify(logEntry) + "\n";
31
31
 
32
- try {
33
- writeStream.write(jsonLine, (error) => {
34
- if (error) {
35
- console.error("Failed to write to log file:", error);
36
- console[level]({ id, scope, event, data, timestamp });
37
- }
38
- });
39
- } catch (error) {
40
- console.error("Failed to write to log file:", error);
41
- console[level]({ id, scope, event, data, timestamp });
42
- }
43
- },
44
- flush: () =>
45
- new Promise<void>((resolve, reject) => {
46
- if (!writeStream.writable || writeStream.writableEnded || writeStream.destroyed) {
47
- resolve();
48
- return;
49
- }
50
- writeStream.write("", (error) => {
51
- if (error) {
52
- reject(error);
53
- } else {
54
- resolve();
55
- }
56
- });
57
- }),
58
- close: () =>
59
- new Promise<void>((resolve) => {
60
- if (writeStream.destroyed || writeStream.closed) {
61
- resolve();
62
- return;
63
- }
64
- let settled = false;
65
- const done = () => {
66
- if (settled) return;
67
- settled = true;
68
- resolve();
69
- };
70
- writeStream.once("finish", done);
71
- writeStream.once("close", done);
72
- writeStream.once("error", done);
73
- try {
74
- writeStream.end();
75
- } catch {
76
- done();
77
- }
78
- }),
79
- };
32
+ try {
33
+ writeStream.write(jsonLine, (error) => {
34
+ if (error) {
35
+ console.error("Failed to write to log file:", error);
36
+ console[level]({ id, scope, event, data, timestamp });
37
+ }
38
+ });
39
+ } catch (error) {
40
+ console.error("Failed to write to log file:", error);
41
+ console[level]({ id, scope, event, data, timestamp });
42
+ }
43
+ },
44
+ flush: () =>
45
+ new Promise<void>((resolve, reject) => {
46
+ if (
47
+ !writeStream.writable ||
48
+ writeStream.writableEnded ||
49
+ writeStream.destroyed
50
+ ) {
51
+ resolve();
52
+ return;
53
+ }
54
+ writeStream.write("", (error) => {
55
+ if (error) {
56
+ reject(error);
57
+ } else {
58
+ resolve();
59
+ }
60
+ });
61
+ }),
62
+ close: () =>
63
+ new Promise<void>((resolve) => {
64
+ if (writeStream.destroyed || writeStream.closed) {
65
+ resolve();
66
+ return;
67
+ }
68
+ let settled = false;
69
+ const done = () => {
70
+ if (settled) return;
71
+ settled = true;
72
+ resolve();
73
+ };
74
+ writeStream.once("finish", done);
75
+ writeStream.once("close", done);
76
+ writeStream.once("error", done);
77
+ try {
78
+ writeStream.end();
79
+ } catch {
80
+ done();
81
+ }
82
+ }),
83
+ };
80
84
  }
81
85
 
82
86
  // ANSI color codes
83
87
  const colors = {
84
- reset: "\x1b[0m",
85
- gray: "\x1b[90m",
86
- red: "\x1b[31m",
87
- yellow: "\x1b[33m",
88
- blue: "\x1b[34m",
89
- cyan: "\x1b[36m",
88
+ reset: "\x1b[0m",
89
+ gray: "\x1b[90m",
90
+ red: "\x1b[31m",
91
+ yellow: "\x1b[33m",
92
+ blue: "\x1b[34m",
93
+ cyan: "\x1b[36m",
90
94
  };
91
95
 
92
96
  function formatTimestamp(date: Date): string {
93
- return date.toISOString().replace("T", " ").replace("Z", "");
97
+ return date.toISOString().replace("T", " ").replace("Z", "");
94
98
  }
95
99
 
96
100
  export const prettyConsoleSink: LoggerSink = {
97
- write: ({ scope, level, event, data, options }) => {
98
- const timestamp = `${colors.gray}${formatTimestamp(options?.timestamp || new Date())}${colors.reset}`;
99
- const levelColor =
100
- level === "error"
101
- ? colors.red
102
- : level === "warn"
103
- ? colors.yellow
104
- : colors.blue;
105
- const coloredScope = scope ? `${colors.cyan}[${scope}]${colors.reset}` : "";
101
+ write: ({ scope, level, event, data, options }) => {
102
+ const timestamp = `${colors.gray}${formatTimestamp(options?.timestamp || new Date())}${colors.reset}`;
103
+ const levelColor =
104
+ level === "error"
105
+ ? colors.red
106
+ : level === "warn"
107
+ ? colors.yellow
108
+ : colors.blue;
109
+ const coloredScope = scope ? `${colors.cyan}[${scope}]${colors.reset}` : "";
106
110
 
107
- const logPrefix = `${timestamp} ${levelColor}${level.toUpperCase()}${colors.reset} ${coloredScope} ${event}`;
111
+ const logPrefix = `${timestamp} ${levelColor}${level.toUpperCase()}${colors.reset} ${coloredScope} ${event}`;
108
112
 
109
- if (level === "error" && data.error) {
110
- const { error, ...otherData } = data;
111
- console.error(logPrefix);
112
- if (error.stack) {
113
- console.error(` ${error.stack}`);
114
- } else if (error.type && error.message) {
115
- console.error(` ${error.type}: ${error.message}`);
116
- }
117
- if (Object.keys(otherData).length > 0) {
118
- console.error(JSON.stringify(otherData, null, 2));
119
- }
120
- } else {
121
- console[level](logPrefix);
122
- if (Object.keys(data).length > 0) {
123
- console[level](JSON.stringify(data, null, 2));
124
- }
125
- }
126
- },
113
+ if (level === "error" && data.error) {
114
+ const { error, ...otherData } = data;
115
+ console.error(logPrefix);
116
+ if (error.stack) {
117
+ console.error(` ${error.stack}`);
118
+ } else if (error.type && error.message) {
119
+ console.error(` ${error.type}: ${error.message}`);
120
+ }
121
+ if (Object.keys(otherData).length > 0) {
122
+ console.error(JSON.stringify(otherData, null, 2));
123
+ }
124
+ } else {
125
+ console[level](logPrefix);
126
+ if (Object.keys(data).length > 0) {
127
+ console[level](JSON.stringify(data, null, 2));
128
+ }
129
+ }
130
+ },
127
131
  };
128
132
 
129
133
  export const jsonlConsoleSink: LoggerSink = {
130
- write: ({ id, scope, level, event, data, options }) => {
131
- const timestamp = options?.timestamp || new Date();
134
+ write: ({ id, scope, level, event, data, options }) => {
135
+ const timestamp = options?.timestamp || new Date();
132
136
 
133
- const logEntry = {
134
- timestamp: timestamp.toISOString(),
135
- id,
136
- level,
137
- scope: scope || undefined,
138
- event,
139
- data,
140
- };
137
+ const logEntry = {
138
+ timestamp: timestamp.toISOString(),
139
+ id,
140
+ level,
141
+ scope: scope || undefined,
142
+ event,
143
+ data,
144
+ };
141
145
 
142
- console.log(JSON.stringify(logEntry));
143
- },
146
+ console.log(JSON.stringify(logEntry));
147
+ },
144
148
  };
@@ -11,99 +11,100 @@ const PAUSED_SIGNAL_SUFFIX = "paused";
11
11
  const RESUME_SIGNAL_SUFFIX = "resume";
12
12
 
13
13
  function getLibrettoRoot(cwd: string = process.cwd()): string {
14
- return join(resolveLibrettoRepoRoot(cwd), LIBRETTO_DIRNAME);
14
+ return join(resolveLibrettoRepoRoot(cwd), LIBRETTO_DIRNAME);
15
15
  }
16
16
 
17
17
  function getLibrettoSessionsDir(cwd: string = process.cwd()): string {
18
- return join(getLibrettoRoot(cwd), LIBRETTO_SESSIONS_DIRNAME);
18
+ return join(getLibrettoRoot(cwd), LIBRETTO_SESSIONS_DIRNAME);
19
19
  }
20
20
 
21
21
  function getLibrettoSessionDir(
22
- sessionName: string,
23
- cwd: string = process.cwd(),
22
+ sessionName: string,
23
+ cwd: string = process.cwd(),
24
24
  ): string {
25
- return join(getLibrettoSessionsDir(cwd), sessionName);
25
+ return join(getLibrettoSessionsDir(cwd), sessionName);
26
26
  }
27
27
 
28
28
  function getLibrettoSessionStatePath(
29
- sessionName: string,
30
- cwd: string = process.cwd(),
29
+ sessionName: string,
30
+ cwd: string = process.cwd(),
31
31
  ): string {
32
- return join(getLibrettoSessionDir(sessionName, cwd), SESSION_STATE_FILENAME);
32
+ return join(getLibrettoSessionDir(sessionName, cwd), SESSION_STATE_FILENAME);
33
33
  }
34
34
 
35
35
  export function getLibrettoPauseSignalDir(
36
- sessionName: string,
37
- cwd: string = process.cwd(),
36
+ sessionName: string,
37
+ cwd: string = process.cwd(),
38
38
  ): string {
39
- return getLibrettoSessionDir(sessionName, cwd);
39
+ return getLibrettoSessionDir(sessionName, cwd);
40
40
  }
41
41
 
42
42
  function getLibrettoRunnerLogDir(
43
- sessionName: string,
44
- cwd: string = process.cwd(),
43
+ sessionName: string,
44
+ cwd: string = process.cwd(),
45
45
  ): string {
46
- return join(getLibrettoSessionDir(sessionName, cwd), RUNNER_LOG_DIRNAME);
46
+ return join(getLibrettoSessionDir(sessionName, cwd), RUNNER_LOG_DIRNAME);
47
47
  }
48
48
 
49
49
  export function getRunnerLogPathForDir(logDir: string): string {
50
- return join(logDir, RUNNER_LOG_FILENAME);
50
+ return join(logDir, RUNNER_LOG_FILENAME);
51
51
  }
52
52
 
53
53
  export function getPauseSignalPathForDir(
54
- signalDir: string,
55
- sessionName: string,
56
- signal: "paused" | "resume",
54
+ signalDir: string,
55
+ sessionName: string,
56
+ signal: "paused" | "resume",
57
57
  ): string {
58
- const suffix = signal === "paused" ? PAUSED_SIGNAL_SUFFIX : RESUME_SIGNAL_SUFFIX;
59
- return join(signalDir, `${sessionName}.${suffix}`);
58
+ const suffix =
59
+ signal === "paused" ? PAUSED_SIGNAL_SUFFIX : RESUME_SIGNAL_SUFFIX;
60
+ return join(signalDir, `${sessionName}.${suffix}`);
60
61
  }
61
62
 
62
63
  export function getLibrettoPausedSignalPath(
63
- sessionName: string,
64
- cwd: string = process.cwd(),
64
+ sessionName: string,
65
+ cwd: string = process.cwd(),
65
66
  ): string {
66
- return getPauseSignalPathForDir(
67
- getLibrettoPauseSignalDir(sessionName, cwd),
68
- sessionName,
69
- "paused",
70
- );
67
+ return getPauseSignalPathForDir(
68
+ getLibrettoPauseSignalDir(sessionName, cwd),
69
+ sessionName,
70
+ "paused",
71
+ );
71
72
  }
72
73
 
73
74
  export function getLibrettoResumeSignalPath(
74
- sessionName: string,
75
- cwd: string = process.cwd(),
75
+ sessionName: string,
76
+ cwd: string = process.cwd(),
76
77
  ): string {
77
- return getPauseSignalPathForDir(
78
- getLibrettoPauseSignalDir(sessionName, cwd),
79
- sessionName,
80
- "resume",
81
- );
78
+ return getPauseSignalPathForDir(
79
+ getLibrettoPauseSignalDir(sessionName, cwd),
80
+ sessionName,
81
+ "resume",
82
+ );
82
83
  }
83
84
 
84
85
  export function ensureLibrettoSessionStatePath(
85
- sessionName: string,
86
- cwd: string = process.cwd(),
86
+ sessionName: string,
87
+ cwd: string = process.cwd(),
87
88
  ): string {
88
- const filePath = getLibrettoSessionStatePath(sessionName, cwd);
89
- mkdirSync(dirname(filePath), { recursive: true });
90
- return filePath;
89
+ const filePath = getLibrettoSessionStatePath(sessionName, cwd);
90
+ mkdirSync(dirname(filePath), { recursive: true });
91
+ return filePath;
91
92
  }
92
93
 
93
94
  export function ensureLibrettoPauseSignalDir(
94
- sessionName: string,
95
- cwd: string = process.cwd(),
95
+ sessionName: string,
96
+ cwd: string = process.cwd(),
96
97
  ): string {
97
- const dir = getLibrettoPauseSignalDir(sessionName, cwd);
98
- mkdirSync(dir, { recursive: true });
99
- return dir;
98
+ const dir = getLibrettoPauseSignalDir(sessionName, cwd);
99
+ mkdirSync(dir, { recursive: true });
100
+ return dir;
100
101
  }
101
102
 
102
103
  export function ensureLibrettoRunnerLogDir(
103
- sessionName: string,
104
- cwd: string = process.cwd(),
104
+ sessionName: string,
105
+ cwd: string = process.cwd(),
105
106
  ): string {
106
- const dir = getLibrettoRunnerLogDir(sessionName, cwd);
107
- mkdirSync(dir, { recursive: true });
108
- return dir;
107
+ const dir = getLibrettoRunnerLogDir(sessionName, cwd);
108
+ mkdirSync(dir, { recursive: true });
109
+ return dir;
109
110
  }
@@ -4,24 +4,24 @@ import { resolve } from "node:path";
4
4
  const repoRootCache = new Map<string, string>();
5
5
 
6
6
  export function resolveLibrettoRepoRoot(cwd: string = process.cwd()): string {
7
- const override = process.env.LIBRETTO_REPO_ROOT?.trim();
8
- if (override) {
9
- return resolve(override);
10
- }
7
+ const override = process.env.LIBRETTO_REPO_ROOT?.trim();
8
+ if (override) {
9
+ return resolve(override);
10
+ }
11
11
 
12
- const normalizedCwd = resolve(cwd);
13
- const cached = repoRootCache.get(normalizedCwd);
14
- if (cached) {
15
- return cached;
16
- }
12
+ const normalizedCwd = resolve(cwd);
13
+ const cached = repoRootCache.get(normalizedCwd);
14
+ if (cached) {
15
+ return cached;
16
+ }
17
17
 
18
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
19
- cwd: normalizedCwd,
20
- encoding: "utf-8",
21
- });
18
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
19
+ cwd: normalizedCwd,
20
+ encoding: "utf-8",
21
+ });
22
22
 
23
- const repoRoot =
24
- result.status === 0 && result.stdout ? result.stdout.trim() : normalizedCwd;
25
- repoRootCache.set(normalizedCwd, repoRoot);
26
- return repoRoot;
23
+ const repoRoot =
24
+ result.status === 0 && result.stdout ? result.stdout.trim() : normalizedCwd;
25
+ repoRootCache.set(normalizedCwd, repoRoot);
26
+ return repoRoot;
27
27
  }
@@ -1,2 +1,6 @@
1
1
  // --- Browser ---
2
- export { launchBrowser, type LaunchBrowserArgs, type BrowserSession } from "./browser.js";
2
+ export {
3
+ launchBrowser,
4
+ type LaunchBrowserArgs,
5
+ type BrowserSession,
6
+ } from "./browser.js";
@@ -1,8 +1,17 @@
1
- import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
1
+ import {
2
+ chromium,
3
+ type Browser,
4
+ type BrowserContext,
5
+ type Page,
6
+ } from "playwright";
2
7
  import { createServer } from "node:net";
3
8
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
9
  import { ensureLibrettoSessionStatePath } from "../paths/paths.js";
5
- import { SESSION_STATE_VERSION, SessionStateFileSchema } from "../state/session-state.js";
10
+ import {
11
+ SESSION_STATE_VERSION,
12
+ SessionStateFileSchema,
13
+ } from "../state/session-state.js";
14
+ import { readLibrettoConfig } from "../../cli/core/ai-config.js";
6
15
 
7
16
  async function pickFreePort(): Promise<number> {
8
17
  return await new Promise((resolve, reject) => {
@@ -36,6 +45,53 @@ export type BrowserSession = {
36
45
  close: () => Promise<void>;
37
46
  };
38
47
 
48
+ function resolveWindowPosition(): { x: number; y: number } | undefined {
49
+ return readLibrettoConfig().windowPosition;
50
+ }
51
+
52
+ async function applyWindowPosition(
53
+ browser: Browser,
54
+ context: BrowserContext,
55
+ page: Page,
56
+ windowPosition: { x: number; y: number } | undefined,
57
+ ): Promise<void> {
58
+ if (!windowPosition) {
59
+ return;
60
+ }
61
+
62
+ const requestedBounds = {
63
+ left: windowPosition.x,
64
+ top: windowPosition.y,
65
+ windowState: "normal" as const,
66
+ };
67
+
68
+ const pageCdp = await context.newCDPSession(page);
69
+ let browserCdp:
70
+ | Awaited<ReturnType<Browser["newBrowserCDPSession"]>>
71
+ | undefined;
72
+ try {
73
+ const targetInfo = await pageCdp.send("Target.getTargetInfo");
74
+ const targetId = (
75
+ targetInfo as { targetInfo?: { targetId?: string } }
76
+ ).targetInfo?.targetId;
77
+ browserCdp = await browser.newBrowserCDPSession();
78
+ const windowResult = await browserCdp.send(
79
+ "Browser.getWindowForTarget",
80
+ targetId ? { targetId } : {},
81
+ );
82
+ await browserCdp.send("Browser.setWindowBounds", {
83
+ windowId: windowResult.windowId,
84
+ bounds: requestedBounds,
85
+ });
86
+ await new Promise((resolve) => setTimeout(resolve, 250));
87
+ } catch {
88
+ // Best-effort: window positioning should not prevent browser launch.
89
+ } finally {
90
+ await pageCdp.detach().catch(() => {});
91
+ await browserCdp?.detach().catch(() => {});
92
+ }
93
+ }
94
+
39
95
  export async function launchBrowser({
40
96
  sessionName,
41
97
  headless = false,
@@ -43,12 +99,16 @@ export async function launchBrowser({
43
99
  storageStatePath,
44
100
  }: LaunchBrowserArgs): Promise<BrowserSession> {
45
101
  const debugPort = await pickFreePort();
102
+ const windowPosition = headless ? undefined : resolveWindowPosition();
46
103
  const browser = await chromium.launch({
47
104
  headless,
48
105
  args: [
49
106
  "--disable-blink-features=AutomationControlled",
50
107
  `--remote-debugging-port=${debugPort}`,
51
108
  "--no-focus-on-check",
109
+ ...(windowPosition
110
+ ? [`--window-position=${windowPosition.x},${windowPosition.y}`]
111
+ : []),
52
112
  ],
53
113
  });
54
114
 
@@ -57,6 +117,7 @@ export async function launchBrowser({
57
117
  ...(storageStatePath ? { storageState: storageStatePath } : {}),
58
118
  });
59
119
  const page = await context.newPage();
120
+ await applyWindowPosition(browser, context, page, windowPosition);
60
121
  page.setDefaultTimeout(30_000);
61
122
  page.setDefaultNavigationTimeout(45_000);
62
123
 
@@ -65,7 +126,8 @@ export async function launchBrowser({
65
126
  ? (JSON.parse(readFileSync(metadataPath, "utf-8")) as unknown)
66
127
  : undefined;
67
128
 
68
- const parsedExistingState = SessionStateFileSchema.safeParse(existingStateRaw);
129
+ const parsedExistingState =
130
+ SessionStateFileSchema.safeParse(existingStateRaw);
69
131
 
70
132
  writeFileSync(
71
133
  metadataPath,
@@ -1,11 +1,11 @@
1
1
  export {
2
- SESSION_STATE_VERSION,
3
- SessionStatusSchema,
4
- SessionStateFileSchema,
5
- parseSessionStateData,
6
- parseSessionStateContent,
7
- serializeSessionState,
8
- type SessionStatus,
9
- type SessionState,
10
- type SessionStateFile,
2
+ SESSION_STATE_VERSION,
3
+ SessionStatusSchema,
4
+ SessionStateFileSchema,
5
+ parseSessionStateData,
6
+ parseSessionStateContent,
7
+ serializeSessionState,
8
+ type SessionStatus,
9
+ type SessionState,
10
+ type SessionStateFile,
11
11
  } from "./session-state.js";