@bman654/clodex 2.13.0 → 2.14.0

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 CHANGED
@@ -107,7 +107,7 @@ flowchart LR
107
107
  > Using Claude Code's agents view or background agents? Ask your Claude Code agent to read [docs/background-agents.md](docs/background-agents.md) and set it up for you — one global `clodex server --proxy` plus the `clodex-claude` wrapper bin bridges every claude process automatically.
108
108
 
109
109
  > [!TIP]
110
- > On Windows with the Claude Code VS Code extension? See [docs/windows-setup.md](docs/windows-setup.md) — proxy-mode env vars route the extension through clodex, and an optional process wrapper makes clodex models appear in its model picker.
110
+ > Using the Claude Code VS Code extension? `clodex-claude` launches your verified clodex-patched install in place of the extension's bundled binary, so clodex models appear in its model picker; follow the [VS Code setup](docs/background-agents.md#vs-code-model-picker). On Windows, `clodex install-vscode-launcher` first builds the `.exe` the extension needs to launch Claude Code through `clodex-claude`; see [docs/windows-setup.md](docs/windows-setup.md).
111
111
 
112
112
  ## CLI reference
113
113
 
@@ -146,7 +146,7 @@ Common options (both modes):
146
146
  | `--save-mode` | With `--endpoint`/`--proxy`: save that mode as the `server` default |
147
147
  | `--port <1-65535>` | Listen port (default 17645) |
148
148
  | `--no-discovery` | Don't advertise this server in `~/.clodex/server-runtime.json` (`CLODEX_NO_DISCOVERY=1` also works). Use it for a standalone endpoint the `clodex-claude` wrapper should ignore. |
149
- | `--ws-diagnostics` | Log sanitized request envelopes and WebSocket head decisions |
149
+ | `--ws-diagnostics` | Log sanitized request envelopes and WebSocket head decisions, plus the usage-limit reports the OpenAI socket sends, verbatim (they include account state such as credits) |
150
150
  | `--help`, `--version` | Help / version |
151
151
 
152
152
  Endpoint mode only (an error if combined with `--proxy`):
@@ -293,6 +293,10 @@ Two things worth knowing about the numbers:
293
293
 
294
294
  Providers supported: `openai` (API key, platform.openai.com), `openai-oauth` (ChatGPT/Codex plan), and `opencode-go` (OpenCode Go API key). OpenCode Go exposes its Anthropic Messages and Chat Completions models; Responses-only entries are intentionally excluded. See [OpenCode Go provider](docs/opencode-go.md).
295
295
 
296
+ ### `clodex install-vscode-launcher`
297
+
298
+ Windows only. Builds `%USERPROFILE%\.clodex\bin\clodex-claude.exe`, the executable the Claude Code VS Code extension can use as its `claudeCode.claudeProcessWrapper`, so chats in the editor launch Claude Code through `clodex-claude` (on Windows npm installs that wrapper as three script shims — extensionless, `.cmd`, `.ps1` — and no executable the extension can spawn). It compiles a short C# source shipped in the package with the compiler already inside the .NET Framework — nothing is downloaded — and prints the setting to paste; it never edits VS Code or Claude settings. The paths to `node.exe` and the clodex install are compiled in, so re-run it after switching Node versions or moving clodex. Setup guide: [docs/windows-setup.md](docs/windows-setup.md).
299
+
296
300
  ### Root
297
301
 
