@shardflux/cli 0.15.2 → 0.17.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 (77) hide show
  1. package/CHANGELOG.md +144 -14
  2. package/README.md +92 -7
  3. package/dist/acp-models.js +5 -1
  4. package/dist/agent-commands.d.ts +15 -1
  5. package/dist/agent-commands.js +84 -23
  6. package/dist/agent-models.d.ts +61 -0
  7. package/dist/agent-models.js +79 -0
  8. package/dist/agent-state.d.ts +10 -0
  9. package/dist/agent-state.js +13 -0
  10. package/dist/agent-target-commands.d.ts +13 -3
  11. package/dist/agent-target-commands.js +129 -23
  12. package/dist/agent-targets.d.ts +5 -0
  13. package/dist/agent-targets.js +14 -5
  14. package/dist/agent-terminal.d.ts +8 -0
  15. package/dist/agent-terminal.js +35 -2
  16. package/dist/args.d.ts +5 -1
  17. package/dist/args.js +74 -11
  18. package/dist/cli-proxy-forward.d.ts +45 -0
  19. package/dist/cli-proxy-forward.js +432 -0
  20. package/dist/cli-proxy-hub.d.ts +273 -0
  21. package/dist/cli-proxy-hub.js +749 -0
  22. package/dist/cli-proxy.d.ts +325 -0
  23. package/dist/cli-proxy.js +1016 -0
  24. package/dist/codex-cloud.d.ts +3 -0
  25. package/dist/codex-cloud.js +17 -5
  26. package/dist/command-center-actions.js +25 -1
  27. package/dist/commands.d.ts +22 -0
  28. package/dist/commands.js +54 -14
  29. package/dist/conversation-commands.js +6 -0
  30. package/dist/conversation-local.js +2 -2
  31. package/dist/conversation-stash.js +4 -3
  32. package/dist/conversation-terminal.d.ts +5 -0
  33. package/dist/conversation-terminal.js +54 -6
  34. package/dist/conversations.d.ts +31 -3
  35. package/dist/conversations.js +53 -10
  36. package/dist/device-approvals.d.ts +12 -0
  37. package/dist/device-approvals.js +73 -0
  38. package/dist/device-helper.d.ts +3 -1
  39. package/dist/device-helper.js +14 -4
  40. package/dist/errors.d.ts +7 -0
  41. package/dist/errors.js +12 -2
  42. package/dist/files-mirror.d.ts +164 -0
  43. package/dist/files-mirror.js +761 -0
  44. package/dist/handlers.js +5 -1
  45. package/dist/index.d.ts +4 -0
  46. package/dist/index.js +3 -0
  47. package/dist/linux-helper.d.ts +12 -0
  48. package/dist/linux-helper.js +88 -0
  49. package/dist/linux-terminal.d.ts +11 -0
  50. package/dist/linux-terminal.js +42 -0
  51. package/dist/main.js +38 -8
  52. package/dist/onboarding.d.ts +2 -0
  53. package/dist/onboarding.js +53 -5
  54. package/dist/registry.d.ts +2 -42
  55. package/dist/registry.js +6 -90
  56. package/dist/t3-launch.d.ts +37 -0
  57. package/dist/t3-launch.js +133 -0
  58. package/dist/t3-mode.d.ts +121 -0
  59. package/dist/t3-mode.js +654 -0
  60. package/dist/t3-phone.d.ts +17 -0
  61. package/dist/t3-phone.js +15 -0
  62. package/dist/t3-thread-script.d.ts +18 -0
  63. package/dist/t3-thread-script.js +245 -0
  64. package/dist/terminal-onboarding.d.ts +11 -0
  65. package/dist/terminal-onboarding.js +75 -0
  66. package/dist/thread-on-computer.d.ts +35 -0
  67. package/dist/thread-on-computer.js +206 -0
  68. package/dist/update.d.ts +1 -1
  69. package/dist/update.js +12 -1
  70. package/dist/wait-status.d.ts +22 -0
  71. package/dist/wait-status.js +59 -0
  72. package/dist/worktree-twin-engine.d.ts +125 -0
  73. package/dist/worktree-twin-engine.js +615 -0
  74. package/dist/worktree-twin-land.py +331 -0
  75. package/dist/worktree-twin.d.ts +313 -0
  76. package/dist/worktree-twin.js +843 -0
  77. package/package.json +3 -3
