patch-cc 0.1.3__tar.gz → 0.1.4__tar.gz
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-0.1.3 → patch_cc-0.1.4}/PKG-INFO +8 -6
- {patch_cc-0.1.3 → patch_cc-0.1.4}/README.md +7 -5
- {patch_cc-0.1.3 → patch_cc-0.1.4}/docs/CONDUCT.md +5 -3
- {patch_cc-0.1.3 → patch_cc-0.1.4}/docs/PLAYBOOK.md +3 -2
- {patch_cc-0.1.3 → patch_cc-0.1.4}/pyproject.toml +1 -1
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/__init__.py +1 -1
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/cache.py +7 -5
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/cli.py +306 -34
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/menu.py +10 -1
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/__init__.py +1 -2
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/agents.py +1 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/base.py +5 -1
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/chrome.py +2 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/streaming.py +2 -2
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/thinking.py +2 -2
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/ui.py +17 -1
- {patch_cc-0.1.3 → patch_cc-0.1.4}/uv.lock +1 -1
- {patch_cc-0.1.3 → patch_cc-0.1.4}/.github/workflows/ci.yml +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/.github/workflows/release.yml +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/.gitignore +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/LICENSE +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/docs/INTERNALS.md +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/__init__.py +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/blob.py +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/container.py +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/elf.py +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/errors.py +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/macho.py +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/doctor.py +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/locate.py +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patcher.py +0 -0
- {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patch-cc
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Interactive patcher for the Claude Code native binary
|
|
5
5
|
Project-URL: Homepage, https://github.com/anfreire/patch-cc
|
|
6
6
|
Project-URL: Repository, https://github.com/anfreire/patch-cc
|
|
@@ -76,9 +76,9 @@ patch-cc # then just run it
|
|
|
76
76
|
|---|---|---|
|
|
77
77
|
| Output & diffs | Detailed tool calls | Show full read/search calls, not collapsed summaries |
|
|
78
78
|
| | Colour new files as diffs | Created files render with `+` lines and green |
|
|
79
|
-
| Thinking | Opt out of server-side
|
|
79
|
+
| Thinking | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
|
|
80
80
|
| | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
|
|
81
|
-
|
|
|
81
|
+
| | Stream thinking live | See reasoning as it is generated, inline and in order |
|
|
82
82
|
| Subagents | Show subagent prompts | Prompt blocks visible during normal use |
|
|
83
83
|
| | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
|
|
84
84
|
| Chrome | Disable spinner tips | No rotating tips on the spinner |
|
|
@@ -97,10 +97,11 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
|
|
|
97
97
|
uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
|
|
98
98
|
uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
|
|
99
99
|
uvx patch-cc apply --suffix "(mine)" # custom --version marker
|
|
100
|
+
uvx patch-cc apply --from-cache # replay your last remembered selection
|
|
100
101
|
uvx patch-cc status # exactly what is applied
|
|
101
102
|
uvx patch-cc doctor # do all patches match this build?
|
|
102
103
|
uvx patch-cc doctor path/to/claude # ...or match some other binary
|
|
103
|
-
uvx patch-cc list #
|
|
104
|
+
uvx patch-cc list # every patch, described
|
|
104
105
|
uvx patch-cc restore # put the original back
|
|
105
106
|
```
|
|
106
107
|
|
|
@@ -110,8 +111,9 @@ against what your installed binary actually ships.
|
|
|
110
111
|
## After a Claude update
|
|
111
112
|
|
|
112
113
|
Claude auto-updates roughly daily and replaces the binary, which reverts the
|
|
113
|
-
patch. Re-run `patch-cc` — the menu remembers your last selection —
|
|
114
|
-
your set
|
|
114
|
+
patch. Re-run `patch-cc` — the menu remembers your last selection — replay it
|
|
115
|
+
without the menu via `patch-cc apply --from-cache`, or re-apply your set
|
|
116
|
+
explicitly:
|
|
115
117
|
|
|
116
118
|
```bash
|
|
117
119
|
uvx patch-cc apply --brand --model Explore=haiku
|
|
@@ -48,9 +48,9 @@ patch-cc # then just run it
|
|
|
48
48
|
|---|---|---|
|
|
49
49
|
| Output & diffs | Detailed tool calls | Show full read/search calls, not collapsed summaries |
|
|
50
50
|
| | Colour new files as diffs | Created files render with `+` lines and green |
|
|
51
|
-
| Thinking | Opt out of server-side
|
|
51
|
+
| Thinking | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
|
|
52
52
|
| | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
|
|
53
|
-
|
|
|
53
|
+
| | Stream thinking live | See reasoning as it is generated, inline and in order |
|
|
54
54
|
| Subagents | Show subagent prompts | Prompt blocks visible during normal use |
|
|
55
55
|
| | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
|
|
56
56
|
| Chrome | Disable spinner tips | No rotating tips on the spinner |
|
|
@@ -69,10 +69,11 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
|
|
|
69
69
|
uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
|
|
70
70
|
uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
|
|
71
71
|
uvx patch-cc apply --suffix "(mine)" # custom --version marker
|
|
72
|
+
uvx patch-cc apply --from-cache # replay your last remembered selection
|
|
72
73
|
uvx patch-cc status # exactly what is applied
|
|
73
74
|
uvx patch-cc doctor # do all patches match this build?
|
|
74
75
|
uvx patch-cc doctor path/to/claude # ...or match some other binary
|
|
75
|
-
uvx patch-cc list #
|
|
76
|
+
uvx patch-cc list # every patch, described
|
|
76
77
|
uvx patch-cc restore # put the original back
|
|
77
78
|
```
|
|
78
79
|
|
|
@@ -82,8 +83,9 @@ against what your installed binary actually ships.
|
|
|
82
83
|
## After a Claude update
|
|
83
84
|
|
|
84
85
|
Claude auto-updates roughly daily and replaces the binary, which reverts the
|
|
85
|
-
patch. Re-run `patch-cc` — the menu remembers your last selection —
|
|
86
|
-
your set
|
|
86
|
+
patch. Re-run `patch-cc` — the menu remembers your last selection — replay it
|
|
87
|
+
without the menu via `patch-cc apply --from-cache`, or re-apply your set
|
|
88
|
+
explicitly:
|
|
87
89
|
|
|
88
90
|
```bash
|
|
89
91
|
uvx patch-cc apply --brand --model Explore=haiku
|
|
@@ -29,9 +29,11 @@ shape that already absorbs every case, found rather than bolted on.
|
|
|
29
29
|
brick.
|
|
30
30
|
|
|
31
31
|
- **Explicit invocations are hermetic.** A non-interactive command's arguments
|
|
32
|
-
are its whole input; no saved state may silently change what it does
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
are its whole input; no saved state may *silently* change what it does.
|
|
33
|
+
Persisted choices pre-fill the interactive UI and never act on their own — the
|
|
34
|
+
lone exception is `apply --from-cache`, which takes the cache as its *named*
|
|
35
|
+
input, so the state it reads is declared in the arguments, not hidden behind a
|
|
36
|
+
default. Absent that flag, the same command still always yields the same result.
|
|
35
37
|
|
|
36
38
|
- **Anchor matchers on meaning.** String literals, `case` labels, prop names,
|
|
37
39
|
control-flow shape — never a minified local that changes every build. A new
|
|
@@ -223,8 +223,9 @@ that broke a patch along with every note.
|
|
|
223
223
|
|
|
224
224
|
## Patch reference
|
|
225
225
|
|
|
226
|
-
Grouped
|
|
227
|
-
|
|
226
|
+
Grouped by source module — the UI folds `live-thinking` into the Thinking group,
|
|
227
|
+
but it still lives in `streaming.py`. Each entry: what it changes, the stable
|
|
228
|
+
anchor, and where it lives.
|
|
228
229
|
|
|
229
230
|
### Output & diffs — `output.py`
|
|
230
231
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Remembered interactive selection.
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
the
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
The last selection made, so the next interactive run comes up pre-filled even
|
|
4
|
+
after a Claude auto-update wiped the patched binary (and its manifest) away.
|
|
5
|
+
Written by the interactive menu and by any ``apply`` given an explicit selection
|
|
6
|
+
-- a bare ``apply`` (the default set) leaves it untouched, so it never clobbers a
|
|
7
|
+
remembered custom pick. ``apply --from-cache`` is the non-interactive reader,
|
|
8
|
+
replaying that selection when explicitly asked. Deleting the file resets the
|
|
9
|
+
menu to defaults and leaves ``--from-cache`` with nothing to replay.
|
|
8
10
|
"""
|
|
9
11
|
|
|
10
12
|
from __future__ import annotations
|
|
@@ -25,16 +25,64 @@ from pathlib import Path
|
|
|
25
25
|
|
|
26
26
|
from rich.markup import escape
|
|
27
27
|
|
|
28
|
-
from . import locate, patcher
|
|
28
|
+
from . import cache, locate, patcher
|
|
29
29
|
from .bun import BunError, Bundle
|
|
30
|
-
from .patches import
|
|
30
|
+
from .patches import (
|
|
31
|
+
DEFAULT_SUFFIX,
|
|
32
|
+
GROUP_ORDER,
|
|
33
|
+
Options,
|
|
34
|
+
Outcome,
|
|
35
|
+
Patch,
|
|
36
|
+
by_group,
|
|
37
|
+
default_ids,
|
|
38
|
+
derived_brand,
|
|
39
|
+
ids,
|
|
40
|
+
)
|
|
31
41
|
from .patches.agents import INHERIT, discover_agents, discover_models
|
|
32
|
-
from .ui import MARKS, console, err, findings, heading, ok, warn
|
|
42
|
+
from .ui import MARKS, applied_value, console, err, findings, heading, ok, warn
|
|
33
43
|
|
|
34
44
|
#: ``--brand`` with no value: derive the name from the system username.
|
|
35
45
|
_DERIVE = ""
|
|
36
46
|
|
|
37
47
|
|
|
48
|
+
def _enable_hint(patch: Patch) -> str:
|
|
49
|
+
"""How a patch is turned on: blank for the default set, else how to opt in.
|
|
50
|
+
|
|
51
|
+
Reads the patch's own ``option``, so a non-default patch says exactly how it
|
|
52
|
+
is enabled -- ``subagent-models`` needs ``--model``, ``spinner-tips`` is just
|
|
53
|
+
named -- instead of the old blanket ``(via --model)`` that mislabelled every
|
|
54
|
+
off-by-default patch.
|
|
55
|
+
"""
|
|
56
|
+
if patch.default:
|
|
57
|
+
return ""
|
|
58
|
+
if patch.option:
|
|
59
|
+
return f"(off by default; enable with {patch.option})"
|
|
60
|
+
return "(off by default; name it to apply)"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _list_hint(patch: Patch, cached: cache.Selection) -> str:
|
|
64
|
+
"""Simple, binary-free hints for `list`: how to enable a patch, its default
|
|
65
|
+
configurable value, and the value your last interactive run cached where
|
|
66
|
+
that is meaningful. The dynamic agent/model catalog lives in `apply --help`.
|
|
67
|
+
"""
|
|
68
|
+
parts: list[str] = []
|
|
69
|
+
if enable := _enable_hint(patch):
|
|
70
|
+
parts.append(enable.strip("()"))
|
|
71
|
+
opts = cached.options
|
|
72
|
+
if patch.id == "branding":
|
|
73
|
+
parts.append(f"default {derived_brand()!r}")
|
|
74
|
+
if opts.rebrands and "branding" in cached.patches:
|
|
75
|
+
parts.append(f"cached {opts.brand!r}")
|
|
76
|
+
elif patch.id == "version-marker":
|
|
77
|
+
parts.append(f"default {DEFAULT_SUFFIX!r}")
|
|
78
|
+
if opts.version_suffix != DEFAULT_SUFFIX:
|
|
79
|
+
parts.append(f"cached {opts.version_suffix!r}")
|
|
80
|
+
elif patch.id == "subagent-models" and opts.subagent_models:
|
|
81
|
+
picks = ", ".join(f"{a}={m}" for a, m in opts.subagent_models.items())
|
|
82
|
+
parts.append(f"cached {picks}")
|
|
83
|
+
return " · ".join(parts)
|
|
84
|
+
|
|
85
|
+
|
|
38
86
|
def _parse_models(specs: list[str], source: str) -> dict[str, str]:
|
|
39
87
|
"""Validate ``AGENT=MODEL`` pairs against what this bundle offers."""
|
|
40
88
|
agents = {a.name: a for a in discover_agents(source)}
|
|
@@ -96,17 +144,86 @@ def _requested(args, source: str) -> tuple[list[str], Options]:
|
|
|
96
144
|
return selected, options
|
|
97
145
|
|
|
98
146
|
|
|
147
|
+
def _has_selection_args(args) -> bool:
|
|
148
|
+
"""Did the user pass an explicit selection (vs. bare `apply` = the defaults)?
|
|
149
|
+
|
|
150
|
+
Only an explicit pick is worth remembering: caching the default set would
|
|
151
|
+
clobber a previously remembered custom selection with nothing meaningful.
|
|
152
|
+
"""
|
|
153
|
+
return bool(args.patches or args.brand is not None or args.model or args.suffix)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _valid_models(
|
|
157
|
+
models: dict[str, str], source: str
|
|
158
|
+
) -> tuple[dict[str, str], list[str]]:
|
|
159
|
+
"""Split cached overrides into those this binary still accepts and the rest.
|
|
160
|
+
|
|
161
|
+
A build can drop an agent or retire a model between the interactive apply
|
|
162
|
+
that cached the choice and a later ``--from-cache`` replay; those are skipped
|
|
163
|
+
with a warning rather than written blind, mirroring the menu's own check.
|
|
164
|
+
"""
|
|
165
|
+
known_agents = {a.name for a in discover_agents(source)}
|
|
166
|
+
known_models = {INHERIT, *discover_models(source)}
|
|
167
|
+
valid: dict[str, str] = {}
|
|
168
|
+
dropped: list[str] = []
|
|
169
|
+
for agent, model in models.items():
|
|
170
|
+
if agent in known_agents and model in known_models:
|
|
171
|
+
valid[agent] = model
|
|
172
|
+
else:
|
|
173
|
+
dropped.append(f"{agent}={model}")
|
|
174
|
+
return valid, dropped
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _from_cache(args, source: str) -> tuple[list[str], Options]:
|
|
178
|
+
"""Rebuild the last interactive selection for a non-interactive apply.
|
|
179
|
+
|
|
180
|
+
The single place a persisted choice drives an action rather than pre-filling
|
|
181
|
+
the menu -- entered explicitly via ``--from-cache``, so the cache is a named
|
|
182
|
+
argument, not hidden state (see docs/CONDUCT.md). Model overrides are
|
|
183
|
+
re-validated against the binary in hand.
|
|
184
|
+
"""
|
|
185
|
+
if args.patches or args.brand is not None or args.model or args.suffix:
|
|
186
|
+
err(
|
|
187
|
+
"--from-cache replays your last interactive selection; do not combine "
|
|
188
|
+
"it with patch ids or --brand / --model / --suffix."
|
|
189
|
+
)
|
|
190
|
+
raise SystemExit(2)
|
|
191
|
+
if not cache.cache_path().exists():
|
|
192
|
+
err(
|
|
193
|
+
"No cached selection yet. Apply once from the interactive menu "
|
|
194
|
+
"(run `patch-cc`), then `--from-cache` replays it."
|
|
195
|
+
)
|
|
196
|
+
raise SystemExit(2)
|
|
197
|
+
|
|
198
|
+
selection = cache.load()
|
|
199
|
+
options = selection.options
|
|
200
|
+
selected = list(selection.patches)
|
|
201
|
+
if options.subagent_models:
|
|
202
|
+
valid, dropped = _valid_models(options.subagent_models, source)
|
|
203
|
+
options.subagent_models = valid
|
|
204
|
+
if dropped:
|
|
205
|
+
warn(
|
|
206
|
+
"cached model override(s) not valid for this build, skipped: "
|
|
207
|
+
+ ", ".join(dropped)
|
|
208
|
+
)
|
|
209
|
+
if not valid and "subagent-models" in selected:
|
|
210
|
+
selected.remove("subagent-models")
|
|
211
|
+
return selected, options
|
|
212
|
+
|
|
213
|
+
|
|
99
214
|
def _print_findings(outcome: Outcome) -> None:
|
|
100
215
|
"""The detail under a patch line -- worded in :func:`ui.findings`."""
|
|
101
216
|
for style, text in findings(outcome):
|
|
102
217
|
console.print(f" [{style}]· {text}[/{style}]")
|
|
103
218
|
|
|
104
219
|
|
|
105
|
-
def _print_report(report: patcher.PatchReport) -> None:
|
|
220
|
+
def _print_report(report: patcher.PatchReport, options: Options) -> None:
|
|
106
221
|
heading("Patch results")
|
|
107
222
|
for patch, outcome in report.results:
|
|
108
223
|
mark, colour = MARKS[outcome.health]
|
|
109
224
|
detail = f" applied {outcome.applied}" if outcome.applied else ""
|
|
225
|
+
if outcome.applied and (value := applied_value(patch, options)):
|
|
226
|
+
detail += f" [dim]→ {escape(value)}[/dim]"
|
|
110
227
|
console.print(f" [{colour}]{mark}[/{colour}] {patch.title:28s}{detail}")
|
|
111
228
|
_print_findings(outcome)
|
|
112
229
|
|
|
@@ -139,9 +256,15 @@ def _print_report(report: patcher.PatchReport) -> None:
|
|
|
139
256
|
def cmd_apply(args) -> int:
|
|
140
257
|
install = locate.find_or_raise()
|
|
141
258
|
bundle = patcher.read_pristine(install)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
259
|
+
if args.from_cache:
|
|
260
|
+
selected, options = _from_cache(args, bundle.source)
|
|
261
|
+
else:
|
|
262
|
+
selected, options = _requested(args, bundle.source)
|
|
263
|
+
|
|
264
|
+
version = install.version or "?"
|
|
265
|
+
name = install.binary.name
|
|
266
|
+
where = version if name == version else f"{version} ({name})"
|
|
267
|
+
heading(f"Patching Claude {where}")
|
|
145
268
|
try:
|
|
146
269
|
report = patcher.patch_installation(install, selected, options, bundle=bundle)
|
|
147
270
|
except patcher.AlreadyPatchedError as exc:
|
|
@@ -151,8 +274,14 @@ def cmd_apply(args) -> int:
|
|
|
151
274
|
err(str(exc))
|
|
152
275
|
return 1
|
|
153
276
|
|
|
154
|
-
_print_report(report)
|
|
277
|
+
_print_report(report, options)
|
|
155
278
|
if report.output is not None:
|
|
279
|
+
# Remember an explicit pick so `apply --from-cache` and the menu can
|
|
280
|
+
# replay it -- but never a bare `apply`, which would overwrite a
|
|
281
|
+
# remembered selection with the defaults. `--from-cache` never reaches
|
|
282
|
+
# here with selection args, so a replay does not re-cache itself.
|
|
283
|
+
if _has_selection_args(args):
|
|
284
|
+
cache.save(cache.Selection(patches=selected, options=options))
|
|
156
285
|
console.print("\n[dim]Restart Claude Code for changes to take effect.[/dim]")
|
|
157
286
|
return 0 if report.ok else 1
|
|
158
287
|
|
|
@@ -279,32 +408,25 @@ def cmd_doctor(args) -> int:
|
|
|
279
408
|
|
|
280
409
|
|
|
281
410
|
def cmd_list(args) -> int:
|
|
411
|
+
"""The quick catalog: id, description, simple hints. Registry + cache only --
|
|
412
|
+
|
|
413
|
+
no binary read, so it is fast and works anywhere. The dynamic agent/model
|
|
414
|
+
catalog and full usage live in `apply --help`.
|
|
415
|
+
"""
|
|
282
416
|
heading("Available patches")
|
|
417
|
+
console.print(
|
|
418
|
+
" [dim]apply with `patch-cc apply <id>`; "
|
|
419
|
+
"see `apply --help` for flags, examples & your binary's agents/models[/dim]"
|
|
420
|
+
)
|
|
421
|
+
cached = cache.load()
|
|
283
422
|
for group, patches in by_group().items():
|
|
284
423
|
if not patches:
|
|
285
424
|
continue
|
|
286
425
|
console.print(f"\n[bold]{group}[/bold]")
|
|
287
426
|
for patch in patches:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
install = locate.find()
|
|
292
|
-
if install is None:
|
|
293
|
-
return 0
|
|
294
|
-
try:
|
|
295
|
-
source = patcher.read_pristine(install).source
|
|
296
|
-
except (BunError, OSError):
|
|
297
|
-
return 0
|
|
298
|
-
agents = discover_agents(source)
|
|
299
|
-
if agents:
|
|
300
|
-
console.print(f"\n[bold]Subagents in Claude {install.version or '?'}[/bold]")
|
|
301
|
-
for agent in agents:
|
|
302
|
-
console.print(
|
|
303
|
-
f" [cyan]{agent.name:18s}[/cyan] default model: {agent.effective_model}"
|
|
304
|
-
)
|
|
305
|
-
console.print(
|
|
306
|
-
f" [dim]models: {', '.join([INHERIT, *discover_models(source)])}[/dim]"
|
|
307
|
-
)
|
|
427
|
+
console.print(f" [cyan]{patch.id:18s}[/cyan] {patch.summary}")
|
|
428
|
+
if hint := _list_hint(patch, cached):
|
|
429
|
+
console.print(f" [dim]{'':18s} {hint}[/dim]")
|
|
308
430
|
return 0
|
|
309
431
|
|
|
310
432
|
|
|
@@ -335,36 +457,182 @@ def cmd_menu(args) -> int:
|
|
|
335
457
|
return run_menu()
|
|
336
458
|
|
|
337
459
|
|
|
460
|
+
_MAIN_EPILOG = """\
|
|
461
|
+
Run with no arguments to open the interactive menu.
|
|
462
|
+
|
|
463
|
+
common tasks:
|
|
464
|
+
patch-cc apply apply the default patch set to the installed binary
|
|
465
|
+
patch-cc apply --help every patch id, the flags, and worked examples
|
|
466
|
+
patch-cc status show exactly what is applied right now
|
|
467
|
+
patch-cc doctor check every patch still matches this build
|
|
468
|
+
patch-cc list describe every patch: ids, descriptions, hints
|
|
469
|
+
patch-cc restore put the original binary back from backup
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _discover_binary() -> tuple[str | None, str | None]:
|
|
474
|
+
"""The installed binary's JS source and version, or ``(None, version?)``.
|
|
475
|
+
|
|
476
|
+
Best-effort: ``apply --help`` reads the real binary so its agent/model list
|
|
477
|
+
matches what ``--model`` accepts, but a missing or unreadable install just
|
|
478
|
+
drops the dynamic block instead of failing the help.
|
|
479
|
+
"""
|
|
480
|
+
install = locate.find()
|
|
481
|
+
if install is None:
|
|
482
|
+
return None, None
|
|
483
|
+
try:
|
|
484
|
+
return patcher.read_pristine(install).source, install.version
|
|
485
|
+
except (BunError, OSError):
|
|
486
|
+
return None, install.version
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _example_model(models: list[str], offset: int = 0) -> str:
|
|
490
|
+
"""A concrete (non-inherit) model alias for a ``--model`` example."""
|
|
491
|
+
concrete = [m for m in models if m != INHERIT]
|
|
492
|
+
return concrete[offset % len(concrete)] if concrete else INHERIT
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _apply_epilog() -> str:
|
|
496
|
+
"""Build ``apply --help`` from the registry *and* the installed binary.
|
|
497
|
+
|
|
498
|
+
Nothing is hardcoded: the patch list and default markers come from the
|
|
499
|
+
registry, the subagent agents and models from the real binary. The binary
|
|
500
|
+
read is deferred to the help action, so only ``apply --help`` pays for it and
|
|
501
|
+
a missing install degrades to the static parts.
|
|
502
|
+
"""
|
|
503
|
+
source, version = _discover_binary()
|
|
504
|
+
agents = discover_agents(source) if source else []
|
|
505
|
+
models = [INHERIT, *discover_models(source)] if source else []
|
|
506
|
+
grouped = by_group()
|
|
507
|
+
|
|
508
|
+
lines = ["patches (name them to apply exactly those; * = the default set):", ""]
|
|
509
|
+
for group in GROUP_ORDER:
|
|
510
|
+
group_patches = grouped.get(group, [])
|
|
511
|
+
if not group_patches:
|
|
512
|
+
continue
|
|
513
|
+
lines.append(f" {group}")
|
|
514
|
+
for patch in group_patches:
|
|
515
|
+
mark = "*" if patch.default else " "
|
|
516
|
+
hint = _enable_hint(patch)
|
|
517
|
+
row = f" {mark} {patch.id:20s}{patch.title}"
|
|
518
|
+
lines.append(f"{row} {hint}" if hint else row)
|
|
519
|
+
lines.append("")
|
|
520
|
+
|
|
521
|
+
lines += [
|
|
522
|
+
"naming ids replaces the default set with exactly what you name; the",
|
|
523
|
+
"flags below always add their own patch on top of whatever you named.",
|
|
524
|
+
"",
|
|
525
|
+
"configuring patches:",
|
|
526
|
+
f" --brand [NAME] selects branding · no value -> {derived_brand()!r}",
|
|
527
|
+
f" --suffix TEXT selects version-marker · default {DEFAULT_SUFFIX!r}",
|
|
528
|
+
" --model AGENT=MODEL selects subagent-models · repeatable, one per agent",
|
|
529
|
+
]
|
|
530
|
+
if agents:
|
|
531
|
+
lines += [
|
|
532
|
+
f" agents (Claude {version or '?'}): "
|
|
533
|
+
+ " ".join(a.name for a in agents),
|
|
534
|
+
" models: " + " ".join(models),
|
|
535
|
+
]
|
|
536
|
+
elif source is None:
|
|
537
|
+
lines.append(
|
|
538
|
+
" (the agents/models list shows when a Claude install is present)"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
ex_one = f"{agents[0].name}={_example_model(models)}" if agents else "Explore=haiku"
|
|
542
|
+
ex_two = f"{agents[1].name}={_example_model(models, 1)}" if len(agents) > 1 else ""
|
|
543
|
+
|
|
544
|
+
lines += [
|
|
545
|
+
"",
|
|
546
|
+
"examples:",
|
|
547
|
+
" patch-cc apply",
|
|
548
|
+
" the default set (the * patches above)",
|
|
549
|
+
" patch-cc apply tool-calls live-thinking",
|
|
550
|
+
" only these two; the default set is replaced",
|
|
551
|
+
" patch-cc apply --brand",
|
|
552
|
+
f" the default set, branded {derived_brand()!r}",
|
|
553
|
+
' patch-cc apply --brand "Ada\'s Code" --suffix "(ada)"',
|
|
554
|
+
" default set with an explicit startup name and version marker",
|
|
555
|
+
f" patch-cc apply --model {ex_one}" + (f" --model {ex_two}" if ex_two else ""),
|
|
556
|
+
" default set plus subagent model override" + ("s" if ex_two else ""),
|
|
557
|
+
" patch-cc apply --from-cache",
|
|
558
|
+
" re-apply your last interactive menu selection (saved by `patch-cc`)",
|
|
559
|
+
]
|
|
560
|
+
return "\n".join(lines)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
class _ApplyHelpAction(argparse.Action):
|
|
564
|
+
"""``apply -h/--help``: assemble the dynamic epilog, then print help.
|
|
565
|
+
|
|
566
|
+
Deferred here rather than at parser-build time so only ``apply --help`` reads
|
|
567
|
+
the binary -- every other ``patch-cc`` invocation stays fast and needs no
|
|
568
|
+
install.
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
def __init__(self, option_strings, dest, **kwargs):
|
|
572
|
+
super().__init__(
|
|
573
|
+
option_strings, dest, nargs=0, default=argparse.SUPPRESS, **kwargs
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
577
|
+
parser.epilog = _apply_epilog()
|
|
578
|
+
parser.print_help()
|
|
579
|
+
parser.exit()
|
|
580
|
+
|
|
581
|
+
|
|
338
582
|
def build_parser() -> argparse.ArgumentParser:
|
|
339
583
|
parser = argparse.ArgumentParser(
|
|
340
584
|
prog="patch-cc",
|
|
341
585
|
description="Interactive patcher for the Claude Code native binary.",
|
|
586
|
+
epilog=_MAIN_EPILOG,
|
|
587
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
342
588
|
)
|
|
343
589
|
parser.set_defaults(func=cmd_menu)
|
|
344
590
|
sub = parser.add_subparsers(dest="command")
|
|
345
591
|
|
|
346
|
-
p_apply = sub.add_parser(
|
|
592
|
+
p_apply = sub.add_parser(
|
|
593
|
+
"apply",
|
|
594
|
+
help="apply patches to the installed binary",
|
|
595
|
+
description="Apply patches to the installed Claude binary. Always starts "
|
|
596
|
+
"from a pristine copy, so re-applying replaces the previous set rather "
|
|
597
|
+
"than stacking on it.",
|
|
598
|
+
add_help=False,
|
|
599
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
600
|
+
)
|
|
601
|
+
p_apply.add_argument(
|
|
602
|
+
"-h",
|
|
603
|
+
"--help",
|
|
604
|
+
action=_ApplyHelpAction,
|
|
605
|
+
help="show this help (reads the installed binary for its agents/models)",
|
|
606
|
+
)
|
|
347
607
|
p_apply.add_argument(
|
|
348
608
|
"patches",
|
|
349
609
|
nargs="*",
|
|
350
610
|
metavar="PATCH",
|
|
351
|
-
help="patch ids to apply (default: the default set)",
|
|
611
|
+
help="patch ids to apply (default: the default set; all ids listed below)",
|
|
612
|
+
)
|
|
613
|
+
p_apply.add_argument(
|
|
614
|
+
"--from-cache",
|
|
615
|
+
action="store_true",
|
|
616
|
+
help="re-apply your last interactive menu selection (ignores other args)",
|
|
352
617
|
)
|
|
353
618
|
p_apply.add_argument(
|
|
354
619
|
"--brand",
|
|
355
620
|
nargs="?",
|
|
356
621
|
const=_DERIVE,
|
|
357
622
|
metavar="NAME",
|
|
358
|
-
help="
|
|
623
|
+
help="startup name; selects `branding` (no value: <username>'s Code)",
|
|
359
624
|
)
|
|
360
625
|
p_apply.add_argument(
|
|
361
626
|
"--model",
|
|
362
627
|
action="append",
|
|
363
628
|
metavar="AGENT=MODEL",
|
|
364
|
-
help="
|
|
629
|
+
help="set a subagent's model; selects `subagent-models` (repeatable)",
|
|
365
630
|
)
|
|
366
631
|
p_apply.add_argument(
|
|
367
|
-
"--suffix",
|
|
632
|
+
"--suffix",
|
|
633
|
+
metavar="TEXT",
|
|
634
|
+
help="`claude --version` marker text; selects `version-marker` "
|
|
635
|
+
"(default: (patched))",
|
|
368
636
|
)
|
|
369
637
|
p_apply.set_defaults(func=cmd_apply)
|
|
370
638
|
|
|
@@ -381,7 +649,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
381
649
|
)
|
|
382
650
|
p_doctor.set_defaults(func=cmd_doctor)
|
|
383
651
|
sub.add_parser(
|
|
384
|
-
"list",
|
|
652
|
+
"list",
|
|
653
|
+
help="describe every patch + the subagent models your binary offers",
|
|
654
|
+
description="Describe every patch (what it does, whether it is on by "
|
|
655
|
+
"default, and how to enable it) plus the subagent models discovered in "
|
|
656
|
+
"your installed binary. For apply syntax and examples, see `apply --help`.",
|
|
385
657
|
).set_defaults(func=cmd_list)
|
|
386
658
|
sub.add_parser(
|
|
387
659
|
"restore", help="restore the original binary from backup"
|
|
@@ -49,7 +49,7 @@ from .patches import (
|
|
|
49
49
|
derived_brand,
|
|
50
50
|
)
|
|
51
51
|
from .patches.agents import INHERIT, BuiltinAgent, discover_agents, discover_models
|
|
52
|
-
from .ui import MARKS, console, err, findings
|
|
52
|
+
from .ui import MARKS, applied_value, console, err, findings
|
|
53
53
|
|
|
54
54
|
if TYPE_CHECKING:
|
|
55
55
|
from .doctor import DryRun, Status
|
|
@@ -1028,12 +1028,15 @@ class MenuApp:
|
|
|
1028
1028
|
report = self.report
|
|
1029
1029
|
if report is None:
|
|
1030
1030
|
return lines, None
|
|
1031
|
+
options = self.model.selection().options
|
|
1031
1032
|
for patch, outcome in report.results:
|
|
1032
1033
|
mark, style = MARKS[outcome.health]
|
|
1033
1034
|
line = Text()
|
|
1034
1035
|
line.append(f" {mark} ", style=style)
|
|
1035
1036
|
line.append(f"{patch.title:<32}")
|
|
1036
1037
|
line.append(f"{outcome.applied or '':>3}", style="dim")
|
|
1038
|
+
if outcome.applied and (value := applied_value(patch, options)):
|
|
1039
|
+
line.append(f" → {value}", style="dim")
|
|
1037
1040
|
lines.append(line)
|
|
1038
1041
|
lines += _findings(outcome)
|
|
1039
1042
|
|
|
@@ -1063,6 +1066,12 @@ class MenuApp:
|
|
|
1063
1066
|
)
|
|
1064
1067
|
lines.append(line)
|
|
1065
1068
|
lines.append(Text(" Restart Claude Code to see it.", style="dim"))
|
|
1069
|
+
lines.append(
|
|
1070
|
+
Text(
|
|
1071
|
+
" Reapply this selection anytime: patch-cc apply --from-cache",
|
|
1072
|
+
style="dim",
|
|
1073
|
+
)
|
|
1074
|
+
)
|
|
1066
1075
|
return lines, None
|
|
1067
1076
|
|
|
1068
1077
|
def _body_doctor(self, panel_width: int) -> tuple[list[Text], int | None]:
|
|
@@ -13,7 +13,6 @@ from .base import (
|
|
|
13
13
|
DEFAULT_SUFFIX,
|
|
14
14
|
GROUP_AGENTS,
|
|
15
15
|
GROUP_CHROME,
|
|
16
|
-
GROUP_LIVE,
|
|
17
16
|
GROUP_OUTPUT,
|
|
18
17
|
GROUP_THINKING,
|
|
19
18
|
SENTINEL,
|
|
@@ -31,7 +30,7 @@ ALL_PATCHES: list[Patch] = [
|
|
|
31
30
|
*chrome.PATCHES,
|
|
32
31
|
]
|
|
33
32
|
|
|
34
|
-
GROUP_ORDER = [GROUP_OUTPUT, GROUP_THINKING,
|
|
33
|
+
GROUP_ORDER = [GROUP_OUTPUT, GROUP_THINKING, GROUP_AGENTS, GROUP_CHROME]
|
|
35
34
|
|
|
36
35
|
_BY_ID = {patch.id: patch for patch in ALL_PATCHES}
|
|
37
36
|
|
|
@@ -33,7 +33,6 @@ SENTINEL = "__cc_"
|
|
|
33
33
|
# Groups, in display order.
|
|
34
34
|
GROUP_OUTPUT = "Output & diffs"
|
|
35
35
|
GROUP_THINKING = "Thinking"
|
|
36
|
-
GROUP_LIVE = "Live thinking (streaming)"
|
|
37
36
|
GROUP_AGENTS = "Subagents"
|
|
38
37
|
GROUP_CHROME = "Chrome & branding"
|
|
39
38
|
|
|
@@ -218,6 +217,11 @@ class Patch:
|
|
|
218
217
|
group: str
|
|
219
218
|
fn: PatchFn
|
|
220
219
|
default: bool = True
|
|
220
|
+
#: The CLI flag that configures and auto-selects this patch (``--brand``,
|
|
221
|
+
#: ``--model``, ``--suffix``); ``None`` for patches selected only by id. The
|
|
222
|
+
#: single home for the patch<->flag coupling, so ``list`` and ``apply
|
|
223
|
+
#: --help`` can say how an opt-in patch is turned on rather than inferring it.
|
|
224
|
+
option: str | None = None
|
|
221
225
|
#: Anchors to report on when this patch stops matching.
|
|
222
226
|
anchors: tuple[str, ...] = ()
|
|
223
227
|
|
|
@@ -184,6 +184,7 @@ PATCHES = [
|
|
|
184
184
|
summary="Append a marker line to `claude --version` (custom text via --suffix).",
|
|
185
185
|
group=GROUP_CHROME,
|
|
186
186
|
fn=_version_output,
|
|
187
|
+
option="--suffix",
|
|
187
188
|
anchors=("}.VERSION} (Claude Code)",),
|
|
188
189
|
),
|
|
189
190
|
Patch(
|
|
@@ -192,6 +193,7 @@ PATCHES = [
|
|
|
192
193
|
summary="Rename the startup/help branding (default: your username's Code).",
|
|
193
194
|
group=GROUP_CHROME,
|
|
194
195
|
fn=_branding,
|
|
196
|
+
option="--brand",
|
|
195
197
|
anchors=('"Welcome to Claude Code"', '{bold:!0},"Claude Code"'),
|
|
196
198
|
),
|
|
197
199
|
]
|
|
@@ -23,7 +23,7 @@ from __future__ import annotations
|
|
|
23
23
|
import re
|
|
24
24
|
from dataclasses import dataclass
|
|
25
25
|
|
|
26
|
-
from .base import
|
|
26
|
+
from .base import GROUP_THINKING, IDENT, Options, Outcome, Patch, compile_js, splice
|
|
27
27
|
|
|
28
28
|
#: The mutually-exclusive reducer variants. On any one build one of them lands;
|
|
29
29
|
#: none landing means upstream shipped a fourth shape and live thinking is dead.
|
|
@@ -974,7 +974,7 @@ PATCHES = [
|
|
|
974
974
|
title="Stream thinking live",
|
|
975
975
|
summary="Show thinking as it is generated, inline and in order, instead of "
|
|
976
976
|
"only after the turn finishes.",
|
|
977
|
-
group=
|
|
977
|
+
group=GROUP_THINKING,
|
|
978
978
|
fn=_live_thinking,
|
|
979
979
|
anchors=(
|
|
980
980
|
"onStreamingThinking:",
|
|
@@ -192,8 +192,8 @@ def _thinking_summaries(content: str, _options: Options, outcome: Outcome) -> st
|
|
|
192
192
|
PATCHES = [
|
|
193
193
|
Patch(
|
|
194
194
|
id="thinking-summaries",
|
|
195
|
-
title="
|
|
196
|
-
summary="
|
|
195
|
+
title="Fix blank thinking blocks",
|
|
196
|
+
summary="Opt out of the server-side experiment bucket that can return empty thinking blocks on some accounts. Drops all experiment enrollment, not just this one.",
|
|
197
197
|
group=GROUP_THINKING,
|
|
198
198
|
fn=_thinking_summaries,
|
|
199
199
|
anchors=(_BUCKET_HEADER, "?.atis"),
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
|
|
7
|
-
from .patches.base import Outcome
|
|
7
|
+
from .patches.base import Options, Outcome, Patch
|
|
8
8
|
|
|
9
9
|
console = Console()
|
|
10
10
|
|
|
@@ -37,6 +37,22 @@ def findings(outcome: Outcome) -> list[tuple[str, str]]:
|
|
|
37
37
|
return lines + [("dim", note) for note in notes]
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def applied_value(patch: Patch, options: Options) -> str | None:
|
|
41
|
+
"""The value a configurable patch actually wrote, for the report line.
|
|
42
|
+
|
|
43
|
+
Branding, the version marker, and model overrides each carry a chosen value;
|
|
44
|
+
a plain toggle patch carries none. One source so the CLI and the menu report
|
|
45
|
+
the same thing after an apply.
|
|
46
|
+
"""
|
|
47
|
+
if patch.id == "branding":
|
|
48
|
+
return options.brand
|
|
49
|
+
if patch.id == "version-marker":
|
|
50
|
+
return options.version_suffix
|
|
51
|
+
if patch.id == "subagent-models" and options.subagent_models:
|
|
52
|
+
return ", ".join(f"{a}={m}" for a, m in options.subagent_models.items())
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
40
56
|
def heading(text: str) -> None:
|
|
41
57
|
console.print(f"\n[bold]{text}[/bold]")
|
|
42
58
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|