@allternit/computer-driver 0.0.0-stage → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-10-10
4
+
5
+ ### Added
6
+
7
+ - **Contract v2 (`allternit.computer.v2`) structured members.** Typed client methods for all ten driver-backed members — `read_ui`, `act`, `run_batch`, `verify`, `request_human`, `use_credential`, `run_subtask`, `run_parallel`, `run_skill`, `skills` — on the new `ComputerV2Driver` (exported from the package root). Input and result types are generated from `contracts/computer-toolset/allternit-computer-v2.json` (`src/v2-generated.ts`, emitted by `contracts/computer-toolset/generate.mjs`), so they cannot drift from the server contract.
8
+ - **`computer_v2` function tool next to the pixel tool in every adapter**, with the same steering guidance gizzi sends: `computerV2AnthropicTool` + `runAnthropicComputerV2` (anthropic subpath), `openaiComputerV2Tool` + `runOpenAIV2Call`, `geminiComputerV2Declaration` + `runGeminiV2Call`, plus the provider-neutral `computerV2Tool()`.
9
+ - **Approval flow helper.** `client.toolsetWithApproval(id, call, onApproval)` answers a 409 `approval_required` hold (approve + resend with the single-use `approval_grant`) for any member, pixel or structured.
10
+ - **Typed errors.** `ComputerV2Error` (a structured member answered `is_error`), `ComputerBusyError` (423 `computer_busy` / `computer_controlled_elsewhere`), `SandboxRequiredError` (409 `sandbox_required`) and `ComputerConflictError` (409 `computer_conflict`).
11
+
12
+ ### Notes
13
+
14
+ - Requires the Allternit Driver sidecar on the computer for the structured members (this-device today). The 17 pixel members are unchanged.
package/README.md CHANGED
@@ -1,3 +1,105 @@
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
+ ## Structured UI driving (allternit.computer.v2)
12
+
13
+ Contract v2 adds ten driver-backed structured members — `read_ui`, `act`, `run_batch`, `verify`, `request_human`, `use_credential`, `run_subtask`, `run_parallel`, `run_skill`, `skills` — typed end to end. `ComputerV2Driver` calls them directly; every adapter also exposes them to the model as the `computer_v2` function tool. The structured members need the Allternit Driver on the computer (this-device today).
14
+
15
+ ```ts
16
+ import { ComputerV2Driver } from "@allternit/computer-driver"
17
+ const v2 = new ComputerV2Driver({ client, computerId: computer.id, onApproval: async (a) => askAPerson(a) })
18
+
19
+ // Read the UI as an element tree — no screenshots.
20
+ const ui = await v2.read_ui({ app: "Safari" })
21
+ const field = ui.elements?.find((e) => e.role === "textfield")
22
+ if (field) await v2.act({ id: field.id, op: "set_value", value: "hello@example.com", version: ui.version })
23
+
24
+ // Hand a bounded step sequence to the fast decision loop instead of clicking through it yourself.
25
+ const sub = await v2.run_subtask({
26
+ goal: "Fill the signup form and submit it",
27
+ inputs: [{ name: "email", value: "hello@example.com" }],
28
+ success: [{ role: "button", name: "Submit" }],
29
+ })
30
+ if (sub.status !== "done") console.log(sub.status, sub.next)
31
+ ```
32
+
33
+ ### Subtask safety statuses
34
+
35
+ `run_subtask` / `run_skill` / `run_parallel` results end with a `status`:
36
+
37
+ - `done` — the goal is met.
38
+ - `escalated` — handed back with a `reason` and the current `screen`; continue from there yourself.
39
+ - `needs_confirmation` — the next step (`held_step`) needs the person's confirmation; run it yourself with `act`/`run_batch` and the server asks them.
40
+ - `paused` — the safety monitor paused the subtask; stop and call `request_human`.
41
+ - `denied` — the step isn't allowed on this computer (app/domain lists or a credential binding); find another way.
42
+ - `use_api` — make the named API/MCP call yourself (`api.tool`); the screen is untouched.
43
+ - `failed` — ran out of steps, budget or a hard error.
44
+
45
+ ### The computer_v2 function tool in a model loop
46
+
47
+ Each adapter exposes the structured members with the same steering guidance gizzi sends:
48
+
49
+ ```ts
50
+ import { computerV2AnthropicTool, runAnthropicComputerV2 } from "@allternit/computer-driver/anthropic"
51
+ import { openaiComputerV2Tool, runOpenAIV2Call } from "@allternit/computer-driver"
52
+ import { geminiComputerV2Declaration, runGeminiV2Call } from "@allternit/computer-driver"
53
+
54
+ // Anthropic: put computerV2AnthropicTool() next to the toolset in tools, and answer
55
+ // tool_use blocks whose name is "computer_v2" with runAnthropicComputerV2({ client, computerId, onApproval }, name, input).
56
+ // OpenAI: put openaiComputerV2Tool() in tools and answer function_call items with
57
+ // runOpenAIV2Call({ client, computerId }, call.call_id, JSON.parse(call.arguments)).
58
+ // Gemini: put geminiComputerV2Declaration() in functionDeclarations and answer
59
+ // functionCall parts with runGeminiV2Call({ client, computerId }, part.functionCall.name, part.functionCall.args).
60
+ ```
61
+
62
+ `computerV2Tool()` returns the provider-neutral `{ name, description, schema }` if you wire tools yourself.
63
+
64
+ ## Claude (`computer_toolset_20260801` / `browser_toolset_20260801`)
65
+
66
+ Requires `@anthropic-ai/sdk` >= 0.132.0 (optional peer).
67
+
68
+ ```ts
69
+ import { AllternitComputerToolset } from "@allternit/computer-driver/anthropic"
70
+ const toolset = new AllternitComputerToolset({
71
+ client, computerId: computer.id,
72
+ onApproval: async (approval) => askAPerson(approval), // server-held risky calls
73
+ })
74
+ // pass `toolset` to client.beta.messages.toolRunner({ tools: [toolset, computerV2AnthropicTool()], ... })
75
+ ```
76
+
77
+ 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`.
78
+
79
+ ## OpenAI computer use
80
+
81
+ ```ts
82
+ import { runOpenAIAction } from "@allternit/computer-driver"
83
+ const { output } = await runOpenAIAction({ client, computerId, display: { width: 1280, height: 800 } }, call.call_id, call.action)
84
+ // push `output` into the next responses.create input
85
+ ```
86
+
87
+ ## Gemini computer use
88
+
89
+ ```ts
90
+ import { runGeminiCall } from "@allternit/computer-driver"
91
+ const { functionResponse, inlineData } = await runGeminiCall({ client, computerId }, part.functionCall)
92
+ ```
93
+
94
+ Gemini's 0–999 coordinates are sent as `coordinate_space: "normalized_1000"`; the server scales them.
95
+
96
+ The OpenAI and Gemini pixel adapters let `ApprovalRequiredError` propagate; catch it, call `client.approve()`, and resend the action.
97
+
98
+ ## Errors
99
+
100
+ - `ApprovalRequiredError` — 409 `approval_required`; carries `approval` (id, member, `approve_url`) and the held `result`. `client.toolsetWithApproval(id, call, onApproval)` approves and resends with the `approval_grant` for you.
101
+ - `ComputerV2Error` — a structured member answered `is_error: true`; carries the member name and raw `result`.
102
+ - `ComputerBusyError` — 423 `computer_busy` / `computer_controlled_elsewhere`.
103
+ - `SandboxRequiredError` — 409 `sandbox_required` (the call needs a sandbox computer).
104
+ - `ComputerConflictError` — 409 `computer_conflict` (another subtask or lease conflicts).
105
+ - `AllternitApiError` — everything else, with `status`, `code`, `type`.
@@ -0,0 +1,43 @@
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
+ }
26
+ /**
27
+ * The `computer_v2` function tool for Anthropic models: the ten structured
28
+ * members (read_ui … skills) next to the native pixel toolset. The Anthropic
29
+ * wire hook only rewrites tools carrying the v1 pixel marker, so this passes
30
+ * through untouched.
31
+ */
32
+ export declare function computerV2AnthropicTool(): {
33
+ name: string;
34
+ description: string;
35
+ input_schema: Record<string, unknown>;
36
+ };
37
+ /**
38
+ * Run one `computer_v2` function call (`{action, ...fields}`) and return the
39
+ * tool-result content blocks, like any toolset member. Throws ToolError on a
40
+ * failed action; without `onApproval`, a held call (409 approval_required)
41
+ * resolves its held result as an error, the same rule as the toolsets.
42
+ */
43
+ export declare function runAnthropicComputerV2(o: DriverOptions, name: string, input: unknown): Promise<any[]>;
@@ -0,0 +1,157 @@
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
+ import { computerV2Tool, runComputerV2Member } from "./v2.js";
7
+ /**
8
+ * Default SDK `confirm`: let the call through. The Allternit server is the gate: it applies the
9
+ * project's member policy and holds risky calls with a 409, which `onApproval` answers. Pass your
10
+ * own `confirm` to also prompt locally before the call leaves the process.
11
+ */
12
+ const serverDecides = () => true;
13
+ async function callMember(o, toolset, ctx, member, input) {
14
+ const call = {
15
+ toolset,
16
+ member,
17
+ input: (input ?? {}),
18
+ // The SDK only calls members its toolset config turned on, which is the
19
+ // opt-in the executor asks for on off-by-default members.
20
+ enable: [member],
21
+ ...(o.callIds?.(ctx, member) ?? {}),
22
+ ...(o.browserSessionId ? { browser_session_id: o.browserSessionId } : {}),
23
+ };
24
+ let res;
25
+ try {
26
+ res = await o.client.toolset(o.computerId, call);
27
+ }
28
+ catch (e) {
29
+ if (!(e instanceof ApprovalRequiredError))
30
+ throw e;
31
+ if (!o.onApproval || !(await o.onApproval(e.approval)))
32
+ res = e.result;
33
+ else {
34
+ await o.client.approve(o.computerId, e.approval.id);
35
+ res = await o.client.toolset(o.computerId, { ...call, approval_grant: e.approval.id });
36
+ }
37
+ }
38
+ if (res.is_error)
39
+ throw new ToolError(toAnthropicBlocks(res));
40
+ return res;
41
+ }
42
+ function toAnthropicBlocks(r) {
43
+ const blocks = r.content.map((b) => b.type === "text"
44
+ ? { type: "text", text: b.text }
45
+ : { type: "image", source: { type: "base64", media_type: b.media_type, data: b.data } });
46
+ return blocks.length ? blocks : [{ type: "text", text: "The action failed." }];
47
+ }
48
+ function shot(r) {
49
+ const img = resultImage(r);
50
+ if (!img)
51
+ throw new ToolError("The computer returned no screenshot.");
52
+ return { data: img.data, mediaType: img.media_type };
53
+ }
54
+ function textOrVoid(r) {
55
+ const t = resultText(r);
56
+ return t ? t : undefined;
57
+ }
58
+ function jsonOf(r) {
59
+ try {
60
+ return JSON.parse(resultText(r));
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ export class AllternitComputerToolset extends BetaAbstractComputerToolset20260801 {
67
+ #o;
68
+ constructor(o) {
69
+ const { client: _c, computerId: _i, onApproval: _a, callIds: _d, browserSessionId: _b, ...sdk } = o;
70
+ super({ ...sdk, confirm: sdk.confirm ?? serverDecides });
71
+ this.#o = o;
72
+ }
73
+ async execute(ctx, name, input) {
74
+ const r = await callMember(this.#o, "computer", ctx, name, input);
75
+ if (name === "screenshot" || name === "zoom")
76
+ return shot(r);
77
+ if (name === "cursor_position") {
78
+ const j = jsonOf(r);
79
+ if (j && typeof j.x === "number")
80
+ return { x: j.x, y: j.y };
81
+ const m = /(-?\d+)\D+(-?\d+)/.exec(resultText(r));
82
+ if (!m)
83
+ throw new ToolError("The computer returned no cursor position.");
84
+ return { x: Number(m[1]), y: Number(m[2]) };
85
+ }
86
+ return textOrVoid(r);
87
+ }
88
+ }
89
+ export class AllternitBrowserToolset extends BetaAbstractBrowserToolset20260801 {
90
+ #o;
91
+ #state;
92
+ constructor(o) {
93
+ const { client: _c, computerId: _i, onApproval: _a, callIds: _d, browserSessionId: _b, ...sdk } = o;
94
+ const self = {};
95
+ super({ ...sdk, confirm: sdk.confirm ?? serverDecides, browserState: sdk.browserState ?? ((ctx) => self.t.#currentState(ctx)) });
96
+ this.#o = o;
97
+ self.t = this;
98
+ }
99
+ async #currentState(ctx) {
100
+ if (this.#state)
101
+ return this.#state;
102
+ const r = await callMember(this.#o, "browser", ctx, "list_tabs", {});
103
+ return (this.#state = r.browser_state ?? { tabs: jsonOf(r) ?? [] });
104
+ }
105
+ async execute(ctx, name, input) {
106
+ const r = await callMember(this.#o, "browser", ctx, name, input);
107
+ if (r.browser_state)
108
+ this.#state = r.browser_state;
109
+ if (name === "screenshot" || name === "zoom")
110
+ return shot(r);
111
+ const j = jsonOf(r);
112
+ const tabs = this.#state?.tabs ?? [];
113
+ switch (name) {
114
+ case "navigate":
115
+ return j?.url ? j : { url: tabs[0]?.url ?? String(input?.url ?? "") };
116
+ case "list_tabs":
117
+ return Array.isArray(j) ? j : tabs;
118
+ case "new_tab":
119
+ case "switch_tab":
120
+ if (j?.tab_id)
121
+ return j;
122
+ if (tabs[0])
123
+ return tabs[0];
124
+ throw new ToolError("The browser returned no tab.");
125
+ case "close_tab":
126
+ return undefined;
127
+ default:
128
+ return textOrVoid(r);
129
+ }
130
+ }
131
+ }
132
+ // ------------------------------------------------------------------ computer_v2
133
+ /**
134
+ * The `computer_v2` function tool for Anthropic models: the ten structured
135
+ * members (read_ui … skills) next to the native pixel toolset. The Anthropic
136
+ * wire hook only rewrites tools carrying the v1 pixel marker, so this passes
137
+ * through untouched.
138
+ */
139
+ export function computerV2AnthropicTool() {
140
+ const t = computerV2Tool();
141
+ return { name: t.name, description: t.description, input_schema: t.schema };
142
+ }
143
+ /**
144
+ * Run one `computer_v2` function call (`{action, ...fields}`) and return the
145
+ * tool-result content blocks, like any toolset member. Throws ToolError on a
146
+ * failed action; without `onApproval`, a held call (409 approval_required)
147
+ * resolves its held result as an error, the same rule as the toolsets.
148
+ */
149
+ export async function runAnthropicComputerV2(o, name, input) {
150
+ const { action, ...fields } = (input ?? {});
151
+ if (!action)
152
+ throw new ToolError("The computer_v2 call needs an action.");
153
+ const res = await runComputerV2Member({ client: o.client, computerId: o.computerId, onApproval: o.onApproval }, action, fields);
154
+ if (res.is_error)
155
+ throw new ToolError(toAnthropicBlocks(res));
156
+ return toAnthropicBlocks(res);
157
+ }
@@ -0,0 +1,157 @@
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
+ /** 423 computer_busy / computer_controlled_elsewhere: someone else holds the computer right now; retry shortly or request_human. */
96
+ export declare class ComputerBusyError extends AllternitApiError {
97
+ constructor(status: number, err: Partial<ApiErrorBody>, body: unknown);
98
+ }
99
+ /** 409 sandbox_required: the call only runs on a sandbox (cloud/bot) computer. */
100
+ export declare class SandboxRequiredError extends AllternitApiError {
101
+ constructor(status: number, err: Partial<ApiErrorBody>, body: unknown);
102
+ }
103
+ /** 409 computer_conflict: the call conflicts with another subtask or lease on the computer. */
104
+ export declare class ComputerConflictError extends AllternitApiError {
105
+ constructor(status: number, err: Partial<ApiErrorBody>, body: unknown);
106
+ }
107
+ export interface ClientOptions {
108
+ /** Project key (alt_live_… / alt_test_…). Defaults to ALLTERNIT_API_KEY. */
109
+ apiKey?: string;
110
+ baseUrl?: string;
111
+ fetch?: typeof fetch;
112
+ }
113
+ export declare class AllternitComputers {
114
+ #private;
115
+ readonly baseUrl: string;
116
+ constructor(opts?: ClientOptions);
117
+ create(body?: {
118
+ name?: string;
119
+ account_id?: string;
120
+ metadata?: Record<string, unknown>;
121
+ }): Promise<Computer>;
122
+ list(q?: {
123
+ limit?: number;
124
+ after?: string;
125
+ }): Promise<Page<Computer>>;
126
+ get(id: string): Promise<Computer>;
127
+ start(id: string): Promise<Computer>;
128
+ stop(id: string): Promise<Computer>;
129
+ delete(id: string): Promise<Computer>;
130
+ /** Run one toolset member. Action failures resolve with is_error:true; a held call throws ApprovalRequiredError. */
131
+ toolset(id: string, call: ToolsetCall): Promise<ToolsetResult>;
132
+ /**
133
+ * Run one toolset member, answering a 409 approval_required hold. When the
134
+ * server holds the call and `onApproval` returns true, the approval is
135
+ * granted (POST /approvals/{id}) and the same call resent with the
136
+ * single-use `approval_grant`. When `onApproval` is missing or returns
137
+ * false, the held result resolves (is_error:true) so the model sees it.
138
+ */
139
+ toolsetWithApproval(id: string, call: ToolsetCall, onApproval?: (approval: Approval) => boolean | Promise<boolean>): Promise<ToolsetResult>;
140
+ schema(id: string, toolset?: ToolsetName): Promise<Record<string, unknown>>;
141
+ events(id: string, q?: {
142
+ after?: string;
143
+ limit?: number;
144
+ }): Promise<Page<ComputerEvent>>;
145
+ approve(id: string, approvalId: string): Promise<{
146
+ approval_id: string;
147
+ approved: true;
148
+ approval_grant: string;
149
+ }>;
150
+ }
151
+ /** Text of a result, joined. */
152
+ export declare function resultText(r: ToolsetResult): string;
153
+ /** First image of a result, if any. */
154
+ export declare function resultImage(r: ToolsetResult): {
155
+ media_type: string;
156
+ data: string;
157
+ } | undefined;
package/dist/client.js ADDED
@@ -0,0 +1,173 @@
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
+ /** 423 computer_busy / computer_controlled_elsewhere: someone else holds the computer right now; retry shortly or request_human. */
32
+ export class ComputerBusyError extends AllternitApiError {
33
+ constructor(status, err, body) {
34
+ super(status, err, body);
35
+ this.name = "ComputerBusyError";
36
+ }
37
+ }
38
+ /** 409 sandbox_required: the call only runs on a sandbox (cloud/bot) computer. */
39
+ export class SandboxRequiredError extends AllternitApiError {
40
+ constructor(status, err, body) {
41
+ super(status, err, body);
42
+ this.name = "SandboxRequiredError";
43
+ }
44
+ }
45
+ /** 409 computer_conflict: the call conflicts with another subtask or lease on the computer. */
46
+ export class ComputerConflictError extends AllternitApiError {
47
+ constructor(status, err, body) {
48
+ super(status, err, body);
49
+ this.name = "ComputerConflictError";
50
+ }
51
+ }
52
+ /** Maps a toolset error body onto the typed error for its code, if one exists. */
53
+ function typedApiError(status, err, body) {
54
+ switch (err.code) {
55
+ case "computer_busy":
56
+ case "computer_controlled_elsewhere":
57
+ return new ComputerBusyError(status, err, body);
58
+ case "sandbox_required":
59
+ return new SandboxRequiredError(status, err, body);
60
+ case "computer_conflict":
61
+ return new ComputerConflictError(status, err, body);
62
+ default:
63
+ return new AllternitApiError(status, err, body);
64
+ }
65
+ }
66
+ export class AllternitComputers {
67
+ baseUrl;
68
+ #apiKey;
69
+ #fetch;
70
+ constructor(opts = {}) {
71
+ const env = globalThis.process?.env;
72
+ const key = opts.apiKey ?? env?.ALLTERNIT_API_KEY;
73
+ if (!key)
74
+ throw new Error("AllternitComputers: apiKey is required (or set ALLTERNIT_API_KEY).");
75
+ this.#apiKey = key;
76
+ this.baseUrl = (opts.baseUrl ?? env?.ALLTERNIT_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
77
+ this.#fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
78
+ }
79
+ create(body = {}) {
80
+ return this.#req("POST", "/v1/computers", body);
81
+ }
82
+ list(q = {}) {
83
+ return this.#req("GET", `/v1/computers${qs(q)}`);
84
+ }
85
+ get(id) {
86
+ return this.#req("GET", `/v1/computers/${enc(id)}`);
87
+ }
88
+ start(id) {
89
+ return this.#req("POST", `/v1/computers/${enc(id)}/start`, {});
90
+ }
91
+ stop(id) {
92
+ return this.#req("POST", `/v1/computers/${enc(id)}/stop`, {});
93
+ }
94
+ delete(id) {
95
+ return this.#req("DELETE", `/v1/computers/${enc(id)}`);
96
+ }
97
+ /** Run one toolset member. Action failures resolve with is_error:true; a held call throws ApprovalRequiredError. */
98
+ toolset(id, call) {
99
+ return this.#req("POST", `/v1/computers/${enc(id)}/toolset`, call);
100
+ }
101
+ /**
102
+ * Run one toolset member, answering a 409 approval_required hold. When the
103
+ * server holds the call and `onApproval` returns true, the approval is
104
+ * granted (POST /approvals/{id}) and the same call resent with the
105
+ * single-use `approval_grant`. When `onApproval` is missing or returns
106
+ * false, the held result resolves (is_error:true) so the model sees it.
107
+ */
108
+ async toolsetWithApproval(id, call, onApproval) {
109
+ try {
110
+ return await this.toolset(id, call);
111
+ }
112
+ catch (e) {
113
+ if (!(e instanceof ApprovalRequiredError))
114
+ throw e;
115
+ if (!onApproval || !(await onApproval(e.approval)))
116
+ return e.result;
117
+ await this.approve(id, e.approval.id);
118
+ return this.toolset(id, { ...call, approval_grant: e.approval.id });
119
+ }
120
+ }
121
+ schema(id, toolset = "computer") {
122
+ return this.#req("GET", `/v1/computers/${enc(id)}/toolset/schema${qs({ toolset })}`);
123
+ }
124
+ events(id, q = {}) {
125
+ return this.#req("GET", `/v1/computers/${enc(id)}/events${qs(q)}`);
126
+ }
127
+ approve(id, approvalId) {
128
+ return this.#req("POST", `/v1/computers/${enc(id)}/approvals/${enc(approvalId)}`, {});
129
+ }
130
+ async #req(method, path, body) {
131
+ const headers = { Authorization: `Bearer ${this.#apiKey}`, Accept: "application/json" };
132
+ if (body !== undefined)
133
+ headers["Content-Type"] = "application/json";
134
+ if (method === "POST")
135
+ headers["Idempotency-Key"] = crypto.randomUUID();
136
+ const res = await this.#fetch(this.baseUrl + path, {
137
+ method,
138
+ headers,
139
+ body: body === undefined ? undefined : JSON.stringify(body),
140
+ });
141
+ const text = await res.text();
142
+ let json = undefined;
143
+ try {
144
+ json = text ? JSON.parse(text) : undefined;
145
+ }
146
+ catch {
147
+ json = { error: { message: text } };
148
+ }
149
+ if (res.ok)
150
+ return json;
151
+ const err = json?.error ?? {};
152
+ if (res.status === 409 && err.code === "approval_required" && json?.approval) {
153
+ throw new ApprovalRequiredError(err, json.approval, json.result ?? { is_error: true, content: [] }, json);
154
+ }
155
+ throw typedApiError(res.status, err, json);
156
+ }
157
+ }
158
+ /** Text of a result, joined. */
159
+ export function resultText(r) {
160
+ return r.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("\n");
161
+ }
162
+ /** First image of a result, if any. */
163
+ export function resultImage(r) {
164
+ const img = r.content.find((b) => b.type === "image");
165
+ return img && img.type === "image" ? { media_type: img.media_type, data: img.data } : undefined;
166
+ }
167
+ function enc(s) {
168
+ return encodeURIComponent(s);
169
+ }
170
+ function qs(q) {
171
+ const p = Object.entries(q).filter(([, v]) => v !== undefined && v !== "");
172
+ return p.length ? "?" + new URLSearchParams(p.map(([k, v]) => [k, String(v)])).toString() : "";
173
+ }