@bman654/clodex 2.1.4 → 2.1.5

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.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  ensureSecureAppHome,
5
5
  findClaudeBinary,
6
6
  getAppHome,
7
+ getClaudeVersionForBinary,
7
8
  getCredentialCleanupPath,
8
9
  getCredentialMutationLockPath,
9
10
  getCredentialStateRoot,
@@ -38,7 +39,7 @@ import {
38
39
  withProviderMutationLock,
39
40
  withRegistryWriteLock,
40
41
  withRegistryWriteLockSync
41
- } from "./chunk-OVO6OUZG.js";
42
+ } from "./chunk-VQOX3AXE.js";
42
43
 
43
44
  // src/cli.ts
44
45
  import pc13 from "picocolors";
@@ -216,7 +217,7 @@ import { join } from "path";
216
217
  // package.json
217
218
  var package_default = {
218
219
  name: "@bman654/clodex",
219
- version: "2.1.4",
220
+ version: "2.1.5",
220
221
  publishConfig: {
221
222
  access: "public"
222
223
  },
@@ -5538,6 +5539,7 @@ async function createLanguageModel(spec) {
5538
5539
  }
5539
5540
  var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
5540
5541
  var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
5542
+ var GPT_56_EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
5541
5543
  var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
5542
5544
  var MISTRAL_EFFORT_LEVELS = ["high", "off"];
5543
5545
  var XAI_EFFORT_LEVELS = ["none", "low", "medium", "high"];
@@ -5701,7 +5703,13 @@ function mapCodexEffortToAnthropic(effort) {
5701
5703
  return void 0;
5702
5704
  }
5703
5705
  }
