@bman654/clodex 2.13.1 → 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 +6 -2
- package/dist/{chunk-ZM2KYLPH.js → chunk-3NCDMS7N.js} +468 -2
- package/dist/chunk-3NCDMS7N.js.map +1 -0
- package/dist/claude-wrapper.js +276 -7
- package/dist/claude-wrapper.js.map +1 -1
- package/dist/cli.js +541 -481
- package/dist/cli.js.map +1 -1
- package/docs/background-agents.md +35 -0
- package/docs/windows-setup.md +203 -168
- package/launcher/clodex-claude-launcher.cs +311 -0
- package/package.json +2 -1
- package/dist/chunk-ZM2KYLPH.js.map +0 -1
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
|
-
>
|
|
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
|
-
|
|
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-
|
|
2196
|
+
//# sourceMappingURL=chunk-3NCDMS7N.js.map
|