libretto 0.1.5 → 0.2.1

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 (168) hide show
  1. package/README.md +215 -17
  2. package/bin/libretto.mjs +18 -0
  3. package/dist/cli/cli.js +203 -0
  4. package/dist/cli/commands/ai.js +21 -0
  5. package/dist/cli/commands/browser.js +59 -0
  6. package/dist/cli/commands/execution.js +422 -0
  7. package/dist/cli/commands/logs.js +93 -0
  8. package/dist/cli/commands/snapshot.js +106 -0
  9. package/dist/cli/core/ai-config.js +149 -0
  10. package/dist/cli/core/browser.js +523 -0
  11. package/dist/cli/core/context.js +113 -0
  12. package/dist/cli/core/pause-signals.js +29 -0
  13. package/dist/cli/core/session-telemetry.js +491 -0
  14. package/dist/cli/core/session.js +183 -0
  15. package/dist/cli/core/snapshot-analyzer.js +492 -0
  16. package/dist/cli/core/telemetry.js +362 -0
  17. package/dist/cli/index.js +13 -0
  18. package/dist/cli/workers/run-integration-runtime.js +222 -0
  19. package/dist/cli/workers/run-integration-worker-protocol.js +0 -0
  20. package/dist/cli/workers/run-integration-worker.js +83 -0
  21. package/dist/index.cjs +123 -0
  22. package/dist/index.d.cts +19 -0
  23. package/dist/index.d.ts +19 -0
  24. package/dist/index.js +107 -0
  25. package/dist/runtime/download/download.cjs +70 -0
  26. package/dist/runtime/download/download.d.cts +35 -0
  27. package/dist/runtime/download/download.d.ts +35 -0
  28. package/dist/runtime/download/download.js +45 -0
  29. package/dist/runtime/download/index.cjs +30 -0
  30. package/dist/runtime/download/index.d.cts +3 -0
  31. package/dist/runtime/download/index.d.ts +3 -0
  32. package/dist/runtime/download/index.js +8 -0
  33. package/dist/runtime/extract/extract.cjs +88 -0
  34. package/dist/runtime/extract/extract.d.cts +23 -0
  35. package/dist/runtime/extract/extract.d.ts +23 -0
  36. package/dist/runtime/extract/extract.js +64 -0
  37. package/dist/runtime/extract/index.cjs +28 -0
  38. package/dist/runtime/extract/index.d.cts +5 -0
  39. package/dist/runtime/extract/index.d.ts +5 -0
  40. package/dist/runtime/extract/index.js +4 -0
  41. package/dist/runtime/network/index.cjs +28 -0
  42. package/dist/runtime/network/index.d.cts +4 -0
  43. package/dist/runtime/network/index.d.ts +4 -0
  44. package/dist/runtime/network/index.js +6 -0
  45. package/dist/runtime/network/network.cjs +91 -0
  46. package/dist/runtime/network/network.d.cts +28 -0
  47. package/dist/runtime/network/network.d.ts +28 -0
  48. package/dist/runtime/network/network.js +67 -0
  49. package/dist/runtime/recovery/agent.cjs +223 -0
  50. package/dist/runtime/recovery/agent.d.cts +13 -0
  51. package/dist/runtime/recovery/agent.d.ts +13 -0
  52. package/dist/runtime/recovery/agent.js +199 -0
  53. package/dist/runtime/recovery/errors.cjs +124 -0
  54. package/dist/runtime/recovery/errors.d.cts +31 -0
  55. package/dist/runtime/recovery/errors.d.ts +31 -0
  56. package/dist/runtime/recovery/errors.js +100 -0
  57. package/dist/runtime/recovery/index.cjs +34 -0
  58. package/dist/runtime/recovery/index.d.cts +7 -0
  59. package/dist/runtime/recovery/index.d.ts +7 -0
  60. package/dist/runtime/recovery/index.js +10 -0
  61. package/dist/runtime/recovery/recovery.cjs +55 -0
  62. package/dist/runtime/recovery/recovery.d.cts +12 -0
  63. package/dist/runtime/recovery/recovery.d.ts +12 -0
  64. package/dist/runtime/recovery/recovery.js +31 -0
  65. package/dist/shared/config/config.cjs +44 -0
  66. package/dist/shared/config/config.d.cts +10 -0
  67. package/dist/shared/config/config.d.ts +10 -0
  68. package/dist/shared/config/config.js +18 -0
  69. package/dist/shared/config/index.cjs +32 -0
  70. package/dist/shared/config/index.d.cts +1 -0
  71. package/dist/shared/config/index.d.ts +1 -0
  72. package/dist/shared/config/index.js +10 -0
  73. package/dist/shared/debug/index.cjs +32 -0
  74. package/dist/shared/debug/index.d.cts +2 -0
  75. package/dist/shared/debug/index.d.ts +2 -0
  76. package/dist/shared/debug/index.js +10 -0
  77. package/dist/shared/debug/pause.cjs +56 -0
  78. package/dist/shared/debug/pause.d.cts +23 -0
  79. package/dist/shared/debug/pause.d.ts +23 -0
  80. package/dist/shared/debug/pause.js +30 -0
  81. package/dist/shared/instrumentation/errors.cjs +81 -0
  82. package/dist/shared/instrumentation/errors.d.cts +12 -0
  83. package/dist/shared/instrumentation/errors.d.ts +12 -0
  84. package/dist/shared/instrumentation/errors.js +57 -0
  85. package/dist/shared/instrumentation/index.cjs +35 -0
  86. package/dist/shared/instrumentation/index.d.cts +6 -0
  87. package/dist/shared/instrumentation/index.d.ts +6 -0
  88. package/dist/shared/instrumentation/index.js +12 -0
  89. package/dist/shared/instrumentation/instrument.cjs +206 -0
  90. package/dist/shared/instrumentation/instrument.d.cts +32 -0
  91. package/dist/shared/instrumentation/instrument.d.ts +32 -0
  92. package/dist/shared/instrumentation/instrument.js +190 -0
  93. package/dist/shared/llm/client.cjs +139 -0
  94. package/dist/shared/llm/client.d.cts +6 -0
  95. package/dist/shared/llm/client.d.ts +6 -0
  96. package/dist/shared/llm/client.js +115 -0
  97. package/dist/shared/llm/index.cjs +28 -0
  98. package/dist/shared/llm/index.d.cts +3 -0
  99. package/dist/shared/llm/index.d.ts +3 -0
  100. package/dist/shared/llm/index.js +4 -0
  101. package/dist/shared/llm/types.cjs +16 -0
  102. package/dist/shared/llm/types.d.cts +34 -0
  103. package/dist/shared/llm/types.d.ts +34 -0
  104. package/dist/shared/llm/types.js +0 -0
  105. package/dist/shared/logger/index.cjs +37 -0
  106. package/dist/shared/logger/index.d.cts +2 -0
  107. package/dist/shared/logger/index.d.ts +2 -0
  108. package/dist/shared/logger/index.js +13 -0
  109. package/dist/shared/logger/logger.cjs +213 -0
  110. package/dist/shared/logger/logger.d.cts +82 -0
  111. package/dist/shared/logger/logger.d.ts +82 -0
  112. package/dist/shared/logger/logger.js +188 -0
  113. package/dist/shared/logger/sinks.cjs +160 -0
  114. package/dist/shared/logger/sinks.d.cts +9 -0
  115. package/dist/shared/logger/sinks.d.ts +9 -0
  116. package/dist/shared/logger/sinks.js +124 -0
  117. package/dist/shared/paths/paths.cjs +104 -0
  118. package/dist/shared/paths/paths.d.cts +10 -0
  119. package/dist/shared/paths/paths.d.ts +10 -0
  120. package/dist/shared/paths/paths.js +73 -0
  121. package/dist/shared/run/api.cjs +35 -0
  122. package/dist/shared/run/api.d.cts +3 -0
  123. package/dist/shared/run/api.d.ts +3 -0
  124. package/dist/shared/run/api.js +12 -0
  125. package/dist/shared/run/browser.cjs +98 -0
  126. package/dist/shared/run/browser.d.cts +22 -0
  127. package/dist/shared/run/browser.d.ts +22 -0
  128. package/dist/shared/run/browser.js +74 -0
  129. package/dist/shared/state/index.cjs +38 -0
  130. package/dist/shared/state/index.d.cts +2 -0
  131. package/dist/shared/state/index.d.ts +2 -0
  132. package/dist/shared/state/index.js +16 -0
  133. package/dist/shared/state/session-state.cjs +85 -0
  134. package/dist/shared/state/session-state.d.cts +34 -0
  135. package/dist/shared/state/session-state.d.ts +34 -0
  136. package/dist/shared/state/session-state.js +56 -0
  137. package/dist/shared/visualization/ghost-cursor.cjs +174 -0
  138. package/dist/shared/visualization/ghost-cursor.d.cts +37 -0
  139. package/dist/shared/visualization/ghost-cursor.d.ts +37 -0
  140. package/dist/shared/visualization/ghost-cursor.js +145 -0
  141. package/dist/shared/visualization/highlight.cjs +134 -0
  142. package/dist/shared/visualization/highlight.d.cts +22 -0
  143. package/dist/shared/visualization/highlight.d.ts +22 -0
  144. package/dist/shared/visualization/highlight.js +108 -0
  145. package/dist/shared/visualization/index.cjs +45 -0
  146. package/dist/shared/visualization/index.d.cts +3 -0
  147. package/dist/shared/visualization/index.d.ts +3 -0
  148. package/dist/shared/visualization/index.js +24 -0
  149. package/dist/shared/workflow/workflow.cjs +47 -0
  150. package/dist/shared/workflow/workflow.d.cts +33 -0
  151. package/dist/shared/workflow/workflow.d.ts +33 -0
  152. package/dist/shared/workflow/workflow.js +21 -0
  153. package/package.json +125 -26
  154. package/.npmignore +0 -2
  155. package/bin/libretto +0 -31
  156. package/lib/connect.js +0 -34
  157. package/lib/export.js +0 -224
  158. package/lib/import.js +0 -166
  159. package/lib/index.js +0 -8
  160. package/lib/log.js +0 -9
  161. package/lib/validate.js +0 -20
  162. package/makefile +0 -8
  163. package/src/connect.coffee +0 -25
  164. package/src/export.coffee +0 -222
  165. package/src/import.coffee +0 -166
  166. package/src/index.coffee +0 -3
  167. package/src/log.coffee +0 -3
  168. package/src/validate.coffee +0 -10
