libretto 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +109 -35
  2. package/dist/cli/cli.js +22 -97
  3. package/dist/cli/commands/browser.js +86 -59
  4. package/dist/cli/commands/execution.js +199 -86
  5. package/dist/cli/commands/init.js +34 -29
  6. package/dist/cli/commands/logs.js +4 -5
  7. package/dist/cli/commands/shared.js +30 -29
  8. package/dist/cli/commands/snapshot.js +26 -39
  9. package/dist/cli/core/ai-config.js +21 -4
  10. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  11. package/dist/cli/core/browser.js +207 -37
  12. package/dist/cli/core/context.js +4 -1
  13. package/dist/cli/core/session-telemetry.js +434 -174
  14. package/dist/cli/core/session.js +21 -8
  15. package/dist/cli/core/snapshot-analyzer.js +14 -31
  16. package/dist/cli/core/snapshot-api-config.js +2 -6
  17. package/dist/cli/core/telemetry.js +20 -4
  18. package/dist/cli/framework/simple-cli.js +45 -25
  19. package/dist/cli/router.js +14 -21
  20. package/dist/cli/workers/run-integration-runtime.js +24 -5
  21. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  22. package/dist/cli/workers/run-integration-worker.js +1 -4
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.js +7 -10
  25. package/dist/runtime/download/download.js +5 -1
  26. package/dist/runtime/extract/extract.js +11 -2
  27. package/dist/runtime/network/network.js +8 -1
  28. package/dist/runtime/recovery/agent.js +6 -2
  29. package/dist/runtime/recovery/errors.js +3 -1
  30. package/dist/runtime/recovery/recovery.js +3 -1
  31. package/dist/shared/condense-dom/condense-dom.js +17 -69
  32. package/dist/shared/config/config.d.ts +1 -9
  33. package/dist/shared/config/config.js +0 -18
  34. package/dist/shared/config/index.d.ts +2 -1
  35. package/dist/shared/config/index.js +0 -10
  36. package/dist/shared/debug/pause.js +9 -3
  37. package/dist/shared/dom-semantics.d.ts +8 -0
  38. package/dist/shared/dom-semantics.js +69 -0
  39. package/dist/shared/instrumentation/instrument.js +101 -5
  40. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  41. package/dist/shared/llm/client.js +3 -1
  42. package/dist/shared/logger/index.js +4 -1
  43. package/dist/shared/run/api.js +3 -1
  44. package/dist/shared/run/browser.js +47 -3
  45. package/dist/shared/state/session-state.d.ts +2 -1
  46. package/dist/shared/state/session-state.js +5 -2
  47. package/dist/shared/visualization/ghost-cursor.js +36 -14
  48. package/dist/shared/visualization/highlight.js +9 -6
  49. package/dist/shared/workflow/workflow.d.ts +4 -5
  50. package/dist/shared/workflow/workflow.js +3 -5
  51. package/package.json +6 -2
  52. package/scripts/check-skills-sync.mjs +25 -0
  53. package/scripts/compare-eval-summary.mjs +47 -0
  54. package/scripts/postinstall.mjs +15 -15
  55. package/scripts/prepare-release.sh +97 -0
  56. package/scripts/skills-libretto.mjs +103 -0
  57. package/scripts/summarize-evals.mjs +135 -0
  58. package/scripts/sync-skills.mjs +12 -0
  59. package/skills/libretto/SKILL.md +132 -54
  60. package/skills/libretto/references/action-logs.md +101 -0
  61. package/skills/libretto/references/auth-profiles.md +1 -2
  62. package/skills/libretto/references/code-generation-rules.md +210 -0
  63. package/skills/libretto/references/configuration-file-reference.md +53 -0
  64. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  65. package/skills/libretto/references/site-security-review.md +143 -0
  66. package/src/cli/cli.ts +23 -110
  67. package/src/cli/commands/browser.ts +94 -70
  68. package/src/cli/commands/execution.ts +233 -102
  69. package/src/cli/commands/init.ts +37 -33
  70. package/src/cli/commands/logs.ts +7 -7
  71. package/src/cli/commands/shared.ts +36 -37
  72. package/src/cli/commands/snapshot.ts +44 -59
  73. package/src/cli/core/ai-config.ts +24 -4
  74. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  75. package/src/cli/core/browser.ts +260 -49
  76. package/src/cli/core/context.ts +7 -2
  77. package/src/cli/core/session-telemetry.ts +449 -197
  78. package/src/cli/core/session.ts +21 -7
  79. package/src/cli/core/snapshot-analyzer.ts +26 -46
  80. package/src/cli/core/snapshot-api-config.ts +170 -175
  81. package/src/cli/core/telemetry.ts +39 -4
  82. package/src/cli/framework/simple-cli.ts +144 -77
  83. package/src/cli/router.ts +13 -21
  84. package/src/cli/workers/run-integration-runtime.ts +36 -9
  85. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  86. package/src/cli/workers/run-integration-worker.ts +1 -4
  87. package/src/index.ts +73 -66
  88. package/src/runtime/download/download.ts +62 -58
  89. package/src/runtime/download/index.ts +5 -5
  90. package/src/runtime/extract/extract.ts +71 -61
  91. package/src/runtime/network/index.ts +3 -3
  92. package/src/runtime/network/network.ts +99 -93
  93. package/src/runtime/recovery/agent.ts +217 -212
  94. package/src/runtime/recovery/errors.ts +107 -104
  95. package/src/runtime/recovery/index.ts +3 -3
  96. package/src/runtime/recovery/recovery.ts +38 -35
  97. package/src/shared/condense-dom/condense-dom.ts +27 -82
  98. package/src/shared/config/config.ts +0 -19
  99. package/src/shared/config/index.ts +0 -5
  100. package/src/shared/debug/pause.ts +57 -51
  101. package/src/shared/dom-semantics.ts +68 -0
  102. package/src/shared/instrumentation/errors.ts +64 -62
  103. package/src/shared/instrumentation/index.ts +5 -5
  104. package/src/shared/instrumentation/instrument.ts +339 -209
  105. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  106. package/src/shared/llm/client.ts +181 -174
  107. package/src/shared/llm/types.ts +39 -39
  108. package/src/shared/logger/index.ts +11 -4
  109. package/src/shared/logger/logger.ts +312 -306
  110. package/src/shared/logger/sinks.ts +118 -114
  111. package/src/shared/paths/paths.ts +50 -49
  112. package/src/shared/paths/repo-root.ts +17 -17
  113. package/src/shared/run/api.ts +5 -1
  114. package/src/shared/run/browser.ts +65 -3
  115. package/src/shared/state/index.ts +9 -9
  116. package/src/shared/state/session-state.ts +46 -43
  117. package/src/shared/visualization/ghost-cursor.ts +180 -149
  118. package/src/shared/visualization/highlight.ts +89 -86
  119. package/src/shared/visualization/index.ts +13 -13
  120. package/src/shared/workflow/workflow.ts +19 -25
  121. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
  122. package/skills/libretto/references/user-action-log.md +0 -31
