libretto 0.4.4 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -36
- package/dist/cli/cli.js +39 -113
- package/dist/cli/commands/ai.js +1 -1
- package/dist/cli/commands/browser.js +87 -60
- package/dist/cli/commands/execution.js +201 -88
- package/dist/cli/commands/init.js +30 -8
- package/dist/cli/commands/logs.js +5 -6
- 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 +141 -33
- package/dist/cli/core/context.js +7 -18
- package/dist/cli/core/session-telemetry.js +5 -2
- package/dist/cli/core/session.js +23 -10
- package/dist/cli/core/snapshot-analyzer.js +16 -33
- 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 +26 -7
- 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/paths/paths.js +2 -1
- package/dist/shared/paths/repo-root.d.ts +3 -0
- package/dist/shared/paths/repo-root.js +24 -0
- 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 +11 -8
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +26 -17
- 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 +130 -377
- package/skills/libretto/references/auth-profiles.md +30 -0
- package/skills/libretto/{code-generation-rules.md → references/code-generation-rules.md} +27 -42
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +29 -0
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +86 -0
- package/src/cli/commands/ai.ts +35 -0
- package/src/cli/commands/browser.ts +189 -0
- package/src/cli/commands/execution.ts +822 -0
- package/src/cli/commands/init.ts +350 -0
- package/src/cli/commands/logs.ts +128 -0
- package/src/cli/commands/shared.ts +69 -0
- package/src/cli/commands/snapshot.ts +312 -0
- package/src/cli/core/ai-config.ts +264 -0
- package/src/cli/core/api-snapshot-analyzer.ts +108 -0
- package/src/cli/core/browser.ts +976 -0
- package/src/cli/core/context.ts +127 -0
- package/src/cli/core/pause-signals.ts +35 -0
- package/src/cli/core/session-telemetry.ts +564 -0
- package/src/cli/core/session.ts +223 -0
- package/src/cli/core/snapshot-analyzer.ts +855 -0
- package/src/cli/core/snapshot-api-config.ts +231 -0
- package/src/cli/core/telemetry.ts +459 -0
- package/src/cli/framework/simple-cli.ts +1340 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/router.ts +20 -0
- package/src/cli/workers/run-integration-runtime.ts +338 -0
- package/src/cli/workers/run-integration-worker-protocol.ts +16 -0
- package/src/cli/workers/run-integration-worker.ts +72 -0
- package/src/index.ts +127 -0
- package/src/runtime/download/download.ts +104 -0
- package/src/runtime/download/index.ts +7 -0
- package/src/runtime/extract/extract.ts +102 -0
- package/src/runtime/extract/index.ts +1 -0
- package/src/runtime/network/index.ts +5 -0
- package/src/runtime/network/network.ts +119 -0
- package/{dist/runtime/recovery/agent.cjs → src/runtime/recovery/agent.ts} +114 -76
- package/src/runtime/recovery/errors.ts +155 -0
- package/src/runtime/recovery/index.ts +7 -0
- package/src/runtime/recovery/recovery.ts +53 -0
- package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +249 -124
- package/src/shared/config/config.ts +3 -0
- package/src/shared/config/index.ts +0 -0
- package/src/shared/debug/index.ts +1 -0
- package/src/shared/debug/pause.ts +91 -0
- package/src/shared/instrumentation/errors.ts +84 -0
- package/src/shared/instrumentation/index.ts +9 -0
- package/src/shared/instrumentation/instrument.ts +406 -0
- package/src/shared/llm/ai-sdk-adapter.ts +81 -0
- package/{dist/shared/llm/client.cjs → src/shared/llm/client.ts} +86 -80
- package/src/shared/llm/index.ts +3 -0
- package/src/shared/llm/types.ts +63 -0
- package/src/shared/logger/index.ts +13 -0
- package/src/shared/logger/logger.ts +358 -0
- package/src/shared/logger/sinks.ts +148 -0
- package/src/shared/paths/paths.ts +110 -0
- package/src/shared/paths/repo-root.ts +27 -0
- package/src/shared/run/api.ts +6 -0
- package/src/shared/run/browser.ts +107 -0
- package/src/shared/state/index.ts +11 -0
- package/src/shared/state/session-state.ts +77 -0
- package/src/shared/visualization/ghost-cursor.ts +213 -0
- package/src/shared/visualization/highlight.ts +149 -0
- package/src/shared/visualization/index.ts +18 -0
- package/src/shared/workflow/workflow.ts +36 -0
- package/dist/index.cjs +0 -144
- package/dist/index.d.cts +0 -21
- package/dist/runtime/download/download.cjs +0 -70
- package/dist/runtime/download/download.d.cts +0 -35
- package/dist/runtime/download/index.cjs +0 -30
- package/dist/runtime/download/index.d.cts +0 -3
- package/dist/runtime/extract/extract.cjs +0 -88
- package/dist/runtime/extract/extract.d.cts +0 -23
- package/dist/runtime/extract/index.cjs +0 -28
- package/dist/runtime/extract/index.d.cts +0 -5
- package/dist/runtime/network/index.cjs +0 -28
- package/dist/runtime/network/index.d.cts +0 -4
- package/dist/runtime/network/network.cjs +0 -91
- package/dist/runtime/network/network.d.cts +0 -28
- package/dist/runtime/recovery/agent.d.cts +0 -13
- package/dist/runtime/recovery/errors.cjs +0 -124
- package/dist/runtime/recovery/errors.d.cts +0 -31
- package/dist/runtime/recovery/index.cjs +0 -34
- package/dist/runtime/recovery/index.d.cts +0 -7
- package/dist/runtime/recovery/recovery.cjs +0 -55
- package/dist/runtime/recovery/recovery.d.cts +0 -12
- package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
- package/dist/shared/config/config.cjs +0 -44
- package/dist/shared/config/config.d.cts +0 -10
- package/dist/shared/config/index.cjs +0 -32
- package/dist/shared/config/index.d.cts +0 -1
- package/dist/shared/debug/index.cjs +0 -28
- package/dist/shared/debug/index.d.cts +0 -1
- package/dist/shared/debug/pause.cjs +0 -86
- package/dist/shared/debug/pause.d.cts +0 -12
- package/dist/shared/instrumentation/errors.cjs +0 -81
- package/dist/shared/instrumentation/errors.d.cts +0 -12
- package/dist/shared/instrumentation/index.cjs +0 -35
- package/dist/shared/instrumentation/index.d.cts +0 -6
- package/dist/shared/instrumentation/instrument.cjs +0 -206
- package/dist/shared/instrumentation/instrument.d.cts +0 -32
- package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
- package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
- package/dist/shared/llm/client.d.cts +0 -13
- package/dist/shared/llm/index.cjs +0 -31
- package/dist/shared/llm/index.d.cts +0 -5
- package/dist/shared/llm/types.cjs +0 -16
- package/dist/shared/llm/types.d.cts +0 -67
- package/dist/shared/logger/index.cjs +0 -37
- package/dist/shared/logger/index.d.cts +0 -2
- package/dist/shared/logger/logger.cjs +0 -232
- package/dist/shared/logger/logger.d.cts +0 -86
- package/dist/shared/logger/sinks.cjs +0 -160
- package/dist/shared/logger/sinks.d.cts +0 -9
- package/dist/shared/paths/paths.cjs +0 -104
- package/dist/shared/paths/paths.d.cts +0 -10
- package/dist/shared/run/api.cjs +0 -28
- package/dist/shared/run/api.d.cts +0 -2
- package/dist/shared/run/browser.cjs +0 -98
- package/dist/shared/run/browser.d.cts +0 -22
- package/dist/shared/state/index.cjs +0 -38
- package/dist/shared/state/index.d.cts +0 -2
- package/dist/shared/state/session-state.cjs +0 -92
- package/dist/shared/state/session-state.d.cts +0 -40
- package/dist/shared/visualization/ghost-cursor.cjs +0 -174
- package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
- package/dist/shared/visualization/highlight.cjs +0 -134
- package/dist/shared/visualization/highlight.d.cts +0 -22
- package/dist/shared/visualization/index.cjs +0 -45
- package/dist/shared/visualization/index.d.cts +0 -3
- package/dist/shared/workflow/workflow.cjs +0 -47
- package/dist/shared/workflow/workflow.d.cts +0 -21
- package/skills/libretto/integration-approach-selection.md +0 -174
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runLibrettoCLI } from "./cli.js";
|
|
3
|
+
import {
|
|
4
|
+
maybeConfigureLLMClientFactoryFromEnv,
|
|
5
|
+
setLLMClientFactory,
|
|
6
|
+
} from "./core/context.js";
|
|
7
|
+
|
|
8
|
+
export { setLLMClientFactory };
|
|
9
|
+
export { runClose } from "./commands/browser.js";
|
|
10
|
+
export { runLibrettoCLI };
|
|
11
|
+
|
|
12
|
+
maybeConfigureLLMClientFactoryFromEnv();
|
|
13
|
+
void runLibrettoCLI();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { aiCommands } from "./commands/ai.js";
|
|
2
|
+
import { browserCommands } from "./commands/browser.js";
|
|
3
|
+
import { executionCommands } from "./commands/execution.js";
|
|
4
|
+
import { initCommand } from "./commands/init.js";
|
|
5
|
+
import { logCommands } from "./commands/logs.js";
|
|
6
|
+
import { snapshotCommand } from "./commands/snapshot.js";
|
|
7
|
+
import { SimpleCLI } from "./framework/simple-cli.js";
|
|
8
|
+
|
|
9
|
+
export const cliRoutes = {
|
|
10
|
+
...browserCommands,
|
|
11
|
+
...executionCommands,
|
|
12
|
+
...logCommands,
|
|
13
|
+
ai: aiCommands,
|
|
14
|
+
init: initCommand,
|
|
15
|
+
snapshot: snapshotCommand,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function createCLIApp() {
|
|
19
|
+
return SimpleCLI.define("libretto", cliRoutes);
|
|
20
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright";
|
|
2
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { cwd } from "node:process";
|
|
5
|
+
import { isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
instrumentContext,
|
|
9
|
+
launchBrowser,
|
|
10
|
+
type LibrettoWorkflowContext,
|
|
11
|
+
} from "../../index.js";
|
|
12
|
+
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
13
|
+
import { parseSessionStateContent } from "../../shared/state/index.js";
|
|
14
|
+
import { getProfilePath, normalizeDomain } from "../core/browser.js";
|
|
15
|
+
import {
|
|
16
|
+
getSessionActionsLogPath,
|
|
17
|
+
getSessionDir,
|
|
18
|
+
getSessionNetworkLogPath,
|
|
19
|
+
getSessionStatePath,
|
|
20
|
+
} from "../core/context.js";
|
|
21
|
+
import {
|
|
22
|
+
getPauseSignalPaths,
|
|
23
|
+
removeSignalIfExists,
|
|
24
|
+
} from "../core/pause-signals.js";
|
|
25
|
+
import { installSessionTelemetry } from "../core/session-telemetry.js";
|
|
26
|
+
import type { RunIntegrationWorkerRequest } from "./run-integration-worker-protocol.js";
|
|
27
|
+
|
|
28
|
+
const LIBRETTO_WORKFLOW_BRAND = Symbol.for("libretto.workflow");
|
|
29
|
+
|
|
30
|
+
type LoadedLibrettoWorkflow = {
|
|
31
|
+
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type RunIntegrationOutcome =
|
|
35
|
+
| { status: "completed" }
|
|
36
|
+
| { status: "failed-held" };
|
|
37
|
+
|
|
38
|
+
const FAILURE_HOLD_POLL_INTERVAL_MS = 250;
|
|
39
|
+
const TSCONFIG_HINT =
|
|
40
|
+
"TypeScript compilation failed. Pass --tsconfig <path> to run against a specific tsconfig.";
|
|
41
|
+
|
|
42
|
+
function isTsxCompileError(error: unknown): error is Error {
|
|
43
|
+
return (
|
|
44
|
+
error instanceof Error &&
|
|
45
|
+
(error.name === "TransformError" ||
|
|
46
|
+
error.message.startsWith("Cannot resolve tsconfig at path:"))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mirrorStdoutToFile(filePath: string): () => void {
|
|
51
|
+
const stdout = process.stdout as NodeJS.WriteStream & {
|
|
52
|
+
write: (...args: any[]) => boolean;
|
|
53
|
+
};
|
|
54
|
+
const originalWrite = stdout.write.bind(stdout);
|
|
55
|
+
|
|
56
|
+
stdout.write = ((chunk: unknown, ...args: unknown[]) => {
|
|
57
|
+
try {
|
|
58
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
59
|
+
? chunk
|
|
60
|
+
: Buffer.from(String(chunk), "utf8");
|
|
61
|
+
appendFileSync(filePath, buffer);
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore log mirroring failures; primary stdout should still flow.
|
|
64
|
+
}
|
|
65
|
+
return originalWrite(chunk, ...args);
|
|
66
|
+
}) as typeof stdout.write;
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
stdout.write = originalWrite as typeof stdout.write;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readSessionStatePid(session: string): number | null {
|
|
74
|
+
const statePath = getSessionStatePath(session);
|
|
75
|
+
if (!existsSync(statePath)) return null;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
return (
|
|
79
|
+
parseSessionStateContent(readFileSync(statePath, "utf8"), statePath)
|
|
80
|
+
.pid ?? null
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function waitForFailureSessionRelease(args: {
|
|
88
|
+
session: string;
|
|
89
|
+
expectedPid: number;
|
|
90
|
+
logger: LoggerApi;
|
|
91
|
+
}): Promise<void> {
|
|
92
|
+
const { session, expectedPid, logger } = args;
|
|
93
|
+
logger.info("run-failure-session-hold", { session, expectedPid });
|
|
94
|
+
|
|
95
|
+
while (true) {
|
|
96
|
+
const currentPid = readSessionStatePid(session);
|
|
97
|
+
if (currentPid !== expectedPid) {
|
|
98
|
+
logger.info("run-failure-session-released", {
|
|
99
|
+
session,
|
|
100
|
+
expectedPid,
|
|
101
|
+
currentPid,
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
await new Promise((resolveWait) =>
|
|
106
|
+
setTimeout(resolveWait, FAILURE_HOLD_POLL_INTERVAL_MS),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isLoadedLibrettoWorkflow(
|
|
112
|
+
value: unknown,
|
|
113
|
+
): value is LoadedLibrettoWorkflow {
|
|
114
|
+
if (!value || typeof value !== "object") return false;
|
|
115
|
+
const candidate = value as Record<PropertyKey, unknown>;
|
|
116
|
+
return (
|
|
117
|
+
candidate[LIBRETTO_WORKFLOW_BRAND] === true &&
|
|
118
|
+
typeof candidate.run === "function"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveLocalAuthProfilePath(domain: string): string {
|
|
123
|
+
return getProfilePath(normalizeDomain(domain));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getMissingLocalAuthProfileError(args: {
|
|
127
|
+
domain: string;
|
|
128
|
+
profilePath: string;
|
|
129
|
+
session: string;
|
|
130
|
+
}): string {
|
|
131
|
+
const normalizedDomain = normalizeDomain(args.domain);
|
|
132
|
+
return [
|
|
133
|
+
`Local auth profile not found for domain "${normalizedDomain}".`,
|
|
134
|
+
`Expected profile file: ${args.profilePath}`,
|
|
135
|
+
"To create it:",
|
|
136
|
+
` 1. libretto open https://${normalizedDomain} --headed --session ${args.session}`,
|
|
137
|
+
" 2. Log in manually in the browser window.",
|
|
138
|
+
` 3. libretto save ${normalizedDomain} --session ${args.session}`,
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getAbsoluteIntegrationPath(integrationPath: string): string {
|
|
143
|
+
const absolutePath = isAbsolute(integrationPath)
|
|
144
|
+
? integrationPath
|
|
145
|
+
: resolve(cwd(), integrationPath);
|
|
146
|
+
if (!existsSync(absolutePath)) {
|
|
147
|
+
throw new Error(`Integration file does not exist: ${absolutePath}`);
|
|
148
|
+
}
|
|
149
|
+
return absolutePath;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function loadWorkflowExport(
|
|
153
|
+
absolutePath: string,
|
|
154
|
+
exportName: string,
|
|
155
|
+
): Promise<LoadedLibrettoWorkflow> {
|
|
156
|
+
let loadedModule: Record<string, unknown>;
|
|
157
|
+
try {
|
|
158
|
+
loadedModule = (await import(pathToFileURL(absolutePath).href)) as Record<
|
|
159
|
+
string,
|
|
160
|
+
unknown
|
|
161
|
+
>;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
+
const compileHint = isTsxCompileError(error) ? `\n${TSCONFIG_HINT}` : "";
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Failed to import integration module at ${absolutePath}: ${message}${compileHint}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const targetExport = loadedModule[exportName];
|
|
171
|
+
if (!targetExport) {
|
|
172
|
+
const availableExports = Object.keys(loadedModule);
|
|
173
|
+
const detail =
|
|
174
|
+
availableExports.length > 0
|
|
175
|
+
? ` Available exports: ${availableExports.join(", ")}`
|
|
176
|
+
: " The module has no exports.";
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Export "${exportName}" was not found in ${absolutePath}.${detail}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!isLoadedLibrettoWorkflow(targetExport)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
[
|
|
185
|
+
`Export "${exportName}" in ${absolutePath} is not a valid Libretto workflow.`,
|
|
186
|
+
"",
|
|
187
|
+
'A workflow must be created using the workflow() function from "libretto":',
|
|
188
|
+
"",
|
|
189
|
+
' import { workflow } from "libretto";',
|
|
190
|
+
"",
|
|
191
|
+
` export const ${exportName} = workflow<InputType, OutputType>(`,
|
|
192
|
+
" async (ctx, input) => {",
|
|
193
|
+
" // ctx.session — libretto session name",
|
|
194
|
+
" // ctx.page — Playwright Page instance",
|
|
195
|
+
" // ctx.logger — MinimalLogger",
|
|
196
|
+
" // ctx.services — injected dependencies (generic, default {})",
|
|
197
|
+
" // input — JSON-serializable input matching InputType",
|
|
198
|
+
" return output; // must match OutputType",
|
|
199
|
+
" },",
|
|
200
|
+
" );",
|
|
201
|
+
].join("\n"),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return targetExport;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function installHeadedWorkflowVisualization(args: {
|
|
209
|
+
context: BrowserContext;
|
|
210
|
+
logger: LoggerApi;
|
|
211
|
+
instrument?: typeof instrumentContext;
|
|
212
|
+
}): Promise<void> {
|
|
213
|
+
await (args.instrument ?? instrumentContext)(args.context, {
|
|
214
|
+
visualize: true,
|
|
215
|
+
logger: args.logger,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function runIntegrationInternal(
|
|
220
|
+
args: RunIntegrationWorkerRequest,
|
|
221
|
+
options: {
|
|
222
|
+
logger: LoggerApi;
|
|
223
|
+
},
|
|
224
|
+
): Promise<RunIntegrationOutcome> {
|
|
225
|
+
const { logger } = options;
|
|
226
|
+
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
227
|
+
const workflow = await loadWorkflowExport(absolutePath, args.exportName);
|
|
228
|
+
const signalPaths = getPauseSignalPaths(args.session);
|
|
229
|
+
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
230
|
+
await removeSignalIfExists(signalPaths.resumeSignalPath);
|
|
231
|
+
await removeSignalIfExists(signalPaths.completedSignalPath);
|
|
232
|
+
await removeSignalIfExists(signalPaths.failedSignalPath);
|
|
233
|
+
const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
|
|
234
|
+
|
|
235
|
+
console.log(
|
|
236
|
+
`Running integration "${args.exportName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const integrationLogger = logger.withScope("integration-run", {
|
|
240
|
+
integrationPath: absolutePath,
|
|
241
|
+
integrationExport: args.exportName,
|
|
242
|
+
session: args.session,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Resolve auth profile from CLI flag (--auth-profile <domain>)
|
|
246
|
+
const authProfileDomain = args.authProfileDomain;
|
|
247
|
+
const storageStatePath = authProfileDomain
|
|
248
|
+
? resolveLocalAuthProfilePath(authProfileDomain)
|
|
249
|
+
: undefined;
|
|
250
|
+
if (authProfileDomain && storageStatePath && !existsSync(storageStatePath)) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
getMissingLocalAuthProfileError({
|
|
253
|
+
domain: authProfileDomain,
|
|
254
|
+
profilePath: storageStatePath,
|
|
255
|
+
session: args.session,
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const browserSession = await launchBrowser({
|
|
260
|
+
sessionName: args.session,
|
|
261
|
+
headless: args.headless,
|
|
262
|
+
storageStatePath,
|
|
263
|
+
viewport: args.viewport,
|
|
264
|
+
});
|
|
265
|
+
if (!args.headless && args.visualize !== false) {
|
|
266
|
+
await installHeadedWorkflowVisualization({
|
|
267
|
+
context: browserSession.context,
|
|
268
|
+
logger: integrationLogger,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const actionsLogPath = getSessionActionsLogPath(args.session);
|
|
272
|
+
const networkLogPath = getSessionNetworkLogPath(args.session);
|
|
273
|
+
await installSessionTelemetry({
|
|
274
|
+
context: browserSession.context,
|
|
275
|
+
initialPage: browserSession.page,
|
|
276
|
+
includeUserDomActions: true,
|
|
277
|
+
logAction: (entry) => {
|
|
278
|
+
appendFileSync(actionsLogPath, JSON.stringify(entry) + "\n");
|
|
279
|
+
},
|
|
280
|
+
logNetwork: (entry) => {
|
|
281
|
+
appendFileSync(networkLogPath, JSON.stringify(entry) + "\n");
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const workflowContext: LibrettoWorkflowContext = {
|
|
286
|
+
session: args.session,
|
|
287
|
+
logger: integrationLogger,
|
|
288
|
+
page: browserSession.page,
|
|
289
|
+
services: {},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
try {
|
|
294
|
+
await workflow.run(workflowContext, args.params ?? {});
|
|
295
|
+
} catch (error) {
|
|
296
|
+
const errorMessage =
|
|
297
|
+
error instanceof Error ? error.message : String(error);
|
|
298
|
+
await writeFile(
|
|
299
|
+
signalPaths.failedSignalPath,
|
|
300
|
+
JSON.stringify(
|
|
301
|
+
{
|
|
302
|
+
failedAt: new Date().toISOString(),
|
|
303
|
+
message: errorMessage,
|
|
304
|
+
phase: "workflow",
|
|
305
|
+
},
|
|
306
|
+
null,
|
|
307
|
+
2,
|
|
308
|
+
),
|
|
309
|
+
"utf8",
|
|
310
|
+
);
|
|
311
|
+
await waitForFailureSessionRelease({
|
|
312
|
+
session: args.session,
|
|
313
|
+
expectedPid: process.pid,
|
|
314
|
+
logger,
|
|
315
|
+
});
|
|
316
|
+
return { status: "failed-held" };
|
|
317
|
+
}
|
|
318
|
+
await writeFile(
|
|
319
|
+
signalPaths.completedSignalPath,
|
|
320
|
+
JSON.stringify({ completedAt: new Date().toISOString() }, null, 2),
|
|
321
|
+
"utf8",
|
|
322
|
+
);
|
|
323
|
+
console.log("Integration completed.");
|
|
324
|
+
return { status: "completed" };
|
|
325
|
+
} finally {
|
|
326
|
+
restoreStdout();
|
|
327
|
+
await browserSession.close();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function runIntegrationFromFileInWorker(
|
|
332
|
+
args: RunIntegrationWorkerRequest,
|
|
333
|
+
logger: LoggerApi,
|
|
334
|
+
): Promise<RunIntegrationOutcome> {
|
|
335
|
+
return await runIntegrationInternal(args, {
|
|
336
|
+
logger,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const RunIntegrationWorkerRequestSchema = z.object({
|
|
4
|
+
integrationPath: z.string().min(1),
|
|
5
|
+
exportName: z.string().min(1),
|
|
6
|
+
session: z.string().min(1),
|
|
7
|
+
params: z.unknown(),
|
|
8
|
+
headless: z.boolean(),
|
|
9
|
+
visualize: z.boolean().default(true),
|
|
10
|
+
authProfileDomain: z.string().optional(),
|
|
11
|
+
viewport: z.object({ width: z.number(), height: z.number() }).optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type RunIntegrationWorkerRequest = z.infer<
|
|
15
|
+
typeof RunIntegrationWorkerRequestSchema
|
|
16
|
+
>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
RunIntegrationWorkerRequestSchema,
|
|
5
|
+
type RunIntegrationWorkerRequest,
|
|
6
|
+
} from "./run-integration-worker-protocol.js";
|
|
7
|
+
import { runIntegrationFromFileInWorker } from "./run-integration-runtime.js";
|
|
8
|
+
import { ensureLibrettoSetup, withSessionLogger } from "../core/context.js";
|
|
9
|
+
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
10
|
+
|
|
11
|
+
function parseWorkerRequest(argv: string[]): RunIntegrationWorkerRequest {
|
|
12
|
+
const rawPayload = argv[2];
|
|
13
|
+
if (!rawPayload) {
|
|
14
|
+
throw new Error("Missing worker payload argument.");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let parsed: unknown;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(rawPayload);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Invalid worker payload JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
return RunIntegrationWorkerRequestSchema.parse(parsed);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof ZodError) {
|
|
30
|
+
const details = error.issues
|
|
31
|
+
.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`)
|
|
32
|
+
.join("; ");
|
|
33
|
+
throw new Error(`Worker payload is invalid: ${details}`);
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main(): Promise<void> {
|
|
40
|
+
let request: RunIntegrationWorkerRequest | null = null;
|
|
41
|
+
let exitCode = 0;
|
|
42
|
+
try {
|
|
43
|
+
request = parseWorkerRequest(process.argv);
|
|
44
|
+
const workerRequest = request;
|
|
45
|
+
ensureLibrettoSetup();
|
|
46
|
+
await withSessionLogger(workerRequest.session, async (logger) => {
|
|
47
|
+
await runIntegrationFromFileInWorker(workerRequest, logger);
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
if (request) {
|
|
52
|
+
const { failedSignalPath } = getPauseSignalPaths(request.session);
|
|
53
|
+
await writeFile(
|
|
54
|
+
failedSignalPath,
|
|
55
|
+
JSON.stringify(
|
|
56
|
+
{
|
|
57
|
+
failedAt: new Date().toISOString(),
|
|
58
|
+
message,
|
|
59
|
+
phase: "setup",
|
|
60
|
+
},
|
|
61
|
+
null,
|
|
62
|
+
2,
|
|
63
|
+
),
|
|
64
|
+
"utf8",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
process.exit(exitCode);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
void main();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
// Logger
|
|
5
|
+
export {
|
|
6
|
+
Logger,
|
|
7
|
+
defaultLogger,
|
|
8
|
+
type LoggerApi,
|
|
9
|
+
type MinimalLogger,
|
|
10
|
+
type LoggerSink,
|
|
11
|
+
type LogOptions,
|
|
12
|
+
} from "./shared/logger/logger.js";
|
|
13
|
+
export {
|
|
14
|
+
createFileLogSink,
|
|
15
|
+
prettyConsoleSink,
|
|
16
|
+
jsonlConsoleSink,
|
|
17
|
+
} from "./shared/logger/sinks.js";
|
|
18
|
+
|
|
19
|
+
// LLM client interface
|
|
20
|
+
export type {
|
|
21
|
+
LLMClient,
|
|
22
|
+
Message,
|
|
23
|
+
MessageContentPart,
|
|
24
|
+
} from "./shared/llm/types.js";
|
|
25
|
+
export { createLLMClientFromModel } from "./shared/llm/ai-sdk-adapter.js";
|
|
26
|
+
export {
|
|
27
|
+
SESSION_STATE_VERSION,
|
|
28
|
+
SessionStatusSchema,
|
|
29
|
+
SessionStateFileSchema,
|
|
30
|
+
parseSessionStateData,
|
|
31
|
+
parseSessionStateContent,
|
|
32
|
+
serializeSessionState,
|
|
33
|
+
type SessionStatus,
|
|
34
|
+
type SessionState,
|
|
35
|
+
type SessionStateFile,
|
|
36
|
+
} from "./shared/state/index.js";
|
|
37
|
+
|
|
38
|
+
// Recovery
|
|
39
|
+
export { executeRecoveryAgent } from "./runtime/recovery/agent.js";
|
|
40
|
+
export { attemptWithRecovery } from "./runtime/recovery/recovery.js";
|
|
41
|
+
export {
|
|
42
|
+
detectSubmissionError,
|
|
43
|
+
type KnownSubmissionError,
|
|
44
|
+
type DetectedSubmissionError,
|
|
45
|
+
} from "./runtime/recovery/errors.js";
|
|
46
|
+
|
|
47
|
+
// AI extraction
|
|
48
|
+
export {
|
|
49
|
+
extractFromPage,
|
|
50
|
+
type ExtractOptions,
|
|
51
|
+
} from "./runtime/extract/extract.js";
|
|
52
|
+
|
|
53
|
+
// Network helpers
|
|
54
|
+
export {
|
|
55
|
+
pageRequest,
|
|
56
|
+
type RequestConfig,
|
|
57
|
+
type PageRequestOptions,
|
|
58
|
+
} from "./runtime/network/network.js";
|
|
59
|
+
|
|
60
|
+
// Download helpers
|
|
61
|
+
export {
|
|
62
|
+
downloadViaClick,
|
|
63
|
+
downloadAndSave,
|
|
64
|
+
type DownloadResult,
|
|
65
|
+
type DownloadViaClickOptions,
|
|
66
|
+
type SaveDownloadOptions,
|
|
67
|
+
} from "./runtime/download/download.js";
|
|
68
|
+
|
|
69
|
+
// Debug / Pause
|
|
70
|
+
export { pause } from "./shared/debug/pause.js";
|
|
71
|
+
|
|
72
|
+
// Instrumentation
|
|
73
|
+
export {
|
|
74
|
+
instrumentPage,
|
|
75
|
+
installInstrumentation,
|
|
76
|
+
instrumentContext,
|
|
77
|
+
type InstrumentationOptions,
|
|
78
|
+
type InstrumentedPage,
|
|
79
|
+
} from "./shared/instrumentation/instrument.js";
|
|
80
|
+
|
|
81
|
+
// Visualization
|
|
82
|
+
export {
|
|
83
|
+
ensureGhostCursor,
|
|
84
|
+
moveGhostCursor,
|
|
85
|
+
ghostClick,
|
|
86
|
+
hideGhostCursor,
|
|
87
|
+
type GhostCursorOptions,
|
|
88
|
+
} from "./shared/visualization/ghost-cursor.js";
|
|
89
|
+
export {
|
|
90
|
+
ensureHighlightLayer,
|
|
91
|
+
showHighlight,
|
|
92
|
+
clearHighlights,
|
|
93
|
+
type HighlightOptions,
|
|
94
|
+
} from "./shared/visualization/highlight.js";
|
|
95
|
+
|
|
96
|
+
// Run helpers
|
|
97
|
+
export {
|
|
98
|
+
launchBrowser,
|
|
99
|
+
type LaunchBrowserArgs,
|
|
100
|
+
type BrowserSession,
|
|
101
|
+
} from "./shared/run/api.js";
|
|
102
|
+
|
|
103
|
+
// Workflow helpers
|
|
104
|
+
export {
|
|
105
|
+
LibrettoWorkflow,
|
|
106
|
+
LIBRETTO_WORKFLOW_BRAND,
|
|
107
|
+
workflow,
|
|
108
|
+
type LibrettoWorkflowContext,
|
|
109
|
+
type LibrettoWorkflowHandler,
|
|
110
|
+
} from "./shared/workflow/workflow.js";
|
|
111
|
+
|
|
112
|
+
const isDirectExecution = (): boolean => {
|
|
113
|
+
const entryArg = process.argv[1];
|
|
114
|
+
if (!entryArg) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return pathToFileURL(resolve(entryArg)).href === import.meta.url;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (isDirectExecution()) {
|
|
121
|
+
void import("./cli/index.js").catch((error: unknown) => {
|
|
122
|
+
const message =
|
|
123
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
124
|
+
process.stderr.write(`${message}\n`);
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { Page, Download } from "playwright";
|
|
4
|
+
import type { MinimalLogger } from "../../shared/logger/logger.js";
|
|
5
|
+
|
|
6
|
+
export type DownloadResult = {
|
|
7
|
+
/** The raw file contents. */
|
|
8
|
+
buffer: Buffer;
|
|
9
|
+
/** The filename suggested by the server (Content-Disposition header or URL). */
|
|
10
|
+
filename: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type DownloadViaClickOptions = {
|
|
14
|
+
logger?: MinimalLogger;
|
|
15
|
+
/** Timeout in milliseconds for waiting on the download event. Defaults to 30 000. */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Triggers a file download by clicking a DOM element and intercepts the
|
|
21
|
+
* resulting download using Playwright's download event.
|
|
22
|
+
*
|
|
23
|
+
* The download promise is registered **before** the click so the event is
|
|
24
|
+
* never missed.
|
|
25
|
+
*/
|
|
26
|
+
export async function downloadViaClick(
|
|
27
|
+
page: Page,
|
|
28
|
+
selector: string,
|
|
29
|
+
options?: DownloadViaClickOptions,
|
|
30
|
+
): Promise<DownloadResult> {
|
|
31
|
+
const { logger, timeout = 30_000 } = options ?? {};
|
|
32
|
+
|
|
33
|
+
const startTime = Date.now();
|
|
34
|
+
|
|
35
|
+
// 1. Register the download listener BEFORE clicking
|
|
36
|
+
const downloadPromise = page.waitForEvent("download", { timeout });
|
|
37
|
+
|
|
38
|
+
// 2. Click the element that triggers the download
|
|
39
|
+
await page.locator(selector).click();
|
|
40
|
+
|
|
41
|
+
// 3. Await the download event
|
|
42
|
+
const download: Download = await downloadPromise;
|
|
43
|
+
|
|
44
|
+
// 4. Get the suggested filename
|
|
45
|
+
const filename = download.suggestedFilename();
|
|
46
|
+
|
|
47
|
+
// 5. Read the downloaded file into a buffer
|
|
48
|
+
const readStream = await download.createReadStream();
|
|
49
|
+
if (!readStream) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Download stream unavailable for "${filename}". The browser may have been closed before the download completed.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const chunks: Buffer[] = [];
|
|
56
|
+
for await (const chunk of readStream) {
|
|
57
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
58
|
+
}
|
|
59
|
+
const buffer = Buffer.concat(chunks);
|
|
60
|
+
|
|
61
|
+
const duration = Date.now() - startTime;
|
|
62
|
+
|
|
63
|
+
logger?.info("download:click", {
|
|
64
|
+
selector,
|
|
65
|
+
filename,
|
|
66
|
+
size: buffer.length,
|
|
67
|
+
duration,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return { buffer, filename };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type SaveDownloadOptions = DownloadViaClickOptions & {
|
|
74
|
+
/** Absolute or relative path to save the file to. When omitted the suggested filename is used in the current working directory. */
|
|
75
|
+
savePath?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convenience wrapper around {@link downloadViaClick} that also writes the
|
|
80
|
+
* downloaded file to disk.
|
|
81
|
+
*/
|
|
82
|
+
export async function downloadAndSave(
|
|
83
|
+
page: Page,
|
|
84
|
+
selector: string,
|
|
85
|
+
options?: SaveDownloadOptions,
|
|
86
|
+
): Promise<DownloadResult & { savedTo: string }> {
|
|
87
|
+
const { savePath, ...downloadOpts } = options ?? {};
|
|
88
|
+
const { buffer, filename } = await downloadViaClick(
|
|
89
|
+
page,
|
|
90
|
+
selector,
|
|
91
|
+
downloadOpts,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const dest = resolve(savePath ?? filename);
|
|
95
|
+
await writeFile(dest, buffer);
|
|
96
|
+
|
|
97
|
+
options?.logger?.info("download:saved", {
|
|
98
|
+
filename,
|
|
99
|
+
savedTo: dest,
|
|
100
|
+
size: buffer.length,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return { buffer, filename, savedTo: dest };
|
|
104
|
+
}
|