@bman654/clodex 1.0.0 → 1.0.3

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.3",
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
  },
@@ -7091,13 +7092,24 @@ function translateMessages(messages, npm, openAiPromptCacheBreakpoints = false)
7091
7092
  }
7092
7093
  return out;
7093
7094
  }
7094
- function stripNullInputs(input) {
7095
+ function sanitizeToolInput(input, requiredProps) {
7095
7096
  const out = {};
7096
7097
  for (const [k, v] of Object.entries(input)) {
7097
- if (v !== null) out[k] = v;
7098
+ if (v === null) continue;
7099
+ if (Array.isArray(v) && v.length === 0 && !requiredProps?.has(k)) continue;
7100
+ out[k] = v;
7098
7101
  }
7099
7102
  return out;
7100
7103
  }
7104
+ function toolRequiredProps(tools) {
7105
+ const map = /* @__PURE__ */ new Map();
7106
+ for (const [name, t] of Object.entries(tools ?? {})) {
7107
+ const schema = t.inputSchema?.jsonSchema;
7108
+ const required = Array.isArray(schema?.required) ? schema.required : [];
7109
+ map.set(name, new Set(required.filter((r) => typeof r === "string")));
7110
+ }
7111
+ return map;
7112
+ }
7101
7113
  function translateTools(anthropicTools) {
7102
7114
  if (!anthropicTools?.length) return void 0;
7103
7115
  const tools = {};
@@ -7207,13 +7219,17 @@ function forwardAbortSignal(source, target) {
7207
7219
  source.addEventListener("abort", forward, { once: true });
7208
7220
  return () => source.removeEventListener("abort", forward);
7209
7221
  }
7210
- async function writeAnthropicStream(stream, modelId, write, log12, observer) {
7222
+ async function writeAnthropicStream(stream, modelId, write, log12, observer, tools) {
7211
7223
  const messageId = "msg_" + Date.now();
7224
+ const requiredProps = toolRequiredProps(tools);
7212
7225
  let blockIndex = -1;
7213
7226
  let started = false;
7214
7227
  let openType = null;
7215
7228
  let pendingThinkingSig;
7216
7229
  const idToBlock = /* @__PURE__ */ new Map();
7230
+ const toolJsonBuffer = /* @__PURE__ */ new Map();
7231
+ const flushedTools = /* @__PURE__ */ new Set();
7232
+ let openToolId = null;
7217
7233
  let finishReason = "end_turn";
7218
7234
  let usage = {
7219
7235
  input_tokens: 0,
@@ -7253,8 +7269,20 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer) {
7253
7269
  });
7254
7270
  pendingThinkingSig = void 0;
7255
7271
  }
7272
+ if (openType === "tool" && openToolId !== null && !flushedTools.has(openToolId)) {
7273
+ const buffered = toolJsonBuffer.get(openToolId);
7274
+ if (buffered) {
7275
+ emit("content_block_delta", {
7276
+ type: "content_block_delta",
7277
+ index: blockIndex,
7278
+ delta: { type: "input_json_delta", partial_json: buffered }
7279
+ });
7280
+ }
7281
+ flushedTools.add(openToolId);
7282
+ }
7256
7283
  if (openType) emit("content_block_stop", { type: "content_block_stop", index: blockIndex });
7257
7284
  openType = null;
7285
+ openToolId = null;
7258
7286
  };
7259
7287
  const openBlock = (type, contentBlock) => {
7260
7288
  ensureStart();
@@ -7316,32 +7344,45 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer) {
7316
7344
  input: {}
7317
7345
  });
7318
7346
  idToBlock.set(part.id ?? "", blockIndex);
7347
+ openToolId = part.id ?? "";
7319
7348
  break;
7320
7349
  }