@@ -0,0 +1,149 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { z } from "zod";
5
+ import { LIBRETTO_CONFIG_PATH } from "./context.js";
6
+ const CURRENT_CONFIG_VERSION = 1;
7
+ const AiPresetSchema = z.enum(["codex", "claude", "gemini"]);
8
+ const AiConfigSchema = z.object({
9
+ preset: AiPresetSchema,
10
+ commandPrefix: z.array(z.string()).min(1),
11
+ updatedAt: z.string()
12
+ }).strict();
13
+ const LibrettoConfigSchema = z.object({
14
+ version: z.literal(CURRENT_CONFIG_VERSION),
15
+ ai: AiConfigSchema.optional()
16
+ }).passthrough();
17
+ const AI_CONFIG_PRESETS = {
18
+ codex: ["codex", "exec", "--skip-git-repo-check", "--sandbox", "read-only"],
19
+ claude: [join(homedir(), ".claude", "local", "claude"), "-p"],
20
+ gemini: ["gemini", "--output-format", "json"]
21
+ };
22
+ function invalidConfigError(configPath) {
23
+ return new Error(
24
+ `AI config is invalid at ${configPath}. Fix the file to match the expected schema or delete it.`
25
+ );
26
+ }
27
+ function parseConfig(raw, configPath) {
28
+ try {
29
+ return LibrettoConfigSchema.parse(JSON.parse(raw));
30
+ } catch {
31
+ throw invalidConfigError(configPath);
32
+ }
33
+ }
34
+ function readLibrettoConfig(configPath = LIBRETTO_CONFIG_PATH) {
35
+ if (!existsSync(configPath)) {
36
+ return { version: CURRENT_CONFIG_VERSION };
37
+ }
38
+ return parseConfig(readFileSync(configPath, "utf-8"), configPath);
39
+ }
40
+ function writeLibrettoConfig(config, configPath = LIBRETTO_CONFIG_PATH) {
41
+ const parsed = LibrettoConfigSchema.parse(config);
42
+ mkdirSync(dirname(configPath), { recursive: true });
43
+ writeFileSync(configPath, JSON.stringify(parsed, null, 2), "utf-8");
44
+ return parsed;
45
+ }
46
+ function readAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
47
+ return readLibrettoConfig(configPath).ai ?? null;
48
+ }
49
+ function quoteShellArg(value) {
50
+ if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) return value;
51
+ return JSON.stringify(value);
52
+ }
53
+ function formatCommandPrefix(prefix) {
54
+ return prefix.map((arg) => quoteShellArg(arg)).join(" ");
55
+ }
56
+ function writeAiConfig(preset, commandPrefix, configPath = LIBRETTO_CONFIG_PATH) {
57
+ const librettoConfig = readLibrettoConfig(configPath);
58
+ const ai = AiConfigSchema.parse({
59
+ preset,
60
+ commandPrefix,
61
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
62
+ });
63
+ writeLibrettoConfig(
64
+ {
65
+ ...librettoConfig,
66
+ version: CURRENT_CONFIG_VERSION,
67
+ ai
68
+ },
69
+ configPath
70
+ );
71
+ return ai;
72
+ }
73
+ function clearAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
74
+ const librettoConfig = readLibrettoConfig(configPath);
75
+ if (!librettoConfig.ai) return false;
76
+ writeLibrettoConfig(
77
+ {
78
+ version: librettoConfig.version
79
+ },
80
+ configPath
81
+ );
82
+ return true;
83
+ }
84
+ function printAiConfig(config, configPath) {
85
+ console.log(`AI preset: ${config.preset}`);
86
+ console.log(`Command prefix: ${formatCommandPrefix(config.commandPrefix)}`);
87
+ console.log(`Config file: ${configPath}`);
88
+ console.log(`Updated at: ${config.updatedAt}`);
89
+ }
90
+ function printConfigureUsage(commandName) {
91
+ console.log(
92
+ `Usage: ${commandName} <codex|claude|gemini> [-- <command prefix...>]
93
+ ${commandName}
94
+ ${commandName} --clear`
95
+ );
96
+ }
97
+ function runAiConfigure(input, options = {}) {
98
+ const configureCommandName = options.configureCommandName ?? "libretto-cli ai configure";
99
+ const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
100
+ const presetArg = input.preset?.trim();
101
+ const customPrefix = (input.customPrefix ?? []).filter(Boolean);
102
+ if (!presetArg && customPrefix.length === 0 && !input.clear) {
103
+ const config2 = readAiConfig(configPath);
104
+ if (!config2) {
105
+ console.log(`No AI config set. Run '${configureCommandName} codex' to set one.`);
106
+ return;
107
+ }
108
+ printAiConfig(config2, configPath);
109
+ return;
110
+ }
111
+ if (input.clear) {
112
+ const removed = clearAiConfig(configPath);
113
+ if (removed) {
114
+ console.log(`Cleared AI config: ${configPath}`);
115
+ } else {
116
+ console.log("No AI config was set.");
117
+ }
118
+ return;
119
+ }
120
+ const parsedPreset = AiPresetSchema.safeParse(presetArg);
121
+ if (!parsedPreset.success) {
122
+ printConfigureUsage(configureCommandName);
123
+ throw new Error(
124
+ "Missing or invalid preset. Use one of: codex, claude, gemini."
125
+ );
126
+ }
127
+ if (input.customPrefix && input.customPrefix.length > 0 && customPrefix.length === 0) {
128
+ throw new Error("Custom command prefix cannot be empty.");
129
+ }
130
+ const preset = parsedPreset.data;
131
+ const commandPrefix = customPrefix.length > 0 ? customPrefix : AI_CONFIG_PRESETS[preset];
132
+ const config = writeAiConfig(preset, commandPrefix, configPath);
133
+ console.log("AI config saved.");
134
+ printAiConfig(config, configPath);
135
+ }
136
+ export {
137
+ AI_CONFIG_PRESETS,
138
+ AiConfigSchema,
139
+ AiPresetSchema,
140
+ CURRENT_CONFIG_VERSION,
141
+ LibrettoConfigSchema,
142
+ clearAiConfig,
143
+ formatCommandPrefix,
144
+ readAiConfig,
145
+ readLibrettoConfig,
146
+ runAiConfigure,
147
+ writeAiConfig,
148
+ writeLibrettoConfig
149
+ };
@@ -0,0 +1,523 @@
1
+ import { chromium } from "playwright";
2
+ import { openSync, existsSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createServer } from "node:net";
6
+ import { spawn } from "node:child_process";
7
+ import {
8
+ getSessionActionsLogPath,
9
+ getSessionNetworkLogPath,
10
+ PROFILES_DIR
11
+ } from "./context.js";
12
+ import {
13
+ assertSessionAvailableForStart,
14
+ clearSessionState,
15
+ readSessionStateOrThrow,
16
+ logFileForSession,
17
+ readSessionState,
18
+ writeSessionState
19
+ } from "./session.js";
20
+ import { installSessionTelemetry } from "./session-telemetry.js";
21
+ async function pickFreePort() {
22
+ return await new Promise((resolve2, reject) => {
23
+ const server = createServer();
24
+ server.listen(0, "127.0.0.1", () => {
25
+ const address = server.address();
26
+ if (!address || typeof address === "string") {
27
+ reject(new Error("Failed to pick free port"));
28
+ return;
29
+ }
30
+ const port = address.port;
31
+ server.close(() => resolve2(port));
32
+ });
33
+ server.on("error", reject);
34
+ });
35
+ }
36
+ function normalizeUrl(url) {
37
+ if (!/^https?:\/\//i.test(url)) {
38
+ return `https://${url}`;
39
+ }
40
+ return url;
41
+ }
42
+ function normalizeDomain(url) {
43
+ try {
44
+ const u = new URL(normalizeUrl(url));
45
+ return u.hostname.replace(/^www\./, "");
46
+ } catch {
47
+ return url.replace(/^www\./, "");
48
+ }
49
+ }
50
+ function getProfilePath(domain) {
51
+ return join(PROFILES_DIR, `${domain}.json`);
52
+ }
53
+ function hasProfile(domain) {
54
+ return existsSync(getProfilePath(domain));
55
+ }
56
+ async function tryConnectToPort(port, logger, timeoutMs = 5e3) {
57
+ const endpoint = `http://localhost:${port}`;
58
+ logger.info("cdp-connect-attempt", { port, endpoint, timeoutMs });
59
+ try {
60
+ const connectPromise = chromium.connectOverCDP(endpoint);
61
+ const timeoutPromise = new Promise(
62
+ (resolve2) => setTimeout(() => resolve2(null), timeoutMs)
63
+ );
64
+ const browser = await Promise.race([connectPromise, timeoutPromise]);
65
+ if (browser) {
66
+ logger.info("cdp-connect-success", {
67
+ port,
68
+ endpoint,
69
+ contexts: browser.contexts().length
70
+ });
71
+ } else {
72
+ logger.warn("cdp-connect-timeout", { port, endpoint, timeoutMs });
73
+ }
74
+ return browser;
75
+ } catch (err) {
76
+ logger.error("cdp-connect-error", { error: err, port, endpoint });
77
+ return null;
78
+ }
79
+ }
80
+ function isOperationalPage(page) {
81
+ const url = page.url();
82
+ return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
83
+ }
84
+ function disconnectBrowser(browser, logger, session) {
85
+ logger.info("cdp-disconnect", { session });
86
+ try {
87
+ browser._connection?.close();
88
+ } catch (err) {
89
+ logger.warn("cdp-disconnect-already-closed", { error: err });
90
+ }
91
+ }
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) {
132
+ logger.info("connect", { session, timeoutMs });
133
+ const state = readSessionStateOrThrow(session);
134
+ const browser = await tryConnectToPort(state.port, logger, timeoutMs);
135
+ if (!browser) {
136
+ logger.error("connect-no-browser", {
137
+ session,
138
+ port: state.port,
139
+ pid: state.pid
140
+ });
141
+ clearSessionState(session, logger);
142
+ throw new Error(
143
+ `No browser running for session "${session}". Run 'libretto-cli open <url> --session ${session}' first.`
144
+ );
145
+ }
146
+ const contexts = browser.contexts();
147
+ logger.info("connect-contexts", { session, contextCount: contexts.length });
148
+ if (contexts.length === 0) {
149
+ logger.error("connect-no-contexts", { session });
150
+ throw new Error("No browser context found.");
151
+ }
152
+ const allPages = contexts.flatMap((c) => c.pages());
153
+ const pages = resolveOperationalPages(browser);
154
+ logger.info("connect-pages", {
155
+ session,
156
+ totalPages: allPages.length,
157
+ filteredPages: pages.length,
158
+ urls: allPages.map((p) => p.url())
159
+ });
160
+ if (pages.length === 0) {
161
+ logger.error("connect-no-pages", {
162
+ session,
163
+ allPageUrls: allPages.map((p) => p.url())
164
+ });
165
+ throw new Error("No pages found.");
166
+ }
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;
180
+ const context = page.context();
181
+ page.on("close", () => {
182
+ logger.error("page-closed-during-command", {
183
+ session,
184
+ url: page.url(),
185
+ trace: new Error("page-closed-trace").stack
186
+ });
187
+ });
188
+ page.on("crash", () => {
189
+ logger.error("page-crashed-during-command", {
190
+ session,
191
+ url: page.url()
192
+ });
193
+ });
194
+ browser.on("disconnected", () => {
195
+ logger.error("browser-disconnected-during-command", {
196
+ session,
197
+ trace: new Error("browser-disconnected-trace").stack
198
+ });
199
+ });
200
+ logger.info("connect-success", { session, pageUrl: page.url() });
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
+ });
215
+ }
216
+ async function runOpen(rawUrl, headed, session, logger) {
217
+ const url = normalizeUrl(rawUrl);
218
+ logger.info("open-start", { url, headed, session });
219
+ assertSessionAvailableForStart(session, logger);
220
+ const port = await pickFreePort();
221
+ const runLogPath = logFileForSession(session);
222
+ const networkLogPath = getSessionNetworkLogPath(session);
223
+ const actionsLogPath = getSessionActionsLogPath(session);
224
+ const browserMode = headed ? "headed" : "headless";
225
+ const domain = normalizeDomain(url);
226
+ const profilePath = getProfilePath(domain);
227
+ const useProfile = hasProfile(domain);
228
+ logger.info("open-launching", {
229
+ url,
230
+ mode: browserMode,
231
+ session,
232
+ port,
233
+ domain,
234
+ useProfile,
235
+ profilePath: useProfile ? profilePath : void 0
236
+ });
237
+ if (useProfile) {
238
+ console.log(`Loading saved profile for ${domain}`);
239
+ }
240
+ console.log(`Launching ${browserMode} browser (session: ${session})...`);
241
+ const escapedProfilePath = profilePath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
242
+ const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
243
+ const storageStateCode = useProfile ? `storageState: '${escapedProfilePath}',` : "";
244
+ const escapedLogPath = runLogPath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
245
+ const escapedNetworkLogPath = networkLogPath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
246
+ const escapedActionsLogPath = actionsLogPath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
247
+ const launcherCode = `
248
+ import { chromium } from 'playwright';
249
+ import { appendFileSync, mkdirSync } from 'node:fs';
250
+ import { dirname } from 'node:path';
251
+
252
+ const LOG_FILE = '${escapedLogPath}';
253
+ const NETWORK_LOG = '${escapedNetworkLogPath}';
254
+ const ACTIONS_LOG = '${escapedActionsLogPath}';
255
+ mkdirSync(dirname(NETWORK_LOG), { recursive: true });
256
+
257
+ ${installSessionTelemetry.toString()}
258
+
259
+ function logAction(entry) {
260
+ appendFileSync(ACTIONS_LOG, JSON.stringify(entry) + '\\n');
261
+ }
262
+
263
+ function logNetwork(entry) {
264
+ appendFileSync(NETWORK_LOG, JSON.stringify(entry) + '\\n');
265
+ }
266
+
267
+ function childLog(level, event, data = {}) {
268
+ try {
269
+ const entry = JSON.stringify({
270
+ timestamp: new Date().toISOString(),
271
+ id: Math.random().toString(36).slice(2, 10),
272
+ level,
273
+ scope: 'libretto-cli.child',
274
+ event,
275
+ data,
276
+ });
277
+ appendFileSync(LOG_FILE, entry + '\\n');
278
+ } catch {}
279
+ }
280
+
281
+ const browser = await chromium.launch({
282
+ headless: ${!headed},
283
+ args: ['--disable-blink-features=AutomationControlled', '--remote-debugging-port=${port}', '--remote-debugging-address=127.0.0.1', '--no-focus-on-check'],
284
+ });
285
+
286
+ browser.on('disconnected', () => {
287
+ childLog('warn', 'browser-disconnected', { port: ${port} });
288
+ });
289
+
290
+ const context = await browser.newContext({
291
+ ${storageStateCode}
292
+ viewport: { width: 1366, height: 768 },
293
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
294
+ });
295
+
296
+ const page = await context.newPage();
297
+ page.setDefaultTimeout(30000);
298
+ page.setDefaultNavigationTimeout(45000);
299
+
300
+ await installSessionTelemetry({
301
+ context,
302
+ initialPage: page,
303
+ includeUserDomActions: true,
304
+ logAction,
305
+ logNetwork,
306
+ });
307
+
308
+
309
+ await page.goto('${escapedUrl}');
310
+
311
+ process.on('SIGTERM', async () => {
312
+ childLog('info', 'child-sigterm');
313
+ await browser.close();
314
+ process.exit(0);
315
+ });
316
+
317
+ process.on('SIGINT', async () => {
318
+ childLog('info', 'child-sigint');
319
+ await browser.close();
320
+ process.exit(0);
321
+ });
322
+
323
+ process.on('uncaughtException', (err) => {
324
+ childLog('error', 'uncaught-exception', { message: err.message, stack: err.stack });
325
+ process.exit(1);
326
+ });
327
+
328
+ process.on('unhandledRejection', (reason) => {
329
+ childLog('warn', 'unhandled-rejection', { reason: String(reason) });
330
+ });
331
+
332
+ process.on('exit', (code) => {
333
+ childLog('info', 'child-exit', { code, pid: process.pid, port: ${port} });
334
+ });
335
+
336
+ childLog('info', 'child-launched', { port: ${port}, pid: process.pid, session: '${session}' });
337
+
338
+ await new Promise(() => {});
339
+ `;
340
+ const childStderrFd = openSync(runLogPath, "a");
341
+ const child = spawn("node", ["--input-type=module", "-e", launcherCode], {
342
+ detached: true,
343
+ stdio: ["ignore", "ignore", childStderrFd],
344
+ cwd: resolve(dirname(fileURLToPath(import.meta.url)), "../../..")
345
+ });
346
+ child.unref();
347
+ logger.info("open-child-spawned", { pid: child.pid, port, session });
348
+ let childSpawnError = null;
349
+ let childEarlyExit = null;
350
+ child.on("error", (err) => {
351
+ childSpawnError = err;
352
+ logger.error("open-child-spawn-error", { error: err, session, port });
353
+ });
354
+ child.on("exit", (code, signal) => {
355
+ childEarlyExit = { code, signal };
356
+ logger.warn("open-child-exited", {
357
+ code,
358
+ signal,
359
+ session,
360
+ port,
361
+ pid: child.pid
362
+ });
363
+ });
364
+ const cdpPollIntervalMs = 500;
365
+ const cdpMaxAttempts = 30;
366
+ const cdpStartupTimeoutMs = cdpPollIntervalMs * cdpMaxAttempts;
367
+ for (let i = 0; i < cdpMaxAttempts; i++) {
368
+ const spawnError = childSpawnError;
369
+ if (spawnError !== null) {
370
+ const errWithCode = spawnError;
371
+ const hint = errWithCode.code === "ENOENT" ? " Ensure Node.js is available in PATH for child processes." : "";
372
+ throw new Error(
373
+ `Failed to launch browser child process: ${spawnError.message}.${hint} Check logs: ${runLogPath}`
374
+ );
375
+ }
376
+ const earlyExit = childEarlyExit;
377
+ if (earlyExit !== null) {
378
+ const status = earlyExit.code ?? earlyExit.signal ?? "unknown";
379
+ throw new Error(
380
+ `Browser child process exited before startup (status: ${status}). Check logs: ${runLogPath}`
381
+ );
382
+ }
383
+ await new Promise((r) => setTimeout(r, cdpPollIntervalMs));
384
+ const ready = await fetch(`http://127.0.0.1:${port}/json/version`).then(() => true).catch(() => false);
385
+ if (i > 0 && i % 5 === 0) {
386
+ logger.info("open-waiting-for-cdp", { attempt: i, port, session });
387
+ }
388
+ if (ready) {
389
+ writeSessionState({
390
+ port,
391
+ pid: child.pid,
392
+ session,
393
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
394
+ status: "active"
395
+ }, logger);
396
+ logger.info("open-success", {
397
+ url,
398
+ mode: browserMode,
399
+ session,
400
+ port,
401
+ pid: child.pid
402
+ });
403
+ console.log(`Browser open (${browserMode}): ${url}`);
404
+ await new Promise((r) => setTimeout(r, 2e3));
405
+ return;
406
+ }
407
+ }
408
+ logger.error("open-timeout", {
409
+ session,
410
+ port,
411
+ pid: child.pid,
412
+ attempts: cdpMaxAttempts
413
+ });
414
+ throw new Error(
415
+ `Failed to connect to browser after ${Math.ceil(cdpStartupTimeoutMs / 1e3)}s. Check startup logs: ${runLogPath}`
416
+ );
417
+ }
418
+ async function runSave(urlOrDomain, session, logger) {
419
+ logger.info("save-start", { urlOrDomain, session });
420
+ const { browser, context, page } = await connect(session, logger);
421
+ try {
422
+ await new Promise((r) => setTimeout(r, 500));
423
+ const domain = normalizeDomain(urlOrDomain);
424
+ const profilePath = getProfilePath(domain);
425
+ const cdpSession = await context.newCDPSession(page);
426
+ const { cookies: rawCookies } = await cdpSession.send(
427
+ "Network.getAllCookies"
428
+ );
429
+ const cookies = rawCookies.map((c) => {
430
+ const cookie = { ...c };
431
+ if (cookie.partitionKey && typeof cookie.partitionKey === "object") {
432
+ delete cookie.partitionKey;
433
+ }
434
+ return cookie;
435
+ });
436
+ await cdpSession.detach();
437
+ const origins = [];
438
+ for (const ctx of browser.contexts()) {
439
+ for (const pg of ctx.pages()) {
440
+ try {
441
+ const origin = new URL(pg.url()).origin;
442
+ const localStorage = await pg.evaluate(() => {
443
+ const items = [];
444
+ for (let i = 0; i < window.localStorage.length; i++) {
445
+ const key = window.localStorage.key(i);
446
+ if (key) {
447
+ items.push({
448
+ name: key,
449
+ value: window.localStorage.getItem(key) || ""
450
+ });
451
+ }
452
+ }
453
+ return items;
454
+ });
455
+ if (localStorage.length > 0) {
456
+ origins.push({ origin, localStorage });
457
+ }
458
+ } catch {
459
+ }
460
+ }
461
+ }
462
+ const state = { cookies, origins };
463
+ const fs = await import("node:fs/promises");
464
+ await fs.mkdir(dirname(profilePath), { recursive: true });
465
+ await fs.writeFile(profilePath, JSON.stringify(state, null, 2));
466
+ logger.info("save-success", {
467
+ domain,
468
+ profilePath,
469
+ cookieCount: cookies.length,
470
+ originCount: origins.length
471
+ });
472
+ console.log(`Profile saved for ${domain}`);
473
+ console.log(` Location: ${profilePath}`);
474
+ console.log(` Cookies: ${cookies.length}, Origins: ${origins.length}`);
475
+ } catch (err) {
476
+ logger.error("save-error", { error: err, urlOrDomain, session });
477
+ throw err;
478
+ } finally {
479
+ disconnectBrowser(browser, logger, session);
480
+ }
481
+ }
482
+ async function runClose(session, logger) {
483
+ logger.info("close-start", { session });
484
+ const state = readSessionState(session, logger);
485
+ if (!state) {
486
+ logger.info("close-no-session", { session });
487
+ console.log(`No browser running for session "${session}".`);
488
+ return;
489
+ }
490
+ logger.info("close-killing", { session, pid: state.pid, port: state.port });
491
+ try {
492
+ process.kill(state.pid, "SIGTERM");
493
+ } catch (err) {
494
+ logger.warn("close-kill-failed", { error: err, session, pid: state.pid });
495
+ }
496
+ await new Promise((r) => setTimeout(r, 1500));
497
+ clearSessionState(session, logger);
498
+ logger.info("close-success", { session });
499
+ console.log(`Browser closed (session: ${session}).`);
500
+ }
501
+ function resolvePath(filePath) {
502
+ return join(process.cwd(), filePath);
503
+ }
504
+ function getScreenshotBaseName(title) {
505
+ const sanitizedTitle = title.replace(/[^a-zA-Z0-9\s-]/g, "").replace(/\s+/g, "-").toLowerCase().slice(0, 50);
506
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
507
+ return `${sanitizedTitle}-${timestamp}`;
508
+ }
509
+ export {
510
+ connect,
511
+ disconnectBrowser,
512
+ getProfilePath,
513
+ getScreenshotBaseName,
514
+ hasProfile,
515
+ listOpenPages,
516
+ normalizeDomain,
517
+ normalizeUrl,
518
+ resolvePath,
519
+ runClose,
520
+ runOpen,
521
+ runPages,
522
+ runSave
523
+ };