agent-shim 11.78.0 → 11.80.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.
package/README.md CHANGED
@@ -57,7 +57,7 @@ That's it — with no further configuration, everything in `~/.claude` that isn'
57
57
  - **An identity** is a login: the thing that owns credentials and daemon state, selected with `agent-shim run @<name>` or, once `agent-shim shim enable` has run, `claude @<name>`.
58
58
  - **A configuration profile** is a reusable, named bundle of sharing rules, independent of any identity, resolved per working directory via directory rules or a committed `.agent-shim.json`.
59
59
 
60
- A third, optional thing sits on top of identities: **a pool** is a named list of them. Select `pool:<name>` where an identity name goes (`claude @pool:subs`) and agent-shim picks the member whose unused quota is closest to expiring, skipping any that are refused right now, using the usage it already records. A pool can instead declare `--preference listed`, which tries the members in the order the pool lists them, skipping only one that is refused right now. A member entry may itself name another pool (`pool:<name>`), which is ranked with its own policy and contributes its pick at that position, so "this identity first, then the fleet" nests the fleet rather than copying its member list; cycles are refused. See [docs/configuration-model.md](docs/configuration-model.md#pools-picking-an-account-at-launch).
60
+ A third, optional thing sits on top of identities: **a pool** is a named list of them. Select `pool:<name>` where an identity name goes (`claude @pool:subs`) and agent-shim picks the member whose unused quota is closest to expiring, skipping any that are refused right now, using the usage it already records. A pool can instead declare `--preference listed`, which tries the members in the order the pool lists them, skipping only one that is refused right now. A member entry may itself name another pool (`pool:<name>`), which is ranked with its own policy and contributes its pick at that position, so "this identity first, then the fleet" nests the fleet rather than copying its member list; cycles are refused. Members that hold the same login (the same account in the same organisation) share one quota window, so a pool ranks them as one candidate and never fails over from one to the other; `pool add`, `pool set` and `doctor` warn when a pool lists two of them. See [docs/configuration-model.md](docs/configuration-model.md#pools-picking-an-account-at-launch). On a terminal, `agent-shim run` asks which identity to run as when none resolves, when a pool pick would change account, or when the identity is refused right now (`--pick` forces the question, `--no-pick` never asks); see [docs/configuration-model.md](docs/configuration-model.md#choosing-the-identity-at-launch-the-picker).
61
61
 
62
62
  Every top-level entry in `~/.claude` is classified into one of five categories, shipped as a default map (`config/categories.default.json`):
63
63
 
@@ -200,6 +200,7 @@ agent-shim run [@<identity>] [launch flags] [claude arguments]
200
200
  # --claude-version <version> run exactly this installed Claude Code version
201
201
  # --delay <duration> wait this long after the launcher's own output before starting claude; --[no-]pause waits for a key press instead
202
202
  # --[no-]explain with @pool:<name>, list every member the pick ranked and why, not only the one picked
203
+ # --[no-]pick on a terminal, ask which identity to run as, even when one resolved (--no-pick never asks); a launch also asks by itself when no identity resolves, a pool pick would change account or organisation, or the identity is refused right now
203
204
  # --native run the real claude with nothing from agent-shim applied; cannot be combined with the flags above
204
205
  claude @<identity> ... # the same, once `agent-shim shim enable` has run
205
206
  ```
package/dist/cli.cjs CHANGED
@@ -38192,13 +38192,95 @@ var init_account = __esm({
38192
38192
  }
38193
38193
  });
38194
38194
 
38195
+ // src/usage/accountDescription.ts
38196
+ function describeAccountOrganisation(account) {
38197
+ const parts = [account.organizationName, account.organizationType].filter((part) => part !== void 0);
38198
+ return parts.length === 0 ? void 0 : parts.join(", ");
38199
+ }
38200
+ function describeAccountPlan(account) {
38201
+ const parts = [account.billingType === void 0 ? void 0 : `billing ${account.billingType}`, account.seatTier === void 0 ? void 0 : `seat tier ${account.seatTier}`].filter((part) => part !== void 0);
38202
+ return parts.length === 0 ? void 0 : parts.join(", ");
38203
+ }
38204
+ var init_accountDescription = __esm({
38205
+ "src/usage/accountDescription.ts"() {
38206
+ "use strict";
38207
+ }
38208
+ });
38209
+
38210
+ // src/identityAccounts.ts
38211
+ function readIdentityAccount(fs31, identitiesDir, identity) {
38212
+ try {
38213
+ const account = readAccountMetadata(fs31, identitiesDir, identity);
38214
+ return account === void 0 ? void 0 : { account };
38215
+ } catch (error63) {
38216
+ if (error63 instanceof AccountMetadataError) {
38217
+ return { problem: error63.message };
38218
+ }
38219
+ throw error63;
38220
+ }
38221
+ }
38222
+ function summariseIdentityAccount(account) {
38223
+ const summary = {
38224
+ ...account.emailAddress === void 0 ? {} : { emailAddress: account.emailAddress },
38225
+ ...account.organizationName === void 0 ? {} : { organizationName: account.organizationName },
38226
+ ...account.organizationType === void 0 ? {} : { organizationType: account.organizationType },
38227
+ ...account.organizationRole === void 0 ? {} : { organizationRole: account.organizationRole },
38228
+ ...account.billingType === void 0 ? {} : { billingType: account.billingType },
38229
+ ...account.seatTier === void 0 ? {} : { seatTier: account.seatTier }
38230
+ };
38231
+ return Object.keys(summary).length === 0 ? void 0 : summary;
38232
+ }
38233
+ function loginKeyOf(account) {
38234
+ return account.accountUuid === void 0 ? void 0 : JSON.stringify([account.accountUuid, account.organizationUuid ?? null]);
38235
+ }
38236
+ function collectByLogin(items, loginOf) {
38237
+ const groups = /* @__PURE__ */ new Map();
38238
+ for (const item of items) {
38239
+ const key = loginOf(item) ?? /* @__PURE__ */ Symbol("no login");
38240
+ const group = groups.get(key);
38241
+ if (group === void 0) {
38242
+ groups.set(key, [item]);
38243
+ } else {
38244
+ group.push(item);
38245
+ }
38246
+ }
38247
+ return [...groups.values()];
38248
+ }
38249
+ function groupByLogin(items, loginOf) {
38250
+ return collectByLogin(items, loginOf).flat();
38251
+ }
38252
+ function groupSharedLogins(items, loginOf) {
38253
+ return collectByLogin(items, loginOf).filter((group) => group.length > 1);
38254
+ }
38255
+ function joinNames(names) {
38256
+ const last = names.at(-1);
38257
+ return names.length < 2 || last === void 0 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${last}`;
38258
+ }
38259
+ function describeAccountHolder(summary) {
38260
+ const organisation = describeAccountOrganisation(summary);
38261
+ if (summary.emailAddress === void 0) {
38262
+ return organisation;
38263
+ }
38264
+ return organisation === void 0 ? summary.emailAddress : `${summary.emailAddress} (${organisation})`;
38265
+ }
38266
+ function describeIdentityAccount(summary) {
38267
+ return [describeAccountHolder(summary), summary.organizationRole === void 0 ? void 0 : `role ${summary.organizationRole}`, describeAccountPlan(summary)].filter((part) => part !== void 0).join("; ");
38268
+ }
38269
+ var init_identityAccounts = __esm({
38270
+ "src/identityAccounts.ts"() {
38271
+ "use strict";
38272
+ init_account();
38273
+ init_accountDescription();
38274
+ }
38275
+ });
38276
+
38195
38277
  // package.json
38196
38278
  var package_default;
38197
38279
  var init_package = __esm({
38198
38280
  "package.json"() {
38199
38281
  package_default = {
38200
38282
  name: "agent-shim",
38201
- version: "11.78.0",
38283
+ version: "11.80.0",
38202
38284
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
38203
38285
  license: "MIT",
38204
38286
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -38918,6 +39000,9 @@ function assessRefusedNested(member, nested, nowMs) {
38918
39000
  };
38919
39001
  }
38920
39002
  function assess(member, nowMs) {
39003
+ if (member.sameLoginAs !== void 0) {
39004
+ return { candidate: { identity: member.identity, class: "ineligible", plan: planOf(member.account), reasons: [`same login as ${member.sameLoginAs}`] } };
39005
+ }
38921
39006
  const nested = member.nested;
38922
39007
  if (nested === void 0) {
38923
39008
  return assessIdentity(member, nowMs);
@@ -39461,6 +39546,29 @@ function loadPoolMembers(fs31, paths, identities, nowMs) {
39461
39546
  }
39462
39547
  });
39463
39548
  }
39549
+ function snapshotAgeKey(member) {
39550
+ return member.snapshot === void 0 ? Number.NEGATIVE_INFINITY : Date.parse(member.snapshot.updatedAt);
39551
+ }
39552
+ function collapseSharedLogins(members2) {
39553
+ const groups = groupSharedLogins(members2, (member) => member.nested === void 0 && member.account !== void 0 ? loginKeyOf(member.account) : void 0);
39554
+ const representativeOf = /* @__PURE__ */ new Map();
39555
+ for (const group of groups) {
39556
+ const representative = group.reduce((freshest, member) => snapshotAgeKey(member) > snapshotAgeKey(freshest) ? member : freshest);
39557
+ for (const member of group) {
39558
+ if (member !== representative) {
39559
+ representativeOf.set(member, representative.identity);
39560
+ }
39561
+ }
39562
+ }
39563
+ return members2.map((member) => {
39564
+ const representative = representativeOf.get(member);
39565
+ return representative === void 0 ? member : { ...member, sameLoginAs: representative };
39566
+ });
39567
+ }
39568
+ function stickyOnRepresentative(sticky, members2) {
39569
+ const twin = sticky === void 0 ? void 0 : members2.find((member) => member.identity === sticky.identity && member.sameLoginAs !== void 0);
39570
+ return sticky === void 0 || twin?.sameLoginAs === void 0 ? sticky : { ...sticky, identity: twin.sameLoginAs };
39571
+ }
39464
39572
  function rankPoolGraph(input2) {
39465
39573
  return rankNamedPool(input2, input2.poolName, [], input2.sticky);
39466
39574
  }
@@ -39488,13 +39596,15 @@ function rankNamedPool(input2, poolName, stack, sticky) {
39488
39596
  members2.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
39489
39597
  missing.push(...nested.missing);
39490
39598
  }
39599
+ const collapsed = collapseSharedLogins(members2);
39600
+ const stickyPick = stickyOnRepresentative(sticky, collapsed);
39491
39601
  const ranking = rankPool({
39492
- members: members2,
39602
+ members: collapsed,
39493
39603
  nowMs: input2.nowMs,
39494
39604
  resuming: input2.resuming,
39495
39605
  ...input2.resumeContextTokens === void 0 ? {} : { resumeContextTokens: input2.resumeContextTokens },
39496
39606
  ...input2.committedContextTokens === void 0 ? {} : { committedContextTokens: input2.committedContextTokens },
39497
- ...sticky === void 0 ? {} : { sticky },
39607
+ ...stickyPick === void 0 ? {} : { sticky: stickyPick },
39498
39608
  ...pool.preference === void 0 ? {} : { preference: pool.preference }
39499
39609
  });
39500
39610
  return { ranking, missing };
@@ -39561,6 +39671,7 @@ var init_poolPick = __esm({
39561
39671
  init_cliError();
39562
39672
  init_identity();
39563
39673
  init_schema();
39674
+ init_identityAccounts();
39564
39675
  init_account();
39565
39676
  init_pick();
39566
39677
  init_read();
@@ -67175,7 +67286,8 @@ var init_argv = __esm({
67175
67286
  { flag: "--remote-control", key: "remoteControl" },
67176
67287
  { flag: "--wait", key: "wait" },
67177
67288
  { flag: "--pause", key: "pause" },
67178
- { flag: "--explain", key: "explain" }
67289
+ { flag: "--explain", key: "explain" },
67290
+ { flag: "--pick", key: "pick" }
67179
67291
  ];
67180
67292
  LAUNCHER_FLAG_NAMES = [
67181
67293
  ...VALUED_FLAGS,
@@ -69956,7 +70068,7 @@ function decideLaunch(params, readOnly) {
69956
70068
  const configDirEscapeHatchApplies = env.CLAUDE_CONFIG_DIR !== void 0 && env.CLAUDE_CONFIG_DIR !== "";
69957
70069
  const decidedIdentity = decideIdentity({
69958
70070
  env,
69959
- argv0Identity: parsedArgv.identity,
70071
+ argv0Identity: params.pickedIdentity ?? parsedArgv.identity,
69960
70072
  directoryPinnedIdentity: params.directoryPinnedIdentity,
69961
70073
  readActiveIdentityFile: () => {
69962
70074
  const raw = fs31.readFileUtf8(paths.activeIdentityFile);
@@ -117703,77 +117815,8 @@ function registerProfileCommand(program2, deps) {
117703
117815
  init_providersStore();
117704
117816
  init_identity();
117705
117817
  init_realPorts();
117706
-
117707
- // src/identityAccounts.ts
117708
- init_account();
117709
-
117710
- // src/usage/accountDescription.ts
117711
- function describeAccountOrganisation(account) {
117712
- const parts = [account.organizationName, account.organizationType].filter((part) => part !== void 0);
117713
- return parts.length === 0 ? void 0 : parts.join(", ");
117714
- }
117715
- function describeAccountPlan(account) {
117716
- const parts = [account.billingType === void 0 ? void 0 : `billing ${account.billingType}`, account.seatTier === void 0 ? void 0 : `seat tier ${account.seatTier}`].filter((part) => part !== void 0);
117717
- return parts.length === 0 ? void 0 : parts.join(", ");
117718
- }
117719
-
117720
- // src/identityAccounts.ts
117721
- function readIdentityAccount(fs31, identitiesDir, identity) {
117722
- try {
117723
- const account = readAccountMetadata(fs31, identitiesDir, identity);
117724
- return account === void 0 ? void 0 : { account };
117725
- } catch (error63) {
117726
- if (error63 instanceof AccountMetadataError) {
117727
- return { problem: error63.message };
117728
- }
117729
- throw error63;
117730
- }
117731
- }
117732
- function summariseIdentityAccount(account) {
117733
- const summary = {
117734
- ...account.emailAddress === void 0 ? {} : { emailAddress: account.emailAddress },
117735
- ...account.organizationName === void 0 ? {} : { organizationName: account.organizationName },
117736
- ...account.organizationType === void 0 ? {} : { organizationType: account.organizationType },
117737
- ...account.organizationRole === void 0 ? {} : { organizationRole: account.organizationRole },
117738
- ...account.billingType === void 0 ? {} : { billingType: account.billingType },
117739
- ...account.seatTier === void 0 ? {} : { seatTier: account.seatTier }
117740
- };
117741
- return Object.keys(summary).length === 0 ? void 0 : summary;
117742
- }
117743
- function loginKeyOf(account) {
117744
- return account.accountUuid === void 0 ? void 0 : JSON.stringify([account.accountUuid, account.organizationUuid ?? null]);
117745
- }
117746
- function collectByLogin(items, loginOf) {
117747
- const groups = /* @__PURE__ */ new Map();
117748
- for (const item of items) {
117749
- const key = loginOf(item) ?? /* @__PURE__ */ Symbol("no login");
117750
- const group = groups.get(key);
117751
- if (group === void 0) {
117752
- groups.set(key, [item]);
117753
- } else {
117754
- group.push(item);
117755
- }
117756
- }
117757
- return [...groups.values()];
117758
- }
117759
- function groupByLogin(items, loginOf) {
117760
- return collectByLogin(items, loginOf).flat();
117761
- }
117762
- function groupSharedLogins(items, loginOf) {
117763
- return collectByLogin(items, loginOf).filter((group) => group.length > 1);
117764
- }
117765
- function describeAccountHolder(summary) {
117766
- const organisation = describeAccountOrganisation(summary);
117767
- if (summary.emailAddress === void 0) {
117768
- return organisation;
117769
- }
117770
- return organisation === void 0 ? summary.emailAddress : `${summary.emailAddress} (${organisation})`;
117771
- }
117772
- function describeIdentityAccount(summary) {
117773
- return [describeAccountHolder(summary), summary.organizationRole === void 0 ? void 0 : `role ${summary.organizationRole}`, describeAccountPlan(summary)].filter((part) => part !== void 0).join("; ");
117774
- }
117775
-
117776
- // src/identityManager.ts
117818
+ init_identityAccounts();
117819
+ init_accountDescription();
117777
117820
  async function runIdentityWizard(prompts, paths, name, options = {}) {
117778
117821
  if (!IdentitySchema.safeParse({ name, allowAmbientCredential: false }).success) {
117779
117822
  throw new InvalidIdentityNameError(name);
@@ -123233,6 +123276,7 @@ init_realPorts();
123233
123276
  var import_node_fs20 = __toESM(require("node:fs"), 1);
123234
123277
  var import_node_path47 = __toESM(require("node:path"), 1);
123235
123278
  init_signIn();
123279
+ init_identityAccounts();
123236
123280
  init_load();
123237
123281
  init_unrecognisedKeys();
123238
123282
  init_schema();
@@ -123507,6 +123551,27 @@ init_providerAliases();
123507
123551
  init_providersStore();
123508
123552
  init_guard();
123509
123553
  init_identity();
123554
+
123555
+ // src/poolSharedLogins.ts
123556
+ init_identityAccounts();
123557
+ init_identity();
123558
+ function sharedLoginMembers(members2, accountOf) {
123559
+ const known = members2.flatMap((identity) => {
123560
+ const account = poolNameOf(identity) === void 0 ? accountOf(identity) : void 0;
123561
+ return account === void 0 ? [] : [{ identity, account }];
123562
+ });
123563
+ return groupSharedLogins(known, (entry) => loginKeyOf(entry.account)).flatMap((group) => {
123564
+ const first = group[0];
123565
+ return first === void 0 ? [] : [{ members: group.map((entry) => entry.identity), account: first.account }];
123566
+ });
123567
+ }
123568
+ function describeSharedLoginMembers(poolName, shared) {
123569
+ const summary = summariseIdentityAccount(shared.account);
123570
+ const holder = summary === void 0 ? void 0 : describeAccountHolder(summary);
123571
+ return `Pool "${poolName}" lists ${joinNames(shared.members)}, which hold the same login${holder === void 0 ? "" : ` (${holder})`}, so they share one quota window. A launch ranks them as one candidate and never fails over from one to another; keep one of them in the pool.`;
123572
+ }
123573
+
123574
+ // src/doctorReport.ts
123510
123575
  init_extends();
123511
123576
  var DOCTOR_SEVERITIES = ["pass", "warn", "fail"];
123512
123577
  var DOCTOR_SECTIONS = [
@@ -123529,10 +123594,6 @@ var DOCTOR_SECTIONS = [
123529
123594
  "frontdoor",
123530
123595
  "models"
123531
123596
  ];
123532
- function joinNames(names) {
123533
- const last = names.at(-1);
123534
- return names.length < 2 || last === void 0 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${last}`;
123535
- }
123536
123597
  function refinePathShadow(status, ownExecutablePath, realpath) {
123537
123598
  if (status.status !== "shadowed") {
123538
123599
  return status;
@@ -123845,6 +123906,7 @@ function runDoctor(params) {
123845
123906
  }
123846
123907
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
123847
123908
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
123909
+ const accountByIdentity = new Map(params.identities.flatMap((entry) => validIdentityNames.has(entry.name) && entry.account !== void 0 ? [[entry.name, entry.account]] : []));
123848
123910
  for (const [poolName, pool] of Object.entries(pools)) {
123849
123911
  const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
123850
123912
  const undefinedPools = pool.identities.flatMap((member) => {
@@ -123860,6 +123922,9 @@ function runDoctor(params) {
123860
123922
  ];
123861
123923
  push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
123862
123924
  }
123925
+ for (const shared of sharedLoginMembers(pool.identities.map(poolMemberIdentity), (identityName) => accountByIdentity.get(identityName))) {
123926
+ push("pool", "warn", describeSharedLoginMembers(poolName, shared), poolName);
123927
+ }
123863
123928
  }
123864
123929
  const reportedCycles = /* @__PURE__ */ new Set();
123865
123930
  for (const poolName of Object.keys(pools).sort()) {
@@ -156375,6 +156440,7 @@ init_schema();
156375
156440
  init_realPorts();
156376
156441
  init_candidateText();
156377
156442
  init_preflight();
156443
+ init_identityAccounts();
156378
156444
  function formatPoolPick(report, nowMs) {
156379
156445
  const head = report.pick === void 0 ? `Pool ${report.pool}: every member is refused right now${report.earliestReturn === void 0 ? "" : `; ${report.earliestReturn.identity} returns at ${report.earliestReturn.at} (in ${formatAge(Date.parse(report.earliestReturn.at) - nowMs)})`}` : `Pool ${report.pool}: a launch from ${report.directory} would run as ${report.pick}`;
156380
156446
  const rows = report.candidates.map((candidate, index) => {
@@ -156383,6 +156449,11 @@ function formatPoolPick(report, nowMs) {
156383
156449
  });
156384
156450
  return [head, ...rows, ...report.missing.map((name) => ` (skipped: ${name} is not an identity)`), ...report.stickyProblem === void 0 ? [] : [`Note: ${report.stickyProblem}; the last-pick record was ignored.`]];
156385
156451
  }
156452
+ function warnSharedLogins(paths, poolName, members2) {
156453
+ for (const shared of sharedLoginMembers(members2, (identity) => readIdentityAccount(realFsPort, paths.identitiesDir, identity)?.account)) {
156454
+ console.error(`agent-shim: warning: ${describeSharedLoginMembers(poolName, shared)}`);
156455
+ }
156456
+ }
156386
156457
  function policyNotes(entry) {
156387
156458
  const notes = [
156388
156459
  ...entry?.preference === void 0 ? [] : [`preference: ${entry.preference}`],
@@ -156415,6 +156486,7 @@ function registerPoolCommand(program2, deps) {
156415
156486
  reportMutation(options.json, { action: "created", kind: "pool", name, value: created }, () => {
156416
156487
  console.log(`Created pool "${name}" with ${members2.join(", ")}.`);
156417
156488
  });
156489
+ warnSharedLogins(paths, name, created.identities.map(poolMemberIdentity));
156418
156490
  }),
156419
156491
  ["agent-shim pool add subs --identity work --identity personal", "agent-shim pool add client --identity client-main --identity spare --preference listed"]
156420
156492
  );
@@ -156432,6 +156504,7 @@ function registerPoolCommand(program2, deps) {
156432
156504
  reportMutation(options.json, { action: "updated", kind: "pool", name, value: updated }, () => {
156433
156505
  console.log(`Pool "${name}" now has ${updated.identities.map(poolMemberIdentity).join(", ")}.`);
156434
156506
  });
156507
+ warnSharedLogins(paths, name, updated.identities.map(poolMemberIdentity));
156435
156508
  }),
156436
156509
  ["agent-shim pool set subs --identity work --identity personal --identity spare", "agent-shim pool set subs --preference listed", "agent-shim pool set subs --resume-on-limit --compact-before-limit --compact-at-utilisation 85"]
156437
156510
  );
@@ -157480,6 +157553,7 @@ var import_node_path70 = __toESM(require("node:path"), 1);
157480
157553
  init_commandDeps();
157481
157554
  init_realPorts();
157482
157555
  init_account();
157556
+ init_accountDescription();
157483
157557
  init_signIn();
157484
157558
  init_resets();
157485
157559
  init_read();
@@ -157820,6 +157894,242 @@ function buildProgram(deps) {
157820
157894
  // src/runClaude.ts
157821
157895
  init_cliError();
157822
157896
  init_configProfilesStore();
157897
+
157898
+ // src/launcher/pickerPrompt.ts
157899
+ function promptsIdentityPicker(prompts) {
157900
+ return {
157901
+ pick: async (request7) => {
157902
+ const answer = await prompts.select({
157903
+ message: request7.message,
157904
+ options: request7.entries.map((entry) => ({ value: entry.value, label: entry.label })),
157905
+ ...request7.initial === void 0 ? {} : { initialValue: request7.initial }
157906
+ });
157907
+ return prompts.isCancel(answer) ? { kind: "cancelled" } : { kind: "picked", value: answer };
157908
+ }
157909
+ };
157910
+ }
157911
+
157912
+ // src/launcher/runPicker.ts
157913
+ init_cliError();
157914
+ init_schema();
157915
+ init_identityAccounts();
157916
+ init_account();
157917
+ init_candidateText();
157918
+ init_pick();
157919
+ init_poolPick();
157920
+ init_identity();
157921
+ init_launchReport();
157922
+ init_hold();
157923
+
157924
+ // src/launcher/pickerEntries.ts
157925
+ var PART_SEPARATOR = " | ";
157926
+ function pickerLabel(facts) {
157927
+ const name = [facts.rank === void 0 ? void 0 : `${String(facts.rank)}.`, facts.identity, facts.heading, facts.picked === true ? "(picked)" : void 0].filter((part) => part !== void 0).join(" ");
157928
+ return [name, facts.detail, facts.account, facts.quota].filter((part) => part !== void 0).join(PART_SEPARATOR);
157929
+ }
157930
+
157931
+ // src/launcher/pickerDecision.ts
157932
+ init_claudeSubcommand();
157933
+ function decidePicker(facts) {
157934
+ if (!facts.interactive || facts.configDirEscapeHatch || facts.pick === false) {
157935
+ return void 0;
157936
+ }
157937
+ if (facts.pick === true) {
157938
+ return "requested";
157939
+ }
157940
+ if (facts.unattended) {
157941
+ return void 0;
157942
+ }
157943
+ const { selection } = facts;
157944
+ switch (selection.kind) {
157945
+ case "none":
157946
+ return "no-identity";
157947
+ case "identity":
157948
+ return selection.limited ? "no-eligible-identity" : void 0;
157949
+ case "pool":
157950
+ if (selection.outcome === "none-eligible") {
157951
+ return facts.waiting ? void 0 : "no-eligible-identity";
157952
+ }
157953
+ return selection.outcome === "changes-account" ? "account-change" : void 0;
157954
+ default:
157955
+ return selection;
157956
+ }
157957
+ }
157958
+ var TERMINATOR4 = "--";
157959
+ var UNATTENDED_FLAGS = /* @__PURE__ */ new Set(["-p", "--print", "--bg"]);
157960
+ function isUnattendedLaunch(passthrough) {
157961
+ const end = passthrough.indexOf(TERMINATOR4);
157962
+ const head = end === -1 ? passthrough : passthrough.slice(0, end);
157963
+ return head.some((token) => UNATTENDED_FLAGS.has(token)) || isClaudeSubcommand(passthrough);
157964
+ }
157965
+
157966
+ // src/launcher/runPicker.ts
157967
+ init_pool();
157968
+ var IdentityPickerCancelledError = class extends CliError {
157969
+ exitCode = LAUNCH_CANCELLED_EXIT;
157970
+ constructor() {
157971
+ super("launch cancelled at the identity picker: no identity was chosen.");
157972
+ this.name = "IdentityPickerCancelledError";
157973
+ }
157974
+ };
157975
+ function memberTexts(member, nowMs) {
157976
+ if (member === void 0) {
157977
+ return {};
157978
+ }
157979
+ const billing = { login: member.account, snapshot: member.snapshot, provider: void 0, nowMs };
157980
+ const account = accountRows(billing)[0]?.value;
157981
+ const quotaRow2 = quotaRows(billing)[0];
157982
+ return {
157983
+ ...account === void 0 ? {} : { account },
157984
+ ...quotaRow2 === void 0 ? {} : { quota: quotaRow2.detail === void 0 ? quotaRow2.value : `${quotaRow2.value} (${quotaRow2.detail})` }
157985
+ };
157986
+ }
157987
+ function refusalOf(candidate) {
157988
+ return candidate.class === "ineligible" ? candidate.reasons[0] : void 0;
157989
+ }
157990
+ function accountsDiffer(members2, left, right) {
157991
+ const keyOf3 = (identity) => {
157992
+ const account = members2.find((member) => member.identity === identity)?.account;
157993
+ return account === void 0 ? void 0 : loginKeyOf(account);
157994
+ };
157995
+ const leftKey = keyOf3(left);
157996
+ const rightKey = keyOf3(right);
157997
+ return leftKey !== void 0 && rightKey !== void 0 && leftKey !== rightKey;
157998
+ }
157999
+ function poolContext(params, poolName, resuming, nowMs) {
158000
+ const { paths, usageFs } = params;
158001
+ const { sticky } = readStickyPick(usageFs, paths.usagePicksFile, params.cwd);
158002
+ const { ranking } = rankPoolGraph({
158003
+ fs: usageFs,
158004
+ paths,
158005
+ pools: params.pools ?? {},
158006
+ poolName,
158007
+ nowMs,
158008
+ resuming,
158009
+ identityExists: (name) => loadIdentity(paths.identitiesDir, name, params.fs) !== void 0,
158010
+ ...sticky === void 0 ? {} : { sticky }
158011
+ });
158012
+ const pick2 = ranking.pick?.identity;
158013
+ const named = [.../* @__PURE__ */ new Set([...ranking.candidates.map((candidate) => candidate.identity), ...sticky === void 0 ? [] : [sticky.identity]])].filter(isIdentityName);
158014
+ const members2 = loadPoolMembers(usageFs, paths, named, nowMs);
158015
+ const changes = pick2 !== void 0 && sticky !== void 0 && sticky.identity !== pick2 && accountsDiffer(members2, sticky.identity, pick2);
158016
+ return {
158017
+ selection: { kind: "pool", outcome: pick2 === void 0 ? "none-eligible" : changes ? "changes-account" : "keeps-account" },
158018
+ entries: ranking.candidates.map((candidate, index) => ({
158019
+ value: candidate.identity,
158020
+ label: pickerLabel({
158021
+ identity: candidate.identity,
158022
+ rank: index + 1,
158023
+ heading: candidateHeading(candidate),
158024
+ picked: candidate.identity === pick2,
158025
+ detail: refusalOf(candidate),
158026
+ ...memberTexts(members2.find((member) => member.identity === candidate.identity), nowMs)
158027
+ })
158028
+ })),
158029
+ initial: pick2,
158030
+ poolPick: pick2,
158031
+ message: (reason) => poolMessage(reason, poolName, pick2, sticky)
158032
+ };
158033
+ }
158034
+ function poolMessage(reason, poolName, pick2, sticky) {
158035
+ switch (reason) {
158036
+ case "account-change":
158037
+ return `Pool "${poolName}" would run this launch as ${pick2 ?? "(none)"}, a different account from ${sticky?.identity ?? "(none)"}, which this directory last ran as. Which identity should it run as?`;
158038
+ case "no-eligible-identity":
158039
+ return `Every member of pool "${poolName}" is refused right now. Which identity should this launch run as?`;
158040
+ case "requested":
158041
+ case "no-identity":
158042
+ return `Pool "${poolName}" is selected for this launch. Which identity should it run as?`;
158043
+ default:
158044
+ return reason;
158045
+ }
158046
+ }
158047
+ function identityContext(params, resuming, nowMs) {
158048
+ const { paths, usageFs } = params;
158049
+ const members2 = loadPoolMembers(usageFs, paths, params.identityNames(), nowMs);
158050
+ const { candidates } = rankPool({ members: members2, nowMs, resuming, preference: "listed" });
158051
+ const named = params.decision.name;
158052
+ const limited = named !== void 0 && candidates.some((candidate) => candidate.identity === named && candidate.class === "ineligible");
158053
+ const identityEntries = candidates.map((candidate) => ({
158054
+ value: candidate.identity,
158055
+ label: pickerLabel({ identity: candidate.identity, detail: refusalOf(candidate), ...memberTexts(members2.find((member) => member.identity === candidate.identity), nowMs) })
158056
+ }));
158057
+ const poolEntries = Object.entries(params.pools ?? {}).map(([name, pool]) => ({
158058
+ value: `pool:${name}`,
158059
+ label: pickerLabel({ identity: `pool:${name}`, detail: `members ${pool.identities.map(poolMemberIdentity).join(", ")}` })
158060
+ }));
158061
+ return {
158062
+ selection: named === void 0 ? { kind: "none" } : { kind: "identity", limited },
158063
+ entries: [...identityEntries, ...poolEntries],
158064
+ initial: named,
158065
+ poolPick: void 0,
158066
+ message: (reason) => {
158067
+ switch (reason) {
158068
+ case "no-identity":
158069
+ return "No identity is selected for this launch. Which identity should it run as?";
158070
+ case "no-eligible-identity":
158071
+ return `${named ?? "The selected identity"} is refused right now. Which identity should this launch run as?`;
158072
+ case "requested":
158073
+ case "account-change":
158074
+ return "Which identity should this launch run as?";
158075
+ default:
158076
+ return reason;
158077
+ }
158078
+ }
158079
+ };
158080
+ }
158081
+ async function runIdentityPicker(params) {
158082
+ if (!params.interactive) {
158083
+ return { kind: "unchanged" };
158084
+ }
158085
+ const nowMs = params.now();
158086
+ const resuming = isResuming(params.parsed.rest);
158087
+ const context = params.decision.pool === void 0 ? identityContext(params, resuming, nowMs) : poolContext(params, params.decision.pool, resuming, nowMs);
158088
+ const reason = decidePicker({
158089
+ interactive: params.interactive,
158090
+ unattended: isUnattendedLaunch(params.parsed.rest),
158091
+ pick: params.parsed.pick,
158092
+ configDirEscapeHatch: params.decision.configDirEscapeHatch,
158093
+ waiting: params.parsed.wait === true,
158094
+ selection: context.selection
158095
+ });
158096
+ if (reason === void 0) {
158097
+ return { kind: "unchanged" };
158098
+ }
158099
+ const request7 = { message: context.message(reason), entries: context.entries, ...context.initial === void 0 ? {} : { initial: context.initial } };
158100
+ const answer = await params.picker.pick(request7);
158101
+ if (answer.kind === "cancelled") {
158102
+ return { kind: "cancelled" };
158103
+ }
158104
+ const chosen = answer.value;
158105
+ if (chosen === (params.decision.pool === void 0 ? params.decision.name : context.poolPick)) {
158106
+ return { kind: "unchanged" };
158107
+ }
158108
+ if (params.decision.pool !== void 0 && poolNameOf(chosen) === void 0) {
158109
+ recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, chosen, nowMs);
158110
+ }
158111
+ return { kind: "selected", selector: chosen };
158112
+ }
158113
+
158114
+ // src/launchPicker.ts
158115
+ init_realPorts();
158116
+ async function pickLaunchIdentity(params) {
158117
+ return await runIdentityPicker({
158118
+ parsed: params.parsed,
158119
+ decision: params.decision,
158120
+ interactive: process.stdin.isTTY && process.stdout.isTTY,
158121
+ paths: params.paths,
158122
+ fs: realFsPort,
158123
+ usageFs: params.farm.runtime.fs,
158124
+ pools: params.farm.pools,
158125
+ cwd: params.farm.runtime.cwd,
158126
+ now: params.farm.runtime.now,
158127
+ identityNames: () => listIdentities(params.paths).flatMap((listing) => "problem" in listing ? [] : [listing.name]),
158128
+ picker: promptsIdentityPicker(realPromptsPort)
158129
+ });
158130
+ }
158131
+
158132
+ // src/runClaude.ts
157823
158133
  init_launchWiring();
157824
158134
  init_paths();
157825
158135
 
@@ -158589,6 +158899,7 @@ function otherLaunchFlags(parsed) {
158589
158899
  ...parsed.skipPermissions === void 0 ? [] : ["--[no-]skip-permissions"],
158590
158900
  ...parsed.remoteControl === void 0 ? [] : ["--[no-]remote-control"],
158591
158901
  ...parsed.wait === void 0 ? [] : ["--[no-]wait"],
158902
+ ...parsed.pick === void 0 ? [] : ["--[no-]pick"],
158592
158903
  ...parsed.categoryFlags.length === 0 ? [] : ["--category"],
158593
158904
  ...parsed.shareFlags.length === 0 ? [] : ["--share"],
158594
158905
  ...parsed.hideFlags.length === 0 ? [] : ["--hide"]
@@ -158654,13 +158965,19 @@ async function runClaude(argvOverride) {
158654
158965
  }
158655
158966
  }
158656
158967
  }
158657
- await refreshPools({
158658
- paths,
158659
- cwd: process.cwd(),
158660
- poolNames: identityDecision.pool === void 0 ? [] : [identityDecision.pool],
158661
- spawnDaemon: spawnDetachedSupervisor,
158662
- log: realLogPort.warn
158663
- });
158968
+ const refreshUsage = async (poolName2) => {
158969
+ await refreshPools({ paths, cwd: process.cwd(), poolNames: poolName2 === void 0 ? [] : [poolName2], spawnDaemon: spawnDetachedSupervisor, log: realLogPort.warn });
158970
+ };
158971
+ await refreshUsage(identityDecision.pool);
158972
+ const picked = await pickLaunchIdentity({ paths, farm, parsed: nativeArgv, decision: identityDecision });
158973
+ if (picked.kind === "cancelled") {
158974
+ throw new IdentityPickerCancelledError();
158975
+ }
158976
+ const pickedIdentity = picked.kind === "selected" ? picked.selector : void 0;
158977
+ const launchDecision = pickedIdentity === void 0 ? identityDecision : decideIdentity({ env: procForDecision.env, argv0Identity: pickedIdentity, readActiveIdentityFile: () => void 0 });
158978
+ if (launchDecision.pool !== identityDecision.pool) {
158979
+ await refreshUsage(launchDecision.pool);
158980
+ }
158664
158981
  const prepareParams = {
158665
158982
  ...realPrepareLaunchParams(paths, {
158666
158983
  proc: argvOverride === void 0 ? realProcPort : { ...realProcPort, argv: argvOverride },
@@ -158669,10 +158986,11 @@ async function runClaude(argvOverride) {
158669
158986
  farm,
158670
158987
  allowMissingConfigProfile
158671
158988
  }),
158989
+ ...pickedIdentity === void 0 ? {} : { pickedIdentity },
158672
158990
  // Wired here rather than in realPrepareLaunchParams because the port re-invokes this very binary: correct for the agent-shim command line, wrong for a library host whose own executable is not agent-shim.
158673
158991
  update: realLaunchUpdatePort(paths)
158674
158992
  };
158675
- const poolName = identityDecision.pool;
158993
+ const poolName = launchDecision.pool;
158676
158994
  if (poolName !== void 0 && farm.pools !== void 0 && supervisesLaunch(farm.pools[poolName], nativeArgv.rest)) {
158677
158995
  const code = await runSupervised({
158678
158996
  paths,
package/dist/index.cjs CHANGED
@@ -4764,7 +4764,7 @@ var init_package = __esm({
4764
4764
  "package.json"() {
4765
4765
  package_default = {
4766
4766
  name: "agent-shim",
4767
- version: "11.78.0",
4767
+ version: "11.80.0",
4768
4768
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
4769
4769
  license: "MIT",
4770
4770
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -7037,6 +7037,78 @@ var init_account = __esm({
7037
7037
  }
7038
7038
  });
7039
7039
 
7040
+ // src/usage/accountDescription.ts
7041
+ function describeAccountOrganisation(account) {
7042
+ const parts = [account.organizationName, account.organizationType].filter((part) => part !== void 0);
7043
+ return parts.length === 0 ? void 0 : parts.join(", ");
7044
+ }
7045
+ var init_accountDescription = __esm({
7046
+ "src/usage/accountDescription.ts"() {
7047
+ "use strict";
7048
+ }
7049
+ });
7050
+
7051
+ // src/identityAccounts.ts
7052
+ function readIdentityAccount(fs14, identitiesDir, identity) {
7053
+ try {
7054
+ const account = readAccountMetadata(fs14, identitiesDir, identity);
7055
+ return account === void 0 ? void 0 : { account };
7056
+ } catch (error) {
7057
+ if (error instanceof AccountMetadataError) {
7058
+ return { problem: error.message };
7059
+ }
7060
+ throw error;
7061
+ }
7062
+ }
7063
+ function summariseIdentityAccount(account) {
7064
+ const summary = {
7065
+ ...account.emailAddress === void 0 ? {} : { emailAddress: account.emailAddress },
7066
+ ...account.organizationName === void 0 ? {} : { organizationName: account.organizationName },
7067
+ ...account.organizationType === void 0 ? {} : { organizationType: account.organizationType },
7068
+ ...account.organizationRole === void 0 ? {} : { organizationRole: account.organizationRole },
7069
+ ...account.billingType === void 0 ? {} : { billingType: account.billingType },
7070
+ ...account.seatTier === void 0 ? {} : { seatTier: account.seatTier }
7071
+ };
7072
+ return Object.keys(summary).length === 0 ? void 0 : summary;
7073
+ }
7074
+ function loginKeyOf(account) {
7075
+ return account.accountUuid === void 0 ? void 0 : JSON.stringify([account.accountUuid, account.organizationUuid ?? null]);
7076
+ }
7077
+ function collectByLogin(items, loginOf) {
7078
+ const groups = /* @__PURE__ */ new Map();
7079
+ for (const item of items) {
7080
+ const key = loginOf(item) ?? /* @__PURE__ */ Symbol("no login");
7081
+ const group = groups.get(key);
7082
+ if (group === void 0) {
7083
+ groups.set(key, [item]);
7084
+ } else {
7085
+ group.push(item);
7086
+ }
7087
+ }
7088
+ return [...groups.values()];
7089
+ }
7090
+ function groupSharedLogins(items, loginOf) {
7091
+ return collectByLogin(items, loginOf).filter((group) => group.length > 1);
7092
+ }
7093
+ function joinNames(names) {
7094
+ const last = names.at(-1);
7095
+ return names.length < 2 || last === void 0 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${last}`;
7096
+ }
7097
+ function describeAccountHolder(summary) {
7098
+ const organisation = describeAccountOrganisation(summary);
7099
+ if (summary.emailAddress === void 0) {
7100
+ return organisation;
7101
+ }
7102
+ return organisation === void 0 ? summary.emailAddress : `${summary.emailAddress} (${organisation})`;
7103
+ }
7104
+ var init_identityAccounts = __esm({
7105
+ "src/identityAccounts.ts"() {
7106
+ "use strict";
7107
+ init_account();
7108
+ init_accountDescription();
7109
+ }
7110
+ });
7111
+
7040
7112
  // src/usage/rateLimit.ts
7041
7113
  function epochSecondsToIso(value) {
7042
7114
  if (value === void 0) {
@@ -7404,6 +7476,9 @@ function assessRefusedNested(member, nested, nowMs) {
7404
7476
  };
7405
7477
  }
7406
7478
  function assess(member, nowMs) {
7479
+ if (member.sameLoginAs !== void 0) {
7480
+ return { candidate: { identity: member.identity, class: "ineligible", plan: planOf(member.account), reasons: [`same login as ${member.sameLoginAs}`] } };
7481
+ }
7407
7482
  const nested = member.nested;
7408
7483
  if (nested === void 0) {
7409
7484
  return assessIdentity(member, nowMs);
@@ -7876,6 +7951,29 @@ function loadPoolMembers(fs14, paths, identities, nowMs) {
7876
7951
  }
7877
7952
  });
7878
7953
  }
7954
+ function snapshotAgeKey(member) {
7955
+ return member.snapshot === void 0 ? Number.NEGATIVE_INFINITY : Date.parse(member.snapshot.updatedAt);
7956
+ }
7957
+ function collapseSharedLogins(members) {
7958
+ const groups = groupSharedLogins(members, (member) => member.nested === void 0 && member.account !== void 0 ? loginKeyOf(member.account) : void 0);
7959
+ const representativeOf = /* @__PURE__ */ new Map();
7960
+ for (const group of groups) {
7961
+ const representative = group.reduce((freshest, member) => snapshotAgeKey(member) > snapshotAgeKey(freshest) ? member : freshest);
7962
+ for (const member of group) {
7963
+ if (member !== representative) {
7964
+ representativeOf.set(member, representative.identity);
7965
+ }
7966
+ }
7967
+ }
7968
+ return members.map((member) => {
7969
+ const representative = representativeOf.get(member);
7970
+ return representative === void 0 ? member : { ...member, sameLoginAs: representative };
7971
+ });
7972
+ }
7973
+ function stickyOnRepresentative(sticky, members) {
7974
+ const twin = sticky === void 0 ? void 0 : members.find((member) => member.identity === sticky.identity && member.sameLoginAs !== void 0);
7975
+ return sticky === void 0 || twin?.sameLoginAs === void 0 ? sticky : { ...sticky, identity: twin.sameLoginAs };
7976
+ }
7879
7977
  function rankPoolGraph(input) {
7880
7978
  return rankNamedPool(input, input.poolName, [], input.sticky);
7881
7979
  }
@@ -7903,13 +8001,15 @@ function rankNamedPool(input, poolName, stack, sticky) {
7903
8001
  members.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
7904
8002
  missing.push(...nested.missing);
7905
8003
  }
8004
+ const collapsed = collapseSharedLogins(members);
8005
+ const stickyPick = stickyOnRepresentative(sticky, collapsed);
7906
8006
  const ranking = rankPool({
7907
- members,
8007
+ members: collapsed,
7908
8008
  nowMs: input.nowMs,
7909
8009
  resuming: input.resuming,
7910
8010
  ...input.resumeContextTokens === void 0 ? {} : { resumeContextTokens: input.resumeContextTokens },
7911
8011
  ...input.committedContextTokens === void 0 ? {} : { committedContextTokens: input.committedContextTokens },
7912
- ...sticky === void 0 ? {} : { sticky },
8012
+ ...stickyPick === void 0 ? {} : { sticky: stickyPick },
7913
8013
  ...pool.preference === void 0 ? {} : { preference: pool.preference }
7914
8014
  });
7915
8015
  return { ranking, missing };
@@ -7997,6 +8097,7 @@ var init_poolPick = __esm({
7997
8097
  init_cliError();
7998
8098
  init_identity();
7999
8099
  init_schema();
8100
+ init_identityAccounts();
8000
8101
  init_account();
8001
8102
  init_pick();
8002
8103
  init_read();
@@ -10726,7 +10827,8 @@ var init_argv = __esm({
10726
10827
  { flag: "--remote-control", key: "remoteControl" },
10727
10828
  { flag: "--wait", key: "wait" },
10728
10829
  { flag: "--pause", key: "pause" },
10729
- { flag: "--explain", key: "explain" }
10830
+ { flag: "--explain", key: "explain" },
10831
+ { flag: "--pick", key: "pick" }
10730
10832
  ];
10731
10833
  LAUNCHER_FLAG_NAMES = [
10732
10834
  ...VALUED_FLAGS,
@@ -11812,7 +11914,7 @@ function decideLaunch(params, readOnly) {
11812
11914
  const configDirEscapeHatchApplies = env.CLAUDE_CONFIG_DIR !== void 0 && env.CLAUDE_CONFIG_DIR !== "";
11813
11915
  const decidedIdentity = decideIdentity({
11814
11916
  env,
11815
- argv0Identity: parsedArgv.identity,
11917
+ argv0Identity: params.pickedIdentity ?? parsedArgv.identity,
11816
11918
  directoryPinnedIdentity: params.directoryPinnedIdentity,
11817
11919
  readActiveIdentityFile: () => {
11818
11920
  const raw = fs14.readFileUtf8(paths.activeIdentityFile);
@@ -17083,65 +17185,7 @@ init_realPorts();
17083
17185
  var import_node_fs10 = __toESM(require("node:fs"), 1);
17084
17186
  var import_node_path29 = __toESM(require("node:path"), 1);
17085
17187
  init_signIn();
17086
-
17087
- // src/identityAccounts.ts
17088
- init_account();
17089
-
17090
- // src/usage/accountDescription.ts
17091
- function describeAccountOrganisation(account) {
17092
- const parts = [account.organizationName, account.organizationType].filter((part) => part !== void 0);
17093
- return parts.length === 0 ? void 0 : parts.join(", ");
17094
- }
17095
-
17096
- // src/identityAccounts.ts
17097
- function readIdentityAccount(fs14, identitiesDir, identity) {
17098
- try {
17099
- const account = readAccountMetadata(fs14, identitiesDir, identity);
17100
- return account === void 0 ? void 0 : { account };
17101
- } catch (error) {
17102
- if (error instanceof AccountMetadataError) {
17103
- return { problem: error.message };
17104
- }
17105
- throw error;
17106
- }
17107
- }
17108
- function summariseIdentityAccount(account) {
17109
- const summary = {
17110
- ...account.emailAddress === void 0 ? {} : { emailAddress: account.emailAddress },
17111
- ...account.organizationName === void 0 ? {} : { organizationName: account.organizationName },
17112
- ...account.organizationType === void 0 ? {} : { organizationType: account.organizationType },
17113
- ...account.organizationRole === void 0 ? {} : { organizationRole: account.organizationRole },
17114
- ...account.billingType === void 0 ? {} : { billingType: account.billingType },
17115
- ...account.seatTier === void 0 ? {} : { seatTier: account.seatTier }
17116
- };
17117
- return Object.keys(summary).length === 0 ? void 0 : summary;
17118
- }
17119
- function loginKeyOf(account) {
17120
- return account.accountUuid === void 0 ? void 0 : JSON.stringify([account.accountUuid, account.organizationUuid ?? null]);
17121
- }
17122
- function collectByLogin(items, loginOf) {
17123
- const groups = /* @__PURE__ */ new Map();
17124
- for (const item of items) {
17125
- const key = loginOf(item) ?? /* @__PURE__ */ Symbol("no login");
17126
- const group = groups.get(key);
17127
- if (group === void 0) {
17128
- groups.set(key, [item]);
17129
- } else {
17130
- group.push(item);
17131
- }
17132
- }
17133
- return [...groups.values()];
17134
- }
17135
- function groupSharedLogins(items, loginOf) {
17136
- return collectByLogin(items, loginOf).filter((group) => group.length > 1);
17137
- }
17138
- function describeAccountHolder(summary) {
17139
- const organisation = describeAccountOrganisation(summary);
17140
- if (summary.emailAddress === void 0) {
17141
- return organisation;
17142
- }
17143
- return organisation === void 0 ? summary.emailAddress : `${summary.emailAddress} (${organisation})`;
17144
- }
17188
+ init_identityAccounts();
17145
17189
 
17146
17190
  // src/codex/siwcStore.ts
17147
17191
  var import_zod26 = require("zod");
@@ -17495,6 +17539,27 @@ init_providerAliases();
17495
17539
  init_providersStore();
17496
17540
  init_guard();
17497
17541
  init_identity();
17542
+
17543
+ // src/poolSharedLogins.ts
17544
+ init_identityAccounts();
17545
+ init_identity();
17546
+ function sharedLoginMembers(members, accountOf) {
17547
+ const known = members.flatMap((identity) => {
17548
+ const account = poolNameOf(identity) === void 0 ? accountOf(identity) : void 0;
17549
+ return account === void 0 ? [] : [{ identity, account }];
17550
+ });
17551
+ return groupSharedLogins(known, (entry) => loginKeyOf(entry.account)).flatMap((group) => {
17552
+ const first = group[0];
17553
+ return first === void 0 ? [] : [{ members: group.map((entry) => entry.identity), account: first.account }];
17554
+ });
17555
+ }
17556
+ function describeSharedLoginMembers(poolName, shared) {
17557
+ const summary = summariseIdentityAccount(shared.account);
17558
+ const holder = summary === void 0 ? void 0 : describeAccountHolder(summary);
17559
+ return `Pool "${poolName}" lists ${joinNames(shared.members)}, which hold the same login${holder === void 0 ? "" : ` (${holder})`}, so they share one quota window. A launch ranks them as one candidate and never fails over from one to another; keep one of them in the pool.`;
17560
+ }
17561
+
17562
+ // src/doctorReport.ts
17498
17563
  init_extends();
17499
17564
  var DOCTOR_SEVERITIES = ["pass", "warn", "fail"];
17500
17565
  var DOCTOR_SECTIONS = [
@@ -17517,10 +17582,6 @@ var DOCTOR_SECTIONS = [
17517
17582
  "frontdoor",
17518
17583
  "models"
17519
17584
  ];
17520
- function joinNames(names) {
17521
- const last = names.at(-1);
17522
- return names.length < 2 || last === void 0 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${last}`;
17523
- }
17524
17585
  function refinePathShadow(status, ownExecutablePath, realpath) {
17525
17586
  if (status.status !== "shadowed") {
17526
17587
  return status;
@@ -17833,6 +17894,7 @@ function runDoctor(params) {
17833
17894
  }
17834
17895
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
17835
17896
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
17897
+ const accountByIdentity = new Map(params.identities.flatMap((entry) => validIdentityNames.has(entry.name) && entry.account !== void 0 ? [[entry.name, entry.account]] : []));
17836
17898
  for (const [poolName, pool] of Object.entries(pools)) {
17837
17899
  const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
17838
17900
  const undefinedPools = pool.identities.flatMap((member) => {
@@ -17848,6 +17910,9 @@ function runDoctor(params) {
17848
17910
  ];
17849
17911
  push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
17850
17912
  }
17913
+ for (const shared of sharedLoginMembers(pool.identities.map(poolMemberIdentity), (identityName) => accountByIdentity.get(identityName))) {
17914
+ push("pool", "warn", describeSharedLoginMembers(poolName, shared), poolName);
17915
+ }
17851
17916
  }
17852
17917
  const reportedCycles = /* @__PURE__ */ new Set();
17853
17918
  for (const poolName of Object.keys(pools).sort()) {
package/dist/index.mjs CHANGED
@@ -4740,7 +4740,7 @@ var init_package = __esm({
4740
4740
  "package.json"() {
4741
4741
  package_default = {
4742
4742
  name: "agent-shim",
4743
- version: "11.78.0",
4743
+ version: "11.80.0",
4744
4744
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
4745
4745
  license: "MIT",
4746
4746
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -7012,6 +7012,78 @@ var init_account = __esm({
7012
7012
  }
7013
7013
  });
7014
7014
 
7015
+ // src/usage/accountDescription.ts
7016
+ function describeAccountOrganisation(account) {
7017
+ const parts = [account.organizationName, account.organizationType].filter((part) => part !== void 0);
7018
+ return parts.length === 0 ? void 0 : parts.join(", ");
7019
+ }
7020
+ var init_accountDescription = __esm({
7021
+ "src/usage/accountDescription.ts"() {
7022
+ "use strict";
7023
+ }
7024
+ });
7025
+
7026
+ // src/identityAccounts.ts
7027
+ function readIdentityAccount(fs14, identitiesDir, identity) {
7028
+ try {
7029
+ const account = readAccountMetadata(fs14, identitiesDir, identity);
7030
+ return account === void 0 ? void 0 : { account };
7031
+ } catch (error) {
7032
+ if (error instanceof AccountMetadataError) {
7033
+ return { problem: error.message };
7034
+ }
7035
+ throw error;
7036
+ }
7037
+ }
7038
+ function summariseIdentityAccount(account) {
7039
+ const summary = {
7040
+ ...account.emailAddress === void 0 ? {} : { emailAddress: account.emailAddress },
7041
+ ...account.organizationName === void 0 ? {} : { organizationName: account.organizationName },
7042
+ ...account.organizationType === void 0 ? {} : { organizationType: account.organizationType },
7043
+ ...account.organizationRole === void 0 ? {} : { organizationRole: account.organizationRole },
7044
+ ...account.billingType === void 0 ? {} : { billingType: account.billingType },
7045
+ ...account.seatTier === void 0 ? {} : { seatTier: account.seatTier }
7046
+ };
7047
+ return Object.keys(summary).length === 0 ? void 0 : summary;
7048
+ }
7049
+ function loginKeyOf(account) {
7050
+ return account.accountUuid === void 0 ? void 0 : JSON.stringify([account.accountUuid, account.organizationUuid ?? null]);
7051
+ }
7052
+ function collectByLogin(items, loginOf) {
7053
+ const groups = /* @__PURE__ */ new Map();
7054
+ for (const item of items) {
7055
+ const key = loginOf(item) ?? /* @__PURE__ */ Symbol("no login");
7056
+ const group = groups.get(key);
7057
+ if (group === void 0) {
7058
+ groups.set(key, [item]);
7059
+ } else {
7060
+ group.push(item);
7061
+ }
7062
+ }
7063
+ return [...groups.values()];
7064
+ }
7065
+ function groupSharedLogins(items, loginOf) {
7066
+ return collectByLogin(items, loginOf).filter((group) => group.length > 1);
7067
+ }
7068
+ function joinNames(names) {
7069
+ const last = names.at(-1);
7070
+ return names.length < 2 || last === void 0 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${last}`;
7071
+ }
7072
+ function describeAccountHolder(summary) {
7073
+ const organisation = describeAccountOrganisation(summary);
7074
+ if (summary.emailAddress === void 0) {
7075
+ return organisation;
7076
+ }
7077
+ return organisation === void 0 ? summary.emailAddress : `${summary.emailAddress} (${organisation})`;
7078
+ }
7079
+ var init_identityAccounts = __esm({
7080
+ "src/identityAccounts.ts"() {
7081
+ "use strict";
7082
+ init_account();
7083
+ init_accountDescription();
7084
+ }
7085
+ });
7086
+
7015
7087
  // src/usage/rateLimit.ts
7016
7088
  function epochSecondsToIso(value) {
7017
7089
  if (value === void 0) {
@@ -7379,6 +7451,9 @@ function assessRefusedNested(member, nested, nowMs) {
7379
7451
  };
7380
7452
  }
7381
7453
  function assess(member, nowMs) {
7454
+ if (member.sameLoginAs !== void 0) {
7455
+ return { candidate: { identity: member.identity, class: "ineligible", plan: planOf(member.account), reasons: [`same login as ${member.sameLoginAs}`] } };
7456
+ }
7382
7457
  const nested = member.nested;
7383
7458
  if (nested === void 0) {
7384
7459
  return assessIdentity(member, nowMs);
@@ -7853,6 +7928,29 @@ function loadPoolMembers(fs14, paths, identities, nowMs) {
7853
7928
  }
7854
7929
  });
7855
7930
  }
7931
+ function snapshotAgeKey(member) {
7932
+ return member.snapshot === void 0 ? Number.NEGATIVE_INFINITY : Date.parse(member.snapshot.updatedAt);
7933
+ }
7934
+ function collapseSharedLogins(members) {
7935
+ const groups = groupSharedLogins(members, (member) => member.nested === void 0 && member.account !== void 0 ? loginKeyOf(member.account) : void 0);
7936
+ const representativeOf = /* @__PURE__ */ new Map();
7937
+ for (const group of groups) {
7938
+ const representative = group.reduce((freshest, member) => snapshotAgeKey(member) > snapshotAgeKey(freshest) ? member : freshest);
7939
+ for (const member of group) {
7940
+ if (member !== representative) {
7941
+ representativeOf.set(member, representative.identity);
7942
+ }
7943
+ }
7944
+ }
7945
+ return members.map((member) => {
7946
+ const representative = representativeOf.get(member);
7947
+ return representative === void 0 ? member : { ...member, sameLoginAs: representative };
7948
+ });
7949
+ }
7950
+ function stickyOnRepresentative(sticky, members) {
7951
+ const twin = sticky === void 0 ? void 0 : members.find((member) => member.identity === sticky.identity && member.sameLoginAs !== void 0);
7952
+ return sticky === void 0 || twin?.sameLoginAs === void 0 ? sticky : { ...sticky, identity: twin.sameLoginAs };
7953
+ }
7856
7954
  function rankPoolGraph(input) {
7857
7955
  return rankNamedPool(input, input.poolName, [], input.sticky);
7858
7956
  }
@@ -7880,13 +7978,15 @@ function rankNamedPool(input, poolName, stack, sticky) {
7880
7978
  members.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
7881
7979
  missing.push(...nested.missing);
7882
7980
  }
7981
+ const collapsed = collapseSharedLogins(members);
7982
+ const stickyPick = stickyOnRepresentative(sticky, collapsed);
7883
7983
  const ranking = rankPool({
7884
- members,
7984
+ members: collapsed,
7885
7985
  nowMs: input.nowMs,
7886
7986
  resuming: input.resuming,
7887
7987
  ...input.resumeContextTokens === void 0 ? {} : { resumeContextTokens: input.resumeContextTokens },
7888
7988
  ...input.committedContextTokens === void 0 ? {} : { committedContextTokens: input.committedContextTokens },
7889
- ...sticky === void 0 ? {} : { sticky },
7989
+ ...stickyPick === void 0 ? {} : { sticky: stickyPick },
7890
7990
  ...pool.preference === void 0 ? {} : { preference: pool.preference }
7891
7991
  });
7892
7992
  return { ranking, missing };
@@ -7972,6 +8072,7 @@ var init_poolPick = __esm({
7972
8072
  init_cliError();
7973
8073
  init_identity();
7974
8074
  init_schema();
8075
+ init_identityAccounts();
7975
8076
  init_account();
7976
8077
  init_pick();
7977
8078
  init_read();
@@ -10699,7 +10800,8 @@ var init_argv = __esm({
10699
10800
  { flag: "--remote-control", key: "remoteControl" },
10700
10801
  { flag: "--wait", key: "wait" },
10701
10802
  { flag: "--pause", key: "pause" },
10702
- { flag: "--explain", key: "explain" }
10803
+ { flag: "--explain", key: "explain" },
10804
+ { flag: "--pick", key: "pick" }
10703
10805
  ];
10704
10806
  LAUNCHER_FLAG_NAMES = [
10705
10807
  ...VALUED_FLAGS,
@@ -11787,7 +11889,7 @@ function decideLaunch(params, readOnly) {
11787
11889
  const configDirEscapeHatchApplies = env.CLAUDE_CONFIG_DIR !== void 0 && env.CLAUDE_CONFIG_DIR !== "";
11788
11890
  const decidedIdentity = decideIdentity({
11789
11891
  env,
11790
- argv0Identity: parsedArgv.identity,
11892
+ argv0Identity: params.pickedIdentity ?? parsedArgv.identity,
11791
11893
  directoryPinnedIdentity: params.directoryPinnedIdentity,
11792
11894
  readActiveIdentityFile: () => {
11793
11895
  const raw = fs14.readFileUtf8(paths.activeIdentityFile);
@@ -16668,65 +16770,7 @@ init_realPorts();
16668
16770
  import fs11 from "node:fs";
16669
16771
  import path29 from "node:path";
16670
16772
  init_signIn();
16671
-
16672
- // src/identityAccounts.ts
16673
- init_account();
16674
-
16675
- // src/usage/accountDescription.ts
16676
- function describeAccountOrganisation(account) {
16677
- const parts = [account.organizationName, account.organizationType].filter((part) => part !== void 0);
16678
- return parts.length === 0 ? void 0 : parts.join(", ");
16679
- }
16680
-
16681
- // src/identityAccounts.ts
16682
- function readIdentityAccount(fs14, identitiesDir, identity) {
16683
- try {
16684
- const account = readAccountMetadata(fs14, identitiesDir, identity);
16685
- return account === void 0 ? void 0 : { account };
16686
- } catch (error) {
16687
- if (error instanceof AccountMetadataError) {
16688
- return { problem: error.message };
16689
- }
16690
- throw error;
16691
- }
16692
- }
16693
- function summariseIdentityAccount(account) {
16694
- const summary = {
16695
- ...account.emailAddress === void 0 ? {} : { emailAddress: account.emailAddress },
16696
- ...account.organizationName === void 0 ? {} : { organizationName: account.organizationName },
16697
- ...account.organizationType === void 0 ? {} : { organizationType: account.organizationType },
16698
- ...account.organizationRole === void 0 ? {} : { organizationRole: account.organizationRole },
16699
- ...account.billingType === void 0 ? {} : { billingType: account.billingType },
16700
- ...account.seatTier === void 0 ? {} : { seatTier: account.seatTier }
16701
- };
16702
- return Object.keys(summary).length === 0 ? void 0 : summary;
16703
- }
16704
- function loginKeyOf(account) {
16705
- return account.accountUuid === void 0 ? void 0 : JSON.stringify([account.accountUuid, account.organizationUuid ?? null]);
16706
- }
16707
- function collectByLogin(items, loginOf) {
16708
- const groups = /* @__PURE__ */ new Map();
16709
- for (const item of items) {
16710
- const key = loginOf(item) ?? /* @__PURE__ */ Symbol("no login");
16711
- const group = groups.get(key);
16712
- if (group === void 0) {
16713
- groups.set(key, [item]);
16714
- } else {
16715
- group.push(item);
16716
- }
16717
- }
16718
- return [...groups.values()];
16719
- }
16720
- function groupSharedLogins(items, loginOf) {
16721
- return collectByLogin(items, loginOf).filter((group) => group.length > 1);
16722
- }
16723
- function describeAccountHolder(summary) {
16724
- const organisation = describeAccountOrganisation(summary);
16725
- if (summary.emailAddress === void 0) {
16726
- return organisation;
16727
- }
16728
- return organisation === void 0 ? summary.emailAddress : `${summary.emailAddress} (${organisation})`;
16729
- }
16773
+ init_identityAccounts();
16730
16774
 
16731
16775
  // src/codex/siwcStore.ts
16732
16776
  import { z as z26 } from "zod";
@@ -17080,6 +17124,27 @@ init_providerAliases();
17080
17124
  init_providersStore();
17081
17125
  init_guard();
17082
17126
  init_identity();
17127
+
17128
+ // src/poolSharedLogins.ts
17129
+ init_identityAccounts();
17130
+ init_identity();
17131
+ function sharedLoginMembers(members, accountOf) {
17132
+ const known = members.flatMap((identity) => {
17133
+ const account = poolNameOf(identity) === void 0 ? accountOf(identity) : void 0;
17134
+ return account === void 0 ? [] : [{ identity, account }];
17135
+ });
17136
+ return groupSharedLogins(known, (entry) => loginKeyOf(entry.account)).flatMap((group) => {
17137
+ const first = group[0];
17138
+ return first === void 0 ? [] : [{ members: group.map((entry) => entry.identity), account: first.account }];
17139
+ });
17140
+ }
17141
+ function describeSharedLoginMembers(poolName, shared) {
17142
+ const summary = summariseIdentityAccount(shared.account);
17143
+ const holder = summary === void 0 ? void 0 : describeAccountHolder(summary);
17144
+ return `Pool "${poolName}" lists ${joinNames(shared.members)}, which hold the same login${holder === void 0 ? "" : ` (${holder})`}, so they share one quota window. A launch ranks them as one candidate and never fails over from one to another; keep one of them in the pool.`;
17145
+ }
17146
+
17147
+ // src/doctorReport.ts
17083
17148
  init_extends();
17084
17149
  var DOCTOR_SEVERITIES = ["pass", "warn", "fail"];
17085
17150
  var DOCTOR_SECTIONS = [
@@ -17102,10 +17167,6 @@ var DOCTOR_SECTIONS = [
17102
17167
  "frontdoor",
17103
17168
  "models"
17104
17169
  ];
17105
- function joinNames(names) {
17106
- const last = names.at(-1);
17107
- return names.length < 2 || last === void 0 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${last}`;
17108
- }
17109
17170
  function refinePathShadow(status, ownExecutablePath, realpath) {
17110
17171
  if (status.status !== "shadowed") {
17111
17172
  return status;
@@ -17418,6 +17479,7 @@ function runDoctor(params) {
17418
17479
  }
17419
17480
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
17420
17481
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
17482
+ const accountByIdentity = new Map(params.identities.flatMap((entry) => validIdentityNames.has(entry.name) && entry.account !== void 0 ? [[entry.name, entry.account]] : []));
17421
17483
  for (const [poolName, pool] of Object.entries(pools)) {
17422
17484
  const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
17423
17485
  const undefinedPools = pool.identities.flatMap((member) => {
@@ -17433,6 +17495,9 @@ function runDoctor(params) {
17433
17495
  ];
17434
17496
  push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
17435
17497
  }
17498
+ for (const shared of sharedLoginMembers(pool.identities.map(poolMemberIdentity), (identityName) => accountByIdentity.get(identityName))) {
17499
+ push("pool", "warn", describeSharedLoginMembers(poolName, shared), poolName);
17500
+ }
17436
17501
  }
17437
17502
  const reportedCycles = /* @__PURE__ */ new Set();
17438
17503
  for (const poolName of Object.keys(pools).sort()) {
@@ -14032,6 +14032,8 @@ interface PrepareLaunchParams {
14032
14032
  readonly log: LogPort;
14033
14033
  /** Discovers the real `claude` binary to spawn. Injected so `runLauncher` never depends on `src/versionDiscovery.ts`'s own filesystem/PATH inputs directly — the caller (`src/cli.ts`) wires the real discovery, tests wire a fake that returns a fixed path. */
14034
14034
  readonly resolveClaudeBinary: ClaudeBinaryResolver;
14035
+ /** The identity (or `pool:<name>`) the person chose at the identity picker, which takes the place of whatever the arguments named: the question was asked because that choice was not settled. */
14036
+ readonly pickedIdentity?: string;
14035
14037
  /** An identity pinned to `$PWD` by a directory rule. Accepted as an already-resolved value — the rules-loading code that produces it lands in Phase 4/5. */
14036
14038
  readonly directoryPinnedIdentity?: string;
14037
14039
  /** A directory rule's `configProfile` selection for `$PWD`. Accepted as an already-resolved value for the same reason. */
@@ -26334,6 +26336,8 @@ interface PoolMember {
26334
26336
  readonly records: readonly UsageRecord[];
26335
26337
  /** Present when this member stands for a nested `pool:<name>` entry rather than a direct identity. */
26336
26338
  readonly nested?: NestedContribution;
26339
+ /** The member that represents this member's login in the ranking, when both hold the same login and so draw on one quota window. This member is then ineligible as a candidate of its own, since ranking it beside its representative would count one window twice and could fail over to a login that is limited too. */
26340
+ readonly sameLoginAs?: string;
26337
26341
  /** The member entry's policy condition, from the object form of a pool member: definite false skips the member as ineligible by policy, indeterminate demotes it to last within its class, naming the missing fact either way. */
26338
26342
  readonly policy?: WhenCondition;
26339
26343
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-shim",
3
- "version": "11.78.0",
3
+ "version": "11.80.0",
4
4
  "description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5
5
  "license": "MIT",
6
6
  "author": "Joseph Mearman <joseph@mearman.co.uk>",