@shardflux/cli 0.20.0 → 0.22.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 (110) hide show
  1. package/CHANGELOG.md +214 -1
  2. package/README.md +215 -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-login.d.ts +4 -3
  10. package/dist/acp-login.js +7 -5
  11. package/dist/acp-models.js +20 -15
  12. package/dist/acp.js +66 -13
  13. package/dist/agent-commands.d.ts +4 -1
  14. package/dist/agent-commands.js +86 -32
  15. package/dist/agent-config-return.d.ts +46 -2
  16. package/dist/agent-config-return.js +110 -4
  17. package/dist/agent-credentials.js +7 -3
  18. package/dist/agent-repository.d.ts +16 -3
  19. package/dist/agent-repository.js +149 -74
  20. package/dist/agent-setup.d.ts +14 -3
  21. package/dist/agent-setup.js +78 -36
  22. package/dist/agent-target-commands.js +24 -13
  23. package/dist/args.d.ts +1 -1
  24. package/dist/args.js +94 -54
  25. package/dist/bin.js +5 -1
  26. package/dist/browser.d.ts +13 -2
  27. package/dist/browser.js +30 -14
  28. package/dist/claude-cloud-login.d.ts +24 -2
  29. package/dist/claude-cloud-login.js +151 -11
  30. package/dist/cli-proxy-forward.d.ts +2 -0
  31. package/dist/cli-proxy-forward.js +9 -2
  32. package/dist/cli-proxy-hub.d.ts +69 -25
  33. package/dist/cli-proxy-hub.js +231 -54
  34. package/dist/cli-proxy.d.ts +70 -1
  35. package/dist/cli-proxy.js +298 -32
  36. package/dist/client-settings.d.ts +39 -0
  37. package/dist/client-settings.js +91 -0
  38. package/dist/codex-cloud.d.ts +5 -0
  39. package/dist/codex-cloud.js +88 -17
  40. package/dist/command-center-actions.js +5 -1
  41. package/dist/commands.js +171 -22
  42. package/dist/conversation-commands.d.ts +5 -0
  43. package/dist/conversation-commands.js +8 -5
  44. package/dist/conversation-terminal.d.ts +10 -2
  45. package/dist/conversation-terminal.js +77 -13
  46. package/dist/conversations.d.ts +73 -1
  47. package/dist/conversations.js +257 -23
  48. package/dist/device-bridge.d.ts +1 -1
  49. package/dist/device-bridge.js +6 -4
  50. package/dist/device-helper.js +43 -22
  51. package/dist/errors.d.ts +27 -1
  52. package/dist/errors.js +45 -5
  53. package/dist/filesystem-commands.d.ts +16 -9
  54. package/dist/filesystem-commands.js +108 -54
  55. package/dist/format.d.ts +2 -1
  56. package/dist/format.js +10 -1
  57. package/dist/handlers.js +3 -1
  58. package/dist/initial-prompt.d.ts +8 -0
  59. package/dist/initial-prompt.js +43 -1
  60. package/dist/large-media.d.ts +14 -0
  61. package/dist/large-media.js +100 -0
  62. package/dist/linux-terminal.d.ts +5 -3
  63. package/dist/linux-terminal.js +6 -20
  64. package/dist/local-sessions.d.ts +26 -2
  65. package/dist/local-sessions.js +113 -24
  66. package/dist/machine-setup.d.ts +11 -1
  67. package/dist/machine-setup.js +69 -22
  68. package/dist/machine-status.d.ts +67 -0
  69. package/dist/machine-status.js +140 -0
  70. package/dist/main.js +11 -13
  71. package/dist/move-worktrees-restore.py +144 -0
  72. package/dist/move-worktrees.d.ts +63 -0
  73. package/dist/move-worktrees.js +201 -0
  74. package/dist/move.d.ts +10 -1
  75. package/dist/move.js +116 -16
  76. package/dist/onboarding.d.ts +11 -7
  77. package/dist/onboarding.js +59 -27
  78. package/dist/prepared-repository.d.ts +202 -0
  79. package/dist/prepared-repository.js +860 -0
  80. package/dist/prepared-repository.py +1306 -0
  81. package/dist/project-preparation.d.ts +48 -0
  82. package/dist/project-preparation.js +304 -0
  83. package/dist/project-sync-manifest.d.ts +4 -0
  84. package/dist/project-sync-manifest.js +5 -1
  85. package/dist/project-sync-transfer.js +1 -1
  86. package/dist/project-sync.d.ts +19 -4
  87. package/dist/project-sync.js +31 -8
  88. package/dist/project-template-setup.d.ts +33 -1
  89. package/dist/project-template-setup.js +59 -2
  90. package/dist/secret-ask.d.ts +72 -0
  91. package/dist/secret-ask.js +351 -0
  92. package/dist/seed-phases.d.ts +8 -1
  93. package/dist/seed-phases.js +17 -2
  94. package/dist/setup-cache.d.ts +7 -0
  95. package/dist/setup-cache.js +32 -0
  96. package/dist/t3-launch.js +1 -4
  97. package/dist/t3-mode.d.ts +8 -2
  98. package/dist/t3-mode.js +19 -4
  99. package/dist/t3-phone.d.ts +4 -2
  100. package/dist/t3-phone.js +12 -4
  101. package/dist/terminal-apps.d.ts +157 -0
  102. package/dist/terminal-apps.js +561 -0
  103. package/dist/terminal-onboarding.js +6 -1
  104. package/dist/this-computer.d.ts +11 -0
  105. package/dist/this-computer.js +36 -0
  106. package/dist/thread-on-computer.js +4 -1
  107. package/dist/update.js +11 -3
  108. package/dist/worktree-twin.d.ts +10 -2
  109. package/dist/worktree-twin.js +27 -9
  110. package/package.json +5 -5
@@ -21,6 +21,7 @@ import { CLI_VERSION, UUID, parseDuration, parsePositiveInt } from "./args.js";
21
21
  import { openUrl } from "./browser.js";
22
22
  import { defaultClientName, startDeviceAuthorization, waitForDeviceSession } from "./browser-login.js";
23
23
  import { setupGuide, setupGuideText } from "./agent-setup.js";
24
+ import { allOrgs, allProjects, byRef, choices, findOrg, gone, keyOrganizationWarning, keyScope, resolveOrg, resolveProject, staleDefaultOrg } from "./account-scope.js";
24
25
  import { readFileSync, rmSync } from 'node:fs';
25
26
  import { join } from 'node:path';