7321
- case "tool-input-delta":
7322
- emit("content_block_delta", {
7323
- type: "content_block_delta",
7324
- index: idToBlock.get(part.id ?? "") ?? blockIndex,
7325
- delta: { type: "input_json_delta", partial_json: part.delta ?? part.text ?? "" }
7326
- });
7350
+ case "tool-input-delta": {
7351
+ const id = part.id ?? "";
7352
+ toolJsonBuffer.set(id, (toolJsonBuffer.get(id) ?? "") + (part.delta ?? part.text ?? ""));
7327
7353
  break;
7354
+ }
7328
7355
  case "tool-input-end":
7329
7356
  break;
7330
7357
  case "tool-call": {
7331
7358
  finishReason = "tool_use";
7332
- if (!idToBlock.has(part.toolCallId ?? "") && openType !== "tool") {
7359
+ const id = part.toolCallId ?? "";
7360
+ if (idToBlock.has(id)) {
7361
+ if (!flushedTools.has(id)) {
7362
+ const json = part.input !== void 0 && part.input !== null ? JSON.stringify(sanitizeToolInput(part.input, requiredProps.get(part.toolName ?? ""))) : toolJsonBuffer.get(id) ?? "";
7363
+ if (json) {
7364
+ emit("content_block_delta", {
7365
+ type: "content_block_delta",
7366
+ index: idToBlock.get(id) ?? blockIndex,
7367
+ delta: { type: "input_json_delta", partial_json: json }
7368
+ });
7369
+ }
7370
+ flushedTools.add(id);
7371
+ }
7372
+ } else if (openType !== "tool") {
7333
7373
  const sig = grabRoundTripSignature(part);
7334
7374
  openBlock("tool", {
7335
7375
  type: "tool_use",
7336
- id: encodeToolUseId(part.toolCallId ?? "", sig),
7376
+ id: encodeToolUseId(id, sig),
7337
7377
  name: part.toolName,
7338
7378
  input: {}
7339
7379
  });
7340
7380
  emit("content_block_delta", {
7341
7381
  type: "content_block_delta",
7342
7382
  index: blockIndex,
7343
- delta: { type: "input_json_delta", partial_json: JSON.stringify(stripNullInputs(part.input ?? {})) }
7383
+ delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeToolInput(part.input ?? {}, requiredProps.get(part.toolName ?? ""))) }
7344
7384
  });
7385
+ flushedTools.add(id);
7345
7386
  }
7346
7387
  break;
7347
7388
  }
@@ -7406,7 +7447,7 @@ async function streamAnthropicResponse(model, params, modelId, write, log12, obs
7406
7447
  }
7407
7448
  })();
7408
7449
  try {
7409
- await writeAnthropicStream(watchedStream, modelId, write, log12, { ...observer, abortSignal });
7450
+ await writeAnthropicStream(watchedStream, modelId, write, log12, { ...observer, abortSignal }, params.tools);
7410
7451
  } finally {
7411
7452
  stopForwardingAbort();
7412
7453
  clearTimeout(idleTimer);
@@ -7497,6 +7538,7 @@ async function generateAnthropicResponse(model, params, modelId, options) {
7497
7538
  if (!generateAbort.signal.aborted) generateAbort.abort();
7498
7539
  }
7499
7540
  }
7541
+ const requiredProps = toolRequiredProps(params.tools);
7500
7542
  return {
7501
7543
  id: "msg_" + Date.now(),
7502
7544
  type: "message",
@@ -7508,7 +7550,7 @@ async function generateAnthropicResponse(model, params, modelId, options) {
7508
7550
  type: "tool_use",
7509
7551
  id: encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc)),
7510
7552
  name: tc.toolName,
7511
- input: stripNullInputs(tc.input)
7553
+ input: sanitizeToolInput(tc.input ?? {}, requiredProps.get(tc.toolName))
7512
7554
  }))
7513
7555
  ],
7514
7556
  stop_reason: finishReason === "tool-calls" ? "tool_use" : "end_turn",
@@ -10516,14 +10558,12 @@ function planLaunchWizard(opts) {
10516
10558
  }
