@shardflux/cli 0.6.2 → 0.7.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 CHANGED
@@ -38,6 +38,29 @@ New organizations can start on a trial: 60 days at Developer limits, no card. Th
38
38
  - An interrupted file-first `ws exec` says `(an execution runs to completion)`; `mode_not_available` drops "yet";
39
39
  `--open` no longer says "best effort"; `templates init --help` no longer names an internal document.
40
40
 
41
+ ## 0.7.0 (release candidate)
42
+
43
+ Completed exec output streams are drained before releasing their HTTP connections, with a bounded cleanup if a peer does not close.
44
+
45
+ Uses SDK 0.12.0 and its pooled transport. Adds --labels/--idle-policy to open, --labels to list, ws labels, ws idle, ws exec-start --stdin-open and ws exec-input --offset. Protocol-error JSON includes source. Resume recovers failed workspaces; completed deletion permits key reuse.
46
+
47
+ Requires the QM integration backend release for labels, failed recovery, key reuse and pipe stdin. No production deployment has occurred from this branch.
48
+
49
+ ### Instant suspend: durable storage
50
+
51
+ - `shard ws suspend --wait` still returns when the suspend succeeds (sealed on its host); a finished suspend or fork
52
+ now prints `durable: yes (<n> ms after the suspend)` or `durable: in progress ...` when the result says so.
53
+ - `shard ws suspend --durable`: waits until `result.durable` is true (implies `--wait`, shares `--timeout`). A copy
54
+ that cannot be made exits 6 with code `durability_lost` (operation id, `details.state` `lost`, `details.reason`); a
55
+ timeout exits 5 with `details.durable` false and `details.state` `pending`. `--durable` with `--when-idle` or
56
+ `--cancel-when-idle` is a usage error.
57
+ - `shard operations wait <id> --durable`, and a `durable` line in `operations get|wait` for suspends and forks.
58
+ - `--timing --json`: `server` gains `durable`, `suspend_path`, `durability` and `lost_suspend`.
59
+ - A resume (or a command's wake) whose result carries `lost_suspend` prints a note on stderr naming the restored
60
+ checkpoint and its time.
61
+ - A suspend-when-idle canceled `workspace_active` says the workspace was in use and keeps running.
62
+ - The `ws suspend` summary no longer says "durable checkpoint".
63
+
41
64
  ## 0.6.1
42
65
 
43
66
  Needs `@shardflux/sdk` 0.11.0 or later.
package/README.md CHANGED
@@ -387,9 +387,10 @@ shows the status; `shard version --check` asks now (`--json`: `{"cli", "sdk", "u
387
387
  | `files search <id\|key> <path> <pattern> [--regex] [-i\|--ignore-case] [--include G]... [--exclude G]... [--max N]` | Search file contents under a directory (or in one file; gitignore-style globs): `path:line:column: text` per match (0.5.0+). |
388
388
  | `files patch <id\|key> <path> (--old T --new T)... [--replace-all] \| --content-file F\|- [--expected-revision R] [--if-revision N]` | Replace exact text (each `--old` must occur once unless `--replace-all`), or the whole content, atomically; `R` is a revision from `files stat --revision` or `absent` (0.5.0+). `--if-revision` (0.5.0+): file-first workspaces only. |
389
389
  | `workspaces suspend\|resume <id\|key> [--wait] [--timeout T] [--timing]` | Lifecycle operation. Returns immediately unless you pass `--wait`. |
390
+ | `workspaces suspend <id\|key> --durable [--timeout T]` | (0.7.0+) Suspend and wait until the copy is in durable storage. See "Durable storage". |
390
391
  | `workspaces suspend <id\|key> --when-idle D` / `--cancel-when-idle` | (0.5.1+) Suspend once the workspace has been idle for `D` (0 to 1h; `0` = as soon as it is idle, 0.6.1+), or cancel that. See "Suspend when idle". |
391
392
  | `workspaces fork <id\|key> <new-key> [caps] [--wait]` | Fork into a new key. |
392
- | `workspaces delete <id\|key> --yes [--wait]` | Delete the workspace. Tool access ends at once. The key of a persistent workspace is never reused. |
393
+ | `workspaces delete <id\|key> --yes [--wait]` | Delete the workspace. Tool access ends at once. After deletion finishes, the key can open a new workspace ID. |
393
394
  | `workspaces close <id\|key> [--wait]` | End a session workspace now: it is deleted, and the key then opens a new workspace. A persistent workspace is refused (`not_session`). |
394
395
  | `workspaces reset <id\|key> --yes [--wait]` | Layered workspaces: wipe every change and restart on the template. The previous state stays restorable for 7 days. |
395
396
  | `workspaces save-as-template <id\|key> --template <slug> [--display-name N] [--description T] [--checkpoint ID] [--default-lifetime L] [--default-idle-timeout D] [--acknowledge PATH]... [--no-auto-publish] [--wait]` | Save a layered workspace as the next version of an organization template. `--wait` follows the build. |
@@ -792,11 +793,49 @@ printf %s "$NEW_KEY" | shard secrets rotate OPENAI_API_KEY
792
793
  | 130 | interrupted (Ctrl-C). Waits stop at once, and exec sends a cancel to the session. A file-first execution runs to completion, and the error names the `shard executions get` command for its result. |
793
794
  | exec, executions get | the command's own code: 124 if it timed out, 128+N if killed by signal N. Failures before the command ran use the codes above; use `--json` to tell them apart. |
794
795
 
796
+ ## Durable storage (0.7.0+)
797
+
798
+ `shard ws suspend --wait` returns as soon as the workspace is sealed on its host, typically in a few hundred ms, and
799
+ its RAM and CPU are released then. The copy lands in durable storage right after, typically within a second:
800
+
801
+ ```sh
802
+ shard ws suspend acme/demo --wait
803
+ # Suspending acme/demo (<id>): operation <op> suspend succeeded
804
+ # durable: in progress, typically within a second (wait with: shard operations wait <op> --durable)
805
+ shard ws suspend acme/demo --durable # returns once it is durable: "durable: yes (435 ms after the suspend)"
806
+ shard operations wait <op> --durable # the same for a suspend or fork you already started
807
+ ```
808
+
809
+ - `--durable` implies `--wait` and shares its `--timeout`.
810
+ - `--json` prints the operation as the API returns it: `result.durable` and `result.durability` (`state`
811
+ `pending` or `durable`, `local_commit_at`, `durable_at`, ...). `--timing` adds `durable`, `suspend_path` and
812
+ `durability` to `server`.
813
+ - `operations get|wait` show a `durable` line for suspends and forks. A fork of a running workspace reports its copy the
814
+ same way.
815
+ - The exit codes and errors of `--durable` are in the CLI reference on docs.shardflux.dev.
816
+
795
817
  ## HTTP connections
796
818
 
797
- By default every request uses a fresh connection (`Connection: close`); `SHARDFLUX_HTTP_KEEPALIVE=1` reuses
798
- connections.
819
+ `shard` 0.7.0 uses the SDK’s pooled HTTP/1.1 transport on Node 26. Custom fetch is preserved. `SHARDFLUX_HTTP_KEEPALIVE=0` forces connection closure for diagnosis; `=1` opts into native pooling.
799
820
 
800
821
  ## License
801
822
 
802
823
  Apache-2.0
824
+
825
+
826
+ ## Integration controls (0.7.0+)
827
+
828
+ ```sh
829
+ shard ws open qm/project-42 --template python-node-browser --labels '{"scope":"project-42"}' --idle-policy never
830
+ shard ws list --labels '{"scope":"project-42"}' --json
831
+ shard ws labels qm/project-42 --set '{"scope":"project-42","owner":"qm"}'
832
+ shard ws idle qm/project-42 --json
833
+ shard ws idle qm/project-42 --keepalive 60
834
+ shard ws idle qm/project-42 --policy adaptive
835
+ shard ws suspend qm/project-42 --when-idle 2s
836
+ shard ws exec-start qm/project-42 --stdin-open --session-id qm-worker-1 -- cat
837
+ shard ws exec-input qm/project-42 qm-worker-1 hello --offset 0
838
+ shard ws exec-input qm/project-42 qm-worker-1 --offset 5 --close
839
+ ```
840
+
841
+ Use the byte offset in each acknowledgement, including partial writes. `ws resume` can recover a failed workspace with its existing ID and disk. A completed delete frees the key; old IDs remain deleted. Protocol-error JSON includes `source`; HTTP 404 alone does not mean a workspace was deleted.
package/dist/args.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.6.2";
1
+ export declare const CLI_VERSION = "0.7.0";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;
package/dist/args.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import { parseArgs } from 'node:util';
13
13
  import { ACCOUNT_COMMANDS } from "./account-args.js";
14
14
  import { UsageError } from "./errors.js";
15
- export const CLI_VERSION = '0.6.2';
15
+ export const CLI_VERSION = '0.7.0';
16
16
  export const GLOBAL_OPTIONS = {
17
17
  'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
18
18
  json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
@@ -189,6 +189,8 @@ const KEY_COMMANDS = [
189
189
  positionals: [{ name: 'key', required: true }],
190
190
  options: {
191
191
  template: { type: 'string', value: '<slug>', description: 'Template slug (required), e.g. python-node-browser.' },
192
+ labels: { type: 'string', value: '<json>', description: 'Replace workspace labels with a JSON object of strings.' },
193
+ 'idle-policy': { type: 'string', value: '<policy>', description: 'adaptive, never or fixed:<seconds>.' },
192
194
  secret: {
193
195
  type: 'string',
194
196
  multiple: true,
@@ -228,6 +230,7 @@ const KEY_COMMANDS = [
228
230
  options: {
229
231
  state: { type: 'string', value: '<state>', description: 'Observed state filter (running, suspended, creating, ...).' },
230
232
  prefix: { type: 'string', value: '<key-prefix>', description: 'Only keys starting with this prefix.' },
233
+ labels: { type: 'string', value: '<json>', description: 'Exact label matches (JSON object; all pairs must match).' },
231
234
  ...LIFETIME_FILTER,
232
235
  'include-deleted': { type: 'boolean', description: 'Include deleted workspaces and ended sessions (tombstones).' },
233
236
  all: { type: 'boolean', description: 'Fetch every page.' },
@@ -235,6 +238,22 @@ const KEY_COMMANDS = [
235
238
  cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
236
239
  },
237
240
  },
241
+ { path: ['workspaces', 'labels'], summary: 'Read or replace workspace labels', positionals: [REF], options: {
242
+ set: { type: 'string', value: '<json>', description: 'Replace all labels; {} clears them.' },
243
+ } },
244
+ { path: ['workspaces', 'idle'], summary: 'Inspect idle status, set policy or declare a keepalive', positionals: [REF], options: {
245
+ policy: { type: 'string', value: '<policy>', description: 'adaptive, never, fixed:<seconds>, or default to clear the override.' },
246
+ keepalive: { type: 'string', value: '<seconds>', description: 'Declare ongoing work for this many seconds.' },
247
+ } },
248
+ { path: ['workspaces', 'exec-start'], summary: 'Start a background exec session with optional pipe stdin', positionals: [REF, { name: 'command', required: true, variadic: true }], options: {
249
+ 'stdin-open': { type: 'boolean', description: 'Keep pipe stdin open for exec-input.' },
250
+ 'session-id': { type: 'string', value: '<id>', description: 'Caller-chosen id to deduplicate starts.' },
251
+ cwd: { type: 'string', value: '<dir>', description: 'Absolute working directory.' },
252
+ } },
253
+ { path: ['workspaces', 'exec-input'], summary: 'Write pipe stdin at its acknowledged byte offset', positionals: [REF, { name: 'session-id', required: true }, { name: 'data' }], options: {
254
+ offset: { type: 'string', value: '<bytes>', description: 'Required acknowledged offset (0 initially).' },
255
+ close: { type: 'boolean', description: 'Send EOF after these bytes are accepted.' },
256
+ } },
238
257
  { path: ['workspaces', 'get'], summary: 'Show one workspace (state, template, startup)', positionals: [REF], options: {} },
239
258
  { path: ['workspaces', 'inputs'], summary: 'Show the text inputs of a workspace (secret inputs are bound secrets: shard ws secrets)', positionals: [REF], options: {} },
240
259
  {
@@ -269,15 +288,16 @@ const KEY_COMMANDS = [
269
288
  },
270
289
  {
271
290
  path: ['workspaces', 'suspend'],
272
- summary: 'Suspend a running workspace (durable checkpoint), now or once it has been idle',
273
- description: 'Without options the suspend is requested now. --when-idle (0.5.1+) defers it instead: the workspace is suspended once it has been idle for that long, counted from the later of its last work and the request, so call it when an agent turn ends. A command still running, an attached exec or terminal stream, or a keepalive postpones it; the next tool call (the next turn) or a resume cancels it. Repeating replaces the pending request. --cancel-when-idle cancels it.',
291
+ summary: 'Suspend a running workspace (memory and processes checkpointed), now or once it has been idle',
292
+ description: 'Without options the suspend is requested now. With --wait it returns as soon as the workspace is sealed on its host, typically in a few hundred ms; the result\'s durable turns true when the copy lands in durable storage, typically within a second. --durable (0.7.0+) waits for that too. --when-idle (0.5.1+) defers it instead: the workspace is suspended once it has been idle for that long, counted from the later of its last work and the request, so call it when an agent turn ends. A command still running, an attached exec or terminal stream, or a keepalive postpones it; the next tool call (the next turn) or a resume cancels it. Repeating replaces the pending request. --cancel-when-idle cancels it.',
274
293
  positionals: [REF],
275
294
  options: {
276
295
  'when-idle': { type: 'string', value: '<duration>', description: 'Suspend once the workspace has been idle this long (0.5.1+; 0 to 1h, e.g. 0, 30s, 2m; 0 = as soon as it is idle, 0.6.1+). Cannot be combined with --wait.' },
277
296
  'cancel-when-idle': { type: 'boolean', description: 'Cancel a pending --when-idle request (0.5.1+; idempotent).' },
297
+ durable: { type: 'boolean', description: 'Wait until the suspend is in durable storage (result durable: true; 0.7.0+). Implies --wait and shares its --timeout.' },
278
298
  ...WAIT,
279
299
  },
280
- examples: ['shard ws suspend acme/demo --wait', 'shard ws suspend acme/demo --when-idle 2m', 'shard ws suspend acme/demo --cancel-when-idle'],
300
+ examples: ['shard ws suspend acme/demo --wait', 'shard ws suspend acme/demo --durable', 'shard ws suspend acme/demo --when-idle 2m', 'shard ws suspend acme/demo --cancel-when-idle'],
281
301
  },
282
302
  { path: ['workspaces', 'resume'], summary: 'Resume a suspended workspace', positionals: [REF], options: { ...WAIT } },
283
303
  {
@@ -695,7 +715,11 @@ const KEY_COMMANDS = [
695
715
  path: ['operations', 'wait'],
696
716
  summary: 'Wait for an operation to finish (exit 0 succeeded, 6 failed/canceled, 5 timeout)',
697
717
  positionals: [{ name: 'operation-id', required: true }],
698
- options: { timeout: { type: 'string', value: '<duration>', description: 'Give up after this long (default 5m).' }, ...TIMING },
718
+ options: {
719
+ timeout: { type: 'string', value: '<duration>', description: 'Give up after this long (default 5m).' },
720
+ durable: { type: 'boolean', description: 'A suspend or fork: also wait until its copy is in durable storage (result durable: true; 0.7.0+).' },
721
+ ...TIMING,
722
+ },
699
723
  },
700
724
  ];
701
725
  /** Every command: the account plane first (sign in, set up), then the project-key commands. */
package/dist/commands.js CHANGED
@@ -16,7 +16,7 @@ import { updateCheckDisabled } from "./update-check.js";
16
16
  import { initTemplateYaml, recipeYaml } from "./template-yaml.js";
17
17
  import { BuildTimer, buildTimingJson, buildTimingText, progressLine } from "./timing.js";
18
18
  import { timingJson } from "./timing.js";
19
- import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
19
+ import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, durableText, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
20
20
  const DEFAULT_WAIT_MS = 300_000;
21
21
  const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
22
22
  const MAX_STDIN = 1024 * 1024;
@@ -496,7 +496,7 @@ function reportExecution(ctx, ref, r, opts) {
496
496
  }
497
497
  function lifecycleText(verb, w, op) {
498
498
  const finished = op.state === 'succeeded' || op.state === 'failed' || op.state === 'canceled';
499
- return `${verb} ${w.key} (${w.id}): operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? '' : `wait with: shard operations wait ${op.id}\n`}`;
499
+ return `${verb} ${w.key} (${w.id}): operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? durableText(op) : `wait with: shard operations wait ${op.id}\n`}`;
500
500
  }
501
501
  /** The project-key commands; handlers.ts adds the account plane's. */
502
502
  export const KEY_HANDLERS = {
@@ -565,6 +565,46 @@ export const KEY_HANDLERS = {
565
565
  out(ctx, usageText(summary), summary);
566
566
  return 0;
567
567
  },
568
+ async 'workspaces labels'(ctx, values, [ref]) {
569
+ const ws = await resolveWorkspace(ctx, ref);
570
+ const labels = labelMap(str(values.set));
571
+ if (labels !== undefined)
572
+ await ws.setLabels(labels);
573
+ out(ctx, JSON.stringify(ws.labels, null, 2) + '\n', ws.labels);
574
+ return 0;
575
+ },
576
+ async 'workspaces idle'(ctx, values, [ref]) {
577
+ const ws = await resolveWorkspace(ctx, ref);
578
+ const policy = str(values.policy);
579
+ const keepalive = str(values.keepalive);
580
+ if (policy !== undefined && keepalive !== undefined)
581
+ throw new UsageError('--policy and --keepalive cannot be combined');
582
+ let result;
583
+ if (policy !== undefined)
584
+ result = (await ws.setIdlePolicy(policy === 'default' ? null : policy)).data;
585
+ else if (keepalive !== undefined)
586
+ result = await ws.keepalive(parsePositiveInt(keepalive, 'keepalive', 86400));
587
+ else
588
+ result = await ws.idle(ctx.signal);
589
+ out(ctx, JSON.stringify(result, null, 2) + '\n', result);
590
+ return 0;
591
+ },
592
+ async 'workspaces exec-start'(ctx, values, [ref, ...argv]) {
593
+ const ws = await resolveWorkspace(ctx, ref);
594
+ const session = await cellOf(ctx, ws).exec.start({ argv, stdin_open: values['stdin-open'] === true,
595
+ ...(str(values['session-id']) ? { session_id: str(values['session-id']) } : {}), ...(str(values.cwd) ? { cwd: str(values.cwd) } : {}) }, ctx.signal);
596
+ out(ctx, JSON.stringify(session, null, 2) + '\n', session);
597
+ return 0;
598
+ },
599
+ async 'workspaces exec-input'(ctx, values, [ref, sessionId, data]) {
600
+ const offset = Number(str(values.offset));
601
+ if (str(values.offset) === undefined || !Number.isSafeInteger(offset) || offset < 0)
602
+ throw new UsageError('--offset must be a non-negative integer');
603
+ const ws = await resolveWorkspace(ctx, ref);
604
+ const ack = await cellOf(ctx, ws).exec.input(sessionId, data ?? '', { offset, close: values.close === true, signal: ctx.signal });
605
+ out(ctx, JSON.stringify(ack) + '\n', ack);
606
+ return 0;
607
+ },
568
608
  async 'workspaces open'(ctx, values, [key]) {
569
609
  const template = str(values.template);
570
610
  if (!template)
@@ -583,6 +623,8 @@ export const KEY_HANDLERS = {
583
623
  const ws = await waited(ctx, 'the open continues server side (open the key again to keep waiting).', () => cloud.workspaces.open({
584
624
  key: key,
585
625
  template,
626
+ ...(str(values.labels) !== undefined ? { labels: labelMap(str(values.labels)) } : {}),
627
+ ...(str(values['idle-policy']) ? { idlePolicy: str(values['idle-policy']) } : {}),
586
628
  ...(c ? { caps: c } : {}),
587
629
  ...(bind.length ? { secrets: bind } : {}),
588
630
  ...(inputs ? { inputs } : {}),
@@ -609,6 +651,7 @@ export const KEY_HANDLERS = {
609
651
  const params = {
610
652
  ...(state ? { state: state } : {}),
611
653
  ...(prefix ? { keyPrefix: prefix } : {}),
654
+ ...(str(values.labels) !== undefined ? { labels: labelMap(str(values.labels)) } : {}),
612
655
  ...(lifetime ? { lifetime } : {}),
613
656
  ...(purpose ? { purpose } : {}),
614
657
  ...(values['include-deleted'] === true ? { includeDeleted: true } : {}),
@@ -854,8 +897,12 @@ export const KEY_HANDLERS = {
854
897
  async 'workspaces suspend'(ctx, values, [ref]) {
855
898
  const whenIdle = str(values['when-idle']);
856
899
  const cancel = values['cancel-when-idle'] === true;
857
- if (whenIdle === undefined && !cancel)
858
- return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => ctx.cloud().workspaces.suspend(ws.id, o), ['suspend', 'Suspend']);
900
+ const durable = values.durable === true;
901
+ if (whenIdle === undefined && !cancel) {
902
+ return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => ctx.cloud().workspaces.suspend(ws.id, durable ? { ...o, durable: true, wait: o.wait ?? { timeoutMs: waitTimeout(values), signal: ctx.signal } } : o), ['suspend', 'Suspend']);
903
+ }
904
+ if (durable)
905
+ throw new UsageError(`${whenIdle !== undefined ? '--when-idle' : '--cancel-when-idle'} cannot be combined with --durable (nothing is suspended now)`);
859
906
  if (whenIdle !== undefined && cancel)
860
907
  throw new UsageError('--when-idle and --cancel-when-idle cannot be combined');
861
908
  const flag = cancel ? '--cancel-when-idle' : '--when-idle';
@@ -1526,7 +1573,10 @@ export const KEY_HANDLERS = {
1526
1573
  async 'operations wait'(ctx, values, [id]) {
1527
1574
  if (!UUID.test(id))
1528
1575
  throw new UsageError(`<operation-id> must be a UUID (got "${id}")`);
1529
- const op = await waitOperation(ctx, id, waitTimeout(values));
1576
+ const timeoutMs = waitTimeout(values);
1577
+ const op = values.durable === true
1578
+ ? await waited(ctx, `operation ${id} continues server side.`, () => ctx.cloud().workspaces.waitForDurable(id, { timeoutMs, signal: ctx.signal }))
1579
+ : await waitOperation(ctx, id, timeoutMs);
1530
1580
  out(ctx, operationDetail(op), op);
1531
1581
  return 0;
1532
1582
  },
@@ -1681,3 +1731,17 @@ async function lifecycle(ctx, values, ref, verb, start, processfulOnly) {
1681
1731
  out(ctx, lifecycleText(verb, ws, op), op);
1682
1732
  return 0;
1683
1733
  }
1734
+ function labelMap(value) {
1735
+ if (value === undefined)
1736
+ return undefined;
1737
+ let parsed;
1738
+ try {
1739
+ parsed = JSON.parse(value);
1740
+ }
1741
+ catch {
1742
+ throw new UsageError('labels must be a JSON object of strings');
1743
+ }
1744
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || Object.values(parsed).some((v) => typeof v !== 'string'))
1745
+ throw new UsageError('labels must be a JSON object of strings');
1746
+ return parsed;
1747
+ }
package/dist/errors.d.ts CHANGED
@@ -84,7 +84,7 @@ export interface ErrorInfo {
84
84
  retryable?: boolean;
85
85
  operation_id?: string;
86
86
  details?: Record<string, unknown>;
87
- source?: 'api' | 'cell';
87
+ source?: 'api' | 'cell' | 'unknown';
88
88
  last_state?: string;
89
89
  operation?: unknown;
90
90
  /** A file-first execution the error is about (failed, lost, still running, or left running by Ctrl-C). */
package/dist/errors.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * stderr with the API's error code, message and request id; the process exit
4
4
  * code tells scripts what kind of failure it was (README "Exit codes").
5
5
  */
6
- import { CheckoutTimeoutError, ExecStartError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
6
+ import { CheckoutTimeoutError, DurabilityLostError, ExecStartError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
7
7
  export const EXIT = {
8
8
  ok: 0,
9
9
  /** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
@@ -137,15 +137,22 @@ export function describeError(err) {
137
137
  source: err.source,
138
138
  };
139
139
  }
140
- if (err instanceof OperationTimeoutError)
141
- return { code: 'timeout', message: err.message, operation_id: err.operationId, last_state: err.lastState };
140
+ if (err instanceof OperationTimeoutError) {
141
+ return { code: 'timeout', message: err.message, operation_id: err.operationId, last_state: err.lastState, ...(err.durable ? { details: { durable: false, state: 'pending' } } : {}) };
142
+ }
143
+ // --durable (0.7.0+): the suspend succeeded; its durable copy could not be made (see the lifecycle reference).
144
+ if (err instanceof DurabilityLostError) {
145
+ return { code: 'durability_lost', message: err.message, operation_id: err.operationId, retryable: false, details: { state: 'lost', ...(err.durability.reason ? { reason: err.durability.reason } : {}), checkpoint_id: err.durability.checkpointId }, operation: err.operation };
146
+ }
142
147
  if (err instanceof OperationFailedError) {
143
148
  // retryable: the operation error's own flag (true for capacity_unavailable: nothing was started, retry later).
144
149
  // details.reason: the operation error's reason (e.g. the scheduler's no_ready_hosts), printed after the message.
145
150
  const opDetails = err.operation.error?.details;
146
151
  const reason = typeof opDetails === 'object' && opDetails !== null ? opDetails.reason : undefined;
147
152
  const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}) };
148
- return { code: 'operation_failed', message: err.message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
153
+ // A suspend-when-idle canceled workspace_active (0.7.0+): the workspace was in use and keeps running.
154
+ const message = err.workspaceActive ? `${err.message}: the workspace was in use, so the suspend-when-idle was canceled; it keeps running` : err.message;
155
+ return { code: 'operation_failed', message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
149
156
  }
150
157
  // File-first executions (0.5.0): failed or lost like a failed operation (details.reason says why;
151
158
  // retryable: a new execution id may succeed), still running like a wait that gave up.
@@ -180,7 +187,7 @@ export function describeError(err) {
180
187
  return { code: 'interrupted', message: err.message, ...(err.operationId ? { operation_id: err.operationId } : {}), ...(err.executionId ? { execution_id: err.executionId } : {}) };
181
188
  }
182
189
  if (err instanceof ShardfluxProtocolError)
183
- return { code: 'protocol_error', message: err.message, status: err.status };
190
+ return { code: 'protocol_error', message: err.message, status: err.status, source: err.source };
184
191
  if (isNamed(err, 'TimeoutError'))
185
192
  return { code: 'timeout', message: 'The request timed out.' };
186
193
  if (isNamed(err, 'AbortError'))
@@ -205,7 +212,7 @@ export function exitCodeFor(err, interrupted = false) {
205
212
  return EXIT.notFound;
206
213
  if (err instanceof OperationTimeoutError || err instanceof CheckoutTimeoutError || isNamed(err, 'TimeoutError'))
207
214
  return EXIT.timeout;
208
- if (err instanceof OperationFailedError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
215
+ if (err instanceof OperationFailedError || err instanceof DurabilityLostError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
209
216
  return EXIT.operationFailed;
210
217
  if (err instanceof ExecutionPendingError)
211
218
  return EXIT.timeout;
package/dist/format.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * Human-readable output (aligned tables, key/value blocks) and the error line.
3
- * `--json` output is the API's own objects (see README "JSON output"), so
4
- * these functions only shape what people read.
5
- */
6
1
  import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, WorkspaceChange, WorkspaceSecretBindings, WorkspaceView } from '@shardflux/sdk';
7
2
  import type { ErrorInfo } from './errors.js';
8
3
  export declare function json(value: unknown): string;
@@ -41,6 +36,11 @@ export declare function suspendWhenIdleText(w: {
41
36
  export declare function workspaceDetail(w: WorkspaceView): string;
42
37
  export declare const OPERATION_HEADERS: readonly ["ID", "KIND", "STATE", "REASON", "CREATED", "COMPLETED"];
43
38
  export declare function operationTable(ops: readonly Operation[]): string;
39
+ /**
40
+ * A finished suspend or fork (0.7.0+): whether its copy is in durable storage, when the result says (`durable`,
41
+ * `durability`). Empty when the result has no durability block (stored durably before the operation completed).
42
+ */
43
+ export declare function durableText(op: Operation): string;
44
44
  export declare function operationDetail(op: Operation): string;
45
45
  export declare function sessionTable(sessions: readonly AgentSession[]): string;
46
46
  /** `files stat`: one path's metadata; the revision only when requested and returned. */
package/dist/format.js CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Human-readable output (aligned tables, key/value blocks) and the error line.
3
+ * `--json` output is the API's own objects (see README "JSON output"), so
4
+ * these functions only shape what people read.
5
+ */
6
+ import { durabilityOf, isDurable } from '@shardflux/sdk';
1
7
  export function json(value) {
2
8
  return `${JSON.stringify(value, null, 2)}\n`;
3
9
  }
@@ -161,6 +167,28 @@ export const OPERATION_HEADERS = ['ID', 'KIND', 'STATE', 'REASON', 'CREATED', 'C
161
167
  export function operationTable(ops) {
162
168
  return table(OPERATION_HEADERS, ops.map((o) => [o.id, o.kind, o.state, o.state_reason ?? '-', time(o.created_at), time(o.completed_at)]));
163
169
  }
170
+ /**
171
+ * A finished suspend or fork (0.7.0+): whether its copy is in durable storage, when the result says (`durable`,
172
+ * `durability`). Empty when the result has no durability block (stored durably before the operation completed).
173
+ */
174
+ export function durableText(op) {
175
+ const d = durabilityOf(op);
176
+ if (op.state !== 'succeeded' || !d)
177
+ return '';
178
+ if (d.state === 'durable')
179
+ return `durable: yes${d.localCommitToDurableMs !== null ? ` (${d.localCommitToDurableMs} ms after the suspend)` : ''}\n`;
180
+ if (d.state === 'pending')
181
+ return `durable: in progress, typically within a second (wait with: shard operations wait ${op.id} --durable)\n`;
182
+ return `durable: no (${d.reason ?? 'lost'}); see docs.shardflux.dev/concepts/lifecycle\n`;
183
+ }
184
+ /** `durable` of a finished suspend or fork for `operations get`/`wait`: yes, in progress, no, or '-' for other kinds. */
185
+ function durableField(op) {
186
+ const d = durabilityOf(op);
187
+ if (d)
188
+ return d.state === 'durable' ? 'yes' : d.state === 'pending' ? 'in progress' : `no (${d.reason ?? 'lost'})`;
189
+ const v = isDurable(op);
190
+ return v === null ? '-' : v ? 'yes' : 'no';
191
+ }
164
192
  export function operationDetail(op) {
165
193
  const err = op.error;
166
194
  return kv([
@@ -168,6 +196,7 @@ export function operationDetail(op) {
168
196
  ['kind', op.kind],
169
197
  ['state', op.state],
170
198
  ['reason', op.state_reason ?? '-'],
199
+ ...(op.kind === 'suspend' || op.kind === 'fork' ? [['durable', durableField(op)]] : []),
171
200
  ['workspace', op.workspace_id ?? '-'],
172
201
  ['created', time(op.created_at)],
173
202
  ['updated', time(op.updated_at)],
package/dist/http.d.ts CHANGED
@@ -1,11 +1,2 @@
1
- /**
2
- * The fetch given to the SDK: the runtime's fetch (API and cell gateway). By
3
- * default every request uses a fresh connection (`Connection: close`);
4
- * SHARDFLUX_HTTP_KEEPALIVE=1 reuses connections.
5
- *
6
- * `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
7
- * held by the server for up to 20 s). The SDK passes the caller's signal to its
8
- * held polls itself, but not to the held open (`workspaces.open()` with `wait`
9
- * sends its POST without one), so Ctrl-C would otherwise wait the hold out.
10
- */
1
+ /** Share the SDK pool and attach Ctrl-C to held API requests. */
11
2
  export declare function makeFetch(env: Record<string, string | undefined>, base?: typeof fetch, signal?: AbortSignal): typeof fetch;
