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.
Files changed (63) hide show
  1. package/README.md +9 -1
  2. package/README.template.md +9 -1
  3. package/dist/cli/commands/browser.js +17 -10
  4. package/dist/cli/commands/cloud-credentials.js +70 -0
  5. package/dist/cli/commands/deploy.js +24 -2
  6. package/dist/cli/commands/execution.js +9 -30
  7. package/dist/cli/commands/import-chrome-profiles.js +46 -0
  8. package/dist/cli/commands/profiles.js +71 -0
  9. package/dist/cli/commands/shared.js +1 -3
  10. package/dist/cli/core/browser.js +89 -75
  11. package/dist/cli/core/daemon/daemon.js +47 -35
  12. package/dist/cli/core/daemon/ipc.js +3 -0
  13. package/dist/cli/core/deploy-artifact.js +85 -22
  14. package/dist/cli/core/profiles.js +47 -0
  15. package/dist/cli/core/prompt.js +9 -0
  16. package/dist/cli/core/providers/libretto-cloud.js +6 -2
  17. package/dist/cli/core/session-logs.js +325 -0
  18. package/dist/cli/core/telemetry.js +110 -311
  19. package/dist/cli/core/workflow-runner/runner.js +65 -0
  20. package/dist/cli/router.js +9 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +12 -0
  23. package/dist/shared/workflow/auth-profile-name.d.ts +3 -0
  24. package/dist/shared/workflow/auth-profile-name.js +29 -0
  25. package/dist/shared/workflow/auth-profile-state.d.ts +20 -0
  26. package/dist/shared/workflow/auth-profile-state.js +105 -0
  27. package/dist/shared/workflow/authenticate.d.ts +17 -0
  28. package/dist/shared/workflow/authenticate.js +37 -0
  29. package/dist/shared/workflow/credentials.d.ts +5 -0
  30. package/dist/shared/workflow/credentials.js +68 -0
  31. package/dist/shared/workflow/workflow.d.ts +16 -1
  32. package/dist/shared/workflow/workflow.js +56 -4
  33. package/package.json +1 -1
  34. package/skills/libretto/SKILL.md +3 -4
  35. package/skills/libretto/references/auth-profiles.md +61 -11
  36. package/skills/libretto/references/code-generation-rules.md +31 -1
  37. package/skills/libretto-readonly/SKILL.md +1 -1
  38. package/src/cli/commands/browser.ts +19 -11
  39. package/src/cli/commands/cloud-credentials.ts +82 -0
  40. package/src/cli/commands/deploy.ts +41 -2
  41. package/src/cli/commands/execution.ts +10 -31
  42. package/src/cli/commands/import-chrome-profiles.ts +46 -0
  43. package/src/cli/commands/profiles.ts +90 -0
  44. package/src/cli/commands/shared.ts +4 -8
  45. package/src/cli/core/browser.ts +102 -91
  46. package/src/cli/core/daemon/config.ts +4 -1
  47. package/src/cli/core/daemon/daemon.ts +52 -44
  48. package/src/cli/core/daemon/ipc.ts +15 -0
  49. package/src/cli/core/deploy-artifact.ts +131 -32
  50. package/src/cli/core/profiles.ts +53 -0
  51. package/src/cli/core/prompt.ts +15 -0
  52. package/src/cli/core/providers/libretto-cloud.ts +6 -2
  53. package/src/cli/core/providers/types.ts +4 -1
  54. package/src/cli/core/session-logs.ts +445 -0
  55. package/src/cli/core/telemetry.ts +142 -413
  56. package/src/cli/core/workflow-runner/runner.ts +86 -1
  57. package/src/cli/router.ts +8 -0
  58. package/src/index.ts +10 -0
  59. package/src/shared/workflow/auth-profile-name.ts +27 -0
  60. package/src/shared/workflow/auth-profile-state.ts +144 -0
  61. package/src/shared/workflow/authenticate.ts +63 -0
  62. package/src/shared/workflow/credentials.ts +91 -0
  63. package/src/shared/workflow/workflow.ts +89 -4
@@ -1,325 +1,124 @@
1
- import {
2
- appendFileSync,
3
- existsSync,
4
- readFileSync
5
- } from "node:fs";
6
- import {
7
- getSessionActionsLogPath,
8
- getSessionNetworkLogPath
9
- } from "./context.js";
10
- import { assertSessionStateExistsOrThrow } from "./session.js";
11
- function readNetworkLog(session, opts = {}) {
12
- assertSessionStateExistsOrThrow(session);
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 parentLogAction(session, entry) {
14
+ function readPackageVersion() {
37
15
  try {
38
- const record = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
39
- appendFileSync(
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 readActionLog(session, opts = {}) {
47
- assertSessionStateExistsOrThrow(session);
48
- const logPath = getSessionActionsLogPath(session);
49
- if (!existsSync(logPath)) return [];
50
- const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
51
- let entries = lines.map(
52
- (line) => JSON.parse(line)
53
- );
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;
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 LOCATOR_ACTION_METHODS = [
78
- "click",
79
- "dblclick",
80
- "fill",
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 wrapLocator(locator, hint, session, page, pageId, onActivity) {
134
- if (locator.__librettoActionLogged) return locator;
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 wrapPageForActionLogging(page, session, pageId, onActivity) {
212
- const PAGE_ACTIONS = [
213
- "click",
214
- "dblclick",
215
- "fill",
216
- "type",
217
- "press",
218
- "check",
219
- "uncheck",
220
- "selectOption",
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
- 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
- };
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
- parentLogAction,
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
  }
@@ -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,3 @@
1
+ declare function normalizeProfileName(name: string): string;
2
+
3
+ export { normalizeProfileName };
@@ -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 };