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,445 +1,128 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
ts: string;
|
|
16
|
-
pageId?: string;
|
|
17
|
-
method: string;
|
|
18
|
-
url: string;
|
|
19
|
-
resourceType?: string;
|
|
20
|
-
status: number | null;
|
|
21
|
-
statusText?: string | null;
|
|
22
|
-
contentType: string | null;
|
|
23
|
-
requestHeaders?: Record<string, string> | null;
|
|
24
|
-
responseHeaders?: Record<string, string> | null;
|
|
25
|
-
requestBodyPreview?: string | null;
|
|
26
|
-
requestBodyPath?: string | null;
|
|
27
|
-
requestBodyBytes?: number | null;
|
|
28
|
-
requestBodyTruncated?: boolean;
|
|
29
|
-
requestBodyOmittedReason?: string | null;
|
|
30
|
-
responseBodyPreview?: string | null;
|
|
31
|
-
responseBodyPath?: string | null;
|
|
32
|
-
responseBodyBytes?: number | null;
|
|
33
|
-
responseBodyTruncated?: boolean;
|
|
34
|
-
responseBodyOmittedReason?: string | null;
|
|
35
|
-
errorText?: string | null;
|
|
36
|
-
postData?: string;
|
|
37
|
-
responseBody?: string | null;
|
|
38
|
-
size?: number | null;
|
|
39
|
-
durationMs?: number | null;
|
|
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 type { SimpleCLICommandMeta, SimpleCLIMiddleware } from "affordance";
|
|
6
|
+
import { resolveHostedApiUrl } from "./auth-fetch.js";
|
|
7
|
+
|
|
8
|
+
const TELEMETRY_FILE_NAME = "telemetry.json";
|
|
9
|
+
const TELEMETRY_ENDPOINT_PATH = "/v1/telemetry/recordCliEvent";
|
|
10
|
+
const TELEMETRY_TIMEOUT_MS = 250;
|
|
11
|
+
|
|
12
|
+
type StoredTelemetryState = {
|
|
13
|
+
installId?: string;
|
|
14
|
+
enabled?: boolean;
|
|
40
15
|
};
|
|
41
16
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
method?: string;
|
|
48
|
-
pageId?: string;
|
|
49
|
-
} = {},
|
|
50
|
-
): NetworkLogEntry[] {
|
|
51
|
-
assertSessionStateExistsOrThrow(session);
|
|
52
|
-
const logPath = getSessionNetworkLogPath(session);
|
|
53
|
-
if (!existsSync(logPath)) return [];
|
|
54
|
-
|
|
55
|
-
const lines = readFileSync(logPath, "utf-8")
|
|
56
|
-
.trim()
|
|
57
|
-
.split("\n")
|
|
58
|
-
.filter(Boolean);
|
|
59
|
-
let entries: NetworkLogEntry[] = lines.map(
|
|
60
|
-
(line) => JSON.parse(line) as NetworkLogEntry,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
if (opts.method) {
|
|
64
|
-
const m = opts.method.toUpperCase();
|
|
65
|
-
entries = entries.filter((e) => e.method === m);
|
|
66
|
-
}
|
|
67
|
-
if (opts.filter) {
|
|
68
|
-
const re = new RegExp(opts.filter, "i");
|
|
69
|
-
entries = entries.filter((e) => re.test(e.url));
|
|
70
|
-
}
|
|
71
|
-
if (opts.pageId) {
|
|
72
|
-
entries = entries.filter((e) => e.pageId === opts.pageId);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const last = opts.last ?? 20;
|
|
76
|
-
if (entries.length > last) {
|
|
77
|
-
entries = entries.slice(-last);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return entries;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export type ActionLogEntry = {
|
|
84
|
-
ts: string;
|
|
85
|
-
pageId?: string;
|
|
86
|
-
action: string;
|
|
87
|
-
source: "user" | "agent";
|
|
88
|
-
selector?: string;
|
|
89
|
-
bestSemanticSelector?: string;
|
|
90
|
-
targetSelector?: string;
|
|
91
|
-
ancestorSelectors?: string[];
|
|
92
|
-
nearbyText?: string;
|
|
93
|
-
composedPath?: string[];
|
|
94
|
-
coordinates?: {
|
|
95
|
-
x: number;
|
|
96
|
-
y: number;
|
|
97
|
-
};
|
|
98
|
-
value?: string;
|
|
99
|
-
url?: string;
|
|
100
|
-
duration?: number;
|
|
101
|
-
success: boolean;
|
|
102
|
-
error?: string;
|
|
17
|
+
type CliTelemetryPayload = {
|
|
18
|
+
installId: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
event: string;
|
|
21
|
+
error: boolean;
|
|
103
22
|
};
|
|
104
23
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
entry: Record<string, unknown>,
|
|
108
|
-
): void {
|
|
109
|
-
try {
|
|
110
|
-
const record = { ts: new Date().toISOString(), ...entry };
|
|
111
|
-
appendFileSync(
|
|
112
|
-
getSessionActionsLogPath(session),
|
|
113
|
-
JSON.stringify(record) + "\n",
|
|
114
|
-
);
|
|
115
|
-
} catch {}
|
|
24
|
+
function telemetryDir(): string {
|
|
25
|
+
return join(homedir(), ".libretto");
|
|
116
26
|
}
|
|
117
27
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
opts: {
|
|
121
|
-
last?: number;
|
|
122
|
-
filter?: string;
|
|
123
|
-
action?: string;
|
|
124
|
-
source?: string;
|
|
125
|
-
pageId?: string;
|
|
126
|
-
} = {},
|
|
127
|
-
): ActionLogEntry[] {
|
|
128
|
-
assertSessionStateExistsOrThrow(session);
|
|
129
|
-
const logPath = getSessionActionsLogPath(session);
|
|
130
|
-
if (!existsSync(logPath)) return [];
|
|
131
|
-
|
|
132
|
-
const lines = readFileSync(logPath, "utf-8")
|
|
133
|
-
.trim()
|
|
134
|
-
.split("\n")
|
|
135
|
-
.filter(Boolean);
|
|
136
|
-
let entries: ActionLogEntry[] = lines.map(
|
|
137
|
-
(line) => JSON.parse(line) as ActionLogEntry,
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
if (opts.action) {
|
|
141
|
-
const a = opts.action.toLowerCase();
|
|
142
|
-
entries = entries.filter((e) => e.action === a);
|
|
143
|
-
}
|
|
144
|
-
if (opts.source) {
|
|
145
|
-
const s = opts.source.toLowerCase();
|
|
146
|
-
entries = entries.filter((e) => e.source === s);
|
|
147
|
-
}
|
|
148
|
-
if (opts.filter) {
|
|
149
|
-
const re = new RegExp(opts.filter, "i");
|
|
150
|
-
entries = entries.filter(
|
|
151
|
-
(e) =>
|
|
152
|
-
re.test(e.action) ||
|
|
153
|
-
re.test(e.selector || "") ||
|
|
154
|
-
re.test(e.bestSemanticSelector || "") ||
|
|
155
|
-
re.test(e.targetSelector || "") ||
|
|
156
|
-
re.test((e.ancestorSelectors || []).join(" ")) ||
|
|
157
|
-
re.test(e.nearbyText || "") ||
|
|
158
|
-
re.test((e.composedPath || []).join(" ")) ||
|
|
159
|
-
re.test(e.value || "") ||
|
|
160
|
-
re.test(e.url || ""),
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
if (opts.pageId) {
|
|
164
|
-
entries = entries.filter((e) => e.pageId === opts.pageId);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const last = opts.last ?? 20;
|
|
168
|
-
if (entries.length > last) {
|
|
169
|
-
entries = entries.slice(-last);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return entries;
|
|
28
|
+
function telemetryPath(): string {
|
|
29
|
+
return join(telemetryDir(), TELEMETRY_FILE_NAME);
|
|
173
30
|
}
|
|
174
31
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
"check",
|
|
182
|
-
"uncheck",
|
|
183
|
-
"selectOption",
|
|
184
|
-
"hover",
|
|
185
|
-
"focus",
|
|
186
|
-
"scrollIntoViewIfNeeded",
|
|
187
|
-
"waitFor",
|
|
188
|
-
"innerHTML",
|
|
189
|
-
"innerText",
|
|
190
|
-
"textContent",
|
|
191
|
-
"inputValue",
|
|
192
|
-
"isChecked",
|
|
193
|
-
"isDisabled",
|
|
194
|
-
"isEditable",
|
|
195
|
-
"isEnabled",
|
|
196
|
-
"isHidden",
|
|
197
|
-
"isVisible",
|
|
198
|
-
"count",
|
|
199
|
-
"boundingBox",
|
|
200
|
-
"screenshot",
|
|
201
|
-
"evaluate",
|
|
202
|
-
"evaluateAll",
|
|
203
|
-
"evaluateHandle",
|
|
204
|
-
"getAttribute",
|
|
205
|
-
"dispatchEvent",
|
|
206
|
-
"setInputFiles",
|
|
207
|
-
"selectText",
|
|
208
|
-
"dragTo",
|
|
209
|
-
"highlight",
|
|
210
|
-
"tap",
|
|
211
|
-
] as const;
|
|
212
|
-
|
|
213
|
-
const LOCATOR_RETURNING_METHODS = [
|
|
214
|
-
"first",
|
|
215
|
-
"last",
|
|
216
|
-
"locator",
|
|
217
|
-
"getByRole",
|
|
218
|
-
"getByText",
|
|
219
|
-
"getByLabel",
|
|
220
|
-
"getByPlaceholder",
|
|
221
|
-
"getByAltText",
|
|
222
|
-
"getByTitle",
|
|
223
|
-
"getByTestId",
|
|
224
|
-
"filter",
|
|
225
|
-
"and",
|
|
226
|
-
"or",
|
|
227
|
-
] as const;
|
|
228
|
-
|
|
229
|
-
function formatHint(method: string, args: any[]): string {
|
|
230
|
-
const formatted = args.map((a: any) => JSON.stringify(a)).join(", ");
|
|
231
|
-
return `${method}(${formatted})`;
|
|
32
|
+
function isTelemetryDisabled(): boolean {
|
|
33
|
+
return (
|
|
34
|
+
process.env.LIBRETTO_TELEMETRY_DISABLED === "1" ||
|
|
35
|
+
process.env.DO_NOT_TRACK === "1" ||
|
|
36
|
+
process.env.CI === "1"
|
|
37
|
+
);
|
|
232
38
|
}
|
|
233
39
|
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
): any {
|
|
242
|
-
if (locator.__librettoActionLogged) return locator;
|
|
243
|
-
locator.__librettoActionLogged = true;
|
|
244
|
-
|
|
245
|
-
for (const actMethod of LOCATOR_ACTION_METHODS) {
|
|
246
|
-
if (typeof locator[actMethod] !== "function") continue;
|
|
247
|
-
const origAct = locator[actMethod].bind(locator);
|
|
248
|
-
locator[actMethod] = async (...actArgs: any[]) => {
|
|
249
|
-
const start = Date.now();
|
|
250
|
-
try {
|
|
251
|
-
await page.evaluate(() => {
|
|
252
|
-
(window as any).__btApiActionInProgress = true;
|
|
253
|
-
});
|
|
254
|
-
} catch {}
|
|
255
|
-
try {
|
|
256
|
-
const result = await origAct(...actArgs);
|
|
257
|
-
parentLogAction(session, {
|
|
258
|
-
pageId,
|
|
259
|
-
action: actMethod,
|
|
260
|
-
source: "agent",
|
|
261
|
-
selector: hint,
|
|
262
|
-
value:
|
|
263
|
-
actArgs[0] !== undefined
|
|
264
|
-
? String(actArgs[0]).slice(0, 100)
|
|
265
|
-
: undefined,
|
|
266
|
-
duration: Date.now() - start,
|
|
267
|
-
success: true,
|
|
268
|
-
});
|
|
269
|
-
onActivity?.();
|
|
270
|
-
return result;
|
|
271
|
-
} catch (err: any) {
|
|
272
|
-
parentLogAction(session, {
|
|
273
|
-
pageId,
|
|
274
|
-
action: actMethod,
|
|
275
|
-
source: "agent",
|
|
276
|
-
selector: hint,
|
|
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 as any).__btApiActionInProgress = false;
|
|
287
|
-
});
|
|
288
|
-
} catch {}
|
|
289
|
-
}
|
|
290
|
-
};
|
|
40
|
+
async function readTelemetryState(): Promise<StoredTelemetryState | null> {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await fs.readFile(telemetryPath(), "utf8");
|
|
43
|
+
return JSON.parse(raw) as Partial<StoredTelemetryState>;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
46
|
+
return null;
|
|
291
47
|
}
|
|
48
|
+
}
|
|
292
49
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
locator[method] = (...args: any[]) => {
|
|
297
|
-
const child = origMethod(...args);
|
|
298
|
-
const childHint =
|
|
299
|
-
args.length > 0
|
|
300
|
-
? `${hint}.${formatHint(method, args)}`
|
|
301
|
-
: `${hint}.${method}()`;
|
|
302
|
-
return wrapLocator(child, childHint, session, page, pageId, onActivity);
|
|
303
|
-
};
|
|
304
|
-
}
|
|
50
|
+
async function readOrCreateInstallId(): Promise<string | null> {
|
|
51
|
+
const state = await readTelemetryState();
|
|
52
|
+
if (state?.enabled === false) return null;
|
|
305
53
|
|
|
306
|
-
if (typeof
|
|
307
|
-
|
|
308
|
-
locator.nth = (index: number) => {
|
|
309
|
-
const child = origNth(index);
|
|
310
|
-
const childHint = `${hint}.nth(${index})`;
|
|
311
|
-
return wrapLocator(child, childHint, session, page, pageId, onActivity);
|
|
312
|
-
};
|
|
54
|
+
if (typeof state?.installId === "string" && state.installId.length > 0) {
|
|
55
|
+
return state.installId;
|
|
313
56
|
}
|
|
314
57
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const childHint = `${hint}.all()[${i}]`;
|
|
321
|
-
return wrapLocator(item, childHint, session, page, pageId, onActivity);
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
}
|
|
58
|
+
const installId = randomUUID();
|
|
59
|
+
writeTelemetryNotice();
|
|
60
|
+
await writeTelemetryState({ installId, enabled: true });
|
|
61
|
+
return installId;
|
|
62
|
+
}
|
|
325
63
|
|
|
326
|
-
|
|
64
|
+
function writeTelemetryNotice(): void {
|
|
65
|
+
if (!process.stderr.isTTY) return;
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
[
|
|
68
|
+
"Libretto collects anonymous CLI telemetry: install id, timestamp, command event, and error status only.",
|
|
69
|
+
"Set LIBRETTO_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 to disable it, or set enabled:false in ~/.libretto/telemetry.json.",
|
|
70
|
+
].join(" ") + "\n",
|
|
71
|
+
);
|
|
327
72
|
}
|
|
328
73
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
"click",
|
|
337
|
-
"dblclick",
|
|
338
|
-
"fill",
|
|
339
|
-
"type",
|
|
340
|
-
"press",
|
|
341
|
-
"check",
|
|
342
|
-
"uncheck",
|
|
343
|
-
"selectOption",
|
|
344
|
-
"hover",
|
|
345
|
-
"focus",
|
|
346
|
-
] as const;
|
|
347
|
-
const NAV_ACTIONS = ["goto", "reload", "goBack", "goForward"] as const;
|
|
74
|
+
async function writeTelemetryState(state: StoredTelemetryState): Promise<void> {
|
|
75
|
+
await fs.mkdir(telemetryDir(), { recursive: true, mode: 0o700 });
|
|
76
|
+
const target = telemetryPath();
|
|
77
|
+
const tmp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
78
|
+
await fs.writeFile(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
79
|
+
await fs.rename(tmp, target);
|
|
80
|
+
}
|
|
348
81
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
selector: typeof args[0] === "string" ? args[0] : undefined,
|
|
365
|
-
value:
|
|
366
|
-
args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
|
|
367
|
-
duration: Date.now() - start,
|
|
368
|
-
success: true,
|
|
369
|
-
});
|
|
370
|
-
onActivity?.();
|
|
371
|
-
return result;
|
|
372
|
-
} catch (err: any) {
|
|
373
|
-
parentLogAction(session, {
|
|
374
|
-
pageId,
|
|
375
|
-
action: method,
|
|
376
|
-
source: "agent",
|
|
377
|
-
selector: typeof args[0] === "string" ? args[0] : undefined,
|
|
378
|
-
duration: Date.now() - start,
|
|
379
|
-
success: false,
|
|
380
|
-
error: err.message,
|
|
381
|
-
});
|
|
382
|
-
onActivity?.();
|
|
383
|
-
throw err;
|
|
384
|
-
} finally {
|
|
385
|
-
try {
|
|
386
|
-
await page.evaluate(() => {
|
|
387
|
-
(window as any).__btApiActionInProgress = false;
|
|
388
|
-
});
|
|
389
|
-
} catch {}
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
}
|
|
82
|
+
async function recordCliTelemetryEvent(
|
|
83
|
+
command: SimpleCLICommandMeta,
|
|
84
|
+
error: boolean,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
if (isTelemetryDisabled()) return;
|
|
87
|
+
const installId = await readOrCreateInstallId();
|
|
88
|
+
if (!installId) return;
|
|
89
|
+
|
|
90
|
+
await sendWithTimeout({
|
|
91
|
+
installId,
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
event: `libretto ${command.path.join(" ")}`,
|
|
94
|
+
error,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
393
97
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
onActivity?.();
|
|
409
|
-
return result;
|
|
410
|
-
} catch (err: any) {
|
|
411
|
-
parentLogAction(session, {
|
|
412
|
-
pageId,
|
|
413
|
-
action: method,
|
|
414
|
-
source: "agent",
|
|
415
|
-
url: typeof args[0] === "string" ? args[0] : undefined,
|
|
416
|
-
duration: Date.now() - start,
|
|
417
|
-
success: false,
|
|
418
|
-
error: err.message,
|
|
419
|
-
});
|
|
420
|
-
onActivity?.();
|
|
421
|
-
throw err;
|
|
422
|
-
}
|
|
423
|
-
};
|
|
98
|
+
async function sendWithTimeout(payload: CliTelemetryPayload): Promise<void> {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
101
|
+
try {
|
|
102
|
+
await fetch(`${resolveHostedApiUrl()}${TELEMETRY_ENDPOINT_PATH}`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({ json: payload }),
|
|
108
|
+
signal: controller.signal,
|
|
109
|
+
});
|
|
110
|
+
} finally {
|
|
111
|
+
clearTimeout(timeout);
|
|
424
112
|
}
|
|
113
|
+
}
|
|
425
114
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const orig = (page as any)[factory].bind(page);
|
|
439
|
-
(page as any)[factory] = (...factoryArgs: any[]) => {
|
|
440
|
-
const locator = orig(...factoryArgs);
|
|
441
|
-
const hint = formatHint(factory, factoryArgs);
|
|
442
|
-
return wrapLocator(locator, hint, session, page, pageId, onActivity);
|
|
443
|
-
};
|
|
115
|
+
export const telemetryMiddleware: SimpleCLIMiddleware<
|
|
116
|
+
unknown,
|
|
117
|
+
{},
|
|
118
|
+
{}
|
|
119
|
+
> = async ({ command, next }) => {
|
|
120
|
+
try {
|
|
121
|
+
const result = await next();
|
|
122
|
+
await recordCliTelemetryEvent(command, false).catch(() => {});
|
|
123
|
+
return result;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
await recordCliTelemetryEvent(command, true).catch(() => {});
|
|
126
|
+
throw error;
|
|
444
127
|
}
|
|
445
|
-
}
|
|
128
|
+
};
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import type { BrowserContext, Page } from "playwright";
|
|
1
|
+
import type { BrowserContext, Frame, Page } from "playwright";
|
|
2
2
|
import type { LoggerApi } from "../../../shared/logger/index.js";
|
|
3
3
|
import { installPauseHandler } from "../../../shared/debug/pause-handler.js";
|
|
4
|
+
import {
|
|
5
|
+
mergeAuthProfileStorageState,
|
|
6
|
+
normalizeAuthProfileSite,
|
|
7
|
+
} from "../../../shared/workflow/auth-profile-state.js";
|
|
4
8
|
import type {
|
|
5
9
|
ExportedLibrettoWorkflow,
|
|
6
10
|
LibrettoWorkflowContext,
|
|
7
11
|
} from "../../../shared/workflow/workflow.js";
|
|
12
|
+
import { readProfile, writeProfile } from "../profiles.js";
|
|
8
13
|
import {
|
|
9
14
|
getAbsoluteIntegrationPath,
|
|
10
15
|
installHeadedWorkflowVisualization,
|
|
@@ -44,6 +49,7 @@ export type WorkflowControllerConfig = {
|
|
|
44
49
|
page: Page;
|
|
45
50
|
context: BrowserContext;
|
|
46
51
|
logger: LoggerApi;
|
|
52
|
+
refreshLocalAuthProfiles?: boolean;
|
|
47
53
|
onLog?: (event: WorkflowLogEvent) => void;
|
|
48
54
|
onOutcome?: (outcome: WorkflowOutcome) => void;
|
|
49
55
|
};
|
|
@@ -155,6 +161,7 @@ export class WorkflowController {
|
|
|
155
161
|
session: this.config.session,
|
|
156
162
|
page: this.config.page,
|
|
157
163
|
};
|
|
164
|
+
const visitedSites = createVisitedSiteTracker(this.config.context);
|
|
158
165
|
|
|
159
166
|
const uninstallPauseHandler = installPauseHandler((pauseArgs) =>
|
|
160
167
|
this.pause({
|
|
@@ -164,6 +171,12 @@ export class WorkflowController {
|
|
|
164
171
|
);
|
|
165
172
|
try {
|
|
166
173
|
await workflow.run(workflowContext, workflowConfig.params ?? {});
|
|
174
|
+
await refreshLocalAuthProfileIfEnabled({
|
|
175
|
+
context: this.config.context,
|
|
176
|
+
enabled: this.config.refreshLocalAuthProfiles === true,
|
|
177
|
+
sites: visitedSites.sites(),
|
|
178
|
+
workflow,
|
|
179
|
+
});
|
|
167
180
|
} catch (error) {
|
|
168
181
|
this.emitOutcome({
|
|
169
182
|
state: "finished",
|
|
@@ -174,6 +187,7 @@ export class WorkflowController {
|
|
|
174
187
|
return;
|
|
175
188
|
} finally {
|
|
176
189
|
uninstallPauseHandler();
|
|
190
|
+
visitedSites.dispose();
|
|
177
191
|
}
|
|
178
192
|
|
|
179
193
|
this.emitOutcome({
|
|
@@ -232,6 +246,77 @@ export class WorkflowController {
|
|
|
232
246
|
}
|
|
233
247
|
}
|
|
234
248
|
|
|
249
|
+
async function refreshLocalAuthProfileIfEnabled(
|
|
250
|
+
args: {
|
|
251
|
+
context: BrowserContext;
|
|
252
|
+
enabled: boolean;
|
|
253
|
+
sites: readonly string[];
|
|
254
|
+
workflow: ExportedLibrettoWorkflow;
|
|
255
|
+
},
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
const { context, enabled, sites, workflow } = args;
|
|
258
|
+
if (!workflow.authProfileName || workflow.authProfileRefresh !== true) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!enabled) return;
|
|
262
|
+
if (sites.length === 0) {
|
|
263
|
+
console.warn(
|
|
264
|
+
`Auth profile refresh skipped for "${workflow.authProfileName}": workflow did not visit any http(s) sites.`,
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const existing = readProfile(workflow.authProfileName);
|
|
269
|
+
const latest = await context.storageState({ indexedDB: true });
|
|
270
|
+
const state = mergeAuthProfileStorageState(existing, latest, sites);
|
|
271
|
+
await writeProfile(workflow.authProfileName, state);
|
|
272
|
+
console.warn(`Auth profile refreshed: ${workflow.authProfileName}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function createVisitedSiteTracker(context: BrowserContext): {
|
|
276
|
+
sites: () => string[];
|
|
277
|
+
dispose: () => void;
|
|
278
|
+
} {
|
|
279
|
+
const sites = new Set<string>();
|
|
280
|
+
const pageListeners = new Map<Page, (frame: Frame) => void>();
|
|
281
|
+
|
|
282
|
+
const recordUrl = (url: string): void => {
|
|
283
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) return;
|
|
284
|
+
const site = normalizeAuthProfileSite(url);
|
|
285
|
+
if (site) sites.add(site);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const trackPage = (page: Page): void => {
|
|
289
|
+
if (pageListeners.has(page)) return;
|
|
290
|
+
recordUrl(page.url());
|
|
291
|
+
|
|
292
|
+
const onFrameNavigated = (frame: Frame): void => {
|
|
293
|
+
if (frame === page.mainFrame()) {
|
|
294
|
+
recordUrl(frame.url());
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
pageListeners.set(page, onFrameNavigated);
|
|
299
|
+
page.on("framenavigated", onFrameNavigated);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
for (const page of context.pages()) {
|
|
303
|
+
trackPage(page);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
context.on("page", trackPage);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
sites: () => [...sites],
|
|
310
|
+
dispose: () => {
|
|
311
|
+
context.off("page", trackPage);
|
|
312
|
+
for (const [page, listener] of pageListeners) {
|
|
313
|
+
page.off("framenavigated", listener);
|
|
314
|
+
}
|
|
315
|
+
pageListeners.clear();
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
235
320
|
function chunkToString(chunk: unknown): string {
|
|
236
321
|
return Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
237
322
|
}
|