runline 0.9.0 → 0.10.0
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/plugins/steel/src/browser.js +175 -0
- package/dist/plugins/steel/src/captchas.js +19 -0
- package/dist/plugins/steel/src/credentials.js +38 -0
- package/dist/plugins/steel/src/extensions.js +46 -0
- package/dist/plugins/steel/src/files.js +96 -0
- package/dist/plugins/steel/src/index.js +21 -374
- package/dist/plugins/steel/src/profiles.js +55 -0
- package/dist/plugins/steel/src/sessions.js +119 -0
- package/dist/plugins/steel/src/shared.js +72 -0
- package/package.json +9 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { SESSION_OPTIONS_SCHEMA, api, apiKey, compactRecord } from "./shared.js";
|
|
3
|
+
const SCRAPE_SCHEMA = {
|
|
4
|
+
url: t.String({ description: "URL to scrape" }),
|
|
5
|
+
format: t.Optional(t.Array(t.String(), { description: "Formats: html, cleaned_html, markdown, readability" })),
|
|
6
|
+
delay: t.Optional(t.Number({ description: "Milliseconds to wait after navigation" })),
|
|
7
|
+
useProxy: t.Optional(t.Any({ description: "true or proxy config" })),
|
|
8
|
+
screenshot: t.Optional(t.Boolean({ description: "Also capture a screenshot URL" })),
|
|
9
|
+
pdf: t.Optional(t.Boolean({ description: "Also capture a PDF URL" })),
|
|
10
|
+
};
|
|
11
|
+
const SCREENSHOT_SCHEMA = {
|
|
12
|
+
url: t.String({ description: "URL to screenshot" }),
|
|
13
|
+
fullPage: t.Optional(t.Boolean({ description: "Capture full scrollable page" })),
|
|
14
|
+
delay: t.Optional(t.Number({ description: "Milliseconds to wait after navigation" })),
|
|
15
|
+
useProxy: t.Optional(t.Any({ description: "true or proxy config" })),
|
|
16
|
+
};
|
|
17
|
+
async function scrape(input, ctx) {
|
|
18
|
+
return api(ctx, "/v1/scrape", { method: "POST", body: compactRecord(input) });
|
|
19
|
+
}
|
|
20
|
+
async function screenshot(input, ctx) {
|
|
21
|
+
return api(ctx, "/v1/screenshot", { method: "POST", body: compactRecord(input) });
|
|
22
|
+
}
|
|
23
|
+
async function connectMiniCdp(cdpUrl) {
|
|
24
|
+
const ws = new WebSocket(cdpUrl);
|
|
25
|
+
let nextId = 0;
|
|
26
|
+
const pending = new Map();
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
const timer = setTimeout(() => reject(new Error("CDP websocket connection timed out")), 30000);
|
|
29
|
+
ws.addEventListener("open", () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
30
|
+
ws.addEventListener("error", () => { clearTimeout(timer); reject(new Error("CDP websocket connection failed")); }, { once: true });
|
|
31
|
+
});
|
|
32
|
+
ws.addEventListener("message", (event) => {
|
|
33
|
+
let message;
|
|
34
|
+
try {
|
|
35
|
+
message = JSON.parse(String(event.data));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (typeof message.id !== "number")
|
|
41
|
+
return;
|
|
42
|
+
const wait = pending.get(message.id);
|
|
43
|
+
if (!wait)
|
|
44
|
+
return;
|
|
45
|
+
pending.delete(message.id);
|
|
46
|
+
if (message.error)
|
|
47
|
+
wait.reject(new Error(JSON.stringify(message.error)));
|
|
48
|
+
else
|
|
49
|
+
wait.resolve(message.result);
|
|
50
|
+
});
|
|
51
|
+
const send = (method, params = {}, sessionId) => new Promise((resolve, reject) => {
|
|
52
|
+
const id = ++nextId;
|
|
53
|
+
pending.set(id, { resolve, reject });
|
|
54
|
+
ws.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
if (pending.delete(id))
|
|
57
|
+
reject(new Error(`CDP ${method} timed out`));
|
|
58
|
+
}, 30000);
|
|
59
|
+
});
|
|
60
|
+
const targets = await send("Target.getTargets");
|
|
61
|
+
const target = targets.targetInfos?.find((info) => info.type === "page") ?? targets.targetInfos?.[0];
|
|
62
|
+
if (!target)
|
|
63
|
+
throw new Error("Steel CDP session has no browser target");
|
|
64
|
+
const attached = await send("Target.attachToTarget", { targetId: target.targetId, flatten: true });
|
|
65
|
+
const sid = attached.sessionId;
|
|
66
|
+
const evaluate = async (expression) => {
|
|
67
|
+
const result = await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }, sid);
|
|
68
|
+
return result.result?.value;
|
|
69
|
+
};
|
|
70
|
+
const page = {
|
|
71
|
+
async goto(url, _options) {
|
|
72
|
+
await send("Page.navigate", { url }, sid);
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
74
|
+
return null;
|
|
75
|
+
},
|
|
76
|
+
title: () => evaluate("document.title"),
|
|
77
|
+
url: () => evaluate("location.href"),
|
|
78
|
+
text: () => evaluate("document.body?.innerText ?? ''"),
|
|
79
|
+
html: () => evaluate("document.documentElement?.outerHTML ?? ''"),
|
|
80
|
+
evaluate: (expression) => evaluate(`(${expression})()`),
|
|
81
|
+
};
|
|
82
|
+
return { page, browser: { close: () => ws.close() }, context: {}, close: () => ws.close() };
|
|
83
|
+
}
|
|
84
|
+
export function registerBrowserActions(rl) {
|
|
85
|
+
rl.registerAction("scrape", {
|
|
86
|
+
description: "One-shot Steel scrape. Loads a URL and returns requested formats such as markdown, html, cleaned_html, or readability.",
|
|
87
|
+
inputSchema: t.Object(SCRAPE_SCHEMA),
|
|
88
|
+
execute: scrape,
|
|
89
|
+
});
|
|
90
|
+
rl.registerAction("browser.scrape", {
|
|
91
|
+
description: "Backward-compatible alias for scrape.",
|
|
92
|
+
inputSchema: t.Object(SCRAPE_SCHEMA),
|
|
93
|
+
execute: scrape,
|
|
94
|
+
});
|
|
95
|
+
rl.registerAction("screenshot", {
|
|
96
|
+
description: "One-shot Steel screenshot. Returns a hosted PNG URL.",
|
|
97
|
+
inputSchema: t.Object(SCREENSHOT_SCHEMA),
|
|
98
|
+
execute: screenshot,
|
|
99
|
+
});
|
|
100
|
+
rl.registerAction("browser.screenshot", {
|
|
101
|
+
description: "Backward-compatible alias for screenshot.",
|
|
102
|
+
inputSchema: t.Object(SCREENSHOT_SCHEMA),
|
|
103
|
+
execute: screenshot,
|
|
104
|
+
});
|
|
105
|
+
rl.registerAction("browser.extract", {
|
|
106
|
+
description: "Fetch a page through Steel scrape and return selected content fields. Use selectors with browser.run for DOM-specific extraction.",
|
|
107
|
+
inputSchema: t.Object({
|
|
108
|
+
url: t.String({ description: "URL to scrape" }),
|
|
109
|
+
format: t.Optional(t.Array(t.String(), { description: "Formats to request; defaults to markdown and html" })),
|
|
110
|
+
delay: t.Optional(t.Number({ description: "Milliseconds to wait after navigation" })),
|
|
111
|
+
useProxy: t.Optional(t.Any({ description: "true or proxy config" })),
|
|
112
|
+
}),
|
|
113
|
+
async execute(input, ctx) {
|
|
114
|
+
const body = { format: ["markdown", "html"], ...input };
|
|
115
|
+
return api(ctx, "/v1/scrape", { method: "POST", body: compactRecord(body) });
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
rl.registerAction("pdf", {
|
|
119
|
+
description: "One-shot Steel PDF capture. Returns a hosted PDF URL.",
|
|
120
|
+
inputSchema: t.Object({
|
|
121
|
+
url: t.String({ description: "URL to render as PDF" }),
|
|
122
|
+
delay: t.Optional(t.Number({ description: "Milliseconds to wait after navigation" })),
|
|
123
|
+
useProxy: t.Optional(t.Any({ description: "true or proxy config" })),
|
|
124
|
+
}),
|
|
125
|
+
async execute(input, ctx) {
|
|
126
|
+
return api(ctx, "/v1/pdf", { method: "POST", body: compactRecord(input) });
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
rl.registerAction("browser.run", {
|
|
130
|
+
description: "Create a Steel session, connect with Playwright over CDP, run an async JavaScript script, then release by default. The script receives { page, browser, context, session }. Requires the host app to have playwright installed.",
|
|
131
|
+
inputSchema: t.Object({
|
|
132
|
+
script: t.String({ description: "Async JavaScript body. Example: await page.goto('https://example.com'); return { title: await page.title() };" }),
|
|
133
|
+
release: t.Optional(t.Boolean({ description: "Release the Steel session after the script finishes (default true)" })),
|
|
134
|
+
...SESSION_OPTIONS_SCHEMA,
|
|
135
|
+
}),
|
|
136
|
+
async execute(input, ctx) {
|
|
137
|
+
const { script, release, ...sessionOptions } = input;
|
|
138
|
+
let playwright;
|
|
139
|
+
try {
|
|
140
|
+
playwright = await import("playwright");
|
|
141
|
+
}
|
|
142
|
+
catch (_error) {
|
|
143
|
+
throw new Error("steel.browser.run requires the host project to install playwright. Install playwright or use session.create + session.cdpUrl instead.");
|
|
144
|
+
}
|
|
145
|
+
const session = await api(ctx, "/v1/sessions", { method: "POST", body: compactRecord(sessionOptions) });
|
|
146
|
+
const cdpUrl = `wss://connect.steel.dev?apiKey=${encodeURIComponent(apiKey(ctx))}&sessionId=${encodeURIComponent(String(session.id))}`;
|
|
147
|
+
let browser;
|
|
148
|
+
try {
|
|
149
|
+
let context;
|
|
150
|
+
let page;
|
|
151
|
+
try {
|
|
152
|
+
const playwrightBrowser = await playwright.chromium.connectOverCDP(cdpUrl, { timeout: 30000 });
|
|
153
|
+
browser = playwrightBrowser;
|
|
154
|
+
context = playwrightBrowser.contexts()[0] ?? await playwrightBrowser.newContext();
|
|
155
|
+
page = context.pages()[0] ?? await context.newPage();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
const mini = await connectMiniCdp(cdpUrl);
|
|
159
|
+
browser = mini.browser;
|
|
160
|
+
context = mini.context;
|
|
161
|
+
page = mini.page;
|
|
162
|
+
}
|
|
163
|
+
const fn = new Function("page", "browser", "context", "session", `return (async () => {\n${script}\n})();`);
|
|
164
|
+
const result = await fn(page, browser, context, session);
|
|
165
|
+
return { session, result };
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await browser?.close()?.catch?.(() => { });
|
|
169
|
+
if (release !== false && session.id) {
|
|
170
|
+
await api(ctx, `/v1/sessions/${encodeURIComponent(String(session.id))}/release`, { method: "POST" }).catch(() => { });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { api, compactRecord } from "./shared.js";
|
|
3
|
+
export function registerCaptchaActions(rl) {
|
|
4
|
+
rl.registerAction("captcha.status", {
|
|
5
|
+
description: "Get CAPTCHA detection/solving status for a Steel session.",
|
|
6
|
+
inputSchema: t.Object({ sessionId: t.String() }),
|
|
7
|
+
async execute(input, ctx) { return api(ctx, `/v1/sessions/${encodeURIComponent(input.sessionId)}/captchas/status`); },
|
|
8
|
+
});
|
|
9
|
+
rl.registerAction("captcha.solve", {
|
|
10
|
+
description: "Trigger CAPTCHA solving for all detected CAPTCHAs or a specific task/url/page.",
|
|
11
|
+
inputSchema: t.Object({ sessionId: t.String(), taskId: t.Optional(t.String()), url: t.Optional(t.String()), pageId: t.Optional(t.String()) }),
|
|
12
|
+
async execute(input, ctx) { const { sessionId, ...body } = input; return api(ctx, `/v1/sessions/${encodeURIComponent(String(sessionId))}/captchas/solve`, { method: "POST", body: compactRecord(body) }); },
|
|
13
|
+
});
|
|
14
|
+
rl.registerAction("captcha.solveImage", {
|
|
15
|
+
description: "Solve an image CAPTCHA by XPath selectors.",
|
|
16
|
+
inputSchema: t.Object({ sessionId: t.String(), imageXPath: t.String(), inputXPath: t.String(), url: t.Optional(t.String()) }),
|
|
17
|
+
async execute(input, ctx) { const { sessionId, ...body } = input; return api(ctx, `/v1/sessions/${encodeURIComponent(String(sessionId))}/captchas/solve-image`, { method: "POST", body: compactRecord(body) }); },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { api, compactRecord } from "./shared.js";
|
|
3
|
+
const CREDENTIAL_KEY_SCHEMA = {
|
|
4
|
+
origin: t.String({ description: "Credential origin" }),
|
|
5
|
+
namespace: t.Optional(t.String({ description: "Credential namespace (defaults to Steel default)" })),
|
|
6
|
+
};
|
|
7
|
+
export function registerCredentialActions(rl) {
|
|
8
|
+
rl.registerAction("credential.list", {
|
|
9
|
+
description: "List Steel credentials. Filter by origin and/or namespace.",
|
|
10
|
+
inputSchema: t.Object({ namespace: t.Optional(t.String()), origin: t.Optional(t.String()) }),
|
|
11
|
+
async execute(input, ctx) {
|
|
12
|
+
return api(ctx, "/v1/credentials", { query: input });
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
rl.registerAction("credential.create", {
|
|
16
|
+
description: "Create a Steel credential for an origin/namespace. Value may include username, password, and totpSecret.",
|
|
17
|
+
inputSchema: t.Object({ ...CREDENTIAL_KEY_SCHEMA, value: t.Any({ description: "Credential payload" }) }),
|
|
18
|
+
async execute(input, ctx) {
|
|
19
|
+
return api(ctx, "/v1/credentials", { method: "POST", body: compactRecord(input) });
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
rl.registerAction("credential.get", {
|
|
23
|
+
description: "Retrieve credential metadata by origin and optional namespace.",
|
|
24
|
+
inputSchema: t.Object(CREDENTIAL_KEY_SCHEMA),
|
|
25
|
+
async execute(input, ctx) {
|
|
26
|
+
const result = await api(ctx, "/v1/credentials", { query: compactRecord(input) });
|
|
27
|
+
const credentials = result.credentials;
|
|
28
|
+
return Array.isArray(credentials) ? (credentials[0] ?? null) : null;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
rl.registerAction("credential.delete", {
|
|
32
|
+
description: "Delete a Steel credential by origin and optional namespace.",
|
|
33
|
+
inputSchema: t.Object(CREDENTIAL_KEY_SCHEMA),
|
|
34
|
+
async execute(input, ctx) {
|
|
35
|
+
return api(ctx, "/v1/credentials", { method: "DELETE", body: compactRecord(input) });
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { api } from "./shared.js";
|
|
3
|
+
function extensionForm(input) {
|
|
4
|
+
const form = new FormData();
|
|
5
|
+
if (input.url !== undefined && input.url !== null)
|
|
6
|
+
form.set("url", String(input.url));
|
|
7
|
+
return form;
|
|
8
|
+
}
|
|
9
|
+
export function registerExtensionActions(rl) {
|
|
10
|
+
rl.registerAction("extension.list", {
|
|
11
|
+
description: "List Steel Chrome extensions installed for the organization.",
|
|
12
|
+
inputSchema: t.Object({}),
|
|
13
|
+
async execute(_input, ctx) {
|
|
14
|
+
return api(ctx, "/v1/extensions");
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
rl.registerAction("extension.upload", {
|
|
18
|
+
description: "Upload an extension from a Chrome Web Store URL. Raw zip/crx uploads should use the API directly.",
|
|
19
|
+
inputSchema: t.Object({ url: t.String() }),
|
|
20
|
+
async execute(input, ctx) {
|
|
21
|
+
return api(ctx, "/v1/extensions", { method: "POST", body: extensionForm(input) });
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
rl.registerAction("extension.update", {
|
|
25
|
+
description: "Update an extension from a Chrome Web Store URL.",
|
|
26
|
+
inputSchema: t.Object({ id: t.String(), url: t.String() }),
|
|
27
|
+
async execute(input, ctx) {
|
|
28
|
+
const { id, ...body } = input;
|
|
29
|
+
return api(ctx, `/v1/extensions/${encodeURIComponent(String(id))}`, { method: "PUT", body: extensionForm(body) });
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
rl.registerAction("extension.delete", {
|
|
33
|
+
description: "Delete an extension by ID.",
|
|
34
|
+
inputSchema: t.Object({ id: t.String() }),
|
|
35
|
+
async execute(input, ctx) {
|
|
36
|
+
return api(ctx, `/v1/extensions/${encodeURIComponent(input.id)}`, { method: "DELETE" });
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
rl.registerAction("extension.deleteAll", {
|
|
40
|
+
description: "Delete all organization extensions.",
|
|
41
|
+
inputSchema: t.Object({}),
|
|
42
|
+
async execute(_input, ctx) {
|
|
43
|
+
return api(ctx, "/v1/extensions", { method: "DELETE" });
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { api } from "./shared.js";
|
|
3
|
+
function fileSchema() {
|
|
4
|
+
return {
|
|
5
|
+
file: t.String({ description: "Global/session file path or absolute URL. Raw local file upload is not supported through Runline JSON actions." }),
|
|
6
|
+
path: t.Optional(t.String({ description: "Destination path" })),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function fileForm(input) {
|
|
10
|
+
const form = new FormData();
|
|
11
|
+
form.set("file", String(input.file));
|
|
12
|
+
if (input.path !== undefined && input.path !== null && input.path !== "")
|
|
13
|
+
form.set("path", String(input.path));
|
|
14
|
+
return form;
|
|
15
|
+
}
|
|
16
|
+
function normalizeFilePath(path) {
|
|
17
|
+
return String(path).replace(/^\/files\/+/, "").replace(/^\/+/, "");
|
|
18
|
+
}
|
|
19
|
+
function encodeFilePath(path) {
|
|
20
|
+
return normalizeFilePath(path).split("/").map(encodeURIComponent).join("/");
|
|
21
|
+
}
|
|
22
|
+
export function registerFileActions(rl) {
|
|
23
|
+
rl.registerAction("file.list", {
|
|
24
|
+
description: "List global Steel files.",
|
|
25
|
+
inputSchema: t.Object({}),
|
|
26
|
+
async execute(_input, ctx) {
|
|
27
|
+
return api(ctx, "/v1/files");
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
rl.registerAction("file.upload", {
|
|
31
|
+
description: "Upload a global file from a URL or existing path reference.",
|
|
32
|
+
inputSchema: t.Object(fileSchema()),
|
|
33
|
+
async execute(input, ctx) {
|
|
34
|
+
return api(ctx, "/v1/files", { method: "POST", body: fileForm(input) });
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
rl.registerAction("file.download", {
|
|
38
|
+
description: "Download/read a global file by path. Binary files are returned as text by fetch when possible; use the URL/API directly for raw bytes.",
|
|
39
|
+
inputSchema: t.Object({ path: t.String() }),
|
|
40
|
+
async execute(input, ctx) {
|
|
41
|
+
return api(ctx, `/v1/files/${encodeFilePath(input.path)}`);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
rl.registerAction("file.delete", {
|
|
45
|
+
description: "Delete a global Steel file by path.",
|
|
46
|
+
inputSchema: t.Object({ path: t.String() }),
|
|
47
|
+
async execute(input, ctx) {
|
|
48
|
+
return api(ctx, `/v1/files/${encodeFilePath(input.path)}`, { method: "DELETE" });
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
rl.registerAction("sessionFile.list", {
|
|
52
|
+
description: "List files in a Steel session filesystem.",
|
|
53
|
+
inputSchema: t.Object({ sessionId: t.String() }),
|
|
54
|
+
async execute(input, ctx) {
|
|
55
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(input.sessionId)}/files`);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
rl.registerAction("sessionFile.upload", {
|
|
59
|
+
description: "Upload/copy a URL or global file into a session filesystem.",
|
|
60
|
+
inputSchema: t.Object({ sessionId: t.String(), ...fileSchema() }),
|
|
61
|
+
async execute(input, ctx) {
|
|
62
|
+
const { sessionId, ...body } = input;
|
|
63
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(String(sessionId))}/files`, { method: "POST", body: fileForm(body) });
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
rl.registerAction("sessionFile.download", {
|
|
67
|
+
description: "Download/read a session file by path.",
|
|
68
|
+
inputSchema: t.Object({ sessionId: t.String(), path: t.String() }),
|
|
69
|
+
async execute(input, ctx) {
|
|
70
|
+
const { sessionId, path } = input;
|
|
71
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(String(sessionId))}/files/${encodeFilePath(path)}`);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
rl.registerAction("sessionFile.downloadArchive", {
|
|
75
|
+
description: "Download/read the zip archive of all files in a session.",
|
|
76
|
+
inputSchema: t.Object({ sessionId: t.String() }),
|
|
77
|
+
async execute(input, ctx) {
|
|
78
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(input.sessionId)}/files.zip`);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
rl.registerAction("sessionFile.delete", {
|
|
82
|
+
description: "Delete a file from a session filesystem.",
|
|
83
|
+
inputSchema: t.Object({ sessionId: t.String(), path: t.String() }),
|
|
84
|
+
async execute(input, ctx) {
|
|
85
|
+
const { sessionId, path } = input;
|
|
86
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(String(sessionId))}/files/${encodeFilePath(path)}`, { method: "DELETE" });
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
rl.registerAction("sessionFile.deleteAll", {
|
|
90
|
+
description: "Delete all files in a session filesystem.",
|
|
91
|
+
inputSchema: t.Object({ sessionId: t.String() }),
|
|
92
|
+
async execute(input, ctx) {
|
|
93
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(input.sessionId)}/files`, { method: "DELETE" });
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -1,378 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
function clampWait(value, fallback = 9000, max = 45000) {
|
|
11
|
-
const n = Number(value);
|
|
12
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
13
|
-
return fallback;
|
|
14
|
-
return Math.min(Math.floor(n), max);
|
|
15
|
-
}
|
|
16
|
-
function getConnectionConfig(ctx) {
|
|
17
|
-
const cfg = (ctx?.connection?.config ?? {});
|
|
18
|
-
return {
|
|
19
|
-
steelApiKey: compactText(cfg.steelApiKey),
|
|
20
|
-
steelBaseUrl: compactText(cfg.steelBaseUrl || DEFAULT_STEEL_BASE).replace(/\/+$/, ""),
|
|
21
|
-
steelProxyUrl: compactText(cfg.steelProxyUrl),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
class SteelClient {
|
|
25
|
-
apiKey;
|
|
26
|
-
baseUrl;
|
|
27
|
-
proxyUrl;
|
|
28
|
-
constructor(cfg) {
|
|
29
|
-
if (!cfg.steelApiKey) {
|
|
30
|
-
throw new Error("Missing STEEL_API_KEY. Configure it before using the steel browser plugin.");
|
|
31
|
-
}
|
|
32
|
-
this.apiKey = cfg.steelApiKey;
|
|
33
|
-
this.baseUrl = cfg.steelBaseUrl || DEFAULT_STEEL_BASE;
|
|
34
|
-
this.proxyUrl = cfg.steelProxyUrl;
|
|
35
|
-
}
|
|
36
|
-
async request(method, pathname, body) {
|
|
37
|
-
const resp = await fetch(`${this.baseUrl}${pathname}`, {
|
|
38
|
-
method,
|
|
39
|
-
headers: { "steel-api-key": this.apiKey, "content-type": "application/json" },
|
|
40
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
41
|
-
});
|
|
42
|
-
const text = await resp.text();
|
|
43
|
-
if (!resp.ok) {
|
|
44
|
-
throw new Error(`Steel ${method} ${pathname} -> ${resp.status}: ${text.slice(0, 300)}`);
|
|
45
|
-
}
|
|
46
|
-
return text ? JSON.parse(text) : {};
|
|
47
|
-
}
|
|
48
|
-
async createSession({ useProxy = false, timeout = 180000 } = {}) {
|
|
49
|
-
const body = { timeout };
|
|
50
|
-
if (useProxy) {
|
|
51
|
-
if (!this.proxyUrl)
|
|
52
|
-
throw new Error("useProxy requested but STEEL_PROXY_URL is not configured.");
|
|
53
|
-
body.proxyUrl = this.proxyUrl;
|
|
54
|
-
}
|
|
55
|
-
return this.request("POST", "/v1/sessions", body);
|
|
56
|
-
}
|
|
57
|
-
async release(sessionId) {
|
|
58
|
-
try {
|
|
59
|
-
return await this.request("POST", `/v1/sessions/${sessionId}/release`);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return { success: false };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// Steel's hosted screenshot API stores the image on images.steel.dev and
|
|
66
|
-
// returns a PUBLIC URL (no auth) that renders inline in chat — the bytes
|
|
67
|
-
// never have to traverse the agent runtime (which strips large base64).
|
|
68
|
-
async hostedScreenshot({ url, fullPage = false, delay, useProxy = false }) {
|
|
69
|
-
const body = { url };
|
|
70
|
-
if (fullPage)
|
|
71
|
-
body.fullPage = true;
|
|
72
|
-
if (Number.isFinite(delay) && delay > 0)
|
|
73
|
-
body.delay = delay;
|
|
74
|
-
if (useProxy) {
|
|
75
|
-
if (!this.proxyUrl)
|
|
76
|
-
throw new Error("useProxy requested but STEEL_PROXY_URL is not configured.");
|
|
77
|
-
body.proxyUrl = this.proxyUrl;
|
|
78
|
-
}
|
|
79
|
-
const res = await this.request("POST", "/v1/screenshot", body);
|
|
80
|
-
return res?.url || null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
class CdpConnection {
|
|
84
|
-
wsUrl;
|
|
85
|
-
nextId = 0;
|
|
86
|
-
pending = new Map();
|
|
87
|
-
ws = null;
|
|
88
|
-
constructor(wsUrl) {
|
|
89
|
-
this.wsUrl = wsUrl;
|
|
90
|
-
}
|
|
91
|
-
connect() {
|
|
92
|
-
return new Promise((resolve, reject) => {
|
|
93
|
-
if (typeof WebSocket === "undefined") {
|
|
94
|
-
reject(new Error("Global WebSocket is not available in this runtime; the steel browser plugin needs Node 22+ / Bun."));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const ws = new WebSocket(this.wsUrl);
|
|
98
|
-
this.ws = ws;
|
|
99
|
-
const onError = () => reject(new Error("Steel CDP websocket connection failed"));
|
|
100
|
-
ws.addEventListener("open", () => {
|
|
101
|
-
ws.removeEventListener("error", onError);
|
|
102
|
-
resolve();
|
|
103
|
-
});
|
|
104
|
-
ws.addEventListener("error", onError);
|
|
105
|
-
ws.addEventListener("close", () => {
|
|
106
|
-
for (const [, p] of this.pending)
|
|
107
|
-
p.reject(new Error("Steel CDP connection closed"));
|
|
108
|
-
this.pending.clear();
|
|
109
|
-
});
|
|
110
|
-
ws.addEventListener("message", (ev) => {
|
|
111
|
-
let msg;
|
|
112
|
-
try {
|
|
113
|
-
msg = JSON.parse(typeof ev.data === "string" ? ev.data : String(ev.data));
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (msg.id != null && this.pending.has(msg.id)) {
|
|
119
|
-
const p = this.pending.get(msg.id);
|
|
120
|
-
this.pending.delete(msg.id);
|
|
121
|
-
if (msg.error)
|
|
122
|
-
p.reject(new Error(msg.error.message || "CDP error"));
|
|
123
|
-
else
|
|
124
|
-
p.resolve(msg.result);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
send(method, params = {}, sessionId, timeoutMs = 45000) {
|
|
130
|
-
const requestId = ++this.nextId;
|
|
131
|
-
const payload = { id: requestId, method, params };
|
|
132
|
-
if (sessionId)
|
|
133
|
-
payload.sessionId = sessionId;
|
|
134
|
-
return new Promise((resolve, reject) => {
|
|
135
|
-
this.pending.set(requestId, { resolve, reject });
|
|
136
|
-
try {
|
|
137
|
-
this.ws.send(JSON.stringify(payload));
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
this.pending.delete(requestId);
|
|
141
|
-
reject(err);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
setTimeout(() => {
|
|
145
|
-
if (this.pending.has(requestId)) {
|
|
146
|
-
this.pending.delete(requestId);
|
|
147
|
-
reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms`));
|
|
148
|
-
}
|
|
149
|
-
}, timeoutMs);
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
close() {
|
|
153
|
-
try {
|
|
154
|
-
this.ws?.close();
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
/* noop */
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
class PageDriver {
|
|
162
|
-
conn;
|
|
163
|
-
sessionId;
|
|
164
|
-
constructor(conn, sessionId) {
|
|
165
|
-
this.conn = conn;
|
|
166
|
-
this.sessionId = sessionId;
|
|
167
|
-
}
|
|
168
|
-
static async attachFirstPage(conn) {
|
|
169
|
-
const { targetInfos } = await conn.send("Target.getTargets");
|
|
170
|
-
let target = (targetInfos || []).find((t) => t.type === "page");
|
|
171
|
-
if (!target) {
|
|
172
|
-
const created = await conn.send("Target.createTarget", { url: "about:blank" });
|
|
173
|
-
target = { targetId: created.targetId };
|
|
174
|
-
}
|
|
175
|
-
const { sessionId } = await conn.send("Target.attachToTarget", { targetId: target.targetId, flatten: true });
|
|
176
|
-
const driver = new PageDriver(conn, sessionId);
|
|
177
|
-
await conn.send("Page.enable", {}, sessionId);
|
|
178
|
-
await conn.send("Runtime.enable", {}, sessionId);
|
|
179
|
-
return driver;
|
|
180
|
-
}
|
|
181
|
-
async navigate(url) {
|
|
182
|
-
await this.conn.send("Page.navigate", { url }, this.sessionId);
|
|
183
|
-
}
|
|
184
|
-
async eval(expression) {
|
|
185
|
-
const r = await this.conn.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }, this.sessionId);
|
|
186
|
-
if (r.exceptionDetails) {
|
|
187
|
-
throw new Error(`page eval failed: ${r.exceptionDetails.text || r.exceptionDetails.exception?.description || "exception"}`);
|
|
188
|
-
}
|
|
189
|
-
return r.result?.value;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// Render a URL in a Steel browser and run a callback with the live PageDriver.
|
|
193
|
-
async function withRenderedPage(cfg, { url, useProxy = false, waitMs = 9000, waitSelector }, fn) {
|
|
194
|
-
const steel = new SteelClient(cfg);
|
|
195
|
-
const session = await steel.createSession({ useProxy });
|
|
196
|
-
const wsUrl = `${session.websocketUrl}${session.websocketUrl.includes("?") ? "&" : "?"}apiKey=${encodeURIComponent(steel.apiKey)}`;
|
|
197
|
-
const conn = new CdpConnection(wsUrl);
|
|
198
|
-
try {
|
|
199
|
-
await conn.connect();
|
|
200
|
-
const page = await PageDriver.attachFirstPage(conn);
|
|
201
|
-
await page.navigate(url);
|
|
202
|
-
await sleep(Math.min(waitMs, 6000));
|
|
203
|
-
if (waitSelector) {
|
|
204
|
-
const deadline = Date.now() + Math.max(0, waitMs - 6000);
|
|
205
|
-
while (Date.now() < deadline) {
|
|
206
|
-
const found = await page.eval(`!!document.querySelector(${JSON.stringify(waitSelector)})`);
|
|
207
|
-
if (found)
|
|
208
|
-
break;
|
|
209
|
-
await sleep(1500);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
else if (waitMs > 6000) {
|
|
213
|
-
await sleep(waitMs - 6000);
|
|
214
|
-
}
|
|
215
|
-
return await fn(page, { sessionId: session.id, viewer: session.sessionViewerUrl });
|
|
216
|
-
}
|
|
217
|
-
finally {
|
|
218
|
-
conn.close();
|
|
219
|
-
await steel.release(session.id);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
function robotWallExpr() {
|
|
223
|
-
return `/are you a human|verify you('| a)re not a robot|not a robot|enable javascript|access denied|unusual traffic/i.test((document.body && document.body.innerText || '').slice(0, 4000))`;
|
|
224
|
-
}
|
|
225
|
-
// ── Plugin registration ──────────────────────────────────────────────
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { registerBrowserActions } from "./browser.js";
|
|
3
|
+
import { registerCaptchaActions } from "./captchas.js";
|
|
4
|
+
import { registerCredentialActions } from "./credentials.js";
|
|
5
|
+
import { registerExtensionActions } from "./extensions.js";
|
|
6
|
+
import { registerFileActions } from "./files.js";
|
|
7
|
+
import { registerProfileActions } from "./profiles.js";
|
|
8
|
+
import { registerSessionActions } from "./sessions.js";
|
|
226
9
|
export default function steel(rl) {
|
|
227
|
-
rl.setName(
|
|
10
|
+
rl.setName("steel");
|
|
228
11
|
rl.setVersion("0.1.0");
|
|
229
|
-
rl.setConnectionSchema({
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
required: true,
|
|
12
|
+
rl.setConnectionSchema(t.Object({
|
|
13
|
+
apiKey: t.String({
|
|
14
|
+
description: "Steel API key (https://app.steel.dev/settings/api-keys)",
|
|
233
15
|
env: "STEEL_API_KEY",
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
steelProxyUrl: {
|
|
244
|
-
type: "string",
|
|
245
|
-
required: false,
|
|
246
|
-
env: "STEEL_PROXY_URL",
|
|
247
|
-
description: "Residential proxy URL, used only when an action passes useProxy:true. Store only in secrets.",
|
|
248
|
-
},
|
|
249
|
-
});
|
|
250
|
-
rl.registerAction("browser.scrape", {
|
|
251
|
-
description: "Render a URL in a real Steel cloud browser (executes JavaScript, passes 'verify you're not a robot' walls) and return its content. Use this instead of plain fetch for JS-heavy or anti-bot pages. No proxy by default; set useProxy:true only for datacenter-IP-blocked surfaces.",
|
|
252
|
-
inputSchema: {
|
|
253
|
-
url: { type: "string", required: true, description: "Absolute URL to render." },
|
|
254
|
-
format: { type: "string", required: false, default: "text", description: "text (visible innerText), html (rendered outerHTML), or links (anchor list)." },
|
|
255
|
-
waitMs: { type: "number", required: false, default: 9000, description: "How long to let the page render (ms, max 45000)." },
|
|
256
|
-
waitSelector: { type: "string", required: false, description: "Optional CSS selector to wait for before extracting." },
|
|
257
|
-
maxChars: { type: "number", required: false, default: 20000, description: "Cap on returned content length." },
|
|
258
|
-
useProxy: { type: "boolean", required: false, default: false, description: "Route through the residential proxy. Only needed for datacenter-blocked sites." },
|
|
259
|
-
},
|
|
260
|
-
async execute(input, ctx) {
|
|
261
|
-
const cfg = getConnectionConfig(ctx);
|
|
262
|
-
const url = compactText(input.url);
|
|
263
|
-
if (!/^https?:\/\//i.test(url))
|
|
264
|
-
throw new Error("url must be an absolute http(s) URL");
|
|
265
|
-
const format = compactText(input.format || "text").toLowerCase();
|
|
266
|
-
const maxChars = Number(input.maxChars) > 0 ? Math.min(Number(input.maxChars), 200000) : 20000;
|
|
267
|
-
return withRenderedPage(cfg, { url, useProxy: input.useProxy === true, waitMs: clampWait(input.waitMs), waitSelector: input.waitSelector }, async (page, meta) => {
|
|
268
|
-
const title = await page.eval("document.title");
|
|
269
|
-
const finalUrl = await page.eval("location.href");
|
|
270
|
-
const robotWall = await page.eval(robotWallExpr());
|
|
271
|
-
let content;
|
|
272
|
-
if (format === "html")
|
|
273
|
-
content = await page.eval("document.documentElement.outerHTML");
|
|
274
|
-
else if (format === "links")
|
|
275
|
-
content = JSON.stringify(await page.eval(`Array.from(document.querySelectorAll('a[href]')).slice(0,300).map(function(a){return {text:(a.innerText||'').replace(/\\s+/g,' ').trim().slice(0,80), href:a.href};})`));
|
|
276
|
-
else
|
|
277
|
-
content = await page.eval("document.body && document.body.innerText || ''");
|
|
278
|
-
return {
|
|
279
|
-
url,
|
|
280
|
-
finalUrl,
|
|
281
|
-
title,
|
|
282
|
-
robotWall,
|
|
283
|
-
format,
|
|
284
|
-
truncated: String(content).length > maxChars,
|
|
285
|
-
content: String(content).slice(0, maxChars),
|
|
286
|
-
viewerUrl: meta.viewer,
|
|
287
|
-
source: "steel.browser.scrape",
|
|
288
|
-
};
|
|
289
|
-
});
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
rl.registerAction("browser.extract", {
|
|
293
|
-
description: "Render a URL in a Steel browser and extract structured data via a map of CSS selectors. Returns the first match (or all matches with all:true) of each selector as text.",
|
|
294
|
-
inputSchema: {
|
|
295
|
-
url: { type: "string", required: true, description: "Absolute URL to render." },
|
|
296
|
-
selectors: { type: "object", required: true, description: "Map of { fieldName: cssSelector }." },
|
|
297
|
-
all: { type: "boolean", required: false, default: false, description: "Return all matches per selector instead of the first." },
|
|
298
|
-
waitMs: { type: "number", required: false, default: 9000, description: "Render wait (ms)." },
|
|
299
|
-
waitSelector: { type: "string", required: false, description: "Optional CSS selector to wait for." },
|
|
300
|
-
useProxy: { type: "boolean", required: false, default: false, description: "Route through the residential proxy." },
|
|
301
|
-
},
|
|
302
|
-
async execute(input, ctx) {
|
|
303
|
-
const cfg = getConnectionConfig(ctx);
|
|
304
|
-
const url = compactText(input.url);
|
|
305
|
-
if (!/^https?:\/\//i.test(url))
|
|
306
|
-
throw new Error("url must be an absolute http(s) URL");
|
|
307
|
-
const selectors = input.selectors && typeof input.selectors === "object" ? input.selectors : null;
|
|
308
|
-
if (!selectors || !Object.keys(selectors).length)
|
|
309
|
-
throw new Error("selectors must be a non-empty object of { name: cssSelector }");
|
|
310
|
-
const all = input.all === true;
|
|
311
|
-
return withRenderedPage(cfg, { url, useProxy: input.useProxy === true, waitMs: clampWait(input.waitMs), waitSelector: input.waitSelector }, async (page, meta) => {
|
|
312
|
-
const expr = `
|
|
313
|
-
(function(){
|
|
314
|
-
var sels = ${JSON.stringify(selectors)};
|
|
315
|
-
var all = ${all};
|
|
316
|
-
function txt(el){ return el ? (el.innerText||el.textContent||'').replace(/\\s+/g,' ').trim() : null; }
|
|
317
|
-
var out = {};
|
|
318
|
-
for (var k in sels){
|
|
319
|
-
if (all){ out[k] = Array.from(document.querySelectorAll(sels[k])).slice(0,50).map(txt); }
|
|
320
|
-
else { out[k] = txt(document.querySelector(sels[k])); }
|
|
321
|
-
}
|
|
322
|
-
return out;
|
|
323
|
-
})()`;
|
|
324
|
-
return {
|
|
325
|
-
url,
|
|
326
|
-
finalUrl: await page.eval("location.href"),
|
|
327
|
-
title: await page.eval("document.title"),
|
|
328
|
-
robotWall: await page.eval(robotWallExpr()),
|
|
329
|
-
data: await page.eval(expr),
|
|
330
|
-
viewerUrl: meta.viewer,
|
|
331
|
-
source: "steel.browser.extract",
|
|
332
|
-
};
|
|
333
|
-
});
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
rl.registerAction("browser.screenshot", {
|
|
337
|
-
description: "Capture a screenshot of a URL with the Steel cloud browser and return imageUrl — a PUBLIC, login-free PNG link (https://images.steel.dev/...). To 'send a screenshot', put imageUrl in your reply: WhatsApp/Slack render it inline and anyone can open it without a Steel account. base64 is opt-in and usually pointless (the agent runtime strips large base64 from action results), so prefer imageUrl.",
|
|
338
|
-
inputSchema: {
|
|
339
|
-
url: { type: "string", required: true, description: "Absolute URL to capture." },
|
|
340
|
-
fullPage: { type: "boolean", required: false, default: false, description: "Capture the full scrollable page." },
|
|
341
|
-
waitMs: { type: "number", required: false, default: 0, description: "Milliseconds to wait before capturing (for JS-heavy pages). 0 = capture as soon as loaded." },
|
|
342
|
-
useProxy: { type: "boolean", required: false, default: false, description: "Route through the BYO residential proxy (STEEL_PROXY_URL). Only for datacenter-IP-blocked surfaces." },
|
|
343
|
-
includeBase64: { type: "boolean", required: false, default: false, description: "Also fetch the hosted image and return raw PNG base64. Off by default — share imageUrl instead." },
|
|
344
|
-
},
|
|
345
|
-
async execute(input, ctx) {
|
|
346
|
-
const cfg = getConnectionConfig(ctx);
|
|
347
|
-
const url = compactText(input.url);
|
|
348
|
-
if (!/^https?:\/\//i.test(url))
|
|
349
|
-
throw new Error("url must be an absolute http(s) URL");
|
|
350
|
-
const steel = new SteelClient(cfg);
|
|
351
|
-
const imageUrl = await steel.hostedScreenshot({
|
|
352
|
-
url,
|
|
353
|
-
fullPage: input.fullPage === true,
|
|
354
|
-
delay: clampWait(input.waitMs, 0, 30000) || undefined,
|
|
355
|
-
useProxy: input.useProxy === true,
|
|
356
|
-
});
|
|
357
|
-
const out = {
|
|
358
|
-
url,
|
|
359
|
-
imageUrl,
|
|
360
|
-
mimeType: "image/png",
|
|
361
|
-
note: "Share imageUrl — it's a public, login-free screenshot link that renders inline in chat. Put it in your reply text; don't screenshot-to-base64.",
|
|
362
|
-
source: "steel.browser.screenshot",
|
|
363
|
-
};
|
|
364
|
-
if (input.includeBase64 === true && imageUrl) {
|
|
365
|
-
try {
|
|
366
|
-
const resp = await fetch(imageUrl);
|
|
367
|
-
const buf = Buffer.from(await resp.arrayBuffer());
|
|
368
|
-
out.byteLength = buf.length;
|
|
369
|
-
out.base64 = buf.toString("base64");
|
|
370
|
-
}
|
|
371
|
-
catch (e) {
|
|
372
|
-
out.base64Error = String(e?.message || e);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return out;
|
|
376
|
-
},
|
|
377
|
-
});
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
registerSessionActions(rl);
|
|
19
|
+
registerBrowserActions(rl);
|
|
20
|
+
registerFileActions(rl);
|
|
21
|
+
registerCredentialActions(rl);
|
|
22
|
+
registerProfileActions(rl);
|
|
23
|
+
registerExtensionActions(rl);
|
|
24
|
+
registerCaptchaActions(rl);
|
|
378
25
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { api } from "./shared.js";
|
|
3
|
+
function profileForm(input) {
|
|
4
|
+
const form = new FormData();
|
|
5
|
+
for (const [key, value] of Object.entries(input)) {
|
|
6
|
+
if (value === undefined || value === null || value === "")
|
|
7
|
+
continue;
|
|
8
|
+
form.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
9
|
+
}
|
|
10
|
+
return form;
|
|
11
|
+
}
|
|
12
|
+
export function registerProfileActions(rl) {
|
|
13
|
+
rl.registerAction("profile.list", {
|
|
14
|
+
description: "List Steel browser profiles.",
|
|
15
|
+
inputSchema: t.Object({}),
|
|
16
|
+
async execute(_input, ctx) {
|
|
17
|
+
return api(ctx, "/v1/profiles");
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
rl.registerAction("profile.get", {
|
|
21
|
+
description: "Get a Steel profile by ID.",
|
|
22
|
+
inputSchema: t.Object({ id: t.String() }),
|
|
23
|
+
async execute(input, ctx) {
|
|
24
|
+
return api(ctx, `/v1/profiles/${encodeURIComponent(input.id)}`);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
rl.registerAction("profile.create", {
|
|
28
|
+
description: "Create an empty persisted Steel profile by opening and releasing a short-lived session with persistProfile=true. For userDataDir archive imports, use the Steel API directly.",
|
|
29
|
+
inputSchema: t.Object({
|
|
30
|
+
timeout: t.Optional(t.Number({ description: "Temporary session timeout in milliseconds" })),
|
|
31
|
+
inactivityTimeout: t.Optional(t.Number({ description: "Temporary session inactivity timeout in milliseconds" })),
|
|
32
|
+
}),
|
|
33
|
+
async execute(input, ctx) {
|
|
34
|
+
const session = await api(ctx, "/v1/sessions", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
body: { timeout: 60000, inactivityTimeout: 30000, ...input, persistProfile: true },
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
await api(ctx, `/v1/sessions/${encodeURIComponent(String(session.id))}/release`, { method: "POST" });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Profile creation is tied to session release. Return the session metadata even if release cleanup fails.
|
|
43
|
+
}
|
|
44
|
+
return { profileId: session.profileId, session };
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
rl.registerAction("profile.update", {
|
|
48
|
+
description: "Update profile metadata/settings used by later sessions.",
|
|
49
|
+
inputSchema: t.Object({ id: t.String(), userAgent: t.Optional(t.String()), proxy: t.Optional(t.Any()), metadata: t.Optional(t.Any()) }),
|
|
50
|
+
async execute(input, ctx) {
|
|
51
|
+
const { id, ...body } = input;
|
|
52
|
+
return api(ctx, `/v1/profiles/${encodeURIComponent(String(id))}`, { method: "PATCH", body: profileForm(body) });
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { LIST_INPUT_SCHEMA, SESSION_OPTIONS_SCHEMA, api, apiKey, compactRecord } from "./shared.js";
|
|
3
|
+
export function registerSessionActions(rl) {
|
|
4
|
+
rl.registerAction("session.create", {
|
|
5
|
+
description: "Create a Steel browser session. Returns session id, websocketUrl/CDP URL, debug/viewer URLs, and profile metadata when present.",
|
|
6
|
+
inputSchema: t.Object(SESSION_OPTIONS_SCHEMA),
|
|
7
|
+
async execute(input, ctx) {
|
|
8
|
+
return api(ctx, "/v1/sessions", { method: "POST", body: compactRecord((input ?? {})) });
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
rl.registerAction("session.list", {
|
|
12
|
+
description: "List Steel sessions.",
|
|
13
|
+
inputSchema: t.Object(LIST_INPUT_SCHEMA),
|
|
14
|
+
async execute(input, ctx) {
|
|
15
|
+
return api(ctx, "/v1/sessions", { query: (input ?? {}) });
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
rl.registerAction("session.get", {
|
|
19
|
+
description: "Get a Steel session by ID.",
|
|
20
|
+
inputSchema: t.Object({ id: t.String({ description: "Session ID" }) }),
|
|
21
|
+
async execute(input, ctx) {
|
|
22
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(input.id)}`);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
rl.registerAction("session.release", {
|
|
26
|
+
description: "Release a Steel session when work is done.",
|
|
27
|
+
inputSchema: t.Object({ id: t.String({ description: "Session ID" }) }),
|
|
28
|
+
async execute(input, ctx) {
|
|
29
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(input.id)}/release`, { method: "POST" });
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
rl.registerAction("session.releaseAll", {
|
|
33
|
+
description: "Release all live Steel sessions for the organization by listing sessions and releasing each live session individually.",
|
|
34
|
+
inputSchema: t.Object({}),
|
|
35
|
+
async execute(_input, ctx) {
|
|
36
|
+
const listed = await api(ctx, "/v1/sessions");
|
|
37
|
+
const sessions = Array.isArray(listed.sessions) ? listed.sessions : [];
|
|
38
|
+
const live = sessions.filter((session) => session.status === "live" || session.status === "LIVE");
|
|
39
|
+
const released = [];
|
|
40
|
+
const failed = [];
|
|
41
|
+
for (const session of live) {
|
|
42
|
+
const id = String(session.id);
|
|
43
|
+
try {
|
|
44
|
+
released.push({ id, result: await api(ctx, `/v1/sessions/${encodeURIComponent(id)}/release`, { method: "POST" }) });
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
failed.push({ id, error: String(error.message ?? error) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { released, failed, count: released.length };
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
rl.registerAction("session.context", {
|
|
54
|
+
description: "Capture cookies/localStorage context from a live session. Treat output as sensitive auth material.",
|
|
55
|
+
inputSchema: t.Object({ id: t.String({ description: "Session ID" }) }),
|
|
56
|
+
async execute(input, ctx) {
|
|
57
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(input.id)}/context`);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
rl.registerAction("session.traces", {
|
|
61
|
+
description: "Fetch the Agent Traces timeline for a Steel session. Supports optional ISO startTime/endTime filters.",
|
|
62
|
+
inputSchema: t.Object({
|
|
63
|
+
id: t.String({ description: "Session ID" }),
|
|
64
|
+
startTime: t.Optional(t.String({ description: "ISO timestamp lower bound" })),
|
|
65
|
+
endTime: t.Optional(t.String({ description: "ISO timestamp upper bound" })),
|
|
66
|
+
}),
|
|
67
|
+
async execute(input, ctx) {
|
|
68
|
+
const { id, ...query } = input;
|
|
69
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(String(id))}/agent-traces`, { query });
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
rl.registerAction("session.computer", {
|
|
73
|
+
description: "Execute a Steel computer-use action against a live session. Useful for model-native computer-use loops; pass actions such as take_screenshot, click_mouse, type_text, press_key, scroll, or drag_mouse.",
|
|
74
|
+
inputSchema: t.Object({
|
|
75
|
+
id: t.String({ description: "Session ID" }),
|
|
76
|
+
action: t.String({ description: "Steel computer action, e.g. take_screenshot, click_mouse, type_text, press_key, scroll, drag_mouse" }),
|
|
77
|
+
button: t.Optional(t.String({ description: "Mouse button for click actions" })),
|
|
78
|
+
coordinates: t.Optional(t.Array(t.Number(), { description: "[x, y] pixel coordinates" })),
|
|
79
|
+
text: t.Optional(t.String({ description: "Text for type_text or key for press_key" })),
|
|
80
|
+
keys: t.Optional(t.Array(t.String(), { description: "Keys for keyboard actions when supported" })),
|
|
81
|
+
deltaX: t.Optional(t.Number({ description: "Horizontal scroll delta" })),
|
|
82
|
+
deltaY: t.Optional(t.Number({ description: "Vertical scroll delta" })),
|
|
83
|
+
path: t.Optional(t.Array(t.Any(), { description: "Drag path points when supported" })),
|
|
84
|
+
screenshot: t.Optional(t.Boolean({ description: "Return a screenshot after the action" })),
|
|
85
|
+
}),
|
|
86
|
+
async execute(input, ctx) {
|
|
87
|
+
const { id, deltaX, deltaY, ...body } = input;
|
|
88
|
+
const payload = compactRecord({ ...body, delta_x: deltaX, delta_y: deltaY });
|
|
89
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(String(id))}/computer`, { method: "POST", body: payload });
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
rl.registerAction("session.events", {
|
|
93
|
+
description: "Fetch legacy recorded session events for replay tooling.",
|
|
94
|
+
inputSchema: t.Object({ id: t.String({ description: "Session ID" }) }),
|
|
95
|
+
async execute(input, ctx) {
|
|
96
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(input.id)}/events`);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
rl.registerAction("session.hls", {
|
|
100
|
+
description: "Fetch the HLS playlist for a recorded headful Steel session.",
|
|
101
|
+
inputSchema: t.Object({ id: t.String({ description: "Session ID" }) }),
|
|
102
|
+
async execute(input, ctx) {
|
|
103
|
+
return api(ctx, `/v1/sessions/${encodeURIComponent(input.id)}/hls`);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
rl.registerAction("session.cdpUrl", {
|
|
107
|
+
description: "Build a Playwright/Puppeteer CDP URL for a Steel session using the configured API key.",
|
|
108
|
+
inputSchema: t.Object({ id: t.String({ description: "Session ID" }), websocketUrl: t.Optional(t.String({ description: "Optional websocketUrl returned by session.create. If omitted, uses wss://connect.steel.dev with sessionId." })) }),
|
|
109
|
+
async execute(input, ctx) {
|
|
110
|
+
const { id, websocketUrl } = input;
|
|
111
|
+
const key = encodeURIComponent(apiKey(ctx));
|
|
112
|
+
if (websocketUrl) {
|
|
113
|
+
const sep = websocketUrl.includes("?") ? "&" : "?";
|
|
114
|
+
return { cdpUrl: `${websocketUrl}${sep}apiKey=${key}` };
|
|
115
|
+
}
|
|
116
|
+
return { cdpUrl: `wss://connect.steel.dev?apiKey=${key}&sessionId=${encodeURIComponent(id)}` };
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
const BASE_URL = "https://api.steel.dev";
|
|
3
|
+
export function apiKey(ctx) {
|
|
4
|
+
return ctx.connection.config.apiKey;
|
|
5
|
+
}
|
|
6
|
+
export async function api(ctx, path, options = {}) {
|
|
7
|
+
const url = new URL(path, BASE_URL);
|
|
8
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
9
|
+
if (value === undefined || value === null || value === "")
|
|
10
|
+
continue;
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
for (const item of value)
|
|
13
|
+
url.searchParams.append(key, String(item));
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
url.searchParams.set(key, String(value));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const headers = {
|
|
20
|
+
"steel-api-key": apiKey(ctx),
|
|
21
|
+
...(options.headers ?? {}),
|
|
22
|
+
};
|
|
23
|
+
let body;
|
|
24
|
+
if (options.body !== undefined) {
|
|
25
|
+
if (options.body instanceof FormData) {
|
|
26
|
+
body = options.body;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
headers["Content-Type"] = "application/json";
|
|
30
|
+
body = JSON.stringify(options.body);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const res = await fetch(url.toString(), {
|
|
34
|
+
method: options.method ?? "GET",
|
|
35
|
+
headers,
|
|
36
|
+
body,
|
|
37
|
+
});
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
if (!res.ok)
|
|
40
|
+
throw new Error(`Steel API error ${res.status}: ${text || res.statusText}`);
|
|
41
|
+
if (!text)
|
|
42
|
+
return {};
|
|
43
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
44
|
+
if (contentType.includes("application/json") || text.startsWith("{") || text.startsWith("["))
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
export function compactRecord(input) {
|
|
49
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null));
|
|
50
|
+
}
|
|
51
|
+
export const LIST_INPUT_SCHEMA = {
|
|
52
|
+
limit: t.Optional(t.Number({ description: "Maximum number of results when supported" })),
|
|
53
|
+
cursor: t.Optional(t.String({ description: "Pagination cursor when supported" })),
|
|
54
|
+
};
|
|
55
|
+
export const SESSION_OPTIONS_SCHEMA = {
|
|
56
|
+
timeout: t.Optional(t.Number({ description: "Session hard timeout in milliseconds" })),
|
|
57
|
+
inactivityTimeout: t.Optional(t.Number({ description: "Release after this many milliseconds of inactivity" })),
|
|
58
|
+
useProxy: t.Optional(t.Any({ description: "true for Steel managed proxy, or proxy config object" })),
|
|
59
|
+
solveCaptcha: t.Optional(t.Boolean({ description: "Enable CAPTCHA detection/solving" })),
|
|
60
|
+
region: t.Optional(t.String({ description: "Steel region, e.g. lax or iad" })),
|
|
61
|
+
namespace: t.Optional(t.String({ description: "Credential namespace to use for this session" })),
|
|
62
|
+
userAgent: t.Optional(t.String({ description: "Custom browser user agent" })),
|
|
63
|
+
dimensions: t.Optional(t.Any({ description: "Viewport dimensions, e.g. { width: 1280, height: 768 }" })),
|
|
64
|
+
stealthConfig: t.Optional(t.Any({ description: "Steel stealth configuration, e.g. { autoCaptchaSolving: false }" })),
|
|
65
|
+
deviceConfig: t.Optional(t.Any({ description: "Device config, e.g. { device: 'mobile' }" })),
|
|
66
|
+
profileId: t.Optional(t.String({ description: "Profile ID to load" })),
|
|
67
|
+
persistProfile: t.Optional(t.Boolean({ description: "Persist profile changes on release" })),
|
|
68
|
+
credentials: t.Optional(t.Any({ description: "Credentials injection options, or {} to enable defaults" })),
|
|
69
|
+
extensionIds: t.Optional(t.Array(t.String(), { description: "Extension IDs to attach, or ['all_ext']" })),
|
|
70
|
+
sessionContext: t.Optional(t.Any({ description: "Captured session context to restore" })),
|
|
71
|
+
isSelenium: t.Optional(t.Boolean({ description: "Provision a Selenium-compatible session" })),
|
|
72
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Code mode for agents — turn any API or command into a callable action",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -62,6 +62,14 @@
|
|
|
62
62
|
"minisearch": "^7.2.0",
|
|
63
63
|
"typescript": "^5.8.0"
|
|
64
64
|
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"playwright": "^1.57.0"
|
|
67
|
+
},
|
|
68
|
+
"peerDependenciesMeta": {
|
|
69
|
+
"playwright": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
65
73
|
"dependencies": {
|
|
66
74
|
"chalk": "^5.6.2",
|
|
67
75
|
"commander": "^14.0.3",
|