@allternit/computer-driver 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # @allternit/computer-driver
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Drive an Allternit hosted computer (`/v1/computers`) from Claude, OpenAI computer use, or Gemini computer use. Needs a project key with the `computers` scope and the project's hosted driver turned on.
4
+
5
+ ```ts
6
+ import { AllternitComputers } from "@allternit/computer-driver"
7
+ const client = new AllternitComputers({ apiKey: process.env.ALLTERNIT_API_KEY })
8
+ const computer = await client.create({ name: "agent-1" })
9
+ ```
10
+
11
+ ## Claude (`computer_toolset_20260801` / `browser_toolset_20260801`)
12
+
13
+ Requires `@anthropic-ai/sdk` >= 0.132.0 (optional peer).
14
+
15
+ ```ts
16
+ import { AllternitComputerToolset } from "@allternit/computer-driver/anthropic"
17
+ const toolset = new AllternitComputerToolset({
18
+ client, computerId: computer.id,
19
+ onApproval: async (approval) => askAPerson(approval), // server-held risky calls
20
+ })
21
+ // pass `toolset` to client.beta.messages.toolRunner({ tools: [toolset], ... })
22
+ ```
23
+
24
+ Every member runs on the server through `POST /v1/computers/{id}/toolset`. When the server holds a call (409 `approval_required`), `onApproval` decides: `true` approves it and resends with the grant, `false` returns the held result to the model as an error. The SDK's own `confirm` option still runs first. Approving with an API key needs the project's `approval_mode` set to `api_key`.
25
+
26
+ ## OpenAI computer use
27
+
28
+ ```ts
29
+ import { runOpenAIAction } from "@allternit/computer-driver"
30
+ const { output } = await runOpenAIAction({ client, computerId, display: { width: 1280, height: 800 } }, call.call_id, call.action)
31
+ // push `output` into the next responses.create input
32
+ ```
33
+
34
+ ## Gemini computer use
35
+
36
+ ```ts
37
+ import { runGeminiCall } from "@allternit/computer-driver"
38
+ const { functionResponse, inlineData } = await runGeminiCall({ client, computerId }, part.functionCall)
39
+ ```
40
+
41
+ Gemini's 0–999 coordinates are sent as `coordinate_space: "normalized_1000"`; the server scales them.
42
+
43
+ The OpenAI and Gemini adapters let `ApprovalRequiredError` propagate; catch it, call `client.approve()`, and resend the action.
44
+
45
+ Errors: `ApprovalRequiredError` (409, carries `approval` and `result`) and `AllternitApiError` (everything else, with `status`, `code`, `type`).
@@ -0,0 +1,25 @@
1
+ import { BetaAbstractBrowserToolset20260801, BetaAbstractComputerToolset20260801, type BetaBrowserMemberResult, type BetaBrowserToolsetOptions, type BetaComputerMemberResult, type BetaComputerToolsetOptions, type BetaToolsetCallContext } from "@anthropic-ai/sdk/helpers/beta/toolsets";
2
+ import { type AllternitComputers, type Approval } from "./client.ts";
3
+ export interface DriverOptions {
4
+ client: AllternitComputers;
5
+ computerId: string;
6
+ /** Called when the server holds a call for approval. true → approve() and resend with the grant. */
7
+ onApproval?: (approval: Approval) => Promise<boolean>;
8
+ /** run_id / turn_id / call_index for this call, if your loop tracks them. */
9
+ callIds?: (ctx: BetaToolsetCallContext, member: string) => {
10
+ run_id?: string;
11
+ turn_id?: string;
12
+ call_index?: number;
13
+ };
14
+ browserSessionId?: string;
15
+ }
16
+ export declare class AllternitComputerToolset extends BetaAbstractComputerToolset20260801 {
17
+ #private;
18
+ constructor(o: DriverOptions & BetaComputerToolsetOptions);
19
+ protected execute(ctx: BetaToolsetCallContext, name: string, input: unknown): Promise<BetaComputerMemberResult>;
20
+ }
21
+ export declare class AllternitBrowserToolset extends BetaAbstractBrowserToolset20260801 {
22
+ #private;
23
+ constructor(o: DriverOptions & Omit<BetaBrowserToolsetOptions, "browserState"> & Partial<Pick<BetaBrowserToolsetOptions, "browserState">>);
24
+ protected execute(ctx: BetaToolsetCallContext, name: string, input: unknown): Promise<BetaBrowserMemberResult>;
25
+ }
@@ -0,0 +1,130 @@
1
+ // Anthropic toolset drivers: subclasses of the SDK's abstract computer and browser
2
+ // toolsets (computer_toolset_20260801 / browser_toolset_20260801) that run every
3
+ // member on an Allternit hosted computer via POST /v1/computers/{id}/toolset.
4
+ import { BetaAbstractBrowserToolset20260801, BetaAbstractComputerToolset20260801, ToolError, } from "@anthropic-ai/sdk/helpers/beta/toolsets";
5
+ import { ApprovalRequiredError, resultImage, resultText, } from "./client.js";
6
+ /**
7
+ * Default SDK `confirm`: let the call through. The Allternit server is the gate: it applies the
8
+ * project's member policy and holds risky calls with a 409, which `onApproval` answers. Pass your
9
+ * own `confirm` to also prompt locally before the call leaves the process.
10
+ */
11
+ const serverDecides = () => true;
12
+ async function callMember(o, toolset, ctx, member, input) {
13
+ const call = {
14
+ toolset,
15
+ member,
16
+ input: (input ?? {}),
17
+ // The SDK only calls members its toolset config turned on, which is the
18
+ // opt-in the executor asks for on off-by-default members.
19
+ enable: [member],
20
+ ...(o.callIds?.(ctx, member) ?? {}),
21
+ ...(o.browserSessionId ? { browser_session_id: o.browserSessionId } : {}),
22
+ };
23
+ let res;
24
+ try {
25
+ res = await o.client.toolset(o.computerId, call);
26
+ }
27
+ catch (e) {
28
+ if (!(e instanceof ApprovalRequiredError))
29
+ throw e;
30
+ if (!o.onApproval || !(await o.onApproval(e.approval)))
31
+ res = e.result;
32
+ else {
33
+ await o.client.approve(o.computerId, e.approval.id);
34
+ res = await o.client.toolset(o.computerId, { ...call, approval_grant: e.approval.id });
35
+ }
36
+ }
37
+ if (res.is_error)
38
+ throw new ToolError(toAnthropicBlocks(res));
39
+ return res;
40
+ }
41
+ function toAnthropicBlocks(r) {
42
+ const blocks = r.content.map((b) => b.type === "text"
43
+ ? { type: "text", text: b.text }
44
+ : { type: "image", source: { type: "base64", media_type: b.media_type, data: b.data } });
45
+ return blocks.length ? blocks : [{ type: "text", text: "The action failed." }];
46
+ }
47
+ function shot(r) {
48
+ const img = resultImage(r);
49
+ if (!img)
50
+ throw new ToolError("The computer returned no screenshot.");
51
+ return { data: img.data, mediaType: img.media_type };
52
+ }
53
+ function textOrVoid(r) {
54
+ const t = resultText(r);
55
+ return t ? t : undefined;
56
+ }
57
+ function jsonOf(r) {
58
+ try {
59
+ return JSON.parse(resultText(r));
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ export class AllternitComputerToolset extends BetaAbstractComputerToolset20260801 {
66
+ #o;
67
+ constructor(o) {
68
+ const { client: _c, computerId: _i, onApproval: _a, callIds: _d, browserSessionId: _b, ...sdk } = o;
69
+ super({ ...sdk, confirm: sdk.confirm ?? serverDecides });
70
+ this.#o = o;
71
+ }
72
+ async execute(ctx, name, input) {
73
+ const r = await callMember(this.#o, "computer", ctx, name, input);
74
+ if (name === "screenshot" || name === "zoom")
75
+ return shot(r);
76
+ if (name === "cursor_position") {
77
+ const j = jsonOf(r);
78
+ if (j && typeof j.x === "number")
79
+ return { x: j.x, y: j.y };
80
+ const m = /(-?\d+)\D+(-?\d+)/.exec(resultText(r));
81
+ if (!m)
82
+ throw new ToolError("The computer returned no cursor position.");
83
+ return { x: Number(m[1]), y: Number(m[2]) };
84
+ }
85
+ return textOrVoid(r);
86
+ }
87
+ }
88
+ export class AllternitBrowserToolset extends BetaAbstractBrowserToolset20260801 {
89
+ #o;
90
+ #state;
91
+ constructor(o) {
92
+ const { client: _c, computerId: _i, onApproval: _a, callIds: _d, browserSessionId: _b, ...sdk } = o;
93
+ const self = {};
94
+ super({ ...sdk, confirm: sdk.confirm ?? serverDecides, browserState: sdk.browserState ?? ((ctx) => self.t.#currentState(ctx)) });
95
+ this.#o = o;
96
+ self.t = this;
97
+ }
98
+ async #currentState(ctx) {
99
+ if (this.#state)
100
+ return this.#state;
101
+ const r = await callMember(this.#o, "browser", ctx, "list_tabs", {});
102
+ return (this.#state = r.browser_state ?? { tabs: jsonOf(r) ?? [] });
103
+ }
104
+ async execute(ctx, name, input) {
105
+ const r = await callMember(this.#o, "browser", ctx, name, input);
106
+ if (r.browser_state)
107
+ this.#state = r.browser_state;
108
+ if (name === "screenshot" || name === "zoom")
109
+ return shot(r);
110
+ const j = jsonOf(r);
111
+ const tabs = this.#state?.tabs ?? [];
112
+ switch (name) {
113
+ case "navigate":
114
+ return j?.url ? j : { url: tabs[0]?.url ?? String(input?.url ?? "") };
115
+ case "list_tabs":
116
+ return Array.isArray(j) ? j : tabs;
117
+ case "new_tab":
118
+ case "switch_tab":
119
+ if (j?.tab_id)
120
+ return j;
121
+ if (tabs[0])
122
+ return tabs[0];
123
+ throw new ToolError("The browser returned no tab.");
124
+ case "close_tab":
125
+ return undefined;
126
+ default:
127
+ return textOrVoid(r);
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,137 @@
1
+ export declare const DEFAULT_BASE_URL = "https://api.allternit.com";
2
+ export type ToolsetName = "computer" | "browser";
3
+ export type CoordinateSpace = "pixels" | "normalized_1000";
4
+ export type ComputerStatus = "provisioning" | "running" | "stopped" | "starting" | "stopping" | "error" | "deleted";
5
+ export interface Computer {
6
+ id: string;
7
+ object: "computer";
8
+ name: string | null;
9
+ status: ComputerStatus;
10
+ account_id: string | null;
11
+ key_id: string;
12
+ created_at: string;
13
+ started_at: string | null;
14
+ metadata: Record<string, unknown>;
15
+ }
16
+ export interface Page<T> {
17
+ data: T[];
18
+ has_more: boolean;
19
+ next_cursor: string | null;
20
+ }
21
+ export type ContentBlock = {
22
+ type: "text";
23
+ text: string;
24
+ } | {
25
+ type: "image";
26
+ media_type: string;
27
+ data: string;
28
+ };
29
+ export interface Screen {
30
+ width: number;
31
+ height: number;
32
+ scale: number;
33
+ frame_width: number;
34
+ frame_height: number;
35
+ }
36
+ export interface ToolsetResult {
37
+ is_error: boolean;
38
+ content: ContentBlock[];
39
+ browser_state?: unknown;
40
+ screen?: Screen;
41
+ error?: unknown;
42
+ }
43
+ export interface ToolsetCall {
44
+ toolset: ToolsetName;
45
+ member: string;
46
+ input?: Record<string, unknown>;
47
+ run_id?: string;
48
+ turn_id?: string;
49
+ call_index?: number;
50
+ model_frame?: {
51
+ width: number;
52
+ height: number;
53
+ };
54
+ coordinate_space?: CoordinateSpace;
55
+ approval_grant?: string;
56
+ browser_session_id?: string;
57
+ /** Off-by-default members (file_upload, read_console, read_network, javascript_exec) to allow for this call. */
58
+ enable?: string[];
59
+ }
60
+ export interface Approval {
61
+ id: string;
62
+ action_hash: string;
63
+ member: string;
64
+ toolset: ToolsetName;
65
+ risk: string;
66
+ confirmation_class: string;
67
+ approve_url: string;
68
+ }
69
+ export interface ComputerEvent {
70
+ id: string;
71
+ type: "computer.action";
72
+ ts: string;
73
+ data: Record<string, unknown>;
74
+ }
75
+ export interface ApiErrorBody {
76
+ type: string;
77
+ code: string;
78
+ message: string;
79
+ param: string | null;
80
+ }
81
+ export declare class AllternitApiError extends Error {
82
+ readonly status: number;
83
+ readonly type: string;
84
+ readonly code: string;
85
+ readonly param: string | null;
86
+ readonly body: unknown;
87
+ constructor(status: number, err: Partial<ApiErrorBody>, body: unknown);
88
+ }
89
+ /** 409 approval_required: the call was held until someone approves it. */
90
+ export declare class ApprovalRequiredError extends AllternitApiError {
91
+ readonly approval: Approval;
92
+ readonly result: ToolsetResult;
93
+ constructor(err: Partial<ApiErrorBody>, approval: Approval, result: ToolsetResult, body: unknown);
94
+ }
95
+ export interface ClientOptions {
96
+ /** Project key (alt_live_… / alt_test_…). Defaults to ALLTERNIT_API_KEY. */
97
+ apiKey?: string;
98
+ baseUrl?: string;
99
+ fetch?: typeof fetch;
100
+ }
101
+ export declare class AllternitComputers {
102
+ #private;
103
+ readonly baseUrl: string;
104
+ constructor(opts?: ClientOptions);
105
+ create(body?: {
106
+ name?: string;
107
+ account_id?: string;
108
+ metadata?: Record<string, unknown>;
109
+ }): Promise<Computer>;
110
+ list(q?: {
111
+ limit?: number;
112
+ after?: string;
113
+ }): Promise<Page<Computer>>;
114
+ get(id: string): Promise<Computer>;
115
+ start(id: string): Promise<Computer>;
116
+ stop(id: string): Promise<Computer>;
117
+ delete(id: string): Promise<Computer>;
118
+ /** Run one toolset member. Action failures resolve with is_error:true; a held call throws ApprovalRequiredError. */
119
+ toolset(id: string, call: ToolsetCall): Promise<ToolsetResult>;
120
+ schema(id: string, toolset?: ToolsetName): Promise<Record<string, unknown>>;
121
+ events(id: string, q?: {
122
+ after?: string;
123
+ limit?: number;
124
+ }): Promise<Page<ComputerEvent>>;
125
+ approve(id: string, approvalId: string): Promise<{
126
+ approval_id: string;
127
+ approved: true;
128
+ approval_grant: string;
129
+ }>;
130
+ }
131
+ /** Text of a result, joined. */
132
+ export declare function resultText(r: ToolsetResult): string;
133
+ /** First image of a result, if any. */
134
+ export declare function resultImage(r: ToolsetResult): {
135
+ media_type: string;
136
+ data: string;
137
+ } | undefined;
package/dist/client.js ADDED
@@ -0,0 +1,118 @@
1
+ // Plain client for the Allternit hosted computer API (/v1/computers).
2
+ // Standard fetch only; every adapter in this package goes through it.
3
+ export const DEFAULT_BASE_URL = "https://api.allternit.com";
4
+ export class AllternitApiError extends Error {
5
+ status;
6
+ type;
7
+ code;
8
+ param;
9
+ body;
10
+ constructor(status, err, body) {
11
+ super(err.message ?? `Allternit API error ${status}`);
12
+ this.name = "AllternitApiError";
13
+ this.status = status;
14
+ this.type = err.type ?? "api_error";
15
+ this.code = err.code ?? "unknown";
16
+ this.param = err.param ?? null;
17
+ this.body = body;
18
+ }
19
+ }
20
+ /** 409 approval_required: the call was held until someone approves it. */
21
+ export class ApprovalRequiredError extends AllternitApiError {
22
+ approval;
23
+ result;
24
+ constructor(err, approval, result, body) {
25
+ super(409, err, body);
26
+ this.name = "ApprovalRequiredError";
27
+ this.approval = approval;
28
+ this.result = result;
29
+ }
30
+ }
31
+ export class AllternitComputers {
32
+ baseUrl;
33
+ #apiKey;
34
+ #fetch;
35
+ constructor(opts = {}) {
36
+ const env = globalThis.process?.env;
37
+ const key = opts.apiKey ?? env?.ALLTERNIT_API_KEY;
38
+ if (!key)
39
+ throw new Error("AllternitComputers: apiKey is required (or set ALLTERNIT_API_KEY).");
40
+ this.#apiKey = key;
41
+ this.baseUrl = (opts.baseUrl ?? env?.ALLTERNIT_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
42
+ this.#fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
43
+ }
44
+ create(body = {}) {
45
+ return this.#req("POST", "/v1/computers", body);
46
+ }
47
+ list(q = {}) {
48
+ return this.#req("GET", `/v1/computers${qs(q)}`);
49
+ }
50
+ get(id) {
51
+ return this.#req("GET", `/v1/computers/${enc(id)}`);
52
+ }
53
+ start(id) {
54
+ return this.#req("POST", `/v1/computers/${enc(id)}/start`, {});
55
+ }
56
+ stop(id) {
57
+ return this.#req("POST", `/v1/computers/${enc(id)}/stop`, {});
58
+ }
59
+ delete(id) {
60
+ return this.#req("DELETE", `/v1/computers/${enc(id)}`);
61
+ }
62
+ /** Run one toolset member. Action failures resolve with is_error:true; a held call throws ApprovalRequiredError. */
63
+ toolset(id, call) {
64
+ return this.#req("POST", `/v1/computers/${enc(id)}/toolset`, call);
65
+ }
66
+ schema(id, toolset = "computer") {
67
+ return this.#req("GET", `/v1/computers/${enc(id)}/toolset/schema${qs({ toolset })}`);
68
+ }
69
+ events(id, q = {}) {
70
+ return this.#req("GET", `/v1/computers/${enc(id)}/events${qs(q)}`);
71
+ }
72
+ approve(id, approvalId) {
73
+ return this.#req("POST", `/v1/computers/${enc(id)}/approvals/${enc(approvalId)}`, {});
74
+ }
75
+ async #req(method, path, body) {
76
+ const headers = { Authorization: `Bearer ${this.#apiKey}`, Accept: "application/json" };
77
+ if (body !== undefined)
78
+ headers["Content-Type"] = "application/json";
79
+ if (method === "POST")
80
+ headers["Idempotency-Key"] = crypto.randomUUID();
81
+ const res = await this.#fetch(this.baseUrl + path, {
82
+ method,
83
+ headers,
84
+ body: body === undefined ? undefined : JSON.stringify(body),
85
+ });
86
+ const text = await res.text();
87
+ let json = undefined;
88
+ try {
89
+ json = text ? JSON.parse(text) : undefined;
90
+ }
91
+ catch {
92
+ json = { error: { message: text } };
93
+ }
94
+ if (res.ok)
95
+ return json;
96
+ const err = json?.error ?? {};
97
+ if (res.status === 409 && err.code === "approval_required" && json?.approval) {
98
+ throw new ApprovalRequiredError(err, json.approval, json.result ?? { is_error: true, content: [] }, json);
99
+ }
100
+ throw new AllternitApiError(res.status, err, json);
101
+ }
102
+ }
103
+ /** Text of a result, joined. */
104
+ export function resultText(r) {
105
+ return r.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("\n");
106
+ }
107
+ /** First image of a result, if any. */
108
+ export function resultImage(r) {
109
+ const img = r.content.find((b) => b.type === "image");
110
+ return img && img.type === "image" ? { media_type: img.media_type, data: img.data } : undefined;
111
+ }
112
+ function enc(s) {
113
+ return encodeURIComponent(s);
114
+ }
115
+ function qs(q) {
116
+ const p = Object.entries(q).filter(([, v]) => v !== undefined && v !== "");
117
+ return p.length ? "?" + new URLSearchParams(p.map(([k, v]) => [k, String(v)])).toString() : "";
118
+ }
@@ -0,0 +1,32 @@
1
+ import { type AllternitComputers, type ToolsetCall, type ToolsetResult } from "./client.ts";
2
+ export interface GeminiFunctionCall {
3
+ name: string;
4
+ args?: Record<string, unknown>;
5
+ }
6
+ export interface GeminiAdapterOptions {
7
+ client: AllternitComputers;
8
+ computerId: string;
9
+ /** URL used by the `search` function. Default https://www.google.com. */
10
+ searchUrl?: string;
11
+ }
12
+ type Step = Pick<ToolsetCall, "member" | "input">;
13
+ /** "Control+Shift+T" → "ctrl+shift+t". */
14
+ export declare function geminiKeys(keys: string): string;
15
+ /** Contract calls for one Gemini computer_use function call, in order. */
16
+ export declare function geminiToCalls(fc: GeminiFunctionCall, searchUrl?: string): Step[];
17
+ export interface GeminiStepResult {
18
+ /** A `functionResponse` part for the next turn. */
19
+ functionResponse: {
20
+ name: string;
21
+ response: Record<string, unknown>;
22
+ };
23
+ /** The screenshot after the action, as an `inlineData` part. */
24
+ inlineData: {
25
+ mimeType: string;
26
+ data: string;
27
+ };
28
+ error?: ToolsetResult;
29
+ }
30
+ /** Run one Gemini function call and return the parts Gemini expects back. */
31
+ export declare function runGeminiCall(o: GeminiAdapterOptions, fc: GeminiFunctionCall): Promise<GeminiStepResult>;
32
+ export {};
package/dist/gemini.js ADDED
@@ -0,0 +1,81 @@
1
+ // Gemini computer_use adapter: runs one predefined function call on an Allternit
2
+ // hosted computer. Gemini coordinates are 0–999 normalized, so every call is sent
3
+ // with coordinate_space "normalized_1000" and the server scales to the screen.
4
+ import { resultImage } from "./client.js";
5
+ const pt = (x, y) => [Math.round(Number(x)), Math.round(Number(y))];
6
+ const key = (text) => ({ member: "key", input: { text } });
7
+ const typed = (text) => ({ member: "type", input: { text } });
8
+ const goTo = (url) => [key("ctrl+l"), typed(url), key("Return")];
9
+ /** "Control+Shift+T" → "ctrl+shift+t". */
10
+ export function geminiKeys(keys) {
11
+ const map = { control: "ctrl", meta: "super", command: "super", enter: "Return", escape: "Escape", backspace: "BackSpace", tab: "Tab", delete: "Delete" };
12
+ return keys.split("+").map((k) => k.trim()).map((k) => map[k.toLowerCase()] ?? (k.length === 1 ? k.toLowerCase() : k)).join("+");
13
+ }
14
+ /** Contract calls for one Gemini computer_use function call, in order. */
15
+ export function geminiToCalls(fc, searchUrl = "https://www.google.com") {
16
+ const a = fc.args ?? {};
17
+ const dir = String(a.direction ?? "down");
18
+ switch (fc.name) {
19
+ case "open_web_browser":
20
+ return [];
21
+ case "wait_5_seconds":
22
+ return [{ member: "wait", input: { duration: 5 } }];
23
+ case "go_back":
24
+ return [key("alt+Left")];
25
+ case "go_forward":
26
+ return [key("alt+Right")];
27
+ case "search":
28
+ return goTo(searchUrl);
29
+ case "navigate":
30
+ return goTo(String(a.url ?? ""));
31
+ case "click_at":
32
+ return [{ member: "left_click", input: { coordinate: pt(a.x, a.y) } }];
33
+ case "hover_at":
34
+ return [{ member: "mouse_move", input: { coordinate: pt(a.x, a.y) } }];
35
+ case "type_text_at": {
36
+ const steps = [{ member: "left_click", input: { coordinate: pt(a.x, a.y) } }];
37
+ if (a.clear_before_typing !== false)
38
+ steps.push(key("ctrl+a"), key("BackSpace"));
39
+ steps.push(typed(String(a.text ?? "")));
40
+ if (a.press_enter !== false)
41
+ steps.push(key("Return"));
42
+ return steps;
43
+ }
44
+ case "key_combination":
45
+ return [key(geminiKeys(String(a.keys ?? "")))];
46
+ case "scroll_document":
47
+ return [{ member: "scroll", input: { coordinate: [500, 500], scroll_direction: dir, scroll_amount: 5 } }];
48
+ case "scroll_at": {
49
+ const amount = Math.max(1, Math.round(Number(a.magnitude ?? 800) / 160));
50
+ return [{ member: "scroll", input: { coordinate: pt(a.x, a.y), scroll_direction: dir, scroll_amount: amount } }];
51
+ }
52
+ case "drag_and_drop":
53
+ return [{ member: "left_click_drag", input: { start_coordinate: pt(a.x, a.y), coordinate: pt(a.destination_x, a.destination_y) } }];
54
+ default:
55
+ throw new Error(`Unsupported Gemini computer_use function: ${fc.name}`);
56
+ }
57
+ }
58
+ /** Run one Gemini function call and return the parts Gemini expects back. */
59
+ export async function runGeminiCall(o, fc) {
60
+ const base = { toolset: "computer", coordinate_space: "normalized_1000" };
61
+ let error;
62
+ for (const step of geminiToCalls(fc, o.searchUrl)) {
63
+ const r = await o.client.toolset(o.computerId, { ...base, ...step });
64
+ if (r.is_error) {
65
+ error = r;
66
+ break;
67
+ }
68
+ }
69
+ const s = await o.client.toolset(o.computerId, { ...base, member: "screenshot", input: {} });
70
+ const img = resultImage(s);
71
+ if (!img)
72
+ throw new Error("Allternit computer returned no screenshot.");
73
+ const response = { url: "" };
74
+ if (error)
75
+ response.error = error.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("\n");
76
+ return {
77
+ functionResponse: { name: fc.name, response },
78
+ inlineData: { mimeType: img.media_type, data: img.data },
79
+ ...(error ? { error } : {}),
80
+ };
81
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./client.ts";
2
+ export * from "./openai.ts";
3
+ export * from "./gemini.ts";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // @allternit/computer-driver: drive Allternit hosted computers from any model loop.
2
+ // The Anthropic toolsets live on the "@allternit/computer-driver/anthropic" subpath
3
+ // so this entry never loads the optional @anthropic-ai/sdk peer.
4
+ export * from "./client.js";
5
+ export * from "./openai.js";
6
+ export * from "./gemini.js";
@@ -0,0 +1,70 @@
1
+ import { type AllternitComputers, type ToolsetCall, type ToolsetResult } from "./client.ts";
2
+ type Pt = {
3
+ x: number;
4
+ y: number;
5
+ };
6
+ export type OpenAIComputerAction = {
7
+ type: "click";
8
+ x: number;
9
+ y: number;
10
+ button?: "left" | "right" | "wheel" | "back" | "forward";
11
+ } | {
12
+ type: "double_click";
13
+ x: number;
14
+ y: number;
15
+ } | {
16
+ type: "drag";
17
+ path: Pt[];
18
+ } | {
19
+ type: "keypress";
20
+ keys: string[];
21
+ } | {
22
+ type: "move";
23
+ x: number;
24
+ y: number;
25
+ } | {
26
+ type: "scroll";
27
+ x: number;
28
+ y: number;
29
+ scroll_x: number;
30
+ scroll_y: number;
31
+ } | {
32
+ type: "type";
33
+ text: string;
34
+ } | {
35
+ type: "wait";
36
+ ms?: number;
37
+ } | {
38
+ type: "screenshot";
39
+ };
40
+ export interface OpenAIAdapterOptions {
41
+ client: AllternitComputers;
42
+ computerId: string;
43
+ /** display_width / display_height you declared on the computer_use_preview tool. */
44
+ display?: {
45
+ width: number;
46
+ height: number;
47
+ };
48
+ /** Pixels of scroll per wheel click when converting scroll_x / scroll_y. Default 100. */
49
+ pixelsPerScrollClick?: number;
50
+ }
51
+ /** ["CTRL","C"] → "ctrl+c" (the contract's xdotool-style key text). */
52
+ export declare function openaiKeys(keys: string[]): string;
53
+ /** Contract calls for one OpenAI action, in order. */
54
+ export declare function openaiToCalls(a: OpenAIComputerAction, perClick?: number): Array<Pick<ToolsetCall, "member" | "input">>;
55
+ export interface OpenAIStepResult {
56
+ /** Ready for the Responses API `input`: `{type:"computer_call_output", call_id, output}`. */
57
+ output: {
58
+ type: "computer_call_output";
59
+ call_id: string;
60
+ output: {
61
+ type: "computer_screenshot";
62
+ image_url: string;
63
+ };
64
+ };
65
+ /** The last failed action result, if any action failed. */
66
+ error?: ToolsetResult;
67
+ }
68
+ /** Run one OpenAI `computer_call` (its `call_id` and `action`) and return the output item. */
69
+ export declare function runOpenAIAction(o: OpenAIAdapterOptions, callId: string, action: OpenAIComputerAction): Promise<OpenAIStepResult>;
70
+ export {};
package/dist/openai.js ADDED
@@ -0,0 +1,87 @@
1
+ // OpenAI computer-use adapter: runs one `computer_call` action on an Allternit
2
+ // hosted computer and returns a `computer_call_output` with the next screenshot.
3
+ import { resultImage } from "./client.js";
4
+ const KEYS = {
5
+ CTRL: "ctrl", CONTROL: "ctrl", ALT: "alt", OPTION: "alt", SHIFT: "shift", CMD: "super", META: "super", SUPER: "super",
6
+ WIN: "super", ENTER: "Return", RETURN: "Return", ESC: "Escape", ESCAPE: "Escape", TAB: "Tab", SPACE: "space",
7
+ BACKSPACE: "BackSpace", DELETE: "Delete", UP: "Up", DOWN: "Down", LEFT: "Left", RIGHT: "Right",
8
+ ARROWUP: "Up", ARROWDOWN: "Down", ARROWLEFT: "Left", ARROWRIGHT: "Right", HOME: "Home", END: "End",
9
+ PAGEUP: "Page_Up", PAGEDOWN: "Page_Down",
10
+ };
11
+ /** ["CTRL","C"] → "ctrl+c" (the contract's xdotool-style key text). */
12
+ export function openaiKeys(keys) {
13
+ return keys.map((k) => KEYS[k.toUpperCase()] ?? (k.length === 1 ? k.toLowerCase() : k)).join("+");
14
+ }
15
+ /** Contract calls for one OpenAI action, in order. */
16
+ export function openaiToCalls(a, perClick = 100) {
17
+ const c = (x, y) => [Math.round(x), Math.round(y)];
18
+ switch (a.type) {
19
+ case "click": {
20
+ const b = a.button ?? "left";
21
+ if (b === "back")
22
+ return [{ member: "key", input: { text: "alt+Left" } }];
23
+ if (b === "forward")
24
+ return [{ member: "key", input: { text: "alt+Right" } }];
25
+ const member = b === "right" ? "right_click" : b === "wheel" ? "middle_click" : "left_click";
26
+ return [{ member, input: { coordinate: c(a.x, a.y) } }];
27
+ }
28
+ case "double_click":
29
+ return [{ member: "double_click", input: { coordinate: c(a.x, a.y) } }];
30
+ case "drag": {
31
+ const p = a.path;
32
+ if (p.length < 2)
33
+ return [];
34
+ if (p.length === 2)
35
+ return [{ member: "left_click_drag", input: { start_coordinate: c(p[0].x, p[0].y), coordinate: c(p[1].x, p[1].y) } }];
36
+ return [
37
+ { member: "mouse_move", input: { coordinate: c(p[0].x, p[0].y) } },
38
+ { member: "left_mouse_down", input: {} },
39
+ ...p.slice(1).map((q) => ({ member: "mouse_move", input: { coordinate: c(q.x, q.y) } })),
40
+ { member: "left_mouse_up", input: {} },
41
+ ];
42
+ }
43
+ case "keypress":
44
+ return [{ member: "key", input: { text: openaiKeys(a.keys) } }];
45
+ case "move":
46
+ return [{ member: "mouse_move", input: { coordinate: c(a.x, a.y) } }];
47
+ case "scroll": {
48
+ const out = [];
49
+ const amt = (v) => Math.max(1, Math.round(Math.abs(v) / perClick));
50
+ if (a.scroll_y)
51
+ out.push({ member: "scroll", input: { coordinate: c(a.x, a.y), scroll_direction: a.scroll_y > 0 ? "down" : "up", scroll_amount: amt(a.scroll_y) } });
52
+ if (a.scroll_x)
53
+ out.push({ member: "scroll", input: { coordinate: c(a.x, a.y), scroll_direction: a.scroll_x > 0 ? "right" : "left", scroll_amount: amt(a.scroll_x) } });
54
+ return out;
55
+ }
56
+ case "type":
57
+ return [{ member: "type", input: { text: a.text } }];
58
+ case "wait":
59
+ return [{ member: "wait", input: { duration: Math.max(1, Math.round((a.ms ?? 2000) / 1000)) } }];
60
+ case "screenshot":
61
+ return [];
62
+ }
63
+ }
64
+ /** Run one OpenAI `computer_call` (its `call_id` and `action`) and return the output item. */
65
+ export async function runOpenAIAction(o, callId, action) {
66
+ const frame = o.display ? { model_frame: o.display, coordinate_space: "pixels" } : {};
67
+ let error;
68
+ for (const step of openaiToCalls(action, o.pixelsPerScrollClick)) {
69
+ const r = await o.client.toolset(o.computerId, { toolset: "computer", ...step, ...frame });
70
+ if (r.is_error) {
71
+ error = r;
72
+ break;
73
+ }
74
+ }
75
+ const s = await o.client.toolset(o.computerId, { toolset: "computer", member: "screenshot", input: {}, ...frame });
76
+ const img = resultImage(s);
77
+ if (!img)
78
+ throw new Error("Allternit computer returned no screenshot.");
79
+ return {
80
+ output: {
81
+ type: "computer_call_output",
82
+ call_id: callId,
83
+ output: { type: "computer_screenshot", image_url: `data:${img.media_type};base64,${img.data}` },
84
+ },
85
+ ...(error ? { error } : {}),
86
+ };
87
+ }
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@allternit/computer-driver",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Drop-in drivers for Allternit hosted computers: a plain client plus Anthropic (computer/browser toolset), OpenAI computer-use and Gemini computer_use adapters.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./anthropic": {
15
+ "types": "./dist/anthropic.d.ts",
16
+ "import": "./dist/anthropic.js"
17
+ },
18
+ "./openai": {
19
+ "types": "./dist/openai.d.ts",
20
+ "import": "./dist/openai.js"
21
+ },
22
+ "./gemini": {
23
+ "types": "./dist/gemini.d.ts",
24
+ "import": "./dist/gemini.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit"
37
+ },
38
+ "peerDependencies": {
39
+ "@anthropic-ai/sdk": ">=0.132.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@anthropic-ai/sdk": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "@anthropic-ai/sdk": "0.132.0",
48
+ "typescript": "^5.9.0"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/Allternit/allternit-platform.git",
53
+ "directory": "sdk/computer-driver"
54
+ },
55
+ "homepage": "https://docs.allternit.com/api/platform/sdks/typescript",
56
+ "publishConfig": {
57
+ "access": "public"
58
+ }
59
+ }