@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
@@ -11,10 +11,10 @@
11
11
  * Hub info (`cli-proxy.json`, 0600, in the config directory, in the hub and in the machine template so every VM has
12
12
  * it): { version, api_url, workspace_id, callback_url, callback_created_at, management_key }. Never printed or logged.
13
13
  */
14
- import { randomBytes } from 'node:crypto';
14
+ import { createHash, randomBytes } from 'node:crypto';
15
15
  import { readFileSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
- import { Document, isMap, parseDocument } from 'yaml';
17
+ import { Document, isMap, isScalar, isSeq, parseDocument } from 'yaml';
18
18
  import { ShardfluxApiError } from '@shardflux/sdk';
19
19
  import { writePrivateFile } from "./credentials.js";
20
20
  import { CodedError, UsageError } from "./errors.js";
@@ -42,21 +42,14 @@ export const HUB_TEMPLATE = 'codex-connector';
42
42
  /**
43
43
  * The codex-connector version a hub runs at least: the first with the hub's boot service and an sf-agent that serves
44
44
  * the VM port (cli-proxy-filter). New hubs are created on it; an older hub is upgraded in place (its home, and so its
45
- * logins and settings, stay) before VMs get the hub's address.
45
+ * logins and settings, stay) before VMs get the hub's address. A hub on a newer version keeps it (never downgraded).
46
46
  */
47
47
  export const HUB_TEMPLATE_VERSION = 4;
48
- /**
49
- * The codex-connector version whose sf-agent runs the account pool router: a hub set up for pooled accounts is created
50
- * on it, or upgraded to it. HUB_TEMPLATE_VERSION moves to it once it is registered in every environment.
51
- */
52
- export const POOL_HUB_TEMPLATE_VERSION = 5;
53
48
  export const HUB_CAPS = { cpu_millis: 1000, memory_mib: 256, disk_gib: 2, allocation_mode: 'fixed' };
54
49
  export const HUB_INFO_FILE = 'cli-proxy.json';
55
50
  /** Paths in the hub workspace. */
56
51
  export const HUB_DIR = '/home/user/.cli-proxy-hub';
57
52
  export const HUB_AUTH_DIR = '/home/user/.cli-proxy-api';
58
- /** Pool mode: the router reads the API URL and the hub token (`sfh_…`) from these hub files (0600). */
59
- export const HUB_POOL_FILES = { apiUrl: `${HUB_DIR}/api-url`, apiToken: `${HUB_DIR}/api-token` };
60
53
  const CALLBACK_PATH = /^\/__shardflux\/callback\/sfcb_[A-Za-z0-9_-]+\/$/;
61
54
  function callbackUrlOk(cb) {
62
55
  try {
@@ -315,7 +308,7 @@ export function sdkHubCloud(cloud, opts = {}) {
315
308
  }
316
309
  },
317
310
  async authFiles() {
318
- const r = await hub.sh(`[ -d ${HUB_AUTH_DIR} ] || exit 0; cd ${HUB_AUTH_DIR} && find . -type f -name '*.json' -printf '%P\\n'`);
311
+ const r = await hub.sh(AUTH_FILES_SCRIPT);
319
312
  if (r.code !== 0)
320
313
  throw new HubError(`Could not list the hub’s logins: ${r.stderr.trim() || `exit ${r.code}`}`);
321
314
  return r.stdout.split('\n').filter(Boolean);
@@ -380,6 +373,11 @@ export function sdkHubCloud(cloud, opts = {}) {
380
373
  probe: (url) => probeCallback(url),
381
374
  };
382
375
  }
376
+ /** The hub's auth files, oldest first (modification time, then path in byte order), one relative path per line. */
377
+ export const AUTH_FILES_SCRIPT = `set -euo pipefail
378
+ [ -d ${HUB_AUTH_DIR} ] || exit 0; cd ${HUB_AUTH_DIR}
379
+ find . -type f -name '*.json' -printf '%T@ %P\\n' | LC_ALL=C sort -t ' ' -k1,1n -k2 | cut -d ' ' -f 2-
380
+ `;
383
381
  /** Installs the pinned release once: download, verify its SHA-256, then move the binary into place. */
384
382
  export const INSTALL_SCRIPT = `set -euo pipefail
385
383
  v=${CLI_PROXY_VERSION}; dir=${HUB_DIR}; bin="$dir/bin/cli-proxy-api-$v"
@@ -600,12 +598,12 @@ export async function ensureCliProxyHub(cloud, opts) {
600
598
  const progress = opts.progress ?? (() => { });
601
599
  const local = readHubInfo(opts.dir, opts.apiUrl);
602
600
  progress('Opening the CLIProxyAPI hub…');
603
- // Pooled accounts need the router's template; everything else keeps the general one.
604
- const want = opts.pool ? POOL_HUB_TEMPLATE_VERSION : HUB_TEMPLATE_VERSION;
601
+ const want = HUB_TEMPLATE_VERSION;
605
602
  let opened = await hubCloud.open(!opts.repairOnly, want);
606
603
  if (!opened)
607
604
  throw new HubError('This account has no hosted CLIProxyAPI yet.', 'move yours: shard cli-proxy import');
608
- // A hub on an older template is upgraded before VMs get its address (a plain repair only starts it again).
605
+ // A hub on an older template is upgraded before VMs get its address (a plain repair only starts it again). One on a
606
+ // newer version (codex-connector 5) keeps it: its VM port serves exactly as on 4 once retirePool has run.
609
607
  const template = opened.template;
610
608
  if (!opened.created && (!opts.repairOnly || opts.renewInvalid) && hubCloud.upgrade && template?.version != null && template.version < want && (template.upgradeTo ?? 0) >= want) {
611
609
  progress('Updating the CLIProxyAPI hub…');
@@ -654,14 +652,11 @@ export async function ensureCliProxyHub(cloud, opts) {
654
652
  }
655
653
  if ((await ws.list(HUB_AUTH_DIR)) === null)
656
654
  await ws.sh(`mkdir -p -m 0700 ${HUB_AUTH_DIR}`);
657
- if (opts.pool) {
658
- // The router is in the hub's sf-agent: an older hub that could not be upgraded above cannot pool.
659
- const running = opened.template?.version;
660
- if (running != null && running < POOL_HUB_TEMPLATE_VERSION) {
661
- throw new HubError(`Pooled accounts need the hub on ${HUB_TEMPLATE} ${POOL_HUB_TEMPLATE_VERSION}; it runs ${running} and no update is published yet.`);
662
- }
663
- await setUpPool(ws, opts.apiUrl, opts.pool);
664
- }
655
+ // A hub that once ran the retired account pool router: its files go, and its sf-agent starts again without it.
656
+ if (await retirePool(ws))
657
+ restart = true;
658
+ // The user's logins stay as they are; only the retired account pool router's files are kept disabled.
659
+ const logins = await ws.withLock('logins', () => settleHubLogins(ws));
665
660
  const started = await ws.sh(START_SCRIPT, [restart ? '1' : '0'], { timeoutMs: 180_000 });
666
661
  const startLines = started.stdout.trim().split('\n');
667
662
  const state = startLines.find((l) => ['running', 'started', 'restarted'].includes(l));
@@ -681,46 +676,199 @@ export async function ensureCliProxyHub(cloud, opts) {
681
676
  };
682
677
  if (!local || hubInfoText(local) !== hubInfoText(info))
683
678
  writeHubInfo(opts.dir, info);
684
- return { info, created, process: state, callbackReplaced, config };
679
+ return { info, created, process: state, callbackReplaced, config, logins };
680
+ }
681
+ const docOf = (text) => {
682
+ try {
683
+ const v = JSON.parse(text ?? '');
684
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : null;
685
+ }
686
+ catch {
687
+ return null;
688
+ }
689
+ };
690
+ const publicLogins = (logins) => logins.filter((l) => !l.pool).map(({ file, provider, email, disabled, active }) => ({ file, provider, email, disabled, active }));
691
+ /** The hub's logins, oldest first, as they are (nothing changes). Files the retired account pool router left are not listed. */
692
+ export async function listHubLogins(ws) {
693
+ return publicLogins(await readHubLogins(ws));
694
+ }
695
+ /** The hub's logins, oldest first (the order authFiles lists them in). */
696
+ async function readHubLogins(ws) {
697
+ const files = await ws.authFiles();
698
+ const texts = await Promise.all(files.map((f) => ws.read(`${HUB_AUTH_DIR}/${f}`)));
699
+ return files.map((file, i) => {
700
+ const text = texts[i] ?? null;
701
+ const doc = docOf(text);
702
+ const type = typeof doc?.type === 'string' && doc.type.trim() ? doc.type.trim().toLowerCase() : null;
703
+ const email = typeof doc?.email === 'string' && doc.email ? doc.email : null;
704
+ // The retired pool router's files (`sfa-<account id>.json`, `"prefix": "sfa-…"`) were never the user's logins.
705
+ const pool = /^sfa-[^/]+\.json$/.test(file) || (typeof doc?.prefix === 'string' && doc.prefix.startsWith('sfa-'));
706
+ const disabled = doc?.disabled === true;
707
+ return { file, provider: type, email, disabled, active: !pool && !disabled && type !== null, text, doc, pool };
708
+ });
709
+ }
710
+ /** The auth file's text with `disabled` set (true) or removed (false), every other field kept; null when unchanged. */
711
+ export function withDisabled(text, disabled) {
712
+ const doc = docOf(text);
713
+ if (!doc || (doc.disabled === true) === disabled)
714
+ return null;
715
+ if (disabled)
716
+ doc.disabled = true;
717
+ else
718
+ delete doc.disabled;
719
+ return `${JSON.stringify(doc, null, text.trimEnd().includes('\n') ? 2 : undefined)}${text.endsWith('\n') ? '\n' : ''}`;
720
+ }
721
+ /**
722
+ * Replaces one auth file atomically (0600, a temporary file in its folder then rename), only while it still holds what
723
+ * the device read ($2, its SHA-256): CLIProxyAPI rewrites a file when it refreshes the login, and a newer token must
724
+ * never be overwritten. The modification time stays, so the order of the hub's logins (oldest first) does not change.
725
+ * $1 is the path relative to the auth directory, the new content comes on standard input. Prints `written`, `changed`
726
+ * or `gone`.
727
+ */
728
+ export const SET_AUTH_SCRIPT = `set -euo pipefail
729
+ a=${HUB_AUTH_DIR}; rel=$1; want=$2
730
+ case "$rel" in ''|/*|*..*) echo "not a path inside the auth folder" >&2; exit 2 ;; esac
731
+ f="$a/$rel"
732
+ [ -f "$f" ] || { echo gone; exit 0; }
733
+ tmp=$(mktemp "$(dirname "$f")/.writing.XXXXXX"); trap 'rm -f "$tmp"' EXIT
734
+ chmod 0600 "$tmp"; cat > "$tmp"; touch -r "$f" "$tmp"
735
+ if [ "$(sha256sum < "$f" | cut -d' ' -f1)" != "$want" ]; then echo changed; exit 0; fi
736
+ mv -f "$tmp" "$f"; trap - EXIT
737
+ echo written
738
+ `;
739
+ const sha256 = (text) => createHash('sha256').update(text, 'utf8').digest('hex');
740
+ /** Sets one login's `disabled` (true) or clears it (false), reading the file again when CLIProxyAPI changed it meanwhile. */
741
+ async function setDisabled(ws, login, disabled) {
742
+ let text = login.text;
743
+ for (let attempt = 0; attempt < 4; attempt++) {
744
+ if (text === null)
745
+ return;
746
+ const next = withDisabled(text, disabled);
747
+ if (next === null)
748
+ return;
749
+ const r = await ws.sh(SET_AUTH_SCRIPT, [login.file, sha256(text)], { stdin: next });
750
+ const out = r.stdout.trim();
751
+ if (r.code !== 0)
752
+ throw new HubError(`Could not update ${login.file} in the hub: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, 'run: shard cli-proxy import');
753
+ if (out === 'written' || out === 'gone')
754
+ return;
755
+ text = await ws.read(`${HUB_AUTH_DIR}/${login.file}`);
756
+ }
757
+ throw new HubError(`${login.file} kept changing in the hub while it was being ${disabled ? 'disabled' : 'enabled'}.`, 'run: shard cli-proxy import');
758
+ }
759
+ /**
760
+ * Under the hub's `logins` lock: the retired account pool router's files stay disabled (they were never the user's);
761
+ * every other login keeps the state its owner gave it. Returns the logins as they are now, oldest first.
762
+ */
763
+ export async function settleHubLogins(ws) {
764
+ const logins = await readHubLogins(ws);
765
+ for (const l of logins)
766
+ if (l.pool && !l.disabled)
767
+ await setDisabled(ws, l, true);
768
+ return publicLogins(logins);
685
769
  }
686
770
  /**
687
- * The hub config with the providers' rate-limit headers passed through (`requests.passthrough-headers`, or the legacy
688
- * top-level key): CLIProxyAPI 8.0.23 records them per login only for non-streaming answers, so the pool router reads
689
- * them on its own answers. Returns the text unchanged when it already passes them.
771
+ * The user's choice of which logins serve requests (`shard cli-proxy use`; call it under the hub's `logins` lock).
772
+ * `only[provider]` names the one login of that provider that stays enabled (it is enabled first, then the provider's
773
+ * other logins are disabled, so the provider is never without one); `all` enables every login of the hub, which then
774
+ * routes across them as its config says. Providers the choice does not name are left as they are. Returns the logins
775
+ * as they are now, oldest first.
690
776
  */
691
- export function poolHubConfig(text) {
692
- let doc = parseDocument(text || '{}');
777
+ export async function useHubLogins(ws, choice) {
778
+ const logins = await readHubLogins(ws);
779
+ const only = choice.only ?? {};
780
+ for (const [p, file] of Object.entries(only)) {
781
+ if (!logins.some((l) => !l.pool && l.provider === p && l.file === file))
782
+ throw new HubError(`The hub has no ${p} login ${file} any more.`, 'see the hub’s logins: shard cli-proxy');
783
+ }
784
+ const wanted = (l) => {
785
+ if (l.pool || l.provider === null)
786
+ return null;
787
+ if (only[l.provider] !== undefined)
788
+ return only[l.provider] === l.file;
789
+ return choice.all ? true : null;
790
+ };
791
+ for (const l of logins)
792
+ if (wanted(l) === true && l.disabled)
793
+ await setDisabled(ws, l, false);
794
+ for (const l of logins)
795
+ if (wanted(l) === false && !l.disabled)
796
+ await setDisabled(ws, l, true);
797
+ // Computers still on CLI 0.22.0 keep the login active.json names for each provider: it names this choice too.
798
+ if (Object.keys(only).length) {
799
+ const text = await ws.read(LEGACY_ACTIVE_FILE);
800
+ let stored = {};
801
+ try {
802
+ const v = JSON.parse(text ?? '');
803
+ if (v && typeof v === 'object' && !Array.isArray(v))
804
+ stored = v;
805
+ }
806
+ catch { /* none yet */ }
807
+ const next = `${JSON.stringify({ ...stored, ...only }, null, 2)}\n`;
808
+ if (next !== text) {
809
+ const r = await ws.sh(LEGACY_ACTIVE_SCRIPT, [], { stdin: next });
810
+ if (r.code !== 0)
811
+ throw new HubError(`The hub did not record the choice: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, 'run: shard cli-proxy use <email>');
812
+ }
813
+ }
814
+ return listHubLogins(ws);
815
+ }
816
+ /** Where CLI 0.22.0 reads each provider's one login ({"<provider>": "<auth file>"}); written atomically (0600). */
817
+ export const LEGACY_ACTIVE_FILE = `${HUB_DIR}/active.json`;
818
+ export const LEGACY_ACTIVE_SCRIPT = `set -euo pipefail
819
+ dir=${HUB_DIR}; mkdir -p "$dir"; umask 077
820
+ tmp=$(mktemp "$dir/.active.XXXXXX"); trap 'rm -f "$tmp"' EXIT
821
+ cat > "$tmp"; mv -f "$tmp" "$dir/active.json"; trap - EXIT
822
+ echo written
823
+ `;
824
+ /** What the retired account pool router kept in the hub (codex-connector 5 with pooled accounts). */
825
+ const POOL_LEFTOVERS = ['api-token', 'api-url', 'pool.json', 'pool-creds.json', 'pool-status.json', 'pool-client.key', 'pool.lock'];
826
+ /** Removes the router's files; prints `retired` when its hub token was there (its sf-agent then starts again without it). */
827
+ export const RETIRE_POOL_SCRIPT = `set -euo pipefail
828
+ cd ${HUB_DIR} 2>/dev/null || exit 0
829
+ router=no; if [ -e api-token ] || [ -e api-url ]; then router=yes; fi
830
+ rm -f ${POOL_LEFTOVERS.join(' ')}
831
+ if [ "$router" = yes ]; then echo retired; fi
832
+ `;
833
+ /** The config without the router's own client key (`api-keys`, or v8 `access.api-keys`); the text itself when absent. */
834
+ export function withoutClientKey(text, key) {
835
+ const doc = parseDocument(text);
693
836
  if (doc.errors.length > 0 || !isMap(doc.contents))
694
- doc = new Document({});
695
- const v8 = doc.hasIn(['config-version']) || V8_SECTIONS.some((k) => isMap(doc.getIn([k])));
696
- const path = v8 ? ['requests', 'passthrough-headers'] : ['passthrough-headers'];
697
- if (doc.getIn(path) === true)
698
837
  return text;
699
- if (v8 && doc.hasIn(['passthrough-headers']))
700
- doc.deleteIn(['passthrough-headers']);
701
- doc.setIn(path, true);
702
- return doc.toString();
703
- }
704
- const HUB_TOKEN_RE = /^sfh_[A-Za-z0-9_-]{43}$/;
705
- /** Pool mode in the hub: the API URL, the hub token (minted once, or again with renew) and header passthrough. */
706
- async function setUpPool(ws, apiUrl, pool) {
707
- await ws.withLock('pool', async () => {
708
- if ((await ws.read(HUB_POOL_FILES.apiUrl))?.trim() !== apiUrl)
709
- await ws.write(HUB_POOL_FILES.apiUrl, `${apiUrl}\n`, '0600');
710
- const have = (await ws.read(HUB_POOL_FILES.apiToken))?.trim() ?? '';
711
- if (pool.renew || !HUB_TOKEN_RE.test(have)) {
712
- const token = (await pool.hubToken(ws.id)).trim();
713
- if (!HUB_TOKEN_RE.test(token))
714
- throw new HubError('The Shardflux API did not return a hub token.');
715
- await ws.write(HUB_POOL_FILES.apiToken, `${token}\n`, '0600');
838
+ let changed = false;
839
+ for (const path of [['api-keys'], ['access', 'api-keys']]) {
840
+ const seq = doc.getIn(path, true);
841
+ if (!isSeq(seq))
842
+ continue;
843
+ const keep = seq.items.filter((item) => (isScalar(item) ? item.value : item) !== key);
844
+ if (keep.length !== seq.items.length) {
845
+ seq.items = keep;
846
+ changed = true;
716
847
  }
717
- const configPath = `${HUB_DIR}/config.yaml`;
718
- const config = (await ws.read(configPath)) ?? '';
719
- const next = poolHubConfig(config);
720
- // A running CLIProxyAPI reloads its config by itself; the router starts within a minute of the token.
848
+ }
849
+ return changed ? doc.toString() : text;
850
+ }
851
+ /**
852
+ * A hub that ran the account pool router (codex-connector 5): its hub token, API URL and router state go, and so does
853
+ * the router's client key in the hub's config. Returns whether the router could be running (the hub then restarts its
854
+ * sf-agent, which starts without it). A hub that never pooled costs one directory listing.
855
+ */
856
+ async function retirePool(ws) {
857
+ const names = (await ws.list(HUB_DIR)) ?? [];
858
+ if (!names.some((n) => POOL_LEFTOVERS.includes(n)))
859
+ return false;
860
+ const key = (await ws.read(`${HUB_DIR}/pool-client.key`))?.trim();
861
+ const configPath = `${HUB_DIR}/config.yaml`;
862
+ const config = key ? await ws.read(configPath) : null;
863
+ if (key && config !== null) {
864
+ const next = withoutClientKey(config, key);
721
865
  if (next !== config)
722
866
  await ws.write(configPath, next, '0600');
723
- });
867
+ }
868
+ const r = await ws.sh(RETIRE_POOL_SCRIPT);
869
+ if (r.code !== 0)
870
+ throw new HubError(`The hub did not remove the account pool’s files: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, 'run: shard cli-proxy import');
871
+ return r.stdout.trim() === 'retired';
724
872
  }
725
873
  /**
726
874
  * The hub's one callback URL. Under the hub's callback lock: the stored one is kept while the port's callback still
@@ -211,6 +211,10 @@ export interface HubAccount {
211
211
  email: string | null;
212
212
  status: string | null;
213
213
  disabled: boolean;
214
+ /** The auth file, relative to the hub's auth directory (null when CLIProxyAPI does not name it). */
215
+ file: string | null;
216
+ /** It serves requests (enabled); a disabled login stays in the hub unused. */
217
+ active: boolean;
214
218
  }
215
219
  /** The hub's logins from its management API (wakes the hub). */
216
220
  export declare function hubAccounts(info: HubInfo, timeoutMs?: number): Promise<HubAccount[]>;
@@ -230,6 +234,12 @@ export interface ImportOptions {
230
234
  machine?: boolean;
231
235
  platform?: NodeJS.Platform;
232
236
  }
237
+ /** A login named by provider, email and auth file (never its tokens). */
238
+ export interface LoginRef {
239
+ provider: string;
240
+ email: string | null;
241
+ file: string;
242
+ }
233
243
  export interface ImportResult {
234
244
  hub: {
235
245
  workspace_id: string;
@@ -238,7 +248,8 @@ export interface ImportResult {
238
248
  };
239
249
  /**
240
250
  * added: accounts this import gave the hub; already: accounts the hub had (its tokens stay); moved_aside: where this
241
- * computer's auth files went once the hub had them (`<dir>.moved-to-shardflux`).
251
+ * computer's auth files went once the hub had them (`<dir>.moved-to-shardflux`). active: the logins that serve
252
+ * requests; disabled: logins kept in the hub unused (as they were on the computer, or by --use).
242
253
  */
243
254
  logins: {
244
255
  uploaded: string[];
@@ -252,6 +263,8 @@ export interface ImportResult {
252
263
  email: string | null;
253
264
  }>;
254
265
  moved_aside: string[];
266
+ active: LoginRef[];
267
+ disabled: LoginRef[];
255
268
  };
256
269
  /** Whether the hub's config was made from this computer's, or the hub kept the one it has. */
257
270
  config: 'from_this_computer' | 'kept';
@@ -271,11 +284,21 @@ export interface ImportResult {
271
284
  dropped: string[];
272
285
  accounts: HubAccount[] | null;
273
286
  }
287
+ /** The logins a `--use` or `shard cli-proxy use` value names: by email (any case), path in the auth folder, or file name. */
288
+ export declare function matchLogins<T extends {
289
+ provider: string | null;
290
+ email: string | null;
291
+ file: string;
292
+ }>(spec: string, logins: readonly T[]): T[];
274
293
  /**
275
294
  * Moves CLIProxyAPI from this computer into the account (or connects this computer to the account's hub): ensure the
276
295
  * hub from the user's config, stop the local copy, upload the auth files the hub does not have yet (never overwriting
277
296
  * one: the hub's copy may hold a newer refresh token), start the forwarder on 127.0.0.1:8317 and put the hub info into
278
297
  * the machine template.
298
+ *
299
+ * Each login arrives as it is on this computer (enabled or disabled), and the hub routes across the enabled ones as
300
+ * the user's config says. `--use <email|file>` (repeatable, one per provider) makes that login the only one serving
301
+ * its provider (the provider's others are kept in the hub, disabled), as `shard cli-proxy use` does.
279
302
  */
280
303
  export declare function importCliProxy(ctx: Ctx, opts?: ImportOptions): Promise<ImportResult>;
281
304
  export interface ExportOptions {
@@ -306,6 +329,26 @@ export interface ExportResult {
306
329
  * Undone by `shard cli-proxy import`.
307
330
  */
308
331
  export declare function exportCliProxy(ctx: Ctx, opts?: ExportOptions): Promise<ExportResult>;
332
+ export interface UseResult {
333
+ /** Each provider this command set to one login, with the login that served it alone before (null if not one) and the one now. */
334
+ switched: Array<{
335
+ provider: string;
336
+ from: LoginRef | null;
337
+ to: LoginRef;
338
+ }>;
339
+ /** The logins that serve requests now, and the ones kept in the hub, disabled. */
340
+ active: LoginRef[];
341
+ disabled: LoginRef[];
342
+ }
343
+ /**
344
+ * `shard cli-proxy use <email|file>`: that login alone serves its provider (an email with a Claude and a Codex login
345
+ * sets both); the provider's other logins stay in the hub, disabled. `--all`: every login serves again, and the hub
346
+ * routes across them as its config says. Under the hub's logins lock.
347
+ */
348
+ export declare function useCliProxyLogin(ctx: Ctx, spec: string, opts?: {
349
+ hub?: HubCloud;
350
+ all?: boolean;
351
+ }): Promise<UseResult>;
309
352
  /**
310
353
  * After the edge refused the hub's callback URL (`callback_invalid`: the API key that created it was revoked):
311
354
  * replaces the URLs the edge refuses (under the hub's lock), saves them locally and gives VMs the new VM address.