5704
- function mapCodexEffortToOpenAI(effort) {
5706
+ function isGpt56Model(modelId) {
5707
+ return /^gpt-5\.6(?:-|$)/i.test(modelId);
5708
+ }
5709
+ function mapCodexEffortToOpenAI(effort, modelId) {
5710
+ if (modelId && isGpt56Model(modelId) && GPT_56_EFFORT_LEVELS.includes(effort)) {
5711
+ return effort;
5712
+ }
5705
5713
  if (effort === "xhigh") return "high";
5706
5714
  const allowed = ["low", "medium", "high"];
5707
5715
  return allowed.includes(effort) ? effort : void 0;
@@ -5782,7 +5790,7 @@ function getReasoningCapabilities(npm, modelId, metadata) {
5782
5790
  const prefersResponses = modelPrefersResponsesApi(modelId);
5783
5791
  if (prefersResponses || metadata?.reasoning) {
5784
5792
  return {
5785
- levels: [...OPENAI_EFFORT_LEVELS],
5793
+ levels: isGpt56Model(modelId) ? [...GPT_56_EFFORT_LEVELS] : [...OPENAI_EFFORT_LEVELS],
5786
5794
  defaultLevel: "medium",
5787
5795
  supportsSummaries: true,
5788
5796
  mode: "controllable",
@@ -5904,6 +5912,24 @@ function getReasoningCapabilities(npm, modelId, metadata) {
5904
5912
  }
5905
5913
  return EMPTY_REASONING;
5906
5914
  }
5915
+ function getPatchReasoningCapabilities(npm, modelId, metadata) {
5916
+ if (metadata?.reasoning === false && !hasSupportedParameter(metadata, "reasoning_effort") && !hasSupportedParameter(metadata, "reasoning")) {
5917
+ return EMPTY_REASONING;
5918
+ }
5919
+ const capabilities = getReasoningCapabilities(npm, modelId, metadata);
5920
+ const seenProviderOptions = /* @__PURE__ */ new Set();
5921
+ return {
5922
+ ...capabilities,
5923
+ levels: capabilities.levels.filter((level) => {
5924
+ const providerOptions = effortProviderOptions(npm, level, modelId, metadata);
5925
+ if (!providerOptions) return false;
5926
+ const fingerprint = JSON.stringify(providerOptions);
5927
+ if (seenProviderOptions.has(fingerprint)) return false;
5928
+ seenProviderOptions.add(fingerprint);
5929
+ return true;
5930
+ })
5931
+ };
5932
+ }
5907
5933
  function effortProviderOptions(npm, effort, modelId, metadata) {
5908
5934
  if (!effort) return void 0;
5909
5935
  if (isOpenRouterRoute(npm, metadata)) {
@@ -5915,7 +5941,7 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
5915
5941
  }
5916
5942
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
5917
5943
  if (!modelId || !modelPrefersResponsesApi(modelId)) return void 0;
5918
- const reasoningEffort = mapCodexEffortToOpenAI(effort);
5944
+ const reasoningEffort = mapCodexEffortToOpenAI(effort, modelId);
5919
5945
  return reasoningEffort ? { openai: { reasoningEffort } } : void 0;
5920
5946
  }
5921
5947
  if (npm === "@ai-sdk/xai") {
@@ -8817,31 +8843,151 @@ function sendJson(res, status, body) {
8817
8843
  }
8818
8844
 
8819
8845
  // src/model-aliases.ts
8820
- var MODEL_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
8821
- function isValidModelAlias(name) {
8822
- return MODEL_ALIAS_PATTERN.test(name);
8846
+ var MODEL_ALIAS_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
8847
+ var RESERVED_MODEL_ALIASES = /* @__PURE__ */ new Set([
8848
+ "sonnet",
8849
+ "opus",
8850
+ "haiku",
8851
+ "fable",
8852
+ "best",
8853
+ "default",
8854
+ "opusplan",
8855
+ "inherit"
8856
+ ]);
8857
+ function canonicalModelAliasName(name) {
8858
+ return name.trim().toLowerCase();
8859
+ }
8860
+ function isReservedModelAlias(name) {
8861
+ return RESERVED_MODEL_ALIASES.has(
8862
+ stripOneMContextSuffix(canonicalModelAliasName(name))
8863
+ );
8864
+ }
8865
+ function isModelAliasNameSyntax(name) {
8866
+ return MODEL_ALIAS_PATTERN.test(canonicalModelAliasName(name));
8867
+ }
8868
+ function modelAliasMatchesName(value, name) {
8869
+ if (!value || typeof value !== "object" || !("name" in value)) return false;
8870
+ const candidate = value.name;
8871
+ return typeof candidate === "string" && canonicalModelAliasName(candidate) === canonicalModelAliasName(name);
8872
+ }
8873
+ function modelAliasMatchesStoredName(value, name) {
8874
+ if (!value || typeof value !== "object" || !("name" in value)) return false;
8875
+ const candidate = value.name;
8876
+ if (typeof candidate !== "string") return false;
8877
+ const requested = name.trim();
8878
+ return isModelAliasNameSyntax(requested) ? canonicalModelAliasName(candidate) === canonicalModelAliasName(requested) : candidate.trim() === requested;
8879
+ }
8880
+ function describeModelAliasRejection(reason) {
8881
+ switch (reason) {
8882
+ case "invalid-name":
8883
+ return "invalid name";
8884
+ case "reserved-name":
8885
+ return "reserved client name";
8886
+ case "invalid-target":
8887
+ return "invalid target";
8888
+ case "conflicting-targets":
8889
+ return "conflicting targets";
8890
+ case "target-not-favorite":
8891
+ return "target is not a saved favorite";
8892
+ }
8893
+ }
8894
+ function normalizeModelAliases(value) {
8895
+ if (value === void 0) {
8896
+ return { aliases: [], accepted: [], rejected: [], rejections: [] };
8897
+ }
8898
+ if (!Array.isArray(value)) {
8899
+ throw new TypeError('Saved model aliases are malformed: "modelAliases" must be an array.');
8900
+ }
8901
+ const candidates = [];
8902
+ const groups = /* @__PURE__ */ new Map();
8903
+ for (const [index, item] of value.entries()) {
8904
+ if (!item || typeof item !== "object" || typeof item.name !== "string") {
8905
+ throw new TypeError(
8906
+ `Saved model aliases are malformed: "modelAliases[${index}]" must be an object with a string "name".`
8907
+ );
8908
+ }
8909
+ const source = item;
8910
+ const candidate = { source };
8911
+ candidates.push(candidate);
8912
+ if (typeof source.providerId !== "string" || typeof source.modelId !== "string") {
8913
+ candidate.rejectionReason = "invalid-target";
8914
+ continue;
8915
+ }
8916
+ const normalized = {
8917
+ name: canonicalModelAliasName(source.name),
8918
+ providerId: source.providerId,
8919
+ modelId: source.modelId
8920
+ };
8921
+ if (!isModelAliasNameSyntax(normalized.name)) {
8922
+ candidate.rejectionReason = "invalid-name";
8923
+ continue;
8924
+ }
8925
+ if (isReservedModelAlias(normalized.name)) {
8926
+ candidate.rejectionReason = "reserved-name";
8927
+ continue;
8928
+ }
8929
+ if (!normalized.providerId.trim() || !normalized.modelId.trim()) {
8930
+ candidate.rejectionReason = "invalid-target";
8931
+ continue;
8932
+ }
8933
+ candidate.normalized = normalized;
8934
+ const group = groups.get(normalized.name) ?? [];
8935
+ group.push(candidate);
8936
+ groups.set(normalized.name, group);
8937
+ }
8938
+ for (const group of groups.values()) {
8939
+ const targets = new Set(
8940
+ group.map((candidate) => `${candidate.normalized.providerId}\0${candidate.normalized.modelId}`)
8941
+ );
8942
+ if (targets.size === 1) {
8943
+ group[0].accepted = true;
8944
+ group[0].sources = group.map((candidate) => candidate.source);
8945
+ } else {
8946
+ for (const candidate of group) candidate.rejectionReason = "conflicting-targets";
8947
+ }
8948
+ }
8949
+ const rejections = candidates.filter((candidate) => candidate.rejectionReason !== void 0).map((candidate) => ({
8950
+ alias: candidate.source,
8951
+ reason: candidate.rejectionReason
8952
+ }));
8953
+ const accepted = candidates.filter((candidate) => candidate.accepted === true && candidate.normalized !== void 0).map((candidate) => ({
8954
+ alias: candidate.normalized,
8955
+ source: candidate.source,
8956
+ sources: candidate.sources ?? [candidate.source]
8957
+ }));
8958
+ return {
8959
+ aliases: accepted.map((entry) => entry.alias),
8960
+ accepted,
8961
+ rejected: rejections.map((rejection) => rejection.alias),
8962
+ rejections
8963
+ };
8823
8964
  }
8824
8965
  function parseModelAliasAssignment(value) {
8825
8966
  const separator = value.indexOf("=");
8826
8967
  if (separator < 1 || separator === value.length - 1) {
8827
8968
  return { error: "Alias must use name=clodex:<provider-id>:<model-id>." };
8828
8969
  }
8829
- const name = value.slice(0, separator).trim();
8830
- if (!isValidModelAlias(name)) {
8970
+ const name = canonicalModelAliasName(value.slice(0, separator));
8971
+ if (!MODEL_ALIAS_PATTERN.test(name)) {
8831
8972
  return { error: "Alias names must be 1-64 letters, numbers, dots, underscores, or hyphens." };
8832
8973
  }
8974
+ if (isReservedModelAlias(name)) {
8975
+ return { error: "That alias name is reserved by the client." };
8976
+ }
8833
8977
  const rawTarget = value.slice(separator + 1).trim();
8834
8978
  const target = rawTarget.startsWith("clodex:") ? rawTarget.slice("clodex:".length) : rawTarget;
8835
8979
  const targetSeparator = target.indexOf(":");
8836
- if (targetSeparator < 1 || targetSeparator === target.length - 1) {
8980
+ const providerId = target.slice(0, targetSeparator).trim();
8981
+ const modelId = stripOneMContextSuffix(target.slice(targetSeparator + 1).trim());
8982
+ if (targetSeparator < 1 || targetSeparator === target.length - 1 || !providerId || !modelId) {
8837
8983
  return { error: "Alias target must use clodex:<provider-id>:<model-id>." };
8838
8984
  }
8839
8985
  return {
8840
8986
  name,
8841
- providerId: target.slice(0, targetSeparator),
8987
+ providerId,
8842
8988
  // `models --list` prints Claude's synthetic context suffix. It is a client
8843
8989
  // routing hint, not part of the provider catalog id stored in favorites.
8844
- modelId: stripOneMContextSuffix(target.slice(targetSeparator + 1))
8990
+ modelId
8845
8991
  };
8846
8992
  }
8847
8993
  function modelAliasTarget(alias) {
@@ -8888,11 +9034,32 @@ function makeRouteResolver(localProviders) {
8888
9034
  return provider && model ? localModelToRoute(provider, model) ?? void 0 : void 0;
8889
9035
  };
8890
9036
  }
8891
- function resolveCatalogModelAliases(modelAliases, resolveRoute) {
8892
- return modelAliases.map((alias) => ({
8893
- name: alias.name,
8894
- routeId: resolveRoute(alias.providerId, alias.modelId)?.aliasId ?? modelAliasTarget(alias)
8895
- }));
9037
+ function resolveCatalogModelAliases(modelAliases, resolveRoute, catalogRoutes = []) {
9038
+ const normalized = normalizeModelAliases(modelAliases);
9039
+ const catalogRouteIds = new Set(
9040
+ catalogRoutes.map((route) => normalizeRouteLookupId(route.aliasId))
9041
+ );
9042
+ return [
9043
+ ...normalized.accepted.map(({ alias, source, sources }) => {
9044
+ const route = resolveRoute(alias.providerId, alias.modelId);
9045
+ const collidesWithCatalog = catalogRouteIds.has(
9046
+ normalizeRouteLookupId(alias.name)
9047
+ );
9048
+ const sourceNames = [...new Set(sources.map((entry) => entry.name))];
9049
+ return {
9050
+ name: alias.name,
9051
+ ...source.name === alias.name ? {} : { savedName: source.name },
9052
+ ...sourceNames.length === 1 && sourceNames[0] === alias.name ? {} : { sourceNames },
9053
+ routeId: route?.aliasId ?? modelAliasTarget(alias),
9054
+ ...collidesWithCatalog ? { unavailableReason: "conflicts with a catalog model id" } : route ? {} : { unavailableReason: "target unavailable" }
9055
+ };
9056
+ }),
9057
+ ...normalized.rejections.map((rejection) => ({
9058
+ name: canonicalModelAliasName(rejection.alias.name),
9059
+ ...rejection.alias.name === canonicalModelAliasName(rejection.alias.name) ? {} : { savedName: rejection.alias.name },
9060
+ unavailableReason: describeModelAliasRejection(rejection.reason)
9061
+ }))
9062
+ ];
8896
9063
  }
8897
9064
  function buildCatalogRoutes(startingRoute, favorites, resolveRoute, max = MAX_MODEL_CATALOG) {
8898
9065
  const droppedFavorites = [];
@@ -8916,7 +9083,7 @@ function httpProxyModelId(providerId, modelId) {
8916
9083
  function httpProxyDisplayName(model, providerName) {
8917
9084
  return `${formatModelLabel(model)} (${providerName})`;
8918
9085
  }
8919
- function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX_MODEL_CATALOG) {
9086
+ function buildHttpProxyRoutes(providers, favorites, modelAliases = void 0, max = MAX_MODEL_CATALOG) {
8920
9087
  const routes = [];
8921
9088
  const unavailable = [];
8922
9089
  const unsupported = [];
@@ -8954,16 +9121,21 @@ function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX
8954
9121
  routesByFavorite.set(`${favorite.providerId}:${favorite.modelId}`, proxyRoute);
8955
9122
  }
8956
9123
  const aliases = [];
8957
- const unavailableAliases = [];
8958
- const seenAliases = /* @__PURE__ */ new Set();
8959
- for (const alias of modelAliases) {
9124
+ const normalizedAliases = normalizeModelAliases(modelAliases);
9125
+ const unavailableAliases = [...normalizedAliases.rejected];
9126
+ for (const { alias, sources } of normalizedAliases.accepted) {
8960
9127
  const route = routesByFavorite.get(`${alias.providerId}:${alias.modelId}`);
8961
- if (!isValidModelAlias(alias.name) || seenAliases.has(alias.name) || !route) {
8962
- unavailableAliases.push(alias);
9128
+ if (!route) {
9129
+ unavailableAliases.push(...sources);
8963
9130
  continue;
8964
9131
  }
8965
- seenAliases.add(alias.name);
8966
- aliases.push({ name: alias.name, routeId: route.aliasId, displayName: route.displayName });
9132
+ const sourceNames = [...new Set(sources.map((source) => source.name))];
9133
+ aliases.push({
9134
+ name: alias.name,
9135
+ routeId: route.aliasId,
9136
+ displayName: route.displayName,
9137
+ ...sourceNames.length === 1 && sourceNames[0] === alias.name ? {} : { sourceNames }
9138
+ });
8967
9139
  }
8968
9140
  return { routes, unavailable, unsupported, aliases, unavailableAliases };
8969
9141
  }
@@ -9044,7 +9216,7 @@ function createGatewayModelCatalog(models, opts, modelAliases) {
9044
9216
  const canonicalId = httpProxyModelId(gatewayProviderId(model), model.id);
9045
9217
  if (!byId.has(canonicalId)) byId.set(canonicalId, model);
9046
9218
  }
9047
- for (const alias of modelAliases ?? []) {
9219
+ for (const alias of normalizeModelAliases(modelAliases).aliases) {
9048
9220
  if (byId.has(alias.name)) continue;
9049
9221
  const target = models.find(
9050
9222
  (model) => gatewayProviderId(model) === alias.providerId && model.id === alias.modelId
@@ -9092,8 +9264,9 @@ function formatOpenAIModels(models) {
9092
9264
  }
9093
9265
 
9094
9266
  // src/route-unavailable.ts
9095
- function routeUnavailableMessage(modelId) {
9096
- return `Clodex model route '${modelId}' is unavailable. Run \`clodex models --list\` to see available routes, or \`clodex patch\` to refresh saved aliases.`;
9267
+ function routeUnavailableMessage(modelId, reason) {
9268
+ const detail = reason ? `: ${reason}` : "";
9269
+ return `Clodex model route '${modelId}' is unavailable${detail}. Run \`clodex models --list\` to inspect saved routes and aliases.`;
9097
9270
  }
9098
9271
 
9099
9272
  // src/upstream-forward.ts
@@ -10256,6 +10429,17 @@ function aliasModelId(realId, providerId) {
10256
10429
  function lookupRoute(byAlias, id) {
10257
10430
  return byAlias.get(normalizeRouteLookupId(id));
10258
10431
  }
10432
+ function configuredAliasLookupNames(alias) {
10433
+ const sourceNames = [
10434
+ alias.name,
10435
+ ...alias.savedName === void 0 ? [] : [alias.savedName],
10436
+ ...alias.sourceNames ?? []
10437
+ ];
10438
+ return [...new Set(sourceNames.flatMap((name) => {
10439
+ const trimmed = name.trim();
10440
+ return trimmed === name ? [name] : [name, trimmed];
10441
+ }))].map(normalizeRouteLookupId);
10442
+ }
10259
10443
  async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
10260
10444
  const proxyToken = randomUUID4();
10261
10445
  silenceSdkWarnings();
@@ -10264,9 +10448,13 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
10264
10448
  }
10265
10449
  const byAlias = new Map(routes.map((r) => [normalizeRouteLookupId(r.aliasId), r]));
10266
10450
  const configuredAliasNames = new Set(
10267
- (modelAliases ?? []).map((alias) => normalizeRouteLookupId(alias.name))
10451
+ (modelAliases ?? []).flatMap(configuredAliasLookupNames)
10452
+ );
10453
+ const unavailableAliasReasons = new Map(
10454
+ (modelAliases ?? []).filter((alias) => alias.unavailableReason !== void 0).flatMap((alias) => configuredAliasLookupNames(alias).map((name) => [name, alias.unavailableReason]))
10268
10455
  );
10269
10456
  for (const alias of modelAliases ?? []) {
10457
+ if (alias.routeId === void 0 || alias.unavailableReason !== void 0) continue;
10270
10458
  const route = lookupRoute(byAlias, alias.routeId);
10271
10459
  const aliasId = normalizeRouteLookupId(alias.name);
10272
10460
  if (route && !byAlias.has(aliasId)) byAlias.set(aliasId, route);
@@ -10341,7 +10529,14 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
10341
10529
  const resolvedRoute = typeof originalModel === "string" ? lookupRoute(byAlias, originalModel) : void 0;
10342
10530
  const configuredModelUnavailable = typeof originalModel === "string" && (normalizeRouteLookupId(originalModel).startsWith("clodex:") || configuredAliasNames.has(normalizeRouteLookupId(originalModel)));
10343
10531
  if (!resolvedRoute && configuredModelUnavailable) {
10344
- anthropicError(res, 400, routeUnavailableMessage(originalModel));
10532
+ anthropicError(
10533
+ res,
10534
+ 400,
10535
+ routeUnavailableMessage(
10536
+ originalModel,
10537
+ unavailableAliasReasons.get(normalizeRouteLookupId(originalModel))
10538
+ )
10539
+ );
10345
10540
  return;
10346
10541
  }
10347
10542
  const route = resolvedRoute ?? defaultRoute;
@@ -12382,6 +12577,10 @@ async function startHttpProxy(options) {
12382
12577
  for (const alias of options.modelAliases ?? []) {
12383
12578
  const aliasId = normalizeRouteLookupId(alias.name);
12384
12579
  reservedModelIds.add(aliasId);
12580
+ for (const sourceName of alias.sourceNames ?? []) {
12581
+ reservedModelIds.add(normalizeRouteLookupId(sourceName));
12582
+ reservedModelIds.add(normalizeRouteLookupId(sourceName.trim()));
12583
+ }
12385
12584
  const route = routesById.get(normalizeRouteLookupId(alias.routeId));
12386
12585
  if (!route) continue;
12387
12586
  routesById.set(aliasId, route);
@@ -12632,13 +12831,17 @@ async function startHttpProxy(options) {
12632
12831
  async function loadHttpProxyRoutes() {
12633
12832
  const prefs = loadPreferences();
12634
12833
  const favorites = prefs.favoriteModels ?? [];
12834
+ const normalizedAliases = normalizeModelAliases(prefs.modelAliases);
12635
12835
  if (favorites.length === 0) {
12636
12836
  return {
12637
12837
  routes: [],
12638
12838
  unavailable: [],
12639
12839
  unsupported: [],
12640
12840
  aliases: [],
12641
- unavailableAliases: prefs.modelAliases ?? [],
12841
+ unavailableAliases: [
12842
+ ...normalizedAliases.rejected,
12843
+ ...normalizedAliases.accepted.flatMap(({ sources }) => sources)
12844
+ ],
12642
12845
  favoriteCount: 0
12643
12846
  };
12644
12847
  }
@@ -12648,7 +12851,7 @@ async function loadHttpProxyRoutes() {
12648
12851
  apiKey: await resolveLocalProviderApiKey(provider) ?? ""
12649
12852
  })));
12650
12853
  return {
12651
- ...buildHttpProxyRoutes(catalog, favorites, prefs.modelAliases ?? []),
12854
+ ...buildHttpProxyRoutes(catalog, favorites, prefs.modelAliases),
12652
12855
  favoriteCount: favorites.length
12653
12856
  };
12654
12857
  }
@@ -12682,8 +12885,16 @@ function reportSkippedHttpProxyFavorites(loaded) {
12682
12885
  );
12683
12886
  }
12684
12887
  if (loaded.unavailableAliases.length > 0) {
12888
+ const normalizedAliases = normalizeModelAliases(loaded.unavailableAliases);
12889
+ const reasonByAlias = new Map(
12890
+ normalizedAliases.rejections.map((rejection) => [
12891
+ rejection.alias,
12892
+ describeModelAliasRejection(rejection.reason)
12893
+ ])
12894
+ );
12685
12895
  p8.log.warn(
12686
- `${loaded.unavailableAliases.length} model alias${loaded.unavailableAliases.length === 1 ? "" : "es"} skipped \u2014 its target must be an available HTTP-proxy favorite.`
12896
+ `${loaded.unavailableAliases.length} model alias${loaded.unavailableAliases.length === 1 ? "" : "es"} skipped. Saved entries were preserved.
12897
+ ` + loaded.unavailableAliases.map((alias) => ` ${JSON.stringify(alias.name)} \u2014 ${reasonByAlias.get(alias) ?? "target unavailable"}`).join("\n")
12687
12898
  );
12688
12899
  }
12689
12900
  }
@@ -12693,7 +12904,14 @@ function buildConfiguredHttpProxyOptions(loaded, port, debug = false, inferenceL
12693
12904
  port,
12694
12905
  routes: loaded.routes,
12695
12906
  modelAliases: loaded.aliases,
12696
- reservedModelIds: loaded.unavailableAliases.map((alias) => alias.name),
12907
+ reservedModelIds: [...new Set([
12908
+ ...loaded.aliases.flatMap((alias) => alias.sourceNames ?? []),
12909
+ ...loaded.unavailableAliases.map((alias) => alias.name)
12910
+ ].flatMap((name) => {
12911
+ const trimmedName = name.trim();
12912
+ const canonicalName = canonicalModelAliasName(name);
12913
+ return [name, trimmedName, canonicalName].filter(Boolean);
12914
+ }))],
12697
12915
  debug,
12698
12916
  debugLogPath,
12699
12917
  inferenceLogPath,
@@ -13104,7 +13322,28 @@ async function runServerCommand(options = {}) {
13104
13322
  return 1;
13105
13323
  }
13106
13324
  const gateway = runConfig.maskGatewayIds ? { maskGatewayIds: true } : void 0;
13107
- const modelAliases = loadPreferences().modelAliases ?? [];
13325
+ const normalizedAliases = normalizeModelAliases(loadPreferences().modelAliases);
13326
+ const baseCatalog = createGatewayModelCatalog(models, gateway);
13327
+ const unavailableAliases = normalizedAliases.accepted.filter(({ alias }) => !models.some((model) => gatewayProviderId(model) === alias.providerId && model.id === alias.modelId));
13328
+ const collidingAliases = normalizedAliases.accepted.filter(({ alias }) => baseCatalog.get(alias.name) !== void 0);
13329
+ const collidingAliasNames = new Set(collidingAliases.map(({ alias }) => alias.name));
13330
+ const targetUnavailableAliases = unavailableAliases.filter(({ alias }) => !collidingAliasNames.has(alias.name));
13331
+ const inactiveAliasNames = /* @__PURE__ */ new Set([
13332
+ ...targetUnavailableAliases.map(({ alias }) => alias.name),
13333
+ ...collidingAliasNames
13334
+ ]);
13335
+ const modelAliases = normalizedAliases.aliases.filter((alias) => !inactiveAliasNames.has(alias.name));
13336
+ const aliasWarnings = [
13337
+ ...normalizedAliases.rejections.map((rejection) => `${JSON.stringify(rejection.alias.name)} \u2014 ${describeModelAliasRejection(rejection.reason)}`),
13338
+ ...targetUnavailableAliases.flatMap(({ sources }) => sources.map((source) => `${JSON.stringify(source.name)} \u2014 target unavailable`)),
13339
+ ...collidingAliases.flatMap(({ sources }) => sources.map((source) => `${JSON.stringify(source.name)} \u2014 conflicts with a catalog model id`))
13340
+ ];
13341
+ if (aliasWarnings.length > 0) {
13342
+ p9.log.warn(
13343
+ `${aliasWarnings.length} saved model alias${aliasWarnings.length === 1 ? "" : "es"} inactive. Saved entries were preserved.
13344
+ ${aliasWarnings.join("\n ")}`
13345
+ );
13346
+ }
13108
13347
  const inferenceLogPath = getInferenceRequestLogPath();
13109
13348
  const webSocketDiagnosticsLogPath = options.wsDiagnostics ? getSessionLogPath("server-websocket-diagnostics", "jsonl") : void 0;
13110
13349
  const server = await startServer({
@@ -13416,25 +13655,192 @@ function planLaunchWizard(opts) {
13416
13655
  }
13417
13656
 
13418
13657
  // src/patcher.ts
13419
- import { createHash as createHash7 } from "crypto";
13658
+ import { createHash as createHash8 } from "crypto";
13420
13659
  import {
13421
13660
  copyFileSync,
13422
- existsSync as existsSync6,
13661
+ existsSync as existsSync7,
13662
+ mkdtempSync,
13423
13663
  mkdirSync as mkdirSync7,
13424
- readFileSync as readFileSync8,
13425
- statSync as statSync3,
13664
+ readFileSync as readFileSync9,
13665
+ renameSync as renameSync3,
13666
+ rmSync,
13667
+ statSync as statSync4,
13426
13668
  unlinkSync as unlinkSync4,
13427
13669
  writeFileSync as writeFileSync7,
13428
13670
  openSync as openSync4,
13429
13671
  closeSync as closeSync4,
13430
13672
  realpathSync
13431
13673
  } from "fs";
13432
- import { homedir as homedir2 } from "os";
13433
- import { basename, join as join7 } from "path";
13674
+ import { homedir as homedir3 } from "os";
13675
+ import { basename, dirname as dirname5, join as join8 } from "path";
13434
13676
  import pc12 from "picocolors";
13435
13677
  import * as p11 from "@clack/prompts";
13436
13678
 
13679
+ // src/patch-backup.ts
13680
+ import { createHash as createHash7 } from "crypto";
13681
+ import { existsSync as existsSync6, readFileSync as readFileSync8, readdirSync, statSync as statSync3 } from "fs";
13682
+ import { homedir as homedir2 } from "os";
13683
+ import { join as join7 } from "path";
13684
+ var BACKUP_SHA_PREFIX_LENGTH = 16;
13685
+ function backupDir() {
13686
+ return process.env["TWEAKCC_CONFIG_DIR"]?.trim() || join7(homedir2(), ".tweakcc");
13687
+ }
13688
+ function sha256File(path) {
13689
+ return createHash7("sha256").update(readFileSync8(path)).digest("hex");
13690
+ }
13691
+ function backupVersionTag(version) {
13692
+ const tag = version.trim().replace(/[^\w.-]+/g, "_");
13693
+ if (!tag) throw new Error("clodex patch: refusing to name a pristine backup for an empty claude version");
13694
+ return tag;
13695
+ }
13696
+ function contentAddressedBackupPath(version, sha256, dir = backupDir()) {
13697
+ return join7(dir, `claude-${backupVersionTag(version)}-${sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH)}.orig`);
13698
+ }
13699
+ function tweakccMirrorBackupPath(dir = backupDir()) {
13700
+ return join7(dir, "native-binary.backup");
13701
+ }
13702
+ function scanPristineBackups(version, dir = backupDir()) {
13703
+ const tag = backupVersionTag(version);
13704
+ const pattern = new RegExp(`^claude-${tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:-([0-9a-f]{${BACKUP_SHA_PREFIX_LENGTH}}))?\\.orig$`);
13705
+ let entries;
13706
+ try {
13707
+ entries = readdirSync(dir);
13708
+ } catch {
13709
+ return { valid: [], corrupt: [] };
13710
+ }
13711
+ const valid = [];
13712
+ const corrupt = [];
13713
+ for (const entry of entries.sort()) {
13714
+ const match = pattern.exec(entry);
13715
+ if (!match) continue;
13716
+ const path = join7(dir, entry);
13717
+ let sha256;
13718
+ try {
13719
+ if (!statSync3(path).isFile()) continue;
13720
+ sha256 = sha256File(path);
13721
+ } catch {
13722
+ corrupt.push(path);
13723
+ continue;
13724
+ }
13725
+ const embedded = match[1];
13726
+ if (embedded) {
13727
+ if (sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH) !== embedded) {
13728
+ corrupt.push(path);
13729
+ continue;
13730
+ }
13731
+ valid.push({ path, kind: "content-addressed", sha256 });
13732
+ } else {
13733
+ valid.push({ path, kind: "legacy", sha256 });
13734
+ }
13735
+ }
13736
+ return { valid, corrupt };
13737
+ }
13738
+ var CLODEX_PATCH_COMMENT_PREFIX = "/*ccpatch:";
13739
+ var LEGACY_CLODEX_PATCH_MARKERS = [
13740
+ "Additional custom models: ",
13741
+ // PATCH 4 — Agent tool model description
13742
+ "function(_i){return _i.value===_o.value}",
13743
+ // PATCH 5 — picker dedupe guard
13744
+ '"clodex:'
13745
+ // PATCH 1/3 — canonical ids as model identities
13746
+ ];
13747
+ function isPatchedClaudeSource(source) {
13748
+ return source.includes(CLODEX_PATCH_COMMENT_PREFIX);
13749
+ }
13750
+ function looksLikeLegacyClodexPatch(source) {
13751
+ return !isPatchedClaudeSource(source) && LEGACY_CLODEX_PATCH_MARKERS.some((marker) => source.includes(marker));
13752
+ }
13753
+ function noBackupMessage(facts) {
13754
+ const corrupt = facts.corruptBackups?.length ? ` (${facts.corruptBackups.length} backup file(s) for this version failed integrity checks and were ignored)` : "";
13755
+ return `claude ${facts.version} is already patched and no trustworthy pristine backup for that version exists in ${backupDir()}${corrupt}. Reinstall Claude Code to get a pristine binary, then run \`clodex patch\`.`;
13756
+ }
13757
+ function selectRestoreSource(facts) {
13758
+ const notes = [];
13759
+ const manifest = facts.manifest && facts.manifest.binaryPath === facts.binaryPath ? facts.manifest : null;
13760
+ let chosen = manifest?.pristineSha256 ? facts.backups.find((backup) => backup.sha256 === manifest.pristineSha256) : void 0;
13761
+ if (!chosen && manifest?.backupPath) {
13762
+ chosen = facts.backups.find((backup) => backup.path === manifest.backupPath);
13763
+ if (!chosen) {
13764
+ notes.push(`Recorded pristine backup ${manifest.backupPath} is missing, corrupt, or belongs to another claude version \u2014 ignoring it.`);
13765
+ }
13766
+ }
13767
+ if (!chosen) {
13768
+ const distinct = [...new Set(facts.backups.map((backup) => backup.sha256))];
13769
+ if (distinct.length > 1) {
13770
+ return {
13771
+ action: "error",
13772
+ message: `Found conflicting pristine backups for claude ${facts.version}: ${facts.backups.map((backup) => backup.path).join(", ")}. They do not hold the same bytes, so clodex cannot tell which one is pristine. Remove the wrong one (or reinstall Claude Code), then run \`clodex patch\`.`
13773
+ };
13774
+ }
13775
+ chosen = facts.backups.find((backup) => backup.kind === "content-addressed") ?? facts.backups[0];
13776
+ }
13777
+ if (!chosen) return { action: "error", message: noBackupMessage(facts) };
13778
+ return {
13779
+ action: "restore",
13780
+ backupPath: chosen.path,
13781
+ pristineSha256: chosen.sha256,
13782
+ // A legacy name carries no hash, so its bytes could be anything — including
13783
+ // another version's binary, stored under a mislabeled name by an older
13784
+ // clodex. Executing it is the only evidence available; require it.
13785
+ probeVersion: chosen.kind === "legacy",
13786
+ notes
13787
+ };
13788
+ }
13789
+ function planPristineSource(facts) {
13790
+ const identical = facts.backups.find((backup) => backup.sha256 === facts.liveSha256 && backup.kind === "content-addressed") ?? facts.backups.find((backup) => backup.sha256 === facts.liveSha256);
13791
+ if (identical) {
13792
+ return { action: "reuse", backupPath: identical.path, pristineSha256: identical.sha256, notes: [] };
13793
+ }
13794
+ const manifest = facts.manifest;
13795
+ if (manifest && manifest.binaryPath === facts.binaryPath && manifest.patchedSha256 === facts.liveSha256) {
13796
+ return selectRestoreSource(facts);
13797
+ }
13798
+ return { action: "inspect" };
13799
+ }
13800
+ function planInspectedPristineSource(facts, inspection) {
13801
+ if (inspection.patched) return selectRestoreSource(facts);
13802
+ const notes = [];
13803
+ const conflicting = facts.backups.filter((backup) => backup.sha256 !== facts.liveSha256);
13804
+ if (conflicting.length) {
13805
+ notes.push(
13806
+ `Existing backup(s) for claude ${facts.version} hold different bytes (${conflicting.map((b) => b.path).join(", ")}); the binary being patched carries no clodex patch marker, so it is being stored under its own content address. Both files are kept.`
13807
+ );
13808
+ }
13809
+ return {
13810
+ action: "snapshot",
13811
+ backupPath: contentAddressedBackupPath(facts.version, facts.liveSha256),
13812
+ pristineSha256: facts.liveSha256,
13813
+ notes
13814
+ };
13815
+ }
13816
+ function planRestoreOnly(facts) {
13817
+ return selectRestoreSource(facts);
13818
+ }
13819
+ function collectPristineFacts(args) {
13820
+ const dir = args.dir ?? backupDir();
13821
+ const scan = scanPristineBackups(args.version, dir);
13822
+ return {
13823
+ version: args.version,
13824
+ binaryPath: args.binaryPath,
13825
+ liveSha256: existsSync6(args.binaryPath) ? sha256File(args.binaryPath) : "",
13826
+ manifest: args.manifest,
13827
+ backups: scan.valid,
13828
+ corruptBackups: scan.corrupt
13829
+ };
13830
+ }
13831
+
13437
13832
  // src/patch-transforms.ts
13833
+ var PATCH_TRANSFORMS_VERSION = 2;
13834
+ var NATIVE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
13835
+ var BASE_EFFORT_LEVELS = ["low", "medium", "high"];
13836
+ function projectNativeEffort(effort) {
13837
+ if (!effort || !Array.isArray(effort.levels) || typeof effort.defaultLevel !== "string") return void 0;
13838
+ const declared = new Set(effort.levels);
13839
+ const levels = NATIVE_EFFORT_LEVELS.filter((level) => declared.has(level));
13840
+ if (!BASE_EFFORT_LEVELS.every((level) => declared.has(level))) return void 0;
13841
+ if (!levels.some((level) => level === effort.defaultLevel)) return void 0;
13842
+ return { levels, defaultLevel: "high" };
13843
+ }
13438
13844
  var PatchApplyError = class extends Error {
13439
13845
  results;
13440
13846
  constructor(message, results) {
@@ -13449,21 +13855,37 @@ function formatPatchSiteLine(result) {
13449
13855
  function applyClodexPatches(source, config) {
13450
13856
  let js = source;
13451
13857
  const MODEL_CONFIG = config;
13452
- const ALIAS_TO_ID = {};
13858
+ const ALIAS_TO_ID = /* @__PURE__ */ Object.create(null);
13453
13859
  const IDENTITIES = [];
13454
- const DISPLAY_BY_IDENTITY = {};
13455
- const CONTEXT_BY_KEY = {};
13860
+ const DISPLAY_BY_IDENTITY = /* @__PURE__ */ Object.create(null);
13861
+ const CONTEXT_BY_KEY = /* @__PURE__ */ Object.create(null);
13862
+ const CONFIGURED_CAPABILITY_KEYS = /* @__PURE__ */ new Set();
13863
+ const EFFORT_BY_KEY = /* @__PURE__ */ Object.create(null);
13456
13864
  const report = [];
13457
13865
  const fail = (message) => {
13458
13866
  throw new PatchApplyError(message, report);
13459
13867
  };
13868
+ const capabilityKeys = (value) => {
13869
+ const normalized = value.trim().toLowerCase();
13870
+ const bare = normalized.replace(/\[1m\]$/i, "");
13871
+ return [.../* @__PURE__ */ new Set([bare, `${bare}[1m]`])];
13872
+ };
13873
+ const registerCapabilityKeys = (value) => {
13874
+ for (const key of capabilityKeys(value)) {
13875
+ CONFIGURED_CAPABILITY_KEYS.add(key);
13876
+ }
13877
+ };
13460
13878
  for (const [id, value] of Object.entries(MODEL_CONFIG)) {
13461
13879
  const spec = value && typeof value === "object" ? value : { alias: value };
13462
13880
  if (spec.alias !== void 0) {
13463
- const a = String(spec.alias).trim().toLowerCase();
13881
+ const rawAlias = String(spec.alias).trim();
13882
+ const a = rawAlias.toLowerCase();
13464
13883
  if (!/^[a-z0-9][a-z0-9._-]*(\[1m\])?$/.test(a)) {
13465
13884
  fail('clodex patch: alias "' + spec.alias + '" is not a safe lowercase alias');
13466
13885
  }
13886
+ if (isReservedModelAlias(a)) {
13887
+ fail('clodex patch: reserved alias "' + a + '" cannot be reassigned');
13888
+ }
13467
13889
  ALIAS_TO_ID[a] = String(id);
13468
13890
  IDENTITIES.push(a);
13469
13891
  if (spec.display) DISPLAY_BY_IDENTITY[a] = String(spec.display);
@@ -13471,6 +13893,10 @@ function applyClodexPatches(source, config) {
13471
13893
  IDENTITIES.push(String(id));
13472
13894
  if (spec.display) DISPLAY_BY_IDENTITY[String(id)] = String(spec.display);
13473
13895
  }
13896
+ if (spec.alias !== void 0) {
13897
+ registerCapabilityKeys(String(spec.alias));
13898
+ }
13899
+ registerCapabilityKeys(String(id));
13474
13900
  if (spec.context !== void 0) {
13475
13901
  const n = Number(spec.context);
13476
13902
  if (!Number.isInteger(n) || n <= 0) {
@@ -13484,6 +13910,22 @@ function applyClodexPatches(source, config) {
13484
13910
  if (spec.alias !== void 0) CONTEXT_BY_KEY[String(spec.alias).trim().toLowerCase()] = n;
13485
13911
  CONTEXT_BY_KEY[String(id).trim().toLowerCase()] = n;
13486
13912
  }
13913
+ if (spec.effort) {
13914
+ const effort = projectNativeEffort(spec.effort);
13915
+ if (!effort) {
13916
+ fail(
13917
+ `clodex patch: effort for "${id}" must include low, medium, and high with a native default`
13918
+ );
13919
+ }
13920
+ if (spec.alias !== void 0) {
13921
+ for (const key of capabilityKeys(String(spec.alias))) {
13922
+ EFFORT_BY_KEY[key] = effort;
13923
+ }
13924
+ }
13925
+ for (const key of capabilityKeys(String(id))) {
13926
+ EFFORT_BY_KEY[key] = effort;
13927
+ }
13928
+ }
13487
13929
  }
13488
13930
  const ALIASES = Object.keys(ALIAS_TO_ID);
13489
13931
  const MODELS = Object.keys(MODEL_CONFIG);
@@ -13611,19 +14053,91 @@ function applyClodexPatches(source, config) {
13611
14053
  );
13612
14054
  }
13613
14055
  }
14056
+ function patchEffortCapability(capability, marker, name, anchor) {
14057
+ const verdicts = Object.fromEntries(
14058
+ [...CONFIGURED_CAPABILITY_KEYS].map((key) => {
14059
+ const effort = EFFORT_BY_KEY[key];
14060
+ return [
14061
+ key,
14062
+ effort !== void 0 && (capability === "effort" || effort.levels.includes(capability === "xhigh_effort" ? "xhigh" : "max"))
14063
+ ];
14064
+ })
14065
+ );
14066
+ const hasMarker = js.includes(marker);
14067
+ if (Object.keys(verdicts).length === 0 && !hasMarker) return;
14068
+ const snippet = (arg) => marker + "var _ccv=Object.assign(Object.create(null)," + JSON.stringify(verdicts) + ")[String(" + arg + '||"").trim().toLowerCase()];if(_ccv!==void 0)return _ccv;';
14069
+ if (hasMarker) {
14070
+ const markerPattern = reEsc(marker);
14071
+ applyOnce(
14072
+ name + " (refresh)",
14073
+ new RegExp(
14074
+ markerPattern + 'var _ccv=Object\\.assign\\(Object\\.create\\(null\\),\\{[^{}]*\\}\\)\\[String\\(([\\w$]+)\\|\\|""\\)\\.trim\\(\\)\\.toLowerCase\\(\\)\\];if\\(_ccv!==void 0\\)return _ccv;'
14075
+ ),
14076
+ (_m, arg) => snippet(arg),
14077
+ { required: false, noopIsSkip: true }
14078
+ );
14079
+ return;
14080
+ }
14081
+ applyOnce(
14082
+ name,
14083
+ anchor,
14084
+ (_m, head, arg, body) => head + snippet(arg) + body,
14085
+ { required: false }
14086
+ );
14087
+ }
14088
+ patchEffortCapability(
14089
+ "effort",
14090
+ "/*ccpatch:effort*/",
14091
+ "PATCH 8a: effort capability",
14092
+ /(function [\w$]+\(([\w$]+)\)\{if\([\w$]+\(\2\)\)return!1;)(let [\w$]+=[\w$]+\(\2,"effort"\);)/
14093
+ );
14094
+ patchEffortCapability(
14095
+ "xhigh_effort",
14096
+ "/*ccpatch:xhigh-effort*/",
14097
+ "PATCH 8b: xhigh effort capability",
14098
+ /(function [\w$]+\(([\w$]+)\)\{if\([\w$]+\(\2\)\)return!1;)(let [\w$]+=[\w$]+\(\2,"xhigh_effort"\);)/
14099
+ );
14100
+ patchEffortCapability(
14101
+ "max_effort",
14102
+ "/*ccpatch:max-effort*/",
14103
+ "PATCH 8c: max effort capability",
14104
+ /(function [\w$]+\(([\w$]+)\)\{if\([\w$]+\(\2\)\)return!1;)(let [\w$]+=[\w$]+\(\2,"max_effort"\);)/
14105
+ );
14106
+ const DEFAULT_EFFORT_MARKER = "/*ccpatch:default-effort*/";
14107
+ const defaults = Object.fromEntries(
14108
+ Object.entries(EFFORT_BY_KEY).map(([key, effort]) => [key, effort.defaultLevel])
14109
+ );
14110
+ if (Object.keys(defaults).length || js.includes(DEFAULT_EFFORT_MARKER)) {
14111
+ const snippet = (arg) => DEFAULT_EFFORT_MARKER + "var _cce=Object.assign(Object.create(null)," + JSON.stringify(defaults) + ")[String(" + arg + '||"").trim().toLowerCase()];if(_cce!==void 0)return _cce;';
14112
+ if (js.includes(DEFAULT_EFFORT_MARKER)) {
14113
+ applyOnce(
14114
+ "PATCH 9: default effort (refresh)",
14115
+ /\/\*ccpatch:default-effort\*\/var _cce=Object\.assign\(Object\.create\(null\),\{[^{}]*\}\)\[String\(([\w$]+)\|\|""\)\.trim\(\)\.toLowerCase\(\)\];if\(_cce!==void 0\)return _cce;/,
14116
+ (_m, arg) => snippet(arg),
14117
+ { required: false, noopIsSkip: true }
14118
+ );
14119
+ } else {
14120
+ applyOnce(
14121
+ "PATCH 9: default effort",
14122
+ /(function [\w$]+\(([\w$]+)\)\{)(return [\w$]+\([\w$]+\(\2\)\)\?\.default_effort\?\?"high"\})/,
14123
+ (_m, head, arg, body) => head + snippet(arg) + body,
14124
+ { required: false }
14125
+ );
14126
+ }
14127
+ }
13614
14128
  return { content: js, results: report };
13615
14129
  }
13616
14130
 
13617
14131
  // src/patcher.ts
13618
14132
  function getPatchManifestPath() {
13619
- return join7(getAppHome(), "patch-state.json");
14133
+ return join8(getAppHome(), "patch-state.json");
13620
14134
  }
13621
14135
  function getPatchLockPath() {
13622
- return join7(getAppHome(), "patch.lock");
14136
+ return join8(getAppHome(), "patch.lock");
13623
14137
  }
13624
14138
  function readPatchManifest(path = getPatchManifestPath()) {
13625
14139
  try {
13626
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
14140
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
13627
14141
  if (parsed && typeof parsed.binaryPath === "string" && typeof parsed.configHash === "string") {
13628
14142
  return parsed;
13629
14143
  }
@@ -13639,7 +14153,20 @@ function writePatchManifest(manifest, path = getPatchManifestPath()) {
13639
14153
  function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
13640
14154
  const config = {};
13641
14155
  const unknownWindows = [];
13642
- const aliasByFavorite = new Map(aliases.map((a) => [`${a.providerId}:${a.modelId}`, a.name]));
14156
+ const normalizedAliases = normalizeModelAliases(aliases);
14157
+ const favoriteTargets = new Set(
14158
+ favorites.map((favorite) => `${favorite.providerId}:${favorite.modelId}`)
14159
+ );
14160
+ const targetRejections = normalizedAliases.accepted.filter(({ alias }) => !favoriteTargets.has(`${alias.providerId}:${alias.modelId}`)).flatMap(({ sources }) => sources.map((source) => ({
14161
+ alias: source,
14162
+ reason: "target-not-favorite"
14163
+ })));
14164
+ const aliasByFavorite = new Map(
14165
+ normalizedAliases.aliases.filter((alias) => favoriteTargets.has(`${alias.providerId}:${alias.modelId}`)).map((alias) => [
14166
+ `${alias.providerId}:${alias.modelId}`,
14167
+ alias.name
14168
+ ])
14169
+ );
13643
14170
  for (const favorite of favorites) {
13644
14171
  const id = stripOneMContextSuffix(httpProxyModelId(favorite.providerId, favorite.modelId));
13645
14172
  if (config[id]) continue;
@@ -13652,29 +14179,68 @@ function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
13652
14179
  else if (context !== 2e5) entry.context = context;
13653
14180
  const display = meta?.displayName?.trim();
13654
14181
  if (display) entry.display = display;
14182
+ const effort = projectNativeEffort(meta?.effort);
14183
+ if (effort) entry.effort = effort;
13655
14184
  config[id] = entry;
13656
14185
  }
13657
- return { config, unknownWindows };
14186
+ return {
14187
+ config,
14188
+ unknownWindows,
14189
+ rejectedAliases: [
14190
+ ...normalizedAliases.rejected,
14191
+ ...targetRejections.map((rejection) => rejection.alias)
14192
+ ],
14193
+ rejectedAliasRejections: [
14194
+ ...normalizedAliases.rejections,
14195
+ ...targetRejections
14196
+ ]
14197
+ };
13658
14198
  }
13659
- function computePatchConfigHash(config) {
14199
+ function reportRejectedModelAliases(rejections) {
14200
+ for (const rejection of rejections) {
14201
+ p11.log.warn(
14202
+ `Saved model alias ${JSON.stringify(rejection.alias.name)} was not patched \u2014 ${describeModelAliasRejection(rejection.reason)}. The saved entry was preserved.`
14203
+ );
14204
+ }
14205
+ }
14206
+ function computePatchConfigHash(config, transformsVersion = PATCH_TRANSFORMS_VERSION) {
13660
14207
  const canonical = Object.keys(config).sort().map((key) => {
13661
14208
  const entry = config[key];
13662
- return [key, entry.alias ?? null, entry.context ?? null, entry.display ?? null];
14209
+ return [
14210
+ key,
14211
+ entry.alias ?? null,
14212
+ entry.context ?? null,
14213
+ entry.display ?? null,
14214
+ entry.effort?.levels ?? null,
14215
+ entry.effort?.defaultLevel ?? null
14216
+ ];
13663
14217
  });
13664
- return createHash7("sha256").update(JSON.stringify(canonical)).digest("hex");
14218
+ return createHash8("sha256").update(JSON.stringify([transformsVersion, canonical])).digest("hex");
13665
14219
  }
13666
14220
  function buildDesiredPatchConfig() {
13667
14221
  const prefs = loadPreferences();
13668
14222
  const favorites = prefs.favoriteModels ?? [];
13669
- const aliases = prefs.modelAliases ?? [];
14223
+ const aliases = prefs.modelAliases;
13670
14224
  const registry = loadRegistry();
13671
14225
  const meta = /* @__PURE__ */ new Map();
13672
14226
  for (const provider of registry.providers) {
13673
14227
  for (const model of provider.modelsCache?.models ?? []) {
14228
+ const npm = model.npm ?? provider.api.npm ?? "";
14229
+ const upstreamModelId2 = model.upstreamModelId ?? model.id;
14230
+ const modelsDev = findModelsDevModel(provider.id, model.id);
14231
+ const effort = getPatchReasoningCapabilities(npm, upstreamModelId2, {
14232
+ providerId: provider.id,
14233
+ apiBaseUrl: model.apiUrl ?? provider.api.url,
14234
+ supportedParameters: model.supportedParameters,
14235
+ reasoning: model.reasoning ?? modelsDev?.reasoning,
14236
+ interleavedReasoningField: model.interleavedReasoningField ?? modelsDev?.interleaved?.field,
14237
+ upstreamModelId: upstreamModelId2
14238
+ });
13674
14239
  meta.set(`${provider.id}:${model.id}`, {
13675
14240
  contextWindow: model.contextWindow && model.contextWindow > 0 ? model.contextWindow : void 0,
13676
14241
  // Same label `clodex server` prints at startup and `models --list` shows.
13677
- displayName: httpProxyDisplayName(model, provider.name)
14242
+ displayName: httpProxyDisplayName(model, provider.name),
14243
+ effort: effort.mode === "controllable" ? { levels: effort.levels, defaultLevel: effort.defaultLevel } : void 0
13678
14244
  });
13679
14245
  }
13680
14246
  }
@@ -13704,7 +14270,7 @@ function pidIsAlive(pid) {
13704
14270
  function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13705
14271
  const now = opts.now ?? Date.now();
13706
14272
  const isAlive = opts.isAlive ?? pidIsAlive;
13707
- mkdirSync7(join7(lockPath, ".."), { recursive: true, mode: 448 });
14273
+ mkdirSync7(join8(lockPath, ".."), { recursive: true, mode: 448 });
13708
14274
  for (let attempt = 0; attempt < 2; attempt++) {
13709
14275
  try {
13710
14276
  const fd = openSync4(lockPath, "wx");
@@ -13720,7 +14286,7 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13720
14286
  } catch {
13721
14287
  let stale = false;
13722
14288
  try {
13723
- const existing = JSON.parse(readFileSync8(lockPath, "utf8"));
14289
+ const existing = JSON.parse(readFileSync9(lockPath, "utf8"));
13724
14290
  stale = !existing.pid || !isAlive(existing.pid) || typeof existing.startedAt === "number" && now - existing.startedAt > PATCH_LOCK_STALE_MS;
13725
14291
  } catch {
13726
14292
  stale = true;
@@ -13734,33 +14300,28 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13734
14300
  }
13735
14301
  return null;
13736
14302
  }
13737
- function sha256File(path) {
13738
- return createHash7("sha256").update(readFileSync8(path)).digest("hex");
13739
- }
13740
14303
  function resolveClaudeBinaryForPatch() {
13741
14304
  const envOverride = process.env["TWEAKCC_CC_INSTALLATION_PATH"];
13742
- const nativeSymlink = join7(homedir2(), ".local", "bin", "claude");
13743
- const source = envOverride?.trim() || (existsSync6(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
13744
- if (!source) return null;
14305
+ const nativeSymlink = join8(homedir3(), ".local", "bin", "claude");
14306
+ const source = envOverride?.trim() || (existsSync7(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
14307
+ if (!source) return { ok: false, reason: "binary-not-found" };
13745
14308
  let resolved;
13746
14309
  try {
13747
14310
  resolved = realpathSync(source);
13748
14311
  } catch {
13749
- return null;
14312
+ return { ok: false, reason: "binary-not-found" };
13750
14313
  }
13751
14314
  try {
13752
- if (!statSync3(resolved).isFile()) return null;
14315
+ if (!statSync4(resolved).isFile()) return { ok: false, reason: "binary-not-found" };
13753
14316
  } catch {
13754
- return null;
14317
+ return { ok: false, reason: "binary-not-found" };
13755
14318
  }
13756
- return { binaryPath: resolved, version: getInstalledClaudeVersion() };
14319
+ const version = getClaudeVersionForBinary(resolved);
14320
+ if (!version) return { ok: false, reason: "version-unknown", binaryPath: resolved };
14321
+ return { ok: true, binaryPath: resolved, version };
13757
14322
  }
13758
- function backupDir() {
13759
- return process.env["TWEAKCC_CONFIG_DIR"]?.trim() || join7(homedir2(), ".tweakcc");
13760
- }
13761
- function pristineBackupPath(version, binaryPath) {
13762
- const tag = version.replace(/[^\w.-]+/g, "_") || basename(binaryPath);
13763
- return join7(backupDir(), `claude-${tag}.orig`);
14323
+ function describePatchTargetFailure(target) {
14324
+ return target.reason === "binary-not-found" ? "claude binary not found. Install Claude Code or set TWEAKCC_CC_INSTALLATION_PATH." : `Could not determine the version of ${target.binaryPath} (\`claude --version\` failed). clodex will not patch a binary whose version it cannot read, because the version selects the pristine backup it patches from. If a previous patch left the install broken, \`clodex patch --restore\` still works \u2014 it reads the version from the patch manifest.`;
13764
14325
  }
13765
14326
  function summarizePatchResults(results) {
13766
14327
  const lines = results.map(formatPatchSiteLine);
@@ -13773,26 +14334,112 @@ function summarizePatchResults(results) {
13773
14334
  }
13774
14335
  return lines;
13775
14336
  }
14337
+ function verifyPristineSource(plan, version) {
14338
+ if (!plan.probeVersion) return { ok: true };
14339
+ const backupVersion = getClaudeVersionForBinary(plan.backupPath);
14340
+ if (backupVersion === version) return { ok: true };
14341
+ return {
14342
+ ok: false,
14343
+ message: `Refusing to use ${plan.backupPath} as the pristine source for claude ${version}: it reports ${backupVersion ? `version ${backupVersion}` : "no version at all"}. That backup predates content-addressed backup names and does not hold this version's bytes. Remove it (or reinstall Claude Code), then run \`clodex patch\`.`
14344
+ };
14345
+ }
14346
+ function publishBackupFile(from, to) {
14347
+ const temp = `${to}.tmp-${process.pid}-${Date.now().toString(36)}`;
14348
+ try {
14349
+ copyFileSync(from, temp);
14350
+ renameSync3(temp, to);
14351
+ } catch (err) {
14352
+ try {
14353
+ rmSync(temp, { force: true });
14354
+ } catch {
14355
+ }
14356
+ throw err;
14357
+ }
14358
+ }
14359
+ function describePoisonedPristineSource(backupPath, version) {
14360
+ return `Refusing to patch from ${backupPath}: those bytes already carry a clodex patch, so they are not pristine and patching them would stack a patch on a patch. That backup was recorded as pristine for claude ${version} by an older clodex, which snapshotted whatever binary was live when no backup existed. Delete ${backupPath}, reinstall Claude Code, then run \`clodex patch\`.`;
14361
+ }
14362
+ function requiredEffortPatchFailures(results) {
14363
+ return results.filter(
14364
+ (result) => result.status === "FAIL" && (result.name.startsWith("PATCH 8a:") || result.name.startsWith("PATCH 8b:") || result.name.startsWith("PATCH 8c:") || result.name.startsWith("PATCH 9:"))
14365
+ );
14366
+ }
13776
14367
  async function applyPatch(binaryPath, version, desired, configHash, opts) {
13777
- const backup = pristineBackupPath(version, binaryPath);
13778
- mkdirSync7(backupDir(), { recursive: true });
13779
- if (opts.restoreFirst) {
13780
- if (!existsSync6(backup)) {
13781
- return { ok: false, message: `Cannot re-patch: pristine backup missing at ${backup}. Reinstall claude, then run clodex patch.` };
13782
- }
13783
- copyFileSync(backup, binaryPath);
13784
- } else if (!existsSync6(backup)) {
13785
- copyFileSync(binaryPath, backup);
13786
- }
13787
- copyFileSync(backup, join7(backupDir(), "native-binary.backup"));
13788
- const { tryDetectInstallation, readContent, writeContent } = await import("tweakcc");
14368
+ let candidateDir;
13789
14369
  let results;
14370
+ let patchedSize;
14371
+ let patchedSha256;
14372
+ let backup;
14373
+ let pristineSha256;
13790
14374
  try {
13791
- const installation = await tryDetectInstallation({ path: binaryPath });
13792
- const source = await readContent(installation);
13793
- const patched = applyClodexPatches(source, desired.config);
14375
+ mkdirSync7(backupDir(), { recursive: true });
14376
+ const { tryDetectInstallation, readContent, writeContent } = await import("tweakcc");
14377
+ candidateDir = mkdtempSync(join8(dirname5(binaryPath), ".clodex-patch-"));
14378
+ const candidatePath = join8(candidateDir, basename(binaryPath));
14379
+ const seedCandidate = async (from) => {
14380
+ copyFileSync(from, candidatePath);
14381
+ const installation = await tryDetectInstallation({ path: candidatePath });
14382
+ return { installation, source: await readContent(installation) };
14383
+ };
14384
+ const facts = collectPristineFacts({
14385
+ version,
14386
+ binaryPath,
14387
+ manifest: opts.manifest
14388
+ });
14389
+ const initial = planPristineSource(facts);
14390
+ let loaded = null;
14391
+ let plan;
14392
+ if (initial.action === "inspect") {
14393
+ loaded = await seedCandidate(binaryPath);
14394
+ if (looksLikeLegacyClodexPatch(loaded.source)) {
14395
+ p11.log.warn(
14396
+ `${binaryPath} carries no clodex patch marker but does look like a patch from a clodex older than the effort patch sites. Treating it as pristine; if \`/model\` shows stale entries afterwards, reinstall Claude Code and run \`clodex patch\` again.`
14397
+ );
14398
+ }
14399
+ plan = planInspectedPristineSource(facts, { patched: isPatchedClaudeSource(loaded.source) });
14400
+ if (plan.action !== "snapshot") loaded = null;
14401
+ } else {
14402
+ plan = initial;
14403
+ }
14404
+ if (plan.action === "error") return { ok: false, message: plan.message };
14405
+ for (const note of plan.notes) p11.log.warn(note);
14406
+ if (plan.action === "restore") {
14407
+ const verified = verifyPristineSource(plan, version);
14408
+ if (!verified.ok) return { ok: false, message: verified.message };
14409
+ p11.log.info(`Binary differs from its pristine backup \u2014 building a fresh patch candidate from ${plan.backupPath}.`);
14410
+ }
14411
+ pristineSha256 = plan.pristineSha256;
14412
+ backup = plan.backupPath;
14413
+ if (!loaded) {
14414
+ loaded = await seedCandidate(backup);
14415
+ if (isPatchedClaudeSource(loaded.source)) {
14416
+ return { ok: false, message: describePoisonedPristineSource(backup, version) };
14417
+ }
14418
+ } else if (plan.action === "snapshot") {
14419
+ publishBackupFile(candidatePath, plan.backupPath);
14420
+ }
14421
+ const canonical = contentAddressedBackupPath(version, pristineSha256);
14422
+ if (canonical !== backup) {
14423
+ const alreadyStored = facts.backups.some(
14424
+ (candidate) => candidate.path === canonical && candidate.sha256 === pristineSha256
14425
+ );
14426
+ if (!alreadyStored) publishBackupFile(backup, canonical);
14427
+ backup = canonical;
14428
+ }
14429
+ publishBackupFile(backup, tweakccMirrorBackupPath());
14430
+ const patched = applyClodexPatches(loaded.source, desired.config);
13794
14431
  results = patched.results;
13795
- await writeContent(installation, patched.content);
14432
+ const failedEffortPatches = requiredEffortPatchFailures(results);
14433
+ if (failedEffortPatches.length > 0) {
14434
+ throw new PatchApplyError(
14435
+ `clodex patch: required effort patches failed: ${failedEffortPatches.map((result) => result.name).join("; ")}`,
14436
+ results
14437
+ );
14438
+ }
14439
+ await writeContent(loaded.installation, patched.content);
14440
+ patchedSize = statSync4(candidatePath).size;
14441
+ patchedSha256 = sha256File(candidatePath);
14442
+ renameSync3(candidatePath, binaryPath);
13796
14443
  } catch (err) {
13797
14444
  const detailLines = err instanceof PatchApplyError ? summarizePatchResults(err.results) : [];
13798
14445
  if (opts.trace && detailLines.length) {
@@ -13804,6 +14451,19 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
13804
14451
  message: `Patch failed: ${err instanceof Error ? err.message : String(err)}`,
13805
14452
  detailLines
13806
14453
  };
14454
+ } finally {
14455
+ if (candidateDir !== void 0) {
14456
+ try {
14457
+ rmSync(candidateDir, { recursive: true, force: true });
14458
+ } catch (err) {
14459
+ if (opts.trace) {
14460
+ process.stderr.write(
14461
+ `clodex patch: could not remove temporary candidate directory: ${err instanceof Error ? err.message : String(err)}
14462
+ `
14463
+ );
14464
+ }
14465
+ }
14466
+ }
13807
14467
  }
13808
14468
  if (opts.trace) {
13809
14469
  process.stderr.write(`${summarizePatchResults(results).join("\n")}
@@ -13813,9 +14473,10 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
13813
14473
  binaryPath,
13814
14474
  claudeVersion: version,
13815
14475
  configHash,
13816
- patchedSize: statSync3(binaryPath).size,
13817
- patchedSha256: sha256File(binaryPath),
14476
+ patchedSize,
14477
+ patchedSha256,
13818
14478
  backupPath: backup,
14479
+ pristineSha256,
13819
14480
  patchedAt: (/* @__PURE__ */ new Date()).toISOString()
13820
14481
  };
13821
14482
  writePatchManifest(manifest);
@@ -13828,28 +14489,54 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
13828
14489
  detailLines: summarizePatchResults(results)
13829
14490
  };
13830
14491
  }
13831
- async function runPatchCommand(opts = {}) {
13832
- const resolved = resolveClaudeBinaryForPatch();
13833
- if (!resolved) {
13834
- p11.log.error("claude binary not found. Install Claude Code or set TWEAKCC_CC_INSTALLATION_PATH.");
14492
+ function runRestoreCommand(target) {
14493
+ if (!target.ok && target.reason === "binary-not-found") {
14494
+ p11.log.error(describePatchTargetFailure(target));
13835
14495
  return 1;
13836
14496
  }
13837
- const { binaryPath, version } = resolved;
13838
- if (opts.restore) {
13839
- const manifest2 = readPatchManifest();
13840
- const backup = manifest2?.backupPath && existsSync6(manifest2.backupPath) ? manifest2.backupPath : pristineBackupPath(version, binaryPath);
13841
- if (!existsSync6(backup)) {
13842
- p11.log.error(`No pristine backup found for claude ${version} (${backup}).`);
13843
- return 1;
13844
- }
13845
- copyFileSync(backup, binaryPath);
13846
- try {
13847
- unlinkSync4(getPatchManifestPath());
13848
- } catch {
13849
- }
13850
- p11.log.success(`Restored pristine claude ${version} from ${backup}.`);
13851
- return 0;
14497
+ const binaryPath = target.binaryPath;
14498
+ const manifest = readPatchManifest();
14499
+ let version;
14500
+ if (target.ok) {
14501
+ version = target.version;
14502
+ } else if (manifest?.binaryPath === binaryPath && manifest.claudeVersion) {
14503
+ version = manifest.claudeVersion;
14504
+ p11.log.warn(
14505
+ `Could not read the version of ${binaryPath} (\`claude --version\` failed) \u2014 using claude ${version} from the patch manifest, which recorded it when this binary was patched. This is expected when a bad patch left the install unable to run.`
14506
+ );
14507
+ } else {
14508
+ p11.log.error(
14509
+ `Could not determine the version of ${binaryPath} (\`claude --version\` failed), and no patch manifest records a pristine backup for it, so clodex cannot tell which backup belongs to this install. Restore it by hand from ${backupDir()} (or reinstall Claude Code).`
14510
+ );
14511
+ return 1;
14512
+ }
14513
+ const plan = planRestoreOnly(collectPristineFacts({ version, binaryPath, manifest }));
14514
+ if (plan.action === "error") {
14515
+ p11.log.error(plan.message);
14516
+ return 1;
14517
+ }
14518
+ for (const note of plan.notes) p11.log.warn(note);
14519
+ const verified = verifyPristineSource(plan, version);
14520
+ if (!verified.ok) {
14521
+ p11.log.error(verified.message);
14522
+ return 1;
14523
+ }
14524
+ copyFileSync(plan.backupPath, binaryPath);
14525
+ try {
14526
+ unlinkSync4(getPatchManifestPath());
14527
+ } catch {
14528
+ }
14529
+ p11.log.success(`Restored pristine claude ${version} from ${plan.backupPath}.`);
14530
+ return 0;
14531
+ }
14532
+ async function runPatchCommand(opts = {}) {
14533
+ const target = resolveClaudeBinaryForPatch();
14534
+ if (opts.restore) return runRestoreCommand(target);
14535
+ if (!target.ok) {
14536
+ p11.log.error(describePatchTargetFailure(target));
14537
+ return 1;
13852
14538
  }
14539
+ const { binaryPath, version } = target;
13853
14540
  const desired = buildDesiredPatchConfig();
13854
14541
  if (Object.keys(desired.config).length === 0) {
13855
14542
  p11.log.error("No favorite models to patch. Save favorites with `clodex models` first.");
@@ -13858,13 +14545,14 @@ async function runPatchCommand(opts = {}) {
13858
14545
  for (const id of desired.unknownWindows) {
13859
14546
  p11.log.warn(`No context window metadata for ${id} \u2014 Claude Code will assume the 200k default.`);
13860
14547
  }
14548
+ reportRejectedModelAliases(desired.rejectedAliasRejections);
13861
14549
  const configHash = computePatchConfigHash(desired.config);
13862
14550
  const manifest = readPatchManifest();
13863
14551
  const state = evaluatePatchState(manifest, {
13864
14552
  binaryPath,
13865
14553
  claudeVersion: version,
13866
14554
  configHash,
13867
- binarySize: statSync3(binaryPath).size
14555
+ binarySize: statSync4(binaryPath).size
13868
14556
  });
13869
14557
  if (state === "current") {
13870
14558
  p11.log.success(`claude ${version} is already patched with the current model config \u2014 nothing to do.`);
@@ -13876,14 +14564,9 @@ async function runPatchCommand(opts = {}) {
13876
14564
  return 1;
13877
14565
  }
13878
14566
  try {
13879
- const backup = pristineBackupPath(version, binaryPath);
13880
- const restoreFirst = existsSync6(backup) && sha256File(backup) !== sha256File(binaryPath);
13881
- if (restoreFirst) {
13882
- p11.log.info("Binary differs from its pristine backup \u2014 restoring it before patching fresh.");
13883
- }
13884
14567
  const outcome = await applyPatch(binaryPath, version, desired, configHash, {
13885
14568
  trace: opts.trace ?? false,
13886
- restoreFirst
14569
+ manifest
13887
14570
  });
13888
14571
  if (!outcome.ok) {
13889
14572
  p11.log.error(outcome.message);
@@ -13903,15 +14586,21 @@ async function runLaunchPatchCheck(opts = {}) {
13903
14586
  try {
13904
14587
  const desired = buildDesiredPatchConfig();
13905
14588
  if (Object.keys(desired.config).length === 0) return;
13906
- const resolved = resolveClaudeBinaryForPatch();
13907
- if (!resolved) return;
14589
+ const target = resolveClaudeBinaryForPatch();
14590
+ if (!target.ok) {
14591
+ if (target.reason === "version-unknown" && !opts.agentStdout) {
14592
+ console.error(pc12.dim(`clodex: ${describePatchTargetFailure(target)}`));
14593
+ }
14594
+ return;
14595
+ }
14596
+ const resolved = target;
13908
14597
  const configHash = computePatchConfigHash(desired.config);
13909
14598
  const manifest = readPatchManifest();
13910
14599
  const state = evaluatePatchState(manifest, {
13911
14600
  binaryPath: resolved.binaryPath,
13912
14601
  claudeVersion: resolved.version,
13913
14602
  configHash,
13914
- binarySize: statSync3(resolved.binaryPath).size
14603
+ binarySize: statSync4(resolved.binaryPath).size
13915
14604
  });
13916
14605
  if (state === "current") return;
13917
14606
  const interactive = !opts.dryRun && !opts.agentStdout && process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -14351,6 +15040,7 @@ ${pc13.bold("Behavior:")}
14351
15040
  proxy mode, without opening the interactive manager.
14352
15041
  --alias <name=target> saves a short name for a proxy-mode favorite. The
14353
15042
  target is clodex:<provider-id>:<model-id> (the clodex: prefix is optional).
15043
+ Alias names are stored lowercase and cannot use client-reserved model names.
14354
15044
  --unalias <name> removes a saved short name.
14355
15045
 
14356
15046
  ${pc13.bold("How it works:")}
@@ -14392,7 +15082,19 @@ function printHelp(text4) {
14392
15082
  ${text4}
14393
15083
  `);
14394
15084
  }
15085
+ function reportInactiveCatalogAliases(modelAliases) {
15086
+ const unavailableAliases = modelAliases.filter((alias) => alias.unavailableReason !== void 0);
15087
+ if (unavailableAliases.length === 0) return;
15088
+ const warningLines = unavailableAliases.flatMap((alias) => alias.sourceNames?.length ? alias.sourceNames.map((name) => ` ${JSON.stringify(name)} \u2014 ${alias.unavailableReason}`) : [
15089
+ ` ${JSON.stringify(alias.savedName ?? alias.name)} \u2014 ${alias.unavailableReason}`
15090
+ ]);
15091
+ p12.log.warn(
15092
+ `${warningLines.length} saved model alias${warningLines.length === 1 ? "" : "es"} inactive. Saved entries were preserved.
15093
+ ` + warningLines.join("\n")
15094
+ );
15095
+ }
14395
15096
  async function launchClaudeViaCatalog(catalogRoutes, startingRoute, modelAliases, contextWindow, trace, claudeArgs) {
15097
+ reportInactiveCatalogAliases(modelAliases);
14396
15098
  let proxyHandle;
14397
15099
  try {
14398
15100
  proxyHandle = await startProxyCatalog(
@@ -14452,27 +15154,36 @@ async function runModelsCommand(opts = {}) {
14452
15154
  p12.log.info("Add it with `clodex models`, then save the alias.");
14453
15155
  return 1;
14454
15156
  }
14455
- const modelAliases = (prefs2.modelAliases ?? []).filter((alias) => alias.name !== parsed.name);
15157
+ if (prefs2.modelAliases !== void 0 && !Array.isArray(prefs2.modelAliases)) {
15158
+ p12.log.error('Saved model aliases are malformed: "modelAliases" must be an array.');
15159
+ return 1;
15160
+ }
15161
+ const aliases = prefs2.modelAliases ?? [];
15162
+ const modelAliases = aliases.filter((alias) => !modelAliasMatchesName(alias, parsed.name));
14456
15163
  modelAliases.push(parsed);
14457
15164
  savePreferences({ modelAliases });
14458
15165
  p12.log.success(`Saved model alias ${parsed.name} \u2192 ${modelAliasTarget(parsed)}.`);
14459
15166
  return 0;
14460
15167
  }
14461
15168
  if (opts.unalias !== void 0) {
14462
- const name = opts.unalias.trim();
14463
- if (!isValidModelAlias(name)) {
14464
- p12.log.error("Alias names must be 1-64 letters, numbers, dots, underscores, or hyphens.");
15169
+ const requestedName = opts.unalias.trim();
15170
+ const name = canonicalModelAliasName(requestedName);
15171
+ const prefs2 = loadPreferences();
15172
+ if (prefs2.modelAliases !== void 0 && !Array.isArray(prefs2.modelAliases)) {
15173
+ p12.log.error('Saved model aliases are malformed: "modelAliases" must be an array.');
14465
15174
  return 1;
14466
15175
  }
14467
- const prefs2 = loadPreferences();
14468
15176
  const aliases = prefs2.modelAliases ?? [];
14469
- const modelAliases = aliases.filter((alias) => alias.name !== name);
14470
- if (modelAliases.length === aliases.length) {
14471
- p12.log.error(`No model alias named ${name} is saved.`);
15177
+ const modelAliases = aliases.filter((alias) => !modelAliasMatchesStoredName(alias, requestedName));
15178
+ const removedCount = aliases.length - modelAliases.length;
15179
+ if (removedCount === 0) {
15180
+ p12.log.error(`No model alias named ${JSON.stringify(requestedName)} is saved.`);
14472
15181
  return 1;
14473
15182
  }
14474
15183
  savePreferences({ modelAliases });
14475
- p12.log.success(`Removed model alias ${name}.`);
15184
+ p12.log.success(
15185
+ removedCount === 1 ? `Removed model alias ${name || JSON.stringify(requestedName)}.` : `Removed ${removedCount} model aliases named ${name || JSON.stringify(requestedName)}.`
15186
+ );
14476
15187
  return 0;
14477
15188
  }
14478
15189
  if (opts.list) {
@@ -14956,7 +15667,11 @@ Error: ${launchPlan.error}
14956
15667
  return launchClaudeViaCatalog(
14957
15668
  catalogRoutes,
14958
15669
  startingRoute,
14959
- resolveCatalogModelAliases(prefs.modelAliases ?? [], resolveRoute),
15670
+ resolveCatalogModelAliases(
15671
+ prefs.modelAliases,
15672
+ resolveRoute,
15673
+ catalogRoutes
15674
+ ),
14960
15675
  selectedModel.contextWindow,
14961
15676
  trace,
14962
15677
  claudeArgs
@@ -15203,6 +15918,7 @@ export {
15203
15918
  modelsHelpText,
15204
15919
  parseArgs,
15205
15920
  patchHelpText,
15921
+ reportInactiveCatalogAliases,
15206
15922
  rootHelpText,
15207
15923
  runClaudeCommand,
15208
15924
  runModelsCommand,