@bman654/clodex 2.1.4 → 2.1.6

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.6",
220
221
  publishConfig: {
221
222
  access: "public"
222
223
  },
@@ -4006,6 +4007,11 @@ function clampRetryAfterSeconds(value) {
4006
4007
  }
4007
4008
  return Math.min(Math.round(value), MAX_RETRY_AFTER_SECONDS);
4008
4009
  }
4010
+ function retryAfterFromText(message) {
4011
+ if (typeof message !== "string") return void 0;
4012
+ const match = /retry after (\d+)s\b/i.exec(message);
4013
+ return match ? Number(match[1]) : void 0;
4014
+ }
4009
4015
  function numericRetryAfterSeconds(inner) {
4010
4016
  const data = inner.data;
4011
4017
  const fromBody = data?.error?.retry_after_seconds;
@@ -4013,12 +4019,84 @@ function numericRetryAfterSeconds(inner) {
4013
4019
  const fromHeader = inner.responseHeaders?.["retry-after"];
4014
4020
  if (typeof fromHeader === "string" && /^\d+$/.test(fromHeader.trim())) return Number(fromHeader.trim());
4015
4021
  for (const message of [data?.error?.message, inner.message]) {
4016
- if (typeof message !== "string") continue;
4017
- const match = /retry after (\d+)s\b/i.exec(message);
4018
- if (match) return Number(match[1]);
4022
+ const fromText = retryAfterFromText(message);
4023
+ if (fromText !== void 0) return fromText;
4019
4024
  }
4020
4025
  return void 0;
4021
4026
  }
4027
+ var MAX_CLIENT_MESSAGE_CHARS = 240;
4028
+ var CHUNK_DISCRIMINATOR_TYPES = /* @__PURE__ */ new Set(["error", "response.failed"]);
4029
+ function asRecord(value) {
4030
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
4031
+ }
4032
+ function nonEmptyString(record, key) {
4033
+ const value = record[key];
4034
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
4035
+ }
4036
+ function errorCodeValue(record) {
4037
+ const value = record.code;
4038
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
4039
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
4040
+ }
4041
+ function frameStatusCode(code, discriminator) {
4042
+ if (code !== void 0 && /^\d{3}$/.test(code)) {
4043
+ const numeric = Number(code);
4044
+ if (numeric >= 400 && numeric <= 599) return numeric;
4045
+ }
4046
+ if (/insufficient_quota|rate_limit/.test(discriminator)) return 429;
4047
+ if (discriminator.includes("authentication")) return 401;
4048
+ if (discriminator.includes("permission")) return 403;
4049
+ if (discriminator.includes("not_found")) return 404;
4050
+ if (/invalid|bad_request|context_length/.test(discriminator)) return 400;
4051
+ if (discriminator.includes("overload")) return 503;
4052
+ if (discriminator.includes("timeout")) return 504;
4053
+ return 500;
4054
+ }
4055
+ function frameIsContextLengthExceeded(discriminator, message) {
4056
+ if (/context_length|context_window/.test(discriminator)) return true;
4057
+ return /context_length_exceeded|maximum context length|prompt is too long/i.test(message);
4058
+ }
4059
+ function frameIsRetryable(frame) {
4060
+ if (frame.transportCode !== void 0) return true;
4061
+ const { statusCode } = frame;
4062
+ return statusCode === 408 || statusCode === 409 || statusCode === 429 || statusCode >= 500;
4063
+ }
4064
+ function providerErrorFrame(err) {
4065
+ const outer = asRecord(err);
4066
+ if (!outer) return void 0;
4067
+ const nested = asRecord(outer.error) ?? asRecord(asRecord(outer.response)?.error);
4068
+ const unwrapped = !(err instanceof Error) && (typeof outer.type === "string" || Object.hasOwn(outer, "code") || Object.hasOwn(outer, "param")) ? outer : void 0;
4069
+ const payload = nested ?? unwrapped;
4070
+ if (!payload) return void 0;
4071
+ const rawMessage = payload.message;
4072
+ if (typeof rawMessage !== "string") return void 0;
4073
+ const message = rawMessage.trim() !== "" ? rawMessage : "Provider returned an error";
4074
+ const code = errorCodeValue(payload);
4075
+ const rawType = nonEmptyString(payload, "type");
4076
+ const type = rawType !== void 0 && CHUNK_DISCRIMINATOR_TYPES.has(rawType) ? void 0 : rawType;
4077
+ const discriminator = [code, type].filter((value) => value !== void 0).join(" ").toLowerCase();
4078
+ const statusCode = frameStatusCode(code, discriminator);
4079
+ const rawRetryAfter = statusCode === 429 ? typeof payload.retry_after_seconds === "number" && Number.isFinite(payload.retry_after_seconds) && payload.retry_after_seconds >= 0 ? payload.retry_after_seconds : retryAfterFromText(message) : void 0;
4080
+ let serialized;
4081
+ try {
4082
+ serialized = JSON.stringify(payload) ?? message;
4083
+ } catch {
4084
+ serialized = message;
4085
+ }
4086
+ return {
4087
+ message,
4088
+ statusCode,
4089
+ contextLengthExceeded: frameIsContextLengthExceeded(discriminator, message),
4090
+ ...rawRetryAfter !== void 0 ? { retryAfterSeconds: clampRetryAfterSeconds(rawRetryAfter) } : {},
4091
+ ...code === "websocket_transport_error" ? { transportCode: "websocket_transport_error" } : {},
4092
+ serialized
4093
+ };
4094
+ }
4095
+ function frameFromError(err) {
4096
+ const retry = RetryError.isInstance(err) ? err : void 0;
4097
+ const frame = providerErrorFrame(retry?.lastError ?? err);
4098
+ return frame ? { frame, attemptCount: retry?.errors.length ?? 1 } : void 0;
4099
+ }
4022
4100
  function boundedTransportCode(data) {
4023
4101
  if (!data || typeof data !== "object") return void 0;
4024
4102
  const error = data.error;
@@ -4028,7 +4106,19 @@ function boundedTransportCode(data) {
4028
4106
  function sdkUpstreamErrorDetails(err) {
4029
4107
  const retry = RetryError.isInstance(err) ? err : void 0;
4030
4108
  const inner = retry?.lastError ?? err;
4031
- if (!APICallError.isInstance(inner)) return void 0;
4109
+ if (!APICallError.isInstance(inner)) {
4110
+ const recovered = frameFromError(err);
4111
+ if (!recovered) return void 0;
4112
+ const { frame, attemptCount } = recovered;
4113
+ return {
4114
+ statusCode: frame.statusCode,
4115
+ errorContent: frame.serialized,
4116
+ isRetryable: frameIsRetryable(frame),
4117
+ attemptCount,
4118
+ ...frame.retryAfterSeconds !== void 0 ? { retryAfterSeconds: frame.retryAfterSeconds } : {},
4119
+ ...frame.transportCode !== void 0 ? { transportCode: frame.transportCode } : {}
4120
+ };
4121
+ }
4032
4122
  let errorContent = inner.responseBody;
4033
4123
  if (!errorContent && inner.data !== void 0) {
4034
4124
  try {
@@ -4049,6 +4139,8 @@ function sdkUpstreamErrorDetails(err) {
4049
4139
  };
4050
4140
  }
4051
4141
  function isContextLengthExceededError(err, formattedMessage = "") {
4142
+ const frame = frameFromError(err)?.frame;
4143
+ if (frame) return frame.contextLengthExceeded;
4052
4144
  const details = sdkUpstreamErrorDetails(err);
4053
4145
  const rec = err && typeof err === "object" ? err : void 0;
4054
4146
  const candidates = [
@@ -4067,6 +4159,11 @@ function isContextLengthExceededError(err, formattedMessage = "") {
4067
4159
  function formatUpstreamError(err) {
4068
4160
  if (!err || typeof err !== "object") return "Upstream model request failed.";
4069
4161
  const rec = err;
4162
+ const frame = frameFromError(err)?.frame;
4163
+ if (frame) {
4164
+ const short = truncateForClient(sanitizeMessage(frame.message)) || "Upstream model request failed.";
4165
+ return `${short} (HTTP ${frame.statusCode})`;
4166
+ }
4070
4167
  if (rec.data?.error?.message) {
4071
4168
  const short = sanitizeMessage(rec.data.error.message);
4072
4169
  return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
@@ -4105,6 +4202,8 @@ function upstreamHttpStatus(err, message) {
4105
4202
  const code = err.statusCode;
4106
4203
  if (typeof code === "number" && code >= 400 && code <= 599) return code;
4107
4204
  }
4205
+ const frameStatus = frameFromError(err)?.frame.statusCode;
4206
+ if (frameStatus !== void 0) return frameStatus;
4108
4207
  if (message.includes("HTTP 429") || message.includes("429")) return 429;
4109
4208
  if (message.includes("HTTP 400")) return 400;
4110
4209
  return 500;
@@ -4125,6 +4224,10 @@ function anthropicErrorType(status) {
4125
4224
  return "api_error";
4126
4225
  }
4127
4226
  }
4227
+ function truncateForClient(message) {
4228
+ if (message.length <= MAX_CLIENT_MESSAGE_CHARS) return message;
4229
+ return `${[...message].slice(0, MAX_CLIENT_MESSAGE_CHARS - 1).join("")}\u2026`;
4230
+ }
4128
4231
  function sanitizeMessage(message) {
4129
4232
  const line = message.split("\n")[0]?.trim() ?? message;
4130
4233
  if (line.startsWith("RetryError") || line.includes("AI_RetryError")) {
@@ -4373,6 +4476,15 @@ function responseErrorCode(event) {
4373
4476
  const responseError = response?.error && typeof response.error === "object" ? response.error : void 0;
4374
4477
  return typeof responseError?.code === "string" ? responseError.code : void 0;
4375
4478
  }
4479
+ function responseErrorType(event) {
4480
+ if (!event || typeof event !== "object") return void 0;
4481
+ const record = event;
4482
+ const error = record.error && typeof record.error === "object" ? record.error : void 0;
4483
+ if (typeof error?.type === "string") return error.type;
4484
+ const response = record.response && typeof record.response === "object" ? record.response : void 0;
4485
+ const responseError = response?.error && typeof response.error === "object" ? response.error : void 0;
4486
+ return typeof responseError?.type === "string" ? responseError.type : void 0;
4487
+ }
4376
4488
  function responseRetryAfterSeconds(event) {
4377
4489
  if (!event || typeof event !== "object") return void 0;
4378
4490
  const record = event;
@@ -4387,6 +4499,27 @@ function responseRetryAfterSeconds(event) {
4387
4499
  }
4388
4500
  return void 0;
4389
4501
  }
4502
+ function responseErrorStatus(event) {
4503
+ if (!event || typeof event !== "object") return void 0;
4504
+ const record = event;
4505
+ for (const candidate of [record.status, record.error?.status]) {
4506
+ if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
4507
+ return candidate;
4508
+ }
4509
+ }
4510
+ return void 0;
4511
+ }
4512
+ function responseErrorMessage(event) {
4513
+ if (!event || typeof event !== "object") return void 0;
4514
+ const record = event;
4515
+ const response = record.response && typeof record.response === "object" ? record.response : void 0;
4516
+ for (const candidate of [record.error, response?.error, record]) {
4517
+ if (!candidate || typeof candidate !== "object") continue;
4518
+ const message = candidate.message;
4519
+ if (typeof message === "string" && message.trim()) return message.trim();
4520
+ }
4521
+ return void 0;
4522
+ }
4390
4523
  function boundedDiagnosticIdentifier(value) {
4391
4524
  if (typeof value !== "string") return void 0;
4392
4525
  const normalized = value.trim();
@@ -4893,7 +5026,8 @@ function handleSocketMessage(entry, data) {
4893
5026
  );
4894
5027
  return;
4895
5028
  }
4896
- if (FAILURE_EVENT_TYPES.has(type ?? "")) {
5029
+ const errorStatus = type === "error" && !ctx.emittedModelData ? responseErrorStatus(event) : void 0;
5030
+ if (FAILURE_EVENT_TYPES.has(type ?? "") && (errorStatus === void 0 || willRetry)) {
4897
5031
  emitResponseErrorDiagnostic(entry, ctx, {
4898
5032
  source: "response_event",
4899
5033
  upstreamEventType: type,
@@ -4910,6 +5044,32 @@ function handleSocketMessage(entry, data) {
4910
5044
  dispatchContext(replacement, ctx);
4911
5045
  return;
4912
5046
  }
5047
+ if (errorStatus !== void 0) {
5048
+ const statedRetryAfter = errorStatus === 429 ? responseRetryAfterSeconds(event) : void 0;
5049
+ const retryAfterSeconds = statedRetryAfter === void 0 ? void 0 : clampRetryAfterSeconds(statedRetryAfter);
5050
+ const reason = responseErrorMessage(event) ?? `OpenAI rejected the request (HTTP ${errorStatus})`;
5051
+ failContext(
5052
+ entry,
5053
+ ctx,
5054
+ retryAfterSeconds === void 0 ? reason : `${reason}; retry after ${retryAfterSeconds}s`,
5055
+ {
5056
+ source: "error_frame",
5057
+ // Names the failure. Without it this record — now the ONLY one for a
5058
+ // rejection — can carry no indication of what failed, since a bare
5059
+ // error frame often has no `code` at all.
5060
+ errorType: boundedDiagnosticIdentifier(responseErrorType(event)),
5061
+ // Upstream-controlled, so bounded like every other identifier in this
5062
+ // file's diagnostics. The connection-limit branch can pass its code raw
5063
+ // only because it has just been compared `===` to a known constant.
5064
+ errorCode: boundedDiagnosticIdentifier(errorCode),
5065
+ mappedStatusCode: errorStatus,
5066
+ ...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
5067
+ },
5068
+ errorStatus,
5069
+ retryAfterSeconds
5070
+ );
5071
+ return;
5072
+ }
4913
5073
  ctx.pendingEvents.push(event);
4914
5074
  if (isModelDataEvent(type)) flushPending(ctx);
4915
5075
  if (TERMINAL_EVENT_TYPES.has(type ?? "") || type === "error") {
@@ -5538,6 +5698,7 @@ async function createLanguageModel(spec) {
5538
5698
  }
5539
5699
  var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
5540
5700
  var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
5701
+ var GPT_56_EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
5541
5702
  var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
5542
5703
  var MISTRAL_EFFORT_LEVELS = ["high", "off"];
5543
5704
  var XAI_EFFORT_LEVELS = ["none", "low", "medium", "high"];
@@ -5701,7 +5862,16 @@ function mapCodexEffortToAnthropic(effort) {
5701
5862
  return void 0;
5702
5863
  }
5703
5864
  }
5704
- function mapCodexEffortToOpenAI(effort) {
5865
+ function isGpt56Model(modelId) {
5866
+ return /^gpt-5\.6(?:-|$)/i.test(modelId);
5867
+ }
5868
+ function isReasoningSummaryUnsupportedModel(modelId) {
5869
+ return /codex-spark(?:-|$)/i.test(modelId);
5870
+ }
5871
+ function mapCodexEffortToOpenAI(effort, modelId) {
5872
+ if (modelId && isGpt56Model(modelId) && GPT_56_EFFORT_LEVELS.includes(effort)) {
5873
+ return effort;
5874
+ }
5705
5875
  if (effort === "xhigh") return "high";
5706
5876
  const allowed = ["low", "medium", "high"];
5707
5877
  return allowed.includes(effort) ? effort : void 0;
@@ -5782,7 +5952,7 @@ function getReasoningCapabilities(npm, modelId, metadata) {
5782
5952
  const prefersResponses = modelPrefersResponsesApi(modelId);
5783
5953
  if (prefersResponses || metadata?.reasoning) {
5784
5954
  return {
5785
- levels: [...OPENAI_EFFORT_LEVELS],
5955
+ levels: isGpt56Model(modelId) ? [...GPT_56_EFFORT_LEVELS] : [...OPENAI_EFFORT_LEVELS],
5786
5956
  defaultLevel: "medium",
5787
5957
  supportsSummaries: true,
5788
5958
  mode: "controllable",
@@ -5904,6 +6074,24 @@ function getReasoningCapabilities(npm, modelId, metadata) {
5904
6074
  }
5905
6075
  return EMPTY_REASONING;
5906
6076
  }
6077
+ function getPatchReasoningCapabilities(npm, modelId, metadata) {
6078
+ if (metadata?.reasoning === false && !hasSupportedParameter(metadata, "reasoning_effort") && !hasSupportedParameter(metadata, "reasoning")) {
6079
+ return EMPTY_REASONING;
6080
+ }
6081
+ const capabilities = getReasoningCapabilities(npm, modelId, metadata);
6082
+ const seenProviderOptions = /* @__PURE__ */ new Set();
6083
+ return {
6084
+ ...capabilities,
6085
+ levels: capabilities.levels.filter((level) => {
6086
+ const providerOptions = effortProviderOptions(npm, level, modelId, metadata);
6087
+ if (!providerOptions) return false;
6088
+ const fingerprint = JSON.stringify(providerOptions);
6089
+ if (seenProviderOptions.has(fingerprint)) return false;
6090
+ seenProviderOptions.add(fingerprint);
6091
+ return true;
6092
+ })
6093
+ };
6094
+ }
5907
6095
  function effortProviderOptions(npm, effort, modelId, metadata) {
5908
6096
  if (!effort) return void 0;
5909
6097
  if (isOpenRouterRoute(npm, metadata)) {
@@ -5915,8 +6103,9 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
5915
6103
  }
5916
6104
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
5917
6105
  if (!modelId || !modelPrefersResponsesApi(modelId)) return void 0;
5918
- const reasoningEffort = mapCodexEffortToOpenAI(effort);
5919
- return reasoningEffort ? { openai: { reasoningEffort } } : void 0;
6106
+ const reasoningEffort = mapCodexEffortToOpenAI(effort, modelId);
6107
+ if (!reasoningEffort) return void 0;
6108
+ return isReasoningSummaryUnsupportedModel(modelId) ? { openai: { reasoningEffort, reasoningSummary: null } } : { openai: { reasoningEffort } };
5920
6109
  }
5921
6110
  if (npm === "@ai-sdk/xai") {
5922
6111
  if (!modelId || !isXaiReasoningEffortModel(modelId)) return void 0;
@@ -8817,31 +9006,151 @@ function sendJson(res, status, body) {
8817
9006
  }
8818
9007
 
8819
9008
  // 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);
9009
+ var MODEL_ALIAS_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
9010
+ var RESERVED_MODEL_ALIASES = /* @__PURE__ */ new Set([
9011
+ "sonnet",
9012
+ "opus",
9013
+ "haiku",
9014
+ "fable",
9015
+ "best",
9016
+ "default",
9017
+ "opusplan",
9018
+ "inherit"
9019
+ ]);
9020
+ function canonicalModelAliasName(name) {
9021
+ return name.trim().toLowerCase();
9022
+ }
9023
+ function isReservedModelAlias(name) {
9024
+ return RESERVED_MODEL_ALIASES.has(
9025
+ stripOneMContextSuffix(canonicalModelAliasName(name))
9026
+ );
9027
+ }
9028
+ function isModelAliasNameSyntax(name) {
9029
+ return MODEL_ALIAS_PATTERN.test(canonicalModelAliasName(name));
9030
+ }
9031
+ function modelAliasMatchesName(value, name) {
9032
+ if (!value || typeof value !== "object" || !("name" in value)) return false;
9033
+ const candidate = value.name;
9034
+ return typeof candidate === "string" && canonicalModelAliasName(candidate) === canonicalModelAliasName(name);
9035
+ }
9036
+ function modelAliasMatchesStoredName(value, name) {
9037
+ if (!value || typeof value !== "object" || !("name" in value)) return false;
9038
+ const candidate = value.name;
9039
+ if (typeof candidate !== "string") return false;
9040
+ const requested = name.trim();
9041
+ return isModelAliasNameSyntax(requested) ? canonicalModelAliasName(candidate) === canonicalModelAliasName(requested) : candidate.trim() === requested;
9042
+ }
9043
+ function describeModelAliasRejection(reason) {
9044
+ switch (reason) {
9045
+ case "invalid-name":
9046
+ return "invalid name";
9047
+ case "reserved-name":
9048
+ return "reserved client name";
9049
+ case "invalid-target":
9050
+ return "invalid target";
9051
+ case "conflicting-targets":
9052
+ return "conflicting targets";
9053
+ case "target-not-favorite":
9054
+ return "target is not a saved favorite";
9055
+ }
9056
+ }
9057
+ function normalizeModelAliases(value) {
9058
+ if (value === void 0) {
9059
+ return { aliases: [], accepted: [], rejected: [], rejections: [] };
9060
+ }
9061
+ if (!Array.isArray(value)) {
9062
+ throw new TypeError('Saved model aliases are malformed: "modelAliases" must be an array.');
9063
+ }
9064
+ const candidates = [];
9065
+ const groups = /* @__PURE__ */ new Map();
9066
+ for (const [index, item] of value.entries()) {
9067
+ if (!item || typeof item !== "object" || typeof item.name !== "string") {
9068
+ throw new TypeError(
9069
+ `Saved model aliases are malformed: "modelAliases[${index}]" must be an object with a string "name".`
9070
+ );
9071
+ }
9072
+ const source = item;
9073
+ const candidate = { source };
9074
+ candidates.push(candidate);
9075
+ if (typeof source.providerId !== "string" || typeof source.modelId !== "string") {
9076
+ candidate.rejectionReason = "invalid-target";
9077
+ continue;
9078
+ }
9079
+ const normalized = {
9080
+ name: canonicalModelAliasName(source.name),
9081
+ providerId: source.providerId,
9082
+ modelId: source.modelId
9083
+ };
9084
+ if (!isModelAliasNameSyntax(normalized.name)) {
9085
+ candidate.rejectionReason = "invalid-name";
9086
+ continue;
9087
+ }
9088
+ if (isReservedModelAlias(normalized.name)) {
9089
+ candidate.rejectionReason = "reserved-name";
9090
+ continue;
9091
+ }
9092
+ if (!normalized.providerId.trim() || !normalized.modelId.trim()) {
9093
+ candidate.rejectionReason = "invalid-target";
9094
+ continue;
9095
+ }
9096
+ candidate.normalized = normalized;
9097
+ const group = groups.get(normalized.name) ?? [];
9098
+ group.push(candidate);
9099
+ groups.set(normalized.name, group);
9100
+ }
9101
+ for (const group of groups.values()) {
9102
+ const targets = new Set(
9103
+ group.map((candidate) => `${candidate.normalized.providerId}\0${candidate.normalized.modelId}`)
9104
+ );
9105
+ if (targets.size === 1) {
9106
+ group[0].accepted = true;
9107
+ group[0].sources = group.map((candidate) => candidate.source);
9108
+ } else {
9109
+ for (const candidate of group) candidate.rejectionReason = "conflicting-targets";
9110
+ }
9111
+ }
9112
+ const rejections = candidates.filter((candidate) => candidate.rejectionReason !== void 0).map((candidate) => ({
9113
+ alias: candidate.source,
9114
+ reason: candidate.rejectionReason
9115
+ }));
9116
+ const accepted = candidates.filter((candidate) => candidate.accepted === true && candidate.normalized !== void 0).map((candidate) => ({
9117
+ alias: candidate.normalized,
9118
+ source: candidate.source,
9119
+ sources: candidate.sources ?? [candidate.source]
9120
+ }));
9121
+ return {
9122
+ aliases: accepted.map((entry) => entry.alias),
9123
+ accepted,
9124
+ rejected: rejections.map((rejection) => rejection.alias),
9125
+ rejections
9126
+ };
8823
9127
  }
8824
9128
  function parseModelAliasAssignment(value) {
8825
9129
  const separator = value.indexOf("=");
8826
9130
  if (separator < 1 || separator === value.length - 1) {
8827
9131
  return { error: "Alias must use name=clodex:<provider-id>:<model-id>." };
8828
9132
  }
8829
- const name = value.slice(0, separator).trim();
8830
- if (!isValidModelAlias(name)) {
9133
+ const name = canonicalModelAliasName(value.slice(0, separator));
9134
+ if (!MODEL_ALIAS_PATTERN.test(name)) {
8831
9135
  return { error: "Alias names must be 1-64 letters, numbers, dots, underscores, or hyphens." };
8832
9136
  }
9137
+ if (isReservedModelAlias(name)) {
9138
+ return { error: "That alias name is reserved by the client." };
9139
+ }
8833
9140
  const rawTarget = value.slice(separator + 1).trim();
8834
9141
  const target = rawTarget.startsWith("clodex:") ? rawTarget.slice("clodex:".length) : rawTarget;
8835
9142
  const targetSeparator = target.indexOf(":");
8836
- if (targetSeparator < 1 || targetSeparator === target.length - 1) {
9143
+ const providerId = target.slice(0, targetSeparator).trim();
9144
+ const modelId = stripOneMContextSuffix(target.slice(targetSeparator + 1).trim());
9145
+ if (targetSeparator < 1 || targetSeparator === target.length - 1 || !providerId || !modelId) {
8837
9146
  return { error: "Alias target must use clodex:<provider-id>:<model-id>." };
8838
9147
  }
8839
9148
  return {
8840
9149
  name,
8841
- providerId: target.slice(0, targetSeparator),
9150
+ providerId,
8842
9151
  // `models --list` prints Claude's synthetic context suffix. It is a client
8843
9152
  // routing hint, not part of the provider catalog id stored in favorites.
8844
- modelId: stripOneMContextSuffix(target.slice(targetSeparator + 1))
9153
+ modelId
8845
9154
  };
8846
9155
  }
8847
9156
  function modelAliasTarget(alias) {
@@ -8888,11 +9197,32 @@ function makeRouteResolver(localProviders) {
8888
9197
  return provider && model ? localModelToRoute(provider, model) ?? void 0 : void 0;
8889
9198
  };
8890
9199
  }
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
- }));
9200
+ function resolveCatalogModelAliases(modelAliases, resolveRoute, catalogRoutes = []) {
9201
+ const normalized = normalizeModelAliases(modelAliases);
9202
+ const catalogRouteIds = new Set(
9203
+ catalogRoutes.map((route) => normalizeRouteLookupId(route.aliasId))
9204
+ );
9205
+ return [
9206
+ ...normalized.accepted.map(({ alias, source, sources }) => {
9207
+ const route = resolveRoute(alias.providerId, alias.modelId);
9208
+ const collidesWithCatalog = catalogRouteIds.has(
9209
+ normalizeRouteLookupId(alias.name)
9210
+ );
9211
+ const sourceNames = [...new Set(sources.map((entry) => entry.name))];
9212
+ return {
9213
+ name: alias.name,
9214
+ ...source.name === alias.name ? {} : { savedName: source.name },
9215
+ ...sourceNames.length === 1 && sourceNames[0] === alias.name ? {} : { sourceNames },
9216
+ routeId: route?.aliasId ?? modelAliasTarget(alias),
9217
+ ...collidesWithCatalog ? { unavailableReason: "conflicts with a catalog model id" } : route ? {} : { unavailableReason: "target unavailable" }
9218
+ };
9219
+ }),
9220
+ ...normalized.rejections.map((rejection) => ({
9221
+ name: canonicalModelAliasName(rejection.alias.name),
9222
+ ...rejection.alias.name === canonicalModelAliasName(rejection.alias.name) ? {} : { savedName: rejection.alias.name },
9223
+ unavailableReason: describeModelAliasRejection(rejection.reason)
9224
+ }))
9225
+ ];
8896
9226
  }
8897
9227
  function buildCatalogRoutes(startingRoute, favorites, resolveRoute, max = MAX_MODEL_CATALOG) {
8898
9228
  const droppedFavorites = [];
@@ -8916,7 +9246,7 @@ function httpProxyModelId(providerId, modelId) {
8916
9246
  function httpProxyDisplayName(model, providerName) {
8917
9247
  return `${formatModelLabel(model)} (${providerName})`;
8918
9248
  }
8919
- function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX_MODEL_CATALOG) {
9249
+ function buildHttpProxyRoutes(providers, favorites, modelAliases = void 0, max = MAX_MODEL_CATALOG) {
8920
9250
  const routes = [];
8921
9251
  const unavailable = [];
8922
9252
  const unsupported = [];
@@ -8954,16 +9284,21 @@ function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX
8954
9284
  routesByFavorite.set(`${favorite.providerId}:${favorite.modelId}`, proxyRoute);
8955
9285
  }
8956
9286
  const aliases = [];
8957
- const unavailableAliases = [];
8958
- const seenAliases = /* @__PURE__ */ new Set();
8959
- for (const alias of modelAliases) {
9287
+ const normalizedAliases = normalizeModelAliases(modelAliases);
9288
+ const unavailableAliases = [...normalizedAliases.rejected];
9289
+ for (const { alias, sources } of normalizedAliases.accepted) {
8960
9290
  const route = routesByFavorite.get(`${alias.providerId}:${alias.modelId}`);
8961
- if (!isValidModelAlias(alias.name) || seenAliases.has(alias.name) || !route) {
8962
- unavailableAliases.push(alias);
9291
+ if (!route) {
9292
+ unavailableAliases.push(...sources);
8963
9293
  continue;
8964
9294
  }
8965
- seenAliases.add(alias.name);
8966
- aliases.push({ name: alias.name, routeId: route.aliasId, displayName: route.displayName });
9295
+ const sourceNames = [...new Set(sources.map((source) => source.name))];
9296
+ aliases.push({
9297
+ name: alias.name,
9298
+ routeId: route.aliasId,
9299
+ displayName: route.displayName,
9300
+ ...sourceNames.length === 1 && sourceNames[0] === alias.name ? {} : { sourceNames }
9301
+ });
8967
9302
  }
8968
9303
  return { routes, unavailable, unsupported, aliases, unavailableAliases };
8969
9304
  }
@@ -9044,7 +9379,7 @@ function createGatewayModelCatalog(models, opts, modelAliases) {
9044
9379
  const canonicalId = httpProxyModelId(gatewayProviderId(model), model.id);
9045
9380
  if (!byId.has(canonicalId)) byId.set(canonicalId, model);
9046
9381
  }
9047
- for (const alias of modelAliases ?? []) {
9382
+ for (const alias of normalizeModelAliases(modelAliases).aliases) {
9048
9383
  if (byId.has(alias.name)) continue;
9049
9384
  const target = models.find(
9050
9385
  (model) => gatewayProviderId(model) === alias.providerId && model.id === alias.modelId
@@ -9092,8 +9427,9 @@ function formatOpenAIModels(models) {
9092
9427
  }
9093
9428
 
9094
9429
  // 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.`;
9430
+ function routeUnavailableMessage(modelId, reason) {
9431
+ const detail = reason ? `: ${reason}` : "";
9432
+ return `Clodex model route '${modelId}' is unavailable${detail}. Run \`clodex models --list\` to inspect saved routes and aliases.`;
9097
9433
  }
9098
9434
 
9099
9435
  // src/upstream-forward.ts
@@ -10256,6 +10592,17 @@ function aliasModelId(realId, providerId) {
10256
10592
  function lookupRoute(byAlias, id) {
10257
10593
  return byAlias.get(normalizeRouteLookupId(id));
10258
10594
  }
10595
+ function configuredAliasLookupNames(alias) {
10596
+ const sourceNames = [
10597
+ alias.name,
10598
+ ...alias.savedName === void 0 ? [] : [alias.savedName],
10599
+ ...alias.sourceNames ?? []
10600
+ ];
10601
+ return [...new Set(sourceNames.flatMap((name) => {
10602
+ const trimmed = name.trim();
10603
+ return trimmed === name ? [name] : [name, trimmed];
10604
+ }))].map(normalizeRouteLookupId);
10605
+ }
10259
10606
  async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
10260
10607
  const proxyToken = randomUUID4();
10261
10608
  silenceSdkWarnings();
@@ -10264,9 +10611,13 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
10264
10611
  }
10265
10612
  const byAlias = new Map(routes.map((r) => [normalizeRouteLookupId(r.aliasId), r]));
10266
10613
  const configuredAliasNames = new Set(
10267
- (modelAliases ?? []).map((alias) => normalizeRouteLookupId(alias.name))
10614
+ (modelAliases ?? []).flatMap(configuredAliasLookupNames)
10615
+ );
10616
+ const unavailableAliasReasons = new Map(
10617
+ (modelAliases ?? []).filter((alias) => alias.unavailableReason !== void 0).flatMap((alias) => configuredAliasLookupNames(alias).map((name) => [name, alias.unavailableReason]))
10268
10618
  );
10269
10619
  for (const alias of modelAliases ?? []) {
10620
+ if (alias.routeId === void 0 || alias.unavailableReason !== void 0) continue;
10270
10621
  const route = lookupRoute(byAlias, alias.routeId);
10271
10622
  const aliasId = normalizeRouteLookupId(alias.name);
10272
10623
  if (route && !byAlias.has(aliasId)) byAlias.set(aliasId, route);
@@ -10341,7 +10692,14 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
10341
10692
  const resolvedRoute = typeof originalModel === "string" ? lookupRoute(byAlias, originalModel) : void 0;
10342
10693
  const configuredModelUnavailable = typeof originalModel === "string" && (normalizeRouteLookupId(originalModel).startsWith("clodex:") || configuredAliasNames.has(normalizeRouteLookupId(originalModel)));
10343
10694
  if (!resolvedRoute && configuredModelUnavailable) {
10344
- anthropicError(res, 400, routeUnavailableMessage(originalModel));
10695
+ anthropicError(
10696
+ res,
10697
+ 400,
10698
+ routeUnavailableMessage(
10699
+ originalModel,
10700
+ unavailableAliasReasons.get(normalizeRouteLookupId(originalModel))
10701
+ )
10702
+ );
10345
10703
  return;
10346
10704
  }
10347
10705
  const route = resolvedRoute ?? defaultRoute;
@@ -12382,6 +12740,10 @@ async function startHttpProxy(options) {
12382
12740
  for (const alias of options.modelAliases ?? []) {
12383
12741
  const aliasId = normalizeRouteLookupId(alias.name);
12384
12742
  reservedModelIds.add(aliasId);
12743
+ for (const sourceName of alias.sourceNames ?? []) {
12744
+ reservedModelIds.add(normalizeRouteLookupId(sourceName));
12745
+ reservedModelIds.add(normalizeRouteLookupId(sourceName.trim()));
12746
+ }
12385
12747
  const route = routesById.get(normalizeRouteLookupId(alias.routeId));
12386
12748
  if (!route) continue;
12387
12749
  routesById.set(aliasId, route);
@@ -12632,13 +12994,17 @@ async function startHttpProxy(options) {
12632
12994
  async function loadHttpProxyRoutes() {
12633
12995
  const prefs = loadPreferences();
12634
12996
  const favorites = prefs.favoriteModels ?? [];
12997
+ const normalizedAliases = normalizeModelAliases(prefs.modelAliases);
12635
12998
  if (favorites.length === 0) {
12636
12999
  return {
12637
13000
  routes: [],
12638
13001
  unavailable: [],
12639
13002
  unsupported: [],
12640
13003
  aliases: [],
12641
- unavailableAliases: prefs.modelAliases ?? [],
13004
+ unavailableAliases: [
13005
+ ...normalizedAliases.rejected,
13006
+ ...normalizedAliases.accepted.flatMap(({ sources }) => sources)
13007
+ ],
12642
13008
  favoriteCount: 0
12643
13009
  };
12644
13010
  }
@@ -12648,7 +13014,7 @@ async function loadHttpProxyRoutes() {
12648
13014
  apiKey: await resolveLocalProviderApiKey(provider) ?? ""
12649
13015
  })));
12650
13016
  return {
12651
- ...buildHttpProxyRoutes(catalog, favorites, prefs.modelAliases ?? []),
13017
+ ...buildHttpProxyRoutes(catalog, favorites, prefs.modelAliases),
12652
13018
  favoriteCount: favorites.length
12653
13019
  };
12654
13020
  }
@@ -12682,8 +13048,16 @@ function reportSkippedHttpProxyFavorites(loaded) {
12682
13048
  );
12683
13049
  }
12684
13050
  if (loaded.unavailableAliases.length > 0) {
13051
+ const normalizedAliases = normalizeModelAliases(loaded.unavailableAliases);
13052
+ const reasonByAlias = new Map(
13053
+ normalizedAliases.rejections.map((rejection) => [
13054
+ rejection.alias,
13055
+ describeModelAliasRejection(rejection.reason)
13056
+ ])
13057
+ );
12685
13058
  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.`
13059
+ `${loaded.unavailableAliases.length} model alias${loaded.unavailableAliases.length === 1 ? "" : "es"} skipped. Saved entries were preserved.
13060
+ ` + loaded.unavailableAliases.map((alias) => ` ${JSON.stringify(alias.name)} \u2014 ${reasonByAlias.get(alias) ?? "target unavailable"}`).join("\n")
12687
13061
  );
12688
13062
  }
12689
13063
  }
@@ -12693,7 +13067,14 @@ function buildConfiguredHttpProxyOptions(loaded, port, debug = false, inferenceL
12693
13067
  port,
12694
13068
  routes: loaded.routes,
12695
13069
  modelAliases: loaded.aliases,
12696
- reservedModelIds: loaded.unavailableAliases.map((alias) => alias.name),
13070
+ reservedModelIds: [...new Set([
13071
+ ...loaded.aliases.flatMap((alias) => alias.sourceNames ?? []),
13072
+ ...loaded.unavailableAliases.map((alias) => alias.name)
13073
+ ].flatMap((name) => {
13074
+ const trimmedName = name.trim();
13075
+ const canonicalName = canonicalModelAliasName(name);
13076
+ return [name, trimmedName, canonicalName].filter(Boolean);
13077
+ }))],
12697
13078
  debug,
12698
13079
  debugLogPath,
12699
13080
  inferenceLogPath,
@@ -13104,7 +13485,28 @@ async function runServerCommand(options = {}) {
13104
13485
  return 1;
13105
13486
  }
13106
13487
  const gateway = runConfig.maskGatewayIds ? { maskGatewayIds: true } : void 0;
13107
- const modelAliases = loadPreferences().modelAliases ?? [];
13488
+ const normalizedAliases = normalizeModelAliases(loadPreferences().modelAliases);
13489
+ const baseCatalog = createGatewayModelCatalog(models, gateway);
13490
+ const unavailableAliases = normalizedAliases.accepted.filter(({ alias }) => !models.some((model) => gatewayProviderId(model) === alias.providerId && model.id === alias.modelId));
13491
+ const collidingAliases = normalizedAliases.accepted.filter(({ alias }) => baseCatalog.get(alias.name) !== void 0);
13492
+ const collidingAliasNames = new Set(collidingAliases.map(({ alias }) => alias.name));
13493
+ const targetUnavailableAliases = unavailableAliases.filter(({ alias }) => !collidingAliasNames.has(alias.name));
13494
+ const inactiveAliasNames = /* @__PURE__ */ new Set([
13495
+ ...targetUnavailableAliases.map(({ alias }) => alias.name),
13496
+ ...collidingAliasNames
13497
+ ]);
13498
+ const modelAliases = normalizedAliases.aliases.filter((alias) => !inactiveAliasNames.has(alias.name));
13499
+ const aliasWarnings = [
13500
+ ...normalizedAliases.rejections.map((rejection) => `${JSON.stringify(rejection.alias.name)} \u2014 ${describeModelAliasRejection(rejection.reason)}`),
13501
+ ...targetUnavailableAliases.flatMap(({ sources }) => sources.map((source) => `${JSON.stringify(source.name)} \u2014 target unavailable`)),
13502
+ ...collidingAliases.flatMap(({ sources }) => sources.map((source) => `${JSON.stringify(source.name)} \u2014 conflicts with a catalog model id`))
13503
+ ];
13504
+ if (aliasWarnings.length > 0) {
13505
+ p9.log.warn(
13506
+ `${aliasWarnings.length} saved model alias${aliasWarnings.length === 1 ? "" : "es"} inactive. Saved entries were preserved.
13507
+ ${aliasWarnings.join("\n ")}`
13508
+ );
13509
+ }
13108
13510
  const inferenceLogPath = getInferenceRequestLogPath();
13109
13511
  const webSocketDiagnosticsLogPath = options.wsDiagnostics ? getSessionLogPath("server-websocket-diagnostics", "jsonl") : void 0;
13110
13512
  const server = await startServer({
@@ -13416,25 +13818,192 @@ function planLaunchWizard(opts) {
13416
13818
  }
13417
13819
 
13418
13820
  // src/patcher.ts
13419
- import { createHash as createHash7 } from "crypto";
13821
+ import { createHash as createHash8 } from "crypto";
13420
13822
  import {
13421
13823
  copyFileSync,
13422
- existsSync as existsSync6,
13824
+ existsSync as existsSync7,
13825
+ mkdtempSync,
13423
13826
  mkdirSync as mkdirSync7,
13424
- readFileSync as readFileSync8,
13425
- statSync as statSync3,
13827
+ readFileSync as readFileSync9,
13828
+ renameSync as renameSync3,
13829
+ rmSync,
13830
+ statSync as statSync4,
13426
13831
  unlinkSync as unlinkSync4,
13427
13832
  writeFileSync as writeFileSync7,
13428
13833
  openSync as openSync4,
13429
13834
  closeSync as closeSync4,
13430
13835
  realpathSync
13431
13836
  } from "fs";
13432
- import { homedir as homedir2 } from "os";
13433
- import { basename, join as join7 } from "path";
13837
+ import { homedir as homedir3 } from "os";
13838
+ import { basename, dirname as dirname5, join as join8 } from "path";
13434
13839
  import pc12 from "picocolors";
13435
13840
  import * as p11 from "@clack/prompts";
13436
13841
 
13842
+ // src/patch-backup.ts
13843
+ import { createHash as createHash7 } from "crypto";
13844
+ import { existsSync as existsSync6, readFileSync as readFileSync8, readdirSync, statSync as statSync3 } from "fs";
13845
+ import { homedir as homedir2 } from "os";
13846
+ import { join as join7 } from "path";
13847
+ var BACKUP_SHA_PREFIX_LENGTH = 16;
13848
+ function backupDir() {
13849
+ return process.env["TWEAKCC_CONFIG_DIR"]?.trim() || join7(homedir2(), ".tweakcc");
13850
+ }
13851
+ function sha256File(path) {
13852
+ return createHash7("sha256").update(readFileSync8(path)).digest("hex");
13853
+ }
13854
+ function backupVersionTag(version) {
13855
+ const tag = version.trim().replace(/[^\w.-]+/g, "_");
13856
+ if (!tag) throw new Error("clodex patch: refusing to name a pristine backup for an empty claude version");
13857
+ return tag;
13858
+ }
13859
+ function contentAddressedBackupPath(version, sha256, dir = backupDir()) {
13860
+ return join7(dir, `claude-${backupVersionTag(version)}-${sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH)}.orig`);
13861
+ }
13862
+ function tweakccMirrorBackupPath(dir = backupDir()) {
13863
+ return join7(dir, "native-binary.backup");
13864
+ }
13865
+ function scanPristineBackups(version, dir = backupDir()) {
13866
+ const tag = backupVersionTag(version);
13867
+ const pattern = new RegExp(`^claude-${tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:-([0-9a-f]{${BACKUP_SHA_PREFIX_LENGTH}}))?\\.orig$`);
13868
+ let entries;
13869
+ try {
13870
+ entries = readdirSync(dir);
13871
+ } catch {
13872
+ return { valid: [], corrupt: [] };
13873
+ }
13874
+ const valid = [];
13875
+ const corrupt = [];
13876
+ for (const entry of entries.sort()) {
13877
+ const match = pattern.exec(entry);
13878
+ if (!match) continue;
13879
+ const path = join7(dir, entry);
13880
+ let sha256;
13881
+ try {
13882
+ if (!statSync3(path).isFile()) continue;
13883
+ sha256 = sha256File(path);
13884
+ } catch {
13885
+ corrupt.push(path);
13886
+ continue;
13887
+ }
13888
+ const embedded = match[1];
13889
+ if (embedded) {
13890
+ if (sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH) !== embedded) {
13891
+ corrupt.push(path);
13892
+ continue;
13893
+ }
13894
+ valid.push({ path, kind: "content-addressed", sha256 });
13895
+ } else {
13896
+ valid.push({ path, kind: "legacy", sha256 });
13897
+ }
13898
+ }
13899
+ return { valid, corrupt };
13900
+ }
13901
+ var CLODEX_PATCH_COMMENT_PREFIX = "/*ccpatch:";
13902
+ var LEGACY_CLODEX_PATCH_MARKERS = [
13903
+ "Additional custom models: ",
13904
+ // PATCH 4 — Agent tool model description
13905
+ "function(_i){return _i.value===_o.value}",
13906
+ // PATCH 5 — picker dedupe guard
13907
+ '"clodex:'
13908
+ // PATCH 1/3 — canonical ids as model identities
13909
+ ];
13910
+ function isPatchedClaudeSource(source) {
13911
+ return source.includes(CLODEX_PATCH_COMMENT_PREFIX);
13912
+ }
13913
+ function looksLikeLegacyClodexPatch(source) {
13914
+ return !isPatchedClaudeSource(source) && LEGACY_CLODEX_PATCH_MARKERS.some((marker) => source.includes(marker));
13915
+ }
13916
+ function noBackupMessage(facts) {
13917
+ const corrupt = facts.corruptBackups?.length ? ` (${facts.corruptBackups.length} backup file(s) for this version failed integrity checks and were ignored)` : "";
13918
+ 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\`.`;
13919
+ }
13920
+ function selectRestoreSource(facts) {
13921
+ const notes = [];
13922
+ const manifest = facts.manifest && facts.manifest.binaryPath === facts.binaryPath ? facts.manifest : null;
13923
+ let chosen = manifest?.pristineSha256 ? facts.backups.find((backup) => backup.sha256 === manifest.pristineSha256) : void 0;
13924
+ if (!chosen && manifest?.backupPath) {
13925
+ chosen = facts.backups.find((backup) => backup.path === manifest.backupPath);
13926
+ if (!chosen) {
13927
+ notes.push(`Recorded pristine backup ${manifest.backupPath} is missing, corrupt, or belongs to another claude version \u2014 ignoring it.`);
13928
+ }
13929
+ }
13930
+ if (!chosen) {
13931
+ const distinct = [...new Set(facts.backups.map((backup) => backup.sha256))];
13932
+ if (distinct.length > 1) {
13933
+ return {
13934
+ action: "error",
13935
+ 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\`.`
13936
+ };
13937
+ }
13938
+ chosen = facts.backups.find((backup) => backup.kind === "content-addressed") ?? facts.backups[0];
13939
+ }
13940
+ if (!chosen) return { action: "error", message: noBackupMessage(facts) };
13941
+ return {
13942
+ action: "restore",
13943
+ backupPath: chosen.path,
13944
+ pristineSha256: chosen.sha256,
13945
+ // A legacy name carries no hash, so its bytes could be anything — including
13946
+ // another version's binary, stored under a mislabeled name by an older
13947
+ // clodex. Executing it is the only evidence available; require it.
13948
+ probeVersion: chosen.kind === "legacy",
13949
+ notes
13950
+ };
13951
+ }
13952
+ function planPristineSource(facts) {
13953
+ const identical = facts.backups.find((backup) => backup.sha256 === facts.liveSha256 && backup.kind === "content-addressed") ?? facts.backups.find((backup) => backup.sha256 === facts.liveSha256);
13954
+ if (identical) {
13955
+ return { action: "reuse", backupPath: identical.path, pristineSha256: identical.sha256, notes: [] };
13956
+ }
13957
+ const manifest = facts.manifest;
13958
+ if (manifest && manifest.binaryPath === facts.binaryPath && manifest.patchedSha256 === facts.liveSha256) {
13959
+ return selectRestoreSource(facts);
13960
+ }
13961
+ return { action: "inspect" };
13962
+ }
13963
+ function planInspectedPristineSource(facts, inspection) {
13964
+ if (inspection.patched) return selectRestoreSource(facts);
13965
+ const notes = [];
13966
+ const conflicting = facts.backups.filter((backup) => backup.sha256 !== facts.liveSha256);
13967
+ if (conflicting.length) {
13968
+ notes.push(
13969
+ `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.`
13970
+ );
13971
+ }
13972
+ return {
13973
+ action: "snapshot",
13974
+ backupPath: contentAddressedBackupPath(facts.version, facts.liveSha256),
13975
+ pristineSha256: facts.liveSha256,
13976
+ notes
13977
+ };
13978
+ }
13979
+ function planRestoreOnly(facts) {
13980
+ return selectRestoreSource(facts);
13981
+ }
13982
+ function collectPristineFacts(args) {
13983
+ const dir = args.dir ?? backupDir();
13984
+ const scan = scanPristineBackups(args.version, dir);
13985
+ return {
13986
+ version: args.version,
13987
+ binaryPath: args.binaryPath,
13988
+ liveSha256: existsSync6(args.binaryPath) ? sha256File(args.binaryPath) : "",
13989
+ manifest: args.manifest,
13990
+ backups: scan.valid,
13991
+ corruptBackups: scan.corrupt
13992
+ };
13993
+ }
13994
+
13437
13995
  // src/patch-transforms.ts
13996
+ var PATCH_TRANSFORMS_VERSION = 2;
13997
+ var NATIVE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
13998
+ var BASE_EFFORT_LEVELS = ["low", "medium", "high"];
13999
+ function projectNativeEffort(effort) {
14000
+ if (!effort || !Array.isArray(effort.levels) || typeof effort.defaultLevel !== "string") return void 0;
14001
+ const declared = new Set(effort.levels);
14002
+ const levels = NATIVE_EFFORT_LEVELS.filter((level) => declared.has(level));
14003
+ if (!BASE_EFFORT_LEVELS.every((level) => declared.has(level))) return void 0;
14004
+ if (!levels.some((level) => level === effort.defaultLevel)) return void 0;
14005
+ return { levels, defaultLevel: "high" };
14006
+ }
13438
14007
  var PatchApplyError = class extends Error {
13439
14008
  results;
13440
14009
  constructor(message, results) {
@@ -13449,21 +14018,37 @@ function formatPatchSiteLine(result) {
13449
14018
  function applyClodexPatches(source, config) {
13450
14019
  let js = source;
13451
14020
  const MODEL_CONFIG = config;
13452
- const ALIAS_TO_ID = {};
14021
+ const ALIAS_TO_ID = /* @__PURE__ */ Object.create(null);
13453
14022
  const IDENTITIES = [];
13454
- const DISPLAY_BY_IDENTITY = {};
13455
- const CONTEXT_BY_KEY = {};
14023
+ const DISPLAY_BY_IDENTITY = /* @__PURE__ */ Object.create(null);
14024
+ const CONTEXT_BY_KEY = /* @__PURE__ */ Object.create(null);
14025
+ const CONFIGURED_CAPABILITY_KEYS = /* @__PURE__ */ new Set();
14026
+ const EFFORT_BY_KEY = /* @__PURE__ */ Object.create(null);
13456
14027
  const report = [];
13457
14028
  const fail = (message) => {
13458
14029
  throw new PatchApplyError(message, report);
13459
14030
  };
14031
+ const capabilityKeys = (value) => {
14032
+ const normalized = value.trim().toLowerCase();
14033
+ const bare = normalized.replace(/\[1m\]$/i, "");
14034
+ return [.../* @__PURE__ */ new Set([bare, `${bare}[1m]`])];
14035
+ };
14036
+ const registerCapabilityKeys = (value) => {
14037
+ for (const key of capabilityKeys(value)) {
14038
+ CONFIGURED_CAPABILITY_KEYS.add(key);
14039
+ }
14040
+ };
13460
14041
  for (const [id, value] of Object.entries(MODEL_CONFIG)) {
13461
14042
  const spec = value && typeof value === "object" ? value : { alias: value };
13462
14043
  if (spec.alias !== void 0) {
13463
- const a = String(spec.alias).trim().toLowerCase();
14044
+ const rawAlias = String(spec.alias).trim();
14045
+ const a = rawAlias.toLowerCase();
13464
14046
  if (!/^[a-z0-9][a-z0-9._-]*(\[1m\])?$/.test(a)) {
13465
14047
  fail('clodex patch: alias "' + spec.alias + '" is not a safe lowercase alias');
13466
14048
  }
14049
+ if (isReservedModelAlias(a)) {
14050
+ fail('clodex patch: reserved alias "' + a + '" cannot be reassigned');
14051
+ }
13467
14052
  ALIAS_TO_ID[a] = String(id);
13468
14053
  IDENTITIES.push(a);
13469
14054
  if (spec.display) DISPLAY_BY_IDENTITY[a] = String(spec.display);
@@ -13471,6 +14056,10 @@ function applyClodexPatches(source, config) {
13471
14056
  IDENTITIES.push(String(id));
13472
14057
  if (spec.display) DISPLAY_BY_IDENTITY[String(id)] = String(spec.display);
13473
14058
  }
14059
+ if (spec.alias !== void 0) {
14060
+ registerCapabilityKeys(String(spec.alias));
14061
+ }
14062
+ registerCapabilityKeys(String(id));
13474
14063
  if (spec.context !== void 0) {
13475
14064
  const n = Number(spec.context);
13476
14065
  if (!Number.isInteger(n) || n <= 0) {
@@ -13484,6 +14073,22 @@ function applyClodexPatches(source, config) {
13484
14073
  if (spec.alias !== void 0) CONTEXT_BY_KEY[String(spec.alias).trim().toLowerCase()] = n;
13485
14074
  CONTEXT_BY_KEY[String(id).trim().toLowerCase()] = n;
13486
14075
  }
14076
+ if (spec.effort) {
14077
+ const effort = projectNativeEffort(spec.effort);
14078
+ if (!effort) {
14079
+ fail(
14080
+ `clodex patch: effort for "${id}" must include low, medium, and high with a native default`
14081
+ );
14082
+ }
14083
+ if (spec.alias !== void 0) {
14084
+ for (const key of capabilityKeys(String(spec.alias))) {
14085
+ EFFORT_BY_KEY[key] = effort;
14086
+ }
14087
+ }
14088
+ for (const key of capabilityKeys(String(id))) {
14089
+ EFFORT_BY_KEY[key] = effort;
14090
+ }
14091
+ }
13487
14092
  }
13488
14093
  const ALIASES = Object.keys(ALIAS_TO_ID);
13489
14094
  const MODELS = Object.keys(MODEL_CONFIG);
@@ -13611,19 +14216,91 @@ function applyClodexPatches(source, config) {
13611
14216
  );
13612
14217
  }
13613
14218
  }
14219
+ function patchEffortCapability(capability, marker, name, anchor) {
14220
+ const verdicts = Object.fromEntries(
14221
+ [...CONFIGURED_CAPABILITY_KEYS].map((key) => {
14222
+ const effort = EFFORT_BY_KEY[key];
14223
+ return [
14224
+ key,
14225
+ effort !== void 0 && (capability === "effort" || effort.levels.includes(capability === "xhigh_effort" ? "xhigh" : "max"))
14226
+ ];
14227
+ })
14228
+ );
14229
+ const hasMarker = js.includes(marker);
14230
+ if (Object.keys(verdicts).length === 0 && !hasMarker) return;
14231
+ const snippet = (arg) => marker + "var _ccv=Object.assign(Object.create(null)," + JSON.stringify(verdicts) + ")[String(" + arg + '||"").trim().toLowerCase()];if(_ccv!==void 0)return _ccv;';
14232
+ if (hasMarker) {
14233
+ const markerPattern = reEsc(marker);
14234
+ applyOnce(
14235
+ name + " (refresh)",
14236
+ new RegExp(
14237
+ markerPattern + 'var _ccv=Object\\.assign\\(Object\\.create\\(null\\),\\{[^{}]*\\}\\)\\[String\\(([\\w$]+)\\|\\|""\\)\\.trim\\(\\)\\.toLowerCase\\(\\)\\];if\\(_ccv!==void 0\\)return _ccv;'
14238
+ ),
14239
+ (_m, arg) => snippet(arg),
14240
+ { required: false, noopIsSkip: true }
14241
+ );
14242
+ return;
14243
+ }
14244
+ applyOnce(
14245
+ name,
14246
+ anchor,
14247
+ (_m, head, arg, body) => head + snippet(arg) + body,
14248
+ { required: false }
14249
+ );
14250
+ }
14251
+ patchEffortCapability(
14252
+ "effort",
14253
+ "/*ccpatch:effort*/",
14254
+ "PATCH 8a: effort capability",
14255
+ /(function [\w$]+\(([\w$]+)\)\{if\([\w$]+\(\2\)\)return!1;)(let [\w$]+=[\w$]+\(\2,"effort"\);)/
14256
+ );
14257
+ patchEffortCapability(
14258
+ "xhigh_effort",
14259
+ "/*ccpatch:xhigh-effort*/",
14260
+ "PATCH 8b: xhigh effort capability",
14261
+ /(function [\w$]+\(([\w$]+)\)\{if\([\w$]+\(\2\)\)return!1;)(let [\w$]+=[\w$]+\(\2,"xhigh_effort"\);)/
14262
+ );
14263
+ patchEffortCapability(
14264
+ "max_effort",
14265
+ "/*ccpatch:max-effort*/",
14266
+ "PATCH 8c: max effort capability",
14267
+ /(function [\w$]+\(([\w$]+)\)\{if\([\w$]+\(\2\)\)return!1;)(let [\w$]+=[\w$]+\(\2,"max_effort"\);)/
14268
+ );
14269
+ const DEFAULT_EFFORT_MARKER = "/*ccpatch:default-effort*/";
14270
+ const defaults = Object.fromEntries(
14271
+ Object.entries(EFFORT_BY_KEY).map(([key, effort]) => [key, effort.defaultLevel])
14272
+ );
14273
+ if (Object.keys(defaults).length || js.includes(DEFAULT_EFFORT_MARKER)) {
14274
+ 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;';
14275
+ if (js.includes(DEFAULT_EFFORT_MARKER)) {
14276
+ applyOnce(
14277
+ "PATCH 9: default effort (refresh)",
14278
+ /\/\*ccpatch:default-effort\*\/var _cce=Object\.assign\(Object\.create\(null\),\{[^{}]*\}\)\[String\(([\w$]+)\|\|""\)\.trim\(\)\.toLowerCase\(\)\];if\(_cce!==void 0\)return _cce;/,
14279
+ (_m, arg) => snippet(arg),
14280
+ { required: false, noopIsSkip: true }
14281
+ );
14282
+ } else {
14283
+ applyOnce(
14284
+ "PATCH 9: default effort",
14285
+ /(function [\w$]+\(([\w$]+)\)\{)(return [\w$]+\([\w$]+\(\2\)\)\?\.default_effort\?\?"high"\})/,
14286
+ (_m, head, arg, body) => head + snippet(arg) + body,
14287
+ { required: false }
14288
+ );
14289
+ }
14290
+ }
13614
14291
  return { content: js, results: report };
13615
14292
  }
13616
14293
 
13617
14294
  // src/patcher.ts
13618
14295
  function getPatchManifestPath() {
13619
- return join7(getAppHome(), "patch-state.json");
14296
+ return join8(getAppHome(), "patch-state.json");
13620
14297
  }
13621
14298
  function getPatchLockPath() {
13622
- return join7(getAppHome(), "patch.lock");
14299
+ return join8(getAppHome(), "patch.lock");
13623
14300
  }
13624
14301
  function readPatchManifest(path = getPatchManifestPath()) {
13625
14302
  try {
13626
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
14303
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
13627
14304
  if (parsed && typeof parsed.binaryPath === "string" && typeof parsed.configHash === "string") {
13628
14305
  return parsed;
13629
14306
  }
@@ -13639,7 +14316,20 @@ function writePatchManifest(manifest, path = getPatchManifestPath()) {
13639
14316
  function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
13640
14317
  const config = {};
13641
14318
  const unknownWindows = [];
13642
- const aliasByFavorite = new Map(aliases.map((a) => [`${a.providerId}:${a.modelId}`, a.name]));
14319
+ const normalizedAliases = normalizeModelAliases(aliases);
14320
+ const favoriteTargets = new Set(
14321
+ favorites.map((favorite) => `${favorite.providerId}:${favorite.modelId}`)
14322
+ );
14323
+ const targetRejections = normalizedAliases.accepted.filter(({ alias }) => !favoriteTargets.has(`${alias.providerId}:${alias.modelId}`)).flatMap(({ sources }) => sources.map((source) => ({
14324
+ alias: source,
14325
+ reason: "target-not-favorite"
14326
+ })));
14327
+ const aliasByFavorite = new Map(
14328
+ normalizedAliases.aliases.filter((alias) => favoriteTargets.has(`${alias.providerId}:${alias.modelId}`)).map((alias) => [
14329
+ `${alias.providerId}:${alias.modelId}`,
14330
+ alias.name
14331
+ ])
14332
+ );
13643
14333
  for (const favorite of favorites) {
13644
14334
  const id = stripOneMContextSuffix(httpProxyModelId(favorite.providerId, favorite.modelId));
13645
14335
  if (config[id]) continue;
@@ -13652,29 +14342,68 @@ function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
13652
14342
  else if (context !== 2e5) entry.context = context;
13653
14343
  const display = meta?.displayName?.trim();
13654
14344
  if (display) entry.display = display;
14345
+ const effort = projectNativeEffort(meta?.effort);
14346
+ if (effort) entry.effort = effort;
13655
14347
  config[id] = entry;
13656
14348
  }
13657
- return { config, unknownWindows };
14349
+ return {
14350
+ config,
14351
+ unknownWindows,
14352
+ rejectedAliases: [
14353
+ ...normalizedAliases.rejected,
14354
+ ...targetRejections.map((rejection) => rejection.alias)
14355
+ ],
14356
+ rejectedAliasRejections: [
14357
+ ...normalizedAliases.rejections,
14358
+ ...targetRejections
14359
+ ]
14360
+ };
13658
14361
  }
13659
- function computePatchConfigHash(config) {
14362
+ function reportRejectedModelAliases(rejections) {
14363
+ for (const rejection of rejections) {
14364
+ p11.log.warn(
14365
+ `Saved model alias ${JSON.stringify(rejection.alias.name)} was not patched \u2014 ${describeModelAliasRejection(rejection.reason)}. The saved entry was preserved.`
14366
+ );
14367
+ }
14368
+ }
14369
+ function computePatchConfigHash(config, transformsVersion = PATCH_TRANSFORMS_VERSION) {
13660
14370
  const canonical = Object.keys(config).sort().map((key) => {
13661
14371
  const entry = config[key];
13662
- return [key, entry.alias ?? null, entry.context ?? null, entry.display ?? null];
14372
+ return [
14373
+ key,
14374
+ entry.alias ?? null,
14375
+ entry.context ?? null,
14376
+ entry.display ?? null,
14377
+ entry.effort?.levels ?? null,
14378
+ entry.effort?.defaultLevel ?? null
14379
+ ];
13663
14380
  });
13664
- return createHash7("sha256").update(JSON.stringify(canonical)).digest("hex");
14381
+ return createHash8("sha256").update(JSON.stringify([transformsVersion, canonical])).digest("hex");
13665
14382
  }
13666
14383
  function buildDesiredPatchConfig() {
13667
14384
  const prefs = loadPreferences();
13668
14385
  const favorites = prefs.favoriteModels ?? [];
13669
- const aliases = prefs.modelAliases ?? [];
14386
+ const aliases = prefs.modelAliases;
13670
14387
  const registry = loadRegistry();
13671
14388
  const meta = /* @__PURE__ */ new Map();
13672
14389
  for (const provider of registry.providers) {
13673
14390
  for (const model of provider.modelsCache?.models ?? []) {
14391
+ const npm = model.npm ?? provider.api.npm ?? "";
14392
+ const upstreamModelId2 = model.upstreamModelId ?? model.id;
14393
+ const modelsDev = findModelsDevModel(provider.id, model.id);
14394
+ const effort = getPatchReasoningCapabilities(npm, upstreamModelId2, {
14395
+ providerId: provider.id,
14396
+ apiBaseUrl: model.apiUrl ?? provider.api.url,
14397
+ supportedParameters: model.supportedParameters,
14398
+ reasoning: model.reasoning ?? modelsDev?.reasoning,
14399
+ interleavedReasoningField: model.interleavedReasoningField ?? modelsDev?.interleaved?.field,
14400
+ upstreamModelId: upstreamModelId2
14401
+ });
13674
14402
  meta.set(`${provider.id}:${model.id}`, {
13675
14403
  contextWindow: model.contextWindow && model.contextWindow > 0 ? model.contextWindow : void 0,
13676
14404
  // Same label `clodex server` prints at startup and `models --list` shows.
13677
- displayName: httpProxyDisplayName(model, provider.name)
14405
+ displayName: httpProxyDisplayName(model, provider.name),
14406
+ effort: effort.mode === "controllable" ? { levels: effort.levels, defaultLevel: effort.defaultLevel } : void 0
13678
14407
  });
13679
14408
  }
13680
14409
  }
@@ -13704,7 +14433,7 @@ function pidIsAlive(pid) {
13704
14433
  function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13705
14434
  const now = opts.now ?? Date.now();
13706
14435
  const isAlive = opts.isAlive ?? pidIsAlive;
13707
- mkdirSync7(join7(lockPath, ".."), { recursive: true, mode: 448 });
14436
+ mkdirSync7(join8(lockPath, ".."), { recursive: true, mode: 448 });
13708
14437
  for (let attempt = 0; attempt < 2; attempt++) {
13709
14438
  try {
13710
14439
  const fd = openSync4(lockPath, "wx");
@@ -13720,7 +14449,7 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13720
14449
  } catch {
13721
14450
  let stale = false;
13722
14451
  try {
13723
- const existing = JSON.parse(readFileSync8(lockPath, "utf8"));
14452
+ const existing = JSON.parse(readFileSync9(lockPath, "utf8"));
13724
14453
  stale = !existing.pid || !isAlive(existing.pid) || typeof existing.startedAt === "number" && now - existing.startedAt > PATCH_LOCK_STALE_MS;
13725
14454
  } catch {
13726
14455
  stale = true;
@@ -13734,33 +14463,28 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13734
14463
  }
13735
14464
  return null;
13736
14465
  }
13737
- function sha256File(path) {
13738
- return createHash7("sha256").update(readFileSync8(path)).digest("hex");
13739
- }
13740
14466
  function resolveClaudeBinaryForPatch() {
13741
14467
  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;
14468
+ const nativeSymlink = join8(homedir3(), ".local", "bin", "claude");
14469
+ const source = envOverride?.trim() || (existsSync7(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
14470
+ if (!source) return { ok: false, reason: "binary-not-found" };
13745
14471
  let resolved;
13746
14472
  try {
13747
14473
  resolved = realpathSync(source);
13748
14474
  } catch {
13749
- return null;
14475
+ return { ok: false, reason: "binary-not-found" };
13750
14476
  }
13751
14477
  try {
13752
- if (!statSync3(resolved).isFile()) return null;
14478
+ if (!statSync4(resolved).isFile()) return { ok: false, reason: "binary-not-found" };
13753
14479
  } catch {
13754
- return null;
14480
+ return { ok: false, reason: "binary-not-found" };
13755
14481
  }
13756
- return { binaryPath: resolved, version: getInstalledClaudeVersion() };
14482
+ const version = getClaudeVersionForBinary(resolved);
14483
+ if (!version) return { ok: false, reason: "version-unknown", binaryPath: resolved };
14484
+ return { ok: true, binaryPath: resolved, version };
13757
14485
  }
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`);
14486
+ function describePatchTargetFailure(target) {
14487
+ 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
14488
  }
13765
14489
  function summarizePatchResults(results) {
13766
14490
  const lines = results.map(formatPatchSiteLine);
@@ -13773,26 +14497,112 @@ function summarizePatchResults(results) {
13773
14497
  }
13774
14498
  return lines;
13775
14499
  }
14500
+ function verifyPristineSource(plan, version) {
14501
+ if (!plan.probeVersion) return { ok: true };
14502
+ const backupVersion = getClaudeVersionForBinary(plan.backupPath);
14503
+ if (backupVersion === version) return { ok: true };
14504
+ return {
14505
+ ok: false,
14506
+ 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\`.`
14507
+ };
14508
+ }
14509
+ function publishBackupFile(from, to) {
14510
+ const temp = `${to}.tmp-${process.pid}-${Date.now().toString(36)}`;
14511
+ try {
14512
+ copyFileSync(from, temp);
14513
+ renameSync3(temp, to);
14514
+ } catch (err) {
14515
+ try {
14516
+ rmSync(temp, { force: true });
14517
+ } catch {
14518
+ }
14519
+ throw err;
14520
+ }
14521
+ }
14522
+ function describePoisonedPristineSource(backupPath, version) {
14523
+ 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\`.`;
14524
+ }
14525
+ function requiredEffortPatchFailures(results) {
14526
+ return results.filter(
14527
+ (result) => result.status === "FAIL" && (result.name.startsWith("PATCH 8a:") || result.name.startsWith("PATCH 8b:") || result.name.startsWith("PATCH 8c:") || result.name.startsWith("PATCH 9:"))
14528
+ );
14529
+ }
13776
14530
  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");
14531
+ let candidateDir;
13789
14532
  let results;
14533
+ let patchedSize;
14534
+ let patchedSha256;
14535
+ let backup;
14536
+ let pristineSha256;
13790
14537
  try {
13791
- const installation = await tryDetectInstallation({ path: binaryPath });
13792
- const source = await readContent(installation);
13793
- const patched = applyClodexPatches(source, desired.config);
14538
+ mkdirSync7(backupDir(), { recursive: true });
14539
+ const { tryDetectInstallation, readContent, writeContent } = await import("tweakcc");
14540
+ candidateDir = mkdtempSync(join8(dirname5(binaryPath), ".clodex-patch-"));
14541
+ const candidatePath = join8(candidateDir, basename(binaryPath));
14542
+ const seedCandidate = async (from) => {
14543
+ copyFileSync(from, candidatePath);
14544
+ const installation = await tryDetectInstallation({ path: candidatePath });
14545
+ return { installation, source: await readContent(installation) };
14546
+ };
14547
+ const facts = collectPristineFacts({
14548
+ version,
14549
+ binaryPath,
14550
+ manifest: opts.manifest
14551
+ });
14552
+ const initial = planPristineSource(facts);
14553
+ let loaded = null;
14554
+ let plan;
14555
+ if (initial.action === "inspect") {
14556
+ loaded = await seedCandidate(binaryPath);
14557
+ if (looksLikeLegacyClodexPatch(loaded.source)) {
14558
+ p11.log.warn(
14559
+ `${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.`
14560
+ );
14561
+ }
14562
+ plan = planInspectedPristineSource(facts, { patched: isPatchedClaudeSource(loaded.source) });
14563
+ if (plan.action !== "snapshot") loaded = null;
14564
+ } else {
14565
+ plan = initial;
14566
+ }
14567
+ if (plan.action === "error") return { ok: false, message: plan.message };
14568
+ for (const note of plan.notes) p11.log.warn(note);
14569
+ if (plan.action === "restore") {
14570
+ const verified = verifyPristineSource(plan, version);
14571
+ if (!verified.ok) return { ok: false, message: verified.message };
14572
+ p11.log.info(`Binary differs from its pristine backup \u2014 building a fresh patch candidate from ${plan.backupPath}.`);
14573
+ }
14574
+ pristineSha256 = plan.pristineSha256;
14575
+ backup = plan.backupPath;
14576
+ if (!loaded) {
14577
+ loaded = await seedCandidate(backup);
14578
+ if (isPatchedClaudeSource(loaded.source)) {
14579
+ return { ok: false, message: describePoisonedPristineSource(backup, version) };
14580
+ }
14581
+ } else if (plan.action === "snapshot") {
14582
+ publishBackupFile(candidatePath, plan.backupPath);
14583
+ }
14584
+ const canonical = contentAddressedBackupPath(version, pristineSha256);
14585
+ if (canonical !== backup) {
14586
+ const alreadyStored = facts.backups.some(
14587
+ (candidate) => candidate.path === canonical && candidate.sha256 === pristineSha256
14588
+ );
14589
+ if (!alreadyStored) publishBackupFile(backup, canonical);
14590
+ backup = canonical;
14591
+ }
14592
+ publishBackupFile(backup, tweakccMirrorBackupPath());
14593
+ const patched = applyClodexPatches(loaded.source, desired.config);
13794
14594
  results = patched.results;
13795
- await writeContent(installation, patched.content);
14595
+ const failedEffortPatches = requiredEffortPatchFailures(results);
14596
+ if (failedEffortPatches.length > 0) {
14597
+ throw new PatchApplyError(
14598
+ `clodex patch: required effort patches failed: ${failedEffortPatches.map((result) => result.name).join("; ")}`,
14599
+ results
14600
+ );
14601
+ }
14602
+ await writeContent(loaded.installation, patched.content);
14603
+ patchedSize = statSync4(candidatePath).size;
14604
+ patchedSha256 = sha256File(candidatePath);
14605
+ renameSync3(candidatePath, binaryPath);
13796
14606
  } catch (err) {
13797
14607
  const detailLines = err instanceof PatchApplyError ? summarizePatchResults(err.results) : [];
13798
14608
  if (opts.trace && detailLines.length) {
@@ -13804,6 +14614,19 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
13804
14614
  message: `Patch failed: ${err instanceof Error ? err.message : String(err)}`,
13805
14615
  detailLines
13806
14616
  };
14617
+ } finally {
14618
+ if (candidateDir !== void 0) {
14619
+ try {
14620
+ rmSync(candidateDir, { recursive: true, force: true });
14621
+ } catch (err) {
14622
+ if (opts.trace) {
14623
+ process.stderr.write(
14624
+ `clodex patch: could not remove temporary candidate directory: ${err instanceof Error ? err.message : String(err)}
14625
+ `
14626
+ );
14627
+ }
14628
+ }
14629
+ }
13807
14630
  }
13808
14631
  if (opts.trace) {
13809
14632
  process.stderr.write(`${summarizePatchResults(results).join("\n")}
@@ -13813,9 +14636,10 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
13813
14636
  binaryPath,
13814
14637
  claudeVersion: version,
13815
14638
  configHash,
13816
- patchedSize: statSync3(binaryPath).size,
13817
- patchedSha256: sha256File(binaryPath),
14639
+ patchedSize,
14640
+ patchedSha256,
13818
14641
  backupPath: backup,
14642
+ pristineSha256,
13819
14643
  patchedAt: (/* @__PURE__ */ new Date()).toISOString()
13820
14644
  };
13821
14645
  writePatchManifest(manifest);
@@ -13828,28 +14652,54 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
13828
14652
  detailLines: summarizePatchResults(results)
13829
14653
  };
13830
14654
  }
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.");
14655
+ function runRestoreCommand(target) {
14656
+ if (!target.ok && target.reason === "binary-not-found") {
14657
+ p11.log.error(describePatchTargetFailure(target));
13835
14658
  return 1;
13836
14659
  }
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;
14660
+ const binaryPath = target.binaryPath;
14661
+ const manifest = readPatchManifest();
14662
+ let version;
14663
+ if (target.ok) {
14664
+ version = target.version;
14665
+ } else if (manifest?.binaryPath === binaryPath && manifest.claudeVersion) {
14666
+ version = manifest.claudeVersion;
14667
+ p11.log.warn(
14668
+ `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.`
14669
+ );
14670
+ } else {
14671
+ p11.log.error(
14672
+ `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).`
14673
+ );
14674
+ return 1;
13852
14675
  }
14676
+ const plan = planRestoreOnly(collectPristineFacts({ version, binaryPath, manifest }));
14677
+ if (plan.action === "error") {
14678
+ p11.log.error(plan.message);
14679
+ return 1;
14680
+ }
14681
+ for (const note of plan.notes) p11.log.warn(note);
14682
+ const verified = verifyPristineSource(plan, version);
14683
+ if (!verified.ok) {
14684
+ p11.log.error(verified.message);
14685
+ return 1;
14686
+ }
14687
+ copyFileSync(plan.backupPath, binaryPath);
14688
+ try {
14689
+ unlinkSync4(getPatchManifestPath());
14690
+ } catch {
14691
+ }
14692
+ p11.log.success(`Restored pristine claude ${version} from ${plan.backupPath}.`);
14693
+ return 0;
14694
+ }
14695
+ async function runPatchCommand(opts = {}) {
14696
+ const target = resolveClaudeBinaryForPatch();
14697
+ if (opts.restore) return runRestoreCommand(target);
14698
+ if (!target.ok) {
14699
+ p11.log.error(describePatchTargetFailure(target));
14700
+ return 1;
14701
+ }
14702
+ const { binaryPath, version } = target;
13853
14703
  const desired = buildDesiredPatchConfig();
13854
14704
  if (Object.keys(desired.config).length === 0) {
13855
14705
  p11.log.error("No favorite models to patch. Save favorites with `clodex models` first.");
@@ -13858,13 +14708,14 @@ async function runPatchCommand(opts = {}) {
13858
14708
  for (const id of desired.unknownWindows) {
13859
14709
  p11.log.warn(`No context window metadata for ${id} \u2014 Claude Code will assume the 200k default.`);
13860
14710
  }
14711
+ reportRejectedModelAliases(desired.rejectedAliasRejections);
13861
14712
  const configHash = computePatchConfigHash(desired.config);
13862
14713
  const manifest = readPatchManifest();
13863
14714
  const state = evaluatePatchState(manifest, {
13864
14715
  binaryPath,
13865
14716
  claudeVersion: version,
13866
14717
  configHash,
13867
- binarySize: statSync3(binaryPath).size
14718
+ binarySize: statSync4(binaryPath).size
13868
14719
  });
13869
14720
  if (state === "current") {
13870
14721
  p11.log.success(`claude ${version} is already patched with the current model config \u2014 nothing to do.`);
@@ -13876,14 +14727,9 @@ async function runPatchCommand(opts = {}) {
13876
14727
  return 1;
13877
14728
  }
13878
14729
  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
14730
  const outcome = await applyPatch(binaryPath, version, desired, configHash, {
13885
14731
  trace: opts.trace ?? false,
13886
- restoreFirst
14732
+ manifest
13887
14733
  });
13888
14734
  if (!outcome.ok) {
13889
14735
  p11.log.error(outcome.message);
@@ -13903,15 +14749,21 @@ async function runLaunchPatchCheck(opts = {}) {
13903
14749
  try {
13904
14750
  const desired = buildDesiredPatchConfig();
13905
14751
  if (Object.keys(desired.config).length === 0) return;
13906
- const resolved = resolveClaudeBinaryForPatch();
13907
- if (!resolved) return;
14752
+ const target = resolveClaudeBinaryForPatch();
14753
+ if (!target.ok) {
14754
+ if (target.reason === "version-unknown" && !opts.agentStdout) {
14755
+ console.error(pc12.dim(`clodex: ${describePatchTargetFailure(target)}`));
14756
+ }
14757
+ return;
14758
+ }
14759
+ const resolved = target;
13908
14760
  const configHash = computePatchConfigHash(desired.config);
13909
14761
  const manifest = readPatchManifest();
13910
14762
  const state = evaluatePatchState(manifest, {
13911
14763
  binaryPath: resolved.binaryPath,
13912
14764
  claudeVersion: resolved.version,
13913
14765
  configHash,
13914
- binarySize: statSync3(resolved.binaryPath).size
14766
+ binarySize: statSync4(resolved.binaryPath).size
13915
14767
  });
13916
14768
  if (state === "current") return;
13917
14769
  const interactive = !opts.dryRun && !opts.agentStdout && process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -14351,6 +15203,7 @@ ${pc13.bold("Behavior:")}
14351
15203
  proxy mode, without opening the interactive manager.
14352
15204
  --alias <name=target> saves a short name for a proxy-mode favorite. The
14353
15205
  target is clodex:<provider-id>:<model-id> (the clodex: prefix is optional).
15206
+ Alias names are stored lowercase and cannot use client-reserved model names.
14354
15207
  --unalias <name> removes a saved short name.
14355
15208
 
14356
15209
  ${pc13.bold("How it works:")}
@@ -14392,7 +15245,19 @@ function printHelp(text4) {
14392
15245
  ${text4}
14393
15246
  `);
14394
15247
  }
15248
+ function reportInactiveCatalogAliases(modelAliases) {
15249
+ const unavailableAliases = modelAliases.filter((alias) => alias.unavailableReason !== void 0);
15250
+ if (unavailableAliases.length === 0) return;
15251
+ const warningLines = unavailableAliases.flatMap((alias) => alias.sourceNames?.length ? alias.sourceNames.map((name) => ` ${JSON.stringify(name)} \u2014 ${alias.unavailableReason}`) : [
15252
+ ` ${JSON.stringify(alias.savedName ?? alias.name)} \u2014 ${alias.unavailableReason}`
15253
+ ]);
15254
+ p12.log.warn(
15255
+ `${warningLines.length} saved model alias${warningLines.length === 1 ? "" : "es"} inactive. Saved entries were preserved.
15256
+ ` + warningLines.join("\n")
15257
+ );
15258
+ }
14395
15259
  async function launchClaudeViaCatalog(catalogRoutes, startingRoute, modelAliases, contextWindow, trace, claudeArgs) {
15260
+ reportInactiveCatalogAliases(modelAliases);
14396
15261
  let proxyHandle;
14397
15262
  try {
14398
15263
  proxyHandle = await startProxyCatalog(
@@ -14452,27 +15317,36 @@ async function runModelsCommand(opts = {}) {
14452
15317
  p12.log.info("Add it with `clodex models`, then save the alias.");
14453
15318
  return 1;
14454
15319
  }
14455
- const modelAliases = (prefs2.modelAliases ?? []).filter((alias) => alias.name !== parsed.name);
15320
+ if (prefs2.modelAliases !== void 0 && !Array.isArray(prefs2.modelAliases)) {
15321
+ p12.log.error('Saved model aliases are malformed: "modelAliases" must be an array.');
15322
+ return 1;
15323
+ }
15324
+ const aliases = prefs2.modelAliases ?? [];
15325
+ const modelAliases = aliases.filter((alias) => !modelAliasMatchesName(alias, parsed.name));
14456
15326
  modelAliases.push(parsed);
14457
15327
  savePreferences({ modelAliases });
14458
15328
  p12.log.success(`Saved model alias ${parsed.name} \u2192 ${modelAliasTarget(parsed)}.`);
14459
15329
  return 0;
14460
15330
  }
14461
15331
  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.");
15332
+ const requestedName = opts.unalias.trim();
15333
+ const name = canonicalModelAliasName(requestedName);
15334
+ const prefs2 = loadPreferences();
15335
+ if (prefs2.modelAliases !== void 0 && !Array.isArray(prefs2.modelAliases)) {
15336
+ p12.log.error('Saved model aliases are malformed: "modelAliases" must be an array.');
14465
15337
  return 1;
14466
15338
  }
14467
- const prefs2 = loadPreferences();
14468
15339
  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.`);
15340
+ const modelAliases = aliases.filter((alias) => !modelAliasMatchesStoredName(alias, requestedName));
15341
+ const removedCount = aliases.length - modelAliases.length;
15342
+ if (removedCount === 0) {
15343
+ p12.log.error(`No model alias named ${JSON.stringify(requestedName)} is saved.`);
14472
15344
  return 1;
14473
15345
  }
14474
15346
  savePreferences({ modelAliases });
14475
- p12.log.success(`Removed model alias ${name}.`);
15347
+ p12.log.success(
15348
+ removedCount === 1 ? `Removed model alias ${name || JSON.stringify(requestedName)}.` : `Removed ${removedCount} model aliases named ${name || JSON.stringify(requestedName)}.`
15349
+ );
14476
15350
  return 0;
14477
15351
  }
14478
15352
  if (opts.list) {
@@ -14956,7 +15830,11 @@ Error: ${launchPlan.error}
14956
15830
  return launchClaudeViaCatalog(
14957
15831
  catalogRoutes,
14958
15832
  startingRoute,
14959
- resolveCatalogModelAliases(prefs.modelAliases ?? [], resolveRoute),
15833
+ resolveCatalogModelAliases(
15834
+ prefs.modelAliases,
15835
+ resolveRoute,
15836
+ catalogRoutes
15837
+ ),
14960
15838
  selectedModel.contextWindow,
14961
15839
  trace,
14962
15840
  claudeArgs
@@ -15203,6 +16081,7 @@ export {
15203
16081
  modelsHelpText,
15204
16082
  parseArgs,
15205
16083
  patchHelpText,
16084
+ reportInactiveCatalogAliases,
15206
16085
  rootHelpText,
15207
16086
  runClaudeCommand,
15208
16087
  runModelsCommand,