@shardflux/cli 0.28.2 → 0.28.4

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
@@ -2,6 +2,59 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Keep queued messages moving after an obsolete computer upgrade choice is cleared or changed; offer only compatible computer upgrades.
6
+
7
+ ## 0.28.4
8
+
9
+ - Pending native conversation messages keep their original sequence until acknowledged, including across a paused computer or an API restart. The retained computer wakes through its existing authorized path.
10
+
11
+ - Available workspace updates use the current saved machine or project environment, matching the target applied by an update after a saved-version rollback.
12
+
13
+ - Conversation moves require a saved checkpoint when idle. A new turn releases an incomplete idle move while keeping its computer, session, files and queued messages; active turns keep their deferred move.
14
+
15
+ - Read older and dismissed orchestrator answers with `shard orchestrator history`; `--cursor`, `--limit`, and `--organization` select a read-only page. JSON returns `inputs` and `next_cursor`.
16
+
17
+ - Home computer updates select the organization’s current saved machine, including after a machine rollback; repository and explicit update targets keep their existing behavior. Request and response fields are unchanged.
18
+
19
+ - Read current recipient input-file capability through the authenticated orchestrator API; unsupported file requests return a retryable update action, and pending file delivery keeps its actual waiting reason.
20
+
21
+ - Orchestrator dismissal: the API keeps dismissed, unstarted inputs out of dispatch and queue positions until they are restored. Work that already started continues; request and response fields are unchanged.
22
+
23
+ - Folder starts (0.28.4+): an agent's `project_link` with a first prompt (the device helper) never stays running: a start from the folder that does not finish within 5 minutes fails with what held it (for example `Setting up <project>: saving template`), and a start process that ignores its stop is killed 10 seconds later.
24
+
25
+ - The Shardflux app 0.9.5 (0.28.4+): `shard` installs and updates `@shardflux/desktop-darwin-arm64` / `-x64` and
26
+ `@shardflux/desktop-linux-x64` / `-arm64` 0.9.5 (a three-step first run, ⌘2 History search, request details
27
+ from your computer, full-screen pictures and videos on a phone, agents that stay in view, New project and Sign
28
+ out on the web, talking on Android and with several organizations).
29
+
30
+ - Cloud input-file delivery (0.28.4+, SDK 0.30.4): native-route refusals preserve `codex_cloud_input_files_require_native`; explicit `delivery.waiting` returns immediately with its reason and message, and is never reported as delivered.
31
+
32
+ - Settled reason (0.28.4+, SDK 0.30.4): preserve optional `settled_reason` on conversations and agents: `computer_suspended` identifies a computer that froze an active turn; `computer_failed` identifies a failed computer. Absent/null does not establish completion.
33
+
34
+ - Orchestrator usage waits (0.28.4+, SDK 0.30.4): orchestrator waits preserve optional input `waiting.reason` / `waiting.until` and orchestrator `waiting.until`; queued state and queue position stay unchanged. Message-only waits from older APIs remain compatible.
35
+
36
+ - Credential imports (0.28.4+): queue bulk credential imports instead of opening every local source and upload together. Every selected source is processed, and an individual failure does not stop the others.
37
+
38
+ - The Shardflux app 0.9.4 (0.28.3+): `shard` installs and updates `@shardflux/desktop-darwin-arm64` / `-x64` and
39
+ `@shardflux/desktop-linux-x64` / `-arm64` 0.9.4 (files with a ⌘2 task, ⌘2 Credentials and Set up from this Mac,
40
+ talking in Safari, new answers stay visible, device request outcomes).
41
+
42
+ - Device request outcomes (0.28.3+, SDK 0.30.3): `shard orchestrator show <input-id>` and `shard task show <task-id>` show device requests, paths and recorded errors, failures first. Device activity keeps the actual error before API delivery.
43
+
44
+ - Input attachments (0.28.3+, SDK 0.30.3): `shard orchestrator say --file <path>` attaches files; repeat the flag, or use `--file - --file-name <name>` to stream stdin. Files-only inputs are supported.
45
+
46
+ - Automatic task landing (0.28.3+): the device helper automatically lands a finished board task's tested result in its linked repository even when the desktop board is closed. It respects Auto land and project landing settings, keeps local edits, waits for a missing folder, retries an interrupted receipt, and records the board only after delivery. Conflicts and unfinished master integration wait for a new result instead of repeating a steer after a service restart.
47
+
48
+ - Linux helper shutdown (0.28.3+): short Linux commands shut down their command-lifetime helper cleanly; service helpers leave stdin with the foreground command.
49
+
50
+ - Task results (0.28.3+, SDK 0.30.3): keep canonical task-result links and automatic resend metadata in orchestrator input output.
51
+
52
+ - Coordinator questions (0.28.3+, SDK 0.30.3): input reads preserve a coordinator wait and its message; internal worker blockers reach their coordinator before a question reaches the person. Existing needs-you answer commands remain unchanged.
53
+
54
+ - Credential setup (0.28.3+, SDK 0.30.3): `shard credentials setup --from-this-computer` brings existing shell, project and login credentials along automatically. `--if-needed` leaves unchanged values alone; `--plan` offers an optional names-only review. Connected agents can request the same secure local transfer through the device helper.
55
+
56
+ - Credential errors (0.28.3+): write-only credential failures never copy upstream prose or details into output, including values shorter than eight bytes and JSON-escaped values. Status and request id remain available.
57
+
5
58
  - The Shardflux app 0.9.3 (0.28.2+): `shard` installs and updates `@shardflux/desktop-darwin-arm64` / `-x64` and
