TypeScript SDK: skeleton (#4236)

This commit is contained in:
Stanislav Novosad
2025-12-10 11:12:47 -07:00
committed by GitHub
parent 86ec31f556
commit 2be36c7738
14 changed files with 1240 additions and 17 deletions

View File

@@ -30,6 +30,8 @@ groups:
repository: Skyvern-AI/skyvern-typescript repository: Skyvern-AI/skyvern-typescript
config: config:
namespaceExport: Skyvern namespaceExport: Skyvern
extraDependencies:
playwright: '^1.48.0'
packageJson: packageJson:
description: "The Skyvern TypeScript library provides convenient access to the Skyvern APIs from TypeScript." description: "The Skyvern TypeScript library provides convenient access to the Skyvern APIs from TypeScript."
publishConfig: publishConfig:

View File

@@ -1,19 +1,35 @@
#!/usr/bin/env bash #!/usr/bin/env bash
CURRENT_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') #CURRENT_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
fern generate --group ts-sdk --log-level debug --version "$CURRENT_VERSION" --preview fern generate --group ts-sdk --log-level debug --version "$CURRENT_VERSION" --preview
(cd fern/.preview/fern-typescript-sdk \
&& npm install \
&& npx tsc --project ./tsconfig.cjs.json \
&& npx tsc --project ./tsconfig.esm.json \
&& node scripts/rename-to-esm-files.js dist/esm)
rm -fr skyvern-ts/client
mkdir -p skyvern-ts/client mkdir -p skyvern-ts/client
mv skyvern-ts/client/src/library skyvern-ts/library
rm -rf skyvern-ts/client
mkdir -p skyvern-ts/client/src/library
mv skyvern-ts/library skyvern-ts/client/src/
cp -rf fern/.preview/fern-typescript-sdk/* skyvern-ts/client/ cp -rf fern/.preview/fern-typescript-sdk/* skyvern-ts/client/
# Post-processing: Update repository references the monorepo # Post-processing: Update repository references the monorepo
sed -i.bak 's|Skyvern-AI/skyvern-typescript|Skyvern-AI/skyvern|g' skyvern-ts/client/package.json sed -i.bak 's|Skyvern-AI/skyvern-typescript|Skyvern-AI/skyvern|g' skyvern-ts/client/package.json
sed -i.bak 's|https://github.com/Skyvern-AI/skyvern-typescript/blob/HEAD/./reference.md|https://www.skyvern.com/docs/api-reference/api-reference|g' skyvern-ts/client/README.md sed -i.bak 's|https://github.com/Skyvern-AI/skyvern-typescript/blob/HEAD/./reference.md|https://www.skyvern.com/docs/api-reference/api-reference|g' skyvern-ts/client/README.md
rm -f skyvern-ts/client/package.json.bak skyvern-ts/client/README.md.bak rm -f skyvern-ts/client/package.json.bak skyvern-ts/client/README.md.bak
# Export library classes from main index
cat >> skyvern-ts/client/src/index.ts << 'EOF'
export { Skyvern, SkyvernBrowser, SkyvernBrowserPageAgent, SkyvernBrowserPageAi } from "./library/index.js";
export type { SkyvernOptions, SkyvernBrowserPage } from "./library/index.js";
EOF
# Rename the API namespace to avoid conflict with Skyvern class
sed -i.bak 's/export \* as Skyvern from/export * as SkyvernApi from/g' skyvern-ts/client/src/index.ts
rm -f skyvern-ts/client/src/index.ts.bak
(cd skyvern-ts/client \
&& rm -rf node_modules package-lock.json \
&& npm install \
&& npx tsc --project ./tsconfig.cjs.json \
&& npx tsc --project ./tsconfig.esm.json \
&& node scripts/rename-to-esm-files.js dist/esm)
pre-commit run --all-files

View File

@@ -7,6 +7,9 @@
"": { "": {
"name": "@skyvern/client", "name": "@skyvern/client",
"version": "1.0.2", "version": "1.0.2",
"dependencies": {
"playwright": "^1.48.0"
},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.2.5", "@biomejs/biome": "2.2.5",
"@types/node": "^18.19.70", "@types/node": "^18.19.70",
@@ -1660,9 +1663,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001759", "version": "1.0.30001760",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
"integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2044,10 +2047,9 @@
} }
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -2410,6 +2412,36 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.57.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -3073,6 +3105,21 @@
} }
} }
}, },
"node_modules/vite-node/node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/vite-node/node_modules/picomatch": { "node_modules/vite-node/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
@@ -3288,6 +3335,21 @@
} }
} }
}, },
"node_modules/vitest/node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/vitest/node_modules/picomatch": { "node_modules/vitest/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",

View File

@@ -46,6 +46,9 @@
"test:unit": "vitest --project unit", "test:unit": "vitest --project unit",
"test:wire": "vitest --project wire" "test:wire": "vitest --project wire"
}, },
"dependencies": {
"playwright": "^1.48.0"
},
"devDependencies": { "devDependencies": {
"webpack": "^5.97.1", "webpack": "^5.97.1",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",

View File

@@ -4,6 +4,10 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
importers: importers:
.: .:
dependencies:
playwright:
specifier: ^1.48.0
version: 1.57.0
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: 2.2.5 specifier: 2.2.5
@@ -555,6 +559,10 @@ packages:
fill-range@7.1.1: fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'} engines: {node: '>=8'}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -648,6 +656,14 @@ packages:
picomatch@4.0.3: picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
playwright-core@1.57.0:
resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==}
engines: {node: '>=18'}
hasBin: true
playwright@1.57.0:
resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==}
engines: {node: '>=18'}
hasBin: true
postcss@8.5.6: postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@@ -1313,6 +1329,8 @@ snapshots:
fill-range@7.1.1: fill-range@7.1.1:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
fsevents@2.3.2:
optional: true
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
@@ -1383,6 +1401,12 @@ snapshots:
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@2.3.1: {} picomatch@2.3.1: {}
picomatch@4.0.3: {} picomatch@4.0.3: {}
playwright-core@1.57.0: {}
playwright@1.57.0:
dependencies:
playwright-core: 1.57.0
optionalDependencies:
fsevents: 2.3.2
postcss@8.5.6: postcss@8.5.6:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11

View File

@@ -1,5 +1,7 @@
export * as Skyvern from "./api/index.js"; export * as SkyvernApi from "./api/index.js";
export type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; export type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js";
export { SkyvernClient } from "./Client.js"; export { SkyvernClient } from "./Client.js";
export { SkyvernEnvironment } from "./environments.js"; export { SkyvernEnvironment } from "./environments.js";
export { SkyvernError, SkyvernTimeoutError } from "./errors/index.js"; export { SkyvernError, SkyvernTimeoutError } from "./errors/index.js";
export { Skyvern, SkyvernBrowser, SkyvernBrowserPageAgent, SkyvernBrowserPageAi } from "./library/index.js";
export type { SkyvernOptions, SkyvernBrowserPage } from "./library/index.js";

View File

@@ -0,0 +1,279 @@
import { chromium } from "playwright";
import type * as SkyvernApi from "../api/index.js";
import type { BaseClientOptions } from "../BaseClient.js";
import { SkyvernClient } from "../Client.js";
import { SkyvernEnvironment } from "../environments.js";
import { SkyvernBrowser } from "./SkyvernBrowser.js";
import type { GetRunResponse, ProxyLocation } from "../api/index.js";
import { LOG } from "./logger.js";
import * as core from "../core/index.js";
export interface SkyvernOptions extends BaseClientOptions {
apiKey: string;
}
export interface RunTaskOptions extends SkyvernApi.RunTaskRequest {
waitForCompletion?: boolean;
timeout?: number;
}
export interface RunWorkflowOptions extends SkyvernApi.RunWorkflowRequest {
waitForCompletion?: boolean;
timeout?: number;
}
export interface LoginOptions extends SkyvernApi.LoginRequest {
waitForCompletion?: boolean;
timeout?: number;
}
export interface DownloadFilesOptions extends SkyvernApi.DownloadFilesRequest {
waitForCompletion?: boolean;
timeout?: number;
}
export class Skyvern extends SkyvernClient {
private readonly _apiKey: string;
private readonly _environment: SkyvernEnvironment | string;
private readonly _browsers: Set<SkyvernBrowser> = new Set();
constructor(options: SkyvernOptions) {
super({
...options,
environment: options.environment ?? SkyvernEnvironment.Cloud,
});
this._apiKey = options.apiKey;
this._environment = (options.environment ?? SkyvernEnvironment.Cloud) as SkyvernEnvironment | string;
}
get environment(): SkyvernEnvironment | string {
return this._environment;
}
runTask(
request: RunTaskOptions,
requestOptions?: SkyvernClient.RequestOptions,
): core.HttpResponsePromise<SkyvernApi.TaskRunResponse> {
return core.HttpResponsePromise.fromPromise(this.__runTaskWithCompletion(request, requestOptions));
}
private async __runTaskWithCompletion(
request: RunTaskOptions,
requestOptions?: SkyvernClient.RequestOptions,
): Promise<core.WithRawResponse<SkyvernApi.TaskRunResponse>> {
const { waitForCompletion, timeout, ...taskRequest } = request;
const response = await super.runTask(taskRequest, requestOptions).withRawResponse();
if (waitForCompletion) {
const completedRun = await this._waitForRunCompletion(
response.data.run_id,
timeout ?? 1800,
) as SkyvernApi.TaskRunResponse;
return { data: completedRun, rawResponse: response.rawResponse };
}
return response;
}
runWorkflow(
request: RunWorkflowOptions,
requestOptions?: SkyvernClient.RequestOptions,
): core.HttpResponsePromise<SkyvernApi.WorkflowRunResponse> {
return core.HttpResponsePromise.fromPromise(this.__runWorkflowWithCompletion(request, requestOptions));
}
private async __runWorkflowWithCompletion(
request: RunWorkflowOptions,
requestOptions?: SkyvernClient.RequestOptions,
): Promise<core.WithRawResponse<SkyvernApi.WorkflowRunResponse>> {
const { waitForCompletion, timeout, ...workflowRequest } = request;
const response = await super.runWorkflow(workflowRequest, requestOptions).withRawResponse();
if (waitForCompletion) {
const completedRun = await this._waitForRunCompletion(
response.data.run_id,
timeout ?? 1800,
) as SkyvernApi.WorkflowRunResponse;
return { data: completedRun, rawResponse: response.rawResponse };
}
return response;
}
login(
request: LoginOptions,
requestOptions?: SkyvernClient.RequestOptions,
): core.HttpResponsePromise<SkyvernApi.WorkflowRunResponse> {
return core.HttpResponsePromise.fromPromise(this.__loginWithCompletion(request, requestOptions));
}
private async __loginWithCompletion(
request: LoginOptions,
requestOptions?: SkyvernClient.RequestOptions,
): Promise<core.WithRawResponse<SkyvernApi.WorkflowRunResponse>> {
const { waitForCompletion, timeout, ...loginRequest } = request;
const response = await super.login(loginRequest, requestOptions).withRawResponse();
if (waitForCompletion) {
const completedRun = await this._waitForRunCompletion(
response.data.run_id,
timeout ?? 1800,
) as SkyvernApi.WorkflowRunResponse;
return { data: completedRun, rawResponse: response.rawResponse };
}
return response;
}
downloadFiles(
request: DownloadFilesOptions,
requestOptions?: SkyvernClient.RequestOptions,
): core.HttpResponsePromise<SkyvernApi.WorkflowRunResponse> {
return core.HttpResponsePromise.fromPromise(this.__downloadFilesWithCompletion(request, requestOptions));
}
private async __downloadFilesWithCompletion(
request: DownloadFilesOptions,
requestOptions?: SkyvernClient.RequestOptions,
): Promise<core.WithRawResponse<SkyvernApi.WorkflowRunResponse>> {
const { waitForCompletion, timeout, ...downloadFilesRequest } = request;
const response = await super.downloadFiles(downloadFilesRequest, requestOptions).withRawResponse();
if (waitForCompletion) {
const completedRun = await this._waitForRunCompletion(
response.data.run_id,
timeout ?? 1800,
) as SkyvernApi.WorkflowRunResponse;
return { data: completedRun, rawResponse: response.rawResponse };
}
return response;
}
async launchCloudBrowser(options?: {
timeout?: number;
proxyLocation?: SkyvernApi.ProxyLocation;
}): Promise<SkyvernBrowser> {
this._ensureCloudEnvironment();
const browserSession = await this.createBrowserSession({
timeout: options?.timeout,
proxy_location: options?.proxyLocation,
});
LOG.info("Launched new cloud browser session", { browser_session_id: browserSession.browser_session_id });
return this._connectToCloudBrowserSession(browserSession);
}
async connectToCloudBrowserSession(browserSessionId: string): Promise<SkyvernBrowser> {
this._ensureCloudEnvironment();
const browserSession = await this.getBrowserSession(browserSessionId);
LOG.info("Connecting to existing cloud browser session", { browser_session_id: browserSession.browser_session_id });
return this._connectToCloudBrowserSession(browserSession);
}
async useCloudBrowser(options?: { timeout?: number; proxyLocation?: ProxyLocation }): Promise<SkyvernBrowser> {
this._ensureCloudEnvironment();
const browserSessions = await this.getBrowserSessions();
const browserSession = browserSessions
.filter((s) => s.runnable_id == null)
.sort((a, b) => {
const aTime = a.started_at ? new Date(a.started_at).getTime() : 0;
const bTime = b.started_at ? new Date(b.started_at).getTime() : 0;
return bTime - aTime;
})[0];
if (!browserSession) {
LOG.info("No existing cloud browser session found, launching a new session");
return this.launchCloudBrowser(options);
}
LOG.info("Reusing existing cloud browser session", { browser_session_id: browserSession.browser_session_id });
return this._connectToCloudBrowserSession(browserSession);
}
async connectToBrowserOverCdp(cdpUrl: string): Promise<SkyvernBrowser> {
const browser = await chromium.connectOverCDP(cdpUrl);
const browserContext = browser.contexts()[0] ?? (await browser.newContext());
const skyvernBrowser = new SkyvernBrowser(this, browserContext, { browser, browserAddress: cdpUrl });
this._browsers.add(skyvernBrowser);
return skyvernBrowser;
}
async close(): Promise<void> {
await Promise.all(Array.from(this._browsers).map((browser) => browser.close()));
this._browsers.clear();
}
_untrackBrowser(browser: SkyvernBrowser): void {
this._browsers.delete(browser);
}
private _ensureCloudEnvironment(): void {
if (this._environment !== SkyvernEnvironment.Cloud && this._environment !== SkyvernEnvironment.Staging) {
throw new Error("Cloud browser sessions are supported only in the cloud environment");
}
}
private async _connectToCloudBrowserSession(
browserSession: SkyvernApi.BrowserSessionResponse,
): Promise<SkyvernBrowser> {
if (!browserSession.browser_address) {
throw new Error(`Browser address is missing for session ${browserSession.browser_session_id}`);
}
const browser = await chromium.connectOverCDP(browserSession.browser_address, {
headers: { "x-api-key": this._apiKey },
});
const browserContext = browser.contexts()[0] ?? (await browser.newContext());
const skyvernBrowser = new SkyvernBrowser(this, browserContext, {
browser,
browserSessionId: browserSession.browser_session_id,
});
this._browsers.add(skyvernBrowser);
return skyvernBrowser;
}
private async _waitForRunCompletion(runId: string, timeoutSeconds: number): Promise<GetRunResponse> {
const startTime = Date.now();
const timeoutMs = timeoutSeconds * 1000;
while (true) {
const run = await this.getRun(runId);
// Check if the run is in a final state
const status = run.status;
if (
status === "completed" ||
status === "failed" ||
status === "terminated" ||
status === "timed_out" ||
status === "canceled"
) {
return run;
}
// Check timeout
if (Date.now() - startTime >= timeoutMs) {
throw new Error(`Timeout waiting for run ${runId} to complete after ${timeoutSeconds} seconds`);
}
// Wait before polling again
await new Promise((resolve) => setTimeout(resolve, 10000)); // 10 seconds
}
}
}

View File

@@ -0,0 +1,78 @@
import type { Browser, BrowserContext, Page } from "playwright";
import type { Skyvern } from "./Skyvern.js";
import { SkyvernBrowserPageCore, type SkyvernBrowserPage } from "./SkyvernBrowserPage.js";
export class SkyvernBrowser {
private readonly _skyvern: Skyvern;
private readonly _browserContext: BrowserContext;
private readonly _browser?: Browser;
private readonly _browserSessionId?: string;
private readonly _browserAddress?: string;
public workflowRunId?: string;
constructor(
skyvern: Skyvern,
browserContext: BrowserContext,
options?: {
browser?: Browser;
browserSessionId?: string;
browserAddress?: string;
},
) {
this._skyvern = skyvern;
this._browserContext = browserContext;
this._browser = options?.browser;
this._browserSessionId = options?.browserSessionId;
this._browserAddress = options?.browserAddress;
}
get browserSessionId(): string | undefined {
return this._browserSessionId;
}
get browserAddress(): string | undefined {
return this._browserAddress;
}
get skyvern(): Skyvern {
return this._skyvern;
}
get context(): BrowserContext {
return this._browserContext;
}
async getWorkingPage(): Promise<SkyvernBrowserPage> {
const pages = this._browserContext.pages();
const page = pages.length > 0 ? pages[pages.length - 1] : await this._browserContext.newPage();
return this._createSkyvernPage(page);
}
async newPage(): Promise<SkyvernBrowserPage> {
const page = await this._browserContext.newPage();
return this._createSkyvernPage(page);
}
pages(): SkyvernBrowserPage[] {
return this._browserContext.pages().map((page) => SkyvernBrowserPageCore.create(this, page));
}
async close(): Promise<void> {
if (this._browser) {
await this._browser.close();
} else {
await this._browserContext.close();
}
if (this._browserSessionId) {
await this._skyvern.closeBrowserSession(this._browserSessionId);
}
this._skyvern._untrackBrowser(this);
}
private _createSkyvernPage(page: Page): SkyvernBrowserPage {
return SkyvernBrowserPageCore.create(this, page);
}
}

View File

@@ -0,0 +1,149 @@
import type { Page } from "playwright";
import type { SkyvernBrowser } from "./SkyvernBrowser.js";
import { SkyvernBrowserPageAgent } from "./SkyvernBrowserPageAgent.js";
import { SkyvernBrowserPageAi } from "./SkyvernBrowserPageAi.js";
export class SkyvernBrowserPageCore {
private readonly _browser: SkyvernBrowser;
private readonly _page: Page;
private readonly _ai: SkyvernBrowserPageAi;
public readonly agent: SkyvernBrowserPageAgent;
private readonly _proxy: SkyvernBrowserPage;
private constructor(browser: SkyvernBrowser, page: Page) {
this._browser = browser;
this._page = page;
this._ai = new SkyvernBrowserPageAi(browser, page);
this.agent = new SkyvernBrowserPageAgent(browser, page);
this._proxy = new Proxy(this, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
}
const value = Reflect.get(target._page, prop, target._page);
if (typeof value === "function") {
return value.bind(target._page);
}
return value;
},
}) as unknown as SkyvernBrowserPage;
}
static create(browser: SkyvernBrowser, page: Page): SkyvernBrowserPage {
const instance = new SkyvernBrowserPageCore(browser, page);
return instance._proxy;
}
get page(): Page {
return this._page;
}
get browser(): SkyvernBrowser {
return this._browser;
}
async click(selector: string, options?: Parameters<Page["click"]>[1]): Promise<void>;
async click(options: { prompt: string } & Partial<Parameters<Page["click"]>[1]>): Promise<void>;
async click(
selectorOrOptions: string | ({ prompt: string } & Partial<Parameters<Page["click"]>[1]>),
options?: Parameters<Page["click"]>[1],
): Promise<void> {
if (typeof selectorOrOptions === "string") {
return this._page.click(selectorOrOptions, options);
} else {
const { prompt, timeout, ...data } = selectorOrOptions;
await this._ai.aiClick({
intention: prompt,
timeout,
data: Object.keys(data).length > 0 ? data : undefined,
});
}
}
async fill(selector: string, value: string, options?: Parameters<Page["fill"]>[2]): Promise<void>;
async fill(options: { prompt: string; value?: string } & Partial<Parameters<Page["fill"]>[2]>): Promise<void>;
async fill(
selectorOrOptions: string | ({ prompt: string; value?: string } & Partial<Parameters<Page["fill"]>[2]>),
value?: string,
options?: Parameters<Page["fill"]>[2],
): Promise<void> {
if (typeof selectorOrOptions === "string") {
if (value === undefined) {
throw new Error("value is required when selector is provided");
}
return this._page.fill(selectorOrOptions, value, options);
} else {
const { prompt, value: fillValue, timeout, ...data } = selectorOrOptions;
await this._ai.aiInputText({
value: fillValue,
intention: prompt,
timeout,
data: Object.keys(data).length > 0 ? data : undefined,
});
}
}
async selectOption(
selector: string,
values: string | string[],
options?: Parameters<Page["selectOption"]>[2],
): Promise<string[]>;
async selectOption(
options: { prompt: string; value?: string } & Partial<Parameters<Page["selectOption"]>[2]>,
): Promise<string[]>;
async selectOption(
selectorOrOptions: string | ({ prompt: string; value?: string } & Partial<Parameters<Page["selectOption"]>[2]>),
values?: string | string[],
options?: Parameters<Page["selectOption"]>[2],
): Promise<string[]> {
if (typeof selectorOrOptions === "string") {
if (values === undefined) {
throw new Error("value is required when selector is provided");
}
return this._page.selectOption(selectorOrOptions, values, options);
} else {
const { prompt, value, timeout, ...data } = selectorOrOptions;
await this._ai.aiSelectOption({
value,
intention: prompt,
timeout,
data: Object.keys(data).length > 0 ? data : undefined,
});
return value ? [value] : [];
}
}
async act(prompt: string): Promise<void> {
return this._ai.aiAct(prompt);
}
async extract(options: {
prompt: string;
schema?: Record<string, unknown> | unknown[] | string;
errorCodeMapping?: Record<string, string>;
intention?: string;
data?: string | Record<string, unknown>;
}): Promise<Record<string, unknown> | unknown[] | string | null> {
return this._ai.aiExtract(options);
}
async validate(prompt: string, model?: Record<string, unknown> | string): Promise<boolean> {
const normalizedModel: Record<string, unknown> | undefined =
typeof model === "string" ? { modelName: model } : model;
return this._ai.aiValidate({ prompt, model: normalizedModel });
}
async prompt(
prompt: string,
schema?: Record<string, unknown>,
model?: Record<string, unknown> | string,
): Promise<Record<string, unknown> | unknown[] | string | null> {
const normalizedModel: Record<string, unknown> | undefined =
typeof model === "string" ? { modelName: model } : model;
return this._ai.aiPrompt({ prompt, schema, model: normalizedModel });
}
}
export type SkyvernBrowserPage = SkyvernBrowserPageCore & Page;

View File

@@ -0,0 +1,260 @@
import type { Page } from "playwright";
import type * as Skyvern from "../api/index.js";
import { SkyvernEnvironment } from "../environments.js";
import { DEFAULT_AGENT_HEARTBEAT_INTERVAL, DEFAULT_AGENT_TIMEOUT } from "./constants.js";
import type { SkyvernBrowser } from "./SkyvernBrowser.js";
import { LOG } from "./logger.js";
function getAppUrlForRun(runId: string): string {
return `https://app.skyvern.com/runs/${runId}`;
}
export class SkyvernBrowserPageAgent {
private readonly _browser: SkyvernBrowser;
private readonly _page: Page;
constructor(browser: SkyvernBrowser, page: Page) {
this._browser = browser;
this._page = page;
}
async runTask(
prompt: string,
options?: {
engine?: Skyvern.RunEngine;
model?: Record<string, unknown>;
url?: string;
webhookUrl?: string;
totpIdentifier?: string;
totpUrl?: string;
title?: string;
errorCodeMapping?: Record<string, string>;
dataExtractionSchema?: Record<string, unknown> | string;
maxSteps?: number;
timeout?: number;
},
): Promise<Skyvern.TaskRunResponse> {
LOG.info("AI run task", { prompt });
const taskRun = await this._browser.skyvern.runTask({
"x-user-agent": "skyvern-sdk",
body: {
prompt: prompt,
engine: options?.engine,
model: options?.model,
url: options?.url ?? this._getPageUrl(),
webhook_url: options?.webhookUrl,
totp_identifier: options?.totpIdentifier,
totp_url: options?.totpUrl,
title: options?.title,
error_code_mapping: options?.errorCodeMapping,
data_extraction_schema: options?.dataExtractionSchema,
max_steps: options?.maxSteps,
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
},
});
if (this._browser.skyvern.environment === SkyvernEnvironment.Cloud) {
LOG.info("AI task is running, this may take a while", { url: getAppUrlForRun(taskRun.run_id), run_id: taskRun.run_id });
} else {
LOG.info("AI task is running, this may take a while", { run_id: taskRun.run_id });
}
const completedRun = await this._waitForRunCompletion(
taskRun.run_id,
options?.timeout ?? DEFAULT_AGENT_TIMEOUT,
);
LOG.info("AI task finished", { run_id: completedRun.run_id, status: completedRun.status });
return completedRun as Skyvern.TaskRunResponse;
}
async login(
credentialType: string,
options?: {
url?: string;
credentialId?: string;
bitwardenCollectionId?: string;
bitwardenItemId?: string;
onepasswordVaultId?: string;
onepasswordItemId?: string;
prompt?: string;
webhookUrl?: string;
totpIdentifier?: string;
totpUrl?: string;
extraHttpHeaders?: Record<string, string>;
timeout?: number;
},
): Promise<Skyvern.WorkflowRunResponse> {
LOG.info("Starting AI login workflow", { credential_type: credentialType });
const workflowRun = await this._browser.skyvern.login(
{
credential_type: credentialType as Skyvern.SkyvernSchemasRunBlocksCredentialType,
url: options?.url ?? this._getPageUrl(),
credential_id: options?.credentialId,
bitwarden_collection_id: options?.bitwardenCollectionId,
bitwarden_item_id: options?.bitwardenItemId,
onepassword_vault_id: options?.onepasswordVaultId,
onepassword_item_id: options?.onepasswordItemId,
prompt: options?.prompt,
webhook_url: options?.webhookUrl,
totp_identifier: options?.totpIdentifier,
totp_url: options?.totpUrl,
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
extra_http_headers: options?.extraHttpHeaders,
},
{
headers: { "x-user-agent": "skyvern-sdk" },
},
);
if (this._browser.skyvern.environment === SkyvernEnvironment.Cloud) {
LOG.info("AI login workflow is running, this may take a while", { url: getAppUrlForRun(workflowRun.run_id), run_id: workflowRun.run_id });
} else {
LOG.info("AI login workflow is running, this may take a while", { run_id: workflowRun.run_id });
}
const completedRun = await this._waitForRunCompletion(
workflowRun.run_id,
options?.timeout ?? DEFAULT_AGENT_TIMEOUT,
);
LOG.info("AI login workflow finished", { run_id: completedRun.run_id, status: completedRun.status });
return completedRun as Skyvern.WorkflowRunResponse;
}
async downloadFiles(
prompt: string,
options?: {
url?: string;
downloadSuffix?: string;
downloadTimeout?: number;
maxStepsPerRun?: number;
webhookUrl?: string;
totpIdentifier?: string;
totpUrl?: string;
extraHttpHeaders?: Record<string, string>;
timeout?: number;
},
): Promise<Skyvern.WorkflowRunResponse> {
LOG.info("Starting AI file download workflow", { navigation_goal: prompt });
const workflowRun = await this._browser.skyvern.downloadFiles(
{
navigation_goal: prompt,
url: options?.url ?? this._getPageUrl(),
download_suffix: options?.downloadSuffix,
download_timeout: options?.downloadTimeout,
max_steps_per_run: options?.maxStepsPerRun,
webhook_url: options?.webhookUrl,
totp_identifier: options?.totpIdentifier,
totp_url: options?.totpUrl,
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
extra_http_headers: options?.extraHttpHeaders,
},
{
headers: { "x-user-agent": "skyvern-sdk" },
},
);
LOG.info("AI file download workflow is running, this may take a while", { run_id: workflowRun.run_id });
const completedRun = await this._waitForRunCompletion(
workflowRun.run_id,
options?.timeout ?? DEFAULT_AGENT_TIMEOUT,
);
LOG.info("AI file download workflow finished", { run_id: completedRun.run_id, status: completedRun.status });
return completedRun as Skyvern.WorkflowRunResponse;
}
async runWorkflow(
workflowId: string,
options?: {
parameters?: Record<string, unknown>;
template?: boolean;
title?: string;
webhookUrl?: string;
totpUrl?: string;
totpIdentifier?: string;
timeout?: number;
},
): Promise<Skyvern.WorkflowRunResponse> {
LOG.info("Starting AI workflow", { workflow_id: workflowId });
const workflowRun = await this._browser.skyvern.runWorkflow(
{
"x-user-agent": "skyvern-sdk",
template: options?.template,
body: {
workflow_id: workflowId,
parameters: options?.parameters,
title: options?.title,
webhook_url: options?.webhookUrl,
totp_url: options?.totpUrl,
totp_identifier: options?.totpIdentifier,
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
},
},
{
headers: { "x-user-agent": "skyvern-sdk" },
},
);
if (this._browser.skyvern.environment === SkyvernEnvironment.Cloud) {
LOG.info("AI workflow is running, this may take a while", { url: getAppUrlForRun(workflowRun.run_id), run_id: workflowRun.run_id });
} else {
LOG.info("AI workflow is running, this may take a while", { run_id: workflowRun.run_id });
}
const completedRun = await this._waitForRunCompletion(
workflowRun.run_id,
options?.timeout ?? DEFAULT_AGENT_TIMEOUT,
);
LOG.info("AI workflow finished", { run_id: completedRun.run_id, status: completedRun.status });
return completedRun as Skyvern.WorkflowRunResponse;
}
private async _waitForRunCompletion(runId: string, timeoutSeconds: number): Promise<Skyvern.GetRunResponse> {
const startTime = Date.now();
const timeoutMs = timeoutSeconds * 1000;
while (true) {
const run = await this._browser.skyvern.getRun(runId);
// Check if the run is in a final state
const status = run.status;
if (
status === "completed" ||
status === "failed" ||
status === "terminated" ||
status === "timed_out" ||
status === "canceled"
) {
return run;
}
// Check timeout
if (Date.now() - startTime >= timeoutMs) {
throw new Error(`Timeout waiting for run ${runId} to complete after ${timeoutSeconds} seconds`);
}
// Wait before polling again
await new Promise((resolve) => setTimeout(resolve, DEFAULT_AGENT_HEARTBEAT_INTERVAL * 1000));
}
}
private _getPageUrl(): string | undefined {
const url = this._page.url();
if (url === "about:blank") {
return undefined;
}
return url;
}
}

View File

@@ -0,0 +1,264 @@
import type { Page } from "playwright";
import type * as Skyvern from "../api/index.js";
import type { SkyvernBrowser } from "./SkyvernBrowser.js";
import { LOG } from "./logger.js";
export class SkyvernBrowserPageAi {
private readonly _browser: SkyvernBrowser;
private readonly _page: Page;
constructor(browser: SkyvernBrowser, page: Page) {
this._browser = browser;
this._page = page;
}
async aiClick(options: {
selector?: string;
intention: string;
data?: string | Record<string, unknown>;
timeout?: number;
}): Promise<string | null> {
LOG.info("AI click", { intention: options.intention, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_click",
selector: options.selector,
intention: options.intention,
data: options.data,
timeout: options.timeout,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result ? String(response.result) : options.selector || null;
}
async aiInputText(options: {
selector?: string;
value?: string;
intention: string;
data?: string | Record<string, unknown>;
totpIdentifier?: string;
totpUrl?: string;
timeout?: number;
}): Promise<string> {
LOG.info("AI input text", { intention: options.intention, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_input_text",
selector: options.selector,
value: options.value,
intention: options.intention,
data: options.data,
totp_identifier: options.totpIdentifier,
totp_url: options.totpUrl,
timeout: options.timeout,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result ? String(response.result) : options.value || "";
}
async aiSelectOption(options: {
selector?: string;
value?: string;
intention: string;
data?: string | Record<string, unknown>;
timeout?: number;
}): Promise<string> {
LOG.info("AI select option", { intention: options.intention, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_select_option",
selector: options.selector,
value: options.value,
intention: options.intention,
data: options.data,
timeout: options.timeout,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result ? String(response.result) : options.value || "";
}
async aiUploadFile(options: {
selector?: string;
fileUrl?: string;
intention: string;
data?: string | Record<string, unknown>;
timeout?: number;
}): Promise<string> {
LOG.info("AI upload file", { intention: options.intention, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_upload_file",
selector: options.selector,
file_url: options.fileUrl,
intention: options.intention,
data: options.data,
timeout: options.timeout,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result ? String(response.result) : options.fileUrl || "";
}
async aiExtract(options: {
prompt: string;
extractSchema?: Record<string, unknown> | unknown[] | string;
errorCodeMapping?: Record<string, string>;
intention?: string;
data?: string | Record<string, unknown>;
}): Promise<Record<string, unknown> | unknown[] | string | null> {
LOG.info("AI extract", { prompt: options.prompt, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "extract",
prompt: options.prompt,
extract_schema: options.extractSchema,
error_code_mapping: options.errorCodeMapping,
intention: options.intention,
data: options.data,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return (response.result as Record<string, unknown> | unknown[] | string) || null;
}
async aiValidate(options: { prompt: string; model?: Record<string, unknown> }): Promise<boolean> {
LOG.info("AI validate", { prompt: options.prompt, model: options.model, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "validate",
prompt: options.prompt,
model: options.model,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result != null ? Boolean(response.result) : false;
}
async aiAct(prompt: string): Promise<void> {
LOG.info("AI act", { prompt, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_act",
intention: prompt,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
}
async aiLocateElement(prompt: string): Promise<string | null> {
LOG.info("AI locate element", { prompt, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "locate_element",
prompt,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
if (response.result && typeof response.result === "string") {
return response.result;
}
return null;
}
async aiPrompt(options: {
prompt: string;
schema?: Record<string, unknown>;
model?: Record<string, unknown>;
}): Promise<Record<string, unknown> | unknown[] | string | null> {
LOG.info("AI prompt", { prompt: options.prompt, model: options.model, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "prompt",
prompt: options.prompt,
schema: options.schema,
model: options.model,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return (response.result as Record<string, unknown> | unknown[] | string) || null;
}
}

View File

@@ -0,0 +1,3 @@
export const DEFAULT_AGENT_TIMEOUT = 1800; // 30 minutes in seconds
export const DEFAULT_AGENT_HEARTBEAT_INTERVAL = 10; // 10 seconds
export const DEFAULT_CDP_PORT = 9222;

View File

@@ -0,0 +1,10 @@
export { Skyvern } from "./Skyvern.js";
export type { SkyvernOptions, RunTaskOptions, RunWorkflowOptions, LoginOptions, DownloadFilesOptions } from "./Skyvern.js";
export { SkyvernBrowser } from "./SkyvernBrowser.js";
export { SkyvernBrowserPageCore as SkyvernBrowserPageIml } from "./SkyvernBrowserPage.js";
export type { SkyvernBrowserPage } from "./SkyvernBrowserPage.js";
export { SkyvernBrowserPageAgent } from "./SkyvernBrowserPageAgent.js";
export { SkyvernBrowserPageAi } from "./SkyvernBrowserPageAi.js";
export { DEFAULT_AGENT_TIMEOUT, DEFAULT_AGENT_HEARTBEAT_INTERVAL, DEFAULT_CDP_PORT } from "./constants.js";
export type { Logger } from "./logger.js";
export { setLogger, LOG } from "./logger.js";

View File

@@ -0,0 +1,71 @@
export interface Logger {
debug(message: string, context?: Record<string, unknown>): void;
info(message: string, context?: Record<string, unknown>): void;
warn(message: string, context?: Record<string, unknown>): void;
error(message: string, context?: Record<string, unknown>): void;
}
function formatLogMessage(message: string, context?: Record<string, unknown>): string {
const prefix = "[Skyvern]";
if (!context || Object.keys(context).length === 0) {
return `${prefix} ${message}`;
}
const contextParts = Object.entries(context)
.filter(([, value]) => value !== null && value !== undefined)
.map(([key, value]) => {
if (typeof value === "object") {
return `${key}=${JSON.stringify(value)}`;
}
return `${key}=${value}`;
})
.join(" ");
if (contextParts.length === 0) {
return `${prefix} ${message}`;
}
return `${prefix} ${message}\t${contextParts}`;
}
const defaultLogger: Logger = {
debug: (message: string, context?: Record<string, unknown>) => {
console.debug(formatLogMessage(message, context));
},
info: (message: string, context?: Record<string, unknown>) => {
console.info(formatLogMessage(message, context));
},
warn: (message: string, context?: Record<string, unknown>) => {
console.warn(formatLogMessage(message, context));
},
error: (message: string, context?: Record<string, unknown>) => {
console.error(formatLogMessage(message, context));
},
};
let currentLogger: Logger = defaultLogger;
export function setLogger(logger: Logger): void {
currentLogger = logger;
}
class StructuredLogger {
debug(message: string, context?: Record<string, unknown>): void {
currentLogger.debug(message, context);
}
info(message: string, context?: Record<string, unknown>): void {
currentLogger.info(message, context);
}
warn(message: string, context?: Record<string, unknown>): void {
currentLogger.warn(message, context);
}
error(message: string, context?: Record<string, unknown>): void {
currentLogger.error(message, context);
}
}
export const LOG: StructuredLogger = new StructuredLogger();