@bman654/clodex 2.1.9 → 2.2.1

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
@@ -9,6 +9,7 @@ import {
9
9
  getCredentialMutationLockPath,
10
10
  getCredentialStateRoot,
11
11
  getInstalledClaudeVersion,
12
+ getLocalPatchesPath,
12
13
  getLogsPath,
13
14
  getSavedServerPassword,
14
15
  getServerExposedProviders,
@@ -39,7 +40,7 @@ import {
39
40
  withProviderMutationLock,
40
41
  withRegistryWriteLock,
41
42
  withRegistryWriteLockSync
42
- } from "./chunk-VQOX3AXE.js";
43
+ } from "./chunk-W4SVDQCZ.js";
43
44
 
44
45
  // src/cli.ts
45
46
  import pc13 from "picocolors";
@@ -217,7 +218,7 @@ import { join } from "path";
217
218
  // package.json
218
219
  var package_default = {
219
220
  name: "@bman654/clodex",
220
- version: "2.1.9",
221
+ version: "2.2.1",
221
222
  publishConfig: {
222
223
  access: "public"
223
224
  },
@@ -273,7 +274,7 @@ var package_default = {
273
274
  open: "11.0.0",
274
275
  picocolors: "1.1.1",
275
276
  tweakcc: "4.3.0",
276
- undici: "7.28.0",
277
+ undici: "7.29.0",
277
278
  ws: "8.21.0"
278
279
  },
279
280
  devDependencies: {
@@ -13917,16 +13918,16 @@ function planLaunchWizard(opts) {
13917
13918
  }
13918
13919
 
13919
13920
  // src/patcher.ts
13920
- import { createHash as createHash8 } from "crypto";
13921
+ import { createHash as createHash9 } from "crypto";
13921
13922
  import {
13922
13923
  copyFileSync,
13923
13924
  existsSync as existsSync7,
13924
13925
  mkdtempSync,
13925
13926
  mkdirSync as mkdirSync7,
13926
- readFileSync as readFileSync9,
13927
+ readFileSync as readFileSync10,
13927
13928
  renameSync as renameSync3,
13928
13929
  rmSync,
13929
- statSync as statSync4,
13930
+ statSync as statSync5,
13930
13931
  unlinkSync as unlinkSync4,
13931
13932
  writeFileSync as writeFileSync7,
13932
13933
  openSync as openSync4,
@@ -13938,9 +13939,380 @@ import { basename, dirname as dirname5, join as join8 } from "path";
13938
13939
  import pc12 from "picocolors";
13939
13940
  import * as p11 from "@clack/prompts";
13940
13941
 
13941
- // src/patch-backup.ts
13942
+ // src/local-patches.ts
13942
13943
  import { createHash as createHash7 } from "crypto";
13943
- import { existsSync as existsSync6, readFileSync as readFileSync8, readdirSync, statSync as statSync3 } from "fs";
13944
+ import { readFileSync as readFileSync8, statSync as statSync3 } from "fs";
13945
+ var LOCAL_PATCH_API_VERSION = 1;
13946
+ var LOCAL_PATCH_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
13947
+ var MAX_LOCAL_PATCH_BYTES = 1024 * 1024;
13948
+ var RESERVED_MARKER_PREFIX = "/*ccpatch:";
13949
+ var LOCAL_MARKER_PREFIX = "/*clodex-local:";
13950
+ var MAX_ERROR_LENGTH = 300;
13951
+ var localModuleEvaluationSequence = 0;
13952
+ function describeLocalError(error) {
13953
+ const raw = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
13954
+ const redacted = raw.replace(
13955
+ /data:text\/javascript;base64,[^\s"'`]+/g,
13956
+ "<local-patches.mjs>"
13957
+ );
13958
+ const singleLine = redacted.replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
13959
+ if (singleLine.length <= MAX_ERROR_LENGTH) return singleLine || "unknown error";
13960
+ return `${singleLine.slice(0, MAX_ERROR_LENGTH - 1)}\u2026`;
13961
+ }
13962
+ function describeLocalId(id) {
13963
+ const encoded = JSON.stringify(id);
13964
+ return encoded.length <= 96 ? encoded : `${encoded.slice(0, 95)}\u2026`;
13965
+ }
13966
+ function countOccurrences(source, needle) {
13967
+ let count = 0;
13968
+ let offset = 0;
13969
+ while ((offset = source.indexOf(needle, offset)) !== -1) {
13970
+ count += 1;
13971
+ offset += needle.length;
13972
+ }
13973
+ return count;
13974
+ }
13975
+ function reservedMarkerInventory(source) {
13976
+ const count = countOccurrences(source, RESERVED_MARKER_PREFIX);
13977
+ const markers = source.match(/\/\*ccpatch:[\s\S]*?\*\//g)?.sort() ?? [];
13978
+ return JSON.stringify([count, markers]);
13979
+ }
13980
+ function localMarkerInventory(source) {
13981
+ const count = countOccurrences(source, LOCAL_MARKER_PREFIX);
13982
+ const markers = source.match(/\/\*clodex-local:[\s\S]*?\*\//g)?.sort() ?? [];
13983
+ return JSON.stringify([count, markers]);
13984
+ }
13985
+ function inspectLocalPatchSource(enabled) {
13986
+ const path = getLocalPatchesPath();
13987
+ if (!enabled) return { enabled: false, path };
13988
+ try {
13989
+ const stats = statSync3(path);
13990
+ if (!stats.isFile()) throw Object.assign(new Error("not a regular file"), { code: "EINVAL" });
13991
+ if (stats.size > MAX_LOCAL_PATCH_BYTES) {
13992
+ throw Object.assign(new Error("module exceeds 1 MiB"), { code: "EFBIG" });
13993
+ }
13994
+ const bytes = readFileSync8(path);
13995
+ if (bytes.length > MAX_LOCAL_PATCH_BYTES) {
13996
+ throw Object.assign(new Error("module exceeds 1 MiB"), { code: "EFBIG" });
13997
+ }
13998
+ const digest = createHash7("sha256").update(bytes).digest("hex");
13999
+ const source = bytes.toString("utf8");
14000
+ if (!Buffer.from(source, "utf8").equals(bytes)) {
14001
+ return {
14002
+ enabled: true,
14003
+ path,
14004
+ configIdentity: `v${LOCAL_PATCH_API_VERSION}:invalid-utf8:${digest}`,
14005
+ error: `could not read ${path}: module must be UTF-8`
14006
+ };
14007
+ }
14008
+ return {
14009
+ enabled: true,
14010
+ path,
14011
+ configIdentity: `v${LOCAL_PATCH_API_VERSION}:${digest}`,
14012
+ source
14013
+ };
14014
+ } catch (error) {
14015
+ const code = error instanceof Error && "code" in error ? String(error.code) : "UNKNOWN";
14016
+ const reason = code === "EFBIG" ? "module exceeds 1 MiB" : code === "EINVAL" ? "not a regular file" : code;
14017
+ return {
14018
+ enabled: true,
14019
+ path,
14020
+ configIdentity: `v${LOCAL_PATCH_API_VERSION}:unavailable:${code}`,
14021
+ error: `could not read ${path}: ${reason}`
14022
+ };
14023
+ }
14024
+ }
14025
+ function failedLocalTransaction(source, patches, results, failedIndex, extra) {
14026
+ const failedName = `LOCAL ${patches[failedIndex].id}`;
14027
+ const rolledBack = results.map((result) => result.status === "OK" ? {
14028
+ status: "SKIP",
14029
+ name: result.name,
14030
+ extra: `rolled back after ${failedName} failed`
14031
+ } : result);
14032
+ rolledBack.push({ status: "FAIL", name: failedName, extra });
14033
+ for (const remaining of patches.slice(failedIndex + 1)) {
14034
+ rolledBack.push({
14035
+ status: "SKIP",
14036
+ name: `LOCAL ${remaining.id}`,
14037
+ extra: `not run after ${failedName} failed`
14038
+ });
14039
+ }
14040
+ return { content: source, results: rolledBack };
14041
+ }
14042
+ function snapshotLocalPatchDefinitions(value) {
14043
+ if (!Array.isArray(value)) return "default export must be an array";
14044
+ const rawDefinitions = [...value];
14045
+ const snapshot = [];
14046
+ const ids = /* @__PURE__ */ new Set();
14047
+ for (const [index, definition] of rawDefinitions.entries()) {
14048
+ if (definition === null || typeof definition !== "object") {
14049
+ return `entry at index ${index} must be an object`;
14050
+ }
14051
+ const candidate = definition;
14052
+ const id = candidate.id;
14053
+ const apply = candidate.apply;
14054
+ if (typeof id !== "string") return `entry at index ${index} must have a string id`;
14055
+ if (typeof apply !== "function") {
14056
+ return `entry ${describeLocalId(id)} must have an apply function`;
14057
+ }
14058
+ if (!LOCAL_PATCH_ID_PATTERN.test(id)) {
14059
+ return `invalid id at index ${index}: ${describeLocalId(id)}`;
14060
+ }
14061
+ if (ids.has(id)) return `duplicate id at index ${index}: ${describeLocalId(id)}`;
14062
+ ids.add(id);
14063
+ snapshot.push(Object.freeze({ id, apply }));
14064
+ }
14065
+ return Object.freeze(snapshot);
14066
+ }
14067
+ function applyLocalPatchSnapshot(source, patches) {
14068
+ let content = source;
14069
+ const results = [];
14070
+ for (const [index, patch] of patches.entries()) {
14071
+ const marker = `/*clodex-local:${patch.id}*/`;
14072
+ const existingMarkerCount = countOccurrences(content, marker);
14073
+ if (existingMarkerCount > 1) {
14074
+ return failedLocalTransaction(
14075
+ source,
14076
+ patches,
14077
+ results,
14078
+ index,
14079
+ "existing marker appears more than once"
14080
+ );
14081
+ }
14082
+ if (existingMarkerCount === 1) {
14083
+ results.push({
14084
+ status: "SKIP",
14085
+ name: `LOCAL ${patch.id}`,
14086
+ extra: "already patched"
14087
+ });
14088
+ continue;
14089
+ }
14090
+ let next;
14091
+ const reservedMarkers = reservedMarkerInventory(content);
14092
+ const localMarkers = localMarkerInventory(content);
14093
+ try {
14094
+ next = patch.apply(content, { marker });
14095
+ } catch (error) {
14096
+ return failedLocalTransaction(
14097
+ source,
14098
+ patches,
14099
+ results,
14100
+ index,
14101
+ describeLocalError(error)
14102
+ );
14103
+ }
14104
+ if (typeof next !== "string") {
14105
+ return failedLocalTransaction(
14106
+ source,
14107
+ patches,
14108
+ results,
14109
+ index,
14110
+ `transform returned ${typeof next} instead of a string`
14111
+ );
14112
+ }
14113
+ if (reservedMarkerInventory(next) !== reservedMarkers) {
14114
+ return failedLocalTransaction(
14115
+ source,
14116
+ patches,
14117
+ results,
14118
+ index,
14119
+ "transform changed reserved /*ccpatch: markers"
14120
+ );
14121
+ }
14122
+ const markerCount = countOccurrences(next, marker);
14123
+ if (markerCount !== 1) {
14124
+ return failedLocalTransaction(
14125
+ source,
14126
+ patches,
14127
+ results,
14128
+ index,
14129
+ markerCount === 0 ? `transform did not emit ${marker}` : `transform must emit exactly one ${marker} marker`
14130
+ );
14131
+ }
14132
+ if (localMarkerInventory(next.replace(marker, "")) !== localMarkers) {
14133
+ return failedLocalTransaction(
14134
+ source,
14135
+ patches,
14136
+ results,
14137
+ index,
14138
+ "transform changed another local patch marker"
14139
+ );
14140
+ }
14141
+ content = next;
14142
+ results.push({ status: "OK", name: `LOCAL ${patch.id}` });
14143
+ }
14144
+ return { content, results };
14145
+ }
14146
+ function localPatchSetFailure(source, extra) {
14147
+ return {
14148
+ content: source,
14149
+ results: [{ status: "FAIL", name: "LOCAL PATCH SET", extra }]
14150
+ };
14151
+ }
14152
+ async function applyLocalPatches(source, localSource) {
14153
+ if (!localSource.enabled) return { content: source, results: [] };
14154
+ if (typeof localSource.error === "string") {
14155
+ return localPatchSetFailure(source, localSource.error);
14156
+ }
14157
+ try {
14158
+ const moduleUrl = `data:text/javascript;base64,${Buffer.from(localSource.source).toString("base64")}#clodex-local-eval-${++localModuleEvaluationSequence}`;
14159
+ const loaded = await import(
14160
+ /* @vite-ignore */
14161
+ moduleUrl
14162
+ );
14163
+ const definitions = snapshotLocalPatchDefinitions(loaded.default);
14164
+ if (typeof definitions === "string") {
14165
+ return localPatchSetFailure(source, definitions);
14166
+ }
14167
+ if (definitions.length === 0) {
14168
+ return {
14169
+ content: source,
14170
+ results: [{
14171
+ status: "SKIP",
14172
+ name: "LOCAL PATCH SET",
14173
+ extra: "no patches exported"
14174
+ }]
14175
+ };
14176
+ }
14177
+ return applyLocalPatchSnapshot(source, definitions);
14178
+ } catch (error) {
14179
+ return localPatchSetFailure(
14180
+ source,
14181
+ describeLocalError(error)
14182
+ );
14183
+ }
14184
+ }
14185
+
14186
+ // src/built-in-patch-proofs.ts
14187
+ function escapeRegex(value) {
14188
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14189
+ }
14190
+ function countExactOccurrences(source, needle) {
14191
+ let count = 0;
14192
+ let offset = 0;
14193
+ while ((offset = source.indexOf(needle, offset)) !== -1) {
14194
+ count += 1;
14195
+ offset += needle.length;
14196
+ }
14197
+ return count;
14198
+ }
14199
+ function uniqueMatch(source, name, pattern) {
14200
+ const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
14201
+ const matcher = new RegExp(pattern.source, flags);
14202
+ const match = matcher.exec(source);
14203
+ if (!match || matcher.exec(source)) {
14204
+ throw new Error(`clodex patch: could not capture built-in postcondition: ${name}`);
14205
+ }
14206
+ return match[0];
14207
+ }
14208
+ function captureNeedle(source, name, needle) {
14209
+ if (countExactOccurrences(source, needle) !== 1) {
14210
+ throw new Error(`clodex patch: could not capture built-in postcondition: ${name}`);
14211
+ }
14212
+ return {
14213
+ name,
14214
+ protectedText: needle,
14215
+ occurrences: 1
14216
+ };
14217
+ }
14218
+ function capturePattern(source, name, pattern) {
14219
+ return captureNeedle(source, name, uniqueMatch(source, name, pattern));
14220
+ }
14221
+ function normalizedPatchName(name) {
14222
+ return name.replace(/ \(refresh\)$/, "");
14223
+ }
14224
+ function protectsResult(results, name) {
14225
+ const result = results.find((entry) => normalizedPatchName(entry.name) === name);
14226
+ return result !== void 0 && result.status !== "FAIL" && result.extra !== "no aliases configured";
14227
+ }
14228
+ function configuredAliases(config) {
14229
+ const aliases = /* @__PURE__ */ new Map();
14230
+ for (const [id, entry] of Object.entries(config)) {
14231
+ if (entry.alias === void 0) continue;
14232
+ const alias = String(entry.alias).trim().toLowerCase();
14233
+ aliases.set(alias, {
14234
+ id,
14235
+ ...entry.display === void 0 ? {} : { display: String(entry.display) }
14236
+ });
14237
+ }
14238
+ return [...aliases].map(([alias, entry]) => ({ alias, ...entry }));
14239
+ }
14240
+ function effortProofPattern(marker) {
14241
+ return new RegExp(
14242
+ escapeRegex(marker) + 'var _ccv=Object\\.assign\\(Object\\.create\\(null\\),\\{[^{}]*\\}\\)\\[String\\([\\w$]+\\|\\|""\\)\\.trim\\(\\)\\.toLowerCase\\(\\)\\];if\\(_ccv!==void 0\\)return _ccv;'
14243
+ );
14244
+ }
14245
+ function captureBuiltInPatchProofs(source, config, results) {
14246
+ const proofs = [];
14247
+ const addPattern = (name, pattern) => {
14248
+ if (protectsResult(results, name)) {
14249
+ proofs.push(capturePattern(source, name, pattern));
14250
+ }
14251
+ };
14252
+ addPattern(
14253
+ "PATCH 1: Agent tool model enum",
14254
+ /(?:\.enum|model:[A-Za-z_$][\w$]*)\(\["sonnet","opus","haiku"(?:,"[^"]+")*\]\)\.optional\(\)\.describe\(/
14255
+ );
14256
+ addPattern(
14257
+ "PATCH 3: known-alias validator list",
14258
+ /\["sonnet","opus","haiku","fable"(?:,"[^"]+")*,"opusplan"(?:,"[^"]+")*\]/
14259
+ );
14260
+ addPattern(
14261
+ "PATCH 4: Agent tool model description",
14262
+ /describe\(`Optional model override for this agent[^`]*?`\)/
14263
+ );
14264
+ const aliases = configuredAliases(config);
14265
+ if (protectsResult(results, "PATCH 6: alias resolver switch")) {
14266
+ for (const { alias } of aliases) {
14267
+ const value = JSON.stringify(alias);
14268
+ proofs.push(captureNeedle(
14269
+ source,
14270
+ `PATCH 6: alias resolver switch (${alias})`,
14271
+ `case${value}:return ${value};`
14272
+ ));
14273
+ }
14274
+ }
14275
+ if (protectsResult(results, "PATCH 5: model picker options")) {
14276
+ for (const { alias, id, display } of aliases) {
14277
+ const entry = "{value:" + JSON.stringify(alias) + ",label:" + JSON.stringify(alias.charAt(0).toUpperCase() + alias.slice(1)) + ",description:" + JSON.stringify(display ?? `Custom model (${id})`) + "}";
14278
+ proofs.push(captureNeedle(
14279
+ source,
14280
+ `PATCH 5: model picker options (${alias})`,
14281
+ entry
14282
+ ));
14283
+ }
14284
+ }
14285
+ addPattern(
14286
+ "PATCH 7: per-model context window",
14287
+ /\/\*ccpatch:ctx\*\/var _ccw=\(\{[^{}]*\}\)\[[^\]]*\];if\(_ccw!==void 0\)return _ccw;/
14288
+ );
14289
+ addPattern(
14290
+ "PATCH 8a: effort capability",
14291
+ effortProofPattern("/*ccpatch:effort*/")
14292
+ );
14293
+ addPattern(
14294
+ "PATCH 8b: xhigh effort capability",
14295
+ effortProofPattern("/*ccpatch:xhigh-effort*/")
14296
+ );
14297
+ addPattern(
14298
+ "PATCH 8c: max effort capability",
14299
+ effortProofPattern("/*ccpatch:max-effort*/")
14300
+ );
14301
+ addPattern(
14302
+ "PATCH 9: default effort",
14303
+ /\/\*ccpatch:default-effort\*\/var _cce=Object\.assign\(Object\.create\(null\),\{[^{}]*\}\)\[String\([\w$]+\|\|""\)\.trim\(\)\.toLowerCase\(\)\];if\(_cce!==void 0\)return _cce;/
14304
+ );
14305
+ return proofs;
14306
+ }
14307
+ function builtInPatchProofsChanged(source, proofs) {
14308
+ return proofs.some(
14309
+ (proof) => countExactOccurrences(source, proof.protectedText) !== proof.occurrences
14310
+ );
14311
+ }
14312
+
14313
+ // src/patch-backup.ts
14314
+ import { createHash as createHash8 } from "crypto";
14315
+ import { existsSync as existsSync6, readFileSync as readFileSync9, readdirSync, statSync as statSync4 } from "fs";
13944
14316
  import { homedir as homedir2 } from "os";
13945
14317
  import { join as join7 } from "path";
13946
14318
  var BACKUP_SHA_PREFIX_LENGTH = 16;
@@ -13948,7 +14320,7 @@ function backupDir() {
13948
14320
  return process.env["TWEAKCC_CONFIG_DIR"]?.trim() || join7(homedir2(), ".tweakcc");
13949
14321
  }
13950
14322
  function sha256File(path) {
13951
- return createHash7("sha256").update(readFileSync8(path)).digest("hex");
14323
+ return createHash8("sha256").update(readFileSync9(path)).digest("hex");
13952
14324
  }
13953
14325
  function backupVersionTag(version) {
13954
14326
  const tag = version.trim().replace(/[^\w.-]+/g, "_");
@@ -13978,7 +14350,7 @@ function scanPristineBackups(version, dir = backupDir()) {
13978
14350
  const path = join7(dir, entry);
13979
14351
  let sha256;
13980
14352
  try {
13981
- if (!statSync3(path).isFile()) continue;
14353
+ if (!statSync4(path).isFile()) continue;
13982
14354
  sha256 = sha256File(path);
13983
14355
  } catch {
13984
14356
  corrupt.push(path);
@@ -14092,7 +14464,7 @@ function collectPristineFacts(args) {
14092
14464
  }
14093
14465
 
14094
14466
  // src/patch-transforms.ts
14095
- var PATCH_TRANSFORMS_VERSION = 2;
14467
+ var PATCH_TRANSFORMS_VERSION = 3;
14096
14468
  var NATIVE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
14097
14469
  var BASE_EFFORT_LEVELS = ["low", "medium", "high"];
14098
14470
  function projectNativeEffort(effort) {
@@ -14238,8 +14610,8 @@ function applyClodexPatches(source, config) {
14238
14610
  }
14239
14611
  applyOnce(
14240
14612
  "PATCH 1: Agent tool model enum",
14241
- /\.enum\((\["sonnet","opus","haiku"(?:,"[^"]+")*\])\)\.optional\(\)\.describe\(/,
14242
- (_m, arr) => ".enum(" + extendAliasArray(arr) + ").optional().describe(",
14613
+ /((?:\.enum|model:[A-Za-z_$][\w$]*)\()(\["sonnet","opus","haiku"(?:,"[^"]+")*\])\)(\.optional\(\)\.describe\()/,
14614
+ (_m, open2, arr, tail) => open2 + extendAliasArray(arr) + ")" + tail,
14243
14615
  { required: true, noopIsSkip: true }
14244
14616
  );
14245
14617
  applyOnce(
@@ -14399,7 +14771,7 @@ function getPatchLockPath() {
14399
14771
  }
14400
14772
  function readPatchManifest(path = getPatchManifestPath()) {
14401
14773
  try {
14402
- const parsed = JSON.parse(readFileSync9(path, "utf8"));
14774
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
14403
14775
  if (parsed && typeof parsed.binaryPath === "string" && typeof parsed.configHash === "string") {
14404
14776
  return parsed;
14405
14777
  }
@@ -14465,7 +14837,7 @@ function reportRejectedModelAliases(rejections) {
14465
14837
  );
14466
14838
  }
14467
14839
  }
14468
- function computePatchConfigHash(config, transformsVersion = PATCH_TRANSFORMS_VERSION) {
14840
+ function computePatchConfigHash(config, transformsVersion = PATCH_TRANSFORMS_VERSION, localPatchIdentity) {
14469
14841
  const canonical = Object.keys(config).sort().map((key) => {
14470
14842
  const entry = config[key];
14471
14843
  return [
@@ -14477,7 +14849,11 @@ function computePatchConfigHash(config, transformsVersion = PATCH_TRANSFORMS_VER
14477
14849
  entry.effort?.defaultLevel ?? null
14478
14850
  ];
14479
14851
  });
14480
- return createHash8("sha256").update(JSON.stringify([transformsVersion, canonical])).digest("hex");
14852
+ const payload = [transformsVersion, canonical];
14853
+ if (localPatchIdentity !== void 0) {
14854
+ payload.push(["local-patches", localPatchIdentity]);
14855
+ }
14856
+ return createHash9("sha256").update(JSON.stringify(payload)).digest("hex");
14481
14857
  }
14482
14858
  function buildDesiredPatchConfig() {
14483
14859
  const prefs = loadPreferences();
@@ -14548,7 +14924,7 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
14548
14924
  } catch {
14549
14925
  let stale = false;
14550
14926
  try {
14551
- const existing = JSON.parse(readFileSync9(lockPath, "utf8"));
14927
+ const existing = JSON.parse(readFileSync10(lockPath, "utf8"));
14552
14928
  stale = !existing.pid || !isAlive(existing.pid) || typeof existing.startedAt === "number" && now - existing.startedAt > PATCH_LOCK_STALE_MS;
14553
14929
  } catch {
14554
14930
  stale = true;
@@ -14574,7 +14950,7 @@ function resolveClaudeBinaryForPatch() {
14574
14950
  return { ok: false, reason: "binary-not-found" };
14575
14951
  }
14576
14952
  try {
14577
- if (!statSync4(resolved).isFile()) return { ok: false, reason: "binary-not-found" };
14953
+ if (!statSync5(resolved).isFile()) return { ok: false, reason: "binary-not-found" };
14578
14954
  } catch {
14579
14955
  return { ok: false, reason: "binary-not-found" };
14580
14956
  }
@@ -14626,6 +15002,49 @@ function requiredEffortPatchFailures(results) {
14626
15002
  (result) => result.status === "FAIL" && (result.name.startsWith("PATCH 8a:") || result.name.startsWith("PATCH 8b:") || result.name.startsWith("PATCH 8c:") || result.name.startsWith("PATCH 9:"))
14627
15003
  );
14628
15004
  }
15005
+ function builtInPatchVerificationFailed(source, config, expected, proofs) {
15006
+ if (builtInPatchProofsChanged(source, proofs)) return true;
15007
+ let verification;
15008
+ try {
15009
+ verification = applyClodexPatches(source, config);
15010
+ } catch {
15011
+ return true;
15012
+ }
15013
+ if (verification.content !== source || verification.results.length !== expected.length) {
15014
+ return true;
15015
+ }
15016
+ const expectedByName = new Map(expected.map((result) => [
15017
+ result.name.replace(/ \(refresh\)$/, ""),
15018
+ result
15019
+ ]));
15020
+ for (const result of verification.results) {
15021
+ const original = expectedByName.get(result.name.replace(/ \(refresh\)$/, ""));
15022
+ if (!original) return true;
15023
+ if (original.status === "FAIL") {
15024
+ if (result.status !== "FAIL" || result.extra !== original.extra) return true;
15025
+ } else if (result.status !== "SKIP") {
15026
+ return true;
15027
+ }
15028
+ }
15029
+ return false;
15030
+ }
15031
+ function discardLocalPatchOutcome(builtInContent, results) {
15032
+ return {
15033
+ content: builtInContent,
15034
+ results: [
15035
+ ...results.map((result) => result.status === "OK" ? {
15036
+ status: "SKIP",
15037
+ name: result.name,
15038
+ extra: "rolled back after built-in verification failed"
15039
+ } : result),
15040
+ {
15041
+ status: "FAIL",
15042
+ name: "LOCAL PATCH SET",
15043
+ extra: "local output changed built-in patch sites"
15044
+ }
15045
+ ]
15046
+ };
15047
+ }
14629
15048
  async function applyPatch(binaryPath, version, desired, configHash, opts) {
14630
15049
  let candidateDir;
14631
15050
  let results;
@@ -14689,8 +15108,8 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
14689
15108
  backup = canonical;
14690
15109
  }
14691
15110
  publishBackupFile(backup, tweakccMirrorBackupPath());
14692
- const patched = applyClodexPatches(loaded.source, desired.config);
14693
- results = patched.results;
15111
+ const builtIn = applyClodexPatches(loaded.source, desired.config);
15112
+ results = builtIn.results;
14694
15113
  const failedEffortPatches = requiredEffortPatchFailures(results);
14695
15114
  if (failedEffortPatches.length > 0) {
14696
15115
  throw new PatchApplyError(
@@ -14698,8 +15117,38 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
14698
15117
  results
14699
15118
  );
14700
15119
  }
14701
- await writeContent(loaded.installation, patched.content);
14702
- patchedSize = statSync4(candidatePath).size;
15120
+ let builtInProofs = [];
15121
+ let local;
15122
+ if (opts.localPatches?.enabled && typeof opts.localPatches.source === "string") {
15123
+ try {
15124
+ builtInProofs = captureBuiltInPatchProofs(
15125
+ builtIn.content,
15126
+ desired.config,
15127
+ builtIn.results
15128
+ );
15129
+ } catch {
15130
+ local = {
15131
+ content: builtIn.content,
15132
+ results: [{
15133
+ status: "FAIL",
15134
+ name: "LOCAL PATCH SET",
15135
+ extra: "could not capture built-in postconditions"
15136
+ }]
15137
+ };
15138
+ }
15139
+ }
15140
+ local ??= opts.localPatches ? await applyLocalPatches(builtIn.content, opts.localPatches) : { content: builtIn.content, results: [] };
15141
+ if (local.results.some((result) => result.status === "OK") && builtInPatchVerificationFailed(
15142
+ local.content,
15143
+ desired.config,
15144
+ builtIn.results,
15145
+ builtInProofs
15146
+ )) {
15147
+ local = discardLocalPatchOutcome(builtIn.content, local.results);
15148
+ }
15149
+ results = [...results, ...local.results];
15150
+ await writeContent(loaded.installation, local.content);
15151
+ patchedSize = statSync5(candidatePath).size;
14703
15152
  patchedSha256 = sha256File(candidatePath);
14704
15153
  renameSync3(candidatePath, binaryPath);
14705
15154
  } catch (err) {
@@ -14799,6 +15248,9 @@ async function runPatchCommand(opts = {}) {
14799
15248
  return 1;
14800
15249
  }
14801
15250
  const { binaryPath, version } = target;
15251
+ if (opts.localPatches !== void 0) {
15252
+ savePreferences({ localPatchesEnabled: opts.localPatches });
15253
+ }
14802
15254
  const desired = buildDesiredPatchConfig();
14803
15255
  if (Object.keys(desired.config).length === 0) {
14804
15256
  p11.log.error("No favorite models to patch. Save favorites with `clodex models` first.");
@@ -14808,13 +15260,20 @@ async function runPatchCommand(opts = {}) {
14808
15260
  p11.log.warn(`No context window metadata for ${id} \u2014 Claude Code will assume the 200k default.`);
14809
15261
  }
14810
15262
  reportRejectedModelAliases(desired.rejectedAliasRejections);
14811
- const configHash = computePatchConfigHash(desired.config);
15263
+ const localPatches = inspectLocalPatchSource(
15264
+ loadPreferences().localPatchesEnabled === true
15265
+ );
15266
+ const configHash = computePatchConfigHash(
15267
+ desired.config,
15268
+ PATCH_TRANSFORMS_VERSION,
15269
+ localPatches.enabled ? localPatches.configIdentity : void 0
15270
+ );
14812
15271
  const manifest = readPatchManifest();
14813
15272
  const state = evaluatePatchState(manifest, {
14814
15273
  binaryPath,
14815
15274
  claudeVersion: version,
14816
15275
  configHash,
14817
- binarySize: statSync4(binaryPath).size
15276
+ binarySize: statSync5(binaryPath).size
14818
15277
  });
14819
15278
  if (state === "current") {
14820
15279
  p11.log.success(`claude ${version} is already patched with the current model config \u2014 nothing to do.`);
@@ -14828,7 +15287,8 @@ async function runPatchCommand(opts = {}) {
14828
15287
  try {
14829
15288
  const outcome = await applyPatch(binaryPath, version, desired, configHash, {
14830
15289
  trace: opts.trace ?? false,
14831
- manifest
15290
+ manifest,
15291
+ localPatches
14832
15292
  });
14833
15293
  if (!outcome.ok) {
14834
15294
  p11.log.error(outcome.message);
@@ -14856,13 +15316,20 @@ async function runLaunchPatchCheck(opts = {}) {
14856
15316
  return;
14857
15317
  }
14858
15318
  const resolved = target;
14859
- const configHash = computePatchConfigHash(desired.config);
15319
+ const localPatches = inspectLocalPatchSource(
15320
+ loadPreferences().localPatchesEnabled === true
15321
+ );
15322
+ const configHash = computePatchConfigHash(
15323
+ desired.config,
15324
+ PATCH_TRANSFORMS_VERSION,
15325
+ localPatches.enabled ? localPatches.configIdentity : void 0
15326
+ );
14860
15327
  const manifest = readPatchManifest();
14861
15328
  const state = evaluatePatchState(manifest, {
14862
15329
  binaryPath: resolved.binaryPath,
14863
15330
  claudeVersion: resolved.version,
14864
15331
  configHash,
14865
- binarySize: statSync4(resolved.binaryPath).size
15332
+ binarySize: statSync5(resolved.binaryPath).size
14866
15333
  });
14867
15334
  if (state === "current") return;
14868
15335
  const interactive = !opts.dryRun && !opts.agentStdout && process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -15068,9 +15535,22 @@ function parseArgs(args) {
15068
15535
  if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
15069
15536
  else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
15070
15537
  else if (arg === "--restore") parsed2.patchRestore = true;
15071
- else if (arg === "--trace") parsed2.trace = true;
15538
+ else if (arg === "--enable-local-patches") {
15539
+ if (parsed2.patchLocalPatches === false && !parsed2.error) {
15540
+ parsed2.error = "--enable-local-patches and --disable-local-patches cannot be combined";
15541
+ }
15542
+ parsed2.patchLocalPatches = true;
15543
+ } else if (arg === "--disable-local-patches") {
15544
+ if (parsed2.patchLocalPatches === true && !parsed2.error) {
15545
+ parsed2.error = "--enable-local-patches and --disable-local-patches cannot be combined";
15546
+ }
15547
+ parsed2.patchLocalPatches = false;
15548
+ } else if (arg === "--trace") parsed2.trace = true;
15072
15549
  else if (!parsed2.error) parsed2.error = `Unknown patch option: ${arg}`;
15073
15550
  }
15551
+ if (parsed2.patchRestore && parsed2.patchLocalPatches !== void 0 && !parsed2.error) {
15552
+ parsed2.error = "--restore cannot be combined with local-patch settings";
15553
+ }
15074
15554
  return parsed2;
15075
15555
  }
15076
15556
  if (first !== "claude") {
@@ -15113,7 +15593,7 @@ Bridge Claude Code to OpenAI models \u2014 OpenAI API key or ChatGPT/Codex-plan
15113
15593
  ${pc13.bold("Usage:")}
15114
15594
  clodex claude [options] [claude-flags]
15115
15595
  clodex server [options]
15116
- clodex patch [--restore]
15596
+ clodex patch [options]
15117
15597
  clodex models
15118
15598
  clodex favorites
15119
15599
  clodex providers
@@ -15325,11 +15805,15 @@ real ids, and reporting the correct context window.
15325
15805
  ${pc13.bold("Usage:")}
15326
15806
  clodex patch
15327
15807
  clodex patch --restore
15808
+ clodex patch --enable-local-patches
15809
+ clodex patch --disable-local-patches
15328
15810
  clodex patch --help
15329
15811
 
15330
15812
  ${pc13.bold("Options:")}
15331
- --restore Restore the pristine (unpatched) Claude Code binary
15332
- --trace Show per-patch-site results (OK/SKIP/FAIL)
15813
+ --restore Restore the pristine Claude Code binary
15814
+ --trace Show per-patch-site results (OK/SKIP/FAIL)
15815
+ --enable-local-patches Persistently enable ~/.clodex/local-patches.mjs
15816
+ --disable-local-patches Disable local patches and rebuild without them
15333
15817
 
15334
15818
  ${pc13.bold("Behavior:")}
15335
15819
  The patch map is built automatically from your clodex favorites and aliases
@@ -15337,6 +15821,10 @@ ${pc13.bold("Behavior:")}
15337
15821
  per-version backup is kept, and a manifest (~/.clodex/patch-state.json)
15338
15822
  makes re-runs no-ops until your config or Claude Code version changes \u2014
15339
15823
  then the binary is restored first and re-patched fresh.
15824
+
15825
+ Local patches execute after the built-ins as an all-or-none set. Enabling
15826
+ them executes trusted JavaScript from local-patches.mjs with your full user
15827
+ permissions. Local failures are reported but never block the built-ins.
15340
15828
  Run clodex patch again after every claude update.`;
15341
15829
  }
15342
15830
  function printHelp(text4) {
@@ -16143,7 +16631,11 @@ Error: ${parsed.error}
16143
16631
  printHelp(patchHelpText());
16144
16632
  return 0;
16145
16633
  }
16146
- return runPatchCommand({ restore: parsed.patchRestore, trace: parsed.trace });
16634
+ return runPatchCommand({
16635
+ restore: parsed.patchRestore,
16636
+ trace: parsed.trace,
16637
+ localPatches: parsed.patchLocalPatches
16638
+ });
16147
16639
  }
16148
16640
  if (parsed.showVersion) {
16149
16641
  console.log(VERSION);