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.
- package/README.md +106 -36
- 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 +30 -8
- 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 +9 -2
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +132 -29
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +5 -2
- 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 +10 -2
- 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 +6 -13
- 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/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 +7 -2
- 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 +19 -10
- 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 +113 -49
- package/skills/libretto/references/code-generation-rules.md +208 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- 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 +32 -9
- 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 +12 -3
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +178 -41
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +19 -8
- 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 +16 -3
- 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 +15 -18
- 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/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 +12 -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 +161 -148
- 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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
(
|
|
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 (
|
|
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,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
|
}
|