libretto 0.5.0 → 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 (116) hide show
  1. package/README.md +106 -36
  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 +30 -8
  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 +9 -2
  10. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  11. package/dist/cli/core/browser.js +132 -29
  12. package/dist/cli/core/context.js +4 -1
  13. package/dist/cli/core/session-telemetry.js +5 -2
  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 +10 -2
  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 +6 -13
  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/instrumentation/instrument.js +101 -5
  38. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  39. package/dist/shared/llm/client.js +3 -1
  40. package/dist/shared/logger/index.js +4 -1
  41. package/dist/shared/run/api.js +3 -1
  42. package/dist/shared/run/browser.js +7 -2
  43. package/dist/shared/state/session-state.d.ts +2 -1
  44. package/dist/shared/state/session-state.js +5 -2
  45. package/dist/shared/visualization/ghost-cursor.js +19 -10
  46. package/dist/shared/visualization/highlight.js +9 -6
  47. package/dist/shared/workflow/workflow.d.ts +4 -5
  48. package/dist/shared/workflow/workflow.js +3 -5
  49. package/package.json +6 -2
  50. package/scripts/check-skills-sync.mjs +25 -0
  51. package/scripts/compare-eval-summary.mjs +47 -0
  52. package/scripts/postinstall.mjs +15 -15
  53. package/scripts/prepare-release.sh +97 -0
  54. package/scripts/skills-libretto.mjs +103 -0
  55. package/scripts/summarize-evals.mjs +135 -0
  56. package/scripts/sync-skills.mjs +12 -0
  57. package/skills/libretto/SKILL.md +113 -49
  58. package/skills/libretto/references/code-generation-rules.md +208 -0
  59. package/skills/libretto/references/configuration-file-reference.md +53 -0
  60. package/skills/libretto/references/site-security-review.md +143 -0
  61. package/src/cli/cli.ts +23 -110
  62. package/src/cli/commands/browser.ts +94 -70
  63. package/src/cli/commands/execution.ts +233 -102
  64. package/src/cli/commands/init.ts +32 -9
  65. package/src/cli/commands/logs.ts +7 -7
  66. package/src/cli/commands/shared.ts +36 -37
  67. package/src/cli/commands/snapshot.ts +44 -59
  68. package/src/cli/core/ai-config.ts +12 -3
  69. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  70. package/src/cli/core/browser.ts +178 -41
  71. package/src/cli/core/context.ts +7 -2
  72. package/src/cli/core/session-telemetry.ts +19 -8
  73. package/src/cli/core/session.ts +21 -7
  74. package/src/cli/core/snapshot-analyzer.ts +26 -46
  75. package/src/cli/core/snapshot-api-config.ts +170 -175
  76. package/src/cli/core/telemetry.ts +16 -3
  77. package/src/cli/framework/simple-cli.ts +144 -77
  78. package/src/cli/router.ts +13 -21
  79. package/src/cli/workers/run-integration-runtime.ts +36 -9
  80. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  81. package/src/cli/workers/run-integration-worker.ts +1 -4
  82. package/src/index.ts +73 -66
  83. package/src/runtime/download/download.ts +62 -58
  84. package/src/runtime/download/index.ts +5 -5
  85. package/src/runtime/extract/extract.ts +71 -61
  86. package/src/runtime/network/index.ts +3 -3
  87. package/src/runtime/network/network.ts +99 -93
  88. package/src/runtime/recovery/agent.ts +217 -212
  89. package/src/runtime/recovery/errors.ts +107 -104
  90. package/src/runtime/recovery/index.ts +3 -3
  91. package/src/runtime/recovery/recovery.ts +38 -35
  92. package/src/shared/condense-dom/condense-dom.ts +15 -18
  93. package/src/shared/config/config.ts +0 -19
  94. package/src/shared/config/index.ts +0 -5
  95. package/src/shared/debug/pause.ts +57 -51
  96. package/src/shared/instrumentation/errors.ts +64 -62
  97. package/src/shared/instrumentation/index.ts +5 -5
  98. package/src/shared/instrumentation/instrument.ts +339 -209
  99. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  100. package/src/shared/llm/client.ts +181 -174
  101. package/src/shared/llm/types.ts +39 -39
  102. package/src/shared/logger/index.ts +11 -4
  103. package/src/shared/logger/logger.ts +312 -306
  104. package/src/shared/logger/sinks.ts +118 -114
  105. package/src/shared/paths/paths.ts +50 -49
  106. package/src/shared/paths/repo-root.ts +17 -17
  107. package/src/shared/run/api.ts +5 -1
  108. package/src/shared/run/browser.ts +12 -3
  109. package/src/shared/state/index.ts +9 -9
  110. package/src/shared/state/session-state.ts +46 -43
  111. package/src/shared/visualization/ghost-cursor.ts +161 -148
  112. package/src/shared/visualization/highlight.ts +89 -86
  113. package/src/shared/visualization/index.ts +13 -13
  114. package/src/shared/workflow/workflow.ts +19 -25
  115. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
  116. package/skills/libretto/references/user-action-log.md +0 -31
