libretto 0.1.5 → 0.2.0
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 +213 -17
- package/bin/libretto.mjs +18 -0
- package/dist/cli/cli.js +201 -0
- package/dist/cli/commands/ai.js +21 -0
- package/dist/cli/commands/browser.js +56 -0
- package/dist/cli/commands/execution.js +407 -0
- package/dist/cli/commands/logs.js +65 -0
- package/dist/cli/commands/snapshot.js +99 -0
- package/dist/cli/core/ai-config.js +149 -0
- package/dist/cli/core/browser.js +687 -0
- package/dist/cli/core/context.js +113 -0
- package/dist/cli/core/pause-signals.js +29 -0
- package/dist/cli/core/session.js +183 -0
- package/dist/cli/core/snapshot-analyzer.js +492 -0
- package/dist/cli/core/telemetry.js +350 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/workers/run-integration-runtime.js +204 -0
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -0
- package/dist/cli/workers/run-integration-worker.js +83 -0
- package/dist/index.cjs +127 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +110 -0
- package/dist/runtime/download/download.cjs +70 -0
- package/dist/runtime/download/download.d.cts +35 -0
- package/dist/runtime/download/download.d.ts +35 -0
- package/dist/runtime/download/download.js +45 -0
- package/dist/runtime/download/index.cjs +30 -0
- package/dist/runtime/download/index.d.cts +3 -0
- package/dist/runtime/download/index.d.ts +3 -0
- package/dist/runtime/download/index.js +8 -0
- package/dist/runtime/extract/extract.cjs +87 -0
- package/dist/runtime/extract/extract.d.cts +23 -0
- package/dist/runtime/extract/extract.d.ts +23 -0
- package/dist/runtime/extract/extract.js +63 -0
- package/dist/runtime/extract/index.cjs +28 -0
- package/dist/runtime/extract/index.d.cts +5 -0
- package/dist/runtime/extract/index.d.ts +5 -0
- package/dist/runtime/extract/index.js +4 -0
- package/dist/runtime/network/index.cjs +28 -0
- package/dist/runtime/network/index.d.cts +4 -0
- package/dist/runtime/network/index.d.ts +4 -0
- package/dist/runtime/network/index.js +6 -0
- package/dist/runtime/network/network.cjs +91 -0
- package/dist/runtime/network/network.d.cts +28 -0
- package/dist/runtime/network/network.d.ts +28 -0
- package/dist/runtime/network/network.js +67 -0
- package/dist/runtime/recovery/agent.cjs +218 -0
- package/dist/runtime/recovery/agent.d.cts +13 -0
- package/dist/runtime/recovery/agent.d.ts +13 -0
- package/dist/runtime/recovery/agent.js +194 -0
- package/dist/runtime/recovery/errors.cjs +122 -0
- package/dist/runtime/recovery/errors.d.cts +31 -0
- package/dist/runtime/recovery/errors.d.ts +31 -0
- package/dist/runtime/recovery/errors.js +98 -0
- package/dist/runtime/recovery/index.cjs +34 -0
- package/dist/runtime/recovery/index.d.cts +7 -0
- package/dist/runtime/recovery/index.d.ts +7 -0
- package/dist/runtime/recovery/index.js +10 -0
- package/dist/runtime/recovery/recovery.cjs +53 -0
- package/dist/runtime/recovery/recovery.d.cts +12 -0
- package/dist/runtime/recovery/recovery.d.ts +12 -0
- package/dist/runtime/recovery/recovery.js +29 -0
- package/dist/runtime/step/index.cjs +31 -0
- package/dist/runtime/step/index.d.cts +7 -0
- package/dist/runtime/step/index.d.ts +7 -0
- package/dist/runtime/step/index.js +6 -0
- package/dist/runtime/step/runner.cjs +208 -0
- package/dist/runtime/step/runner.d.cts +16 -0
- package/dist/runtime/step/runner.d.ts +16 -0
- package/dist/runtime/step/runner.js +187 -0
- package/dist/runtime/step/step.cjs +67 -0
- package/dist/runtime/step/step.d.cts +23 -0
- package/dist/runtime/step/step.d.ts +23 -0
- package/dist/runtime/step/step.js +43 -0
- package/dist/runtime/step/types.cjs +16 -0
- package/dist/runtime/step/types.d.cts +72 -0
- package/dist/runtime/step/types.d.ts +72 -0
- package/dist/runtime/step/types.js +0 -0
- package/dist/shared/config/config.cjs +44 -0
- package/dist/shared/config/config.d.cts +10 -0
- package/dist/shared/config/config.d.ts +10 -0
- package/dist/shared/config/config.js +18 -0
- package/dist/shared/config/index.cjs +32 -0
- package/dist/shared/config/index.d.cts +1 -0
- package/dist/shared/config/index.d.ts +1 -0
- package/dist/shared/config/index.js +10 -0
- package/dist/shared/debug/index.cjs +32 -0
- package/dist/shared/debug/index.d.cts +2 -0
- package/dist/shared/debug/index.d.ts +2 -0
- package/dist/shared/debug/index.js +10 -0
- package/dist/shared/debug/pause.cjs +56 -0
- package/dist/shared/debug/pause.d.cts +23 -0
- package/dist/shared/debug/pause.d.ts +23 -0
- package/dist/shared/debug/pause.js +30 -0
- package/dist/shared/instrumentation/errors.cjs +81 -0
- package/dist/shared/instrumentation/errors.d.cts +12 -0
- package/dist/shared/instrumentation/errors.d.ts +12 -0
- package/dist/shared/instrumentation/errors.js +57 -0
- package/dist/shared/instrumentation/index.cjs +35 -0
- package/dist/shared/instrumentation/index.d.cts +6 -0
- package/dist/shared/instrumentation/index.d.ts +6 -0
- package/dist/shared/instrumentation/index.js +12 -0
- package/dist/shared/instrumentation/instrument.cjs +206 -0
- package/dist/shared/instrumentation/instrument.d.cts +32 -0
- package/dist/shared/instrumentation/instrument.d.ts +32 -0
- package/dist/shared/instrumentation/instrument.js +190 -0
- package/dist/shared/llm/client.cjs +139 -0
- package/dist/shared/llm/client.d.cts +6 -0
- package/dist/shared/llm/client.d.ts +6 -0
- package/dist/shared/llm/client.js +115 -0
- package/dist/shared/llm/index.cjs +28 -0
- package/dist/shared/llm/index.d.cts +3 -0
- package/dist/shared/llm/index.d.ts +3 -0
- package/dist/shared/llm/index.js +4 -0
- package/dist/shared/llm/types.cjs +16 -0
- package/dist/shared/llm/types.d.cts +34 -0
- package/dist/shared/llm/types.d.ts +34 -0
- package/dist/shared/llm/types.js +0 -0
- package/dist/shared/logger/index.cjs +35 -0
- package/dist/shared/logger/index.d.cts +2 -0
- package/dist/shared/logger/index.d.ts +2 -0
- package/dist/shared/logger/index.js +12 -0
- package/dist/shared/logger/logger.cjs +200 -0
- package/dist/shared/logger/logger.d.cts +70 -0
- package/dist/shared/logger/logger.d.ts +70 -0
- package/dist/shared/logger/logger.js +176 -0
- package/dist/shared/logger/sinks.cjs +160 -0
- package/dist/shared/logger/sinks.d.cts +9 -0
- package/dist/shared/logger/sinks.d.ts +9 -0
- package/dist/shared/logger/sinks.js +124 -0
- package/dist/shared/paths/paths.cjs +104 -0
- package/dist/shared/paths/paths.d.cts +10 -0
- package/dist/shared/paths/paths.d.ts +10 -0
- package/dist/shared/paths/paths.js +73 -0
- package/dist/shared/run/api.cjs +35 -0
- package/dist/shared/run/api.d.cts +3 -0
- package/dist/shared/run/api.d.ts +3 -0
- package/dist/shared/run/api.js +12 -0
- package/dist/shared/run/browser.cjs +98 -0
- package/dist/shared/run/browser.d.cts +22 -0
- package/dist/shared/run/browser.d.ts +22 -0
- package/dist/shared/run/browser.js +74 -0
- package/dist/shared/state/index.cjs +38 -0
- package/dist/shared/state/index.d.cts +2 -0
- package/dist/shared/state/index.d.ts +2 -0
- package/dist/shared/state/index.js +16 -0
- package/dist/shared/state/session-state.cjs +85 -0
- package/dist/shared/state/session-state.d.cts +34 -0
- package/dist/shared/state/session-state.d.ts +34 -0
- package/dist/shared/state/session-state.js +56 -0
- package/dist/shared/visualization/ghost-cursor.cjs +174 -0
- package/dist/shared/visualization/ghost-cursor.d.cts +37 -0
- package/dist/shared/visualization/ghost-cursor.d.ts +37 -0
- package/dist/shared/visualization/ghost-cursor.js +145 -0
- package/dist/shared/visualization/highlight.cjs +134 -0
- package/dist/shared/visualization/highlight.d.cts +22 -0
- package/dist/shared/visualization/highlight.d.ts +22 -0
- package/dist/shared/visualization/highlight.js +108 -0
- package/dist/shared/visualization/index.cjs +45 -0
- package/dist/shared/visualization/index.d.cts +3 -0
- package/dist/shared/visualization/index.d.ts +3 -0
- package/dist/shared/visualization/index.js +24 -0
- package/dist/shared/workflow/workflow.cjs +47 -0
- package/dist/shared/workflow/workflow.d.cts +33 -0
- package/dist/shared/workflow/workflow.d.ts +33 -0
- package/dist/shared/workflow/workflow.js +21 -0
- package/package.json +123 -26
- package/.npmignore +0 -2
- package/bin/libretto +0 -31
- package/lib/connect.js +0 -34
- package/lib/export.js +0 -224
- package/lib/import.js +0 -166
- package/lib/index.js +0 -8
- package/lib/log.js +0 -9
- package/lib/validate.js +0 -20
- package/makefile +0 -8
- package/src/connect.coffee +0 -25
- package/src/export.coffee +0 -222
- package/src/import.coffee +0 -166
- package/src/index.coffee +0 -3
- package/src/log.coffee +0 -3
- package/src/validate.coffee +0 -10
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
getSessionActionsLogPath,
|
|
4
|
+
getSessionNetworkLogPath
|
|
5
|
+
} from "./context.js";
|
|
6
|
+
import { assertSessionStateExistsOrThrow } from "./session.js";
|
|
7
|
+
function readNetworkLog(session, opts = {}) {
|
|
8
|
+
assertSessionStateExistsOrThrow(session);
|
|
9
|
+
const logPath = getSessionNetworkLogPath(session);
|
|
10
|
+
if (!existsSync(logPath)) return [];
|
|
11
|
+
const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
12
|
+
let entries = lines.map(
|
|
13
|
+
(line) => JSON.parse(line)
|
|
14
|
+
);
|
|
15
|
+
if (opts.method) {
|
|
16
|
+
const m = opts.method.toUpperCase();
|
|
17
|
+
entries = entries.filter((e) => e.method === m);
|
|
18
|
+
}
|
|
19
|
+
if (opts.filter) {
|
|
20
|
+
const re = new RegExp(opts.filter, "i");
|
|
21
|
+
entries = entries.filter((e) => re.test(e.url));
|
|
22
|
+
}
|
|
23
|
+
const last = opts.last ?? 20;
|
|
24
|
+
if (entries.length > last) {
|
|
25
|
+
entries = entries.slice(-last);
|
|
26
|
+
}
|
|
27
|
+
return entries;
|
|
28
|
+
}
|
|
29
|
+
function formatNetworkEntry(e) {
|
|
30
|
+
const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
|
|
31
|
+
const duration = e.durationMs != null ? `${e.durationMs}ms` : "?ms";
|
|
32
|
+
const size = e.size != null ? `${e.size}B` : "";
|
|
33
|
+
const parts = [
|
|
34
|
+
`[${time}]`,
|
|
35
|
+
`${e.status}`,
|
|
36
|
+
`${e.method.padEnd(6)}`,
|
|
37
|
+
e.url,
|
|
38
|
+
duration,
|
|
39
|
+
size
|
|
40
|
+
].filter(Boolean);
|
|
41
|
+
let line = parts.join(" ");
|
|
42
|
+
if (e.postData) {
|
|
43
|
+
line += `
|
|
44
|
+
body: ${e.postData.substring(0, 120)}${e.postData.length > 120 ? "..." : ""}`;
|
|
45
|
+
}
|
|
46
|
+
return line;
|
|
47
|
+
}
|
|
48
|
+
function clearNetworkLog(session) {
|
|
49
|
+
assertSessionStateExistsOrThrow(session);
|
|
50
|
+
const logPath = getSessionNetworkLogPath(session);
|
|
51
|
+
writeFileSync(logPath, "");
|
|
52
|
+
}
|
|
53
|
+
function parentLogAction(session, entry) {
|
|
54
|
+
try {
|
|
55
|
+
const record = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
|
|
56
|
+
appendFileSync(getSessionActionsLogPath(session), JSON.stringify(record) + "\n");
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function readActionLog(session, opts = {}) {
|
|
61
|
+
assertSessionStateExistsOrThrow(session);
|
|
62
|
+
const logPath = getSessionActionsLogPath(session);
|
|
63
|
+
if (!existsSync(logPath)) return [];
|
|
64
|
+
const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
65
|
+
let entries = lines.map(
|
|
66
|
+
(line) => JSON.parse(line)
|
|
67
|
+
);
|
|
68
|
+
if (opts.action) {
|
|
69
|
+
const a = opts.action.toLowerCase();
|
|
70
|
+
entries = entries.filter((e) => e.action === a);
|
|
71
|
+
}
|
|
72
|
+
if (opts.source) {
|
|
73
|
+
const s = opts.source.toLowerCase();
|
|
74
|
+
entries = entries.filter((e) => e.source === s);
|
|
75
|
+
}
|
|
76
|
+
if (opts.filter) {
|
|
77
|
+
const re = new RegExp(opts.filter, "i");
|
|
78
|
+
entries = entries.filter(
|
|
79
|
+
(e) => re.test(e.action) || re.test(e.selector || "") || re.test(e.value || "") || re.test(e.url || "")
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const last = opts.last ?? 20;
|
|
83
|
+
if (entries.length > last) {
|
|
84
|
+
entries = entries.slice(-last);
|
|
85
|
+
}
|
|
86
|
+
return entries;
|
|
87
|
+
}
|
|
88
|
+
function formatActionEntry(e) {
|
|
89
|
+
const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
|
|
90
|
+
const src = e.source.toUpperCase().padEnd(5);
|
|
91
|
+
const parts = [`[${time}]`, `[${src}]`, e.action];
|
|
92
|
+
if (e.selector) parts.push(e.selector);
|
|
93
|
+
if (e.value) parts.push(`"${e.value}"`);
|
|
94
|
+
if (e.url) parts.push(e.url);
|
|
95
|
+
if (e.duration != null) parts.push(`${e.duration}ms`);
|
|
96
|
+
if (!e.success) parts.push(`ERROR: ${e.error || "unknown"}`);
|
|
97
|
+
return parts.join(" ");
|
|
98
|
+
}
|
|
99
|
+
function clearActionLog(session) {
|
|
100
|
+
assertSessionStateExistsOrThrow(session);
|
|
101
|
+
const logPath = getSessionActionsLogPath(session);
|
|
102
|
+
writeFileSync(logPath, "");
|
|
103
|
+
}
|
|
104
|
+
const LOCATOR_ACTION_METHODS = [
|
|
105
|
+
"click",
|
|
106
|
+
"dblclick",
|
|
107
|
+
"fill",
|
|
108
|
+
"type",
|
|
109
|
+
"press",
|
|
110
|
+
"check",
|
|
111
|
+
"uncheck",
|
|
112
|
+
"selectOption",
|
|
113
|
+
"hover",
|
|
114
|
+
"focus",
|
|
115
|
+
"scrollIntoViewIfNeeded",
|
|
116
|
+
"waitFor",
|
|
117
|
+
"innerHTML",
|
|
118
|
+
"innerText",
|
|
119
|
+
"textContent",
|
|
120
|
+
"inputValue",
|
|
121
|
+
"isChecked",
|
|
122
|
+
"isDisabled",
|
|
123
|
+
"isEditable",
|
|
124
|
+
"isEnabled",
|
|
125
|
+
"isHidden",
|
|
126
|
+
"isVisible",
|
|
127
|
+
"count",
|
|
128
|
+
"boundingBox",
|
|
129
|
+
"screenshot",
|
|
130
|
+
"evaluate",
|
|
131
|
+
"evaluateAll",
|
|
132
|
+
"evaluateHandle",
|
|
133
|
+
"getAttribute",
|
|
134
|
+
"dispatchEvent",
|
|
135
|
+
"setInputFiles",
|
|
136
|
+
"selectText",
|
|
137
|
+
"dragTo",
|
|
138
|
+
"highlight",
|
|
139
|
+
"tap"
|
|
140
|
+
];
|
|
141
|
+
const LOCATOR_RETURNING_METHODS = [
|
|
142
|
+
"first",
|
|
143
|
+
"last",
|
|
144
|
+
"locator",
|
|
145
|
+
"getByRole",
|
|
146
|
+
"getByText",
|
|
147
|
+
"getByLabel",
|
|
148
|
+
"getByPlaceholder",
|
|
149
|
+
"getByAltText",
|
|
150
|
+
"getByTitle",
|
|
151
|
+
"getByTestId",
|
|
152
|
+
"filter",
|
|
153
|
+
"and",
|
|
154
|
+
"or"
|
|
155
|
+
];
|
|
156
|
+
function formatHint(method, args) {
|
|
157
|
+
const formatted = args.map((a) => JSON.stringify(a)).join(", ");
|
|
158
|
+
return `${method}(${formatted})`;
|
|
159
|
+
}
|
|
160
|
+
function wrapLocator(locator, hint, session, page, onActivity) {
|
|
161
|
+
if (locator.__librettoActionLogged) return locator;
|
|
162
|
+
locator.__librettoActionLogged = true;
|
|
163
|
+
for (const actMethod of LOCATOR_ACTION_METHODS) {
|
|
164
|
+
if (typeof locator[actMethod] !== "function") continue;
|
|
165
|
+
const origAct = locator[actMethod].bind(locator);
|
|
166
|
+
locator[actMethod] = async (...actArgs) => {
|
|
167
|
+
const start = Date.now();
|
|
168
|
+
try {
|
|
169
|
+
await page.evaluate(() => {
|
|
170
|
+
window.__btApiActionInProgress = true;
|
|
171
|
+
});
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const result = await origAct(...actArgs);
|
|
176
|
+
parentLogAction(session, {
|
|
177
|
+
action: actMethod,
|
|
178
|
+
source: "agent",
|
|
179
|
+
selector: hint,
|
|
180
|
+
value: actArgs[0] !== void 0 ? String(actArgs[0]).slice(0, 100) : void 0,
|
|
181
|
+
duration: Date.now() - start,
|
|
182
|
+
success: true
|
|
183
|
+
});
|
|
184
|
+
onActivity?.();
|
|
185
|
+
return result;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
parentLogAction(session, {
|
|
188
|
+
action: actMethod,
|
|
189
|
+
source: "agent",
|
|
190
|
+
selector: hint,
|
|
191
|
+
duration: Date.now() - start,
|
|
192
|
+
success: false,
|
|
193
|
+
error: err.message
|
|
194
|
+
});
|
|
195
|
+
onActivity?.();
|
|
196
|
+
throw err;
|
|
197
|
+
} finally {
|
|
198
|
+
try {
|
|
199
|
+
await page.evaluate(() => {
|
|
200
|
+
window.__btApiActionInProgress = false;
|
|
201
|
+
});
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
for (const method of LOCATOR_RETURNING_METHODS) {
|
|
208
|
+
if (typeof locator[method] !== "function") continue;
|
|
209
|
+
const origMethod = locator[method].bind(locator);
|
|
210
|
+
locator[method] = (...args) => {
|
|
211
|
+
const child = origMethod(...args);
|
|
212
|
+
const childHint = args.length > 0 ? `${hint}.${formatHint(method, args)}` : `${hint}.${method}()`;
|
|
213
|
+
return wrapLocator(child, childHint, session, page, onActivity);
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (typeof locator.nth === "function") {
|
|
217
|
+
const origNth = locator.nth.bind(locator);
|
|
218
|
+
locator.nth = (index) => {
|
|
219
|
+
const child = origNth(index);
|
|
220
|
+
const childHint = `${hint}.nth(${index})`;
|
|
221
|
+
return wrapLocator(child, childHint, session, page, onActivity);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (typeof locator.all === "function") {
|
|
225
|
+
const origAll = locator.all.bind(locator);
|
|
226
|
+
locator.all = async () => {
|
|
227
|
+
const items = await origAll();
|
|
228
|
+
return items.map((item, i) => {
|
|
229
|
+
const childHint = `${hint}.all()[${i}]`;
|
|
230
|
+
return wrapLocator(item, childHint, session, page, onActivity);
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return locator;
|
|
235
|
+
}
|
|
236
|
+
function wrapPageForActionLogging(page, session, onActivity) {
|
|
237
|
+
const PAGE_ACTIONS = [
|
|
238
|
+
"click",
|
|
239
|
+
"dblclick",
|
|
240
|
+
"fill",
|
|
241
|
+
"type",
|
|
242
|
+
"press",
|
|
243
|
+
"check",
|
|
244
|
+
"uncheck",
|
|
245
|
+
"selectOption",
|
|
246
|
+
"hover",
|
|
247
|
+
"focus"
|
|
248
|
+
];
|
|
249
|
+
const NAV_ACTIONS = ["goto", "reload", "goBack", "goForward"];
|
|
250
|
+
for (const method of PAGE_ACTIONS) {
|
|
251
|
+
const orig = page[method].bind(page);
|
|
252
|
+
page[method] = async (...args) => {
|
|
253
|
+
const start = Date.now();
|
|
254
|
+
try {
|
|
255
|
+
await page.evaluate(() => {
|
|
256
|
+
window.__btApiActionInProgress = true;
|
|
257
|
+
});
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const result = await orig(...args);
|
|
262
|
+
parentLogAction(session, {
|
|
263
|
+
action: method,
|
|
264
|
+
source: "agent",
|
|
265
|
+
selector: typeof args[0] === "string" ? args[0] : void 0,
|
|
266
|
+
value: args[1] !== void 0 ? String(args[1]).slice(0, 100) : void 0,
|
|
267
|
+
duration: Date.now() - start,
|
|
268
|
+
success: true
|
|
269
|
+
});
|
|
270
|
+
onActivity?.();
|
|
271
|
+
return result;
|
|
272
|
+
} catch (err) {
|
|
273
|
+
parentLogAction(session, {
|
|
274
|
+
action: method,
|
|
275
|
+
source: "agent",
|
|
276
|
+
selector: typeof args[0] === "string" ? args[0] : void 0,
|
|
277
|
+
duration: Date.now() - start,
|
|
278
|
+
success: false,
|
|
279
|
+
error: err.message
|
|
280
|
+
});
|
|
281
|
+
onActivity?.();
|
|
282
|
+
throw err;
|
|
283
|
+
} finally {
|
|
284
|
+
try {
|
|
285
|
+
await page.evaluate(() => {
|
|
286
|
+
window.__btApiActionInProgress = false;
|
|
287
|
+
});
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
for (const method of NAV_ACTIONS) {
|
|
294
|
+
const orig = page[method].bind(page);
|
|
295
|
+
page[method] = async (...args) => {
|
|
296
|
+
const start = Date.now();
|
|
297
|
+
try {
|
|
298
|
+
const result = await orig(...args);
|
|
299
|
+
parentLogAction(session, {
|
|
300
|
+
action: method,
|
|
301
|
+
source: "agent",
|
|
302
|
+
url: typeof args[0] === "string" ? args[0] : page.url(),
|
|
303
|
+
duration: Date.now() - start,
|
|
304
|
+
success: true
|
|
305
|
+
});
|
|
306
|
+
onActivity?.();
|
|
307
|
+
return result;
|
|
308
|
+
} catch (err) {
|
|
309
|
+
parentLogAction(session, {
|
|
310
|
+
action: method,
|
|
311
|
+
source: "agent",
|
|
312
|
+
url: typeof args[0] === "string" ? args[0] : void 0,
|
|
313
|
+
duration: Date.now() - start,
|
|
314
|
+
success: false,
|
|
315
|
+
error: err.message
|
|
316
|
+
});
|
|
317
|
+
onActivity?.();
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const LOCATOR_FACTORIES = [
|
|
323
|
+
"locator",
|
|
324
|
+
"getByRole",
|
|
325
|
+
"getByText",
|
|
326
|
+
"getByLabel",
|
|
327
|
+
"getByPlaceholder",
|
|
328
|
+
"getByAltText",
|
|
329
|
+
"getByTitle",
|
|
330
|
+
"getByTestId"
|
|
331
|
+
];
|
|
332
|
+
for (const factory of LOCATOR_FACTORIES) {
|
|
333
|
+
const orig = page[factory].bind(page);
|
|
334
|
+
page[factory] = (...factoryArgs) => {
|
|
335
|
+
const locator = orig(...factoryArgs);
|
|
336
|
+
const hint = formatHint(factory, factoryArgs);
|
|
337
|
+
return wrapLocator(locator, hint, session, page, onActivity);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
export {
|
|
342
|
+
clearActionLog,
|
|
343
|
+
clearNetworkLog,
|
|
344
|
+
formatActionEntry,
|
|
345
|
+
formatNetworkEntry,
|
|
346
|
+
parentLogAction,
|
|
347
|
+
readActionLog,
|
|
348
|
+
readNetworkLog,
|
|
349
|
+
wrapPageForActionLogging
|
|
350
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { runLibrettoCLI } from "./cli.js";
|
|
2
|
+
import {
|
|
3
|
+
maybeConfigureLLMClientFactoryFromEnv,
|
|
4
|
+
setLLMClientFactory
|
|
5
|
+
} from "./core/context.js";
|
|
6
|
+
import { runClose } from "./commands/browser.js";
|
|
7
|
+
maybeConfigureLLMClientFactoryFromEnv();
|
|
8
|
+
void runLibrettoCLI();
|
|
9
|
+
export {
|
|
10
|
+
runClose,
|
|
11
|
+
runLibrettoCLI,
|
|
12
|
+
setLLMClientFactory
|
|
13
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { appendFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { cwd } from "node:process";
|
|
4
|
+
import { isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
launchBrowser
|
|
8
|
+
} from "../../index.js";
|
|
9
|
+
import { getProfilePath, normalizeDomain } from "../core/browser.js";
|
|
10
|
+
import { getSessionDir } from "../core/context.js";
|
|
11
|
+
import { getPauseSignalPaths, removeSignalIfExists } from "../core/pause-signals.js";
|
|
12
|
+
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
13
|
+
const RESUME_POLL_INTERVAL_MS = 250;
|
|
14
|
+
function mirrorStdoutToFile(filePath) {
|
|
15
|
+
const stdout = process.stdout;
|
|
16
|
+
const originalWrite = stdout.write.bind(stdout);
|
|
17
|
+
stdout.write = ((chunk, ...args) => {
|
|
18
|
+
try {
|
|
19
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
20
|
+
appendFileSync(filePath, buffer);
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
return originalWrite(chunk, ...args);
|
|
24
|
+
});
|
|
25
|
+
return () => {
|
|
26
|
+
stdout.write = originalWrite;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function waitForResumeSignal(args) {
|
|
30
|
+
const { pausedSignalPath, resumeSignalPath } = args.signalPaths;
|
|
31
|
+
await mkdir(getSessionDir(args.session), { recursive: true });
|
|
32
|
+
await removeSignalIfExists(resumeSignalPath);
|
|
33
|
+
await writeFile(
|
|
34
|
+
pausedSignalPath,
|
|
35
|
+
JSON.stringify(args.details, null, 2),
|
|
36
|
+
"utf8"
|
|
37
|
+
);
|
|
38
|
+
await args.onPaused?.(args.details);
|
|
39
|
+
while (!existsSync(resumeSignalPath)) {
|
|
40
|
+
await new Promise(
|
|
41
|
+
(resolveWait) => setTimeout(resolveWait, RESUME_POLL_INTERVAL_MS)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
await removeSignalIfExists(resumeSignalPath);
|
|
45
|
+
await removeSignalIfExists(pausedSignalPath);
|
|
46
|
+
}
|
|
47
|
+
function isLoadedLibrettoWorkflow(value) {
|
|
48
|
+
if (!value || typeof value !== "object") return false;
|
|
49
|
+
const candidate = value;
|
|
50
|
+
return candidate[LIBRETTO_WORKFLOW_BRAND] === true && typeof candidate.run === "function" && !!candidate.metadata && typeof candidate.metadata === "object";
|
|
51
|
+
}
|
|
52
|
+
function resolveLocalAuthProfilePath(domain) {
|
|
53
|
+
return getProfilePath(normalizeDomain(domain));
|
|
54
|
+
}
|
|
55
|
+
function resolveWorkflowStorageStatePath(workflow) {
|
|
56
|
+
const authProfile = workflow.metadata.authProfile;
|
|
57
|
+
if (authProfile?.type !== "local") {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
return resolveLocalAuthProfilePath(authProfile.domain);
|
|
61
|
+
}
|
|
62
|
+
function getMissingLocalAuthProfileError(args) {
|
|
63
|
+
const normalizedDomain = normalizeDomain(args.domain);
|
|
64
|
+
return [
|
|
65
|
+
`Local auth profile not found for domain "${normalizedDomain}".`,
|
|
66
|
+
`Expected profile file: ${args.profilePath}`,
|
|
67
|
+
"To create it:",
|
|
68
|
+
` 1. libretto-cli open https://${normalizedDomain} --headed --session ${args.session}`,
|
|
69
|
+
" 2. Log in manually in the browser window.",
|
|
70
|
+
` 3. libretto-cli save ${normalizedDomain} --session ${args.session}`
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
function getAbsoluteIntegrationPath(integrationPath) {
|
|
74
|
+
const absolutePath = isAbsolute(integrationPath) ? integrationPath : resolve(cwd(), integrationPath);
|
|
75
|
+
if (!existsSync(absolutePath)) {
|
|
76
|
+
throw new Error(`Integration file does not exist: ${absolutePath}`);
|
|
77
|
+
}
|
|
78
|
+
return absolutePath;
|
|
79
|
+
}
|
|
80
|
+
async function loadWorkflowExport(absolutePath, exportName) {
|
|
81
|
+
let loadedModule;
|
|
82
|
+
try {
|
|
83
|
+
loadedModule = await import(pathToFileURL(absolutePath).href);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Failed to import integration module at ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const targetExport = loadedModule[exportName];
|
|
90
|
+
if (!targetExport) {
|
|
91
|
+
const availableExports = Object.keys(loadedModule);
|
|
92
|
+
const detail = availableExports.length > 0 ? ` Available exports: ${availableExports.join(", ")}` : " The module has no exports.";
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Export "${exportName}" was not found in ${absolutePath}.${detail}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (!isLoadedLibrettoWorkflow(targetExport)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Export "${exportName}" in ${absolutePath} must be a Libretto workflow instance. Use workflow(...) from "libretto".`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return targetExport;
|
|
103
|
+
}
|
|
104
|
+
async function runIntegrationInternal(args, options) {
|
|
105
|
+
const { logger } = options;
|
|
106
|
+
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
107
|
+
const workflow = await loadWorkflowExport(absolutePath, args.exportName);
|
|
108
|
+
const signalPaths = getPauseSignalPaths(args.session);
|
|
109
|
+
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
110
|
+
await removeSignalIfExists(signalPaths.resumeSignalPath);
|
|
111
|
+
await removeSignalIfExists(signalPaths.completedSignalPath);
|
|
112
|
+
await removeSignalIfExists(signalPaths.failedSignalPath);
|
|
113
|
+
await removeSignalIfExists(signalPaths.outputSignalPath);
|
|
114
|
+
const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
|
|
115
|
+
console.log(
|
|
116
|
+
`Running integration "${args.exportName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`
|
|
117
|
+
);
|
|
118
|
+
const integrationLogger = logger.withScope("integration-run", {
|
|
119
|
+
integrationPath: absolutePath,
|
|
120
|
+
integrationExport: args.exportName,
|
|
121
|
+
session: args.session
|
|
122
|
+
});
|
|
123
|
+
const authProfile = workflow.metadata.authProfile;
|
|
124
|
+
const storageStatePath = resolveWorkflowStorageStatePath(workflow);
|
|
125
|
+
if (authProfile?.type === "local" && storageStatePath && !existsSync(storageStatePath)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
getMissingLocalAuthProfileError({
|
|
128
|
+
domain: authProfile.domain,
|
|
129
|
+
profilePath: storageStatePath,
|
|
130
|
+
session: args.session
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const browserSession = await launchBrowser({
|
|
135
|
+
sessionName: args.session,
|
|
136
|
+
headless: args.headless,
|
|
137
|
+
storageStatePath
|
|
138
|
+
});
|
|
139
|
+
const workflowContext = {
|
|
140
|
+
logger: integrationLogger,
|
|
141
|
+
page: browserSession.page,
|
|
142
|
+
context: browserSession.context,
|
|
143
|
+
browser: browserSession.browser,
|
|
144
|
+
session: args.session,
|
|
145
|
+
integrationPath: absolutePath,
|
|
146
|
+
exportName: args.exportName,
|
|
147
|
+
headless: args.headless,
|
|
148
|
+
debug: args.debug,
|
|
149
|
+
pause: async () => {
|
|
150
|
+
const details = {
|
|
151
|
+
sessionName: args.session,
|
|
152
|
+
pausedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
153
|
+
url: browserSession.page.url()
|
|
154
|
+
};
|
|
155
|
+
console.log(`[pause] Paused at ${details.url}`);
|
|
156
|
+
console.log("[pause] Waiting for resume signal...");
|
|
157
|
+
await waitForResumeSignal({
|
|
158
|
+
signalPaths,
|
|
159
|
+
session: args.session,
|
|
160
|
+
details,
|
|
161
|
+
onPaused: options.onPaused
|
|
162
|
+
});
|
|
163
|
+
console.log("[pause] Resume signal received. Continuing workflow...");
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
try {
|
|
168
|
+
await workflow.run(workflowContext, args.params ?? {});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
await writeFile(
|
|
171
|
+
signalPaths.failedSignalPath,
|
|
172
|
+
JSON.stringify(
|
|
173
|
+
{
|
|
174
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
175
|
+
message: error instanceof Error ? error.message : String(error)
|
|
176
|
+
},
|
|
177
|
+
null,
|
|
178
|
+
2
|
|
179
|
+
),
|
|
180
|
+
"utf8"
|
|
181
|
+
);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
await writeFile(
|
|
185
|
+
signalPaths.completedSignalPath,
|
|
186
|
+
JSON.stringify({ completedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
187
|
+
"utf8"
|
|
188
|
+
);
|
|
189
|
+
console.log("Integration completed.");
|
|
190
|
+
return { status: "completed" };
|
|
191
|
+
} finally {
|
|
192
|
+
restoreStdout();
|
|
193
|
+
await browserSession.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function runIntegrationFromFileInWorker(args, logger, onPaused) {
|
|
197
|
+
return await runIntegrationInternal(args, {
|
|
198
|
+
logger,
|
|
199
|
+
onPaused
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
runIntegrationFromFileInWorker
|
|
204
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { runIntegrationFromFileInWorker } from "./run-integration-runtime.js";
|
|
3
|
+
import {
|
|
4
|
+
ensureLibrettoSetup,
|
|
5
|
+
withSessionLogger
|
|
6
|
+
} from "../core/context.js";
|
|
7
|
+
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
8
|
+
function sendMessage(message) {
|
|
9
|
+
if (typeof process.send !== "function" || !process.connected) return;
|
|
10
|
+
try {
|
|
11
|
+
process.send(message);
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function parseWorkerRequest(argv) {
|
|
16
|
+
const rawPayload = argv[2];
|
|
17
|
+
if (!rawPayload) {
|
|
18
|
+
throw new Error("Missing worker payload argument.");
|
|
19
|
+
}
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(rawPayload);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid worker payload JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (!parsed || typeof parsed !== "object") {
|
|
29
|
+
throw new Error("Worker payload must be an object.");
|
|
30
|
+
}
|
|
31
|
+
const candidate = parsed;
|
|
32
|
+
if (typeof candidate.integrationPath !== "string" || typeof candidate.exportName !== "string" || typeof candidate.session !== "string" || typeof candidate.headless !== "boolean" || typeof candidate.debug !== "boolean" || !("params" in candidate)) {
|
|
33
|
+
throw new Error("Worker payload is missing required fields.");
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
integrationPath: candidate.integrationPath,
|
|
37
|
+
exportName: candidate.exportName,
|
|
38
|
+
session: candidate.session,
|
|
39
|
+
headless: candidate.headless,
|
|
40
|
+
debug: candidate.debug,
|
|
41
|
+
params: candidate.params
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function main() {
|
|
45
|
+
let request = null;
|
|
46
|
+
let exitCode = 0;
|
|
47
|
+
try {
|
|
48
|
+
request = parseWorkerRequest(process.argv);
|
|
49
|
+
const workerRequest = request;
|
|
50
|
+
ensureLibrettoSetup();
|
|
51
|
+
await withSessionLogger(workerRequest.session, async (logger) => {
|
|
52
|
+
await runIntegrationFromFileInWorker(
|
|
53
|
+
workerRequest,
|
|
54
|
+
logger,
|
|
55
|
+
async (details) => {
|
|
56
|
+
sendMessage({ type: "paused", details });
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
sendMessage({ type: "completed" });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
if (request) {
|
|
64
|
+
const { failedSignalPath } = getPauseSignalPaths(request.session);
|
|
65
|
+
await writeFile(
|
|
66
|
+
failedSignalPath,
|
|
67
|
+
JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
70
|
+
message
|
|
71
|
+
},
|
|
72
|
+
null,
|
|
73
|
+
2
|
|
74
|
+
),
|
|
75
|
+
"utf8"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
sendMessage({ type: "failed", message });
|
|
79
|
+
exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
process.exit(exitCode);
|
|
82
|
+
}
|
|
83
|
+
void main();
|