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.
- package/README.md +109 -35
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/execution.js +199 -86
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +24 -5
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +6 -2
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +15 -15
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +132 -54
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +210 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/execution.ts +233 -102
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +144 -77
- package/src/cli/router.ts +13 -21
- package/src/cli/workers/run-integration-runtime.ts +36 -9
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +73 -66
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +19 -25
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
- package/skills/libretto/references/user-action-log.md +0 -31
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Page } from "playwright";
|
|
2
|
-
import {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
id: string;
|
|
16
|
+
errorPatterns: string[];
|
|
17
|
+
userMessage: string;
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
export type DetectedSubmissionError = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
matched: true;
|
|
22
|
+
errorId: string;
|
|
23
|
+
message: string;
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
const detectSubmissionErrorSchema = z.object({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
page: Page,
|
|
48
|
+
error: unknown,
|
|
49
|
+
logContext: string,
|
|
50
|
+
llmClient: LLMClient,
|
|
51
|
+
knownErrors: KnownSubmissionError[] = [],
|
|
52
|
+
logger?: MinimalLogger,
|
|
50
53
|
): Promise<DetectedSubmissionError> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
page: Page,
|
|
15
|
+
fn: () => Promise<T>,
|
|
16
|
+
logger?: MinimalLogger,
|
|
17
|
+
llmClient?: LLMClient,
|
|
15
18
|
): Promise<T> {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if (!llmClient) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
log.info("Action failed, attempting popup recovery", {
|
|
41
|
+
error: error instanceof Error ? error.message : String(error),
|
|
42
|
+
});
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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 (
|
|
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,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 {
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
30
|
+
const runningSessions = getRunningSessions();
|
|
31
|
+
const lines = ["pause(session) requires a non-empty session ID."];
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
if (process.env.NODE_ENV === "production") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
if (typeof session !== "string" || session.trim().length === 0) {
|
|
58
|
+
throwMissingSessionError();
|
|
59
|
+
}
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
const signalPaths = getPauseSignalPaths(session);
|
|
62
|
+
const { pausedSignalPath, resumeSignalPath } = signalPaths;
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
await mkdir(getSessionDir(session), { recursive: true });
|
|
65
|
+
await removeSignalIfExists(resumeSignalPath);
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const details = {
|
|
68
|
+
sessionName: session,
|
|
69
|
+
pausedAt: new Date().toISOString(),
|
|
70
|
+
url: "unknown",
|
|
71
|
+
};
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
78
|
+
console.log(`[pause] Paused (session: ${session})`);
|
|
79
|
+
console.log("[pause] Waiting for resume signal...");
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
await removeSignalIfExists(resumeSignalPath);
|
|
89
|
+
await removeSignalIfExists(pausedSignalPath);
|
|
90
|
+
console.log("[pause] Resume signal received. Continuing workflow...");
|
|
85
91
|
}
|