libretto 0.6.24 → 0.6.26
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 +110 -311
- 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/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 +142 -413
- 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/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,124 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "node:
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
const logPath = getSessionNetworkLogPath(session);
|
|
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 { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { resolveHostedApiUrl } from "./auth-fetch.js";
|
|
8
|
+
const TELEMETRY_FILE_NAME = "telemetry.json";
|
|
9
|
+
const TELEMETRY_ENDPOINT_PATH = "/v1/telemetry/recordCliEvent";
|
|
10
|
+
const TELEMETRY_TIMEOUT_MS = 250;
|
|
11
|
+
function packageRoot() {
|
|
12
|
+
return join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
35
13
|
}
|
|
36
|
-
function
|
|
14
|
+
function readPackageVersion() {
|
|
37
15
|
try {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
getSessionActionsLogPath(session),
|
|
41
|
-
JSON.stringify(record) + "\n"
|
|
16
|
+
const parsed = JSON.parse(
|
|
17
|
+
readFileSync(join(packageRoot(), "package.json"), "utf8")
|
|
42
18
|
);
|
|
19
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
43
20
|
} catch {
|
|
21
|
+
return "unknown";
|
|
44
22
|
}
|
|
45
23
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
54
|
-
|
|
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;
|
|
24
|
+
function resolveBuildChannel() {
|
|
25
|
+
const root = packageRoot();
|
|
26
|
+
const workspaceRoot = join(root, "../..");
|
|
27
|
+
if (basename(dirname(root)) === "packages" && existsSync(join(workspaceRoot, "pnpm-workspace.yaml"))) {
|
|
28
|
+
return "source";
|
|
29
|
+
}
|
|
30
|
+
const pathSegments = root.split(/[\\/]+/);
|
|
31
|
+
if (pathSegments.includes("node_modules")) return "node_modules";
|
|
32
|
+
return "unknown";
|
|
76
33
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
"type",
|
|
82
|
-
"press",
|
|
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})`;
|
|
34
|
+
const packageVersion = readPackageVersion();
|
|
35
|
+
const buildChannel = resolveBuildChannel();
|
|
36
|
+
function telemetryDir() {
|
|
37
|
+
return join(homedir(), ".libretto");
|
|
132
38
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
locator.__librettoActionLogged = true;
|
|
136
|
-
for (const actMethod of LOCATOR_ACTION_METHODS) {
|
|
137
|
-
if (typeof locator[actMethod] !== "function") continue;
|
|
138
|
-
const origAct = locator[actMethod].bind(locator);
|
|
139
|
-
locator[actMethod] = async (...actArgs) => {
|
|
140
|
-
const start = Date.now();
|
|
141
|
-
try {
|
|
142
|
-
await page.evaluate(() => {
|
|
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;
|
|
39
|
+
function telemetryPath() {
|
|
40
|
+
return join(telemetryDir(), TELEMETRY_FILE_NAME);
|
|
210
41
|
}
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
"
|
|
220
|
-
|
|
221
|
-
"hover",
|
|
222
|
-
"focus"
|
|
223
|
-
];
|
|
224
|
-
const NAV_ACTIONS = ["goto", "reload", "goBack", "goForward"];
|
|
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
|
-
};
|
|
42
|
+
function isTelemetryDisabled() {
|
|
43
|
+
return process.env.LIBRETTO_TELEMETRY_DISABLED === "1" || process.env.DO_NOT_TRACK === "1" || process.env.CI === "1";
|
|
44
|
+
}
|
|
45
|
+
async function readTelemetryState() {
|
|
46
|
+
try {
|
|
47
|
+
const raw = await fs.readFile(telemetryPath(), "utf8");
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code !== "ENOENT") throw error;
|
|
51
|
+
return null;
|
|
300
52
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
53
|
+
}
|
|
54
|
+
async function readOrCreateInstallId() {
|
|
55
|
+
const state = await readTelemetryState();
|
|
56
|
+
if (state?.enabled === false) return null;
|
|
57
|
+
if (typeof state?.installId === "string" && state.installId.length > 0) {
|
|
58
|
+
return state.installId;
|
|
59
|
+
}
|
|
60
|
+
const installId = randomUUID();
|
|
61
|
+
writeTelemetryNotice();
|
|
62
|
+
await writeTelemetryState({ installId, enabled: true });
|
|
63
|
+
return installId;
|
|
64
|
+
}
|
|
65
|
+
function writeTelemetryNotice() {
|
|
66
|
+
if (!process.stderr.isTTY) return;
|
|
67
|
+
process.stderr.write(
|
|
68
|
+
[
|
|
69
|
+
"Libretto collects anonymous CLI telemetry: install id, timestamp, command event, error status, package version, and build channel only.",
|
|
70
|
+
"Set LIBRETTO_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 to disable it, or set enabled:false in ~/.libretto/telemetry.json."
|
|
71
|
+
].join(" ") + "\n"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
async function writeTelemetryState(state) {
|
|
75
|
+
await fs.mkdir(telemetryDir(), { recursive: true, mode: 448 });
|
|
76
|
+
const target = telemetryPath();
|
|
77
|
+
const tmp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
78
|
+
await fs.writeFile(tmp, JSON.stringify(state, null, 2), { mode: 384 });
|
|
79
|
+
await fs.rename(tmp, target);
|
|
80
|
+
}
|
|
81
|
+
async function recordCliTelemetryEvent(command, error) {
|
|
82
|
+
if (isTelemetryDisabled()) return;
|
|
83
|
+
const installId = await readOrCreateInstallId();
|
|
84
|
+
if (!installId) return;
|
|
85
|
+
await sendWithTimeout({
|
|
86
|
+
installId,
|
|
87
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
88
|
+
event: `libretto ${command.path.join(" ")}`,
|
|
89
|
+
error,
|
|
90
|
+
packageVersion,
|
|
91
|
+
buildChannel
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function sendWithTimeout(payload) {
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
97
|
+
try {
|
|
98
|
+
await fetch(`${resolveHostedApiUrl()}${TELEMETRY_ENDPOINT_PATH}`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Type": "application/json"
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({ json: payload }),
|
|
104
|
+
signal: controller.signal
|
|
105
|
+
});
|
|
106
|
+
} finally {
|
|
107
|
+
clearTimeout(timeout);
|
|
318
108
|
}
|
|
319
109
|
}
|
|
110
|
+
const telemetryMiddleware = async ({ command, next }) => {
|
|
111
|
+
try {
|
|
112
|
+
const result = await next();
|
|
113
|
+
await recordCliTelemetryEvent(command, false).catch(() => {
|
|
114
|
+
});
|
|
115
|
+
return result;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
await recordCliTelemetryEvent(command, true).catch(() => {
|
|
118
|
+
});
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
320
122
|
export {
|
|
321
|
-
|
|
322
|
-
readActionLog,
|
|
323
|
-
readNetworkLog,
|
|
324
|
-
wrapPageForActionLogging
|
|
123
|
+
telemetryMiddleware
|
|
325
124
|
};
|
|
@@ -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,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function normalizeProfileName(name) {
|
|
2
|
+
const trimmed = name.trim();
|
|
3
|
+
if (!trimmed) {
|
|
4
|
+
throw new Error("Profile name is required.");
|
|
5
|
+
}
|
|
6
|
+
if (!isValidProfileName(trimmed)) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`Invalid profile name "${name}". Use letters, numbers, dots, underscores, and dashes only.`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return trimmed;
|
|
12
|
+
}
|
|
13
|
+
function isValidProfileName(name) {
|
|
14
|
+
if (name === "." || name === "..") return false;
|
|
15
|
+
for (let index = 0; index < name.length; index += 1) {
|
|
16
|
+
const code = name.charCodeAt(index);
|
|
17
|
+
const isUppercase = code >= 65 && code <= 90;
|
|
18
|
+
const isLowercase = code >= 97 && code <= 122;
|
|
19
|
+
const isDigit = code >= 48 && code <= 57;
|
|
20
|
+
const isAllowedPunctuation = code === 45 || code === 46 || code === 95;
|
|
21
|
+
if (!isUppercase && !isLowercase && !isDigit && !isAllowedPunctuation) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
normalizeProfileName
|
|
29
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BrowserContext } from 'playwright';
|
|
2
|
+
|
|
3
|
+
type AuthProfileStorageState = {
|
|
4
|
+
sites?: string[];
|
|
5
|
+
cookies?: unknown[];
|
|
6
|
+
origins?: Array<{
|
|
7
|
+
origin: string;
|
|
8
|
+
localStorage: Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}>;
|
|
12
|
+
indexedDB?: unknown;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
declare function parseAuthProfileSites(value: string): string[];
|
|
16
|
+
declare function normalizeAuthProfileSite(value: string): string | null;
|
|
17
|
+
declare function captureAuthProfileStorageState(context: BrowserContext, sites: readonly string[]): Promise<AuthProfileStorageState>;
|
|
18
|
+
declare function mergeAuthProfileStorageState(existing: AuthProfileStorageState, latest: AuthProfileStorageState, sites: readonly string[]): AuthProfileStorageState;
|
|
19
|
+
|
|
20
|
+
export { type AuthProfileStorageState, captureAuthProfileStorageState, mergeAuthProfileStorageState, normalizeAuthProfileSite, parseAuthProfileSites };
|