libretto 0.4.4 → 0.5.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 (194) hide show
  1. package/README.md +106 -36
  2. package/dist/cli/cli.js +39 -113
  3. package/dist/cli/commands/ai.js +1 -1
  4. package/dist/cli/commands/browser.js +87 -60
  5. package/dist/cli/commands/execution.js +201 -88
  6. package/dist/cli/commands/init.js +30 -8
  7. package/dist/cli/commands/logs.js +5 -6
  8. package/dist/cli/commands/shared.js +30 -29
  9. package/dist/cli/commands/snapshot.js +26 -39
  10. package/dist/cli/core/ai-config.js +9 -2
  11. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  12. package/dist/cli/core/browser.js +141 -33
  13. package/dist/cli/core/context.js +7 -18
  14. package/dist/cli/core/session-telemetry.js +5 -2
  15. package/dist/cli/core/session.js +23 -10
  16. package/dist/cli/core/snapshot-analyzer.js +16 -33
  17. package/dist/cli/core/snapshot-api-config.js +2 -6
  18. package/dist/cli/core/telemetry.js +10 -2
  19. package/dist/cli/framework/simple-cli.js +45 -25
  20. package/dist/cli/router.js +14 -21
  21. package/dist/cli/workers/run-integration-runtime.js +26 -7
  22. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  23. package/dist/cli/workers/run-integration-worker.js +1 -4
  24. package/dist/index.d.ts +1 -2
  25. package/dist/index.js +7 -10
  26. package/dist/runtime/download/download.js +5 -1
  27. package/dist/runtime/extract/extract.js +11 -2
  28. package/dist/runtime/network/network.js +8 -1
  29. package/dist/runtime/recovery/agent.js +6 -2
  30. package/dist/runtime/recovery/errors.js +3 -1
  31. package/dist/runtime/recovery/recovery.js +3 -1
  32. package/dist/shared/condense-dom/condense-dom.js +6 -13
  33. package/dist/shared/config/config.d.ts +1 -9
  34. package/dist/shared/config/config.js +0 -18
  35. package/dist/shared/config/index.d.ts +2 -1
  36. package/dist/shared/config/index.js +0 -10
  37. package/dist/shared/debug/pause.js +9 -3
  38. package/dist/shared/instrumentation/instrument.js +101 -5
  39. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  40. package/dist/shared/llm/client.js +3 -1
  41. package/dist/shared/logger/index.js +4 -1
  42. package/dist/shared/paths/paths.js +2 -1
  43. package/dist/shared/paths/repo-root.d.ts +3 -0
  44. package/dist/shared/paths/repo-root.js +24 -0
  45. package/dist/shared/run/api.js +3 -1
  46. package/dist/shared/run/browser.js +7 -2
  47. package/dist/shared/state/session-state.d.ts +2 -1
  48. package/dist/shared/state/session-state.js +5 -2
  49. package/dist/shared/visualization/ghost-cursor.js +19 -10
  50. package/dist/shared/visualization/highlight.js +9 -6
  51. package/dist/shared/workflow/workflow.d.ts +4 -5
  52. package/dist/shared/workflow/workflow.js +3 -5
  53. package/package.json +11 -8
  54. package/scripts/check-skills-sync.mjs +25 -0
  55. package/scripts/compare-eval-summary.mjs +47 -0
  56. package/scripts/postinstall.mjs +26 -17
  57. package/scripts/prepare-release.sh +97 -0
  58. package/scripts/skills-libretto.mjs +103 -0
  59. package/scripts/summarize-evals.mjs +135 -0
  60. package/scripts/sync-skills.mjs +12 -0
  61. package/skills/libretto/SKILL.md +130 -377
  62. package/skills/libretto/references/auth-profiles.md +30 -0
  63. package/skills/libretto/{code-generation-rules.md → references/code-generation-rules.md} +27 -42
  64. package/skills/libretto/references/configuration-file-reference.md +53 -0
  65. package/skills/libretto/references/pages-and-page-targeting.md +29 -0
  66. package/skills/libretto/references/site-security-review.md +143 -0
  67. package/src/cli/cli.ts +86 -0
  68. package/src/cli/commands/ai.ts +35 -0
  69. package/src/cli/commands/browser.ts +189 -0
  70. package/src/cli/commands/execution.ts +822 -0
  71. package/src/cli/commands/init.ts +350 -0
  72. package/src/cli/commands/logs.ts +128 -0
  73. package/src/cli/commands/shared.ts +69 -0
  74. package/src/cli/commands/snapshot.ts +312 -0
  75. package/src/cli/core/ai-config.ts +264 -0
  76. package/src/cli/core/api-snapshot-analyzer.ts +108 -0
  77. package/src/cli/core/browser.ts +976 -0
  78. package/src/cli/core/context.ts +127 -0
  79. package/src/cli/core/pause-signals.ts +35 -0
  80. package/src/cli/core/session-telemetry.ts +564 -0
  81. package/src/cli/core/session.ts +223 -0
  82. package/src/cli/core/snapshot-analyzer.ts +855 -0
  83. package/src/cli/core/snapshot-api-config.ts +231 -0
  84. package/src/cli/core/telemetry.ts +459 -0
  85. package/src/cli/framework/simple-cli.ts +1340 -0
  86. package/src/cli/index.ts +13 -0
  87. package/src/cli/router.ts +20 -0
  88. package/src/cli/workers/run-integration-runtime.ts +338 -0
  89. package/src/cli/workers/run-integration-worker-protocol.ts +16 -0
  90. package/src/cli/workers/run-integration-worker.ts +72 -0
  91. package/src/index.ts +127 -0
  92. package/src/runtime/download/download.ts +104 -0
  93. package/src/runtime/download/index.ts +7 -0
  94. package/src/runtime/extract/extract.ts +102 -0
  95. package/src/runtime/extract/index.ts +1 -0
  96. package/src/runtime/network/index.ts +5 -0
  97. package/src/runtime/network/network.ts +119 -0
  98. package/{dist/runtime/recovery/agent.cjs → src/runtime/recovery/agent.ts} +114 -76
  99. package/src/runtime/recovery/errors.ts +155 -0
  100. package/src/runtime/recovery/index.ts +7 -0
  101. package/src/runtime/recovery/recovery.ts +53 -0
  102. package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +249 -124
  103. package/src/shared/config/config.ts +3 -0
  104. package/src/shared/config/index.ts +0 -0
  105. package/src/shared/debug/index.ts +1 -0
  106. package/src/shared/debug/pause.ts +91 -0
  107. package/src/shared/instrumentation/errors.ts +84 -0
  108. package/src/shared/instrumentation/index.ts +9 -0
  109. package/src/shared/instrumentation/instrument.ts +406 -0
  110. package/src/shared/llm/ai-sdk-adapter.ts +81 -0
  111. package/{dist/shared/llm/client.cjs → src/shared/llm/client.ts} +86 -80
  112. package/src/shared/llm/index.ts +3 -0
  113. package/src/shared/llm/types.ts +63 -0
  114. package/src/shared/logger/index.ts +13 -0
  115. package/src/shared/logger/logger.ts +358 -0
  116. package/src/shared/logger/sinks.ts +148 -0
  117. package/src/shared/paths/paths.ts +110 -0
  118. package/src/shared/paths/repo-root.ts +27 -0
  119. package/src/shared/run/api.ts +6 -0
  120. package/src/shared/run/browser.ts +107 -0
  121. package/src/shared/state/index.ts +11 -0
  122. package/src/shared/state/session-state.ts +77 -0
  123. package/src/shared/visualization/ghost-cursor.ts +213 -0
  124. package/src/shared/visualization/highlight.ts +149 -0
  125. package/src/shared/visualization/index.ts +18 -0
  126. package/src/shared/workflow/workflow.ts +36 -0
  127. package/dist/index.cjs +0 -144
  128. package/dist/index.d.cts +0 -21
  129. package/dist/runtime/download/download.cjs +0 -70
  130. package/dist/runtime/download/download.d.cts +0 -35
  131. package/dist/runtime/download/index.cjs +0 -30
  132. package/dist/runtime/download/index.d.cts +0 -3
  133. package/dist/runtime/extract/extract.cjs +0 -88
  134. package/dist/runtime/extract/extract.d.cts +0 -23
  135. package/dist/runtime/extract/index.cjs +0 -28
  136. package/dist/runtime/extract/index.d.cts +0 -5
  137. package/dist/runtime/network/index.cjs +0 -28
  138. package/dist/runtime/network/index.d.cts +0 -4
  139. package/dist/runtime/network/network.cjs +0 -91
  140. package/dist/runtime/network/network.d.cts +0 -28
  141. package/dist/runtime/recovery/agent.d.cts +0 -13
  142. package/dist/runtime/recovery/errors.cjs +0 -124
  143. package/dist/runtime/recovery/errors.d.cts +0 -31
  144. package/dist/runtime/recovery/index.cjs +0 -34
  145. package/dist/runtime/recovery/index.d.cts +0 -7
  146. package/dist/runtime/recovery/recovery.cjs +0 -55
  147. package/dist/runtime/recovery/recovery.d.cts +0 -12
  148. package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
  149. package/dist/shared/config/config.cjs +0 -44
  150. package/dist/shared/config/config.d.cts +0 -10
  151. package/dist/shared/config/index.cjs +0 -32
  152. package/dist/shared/config/index.d.cts +0 -1
  153. package/dist/shared/debug/index.cjs +0 -28
  154. package/dist/shared/debug/index.d.cts +0 -1
  155. package/dist/shared/debug/pause.cjs +0 -86
  156. package/dist/shared/debug/pause.d.cts +0 -12
  157. package/dist/shared/instrumentation/errors.cjs +0 -81
  158. package/dist/shared/instrumentation/errors.d.cts +0 -12
  159. package/dist/shared/instrumentation/index.cjs +0 -35
  160. package/dist/shared/instrumentation/index.d.cts +0 -6
  161. package/dist/shared/instrumentation/instrument.cjs +0 -206
  162. package/dist/shared/instrumentation/instrument.d.cts +0 -32
  163. package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
  164. package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
  165. package/dist/shared/llm/client.d.cts +0 -13
  166. package/dist/shared/llm/index.cjs +0 -31
  167. package/dist/shared/llm/index.d.cts +0 -5
  168. package/dist/shared/llm/types.cjs +0 -16
  169. package/dist/shared/llm/types.d.cts +0 -67
  170. package/dist/shared/logger/index.cjs +0 -37
  171. package/dist/shared/logger/index.d.cts +0 -2
  172. package/dist/shared/logger/logger.cjs +0 -232
  173. package/dist/shared/logger/logger.d.cts +0 -86
  174. package/dist/shared/logger/sinks.cjs +0 -160
  175. package/dist/shared/logger/sinks.d.cts +0 -9
  176. package/dist/shared/paths/paths.cjs +0 -104
  177. package/dist/shared/paths/paths.d.cts +0 -10
  178. package/dist/shared/run/api.cjs +0 -28
  179. package/dist/shared/run/api.d.cts +0 -2
  180. package/dist/shared/run/browser.cjs +0 -98
  181. package/dist/shared/run/browser.d.cts +0 -22
  182. package/dist/shared/state/index.cjs +0 -38
  183. package/dist/shared/state/index.d.cts +0 -2
  184. package/dist/shared/state/session-state.cjs +0 -92
  185. package/dist/shared/state/session-state.d.cts +0 -40
  186. package/dist/shared/visualization/ghost-cursor.cjs +0 -174
  187. package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
  188. package/dist/shared/visualization/highlight.cjs +0 -134
  189. package/dist/shared/visualization/highlight.d.cts +0 -22
  190. package/dist/shared/visualization/index.cjs +0 -45
  191. package/dist/shared/visualization/index.d.cts +0 -3
  192. package/dist/shared/workflow/workflow.cjs +0 -47
  193. package/dist/shared/workflow/workflow.d.cts +0 -21
  194. package/skills/libretto/integration-approach-selection.md +0 -174
