@shardflux/cli 0.20.0 → 0.23.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +312 -1
  2. package/README.md +242 -75
  3. package/dist/account-args.js +7 -2
  4. package/dist/account-commands.js +56 -106
  5. package/dist/account-pool.d.ts +19 -15
  6. package/dist/account-pool.js +47 -96
  7. package/dist/account-scope.d.ts +65 -0
  8. package/dist/account-scope.js +246 -0
  9. package/dist/acp-journal.js +58 -6
  10. package/dist/acp-login.d.ts +13 -5
  11. package/dist/acp-login.js +37 -8
  12. package/dist/acp-models.d.ts +6 -1
  13. package/dist/acp-models.js +29 -19
  14. package/dist/acp.js +120 -14
  15. package/dist/agent-commands.d.ts +4 -1
  16. package/dist/agent-commands.js +110 -42
  17. package/dist/agent-config-return.d.ts +46 -2
  18. package/dist/agent-config-return.js +110 -4
  19. package/dist/agent-credentials.js +7 -3
  20. package/dist/agent-repository.d.ts +16 -3
  21. package/dist/agent-repository.js +149 -74
  22. package/dist/agent-setup.d.ts +14 -3
  23. package/dist/agent-setup.js +78 -36
  24. package/dist/agent-target-commands.d.ts +20 -1
  25. package/dist/agent-target-commands.js +73 -31
  26. package/dist/agent-targets.js +6 -1
  27. package/dist/args.d.ts +1 -1
  28. package/dist/args.js +95 -54
  29. package/dist/bin.js +5 -1
  30. package/dist/browser.d.ts +13 -2
  31. package/dist/browser.js +30 -14
  32. package/dist/claude-cloud-login.d.ts +24 -2
  33. package/dist/claude-cloud-login.js +151 -11
  34. package/dist/cli-proxy-hub.d.ts +61 -25
  35. package/dist/cli-proxy-hub.js +202 -54
  36. package/dist/cli-proxy.d.ts +44 -1
  37. package/dist/cli-proxy.js +177 -28
  38. package/dist/client-settings.d.ts +39 -0
  39. package/dist/client-settings.js +91 -0
  40. package/dist/codex-cloud.d.ts +14 -4
  41. package/dist/codex-cloud.js +135 -31
  42. package/dist/codex-linux.d.ts +14 -0
  43. package/dist/codex-linux.js +69 -0
  44. package/dist/command-center-actions.js +18 -5
  45. package/dist/command-center-model.d.ts +2 -0
  46. package/dist/command-center-model.js +6 -1
  47. package/dist/command-center.js +3 -3
  48. package/dist/commands.js +173 -24
  49. package/dist/conversation-commands.d.ts +5 -0
  50. package/dist/conversation-commands.js +8 -5
  51. package/dist/conversation-terminal.d.ts +10 -2
  52. package/dist/conversation-terminal.js +78 -13
  53. package/dist/conversations.d.ts +86 -1
  54. package/dist/conversations.js +306 -23
  55. package/dist/desktop-linux.js +16 -3
  56. package/dist/device-bridge.d.ts +1 -1
  57. package/dist/device-bridge.js +6 -4
  58. package/dist/device-helper.js +43 -22
  59. package/dist/errors.d.ts +29 -2
  60. package/dist/errors.js +50 -7
  61. package/dist/filesystem-commands.d.ts +16 -9
  62. package/dist/filesystem-commands.js +133 -57
  63. package/dist/format.d.ts +2 -1
  64. package/dist/format.js +10 -1
  65. package/dist/handlers.js +3 -1
  66. package/dist/initial-prompt.d.ts +8 -0
  67. package/dist/initial-prompt.js +43 -1
  68. package/dist/large-media.d.ts +14 -0
  69. package/dist/large-media.js +100 -0
  70. package/dist/linux-terminal.d.ts +5 -3
  71. package/dist/linux-terminal.js +6 -20
  72. package/dist/local-sessions.d.ts +26 -2
  73. package/dist/local-sessions.js +113 -24
  74. package/dist/machine-setup.d.ts +11 -1
  75. package/dist/machine-setup.js +69 -22
  76. package/dist/machine-status.d.ts +67 -0
  77. package/dist/machine-status.js +140 -0
  78. package/dist/main.js +11 -13
  79. package/dist/menubar.d.ts +6 -5
  80. package/dist/menubar.js +38 -28
  81. package/dist/move-worktrees-restore.py +144 -0
  82. package/dist/move-worktrees.d.ts +69 -0
  83. package/dist/move-worktrees.js +252 -0
  84. package/dist/move.d.ts +10 -1
  85. package/dist/move.js +111 -16
  86. package/dist/onboarding.d.ts +11 -7
  87. package/dist/onboarding.js +63 -29
  88. package/dist/prepared-repository.d.ts +204 -0
  89. package/dist/prepared-repository.js +860 -0
  90. package/dist/prepared-repository.py +1307 -0
  91. package/dist/project-preparation.d.ts +62 -0
  92. package/dist/project-preparation.js +316 -0
  93. package/dist/project-sync-manifest.d.ts +4 -0
  94. package/dist/project-sync-manifest.js +5 -1
  95. package/dist/project-sync-transfer.js +1 -1
  96. package/dist/project-sync.d.ts +22 -4
  97. package/dist/project-sync.js +34 -8
  98. package/dist/project-template-setup.d.ts +33 -1
  99. package/dist/project-template-setup.js +59 -2
  100. package/dist/secret-ask.d.ts +72 -0
  101. package/dist/secret-ask.js +351 -0
  102. package/dist/seed-phases.d.ts +8 -1
  103. package/dist/seed-phases.js +17 -2
  104. package/dist/setup-cache.d.ts +7 -0
  105. package/dist/setup-cache.js +32 -0
  106. package/dist/t3-launch.d.ts +2 -1
  107. package/dist/t3-launch.js +12 -6
  108. package/dist/t3-mode.d.ts +8 -2
  109. package/dist/t3-mode.js +19 -4
  110. package/dist/t3-phone.d.ts +4 -2
  111. package/dist/t3-phone.js +12 -4
  112. package/dist/t3-proxy.d.ts +81 -0
  113. package/dist/t3-proxy.js +325 -0
  114. package/dist/terminal-apps.d.ts +168 -0
  115. package/dist/terminal-apps.js +574 -0
  116. package/dist/terminal-onboarding.js +6 -1
  117. package/dist/this-computer.d.ts +11 -0
  118. package/dist/this-computer.js +36 -0
  119. package/dist/thread-on-computer.js +4 -1
  120. package/dist/update.d.ts +2 -0
  121. package/dist/update.js +43 -9
  122. package/dist/worktree-twin.d.ts +10 -2
  123. package/dist/worktree-twin.js +27 -9
  124. package/package.json +5 -5
package/dist/args.js CHANGED
@@ -13,7 +13,7 @@ import { parseArgs } from 'node:util';
13
13
  import { ACCOUNT_COMMANDS } from "./account-args.js";
14
14
  import { UsageError } from "./errors.js";
15
15
  import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
