libretto 0.6.23 → 0.6.25
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 +9 -1
- package/README.template.md +9 -1
- package/dist/cli/commands/browser.js +17 -10
- package/dist/cli/commands/cloud-credentials.js +70 -0
- package/dist/cli/commands/deploy.js +24 -2
- package/dist/cli/commands/execution.js +9 -30
- package/dist/cli/commands/import-chrome-profiles.js +46 -0
- package/dist/cli/commands/profiles.js +71 -0
- package/dist/cli/commands/shared.js +1 -3
- package/dist/cli/core/browser.js +89 -75
- package/dist/cli/core/daemon/daemon.js +47 -35
- package/dist/cli/core/daemon/ipc.js +3 -0
- package/dist/cli/core/deploy-artifact.js +85 -22
- package/dist/cli/core/profiles.js +47 -0
- package/dist/cli/core/prompt.js +9 -0
- package/dist/cli/core/providers/libretto-cloud.js +6 -2
- package/dist/cli/core/session-logs.js +325 -0
- package/dist/cli/core/telemetry.js +83 -313
- package/dist/cli/core/workflow-runner/runner.js +65 -0
- package/dist/cli/router.js +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/runtime/recovery/agent.js +33 -14
- package/dist/shared/workflow/auth-profile-name.d.ts +3 -0
- package/dist/shared/workflow/auth-profile-name.js +29 -0
- package/dist/shared/workflow/auth-profile-state.d.ts +20 -0
- package/dist/shared/workflow/auth-profile-state.js +105 -0
- package/dist/shared/workflow/authenticate.d.ts +17 -0
- package/dist/shared/workflow/authenticate.js +37 -0
- package/dist/shared/workflow/credentials.d.ts +5 -0
- package/dist/shared/workflow/credentials.js +68 -0
- package/dist/shared/workflow/workflow.d.ts +16 -1
- package/dist/shared/workflow/workflow.js +56 -4
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +3 -4
- package/skills/libretto/references/auth-profiles.md +61 -11
- package/skills/libretto/references/code-generation-rules.md +31 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/browser.ts +19 -11
- package/src/cli/commands/cloud-credentials.ts +82 -0
- package/src/cli/commands/deploy.ts +41 -2
- package/src/cli/commands/execution.ts +10 -31
- package/src/cli/commands/import-chrome-profiles.ts +46 -0
- package/src/cli/commands/profiles.ts +90 -0
- package/src/cli/commands/shared.ts +4 -8
- package/src/cli/core/browser.ts +102 -91
- package/src/cli/core/daemon/config.ts +4 -1
- package/src/cli/core/daemon/daemon.ts +52 -44
- package/src/cli/core/daemon/ipc.ts +15 -0
- package/src/cli/core/deploy-artifact.ts +131 -32
- package/src/cli/core/profiles.ts +53 -0
- package/src/cli/core/prompt.ts +15 -0
- package/src/cli/core/providers/libretto-cloud.ts +6 -2
- package/src/cli/core/providers/types.ts +4 -1
- package/src/cli/core/session-logs.ts +445 -0
- package/src/cli/core/telemetry.ts +105 -422
- package/src/cli/core/workflow-runner/runner.ts +86 -1
- package/src/cli/router.ts +8 -0
- package/src/index.ts +10 -0
- package/src/runtime/recovery/agent.ts +51 -12
- package/src/shared/workflow/auth-profile-name.ts +27 -0
- package/src/shared/workflow/auth-profile-state.ts +144 -0
- package/src/shared/workflow/authenticate.ts +63 -0
- package/src/shared/workflow/credentials.ts +91 -0
- package/src/shared/workflow/workflow.ts +89 -4
|
@@ -1,325 +1,95 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!existsSync(logPath)) return [];
|
|
15
|
-
const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
16
|
-
let entries = lines.map(
|
|
17
|
-
(line) => JSON.parse(line)
|
|
18
|
-
);
|
|
19
|
-
if (opts.method) {
|
|
20
|
-
const m = opts.method.toUpperCase();
|
|
21
|
-
entries = entries.filter((e) => e.method === m);
|
|
22
|
-
}
|
|
23
|
-
if (opts.filter) {
|
|
24
|
-
const re = new RegExp(opts.filter, "i");
|
|
25
|
-
entries = entries.filter((e) => re.test(e.url));
|
|
26
|
-
}
|
|
27
|
-
if (opts.pageId) {
|
|
28
|
-
entries = entries.filter((e) => e.pageId === opts.pageId);
|
|
29
|
-
}
|
|
30
|
-
const last = opts.last ?? 20;
|
|
31
|
-
if (entries.length > last) {
|
|
32
|
-
entries = entries.slice(-last);
|
|
33
|
-
}
|
|
34
|
-
return entries;
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { resolveHostedApiUrl } from "./auth-fetch.js";
|
|
6
|
+
const TELEMETRY_FILE_NAME = "telemetry.json";
|
|
7
|
+
const TELEMETRY_ENDPOINT_PATH = "/v1/telemetry/recordCliEvent";
|
|
8
|
+
const TELEMETRY_TIMEOUT_MS = 250;
|
|
9
|
+
function telemetryDir() {
|
|
10
|
+
return join(homedir(), ".libretto");
|
|
11
|
+
}
|
|
12
|
+
function telemetryPath() {
|
|
13
|
+
return join(telemetryDir(), TELEMETRY_FILE_NAME);
|
|
35
14
|
}
|
|
36
|
-
function
|
|
15
|
+
function isTelemetryDisabled() {
|
|
16
|
+
return process.env.LIBRETTO_TELEMETRY_DISABLED === "1" || process.env.DO_NOT_TRACK === "1" || process.env.CI === "1";
|
|
17
|
+
}
|
|
18
|
+
async function readTelemetryState() {
|
|
37
19
|
try {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} catch {
|
|
20
|
+
const raw = await fs.readFile(telemetryPath(), "utf8");
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.code !== "ENOENT") throw error;
|
|
24
|
+
return null;
|
|
44
25
|
}
|
|
45
26
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
async function readOrCreateInstallId() {
|
|
28
|
+
const state = await readTelemetryState();
|
|
29
|
+
if (state?.enabled === false) return null;
|
|
30
|
+
if (typeof state?.installId === "string" && state.installId.length > 0) {
|
|
31
|
+
return state.installId;
|
|
32
|
+
}
|
|
33
|
+
const installId = randomUUID();
|
|
34
|
+
writeTelemetryNotice();
|
|
35
|
+
await writeTelemetryState({ installId, enabled: true });
|
|
36
|
+
return installId;
|
|
37
|
+
}
|
|
38
|
+
function writeTelemetryNotice() {
|
|
39
|
+
if (!process.stderr.isTTY) return;
|
|
40
|
+
process.stderr.write(
|
|
41
|
+
[
|
|
42
|
+
"Libretto collects anonymous CLI telemetry: install id, timestamp, command event, and error status only.",
|
|
43
|
+
"Set LIBRETTO_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 to disable it, or set enabled:false in ~/.libretto/telemetry.json."
|
|
44
|
+
].join(" ") + "\n"
|
|
53
45
|
);
|
|
54
|
-
if (opts.action) {
|
|
55
|
-
const a = opts.action.toLowerCase();
|
|
56
|
-
entries = entries.filter((e) => e.action === a);
|
|
57
|
-
}
|
|
58
|
-
if (opts.source) {
|
|
59
|
-
const s = opts.source.toLowerCase();
|
|
60
|
-
entries = entries.filter((e) => e.source === s);
|
|
61
|
-
}
|
|
62
|
-
if (opts.filter) {
|
|
63
|
-
const re = new RegExp(opts.filter, "i");
|
|
64
|
-
entries = entries.filter(
|
|
65
|
-
(e) => re.test(e.action) || re.test(e.selector || "") || re.test(e.bestSemanticSelector || "") || re.test(e.targetSelector || "") || re.test((e.ancestorSelectors || []).join(" ")) || re.test(e.nearbyText || "") || re.test((e.composedPath || []).join(" ")) || re.test(e.value || "") || re.test(e.url || "")
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
if (opts.pageId) {
|
|
69
|
-
entries = entries.filter((e) => e.pageId === opts.pageId);
|
|
70
|
-
}
|
|
71
|
-
const last = opts.last ?? 20;
|
|
72
|
-
if (entries.length > last) {
|
|
73
|
-
entries = entries.slice(-last);
|
|
74
|
-
}
|
|
75
|
-
return entries;
|
|
76
46
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"check",
|
|
84
|
-
"uncheck",
|
|
85
|
-
"selectOption",
|
|
86
|
-
"hover",
|
|
87
|
-
"focus",
|
|
88
|
-
"scrollIntoViewIfNeeded",
|
|
89
|
-
"waitFor",
|
|
90
|
-
"innerHTML",
|
|
91
|
-
"innerText",
|
|
92
|
-
"textContent",
|
|
93
|
-
"inputValue",
|
|
94
|
-
"isChecked",
|
|
95
|
-
"isDisabled",
|
|
96
|
-
"isEditable",
|
|
97
|
-
"isEnabled",
|
|
98
|
-
"isHidden",
|
|
99
|
-
"isVisible",
|
|
100
|
-
"count",
|
|
101
|
-
"boundingBox",
|
|
102
|
-
"screenshot",
|
|
103
|
-
"evaluate",
|
|
104
|
-
"evaluateAll",
|
|
105
|
-
"evaluateHandle",
|
|
106
|
-
"getAttribute",
|
|
107
|
-
"dispatchEvent",
|
|
108
|
-
"setInputFiles",
|
|
109
|
-
"selectText",
|
|
110
|
-
"dragTo",
|
|
111
|
-
"highlight",
|
|
112
|
-
"tap"
|
|
113
|
-
];
|
|
114
|
-
const LOCATOR_RETURNING_METHODS = [
|
|
115
|
-
"first",
|
|
116
|
-
"last",
|
|
117
|
-
"locator",
|
|
118
|
-
"getByRole",
|
|
119
|
-
"getByText",
|
|
120
|
-
"getByLabel",
|
|
121
|
-
"getByPlaceholder",
|
|
122
|
-
"getByAltText",
|
|
123
|
-
"getByTitle",
|
|
124
|
-
"getByTestId",
|
|
125
|
-
"filter",
|
|
126
|
-
"and",
|
|
127
|
-
"or"
|
|
128
|
-
];
|
|
129
|
-
function formatHint(method, args) {
|
|
130
|
-
const formatted = args.map((a) => JSON.stringify(a)).join(", ");
|
|
131
|
-
return `${method}(${formatted})`;
|
|
47
|
+
async function writeTelemetryState(state) {
|
|
48
|
+
await fs.mkdir(telemetryDir(), { recursive: true, mode: 448 });
|
|
49
|
+
const target = telemetryPath();
|
|
50
|
+
const tmp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
51
|
+
await fs.writeFile(tmp, JSON.stringify(state, null, 2), { mode: 384 });
|
|
52
|
+
await fs.rename(tmp, target);
|
|
132
53
|
}
|
|
133
|
-
function
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
window.__btApiActionInProgress = true;
|
|
144
|
-
});
|
|
145
|
-
} catch {
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
const result = await origAct(...actArgs);
|
|
149
|
-
parentLogAction(session, {
|
|
150
|
-
pageId,
|
|
151
|
-
action: actMethod,
|
|
152
|
-
source: "agent",
|
|
153
|
-
selector: hint,
|
|
154
|
-
value: actArgs[0] !== void 0 ? String(actArgs[0]).slice(0, 100) : void 0,
|
|
155
|
-
duration: Date.now() - start,
|
|
156
|
-
success: true
|
|
157
|
-
});
|
|
158
|
-
onActivity?.();
|
|
159
|
-
return result;
|
|
160
|
-
} catch (err) {
|
|
161
|
-
parentLogAction(session, {
|
|
162
|
-
pageId,
|
|
163
|
-
action: actMethod,
|
|
164
|
-
source: "agent",
|
|
165
|
-
selector: hint,
|
|
166
|
-
duration: Date.now() - start,
|
|
167
|
-
success: false,
|
|
168
|
-
error: err.message
|
|
169
|
-
});
|
|
170
|
-
onActivity?.();
|
|
171
|
-
throw err;
|
|
172
|
-
} finally {
|
|
173
|
-
try {
|
|
174
|
-
await page.evaluate(() => {
|
|
175
|
-
window.__btApiActionInProgress = false;
|
|
176
|
-
});
|
|
177
|
-
} catch {
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
for (const method of LOCATOR_RETURNING_METHODS) {
|
|
183
|
-
if (typeof locator[method] !== "function") continue;
|
|
184
|
-
const origMethod = locator[method].bind(locator);
|
|
185
|
-
locator[method] = (...args) => {
|
|
186
|
-
const child = origMethod(...args);
|
|
187
|
-
const childHint = args.length > 0 ? `${hint}.${formatHint(method, args)}` : `${hint}.${method}()`;
|
|
188
|
-
return wrapLocator(child, childHint, session, page, pageId, onActivity);
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
if (typeof locator.nth === "function") {
|
|
192
|
-
const origNth = locator.nth.bind(locator);
|
|
193
|
-
locator.nth = (index) => {
|
|
194
|
-
const child = origNth(index);
|
|
195
|
-
const childHint = `${hint}.nth(${index})`;
|
|
196
|
-
return wrapLocator(child, childHint, session, page, pageId, onActivity);
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
if (typeof locator.all === "function") {
|
|
200
|
-
const origAll = locator.all.bind(locator);
|
|
201
|
-
locator.all = async () => {
|
|
202
|
-
const items = await origAll();
|
|
203
|
-
return items.map((item, i) => {
|
|
204
|
-
const childHint = `${hint}.all()[${i}]`;
|
|
205
|
-
return wrapLocator(item, childHint, session, page, pageId, onActivity);
|
|
206
|
-
});
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return locator;
|
|
54
|
+
async function recordCliTelemetryEvent(command, error) {
|
|
55
|
+
if (isTelemetryDisabled()) return;
|
|
56
|
+
const installId = await readOrCreateInstallId();
|
|
57
|
+
if (!installId) return;
|
|
58
|
+
await sendWithTimeout({
|
|
59
|
+
installId,
|
|
60
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61
|
+
event: `libretto ${command.path.join(" ")}`,
|
|
62
|
+
error
|
|
63
|
+
});
|
|
210
64
|
}
|
|
211
|
-
function
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
for (const method of PAGE_ACTIONS) {
|
|
226
|
-
const orig = page[method].bind(page);
|
|
227
|
-
page[method] = async (...args) => {
|
|
228
|
-
const start = Date.now();
|
|
229
|
-
try {
|
|
230
|
-
await page.evaluate(() => {
|
|
231
|
-
window.__btApiActionInProgress = true;
|
|
232
|
-
});
|
|
233
|
-
} catch {
|
|
234
|
-
}
|
|
235
|
-
try {
|
|
236
|
-
const result = await orig(...args);
|
|
237
|
-
parentLogAction(session, {
|
|
238
|
-
pageId,
|
|
239
|
-
action: method,
|
|
240
|
-
source: "agent",
|
|
241
|
-
selector: typeof args[0] === "string" ? args[0] : void 0,
|
|
242
|
-
value: args[1] !== void 0 ? String(args[1]).slice(0, 100) : void 0,
|
|
243
|
-
duration: Date.now() - start,
|
|
244
|
-
success: true
|
|
245
|
-
});
|
|
246
|
-
onActivity?.();
|
|
247
|
-
return result;
|
|
248
|
-
} catch (err) {
|
|
249
|
-
parentLogAction(session, {
|
|
250
|
-
pageId,
|
|
251
|
-
action: method,
|
|
252
|
-
source: "agent",
|
|
253
|
-
selector: typeof args[0] === "string" ? args[0] : void 0,
|
|
254
|
-
duration: Date.now() - start,
|
|
255
|
-
success: false,
|
|
256
|
-
error: err.message
|
|
257
|
-
});
|
|
258
|
-
onActivity?.();
|
|
259
|
-
throw err;
|
|
260
|
-
} finally {
|
|
261
|
-
try {
|
|
262
|
-
await page.evaluate(() => {
|
|
263
|
-
window.__btApiActionInProgress = false;
|
|
264
|
-
});
|
|
265
|
-
} catch {
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
for (const method of NAV_ACTIONS) {
|
|
271
|
-
const orig = page[method].bind(page);
|
|
272
|
-
page[method] = async (...args) => {
|
|
273
|
-
const start = Date.now();
|
|
274
|
-
try {
|
|
275
|
-
const result = await orig(...args);
|
|
276
|
-
parentLogAction(session, {
|
|
277
|
-
pageId,
|
|
278
|
-
action: method,
|
|
279
|
-
source: "agent",
|
|
280
|
-
url: typeof args[0] === "string" ? args[0] : page.url(),
|
|
281
|
-
duration: Date.now() - start,
|
|
282
|
-
success: true
|
|
283
|
-
});
|
|
284
|
-
onActivity?.();
|
|
285
|
-
return result;
|
|
286
|
-
} catch (err) {
|
|
287
|
-
parentLogAction(session, {
|
|
288
|
-
pageId,
|
|
289
|
-
action: method,
|
|
290
|
-
source: "agent",
|
|
291
|
-
url: typeof args[0] === "string" ? args[0] : void 0,
|
|
292
|
-
duration: Date.now() - start,
|
|
293
|
-
success: false,
|
|
294
|
-
error: err.message
|
|
295
|
-
});
|
|
296
|
-
onActivity?.();
|
|
297
|
-
throw err;
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
const LOCATOR_FACTORIES = [
|
|
302
|
-
"locator",
|
|
303
|
-
"getByRole",
|
|
304
|
-
"getByText",
|
|
305
|
-
"getByLabel",
|
|
306
|
-
"getByPlaceholder",
|
|
307
|
-
"getByAltText",
|
|
308
|
-
"getByTitle",
|
|
309
|
-
"getByTestId"
|
|
310
|
-
];
|
|
311
|
-
for (const factory of LOCATOR_FACTORIES) {
|
|
312
|
-
const orig = page[factory].bind(page);
|
|
313
|
-
page[factory] = (...factoryArgs) => {
|
|
314
|
-
const locator = orig(...factoryArgs);
|
|
315
|
-
const hint = formatHint(factory, factoryArgs);
|
|
316
|
-
return wrapLocator(locator, hint, session, page, pageId, onActivity);
|
|
317
|
-
};
|
|
65
|
+
async function sendWithTimeout(payload) {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
68
|
+
try {
|
|
69
|
+
await fetch(`${resolveHostedApiUrl()}${TELEMETRY_ENDPOINT_PATH}`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json"
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({ json: payload }),
|
|
75
|
+
signal: controller.signal
|
|
76
|
+
});
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timeout);
|
|
318
79
|
}
|
|
319
80
|
}
|
|
81
|
+
const telemetryMiddleware = async ({ command, next }) => {
|
|
82
|
+
try {
|
|
83
|
+
const result = await next();
|
|
84
|
+
await recordCliTelemetryEvent(command, false).catch(() => {
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
await recordCliTelemetryEvent(command, true).catch(() => {
|
|
89
|
+
});
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
320
93
|
export {
|
|
321
|
-
|
|
322
|
-
readActionLog,
|
|
323
|
-
readNetworkLog,
|
|
324
|
-
wrapPageForActionLogging
|
|
94
|
+
telemetryMiddleware
|
|
325
95
|
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { installPauseHandler } from "../../../shared/debug/pause-handler.js";
|
|
2
|
+
import {
|
|
3
|
+
mergeAuthProfileStorageState,
|
|
4
|
+
normalizeAuthProfileSite
|
|
5
|
+
} from "../../../shared/workflow/auth-profile-state.js";
|
|
6
|
+
import { readProfile, writeProfile } from "../profiles.js";
|
|
2
7
|
import {
|
|
3
8
|
getAbsoluteIntegrationPath,
|
|
4
9
|
installHeadedWorkflowVisualization,
|
|
@@ -72,6 +77,7 @@ class WorkflowController {
|
|
|
72
77
|
session: this.config.session,
|
|
73
78
|
page: this.config.page
|
|
74
79
|
};
|
|
80
|
+
const visitedSites = createVisitedSiteTracker(this.config.context);
|
|
75
81
|
const uninstallPauseHandler = installPauseHandler(
|
|
76
82
|
(pauseArgs) => this.pause({
|
|
77
83
|
...pauseArgs,
|
|
@@ -80,6 +86,12 @@ class WorkflowController {
|
|
|
80
86
|
);
|
|
81
87
|
try {
|
|
82
88
|
await workflow.run(workflowContext, workflowConfig.params ?? {});
|
|
89
|
+
await refreshLocalAuthProfileIfEnabled({
|
|
90
|
+
context: this.config.context,
|
|
91
|
+
enabled: this.config.refreshLocalAuthProfiles === true,
|
|
92
|
+
sites: visitedSites.sites(),
|
|
93
|
+
workflow
|
|
94
|
+
});
|
|
83
95
|
} catch (error) {
|
|
84
96
|
this.emitOutcome({
|
|
85
97
|
state: "finished",
|
|
@@ -90,6 +102,7 @@ class WorkflowController {
|
|
|
90
102
|
return;
|
|
91
103
|
} finally {
|
|
92
104
|
uninstallPauseHandler();
|
|
105
|
+
visitedSites.dispose();
|
|
93
106
|
}
|
|
94
107
|
this.emitOutcome({
|
|
95
108
|
state: "finished",
|
|
@@ -139,6 +152,58 @@ class WorkflowController {
|
|
|
139
152
|
};
|
|
140
153
|
}
|
|
141
154
|
}
|
|
155
|
+
async function refreshLocalAuthProfileIfEnabled(args) {
|
|
156
|
+
const { context, enabled, sites, workflow } = args;
|
|
157
|
+
if (!workflow.authProfileName || workflow.authProfileRefresh !== true) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!enabled) return;
|
|
161
|
+
if (sites.length === 0) {
|
|
162
|
+
console.warn(
|
|
163
|
+
`Auth profile refresh skipped for "${workflow.authProfileName}": workflow did not visit any http(s) sites.`
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const existing = readProfile(workflow.authProfileName);
|
|
168
|
+
const latest = await context.storageState({ indexedDB: true });
|
|
169
|
+
const state = mergeAuthProfileStorageState(existing, latest, sites);
|
|
170
|
+
await writeProfile(workflow.authProfileName, state);
|
|
171
|
+
console.warn(`Auth profile refreshed: ${workflow.authProfileName}`);
|
|
172
|
+
}
|
|
173
|
+
function createVisitedSiteTracker(context) {
|
|
174
|
+
const sites = /* @__PURE__ */ new Set();
|
|
175
|
+
const pageListeners = /* @__PURE__ */ new Map();
|
|
176
|
+
const recordUrl = (url) => {
|
|
177
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) return;
|
|
178
|
+
const site = normalizeAuthProfileSite(url);
|
|
179
|
+
if (site) sites.add(site);
|
|
180
|
+
};
|
|
181
|
+
const trackPage = (page) => {
|
|
182
|
+
if (pageListeners.has(page)) return;
|
|
183
|
+
recordUrl(page.url());
|
|
184
|
+
const onFrameNavigated = (frame) => {
|
|
185
|
+
if (frame === page.mainFrame()) {
|
|
186
|
+
recordUrl(frame.url());
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
pageListeners.set(page, onFrameNavigated);
|
|
190
|
+
page.on("framenavigated", onFrameNavigated);
|
|
191
|
+
};
|
|
192
|
+
for (const page of context.pages()) {
|
|
193
|
+
trackPage(page);
|
|
194
|
+
}
|
|
195
|
+
context.on("page", trackPage);
|
|
196
|
+
return {
|
|
197
|
+
sites: () => [...sites],
|
|
198
|
+
dispose: () => {
|
|
199
|
+
context.off("page", trackPage);
|
|
200
|
+
for (const [page, listener] of pageListeners) {
|
|
201
|
+
page.off("framenavigated", listener);
|
|
202
|
+
}
|
|
203
|
+
pageListeners.clear();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
142
207
|
function chunkToString(chunk) {
|
|
143
208
|
return Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
144
209
|
}
|
package/dist/cli/router.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { authCommands } from "./commands/auth.js";
|
|
2
2
|
import { billingCommands } from "./commands/billing.js";
|
|
3
3
|
import { browserCommands } from "./commands/browser.js";
|
|
4
|
+
import { cloudCredentialCommands } from "./commands/cloud-credentials.js";
|
|
4
5
|
import { deployCommand } from "./commands/deploy.js";
|
|
5
6
|
import { executionCommands } from "./commands/execution.js";
|
|
6
7
|
import { experimentsCommand } from "./commands/experiments.js";
|
|
8
|
+
import { importChromeProfilesCommand } from "./commands/import-chrome-profiles.js";
|
|
9
|
+
import { profileCommands } from "./commands/profiles.js";
|
|
7
10
|
import { setupCommand } from "./commands/setup.js";
|
|
8
11
|
import { statusCommand } from "./commands/status.js";
|
|
9
12
|
import { snapshotCommand } from "./commands/snapshot.js";
|
|
10
13
|
import { searchCommand } from "./commands/search.js";
|
|
14
|
+
import { telemetryMiddleware } from "./core/telemetry.js";
|
|
11
15
|
import { updateCommand } from "./commands/update.js";
|
|
12
16
|
import { SimpleCLI } from "affordance";
|
|
13
17
|
const cliRoutes = {
|
|
@@ -17,10 +21,13 @@ const cliRoutes = {
|
|
|
17
21
|
routes: {
|
|
18
22
|
deploy: deployCommand,
|
|
19
23
|
auth: authCommands,
|
|
20
|
-
billing: billingCommands
|
|
24
|
+
billing: billingCommands,
|
|
25
|
+
credentials: cloudCredentialCommands,
|
|
26
|
+
profiles: profileCommands
|
|
21
27
|
}
|
|
22
28
|
}),
|
|
23
29
|
experiments: experimentsCommand,
|
|
30
|
+
"import-chrome-profiles": importChromeProfilesCommand,
|
|
24
31
|
...executionCommands,
|
|
25
32
|
search: searchCommand,
|
|
26
33
|
setup: setupCommand,
|
|
@@ -30,6 +37,7 @@ const cliRoutes = {
|
|
|
30
37
|
};
|
|
31
38
|
function createCLIApp() {
|
|
32
39
|
return SimpleCLI.define("libretto", cliRoutes, {
|
|
40
|
+
middlewares: [telemetryMiddleware],
|
|
33
41
|
appendHelpText: [
|
|
34
42
|
"Options:",
|
|
35
43
|
" --session <name> Required for session-scoped commands",
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ export { InstrumentationOptions, InstrumentedPage, installInstrumentation, instr
|
|
|
13
13
|
export { GhostCursorOptions, ensureGhostCursor, ghostClick, hideGhostCursor, moveGhostCursor } from './shared/visualization/ghost-cursor.js';
|
|
14
14
|
export { HighlightOptions, clearHighlights, ensureHighlightLayer, showHighlight } from './shared/visualization/highlight.js';
|
|
15
15
|
export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './shared/run/browser.js';
|
|
16
|
+
export { LibrettoAuthenticateOptions, librettoAuthenticate } from './shared/workflow/authenticate.js';
|
|
16
17
|
export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, LibrettoWorkflowInputError, LibrettoWorkflowOptions, WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow } from './shared/workflow/workflow.js';
|
|
18
|
+
export { AuthProfileStorageState, captureAuthProfileStorageState, normalizeAuthProfileSite, parseAuthProfileSites } from './shared/workflow/auth-profile-state.js';
|
|
17
19
|
import 'zod';
|
|
18
20
|
import 'playwright';
|
|
19
21
|
import 'ai';
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,9 @@ import {
|
|
|
60
60
|
import {
|
|
61
61
|
launchBrowser
|
|
62
62
|
} from "./shared/run/api.js";
|
|
63
|
+
import {
|
|
64
|
+
librettoAuthenticate
|
|
65
|
+
} from "./shared/workflow/authenticate.js";
|
|
63
66
|
import {
|
|
64
67
|
getDefaultWorkflowFromModuleExports,
|
|
65
68
|
getWorkflowFromModuleExports,
|
|
@@ -71,6 +74,11 @@ import {
|
|
|
71
74
|
validateWorkflowInput,
|
|
72
75
|
workflow
|
|
73
76
|
} from "./shared/workflow/workflow.js";
|
|
77
|
+
import {
|
|
78
|
+
captureAuthProfileStorageState,
|
|
79
|
+
normalizeAuthProfileSite,
|
|
80
|
+
parseAuthProfileSites
|
|
81
|
+
} from "./shared/workflow/auth-profile-state.js";
|
|
74
82
|
const isDirectExecution = () => {
|
|
75
83
|
const entryArg = process.argv[1];
|
|
76
84
|
if (!entryArg) {
|
|
@@ -97,6 +105,7 @@ export {
|
|
|
97
105
|
SessionStateFileSchema,
|
|
98
106
|
SessionStatusSchema,
|
|
99
107
|
attemptWithRecovery,
|
|
108
|
+
captureAuthProfileStorageState,
|
|
100
109
|
clearHighlights,
|
|
101
110
|
computerUseRecoveryAction,
|
|
102
111
|
createFileLogSink,
|
|
@@ -119,8 +128,11 @@ export {
|
|
|
119
128
|
isLibrettoWorkflow,
|
|
120
129
|
jsonlConsoleSink,
|
|
121
130
|
launchBrowser,
|
|
131
|
+
librettoAuthenticate,
|
|
122
132
|
moveGhostCursor,
|
|
133
|
+
normalizeAuthProfileSite,
|
|
123
134
|
pageRequest,
|
|
135
|
+
parseAuthProfileSites,
|
|
124
136
|
parseSessionStateContent,
|
|
125
137
|
parseSessionStateData,
|
|
126
138
|
pause,
|
|
@@ -88,16 +88,23 @@ function readPngDimensions(buffer) {
|
|
|
88
88
|
height: buffer.readUInt32BE(20)
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
function toPositiveNumber(value) {
|
|
92
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
93
|
+
}
|
|
94
|
+
async function getViewportFromPage(page) {
|
|
95
|
+
const metrics = await page.evaluate(() => ({
|
|
96
|
+
visualViewportWidth: window.visualViewport?.width,
|
|
97
|
+
visualViewportHeight: window.visualViewport?.height,
|
|
98
|
+
innerWidth: window.innerWidth,
|
|
99
|
+
innerHeight: window.innerHeight,
|
|
100
|
+
documentElementClientWidth: document.documentElement?.clientWidth,
|
|
101
|
+
documentElementClientHeight: document.documentElement?.clientHeight
|
|
102
|
+
}));
|
|
103
|
+
const width = toPositiveNumber(metrics.visualViewportWidth) ?? toPositiveNumber(metrics.innerWidth) ?? toPositiveNumber(metrics.documentElementClientWidth);
|
|
104
|
+
const height = toPositiveNumber(metrics.visualViewportHeight) ?? toPositiveNumber(metrics.innerHeight) ?? toPositiveNumber(metrics.documentElementClientHeight);
|
|
105
|
+
return width && height ? { width, height } : null;
|
|
106
|
+
}
|
|
107
|
+
function screenshotState(screenshot, viewport) {
|
|
101
108
|
const dimensions = readPngDimensions(screenshot);
|
|
102
109
|
return {
|
|
103
110
|
screenshot,
|
|
@@ -110,6 +117,18 @@ async function takeViewportScreenshot(page) {
|
|
|
110
117
|
}
|
|
111
118
|
};
|
|
112
119
|
}
|
|
120
|
+
async function takeViewportScreenshot(page) {
|
|
121
|
+
const viewport = page.viewportSize() ?? await getViewportFromPage(page).catch(() => null);
|
|
122
|
+
if (!viewport) {
|
|
123
|
+
throw new Error("Viewport size not found");
|
|
124
|
+
}
|
|
125
|
+
const screenshot = await page.screenshot({
|
|
126
|
+
fullPage: false,
|
|
127
|
+
scale: "css",
|
|
128
|
+
timeout: 1e4
|
|
129
|
+
});
|
|
130
|
+
return screenshotState(screenshot, viewport);
|
|
131
|
+
}
|
|
113
132
|
async function executeBrowserAction(page, action, logger = defaultLogger) {
|
|
114
133
|
switch (action.type) {
|
|
115
134
|
case "click": {
|
|
@@ -247,9 +266,9 @@ async function executeRecoveryAgent(page, instruction, logger, model, maxSteps =
|
|
|
247
266
|
}
|
|
248
267
|
const log = logger ?? defaultLogger;
|
|
249
268
|
log.info("Executing vision-based recovery agent", { instruction });
|
|
250
|
-
let
|
|
269
|
+
let screenshotState2;
|
|
251
270
|
try {
|
|
252
|
-
|
|
271
|
+
screenshotState2 = await takeViewportScreenshot(page);
|
|
253
272
|
} catch (screenshotError) {
|
|
254
273
|
log.warn("Failed to take screenshot for recovery agent, skipping", {
|
|
255
274
|
screenshotError: screenshotError instanceof Error ? screenshotError.message : String(screenshotError)
|
|
@@ -258,7 +277,7 @@ async function executeRecoveryAgent(page, instruction, logger, model, maxSteps =
|
|
|
258
277
|
}
|
|
259
278
|
const steps = [];
|
|
260
279
|
for (let step = 1; step <= maxSteps; step++) {
|
|
261
|
-
const { screenshot, dimensions, scale } =
|
|
280
|
+
const { screenshot, dimensions, scale } = screenshotState2;
|
|
262
281
|
const { object: result } = await generateObject({
|
|
263
282
|
model,
|
|
264
283
|
schema: recoveryActionSchema,
|
|
@@ -305,7 +324,7 @@ Analyze the screenshot and decide what action to take. If the task is complete o
|
|
|
305
324
|
await executeBrowserAction(page, action, log);
|
|
306
325
|
await delay(2e3);
|
|
307
326
|
if (step < maxSteps) {
|
|
308
|
-
|
|
327
|
+
screenshotState2 = await takeViewportScreenshot(page);
|
|
309
328
|
}
|
|
310
329
|
}
|
|
311
330
|
log.info("Recovery agent execution completed");
|