@@ -1,5 +1,8 @@
1
1
  import type { Page } from "playwright";
2
- import { type MinimalLogger, defaultLogger } from "../../shared/logger/logger.js";
2
+ import {
3
+ type MinimalLogger,
4
+ defaultLogger,
5
+ } from "../../shared/logger/logger.js";
3
6
  import type { LLMClient } from "../../shared/llm/types.js";
4
7
  import { z } from "zod";
5
8
 
@@ -9,27 +12,27 @@ import { z } from "zod";
9
12
  * userMessage is the friendly message returned when matched.
10
13
  */
11
14
  export type KnownSubmissionError = {
12
- id: string;
13
- errorPatterns: string[];
14
- userMessage: string;
15
+ id: string;
16
+ errorPatterns: string[];
17
+ userMessage: string;
15
18
  };
16
19
 
17
20
  export type DetectedSubmissionError = {
18
- matched: true;
19
- errorId: string;
20
- message: string;
21
+ matched: true;
22
+ errorId: string;
23
+ message: string;
21
24
  };
22
25
 
23
26
  const detectSubmissionErrorSchema = z.object({
24
- hasError: z.boolean().describe("Whether an error is visible on the page"),
25
- matchedKnownErrorId: z
26
- .string()
27
- .nullable()
28
- .describe("The ID of the matched known error, or null if no match"),
29
- errorMessage: z
30
- .string()
31
- .nullable()
32
- .describe("The error message visible on screen, or null if no error"),
27
+ hasError: z.boolean().describe("Whether an error is visible on the page"),
28
+ matchedKnownErrorId: z
29
+ .string()
30
+ .nullable()
31
+ .describe("The ID of the matched known error, or null if no match"),
32
+ errorMessage: z
33
+ .string()
34
+ .nullable()
35
+ .describe("The error message visible on screen, or null if no error"),
33
36
  });
34
37
 
35
38
  /**
@@ -41,52 +44,52 @@ const detectSubmissionErrorSchema = z.object({
41
44
  * @throws The original error if no known error matches
42
45
  */
