agent-shim 8.33.0 → 8.35.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/cli.cjs +212 -31
- package/dist/index.cjs +80 -24
- package/dist/index.mjs +80 -24
- package/dist/types/index.d.ts +263 -7
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -18047,7 +18047,7 @@ var require_api_pipeline = __commonJS({
|
|
|
18047
18047
|
var {
|
|
18048
18048
|
Readable: Readable2,
|
|
18049
18049
|
Duplex,
|
|
18050
|
-
PassThrough
|
|
18050
|
+
PassThrough: PassThrough2
|
|
18051
18051
|
} = require("node:stream");
|
|
18052
18052
|
var assert2 = require("node:assert");
|
|
18053
18053
|
var { AsyncResource } = require("node:async_hooks");
|
|
@@ -18234,7 +18234,7 @@ var require_api_pipeline = __commonJS({
|
|
|
18234
18234
|
this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler);
|
|
18235
18235
|
return pipelineHandler.ret;
|
|
18236
18236
|
} catch (err) {
|
|
18237
|
-
return new
|
|
18237
|
+
return new PassThrough2().destroy(err);
|
|
18238
18238
|
}
|
|
18239
18239
|
}
|
|
18240
18240
|
module2.exports = pipeline;
|
|
@@ -57101,13 +57101,21 @@ var CodexProviderConfigSchema = external_exports.strictObject({
|
|
|
57101
57101
|
/** Which login the upstream call is made with; see `CODEX_LOGINS`. */
|
|
57102
57102
|
login: external_exports.enum(CODEX_LOGINS).optional()
|
|
57103
57103
|
});
|
|
57104
|
+
var ProviderRouteSchema = external_exports.strictObject({
|
|
57105
|
+
/** The condition, the same `when` union a configuration cascade entry takes: the object form or a trilean predicate tree over the request facts the door resolves (`request.model` and kin). */
|
|
57106
|
+
when: WhenSchema,
|
|
57107
|
+
/** The provider a matching request is sent to, by the name a provider file carries. */
|
|
57108
|
+
provider: external_exports.string().min(1)
|
|
57109
|
+
});
|
|
57104
57110
|
var HttpProviderSchema = external_exports.strictObject({
|
|
57105
57111
|
$schema: external_exports.string().optional(),
|
|
57106
57112
|
kind: external_exports.literal("http").optional(),
|
|
57107
57113
|
displayName: external_exports.string().min(1),
|
|
57108
57114
|
baseUrl: external_exports.url(),
|
|
57109
57115
|
credential: ProviderCredentialSchema,
|
|
57110
|
-
env: ProviderEnvSchema.optional()
|
|
57116
|
+
env: ProviderEnvSchema.optional(),
|
|
57117
|
+
/** Per-request routing: when present, each request this provider would serve is matched against the list in order, and the first definite match is sent to that entry's provider instead (itself resolved exactly as a directly dialled request would be). Unmatched requests stay on this provider. */
|
|
57118
|
+
routes: external_exports.array(ProviderRouteSchema).optional()
|
|
57111
57119
|
});
|
|
57112
57120
|
var CodexProviderSchema = external_exports.strictObject({
|
|
57113
57121
|
$schema: external_exports.string().optional(),
|
|
@@ -57135,11 +57143,14 @@ var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
|
|
|
57135
57143
|
var PoolNameSchema = external_exports.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
|
|
57136
57144
|
var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
|
|
57137
57145
|
var IdentitySelectorSchema = external_exports.string().min(1).regex(SELECTOR_RE);
|
|
57138
|
-
var PoolMemberSchema = external_exports.string().min(1).regex(SELECTOR_RE);
|
|
57146
|
+
var PoolMemberSchema = external_exports.union([external_exports.string().min(1).regex(SELECTOR_RE), external_exports.strictObject({ identity: external_exports.string().min(1).regex(SELECTOR_RE), when: WhenSchema })]);
|
|
57147
|
+
function poolMemberIdentity(member) {
|
|
57148
|
+
return typeof member === "string" ? member : member.identity;
|
|
57149
|
+
}
|
|
57139
57150
|
var POOL_PREFERENCES = ["score", "listed"];
|
|
57140
57151
|
var PoolPreferenceSchema = external_exports.enum(POOL_PREFERENCES);
|
|
57141
57152
|
var PoolSchema = external_exports.strictObject({
|
|
57142
|
-
identities: external_exports.array(PoolMemberSchema).min(1).refine((members2) => new Set(members2).size === members2.length, { message: "pool members must be unique" }),
|
|
57153
|
+
identities: external_exports.array(PoolMemberSchema).min(1).refine((members2) => new Set(members2.map(poolMemberIdentity)).size === members2.length, { message: "pool members must be unique" }),
|
|
57143
57154
|
preference: PoolPreferenceSchema.optional()
|
|
57144
57155
|
});
|
|
57145
57156
|
var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
|
|
@@ -60153,7 +60164,7 @@ function writePools(paths, pools) {
|
|
|
60153
60164
|
function poolCycleOf(pools, name) {
|
|
60154
60165
|
const walk = (current, path46) => {
|
|
60155
60166
|
for (const entry of pools[current]?.identities ?? []) {
|
|
60156
|
-
const nested = poolNameOf(entry);
|
|
60167
|
+
const nested = poolNameOf(poolMemberIdentity(entry));
|
|
60157
60168
|
if (nested === void 0) {
|
|
60158
60169
|
continue;
|
|
60159
60170
|
}
|
|
@@ -60174,9 +60185,10 @@ function poolCycleOf(pools, name) {
|
|
|
60174
60185
|
}
|
|
60175
60186
|
function validateMembers(pools, name, members2) {
|
|
60176
60187
|
for (const entry of members2) {
|
|
60177
|
-
const
|
|
60188
|
+
const identity = poolMemberIdentity(entry);
|
|
60189
|
+
const nested = poolNameOf(identity);
|
|
60178
60190
|
if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
|
|
60179
|
-
throw new UsageError(`Member "${
|
|
60191
|
+
throw new UsageError(`Member "${identity}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
|
|
60180
60192
|
}
|
|
60181
60193
|
}
|
|
60182
60194
|
const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members2] } }, name);
|
|
@@ -60915,12 +60927,20 @@ function parseDuration(value2) {
|
|
|
60915
60927
|
}
|
|
60916
60928
|
var evaluator = createSyncEvaluator({});
|
|
60917
60929
|
function resolversOf(context) {
|
|
60930
|
+
const pool = context.pool;
|
|
60918
60931
|
const values = {
|
|
60919
60932
|
now: context.nowMs,
|
|
60920
60933
|
...context.fact?.latestMtimeMs === void 0 ? {} : { "entry.latestMtimeMs": context.fact.latestMtimeMs },
|
|
60921
60934
|
...context.fact?.totalSizeBytes === void 0 ? {} : { "entry.totalSizeBytes": context.fact.totalSizeBytes },
|
|
60922
60935
|
...context.branch === void 0 ? {} : { "repo.branch": context.branch },
|
|
60923
|
-
"repo.detached": context.branchDetached ?? false
|
|
60936
|
+
"repo.detached": context.branchDetached ?? false,
|
|
60937
|
+
...pool?.fiveHour === void 0 ? {} : { "quota.fiveHour.remaining": pool.fiveHour.remaining, "quota.fiveHour.utilization": pool.fiveHour.utilization, "quota.fiveHour.hoursUntilReset": pool.fiveHour.hoursUntilReset },
|
|
60938
|
+
...pool?.sevenDay === void 0 ? {} : { "quota.sevenDay.remaining": pool.sevenDay.remaining, "quota.sevenDay.utilization": pool.sevenDay.utilization, "quota.sevenDay.hoursUntilReset": pool.sevenDay.hoursUntilReset },
|
|
60939
|
+
...pool?.burnPerHour === void 0 ? {} : { "quota.burnPerHour": pool.burnPerHour },
|
|
60940
|
+
...pool?.resuming === void 0 ? {} : { "session.resuming": pool.resuming },
|
|
60941
|
+
...pool?.lastPickHoursAgo === void 0 ? {} : { "session.lastPickHoursAgo": pool.lastPickHoursAgo },
|
|
60942
|
+
...context.request?.model === void 0 ? {} : { "request.model": context.request.model },
|
|
60943
|
+
...context.request?.hasImage === void 0 ? {} : { "request.hasImage": context.request.hasImage }
|
|
60924
60944
|
};
|
|
60925
60945
|
return {
|
|
60926
60946
|
resolveValue: (key) => {
|
|
@@ -63655,7 +63675,7 @@ function registerIdentityCommand(program2, deps) {
|
|
|
63655
63675
|
// package.json
|
|
63656
63676
|
var package_default = {
|
|
63657
63677
|
name: "agent-shim",
|
|
63658
|
-
version: "8.
|
|
63678
|
+
version: "8.35.0",
|
|
63659
63679
|
description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
63660
63680
|
license: "MIT",
|
|
63661
63681
|
author: "Joseph Mearman <joseph@mearman.co.uk>",
|
|
@@ -64744,12 +64764,48 @@ function assessIdentity(member, nowMs) {
|
|
|
64744
64764
|
...sevenDayBurn === void 0 ? {} : { sevenDayBurn }
|
|
64745
64765
|
};
|
|
64746
64766
|
}
|
|
64767
|
+
function poolFactsOf(member, nowMs, resuming, sticky) {
|
|
64768
|
+
const unified = member.snapshot?.providers[ANTHROPIC_PROVIDER]?.rateLimit?.unified;
|
|
64769
|
+
const window = (which) => {
|
|
64770
|
+
const raw = unified?.[which];
|
|
64771
|
+
if (raw?.utilization === void 0) {
|
|
64772
|
+
return void 0;
|
|
64773
|
+
}
|
|
64774
|
+
if (raw.resetsAt === void 0) {
|
|
64775
|
+
return void 0;
|
|
64776
|
+
}
|
|
64777
|
+
return { remaining: Math.max(0, 1 - raw.utilization), utilization: raw.utilization, hoursUntilReset: Math.max(0, (Date.parse(raw.resetsAt) - nowMs) / MS_PER_HOUR) };
|
|
64778
|
+
};
|
|
64779
|
+
const burn = windowBurn(member.records, "fiveHour");
|
|
64780
|
+
return {
|
|
64781
|
+
...window("fiveHour") === void 0 ? {} : { fiveHour: window("fiveHour") },
|
|
64782
|
+
...window("sevenDay") === void 0 ? {} : { sevenDay: window("sevenDay") },
|
|
64783
|
+
...burn === void 0 ? {} : { burnPerHour: burn * MS_PER_HOUR },
|
|
64784
|
+
resuming,
|
|
64785
|
+
...sticky?.identity === member.identity ? { lastPickHoursAgo: Math.max(0, (nowMs - Date.parse(sticky.at)) / MS_PER_HOUR) } : {}
|
|
64786
|
+
};
|
|
64787
|
+
}
|
|
64747
64788
|
function rankPool(input2) {
|
|
64748
64789
|
const { members: members2, nowMs } = input2;
|
|
64749
|
-
const assessments = members2.map((member) => assess(member, nowMs));
|
|
64790
|
+
const assessments = members2.map((member) => ({ member, ...assess(member, nowMs) }));
|
|
64750
64791
|
const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
|
|
64751
64792
|
const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
|
|
64752
|
-
const
|
|
64793
|
+
const guarded = assessments.map((assessment) => {
|
|
64794
|
+
const { candidate } = assessment;
|
|
64795
|
+
if (candidate.class === "ineligible" || assessment.member.policy === void 0) {
|
|
64796
|
+
return assessment;
|
|
64797
|
+
}
|
|
64798
|
+
const context = { nowMs, env: {}, pool: poolFactsOf(assessment.member, nowMs, input2.resuming, input2.sticky) };
|
|
64799
|
+
const verdict = evaluateWhen(assessment.member.policy, context);
|
|
64800
|
+
if (verdict.status === "indeterminate") {
|
|
64801
|
+
return { ...assessment, candidate: { ...candidate, policyDemoted: true, reasons: [`policy undecided (${verdict.checked.join(", ") === "predicate" ? "predicate" : verdict.checked.join(", ")}): ${verdict.reason}`, ...candidate.reasons] } };
|
|
64802
|
+
}
|
|
64803
|
+
if (verdict.passed) {
|
|
64804
|
+
return assessment;
|
|
64805
|
+
}
|
|
64806
|
+
return { ...assessment, candidate: { ...candidate, class: "ineligible", reasons: [`skipped by policy (${verdict.checked.join(", ")}: did not hold)`, ...candidate.reasons] } };
|
|
64807
|
+
});
|
|
64808
|
+
const candidates = guarded.map((assessment) => {
|
|
64753
64809
|
const { candidate, fiveHour, burn } = assessment;
|
|
64754
64810
|
const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
|
|
64755
64811
|
const { sevenDay, sevenDayBurn } = assessment;
|
|
@@ -64775,7 +64831,7 @@ function rankPool(input2) {
|
|
|
64775
64831
|
return { ...candidate, feasible: false, reasons: [...drySpots, ...candidate.reasons] };
|
|
64776
64832
|
});
|
|
64777
64833
|
const ordered = input2.preference === "listed" ? [...candidates] : [...candidates].sort(
|
|
64778
|
-
(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)
|
|
64834
|
+
(left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(left.policyDemoted ?? false) - Number(right.policyDemoted ?? false) || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
|
|
64779
64835
|
);
|
|
64780
64836
|
const sticky = input2.sticky;
|
|
64781
64837
|
const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
|
|
@@ -65104,27 +65160,27 @@ function rankNamedPool(input2, poolName, stack, sticky) {
|
|
|
65104
65160
|
throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
|
|
65105
65161
|
}
|
|
65106
65162
|
const childStack = [...stack, poolName];
|
|
65107
|
-
const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
|
|
65108
|
-
const missing = direct.filter((entry) => !input2.identityExists(entry)).map((
|
|
65109
|
-
const loaded = new Map(loadPoolMembers(input2.fs, input2.paths, direct.filter(input2.identityExists), input2.nowMs).map((member) => [member.identity, member]));
|
|
65163
|
+
const direct = pool.identities.filter((entry) => poolNameOf(poolMemberIdentity(entry)) === void 0);
|
|
65164
|
+
const missing = direct.filter((entry) => !input2.identityExists(poolMemberIdentity(entry))).map((entry) => ({ pool: poolName, identity: poolMemberIdentity(entry) }));
|
|
65165
|
+
const loaded = new Map(loadPoolMembers(input2.fs, input2.paths, direct.filter((entry) => input2.identityExists(poolMemberIdentity(entry))).map(poolMemberIdentity), input2.nowMs).map((member) => [member.identity, member]));
|
|
65110
65166
|
const members2 = [];
|
|
65111
65167
|
for (const entry of pool.identities) {
|
|
65112
|
-
const nestedName = poolNameOf(entry);
|
|
65168
|
+
const nestedName = poolNameOf(poolMemberIdentity(entry));
|
|
65113
65169
|
if (nestedName === void 0) {
|
|
65114
|
-
const member = loaded.get(entry);
|
|
65170
|
+
const member = loaded.get(poolMemberIdentity(entry));
|
|
65115
65171
|
if (member !== void 0) {
|
|
65116
|
-
members2.push(member);
|
|
65172
|
+
members2.push(typeof entry === "string" ? member : { ...member, policy: entry.when });
|
|
65117
65173
|
}
|
|
65118
65174
|
continue;
|
|
65119
65175
|
}
|
|
65120
65176
|
if (input2.pools[nestedName] === void 0) {
|
|
65121
|
-
throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
|
|
65177
|
+
throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" names a pool that is not defined.`);
|
|
65122
65178
|
}
|
|
65123
65179
|
if (childStack.includes(nestedName)) {
|
|
65124
|
-
throw new PoolGraphError(`Pool "${poolName}" member "${entry}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
|
|
65180
|
+
throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
|
|
65125
65181
|
}
|
|
65126
65182
|
const nested = nestedMember(input2, nestedName, childStack, sticky);
|
|
65127
|
-
members2.push(nested.member);
|
|
65183
|
+
members2.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
|
|
65128
65184
|
missing.push(...nested.missing);
|
|
65129
65185
|
}
|
|
65130
65186
|
const ranking = rankPool({
|
|
@@ -70139,16 +70195,16 @@ function runDoctor(params) {
|
|
|
70139
70195
|
const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
|
|
70140
70196
|
const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
|
|
70141
70197
|
for (const [poolName, pool] of Object.entries(pools)) {
|
|
70142
|
-
const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
|
|
70198
|
+
const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
|
|
70143
70199
|
const undefinedPools = pool.identities.flatMap((member) => {
|
|
70144
|
-
const nested = poolNameOf(member);
|
|
70200
|
+
const nested = poolNameOf(poolMemberIdentity(member));
|
|
70145
70201
|
return nested === void 0 || nested in pools ? [] : [nested];
|
|
70146
70202
|
});
|
|
70147
70203
|
if (missing.length === 0 && undefinedPools.length === 0) {
|
|
70148
|
-
push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
|
|
70204
|
+
push("pool", "pass", `${poolName} is valid (${pool.identities.map(poolMemberIdentity).join(", ")}).`, poolName);
|
|
70149
70205
|
} else {
|
|
70150
70206
|
const problems = [
|
|
70151
|
-
...missing.map((member) => `identity "${member}", which does not exist`),
|
|
70207
|
+
...missing.map((member) => `identity "${poolMemberIdentity(member)}", which does not exist`),
|
|
70152
70208
|
...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
|
|
70153
70209
|
];
|
|
70154
70210
|
push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
|
|
@@ -77336,6 +77392,74 @@ function createPassthroughRoute(name, target) {
|
|
|
77336
77392
|
};
|
|
77337
77393
|
}
|
|
77338
77394
|
|
|
77395
|
+
// src/frontdoor/requestScan.ts
|
|
77396
|
+
var import_node_stream2 = require("node:stream");
|
|
77397
|
+
var BYTES_PER_KIB = 1024;
|
|
77398
|
+
var SCAN_KIB = 64;
|
|
77399
|
+
var REQUEST_SCAN_CAP_BYTES = SCAN_KIB * BYTES_PER_KIB;
|
|
77400
|
+
async function scanRequestHead(request7) {
|
|
77401
|
+
const body = request7.body;
|
|
77402
|
+
const head = [];
|
|
77403
|
+
let seen = 0;
|
|
77404
|
+
let text2 = "";
|
|
77405
|
+
let decided = false;
|
|
77406
|
+
let ended = false;
|
|
77407
|
+
let error62;
|
|
77408
|
+
const replay = new import_node_stream2.PassThrough();
|
|
77409
|
+
const modelIn = () => /"model"\s*:\s*"([^"]+)"/.exec(text2)?.[1];
|
|
77410
|
+
const settled = new Promise((resolve) => {
|
|
77411
|
+
const settle2 = () => {
|
|
77412
|
+
const model = modelIn();
|
|
77413
|
+
const hasImage = /"type"\s*:\s*"image"/.test(text2);
|
|
77414
|
+
for (const chunk of head) {
|
|
77415
|
+
replay.write(chunk);
|
|
77416
|
+
}
|
|
77417
|
+
resolve({ ...model === void 0 ? {} : { model }, hasImage, replayed: { ...request7, body: replay } });
|
|
77418
|
+
};
|
|
77419
|
+
body.on("data", (chunk) => {
|
|
77420
|
+
if (decided) {
|
|
77421
|
+
if (!replay.write(chunk)) {
|
|
77422
|
+
body.pause();
|
|
77423
|
+
replay.once("drain", () => {
|
|
77424
|
+
body.resume();
|
|
77425
|
+
});
|
|
77426
|
+
}
|
|
77427
|
+
return;
|
|
77428
|
+
}
|
|
77429
|
+
head.push(chunk);
|
|
77430
|
+
seen += chunk.length;
|
|
77431
|
+
text2 += chunk.toString("utf8");
|
|
77432
|
+
if (modelIn() !== void 0 || seen >= REQUEST_SCAN_CAP_BYTES) {
|
|
77433
|
+
decided = true;
|
|
77434
|
+
body.pause();
|
|
77435
|
+
replay.once("resume", () => {
|
|
77436
|
+
body.resume();
|
|
77437
|
+
});
|
|
77438
|
+
settle2();
|
|
77439
|
+
}
|
|
77440
|
+
});
|
|
77441
|
+
body.on("end", () => {
|
|
77442
|
+
ended = true;
|
|
77443
|
+
if (!decided) {
|
|
77444
|
+
decided = true;
|
|
77445
|
+
settle2();
|
|
77446
|
+
}
|
|
77447
|
+
replay.end();
|
|
77448
|
+
});
|
|
77449
|
+
body.on("error", (cause) => {
|
|
77450
|
+
error62 = cause;
|
|
77451
|
+
replay.destroy(cause);
|
|
77452
|
+
});
|
|
77453
|
+
body.on("close", () => {
|
|
77454
|
+
if (!decided && !ended && error62 === void 0) {
|
|
77455
|
+
decided = true;
|
|
77456
|
+
settle2();
|
|
77457
|
+
}
|
|
77458
|
+
});
|
|
77459
|
+
});
|
|
77460
|
+
return await settled;
|
|
77461
|
+
}
|
|
77462
|
+
|
|
77339
77463
|
// src/frontdoor/providerRoute.ts
|
|
77340
77464
|
function createProviderRouteResolver(deps) {
|
|
77341
77465
|
const isRcSelfHostPath = (path46) => path46 === "/v1/code/sessions" || path46.startsWith("/v1/code/sessions/") || path46 === "/v1/sessions" || path46.startsWith("/v1/sessions/");
|
|
@@ -77400,7 +77524,64 @@ function createProviderRouteResolver(deps) {
|
|
|
77400
77524
|
)
|
|
77401
77525
|
};
|
|
77402
77526
|
};
|
|
77403
|
-
|
|
77527
|
+
const servingRewritten = (route, url2, body) => ({
|
|
77528
|
+
name: route.name,
|
|
77529
|
+
headroomEligible: route.headroomEligible,
|
|
77530
|
+
headroomUpstream: route.headroomUpstream,
|
|
77531
|
+
// The pipeline serves the route with the request it received; the routing layer's decisions (the rewritten target path, the replayed body) are substituted here, so the route sees exactly the request the resolution acted on.
|
|
77532
|
+
serve: async (request7, response) => {
|
|
77533
|
+
await route.serve({ ...request7, url: url2, body }, response);
|
|
77534
|
+
}
|
|
77535
|
+
});
|
|
77536
|
+
return async (request7) => {
|
|
77537
|
+
let current = request7;
|
|
77538
|
+
let replay;
|
|
77539
|
+
const visited = /* @__PURE__ */ new Set();
|
|
77540
|
+
for (; ; ) {
|
|
77541
|
+
const path46 = new URL(current.url, "http://127.0.0.1").pathname;
|
|
77542
|
+
const scoped = parseProviderPath(path46);
|
|
77543
|
+
const providerName = scoped?.provider ?? current.session.provider;
|
|
77544
|
+
if (providerName !== void 0 && visited.has(providerName)) {
|
|
77545
|
+
return { ok: false, status: HTTP_STATUS.internalServerError, message: `provider routing cycle: ${[...visited, providerName].join(" -> ")}` };
|
|
77546
|
+
}
|
|
77547
|
+
if (providerName !== void 0) {
|
|
77548
|
+
visited.add(providerName);
|
|
77549
|
+
}
|
|
77550
|
+
let routes;
|
|
77551
|
+
try {
|
|
77552
|
+
const definition = providerName === void 0 ? void 0 : loadProvider(deps.providersDir, providerName, deps.fs);
|
|
77553
|
+
routes = definition === void 0 || isCodexProvider(definition) ? void 0 : definition.routes;
|
|
77554
|
+
} catch {
|
|
77555
|
+
routes = void 0;
|
|
77556
|
+
}
|
|
77557
|
+
if (routes === void 0 || routes.length === 0) {
|
|
77558
|
+
const resolution = resolve(current);
|
|
77559
|
+
return resolution.ok && replay !== void 0 ? { ok: true, route: servingRewritten(resolution.route, current.url, replay.body) } : resolution;
|
|
77560
|
+
}
|
|
77561
|
+
const scan = await scanRequestHead(current);
|
|
77562
|
+
if (scan.replayed === void 0) {
|
|
77563
|
+
const resolution = resolve(current);
|
|
77564
|
+
return resolution.ok && replay !== void 0 ? { ok: true, route: servingRewritten(resolution.route, current.url, replay.body) } : resolution;
|
|
77565
|
+
}
|
|
77566
|
+
replay = scan.replayed;
|
|
77567
|
+
const context = { nowMs: Date.now(), env: deps.env, ...scan.model === void 0 && scan.hasImage === void 0 ? {} : { request: { ...scan.model === void 0 ? {} : { model: scan.model }, hasImage: scan.hasImage } } };
|
|
77568
|
+
let matched;
|
|
77569
|
+
for (const entry of routes) {
|
|
77570
|
+
const verdict = evaluateWhen(entry.when, context);
|
|
77571
|
+
if (verdict.status === "definite" && verdict.passed) {
|
|
77572
|
+
matched = entry.provider;
|
|
77573
|
+
break;
|
|
77574
|
+
}
|
|
77575
|
+
}
|
|
77576
|
+
if (matched === void 0) {
|
|
77577
|
+
const resolution = resolve(current);
|
|
77578
|
+
return resolution.ok ? { ok: true, route: servingRewritten(resolution.route, current.url, replay.body) } : resolution;
|
|
77579
|
+
}
|
|
77580
|
+
const rest = scoped === void 0 ? path46 : path46.slice(`${PROVIDER_PATH_PREFIX}${encodeURIComponent(scoped.provider)}`.length);
|
|
77581
|
+
const query = current.url.includes("?") ? current.url.slice(current.url.indexOf("?")) : "";
|
|
77582
|
+
current = { ...current, url: `${PROVIDER_PATH_PREFIX}${encodeURIComponent(matched)}${rest}${query}`, body: replay.body };
|
|
77583
|
+
}
|
|
77584
|
+
};
|
|
77404
77585
|
}
|
|
77405
77586
|
function providerCredentialHeaders(credential) {
|
|
77406
77587
|
return credential.target === "apiKey" ? { "x-api-key": credential.token } : { authorization: `Bearer ${credential.token}` };
|
|
@@ -81780,12 +81961,12 @@ function registerPoolCommand(program2, deps) {
|
|
|
81780
81961
|
}
|
|
81781
81962
|
const members2 = options.identity === void 0 ? existing.identities : membersOf(options);
|
|
81782
81963
|
if (options.identity !== void 0) {
|
|
81783
|
-
requireIdentities(paths,
|
|
81964
|
+
requireIdentities(paths, membersOf(options));
|
|
81784
81965
|
}
|
|
81785
81966
|
const preference = options.preference === void 0 ? existing.preference : options.preference === false ? void 0 : options.preference;
|
|
81786
81967
|
const updated = setPool(paths, name, members2, preference);
|
|
81787
81968
|
reportMutation(options.json, { action: "updated", kind: "pool", name, value: updated }, () => {
|
|
81788
|
-
console.log(`Pool "${name}" now has ${members2.join(", ")}.`);
|
|
81969
|
+
console.log(`Pool "${name}" now has ${members2.map(poolMemberIdentity).join(", ")}.`);
|
|
81789
81970
|
});
|
|
81790
81971
|
}),
|
|
81791
81972
|
["agent-shim pool set subs --identity work --identity personal --identity spare", "agent-shim pool set subs --preference listed"]
|
|
@@ -81805,7 +81986,7 @@ function registerPoolCommand(program2, deps) {
|
|
|
81805
81986
|
}
|
|
81806
81987
|
for (const name of names) {
|
|
81807
81988
|
const entry = pools[name];
|
|
81808
|
-
console.log(`${active === `${POOL_SELECTOR_PREFIX}${name}` ? "*" : " "} ${name}: ${(entry?.identities ?? []).join(", ")}${entry?.preference === void 0 ? "" : " (preference: listed)"}`);
|
|
81989
|
+
console.log(`${active === `${POOL_SELECTOR_PREFIX}${name}` ? "*" : " "} ${name}: ${(entry?.identities ?? []).map(poolMemberIdentity).join(", ")}${entry?.preference === void 0 ? "" : " (preference: listed)"}`);
|
|
81809
81990
|
}
|
|
81810
81991
|
}),
|
|
81811
81992
|
["agent-shim pool list", "agent-shim pool list --json"]
|
|
@@ -81818,7 +81999,7 @@ function registerPoolCommand(program2, deps) {
|
|
|
81818
81999
|
return;
|
|
81819
82000
|
}
|
|
81820
82001
|
console.log(`Pool: ${name}`);
|
|
81821
|
-
console.log(`Members: ${entry.identities.join(", ")}`);
|
|
82002
|
+
console.log(`Members: ${entry.identities.map(poolMemberIdentity).join(", ")}`);
|
|
81822
82003
|
if (entry.preference !== void 0) {
|
|
81823
82004
|
console.log(`Preference: ${entry.preference}`);
|
|
81824
82005
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -589,13 +589,21 @@ var CodexProviderConfigSchema = import_zod.z.strictObject({
|
|
|
589
589
|
/** Which login the upstream call is made with; see `CODEX_LOGINS`. */
|
|
590
590
|
login: import_zod.z.enum(CODEX_LOGINS).optional()
|
|
591
591
|
});
|
|
592
|
+
var ProviderRouteSchema = import_zod.z.strictObject({
|
|
593
|
+
/** The condition, the same `when` union a configuration cascade entry takes: the object form or a trilean predicate tree over the request facts the door resolves (`request.model` and kin). */
|
|
594
|
+
when: WhenSchema,
|
|
595
|
+
/** The provider a matching request is sent to, by the name a provider file carries. */
|
|
596
|
+
provider: import_zod.z.string().min(1)
|
|
597
|
+
});
|
|
592
598
|
var HttpProviderSchema = import_zod.z.strictObject({
|
|
593
599
|
$schema: import_zod.z.string().optional(),
|
|
594
600
|
kind: import_zod.z.literal("http").optional(),
|
|
595
601
|
displayName: import_zod.z.string().min(1),
|
|
596
602
|
baseUrl: import_zod.z.url(),
|
|
597
603
|
credential: ProviderCredentialSchema,
|
|
598
|
-
env: ProviderEnvSchema.optional()
|
|
604
|
+
env: ProviderEnvSchema.optional(),
|
|
605
|
+
/** Per-request routing: when present, each request this provider would serve is matched against the list in order, and the first definite match is sent to that entry's provider instead (itself resolved exactly as a directly dialled request would be). Unmatched requests stay on this provider. */
|
|
606
|
+
routes: import_zod.z.array(ProviderRouteSchema).optional()
|
|
599
607
|
});
|
|
600
608
|
var CodexProviderSchema = import_zod.z.strictObject({
|
|
601
609
|
$schema: import_zod.z.string().optional(),
|
|
@@ -623,11 +631,14 @@ var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
|
|
|
623
631
|
var PoolNameSchema = import_zod.z.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
|
|
624
632
|
var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
|
|
625
633
|
var IdentitySelectorSchema = import_zod.z.string().min(1).regex(SELECTOR_RE);
|
|
626
|
-
var PoolMemberSchema = import_zod.z.string().min(1).regex(SELECTOR_RE);
|
|
634
|
+
var PoolMemberSchema = import_zod.z.union([import_zod.z.string().min(1).regex(SELECTOR_RE), import_zod.z.strictObject({ identity: import_zod.z.string().min(1).regex(SELECTOR_RE), when: WhenSchema })]);
|
|
635
|
+
function poolMemberIdentity(member) {
|
|
636
|
+
return typeof member === "string" ? member : member.identity;
|
|
637
|
+
}
|
|
627
638
|
var POOL_PREFERENCES = ["score", "listed"];
|
|
628
639
|
var PoolPreferenceSchema = import_zod.z.enum(POOL_PREFERENCES);
|
|
629
640
|
var PoolSchema = import_zod.z.strictObject({
|
|
630
|
-
identities: import_zod.z.array(PoolMemberSchema).min(1).refine((members) => new Set(members).size === members.length, { message: "pool members must be unique" }),
|
|
641
|
+
identities: import_zod.z.array(PoolMemberSchema).min(1).refine((members) => new Set(members.map(poolMemberIdentity)).size === members.length, { message: "pool members must be unique" }),
|
|
631
642
|
preference: PoolPreferenceSchema.optional()
|
|
632
643
|
});
|
|
633
644
|
var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
|
|
@@ -1004,12 +1015,20 @@ function matchBranch(pattern, branch, detached = false) {
|
|
|
1004
1015
|
}
|
|
1005
1016
|
var evaluator = (0, import_trilean2.createSyncEvaluator)({});
|
|
1006
1017
|
function resolversOf(context) {
|
|
1018
|
+
const pool = context.pool;
|
|
1007
1019
|
const values = {
|
|
1008
1020
|
now: context.nowMs,
|
|
1009
1021
|
...context.fact?.latestMtimeMs === void 0 ? {} : { "entry.latestMtimeMs": context.fact.latestMtimeMs },
|
|
1010
1022
|
...context.fact?.totalSizeBytes === void 0 ? {} : { "entry.totalSizeBytes": context.fact.totalSizeBytes },
|
|
1011
1023
|
...context.branch === void 0 ? {} : { "repo.branch": context.branch },
|
|
1012
|
-
"repo.detached": context.branchDetached ?? false
|
|
1024
|
+
"repo.detached": context.branchDetached ?? false,
|
|
1025
|
+
...pool?.fiveHour === void 0 ? {} : { "quota.fiveHour.remaining": pool.fiveHour.remaining, "quota.fiveHour.utilization": pool.fiveHour.utilization, "quota.fiveHour.hoursUntilReset": pool.fiveHour.hoursUntilReset },
|
|
1026
|
+
...pool?.sevenDay === void 0 ? {} : { "quota.sevenDay.remaining": pool.sevenDay.remaining, "quota.sevenDay.utilization": pool.sevenDay.utilization, "quota.sevenDay.hoursUntilReset": pool.sevenDay.hoursUntilReset },
|
|
1027
|
+
...pool?.burnPerHour === void 0 ? {} : { "quota.burnPerHour": pool.burnPerHour },
|
|
1028
|
+
...pool?.resuming === void 0 ? {} : { "session.resuming": pool.resuming },
|
|
1029
|
+
...pool?.lastPickHoursAgo === void 0 ? {} : { "session.lastPickHoursAgo": pool.lastPickHoursAgo },
|
|
1030
|
+
...context.request?.model === void 0 ? {} : { "request.model": context.request.model },
|
|
1031
|
+
...context.request?.hasImage === void 0 ? {} : { "request.hasImage": context.request.hasImage }
|
|
1013
1032
|
};
|
|
1014
1033
|
return {
|
|
1015
1034
|
resolveValue: (key) => {
|
|
@@ -5390,7 +5409,7 @@ var import_undici = require("undici");
|
|
|
5390
5409
|
// package.json
|
|
5391
5410
|
var package_default = {
|
|
5392
5411
|
name: "agent-shim",
|
|
5393
|
-
version: "8.
|
|
5412
|
+
version: "8.35.0",
|
|
5394
5413
|
description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
5395
5414
|
license: "MIT",
|
|
5396
5415
|
author: "Joseph Mearman <joseph@mearman.co.uk>",
|
|
@@ -6682,12 +6701,48 @@ function assessIdentity(member, nowMs) {
|
|
|
6682
6701
|
...sevenDayBurn === void 0 ? {} : { sevenDayBurn }
|
|
6683
6702
|
};
|
|
6684
6703
|
}
|
|
6704
|
+
function poolFactsOf(member, nowMs, resuming, sticky) {
|
|
6705
|
+
const unified = member.snapshot?.providers[ANTHROPIC_PROVIDER]?.rateLimit?.unified;
|
|
6706
|
+
const window = (which) => {
|
|
6707
|
+
const raw = unified?.[which];
|
|
6708
|
+
if (raw?.utilization === void 0) {
|
|
6709
|
+
return void 0;
|
|
6710
|
+
}
|
|
6711
|
+
if (raw.resetsAt === void 0) {
|
|
6712
|
+
return void 0;
|
|
6713
|
+
}
|
|
6714
|
+
return { remaining: Math.max(0, 1 - raw.utilization), utilization: raw.utilization, hoursUntilReset: Math.max(0, (Date.parse(raw.resetsAt) - nowMs) / MS_PER_HOUR) };
|
|
6715
|
+
};
|
|
6716
|
+
const burn = windowBurn(member.records, "fiveHour");
|
|
6717
|
+
return {
|
|
6718
|
+
...window("fiveHour") === void 0 ? {} : { fiveHour: window("fiveHour") },
|
|
6719
|
+
...window("sevenDay") === void 0 ? {} : { sevenDay: window("sevenDay") },
|
|
6720
|
+
...burn === void 0 ? {} : { burnPerHour: burn * MS_PER_HOUR },
|
|
6721
|
+
resuming,
|
|
6722
|
+
...sticky?.identity === member.identity ? { lastPickHoursAgo: Math.max(0, (nowMs - Date.parse(sticky.at)) / MS_PER_HOUR) } : {}
|
|
6723
|
+
};
|
|
6724
|
+
}
|
|
6685
6725
|
function rankPool(input) {
|
|
6686
6726
|
const { members, nowMs } = input;
|
|
6687
|
-
const assessments = members.map((member) => assess(member, nowMs));
|
|
6727
|
+
const assessments = members.map((member) => ({ member, ...assess(member, nowMs) }));
|
|
6688
6728
|
const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
|
|
6689
6729
|
const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
|
|
6690
|
-
const
|
|
6730
|
+
const guarded = assessments.map((assessment) => {
|
|
6731
|
+
const { candidate } = assessment;
|
|
6732
|
+
if (candidate.class === "ineligible" || assessment.member.policy === void 0) {
|
|
6733
|
+
return assessment;
|
|
6734
|
+
}
|
|
6735
|
+
const context = { nowMs, env: {}, pool: poolFactsOf(assessment.member, nowMs, input.resuming, input.sticky) };
|
|
6736
|
+
const verdict = evaluateWhen(assessment.member.policy, context);
|
|
6737
|
+
if (verdict.status === "indeterminate") {
|
|
6738
|
+
return { ...assessment, candidate: { ...candidate, policyDemoted: true, reasons: [`policy undecided (${verdict.checked.join(", ") === "predicate" ? "predicate" : verdict.checked.join(", ")}): ${verdict.reason}`, ...candidate.reasons] } };
|
|
6739
|
+
}
|
|
6740
|
+
if (verdict.passed) {
|
|
6741
|
+
return assessment;
|
|
6742
|
+
}
|
|
6743
|
+
return { ...assessment, candidate: { ...candidate, class: "ineligible", reasons: [`skipped by policy (${verdict.checked.join(", ")}: did not hold)`, ...candidate.reasons] } };
|
|
6744
|
+
});
|
|
6745
|
+
const candidates = guarded.map((assessment) => {
|
|
6691
6746
|
const { candidate, fiveHour, burn } = assessment;
|
|
6692
6747
|
const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
|
|
6693
6748
|
const { sevenDay, sevenDayBurn } = assessment;
|
|
@@ -6713,7 +6768,7 @@ function rankPool(input) {
|
|
|
6713
6768
|
return { ...candidate, feasible: false, reasons: [...drySpots, ...candidate.reasons] };
|
|
6714
6769
|
});
|
|
6715
6770
|
const ordered = input.preference === "listed" ? [...candidates] : [...candidates].sort(
|
|
6716
|
-
(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)
|
|
6771
|
+
(left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(left.policyDemoted ?? false) - Number(right.policyDemoted ?? false) || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
|
|
6717
6772
|
);
|
|
6718
6773
|
const sticky = input.sticky;
|
|
6719
6774
|
const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
|
|
@@ -6884,27 +6939,27 @@ function rankNamedPool(input, poolName, stack, sticky) {
|
|
|
6884
6939
|
throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
|
|
6885
6940
|
}
|
|
6886
6941
|
const childStack = [...stack, poolName];
|
|
6887
|
-
const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
|
|
6888
|
-
const missing = direct.filter((entry) => !input.identityExists(entry)).map((
|
|
6889
|
-
const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter(input.identityExists), input.nowMs).map((member) => [member.identity, member]));
|
|
6942
|
+
const direct = pool.identities.filter((entry) => poolNameOf(poolMemberIdentity(entry)) === void 0);
|
|
6943
|
+
const missing = direct.filter((entry) => !input.identityExists(poolMemberIdentity(entry))).map((entry) => ({ pool: poolName, identity: poolMemberIdentity(entry) }));
|
|
6944
|
+
const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter((entry) => input.identityExists(poolMemberIdentity(entry))).map(poolMemberIdentity), input.nowMs).map((member) => [member.identity, member]));
|
|
6890
6945
|
const members = [];
|
|
6891
6946
|
for (const entry of pool.identities) {
|
|
6892
|
-
const nestedName = poolNameOf(entry);
|
|
6947
|
+
const nestedName = poolNameOf(poolMemberIdentity(entry));
|
|
6893
6948
|
if (nestedName === void 0) {
|
|
6894
|
-
const member = loaded.get(entry);
|
|
6949
|
+
const member = loaded.get(poolMemberIdentity(entry));
|
|
6895
6950
|
if (member !== void 0) {
|
|
6896
|
-
members.push(member);
|
|
6951
|
+
members.push(typeof entry === "string" ? member : { ...member, policy: entry.when });
|
|
6897
6952
|
}
|
|
6898
6953
|
continue;
|
|
6899
6954
|
}
|
|
6900
6955
|
if (input.pools[nestedName] === void 0) {
|
|
6901
|
-
throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
|
|
6956
|
+
throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" names a pool that is not defined.`);
|
|
6902
6957
|
}
|
|
6903
6958
|
if (childStack.includes(nestedName)) {
|
|
6904
|
-
throw new PoolGraphError(`Pool "${poolName}" member "${entry}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
|
|
6959
|
+
throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
|
|
6905
6960
|
}
|
|
6906
6961
|
const nested = nestedMember(input, nestedName, childStack, sticky);
|
|
6907
|
-
members.push(nested.member);
|
|
6962
|
+
members.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
|
|
6908
6963
|
missing.push(...nested.missing);
|
|
6909
6964
|
}
|
|
6910
6965
|
const ranking = rankPool({
|
|
@@ -7165,7 +7220,7 @@ function writePools(paths, pools) {
|
|
|
7165
7220
|
function poolCycleOf(pools, name) {
|
|
7166
7221
|
const walk = (current, path32) => {
|
|
7167
7222
|
for (const entry of pools[current]?.identities ?? []) {
|
|
7168
|
-
const nested = poolNameOf(entry);
|
|
7223
|
+
const nested = poolNameOf(poolMemberIdentity(entry));
|
|
7169
7224
|
if (nested === void 0) {
|
|
7170
7225
|
continue;
|
|
7171
7226
|
}
|
|
@@ -7186,9 +7241,10 @@ function poolCycleOf(pools, name) {
|
|
|
7186
7241
|
}
|
|
7187
7242
|
function validateMembers(pools, name, members) {
|
|
7188
7243
|
for (const entry of members) {
|
|
7189
|
-
const
|
|
7244
|
+
const identity = poolMemberIdentity(entry);
|
|
7245
|
+
const nested = poolNameOf(identity);
|
|
7190
7246
|
if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
|
|
7191
|
-
throw new UsageError(`Member "${
|
|
7247
|
+
throw new UsageError(`Member "${identity}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
|
|
7192
7248
|
}
|
|
7193
7249
|
}
|
|
7194
7250
|
const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members] } }, name);
|
|
@@ -9893,16 +9949,16 @@ function runDoctor(params) {
|
|
|
9893
9949
|
const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
|
|
9894
9950
|
const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
|
|
9895
9951
|
for (const [poolName, pool] of Object.entries(pools)) {
|
|
9896
|
-
const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
|
|
9952
|
+
const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
|
|
9897
9953
|
const undefinedPools = pool.identities.flatMap((member) => {
|
|
9898
|
-
const nested = poolNameOf(member);
|
|
9954
|
+
const nested = poolNameOf(poolMemberIdentity(member));
|
|
9899
9955
|
return nested === void 0 || nested in pools ? [] : [nested];
|
|
9900
9956
|
});
|
|
9901
9957
|
if (missing.length === 0 && undefinedPools.length === 0) {
|
|
9902
|
-
push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
|
|
9958
|
+
push("pool", "pass", `${poolName} is valid (${pool.identities.map(poolMemberIdentity).join(", ")}).`, poolName);
|
|
9903
9959
|
} else {
|
|
9904
9960
|
const problems = [
|
|
9905
|
-
...missing.map((member) => `identity "${member}", which does not exist`),
|
|
9961
|
+
...missing.map((member) => `identity "${poolMemberIdentity(member)}", which does not exist`),
|
|
9906
9962
|
...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
|
|
9907
9963
|
];
|
|
9908
9964
|
push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
|
package/dist/index.mjs
CHANGED
|
@@ -277,13 +277,21 @@ var CodexProviderConfigSchema = z.strictObject({
|
|
|
277
277
|
/** Which login the upstream call is made with; see `CODEX_LOGINS`. */
|
|
278
278
|
login: z.enum(CODEX_LOGINS).optional()
|
|
279
279
|
});
|
|
280
|
+
var ProviderRouteSchema = z.strictObject({
|
|
281
|
+
/** The condition, the same `when` union a configuration cascade entry takes: the object form or a trilean predicate tree over the request facts the door resolves (`request.model` and kin). */
|
|
282
|
+
when: WhenSchema,
|
|
283
|
+
/** The provider a matching request is sent to, by the name a provider file carries. */
|
|
284
|
+
provider: z.string().min(1)
|
|
285
|
+
});
|
|
280
286
|
var HttpProviderSchema = z.strictObject({
|
|
281
287
|
$schema: z.string().optional(),
|
|
282
288
|
kind: z.literal("http").optional(),
|
|
283
289
|
displayName: z.string().min(1),
|
|
284
290
|
baseUrl: z.url(),
|
|
285
291
|
credential: ProviderCredentialSchema,
|
|
286
|
-
env: ProviderEnvSchema.optional()
|
|
292
|
+
env: ProviderEnvSchema.optional(),
|
|
293
|
+
/** Per-request routing: when present, each request this provider would serve is matched against the list in order, and the first definite match is sent to that entry's provider instead (itself resolved exactly as a directly dialled request would be). Unmatched requests stay on this provider. */
|
|
294
|
+
routes: z.array(ProviderRouteSchema).optional()
|
|
287
295
|
});
|
|
288
296
|
var CodexProviderSchema = z.strictObject({
|
|
289
297
|
$schema: z.string().optional(),
|
|
@@ -311,11 +319,14 @@ var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
|
|
|
311
319
|
var PoolNameSchema = z.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
|
|
312
320
|
var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
|
|
313
321
|
var IdentitySelectorSchema = z.string().min(1).regex(SELECTOR_RE);
|
|
314
|
-
var PoolMemberSchema = z.string().min(1).regex(SELECTOR_RE);
|
|
322
|
+
var PoolMemberSchema = z.union([z.string().min(1).regex(SELECTOR_RE), z.strictObject({ identity: z.string().min(1).regex(SELECTOR_RE), when: WhenSchema })]);
|
|
323
|
+
function poolMemberIdentity(member) {
|
|
324
|
+
return typeof member === "string" ? member : member.identity;
|
|
325
|
+
}
|
|
315
326
|
var POOL_PREFERENCES = ["score", "listed"];
|
|
316
327
|
var PoolPreferenceSchema = z.enum(POOL_PREFERENCES);
|
|
317
328
|
var PoolSchema = z.strictObject({
|
|
318
|
-
identities: z.array(PoolMemberSchema).min(1).refine((members) => new Set(members).size === members.length, { message: "pool members must be unique" }),
|
|
329
|
+
identities: z.array(PoolMemberSchema).min(1).refine((members) => new Set(members.map(poolMemberIdentity)).size === members.length, { message: "pool members must be unique" }),
|
|
319
330
|
preference: PoolPreferenceSchema.optional()
|
|
320
331
|
});
|
|
321
332
|
var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
|
|
@@ -692,12 +703,20 @@ function matchBranch(pattern, branch, detached = false) {
|
|
|
692
703
|
}
|
|
693
704
|
var evaluator = createSyncEvaluator({});
|
|
694
705
|
function resolversOf(context) {
|
|
706
|
+
const pool = context.pool;
|
|
695
707
|
const values = {
|
|
696
708
|
now: context.nowMs,
|
|
697
709
|
...context.fact?.latestMtimeMs === void 0 ? {} : { "entry.latestMtimeMs": context.fact.latestMtimeMs },
|
|
698
710
|
...context.fact?.totalSizeBytes === void 0 ? {} : { "entry.totalSizeBytes": context.fact.totalSizeBytes },
|
|
699
711
|
...context.branch === void 0 ? {} : { "repo.branch": context.branch },
|
|
700
|
-
"repo.detached": context.branchDetached ?? false
|
|
712
|
+
"repo.detached": context.branchDetached ?? false,
|
|
713
|
+
...pool?.fiveHour === void 0 ? {} : { "quota.fiveHour.remaining": pool.fiveHour.remaining, "quota.fiveHour.utilization": pool.fiveHour.utilization, "quota.fiveHour.hoursUntilReset": pool.fiveHour.hoursUntilReset },
|
|
714
|
+
...pool?.sevenDay === void 0 ? {} : { "quota.sevenDay.remaining": pool.sevenDay.remaining, "quota.sevenDay.utilization": pool.sevenDay.utilization, "quota.sevenDay.hoursUntilReset": pool.sevenDay.hoursUntilReset },
|
|
715
|
+
...pool?.burnPerHour === void 0 ? {} : { "quota.burnPerHour": pool.burnPerHour },
|
|
716
|
+
...pool?.resuming === void 0 ? {} : { "session.resuming": pool.resuming },
|
|
717
|
+
...pool?.lastPickHoursAgo === void 0 ? {} : { "session.lastPickHoursAgo": pool.lastPickHoursAgo },
|
|
718
|
+
...context.request?.model === void 0 ? {} : { "request.model": context.request.model },
|
|
719
|
+
...context.request?.hasImage === void 0 ? {} : { "request.hasImage": context.request.hasImage }
|
|
701
720
|
};
|
|
702
721
|
return {
|
|
703
722
|
resolveValue: (key) => {
|
|
@@ -5078,7 +5097,7 @@ import { Agent as Agent3, fetch as undiciFetch } from "undici";
|
|
|
5078
5097
|
// package.json
|
|
5079
5098
|
var package_default = {
|
|
5080
5099
|
name: "agent-shim",
|
|
5081
|
-
version: "8.
|
|
5100
|
+
version: "8.35.0",
|
|
5082
5101
|
description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
5083
5102
|
license: "MIT",
|
|
5084
5103
|
author: "Joseph Mearman <joseph@mearman.co.uk>",
|
|
@@ -6370,12 +6389,48 @@ function assessIdentity(member, nowMs) {
|
|
|
6370
6389
|
...sevenDayBurn === void 0 ? {} : { sevenDayBurn }
|
|
6371
6390
|
};
|
|
6372
6391
|
}
|
|
6392
|
+
function poolFactsOf(member, nowMs, resuming, sticky) {
|
|
6393
|
+
const unified = member.snapshot?.providers[ANTHROPIC_PROVIDER]?.rateLimit?.unified;
|
|
6394
|
+
const window = (which) => {
|
|
6395
|
+
const raw = unified?.[which];
|
|
6396
|
+
if (raw?.utilization === void 0) {
|
|
6397
|
+
return void 0;
|
|
6398
|
+
}
|
|
6399
|
+
if (raw.resetsAt === void 0) {
|
|
6400
|
+
return void 0;
|
|
6401
|
+
}
|
|
6402
|
+
return { remaining: Math.max(0, 1 - raw.utilization), utilization: raw.utilization, hoursUntilReset: Math.max(0, (Date.parse(raw.resetsAt) - nowMs) / MS_PER_HOUR) };
|
|
6403
|
+
};
|
|
6404
|
+
const burn = windowBurn(member.records, "fiveHour");
|
|
6405
|
+
return {
|
|
6406
|
+
...window("fiveHour") === void 0 ? {} : { fiveHour: window("fiveHour") },
|
|
6407
|
+
...window("sevenDay") === void 0 ? {} : { sevenDay: window("sevenDay") },
|
|
6408
|
+
...burn === void 0 ? {} : { burnPerHour: burn * MS_PER_HOUR },
|
|
6409
|
+
resuming,
|
|
6410
|
+
...sticky?.identity === member.identity ? { lastPickHoursAgo: Math.max(0, (nowMs - Date.parse(sticky.at)) / MS_PER_HOUR) } : {}
|
|
6411
|
+
};
|
|
6412
|
+
}
|
|
6373
6413
|
function rankPool(input) {
|
|
6374
6414
|
const { members, nowMs } = input;
|
|
6375
|
-
const assessments = members.map((member) => assess(member, nowMs));
|
|
6415
|
+
const assessments = members.map((member) => ({ member, ...assess(member, nowMs) }));
|
|
6376
6416
|
const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
|
|
6377
6417
|
const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
|
|
6378
|
-
const
|
|
6418
|
+
const guarded = assessments.map((assessment) => {
|
|
6419
|
+
const { candidate } = assessment;
|
|
6420
|
+
if (candidate.class === "ineligible" || assessment.member.policy === void 0) {
|
|
6421
|
+
return assessment;
|
|
6422
|
+
}
|
|
6423
|
+
const context = { nowMs, env: {}, pool: poolFactsOf(assessment.member, nowMs, input.resuming, input.sticky) };
|
|
6424
|
+
const verdict = evaluateWhen(assessment.member.policy, context);
|
|
6425
|
+
if (verdict.status === "indeterminate") {
|
|
6426
|
+
return { ...assessment, candidate: { ...candidate, policyDemoted: true, reasons: [`policy undecided (${verdict.checked.join(", ") === "predicate" ? "predicate" : verdict.checked.join(", ")}): ${verdict.reason}`, ...candidate.reasons] } };
|
|
6427
|
+
}
|
|
6428
|
+
if (verdict.passed) {
|
|
6429
|
+
return assessment;
|
|
6430
|
+
}
|
|
6431
|
+
return { ...assessment, candidate: { ...candidate, class: "ineligible", reasons: [`skipped by policy (${verdict.checked.join(", ")}: did not hold)`, ...candidate.reasons] } };
|
|
6432
|
+
});
|
|
6433
|
+
const candidates = guarded.map((assessment) => {
|
|
6379
6434
|
const { candidate, fiveHour, burn } = assessment;
|
|
6380
6435
|
const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
|
|
6381
6436
|
const { sevenDay, sevenDayBurn } = assessment;
|
|
@@ -6401,7 +6456,7 @@ function rankPool(input) {
|
|
|
6401
6456
|
return { ...candidate, feasible: false, reasons: [...drySpots, ...candidate.reasons] };
|
|
6402
6457
|
});
|
|
6403
6458
|
const ordered = input.preference === "listed" ? [...candidates] : [...candidates].sort(
|
|
6404
|
-
(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)
|
|
6459
|
+
(left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(left.policyDemoted ?? false) - Number(right.policyDemoted ?? false) || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
|
|
6405
6460
|
);
|
|
6406
6461
|
const sticky = input.sticky;
|
|
6407
6462
|
const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
|
|
@@ -6572,27 +6627,27 @@ function rankNamedPool(input, poolName, stack, sticky) {
|
|
|
6572
6627
|
throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
|
|
6573
6628
|
}
|
|
6574
6629
|
const childStack = [...stack, poolName];
|
|
6575
|
-
const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
|
|
6576
|
-
const missing = direct.filter((entry) => !input.identityExists(entry)).map((
|
|
6577
|
-
const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter(input.identityExists), input.nowMs).map((member) => [member.identity, member]));
|
|
6630
|
+
const direct = pool.identities.filter((entry) => poolNameOf(poolMemberIdentity(entry)) === void 0);
|
|
6631
|
+
const missing = direct.filter((entry) => !input.identityExists(poolMemberIdentity(entry))).map((entry) => ({ pool: poolName, identity: poolMemberIdentity(entry) }));
|
|
6632
|
+
const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter((entry) => input.identityExists(poolMemberIdentity(entry))).map(poolMemberIdentity), input.nowMs).map((member) => [member.identity, member]));
|
|
6578
6633
|
const members = [];
|
|
6579
6634
|
for (const entry of pool.identities) {
|
|
6580
|
-
const nestedName = poolNameOf(entry);
|
|
6635
|
+
const nestedName = poolNameOf(poolMemberIdentity(entry));
|
|
6581
6636
|
if (nestedName === void 0) {
|
|
6582
|
-
const member = loaded.get(entry);
|
|
6637
|
+
const member = loaded.get(poolMemberIdentity(entry));
|
|
6583
6638
|
if (member !== void 0) {
|
|
6584
|
-
members.push(member);
|
|
6639
|
+
members.push(typeof entry === "string" ? member : { ...member, policy: entry.when });
|
|
6585
6640
|
}
|
|
6586
6641
|
continue;
|
|
6587
6642
|
}
|
|
6588
6643
|
if (input.pools[nestedName] === void 0) {
|
|
6589
|
-
throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
|
|
6644
|
+
throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" names a pool that is not defined.`);
|
|
6590
6645
|
}
|
|
6591
6646
|
if (childStack.includes(nestedName)) {
|
|
6592
|
-
throw new PoolGraphError(`Pool "${poolName}" member "${entry}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
|
|
6647
|
+
throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
|
|
6593
6648
|
}
|
|
6594
6649
|
const nested = nestedMember(input, nestedName, childStack, sticky);
|
|
6595
|
-
members.push(nested.member);
|
|
6650
|
+
members.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
|
|
6596
6651
|
missing.push(...nested.missing);
|
|
6597
6652
|
}
|
|
6598
6653
|
const ranking = rankPool({
|
|
@@ -6853,7 +6908,7 @@ function writePools(paths, pools) {
|
|
|
6853
6908
|
function poolCycleOf(pools, name) {
|
|
6854
6909
|
const walk = (current, path32) => {
|
|
6855
6910
|
for (const entry of pools[current]?.identities ?? []) {
|
|
6856
|
-
const nested = poolNameOf(entry);
|
|
6911
|
+
const nested = poolNameOf(poolMemberIdentity(entry));
|
|
6857
6912
|
if (nested === void 0) {
|
|
6858
6913
|
continue;
|
|
6859
6914
|
}
|
|
@@ -6874,9 +6929,10 @@ function poolCycleOf(pools, name) {
|
|
|
6874
6929
|
}
|
|
6875
6930
|
function validateMembers(pools, name, members) {
|
|
6876
6931
|
for (const entry of members) {
|
|
6877
|
-
const
|
|
6932
|
+
const identity = poolMemberIdentity(entry);
|
|
6933
|
+
const nested = poolNameOf(identity);
|
|
6878
6934
|
if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
|
|
6879
|
-
throw new UsageError(`Member "${
|
|
6935
|
+
throw new UsageError(`Member "${identity}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
|
|
6880
6936
|
}
|
|
6881
6937
|
}
|
|
6882
6938
|
const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members] } }, name);
|
|
@@ -9581,16 +9637,16 @@ function runDoctor(params) {
|
|
|
9581
9637
|
const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
|
|
9582
9638
|
const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
|
|
9583
9639
|
for (const [poolName, pool] of Object.entries(pools)) {
|
|
9584
|
-
const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
|
|
9640
|
+
const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
|
|
9585
9641
|
const undefinedPools = pool.identities.flatMap((member) => {
|
|
9586
|
-
const nested = poolNameOf(member);
|
|
9642
|
+
const nested = poolNameOf(poolMemberIdentity(member));
|
|
9587
9643
|
return nested === void 0 || nested in pools ? [] : [nested];
|
|
9588
9644
|
});
|
|
9589
9645
|
if (missing.length === 0 && undefinedPools.length === 0) {
|
|
9590
|
-
push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
|
|
9646
|
+
push("pool", "pass", `${poolName} is valid (${pool.identities.map(poolMemberIdentity).join(", ")}).`, poolName);
|
|
9591
9647
|
} else {
|
|
9592
9648
|
const problems = [
|
|
9593
|
-
...missing.map((member) => `identity "${member}", which does not exist`),
|
|
9649
|
+
...missing.map((member) => `identity "${poolMemberIdentity(member)}", which does not exist`),
|
|
9594
9650
|
...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
|
|
9595
9651
|
];
|
|
9596
9652
|
push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as zod from 'zod';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';
|
|
6
6
|
import * as net from 'node:net';
|
|
7
|
-
import { Duplex } from 'node:stream';
|
|
7
|
+
import { Duplex, Readable } from 'node:stream';
|
|
8
8
|
import * as _orpc_client from '@orpc/client';
|
|
9
9
|
import * as zod_v4_core from 'zod/v4/core';
|
|
10
10
|
import * as _orpc_server from '@orpc/server';
|
|
@@ -446,6 +446,81 @@ declare const ProviderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
446
446
|
}>>;
|
|
447
447
|
}, z.core.$strict>;
|
|
448
448
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
449
|
+
routes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
450
|
+
when: z.ZodUnion<readonly [z.ZodObject<{
|
|
451
|
+
newerThan: z.ZodOptional<z.ZodString>;
|
|
452
|
+
olderThan: z.ZodOptional<z.ZodString>;
|
|
453
|
+
maxSizeBytes: z.ZodOptional<z.ZodInt>;
|
|
454
|
+
branch: z.ZodOptional<z.ZodString>;
|
|
455
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
456
|
+
}, z.core.$strict>, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
457
|
+
kind: z.ZodLiteral<"not">;
|
|
458
|
+
operand: typeof PredicateNodeSchema;
|
|
459
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
460
|
+
kind: z.ZodLiteral<"and">;
|
|
461
|
+
left: typeof PredicateNodeSchema;
|
|
462
|
+
right: typeof PredicateNodeSchema;
|
|
463
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
464
|
+
kind: z.ZodLiteral<"or">;
|
|
465
|
+
left: typeof PredicateNodeSchema;
|
|
466
|
+
right: typeof PredicateNodeSchema;
|
|
467
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
468
|
+
kind: z.ZodLiteral<"allOf">;
|
|
469
|
+
operands: z.ZodArray<typeof PredicateNodeSchema>;
|
|
470
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
471
|
+
kind: z.ZodLiteral<"anyOf">;
|
|
472
|
+
operands: z.ZodArray<typeof PredicateNodeSchema>;
|
|
473
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
474
|
+
kind: z.ZodLiteral<"compare">;
|
|
475
|
+
op: z.ZodEnum<{
|
|
476
|
+
gt: "gt";
|
|
477
|
+
gte: "gte";
|
|
478
|
+
lt: "lt";
|
|
479
|
+
lte: "lte";
|
|
480
|
+
eq: "eq";
|
|
481
|
+
neq: "neq";
|
|
482
|
+
}>;
|
|
483
|
+
left: typeof trilean.ExpressionNodeSchema;
|
|
484
|
+
right: typeof trilean.ExpressionNodeSchema;
|
|
485
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
486
|
+
kind: z.ZodLiteral<"textCompare">;
|
|
487
|
+
op: z.ZodEnum<{
|
|
488
|
+
equals: "equals";
|
|
489
|
+
notEquals: "notEquals";
|
|
490
|
+
matches: "matches";
|
|
491
|
+
notMatches: "notMatches";
|
|
492
|
+
portableMatches: "portableMatches";
|
|
493
|
+
portableNotMatches: "portableNotMatches";
|
|
494
|
+
}>;
|
|
495
|
+
left: typeof trilean.ExpressionNodeSchema;
|
|
496
|
+
right: typeof trilean.ExpressionNodeSchema;
|
|
497
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
498
|
+
kind: z.ZodLiteral<"memberOf">;
|
|
499
|
+
op: z.ZodEnum<{
|
|
500
|
+
in: "in";
|
|
501
|
+
notIn: "notIn";
|
|
502
|
+
}>;
|
|
503
|
+
operand: typeof trilean.ExpressionNodeSchema;
|
|
504
|
+
candidates: z.ZodArray<typeof trilean.ExpressionNodeSchema>;
|
|
505
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
506
|
+
kind: z.ZodLiteral<"exists">;
|
|
507
|
+
operand: typeof trilean.ExpressionNodeSchema;
|
|
508
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
509
|
+
kind: z.ZodLiteral<"some">;
|
|
510
|
+
collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
|
|
511
|
+
item: typeof PredicateNodeSchema;
|
|
512
|
+
filter: z.ZodOptional<typeof PredicateNodeSchema>;
|
|
513
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
514
|
+
kind: z.ZodLiteral<"every">;
|
|
515
|
+
collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
|
|
516
|
+
item: typeof PredicateNodeSchema;
|
|
517
|
+
filter: z.ZodOptional<typeof PredicateNodeSchema>;
|
|
518
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
519
|
+
kind: z.ZodLiteral<"treeReference">;
|
|
520
|
+
key: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
|
|
521
|
+
}, z.core.$strip>], "kind">]>;
|
|
522
|
+
provider: z.ZodString;
|
|
523
|
+
}, z.core.$strict>>>;
|
|
449
524
|
}, z.core.$strict>, z.ZodObject<{
|
|
450
525
|
$schema: z.ZodOptional<z.ZodString>;
|
|
451
526
|
kind: z.ZodLiteral<"codex">;
|
|
@@ -630,7 +705,81 @@ type PoolPreference = z.infer<typeof PoolPreferenceSchema>;
|
|
|
630
705
|
* `preference` decides how the member is chosen, and an absent field means "score". "score" ranks members by plan-size-weighted remaining quota per hour until reset, so the quota closest to expiring is spent first (use-it-or-lose-it). "listed" ranks members strictly in the order the pool lists them, skipping only a member that is currently refused, so a pool can pin one identity first and fall back to the others only while that one is refused.
|
|
631
706
|
*/
|
|
632
707
|
declare const PoolSchema: z.ZodObject<{
|
|
633
|
-
identities: z.ZodArray<z.ZodString
|
|
708
|
+
identities: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
709
|
+
identity: z.ZodString;
|
|
710
|
+
when: z.ZodUnion<readonly [z.ZodObject<{
|
|
711
|
+
newerThan: z.ZodOptional<z.ZodString>;
|
|
712
|
+
olderThan: z.ZodOptional<z.ZodString>;
|
|
713
|
+
maxSizeBytes: z.ZodOptional<z.ZodInt>;
|
|
714
|
+
branch: z.ZodOptional<z.ZodString>;
|
|
715
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
716
|
+
}, z.core.$strict>, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
717
|
+
kind: z.ZodLiteral<"not">;
|
|
718
|
+
operand: typeof PredicateNodeSchema;
|
|
719
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
720
|
+
kind: z.ZodLiteral<"and">;
|
|
721
|
+
left: typeof PredicateNodeSchema;
|
|
722
|
+
right: typeof PredicateNodeSchema;
|
|
723
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
724
|
+
kind: z.ZodLiteral<"or">;
|
|
725
|
+
left: typeof PredicateNodeSchema;
|
|
726
|
+
right: typeof PredicateNodeSchema;
|
|
727
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
728
|
+
kind: z.ZodLiteral<"allOf">;
|
|
729
|
+
operands: z.ZodArray<typeof PredicateNodeSchema>;
|
|
730
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
731
|
+
kind: z.ZodLiteral<"anyOf">;
|
|
732
|
+
operands: z.ZodArray<typeof PredicateNodeSchema>;
|
|
733
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
734
|
+
kind: z.ZodLiteral<"compare">;
|
|
735
|
+
op: z.ZodEnum<{
|
|
736
|
+
gt: "gt";
|
|
737
|
+
gte: "gte";
|
|
738
|
+
lt: "lt";
|
|
739
|
+
lte: "lte";
|
|
740
|
+
eq: "eq";
|
|
741
|
+
neq: "neq";
|
|
742
|
+
}>;
|
|
743
|
+
left: typeof trilean.ExpressionNodeSchema;
|
|
744
|
+
right: typeof trilean.ExpressionNodeSchema;
|
|
745
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
746
|
+
kind: z.ZodLiteral<"textCompare">;
|
|
747
|
+
op: z.ZodEnum<{
|
|
748
|
+
equals: "equals";
|
|
749
|
+
notEquals: "notEquals";
|
|
750
|
+
matches: "matches";
|
|
751
|
+
notMatches: "notMatches";
|
|
752
|
+
portableMatches: "portableMatches";
|
|
753
|
+
portableNotMatches: "portableNotMatches";
|
|
754
|
+
}>;
|
|
755
|
+
left: typeof trilean.ExpressionNodeSchema;
|
|
756
|
+
right: typeof trilean.ExpressionNodeSchema;
|
|
757
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
758
|
+
kind: z.ZodLiteral<"memberOf">;
|
|
759
|
+
op: z.ZodEnum<{
|
|
760
|
+
in: "in";
|
|
761
|
+
notIn: "notIn";
|
|
762
|
+
}>;
|
|
763
|
+
operand: typeof trilean.ExpressionNodeSchema;
|
|
764
|
+
candidates: z.ZodArray<typeof trilean.ExpressionNodeSchema>;
|
|
765
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
766
|
+
kind: z.ZodLiteral<"exists">;
|
|
767
|
+
operand: typeof trilean.ExpressionNodeSchema;
|
|
768
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
769
|
+
kind: z.ZodLiteral<"some">;
|
|
770
|
+
collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
|
|
771
|
+
item: typeof PredicateNodeSchema;
|
|
772
|
+
filter: z.ZodOptional<typeof PredicateNodeSchema>;
|
|
773
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
774
|
+
kind: z.ZodLiteral<"every">;
|
|
775
|
+
collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
|
|
776
|
+
item: typeof PredicateNodeSchema;
|
|
777
|
+
filter: z.ZodOptional<typeof PredicateNodeSchema>;
|
|
778
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
779
|
+
kind: z.ZodLiteral<"treeReference">;
|
|
780
|
+
key: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
|
|
781
|
+
}, z.core.$strip>], "kind">]>;
|
|
782
|
+
}, z.core.$strict>]>>;
|
|
634
783
|
preference: z.ZodOptional<z.ZodEnum<{
|
|
635
784
|
score: "score";
|
|
636
785
|
listed: "listed";
|
|
@@ -1332,7 +1481,81 @@ declare const GlobalConfigSchema: z.ZodObject<{
|
|
|
1332
1481
|
}>>;
|
|
1333
1482
|
}, z.core.$strict>>;
|
|
1334
1483
|
pools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
1335
|
-
identities: z.ZodArray<z.ZodString
|
|
1484
|
+
identities: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
1485
|
+
identity: z.ZodString;
|
|
1486
|
+
when: z.ZodUnion<readonly [z.ZodObject<{
|
|
1487
|
+
newerThan: z.ZodOptional<z.ZodString>;
|
|
1488
|
+
olderThan: z.ZodOptional<z.ZodString>;
|
|
1489
|
+
maxSizeBytes: z.ZodOptional<z.ZodInt>;
|
|
1490
|
+
branch: z.ZodOptional<z.ZodString>;
|
|
1491
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
1492
|
+
}, z.core.$strict>, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1493
|
+
kind: z.ZodLiteral<"not">;
|
|
1494
|
+
operand: typeof PredicateNodeSchema;
|
|
1495
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1496
|
+
kind: z.ZodLiteral<"and">;
|
|
1497
|
+
left: typeof PredicateNodeSchema;
|
|
1498
|
+
right: typeof PredicateNodeSchema;
|
|
1499
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1500
|
+
kind: z.ZodLiteral<"or">;
|
|
1501
|
+
left: typeof PredicateNodeSchema;
|
|
1502
|
+
right: typeof PredicateNodeSchema;
|
|
1503
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1504
|
+
kind: z.ZodLiteral<"allOf">;
|
|
1505
|
+
operands: z.ZodArray<typeof PredicateNodeSchema>;
|
|
1506
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1507
|
+
kind: z.ZodLiteral<"anyOf">;
|
|
1508
|
+
operands: z.ZodArray<typeof PredicateNodeSchema>;
|
|
1509
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1510
|
+
kind: z.ZodLiteral<"compare">;
|
|
1511
|
+
op: z.ZodEnum<{
|
|
1512
|
+
gt: "gt";
|
|
1513
|
+
gte: "gte";
|
|
1514
|
+
lt: "lt";
|
|
1515
|
+
lte: "lte";
|
|
1516
|
+
eq: "eq";
|
|
1517
|
+
neq: "neq";
|
|
1518
|
+
}>;
|
|
1519
|
+
left: typeof trilean.ExpressionNodeSchema;
|
|
1520
|
+
right: typeof trilean.ExpressionNodeSchema;
|
|
1521
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1522
|
+
kind: z.ZodLiteral<"textCompare">;
|
|
1523
|
+
op: z.ZodEnum<{
|
|
1524
|
+
equals: "equals";
|
|
1525
|
+
notEquals: "notEquals";
|
|
1526
|
+
matches: "matches";
|
|
1527
|
+
notMatches: "notMatches";
|
|
1528
|
+
portableMatches: "portableMatches";
|
|
1529
|
+
portableNotMatches: "portableNotMatches";
|
|
1530
|
+
}>;
|
|
1531
|
+
left: typeof trilean.ExpressionNodeSchema;
|
|
1532
|
+
right: typeof trilean.ExpressionNodeSchema;
|
|
1533
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1534
|
+
kind: z.ZodLiteral<"memberOf">;
|
|
1535
|
+
op: z.ZodEnum<{
|
|
1536
|
+
in: "in";
|
|
1537
|
+
notIn: "notIn";
|
|
1538
|
+
}>;
|
|
1539
|
+
operand: typeof trilean.ExpressionNodeSchema;
|
|
1540
|
+
candidates: z.ZodArray<typeof trilean.ExpressionNodeSchema>;
|
|
1541
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1542
|
+
kind: z.ZodLiteral<"exists">;
|
|
1543
|
+
operand: typeof trilean.ExpressionNodeSchema;
|
|
1544
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1545
|
+
kind: z.ZodLiteral<"some">;
|
|
1546
|
+
collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
|
|
1547
|
+
item: typeof PredicateNodeSchema;
|
|
1548
|
+
filter: z.ZodOptional<typeof PredicateNodeSchema>;
|
|
1549
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1550
|
+
kind: z.ZodLiteral<"every">;
|
|
1551
|
+
collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
|
|
1552
|
+
item: typeof PredicateNodeSchema;
|
|
1553
|
+
filter: z.ZodOptional<typeof PredicateNodeSchema>;
|
|
1554
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1555
|
+
kind: z.ZodLiteral<"treeReference">;
|
|
1556
|
+
key: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
|
|
1557
|
+
}, z.core.$strip>], "kind">]>;
|
|
1558
|
+
}, z.core.$strict>]>>;
|
|
1336
1559
|
preference: z.ZodOptional<z.ZodEnum<{
|
|
1337
1560
|
score: "score";
|
|
1338
1561
|
listed: "listed";
|
|
@@ -2517,8 +2740,8 @@ interface RoutedRequest {
|
|
|
2517
2740
|
readonly url: string;
|
|
2518
2741
|
/** The request's headers with every internal header already stripped: exactly what a route may forward onwards. */
|
|
2519
2742
|
readonly headers: IncomingHttpHeaders;
|
|
2520
|
-
/** The request's body stream: a route either pipes it onward untouched or reads it whole, never both. */
|
|
2521
|
-
readonly body:
|
|
2743
|
+
/** The request's body stream: a route either pipes it onward untouched or reads it whole, never both. A readable, not the socket-bound type the pipeline received it as, because the routing layer's scan hands routes a replayed body (the scanned head re-emitted in front of the unread remainder) that is a plain stream. */
|
|
2744
|
+
readonly body: Readable;
|
|
2522
2745
|
/** Aborted the moment the client goes away; a route aborts its own upstream work with it. */
|
|
2523
2746
|
readonly signal: AbortSignal;
|
|
2524
2747
|
readonly session: SessionIdentity;
|
|
@@ -6784,6 +7007,8 @@ interface PoolMember {
|
|
|
6784
7007
|
readonly records: readonly UsageRecord[];
|
|
6785
7008
|
/** Present when this member stands for a nested `pool:<name>` entry rather than a direct identity. */
|
|
6786
7009
|
readonly nested?: NestedContribution;
|
|
7010
|
+
/** The member entry's policy condition, from the object form of a pool member: definite false skips the member as ineligible by policy, indeterminate demotes it to last within its class, naming the missing fact either way. */
|
|
7011
|
+
readonly policy?: WhenCondition;
|
|
6787
7012
|
}
|
|
6788
7013
|
/** The member last picked for this directory, kept so a conversation stays on the account whose prompt cache is warm. */
|
|
6789
7014
|
interface StickyPick {
|
|
@@ -6811,6 +7036,8 @@ interface Candidate {
|
|
|
6811
7036
|
/** When an `ineligible` candidate can be used again. */
|
|
6812
7037
|
readonly blockedUntilMs?: number;
|
|
6813
7038
|
readonly plan: PlanClass;
|
|
7039
|
+
/** True when the member's policy condition could not be decided and it is ranked last within its class, naming the missing fact in its reasons. */
|
|
7040
|
+
readonly policyDemoted?: boolean;
|
|
6814
7041
|
/** Human-readable facts behind the class and score, in the order they matter. */
|
|
6815
7042
|
readonly reasons: readonly string[];
|
|
6816
7043
|
}
|
|
@@ -6852,6 +7079,15 @@ declare function collectPoolPick(params: Readonly<{
|
|
|
6852
7079
|
nowMs: number;
|
|
6853
7080
|
}>): PoolPickReport;
|
|
6854
7081
|
|
|
7082
|
+
/** One quota window's pool-selection facts, in the units a policy reads: fractions of the window, and hours until it resets. */
|
|
7083
|
+
interface PoolWindowFacts {
|
|
7084
|
+
/** The fraction of the window still unused, between zero and one. */
|
|
7085
|
+
readonly remaining: number;
|
|
7086
|
+
/** The fraction of the window already used, between zero and one. */
|
|
7087
|
+
readonly utilization: number;
|
|
7088
|
+
/** How long until the window resets, in hours. */
|
|
7089
|
+
readonly hoursUntilReset: number;
|
|
7090
|
+
}
|
|
6855
7091
|
/** Everything a `when` condition can be evaluated against, injected rather than read. */
|
|
6856
7092
|
interface ConditionContext {
|
|
6857
7093
|
readonly nowMs: number;
|
|
@@ -6862,6 +7098,23 @@ interface ConditionContext {
|
|
|
6862
7098
|
/** True when the repository is in detached-HEAD state, in which case no `branch` condition can match. */
|
|
6863
7099
|
readonly branchDetached?: boolean;
|
|
6864
7100
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
7101
|
+
/**
|
|
7102
|
+
* The pool-selection facts a member entry's policy condition reads. Present only when the condition guards a pool member, so the same reference namespace serves the cascade and the pool: `quota.fiveHour.remaining`, `quota.fiveHour.utilization`, `quota.fiveHour.hoursUntilReset`, the same three under `quota.sevenDay`, `quota.burnPerHour` (the five-hour window's observed utilisation pace, in fractions per hour), `session.resuming` (this launch resumes or continues a conversation) and `session.lastPickHoursAgo` (how long ago this directory last picked this member). Conversation context is deliberately not provided: nothing the door holds knows it, so a condition referencing it is indeterminate, naming the missing fact, rather than guessed.
|
|
7103
|
+
*/
|
|
7104
|
+
readonly pool?: {
|
|
7105
|
+
readonly fiveHour?: PoolWindowFacts;
|
|
7106
|
+
readonly sevenDay?: PoolWindowFacts;
|
|
7107
|
+
readonly burnPerHour?: number;
|
|
7108
|
+
readonly resuming?: boolean;
|
|
7109
|
+
readonly lastPickHoursAgo?: number;
|
|
7110
|
+
};
|
|
7111
|
+
/**
|
|
7112
|
+
* The request facts a provider's route condition reads, present only when the condition routes a request the door is resolving: `request.model` (the Messages body's own model field) and `request.hasImage` (an image block anywhere in the scanned head of the body). A request the door could not read that far carries neither, so a condition naming them is indeterminate and the routing falls through, never to a cheaper provider by accident.
|
|
7113
|
+
*/
|
|
7114
|
+
readonly request?: {
|
|
7115
|
+
readonly model?: string;
|
|
7116
|
+
readonly hasImage?: boolean;
|
|
7117
|
+
};
|
|
6865
7118
|
}
|
|
6866
7119
|
/**
|
|
6867
7120
|
* The result of evaluating one `when`: definite, with which condition fields were present and which of those did not hold, or indeterminate, when a datum the condition needed is missing. Indeterminate is not false: a missing fact has not failed the condition, it has left it undecided, and the consumer decides what an undecided entry does (the resolver keeps today's behaviour of not passing it, so nothing changes for existing configurations; the pool ranking of #258 instead keeps such a member but ranks it last, naming the missing fact).
|
|
@@ -7215,6 +7468,9 @@ declare function updateProvider(paths: LayoutPaths, name: string, input: UpdateP
|
|
|
7215
7468
|
/** Deletes a provider definition. Throws `ProviderNotFoundError` when it does not exist. */
|
|
7216
7469
|
declare function removeProvider(paths: LayoutPaths, name: string): void;
|
|
7217
7470
|
|
|
7471
|
+
/** One pool member entry, whichever form it takes: the plain selector string, or the object form carrying its policy condition. */
|
|
7472
|
+
type PoolMemberEntry = Pool["identities"][number];
|
|
7473
|
+
|
|
7218
7474
|
/** Raised when a command names a pool that is not defined in the global config. */
|
|
7219
7475
|
declare class PoolNotFoundError extends CliError {
|
|
7220
7476
|
readonly poolName: string;
|
|
@@ -7225,9 +7481,9 @@ declare function readPools(paths: LayoutPaths): Readonly<Record<string, Pool>>;
|
|
|
7225
7481
|
/** The pool named `name`; throws `PoolNotFoundError` when none is defined. */
|
|
7226
7482
|
declare function requirePool(paths: LayoutPaths, name: string): Pool;
|
|
7227
7483
|
/** Defines a new pool. Throws `InvalidPoolNameError` for a name `PoolNameSchema` rejects, `PoolAlreadyExistsError` for one already defined, and `UsageError` for a nested member that names an undefined pool or a cycle. */
|
|
7228
|
-
declare function addPool(paths: LayoutPaths, name: string, identities: readonly
|
|
7484
|
+
declare function addPool(paths: LayoutPaths, name: string, identities: readonly PoolMemberEntry[], preference?: PoolPreference): Pool;
|
|
7229
7485
|
/** Replaces an existing pool's members and preference. Throws `PoolNotFoundError` when it is not defined, and `UsageError` for a nested member that names an undefined pool or a cycle. */
|
|
7230
|
-
declare function setPool(paths: LayoutPaths, name: string, identities: readonly
|
|
7486
|
+
declare function setPool(paths: LayoutPaths, name: string, identities: readonly PoolMemberEntry[], preference?: PoolPreference): Pool;
|
|
7231
7487
|
/** Removes a pool. Throws `PoolNotFoundError` when it is not defined. Anything that selected it (an active-identity file, a directory rule) is left for `doctor` to report, as with a removed identity. */
|
|
7232
7488
|
declare function removePool(paths: LayoutPaths, name: string): void;
|
|
7233
7489
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-shim",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.35.0",
|
|
4
4
|
"description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Joseph Mearman <joseph@mearman.co.uk>",
|