package/dist/args.js CHANGED
@@ -12,7 +12,8 @@
12
12
  import { parseArgs } from 'node:util';
13
13
  import { ACCOUNT_COMMANDS } from "./account-args.js";
14
14
  import { UsageError } from "./errors.js";
15
- export const CLI_VERSION = '0.15.2';
15
+ import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
16
+ export const CLI_VERSION = '0.17.0';
16
17
  export const GLOBAL_OPTIONS = {
17
18
  'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
18
19
  json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
@@ -57,7 +58,7 @@ const IF_REVISION = {
57
58
  };
58
59
  const WAIT = {
59
60
  wait: { type: 'boolean', description: 'Wait until the operation finishes.' },
60
- timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (e.g. 90s, 5m; default 5m). The operation continues server side (a start waiting for capacity until its deadline, 15 minutes after it began; then it fails with capacity_unavailable).' },
61
+ timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (e.g. 90s, 5m; default 5m, and a start queued for capacity is waited for until its deadline, its place in the queue shown on stderr). The operation continues server side (a start waiting for capacity until its deadline, 15 minutes after it began; then it fails with capacity_unavailable).' },
61
62
  ...TIMING,
62
63
  };
63
64
  const REF = { name: 'id|key', required: true };
@@ -158,6 +159,11 @@ const KEY_COMMANDS = [
158
159
  } },
159
160
  { path: ['conversations', 'land'], summary: 'Record the landing action performed by the agent', positionals: [{ name: 'id', required: true }, { name: 'action', required: true }], options: { into: { type: 'string', value: '<ref>', description: 'Destination branch or PR URL.' } } },
160
161
  { path: ['conversations', 'move'], summary: 'Move at the end of the turn', positionals: [{ name: 'id', required: true }, { name: 'to', required: true }], options: {} },