10517
10559
 
10518
10560
  // src/patcher.ts
10519
- import { spawn } from "child_process";
10520
10561
  import { createHash as createHash5 } from "crypto";
10521
10562
  import {
10522
10563
  copyFileSync as copyFileSync2,
10523
10564
  existsSync as existsSync6,
10524
10565
  mkdirSync as mkdirSync6,
10525
10566
  readFileSync as readFileSync7,
10526
- rmSync,
10527
10567
  statSync as statSync2,
10528
10568
  unlinkSync as unlinkSync2,
10529
10569
  writeFileSync as writeFileSync5,
@@ -10531,313 +10571,189 @@ import {
10531
10571
  closeSync as closeSync3,
10532
10572
  realpathSync
10533
10573
  } from "fs";
10534
- import { homedir as homedir2, tmpdir } from "os";
10574
+ import { homedir as homedir2 } from "os";
10535
10575
  import { basename, join as join6 } from "path";
10536
10576
  import pc12 from "picocolors";
10537
10577
  import * as p11 from "@clack/prompts";
10538
10578
 
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);
10579
+ // src/patch-transforms.ts
10580
+ var PatchApplyError = class extends Error {
10581
+ results;
10582
+ constructor(message, results) {
10583
+ super(message);
10584
+ this.name = "PatchApplyError";
10585
+ this.results = results;
10577
10586
  }
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);
10587
+ };
10588
+ function formatPatchSiteLine(result) {
10589
+ return " " + result.status.padEnd(4) + " " + result.name + (result.extra ? " \u2014 " + result.extra : "");
10590
+ }
10591
+ function applyClodexPatches(source, config) {
10592
+ let js = source;
10593
+ const MODEL_CONFIG = config;
10594
+ const ALIAS_TO_ID = {};
10595
+ const IDENTITIES = [];
10596
+ const DISPLAY_BY_IDENTITY = {};
10597
+ const CONTEXT_BY_KEY = {};
10598
+ const report = [];
10599
+ const fail = (message) => {
10600
+ throw new PatchApplyError(message, report);
10601
+ };
10602
+ for (const [id, value] of Object.entries(MODEL_CONFIG)) {
10603
+ const spec = value && typeof value === "object" ? value : { alias: value };
10604
+ if (spec.alias !== void 0) {
10605
+ const a = String(spec.alias).trim().toLowerCase();
10606
+ if (!/^[a-z0-9][a-z0-9._-]*(\[1m\])?$/.test(a)) {
10607
+ fail('clodex patch: alias "' + spec.alias + '" is not a safe lowercase alias');
10608
+ }
10609
+ ALIAS_TO_ID[a] = String(id);
10610
+ IDENTITIES.push(a);
10611
+ if (spec.display) DISPLAY_BY_IDENTITY[a] = String(spec.display);
10612
+ } else {
10613
+ IDENTITIES.push(String(id));
10614
+ if (spec.display) DISPLAY_BY_IDENTITY[String(id)] = String(spec.display);
10583
10615
  }
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
- );
10616
+ if (spec.context !== void 0) {
10617
+ const n = Number(spec.context);
10618
+ if (!Number.isInteger(n) || n <= 0) {
10619
+ fail('clodex patch: context for "' + id + '" must be a positive integer, got ' + spec.context);
10620
+ }
10621
+ if (/\[1m\]/i.test(String(spec.alias ?? "")) || /\[1m\]/i.test(id)) {
10622
+ fail(
10623
+ 'clodex patch: "' + id + '" sets context but keeps the [1m] suffix \u2014 drop the suffix from both the id and the alias'
10624
+ );
10625
+ }
10626
+ if (spec.alias !== void 0) CONTEXT_BY_KEY[String(spec.alias).trim().toLowerCase()] = n;
10627
+ CONTEXT_BY_KEY[String(id).trim().toLowerCase()] = n;
10591
10628
  }
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
10629
  }
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;
10630
+ const ALIASES = Object.keys(ALIAS_TO_ID);
10631
+ const MODELS = Object.keys(MODEL_CONFIG);
10632
+ if (MODELS.length === 0) fail("clodex patch: MODEL_CONFIG is empty");
10633
+ function displayFor(identity, fallbackId) {
10634
+ return DISPLAY_BY_IDENTITY[identity] || "Custom model (" + fallbackId + ")";
10639
10635
  }
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;
10636
+ const reEsc = (s) => s.replace(/[.*+?^$\{\}()|[\]\\]/g, "\\$&");
10637
+ const q = (s) => JSON.stringify(s);
10638
+ function log12(status, name, extra) {
10639
+ report.push(extra === void 0 ? { status, name } : { status, name, extra });
10649
10640
  }
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
- );
10641
+ function applyOnce(name, regex, fn, { marker, required, noopIsSkip } = {}) {
10642
+ if (marker && js.includes(marker)) {
10643
+ log12("SKIP", name, "already patched");
10644
+ return;
10645
+ }
10646
+ const g = new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : regex.flags + "g");
10647
+ const matches = js.match(g);
10648
+ const count = matches ? matches.length : 0;
10649
+ if (count === 0) {
10650
+ log12("FAIL", name, "anchor not found");
10651
+ if (required) fail("clodex patch: required patch failed: " + name);
10652
+ return;
10653
+ }
10654
+ if (count > 1) {
10655
+ log12("FAIL", name, "anchor matched " + count + " times (expected 1)");
10656
+ if (required) fail("clodex patch: ambiguous anchor: " + name);
10657
+ return;
10658
+ }
10659
+ const before = js;
10660
+ js = js.replace(regex, fn);
10661
+ if (js === before) {
10662
+ if (noopIsSkip) {
10663
+ log12("SKIP", name, "already patched");
10664
+ return;
10665
+ }
10666
+ log12("FAIL", name, "replacement made no change");
10667
+ if (required) fail(name);
10668
+ return;
10669
+ }
10670
+ log12("OK", name);
10723
10671
  }
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.
10672
+ function extendAliasArray(arrLiteral) {
10673
+ const toAdd = IDENTITIES.filter((a) => !new RegExp('"' + reEsc(a) + '"').test(arrLiteral));
10674
+ if (toAdd.length === 0) return arrLiteral;
10675
+ return arrLiteral.replace(/\]\s*$/, "," + toAdd.map(q).join(",") + "]");
10676
+ }
10677
+ applyOnce(
10678
+ "PATCH 1: Agent tool model enum",
10679
+ /\.enum\((\["sonnet","opus","haiku"(?:,"[^"]+")*\])\)\.optional\(\)\.describe\(/,
10680
+ (_m, arr) => ".enum(" + extendAliasArray(arr) + ").optional().describe(",
10681
+ { required: true, noopIsSkip: true }
10682
+ );
10683
+ applyOnce(
10684
+ "PATCH 3: known-alias validator list",
10685
+ /\["sonnet","opus","haiku","fable"(?:,"[^"]+")*,"opusplan"(?:,"[^"]+")*\]/,
10686
+ (m) => extendAliasArray(m),
10687
+ { required: true, noopIsSkip: true }
10688
+ );
10689
+ {
10690
+ const missing = ALIASES.filter((a) => !new RegExp("case" + reEsc(q(a)) + ":return").test(js));
10691
+ const cases = missing.map((a) => "case" + q(a) + ":return " + q(a) + ";").join("");
10692
+ if (ALIASES.length === 0) {
10693
+ log12("SKIP", "PATCH 6: alias resolver switch", "no aliases configured");
10694
+ } else {
10695
+ applyOnce(
10696
+ "PATCH 6: alias resolver switch",
10697
+ /(case"best":\{[^{}]*\})/,
10698
+ (m) => m + cases,
10699
+ { required: true, noopIsSkip: true }
10700
+ );
10701
+ }
10702
+ }
10703
+ {
10704
+ const missing = ALIASES.filter((a) => !new RegExp("value:" + reEsc(q(a))).test(js));
10705
+ const entries = missing.map(
10740
10706
  // value = the alias (the name the user types and the binary sends);
10741
10707
  // description = the real model label, e.g. "GPT-5.6 Sol (OpenAI (ChatGPT))".
10708
+ // (tweakcc's writeContent round-trips utf8 faithfully — verified — so the
10709
+ // old adhoc-patch ASCII-only constraint no longer applies.)
10742
10710
  (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
- );
10711
+ ).join(",");
10712
+ const inject = missing.length ? "[" + entries + "].forEach(function(_o){if(!e.some(function(_i){return _i.value===_o.value}))e.push(_o)});" : "";
10713
+ if (ALIASES.length === 0) {
10714
+ log12("SKIP", "PATCH 5: model picker options", "no aliases configured");
10715
+ } else {
10716
+ applyOnce(
10717
+ "PATCH 5: model picker options",
10718
+ /(\?\[[\w$]+,r\]:\[r\];for\(let [\w$]+ of [\w$]+\)[\w$]+\(e,[\w$]+,t\);)/,
10719
+ (m) => m + inject,
10720
+ { required: false, noopIsSkip: true }
10721
+ );
10722
+ }
10757
10723
  }
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 {
10724
+ {
10725
+ const safe = (s) => String(s).replace(/`/g, "'").replace(/\$\{/g, "(");
10726
+ const listing = IDENTITIES.map(function(i) {
10727
+ const d = DISPLAY_BY_IDENTITY[i];
10728
+ return d ? safe(i) + " = " + safe(d) : safe(i);
10729
+ }).join("; ");
10813
10730
  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 }
10731
+ "PATCH 4: Agent tool model description",
10732
+ /(describe\(`Optional model override for this agent[^`]*?)(`\))/,
10733
+ (_m, body, close) => body.includes("Additional custom models") ? body + close : body + " Additional custom models: " + listing + "." + close,
10734
+ { required: false, noopIsSkip: true }
10818
10735
  );
10819
10736
  }
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");
10737
+ if (Object.keys(CONTEXT_BY_KEY).length) {
10738
+ const MARKER = "/*ccpatch:ctx*/";
10739
+ const SNIPPET = MARKER + "var _ccw=(" + JSON.stringify(CONTEXT_BY_KEY) + ')[String(e||"").trim().toLowerCase()];if(_ccw!==void 0)return _ccw;';
10740
+ if (js.includes(MARKER)) {
10741
+ applyOnce(
10742
+ "PATCH 7: per-model context window (refresh)",
10743
+ /\/\*ccpatch:ctx\*\/var _ccw=\(\{[^{}]*\}\)\[[^\]]*\];if\(_ccw!==void 0\)return _ccw;/,
10744
+ () => SNIPPET,
10745
+ { required: true, noopIsSkip: true }
10746
+ );
10747
+ } else {
10748
+ applyOnce(
10749
+ "PATCH 7: per-model context window",
10750
+ /(function [\w$]+\(e,t\)\{)(let [\w$]+=[\w$]+\(\);if\([\w$]+!==void 0\)return [\w$]+;if\([\w$]+\(e,t\)\)return [\w$]+;return [\w$]+\(e,t\)\})/,
10751
+ (_m, head, body) => head + SNIPPET + body,
10752
+ { required: true }
10753
+ );
10754
+ }
10836
10755
  }
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);
10756
+ return { content: js, results: report };
10841
10757
  }
