diff --git a/package.json b/package.json index 55d2f4d..6179ad2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hyperbrowser/sdk", - "version": "0.92.7", + "version": "0.93.0", "description": "Node SDK for Hyperbrowser API", "author": "", "repository": { diff --git a/src/client.ts b/src/client.ts index 25e117e..5f585dc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -14,6 +14,7 @@ import { ComputerActionService } from "./services/computer-action"; import { GeminiComputerUseService } from "./services/agents/gemini-computer-use"; import { GrokComputerUseService } from "./services/agents/grok-computer-use"; import { MetaComputerUseService } from "./services/agents/meta-computer-use"; +import { JevComputerUseService } from "./services/agents/jev-computer-use"; import { WebService } from "./services/web"; import { SandboxesService } from "./services/sandboxes"; import { VolumesService } from "./services/volumes"; @@ -71,6 +72,7 @@ export class HyperbrowserClient { geminiComputerUse: GeminiComputerUseService; grokComputerUse: GrokComputerUseService; metaComputerUse: MetaComputerUseService; + jevComputerUse: JevComputerUseService; }; public readonly team: TeamService; public readonly computerAction: ComputerActionService; @@ -108,6 +110,7 @@ export class HyperbrowserClient { geminiComputerUse: new GeminiComputerUseService(apiKey, baseUrl, timeout), grokComputerUse: new GrokComputerUseService(apiKey, baseUrl, timeout), metaComputerUse: new MetaComputerUseService(apiKey, baseUrl, timeout), + jevComputerUse: new JevComputerUseService(apiKey, baseUrl, timeout), }; } } diff --git a/src/services/agents/jev-computer-use.ts b/src/services/agents/jev-computer-use.ts new file mode 100644 index 0000000..d83cb66 --- /dev/null +++ b/src/services/agents/jev-computer-use.ts @@ -0,0 +1,114 @@ +import { HyperbrowserError } from "../../client"; +import { BasicResponse } from "../../types"; +import { POLLING_ATTEMPTS } from "../../types/constants"; +import { sleep } from "../../utils"; +import { BaseService } from "../base"; +import { + JevComputerUseTaskResponse, + JevComputerUseTaskStatusResponse, + StartJevComputerUseTaskParams, + StartJevComputerUseTaskResponse, +} from "../../types/agents/jev-computer-use"; + +export class JevComputerUseService extends BaseService { + /** + * Start a new Jev Computer Use task job + * @param params The parameters for the task job + */ + async start(params: StartJevComputerUseTaskParams): Promise { + try { + return await this.request("/task/jev", { + method: "POST", + body: JSON.stringify(params), + }); + } catch (error) { + if (error instanceof HyperbrowserError) { + throw error; + } + throw new HyperbrowserError("Failed to start Jev Computer Use task job", undefined); + } + } + + /** + * Get the status of a Jev Computer Use task job + * @param id The ID of the task job to get + */ + async getStatus(id: string): Promise { + try { + return await this.request(`/task/jev/${id}/status`); + } catch (error) { + if (error instanceof HyperbrowserError) { + throw error; + } + throw new HyperbrowserError( + `Failed to get Jev Computer Use task job ${id} status`, + undefined + ); + } + } + + /** + * Get the result of a Jev Computer Use task job + * @param id The ID of the task job to get + */ + async get(id: string): Promise { + try { + return await this.request(`/task/jev/${id}`); + } catch (error) { + if (error instanceof HyperbrowserError) { + throw error; + } + throw new HyperbrowserError(`Failed to get Jev Computer Use task job ${id}`, undefined); + } + } + + /** + * Stop a Jev Computer Use task job + * @param id The ID of the task job to stop + */ + async stop(id: string): Promise { + try { + return await this.request(`/task/jev/${id}/stop`, { + method: "PUT", + }); + } catch (error) { + if (error instanceof HyperbrowserError) { + throw error; + } + throw new HyperbrowserError(`Failed to stop Jev Computer Use task job ${id}`, undefined); + } + } + + /** + * Start a Jev Computer Use task job and wait for it to complete + * @param params The parameters for the task job + */ + async startAndWait(params: StartJevComputerUseTaskParams): Promise { + const job = await this.start(params); + const jobId = job.jobId; + if (!jobId) { + throw new HyperbrowserError( + "Failed to start Jev Computer Use task job, could not get job ID" + ); + } + + let failures = 0; + while (true) { + try { + const { status } = await this.getStatus(jobId); + if (status === "completed" || status === "failed" || status === "stopped") { + return await this.get(jobId); + } + failures = 0; + } catch (error) { + failures++; + if (failures >= POLLING_ATTEMPTS) { + throw new HyperbrowserError( + `Failed to poll Jev Computer Use task job ${jobId} after ${POLLING_ATTEMPTS} attempts: ${error}` + ); + } + } + await sleep(2000); + } + } +} diff --git a/src/types/agents/jev-computer-use.ts b/src/types/agents/jev-computer-use.ts new file mode 100644 index 0000000..9416ac8 --- /dev/null +++ b/src/types/agents/jev-computer-use.ts @@ -0,0 +1,51 @@ +import { JevComputerUseLlm, JevComputerUseTaskStatus, JevTextLlm } from "../constants"; +import { CreateSessionParams } from "../session"; + +export interface JevComputerUseApiKeys { + jev?: string; + google?: string; +} + +export interface StartJevComputerUseTaskParams { + task: string; + llm?: JevComputerUseLlm; + textLlm?: JevTextLlm; + sessionId?: string; + maxFailures?: number; + maxSteps?: number; + keepBrowserOpen?: boolean; + sessionOptions?: CreateSessionParams; + useCustomApiKeys?: boolean; + apiKeys?: JevComputerUseApiKeys; +} + +export interface StartJevComputerUseTaskResponse { + jobId: string; + liveUrl: string | null; +} + +export interface JevComputerUseTaskStatusResponse { + status: JevComputerUseTaskStatus; +} + +export type JevComputerUseStepResponse = Record; + +export interface JevComputerUseTaskData { + steps: JevComputerUseStepResponse[]; + finalResult: string | null; +} + +export interface JevComputerUseTaskMetadata { + inputTokens?: number | null; + outputTokens?: number | null; + numTaskStepsCompleted?: number | null; +} + +export interface JevComputerUseTaskResponse { + jobId: string; + status: JevComputerUseTaskStatus; + metadata?: JevComputerUseTaskMetadata | null; + data?: JevComputerUseTaskData | null; + error?: string | null; + liveUrl: string | null; +} diff --git a/src/types/constants.ts b/src/types/constants.ts index b5f9f3c..6133f30 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -24,6 +24,7 @@ export type GeminiComputerUseTaskStatus = | "stopped"; export type GrokComputerUseTaskStatus = "pending" | "running" | "completed" | "failed" | "stopped"; export type MetaComputerUseTaskStatus = "pending" | "running" | "completed" | "failed" | "stopped"; +export type JevComputerUseTaskStatus = "pending" | "running" | "completed" | "failed" | "stopped"; export type ScrapePageStatus = "completed" | "failed" | "pending" | "running"; export type CrawlPageStatus = "completed" | "failed"; export type ScrapeWaitUntil = "load" | "domcontentloaded" | "networkidle"; @@ -53,6 +54,7 @@ export type BrowserUseLlm = | "gemini-3.8-flash"; export type ClaudeComputerUseLlm = + | "claude-opus-5-5" | "claude-fable-5-1" | "claude-opus-5" | "claude-opus-4-5" @@ -71,6 +73,8 @@ export type ClaudeComputerUseReasoningEffort = "low" | "medium" | "high" | "xhig export type CuaLlm = | "computer-use-preview" | "gpt-6-astra" + | "gpt-6-sol" + | "gpt-6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-5.6-luna" @@ -119,6 +123,10 @@ export type MetaComputerUseLlm = "muse-spark-1.1" | "muse-spark-1.2" | "muse-spa export type MetaReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; +export type JevComputerUseLlm = "jev-1.13.0" | "jev-latest"; + +export type JevTextLlm = "gemini-3.5-flash-lite"; + export type SessionRegion = | "us" | "us-central" diff --git a/src/types/index.ts b/src/types/index.ts index 3f6bde8..42b45b2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -115,6 +115,16 @@ export { MetaComputerUseApiKeys, MetaComputerUseTaskMetadata, } from "./agents/meta-computer-use"; +export { + StartJevComputerUseTaskParams, + StartJevComputerUseTaskResponse, + JevComputerUseTaskStatusResponse, + JevComputerUseTaskResponse, + JevComputerUseTaskData, + JevComputerUseStepResponse, + JevComputerUseApiKeys, + JevComputerUseTaskMetadata, +} from "./agents/jev-computer-use"; export { BasicResponse, BrowserMemorySize, @@ -261,6 +271,8 @@ export { GrokReasoningEffort, MetaComputerUseLlm, MetaReasoningEffort, + JevComputerUseLlm, + JevTextLlm, ScrapeScreenshotFormat, ScrapeJobStatus, CrawlJobStatus, @@ -282,6 +294,7 @@ export { GeminiComputerUseTaskStatus, GrokComputerUseTaskStatus, MetaComputerUseTaskStatus, + JevComputerUseTaskStatus, SessionEventLogType, SessionRegion, BrowserUseVersion, diff --git a/tests/integration/client-http.test.ts b/tests/integration/client-http.test.ts index 6930e96..a5b8e3c 100644 --- a/tests/integration/client-http.test.ts +++ b/tests/integration/client-http.test.ts @@ -70,7 +70,10 @@ const startServer = async (): Promise => { return; } - if (request.method === "GET" && request.url === "/api/task/meta-computer-use/meta_job_123/status") { + if ( + request.method === "GET" && + request.url === "/api/task/meta-computer-use/meta_job_123/status" + ) { sendJson(response, 200, { status: "completed" }); return; } @@ -108,7 +111,49 @@ const startServer = async (): Promise => { return; } - if (request.method === "PUT" && request.url === "/api/task/meta-computer-use/meta_job_123/stop") { + if ( + request.method === "PUT" && + request.url === "/api/task/meta-computer-use/meta_job_123/stop" + ) { + sendJson(response, 200, { success: true }); + return; + } + + if (request.method === "POST" && request.url === "/api/task/jev") { + sendJson(response, 200, { jobId: "jev_job_123", liveUrl: null }); + return; + } + + if (request.method === "GET" && request.url === "/api/task/jev/jev_job_123/status") { + sendJson(response, 200, { status: "completed" }); + return; + } + + if (request.method === "GET" && request.url === "/api/task/jev/jev_job_123") { + sendJson(response, 200, { + jobId: "jev_job_123", + status: "completed", + data: { + steps: [ + { + step: 1, + action: { kind: "click", target: "submit" }, + url: "https://example.com", + title: "Example", + targetDescription: "Submit button", + outcome: "dispatched", + pageChanged: true, + }, + ], + finalResult: "Found the order", + }, + error: null, + liveUrl: null, + }); + return; + } + + if (request.method === "PUT" && request.url === "/api/task/jev/jev_job_123/stop") { sendJson(response, 200, { success: true }); return; } @@ -160,8 +205,7 @@ const startServer = async (): Promise => { if ( request.method === "POST" && - request.url === - "/api/session/52dd29fb-75a2-43f9-9831-8ff377fedb0a/captcha/evaluate" + request.url === "/api/session/52dd29fb-75a2-43f9-9831-8ff377fedb0a/captcha/evaluate" ) { await new Promise((resolve) => setTimeout(resolve, 10)); sendJson(response, 200, { @@ -321,7 +365,7 @@ describe("client HTTP integration", () => { const started = await client.agents.claudeComputerUse.start({ task: "Complete the task", - llm: "claude-opus-5", + llm: "claude-opus-5-5", reasoningEffort: "xhigh", }); @@ -334,7 +378,7 @@ describe("client HTTP integration", () => { contentType: "application/json", body: { task: "Complete the task", - llm: "claude-opus-5", + llm: "claude-opus-5-5", reasoningEffort: "xhigh", }, }, @@ -412,6 +456,105 @@ describe("client HTTP integration", () => { ]); }); + test("Jev Computer Use starts, polls, reads, and stops a task", async () => { + const server = await startServer(); + servers.push(server); + const client = new HyperbrowserClient({ + apiKey: "test-api-key", + baseUrl: server.baseUrl, + }); + + const started = await client.agents.jevComputerUse.start({ + task: "Find the order", + llm: "jev-1.13.0", + textLlm: "gemini-3.5-flash-lite", + useCustomApiKeys: true, + apiKeys: { jev: "jev-key", google: "google-key" }, + }); + const status = await client.agents.jevComputerUse.getStatus(started.jobId); + const result = await client.agents.jevComputerUse.get(started.jobId); + const stopped = await client.agents.jevComputerUse.stop(started.jobId); + + expect(started).toEqual({ jobId: "jev_job_123", liveUrl: null }); + expect(status).toEqual({ status: "completed" }); + expect(result.data?.finalResult).toBe("Found the order"); + expect(result.data?.steps[0]).toMatchObject({ + step: 1, + action: { kind: "click", target: "submit" }, + url: "https://example.com", + }); + expect(stopped).toEqual({ success: true }); + expect(server.requests).toEqual([ + { + method: "POST", + url: "/api/task/jev", + apiKey: "test-api-key", + contentType: "application/json", + body: { + task: "Find the order", + llm: "jev-1.13.0", + textLlm: "gemini-3.5-flash-lite", + useCustomApiKeys: true, + apiKeys: { jev: "jev-key", google: "google-key" }, + }, + }, + { + method: "GET", + url: "/api/task/jev/jev_job_123/status", + apiKey: "test-api-key", + contentType: "application/json", + body: undefined, + }, + { + method: "GET", + url: "/api/task/jev/jev_job_123", + apiKey: "test-api-key", + contentType: "application/json", + body: undefined, + }, + { + method: "PUT", + url: "/api/task/jev/jev_job_123/stop", + apiKey: "test-api-key", + contentType: "application/json", + body: undefined, + }, + ]); + }); + + test("OpenAI CUA accepts GPT-6 Sol and Luna models", async () => { + const server = await startServer(); + servers.push(server); + const client = new HyperbrowserClient({ + apiKey: "test-api-key", + baseUrl: server.baseUrl, + }); + + await client.agents.cua.start({ + task: "Complete the task", + llm: "gpt-6-sol", + reasoningEffort: "max", + }); + await client.agents.cua.start({ + task: "Complete the task", + llm: "gpt-6-luna", + reasoningEffort: "none", + }); + + expect(server.requests.map((request) => request.body)).toEqual([ + { + task: "Complete the task", + llm: "gpt-6-sol", + reasoningEffort: "max", + }, + { + task: "Complete the task", + llm: "gpt-6-luna", + reasoningEffort: "none", + }, + ]); + }); + test("session create can start from a snapshot", async () => { const server = await startServer(); servers.push(server); @@ -452,9 +595,7 @@ describe("client HTTP integration", () => { baseUrl: server.baseUrl, }); - const snapshot = await client.sessions.createSnapshot( - "52dd29fb-75a2-43f9-9831-8ff377fedb0a" - ); + const snapshot = await client.sessions.createSnapshot("52dd29fb-75a2-43f9-9831-8ff377fedb0a"); expect(snapshot).toEqual({ snapshotName: "browser-session-11111111-1111-4111-8111-111111111111", @@ -487,13 +628,10 @@ describe("client HTTP integration", () => { timeout: 1, }); - const result = await client.sessions.evaluateCaptcha( - "52dd29fb-75a2-43f9-9831-8ff377fedb0a", - { - captcha: "recaptcha-visual", - iterations: 5, - } - ); + const result = await client.sessions.evaluateCaptcha("52dd29fb-75a2-43f9-9831-8ff377fedb0a", { + captcha: "recaptcha-visual", + iterations: 5, + }); expect(result).toEqual({ success: true,