26
27
  import { legalFields, requireTermsAcceptance, showLegalNotice } from "./legal.js";
@@ -135,100 +136,6 @@ async function withStepUp(ctx, account, call) {
135
136
  return call();
136
137
  }
137
138
  }
138
- // ---- organization / project resolution ------------------------------------------------------------------------
139
- async function allOrgs(account) {
140
- const rows = [];
141
- for await (const o of account.organizations.listAll())
142
- rows.push(o);
143
- return rows;
144
- }
145
- async function allProjects(account, organizationId) {
146
- const rows = [];
147
- for await (const p of account.projects.listAll(organizationId))
148
- rows.push(p);
149
- return rows;
150
- }
151
- const choices = (rows) => rows.map((r) => ` ${r.id} ${r.slug} ${r.name}`).join('\n');
152
- function byRef(rows, ref) {
153
- const exact = rows.find((r) => r.id === ref || r.slug === ref);
154
- if (exact)
155
- return exact;
156
- const named = rows.filter((r) => r.name.toLowerCase() === ref.toLowerCase());
157
- return named.length === 1 ? named[0] : undefined;
158
- }
159
- const gone = (err) => err instanceof ShardfluxApiError && (err.status === 404 || err.status === 403);
160
- async function findOrg(account, ref) {
161
- if (UUID.test(ref))
162
- return account.organizations.get(ref);
163
- const hit = byRef(await allOrgs(account), ref);
164
- if (!hit)
165
- throw new NotFoundError(`No organization "${ref}" among yours.`, 'list them: shard orgs list');
166
- return hit;
167
- }
168
- /** `ref` (a positional), else --org, else the saved default, else the only organization. */
169
- async function resolveOrg(ctx, account, values, ref) {
170
- const given = ref ?? str(values.org);
171
- if (given !== undefined)
172
- return findOrg(account, given);
173
- const saved = ctx.store.profile(ctx.apiUrl).organization_id;
174
- if (saved) {
175
- try {
176
- return await account.organizations.get(saved);
177
- }
178
- catch (err) {
179
- if (gone(err))
180
- throw new NotFoundError(`The default organization ${saved} is no longer available to you.`, 'pick another: shard orgs use <org> (list: shard orgs list)');
181
- throw err;
182
- }
183
- }
184
- const orgs = await allOrgs(account);
185
- if (orgs.length === 1)
186
- return orgs[0];
187
- if (orgs.length === 0)
188
- throw new UsageError('You have no organization yet.', 'create one with a project and an API key: shard setup (or shard orgs create <name>)');
189
- throw new UsageError(`You belong to ${orgs.length} organizations; pick one with --org <id|slug>:\n${choices(orgs)}\n`, 'or save a default: shard orgs use <id|slug>');
190
- }
191
- /** `ref` (a positional), else --project, else the saved default (unless --org names another), else the only project. */
192
- async function resolveProject(ctx, account, values, ref) {
193
- const given = ref ?? str(values.project);
194
- const orgFlag = str(values.org);
195
- if (given !== undefined && UUID.test(given)) {
196
- const p = await account.projects.get(given);
197
- if (orgFlag !== undefined) {
198
- const org = await findOrg(account, orgFlag);
199
- if (p.organization_id !== org.id)
200
- throw new UsageError(`project ${given} is not in organization ${org.slug}`);
201
- }
202
- return p;
203
- }
204
- const profile = ctx.store.profile(ctx.apiUrl);
205
- if (given === undefined && profile.project_id && orgFlag === undefined) {
206
- try {
207
- return await account.projects.get(profile.project_id);
208
- }
209
- catch (err) {
210
- if (gone(err))
211
- throw new NotFoundError(`The default project ${profile.project_id} is no longer available to you.`, 'pick another: shard projects use <project> (list: shard projects list)');
212
- throw err;
213
- }
214
- }
215
- const org = await resolveOrg(ctx, account, values);
216
- const projects = await allProjects(account, org.id);
217
- if (given !== undefined) {
218
- const hit = byRef(projects, given);
219
- if (!hit)
220
- throw new NotFoundError(`No project "${given}" in ${org.name} (${org.slug}).`, 'list them: shard projects list');
221
- return hit;
222
- }
223
- const saved = profile.project_id ? projects.find((p) => p.id === profile.project_id) : undefined;
224
- if (saved)
225
- return saved;
226
- if (projects.length === 1)
227
- return projects[0];
228
- if (projects.length === 0)
229
- throw new UsageError(`${org.name} (${org.slug}) has no project yet.`, 'create one: shard projects create <name> (or shard setup)');
230
- throw new UsageError(`${org.name} has ${projects.length} projects; pick one with --project <id|slug>:\n${choices(projects)}\n`, 'or save a default: shard projects use <id|slug>');
231
- }
232
139
  /** Removes a saved default (and the saved key) that pointed into an organization that is gone. */
