@shardflux/sdk 0.8.0 → 0.10.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 +258 -0
- package/README.md +338 -7
- package/dist/account.d.ts +493 -0
- package/dist/account.js +641 -0
- package/dist/cell.d.ts +203 -8
- package/dist/cell.js +457 -32
- package/dist/client.d.ts +120 -5
- package/dist/client.js +142 -6
- package/dist/errors.d.ts +90 -3
- package/dist/errors.js +93 -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 +10546 -5855
- package/dist/generated/cell-api.d.ts +501 -9
- 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 +28 -1
- package/dist/tools.js +172 -21
- package/dist/usage.d.ts +36 -6
- package/dist/usage.js +19 -4
- package/dist/version-check.d.ts +101 -0
- package/dist/version-check.js +191 -0
- package/dist/workspace.d.ts +100 -6
- package/dist/workspace.js +198 -12
- package/package.json +1 -1
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
|
-
> and **(0.6.0+)** not in 0.5.0; [CHANGELOG.md](./CHANGELOG.md) lists what each version added. Check yours with
|
|
13
|
+
> **Versions.** This README describes 0.10.0. Anything marked **(0.10.0+)** is not in 0.9.0, **(0.9.0+)** 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,70 @@ 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
|
+
`cwd` is an absolute path (commands start in `/home/user` without one). The API refuses a relative
|
|
105
|
+
`cwd` with 422 `validation_failed` (`err.reason === 'invalid_cwd'`); the message names the absolute
|
|
106
|
+
path it likely means. A command that could not start (a `cwd` that is not a directory, a program
|
|
107
|
+
that is not on `PATH`) rejects with `ExecStartError` **(0.10.0+)**, whose message is the workspace's
|
|
108
|
+
reason; before 0.10.0 `exec.run()` resolved with `exitCode: null` and no output.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { ExecStartError } from '@shardflux/sdk';
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await cell.exec.run(['ls'], { cwd: '/home/user/app' });
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (!(err instanceof ExecStartError)) throw err;
|
|
117
|
+
console.error(err.message); // The command could not start: working directory "/home/user/app" is not a directory
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Search, patch and revisions (0.9.0+)
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const hits = await cell.files.search('/home/user/project', 'TODO', { include: ['**/*.py'], contextLines: 1 });
|
|
125
|
+
for (const m of hits.matches) console.log(`${m.path}:${m.line}:${m.column}: ${m.text}`);
|
|
126
|
+
|
|
127
|
+
// A file's revision is the SHA-256 of its content.
|
|
128
|
+
const { revision } = await cell.files.stat('/home/user/project/app.py', { revision: true });
|
|
129
|
+
const patched = await cell.files.patch({
|
|
130
|
+
path: '/home/user/project/app.py',
|
|
131
|
+
edits: [{ oldText: 'DEBUG = True', newText: 'DEBUG = False' }], // must occur exactly once (or replaceAll)
|
|
132
|
+
expectedRevision: revision, // refused if the file changed meanwhile
|
|
133
|
+
});
|
|
134
|
+
patched.revision; // the next expectedRevision
|
|
135
|
+
|
|
136
|
+
const { data, revision: current, servedFrom } = await cell.files.readWithInfo('/home/user/project/app.py');
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- `search()` searches a directory (or one file) and returns matching lines in path order (`path`, 1-based `line` and
|
|
140
|
+
byte `column`, `text`), stopping at `maxMatches` (default 200), a 10 s budget or 4 MiB of results (`truncated`,
|
|
141
|
+
`stop_reason`). `include`/`exclude` globs are gitignore-style: `*.py` matches a name at any depth, `src/**/*.ts`
|
|
142
|
+
a path relative to the searched directory, `build/` directories only. Binary files, symbolic links, files above
|
|
143
|
+
`maxFileBytes` and `.git`/`node_modules` (unless `exclude` is given) are skipped.
|
|
144
|
+
- `patch()` requests are limited to 7 MiB (413 `payload_too_large`; write larger files with `write()`) and files to
|
|
145
|
+
64 MiB.
|
|
146
|
+
- `patch()` applies all edits or none, atomically and durably, and always sends an `Idempotency-Key`. `content`
|
|
147
|
+
replaces the whole file instead; `expectedRevision: 'absent'` requires that the file does not exist yet. A changed
|
|
148
|
+
file is 409 `conflict` with `reason` `revision_mismatch` and `details.current_revision`; an edit that does not match
|
|
149
|
+
exactly once is 422 `edit_not_found` or `edit_ambiguous` with `details.index`.
|
|
150
|
+
- A suspended workspace whose disk is still on a host is read, listed and searched there without waking it; such
|
|
151
|
+
results say `servedFrom: 'disk'` (`served_from` on search results). Everything else wakes it as usual. This also
|
|
152
|
+
works for a handle without a tool token from before the suspend: the API issues tokens for suspended workspaces.
|
|
153
|
+
- While the fleet is being upgraded, a workspace may run on a host that predates search and patches: they are 409
|
|
154
|
+
`conflict` with `reason` `host_feature_unavailable` and `details.feature` (`file_search`, `file_patch`), not
|
|
155
|
+
retryable (read and write the file, or run `grep` with `exec`, instead), and revisions are omitted.
|
|
156
|
+
|
|
157
|
+
### Wake hint (0.9.0+)
|
|
158
|
+
|
|
159
|
+
An idle running workspace may be parked by its host (frozen or hibernated) and is restored by the next tool call.
|
|
160
|
+
`workspace.hint()` tells the host a tool call is coming so the restore starts earlier: call it when your model starts
|
|
161
|
+
emitting a tool call, before its arguments are complete. It is cheap and returns at once; a suspended workspace is
|
|
162
|
+
resumed in the background (`result.wake`). The agent tools below send it when each call starts.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
void workspace.hint().catch(() => {}); // fire and forget
|
|
166
|
+
```
|
|
167
|
+
|
|
103
168
|
The same client has `exec.start/get/output/signal/cancel`, `pty`, `processes`, `git` and
|
|
104
169
|
`browser` (screenshot and page content).
|
|
105
170
|
|
|
@@ -146,6 +211,29 @@ try {
|
|
|
146
211
|
}
|
|
147
212
|
```
|
|
148
213
|
|
|
214
|
+
### Suspend when idle (0.10.0+)
|
|
215
|
+
|
|
216
|
+
A running workspace is billed while it is awake, and its idle policy waits a while before suspending it. When your
|
|
217
|
+
agent's turn ends, ask for a suspend once the workspace has been idle for a short time instead:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const { suspendRequest } = await workspace.suspendWhenIdle({ afterSeconds: 60 }); // 30..3600
|
|
221
|
+
workspace.suspendRequest; // { requested_at, after_seconds, not_before } until it applies or is cancelled
|
|
222
|
+
await workspace.cancelSuspendWhenIdle(); // idempotent
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- The idle time counts from the later of the workspace's last work and the request; `not_before` is the earliest
|
|
226
|
+
suspend.
|
|
227
|
+
- A command still running, an attached exec or terminal stream, or a keepalive postpones the suspend until
|
|
228
|
+
`afterSeconds` after it ends.
|
|
229
|
+
- The next tool call on the workspace (the next turn) or a resume cancels the request. Repeating replaces it.
|
|
230
|
+
- It applies under every idle policy, `never` included, and never delays a suspend the policy would do sooner.
|
|
231
|
+
- When a suspend is already in progress, the result's `operation` is that suspend and nothing is recorded.
|
|
232
|
+
- Errors: `ShardfluxApiError` 409 with `reason` `not_running`, `operation_in_progress`, `session_lifetime` or
|
|
233
|
+
`workspace_deleted`, and 422 `validation_failed` for `afterSeconds` outside 30..3600. A file-first workspace is never
|
|
234
|
+
suspended: `NotSupportedForModeError` (409 `not_supported_for_mode`).
|
|
235
|
+
- By id: `cloud.workspaces.suspendWhenIdle(id, { afterSeconds })` and `cloud.workspaces.cancelSuspendWhenIdle(id)`.
|
|
236
|
+
|
|
149
237
|
**Suspended workspaces wake on use.** A tool call on a suspended workspace resumes it (or joins the resume or open
|
|
150
238
|
already running), then runs. A call made during a suspend or resume waits for the transition to finish. The call
|
|
151
239
|
never runs twice: the cell executes nothing it refused.
|
|
@@ -153,8 +241,13 @@ never runs twice: the cell executes nothing it refused.
|
|
|
153
241
|
- The wait is bounded per call by `transitionTimeoutMs` (default 120 000 ms), shared by the transition waits and at
|
|
154
242
|
most 3 wakes. A resume or open still pending at the end throws `OperationTimeoutError`, naming the operation, its
|
|
155
243
|
state and reason. A failed resume or open throws `OperationFailedError` at once.
|
|
244
|
+
- The wake is one request (0.9.0+): the API holds the resume until the workspace runs and answers with the view and a
|
|
245
|
+
tool token for the calling client, so the refused call is retried at once (refused call, resume, the call). Its
|
|
246
|
+
timing is one `request` phase with reason `held`. An API without the held resume answers at once; the SDK then waits
|
|
247
|
+
for the operation, reads the view and fetches a token, as before.
|
|
156
248
|
- Following an exec's output never wakes a workspace, so an explicit `suspend()` is respected.
|
|
157
|
-
- `workspace.wake({ timeoutMs })` does the same on demand.
|
|
249
|
+
- `workspace.wake({ timeoutMs })` does the same on demand (`agentLabel` / `tools` pick the token it brings back).
|
|
250
|
+
`workspace.resume({ wait: true })` is the same single held request: the handle keeps the view and the token.
|
|
158
251
|
- `workspace.cell({ wake: null })` returns `workspace_not_running` instead.
|
|
159
252
|
|
|
160
253
|
```ts
|
|
@@ -273,6 +366,50 @@ const page = await ws.changes({ pathPrefix: '/home/user', summary: true }); //
|
|
|
273
366
|
`legacy_disk_layout`. `changes()` is served by the cell from the running workspace and needs the `files` tool.
|
|
274
367
|
`cell().changesAll()` follows the pages.
|
|
275
368
|
|
|
369
|
+
## File-first workspaces (0.9.0+)
|
|
370
|
+
|
|
371
|
+
A file-first workspace has no VM between commands. Its state is a versioned file tree under /home/user (tree
|
|
372
|
+
revisions 0, 1, 2, ...). Each command runs as an execution: a fresh VM on the latest revision, whose changed files
|
|
373
|
+
become the next revision. Nothing else survives an execution (processes, memory, files outside /home/user), so install
|
|
374
|
+
dependencies into /home/user (for example a virtualenv) and start servers within the command that uses them. It is
|
|
375
|
+
ready as soon as it is opened and is never suspended.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const ws = await cloud.workspaces.open({ key: 'customer-42/repo', template: 'python-node-browser', mode: 'file_first' });
|
|
379
|
+
const cell = ws.cell();
|
|
380
|
+
await cell.files.write('/home/user/app/main.py', 'print("hi")\n', { createParents: true });
|
|
381
|
+
|
|
382
|
+
const r = await ws.executions.run(['bash', '-lc', 'cd app && python3 main.py > out.txt && cat out.txt'], { timeoutMs: 600_000 });
|
|
383
|
+
r.state; // 'succeeded' (any exit code), 'failed' or 'lost' (nothing published; r.errorReason says why)
|
|
384
|
+
r.exitCode; // 0
|
|
385
|
+
r.stdoutText; // 'hi\n' (r.stdout holds the bytes)
|
|
386
|
+
r.changed; // [{ path: '/home/user/app/out.txt', change: 'added', type: 'file' }]
|
|
387
|
+
r.treeRevision; // 2: the revision the execution published (= baseRevision when it changed nothing)
|
|
388
|
+
ws.treeRevision; // 2
|
|
389
|
+
|
|
390
|
+
// Conditional writes: applied only if the tree is still at that revision.
|
|
391
|
+
await cell.files.write('/home/user/app/main.py', 'print("bye")\n', { ifTreeRevision: ws.treeRevision! });
|
|
392
|
+
// ... else TreeRevisionMismatchError (err.currentTreeRevision): read what changed and try again.
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
- **Executions are idempotent by id.** `executionId` defaults to a fresh `ex-<uuid>`; pass your own to make a call safe
|
|
396
|
+
to repeat across processes. Network failures and retryable 5xx answers (503 `no_execution_host`: no host has room;
|
|
397
|
+
the SDK waits `Retry-After`) are retried with the same id, at most `maxRetries` (5) times, and an answer that takes
|
|
398
|
+
longer than `attemptTimeoutMs` (300 s) is awaited again with the same id. The cell runs a command once per id: a
|
|
399
|
+
repeated call gets the recorded result (`r.replayed`). The SDK never retries with a new id: a `failed` or `lost`
|
|
400
|
+
result is returned, and running it again is your decision.
|
|
401
|
+
- `ws.executions.get(id, { waitMs })` reads an execution: `pending` while it runs, its result when it ended (kept 7
|
|
402
|
+
days). Use it after a call that stopped waiting (an aborted `signal`): an execution cannot be canceled.
|
|
403
|
+
- While an execution runs, other executions and file writes are refused with 409 `workspace_busy`
|
|
404
|
+
(`execution_in_progress`); the SDK waits them out within `transitionTimeoutMs` (120 s).
|
|
405
|
+
- Only paths under /home/user exist (`outside_tree_root` otherwise). Reads, writes, `search()` and `patch()` work as on
|
|
406
|
+
a processful workspace, on the latest revision.
|
|
407
|
+
- Calls a file-first workspace does not have fail with `NotSupportedForModeError` before any request: exec sessions
|
|
408
|
+
(`cell.exec.*`), PTY, processes, git, browser, `changes()`, `suspend`, `resume`, `snapshot`, `fork`, `reset` and
|
|
409
|
+
`saveAsTemplate`. On a processful workspace `executions` and `ifTreeRevision` are refused the same way.
|
|
410
|
+
- Reopening a key with another `mode` is 409 `mode_mismatch`; a deployment without file-first workspaces answers 422
|
|
411
|
+
`mode_not_available`; a legacy template 409 `layout_unsupported`.
|
|
412
|
+
|
|
276
413
|
## Templates: file tree, diff and dev mode
|
|
277
414
|
|
|
278
415
|
```ts
|
|
@@ -403,6 +540,18 @@ it.
|
|
|
403
540
|
Your agent loop and model calls stay in your application; the workspace is the computer the tools
|
|
404
541
|
act on.
|
|
405
542
|
|
|
543
|
+
The tools are `exec`, `read_file`, `write_file`, `list_files`, `search_files` and `edit_file` (0.9.0+), the process,
|
|
544
|
+
terminal, git and browser tools, filtered by the tools your key grants. `edit_file` replaces exact text; when the
|
|
545
|
+
model passes no `expected_revision` it reads the file's revision first, so a change made in between fails the edit
|
|
546
|
+
instead of being overwritten. Each call first sends `workspace.hint()` without waiting for it (`hint: false` turns
|
|
547
|
+
that off, e.g. when you send the hint yourself as the model starts a tool call), except `read_file`, `list_files` and
|
|
548
|
+
`search_files`: a sleeping workspace answers them from its disk without waking.
|
|
549
|
+
|
|
550
|
+
For a file-first workspace (0.9.0+) the tools are `exec` and the files tools only: `exec` runs each command as an
|
|
551
|
+
execution and adds `execution_id`, `state`, `tree_revision` and `changed` to its result, and the process, terminal,
|
|
552
|
+
git and browser tools are not offered. `workspaceTools(ws, { mode })` builds the definitions without touching the
|
|
553
|
+
workspace; `onExecution(id)` is called with each execution id before it is sent.
|
|
554
|
+
|
|
406
555
|
## Tool-call capture (0.7.0+)
|
|
407
556
|
|
|
408
557
|
Your harness's tools (web search, SQL, HTTP APIs, MCP servers) run in your application, so their results reach the
|
|
@@ -480,7 +629,7 @@ never injected).
|
|
|
480
629
|
|
|
481
630
|
**Read-your-writes.** Calls through the same client first wait for capture writes recorded before them (bounded by
|
|
482
631
|
`settleTimeoutMs`, 30 s; they never fail because of capture): `exec` and files calls through `workspace.cell()`,
|
|
483
|
-
`workspaceTools`, `snapshot`, `fork`, `suspend`, `saveAsTemplate` and `close` (on the workspace handle and on
|
|
632
|
+
`workspaceTools`, `snapshot`, `fork`, `suspend`, `suspendWhenIdle` (0.10.0+), `saveAsTemplate` and `close` (on the workspace handle and on
|
|
484
633
|
`cloud.workspaces.*(id)`). A lifecycle call's timing shows the wait as a `capture_flush` phase. `delete` and `reset`
|
|
485
634
|
drop pending writes. A write to a suspended workspace wakes it (`wake: null` opts out).
|
|
486
635
|
|
|
@@ -542,6 +691,78 @@ await workspace.cell().exec.run(['python3', 'agent.py']); // sees $OPEN
|
|
|
542
691
|
`accessEvents`, `createOrganization` and `listOrganization`. Organization-wide secrets and access
|
|
543
692
|
logs belong to organization owners and admins, so a project API key gets 403 for those.
|
|
544
693
|
|
|
694
|
+
## Account (ShardfluxAccount) (0.9.0+)
|
|
695
|
+
|
|
696
|
+
`ShardfluxAccount` does what a person does in the web app, with a user session instead of an API key: sign up, sign
|
|
697
|
+
in (with MFA), organizations, projects, API keys, members, invitations, billing, audit, data export and account
|
|
698
|
+
deletion. Two steps stay human: opening the verification email, and paying in Stripe Checkout.
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
import { Shardflux, ShardfluxAccount } from '@shardflux/sdk';
|
|
702
|
+
|
|
703
|
+
// Before a session exists (static; no token needed). Emailed links can be passed whole.
|
|
704
|
+
await ShardfluxAccount.register({ email, password, displayName: 'Ada' }); // { status: 'accepted' }
|
|
705
|
+
await ShardfluxAccount.verifyEmail('https://app.shardflux.dev/auth/verify-email#token=...');
|
|
706
|
+
|
|
707
|
+
const { account, result } = await ShardfluxAccount.login({ email, password, onSessionToken: (s) => save(s.token) });
|
|
708
|
+
if (result.status === 'mfa_required') await account.auth.completeMfa({ code: '123456' }); // or { recoveryCode }
|
|
709
|
+
|
|
710
|
+
// Later, with the saved token (sfu_<43 characters>; a malformed one throws).
|
|
711
|
+
const again = new ShardfluxAccount({ sessionToken: saved, onSessionToken: (s) => save(s.token) });
|
|
712
|
+
const org = await again.organizations.create({ name: 'Acme' });
|
|
713
|
+
const project = await again.projects.create(org.id, { name: 'Default' });
|
|
714
|
+
const { secret } = await again.apiKeys.create(project.id, { name: 'agent', toolPermissions: ['exec', 'files'] });
|
|
715
|
+
const cloud = new Shardflux({ apiKey: secret }); // the sfk_ key, shown once
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
- The session token is sent as `Authorization: Bearer sfu_...` on `/v1`. Login completion, `auth.stepUp()`,
|
|
719
|
+
`auth.changePassword()`, `auth.totp.confirm()` and `auth.totp.disable()` rotate it (the old token stops working):
|
|
720
|
+
`account.sessionToken` always holds the current token, every later call uses it, and `onSessionToken({ token,
|
|
721
|
+
expiresAt })` is called (and awaited) with each new one. Save it there. Sessions idle out after 30 days.
|
|
722
|
+
- Sensitive calls (exports, deletions, TOTP changes, …) answer 403 `step_up_required` without a recent password
|
|
723
|
+
check: `await account.auth.stepUp({ password, code })`, then retry. A session still waiting for its second factor
|
|
724
|
+
gets 403 `mfa_required`; an unverified email 403 `email_unverified`.
|
|
725
|
+
- Namespaces: `auth` (session, MFA, logout, sessions, step-up, password and email change, `totp`), `organizations`
|
|
726
|
+
(`list`, `listAll`, `create`, `get`, `entitlements`, `deletion`, `delete`, `exports`, `workspaces`), `projects`,
|
|
727
|
+
`apiKeys` (`create` sends an Idempotency-Key, so a retry never makes a second key; `toolPermissions` defaults to
|
|
728
|
+
`[]`), `members`, `invitations` (`accept(linkOrToken)`), `billing`, `user` (account `deletion`, `scheduleDeletion`,
|
|
729
|
+
`cancelDeletion`, `exports`), `templates` (plus `publishVersion` and `archiveVersion`), `audit` (plus `export(orgId,
|
|
730
|
+
{ format: 'csv' | 'ndjson', ...filters })`, the text), and `usage`, `secrets`, `egress`, `volumes` with explicit
|
|
731
|
+
organization and project ids; `me()` and `request()`. List methods return the API's page `{ data, next_cursor }`;
|
|
732
|
+
`listAll` iterates every page.
|
|
733
|
+
- `parseEmailToken(input)` returns the `token` of a link's `#token=` fragment or `?token=` query (else the trimmed
|
|
734
|
+
input), and throws for a link without one.
|
|
735
|
+
|
|
736
|
+
Upgrading a plan: a person pays at the Checkout `url`; the code waits for the subscription.
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
const checkout = await account.billing.checkout(org.id, { planKey: 'pro' }); // 409 subscription_exists: use billing.portal()
|
|
740
|
+
console.log(`Pay here: ${checkout.url}`);
|
|
741
|
+
const done = await account.billing.waitForCheckout(org.id, checkout.id, { timeoutMs: 15 * 60_000 });
|
|
742
|
+
if (!done.subscription_active) console.log(`checkout ${done.status}`); // expired, canceled or failed
|
|
743
|
+
// After timeoutMs: CheckoutTimeoutError (err.checkout is the last status); on abort: the signal's reason.
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Opt-in overage and its spend cap (owners and billing members): read the policy, then change it. Every field is
|
|
747
|
+
optional (give at least one); `ifMatch` (the `version` you read) makes a concurrent change a 409 `version_mismatch`
|
|
748
|
+
instead of overwriting it.
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
const policy = await account.billing.spendPolicy(org.id);
|
|
752
|
+
// overage_state: unavailable | off | on | paused; the cap range: spend_cap_min_minor..spend_cap_max_minor (the plan price)
|
|
753
|
+
if (policy.overage_available) {
|
|
754
|
+
await account.billing.setSpendPolicy(org.id, { overageEnabled: true, spendCapMinor: 900, ifMatch: policy.version }); // $9.00
|
|
755
|
+
}
|
|
756
|
+
await account.billing.setSpendPolicy(org.id, { overageEnabled: false }); // always allowed
|
|
757
|
+
await account.billing.setSpendPolicy(org.id, { alertThresholdsPercent: [50, 80, 100] });
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
A refused change is a `ShardfluxApiError` 422 `validation_failed` with `reason` `overage_unavailable`,
|
|
761
|
+
`spend_cap_required`, `spend_cap_below_minimum` (`details.min_minor`), `spend_cap_above_plan_price`
|
|
762
|
+
(`details.max_minor`) or `spend_cap_below_charges` (`details.charges_minor`: the cap cannot go below what overage
|
|
763
|
+
already charged this period). Every owner and billing member gets an email when overage is turned on or off or the cap
|
|
764
|
+
changes.
|
|
765
|
+
|
|
545
766
|
## Errors
|
|
546
767
|
|
|
547
768
|
- `ShardfluxApiError`: the API or the workspace refused the request. Fields: `status`, `code`,
|
|
@@ -554,17 +775,127 @@ await workspace.cell().exec.run(['python3', 'agent.py']); // sees $OPEN
|
|
|
554
775
|
- `OperationTimeoutError`: waiting gave up; the operation continues (`operationId`, `lastState`, `lastReason`,
|
|
555
776
|
`deadlineAt` **(0.6.2+)** while it waits for capacity, `timing`).
|
|
556
777
|
- `ShardfluxProtocolError`: a response was not the documented shape.
|
|
778
|
+
- `NotSupportedForModeError` **(0.9.0+)**, a `ShardfluxApiError` (409 `conflict`, `reason` `not_supported_for_mode`):
|
|
779
|
+
the call does not exist for the workspace's `mode`; `local` is true when the SDK refused it without a request.
|
|
780
|
+
- `TreeRevisionMismatchError` **(0.9.0+)**, a `ShardfluxApiError` (409 `conflict`, `reason` `tree_revision_mismatch`):
|
|
781
|
+
an `ifTreeRevision` call found the tree at `currentTreeRevision`; nothing changed.
|
|
782
|
+
- `CheckoutTimeoutError` **(0.9.0+)**: `billing.waitForCheckout()` gave up; the checkout stays open (`checkout`,
|
|
783
|
+
`waitedMs`).
|
|
557
784
|
|
|
558
785
|
Treat unknown error codes and reasons as generic errors: show `message`, and use `retryable`.
|
|
559
786
|
|
|
787
|
+
A 402 `allowance_exhausted` (opens, resumes and forks refused while a CPU-hours or RAM GiB-hours allowance is used up)
|
|
788
|
+
carries `reason` **(0.9.0+)**:
|
|
789
|
+
|
|
790
|
+
| `reason` | Meaning | What to do |
|
|
791
|
+
| --- | --- | --- |
|
|
792
|
+
| `allowance_used` | The allowance is used up and overage is off or not on the plan. | Upgrade, or have an owner or billing member turn on overage under Usage & billing; or wait for `details.resets_at`. |
|
|
793
|
+
| `overage_paused` | Overage is on but paused while a plan payment is past due. | An owner or billing member updates the payment method. |
|
|
794
|
+
| `spend_cap_reached` | Overage charges reached the spend cap for this billing period. | Raise the cap (up to the plan price) or upgrade under Usage & billing; or wait for `details.resets_at`. |
|
|
795
|
+
|
|
796
|
+
`details.spend_cap` is `{ cap_minor, effective_cap_minor, charges_minor, currency }` (minor units, `null` when the plan
|
|
797
|
+
has no overage). An older API sends no `reason`. Do not retry these in a loop.
|
|
798
|
+
|
|
799
|
+
Retryable 429/502/503/504 refusals (for example 503 `host_capacity`, when the workspace's host has no room to restore
|
|
800
|
+
it right now, or `wake_failed`) are retried after `Retry-After` for reads, searches and calls that carry an
|
|
801
|
+
Idempotency-Key (writes and patches); other calls surface them with `retryable: true` and `retryAfterSeconds`.
|
|
802
|
+
A read of a sleeping workspace that its disk cannot answer (409 `workspace_not_running` with `reason`
|
|
803
|
+
`offline_unavailable` or `offline_budget`) wakes the workspace and is retried like any `workspace_not_running`; 503
|
|
804
|
+
`offline_changed` (the disk changed during the read) is retried and served by the running workspace. 409 `conflict`
|
|
805
|
+
`host_feature_unavailable` (the workspace's host predates the call, `details.feature`) is neither retried nor
|
|
806
|
+
woken: it lasts until the workspace runs on an upgraded host.
|
|
807
|
+
|
|
808
|
+
## Usage and overage
|
|
809
|
+
|
|
810
|
+
`cloud.usage` reads the organization's usage (API keys see organization totals and their own project's workspaces):
|
|
811
|
+
`summary(orgId)`, `series(orgId, params)`, `workspace(workspaceId, params)`, `estimate(orgId)`, `grants(orgId)`,
|
|
812
|
+
`spend(orgId)` and `spendPolicy(orgId)`.
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
const s = await cloud.usage.summary(orgId);
|
|
816
|
+
if (s.allowance_exhausted) console.log('starts are refused:', s.exhausted_reason); // allowance_used | overage_paused | spend_cap_reached
|
|
817
|
+
const cap = s.spend_cap; // opt-in overage this period (0.10.0+)
|
|
818
|
+
const usd = (minor: number) => `$${(minor / 100).toFixed(2)}`; // amounts are minor units of cap.currency
|
|
819
|
+
if (cap.state === 'accruing' || cap.state === 'warning') {
|
|
820
|
+
console.log(`overage ${usd(cap.charges_minor)} of ${usd(cap.effective_cap_minor)}; cap reached ${cap.projected_reached_at ?? 'not this period'}`);
|
|
821
|
+
}
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
Opt-in overage **(0.10.0+)**: while an owner or billing member has turned it on, workspaces keep opening and running
|
|
825
|
+
past the CPU-hours and RAM GiB-hours allowances (those allowances show `cap_state: 'overage'`), and the usage past them
|
|
826
|
+
is charged on the next invoice until the charges reach the spend cap.
|
|
827
|
+
|
|
828
|
+
- `summary()`, `spend()` and `estimate()` carry `spend_cap` (type `SpendCap`): `state` (`unavailable`, `off`,
|
|
829
|
+
`paused`, `within_allowance`, `accruing`, `warning`, `reached`), `cap_minor`, `effective_cap_minor`,
|
|
830
|
+
`charges_minor`, `remaining_minor`, `percent_of_cap`, `currency`, `resets_at`, `lines` (per allowance: `units_over`,
|
|
831
|
+
`billed_units`, `rate_minor`, `amount_minor`) and `projected_reached_at`. `summary()` and `spend()` also carry
|
|
832
|
+
`exhausted_reason`.
|
|
833
|
+
- `spendPolicy()` returns the settings: `overage_available`, `overage_enabled`, `overage_state` (`unavailable`, `off`,
|
|
834
|
+
`on`, `paused`), `spend_cap_minor`, `spend_cap_min_minor`, `spend_cap_max_minor`, `rates` and `currency`.
|
|
835
|
+
- An API key only reads them. Owners and billing members turn overage on or off and change the cap in the console,
|
|
836
|
+
or with a user session: `ShardfluxAccount.billing.setSpendPolicy()` (see below).
|
|
837
|
+
|
|
838
|
+
## Feedback (0.9.0+)
|
|
839
|
+
|
|
840
|
+
`cloud.sendFeedback()` sends a message straight to the Shardflux founder, who reads every one. If you or your coding
|
|
841
|
+
agent hit something while building with Shardflux, send it the moment it happens: a call that failed unexpectedly, an
|
|
842
|
+
error or doc that was confusing, something missing or slow, a workaround you needed. Short and specific beats polished;
|
|
843
|
+
the request id and error code let the founder find the logs.
|
|
844
|
+
|
|
845
|
+
```ts
|
|
846
|
+
try {
|
|
847
|
+
await cloud.workspaces.open({ key: 'acme/demo', template: 'python-node-browser' });
|
|
848
|
+
} catch (err) {
|
|
849
|
+
if (err instanceof ShardfluxApiError) {
|
|
850
|
+
await cloud.sendFeedback({
|
|
851
|
+
message: 'open failed with capacity_unavailable twice in 10 minutes; expected a start within a minute',
|
|
852
|
+
category: 'bug',
|
|
853
|
+
context: { requestId: err.requestId, errorCode: err.code, workspace: 'acme/demo', agent: 'claude-code' },
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
throw err;
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
- `message`: 1-8000 characters. `category`: `bug` (something failed or behaved wrongly), `confusing` (an error, doc,
|
|
861
|
+
name or output was unclear), `missing` (a capability, option or template you needed), `idea`, `praise` or `other`
|
|
862
|
+
(the default).
|
|
863
|
+
- `context` (all optional): `agent` (who is reporting, e.g. `claude-code`), `workspace`, `requestId`, `errorCode`,
|
|
864
|
+
`command` (the call that led to it), and `client`, which defaults to `shardflux-sdk-ts/<version>`.
|
|
865
|
+
- Returns `{ id, receivedAt, duplicate }`. The same message from the same key within 24 hours returns the original
|
|
866
|
+
with `duplicate: true` and sends no second email.
|
|
867
|
+
- Any API key may send feedback. Signed in with a CLI session instead, `account.sendFeedback({ message, category?,
|
|
868
|
+
context?, organizationId? })` sends it as the user (`ShardfluxAccount`; `organizationId`: one of yours). It is rate
|
|
869
|
+
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.
|
|
870
|
+
- Anything shaped like an API key is redacted before the message is stored or emailed; still, leave secrets out.
|
|
871
|
+
|
|
560
872
|
## More of the API
|
|
561
873
|
|
|
562
874
|
The `Shardflux` object also has `templates` (including custom template builds and the template editor), `volumes`
|
|
563
875
|
(shared persistent storage attached to workspaces), `secrets` (see above), `egress` (outbound allowlists),
|
|
564
|
-
`usage`, `billing`, `me()`, `entitlements(orgId)` and `request(method, path)` for any `/v1`
|
|
876
|
+
`usage`, `billing`, `me()`, `entitlements(orgId)`, `sendFeedback()` and `request(method, path)` for any `/v1`
|
|
565
877
|
route. The package exports the OpenAPI-generated types as well (`paths`, `components`,
|
|
566
878
|
`WorkspaceView`, `Operation` and more).
|
|
567
879
|
|
|
880
|
+
## Version check (0.9.0+)
|
|
881
|
+
|
|
882
|
+
After the first successful API response of the process, the SDK asks `GET /v1/client-versions` in the background
|
|
883
|
+
(once per process, 3 s timeout, every error ignored; it never delays or fails a call). When this version is outdated
|
|
884
|
+
or no longer supported, it emits one warning:
|
|
885
|
+
|
|
886
|
+
```
|
|
887
|
+
(node:1234) [SHARDFLUX_UPDATE_AVAILABLE] ShardfluxUpdateWarning: @shardflux/sdk 0.9.0 is outdated: 0.10.0 is available. Update: npm install @shardflux/sdk@latest
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
- Turn it off with `versionCheck: false` (on `Shardflux` or `ShardfluxAccount`), `SHARDFLUX_NO_UPDATE_CHECK=1` (also
|
|
891
|
+
`true`, `yes`, `on`) or `NO_UPDATE_NOTIFIER=1`. Handle it with `process.on('warning', (w) => ...)`
|
|
892
|
+
(`w.name === 'ShardfluxUpdateWarning'`).
|
|
893
|
+
- A tool built on the SDK checks its own package instead: `versionCheck: { package: '@acme/tool', version: '1.2.3' }`.
|
|
894
|
+
- On demand: `await checkClientVersion()` returns `{ status, package, ecosystem, current, latest, minimumSupported,
|
|
895
|
+
upgradeCommand, releaseNotesUrl, message? }` with `status` `current`, `outdated`, `unsupported` or `unknown` (the
|
|
896
|
+
request failed, or the package is not listed yet). `compareVersions(a, b)` compares two `major.minor.patch` versions
|
|
897
|
+
(a pre-release sorts first; `null` when one does not parse).
|
|
898
|
+
|
|
568
899
|
## Compatibility
|
|
569
900
|
|
|
570
901
|
- The SDK follows the API's `/v1` contract. New fields, enum values and error codes can appear in
|