10842
10758
 
10843
10759
  // src/patcher.ts
@@ -10988,29 +10904,16 @@ function pristineBackupPath(version, binaryPath) {
10988
10904
  const tag = version.replace(/[^\w.-]+/g, "_") || basename(binaryPath);
10989
10905
  return join6(backupDir(), `claude-${tag}.orig`);
10990
10906
  }
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());
10907
+ function summarizePatchResults(results) {
10908
+ const lines = results.map(formatPatchSiteLine);
10909
+ const ok = results.filter((r) => r.status === "OK").length;
10910
+ const skip = results.filter((r) => r.status === "SKIP").length;
10911
+ const failed = results.filter((r) => r.status === "FAIL");
10912
+ lines.push(`clodex patch: ${ok} applied, ${skip} skipped, ${failed.length} failed`);
10913
+ if (failed.length) {
10914
+ lines.push(`clodex patch: FAILED patches: ${failed.map((f) => f.name).join("; ")}`);
10915
+ }
10916
+ return lines;
11014
10917
  }
11015
10918
  async function applyPatch(binaryPath, version, desired, configHash, opts) {
11016
10919
  const backup = pristineBackupPath(version, binaryPath);
@@ -11024,41 +10927,48 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
11024
10927
  copyFileSync2(binaryPath, backup);
11025
10928
  }
