@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/cli-proxy.js CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
- * `shard cli-proxy` (status), `shard cli-proxy import` and the hidden `shard cli-proxy forward`: one hosted
2
+ * `shard cli-proxy` (status), `shard cli-proxy import`, `shard cli-proxy use` and the hidden `shard cli-proxy forward`: one hosted
3
3
  * CLIProxyAPI per account (cli-proxy-hub.ts), reached at http://127.0.0.1:8317 on this computer (a launchd agent runs
4
4
  * the forwarder, cli-proxy-forward.ts) and in every VM (the hub info in the machine template; sf-agent forwards there).
5
5
  *
6
6
  * Import moves the user's CLIProxyAPI from this computer: the hub is made from their config, the local copy stops
7
7
  * (only one copy may refresh a login), their auth files go to the hub (never printed), the forwarder takes 127.0.0.1:8317
8
8
  * and the machine template carries the hub info. Onboarding calls `detectLocalCliProxy()` and `importCliProxy(ctx)`.
9
+ *
10
+ * The hub is the user's own CLIProxyAPI: it routes requests across their logins as their config says, and each login
11
+ * stays enabled or disabled as they left it. `shard cli-proxy use` is their switch: one login for a provider, or all.
9
12
  */
10
13
  import { execFile } from 'node:child_process';
11
14
  import { createHash } from 'node:crypto';
@@ -18,7 +21,7 @@ import { parse as parseYaml } from 'yaml';
18
21
  import { AgentStateStore } from "./agent-state.js";
19
22
  import { syncAgentConfig } from "./agent-config-sync.js";
20
23
  import { CliProxyForwarder, IDENTITY_PATH, hubInfoSource } from "./cli-proxy-forward.js";
21
- import { ADD_AUTH_SCRIPT, CLI_PROXY_PORT, CLI_PROXY_VM_PORT, HUB_AUTH_DIR, HUB_KEY, HubError, SET_ASIDE_SCRIPT, STOP_SCRIPT, ensureCliProxyHub, hubCallback, readHubInfo, sdkHubCloud, vmHubInfoText, writeHubInfo } from "./cli-proxy-hub.js";
24
+ import { ADD_AUTH_SCRIPT, CLI_PROXY_PORT, CLI_PROXY_VM_PORT, HUB_AUTH_DIR, HUB_KEY, HubError, SET_ASIDE_SCRIPT, STOP_SCRIPT, ensureCliProxyHub, hubCallback, listHubLogins, readHubInfo, sdkHubCloud, useHubLogins, vmHubInfoText, writeHubInfo } from "./cli-proxy-hub.js";
22
25
  import { CLI_VERSION } from "./args.js";
23
26
  import { UsageError } from "./errors.js";
24
27
  import { json } from "./format.js";
@@ -625,6 +628,12 @@ export function sdkMachine(ctx) {
625
628
  },
626
629
  };
627
630
  }
631
+ /** The auth file a management API entry names, relative to the hub's auth directory. */
632
+ function entryFile(f) {
633
+ if (typeof f.path === 'string' && f.path.startsWith(`${HUB_AUTH_DIR}/`))
634
+ return f.path.slice(HUB_AUTH_DIR.length + 1);
635
+ return [f.id, f.name].find((v) => typeof v === 'string' && v !== '') ?? null;
636
+ }
628
637
  /** The hub's logins from its management API (wakes the hub). */
629
638
  export async function hubAccounts(info, timeoutMs = 45_000) {
630
639
  const res = await fetch(new URL('v0/management/auth-files', info.callback_url), {
@@ -640,6 +649,8 @@ export async function hubAccounts(info, timeoutMs = 45_000) {
640
649
  email: typeof f.email === 'string' && f.email ? f.email : null,
641
650
  status: typeof f.status === 'string' && f.status ? f.status : null,
642
651
  disabled: f.disabled === true,
652
+ file: entryFile(f),
653
+ active: f.disabled !== true,
643
654
  }));
644
655
  }
645
656
  /** Whether the hub answers /healthz through its callback URL, and the edge's reason when it does not. */
@@ -654,11 +665,61 @@ export async function hubHealth(info, timeoutMs = 45_000) {
654
665
  }
