libretto 0.6.16 → 0.6.17

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 (40) hide show
  1. package/dist/cli/cli.js +32 -13
  2. package/dist/cli/commands/browser.js +2 -2
  3. package/dist/cli/commands/execution.js +1 -1
  4. package/dist/cli/commands/search.js +69 -0
  5. package/dist/cli/commands/update.js +122 -0
  6. package/dist/cli/core/context.js +4 -0
  7. package/dist/cli/core/daemon/daemon.js +3 -0
  8. package/dist/cli/core/experiments.js +14 -1
  9. package/dist/cli/core/providers/index.js +5 -1
  10. package/dist/cli/core/providers/steel.js +56 -0
  11. package/dist/cli/core/session-telemetry.js +143 -7
  12. package/dist/cli/core/skill-version.js +1 -0
  13. package/dist/cli/router.js +14 -3
  14. package/dist/shared/html-search/search-html.d.ts +9 -0
  15. package/dist/shared/html-search/search-html.js +46 -0
  16. package/dist/shared/html-search/search-html.spec.d.ts +2 -0
  17. package/dist/shared/html-search/search-html.spec.js +57 -0
  18. package/docs/releasing.md +3 -9
  19. package/package.json +2 -2
  20. package/scripts/generate-changelog.ts +207 -12
  21. package/skills/libretto/SKILL.md +22 -15
  22. package/skills/libretto/references/code-generation-rules.md +2 -2
  23. package/skills/libretto/references/configuration-file-reference.md +3 -2
  24. package/skills/libretto-readonly/SKILL.md +1 -1
  25. package/src/cli/cli.ts +38 -13
  26. package/src/cli/commands/browser.ts +2 -3
  27. package/src/cli/commands/execution.ts +1 -1
  28. package/src/cli/commands/search.ts +74 -0
  29. package/src/cli/commands/update.ts +149 -0
  30. package/src/cli/core/context.ts +4 -0
  31. package/src/cli/core/daemon/daemon.ts +3 -0
  32. package/src/cli/core/experiments.ts +15 -1
  33. package/src/cli/core/providers/index.ts +5 -1
  34. package/src/cli/core/providers/steel.ts +75 -0
  35. package/src/cli/core/session-telemetry.ts +176 -13
  36. package/src/cli/core/skill-version.ts +1 -1
  37. package/src/cli/core/telemetry.ts +19 -3
  38. package/src/cli/router.ts +13 -2
  39. package/src/shared/html-search/search-html.spec.ts +65 -0
  40. package/src/shared/html-search/search-html.ts +75 -0
package/dist/cli/cli.js CHANGED
@@ -1,15 +1,12 @@
1
1
  import { ensureLibrettoSetup } from "./core/context.js";
2
2
  import { createCLIApp } from "./router.js";
3
- import { warnIfInstalledSkillOutOfDate } from "./core/skill-version.js";
3
+ import {
4
+ readCurrentCliVersion,
5
+ warnIfInstalledSkillOutOfDate
6
+ } from "./core/skill-version.js";
4
7
  import { loadEnv } from "../shared/env/load-env.js";