162
+ { path: ['conversations', 'landing'], summary: 'Show or set how finished conversations reach this checkout', positionals: [{ name: 'mode' }, { name: 'backup' }], options: {
163
+ path: { type: 'string', value: '<path>', description: 'Local project checkout (default the current directory).' }
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.',
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'] },
161
167
  ...['machine', 'template'].flatMap((level) => [
162
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: {},
163
169
  description: LAYER_HELP[level] },
@@ -851,6 +857,14 @@ const KEY_COMMANDS = [
851
857
  'shard files patch acme/agent-7 /home/user/app.py --old "DEBUG = True" --new "DEBUG = False" --if-revision "$(shard ws get acme/agent-7 --json | jq .tree_revision)"',
852
858
  ],
853
859
  },
860
+ {
861
+ path: ['files', 'pull'],
862
+ summary: 'Download a large file a conversation committed into ~/Shardflux',
863
+ 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.',
864
+ positionals: [{ name: 'conversation', required: true }, { name: 'path', required: true }],
865
+ options: {},
866
+ examples: ['shard files pull hype-video out/hype.mp4'],
867
+ },
854
868
  {
855
869
  path: ['operations', 'list'],
856
870
  summary: 'List a workspace’s operations, newest first',
@@ -1175,11 +1189,30 @@ const AGENT_OPTIONS = {
1175
1189
  reachable: { type: 'string', value: '<duration>', hidden: true, description: 'Deprecated; ignored. Claude conversations stay reachable without a time limit.' },
1176
1190
  approvals: { type: 'boolean', description: 'Keep the agent’s permission prompts.' },
1177
1191
  };
1192
+ /** `Claude fable, opus, sonnet or haiku; Codex …` from the CLI's one list (agent-models.ts), without "default". */
1193
+ function choiceIds(kind) {
1194
+ return ['claude', 'codex'].map(app => {
1195
+ const ids = MODEL_CHOICES[app][kind].map(c => c.id).filter(id => id !== DEFAULT_CHOICE);
1196
+ return `${app === 'claude' ? 'Claude' : 'Codex'} ${ids.slice(0, -1).join(', ')} or ${ids.at(-1)}`;
1197
+ }).join('; ');
1198
+ }
1199
+ /** The model and effort a new thread starts with; `shard agents targets --json` lists the choices (0.17.0+). */
1200
+ const MODEL_OPTIONS = {
1201
+ model: { type: 'string', value: '<model>', description: `Model the new thread starts with: ${choiceIds('models')}; default is the agent’s own (0.17.0+).` },
1202
+ effort: { type: 'string', value: '<level>', description: `Reasoning effort the new thread starts with: ${choiceIds('efforts')}; default is the agent’s own (0.17.0+).` },
1203
+ };
1204
+ /** A new thread on an existing conversation's computer (0.16.0+). */
1205
+ const ON_OPTIONS = {
1206
+ on: { type: 'string', value: '<conversation|computer>', description: 'Start the thread on the computer of an existing conversation, sharing its packages, caches, services, ports and files, on its own branch: a conversation id, key or name, or its workspace key or id (shard agents computers lists them) (0.16.0+).' },
1207
+ review: { type: 'boolean', description: 'With --on: a read-only Claude reviewer of that conversation’s worktree instead of a thread with its own branch (0.16.0+).' },
1208
+ };
1178
1209
  export const PROJECT_COMMAND = {
1179
1210
  path: ['<path>'], summary: 'Start a conversation in its own cloud VM',
1180
1211
  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.',
1181
1212
  positionals: [{ name: 'path', required: true }, { name: 'claude|codex' }], options: {
1182
1213
  ...AGENT_OPTIONS,
1214
+ ...MODEL_OPTIONS,
1215
+ ...ON_OPTIONS,
1183
1216
  computer: COMPUTER_OPTION,
1184
1217
  new: { type: 'boolean', description: 'Start a new conversation.' },
1185
1218
  resume: { type: 'boolean', description: 'Continue a conversation from this project.' },
@@ -1192,10 +1225,17 @@ function projectExample(rest, app) {
1192
1225
  const path = rest.find((r, i) => !r.startsWith('-') && !GLOBAL_VALUE_FLAGS.has(rest[i - 1] ?? '')) ?? './';
1193
1226
  return `shard ${/^[\w./~@:+,=-]+$/.test(path) ? path : `'${path.replaceAll("'", `'\\''`)}'`} ${app}`;
1194
1227
  }
1228
+ export const ONBOARDING_COMMAND = { path: ['<onboarding>'], summary: 'Set up Shardflux in your terminal', positionals: [], options: { 'accept-terms': { type: 'boolean', description: 'Accept the Terms of Service and Privacy Policy.' }, apps: { type: 'string', value: '<claude,codex>', description: 'Connect Claude, Codex or both.' } } };
1195
1229
  export const CENTER_COMMAND = { path: ['<center>'], summary: 'Open the cloud agent command center', positionals: [], options: {} };
1196
1230
  const AGENT_COMMANDS = [
1231
+ { path: ['device', 'requests'], summary: 'Review local device requests waiting for approval', positionals: [], options: {} },
1232
+ ...['approve', 'deny'].map(action => ({ path: ['device', action], summary: `${action} a local device request`, positionals: [{ name: 'id', required: true }], options: {} })),
1197
1233
  { 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.' } } },
1198
- { path: ['agents', 'targets'], summary: 'Detect apps or configure managed desktop connections', positionals: [{ name: 'agent' }], options: { remove: { type: 'boolean', description: 'Remove this agent’s managed entries, or all entries for this API URL.' } } },
1234
+ { path: ['agents', 'targets'], summary: 'Detect apps or configure managed desktop connections', positionals: [{ name: 'agent' }], options: {
1235
+ remove: { type: 'boolean', description: 'Remove this agent’s managed entries, or all entries for this API URL.' },
1236
+ '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.' },
1237
+ 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+).' },
1238
+ } },
1199
1239
  ...['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 } })),
1200
1240
  { path: ['ssh-proxy'], summary: 'Forward SSH standard IO to a managed workspace', positionals: [{ name: 'host', required: true }], options: {} },
1201
1241
  ...['worktree', 'merge', 'keep', 'discard', 'restore'].map((action) => ({
@@ -1208,13 +1248,18 @@ const AGENT_COMMANDS = [
1208
1248
  })),
1209
1249
  ...['status', 'enable', 'disable'].map(action => ({ path: ['agents', 'codex', action], summary: `${action} the account's billed Codex Cloud connector`, positionals: [], options: {} })),
1210
1250
  { 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.' } } },
1211
- { path: ['agents', 'new'], summary: 'Start a conversation in a fresh VM; Codex opens a genuine Cloud thread', positionals: [{ name: 'agent' }], options: {
1251
+ { 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: {
1252
+ path: { type: 'string', value: '<path>', description: 'Local project path (default the current directory).' },
1253
+ } },
1254
+ { path: ['agents', 'new'], summary: 'Start a conversation in a fresh VM, or with --on on an existing conversation’s computer; Codex opens a genuine Cloud thread', positionals: [{ name: 'agent' }], options: {
1212
1255
  ...AGENT_OPTIONS,
1256
+ ...MODEL_OPTIONS,
1257
+ ...ON_OPTIONS,
1213
1258
  computer: COMPUTER_OPTION,
1214
1259
  app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' },
1215
1260
  path: { type: 'string', value: '<path>', description: 'Local project path for first launch.' },
1216
1261
  name: { type: 'string', value: '<name>', description: 'New agent name; copies the project main workspace with --like.' },
1217
- in: { type: 'string', value: '<terminal|claude-desktop|codex-app|t3>', description: 'Where to open the new agent (default terminal).' },
1262
+ in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|t3>', description: 'Where to open the new agent (default terminal). t3 (0.17.0+): the first message starts a T3 Code thread (with --on, on that computer’s T3 server); auto: where agents targets --open-in says.' },
1218
1263
  like: { type: 'string', value: '<id|key>', description: 'Copy the workspace configuration onto a fresh disk.' },
1219
1264
  } },
1220
1265
  { path: ['agents', 'fork'], summary: 'Fork an agent’s memory and disk', positionals: [{ name: 'agent', required: true }, { name: 'name', required: true }], options: WAIT },
@@ -1225,13 +1270,29 @@ const AGENT_COMMANDS = [
1225
1270
  follow: { type: 'boolean', description: 'Follow output until interrupted.' },
1226
1271
  } },
1227
1272
  ];
1273
+ const CLI_PROXY_HELP = 'One CLIProxyAPI for your whole account: your Claude, Codex and other subscription logins live in a small hosted copy that sleeps between requests and wakes on the next one. This computer and every VM reach it at http://127.0.0.1:8317, so Claude Code, Codex and T3 Code keep their settings.';
1274
+ const CLI_PROXY_COMMANDS = [
1275
+ { path: ['cli-proxy'], summary: 'Your hosted CLIProxyAPI: its accounts, and where it is reachable', description: CLI_PROXY_HELP, positionals: [], options: {
1276
+ config: { type: 'string', value: '<path>', description: 'This computer’s CLIProxyAPI config file, when none is set up yet (default: found automatically).' },
1277
+ } },
1278
+ { 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: {
1279
+ config: { type: 'string', value: '<path>', description: 'Your CLIProxyAPI config file (default: the one the running CLIProxyAPI uses, else the usual locations).' },
1280
+ yes: { type: 'boolean', description: 'Stop the local CLIProxyAPI without asking.' },
1281
+ }, examples: ['shard cli-proxy import', 'shard cli-proxy'] },
1282
+ { 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: {
1283
+ yes: { type: 'boolean', description: 'Do it without asking.' },
1284
+ } },
1285
+ { path: ['cli-proxy', 'forward'], hidden: true, summary: 'Forward 127.0.0.1:8317 to your hosted CLIProxyAPI (started at login)', positionals: [], options: {} },
1286
+ ];
1228
1287
  /** Every command: the account plane first (sign in, set up), then the project-key commands. */
1229
1288
  const MENUBAR_COMMANDS = ['start', 'stop', 'status'].map(action => ({
1230
1289
  path: ['menubar', action], summary: `${action === 'start' ? 'Install and start' : action === 'stop' ? 'Stop' : 'Show'} the macOS menu bar app`,
1231
1290
  positionals: [], options: { login: { type: 'string', value: '<on|off>', description: 'Enable or disable launch at login.' } },
1232
1291
  }));
1233
- export const COMMANDS = [...ACCOUNT_COMMANDS.filter((c) => c.path[0] !== 'templates'), ...AGENT_COMMANDS, ...AGENT_CREDENTIAL_COMMANDS, ...MENUBAR_COMMANDS, ...KEY_COMMANDS, ...ACCOUNT_COMMANDS.filter((c) => c.path[0] === 'templates')];
1292
+ export const COMMANDS = [...ACCOUNT_COMMANDS.filter((c) => c.path[0] !== 'templates'), ...AGENT_COMMANDS, ...AGENT_CREDENTIAL_COMMANDS, ...CLI_PROXY_COMMANDS, ...MENUBAR_COMMANDS, ...KEY_COMMANDS, ...ACCOUNT_COMMANDS.filter((c) => c.path[0] === 'templates')];
1234
1293
  const GROUPS = {
1294
+ device: 'Local device approvals',
1295
+ 'cli-proxy': 'Your hosted CLIProxyAPI, reachable at http://127.0.0.1:8317 on this computer and in every VM',
1235
1296
  menubar: 'macOS menu bar app',
1236
1297
  conversations: 'Project conversations, branches and messages',
1237
1298
  agents: 'Cloud agent sessions and copied credentials',
@@ -1410,6 +1471,8 @@ export function parseCommandLine(argv, options = {}) {
1410
1471
  // `menubar refresh` (0.15.2+, not in help): the background step each command may start (menubar.ts).
1411
1472
  if (argv.length === 2 && argv[0] === 'menubar' && argv[1] === 'refresh')
1412
1473
  return { kind: 'run', command: { path: ['menubar', 'refresh'], summary: '', positionals: [], options: {} }, values: {}, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
1474
+ if (argv[0] === 'device-helper' && argv.length === 2 && argv[1] === '--service')
1475
+ return { kind: 'run', command: { path: ['device-helper'], summary: '', positionals: [], options: {} }, values: { service: true }, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
1413
1476
  if (argv.length === 1 && (argv[0] === 'acp' || argv[0] === 'device-helper'))
1414
1477
  return { kind: 'run', command: { path: [argv[0]], summary: '', positionals: [], options: {} }, values: {}, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
1415
1478
  const terminator = argv.indexOf('--');
@@ -1471,8 +1534,8 @@ export function parseCommandLine(argv, options = {}) {
1471
1534
  const path = rest.find((r, i) => !r.startsWith('-') && !GLOBAL_VALUE_FLAGS.has(rest[i - 1] ?? ''));
1472
1535
  if (path !== undefined && (/^(?:\.|\/|~\/)/.test(path) || options.isProjectPath?.(path)))
1473
1536
  command = PROJECT_COMMAND;
1474
- else if (path === undefined && !wantsHelp && !head.includes('--version') && !head.includes('-V') && (options.tty || head.includes('--json')))
1475
- command = CENTER_COMMAND;
1537
+ else if (path === undefined && !wantsHelp && !head.includes('--version') && !head.includes('-V') && ((options.platform ?? process.platform) === 'linux' || options.tty || head.includes('--json')))
1538
+ command = (options.platform ?? process.platform) === 'linux' && !head.includes('--json') ? ONBOARDING_COMMAND : CENTER_COMMAND;
1476
1539
  }
1477
1540
  if (!command) {
1478
1541
  if (words.length === 0) {
@@ -1600,7 +1663,7 @@ export function commandHelp(c) {
1600
1663
  export function groupHelp(prefix) {
1601
1664
  const words = typeof prefix === 'string' ? [prefix] : [...prefix];
1602
1665
  const group = words.join(' ');
1603
- const cmds = COMMANDS.filter((c) => words.every((w, i) => c.path[i] === w));
1666
+ const cmds = COMMANDS.filter((c) => !c.hidden && words.every((w, i) => c.path[i] === w));
1604
1667
  const w = Math.max(...cmds.map((c) => usageLine(c).length));
1605
1668
  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';
1606
1669
  }
@@ -1618,14 +1681,14 @@ export const EXIT_CODE_HELP = [
1618
1681
  /** The top help's sections: every command's group (first word) is in exactly one (args.test.ts checks). */
1619
1682
  export const HELP_SECTIONS = [
1620
1683
  { 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'] },
1621
- { title: 'Cloud agents (shard ./ starts one; a project API key)', groups: ['conversations', 'machine', 'template', 'agents', 'ssh-proxy'] },
1684
+ { title: 'Cloud agents (shard ./ starts one; a project API key)', groups: ['conversations', 'machine', 'template', 'agents', 'device', 'cli-proxy', 'ssh-proxy'] },
1622
1685
  { 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'] },
1623
1686
  { title: 'Templates for your app’s workspaces (not for cloud agents; publish and archive use the session, the rest the API key)', groups: ['templates'] },
1624
1687
  { title: 'Other', groups: ['menubar', 'update', 'feedback', 'version'] },
1625
1688
  ];
1626
1689
  export function topHelp() {
1627
1690
  const w = Math.max(...COMMANDS.map((c) => usageLine(c).length));
1628
- const sections = HELP_SECTIONS.flatMap((s) => ['', `${s.title}:`, ...s.groups.flatMap((g) => COMMANDS.filter((c) => c.path[0] === g)).map((c) => ` ${usageLine(c).padEnd(w)} ${c.summary}`)]);
1691
+ const sections = HELP_SECTIONS.flatMap((s) => ['', `${s.title}:`, ...s.groups.flatMap((g) => COMMANDS.filter((c) => c.path[0] === g && !c.hidden)).map((c) => ` ${usageLine(c).padEnd(w)} ${c.summary}`)]);
1629
1692
  return [
1630
1693
  `shard ${CLI_VERSION} — the Shardflux command line (built on @shardflux/sdk)`,
1631
1694
  '',
@@ -0,0 +1,45 @@
1
+ import type { HubInfo } from './cli-proxy-hub.js';
2
+ export declare const REPLAY_LIMIT: number;
3
+ /** The forwarder answers this path itself (paths under /__shardflux/ are reserved by the edge, so no hub route is lost). */
4
+ export declare const IDENTITY_PATH = "/__shardflux/forwarder";
5
+ export interface ForwarderOptions {
6
+ /** The current hub info, read on every request (cheap: hubInfoSource caches by mtime). */
7
+ hub: () => HubInfo | null;
8
+ /** Starts CLIProxyAPI in the hub again (after `port_not_listening`). At most one runs at a time. */
9
+ repair?: () => Promise<void>;
10
+ /**
11
+ * Replaces the hub's callback URLs after the edge answers `callback_invalid` (the API key that created them was
12
+ * revoked) and gives VMs the new one. At most one runs at a time and at most one a minute; each request is renewed
13
+ * for once, so a URL that stays invalid is answered as is.
14
+ */
15
+ renew?: () => Promise<void>;
16
+ host?: string;
17
+ port?: number;
18
+ /** How long one request may be retried for (default 60 s, plus the time a repair takes). */
19
+ retryBudgetMs?: number;
20
+ /** Between attempts to bind a port another process holds (default 5 s). */
21
+ bindRetryMs?: number;
22
+ log?: (line: string) => void;
23
+ sleep?: (ms: number) => Promise<void>;
24
+ /** Request bodies kept for a retry, in bytes (default REPLAY_LIMIT); a larger body streams and is not retried. */
25
+ replayLimit?: number;
26
+ }
27
+ /** The hub info file, re-read when it changes (a replaced callback URL takes effect on the next request). */
28
+ export declare function hubInfoSource(dir: string): () => HubInfo | null;
29
+ export declare class CliProxyForwarder {
30
+ #private;
31
+ constructor(opts: ForwarderOptions);
32
+ get address(): {
33
+ host: string;
34
+ port: number;
35
+ } | null;
36
+ /**
37
+ * Listens on host:port. While another program holds the port (also one listening on all interfaces, which a
38
+ * loopback bind would not notice), logs once and tries again every bindRetryMs (until close()); resolves once
39
+ * listening.
40
+ */
41
+ listen(): Promise<void>;
42
+ /** Closes the idle keep-alive connections to the edge (the next request opens a fresh one). */
43
+ dropIdleConnections(): void;
44
+ close(): Promise<void>;
45
+ }