233
140
  function forgetOrganization(ctx, organizationId, projectIds) {
234
141
  ctx.store.update(ctx.apiUrl, (p) => {
@@ -593,30 +500,40 @@ async function runSetup(ctx, values, account) {
593
500
  if (!gone(err))
594
501
  throw err;
595
502
  if (!ctx.json)
596
- ctx.io.stderr.write(`note: the saved API key ${profile.api_key.key_id} belongs to a project you cannot see; creating a key for your own.\n`);
503
+ ctx.io.stderr.write(`note: the saved API key ${profile.api_key.key_id} belongs to organization ${keyCheck.organizationId}, which you are no longer in; making a key for one of yours.\n`);
597
504
  keyCheck = { ok: false, reason: 'another account' };
598
505
  }
599
506
  }
507
+ // Several organizations: which one this setup picked, and how that choice is kept and changed (0.20.2+).
508
+ let orgCount = 1;
600
509
  if (home) {
601
510
  org = home.org;
602
511
  project = home.project;
603
512
  }
604
513
  else {
605
514
  const orgs = await allOrgs(account);
515
+ orgCount = orgs.length;
606
516
  const orgRef = str(values.org);
607
517
  const savedOrg = profile.organization_id ? orgs.find((o) => o.id === profile.organization_id) : undefined;
608
518
  if (orgRef !== undefined)
609
519
  org = await findOrg(account, orgRef);
610
520
  else if (savedOrg)
611
521
  org = savedOrg;
612
- else if (orgs.length === 1)
522
+ else if (profile.organization_id && orgs.length > 1)
523
+ throw staleDefaultOrg(profile.organization_id, orgs);
524
+ else if (orgs.length === 1) {
613
525
  org = orgs[0];
526
+ if (profile.organization_id && profile.organization_id !== org.id && !ctx.json)
527
+ ctx.io.stderr.write(`note: your saved default organization ${profile.organization_id} is no longer one of yours; using ${org.name} (${org.slug}).\n`);
528
+ }
614
529
  else if (orgs.length === 0) {
615
530
  org = await account.organizations.create({ name: str(values['org-name'])?.trim() || defaultOrgName(me.user) });
616
531
  created.organization = true;
617
532
  }
618
- else
619
- throw new UsageError(`You belong to ${orgs.length} organizations; pick one with --org <id|slug>:\n${choices(orgs)}\n`, 'or save a default first: shard orgs use <id|slug>');
533
+ else {
534
+ throw new UsageError(`You belong to ${orgs.length} organizations; pick one with --org <id|slug>:\n${choices(orgs)}\n` +
535
+ `The one you pick becomes this computer's default, with an API key for it (saved in ${ctx.store.file}); switch later: shard setup --org <id|slug> --new-key\n`, 'or save a default first: shard orgs use <id|slug>');
536
+ }
620
537
  const projects = created.organization ? [] : await allProjects(account, org.id);
621
538
  const projectRef = str(values.project);
622
539
  const savedProject = profile.project_id ? projects.find((p) => p.id === profile.project_id) : undefined;
@@ -680,6 +597,7 @@ async function runSetup(ctx, values, account) {
680
597
  codex = { ...await cloud.codex.waitConnector({ timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal }) };
681
598
  }
682
599
  const envNote = ctx.io.env.SHARDFLUX_API_KEY?.trim() ? 'note: SHARDFLUX_API_KEY is set in this environment and wins over the saved key; unset it to use the saved key.\n' : '';
600
+ const warnings = envNote ? [{ code: 'api_key_environment_override', message: 'SHARDFLUX_API_KEY is set in this environment and wins over the saved key.', fix: 'unset SHARDFLUX_API_KEY' }] : [];
683
601
  return {
684
602
  text: (keep ? 'Already set up; the saved API key works.\n' : 'Set up.\n') +
685
603
  kv([
@@ -688,6 +606,7 @@ async function runSetup(ctx, values, account) {
688
606
  ['api key', `"${key.name}" ${key.key_id} (tools: ${toolsText(permissions)}) ${keep ? 'saved earlier' : 'created'}, saved in ${ctx.store.file}`],
689
607
  ]) +
690
608
  envNote +
609
+ (orgCount > 1 ? `${org.name} is your default organization on this computer (saved in ${ctx.store.file}, with the key); switch: shard setup --org <id|slug> --new-key\n` : '') +
691
610
  (codex ? 'Codex Cloud is ready. One account executor; no pairing. Its residency is billed as workspace usage.\n' : '') +
692
611
  nextLines(),
693
612
  data: {
@@ -699,6 +618,7 @@ async function runSetup(ctx, values, account) {
699
618
  credentials_file: ctx.store.file,
700
619
  next: NEXT_WORKSPACE,
701
620
  next_steps: NEXT_STEPS,
621
+ ...(warnings.length ? { warnings } : {}),
702
622
  },
703
623
  };
704
624
  }
@@ -767,7 +687,7 @@ async function browserSignIn(ctx, values, noticeShown = false) {
767
687
  else if (!noticeShown)
768
688
  showLegalNotice(ctx);
769
689
  // A resumed request was opened (or printed) by the command that started it.
770
- const opened = !resumed && values.open !== false && openUrl(d.verification_uri_complete, ctx.io.env);
690
+ const opened = !resumed && values.open !== false && (await openUrl(d.verification_uri_complete, ctx.io.env));
771
691
  const minutes = Math.max(1, Math.round(d.expires_in / 60));
772
692
  if (!ctx.json) {
773
693
  ctx.io.stderr.write(`${resumed ? 'Continuing the sign-in started earlier' : opened ? 'Opening your browser to sign in' : 'Open this page to sign in'}: ${d.verification_uri_complete}\n` +
@@ -852,6 +772,32 @@ export function agentSetupGuide(ctx) {
852
772
  const profile = ctx.store.profile(ctx.apiUrl);
853
773
  return setupGuide({ signedIn: ctx.sessionSource() !== null, email: profile.session?.user?.email ?? null, apiKey: profile.api_key !== undefined || Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) });
854
774
  }
775
+ /**
776
+ * After `orgs use` / `projects use` (0.20.2+): the defaults moved, but workspace, agent and usage commands use the API
777
+ * key, which may act in another organization. Says so; on a terminal offers to make a key for the new default (a
778
+ * `shard setup --new-key` for it), otherwise names that command. Nothing when the key is already there (or none).
779
+ */
780
+ async function keyElsewhereOffer(ctx, account, orgId, loadOrg, setupValues) {
781
+ const key = await keyScope(ctx);
782
+ if (key === null || key.organizationId === orgId)
783
+ return { text: '', data: {} };
784
+ const org = await loadOrg();
785
+ const w = keyOrganizationWarning(key, await allOrgs(account), org, str(setupValues.project));
786
+ const { fix, message: warning } = w;
787
+ if (key.source === 'file' && ctx.input.interactive && !ctx.json) {
788
+ ctx.io.stderr.write(`${warning}\n`);
789
+ const answer = (await ctx.input.answer(`Make an API key for ${org.name} and save it in place of ${key.keyId}? [y/N] `))?.trim() ?? '';
790
+ if (/^y(es)?$/i.test(answer)) {
791
+ const r = await runSetup(ctx, { ...setupValues, 'new-key': true, codex: false }, account);
792
+ return { text: r.text, data: { setup: r.data } };
793
+ }
794
+ }
795
+ else if (!ctx.json)
796
+ ctx.io.stderr.write(`warning: ${warning}\n`);
797
+ if (!ctx.json)
798
+ ctx.io.stderr.write(`fix: ${fix}\n`);
799
+ return { text: '', data: { api_key: { key_id: key.keyId, source: key.source, organization_id: key.organizationId, project_id: key.projectId }, warnings: [w] } };
800
+ }
855
801
  // ---- handlers --------------------------------------------------------------------------------------------------
856
802
  export const ACCOUNT_HANDLERS = {
857
803
  async 'auth register'(ctx, values) {
@@ -1358,13 +1304,15 @@ export const ACCOUNT_HANDLERS = {
1358
1304
  return 0;
1359
1305
  },
1360
1306
  async 'orgs use'(ctx, values, [ref]) {
1361
- const org = await resolveOrg(ctx, ctx.account(), values, ref);
1307
+ const account = ctx.account();
1308
+ const org = await resolveOrg(ctx, account, values, ref);
1362
1309
  ctx.store.update(ctx.apiUrl, (p) => {
1363
1310
  if (p.organization_id !== org.id)
1364
1311
  delete p.project_id;
1365
1312
  p.organization_id = org.id;
1366
1313
  });
1367
- out(ctx, `Default organization: ${org.name} (${org.slug}, ${org.id}).\n`, { default_organization_id: org.id, organization: org });
1314
+ const key = await keyElsewhereOffer(ctx, account, org.id, () => Promise.resolve(org), { org: org.id });
1315
+ out(ctx, `Default organization: ${org.name} (${org.slug}, ${org.id}).\n${key.text}`, { default_organization_id: org.id, organization: org, ...key.data });
1368
1316
  return 0;
1369
1317
  },
1370
1318
  async 'orgs export'(ctx, values, [ref]) {
@@ -1479,12 +1427,14 @@ export const ACCOUNT_HANDLERS = {
1479
1427
  return 0;
1480
1428
  },
1481
1429
  async 'projects use'(ctx, values, [ref]) {
1482
- const p = await resolveProject(ctx, ctx.account(), values, ref);
1430
+ const account = ctx.account();
1431
+ const p = await resolveProject(ctx, account, values, ref);
1483
1432
  ctx.store.update(ctx.apiUrl, (x) => {
1484
1433
  x.organization_id = p.organization_id;
1485
1434
  x.project_id = p.id;
1486
1435
  });
1487
- out(ctx, `Default project: ${p.name} (${p.slug}, ${p.id}) of organization ${p.organization_id}.\n`, { default_project_id: p.id, default_organization_id: p.organization_id, project: p });
1436
+ const key = await keyElsewhereOffer(ctx, account, p.organization_id, () => account.organizations.get(p.organization_id), { project: p.id });
1437
+ out(ctx, `Default project: ${p.name} (${p.slug}, ${p.id}) of organization ${p.organization_id}.\n${key.text}`, { default_project_id: p.id, default_organization_id: p.organization_id, project: p, ...key.data });
1488
1438
  return 0;
1489
1439
  },
1490
1440
  async 'api-keys list'(ctx, values) {
@@ -1673,13 +1623,13 @@ export const ACCOUNT_HANDLERS = {
1673
1623
  if (!(err instanceof ShardfluxApiError && err.code === 'conflict' && err.details?.reason === 'subscription_exists'))
1674
1624
  throw err;
1675
1625
  const portal = await account.billing.portal(org.id);
1676
- const opened = values.open === true && openUrl(portal.url, ctx.io.env);
1626
+ const opened = values.open === true && (await openUrl(portal.url, ctx.io.env));
1677
1627
  out(ctx, `${org.name} already has a subscription: plan changes happen in the Stripe billing portal (it shows the new price before you confirm):\n${portal.url}\n${values.open === true ? (opened ? 'Opened it in your browser.\n' : 'Could not open a browser; open the URL yourself.\n') : ''}`, { subscription_exists: true, organization_id: org.id, plan_key: plan.plan_key, portal_url: portal.url, opened });
1678
1628
  return 0;
1679
1629
  }
1680
1630
  if (!checkout.url && !checkout.subscription_active)
1681
1631
  throw new UsageError(`checkout ${checkout.id} is ${checkout.status} and has no URL; start again: shard billing upgrade ${plan.plan_key}`);
1682
- const opened = values.open === true && checkout.url !== null && openUrl(checkout.url, ctx.io.env);
1632
+ const opened = values.open === true && checkout.url !== null && (await openUrl(checkout.url, ctx.io.env));
1683
1633
  if (!ctx.json) {
1684
1634
  ctx.io.stdout.write(`Checkout for ${plan.name} (${plan.plan_key}, ${priceText(catalog, plan.plan_key)}) for ${org.name}:\n${checkout.url ?? '-'}\n` +
1685
1635
  `A person pays there (Stripe Checkout${checkout.expires_at ? `; the link works until ${time(checkout.expires_at)}` : ''}). The plan is active once Stripe confirms the payment${values.wait === true ? '.' : ': shard billing status'}\n` +
@@ -1699,7 +1649,7 @@ export const ACCOUNT_HANDLERS = {
1699
1649
  const account = ctx.account();
1700
1650
  const org = await resolveOrg(ctx, account, values);
1701
1651
  const portal = await account.billing.portal(org.id);
1702
- const opened = values.open === true && openUrl(portal.url, ctx.io.env);
1652
+ const opened = values.open === true && (await openUrl(portal.url, ctx.io.env));
1703
1653
  out(ctx, `Billing portal of ${org.name} (plan changes, payment methods, invoices, cancellation; the link is short-lived):\n${portal.url}\n${values.open === true ? (opened ? 'Opened it in your browser.\n' : 'Could not open a browser; open the URL yourself.\n') : ''}`, { organization_id: org.id, url: portal.url, opened });
1704
1654
  return 0;
1705
1655
  },
@@ -1,16 +1,28 @@
1
1
  /**
2
- * `shard agents accounts` (0.18.0+); with --json every write prints the updated `{accounts, pool}` block (the menu bar
3
- * redraws from it), plus the account it changed.: the organization's Claude and ChatGPT/Codex accounts, with each one's usage
4
- * windows, reset times, status and threads (docs/progress/account-pool.md). Sign-ins run the provider's own login in a
5
- * workspace, as `shard agents credentials login claude` does; API keys come from standard input and are write-only.
2
+ * `shard agents accounts` (0.18.0+): the organization's Claude and ChatGPT/Codex accounts, with each one's usage
3
+ * windows, reset times and status, and which one is active. Each provider has one active account: new conversations
4
+ * start with its login and running conversation VMs get it. The person chooses it (`shard agents accounts use`);
5
+ * nothing switches on usage (switching accounts automatically because usage runs out is against the providers' terms).
6
+ * With --json every write prints the updated `{accounts, pool}` block (the menu bar redraws from it), plus the account
7
+ * it changed. Sign-ins run the provider's own login in a workspace, as `shard agents credentials login claude` does.
6
8
  */
7
- import type { AgentAccount, AgentAccountList } from '@shardflux/sdk';
9
+ import type { AgentAccount, AgentAccountList, AgentAccountWindow } from '@shardflux/sdk';
8
10
  import type { Ctx, Handler } from './commands.js';
9
- import { CodedError } from './errors.js';
10
- import type { HubCloud } from './cli-proxy-hub.js';
11
+ /**
12
+ * Codex names its windows primary and secondary; their length says which is the 5-hour and which the weekly one (a
13
+ * ChatGPT Pro Max login's primary window is its weekly one). Unknown length: the provider's name.
14
+ */
15
+ export declare const windowLabel: (w: AgentAccountWindow) => string;
11
16
  /** "in 2h 10m", "in 4d 8h", "in 41m"; "now" once passed. */
12
17
  export declare function resetIn(resetsAt: string | null, now: number): string | null;
13
18
  export declare const planName: (planType: string) => string;
19
+ /** The provider's active account; an API before 2026-10-12 sends only `preferred` (the same account). */
20
+ export declare const isActive: (a: AgentAccount) => boolean;
21
+ /**
22
+ * Whether the caller can make it the active account: the API says so (`can_activate`, 2026-10-12+); an API from before
23
+ * it, by the same rules as far as the list shows them (a working subscription sign-in, not active, team or yours).
24
+ */
25
+ export declare const canActivate: (a: AgentAccount) => boolean;
14
26
  /** Account references: the id, `<provider>:<n>` (its place in the list, from 1), the email or the label. */
15
27
  export declare function findAccount(list: AgentAccountList, ref: string): AgentAccount;
16
28
  /** The compact list: one row per account, grouped by provider. */
@@ -22,11 +34,3 @@ export declare function codexLoginUrls(output: string): string[];
22
34
  export declare const ACCOUNT_POOL_HANDLERS: Record<string, Handler>;
23
35
  /** The `accounts` block of `shard agents targets --json` (0.18.0+): the account list for the menu bar; null when it cannot be read. */
24
36
  export declare function targetsAccounts(ctx: Ctx): Promise<AgentAccountList | null>;
25
- /** The conversation's thread runs on the account pool (its `account.mode`, API with the pool). */
26
- export declare function conversationPooled(conversation: unknown): boolean;
27
- /** A pooled thread has no provider login on its session, so claude.ai, Claude Desktop and the Codex app cannot reach it. */
28
- export declare const pooledNeedsT3: (ref: string) => CodedError;
29
- /** Test seam: the hub cloud the pool toggle ensures (the SDK's by default). */
30
- export declare const accountPoolSeams: {
31
- hubCloud?: (ctx: Ctx) => HubCloud;
32
- };
@@ -1,17 +1,17 @@
1
- import { ShardfluxApiError } from '@shardflux/sdk';
2
- import { out, readInput, resolveWorkspace } from "./commands.js";
3
- import { CodedError, UsageError, describeError } from "./errors.js";
1
+ import { out, resolveWorkspace } from "./commands.js";
2
+ import { UsageError } from "./errors.js";
4
3
  import { claudeLoginUrls, requireClaudeTerminal, runWorkspaceLogin } from "./claude-cloud-login.js";
5
4
  import { openMachine } from "./filesystem-commands.js";
6
- import { t3Install } from "./t3-mode.js";
7
- import { ensureCliProxyHub, sdkHubCloud } from "./cli-proxy-hub.js";
8
- import { homeProject } from "./cli-proxy.js";
9
- import { progress } from "./progress.js";
10
5
  const PROVIDERS = { claude: 'Claude', codex: 'ChatGPT / Codex' };
11
6
  const WINDOW_LABELS = { five_hour: '5-hour', seven_day: 'Weekly', seven_day_opus: 'Weekly Opus', seven_day_sonnet: 'Weekly Sonnet', primary: 'Primary', secondary: 'Secondary' };
12
- /** Codex names its windows primary and secondary; their length says which is the 5-hour and which the weekly one. */
13
- const windowLabel = (w) => (w.window === 'primary' || w.window === 'secondary') && w.window_minutes === 300 ? '5-hour'
14
- : (w.window === 'primary' || w.window === 'secondary') && w.window_minutes === 10080 ? 'Weekly' : WINDOW_LABELS[w.window] ?? w.window;
7
+ /** A window's length as a title, as Claude's windows read: "Weekly", "5-hour", "2-day", "90-minute". */
8
+ const lengthLabel = (m) => m === 10080 ? 'Weekly' : m % 1440 === 0 ? `${m / 1440}-day` : m % 60 === 0 ? `${m / 60}-hour` : `${m}-minute`;
9
+ /**
10
+ * Codex names its windows primary and secondary; their length says which is the 5-hour and which the weekly one (a
11
+ * ChatGPT Pro Max login's primary window is its weekly one). Unknown length: the provider's name.
12
+ */
13
+ export const windowLabel = (w) => (w.window === 'primary' || w.window === 'secondary') && typeof w.window_minutes === 'number' && w.window_minutes > 0
14
+ ? lengthLabel(w.window_minutes) : WINDOW_LABELS[w.window] ?? w.window;
15
15
  const STATUS_LABELS = { signed_in: 'Signed in', low: 'Low', limited: 'Limited', expired: 'Expired' };
16
16
  const CLAUDE_VERSION = '2.1.289';
17
17
  function provider(value, what = 'provider') {
@@ -37,7 +37,14 @@ const pct = (w) => `${Math.round(w.used_percent)}%`;
37
37
  const PLAN_NAMES = { free: 'Free', go: 'Go', plus: 'Plus', pro: 'Pro', prolite: 'Pro Lite', promax: 'Pro Max', max: 'Max' };
38
38
  export const planName = (planType) => PLAN_NAMES[planType] ?? planType.charAt(0).toUpperCase() + planType.slice(1);
39
39
  const plan = (a) => a.kind === 'api_key' ? 'API key' : a.plan_type ? planName(a.plan_type) : '';
40
- const threads = (n) => `${n} ${n === 1 ? 'thread' : 'threads'}`;
40
+ const threads = (n) => `${n} ${n === 1 ? 'conversation' : 'conversations'}`;
41
+ /** The provider's active account; an API before 2026-10-12 sends only `preferred` (the same account). */
42
+ export const isActive = (a) => a.active ?? a.preferred;
43
+ /**
44
+ * Whether the caller can make it the active account: the API says so (`can_activate`, 2026-10-12+); an API from before
45
+ * it, by the same rules as far as the list shows them (a working subscription sign-in, not active, team or yours).
46
+ */
47
+ export const canActivate = (a) => a.can_activate ?? (a.kind === 'oauth' && !isActive(a) && a.status !== 'expired' && (a.scope === 'team' || a.mine));
41
48
  const name = (a) => a.email && a.label !== a.email ? `${a.label} (${a.email})` : a.label;
42
49
  function bar(used, width = 10) {
43
50
  const filled = Math.max(0, Math.min(width, Math.round(used / 100 * width)));
@@ -72,14 +79,22 @@ export function formatAccounts(list, now) {
72
79
  for (const a of rows) {
73
80
  const windows = a.kind === 'api_key' ? 'Pay as you go' : a.windows.filter(w => ['five_hour', 'seven_day', 'primary', 'secondary'].includes(w.window))
74
81
  .map(w => `${windowLabel(w)} ${pct(w)}${resetIn(w.resets_at, now) ? ` (resets ${resetIn(w.resets_at, now) === 'now' ? 'now' : `in ${resetIn(w.resets_at, now)}`})` : ''}`).join(' · ') || 'No usage read yet';
75
- const tags = [plan(a), a.preferred ? 'in use' : '', list.pool.pooled ? threads(a.threads) : ''].filter(Boolean).join(', ');
82
+ const tags = [plan(a), isActive(a) ? 'active' : ''].filter(Boolean).join(', ');
76
83
  lines.push(` ${`${p}:${a.position + 1}`.padEnd(9)} ${name(a)}${tags ? ` (${tags})` : ''} · ${STATUS_LABELS[a.status]}${a.status === 'expired' ? ': sign in again with shard agents accounts add ' + p : ''}`);
77
84
  lines.push(` ${''.padEnd(9)} ${windows}${asOf(a, now)}`);
78
85
  }
79
86
  }
80
- if (list.pool.pooled) {
81
- const open = list.accounts.reduce((n, a) => n + a.threads, 0), used = list.accounts.filter(a => a.threads > 0).length;
82
- lines.push(`Pooled: ${threads(open)} on ${used} ${used === 1 ? 'account' : 'accounts'}; open threads remotely in T3 Code.`);
87
+ // Only what the caller can do: the accounts the API says they can make active (can_activate).
88
+ for (const p of ['claude', 'codex']) {
89
+ const rows = list.accounts.filter(a => a.provider === p);
90
+ if (!rows.length)
91
+ continue;
92
+ const can = rows.filter(canActivate);
93
+ const ref = can.length === 1 ? `${p}:${can[0].position + 1}` : `${p}:<n>`;
94
+ if (can.length)
95
+ lines.push(rows.some(isActive) ? `Switch ${PROVIDERS[p]}: shard agents accounts use ${ref}` : `No ${PROVIDERS[p]} account is active: shard agents accounts use ${ref}`);
96
+ else if (!rows.some(isActive))
97
+ lines.push(`No ${PROVIDERS[p]} account is active: an organization owner or admin makes one active.`);
83
98
  }
84
99
  return `${lines.join('\n')}\n`;
85
100
  }
@@ -89,7 +104,7 @@ export function formatUsage(list, now) {
89
104
  return formatAccounts(list, now);
90
105
  const lines = [];
91
106
  for (const a of list.accounts) {
92
- lines.push(`${PROVIDERS[a.provider]} · ${name(a)}${plan(a) ? ` · ${plan(a)}` : ''} · ${STATUS_LABELS[a.status]} · ${threads(a.threads)}${a.preferred ? ' · in use' : ''}${asOf(a, now)}`);
107
+ lines.push(`${PROVIDERS[a.provider]} · ${name(a)}${plan(a) ? ` · ${plan(a)}` : ''} · ${STATUS_LABELS[a.status]}${isActive(a) ? ` · active · ${threads(a.threads)}` : ''}${asOf(a, now)}`);
93
108
  if (a.kind === 'api_key')
94
109
  lines.push(' Pay as you go');
95
110
  else if (!a.windows.length)
@@ -142,17 +157,17 @@ export function codexLoginUrls(output) {
142
157
  /**
143
158
  * Adds a subscription account: the provider's own sign-in runs in a fresh profile in the workspace (Claude:
144
159
  * `claude auth login --claudeai`; Codex: `codex login --device-auth`), its login files go to the API, and the profile
145
- * is deleted. The workspace's own logins are untouched; the API delivers the account threads use.
160
+ * is deleted. The workspace's own logins are untouched; the API delivers the active account's login.
146
161
  */
147
162
  async function signIn(ctx, values, app) {
148
163
  if (ctx.json || !ctx.io.stdin.isTTY || !ctx.io.stdin.setRawMode || !ctx.io.stdin.on) {
149
- throw new UsageError(`Signing in to ${PROVIDERS[app]} runs in a terminal: shard agents accounts add ${app} (or --api-key-stdin for an API key).`);
164
+ throw new UsageError(`Signing in to ${PROVIDERS[app]} runs in a terminal: shard agents accounts add ${app}`);
150
165
  }
151
166
  if (app === 'claude')
152
167
  requireClaudeTerminal(ctx);
153
168
  const ws = await signInWorkspace(ctx, values);
154
169
  const api = ctx.cloud().agentAccounts;
155
- const claim = await api.signIn(app, ws.id, { ...(typeof values.label === 'string' ? { label: values.label } : {}), ...(values.scope === 'team' || values.scope === 'personal' ? { scope: values.scope } : {}) });
170
+ const claim = await api.signIn(app, ws.id, typeof values.label === 'string' ? { label: values.label } : {});
156
171
  const root = `/tmp/shard-account-${claim.sign_in_id}`;
157
172
  const session = `account-sign-in-${claim.sign_in_id}`;
158
173
  const cell = ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs });
@@ -191,16 +206,6 @@ async function signIn(ctx, values, app) {
191
206
  await api.cancelSignIn(claim.sign_in_id).catch(() => undefined);
192
207
  }
193
208
  }
194
- /**
195
- * Pooled threads are used remotely in T3 Code (desktop, web, phone): they have no provider login for claude.ai or the
196
- * ChatGPT app. Where new conversations open stays the person's choice (Settings, or agents targets --open-in).
197
- */
198
- function remoteHint(ctx) {
199
- return t3Install(ctx.io.env)
200
- ? 'Use pooled threads remotely in T3 Code: shard agents targets --open-in claude=t3,codex=t3 opens new conversations there.\n'
201
- : 'Use pooled threads remotely in T3 Code (desktop, web and phone): install it from https://t3.codes.\n';
202
- }
203
- const notAvailable = () => new CodedError('pool_not_available', 'Pooling accounts is not available for this organization.', 'Your accounts keep serving threads directly; shard agents accounts shows their usage.');
204
209
  export const ACCOUNT_POOL_HANDLERS = {
205
210
  async 'agents accounts'(ctx, values) {
206
211
  const list = await ctx.cloud().agentAccounts.list({ provider: optionalProvider(values.provider) });
@@ -218,20 +223,9 @@ export const ACCOUNT_POOL_HANDLERS = {
218
223
  return 0;
219
224
  },
220
225
  async 'agents accounts add'(ctx, values, [app]) {
221
- const p = provider(app);
222
- if (values.scope !== undefined && values.scope !== 'personal' && values.scope !== 'team')
223
- throw new UsageError('--scope is personal or team');
224
- let account;
225
- if (values['api-key-stdin'] === true) {
226
- const key = Buffer.from(await readInput(ctx, '-', 64 * 1024, '--api-key-stdin')).toString('utf8').trim();
227
- if (!key || /\s/.test(key))
228
- throw new UsageError('--api-key-stdin reads one API key from standard input, e.g. pbpaste | shard agents accounts add claude --api-key-stdin');
229
- ctx.addSecret(key);
230
- account = await ctx.cloud().agentAccounts.addApiKey(p, key, { ...(typeof values.label === 'string' ? { label: values.label } : {}), ...(values.scope === 'team' || values.scope === 'personal' ? { scope: values.scope } : {}) });
231
- }
232
- else
233
- account = await signIn(ctx, values, p);
234
- out(ctx, `Added ${name(account)} (${PROVIDERS[account.provider]}${plan(account) ? ` ${plan(account)}` : ''}) as ${account.provider}:${account.position + 1}.\n`, ctx.json ? { ...await ctx.cloud().agentAccounts.list(), account } : account);
226
+ const account = await signIn(ctx, values, provider(app));
227
+ const ref = `${account.provider}:${account.position + 1}`;
228
+ out(ctx, `Added ${name(account)} (${PROVIDERS[account.provider]}${plan(account) ? ` ${plan(account)}` : ''}) as ${ref}${isActive(account) ? ', the active account' : `. Make it active: shard agents accounts use ${ref}`}.\n`, ctx.json ? { ...await ctx.cloud().agentAccounts.list(), account } : account);
235
229
  return 0;
236
230
  },
237
231
  async 'agents accounts remove'(ctx, _values, [ref]) {
@@ -241,33 +235,17 @@ export const ACCOUNT_POOL_HANDLERS = {
241
235
  out(ctx, `Removed ${name(account)} (${PROVIDERS[account.provider]}).\n`, ctx.json ? { ...await api.list(), removed: account.id, provider: account.provider } : null);
242
236
  return 0;
243
237
  },
244
- async 'agents accounts move'(ctx, _values, [ref, direction]) {
245
- if (direction !== 'up' && direction !== 'down')
246
- throw new UsageError('move takes an account and up or down, e.g. shard agents accounts move claude:2 up');
238
+ async 'agents accounts use'(ctx, _values, [ref]) {
247
239
  const api = ctx.cloud().agentAccounts;
248
- const account = findAccount(await api.list(), ref);
249
- const list = await api.move(account.id, direction);
250
- const moved = list.accounts.find(a => a.id === account.id) ?? account;
251
- out(ctx, moved.position === account.position ? `${name(account)} is already ${direction === 'up' ? 'first' : 'last'}.\n` : `Moved ${name(moved)} to ${moved.provider}:${moved.position + 1}.\n`, { ...list, account: moved });
252
- return 0;
253
- },
254
- async 'agents accounts pool'(ctx, _values, [state]) {
255
- if (state !== 'on' && state !== 'off')
256
- throw new UsageError('pool is on or off');
257
- let pool;
258
- try {
259
- pool = await ctx.cloud().agentPool.set({ pooled: state === 'on' });
260
- }
261
- catch (error) {
262
- if (error instanceof ShardfluxApiError && (error.status === 404 || error.details?.reason === 'pool_not_on_plan'))
263
- throw notAvailable();
264
- throw error;
265
- }
266
- // Pooled threads run through the account hub: it needs the router (codex-connector 5) and its hub token.
267
- const hub = pool.pooled ? await ensurePoolHub(ctx) : null;
268
- out(ctx, pool.pooled
269
- ? `Pooled: each new thread runs on the account with the most room. Running threads keep their account.\n${hub ? `The account hub is not ready (${hub}); new threads start directly until it is. Run shard agents accounts pool on again.\n` : ''}${remoteHint(ctx)}`
270
- : 'Direct: new threads use your account as before. Pooled threads keep their account until they end.\n', ctx.json ? { ...await ctx.cloud().agentAccounts.list(), pool, ...(pool.pooled ? { hub: hub ? { ready: false, error: hub } : { ready: true } } : {}) } : pool);
240
+ const before = await api.list();
241
+ const account = findAccount(before, ref);
242
+ const previous = before.accounts.find(a => a.provider === account.provider && isActive(a) && a.id !== account.id);
243
+ const list = await api.activate(account.id);
244
+ const active = list.accounts.find(a => a.id === account.id) ?? account;
245
+ const follows = account.provider === 'claude'
246
+ ? 'New conversations start with it; running ones continue on it, and Remote Control turns back on by itself.'
247
+ : `New conversations and every running conversation VM use it; Codex Cloud moves to it${previous ? ` (Cloud threads started on ${name(previous)} stay in that ChatGPT account)` : ''}.`;
248
+ out(ctx, previous || !isActive(account) ? `${name(active)} is the active ${PROVIDERS[account.provider]} account. ${follows}\n` : `${name(active)} is already the active ${PROVIDERS[account.provider]} account.\n`, { ...list, account: active });
271
249
  return 0;
272
250
  },
273
251
  };
@@ -284,33 +262,6 @@ export async function targetsAccounts(ctx) {
284
262
  clearTimeout(timer);
285
263
  }
286
264
  }
287
- /** The conversation's thread runs on the account pool (its `account.mode`, API with the pool). */
288
- export function conversationPooled(conversation) {
289
- const account = conversation?.account;
290
- return account?.mode === 'pooled';
291
- }
292
- /** A pooled thread has no provider login on its session, so claude.ai, Claude Desktop and the Codex app cannot reach it. */
293
- export const pooledNeedsT3 = (ref) => new CodedError('t3_not_installed', `${ref} runs on pooled accounts; open it in T3 Code.`, `Install T3 Code from https://t3.codes, open it once, then run shard agents open t3 ${ref}.`);
294
265
  // `shard accounts …` runs the same handlers (hidden aliases in args.ts).
295
266
  for (const [path, handler] of Object.entries(ACCOUNT_POOL_HANDLERS))
296
267
  ACCOUNT_POOL_HANDLERS[path.replace(/^agents /, '')] = handler;
297
- /**
298
- * The account hub in pool mode (W1): created or upgraded on codex-connector 5 with its hub token (POST
299
- * /v1/agent-pool/hub-token, minted for the hub workspace). Null when ready, else what stopped it.
300
- */
301
- async function ensurePoolHub(ctx) {
302
- try {
303
- const cloud = ctx.cloud();
304
- const hub = (accountPoolSeams.hubCloud ?? ((c) => sdkHubCloud(c.cloud(), { signal: c.signal, wakeTimeoutMs: c.wakeTimeoutMs, agentLabel: c.agentLabel, homeProject: (org) => homeProject(c, org) })))(ctx);
305
- const ensured = await ensureCliProxyHub(hub, { dir: ctx.store.dir, apiUrl: ctx.apiUrl, progress: (m) => progress(ctx, m),
306
- pool: { hubToken: async (workspaceId) => (await cloud.request('POST', '/v1/agent-pool/hub-token', { json: { workspace_id: workspaceId } })).token } });
307
- for (const value of [ensured.info.callback_url, ensured.info.management_key])
308
- ctx.addSecret(value);
309
- return null;
310
- }
311
- catch (error) {
312
- return describeError(error).message;
313
- }
314
- }
315
- /** Test seam: the hub cloud the pool toggle ensures (the SDK's by default). */
316
- export const accountPoolSeams = {};
@@ -0,0 +1,65 @@
1
+ import type { Organization, Project, ShardfluxAccount } from '@shardflux/sdk';
2
+ import type { Values } from './args.js';
3
+ import type { Ctx } from './commands.js';
4
+ import { UsageError } from './errors.js';
5
+ export declare function allOrgs(account: ShardfluxAccount): Promise<Organization[]>;
6
+ export declare function allProjects(account: ShardfluxAccount, organizationId: string): Promise<Project[]>;
7
+ export declare const choices: (rows: readonly {
8
+ id: string;
9
+ slug: string;
10
+ name: string;
11
+ }[]) => string;
12
+ export declare function byRef<T extends {
13
+ id: string;
14
+ slug: string;
15
+ name: string;
16
+ }>(rows: readonly T[], ref: string): T | undefined;
17
+ export declare const gone: (err: unknown) => boolean;
18
+ export declare function findOrg(account: ShardfluxAccount, ref: string): Promise<Organization>;
19
+ /** The fix for a saved default organization the person is no longer in. */
20
+ export declare function staleDefaultOrg(saved: string, orgs: readonly Organization[]): UsageError;
21
+ /** `ref` (a positional), else --org, else the saved default, else the only organization. */
22
+ export declare function resolveOrg(ctx: Ctx, account: ShardfluxAccount, values: Values, ref?: string): Promise<Organization>;
23
+ /** `ref` (a positional), else --project, else the saved default (unless --org names another), else the only project. */
24
+ export declare function resolveProject(ctx: Ctx, account: ShardfluxAccount, values: Values, ref?: string): Promise<Project>;
25
+ /** The API key in use (SHARDFLUX_API_KEY, else the saved one): where it acts. Null without a key or when it is rejected. */
26
+ export interface KeyScope {
27
+ keyId: string;
28
+ /** Where the key comes from: SHARDFLUX_API_KEY or the credentials file. */
29
+ source: 'env' | 'file';
30
+ organizationId: string;
31
+ projectId: string;
32
+ }
33
+ export declare function keyScope(ctx: Ctx): Promise<KeyScope | null>;
34
+ /** A default that points somewhere it should not, and the exact command that fixes it (`--json`: `warnings`). */
35
+ export interface ScopeWarning {
36
+ code: 'default_organization_gone' | 'api_key_organization_gone' | 'api_key_in_other_organization' | 'api_key_environment_override';
37
+ message: string;
38
+ fix: string;
39
+ }
40
+ /**
41
+ * Plain warnings about defaults that point into an organization the signed-in person is no longer in: the saved
42
+ * default organization, and the API key's organization. Each names the command that fixes it.
43
+ */
44
+ export declare function staleScopeWarnings(ctx: Ctx, orgs: readonly Organization[], key: KeyScope | null): ScopeWarning[];
45
+ /** Compare membership before defaults: a departed organization is different from another one still accessible. */
46
+ export declare function keyOrganizationWarning(key: KeyScope, orgs: readonly Organization[], target?: Organization, projectId?: string): ScopeWarning | null;
47
+ /**
48
+ * The stale-default warnings for the signed-in person, or none: without a session (or with one the API no longer
49
+ * accepts) there is no list of their organizations to compare with, and the command itself goes on either way.
50
+ */
51
+ export declare function sessionScopeWarnings(ctx: Ctx, key: KeyScope | null): Promise<ScopeWarning[]>;
52
+ /** Warnings on stderr, one `warning:` line and its `fix:` each (text mode). */
53
+ export declare function writeWarnings(ctx: Ctx, warnings: readonly ScopeWarning[]): void;
54
+ /** The note for a default just saved in `org` while the API key acts in another organization (orgs use, projects use). */
55
+ export declare function keyElsewhere(key: KeyScope, org: Organization): string;
56
+ /**
57
+ * The project a T3 Connect sign-in is scoped by (0.20.2+). A sign-in is per person and organization, and a session in
58
+ * more than one organization must name one (the API answers 422 without `project_id`). --project, else a project of
59
+ * --org (the saved default or the key's project when it is in that organization, else its first), else the API key's
60
+ * project (T3 links the key's workspaces with the sign-in of the key's organization, and the onboarding row checks it
61
+ * with the key), else the saved default project. Undefined when none is known: one organization needs none.
62
+ */
63
+ export declare function t3SignInProject(ctx: Ctx, values: Values): Promise<string | undefined>;
64
+ /** The API's 422 for a session in several organizations without a project (production before train 22): what to run. */
65
+ export declare function t3NeedsProject(err: unknown): UsageError | null;