16
- export const CLI_VERSION = '0.20.0';
16
+ export const CLI_VERSION = '0.23.0';
17
17
  export const GLOBAL_OPTIONS = {
18
18
  'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
19
19
  json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
@@ -110,18 +110,14 @@ const FEEDBACK_HELP = [
110
110
  ];
111
111
  /**
112
112
  * Why `shard feedback` exists, for the coding agents that drive shard: the same words as the top-level help and the
113
- * MCP server's send_feedback tool. It has to make an agent actually send feedback while it works.
113
+ * MCP server's send_feedback tool. Reporting is user-directed and requires approval of the message.
114
114
  */
115
115
  const FEEDBACK_WHY = [
116
- 'Sends your message straight to the Shardflux team, who read every one. Agents: use it actively while you work,',
117
- 'not once at the end. Send a short note the moment:',
118
- '- something fails unexpectedly, an error or doc is unclear, an option is missing, or you needed a workaround;',
119
- '- your user asks for a capability, an option or a smoother workflow: paraphrase it, with what they were trying',
120
- ' to do;',
121
- '- your user has an idea for how Shardflux could fit their work better.',
122
- 'Short and frequent beats polished; include the request id and error code (a failed shard command prints a',
123
- 'ready-made "feedback:" line to fill in). No need to ask first, but tell your user you sent it. Leave out private',
124
- 'data about the user and secrets or code they did not mean to share; paraphrase, never paste transcripts.',
116
+ 'Previews a message for the Shardflux team without sending it. Use --yes to send only after approval.',
117
+ 'Agents: report only when your user asks you to send feedback. Show the exact message and context first,',
118
+ 'then obtain explicit authorization before using --yes. A failure or request for a feature is not consent.',
119
+ 'Include the request id and error code. Leave out private data, secrets and code the user did not mean to',
120
+ 'share; paraphrase, never paste transcripts. Tell your user what was sent.',
125
121
  ];
126
122
  const LIFETIME_FILTER = {
127
123
  lifetime: { type: 'string', value: '<persistent|session|any>', description: 'Lifetime filter (default persistent: sessions are hidden).' },
@@ -132,8 +128,12 @@ const SECRET_SCOPE = {
132
128
  org: { type: 'string', value: '<organization-id>', description: 'Organization-wide secrets of this organization (owners/admins; project API keys get 403). Default: the API key’s project.' },
133
129
  };
134
130
  const SECRET_VALUE = {
135
- 'from-file': { type: 'string', value: '<file>', description: 'Read the value from this file. Default: standard input (pipe it in). One trailing newline is removed. Values are never accepted as arguments.' },
131
+ 'from-file': { type: 'string', value: '<file>', description: 'Read the value from this file. Default: standard input (pipe it in), a hidden prompt on a terminal, or the --ask page in a coding agent’s shell. One trailing newline is removed. Values are never accepted as arguments.' },
132
+ ask: { type: 'boolean', description: 'Your user types the value in a page shard opens in the browser; it goes straight to the API, never through the coding agent (0.20.1+). The default in a coding agent’s shell when nothing is piped.' },
133
+ timeout: { type: 'string', value: '<duration>', description: 'How long the --ask page waits (default 10m).' },
136
134
  };
135
+ /** What --ask prints (0.20.1+), for the help of each command that asks. */
136
+ const ASK_EVENTS = 'The page’s link is printed too ("Opened a page in your browser to type …"); with --json the first line is {"event":"open","url":…,"opened":true|false,"expires_in":600}, then the result once the page was submitted.';
137
137
  const SECRET_PERMISSIONS = {
138
138
  tool: { type: 'string', multiple: true, value: '<tool>', description: 'Tool that receives the value at session start (repeatable; exec, pty, files, process, git, browser). Default exec and pty.' },
139
139
  'allow-workspace': { type: 'string', multiple: true, value: '<workspace-id>', description: 'Only these workspaces may use it (repeatable). Default: any workspace of the allowed projects.' },
@@ -142,8 +142,8 @@ const SECRET_PERMISSIONS = {
142
142
  };
143
143
  // Cloud agents: a VM is the coding template, then the machine, then the project template.
144
144
  const LAYER_HELP = {
145
- machine: 'Your machine is what every cloud agent starts with: tools, credentials, dotfiles, browser logins, git identity, Claude/Codex settings and skills. "shard machine" opens its VM; change it there (or in any agent’s VM), then run "shard machine save". It is saved from an agent’s VM, so it always keeps Claude Code, Codex and the agent runtime.',
146
- template: 'The project template is this repository’s layer: toolchains, dependencies, services and project secrets. Setup after "shard ./" saves it; "shard template save" saves the current agent VM’s project changes. It is saved from an agent’s VM; "shard templates" builds templates for your own app’s workspaces instead.',
145
+ machine: 'Your machine is what every cloud agent starts with: tools, credentials, dotfiles, browser logins, git identity, Claude/Codex settings and skills. On your computer, "shard machine open --json" opens its source VM; change that VM, then run "shard machine save --wait". The computer’s CLI selects the machine VM, regardless of your current folder or conversation. Inside a VM, save uses that VM and requires a machine source; a conversation or unmarked fork returns filesystem_source_mismatch. To keep changes from another VM, copy the intended files or repeat the install in the machine VM, then save it. A machine source keeps Claude Code, Codex and the agent runtime.',
146
+ template: 'The project template is this repository’s layer: toolchains, dependencies, services and project secrets. Setup after "shard ./" saves it; "shard template save" saves the current agent VM’s project changes, and "shard template delete" starts the project over from your machine. It is saved from an agent’s VM; "shard templates" builds templates for your own app’s workspaces instead.',
147
147
  };
148
148
  const KEY_COMMANDS = [
149
149
  { path: ['conversations', 'list'], summary: 'List project conversations', positionals: [], options: {
@@ -162,19 +162,23 @@ const KEY_COMMANDS = [
162
162
  { path: ['conversations', 'landing'], summary: 'Show or set how finished conversations reach this checkout', positionals: [{ name: 'mode' }, { name: 'backup' }], options: {
163
163
  path: { type: 'string', value: '<path>', description: 'Local project checkout (default the current directory).' }
164
164
  },
165
- description: 'While the macOS app runs, a conversation whose turn ends with committed work lands in the local branch it started from: the branch moves to its commits and the changed files are written, keeping your uncommitted edits (auto, the default). pr opens a pull request instead; off leaves conversations in the cloud. Every file a landing rewrites is backed up first: backups lists them, restore <backup> puts the files back.',
165
+ description: 'While the Shardflux app runs (the menu bar on macOS, the background helper on Linux), a conversation whose turn ends with committed work lands in the local branch it started from: the branch moves to its commits and the changed files are written, keeping your uncommitted edits (auto, the default). pr opens a pull request instead; off leaves conversations in the cloud. Every file a landing rewrites is backed up first: backups lists them, restore <backup> puts the files back.',
166
166
  examples: ['shard conversations landing', 'shard conversations landing pr', 'shard conversations landing backups', 'shard conversations landing restore 2026-10-09T12-00-00-000Z-fix-login-3fa2c1'] },
167
167
  ...['machine', 'template'].flatMap((level) => [
168
168
  { path: [level], summary: level === 'machine' ? 'Open your machine agent, or show its saved filesystem with --json' : 'Show this project’s template filesystem', positionals: [], options: {},
169
169
  description: LAYER_HELP[level] },
170
170
  { path: [level, 'save'], summary: `Save the ${level} filesystem as a new private version`, description: LAYER_HELP[level], positionals: [], options: {
171
- wait: { type: 'boolean', description: 'Return when the new version is ready (or failed), printing progress on stderr.' },
171
+ wait: { type: 'boolean', description: 'Return when the new version is ready (or failed), printing each phase and its progress on stderr (0.20.2+; with --json as {"event":"save_progress"} lines, and `phases` in the result).' },
172
172
  timeout: { type: 'string', value: '<duration>', description: 'With --wait: give up waiting after this long (default 10m). The save continues server side.' },
173
173
  } },
174
174
  { path: [level, 'rollback'], summary: `Restore a saved ${level} version`, positionals: [], options: { to: { type: 'string', value: '<version>', description: 'Version to restore (default previous saved version).' } } },
175
+ ...(level === 'template' ? [{ path: [level, 'delete'], summary: 'Delete this project’s template and its saved versions (0.20.1+)', positionals: [], options: {
176
+ yes: { type: 'boolean', description: 'Skip the confirmation.' },
177
+ }, description: 'Deletes the project template of this folder’s repository: its saved versions, binding and setup state. It is no longer rebuilt when your machine or the platform changes; new conversations start from your machine until setup after "shard ./" (or "shard template save") saves a new one. VMs running on it keep their files, and builds still running for it are stopped. In a terminal it asks first; elsewhere it needs --yes.',
178
+ examples: ['shard template delete', 'shard template delete --yes --json'] }] : []),
175
179
  ]),
176
180
  { path: ['machine', 'open'], summary: 'Create or resume your machine VM without attaching to it, and print its workspace key (0.17.1+)', positionals: [], options: {},
177
- description: 'For an agent building your machine from a terminal it does not attach to: install with "shard ws exec <key> -- bash -lc ...", write dotfiles and secret files with "shard files write <key> <path> --from -" (contents on standard input), then "shard machine save --wait". The first open copies your Claude and Codex settings into it, as "shard machine" does.',
181
+ description: 'For an agent building your machine from a terminal it does not attach to: install with "shard ws exec <key> -- bash -lc ...", write dotfiles with "shard files write <key> <path> --from -" (contents on standard input) and secret files with --ask (your user types them in a page shard opens; 0.20.1+), then "shard machine save --wait". The first open copies your Claude and Codex settings into it, as "shard machine" does.',
178
182
  examples: ['shard machine open --json', "shard ws exec \"$(shard machine open)\" -- bash -lc 'sudo apt-get install -y ripgrep'", 'shard machine save --wait --json'] },
179
183
  { path: ['machine', 'setup'], summary: 'Let your own Claude Code or Codex set up your machine from this computer (0.20.0+)', positionals: [], options: {
180
184
  '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.' },
@@ -200,13 +204,13 @@ const KEY_COMMANDS = [
200
204
  { path: ['whoami'], summary: 'Show the principal behind the project API key (SHARDFLUX_API_KEY, else the saved one)', positionals: [], options: {} },
201
205
  {
202
206
  path: ['update'],
203
- summary: 'Update shard, the menu bar app and a global MCP server to their latest versions',
207
+ summary: 'Update shard, the desktop app and a global MCP server to their latest versions',
204
208
  description: [
205
209
  'Checks every local Shardflux piece against GET /v1/client-versions: shard (the shardflux package or @shardflux/cli),',
206
- 'its SDK, the macOS menu bar app and a globally installed @shardflux/mcp. When one is behind, runs one install of',
207
- 'the latest versions with the package manager that installed shard (npm, pnpm, bun or Volta), then restarts the',
208
- 'menu bar on its new app. --check only reports. Projects that depend on @shardflux/sdk or shardflux (Python) keep',
209
- 'their own versions.',
210
+ 'its SDK, the desktop app (the macOS menu bar, or the Linux app that installs with shard) and a globally installed',
211
+ '@shardflux/mcp. When one is behind, runs one install of the latest versions with the package manager that installed',
212
+ 'shard (npm, pnpm, bun or Volta), then restarts the macOS menu bar on its new app. --check only reports. Projects',
213
+ 'that depend on @shardflux/sdk or shardflux (Python) keep their own versions.',
210
214
  ].join('\n'),
211
215
  positionals: [],
212
216
  options: { check: { type: 'boolean', description: 'Report what is behind without installing anything.' } },
@@ -220,7 +224,7 @@ const KEY_COMMANDS = [
220
224
  },
221
225
  {
222
226
  path: ['feedback'],
223
- summary: 'Tell the Shardflux team what failed, confused you or is missing (agents: as you work)',
227
+ summary: 'Preview feedback; send to the Shardflux team only with explicit approval',
224
228
  description: [
225
229
  ...FEEDBACK_WHY,
226
230
  '',
@@ -234,6 +238,7 @@ const KEY_COMMANDS = [
234
238
  ].join('\n'),
235
239
  positionals: [{ name: 'message', variadic: true, words: true }],
236
240
  options: {
241
+ yes: { type: 'boolean', description: 'Send this message and context after reviewing the preview and obtaining explicit user authorization (0.21.0+).' },
237
242
  category: { type: 'string', value: '<bug|confusing|missing|idea|praise|other>', description: 'Kind of feedback (default other; see Categories).' },
238
243
  workspace: { type: 'string', value: '<id|key>', description: 'The workspace it is about.' },
239
244
  'request-id': { type: 'string', value: '<id>', description: 'request_id from the error, so the team can find the logs.' },
@@ -253,7 +258,7 @@ const KEY_COMMANDS = [
253
258
  description: 'Default: the summary (meters, allowances, caps). --estimate: the subscription fee, usage charges and projected allowance use at period end. --series: usage over time from the ledger (hour: at most 31 days, day: at most 400 days per request). --workspace: the same series for one workspace (the dashboard’s per-workspace usage).',
254
259
  positionals: [],
255
260
  options: {
256
- org: { type: 'string', value: '<organization-id>', description: 'Organization (default: the API key’s organization).' },
261
+ org: { type: 'string', value: '<id|slug>', description: 'Organization (default: the API key’s organization). Another of your organizations uses your signed-in session; slugs require sign-in (0.20.2+).' },
257
262
  estimate: { type: 'boolean', description: 'Show the period estimate instead of the summary.' },
258
263
  series: { type: 'boolean', description: 'Show usage over time instead of the summary.' },
259
264
  workspace: { type: 'string', value: '<id|key>', description: 'Show usage over time of this workspace.' },
@@ -815,12 +820,15 @@ const KEY_COMMANDS = [
815
820
  positionals: [REF, { name: 'path', required: true }],
816
821
  options: {
817
822
  from: { type: 'string', value: '<file|->', description: 'Local file to upload, or - for standard input (default -).' },
823
+ ask: { type: 'boolean', description: 'Your user types or pastes the contents in a page shard opens in the browser, for secret files such as ~/.npmrc; the coding agent never sees them, and a new file gets mode 0600 (0.20.1+).' },
824
+ timeout: { type: 'string', value: '<duration>', description: 'How long the --ask page waits (default 10m).' },
818
825
  append: { type: 'boolean', description: 'Append instead of replacing.' },
819
826
  parents: { type: 'boolean', description: 'Create missing parent directories (default; --no-parents to refuse).' },
820
827
  ...IF_REVISION,
821
828
  ...WAKE_TIMING,
822
829
  },
823
- examples: ['shard files write acme/demo /home/user/notes.txt --from notes.txt', 'printf "x = 1\\n" | shard files write acme/agent-7 /home/user/app/config.py --if-revision 4'],
830
+ description: `--ask (0.20.1+) asks your user for a secret file’s contents on a page on this computer; the page says Saved once the file is written. ${ASK_EVENTS}`,
831
+ examples: ['shard files write acme/demo /home/user/notes.txt --from notes.txt', 'printf "x = 1\\n" | shard files write acme/agent-7 /home/user/app/config.py --if-revision 4', 'shard files write "$(shard machine open)" /home/user/.npmrc --ask --json'],
824
832
  },
825
833
  {
826
834
  path: ['files', 'ls'],
@@ -872,7 +880,7 @@ const KEY_COMMANDS = [
872
880
  {
873
881
  path: ['files', 'pull'],
874
882
  summary: 'Download a large file a conversation committed into ~/Shardflux',
875
- description: 'Each conversation’s committed changes appear in ~/Shardflux/<repository>/<base branch>/<conversation>/ while the macOS app runs. Files above the size limit are listed there as <file>.placeholder.txt; this downloads one in its place. <conversation> is its name, id or folder name.',
883
+ description: 'Each conversation’s committed changes appear in ~/Shardflux/<repository>/<base branch>/<conversation>/ while the Shardflux app runs (the menu bar on macOS, the background helper on Linux). Files above the size limit are listed there as <file>.placeholder.txt; this downloads one in its place. <conversation> is its name, id or folder name.',
876
884
  positionals: [{ name: 'conversation', required: true }, { name: 'path', required: true }],
877
885
  options: {},
878
886
  examples: ['shard files pull hype-video out/hype.mp4'],
@@ -888,6 +896,14 @@ const KEY_COMMANDS = [
888
896
  },
889
897
  },
890
898
  { path: ['operations', 'get'], summary: 'Show one operation', positionals: [{ name: 'operation-id', required: true }], options: {} },
899
+ { path: ['operations', 'progress'], summary: 'Read suspension preparation and capture progress without waking the workspace',
900
+ positionals: [{ name: 'operation-id', required: true }], options: {} },
901
+ { path: ['operations', 'cancel'], summary: 'Cancel suspension preparation before capture and retain the original workspace',
902
+ description: 'Only host-acknowledged pre-capture cancellation is accepted. Without --wait this is a request; poll the same operation for recovery. After capture starts, wait for the suspension outcome and resume. No reset or replacement.',
903
+ positionals: [{ name: 'operation-id', required: true }], options: {
904
+ wait: { type: 'boolean', description: 'Wait for confirmed original-workspace recovery (suspend_canceled).' },
905
+ timeout: { type: 'string', value: '<duration>', description: 'With --wait, give up after this long (default 5m); recovery continues server side.' },
906
+ } },
891
907
  {
892
908
  path: ['executions', 'get'],
893
909
  summary: 'Fetch the result of a file-first workspace’s execution (exit: its command’s code; 6 failed/lost; 5 still running)',
@@ -920,11 +936,11 @@ const KEY_COMMANDS = [
920
936
  },
921
937
  {
922
938
  path: ['secrets', 'create'],
923
- summary: 'Create a secret; the value is read from standard input or --from-file, never from arguments',
924
- description: 'Project secret of the API key’s project by default, or an organization-wide secret with --org. The value (UTF-8 text, up to 64 KiB) is encrypted and never shown again. Bind it to workspaces with "shard ws open --secret NAME" or "shard ws secrets <ws> --set NAME".',
939
+ summary: 'Create a secret (the value from the --ask page, standard input or --from-file; never from arguments)',
940
+ description: `Project secret of the API key’s project by default, or an organization-wide secret with --org. The value (UTF-8 text, up to 64 KiB) is encrypted and never shown again. Bind it to workspaces with "shard ws open --secret NAME" or "shard ws secrets <ws> --set NAME". With --ask (0.20.1+; the default in a coding agent’s shell when nothing is piped) your user types the value in a page on this computer and the coding agent never sees it; the page says Saved once the API stored it. ${ASK_EVENTS}`,
925
941
  positionals: [{ name: 'name', required: true }],
926
942
  options: { ...SECRET_SCOPE, ...SECRET_VALUE, description: { type: 'string', value: '<text>', description: 'Description (up to 500 characters).' }, ...SECRET_PERMISSIONS },
927
- examples: ['printf %s "$OPENAI_API_KEY" | shard secrets create OPENAI_API_KEY', 'shard secrets create DATABASE_URL --from-file ./database-url.txt --description "Staging database"'],
943
+ examples: ['shard secrets create GITHUB_TOKEN --ask --json', 'printf %s "$OPENAI_API_KEY" | shard secrets create OPENAI_API_KEY', 'shard secrets create DATABASE_URL --from-file ./database-url.txt --description "Staging database"'],
928
944
  },
929
945
  {
930
946
  path: ['secrets', 'list'],
@@ -953,10 +969,11 @@ const KEY_COMMANDS = [
953
969
  },
954
970
  {
955
971
  path: ['secrets', 'rotate'],
956
- summary: 'Store a new value as the next version (standard input or --from-file); older values are erased',
972
+ summary: 'Store a new value as the next version (the --ask page, standard input or --from-file); older values are erased',
973
+ description: `With --ask (0.20.1+; the default in a coding agent’s shell when nothing is piped) your user types the new value in a page on this computer and the coding agent never sees it. ${ASK_EVENTS}`,
957
974
  positionals: [SECRET_REF],
958
975
  options: { ...SECRET_SCOPE, ...SECRET_VALUE },
959
- examples: ['printf %s "$NEW_TOKEN" | shard secrets rotate API_TOKEN'],
976
+ examples: ['shard secrets rotate GITHUB_TOKEN --ask --json', 'printf %s "$NEW_TOKEN" | shard secrets rotate API_TOKEN'],
960
977
  },
961
978
  {
962
979
  path: ['secrets', 'versions'],
@@ -1191,24 +1208,28 @@ const KEY_COMMANDS = [
1191
1208
  /** Copied sessions are project-key commands; --json is the normal global output option. */
1192
1209
  const AGENT_CREDENTIAL_COMMANDS = [
1193
1210
  ...['sync', 'status', 'forget'].map((action) => ({ path: ['agents', 'credentials', action], summary: `${action} Codex copies and the account's cloud Claude login (contents never printed)`, positionals: [], options: {} })),
1194
- { path: ['agents', 'credentials', 'login'], summary: 'Sign in once to cloud Claude for this account', positionals: [{ name: 'app', required: true }], options: { workspace: { type: 'string', value: '<id|key>', description: 'Use this workspace for the cloud sign-in.' } } },
1211
+ { path: ['agents', 'credentials', 'login'], summary: 'Sign in once to cloud Claude for this account', positionals: [{ name: 'app', required: true }], options: {
1212
+ workspace: { type: 'string', value: '<id|key>', description: 'Use this workspace for the cloud sign-in.' },
1213
+ ask: { type: 'boolean', description: 'Use the browser pages even on a terminal (0.20.1+).' },
1214
+ timeout: { type: 'string', value: '<duration>', description: 'How long the sign-in waits for the code (default 10m).' },
1215
+ },
1216
+ description: 'On a terminal the Claude sign-in runs attached to it: open the claude.ai link, then paste its code. In a coding agent’s shell, with --json or without a terminal (0.20.1+) shard opens the claude.ai sign-in and a page on this computer where your user pastes the code, so the coding agent runs it itself and never sees the code. The links are printed too; with --json the first lines are {"event":"open","url":…} (claude.ai) and {"event":"ask","url":…} (the page), then the login.',
1217
+ examples: ['shard agents credentials login claude --json'] },
1195
1218
  ];
1196
1219
  const ACCOUNT_PROVIDER = { type: 'string', value: '<claude|codex>', description: 'Only this provider’s accounts.' };
1197
- /** Claude and ChatGPT/Codex accounts with their usage windows, resets and threads (0.18.0+). */
1220
+ /** Claude and ChatGPT/Codex accounts with their usage windows and resets, and the active one of each provider (0.18.0+). */
1198
1221
  const AGENT_ACCOUNT_COMMANDS = [
1199
- { path: ['agents', 'accounts'], summary: 'Claude and ChatGPT/Codex accounts with usage, resets and status (0.18.0+)', positionals: [], options: { provider: ACCOUNT_PROVIDER },
1200
- examples: ['shard agents accounts', 'shard agents accounts usage', 'shard agents accounts add claude', 'pbpaste | shard agents accounts add codex --api-key-stdin'] },
1201
- { path: ['agents', 'accounts', 'list'], summary: 'List the accounts: status, usage windows and when they reset (0.18.0+)', positionals: [], options: { provider: ACCOUNT_PROVIDER } },
1222
+ { path: ['agents', 'accounts'], summary: 'Claude and ChatGPT/Codex accounts: which one is active, usage, resets and status (0.18.0+)', positionals: [], options: { provider: ACCOUNT_PROVIDER },
1223
+ examples: ['shard agents accounts', 'shard agents accounts usage', 'shard agents accounts add claude', 'shard agents accounts use claude:2'] },
1224
+ { path: ['agents', 'accounts', 'list'], summary: 'List the accounts: which one is active, status, usage windows and when they reset (0.18.0+)', positionals: [], options: { provider: ACCOUNT_PROVIDER } },
1202
1225
  { path: ['agents', 'accounts', 'usage'], summary: 'Every usage window of every account, with its reset time (0.18.0+)', positionals: [], options: { provider: ACCOUNT_PROVIDER } },
1203
- { path: ['agents', 'accounts', 'add'], summary: 'Sign in a Claude or ChatGPT account, or add an API key (0.18.0+)', positionals: [{ name: 'provider', required: true }], options: {
1204
- 'api-key-stdin': { type: 'boolean', description: 'Add an API key read from standard input instead of signing in.' },
1226
+ { path: ['agents', 'accounts', 'add'], summary: 'Sign in a Claude or ChatGPT account (0.18.0+)', positionals: [{ name: 'provider', required: true }], options: {
1205
1227
  label: { type: 'string', value: '<name>', description: 'The account’s name in lists (default its email).' },
1206
- scope: { type: 'string', value: '<personal|team>', description: 'personal: only your threads use it; team: any member’s (default: team for team plans and API keys, else personal).' },
1207
1228
  workspace: { type: 'string', value: '<id|key>', description: 'Run the sign-in in this workspace (default your machine VM, from any folder).' },
1208
1229
  } },
1230
+ { path: ['agents', 'accounts', 'use'], summary: 'Make an account the active one of its provider: new conversations start with it and running ones switch to it', positionals: [{ name: 'account', required: true }], options: {},
1231
+ examples: ['shard agents accounts use claude:2', 'shard agents accounts use marc@example.com --json'] },
1209
1232
  { path: ['agents', 'accounts', 'remove'], summary: 'Remove an account and its login (0.18.0+)', positionals: [{ name: 'account', required: true }], options: {} },
1210
- { path: ['agents', 'accounts', 'move'], summary: 'Move an account up or down in its provider’s order (0.18.0+)', positionals: [{ name: 'account', required: true }, { name: 'direction', required: true }], options: {} },
1211
- { path: ['agents', 'accounts', 'pool'], hidden: true, summary: 'Pool accounts: each new thread on the account with the most room', positionals: [{ name: 'state', required: true }], options: {} },
1212
1233
  ];
1213
1234
  /** `shard accounts …`: the same commands, as the menu bar and the design name them. */
1214
1235
  const ACCOUNT_ALIASES = AGENT_ACCOUNT_COMMANDS.map(c => ({ ...c, path: c.path.slice(1), hidden: true }));
@@ -1239,7 +1260,7 @@ const ON_OPTIONS = {
1239
1260
  };
1240
1261
  export const PROJECT_COMMAND = {
1241
1262
  path: ['<path>'], summary: 'Start a conversation in its own cloud VM',
1242
- description: 'Opens Claude Code or Codex directly. --resume continues a project conversation. With --json it starts detached and prints the session. Detach with Ctrl-] then d.',
1263
+ description: 'Opens Claude Code or Codex directly. --resume continues a project conversation. With --json it starts detached and prints the session. Detach with Ctrl-] then d. From a shell with no terminal of its own (a coding agent\'s) it opens in a new tab of your terminal app, the one agents targets --terminal chose (0.20.1+).',
1243
1264
  positionals: [{ name: 'path', required: true }, { name: 'claude|codex' }], options: {
1244
1265
  ...AGENT_OPTIONS,
1245
1266
  ...MODEL_OPTIONS,
@@ -1267,6 +1288,9 @@ const AGENT_COMMANDS = [
1267
1288
  last: { type: 'boolean', description: 'This project’s most recent session.' },
1268
1289
  session: { type: 'string', value: '<id>', description: 'The session to move: a Claude Code session id or Codex thread id.' },
1269
1290
  path: { type: 'string', value: '<path>', description: 'The project folder (default: the current folder).' },
1291
+ plan: { type: 'boolean', description: 'Inspect all registered worktree paths, reference hits and file bytes before any upload.' },
1292
+ 'preserve-all-worktrees': { type: 'boolean', description: 'Also bring every registered worktree, all local refs, indices and ignored files. Reports paths and bytes; keeps a durable recovery archive.' },
1293
+ 'this-worktree-only': { type: 'boolean', description: 'Move the session’s checkout only (the default): other worktrees and local refs stay on this computer.' },
1270
1294
  claude: { type: 'boolean', description: 'With --last or --list: Claude Code sessions only.' },
1271
1295
  codex: { type: 'boolean', description: 'With --last or --list: Codex sessions only.' },
1272
1296
  list: { type: 'boolean', description: 'List this project’s sessions on this computer, and where the moved ones continue.' },
@@ -1277,7 +1301,8 @@ const AGENT_COMMANDS = [
1277
1301
  { path: ['conversations', 'try'], summary: 'Try a conversation in a synced local worktree', positionals: [{ name: 'conversation', required: true }], options: { path: { type: 'string', value: '<path>', description: 'Local project checkout.' }, stop: { type: 'boolean', description: 'Stop the device helper’s local sync and ports.' } } },
1278
1302
  { path: ['agents', 'targets'], summary: 'Detect apps or configure managed desktop connections', positionals: [{ name: 'agent' }], options: {
1279
1303
  remove: { type: 'boolean', description: 'Remove this agent’s managed entries, or all entries for this API URL.' },
1280
- 'open-in': { type: 'string', value: '<app=target,…>', description: 'Where new conversations open (0.17.0+): claude=terminal|claude-desktop|web|t3, codex=codex-app|t3, e.g. claude=t3,codex=t3.' },
1304
+ 'open-in': { type: 'string', value: '<target|app=target,…>', description: 'Where new conversations open (0.17.0+): claude=terminal|claude-desktop|web|t3, codex=codex-app|t3, e.g. claude=t3,codex=t3; a bare target sets every app that opens there, e.g. terminal or t3 (0.20.1+).' },
1305
+ terminal: { type: 'string', value: '<app>[:tab|:window]', description: 'The terminal app terminal conversations open in (0.20.1+): ghostty, iterm, terminal, wezterm, kitty, alacritty or warp, a new tab in its front window (:window for a window); auto clears the choice.' },
1281
1306
  path: { type: 'string', value: '<path>', description: 'Local project folder whose T3 Code state and open_in to report, as agents new --path names it (default the current folder; the agent argument wins) (0.17.0+).' },
1282
1307
  } },
1283
1308
  ...['claude-desktop', 'codex-app', 'web', 't3'].map(target => ({ path: ['agents', 'open', target], summary: `Open the agent in ${target}`, positionals: [{ name: 'agent' }], options: { reachable: AGENT_OPTIONS.reachable } })),
@@ -1291,7 +1316,12 @@ const AGENT_COMMANDS = [
1291
1316
  },
1292
1317
  })),
1293
1318
  ...['status', 'enable', 'disable'].map(action => ({ path: ['agents', 'codex', action], summary: `${action} the account's billed Codex Cloud connector`, positionals: [], options: {} })),
1294
- { path: ['agents', 'open'], summary: 'Open a conversation (Codex Cloud or the native Claude session)', description: 'Codex opens its existing Cloud thread without waking its VM. Claude attaches its native session.', positionals: [{ name: 'agent', required: true }], options: { ...AGENT_OPTIONS, app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' }, in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|web|t3>', description: 'Where to open it. auto (0.15.1+): the saved target, else a Claude conversation in Claude Desktop when it is installed, else in this terminal; web opens claude.ai.' } } },
1319
+ { path: ['agents', 'open'], summary: 'Open a conversation (Codex Cloud or the native Claude session)', description: 'Codex opens its existing Cloud thread without waking its VM. Claude attaches its native session.', positionals: [{ name: 'agent', required: true }], options: { ...AGENT_OPTIONS, original: { type: 'boolean', description: 'Open the original Codex Cloud history after a connector change.' }, history: { type: 'string', value: '<thread-id>', description: 'Open an earlier thread listed in the Cloud conversation history.' }, app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' }, in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|web|t3>', description: 'Where to open it. auto (0.15.1+): the saved target, else a Claude conversation in Claude Desktop when it is installed, else in this terminal; web opens claude.ai. terminal (0.20.1+): a new tab of the terminal app agents targets --terminal chose, or this terminal when it is that one.' },
1320
+ window: { type: 'boolean', description: 'With --in terminal: a new window of the terminal app instead of a tab (0.20.1+).' } } },
1321
+ { path: ['agents', 'terminal-run'], hidden: true, summary: 'Run a command in the terminal app agents targets --terminal chose (0.20.1+)', positionals: [{ name: 'command', required: true, variadic: true }], options: {
1322
+ cwd: { type: 'string', value: '<path>', description: 'The folder the new tab starts in (default the current folder).' },
1323
+ window: { type: 'boolean', description: 'A new window instead of a tab.' },
1324
+ } },
1295
1325
  { path: ['agents', 'computers'], summary: 'List the project’s computers a new thread can start on with --on, and the threads on each (0.16.0+)', positionals: [], options: {
1296
1326
  path: { type: 'string', value: '<path>', description: 'Local project path (default the current directory).' },
1297
1327
  } },
@@ -1323,7 +1353,11 @@ const CLI_PROXY_COMMANDS = [
1323
1353
  { path: ['cli-proxy', 'import'], summary: 'Move CLIProxyAPI from this computer into your account, reachable at 127.0.0.1:8317 everywhere', description: `${CLI_PROXY_HELP} Import brings your config and logins over, stops the local copy (one copy refreshes each login), and connects this computer and your machine template. On a second computer it connects that computer.`, positionals: [], options: {
1324
1354
  config: { type: 'string', value: '<path>', description: 'Your CLIProxyAPI config file (default: the one the running CLIProxyAPI uses, else the usual locations).' },
1325
1355
  yes: { type: 'boolean', description: 'Stop the local CLIProxyAPI without asking.' },
1356
+ use: { type: 'string', multiple: true, value: '<email|file>', description: 'Serve its provider with only this login; the provider’s others are kept, disabled (repeatable, one per provider). Default: every login as it is on this computer.' },
1326
1357
  }, examples: ['shard cli-proxy import', 'shard cli-proxy'] },
1358
+ { path: ['cli-proxy', 'use'], summary: 'Choose which logins your hosted CLIProxyAPI uses: one for its provider, or all', description: 'Your hosted CLIProxyAPI routes requests across its enabled logins as its config says. `use <login>` keeps only that login enabled for its provider (the others stay in the hub, disabled); `use --all` enables every login again.', positionals: [{ name: 'login' }], options: {
1359
+ all: { type: 'boolean', description: 'Enable every login; the hub routes across them as its config says.' },
1360
+ }, examples: ['shard cli-proxy use ana@example.com', 'shard cli-proxy use --all'] },
1327
1361
  { path: ['cli-proxy', 'export'], summary: 'Bring your logins back to this computer and stop the hosted CLIProxyAPI', description: 'The hosted copy stops first, so only this computer refreshes your logins from then on. Its logins, as last refreshed, go to the auth folder the import took them from (0600), this computer stops forwarding 127.0.0.1:8317, and the command that starts your local CLIProxyAPI is shown. shard cli-proxy import moves them back.', positionals: [], options: {
1328
1362
  yes: { type: 'boolean', description: 'Do it without asking.' },
1329
1363
  } },
@@ -1334,7 +1368,14 @@ const MENUBAR_COMMANDS = ['start', 'stop', 'status'].map(action => ({
1334
1368
  path: ['menubar', action], summary: `${action === 'start' ? 'Install and start' : action === 'stop' ? 'Stop' : 'Show'} the macOS menu bar app`,
1335
1369
  positionals: [], options: { login: { type: 'string', value: '<on|off>', description: 'Enable or disable launch at login.' } },
1336
1370
  }));
1337
- export const COMMANDS = [...ACCOUNT_COMMANDS.filter((c) => c.path[0] !== 'templates'), ...AGENT_COMMANDS, ...AGENT_CREDENTIAL_COMMANDS, ...AGENT_ACCOUNT_COMMANDS, ...ACCOUNT_ALIASES, ...CLI_PROXY_COMMANDS, ...MENUBAR_COMMANDS, ...KEY_COMMANDS, ...ACCOUNT_COMMANDS.filter((c) => c.path[0] === 'templates')];
1371
+ const SETTINGS_COMMANDS = [{
1372
+ path: ['settings'], summary: 'Show or change this computer’s settings for cloud conversations (0.20.1+)', positionals: [], options: {
1373
+ 'title-prefix': { type: 'string', value: '<text|none|default>', description: 'The marker your cloud conversations’ Claude session titles start with in claude.ai and the Claude app ("☁ Fix login bug · todo-app"): up to 16 characters on one line; none for no marker, default for ☁. New conversations use it.' },
1374
+ },
1375
+ description: 'Shows the title marker, where new conversations open and the terminal app (change those two with "shard agents targets --open-in" and "--terminal"). The menu bar and desktop apps change the same settings.',
1376
+ examples: ['shard settings', 'shard settings --title-prefix SF', 'shard settings --title-prefix none', 'shard settings --title-prefix default --json'],
1377
+ }];
1378
+ export const COMMANDS = [...ACCOUNT_COMMANDS.filter((c) => c.path[0] !== 'templates'), ...AGENT_COMMANDS, ...AGENT_CREDENTIAL_COMMANDS, ...AGENT_ACCOUNT_COMMANDS, ...ACCOUNT_ALIASES, ...CLI_PROXY_COMMANDS, ...MENUBAR_COMMANDS, ...SETTINGS_COMMANDS, ...KEY_COMMANDS, ...ACCOUNT_COMMANDS.filter((c) => c.path[0] === 'templates')];
1338
1379
  const GROUPS = {
1339
1380
  device: 'Local device approvals',
1340
1381
  'cli-proxy': 'Your hosted CLIProxyAPI, reachable at http://127.0.0.1:8317 on this computer and in every VM',
@@ -1358,7 +1399,7 @@ const GROUPS = {
1358
1399
  workspaces: 'Open, inspect and manage workspaces',
1359
1400
  'workspaces port': 'Serve ports of a workspace at private HTTPS URLs: expose, list, close, tokens, browser links and callback URLs (0.9.0+)',
1360
1401
  files: 'Read, write, list, search and patch files in a workspace',
1361
- operations: 'Inspect and wait for lifecycle operations',
1402
+ operations: 'Inspect lifecycle operations, wait for outcomes and cancel suspension preparation',
1362
1403
  executions: 'Fetch the results of file-first executions (the commands "shard ws exec" runs in file-first workspaces)',
1363
1404
  credentials: 'Approve browser login requests from your vault',
1364
1405
  secrets: 'Manage secrets (values are write-only) and bind them to workspaces',
@@ -1510,8 +1551,9 @@ function toParseArgsOptions(opts) {
1510
1551
  * returns help text for --help, bare groups and non-TTY bare `shard`.
1511
1552
  */
1512
1553
  export function parseCommandLine(argv, options = {}) {
1513
- // `acp --login` is the ACP terminal sign-in an editor runs (ACP appends the method's args to `shard acp`).
1514
- if (argv.length === 2 && argv[0] === 'acp' && argv[1] === '--login')
1554
+ // The ACP terminal sign-in an editor runs: T3 Code appends the method's args to `shard acp` (`acp --login`); clients
1555
+ // following the ACP registry replace the agent's args with them (`--login`, 0.21.1+).
1556
+ if ((argv.length === 2 && argv[0] === 'acp' && argv[1] === '--login') || (argv.length === 1 && argv[0] === '--login'))
1515
1557
  return { kind: 'run', command: { path: ['acp'], summary: '', positionals: [], options: {} }, values: { login: true }, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
1516
1558
  // `menubar refresh` (0.15.2+, not in help): the background step each command may start (menubar.ts).
1517
1559
  if (argv.length === 2 && argv[0] === 'menubar' && argv[1] === 'refresh')
@@ -1724,7 +1766,7 @@ export function groupHelp(prefix) {
1724
1766
  const group = words.join(' ');
1725
1767
  const cmds = COMMANDS.filter((c) => !c.hidden && words.every((w, i) => c.path[i] === w));
1726
1768
  const w = Math.max(...cmds.map((c) => usageLine(c).length));
1727
- return [`Usage: shard ${group} <command> [options]`, '', `${GROUPS[group] ?? GROUPS[words[0]] ?? ''}.`, '', 'Commands:', ...cmds.map((c) => ` ${usageLine(c).padEnd(w)} ${c.summary}`), ...(group === 'workspaces' ? ['', 'Also: shard workspaces files <read|write|ls|stat|search|patch> ..., shard workspaces operations <list|get|wait> ... and shard workspaces executions get ...'] : []), '', `Run "shard ${group} <command> --help" for details.`].join('\n') + '\n';
1769
+ return [`Usage: shard ${group} <command> [options]`, '', `${GROUPS[group] ?? GROUPS[words[0]] ?? ''}.`, '', 'Commands:', ...cmds.map((c) => ` ${usageLine(c).padEnd(w)} ${c.summary}`), ...(group === 'workspaces' ? ['', 'Also: shard workspaces files <read|write|ls|stat|search|patch> ..., shard workspaces operations <list|get|wait|progress|cancel> ... and shard workspaces executions get ...'] : []), '', `Run "shard ${group} <command> --help" for details.`].join('\n') + '\n';
1728
1770
  }
1729
1771
  export const EXIT_CODE_HELP = [
1730
1772
  ' 0 success',
@@ -1740,7 +1782,7 @@ export const EXIT_CODE_HELP = [
1740
1782
  /** The top help's sections: every command's group (first word) is in exactly one (args.test.ts checks). */
1741
1783
  export const HELP_SECTIONS = [
1742
1784
  { title: 'Account (a user session: shard signup or shard auth login)', groups: ['signup', 'verify', 'auth', 'setup', 'env', 'orgs', 'projects', 'api-keys', 'members', 'invitations', 'billing', 'audit', 'account'] },
1743
- { title: 'Cloud agents (shard ./ starts one; a project API key)', groups: ['conversations', 'move', 'machine', 'template', 'agents', 'accounts', 'device', 'cli-proxy', 'ssh-proxy'] },
1785
+ { title: 'Cloud agents (shard ./ starts one; a project API key)', groups: ['conversations', 'move', 'machine', 'template', 'agents', 'settings', 'accounts', 'device', 'cli-proxy', 'ssh-proxy'] },
1744
1786
  { title: 'Workspaces for your app (a project API key: SHARDFLUX_API_KEY, else the one shard setup saved)', groups: ['login', 'whoami', 'status', 'usage', 'workspaces', 'files', 'operations', 'executions', 'secrets', 'credentials', 'volumes', 'egress'] },
1745
1787
  { title: 'Templates for your app’s workspaces (not for cloud agents; publish and archive use the session, the rest the API key)', groups: ['templates'] },
1746
1788
  { title: 'Other', groups: ['menubar', 'update', 'feedback', 'version'] },
@@ -1755,10 +1797,8 @@ export function topHelp() {
1755
1797
  ' shard <path> [claude|codex] [--approvals]',
1756
1798
  'Terminal: detach with Ctrl-] then d; /exit ends the agent session.',
1757
1799
  '',
1758
- 'Setting Shardflux up with a coding agent? Ask your user one question first: is it for a SaaS platform (their',
1759
- 'product runs VMs for its own customers through the API, SDK or MCP) or for their personal setup (their own cloud',
1760
- 'coding agents: each Claude or Codex conversation gets its own VM)? "shard setup --guide" prints the steps of both',
1761
- '(0.17.1+; --json for a list); so does "shard" in a shell without a terminal.',
1800
+ 'Setting Shardflux up for someone as their coding agent? "shard setup --guide" prints how (0.17.1+; --json for a',
1801
+ 'list; so does "shard" in a shell without a terminal): you run every step, they answer multiple-choice questions.',
1762
1802
  '',
1763
1803
  'Feedback: shard feedback [options] <message...>',
1764
1804
  ...FEEDBACK_WHY.map((l) => ` ${l}`),
@@ -1779,6 +1819,7 @@ export function topHelp() {
1779
1819
  ' shard ./ # a conversation on this folder’s repository: Claude Code in its own VM (shard ./ codex: Codex; --resume: a saved one)',
1780
1820
  ' shard machine save # what every agent starts with: tools, logins, dotfiles, Claude/Codex config',
1781
1821
  ' shard template save # this project: toolchains, dependencies, services, project secrets',
1822
+ ' shard template delete # start this project over from your machine; setup saves a new one',
1782
1823
  ' A VM is the coding template, then your machine, then the project template. Setup after shard ./ saves the',
1783
1824
  ' project template; machine and project templates are always saved from an agent’s VM, so they keep Claude Code,',
1784
1825
  ' Codex and the agent runtime. "shard templates" is for your own app’s workspaces, not for agents: a repository',
package/dist/bin.js CHANGED
@@ -4,5 +4,9 @@
4
4
  * waits/streams cleanly (exit 130); a second Ctrl-C exits at once. Output is flushed before exiting (stdout/stderr
5
5
  * pipes are asynchronous on macOS). The `shardflux` npm bundle calls main() with its own identity instead.
6
6
  */
7
- import { main } from "./main.js";
7
+ import { enableCompileCache } from 'node:module';
8
+ // V8's compiled code for the CLI's modules is kept on disk (the OS temp directory) and reused: ~0.1 s off each
9
+ // command's start (0.21.1+). Best effort: without a writable cache the CLI compiles as before.
10
+ enableCompileCache?.();
11
+ const { main } = await import("./main.js");
8
12
  await main();
package/dist/browser.d.ts CHANGED
@@ -3,5 +3,16 @@ export declare function openerFor(url: string, env: Record<string, string | unde
3
3
  command: string;
4
4
  args: string[];
5
5
  };
6
- /** Starts the browser detached and returns whether it could be started; only http(s) URLs are opened. */
7
- export declare function openUrl(url: string, env: Record<string, string | undefined>): boolean;
6
+ /** How long an opener that has not exited counts as a browser that started in the foreground ($BROWSER=firefox, or
7
+ * xdg-open running one): opened. xdg-open hands a link to the desktop and exits well within it. */
8
+ export declare const OPENER_SETTLE_MS = 3000;
9
+ /**
10
+ * Starts the browser detached and resolves whether it took the URL: false when the opener cannot start (no xdg-open:
11
+ * ENOENT) or exits with a failure (xdg-open exits 3 or 4 when nothing on the desktop opens the link, `open` 1), so the
12
+ * caller says to open the printed link; true when it exits 0 or is still running after `settleMs`. Only http(s) URLs
13
+ * are opened.
14
+ */
15
+ export declare function openUrl(url: string, env: Record<string, string | undefined>, options?: {
16
+ platform?: NodeJS.Platform;
17
+ settleMs?: number;
18
+ }): Promise<boolean>;
package/dist/browser.js CHANGED
@@ -15,25 +15,41 @@ export function openerFor(url, env, platform = process.platform) {
15
15
  return { command: 'rundll32', args: ['url.dll,FileProtocolHandler', url] };
16
16
  return { command: 'xdg-open', args: [url] };
17
17
  }
18
- /** Starts the browser detached and returns whether it could be started; only http(s) URLs are opened. */
19
- export function openUrl(url, env) {
18
+ /** How long an opener that has not exited counts as a browser that started in the foreground ($BROWSER=firefox, or
19
+ * xdg-open running one): opened. xdg-open hands a link to the desktop and exits well within it. */
20
+ export const OPENER_SETTLE_MS = 3000;
21
+ /**
22
+ * Starts the browser detached and resolves whether it took the URL: false when the opener cannot start (no xdg-open:
23
+ * ENOENT) or exits with a failure (xdg-open exits 3 or 4 when nothing on the desktop opens the link, `open` 1), so the
24
+ * caller says to open the printed link; true when it exits 0 or is still running after `settleMs`. Only http(s) URLs
25
+ * are opened.
26
+ */
27
+ export function openUrl(url, env, options = {}) {
20
28
  let u;
21
29
  try {
22
30
  u = new URL(url);
23
31
  }
24
32
  catch {
25
- return false;
33
+ return Promise.resolve(false);
26
34
  }
27
35
  if (u.protocol !== 'https:' && u.protocol !== 'http:')
28
- return false;
29
- const { command, args } = openerFor(u.toString(), env);
30
- try {
31
- const child = spawn(command, args, { stdio: 'ignore', detached: true, env });
32
- child.on('error', () => undefined);
33
- child.unref();
34
- return true;
35
- }
36
- catch {
37
- return false;
38
- }
36
+ return Promise.resolve(false);
37
+ const { command, args } = openerFor(u.toString(), env, options.platform);
38
+ return new Promise((resolve) => {
39
+ let timer;
40
+ const done = (opened) => {
41
+ clearTimeout(timer);
42
+ resolve(opened);
43
+ };
44
+ try {
45
+ const child = spawn(command, args, { stdio: 'ignore', detached: true, env });
46
+ child.once('error', () => done(false));
47
+ child.once('exit', (code) => done(code === 0));
48
+ child.unref();
49
+ timer = setTimeout(() => done(true), options.settleMs ?? OPENER_SETTLE_MS);
50
+ }
51
+ catch {
52
+ done(false);
53
+ }
54
+ });
39
55
  }
@@ -1,5 +1,7 @@
1
1
  import type { AgentCredentialMetadata, CellClient, Workspace } from '@shardflux/sdk';
2
2
  import type { Ctx } from './commands.js';
3
+ /** Someone types into this terminal: not --json, a TTY with raw mode, and not a coding agent's shell. */
4
+ export declare function claudeTerminal(ctx: Ctx): boolean;
3
5
  export declare function requireClaudeTerminal(ctx: Ctx): void;
4
6
  export declare function cloudClaudeLogin(ctx: Ctx): Promise<AgentCredentialMetadata | undefined>;
5
7
  /** Parse only complete provider authorize links, including the native OSC-8 link. */
@@ -21,6 +23,26 @@ export declare function runWorkspaceLogin(ctx: Ctx, ws: Workspace, cell: CellCli
21
23
  cwd: string;
22
24
  urls: (output: string) => string[];
23
25
  }): Promise<boolean>;
24
- /** A transient Linux profile holds the initial pair; it is removed before an agent starts. */
25
- export declare function loginClaudeInWorkspace(ctx: Ctx, ws: Workspace): Promise<AgentCredentialMetadata>;
26
+ /**
27
+ * The same sign-in without an attached terminal (0.20.1+: a coding agent's shell, --json, no TTY). The workspace PTY
28
+ * runs the login exactly as runWorkspaceLogin does; shard reads its output, opens the claude.ai link it prints, and
29
+ * opens a page on this computer where the person pastes the code claude.ai shows (the agent never sees it). The code
30
+ * and Enter go into the PTY; the page says "Signed in" once the login exited 0. True when it did.
31
+ */
32
+ export declare function runWorkspaceLoginHeadless(ctx: Ctx, cell: CellClient, login: {
33
+ session: string;
34
+ argv: string[];
35
+ env: Record<string, string>;
36
+ cwd: string;
37
+ urls: (output: string) => string[];
38
+ }, timeoutMs: number): Promise<boolean>;
39
+ /**
40
+ * A transient Linux profile holds the initial pair; it is removed before an agent starts. `headless` (0.20.1+): the
41
+ * sign-in runs through the pages shard opens (runWorkspaceLoginHeadless) instead of an attached terminal.
42
+ */
43
+ export declare function loginClaudeInWorkspace(ctx: Ctx, ws: Workspace, opts?: {
44
+ headless?: {
45
+ timeoutMs: number;
46
+ };
47
+ }): Promise<AgentCredentialMetadata>;
26
48
  export declare function ensureCloudClaude(ctx: Ctx, ws: Workspace): Promise<void>;