@shardflux/sdk 0.14.0 → 0.16.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 +85 -23
- package/README.md +226 -7
- package/dist/account.d.ts +2 -0
- package/dist/account.js +6 -0
- package/dist/cell.d.ts +27 -0
- package/dist/cell.js +71 -0
- package/dist/client.d.ts +54 -5
- package/dist/client.js +100 -7
- package/dist/computer.d.ts +153 -0
- package/dist/computer.js +229 -0
- package/dist/errors.d.ts +5 -1
- package/dist/errors.js +9 -0
- package/dist/executions.d.ts +2 -6
- package/dist/executions.js +9 -0
- package/dist/exit-code.d.ts +7 -0
- package/dist/exit-code.js +12 -0
- package/dist/generated/app-api.d.ts +1055 -119
- package/dist/generated/cell-api.d.ts +334 -0
- package/dist/http.d.ts +7 -1
- package/dist/http.js +34 -13
- package/dist/index.d.ts +13 -6
- package/dist/index.js +7 -2
- package/dist/ports.d.ts +7 -0
- package/dist/ports.js +1 -1
- package/dist/progress.d.ts +2 -2
- package/dist/progress.js +1 -1
- package/dist/templates.d.ts +12 -0
- package/dist/templates.js +9 -0
- package/dist/testing/index.d.ts +62 -0
- package/dist/testing/index.js +585 -0
- package/dist/testing/seed.d.ts +433 -0
- package/dist/testing/seed.js +449 -0
- package/dist/tools.d.ts +21 -3
- package/dist/tools.js +113 -22
- package/dist/tunnel-assets/linux-amd64.gz +0 -0
- package/dist/tunnel-assets/linux-arm64.gz +0 -0
- package/dist/tunnel-assets.d.ts +10 -0
- package/dist/tunnel-assets.js +11 -0
- package/dist/tunnel-packet.d.ts +3 -0
- package/dist/tunnel-packet.js +43 -0
- package/dist/tunnel-pty.d.ts +86 -0
- package/dist/tunnel-pty.js +243 -0
- package/dist/tunnels.d.ts +47 -0
- package/dist/tunnels.js +454 -0
- package/dist/workspace-ref.d.ts +87 -0
- package/dist/workspace-ref.js +173 -0
- package/dist/workspace.d.ts +40 -1
- package/dist/workspace.js +111 -2
- package/package.json +7 -2
package/dist/computer.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
#streams = new Map();
|
|
10
|
+
#minting = new Map();
|
|
11
|
+
#pendingStreams = new Set();
|
|
12
|
+
#stopping;
|
|
13
|
+
#streamGeneration = 0;
|
|
14
|
+
constructor(deps) {
|
|
15
|
+
this.#deps = deps;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs actions in order (the first failure stops the batch; later actions come back `skipped`), optionally ending
|
|
19
|
+
* with a screenshot. The actions are Claude's computer toolset members with their parameter names.
|
|
20
|
+
*/
|
|
21
|
+
act(actions, opts = {}) {
|
|
22
|
+
return this.#deps.cell().computer.act({
|
|
23
|
+
actions,
|
|
24
|
+
...(LOOKS.has(actions.at(-1)?.action ?? '') ? { screenshot: false } : opts.screenshot !== undefined ? { screenshot: opts.screenshot } : {}),
|
|
25
|
+
...(opts.settleMs !== undefined ? { settle_ms: opts.settleMs } : {}),
|
|
26
|
+
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
27
|
+
...(opts.quality !== undefined ? { quality: opts.quality } : {}),
|
|
28
|
+
}, opts.signal);
|
|
29
|
+
}
|
|
30
|
+
/** The screen now (the pointer drawn in). */
|
|
31
|
+
async screenshot(opts = {}) {
|
|
32
|
+
const r = await this.act([], { ...opts, screenshot: true });
|
|
33
|
+
if (!r.screenshot)
|
|
34
|
+
throw new ShardfluxProtocolError('POST /computer/actions: the desktop returned no screenshot', 200, 'cell');
|
|
35
|
+
return decodeComputerImage(r.screenshot);
|
|
36
|
+
}
|
|
37
|
+
/** Whether the desktop runs, its size and its viewers. Never starts it. */
|
|
38
|
+
status() {
|
|
39
|
+
return this.#deps.cell().computer.status();
|
|
40
|
+
}
|
|
41
|
+
/** Starts the desktop (a no-op while it runs; the size applies to a start only: 640x480..2560x1600, default 1280x800). */
|
|
42
|
+
start(size = {}) {
|
|
43
|
+
return this.#deps.cell().computer.start(size);
|
|
44
|
+
}
|
|
45
|
+
/** Stops the desktop; its windows close. The next call that needs it starts a fresh one. */
|
|
46
|
+
stop() {
|
|
47
|
+
return this.#deps.cell().computer.stop();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A private link to watch the desktop (or use it, with `interactive`): starts the viewer in the workspace, exposes
|
|
51
|
+
* its port and signs a link to the viewer page (inbound ports, contracts §39). Anyone with the link can open it until
|
|
52
|
+
* it expires; `stopStream()` ends every view.
|
|
53
|
+
*/
|
|
54
|
+
async stream(opts = {}) {
|
|
55
|
+
if (opts.embed !== undefined)
|
|
56
|
+
opts = { ...opts, embed: structuredClone(opts.embed) };
|
|
57
|
+
if (this.#stopping)
|
|
58
|
+
await this.#stopping;
|
|
59
|
+
const kind = JSON.stringify({ interactive: opts.interactive ?? false, ttlSeconds: opts.ttlSeconds, embed: opts.embed });
|
|
60
|
+
const cached = this.#streams.get(kind);
|
|
61
|
+
if (!opts.fresh && cached && Date.parse(cached.expiresAt) - Date.now() > 60_000)
|
|
62
|
+
return { ...cached };
|
|
63
|
+
const pending = this.#minting.get(kind);
|
|
64
|
+
if (!opts.fresh && pending)
|
|
65
|
+
return { ...await pending };
|
|
66
|
+
const generation = this.#streamGeneration;
|
|
67
|
+
const mint = this.#stream(opts).then((link) => {
|
|
68
|
+
if (generation === this.#streamGeneration && this.#minting.get(kind) === mint)
|
|
69
|
+
this.#streams.set(kind, link);
|
|
70
|
+
return link;
|
|
71
|
+
});
|
|
72
|
+
this.#minting.set(kind, mint);
|
|
73
|
+
this.#pendingStreams.add(mint);
|
|
74
|
+
try {
|
|
75
|
+
return { ...await mint };
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
this.#pendingStreams.delete(mint);
|
|
79
|
+
if (this.#minting.get(kind) === mint)
|
|
80
|
+
this.#minting.delete(kind);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async #stream(opts) {
|
|
84
|
+
const info = await this.#deps.cell().computer.streamStart({ interactive: opts.interactive ?? false });
|
|
85
|
+
const ports = this.#deps.ports();
|
|
86
|
+
await ports.expose(info.port);
|
|
87
|
+
const link = await ports.link(info.port, { ...(opts.embed !== undefined ? { embed: opts.embed } : {}), path: info.path, ...(opts.ttlSeconds !== undefined ? { ttlSeconds: opts.ttlSeconds } : {}) });
|
|
88
|
+
return { url: link.url, expiresAt: link.expiresAt, port: info.port, interactive: info.interactive };
|
|
89
|
+
}
|
|
90
|
+
/** Ends every view and closes the viewer ports (their links stop working). */
|
|
91
|
+
stopStream() {
|
|
92
|
+
if (this.#stopping)
|
|
93
|
+
return this.#stopping;
|
|
94
|
+
const stop = this.#stopStream();
|
|
95
|
+
this.#stopping = stop;
|
|
96
|
+
void stop.finally(() => { if (this.#stopping === stop)
|
|
97
|
+
this.#stopping = undefined; }).catch(() => undefined);
|
|
98
|
+
return stop;
|
|
99
|
+
}
|
|
100
|
+
async #stopStream() {
|
|
101
|
+
this.#streamGeneration += 1;
|
|
102
|
+
this.#streams.clear();
|
|
103
|
+
const pending = [...this.#pendingStreams];
|
|
104
|
+
this.#minting.clear();
|
|
105
|
+
await Promise.allSettled(pending);
|
|
106
|
+
const ports = this.#deps.ports();
|
|
107
|
+
await Promise.all([ports.close(61002), ports.close(61003)]);
|
|
108
|
+
await this.#deps.cell().computer.streamStop();
|
|
109
|
+
}
|
|
110
|
+
/** Switches computer use on or off for this workspace (null follows the template). */
|
|
111
|
+
setEnabled(enabled) {
|
|
112
|
+
return this.#deps.setEnabled(enabled);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ---- Claude's computer toolset ------------------------------------------------------------------------------------------
|
|
116
|
+
/** The `tools` entry of Claude's computer toolset (no name, no display size). */
|
|
117
|
+
export const COMPUTER_TOOLSET = { type: 'computer_toolset_20260801' };
|
|
118
|
+
/** Parameters the toolset members take; anything else a model sends is not forwarded. */
|
|
119
|
+
const PARAMS = ['coordinate', 'start_coordinate', 'region', 'text', 'scroll_direction', 'scroll_amount', 'duration', 'repeat'];
|
|
120
|
+
const LOOKS = new Set(['screenshot', 'zoom']);
|
|
121
|
+
function imageContent(img) {
|
|
122
|
+
return { type: 'image', source: { type: 'base64', media_type: img.format === 'jpeg' ? 'image/jpeg' : 'image/png', data: img.data } };
|
|
123
|
+
}
|
|
124
|
+
function actionOf(block) {
|
|
125
|
+
const input = (block.input !== null && typeof block.input === 'object' ? block.input : {});
|
|
126
|
+
const a = { action: block.name };
|
|
127
|
+
for (const k of PARAMS)
|
|
128
|
+
if (input[k] !== undefined && input[k] !== null)
|
|
129
|
+
a[k] = input[k];
|
|
130
|
+
return a;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Answers Claude's computer toolset (`tools: [COMPUTER_TOOLSET]`, Claude Opus 5.5 / Sonnet 5.5 and later on the
|
|
134
|
+
* Claude API) from this workspace's desktop:
|
|
135
|
+
*
|
|
136
|
+
* const computer = computerToolset(workspace);
|
|
137
|
+
* const msg = await anthropic.messages.create({ model, max_tokens, tools: [computer.definition], messages });
|
|
138
|
+
* messages.push({ role: 'assistant', content: msg.content });
|
|
139
|
+
* messages.push({ role: 'user', content: await computer.run(msg.content) });
|
|
140
|
+
*
|
|
141
|
+
* `run` takes the response content, runs every `toolset_name: "computer"` call of the turn as one batch (in order;
|
|
142
|
+
* the first failure stops it and the rest are answered "Not executed"), and returns one `tool_result` per call:
|
|
143
|
+
* images for screenshot and zoom, the position for cursor_position, `OK` otherwise. When the batch does not end with a
|
|
144
|
+
* look, a screenshot is attached to the last result, so the model sees the outcome without another round trip. A
|
|
145
|
+
* batch the desktop refuses as invalid (a coordinate outside the screen) is answered with the reason on every call.
|
|
146
|
+
*/
|
|
147
|
+
export function computerToolset(workspace, defaults = {}) {
|
|
148
|
+
return {
|
|
149
|
+
definition: COMPUTER_TOOLSET,
|
|
150
|
+
async run(content, opts = {}) {
|
|
151
|
+
opts = { ...defaults, ...opts };
|
|
152
|
+
const calls = content.filter((b) => b !== null && typeof b === 'object' && b.type === 'tool_use' && b.toolset_name === 'computer');
|
|
153
|
+
if (calls.length === 0)
|
|
154
|
+
return [];
|
|
155
|
+
const lastLooks = LOOKS.has(calls[calls.length - 1].name);
|
|
156
|
+
let r;
|
|
157
|
+
try {
|
|
158
|
+
r = await runComputerActions((actions, options) => workspace.computer.act(actions, options), calls.map(actionOf), {
|
|
159
|
+
screenshot: !lastLooks,
|
|
160
|
+
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
161
|
+
...(opts.quality !== undefined ? { quality: opts.quality } : {}),
|
|
162
|
+
...(opts.settleMs !== undefined ? { settleMs: opts.settleMs } : {}),
|
|
163
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
164
|
+
}, opts.beforeAction);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
if (err instanceof ShardfluxApiError && err.code === 'validation_failed') {
|
|
168
|
+
const at = typeof err.details?.index === 'number' ? err.details.index : -1;
|
|
169
|
+
return calls.map((c, i) => ({
|
|
170
|
+
type: 'tool_result',
|
|
171
|
+
tool_use_id: c.id,
|
|
172
|
+
toolset_name: 'computer',
|
|
173
|
+
is_error: true,
|
|
174
|
+
content: [{ type: 'text', text: i === at || at < 0 ? err.message : 'Not executed: another computer action in this turn was invalid.' }],
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
const out = calls.map((c, i) => {
|
|
180
|
+
const res = r.results[i];
|
|
181
|
+
if (!res || res.skipped) {
|
|
182
|
+
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.' }] };
|
|
183
|
+
}
|
|
184
|
+
if (!res.ok) {
|
|
185
|
+
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.' }] };
|
|
186
|
+
}
|
|
187
|
+
const body = res.image ? [imageContent(res.image)] : [{ type: 'text', text: res.output ?? 'OK' }];
|
|
188
|
+
return { type: 'tool_result', tool_use_id: c.id, toolset_name: 'computer', content: body };
|
|
189
|
+
});
|
|
190
|
+
if (r.screenshot)
|
|
191
|
+
out[out.length - 1].content.push(imageContent(r.screenshot));
|
|
192
|
+
return out;
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** No hook: one batch as before. With a hook, check immediately before each individual action runs. */
|
|
197
|
+
export async function runComputerActions(act, actions, opts, beforeAction) {
|
|
198
|
+
if (!beforeAction)
|
|
199
|
+
return act(actions, opts);
|
|
200
|
+
const out = { results: [] };
|
|
201
|
+
for (const [i, action] of actions.entries()) {
|
|
202
|
+
const checked = structuredClone(action);
|
|
203
|
+
let refusal;
|
|
204
|
+
try {
|
|
205
|
+
const decision = await beforeAction(structuredClone(checked));
|
|
206
|
+
if (decision === false || typeof decision === 'string')
|
|
207
|
+
refusal = typeof decision === 'string' && decision ? decision : 'Computer action refused by beforeAction.';
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
refusal = (err instanceof Error ? err.message : String(err)) || 'Computer action refused by beforeAction.';
|
|
211
|
+
}
|
|
212
|
+
if (refusal !== undefined) {
|
|
213
|
+
out.results.push({ action: action.action, ok: false, took_ms: 0, error: { code: 'action_failed', message: refusal } });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const r = await act([checked], { ...opts, screenshot: i === actions.length - 1 && opts.screenshot !== false && !LOOKS.has(action.action) });
|
|
217
|
+
out.results.push(...r.results);
|
|
218
|
+
out.cursor = r.cursor;
|
|
219
|
+
out.display = r.display;
|
|
220
|
+
if (r.screenshot)
|
|
221
|
+
out.screenshot = r.screenshot;
|
|
222
|
+
}
|
|
223
|
+
if (!out.results.at(-1)?.ok) {
|
|
224
|
+
out.results.push(...actions.slice(i + 1).map((a) => ({ action: a.action, ok: false, skipped: true, took_ms: 0 })));
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
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' | 'resize_not_available' | 'shrink_not_supported' | 'resize_failed' | 'host_lost' | 'inbound_ports_not_available' | 'port_not_exposed' | 'port_limit';
|
|
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' | 'already_current' | 'not_upgradable' | 'upgrade_base_unavailable' | 'upgrade_host_unavailable' | 'upgrade_host_unsupported' | 'upgrade_template_mismatch' | 'upgrade_runtime_mismatch' | 'checkpoint_template_mismatch' | '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 {
|
|
@@ -117,6 +117,10 @@ export declare class ShardfluxApiError extends Error {
|
|
|
117
117
|
readonly treeRevision: number | undefined;
|
|
118
118
|
constructor(status: number, body: ErrorBodyLike, source: 'api' | 'cell', retryAfterSeconds?: number, treeRevision?: number);
|
|
119
119
|
}
|
|
120
|
+
/** A cell file operation found that the requested path does not exist. */
|
|
121
|
+
export declare class FileNotFoundError extends ShardfluxApiError {
|
|
122
|
+
constructor(status: number, body: ErrorBodyLike, source: 'api' | 'cell', retryAfterSeconds?: number, treeRevision?: number);
|
|
123
|
+
}
|
|
120
124
|
/**
|
|
121
125
|
* The cell gateway's working-at-once refusal (0.14.0+): 429 `quota_exceeded`, retryable, with Retry-After. The gateway
|
|
122
126
|
* refuses the call at admission, before the workspace is touched, so nothing ran and any request (exec start, stdin,
|
package/dist/errors.js
CHANGED
|
@@ -40,6 +40,13 @@ export class ShardfluxApiError extends Error {
|
|
|
40
40
|
this.reason = typeof reason === 'string' ? reason : undefined;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/** A cell file operation found that the requested path does not exist. */
|
|
44
|
+
export class FileNotFoundError extends ShardfluxApiError {
|
|
45
|
+
constructor(status, body, source, retryAfterSeconds, treeRevision) {
|
|
46
|
+
super(status, body, source, retryAfterSeconds, treeRevision);
|
|
47
|
+
this.name = 'FileNotFoundError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
43
50
|
/**
|
|
44
51
|
* The cell gateway's working-at-once refusal (0.14.0+): 429 `quota_exceeded`, retryable, with Retry-After. The gateway
|
|
45
52
|
* refuses the call at admission, before the workspace is touched, so nothing ran and any request (exec start, stdin,
|
|
@@ -126,6 +133,8 @@ export class ExecStartError extends ShardfluxApiError {
|
|
|
126
133
|
* here, so `instanceof` works whichever call was refused.
|
|
127
134
|
*/
|
|
128
135
|
export function apiError(status, body, source, retryAfterSeconds, treeRevision) {
|
|
136
|
+
if (source === 'cell' && status === 404 && body.error.code === 'not_found' && body.error.details?.reason === 'path_not_found')
|
|
137
|
+
return new FileNotFoundError(status, body, source, retryAfterSeconds, treeRevision);
|
|
129
138
|
switch (body.error.details?.reason) {
|
|
130
139
|
case 'not_supported_for_mode':
|
|
131
140
|
return new NotSupportedForModeError(status, body, source, retryAfterSeconds, treeRevision);
|
package/dist/executions.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Executions of file-first workspaces: each `POST /exec` runs one command in a fresh VM on the
|
|
3
|
-
* workspace's latest tree revision and answers when it ended; the files it changed under /home/user become the next
|
|
4
|
-
* revision. The execution id is the idempotency key: a request retried with the same id returns the recorded result
|
|
5
|
-
* (or waits for the running execution) and never runs the command a second time.
|
|
6
|
-
*/
|
|
7
1
|
import type { components } from './generated/cell-api.js';
|
|
8
2
|
type S = components['schemas'];
|
|
9
3
|
/** The cell's `ExecutionResult` as sent on the wire (stdout/stderr base64). */
|
|
@@ -105,6 +99,8 @@ export declare class ExecutionResult {
|
|
|
105
99
|
readonly replayed: boolean;
|
|
106
100
|
/** The body as the cell sent it. */
|
|
107
101
|
readonly raw: ExecutionResultBody;
|
|
102
|
+
/** POSIX status of the command. */
|
|
103
|
+
get exitCodePosix(): number;
|
|
108
104
|
constructor(body: ExecutionResultBody, replayed: boolean);
|
|
109
105
|
/** Still queued or running (only `executions.get()` returns such a result). */
|
|
110
106
|
get pending(): boolean;
|
package/dist/executions.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executions of file-first workspaces: each `POST /exec` runs one command in a fresh VM on the
|
|
3
|
+
* workspace's latest tree revision and answers when it ended; the files it changed under /home/user become the next
|
|
4
|
+
* revision. The execution id is the idempotency key: a request retried with the same id returns the recorded result
|
|
5
|
+
* (or waits for the running execution) and never runs the command a second time.
|
|
6
|
+
*/
|
|
7
|
+
import { exitCodePosix } from "./exit-code.js";
|
|
1
8
|
/** `^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$` (cell-api.yaml ExecutionIdValue). */
|
|
2
9
|
export const EXECUTION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
3
10
|
/** A fresh execution id: `ex-` plus a random UUID (39 characters, valid per EXECUTION_ID). */
|
|
@@ -43,6 +50,8 @@ export class ExecutionResult {
|
|
|
43
50
|
replayed;
|
|
44
51
|
/** The body as the cell sent it. */
|
|
45
52
|
raw;
|
|
53
|
+
/** POSIX status of the command. */
|
|
54
|
+
get exitCodePosix() { return exitCodePosix(this); }
|
|
46
55
|
constructor(body, replayed) {
|
|
47
56
|
this.raw = body;
|
|
48
57
|
this.executionId = body.execution_id;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The command's POSIX status, shared by exec results and the CLI. */
|
|
2
|
+
export function exitCodePosix(result) {
|
|
3
|
+
if (result.timedOut)
|
|
4
|
+
return 124;
|
|
5
|
+
if (result.canceled)
|
|
6
|
+
return 130;
|
|
7
|
+
if (result.exitCode !== null && result.exitCode >= 0)
|
|
8
|
+
return result.exitCode;
|
|
9
|
+
if (result.termSignal !== null && result.termSignal > 0)
|
|
10
|
+
return 128 + result.termSignal;
|
|
11
|
+
return 1;
|
|
12
|
+
}
|