@@ -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
  }
@@ -20,6 +20,14 @@
20
20
  * 12. Whitespace — collapse (preserve <pre> content)
21
21
  */
22
22
 
23
+ import {
24
+ filterSemanticClasses,
25
+ INTERACTIVE_ROLE_NAMES,
26
+ INTERACTIVE_TAG_NAMES,
27
+ TEST_ATTRIBUTE_NAMES,
28
+ TRUSTED_ATTRIBUTE_NAMES,
29
+ } from "../dom-semantics.js";
30
+
23
31
  export type CondenseDomResult = {
24
32
  /** The condensed HTML string. Valid, parseable HTML. */
25
33
  html: string;
@@ -37,25 +45,8 @@ type ParsedAttribute = {
37
45
  value: string | null;
38
46
  };
39
47
 
40
- const TEST_ATTRS = new Set(["data-testid", "data-test", "data-qa", "data-cy"]);
41
- const TRUSTED_ATTRS = new Set([
42
- "id",
43
- "name",
44
- "for",
45
- "tabindex",
46
- "contenteditable",
47
- "role",
48
- "title",
49
- "alt",
50
- "type",
51
- "value",
52
- "placeholder",
53
- "autocomplete",
54
- "href",
55
- "action",
56
- "method",
57
- "src",
58
- ]);
48
+ const TEST_ATTRS: Set<string> = new Set(TEST_ATTRIBUTE_NAMES);
49
+ const TRUSTED_ATTRS: Set<string> = new Set(TRUSTED_ATTRIBUTE_NAMES);
59
50
  const STATE_ATTRS = new Set([
60
51
  "disabled",
61
52
  "hidden",
@@ -67,12 +58,7 @@ const STATE_ATTRS = new Set([
67
58
  "open",
68
59
  "multiple",
69
60
  ]);
70
- const BOOLEAN_ATTRS = new Set([
71
- ...STATE_ATTRS,
72
- "async",
73
- "defer",
74
- "nomodule",
75
- ]);
61
+ const BOOLEAN_ATTRS = new Set([...STATE_ATTRS, "async", "defer", "nomodule"]);
76
62
  const EMPTY_VALUE_DROP_ATTRS = new Set([
77
63
  "alt",
78
64
  "autocomplete",
@@ -99,28 +85,8 @@ const SCRIPT_ATTRS = new Set([
99
85
  "referrerpolicy",
100
86
  ]);
101
87
  const STYLE_TAG_ATTRS = new Set(["media", "type", "nonce", "title"]);
102
- const INTERACTIVE_TAGS = new Set([
103
- "a",
104
- "button",
105
- "input",
106
- "select",
107
- "textarea",
108
- "form",
109
- "details",
110
- "dialog",
111
- "label",
112
- ]);
113
- const INTERACTIVE_ROLES = new Set([
114
- "button",
115
- "link",
116
- "tab",
117
- "menuitem",
118
- "checkbox",
119
- "radio",
120
- "switch",
121
- "slider",
122
- "combobox",
123
- ]);
88
+ const INTERACTIVE_TAGS: Set<string> = new Set(INTERACTIVE_TAG_NAMES);
89
+ const INTERACTIVE_ROLES: Set<string> = new Set(INTERACTIVE_ROLE_NAMES);
124
90
  const OPEN_TAG_PATTERN =
125
91
  /<([a-zA-Z][\w:-]*)(\s(?:[^"'<>/]|"[^"]*"|'[^']*')*)?\s*(\/?)>/g;
126
92
 
@@ -232,12 +198,8 @@ export function condenseDom(html: string): CondenseDomResult {
232
198
 
233
199
  const hasAriaLabel = /aria-label\s*=/i.test(attrs);
234
200
  if (!hasAriaLabel) {
235
- const titleMatch = inner.match(
236
- /<title[^>]*>([^<]+)<\/title>/i,
237
- );
238
- const descMatch = inner.match(
239
- /<desc[^>]*>([^<]+)<\/desc>/i,
240
- );
201
+ const titleMatch = inner.match(/<title[^>]*>([^<]+)<\/title>/i);
202
+ const descMatch = inner.match(/<desc[^>]*>([^<]+)<\/desc>/i);
241
203
  const labelText =
242
204
  titleMatch?.[1]?.trim() || descMatch?.[1]?.trim();
243
205
  if (labelText) {
@@ -342,7 +304,12 @@ export function condenseDom(html: string): CondenseDomResult {
342
304
  function rewriteTagAttributes(html: string): string {
343
305
  return html.replace(
344
306
  OPEN_TAG_PATTERN,
345
- (match, rawTagName: string, rawAttrs: string | undefined, selfClosing: string) => {
307
+ (
308
+ match,
309
+ rawTagName: string,
310
+ rawAttrs: string | undefined,
311
+ selfClosing: string,
312
+ ) => {
346
313
  const tagName = rawTagName.toLowerCase();
347
314
  if (!rawAttrs?.trim()) return match;
348
315
 
@@ -431,10 +398,7 @@ function serializePreservedAttribute(attr: ParsedAttribute): string | null {
431
398
  return attr.rawToken;
432
399
  }
433
400
 
434
- function shouldDropEmptyValue(
435
- name: string,
436
- value: string | null,
437
- ): boolean {
401
+ function shouldDropEmptyValue(name: string, value: string | null): boolean {
438
402
  if (value === null) return false;
439
403
  if (value.trim()) return false;
440
404
  if (name.startsWith("aria-")) return true;
@@ -465,29 +429,6 @@ function normalizeUrlValue(value: string): string {
465
429
  }
466
430
  }
467
431
 
468
- function filterSemanticClasses(value: string): string {
469
- const classes = value.split(/\s+/).filter(Boolean);
470
- const kept = classes.filter((cls) => !isObfuscatedClass(cls));
471
- return kept.join(" ");
472
- }
473
-
474
- /**
475
- * Heuristic: a class name is "obfuscated" if it looks like a hash or random ID
476
- * rather than a human-readable semantic name.
477
- */
478
- function isObfuscatedClass(cls: string): boolean {
479
- if (cls.length > 80) return true;
480
- if (/^_?[0-9a-f]{6,}$/i.test(cls)) return true;
481
- if (/^[a-z]+_[0-9a-f]{4,}$/i.test(cls)) return true;
482
- if (/^[a-z]{1,2}[0-9]{2,}$/i.test(cls)) return true;
483
-
484
- const digits = (cls.match(/[0-9]/g) || []).length;
485
- const letters = (cls.match(/[a-zA-Z]/g) || []).length;
486
- if (cls.length >= 6 && digits >= letters * 0.5 && digits >= 2) return true;
487
-
488
- return false;
489
- }
490
-
491
432
  function parseAttributes(rawAttrs: string): ParsedAttribute[] {
492
433
  const attrs: ParsedAttribute[] = [];
493
434
  const attrPattern =
@@ -550,7 +491,11 @@ function shouldKeepCustomDataAttribute(
550
491
  function looksMeaningfulToken(value: string): boolean {
551
492
  if (!/^[a-z][a-z0-9-]{1,40}$/i.test(value)) return false;
552
493
  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)) {
494
+ if (
495
+ /(track|metric|telemetry|analytics|component|display|loaded|token|dps|color|screen|strict|rehydr|fetch)/i.test(
496
+ value,
497
+ )
498
+ ) {
554
499
  return false;
555
500
  }
556
501
  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
  }