@shardflux/sdk 0.7.0 → 0.9.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 +220 -1
- package/README.md +247 -10
- package/dist/account.d.ts +469 -0
- package/dist/account.js +620 -0
- package/dist/cell.d.ts +197 -8
- package/dist/cell.js +449 -31
- package/dist/client.d.ts +76 -5
- package/dist/client.js +114 -6
- package/dist/errors.d.ts +62 -3
- package/dist/errors.js +65 -1
- package/dist/executions.d.ts +120 -0
- package/dist/executions.js +99 -0
- package/dist/feedback.d.ts +67 -0
- package/dist/feedback.js +39 -0
- package/dist/generated/app-api.d.ts +12323 -8072
- package/dist/generated/cell-api.d.ts +463 -8
- package/dist/http.d.ts +7 -1
- package/dist/http.js +26 -7
- package/dist/index.d.ts +18 -7
- package/dist/index.js +5 -1
- package/dist/lifecycle.d.ts +27 -2
- package/dist/lifecycle.js +5 -0
- package/dist/progress.js +4 -2
- package/dist/templates.js +2 -2
- package/dist/tools.d.ts +72 -22
- package/dist/tools.js +156 -23
- package/dist/version-check.d.ts +101 -0
- package/dist/version-check.js +191 -0
- package/dist/workspace.d.ts +77 -5
- package/dist/workspace.js +164 -12
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,226 @@
|
|
|
3
3
|
Every API the README shows is available from the version named here. Below 1.0, a minor release may break
|
|
4
4
|
compatibility; breaking changes are marked **Breaking**.
|
|
5
5
|
|
|
6
|
-
## 0.
|
|
6
|
+
## 0.9.0 (not yet published; npm `latest` is 0.8.0)
|
|
7
|
+
|
|
8
|
+
Elastic compute (decision 0007): file tools and wake hints for parked workspaces. Additive; older APIs and cell
|
|
9
|
+
gateways keep working (the new calls answer 404 there).
|
|
10
|
+
|
|
11
|
+
### File search, patches with revisions, the wake hint
|
|
12
|
+
|
|
13
|
+
Needs a cell gateway with the contracts §26 routes (`files/search`, `files/patch`, `wake-hint`, revisions); an older
|
|
14
|
+
gateway does not serve them and returns no revisions.
|
|
15
|
+
|
|
16
|
+
- `cell.files.search(path, pattern, opts)`: content search under a directory (literal or RE2 with `regex`,
|
|
17
|
+
`caseInsensitive`, `include`/`exclude` globs, `maxMatches`, `maxFileBytes`, `contextLines`). Returns the gateway's
|
|
18
|
+
`FileSearchResult` (`matches` with `path`, 1-based `line` and byte `column`, `text`, optional `before`/`after`;
|
|
19
|
+
`truncated` and `stop_reason`; `files_scanned`) plus `served_from` (`disk` when a suspended workspace was searched on its disk
|
|
20
|
+
without waking it). It is read-only, so the HTTP layer retries it like a GET (new `RequestOptions.idempotent`).
|
|
21
|
+
- `cell.files.patch({ path, edits | content, expectedRevision, createParents, mode }, { idempotencyKey })`: text
|
|
22
|
+
edits (`{ oldText, newText, replaceAll }`, each must match exactly once unless `replaceAll`, applied in order,
|
|
23
|
+
all or nothing) or a whole new `content`, atomic and durable. Every call sends an `Idempotency-Key` (like `write`),
|
|
24
|
+
so a retried call is applied once. `expectedRevision` (or `absent`) makes a concurrent change a 409.
|
|
25
|
+
- Revisions: a file's revision is the SHA-256 of its content. `cell.files.stat(path, { revision: true })` returns it
|
|
26
|
+
(`FileInfo.revision`), writes and patches return it, and the new `cell.files.readWithInfo(path, opts)` returns
|
|
27
|
+
`{ data, size, revision, servedFrom }` from the read's `X-File-Size`, `X-File-Revision` and `X-Served-From` headers.
|
|
28
|
+
`read()` and `readText()` keep returning bytes and text: a separate method keeps every existing caller and return
|
|
29
|
+
type unchanged, where an options flag would have made `read()`'s return type depend on an argument. A read
|
|
30
|
+
continued over several requests reports a revision only when every part carried the same one.
|
|
31
|
+
- `workspace.hint(opts)` sends `POST /wake-hint`: a hibernated workspace is restored ahead of the tool call that
|
|
32
|
+
follows. It returns `{ residency, wake }`; for a workspace that is not running (409 `workspace_not_running`, also
|
|
33
|
+
when its tool token cannot be issued) it starts `wake()` in the background and returns at once (`wake` is that
|
|
34
|
+
promise, shared by concurrent hints; `wake: null` in the options only reports it). It is never retried and never
|
|
35
|
+
waits out `workspace_busy`. `cell.wakeHint()` is the bare request.
|
|
36
|
+
- Agent tools: `search_files` and `edit_file` (permission `files`), after `list_files`. `edit_file` patches with
|
|
37
|
+
`edits`; when the model gives no `expected_revision` it reads the file's revision first
|
|
38
|
+
(`stat?revision=true`) and pins the patch to it, so a change made in between is refused (`revision_mismatch`)
|
|
39
|
+
instead of edited blindly. `search_files` returns whole matches up to `maxOutputBytes` and counts the rest in
|
|
40
|
+
`omitted_matches`. The existing tools are unchanged. The tool runner sends `workspace.hint()` when a tool call
|
|
41
|
+
starts, without waiting for it; `workspaceTools(ws, { hint: false })` turns that off.
|
|
42
|
+
- Errors: `KnownErrorReason` adds `revision_mismatch` (409, `details.current_revision`), `edit_not_found`,
|
|
43
|
+
`edit_ambiguous` (422, `details.index`), `edit_not_text`, `patch_invalid`, `host_capacity` and `wake_failed` (503,
|
|
44
|
+
retryable, `Retry-After`: retried by the HTTP layer for GETs, searches and requests with an Idempotency-Key, like
|
|
45
|
+
any retryable 503), `workspace_fenced` (409 `workspace_busy`, waited out like any `workspace_busy`), and for reads
|
|
46
|
+
of a sleeping workspace `offline_unavailable` and `offline_budget` (409 `workspace_not_running`: woken and retried
|
|
47
|
+
like any) and `offline_changed` (503, retried; served by the running workspace).
|
|
48
|
+
- Reads of a suspended workspace without a held token: the API now issues tool tokens for a suspended workspace
|
|
49
|
+
(contracts §26.4), so `read`, `readText`, `readWithInfo`, `stat`, `list` and `search` of a suspended workspace are
|
|
50
|
+
served from its disk without waking it also for a handle that fetches its first token after the suspend (before,
|
|
51
|
+
the token was refused and the call woke the workspace). Any other call still wakes it: the cell refuses it with 409
|
|
52
|
+
`workspace_not_running`. Needs that API; with an older one the token is refused and the call wakes it as before.
|
|
53
|
+
- The agent-tool runner sends no wake hint for `read_file`, `list_files` and `search_files`: a sleeping workspace
|
|
54
|
+
serves them from its disk, and the hint would wake a suspended one (or restore a hibernated one) for nothing.
|
|
55
|
+
- Hosts without the features (contracts §26.7): `KnownErrorReason` adds `host_feature_unavailable` (409 `conflict`,
|
|
56
|
+
not retryable, `details.feature` `file_search` or `file_patch`): the workspace runs on a host agent that predates
|
|
57
|
+
the call, until it runs on an upgraded host. It is neither retried nor answered with a wake. Such a host also
|
|
58
|
+
returns no revisions (`FileInfo.revision`, `readWithInfo().revision` are absent).
|
|
59
|
+
- `JsonSchema.pattern`, checked by `validateArgs()`.
|
|
60
|
+
- Types: `FileSearchOptions`, `FileSearchResponse`, `FileSearchRequest`, `FileSearchResult`, `FileSearchMatch`,
|
|
61
|
+
`FilePatchParams`, `FilePatchEdit`, `FilePatchRequest`, `FilePatchResult`, `FileEdit`, `FileRevision`,
|
|
62
|
+
`FileReadResult`, `ServedFrom`, `Residency`, `WakeHintResult`, `HintOptions`, `HintResult`.
|
|
63
|
+
|
|
64
|
+
### File-first workspaces (contracts §29)
|
|
65
|
+
|
|
66
|
+
Needs an API with `FILE_FIRST_WORKSPACES` on and a cell that serves file-first workspaces (contracts §29.8). A
|
|
67
|
+
file-first workspace has no VM between executions: its state is a versioned file tree under /home/user, and each
|
|
68
|
+
command runs in a fresh VM whose changed files become the next tree revision. Processful workspaces are unchanged.
|
|
69
|
+
|
|
70
|
+
- `workspaces.open({ mode: 'file_first' })` (`mode: 'processful' | 'file_first'`; omitted: the API's default). A
|
|
71
|
+
file-first open is ready at once (200 with a tool token, no operation). `workspace.mode` and `workspace.treeRevision`
|
|
72
|
+
(null for processful): the handle follows every `X-Tree-Revision` its cell clients see, execution results and
|
|
73
|
+
`refresh()`, and never moves backwards.
|
|
74
|
+
- `workspace.executions.run(argv, opts)` / `cell.executions.run()`: runs one command as an execution and resolves when
|
|
75
|
+
it ended with an `ExecutionResult`: `stdout` / `stderr` bytes plus `text(stream)`, `stdoutText`, `stderrText`;
|
|
76
|
+
`exitCode`, `termSignal`, `timedOut`, `state` (`succeeded`, `failed`, `lost`), `baseRevision`, `treeRevision`,
|
|
77
|
+
`changed` (`{ path, change: added | modified | deleted, type }`), `changedTruncated`, the truncation flags,
|
|
78
|
+
`timings`, `error` / `errorReason`, `replayed`, `ok`, `raw`. Options: `executionId`, `cwd`, `env`, `user`, `stdin`,
|
|
79
|
+
`timeoutMs`, `killGraceMs`, `secretRefs`, `outputLimitBytes`, `maxRetries`, `attemptTimeoutMs`, `signal`.
|
|
80
|
+
- Idempotent by execution id: `executionId` defaults to a fresh `ex-<uuid>` (`newExecutionId()`, checked against
|
|
81
|
+
`EXECUTION_ID` before any request). Network failures and retryable 429/5xx answers (503 `no_execution_host`, with
|
|
82
|
+
`Retry-After`, honoured up to 30 s) are retried with the same id and body at most `maxRetries` (5) times; a wait
|
|
83
|
+
longer than `attemptTimeoutMs` (300 s) re-attaches with the same id without counting as a failure. The cell answers
|
|
84
|
+
201 for the call that ran the command and 200 (`replayed`) with the recorded result for any other, so the command
|
|
85
|
+
runs at most once. `workspace_busy` `execution_in_progress` is waited out like any `workspace_busy`;
|
|
86
|
+
`execution_id_reused` is thrown at once. A `failed` or `lost` result is returned, never retried with a new id.
|
|
87
|
+
- `executions.get(id, { waitMs })`: the result (`GET /executions/{id}`), or a `pending` one (202, state `queued` /
|
|
88
|
+
`running`) while it runs; `waitMs` polls (250 ms doubling to 2 s) until it ended or the wait elapsed.
|
|
89
|
+
- `ifTreeRevision` on `files.write`, `remove`, `mkdir`, `move` and `patch` sends `If-Match`: the call applies only at
|
|
90
|
+
that tree revision, else `TreeRevisionMismatchError` (409 `conflict` `tree_revision_mismatch`, with
|
|
91
|
+
`currentTreeRevision`) and nothing changed.
|
|
92
|
+
- Calls the mode lacks fail with `NotSupportedForModeError` (409 `conflict` `not_supported_for_mode`, `mode`,
|
|
93
|
+
`operation`, `local`): locally, without a request, when the handle knows the mode (file-first: exec sessions, PTY,
|
|
94
|
+
processes, git, browser, `changes()`, `suspend`, `resume`, `snapshot`, `fork`, `reset`, `saveAsTemplate`;
|
|
95
|
+
processful: `executions.*` and `ifTreeRevision`, which a processful cell would ignore), otherwise as the server's
|
|
96
|
+
refusal, typed the same way. On a file-first workspace `wake()` resolves `false` and `hint()` / `cell.wakeHint()`
|
|
97
|
+
answer `resident` without a request (nothing sleeps).
|
|
98
|
+
- Agent tools: `workspaceTools()` of a file-first workspace offers `exec` and the files tools only; its `exec` runs an
|
|
99
|
+
execution and returns `execution_id`, `state`, `tree_revision`, `changed` (up to 200, `changed_truncated`) and
|
|
100
|
+
`error` besides the output. New options: `mode` (build definitions without touching the workspace) and
|
|
101
|
+
`onExecution(id)` (called with the execution id before it is sent, so a caller that gives up can fetch the result).
|
|
102
|
+
**Breaking:** `workspaceTools()` now reads `workspace.mode` while building the definitions unless `mode` is given; a
|
|
103
|
+
caller that builds definitions from a stand-in workspace (as the MCP server does) passes `mode`.
|
|
104
|
+
- Errors: every error is built by reason, so `NotSupportedForModeError` and `TreeRevisionMismatchError` (both
|
|
105
|
+
`ShardfluxApiError`) come from any call; `ShardfluxApiError.treeRevision` is the refusal's `X-Tree-Revision`. Their
|
|
106
|
+
`name` is the subclass's (code that compared `err.name` with `'ShardfluxApiError'` should use `instanceof`).
|
|
107
|
+
`KnownErrorReason` adds `not_supported_for_mode`, `mode_mismatch`, `mode_not_available`, `layout_unsupported`,
|
|
108
|
+
`tree_revision_mismatch`, `outside_tree_root`, `execution_in_progress`, `execution_id_reused`,
|
|
109
|
+
`operation_id_reused`, `no_execution_host`, and the execution error reasons `lease_expired`, `host_unreachable`,
|
|
110
|
+
`host_restarted`, `tree_moved`, `blob_missing`, `blob_corrupt`, `exec_failed_to_start`. Retry progress causes name the
|
|
111
|
+
reason (`HTTP 503 service_unavailable no_execution_host`).
|
|
112
|
+
- Types: `WorkspaceMode`, `ExecutionResult`, `ExecutionResultBody`, `ExecutionChange`, `ExecutionState`,
|
|
113
|
+
`ExecutionError`, `ExecutionRunOptions`, `ExecutionGetOptions`, `TreeRevisionOptions`.
|
|
114
|
+
|
|
115
|
+
### Waking a suspended workspace is one request (contracts §22.6)
|
|
116
|
+
|
|
117
|
+
Needs an API with the held resume for the single request; against an older API every call below works as in 0.7.0.
|
|
118
|
+
|
|
119
|
+
- The wake of a tool call refused with `workspace_not_running` (and `hint()`'s background wake, and `wake()`) sends
|
|
120
|
+
one `POST /v1/workspaces/{id}/resume` with `Prefer: wait` and the calling client's agent label and tools. The API
|
|
121
|
+
answers once the workspace runs, with the view and a tool token minted during the restore: the handle keeps both
|
|
122
|
+
and the refused call is retried at once. On staging the automatic wake made five requests before the command
|
|
123
|
+
(refused call, resume, wait, view, token: 1,118 ms median, against 838 ms for a held reopen); it is now two.
|
|
124
|
+
- Without `Preference-Applied`, or on a 202 (not finished within the hold), the wake waits for the operation and reads
|
|
125
|
+
the view as before; the retried call then fetches its token.
|
|
126
|
+
- `resume({ wait })` (handle and `workspaces.resume(id)`) is the same single held request; a handle takes the view and
|
|
127
|
+
the token. `serverWait: false` keeps the polled path. A workspace that is already running still throws `conflict`
|
|
128
|
+
`already_running` (the held answer is 200 without an operation; the SDK reports it as before).
|
|
129
|
+
- Timing: a held resume or wake is one `request` phase with reason `held`; `server` comes from the returned operation.
|
|
130
|
+
- New options: `WakeOptions.agentLabel` / `tools` and `ResumeOptions` (`agentLabel`, `tools`; `WaitedResumeOptions`)
|
|
131
|
+
choose the token that comes back; `cell()`'s own wake passes its label and tools. `WorkspacesApi.requestResume()`
|
|
132
|
+
is the bare request (`ResumeAnswer`, `ResumeRequestOptions`, `ResumeResponse`).
|
|
133
|
+
- `CellClient`: after a wake that put a new token into the client's manager, the retry uses it instead of fetching one.
|
|
134
|
+
### The account plane and the version check (contracts §30)
|
|
135
|
+
|
|
136
|
+
Needs an API with CLI sessions and client versions (contracts §30). Every API is additive; the one change in
|
|
137
|
+
behavior is the automatic version check (below), which makes one background request per process.
|
|
138
|
+
|
|
139
|
+
### Account plane: `ShardfluxAccount`
|
|
140
|
+
|
|
141
|
+
- `ShardfluxAccount`: what a person does in the web app, from code, with a CLI session (`sfu_<token>`, sent as
|
|
142
|
+
`Authorization: Bearer` on `/v1`). Options: `sessionToken?`, `baseUrl?`, `fetch?`, `userAgent?`, `timeoutMs?`,
|
|
143
|
+
`maxRetries?`, `sleep?`, `onSessionToken?`, `versionCheck?`, `onProgress?`. A malformed token throws (shape
|
|
144
|
+
`^sfu_[A-Za-z0-9_-]{43}$`, also `isSessionToken()` and `SESSION_TOKEN_PATTERN`).
|
|
145
|
+
- Before a session exists (static, no token): `register({ email, password, displayName? })`,
|
|
146
|
+
`verifyEmail(linkOrToken)`, `requestPasswordReset(email)`, `confirmPasswordReset({ token, newPassword })`,
|
|
147
|
+
`confirmEmailChange(linkOrToken)` and `login({ email, password, onSessionToken? })` → `{ account, result }`
|
|
148
|
+
(`result.status` `authenticated` or `mfa_required`). Each takes the client options.
|
|
149
|
+
- Token rotation: login, `auth.completeMfa()`, `auth.stepUp()`, `auth.changePassword()`, `auth.totp.confirm()` and
|
|
150
|
+
`auth.totp.disable()` return a new token (the previous one is revoked). `account.sessionToken` always holds the
|
|
151
|
+
current one, every later call (the reused namespaces included) sends it, and `onSessionToken({ token, expiresAt })`
|
|
152
|
+
is called (and awaited) with each.
|
|
153
|
+
- Namespaces: `auth` (`session`, `completeMfa`, `logout`, `logoutAll`, `sessions`, `revokeSession`, `stepUp`,
|
|
154
|
+
`changePassword`, `changeEmail`, `resendVerification`, `totp.enroll|confirm|disable|regenerateRecoveryCodes`),
|
|
155
|
+
`organizations` (`list`, `listAll`, `create`, `get`, `entitlements`, `deletion`, `delete`, `exports.create|get|download`,
|
|
156
|
+
`workspaces`), `projects` (`list`, `listAll`, `create`, `get`), `apiKeys` (`list`, `create`, `revoke`), `members`
|
|
157
|
+
(`list`, `update`, `remove`), `invitations` (`list`, `create`, `revoke`, `accept`), `billing` (`catalog`,
|
|
158
|
+
`subscription`, `checkout`, `checkoutStatus`, `waitForCheckout`, `portal`, `invoices`, `spendPolicy`,
|
|
159
|
+
`setSpendPolicy`), `user` (`deletion`, `scheduleDeletion`, `cancelDeletion`, `exports.create|get|download`),
|
|
160
|
+
`templates` (every `TemplatesApi` method plus `publishVersion` and `archiveVersion`), `audit` (`list`, `listAll`,
|
|
161
|
+
`export` → the CSV or NDJSON text), `usage`, `secrets`, `egress`, `volumes`, `workspaces`, `me()` and `request()`.
|
|
162
|
+
List methods return the API's page `{ data, next_cursor }`.
|
|
163
|
+
- `apiKeys.create(projectId, { name, toolPermissions?, expiresAt?, idempotencyKey? })` sends an Idempotency-Key (a
|
|
164
|
+
fresh one unless given), so a retried request returns the same key and secret instead of creating a second key.
|
|
165
|
+
`toolPermissions` defaults to `[]`; `API_KEY_TOOL_PERMISSIONS` lists every tool.
|
|
166
|
+
- `billing.waitForCheckout(orgId, checkoutId, { timeoutMs?, intervalMs?, signal? })` polls until the subscription is
|
|
167
|
+
active or the checkout expired, was canceled or failed, and returns that status; after `timeoutMs` (default 15 min)
|
|
168
|
+
it throws `CheckoutTimeoutError` (`checkout`, `waitedMs`), on abort the signal's reason.
|
|
169
|
+
- `parseEmailToken(input)`: the `token` of an emailed link's `#token=` fragment or `?token=` query (percent-decoded),
|
|
170
|
+
else the trimmed input; throws for an empty input or a link without a token. Every method that takes an emailed
|
|
171
|
+
token accepts the whole link.
|
|
172
|
+
- Errors are `ShardfluxApiError` as elsewhere; the session adds the codes `step_up_required` (call `auth.stepUp()` and
|
|
173
|
+
retry), `mfa_required` and `email_unverified` (403).
|
|
174
|
+
|
|
175
|
+
### Version check
|
|
176
|
+
|
|
177
|
+
- `checkClientVersion({ baseUrl?, fetch?, package?, version?, ecosystem?, timeoutMs?, signal? })` →
|
|
178
|
+
`ClientVersionStatus` `{ status, package, ecosystem, current, latest, minimumSupported, upgradeCommand,
|
|
179
|
+
releaseNotesUrl, message? }` from `GET /v1/client-versions`. `status`: `unsupported` (below `minimum_supported`),
|
|
180
|
+
`outdated` (below `latest`), `current`, or `unknown` (no entry, `latest` null, a version that does not parse, or the
|
|
181
|
+
request failed; it never throws). `compareVersions(a, b)` → -1, 0, 1, or null for a string that is not
|
|
182
|
+
`major.minor.patch` (a pre-release sorts before its release).
|
|
183
|
+
- Automatic: after the first successful API response of a `Shardflux` or `ShardfluxAccount` client, once per process
|
|
184
|
+
per package identity, a background request (3 s timeout, every error swallowed; it never delays a call) emits
|
|
185
|
+
`process.emitWarning(message, { type: 'ShardfluxUpdateWarning', code: 'SHARDFLUX_UPDATE_AVAILABLE' })` when the
|
|
186
|
+
package is outdated or unsupported. Option `versionCheck?: boolean | { package, version, ecosystem? }` on both
|
|
187
|
+
clients (default `@shardflux/sdk` at `SDK_VERSION`; tools built on the SDK pass their own identity or `false`).
|
|
188
|
+
Off with `SHARDFLUX_NO_UPDATE_CHECK=1` (also `true`, `yes`, `on`) or `NO_UPDATE_NOTIFIER=1`. `fetchBillingCatalog()`
|
|
189
|
+
does not check.
|
|
190
|
+
|
|
191
|
+
### Feedback straight to the founder (POST /v1/feedback)
|
|
192
|
+
|
|
193
|
+
- `cloud.sendFeedback({ message, category?, context? })` sends feedback to the Shardflux founder by email and returns
|
|
194
|
+
`{ id, receivedAt, duplicate }`. `category`: `bug`, `confusing`, `missing`, `idea`, `praise` or `other` (default).
|
|
195
|
+
`context`: `agent`, `client`, `workspace`, `requestId`, `errorCode`, `command`, `page` (sent in snake_case);
|
|
196
|
+
`client` defaults to `shardflux-sdk-ts/<SDK_VERSION>`.
|
|
197
|
+
- `duplicate: true`: the same message from the same key within 24 hours; the original is returned and no second email
|
|
198
|
+
is sent. 429 `rate_limited` (with `retryAfterSeconds`) and 422 `validation_failed` are `ShardfluxApiError`s. The call
|
|
199
|
+
is a POST without an idempotency key, so it is never retried.
|
|
200
|
+
- `ShardfluxAccount.sendFeedback({ ..., organizationId? })`: the same with a CLI session, as the signed-in user,
|
|
201
|
+
optionally about one of their organizations (404 `not_found` for another).
|
|
202
|
+
- New exports: `FeedbackCategory`, `FeedbackContext`, `FeedbackReceipt`, `SendFeedbackParams`, `AccountFeedbackParams`,
|
|
203
|
+
`FEEDBACK_CATEGORIES`, `FEEDBACK_MESSAGE_MAX_LENGTH`.
|
|
204
|
+
|
|
205
|
+
## 0.8.0 (2026-09-28)
|
|
206
|
+
|
|
207
|
+
Types only; nothing changes at run time and the API is unchanged.
|
|
208
|
+
|
|
209
|
+
### Provider tool exports type-check without casts
|
|
210
|
+
|
|
211
|
+
- `toAnthropicTools(tools)` is assignable to `Anthropic.Tool[]` (`@anthropic-ai/sdk`) and `toOpenAITools(tools, { api:
|
|
212
|
+
'responses' })` to `OpenAI.Responses.FunctionTool[]` (`openai`) under TypeScript's `strict` checks, with no `as`
|
|
213
|
+
casts. `toOpenAITools(tools)` (and `{ api: 'chat' }`) is assignable to `OpenAI.Chat.ChatCompletionTool[]`.
|
|
214
|
+
- `toOpenAITools` has one return type per format (overloads): an `api` known only at run time still returns either
|
|
215
|
+
array, as before.
|
|
216
|
+
- `JsonSchema` is a type alias instead of an interface, so a schema is assignable to the providers' open schema types
|
|
217
|
+
(`{ [key: string]: unknown }`). An object literal typed as `JsonSchema` still rejects a misspelt keyword.
|
|
218
|
+
- `executeToolCall(tools, call)` takes `input` as `unknown`, as the Anthropic SDK's `ToolUseBlock` types it, so a
|
|
219
|
+
`tool_use` block is passed as it is (no `block.input as Record<string, unknown>`); `execute` validates it as before.
|
|
220
|
+
- New exported types for the three formats: `AnthropicToolDefinition`, `OpenAIChatToolDefinition`,
|
|
221
|
+
`OpenAIResponsesToolDefinition`.
|
|
222
|
+
- Checked at compile time against `@anthropic-ai/sdk` 0.128.0 and `openai` 7.23.0 (devDependencies only; the SDK still
|
|
223
|
+
has no runtime dependencies).
|
|
224
|
+
|
|
225
|
+
## 0.7.0 (2026-09-28)
|
|
7
226
|
|
|
8
227
|
Needs an API with the template editor (contracts §24); every new field is additive and older fields are unchanged.
|
|
9
228
|
|
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ tool calls into the workspace ([tool-call capture](#tool-call-capture-070)).
|
|
|
10
10
|
> **Early access.** Shardflux is in early access. The API is versioned (`/v1`), but this SDK is
|
|
11
11
|
> below 1.0: a minor release may contain breaking changes (see [Compatibility](#compatibility)).
|
|
12
12
|
|
|
13
|
-
> **Versions.** This README describes 0.
|
|
14
|
-
> 0.5.0; [CHANGELOG.md](./CHANGELOG.md) lists what each version added. Check yours with
|
|
13
|
+
> **Versions.** This README describes 0.9.0. Anything marked **(0.9.0+)** is not in 0.8.x, **(0.8.0+)** not in 0.7.x,
|
|
14
|
+
> **(0.7.0+)** not in 0.6.x and **(0.6.0+)** not in 0.5.0; [CHANGELOG.md](./CHANGELOG.md) lists what each version added. Check yours with
|
|
15
15
|
> `npm ls @shardflux/sdk` or the exported `SDK_VERSION`.
|
|
16
16
|
|
|
17
17
|
- ESM only, no runtime dependencies, Node.js 24 or later. Reading a YAML template file uses the optional peer
|
|
@@ -67,11 +67,12 @@ const cloud = new Shardflux({
|
|
|
67
67
|
baseUrl: process.env.SHARDFLUX_API_URL, // optional: default https://api.shardflux.dev
|
|
68
68
|
timeoutMs: 30_000, // optional: per-request timeout
|
|
69
69
|
maxRetries: 2, // optional: retries of safe or idempotent requests
|
|
70
|
+
versionCheck: true, // optional (0.9.0+): see Version check
|
|
70
71
|
});
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
The SDK reads nothing from the environment by itself. `SHARDFLUX_API_KEY` and
|
|
74
|
-
are the conventional names (the `shard` CLI reads them); pass them in as shown.
|
|
74
|
+
The SDK reads nothing from the environment by itself (except the version check's opt-out). `SHARDFLUX_API_KEY` and
|
|
75
|
+
`SHARDFLUX_API_URL` are the conventional names (the `shard` CLI reads them); pass them in as shown.
|
|
75
76
|
|
|
76
77
|
## Commands and files
|
|
77
78
|
|
|
@@ -100,6 +101,53 @@ await cell.files.remove('/home/user/data.bin');
|
|
|
100
101
|
twice. Aborting its `signal` also cancels the command in the workspace. File writes are atomic
|
|
101
102
|
and durable (acknowledged after fsync).
|
|
102
103
|
|
|
104
|
+
### Search, patch and revisions (0.9.0+)
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const hits = await cell.files.search('/home/user/project', 'TODO', { include: ['**/*.py'], contextLines: 1 });
|
|
108
|
+
for (const m of hits.matches) console.log(`${m.path}:${m.line}:${m.column}: ${m.text}`);
|
|
109
|
+
|
|
110
|
+
// A file's revision is the SHA-256 of its content.
|
|
111
|
+
const { revision } = await cell.files.stat('/home/user/project/app.py', { revision: true });
|
|
112
|
+
const patched = await cell.files.patch({
|
|
113
|
+
path: '/home/user/project/app.py',
|
|
114
|
+
edits: [{ oldText: 'DEBUG = True', newText: 'DEBUG = False' }], // must occur exactly once (or replaceAll)
|
|
115
|
+
expectedRevision: revision, // refused if the file changed meanwhile
|
|
116
|
+
});
|
|
117
|
+
patched.revision; // the next expectedRevision
|
|
118
|
+
|
|
119
|
+
const { data, revision: current, servedFrom } = await cell.files.readWithInfo('/home/user/project/app.py');
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- `search()` searches a directory (or one file) and returns matching lines in path order (`path`, 1-based `line` and
|
|
123
|
+
byte `column`, `text`), stopping at `maxMatches` (default 200), a 10 s budget or 4 MiB of results (`truncated`,
|
|
124
|
+
`stop_reason`). `include`/`exclude` globs are gitignore-style: `*.py` matches a name at any depth, `src/**/*.ts`
|
|
125
|
+
a path relative to the searched directory, `build/` directories only. Binary files, symbolic links, files above
|
|
126
|
+
`maxFileBytes` and `.git`/`node_modules` (unless `exclude` is given) are skipped.
|
|
127
|
+
- `patch()` requests are limited to 7 MiB (413 `payload_too_large`; write larger files with `write()`) and files to
|
|
128
|
+
64 MiB.
|
|
129
|
+
- `patch()` applies all edits or none, atomically and durably, and always sends an `Idempotency-Key`. `content`
|
|
130
|
+
replaces the whole file instead; `expectedRevision: 'absent'` requires that the file does not exist yet. A changed
|
|
131
|
+
file is 409 `conflict` with `reason` `revision_mismatch` and `details.current_revision`; an edit that does not match
|
|
132
|
+
exactly once is 422 `edit_not_found` or `edit_ambiguous` with `details.index`.
|
|
133
|
+
- A suspended workspace whose disk is still on a host is read, listed and searched there without waking it; such
|
|
134
|
+
results say `servedFrom: 'disk'` (`served_from` on search results). Everything else wakes it as usual. This also
|
|
135
|
+
works for a handle without a tool token from before the suspend: the API issues tokens for suspended workspaces.
|
|
136
|
+
- While the fleet is being upgraded, a workspace may run on a host that predates search and patches: they are 409
|
|
137
|
+
`conflict` with `reason` `host_feature_unavailable` and `details.feature` (`file_search`, `file_patch`), not
|
|
138
|
+
retryable (read and write the file, or run `grep` with `exec`, instead), and revisions are omitted.
|
|
139
|
+
|
|
140
|
+
### Wake hint (0.9.0+)
|
|
141
|
+
|
|
142
|
+
An idle running workspace may be parked by its host (frozen or hibernated) and is restored by the next tool call.
|
|
143
|
+
`workspace.hint()` tells the host a tool call is coming so the restore starts earlier: call it when your model starts
|
|
144
|
+
emitting a tool call, before its arguments are complete. It is cheap and returns at once; a suspended workspace is
|
|
145
|
+
resumed in the background (`result.wake`). The agent tools below send it when each call starts.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
void workspace.hint().catch(() => {}); // fire and forget
|
|
149
|
+
```
|
|
150
|
+
|
|
103
151
|
The same client has `exec.start/get/output/signal/cancel`, `pty`, `processes`, `git` and
|
|
104
152
|
`browser` (screenshot and page content).
|
|
105
153
|
|
|
@@ -153,8 +201,13 @@ never runs twice: the cell executes nothing it refused.
|
|
|
153
201
|
- The wait is bounded per call by `transitionTimeoutMs` (default 120 000 ms), shared by the transition waits and at
|
|
154
202
|
most 3 wakes. A resume or open still pending at the end throws `OperationTimeoutError`, naming the operation, its
|
|
155
203
|
state and reason. A failed resume or open throws `OperationFailedError` at once.
|
|
204
|
+
- The wake is one request (0.9.0+): the API holds the resume until the workspace runs and answers with the view and a
|
|
205
|
+
tool token for the calling client, so the refused call is retried at once (refused call, resume, the call). Its
|
|
206
|
+
timing is one `request` phase with reason `held`. An API without the held resume answers at once; the SDK then waits
|
|
207
|
+
for the operation, reads the view and fetches a token, as before.
|
|
156
208
|
- Following an exec's output never wakes a workspace, so an explicit `suspend()` is respected.
|
|
157
|
-
- `workspace.wake({ timeoutMs })` does the same on demand.
|
|
209
|
+
- `workspace.wake({ timeoutMs })` does the same on demand (`agentLabel` / `tools` pick the token it brings back).
|
|
210
|
+
`workspace.resume({ wait: true })` is the same single held request: the handle keeps the view and the token.
|
|
158
211
|
- `workspace.cell({ wake: null })` returns `workspace_not_running` instead.
|
|
159
212
|
|
|
160
213
|
```ts
|
|
@@ -273,6 +326,50 @@ const page = await ws.changes({ pathPrefix: '/home/user', summary: true }); //
|
|
|
273
326
|
`legacy_disk_layout`. `changes()` is served by the cell from the running workspace and needs the `files` tool.
|
|
274
327
|
`cell().changesAll()` follows the pages.
|
|
275
328
|
|
|
329
|
+
## File-first workspaces (0.9.0+)
|
|
330
|
+
|
|
331
|
+
A file-first workspace has no VM between commands. Its state is a versioned file tree under /home/user (tree
|
|
332
|
+
revisions 0, 1, 2, ...). Each command runs as an execution: a fresh VM on the latest revision, whose changed files
|
|
333
|
+
become the next revision. Nothing else survives an execution (processes, memory, files outside /home/user), so install
|
|
334
|
+
dependencies into /home/user (for example a virtualenv) and start servers within the command that uses them. It is
|
|
335
|
+
ready as soon as it is opened and is never suspended.
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const ws = await cloud.workspaces.open({ key: 'customer-42/repo', template: 'python-node-browser', mode: 'file_first' });
|
|
339
|
+
const cell = ws.cell();
|
|
340
|
+
await cell.files.write('/home/user/app/main.py', 'print("hi")\n', { createParents: true });
|
|
341
|
+
|
|
342
|
+
const r = await ws.executions.run(['bash', '-lc', 'cd app && python3 main.py > out.txt && cat out.txt'], { timeoutMs: 600_000 });
|
|
343
|
+
r.state; // 'succeeded' (any exit code), 'failed' or 'lost' (nothing published; r.errorReason says why)
|
|
344
|
+
r.exitCode; // 0
|
|
345
|
+
r.stdoutText; // 'hi\n' (r.stdout holds the bytes)
|
|
346
|
+
r.changed; // [{ path: '/home/user/app/out.txt', change: 'added', type: 'file' }]
|
|
347
|
+
r.treeRevision; // 2: the revision the execution published (= baseRevision when it changed nothing)
|
|
348
|
+
ws.treeRevision; // 2
|
|
349
|
+
|
|
350
|
+
// Conditional writes: applied only if the tree is still at that revision.
|
|
351
|
+
await cell.files.write('/home/user/app/main.py', 'print("bye")\n', { ifTreeRevision: ws.treeRevision! });
|
|
352
|
+
// ... else TreeRevisionMismatchError (err.currentTreeRevision): read what changed and try again.
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- **Executions are idempotent by id.** `executionId` defaults to a fresh `ex-<uuid>`; pass your own to make a call safe
|
|
356
|
+
to repeat across processes. Network failures and retryable 5xx answers (503 `no_execution_host`: no host has room;
|
|
357
|
+
the SDK waits `Retry-After`) are retried with the same id, at most `maxRetries` (5) times, and an answer that takes
|
|
358
|
+
longer than `attemptTimeoutMs` (300 s) is awaited again with the same id. The cell runs a command once per id: a
|
|
359
|
+
repeated call gets the recorded result (`r.replayed`). The SDK never retries with a new id: a `failed` or `lost`
|
|
360
|
+
result is returned, and running it again is your decision.
|
|
361
|
+
- `ws.executions.get(id, { waitMs })` reads an execution: `pending` while it runs, its result when it ended (kept 7
|
|
362
|
+
days). Use it after a call that stopped waiting (an aborted `signal`): an execution cannot be canceled.
|
|
363
|
+
- While an execution runs, other executions and file writes are refused with 409 `workspace_busy`
|
|
364
|
+
(`execution_in_progress`); the SDK waits them out within `transitionTimeoutMs` (120 s).
|
|
365
|
+
- Only paths under /home/user exist (`outside_tree_root` otherwise). Reads, writes, `search()` and `patch()` work as on
|
|
366
|
+
a processful workspace, on the latest revision.
|
|
367
|
+
- Calls a file-first workspace does not have fail with `NotSupportedForModeError` before any request: exec sessions
|
|
368
|
+
(`cell.exec.*`), PTY, processes, git, browser, `changes()`, `suspend`, `resume`, `snapshot`, `fork`, `reset` and
|
|
369
|
+
`saveAsTemplate`. On a processful workspace `executions` and `ifTreeRevision` are refused the same way.
|
|
370
|
+
- Reopening a key with another `mode` is 409 `mode_mismatch`; a deployment without file-first workspaces answers 422
|
|
371
|
+
`mode_not_available`; a legacy template 409 `layout_unsupported`.
|
|
372
|
+
|
|
276
373
|
## Templates: file tree, diff and dev mode
|
|
277
374
|
|
|
278
375
|
```ts
|
|
@@ -383,18 +480,38 @@ parameters and an `execute` function. Export them for your model provider and di
|
|
|
383
480
|
calls:
|
|
384
481
|
|
|
385
482
|
```ts
|
|
483
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
386
484
|
import { executeToolCall, toAnthropicTools, toOpenAITools, workspaceTools } from '@shardflux/sdk';
|
|
387
485
|
|
|
388
486
|
const tools = workspaceTools(workspace);
|
|
389
|
-
const anthropicTools = toAnthropicTools(tools);
|
|
487
|
+
const anthropicTools: Anthropic.Tool[] = toAnthropicTools(tools);
|
|
488
|
+
const chatTools = toOpenAITools(tools); // OpenAI Chat Completions
|
|
489
|
+
const responsesTools = toOpenAITools(tools, { api: 'responses' }); // OpenAI Responses API
|
|
390
490
|
|
|
391
|
-
// For each tool call the model makes:
|
|
392
|
-
const output = await executeToolCall(tools,
|
|
491
|
+
// For each tool call the model makes (an Anthropic tool_use block, or an OpenAI function_call item, as it is):
|
|
492
|
+
const output = await executeToolCall(tools, block);
|
|
393
493
|
```
|
|
394
494
|
|
|
495
|
+
**(0.8.0+)** The exports type-check as the provider SDKs' own types under `strict`, with no casts:
|
|
496
|
+
`Anthropic.Tool[]`, `OpenAI.Chat.ChatCompletionTool[]` and, with `{ api: 'responses' }`,
|
|
497
|
+
`OpenAI.Responses.FunctionTool[]`. `executeToolCall` takes a `tool_use` block's `unknown` input as it is and validates
|
|
498
|
+
it.
|
|
499
|
+
|
|
395
500
|
Your agent loop and model calls stay in your application; the workspace is the computer the tools
|
|
396
501
|
act on.
|
|
397
502
|
|
|
503
|
+
The tools are `exec`, `read_file`, `write_file`, `list_files`, `search_files` and `edit_file` (0.9.0+), the process,
|
|
504
|
+
terminal, git and browser tools, filtered by the tools your key grants. `edit_file` replaces exact text; when the
|
|
505
|
+
model passes no `expected_revision` it reads the file's revision first, so a change made in between fails the edit
|
|
506
|
+
instead of being overwritten. Each call first sends `workspace.hint()` without waiting for it (`hint: false` turns
|
|
507
|
+
that off, e.g. when you send the hint yourself as the model starts a tool call), except `read_file`, `list_files` and
|
|
508
|
+
`search_files`: a sleeping workspace answers them from its disk without waking.
|
|
509
|
+
|
|
510
|
+
For a file-first workspace (0.9.0+) the tools are `exec` and the files tools only: `exec` runs each command as an
|
|
511
|
+
execution and adds `execution_id`, `state`, `tree_revision` and `changed` to its result, and the process, terminal,
|
|
512
|
+
git and browser tools are not offered. `workspaceTools(ws, { mode })` builds the definitions without touching the
|
|
513
|
+
workspace; `onExecution(id)` is called with each execution id before it is sent.
|
|
514
|
+
|
|
398
515
|
## Tool-call capture (0.7.0+)
|
|
399
516
|
|
|
400
517
|
Your harness's tools (web search, SQL, HTTP APIs, MCP servers) run in your application, so their results reach the
|
|
@@ -534,6 +651,58 @@ await workspace.cell().exec.run(['python3', 'agent.py']); // sees $OPEN
|
|
|
534
651
|
`accessEvents`, `createOrganization` and `listOrganization`. Organization-wide secrets and access
|
|
535
652
|
logs belong to organization owners and admins, so a project API key gets 403 for those.
|
|
536
653
|
|
|
654
|
+
## Account (ShardfluxAccount) (0.9.0+)
|
|
655
|
+
|
|
656
|
+
`ShardfluxAccount` does what a person does in the web app, with a user session instead of an API key: sign up, sign
|
|
657
|
+
in (with MFA), organizations, projects, API keys, members, invitations, billing, audit, data export and account
|
|
658
|
+
deletion. Two steps stay human: opening the verification email, and paying in Stripe Checkout.
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
import { Shardflux, ShardfluxAccount } from '@shardflux/sdk';
|
|
662
|
+
|
|
663
|
+
// Before a session exists (static; no token needed). Emailed links can be passed whole.
|
|
664
|
+
await ShardfluxAccount.register({ email, password, displayName: 'Ada' }); // { status: 'accepted' }
|
|
665
|
+
await ShardfluxAccount.verifyEmail('https://app.shardflux.dev/auth/verify-email#token=...');
|
|
666
|
+
|
|
667
|
+
const { account, result } = await ShardfluxAccount.login({ email, password, onSessionToken: (s) => save(s.token) });
|
|
668
|
+
if (result.status === 'mfa_required') await account.auth.completeMfa({ code: '123456' }); // or { recoveryCode }
|
|
669
|
+
|
|
670
|
+
// Later, with the saved token (sfu_<43 characters>; a malformed one throws).
|
|
671
|
+
const again = new ShardfluxAccount({ sessionToken: saved, onSessionToken: (s) => save(s.token) });
|
|
672
|
+
const org = await again.organizations.create({ name: 'Acme' });
|
|
673
|
+
const project = await again.projects.create(org.id, { name: 'Default' });
|
|
674
|
+
const { secret } = await again.apiKeys.create(project.id, { name: 'agent', toolPermissions: ['exec', 'files'] });
|
|
675
|
+
const cloud = new Shardflux({ apiKey: secret }); // the sfk_ key, shown once
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
- The session token is sent as `Authorization: Bearer sfu_...` on `/v1`. Login completion, `auth.stepUp()`,
|
|
679
|
+
`auth.changePassword()`, `auth.totp.confirm()` and `auth.totp.disable()` rotate it (the old token stops working):
|
|
680
|
+
`account.sessionToken` always holds the current token, every later call uses it, and `onSessionToken({ token,
|
|
681
|
+
expiresAt })` is called (and awaited) with each new one. Save it there. Sessions idle out after 30 days.
|
|
682
|
+
- Sensitive calls (exports, deletions, TOTP changes, …) answer 403 `step_up_required` without a recent password
|
|
683
|
+
check: `await account.auth.stepUp({ password, code })`, then retry. A session still waiting for its second factor
|
|
684
|
+
gets 403 `mfa_required`; an unverified email 403 `email_unverified`.
|
|
685
|
+
- Namespaces: `auth` (session, MFA, logout, sessions, step-up, password and email change, `totp`), `organizations`
|
|
686
|
+
(`list`, `listAll`, `create`, `get`, `entitlements`, `deletion`, `delete`, `exports`, `workspaces`), `projects`,
|
|
687
|
+
`apiKeys` (`create` sends an Idempotency-Key, so a retry never makes a second key; `toolPermissions` defaults to
|
|
688
|
+
`[]`), `members`, `invitations` (`accept(linkOrToken)`), `billing`, `user` (account `deletion`, `scheduleDeletion`,
|
|
689
|
+
`cancelDeletion`, `exports`), `templates` (plus `publishVersion` and `archiveVersion`), `audit` (plus `export(orgId,
|
|
690
|
+
{ format: 'csv' | 'ndjson', ...filters })`, the text), and `usage`, `secrets`, `egress`, `volumes` with explicit
|
|
691
|
+
organization and project ids; `me()` and `request()`. List methods return the API's page `{ data, next_cursor }`;
|
|
692
|
+
`listAll` iterates every page.
|
|
693
|
+
- `parseEmailToken(input)` returns the `token` of a link's `#token=` fragment or `?token=` query (else the trimmed
|
|
694
|
+
input), and throws for a link without one.
|
|
695
|
+
|
|
696
|
+
Upgrading a plan: a person pays at the Checkout `url`; the code waits for the subscription.
|
|
697
|
+
|
|
698
|
+
```ts
|
|
699
|
+
const checkout = await account.billing.checkout(org.id, { planKey: 'pro' }); // 409 subscription_exists: use billing.portal()
|
|
700
|
+
console.log(`Pay here: ${checkout.url}`);
|
|
701
|
+
const done = await account.billing.waitForCheckout(org.id, checkout.id, { timeoutMs: 15 * 60_000 });
|
|
702
|
+
if (!done.subscription_active) console.log(`checkout ${done.status}`); // expired, canceled or failed
|
|
703
|
+
// After timeoutMs: CheckoutTimeoutError (err.checkout is the last status); on abort: the signal's reason.
|
|
704
|
+
```
|
|
705
|
+
|
|
537
706
|
## Errors
|
|
538
707
|
|
|
539
708
|
- `ShardfluxApiError`: the API or the workspace refused the request. Fields: `status`, `code`,
|
|
@@ -546,22 +715,90 @@ await workspace.cell().exec.run(['python3', 'agent.py']); // sees $OPEN
|
|
|
546
715
|
- `OperationTimeoutError`: waiting gave up; the operation continues (`operationId`, `lastState`, `lastReason`,
|
|
547
716
|
`deadlineAt` **(0.6.2+)** while it waits for capacity, `timing`).
|
|
548
717
|
- `ShardfluxProtocolError`: a response was not the documented shape.
|
|
718
|
+
- `NotSupportedForModeError` **(0.9.0+)**, a `ShardfluxApiError` (409 `conflict`, `reason` `not_supported_for_mode`):
|
|
719
|
+
the call does not exist for the workspace's `mode`; `local` is true when the SDK refused it without a request.
|
|
720
|
+
- `TreeRevisionMismatchError` **(0.9.0+)**, a `ShardfluxApiError` (409 `conflict`, `reason` `tree_revision_mismatch`):
|
|
721
|
+
an `ifTreeRevision` call found the tree at `currentTreeRevision`; nothing changed.
|
|
722
|
+
- `CheckoutTimeoutError` **(0.9.0+)**: `billing.waitForCheckout()` gave up; the checkout stays open (`checkout`,
|
|
723
|
+
`waitedMs`).
|
|
549
724
|
|
|
550
725
|
Treat unknown error codes and reasons as generic errors: show `message`, and use `retryable`.
|
|
551
726
|
|
|
727
|
+
Retryable 429/502/503/504 refusals (for example 503 `host_capacity`, when the workspace's host has no room to restore
|
|
728
|
+
it right now, or `wake_failed`) are retried after `Retry-After` for reads, searches and calls that carry an
|
|
729
|
+
Idempotency-Key (writes and patches); other calls surface them with `retryable: true` and `retryAfterSeconds`.
|
|
730
|
+
A read of a sleeping workspace that its disk cannot answer (409 `workspace_not_running` with `reason`
|
|
731
|
+
`offline_unavailable` or `offline_budget`) wakes the workspace and is retried like any `workspace_not_running`; 503
|
|
732
|
+
`offline_changed` (the disk changed during the read) is retried and served by the running workspace. 409 `conflict`
|
|
733
|
+
`host_feature_unavailable` (the workspace's host predates the call, `details.feature`) is neither retried nor
|
|
734
|
+
woken: it lasts until the workspace runs on an upgraded host.
|
|
735
|
+
|
|
736
|
+
## Feedback (0.9.0+)
|
|
737
|
+
|
|
738
|
+
`cloud.sendFeedback()` sends a message straight to the Shardflux founder, who reads every one. If you or your coding
|
|
739
|
+
agent hit something while building with Shardflux, send it the moment it happens: a call that failed unexpectedly, an
|
|
740
|
+
error or doc that was confusing, something missing or slow, a workaround you needed. Short and specific beats polished;
|
|
741
|
+
the request id and error code let the founder find the logs.
|
|
742
|
+
|
|
743
|
+
```ts
|
|
744
|
+
try {
|
|
745
|
+
await cloud.workspaces.open({ key: 'acme/demo', template: 'python-node-browser' });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
if (err instanceof ShardfluxApiError) {
|
|
748
|
+
await cloud.sendFeedback({
|
|
749
|
+
message: 'open failed with capacity_unavailable twice in 10 minutes; expected a start within a minute',
|
|
750
|
+
category: 'bug',
|
|
751
|
+
context: { requestId: err.requestId, errorCode: err.code, workspace: 'acme/demo', agent: 'claude-code' },
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
throw err;
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
- `message`: 1-8000 characters. `category`: `bug` (something failed or behaved wrongly), `confusing` (an error, doc,
|
|
759
|
+
name or output was unclear), `missing` (a capability, option or template you needed), `idea`, `praise` or `other`
|
|
760
|
+
(the default).
|
|
761
|
+
- `context` (all optional): `agent` (who is reporting, e.g. `claude-code`), `workspace`, `requestId`, `errorCode`,
|
|
762
|
+
`command` (the call that led to it), and `client`, which defaults to `shardflux-sdk-ts/<version>`.
|
|
763
|
+
- Returns `{ id, receivedAt, duplicate }`. The same message from the same key within 24 hours returns the original
|
|
764
|
+
with `duplicate: true` and sends no second email.
|
|
765
|
+
- Any API key may send feedback. Signed in with a CLI session instead, `account.sendFeedback({ message, category?,
|
|
766
|
+
context?, organizationId? })` sends it as the user (`ShardfluxAccount`; `organizationId`: one of yours). It is rate
|
|
767
|
+
limited per key or user: a `ShardfluxApiError` with `code: 'rate_limited'` and `retryAfterSeconds`. An empty or too-long message is `validation_failed`. The SDK never retries the call.
|
|
768
|
+
- Anything shaped like an API key is redacted before the message is stored or emailed; still, leave secrets out.
|
|
769
|
+
|
|
552
770
|
## More of the API
|
|
553
771
|
|
|
554
772
|
The `Shardflux` object also has `templates` (including custom template builds and the template editor), `volumes`
|
|
555
773
|
(shared persistent storage attached to workspaces), `secrets` (see above), `egress` (outbound allowlists),
|
|
556
|
-
`usage`, `billing`, `me()`, `entitlements(orgId)` and `request(method, path)` for any `/v1`
|
|
774
|
+
`usage`, `billing`, `me()`, `entitlements(orgId)`, `sendFeedback()` and `request(method, path)` for any `/v1`
|
|
557
775
|
route. The package exports the OpenAPI-generated types as well (`paths`, `components`,
|
|
558
776
|
`WorkspaceView`, `Operation` and more).
|
|
559
777
|
|
|
778
|
+
## Version check (0.9.0+)
|
|
779
|
+
|
|
780
|
+
After the first successful API response of the process, the SDK asks `GET /v1/client-versions` in the background
|
|
781
|
+
(once per process, 3 s timeout, every error ignored; it never delays or fails a call). When this version is outdated
|
|
782
|
+
or no longer supported, it emits one warning:
|
|
783
|
+
|
|
784
|
+
```
|
|
785
|
+
(node:1234) [SHARDFLUX_UPDATE_AVAILABLE] ShardfluxUpdateWarning: @shardflux/sdk 0.9.0 is outdated: 0.10.0 is available. Update: npm install @shardflux/sdk@latest
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
- Turn it off with `versionCheck: false` (on `Shardflux` or `ShardfluxAccount`), `SHARDFLUX_NO_UPDATE_CHECK=1` (also
|
|
789
|
+
`true`, `yes`, `on`) or `NO_UPDATE_NOTIFIER=1`. Handle it with `process.on('warning', (w) => ...)`
|
|
790
|
+
(`w.name === 'ShardfluxUpdateWarning'`).
|
|
791
|
+
- A tool built on the SDK checks its own package instead: `versionCheck: { package: '@acme/tool', version: '1.2.3' }`.
|
|
792
|
+
- On demand: `await checkClientVersion()` returns `{ status, package, ecosystem, current, latest, minimumSupported,
|
|
793
|
+
upgradeCommand, releaseNotesUrl, message? }` with `status` `current`, `outdated`, `unsupported` or `unknown` (the
|
|
794
|
+
request failed, or the package is not listed yet). `compareVersions(a, b)` compares two `major.minor.patch` versions
|
|
795
|
+
(a pre-release sorts first; `null` when one does not parse).
|
|
796
|
+
|
|
560
797
|
## Compatibility
|
|
561
798
|
|
|
562
799
|
- The SDK follows the API's `/v1` contract. New fields, enum values and error codes can appear in
|
|
563
800
|
any release; ignore unknown fields.
|
|
564
|
-
- While below 1.0, a breaking change bumps the minor version (0.
|
|
801
|
+
- While below 1.0, a breaking change bumps the minor version (0.7 to 0.8).
|
|
565
802
|
- `SDK_VERSION` is exported; requests send `User-Agent: shardflux-sdk-ts/<version>`.
|
|
566
803
|
- Examples in this README, in `examples/` and on shardflux.dev name the version they need. The examples on the
|
|
567
804
|
website and in the console are checked against the version published on npm before they ship.
|