6
59
  `@shardflux/desktop-linux-x64` / `-arm64` 0.9.3 (files open beside your work, pictures and videos picture in picture,
7
60
  ⌘2 shows the agents your orchestrator starts, needs-you questions read whole).
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @shardflux/cli: `shard`
2
2
 
3
+ Queued messages retain their computer and native session when a pending upgrade choice is cleared or changed.
4
+
3
5
  Use `shard --help` or `shard <command> --help` to explore commands before signing in.
4
6
 
5
7
  For error codes, retry timing, and plan recovery, see the [error reference](https://docs.shardflux.dev/reference/errors).
@@ -101,7 +103,7 @@ and restarts after a CLI update. It keeps the live twin, `~/Shardflux` files and
101
103
  Use `shard device requests`, `shard device approve <id>` or `shard device deny <id>` for local approvals, and
102
104
  `shard device approvals auto|read|ask` to choose whether your agents ask first (0.28.0+);
103
105
  desktop notifications point to pending requests. On systems without a systemd user session, the helper runs
104
- while your `shard` commands are open. `shard agents open` opens a terminal when launched outside one, using
106
+ while your `shard` commands are open; their input stays with the foreground command. `shard agents open` opens a terminal when launched outside one, using
105
107
  `$TERMINAL`, `x-terminal-emulator` or `gnome-terminal`, or prints the attach command. On macOS the same app lives in
106
108
  the menu bar.
107
109
 
@@ -263,6 +265,9 @@ Finished cloud work lands in your folder by itself: a conversation's commits com
263
265
  board task's result into your linked folder and the project's default branch. `shard settings --auto-land off`
264
266
  (0.28.0+) keeps it in the cloud on this computer (`shard task land` and `shard conversations land` still land on
265
267
  request); `--auto-land on` turns it back on. The Shardflux app's Settings → Projects changes the same switch.
268
+ The device helper brings finished task results back while your computer is reachable, including work requested from
269
+ your phone; it catches up after your computer reconnects. Your local edits stay in place, and a conflicting merge
270
+ waits for a decision instead of overwriting them.
266
271
 
267
272
  Terminal conversations open in the terminal you choose (0.22.0+):
268
273
 
@@ -1529,8 +1534,10 @@ your agent uses the matching machine or project save/rollback commands in its so
1529
1534
  `shard machine open` (0.17.1+): it creates or resumes the machine VM without attaching and prints its key for
1530
1535
  `shard ws exec` and `shard files write`. `shard machine setup --from-this-computer` (0.20.0+) does it for you: your own
1531
1536
  Claude Code (`claude -p`, else Codex) recreates this computer's CLIs and tools, language runtimes, shell config and
1532
- aliases, git identity and dotfiles on the machine, on your subscription, then saves it. On this computer it only reads;
1533
- `~/.ssh`, keychains, `.env`, token and credential files are never read. The apps and the first run start it once,
1537
+ aliases, git identity and dotfiles on the machine, on your subscription, then saves it. That setup agent reads
1538
+ tool and configuration metadata and excludes `~/.ssh`, keychains, `.env`, token and credential files. Optional
1539
+ `--with-credentials` (0.28.3+) runs the trusted local credential transfer afterwards; onboarding includes that transfer.
1540
+ The apps and the first run start machine setup once,
1534
1541
  right after you connect your agents. `shard machine setup --status` shows the machine as your account has it, in the
1535
1542
  same line the apps show: ready, saving (with its place in the queue), or the one command to run next (0.22.0+); `shard
1536
1543
  setup --inspect --json` carries it as `onboarding.machine`. `connections_ready` (and the legacy `ready` field) means
@@ -1719,6 +1726,8 @@ shard c delete conv_k3m5q7s2u4w6y3b5d7f2h4j6ln --yes
1719
1726
 
1720
1727
  ### Your orchestrator (0.28.0+)
1721
1728
 
1729
+ A queued input (0.28.4+) can carry `waiting.message`: the platform explains what it waits for, including a known usage-reset time. Keep that line alongside its queue position while the input remains queued.
1730
+
1722
1731
  `shard orchestrator say` passes your words to the API as one input, without cutting the final instruction.
1723
1732
 
1724
1733
  Your orchestrator is your own agent on top of every project, on its own Shardflux computer and the same on every
@@ -2100,3 +2109,55 @@ Bare `shard` in a coding-agent shell or without a TTY returns its setup guide us
2100
2109
  Needs-you output prints each question and its optional `option_details` in full, including what each choice does and what follows it. The first suggested answer is the agent’s recommendation. Secret values go through the project’s secure setup, never a question or its ordinary answer.
2101
2110
 
2102
2111
  Conversation creation (0.28.2+) includes the optional launching conversation only when `/v1/meta` advertises `capabilities.conversation_origin: true`. Older APIs receive the existing create body; no rejected create is replayed with different fields.
2112
+
2113
+
2114
+ ## Input attachments (0.28.3+)
2115
+
2116
+ `shard orchestrator say --file <path>` attaches files; repeat the flag, or use `--file - --file-name <name>` to stream stdin. Files-only inputs are supported.
2117
+
2118
+ ```sh
2119
+ shard orchestrator say "Review these" --file spec.pdf --file photo.png
2120
+ cat archive.tar | shard orchestrator say --file - --file-name archive.tar
2121
+ ```
2122
+ File uploads stream to durable API storage, then the input carries their pinned references. Each receiving thread gets the files before its turn, under `~/inputs/`.
2123
+
2124
+ ### Device request outcomes (0.28.3+)
2125
+
2126
+ ```sh
2127
+ shard orchestrator show in_abcdefghijklmnopqrstuvwxyz
2128
+ shard task show task_abcdefghijklmnopqrstuvwxyz
2129
+ ```
2130
+
2131
+ Each request shows what ran, its paths and outcome, and its recorded error. Failures appear first. Add `--json` for the full detail object. Reading starts no computer.
2132
+
2133
+ Scoped vault imports (0.28.3+) use `shard secrets import NAME --from hsec --scope project --project PROJECT` (or `--scope agents --org ORG` while signed in). Re-importing an identical value leaves its version unchanged; a changed value rotates it. The vault sends the value directly to the encrypted write-only API. Failures show a safe status and request id without upstream prose or details that could echo the value.
2134
+
2135
+ ## Credential setup (0.28.3+)
2136
+
2137
+ Every selected source is processed, and an unavailable source does not stop other credentials from being imported.
2138
+
2139
+ Run `shard credentials setup --from-this-computer --if-needed` after machine setup to bring your existing credentials along. Machine credentials are available to all your agents; project `.env` credentials stay local and override machine values by name. Values transfer directly from the local source into the write-only secret store and private credential files. The command prints names and delivery progress. Use `--plan` for an optional review, then `--apply PLAN_ID`. If a requested credential is missing everywhere, your agent asks once through the secure credential field.
2140
+
2141
+ ## Conversation delivery waits (0.28.4+)
2142
+
2143
+ `shard conversations send --wait` prints the server’s explicit delivery-wait message; JSON retains `delivery.waiting.reason` and `message`. Route file-bearing delegated workers through native threads. Reason `codex_cloud_input_files_require_native` identifies a Cloud route that needs a native thread before the message is accepted.
2144
+
2145
+ ### Dismissed orchestrator inputs
2146
+
2147
+ Dismiss an orchestrator input before it starts to keep it from running. Restoring that queued input puts it back in line. Dismissing an input that already started leaves its existing work running.
2148
+
2149
+ ### Home computer updates
2150
+
2151
+ Updating a home conversation’s computer selects the organization’s current saved machine, including after a machine rollback. Repository conversations keep their project’s saved environment.
2152
+
2153
+ ### Recipient file support (0.28.4+)
2154
+
2155
+ Run `shard orchestrator capabilities --json` to read `{capabilities: {input_files}}` for your recipient without creating or waking a computer.
2156
+
2157
+ A conversation moves from a saved turn. A running conversation finishes its turn before moving; a new turn continues on its retained computer when an idle move has no checkpoint.
2158
+
2159
+ Available computer updates report the same current saved machine or project environment that an update applies, including a saved version selected by rollback.
2160
+
2161
+ Paged History (0.28.4+): Read older and dismissed orchestrator answers with `shard orchestrator history`; `--cursor`, `--limit`, and `--organization` select a read-only page. JSON returns `inputs` and `next_cursor`.
2162
+
2163
+ Pending native conversation messages retain their original sequence until the harness acknowledges delivery, including after the computer pauses or the API restarts.
package/dist/args.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.28.2";
1
+ export declare const CLI_VERSION = "0.28.4";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;
package/dist/args.js CHANGED
@@ -15,7 +15,7 @@ import { BOARD_COMMANDS } from "./board-args.js";
15
15
  import { ORCHESTRATOR_COMMANDS } from "./orchestrator-args.js";
16
16
  import { UsageError } from "./errors.js";
17
17
  import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
18
- export const CLI_VERSION = '0.28.2';
18
+ export const CLI_VERSION = '0.28.4';
19
19
  export const GLOBAL_OPTIONS = {
20
20
  'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
21
21
  json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
@@ -244,6 +244,7 @@ const KEY_COMMANDS = [
244
244
  { path: ['machine', 'setup'], summary: 'Let your own Claude Code or Codex set up your machine from this computer (0.20.0+)', positionals: [], options: {
245
245
  'from-this-computer': { type: 'boolean', description: 'Recreate this computer’s development setup on your machine: its CLIs and tools, language runtimes, shell config and aliases, git identity and dotfiles. Required.' },
246
246
  'if-needed': { type: 'boolean', description: 'Only when your machine was never set up from this computer (what the apps run after onboarding).' },
247
+ 'with-credentials': { type: 'boolean', description: 'After the machine is saved, bring existing credentials along with the trusted local credential setup.' },
247
248
  status: { type: 'boolean', description: 'Show the last setup from this computer instead of running one.' },
248
249
  agent: { type: 'string', value: '<claude|codex|cloud>', description: 'Which agent sets it up (default: this computer’s Claude Code, else its Codex, else your cloud Claude in the machine VM).' },
249
250
  timeout: { type: 'string', value: '<duration>', description: 'Time box for the agent (default 10m).' },
@@ -982,6 +983,16 @@ const KEY_COMMANDS = [
982
983
  examples: ['shard executions get acme/agent-7 build-42', 'shard ws executions get acme/agent-7 ex-6f1c2a9e-0d7b-4c1e-9a53-2b8e1f0c4d7a --wait 10m --json'],
983
984
  },
984
985
  { path: ['secrets', 'log'], summary: 'Show credential request and fill audit events', positionals: [], options: {} },
986
+ { path: ['credentials', 'setup'], summary: 'Bring existing credentials from this computer to your machine and project', positionals: [], options: {
987
+ 'from-this-computer': { type: 'boolean', description: 'Discover source names locally and transfer values directly; no value goes to a model.' },
988
+ 'if-needed': { type: 'boolean', description: 'Reconcile existing sources; report needed:false when their stored versions are unchanged.' },
989
+ plan: { type: 'boolean', description: 'Review the names and scopes once before transferring selected sources.' },
990
+ apply: { type: 'string', value: '<plan-id>', description: 'Continue a reviewed setup using its original source descriptors.' },
991
+ deny: { type: 'string', value: '<plan-id>', description: 'Decline a reviewed setup.' },
992
+ picks: { type: 'string', value: '<file|->', description: 'JSON source IDs and scopes for --apply; - reads stdin. No values.' },
993
+ project: { type: 'string', value: '<project-id>', description: 'Project for local credentials (default current context).' },
994
+ path: { type: 'string', value: '<folder>', description: 'Project folder whose .env files to discover (default current folder).' },
995
+ } },
985
996
  { path: ['credentials', 'list'], summary: 'Show pending login requests', positionals: [], options: {} },
986
997
  { path: ['credentials', 'enable'], summary: 'Enable credential fill in a workspace', positionals: [REF], options: {} },
987
998
  { path: ['credentials', 'deny'], summary: 'Deny a login request', positionals: [{ name: 'request-id', required: true }], options: {} },
@@ -84,7 +84,8 @@ export const BOARD_COMMANDS = [
84
84
  path: ['task', 'land'],
85
85
  summary: 'Land a finished task: its result into your folder and the project’s default branch (0.25.0+)',
86
86
  positionals: [{ name: 'id', required: true }],
87
- options: { path: { type: 'string', value: '<folder>', description: 'The folder to land in (default: the project’s folder on this computer, if any).' } },
87
+ options: { path: { type: 'string', value: '<folder>', description: 'The folder to land in (default: the project’s folder on this computer, if any).' },
88
+ automatic: { type: 'boolean', hidden: true, description: 'Device automatic delivery: require Auto land, a linked folder and the master’s tested result.' } },
88
89
  description: [
89
90
  'A task has one result: the branch shard/<task id>, where its master merged its workers’ branches. Landing merges it',
90
91
  'into the project’s default branch on the project hub (a fast-forward, or a merge commit when the merge is clean),',
@@ -1,3 +1,4 @@
1
+ import { deviceOutcomeLines } from "./device-outcomes.js";
1
2
  import { findOrg, resolveOrg } from "./account-scope.js";
2
3
  import { out, principal, str, strs, readSecretValue, askSecretValue } from "./commands.js";
3
4
  import { detailLine, pickAnswers } from "./conversation-commands.js";
@@ -150,6 +151,8 @@ export function taskDetailText(d) {
150
151
  lines.push(` Landed: ${r.landed.how} ${r.landed.ref}`);
151
152
  }
152
153
  }
154
+ if (d.device_requests !== undefined)
155
+ lines.push(...deviceOutcomeLines(d.device_requests));
153
156
  return `${lines.join('\n')}\n`;
154
157
  }
155
158
  /** --now, --next or --later (one at most); undefined when none was given. */
@@ -350,7 +353,7 @@ export const BOARD_HANDLERS = {
350
353
  return 0;
351
354
  },
352
355
  async 'task land'(ctx, values, [id]) {
353
- const o = await landTask(landDeps(ctx), id, { path: str(values.path) });
356
+ const o = await landTask(landDeps(ctx), id, { path: str(values.path), automatic: values.automatic === true });
354
357
  out(ctx, landedText(o), o);
355
358
  return 0;
356
359
  },
@@ -391,7 +394,7 @@ export const BOARD_HANDLERS = {
391
394
  await findOrg(ctx.account(), organizationId);
392
395
  }
393
396
  const save = (value) => {
394
- ctx.addSecret(value);
397
+ ctx.addWriteOnlyValue(value);
395
398
  return cloud.needsYou.answer(id, { credential: { value, scope: target } }, { ...(organizationId ? { organizationId } : {}), signal: ctx.signal });
396
399
  };
397
400
  const value = await readSecretValue(ctx, values, item.credential.name);
@@ -61,6 +61,8 @@ export interface Ctx {
61
61
  };
62
62
  /** A credential this command received (redacted from error output). */
63
63
  addSecret(value: string): void;
64
+ /** A credential value this command writes: redacted like addSecret, and failures report only status-based codes. */
65
+ addWriteOnlyValue(value: string): void;
64
66
  /** The update status for `shard version` (forced: ask the API now). Null when unknown. */
65
67
  versionStatus(force: boolean): Promise<UpdateView | null>;
66
68
  /** The credentials file (config directory from the environment). */
package/dist/commands.js CHANGED
@@ -2349,9 +2349,9 @@ export const KEY_HANDLERS = {
2349
2349
  const project = agent ? null : (await principal(ctx)).project_id;
2350
2350
  const imported = [];
2351
2351
  for (const [i, name] of names.entries()) {
2352
- const value = await vaultValue(from, name, { signal: ctx.signal, ...(keychain ? { keychain } : {}) });
2353
- ctx.addSecret(value);
2354
- imported.push(agent ? await agent.cloud.secrets.createAgent(agent.target, { name: targets[i], value }) : await ctx.cloud().secrets.create(project, { name: targets[i], value }));
2352
+ const value = await vaultValue(from, name, { signal: ctx.signal, env: ctx.io.env, ...(keychain ? { keychain } : {}) });
2353
+ ctx.addWriteOnlyValue(value);
2354
+ imported.push(agent ? await agent.cloud.secrets.importAgent(agent.target, { name: targets[i], value }) : await ctx.cloud().secrets.create(project, { name: targets[i], value }));
2355
2355
  }
2356
2356
  out(ctx, `Imported ${imported.map(s => s.name).join(', ')}. ${SECRET_SECURITY_NOTE}\n`, { data: imported, security_note: SECRET_SECURITY_NOTE });
2357
2357
  return 0;
@@ -2612,7 +2612,7 @@ async function agentSecretCommand(ctx, values, action, ref) {
2612
2612
  if (action === 'create') {
2613
2613
  if (!SECRET_NAME.test(ref ?? ''))
2614
2614
  throw new UsageError('Use an environment variable name for the credential.');
2615
- const value = await readSecretValue(ctx, values, ref), save = (value) => { ctx.addSecret(value); return cloud.secrets.createAgent(target, { name: ref, value, ...(str(values.description) !== undefined ? { description: str(values.description) } : {}) }); };
2615
+ const value = await readSecretValue(ctx, values, ref), save = (value) => { ctx.addWriteOnlyValue(value); return cloud.secrets.createAgent(target, { name: ref, value, ...(str(values.description) !== undefined ? { description: str(values.description) } : {}) }); };
2616
2616
  const r = value !== null ? await save(value) : await askSecretValue(ctx, values, { title: `Add ${ref}`, purpose: `Saved encrypted for your agents as ${ref}.`, name: ref }, save);
2617
2617
  out(ctx, describe(r), r);
2618
2618
  return 0;
@@ -2636,7 +2636,7 @@ async function agentSecretCommand(ctx, values, action, ref) {
2636
2636
  return 0;
2637
2637
  }
2638
2638
  if (action === 'rotate') {
2639
- const value = await readSecretValue(ctx, values, r.name), save = (value) => { ctx.addSecret(value); return cloud.secrets.rotateAgent(r.id, value); };
2639
+ const value = await readSecretValue(ctx, values, r.name), save = (value) => { ctx.addWriteOnlyValue(value); return cloud.secrets.rotateAgent(r.id, value); };
2640
2640
  const changed = value !== null ? await save(value) : await askSecretValue(ctx, values, { title: `New value for ${r.name}`, purpose: `Replaces the encrypted credential ${r.name}.`, name: r.name }, save);
2641
2641
  out(ctx, describe(changed), changed);
2642
2642
  return 0;
@@ -87,6 +87,8 @@ const sentLines = (r) => `${r.restored ? 'Restored\n' : ''}${(r.files ?? []).map
87
87
  export const SEND_WAIT_MS = 60_000;
88
88
  /** What happened to a sent message, as `conversations send --wait` prints it (0.27.0+). */
89
89
  export function deliveryLine(d) {
90
+ if (d.waiting)
91
+ return d.waiting.message;
90
92
  if (d.state === 'pending')
91
93
  return 'Not delivered yet.';
92
94
  if (d.state === 'uncertain')
@@ -0,0 +1,8 @@
1
+ import type { Ctx } from './commands.js';
2
+ import { conversationClient } from './conversation-commands.js';
3
+ type Input = Parameters<ReturnType<typeof conversationClient>['deviceRequests']['heartbeat']>[0];
4
+ export declare function credentialHeartbeat(ctx: Ctx, input: Input): Promise<{
5
+ device_id: string;
6
+ online_until: string;
7
+ }>;
8
+ export {};
@@ -0,0 +1,21 @@
1
+ /** Old APIs must keep accepting ordinary helper heartbeats during a rolling upgrade. */
2
+ import { ShardfluxApiError } from '@shardflux/sdk';
3
+ import { conversationClient } from "./conversation-commands.js";
4
+ const probes = new WeakMap();
5
+ export async function credentialHeartbeat(ctx, input) {
6
+ let probe = probes.get(ctx);
7
+ if (!probe || Date.now() - probe.at >= 30_000) {
8
+ probe = { at: Date.now(), pending: conversationClient(ctx).credentialSetups.available().catch(() => false) };
9
+ probes.set(ctx, probe);
10
+ }
11
+ const available = await probe.pending, cloud = conversationClient(ctx);
12
+ try {
13
+ return await cloud.deviceRequests.heartbeat({ ...input, ...(available ? { capabilities: ['credentials_setup.v1'] } : {}) });
14
+ }
15
+ catch (e) {
16
+ if (!available || !(e instanceof ShardfluxApiError && [400, 422].includes(e.status)))
17
+ throw e;
18
+ probes.set(ctx, { at: Date.now(), pending: Promise.resolve(false) });
19
+ return cloud.deviceRequests.heartbeat(input);
20
+ }
21
+ }
@@ -0,0 +1,12 @@
1
+ import type { CredentialSetup, CredentialSetupsApi } from '@shardflux/sdk';
2
+ import type { Ctx, Handler } from './commands.js';
3
+ import type { CredentialDiscoveryOptions } from './credential-sources.js';
4
+ type TransferContext = Pick<Ctx, 'json' | 'io' | 'signal' | 'addWriteOnlyValue'>;
5
+ type TransferClient = {
6
+ credentialSetups: Pick<CredentialSetupsApi, 'storeValue' | 'sourceResult' | 'get'>;
7
+ };
8
+ /** Onboarding opts in explicitly. The restricted machine agent never reads the source values. */
9
+ export declare function credentialSetupAfterMachine(ctx: Ctx): Promise<void>;
10
+ export declare function transferCredentialSetup(ctx: TransferContext, cloud: TransferClient, plan: CredentialSetup, options: CredentialDiscoveryOptions): Promise<CredentialSetup>;
11
+ export declare const CREDENTIAL_SETUP_HANDLERS: Record<string, Handler>;
12
+ export {};
@@ -0,0 +1,181 @@
1
+ /** Trusted local credential transfer. Models and stdout receive names, sources and status only. */
2
+ import { readFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { UsageError } from "./errors.js";
5
+ import { captureLocalLoginEnvironment, discoverCredentialSources, readCredentialSource, CredentialSourceError } from "./credential-sources.js";
6
+ const str = (value) => typeof value === 'string' ? value : undefined;
7
+ function client(ctx) { return ctx.sessionSource() ? ctx.account() : ctx.cloud(); }
8
+ /** Onboarding opts in explicitly. The restricted machine agent never reads the source values. */
9
+ export async function credentialSetupAfterMachine(ctx) {
10
+ try {
11
+ const cloud = client(ctx);
12
+ if (!cloud.credentialSetups || !await cloud.credentialSetups.available())
13
+ return;
14
+ const quiet = { ...ctx, json: true, io: { ...ctx.io, stdout: { ...ctx.io.stdout, write: () => true } } };
15
+ await CREDENTIAL_SETUP_HANDLERS['credentials setup'](quiet, { 'from-this-computer': true, 'if-needed': true }, []);
16
+ }
17
+ catch {
18
+ ctx.io.stderr.write('Credential setup is pending. Continue with shard credentials setup --from-this-computer --if-needed.\n');
19
+ }
20
+ }
21
+ const itemResult = (item) => ({ id: item.id, name: item.name, state: item.state, scope: item.selected_scope, project_id: item.project_id, ...(item.delivery ? { delivery: item.delivery } : {}), ...(item.error_code ? { error_code: item.error_code } : {}) });
22
+ function output(ctx, value) { if (ctx.json)
23
+ ctx.io.stdout.write(JSON.stringify(value) + '\n'); }
24
+ export async function transferCredentialSetup(ctx, cloud, plan, options) {
25
+ if (plan.state === 'awaiting_approval')
26
+ return plan;
27
+ if (plan.state === 'denied' || plan.state === 'completed')
28
+ return plan;
29
+ const items = plan.items.filter(i => i.state === 'pending' && i.selected_scope !== null);
30
+ const transfer = async (item) => {
31
+ try {
32
+ const { project_id: sourceProject, ...descriptor } = item;
33
+ const candidate = { ...descriptor, ...(sourceProject ? { project_id: sourceProject } : {}), preferred: true };
34
+ const value = await readCredentialSource(candidate, options);
35
+ ctx.addWriteOnlyValue(value);
36
+ const stored = await cloud.credentialSetups.storeValue(plan.id, item.id, value);
37
+ const result = stored.items.find(i => i.id === item.id);
38
+ if (result)
39
+ output(ctx, { plan_id: plan.id, result: itemResult(result) });
40
+ }
41
+ catch (err) {
42
+ const code = err instanceof CredentialSourceError ? err.code : 'import_failed';
43
+ try {
44
+ const failed = await cloud.credentialSetups.sourceResult(plan.id, item.id, code);
45
+ const result = failed.items.find(i => i.id === item.id);
46
+ if (result)
47
+ output(ctx, { plan_id: plan.id, result: itemResult(result) });
48
+ }
49
+ catch { /* Read authoritative status below; a lost write response may already have stored it. */ }
50
+ }
51
+ };
52
+ // Queue every approved source, while opening only a few vault readers/uploads together.
53
+ // Read the value inside the worker so queued credentials remain in their original source.
54
+ let next = 0;
55
+ await Promise.all(Array.from({ length: Math.min(4, items.length) }, async () => {
56
+ for (;;) {
57
+ const item = items[next++];
58
+ if (!item)
59
+ return;
60
+ await transfer(item);
61
+ }
62
+ }));
63
+ return cloud.credentialSetups.get(plan.id, { signal: ctx.signal });
64
+ }
65
+ async function picks(ctx, path) {
66
+ let value;
67
+ if (path === '-') {
68
+ const chunks = [];
69
+ for await (const chunk of ctx.io.stdin)
70
+ chunks.push(Buffer.from(chunk));
71
+ value = Buffer.concat(chunks).toString('utf8');
72
+ }
73
+ else
74
+ value = await readFile(path, 'utf8');
75
+ try {
76
+ const parsed = JSON.parse(value);
77
+ if (!Array.isArray(parsed) || !parsed.every((item) => {
78
+ if (!item || typeof item !== 'object' || Array.isArray(item))
79
+ return false;
80
+ const i = item;
81
+ return typeof i.id === 'string' && (i.scope === 'agents' || i.scope === 'project') && (i.project_id === undefined || typeof i.project_id === 'string') && Object.keys(i).every(k => ['id', 'scope', 'project_id'].includes(k));
82
+ }))
83
+ throw new Error();
84
+ return parsed;
85
+ }
86
+ catch {
87
+ throw new UsageError('--picks takes a JSON list of discovered source IDs and scopes.');
88
+ }
89
+ }
90
+ export const CREDENTIAL_SETUP_HANDLERS = {
91
+ async 'credentials setup'(ctx, values) {
92
+ const cloud = client(ctx), apply = str(values.apply), deny = str(values.deny), path = str(values.path);
93
+ if (!await cloud.credentialSetups.available())
94
+ throw new UsageError('Credential setup is being prepared. Update Shardflux and try again.');
95
+ if (apply && deny)
96
+ throw new UsageError('Choose --apply or --deny.');
97
+ if (deny) {
98
+ await cloud.needsYou.answer('csetup_' + deny, { decision: 'deny' });
99
+ output(ctx, { plan_id: deny, state: 'denied' });
100
+ if (!ctx.json)
101
+ ctx.io.stdout.write('Credential setup declined.\n');
102
+ return 0;
103
+ }
104
+ if (!apply && values['from-this-computer'] !== true)
105
+ throw new UsageError('Use --from-this-computer to bring your existing credentials along.');
106
+ let projectId = str(values.project) ?? ctx.store.profile(ctx.apiUrl).project_id, organizationId = ctx.store.profile(ctx.apiUrl).organization_id;
107
+ if (!projectId && !apply) {
108
+ try {
109
+ const me = await ctx.cloud().me();
110
+ if (!me.api_key)
111
+ throw new UsageError('Select a project with --project.');
112
+ projectId = me.api_key.project_id;
113
+ organizationId ??= me.api_key.organization_id;
114
+ }
115
+ catch {
116
+ if (!ctx.sessionSource())
117
+ throw new UsageError('Select a project with --project, or sign in first.');
118
+ }
119
+ }
120
+ const home = ctx.io.env.HOME ?? homedir();
121
+ const base = { home, env: ctx.io.env, ...(projectId ? { projectId, projectPath: path ?? process.cwd() } : {}), signal: ctx.signal };
122
+ const env = await captureLocalLoginEnvironment(base), options = { ...base, env };
123
+ let plan, prior = new Map();
124
+ if (apply) {
125
+ const selection = str(values.picks);
126
+ if (selection)
127
+ await cloud.needsYou.answer('csetup_' + apply, { credential_setup: { items: await picks(ctx, selection) } });
128
+ plan = await cloud.credentialSetups.get(apply, { signal: ctx.signal });
129
+ }
130
+ else {
131
+ const candidates = await discoverCredentialSources(options), localOnly = !ctx.sessionSource();
132
+ const normalized = (localOnly ? [...candidates].sort((a, b) => Number(b.suggested_scope === 'project') - Number(a.suggested_scope === 'project')) : candidates).map(c => ({ ...c, ...(localOnly ? { suggested_scope: 'project', project_id: projectId } : {}) }));
133
+ if (localOnly && !projectId)
134
+ throw new UsageError('Select a project with --project.');
135
+ const selected = new Set(), selectedItems = [];
136
+ for (const c of normalized)
137
+ if (c.preferred) {
138
+ const key = JSON.stringify([c.suggested_scope, c.project_id, c.name]);
139
+ if (!selected.has(key)) {
140
+ selected.add(key);
141
+ selectedItems.push(c.id);
142
+ }
143
+ }
144
+ const items = normalized.map(({ id, name, source, suggested_scope, project_id }) => ({ id, name, source, suggested_scope, ...(project_id ? { project_id } : {}) }));
145
+ if (!items.length && values['if-needed'] === true) {
146
+ output(ctx, { needed: false, results: [] });
147
+ if (!ctx.json)
148
+ ctx.io.stdout.write('No new credential sources found.\n');
149
+ return 0;
150
+ }
151
+ try {
152
+ const metadata = await cloud.secrets.listAgents({ ...organizationId ? { organizationId } : {} });
153
+ prior = new Map(metadata.map(s => [s.id, s.current_version]));
154
+ }
155
+ catch { /* An unavailable metadata read does not block the direct transfer. */ }
156
+ plan = await cloud.credentialSetups.create({ items, ...(projectId ? { projectId } : {}), ...(organizationId ? { organizationId } : {}), review: values.plan === true, ...(values.plan === true ? {} : { selectedItems }) });
157
+ if (!prior.size)
158
+ try {
159
+ prior = new Map((await cloud.secrets.listAgents({ organizationId: plan.organization_id })).map(s => [s.id, s.current_version]));
160
+ }
161
+ catch { /* Transfers remain independent of this optional metadata read. */ }
162
+ output(ctx, { plan_id: plan.id, ...(values.plan === true ? { candidates: plan.items } : {}) });
163
+ if (values.plan === true) {
164
+ if (!ctx.json)
165
+ ctx.io.stdout.write('Review the discovered names in Shard, then import the selected sources.\n');
166
+ return 0;
167
+ }
168
+ }
169
+ if (plan.state === 'awaiting_approval') {
170
+ output(ctx, { plan_id: plan.id, state: plan.state, results: plan.items.map(itemResult) });
171
+ return 0;
172
+ }
173
+ const final = await transferCredentialSetup(ctx, cloud, plan, options), failed = final.items.filter(i => i.state === 'failed').length, pending = final.items.filter(i => i.state === 'pending' || i.state === 'stored' && i.delivery?.state !== 'ready').length;
174
+ const stored = final.items.filter(i => i.state === 'stored'), needed = stored.some(i => !i.secret_id || prior.get(i.secret_id) !== i.secret_version);
175
+ const result = { plan_id: final.id, state: final.state, needed, results: final.items.map(itemResult), stored: stored.length, failed, pending };
176
+ output(ctx, result);
177
+ if (!ctx.json)
178
+ ctx.io.stdout.write(`${stored.length} credential${stored.length === 1 ? '' : 's'} stored${failed ? `, ${failed} source${failed === 1 ? '' : 's'} need attention` : ''}${pending ? `, ${pending} delivery pending` : ''}.\n`);
179
+ return failed ? 1 : 0;
180
+ },
181
+ };
@@ -0,0 +1,34 @@
1
+ export type CredentialSourceCode = 'source_unavailable' | 'source_changed' | 'source_not_text' | 'source_empty' | 'source_too_large' | 'source_has_nul' | 'vault_locked' | 'login_expired' | 'import_failed';
2
+ export declare class CredentialSourceError extends Error {
3
+ readonly code: CredentialSourceCode;
4
+ constructor(code: CredentialSourceCode);
5
+ }
6
+ export interface CredentialCandidate {
7
+ id: string;
8
+ name: string;
9
+ source: {
10
+ kind: 'environment' | 'dotenv' | 'shell' | 'vault' | 'cli';
11
+ label: string;
12
+ reference: string;
13
+ };
14
+ suggested_scope: 'agents' | 'project';
15
+ project_id?: string;
16
+ preferred: boolean;
17
+ }
18
+ export interface CredentialDiscoveryOptions {
19
+ home: string;
20
+ env: Readonly<NodeJS.ProcessEnv>;
21
+ projectPath?: string;
22
+ projectId?: string;
23
+ signal?: AbortSignal;
24
+ /** Fixture-injected process boundary. stdout is private; stderr is never returned. */
25
+ run?: (argv: readonly string[], env: Readonly<NodeJS.ProcessEnv>, signal?: AbortSignal) => Promise<Buffer>;
26
+ /** Tests declare the fixture executables. Production resolves only the caller's PATH. */
27
+ availableCommands?: readonly string[];
28
+ }
29
+ export declare function credentialVariableName(name: string): boolean;
30
+ /** Normal local login startup resolves exported vault expressions privately, without a model reading shell files. */
31
+ export declare function captureLocalLoginEnvironment(options: CredentialDiscoveryOptions): Promise<NodeJS.ProcessEnv>;
32
+ /** Reconstruct a source from metadata only, so a review can finish in a later CLI process. */
33
+ export declare function readCredentialSource(candidate: CredentialCandidate, options: CredentialDiscoveryOptions): Promise<string>;
34
+ export declare function discoverCredentialSources(options: CredentialDiscoveryOptions): Promise<CredentialCandidate[]>;