@shardflux/cli 0.6.0 → 0.6.2

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/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.0';
15
+ export const CLI_VERSION = '0.6.2';
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).' },
@@ -38,7 +38,7 @@ const TIMING = {
38
38
  const WAKE_TIMING = {
39
39
  timing: { type: 'boolean', description: 'Print where the time went when the command had to wake the workspace (stderr; with --json a "timing" field, null without a wake).' },
40
40
  };
41
- /** `--if-revision` (0.5.0) of the mutating files commands: the tree revision a file-first write applies to (contracts §29.8). */
41
+ /** `--if-revision` (0.5.0) of the mutating files commands: the tree revision a file-first write applies to. */
42
42
  const IF_REVISION = {
43
43
  'if-revision': {
44
44
  type: 'string',
@@ -58,7 +58,7 @@ const PAGE = {
58
58
  limit: { type: 'string', value: '<n>', description: 'Page size.' },
59
59
  cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
60
60
  };
61
- /** Options of a version produced by save-as-template or a draft publish (contracts §19.8). */
61
+ /** Options of a version produced by save-as-template or a draft publish. */
62
62
  const SAVE = {
63
63
  description: { type: 'string', value: '<text>', description: 'Version description (default: the source version’s).' },
64
64
  'default-lifetime': { type: 'string', value: '<persistent|session>', description: 'Default lifetime of workspaces opened from the new version.' },
@@ -68,7 +68,7 @@ const SAVE = {
68
68
  wait: { type: 'boolean', description: 'Wait until the build is published and registered (or fails).' },
69
69
  timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 30m). The build continues server side.' },
70
70
  };
71
- /** Open-time text inputs of a template version (0.4.0; contracts §24.3). */
71
+ /** Open-time text inputs of a template version (0.4.0). */
72
72
  const INPUTS = {
73
73
  input: {
74
74
  type: 'string',
@@ -93,12 +93,12 @@ const FEEDBACK_HELP = [
93
93
  * MCP server's send_feedback tool. It has to make an agent actually send feedback while it works.
94
94
  */
95
95
  const FEEDBACK_WHY = [
96
- 'Sends your message straight to the Shardflux founder, who reads every one. Agents: use it actively while you work,',
96
+ 'Sends your message straight to the Shardflux team, who read every one. Agents: use it actively while you work,',
97
97
  'not once at the end. Send a short note the moment:',
98
98
  '- something fails unexpectedly, an error or doc is unclear, an option is missing, or you needed a workaround;',
99
- '- your user complains or is frustrated about Shardflux or the workflow around it (slowness, cost or limits,',
100
- ' confusing setup, a missing feature, a workaround): paraphrase it, with what they were trying to do;',
101
- '- your user asked for something Shardflux could not do, or made awkward.',
99
+ '- your user asks for a capability, an option or a smoother workflow: paraphrase it, with what they were trying',
100
+ ' to do;',
101
+ '- your user has an idea for how Shardflux could fit their work better.',
102
102
  'Short and frequent beats polished; include the request id and error code (a failed shard command prints a',
103
103
  'ready-made "feedback:" line to fill in). No need to ask first, but tell your user you sent it. Leave out private',
104
104
  'data about the user and secrets or code they did not mean to share; paraphrase, never paste transcripts.',
@@ -138,7 +138,7 @@ const KEY_COMMANDS = [
138
138
  },
139
139
  {
140
140
  path: ['feedback'],
141
- summary: 'Tell the Shardflux founder what failed, confused you or is missing (agents: as you work)',
141
+ summary: 'Tell the Shardflux team what failed, confused you or is missing (agents: as you work)',
142
142
  description: [
143
143
  ...FEEDBACK_WHY,
144
144
  '',
@@ -154,15 +154,14 @@ const KEY_COMMANDS = [
154
154
  options: {
155
155
  category: { type: 'string', value: '<bug|confusing|missing|idea|praise|other>', description: 'Kind of feedback (default other; see Categories).' },
156
156
  workspace: { type: 'string', value: '<id|key>', description: 'The workspace it is about.' },
157
- 'request-id': { type: 'string', value: '<id>', description: 'request_id from the error, so the founder can find the logs.' },
158
- 'error-code': { type: 'string', value: '<code>', description: 'The error code you saw, e.g. capacity_unavailable.' },
157
+ 'request-id': { type: 'string', value: '<id>', description: 'request_id from the error, so the team can find the logs.' },
158
+ 'error-code': { type: 'string', value: '<code>', description: 'The error code you saw, e.g. template_not_found.' },
159
159
  command: { type: 'string', value: '<text>', description: 'What you ran, e.g. "shard ws exec acme/demo -- npm test".' },
160
160
  'from-file': { type: 'string', value: '<file|->', description: 'Read the message from this file, or - for standard input (long reports, logs); instead of <message...>.' },
161
161
  },
162
162
  examples: [
163
- 'shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 --error-code capacity_unavailable "ws open failed twice in 10 min; expected a start within a minute"',
164
- 'shard feedback --category confusing --command "shard ws exec acme/demo -- npm test" exit 137 with no message, took a while to see it was the memory cap',
165
- 'shard feedback --category missing no way to copy a file between two workspaces, piped files read into files write instead',
163
+ 'shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 --error-code template_not_found "ws open --template python-node failed; expected a suggestion of the closest slug"',
164
+ 'shard feedback --category idea "user would like shard ws open to remember the last template they used"',
166
165
  'npm test 2>&1 | tail -n 60 | shard feedback --category bug --workspace acme/demo --from-file -',
167
166
  ],
168
167
  },
@@ -241,7 +240,7 @@ const KEY_COMMANDS = [
241
240
  {
242
241
  path: ['workspaces', 'exec'],
243
242
  summary: 'Run a command in a workspace; prints its output and exits with its exit code',
244
- description: 'Runs argv directly (no shell; use `-- bash -lc "..."` for shell syntax). Processful workspaces: output is streamed as it arrives and resumes from byte offsets after dropped connections; Ctrl-C stops waiting and cancels the command. File-first workspaces: the command runs as an execution in a fresh VM on the latest tree revision; its output is printed when it ended, and the files it changed under /home/user become the next tree revision (-v lists them). An execution cannot be canceled: Ctrl-C stops waiting and names the command that fetches its result later. A failed or lost execution (nothing published) exits 6, as does a command that could not start (a --cwd that is not a directory, a program not on PATH), with the workspace’s reason.',
243
+ description: 'Runs argv directly (no shell; use `-- bash -lc "..."` for shell syntax). Processful workspaces: output is streamed as it arrives and resumes from byte offsets after dropped connections; Ctrl-C stops waiting and cancels the command. File-first workspaces: the command runs as an execution in a fresh VM on the latest tree revision; its output is printed when it ended, and the files it changed under /home/user become the next tree revision (-v lists them). An execution runs to completion: Ctrl-C stops waiting and names the command that fetches its result later. A failed or lost execution (nothing published) exits 6, as does a command that could not start (a --cwd that is not a directory, a program not on PATH), with the workspace’s reason.',
245
244
  positionals: [REF, { name: 'command', required: true, variadic: true }],
246
245
  options: {
247
246
  cwd: { type: 'string', value: '<dir>', description: 'Working directory, an absolute path (default /home/user; under /home/user on a file-first workspace). A relative path is refused (invalid_cwd).' },
@@ -274,7 +273,7 @@ const KEY_COMMANDS = [
274
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.',
275
274
  positionals: [REF],
276
275
  options: {
277
- 'when-idle': { type: 'string', value: '<duration>', description: 'Suspend once the workspace has been idle this long (30s to 1h, e.g. 30s, 2m; 0.5.1+). Cannot be combined with --wait.' },
276
+ '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.' },
278
277
  'cancel-when-idle': { type: 'boolean', description: 'Cancel a pending --when-idle request (0.5.1+; idempotent).' },
279
278
  ...WAIT,
280
279
  },
@@ -519,7 +518,7 @@ const KEY_COMMANDS = [
519
518
  {
520
519
  path: ['templates', 'init'],
521
520
  summary: 'Write a template.yaml that starts from a base template',
522
- description: 'The file is the recipe v2 document (contracts §24.1): languages, packages, files (local "from" paths are uploaded by "shard templates build"), build steps, and the settings a workspace gets when it opens (env, inputs, start commands, services, defaults). Edit it, then build it.',
521
+ description: 'The file is the recipe v2 document: languages, packages, files (local "from" paths are uploaded by "shard templates build"), build steps, and the settings a workspace gets when it opens (env, inputs, start commands, services, defaults). Edit it, then build it.',
523
522
  positionals: [],
524
523
  options: {
525
524
  base: { type: 'string', value: '<slug[@version]>', description: 'Base template (required), e.g. ubuntu-24.04; without @version, the version open resolves to now.' },
@@ -1058,8 +1057,8 @@ export function topHelp() {
1058
1057
  '',
1059
1058
  'Feedback: shard feedback [options] <message...>',
1060
1059
  ...FEEDBACK_WHY.map((l) => ` ${l}`),
1061
- ' shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 "ws open waited 40 s in capacity_pending; expected under 5 s"',
1062
- ' shard feedback --category confusing "user was frustrated that ws open needs a template slug they had to look up; they expected a default"',
1060
+ ' shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 --error-code template_not_found "ws open --template python-node failed; expected a suggestion of the closest slug"',
1061
+ ' shard feedback --category idea "user would like shard ws open to remember the last template they used"',
1063
1062
  '',
1064
1063
  'Start here (everything the web app does, from a terminal; a person only opens the emailed link and pays in Checkout):',
1065
1064
  ` printf '%s\\n' "$PASSWORD" | shard auth register --email you@example.com`,
package/dist/browser.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `--open` (0.5.0+): opens a URL in the default browser, best effort. The URL is always printed as well, so a
2
+ * `--open` (0.5.0+): opens a URL in the default browser when it can. The URL is always printed as well, so a
3
3
  * failure to open changes nothing: no error, no exit code. $BROWSER (a program run with the URL as its only argument)
4
4
  * wins; otherwise `open` (macOS), `rundll32 url.dll,FileProtocolHandler` (Windows) or `xdg-open` (elsewhere).
5
5
  */
@@ -70,13 +70,13 @@ export type Handler = (ctx: Ctx, values: Values, positionals: string[]) => Promi
70
70
  export declare const str: (v: Values[string]) => string | undefined;
71
71
  export declare const strs: (v: Values[string]) => string[];
72
72
  export declare function out(ctx: Ctx, human: string, machine: unknown): void;
73
- /** `--when-idle <duration>`: whole seconds from 30 to 3600 (the API's after_seconds bounds, contracts §20.6). */
73
+ /** `--when-idle <duration>`: whole seconds from 0 (as soon as it is idle; 0.6.1) to 3600 (the API's after_seconds bounds). */
74
74
  export declare function whenIdleSeconds(text: string): number;
75
75
  export declare function waitTimeout(values: Values): number;
76
76
  /**
77
77
  * `<id|key>`: a UUID is tried as an id first; anything else (or an unknown UUID) as an exact workspace key, across every
78
78
  * lifetime and purpose (sessions, drafts and test instances are hidden from the default list), preferring the live
79
- * workspace over tombstones of ended sessions that had the same key (contracts §19.11).
79
+ * workspace over tombstones of ended sessions that had the same key.
80
80
  */
81
81
  export declare function resolveWorkspace(ctx: Ctx, ref: string): Promise<Workspace>;
82
82
  export declare function oneOf<T extends string>(values: Values, option: string, allowed: readonly T[]): T | undefined;
package/dist/commands.js CHANGED
@@ -30,10 +30,10 @@ export const str = (v) => (typeof v === 'string' ? v : undefined);
30
30
  const cellOf = (ctx, ws) => ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs, ...(ctx.wake ? {} : { wake: null }) });
31
31
  export const strs = (v) => (Array.isArray(v) ? v : typeof v === 'string' ? [v] : []);
32
32
  const MAX_SECRET_VALUE_BYTES = 65_536;
33
- /** `files patch --content-file`: a patch request is at most 7 MiB (contracts §26.2); larger files go through `files write`. */
33
+ /** `files patch --content-file`: a patch request is at most 7 MiB; larger files go through `files write`. */
34
34
  const MAX_PATCH_CONTENT = 7 * 1024 * 1024;
35
35
  const STOPPED_AT = { max_matches: '--max matches', budget: 'its 10 s budget', max_bytes: '4 MiB of results' };
36
- /** A file revision (SHA-256 of the content) or `absent` (contracts §26.2). */
36
+ /** A file revision (SHA-256 of the content) or `absent`. */
37
37
  const REVISION = /^([0-9a-f]{64}|absent)$/;
38
38
  const TOOLS = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
39
39
  function secretNames(names, option) {
@@ -66,14 +66,16 @@ function caps(values) {
66
66
  c.disk_gib = parsePositiveInt(disk, 'disk-gib', 1_000_000);
67
67
  return Object.keys(c).length ? c : undefined;
68
68
  }
69
- /** `--when-idle <duration>`: whole seconds from 30 to 3600 (the API's after_seconds bounds, contracts §20.6). */
69
+ /** `--when-idle <duration>`: whole seconds from 0 (as soon as it is idle; 0.6.1) to 3600 (the API's after_seconds bounds). */
70
70
  export function whenIdleSeconds(text) {
71
+ if (/^0+(?:\.0+)?(?:ms|s|m|h)?$/.test(text.trim()))
72
+ return 0;
71
73
  const ms = parseDuration(text, 'when-idle');
72
74
  if (ms % 1000 !== 0)
73
75
  throw new UsageError('--when-idle must be a whole number of seconds (e.g. 30s, 90s, 2m)');
74
76
  const s = ms / 1000;
75
- if (s < 30 || s > 3600)
76
- throw new UsageError(`--when-idle must be between 30s and 1h (got "${text}")`);
77
+ if (s > 3600)
78
+ throw new UsageError(`--when-idle must be between 0s and 1h (got "${text}")`);
77
79
  return s;
78
80
  }
79
81
  export function waitTimeout(values) {
@@ -83,7 +85,7 @@ export function waitTimeout(values) {
83
85
  /**
84
86
  * `<id|key>`: a UUID is tried as an id first; anything else (or an unknown UUID) as an exact workspace key, across every
85
87
  * lifetime and purpose (sessions, drafts and test instances are hidden from the default list), preferring the live
86
- * workspace over tombstones of ended sessions that had the same key (contracts §19.11).
88
+ * workspace over tombstones of ended sessions that had the same key.
87
89
  */
88
90
  export async function resolveWorkspace(ctx, ref) {
89
91
  const cloud = ctx.cloud();
@@ -190,7 +192,7 @@ export function parseSlugVersion(text, what) {
190
192
  }
191
193
  const INPUT_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
192
194
  /**
193
- * Open-time text inputs (contracts §24.3) from --input-file (NAME=value lines with # comments, or a JSON object of
195
+ * Open-time text inputs from --input-file (NAME=value lines with # comments, or a JSON object of
194
196
  * strings), then each --input NAME=value (a later one wins). Undefined when neither option is given.
195
197
  */
196
198
  async function inputsFrom(ctx, values) {
@@ -415,7 +417,7 @@ export function execExitCode(r) {
415
417
  async function bestEffortCancel(cell, sessionId) {
416
418
  await Promise.race([cell.exec.cancel(sessionId).catch(() => undefined), new Promise((r) => setTimeout(r, 3_000).unref())]);
417
419
  }
418
- // ---- File-first workspaces (0.5.0; contracts §29) ----------------------------------------------------------------------
420
+ // ---- File-first workspaces (0.5.0) ----------------------------------------------------------------------
419
421
  const MODES = { processful: 'processful', 'file-first': 'file_first', file_first: 'file_first' };
420
422
  /** `--mode processful|file-first` (`file_first` too). */
421
423
  function modeFlag(values) {
@@ -450,7 +452,7 @@ function treeRevisionAfter(ws, cell) {
450
452
  return cell.treeRevision ?? ws.treeRevision;
451
453
  }
452
454
  /**
453
- * A lifecycle call a file-first workspace does not have (contracts §29.7: suspend, resume, fork, reset,
455
+ * A lifecycle call a file-first workspace does not have (suspend, resume, fork, reset,
454
456
  * save-as-template), refused before any request as the SDK's workspace handle does: NotSupportedForModeError, printed
455
457
  * with its hint like the API's own refusal.
456
458
  */
@@ -517,7 +519,7 @@ export const KEY_HANDLERS = {
517
519
  out(ctx, `shard ${CLI_VERSION} (@shardflux/sdk ${SDK_VERSION})\n${line}`, { cli: CLI_VERSION, sdk: SDK_VERSION, update });
518
520
  return 0;
519
521
  },
520
- // Feedback (0.5.0+): POST /v1/feedback, emailed to the founder. Validated here first so a bad call costs no request.
522
+ // Feedback (0.5.0+): POST /v1/feedback, emailed to the Shardflux team. Validated here first so a bad call costs no request.
521
523
  async feedback(ctx, values, words) {
522
524
  const category = oneOf(values, 'category', FEEDBACK_CATEGORIES);
523
525
  const context = feedbackContext(ctx, values);
@@ -525,7 +527,7 @@ export const KEY_HANDLERS = {
525
527
  const r = await feedbackSender(ctx).sendFeedback({ message, ...(category ? { category } : {}), context });
526
528
  const human = r.duplicate
527
529
  ? `Already received: the same message was sent in the last 24 hours (feedback ${r.id}); not emailed again.\n`
528
- : `Sent to the Shardflux founder (feedback ${r.id}). Thank you.\n`;
530
+ : `Sent to the Shardflux team (feedback ${r.id}). Thank you.\n`;
529
531
  out(ctx, human, { id: r.id, received_at: r.receivedAt, duplicate: r.duplicate });
530
532
  return 0;
531
533
  },
@@ -659,7 +661,7 @@ export const KEY_HANDLERS = {
659
661
  const cell = cellOf(ctx, ws);
660
662
  const cwd = str(values.cwd);
661
663
  if (ws.mode === 'file_first') {
662
- // An execution (contracts §29.8): answered when the command ended. The id is ours, so Ctrl-C can name it.
664
+ // An execution: answered when the command ended. The id is ours, so Ctrl-C can name it.
663
665
  const id = executionId ?? newExecutionId();
664
666
  let r;
665
667
  try {
@@ -675,7 +677,7 @@ export const KEY_HANDLERS = {
675
677
  }
676
678
  catch (err) {
677
679
  if (ctx.signal.aborted) {
678
- throw new InterruptedError(`Interrupted; execution ${id} keeps running server side if it started (an execution cannot be canceled).`, undefined, {
680
+ throw new InterruptedError(`Interrupted; execution ${id} keeps running server side if it started (an execution runs to completion).`, undefined, {
679
681
  executionId: id,
680
682
  hint: `fetch its result later with: shard executions get ${ref} ${id}`,
681
683
  });
@@ -874,7 +876,7 @@ export const KEY_HANDLERS = {
874
876
  return 0;
875
877
  },
876
878
  async 'workspaces resume'(ctx, values, [ref]) {
877
- // With --wait the resume is held until the workspace runs (contracts §22.6); its tool token is the CLI's label.
879
+ // With --wait the resume is held until the workspace runs; its tool token is the CLI's label.
878
880
  return lifecycle(ctx, values, ref, 'Resuming', (ws, o) => ctx.cloud().workspaces.resume(ws.id, { ...o, agentLabel: ctx.agentLabel }), ['resume', 'Resume']);
879
881
  },
880
882
  async 'workspaces delete'(ctx, values, [ref]) {
package/dist/errors.d.ts CHANGED
@@ -48,7 +48,7 @@ export declare class CheckoutEndedError extends Error {
48
48
  }
49
49
  /**
50
50
  * The user pressed Ctrl-C; `operationId` is set when a server-side operation keeps running, `executionId` when a
51
- * file-first execution does (it cannot be canceled), with `hint` naming the command that fetches its result.
51
+ * file-first execution does (an execution runs to completion), with `hint` naming the command that fetches its result.
52
52
  */
53
53
  export declare class InterruptedError extends Error {
54
54
  name: string;
@@ -60,7 +60,7 @@ export declare class InterruptedError extends Error {
60
60
  hint?: string;
61
61
  });
62
62
  }
63
- /** A file-first execution that ended `failed` or `lost` (contracts §29.8): nothing was published. */
63
+ /** A file-first execution that ended `failed` or `lost`: nothing was published. */
64
64
  export declare class ExecutionFailedError extends Error {
65
65
  name: string;
66
66
  readonly result: ExecutionResult;
package/dist/errors.js CHANGED
@@ -59,7 +59,7 @@ export class CheckoutEndedError extends Error {
59
59
  }
60
60
  /**
61
61
  * The user pressed Ctrl-C; `operationId` is set when a server-side operation keeps running, `executionId` when a
62
- * file-first execution does (it cannot be canceled), with `hint` naming the command that fetches its result.
62
+ * file-first execution does (an execution runs to completion), with `hint` naming the command that fetches its result.
63
63
  */
64
64
  export class InterruptedError extends Error {
65
65
  name = 'InterruptedError';
@@ -73,7 +73,7 @@ export class InterruptedError extends Error {
73
73
  this.hint = more.hint;
74
74
  }
75
75
  }
76
- /** A file-first execution that ended `failed` or `lost` (contracts §29.8): nothing was published. */
76
+ /** A file-first execution that ended `failed` or `lost`: nothing was published. */
77
77
  export class ExecutionFailedError extends Error {
78
78
  name = 'ExecutionFailedError';
79
79
  result;
@@ -147,7 +147,7 @@ export function describeError(err) {
147
147
  const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}) };
148
148
  return { code: 'operation_failed', message: err.message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
149
149
  }
150
- // File-first executions (0.5.0; contracts §29.8): failed or lost like a failed operation (details.reason says why;
150
+ // File-first executions (0.5.0): failed or lost like a failed operation (details.reason says why;
151
151
  // retryable: a new execution id may succeed), still running like a wait that gave up.
152
152
  if (err instanceof ExecutionFailedError) {
153
153
  const e = err.result.error;
package/dist/format.d.ts CHANGED
@@ -22,13 +22,13 @@ export declare function workspaceRow(w: WorkspaceView): string[];
22
22
  export declare function workspaceTable(ws: readonly WorkspaceView[]): string;
23
23
  export declare function isReady(w: WorkspaceView): boolean;
24
24
  /**
25
- * The startup of a workspace's template version (contracts §24.4) in one line: state, trigger, and for a failure the
25
+ * The startup of a workspace's template version in one line: state, trigger, and for a failure the
26
26
  * step or service, exit code and reason. Null when the version has no start commands or services.
27
27
  */
28
28
  export declare function startupText(s: WorkspaceView['startup'] | undefined): string | null;
29
29
  /** The failed step's output tail, indented, after the detail block (empty otherwise). */
30
30
  export declare function startupTail(s: WorkspaceView['startup'] | undefined): string;
31
- /** A pending suspend-when-idle request in one line (0.5.1; contracts §20.6). */
31
+ /** A pending suspend-when-idle request in one line (0.5.1). */
32
32
  export declare function suspendRequestText(r: SuspendRequest): string;
33
33
  /** `shard ws suspend --when-idle`: the recorded request, or the suspend already in progress. */
34
34
  export declare function suspendWhenIdleText(w: {
@@ -63,8 +63,8 @@ export declare function capMoney(minor: unknown, currency: string): string;
63
63
  */
64
64
  export declare function usageText(summary: unknown): string;
65
65
  /**
66
- * The `shard feedback` command a failed command suggests (0.5.0+), with the error's request id and code so the founder
67
- * can find the logs (a failed operation's or execution's own error code, e.g. capacity_unavailable); the agent fills in
66
+ * The `shard feedback` command a failed command suggests (0.5.0+), with the error's request id and code so the team
67
+ * can find the logs (a failed operation's or execution's own error code, e.g. template_not_found); the agent fills in
68
68
  * the text.
69
69
  */
70
70
  export declare function feedbackCommand(e: ErrorInfo): string;
@@ -90,7 +90,7 @@ export declare function languageTable(l: TemplateLanguages): string;
90
90
  export declare function packageTable(page: TemplatePackagePage): string;
91
91
  /** A template: identity, what open resolves to, its versions, and the open version's settings. */
92
92
  export declare function templateDetailText(t: TemplateDetail): string;
93
- /** A version's settings (contracts §24.3), one line per part. */
93
+ /** A version's settings, one line per part. */
94
94
  export declare function settingsText(s: TemplateSettings): string;
95
95
  /** How many changed paths the human output lists before "... N more" (--json lists every one the result has). */
96
96
  export declare const MAX_CHANGED_SHOWN = 100;
@@ -133,6 +133,17 @@ export declare function usageSeriesText(series: unknown): string;
133
133
  /** The period estimate (GET .../usage/estimate): the bill so far and where each allowance is heading. */
134
134
  export declare function usageEstimateText(estimate: unknown): string;
135
135
  export declare function money(amountMinor: number, currency: string): string;
136
+ export declare function isGrantPlan(plan: {
137
+ plan_key: string;
138
+ grant_days?: unknown;
139
+ }): boolean;
140
+ /** The plan in force with the grant's end (`ends_at`, 0.6.2+), which the SDK's generated types add with the API change. */
141
+ type PlanInForce = NonNullable<BillingSubscription['plan']> & {
142
+ ends_at?: string | null;
143
+ };
144
+ /** "trial, ends 2026-11-29 14:03:00Z, then free (trial, catalog live-v7)"; a plan without an end as before. */
145
+ export declare function planInForceText(plan: PlanInForce | null): string;
146
+ /** The public plans: granted plans (the trial, design partners) are never listed or offered. */
136
147
  export declare function planTable(catalog: BillingCatalog): string;
137
148
  export declare function billingStatusText(s: BillingSubscription, org: Pick<Organization, 'name' | 'slug'>): string;
138
149
  export declare function invoiceTable(rows: readonly BillingInvoicePage['data'][number][]): string;
@@ -149,3 +160,4 @@ export declare function egressVersionTable(rows: readonly EgressPolicyVersion[])
149
160
  * cap and its bounds, the rates, and the next step. `saved` adds the version the change was saved as.
150
161
  */
151
162
  export declare function overagePolicyText(p: SpendPolicy, orgName: string, saved?: boolean): string;
163
+ export {};
package/dist/format.js CHANGED
@@ -85,7 +85,7 @@ function originText(o) {
85
85
  }
86
86
  }
87
87
  /**
88
- * The startup of a workspace's template version (contracts §24.4) in one line: state, trigger, and for a failure the
88
+ * The startup of a workspace's template version in one line: state, trigger, and for a failure the
89
89
  * step or service, exit code and reason. Null when the version has no start commands or services.
90
90
  */
91
91
  export function startupText(s) {
@@ -107,15 +107,15 @@ export function startupTail(s) {
107
107
  const lines = s.output_tail.replace(/\n$/, '').split('\n');
108
108
  return `startup output (last ${Buffer.byteLength(s.output_tail)} bytes):\n${lines.map((l) => ` | ${l}`).join('\n')}\n`;
109
109
  }
110
- /** A pending suspend-when-idle request in one line (0.5.1; contracts §20.6). */
110
+ /** A pending suspend-when-idle request in one line (0.5.1). */
111
111
  export function suspendRequestText(r) {
112
- return `after ${r.after_seconds} s idle, not before ${time(r.not_before)} (requested ${time(r.requested_at)})`;
112
+ return `${r.after_seconds === 0 ? 'as soon as idle' : `after ${r.after_seconds} s idle`}, not before ${time(r.not_before)} (requested ${time(r.requested_at)})`;
113
113
  }
114
114
  /** `shard ws suspend --when-idle`: the recorded request, or the suspend already in progress. */
115
115
  export function suspendWhenIdleText(w, r) {
116
116
  if (r.suspend_request) {
117
117
  const s = r.suspend_request;
118
- return (`Suspend when idle: ${w.key} (${w.id}) is suspended once it has been idle for ${s.after_seconds} s, not before ${time(s.not_before)}.\n` +
118
+ return (`Suspend when idle: ${w.key} (${w.id}) is suspended ${s.after_seconds === 0 ? 'as soon as it is idle' : `once it has been idle for ${s.after_seconds} s`}, not before ${time(s.not_before)}.\n` +
119
119
  'A running command, an attached stream or a keepalive postpones it; the next tool call or a resume cancels it.\n' +
120
120
  `cancel with: shard workspaces suspend ${w.key} --cancel-when-idle\n`);
121
121
  }
@@ -132,7 +132,7 @@ export function workspaceDetail(w) {
132
132
  ['state', `${w.observed_state} (desired ${w.desired_state})`],
133
133
  ['ready', isReady(w) ? 'yes' : 'no'],
134
134
  ['template', `${w.template.slug} v${w.template.version} (${w.template.version_id})`],
135
- // The mode (0.5.0; contracts §29) when the API reports it; a file-first workspace's state is its tree revision.
135
+ // The mode (0.5.0) when the API reports it; a file-first workspace's state is its tree revision.
136
136
  ...(w.mode !== undefined ? [['mode', modeText(w.mode)]] : []),
137
137
  ...(w.mode === 'file_first' ? [['tree revision', String(w.tree_revision)]] : []),
138
138
  ['cell', w.cell_id ? `${w.cell_id} ${w.cell_endpoint ?? '(endpoint unresolved)'}` : '-'],
@@ -317,8 +317,12 @@ export function usageText(summary) {
317
317
  const head = [];
318
318
  if (isObject(s.plan))
319
319
  head.push(['plan', `${scalar(s.plan.plan_key)}${s.plan.catalog_version !== undefined ? ` (catalog ${scalar(s.plan.catalog_version)})` : ''}`]);
320
- if (isObject(s.period))
321
- head.push(['period', `${time(scalar(s.period.start))} .. ${time(scalar(s.period.end))}`]);
320
+ if (isObject(s.period)) {
321
+ // A plan grant's own window (source `grant`, 0.6.2+): the trial's 60 days rather than the calendar month.
322
+ const trial = isObject(s.plan) && s.plan.plan_key === 'trial';
323
+ const grant = s.period.source === 'grant' ? (trial ? ' (the trial; then free)' : ' (the plan grant; it ends then)') : '';
324
+ head.push(['period', `${time(scalar(s.period.start))} .. ${time(scalar(s.period.end))}${grant}`]);
325
+ }
322
326
  const m = isObject(s.measurement) ? s.measurement : s;
323
327
  if (m.measured_through !== undefined)
324
328
  head.push(['measured through', time(m.measured_through)]);
@@ -326,7 +330,8 @@ export function usageText(summary) {
326
330
  head.push(['estimate', scalar(s.measurement.estimate_status)]);
327
331
  if (typeof s.allowance_exhausted === 'boolean') {
328
332
  const reason = typeof s.exhausted_reason === 'string' ? `${s.exhausted_reason}: ` : '';
329
- head.push(['allowance exhausted', s.allowance_exhausted ? `yes (${reason}new opens/resumes are refused)` : 'no']);
333
+ const trialEnded = isObject(s.plan) && s.plan.plan_key === 'trial';
334
+ head.push(['allowance exhausted', s.allowance_exhausted ? (trialEnded ? `yes (${reason}the trial ends; the organization is back on free)` : `yes (${reason}new opens/resumes are refused)`) : 'no']);
330
335
  }
331
336
  for (const [k, v] of Object.entries(s))
332
337
  if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
@@ -363,8 +368,8 @@ export function usageText(summary) {
363
368
  /** Request ids and error codes pasted unquoted into the suggested command must be shell-inert. */
364
369
  const SHELL_SAFE = /^[A-Za-z0-9._:-]{1,200}$/;
365
370
  /**
366
- * The `shard feedback` command a failed command suggests (0.5.0+), with the error's request id and code so the founder
367
- * can find the logs (a failed operation's or execution's own error code, e.g. capacity_unavailable); the agent fills in
371
+ * The `shard feedback` command a failed command suggests (0.5.0+), with the error's request id and code so the team
372
+ * can find the logs (a failed operation's or execution's own error code, e.g. template_not_found); the agent fills in
368
373
  * the text.
369
374
  */
370
375
  export function feedbackCommand(e) {
@@ -402,7 +407,7 @@ export function errorText(e, hint, feedback) {
402
407
  : '';
403
408
  return `error: ${e.message}${reason} [${meta.join(' ')}]\n${issues}${hint ? `hint: ${hint}\n` : ''}${feedback ? `feedback: ${feedback}\n` : ''}`;
404
409
  }
405
- // ---- Templates v2 (contracts §19) ---------------------------------------------------------------
410
+ // ---- Templates v2 ---------------------------------------------------------------
406
411
  export function templateTable(rows) {
407
412
  return table(['SLUG', 'OWNER', 'OPEN VERSION', 'LAYOUTS', 'FILES', 'DRAFT', 'ARCHIVED'], rows.map((t) => {
408
413
  const v = t.open_version;
@@ -514,7 +519,7 @@ export function templateDetailText(t) {
514
519
  : 'No versions.\n';
515
520
  return `${head}\n${versions}${open?.settings ? `\nsettings of v${open.version}:\n${settingsText(open.settings)}` : ''}`;
516
521
  }
517
- /** A version's settings (contracts §24.3), one line per part. */
522
+ /** A version's settings, one line per part. */
518
523
  export function settingsText(s) {
519
524
  const inputs = Object.entries(s.inputs).map(([n, i]) => `${n} (${i.kind}${i.required ? ', required' : ''}${i.default !== null ? `, default ${JSON.stringify(i.default)}` : ''})`);
520
525
  const d = s.defaults;
@@ -528,7 +533,7 @@ export function settingsText(s) {
528
533
  ['egress', egress],
529
534
  ]).replace(/^/gm, ' ');
530
535
  }
531
- // ---- File-first executions (0.5.0; contracts §29.8) ----------------------------------------------------------------------
536
+ // ---- File-first executions (0.5.0) ----------------------------------------------------------------------
532
537
  const CHANGE_LETTER = { added: 'A', modified: 'M', deleted: 'D' };
533
538
  /** How many changed paths the human output lists before "... N more" (--json lists every one the result has). */
534
539
  export const MAX_CHANGED_SHOWN = 100;
@@ -730,8 +735,25 @@ export function money(amountMinor, currency) {
730
735
  const c = currency.toLowerCase();
731
736
  return `${zeroDecimal.has(c) ? String(amountMinor) : (amountMinor / 100).toFixed(2)} ${c.toUpperCase()}`;
732
737
  }
738
+ /**
739
+ * Plans that are only ever granted, never bought: the signup trial and the design-partner plan (0.6.2+). The API's
740
+ * catalog marks them with `grant_days`; the keys cover an API that lists them without it.
741
+ */
742
+ const GRANT_PLANS = new Set(['trial', 'partner']);
743
+ export function isGrantPlan(plan) {
744
+ return GRANT_PLANS.has(plan.plan_key) || (typeof plan.grant_days === 'number' && plan.grant_days > 0);
745
+ }
746
+ /** "trial, ends 2026-11-29 14:03:00Z, then free (trial, catalog live-v7)"; a plan without an end as before. */
747
+ export function planInForceText(plan) {
748
+ if (!plan)
749
+ return '-';
750
+ const endsAt = typeof plan.ends_at === 'string' && plan.ends_at !== '' ? plan.ends_at : null;
751
+ const ends = endsAt ? `, ends ${time(endsAt)}${plan.plan_key === 'trial' ? ', then free' : ''}` : '';
752
+ return `${plan.plan_key}${ends} (${plan.source}, catalog ${plan.catalog_version})`;
753
+ }
754
+ /** The public plans: granted plans (the trial, design partners) are never listed or offered. */
733
755
  export function planTable(catalog) {
734
- const rows = catalog.plans.map((p) => {
756
+ const rows = catalog.plans.filter((p) => !isGrantPlan(p)).map((p) => {
735
757
  const prices = p.prices.length ? p.prices.map((x) => `${money(x.amount_minor, x.currency)}/${x.interval}`).join(', ') : 'free';
736
758
  const buy = p.prices.some((x) => x.purchasable) ? 'yes' : '-';
737
759
  return [p.plan_key, p.name, prices, buy];
@@ -743,7 +765,7 @@ export function billingStatusText(s, org) {
743
765
  const pending = s.pending_checkout;
744
766
  return kv([
745
767
  ['organization', `${org.name} (${org.slug})`],
746
- ['plan', s.plan ? `${s.plan.plan_key} (${s.plan.source}, catalog ${s.plan.catalog_version})` : '-'],
768
+ ['plan', planInForceText(s.plan)],
747
769
  ['billing state', s.billing_state],
748
770
  ['subscription', sub ? `${sub.status}${sub.plan_key ? ` ${sub.plan_key}` : ''}${sub.cancel_at_period_end ? ', cancels at period end' : ''}` : 'none'],
749
771
  ['period end', sub ? time(sub.current_period_end) : '-'],
package/dist/http.d.ts CHANGED
@@ -1,14 +1,7 @@
1
1
  /**
2
- * The fetch given to the SDK: the runtime's fetch with `Connection: close` on
3
- * every request (API and cell gateway).
4
- *
5
- * Why: Node 26.7's bundled undici (8.9.0) intermittently stalls a request
6
- * sent on a reused keep-alive connection until an unrelated timer fires (up to
7
- * ~30 s, the SDK's request timeout, after which the SDK retries). Reproduced
8
- * with bare fetch against both node:http and Fastify servers; Node 22 (undici
9
- * 6.28) and `Connection: close` do not stall. A fresh connection per request
10
- * costs one TCP/TLS handshake, which is negligible for these call rates.
11
- * Set SHARDFLUX_HTTP_KEEPALIVE=1 to use the runtime's keep-alive pooling.
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.
12
5
  *
13
6
  * `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
14
7
  * held by the server for up to 20 s). The SDK passes the caller's signal to its
package/dist/http.js CHANGED
@@ -1,14 +1,7 @@
1
1
  /**
2
- * The fetch given to the SDK: the runtime's fetch with `Connection: close` on
3
- * every request (API and cell gateway).
4
- *
5
- * Why: Node 26.7's bundled undici (8.9.0) intermittently stalls a request
6
- * sent on a reused keep-alive connection until an unrelated timer fires (up to
7
- * ~30 s, the SDK's request timeout, after which the SDK retries). Reproduced
8
- * with bare fetch against both node:http and Fastify servers; Node 22 (undici
9
- * 6.28) and `Connection: close` do not stall. A fresh connection per request
10
- * costs one TCP/TLS handshake, which is negligible for these call rates.
11
- * Set SHARDFLUX_HTTP_KEEPALIVE=1 to use the runtime's keep-alive pooling.
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.
12
5
  *
13
6
  * `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
14
7
  * held by the server for up to 20 s). The SDK passes the caller's signal to its
@@ -16,6 +9,7 @@
16
9
  * sends its POST without one), so Ctrl-C would otherwise wait the hold out.
17
10
  */
18
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.
19
13
  const keepAlive = env.SHARDFLUX_HTTP_KEEPALIVE === '1';
20
14
  return (input, init) => {
21
15
  const headers = new Headers(init?.headers);