libretto 0.6.16 → 0.6.18
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 +18 -19
- 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/src/cli/cli.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
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
8
|
|
|
6
|
-
function
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
Options:
|
|
10
|
-
--session <name> Use a named session (auto-generated for open/run if omitted)
|
|
11
|
-
|
|
12
|
-
Docs (agent-friendly): https://libretto.sh/docs
|
|
13
|
-
`;
|
|
9
|
+
function renderVersion(): string {
|
|
10
|
+
return readCurrentCliVersion();
|
|
14
11
|
}
|
|
15
12
|
|
|
16
13
|
function printSetupAudit(): void {
|
|
@@ -35,10 +32,25 @@ function warnIfPackageManagerExec(): void {
|
|
|
35
32
|
|
|
36
33
|
function isRootHelpRequest(rawArgs: readonly string[]): boolean {
|
|
37
34
|
if (rawArgs.length === 0) return true;
|
|
38
|
-
if (rawArgs[0] === "--help" || rawArgs[0] === "-h") return true;
|
|
39
35
|
return rawArgs[0] === "help" && rawArgs.length === 1;
|
|
40
36
|
}
|
|
41
37
|
|
|
38
|
+
function isVersionRequest(rawArgs: readonly string[]): boolean {
|
|
39
|
+
if (rawArgs.length !== 1) return false;
|
|
40
|
+
return rawArgs[0] === "--version" || rawArgs[0] === "-v";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasRootHelp(
|
|
44
|
+
message: string,
|
|
45
|
+
app: ReturnType<typeof createCLIApp>,
|
|
46
|
+
): boolean {
|
|
47
|
+
return message.endsWith(app.renderHelp());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hasScopedHelp(message: string): boolean {
|
|
51
|
+
return message.includes("\nUsage: ");
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
export async function runLibrettoCLI(): Promise<void> {
|
|
43
55
|
const rawArgs = process.argv.slice(2);
|
|
44
56
|
let exitCode = 0;
|
|
@@ -48,8 +60,13 @@ export async function runLibrettoCLI(): Promise<void> {
|
|
|
48
60
|
const app = createCLIApp();
|
|
49
61
|
|
|
50
62
|
try {
|
|
63
|
+
if (isVersionRequest(rawArgs)) {
|
|
64
|
+
console.log(renderVersion());
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
if (isRootHelpRequest(rawArgs)) {
|
|
52
|
-
console.log(
|
|
69
|
+
console.log(app.renderHelp());
|
|
53
70
|
printSetupAudit();
|
|
54
71
|
return;
|
|
55
72
|
}
|
|
@@ -61,8 +78,16 @@ export async function runLibrettoCLI(): Promise<void> {
|
|
|
61
78
|
} catch (err) {
|
|
62
79
|
const message = err instanceof Error ? err.message : String(err);
|
|
63
80
|
if (message.startsWith("Unknown command: ")) {
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
if (hasRootHelp(message, app)) {
|
|
82
|
+
const summary = message.split("\n", 1)[0] ?? message;
|
|
83
|
+
console.error(`${summary}\n`);
|
|
84
|
+
console.log(app.renderHelp());
|
|
85
|
+
} else if (hasScopedHelp(message)) {
|
|
86
|
+
console.error(message);
|
|
87
|
+
} else {
|
|
88
|
+
console.error(`${message}\n`);
|
|
89
|
+
console.log(app.renderHelp());
|
|
90
|
+
}
|
|
66
91
|
} else {
|
|
67
92
|
console.error(message);
|
|
68
93
|
}
|
|
@@ -86,7 +86,7 @@ export const openInput = SimpleCLI.input({
|
|
|
86
86
|
help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
|
|
87
87
|
}),
|
|
88
88
|
provider: SimpleCLI.option(z.string().optional(), {
|
|
89
|
-
help: "Browser provider (local, kernel, browserbase)",
|
|
89
|
+
help: "Browser provider (local, kernel, browserbase, steel)",
|
|
90
90
|
aliases: ["-p"],
|
|
91
91
|
}),
|
|
92
92
|
},
|
|
@@ -101,8 +101,7 @@ export const openInput = SimpleCLI.input({
|
|
|
101
101
|
);
|
|
102
102
|
|
|
103
103
|
export const openCommand = SimpleCLI.command({
|
|
104
|
-
description:
|
|
105
|
-
"Launch browser and open URL (headed by default). Automatically loads a saved auth profile for the URL's domain if one exists.",
|
|
104
|
+
description: "Launch browser and open URL",
|
|
106
105
|
})
|
|
107
106
|
.input(openInput)
|
|
108
107
|
.use(withAutoSession())
|
|
@@ -810,7 +810,7 @@ export const runInput = SimpleCLI.input({
|
|
|
810
810
|
help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
|
|
811
811
|
}),
|
|
812
812
|
provider: SimpleCLI.option(z.string().optional(), {
|
|
813
|
-
help: "Browser provider (local, kernel, browserbase)",
|
|
813
|
+
help: "Browser provider (local, kernel, browserbase, steel)",
|
|
814
814
|
aliases: ["-p"],
|
|
815
815
|
}),
|
|
816
816
|
},
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
|
|
11
|
+
export const searchInput = SimpleCLI.input({
|
|
12
|
+
positionals: [
|
|
13
|
+
SimpleCLI.positional("pattern", z.string().optional(), {
|
|
14
|
+
help: "JavaScript regex pattern to search for in the formatted HTML snapshot",
|
|
15
|
+
}),
|
|
16
|
+
],
|
|
17
|
+
named: {
|
|
18
|
+
session: sessionOption(),
|
|
19
|
+
page: pageOption(),
|
|
20
|
+
},
|
|
21
|
+
}).refine(
|
|
22
|
+
(input) => input.pattern !== undefined,
|
|
23
|
+
"Usage: libretto search <regex> --session <name> [--page <id>]",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const searchCommand = SimpleCLI.command({
|
|
27
|
+
description: "Search the current page HTML snapshot",
|
|
28
|
+
})
|
|
29
|
+
.input(searchInput)
|
|
30
|
+
.use(withRequiredSession())
|
|
31
|
+
.handle(async ({ input, ctx }) => {
|
|
32
|
+
if (!resolveExperiments().search) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
[
|
|
35
|
+
'The "search" experiment is disabled.',
|
|
36
|
+
"Enable it with: libretto experiments enable search",
|
|
37
|
+
].join("\n"),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!ctx.sessionState.daemonSocketPath) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Session "${ctx.session}" has no daemon socket. Close and reopen it with: libretto open <url> --session ${ctx.session}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const client = await DaemonClient.connect(ctx.sessionState.daemonSocketPath);
|
|
48
|
+
try {
|
|
49
|
+
const response = await client.readonlyExec({
|
|
50
|
+
code: "return await page.content()",
|
|
51
|
+
pageId: input.page,
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(response.message);
|
|
55
|
+
}
|
|
56
|
+
if (typeof response.data.result !== "string") {
|
|
57
|
+
throw new Error("Expected page.content() to return an HTML string.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const formattedHtml = formatHtmlForSearch(response.data.result);
|
|
61
|
+
const matches = searchFormattedHtml(formattedHtml, input.pattern!);
|
|
62
|
+
if (matches.length === 0) {
|
|
63
|
+
console.log(`No matches for /${input.pattern}/.`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const [index, match] of matches.entries()) {
|
|
68
|
+
if (index > 0) console.log("--");
|
|
69
|
+
console.log(match.lines.join("\n"));
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
client.destroy();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
|
|
6
|
+
const UPDATE_COMMAND = "curl -fsSL https://libretto.sh/install.sh | bash";
|
|
7
|
+
|
|
8
|
+
type PackageManifest = {
|
|
9
|
+
version?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function readCurrentCliVersion(): string {
|
|
13
|
+
const packageJsonPath = fileURLToPath(
|
|
14
|
+
new URL("../../../package.json", import.meta.url),
|
|
15
|
+
);
|
|
16
|
+
const manifest = JSON.parse(
|
|
17
|
+
readFileSync(packageJsonPath, "utf8"),
|
|
18
|
+
) as PackageManifest;
|
|
19
|
+
|
|
20
|
+
if (!manifest.version) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Unable to determine current libretto version from ${packageJsonPath}.`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return manifest.version;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readLatestNpmVersion(): string {
|
|
30
|
+
const result = spawnSync("npm", ["view", "libretto@latest", "version"], {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (result.error) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
[
|
|
37
|
+
"Error: failed to check the latest Libretto version on npm.",
|
|
38
|
+
`Known state: ${result.error.message}`,
|
|
39
|
+
"Try: npm view libretto@latest version",
|
|
40
|
+
"Help: libretto help update",
|
|
41
|
+
].join("\n"),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
const detail = result.stderr.trim();
|
|
47
|
+
throw new Error(
|
|
48
|
+
[
|
|
49
|
+
"Error: failed to check the latest Libretto version on npm.",
|
|
50
|
+
`Known state: npm exited with status ${result.status}.`,
|
|
51
|
+
...(detail ? [`npm stderr: ${detail}`] : []),
|
|
52
|
+
"Try: npm view libretto@latest version",
|
|
53
|
+
"Help: libretto help update",
|
|
54
|
+
].join("\n"),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const version = result.stdout.trim();
|
|
59
|
+
if (!version) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
[
|
|
62
|
+
"Error: failed to check the latest Libretto version on npm.",
|
|
63
|
+
"Known state: npm did not print a version.",
|
|
64
|
+
"Try: npm view libretto@latest version",
|
|
65
|
+
"Help: libretto help update",
|
|
66
|
+
].join("\n"),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return version;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const updateInput = SimpleCLI.input({
|
|
74
|
+
positionals: [],
|
|
75
|
+
named: {
|
|
76
|
+
dryRun: SimpleCLI.flag({
|
|
77
|
+
name: "dry-run",
|
|
78
|
+
help: "Print the update command without running it",
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function formatUpdateFailure(
|
|
84
|
+
status: number | null,
|
|
85
|
+
signal: string | null,
|
|
86
|
+
): string {
|
|
87
|
+
const knownState =
|
|
88
|
+
status === null
|
|
89
|
+
? `installer was interrupted${signal ? ` by ${signal}` : ""}.`
|
|
90
|
+
: `installer exited with status ${status}.`;
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
"Error: failed to update Libretto to the latest version.",
|
|
94
|
+
`Known state: ${knownState}`,
|
|
95
|
+
`Try: ${UPDATE_COMMAND}`,
|
|
96
|
+
"Help: libretto help update",
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const updateCommand = SimpleCLI.command({
|
|
101
|
+
description: "Update Libretto to the latest version",
|
|
102
|
+
})
|
|
103
|
+
.input(updateInput)
|
|
104
|
+
.handle(async ({ input }) => {
|
|
105
|
+
if (input.dryRun) {
|
|
106
|
+
console.log("Update command:");
|
|
107
|
+
console.log(` ${UPDATE_COMMAND}`);
|
|
108
|
+
console.log("No changes made.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const currentVersion = readCurrentCliVersion();
|
|
113
|
+
const latestVersion = readLatestNpmVersion();
|
|
114
|
+
console.log(`Current version: ${currentVersion}`);
|
|
115
|
+
console.log(`Latest version: ${latestVersion}`);
|
|
116
|
+
|
|
117
|
+
if (currentVersion === latestVersion) {
|
|
118
|
+
console.log(`Libretto is already up to date (${currentVersion}).`);
|
|
119
|
+
console.log("No further action required.");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log("Updating Libretto to latest...");
|
|
124
|
+
const result = spawnSync("bash", ["-lc", UPDATE_COMMAND], {
|
|
125
|
+
stdio: "inherit",
|
|
126
|
+
env: {
|
|
127
|
+
...process.env,
|
|
128
|
+
LIBRETTO_VERSION: "latest",
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (result.error) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
[
|
|
135
|
+
"Error: failed to start the Libretto installer.",
|
|
136
|
+
`Known state: ${result.error.message}`,
|
|
137
|
+
`Try: ${UPDATE_COMMAND}`,
|
|
138
|
+
"Help: libretto help update",
|
|
139
|
+
].join("\n"),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (result.status !== 0) {
|
|
144
|
+
throw new Error(formatUpdateFailure(result.status, result.signal));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log("Libretto updated to latest.");
|
|
148
|
+
console.log("No further action required.");
|
|
149
|
+
});
|
package/src/cli/core/context.ts
CHANGED
|
@@ -34,6 +34,10 @@ export function getSessionNetworkLogPath(session: string): string {
|
|
|
34
34
|
return join(getSessionDir(session), "network.jsonl");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function getSessionRawNetworkDir(session: string): string {
|
|
38
|
+
return join(getSessionDir(session), "raw-network");
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
export function getSessionActionsLogPath(session: string): string {
|
|
38
42
|
return join(getSessionDir(session), "actions.jsonl");
|
|
39
43
|
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import {
|
|
40
40
|
createLoggerForSession,
|
|
41
41
|
getSessionDir,
|
|
42
|
+
getSessionRawNetworkDir,
|
|
42
43
|
getSessionNetworkLogPath,
|
|
43
44
|
getSessionActionsLogPath,
|
|
44
45
|
getSessionProviderClosePath,
|
|
@@ -255,6 +256,7 @@ class BrowserDaemon {
|
|
|
255
256
|
// Telemetry — may fail on connect-mode reconnections where
|
|
256
257
|
// exposeFunction bindings already exist; log and continue.
|
|
257
258
|
const networkLogFile = getSessionNetworkLogPath(session);
|
|
259
|
+
const rawNetworkDir = getSessionRawNetworkDir(session);
|
|
258
260
|
const actionsLogFile = getSessionActionsLogPath(session);
|
|
259
261
|
const logger = createLoggerForSession(session);
|
|
260
262
|
|
|
@@ -263,6 +265,7 @@ class BrowserDaemon {
|
|
|
263
265
|
context,
|
|
264
266
|
initialPage: page,
|
|
265
267
|
includeUserDomActions: true,
|
|
268
|
+
rawNetworkDir,
|
|
266
269
|
logAction: (entry: TelemetryEntry) => {
|
|
267
270
|
appendFileSync(actionsLogFile, JSON.stringify(entry) + "\n");
|
|
268
271
|
},
|
|
@@ -11,7 +11,21 @@ export type ExperimentMetadata = {
|
|
|
11
11
|
defaultValue: boolean;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
export const EXPERIMENTS: Readonly<Record<string, ExperimentMetadata>> = {
|
|
14
|
+
export const EXPERIMENTS: Readonly<Record<string, ExperimentMetadata>> = {
|
|
15
|
+
search: {
|
|
16
|
+
title: "HTML Search",
|
|
17
|
+
oneSentenceDescription:
|
|
18
|
+
"Adds a search command that greps the current page's formatted HTML snapshot.",
|
|
19
|
+
docs: [
|
|
20
|
+
"Adds a search command for inspecting the current page's HTML snapshot with a JavaScript regex.",
|
|
21
|
+
"",
|
|
22
|
+
"Usage: libretto search <regex> --session <name> [--page <id>]",
|
|
23
|
+
"",
|
|
24
|
+
"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.",
|
|
25
|
+
].join("\n"),
|
|
26
|
+
defaultValue: false,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
15
29
|
|
|
16
30
|
export type ExperimentName = string;
|
|
17
31
|
export type Experiments = Record<ExperimentName, boolean>;
|
|
@@ -2,12 +2,14 @@ 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
|
import type { ProviderApi } from "./types.js";
|
|
6
7
|
|
|
7
8
|
const VALID_PROVIDERS = new Set([
|
|
8
9
|
"local",
|
|
9
10
|
"kernel",
|
|
10
11
|
"browserbase",
|
|
12
|
+
"steel",
|
|
11
13
|
"libretto-cloud",
|
|
12
14
|
] as const);
|
|
13
15
|
export type ProviderName =
|
|
@@ -56,12 +58,14 @@ export function getCloudProviderApi(name: string): ProviderApi {
|
|
|
56
58
|
return createKernelProvider();
|
|
57
59
|
case "browserbase":
|
|
58
60
|
return createBrowserbaseProvider();
|
|
61
|
+
case "steel":
|
|
62
|
+
return createSteelProvider();
|
|
59
63
|
case "libretto-cloud":
|
|
60
64
|
console.warn("Note: The libretto-cloud provider is in alpha.");
|
|
61
65
|
return createLibrettoCloudProvider();
|
|
62
66
|
default:
|
|
63
67
|
throw new Error(
|
|
64
|
-
`Unknown provider "${name}". Valid cloud providers: kernel, browserbase`,
|
|
68
|
+
`Unknown provider "${name}". Valid cloud providers: kernel, browserbase, steel`,
|
|
65
69
|
);
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ProviderApi } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_STEEL_API_ENDPOINT = "https://api.steel.dev";
|
|
4
|
+
const DEFAULT_STEEL_CONNECT_ENDPOINT = "wss://connect.steel.dev";
|
|
5
|
+
|
|
6
|
+
type SteelSessionResponse = {
|
|
7
|
+
id: string;
|
|
8
|
+
sessionViewerUrl?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SteelProviderOptions = {
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createSteelProvider(
|
|
16
|
+
options: SteelProviderOptions = {},
|
|
17
|
+
): ProviderApi {
|
|
18
|
+
const apiKey = options.apiKey ?? process.env.STEEL_API_KEY;
|
|
19
|
+
if (!apiKey) throw new Error("STEEL_API_KEY is required for Steel provider.");
|
|
20
|
+
|
|
21
|
+
const endpoint = process.env.STEEL_BASE_URL ?? DEFAULT_STEEL_API_ENDPOINT;
|
|
22
|
+
const connectEndpoint =
|
|
23
|
+
process.env.STEEL_CONNECT_URL ?? DEFAULT_STEEL_CONNECT_ENDPOINT;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
async createSession() {
|
|
27
|
+
const resp = await fetch(`${endpoint}/v1/sessions`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"steel-api-key": apiKey,
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({}),
|
|
34
|
+
});
|
|
35
|
+
if (!resp.ok) {
|
|
36
|
+
const body = await resp.text();
|
|
37
|
+
throw new Error(`Steel API error (${resp.status}): ${body}`);
|
|
38
|
+
}
|
|
39
|
+
const json = (await resp.json()) as SteelSessionResponse;
|
|
40
|
+
return {
|
|
41
|
+
sessionId: json.id,
|
|
42
|
+
cdpEndpoint: buildSteelCdpEndpoint(connectEndpoint, apiKey, json.id),
|
|
43
|
+
liveViewUrl: json.sessionViewerUrl,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
async closeSession(sessionId) {
|
|
47
|
+
const resp = await fetch(`${endpoint}/v1/sessions/${sessionId}/release`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
"steel-api-key": apiKey,
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({}),
|
|
54
|
+
});
|
|
55
|
+
if (!resp.ok) {
|
|
56
|
+
const body = await resp.text();
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Steel API error closing session ${sessionId} (${resp.status}): ${body}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return {};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildSteelCdpEndpoint(
|
|
67
|
+
connectEndpoint: string,
|
|
68
|
+
apiKey: string,
|
|
69
|
+
sessionId: string,
|
|
70
|
+
): string {
|
|
71
|
+
const endpoint = new URL(connectEndpoint);
|
|
72
|
+
endpoint.searchParams.set("apiKey", apiKey);
|
|
73
|
+
endpoint.searchParams.set("sessionId", sessionId);
|
|
74
|
+
return endpoint.toString();
|
|
75
|
+
}
|