11026
10929
  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 });
10930
+ const { tryDetectInstallation, readContent, writeContent } = await import("tweakcc");
10931
+ let results;
11029
10932
  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
- };
10933
+ const installation = await tryDetectInstallation({ path: binaryPath });
10934
+ const source = await readContent(installation);
10935
+ const patched = applyClodexPatches(source, desired.config);
10936
+ results = patched.results;
10937
+ await writeContent(installation, patched.content);
10938
+ } catch (err) {
10939
+ const detailLines = err instanceof PatchApplyError ? summarizePatchResults(err.results) : [];
10940
+ if (opts.trace && detailLines.length) {
10941
+ process.stderr.write(`${detailLines.join("\n")}
10942
+ `);
11040
10943
  }
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
10944
  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)
10945
+ ok: false,
10946
+ message: `Patch failed: ${err instanceof Error ? err.message : String(err)}`,
10947
+ detailLines
11058
10948
  };
11059
- } finally {
11060
- rmSync(scriptPath, { force: true });
11061
10949
  }
10950
+ if (opts.trace) {
10951
+ process.stderr.write(`${summarizePatchResults(results).join("\n")}
10952
+ `);
10953
+ }
10954
+ const manifest = {
10955
+ binaryPath,
10956
+ claudeVersion: version,
10957
+ configHash,
10958
+ patchedSize: statSync2(binaryPath).size,
10959
+ patchedSha256: sha256File(binaryPath),
10960
+ backupPath: backup,
10961
+ patchedAt: (/* @__PURE__ */ new Date()).toISOString()
10962
+ };
10963
+ writePatchManifest(manifest);
10964
+ const modelCount = Object.keys(desired.config).length;
10965
+ const aliasCount = Object.values(desired.config).filter((entry) => entry.alias).length;
10966
+ const windowCount = Object.values(desired.config).filter((entry) => entry.context).length;
10967
+ return {
10968
+ ok: true,
10969
+ message: `Patched claude ${version}: ${modelCount} model${modelCount === 1 ? "" : "s"}, ${aliasCount} alias${aliasCount === 1 ? "" : "es"}, ${windowCount} context window${windowCount === 1 ? "" : "s"}.`,
10970
+ detailLines: summarizePatchResults(results)
10971
+ };
11062
10972
  }
11063
10973
  async function runPatchCommand(opts = {}) {
11064
10974
  const resolved = resolveClaudeBinaryForPatch();
@@ -11609,7 +11519,7 @@ ${pc13.bold("Usage:")}
11609
11519
 
11610
11520
  ${pc13.bold("Options:")}
11611
11521
  --restore Restore the pristine (unpatched) Claude Code binary
11612
- --trace Show the underlying tweakcc output
11522
+ --trace Show per-patch-site results (OK/SKIP/FAIL)
11613
11523
 
11614
11524
  ${pc13.bold("Behavior:")}
11615
11525
  The patch map is built automatically from your clodex favorites and aliases