libretto 0.6.24 → 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/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/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,
|
|
@@ -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 };
|