@bman654/clodex 2.12.0 → 2.12.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/README.md +6 -3
- package/dist/cli.js +270 -35
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -198,9 +198,9 @@ Patch the installed Claude Code binary so clodex favorites and aliases are first
|
|
|
198
198
|
| `--disable-local-patches` | Disable local patches and rebuild from pristine bytes without them |
|
|
199
199
|
| `--help` | Help |
|
|
200
200
|
|
|
201
|
-
On Windows, when Claude Code was installed with `npm install -g`, the `claude` on your PATH is a small launcher script (`claude.cmd`, `claude.ps1`, and an extensionless one) rather than the program itself; `clodex patch` follows it to the program and patches that. (On macOS and Linux npm makes a symlink instead, which always worked.) If the launcher cannot be followed — its program moved or removed, or it names two different programs — `clodex patch` stops without touching anything and tells you to set `
|
|
201
|
+
On Windows, when Claude Code was installed with `npm install -g`, the `claude` on your PATH is a small launcher script (`claude.cmd`, `claude.ps1`, and an extensionless one) rather than the program itself; `clodex patch` follows it to the program and patches that. (On macOS and Linux npm makes a symlink instead, which always worked.) If the launcher cannot be followed — its program moved or removed, or it names two different programs — `clodex patch` stops without touching anything and tells you to set `TWEAKCC_CC_INSTALLATION_PATH` to the program directly.
|
|
202
202
|
|
|
203
|
-
The patch map is built from your favorites and aliases; context windows come from provider metadata. A pristine per-version backup is kept, and a manifest (`~/.clodex/patch-state.json`) makes re-runs no-ops until your config or Claude Code version changes — then the binary is restored first and re-patched fresh. `clodex claude` checks patch freshness at launch and offers to re-patch (a non-blocking notice when not interactive). Re-run `clodex patch` after every `claude` update.
|
|
203
|
+
The patch map is built from your favorites and aliases; context windows come from provider metadata. A pristine per-version backup is kept — recorded against the install it was made for, so a machine with two installs of one Claude Code version restores each from its own bytes — and a manifest (`~/.clodex/patch-state.json`) makes re-runs no-ops until your config or Claude Code version changes — then the binary is restored first and re-patched fresh. `clodex claude` checks patch freshness at launch and offers to re-patch (a non-blocking notice when not interactive). Re-run `clodex patch` after every `claude` update.
|
|
204
204
|
|
|
205
205
|
#### Local patches (trusted code)
|
|
206
206
|
|
|
@@ -337,7 +337,10 @@ clodex --version # version
|
|
|
337
337
|
`CLODEX_CREDENTIAL_HELPER` to an absolute executable path to use an external
|
|
338
338
|
secure store instead; see [credential helpers](docs/credential-helpers.md).
|
|
339
339
|
- Proxied routes forward configured provider headers for API-key and OAuth authentication. Anonymous routes preserve non-credential headers while removing authorization, API-key, cookie, token, secret, and credential-bearing header names before dispatch.
|
|
340
|
-
- `CLODEX_CLAUDE_PATH` overrides Claude Code
|
|
340
|
+
- `CLODEX_CLAUDE_PATH` overrides which Claude Code gets **launched**. It does not choose what
|
|
341
|
+
`clodex patch` writes to — set `TWEAKCC_CC_INSTALLATION_PATH` for that, and `clodex patch`
|
|
342
|
+
says so when `CLODEX_CLAUDE_PATH` points somewhere else. The two are separate because a
|
|
343
|
+
`CLODEX_CLAUDE_PATH` aimed at a wrapper script is right for launching and wrong for patching.
|
|
341
344
|
- **Codex service tier:** `CLODEX_SERVICE_TIER` accepts `fast` (normalized to
|
|
342
345
|
`priority`), `priority`, `flex`, `auto`, or `default`. Clodex requests the
|
|
343
346
|
resolved value only after selecting a ChatGPT/Codex OAuth route; OpenAI
|
package/dist/cli.js
CHANGED
|
@@ -382,7 +382,7 @@ import { join } from "path";
|
|
|
382
382
|
// package.json
|
|
383
383
|
var package_default = {
|
|
384
384
|
name: "@bman654/clodex",
|
|
385
|
-
version: "2.12.
|
|
385
|
+
version: "2.12.1",
|
|
386
386
|
publishConfig: {
|
|
387
387
|
access: "public"
|
|
388
388
|
},
|
|
@@ -3884,16 +3884,17 @@ function applyClodexPatches(source, config) {
|
|
|
3884
3884
|
// src/patcher.ts
|
|
3885
3885
|
import { createHash as createHash9 } from "crypto";
|
|
3886
3886
|
import {
|
|
3887
|
+
chmodSync as chmodSync4,
|
|
3887
3888
|
copyFileSync,
|
|
3888
3889
|
existsSync as existsSync5,
|
|
3889
3890
|
mkdtempSync,
|
|
3890
3891
|
mkdirSync as mkdirSync5,
|
|
3891
3892
|
readFileSync as readFileSync10,
|
|
3892
|
-
renameSync as
|
|
3893
|
-
rmSync,
|
|
3893
|
+
renameSync as renameSync3,
|
|
3894
|
+
rmSync as rmSync2,
|
|
3894
3895
|
statSync as statSync8,
|
|
3895
3896
|
unlinkSync as unlinkSync3,
|
|
3896
|
-
writeFileSync as
|
|
3897
|
+
writeFileSync as writeFileSync6,
|
|
3897
3898
|
openSync as openSync7,
|
|
3898
3899
|
closeSync as closeSync7,
|
|
3899
3900
|
realpathSync as realpathSync2
|
|
@@ -7772,7 +7773,7 @@ function writeAll(fd, bytes, position, path) {
|
|
|
7772
7773
|
|
|
7773
7774
|
// src/patch-backup.ts
|
|
7774
7775
|
import { createHash as createHash5 } from "crypto";
|
|
7775
|
-
import { existsSync as existsSync4, readFileSync as readFileSync9, readdirSync, statSync as statSync7 } from "fs";
|
|
7776
|
+
import { existsSync as existsSync4, readFileSync as readFileSync9, readdirSync, renameSync as renameSync2, rmSync, statSync as statSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
7776
7777
|
import { homedir as homedir2 } from "os";
|
|
7777
7778
|
import { join as join7 } from "path";
|
|
7778
7779
|
var BACKUP_SHA_PREFIX_LENGTH = 16;
|
|
@@ -7790,9 +7791,60 @@ function backupVersionTag(version) {
|
|
|
7790
7791
|
function contentAddressedBackupPath(version, sha256, dir = backupDir()) {
|
|
7791
7792
|
return join7(dir, `claude-${backupVersionTag(version)}-${sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH)}.orig`);
|
|
7792
7793
|
}
|
|
7794
|
+
function legacyBackupPath(version, dir = backupDir()) {
|
|
7795
|
+
return join7(dir, `claude-${backupVersionTag(version)}.orig`);
|
|
7796
|
+
}
|
|
7793
7797
|
function tweakccMirrorBackupPath(dir = backupDir()) {
|
|
7794
7798
|
return join7(dir, "native-binary.backup");
|
|
7795
7799
|
}
|
|
7800
|
+
function installProvenancePath(backupPath, binaryPath) {
|
|
7801
|
+
const tag = createHash5("sha256").update(binaryPath).digest("hex").slice(0, BACKUP_SHA_PREFIX_LENGTH);
|
|
7802
|
+
return `${backupPath}.for-${tag}.json`;
|
|
7803
|
+
}
|
|
7804
|
+
function installProvenancePattern(name) {
|
|
7805
|
+
return new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.for-[0-9a-f]{${BACKUP_SHA_PREFIX_LENGTH}}\\.json$`);
|
|
7806
|
+
}
|
|
7807
|
+
function readInstallProvenance(path) {
|
|
7808
|
+
let raw;
|
|
7809
|
+
try {
|
|
7810
|
+
raw = readFileSync9(path, "utf8");
|
|
7811
|
+
} catch {
|
|
7812
|
+
return null;
|
|
7813
|
+
}
|
|
7814
|
+
let parsed;
|
|
7815
|
+
try {
|
|
7816
|
+
parsed = JSON.parse(raw);
|
|
7817
|
+
} catch {
|
|
7818
|
+
return "damaged";
|
|
7819
|
+
}
|
|
7820
|
+
if (typeof parsed !== "object" || parsed === null) return "damaged";
|
|
7821
|
+
const { install, assumed } = parsed;
|
|
7822
|
+
if (typeof install !== "string" || install === "") return "damaged";
|
|
7823
|
+
if (typeof assumed !== "boolean") return "damaged";
|
|
7824
|
+
return { install, assumed };
|
|
7825
|
+
}
|
|
7826
|
+
function recordBackupProvenance(backupPath, binaryPath, opts) {
|
|
7827
|
+
const target = installProvenancePath(backupPath, binaryPath);
|
|
7828
|
+
const existing = readInstallProvenance(target);
|
|
7829
|
+
if (existing && existing !== "damaged" && existing.install === binaryPath) {
|
|
7830
|
+
if (existing.assumed === opts.assumed) return existing.assumed ? "assumed" : "established";
|
|
7831
|
+
if (!existing.assumed) return "established";
|
|
7832
|
+
}
|
|
7833
|
+
const record = { install: binaryPath, assumed: opts.assumed };
|
|
7834
|
+
const temp = `${target}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
7835
|
+
try {
|
|
7836
|
+
writeFileSync5(temp, `${JSON.stringify(record, null, 2)}
|
|
7837
|
+
`);
|
|
7838
|
+
renameSync2(temp, target);
|
|
7839
|
+
return opts.assumed ? "assumed" : "established";
|
|
7840
|
+
} catch (err) {
|
|
7841
|
+
try {
|
|
7842
|
+
rmSync(temp, { force: true });
|
|
7843
|
+
} catch {
|
|
7844
|
+
}
|
|
7845
|
+
throw err;
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7796
7848
|
function scanPristineBackups(version, dir = backupDir()) {
|
|
7797
7849
|
const tag = backupVersionTag(version);
|
|
7798
7850
|
const pattern = new RegExp(`^claude-${tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:-([0-9a-f]{${BACKUP_SHA_PREFIX_LENGTH}}))?\\.orig$`);
|
|
@@ -7802,6 +7854,27 @@ function scanPristineBackups(version, dir = backupDir()) {
|
|
|
7802
7854
|
} catch {
|
|
7803
7855
|
return { valid: [], corrupt: [] };
|
|
7804
7856
|
}
|
|
7857
|
+
const provenanceOf = (name) => {
|
|
7858
|
+
const recordPattern = installProvenancePattern(name);
|
|
7859
|
+
const installs = [];
|
|
7860
|
+
const assumedInstalls = [];
|
|
7861
|
+
const damagedProvenance = [];
|
|
7862
|
+
for (const entry of entries) {
|
|
7863
|
+
if (!recordPattern.test(entry)) continue;
|
|
7864
|
+
const path = join7(dir, entry);
|
|
7865
|
+
const record = readInstallProvenance(path);
|
|
7866
|
+
if (record === null || record === "damaged" || installProvenancePath(join7(dir, name), record.install) !== path) {
|
|
7867
|
+
damagedProvenance.push(path);
|
|
7868
|
+
continue;
|
|
7869
|
+
}
|
|
7870
|
+
(record.assumed ? assumedInstalls : installs).push(record.install);
|
|
7871
|
+
}
|
|
7872
|
+
return {
|
|
7873
|
+
installs: installs.sort(),
|
|
7874
|
+
assumedInstalls: assumedInstalls.sort(),
|
|
7875
|
+
damagedProvenance: damagedProvenance.sort()
|
|
7876
|
+
};
|
|
7877
|
+
};
|
|
7805
7878
|
const valid = [];
|
|
7806
7879
|
const corrupt = [];
|
|
7807
7880
|
for (const entry of entries.sort()) {
|
|
@@ -7822,9 +7895,9 @@ function scanPristineBackups(version, dir = backupDir()) {
|
|
|
7822
7895
|
corrupt.push(path);
|
|
7823
7896
|
continue;
|
|
7824
7897
|
}
|
|
7825
|
-
valid.push({ path, kind: "content-addressed", sha256 });
|
|
7898
|
+
valid.push({ path, kind: "content-addressed", sha256, ...provenanceOf(entry) });
|
|
7826
7899
|
} else {
|
|
7827
|
-
valid.push({ path, kind: "legacy", sha256 });
|
|
7900
|
+
valid.push({ path, kind: "legacy", sha256, ...provenanceOf(entry) });
|
|
7828
7901
|
}
|
|
7829
7902
|
}
|
|
7830
7903
|
return { valid, corrupt };
|
|
@@ -7846,7 +7919,7 @@ function looksLikeLegacyClodexPatch(source) {
|
|
|
7846
7919
|
}
|
|
7847
7920
|
function noBackupMessage(facts) {
|
|
7848
7921
|
const corrupt = facts.corruptBackups?.length ? ` (${facts.corruptBackups.length} backup file(s) for this version failed integrity checks and were ignored)` : "";
|
|
7849
|
-
return `claude ${facts.version}
|
|
7922
|
+
return `clodex holds no pristine backup of claude ${facts.version} it can attribute to ${facts.binaryPath}: none was found in ${backupDir()}${corrupt}. If that binary is patched, reinstall Claude Code to get a pristine one and run \`clodex patch\` again. If it was never patched, there is nothing to restore.`;
|
|
7850
7923
|
}
|
|
7851
7924
|
function identifiesABackup(manifest) {
|
|
7852
7925
|
return manifest.pristineSha256 !== void 0 || manifest.backupPath !== void 0;
|
|
@@ -7860,8 +7933,26 @@ function recordedBackupGoneMessage(facts, manifest) {
|
|
|
7860
7933
|
function otherInstallMessage(facts, otherBinaryPath) {
|
|
7861
7934
|
return `The patch manifest records a different Claude Code install (${otherBinaryPath}), so nothing establishes that a pristine backup tagged claude ${facts.version} in ${backupDir()} belongs to ${facts.binaryPath}. Two installs of one Claude Code version are different files, so restoring by version tag alone would overwrite this install with another one's bytes. Set TWEAKCC_CC_INSTALLATION_PATH=${otherBinaryPath} to restore that install instead, or reinstall Claude Code to make this one pristine.`;
|
|
7862
7935
|
}
|
|
7936
|
+
function otherInstallProvenanceMessage(facts, recorded) {
|
|
7937
|
+
const owners = [...new Set(recorded.flatMap((backup) => backup.installs))];
|
|
7938
|
+
return `The pristine backup(s) of claude ${facts.version} in ${backupDir()} are recorded as the pristine content of ${owners.join(", ")}, not of ${facts.binaryPath}. Two installs of one Claude Code version are different files, so restoring one of those would overwrite this install with bytes that were never its own. Set TWEAKCC_CC_INSTALLATION_PATH=${owners[0]} to restore that install instead, or reinstall Claude Code to make this one pristine.`;
|
|
7939
|
+
}
|
|
7940
|
+
function damagedProvenanceMessage(facts, damaged) {
|
|
7941
|
+
return `A pristine backup of claude ${facts.version} in ${backupDir()} carries a provenance record that clodex cannot read (${damaged.join(", ")}). Something recorded which install those bytes belong to and that record is damaged, so clodex will not fall back to matching on the claude version in the file name \u2014 on a machine with more than one install that would hand ${facts.binaryPath} bytes that were never its own. Delete the unreadable record to accept that fallback, or reinstall Claude Code to make this install pristine.`;
|
|
7942
|
+
}
|
|
7943
|
+
function contradictoryProvenanceMessage(facts, mine) {
|
|
7944
|
+
return `More than one pristine backup of claude ${facts.version} is recorded as the pristine content of ${facts.binaryPath} (${mine.map((backup) => backup.path).join(", ")}), and they do not hold the same bytes, so clodex cannot tell which one that install holds now \u2014 being snapshotted twice, at two different sets of unpatched bytes, reaches this state without either record being wrong. Delete whichever is stale to decide it: ${mine.map((backup) => installProvenancePath(backup.path, facts.binaryPath)).join(", ")}. Or reinstall the Claude Code at ${facts.binaryPath} and delete both.`;
|
|
7945
|
+
}
|
|
7946
|
+
function manifestContradictsRecordMessage(facts, manifestChoice, recorded) {
|
|
7947
|
+
return `The patch manifest names ${manifestChoice} as the pristine content of ${facts.binaryPath}, but ${recorded.map((backup) => backup.path).join(", ")} is recorded as that install's pristine content and holds different bytes. Nothing establishes which of them the install at that path is now \u2014 an executable replaced in place with a different build of the same claude version reaches exactly this state \u2014 so clodex will not pick one. Delete whichever of these records is stale to decide it: ${recorded.map((backup) => installProvenancePath(backup.path, facts.binaryPath)).join(", ")}. Or reinstall the Claude Code at ${facts.binaryPath} and delete both: a pristine install needs no backup, and \`clodex patch\` will record its own.`;
|
|
7948
|
+
}
|
|
7949
|
+
function assumedElsewhereMessage(facts, backups) {
|
|
7950
|
+
const others = [...new Set(backups.flatMap((backup) => backup.assumedInstalls))];
|
|
7951
|
+
return `The pristine backup(s) of claude ${facts.version} in ${backupDir()} have already been restored onto ${others.join(", ")} \u2014 not onto ${facts.binaryPath}. That was itself matched on the claude version in a file name, so it is not proof of ownership, but it is a reason not to hand the same bytes to a second install: two installs of one version are different files. Set TWEAKCC_CC_INSTALLATION_PATH=${others[0]} to restore that install, or reinstall Claude Code to make this one pristine.`;
|
|
7952
|
+
}
|
|
7863
7953
|
function selectRestoreSource(facts) {
|
|
7864
7954
|
const notes = [];
|
|
7955
|
+
let assumedForThisInstall = false;
|
|
7865
7956
|
const recorded = facts.manifest;
|
|
7866
7957
|
const speaksForThisVersion = !recorded?.claudeVersion || recorded.claudeVersion === facts.version;
|
|
7867
7958
|
const manifest = recorded && recorded.binaryPath === facts.binaryPath && speaksForThisVersion ? recorded : null;
|
|
@@ -7873,10 +7964,46 @@ function selectRestoreSource(facts) {
|
|
|
7873
7964
|
if (!chosen && manifest && identifiesABackup(manifest)) {
|
|
7874
7965
|
return { action: "error", message: recordedBackupGoneMessage(facts, manifest) };
|
|
7875
7966
|
}
|
|
7967
|
+
const manifestDescribesLiveBytes = !!manifest?.patchedSha256 && manifest.patchedSha256 === facts.liveSha256;
|
|
7968
|
+
if (chosen && !manifestDescribesLiveBytes) {
|
|
7969
|
+
const manifestChoice = chosen;
|
|
7970
|
+
const contradicting = facts.backups.filter(
|
|
7971
|
+
(backup) => backup.installs.includes(facts.binaryPath) && backup.sha256 !== manifestChoice.sha256
|
|
7972
|
+
);
|
|
7973
|
+
if (contradicting.length) {
|
|
7974
|
+
return {
|
|
7975
|
+
action: "error",
|
|
7976
|
+
message: manifestContradictsRecordMessage(facts, manifestChoice.path, contradicting)
|
|
7977
|
+
};
|
|
7978
|
+
}
|
|
7979
|
+
}
|
|
7980
|
+
if (!chosen) {
|
|
7981
|
+
const mine = facts.backups.filter((backup) => backup.installs.includes(facts.binaryPath));
|
|
7982
|
+
if (mine.length) {
|
|
7983
|
+
if (new Set(mine.map((backup) => backup.sha256)).size > 1) {
|
|
7984
|
+
return { action: "error", message: contradictoryProvenanceMessage(facts, mine) };
|
|
7985
|
+
}
|
|
7986
|
+
chosen = mine.find((backup) => backup.kind === "content-addressed") ?? mine[0];
|
|
7987
|
+
}
|
|
7988
|
+
}
|
|
7876
7989
|
if (!chosen) {
|
|
7877
7990
|
if (other) {
|
|
7878
7991
|
return facts.backups.length ? { action: "error", message: otherInstallMessage(facts, other.binaryPath) } : { action: "error", message: noBackupMessage(facts) };
|
|
7879
7992
|
}
|
|
7993
|
+
const recordedElsewhere = facts.backups.filter((backup) => backup.installs.length > 0);
|
|
7994
|
+
if (recordedElsewhere.length) {
|
|
7995
|
+
return { action: "error", message: otherInstallProvenanceMessage(facts, recordedElsewhere) };
|
|
7996
|
+
}
|
|
7997
|
+
const damaged = facts.backups.flatMap((backup) => backup.damagedProvenance);
|
|
7998
|
+
if (damaged.length) {
|
|
7999
|
+
return { action: "error", message: damagedProvenanceMessage(facts, damaged) };
|
|
8000
|
+
}
|
|
8001
|
+
const assumedElsewhere = facts.backups.filter(
|
|
8002
|
+
(backup) => backup.assumedInstalls.some((install) => install !== facts.binaryPath)
|
|
8003
|
+
);
|
|
8004
|
+
if (assumedElsewhere.length) {
|
|
8005
|
+
return { action: "error", message: assumedElsewhereMessage(facts, assumedElsewhere) };
|
|
8006
|
+
}
|
|
7880
8007
|
const distinct = [...new Set(facts.backups.map((backup) => backup.sha256))];
|
|
7881
8008
|
if (distinct.length > 1) {
|
|
7882
8009
|
return {
|
|
@@ -7886,12 +8013,14 @@ function selectRestoreSource(facts) {
|
|
|
7886
8013
|
}
|
|
7887
8014
|
chosen = facts.backups.find((backup) => backup.kind === "content-addressed") ?? facts.backups[0];
|
|
7888
8015
|
if (chosen) {
|
|
8016
|
+
assumedForThisInstall = true;
|
|
7889
8017
|
notes.push(
|
|
7890
|
-
`
|
|
8018
|
+
`Neither a patch manifest nor a provenance record ties ${chosen.path} to ${facts.binaryPath}, so it is being used as that install's pristine content on the strength of its claude ${facts.version} version tag alone \u2014 it predates the records clodex now writes. On a machine with more than one Claude Code install those bytes may belong to the other one, and clodex will not record this guess as provenance.`
|
|
7891
8019
|
);
|
|
7892
8020
|
}
|
|
7893
8021
|
}
|
|
7894
8022
|
if (!chosen) return { action: "error", message: noBackupMessage(facts) };
|
|
8023
|
+
if (manifest?.pristineProvenance === "assumed") assumedForThisInstall = true;
|
|
7895
8024
|
return {
|
|
7896
8025
|
action: "restore",
|
|
7897
8026
|
backupPath: chosen.path,
|
|
@@ -7900,6 +8029,7 @@ function selectRestoreSource(facts) {
|
|
|
7900
8029
|
// another version's binary, stored under a mislabeled name by an older
|
|
7901
8030
|
// clodex. Executing it is the only evidence available; require it.
|
|
7902
8031
|
probeVersion: chosen.kind === "legacy",
|
|
8032
|
+
assumedForThisInstall,
|
|
7903
8033
|
notes
|
|
7904
8034
|
};
|
|
7905
8035
|
}
|
|
@@ -13800,7 +13930,7 @@ function readPatchManifest(path = getPatchManifestPath()) {
|
|
|
13800
13930
|
}
|
|
13801
13931
|
function writePatchManifest(manifest, path = getPatchManifestPath()) {
|
|
13802
13932
|
mkdirSync5(getAppHome(), { recursive: true, mode: 448 });
|
|
13803
|
-
|
|
13933
|
+
writeFileSync6(path, `${JSON.stringify(manifest, null, 2)}
|
|
13804
13934
|
`, { encoding: "utf8", mode: 384 });
|
|
13805
13935
|
}
|
|
13806
13936
|
function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
|
|
@@ -13958,7 +14088,7 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
|
|
|
13958
14088
|
try {
|
|
13959
14089
|
const fd = openSync7(lockPath, "wx");
|
|
13960
14090
|
const content = { pid: process.pid, startedAt: now };
|
|
13961
|
-
|
|
14091
|
+
writeFileSync6(fd, JSON.stringify(content));
|
|
13962
14092
|
closeSync7(fd);
|
|
13963
14093
|
return () => {
|
|
13964
14094
|
try {
|
|
@@ -13983,10 +14113,19 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
|
|
|
13983
14113
|
}
|
|
13984
14114
|
return null;
|
|
13985
14115
|
}
|
|
14116
|
+
var ignoredLaunchOverride = null;
|
|
14117
|
+
function takeIgnoredLaunchOverride() {
|
|
14118
|
+
const ignored = ignoredLaunchOverride;
|
|
14119
|
+
ignoredLaunchOverride = null;
|
|
14120
|
+
return ignored;
|
|
14121
|
+
}
|
|
13986
14122
|
function resolveClaudeBinaryForPatch() {
|
|
13987
|
-
const envOverride = process.env["TWEAKCC_CC_INSTALLATION_PATH"];
|
|
14123
|
+
const envOverride = process.env["TWEAKCC_CC_INSTALLATION_PATH"]?.trim() || null;
|
|
13988
14124
|
const nativeSymlink = join8(homedir3(), ".local", "bin", "claude");
|
|
13989
|
-
|
|
14125
|
+
if (envOverride && !existsSync5(envOverride)) {
|
|
14126
|
+
return { ok: false, reason: "patch-target-missing", declaredPath: envOverride };
|
|
14127
|
+
}
|
|
14128
|
+
const source = envOverride || (existsSync5(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
|
|
13990
14129
|
if (!source) return { ok: false, reason: "binary-not-found" };
|
|
13991
14130
|
let resolved;
|
|
13992
14131
|
try {
|
|
@@ -14006,6 +14145,17 @@ function resolveClaudeBinaryForPatch() {
|
|
|
14006
14145
|
};
|
|
14007
14146
|
}
|
|
14008
14147
|
resolved = followed.path;
|
|
14148
|
+
const launchOverride = process.env["CLODEX_CLAUDE_PATH"]?.trim() || null;
|
|
14149
|
+
let launchOverrideResolved = null;
|
|
14150
|
+
if (launchOverride) {
|
|
14151
|
+
try {
|
|
14152
|
+
const followedOverride = resolveThroughNpmShims(realpathSync2(launchOverride));
|
|
14153
|
+
launchOverrideResolved = followedOverride.ok ? followedOverride.path : realpathSync2(launchOverride);
|
|
14154
|
+
} catch {
|
|
14155
|
+
launchOverrideResolved = launchOverride;
|
|
14156
|
+
}
|
|
14157
|
+
}
|
|
14158
|
+
ignoredLaunchOverride = launchOverrideResolved && launchOverrideResolved !== resolved ? { used: resolved, ignored: launchOverride } : null;
|
|
14009
14159
|
try {
|
|
14010
14160
|
if (!statSync8(resolved).isFile()) return { ok: false, reason: "binary-not-found" };
|
|
14011
14161
|
} catch {
|
|
@@ -14029,7 +14179,7 @@ function installScriptCommand(path) {
|
|
|
14029
14179
|
}
|
|
14030
14180
|
function describePatchTargetFailure(target, command = "patch") {
|
|
14031
14181
|
if (target.reason === "launcher-unresolved") {
|
|
14032
|
-
return `${target.shimPath} starts Claude Code but is not Claude Code itself, and clodex could not follow it: ${target.detail}. clodex will not patch a launcher script \u2014 that fails with "Unable to detect installation type". Set
|
|
14182
|
+
return `${target.shimPath} starts Claude Code but is not Claude Code itself, and clodex could not follow it: ${target.detail}. clodex will not patch a launcher script \u2014 that fails with "Unable to detect installation type". Set TWEAKCC_CC_INSTALLATION_PATH to the Claude Code program itself (for an npm install on Windows that is node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe under the directory holding the launcher), then run the command again.`;
|
|
14033
14183
|
}
|
|
14034
14184
|
if (target.reason === "native-binary-missing") {
|
|
14035
14185
|
const installer = installScriptCommand(target.installScriptPath);
|
|
@@ -14038,6 +14188,9 @@ function describePatchTargetFailure(target, command = "patch") {
|
|
|
14038
14188
|
const restore = command === "restore" ? ` If you need to restore by hand, pristine backups are in ${backupDir()}.` : "";
|
|
14039
14189
|
return `${target.binaryPath} is Claude Code's npm placeholder, not its native binary, so the npm install is incomplete. ${remedy} ${retry}${restore} If this is a custom wrapper rather than the npm placeholder, set TWEAKCC_CC_INSTALLATION_PATH to the native Claude Code binary.`;
|
|
14040
14190
|
}
|
|
14191
|
+
if (target.reason === "patch-target-missing") {
|
|
14192
|
+
return `TWEAKCC_CC_INSTALLATION_PATH is set to ${target.declaredPath}, which does not exist. clodex will not look for another Claude Code instead \u2014 patching or restoring a different install than the one you named is how one install's pristine bytes end up over another's. Point it at the Claude Code program, or unset it to let clodex find your install.`;
|
|
14193
|
+
}
|
|
14041
14194
|
return target.reason === "binary-not-found" ? "claude binary not found. Install Claude Code or set TWEAKCC_CC_INSTALLATION_PATH." : `Could not determine the version of ${target.binaryPath} (\`claude --version\` failed). clodex will not patch a binary whose version it cannot read, because the version selects the pristine backup it patches from. If a previous patch left the install broken, \`clodex patch --restore\` still works \u2014 it reads the version from the patch manifest.`;
|
|
14042
14195
|
}
|
|
14043
14196
|
function summarizePatchResults(results) {
|
|
@@ -14060,14 +14213,15 @@ function verifyPristineSource(plan, version) {
|
|
|
14060
14213
|
message: `Refusing to use ${plan.backupPath} as the pristine source for claude ${version}: it reports ${backupVersion ? `version ${backupVersion}` : "no version at all"}. That backup predates content-addressed backup names and does not hold this version's bytes. Remove it (or reinstall Claude Code), then run \`clodex patch\`.`
|
|
14061
14214
|
};
|
|
14062
14215
|
}
|
|
14063
|
-
function
|
|
14216
|
+
function publishFileByRename(from, to, mode) {
|
|
14064
14217
|
const temp = `${to}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
14065
14218
|
try {
|
|
14066
14219
|
copyFileSync(from, temp);
|
|
14067
|
-
|
|
14220
|
+
if (mode !== void 0) chmodSync4(temp, mode);
|
|
14221
|
+
renameSync3(temp, to);
|
|
14068
14222
|
} catch (err) {
|
|
14069
14223
|
try {
|
|
14070
|
-
|
|
14224
|
+
rmSync2(temp, { force: true });
|
|
14071
14225
|
} catch {
|
|
14072
14226
|
}
|
|
14073
14227
|
throw err;
|
|
@@ -14131,6 +14285,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
|
|
|
14131
14285
|
let patchedSha256;
|
|
14132
14286
|
let backup;
|
|
14133
14287
|
let pristineSha256;
|
|
14288
|
+
const pristine = { provenance: "established" };
|
|
14134
14289
|
try {
|
|
14135
14290
|
mkdirSync5(backupDir(), { recursive: true });
|
|
14136
14291
|
const { tryDetectInstallation, readContent, writeContent } = await import("tweakcc");
|
|
@@ -14179,6 +14334,18 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
|
|
|
14179
14334
|
}
|
|
14180
14335
|
pristineSha256 = plan.pristineSha256;
|
|
14181
14336
|
backup = plan.backupPath;
|
|
14337
|
+
const inheritsAGuess = (path) => {
|
|
14338
|
+
const existing = readInstallProvenance(installProvenancePath(path, binaryPath));
|
|
14339
|
+
return existing === "damaged" || existing !== null && existing.assumed;
|
|
14340
|
+
};
|
|
14341
|
+
const provenanceAssumed = plan.action === "restore" && plan.assumedForThisInstall || inheritsAGuess(plan.backupPath) || inheritsAGuess(contentAddressedBackupPath(version, plan.pristineSha256)) || inheritsAGuess(legacyBackupPath(version)) || facts.backups.some(
|
|
14342
|
+
(candidate) => candidate.sha256 === plan.pristineSha256 && candidate.assumedInstalls.includes(binaryPath)
|
|
14343
|
+
);
|
|
14344
|
+
const recordProvenance = (path) => {
|
|
14345
|
+
if (recordBackupProvenance(path, binaryPath, { assumed: provenanceAssumed }) === "assumed") {
|
|
14346
|
+
pristine.provenance = "assumed";
|
|
14347
|
+
}
|
|
14348
|
+
};
|
|
14182
14349
|
if (!loaded) {
|
|
14183
14350
|
loaded = await seedCandidate(backup);
|
|
14184
14351
|
if (isPatchedClaudeSource(loaded.source)) {
|
|
@@ -14190,17 +14357,22 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
|
|
|
14190
14357
|
`the patch candidate no longer matches the pristine bytes it was seeded from; refusing to publish it as ${plan.backupPath}`
|
|
14191
14358
|
);
|
|
14192
14359
|
}
|
|
14193
|
-
|
|
14360
|
+
recordProvenance(plan.backupPath);
|
|
14361
|
+
publishFileByRename(candidatePath, plan.backupPath);
|
|
14194
14362
|
}
|
|
14195
14363
|
const canonical = contentAddressedBackupPath(version, pristineSha256);
|
|
14196
14364
|
if (canonical !== backup) {
|
|
14365
|
+
recordProvenance(backup);
|
|
14366
|
+
recordProvenance(canonical);
|
|
14197
14367
|
const alreadyStored = facts.backups.some(
|
|
14198
14368
|
(candidate) => candidate.path === canonical && candidate.sha256 === pristineSha256
|
|
14199
14369
|
);
|
|
14200
|
-
if (!alreadyStored)
|
|
14370
|
+
if (!alreadyStored) publishFileByRename(backup, canonical);
|
|
14201
14371
|
backup = canonical;
|
|
14202
14372
|
}
|
|
14203
|
-
|
|
14373
|
+
publishFileByRename(backup, tweakccMirrorBackupPath());
|
|
14374
|
+
recordProvenance(backup);
|
|
14375
|
+
if (plan.backupPath !== backup) recordProvenance(plan.backupPath);
|
|
14204
14376
|
const builtIn = applyClodexPatches(loaded.source, desired.config);
|
|
14205
14377
|
results = builtIn.results;
|
|
14206
14378
|
const failedEffortPatches = requiredEffortPatchFailures(results);
|
|
@@ -14267,7 +14439,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
|
|
|
14267
14439
|
else if (publishedBlob) resignMachOBinary(candidatePath);
|
|
14268
14440
|
patchedSize = statSync8(candidatePath).size;
|
|
14269
14441
|
patchedSha256 = sha256File(candidatePath);
|
|
14270
|
-
|
|
14442
|
+
renameSync3(candidatePath, binaryPath);
|
|
14271
14443
|
} catch (err) {
|
|
14272
14444
|
const detailLines = err instanceof PatchApplyError ? summarizePatchResults(err.results) : [];
|
|
14273
14445
|
if (opts.trace && detailLines.length) {
|
|
@@ -14282,7 +14454,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
|
|
|
14282
14454
|
} finally {
|
|
14283
14455
|
if (candidateDir !== void 0) {
|
|
14284
14456
|
try {
|
|
14285
|
-
|
|
14457
|
+
rmSync2(candidateDir, { recursive: true, force: true });
|
|
14286
14458
|
} catch (err) {
|
|
14287
14459
|
if (opts.trace) {
|
|
14288
14460
|
process.stderr.write(
|
|
@@ -14305,6 +14477,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
|
|
|
14305
14477
|
patchedSha256,
|
|
14306
14478
|
backupPath: backup,
|
|
14307
14479
|
pristineSha256,
|
|
14480
|
+
...pristine.provenance === "assumed" ? { pristineProvenance: "assumed" } : {},
|
|
14308
14481
|
patchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14309
14482
|
};
|
|
14310
14483
|
writePatchManifest(manifest);
|
|
@@ -14318,7 +14491,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
|
|
|
14318
14491
|
};
|
|
14319
14492
|
}
|
|
14320
14493
|
function runRestoreCommand(target) {
|
|
14321
|
-
if (!target.ok && (target.reason === "binary-not-found" || target.reason === "native-binary-missing")) {
|
|
14494
|
+
if (!target.ok && (target.reason === "binary-not-found" || target.reason === "patch-target-missing" || target.reason === "native-binary-missing")) {
|
|
14322
14495
|
p2.log.error(describePatchTargetFailure(target, "restore"));
|
|
14323
14496
|
return 1;
|
|
14324
14497
|
}
|
|
@@ -14353,16 +14526,65 @@ function runRestoreCommand(target) {
|
|
|
14353
14526
|
p2.log.error(verified.message);
|
|
14354
14527
|
return 1;
|
|
14355
14528
|
}
|
|
14356
|
-
|
|
14529
|
+
if (manifest && manifest.backupPath && manifest.binaryPath === binaryPath && manifest.claudeVersion !== version && manifest.backupPath !== plan.backupPath) {
|
|
14530
|
+
try {
|
|
14531
|
+
if (existsSync5(manifest.backupPath)) {
|
|
14532
|
+
recordBackupProvenance(manifest.backupPath, manifest.binaryPath, {
|
|
14533
|
+
assumed: manifest.pristineProvenance === "assumed"
|
|
14534
|
+
});
|
|
14535
|
+
}
|
|
14536
|
+
} catch (err) {
|
|
14537
|
+
p2.log.warn(
|
|
14538
|
+
`Could not record in ${backupDir()} that ${manifest.backupPath} holds the pristine bytes of claude ${manifest.claudeVersion} at ${manifest.binaryPath} (${err instanceof Error ? err.message : String(err)}). That version may need to be reinstalled rather than restored.`
|
|
14539
|
+
);
|
|
14540
|
+
}
|
|
14541
|
+
}
|
|
14542
|
+
let recorded;
|
|
14357
14543
|
try {
|
|
14358
|
-
|
|
14544
|
+
recorded = recordBackupProvenance(plan.backupPath, binaryPath, { assumed: plan.assumedForThisInstall });
|
|
14545
|
+
} catch (err) {
|
|
14546
|
+
recorded = "failed";
|
|
14547
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
14548
|
+
if (plan.assumedForThisInstall) {
|
|
14549
|
+
p2.log.error(
|
|
14550
|
+
`Refusing to restore ${binaryPath} from ${plan.backupPath}: nothing but the claude ${version} version tag ties those bytes to this install, and clodex cannot record that in ${backupDir()} (${detail}). Writing them without that record would leave the machine unable to tell afterwards that the match was a guess. Fix the backup directory, or reinstall Claude Code to make this install pristine.`
|
|
14551
|
+
);
|
|
14552
|
+
return 1;
|
|
14553
|
+
}
|
|
14554
|
+
p2.log.warn(
|
|
14555
|
+
`Could not record in ${backupDir()} that ${plan.backupPath} holds the pristine bytes of ${binaryPath} (${detail}). Restoring anyway and keeping the patch manifest, so that record is not lost \u2014 a later restore would otherwise have nothing tying that backup to this install.`
|
|
14556
|
+
);
|
|
14557
|
+
}
|
|
14558
|
+
let publishedMode;
|
|
14559
|
+
try {
|
|
14560
|
+
publishedMode = statSync8(binaryPath).mode;
|
|
14359
14561
|
} catch {
|
|
14360
14562
|
}
|
|
14563
|
+
try {
|
|
14564
|
+
publishFileByRename(plan.backupPath, binaryPath, publishedMode);
|
|
14565
|
+
} catch (err) {
|
|
14566
|
+
p2.log.warn(
|
|
14567
|
+
`Could not replace ${binaryPath} through a new file (${err instanceof Error ? err.message : String(err)}); writing the pristine bytes in place instead. If claude then fails to start with a code-signing error, copy the backup to a new file and move it over the binary by hand.`
|
|
14568
|
+
);
|
|
14569
|
+
copyFileSync(plan.backupPath, binaryPath);
|
|
14570
|
+
}
|
|
14571
|
+
if (recorded === "established" && (!manifest || manifest.binaryPath === binaryPath)) {
|
|
14572
|
+
try {
|
|
14573
|
+
unlinkSync3(getPatchManifestPath());
|
|
14574
|
+
} catch {
|
|
14575
|
+
}
|
|
14576
|
+
}
|
|
14361
14577
|
p2.log.success(`Restored pristine claude ${version} from ${plan.backupPath}.`);
|
|
14362
14578
|
return 0;
|
|
14363
14579
|
}
|
|
14364
14580
|
async function runPatchCommand(opts = {}) {
|
|
14365
14581
|
const target = resolveClaudeBinaryForPatch();
|
|
14582
|
+
const ignoredOverride = takeIgnoredLaunchOverride();
|
|
14583
|
+
if (ignoredOverride) {
|
|
14584
|
+
p2.log.warn(
|
|
14585
|
+
`CLODEX_CLAUDE_PATH is set to ${ignoredOverride.ignored}, but it does not choose what gets patched \u2014 ${ignoredOverride.used} does. CLODEX_CLAUDE_PATH selects the claude that gets LAUNCHED; set TWEAKCC_CC_INSTALLATION_PATH to patch a specific install.`
|
|
14586
|
+
);
|
|
14587
|
+
}
|
|
14366
14588
|
if (opts.restore) return runRestoreCommand(target);
|
|
14367
14589
|
if (!target.ok) {
|
|
14368
14590
|
p2.log.error(describePatchTargetFailure(target));
|
|
@@ -14405,6 +14627,19 @@ async function runPatchCommand(opts = {}) {
|
|
|
14405
14627
|
p2.log.warn("Another clodex process is patching the claude binary right now \u2014 skipped.");
|
|
14406
14628
|
return 1;
|
|
14407
14629
|
}
|
|
14630
|
+
if (manifest && manifest.backupPath && (manifest.binaryPath !== binaryPath || manifest.claudeVersion !== version)) {
|
|
14631
|
+
try {
|
|
14632
|
+
if (existsSync5(manifest.backupPath)) {
|
|
14633
|
+
recordBackupProvenance(manifest.backupPath, manifest.binaryPath, {
|
|
14634
|
+
assumed: manifest.pristineProvenance === "assumed"
|
|
14635
|
+
});
|
|
14636
|
+
}
|
|
14637
|
+
} catch (err) {
|
|
14638
|
+
p2.log.warn(
|
|
14639
|
+
`Could not record in ${backupDir()} that ${manifest.backupPath} holds the pristine bytes of ${manifest.binaryPath} before replacing the patch manifest (${err instanceof Error ? err.message : String(err)}). That install may need to be reinstalled rather than restored.`
|
|
14640
|
+
);
|
|
14641
|
+
}
|
|
14642
|
+
}
|
|
14408
14643
|
try {
|
|
14409
14644
|
const outcome = await applyPatch(binaryPath, version, desired, configHash, {
|
|
14410
14645
|
trace: opts.trace ?? false,
|
|
@@ -14794,9 +15029,9 @@ import {
|
|
|
14794
15029
|
mkdirSync as mkdirSync6,
|
|
14795
15030
|
openSync as openSync8,
|
|
14796
15031
|
readFileSync as readFileSync11,
|
|
14797
|
-
renameSync as
|
|
15032
|
+
renameSync as renameSync4,
|
|
14798
15033
|
unlinkSync as unlinkSync4,
|
|
14799
|
-
writeFileSync as
|
|
15034
|
+
writeFileSync as writeFileSync7
|
|
14800
15035
|
} from "fs";
|
|
14801
15036
|
import { dirname as dirname6 } from "path";
|
|
14802
15037
|
var JOURNAL_SCHEMA_VERSION = 1;
|
|
@@ -14928,13 +15163,13 @@ function writeJournalUnlocked(journal, path) {
|
|
|
14928
15163
|
let fd;
|
|
14929
15164
|
try {
|
|
14930
15165
|
fd = openSync8(tmp, "wx", FILE_MODE4);
|
|
14931
|
-
|
|
15166
|
+
writeFileSync7(fd, `${JSON.stringify(journal, null, 2)}
|
|
14932
15167
|
`);
|
|
14933
15168
|
fsyncSync2(fd);
|
|
14934
15169
|
closeSync8(fd);
|
|
14935
15170
|
fd = void 0;
|
|
14936
15171
|
assertRegistryWriteOwnership(path);
|
|
14937
|
-
|
|
15172
|
+
renameSync4(tmp, path);
|
|
14938
15173
|
syncParentDirectory(path);
|
|
14939
15174
|
} finally {
|
|
14940
15175
|
if (fd !== void 0) closeSync8(fd);
|
|
@@ -18907,7 +19142,7 @@ import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
|
|
|
18907
19142
|
|
|
18908
19143
|
// src/http-proxy/ca.ts
|
|
18909
19144
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
18910
|
-
import { chmodSync as
|
|
19145
|
+
import { chmodSync as chmodSync5, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
|
|
18911
19146
|
import { dirname as dirname7, join as join9, resolve as resolve3 } from "path";
|
|
18912
19147
|
import forge from "node-forge";
|
|
18913
19148
|
var CERT_DIR = "http-proxy";
|
|
@@ -18934,16 +19169,16 @@ function certPaths() {
|
|
|
18934
19169
|
};
|
|
18935
19170
|
}
|
|
18936
19171
|
function writePrivate(path, value) {
|
|
18937
|
-
|
|
18938
|
-
|
|
19172
|
+
writeFileSync8(path, value, { encoding: "utf8", mode: 384 });
|
|
19173
|
+
chmodSync5(path, 384);
|
|
18939
19174
|
}
|
|
18940
19175
|
function writePublic(path, value) {
|
|
18941
|
-
|
|
18942
|
-
|
|
19176
|
+
writeFileSync8(path, value, { encoding: "utf8", mode: 420 });
|
|
19177
|
+
chmodSync5(path, 420);
|
|
18943
19178
|
}
|
|
18944
19179
|
function generateCertificates(paths) {
|
|
18945
19180
|
mkdirSync7(paths.dir, { recursive: true, mode: 448 });
|
|
18946
|
-
|
|
19181
|
+
chmodSync5(paths.dir, 448);
|
|
18947
19182
|
const caKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
18948
19183
|
const caCert = forge.pki.createCertificate();
|
|
18949
19184
|
caCert.publicKey = caKeys.publicKey;
|