@@ -5,16 +5,10 @@ import { getSessionSnapshotRunDir } from "../core/context.js";
5
5
  import { condenseDom } from "../../shared/condense-dom/condense-dom.js";
6
6
  import { readSessionState } from "../core/session.js";
7
7
  import { SimpleCLI } from "../framework/simple-cli.js";
8
- import {
9
- loadSessionStateMiddleware,
10
- pageOption,
11
- resolveSessionMiddleware,
12
- sessionOption
13
- } from "./shared.js";
8
+ import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
14
9
  import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
15
10
  import { readAiConfig } from "../core/ai-config.js";
16
11
  import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
17
- const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
18
12
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
19
13
  function generateSnapshotRunId() {
20
14
  return `snapshot-${Date.now()}`;
@@ -106,6 +100,12 @@ async function captureScreenshot(session, logger, pageId) {
106
100
  const pngPath = `${snapshotRunDir}/page.png`;
107
101
  const htmlPath = `${snapshotRunDir}/page.html`;
108
102
  const condensedHtmlPath = `${snapshotRunDir}/page.condensed.html`;
103
+ const RENDER_SETTLE_TIMEOUT_MS = 1e4;
104
+ await Promise.race([
105
+ page.waitForLoadState("networkidle").catch(() => {
106
+ }),
107
+ new Promise((resolve) => setTimeout(resolve, RENDER_SETTLE_TIMEOUT_MS))
108
+ ]);
109
109
  const restoreViewport = resolveSnapshotViewport(session, logger);
110
110
  const viewportMetrics = await readSnapshotViewportMetrics(page);
111
111
  logger.info("screenshot-viewport-metrics", {
@@ -185,17 +185,10 @@ async function captureScreenshot(session, logger, pageId) {
185
185
  }
186
186
  }
187
187
  async function runSnapshot(session, logger, pageId, objective, context) {
188
- const normalizedObjective = objective?.trim();
189
- const normalizedContext = context?.trim();
190
- if (!normalizedObjective && normalizedContext) {
191
- throw new Error(
192
- "Couldn't run analysis: --objective is required when providing --context."
193
- );
194
- }
195
- const configuredAi = normalizedObjective ? readAiConfig() : null;
196
- if (normalizedObjective) {
197
- resolveSnapshotApiModelOrThrow(configuredAi);
198
- }
188
+ const normalizedObjective = objective.trim();
189
+ const normalizedContext = context.trim();
190
+ const configuredAi = readAiConfig();
191
+ resolveSnapshotApiModelOrThrow(configuredAi);
199
192
  const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
200
193
  session,
201
194
  logger,
@@ -205,14 +198,10 @@ async function runSnapshot(session, logger, pageId, objective, context) {
205
198
  console.log(` PNG: ${pngPath}`);
206
199
  console.log(` HTML: ${htmlPath}`);
207
200
  console.log(` Condensed HTML: ${condensedHtmlPath}`);
208
- if (!normalizedObjective) {
209
- console.log("Use --objective flag to analyze snapshots.");
210
- return;
211
- }
212
201
  const interpretArgs = {
213
202
  objective: normalizedObjective,
214
203
  session,
215
- context: normalizedContext ?? DEFAULT_SNAPSHOT_CONTEXT,
204
+ context: normalizedContext,
216
205
  pngPath,
217
206
  htmlPath,
218
207
  condensedHtmlPath
@@ -224,24 +213,22 @@ const snapshotInput = SimpleCLI.input({
224
213
  named: {
225
214
  session: sessionOption(),
226
215
  page: pageOption(),
227
- objective: SimpleCLI.option(z.string().optional()),
228
- context: SimpleCLI.option(z.string().optional())
216
+ objective: SimpleCLI.option(z.string()),
217
+ context: SimpleCLI.option(z.string())
229
218
  }
230
219
  });
231
- function createSnapshotCommand(logger) {
232
- return SimpleCLI.command({
233
- description: "Capture PNG + HTML; analyze when --objective is provided (--context optional)"
234
- }).input(snapshotInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
235
- await runSnapshot(
236
- ctx.session,
237
- logger,
238
- input.page,
239
- input.objective,
240
- input.context
241
- );
242
- });
243
- }
220
+ const snapshotCommand = SimpleCLI.command({
221
+ description: "Capture PNG + HTML and analyze with --objective and --context"
222
+ }).input(snapshotInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
223
+ await runSnapshot(
224
+ ctx.session,
225
+ ctx.logger,
226
+ input.page,
227
+ input.objective,
228
+ input.context
229
+ );
230
+ });
244
231
  export {
245
- createSnapshotCommand,
232
+ snapshotCommand,
246
233
  snapshotInput
247
234
  };
@@ -26,7 +26,12 @@ const PROVIDER_ALIASES = {
26
26
  claude: DEFAULT_MODELS.anthropic,
27
27
  google: DEFAULT_MODELS.gemini
28
28
  };
29
- const CONFIGURE_PROVIDERS = ["openai", "anthropic", "gemini", "vertex"];
29
+ const CONFIGURE_PROVIDERS = [
30
+ "openai",
31
+ "anthropic",
32
+ "gemini",
33
+ "vertex"
34
+ ];
30
35
  function formatConfigureProviders(separator = " | ") {
31
36
  return CONFIGURE_PROVIDERS.join(separator);
32
37
  }
@@ -147,7 +152,9 @@ function runAiConfigure(input, options = {}) {
147
152
  console.log(
148
153
  `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
149
154
  );
150
- console.log("Provider credentials still come from your shell or .env file.");
155
+ console.log(
156
+ "Provider credentials still come from your shell or .env file."
157
+ );
151
158
  return;
152
159
  }
153
160
  printAiConfig(config2, configPath);
@@ -1,16 +1,13 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { createLLMClient } from "../../shared/llm/client.js";
3
3
  import {
4
- formatInterpretationOutput,
5
4
  InterpretResultSchema,
6
5
  buildInlinePromptSelection,
7
6
  getMimeType,
8
7
  readFileAsBase64
9
8
  } from "./snapshot-analyzer.js";
10
9
  import { readAiConfig } from "./ai-config.js";
11
- import {
12
- resolveSnapshotApiModelOrThrow
13
- } from "./snapshot-api-config.js";
10
+ import { resolveSnapshotApiModelOrThrow } from "./snapshot-api-config.js";
14
11
  async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
15
12
  const selection = resolveSnapshotApiModelOrThrow(configuredAi);
16
13
  logger.info("api-interpret-start", {
@@ -67,7 +64,20 @@ async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
67
64
  selectorCount: parsed.selectors.length,
68
65
  answer: parsed.answer.slice(0, 200)
69
66
  });
70
- console.log(formatInterpretationOutput(parsed, "Interpretation (via API):"));
67
+ console.log("");
68
+ console.log("Analysis:");
69
+ console.log(parsed.answer);
70
+ if (parsed.selectors.length > 0) {
71
+ console.log("");
72
+ console.log("Selectors:");
73
+ parsed.selectors.forEach((selector, index) => {
74
+ console.log(` ${index + 1}. ${selector.label}: ${selector.selector}`);
75
+ });
76
+ }
77
+ if (parsed.notes?.trim()) {
78
+ console.log("");
79
+ console.log(`Notes: ${parsed.notes.trim()}`);
80
+ }
71
81
  }
72
82
  export {
73
83
  runApiInterpret
@@ -1,4 +1,6 @@
1
- import { chromium } from "playwright";
1
+ import {
2
+ chromium
3
+ } from "playwright";
2
4
  import { openSync, existsSync } from "node:fs";
3
5
  import { dirname, join, resolve } from "node:path";
4
6
  import { fileURLToPath } from "node:url";
@@ -57,9 +59,8 @@ function getProfilePath(domain) {
57
59
  function hasProfile(domain) {
58
60
  return existsSync(getProfilePath(domain));
59
61
  }
60
- async function tryConnectToPort(port, logger, timeoutMs = 5e3) {
61
- const endpoint = `http://localhost:${port}`;
62
- logger.info("cdp-connect-attempt", { port, endpoint, timeoutMs });
62
+ async function tryConnectToCDP(endpoint, logger, timeoutMs = 5e3) {
63
+ logger.info("cdp-connect-attempt", { endpoint, timeoutMs });
63
64
  try {
64
65
  const connectPromise = chromium.connectOverCDP(endpoint);
65
66
  const timeoutPromise = new Promise(
@@ -68,16 +69,15 @@ async function tryConnectToPort(port, logger, timeoutMs = 5e3) {
68
69
  const browser = await Promise.race([connectPromise, timeoutPromise]);
69
70
  if (browser) {
70
71
  logger.info("cdp-connect-success", {
71
- port,
72
72
  endpoint,
73
73
  contexts: browser.contexts().length
74
74
  });
75
75
  } else {
76
- logger.warn("cdp-connect-timeout", { port, endpoint, timeoutMs });
76
+ logger.warn("cdp-connect-timeout", { endpoint, timeoutMs });
77
77
  }
78
78
  return browser;
79
79
  } catch (err) {
80
- logger.error("cdp-connect-error", { error: err, port, endpoint });
80
+ logger.error("cdp-connect-error", { error: err, endpoint });
81
81
  return null;
82
82
  }
83
83
  }
@@ -102,7 +102,9 @@ async function resolvePageId(page) {
102
102
  const targetInfo = await cdpSession.send("Target.getTargetInfo");
103
103
  const targetId = targetInfo?.targetInfo?.targetId;
104
104
  if (typeof targetId !== "string" || targetId.length === 0) {
105
- throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
105
+ throw new Error(
106
+ `Could not resolve target id for page at URL "${page.url()}".`
107
+ );
106
108
  }
107
109
  return targetId;
108
110
  } finally {
@@ -135,16 +137,22 @@ async function listOpenPages(session, logger) {
135
137
  async function connect(session, logger, timeoutMs = 1e4, options) {
136
138
  logger.info("connect", { session, timeoutMs });
137
139
  const state = readSessionStateOrThrow(session);
138
- const browser = await tryConnectToPort(state.port, logger, timeoutMs);
140
+ const endpoint = state.cdpEndpoint ?? `http://localhost:${state.port}`;
141
+ const browser = await tryConnectToCDP(endpoint, logger, timeoutMs);
139
142
  if (!browser) {
140
143
  logger.error("connect-no-browser", {
141
144
  session,
142
- port: state.port,
145
+ endpoint,
143
146
  pid: state.pid
144
147
  });
145
- clearSessionState(session, logger);
148
+ if (state.pid == null || !isPidRunning(state.pid)) {
149
+ clearSessionState(session, logger);
150
+ throw new Error(
151
+ `No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`
152
+ );
153
+ }
146
154
  throw new Error(
147
- `No browser running for session "${session}". Run 'libretto-cli open <url> --session ${session}' first.`
155
+ `Could not connect to the browser for session "${session}" at ${endpoint}, but the session process (pid ${state.pid}) is still running. Try the command again, or close and reopen the session if it stays stuck.`
148
156
  );
149
157
  }
150
158
  const contexts = browser.contexts();
@@ -170,14 +178,14 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
170
178
  }
171
179
  if (options?.requireSinglePage && !options.pageId && pages.length > 1) {
172
180
  throw new Error(
173
- `Multiple pages are open in session "${session}". Pass --page <id> to target a page (run "libretto-cli pages --session ${session}" to list ids).`
181
+ `Multiple pages are open in session "${session}". Pass --page <id> to target a page (run "libretto pages --session ${session}" to list ids).`
174
182
  );
175
183
  }
176
184
  const pageRefs = await resolvePageReferences(pages);
177
185
  const pageRef = options?.pageId ? pageRefs.find((ref) => ref.id === options.pageId) ?? null : pageRefs[pageRefs.length - 1];
178
186
  if (!pageRef) {
179
187
  throw new Error(
180
- `Page "${options?.pageId}" was not found in session "${session}". Run "libretto-cli pages --session ${session}" to list ids.`
188
+ `Page "${options?.pageId}" was not found in session "${session}". Run "libretto pages --session ${session}" to list ids.`
181
189
  );
182
190
  }
183
191
  const page = pageRef.page;
@@ -225,10 +233,16 @@ function resolveViewport(cliViewport, logger) {
225
233
  }
226
234
  const config = readLibrettoConfig();
227
235
  if (config.viewport) {
228
- logger.info("viewport-source", { source: "config", viewport: config.viewport });
236
+ logger.info("viewport-source", {
237
+ source: "config",
238
+ viewport: config.viewport
239
+ });
229
240
  return config.viewport;
230
241
  }
231
- logger.info("viewport-source", { source: "default", viewport: DEFAULT_VIEWPORT });
242
+ logger.info("viewport-source", {
243
+ source: "default",
244
+ viewport: DEFAULT_VIEWPORT
245
+ });
232
246
  return DEFAULT_VIEWPORT;
233
247
  }
234
248
  async function runOpen(rawUrl, headed, session, logger, options) {
@@ -293,7 +307,7 @@ function childLog(level, event, data = {}) {
293
307
  timestamp: new Date().toISOString(),
294
308
  id: Math.random().toString(36).slice(2, 10),
295
309
  level,
296
- scope: 'libretto-cli.child',
310
+ scope: 'libretto.child',
297
311
  event,
298
312
  data,
299
313
  });
@@ -409,14 +423,17 @@ await new Promise(() => {});
409
423
  logger.info("open-waiting-for-cdp", { attempt: i, port, session });
410
424
  }
411
425
  if (ready) {
412
- writeSessionState({
413
- port,
414
- pid: child.pid,
415
- session,
416
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
417
- status: "active",
418
- viewport
419
- }, logger);
426
+ writeSessionState(
427
+ {
428
+ port,
429
+ pid: child.pid,
430
+ session,
431
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
432
+ status: "active",
433
+ viewport
434
+ },
435
+ logger
436
+ );
420
437
  logger.info("open-success", {
421
438
  url,
422
439
  mode: browserMode,
@@ -512,8 +529,10 @@ async function runClose(session, logger) {
512
529
  return;
513
530
  }
514
531
  logger.info("close-killing", { session, pid: state.pid, port: state.port });
515
- sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
516
- await waitForCloseSignalWindow(CLOSE_WAIT_MS);
532
+ if (state.pid != null) {
533
+ sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
534
+ await waitForCloseSignalWindow(CLOSE_WAIT_MS);
535
+ }
517
536
  clearSessionState(session, logger);
518
537
  logger.info("close-success", { session });
519
538
  console.log(`Browser closed (session: ${session}).`);
@@ -570,7 +589,7 @@ function resolveClosableSessions(logger) {
570
589
  function clearStoppedSessionStates(sessions, logger) {
571
590
  let cleared = 0;
572
591
  for (const session of sessions) {
573
- if (!isPidRunning(session.pid)) {
592
+ if (session.pid == null || !isPidRunning(session.pid)) {
574
593
  clearSessionState(session.session, logger);
575
594
  cleared += 1;
576
595
  }
@@ -596,17 +615,26 @@ async function runCloseAll(logger, options) {
596
615
  pid: target.pid,
597
616
  port: target.port
598
617
  });
599
- sendSignalToProcessGroupOrPid(target.pid, "SIGTERM", logger, target.session);
618
+ if (target.pid != null) {
619
+ sendSignalToProcessGroupOrPid(
620
+ target.pid,
621
+ "SIGTERM",
622
+ logger,
623
+ target.session
624
+ );
625
+ }
600
626
  }
601
627
  await waitForCloseSignalWindow(CLOSE_WAIT_MS);
602
- let survivors = closable.filter((target) => isPidRunning(target.pid));
628
+ let survivors = closable.filter(
629
+ (target) => target.pid != null && isPidRunning(target.pid)
630
+ );
603
631
  if (survivors.length > 0 && !force) {
604
632
  const closed = clearStoppedSessionStates(closable, logger);
605
633
  throw new Error(
606
634
  [
607
635
  `Failed to close ${survivors.length} session(s) gracefully: ${formatSessionList(survivors)}.`,
608
636
  `Closed ${closed} session(s).`,
609
- "Retry with: libretto-cli close --all --force"
637
+ `Retry with: libretto close --all --force`
610
638
  ].join("\n")
611
639
  );
612
640
  }
@@ -617,11 +645,20 @@ async function runCloseAll(logger, options) {
617
645
  session: survivor.session,
618
646
  pid: survivor.pid
619
647
  });
620
- sendSignalToProcessGroupOrPid(survivor.pid, "SIGKILL", logger, survivor.session);
648
+ if (survivor.pid != null) {
649
+ sendSignalToProcessGroupOrPid(
650
+ survivor.pid,
651
+ "SIGKILL",
652
+ logger,
653
+ survivor.session
654
+ );
655
+ }
621
656
  forceKilled += 1;
622
657
  }
623
658
  await waitForCloseSignalWindow(FORCE_CLOSE_WAIT_MS);
624
- survivors = survivors.filter((target) => isPidRunning(target.pid));
659
+ survivors = survivors.filter(
660
+ (target) => target.pid != null && isPidRunning(target.pid)
661
+ );
625
662
  if (survivors.length > 0) {
626
663
  const closed = clearStoppedSessionStates(closable, logger);
627
664
  throw new Error(
@@ -643,6 +680,75 @@ async function runCloseAll(logger, options) {
643
680
  console.log(`Force-killed ${forceKilled} session(s).`);
644
681
  }
645
682
  }
683
+ async function runConnect(cdpUrl, session, logger) {
684
+ logger.info("connect-start", { cdpUrl, session });
685
+ assertSessionAvailableForStart(session, logger);
686
+ let parsedUrl;
687
+ try {
688
+ parsedUrl = new URL(cdpUrl);
689
+ } catch {
690
+ throw new Error(
691
+ [
692
+ `Invalid CDP URL: ${cdpUrl}`,
693
+ ``,
694
+ `Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
695
+ ` libretto connect http://127.0.0.1:9222`,
696
+ ` libretto connect http://remote-host:9222`,
697
+ ` libretto connect http://remote-host:9222/devtools/browser/<id>`
698
+ ].join("\n")
699
+ );
700
+ }
701
+ const endpoint = parsedUrl.href;
702
+ const port = parsedUrl.port ? Number(parsedUrl.port) : parsedUrl.protocol === "https:" ? 443 : 80;
703
+ console.log(
704
+ `Connecting to CDP endpoint at ${endpoint} (session: ${session})...`
705
+ );
706
+ const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
707
+ try {
708
+ const resp = await fetch(versionUrl);
709
+ const versionInfo = await resp.json();
710
+ logger.info("connect-version-ok", { versionUrl, versionInfo });
711
+ } catch (err) {
712
+ logger.error("connect-version-failed", { versionUrl, error: err });
713
+ throw new Error(
714
+ `Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`
715
+ );
716
+ }
717
+ const browser = await tryConnectToCDP(endpoint, logger, 1e4);
718
+ if (!browser) {
719
+ throw new Error(
720
+ `CDP endpoint at ${endpoint} is reachable but Playwright could not connect. Check that the URL is a Chrome DevTools Protocol endpoint.`
721
+ );
722
+ }
723
+ const pages = resolveOperationalPages(browser);
724
+ logger.info("connect-pages", {
725
+ session,
726
+ pageCount: pages.length,
727
+ urls: pages.map((p) => p.url())
728
+ });
729
+ disconnectBrowser(browser, logger, session);
730
+ writeSessionState(
731
+ {
732
+ port,
733
+ cdpEndpoint: endpoint,
734
+ session,
735
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
736
+ status: "active"
737
+ },
738
+ logger
739
+ );
740
+ logger.info("connect-success", { cdpUrl: endpoint, session, port });
741
+ console.log(`Connected to ${endpoint} (session: ${session})`);
742
+ console.log(` Pages found: ${pages.length}`);
743
+ if (pages.length > 0) {
744
+ for (const p of pages.slice(0, 5)) {
745
+ console.log(` ${p.url()}`);
746
+ }
747
+ if (pages.length > 5) {
748
+ console.log(` ... and ${pages.length - 5} more`);
749
+ }
750
+ }
751
+ }
646
752
  function resolvePath(filePath) {
647
753
  return join(process.cwd(), filePath);
648
754
  }
@@ -661,8 +767,10 @@ export {
661
767
  normalizeDomain,
662
768
  normalizeUrl,
663
769
  resolvePath,
770
+ resolveViewport,
664
771
  runClose,
665
772
  runCloseAll,
773
+ runConnect,
666
774
  runOpen,
667
775
  runPages,
668
776
  runSave
@@ -1,23 +1,9 @@
1
1
  import { Logger, createFileLogSink } from "../../shared/logger/index.js";
2
- import { spawnSync } from "node:child_process";
3
- import { cwd } from "node:process";
4
2
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
- import { join, resolve } from "node:path";
3
+ import { join } from "node:path";
4
+ import { resolveLibrettoRepoRoot } from "../../shared/paths/repo-root.js";
6
5
  import { validateSessionName } from "./session.js";
7
- function getRepoRoot() {
8
- const override = process.env.LIBRETTO_REPO_ROOT?.trim();
9
- if (override) {
10
- return resolve(override);
11
- }
12
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
13
- encoding: "utf-8"
14
- });
15
- if (result.status === 0 && result.stdout) {
16
- return result.stdout.trim();
17
- }
18
- return cwd();
19
- }
20
- const REPO_ROOT = getRepoRoot();
6
+ const REPO_ROOT = resolveLibrettoRepoRoot();
21
7
  const LIBRETTO_CONFIG_DIR = join(REPO_ROOT, ".libretto");
22
8
  const LIBRETTO_CONFIG_PATH = join(LIBRETTO_CONFIG_DIR, "config.json");
23
9
  const PROFILES_DIR = join(LIBRETTO_CONFIG_DIR, "profiles");
@@ -63,7 +49,10 @@ function createLoggerForSession(session) {
63
49
  const sessionDir = getSessionDir(session);
64
50
  mkdirSync(sessionDir, { recursive: true });
65
51
  const logFilePath = getSessionLogsPath(session);
66
- return new Logger(["libretto-cli"], [createFileLogSink({ filePath: logFilePath })]);
52
+ return new Logger(
53
+ ["libretto"],
54
+ [createFileLogSink({ filePath: logFilePath })]
55
+ );
67
56
  }
68
57
  async function closeLogger(logger) {
69
58
  if (!logger) return;
@@ -12,7 +12,9 @@ async function installSessionTelemetry(options) {
12
12
  const targetInfo = await cdpSession.send("Target.getTargetInfo");
13
13
  const targetId = targetInfo?.targetInfo?.targetId;
14
14
  if (typeof targetId !== "string" || targetId.length === 0) {
15
- throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
15
+ throw new Error(
16
+ `Could not resolve target id for page at URL "${page.url()}".`
17
+ );
16
18
  }
17
19
  pageIdCache.set(page, targetId);
18
20
  return targetId;
@@ -439,7 +441,8 @@ async function installSessionTelemetry(options) {
439
441
  page.on("response", async (response) => {
440
442
  const request = response.request();
441
443
  const url = request.url();
442
- if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://")) return;
444
+ if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
445
+ return;
443
446
  emitNetwork({
444
447
  pageId,
445
448
  method: request.method(),
@@ -18,9 +18,16 @@ import {
18
18
  serializeSessionState
19
19
  } from "../../shared/state/index.js";
20
20
  const SESSION_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
21
- const SESSION_DEFAULT = "default";
22
21
  const SESSION_DEV_SERVER = "dev-server";
23
22
  const SESSION_BROWSER_AGENT = "browser-agent";
23
+ function generateSessionName() {
24
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
25
+ let id = "";
26
+ for (let i = 0; i < 4; i++) {
27
+ id += chars[Math.floor(Math.random() * chars.length)];
28
+ }
29
+ return `ses-${id}`;
30
+ }
24
31
  function logFileForSession(session) {
25
32
  validateSessionName(session);
26
33
  const dir = getSessionDir(session);
@@ -93,7 +100,7 @@ function throwSessionNotFoundError(session) {
93
100
  }
94
101
  lines.push("");
95
102
  lines.push("Start one with:");
96
- lines.push(` libretto-cli open <url> --session ${session}`);
103
+ lines.push(` libretto open <url> --session ${session}`);
97
104
  throw new Error(lines.join("\n"));
98
105
  }
99
106
  function assertSessionStateExistsOrThrow(session) {
@@ -108,7 +115,10 @@ function readSessionStateOrThrow(session) {
108
115
  throwSessionNotFoundError(session);
109
116
  }
110
117
  try {
111
- return parseSessionStateContent(readFileSync(stateFile, "utf-8"), stateFile);
118
+ return parseSessionStateContent(
119
+ readFileSync(stateFile, "utf-8"),
120
+ stateFile
121
+ );
112
122
  } catch (err) {
113
123
  throw new Error(
114
124
  `Could not read session state for "${session}": ${err instanceof Error ? err.message : String(err)}`
@@ -147,31 +157,34 @@ function setSessionStatus(session, status, logger) {
147
157
  const state = readSessionState(session, logger);
148
158
  if (!state) return;
149
159
  if (state.status === status) return;
150
- writeSessionState({
151
- ...state,
152
- status
153
- }, logger);
160
+ writeSessionState(
161
+ {
162
+ ...state,
163
+ status
164
+ },
165
+ logger
166
+ );
154
167
  }
155
168
  function assertSessionAvailableForStart(session, logger) {
156
169
  const existingState = readSessionState(session, logger);
157
170
  if (!existingState) return;
158
- if (!isPidRunning(existingState.pid)) {
171
+ if (existingState.pid == null || !isPidRunning(existingState.pid)) {
159
172
  setSessionStatus(session, "exited", logger);
160
173
  return;
161
174
  }
162
175
  const endpoint = `http://127.0.0.1:${existingState.port}`;
163
176
  throw new Error(
164
- `Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with: libretto-cli close --session ${session}`
177
+ `Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with: libretto close --session ${session}`
165
178
  );
166
179
  }
167
180
  export {
168
181
  SESSION_BROWSER_AGENT,
169
- SESSION_DEFAULT,
170
182
  SESSION_DEV_SERVER,
171
183
  SESSION_STATE_VERSION,
172
184
  assertSessionAvailableForStart,
173
185
  assertSessionStateExistsOrThrow,
174
186
  clearSessionState,
187
+ generateSessionName,
175
188
  getStateFilePath,
176
189
  listSessionsWithStateFile,
177
190
  logFileForSession,