43
46
  export async function detectSubmissionError(
44
- page: Page,
45
- error: unknown,
46
- logContext: string,
47
- llmClient: LLMClient,
48
- knownErrors: KnownSubmissionError[] = [],
49
- logger?: MinimalLogger,
47
+ page: Page,
48
+ error: unknown,
49
+ logContext: string,
50
+ llmClient: LLMClient,
51
+ knownErrors: KnownSubmissionError[] = [],
52
+ logger?: MinimalLogger,
50
53
  ): Promise<DetectedSubmissionError> {
51
- const log = logger ?? defaultLogger;
52
- // Capture screenshot using CDP to handle unresponsive pages
53
- let screenshot: string;
54
- let domSnapshot: string | undefined;
55
-
56
- try {
57
- const cdpClient = await page.context().newCDPSession(page);
58
- await cdpClient.send("Page.enable");
59
- const { data } = await cdpClient.send("Page.captureScreenshot", {
60
- format: "png",
61
- });
62
- screenshot = data;
63
- } catch (screenshotError) {
64
- log.warn(
65
- "Failed to take screenshot via CDP for error detection, skipping LLM analysis",
66
- { screenshotError, originalError: error },
67
- );
68
- throw error;
69
- }
70
-
71
- // Capture DOM snapshot for additional context
72
- try {
73
- const htmlContent = await page.content();
74
- domSnapshot =
75
- htmlContent.length > 50000
76
- ? htmlContent.slice(0, 50000) + "\n... [truncated]"
77
- : htmlContent;
78
- } catch (domError) {
79
- log.warn("Failed to capture DOM snapshot", {
80
- domError: domError instanceof Error ? domError.message : String(domError),
81
- });
82
- }
83
-
84
- const knownErrorsDescription =
85
- knownErrors.length > 0
86
- ? `\nKnown error patterns to look for:\n${knownErrors.map((e, i) => `${i + 1}. ID: "${e.id}" - Patterns: ${e.errorPatterns.join(", ")}`).join("\n")}\n`
87
- : "";
88
-
89
- const prompt = `You are analyzing a screenshot and DOM of a web page to detect if an error occurred during a browser automation process.
54
+ const log = logger ?? defaultLogger;
55
+ // Capture screenshot using CDP to handle unresponsive pages
56
+ let screenshot: string;
57
+ let domSnapshot: string | undefined;
58
+
59
+ try {
60
+ const cdpClient = await page.context().newCDPSession(page);
61
+ await cdpClient.send("Page.enable");
62
+ const { data } = await cdpClient.send("Page.captureScreenshot", {
63
+ format: "png",
64
+ });
65
+ screenshot = data;
66
+ } catch (screenshotError) {
67
+ log.warn(
68
+ "Failed to take screenshot via CDP for error detection, skipping LLM analysis",
69
+ { screenshotError, originalError: error },
70
+ );
71
+ throw error;
72
+ }
73
+
74
+ // Capture DOM snapshot for additional context
75
+ try {
76
+ const htmlContent = await page.content();
77
+ domSnapshot =
78
+ htmlContent.length > 50000
79
+ ? htmlContent.slice(0, 50000) + "\n... [truncated]"
80
+ : htmlContent;
81
+ } catch (domError) {
82
+ log.warn("Failed to capture DOM snapshot", {
83
+ domError: domError instanceof Error ? domError.message : String(domError),
84
+ });
85
+ }
86
+
87
+ const knownErrorsDescription =
88
+ knownErrors.length > 0
89
+ ? `\nKnown error patterns to look for:\n${knownErrors.map((e, i) => `${i + 1}. ID: "${e.id}" - Patterns: ${e.errorPatterns.join(", ")}`).join("\n")}\n`
90
+ : "";
91
+
92
+ const prompt = `You are analyzing a screenshot and DOM of a web page to detect if an error occurred during a browser automation process.
90
93
 
91
94
  Context: ${logContext}
92
95
 
@@ -106,47 +109,47 @@ IMPORTANT:
106
109
 
107
110
  ${domSnapshot ? `<dom_snapshot>\n${domSnapshot}\n</dom_snapshot>` : ""}`;
108
111
 
109
- const result = await llmClient.generateObjectFromMessages({
110
- schema: detectSubmissionErrorSchema,
111
- messages: [
112
- {
113
- role: "user",
114
- content: [
115
- { type: "text", text: prompt },
116
- { type: "image", image: `data:image/png;base64,${screenshot}` },
117
- ],
118
- },
119
- ],
120
- temperature: 0,
121
- });
122
-
123
- if (!result.hasError) {
124
- log.info("No error detected by LLM", { result });
125
- }
126
-
127
- // Check if it matches a known error
128
- if (result.matchedKnownErrorId) {
129
- const knownError = knownErrors.find(
130
- (e) => e.id === result.matchedKnownErrorId,
131
- );
132
- if (knownError) {
133
- log.warn(logContext, {
134
- error,
135
- browserError: result.errorMessage,
136
- knownErrorId: result.matchedKnownErrorId,
137
- });
138
- return {
139
- matched: true,
140
- errorId: knownError.id,
141
- message: knownError.userMessage,
142
- };
143
- }
144
- }
145
-
146
- // Log and re-throw for unknown errors
147
- log.warn(logContext, {
148
- error,
149
- browserError: result.errorMessage,
150
- });
151
- throw error;
112
+ const result = await llmClient.generateObjectFromMessages({
113
+ schema: detectSubmissionErrorSchema,
114
+ messages: [
115
+ {
116
+ role: "user",
117
+ content: [
118
+ { type: "text", text: prompt },
119
+ { type: "image", image: `data:image/png;base64,${screenshot}` },
120
+ ],
121
+ },
122
+ ],
123
+ temperature: 0,
124
+ });
125
+
126
+ if (!result.hasError) {
127
+ log.info("No error detected by LLM", { result });
128
+ }
129
+
130
+ // Check if it matches a known error
131
+ if (result.matchedKnownErrorId) {
132
+ const knownError = knownErrors.find(
133
+ (e) => e.id === result.matchedKnownErrorId,
134
+ );
135
+ if (knownError) {
136
+ log.warn(logContext, {
137
+ error,
138
+ browserError: result.errorMessage,
139
+ knownErrorId: result.matchedKnownErrorId,
140
+ });
141
+ return {
142
+ matched: true,
143
+ errorId: knownError.id,
144
+ message: knownError.userMessage,
145
+ };
146
+ }
147
+ }
148
+
149
+ // Log and re-throw for unknown errors
150
+ log.warn(logContext, {
151
+ error,
152
+ browserError: result.errorMessage,
153
+ });
154
+ throw error;
152
155
  }
@@ -1,7 +1,7 @@
1
1
  export { executeRecoveryAgent } from "./agent.js";
2
2
  export { attemptWithRecovery } from "./recovery.js";
3
3
  export {
4
- detectSubmissionError,
5
- type KnownSubmissionError,
6
- type DetectedSubmissionError,
4
+ detectSubmissionError,
5
+ type KnownSubmissionError,
6
+ type DetectedSubmissionError,
7
7
  } from "./errors.js";
@@ -1,5 +1,8 @@
1
1
  import type { Page } from "playwright";
2
- import { type MinimalLogger, defaultLogger } from "../../shared/logger/logger.js";
2
+ import {
3
+ type MinimalLogger,
4
+ defaultLogger,
5
+ } from "../../shared/logger/logger.js";
3
6
  import type { LLMClient } from "../../shared/llm/types.js";
4
7
  import { executeRecoveryAgent } from "./agent.js";
5
8
 
@@ -8,43 +11,43 @@ import { executeRecoveryAgent } from "./agent.js";
8
11
  * (if an LLM client is provided) and retries the function once.
9
12
  */
10
13
  export async function attemptWithRecovery<T>(
11
- page: Page,
12
- fn: () => Promise<T>,
13
- logger?: MinimalLogger,
14
- llmClient?: LLMClient,
14
+ page: Page,
15
+ fn: () => Promise<T>,
16
+ logger?: MinimalLogger,
17
+ llmClient?: LLMClient,
15
18
  ): Promise<T> {
16
- const log = logger ?? defaultLogger;
17
- try {
18
- return await fn();
19
- } catch (error) {
20
- // Don't attempt recovery if the browser/page is closed
21
- if (
22
- error instanceof Error &&
23
- (error.message.includes("Target closed") ||
24
- error.message.includes("browser has been closed") ||
25
- error.message.includes("context or browser has been closed"))
26
- ) {
27
- log.warn("Page/browser has been closed, cannot recover", {
28
- error: error.message,
29
- });
30
- throw error;
31
- }
19
+ const log = logger ?? defaultLogger;
20
+ try {
21
+ return await fn();
22
+ } catch (error) {
23
+ // Don't attempt recovery if the browser/page is closed
24
+ if (
25
+ error instanceof Error &&
26
+ (error.message.includes("Target closed") ||
27
+ error.message.includes("browser has been closed") ||
28
+ error.message.includes("context or browser has been closed"))
29
+ ) {
30
+ log.warn("Page/browser has been closed, cannot recover", {
31
+ error: error.message,
32
+ });
33
+ throw error;
34
+ }
32
35
 
33
- if (!llmClient) {
34
- throw error;
35
- }
36
+ if (!llmClient) {
37
+ throw error;
38
+ }
36
39
 
37
- log.info("Action failed, attempting popup recovery", {
38
- error: error instanceof Error ? error.message : String(error),
39
- });
40
+ log.info("Action failed, attempting popup recovery", {
41
+ error: error instanceof Error ? error.message : String(error),
42
+ });
40
43
 
41
- await executeRecoveryAgent(
42
- page,
43
- "Look at the page to see if there is a popup blocking the screen. If so, close the popup.",
44
- log,
45
- llmClient,
46
- );
44
+ await executeRecoveryAgent(
45
+ page,
46
+ "Look at the page to see if there is a popup blocking the screen. If so, close the popup.",
47
+ log,
48
+ llmClient,
49
+ );
47
50
 
48
- return await fn();
49
- }
51
+ return await fn();
52
+ }
50
53
  }
@@ -67,12 +67,7 @@ const STATE_ATTRS = new Set([
67
67
  "open",
68
68
  "multiple",
69
69
  ]);
70
- const BOOLEAN_ATTRS = new Set([
71
- ...STATE_ATTRS,
72
- "async",
73
- "defer",
74
- "nomodule",
75
- ]);
70
+ const BOOLEAN_ATTRS = new Set([...STATE_ATTRS, "async", "defer", "nomodule"]);
76
71
  const EMPTY_VALUE_DROP_ATTRS = new Set([
77
72
  "alt",
78
73
  "autocomplete",
@@ -232,12 +227,8 @@ export function condenseDom(html: string): CondenseDomResult {
232
227
 
233
228
  const hasAriaLabel = /aria-label\s*=/i.test(attrs);
234
229
  if (!hasAriaLabel) {
235
- const titleMatch = inner.match(
236
- /<title[^>]*>([^<]+)<\/title>/i,
237
- );
238
- const descMatch = inner.match(
239
- /<desc[^>]*>([^<]+)<\/desc>/i,
240
- );
230
+ const titleMatch = inner.match(/<title[^>]*>([^<]+)<\/title>/i);
231
+ const descMatch = inner.match(/<desc[^>]*>([^<]+)<\/desc>/i);
241
232
  const labelText =
242
233
  titleMatch?.[1]?.trim() || descMatch?.[1]?.trim();
243
234
  if (labelText) {
@@ -342,7 +333,12 @@ export function condenseDom(html: string): CondenseDomResult {
342
333
  function rewriteTagAttributes(html: string): string {
343
334
  return html.replace(
344
335
  OPEN_TAG_PATTERN,
345
- (match, rawTagName: string, rawAttrs: string | undefined, selfClosing: string) => {
336
+ (
337
+ match,
338
+ rawTagName: string,
339
+ rawAttrs: string | undefined,
340
+ selfClosing: string,
341
+ ) => {
346
342
  const tagName = rawTagName.toLowerCase();
347
343
  if (!rawAttrs?.trim()) return match;
348
344
 
@@ -431,10 +427,7 @@ function serializePreservedAttribute(attr: ParsedAttribute): string | null {
431
427
  return attr.rawToken;
432
428
  }
433
429
 
434
- function shouldDropEmptyValue(
435
- name: string,
436
- value: string | null,
437
- ): boolean {
430
+ function shouldDropEmptyValue(name: string, value: string | null): boolean {
438
431
  if (value === null) return false;
439
432
  if (value.trim()) return false;
440
433
  if (name.startsWith("aria-")) return true;
@@ -550,7 +543,11 @@ function shouldKeepCustomDataAttribute(
550
543
  function looksMeaningfulToken(value: string): boolean {
551
544
  if (!/^[a-z][a-z0-9-]{1,40}$/i.test(value)) return false;
552
545
  if (!/[a-z]{3}/i.test(value)) return false;
553
- if (/(track|metric|telemetry|analytics|component|display|loaded|token|dps|color|screen|strict|rehydr|fetch)/i.test(value)) {
546
+ if (
547
+ /(track|metric|telemetry|analytics|component|display|loaded|token|dps|color|screen|strict|rehydr|fetch)/i.test(
548
+ value,
549
+ )
550
+ ) {
554
551
  return false;
555
552
  }
556
553
  return true;
@@ -1,22 +1,3 @@
1
1
  /**
2
2
  * Runtime configuration for libretto.
3
- *
4
- * Values are derived from environment variables only.
5
3
  */
6
-
7
- export function isDebugMode(): boolean {
8
- return process.env.LIBRETTO_DEBUG === "true";
9
- }
10
-
11
- export function isDryRun(): boolean {
12
- const explicit = process.env.LIBRETTO_DRY_RUN;
13
- if (explicit !== undefined) {
14
- return explicit === "true";
15
- }
16
-
17
- return process.env.NODE_ENV === "development";
18
- }
19
-
20
- export function shouldPauseBeforeMutation(): boolean {
21
- return isDryRun() && isDebugMode();
22
- }
@@ -1,5 +0,0 @@
1
- export {
2
- isDebugMode,
3
- isDryRun,
4
- shouldPauseBeforeMutation,
5
- } from "./config.js";
@@ -1,37 +1,43 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { getSessionDir } from "../../cli/core/context.js";
4
- import { getPauseSignalPaths, removeSignalIfExists } from "../../cli/core/pause-signals.js";
5
- import { listSessionsWithStateFile, readSessionState } from "../../cli/core/session.js";
4
+ import {
5
+ getPauseSignalPaths,
6
+ removeSignalIfExists,
7
+ } from "../../cli/core/pause-signals.js";
8
+ import {
9
+ listSessionsWithStateFile,
10
+ readSessionState,
11
+ } from "../../cli/core/session.js";
6
12
 
7
13
  function isPidRunning(pid: number): boolean {
8
- try {
9
- process.kill(pid, 0);
10
- return true;
11
- } catch {
12
- return false;
13
- }
14
+ try {
15
+ process.kill(pid, 0);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
14
20
  }
15
21
 
16
22
  function getRunningSessions(): string[] {
17
- return listSessionsWithStateFile().filter((candidate) => {
18
- const state = readSessionState(candidate);
19
- return state !== null && isPidRunning(state.pid);
20
- });
23
+ return listSessionsWithStateFile().filter((candidate) => {
24
+ const state = readSessionState(candidate);
25
+ return state !== null && state.pid != null && isPidRunning(state.pid);
26
+ });
21
27
  }
22
28
 
23
29
  function throwMissingSessionError(): never {
24
- const runningSessions = getRunningSessions();
25
- const lines = ["pause(session) requires a non-empty session ID."];
30
+ const runningSessions = getRunningSessions();
31
+ const lines = ["pause(session) requires a non-empty session ID."];
26
32
 
27
- if (runningSessions.length > 0) {
28
- lines.push("", "Running sessions:");
29
- for (const runningSession of runningSessions) {
30
- lines.push(` ${runningSession}`);
31
- }
32
- }
33
+ if (runningSessions.length > 0) {
34
+ lines.push("", "Running sessions:");
35
+ for (const runningSession of runningSessions) {
36
+ lines.push(` ${runningSession}`);
37
+ }
38
+ }
33
39
 
34
- throw new Error(lines.join("\n"));
40
+ throw new Error(lines.join("\n"));
35
41
  }
36
42
 
37
43
  /**
@@ -44,42 +50,42 @@ function throwMissingSessionError(): never {
44
50
  * Import directly: `import { pause } from "libretto";`
45
51
  */
46
52
  export async function pause(session: string): Promise<void> {
47
- if (process.env.NODE_ENV === "production") {
48
- return;
49
- }
53
+ if (process.env.NODE_ENV === "production") {
54
+ return;
55
+ }
50
56
 
51
- if (typeof session !== "string" || session.trim().length === 0) {
52
- throwMissingSessionError();
53
- }
57
+ if (typeof session !== "string" || session.trim().length === 0) {
58
+ throwMissingSessionError();
59
+ }
54
60
 
55
- const signalPaths = getPauseSignalPaths(session);
56
- const { pausedSignalPath, resumeSignalPath } = signalPaths;
61
+ const signalPaths = getPauseSignalPaths(session);
62
+ const { pausedSignalPath, resumeSignalPath } = signalPaths;
57
63
 
58
- await mkdir(getSessionDir(session), { recursive: true });
59
- await removeSignalIfExists(resumeSignalPath);
64
+ await mkdir(getSessionDir(session), { recursive: true });
65
+ await removeSignalIfExists(resumeSignalPath);
60
66
 
61
- const details = {
62
- sessionName: session,
63
- pausedAt: new Date().toISOString(),
64
- url: "unknown",
65
- };
67
+ const details = {
68
+ sessionName: session,
69
+ pausedAt: new Date().toISOString(),
70
+ url: "unknown",
71
+ };
66
72
 
67
- // Try to read the current page URL from the process (best-effort).
68
- // The standalone pause doesn't have access to the page object,
69
- // so we just record what we can.
70
- await writeFile(pausedSignalPath, JSON.stringify(details, null, 2), "utf8");
73
+ // Try to read the current page URL from the process (best-effort).
74
+ // The standalone pause doesn't have access to the page object,
75
+ // so we just record what we can.
76
+ await writeFile(pausedSignalPath, JSON.stringify(details, null, 2), "utf8");
71
77
 
72
- console.log(`[pause] Paused (session: ${session})`);
73
- console.log("[pause] Waiting for resume signal...");
78
+ console.log(`[pause] Paused (session: ${session})`);
79
+ console.log("[pause] Waiting for resume signal...");
74
80
 
75
- const RESUME_POLL_INTERVAL_MS = 250;
76
- while (!existsSync(resumeSignalPath)) {
77
- await new Promise((resolve) =>
78
- setTimeout(resolve, RESUME_POLL_INTERVAL_MS),
79
- );
80
- }
81
+ const RESUME_POLL_INTERVAL_MS = 250;
82
+ while (!existsSync(resumeSignalPath)) {
83
+ await new Promise((resolve) =>
84
+ setTimeout(resolve, RESUME_POLL_INTERVAL_MS),
85
+ );
86
+ }
81
87
 
82
- await removeSignalIfExists(resumeSignalPath);
83
- await removeSignalIfExists(pausedSignalPath);
84
- console.log("[pause] Resume signal received. Continuing workflow...");
88
+ await removeSignalIfExists(resumeSignalPath);
89
+ await removeSignalIfExists(pausedSignalPath);
90
+ console.log("[pause] Resume signal received. Continuing workflow...");
85
91
  }