655
666
  }
656
667
  const accountLine = (a) => ` ${a.provider.padEnd(8)} ${a.email ?? '(no email)'}${a.disabled ? ' disabled' : a.status && a.status !== 'active' ? ` ${a.status}` : ''}`;
668
+ /** The logins a `--use` or `shard cli-proxy use` value names: by email (any case), path in the auth folder, or file name. */
669
+ export function matchLogins(spec, logins) {
670
+ const want = spec.trim().toLowerCase();
671
+ return logins.filter((l) => l.provider !== null && want !== '' && (l.email?.toLowerCase() === want || l.file.toLowerCase() === want || basename(l.file).toLowerCase() === want));
672
+ }
673
+ const loginName = (l) => l.email ?? l.file;
674
+ const refs = (logins, active) => logins.filter((l) => l.provider !== null && l.active === active).map((l) => ({ provider: l.provider, email: l.email, file: l.file }));
675
+ /**
676
+ * Resolves `--use` (repeatable): each value names the one login that serves its provider, on this computer or already
677
+ * in the hub.
678
+ * Checked before anything changes, so a typo stops the import with the logins to pick from.
679
+ */
680
+ function resolveUse(specs, local, hub) {
681
+ const out = new Map();
682
+ const localLogins = (local?.logins ?? []).map((l) => {
683
+ let text = '';
684
+ try {
685
+ text = readFileSync(join(local.authDir, l.file), 'utf8');
686
+ }
687
+ catch { /* unreadable: matched by name only */ }
688
+ return { ...l, identity: loginIdentity(text, l.file) };
689
+ });
690
+ const by = new Map();
691
+ for (const spec of specs) {
692
+ const mineHere = matchLogins(spec, localLogins), inHub = matchLogins(spec, hub);
693
+ if (!mineHere.length && !inHub.length) {
694
+ const all = [...localLogins.filter((l) => l.provider), ...hub.filter((l) => l.provider)].map((l) => `${l.provider} ${loginName(l)}`);
695
+ throw new UsageError(`No login matches --use ${spec}.`, all.length ? `pick one of: ${[...new Set(all)].join(', ')}` : 'name a login by its email');
696
+ }
697
+ for (const provider of new Set([...mineHere, ...inHub].map((l) => l.provider))) {
698
+ const here = mineHere.filter((l) => l.provider === provider), there = inHub.filter((l) => l.provider === provider);
699
+ const ids = new Set(here.map((l) => l.identity));
700
+ if (ids.size > 1 || (!here.length && there.length > 1)) {
701
+ throw new UsageError(`--use ${spec} matches more than one ${provider} login (${[...here, ...there].map((l) => l.file).join(', ')}).`, 'name one by its file name: --use <file>');
702
+ }
703
+ const choice = here[0] ? { local: here[0].file, identity: here[0].identity } : { hub: there[0].file };
704
+ const earlier = by.get(provider);
705
+ if (earlier !== undefined && earlier !== spec)
706
+ throw new UsageError(`--use ${earlier} and --use ${spec} both name a ${provider} login; --use names one login per provider.`, 'keep one --use per provider');
707
+ by.set(provider, spec);
708
+ out.set(provider, choice);
709
+ }
710
+ }
711
+ return out;
712
+ }
713
+ const useSpecs = (v) => (Array.isArray(v) ? v : typeof v === 'string' ? [v] : []).filter((x) => x.trim() !== '');
657
714
  /**
658
715
  * Moves CLIProxyAPI from this computer into the account (or connects this computer to the account's hub): ensure the
659
716
  * hub from the user's config, stop the local copy, upload the auth files the hub does not have yet (never overwriting
660
717
  * 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
661
718
  * the machine template.
719
+ *
720
+ * Each login arrives as it is on this computer (enabled or disabled), and the hub routes across the enabled ones as
721
+ * the user's config says. `--use <email|file>` (repeatable, one per provider) makes that login the only one serving
722
+ * its provider (the provider's others are kept in the hub, disabled), as `shard cli-proxy use` does.
662
723
  */
