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.
- package/dist/cli/cli.js +32 -13
- package/dist/cli/commands/browser.js +2 -2
- package/dist/cli/commands/execution.js +1 -1
- package/dist/cli/commands/search.js +69 -0
- package/dist/cli/commands/update.js +122 -0
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/daemon.js +3 -0
- package/dist/cli/core/experiments.js +14 -1
- package/dist/cli/core/providers/index.js +5 -1
- package/dist/cli/core/providers/steel.js +56 -0
- package/dist/cli/core/session-telemetry.js +143 -7
- package/dist/cli/core/skill-version.js +1 -0
- package/dist/cli/router.js +14 -3
- package/dist/shared/html-search/search-html.d.ts +9 -0
- package/dist/shared/html-search/search-html.js +46 -0
- package/dist/shared/html-search/search-html.spec.d.ts +2 -0
- package/dist/shared/html-search/search-html.spec.js +57 -0
- package/docs/releasing.md +3 -9
- package/package.json +2 -2
- package/scripts/generate-changelog.ts +207 -12
- package/skills/libretto/SKILL.md +22 -15
- package/skills/libretto/references/code-generation-rules.md +2 -2
- package/skills/libretto/references/configuration-file-reference.md +3 -2
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +38 -13
- package/src/cli/commands/browser.ts +2 -3
- package/src/cli/commands/execution.ts +1 -1
- package/src/cli/commands/search.ts +74 -0
- package/src/cli/commands/update.ts +149 -0
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/daemon.ts +3 -0
- package/src/cli/core/experiments.ts +15 -1
- package/src/cli/core/providers/index.ts +5 -1
- package/src/cli/core/providers/steel.ts +75 -0
- package/src/cli/core/session-telemetry.ts +176 -13
- package/src/cli/core/skill-version.ts +1 -1
- package/src/cli/core/telemetry.ts +19 -3
- package/src/cli/router.ts +13 -2
- package/src/shared/html-search/search-html.spec.ts +65 -0
- 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 {
|
|
3
|
+
import {
|
|
4
|
+
readCurrentCliVersion,
|
|
5
|
+
warnIfInstalledSkillOutOfDate
|
|
6
|
+
} from "./core/skill-version.js";
|
|
4
7
|
import { loadEnv } from "../shared/env/load-env.js";
|
|
5
|
-
function
|
|
6
|
-
return
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
};
|
package/dist/cli/core/context.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
702
|
-
|
|
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
|
|
793
|
+
method,
|
|
706
794
|
url,
|
|
795
|
+
resourceType,
|
|
707
796
|
status: response.status(),
|
|
708
|
-
|
|
709
|
-
|
|
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
|
package/dist/cli/router.js
CHANGED
|
@@ -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: "
|
|
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 };
|