5
- function renderUsage(app) {
6
- return `${app.renderHelp()}
7
-
8
- Options:
9
- --session <name> Use a named session (auto-generated for open/run if omitted)
10
-
11
- Docs (agent-friendly): https://libretto.sh/docs
12
- `;
8
+ function renderVersion() {
9
+ return readCurrentCliVersion();
13
10
  }
14
11
  function printSetupAudit() {
15
12
  warnIfInstalledSkillOutOfDate();
@@ -29,9 +26,18 @@ function warnIfPackageManagerExec() {
29
26
  }
30
27
  function isRootHelpRequest(rawArgs) {
31
28
  if (rawArgs.length === 0) return true;
32
- if (rawArgs[0] === "--help" || rawArgs[0] === "-h") return true;
33
29
  return rawArgs[0] === "help" && rawArgs.length === 1;
34
30
  }
31
+ function isVersionRequest(rawArgs) {
32
+ if (rawArgs.length !== 1) return false;
33
+ return rawArgs[0] === "--version" || rawArgs[0] === "-v";
34
+ }
35
+ function hasRootHelp(message, app) {
36
+ return message.endsWith(app.renderHelp());
37
+ }
38
+ function hasScopedHelp(message) {
39
+ return message.includes("\nUsage: ");
40
+ }
35
41
  async function runLibrettoCLI() {
36
42
  const rawArgs = process.argv.slice(2);
37
43
  let exitCode = 0;
@@ -40,8 +46,12 @@ async function runLibrettoCLI() {
40
46
  ensureLibrettoSetup();
41
47
  const app = createCLIApp();
42
48
  try {
49
+ if (isVersionRequest(rawArgs)) {
50
+ console.log(renderVersion());
51
+ return;
52
+ }
43
53
  if (isRootHelpRequest(rawArgs)) {
44
- console.log(renderUsage(app));
54
+ console.log(app.renderHelp());
45
55
  printSetupAudit();
46
56
  return;
47
57
  }
@@ -52,9 +62,18 @@ async function runLibrettoCLI() {
52
62
  } catch (err) {
53
63
  const message = err instanceof Error ? err.message : String(err);
54
64
  if (message.startsWith("Unknown command: ")) {
55
- console.error(`${message}
65
+ if (hasRootHelp(message, app)) {
66
+ const summary = message.split("\n", 1)[0] ?? message;
67
+ console.error(`${summary}
68
+ `);
69
+ console.log(app.renderHelp());
70
+ } else if (hasScopedHelp(message)) {
71
+ console.error(message);
72
+ } else {
73
+ console.error(`${message}
56
74
  `);
57
- console.log(renderUsage(app));
75
+ console.log(app.renderHelp());
76
+ }
58
77
  } else {
59
78
  console.error(message);
60
79
  }
@@ -74,7 +74,7 @@ const openInput = SimpleCLI.input({
74
74
  help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)"
75
75
  }),
76
76
  provider: SimpleCLI.option(z.string().optional(), {
77
- help: "Browser provider (local, kernel, browserbase)",
77
+ help: "Browser provider (local, kernel, browserbase, steel)",
78
78
  aliases: ["-p"]
79
79
  })
80
80
  }
@@ -86,7 +86,7 @@ const openInput = SimpleCLI.input({
86
86
  "Cannot pass both --read-only and --write-access."
87
87
  );
88
88
  const openCommand = SimpleCLI.command({
89
- description: "Launch browser and open URL (headed by default). Automatically loads a saved auth profile for the URL's domain if one exists."
89
+ description: "Launch browser and open URL"
90
90
  }).input(openInput).use(withAutoSession()).use(withExperiments()).handle(async ({ input, ctx }) => {
91
91
  warnIfInstalledSkillOutOfDate();
92
92
  assertSessionAvailableForStart(ctx.session, ctx.logger);
@@ -645,7 +645,7 @@ const runInput = SimpleCLI.input({
645
645
  help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)"
646
646
  }),
647
647
  provider: SimpleCLI.option(z.string().optional(), {
648
- help: "Browser provider (local, kernel, browserbase)",
648
+ help: "Browser provider (local, kernel, browserbase, steel)",
649
649
  aliases: ["-p"]
650
650
  })
651
651
  }
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { DaemonClient } from "../core/daemon/ipc.js";
3
+ import { resolveExperiments } from "../core/experiments.js";
4
+ import {
5
+ formatHtmlForSearch,
6
+ searchFormattedHtml
7
+ } from "../../shared/html-search/search-html.js";
8
+ import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
9
+ import { SimpleCLI } from "affordance";
10
+ const searchInput = SimpleCLI.input({
11
+ positionals: [
12
+ SimpleCLI.positional("pattern", z.string().optional(), {
13
+ help: "JavaScript regex pattern to search for in the formatted HTML snapshot"
14
+ })
15
+ ],
16
+ named: {
17
+ session: sessionOption(),
18
+ page: pageOption()
19
+ }
20
+ }).refine(
21
+ (input) => input.pattern !== void 0,
22
+ "Usage: libretto search <regex> --session <name> [--page <id>]"
23
+ );
24
+ const searchCommand = SimpleCLI.command({
25
+ description: "Search the current page HTML snapshot"
26
+ }).input(searchInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
27
+ if (!resolveExperiments().search) {
28
+ throw new Error(
29
+ [
30
+ 'The "search" experiment is disabled.',
31
+ "Enable it with: libretto experiments enable search"
32
+ ].join("\n")
33
+ );
34
+ }
35
+ if (!ctx.sessionState.daemonSocketPath) {
36
+ throw new Error(
37
+ `Session "${ctx.session}" has no daemon socket. Close and reopen it with: libretto open <url> --session ${ctx.session}`
38
+ );
39
+ }
40
+ const client = await DaemonClient.connect(ctx.sessionState.daemonSocketPath);
41
+ try {
42
+ const response = await client.readonlyExec({
43
+ code: "return await page.content()",
44
+ pageId: input.page
45
+ });
46
+ if (!response.ok) {
47
+ throw new Error(response.message);
48
+ }
49
+ if (typeof response.data.result !== "string") {
50
+ throw new Error("Expected page.content() to return an HTML string.");
51
+ }
52
+ const formattedHtml = formatHtmlForSearch(response.data.result);
53
+ const matches = searchFormattedHtml(formattedHtml, input.pattern);
54
+ if (matches.length === 0) {
55
+ console.log(`No matches for /${input.pattern}/.`);
56
+ return;
57
+ }
58
+ for (const [index, match] of matches.entries()) {
59
+ if (index > 0) console.log("--");
60
+ console.log(match.lines.join("\n"));
61
+ }
62
+ } finally {
63
+ client.destroy();
64
+ }
65
+ });
66
+ export {
67
+ searchCommand,
68
+ searchInput
69
+ };
@@ -0,0 +1,122 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { SimpleCLI } from "affordance";
5
+ const UPDATE_COMMAND = "curl -fsSL https://libretto.sh/install.sh | bash";
6
+ function readCurrentCliVersion() {
7
+ const packageJsonPath = fileURLToPath(
8
+ new URL("../../../package.json", import.meta.url)
9
+ );
10
+ const manifest = JSON.parse(
11
+ readFileSync(packageJsonPath, "utf8")
12
+ );
13
+ if (!manifest.version) {
14
+ throw new Error(
15
+ `Unable to determine current libretto version from ${packageJsonPath}.`
16
+ );
17
+ }
18
+ return manifest.version;
19
+ }
20
+ function readLatestNpmVersion() {
21
+ const result = spawnSync("npm", ["view", "libretto@latest", "version"], {
22
+ encoding: "utf8"
23
+ });
24
+ if (result.error) {
25
+ throw new Error(
26
+ [
27
+ "Error: failed to check the latest Libretto version on npm.",
28
+ `Known state: ${result.error.message}`,
29
+ "Try: npm view libretto@latest version",
30
+ "Help: libretto help update"
31
+ ].join("\n")
32
+ );
33
+ }
34
+ if (result.status !== 0) {
35
+ const detail = result.stderr.trim();
36
+ throw new Error(
37
+ [
38
+ "Error: failed to check the latest Libretto version on npm.",
39
+ `Known state: npm exited with status ${result.status}.`,
40
+ ...detail ? [`npm stderr: ${detail}`] : [],
41
+ "Try: npm view libretto@latest version",
42
+ "Help: libretto help update"
43
+ ].join("\n")
44
+ );
45
+ }
46
+ const version = result.stdout.trim();
47
+ if (!version) {
48
+ throw new Error(
49
+ [
50
+ "Error: failed to check the latest Libretto version on npm.",
51
+ "Known state: npm did not print a version.",
52
+ "Try: npm view libretto@latest version",
53
+ "Help: libretto help update"
54
+ ].join("\n")
55
+ );
56
+ }
57
+ return version;
58
+ }
59
+ const updateInput = SimpleCLI.input({
60
+ positionals: [],
61
+ named: {
62
+ dryRun: SimpleCLI.flag({
63
+ name: "dry-run",
64
+ help: "Print the update command without running it"
65
+ })
66
+ }
67
+ });
68
+ function formatUpdateFailure(status, signal) {
69
+ const knownState = status === null ? `installer was interrupted${signal ? ` by ${signal}` : ""}.` : `installer exited with status ${status}.`;
70
+ return [
71
+ "Error: failed to update Libretto to the latest version.",
72
+ `Known state: ${knownState}`,
73
+ `Try: ${UPDATE_COMMAND}`,
74
+ "Help: libretto help update"
75
+ ].join("\n");
76
+ }
77
+ const updateCommand = SimpleCLI.command({
78
+ description: "Update Libretto to the latest version"
79
+ }).input(updateInput).handle(async ({ input }) => {
80
+ if (input.dryRun) {
81
+ console.log("Update command:");
82
+ console.log(` ${UPDATE_COMMAND}`);
83
+ console.log("No changes made.");
84
+ return;
85
+ }
86
+ const currentVersion = readCurrentCliVersion();
87
+ const latestVersion = readLatestNpmVersion();
88
+ console.log(`Current version: ${currentVersion}`);
89
+ console.log(`Latest version: ${latestVersion}`);
90
+ if (currentVersion === latestVersion) {
91
+ console.log(`Libretto is already up to date (${currentVersion}).`);
92
+ console.log("No further action required.");
93
+ return;
94
+ }
95
+ console.log("Updating Libretto to latest...");
96
+ const result = spawnSync("bash", ["-lc", UPDATE_COMMAND], {
97
+ stdio: "inherit",
98
+ env: {
99
+ ...process.env,
100
+ LIBRETTO_VERSION: "latest"
101
+ }
102
+ });
103
+ if (result.error) {
104
+ throw new Error(
105
+ [
106
+ "Error: failed to start the Libretto installer.",
107
+ `Known state: ${result.error.message}`,
108
+ `Try: ${UPDATE_COMMAND}`,
109
+ "Help: libretto help update"
110
+ ].join("\n")
111
+ );
112
+ }
113
+ if (result.status !== 0) {
114
+ throw new Error(formatUpdateFailure(result.status, result.signal));
115
+ }
116
+ console.log("Libretto updated to latest.");
117
+ console.log("No further action required.");
118
+ });
119
+ export {
120
+ updateCommand,
121
+ updateInput
122
+ };
@@ -27,6 +27,9 @@ function getSessionLogsPath(session) {
27
27
  function getSessionNetworkLogPath(session) {
28
28
  return join(getSessionDir(session), "network.jsonl");
29
29
  }
30
+ function getSessionRawNetworkDir(session) {
31
+ return join(getSessionDir(session), "raw-network");
32
+ }
30
33
  function getSessionActionsLogPath(session) {
31
34
  return join(getSessionDir(session), "actions.jsonl");
32
35
  }
@@ -79,6 +82,7 @@ export {
79
82
  getSessionLogsPath,
80
83
  getSessionNetworkLogPath,
81
84
  getSessionProviderClosePath,
85
+ getSessionRawNetworkDir,
82
86
  getSessionSnapshotRunDir,
83
87
  getSessionSnapshotsDir,
84
88
  getSessionStatePath,
@@ -14,6 +14,7 @@ import {
14
14
  import {
15
15
  createLoggerForSession,
16
16
  getSessionDir,
17
+ getSessionRawNetworkDir,
17
18
  getSessionNetworkLogPath,
18
19
  getSessionActionsLogPath,
19
20
  getSessionProviderClosePath,
@@ -155,6 +156,7 @@ class BrowserDaemon {
155
156
  } = args;
156
157
  await mkdir(getSessionDir(session), { recursive: true });
157
158
  const networkLogFile = getSessionNetworkLogPath(session);
159
+ const rawNetworkDir = getSessionRawNetworkDir(session);
158
160
  const actionsLogFile = getSessionActionsLogPath(session);
159
161
  const logger = createLoggerForSession(session);
160
162
  try {
@@ -162,6 +164,7 @@ class BrowserDaemon {
162
164
  context,
163
165
  initialPage: page,
164
166
  includeUserDomActions: true,
167
+ rawNetworkDir,
165
168
  logAction: (entry) => {
166
169
  appendFileSync(actionsLogFile, JSON.stringify(entry) + "\n");
167
170
  },
@@ -2,7 +2,20 @@ import {
2
2
  readLibrettoConfig,
3
3
  writeLibrettoConfig
4
4
  } from "./config.js";
5
- const EXPERIMENTS = {};
5
+ const EXPERIMENTS = {
6
+ search: {
7
+ title: "HTML Search",
8
+ oneSentenceDescription: "Adds a search command that greps the current page's formatted HTML snapshot.",
9
+ docs: [
10
+ "Adds a search command for inspecting the current page's HTML snapshot with a JavaScript regex.",
11
+ "",
12
+ "Usage: libretto search <regex> --session <name> [--page <id>]",
13
+ "",
14
+ "The command captures page HTML through read-only execution, condenses and formats it, then prints matching regions with up to four lines of surrounding context."
15
+ ].join("\n"),
16
+ defaultValue: false
17
+ }
18
+ };
6
19
  function isExperimentName(name) {
7
20
  return Object.hasOwn(EXPERIMENTS, name);
8
21
  }
@@ -2,10 +2,12 @@ import { readLibrettoConfig } from "../config.js";
2
2
  import { createBrowserbaseProvider } from "./browserbase.js";
3
3
  import { createKernelProvider } from "./kernel.js";
4
4
  import { createLibrettoCloudProvider } from "./libretto-cloud.js";
5
+ import { createSteelProvider } from "./steel.js";
5
6
  const VALID_PROVIDERS = /* @__PURE__ */ new Set([
6
7
  "local",
7
8
  "kernel",
8
9
  "browserbase",
10
+ "steel",
9
11
  "libretto-cloud"
10
12
  ]);
11
13
  const DEFAULT_PROVIDER_STARTUP_TIMEOUT_MS = 6e4;
@@ -38,12 +40,14 @@ function getCloudProviderApi(name) {
38
40
  return createKernelProvider();
39
41
  case "browserbase":
40
42
  return createBrowserbaseProvider();
43
+ case "steel":
44
+ return createSteelProvider();
41
45
  case "libretto-cloud":
42
46
  console.warn("Note: The libretto-cloud provider is in alpha.");
43
47
  return createLibrettoCloudProvider();
44
48
  default:
45
49
  throw new Error(
46
- `Unknown provider "${name}". Valid cloud providers: kernel, browserbase`
50
+ `Unknown provider "${name}". Valid cloud providers: kernel, browserbase, steel`
47
51
  );
48
52
  }
49
53
  }
@@ -0,0 +1,56 @@
1
+ const DEFAULT_STEEL_API_ENDPOINT = "https://api.steel.dev";
2
+ const DEFAULT_STEEL_CONNECT_ENDPOINT = "wss://connect.steel.dev";
3
+ function createSteelProvider(options = {}) {
4
+ const apiKey = options.apiKey ?? process.env.STEEL_API_KEY;
5
+ if (!apiKey) throw new Error("STEEL_API_KEY is required for Steel provider.");
6
+ const endpoint = process.env.STEEL_BASE_URL ?? DEFAULT_STEEL_API_ENDPOINT;
7
+ const connectEndpoint = process.env.STEEL_CONNECT_URL ?? DEFAULT_STEEL_CONNECT_ENDPOINT;
8
+ return {
9
+ async createSession() {
10
+ const resp = await fetch(`${endpoint}/v1/sessions`, {
11
+ method: "POST",
12
+ headers: {
13
+ "steel-api-key": apiKey,
14
+ "Content-Type": "application/json"
15
+ },
16
+ body: JSON.stringify({})
17
+ });
18
+ if (!resp.ok) {
19
+ const body = await resp.text();
20
+ throw new Error(`Steel API error (${resp.status}): ${body}`);
21
+ }
22
+ const json = await resp.json();
23
+ return {
24
+ sessionId: json.id,
25
+ cdpEndpoint: buildSteelCdpEndpoint(connectEndpoint, apiKey, json.id),
26
+ liveViewUrl: json.sessionViewerUrl
27
+ };
28
+ },
29
+ async closeSession(sessionId) {
30
+ const resp = await fetch(`${endpoint}/v1/sessions/${sessionId}/release`, {
31
+ method: "POST",
32
+ headers: {
33
+ "steel-api-key": apiKey,
34
+ "Content-Type": "application/json"
35
+ },
36
+ body: JSON.stringify({})
37
+ });
38
+ if (!resp.ok) {
39
+ const body = await resp.text();
40
+ throw new Error(
41
+ `Steel API error closing session ${sessionId} (${resp.status}): ${body}`
42
+ );
43
+ }
44
+ return {};
45
+ }
46
+ };
47
+ }
48
+ function buildSteelCdpEndpoint(connectEndpoint, apiKey, sessionId) {
49
+ const endpoint = new URL(connectEndpoint);
50
+ endpoint.searchParams.set("apiKey", apiKey);
51
+ endpoint.searchParams.set("sessionId", sessionId);
52
+ return endpoint.toString();
53
+ }
54
+ export {
55
+ createSteelProvider
56
+ };
@@ -1,3 +1,6 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { gzipSync } from "node:zlib";
1
4
  import {
2
5
  filterSemanticClasses,
3
6
  INTERACTIVE_ROLE_NAMES,
@@ -6,13 +9,42 @@ import {
6
9
  TEST_ATTRIBUTE_NAMES,
7
10
  TRUSTED_ATTRIBUTE_NAMES
8
11
  } from "../../shared/dom-semantics.js";
12
+ const BODY_PREVIEW_CHARS = 4096;
13
+ const MAX_SAVED_BODY_BYTES = 10 * 1024 * 1024;
14
+ const LOG_RESOURCE_TYPES = /* @__PURE__ */ new Set(["document", "xhr", "fetch"]);
15
+ const SKIP_RESOURCE_TYPES = /* @__PURE__ */ new Set(["image", "font", "media", "stylesheet"]);
16
+ const NOISE_URL_RE = /(google-analytics|googletagmanager|googleadservices|googlesyndication|doubleclick|facebook\.com\/tr|pinterest|criteo|snapchat|2mdn\.net|adtrafficquality|safeframe|recaptcha|analytics|beacon|pixel|\/ads?\/|\/collect|\/event|\/pagead\/|\/gmp\/conversion|\/ccm\/|\/rmkt\/|favicon|\.map(?:\?|$))/i;
17
+ const TEXT_CONTENT_TYPE_RE = /json|html|text|xml|graphql|javascript|x-www-form-urlencoded/i;
18
+ function shouldLogNetworkEntry(method, url, resourceType) {
19
+ if (url.startsWith("chrome-extension://")) return false;
20
+ if (NOISE_URL_RE.test(url)) return false;
21
+ if (resourceType === "ping") return false;
22
+ if (LOG_RESOURCE_TYPES.has(resourceType)) return true;
23
+ if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) return true;
24
+ if (SKIP_RESOURCE_TYPES.has(resourceType)) return false;
25
+ return false;
26
+ }
27
+ function isTextLikeContentType(contentType) {
28
+ return contentType !== null && TEXT_CONTENT_TYPE_RE.test(contentType);
29
+ }
30
+ function bodyPreview(value) {
31
+ return value.slice(0, BODY_PREVIEW_CHARS);
32
+ }
33
+ function saveBodySidecar(rawNetworkDir, id, kind, contentType, body) {
34
+ if (!rawNetworkDir) return null;
35
+ mkdirSync(rawNetworkDir, { recursive: true });
36
+ const ext = contentType?.includes("json") ? "json" : contentType?.includes("html") ? "html" : "txt";
37
+ const filename = `${String(id).padStart(6, "0")}.${kind}.${ext}.gz`;
38
+ writeFileSync(join(rawNetworkDir, filename), gzipSync(body));
39
+ return `raw-network/${filename}`;
40
+ }
9
41
  async function installSessionTelemetry(options) {
10
- const STATIC_EXT_RE = /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
11
- const { context, initialPage, logAction, logNetwork } = options;
42
+ const { context, initialPage, logAction, logNetwork, rawNetworkDir } = options;
12
43
  const includeUserDomActions = options.includeUserDomActions ?? false;
13
44
  const pageIdCache = /* @__PURE__ */ new WeakMap();
14
45
  const wrappedPages = /* @__PURE__ */ new WeakSet();
15
46
  const exposedPages = /* @__PURE__ */ new WeakSet();
47
+ let networkId = 0;
16
48
  const resolvePageId = async (page) => {
17
49
  if (pageIdCache.has(page)) return pageIdCache.get(page);
18
50
  const cdpSession = await context.newCDPSession(page);
@@ -698,15 +730,119 @@ async function installSessionTelemetry(options) {
698
730
  page.on("response", async (response) => {
699
731
  const request = response.request();
700
732
  const url = request.url();
701
- if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
702
- return;
733
+ const method = request.method();
734
+ const resourceType = request.resourceType();
735
+ if (!shouldLogNetworkEntry(method, url, resourceType)) return;
736
+ const id = ++networkId;
737
+ const requestHeaders = request.headers();
738
+ const responseHeaders = response.headers();
739
+ const contentType = responseHeaders["content-type"] ?? null;
740
+ const requestContentType = requestHeaders["content-type"] ?? null;
741
+ const requestBody = request.postData();
742
+ const requestBodyBytes = requestBody === null ? null : Buffer.byteLength(requestBody);
743
+ let requestBodyPath = null;
744
+ let requestBodyOmittedReason = null;
745
+ let responseBodyPreview = null;
746
+ let responseBodyPath = null;
747
+ let responseBodyBytes = null;
748
+ let responseBodyTruncated = false;
749
+ let responseBodyOmittedReason = null;
750
+ let errorText = null;
751
+ if (requestBody === null) {
752
+ requestBodyOmittedReason = "no-request-body";
753
+ } else if (!isTextLikeContentType(requestContentType)) {
754
+ requestBodyOmittedReason = "binary-content-type";
755
+ } else if (requestBodyBytes !== null && requestBodyBytes > MAX_SAVED_BODY_BYTES) {
756
+ requestBodyOmittedReason = "body-too-large";
757
+ } else {
758
+ requestBodyPath = saveBodySidecar(
759
+ rawNetworkDir,
760
+ id,
761
+ "request",
762
+ requestContentType,
763
+ requestBody
764
+ );
765
+ }
766
+ if (!isTextLikeContentType(contentType) || !LOG_RESOURCE_TYPES.has(resourceType)) {
767
+ responseBodyOmittedReason = "binary-content-type";
768
+ } else {
769
+ try {
770
+ const responseBody = await response.text();
771
+ responseBodyBytes = Buffer.byteLength(responseBody);
772
+ responseBodyPreview = bodyPreview(responseBody);
773
+ if (responseBodyBytes > MAX_SAVED_BODY_BYTES) {
774
+ responseBodyTruncated = true;
775
+ responseBodyOmittedReason = "body-too-large";
776
+ } else {
777
+ responseBodyPath = saveBodySidecar(
778
+ rawNetworkDir,
779
+ id,
780
+ "response",
781
+ contentType,
782
+ responseBody
783
+ );
784
+ }
785
+ } catch (error) {
786
+ responseBodyOmittedReason = "read-error";
787
+ errorText = error?.message ?? String(error);
788
+ }
789
+ }
703
790
  emitNetwork({
791
+ id,
704
792
  pageId,
705
- method: request.method(),
793
+ method,
706
794
  url,
795
+ resourceType,
707
796
  status: response.status(),
708
- contentType: response.headers()["content-type"] ?? null,
709
- postData: request.method() === "POST" || request.method() === "PUT" || request.method() === "PATCH" ? (request.postData() ?? "").substring(0, 2e3) : void 0,
797
+ statusText: response.statusText(),
798
+ contentType,
799
+ requestHeaders,
800
+ responseHeaders,
801
+ requestBodyPreview: requestBody ? bodyPreview(requestBody) : null,
802
+ requestBodyPath,
803
+ requestBodyBytes,
804
+ requestBodyTruncated: requestBody !== null && requestBodyBytes !== null && requestBodyBytes > MAX_SAVED_BODY_BYTES,
805
+ requestBodyOmittedReason,
806
+ responseBodyPreview,
807
+ responseBodyPath,
808
+ responseBodyBytes,
809
+ responseBodyTruncated,
810
+ responseBodyOmittedReason,
811
+ errorText,
812
+ postData: requestBody ? bodyPreview(requestBody) : void 0,
813
+ responseBody: null,
814
+ size: null,
815
+ durationMs: null
816
+ });
817
+ });
818
+ page.on("requestfailed", async (request) => {
819
+ const url = request.url();
820
+ const method = request.method();
821
+ const resourceType = request.resourceType();
822
+ if (!shouldLogNetworkEntry(method, url, resourceType)) return;
823
+ const id = ++networkId;
824
+ emitNetwork({
825
+ id,
826
+ pageId,
827
+ method,
828
+ url,
829
+ resourceType,
830
+ status: null,
831
+ statusText: null,
832
+ contentType: null,
833
+ requestHeaders: request.headers(),
834
+ responseHeaders: null,
835
+ requestBodyPreview: null,
836
+ requestBodyPath: null,
837
+ requestBodyBytes: null,
838
+ requestBodyTruncated: false,
839
+ requestBodyOmittedReason: null,
840
+ responseBodyPreview: null,
841
+ responseBodyPath: null,
842
+ responseBodyBytes: null,
843
+ responseBodyTruncated: false,
844
+ responseBodyOmittedReason: "request-failed",
845
+ errorText: request.failure()?.errorText ?? null,
710
846
  responseBody: null,
711
847
  size: null,
712
848
  durationMs: null
@@ -69,5 +69,6 @@ function warnIfInstalledSkillOutOfDate() {
69
69
  }
70
70
  }
71
71
  export {
72
+ readCurrentCliVersion,
72
73
  warnIfInstalledSkillOutOfDate
73
74
  };
@@ -7,11 +7,13 @@ import { experimentsCommand } from "./commands/experiments.js";
7
7
  import { setupCommand } from "./commands/setup.js";
8
8
  import { statusCommand } from "./commands/status.js";
9
9
  import { snapshotCommand } from "./commands/snapshot.js";
10
+ import { searchCommand } from "./commands/search.js";
11
+ import { updateCommand } from "./commands/update.js";
10
12
  import { SimpleCLI } from "affordance";
11
13
  const cliRoutes = {
12
14
  ...browserCommands,
13
15
  cloud: SimpleCLI.group({
14
- description: "Libretto Cloud commands",
16
+ description: "Deploy workflows and manage hosted Libretto",
15
17
  routes: {
16
18
  deploy: deployCommand,
17
19
  auth: authCommands,
@@ -20,12 +22,21 @@ const cliRoutes = {
20
22
  }),
21
23
  experiments: experimentsCommand,
22
24
  ...executionCommands,
25
+ search: searchCommand,
23
26
  setup: setupCommand,
24
27
  status: statusCommand,
25
- snapshot: snapshotCommand
28
+ snapshot: snapshotCommand,
29
+ update: updateCommand
26
30
  };
27
31
  function createCLIApp() {
28
- return SimpleCLI.define("libretto", cliRoutes);
32
+ return SimpleCLI.define("libretto", cliRoutes, {
33
+ appendHelpText: [
34
+ "Options:",
35
+ " --session <name> Required for session-scoped commands",
36
+ " -h, --help",
37
+ " -v, --version"
38
+ ].join("\n")
39
+ });
29
40
  }
30
41
  export {
31
42
  cliRoutes,
@@ -0,0 +1,9 @@
1
+ type SearchHtmlMatch = {
2
+ startLine: number;
3
+ endLine: number;
4
+ lines: string[];
5
+ };
6
+ declare function formatHtmlForSearch(html: string): string;
7
+ declare function searchFormattedHtml(formattedHtml: string, pattern: string, contextLines?: number, matchLimit?: number): SearchHtmlMatch[];
8
+
9
+ export { type SearchHtmlMatch, formatHtmlForSearch, searchFormattedHtml };