663
724
  export async function importCliProxy(ctx, opts = {}) {
664
725
  if (cliProxyTools.inVm()) {
@@ -676,6 +737,8 @@ export async function importCliProxy(ctx, opts = {}) {
676
737
  const ensured = await ensureCliProxyHub(hub, { dir: ctx.store.dir, apiUrl: ctx.apiUrl, ...(userConfig !== undefined ? { userConfig } : {}), progress: (m) => progress(ctx, m) });
677
738
  for (const value of [ensured.info.callback_url, ensured.info.management_key])
678
739
  ctx.addSecret(value);
740
+ // Which login of each provider is active: checked against this computer's and the hub's before anything is stopped.
741
+ const use = resolveUse(useSpecs(values.use), local, ensured.logins);
679
742
  // The forwarder takes 127.0.0.1:8317: refuse before stopping anything when another program holds it.
680
743
  if (local && local.port !== CLI_PROXY_PORT) {
681
744
  const answers = opts.stop?.answers ?? cliProxyTools.answers;
@@ -689,7 +752,8 @@ export async function importCliProxy(ctx, opts = {}) {
689
752
  const uploaded = [], kept = [];
690
753
  const added = [], already = [];
691
754
  let movedAside = [];
692
- if (local?.logins.length) {
755
+ let hubLogins = ensured.logins;
756
+ if (local?.logins.length || use.size) {
693
757
  const opened = await hub.open(false);
694
758
  if (!opened)
695
759
  throw new HubError('The hub was deleted during the import.', `run: ${AGAIN}`);
@@ -697,34 +761,13 @@ export async function importCliProxy(ctx, opts = {}) {
697
761
  // auth file's JSON, the hub's and this computer's), under the hub's logins lock, so two computers importing at
698
762
  // once never add one account twice and an account the hub holds keeps its newer tokens, whatever the file names.
699
763
  await opened.ws.withLock('logins', async () => {
700
- const inHub = new Set();
701
- const present = await opened.ws.authFiles();
702
- for (const rel of present) {
703
- const text = await opened.ws.read(`${HUB_AUTH_DIR}/${rel}`);
704
- inHub.add(text === null ? `file:${rel}` : loginIdentity(text, rel));
705
- }
706
- const names = new Set(present);
707
- for (const login of local.logins) {
708
- const text = readFileSync(join(local.authDir, login.file), 'utf8');
709
- const account = loginIdentity(text, login.file);
710
- const r = inHub.has(account) || names.has(login.file) ? null : await opened.ws.sh(ADD_AUTH_SCRIPT, [login.file], { stdin: text });
711
- if (r && r.code !== 0)
712
- throw new HubError(`Could not add ${login.file} to the hub: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, `run: ${AGAIN}`);
713
- if (!r || r.stdout.trim() === 'exists') {
714
- kept.push(login.file);
715
- already.push({ provider: login.provider, email: login.email });
716
- continue;
717
- }
718
- uploaded.push(login.file);
719
- added.push({ provider: login.provider, email: login.email });
720
- inHub.add(account);
721
- names.add(login.file);
722
- }
764
+ hubLogins = await uploadLogins(opened.ws, local, use, { uploaded, kept, added, already });
723
765
  });
724
766
  if (uploaded.length)
725
767
  progress(ctx, `Moved ${uploaded.length} login${uploaded.length === 1 ? '' : 's'} to the hub`);
726
768
  // The hub holds every login now: no local copy keeps tokens it could refresh.
727
- movedAside = moveAuthFilesAside(local.authDirs);
769
+ if (local?.logins.length)
770
+ movedAside = moveAuthFilesAside(local.authDirs);
728
771
  }
729
772
  if (local) {
730
773
  new AgentStateStore(ctx.store.dir).update((doc) => {
@@ -760,9 +803,50 @@ export async function importCliProxy(ctx, opts = {}) {
760
803
  const accounts = await hubAccounts(ensured.info).catch(() => null);
761
804
  return {
762
805
  hub: { workspace_id: ensured.info.workspace_id, created: ensured.created, process: ensured.process },
763
- logins: { uploaded, kept, added, already, moved_aside: movedAside.map((m) => m.to) }, config: ensured.config ? 'from_this_computer' : 'kept', local: stop.result, port: local?.port ?? CLI_PROXY_PORT, forwarder: { launchd }, machine, dropped: ensured.config?.dropped ?? [], accounts,
806
+ logins: { uploaded, kept, added, already, moved_aside: movedAside.map((m) => m.to), active: refs(hubLogins, true), disabled: refs(hubLogins, false) },
807
+ config: ensured.config ? 'from_this_computer' : 'kept', local: stop.result, port: local?.port ?? CLI_PROXY_PORT, forwarder: { launchd }, machine, dropped: ensured.config?.dropped ?? [], accounts,
764
808
  };
765
809
  }
810
+ /**
811
+ * Under the hub's logins lock: uploads this computer's logins the hub lacks, each as it is (enabled or disabled), then
812
+ * applies --use. Returns the hub's logins after.
813
+ */
814
+ async function uploadLogins(ws, local, use, out) {
815
+ const inHub = new Map();
816
+ const present = await ws.authFiles();
817
+ for (const rel of present) {
818
+ const text = await ws.read(`${HUB_AUTH_DIR}/${rel}`);
819
+ const id = text === null ? `file:${rel}` : loginIdentity(text, rel);
820
+ if (!inHub.has(id))
821
+ inHub.set(id, rel);
822
+ }
823
+ const names = new Set(present);
824
+ for (const login of local?.logins ?? []) {
825
+ const text = readFileSync(join(local.authDir, login.file), 'utf8');
826
+ const identity = loginIdentity(text, login.file);
827
+ const r = inHub.has(identity) || names.has(login.file) ? null : await ws.sh(ADD_AUTH_SCRIPT, [login.file], { stdin: text });
828
+ if (r && r.code !== 0)
829
+ throw new HubError(`Could not add ${login.file} to the hub: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, `run: ${AGAIN}`);
830
+ if (!r || r.stdout.trim() === 'exists') {
831
+ out.kept.push(login.file);
832
+ out.already.push({ provider: login.provider, email: login.email });
833
+ continue;
834
+ }
835
+ out.uploaded.push(login.file);
836
+ out.added.push({ provider: login.provider, email: login.email });
837
+ inHub.set(identity, login.file);
838
+ names.add(login.file);
839
+ }
840
+ if (!use.size)
841
+ return listHubLogins(ws);
842
+ const only = {};
843
+ for (const [provider, choice] of use) {
844
+ const file = 'hub' in choice ? choice.hub : inHub.get(choice.identity);
845
+ if (file !== undefined)
846
+ only[provider] = file;
847
+ }
848
+ return useHubLogins(ws, { only });
849
+ }
766
850
  /**
767
851
  * Brings the hub's logins back to this computer: the hub stops first (from then on only this computer refreshes
768
852
  * them), its auth files as last refreshed are written to the local auth directory (0600), the hub sets its copies
@@ -840,6 +924,69 @@ async function exportHandler(ctx, values) {
840
924
  ].join('\n') + '\n');
841
925
  return 0;
842
926
  }
927
+ /**
928
+ * `shard cli-proxy use <email|file>`: that login alone serves its provider (an email with a Claude and a Codex login
929
+ * sets both); the provider's other logins stay in the hub, disabled. `--all`: every login serves again, and the hub
930
+ * routes across them as its config says. Under the hub's logins lock.
931
+ */
932
+ export async function useCliProxyLogin(ctx, spec, opts = {}) {
933
+ if (cliProxyTools.inVm())
934
+ throw new UsageError('shard cli-proxy use runs on a computer connected to your hosted CLIProxyAPI; this is a Shardflux VM.', 'run it on that computer: shard cli-proxy use <email>');
935
+ if (!opts.all && !spec.trim())
936
+ throw new UsageError('Name the login to use, or --all for every login.', 'run: shard cli-proxy use <email>');
937
+ if (opts.all && spec.trim())
938
+ throw new UsageError('Name a login or use --all, not both.', 'run: shard cli-proxy use --all');
939
+ const hub = opts.hub ?? sdkHubCloud(ctx.cloud(), { signal: ctx.signal, wakeTimeoutMs: ctx.wakeTimeoutMs, agentLabel: ctx.agentLabel });
940
+ const opened = await hub.open(false);
941
+ if (!opened)
942
+ throw new UsageError('This account has no hosted CLIProxyAPI yet.', `move yours: ${AGAIN}`);
943
+ return opened.ws.withLock('logins', async () => {
944
+ const before = await listHubLogins(opened.ws);
945
+ if (opts.all) {
946
+ const after = await useHubLogins(opened.ws, { all: true });
947
+ return { switched: [], active: refs(after, true), disabled: refs(after, false) };
948
+ }
949
+ const matches = matchLogins(spec, before);
950
+ if (!matches.length) {
951
+ const all = before.filter((l) => l.provider).map((l) => `${l.provider} ${loginName(l)}`);
952
+ throw new UsageError(`No login in your hosted CLIProxyAPI matches ${spec}.`, all.length ? `pick one of: ${all.join(', ')}` : `add yours: ${AGAIN}`);
953
+ }
954
+ const only = {};
955
+ for (const provider of new Set(matches.map((l) => l.provider))) {
956
+ const mine = matches.filter((l) => l.provider === provider);
957
+ if (mine.length > 1)
958
+ throw new UsageError(`${spec} matches more than one ${provider} login (${mine.map((l) => l.file).join(', ')}).`, 'name one by its file name: shard cli-proxy use <file>');
959
+ only[provider] = mine[0].file;
960
+ }
961
+ const after = await useHubLogins(opened.ws, { only });
962
+ const ref = (l) => ({ provider: l.provider, email: l.email, file: l.file });
963
+ const switched = Object.entries(only).map(([provider, file]) => {
964
+ const was = before.filter((l) => l.provider === provider && l.active);
965
+ return { provider, from: was.length === 1 ? ref(was[0]) : null, to: ref(after.find((l) => l.file === file)) };
966
+ });
967
+ return { switched, active: refs(after, true), disabled: refs(after, false) };
968
+ });
969
+ }
970
+ async function useHandler(ctx, values, positionals) {
971
+ const spec = positionals[0] ?? (typeof values.login === 'string' ? values.login : '');
972
+ const r = await useCliProxyLogin(ctx, spec, { all: values.all === true });
973
+ if (ctx.json) {
974
+ ctx.io.stdout.write(json(r));
975
+ return 0;
976
+ }
977
+ const lines = values.all === true
978
+ ? [`Every login serves requests, routed as your CLIProxyAPI config says: ${r.active.map((l) => `${l.provider} ${loginName(l)}`).join(', ') || 'none yet'}.`]
979
+ : r.switched.map((s) => (s.from?.file === s.to.file
980
+ ? `${loginName(s.to)} already serves your ${s.provider} requests.`
981
+ : `${loginName(s.to)} serves your ${s.provider} requests now. Claude Code, Codex and T3 Code use it from their next request.`));
982
+ for (const provider of new Set(r.switched.map((s) => s.provider))) {
983
+ const others = r.disabled.filter((l) => l.provider === provider);
984
+ if (others.length)
985
+ lines.push(`Kept in the hub, disabled: ${others.map(loginName).join(', ')}. Use them all again: shard cli-proxy use --all`);
986
+ }
987
+ ctx.io.stdout.write(`${lines.join('\n')}\n`);
988
+ return 0;
989
+ }
843
990
  // ---- handlers ------------------------------------------------------------------------------------------------------
844
991
  function statusLines(state, accounts, forwarding, launchd, machineVersion) {
845
992
  const hub = state === null ? 'missing (run shard cli-proxy import)' : state === 'running' ? 'running' : ['suspended', 'parked', 'stopped'].includes(state) ? 'sleeping (wakes on the next request)' : state;
@@ -948,6 +1095,7 @@ async function importHandler(ctx, values) {
948
1095
  ...(r.config === 'kept' ? ['The hub keeps its settings (client keys, routing, upstream keys).'] : []),
949
1096
  ...r.logins.moved_aside.map((dir) => `This computer’s auth files are in ${dir} now, where no CLIProxyAPI reads them.`),
950
1097
  ...(r.port !== CLI_PROXY_PORT ? [`Point tools that used http://127.0.0.1:${r.port} at http://127.0.0.1:${CLI_PROXY_PORT}.`] : []),
1098
+ ...(r.logins.disabled.length ? [`Kept in the hub, disabled: ${r.logins.disabled.map((l) => `${l.provider} ${loginName(l)}`).join(', ')}. Use them too: shard cli-proxy use --all`] : []),
951
1099
  ...(r.accounts ? [`Accounts: ${r.accounts.length}`, ...r.accounts.map(accountLine)] : []),
952
1100
  r.forwarder.launchd ? `This computer: http://127.0.0.1:${CLI_PROXY_PORT} forwards to it (starts at login).` : `This computer: run shard cli-proxy forward to reach it at http://127.0.0.1:${CLI_PROXY_PORT}.`,
953
1101
  r.machine && 'version' in r.machine ? `VMs: every new VM reaches it at http://127.0.0.1:${CLI_PROXY_PORT} (machine version ${r.machine.version}).` : '',
@@ -1012,5 +1160,6 @@ export const CLI_PROXY_HANDLERS = {
1012
1160
  'cli-proxy': status,
1013
1161
  'cli-proxy import': importHandler,
1014
1162
  'cli-proxy export': exportHandler,
1163
+ 'cli-proxy use': useHandler,
1015
1164
  'cli-proxy forward': forwardHandler,
1016
1165
  };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `shard settings` (0.20.1+): this computer's settings for its cloud conversations, kept in agents.json next to
3
+ * open_in and terminal_app. It sets the title marker (title_prefix), and shows where conversations open and the
4
+ * terminal app (set with `shard agents targets --open-in/--terminal`). The menu bar and the Linux app read and write
5
+ * the marker through it.
6
+ *
7
+ * The marker is what a conversation's Claude session title starts with in claude.ai and the Claude app
8
+ * ("☁ Fix login bug · todo-app"). Stored: absent for the platform's default, "" for none, else the text; the guest
9
+ * adds the space after it. New conversations send it as `title_prefix`, legacy agent launches as SF_TITLE_PREFIX.
10
+ */
11
+ import type { Handler } from './commands.js';
12
+ import type { OpenIn } from './t3-mode.js';
13
+ /** The platform's marker (the guest's "☁ " without its space). */
14
+ export declare const DEFAULT_TITLE_PREFIX = "\u2601";
15
+ /** At most this many characters on one line, as the API takes it (POST /v1/conversations title_prefix). */
16
+ export declare const TITLE_PREFIX_MAX = 16;
17
+ /** `--title-prefix <text|none|default>`: null for the default (the stored choice is removed), "" for none. */
18
+ export declare function parseTitlePrefix(spec: string): string | null;
19
+ /** The marker this computer chose: undefined for the default (nothing is sent), "" for none. An unreadable or invalid
20
+ * stored value counts as the default, so it never blocks a start. */
21
+ export declare function storedTitlePrefix(dir: string): string | undefined;
22
+ export declare function saveTitlePrefix(dir: string, choice: string | null): void;
23
+ /** `shard settings --json`. title_prefix: null is the default, "" none. open_in and terminal_app are the saved
24
+ * choices (null: automatic). */
25
+ export declare function clientSettings(dir: string): {
26
+ title_prefix: string | null;
27
+ title_prefix_default: string;
28
+ open_in: {
29
+ claude: OpenIn | null;
30
+ codex: OpenIn | null;
31
+ };
32
+ terminal_app: {
33
+ name: string;
34
+ id: import("./terminal-apps.js").TerminalId;
35
+ mode: import("./terminal-apps.js").TerminalMode;
36
+ } | null;
37
+ };
38
+ export declare function settingsText(s: ReturnType<typeof clientSettings>): string;
39
+ export declare const SETTINGS_HANDLERS: Record<string, Handler>;
@@ -0,0 +1,91 @@
1
+ import { out } from "./commands.js";
2
+ import { AgentStateStore } from "./agent-state.js";
3
+ import { UsageError } from "./errors.js";
4
+ import { objectValue } from "./acp-rpc.js";
5
+ import { openInTargets } from "./t3-mode.js";
6
+ import { storedTerminal, terminalName } from "./terminal-apps.js";
7
+ /** The platform's marker (the guest's "☁ " without its space). */
8
+ export const DEFAULT_TITLE_PREFIX = '☁';
9
+ /** At most this many characters on one line, as the API takes it (POST /v1/conversations title_prefix). */
10
+ export const TITLE_PREFIX_MAX = 16;
11
+ const SAMPLE = 'Fix login bug · todo-app';
12
+ // The API's pattern: no C0 control characters and no DEL.
13
+ const control = (value) => [...value].some(c => { const n = c.codePointAt(0); return n < 0x20 || n === 0x7f; });
14
+ const validPrefix = (value) => !control(value) && [...value].length <= TITLE_PREFIX_MAX;
15
+ /** `--title-prefix <text|none|default>`: null for the default (the stored choice is removed), "" for none. */
16
+ export function parseTitlePrefix(spec) {
17
+ const text = spec.trim();
18
+ if (text === 'default')
19
+ return null;
20
+ if (text === 'none' || text === '')
21
+ return '';
22
+ if (control(text))
23
+ throw new UsageError('--title-prefix is one line of text: no tabs, line breaks or control characters.');
24
+ if ([...text].length > TITLE_PREFIX_MAX)
25
+ throw new UsageError(`--title-prefix is at most ${TITLE_PREFIX_MAX} characters (none for no marker, default for ${DEFAULT_TITLE_PREFIX}).`);
26
+ return text;
27
+ }
28
+ /** The marker this computer chose: undefined for the default (nothing is sent), "" for none. An unreadable or invalid
29
+ * stored value counts as the default, so it never blocks a start. */
30
+ export function storedTitlePrefix(dir) {
31
+ try {
32
+ const value = new AgentStateStore(dir).read().title_prefix;
33
+ return typeof value === 'string' && validPrefix(value) ? value : undefined;
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ export function saveTitlePrefix(dir, choice) {
40
+ new AgentStateStore(dir).update(doc => { if (choice === null)
41
+ delete doc.title_prefix;
42
+ else
43
+ doc.title_prefix = choice; });
44
+ }
45
+ /** `shard settings --json`. title_prefix: null is the default, "" none. open_in and terminal_app are the saved
46
+ * choices (null: automatic). */
47
+ export function clientSettings(dir) {
48
+ const doc = new AgentStateStore(dir).read();
49
+ const saved = objectValue(doc.open_in);
50
+ const openIn = (app) => {
51
+ const value = saved[app];
52
+ return typeof value === 'string' && openInTargets(app).includes(value) ? value : null;
53
+ };
54
+ const terminal = storedTerminal(dir);
55
+ const prefix = typeof doc.title_prefix === 'string' && validPrefix(doc.title_prefix) ? doc.title_prefix : null;
56
+ return {
57
+ title_prefix: prefix,
58
+ title_prefix_default: DEFAULT_TITLE_PREFIX,
59
+ open_in: { claude: openIn('claude'), codex: openIn('codex') },
60
+ terminal_app: terminal ? { ...terminal, name: terminalName(terminal.id) } : null,
61
+ };
62
+ }
63
+ const sampleTitle = (prefix) => `${prefix === null ? DEFAULT_TITLE_PREFIX + ' ' : prefix ? prefix + ' ' : ''}${SAMPLE}`;
64
+ const markerLine = (prefix) => prefix === null ? `${DEFAULT_TITLE_PREFIX} (default)` : prefix === '' ? 'none' : prefix;
65
+ export function settingsText(s) {
66
+ const target = (t) => t ?? 'automatic';
67
+ return [
68
+ `Title marker ${markerLine(s.title_prefix)} "${sampleTitle(s.title_prefix)}"`,
69
+ `Open in Claude: ${target(s.open_in.claude)} · Codex: ${target(s.open_in.codex)}`,
70
+ `Terminal app ${s.terminal_app ? `${s.terminal_app.name}, ${s.terminal_app.mode === 'tab' ? 'new tab' : 'new window'}` : 'automatic'}`,
71
+ '',
72
+ 'Change the marker with shard settings --title-prefix <text|none|default>; where conversations open with shard agents targets --open-in and --terminal.',
73
+ ].join('\n') + '\n';
74
+ }
75
+ function settings(ctx, values) {
76
+ const spec = values['title-prefix'];
77
+ if (spec !== undefined && typeof spec !== 'string')
78
+ throw new UsageError('--title-prefix takes text, none or default.');
79
+ if (typeof spec === 'string') {
80
+ const choice = parseTitlePrefix(spec);
81
+ saveTitlePrefix(ctx.store.dir, choice);
82
+ const s = clientSettings(ctx.store.dir);
83
+ out(ctx, `${s.title_prefix === '' ? 'New conversations have no title marker' : `New conversations’ titles start with ${markerLine(s.title_prefix)}`}: "${sampleTitle(s.title_prefix)}".\n`, s);
84
+ return 0;
85
+ }
86
+ const s = clientSettings(ctx.store.dir);
87
+ out(ctx, settingsText(s), s);
88
+ return 0;
89
+ }
90
+ // A refused --title-prefix rejects the command's promise (a usage error, exit 2) rather than throwing.
91
+ export const SETTINGS_HANDLERS = { settings: (ctx, values) => Promise.resolve().then(() => settings(ctx, values)) };
@@ -3,12 +3,18 @@ import type { Ctx, Handler } from './commands.js';
3
3
  import type { Values } from './args.js';
4
4
  import type { ModelChoice } from './agent-models.js';
5
5
  import type { Conversation } from './conversations.js';
6
+ import type { LocalConversation } from './conversations.js';
6
7
  import type { InitialImage } from './initial-prompt.js';
7
- /** Only provider-returned task ids enter the registered Codex deep-link route. */
8
- export declare function openCodexThread(task: CodexCloudTask, env: Ctx['io']['env']): Promise<void>;
8
+ /** Cloud threads open in the browser, preserving the durable provider destination on every OS. */
9
+ export declare function openCodexThread(task: CodexCloudTask, env: Ctx['io']['env'], platform?: NodeJS.Platform): Promise<void>;
9
10
  export declare function submitCloudInitialPrompt(ctx: Ctx, c: Conversation, prompt: string, workspaceKey: string): Promise<void>;
10
- /** Prints the Cloud thread (and its link) and opens it in the Codex app; --json prints it instead. */
11
+ /** Prints the Cloud thread (and its link) and opens it in your browser; --json prints it instead. */
11
12
  export declare function printCloudThread(ctx: Ctx, c: Conversation, task: CodexCloudTask, submitted?: boolean, choice?: ModelChoice): Promise<void>;
13
+ /**
14
+ * A Cloud conversation where the Codex app cannot show it (0.23.0+): where its chat lives, whether it waits for an
15
+ * answer, and how to answer and follow it from here. Never a new start: the conversation exists.
16
+ */
17
+ export declare function cloudChatElsewhere(c: Pick<Conversation, 'id' | 'key' | 'state'>, url: string): string;
12
18
  /** How long the account's Codex remote device may take to come up before it counts as stuck. */
13
19
  export declare const CODEX_STARTING_LIMIT_MS: number;
14
20
  export type CodexConnectorView = {
@@ -48,5 +54,9 @@ export declare function ensureCodexCloud(ctx: Ctx): Promise<{
48
54
  last_seen_at: string | null;
49
55
  }>;
50
56
  export declare function launchCodexCloud(ctx: Ctx, values: Values, path: string, images?: readonly InitialImage[]): Promise<number>;
51
- export declare function openCodexConversation(ctx: Ctx, c: Conversation): Promise<number>;
57
+ /** A start whose carrying process said so in the last 45 s. */
58
+ export declare const startCarried: (start: NonNullable<LocalConversation["cloud_start"]>) => boolean;
59
+ export declare function openCodexConversation(ctx: Ctx, c: Conversation, history?: true | string): Promise<number>;
60
+ /** Continues the same accepted identity, never creates a replacement VM/chat. */
61
+ export declare function resumeCloudStartup(ctx: Ctx, c: Conversation): Promise<CodexCloudTask>;
52
62
  export declare const CODEX_CLOUD_HANDLERS: Record<string, Handler>;