@bman654/clodex 1.0.0 → 1.0.2

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
@@ -202,7 +202,7 @@ import { join } from "path";
202
202
  // package.json
203
203
  var package_default = {
204
204
  name: "@bman654/clodex",
205
- version: "1.0.0",
205
+ version: "1.0.2",
206
206
  publishConfig: {
207
207
  access: "public"
208
208
  },
@@ -256,6 +256,7 @@ var package_default = {
256
256
  "node-forge": "1.4.0",
257
257
  open: "11.0.0",
258
258
  picocolors: "1.1.1",
259
+ tweakcc: "4.3.0",
259
260
  undici: "7.28.0",
260
261
  ws: "8.21.0"
261
262
  },
@@ -10516,14 +10517,12 @@ function planLaunchWizard(opts) {
10516
10517
  }
10517
10518
 
10518
10519
  // src/patcher.ts
10519
- import { spawn } from "child_process";
10520
10520
  import { createHash as createHash5 } from "crypto";
10521
10521
  import {
10522
10522
  copyFileSync as copyFileSync2,
10523
10523
  existsSync as existsSync6,
10524
10524
  mkdirSync as mkdirSync6,
10525
10525
  readFileSync as readFileSync7,
10526
- rmSync,
10527
10526
  statSync as statSync2,
10528
10527
  unlinkSync as unlinkSync2,
10529
10528
  writeFileSync as writeFileSync5,
@@ -10531,313 +10530,189 @@ import {
10531
10530
  closeSync as closeSync3,
10532
10531
  realpathSync
10533
10532
  } from "fs";
10534
- import { homedir as homedir2, tmpdir } from "os";
10533
+ import { homedir as homedir2 } from "os";
10535
10534
  import { basename, join as join6 } from "path";
10536
10535
  import pc12 from "picocolors";
10537
10536
  import * as p11 from "@clack/prompts";
10538
10537
 
10539
- // src/patch-script-template.ts
10540
- var CLODEX_CONFIG_START = "// clodex:config:start";
10541
- var CLODEX_CONFIG_END = "// clodex:config:end";
10542
- var TEMPLATE = String.raw`/**
10543
- * clodex patch — inject custom Claude Code model aliases (tweakcc adhoc-patch script).
10544
- * Generated by clodex; do not edit. See clodex patch --help.
10545
- */
10546
-
10547
- // clodex:config:start
10548
- const MODEL_CONFIG = {};
10549
- // clodex:config:end
10550
-
10551
- // ---- derive helpers --------------------------------------------------------
10552
- // alias -> model id (only for entries that define an alias)
10553
- const ALIAS_TO_ID = {};
10554
- // The name Claude Code knows a model by: its alias when it has one, else its
10555
- // canonical id. This single value is used for the Agent-tool enum, the
10556
- // known-alias validator, the /model picker value, and the context-window table,
10557
- // so the name the binary validates == the name it sends upstream == the name
10558
- // the proxy echoes back == the key its context window is stored under.
10559
- const IDENTITIES = [];
10560
- // identity -> human label for the /model picker (falls back at use site)
10561
- const DISPLAY_BY_IDENTITY = {};
10562
- // lowercased alias AND id -> context-window tokens (only for models that set it)
10563
- const CONTEXT_BY_KEY = {};
10564
- for (const [id, value] of Object.entries(MODEL_CONFIG)) {
10565
- const spec = value && typeof value === "object" ? value : { alias: value };
10566
- if (spec.alias !== undefined) {
10567
- const a = String(spec.alias).trim().toLowerCase();
10568
- if (!/^[a-z0-9][a-z0-9._-]*(\[1m\])?$/.test(a)) {
10569
- throw new Error('clodex patch: alias "' + spec.alias + '" is not a safe lowercase alias');
10570
- }
10571
- ALIAS_TO_ID[a] = String(id);
10572
- IDENTITIES.push(a);
10573
- if (spec.display) DISPLAY_BY_IDENTITY[a] = String(spec.display);
10574
- } else {
10575
- IDENTITIES.push(String(id));
10576
- if (spec.display) DISPLAY_BY_IDENTITY[String(id)] = String(spec.display);
10538
+ // src/patch-transforms.ts
10539
+ var PatchApplyError = class extends Error {
10540
+ results;
10541
+ constructor(message, results) {
10542
+ super(message);
10543
+ this.name = "PatchApplyError";
10544
+ this.results = results;
10577
10545
  }
10578
-
10579
- if (spec.context !== undefined) {
10580
- const n = Number(spec.context);
10581
- if (!Number.isInteger(n) || n <= 0) {
10582
- throw new Error('clodex patch: context for "' + id + '" must be a positive integer, got ' + spec.context);
10546
+ };
10547
+ function formatPatchSiteLine(result) {
10548
+ return " " + result.status.padEnd(4) + " " + result.name + (result.extra ? " \u2014 " + result.extra : "");
10549
+ }
10550
+ function applyClodexPatches(source, config) {
10551
+ let js = source;
10552
+ const MODEL_CONFIG = config;
10553
+ const ALIAS_TO_ID = {};
10554
+ const IDENTITIES = [];
10555
+ const DISPLAY_BY_IDENTITY = {};
10556
+ const CONTEXT_BY_KEY = {};
10557
+ const report = [];
10558
+ const fail = (message) => {
10559
+ throw new PatchApplyError(message, report);
10560
+ };
10561
+ for (const [id, value] of Object.entries(MODEL_CONFIG)) {
10562
+ const spec = value && typeof value === "object" ? value : { alias: value };
10563
+ if (spec.alias !== void 0) {
10564
+ const a = String(spec.alias).trim().toLowerCase();
10565
+ if (!/^[a-z0-9][a-z0-9._-]*(\[1m\])?$/.test(a)) {
10566
+ fail('clodex patch: alias "' + spec.alias + '" is not a safe lowercase alias');
10567
+ }
10568
+ ALIAS_TO_ID[a] = String(id);
10569
+ IDENTITIES.push(a);
10570
+ if (spec.display) DISPLAY_BY_IDENTITY[a] = String(spec.display);
10571
+ } else {
10572
+ IDENTITIES.push(String(id));
10573
+ if (spec.display) DISPLAY_BY_IDENTITY[String(id)] = String(spec.display);
10583
10574
  }
10584
- // A [1m] suffix hard-codes 1M upstream (and sends the context-1m beta header
10585
- // + raises the media cap). An explicit context on a [1m] model would win via
10586
- // PATCH 7 while those side effects silently stayed on — so reject it.
10587
- if (/\[1m\]/i.test(String(spec.alias ?? "")) || /\[1m\]/i.test(id)) {
10588
- throw new Error(
10589
- 'clodex patch: "' + id + '" sets context but keeps the [1m] suffix — drop the suffix from both the id and the alias'
10590
- );
10575
+ if (spec.context !== void 0) {
10576
+ const n = Number(spec.context);
10577
+ if (!Number.isInteger(n) || n <= 0) {
10578
+ fail('clodex patch: context for "' + id + '" must be a positive integer, got ' + spec.context);
10579
+ }
10580
+ if (/\[1m\]/i.test(String(spec.alias ?? "")) || /\[1m\]/i.test(id)) {
10581
+ fail(
10582
+ 'clodex patch: "' + id + '" sets context but keeps the [1m] suffix \u2014 drop the suffix from both the id and the alias'
10583
+ );
10584
+ }
10585
+ if (spec.alias !== void 0) CONTEXT_BY_KEY[String(spec.alias).trim().toLowerCase()] = n;
10586
+ CONTEXT_BY_KEY[String(id).trim().toLowerCase()] = n;
10591
10587
  }
10592
- if (spec.alias !== undefined) CONTEXT_BY_KEY[String(spec.alias).trim().toLowerCase()] = n;
10593
- CONTEXT_BY_KEY[String(id).trim().toLowerCase()] = n;
10594
- }
10595
- }
10596
- const ALIASES = Object.keys(ALIAS_TO_ID);
10597
- const MODELS = Object.keys(MODEL_CONFIG);
10598
- if (MODELS.length === 0) throw new Error("clodex patch: MODEL_CONFIG is empty");
10599
-
10600
- /** Picker/description label for an identity; falls back to the old wording. */
10601
- function displayFor(identity, fallbackId) {
10602
- return DISPLAY_BY_IDENTITY[identity] || "Custom model (" + fallbackId + ")";
10603
- }
10604
-
10605
- const reEsc = (s) => s.replace(/[.*+?^$\{\}()|[\]\\]/g, "\\$&");
10606
- const q = (s) => JSON.stringify(s); // safe JS string literal
10607
-
10608
- // ---- reporting -------------------------------------------------------------
10609
- const report = [];
10610
- function log(status, name, extra) {
10611
- report.push({ status, name });
10612
- const line = " " + status.padEnd(4) + " " + name + (extra ? " — " + extra : "");
10613
- // NOTE: tweakcc reads this script's STDOUT as the JSON result, so every
10614
- // diagnostic MUST go to stderr — a stray stdout write corrupts the output.
10615
- console.error(line);
10616
- }
10617
-
10618
- /**
10619
- * Apply exactly one regex replacement.
10620
- * - marker: if present in js, treat as already-patched -> SKIP.
10621
- * - expects exactly one match; 0 -> FAIL, >1 -> FAIL (ambiguous).
10622
- * - fn(match, ...groups) returns the replacement text.
10623
- * - required: on FAIL, throw (aborts the whole adhoc-patch).
10624
- */
10625
- function applyOnce(name, regex, fn, { marker, required, noopIsSkip } = {}) {
10626
- if (marker && js.includes(marker)) { log("SKIP", name, "already patched"); return; }
10627
- const g = new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : regex.flags + "g");
10628
- const matches = js.match(g);
10629
- const count = matches ? matches.length : 0;
10630
- if (count === 0) {
10631
- log("FAIL", name, "anchor not found");
10632
- if (required) throw new Error("clodex patch: required patch failed: " + name);
10633
- return;
10634
10588
  }
10635
- if (count > 1) {
10636
- log("FAIL", name, "anchor matched " + count + " times (expected 1)");
10637
- if (required) throw new Error("clodex patch: ambiguous anchor: " + name);
10638
- return;
10589
+ const ALIASES = Object.keys(ALIAS_TO_ID);
10590
+ const MODELS = Object.keys(MODEL_CONFIG);
10591
+ if (MODELS.length === 0) fail("clodex patch: MODEL_CONFIG is empty");
10592
+ function displayFor(identity, fallbackId) {
10593
+ return DISPLAY_BY_IDENTITY[identity] || "Custom model (" + fallbackId + ")";
10639
10594
  }
10640
- const before = js;
10641
- js = js.replace(regex, fn);
10642
- if (js === before) {
10643
- // For array-extend / append patches, "no change" means the aliases are
10644
- // already present (anchor matched, but fn had nothing new to add) -> SKIP.
10645
- if (noopIsSkip) { log("SKIP", name, "already patched"); return; }
10646
- log("FAIL", name, "replacement made no change");
10647
- if (required) throw new Error(name);
10648
- return;
10595
+ const reEsc = (s) => s.replace(/[.*+?^$\{\}()|[\]\\]/g, "\\$&");
10596
+ const q = (s) => JSON.stringify(s);
10597
+ function log12(status, name, extra) {
10598
+ report.push(extra === void 0 ? { status, name } : { status, name, extra });
10649
10599
  }
10650
- log("OK", name);
10651
- }
10652
-
10653
- /** Insert missing identities just before the closing bracket of a JS array literal string. */
10654
- function extendAliasArray(arrLiteral) {
10655
- const toAdd = IDENTITIES.filter((a) => !new RegExp('"' + reEsc(a) + '"').test(arrLiteral));
10656
- if (toAdd.length === 0) return arrLiteral; // idempotent
10657
- return arrLiteral.replace(/\]\s*$/, "," + toAdd.map(q).join(",") + "]");
10658
- }
10659
-
10660
- console.error("clodex patch: injecting model names [" + IDENTITIES.join(", ") + "]");
10661
-
10662
- // ---------------------------------------------------------------------------
10663
- // PATCH 1 — Agent/subagent tool 'model' zod enum.
10664
- // Anchor: .enum([ "sonnet",...,"fable" ]).optional().describe( the array
10665
- // begins with the built-in aliases and is immediately followed by
10666
- // .optional().describe(. We append our identities (alias when defined, else
10667
- // the canonical id) inside the enum so the tool accepts them — this is the same
10668
- // enum subagent/skill 'model:' frontmatter is validated against, which is why
10669
- // the short alias has to be the value that lands here.
10670
- // (This same .describe( is patched by PATCH 4 below.)
10671
- // ---------------------------------------------------------------------------
10672
- applyOnce(
10673
- "PATCH 1: Agent tool model enum",
10674
- /\.enum\((\["sonnet","opus","haiku"(?:,"[^"]+")*\])\)\.optional\(\)\.describe\(/,
10675
- (_m, arr) => ".enum(" + extendAliasArray(arr) + ").optional().describe(",
10676
- { required: true, noopIsSkip: true }
10677
- );
10678
-
10679
- // ---------------------------------------------------------------------------
10680
- // PATCH 3 — known-alias validator list (drives "is this a known alias?").
10681
- // Anchor: the master list literal, matched loosely as
10682
- // ["sonnet","opus","haiku","fable", ...anything... ,"opusplan"] so it
10683
- // tolerates new built-ins being added in the middle. Appending our identities
10684
- // makes them recognized as first-class aliases everywhere the gate runs.
10685
- // ---------------------------------------------------------------------------
10686
- applyOnce(
10687
- "PATCH 3: known-alias validator list",
10688
- /\["sonnet","opus","haiku","fable"(?:,"[^"]+")*,"opusplan"(?:,"[^"]+")*\]/,
10689
- (m) => extendAliasArray(m),
10690
- { required: true, noopIsSkip: true }
10691
- );
10692
-
10693
- // ---------------------------------------------------------------------------
10694
- // PATCH 6 — alias resolver switch (IDENTITY mapping).
10695
- // Anchor: case"best":{ ... } (the case"best":{ is unique). We inject
10696
- // case"<alias>":return"<alias>"; right after it (before the switch's
10697
- // default:return null).
10698
- //
10699
- // The mapping is deliberately an identity, NOT alias -> canonical id: the alias
10700
- // IS the model's identity everywhere else in the patched binary (enum,
10701
- // validator, picker, context table), and the MITM proxy resolves short alias
10702
- // names as request model ids and echoes request bodies unrewritten. Resolving
10703
- // to the canonical id here would make Claude Code send one name and look its
10704
- // context window up under another — the exact mismatch that stopped auto-compact
10705
- // from firing and killed agents with "Prompt is too long". The case still has to
10706
- // EXIST (rather than be skipped) so the resolver returns the name instead of
10707
- // falling through to default:return null.
10708
- // Only aliases not already present are inserted, so a rerun (or a config
10709
- // edit) tops up cleanly rather than duplicating cases.
10710
- // ---------------------------------------------------------------------------
10711
- {
10712
- const missing = ALIASES.filter((a) => !new RegExp("case" + reEsc(q(a)) + ":return").test(js));
10713
- const cases = missing.map((a) => "case" + q(a) + ":return " + q(a) + ";").join("");
10714
- if (ALIASES.length === 0) {
10715
- log("SKIP", "PATCH 6: alias resolver switch", "no aliases configured");
10716
- } else {
10717
- applyOnce(
10718
- "PATCH 6: alias resolver switch",
10719
- /(case"best":\{[^{}]*\})/,
10720
- (m) => m + cases,
10721
- { required: true, noopIsSkip: true }
10722
- );
10600
+ function applyOnce(name, regex, fn, { marker, required, noopIsSkip } = {}) {
10601
+ if (marker && js.includes(marker)) {
10602
+ log12("SKIP", name, "already patched");
10603
+ return;
10604
+ }
10605
+ const g = new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : regex.flags + "g");
10606
+ const matches = js.match(g);
10607
+ const count = matches ? matches.length : 0;
10608
+ if (count === 0) {
10609
+ log12("FAIL", name, "anchor not found");
10610
+ if (required) fail("clodex patch: required patch failed: " + name);
10611
+ return;
10612
+ }
10613
+ if (count > 1) {
10614
+ log12("FAIL", name, "anchor matched " + count + " times (expected 1)");
10615
+ if (required) fail("clodex patch: ambiguous anchor: " + name);
10616
+ return;
10617
+ }
10618
+ const before = js;
10619
+ js = js.replace(regex, fn);
10620
+ if (js === before) {
10621
+ if (noopIsSkip) {
10622
+ log12("SKIP", name, "already patched");
10623
+ return;
10624
+ }
10625
+ log12("FAIL", name, "replacement made no change");
10626
+ if (required) fail(name);
10627
+ return;
10628
+ }
10629
+ log12("OK", name);
10723
10630
  }
10724
- }
10725
-
10726
- // ---------------------------------------------------------------------------
10727
- // PATCH 5 interactive /model picker.
10728
- // The picker is assembled through a single choke-point function; we insert,
10729
- // right after its loop, a snippet that appends our custom
10730
- // {value,label,description} entries with a runtime .some() dedupe guard so
10731
- // it is safe even if the function runs over the same array twice. Only
10732
- // aliases not already injected are added, so reruns top up cleanly.
10733
- // ---------------------------------------------------------------------------
10734
- {
10735
- const missing = ALIASES.filter((a) => !new RegExp("value:" + reEsc(q(a))).test(js));
10736
- const entries = missing
10737
- .map(
10738
- // ASCII only: tweakcc's script->JSON->repack path double-encodes any
10739
- // non-ASCII byte.
10631
+ function extendAliasArray(arrLiteral) {
10632
+ const toAdd = IDENTITIES.filter((a) => !new RegExp('"' + reEsc(a) + '"').test(arrLiteral));
10633
+ if (toAdd.length === 0) return arrLiteral;
10634
+ return arrLiteral.replace(/\]\s*$/, "," + toAdd.map(q).join(",") + "]");
10635
+ }
10636
+ applyOnce(
10637
+ "PATCH 1: Agent tool model enum",
10638
+ /\.enum\((\["sonnet","opus","haiku"(?:,"[^"]+")*\])\)\.optional\(\)\.describe\(/,
10639
+ (_m, arr) => ".enum(" + extendAliasArray(arr) + ").optional().describe(",
10640
+ { required: true, noopIsSkip: true }
10641
+ );
10642
+ applyOnce(
10643
+ "PATCH 3: known-alias validator list",
10644
+ /\["sonnet","opus","haiku","fable"(?:,"[^"]+")*,"opusplan"(?:,"[^"]+")*\]/,
10645
+ (m) => extendAliasArray(m),
10646
+ { required: true, noopIsSkip: true }
10647
+ );
10648
+ {
10649
+ const missing = ALIASES.filter((a) => !new RegExp("case" + reEsc(q(a)) + ":return").test(js));
10650
+ const cases = missing.map((a) => "case" + q(a) + ":return " + q(a) + ";").join("");
10651
+ if (ALIASES.length === 0) {
10652
+ log12("SKIP", "PATCH 6: alias resolver switch", "no aliases configured");
10653
+ } else {
10654
+ applyOnce(
10655
+ "PATCH 6: alias resolver switch",
10656
+ /(case"best":\{[^{}]*\})/,
10657
+ (m) => m + cases,
10658
+ { required: true, noopIsSkip: true }
10659
+ );
10660
+ }
10661
+ }
10662
+ {
10663
+ const missing = ALIASES.filter((a) => !new RegExp("value:" + reEsc(q(a))).test(js));
10664
+ const entries = missing.map(
10740
10665
  // value = the alias (the name the user types and the binary sends);
10741
10666
  // description = the real model label, e.g. "GPT-5.6 Sol (OpenAI (ChatGPT))".
10667
+ // (tweakcc's writeContent round-trips utf8 faithfully — verified — so the
10668
+ // old adhoc-patch ASCII-only constraint no longer applies.)
10742
10669
  (a) => "{value:" + q(a) + ",label:" + q(a.charAt(0).toUpperCase() + a.slice(1)) + ",description:" + q(displayFor(a, ALIAS_TO_ID[a])) + "}"
10743
- )
10744
- .join(",");
10745
- const inject = missing.length
10746
- ? "[" + entries + "].forEach(function(_o){if(!e.some(function(_i){return _i.value===_o.value}))e.push(_o)});"
10747
- : "";
10748
- if (ALIASES.length === 0) {
10749
- log("SKIP", "PATCH 5: model picker options", "no aliases configured");
10750
- } else {
10751
- applyOnce(
10752
- "PATCH 5: model picker options",
10753
- /(\?\[[\w$]+,r\]:\[r\];for\(let [\w$]+ of [\w$]+\)[\w$]+\(e,[\w$]+,t\);)/,
10754
- (m) => m + inject,
10755
- { required: false, noopIsSkip: true }
10756
- );
10670
+ ).join(",");
10671
+ const inject = missing.length ? "[" + entries + "].forEach(function(_o){if(!e.some(function(_i){return _i.value===_o.value}))e.push(_o)});" : "";
10672
+ if (ALIASES.length === 0) {
10673
+ log12("SKIP", "PATCH 5: model picker options", "no aliases configured");
10674
+ } else {
10675
+ applyOnce(
10676
+ "PATCH 5: model picker options",
10677
+ /(\?\[[\w$]+,r\]:\[r\];for\(let [\w$]+ of [\w$]+\)[\w$]+\(e,[\w$]+,t\);)/,
10678
+ (m) => m + inject,
10679
+ { required: false, noopIsSkip: true }
10680
+ );
10681
+ }
10757
10682
  }
10758
- }
10759
-
10760
- // ---------------------------------------------------------------------------
10761
- // PATCH 4 — Agent tool 'model' parameter description text.
10762
- // Append the available model names (with their real labels) before the closing
10763
- // backtick so the model knows which extra names it may request and what they
10764
- // actually are. Best-effort (cosmetic). The text is spliced into a backtick
10765
- // template literal, so backticks and interpolation openers are stripped.
10766
- // ---------------------------------------------------------------------------
10767
- {
10768
- const safe = (s) => String(s).replace(/\`/g, "'").replace(/\$\{/g, "(");
10769
- const listing = IDENTITIES.map(function (i) {
10770
- const d = DISPLAY_BY_IDENTITY[i];
10771
- return d ? safe(i) + " = " + safe(d) : safe(i);
10772
- }).join("; ");
10773
- applyOnce(
10774
- "PATCH 4: Agent tool model description",
10775
- /(describe\(\`Optional model override for this agent[^\`]*?)(\`\))/,
10776
- (_m, body, close) =>
10777
- body.includes("Additional custom models")
10778
- ? body + close
10779
- : body + " Additional custom models: " + listing + "." + close,
10780
- { required: false, noopIsSkip: true }
10781
- );
10782
- }
10783
-
10784
- // ---------------------------------------------------------------------------
10785
- // PATCH 7 — per-model context window.
10786
- //
10787
- // Claude Code funnels EVERY context-window consumer (autocompact threshold,
10788
- // /context, the countdown, statusline, cost/usage records, subagent budgets)
10789
- // through one resolver function. We inject a baked table lookup at the TOP of
10790
- // that resolver, so it wins over the 200k clamp and the global
10791
- // CLAUDE_CODE_MAX_CONTEXT_TOKENS env override. Lookup is on the raw,
10792
- // lowercased model string — alias and id are both in the table, so it hits
10793
- // pre- or post-alias-resolution.
10794
- //
10795
- // Anchor: the resolver's exact body shape. Identifiers are wildcarded (they
10796
- // churn per build); the (e,t) arity + 3-statement shape matches once.
10797
- // ---------------------------------------------------------------------------
10798
- if (Object.keys(CONTEXT_BY_KEY).length) {
10799
- const MARKER = "/*ccpatch:ctx*/";
10800
- const SNIPPET =
10801
- MARKER + 'var _ccw=(' + JSON.stringify(CONTEXT_BY_KEY) + ')[String(e||"").trim().toLowerCase()];if(_ccw!==void 0)return _ccw;';
10802
-
10803
- if (js.includes(MARKER)) {
10804
- // Re-patching an already-patched binary: refresh the baked table in place
10805
- // so a MODEL_CONFIG edit takes effect without a restore first.
10806
- applyOnce(
10807
- "PATCH 7: per-model context window (refresh)",
10808
- /\/\*ccpatch:ctx\*\/var _ccw=\(\{[^{}]*\}\)\[[^\]]*\];if\(_ccw!==void 0\)return _ccw;/,
10809
- () => SNIPPET,
10810
- { required: true, noopIsSkip: true }
10811
- );
10812
- } else {
10683
+ {
10684
+ const safe = (s) => String(s).replace(/`/g, "'").replace(/\$\{/g, "(");
10685
+ const listing = IDENTITIES.map(function(i) {
10686
+ const d = DISPLAY_BY_IDENTITY[i];
10687
+ return d ? safe(i) + " = " + safe(d) : safe(i);
10688
+ }).join("; ");
10813
10689
  applyOnce(
10814
- "PATCH 7: per-model context window",
10815
- /(function [\w$]+\(e,t\)\{)(let [\w$]+=[\w$]+\(\);if\([\w$]+!==void 0\)return [\w$]+;if\([\w$]+\(e,t\)\)return [\w$]+;return [\w$]+\(e,t\)\})/,
10816
- (_m, head, body) => head + SNIPPET + body,
10817
- { required: true }
10690
+ "PATCH 4: Agent tool model description",
10691
+ /(describe\(`Optional model override for this agent[^`]*?)(`\))/,
10692
+ (_m, body, close) => body.includes("Additional custom models") ? body + close : body + " Additional custom models: " + listing + "." + close,
10693
+ { required: false, noopIsSkip: true }
10818
10694
  );
10819
10695
  }
10820
- }
10821
-
10822
- // ---------------------------------------------------------------------------
10823
- const failed = report.filter((r) => r.status === "FAIL");
10824
- const ok = report.filter((r) => r.status === "OK").length;
10825
- const skip = report.filter((r) => r.status === "SKIP").length;
10826
- console.error("clodex patch: " + ok + " applied, " + skip + " skipped, " + failed.length + " failed");
10827
- if (failed.length) console.error("clodex patch: FAILED patches: " + failed.map((f) => f.name).join("; "));
10828
-
10829
- return js;
10830
- `;
10831
- function renderPatchScript(config) {
10832
- const start = TEMPLATE.indexOf(CLODEX_CONFIG_START);
10833
- const end = TEMPLATE.indexOf(CLODEX_CONFIG_END);
10834
- if (start < 0 || end < 0 || end < start) {
10835
- throw new Error("clodex patch: config markers missing from patch script template");
10696
+ if (Object.keys(CONTEXT_BY_KEY).length) {
10697
+ const MARKER = "/*ccpatch:ctx*/";
10698
+ const SNIPPET = MARKER + "var _ccw=(" + JSON.stringify(CONTEXT_BY_KEY) + ')[String(e||"").trim().toLowerCase()];if(_ccw!==void 0)return _ccw;';
10699
+ if (js.includes(MARKER)) {
10700
+ applyOnce(
10701
+ "PATCH 7: per-model context window (refresh)",
10702
+ /\/\*ccpatch:ctx\*\/var _ccw=\(\{[^{}]*\}\)\[[^\]]*\];if\(_ccw!==void 0\)return _ccw;/,
10703
+ () => SNIPPET,
10704
+ { required: true, noopIsSkip: true }
10705
+ );
10706
+ } else {
10707
+ applyOnce(
10708
+ "PATCH 7: per-model context window",
10709
+ /(function [\w$]+\(e,t\)\{)(let [\w$]+=[\w$]+\(\);if\([\w$]+!==void 0\)return [\w$]+;if\([\w$]+\(e,t\)\)return [\w$]+;return [\w$]+\(e,t\)\})/,
10710
+ (_m, head, body) => head + SNIPPET + body,
10711
+ { required: true }
10712
+ );
10713
+ }
10836
10714
  }
10837
- const block = `${CLODEX_CONFIG_START}
10838
- const MODEL_CONFIG = ${JSON.stringify(config, null, 2)};
10839
- ${CLODEX_CONFIG_END}`;
10840
- return TEMPLATE.slice(0, start) + block + TEMPLATE.slice(end + CLODEX_CONFIG_END.length);
10715
+ return { content: js, results: report };
10841
10716
  }
10842
10717
 
10843
10718
  // src/patcher.ts
@@ -10988,29 +10863,16 @@ function pristineBackupPath(version, binaryPath) {
10988
10863
  const tag = version.replace(/[^\w.-]+/g, "_") || basename(binaryPath);
10989
10864
  return join6(backupDir(), `claude-${tag}.orig`);
10990
10865
  }
10991
- function runTweakcc(binaryPath, scriptPath) {
10992
- return new Promise((resolve2, reject) => {
10993
- const child = spawn(
10994
- "npx",
10995
- ["-y", "tweakcc", "adhoc-patch", "--path", binaryPath, "--script", `@${scriptPath}`, "--confirm-possible-dangerous-patch"],
10996
- {
10997
- stdio: ["ignore", "pipe", "pipe"],
10998
- env: { ...process.env, TWEAKCC_CC_INSTALLATION_PATH: binaryPath }
10999
- }
11000
- );
11001
- let output = "";
11002
- child.stdout.on("data", (chunk) => {
11003
- output += String(chunk);
11004
- });
11005
- child.stderr.on("data", (chunk) => {
11006
- output += String(chunk);
11007
- });
11008
- child.on("error", reject);
11009
- child.on("close", (code) => resolve2({ code: code ?? 1, output }));
11010
- });
11011
- }
11012
- function summarizePatchOutput(output) {
11013
- return output.split(/\r?\n/).filter((line) => /^\s*(OK|SKIP|FAIL)\s{2,}/.test(line) || /^clodex patch:/.test(line.trim())).map((line) => line.trimEnd());
10866
+ function summarizePatchResults(results) {
10867
+ const lines = results.map(formatPatchSiteLine);
10868
+ const ok = results.filter((r) => r.status === "OK").length;
10869
+ const skip = results.filter((r) => r.status === "SKIP").length;
10870
+ const failed = results.filter((r) => r.status === "FAIL");
10871
+ lines.push(`clodex patch: ${ok} applied, ${skip} skipped, ${failed.length} failed`);
10872
+ if (failed.length) {
10873
+ lines.push(`clodex patch: FAILED patches: ${failed.map((f) => f.name).join("; ")}`);
10874
+ }
10875
+ return lines;
11014
10876
  }
11015
10877
  async function applyPatch(binaryPath, version, desired, configHash, opts) {
11016
10878
  const backup = pristineBackupPath(version, binaryPath);
@@ -11024,41 +10886,48 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
11024
10886
  copyFileSync2(binaryPath, backup);
11025
10887
  }
11026
10888
  copyFileSync2(backup, join6(backupDir(), "native-binary.backup"));
11027
- const scriptPath = join6(tmpdir(), `clodex-patch-${process.pid}-${Date.now()}.js`);
11028
- writeFileSync5(scriptPath, renderPatchScript(desired.config), { encoding: "utf8", mode: 384 });
10889
+ const { tryDetectInstallation, readContent, writeContent } = await import("tweakcc");
10890
+ let results;
11029
10891
  try {
11030
- const result = await runTweakcc(binaryPath, scriptPath);
11031
- if (opts.trace) {
11032
- process.stderr.write(result.output);
11033
- }
11034
- if (result.code !== 0) {
11035
- return {
11036
- ok: false,
11037
- message: `tweakcc adhoc-patch failed (exit ${result.code}). Re-run with --trace for full output.`,
11038
- detailLines: summarizePatchOutput(result.output)
11039
- };
10892
+ const installation = await tryDetectInstallation({ path: binaryPath });
10893
+ const source = await readContent(installation);
10894
+ const patched = applyClodexPatches(source, desired.config);
10895
+ results = patched.results;
10896
+ await writeContent(installation, patched.content);
10897
+ } catch (err) {
10898
+ const detailLines = err instanceof PatchApplyError ? summarizePatchResults(err.results) : [];
10899
+ if (opts.trace && detailLines.length) {
10900
+ process.stderr.write(`${detailLines.join("\n")}
10901
+ `);
11040
10902
  }
11041
- const manifest = {
11042
- binaryPath,
11043
- claudeVersion: version,
11044
- configHash,
11045
- patchedSize: statSync2(binaryPath).size,
11046
- patchedSha256: sha256File(binaryPath),
11047
- backupPath: backup,
11048
- patchedAt: (/* @__PURE__ */ new Date()).toISOString()
11049
- };
11050
- writePatchManifest(manifest);
11051
- const modelCount = Object.keys(desired.config).length;
11052
- const aliasCount = Object.values(desired.config).filter((entry) => entry.alias).length;
11053
- const windowCount = Object.values(desired.config).filter((entry) => entry.context).length;
11054
10903
  return {
11055
- ok: true,
11056
- message: `Patched claude ${version}: ${modelCount} model${modelCount === 1 ? "" : "s"}, ${aliasCount} alias${aliasCount === 1 ? "" : "es"}, ${windowCount} context window${windowCount === 1 ? "" : "s"}.`,
11057
- detailLines: summarizePatchOutput(result.output)
10904
+ ok: false,
10905
+ message: `Patch failed: ${err instanceof Error ? err.message : String(err)}`,
10906
+ detailLines
11058
10907
  };
11059
- } finally {
11060
- rmSync(scriptPath, { force: true });
11061
10908
  }
10909
+ if (opts.trace) {
10910
+ process.stderr.write(`${summarizePatchResults(results).join("\n")}
10911
+ `);
10912
+ }
10913
+ const manifest = {
10914
+ binaryPath,
10915
+ claudeVersion: version,
10916
+ configHash,
10917
+ patchedSize: statSync2(binaryPath).size,
10918
+ patchedSha256: sha256File(binaryPath),
10919
+ backupPath: backup,
10920
+ patchedAt: (/* @__PURE__ */ new Date()).toISOString()
10921
+ };
10922
+ writePatchManifest(manifest);
10923
+ const modelCount = Object.keys(desired.config).length;
10924
+ const aliasCount = Object.values(desired.config).filter((entry) => entry.alias).length;
10925
+ const windowCount = Object.values(desired.config).filter((entry) => entry.context).length;
10926
+ return {
10927
+ ok: true,
10928
+ message: `Patched claude ${version}: ${modelCount} model${modelCount === 1 ? "" : "s"}, ${aliasCount} alias${aliasCount === 1 ? "" : "es"}, ${windowCount} context window${windowCount === 1 ? "" : "s"}.`,
10929
+ detailLines: summarizePatchResults(results)
10930
+ };
11062
10931
  }
11063
10932
  async function runPatchCommand(opts = {}) {
11064
10933
  const resolved = resolveClaudeBinaryForPatch();
@@ -11609,7 +11478,7 @@ ${pc13.bold("Usage:")}
11609
11478
 
11610
11479
  ${pc13.bold("Options:")}
11611
11480
  --restore Restore the pristine (unpatched) Claude Code binary
11612
- --trace Show the underlying tweakcc output
11481
+ --trace Show per-patch-site results (OK/SKIP/FAIL)
11613
11482
 
11614
11483
  ${pc13.bold("Behavior:")}
11615
11484
  The patch map is built automatically from your clodex favorites and aliases