patch-cc 0.2.2__tar.gz → 0.2.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.2.2 → patch_cc-0.2.4}/PKG-INFO +2 -2
- {patch_cc-0.2.2 → patch_cc-0.2.4}/docs/PLAYBOOK.md +53 -15
- {patch_cc-0.2.2 → patch_cc-0.2.4}/pyproject.toml +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/__init__.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/__init__.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/container.py +2 -2
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/elf.py +4 -4
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/macho.py +2 -1
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/cli.py +3 -8
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/gateway.py +4 -2
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/oauth.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/doctor.py +9 -8
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/locate.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/menu.py +9 -8
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patcher.py +12 -10
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/__init__.py +2 -4
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/agents.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/base.py +5 -10
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/codex.py +2 -6
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/streaming.py +147 -296
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/ui.py +1 -1
- patch_cc-0.2.4/uv.lock +422 -0
- patch_cc-0.2.2/uv.lock +0 -358
- {patch_cc-0.2.2 → patch_cc-0.2.4}/.github/workflows/ci.yml +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/.github/workflows/release.yml +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/.gitignore +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/AGENTS.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/CLAUDE.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/LICENSE +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/README.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/docs/CONDUCT.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/docs/INTERNALS.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/docs/demo.gif +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/blob.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/errors.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/cache.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/__init__.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/models.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/translate.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/chrome.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/output.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/thinking.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: patch-cc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -46,6 +46,27 @@ constantly. These rules are what keep matchers alive:
|
|
|
46
46
|
every backup before removing any part of it: under the older, tighter callee
|
|
47
47
|
that same probe read 1 on all 8 builds, so a measurement taken before the
|
|
48
48
|
callee was dissolved does not survive it.
|
|
49
|
+
- **Rewrite at the dispatch point; never model the arm.** When an update is
|
|
50
|
+
valid at arm entry, splice it in where the code dispatches on the semantic
|
|
51
|
+
string — after a `case` label chain, or by wrapping the dispatch test
|
|
52
|
+
itself, `(test&&((update),!0))`, which preserves the test's value exactly
|
|
53
|
+
and so survives any surrounding composition (wrapping the *enclosing*
|
|
54
|
+
condition instead would run the update for the other side of a bare `||`) —
|
|
55
|
+
and say nothing about what the arm already does. Arm bodies are upstream's
|
|
56
|
+
busiest surface: 2.1.226 threaded one flag through five arms of the stream
|
|
57
|
+
reducer (`stmt1,stmt2;` became `if(stmt1,stmt2,d)call;`) and appended a
|
|
58
|
+
declarator to the head's `let`, and every literal that had modelled those
|
|
59
|
+
bodies — a cross-product of `mode(` vs `mode?.(`, braced vs unbraced arms,
|
|
60
|
+
four `displayTransform` spellings — died or was already dead weight. The
|
|
61
|
+
dispatch strings themselves (`"thinking_delta"`, `"message_stop"`) are the
|
|
62
|
+
API's vocabulary and do not churn. Same doctrine as the callee rule above:
|
|
63
|
+
the arm is *reached through*, the dispatch point is what the update is
|
|
64
|
+
*applied to*. Entry insertion does reorder against mid-arm work (the
|
|
65
|
+
message-stop reset now runs before upstream's `finalize()`), so it asks
|
|
66
|
+
that the update be independent of the arm's own channels — decide that,
|
|
67
|
+
don't assume it. Bound heads at grammar edges for the same reason — in a
|
|
68
|
+
minified bundle only `;` or `,` can follow `let{…}=t`, and which one is
|
|
69
|
+
noise (`(?=[;,])`, not `;`).
|
|
49
70
|
- Match an optional brace **pair** conditionally (`(\{)?…(?(1)\})`), never as
|
|
50
71
|
two independent `\{?` / `\}?`. A lone optional closing brace eats the
|
|
51
72
|
*enclosing* block's `}` on the unbraced shape, and a rewrite that emits its
|
|
@@ -95,10 +116,18 @@ can finish.
|
|
|
95
116
|
Every patched bundle ends with one comment line recording what was applied; its
|
|
96
117
|
shape lives in [INTERNALS.md](INTERNALS.md#the-manifest). What matters here is
|
|
97
118
|
that it makes `status` a parse instead of a guess — several patches are value
|
|
98
|
-
flips (`verbose:!0`) that leave no other fingerprint — and that
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
119
|
+
flips (`verbose:!0`) that leave no other fingerprint — and that it is the
|
|
120
|
+
**only** evidence `is_patched` accepts. Authorship is declared, never inferred:
|
|
121
|
+
`is_patched` once also took side effects of our edits as proof — the `__cc_`
|
|
122
|
+
identifier prefix, the old `--version` marker — until 2.1.227 shipped
|
|
123
|
+
`__cc_name`/`__cc_line`/`__cc_set` shell variables of its own and every
|
|
124
|
+
pristine install read as patched, hard-blocking `apply`. A prefix tracks
|
|
125
|
+
upstream's naming fashion, never authorship (the same lesson as the `.enum(`
|
|
126
|
+
callee above), and once upstream ships it, it fires on every later build, so no
|
|
127
|
+
narrowing rescues an inferred fingerprint — `__cc_line` was already both our
|
|
128
|
+
arrow-param and their shell variable. Binaries patched by pre-manifest versions
|
|
129
|
+
therefore read as clean, which costs nothing now that Claude's daily
|
|
130
|
+
auto-update has long since replaced that population.
|
|
102
131
|
|
|
103
132
|
## How resilience is detected
|
|
104
133
|
|
|
@@ -174,7 +203,7 @@ first is how a manifest starts lying. The manifest is held to the same rule:
|
|
|
174
203
|
|
|
175
204
|
## Sub-steps, and why `live-thinking` has them
|
|
176
205
|
|
|
177
|
-
Most patches are one rewrite. `live-thinking` is ~
|
|
206
|
+
Most patches are one rewrite. `live-thinking` is ~20 named sub-steps, because
|
|
178
207
|
upstream has reshaped the stream reducer at least three times and a single hit
|
|
179
208
|
count cannot tell "all landed" from "half silently drifted".
|
|
180
209
|
|
|
@@ -183,9 +212,16 @@ kinds:
|
|
|
183
212
|
|
|
184
213
|
- **Independent fixes** (`memo-cache`, `linger`, `display-mode`, `bottom-row`,
|
|
185
214
|
…) — each may or may not be present on a given build.
|
|
186
|
-
- **Mutually-exclusive reducer variants** — `reducer-
|
|
187
|
-
|
|
188
|
-
|
|
215
|
+
- **Mutually-exclusive reducer variants** — `reducer-options` (2.1.138+, the
|
|
216
|
+
options-bag head; whether the bag still destructures `onStreamingThinking`
|
|
217
|
+
is the same shape with the prop present or absent, not a second variant) and
|
|
218
|
+
`reducer-legacy` (pre-2.1.138, positional parameters). On any one build
|
|
219
|
+
exactly one should land; its count is *recognition*, one per reducer. The
|
|
220
|
+
state updates themselves are per-dispatch-point steps (`request-start`,
|
|
221
|
+
`message-stop`, `text-clear`, `message-delta-clear`, `thinking-start`,
|
|
222
|
+
`thinking-append`), so a build that folds one arm away reads as that point's
|
|
223
|
+
absence by name, never as a bare count drop inside an aggregate. On
|
|
224
|
+
2.1.216–2.1.226 the variant is `reducer-options` with all six points.
|
|
189
225
|
|
|
190
226
|
An *optional* sub-step that finds nothing is reported as absent, not broken —
|
|
191
227
|
it is just a shape this build doesn't have. A sub-step that finds a shape but
|
|
@@ -201,11 +237,11 @@ The rest carry expectations, so their absence is checked rather than noted:
|
|
|
201
237
|
but *proof*: they are credited only when the rewritten reducer body actually
|
|
202
238
|
contains the state updates, which is what recognising a reducer does not by
|
|
203
239
|
itself establish.
|
|
204
|
-
- **The `reducer` group** — `reducer-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
240
|
+
- **The `reducer` group** — `reducer-options`, `reducer-legacy`. At least one
|
|
241
|
+
must land; none landing is the signal that upstream shipped a new reducer
|
|
242
|
+
head that needs a new variant. Two landing is allowed on purpose — a
|
|
243
|
+
transitional build carrying two reducers would have both correctly patched,
|
|
244
|
+
and that is no reason to cry wolf.
|
|
209
245
|
- **Optional** — `memo-cache`, `memo-removal`, `linger`, `bottom-row`,
|
|
210
246
|
`final-summary`. The first four match nothing on 2.1.216+ and are kept for
|
|
211
247
|
older builds; `discover` is a notes-only channel that records how far back the
|
|
@@ -354,11 +390,13 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
|
354
390
|
|
|
355
391
|
### Live thinking — `streaming.py`
|
|
356
392
|
|
|
357
|
-
- **`live-thinking`** — the ~
|
|
393
|
+
- **`live-thinking`** — the ~20-step patch above. Discovery anchor is
|
|
358
394
|
`onStreamingThinking:` → `useState(null)` (the older `hidePastThinking`
|
|
359
395
|
anchor is gone as of 2.1.216 — the fallback back-scan is load-bearing).
|
|
360
396
|
Reducer anchors: `type==="stream_request_start"`, `case"thinking_delta"`,
|
|
361
|
-
`content_block_start
|
|
397
|
+
`content_block_start` — the reducer is the function carrying all three, its
|
|
398
|
+
head bounded at the declarator edge, and every state update is an insertion
|
|
399
|
+
at a dispatch point (the rule above); the arm bodies are never matched.
|
|
362
400
|
The `display-mode` sub-step defaults the request's thinking display to
|
|
363
401
|
`"summarized"`; without it the API only streams summary text when the
|
|
364
402
|
`showThinkingSummaries` setting is on. Two shapes: the legacy inline env
|
|
@@ -85,7 +85,7 @@ def write(
|
|
|
85
85
|
compared against ``source`` -- *before* it is moved into place. A rebuild
|
|
86
86
|
bug therefore fails without ever touching the live binary.
|
|
87
87
|
"""
|
|
88
|
-
import shutil
|
|
88
|
+
import shutil
|
|
89
89
|
|
|
90
90
|
new_blob = blobmod.rebuild(
|
|
91
91
|
bundle.blob, source.encode("utf8"), drop_bytecode=drop_bytecode
|
|
@@ -120,7 +120,7 @@ def verify(path: str, expected: str) -> None:
|
|
|
120
120
|
"""Re-extract from a written binary and assert it round-trips exactly."""
|
|
121
121
|
try:
|
|
122
122
|
written = read(path)
|
|
123
|
-
except Exception as exc:
|
|
123
|
+
except Exception as exc:
|
|
124
124
|
raise ContainerError(f"patched binary could not be re-read: {exc}") from exc
|
|
125
125
|
if written.source != expected:
|
|
126
126
|
raise ContainerError(
|
|
@@ -224,11 +224,11 @@ def write_section(buf: bytes, payload: bytes, name: str = ".bun") -> bytes:
|
|
|
224
224
|
and seg.filesz
|
|
225
225
|
and seg.offset >= end
|
|
226
226
|
and seg.align
|
|
227
|
+
and (seg.offset + delta) % seg.align != seg.vaddr % seg.align
|
|
227
228
|
):
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
229
|
+
raise ElfError(
|
|
230
|
+
f"shifting LOAD segment {seg.index} would break its alignment"
|
|
231
|
+
)
|
|
232
232
|
|
|
233
233
|
new_size = bun.size + delta
|
|
234
234
|
body = bytearray(payload)
|
|
@@ -27,7 +27,7 @@ class MachOError(BunError):
|
|
|
27
27
|
|
|
28
28
|
def _lief():
|
|
29
29
|
try:
|
|
30
|
-
import lief
|
|
30
|
+
import lief
|
|
31
31
|
except ImportError as exc: # pragma: no cover - platform dependent
|
|
32
32
|
raise MachOError(
|
|
33
33
|
"LIEF is required to patch macOS binaries. Install it with "
|
|
@@ -107,6 +107,7 @@ def codesign(path: str) -> None:
|
|
|
107
107
|
["codesign", "--sign", "-", "--force", path],
|
|
108
108
|
capture_output=True,
|
|
109
109
|
text=True,
|
|
110
|
+
check=False,
|
|
110
111
|
)
|
|
111
112
|
if result.returncode != 0: # pragma: no cover - macOS only
|
|
112
113
|
raise MachOError(f"codesign failed: {result.stderr.strip()}")
|
|
@@ -36,7 +36,7 @@ from pathlib import Path
|
|
|
36
36
|
from rich.markup import escape
|
|
37
37
|
|
|
38
38
|
from . import cache, locate, patcher
|
|
39
|
-
from .bun import
|
|
39
|
+
from .bun import Bundle, BunError
|
|
40
40
|
from .codex import DEFAULT_PORT, is_valid_port
|
|
41
41
|
from .codex.models import CodexModel, catalogue, is_reserved_id, is_valid_id, reconcile
|
|
42
42
|
from .patches import (
|
|
@@ -525,11 +525,6 @@ def cmd_status(args) -> int:
|
|
|
525
525
|
if isinstance(codex.get("port"), int):
|
|
526
526
|
style, note = gateway_note(codex["port"])
|
|
527
527
|
console.print(f" gateway: [{style}]{note}[/{style}]")
|
|
528
|
-
elif st.patched:
|
|
529
|
-
console.print(
|
|
530
|
-
" [dim]patched by an older patch-cc (no manifest); "
|
|
531
|
-
"re-apply to record one[/dim]"
|
|
532
|
-
)
|
|
533
528
|
console.print(
|
|
534
529
|
f" bytecode: "
|
|
535
530
|
f"{'stripped' if st.bytecode_stripped else f'{st.bytecode_size / 1e6:.0f} MB present'}"
|
|
@@ -549,7 +544,7 @@ def _doctor_target(path: str | None) -> tuple[Bundle, str] | None:
|
|
|
549
544
|
given -- that is how any kept backup becomes a regression corpus -- while the
|
|
550
545
|
installed binary falls back to its pristine copy when it is already patched.
|
|
551
546
|
"""
|
|
552
|
-
from .bun import container
|
|
547
|
+
from .bun import container
|
|
553
548
|
|
|
554
549
|
if path is not None:
|
|
555
550
|
# A name the user typed is data, not markup: an unescaped `claude[old]`
|
|
@@ -695,7 +690,7 @@ def _baked_port() -> int | None:
|
|
|
695
690
|
why ``serve`` asks once at startup rather than per request -- and why a re-bake
|
|
696
691
|
onto a different port needs a ``serve`` restart, as the apply report says.
|
|
697
692
|
"""
|
|
698
|
-
from .bun import container
|
|
693
|
+
from .bun import container
|
|
699
694
|
|
|
700
695
|
install = locate.find()
|
|
701
696
|
if install is None:
|
|
@@ -155,7 +155,7 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
155
155
|
def log_message(self, *_args) -> None: # keep the terminal clean
|
|
156
156
|
pass
|
|
157
157
|
|
|
158
|
-
def do_POST(self) -> None:
|
|
158
|
+
def do_POST(self) -> None:
|
|
159
159
|
# One request per connection: an SSE reply is open-ended (no
|
|
160
160
|
# Content-Length), so the stream ends by connection close.
|
|
161
161
|
self.close_connection = True
|
|
@@ -177,7 +177,9 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
177
177
|
# this API just as unparseable bytes are, and is answered the same
|
|
178
178
|
# way. Reaching `_messages` with one used to raise straight out of
|
|
179
179
|
# the handler thread: a traceback, a reset connection, no reply.
|
|
180
|
-
raise ValueError(
|
|
180
|
+
raise ValueError( # noqa: TRY004 - lands in json's own handler below
|
|
181
|
+
"not a JSON object"
|
|
182
|
+
)
|
|
181
183
|
except ValueError:
|
|
182
184
|
return self._error(
|
|
183
185
|
400, "invalid_request_error", "request body must be a JSON object"
|
|
@@ -74,7 +74,7 @@ class Credentials:
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
@classmethod
|
|
77
|
-
def from_json(cls, data: object) ->
|
|
77
|
+
def from_json(cls, data: object) -> Credentials | None:
|
|
78
78
|
if not isinstance(data, dict):
|
|
79
79
|
return None
|
|
80
80
|
access, refresh = data.get("access"), data.get("refresh")
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Two different questions, deliberately kept apart:
|
|
4
4
|
|
|
5
5
|
* **status** -- is the *installed* binary patched right now? Answered by the
|
|
6
|
-
manifest comment every patched bundle ends with
|
|
7
|
-
|
|
6
|
+
manifest comment every patched bundle ends with, and the bytecode-stripped
|
|
7
|
+
invariant.
|
|
8
8
|
* **dryrun** -- would our patches still apply to *this* bundle? Answered by
|
|
9
9
|
running every patch and reporting per-step hits, so a silently drifted
|
|
10
10
|
matcher shows up as a concrete "reducer.message_stop missed" instead of a
|
|
@@ -21,20 +21,23 @@ from dataclasses import dataclass, field
|
|
|
21
21
|
|
|
22
22
|
from .bun import Bundle
|
|
23
23
|
from .codex.models import CodexModel
|
|
24
|
-
from .patcher import
|
|
24
|
+
from .patcher import read_manifest
|
|
25
25
|
from .patches import ALL_PATCHES, Options, Outcome, Patch
|
|
26
26
|
from .patches.agents import INHERIT, BuiltinAgent, discover_agents, discover_models
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@dataclass(slots=True)
|
|
30
30
|
class Status:
|
|
31
|
-
patched: bool
|
|
32
31
|
bytecode_stripped: bool
|
|
33
32
|
bytecode_size: int
|
|
34
33
|
#: Parsed manifest for binaries patched by this tool; ``None`` when the
|
|
35
|
-
#: binary is pristine
|
|
34
|
+
#: binary is pristine.
|
|
36
35
|
manifest: dict | None
|
|
37
36
|
|
|
37
|
+
@property
|
|
38
|
+
def patched(self) -> bool:
|
|
39
|
+
return self.manifest is not None
|
|
40
|
+
|
|
38
41
|
@property
|
|
39
42
|
def patch_ids(self) -> list[str]:
|
|
40
43
|
if not self.manifest:
|
|
@@ -48,12 +51,10 @@ class Status:
|
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
def status(bundle: Bundle) -> Status:
|
|
51
|
-
source = bundle.source
|
|
52
54
|
return Status(
|
|
53
|
-
patched=is_patched(source),
|
|
54
55
|
bytecode_stripped=bundle.bytecode_size == 0,
|
|
55
56
|
bytecode_size=bundle.bytecode_size,
|
|
56
|
-
manifest=read_manifest(source),
|
|
57
|
+
manifest=read_manifest(bundle.source),
|
|
57
58
|
)
|
|
58
59
|
|
|
59
60
|
|
|
@@ -78,7 +78,7 @@ def _resolve(path: Path) -> Installation | None:
|
|
|
78
78
|
|
|
79
79
|
def _is_native(binary: Path) -> bool:
|
|
80
80
|
"""Is this file one of the containers we can actually open?"""
|
|
81
|
-
from .bun import BunError, container
|
|
81
|
+
from .bun import BunError, container
|
|
82
82
|
|
|
83
83
|
try:
|
|
84
84
|
container.detect(str(binary))
|
|
@@ -27,8 +27,9 @@ from __future__ import annotations
|
|
|
27
27
|
import sys
|
|
28
28
|
import textwrap
|
|
29
29
|
import threading
|
|
30
|
+
from collections.abc import Callable
|
|
30
31
|
from dataclasses import dataclass, field
|
|
31
|
-
from typing import TYPE_CHECKING, Any
|
|
32
|
+
from typing import TYPE_CHECKING, Any
|
|
32
33
|
|
|
33
34
|
from blessed import Terminal
|
|
34
35
|
from rich import box
|
|
@@ -140,7 +141,7 @@ class MenuModel:
|
|
|
140
141
|
"""Everything the menu operates on, independent of the rendering engine."""
|
|
141
142
|
|
|
142
143
|
install: locate.Installation
|
|
143
|
-
status:
|
|
144
|
+
status: Status
|
|
144
145
|
pristine: Bundle
|
|
145
146
|
agents: list[BuiltinAgent]
|
|
146
147
|
models: list[str]
|
|
@@ -154,8 +155,8 @@ class MenuModel:
|
|
|
154
155
|
|
|
155
156
|
@classmethod
|
|
156
157
|
def build(
|
|
157
|
-
cls, install: locate.Installation, status:
|
|
158
|
-
) ->
|
|
158
|
+
cls, install: locate.Installation, status: Status, pristine: Bundle
|
|
159
|
+
) -> MenuModel:
|
|
159
160
|
agents = discover_agents(pristine.source)
|
|
160
161
|
models = [INHERIT, *discover_models(pristine.source)]
|
|
161
162
|
model = cls(
|
|
@@ -577,7 +578,7 @@ class MenuApp:
|
|
|
577
578
|
self.view = "select" # select | busy | report | doctor
|
|
578
579
|
self.stack: list[tuple[Modal, Callable[[object], None] | None]] = []
|
|
579
580
|
self.report: patcher.PatchReport | None = None
|
|
580
|
-
self.doctor_result:
|
|
581
|
+
self.doctor_result: DryRun | None = None
|
|
581
582
|
self.busy_message = ""
|
|
582
583
|
self.flash = ""
|
|
583
584
|
self.exit_code = 0
|
|
@@ -642,7 +643,7 @@ class MenuApp:
|
|
|
642
643
|
attrs = termios.tcgetattr(fd)
|
|
643
644
|
attrs[0] &= ~(termios.IXON | termios.IXOFF)
|
|
644
645
|
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
|
645
|
-
except Exception:
|
|
646
|
+
except Exception: # noqa: BLE001, S110 - best effort; no termios, no freeze risk
|
|
646
647
|
pass
|
|
647
648
|
|
|
648
649
|
def _key_name(self, keystroke) -> str:
|
|
@@ -1080,7 +1081,7 @@ class MenuApp:
|
|
|
1080
1081
|
|
|
1081
1082
|
def _apply(
|
|
1082
1083
|
self, selection: cache.Selection
|
|
1083
|
-
) -> tuple[patcher.PatchReport,
|
|
1084
|
+
) -> tuple[patcher.PatchReport, Status | None]:
|
|
1084
1085
|
options = selection.options
|
|
1085
1086
|
if options.codex_models:
|
|
1086
1087
|
# Fill in each model's name and window right before baking. A pick
|
|
@@ -1154,7 +1155,7 @@ class MenuApp:
|
|
|
1154
1155
|
def run() -> None:
|
|
1155
1156
|
try:
|
|
1156
1157
|
self._worker_result = (kind, work(), None)
|
|
1157
|
-
except Exception as exc:
|
|
1158
|
+
except Exception as exc: # noqa: BLE001 - the worker's failure IS the result
|
|
1158
1159
|
self._worker_result = (kind, None, str(exc) or exc.__class__.__name__)
|
|
1159
1160
|
|
|
1160
1161
|
threading.Thread(target=run, daemon=True).start()
|
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
from . import __version__, locate
|
|
13
13
|
from .bun import Bundle, container
|
|
14
|
-
from .patches import ALL_PATCHES, DEFAULT_SUFFIX,
|
|
14
|
+
from .patches import ALL_PATCHES, DEFAULT_SUFFIX, Options, Outcome, Patch
|
|
15
15
|
|
|
16
16
|
#: Every patched bundle ends with one comment line recording exactly what was
|
|
17
17
|
#: applied. Comments cannot collide with code, survive re-extraction, and make
|
|
@@ -19,9 +19,6 @@ from .patches import ALL_PATCHES, DEFAULT_SUFFIX, SENTINEL, Options, Outcome, Pa
|
|
|
19
19
|
#: fingerprint.
|
|
20
20
|
MANIFEST_PREFIX = "//patch-cc "
|
|
21
21
|
|
|
22
|
-
#: Fingerprints of binaries patched by versions before the manifest existed.
|
|
23
|
-
_LEGACY_MARKER = "(Claude Code)\\n" + DEFAULT_SUFFIX
|
|
24
|
-
|
|
25
22
|
|
|
26
23
|
class AlreadyPatchedError(RuntimeError):
|
|
27
24
|
"""The only source available is already patched; patching it would stack.
|
|
@@ -132,11 +129,16 @@ def read_manifest(source: str) -> dict | None:
|
|
|
132
129
|
|
|
133
130
|
|
|
134
131
|
def is_patched(source: str) -> bool:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
132
|
+
"""Whether the bundle carries our manifest -- the one mark of our work.
|
|
133
|
+
|
|
134
|
+
Authorship is declared, never inferred. This used to also sniff side
|
|
135
|
+
effects of our edits (the ``__cc_`` identifier prefix, the old
|
|
136
|
+
``--version`` marker), until 2.1.227 shipped ``__cc_``-prefixed shell
|
|
137
|
+
variables of its own and every pristine install read as patched. An
|
|
138
|
+
inferred fingerprint is a bet that upstream's vocabulary never overlaps
|
|
139
|
+
ours, and once upstream ships it, it fires on every build after, forever.
|
|
140
|
+
"""
|
|
141
|
+
return read_manifest(source) is not None
|
|
140
142
|
|
|
141
143
|
|
|
142
144
|
def selected_patches(ids: list[str]) -> list[Patch]:
|
|
@@ -326,7 +328,7 @@ def restore(install: locate.Installation) -> Path:
|
|
|
326
328
|
"for this version was never saved -- reinstall to get a clean binary."
|
|
327
329
|
)
|
|
328
330
|
# A full-file copy-back, so it works for both ELF and Mach-O.
|
|
329
|
-
from .bun.elf import atomic_write
|
|
331
|
+
from .bun.elf import atomic_write
|
|
330
332
|
|
|
331
333
|
atomic_write(str(install.binary), backup.read_bytes(), mode_from=str(backup))
|
|
332
334
|
return install.binary
|
|
@@ -22,7 +22,6 @@ from .base import (
|
|
|
22
22
|
GROUP_CHROME,
|
|
23
23
|
GROUP_MODELS,
|
|
24
24
|
GROUP_OUTPUT,
|
|
25
|
-
SENTINEL,
|
|
26
25
|
Options,
|
|
27
26
|
Outcome,
|
|
28
27
|
Patch,
|
|
@@ -64,9 +63,8 @@ __all__ = [
|
|
|
64
63
|
"Options",
|
|
65
64
|
"Outcome",
|
|
66
65
|
"Patch",
|
|
67
|
-
"
|
|
66
|
+
"by_group",
|
|
67
|
+
"default_ids",
|
|
68
68
|
"derived_brand",
|
|
69
69
|
"ids",
|
|
70
|
-
"default_ids",
|
|
71
|
-
"by_group",
|
|
72
70
|
]
|
|
@@ -79,7 +79,7 @@ def _subagent_prompt(content: str, _options: Options, outcome: Outcome) -> str:
|
|
|
79
79
|
transcript_var = transcript.group(1)
|
|
80
80
|
gate_pattern = compile_js(rf"{re.escape(transcript_var)}&&({IDENT})&&")
|
|
81
81
|
|
|
82
|
-
def drop_gate(match: re.Match[str]) -> str:
|
|
82
|
+
def drop_gate(match: re.Match[str], segment: str = segment) -> str:
|
|
83
83
|
prompt_var = match.group(1)
|
|
84
84
|
nearby = segment[match.end() : match.end() + 260]
|
|
85
85
|
if f"{{prompt:{prompt_var},theme:" not in nearby:
|
|
@@ -32,11 +32,6 @@ from ..codex import DEFAULT_PORT
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
33
33
|
from ..codex.models import CodexModel
|
|
34
34
|
|
|
35
|
-
#: Prefix for every identifier we inject. These are the only reliable
|
|
36
|
-
#: fingerprints of our work -- value flips like ``verbose:!0`` are
|
|
37
|
-
#: indistinguishable from the 11 that upstream already ships.
|
|
38
|
-
SENTINEL = "__cc_"
|
|
39
|
-
|
|
40
35
|
# Groups, in display order: what renders, what runs (and how hard), how it is
|
|
41
36
|
# dressed. Generic on purpose -- a group named after a feature (Thinking,
|
|
42
37
|
# Subagents, Codex) fits exactly that feature, and the next patch that is
|
|
@@ -69,7 +64,7 @@ def derived_brand() -> str:
|
|
|
69
64
|
|
|
70
65
|
try:
|
|
71
66
|
user = getpass.getuser().strip()
|
|
72
|
-
except Exception:
|
|
67
|
+
except Exception: # noqa: BLE001 - however it fails, the answer is the same
|
|
73
68
|
user = ""
|
|
74
69
|
return f"{user}'s Code" if user else DEFAULT_BRAND
|
|
75
70
|
|
|
@@ -84,7 +79,7 @@ class Options:
|
|
|
84
79
|
#: Codex models to register and route -- ordinary patch configuration, like
|
|
85
80
|
#: every field here: chosen in the menu or with ``--codex``, and recorded in
|
|
86
81
|
#: the manifest by the patch that bakes it.
|
|
87
|
-
codex_models: list[
|
|
82
|
+
codex_models: list[CodexModel] = field(default_factory=list)
|
|
88
83
|
#: Localhost port shared by the redirect patch and the gateway. Defaulted
|
|
89
84
|
#: from the one home (:data:`patch_cc.codex.DEFAULT_PORT`) so the number
|
|
90
85
|
#: never lives in two places.
|
|
@@ -116,7 +111,7 @@ class Outcome:
|
|
|
116
111
|
applied: int = 0
|
|
117
112
|
notes: list[str] = field(default_factory=list)
|
|
118
113
|
#: Named sub-steps, for patches built from several independent rewrites.
|
|
119
|
-
steps: dict[str,
|
|
114
|
+
steps: dict[str, Outcome] = field(default_factory=dict)
|
|
120
115
|
#: What this sub-step's absence means (set via :meth:`step`). ``False`` --
|
|
121
116
|
#: a shape some builds simply lack; ``True`` -- the patch is broken without
|
|
122
117
|
#: it; a string -- name of a group of which at least one member must land.
|
|
@@ -153,7 +148,7 @@ class Outcome:
|
|
|
153
148
|
def note(self, message: str) -> None:
|
|
154
149
|
self.notes.append(message)
|
|
155
150
|
|
|
156
|
-
def step(self, name: str, expect: bool | str = False) ->
|
|
151
|
+
def step(self, name: str, expect: bool | str = False) -> Outcome:
|
|
157
152
|
"""Get (or create) a named sub-outcome.
|
|
158
153
|
|
|
159
154
|
A single scalar count cannot distinguish "all twelve rewrites landed"
|
|
@@ -167,7 +162,7 @@ class Outcome:
|
|
|
167
162
|
sub.expect = expect
|
|
168
163
|
return sub
|
|
169
164
|
|
|
170
|
-
def finalize(self) ->
|
|
165
|
+
def finalize(self) -> Outcome:
|
|
171
166
|
"""Roll sub-step totals up into this outcome."""
|
|
172
167
|
if self.steps:
|
|
173
168
|
self.candidates += sum(s.candidates for s in self.steps.values())
|
|
@@ -304,12 +304,8 @@ def _register_picker(
|
|
|
304
304
|
# because this is the only place in the picker where a model leaves Anthropic,
|
|
305
305
|
# and nothing else in the list would say so.
|
|
306
306
|
entries = ",".join(
|
|
307
|
-
"{value
|
|
308
|
-
|
|
309
|
-
js_string(value),
|
|
310
|
-
js_string(_display_name(value)),
|
|
311
|
-
js_string(_describe(model)),
|
|
312
|
-
)
|
|
307
|
+
f"{{value:{js_string(value)},label:{js_string(_display_name(value))},"
|
|
308
|
+
f"description:{js_string(_describe(model))}}}"
|
|
313
309
|
for value, model in picks
|
|
314
310
|
)
|
|
315
311
|
# Every row is emitted with no build-time "already present?" filter, and dedup
|