agent-shim 8.6.0 → 8.8.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/dist/index.mjs CHANGED
@@ -3323,25 +3323,19 @@ function realConnectEffects() {
3323
3323
  }
3324
3324
  };
3325
3325
  },
3326
- connectTlsUpstream: async (host, port, clientAlpn) => await new Promise((resolve, reject) => {
3327
- const dial = (localPort) => {
3328
- const raw = net2.connect({ host, port, localPort, lookup: realAddressLookup });
3329
- return tls.connect({ socket: raw, servername: host, ...clientAlpn === void 0 ? {} : { ALPNProtocols: [clientAlpn] } });
3330
- };
3331
- const settle = (candidate) => {
3332
- candidate.once("secureConnect", () => {
3333
- resolve(candidate);
3334
- });
3335
- candidate.once("error", (error) => {
3336
- if ("code" in error && error.code === "EADDRINUSE") {
3337
- settle(dial(upstreamLocalPort()));
3338
- return;
3339
- }
3340
- reject(error);
3326
+ connectTlsUpstream: async (host, port, clientAlpn) => {
3327
+ const dialled = await reservedDial({ host, port });
3328
+ if (dialled.error !== null) {
3329
+ throw dialled.error;
3330
+ }
3331
+ return await new Promise((resolve, reject) => {
3332
+ const secure = tls.connect({ socket: dialled.socket, servername: host, ...clientAlpn === void 0 ? {} : { ALPNProtocols: [clientAlpn] } });
3333
+ secure.once("secureConnect", () => {
3334
+ resolve(secure);
3341
3335
  });
3342
- };
3343
- settle(dial(upstreamLocalPort()));
3344
- }),
3336
+ secure.once("error", reject);
3337
+ });
3338
+ },
3345
3339
  connectTcp: async (host, port) => await new Promise((resolve, reject) => {
3346
3340
  const socket = net2.connect({ host, port });
3347
3341
  socket.once("connect", () => {
@@ -4903,547 +4897,380 @@ function listUsageSnapshots(fs12, snapshotsDir) {
4903
4897
  }).sort((left, right) => left.identity.localeCompare(right.identity));
4904
4898
  }
4905
4899
 
4906
- // src/headroom/ensure.ts
4907
- var HEADROOM_START_TIMEOUT_MS = 12e4;
4908
- var HEADROOM_LAUNCHER_POLL_MS = 250;
4909
- var HeadroomStartError = class extends CliError {
4910
- constructor(message) {
4911
- super(message);
4912
- this.name = "HeadroomStartError";
4913
- }
4914
- };
4915
- function ensureHeadroom(params) {
4916
- const { paths, ports } = params;
4917
- const deadline = ports.now() + HEADROOM_START_TIMEOUT_MS;
4918
- let spawnedSupervisor = false;
4919
- let awaitingAllowlist = false;
4920
- for (; ; ) {
4921
- const state = readHeadroomState(ports.fs, paths.headroomStateFile);
4922
- if (state?.supervisorPid !== void 0 && ports.isRunning(state.supervisorPid)) {
4923
- const daemonUp = state.socketPath !== void 0 && state.headroomPid !== void 0 && ports.isRunning(state.headroomPid);
4924
- if (daemonUp && state.socketPath !== void 0) {
4925
- const required = params.requiredAllowlistHash?.();
4926
- if (required === void 0 || state.allowlistHash === required) {
4927
- writeSession(ports.fs, paths.headroomSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), supervisorPid: state.supervisorPid });
4928
- return { socketPath: state.socketPath };
4929
- }
4930
- const holders = sessionsForSupervisor(ports.fs, paths.headroomSessionsDir, state.supervisorPid).filter((session) => ports.isRunning(session.pid));
4931
- if (holders.length > 0) {
4932
- throw new HeadroomStartError(
4933
- `the running headroom daemon was started before this launch's front door address and would refuse every request this launch sends through it. It restarts once the launches using it end (pid ${holders.map((session) => String(session.pid)).join(", ")}); end them and launch again, or launch with --no-headroom.`
4934
- );
4935
- }
4936
- awaitingAllowlist = true;
4937
- }
4938
- if (state.lastError !== void 0) {
4939
- throw new HeadroomStartError(
4940
- `the headroom daemon reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.headroomLogPath})`
4941
- );
4942
- }
4943
- } else if (!spawnedSupervisor) {
4944
- ports.fs.mkdirp(paths.headroomDir);
4945
- const created = ports.fs.writeFileExclusive(
4946
- paths.headroomLockFile,
4947
- `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
4948
- `
4949
- );
4950
- if (created) {
4951
- ports.spawnSupervisor(paths);
4952
- spawnedSupervisor = true;
4953
- } else {
4954
- const lock = readStartLock(ports.fs, paths.headroomLockFile);
4955
- if (lock === void 0 || !ports.isRunning(lock.pid)) {
4956
- ports.fs.removeRecursive(paths.headroomLockFile);
4957
- continue;
4958
- }
4959
- }
4960
- }
4961
- if (ports.now() >= deadline) {
4962
- const lastError = state?.lastError;
4963
- throw new HeadroomStartError(
4964
- `the headroom daemon did not become ready within ${String(HEADROOM_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (awaitingAllowlist ? " (the running daemon's allowlist does not admit this launch's front door, and it did not restart)" : "") + `. Daemon log: ${paths.headroomLogPath}`
4965
- );
4966
- }
4967
- ports.sleep(HEADROOM_LAUNCHER_POLL_MS);
4900
+ // src/usage/preflight.ts
4901
+ var MS_PER_SECOND2 = 1e3;
4902
+ var SECONDS_PER_MINUTE = 60;
4903
+ var MINUTES_PER_HOUR = 60;
4904
+ var HOURS_PER_DAY = 24;
4905
+ var WARNING_STATUSES = /* @__PURE__ */ new Set(["allowed_warning", "rejected"]);
4906
+ function formatAge(ms) {
4907
+ const minutes = Math.floor(ms / (MS_PER_SECOND2 * SECONDS_PER_MINUTE));
4908
+ if (minutes < MINUTES_PER_HOUR) {
4909
+ return `${String(Math.max(minutes, 0))}m`;
4968
4910
  }
4911
+ const hours = Math.floor(minutes / MINUTES_PER_HOUR);
4912
+ return hours < HOURS_PER_DAY ? `${String(hours)}h` : `${String(Math.floor(hours / HOURS_PER_DAY))}d`;
4969
4913
  }
4970
-
4971
- // src/headroom/settings.ts
4972
- import { createHash as createHash4 } from "node:crypto";
4973
- function settingsOf(config) {
4914
+ function effectiveWindow(window, nowMs) {
4915
+ const resetsAtMs = window.resetsAt === void 0 ? void 0 : Date.parse(window.resetsAt);
4916
+ if (resetsAtMs !== void 0 && resetsAtMs <= nowMs) {
4917
+ return { reset: true, utilization: 0 };
4918
+ }
4974
4919
  return {
4975
- ...config.mode === void 0 ? {} : { mode: config.mode },
4976
- ...config.targetRatio === void 0 ? {} : { targetRatio: config.targetRatio },
4977
- ...config.ccr === void 0 ? {} : { ccr: config.ccr },
4978
- ...config.rolloutChannel === void 0 ? {} : { rolloutChannel: config.rolloutChannel },
4979
- ...config.interceptToolResults === void 0 ? {} : { interceptToolResults: config.interceptToolResults },
4980
- ...config.readMaturation === void 0 ? {} : { readMaturation: config.readMaturation }
4920
+ reset: false,
4921
+ ...window.utilization === void 0 ? {} : { utilization: window.utilization },
4922
+ ...resetsAtMs === void 0 ? {} : { resetsAtMs },
4923
+ ...window.status === void 0 ? {} : { status: window.status }
4981
4924
  };
4982
4925
  }
4983
- function settingsArgs(settings) {
4984
- return [
4985
- ...settings.mode === void 0 ? [] : ["--mode", settings.mode],
4986
- ...settings.targetRatio === void 0 ? [] : ["--target-ratio", String(settings.targetRatio)],
4987
- ...settings.ccr === "lossless" ? ["--lossless"] : [],
4988
- ...settings.ccr === "none" ? ["--no-ccr"] : [],
4989
- ...settings.interceptToolResults === true ? ["--intercept-tool-results"] : [],
4990
- ...settings.readMaturation === true ? ["--read-maturation"] : []
4926
+ function quotaWarnings(snapshot, provider, nowMs) {
4927
+ const rateLimit = snapshot?.providers[provider]?.rateLimit;
4928
+ const unified = rateLimit?.unified;
4929
+ if (snapshot === void 0 || rateLimit === void 0 || unified === void 0) {
4930
+ return [];
4931
+ }
4932
+ const age = formatAge(nowMs - Date.parse(rateLimit.observedAt));
4933
+ const windows = [
4934
+ { name: "five-hour", window: unified.fiveHour },
4935
+ { name: "seven-day", window: unified.sevenDay }
4991
4936
  ];
4992
- }
4993
- function settingsEnv(settings) {
4994
- return settings.rolloutChannel === void 0 ? {} : { HEADROOM_ROLLOUT_CHANNEL: settings.rolloutChannel };
4995
- }
4996
- function hashSettings(settings) {
4997
- return createHash4("sha256").update(JSON.stringify([...settingsArgs(settings), JSON.stringify(settingsEnv(settings))])).digest("hex");
4937
+ return windows.flatMap(({ name, window }) => {
4938
+ const effective = window === void 0 ? void 0 : effectiveWindow(window, nowMs);
4939
+ if (window === void 0 || effective?.status === void 0 || !WARNING_STATUSES.has(effective.status)) {
4940
+ return [];
4941
+ }
4942
+ const reset = window.resetsAt === void 0 ? "" : `, resets ${window.resetsAt}`;
4943
+ const verdict = effective.status === "rejected" ? "exhausted" : "nearly used";
4944
+ return [`agent-shim: identity ${snapshot.identity}: the ${name} quota is ${verdict}${reset} (last seen ${age} ago)`];
4945
+ });
4998
4946
  }
4999
4947
 
5000
- // src/headroom/socket.ts
5001
- import path11 from "node:path";
5002
- var GROUP_OR_OTHER_BITS = 63;
5003
- var OCTAL_RADIX = 8;
5004
- var MODE_DIGITS = 4;
5005
- var HEADROOM_SOCKET_SUFFIX = ".sock";
5006
- var HEADROOM_SOCKET_NAME = /^(\d+)\.sock$/;
5007
- var UNIX_SOCKET_PATH_MAX_BYTES = 103;
5008
- var PLATFORMS_WITHOUT_UNIX_SOCKETS = ["win32"];
5009
- function headroomSocketPath(paths, supervisorPid) {
5010
- return path11.join(paths.headroomSocketDir, `${String(supervisorPid)}${HEADROOM_SOCKET_SUFFIX}`);
5011
- }
5012
- function platformRefusal(platform) {
5013
- return PLATFORMS_WITHOUT_UNIX_SOCKETS.includes(platform) ? `headroom routing needs a unix domain socket, which headroom cannot serve on ${platform}; agent-shim never falls back to a TCP port for this hop` : void 0;
5014
- }
5015
- function ownerOnlyRefusal(label, target, stat, expected, uid) {
5016
- if (stat === void 0) {
5017
- return `the headroom ${label} ${target} does not exist`;
5018
- }
5019
- if (stat.kind === "symlink") {
5020
- return `the headroom ${label} ${target} is a symlink; it must be the real ${expected === "dir" ? "directory" : "socket"}`;
5021
- }
5022
- if (stat.kind !== expected) {
5023
- return `the headroom ${label} ${target} is not a ${expected === "dir" ? "directory" : "socket"}`;
4948
+ // src/usage/plan.ts
4949
+ import { z as z6 } from "zod";
4950
+ var PlanClassSchema = z6.union([
4951
+ z6.strictObject({
4952
+ kind: z6.literal("subscription"),
4953
+ /** The plan's size as a multiple of the smallest subscription tier (Max 5x is 5, Max 20x is 20). */
4954
+ capacity: z6.number().positive(),
4955
+ /** False when the tier string named no multiplier and no tier this project knows, so `capacity` is the neutral 1 rather than a reading. */
4956
+ recognised: z6.boolean(),
4957
+ /** The tier string the classification rests on, when the account had one. */
4958
+ tier: z6.string().optional()
4959
+ }),
4960
+ z6.strictObject({
4961
+ kind: z6.literal("pay-per-use"),
4962
+ tier: z6.string().optional()
4963
+ })
4964
+ ]);
4965
+ var TIER_MULTIPLIER = /(?:^|_)(\d+)x$/;
4966
+ var ZERO_TIER = /(?:^|_)zero$/;
4967
+ var BASE_TIER = /(?:^|_)(?:pro|free)$/;
4968
+ function planOf(account) {
4969
+ const tier = account?.organizationRateLimitTier ?? account?.userRateLimitTier;
4970
+ const tierField = tier === void 0 ? {} : { tier };
4971
+ if (tier === void 0) {
4972
+ return { kind: "subscription", capacity: 1, recognised: false };
5024
4973
  }
5025
- if (stat.uid !== uid) {
5026
- return `the headroom ${label} ${target} is owned by uid ${String(stat.uid)}, not by this user (uid ${String(uid)})`;
4974
+ if (ZERO_TIER.test(tier)) {
4975
+ return { kind: "pay-per-use", ...tierField };
5027
4976
  }
5028
- if ((stat.mode & GROUP_OR_OTHER_BITS) !== 0) {
5029
- return `the headroom ${label} ${target} has mode ${stat.mode.toString(OCTAL_RADIX).padStart(MODE_DIGITS, "0")}, which lets other users reach it; it must be accessible to its owner only`;
4977
+ const multiplier = TIER_MULTIPLIER.exec(tier)?.[1];
4978
+ if (multiplier !== void 0) {
4979
+ return { kind: "subscription", capacity: Number(multiplier), recognised: true, ...tierField };
5030
4980
  }
5031
- return void 0;
4981
+ return { kind: "subscription", capacity: 1, recognised: BASE_TIER.test(tier), ...tierField };
5032
4982
  }
5033
- function prepareHeadroomSocket(fs12, ports, paths, supervisorPid) {
5034
- const unsupported = platformRefusal(ports.platform);
5035
- if (unsupported !== void 0) {
5036
- return { refused: unsupported };
5037
- }
5038
- const socketPath = headroomSocketPath(paths, supervisorPid);
5039
- const length = Buffer.byteLength(socketPath, "utf8");
5040
- if (length > UNIX_SOCKET_PATH_MAX_BYTES) {
5041
- return {
5042
- refused: `the headroom socket path ${socketPath} is ${String(length)} bytes, longer than the ${String(UNIX_SOCKET_PATH_MAX_BYTES)} a unix socket path can hold; set AGENT_SHIM_HOME to a shorter directory`
5043
- };
5044
- }
5045
- if (ports.lstat(paths.headroomSocketDir)?.kind === "symlink") {
5046
- return { refused: `the headroom socket directory ${paths.headroomSocketDir} is a symlink; it must be the real directory` };
4983
+
4984
+ // src/usage/rateLimit.ts
4985
+ var MS_PER_SECOND3 = 1e3;
4986
+ function epochSecondsToIso(value) {
4987
+ if (value === void 0) {
4988
+ return void 0;
5047
4989
  }
5048
- fs12.mkdirPrivate(paths.headroomSocketDir);
5049
- const refusal = ownerOnlyRefusal("socket directory", paths.headroomSocketDir, ports.lstat(paths.headroomSocketDir), "dir", ports.currentUid());
5050
- return refusal === void 0 ? { socketPath } : { refused: refusal };
4990
+ const seconds = Number(value);
4991
+ return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * MS_PER_SECOND3).toISOString() : void 0;
5051
4992
  }
5052
- function pruneStaleHeadroomSockets(fs12, paths, isRunning) {
5053
- const removed = [];
5054
- for (const name of fs12.readdir(paths.headroomSocketDir)) {
5055
- const pid = HEADROOM_SOCKET_NAME.exec(name)?.[1];
5056
- if (pid === void 0 || isRunning(Number.parseInt(pid, 10))) {
5057
- continue;
5058
- }
5059
- fs12.removeRecursive(path11.join(paths.headroomSocketDir, name));
5060
- removed.push(name);
4993
+ function nonNegativeNumber(value) {
4994
+ if (value === void 0 || value.trim() === "") {
4995
+ return void 0;
5061
4996
  }
5062
- return removed;
4997
+ const parsed = Number(value);
4998
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
5063
4999
  }
5064
-
5065
- // src/headroom/source.ts
5066
- import { z as z6 } from "zod";
5067
- var GIT_SPEC_RE = /git\+[^@\s]+(?:@([^\s#]+))?/;
5068
- var COMMIT_SHA_RE = /^[0-9a-f]{40}$/i;
5069
- var DirectUrlSchema = z6.object({ vcs_info: z6.object({ commit_id: z6.string() }) });
5070
- function isMovingGitSource(spec) {
5071
- const match = GIT_SPEC_RE.exec(spec);
5072
- if (match === null) {
5073
- return false;
5000
+ var UNIFIED_PREFIX = "anthropic-ratelimit-unified-";
5001
+ function unifiedWindow(headers, window) {
5002
+ const utilization = nonNegativeNumber(headers[`${UNIFIED_PREFIX}${window}-utilization`]);
5003
+ const resetsAt = epochSecondsToIso(headers[`${UNIFIED_PREFIX}${window}-reset`]);
5004
+ const status = headers[`${UNIFIED_PREFIX}${window}-status`];
5005
+ if (utilization === void 0 && resetsAt === void 0 && status === void 0) {
5006
+ return void 0;
5074
5007
  }
5075
- const ref = match[1];
5076
- return ref === void 0 || !COMMIT_SHA_RE.test(ref);
5077
- }
5078
- function pinnedGitCommit(spec) {
5079
- const ref = GIT_SPEC_RE.exec(spec)?.[1];
5080
- return ref !== void 0 && COMMIT_SHA_RE.test(ref) ? ref.toLowerCase() : void 0;
5008
+ return { ...utilization === void 0 ? {} : { utilization }, ...resetsAt === void 0 ? {} : { resetsAt }, ...status === void 0 ? {} : { status } };
5081
5009
  }
5082
-
5083
- // src/headroom/supervisor.ts
5084
- function resolveSupervisorConfig(configured) {
5010
+ function parseUnifiedRateLimit(headers) {
5011
+ if (!Object.keys(headers).some((name) => name.startsWith(UNIFIED_PREFIX))) {
5012
+ return void 0;
5013
+ }
5014
+ const status = headers[`${UNIFIED_PREFIX}status`];
5015
+ const fiveHour = unifiedWindow(headers, "5h");
5016
+ const sevenDay = unifiedWindow(headers, "7d");
5017
+ const representativeClaim = headers[`${UNIFIED_PREFIX}representative-claim`];
5018
+ const resetAt = epochSecondsToIso(headers[`${UNIFIED_PREFIX}reset`]);
5019
+ const overageStatus = headers[`${UNIFIED_PREFIX}overage-status`];
5085
5020
  return {
5086
- source: configured.source ?? HEADROOM_DEFAULT_SOURCE,
5087
- idleShutdownMinutes: configured.idleShutdownMinutes ?? HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES,
5088
- settings: settingsOf(configured)
5021
+ ...status === void 0 ? {} : { status },
5022
+ ...fiveHour === void 0 ? {} : { fiveHour },
5023
+ ...sevenDay === void 0 ? {} : { sevenDay },
5024
+ ...representativeClaim === void 0 ? {} : { representativeClaim },
5025
+ ...resetAt === void 0 ? {} : { resetAt },
5026
+ ...overageStatus === void 0 ? {} : { overageStatus }
5089
5027
  };
5090
5028
  }
5091
- var HEADROOM_BACKOFF_BASE_MS = 500;
5092
- var HEADROOM_BACKOFF_CAP_MS = 3e4;
5093
- var HEADROOM_START_RETRY_BUDGET = 5;
5094
- var HEADROOM_READY_TIMEOUT_MS = 15e3;
5095
- var HEADROOM_POLL_MS = 1e3;
5096
- var MS_PER_MINUTE2 = 6e4;
5097
- function backoffForAttempt(attempt) {
5098
- return Math.min(HEADROOM_BACKOFF_CAP_MS, HEADROOM_BACKOFF_BASE_MS * 2 ** (attempt - 1));
5029
+
5030
+ // src/usage/scan.ts
5031
+ import { z as z7 } from "zod";
5032
+ var AnthropicUsageSchema = z7.looseObject({
5033
+ input_tokens: z7.number().int().nonnegative().nullish(),
5034
+ output_tokens: z7.number().int().nonnegative().nullish(),
5035
+ cache_creation_input_tokens: z7.number().int().nonnegative().nullish(),
5036
+ cache_read_input_tokens: z7.number().int().nonnegative().nullish()
5037
+ });
5038
+ var ErrorObjectSchema = z7.looseObject({
5039
+ type: z7.string().optional(),
5040
+ code: z7.union([z7.string(), z7.number()]).optional(),
5041
+ message: z7.string().optional()
5042
+ });
5043
+
5044
+ // src/usage/middleware.ts
5045
+ var ANTHROPIC_PROVIDER = "anthropic";
5046
+
5047
+ // src/usage/pick.ts
5048
+ var MS_PER_SECOND4 = 1e3;
5049
+ var MS_PER_HOUR = 36e5;
5050
+ var HOURS_PER_DAY2 = 24;
5051
+ var FIVE_HOURS = 5;
5052
+ var PROMPT_CACHE_TTL_HOURS = 1;
5053
+ var PERCENT = 100;
5054
+ var SEVEN_DAYS = 7;
5055
+ var FIVE_HOUR_WINDOW_MS = FIVE_HOURS * MS_PER_HOUR;
5056
+ var SEVEN_DAY_WINDOW_MS = SEVEN_DAYS * HOURS_PER_DAY2 * MS_PER_HOUR;
5057
+ var PROMPT_CACHE_TTL_MS = PROMPT_CACHE_TTL_HOURS * MS_PER_HOUR;
5058
+ var CLASS_ORDER = { scored: 0, unknown: 1, "pay-per-use": 2, ineligible: 3 };
5059
+ function unifiedOf(state) {
5060
+ return state?.rateLimit?.unified;
5099
5061
  }
5100
- function versionSatisfies(version, source) {
5101
- if (version === void 0) {
5102
- return false;
5103
- }
5104
- const specifier = /==|>=|<=|!=|>|<|~=/.exec(source);
5105
- if (specifier === null) {
5106
- return true;
5107
- }
5108
- const installed = parseVersion(version);
5109
- const wanted = parseVersion(source.slice(specifier.index + specifier[0].length));
5110
- if (installed === void 0 || wanted === void 0) {
5111
- return true;
5062
+ function limitBlockedUntil(state, nowMs) {
5063
+ const limit = state?.lastLimit;
5064
+ if (state === void 0 || limit === void 0) {
5065
+ return void 0;
5112
5066
  }
5113
- const cmp = compareVersions(installed, wanted);
5114
- switch (specifier[0]) {
5115
- case "==":
5116
- return cmp === 0;
5117
- case "!=":
5118
- return cmp !== 0;
5119
- case ">=":
5120
- return cmp >= 0;
5121
- case "<=":
5122
- return cmp <= 0;
5123
- case ">":
5124
- return cmp > 0;
5125
- case "<":
5126
- return cmp < 0;
5127
- case "~=":
5128
- return compareVersions(installed.slice(0, Math.max(0, wanted.length - 1)), wanted.slice(0, Math.max(0, wanted.length - 1))) === 0 && cmp >= 0;
5129
- default:
5130
- return true;
5067
+ if (Date.parse(state.lastRequestAt) > Date.parse(limit.observedAt)) {
5068
+ return void 0;
5131
5069
  }
5070
+ const until = limit.resetAt === void 0 ? limit.retryAfterSeconds === void 0 ? void 0 : Date.parse(limit.observedAt) + limit.retryAfterSeconds * MS_PER_SECOND4 : Date.parse(limit.resetAt);
5071
+ return until !== void 0 && until > nowMs ? until : void 0;
5132
5072
  }
5133
- function parseVersion(input) {
5134
- const match = /(\d+)(\.\d+)*/.exec(input);
5135
- if (match === null) {
5073
+ function windowBlockedUntil(windows) {
5074
+ const resets = windows.flatMap((window) => window.status === "rejected" && window.resetsAtMs !== void 0 ? [window.resetsAtMs] : []);
5075
+ return resets.length === 0 ? void 0 : Math.max(...resets);
5076
+ }
5077
+ function fiveHourBurn(records) {
5078
+ const points = [];
5079
+ for (const record of records) {
5080
+ const window = record.rateLimitHeaders === void 0 ? void 0 : parseUnifiedRateLimit(record.rateLimitHeaders)?.fiveHour;
5081
+ if (window?.utilization !== void 0 && window.resetsAt !== void 0) {
5082
+ points.push({ resetsAt: window.resetsAt, point: { atMs: Date.parse(record.at), utilization: window.utilization } });
5083
+ }
5084
+ }
5085
+ const latest = points.at(-1)?.resetsAt;
5086
+ const current = points.filter((entry) => entry.resetsAt === latest).map((entry) => entry.point);
5087
+ const first = current.at(0);
5088
+ const last = current.at(-1);
5089
+ if (first === void 0 || last === void 0 || last.atMs <= first.atMs || last.utilization <= first.utilization) {
5136
5090
  return void 0;
5137
5091
  }
5138
- return match[0].split(".").map((part) => Number.parseInt(part, 10));
5092
+ return (last.utilization - first.utilization) / (last.atMs - first.atMs);
5139
5093
  }
5140
- function compareVersions(a, b) {
5141
- for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
5142
- const left = a[index] ?? 0;
5143
- const right = b[index] ?? 0;
5144
- if (left !== right) {
5145
- return left - right;
5146
- }
5094
+ function percent(fraction) {
5095
+ return `${String(Math.round(fraction * PERCENT))}%`;
5096
+ }
5097
+ function describeWindow(label, window, effective, nowMs) {
5098
+ if (effective.reset) {
5099
+ return `${label} window has reset`;
5147
5100
  }
5148
- return 0;
5101
+ const used = effective.utilization === void 0 ? "usage unknown" : `${percent(effective.utilization)} used`;
5102
+ const resets = effective.resetsAtMs === void 0 ? "" : `, resets in ${formatAge(effective.resetsAtMs - nowMs)}`;
5103
+ return `${label} ${used}${resets}${window.status === "allowed_warning" ? " (nearly used)" : ""}`;
5149
5104
  }
5150
- var HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
5151
- async function runSupervisor(initialConfig, ports, options = {}) {
5152
- const { fs: fs12, paths } = ports;
5153
- let config = initialConfig;
5154
- const previousState = readHeadroomState(fs12, paths.headroomStateFile);
5155
- if (previousState?.supervisorPid !== void 0 && previousState.supervisorPid !== ports.ownPid && ports.isRunning(previousState.supervisorPid)) {
5156
- ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: supervisor ${String(previousState.supervisorPid)} already owns the daemon; exiting`);
5157
- return 0;
5105
+ function capacityOf(plan) {
5106
+ return plan.kind === "subscription" ? plan.capacity : 1;
5107
+ }
5108
+ function assess(member, nowMs) {
5109
+ const plan = planOf(member.account);
5110
+ const planReasons = plan.kind === "pay-per-use" ? ["billed by use (no plan allowance)"] : plan.recognised ? [`${String(plan.capacity)}x plan`] : [`plan tier ${plan.tier === void 0 ? "not known" : `"${plan.tier}" not recognised`}, counted as 1x`];
5111
+ const base = { identity: member.identity, plan };
5112
+ if (member.readError !== void 0) {
5113
+ return { candidate: { ...base, class: "unknown", reasons: [`usage state unreadable: ${member.readError}`, ...planReasons] } };
5158
5114
  }
5159
- let installedSource = previousState?.installedSource;
5160
- const ownsState = () => {
5161
- const owner = readHeadroomState(fs12, paths.headroomStateFile)?.supervisorPid;
5162
- return owner === void 0 || owner === ports.ownPid || !ports.isRunning(owner);
5163
- };
5164
- const writeOwnState = (state) => {
5165
- if (!ownsState()) {
5166
- return false;
5167
- }
5168
- writeHeadroomState(fs12, paths.headroomStateFile, state);
5169
- return true;
5170
- };
5171
- const fail = (message) => {
5172
- if (writeOwnState({ lastError: message })) {
5173
- fs12.removeRecursive(paths.headroomLockFile);
5174
- }
5175
- ports.log(`agent-shim headroom supervisor: ${message}`);
5176
- return 1;
5177
- };
5178
- const ensureInstalled = () => {
5179
- const version2 = ports.headroomVersion();
5180
- const versionOk = version2 !== void 0 && versionSatisfies(version2, config.source);
5181
- const pinned = pinnedGitCommit(config.source);
5182
- const commitOk = pinned === void 0 || ports.installedCommit() === pinned;
5183
- if (versionOk && commitOk && (installedSource === void 0 || installedSource === config.source)) {
5184
- installedSource = config.source;
5185
- return true;
5186
- }
5187
- ports.log(`agent-shim headroom supervisor: installing headroom from ${config.source}`);
5188
- const result = ports.install(config.source);
5189
- if (!result.ok) {
5190
- return false;
5115
+ const state = member.snapshot?.providers[ANTHROPIC_PROVIDER];
5116
+ const unified = unifiedOf(state);
5117
+ if (member.snapshot === void 0 || state === void 0 || unified === void 0) {
5118
+ return { candidate: { ...base, class: "unknown", reasons: ["no usage recorded yet, so its quota is unknown (turn on trackUsage for it)", ...planReasons] } };
5119
+ }
5120
+ const observedAgo = state.rateLimit === void 0 ? "" : ` (last seen ${formatAge(nowMs - Date.parse(state.rateLimit.observedAt))} ago)`;
5121
+ const fiveHourWindow = unified.fiveHour;
5122
+ const sevenDayWindow = unified.sevenDay;
5123
+ const five = fiveHourWindow === void 0 ? void 0 : effectiveWindow(fiveHourWindow, nowMs);
5124
+ const seven = sevenDayWindow === void 0 ? void 0 : effectiveWindow(sevenDayWindow, nowMs);
5125
+ const windowReasons = [
5126
+ ...fiveHourWindow === void 0 || five === void 0 ? [] : [describeWindow("5h", fiveHourWindow, five, nowMs)],
5127
+ ...sevenDayWindow === void 0 || seven === void 0 ? [] : [describeWindow("7d", sevenDayWindow, seven, nowMs)]
5128
+ ];
5129
+ const blockedUntil = Math.max(
5130
+ windowBlockedUntil([five, seven].flatMap((window) => window === void 0 ? [] : [window])) ?? 0,
5131
+ limitBlockedUntil(state, nowMs) ?? 0
5132
+ );
5133
+ if (blockedUntil > nowMs) {
5134
+ if (unified.overageStatus === "allowed") {
5135
+ return { candidate: { ...base, class: "pay-per-use", reasons: [`plan exhausted until ${new Date(blockedUntil).toISOString()}, continues as extra usage`, ...windowReasons, ...planReasons] } };
5191
5136
  }
5192
- installedSource = config.source;
5193
- return true;
5194
- };
5195
- const socket = prepareHeadroomSocket(fs12, ports.socketTrust, paths, ports.ownPid);
5196
- if (socket.refused !== void 0) {
5197
- return fail(socket.refused);
5137
+ return { candidate: { ...base, class: "ineligible", blockedUntilMs: blockedUntil, reasons: [`refused until ${new Date(blockedUntil).toISOString()} (in ${formatAge(blockedUntil - nowMs)})`, ...windowReasons, ...planReasons] } };
5198
5138
  }
5199
- const { socketPath } = socket;
5200
- pruneStaleHeadroomSockets(fs12, paths, ports.isRunning);
5201
- if (!ensureInstalled()) {
5202
- return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
5139
+ if (plan.kind === "pay-per-use" || fiveHourWindow === void 0 && sevenDayWindow === void 0) {
5140
+ return { candidate: { ...base, class: "pay-per-use", reasons: [...plan.kind === "pay-per-use" ? planReasons : ["no plan windows reported, so usage bills as extra usage"]] } };
5203
5141
  }
5204
- const version = ports.headroomVersion() ?? "unknown";
5205
- writeOwnState({ supervisorPid: ports.ownPid, version, installedSource });
5206
- ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: managing headroom on allowlist [${allowlistOf(ports).join(", ")}]`);
5207
- const orphanPid = previousState?.headroomPid;
5208
- if (orphanPid !== void 0 && ports.isRunning(orphanPid)) {
5209
- ports.log(`agent-shim headroom supervisor: stopping daemon pid ${String(orphanPid)} left behind by the previous supervisor`);
5210
- ports.stopProcess(orphanPid);
5142
+ const basis = seven?.utilization !== void 0 ? { window: seven, windowMs: SEVEN_DAY_WINDOW_MS } : five?.utilization !== void 0 ? { window: five, windowMs: FIVE_HOUR_WINDOW_MS } : void 0;
5143
+ if (basis?.window.utilization === void 0) {
5144
+ return { candidate: { ...base, class: "unknown", reasons: ["quota windows reported no utilisation", ...windowReasons, ...planReasons] } };
5211
5145
  }
5212
- let headroomPid;
5213
- let runningHash;
5214
- let runningSettingsHash;
5215
- let consecutiveFailures = 0;
5216
- let idleSince;
5217
- let superseded = false;
5218
- let ticks = 0;
5219
- for (; ; ) {
5220
- if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
5221
- return HEADROOM_SUPERVISOR_STILL_RUNNING;
5146
+ const remaining = Math.max(0, 1 - basis.window.utilization);
5147
+ const untilResetMs = basis.window.resetsAtMs === void 0 ? basis.windowMs : Math.max(basis.window.resetsAtMs - nowMs, MS_PER_SECOND4);
5148
+ const score = remaining * plan.capacity / (untilResetMs / MS_PER_HOUR);
5149
+ const fiveRemaining = five?.utilization === void 0 ? void 0 : Math.max(0, 1 - five.utilization);
5150
+ const fiveUntil = five?.resetsAtMs === void 0 ? FIVE_HOUR_WINDOW_MS : Math.max(five.resetsAtMs - nowMs, MS_PER_SECOND4);
5151
+ const burn = fiveHourBurn(member.records);
5152
+ return {
5153
+ candidate: { ...base, class: "scored", score, reasons: [...windowReasons.map((reason, index) => index === windowReasons.length - 1 ? `${reason}${observedAgo}` : reason), ...planReasons] },
5154
+ ...fiveRemaining === void 0 ? {} : { fiveHour: { remaining: fiveRemaining, untilResetMs: fiveUntil } },
5155
+ ...burn === void 0 ? {} : { burn }
5156
+ };
5157
+ }
5158
+ function rankPool(input) {
5159
+ const { members, nowMs } = input;
5160
+ const assessments = members.map((member) => assess(member, nowMs));
5161
+ const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
5162
+ const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
5163
+ const candidates = assessments.map((assessment) => {
5164
+ const { candidate, fiveHour, burn } = assessment;
5165
+ const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
5166
+ if (candidate.class !== "scored" || fiveHour === void 0 || pace === void 0) {
5167
+ return { ...candidate, feasible: true };
5222
5168
  }
5223
- ticks += 1;
5224
- config = ports.readConfig();
5225
- const settingsHash = hashSettings(config.settings);
5226
- const allowlist = allowlistOf(ports);
5227
- const allowlistHash = hashAllowlist(allowlist);
5228
- pruneDeadSessions(fs12, paths.headroomSessionsDir, ports.isRunning);
5229
- const ownsNow = ownsState();
5230
- if (!ownsNow && !superseded) {
5231
- ports.log("agent-shim headroom supervisor: another supervisor now owns the state file; serving this daemon's own sessions until they end");
5169
+ const runsDryInMs = fiveHour.remaining / pace;
5170
+ if (runsDryInMs >= fiveHour.untilResetMs) {
5171
+ return { ...candidate, feasible: true };
5232
5172
  }
5233
- superseded = !ownsNow;
5234
- const crashed = headroomPid !== void 0 && !ports.isRunning(headroomPid);
5235
- if (headroomPid === void 0 || crashed) {
5236
- if (crashed) {
5237
- ports.log(`agent-shim headroom supervisor: headroom pid ${String(headroomPid)} died`);
5238
- headroomPid = void 0;
5239
- runningHash = void 0;
5240
- runningSettingsHash = void 0;
5241
- writeOwnState({
5242
- supervisorPid: ports.ownPid,
5243
- version,
5244
- installedSource
5245
- });
5246
- }
5247
- if (consecutiveFailures >= HEADROOM_START_RETRY_BUDGET) {
5248
- return fail(`headroom failed to become ready ${String(consecutiveFailures)} times in a row; giving up`);
5249
- }
5250
- if (installedSource !== config.source && !ensureInstalled()) {
5251
- return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
5252
- }
5253
- const pid = ports.spawnHeadroom(socketPath, allowlist, config.settings);
5254
- const ready = await waitUntilReady(ports, socketPath);
5255
- if (ready) {
5256
- consecutiveFailures = 0;
5257
- headroomPid = pid;
5258
- runningHash = allowlistHash;
5259
- runningSettingsHash = settingsHash;
5260
- const recorded = writeOwnState({
5261
- supervisorPid: ports.ownPid,
5262
- headroomPid: pid,
5263
- socketPath,
5264
- version,
5265
- allowlistHash,
5266
- settingsHash,
5267
- installedSource: config.source
5268
- });
5269
- if (recorded) {
5270
- fs12.removeRecursive(paths.headroomLockFile);
5271
- }
5272
- idleSince = void 0;
5273
- ports.log(`agent-shim headroom supervisor: headroom pid ${String(pid)} ready on unix socket ${socketPath}`);
5274
- } else {
5275
- ports.stopProcess(pid);
5276
- consecutiveFailures += 1;
5277
- ports.log(
5278
- `agent-shim headroom supervisor: headroom did not become ready on unix socket ${socketPath} (attempt ${String(consecutiveFailures)} of ${String(HEADROOM_START_RETRY_BUDGET)})`
5279
- );
5280
- await ports.sleep(backoffForAttempt(consecutiveFailures));
5281
- continue;
5282
- }
5283
- } else {
5284
- if (runningHash !== allowlistHash || runningSettingsHash !== settingsHash || installedSource !== config.source) {
5285
- if (sessionsForSupervisor(fs12, paths.headroomSessionsDir, ports.ownPid).length === 0) {
5286
- ports.log("agent-shim headroom supervisor: configuration drifted and no sessions are live; restarting headroom");
5287
- ports.stopProcess(headroomPid);
5288
- headroomPid = void 0;
5289
- runningHash = void 0;
5290
- runningSettingsHash = void 0;
5291
- continue;
5292
- }
5293
- }
5294
- const sessions = sessionsForSupervisor(fs12, paths.headroomSessionsDir, ports.ownPid);
5295
- if (sessions.length === 0) {
5296
- idleSince ??= ports.now();
5297
- const idleMs = ports.now() - idleSince;
5298
- if (idleMs >= config.idleShutdownMinutes * MS_PER_MINUTE2) {
5299
- ports.log(
5300
- `agent-shim headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and exiting`
5301
- );
5302
- ports.stopProcess(headroomPid);
5303
- writeOwnState({ version });
5304
- return 0;
5305
- }
5306
- } else {
5307
- idleSince = void 0;
5308
- }
5309
- }
5310
- await ports.sleep(HEADROOM_POLL_MS);
5311
- }
5312
- }
5313
- function currentHeadroomAllowlist(fs12, paths) {
5314
- const providers = readAllProviders(fs12, paths.providersDir).map((entry) => entry.provider);
5315
- return headroomAllowlist(headroomUpstreams(providers, frontDoorOrigin(readFrontDoorState(fs12, paths.frontdoorStateFile))));
5316
- }
5317
- function allowlistOf(ports) {
5318
- return currentHeadroomAllowlist(ports.fs, ports.paths);
5319
- }
5320
- async function waitUntilReady(ports, socketPath) {
5321
- const deadline = ports.now() + HEADROOM_READY_TIMEOUT_MS;
5322
- for (; ; ) {
5323
- if (await ports.ready(socketPath)) {
5324
- return true;
5325
- }
5326
- if (ports.now() >= deadline) {
5327
- return false;
5328
- }
5329
- await ports.sleep(HEADROOM_POLL_MS);
5330
- }
5331
- }
5332
-
5333
- // src/launcher/guard.ts
5334
- var AMBIENT_CREDENTIAL_VARS = [
5335
- "ANTHROPIC_API_KEY",
5336
- "ANTHROPIC_AUTH_TOKEN",
5337
- "CLAUDE_CODE_OAUTH_TOKEN",
5338
- "CLAUDE_CODE_USE_BEDROCK",
5339
- "CLAUDE_CODE_USE_VERTEX",
5340
- "CLAUDE_CODE_USE_FOUNDRY"
5341
- ];
5342
- function detectAmbientCredential(env, injected) {
5343
- for (const variable of AMBIENT_CREDENTIAL_VARS) {
5344
- const value = env[variable];
5345
- if (value === void 0 || value === "") {
5346
- continue;
5347
- }
5348
- if (injected?.variable === variable && value === injected.token) {
5349
- continue;
5173
+ return { ...candidate, feasible: false, reasons: [`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`, ...candidate.reasons] };
5174
+ });
5175
+ const ordered = [...candidates].sort(
5176
+ (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
5177
+ );
5178
+ const sticky = input.sticky;
5179
+ const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
5180
+ const stickyCandidate = ordered[stickyIndex];
5181
+ if (sticky !== void 0 && stickyCandidate?.class === "scored" && stickyCandidate.feasible) {
5182
+ const ageMs = nowMs - Date.parse(sticky.at);
5183
+ const warm = ageMs <= PROMPT_CACHE_TTL_MS;
5184
+ if (input.resuming || warm) {
5185
+ const why = warm ? `used here ${formatAge(ageMs)} ago, prompt cache still warm` : "resuming a conversation started on this account";
5186
+ ordered.splice(stickyIndex, 1);
5187
+ ordered.unshift({ ...stickyCandidate, reasons: [why, ...stickyCandidate.reasons] });
5350
5188
  }
5351
- return { variable };
5352
- }
5353
- return void 0;
5354
- }
5355
- function formatAmbientCredentialGuardMessage(variable, identityName) {
5356
- const identitySetCommand = identityName === void 0 ? "agent-shim identity set <name> --allow-ambient-credential" : `agent-shim identity set ${identityName} --allow-ambient-credential`;
5357
- return [
5358
- `error: ${variable} is set in the environment. This identity's isolated`,
5359
- "credential would be bypassed \u2014 every identity authenticates as this same key",
5360
- "while it's set. Unset it, or if this is deliberate, opt in per-launch with",
5361
- "AGENT_SHIM_ALLOW_AMBIENT_CREDENTIAL=1, or persistently for this identity with",
5362
- `\`${identitySetCommand}\`.`
5363
- ].join("\n");
5364
- }
5365
- function evaluateAmbientCredentialGuard(params) {
5366
- const detected = detectAmbientCredential(params.env, params.injectedCredential);
5367
- if (detected === void 0) {
5368
- return { ok: true };
5369
- }
5370
- if (params.allowAmbientCredentialOverride || params.allowAmbientCredential || params.providerSelected === true) {
5371
- return { ok: true };
5372
5189
  }
5190
+ const pick = ordered.find((candidate) => candidate.class !== "ineligible");
5191
+ const returns = ordered.flatMap((candidate) => candidate.blockedUntilMs === void 0 ? [] : [{ identity: candidate.identity, atMs: candidate.blockedUntilMs }]);
5192
+ const earliestReturn = returns.length === 0 ? void 0 : returns.reduce((soonest, entry) => entry.atMs < soonest.atMs ? entry : soonest);
5373
5193
  return {
5374
- ok: false,
5375
- variable: detected.variable,
5376
- message: formatAmbientCredentialGuardMessage(detected.variable, params.identityName)
5194
+ candidates: ordered,
5195
+ ...pick === void 0 ? { ...earliestReturn === void 0 ? {} : { earliestReturn } } : { pick }
5377
5196
  };
5378
5197
  }
5379
5198
 
5380
- // src/paths.ts
5381
- import fs3 from "node:fs";
5382
- import os from "node:os";
5383
- import path12 from "node:path";
5384
- function resolveAgentShimHome(env = process.env, home = os.homedir(), isDirectory = isExistingDirectory) {
5385
- const fromEnv = env.AGENT_SHIM_HOME;
5386
- if (fromEnv !== void 0 && fromEnv !== "") {
5387
- return fromEnv;
5388
- }
5389
- const current = path12.join(home, HOME_DIRNAME);
5390
- const legacy = path12.join(home, LEGACY_HOME_DIRNAME);
5391
- return !isDirectory(current) && isDirectory(legacy) ? legacy : current;
5199
+ // src/usage/poolPick.ts
5200
+ import path11 from "node:path";
5201
+ import { z as z8 } from "zod";
5202
+
5203
+ // src/usage/store.ts
5204
+ var DAY_MS2 = 864e5;
5205
+ var DAYS_IN_WEEK = 7;
5206
+ var USAGE_RETENTION_MS = DAYS_IN_WEEK * DAY_MS2;
5207
+
5208
+ // src/usage/poolPick.ts
5209
+ var SCHEMA_VERSION = 1;
5210
+ var PickEntrySchema = z8.strictObject({ identity: z8.string().min(1), at: z8.iso.datetime() });
5211
+ var PicksFileSchema = z8.strictObject({
5212
+ schemaVersion: z8.literal(SCHEMA_VERSION),
5213
+ picks: z8.record(z8.string().min(1), PickEntrySchema)
5214
+ });
5215
+ function errorMessage(error) {
5216
+ return error instanceof Error ? error.message : String(error);
5392
5217
  }
5393
- function isExistingDirectory(candidate) {
5394
- return fs3.statSync(candidate, { throwIfNoEntry: false })?.isDirectory() ?? false;
5218
+ function loadPoolMembers(fs12, paths, identities, nowMs) {
5219
+ const windowStartMs = nowMs - FIVE_HOUR_WINDOW_MS;
5220
+ const log = readUsageLog(fs12, paths.usageLogDir, { sinceMs: windowStartMs });
5221
+ return identities.map((identity) => {
5222
+ const records = log.records.filter((record) => record.identity === identity && record.provider === ANTHROPIC_PROVIDER && Date.parse(record.at) >= windowStartMs);
5223
+ try {
5224
+ const snapshot = readUsageSnapshot(fs12, paths.usageSnapshotsDir, identity);
5225
+ const account = readAccountMetadata(fs12, paths.identitiesDir, identity) ?? snapshot?.account;
5226
+ return { identity, records, ...snapshot === void 0 ? {} : { snapshot }, ...account === void 0 ? {} : { account } };
5227
+ } catch (error) {
5228
+ if (error instanceof UsageSnapshotError || error instanceof AccountMetadataError) {
5229
+ return { identity, records, readError: errorMessage(error) };
5230
+ }
5231
+ throw error;
5232
+ }
5233
+ });
5395
5234
  }
5396
- function resolveClaudeHome() {
5397
- const fromEnv = process.env.AGENT_SHIM_CLAUDE_HOME;
5398
- if (fromEnv !== void 0 && fromEnv !== "") {
5399
- return fromEnv;
5235
+ function rankPoolFromStore(params) {
5236
+ return rankPool({
5237
+ members: loadPoolMembers(params.fs, params.paths, params.identities, params.nowMs),
5238
+ nowMs: params.nowMs,
5239
+ resuming: params.resuming,
5240
+ ...params.sticky === void 0 ? {} : { sticky: params.sticky }
5241
+ });
5242
+ }
5243
+ function readPicksFile(fs12, file) {
5244
+ const empty = { schemaVersion: SCHEMA_VERSION, picks: {} };
5245
+ const raw = fs12.readFileUtf8(file);
5246
+ if (raw === void 0) {
5247
+ return { file: empty };
5248
+ }
5249
+ let parsed;
5250
+ try {
5251
+ parsed = JSON.parse(raw);
5252
+ } catch {
5253
+ return { file: empty, problem: `${file} is not valid JSON` };
5400
5254
  }
5401
- return path12.join(os.homedir(), ".claude");
5255
+ const result = PicksFileSchema.safeParse(parsed);
5256
+ return result.success ? { file: result.data } : { file: empty, problem: `${file} is not a picks file this agent-shim can read` };
5402
5257
  }
5403
- function buildLayoutPaths(root) {
5404
- return {
5405
- root,
5406
- identitiesDir: path12.join(root, "identities"),
5407
- configProfilesDir: path12.join(root, "config-profiles"),
5408
- providersDir: path12.join(root, "providers"),
5409
- directoryRulesFile: path12.join(root, "directory-rules.json"),
5410
- activeIdentityFile: path12.join(root, "active-identity"),
5411
- globalConfigFile: path12.join(root, "config.json"),
5412
- categoriesLocalFile: path12.join(root, "categories.local.json"),
5413
- claudeShimFile: path12.join(root, "claude-shim.json"),
5414
- headroomDir: path12.join(root, "headroom"),
5415
- headroomStateFile: path12.join(root, "headroom", `state.v${String(HEADROOM_STATE_SCHEMA_VERSION)}.json`),
5416
- headroomSocketDir: path12.join(root, "headroom", "run"),
5417
- headroomLockFile: path12.join(root, "headroom", "start.lock"),
5418
- headroomSessionsDir: path12.join(root, "headroom", "sessions"),
5419
- logsDir: path12.join(root, "logs"),
5420
- headroomLogPath: path12.join(root, "logs", "headroom.log"),
5421
- frontdoorDir: path12.join(root, "frontdoor"),
5422
- frontdoorCaDir: path12.join(root, "frontdoor", "ca"),
5423
- frontdoorCaCertFile: path12.join(root, "frontdoor", "ca", "ca.pem"),
5424
- frontdoorCaKeyFile: path12.join(root, "frontdoor", "ca", "ca.key"),
5425
- frontdoorCaBundlesDir: path12.join(root, "frontdoor", "ca", "bundles"),
5426
- frontdoorStateFile: path12.join(root, "frontdoor", "state.json"),
5427
- frontdoorLockFile: path12.join(root, "frontdoor", "start.lock"),
5428
- frontdoorControlTokenFile: path12.join(root, "frontdoor", "control-token"),
5429
- frontdoorSessionsDir: path12.join(root, "frontdoor", "sessions"),
5430
- frontdoorLogPath: path12.join(root, "logs", "frontdoor.log"),
5431
- usageDir: path12.join(root, "usage"),
5432
- usageLogDir: path12.join(root, "usage", "log"),
5433
- usageSnapshotsDir: path12.join(root, "usage", "snapshots"),
5434
- usagePicksFile: path12.join(root, "usage", "picks.json")
5435
- };
5258
+ function readStickyPick(fs12, picksFile, directory) {
5259
+ const read = readPicksFile(fs12, picksFile);
5260
+ const entry = read.file.picks[directory];
5261
+ return { ...entry === void 0 ? {} : { sticky: entry }, ...read.problem === void 0 ? {} : { problem: read.problem } };
5436
5262
  }
5437
- function resolveLayoutPaths() {
5438
- return buildLayoutPaths(resolveAgentShimHome());
5263
+ function recordStickyPick(fs12, picksFile, directory, identity, nowMs) {
5264
+ const current = readPicksFile(fs12, picksFile).file.picks;
5265
+ const kept = Object.entries(current).filter(([, entry]) => nowMs - Date.parse(entry.at) <= USAGE_RETENTION_MS);
5266
+ const next = { schemaVersion: SCHEMA_VERSION, picks: { ...Object.fromEntries(kept), [directory]: { identity, at: new Date(nowMs).toISOString() } } };
5267
+ fs12.mkdirPrivate(path11.dirname(picksFile));
5268
+ fs12.writeFilePrivate(picksFile, `${JSON.stringify(next, null, 2)}
5269
+ `);
5439
5270
  }
5440
5271
 
5441
- // src/identityStore.ts
5442
- import fs5 from "node:fs";
5443
- import path15 from "node:path";
5444
-
5445
5272
  // src/launcher/identity.ts
5446
- import path13 from "node:path";
5273
+ import path12 from "node:path";
5447
5274
  function poolNameOf(selector) {
5448
5275
  return selector.startsWith(POOL_SELECTOR_PREFIX) ? selector.slice(POOL_SELECTOR_PREFIX.length) : void 0;
5449
5276
  }
@@ -5492,1445 +5319,1775 @@ function decideConfigProfile(params) {
5492
5319
  return { source: "none" };
5493
5320
  }
5494
5321
  function loadIdentity(identitiesDir, name, fs12) {
5495
- return loadConfigFile(path13.join(identitiesDir, name, "identity.json"), IdentitySchema, fs12.readConfigFile);
5322
+ return loadConfigFile(path12.join(identitiesDir, name, "identity.json"), IdentitySchema, fs12.readConfigFile);
5496
5323
  }
5497
5324
 
5498
- // src/configProfilesStore.ts
5499
- import fs4 from "node:fs";
5500
- import path14 from "node:path";
5501
- var ProfileNotFoundError = class extends CliError {
5502
- constructor(profileName) {
5503
- super(`No configuration profile named "${profileName}". Run \`agent-shim profile add ${profileName}\` first.`);
5504
- this.profileName = profileName;
5505
- this.name = "ProfileNotFoundError";
5506
- }
5507
- profileName;
5508
- };
5509
- var ProfileAlreadyExistsError = class extends CliError {
5510
- constructor(profileName) {
5511
- super(`A configuration profile named "${profileName}" already exists.`);
5512
- this.profileName = profileName;
5513
- this.name = "ProfileAlreadyExistsError";
5514
- }
5515
- profileName;
5516
- };
5517
- var InvalidCategoryNameError = class extends CliError {
5518
- constructor(categoryName) {
5519
- super(`"${categoryName}" is not a category a configuration profile may toggle (runtime, history, knowledge, settings).`);
5520
- this.categoryName = categoryName;
5521
- this.name = "InvalidCategoryNameError";
5522
- }
5523
- categoryName;
5524
- };
5525
- function profileJsonPath(paths, name) {
5526
- return path14.join(paths.configProfilesDir, `${name}.json`);
5325
+ // src/launcher/pool.ts
5326
+ var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
5327
+ var TERMINATOR = "--";
5328
+ var REASONS_LOGGED = 3;
5329
+ function splitMembers(pool, paths, fs12) {
5330
+ const exists = (name) => loadIdentity(paths.identitiesDir, name, fs12) !== void 0;
5331
+ return { present: pool.identities.filter(exists), missing: pool.identities.filter((name) => !exists(name)) };
5527
5332
  }
5528
- function profileExists(paths, name) {
5529
- return fs4.existsSync(profileJsonPath(paths, name));
5333
+ function isResuming(passthrough) {
5334
+ const end = passthrough.indexOf(TERMINATOR);
5335
+ return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
5530
5336
  }
5531
- function requireProfileExists(paths, name) {
5532
- if (!profileExists(paths, name)) {
5533
- throw new ProfileNotFoundError(name);
5337
+ function resolvePoolLaunch(params) {
5338
+ const { poolName, log } = params;
5339
+ const pool = params.pools?.[poolName];
5340
+ if (pool === void 0) {
5341
+ return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
5534
5342
  }
5535
- }
5536
- function readProfile(paths, name) {
5537
- return readJson(profileJsonPath(paths, name), ConfigProfileSchema);
5538
- }
5539
- function createProfile(paths, name, extendsList, description) {
5540
- if (profileExists(paths, name)) {
5541
- throw new ProfileAlreadyExistsError(name);
5343
+ const { present: identities, missing } = splitMembers(pool, params.paths, params.fs);
5344
+ for (const name of missing) {
5345
+ log.warn(`agent-shim: pool "${poolName}" names identity "${name}", which does not exist; skipping it`);
5542
5346
  }
5543
- const filePath = profileJsonPath(paths, name);
5544
- const parsed = ConfigProfileSchema.safeParse({
5545
- ...description === void 0 ? {} : { description },
5546
- ...extendsList !== void 0 && extendsList.length > 0 ? { extends: [...extendsList] } : {}
5547
- });
5548
- if (!parsed.success) {
5549
- throw new ConfigValidationError(filePath, parsed.error.issues);
5347
+ if (identities.length === 0) {
5348
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
5550
5349
  }
5551
- writeJsonAtomic(filePath, parsed.data);
5552
- return parsed.data;
5553
- }
5554
- function listProfiles(paths) {
5555
- if (!fs4.existsSync(paths.configProfilesDir)) {
5556
- return [];
5350
+ const resuming = isResuming(params.passthrough);
5351
+ const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
5352
+ if (stickyRead.problem !== void 0) {
5353
+ log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
5557
5354
  }
5558
- const names = fs4.readdirSync(paths.configProfilesDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).sort();
5559
- const result = [];
5560
- for (const name of names) {
5561
- const profile = readProfile(paths, name);
5562
- if (profile !== void 0) {
5563
- result.push({ name, profile });
5355
+ for (; ; ) {
5356
+ const nowMs = params.now();
5357
+ const ranking = rankPoolFromStore({ fs: params.usageFs, paths: params.paths, identities, nowMs, resuming, ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky } });
5358
+ const { pick, earliestReturn } = ranking;
5359
+ if (pick !== void 0) {
5360
+ recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
5361
+ return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
5564
5362
  }
5565
- }
5566
- return result;
5567
- }
5568
- function globalConfigPath(paths) {
5569
- return paths.globalConfigFile;
5570
- }
5571
- function readGlobalConfig(paths) {
5572
- return readJson(globalConfigPath(paths), GlobalConfigSchema);
5573
- }
5574
- function setGlobalDefaultProfile(paths, name) {
5575
- return applyPatch(
5576
- globalConfigPath(paths),
5577
- GlobalConfigSchema,
5578
- { defaultConfigProfile: name },
5579
- { defaults: {} }
5580
- );
5581
- }
5582
- function validateCategoryNames(patch) {
5583
- for (const key of Object.keys(patch)) {
5584
- if (!isOverridableCategory(key)) {
5585
- throw new InvalidCategoryNameError(key);
5363
+ if (earliestReturn === void 0) {
5364
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
5586
5365
  }
5366
+ const until = new Date(earliestReturn.atMs).toISOString();
5367
+ if (!params.wait) {
5368
+ return {
5369
+ ok: false,
5370
+ message: `agent-shim: every member of pool "${poolName}" is refused right now; the earliest, ${earliestReturn.identity}, returns at ${until} (in ${formatAge(earliestReturn.atMs - nowMs)}). Pass --wait to sleep until then.`
5371
+ };
5372
+ }
5373
+ log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
5374
+ params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
5587
5375
  }
5588
5376
  }
5589
- function setProfileCategories(paths, name, patch) {
5590
- requireProfileExists(paths, name);
5591
- const expandedPatch = expandAllCategoryKey(patch);
5592
- validateCategoryNames(expandedPatch);
5593
- const existing = readProfile(paths, name) ?? {};
5594
- const mergedCategories = { ...existing.categories, ...expandedPatch };
5595
- return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, { categories: mergedCategories });
5596
- }
5597
- function setProfileEntries(paths, name, patch) {
5598
- requireProfileExists(paths, name);
5599
- const existing = readProfile(paths, name) ?? {};
5600
- const mergedEntries = { ...existing.entries, ...patch };
5601
- return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, { entries: mergedEntries });
5602
- }
5603
- function setProfileLaunchFlags(paths, name, patch) {
5604
- requireProfileExists(paths, name);
5605
- const existing = readProfile(paths, name) ?? {};
5606
- const mergedLaunch = { ...existing.launch, ...patch };
5607
- return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, {
5608
- // Read as `unknown` so the check sees the `undefined` a cleared key holds, which LaunchFlags' optional properties hide from the value type.
5609
- launch: Object.values(mergedLaunch).every((value) => value === void 0) ? void 0 : mergedLaunch
5610
- });
5377
+
5378
+ // src/poolPickReport.ts
5379
+ function candidateView(candidate) {
5380
+ return {
5381
+ identity: candidate.identity,
5382
+ class: candidate.class,
5383
+ ...candidate.score === void 0 ? {} : { score: candidate.score },
5384
+ feasible: candidate.feasible,
5385
+ ...candidate.blockedUntilMs === void 0 ? {} : { blockedUntil: new Date(candidate.blockedUntilMs).toISOString() },
5386
+ plan: candidate.plan,
5387
+ reasons: [...candidate.reasons]
5388
+ };
5611
5389
  }
5612
- function setProfileMetadata(paths, name, patch) {
5613
- requireProfileExists(paths, name);
5614
- return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, {
5615
- ...patch.extends === void 0 ? {} : { extends: patch.extends.length === 0 ? void 0 : [...patch.extends] },
5616
- ...patch.description === void 0 ? {} : { description: patch.description === false ? void 0 : patch.description }
5390
+ function collectPoolPick(params) {
5391
+ const { present, missing } = splitMembers(params.pool, params.paths, params.fs);
5392
+ const sticky = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.directory);
5393
+ const ranking = rankPoolFromStore({
5394
+ fs: params.usageFs,
5395
+ paths: params.paths,
5396
+ identities: present,
5397
+ nowMs: params.nowMs,
5398
+ resuming: false,
5399
+ ...sticky.sticky === void 0 ? {} : { sticky: sticky.sticky }
5617
5400
  });
5618
- }
5619
- function removeProfile(paths, name) {
5620
- requireProfileExists(paths, name);
5621
- fs4.rmSync(profileJsonPath(paths, name));
5401
+ return {
5402
+ pool: params.poolName,
5403
+ directory: params.directory,
5404
+ ...ranking.pick === void 0 ? {} : { pick: ranking.pick.identity },
5405
+ candidates: ranking.candidates.map(candidateView),
5406
+ missing: [...missing],
5407
+ ...ranking.earliestReturn === void 0 ? {} : { earliestReturn: { identity: ranking.earliestReturn.identity, at: new Date(ranking.earliestReturn.atMs).toISOString() } },
5408
+ ...sticky.problem === void 0 ? {} : { stickyProblem: sticky.problem }
5409
+ };
5622
5410
  }
5623
5411
 
5624
- // src/poolStore.ts
5625
- var PoolNotFoundError = class extends CliError {
5626
- constructor(poolName) {
5627
- super(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
5628
- this.poolName = poolName;
5629
- this.name = "PoolNotFoundError";
5630
- }
5631
- poolName;
5632
- };
5633
- var PoolAlreadyExistsError = class extends CliError {
5634
- constructor(poolName) {
5635
- super(`A pool named "${poolName}" already exists. Use \`agent-shim pool set ${poolName}\` to change its members.`);
5636
- this.poolName = poolName;
5637
- this.name = "PoolAlreadyExistsError";
5638
- }
5639
- poolName;
5640
- };
5641
- var InvalidPoolNameError = class extends CliError {
5642
- constructor(attemptedName) {
5643
- super(`"${attemptedName}" is not a valid pool name: it must start with a letter or number and may then contain letters, numbers, dots, hyphens and underscores.`);
5644
- this.attemptedName = attemptedName;
5645
- this.name = "InvalidPoolNameError";
5646
- }
5647
- attemptedName;
5648
- };
5649
- function readPools(paths) {
5650
- return readGlobalConfig(paths)?.pools ?? {};
5651
- }
5652
- function requirePool(paths, name) {
5653
- const pool = readPools(paths)[name];
5654
- if (pool === void 0) {
5655
- throw new PoolNotFoundError(name);
5656
- }
5657
- return pool;
5658
- }
5659
- function writePools(paths, pools) {
5660
- applyPatch(paths.globalConfigFile, GlobalConfigSchema, { pools }, { defaults: {} });
5661
- }
5662
- function addPool(paths, name, identities) {
5663
- if (!PoolNameSchema.safeParse(name).success) {
5664
- throw new InvalidPoolNameError(name);
5665
- }
5666
- const pools = readPools(paths);
5667
- if (pools[name] !== void 0) {
5668
- throw new PoolAlreadyExistsError(name);
5669
- }
5670
- const pool = { identities: [...identities] };
5671
- writePools(paths, { ...pools, [name]: pool });
5672
- return pool;
5673
- }
5674
- function setPool(paths, name, identities) {
5675
- requirePool(paths, name);
5676
- const pool = { identities: [...identities] };
5677
- writePools(paths, { ...readPools(paths), [name]: pool });
5678
- return pool;
5679
- }
5680
- function removePool(paths, name) {
5681
- requirePool(paths, name);
5682
- const remaining = Object.fromEntries(Object.entries(readPools(paths)).filter(([existing]) => existing !== name));
5683
- writePools(paths, remaining);
5684
- }
5412
+ // src/usage/pickReportSchema.ts
5413
+ import { z as z9 } from "zod";
5414
+ var PoolPickCandidateViewSchema = z9.strictObject({
5415
+ identity: z9.string(),
5416
+ /** In pick order: `scored` has usable quota data, `unknown` has none, `pay-per-use` bills by use, `ineligible` is refused right now. */
5417
+ class: z9.enum(["scored", "unknown", "pay-per-use", "ineligible"]),
5418
+ /** Plan-size-weighted remaining quota per hour until its reset; present for `scored` only. */
5419
+ score: z9.number().optional(),
5420
+ /** False when the five-hour window would run dry, at the observed pace, before it resets. */
5421
+ feasible: z9.boolean(),
5422
+ /** When an `ineligible` member can be used again, as an ISO instant. */
5423
+ blockedUntil: z9.iso.datetime().optional(),
5424
+ plan: PlanClassSchema,
5425
+ /** The facts behind the class and score, in the order they matter. */
5426
+ reasons: z9.array(z9.string())
5427
+ });
5428
+ var PoolPickReportSchema = z9.strictObject({
5429
+ pool: z9.string(),
5430
+ directory: z9.string(),
5431
+ /** The member a launch would run as, absent when every member is refused. */
5432
+ pick: z9.string().optional(),
5433
+ candidates: z9.array(PoolPickCandidateViewSchema),
5434
+ /** Members that name an identity that does not exist; a launch skips them. */
5435
+ missing: z9.array(z9.string()),
5436
+ /** When nothing can be picked, the soonest any member returns. */
5437
+ earliestReturn: z9.strictObject({ identity: z9.string(), at: z9.iso.datetime() }).optional(),
5438
+ /** Why the last-pick record could not be read, when that is the case. */
5439
+ stickyProblem: z9.string().optional()
5440
+ });
5685
5441
 
5686
- // src/identityStore.ts
5687
- var IdentityNotFoundError = class extends CliError {
5688
- constructor(name) {
5689
- super(`No identity named "${name}". Run \`agent-shim identity add ${name}\` first.`);
5690
- this.name = name;
5691
- this.name = "IdentityNotFoundError";
5442
+ // src/headroom/ensure.ts
5443
+ var HEADROOM_START_TIMEOUT_MS = 12e4;
5444
+ var HEADROOM_LAUNCHER_POLL_MS = 250;
5445
+ var HeadroomStartError = class extends CliError {
5446
+ constructor(message) {
5447
+ super(message);
5448
+ this.name = "HeadroomStartError";
5692
5449
  }
5693
- name;
5694
5450
  };
5695
- var IdentityAlreadyExistsError = class extends CliError {
5696
- constructor(identityName) {
5697
- super(`An identity named "${identityName}" already exists.`);
5698
- this.identityName = identityName;
5699
- this.name = "IdentityAlreadyExistsError";
5451
+ function ensureHeadroom(params) {
5452
+ const { paths, ports } = params;
5453
+ const deadline = ports.now() + HEADROOM_START_TIMEOUT_MS;
5454
+ let spawnedSupervisor = false;
5455
+ let awaitingAllowlist = false;
5456
+ for (; ; ) {
5457
+ const state = readHeadroomState(ports.fs, paths.headroomStateFile);
5458
+ if (state?.supervisorPid !== void 0 && ports.isRunning(state.supervisorPid)) {
5459
+ const daemonUp = state.socketPath !== void 0 && state.headroomPid !== void 0 && ports.isRunning(state.headroomPid);
5460
+ if (daemonUp && state.socketPath !== void 0) {
5461
+ const required = params.requiredAllowlistHash?.();
5462
+ if (required === void 0 || state.allowlistHash === required) {
5463
+ writeSession(ports.fs, paths.headroomSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), supervisorPid: state.supervisorPid });
5464
+ return { socketPath: state.socketPath };
5465
+ }
5466
+ const holders = sessionsForSupervisor(ports.fs, paths.headroomSessionsDir, state.supervisorPid).filter((session) => ports.isRunning(session.pid));
5467
+ if (holders.length > 0) {
5468
+ throw new HeadroomStartError(
5469
+ `the running headroom daemon was started before this launch's front door address and would refuse every request this launch sends through it. It restarts once the launches using it end (pid ${holders.map((session) => String(session.pid)).join(", ")}); end them and launch again, or launch with --no-headroom.`
5470
+ );
5471
+ }
5472
+ awaitingAllowlist = true;
5473
+ }
5474
+ if (state.lastError !== void 0) {
5475
+ throw new HeadroomStartError(
5476
+ `the headroom daemon reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.headroomLogPath})`
5477
+ );
5478
+ }
5479
+ } else if (!spawnedSupervisor) {
5480
+ ports.fs.mkdirp(paths.headroomDir);
5481
+ const created = ports.fs.writeFileExclusive(
5482
+ paths.headroomLockFile,
5483
+ `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
5484
+ `
5485
+ );
5486
+ if (created) {
5487
+ ports.spawnSupervisor(paths);
5488
+ spawnedSupervisor = true;
5489
+ } else {
5490
+ const lock = readStartLock(ports.fs, paths.headroomLockFile);
5491
+ if (lock === void 0 || !ports.isRunning(lock.pid)) {
5492
+ ports.fs.removeRecursive(paths.headroomLockFile);
5493
+ continue;
5494
+ }
5495
+ }
5496
+ }
5497
+ if (ports.now() >= deadline) {
5498
+ const lastError = state?.lastError;
5499
+ throw new HeadroomStartError(
5500
+ `the headroom daemon did not become ready within ${String(HEADROOM_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (awaitingAllowlist ? " (the running daemon's allowlist does not admit this launch's front door, and it did not restart)" : "") + `. Daemon log: ${paths.headroomLogPath}`
5501
+ );
5502
+ }
5503
+ ports.sleep(HEADROOM_LAUNCHER_POLL_MS);
5700
5504
  }
5701
- identityName;
5702
- };
5703
- var InvalidIdentityNameError = class extends CliError {
5704
- constructor(attemptedName) {
5705
- super(
5706
- `"${attemptedName}" is not a valid identity name \u2014 identity names must start with a letter or number and may then contain letters, numbers, dots, hyphens, underscores, and at signs.`
5707
- );
5708
- this.attemptedName = attemptedName;
5709
- this.name = "InvalidIdentityNameError";
5505
+ }
5506
+
5507
+ // src/headroom/settings.ts
5508
+ import { createHash as createHash4 } from "node:crypto";
5509
+ function settingsOf(config) {
5510
+ return {
5511
+ ...config.mode === void 0 ? {} : { mode: config.mode },
5512
+ ...config.targetRatio === void 0 ? {} : { targetRatio: config.targetRatio },
5513
+ ...config.ccr === void 0 ? {} : { ccr: config.ccr },
5514
+ ...config.rolloutChannel === void 0 ? {} : { rolloutChannel: config.rolloutChannel },
5515
+ ...config.interceptToolResults === void 0 ? {} : { interceptToolResults: config.interceptToolResults },
5516
+ ...config.readMaturation === void 0 ? {} : { readMaturation: config.readMaturation }
5517
+ };
5518
+ }
5519
+ function settingsArgs(settings) {
5520
+ return [
5521
+ ...settings.mode === void 0 ? [] : ["--mode", settings.mode],
5522
+ ...settings.targetRatio === void 0 ? [] : ["--target-ratio", String(settings.targetRatio)],
5523
+ ...settings.ccr === "lossless" ? ["--lossless"] : [],
5524
+ ...settings.ccr === "none" ? ["--no-ccr"] : [],
5525
+ ...settings.interceptToolResults === true ? ["--intercept-tool-results"] : [],
5526
+ ...settings.readMaturation === true ? ["--read-maturation"] : []
5527
+ ];
5528
+ }
5529
+ function settingsEnv(settings) {
5530
+ return settings.rolloutChannel === void 0 ? {} : { HEADROOM_ROLLOUT_CHANNEL: settings.rolloutChannel };
5531
+ }
5532
+ function hashSettings(settings) {
5533
+ return createHash4("sha256").update(JSON.stringify([...settingsArgs(settings), JSON.stringify(settingsEnv(settings))])).digest("hex");
5534
+ }
5535
+
5536
+ // src/headroom/socket.ts
5537
+ import path13 from "node:path";
5538
+ var GROUP_OR_OTHER_BITS = 63;
5539
+ var OCTAL_RADIX = 8;
5540
+ var MODE_DIGITS = 4;
5541
+ var HEADROOM_SOCKET_SUFFIX = ".sock";
5542
+ var HEADROOM_SOCKET_NAME = /^(\d+)\.sock$/;
5543
+ var UNIX_SOCKET_PATH_MAX_BYTES = 103;
5544
+ var PLATFORMS_WITHOUT_UNIX_SOCKETS = ["win32"];
5545
+ function headroomSocketPath(paths, supervisorPid) {
5546
+ return path13.join(paths.headroomSocketDir, `${String(supervisorPid)}${HEADROOM_SOCKET_SUFFIX}`);
5547
+ }
5548
+ function platformRefusal(platform) {
5549
+ return PLATFORMS_WITHOUT_UNIX_SOCKETS.includes(platform) ? `headroom routing needs a unix domain socket, which headroom cannot serve on ${platform}; agent-shim never falls back to a TCP port for this hop` : void 0;
5550
+ }
5551
+ function ownerOnlyRefusal(label, target, stat, expected, uid) {
5552
+ if (stat === void 0) {
5553
+ return `the headroom ${label} ${target} does not exist`;
5710
5554
  }
5711
- attemptedName;
5712
- };
5713
- function identityJsonPath(paths, name) {
5714
- return path15.join(paths.identitiesDir, name, "identity.json");
5555
+ if (stat.kind === "symlink") {
5556
+ return `the headroom ${label} ${target} is a symlink; it must be the real ${expected === "dir" ? "directory" : "socket"}`;
5557
+ }
5558
+ if (stat.kind !== expected) {
5559
+ return `the headroom ${label} ${target} is not a ${expected === "dir" ? "directory" : "socket"}`;
5560
+ }
5561
+ if (stat.uid !== uid) {
5562
+ return `the headroom ${label} ${target} is owned by uid ${String(stat.uid)}, not by this user (uid ${String(uid)})`;
5563
+ }
5564
+ if ((stat.mode & GROUP_OR_OTHER_BITS) !== 0) {
5565
+ return `the headroom ${label} ${target} has mode ${stat.mode.toString(OCTAL_RADIX).padStart(MODE_DIGITS, "0")}, which lets other users reach it; it must be accessible to its owner only`;
5566
+ }
5567
+ return void 0;
5715
5568
  }
5716
- function identityExists(paths, name) {
5717
- return fs5.existsSync(identityJsonPath(paths, name));
5569
+ function prepareHeadroomSocket(fs12, ports, paths, supervisorPid) {
5570
+ const unsupported = platformRefusal(ports.platform);
5571
+ if (unsupported !== void 0) {
5572
+ return { refused: unsupported };
5573
+ }
5574
+ const socketPath = headroomSocketPath(paths, supervisorPid);
5575
+ const length = Buffer.byteLength(socketPath, "utf8");
5576
+ if (length > UNIX_SOCKET_PATH_MAX_BYTES) {
5577
+ return {
5578
+ refused: `the headroom socket path ${socketPath} is ${String(length)} bytes, longer than the ${String(UNIX_SOCKET_PATH_MAX_BYTES)} a unix socket path can hold; set AGENT_SHIM_HOME to a shorter directory`
5579
+ };
5580
+ }
5581
+ if (ports.lstat(paths.headroomSocketDir)?.kind === "symlink") {
5582
+ return { refused: `the headroom socket directory ${paths.headroomSocketDir} is a symlink; it must be the real directory` };
5583
+ }
5584
+ fs12.mkdirPrivate(paths.headroomSocketDir);
5585
+ const refusal = ownerOnlyRefusal("socket directory", paths.headroomSocketDir, ports.lstat(paths.headroomSocketDir), "dir", ports.currentUid());
5586
+ return refusal === void 0 ? { socketPath } : { refused: refusal };
5718
5587
  }
5719
- function readIdentity(paths, name) {
5720
- return readJson(identityJsonPath(paths, name), IdentitySchema);
5588
+ function pruneStaleHeadroomSockets(fs12, paths, isRunning) {
5589
+ const removed = [];
5590
+ for (const name of fs12.readdir(paths.headroomSocketDir)) {
5591
+ const pid = HEADROOM_SOCKET_NAME.exec(name)?.[1];
5592
+ if (pid === void 0 || isRunning(Number.parseInt(pid, 10))) {
5593
+ continue;
5594
+ }
5595
+ fs12.removeRecursive(path13.join(paths.headroomSocketDir, name));
5596
+ removed.push(name);
5597
+ }
5598
+ return removed;
5721
5599
  }
5722
- function addIdentity(paths, name) {
5723
- if (identityExists(paths, name)) {
5724
- throw new IdentityAlreadyExistsError(name);
5600
+
5601
+ // src/headroom/source.ts
5602
+ import { z as z10 } from "zod";
5603
+ var GIT_SPEC_RE = /git\+[^@\s]+(?:@([^\s#]+))?/;
5604
+ var COMMIT_SHA_RE = /^[0-9a-f]{40}$/i;
5605
+ var DirectUrlSchema = z10.object({ vcs_info: z10.object({ commit_id: z10.string() }) });
5606
+ function isMovingGitSource(spec) {
5607
+ const match = GIT_SPEC_RE.exec(spec);
5608
+ if (match === null) {
5609
+ return false;
5725
5610
  }
5726
- const parsed = IdentitySchema.safeParse({ name, allowAmbientCredential: false });
5727
- if (!parsed.success) {
5728
- throw new InvalidIdentityNameError(name);
5611
+ const ref = match[1];
5612
+ return ref === void 0 || !COMMIT_SHA_RE.test(ref);
5613
+ }
5614
+ function pinnedGitCommit(spec) {
5615
+ const ref = GIT_SPEC_RE.exec(spec)?.[1];
5616
+ return ref !== void 0 && COMMIT_SHA_RE.test(ref) ? ref.toLowerCase() : void 0;
5617
+ }
5618
+
5619
+ // src/headroom/supervisor.ts
5620
+ function resolveSupervisorConfig(configured) {
5621
+ return {
5622
+ source: configured.source ?? HEADROOM_DEFAULT_SOURCE,
5623
+ idleShutdownMinutes: configured.idleShutdownMinutes ?? HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES,
5624
+ settings: settingsOf(configured)
5625
+ };
5626
+ }
5627
+ var HEADROOM_BACKOFF_BASE_MS = 500;
5628
+ var HEADROOM_BACKOFF_CAP_MS = 3e4;
5629
+ var HEADROOM_START_RETRY_BUDGET = 5;
5630
+ var HEADROOM_READY_TIMEOUT_MS = 15e3;
5631
+ var HEADROOM_POLL_MS = 1e3;
5632
+ var MS_PER_MINUTE2 = 6e4;
5633
+ function backoffForAttempt(attempt) {
5634
+ return Math.min(HEADROOM_BACKOFF_CAP_MS, HEADROOM_BACKOFF_BASE_MS * 2 ** (attempt - 1));
5635
+ }
5636
+ function versionSatisfies(version, source) {
5637
+ if (version === void 0) {
5638
+ return false;
5639
+ }
5640
+ const specifier = /==|>=|<=|!=|>|<|~=/.exec(source);
5641
+ if (specifier === null) {
5642
+ return true;
5643
+ }
5644
+ const installed = parseVersion(version);
5645
+ const wanted = parseVersion(source.slice(specifier.index + specifier[0].length));
5646
+ if (installed === void 0 || wanted === void 0) {
5647
+ return true;
5648
+ }
5649
+ const cmp = compareVersions(installed, wanted);
5650
+ switch (specifier[0]) {
5651
+ case "==":
5652
+ return cmp === 0;
5653
+ case "!=":
5654
+ return cmp !== 0;
5655
+ case ">=":
5656
+ return cmp >= 0;
5657
+ case "<=":
5658
+ return cmp <= 0;
5659
+ case ">":
5660
+ return cmp > 0;
5661
+ case "<":
5662
+ return cmp < 0;
5663
+ case "~=":
5664
+ return compareVersions(installed.slice(0, Math.max(0, wanted.length - 1)), wanted.slice(0, Math.max(0, wanted.length - 1))) === 0 && cmp >= 0;
5665
+ default:
5666
+ return true;
5729
5667
  }
5730
- writeJsonAtomic(identityJsonPath(paths, name), parsed.data);
5731
- return parsed.data;
5732
5668
  }
5733
- function useIdentity(paths, name) {
5734
- const pool = poolNameOf(name);
5735
- if (pool !== void 0) {
5736
- requirePool(paths, pool);
5737
- } else if (!identityExists(paths, name)) {
5738
- throw new IdentityNotFoundError(name);
5669
+ function parseVersion(input) {
5670
+ const match = /(\d+)(\.\d+)*/.exec(input);
5671
+ if (match === null) {
5672
+ return void 0;
5673
+ }
5674
+ return match[0].split(".").map((part) => Number.parseInt(part, 10));
5675
+ }
5676
+ function compareVersions(a, b) {
5677
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
5678
+ const left = a[index] ?? 0;
5679
+ const right = b[index] ?? 0;
5680
+ if (left !== right) {
5681
+ return left - right;
5682
+ }
5683
+ }
5684
+ return 0;
5685
+ }
5686
+ var HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
5687
+ async function runSupervisor(initialConfig, ports, options = {}) {
5688
+ const { fs: fs12, paths } = ports;
5689
+ let config = initialConfig;
5690
+ const previousState = readHeadroomState(fs12, paths.headroomStateFile);
5691
+ if (previousState?.supervisorPid !== void 0 && previousState.supervisorPid !== ports.ownPid && ports.isRunning(previousState.supervisorPid)) {
5692
+ ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: supervisor ${String(previousState.supervisorPid)} already owns the daemon; exiting`);
5693
+ return 0;
5694
+ }
5695
+ let installedSource = previousState?.installedSource;
5696
+ const ownsState = () => {
5697
+ const owner = readHeadroomState(fs12, paths.headroomStateFile)?.supervisorPid;
5698
+ return owner === void 0 || owner === ports.ownPid || !ports.isRunning(owner);
5699
+ };
5700
+ const writeOwnState = (state) => {
5701
+ if (!ownsState()) {
5702
+ return false;
5703
+ }
5704
+ writeHeadroomState(fs12, paths.headroomStateFile, state);
5705
+ return true;
5706
+ };
5707
+ const fail = (message) => {
5708
+ if (writeOwnState({ lastError: message })) {
5709
+ fs12.removeRecursive(paths.headroomLockFile);
5710
+ }
5711
+ ports.log(`agent-shim headroom supervisor: ${message}`);
5712
+ return 1;
5713
+ };
5714
+ const ensureInstalled = () => {
5715
+ const version2 = ports.headroomVersion();
5716
+ const versionOk = version2 !== void 0 && versionSatisfies(version2, config.source);
5717
+ const pinned = pinnedGitCommit(config.source);
5718
+ const commitOk = pinned === void 0 || ports.installedCommit() === pinned;
5719
+ if (versionOk && commitOk && (installedSource === void 0 || installedSource === config.source)) {
5720
+ installedSource = config.source;
5721
+ return true;
5722
+ }
5723
+ ports.log(`agent-shim headroom supervisor: installing headroom from ${config.source}`);
5724
+ const result = ports.install(config.source);
5725
+ if (!result.ok) {
5726
+ return false;
5727
+ }
5728
+ installedSource = config.source;
5729
+ return true;
5730
+ };
5731
+ const socket = prepareHeadroomSocket(fs12, ports.socketTrust, paths, ports.ownPid);
5732
+ if (socket.refused !== void 0) {
5733
+ return fail(socket.refused);
5734
+ }
5735
+ const { socketPath } = socket;
5736
+ pruneStaleHeadroomSockets(fs12, paths, ports.isRunning);
5737
+ if (!ensureInstalled()) {
5738
+ return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
5739
+ }
5740
+ const version = ports.headroomVersion() ?? "unknown";
5741
+ writeOwnState({ supervisorPid: ports.ownPid, version, installedSource });
5742
+ ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: managing headroom on allowlist [${allowlistOf(ports).join(", ")}]`);
5743
+ const orphanPid = previousState?.headroomPid;
5744
+ if (orphanPid !== void 0 && ports.isRunning(orphanPid)) {
5745
+ ports.log(`agent-shim headroom supervisor: stopping daemon pid ${String(orphanPid)} left behind by the previous supervisor`);
5746
+ ports.stopProcess(orphanPid);
5747
+ }
5748
+ let headroomPid;
5749
+ let runningHash;
5750
+ let runningSettingsHash;
5751
+ let consecutiveFailures = 0;
5752
+ let idleSince;
5753
+ let superseded = false;
5754
+ let ticks = 0;
5755
+ for (; ; ) {
5756
+ if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
5757
+ return HEADROOM_SUPERVISOR_STILL_RUNNING;
5758
+ }
5759
+ ticks += 1;
5760
+ config = ports.readConfig();
5761
+ const settingsHash = hashSettings(config.settings);
5762
+ const allowlist = allowlistOf(ports);
5763
+ const allowlistHash = hashAllowlist(allowlist);
5764
+ pruneDeadSessions(fs12, paths.headroomSessionsDir, ports.isRunning);
5765
+ const ownsNow = ownsState();
5766
+ if (!ownsNow && !superseded) {
5767
+ ports.log("agent-shim headroom supervisor: another supervisor now owns the state file; serving this daemon's own sessions until they end");
5768
+ }
5769
+ superseded = !ownsNow;
5770
+ const crashed = headroomPid !== void 0 && !ports.isRunning(headroomPid);
5771
+ if (headroomPid === void 0 || crashed) {
5772
+ if (crashed) {
5773
+ ports.log(`agent-shim headroom supervisor: headroom pid ${String(headroomPid)} died`);
5774
+ headroomPid = void 0;
5775
+ runningHash = void 0;
5776
+ runningSettingsHash = void 0;
5777
+ writeOwnState({
5778
+ supervisorPid: ports.ownPid,
5779
+ version,
5780
+ installedSource
5781
+ });
5782
+ }
5783
+ if (consecutiveFailures >= HEADROOM_START_RETRY_BUDGET) {
5784
+ return fail(`headroom failed to become ready ${String(consecutiveFailures)} times in a row; giving up`);
5785
+ }
5786
+ if (installedSource !== config.source && !ensureInstalled()) {
5787
+ return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
5788
+ }
5789
+ const pid = ports.spawnHeadroom(socketPath, allowlist, config.settings);
5790
+ const ready = await waitUntilReady(ports, socketPath);
5791
+ if (ready) {
5792
+ consecutiveFailures = 0;
5793
+ headroomPid = pid;
5794
+ runningHash = allowlistHash;
5795
+ runningSettingsHash = settingsHash;
5796
+ const recorded = writeOwnState({
5797
+ supervisorPid: ports.ownPid,
5798
+ headroomPid: pid,
5799
+ socketPath,
5800
+ version,
5801
+ allowlistHash,
5802
+ settingsHash,
5803
+ installedSource: config.source
5804
+ });
5805
+ if (recorded) {
5806
+ fs12.removeRecursive(paths.headroomLockFile);
5807
+ }
5808
+ idleSince = void 0;
5809
+ ports.log(`agent-shim headroom supervisor: headroom pid ${String(pid)} ready on unix socket ${socketPath}`);
5810
+ } else {
5811
+ ports.stopProcess(pid);
5812
+ consecutiveFailures += 1;
5813
+ ports.log(
5814
+ `agent-shim headroom supervisor: headroom did not become ready on unix socket ${socketPath} (attempt ${String(consecutiveFailures)} of ${String(HEADROOM_START_RETRY_BUDGET)})`
5815
+ );
5816
+ await ports.sleep(backoffForAttempt(consecutiveFailures));
5817
+ continue;
5818
+ }
5819
+ } else {
5820
+ if (runningHash !== allowlistHash || runningSettingsHash !== settingsHash || installedSource !== config.source) {
5821
+ if (sessionsForSupervisor(fs12, paths.headroomSessionsDir, ports.ownPid).length === 0) {
5822
+ ports.log("agent-shim headroom supervisor: configuration drifted and no sessions are live; restarting headroom");
5823
+ ports.stopProcess(headroomPid);
5824
+ headroomPid = void 0;
5825
+ runningHash = void 0;
5826
+ runningSettingsHash = void 0;
5827
+ continue;
5828
+ }
5829
+ }
5830
+ const sessions = sessionsForSupervisor(fs12, paths.headroomSessionsDir, ports.ownPid);
5831
+ if (sessions.length === 0) {
5832
+ idleSince ??= ports.now();
5833
+ const idleMs = ports.now() - idleSince;
5834
+ if (idleMs >= config.idleShutdownMinutes * MS_PER_MINUTE2) {
5835
+ ports.log(
5836
+ `agent-shim headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and exiting`
5837
+ );
5838
+ ports.stopProcess(headroomPid);
5839
+ writeOwnState({ version });
5840
+ return 0;
5841
+ }
5842
+ } else {
5843
+ idleSince = void 0;
5844
+ }
5845
+ }
5846
+ await ports.sleep(HEADROOM_POLL_MS);
5739
5847
  }
5740
- writeTextAtomic(paths.activeIdentityFile, `${name}
5741
- `);
5742
5848
  }
5743
- function readActiveIdentity(paths) {
5744
- if (!fs5.existsSync(paths.activeIdentityFile)) {
5745
- return void 0;
5746
- }
5747
- const raw = fs5.readFileSync(paths.activeIdentityFile, "utf8").trim();
5748
- return raw === "" ? void 0 : raw;
5849
+ function currentHeadroomAllowlist(fs12, paths) {
5850
+ const providers = readAllProviders(fs12, paths.providersDir).map((entry) => entry.provider);
5851
+ return headroomAllowlist(headroomUpstreams(providers, frontDoorOrigin(readFrontDoorState(fs12, paths.frontdoorStateFile))));
5749
5852
  }
5750
- function isIdentityDirectoryName(name) {
5751
- return !name.startsWith(".");
5853
+ function allowlistOf(ports) {
5854
+ return currentHeadroomAllowlist(ports.fs, ports.paths);
5752
5855
  }
5753
- function readIdentityForListing(paths, name) {
5754
- try {
5755
- return readIdentity(paths, name);
5756
- } catch (error) {
5757
- if (error instanceof ConfigValidationError || error instanceof SyntaxError) {
5758
- return { problem: error.message.replace(/\s*\n\s*/g, " ") };
5856
+ async function waitUntilReady(ports, socketPath) {
5857
+ const deadline = ports.now() + HEADROOM_READY_TIMEOUT_MS;
5858
+ for (; ; ) {
5859
+ if (await ports.ready(socketPath)) {
5860
+ return true;
5759
5861
  }
5760
- throw error;
5862
+ if (ports.now() >= deadline) {
5863
+ return false;
5864
+ }
5865
+ await ports.sleep(HEADROOM_POLL_MS);
5761
5866
  }
5762
5867
  }
5763
- function listIdentities(paths) {
5764
- if (!fs5.existsSync(paths.identitiesDir)) {
5765
- return [];
5766
- }
5767
- const active = readActiveIdentity(paths);
5768
- const names = fs5.readdirSync(paths.identitiesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isIdentityDirectoryName(entry.name)).map((entry) => entry.name).sort();
5769
- const result = [];
5770
- for (const name of names) {
5771
- const read = readIdentityForListing(paths, name);
5772
- if (read === void 0) {
5868
+
5869
+ // src/launcher/guard.ts
5870
+ var AMBIENT_CREDENTIAL_VARS = [
5871
+ "ANTHROPIC_API_KEY",
5872
+ "ANTHROPIC_AUTH_TOKEN",
5873
+ "CLAUDE_CODE_OAUTH_TOKEN",
5874
+ "CLAUDE_CODE_USE_BEDROCK",
5875
+ "CLAUDE_CODE_USE_VERTEX",
5876
+ "CLAUDE_CODE_USE_FOUNDRY"
5877
+ ];
5878
+ function detectAmbientCredential(env, injected) {
5879
+ for (const variable of AMBIENT_CREDENTIAL_VARS) {
5880
+ const value = env[variable];
5881
+ if (value === void 0 || value === "") {
5773
5882
  continue;
5774
5883
  }
5775
- const isActive = name === active;
5776
- result.push("problem" in read ? { name, isActive, problem: read.problem } : { name, identity: read, isActive });
5777
- }
5778
- return result;
5779
- }
5780
- function setDefaultConfigProfile(paths, identityName, profileName) {
5781
- if (!identityExists(paths, identityName)) {
5782
- throw new IdentityNotFoundError(identityName);
5884
+ if (injected?.variable === variable && value === injected.token) {
5885
+ continue;
5886
+ }
5887
+ return { variable };
5783
5888
  }
5784
- return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
5785
- defaultConfigProfile: profileName
5786
- });
5889
+ return void 0;
5787
5890
  }
5788
- function setAllowAmbientCredential(paths, identityName, allow) {
5789
- if (!identityExists(paths, identityName)) {
5790
- throw new IdentityNotFoundError(identityName);
5791
- }
5792
- return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
5793
- allowAmbientCredential: allow
5794
- });
5891
+ function formatAmbientCredentialGuardMessage(variable, identityName) {
5892
+ const identitySetCommand = identityName === void 0 ? "agent-shim identity set <name> --allow-ambient-credential" : `agent-shim identity set ${identityName} --allow-ambient-credential`;
5893
+ return [
5894
+ `error: ${variable} is set in the environment. This identity's isolated`,
5895
+ "credential would be bypassed \u2014 every identity authenticates as this same key",
5896
+ "while it's set. Unset it, or if this is deliberate, opt in per-launch with",
5897
+ "AGENT_SHIM_ALLOW_AMBIENT_CREDENTIAL=1, or persistently for this identity with",
5898
+ `\`${identitySetCommand}\`.`
5899
+ ].join("\n");
5795
5900
  }
5796
- function setIdentityCredential(paths, identityName, change) {
5797
- const existing = readIdentity(paths, identityName);
5798
- if (existing === void 0) {
5799
- throw new IdentityNotFoundError(identityName);
5800
- }
5801
- if (change === false) {
5802
- return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, { credential: void 0 });
5901
+ function evaluateAmbientCredentialGuard(params) {
5902
+ const detected = detectAmbientCredential(params.env, params.injectedCredential);
5903
+ if (detected === void 0) {
5904
+ return { ok: true };
5803
5905
  }
5804
- const sources = change.sources ?? existing.credential?.sources;
5805
- if (sources === void 0) {
5806
- throw new UsageError(`Identity "${identityName}" has no credential yet: pass --credential to give it one before choosing its target.`);
5906
+ if (params.allowAmbientCredentialOverride || params.allowAmbientCredential || params.providerSelected === true) {
5907
+ return { ok: true };
5807
5908
  }
5808
- const target = change.target ?? existing.credential?.target;
5809
- const cache = change.cache === void 0 ? existing.credential?.cache : change.cache === false ? void 0 : change.cache;
5810
- return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
5811
- credential: { sources: [...sources], ...target === void 0 ? {} : { target }, ...cache === void 0 ? {} : { cache } }
5812
- });
5813
- }
5814
- function belongsToIdentity(entry, name) {
5815
- return entry === name || entry === path15.basename(identityLockPath("", name)) || entry.startsWith(`.${name}.scratch.`) || entry.startsWith(`.${name}.previous.`);
5909
+ return {
5910
+ ok: false,
5911
+ variable: detected.variable,
5912
+ message: formatAmbientCredentialGuardMessage(detected.variable, params.identityName)
5913
+ };
5816
5914
  }
5817
- function removeIdentity(paths, name) {
5818
- if (!identityExists(paths, name)) {
5819
- throw new IdentityNotFoundError(name);
5820
- }
5821
- for (const entry of fs5.readdirSync(paths.identitiesDir)) {
5822
- if (belongsToIdentity(entry, name)) {
5823
- fs5.rmSync(path15.join(paths.identitiesDir, entry), { recursive: true, force: true });
5824
- }
5825
- }
5826
- if (readActiveIdentity(paths) === name) {
5827
- fs5.rmSync(paths.activeIdentityFile);
5915
+
5916
+ // src/paths.ts
5917
+ import fs3 from "node:fs";
5918
+ import os from "node:os";
5919
+ import path14 from "node:path";
5920
+ function resolveAgentShimHome(env = process.env, home = os.homedir(), isDirectory = isExistingDirectory) {
5921
+ const fromEnv = env.AGENT_SHIM_HOME;
5922
+ if (fromEnv !== void 0 && fromEnv !== "") {
5923
+ return fromEnv;
5828
5924
  }
5925
+ const current = path14.join(home, HOME_DIRNAME);
5926
+ const legacy = path14.join(home, LEGACY_HOME_DIRNAME);
5927
+ return !isDirectory(current) && isDirectory(legacy) ? legacy : current;
5829
5928
  }
5830
-
5831
- // src/providersStore.ts
5832
- import fs6 from "node:fs";
5833
- import path16 from "node:path";
5834
-
5835
- // src/credentialCache.ts
5836
- import { z as z7 } from "zod";
5837
- var MS_PER_UNIT = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
5838
- var CachedCredentialSchema = z7.strictObject({
5839
- token: z7.string().min(1),
5840
- fetchedAt: z7.number().int().nonnegative(),
5841
- source: CredentialSourceSchema
5842
- });
5843
- function effectiveStore(cache, platform) {
5844
- return cache.store ?? (platform === "darwin" ? "keychain" : "file");
5929
+ function isExistingDirectory(candidate) {
5930
+ return fs3.statSync(candidate, { throwIfNoEntry: false })?.isDirectory() ?? false;
5845
5931
  }
5846
- function ttlMs(cache) {
5847
- if (cache.ttl === void 0) {
5848
- return void 0;
5849
- }
5850
- const multiplier = MS_PER_UNIT[cache.ttl.slice(-1)];
5851
- if (multiplier === void 0) {
5852
- throw new Error(`credential cache ttl "${cache.ttl}" has no recognised unit`);
5932
+ function resolveClaudeHome() {
5933
+ const fromEnv = process.env.AGENT_SHIM_CLAUDE_HOME;
5934
+ if (fromEnv !== void 0 && fromEnv !== "") {
5935
+ return fromEnv;
5853
5936
  }
5854
- return Number(cache.ttl.slice(0, -1)) * multiplier;
5937
+ return path14.join(os.homedir(), ".claude");
5855
5938
  }
5856
- function isFresh(entry, cache, now) {
5857
- const limit = ttlMs(cache);
5858
- if (entry.fetchedAt > now) {
5859
- return false;
5860
- }
5861
- return limit === void 0 || now - entry.fetchedAt < limit;
5939
+ function buildLayoutPaths(root) {
5940
+ return {
5941
+ root,
5942
+ identitiesDir: path14.join(root, "identities"),
5943
+ configProfilesDir: path14.join(root, "config-profiles"),
5944
+ providersDir: path14.join(root, "providers"),
5945
+ directoryRulesFile: path14.join(root, "directory-rules.json"),
5946
+ activeIdentityFile: path14.join(root, "active-identity"),
5947
+ globalConfigFile: path14.join(root, "config.json"),
5948
+ categoriesLocalFile: path14.join(root, "categories.local.json"),
5949
+ claudeShimFile: path14.join(root, "claude-shim.json"),
5950
+ headroomDir: path14.join(root, "headroom"),
5951
+ headroomStateFile: path14.join(root, "headroom", `state.v${String(HEADROOM_STATE_SCHEMA_VERSION)}.json`),
5952
+ headroomSocketDir: path14.join(root, "headroom", "run"),
5953
+ headroomLockFile: path14.join(root, "headroom", "start.lock"),
5954
+ headroomSessionsDir: path14.join(root, "headroom", "sessions"),
5955
+ logsDir: path14.join(root, "logs"),
5956
+ headroomLogPath: path14.join(root, "logs", "headroom.log"),
5957
+ frontdoorDir: path14.join(root, "frontdoor"),
5958
+ frontdoorCaDir: path14.join(root, "frontdoor", "ca"),
5959
+ frontdoorCaCertFile: path14.join(root, "frontdoor", "ca", "ca.pem"),
5960
+ frontdoorCaKeyFile: path14.join(root, "frontdoor", "ca", "ca.key"),
5961
+ frontdoorCaBundlesDir: path14.join(root, "frontdoor", "ca", "bundles"),
5962
+ frontdoorStateFile: path14.join(root, "frontdoor", "state.json"),
5963
+ frontdoorLockFile: path14.join(root, "frontdoor", "start.lock"),
5964
+ frontdoorControlTokenFile: path14.join(root, "frontdoor", "control-token"),
5965
+ frontdoorSessionsDir: path14.join(root, "frontdoor", "sessions"),
5966
+ frontdoorLogPath: path14.join(root, "logs", "frontdoor.log"),
5967
+ usageDir: path14.join(root, "usage"),
5968
+ usageLogDir: path14.join(root, "usage", "log"),
5969
+ usageSnapshotsDir: path14.join(root, "usage", "snapshots"),
5970
+ usagePicksFile: path14.join(root, "usage", "picks.json")
5971
+ };
5972
+ }
5973
+ function resolveLayoutPaths() {
5974
+ return buildLayoutPaths(resolveAgentShimHome());
5862
5975
  }
5863
5976
 
5864
- // src/credential.ts
5865
- var CREDENTIAL_UNAVAILABLE_EXIT = 64;
5866
- var CREDENTIAL_COMMAND_TIMEOUT_MS = 1e4;
5867
- var CREDENTIAL_INTERACTIVE_TIMEOUT_MS = 12e4;
5868
- var GROUP_OTHER_PERMISSION_BITS = 63;
5869
- var PERMISSION_BITS = 511;
5870
- var OCTAL_RADIX2 = 8;
5871
- function commandArgv(source) {
5872
- if ("command" in source) {
5873
- return source.command;
5874
- }
5875
- if ("op" in source) {
5876
- return ["op", "read", source.op];
5977
+ // src/identityStore.ts
5978
+ import fs5 from "node:fs";
5979
+ import path16 from "node:path";
5980
+
5981
+ // src/configProfilesStore.ts
5982
+ import fs4 from "node:fs";
5983
+ import path15 from "node:path";
5984
+ var ProfileNotFoundError = class extends CliError {
5985
+ constructor(profileName) {
5986
+ super(`No configuration profile named "${profileName}". Run \`agent-shim profile add ${profileName}\` first.`);
5987
+ this.profileName = profileName;
5988
+ this.name = "ProfileNotFoundError";
5877
5989
  }
5878
- const account = source.keychain.account;
5879
- return ["security", "find-generic-password", "-s", source.keychain.service, ...account === void 0 ? [] : ["-a", account], "-w"];
5880
- }
5881
- function isInteractiveSource(source, env) {
5882
- if ("command" in source || "keychain" in source) {
5883
- return source.interactive ?? false;
5990
+ profileName;
5991
+ };
5992
+ var ProfileAlreadyExistsError = class extends CliError {
5993
+ constructor(profileName) {
5994
+ super(`A configuration profile named "${profileName}" already exists.`);
5995
+ this.profileName = profileName;
5996
+ this.name = "ProfileAlreadyExistsError";
5884
5997
  }
5885
- if ("op" in source) {
5886
- return source.interactive ?? (env.OP_SERVICE_ACCOUNT_TOKEN ?? "") === "";
5998
+ profileName;
5999
+ };
6000
+ var InvalidCategoryNameError = class extends CliError {
6001
+ constructor(categoryName) {
6002
+ super(`"${categoryName}" is not a category a configuration profile may toggle (runtime, history, knowledge, settings).`);
6003
+ this.categoryName = categoryName;
6004
+ this.name = "InvalidCategoryNameError";
5887
6005
  }
5888
- return false;
6006
+ categoryName;
6007
+ };
6008
+ function profileJsonPath(paths, name) {
6009
+ return path15.join(paths.configProfilesDir, `${name}.json`);
5889
6010
  }
5890
- function summariseSource(source) {
5891
- if ("env" in source) {
5892
- return { kind: "env", variable: source.env };
6011
+ function profileExists(paths, name) {
6012
+ return fs4.existsSync(profileJsonPath(paths, name));
6013
+ }
6014
+ function requireProfileExists(paths, name) {
6015
+ if (!profileExists(paths, name)) {
6016
+ throw new ProfileNotFoundError(name);
5893
6017
  }
5894
- if ("file" in source) {
5895
- return { kind: "file", path: source.file };
6018
+ }
6019
+ function readProfile(paths, name) {
6020
+ return readJson(profileJsonPath(paths, name), ConfigProfileSchema);
6021
+ }
6022
+ function createProfile(paths, name, extendsList, description) {
6023
+ if (profileExists(paths, name)) {
6024
+ throw new ProfileAlreadyExistsError(name);
5896
6025
  }
5897
- if ("command" in source) {
5898
- return { kind: "command", program: source.command[0] };
6026
+ const filePath = profileJsonPath(paths, name);
6027
+ const parsed = ConfigProfileSchema.safeParse({
6028
+ ...description === void 0 ? {} : { description },
6029
+ ...extendsList !== void 0 && extendsList.length > 0 ? { extends: [...extendsList] } : {}
6030
+ });
6031
+ if (!parsed.success) {
6032
+ throw new ConfigValidationError(filePath, parsed.error.issues);
5899
6033
  }
5900
- if ("op" in source) {
5901
- return { kind: "op", reference: source.op };
6034
+ writeJsonAtomic(filePath, parsed.data);
6035
+ return parsed.data;
6036
+ }
6037
+ function listProfiles(paths) {
6038
+ if (!fs4.existsSync(paths.configProfilesDir)) {
6039
+ return [];
5902
6040
  }
5903
- if ("keychain" in source) {
5904
- return {
5905
- kind: "keychain",
5906
- service: source.keychain.service,
5907
- ...source.keychain.account === void 0 ? {} : { account: source.keychain.account }
5908
- };
6041
+ const names = fs4.readdirSync(paths.configProfilesDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).sort();
6042
+ const result = [];
6043
+ for (const name of names) {
6044
+ const profile = readProfile(paths, name);
6045
+ if (profile !== void 0) {
6046
+ result.push({ name, profile });
6047
+ }
5909
6048
  }
5910
- return { kind: "literal" };
6049
+ return result;
5911
6050
  }
5912
- function summariseCredential(credential) {
5913
- return {
5914
- target: credential.target ?? "bearer",
5915
- sources: credential.sources.map(summariseSource),
5916
- ...credential.cache === void 0 ? {} : { cache: credential.cache }
5917
- };
6051
+ function globalConfigPath(paths) {
6052
+ return paths.globalConfigFile;
5918
6053
  }
5919
- function formatSourceSummary(summary) {
5920
- switch (summary.kind) {
5921
- case "env":
5922
- return `env ${summary.variable}`;
5923
- case "file":
5924
- return `file ${summary.path}`;
5925
- case "command":
5926
- return `command ${summary.program}`;
5927
- case "op":
5928
- return `op ${summary.reference}`;
5929
- case "keychain":
5930
- return `keychain ${summary.service}${summary.account === void 0 ? "" : ` (account ${summary.account})`}`;
5931
- case "literal":
5932
- return "literal (non-secret placeholder)";
5933
- default:
5934
- return summary;
6054
+ function readGlobalConfig(paths) {
6055
+ return readJson(globalConfigPath(paths), GlobalConfigSchema);
6056
+ }
6057
+ function setGlobalDefaultProfile(paths, name) {
6058
+ return applyPatch(
6059
+ globalConfigPath(paths),
6060
+ GlobalConfigSchema,
6061
+ { defaultConfigProfile: name },
6062
+ { defaults: {} }
6063
+ );
6064
+ }
6065
+ function validateCategoryNames(patch) {
6066
+ for (const key of Object.keys(patch)) {
6067
+ if (!isOverridableCategory(key)) {
6068
+ throw new InvalidCategoryNameError(key);
6069
+ }
5935
6070
  }
5936
6071
  }
5937
- function describeSource(source) {
5938
- return formatSourceSummary(summariseSource(source));
6072
+ function setProfileCategories(paths, name, patch) {
6073
+ requireProfileExists(paths, name);
6074
+ const expandedPatch = expandAllCategoryKey(patch);
6075
+ validateCategoryNames(expandedPatch);
6076
+ const existing = readProfile(paths, name) ?? {};
6077
+ const mergedCategories = { ...existing.categories, ...expandedPatch };
6078
+ return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, { categories: mergedCategories });
5939
6079
  }
5940
- function formatCredentialSummary(summary) {
5941
- const cache = summary.cache === void 0 ? "" : `, cached ${summary.cache.ttl === void 0 ? "with no expiry" : `for ${summary.cache.ttl}`}${summary.cache.store === void 0 ? "" : ` in the ${summary.cache.store} store`}`;
5942
- return `${summary.target} from ${summary.sources.map(formatSourceSummary).join(", then ")}${cache}`;
6080
+ function setProfileEntries(paths, name, patch) {
6081
+ requireProfileExists(paths, name);
6082
+ const existing = readProfile(paths, name) ?? {};
6083
+ const mergedEntries = { ...existing.entries, ...patch };
6084
+ return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, { entries: mergedEntries });
5943
6085
  }
5944
- function describeCredential(credential) {
5945
- return formatCredentialSummary(summariseCredential(credential));
6086
+ function setProfileLaunchFlags(paths, name, patch) {
6087
+ requireProfileExists(paths, name);
6088
+ const existing = readProfile(paths, name) ?? {};
6089
+ const mergedLaunch = { ...existing.launch, ...patch };
6090
+ return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, {
6091
+ // Read as `unknown` so the check sees the `undefined` a cleared key holds, which LaunchFlags' optional properties hide from the value type.
6092
+ launch: Object.values(mergedLaunch).every((value) => value === void 0) ? void 0 : mergedLaunch
6093
+ });
5946
6094
  }
5947
- function trySource(source, params) {
5948
- const none = (reason, warn = false) => ({ ok: false, reason, warn });
5949
- if ("env" in source) {
5950
- const value = params.env[source.env];
5951
- return value === void 0 || value === "" ? none("is unset or empty") : { ok: true, token: value };
5952
- }
5953
- if ("literal" in source) {
5954
- return { ok: true, token: source.literal };
5955
- }
5956
- if ("file" in source) {
5957
- const read = params.port.readSecretFile(source.file);
5958
- if (!read.found) {
5959
- return none("does not exist");
5960
- }
5961
- if (read.mode !== void 0 && (read.mode & GROUP_OTHER_PERMISSION_BITS) !== 0) {
5962
- return none(
5963
- `is readable or writable by group or others (mode ${(read.mode & PERMISSION_BITS).toString(OCTAL_RADIX2)}); run \`chmod 600 ${source.file}\``,
5964
- true
5965
- );
5966
- }
5967
- const token2 = read.content.trim();
5968
- return token2 === "" ? none("is empty") : { ok: true, token: token2 };
6095
+ function setProfileMetadata(paths, name, patch) {
6096
+ requireProfileExists(paths, name);
6097
+ return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, {
6098
+ ...patch.extends === void 0 ? {} : { extends: patch.extends.length === 0 ? void 0 : [...patch.extends] },
6099
+ ...patch.description === void 0 ? {} : { description: patch.description === false ? void 0 : patch.description }
6100
+ });
6101
+ }
6102
+ function removeProfile(paths, name) {
6103
+ requireProfileExists(paths, name);
6104
+ fs4.rmSync(profileJsonPath(paths, name));
6105
+ }
6106
+
6107
+ // src/poolStore.ts
6108
+ var PoolNotFoundError = class extends CliError {
6109
+ constructor(poolName) {
6110
+ super(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
6111
+ this.poolName = poolName;
6112
+ this.name = "PoolNotFoundError";
5969
6113
  }
5970
- const argv = commandArgv(source);
5971
- const interactive = isInteractiveSource(source, params.env);
5972
- if (interactive && !params.port.personPresent()) {
5973
- return none("needs a person to approve it, but there is no terminal or desktop session");
6114
+ poolName;
6115
+ };
6116
+ var PoolAlreadyExistsError = class extends CliError {
6117
+ constructor(poolName) {
6118
+ super(`A pool named "${poolName}" already exists. Use \`agent-shim pool set ${poolName}\` to change its members.`);
6119
+ this.poolName = poolName;
6120
+ this.name = "PoolAlreadyExistsError";
5974
6121
  }
5975
- const timeoutMs = source.timeoutMs ?? (interactive ? CREDENTIAL_INTERACTIVE_TIMEOUT_MS : CREDENTIAL_COMMAND_TIMEOUT_MS);
5976
- const result = params.port.runCommand(argv, { timeoutMs, interactive });
5977
- if (result.timedOut) {
5978
- return none(`timed out after ${String(timeoutMs)}ms`);
6122
+ poolName;
6123
+ };
6124
+ var InvalidPoolNameError = class extends CliError {
6125
+ constructor(attemptedName) {
6126
+ super(`"${attemptedName}" is not a valid pool name: it must start with a letter or number and may then contain letters, numbers, dots, hyphens and underscores.`);
6127
+ this.attemptedName = attemptedName;
6128
+ this.name = "InvalidPoolNameError";
5979
6129
  }
5980
- if (result.status !== 0) {
5981
- const stderr = result.stderr.trim();
5982
- const outcome = result.status === null ? "could not be run or was killed by a signal" : `exited with status ${String(result.status)}`;
5983
- return none(`${outcome}${stderr === "" ? "" : `: ${stderr}`}`);
6130
+ attemptedName;
6131
+ };
6132
+ function readPools(paths) {
6133
+ return readGlobalConfig(paths)?.pools ?? {};
6134
+ }
6135
+ function requirePool(paths, name) {
6136
+ const pool = readPools(paths)[name];
6137
+ if (pool === void 0) {
6138
+ throw new PoolNotFoundError(name);
5984
6139
  }
5985
- const token = result.stdout.trim();
5986
- return token === "" ? none("printed no token") : { ok: true, token };
6140
+ return pool;
5987
6141
  }
5988
- function warmCommandFor(subject) {
5989
- const [kind, name] = subject.split(" ");
5990
- return kind === "provider" ? `agent-shim credential warm --provider ${name ?? ""}` : `agent-shim credential warm ${name ?? ""}`;
6142
+ function writePools(paths, pools) {
6143
+ applyPatch(paths.globalConfigFile, GlobalConfigSchema, { pools }, { defaults: {} });
5991
6144
  }
5992
- function resolveCredential(params) {
5993
- const target = params.credential.target ?? "bearer";
5994
- const cacheBlock = params.credential.cache;
5995
- const cache = cacheBlock === void 0 ? void 0 : params.port.cache;
5996
- const store = cacheBlock === void 0 || cache === void 0 ? void 0 : effectiveStore(cacheBlock, cache.platform);
5997
- if (cacheBlock !== void 0 && cache !== void 0 && store !== void 0 && params.refresh !== true) {
5998
- const entry = cache.port.read(store, params.subject);
5999
- if (entry !== void 0 && isFresh(entry, cacheBlock, cache.now())) {
6000
- return { ok: true, credential: { target, token: entry.token, source: entry.source, warnings: [], cachedAt: entry.fetchedAt } };
6001
- }
6145
+ function addPool(paths, name, identities) {
6146
+ if (!PoolNameSchema.safeParse(name).success) {
6147
+ throw new InvalidPoolNameError(name);
6002
6148
  }
6003
- const attempts = [];
6004
- const warnings = [];
6005
- for (const source of params.credential.sources) {
6006
- const outcome = trySource(source, params);
6007
- if (outcome.ok) {
6008
- if (cache !== void 0 && store !== void 0) {
6009
- cache.port.write(store, params.subject, { token: outcome.token, fetchedAt: cache.now(), source });
6010
- }
6011
- return { ok: true, credential: { target, token: outcome.token, source, warnings } };
6012
- }
6013
- const line = `${describeSource(source)} ${outcome.reason}`;
6014
- attempts.push(line);
6015
- if (outcome.warn) {
6016
- warnings.push(`agent-shim: ${params.subject}: skipped ${line}`);
6017
- }
6149
+ const pools = readPools(paths);
6150
+ if (pools[name] !== void 0) {
6151
+ throw new PoolAlreadyExistsError(name);
6018
6152
  }
6019
- const hint = cacheBlock === void 0 ? "" : `; fill the cache from a terminal with \`${warmCommandFor(params.subject)}\``;
6020
- return { ok: false, message: `agent-shim: ${params.subject} has no usable credential: ${attempts.join("; ")}${hint}` };
6153
+ const pool = { identities: [...identities] };
6154
+ writePools(paths, { ...pools, [name]: pool });
6155
+ return pool;
6021
6156
  }
6022
- function credentialVariables(target, token) {
6023
- return {
6024
- ANTHROPIC_AUTH_TOKEN: target === "bearer" ? token : void 0,
6025
- ANTHROPIC_API_KEY: target === "apiKey" ? token : void 0,
6026
- CLAUDE_CODE_OAUTH_TOKEN: target === "oauthToken" ? token : void 0
6027
- };
6157
+ function setPool(paths, name, identities) {
6158
+ requirePool(paths, name);
6159
+ const pool = { identities: [...identities] };
6160
+ writePools(paths, { ...readPools(paths), [name]: pool });
6161
+ return pool;
6028
6162
  }
6029
-
6030
- // src/providersStore.ts
6031
- var ProviderNotFoundError = class extends CliError {
6163
+ function removePool(paths, name) {
6164
+ requirePool(paths, name);
6165
+ const remaining = Object.fromEntries(Object.entries(readPools(paths)).filter(([existing]) => existing !== name));
6166
+ writePools(paths, remaining);
6167
+ }
6168
+
6169
+ // src/identityStore.ts
6170
+ var IdentityNotFoundError = class extends CliError {
6032
6171
  constructor(name) {
6033
- super(`No provider named "${name}". Run \`agent-shim provider add ${name}\` first.`);
6172
+ super(`No identity named "${name}". Run \`agent-shim identity add ${name}\` first.`);
6034
6173
  this.name = name;
6035
- this.name = "ProviderNotFoundError";
6174
+ this.name = "IdentityNotFoundError";
6036
6175
  }
6037
6176
  name;
6038
6177
  };
6039
- var ProviderAlreadyExistsError = class extends CliError {
6040
- constructor(providerName) {
6041
- super(`A provider named "${providerName}" already exists.`);
6042
- this.providerName = providerName;
6043
- this.name = "ProviderAlreadyExistsError";
6178
+ var IdentityAlreadyExistsError = class extends CliError {
6179
+ constructor(identityName) {
6180
+ super(`An identity named "${identityName}" already exists.`);
6181
+ this.identityName = identityName;
6182
+ this.name = "IdentityAlreadyExistsError";
6044
6183
  }
6045
- providerName;
6184
+ identityName;
6046
6185
  };
6047
- var InvalidProviderNameError = class extends CliError {
6186
+ var InvalidIdentityNameError = class extends CliError {
6048
6187
  constructor(attemptedName) {
6049
6188
  super(
6050
- `"${attemptedName}" is not a valid provider name. Provider names must start with a letter or number and may then contain letters, numbers, dots, hyphens, underscores, and at signs.`
6189
+ `"${attemptedName}" is not a valid identity name \u2014 identity names must start with a letter or number and may then contain letters, numbers, dots, hyphens, underscores, and at signs.`
6051
6190
  );
6052
6191
  this.attemptedName = attemptedName;
6053
- this.name = "InvalidProviderNameError";
6192
+ this.name = "InvalidIdentityNameError";
6054
6193
  }
6055
6194
  attemptedName;
6056
6195
  };
6057
- var PROVIDER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/;
6058
- function providerJsonPath(paths, name) {
6059
- return path16.join(paths.providersDir, `${name}.json`);
6060
- }
6061
- function providerExists(paths, name) {
6062
- return fs6.existsSync(providerJsonPath(paths, name));
6196
+ function identityJsonPath(paths, name) {
6197
+ return path16.join(paths.identitiesDir, name, "identity.json");
6063
6198
  }
6064
- var LEGACY_PROVIDER_FIELDS = ["tokenEnv", "tokenCommand", "authScheme"];
6065
- var LEGACY_ENV_CREDENTIAL_KEYS = Object.values(CREDENTIAL_TARGET_VARS);
6066
- var LEGACY_LITERAL_PLACEHOLDER = "<the value of env.ANTHROPIC_AUTH_TOKEN>";
6067
- function isRecord3(value) {
6068
- return typeof value === "object" && value !== null && !Array.isArray(value);
6199
+ function identityExists(paths, name) {
6200
+ return fs5.existsSync(identityJsonPath(paths, name));
6069
6201
  }
6070
- function legacyProviderConversion(raw) {
6071
- if (!isRecord3(raw) || "credential" in raw) {
6072
- return void 0;
6073
- }
6074
- const env = isRecord3(raw.env) ? raw.env : void 0;
6075
- const legacyEnvKeys = env === void 0 ? [] : Object.keys(env).filter((key) => LEGACY_ENV_CREDENTIAL_KEYS.includes(key));
6076
- const fields = [...LEGACY_PROVIDER_FIELDS.filter((field) => field in raw), ...legacyEnvKeys.map((key) => `env.${key}`)];
6077
- if (fields.length === 0) {
6078
- return void 0;
6079
- }
6080
- const sources = [];
6081
- if (typeof raw.tokenEnv === "string") {
6082
- sources.push({ env: raw.tokenEnv });
6083
- }
6084
- if (Array.isArray(raw.tokenCommand)) {
6085
- sources.push({ command: raw.tokenCommand });
6086
- }
6087
- if (typeof env?.ANTHROPIC_AUTH_TOKEN === "string" && env.ANTHROPIC_AUTH_TOKEN !== "") {
6088
- sources.push({ literal: LEGACY_LITERAL_PLACEHOLDER });
6089
- }
6090
- const remainingEnv = env === void 0 ? void 0 : Object.fromEntries(Object.entries(env).filter(([key]) => !LEGACY_ENV_CREDENTIAL_KEYS.includes(key)));
6091
- const replacedKeys = [...LEGACY_PROVIDER_FIELDS, "env"];
6092
- const rest = Object.fromEntries(Object.entries(raw).filter(([key]) => !replacedKeys.includes(key)));
6093
- return {
6094
- fields,
6095
- replacement: {
6096
- ...rest,
6097
- credential: { sources, ...raw.authScheme === "apiKey" ? { target: "apiKey" } : {} },
6098
- ...remainingEnv === void 0 || Object.keys(remainingEnv).length === 0 ? {} : { env: remainingEnv }
6099
- }
6100
- };
6202
+ function readIdentity(paths, name) {
6203
+ return readJson(identityJsonPath(paths, name), IdentitySchema);
6101
6204
  }
6102
- var LegacyProviderFileError = class extends CliError {
6103
- constructor(filePath, conversion) {
6104
- super(
6105
- `${filePath} uses ${conversion.fields.join(", ")}, which a credential block replaced. Rewrite it as:
6106
- ` + JSON.stringify(conversion.replacement, null, 2)
6107
- );
6108
- this.filePath = filePath;
6109
- this.conversion = conversion;
6110
- this.name = "LegacyProviderFileError";
6111
- }
6112
- filePath;
6113
- conversion;
6114
- };
6115
- function parseProviderFile(filePath, raw) {
6116
- const conversion = legacyProviderConversion(raw);
6117
- if (conversion !== void 0) {
6118
- throw new LegacyProviderFileError(filePath, conversion);
6205
+ function addIdentity(paths, name) {
6206
+ if (identityExists(paths, name)) {
6207
+ throw new IdentityAlreadyExistsError(name);
6119
6208
  }
6120
- const parsed = ProviderSchema.safeParse(raw);
6209
+ const parsed = IdentitySchema.safeParse({ name, allowAmbientCredential: false });
6121
6210
  if (!parsed.success) {
6122
- throw new ConfigValidationError(filePath, parsed.error.issues);
6211
+ throw new InvalidIdentityNameError(name);
6123
6212
  }
6213
+ writeJsonAtomic(identityJsonPath(paths, name), parsed.data);
6124
6214
  return parsed.data;
6125
6215
  }
6126
- function readProvider(paths, name) {
6127
- const filePath = providerJsonPath(paths, name);
6128
- if (!fs6.existsSync(filePath)) {
6216
+ function useIdentity(paths, name) {
6217
+ const pool = poolNameOf(name);
6218
+ if (pool !== void 0) {
6219
+ requirePool(paths, pool);
6220
+ } else if (!identityExists(paths, name)) {
6221
+ throw new IdentityNotFoundError(name);
6222
+ }
6223
+ writeTextAtomic(paths.activeIdentityFile, `${name}
6224
+ `);
6225
+ }
6226
+ function readActiveIdentity(paths) {
6227
+ if (!fs5.existsSync(paths.activeIdentityFile)) {
6129
6228
  return void 0;
6130
6229
  }
6131
- const raw = JSON.parse(fs6.readFileSync(filePath, "utf8"));
6132
- return parseProviderFile(filePath, raw);
6230
+ const raw = fs5.readFileSync(paths.activeIdentityFile, "utf8").trim();
6231
+ return raw === "" ? void 0 : raw;
6133
6232
  }
6134
- function loadProvider(providersDir, name, port) {
6135
- const filePath = path16.join(providersDir, `${name}.json`);
6136
- const raw = port.readConfigFile(filePath);
6137
- return raw === void 0 ? void 0 : parseProviderFile(filePath, raw);
6233
+ function isIdentityDirectoryName(name) {
6234
+ return !name.startsWith(".");
6138
6235
  }
6139
- function listProviders(paths) {
6140
- const names = listProviderNames(
6141
- (dir) => fs6.readdirSync(dir),
6142
- paths.providersDir
6143
- );
6236
+ function readIdentityForListing(paths, name) {
6237
+ try {
6238
+ return readIdentity(paths, name);
6239
+ } catch (error) {
6240
+ if (error instanceof ConfigValidationError || error instanceof SyntaxError) {
6241
+ return { problem: error.message.replace(/\s*\n\s*/g, " ") };
6242
+ }
6243
+ throw error;
6244
+ }
6245
+ }
6246
+ function listIdentities(paths) {
6247
+ if (!fs5.existsSync(paths.identitiesDir)) {
6248
+ return [];
6249
+ }
6250
+ const active = readActiveIdentity(paths);
6251
+ const names = fs5.readdirSync(paths.identitiesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isIdentityDirectoryName(entry.name)).map((entry) => entry.name).sort();
6144
6252
  const result = [];
6145
6253
  for (const name of names) {
6146
- let provider;
6147
- try {
6148
- provider = readProvider(paths, name);
6149
- } catch (error) {
6150
- if (!(error instanceof ConfigValidationError || error instanceof LegacyProviderFileError)) {
6151
- throw error;
6152
- }
6153
- console.error(`agent-shim: providers/${name}.json is invalid and was skipped: ${error.message}`);
6254
+ const read = readIdentityForListing(paths, name);
6255
+ if (read === void 0) {
6154
6256
  continue;
6155
6257
  }
6156
- if (provider !== void 0) {
6157
- result.push({ name, provider });
6158
- }
6258
+ const isActive = name === active;
6259
+ result.push("problem" in read ? { name, isActive, problem: read.problem } : { name, identity: read, isActive });
6159
6260
  }
6160
6261
  return result;
6161
6262
  }
6162
- function listProviderNames(readdir, providersDir) {
6163
- let entries;
6164
- try {
6165
- entries = readdir(providersDir);
6166
- } catch {
6167
- return [];
6168
- }
6169
- return entries.filter((entry) => entry.endsWith(".json")).map((entry) => entry.slice(0, -".json".length)).sort();
6170
- }
6171
- function describeProviderEndpoint(provider) {
6172
- return isCodexProvider(provider) ? "codex translation daemon" : provider.baseUrl;
6173
- }
6174
- var ProviderKindMismatchError = class extends UsageError {
6175
- constructor(message) {
6176
- super(message);
6177
- this.name = "ProviderKindMismatchError";
6263
+ function setDefaultConfigProfile(paths, identityName, profileName) {
6264
+ if (!identityExists(paths, identityName)) {
6265
+ throw new IdentityNotFoundError(identityName);
6178
6266
  }
6179
- };
6180
- function isEmptyCodexConfig(config) {
6181
- return config === void 0 || config.defaultModel === void 0 && config.effort === void 0 && (config.models === void 0 || Object.keys(config.models).length === 0);
6267
+ return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
6268
+ defaultConfigProfile: profileName
6269
+ });
6182
6270
  }
6183
- function addProvider(paths, name, input) {
6184
- if (providerExists(paths, name)) {
6185
- throw new ProviderAlreadyExistsError(name);
6186
- }
6187
- if (!PROVIDER_NAME_RE.test(name)) {
6188
- throw new InvalidProviderNameError(name);
6189
- }
6190
- const kind = input.kind ?? "http";
6191
- if (kind === "codex" && input.baseUrl !== void 0) {
6192
- throw new ProviderKindMismatchError("A codex provider has no base URL: the launcher points its sessions at the front-door daemon. Drop --base-url.");
6193
- }
6194
- if (kind === "http" && input.baseUrl === void 0) {
6195
- throw new ProviderKindMismatchError("An http provider needs --base-url.");
6196
- }
6197
- if (kind === "http" && !isEmptyCodexConfig(input.codex)) {
6198
- throw new ProviderKindMismatchError("Codex settings apply only to a codex provider (--kind codex).");
6199
- }
6200
- const shared = {
6201
- displayName: input.displayName,
6202
- credential: { sources: input.sources, ...input.target === void 0 ? {} : { target: input.target } },
6203
- ...input.env === void 0 || Object.keys(input.env).length === 0 ? {} : { env: input.env }
6204
- };
6205
- const parsed = ProviderSchema.safeParse(
6206
- kind === "codex" ? { kind, ...shared, ...isEmptyCodexConfig(input.codex) ? {} : { codex: input.codex } } : { ...shared, baseUrl: input.baseUrl }
6207
- );
6208
- if (!parsed.success) {
6209
- throw new ConfigValidationError(providerJsonPath(paths, name), parsed.error.issues);
6271
+ function setAllowAmbientCredential(paths, identityName, allow) {
6272
+ if (!identityExists(paths, identityName)) {
6273
+ throw new IdentityNotFoundError(identityName);
6210
6274
  }
6211
- writeJsonAtomic(providerJsonPath(paths, name), parsed.data);
6212
- return parsed.data;
6275
+ return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
6276
+ allowAmbientCredential: allow
6277
+ });
6213
6278
  }
6214
- function updateProvider(paths, name, input) {
6215
- const existing = readProvider(paths, name);
6279
+ function setIdentityCredential(paths, identityName, change) {
6280
+ const existing = readIdentity(paths, identityName);
6216
6281
  if (existing === void 0) {
6217
- throw new ProviderNotFoundError(name);
6218
- }
6219
- const unset = new Set(input.unsetEnv);
6220
- const env = Object.fromEntries(Object.entries({ ...existing.env, ...input.env }).filter(([key]) => !unset.has(key)));
6221
- const target = input.target ?? existing.credential.target;
6222
- const cache = input.cache === void 0 ? existing.credential.cache : input.cache === false ? void 0 : input.cache;
6223
- if (isCodexProvider(existing) && input.baseUrl !== void 0) {
6224
- throw new ProviderKindMismatchError(`Provider "${name}" is a codex provider, which has no base URL.`);
6225
- }
6226
- if (!isCodexProvider(existing) && !isEmptyCodexConfig(input.codex)) {
6227
- throw new ProviderKindMismatchError(`Provider "${name}" is an http provider; codex settings apply only to a codex provider.`);
6228
- }
6229
- const shared = {
6230
- ...input.displayName === void 0 ? {} : { displayName: input.displayName },
6231
- credential: {
6232
- sources: input.sources ?? existing.credential.sources,
6233
- ...target === void 0 ? {} : { target },
6234
- ...cache === void 0 ? {} : { cache }
6235
- },
6236
- env: Object.keys(env).length === 0 ? void 0 : env
6237
- };
6238
- let candidate;
6239
- if (isCodexProvider(existing)) {
6240
- const codex = {
6241
- ...existing.codex,
6242
- ...input.codex,
6243
- ...existing.codex?.models === void 0 && input.codex?.models === void 0 ? {} : { models: { ...existing.codex?.models, ...input.codex?.models } }
6244
- };
6245
- candidate = { ...existing, ...shared, codex: isEmptyCodexConfig(codex) ? void 0 : codex };
6246
- } else {
6247
- candidate = { ...existing, ...shared, ...input.baseUrl === void 0 ? {} : { baseUrl: input.baseUrl } };
6282
+ throw new IdentityNotFoundError(identityName);
6248
6283
  }
6249
- const parsed = ProviderSchema.safeParse(candidate);
6250
- if (!parsed.success) {
6251
- throw new ConfigValidationError(providerJsonPath(paths, name), parsed.error.issues);
6284
+ if (change === false) {
6285
+ return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, { credential: void 0 });
6252
6286
  }
6253
- writeJsonAtomic(providerJsonPath(paths, name), parsed.data);
6254
- return parsed.data;
6255
- }
6256
- function removeProvider(paths, name) {
6257
- if (!providerExists(paths, name)) {
6258
- throw new ProviderNotFoundError(name);
6287
+ const sources = change.sources ?? existing.credential?.sources;
6288
+ if (sources === void 0) {
6289
+ throw new UsageError(`Identity "${identityName}" has no credential yet: pass --credential to give it one before choosing its target.`);
6259
6290
  }
6260
- fs6.rmSync(providerJsonPath(paths, name));
6291
+ const target = change.target ?? existing.credential?.target;
6292
+ const cache = change.cache === void 0 ? existing.credential?.cache : change.cache === false ? void 0 : change.cache;
6293
+ return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
6294
+ credential: { sources: [...sources], ...target === void 0 ? {} : { target }, ...cache === void 0 ? {} : { cache } }
6295
+ });
6261
6296
  }
6262
- function cascadeProviderName(cascade) {
6263
- let name;
6264
- for (const layer of assembleCascade(cascade).layers) {
6265
- if (layer.launch?.provider !== void 0) {
6266
- name = layer.launch.provider;
6297
+ function belongsToIdentity(entry, name) {
6298
+ return entry === name || entry === path16.basename(identityLockPath("", name)) || entry.startsWith(`.${name}.scratch.`) || entry.startsWith(`.${name}.previous.`);
6299
+ }
6300
+ function removeIdentity(paths, name) {
6301
+ if (!identityExists(paths, name)) {
6302
+ throw new IdentityNotFoundError(name);
6303
+ }
6304
+ for (const entry of fs5.readdirSync(paths.identitiesDir)) {
6305
+ if (belongsToIdentity(entry, name)) {
6306
+ fs5.rmSync(path16.join(paths.identitiesDir, entry), { recursive: true, force: true });
6267
6307
  }
6268
6308
  }
6269
- return name;
6309
+ if (readActiveIdentity(paths) === name) {
6310
+ fs5.rmSync(paths.activeIdentityFile);
6311
+ }
6270
6312
  }
6271
- function resolveProvider(params) {
6272
- const cliName = params.cliProvider !== void 0 && params.cliProvider !== "" ? params.cliProvider : void 0;
6273
- const name = cliName ?? (params.cascade === void 0 ? void 0 : cascadeProviderName(params.cascade));
6274
- if (name === void 0) {
6313
+
6314
+ // src/providersStore.ts
6315
+ import fs6 from "node:fs";
6316
+ import path17 from "node:path";
6317
+
6318
+ // src/credentialCache.ts
6319
+ import { z as z11 } from "zod";
6320
+ var MS_PER_UNIT = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
6321
+ var CachedCredentialSchema = z11.strictObject({
6322
+ token: z11.string().min(1),
6323
+ fetchedAt: z11.number().int().nonnegative(),
6324
+ source: CredentialSourceSchema
6325
+ });
6326
+ function effectiveStore(cache, platform) {
6327
+ return cache.store ?? (platform === "darwin" ? "keychain" : "file");
6328
+ }
6329
+ function ttlMs(cache) {
6330
+ if (cache.ttl === void 0) {
6275
6331
  return void 0;
6276
6332
  }
6277
- const definition = loadProvider(params.paths.providersDir, name, params.port);
6278
- if (definition === void 0) {
6279
- const known = listProviderNames(params.port.readdir, params.paths.providersDir);
6280
- return {
6281
- ok: false,
6282
- status: 1,
6283
- message: `agent-shim: no provider named "${name}". ` + (known.length > 0 ? `Known providers: ${known.join(", ")}.` : "No providers are defined yet; run `agent-shim provider add`.")
6284
- };
6333
+ const multiplier = MS_PER_UNIT[cache.ttl.slice(-1)];
6334
+ if (multiplier === void 0) {
6335
+ throw new Error(`credential cache ttl "${cache.ttl}" has no recognised unit`);
6285
6336
  }
6286
- if (params.credentials === void 0) {
6287
- return { ok: false, status: 1, message: `agent-shim: provider ${name} needs its credential resolved, but this launcher has no credential port wired` };
6337
+ return Number(cache.ttl.slice(0, -1)) * multiplier;
6338
+ }
6339
+ function isFresh(entry, cache, now) {
6340
+ const limit = ttlMs(cache);
6341
+ if (entry.fetchedAt > now) {
6342
+ return false;
6288
6343
  }
6289
- const resolution = resolveCredential({ credential: definition.credential, env: params.env, port: params.credentials, subject: `provider ${name}` });
6290
- if (!resolution.ok) {
6291
- return { ok: false, status: CREDENTIAL_UNAVAILABLE_EXIT, message: resolution.message };
6344
+ return limit === void 0 || now - entry.fetchedAt < limit;
6345
+ }
6346
+ function cacheAgeMs(entry, now) {
6347
+ return Math.max(0, now - entry.fetchedAt);
6348
+ }
6349
+ function describeCachedCredential(params) {
6350
+ const store = effectiveStore(params.cache, params.platform);
6351
+ let entry;
6352
+ try {
6353
+ entry = params.port.read(store, params.owner);
6354
+ } catch (error) {
6355
+ return { store, status: "unreadable", reason: error instanceof Error ? error.message : String(error) };
6292
6356
  }
6293
- return { ok: true, provider: { name, definition, credential: resolution.credential }, warnings: resolution.credential.warnings };
6357
+ if (entry === void 0) {
6358
+ return { store, status: "empty" };
6359
+ }
6360
+ const limit = ttlMs(params.cache);
6361
+ const ageMs = cacheAgeMs(entry, params.nowMs);
6362
+ return {
6363
+ store,
6364
+ status: isFresh(entry, params.cache, params.nowMs) ? "fresh" : "expired",
6365
+ ageMs,
6366
+ ...limit === void 0 ? {} : { expiresInMs: limit - ageMs }
6367
+ };
6294
6368
  }
6295
-
6296
- // src/directoryRulesStore.ts
6297
- var DirectoryRuleNotFoundError = class extends CliError {
6298
- constructor(rulePath) {
6299
- super(`No directory rule found for path "${rulePath}".`);
6300
- this.rulePath = rulePath;
6301
- this.name = "DirectoryRuleNotFoundError";
6369
+ function formatCachedCredentialState(state) {
6370
+ if (state.status === "empty") {
6371
+ return `no cached entry in the ${state.store} store yet`;
6302
6372
  }
6303
- rulePath;
6304
- };
6305
- var DirectoryRuleMissingTargetError = class extends CliError {
6306
- constructor() {
6307
- super("A directory rule must set at least one of --config-profile or --identity.");
6308
- this.name = "DirectoryRuleMissingTargetError";
6373
+ if (state.status === "unreadable") {
6374
+ return `the ${state.store} store cannot be read here: ${state.reason}`;
6309
6375
  }
6310
- };
6311
- var DirectoryRuleAlreadyExistsError = class extends CliError {
6312
- constructor(rulePath) {
6313
- super(`A directory rule for path "${rulePath}" already exists. Use \`agent-shim rule set\` to change it.`);
6314
- this.rulePath = rulePath;
6315
- this.name = "DirectoryRuleAlreadyExistsError";
6376
+ if (state.status === "expired" && (state.expiresInMs === void 0 || state.expiresInMs > 0)) {
6377
+ return "cached with a timestamp in the future, treated as expired";
6316
6378
  }
6317
- rulePath;
6318
- };
6319
- function readDirectoryRules(paths) {
6320
- return readJson(paths.directoryRulesFile, DirectoryRulesSchema) ?? { rules: [] };
6321
- }
6322
- function writeDirectoryRules(paths, rules) {
6323
- const parsed = DirectoryRulesSchema.safeParse(rules);
6324
- if (!parsed.success) {
6325
- throw new ConfigValidationError(paths.directoryRulesFile, parsed.error.issues);
6379
+ const age = `cached ${formatAge(state.ageMs)} ago`;
6380
+ if (state.expiresInMs === void 0) {
6381
+ return `${age}, no expiry`;
6326
6382
  }
6327
- writeJsonAtomic(paths.directoryRulesFile, parsed.data);
6383
+ return state.status === "fresh" ? `${age}, expires in ${formatAge(state.expiresInMs)}` : `${age}, expired ${formatAge(-state.expiresInMs)} ago`;
6328
6384
  }
6329
- function listDirectoryRules(paths) {
6330
- return readDirectoryRules(paths).rules;
6331
- }
6332
- function addDirectoryRule(paths, rulePath, options) {
6333
- if (options.configProfile === void 0 && options.identity === void 0) {
6334
- throw new DirectoryRuleMissingTargetError();
6385
+
6386
+ // src/credential.ts
6387
+ var CREDENTIAL_UNAVAILABLE_EXIT = 64;
6388
+ var CREDENTIAL_COMMAND_TIMEOUT_MS = 1e4;
6389
+ var CREDENTIAL_INTERACTIVE_TIMEOUT_MS = 12e4;
6390
+ var GROUP_OTHER_PERMISSION_BITS = 63;
6391
+ var PERMISSION_BITS = 511;
6392
+ var OCTAL_RADIX2 = 8;
6393
+ function commandArgv(source) {
6394
+ if ("command" in source) {
6395
+ return source.command;
6335
6396
  }
6336
- const current = readDirectoryRules(paths);
6337
- if (current.rules.some((rule) => rule.path === rulePath)) {
6338
- throw new DirectoryRuleAlreadyExistsError(rulePath);
6397
+ if ("op" in source) {
6398
+ return ["op", "read", source.op];
6339
6399
  }
6340
- const created = buildNewRule(rulePath, options);
6341
- writeDirectoryRules(paths, { ...current, rules: [...current.rules, created] });
6342
- return created;
6400
+ const account = source.keychain.account;
6401
+ return ["security", "find-generic-password", "-s", source.keychain.service, ...account === void 0 ? [] : ["-a", account], "-w"];
6343
6402
  }
6344
- function ruleHasEffect(rule) {
6345
- return rule.configProfile !== void 0 || rule.identity !== void 0 || rule.categories !== void 0 || rule.entries !== void 0 || rule.launch !== void 0;
6403
+ function isInteractiveSource(source, env) {
6404
+ if ("command" in source || "keychain" in source) {
6405
+ return source.interactive ?? false;
6406
+ }
6407
+ if ("op" in source) {
6408
+ return source.interactive ?? (env.OP_SERVICE_ACCOUNT_TOKEN ?? "") === "";
6409
+ }
6410
+ return false;
6346
6411
  }
6347
- function updateDirectoryRule(paths, rulePath, options) {
6348
- const current = readDirectoryRules(paths);
6349
- const index = current.rules.findIndex((rule) => rule.path === rulePath);
6350
- const existing = index === -1 ? void 0 : current.rules[index];
6351
- if (existing === void 0) {
6352
- throw new DirectoryRuleNotFoundError(rulePath);
6412
+ function summariseSource(source) {
6413
+ if ("env" in source) {
6414
+ return { kind: "env", variable: source.env };
6353
6415
  }
6354
- const { configProfile, identity, ...rest } = existing;
6355
- const nextConfigProfile = options.configProfile === void 0 ? configProfile : options.configProfile === false ? void 0 : options.configProfile;
6356
- const nextIdentity = options.identity === void 0 ? identity : options.identity === false ? void 0 : options.identity;
6357
- const updated = {
6358
- ...rest,
6359
- ...nextConfigProfile === void 0 ? {} : { configProfile: nextConfigProfile },
6360
- ...nextIdentity === void 0 ? {} : { identity: nextIdentity }
6361
- };
6362
- if (!ruleHasEffect(updated)) {
6363
- throw new DirectoryRuleMissingTargetError();
6416
+ if ("file" in source) {
6417
+ return { kind: "file", path: source.file };
6364
6418
  }
6365
- const nextRules = [...current.rules];
6366
- nextRules[index] = updated;
6367
- writeDirectoryRules(paths, { ...current, rules: nextRules });
6368
- return updated;
6419
+ if ("command" in source) {
6420
+ return { kind: "command", program: source.command[0] };
6421
+ }
6422
+ if ("op" in source) {
6423
+ return { kind: "op", reference: source.op };
6424
+ }
6425
+ if ("keychain" in source) {
6426
+ return {
6427
+ kind: "keychain",
6428
+ service: source.keychain.service,
6429
+ ...source.keychain.account === void 0 ? {} : { account: source.keychain.account }
6430
+ };
6431
+ }
6432
+ return { kind: "literal" };
6369
6433
  }
6370
- function buildNewRule(rulePath, options) {
6434
+ function summariseCredential(credential) {
6371
6435
  return {
6372
- path: rulePath,
6373
- ...options.configProfile !== void 0 ? { configProfile: options.configProfile } : {},
6374
- ...options.identity !== void 0 ? { identity: options.identity } : {}
6436
+ target: credential.target ?? "bearer",
6437
+ sources: credential.sources.map(summariseSource),
6438
+ ...credential.cache === void 0 ? {} : { cache: credential.cache }
6375
6439
  };
6376
6440
  }
6377
- function removeDirectoryRule(paths, rulePath) {
6378
- const current = readDirectoryRules(paths);
6379
- const nextRules = current.rules.filter((rule) => rule.path !== rulePath);
6380
- if (nextRules.length === current.rules.length) {
6381
- throw new DirectoryRuleNotFoundError(rulePath);
6441
+ function formatSourceSummary(summary) {
6442
+ switch (summary.kind) {
6443
+ case "env":
6444
+ return `env ${summary.variable}`;
6445
+ case "file":
6446
+ return `file ${summary.path}`;
6447
+ case "command":
6448
+ return `command ${summary.program}`;
6449
+ case "op":
6450
+ return `op ${summary.reference}`;
6451
+ case "keychain":
6452
+ return `keychain ${summary.service}${summary.account === void 0 ? "" : ` (account ${summary.account})`}`;
6453
+ case "literal":
6454
+ return "literal (non-secret placeholder)";
6455
+ default:
6456
+ return summary;
6457
+ }
6458
+ }
6459
+ function describeSource(source) {
6460
+ return formatSourceSummary(summariseSource(source));
6461
+ }
6462
+ function formatCredentialSummary(summary) {
6463
+ const cache = summary.cache === void 0 ? "" : `, cached ${summary.cache.ttl === void 0 ? "with no expiry" : `for ${summary.cache.ttl}`}${summary.cache.store === void 0 ? "" : ` in the ${summary.cache.store} store`}`;
6464
+ return `${summary.target} from ${summary.sources.map(formatSourceSummary).join(", then ")}${cache}`;
6465
+ }
6466
+ function describeCredential(credential) {
6467
+ return formatCredentialSummary(summariseCredential(credential));
6468
+ }
6469
+ function trySource(source, params) {
6470
+ const none = (reason, warn = false) => ({ ok: false, reason, warn });
6471
+ if ("env" in source) {
6472
+ const value = params.env[source.env];
6473
+ return value === void 0 || value === "" ? none("is unset or empty") : { ok: true, token: value };
6474
+ }
6475
+ if ("literal" in source) {
6476
+ return { ok: true, token: source.literal };
6477
+ }
6478
+ if ("file" in source) {
6479
+ const read = params.port.readSecretFile(source.file);
6480
+ if (!read.found) {
6481
+ return none("does not exist");
6482
+ }
6483
+ if (read.mode !== void 0 && (read.mode & GROUP_OTHER_PERMISSION_BITS) !== 0) {
6484
+ return none(
6485
+ `is readable or writable by group or others (mode ${(read.mode & PERMISSION_BITS).toString(OCTAL_RADIX2)}); run \`chmod 600 ${source.file}\``,
6486
+ true
6487
+ );
6488
+ }
6489
+ const token2 = read.content.trim();
6490
+ return token2 === "" ? none("is empty") : { ok: true, token: token2 };
6491
+ }
6492
+ const argv = commandArgv(source);
6493
+ const interactive = isInteractiveSource(source, params.env);
6494
+ if (interactive && !params.port.personPresent()) {
6495
+ return none("needs a person to approve it, but there is no terminal or desktop session");
6382
6496
  }
6383
- writeDirectoryRules(paths, { ...current, rules: nextRules });
6384
- }
6385
-
6386
- // src/checkReport.ts
6387
- import os4 from "node:os";
6388
-
6389
- // src/launcher/cascade.ts
6390
- import path18 from "node:path";
6391
-
6392
- // src/pathNorm.ts
6393
- import path17 from "node:path";
6394
- function expandTilde(value, home) {
6395
- if (value === "~") {
6396
- return home;
6497
+ const timeoutMs = source.timeoutMs ?? (interactive ? CREDENTIAL_INTERACTIVE_TIMEOUT_MS : CREDENTIAL_COMMAND_TIMEOUT_MS);
6498
+ const result = params.port.runCommand(argv, { timeoutMs, interactive });
6499
+ if (result.timedOut) {
6500
+ return none(`timed out after ${String(timeoutMs)}ms`);
6397
6501
  }
6398
- if (value.startsWith("~/")) {
6399
- return path17.join(home, value.slice(2));
6502
+ if (result.status !== 0) {
6503
+ const stderr = result.stderr.trim();
6504
+ const outcome = result.status === null ? "could not be run or was killed by a signal" : `exited with status ${String(result.status)}`;
6505
+ return none(`${outcome}${stderr === "" ? "" : `: ${stderr}`}`);
6400
6506
  }
6401
- return value;
6507
+ const token = result.stdout.trim();
6508
+ return token === "" ? none("printed no token") : { ok: true, token };
6402
6509
  }
6403
- function normaliseRulePath(rulePath, home) {
6404
- return path17.resolve(expandTilde(rulePath, home));
6510
+ function warmCommandFor(subject) {
6511
+ const [kind, name] = subject.split(" ");
6512
+ return kind === "provider" ? `agent-shim credential warm --provider ${name ?? ""}` : `agent-shim credential warm ${name ?? ""}`;
6405
6513
  }
6406
-
6407
- // src/launcher/cascade.ts
6408
- var PORTABLE_CONFIG_FILENAME = ".agent-shim.json";
6409
- var PORTABLE_LOCAL_CONFIG_FILENAME = ".agent-shim.local.json";
6410
- function loadCascadeInput(params) {
6411
- const globalConfig = loadConfigFile(params.paths.globalConfigFile, GlobalConfigSchema, params.read);
6412
- const directoryRules = loadConfigFile(params.paths.directoryRulesFile, DirectoryRulesSchema, params.read);
6413
- const loadProfile = (name) => {
6414
- const filepath = path18.join(params.paths.configProfilesDir, `${name}.json`);
6415
- const loaded = loadConfigFile(filepath, ConfigProfileSchema, params.read);
6416
- if (loaded === void 0) {
6417
- return void 0;
6418
- }
6419
- return { name, profile: loaded.config, entryOrder: loaded.entryOrder, filepath: loaded.filepath };
6420
- };
6421
- const limit = globalConfig?.config.walkUpLimit;
6422
- const dirs = walkDirectoryAncestors(params.cwd, {
6423
- home: params.home,
6424
- ...limit === void 0 ? {} : { limit: expandTilde(limit, params.home) },
6425
- ...params.isReadable === void 0 ? {} : { isReadable: params.isReadable }
6426
- });
6427
- const levels = [];
6428
- for (const dir of dirs) {
6429
- const portable = loadConfigFile(path18.join(dir, PORTABLE_CONFIG_FILENAME), PortableConfigSchema, params.read) ?? loadConfigFile(path18.join(dir, LEGACY_PORTABLE_CONFIG_FILENAME), PortableConfigSchema, params.read);
6430
- const portableLocal = loadConfigFile(path18.join(dir, PORTABLE_LOCAL_CONFIG_FILENAME), PortableConfigSchema, params.read) ?? loadConfigFile(path18.join(dir, LEGACY_PORTABLE_LOCAL_CONFIG_FILENAME), PortableConfigSchema, params.read);
6431
- const rules = (directoryRules?.config.rules ?? []).map((rule, index) => ({ rule, index })).filter(({ rule }) => normaliseRulePath(rule.path, params.home) === dir).map(({ rule, index }) => {
6432
- const entryOrder = directoryRules?.ruleEntryOrders[index];
6433
- return {
6434
- rule,
6435
- ...entryOrder === void 0 ? {} : { entryOrder },
6436
- filepath: params.paths.directoryRulesFile
6437
- };
6438
- });
6439
- if (portable === void 0 && portableLocal === void 0 && rules.length === 0) {
6440
- continue;
6514
+ function resolveCredential(params) {
6515
+ const target = params.credential.target ?? "bearer";
6516
+ const cacheBlock = params.credential.cache;
6517
+ const cache = cacheBlock === void 0 ? void 0 : params.port.cache;
6518
+ const store = cacheBlock === void 0 || cache === void 0 ? void 0 : effectiveStore(cacheBlock, cache.platform);
6519
+ if (cacheBlock !== void 0 && cache !== void 0 && store !== void 0 && params.refresh !== true) {
6520
+ const entry = cache.port.read(store, params.subject);
6521
+ if (entry !== void 0 && isFresh(entry, cacheBlock, cache.now())) {
6522
+ return { ok: true, credential: { target, token: entry.token, source: entry.source, warnings: [], cachedAt: entry.fetchedAt } };
6441
6523
  }
6442
- levels.push({
6443
- dir,
6444
- ...portable === void 0 ? {} : { portable: { config: portable.config, entryOrder: portable.entryOrder, filepath: portable.filepath } },
6445
- ...rules.length === 0 ? {} : { rules },
6446
- ...portableLocal === void 0 ? {} : { portableLocal: { config: portableLocal.config, entryOrder: portableLocal.entryOrder, filepath: portableLocal.filepath } }
6447
- });
6448
6524
  }
6449
- const input = {
6450
- home: params.home,
6451
- ...globalConfig === void 0 ? {} : { globalConfig: { config: globalConfig.config, entryOrder: globalConfig.entryOrder, filepath: globalConfig.filepath } },
6452
- ...params.baseConfigProfile === void 0 ? {} : { baseConfigProfile: params.baseConfigProfile },
6453
- loadProfile,
6454
- levels,
6455
- ...params.cliOverride === void 0 ? {} : { cliOverride: params.cliOverride }
6456
- };
6457
- return { input, ...globalConfig === void 0 ? {} : { globalConfig: globalConfig.config } };
6458
- }
6459
- function readDirectorySelections(loaded) {
6460
- let identity;
6461
- let configProfile;
6462
- for (const level of loaded.input.levels ?? []) {
6463
- for (const source of [level.portable?.config, ...(level.rules ?? []).map((entry) => entry.rule), level.portableLocal?.config]) {
6464
- if (source === void 0) {
6465
- continue;
6466
- }
6467
- if (source.identity !== void 0) {
6468
- identity = source.identity;
6469
- }
6470
- if (source.configProfile !== void 0) {
6471
- configProfile = source.configProfile;
6525
+ const attempts = [];
6526
+ const warnings = [];
6527
+ for (const source of params.credential.sources) {
6528
+ const outcome = trySource(source, params);
6529
+ if (outcome.ok) {
6530
+ if (cache !== void 0 && store !== void 0) {
6531
+ cache.port.write(store, params.subject, { token: outcome.token, fetchedAt: cache.now(), source });
6472
6532
  }
6533
+ return { ok: true, credential: { target, token: outcome.token, source, warnings } };
6534
+ }
6535
+ const line = `${describeSource(source)} ${outcome.reason}`;
6536
+ attempts.push(line);
6537
+ if (outcome.warn) {
6538
+ warnings.push(`agent-shim: ${params.subject}: skipped ${line}`);
6473
6539
  }
6474
6540
  }
6475
- return { ...identity === void 0 ? {} : { identity }, ...configProfile === void 0 ? {} : { configProfile } };
6476
- }
6477
-
6478
- // src/usage/preflight.ts
6479
- var MS_PER_SECOND2 = 1e3;
6480
- var SECONDS_PER_MINUTE = 60;
6481
- var MINUTES_PER_HOUR = 60;
6482
- var HOURS_PER_DAY = 24;
6483
- var WARNING_STATUSES = /* @__PURE__ */ new Set(["allowed_warning", "rejected"]);
6484
- function formatAge(ms) {
6485
- const minutes = Math.floor(ms / (MS_PER_SECOND2 * SECONDS_PER_MINUTE));
6486
- if (minutes < MINUTES_PER_HOUR) {
6487
- return `${String(Math.max(minutes, 0))}m`;
6488
- }
6489
- const hours = Math.floor(minutes / MINUTES_PER_HOUR);
6490
- return hours < HOURS_PER_DAY ? `${String(hours)}h` : `${String(Math.floor(hours / HOURS_PER_DAY))}d`;
6541
+ const hint = cacheBlock === void 0 ? "" : `; fill the cache from a terminal with \`${warmCommandFor(params.subject)}\``;
6542
+ return { ok: false, message: `agent-shim: ${params.subject} has no usable credential: ${attempts.join("; ")}${hint}` };
6491
6543
  }
6492
- function effectiveWindow(window, nowMs) {
6493
- const resetsAtMs = window.resetsAt === void 0 ? void 0 : Date.parse(window.resetsAt);
6494
- if (resetsAtMs !== void 0 && resetsAtMs <= nowMs) {
6495
- return { reset: true, utilization: 0 };
6496
- }
6544
+ function credentialVariables(target, token) {
6497
6545
  return {
6498
- reset: false,
6499
- ...window.utilization === void 0 ? {} : { utilization: window.utilization },
6500
- ...resetsAtMs === void 0 ? {} : { resetsAtMs },
6501
- ...window.status === void 0 ? {} : { status: window.status }
6546
+ ANTHROPIC_AUTH_TOKEN: target === "bearer" ? token : void 0,
6547
+ ANTHROPIC_API_KEY: target === "apiKey" ? token : void 0,
6548
+ CLAUDE_CODE_OAUTH_TOKEN: target === "oauthToken" ? token : void 0
6502
6549
  };
6503
6550
  }
6504
- function quotaWarnings(snapshot, provider, nowMs) {
6505
- const rateLimit = snapshot?.providers[provider]?.rateLimit;
6506
- const unified = rateLimit?.unified;
6507
- if (snapshot === void 0 || rateLimit === void 0 || unified === void 0) {
6508
- return [];
6509
- }
6510
- const age = formatAge(nowMs - Date.parse(rateLimit.observedAt));
6511
- const windows = [
6512
- { name: "five-hour", window: unified.fiveHour },
6513
- { name: "seven-day", window: unified.sevenDay }
6514
- ];
6515
- return windows.flatMap(({ name, window }) => {
6516
- const effective = window === void 0 ? void 0 : effectiveWindow(window, nowMs);
6517
- if (window === void 0 || effective?.status === void 0 || !WARNING_STATUSES.has(effective.status)) {
6518
- return [];
6519
- }
6520
- const reset = window.resetsAt === void 0 ? "" : `, resets ${window.resetsAt}`;
6521
- const verdict = effective.status === "rejected" ? "exhausted" : "nearly used";
6522
- return [`agent-shim: identity ${snapshot.identity}: the ${name} quota is ${verdict}${reset} (last seen ${age} ago)`];
6523
- });
6524
- }
6525
-
6526
- // src/usage/poolPick.ts
6527
- import path19 from "node:path";
6528
- import { z as z9 } from "zod";
6529
6551
 
6530
- // src/usage/plan.ts
6531
- var TIER_MULTIPLIER = /(?:^|_)(\d+)x$/;
6532
- var ZERO_TIER = /(?:^|_)zero$/;
6533
- var BASE_TIER = /(?:^|_)(?:pro|free)$/;
6534
- function planOf(account) {
6535
- const tier = account?.organizationRateLimitTier ?? account?.userRateLimitTier;
6536
- const tierField = tier === void 0 ? {} : { tier };
6537
- if (tier === void 0) {
6538
- return { kind: "subscription", capacity: 1, recognised: false };
6552
+ // src/providersStore.ts
6553
+ var ProviderNotFoundError = class extends CliError {
6554
+ constructor(name) {
6555
+ super(`No provider named "${name}". Run \`agent-shim provider add ${name}\` first.`);
6556
+ this.name = name;
6557
+ this.name = "ProviderNotFoundError";
6539
6558
  }
6540
- if (ZERO_TIER.test(tier)) {
6541
- return { kind: "pay-per-use", ...tierField };
6559
+ name;
6560
+ };
6561
+ var ProviderAlreadyExistsError = class extends CliError {
6562
+ constructor(providerName) {
6563
+ super(`A provider named "${providerName}" already exists.`);
6564
+ this.providerName = providerName;
6565
+ this.name = "ProviderAlreadyExistsError";
6542
6566
  }
6543
- const multiplier = TIER_MULTIPLIER.exec(tier)?.[1];
6544
- if (multiplier !== void 0) {
6545
- return { kind: "subscription", capacity: Number(multiplier), recognised: true, ...tierField };
6567
+ providerName;
6568
+ };
6569
+ var InvalidProviderNameError = class extends CliError {
6570
+ constructor(attemptedName) {
6571
+ super(
6572
+ `"${attemptedName}" is not a valid provider name. Provider names must start with a letter or number and may then contain letters, numbers, dots, hyphens, underscores, and at signs.`
6573
+ );
6574
+ this.attemptedName = attemptedName;
6575
+ this.name = "InvalidProviderNameError";
6546
6576
  }
6547
- return { kind: "subscription", capacity: 1, recognised: BASE_TIER.test(tier), ...tierField };
6577
+ attemptedName;
6578
+ };
6579
+ var PROVIDER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/;
6580
+ function providerJsonPath(paths, name) {
6581
+ return path17.join(paths.providersDir, `${name}.json`);
6548
6582
  }
6549
-
6550
- // src/usage/rateLimit.ts
6551
- var MS_PER_SECOND3 = 1e3;
6552
- function epochSecondsToIso(value) {
6553
- if (value === void 0) {
6554
- return void 0;
6555
- }
6556
- const seconds = Number(value);
6557
- return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * MS_PER_SECOND3).toISOString() : void 0;
6583
+ function providerExists(paths, name) {
6584
+ return fs6.existsSync(providerJsonPath(paths, name));
6558
6585
  }
6559
- function nonNegativeNumber(value) {
6560
- if (value === void 0 || value.trim() === "") {
6561
- return void 0;
6562
- }
6563
- const parsed = Number(value);
6564
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
6586
+ var LEGACY_PROVIDER_FIELDS = ["tokenEnv", "tokenCommand", "authScheme"];
6587
+ var LEGACY_ENV_CREDENTIAL_KEYS = Object.values(CREDENTIAL_TARGET_VARS);
6588
+ var LEGACY_LITERAL_PLACEHOLDER = "<the value of env.ANTHROPIC_AUTH_TOKEN>";
6589
+ function isRecord3(value) {
6590
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6565
6591
  }
6566
- var UNIFIED_PREFIX = "anthropic-ratelimit-unified-";
6567
- function unifiedWindow(headers, window) {
6568
- const utilization = nonNegativeNumber(headers[`${UNIFIED_PREFIX}${window}-utilization`]);
6569
- const resetsAt = epochSecondsToIso(headers[`${UNIFIED_PREFIX}${window}-reset`]);
6570
- const status = headers[`${UNIFIED_PREFIX}${window}-status`];
6571
- if (utilization === void 0 && resetsAt === void 0 && status === void 0) {
6592
+ function legacyProviderConversion(raw) {
6593
+ if (!isRecord3(raw) || "credential" in raw) {
6572
6594
  return void 0;
6573
6595
  }
6574
- return { ...utilization === void 0 ? {} : { utilization }, ...resetsAt === void 0 ? {} : { resetsAt }, ...status === void 0 ? {} : { status } };
6575
- }
6576
- function parseUnifiedRateLimit(headers) {
6577
- if (!Object.keys(headers).some((name) => name.startsWith(UNIFIED_PREFIX))) {
6596
+ const env = isRecord3(raw.env) ? raw.env : void 0;
6597
+ const legacyEnvKeys = env === void 0 ? [] : Object.keys(env).filter((key) => LEGACY_ENV_CREDENTIAL_KEYS.includes(key));
6598
+ const fields = [...LEGACY_PROVIDER_FIELDS.filter((field) => field in raw), ...legacyEnvKeys.map((key) => `env.${key}`)];
6599
+ if (fields.length === 0) {
6578
6600
  return void 0;
6579
6601
  }
6580
- const status = headers[`${UNIFIED_PREFIX}status`];
6581
- const fiveHour = unifiedWindow(headers, "5h");
6582
- const sevenDay = unifiedWindow(headers, "7d");
6583
- const representativeClaim = headers[`${UNIFIED_PREFIX}representative-claim`];
6584
- const resetAt = epochSecondsToIso(headers[`${UNIFIED_PREFIX}reset`]);
6585
- const overageStatus = headers[`${UNIFIED_PREFIX}overage-status`];
6602
+ const sources = [];
6603
+ if (typeof raw.tokenEnv === "string") {
6604
+ sources.push({ env: raw.tokenEnv });
6605
+ }
6606
+ if (Array.isArray(raw.tokenCommand)) {
6607
+ sources.push({ command: raw.tokenCommand });
6608
+ }
6609
+ if (typeof env?.ANTHROPIC_AUTH_TOKEN === "string" && env.ANTHROPIC_AUTH_TOKEN !== "") {
6610
+ sources.push({ literal: LEGACY_LITERAL_PLACEHOLDER });
6611
+ }
6612
+ const remainingEnv = env === void 0 ? void 0 : Object.fromEntries(Object.entries(env).filter(([key]) => !LEGACY_ENV_CREDENTIAL_KEYS.includes(key)));
6613
+ const replacedKeys = [...LEGACY_PROVIDER_FIELDS, "env"];
6614
+ const rest = Object.fromEntries(Object.entries(raw).filter(([key]) => !replacedKeys.includes(key)));
6586
6615
  return {
6587
- ...status === void 0 ? {} : { status },
6588
- ...fiveHour === void 0 ? {} : { fiveHour },
6589
- ...sevenDay === void 0 ? {} : { sevenDay },
6590
- ...representativeClaim === void 0 ? {} : { representativeClaim },
6591
- ...resetAt === void 0 ? {} : { resetAt },
6592
- ...overageStatus === void 0 ? {} : { overageStatus }
6616
+ fields,
6617
+ replacement: {
6618
+ ...rest,
6619
+ credential: { sources, ...raw.authScheme === "apiKey" ? { target: "apiKey" } : {} },
6620
+ ...remainingEnv === void 0 || Object.keys(remainingEnv).length === 0 ? {} : { env: remainingEnv }
6621
+ }
6593
6622
  };
6594
6623
  }
6595
-
6596
- // src/usage/scan.ts
6597
- import { z as z8 } from "zod";
6598
- var AnthropicUsageSchema = z8.looseObject({
6599
- input_tokens: z8.number().int().nonnegative().nullish(),
6600
- output_tokens: z8.number().int().nonnegative().nullish(),
6601
- cache_creation_input_tokens: z8.number().int().nonnegative().nullish(),
6602
- cache_read_input_tokens: z8.number().int().nonnegative().nullish()
6603
- });
6604
- var ErrorObjectSchema = z8.looseObject({
6605
- type: z8.string().optional(),
6606
- code: z8.union([z8.string(), z8.number()]).optional(),
6607
- message: z8.string().optional()
6608
- });
6609
-
6610
- // src/usage/middleware.ts
6611
- var ANTHROPIC_PROVIDER = "anthropic";
6612
-
6613
- // src/usage/pick.ts
6614
- var MS_PER_SECOND4 = 1e3;
6615
- var MS_PER_HOUR = 36e5;
6616
- var HOURS_PER_DAY2 = 24;
6617
- var FIVE_HOURS = 5;
6618
- var PROMPT_CACHE_TTL_HOURS = 1;
6619
- var PERCENT = 100;
6620
- var SEVEN_DAYS = 7;
6621
- var FIVE_HOUR_WINDOW_MS = FIVE_HOURS * MS_PER_HOUR;
6622
- var SEVEN_DAY_WINDOW_MS = SEVEN_DAYS * HOURS_PER_DAY2 * MS_PER_HOUR;
6623
- var PROMPT_CACHE_TTL_MS = PROMPT_CACHE_TTL_HOURS * MS_PER_HOUR;
6624
- var CLASS_ORDER = { scored: 0, unknown: 1, "pay-per-use": 2, ineligible: 3 };
6625
- function unifiedOf(state) {
6626
- return state?.rateLimit?.unified;
6627
- }
6628
- function limitBlockedUntil(state, nowMs) {
6629
- const limit = state?.lastLimit;
6630
- if (state === void 0 || limit === void 0) {
6631
- return void 0;
6624
+ var LegacyProviderFileError = class extends CliError {
6625
+ constructor(filePath, conversion) {
6626
+ super(
6627
+ `${filePath} uses ${conversion.fields.join(", ")}, which a credential block replaced. Rewrite it as:
6628
+ ` + JSON.stringify(conversion.replacement, null, 2)
6629
+ );
6630
+ this.filePath = filePath;
6631
+ this.conversion = conversion;
6632
+ this.name = "LegacyProviderFileError";
6632
6633
  }
6633
- if (Date.parse(state.lastRequestAt) > Date.parse(limit.observedAt)) {
6634
+ filePath;
6635
+ conversion;
6636
+ };
6637
+ function parseProviderFile(filePath, raw) {
6638
+ const conversion = legacyProviderConversion(raw);
6639
+ if (conversion !== void 0) {
6640
+ throw new LegacyProviderFileError(filePath, conversion);
6641
+ }
6642
+ const parsed = ProviderSchema.safeParse(raw);
6643
+ if (!parsed.success) {
6644
+ throw new ConfigValidationError(filePath, parsed.error.issues);
6645
+ }
6646
+ return parsed.data;
6647
+ }
6648
+ function readProvider(paths, name) {
6649
+ const filePath = providerJsonPath(paths, name);
6650
+ if (!fs6.existsSync(filePath)) {
6634
6651
  return void 0;
6635
6652
  }
6636
- const until = limit.resetAt === void 0 ? limit.retryAfterSeconds === void 0 ? void 0 : Date.parse(limit.observedAt) + limit.retryAfterSeconds * MS_PER_SECOND4 : Date.parse(limit.resetAt);
6637
- return until !== void 0 && until > nowMs ? until : void 0;
6653
+ const raw = JSON.parse(fs6.readFileSync(filePath, "utf8"));
6654
+ return parseProviderFile(filePath, raw);
6638
6655
  }
6639
- function windowBlockedUntil(windows) {
6640
- const resets = windows.flatMap((window) => window.status === "rejected" && window.resetsAtMs !== void 0 ? [window.resetsAtMs] : []);
6641
- return resets.length === 0 ? void 0 : Math.max(...resets);
6656
+ function loadProvider(providersDir, name, port) {
6657
+ const filePath = path17.join(providersDir, `${name}.json`);
6658
+ const raw = port.readConfigFile(filePath);
6659
+ return raw === void 0 ? void 0 : parseProviderFile(filePath, raw);
6642
6660
  }
6643
- function fiveHourBurn(records) {
6644
- const points = [];
6645
- for (const record of records) {
6646
- const window = record.rateLimitHeaders === void 0 ? void 0 : parseUnifiedRateLimit(record.rateLimitHeaders)?.fiveHour;
6647
- if (window?.utilization !== void 0 && window.resetsAt !== void 0) {
6648
- points.push({ resetsAt: window.resetsAt, point: { atMs: Date.parse(record.at), utilization: window.utilization } });
6661
+ function listProviders(paths) {
6662
+ const names = listProviderNames(
6663
+ (dir) => fs6.readdirSync(dir),
6664
+ paths.providersDir
6665
+ );
6666
+ const result = [];
6667
+ for (const name of names) {
6668
+ let provider;
6669
+ try {
6670
+ provider = readProvider(paths, name);
6671
+ } catch (error) {
6672
+ if (!(error instanceof ConfigValidationError || error instanceof LegacyProviderFileError)) {
6673
+ throw error;
6674
+ }
6675
+ console.error(`agent-shim: providers/${name}.json is invalid and was skipped: ${error.message}`);
6676
+ continue;
6677
+ }
6678
+ if (provider !== void 0) {
6679
+ result.push({ name, provider });
6649
6680
  }
6650
6681
  }
6651
- const latest = points.at(-1)?.resetsAt;
6652
- const current = points.filter((entry) => entry.resetsAt === latest).map((entry) => entry.point);
6653
- const first = current.at(0);
6654
- const last = current.at(-1);
6655
- if (first === void 0 || last === void 0 || last.atMs <= first.atMs || last.utilization <= first.utilization) {
6656
- return void 0;
6682
+ return result;
6683
+ }
6684
+ function listProviderNames(readdir, providersDir) {
6685
+ let entries;
6686
+ try {
6687
+ entries = readdir(providersDir);
6688
+ } catch {
6689
+ return [];
6657
6690
  }
6658
- return (last.utilization - first.utilization) / (last.atMs - first.atMs);
6691
+ return entries.filter((entry) => entry.endsWith(".json")).map((entry) => entry.slice(0, -".json".length)).sort();
6659
6692
  }
6660
- function percent(fraction) {
6661
- return `${String(Math.round(fraction * PERCENT))}%`;
6693
+ function describeProviderEndpoint(provider) {
6694
+ return isCodexProvider(provider) ? "codex translation daemon" : provider.baseUrl;
6662
6695
  }
6663
- function describeWindow(label, window, effective, nowMs) {
6664
- if (effective.reset) {
6665
- return `${label} window has reset`;
6696
+ var ProviderKindMismatchError = class extends UsageError {
6697
+ constructor(message) {
6698
+ super(message);
6699
+ this.name = "ProviderKindMismatchError";
6666
6700
  }
6667
- const used = effective.utilization === void 0 ? "usage unknown" : `${percent(effective.utilization)} used`;
6668
- const resets = effective.resetsAtMs === void 0 ? "" : `, resets in ${formatAge(effective.resetsAtMs - nowMs)}`;
6669
- return `${label} ${used}${resets}${window.status === "allowed_warning" ? " (nearly used)" : ""}`;
6670
- }
6671
- function capacityOf(plan) {
6672
- return plan.kind === "subscription" ? plan.capacity : 1;
6701
+ };
6702
+ function isEmptyCodexConfig(config) {
6703
+ return config === void 0 || config.defaultModel === void 0 && config.effort === void 0 && (config.models === void 0 || Object.keys(config.models).length === 0);
6673
6704
  }
6674
- function assess(member, nowMs) {
6675
- const plan = planOf(member.account);
6676
- const planReasons = plan.kind === "pay-per-use" ? ["billed by use (no plan allowance)"] : plan.recognised ? [`${String(plan.capacity)}x plan`] : [`plan tier ${plan.tier === void 0 ? "not known" : `"${plan.tier}" not recognised`}, counted as 1x`];
6677
- const base = { identity: member.identity, plan };
6678
- if (member.readError !== void 0) {
6679
- return { candidate: { ...base, class: "unknown", reasons: [`usage state unreadable: ${member.readError}`, ...planReasons] } };
6705
+ function addProvider(paths, name, input) {
6706
+ if (providerExists(paths, name)) {
6707
+ throw new ProviderAlreadyExistsError(name);
6680
6708
  }
6681
- const state = member.snapshot?.providers[ANTHROPIC_PROVIDER];
6682
- const unified = unifiedOf(state);
6683
- if (member.snapshot === void 0 || state === void 0 || unified === void 0) {
6684
- return { candidate: { ...base, class: "unknown", reasons: ["no usage recorded yet, so its quota is unknown (turn on trackUsage for it)", ...planReasons] } };
6709
+ if (!PROVIDER_NAME_RE.test(name)) {
6710
+ throw new InvalidProviderNameError(name);
6685
6711
  }
6686
- const observedAgo = state.rateLimit === void 0 ? "" : ` (last seen ${formatAge(nowMs - Date.parse(state.rateLimit.observedAt))} ago)`;
6687
- const fiveHourWindow = unified.fiveHour;
6688
- const sevenDayWindow = unified.sevenDay;
6689
- const five = fiveHourWindow === void 0 ? void 0 : effectiveWindow(fiveHourWindow, nowMs);
6690
- const seven = sevenDayWindow === void 0 ? void 0 : effectiveWindow(sevenDayWindow, nowMs);
6691
- const windowReasons = [
6692
- ...fiveHourWindow === void 0 || five === void 0 ? [] : [describeWindow("5h", fiveHourWindow, five, nowMs)],
6693
- ...sevenDayWindow === void 0 || seven === void 0 ? [] : [describeWindow("7d", sevenDayWindow, seven, nowMs)]
6694
- ];
6695
- const blockedUntil = Math.max(
6696
- windowBlockedUntil([five, seven].flatMap((window) => window === void 0 ? [] : [window])) ?? 0,
6697
- limitBlockedUntil(state, nowMs) ?? 0
6698
- );
6699
- if (blockedUntil > nowMs) {
6700
- if (unified.overageStatus === "allowed") {
6701
- return { candidate: { ...base, class: "pay-per-use", reasons: [`plan exhausted until ${new Date(blockedUntil).toISOString()}, continues as extra usage`, ...windowReasons, ...planReasons] } };
6702
- }
6703
- return { candidate: { ...base, class: "ineligible", blockedUntilMs: blockedUntil, reasons: [`refused until ${new Date(blockedUntil).toISOString()} (in ${formatAge(blockedUntil - nowMs)})`, ...windowReasons, ...planReasons] } };
6712
+ const kind = input.kind ?? "http";
6713
+ if (kind === "codex" && input.baseUrl !== void 0) {
6714
+ throw new ProviderKindMismatchError("A codex provider has no base URL: the launcher points its sessions at the front-door daemon. Drop --base-url.");
6704
6715
  }
6705
- if (plan.kind === "pay-per-use" || fiveHourWindow === void 0 && sevenDayWindow === void 0) {
6706
- return { candidate: { ...base, class: "pay-per-use", reasons: [...plan.kind === "pay-per-use" ? planReasons : ["no plan windows reported, so usage bills as extra usage"]] } };
6716
+ if (kind === "http" && input.baseUrl === void 0) {
6717
+ throw new ProviderKindMismatchError("An http provider needs --base-url.");
6707
6718
  }
6708
- const basis = seven?.utilization !== void 0 ? { window: seven, windowMs: SEVEN_DAY_WINDOW_MS } : five?.utilization !== void 0 ? { window: five, windowMs: FIVE_HOUR_WINDOW_MS } : void 0;
6709
- if (basis?.window.utilization === void 0) {
6710
- return { candidate: { ...base, class: "unknown", reasons: ["quota windows reported no utilisation", ...windowReasons, ...planReasons] } };
6719
+ if (kind === "http" && !isEmptyCodexConfig(input.codex)) {
6720
+ throw new ProviderKindMismatchError("Codex settings apply only to a codex provider (--kind codex).");
6711
6721
  }
6712
- const remaining = Math.max(0, 1 - basis.window.utilization);
6713
- const untilResetMs = basis.window.resetsAtMs === void 0 ? basis.windowMs : Math.max(basis.window.resetsAtMs - nowMs, MS_PER_SECOND4);
6714
- const score = remaining * plan.capacity / (untilResetMs / MS_PER_HOUR);
6715
- const fiveRemaining = five?.utilization === void 0 ? void 0 : Math.max(0, 1 - five.utilization);
6716
- const fiveUntil = five?.resetsAtMs === void 0 ? FIVE_HOUR_WINDOW_MS : Math.max(five.resetsAtMs - nowMs, MS_PER_SECOND4);
6717
- const burn = fiveHourBurn(member.records);
6718
- return {
6719
- candidate: { ...base, class: "scored", score, reasons: [...windowReasons.map((reason, index) => index === windowReasons.length - 1 ? `${reason}${observedAgo}` : reason), ...planReasons] },
6720
- ...fiveRemaining === void 0 ? {} : { fiveHour: { remaining: fiveRemaining, untilResetMs: fiveUntil } },
6721
- ...burn === void 0 ? {} : { burn }
6722
+ const shared = {
6723
+ displayName: input.displayName,
6724
+ credential: { sources: input.sources, ...input.target === void 0 ? {} : { target: input.target } },
6725
+ ...input.env === void 0 || Object.keys(input.env).length === 0 ? {} : { env: input.env }
6722
6726
  };
6723
- }
6724
- function rankPool(input) {
6725
- const { members, nowMs } = input;
6726
- const assessments = members.map((member) => assess(member, nowMs));
6727
- const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
6728
- const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
6729
- const candidates = assessments.map((assessment) => {
6730
- const { candidate, fiveHour, burn } = assessment;
6731
- const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
6732
- if (candidate.class !== "scored" || fiveHour === void 0 || pace === void 0) {
6733
- return { ...candidate, feasible: true };
6734
- }
6735
- const runsDryInMs = fiveHour.remaining / pace;
6736
- if (runsDryInMs >= fiveHour.untilResetMs) {
6737
- return { ...candidate, feasible: true };
6738
- }
6739
- return { ...candidate, feasible: false, reasons: [`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`, ...candidate.reasons] };
6740
- });
6741
- const ordered = [...candidates].sort(
6742
- (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
6727
+ const parsed = ProviderSchema.safeParse(
6728
+ kind === "codex" ? { kind, ...shared, ...isEmptyCodexConfig(input.codex) ? {} : { codex: input.codex } } : { ...shared, baseUrl: input.baseUrl }
6743
6729
  );
6744
- const sticky = input.sticky;
6745
- const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
6746
- const stickyCandidate = ordered[stickyIndex];
6747
- if (sticky !== void 0 && stickyCandidate?.class === "scored" && stickyCandidate.feasible) {
6748
- const ageMs = nowMs - Date.parse(sticky.at);
6749
- const warm = ageMs <= PROMPT_CACHE_TTL_MS;
6750
- if (input.resuming || warm) {
6751
- const why = warm ? `used here ${formatAge(ageMs)} ago, prompt cache still warm` : "resuming a conversation started on this account";
6752
- ordered.splice(stickyIndex, 1);
6753
- ordered.unshift({ ...stickyCandidate, reasons: [why, ...stickyCandidate.reasons] });
6754
- }
6730
+ if (!parsed.success) {
6731
+ throw new ConfigValidationError(providerJsonPath(paths, name), parsed.error.issues);
6732
+ }
6733
+ writeJsonAtomic(providerJsonPath(paths, name), parsed.data);
6734
+ return parsed.data;
6735
+ }
6736
+ function updateProvider(paths, name, input) {
6737
+ const existing = readProvider(paths, name);
6738
+ if (existing === void 0) {
6739
+ throw new ProviderNotFoundError(name);
6740
+ }
6741
+ const unset = new Set(input.unsetEnv);
6742
+ const env = Object.fromEntries(Object.entries({ ...existing.env, ...input.env }).filter(([key]) => !unset.has(key)));
6743
+ const target = input.target ?? existing.credential.target;
6744
+ const cache = input.cache === void 0 ? existing.credential.cache : input.cache === false ? void 0 : input.cache;
6745
+ if (isCodexProvider(existing) && input.baseUrl !== void 0) {
6746
+ throw new ProviderKindMismatchError(`Provider "${name}" is a codex provider, which has no base URL.`);
6755
6747
  }
6756
- const pick = ordered.find((candidate) => candidate.class !== "ineligible");
6757
- const returns = ordered.flatMap((candidate) => candidate.blockedUntilMs === void 0 ? [] : [{ identity: candidate.identity, atMs: candidate.blockedUntilMs }]);
6758
- const earliestReturn = returns.length === 0 ? void 0 : returns.reduce((soonest, entry) => entry.atMs < soonest.atMs ? entry : soonest);
6759
- return {
6760
- candidates: ordered,
6761
- ...pick === void 0 ? { ...earliestReturn === void 0 ? {} : { earliestReturn } } : { pick }
6748
+ if (!isCodexProvider(existing) && !isEmptyCodexConfig(input.codex)) {
6749
+ throw new ProviderKindMismatchError(`Provider "${name}" is an http provider; codex settings apply only to a codex provider.`);
6750
+ }
6751
+ const shared = {
6752
+ ...input.displayName === void 0 ? {} : { displayName: input.displayName },
6753
+ credential: {
6754
+ sources: input.sources ?? existing.credential.sources,
6755
+ ...target === void 0 ? {} : { target },
6756
+ ...cache === void 0 ? {} : { cache }
6757
+ },
6758
+ env: Object.keys(env).length === 0 ? void 0 : env
6762
6759
  };
6760
+ let candidate;
6761
+ if (isCodexProvider(existing)) {
6762
+ const codex = {
6763
+ ...existing.codex,
6764
+ ...input.codex,
6765
+ ...existing.codex?.models === void 0 && input.codex?.models === void 0 ? {} : { models: { ...existing.codex?.models, ...input.codex?.models } }
6766
+ };
6767
+ candidate = { ...existing, ...shared, codex: isEmptyCodexConfig(codex) ? void 0 : codex };
6768
+ } else {
6769
+ candidate = { ...existing, ...shared, ...input.baseUrl === void 0 ? {} : { baseUrl: input.baseUrl } };
6770
+ }
6771
+ const parsed = ProviderSchema.safeParse(candidate);
6772
+ if (!parsed.success) {
6773
+ throw new ConfigValidationError(providerJsonPath(paths, name), parsed.error.issues);
6774
+ }
6775
+ writeJsonAtomic(providerJsonPath(paths, name), parsed.data);
6776
+ return parsed.data;
6763
6777
  }
6764
-
6765
- // src/usage/store.ts
6766
- var DAY_MS2 = 864e5;
6767
- var DAYS_IN_WEEK = 7;
6768
- var USAGE_RETENTION_MS = DAYS_IN_WEEK * DAY_MS2;
6769
-
6770
- // src/usage/poolPick.ts
6771
- var SCHEMA_VERSION = 1;
6772
- var PickEntrySchema = z9.strictObject({ identity: z9.string().min(1), at: z9.iso.datetime() });
6773
- var PicksFileSchema = z9.strictObject({
6774
- schemaVersion: z9.literal(SCHEMA_VERSION),
6775
- picks: z9.record(z9.string().min(1), PickEntrySchema)
6776
- });
6777
- function errorMessage(error) {
6778
- return error instanceof Error ? error.message : String(error);
6778
+ function removeProvider(paths, name) {
6779
+ if (!providerExists(paths, name)) {
6780
+ throw new ProviderNotFoundError(name);
6781
+ }
6782
+ fs6.rmSync(providerJsonPath(paths, name));
6779
6783
  }
6780
- function loadPoolMembers(fs12, paths, identities, nowMs) {
6781
- const windowStartMs = nowMs - FIVE_HOUR_WINDOW_MS;
6782
- const log = readUsageLog(fs12, paths.usageLogDir, { sinceMs: windowStartMs });
6783
- return identities.map((identity) => {
6784
- const records = log.records.filter((record) => record.identity === identity && record.provider === ANTHROPIC_PROVIDER && Date.parse(record.at) >= windowStartMs);
6785
- try {
6786
- const snapshot = readUsageSnapshot(fs12, paths.usageSnapshotsDir, identity);
6787
- const account = readAccountMetadata(fs12, paths.identitiesDir, identity) ?? snapshot?.account;
6788
- return { identity, records, ...snapshot === void 0 ? {} : { snapshot }, ...account === void 0 ? {} : { account } };
6789
- } catch (error) {
6790
- if (error instanceof UsageSnapshotError || error instanceof AccountMetadataError) {
6791
- return { identity, records, readError: errorMessage(error) };
6792
- }
6793
- throw error;
6784
+ function cascadeProviderName(cascade) {
6785
+ let name;
6786
+ for (const layer of assembleCascade(cascade).layers) {
6787
+ if (layer.launch?.provider !== void 0) {
6788
+ name = layer.launch.provider;
6794
6789
  }
6795
- });
6790
+ }
6791
+ return name;
6796
6792
  }
6797
- function rankPoolFromStore(params) {
6798
- return rankPool({
6799
- members: loadPoolMembers(params.fs, params.paths, params.identities, params.nowMs),
6800
- nowMs: params.nowMs,
6801
- resuming: params.resuming,
6802
- ...params.sticky === void 0 ? {} : { sticky: params.sticky }
6803
- });
6793
+ function resolveProvider(params) {
6794
+ const cliName = params.cliProvider !== void 0 && params.cliProvider !== "" ? params.cliProvider : void 0;
6795
+ const name = cliName ?? (params.cascade === void 0 ? void 0 : cascadeProviderName(params.cascade));
6796
+ if (name === void 0) {
6797
+ return void 0;
6798
+ }
6799
+ const definition = loadProvider(params.paths.providersDir, name, params.port);
6800
+ if (definition === void 0) {
6801
+ const known = listProviderNames(params.port.readdir, params.paths.providersDir);
6802
+ return {
6803
+ ok: false,
6804
+ status: 1,
6805
+ message: `agent-shim: no provider named "${name}". ` + (known.length > 0 ? `Known providers: ${known.join(", ")}.` : "No providers are defined yet; run `agent-shim provider add`.")
6806
+ };
6807
+ }
6808
+ if (params.credentials === void 0) {
6809
+ return { ok: false, status: 1, message: `agent-shim: provider ${name} needs its credential resolved, but this launcher has no credential port wired` };
6810
+ }
6811
+ const resolution = resolveCredential({ credential: definition.credential, env: params.env, port: params.credentials, subject: `provider ${name}` });
6812
+ if (!resolution.ok) {
6813
+ return { ok: false, status: CREDENTIAL_UNAVAILABLE_EXIT, message: resolution.message };
6814
+ }
6815
+ return { ok: true, provider: { name, definition, credential: resolution.credential }, warnings: resolution.credential.warnings };
6804
6816
  }
6805
- function readPicksFile(fs12, file) {
6806
- const empty = { schemaVersion: SCHEMA_VERSION, picks: {} };
6807
- const raw = fs12.readFileUtf8(file);
6808
- if (raw === void 0) {
6809
- return { file: empty };
6817
+
6818
+ // src/directoryRulesStore.ts
6819
+ var DirectoryRuleNotFoundError = class extends CliError {
6820
+ constructor(rulePath) {
6821
+ super(`No directory rule found for path "${rulePath}".`);
6822
+ this.rulePath = rulePath;
6823
+ this.name = "DirectoryRuleNotFoundError";
6810
6824
  }
6811
- let parsed;
6812
- try {
6813
- parsed = JSON.parse(raw);
6814
- } catch {
6815
- return { file: empty, problem: `${file} is not valid JSON` };
6825
+ rulePath;
6826
+ };
6827
+ var DirectoryRuleMissingTargetError = class extends CliError {
6828
+ constructor() {
6829
+ super("A directory rule must set at least one of --config-profile or --identity.");
6830
+ this.name = "DirectoryRuleMissingTargetError";
6816
6831
  }
6817
- const result = PicksFileSchema.safeParse(parsed);
6818
- return result.success ? { file: result.data } : { file: empty, problem: `${file} is not a picks file this agent-shim can read` };
6832
+ };
6833
+ var DirectoryRuleAlreadyExistsError = class extends CliError {
6834
+ constructor(rulePath) {
6835
+ super(`A directory rule for path "${rulePath}" already exists. Use \`agent-shim rule set\` to change it.`);
6836
+ this.rulePath = rulePath;
6837
+ this.name = "DirectoryRuleAlreadyExistsError";
6838
+ }
6839
+ rulePath;
6840
+ };
6841
+ function readDirectoryRules(paths) {
6842
+ return readJson(paths.directoryRulesFile, DirectoryRulesSchema) ?? { rules: [] };
6819
6843
  }
6820
- function readStickyPick(fs12, picksFile, directory) {
6821
- const read = readPicksFile(fs12, picksFile);
6822
- const entry = read.file.picks[directory];
6823
- return { ...entry === void 0 ? {} : { sticky: entry }, ...read.problem === void 0 ? {} : { problem: read.problem } };
6844
+ function writeDirectoryRules(paths, rules) {
6845
+ const parsed = DirectoryRulesSchema.safeParse(rules);
6846
+ if (!parsed.success) {
6847
+ throw new ConfigValidationError(paths.directoryRulesFile, parsed.error.issues);
6848
+ }
6849
+ writeJsonAtomic(paths.directoryRulesFile, parsed.data);
6824
6850
  }
6825
- function recordStickyPick(fs12, picksFile, directory, identity, nowMs) {
6826
- const current = readPicksFile(fs12, picksFile).file.picks;
6827
- const kept = Object.entries(current).filter(([, entry]) => nowMs - Date.parse(entry.at) <= USAGE_RETENTION_MS);
6828
- const next = { schemaVersion: SCHEMA_VERSION, picks: { ...Object.fromEntries(kept), [directory]: { identity, at: new Date(nowMs).toISOString() } } };
6829
- fs12.mkdirPrivate(path19.dirname(picksFile));
6830
- fs12.writeFilePrivate(picksFile, `${JSON.stringify(next, null, 2)}
6831
- `);
6851
+ function listDirectoryRules(paths) {
6852
+ return readDirectoryRules(paths).rules;
6832
6853
  }
6833
-
6834
- // src/launcher/pool.ts
6835
- var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
6836
- var TERMINATOR = "--";
6837
- var REASONS_LOGGED = 3;
6838
- function splitMembers(pool, paths, fs12) {
6839
- const exists = (name) => loadIdentity(paths.identitiesDir, name, fs12) !== void 0;
6840
- return { present: pool.identities.filter(exists), missing: pool.identities.filter((name) => !exists(name)) };
6854
+ function addDirectoryRule(paths, rulePath, options) {
6855
+ if (options.configProfile === void 0 && options.identity === void 0) {
6856
+ throw new DirectoryRuleMissingTargetError();
6857
+ }
6858
+ const current = readDirectoryRules(paths);
6859
+ if (current.rules.some((rule) => rule.path === rulePath)) {
6860
+ throw new DirectoryRuleAlreadyExistsError(rulePath);
6861
+ }
6862
+ const created = buildNewRule(rulePath, options);
6863
+ writeDirectoryRules(paths, { ...current, rules: [...current.rules, created] });
6864
+ return created;
6841
6865
  }
6842
- function isResuming(passthrough) {
6843
- const end = passthrough.indexOf(TERMINATOR);
6844
- return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
6866
+ function ruleHasEffect(rule) {
6867
+ return rule.configProfile !== void 0 || rule.identity !== void 0 || rule.categories !== void 0 || rule.entries !== void 0 || rule.launch !== void 0;
6845
6868
  }
6846
- function resolvePoolLaunch(params) {
6847
- const { poolName, log } = params;
6848
- const pool = params.pools?.[poolName];
6849
- if (pool === void 0) {
6850
- return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
6869
+ function updateDirectoryRule(paths, rulePath, options) {
6870
+ const current = readDirectoryRules(paths);
6871
+ const index = current.rules.findIndex((rule) => rule.path === rulePath);
6872
+ const existing = index === -1 ? void 0 : current.rules[index];
6873
+ if (existing === void 0) {
6874
+ throw new DirectoryRuleNotFoundError(rulePath);
6851
6875
  }
6852
- const { present: identities, missing } = splitMembers(pool, params.paths, params.fs);
6853
- for (const name of missing) {
6854
- log.warn(`agent-shim: pool "${poolName}" names identity "${name}", which does not exist; skipping it`);
6876
+ const { configProfile, identity, ...rest } = existing;
6877
+ const nextConfigProfile = options.configProfile === void 0 ? configProfile : options.configProfile === false ? void 0 : options.configProfile;
6878
+ const nextIdentity = options.identity === void 0 ? identity : options.identity === false ? void 0 : options.identity;
6879
+ const updated = {
6880
+ ...rest,
6881
+ ...nextConfigProfile === void 0 ? {} : { configProfile: nextConfigProfile },
6882
+ ...nextIdentity === void 0 ? {} : { identity: nextIdentity }
6883
+ };
6884
+ if (!ruleHasEffect(updated)) {
6885
+ throw new DirectoryRuleMissingTargetError();
6855
6886
  }
6856
- if (identities.length === 0) {
6857
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
6887
+ const nextRules = [...current.rules];
6888
+ nextRules[index] = updated;
6889
+ writeDirectoryRules(paths, { ...current, rules: nextRules });
6890
+ return updated;
6891
+ }
6892
+ function buildNewRule(rulePath, options) {
6893
+ return {
6894
+ path: rulePath,
6895
+ ...options.configProfile !== void 0 ? { configProfile: options.configProfile } : {},
6896
+ ...options.identity !== void 0 ? { identity: options.identity } : {}
6897
+ };
6898
+ }
6899
+ function removeDirectoryRule(paths, rulePath) {
6900
+ const current = readDirectoryRules(paths);
6901
+ const nextRules = current.rules.filter((rule) => rule.path !== rulePath);
6902
+ if (nextRules.length === current.rules.length) {
6903
+ throw new DirectoryRuleNotFoundError(rulePath);
6858
6904
  }
6859
- const resuming = isResuming(params.passthrough);
6860
- const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
6861
- if (stickyRead.problem !== void 0) {
6862
- log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
6905
+ writeDirectoryRules(paths, { ...current, rules: nextRules });
6906
+ }
6907
+
6908
+ // src/checkReport.ts
6909
+ import os4 from "node:os";
6910
+
6911
+ // src/launcher/cascade.ts
6912
+ import path19 from "node:path";
6913
+
6914
+ // src/pathNorm.ts
6915
+ import path18 from "node:path";
6916
+ function expandTilde(value, home) {
6917
+ if (value === "~") {
6918
+ return home;
6863
6919
  }
6864
- for (; ; ) {
6865
- const nowMs = params.now();
6866
- const ranking = rankPoolFromStore({ fs: params.usageFs, paths: params.paths, identities, nowMs, resuming, ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky } });
6867
- const { pick, earliestReturn } = ranking;
6868
- if (pick !== void 0) {
6869
- recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
6870
- return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
6871
- }
6872
- if (earliestReturn === void 0) {
6873
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
6920
+ if (value.startsWith("~/")) {
6921
+ return path18.join(home, value.slice(2));
6922
+ }
6923
+ return value;
6924
+ }
6925
+ function normaliseRulePath(rulePath, home) {
6926
+ return path18.resolve(expandTilde(rulePath, home));
6927
+ }
6928
+
6929
+ // src/launcher/cascade.ts
6930
+ var PORTABLE_CONFIG_FILENAME = ".agent-shim.json";
6931
+ var PORTABLE_LOCAL_CONFIG_FILENAME = ".agent-shim.local.json";
6932
+ function loadCascadeInput(params) {
6933
+ const globalConfig = loadConfigFile(params.paths.globalConfigFile, GlobalConfigSchema, params.read);
6934
+ const directoryRules = loadConfigFile(params.paths.directoryRulesFile, DirectoryRulesSchema, params.read);
6935
+ const loadProfile = (name) => {
6936
+ const filepath = path19.join(params.paths.configProfilesDir, `${name}.json`);
6937
+ const loaded = loadConfigFile(filepath, ConfigProfileSchema, params.read);
6938
+ if (loaded === void 0) {
6939
+ return void 0;
6874
6940
  }
6875
- const until = new Date(earliestReturn.atMs).toISOString();
6876
- if (!params.wait) {
6941
+ return { name, profile: loaded.config, entryOrder: loaded.entryOrder, filepath: loaded.filepath };
6942
+ };
6943
+ const limit = globalConfig?.config.walkUpLimit;
6944
+ const dirs = walkDirectoryAncestors(params.cwd, {
6945
+ home: params.home,
6946
+ ...limit === void 0 ? {} : { limit: expandTilde(limit, params.home) },
6947
+ ...params.isReadable === void 0 ? {} : { isReadable: params.isReadable }
6948
+ });
6949
+ const levels = [];
6950
+ for (const dir of dirs) {
6951
+ const portable = loadConfigFile(path19.join(dir, PORTABLE_CONFIG_FILENAME), PortableConfigSchema, params.read) ?? loadConfigFile(path19.join(dir, LEGACY_PORTABLE_CONFIG_FILENAME), PortableConfigSchema, params.read);
6952
+ const portableLocal = loadConfigFile(path19.join(dir, PORTABLE_LOCAL_CONFIG_FILENAME), PortableConfigSchema, params.read) ?? loadConfigFile(path19.join(dir, LEGACY_PORTABLE_LOCAL_CONFIG_FILENAME), PortableConfigSchema, params.read);
6953
+ const rules = (directoryRules?.config.rules ?? []).map((rule, index) => ({ rule, index })).filter(({ rule }) => normaliseRulePath(rule.path, params.home) === dir).map(({ rule, index }) => {
6954
+ const entryOrder = directoryRules?.ruleEntryOrders[index];
6877
6955
  return {
6878
- ok: false,
6879
- message: `agent-shim: every member of pool "${poolName}" is refused right now; the earliest, ${earliestReturn.identity}, returns at ${until} (in ${formatAge(earliestReturn.atMs - nowMs)}). Pass --wait to sleep until then.`
6956
+ rule,
6957
+ ...entryOrder === void 0 ? {} : { entryOrder },
6958
+ filepath: params.paths.directoryRulesFile
6880
6959
  };
6960
+ });
6961
+ if (portable === void 0 && portableLocal === void 0 && rules.length === 0) {
6962
+ continue;
6963
+ }
6964
+ levels.push({
6965
+ dir,
6966
+ ...portable === void 0 ? {} : { portable: { config: portable.config, entryOrder: portable.entryOrder, filepath: portable.filepath } },
6967
+ ...rules.length === 0 ? {} : { rules },
6968
+ ...portableLocal === void 0 ? {} : { portableLocal: { config: portableLocal.config, entryOrder: portableLocal.entryOrder, filepath: portableLocal.filepath } }
6969
+ });
6970
+ }
6971
+ const input = {
6972
+ home: params.home,
6973
+ ...globalConfig === void 0 ? {} : { globalConfig: { config: globalConfig.config, entryOrder: globalConfig.entryOrder, filepath: globalConfig.filepath } },
6974
+ ...params.baseConfigProfile === void 0 ? {} : { baseConfigProfile: params.baseConfigProfile },
6975
+ loadProfile,
6976
+ levels,
6977
+ ...params.cliOverride === void 0 ? {} : { cliOverride: params.cliOverride }
6978
+ };
6979
+ return { input, ...globalConfig === void 0 ? {} : { globalConfig: globalConfig.config } };
6980
+ }
6981
+ function readDirectorySelections(loaded) {
6982
+ let identity;
6983
+ let configProfile;
6984
+ for (const level of loaded.input.levels ?? []) {
6985
+ for (const source of [level.portable?.config, ...(level.rules ?? []).map((entry) => entry.rule), level.portableLocal?.config]) {
6986
+ if (source === void 0) {
6987
+ continue;
6988
+ }
6989
+ if (source.identity !== void 0) {
6990
+ identity = source.identity;
6991
+ }
6992
+ if (source.configProfile !== void 0) {
6993
+ configProfile = source.configProfile;
6994
+ }
6881
6995
  }
6882
- log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
6883
- params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
6884
6996
  }
6997
+ return { ...identity === void 0 ? {} : { identity }, ...configProfile === void 0 ? {} : { configProfile } };
6885
6998
  }
6886
6999
 
6887
- // src/poolPickReport.ts
6888
- function candidateView(candidate) {
6889
- return {
6890
- identity: candidate.identity,
6891
- class: candidate.class,
6892
- ...candidate.score === void 0 ? {} : { score: candidate.score },
6893
- feasible: candidate.feasible,
6894
- ...candidate.blockedUntilMs === void 0 ? {} : { blockedUntil: new Date(candidate.blockedUntilMs).toISOString() },
6895
- plan: candidate.plan,
6896
- reasons: candidate.reasons
6897
- };
7000
+ // src/realCredentialCache.ts
7001
+ import { spawnSync } from "node:child_process";
7002
+ import fs7 from "node:fs";
7003
+ import path20 from "node:path";
7004
+ var KEYCHAIN_CACHE_SERVICE = "agent-shim-credential-cache";
7005
+ var CACHE_DIR_MODE = 448;
7006
+ var CACHE_FILE_MODE = 384;
7007
+ function cacheFileNameFor(owner) {
7008
+ return `${owner.replace(/[^A-Za-z0-9._-]/g, "_")}.json`;
6898
7009
  }
6899
- function collectPoolPick(params) {
6900
- const { present, missing } = splitMembers(params.pool, params.paths, params.fs);
6901
- const sticky = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.directory);
6902
- const ranking = rankPoolFromStore({
6903
- fs: params.usageFs,
6904
- paths: params.paths,
6905
- identities: present,
6906
- nowMs: params.nowMs,
6907
- resuming: false,
6908
- ...sticky.sticky === void 0 ? {} : { sticky: sticky.sticky }
6909
- });
7010
+ function parseEntry(raw) {
7011
+ let json;
7012
+ try {
7013
+ json = JSON.parse(raw);
7014
+ } catch {
7015
+ return void 0;
7016
+ }
7017
+ const parsed = CachedCredentialSchema.safeParse(json);
7018
+ return parsed.success ? parsed.data : void 0;
7019
+ }
7020
+ function requireKeychain() {
7021
+ if (process.platform !== "darwin") {
7022
+ throw new Error("the keychain credential store needs macOS; use --credential-cache-store file");
7023
+ }
7024
+ }
7025
+ function security(args, stdin) {
7026
+ const result = spawnSync("security", args, { encoding: "utf8", ...stdin === void 0 ? {} : { input: stdin }, stdio: ["pipe", "pipe", "ignore"] });
7027
+ return { status: result.status, stdout: result.stdout };
7028
+ }
7029
+ function createRealCredentialCache(root) {
7030
+ const dir = path20.join(root, "credential-cache");
7031
+ const filePath = (owner) => path20.join(dir, cacheFileNameFor(owner));
6910
7032
  return {
6911
- pool: params.poolName,
6912
- directory: params.directory,
6913
- ...ranking.pick === void 0 ? {} : { pick: ranking.pick.identity },
6914
- candidates: ranking.candidates.map(candidateView),
6915
- missing,
6916
- ...ranking.earliestReturn === void 0 ? {} : { earliestReturn: { identity: ranking.earliestReturn.identity, at: new Date(ranking.earliestReturn.atMs).toISOString() } },
6917
- ...sticky.problem === void 0 ? {} : { stickyProblem: sticky.problem }
7033
+ read(store, owner) {
7034
+ if (store === "file") {
7035
+ try {
7036
+ return parseEntry(fs7.readFileSync(filePath(owner), "utf8"));
7037
+ } catch {
7038
+ return void 0;
7039
+ }
7040
+ }
7041
+ requireKeychain();
7042
+ const result = security(["find-generic-password", "-s", KEYCHAIN_CACHE_SERVICE, "-a", owner, "-w"]);
7043
+ return result.status === 0 ? parseEntry(Buffer.from(result.stdout.trim(), "base64").toString("utf8")) : void 0;
7044
+ },
7045
+ write(store, owner, entry) {
7046
+ const serialised = JSON.stringify(entry);
7047
+ if (store === "file") {
7048
+ fs7.mkdirSync(dir, { recursive: true, mode: CACHE_DIR_MODE });
7049
+ fs7.chmodSync(dir, CACHE_DIR_MODE);
7050
+ const target = filePath(owner);
7051
+ const temp = `${target}.${String(process.pid)}.tmp`;
7052
+ fs7.writeFileSync(temp, serialised, { mode: CACHE_FILE_MODE });
7053
+ fs7.renameSync(temp, target);
7054
+ return;
7055
+ }
7056
+ requireKeychain();
7057
+ const encoded = Buffer.from(serialised, "utf8").toString("base64");
7058
+ const result = security(["-i"], `add-generic-password -U -s ${KEYCHAIN_CACHE_SERVICE} -a ${JSON.stringify(owner)} -w ${encoded}
7059
+ `);
7060
+ if (result.status !== 0) {
7061
+ throw new Error(`could not write the keychain credential cache for ${owner}`);
7062
+ }
7063
+ },
7064
+ remove(store, owner) {
7065
+ if (store === "file") {
7066
+ fs7.rmSync(filePath(owner), { force: true });
7067
+ return;
7068
+ }
7069
+ requireKeychain();
7070
+ security(["delete-generic-password", "-s", KEYCHAIN_CACHE_SERVICE, "-a", owner]);
7071
+ }
6918
7072
  };
6919
7073
  }
7074
+ function realCredentialCacheEnv(paths) {
7075
+ return { port: createRealCredentialCache(paths.root), platform: process.platform, now: () => Date.now() };
7076
+ }
6920
7077
 
6921
7078
  // src/realPorts.ts
6922
- import { spawn, spawnSync } from "node:child_process";
7079
+ import { spawn, spawnSync as spawnSync2 } from "node:child_process";
6923
7080
  import { createHash as createHash5 } from "node:crypto";
6924
- import fs7 from "node:fs";
7081
+ import fs8 from "node:fs";
6925
7082
  import os3 from "node:os";
6926
- import path21 from "node:path";
7083
+ import path22 from "node:path";
6927
7084
  import { isSea } from "node:sea";
6928
7085
 
6929
7086
  // src/versionDiscovery.ts
6930
7087
  import os2 from "node:os";
6931
- import path20 from "node:path";
7088
+ import path21 from "node:path";
6932
7089
  function defaultVersionsDir() {
6933
- return path20.join(os2.homedir(), ".local", "share", "claude", "versions");
7090
+ return path21.join(os2.homedir(), ".local", "share", "claude", "versions");
6934
7091
  }
6935
7092
  var NUMERIC_DOTTED_VERSION_RE = /^\d+(?:\.\d+)*$/;
6936
7093
  function isNumericDottedVersion(name) {
@@ -6972,7 +7129,7 @@ function discoverClaudeBinary(options) {
6972
7129
  const highestVersion = pickHighestVersion(entries);
6973
7130
  if (highestVersion !== void 0) {
6974
7131
  return {
6975
- path: path20.join(versionsDir, highestVersion),
7132
+ path: path21.join(versionsDir, highestVersion),
6976
7133
  source: "versions-dir",
6977
7134
  version: highestVersion
6978
7135
  };
@@ -6995,7 +7152,7 @@ function isEnoent3(error) {
6995
7152
  var realFsPort = {
6996
7153
  readFileUtf8(filePath) {
6997
7154
  try {
6998
- return fs7.readFileSync(filePath, "utf8");
7155
+ return fs8.readFileSync(filePath, "utf8");
6999
7156
  } catch (error) {
7000
7157
  if (isEnoent3(error)) {
7001
7158
  return void 0;
@@ -7006,7 +7163,7 @@ var realFsPort = {
7006
7163
  readConfigFile: cosmiconfigReader(),
7007
7164
  readdir(dirPath) {
7008
7165
  try {
7009
- return fs7.readdirSync(dirPath);
7166
+ return fs8.readdirSync(dirPath);
7010
7167
  } catch (error) {
7011
7168
  if (isEnoent3(error)) {
7012
7169
  return [];
@@ -7025,7 +7182,7 @@ var realFarmFs = {
7025
7182
  lstat(filePath) {
7026
7183
  let stat;
7027
7184
  try {
7028
- stat = fs7.lstatSync(filePath);
7185
+ stat = fs8.lstatSync(filePath);
7029
7186
  } catch (error) {
7030
7187
  if (isEnoent3(error)) {
7031
7188
  return void 0;
@@ -7037,7 +7194,7 @@ var realFarmFs = {
7037
7194
  },
7038
7195
  readdir(dirPath) {
7039
7196
  try {
7040
- return fs7.readdirSync(dirPath);
7197
+ return fs8.readdirSync(dirPath);
7041
7198
  } catch (error) {
7042
7199
  if (isEnoent3(error) || isErrorWithCode(error, "ENOTDIR")) {
7043
7200
  return [];
@@ -7046,23 +7203,23 @@ var realFarmFs = {
7046
7203
  }
7047
7204
  },
7048
7205
  mkdirp(dirPath) {
7049
- fs7.mkdirSync(dirPath, { recursive: true });
7206
+ fs8.mkdirSync(dirPath, { recursive: true });
7050
7207
  },
7051
7208
  symlink(target, linkPath) {
7052
- fs7.symlinkSync(target, linkPath);
7209
+ fs8.symlinkSync(target, linkPath);
7053
7210
  },
7054
7211
  rename(from, to) {
7055
- fs7.renameSync(from, to);
7212
+ fs8.renameSync(from, to);
7056
7213
  },
7057
7214
  removeRecursive(targetPath) {
7058
- fs7.rmSync(targetPath, { recursive: true, force: true });
7215
+ fs8.rmSync(targetPath, { recursive: true, force: true });
7059
7216
  },
7060
7217
  copyRecursive(from, to) {
7061
- fs7.cpSync(from, to, { recursive: true, force: true, verbatimSymlinks: true });
7218
+ fs8.cpSync(from, to, { recursive: true, force: true, verbatimSymlinks: true });
7062
7219
  },
7063
7220
  readFileUtf8(filePath) {
7064
7221
  try {
7065
- return fs7.readFileSync(filePath, "utf8");
7222
+ return fs8.readFileSync(filePath, "utf8");
7066
7223
  } catch (error) {
7067
7224
  if (isEnoent3(error)) {
7068
7225
  return void 0;
@@ -7071,36 +7228,36 @@ var realFarmFs = {
7071
7228
  }
7072
7229
  },
7073
7230
  writeFileUtf8(filePath, contents) {
7074
- fs7.writeFileSync(filePath, contents, "utf8");
7231
+ fs8.writeFileSync(filePath, contents, "utf8");
7075
7232
  },
7076
7233
  mkdirPrivate(dirPath) {
7077
- fs7.mkdirSync(dirPath, { recursive: true, mode: OWNER_ONLY_DIR_MODE });
7078
- fs7.chmodSync(dirPath, OWNER_ONLY_DIR_MODE);
7234
+ fs8.mkdirSync(dirPath, { recursive: true, mode: OWNER_ONLY_DIR_MODE });
7235
+ fs8.chmodSync(dirPath, OWNER_ONLY_DIR_MODE);
7079
7236
  },
7080
7237
  writeFilePrivate(filePath, contents) {
7081
7238
  const temp = `${filePath}.${String(process.pid)}.tmp`;
7082
- fs7.writeFileSync(temp, contents, { encoding: "utf8", mode: OWNER_ONLY_FILE_MODE });
7083
- fs7.chmodSync(temp, OWNER_ONLY_FILE_MODE);
7084
- fs7.renameSync(temp, filePath);
7239
+ fs8.writeFileSync(temp, contents, { encoding: "utf8", mode: OWNER_ONLY_FILE_MODE });
7240
+ fs8.chmodSync(temp, OWNER_ONLY_FILE_MODE);
7241
+ fs8.renameSync(temp, filePath);
7085
7242
  },
7086
7243
  appendFilePrivate(filePath, contents) {
7087
- const descriptor = fs7.openSync(filePath, "a", OWNER_ONLY_FILE_MODE);
7244
+ const descriptor = fs8.openSync(filePath, "a", OWNER_ONLY_FILE_MODE);
7088
7245
  try {
7089
- if ((fs7.fstatSync(descriptor).mode & PERMISSION_BITS2) !== OWNER_ONLY_FILE_MODE) {
7090
- fs7.fchmodSync(descriptor, OWNER_ONLY_FILE_MODE);
7246
+ if ((fs8.fstatSync(descriptor).mode & PERMISSION_BITS2) !== OWNER_ONLY_FILE_MODE) {
7247
+ fs8.fchmodSync(descriptor, OWNER_ONLY_FILE_MODE);
7091
7248
  }
7092
7249
  const bytes = Buffer.from(contents, "utf8");
7093
7250
  let written = 0;
7094
7251
  while (written < bytes.length) {
7095
- written += fs7.writeSync(descriptor, bytes, written, bytes.length - written);
7252
+ written += fs8.writeSync(descriptor, bytes, written, bytes.length - written);
7096
7253
  }
7097
7254
  } finally {
7098
- fs7.closeSync(descriptor);
7255
+ fs8.closeSync(descriptor);
7099
7256
  }
7100
7257
  },
7101
7258
  writeFileExclusive(filePath, contents) {
7102
7259
  try {
7103
- fs7.writeFileSync(filePath, contents, { encoding: "utf8", flag: "wx" });
7260
+ fs8.writeFileSync(filePath, contents, { encoding: "utf8", flag: "wx" });
7104
7261
  return true;
7105
7262
  } catch (error) {
7106
7263
  if (isErrorWithCode(error, "EEXIST")) {
@@ -7111,7 +7268,7 @@ var realFarmFs = {
7111
7268
  },
7112
7269
  hashFile(filePath) {
7113
7270
  try {
7114
- return createHash5("sha256").update(fs7.readFileSync(filePath)).digest("hex");
7271
+ return createHash5("sha256").update(fs8.readFileSync(filePath)).digest("hex");
7115
7272
  } catch (error) {
7116
7273
  if (isEnoent3(error) || isErrorWithCode(error, "EISDIR")) {
7117
7274
  return void 0;
@@ -7133,7 +7290,7 @@ function realIsProcessAlive(pid) {
7133
7290
  }
7134
7291
  }
7135
7292
  function realIsProcessZombie(pid) {
7136
- const result = spawnSync("ps", ["-o", "stat=", "-p", String(pid)], { encoding: "utf8" });
7293
+ const result = spawnSync2("ps", ["-o", "stat=", "-p", String(pid)], { encoding: "utf8" });
7137
7294
  return result.status === 0 && result.stdout.trim().startsWith("Z");
7138
7295
  }
7139
7296
  function realIsProcessRunning(pid) {
@@ -7141,17 +7298,17 @@ function realIsProcessRunning(pid) {
7141
7298
  }
7142
7299
  var realRunPort = {
7143
7300
  run(command, args) {
7144
- const result = spawnSync(command, [...args], { encoding: "utf8" });
7301
+ const result = spawnSync2(command, [...args], { encoding: "utf8" });
7145
7302
  return { status: result.status, stdout: result.stdout, stderr: result.stderr };
7146
7303
  }
7147
7304
  };
7148
7305
  function expandHome2(filePath) {
7149
- return filePath.startsWith("~/") ? path21.join(os3.homedir(), filePath.slice(1)) : filePath;
7306
+ return filePath.startsWith("~/") ? path22.join(os3.homedir(), filePath.slice(1)) : filePath;
7150
7307
  }
7151
7308
  var MACOS_GUI_SESSION_MANAGER = "Aqua";
7152
7309
  function hasDesktopSession() {
7153
7310
  if (process.platform === "darwin") {
7154
- const result = spawnSync("launchctl", ["managername"], { encoding: "utf8" });
7311
+ const result = spawnSync2("launchctl", ["managername"], { encoding: "utf8" });
7155
7312
  return result.status === 0 && result.stdout.trim() === MACOS_GUI_SESSION_MANAGER;
7156
7313
  }
7157
7314
  if (process.platform === "win32") {
@@ -7165,8 +7322,8 @@ var realCredentialPort = {
7165
7322
  let content;
7166
7323
  let stat;
7167
7324
  try {
7168
- stat = fs7.statSync(resolved);
7169
- content = fs7.readFileSync(resolved, "utf8");
7325
+ stat = fs8.statSync(resolved);
7326
+ content = fs8.readFileSync(resolved, "utf8");
7170
7327
  } catch (error) {
7171
7328
  if (isEnoent3(error)) {
7172
7329
  return { found: false };
@@ -7177,7 +7334,7 @@ var realCredentialPort = {
7177
7334
  },
7178
7335
  runCommand(argv, options) {
7179
7336
  const [command, ...args] = argv;
7180
- const result = spawnSync(command, args, {
7337
+ const result = spawnSync2(command, args, {
7181
7338
  encoding: "utf8",
7182
7339
  timeout: options.timeoutMs,
7183
7340
  stdio: options.interactive ? ["inherit", "pipe", "inherit"] : ["ignore", "pipe", "pipe"]
@@ -7219,8 +7376,8 @@ function spawnDaemonThrough(executable) {
7219
7376
  return (paths, subcommand, logPath) => spawnDetachedDaemon({ command: executable, args: [subcommand] }, paths, subcommand, logPath);
7220
7377
  }
7221
7378
  function spawnDetachedDaemon(invocation, paths, subcommand, logPath) {
7222
- fs7.mkdirSync(path21.dirname(logPath), { recursive: true });
7223
- const logFd = fs7.openSync(logPath, "a");
7379
+ fs8.mkdirSync(path22.dirname(logPath), { recursive: true });
7380
+ const logFd = fs8.openSync(logPath, "a");
7224
7381
  try {
7225
7382
  const child = spawn(invocation.command, invocation.args, detachedDaemonSpawnOptions(logFd, { ...process.env, AGENT_SHIM_HOME: paths.root }));
7226
7383
  child.unref();
@@ -7229,7 +7386,7 @@ function spawnDetachedDaemon(invocation, paths, subcommand, logPath) {
7229
7386
  }
7230
7387
  return child.pid;
7231
7388
  } finally {
7232
- fs7.closeSync(logFd);
7389
+ fs8.closeSync(logFd);
7233
7390
  }
7234
7391
  }
7235
7392
  function spawnHeadroomSupervisor(paths, spawnDaemon) {
@@ -7247,7 +7404,7 @@ var realHeadroomSocketTrust = {
7247
7404
  lstat: (target) => {
7248
7405
  let stat;
7249
7406
  try {
7250
- stat = fs7.lstatSync(target);
7407
+ stat = fs8.lstatSync(target);
7251
7408
  } catch (error) {
7252
7409
  if (isEnoent3(error)) {
7253
7410
  return void 0;
@@ -7293,7 +7450,7 @@ var EXECUTE_BITS_MASK = 73;
7293
7450
  function listVersionsDir(dir) {
7294
7451
  let entries;
7295
7452
  try {
7296
- entries = fs7.readdirSync(dir, { withFileTypes: true });
7453
+ entries = fs8.readdirSync(dir, { withFileTypes: true });
7297
7454
  } catch (error) {
7298
7455
  if (isEnoent3(error)) {
7299
7456
  return [];
@@ -7301,8 +7458,8 @@ function listVersionsDir(dir) {
7301
7458
  throw error;
7302
7459
  }
7303
7460
  return entries.filter((entry) => entry.isFile()).map((entry) => {
7304
- const fullPath = path21.join(dir, entry.name);
7305
- const stat = fs7.statSync(fullPath);
7461
+ const fullPath = path22.join(dir, entry.name);
7462
+ const stat = fs8.statSync(fullPath);
7306
7463
  return {
7307
7464
  name: entry.name,
7308
7465
  isFile: true,
@@ -7316,7 +7473,7 @@ function windowsPathExtensions(pathext) {
7316
7473
  return (pathext ?? WINDOWS_DEFAULT_PATHEXT).split(";").map((ext) => ext.trim()).filter((ext) => ext.length > 0);
7317
7474
  }
7318
7475
  function resolveExecutableCandidate(dir, name, env) {
7319
- const candidate = path21.join(dir, name);
7476
+ const candidate = path22.join(dir, name);
7320
7477
  if (env.platform === "win32") {
7321
7478
  const extensions = windowsPathExtensions(env.pathext);
7322
7479
  const alreadyHasRecognisedExtension = extensions.some((ext) => candidate.toLowerCase().endsWith(ext.toLowerCase()));
@@ -7336,7 +7493,7 @@ function resolveExecutableCandidate(dir, name, env) {
7336
7493
  }
7337
7494
  function realStatFileMode(candidate) {
7338
7495
  try {
7339
- const stat = fs7.statSync(candidate);
7496
+ const stat = fs8.statSync(candidate);
7340
7497
  return stat.isFile() ? stat.mode : void 0;
7341
7498
  } catch (error) {
7342
7499
  if (isEnoent3(error)) {
@@ -7365,13 +7522,13 @@ function resolveOwnExecutablePath(env) {
7365
7522
  if (env.argv1 !== void 0 && !env.argv1.includes("/") && !env.argv1.includes("\\")) {
7366
7523
  return searchPathForExecutable(env.pathDirs, env.argv1, env.findExecutableInDir) ?? env.execPath;
7367
7524
  }
7368
- const candidate = env.argv1 === void 0 ? env.execPath : path21.resolve(env.cwd, env.argv1);
7369
- const candidateDir = path21.dirname(candidate);
7370
- const candidateDirOnPath = env.pathDirs.some((dir) => path21.resolve(dir) === candidateDir);
7525
+ const candidate = env.argv1 === void 0 ? env.execPath : path22.resolve(env.cwd, env.argv1);
7526
+ const candidateDir = path22.dirname(candidate);
7527
+ const candidateDirOnPath = env.pathDirs.some((dir) => path22.resolve(dir) === candidateDir);
7371
7528
  if (candidateDirOnPath) {
7372
7529
  return candidate;
7373
7530
  }
7374
- const viaPath = searchPathForExecutable(env.pathDirs, path21.basename(candidate), env.findExecutableInDir);
7531
+ const viaPath = searchPathForExecutable(env.pathDirs, path22.basename(candidate), env.findExecutableInDir);
7375
7532
  return viaPath ?? candidate;
7376
7533
  }
7377
7534
  function realOwnExecutablePath() {
@@ -7379,7 +7536,7 @@ function realOwnExecutablePath() {
7379
7536
  argv1: process.argv[1],
7380
7537
  execPath: process.execPath,
7381
7538
  cwd: process.cwd(),
7382
- pathDirs: (process.env.PATH ?? "").split(path21.delimiter).filter((dir) => dir !== ""),
7539
+ pathDirs: (process.env.PATH ?? "").split(path22.delimiter).filter((dir) => dir !== ""),
7383
7540
  findExecutableInDir
7384
7541
  });
7385
7542
  }
@@ -7392,15 +7549,15 @@ function realContentSourcePath() {
7392
7549
  function realResolveClaudeBinary(isOwnBinary) {
7393
7550
  return () => discoverClaudeBinary({
7394
7551
  listVersionsDir,
7395
- pathDirs: (process.env.PATH ?? "").split(path21.delimiter).filter((dir) => dir !== ""),
7552
+ pathDirs: (process.env.PATH ?? "").split(path22.delimiter).filter((dir) => dir !== ""),
7396
7553
  findExecutableInDir,
7397
7554
  isOwnBinary
7398
7555
  });
7399
7556
  }
7400
7557
 
7401
7558
  // src/checkReport.ts
7402
- import path22 from "node:path";
7403
- import { z as z10 } from "zod";
7559
+ import path23 from "node:path";
7560
+ import { z as z12 } from "zod";
7404
7561
 
7405
7562
  // src/cli/bool.ts
7406
7563
  function parseBoolWord(input) {
@@ -7515,9 +7672,9 @@ function lookupKeychainService(run, farmRoot) {
7515
7672
  note: serviceName === void 0 ? `A Keychain entry was found for account "${farmRoot}" but its service name could not be parsed from security's output.` : `Keychain service name for this identity: "${serviceName}".`
7516
7673
  };
7517
7674
  }
7518
- var SettingsSecretsShapeSchema = z10.looseObject({
7519
- env: z10.record(z10.string(), z10.unknown()).optional(),
7520
- hooks: z10.record(z10.string(), z10.array(z10.unknown())).optional()
7675
+ var SettingsSecretsShapeSchema = z12.looseObject({
7676
+ env: z12.record(z12.string(), z12.unknown()).optional(),
7677
+ hooks: z12.record(z12.string(), z12.array(z12.unknown())).optional()
7521
7678
  });
7522
7679
  function hookCommandCountOf(group) {
7523
7680
  if (typeof group !== "object" || group === null) {
@@ -7569,14 +7726,23 @@ function inspectSettingsExposure(params) {
7569
7726
  }
7570
7727
  return reports;
7571
7728
  }
7572
- function buildCredentialReport(provider, identity) {
7729
+ function cachedState(cacheEnv, owner, block) {
7730
+ if (cacheEnv === void 0 || block.cache === void 0) {
7731
+ return void 0;
7732
+ }
7733
+ return describeCachedCredential({ port: cacheEnv.port, platform: cacheEnv.platform, owner, cache: block.cache, nowMs: cacheEnv.now() });
7734
+ }
7735
+ function buildCredentialReport(provider, identity, identityName, cacheEnv) {
7573
7736
  const identityCredential = identity?.credential === void 0 ? void 0 : summariseCredential(identity.credential);
7737
+ const identityCached = identity?.credential === void 0 || identityName === void 0 ? void 0 : cachedState(cacheEnv, `identity ${identityName}`, identity.credential);
7738
+ const providerCached = provider?.definition === void 0 ? void 0 : cachedState(cacheEnv, `provider ${provider.name}`, provider.definition.credential);
7574
7739
  return {
7575
7740
  applies: provider !== void 0 ? "provider" : identityCredential !== void 0 ? "identity" : "stored-login",
7576
7741
  ...provider === void 0 ? {} : {
7577
- provider: provider.problem === void 0 ? { name: provider.name, credential: summariseCredential(provider.definition.credential) } : { name: provider.name, problem: provider.problem }
7742
+ provider: provider.problem === void 0 ? { name: provider.name, credential: summariseCredential(provider.definition.credential), ...providerCached === void 0 ? {} : { cached: providerCached } } : { name: provider.name, problem: provider.problem }
7578
7743
  },
7579
- ...identityCredential === void 0 ? {} : { identity: identityCredential }
7744
+ ...identityCredential === void 0 ? {} : { identity: identityCredential },
7745
+ ...identityCached === void 0 ? {} : { identityCached }
7580
7746
  };
7581
7747
  }
7582
7748
  function runCheck(params) {
@@ -7615,7 +7781,7 @@ function runCheck(params) {
7615
7781
  ambientCredential,
7616
7782
  ...keychain === void 0 ? {} : { keychain },
7617
7783
  settingsExposure,
7618
- credential: buildCredentialReport(params.provider, params.identity)
7784
+ credential: buildCredentialReport(params.provider, params.identity, params.identityName, params.credentialCache)
7619
7785
  };
7620
7786
  }
7621
7787
  function formatCheckReport(report) {
@@ -7653,12 +7819,12 @@ function formatCheckReport(report) {
7653
7819
  const { credential } = report;
7654
7820
  if (credential.provider !== void 0) {
7655
7821
  lines.push(
7656
- ` Provider ${credential.provider.name}: ${credential.provider.credential === void 0 ? `unusable: ${credential.provider.problem ?? ""}` : formatCredentialSummary(credential.provider.credential)}`
7822
+ ` Provider ${credential.provider.name}: ${credential.provider.credential === void 0 ? `unusable: ${credential.provider.problem ?? ""}` : formatCredentialSummary(credential.provider.credential)}${credential.provider.cached === void 0 ? "" : `; ${formatCachedCredentialState(credential.provider.cached)}`}`
7657
7823
  );
7658
7824
  }
7659
7825
  if (credential.identity !== void 0) {
7660
7826
  lines.push(
7661
- ` Identity ${report.identityName ?? "(none)"}: ${formatCredentialSummary(credential.identity)}${credential.applies === "provider" ? " (not used: the provider's credential applies)" : ""}`
7827
+ ` Identity ${report.identityName ?? "(none)"}: ${formatCredentialSummary(credential.identity)}${credential.identityCached === void 0 ? "" : `; ${formatCachedCredentialState(credential.identityCached)}`}${credential.applies === "provider" ? " (not used: the provider's credential applies)" : ""}`
7662
7828
  );
7663
7829
  }
7664
7830
  if (credential.applies === "stored-login") {
@@ -7767,10 +7933,10 @@ function collectCheckReport(params) {
7767
7933
  }).input;
7768
7934
  const providerName = cascadeProviderName(cascade);
7769
7935
  const provider = providerName === void 0 ? void 0 : loadCheckProvider(paths, providerName);
7770
- const farmRoot = identityDecision.name === void 0 ? void 0 : path22.join(paths.identitiesDir, identityDecision.name);
7936
+ const farmRoot = identityDecision.name === void 0 ? void 0 : path23.join(paths.identitiesDir, identityDecision.name);
7771
7937
  const settingsFiles = {
7772
- "settings.json": realFsPort.readFileUtf8(path22.join(claudeHome, "settings.json")),
7773
- "settings.local.json": realFsPort.readFileUtf8(path22.join(claudeHome, "settings.local.json"))
7938
+ "settings.json": realFsPort.readFileUtf8(path23.join(claudeHome, "settings.json")),
7939
+ "settings.local.json": realFsPort.readFileUtf8(path23.join(claudeHome, "settings.local.json"))
7774
7940
  };
7775
7941
  const report = runCheck({
7776
7942
  cwd,
@@ -7791,6 +7957,7 @@ function collectCheckReport(params) {
7791
7957
  ...loadedIdentity === void 0 ? {} : { identity: loadedIdentity.config },
7792
7958
  settingsFiles,
7793
7959
  run: realRunPort,
7960
+ credentialCache: realCredentialCacheEnv(paths),
7794
7961
  ...farmRoot === void 0 ? {} : { farmRoot },
7795
7962
  platform: process.platform,
7796
7963
  ...provider === void 0 ? {} : { provider }
@@ -7799,23 +7966,23 @@ function collectCheckReport(params) {
7799
7966
  }
7800
7967
 
7801
7968
  // src/claudeShim.ts
7802
- import fs8 from "node:fs";
7803
- import path23 from "node:path";
7804
- import { z as z11 } from "zod";
7805
- var ClaudeShimStateSchema = z11.strictObject({
7806
- targetPath: z11.string().min(1),
7807
- method: z11.enum(["hardlink", "copy"]),
7808
- installedAtMs: z11.number()
7969
+ import fs9 from "node:fs";
7970
+ import path24 from "node:path";
7971
+ import { z as z13 } from "zod";
7972
+ var ClaudeShimStateSchema = z13.strictObject({
7973
+ targetPath: z13.string().min(1),
7974
+ method: z13.enum(["hardlink", "copy"]),
7975
+ installedAtMs: z13.number()
7809
7976
  });
7810
7977
  function commandFilename(ownExecutablePath, commandName) {
7811
7978
  return ownExecutablePath.toLowerCase().endsWith(".exe") ? `${commandName}.exe` : commandName;
7812
7979
  }
7813
7980
  function resolveOwnBinaryCheck(paths, contentSourcePath) {
7814
7981
  const state = readJson(paths.claudeShimFile, ClaudeShimStateSchema);
7815
- const recordedTarget = state === void 0 ? void 0 : path23.resolve(state.targetPath);
7982
+ const recordedTarget = state === void 0 ? void 0 : path24.resolve(state.targetPath);
7816
7983
  let ownStat;
7817
7984
  return (candidate) => {
7818
- if (recordedTarget !== void 0 && path23.resolve(candidate) === recordedTarget) {
7985
+ if (recordedTarget !== void 0 && path24.resolve(candidate) === recordedTarget) {
7819
7986
  return true;
7820
7987
  }
7821
7988
  ownStat ??= statOrUndefined(contentSourcePath);
@@ -7829,7 +7996,7 @@ function isEnoent4(error) {
7829
7996
  }
7830
7997
  function statOrUndefined(target) {
7831
7998
  try {
7832
- return fs8.statSync(target);
7999
+ return fs9.statSync(target);
7833
8000
  } catch (error) {
7834
8001
  if (isEnoent4(error)) {
7835
8002
  return void 0;
@@ -7837,22 +8004,22 @@ function statOrUndefined(target) {
7837
8004
  throw error;
7838
8005
  }
7839
8006
  }
7840
- var nodeLinkFs = { link: fs8.linkSync, copyFile: fs8.copyFileSync, chmod: fs8.chmodSync };
8007
+ var nodeLinkFs = { link: fs9.linkSync, copyFile: fs9.copyFileSync, chmod: fs9.chmodSync };
7841
8008
  function findPathShadow(params) {
7842
- const resolvedTargetDir = path23.resolve(params.targetDir);
8009
+ const resolvedTargetDir = path24.resolve(params.targetDir);
7843
8010
  for (const dir of params.pathDirs) {
7844
8011
  const found = params.findExecutableInDir(dir, params.targetFilename);
7845
8012
  if (found === void 0) {
7846
8013
  continue;
7847
8014
  }
7848
- return path23.resolve(dir) === resolvedTargetDir ? { status: "ok" } : { status: "shadowed", by: found };
8015
+ return path24.resolve(dir) === resolvedTargetDir ? { status: "ok" } : { status: "shadowed", by: found };
7849
8016
  }
7850
- return params.pathDirs.some((dir) => path23.resolve(dir) === resolvedTargetDir) ? { status: "ok" } : { status: "not-on-path" };
8017
+ return params.pathDirs.some((dir) => path24.resolve(dir) === resolvedTargetDir) ? { status: "ok" } : { status: "not-on-path" };
7851
8018
  }
7852
8019
 
7853
8020
  // src/doctorReport.ts
7854
- import fs9 from "node:fs";
7855
- import path24 from "node:path";
8021
+ import fs10 from "node:fs";
8022
+ import path25 from "node:path";
7856
8023
  function refinePathShadow(status, ownExecutablePath, realpath) {
7857
8024
  if (status.status !== "shadowed") {
7858
8025
  return status;
@@ -7876,7 +8043,7 @@ function validateJson(schema, input) {
7876
8043
  return { ok: true, data: result.data };
7877
8044
  }
7878
8045
  function pushPathResolution(push, resolution) {
7879
- const ownDir = path24.dirname(resolution.ownExecutablePath);
8046
+ const ownDir = path25.dirname(resolution.ownExecutablePath);
7880
8047
  switch (resolution.agentShim.status) {
7881
8048
  case "ok":
7882
8049
  push("path-resolution", "pass", `\`agent-shim\` on PATH resolves to this running executable, ${resolution.ownExecutablePath}.`, "agent-shim");
@@ -7918,7 +8085,14 @@ function pushPathResolution(push, resolution) {
7918
8085
  break;
7919
8086
  }
7920
8087
  }
7921
- function pushProvider(push, entry) {
8088
+ function cacheClause(cacheEnv, owner, block) {
8089
+ if (cacheEnv === void 0 || block.cache === void 0) {
8090
+ return { text: "", unreadable: false };
8091
+ }
8092
+ const state = describeCachedCredential({ port: cacheEnv.port, platform: cacheEnv.platform, owner, cache: block.cache, nowMs: cacheEnv.now() });
8093
+ return { text: `; ${formatCachedCredentialState(state)}`, unreadable: state.status === "unreadable" };
8094
+ }
8095
+ function pushProvider(push, entry, cacheEnv) {
7922
8096
  if (entry.raw === void 0) {
7923
8097
  push("provider", "fail", `${entry.path} is missing.`, entry.name);
7924
8098
  return;
@@ -7940,18 +8114,19 @@ function pushProvider(push, entry) {
7940
8114
  push("provider", "fail", new ConfigValidationError(entry.path, validated.error.issues).message, entry.name);
7941
8115
  return;
7942
8116
  }
7943
- push("provider", "pass", `${entry.name} is valid (${describeProviderEndpoint(validated.data)}, credential ${describeCredential(validated.data.credential)}).`, entry.name);
8117
+ const cache = cacheClause(cacheEnv, `provider ${entry.name}`, validated.data.credential);
8118
+ push("provider", cache.unreadable ? "fail" : "pass", `${entry.name} is valid (${describeProviderEndpoint(validated.data)}, credential ${describeCredential(validated.data.credential)})${cache.text}.`, entry.name);
7944
8119
  }
7945
8120
  function pushLegacyName(push, params) {
7946
8121
  const legacyVariables = Object.keys(params.env).filter((name) => name.startsWith(LEGACY_ENV_PREFIX) && params.env[name] !== void 0).sort();
7947
- if (legacyVariables.length === 0 && path24.basename(params.rootPath) !== LEGACY_HOME_DIRNAME) {
8122
+ if (legacyVariables.length === 0 && path25.basename(params.rootPath) !== LEGACY_HOME_DIRNAME) {
7948
8123
  push("legacy-name", "pass", "Nothing here still uses the former `claude-use` name.");
7949
8124
  return;
7950
8125
  }
7951
8126
  for (const name of legacyVariables) {
7952
8127
  push("legacy-name", "warn", `${name} is the former name of ${name.replace(LEGACY_ENV_PREFIX, ENV_PREFIX)}. Rename it; the old name still works but is only a fallback.`, name);
7953
8128
  }
7954
- if (path24.basename(params.rootPath) === LEGACY_HOME_DIRNAME) {
8129
+ if (path25.basename(params.rootPath) === LEGACY_HOME_DIRNAME) {
7955
8130
  push(
7956
8131
  "legacy-name",
7957
8132
  "warn",
@@ -8033,10 +8208,11 @@ function runDoctor(params) {
8033
8208
  );
8034
8209
  } else {
8035
8210
  const credential = validated.data.credential;
8211
+ const cache = credential === void 0 ? { text: "", unreadable: false } : cacheClause(params.credentialCache, `identity ${entry.name}`, credential);
8036
8212
  push(
8037
8213
  "identity",
8038
- "pass",
8039
- `${entry.name} is valid and authenticates with ${credential === void 0 ? "its stored login" : `credential ${describeCredential(credential)}`}.`,
8214
+ cache.unreadable ? "fail" : "pass",
8215
+ `${entry.name} is valid and authenticates with ${credential === void 0 ? "its stored login" : `credential ${describeCredential(credential)}`}${cache.text}.`,
8040
8216
  entry.name
8041
8217
  );
8042
8218
  }
@@ -8056,7 +8232,7 @@ function runDoctor(params) {
8056
8232
  return poolName === void 0 ? validIdentityNames.has(selector) : poolName in pools;
8057
8233
  };
8058
8234
  for (const entry of params.providers) {
8059
- pushProvider(push, entry);
8235
+ pushProvider(push, entry, params.credentialCache);
8060
8236
  }
8061
8237
  if (params.platform !== "darwin" || params.run === void 0) {
8062
8238
  push("keychain", "pass", "Skipped (not macOS).");
@@ -8250,27 +8426,27 @@ function formatDoctorReport(report) {
8250
8426
  }
8251
8427
  function realpathOrSelf(target) {
8252
8428
  try {
8253
- return fs9.realpathSync(target);
8429
+ return fs10.realpathSync(target);
8254
8430
  } catch {
8255
8431
  return target;
8256
8432
  }
8257
8433
  }
8258
8434
  function collectDoctorReport(params) {
8259
8435
  const { paths } = params;
8260
- const identityNames = fs9.existsSync(paths.identitiesDir) ? fs9.readdirSync(paths.identitiesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isIdentityDirectoryName(entry.name)).map((entry) => entry.name).sort() : [];
8436
+ const identityNames = fs10.existsSync(paths.identitiesDir) ? fs10.readdirSync(paths.identitiesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isIdentityDirectoryName(entry.name)).map((entry) => entry.name).sort() : [];
8261
8437
  const identities = identityNames.map((name) => {
8262
- const farmRoot = path24.join(paths.identitiesDir, name);
8263
- const identityPath = path24.join(farmRoot, "identity.json");
8438
+ const farmRoot = path25.join(paths.identitiesDir, name);
8439
+ const identityPath = path25.join(farmRoot, "identity.json");
8264
8440
  return { name, path: identityPath, raw: realFsPort.readFileUtf8(identityPath), farmRoot };
8265
8441
  });
8266
- const profileNames = fs9.existsSync(paths.configProfilesDir) ? fs9.readdirSync(paths.configProfilesDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).sort() : [];
8442
+ const profileNames = fs10.existsSync(paths.configProfilesDir) ? fs10.readdirSync(paths.configProfilesDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).sort() : [];
8267
8443
  const configProfiles = profileNames.map((name) => {
8268
- const profilePath = path24.join(paths.configProfilesDir, `${name}.json`);
8444
+ const profilePath = path25.join(paths.configProfilesDir, `${name}.json`);
8269
8445
  return { name, path: profilePath, raw: realFsPort.readFileUtf8(profilePath) };
8270
8446
  });
8271
- const providerNames = fs9.existsSync(paths.providersDir) ? fs9.readdirSync(paths.providersDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).sort() : [];
8447
+ const providerNames = fs10.existsSync(paths.providersDir) ? fs10.readdirSync(paths.providersDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).sort() : [];
8272
8448
  const providers = providerNames.map((name) => {
8273
- const providerPath = path24.join(paths.providersDir, `${name}.json`);
8449
+ const providerPath = path25.join(paths.providersDir, `${name}.json`);
8274
8450
  return { name, path: providerPath, raw: realFsPort.readFileUtf8(providerPath) };
8275
8451
  });
8276
8452
  const ownExecutablePath = realOwnExecutablePath();
@@ -8282,12 +8458,12 @@ function collectDoctorReport(params) {
8282
8458
  binaryDiscovery = { ok: false, message: error instanceof Error ? error.message : String(error) };
8283
8459
  }
8284
8460
  const shimState = readJson(paths.claudeShimFile, ClaudeShimStateSchema);
8285
- const pathDirs = (params.env.PATH ?? "").split(path24.delimiter).filter((dir) => dir !== "");
8461
+ const pathDirs = (params.env.PATH ?? "").split(path25.delimiter).filter((dir) => dir !== "");
8286
8462
  const claudeShimShadow = shimState === void 0 ? void 0 : refinePathShadow(
8287
8463
  findPathShadow({
8288
8464
  pathDirs,
8289
- targetDir: path24.dirname(shimState.targetPath),
8290
- targetFilename: path24.basename(shimState.targetPath),
8465
+ targetDir: path25.dirname(shimState.targetPath),
8466
+ targetFilename: path25.basename(shimState.targetPath),
8291
8467
  findExecutableInDir
8292
8468
  }),
8293
8469
  shimState.targetPath,
@@ -8295,6 +8471,7 @@ function collectDoctorReport(params) {
8295
8471
  );
8296
8472
  const report = runDoctor({
8297
8473
  env: params.env,
8474
+ credentialCache: realCredentialCacheEnv(paths),
8298
8475
  identities,
8299
8476
  configProfiles,
8300
8477
  providers,
@@ -8303,14 +8480,14 @@ function collectDoctorReport(params) {
8303
8480
  categoriesLocal: { path: paths.categoriesLocalFile, raw: realFsPort.readFileUtf8(paths.categoriesLocalFile) },
8304
8481
  activeIdentity: { path: paths.activeIdentityFile, raw: realFsPort.readFileUtf8(paths.activeIdentityFile) },
8305
8482
  binaryDiscovery,
8306
- claudeShim: { state: shimState, targetExists: shimState !== void 0 && fs9.existsSync(shimState.targetPath) },
8483
+ claudeShim: { state: shimState, targetExists: shimState !== void 0 && fs10.existsSync(shimState.targetPath) },
8307
8484
  rootPath: paths.root,
8308
8485
  pathResolution: {
8309
8486
  ownExecutablePath,
8310
8487
  agentShim: refinePathShadow(
8311
8488
  findPathShadow({
8312
8489
  pathDirs,
8313
- targetDir: path24.dirname(ownExecutablePath),
8490
+ targetDir: path25.dirname(ownExecutablePath),
8314
8491
  targetFilename: commandFilename(ownExecutablePath, "agent-shim"),
8315
8492
  findExecutableInDir
8316
8493
  }),
@@ -8331,7 +8508,7 @@ function collectDoctorReport(params) {
8331
8508
 
8332
8509
  // src/launcher.ts
8333
8510
  import { randomUUID as randomUUID4 } from "node:crypto";
8334
- import path26 from "node:path";
8511
+ import path27 from "node:path";
8335
8512
 
8336
8513
  // src/launcher/argv.ts
8337
8514
  var ConflictingIdentityError = class extends UsageError {
@@ -8583,7 +8760,7 @@ function buildCliOverride(params) {
8583
8760
  }
8584
8761
 
8585
8762
  // src/launcher/flags.ts
8586
- import path25 from "node:path";
8763
+ import path26 from "node:path";
8587
8764
 
8588
8765
  // src/headroom/headers.ts
8589
8766
  function mergeAnthropicCustomHeaders(existing, additions) {
@@ -8653,7 +8830,7 @@ function buildEnv(params) {
8653
8830
  const parentCustomHeaders = params.baseEnv.ANTHROPIC_CUSTOM_HEADERS;
8654
8831
  let providerCustomHeaders;
8655
8832
  if (!params.configDirEscapeHatch && params.resolvedIdentityName !== void 0) {
8656
- env.CLAUDE_CONFIG_DIR = path25.join(params.identitiesDir, params.resolvedIdentityName);
8833
+ env.CLAUDE_CONFIG_DIR = path26.join(params.identitiesDir, params.resolvedIdentityName);
8657
8834
  }
8658
8835
  if (params.provider !== void 0) {
8659
8836
  const providerEnv = params.provider.definition.env ?? {};
@@ -8814,7 +8991,7 @@ function prepareLaunch(params) {
8814
8991
  identityDefaultConfigProfile: loadedIdentity?.config.defaultConfigProfile,
8815
8992
  globalDefaultConfigProfile: params.globalDefaultConfigProfile
8816
8993
  });
8817
- if (configProfileDecision.name !== void 0 && params.allowMissingConfigProfile !== true && fs12.readConfigFile(path26.join(paths.configProfilesDir, `${configProfileDecision.name}.json`)) === void 0) {
8994
+ if (configProfileDecision.name !== void 0 && params.allowMissingConfigProfile !== true && fs12.readConfigFile(path27.join(paths.configProfilesDir, `${configProfileDecision.name}.json`)) === void 0) {
8818
8995
  log.error(
8819
8996
  `agent-shim: no configuration profile named "${configProfileDecision.name}" (selected via ${configProfileDecision.source}). Run \`agent-shim profile add ${configProfileDecision.name}\` first.`
8820
8997
  );
@@ -8987,84 +9164,6 @@ function prepareLaunch(params) {
8987
9164
  import os5 from "node:os";
8988
9165
  import { randomUUID as randomUUID5 } from "node:crypto";
8989
9166
 
8990
- // src/realCredentialCache.ts
8991
- import { spawnSync as spawnSync2 } from "node:child_process";
8992
- import fs10 from "node:fs";
8993
- import path27 from "node:path";
8994
- var KEYCHAIN_CACHE_SERVICE = "agent-shim-credential-cache";
8995
- var CACHE_DIR_MODE = 448;
8996
- var CACHE_FILE_MODE = 384;
8997
- function cacheFileNameFor(owner) {
8998
- return `${owner.replace(/[^A-Za-z0-9._-]/g, "_")}.json`;
8999
- }
9000
- function parseEntry(raw) {
9001
- let json;
9002
- try {
9003
- json = JSON.parse(raw);
9004
- } catch {
9005
- return void 0;
9006
- }
9007
- const parsed = CachedCredentialSchema.safeParse(json);
9008
- return parsed.success ? parsed.data : void 0;
9009
- }
9010
- function requireKeychain() {
9011
- if (process.platform !== "darwin") {
9012
- throw new Error("the keychain credential store needs macOS; use --credential-cache-store file");
9013
- }
9014
- }
9015
- function security(args, stdin) {
9016
- const result = spawnSync2("security", args, { encoding: "utf8", ...stdin === void 0 ? {} : { input: stdin }, stdio: ["pipe", "pipe", "ignore"] });
9017
- return { status: result.status, stdout: result.stdout };
9018
- }
9019
- function createRealCredentialCache(root) {
9020
- const dir = path27.join(root, "credential-cache");
9021
- const filePath = (owner) => path27.join(dir, cacheFileNameFor(owner));
9022
- return {
9023
- read(store, owner) {
9024
- if (store === "file") {
9025
- try {
9026
- return parseEntry(fs10.readFileSync(filePath(owner), "utf8"));
9027
- } catch {
9028
- return void 0;
9029
- }
9030
- }
9031
- requireKeychain();
9032
- const result = security(["find-generic-password", "-s", KEYCHAIN_CACHE_SERVICE, "-a", owner, "-w"]);
9033
- return result.status === 0 ? parseEntry(Buffer.from(result.stdout.trim(), "base64").toString("utf8")) : void 0;
9034
- },
9035
- write(store, owner, entry) {
9036
- const serialised = JSON.stringify(entry);
9037
- if (store === "file") {
9038
- fs10.mkdirSync(dir, { recursive: true, mode: CACHE_DIR_MODE });
9039
- fs10.chmodSync(dir, CACHE_DIR_MODE);
9040
- const target = filePath(owner);
9041
- const temp = `${target}.${String(process.pid)}.tmp`;
9042
- fs10.writeFileSync(temp, serialised, { mode: CACHE_FILE_MODE });
9043
- fs10.renameSync(temp, target);
9044
- return;
9045
- }
9046
- requireKeychain();
9047
- const encoded = Buffer.from(serialised, "utf8").toString("base64");
9048
- const result = security(["-i"], `add-generic-password -U -s ${KEYCHAIN_CACHE_SERVICE} -a ${JSON.stringify(owner)} -w ${encoded}
9049
- `);
9050
- if (result.status !== 0) {
9051
- throw new Error(`could not write the keychain credential cache for ${owner}`);
9052
- }
9053
- },
9054
- remove(store, owner) {
9055
- if (store === "file") {
9056
- fs10.rmSync(filePath(owner), { force: true });
9057
- return;
9058
- }
9059
- requireKeychain();
9060
- security(["delete-generic-password", "-s", KEYCHAIN_CACHE_SERVICE, "-a", owner]);
9061
- }
9062
- };
9063
- }
9064
- function realCredentialCacheEnv(paths) {
9065
- return { port: createRealCredentialCache(paths.root), platform: process.platform, now: () => Date.now() };
9066
- }
9067
-
9068
9167
  // src/frontdoor/realFrontDoorPort.ts
9069
9168
  import fs11 from "node:fs";
9070
9169
 
@@ -9328,8 +9427,11 @@ export {
9328
9427
  InvalidProviderNameError,
9329
9428
  LaunchRefusedError,
9330
9429
  LegacyProviderFileError,
9430
+ PROMPT_CACHE_TTL_MS,
9431
+ PlanClassSchema,
9331
9432
  PoolNameSchema,
9332
9433
  PoolNotFoundError,
9434
+ PoolPickReportSchema,
9333
9435
  PoolSchema,
9334
9436
  PortableConfigSchema,
9335
9437
  ProfileAlreadyExistsError,
@@ -9366,16 +9468,19 @@ export {
9366
9468
  checkReportToJson,
9367
9469
  collectCheckReport,
9368
9470
  collectDoctorReport,
9471
+ collectPoolPick,
9369
9472
  createLeafCache,
9370
9473
  createProfile,
9371
9474
  createRcControlHandler,
9372
9475
  createRcSessionTracker,
9373
9476
  describeProviderEndpoint,
9374
9477
  detectAmbientCredential,
9478
+ effectiveWindow,
9375
9479
  ensureCa,
9376
9480
  ensureFrontDoor,
9377
9481
  ensureHeadroom,
9378
9482
  evaluateAmbientCredentialGuard,
9483
+ evaluateWhen,
9379
9484
  formatAmbientCredentialGuardMessage,
9380
9485
  formatCheckReport,
9381
9486
  formatDoctorReport,
@@ -9393,13 +9498,16 @@ export {
9393
9498
  listProfiles,
9394
9499
  listProviders,
9395
9500
  listUsageSnapshots,
9501
+ matchBranch,
9396
9502
  mintLeaf,
9397
9503
  observingRoutedRoute,
9398
9504
  parseConnectTarget,
9505
+ planOf,
9399
9506
  prepareClaudeLaunch,
9400
9507
  prepareLaunch,
9401
9508
  profileExists,
9402
9509
  providerExists,
9510
+ rankPool,
9403
9511
  rcEventWriteResultFromAnswer,
9404
9512
  rcObserverAsPassthrough,
9405
9513
  readActiveIdentity,