patch-cc 0.1.0__py3-none-any.whl
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.
- patch_cc/__init__.py +3 -0
- patch_cc/bun/__init__.py +6 -0
- patch_cc/bun/blob.py +256 -0
- patch_cc/bun/container.py +129 -0
- patch_cc/bun/elf.py +281 -0
- patch_cc/bun/errors.py +12 -0
- patch_cc/bun/macho.py +124 -0
- patch_cc/cache.py +86 -0
- patch_cc/cli.py +399 -0
- patch_cc/doctor.py +116 -0
- patch_cc/locate.py +117 -0
- patch_cc/menu.py +1135 -0
- patch_cc/patcher.py +244 -0
- patch_cc/patches/__init__.py +75 -0
- patch_cc/patches/agents.py +289 -0
- patch_cc/patches/base.py +200 -0
- patch_cc/patches/chrome.py +187 -0
- patch_cc/patches/output.py +173 -0
- patch_cc/patches/streaming.py +922 -0
- patch_cc/patches/thinking.py +88 -0
- patch_cc/ui.py +23 -0
- patch_cc-0.1.0.dist-info/METADATA +141 -0
- patch_cc-0.1.0.dist-info/RECORD +26 -0
- patch_cc-0.1.0.dist-info/WHEEL +4 -0
- patch_cc-0.1.0.dist-info/entry_points.txt +2 -0
- patch_cc-0.1.0.dist-info/licenses/LICENSE +21 -0
patch_cc/doctor.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Health checks over an installed binary and the patch set.
|
|
2
|
+
|
|
3
|
+
Two different questions, deliberately kept apart:
|
|
4
|
+
|
|
5
|
+
* **status** -- is the *installed* binary patched right now? Answered by the
|
|
6
|
+
manifest comment every patched bundle ends with (plus legacy fingerprints),
|
|
7
|
+
and the bytecode-stripped invariant.
|
|
8
|
+
* **dryrun** -- would our patches still apply to *this* bundle? Answered by
|
|
9
|
+
running every patch and reporting per-step hits, so a silently drifted
|
|
10
|
+
matcher shows up as a concrete "reducer.message_stop missed" instead of a
|
|
11
|
+
lump count.
|
|
12
|
+
|
|
13
|
+
The dry run feeds every configurable patch a synthetic configuration built
|
|
14
|
+
from the bundle's own discovered agents and models, so branding and the model
|
|
15
|
+
overrides are exercised for real instead of being exempted.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
from .bun import Bundle
|
|
23
|
+
from .patcher import is_patched, read_manifest
|
|
24
|
+
from .patches import ALL_PATCHES, Options, Outcome, Patch
|
|
25
|
+
from .patches.agents import INHERIT, BuiltinAgent, discover_agents, discover_models
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(slots=True)
|
|
29
|
+
class Status:
|
|
30
|
+
patched: bool
|
|
31
|
+
bytecode_stripped: bool
|
|
32
|
+
bytecode_size: int
|
|
33
|
+
#: Parsed manifest for binaries patched by this tool; ``None`` when the
|
|
34
|
+
#: binary is pristine or predates the manifest.
|
|
35
|
+
manifest: dict | None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def patch_ids(self) -> list[str]:
|
|
39
|
+
if not self.manifest:
|
|
40
|
+
return []
|
|
41
|
+
patches = self.manifest.get("patches")
|
|
42
|
+
return (
|
|
43
|
+
[p for p in patches if isinstance(p, str)]
|
|
44
|
+
if isinstance(patches, list)
|
|
45
|
+
else []
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def status(bundle: Bundle) -> Status:
|
|
50
|
+
source = bundle.source
|
|
51
|
+
return Status(
|
|
52
|
+
patched=is_patched(source),
|
|
53
|
+
bytecode_stripped=bundle.bytecode_size == 0,
|
|
54
|
+
bytecode_size=bundle.bytecode_size,
|
|
55
|
+
manifest=read_manifest(source),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(slots=True)
|
|
60
|
+
class PatchHealth:
|
|
61
|
+
patch: Patch
|
|
62
|
+
outcome: Outcome
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def ok(self) -> bool:
|
|
66
|
+
return self.outcome.landed
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(slots=True)
|
|
70
|
+
class DryRun:
|
|
71
|
+
checks: list[PatchHealth] = field(default_factory=list)
|
|
72
|
+
anchors: dict[str, dict[str, int]] = field(default_factory=dict)
|
|
73
|
+
#: What discovery found in this bundle -- the agents and model aliases the
|
|
74
|
+
#: override patch would offer.
|
|
75
|
+
agents: list[BuiltinAgent] = field(default_factory=list)
|
|
76
|
+
models: list[str] = field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def broken(self) -> list[PatchHealth]:
|
|
80
|
+
return [c for c in self.checks if not c.ok]
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def partial(self) -> list[PatchHealth]:
|
|
84
|
+
return [c for c in self.checks if c.ok and c.outcome.missed_steps()]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _synthetic_options(agents: list[BuiltinAgent], models: list[str]) -> Options:
|
|
88
|
+
"""A configuration that forces every configurable patch to do work.
|
|
89
|
+
|
|
90
|
+
Each discovered agent is assigned a model different from its current one,
|
|
91
|
+
so the rewrite (not the already-desired no-op) is what gets tested.
|
|
92
|
+
"""
|
|
93
|
+
overrides = {}
|
|
94
|
+
for agent in agents:
|
|
95
|
+
target = next((m for m in models if m != agent.effective_model), None)
|
|
96
|
+
if target is not None:
|
|
97
|
+
overrides[agent.name] = target
|
|
98
|
+
return Options(brand="patch-cc doctor", subagent_models=overrides)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def dryrun(bundle: Bundle) -> DryRun:
|
|
102
|
+
"""Run every patch against the bundle without writing anything."""
|
|
103
|
+
source = bundle.source
|
|
104
|
+
result = DryRun(
|
|
105
|
+
agents=discover_agents(source),
|
|
106
|
+
models=[INHERIT, *discover_models(source)],
|
|
107
|
+
)
|
|
108
|
+
options = _synthetic_options(result.agents, result.models)
|
|
109
|
+
|
|
110
|
+
for patch in ALL_PATCHES:
|
|
111
|
+
_, outcome = patch.run(source, options)
|
|
112
|
+
result.checks.append(PatchHealth(patch=patch, outcome=outcome))
|
|
113
|
+
if patch.anchors:
|
|
114
|
+
result.anchors[patch.id] = {a: source.count(a) for a in patch.anchors}
|
|
115
|
+
|
|
116
|
+
return result
|
patch_cc/locate.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Find the installed Claude Code native binary.
|
|
2
|
+
|
|
3
|
+
Only the native build is supported. The npm package no longer ships ``cli.js``
|
|
4
|
+
-- it is a thin wrapper that downloads the native binary -- so there is nothing
|
|
5
|
+
to patch in a node_modules tree anymore.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_VERSION_DIR = re.compile(r"^(\d+)\.(\d+)\.(\d+)")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _version_sort_key(path: Path) -> tuple[int, int, int]:
|
|
19
|
+
"""Sort key so 2.1.216 ranks above 2.1.9 (lexicographic order would not)."""
|
|
20
|
+
match = _VERSION_DIR.match(path.name)
|
|
21
|
+
if match is None:
|
|
22
|
+
return (-1, -1, -1)
|
|
23
|
+
major, minor, patch = match.groups()
|
|
24
|
+
return (int(major), int(minor), int(patch))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class Installation:
|
|
29
|
+
"""A resolved Claude install.
|
|
30
|
+
|
|
31
|
+
``launcher`` is what the user runs (often a symlink); ``binary`` is the real
|
|
32
|
+
file we patch. When Claude is installed the canonical way they differ, and
|
|
33
|
+
patching ``binary`` in place means the launcher keeps working.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
launcher: Path
|
|
37
|
+
binary: Path
|
|
38
|
+
version: str | None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_symlinked(self) -> bool:
|
|
42
|
+
return self.launcher != self.binary
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _version_of(binary: Path) -> str | None:
|
|
46
|
+
name = binary.name
|
|
47
|
+
return name if _VERSION_DIR.match(name) else None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _candidates() -> list[Path]:
|
|
51
|
+
home = Path.home()
|
|
52
|
+
found: list[Path] = []
|
|
53
|
+
|
|
54
|
+
on_path = shutil.which("claude")
|
|
55
|
+
if on_path:
|
|
56
|
+
found.append(Path(on_path))
|
|
57
|
+
|
|
58
|
+
# The default native install location, newest version first.
|
|
59
|
+
versions = home / ".local" / "share" / "claude" / "versions"
|
|
60
|
+
if versions.is_dir():
|
|
61
|
+
found.extend(sorted(versions.iterdir(), key=_version_sort_key, reverse=True))
|
|
62
|
+
|
|
63
|
+
for extra in (home / ".local" / "bin" / "claude", home / "bin" / "claude"):
|
|
64
|
+
if extra.exists():
|
|
65
|
+
found.append(extra)
|
|
66
|
+
|
|
67
|
+
return found
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve(path: Path) -> Installation | None:
|
|
71
|
+
if not path.exists():
|
|
72
|
+
return None
|
|
73
|
+
real = path.resolve()
|
|
74
|
+
if not real.is_file():
|
|
75
|
+
return None
|
|
76
|
+
return Installation(launcher=path, binary=real, version=_version_of(real))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def find() -> Installation | None:
|
|
80
|
+
"""Return the first resolvable installation, or ``None``."""
|
|
81
|
+
seen: set[Path] = set()
|
|
82
|
+
for candidate in _candidates():
|
|
83
|
+
try:
|
|
84
|
+
real = candidate.resolve()
|
|
85
|
+
except OSError:
|
|
86
|
+
continue
|
|
87
|
+
if real in seen:
|
|
88
|
+
continue
|
|
89
|
+
seen.add(real)
|
|
90
|
+
install = _resolve(candidate)
|
|
91
|
+
if install:
|
|
92
|
+
return install
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def find_or_raise() -> Installation:
|
|
97
|
+
install = find()
|
|
98
|
+
if install is None:
|
|
99
|
+
raise FileNotFoundError(
|
|
100
|
+
"Could not find a Claude Code install. Install the native build with:\n"
|
|
101
|
+
" curl -fsSL https://claude.ai/install.sh | bash"
|
|
102
|
+
)
|
|
103
|
+
return install
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def all_versions() -> list[Installation]:
|
|
107
|
+
"""Every version binary found under the native versions directory."""
|
|
108
|
+
versions = Path.home() / ".local" / "share" / "claude" / "versions"
|
|
109
|
+
if not versions.is_dir():
|
|
110
|
+
return []
|
|
111
|
+
out: list[Installation] = []
|
|
112
|
+
for entry in sorted(versions.iterdir(), key=_version_sort_key, reverse=True):
|
|
113
|
+
if entry.is_file():
|
|
114
|
+
out.append(
|
|
115
|
+
Installation(launcher=entry, binary=entry, version=_version_of(entry))
|
|
116
|
+
)
|
|
117
|
+
return out
|