package/dist/http.js CHANGED
@@ -1,24 +1,12 @@
1
- /**
2
- * The fetch given to the SDK: the runtime's fetch (API and cell gateway). By
3
- * default every request uses a fresh connection (`Connection: close`);
4
- * SHARDFLUX_HTTP_KEEPALIVE=1 reuses connections.
5
- *
6
- * `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
7
- * held by the server for up to 20 s). The SDK passes the caller's signal to its
8
- * held polls itself, but not to the held open (`workspaces.open()` with `wait`
9
- * sends its POST without one), so Ctrl-C would otherwise wait the hold out.
10
- */
11
- export function makeFetch(env, base = fetch, signal) {
12
- // Why a fresh connection by default (Node 26 keep-alive measurements): docs/progress/startup-latency.md.
13
- const keepAlive = env.SHARDFLUX_HTTP_KEEPALIVE === '1';
1
+ import { defaultFetch } from '@shardflux/sdk';
2
+ /** Share the SDK pool and attach Ctrl-C to held API requests. */
3
+ export function makeFetch(env, base = defaultFetch(env), signal) {
14
4
  return (input, init) => {
15
5
  const headers = new Headers(init?.headers);
16
6
  const held = signal !== undefined && /\bwait\s*=/i.test(headers.get('prefer') ?? '');
17
- if (keepAlive && !held)
7
+ if (!held)
18
8
  return base(input, init);
19
- if (!keepAlive)
20
- headers.set('connection', 'close');
21
- const s = held && signal ? (init?.signal ? AbortSignal.any([init.signal, signal]) : signal) : init?.signal;
22
- return base(input, { ...init, headers, ...(s ? { signal: s } : {}) });
9
+ const s = init?.signal && signal ? AbortSignal.any([init.signal, signal]) : signal;
10
+ return base(input, { ...init, ...(s ? { signal: s } : {}) });
23
11
  };
24
12
  }
package/dist/index.d.ts CHANGED
@@ -22,4 +22,4 @@ export { openUrl, openerFor } from './browser.js';
22
22
  export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from './errors.js';
23
23
  export type { ErrorInfo } from './errors.js';
24
24
  export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from './format.js';
25
- export { TimingRecorder, coldBootNotice, timingJson, timingText } from './timing.js';
25
+ export { TimingRecorder, coldBootNotice, lostSuspendNotice, notices, timingJson, timingText } from './timing.js';
package/dist/index.js CHANGED
@@ -14,4 +14,4 @@ export { CHECK_INTERVAL_MS, CLI_DISTRIBUTION, NOTICE_WAIT_MS, UPDATE_CHECK_FILE,
14
14
  export { openUrl, openerFor } from "./browser.js";
15
15
  export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from "./errors.js";
16
16
  export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from "./format.js";
17
- export { TimingRecorder, coldBootNotice, timingJson, timingText } from "./timing.js";
17
+ export { TimingRecorder, coldBootNotice, lostSuspendNotice, notices, timingJson, timingText } from "./timing.js";
package/dist/main.js CHANGED
@@ -19,7 +19,7 @@ import { errorText, feedbackCommand, json, time } from "./format.js";
19
19
  import { HANDLERS } from "./handlers.js";
20
20
  import { makeFetch } from "./http.js";
21
21
  import { Input } from "./input.js";
22
- import { TimingRecorder, coldBootNotice, timingJson, timingText } from "./timing.js";
22
+ import { TimingRecorder, notices, timingJson, timingText } from "./timing.js";
23
23
  import { CLI_DISTRIBUTION, NOTICE_WAIT_MS, UpdateCheck, noticeOf, updateCheckDisabled, updateView } from "./update-check.js";
24
24
  const KEY_SHAPE = /^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/;
25
25
  // Printable (no C0 controls or DEL), as the API's AgentLabel pattern.
@@ -327,7 +327,7 @@ export async function run(argv, io, opts = {}) {
327
327
  const feedback = nudges(info, command) ? feedbackCommand(info) : undefined;
328
328
  const text = wantsJson
329
329
  ? json({ error: info, ...(showTiming ? { timing: timingJson(timing) } : {}), ...(feedback ? { feedback } : {}) })
330
- : `${timing ? timingText(timing) : ''}${traces.coldBoot ? coldBootNotice(traces.coldBoot) : ''}${errorText(info, hint, feedback)}`;
330
+ : `${timing ? timingText(timing) : ''}${notices(traces)}${errorText(info, hint, feedback)}`;
331
331
  io.stderr.write(redact(text, ...secrets));
332
332
  return code;
333
333
  };
@@ -359,7 +359,7 @@ export async function run(argv, io, opts = {}) {
359
359
  const wakeTimeoutMs = resolveWakeTimeout(parsed.global.wakeTimeout, io.env.SHARDFLUX_WAKE_TIMEOUT_MS);
360
360
  showTiming = parsed.values.timing === true;
361
361
  const userAgent = `shard-cli/${CLI_VERSION} shardflux-sdk-ts/${SDK_VERSION}${distribution.package === CLI_DISTRIBUTION.package ? '' : ` ${distribution.package}/${distribution.version}`}`;
362
- const base = makeFetch(io.env, fetch, signal);
362
+ const base = makeFetch(io.env, undefined, signal);
363
363
  // Notes which credential a 401 unauthenticated answered (the hint, and removing a dead saved session).
364
364
  const watchedFetch = async (input, init) => {
365
365
  const res = await base(input, init);
@@ -560,9 +560,11 @@ export async function run(argv, io, opts = {}) {
560
560
  // With --json the handler put the timing into its output; otherwise it follows the output, on stderr.
561
561
  if (showTiming && !ctx.json && traces.last)
562
562
  io.stderr.write(timingText(traces.last));
563
- // A resume that booted the saved disk (0.6.0+): said on stderr with or without --timing, also with --json.
564
- if (traces.coldBoot)
565
- io.stderr.write(coldBootNotice(traces.coldBoot));
563
+ // A resume that booted the saved disk (0.6.0+), or restored an earlier checkpoint (0.7.0+): said on stderr with or
564
+ // without --timing, also with --json.
565
+ const notice = notices(traces);
566
+ if (notice)
567
+ io.stderr.write(notice);
566
568
  code = outcome.code;
567
569
  }
568
570
  // The update notice: after the output, never after Ctrl-C or a --json failure (stderr then holds only the error).
package/dist/timing.d.ts CHANGED
@@ -10,6 +10,11 @@ export declare class TimingRecorder {
10
10
  * the wake of a command that found the workspace suspended. `coldBootNotice()` says it.
11
11
  */
12
12
  coldBoot: LifecycleTiming | null;
13
+ /**
14
+ * (0.7.0+) The last traced resume whose result names a suspend it could not restore (`lost_suspend`): a `ws resume
15
+ * --wait`, an `operations wait` of that resume, or a command's wake. `lostSuspendNotice()` says it.
16
+ */
17
+ lostSuspend: LifecycleTiming | null;
13
18
  readonly listener: ProgressListener;
14
19
  /**
15
20
  * The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
@@ -22,6 +27,13 @@ export declare class TimingRecorder {
22
27
  * `--timing`: the workspace runs and its files are as of the suspend, but running processes were restarted.
23
28
  */
24
29
  export declare function coldBootNotice(t: LifecycleTiming): string;
30
+ /**
31
+ * The one-line stderr notice after a command whose resume restored an earlier checkpoint because the latest suspend
32
+ * could not be kept (0.7.0+; `lost_suspend`): the state the workspace runs with is as of `state_as_of`.
33
+ */
34
+ export declare function lostSuspendNotice(t: LifecycleTiming): string;
35
+ /** The notices a command's traced calls leave on stderr (cold boot, lost suspend), in that order. */
36
+ export declare function notices(r: TimingRecorder): string;
25
37
  /** The human form (stderr): formatTiming() of the SDK, one block. */
26
38
  export declare function timingText(t: LifecycleTiming): string;
27
39
  /** The `--json` form: the SDK's LifecycleTiming with the CLI's snake_case keys (the rest of its JSON is the API's). */
package/dist/timing.js CHANGED
@@ -17,6 +17,11 @@ export class TimingRecorder {
17
17
  * the wake of a command that found the workspace suspended. `coldBootNotice()` says it.
18
18
  */
19
19
  coldBoot = null;
20
+ /**
21
+ * (0.7.0+) The last traced resume whose result names a suspend it could not restore (`lost_suspend`): a `ws resume
22
+ * --wait`, an `operations wait` of that resume, or a command's wake. `lostSuspendNotice()` says it.
23
+ */
24
+ lostSuspend = null;
20
25
  listener = (e) => {
21
26
  if (e.action === 'token' || e.action === 'tool')
22
27
  return;
@@ -27,6 +32,8 @@ export class TimingRecorder {
27
32
  this.last = e.timing;
28
33
  if (restartedProcesses(e.timing))
29
34
  this.coldBoot = e.timing;
35
+ if (e.timing.server?.lostSuspend)
36
+ this.lostSuspend = e.timing;
30
37
  };
31
38
  /**
32
39
  * The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
@@ -49,6 +56,20 @@ export function coldBootNotice(t) {
49
56
  const reason = t.server?.coldBootReason;
50
57
  return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from its saved disk; running processes were restarted${reason ? ` (${reason})` : ''}\n`;
51
58
  }
59
+ /**
60
+ * The one-line stderr notice after a command whose resume restored an earlier checkpoint because the latest suspend
61
+ * could not be kept (0.7.0+; `lost_suspend`): the state the workspace runs with is as of `state_as_of`.
62
+ */
63
+ export function lostSuspendNotice(t) {
64
+ const l = t.server?.lostSuspend;
65
+ if (!l)
66
+ return '';
67
+ return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from checkpoint ${l.restoredCheckpointId ?? '-'}${l.stateAsOf ? ` (state as of ${l.stateAsOf})` : ''}; the suspend of ${l.suspendedAt ?? l.checkpointId} was not kept${l.reason ? ` (${l.reason})` : ''}\n`;
68
+ }
69
+ /** The notices a command's traced calls leave on stderr (cold boot, lost suspend), in that order. */
70
+ export function notices(r) {
71
+ return `${r.coldBoot ? coldBootNotice(r.coldBoot) : ''}${r.lostSuspend ? lostSuspendNotice(r.lostSuspend) : ''}`;
72
+ }
52
73
  /** The human form (stderr): formatTiming() of the SDK, one block. */
53
74
  export function timingText(t) {
54
75
  return `${formatTiming(t)}\n`;
@@ -79,6 +100,31 @@ export function timingJson(t) {
79
100
  resume_path: s.resumePath,
80
101
  memory_restored: s.memoryRestored ?? null,
81
102
  cold_boot_reason: s.coldBootReason ?? null,
103
+ durable: s.durable ?? null,
104
+ suspend_path: s.suspendPath ?? null,
105
+ durability: s.durability
106
+ ? {
107
+ state: s.durability.state,
108
+ checkpoint_id: s.durability.checkpointId,
109
+ generation_id: s.durability.generationId,
110
+ local_commit_at: s.durability.localCommitAt,
111
+ durable_by: s.durability.durableBy,
112
+ durable_at: s.durability.durableAt,
113
+ local_commit_to_durable_ms: s.durability.localCommitToDurableMs,
114
+ overdue_at: s.durability.overdueAt,
115
+ reason: s.durability.reason,
116
+ }
117
+ : null,
118
+ lost_suspend: s.lostSuspend
119
+ ? {
120
+ checkpoint_id: s.lostSuspend.checkpointId,
121
+ generation_id: s.lostSuspend.generationId,
122
+ reason: s.lostSuspend.reason,
123
+ suspended_at: s.lostSuspend.suspendedAt,
124
+ restored_checkpoint_id: s.lostSuspend.restoredCheckpointId,
125
+ state_as_of: s.lostSuspend.stateAsOf,
126
+ }
127
+ : null,
82
128
  boot_to_ready_ms: s.bootToReadyMs,
83
129
  host_timings_ms: s.hostTimingsMs,
84
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
6
6
  "license": "Apache-2.0",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "yaml": "2.9.1",
47
- "@shardflux/sdk": "^0.11.1"
47
+ "@shardflux/sdk": "^0.12.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@eslint/js": "10.0.1",