@shardflux/sdk 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Codex identity proof (0.14.0+, Node): the OpenAI ID token of the Codex CLI signed in on this machine, for
3
+ * `ShardfluxAccount.signup({ codexIdToken })` and `auth.stepUp({ codexIdToken })`.
4
+ *
5
+ * Codex keeps the token in `$CODEX_HOME/auth.json` (default `~/.codex/auth.json`) and refreshes it only every few
6
+ * days, while the API accepts one issued in the last 15 minutes. So this first asks Codex to refresh its own login
7
+ * (`codex app-server`, JSON-RPC `account/read {"refreshToken": true}`: Codex runs its normal refresh flow and writes
8
+ * its own file), then reads the token. Only the ID token is returned: Codex's access and refresh tokens are never read
9
+ * into memory beyond parsing the file, and never leave the machine. The ID token proves the ChatGPT account's verified
10
+ * email; it is not a credential to OpenAI.
11
+ *
12
+ * const proof = await codexIdentityProof();
13
+ * if (proof.ok) {
14
+ * const { account, result } = await ShardfluxAccount.signup({ codexIdToken: proof.idToken });
15
+ * }
16
+ */
17
+ export type CodexProofUnavailable =
18
+ /** No `codex` executable on PATH (or CODEX_BIN), and no usable auth.json. */
19
+ 'codex_not_found'
20
+ /** Codex is not signed in (no auth.json, or no ID token in it: signed in with an API key, or credentials in the OS keyring). */
21
+ | 'not_signed_in'
22
+ /** Codex is signed in with an OpenAI API key, which carries no identity. */
23
+ | 'api_key_login'
24
+ /** The token on disk is too old and Codex could not refresh it. */
25
+ | 'stale';
26
+ export type CodexIdentityProof = {
27
+ ok: true;
28
+ idToken: string;
29
+ email: string | null;
30
+ refreshed: boolean;
31
+ } | {
32
+ ok: false;
33
+ reason: CodexProofUnavailable;
34
+ message: string;
35
+ };
36
+ export interface CodexProofOptions {
37
+ /** Environment to read CODEX_HOME, CODEX_BIN and HOME from. Default `process.env`. */
38
+ env?: Record<string, string | undefined>;
39
+ /** Longest wait for `codex app-server` (ms). Default 15 000. */
40
+ timeoutMs?: number;
41
+ /** Skip the refresh and only read the file (tests, or when Codex must not be started). */
42
+ refresh?: boolean;
43
+ /** Accept a token issued at most this long ago (s), e.g. when Codex cannot refresh. Default 600 (the API allows 900). */
44
+ maxAgeSeconds?: number;
45
+ /** clientInfo sent to the app server. */
46
+ clientName?: string;
47
+ clientVersion?: string;
48
+ }
49
+ /**
50
+ * The ID token of this machine's Codex login, refreshed by Codex when needed. Never throws: `{ ok: false, reason }`
51
+ * says why there is none (sign up with an email address instead).
52
+ */
53
+ export declare function codexIdentityProof(opts?: CodexProofOptions): Promise<CodexIdentityProof>;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Codex identity proof (0.14.0+, Node): the OpenAI ID token of the Codex CLI signed in on this machine, for
3
+ * `ShardfluxAccount.signup({ codexIdToken })` and `auth.stepUp({ codexIdToken })`.
4
+ *
5
+ * Codex keeps the token in `$CODEX_HOME/auth.json` (default `~/.codex/auth.json`) and refreshes it only every few
6
+ * days, while the API accepts one issued in the last 15 minutes. So this first asks Codex to refresh its own login
7
+ * (`codex app-server`, JSON-RPC `account/read {"refreshToken": true}`: Codex runs its normal refresh flow and writes
8
+ * its own file), then reads the token. Only the ID token is returned: Codex's access and refresh tokens are never read
9
+ * into memory beyond parsing the file, and never leave the machine. The ID token proves the ChatGPT account's verified
10
+ * email; it is not a credential to OpenAI.
11
+ *
12
+ * const proof = await codexIdentityProof();
13
+ * if (proof.ok) {
14
+ * const { account, result } = await ShardfluxAccount.signup({ codexIdToken: proof.idToken });
15
+ * }
16
+ */
17
+ function claimsOf(token) {
18
+ const part = token.split('.')[1];
19
+ if (!part)
20
+ return null;
21
+ try {
22
+ return JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function fresh(token, maxAgeSeconds) {
29
+ const c = claimsOf(token);
30
+ const now = Date.now() / 1000;
31
+ return c !== null && typeof c.iat === 'number' && typeof c.exp === 'number' && now - c.iat <= maxAgeSeconds && c.exp - now > 60;
32
+ }
33
+ async function codexHome(env) {
34
+ const path = await import('node:path');
35
+ const os = await import('node:os');
36
+ const set = env.CODEX_HOME?.trim();
37
+ return set ? path.resolve(set) : path.join(env.HOME?.trim() || os.homedir(), '.codex');
38
+ }
39
+ /** `tokens.id_token` of auth.json, or null (missing file, other shape, API-key login). */
40
+ async function readIdToken(home) {
41
+ const fs = await import('node:fs/promises');
42
+ const path = await import('node:path');
43
+ let raw;
44
+ try {
45
+ raw = await fs.readFile(path.join(home, 'auth.json'), 'utf8');
46
+ }
47
+ catch {
48
+ return { token: null, apiKeyOnly: false, exists: false };
49
+ }
50
+ try {
51
+ const doc = JSON.parse(raw);
52
+ const token = typeof doc.tokens?.id_token === 'string' && doc.tokens.id_token.length > 0 ? doc.tokens.id_token : null;
53
+ const apiKeyOnly = token === null && (doc.auth_mode === 'apikey' || (typeof doc.OPENAI_API_KEY === 'string' && doc.OPENAI_API_KEY.length > 0));
54
+ return { token, apiKeyOnly, exists: true };
55
+ }
56
+ catch {
57
+ return { token: null, apiKeyOnly: false, exists: true };
58
+ }
59
+ }
60
+ /**
61
+ * The `codex` executables to try, in PATH order (CODEX_BIN alone when set). Every match is kept, not only the first:
62
+ * npx puts ancestor node_modules/.bin directories first on PATH, where an unrelated npm package named `codex` can
63
+ * shadow the Codex CLI. Distinct by real path.
64
+ */
65
+ async function codexCandidates(env) {
66
+ const set = env.CODEX_BIN?.trim();
67
+ if (set)
68
+ return [set];
69
+ const fs = await import('node:fs/promises');
70
+ const path = await import('node:path');
71
+ const names = process.platform === 'win32' ? ['codex.exe', 'codex.cmd', 'codex'] : ['codex'];
72
+ const out = [];
73
+ const seen = new Set();
74
+ for (const dir of (env.PATH ?? '').split(path.delimiter)) {
75
+ if (!dir)
76
+ continue;
77
+ for (const name of names) {
78
+ const file = path.join(dir, name);
79
+ try {
80
+ await fs.access(file, fs.constants.X_OK);
81
+ const real = await fs.realpath(file);
82
+ if (seen.has(real))
83
+ continue;
84
+ seen.add(real);
85
+ out.push(file);
86
+ }
87
+ catch {
88
+ // not there, or not executable
89
+ }
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+ /** Asks Codex to refresh its own login through the app server of the first candidate that speaks its protocol. */
95
+ async function refreshViaAppServer(env, opts) {
96
+ const candidates = await codexCandidates(env);
97
+ if (candidates.length === 0)
98
+ return 'not_found';
99
+ let outcome = 'not_found';
100
+ for (const bin of candidates.slice(0, 4)) {
101
+ outcome = await appServerRefresh(bin, env, opts);
102
+ if (outcome !== 'failed' && outcome !== 'not_found')
103
+ return outcome;
104
+ }
105
+ return outcome;
106
+ }
107
+ async function appServerRefresh(bin, env, opts) {
108
+ const { spawn } = await import('node:child_process');
109
+ return new Promise((resolve) => {
110
+ let settled = false;
111
+ let buffer = '';
112
+ const child = spawn(bin, ['app-server'], { stdio: ['pipe', 'pipe', 'ignore'], env, windowsHide: true });
113
+ const done = (o) => {
114
+ if (settled)
115
+ return;
116
+ settled = true;
117
+ clearTimeout(timer);
118
+ child.stdin.end();
119
+ child.kill();
120
+ resolve(o);
121
+ };
122
+ const timer = setTimeout(() => done('failed'), opts.timeoutMs);
123
+ const send = (msg) => child.stdin.write(`${JSON.stringify(msg)}\n`);
124
+ child.on('error', (err) => done(err.code === 'ENOENT' ? 'not_found' : 'failed'));
125
+ child.on('exit', () => done('failed'));
126
+ child.stdin.on('error', () => done('failed'));
127
+ child.stdout.setEncoding('utf8');
128
+ child.stdout.on('data', (chunk) => {
129
+ buffer += chunk;
130
+ let nl;
131
+ while ((nl = buffer.indexOf('\n')) >= 0) {
132
+ const line = buffer.slice(0, nl).trim();
133
+ buffer = buffer.slice(nl + 1);
134
+ if (line.length === 0)
135
+ continue;
136
+ let msg;
137
+ try {
138
+ msg = JSON.parse(line);
139
+ }
140
+ catch {
141
+ continue;
142
+ }
143
+ if (msg.id === 1) {
144
+ if (msg.error !== undefined)
145
+ return done('failed');
146
+ send({ jsonrpc: '2.0', method: 'initialized' });
147
+ send({ jsonrpc: '2.0', id: 2, method: 'account/read', params: { refreshToken: true } });
148
+ }
149
+ else if (msg.id === 2) {
150
+ if (msg.error !== undefined)
151
+ return done('failed');
152
+ const type = msg.result?.account?.type;
153
+ return done(type === 'chatgpt' ? 'refreshed' : type === 'apiKey' ? 'api_key_login' : type === undefined || type === null ? 'not_signed_in' : 'failed');
154
+ }
155
+ }
156
+ });
157
+ send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { clientInfo: { name: opts.clientName, version: opts.clientVersion } } });
158
+ });
159
+ }
160
+ /**
161
+ * The ID token of this machine's Codex login, refreshed by Codex when needed. Never throws: `{ ok: false, reason }`
162
+ * says why there is none (sign up with an email address instead).
163
+ */
164
+ export async function codexIdentityProof(opts = {}) {
165
+ const env = opts.env ?? process.env;
166
+ const maxAge = opts.maxAgeSeconds ?? 600;
167
+ const home = await codexHome(env);
168
+ let outcome = 'skipped';
169
+ // Always refresh first: each refresh mints a token with a new jti, and the API accepts each jti once, so a token
170
+ // still fresh on disk may already have been used. The file alone is the fallback when Codex cannot refresh.
171
+ let file = await readIdToken(home);
172
+ let renewed = false;
173
+ if (opts.refresh !== false) {
174
+ const appServer = { timeoutMs: opts.timeoutMs ?? 15_000, clientName: opts.clientName ?? 'shardflux', clientVersion: opts.clientVersion ?? '0' };
175
+ const before = file.token;
176
+ // A refresh counts only when the token on disk changed. One retry: a refresh can fail on a transient network
177
+ // error, answer without renewing, or race another Codex process refreshing the same login.
178
+ for (let attempt = 0; attempt < 2 && !renewed; attempt++) {
179
+ if (attempt > 0)
180
+ await new Promise((r) => setTimeout(r, 500));
181
+ outcome = await refreshViaAppServer(env, appServer);
182
+ file = await readIdToken(home);
183
+ renewed = file.token !== null && file.token !== before && fresh(file.token, maxAge);
184
+ if (outcome === 'api_key_login' || outcome === 'not_signed_in' || outcome === 'not_found')
185
+ break;
186
+ }
187
+ }
188
+ if (file.token !== null && fresh(file.token, maxAge)) {
189
+ const email = claimsOf(file.token)?.email;
190
+ return { ok: true, idToken: file.token, email: typeof email === 'string' ? email : null, refreshed: renewed };
191
+ }
192
+ if (outcome === 'api_key_login' || file.apiKeyOnly)
193
+ return { ok: false, reason: 'api_key_login', message: 'Codex is signed in with an API key, which carries no identity. Sign in with ChatGPT (codex login), or sign up with an email address.' };
194
+ if (outcome === 'not_signed_in' || (file.token === null && outcome !== 'not_found')) {
195
+ return { ok: false, reason: 'not_signed_in', message: 'Codex is not signed in with ChatGPT on this machine (codex login), or keeps its login in the OS keyring. Sign up with an email address instead.' };
196
+ }
197
+ if (outcome === 'not_found' && !file.exists)
198
+ return { ok: false, reason: 'codex_not_found', message: 'The Codex CLI is not installed (or not on PATH). Sign up with an email address instead.' };
199
+ return { ok: false, reason: 'stale', message: 'The Codex login on this machine could not be refreshed. Run any codex command (or codex login), then try again.' };
200
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Computer use (0.15.0+, contracts §45): the workspace desktop, which the platform starts on the first call that needs
3
+ * it. `workspace.computer` drives it; `computerToolset(workspace)` answers Claude's computer toolset
4
+ * (`computer_toolset_20260801`) with one batch per model turn.
5
+ *
6
+ * await workspace.setComputerUse(true); // or open({ ..., computerUse: true })
7
+ * const shot = await workspace.computer.screenshot(); // { format: 'png', width: 1280, height: 800, data }
8
+ * await workspace.computer.act([{ action: 'left_click', coordinate: [640, 400] }, { action: 'type', text: 'hello' }]);
9
+ * const { url } = await workspace.computer.stream(); // a private link to watch the screen
10
+ */
11
+ import type { CellClient, ComputerAction, ComputerActionsResult, ComputerImage, ComputerStatus } from './cell.js';
12
+ import type { ComputerUse } from './client.js';
13
+ import type { WorkspacePorts } from './ports.js';
14
+ /** A decoded screen image. */
15
+ export interface ComputerScreenshot {
16
+ format: 'png' | 'jpeg';
17
+ width: number;
18
+ height: number;
19
+ data: Uint8Array;
20
+ }
21
+ export interface ComputerActOptions {
22
+ /** Append a screenshot after the last action that ran (also after a failure). */
23
+ screenshot?: boolean;
24
+ /** Wait this long before that screenshot when an action changed the screen (default 250). */
25
+ settleMs?: number;
26
+ format?: 'png' | 'jpeg';
27
+ /** JPEG quality 1..100 (default 80). */
28
+ quality?: number;
29
+ signal?: AbortSignal;
30
+ }
31
+ export interface ComputerStreamOptions {
32
+ /** Let the viewer use the mouse and keyboard (default false: view only, enforced in the workspace). */
33
+ interactive?: boolean;
34
+ /** How long the link works (60..604800 s; default 86400). */
35
+ ttlSeconds?: number;
36
+ }
37
+ /** A private link to the desktop's viewer: open it in a browser or an iframe. */
38
+ export interface ComputerStream {
39
+ url: string;
40
+ expiresAt: string;
41
+ port: number;
42
+ interactive: boolean;
43
+ }
44
+ /** Decodes a result image (base64 in the API) to bytes. */
45
+ export declare function decodeComputerImage(img: ComputerImage): ComputerScreenshot;
46
+ interface ComputerDeps {
47
+ cell: () => CellClient;
48
+ ports: () => WorkspacePorts;
49
+ setEnabled: (enabled: boolean | null) => Promise<ComputerUse>;
50
+ }
51
+ /** The workspace desktop (workspace.computer). */
52
+ export declare class WorkspaceComputer {
53
+ #private;
54
+ constructor(deps: ComputerDeps);
55
+ /**
56
+ * Runs actions in order (the first failure stops the batch; later actions come back `skipped`), optionally ending
57
+ * with a screenshot. The actions are Claude's computer toolset members with their parameter names.
58
+ */
59
+ act(actions: ComputerAction[], opts?: ComputerActOptions): Promise<ComputerActionsResult>;
60
+ /** The screen now (the pointer drawn in). */
61
+ screenshot(opts?: Pick<ComputerActOptions, 'format' | 'quality' | 'signal'>): Promise<ComputerScreenshot>;
62
+ /** Whether the desktop runs, its size and its viewers. Never starts it. */
63
+ status(): Promise<ComputerStatus>;
64
+ /** Starts the desktop (a no-op while it runs; the size applies to a start only: 640x480..2560x1600, default 1280x800). */
65
+ start(size?: {
66
+ width?: number;
67
+ height?: number;
68
+ }): Promise<ComputerStatus>;
69
+ /** Stops the desktop; its windows close. The next call that needs it starts a fresh one. */
70
+ stop(): Promise<void>;
71
+ /**
72
+ * A private link to watch the desktop (or use it, with `interactive`): starts the viewer in the workspace, exposes
73
+ * its port and signs a link to the viewer page (inbound ports, contracts §39). Anyone with the link can open it until
74
+ * it expires; `stopStream()` ends every view.
75
+ */
76
+ stream(opts?: ComputerStreamOptions): Promise<ComputerStream>;
77
+ /** Ends every view and closes the viewer ports (their links stop working). */
78
+ stopStream(): Promise<void>;
79
+ /** Switches computer use on or off for this workspace (null follows the template). */
80
+ setEnabled(enabled: boolean | null): Promise<ComputerUse>;
81
+ }
82
+ /** The `tools` entry of Claude's computer toolset (no name, no display size). */
83
+ export declare const COMPUTER_TOOLSET: {
84
+ readonly type: "computer_toolset_20260801";
85
+ };
86
+ /** A `tool_use` content block (structurally the Anthropic SDK's, so its blocks pass as they are). */
87
+ export interface ToolUseBlockLike {
88
+ type: string;
89
+ id: string;
90
+ name: string;
91
+ input: unknown;
92
+ toolset_name?: string | null;
93
+ }
94
+ type ResultContent = Array<{
95
+ type: 'text';
96
+ text: string;
97
+ } | {
98
+ type: 'image';
99
+ source: {
100
+ type: 'base64';
101
+ media_type: 'image/png' | 'image/jpeg';
102
+ data: string;
103
+ };
104
+ }>;
105
+ /** A `tool_result` block answering a toolset call (each echoes `toolset_name: "computer"`, as the API requires). */
106
+ export interface ComputerToolResult {
107
+ type: 'tool_result';
108
+ tool_use_id: string;
109
+ toolset_name: 'computer';
110
+ content: ResultContent;
111
+ is_error?: true;
112
+ }
113
+ export interface ComputerToolsetOptions {
114
+ /** Screenshot format (default png). */
115
+ format?: 'png' | 'jpeg';
116
+ quality?: number;
117
+ settleMs?: number;
118
+ signal?: AbortSignal;
119
+ }
120
+ /**
121
+ * Answers Claude's computer toolset (`tools: [COMPUTER_TOOLSET]`, Claude Opus 5.5 / Sonnet 5.5 and later on the
122
+ * Claude API) from this workspace's desktop:
123
+ *
124
+ * const computer = computerToolset(workspace);
125
+ * const msg = await anthropic.messages.create({ model, max_tokens, tools: [computer.definition], messages });
126
+ * messages.push({ role: 'assistant', content: msg.content });
127
+ * messages.push({ role: 'user', content: await computer.run(msg.content) });
128
+ *
129
+ * `run` takes the response content, runs every `toolset_name: "computer"` call of the turn as one batch (in order;
130
+ * the first failure stops it and the rest are answered "Not executed"), and returns one `tool_result` per call:
131
+ * images for screenshot and zoom, the position for cursor_position, `OK` otherwise. When the batch does not end with a
132
+ * look, a screenshot is attached to the last result, so the model sees the outcome without another round trip. A
133
+ * batch the desktop refuses as invalid (a coordinate outside the screen) is answered with the reason on every call.
134
+ */
135
+ export declare function computerToolset(workspace: {
136
+ computer: WorkspaceComputer;
137
+ }): {
138
+ definition: {
139
+ readonly type: "computer_toolset_20260801";
140
+ };
141
+ run(content: readonly unknown[], opts?: ComputerToolsetOptions): Promise<ComputerToolResult[]>;
142
+ };
143
+ export {};
@@ -0,0 +1,146 @@
1
+ import { ShardfluxApiError, ShardfluxProtocolError } from "./errors.js";
2
+ /** Decodes a result image (base64 in the API) to bytes. */
3
+ export function decodeComputerImage(img) {
4
+ return { format: img.format, width: img.width, height: img.height, data: new Uint8Array(Buffer.from(img.data, 'base64')) };
5
+ }
6
+ /** The workspace desktop (workspace.computer). */
7
+ export class WorkspaceComputer {
8
+ #deps;
9
+ constructor(deps) {
10
+ this.#deps = deps;
11
+ }
12
+ /**
13
+ * Runs actions in order (the first failure stops the batch; later actions come back `skipped`), optionally ending
14
+ * with a screenshot. The actions are Claude's computer toolset members with their parameter names.
15
+ */
16
+ act(actions, opts = {}) {
17
+ return this.#deps.cell().computer.act({
18
+ actions,
19
+ ...(opts.screenshot !== undefined ? { screenshot: opts.screenshot } : {}),
20
+ ...(opts.settleMs !== undefined ? { settle_ms: opts.settleMs } : {}),
21
+ ...(opts.format !== undefined ? { format: opts.format } : {}),
22
+ ...(opts.quality !== undefined ? { quality: opts.quality } : {}),
23
+ }, opts.signal);
24
+ }
25
+ /** The screen now (the pointer drawn in). */
26
+ async screenshot(opts = {}) {
27
+ const r = await this.act([], { ...opts, screenshot: true });
28
+ if (!r.screenshot)
29
+ throw new ShardfluxProtocolError('POST /computer/actions: the desktop returned no screenshot', 200, 'cell');
30
+ return decodeComputerImage(r.screenshot);
31
+ }
32
+ /** Whether the desktop runs, its size and its viewers. Never starts it. */
33
+ status() {
34
+ return this.#deps.cell().computer.status();
35
+ }
36
+ /** Starts the desktop (a no-op while it runs; the size applies to a start only: 640x480..2560x1600, default 1280x800). */
37
+ start(size = {}) {
38
+ return this.#deps.cell().computer.start(size);
39
+ }
40
+ /** Stops the desktop; its windows close. The next call that needs it starts a fresh one. */
41
+ stop() {
42
+ return this.#deps.cell().computer.stop();
43
+ }
44
+ /**
45
+ * A private link to watch the desktop (or use it, with `interactive`): starts the viewer in the workspace, exposes
46
+ * its port and signs a link to the viewer page (inbound ports, contracts §39). Anyone with the link can open it until
47
+ * it expires; `stopStream()` ends every view.
48
+ */
49
+ async stream(opts = {}) {
50
+ const info = await this.#deps.cell().computer.streamStart({ interactive: opts.interactive ?? false });
51
+ const ports = this.#deps.ports();
52
+ await ports.expose(info.port);
53
+ const link = await ports.link(info.port, { path: info.path, ...(opts.ttlSeconds !== undefined ? { ttlSeconds: opts.ttlSeconds } : {}) });
54
+ return { url: link.url, expiresAt: link.expiresAt, port: info.port, interactive: info.interactive };
55
+ }
56
+ /** Ends every view and closes the viewer ports (their links stop working). */
57
+ async stopStream() {
58
+ const ports = this.#deps.ports();
59
+ await Promise.all([ports.close(61002), ports.close(61003)]);
60
+ await this.#deps.cell().computer.streamStop();
61
+ }
62
+ /** Switches computer use on or off for this workspace (null follows the template). */
63
+ setEnabled(enabled) {
64
+ return this.#deps.setEnabled(enabled);
65
+ }
66
+ }
67
+ // ---- Claude's computer toolset ------------------------------------------------------------------------------------------
68
+ /** The `tools` entry of Claude's computer toolset (no name, no display size). */
69
+ export const COMPUTER_TOOLSET = { type: 'computer_toolset_20260801' };
70
+ /** Parameters the toolset members take; anything else a model sends is not forwarded. */
71
+ const PARAMS = ['coordinate', 'start_coordinate', 'region', 'text', 'scroll_direction', 'scroll_amount', 'duration', 'repeat'];
72
+ const LOOKS = new Set(['screenshot', 'zoom']);
73
+ function imageContent(img) {
74
+ return { type: 'image', source: { type: 'base64', media_type: img.format === 'jpeg' ? 'image/jpeg' : 'image/png', data: img.data } };
75
+ }
76
+ function actionOf(block) {
77
+ const input = (block.input !== null && typeof block.input === 'object' ? block.input : {});
78
+ const a = { action: block.name };
79
+ for (const k of PARAMS)
80
+ if (input[k] !== undefined && input[k] !== null)
81
+ a[k] = input[k];
82
+ return a;
83
+ }
84
+ /**
85
+ * Answers Claude's computer toolset (`tools: [COMPUTER_TOOLSET]`, Claude Opus 5.5 / Sonnet 5.5 and later on the
86
+ * Claude API) from this workspace's desktop:
87
+ *
88
+ * const computer = computerToolset(workspace);
89
+ * const msg = await anthropic.messages.create({ model, max_tokens, tools: [computer.definition], messages });
90
+ * messages.push({ role: 'assistant', content: msg.content });
91
+ * messages.push({ role: 'user', content: await computer.run(msg.content) });
92
+ *
93
+ * `run` takes the response content, runs every `toolset_name: "computer"` call of the turn as one batch (in order;
94
+ * the first failure stops it and the rest are answered "Not executed"), and returns one `tool_result` per call:
95
+ * images for screenshot and zoom, the position for cursor_position, `OK` otherwise. When the batch does not end with a
96
+ * look, a screenshot is attached to the last result, so the model sees the outcome without another round trip. A
97
+ * batch the desktop refuses as invalid (a coordinate outside the screen) is answered with the reason on every call.
98
+ */
99
+ export function computerToolset(workspace) {
100
+ return {
101
+ definition: COMPUTER_TOOLSET,
102
+ async run(content, opts = {}) {
103
+ const calls = content.filter((b) => b !== null && typeof b === 'object' && b.type === 'tool_use' && b.toolset_name === 'computer');
104
+ if (calls.length === 0)
105
+ return [];
106
+ const lastLooks = LOOKS.has(calls[calls.length - 1].name);
107
+ let r;
108
+ try {
109
+ r = await workspace.computer.act(calls.map(actionOf), {
110
+ screenshot: !lastLooks,
111
+ ...(opts.format !== undefined ? { format: opts.format } : {}),
112
+ ...(opts.quality !== undefined ? { quality: opts.quality } : {}),
113
+ ...(opts.settleMs !== undefined ? { settleMs: opts.settleMs } : {}),
114
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
115
+ });
116
+ }
117
+ catch (err) {
118
+ if (err instanceof ShardfluxApiError && err.code === 'validation_failed') {
119
+ const at = typeof err.details?.index === 'number' ? err.details.index : -1;
120
+ return calls.map((c, i) => ({
121
+ type: 'tool_result',
122
+ tool_use_id: c.id,
123
+ toolset_name: 'computer',
124
+ is_error: true,
125
+ content: [{ type: 'text', text: i === at || at < 0 ? err.message : 'Not executed: another computer action in this turn was invalid.' }],
126
+ }));
127
+ }
128
+ throw err;
129
+ }
130
+ const out = calls.map((c, i) => {
131
+ const res = r.results[i];
132
+ if (!res || res.skipped) {
133
+ return { type: 'tool_result', tool_use_id: c.id, toolset_name: 'computer', is_error: true, content: [{ type: 'text', text: 'Not executed: an earlier computer action in this turn failed.' }] };
134
+ }
135
+ if (!res.ok) {
136
+ return { type: 'tool_result', tool_use_id: c.id, toolset_name: 'computer', is_error: true, content: [{ type: 'text', text: res.error?.message ?? 'The action failed.' }] };
137
+ }
138
+ const body = res.image ? [imageContent(res.image)] : [{ type: 'text', text: res.output ?? 'OK' }];
139
+ return { type: 'tool_result', tool_use_id: c.id, toolset_name: 'computer', content: body };
140
+ });
141
+ if (r.screenshot)
142
+ out[out.length - 1].content.push(imageContent(r.screenshot));
143
+ return out;
144
+ },
145
+ };
146
+ }
package/dist/errors.d.ts CHANGED
@@ -82,7 +82,7 @@ export type ErrorCode = AppErrorCode | CellErrorCode;
82
82
  * `retained_state` (details.limit_value and details.current in GiB) when opening a new key or forking with the plan's
83
83
  * Retained state used up. `details.limit` is not a reason: these are not in this union.
84
84
  */
85
- export type KnownErrorReason = 'invalid_recipe' | 'base_not_layered' | 'language_unavailable' | 'language_conflict' | 'invalid_package' | 'too_many_files' | 'platform_owned_path' | 'upload_required' | 'upload_missing' | 'upload_digest_mismatch' | 'upload_too_large' | 'extra_hosts_without_auto' | 'invalid_settings' | 'services_unsupported' | 'input_required' | 'input_unknown' | 'input_invalid' | 'egress_widening' | 'reserved_session_id' | 'env_collision' | 'reserved_template_slug' | 'package_index_unavailable' | 'package_not_found' | 'startup_failed' | 'service_not_ready' | 'secrets_unavailable' | 'workspace_not_running' | 'operation_in_progress' | 'workspace_deleted' | 'secret_not_available' | 'legacy_disk_layout' | 'not_session' | 'session_lifetime' | 'lifetime_mismatch' | 'not_resettable' | 'template_not_layered' | 'draft_exists' | 'draft_stale' | 'build_in_progress' | 'file_list_unavailable' | 'file_list_indexing' | 'guest_feature_unavailable' | 'confirm_destructive_required' | 'reserved_key_prefix' | 'invalid_defaults' | 'invalid_path' | 'too_many_acknowledged_findings' | 'template_dev_mode_role' | 'draft_not_found' | 'version_not_found' | 'path_not_found' | 'revision_mismatch' | 'edit_not_found' | 'edit_ambiguous' | 'edit_not_text' | 'patch_invalid' | 'host_capacity' | 'wake_failed' | 'workspace_fenced' | 'offline_unavailable' | 'offline_budget' | 'offline_changed' | 'host_feature_unavailable' | 'not_supported_for_mode' | 'mode_mismatch' | 'mode_not_available' | 'layout_unsupported' | 'tree_revision_mismatch' | 'outside_tree_root' | 'execution_in_progress' | 'execution_id_reused' | 'operation_id_reused' | 'no_execution_host' | 'lease_expired' | 'host_unreachable' | 'host_restarted' | 'tree_moved' | 'blob_missing' | 'blob_corrupt' | 'exec_failed_to_start' | 'invalid_cwd' | 'allowance_used' | 'overage_paused' | 'spend_cap_reached' | 'overage_unavailable' | 'spend_cap_required' | 'spend_cap_below_minimum' | 'spend_cap_above_plan_price' | 'spend_cap_below_charges' | 'version_mismatch' | 'allocation_mode_not_available' | 'requires_elastic' | 'exceeds_memory_mib' | 'burst_mode_not_supported' | 'burst_not_supported' | 'burst_size_exceeds_plan' | 'not_available' | 'shared_volumes' | 'fence_not_drained' | 'apply_pending' | 'park_failed' | 'workspace_resumed' | 'interrupted' | 'burst_lost' | 'disk_full' | 'apply_failed' | 'reverted' | 'revert_failed' | 'immutable_path_removed' | 'immutable_paths_unsupported_base' | 'read_only_path' | 'host_lost';
85
+ export type KnownErrorReason = 'invalid_recipe' | 'base_not_layered' | 'language_unavailable' | 'language_conflict' | 'invalid_package' | 'too_many_files' | 'platform_owned_path' | 'upload_required' | 'upload_missing' | 'upload_digest_mismatch' | 'upload_too_large' | 'extra_hosts_without_auto' | 'invalid_settings' | 'services_unsupported' | 'input_required' | 'input_unknown' | 'input_invalid' | 'egress_widening' | 'reserved_session_id' | 'env_collision' | 'reserved_template_slug' | 'package_index_unavailable' | 'package_not_found' | 'startup_failed' | 'service_not_ready' | 'secrets_unavailable' | 'workspace_not_running' | 'operation_in_progress' | 'workspace_deleted' | 'secret_not_available' | 'legacy_disk_layout' | 'not_session' | 'session_lifetime' | 'lifetime_mismatch' | 'not_resettable' | 'template_not_layered' | 'draft_exists' | 'draft_stale' | 'build_in_progress' | 'file_list_unavailable' | 'file_list_indexing' | 'guest_feature_unavailable' | 'confirm_destructive_required' | 'reserved_key_prefix' | 'invalid_defaults' | 'invalid_path' | 'too_many_acknowledged_findings' | 'template_dev_mode_role' | 'draft_not_found' | 'version_not_found' | 'path_not_found' | 'revision_mismatch' | 'edit_not_found' | 'edit_ambiguous' | 'edit_not_text' | 'patch_invalid' | 'host_capacity' | 'wake_failed' | 'workspace_fenced' | 'offline_unavailable' | 'offline_budget' | 'offline_changed' | 'host_feature_unavailable' | 'not_supported_for_mode' | 'mode_mismatch' | 'mode_not_available' | 'layout_unsupported' | 'tree_revision_mismatch' | 'outside_tree_root' | 'execution_in_progress' | 'execution_id_reused' | 'operation_id_reused' | 'no_execution_host' | 'lease_expired' | 'host_unreachable' | 'host_restarted' | 'tree_moved' | 'blob_missing' | 'blob_corrupt' | 'exec_failed_to_start' | 'invalid_cwd' | 'allowance_used' | 'overage_paused' | 'spend_cap_reached' | 'overage_unavailable' | 'spend_cap_required' | 'spend_cap_below_minimum' | 'spend_cap_above_plan_price' | 'spend_cap_below_charges' | 'version_mismatch' | 'allocation_mode_not_available' | 'requires_elastic' | 'exceeds_memory_mib' | 'burst_mode_not_supported' | 'burst_not_supported' | 'burst_size_exceeds_plan' | 'not_available' | 'shared_volumes' | 'fence_not_drained' | 'apply_pending' | 'park_failed' | 'workspace_resumed' | 'interrupted' | 'burst_lost' | 'disk_full' | 'apply_failed' | 'reverted' | 'revert_failed' | 'immutable_path_removed' | 'immutable_paths_unsupported_base' | 'read_only_path' | 'resize_not_available' | 'shrink_not_supported' | 'resize_failed' | 'host_lost' | 'inbound_ports_not_available' | 'port_not_exposed' | 'port_limit';
86
86
  /** A known reason, or any other string the server sends (reasons are open-ended). */
87
87
  export type ErrorReason = KnownErrorReason | (string & {});
88
88
  export interface ErrorBodyLike {