298
302
  ```
@@ -1372,6 +1372,62 @@ async function listenTcpServer(server, port, host) {
1372
1372
  );
1373
1373
  }
1374
1374
 
1375
+ // src/parent-notice.ts
1376
+ import { writeSync as writeSync2 } from "fs";
1377
+ var MAX_NOTICE_CHARS = 2e3;
1378
+ var CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
1379
+ var activeSink = null;
1380
+ var stderrErrorGuardInstalled = false;
1381
+ function toNoticeLine(message) {
1382
+ const flattened = message.replace(CONTROL_CHARS, " ").trimEnd();
1383
+ const bounded = flattened.length > MAX_NOTICE_CHARS ? `${flattened.slice(0, MAX_NOTICE_CHARS)}...` : flattened;
1384
+ return `${bounded}
1385
+ `;
1386
+ }
1387
+ function guardStderrErrors() {
1388
+ if (stderrErrorGuardInstalled) return;
1389
+ stderrErrorGuardInstalled = true;
1390
+ try {
1391
+ process.stderr.on("error", () => {
1392
+ });
1393
+ } catch {
1394
+ }
1395
+ }
1396
+ function writeParentNoticeLines(lines) {
1397
+ if (lines.length === 0) return;
1398
+ guardStderrErrors();
1399
+ try {
1400
+ process.stderr.write(lines.join(""), () => {
1401
+ });
1402
+ } catch {
1403
+ }
1404
+ }
1405
+ function writeParentNoticeLinesSync(lines) {
1406
+ if (lines.length === 0) return;
1407
+ try {
1408
+ writeSync2(2, lines.join(""));
1409
+ } catch {
1410
+ }
1411
+ }
1412
+ function emitParentNotice(message) {
1413
+ const line = toNoticeLine(message);
1414
+ try {
1415
+ if (activeSink) activeSink(line);
1416
+ else writeParentNoticeLines([line]);
1417
+ } catch {
1418
+ }
1419
+ }
1420
+ function installParentNoticeSink(sink) {
1421
+ const previous = activeSink;
1422
+ activeSink = sink;
1423
+ let released = false;
1424
+ return () => {
1425
+ if (released) return;
1426
+ released = true;
1427
+ if (activeSink === sink) activeSink = previous;
1428
+ };
1429
+ }
1430
+
1375
1431
  // src/server-runtime.ts
1376
1432
  import {
1377
1433
  closeSync as closeSync3,
@@ -1626,6 +1682,9 @@ function recordNetworkEnvMutation(baseline, injectedEnv) {
1626
1682
 
1627
1683
  // src/wrapper-env.ts
1628
1684
  var REQUIRE_SERVER_ENV = "CLODEX_REQUIRE_SERVER";
1685
+ function wrapperSpawnShell(platform, target) {
1686
+ return platform === "win32" && /\.(cmd|bat)$/i.test(target);
1687
+ }
1629
1688
  function removeAnthropicProxyBypass(env) {
1630
1689
  const noProxyValues = [env["NO_PROXY"], env["no_proxy"]].filter((value) => value !== void 0);
1631
1690
  if (noProxyValues.length === 0) return;
@@ -1649,6 +1708,16 @@ var LOCAL_GATEWAY_API_KEY = "clodex-local";
1649
1708
  function wrapperRequiresServer(env) {
1650
1709
  return env[REQUIRE_SERVER_ENV] === "1";
1651
1710
  }
1711
+ function wrapperIsTopLevelVsCodeHost(env) {
1712
+ return env["CLAUDE_CODE_ENTRYPOINT"] === "claude-vscode" && !env["CLAUDE_CODE_CHILD_SESSION"] && !env["CLAUDECODE"];
1713
+ }
1714
+ function wrapperSubstitutionEligible(env, state) {
1715
+ return wrapperIsTopLevelVsCodeHost(env) && state?.mode === "proxy";
1716
+ }
1717
+ function wrapperInvocationIsChat(args) {
1718
+ const streamJson = args.some((arg, index) => arg === "stream-json" && (args[index - 1] === "--output-format" || args[index - 1] === "--input-format"));
1719
+ return streamJson && !args.includes("--no-session-persistence");
1720
+ }
1652
1721
  function computeWrapperEnv(baseEnv, state) {
1653
1722
  if (!state) return { ...baseEnv };
1654
1723
  const baseline = networkEnvBaseline(baseEnv);
@@ -1669,6 +1738,378 @@ function computeWrapperEnv(baseEnv, state) {
1669
1738
  return env;
1670
1739
  }
1671
1740
 
1741
+ // src/patch-manifest.ts
1742
+ import { readFileSync as readFileSync5 } from "fs";
1743
+ import { join as join5 } from "path";
1744
+ function getPatchManifestPath() {
1745
+ return join5(getAppHome(), "patch-state.json");
1746
+ }
1747
+ function readPatchManifest(path = getPatchManifestPath()) {
1748
+ try {
1749
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
1750
+ if (parsed && typeof parsed.binaryPath === "string" && typeof parsed.configHash === "string") {
1751
+ return parsed;
1752
+ }
1753
+ } catch {
1754
+ }
1755
+ return null;
1756
+ }
1757
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
1758
+ function readWrapperPatchManifest(path = getPatchManifestPath()) {
1759
+ let raw;
1760
+ try {
1761
+ raw = readFileSync5(path, "utf8");
1762
+ } catch (error) {
1763
+ return error.code === "ENOENT" ? { status: "missing", path } : { status: "invalid", path };
1764
+ }
1765
+ let parsed;
1766
+ try {
1767
+ parsed = JSON.parse(raw);
1768
+ } catch {
1769
+ return { status: "invalid", path };
1770
+ }
1771
+ if (typeof parsed !== "object" || parsed === null) {
1772
+ return { status: "invalid", path };
1773
+ }
1774
+ const value = parsed;
1775
+ const hint = {
1776
+ ...typeof value.binaryPath === "string" && value.binaryPath.trim() ? { binaryPath: value.binaryPath } : {},
1777
+ ...typeof value.claudeVersion === "string" && value.claudeVersion.trim() ? { claudeVersion: value.claudeVersion } : {}
1778
+ };
1779
+ const valid = typeof value.binaryPath === "string" && value.binaryPath.trim().length > 0 && typeof value.claudeVersion === "string" && value.claudeVersion.trim().length > 0 && typeof value.configHash === "string" && value.configHash.trim().length > 0 && typeof value.patchedSize === "number" && Number.isSafeInteger(value.patchedSize) && value.patchedSize >= 0 && typeof value.patchedSha256 === "string" && SHA256_HEX.test(value.patchedSha256) && typeof value.backupPath === "string" && value.backupPath.trim().length > 0 && typeof value.pristineSha256 === "string" && SHA256_HEX.test(value.pristineSha256) && typeof value.patchedAt === "string" && value.patchedAt.trim().length > 0 && (value.pristineProvenance === void 0 || value.pristineProvenance === "assumed");
1780
+ if (!valid) return { status: "invalid", path, ...hint };
1781
+ return { status: "valid", path, manifest: value };
1782
+ }
1783
+
1784
+ // src/patch-backup.ts
1785
+ import { createHash as createHash2 } from "crypto";
1786
+ import {
1787
+ closeSync as closeSync4,
1788
+ existsSync as existsSync4,
1789
+ openSync as openSync4,
1790
+ readFileSync as readFileSync6,
1791
+ readSync,
1792
+ readdirSync,
1793
+ renameSync as renameSync4,
1794
+ rmSync as rmSync2,
1795
+ statSync as statSync2,
1796
+ writeFileSync as writeFileSync3
1797
+ } from "fs";
1798
+ import { homedir as homedir3 } from "os";
1799
+ import { join as join6 } from "path";
1800
+ var BACKUP_SHA_PREFIX_LENGTH = 16;
1801
+ function backupDir() {
1802
+ return process.env["TWEAKCC_CONFIG_DIR"]?.trim() || join6(homedir3(), ".tweakcc");
1803
+ }
1804
+ var HASH_CHUNK_BYTES = 1024 * 1024;
1805
+ function sha256File(path) {
1806
+ const hash = createHash2("sha256");
1807
+ const buffer = Buffer.allocUnsafe(HASH_CHUNK_BYTES);
1808
+ const fd = openSync4(path, "r");
1809
+ try {
1810
+ let bytesRead;
1811
+ do {
1812
+ bytesRead = readSync(fd, buffer, 0, buffer.length, null);
1813
+ if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
1814
+ } while (bytesRead > 0);
1815
+ } finally {
1816
+ closeSync4(fd);
1817
+ }
1818
+ return hash.digest("hex");
1819
+ }
1820
+ function backupVersionTag(version) {
1821
+ const tag = version.trim().replace(/[^\w.-]+/g, "_");
1822
+ if (!tag) throw new Error("clodex patch: refusing to name a pristine backup for an empty claude version");
1823
+ return tag;
1824
+ }
1825
+ function contentAddressedBackupPath(version, sha256, dir = backupDir()) {
1826
+ return join6(dir, `claude-${backupVersionTag(version)}-${sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH)}.orig`);
1827
+ }
1828
+ function legacyBackupPath(version, dir = backupDir()) {
1829
+ return join6(dir, `claude-${backupVersionTag(version)}.orig`);
1830
+ }
1831
+ function tweakccMirrorBackupPath(dir = backupDir()) {
1832
+ return join6(dir, "native-binary.backup");
1833
+ }
1834
+ function installProvenancePath(backupPath, binaryPath) {
1835
+ const tag = createHash2("sha256").update(binaryPath).digest("hex").slice(0, BACKUP_SHA_PREFIX_LENGTH);
1836
+ return `${backupPath}.for-${tag}.json`;
1837
+ }
1838
+ function installProvenancePattern(name) {
1839
+ return new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.for-[0-9a-f]{${BACKUP_SHA_PREFIX_LENGTH}}\\.json$`);
1840
+ }
1841
+ function readInstallProvenance(path) {
1842
+ let raw;
1843
+ try {
1844
+ raw = readFileSync6(path, "utf8");
1845
+ } catch {
1846
+ return null;
1847
+ }
1848
+ let parsed;
1849
+ try {
1850
+ parsed = JSON.parse(raw);
1851
+ } catch {
1852
+ return "damaged";
1853
+ }
1854
+ if (typeof parsed !== "object" || parsed === null) return "damaged";
1855
+ const { install, assumed } = parsed;
1856
+ if (typeof install !== "string" || install === "") return "damaged";
1857
+ if (typeof assumed !== "boolean") return "damaged";
1858
+ return { install, assumed };
1859
+ }
1860
+ function recordBackupProvenance(backupPath, binaryPath, opts) {
1861
+ const target = installProvenancePath(backupPath, binaryPath);
1862
+ const existing = readInstallProvenance(target);
1863
+ if (existing && existing !== "damaged" && existing.install === binaryPath) {
1864
+ if (existing.assumed === opts.assumed) return existing.assumed ? "assumed" : "established";
1865
+ if (!existing.assumed) return "established";
1866
+ }
1867
+ const record = { install: binaryPath, assumed: opts.assumed };
1868
+ const temp = `${target}.tmp-${process.pid}-${Date.now().toString(36)}`;
1869
+ try {
1870
+ writeFileSync3(temp, `${JSON.stringify(record, null, 2)}
1871
+ `);
1872
+ renameSync4(temp, target);
1873
+ return opts.assumed ? "assumed" : "established";
1874
+ } catch (err) {
1875
+ try {
1876
+ rmSync2(temp, { force: true });
1877
+ } catch {
1878
+ }
1879
+ throw err;
1880
+ }
1881
+ }
1882
+ function scanPristineBackups(version, dir = backupDir()) {
1883
+ const tag = backupVersionTag(version);
1884
+ const pattern = new RegExp(`^claude-${tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:-([0-9a-f]{${BACKUP_SHA_PREFIX_LENGTH}}))?\\.orig$`);
1885
+ let entries;
1886
+ try {
1887
+ entries = readdirSync(dir);
1888
+ } catch {
1889
+ return { valid: [], corrupt: [] };
1890
+ }
1891
+ const provenanceOf = (name) => {
1892
+ const recordPattern = installProvenancePattern(name);
1893
+ const installs = [];
1894
+ const assumedInstalls = [];
1895
+ const damagedProvenance = [];
1896
+ for (const entry of entries) {
1897
+ if (!recordPattern.test(entry)) continue;
1898
+ const path = join6(dir, entry);
1899
+ const record = readInstallProvenance(path);
1900
+ if (record === null || record === "damaged" || installProvenancePath(join6(dir, name), record.install) !== path) {
1901
+ damagedProvenance.push(path);
1902
+ continue;
1903
+ }
1904
+ (record.assumed ? assumedInstalls : installs).push(record.install);
1905
+ }
1906
+ return {
1907
+ installs: installs.sort(),
1908
+ assumedInstalls: assumedInstalls.sort(),
1909
+ damagedProvenance: damagedProvenance.sort()
1910
+ };
1911
+ };
1912
+ const valid = [];
1913
+ const corrupt = [];
1914
+ for (const entry of entries.sort()) {
1915
+ const match = pattern.exec(entry);
1916
+ if (!match) continue;
1917
+ const path = join6(dir, entry);
1918
+ let sha256;
1919
+ try {
1920
+ if (!statSync2(path).isFile()) continue;
1921
+ sha256 = sha256File(path);
1922
+ } catch {
1923
+ corrupt.push(path);
1924
+ continue;
1925
+ }
1926
+ const embedded = match[1];
1927
+ if (embedded) {
1928
+ if (sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH) !== embedded) {
1929
+ corrupt.push(path);
1930
+ continue;
1931
+ }
1932
+ valid.push({ path, kind: "content-addressed", sha256, ...provenanceOf(entry) });
1933
+ } else {
1934
+ valid.push({ path, kind: "legacy", sha256, ...provenanceOf(entry) });
1935
+ }
1936
+ }
1937
+ return { valid, corrupt };
1938
+ }
1939
+ var CLODEX_PATCH_COMMENT_PREFIX = "/*ccpatch:";
1940
+ var LEGACY_CLODEX_PATCH_MARKERS = [
1941
+ "Additional custom models: ",
1942
+ // PATCH 4 — Agent tool model description
1943
+ "function(_i){return _i.value===_o.value}",
1944
+ // PATCH 5 — picker dedupe guard
1945
+ '"clodex:'
1946
+ // PATCH 1/3 — canonical ids as model identities
1947
+ ];
1948
+ function isPatchedClaudeSource(source) {
1949
+ return source.includes(CLODEX_PATCH_COMMENT_PREFIX);
1950
+ }
1951
+ function looksLikeLegacyClodexPatch(source) {
1952
+ return !isPatchedClaudeSource(source) && LEGACY_CLODEX_PATCH_MARKERS.some((marker) => source.includes(marker));
1953
+ }
1954
+ function noBackupMessage(facts) {
1955
+ const corrupt = facts.corruptBackups?.length ? ` (${facts.corruptBackups.length} backup file(s) for this version failed integrity checks and were ignored)` : "";
1956
+ 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.`;
1957
+ }
1958
+ function identifiesABackup(manifest) {
1959
+ return manifest.pristineSha256 !== void 0 || manifest.backupPath !== void 0;
1960
+ }
1961
+ function recordedBackupGoneMessage(facts, manifest) {
1962
+ const named = manifest.backupPath ?? `the backup holding sha256 ${manifest.pristineSha256}`;
1963
+ const corrupt = facts.corruptBackups?.length ? ` (${facts.corruptBackups.length} backup file(s) for this version failed integrity checks and were ignored)` : "";
1964
+ const others = facts.backups.length ? `The ${facts.backups.length} other pristine backup(s) tagged claude ${facts.version} there were made for some other install \u2014 two installs of one Claude Code version are different files, so restoring one of them would overwrite this install with bytes that were never its own. ` : "";
1965
+ return `The patch manifest records ${named} as the pristine content of ${facts.binaryPath}, and clodex cannot use it: it is missing from ${backupDir()}, failed its integrity check, or holds a different claude version${corrupt}. ${others}Reinstall Claude Code to get a pristine binary, then run \`clodex patch\`.`;
1966
+ }
1967
+ function otherInstallMessage(facts, otherBinaryPath) {
1968
+ 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.`;
1969
+ }
1970
+ function otherInstallProvenanceMessage(facts, recorded) {
1971
+ const owners = [...new Set(recorded.flatMap((backup) => backup.installs))];
1972
+ 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.`;
1973
+ }
1974
+ function damagedProvenanceMessage(facts, damaged) {
1975
+ 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.`;
1976
+ }
1977
+ function contradictoryProvenanceMessage(facts, mine) {
1978
+ 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.`;
1979
+ }
1980
+ function manifestContradictsRecordMessage(facts, manifestChoice, recorded) {
1981
+ 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.`;
1982
+ }
1983
+ function assumedElsewhereMessage(facts, backups) {
1984
+ const others = [...new Set(backups.flatMap((backup) => backup.assumedInstalls))];
1985
+ 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.`;
1986
+ }
1987
+ function selectRestoreSource(facts) {
1988
+ const notes = [];
1989
+ let assumedForThisInstall = false;
1990
+ const recorded = facts.manifest;
1991
+ const speaksForThisVersion = !recorded?.claudeVersion || recorded.claudeVersion === facts.version;
1992
+ const manifest = recorded && recorded.binaryPath === facts.binaryPath && speaksForThisVersion ? recorded : null;
1993
+ const other = recorded && recorded.binaryPath !== facts.binaryPath && speaksForThisVersion ? recorded : null;
1994
+ let chosen = manifest?.pristineSha256 ? facts.backups.find((backup) => backup.sha256 === manifest.pristineSha256) : void 0;
1995
+ if (!chosen && manifest?.backupPath) {
1996
+ chosen = facts.backups.find((backup) => backup.path === manifest.backupPath);
1997
+ }
1998
+ if (!chosen && manifest && identifiesABackup(manifest)) {
1999
+ return { action: "error", message: recordedBackupGoneMessage(facts, manifest) };
2000
+ }
2001
+ const manifestDescribesLiveBytes = !!manifest?.patchedSha256 && manifest.patchedSha256 === facts.liveSha256;
2002
+ if (chosen && !manifestDescribesLiveBytes) {
2003
+ const manifestChoice = chosen;
2004
+ const contradicting = facts.backups.filter(
2005
+ (backup) => backup.installs.includes(facts.binaryPath) && backup.sha256 !== manifestChoice.sha256
2006
+ );
2007
+ if (contradicting.length) {
2008
+ return {
2009
+ action: "error",
2010
+ message: manifestContradictsRecordMessage(facts, manifestChoice.path, contradicting)
2011
+ };
2012
+ }
2013
+ }
2014
+ if (!chosen) {
2015
+ const mine = facts.backups.filter((backup) => backup.installs.includes(facts.binaryPath));
2016
+ if (mine.length) {
2017
+ if (new Set(mine.map((backup) => backup.sha256)).size > 1) {
2018
+ return { action: "error", message: contradictoryProvenanceMessage(facts, mine) };
2019
+ }
2020
+ chosen = mine.find((backup) => backup.kind === "content-addressed") ?? mine[0];
2021
+ }
2022
+ }
2023
+ if (!chosen) {
2024
+ if (other) {
2025
+ return facts.backups.length ? { action: "error", message: otherInstallMessage(facts, other.binaryPath) } : { action: "error", message: noBackupMessage(facts) };
2026
+ }
2027
+ const recordedElsewhere = facts.backups.filter((backup) => backup.installs.length > 0);
2028
+ if (recordedElsewhere.length) {
2029
+ return { action: "error", message: otherInstallProvenanceMessage(facts, recordedElsewhere) };
2030
+ }
2031
+ const damaged = facts.backups.flatMap((backup) => backup.damagedProvenance);
2032
+ if (damaged.length) {
2033
+ return { action: "error", message: damagedProvenanceMessage(facts, damaged) };
2034
+ }
2035
+ const assumedElsewhere = facts.backups.filter(
2036
+ (backup) => backup.assumedInstalls.some((install) => install !== facts.binaryPath)
2037
+ );
2038
+ if (assumedElsewhere.length) {
2039
+ return { action: "error", message: assumedElsewhereMessage(facts, assumedElsewhere) };
2040
+ }
2041
+ const distinct = [...new Set(facts.backups.map((backup) => backup.sha256))];
2042
+ if (distinct.length > 1) {
2043
+ return {
2044
+ action: "error",
2045
+ message: `Found conflicting pristine backups for claude ${facts.version}: ${facts.backups.map((backup) => backup.path).join(", ")}. They do not hold the same bytes, so clodex cannot tell which one is pristine. If this machine has more than one Claude Code install, both are probably genuine \u2014 one per install \u2014 and deleting either one is a guess. Reinstall the Claude Code at ${facts.binaryPath} instead: a pristine install needs no backup, and \`clodex patch\` will record its own.`
2046
+ };
2047
+ }
2048
+ chosen = facts.backups.find((backup) => backup.kind === "content-addressed") ?? facts.backups[0];
2049
+ if (chosen) {
2050
+ assumedForThisInstall = true;
2051
+ notes.push(
2052
+ `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.`
2053
+ );
2054
+ }
2055
+ }
2056
+ if (!chosen) return { action: "error", message: noBackupMessage(facts) };
2057
+ if (manifest?.pristineProvenance === "assumed") assumedForThisInstall = true;
2058
+ return {
2059
+ action: "restore",
2060
+ backupPath: chosen.path,
2061
+ pristineSha256: chosen.sha256,
2062
+ // A legacy name carries no hash, so its bytes could be anything — including
2063
+ // another version's binary, stored under a mislabeled name by an older
2064
+ // clodex. Executing it is the only evidence available; require it.
2065
+ probeVersion: chosen.kind === "legacy",
2066
+ assumedForThisInstall,
2067
+ notes
2068
+ };
2069
+ }
2070
+ function planPristineSource(facts) {
2071
+ const identical = facts.backups.find((backup) => backup.sha256 === facts.liveSha256 && backup.kind === "content-addressed") ?? facts.backups.find((backup) => backup.sha256 === facts.liveSha256);
2072
+ if (identical) {
2073
+ return { action: "reuse", backupPath: identical.path, pristineSha256: identical.sha256, notes: [] };
2074
+ }
2075
+ const manifest = facts.manifest;
2076
+ if (manifest && manifest.binaryPath === facts.binaryPath && manifest.patchedSha256 === facts.liveSha256) {
2077
+ return selectRestoreSource(facts);
2078
+ }
2079
+ return { action: "inspect" };
2080
+ }
2081
+ function planInspectedPristineSource(facts, inspection) {
2082
+ if (inspection.patched) return selectRestoreSource(facts);
2083
+ const notes = [];
2084
+ const conflicting = facts.backups.filter((backup) => backup.sha256 !== facts.liveSha256);
2085
+ if (conflicting.length) {
2086
+ notes.push(
2087
+ `Existing backup(s) for claude ${facts.version} hold different bytes (${conflicting.map((b) => b.path).join(", ")}); the binary being patched carries no clodex patch marker, so it is being stored under its own content address. Both files are kept.`
2088
+ );
2089
+ }
2090
+ return {
2091
+ action: "snapshot",
2092
+ backupPath: contentAddressedBackupPath(facts.version, facts.liveSha256),
2093
+ pristineSha256: facts.liveSha256,
2094
+ notes
2095
+ };
2096
+ }
2097
+ function planRestoreOnly(facts) {
2098
+ return selectRestoreSource(facts);
2099
+ }
2100
+ function collectPristineFacts(args) {
2101
+ const dir = args.dir ?? backupDir();
2102
+ const scan = scanPristineBackups(args.version, dir);
2103
+ return {
2104
+ version: args.version,
2105
+ binaryPath: args.binaryPath,
2106
+ liveSha256: existsSync4(args.binaryPath) ? sha256File(args.binaryPath) : "",
2107
+ manifest: args.manifest,
2108
+ backups: scan.valid,
2109
+ corruptBackups: scan.corrupt
2110
+ };
2111
+ }
2112
+
1672
2113
  export {
1673
2114
  OAUTH_ACCOUNT_ENV,
1674
2115
  getAppHome,
@@ -1714,6 +2155,10 @@ export {
1714
2155
  waitForTcpListenerCandidate,
1715
2156
  waitForTcpListener,
1716
2157
  listenTcpServer,
2158
+ writeParentNoticeLines,
2159
+ writeParentNoticeLinesSync,
2160
+ emitParentNotice,
2161
+ installParentNoticeSink,
1717
2162
  isDiscoveryDisabled,
1718
2163
  registerServerRuntimeState,
1719
2164
  unregisterServerRuntimeState,
@@ -1723,8 +2168,29 @@ export {
1723
2168
  NETWORK_ENV_CONTRACT_VAR,
1724
2169
  networkEnvBaseline,
1725
2170
  recordNetworkEnvMutation,
2171
+ wrapperSpawnShell,
1726
2172
  removeAnthropicProxyBypass,
1727
2173
  wrapperRequiresServer,
1728
- computeWrapperEnv
2174
+ wrapperIsTopLevelVsCodeHost,
2175
+ wrapperSubstitutionEligible,
2176
+ wrapperInvocationIsChat,
2177
+ computeWrapperEnv,
2178
+ getPatchManifestPath,
2179
+ readPatchManifest,
2180
+ readWrapperPatchManifest,
2181
+ backupDir,
2182
+ sha256File,
2183
+ contentAddressedBackupPath,
2184
+ legacyBackupPath,
2185
+ tweakccMirrorBackupPath,
2186
+ installProvenancePath,
2187
+ readInstallProvenance,
2188
+ recordBackupProvenance,
2189
+ isPatchedClaudeSource,
2190
+ looksLikeLegacyClodexPatch,
2191
+ planPristineSource,
2192
+ planInspectedPristineSource,
2193
+ planRestoreOnly,
2194
+ collectPristineFacts
1729
2195
  };
1730
- //# sourceMappingURL=chunk-ZM2KYLPH.js.map
2196
+ //# sourceMappingURL=chunk-3NCDMS7N.js.map