runline 0.9.0 → 0.11.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.
@@ -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
- const NAME = "steel";
2
- const DEFAULT_STEEL_BASE = "https://api.steel.dev";
3
- // ── Helpers ──────────────────────────────────────────────────────────
4
- function compactText(value) {
5
- return String(value ?? "").replace(/\s+/g, " ").trim();
6
- }
7
- function sleep(ms) {
8
- return new Promise((resolve) => setTimeout(resolve, ms));
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(NAME);
10
+ rl.setName("steel");
228
11
  rl.setVersion("0.1.0");
229
- rl.setConnectionSchema({
230
- steelApiKey: {
231
- type: "string",
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
- description: "Steel.dev API key for the cloud browser. Store only in secrets.",
235
- },
236
- steelBaseUrl: {
237
- type: "string",
238
- required: false,
239
- env: "STEEL_API_BASE",
240
- default: DEFAULT_STEEL_BASE,
241
- description: "Steel.dev API base URL.",
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
+ }