patch-cc 0.2.0__tar.gz → 0.2.2__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.
Files changed (41) hide show
  1. {patch_cc-0.2.0 → patch_cc-0.2.2}/PKG-INFO +34 -21
  2. {patch_cc-0.2.0 → patch_cc-0.2.2}/README.md +33 -20
  3. {patch_cc-0.2.0 → patch_cc-0.2.2}/docs/INTERNALS.md +1 -1
  4. {patch_cc-0.2.0 → patch_cc-0.2.2}/docs/PLAYBOOK.md +76 -6
  5. patch_cc-0.2.2/docs/demo.gif +0 -0
  6. {patch_cc-0.2.0 → patch_cc-0.2.2}/pyproject.toml +1 -1
  7. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/__init__.py +1 -1
  8. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/cache.py +3 -0
  9. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/cli.py +27 -2
  10. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/doctor.py +6 -1
  11. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/menu.py +37 -9
  12. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/patcher.py +5 -0
  13. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/patches/__init__.py +2 -4
  14. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/patches/agents.py +14 -4
  15. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/patches/base.py +24 -5
  16. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/patches/chrome.py +56 -0
  17. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/patches/codex.py +2 -2
  18. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/patches/streaming.py +2 -2
  19. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/patches/thinking.py +81 -3
  20. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/ui.py +2 -0
  21. {patch_cc-0.2.0 → patch_cc-0.2.2}/uv.lock +1 -1
  22. {patch_cc-0.2.0 → patch_cc-0.2.2}/.github/workflows/ci.yml +0 -0
  23. {patch_cc-0.2.0 → patch_cc-0.2.2}/.github/workflows/release.yml +0 -0
  24. {patch_cc-0.2.0 → patch_cc-0.2.2}/.gitignore +0 -0
  25. {patch_cc-0.2.0 → patch_cc-0.2.2}/AGENTS.md +0 -0
  26. {patch_cc-0.2.0 → patch_cc-0.2.2}/CLAUDE.md +0 -0
  27. {patch_cc-0.2.0 → patch_cc-0.2.2}/LICENSE +0 -0
  28. {patch_cc-0.2.0 → patch_cc-0.2.2}/docs/CONDUCT.md +0 -0
  29. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/bun/__init__.py +0 -0
  30. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/bun/blob.py +0 -0
  31. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/bun/container.py +0 -0
  32. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/bun/elf.py +0 -0
  33. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/bun/errors.py +0 -0
  34. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/bun/macho.py +0 -0
  35. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/codex/__init__.py +0 -0
  36. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/codex/gateway.py +0 -0
  37. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/codex/models.py +0 -0
  38. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/codex/oauth.py +0 -0
  39. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/codex/translate.py +0 -0
  40. {patch_cc-0.2.0 → patch_cc-0.2.2}/src/patch_cc/locate.py +0 -0
  41. {patch_cc-0.2.0 → patch_cc-0.2.2}/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.2.0
3
+ Version: 0.2.2
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
@@ -34,8 +34,16 @@ Description-Content-Type: text/markdown
34
34
 
35
35
  An interactive patcher for the **Claude Code native binary**. Pick the tweaks
36
36
  you want — inline and live thinking, detailed tool calls, subagent model
37
- overrides, your own startup name and apply them to your installed `claude`
38
- in one keystroke. Pure Python; no Node, no Bun.
37
+ overrides, your own startup name, your **ChatGPT/Codex-plan GPT models as
38
+ native models** and apply them to your installed `claude` in one keystroke.
39
+ Fully reversible: a pristine backup is kept, `patch-cc restore` puts it back.
40
+ Pure Python; no Node, no Bun.
41
+
42
+ ```bash
43
+ uvx patch-cc # fullscreen menu, no install needed
44
+ ```
45
+
46
+ ![patch-cc: pick tweaks, register a Codex model, pin the Plan agent to it, bake — the patched binary is 161 MB smaller](https://raw.githubusercontent.com/anfreire/patch-cc/main/docs/demo.gif)
39
47
 
40
48
  ## Requirements
41
49
 
@@ -48,17 +56,14 @@ in one keystroke. Pure Python; no Node, no Bun.
48
56
  - **macOS only:** the Xcode command line tools, for `codesign` — a patched
49
57
  binary has to be re-signed or macOS refuses to run it.
50
58
 
51
- ```bash
52
- uvx patch-cc # fullscreen menu, no install needed
53
- ```
54
-
55
59
  The menu is a single centered panel: move with `↑ ↓`, toggle with `space`,
56
60
  press `s` to save. Patches that carry a setting — subagent models, Codex models,
57
- the startup name, the `--version` marker — open a centered modal on `enter`, and
58
- the row then shows what you chose. Everything choosable is a picker: the agent
59
- names and model aliases are **discovered from your binary itself** (and the
60
- Codex ones from your plan), so the menu can never offer something your build
61
- would reject. Typing exists only for the genuinely free-text values.
61
+ the startup name, the `--version` marker, the org/email label — open a centered
62
+ modal on `enter`, and the row then shows what you chose. Everything choosable
63
+ is a picker: the agent names and model aliases are **discovered from your
64
+ binary itself** (and the Codex ones from your plan), so the menu can never
65
+ offer something your build would reject. Typing exists only for the genuinely
66
+ free-text values.
62
67
 
63
68
  A patched binary records what was applied inside itself, so the menu always
64
69
  comes up showing the real current state, and `patch-cc status` answers
@@ -75,17 +80,19 @@ patch-cc # then just run it
75
80
 
76
81
  | Group | Patch | |
77
82
  |---|---|---|
78
- | Output & diffs | Detailed tool calls | Show full read/search calls, not collapsed summaries |
83
+ | Output & display | Detailed tool calls | Show full read/search calls, not collapsed summaries |
79
84
  | | Colour new files as diffs | Created files render with `+` lines and green |
80
- | Thinking | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
85
+ | | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
81
86
  | | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
82
87
  | | Stream thinking live | See reasoning as it is generated, inline and in order |
83
- | Subagents | Show subagent prompts | Prompt blocks visible during normal use |
88
+ | | Show subagent prompts | Prompt blocks visible during normal use |
89
+ | Models & effort | Persist max effort | `/effort max` saves as your default for new sessions, like the other levels |
90
+ | | Codex models | Use OpenAI/Codex-plan models in Claude Code — see [Codex models](#codex-models) |
84
91
  | | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
85
- | Chrome | Disable spinner tips | No rotating tips on the spinner |
86
- | | Custom startup name | Defaults to `<your username>'s Code` |
92
+ | Chrome & branding | Disable spinner tips | No rotating tips on the spinner |
87
93
  | | Mark `--version` | Appends `(patched)` — or any marker you choose |
88
- | Codex | Codex models | Use OpenAI/Codex-plan models in Claude Code — see [Codex models](#codex-models) |
94
+ | | Custom startup name | Defaults to `<your username>'s Code` |
95
+ | | Startup org/email label | Replace the org/email on the welcome screen — or hide it |
89
96
 
90
97
  ## Usage
91
98
 
@@ -99,6 +106,8 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
99
106
  uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
100
107
  uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
101
108
  uvx patch-cc apply --suffix "(mine)" # custom --version marker
109
+ uvx patch-cc apply --org-label # hide the welcome screen's org/email
110
+ uvx patch-cc apply --org-label "Ada's Lab" # ...or show this instead
102
111
  uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
103
112
  uvx patch-cc apply --from-cache # replay your last remembered selection
104
113
  uvx patch-cc status # exactly what is applied
@@ -108,9 +117,9 @@ uvx patch-cc list # every patch, described
108
117
  uvx patch-cc restore # put the original back
109
118
  ```
110
119
 
111
- `--brand`, `--model` and `--codex` imply their patches; agents and models are
112
- validated against what your installed binary actually ships, and Codex model ids
113
- against what your plan offers. `apply --help` lists all three.
120
+ A flag that configures a patch also selects it `apply --help` lists them
121
+ all. Agents and models are validated against what your installed binary
122
+ actually ships, and Codex model ids against what your plan offers.
114
123
 
115
124
  ## Codex models
116
125
 
@@ -210,3 +219,7 @@ bundle rather than with clodex's TLS interception.
210
219
  ## License
211
220
 
212
221
  MIT
222
+
223
+ ---
224
+
225
+ **More agent tooling** — [summon-cc](https://github.com/anfreire/summon-cc): give your agent a crew of Claude Code workers · [cc-oc](https://github.com/anfreire/cc-oc): drive opencode from inside Claude Code · [omoctl](https://github.com/anfreire/omoctl): manage oh-my-openagent profiles · [wiki-spaces](https://github.com/anfreire/wiki-spaces): a wiki your AI agent keeps
@@ -5,8 +5,16 @@
5
5
 
6
6
  An interactive patcher for the **Claude Code native binary**. Pick the tweaks
7
7
  you want — inline and live thinking, detailed tool calls, subagent model
8
- overrides, your own startup name and apply them to your installed `claude`
9
- in one keystroke. Pure Python; no Node, no Bun.
8
+ overrides, your own startup name, your **ChatGPT/Codex-plan GPT models as
9
+ native models** and apply them to your installed `claude` in one keystroke.
10
+ Fully reversible: a pristine backup is kept, `patch-cc restore` puts it back.
11
+ Pure Python; no Node, no Bun.
12
+
13
+ ```bash
14
+ uvx patch-cc # fullscreen menu, no install needed
15
+ ```
16
+
17
+ ![patch-cc: pick tweaks, register a Codex model, pin the Plan agent to it, bake — the patched binary is 161 MB smaller](https://raw.githubusercontent.com/anfreire/patch-cc/main/docs/demo.gif)
10
18
 
11
19
  ## Requirements
12
20
 
@@ -19,17 +27,14 @@ in one keystroke. Pure Python; no Node, no Bun.
19
27
  - **macOS only:** the Xcode command line tools, for `codesign` — a patched
20
28
  binary has to be re-signed or macOS refuses to run it.
21
29
 
22
- ```bash
23
- uvx patch-cc # fullscreen menu, no install needed
24
- ```
25
-
26
30
  The menu is a single centered panel: move with `↑ ↓`, toggle with `space`,
27
31
  press `s` to save. Patches that carry a setting — subagent models, Codex models,
28
- the startup name, the `--version` marker — open a centered modal on `enter`, and
29
- the row then shows what you chose. Everything choosable is a picker: the agent
30
- names and model aliases are **discovered from your binary itself** (and the
31
- Codex ones from your plan), so the menu can never offer something your build
32
- would reject. Typing exists only for the genuinely free-text values.
32
+ the startup name, the `--version` marker, the org/email label — open a centered
33
+ modal on `enter`, and the row then shows what you chose. Everything choosable
34
+ is a picker: the agent names and model aliases are **discovered from your
35
+ binary itself** (and the Codex ones from your plan), so the menu can never
36
+ offer something your build would reject. Typing exists only for the genuinely
37
+ free-text values.
33
38
 
34
39
  A patched binary records what was applied inside itself, so the menu always
35
40
  comes up showing the real current state, and `patch-cc status` answers
@@ -46,17 +51,19 @@ patch-cc # then just run it
46
51
 
47
52
  | Group | Patch | |
48
53
  |---|---|---|
49
- | Output & diffs | Detailed tool calls | Show full read/search calls, not collapsed summaries |
54
+ | Output & display | Detailed tool calls | Show full read/search calls, not collapsed summaries |
50
55
  | | Colour new files as diffs | Created files render with `+` lines and green |
51
- | Thinking | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
56
+ | | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
52
57
  | | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
53
58
  | | Stream thinking live | See reasoning as it is generated, inline and in order |
54
- | Subagents | Show subagent prompts | Prompt blocks visible during normal use |
59
+ | | Show subagent prompts | Prompt blocks visible during normal use |
60
+ | Models & effort | Persist max effort | `/effort max` saves as your default for new sessions, like the other levels |
61
+ | | Codex models | Use OpenAI/Codex-plan models in Claude Code — see [Codex models](#codex-models) |
55
62
  | | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
56
- | Chrome | Disable spinner tips | No rotating tips on the spinner |
57
- | | Custom startup name | Defaults to `<your username>'s Code` |
63
+ | Chrome & branding | Disable spinner tips | No rotating tips on the spinner |
58
64
  | | Mark `--version` | Appends `(patched)` — or any marker you choose |
59
- | Codex | Codex models | Use OpenAI/Codex-plan models in Claude Code — see [Codex models](#codex-models) |
65
+ | | Custom startup name | Defaults to `<your username>'s Code` |
66
+ | | Startup org/email label | Replace the org/email on the welcome screen — or hide it |
60
67
 
61
68
  ## Usage
62
69
 
@@ -70,6 +77,8 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
70
77
  uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
71
78
  uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
72
79
  uvx patch-cc apply --suffix "(mine)" # custom --version marker
80
+ uvx patch-cc apply --org-label # hide the welcome screen's org/email
81
+ uvx patch-cc apply --org-label "Ada's Lab" # ...or show this instead
73
82
  uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
74
83
  uvx patch-cc apply --from-cache # replay your last remembered selection
75
84
  uvx patch-cc status # exactly what is applied
@@ -79,9 +88,9 @@ uvx patch-cc list # every patch, described
79
88
  uvx patch-cc restore # put the original back
80
89
  ```
81
90
 
82
- `--brand`, `--model` and `--codex` imply their patches; agents and models are
83
- validated against what your installed binary actually ships, and Codex model ids
84
- against what your plan offers. `apply --help` lists all three.
91
+ A flag that configures a patch also selects it `apply --help` lists them
92
+ all. Agents and models are validated against what your installed binary
93
+ actually ships, and Codex model ids against what your plan offers.
85
94
 
86
95
  ## Codex models
87
96
 
@@ -181,3 +190,7 @@ bundle rather than with clodex's TLS interception.
181
190
  ## License
182
191
 
183
192
  MIT
193
+
194
+ ---
195
+
196
+ **More agent tooling** — [summon-cc](https://github.com/anfreire/summon-cc): give your agent a crew of Claude Code workers · [cc-oc](https://github.com/anfreire/cc-oc): drive opencode from inside Claude Code · [omoctl](https://github.com/anfreire/omoctl): manage oh-my-openagent profiles · [wiki-spaces](https://github.com/anfreire/wiki-spaces): a wiki your AI agent keeps
@@ -93,7 +93,7 @@ its shape; [PLAYBOOK.md](PLAYBOOK.md) covers what it means for matcher health:
93
93
 
94
94
  ```
95
95
  //patch-cc {"v":1,"tool":"<version>","patches":[...],"brand":...,"models":{...},
96
- "codex":{"port":8817,"models":["gpt-5.6-sol", ...]}}
96
+ "org":...,"codex":{"port":8817,"models":["gpt-5.6-sol", ...]}}
97
97
  ```
98
98
 
99
99
  That line is why `patch-cc status` can name exactly what is applied: several
@@ -17,10 +17,35 @@ constantly. These rules are what keep matchers alive:
17
17
  literals, `case` labels, prop names, or control-flow shape.
18
18
  - Match the *semantic shape* of a function body, not its symbol names.
19
19
  - When upstream ships more than one shape for the same feature, add a second
20
- narrow branch — don't widen one regex until it over-matches.
20
+ narrow branch — don't widen one regex until it over-matches. A different
21
+ *spelling* of one shape is not a second shape, and does not earn a branch; see
22
+ "applied to, not reached through" below for where that line falls.
21
23
  - Accept statement variants a minifier flips between. The thinking gate broke
22
24
  on 2.1.216 solely because `if(x)return null;` became `if(x){return null}` —
23
25
  matchers should allow both from day one.
26
+ - **Applied to, not reached through.** Match what a helper is applied to; say
27
+ nothing about how it is reached. The callee of a zod schema is pure minifier
28
+ noise — the persisted `effortLevel` reads `E.enum(` on 2.1.216, `A.enum(`,
29
+ `b.enum(`, `v.enum(`, `w.enum(` through 2.1.223, then `Ir(` on 2.1.224 — and
30
+ every matcher that named any of it dated itself to one build. 2.1.224 is the
31
+ proof: `.enum([` dropped from 241 sites to 23 in a single release, because the
32
+ spelling tracks which *module* a schema lives in (bundled namespace alias vs
33
+ bare standalone factory), never what the schema is. So the callee is
34
+ `base.ARRAY_CALL` — whatever sits between the prop name and the array literal,
35
+ bounded only by statement and block edges — and no spelling is enumerated.
36
+ Enumerating the spellings seen so far only defers the same break to the next
37
+ one; that is the boundary of the narrow-branch rule above.
38
+ - **Dissolving the callee raises what the surviving anchors must carry**, and
39
+ that debt is per-site, never general. `effortLevel:` plus its level array is
40
+ unique on every build. `model:` plus an array is *not* — 6 sites on
41
+ 2.1.216–220, 12 on 2.1.221–224 — so `MODEL_ENUM`'s
42
+ ``.optional().describe(`Optional model override`` tail is its sole
43
+ discriminator, not belt-and-braces. Trimmed, the matcher finds three sites, two
44
+ of them the settings-scope enum `["user","project","local"]`, and
45
+ `codex-models` splices Codex ids into it. Measure an anchor's match count on
46
+ every backup before removing any part of it: under the older, tighter callee
47
+ that same probe read 1 on all 8 builds, so a measurement taken before the
48
+ callee was dissolved does not survive it.
24
49
  - Match an optional brace **pair** conditionally (`(\{)?…(?(1)\})`), never as
25
50
  two independent `\{?` / `\}?`. A lone optional closing brace eats the
26
51
  *enclosing* block's `}` on the unbraced shape, and a rewrite that emits its
@@ -41,8 +66,8 @@ Anything the binary can enumerate for us, it does:
41
66
  `agentType:"<name>",whenToUse:` carrying `source:"built-in"` in the same
42
67
  object (`agents.discover_agents`). Definitions whose `whenToUse` begins with
43
68
  `"Internal` are plumbing and are not offered.
44
- - **Model aliases** come from the Task tool's own input schema: the
45
- `model:<zod>.enum([...])` whose describe-string starts
69
+ - **Model aliases** come from the Task tool's own input schema: the `model`
70
+ enum whose describe-string starts
46
71
  `Optional model override` — `agents.MODEL_ENUM`, one compiled anchor because
47
72
  `codex-models` splices the chosen Codex ids into the very group
48
73
  `discover_models` reads back out.
@@ -233,9 +258,10 @@ that broke a patch along with every note.
233
258
 
234
259
  ## Patch reference
235
260
 
236
- Grouped by source module the UI folds `live-thinking` into the Thinking group,
237
- but it still lives in `streaming.py`. Each entry: what it changes, the stable
238
- anchor, and where it lives.
261
+ Grouped by source module. The menu's three groups (Output & display, Models &
262
+ effort, Chrome & branding) are display categories that each draw from several
263
+ modules module says where a matcher lives, group says what the patch changes
264
+ for you. Each entry: what it changes, the stable anchor, and where it lives.
239
265
 
240
266
  ### Output & diffs — `output.py`
241
267
 
@@ -288,6 +314,36 @@ anchor, and where it lives.
288
314
  A column of `0`s is this patch missing (or a bucket it does not yet cover);
289
315
  a spread of real lengths means the text arrived and the problem is rendering.
290
316
 
317
+ - **`max-effort`** — let `/effort max` save as the session default, as
318
+ low–xhigh already do. `max` is first-class in session (the binary's level
319
+ list is `["low","medium","high","xhigh","max"]`; the CLI `--effort` and
320
+ `CLAUDE_CODE_EFFORT_LEVEL` both accept it) — only *persistence* runs through
321
+ two whitelists that stop at xhigh, and both are required rewrites because
322
+ either alone changes nothing:
323
+
324
+ - **`gate`** — one tiny whitelist function
325
+ (`if(e==="low"||…||e==="xhigh")return e;return`), and the whole choke
326
+ point: the `/effort` save path writes `effortLevel` to userSettings only
327
+ when it returns a value, the startup resolver reads the setting back
328
+ through it, and the model-picker flow persists through it too — so write
329
+ and read widen together in one rewrite. The matcher accepts both
330
+ minifier statement forms and the whole-body match keeps it off any other
331
+ function that merely compares levels.
332
+ - **`schema`** — the settings validator's
333
+ `effortLevel:*.enum(["low","medium","high","xhigh"])`, anchored on the
334
+ prop name (the describe-string `Persisted effort level` is the listed
335
+ anchor). Its `.catch(void 0)` is the safety property: a **clean** binary
336
+ reading a settings file that still says `"max"` (baked, then reverted by
337
+ a Claude update) treats it as unset — the default effort, never a broken
338
+ settings parse. Losing the patch costs the preference, nothing else.
339
+
340
+ Both matchers tolerate the level already being present and count it as
341
+ landed — an upstream that adopts `max` persistence itself is the goal
342
+ achieved, not a miss (the same judged-on-achievement rule
343
+ `subagent-models` follows). Effort semantics downstream are untouched: the
344
+ org-limit clamp runs before the save, and a model that cannot run `max`
345
+ clamps at request time exactly as an interactive `/effort max` does today.
346
+
291
347
  - **`thinking-inline`** — make historical thinking blocks render inline.
292
348
  Anchor: `case"thinking":` containing `isTranscriptMode:`. Two rewrites:
293
349
  remove the early null-return (both `if(!a&&!b)return null;` and the 2.1.216
@@ -516,6 +572,20 @@ anchor, and where it lives.
516
572
  chosen name. Several string shapes, each its own sub-step. On by default,
517
573
  deriving `<username>'s Code`; `--brand NAME` names it explicitly, and selects
518
574
  the patch when it is not already in the set.
575
+ - **`org-label`** — replace or hide the welcome screen's third segment
576
+ (`model · plan · <organizationName>`; for a personal claude.ai account the
577
+ org name *is* the account email). Anchor: the one conditional template
578
+ joining `.organizationName` behind `!process.env.IS_DEMO`, whose false
579
+ branch is upstream's own no-org shape `` `${model} \xB7 ${plan}` ``. An
580
+ empty label (`--org-label` with no value — empty is the *value* "hide", not
581
+ "unset") emits that false branch verbatim, so the separator leaves with the
582
+ segment; a label takes the org text's place, escaped for the template
583
+ literal it lands in. Candidates count off the `\xB7 ${*.organizationName}`
584
+ composition, so a reshaped ternary reads as `candidates > 0, applied == 0`
585
+ — a matcher to repair — rather than the zero that would equally mean
586
+ upstream retired the segment. Only this line is touched: the `/status`
587
+ Organization/Email rows, the login screen, and the org's startup message
588
+ (`"Message from <org>:"`) still show the real account.
519
589
 
520
590
  ## Removed patches
521
591
 
Binary file
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "patch-cc"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Interactive patcher for the Claude Code native binary"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """patch-cc: an interactive patcher for the Claude Code native binary."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
@@ -59,6 +59,7 @@ def load_strict() -> Selection | None:
59
59
  known = set(ids())
60
60
  brand = data.get("brand", DEFAULT_BRAND)
61
61
  suffix = data.get("suffix", DEFAULT_SUFFIX)
62
+ org = data.get("org_label", "")
62
63
  models = data.get("subagent_models", {})
63
64
  # Codex ids only, the same as the manifest keeps: a name and a context
64
65
  # window are the plan's to report, and remembering either would let a
@@ -72,6 +73,7 @@ def load_strict() -> Selection | None:
72
73
  version_suffix=suffix
73
74
  if isinstance(suffix, str) and suffix
74
75
  else DEFAULT_SUFFIX,
76
+ org_label=org if isinstance(org, str) else "",
75
77
  subagent_models={
76
78
  a: m
77
79
  for a, m in models.items()
@@ -98,6 +100,7 @@ def save(selection: Selection) -> None:
98
100
  "patches": selection.patches,
99
101
  "brand": selection.options.brand,
100
102
  "suffix": selection.options.version_suffix,
103
+ "org_label": selection.options.org_label,
101
104
  "subagent_models": selection.options.subagent_models,
102
105
  "codex_models": [m.id for m in selection.options.codex_models],
103
106
  "codex_port": selection.options.codex_port,
@@ -4,7 +4,8 @@ Bare ``patch-cc`` opens the interactive menu. Every action is also a
4
4
  subcommand so nothing needs the TUI:
5
5
 
6
6
  patch-cc apply [PATCH ...] [--brand [NAME]] [--model AGENT=MODEL]
7
- [--suffix TEXT] [--codex MODEL] [--codex-port N]
7
+ [--suffix TEXT] [--org-label [TEXT]]
8
+ [--codex MODEL] [--codex-port N]
8
9
  patch-cc status
9
10
  patch-cc doctor [PATH] # PATH: check any binary, e.g. an old backup
10
11
  patch-cc list
@@ -102,6 +103,12 @@ def _list_hint(patch: Patch, cached: cache.Selection) -> str:
102
103
  parts.append(f"default {DEFAULT_SUFFIX!r}")
103
104
  if opts.version_suffix != DEFAULT_SUFFIX:
104
105
  parts.append(f"cached {opts.version_suffix!r}")
106
+ elif patch.id == "org-label":
107
+ parts.append("default hides it")
108
+ if "org-label" in cached.patches:
109
+ parts.append(
110
+ f"cached {opts.org_label!r}" if opts.org_label else "cached hidden"
111
+ )
105
112
  elif patch.id == "subagent-models" and opts.subagent_models:
106
113
  picks = ", ".join(f"{a}={m}" for a, m in opts.subagent_models.items())
107
114
  parts.append(f"cached {picks}")
@@ -218,6 +225,11 @@ def _requested(args, source: str) -> tuple[list[str], Options]:
218
225
  if "version-marker" not in selected:
219
226
  selected.append("version-marker")
220
227
 
228
+ if args.org_label is not None:
229
+ options.org_label = args.org_label.strip()
230
+ if "org-label" not in selected:
231
+ selected.append("org-label")
232
+
221
233
  # Order-preserving, and deduplicated: a repeated `--codex` is one model asked
222
234
  # for twice, and every array it reaches -- the enum, the routing list, the
223
235
  # remembered selection, the manifest `status` reads back -- would otherwise
@@ -272,6 +284,7 @@ def _has_selection_args(args) -> bool:
272
284
  or args.brand is not None
273
285
  or args.model
274
286
  or args.suffix
287
+ or args.org_label is not None
275
288
  or args.codex
276
289
  or args.codex_port is not None
277
290
  )
@@ -332,7 +345,8 @@ def _from_cache(args, source: str) -> tuple[list[str], Options]:
332
345
  if _has_selection_args(args):
333
346
  err(
334
347
  "--from-cache replays your last interactive selection; do not combine "
335
- "it with patch ids or --brand / --model / --suffix / --codex."
348
+ "it with patch ids or --brand / --model / --suffix / --org-label / "
349
+ "--codex."
336
350
  )
337
351
  raise SystemExit(2)
338
352
  if not cache.cache_path().exists():
@@ -494,6 +508,8 @@ def cmd_status(args) -> int:
494
508
  console.print(f" brand: {st.manifest['brand']}")
495
509
  if "suffix" in st.manifest:
496
510
  console.print(f" suffix: {st.manifest['suffix']}")
511
+ if "org" in st.manifest:
512
+ console.print(f" org: {st.manifest['org'] or '(hidden)'}")
497
513
  for agent, model in (st.manifest.get("models") or {}).items():
498
514
  console.print(f" model: {agent} = {model}")
499
515
  codex = st.manifest.get("codex")
@@ -863,6 +879,7 @@ def _apply_epilog() -> str:
863
879
  "configuring patches:",
864
880
  f" --brand [NAME] selects branding · no value -> {derived_brand()!r}",
865
881
  f" --suffix TEXT selects version-marker · default {DEFAULT_SUFFIX!r}",
882
+ " --org-label [TEXT] selects org-label · no value -> hide the segment",
866
883
  " --model AGENT=MODEL selects subagent-models · repeatable, one per agent",
867
884
  ]
868
885
  if agents:
@@ -1004,6 +1021,14 @@ def build_parser() -> argparse.ArgumentParser:
1004
1021
  help="`claude --version` marker text; selects `version-marker` "
1005
1022
  "(default: (patched))",
1006
1023
  )
1024
+ p_apply.add_argument(
1025
+ "--org-label",
1026
+ nargs="?",
1027
+ const="",
1028
+ metavar="TEXT",
1029
+ help="welcome-screen org/email text; selects `org-label` "
1030
+ "(no value: hide the segment)",
1031
+ )
1007
1032
  p_apply.add_argument(
1008
1033
  "--codex",
1009
1034
  action="append",
@@ -110,7 +110,12 @@ def _synthetic_options(agents: list[BuiltinAgent], models: list[str]) -> Options
110
110
  if (target := next((m for m in models if m != agent.effective_model), None))
111
111
  }
112
112
  return Options(
113
- brand="patch-cc doctor", subagent_models=overrides, codex_models=codex
113
+ brand="patch-cc doctor",
114
+ subagent_models=overrides,
115
+ codex_models=codex,
116
+ # The replace branch; the hidden form differs only in the emitted tail,
117
+ # so one configuration holds the whole matcher in the net.
118
+ org_label="patch-cc doctor",
114
119
  )
115
120
 
116
121
 
@@ -4,9 +4,10 @@ A fullscreen frame: fixed title and status on top, fixed key hints at the
4
4
  bottom, and only the patch list scrolling in between. Every choice is a picker
5
5
  driven by what the binary itself offers -- agents and models are discovered,
6
6
  never typed -- and typing exists only where a value is genuinely free text
7
- (the startup name, the --version marker). All configuration happens in
8
- centered modals floating over the dimmed list; the list itself never grows
9
- sub-rows. ``s`` saves, and the same frame then shows the per-patch results.
7
+ (the startup name, the --version marker, the org/email label). All
8
+ configuration happens in centered modals floating over the dimmed list; the
9
+ list itself never grows sub-rows. ``s`` saves, and the same frame then shows
10
+ the per-patch results.
10
11
 
11
12
  The engine is deliberately small: ``blessed`` owns the terminal (fullscreen,
12
13
  cbreak, parsed keystrokes, live size) and Rich owns every pixel drawn. A frame
@@ -43,6 +44,7 @@ from .bun import Bundle, BunError, container
43
44
  from .codex import DEFAULT_PORT, is_valid_port
44
45
  from .codex.models import CodexModel, discover, reconcile
45
46
  from .patches import (
47
+ ALL_PATCHES,
46
48
  DEFAULT_BRAND,
47
49
  DEFAULT_SUFFIX,
48
50
  GROUP_ORDER,
@@ -83,8 +85,11 @@ def _hints(*pairs: tuple[str, str]) -> Text:
83
85
  return text
84
86
 
85
87
 
86
- #: Patches whose row opens a modal on enter instead of plain toggling.
87
- _CONFIGURABLE = {"subagent-models", "branding", "version-marker", "codex-models"}
88
+ #: Patches whose row opens a modal on enter instead of plain toggling --
89
+ #: exactly the ones that carry a configurable value, which is what
90
+ #: :attr:`Patch.option` declares. Derived, so a new option-carrying patch
91
+ #: cannot be left off this list with its hint reading "enter toggle".
92
+ _CONFIGURABLE = {patch.id for patch in ALL_PATCHES if patch.option}
88
93
 
89
94
 
90
95
  # ----------------------------------------------------------------- rows
@@ -182,6 +187,7 @@ class MenuModel:
182
187
  model.text_rows["suffix"] = TextRow(
183
188
  "suffix", "marker", seed.options.version_suffix
184
189
  )
190
+ model.text_rows["org"] = TextRow("org", "label", seed.options.org_label)
185
191
  return model
186
192
 
187
193
  def _seed(self) -> cache.Selection:
@@ -191,6 +197,7 @@ class MenuModel:
191
197
  codex = manifest.get("codex")
192
198
  codex = codex if isinstance(codex, dict) else {}
193
199
  port = codex.get("port")
200
+ org = manifest.get("org")
194
201
  seed = cache.Selection()
195
202
  seed.patches = [
196
203
  p for p in manifest.get("patches", []) if isinstance(p, str)
@@ -198,6 +205,9 @@ class MenuModel:
198
205
  seed.options = Options(
199
206
  brand=manifest.get("brand") or DEFAULT_BRAND,
200
207
  version_suffix=manifest.get("suffix") or DEFAULT_SUFFIX,
208
+ # `or ""` would also be right, but only by luck: "" (hidden) is
209
+ # a meaningful manifest value, unlike an absent brand/suffix.
210
+ org_label=org if isinstance(org, str) else "",
201
211
  subagent_models={
202
212
  a: m
203
213
  for a, m in (manifest.get("models") or {}).items()
@@ -269,6 +279,10 @@ class MenuModel:
269
279
  options.version_suffix = (
270
280
  self.text_rows["suffix"].value.strip() or DEFAULT_SUFFIX
271
281
  )
282
+ if "org-label" in selected:
283
+ # Empty stays empty: it is the "hide the segment" value, so unlike
284
+ # brand and suffix there is no default to fall back to.
285
+ options.org_label = self.text_rows["org"].value.strip()
272
286
  # A subagent pinned to a Codex model needs codex-models applied too -- it
273
287
  # registers the id and installs the redirect, without which the pin
274
288
  # resolves to nothing. That coupling is enforced where the user can see
@@ -709,15 +723,20 @@ class MenuApp:
709
723
  def _activate(self, row: PatchRow) -> None:
710
724
  """Enter on a patch: toggle plain ones, configure configurable ones."""
711
725
  patch_id = row.patch.id
726
+ text_keys = {
727
+ "branding": "brand",
728
+ "version-marker": "suffix",
729
+ "org-label": "org",
730
+ }
712
731
  if patch_id == "subagent-models":
713
732
  if not self.model.agent_rows:
714
733
  self.flash = "no agents discovered in this bundle"
715
734
  return
716
735
  row.on = True
717
736
  self._open_agents_modal()
718
- elif patch_id in ("branding", "version-marker"):
737
+ elif patch_id in text_keys:
719
738
  row.on = True
720
- self._open_text_modal("brand" if patch_id == "branding" else "suffix")
739
+ self._open_text_modal(text_keys[patch_id])
721
740
  elif patch_id == "codex-models":
722
741
  self._open_codex_menu(row) # submenu: pick models / set gateway port
723
742
  else:
@@ -813,10 +832,16 @@ class MenuApp:
813
832
 
814
833
  def _open_text_modal(self, key: str) -> None:
815
834
  row = self.model.text_rows[key]
816
- titles = {"brand": "Startup name", "suffix": "--version marker"}
835
+ titles = {
836
+ "brand": "Startup name",
837
+ "suffix": "--version marker",
838
+ "org": "Org/email label (empty hides it)",
839
+ }
817
840
 
818
841
  def entered(value: object) -> None:
819
- if isinstance(value, str) and value.strip():
842
+ # For the org label an emptied field is the point -- it means hide
843
+ # the segment -- where an empty brand or marker means "never mind".
844
+ if isinstance(value, str) and (value.strip() or key == "org"):
820
845
  row.value = value.strip()
821
846
 
822
847
  self._push(InputModal(titles[key], row.value), entered)
@@ -1412,6 +1437,9 @@ class MenuApp:
1412
1437
  return Text(model.text_rows["brand"].value, style=_VALUE)
1413
1438
  if row.patch.id == "version-marker":
1414
1439
  return Text(model.text_rows["suffix"].value, style=_VALUE)
1440
+ if row.patch.id == "org-label":
1441
+ value = model.text_rows["org"].value
1442
+ return Text(value, style=_VALUE) if value else Text("hidden", style="dim")
1415
1443
  if row.patch.id == "codex-models":
1416
1444
  count = len(model.codex_models)
1417
1445
  if not count:
@@ -92,6 +92,11 @@ def manifest_payload(landed: list[str], options: Options) -> dict:
92
92
  payload["suffix"] = options.version_suffix
93
93
  if options.subagent_models and "subagent-models" in applied:
94
94
  payload["models"] = options.subagent_models
95
+ if "org-label" in applied:
96
+ # Recorded even when empty: "" is the asked-for value (hide the
97
+ # segment), and omitting it would make hidden indistinguishable from
98
+ # never-configured when the menu seeds from this manifest.
99
+ payload["org"] = options.org_label
95
100
  if options.codex_models and "codex-models" in applied:
96
101
  # Ids and a port -- the whole of what was asked for. A model's name and
97
102
  # context window are baked into the bundle itself (the picker rows, the
@@ -19,11 +19,9 @@ from . import agents, chrome, codex, output, streaming, thinking
19
19
  from .base import (
20
20
  DEFAULT_BRAND,
21
21
  DEFAULT_SUFFIX,
22
- GROUP_AGENTS,
23
22
  GROUP_CHROME,
24
- GROUP_CODEX,
23
+ GROUP_MODELS,
25
24
  GROUP_OUTPUT,
26
- GROUP_THINKING,
27
25
  SENTINEL,
28
26
  Options,
29
27
  Outcome,
@@ -40,7 +38,7 @@ ALL_PATCHES: list[Patch] = [
40
38
  *chrome.PATCHES,
41
39
  ]
42
40
 
43
- GROUP_ORDER = [GROUP_OUTPUT, GROUP_THINKING, GROUP_AGENTS, GROUP_CODEX, GROUP_CHROME]
41
+ GROUP_ORDER = [GROUP_OUTPUT, GROUP_MODELS, GROUP_CHROME]
44
42
 
45
43
 
46
44
  def ids() -> list[str]:
@@ -16,7 +16,17 @@ from __future__ import annotations
16
16
  import re
17
17
  from dataclasses import dataclass
18
18
 
19
- from .base import GROUP_AGENTS, IDENT, Options, Outcome, Patch, compile_js, splice
19
+ from .base import (
20
+ ARRAY_CALL,
21
+ GROUP_MODELS,
22
+ GROUP_OUTPUT,
23
+ IDENT,
24
+ Options,
25
+ Outcome,
26
+ Patch,
27
+ compile_js,
28
+ splice,
29
+ )
20
30
 
21
31
  # ------------------------------------------------------- prompt visibility
22
32
 
@@ -147,7 +157,7 @@ _DEF_WINDOW = 3000
147
157
  #: why that ordering is load-bearing). Matched separately, the two would drift
148
158
  #: apart on the next upstream reshape, and only one of them would be repaired.
149
159
  MODEL_ENUM = compile_js(
150
- rf'model:{IDENT}\.enum\(\[((?:"{_MODEL_CHARS}+",?)+)\]\)\.optional\(\)'
160
+ rf'model:{ARRAY_CALL}((?:"{_MODEL_CHARS}+",?)+)\]\)\.optional\(\)'
151
161
  rf'\.describe\([`"]Optional model override'
152
162
  )
153
163
  #: Used only if the Task-tool schema anchor ever disappears.
@@ -366,7 +376,7 @@ PATCHES = [
366
376
  id="subagent-prompt",
367
377
  title="Show subagent prompts",
368
378
  summary="Show a subagent's Prompt block during normal use, not only in transcript mode.",
369
- group=GROUP_AGENTS,
379
+ group=GROUP_OUTPUT,
370
380
  fn=_subagent_prompt,
371
381
  anchors=('"Backgrounded agent"', 'action:"app:toggleTranscript"'),
372
382
  ),
@@ -374,7 +384,7 @@ PATCHES = [
374
384
  id="subagent-models",
375
385
  title="Override subagent models",
376
386
  summary="Choose the default model for the built-in agents found in your binary.",
377
- group=GROUP_AGENTS,
387
+ group=GROUP_MODELS,
378
388
  fn=_subagent_models,
379
389
  default=False,
380
390
  option="--model",
@@ -37,11 +37,12 @@ if TYPE_CHECKING:
37
37
  #: indistinguishable from the 11 that upstream already ships.
38
38
  SENTINEL = "__cc_"
39
39
 
40
- # Groups, in display order.
41
- GROUP_OUTPUT = "Output & diffs"
42
- GROUP_THINKING = "Thinking"
43
- GROUP_AGENTS = "Subagents"
44
- GROUP_CODEX = "Codex models"
40
+ # Groups, in display order: what renders, what runs (and how hard), how it is
41
+ # dressed. Generic on purpose -- a group named after a feature (Thinking,
42
+ # Subagents, Codex) fits exactly that feature, and the next patch that is
43
+ # about behaviour rather than a surface has no home.
44
+ GROUP_OUTPUT = "Output & display"
45
+ GROUP_MODELS = "Models & effort"
45
46
  GROUP_CHROME = "Chrome & branding"
46
47
 
47
48
  #: Default brand: the name shown unless the user overrides it. One home so the
@@ -88,6 +89,11 @@ class Options:
88
89
  #: from the one home (:data:`patch_cc.codex.DEFAULT_PORT`) so the number
89
90
  #: never lives in two places.
90
91
  codex_port: int = DEFAULT_PORT
92
+ #: The welcome screen's org segment (for personal claude.ai accounts,
93
+ #: upstream shows the account email there). Empty is a *value* -- hide the
94
+ #: segment -- not "unset": whether the patch acts at all is the selection's
95
+ #: question, never this field's.
96
+ org_label: str = ""
91
97
 
92
98
  @property
93
99
  def rebrands(self) -> bool:
@@ -271,6 +277,19 @@ def compile_js(pattern: str, flags: int = 0) -> re.Pattern[str]:
271
277
  # Matches a minified identifier, the JS `[A-Za-z_$][\w$]*` idiom.
272
278
  IDENT = r"[A-Za-z_$][\w$]*"
273
279
 
280
+ #: From a schema's property name to the *opening* of the array literal it is
281
+ #: built from -- the ``([`` included, so a site composes this as
282
+ #: ``prop:{ARRAY_CALL}`` and never spells the call open itself.
283
+ #:
284
+ #: Whatever expression the build reaches the factory through is skipped
285
+ #: unmodelled, because the callee is minifier noise that churns every few
286
+ #: releases (``E.enum(`` ... ``w.enum(`` ... ``Ir(`` for one unchanged schema).
287
+ #: Lazy but not unbounded: ``[^;{}]`` cannot cross a statement or block edge,
288
+ #: and the spans it actually spends are 47-51 and 88-92 characters on every
289
+ #: build we hold. What the surviving anchors must carry instead -- which is a
290
+ #: debt this raises, per site: docs/PLAYBOOK.md.
291
+ ARRAY_CALL = r"[^;{}]*?\(\["
292
+
274
293
 
275
294
  def switch_case_end(content: str, start: int) -> int:
276
295
  """End offset of a ``switch`` arm beginning at ``start``.
@@ -175,6 +175,52 @@ def _branding(content: str, options: Options, outcome: Outcome) -> str:
175
175
  return output
176
176
 
177
177
 
178
+ # ---------------------------------------------------------- startup org label
179
+
180
+ # The separator as the bundle may spell it: today's bundler writes the `·` as
181
+ # the escape text `\xB7`, and a charset flip would emit the raw character. Both
182
+ # spell the same string, so both are accepted -- a representation variant like
183
+ # the minifier's brace flips, not a second shape.
184
+ _SEP = r"(?:\\xB7|\xB7)"
185
+
186
+ # The welcome screen's info line: `model · plan` -- and, when the account has an
187
+ # organizationName (for personal claude.ai accounts, that is the account email),
188
+ # ` · <organizationName>` after it. One conditional template, and upstream ships
189
+ # the no-org variant as its own false branch, so hiding the segment is selecting
190
+ # a shape the bundle already carries -- separator and all -- not inventing one.
191
+ _ORG_LINE = compile_js(
192
+ rf"!process\.env\.IS_DEMO&&({IDENT})\?\.organizationName\?"
193
+ rf"`\$\{{({IDENT})\}} {_SEP} \$\{{({IDENT})\}} {_SEP} \$\{{\1\.organizationName\}}`"
194
+ rf":`\$\{{\2\}} {_SEP} \$\{{\3\}}`"
195
+ )
196
+
197
+ #: The composition itself -- a separator joining the account's organizationName
198
+ #: into a template -- is the durable fingerprint; candidates count off it so a
199
+ #: reshaped ternary reads as "found it, rewrote nothing" (a matcher to repair)
200
+ #: instead of the zero that would equally mean upstream retired the segment.
201
+ _ORG_NEEDLE = compile_js(rf"{_SEP} \$\{{{IDENT}\.organizationName\}}")
202
+
203
+
204
+ def _org_label(content: str, options: Options, outcome: Outcome) -> str:
205
+ """Replace -- or, with an empty label, drop -- the welcome line's org segment.
206
+
207
+ One rule builds the replacement either way: the line is `model · plan`,
208
+ plus ` · <label>` exactly when there is a label. Empty therefore removes
209
+ the separator with the segment, and a custom label takes the org text's
210
+ place. The `IS_DEMO`/has-org gate goes with the ternary: a baked label is
211
+ unconditional by construction, and the hidden form needs no gate at all.
212
+ """
213
+ outcome.candidates += len(_ORG_NEEDLE.findall(content))
214
+ label = options.org_label
215
+ tail = f" \\xB7 {_js_template_escape(label)}" if label else ""
216
+
217
+ def rewrite(match: re.Match[str]) -> str:
218
+ outcome.applied += 1
219
+ return f"`${{{match.group(2)}}} \\xB7 ${{{match.group(3)}}}{tail}`"
220
+
221
+ return _ORG_LINE.sub(rewrite, content)
222
+
223
+
178
224
  PATCHES = [
179
225
  Patch(
180
226
  id="spinner-tips",
@@ -203,4 +249,14 @@ PATCHES = [
203
249
  option="--brand",
204
250
  anchors=('"Welcome to Claude Code"', '{bold:!0},"Claude Code"'),
205
251
  ),
252
+ Patch(
253
+ id="org-label",
254
+ title="Startup org/email label",
255
+ summary="Replace or hide the org/email shown after the plan name on the welcome screen.",
256
+ group=GROUP_CHROME,
257
+ fn=_org_label,
258
+ default=False,
259
+ option="--org-label",
260
+ anchors=("organizationName", "IS_DEMO"),
261
+ ),
206
262
  ]
@@ -50,7 +50,7 @@ import re
50
50
  from ..codex.models import family_aliases, family_of
51
51
  from .agents import MODEL_ENUM
52
52
  from .base import (
53
- GROUP_CODEX,
53
+ GROUP_MODELS,
54
54
  IDENT,
55
55
  Options,
56
56
  Outcome,
@@ -507,7 +507,7 @@ PATCHES = [
507
507
  id="codex-models",
508
508
  title="Codex models",
509
509
  summary="Register your OpenAI/Codex models and route them to the local gateway.",
510
- group=GROUP_CODEX,
510
+ group=GROUP_MODELS,
511
511
  fn=_codex_models,
512
512
  default=False,
513
513
  option="--codex",
@@ -23,7 +23,7 @@ from __future__ import annotations
23
23
  import re
24
24
  from dataclasses import dataclass
25
25
 
26
- from .base import GROUP_THINKING, IDENT, Options, Outcome, Patch, compile_js, splice
26
+ from .base import GROUP_OUTPUT, 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=GROUP_THINKING,
977
+ group=GROUP_OUTPUT,
978
978
  fn=_live_thinking,
979
979
  anchors=(
980
980
  "onStreamingThinking:",
@@ -22,6 +22,9 @@ Rendering is only half of it. A fourth gate is not in the UI at all: the
22
22
  account's server-side experiment bucket, which decides whether the API puts any
23
23
  text in the thinking blocks it returns. `thinking-summaries` is that half, and
24
24
  without it the other three render an empty string perfectly.
25
+
26
+ `max-effort` lives here too: effort is thinking depth, so the module that owns
27
+ how thinking is shown also owns how hard it is asked for.
25
28
  """
26
29
 
27
30
  from __future__ import annotations
@@ -29,7 +32,9 @@ from __future__ import annotations
29
32
  import re
30
33
 
31
34
  from .base import (
32
- GROUP_THINKING,
35
+ ARRAY_CALL,
36
+ GROUP_MODELS,
37
+ GROUP_OUTPUT,
33
38
  IDENT,
34
39
  Options,
35
40
  Outcome,
@@ -189,12 +194,73 @@ def _thinking_summaries(content: str, _options: Options, outcome: Outcome) -> st
189
194
  return _CLIENT_BUCKET.sub(rewrite, content)
190
195
 
191
196
 
197
+ # ------------------------------------------------------------- max effort
198
+ #
199
+ # `max` is a first-class level in session -- the level list the binary ships is
200
+ # ["low","medium","high","xhigh","max"], and the CLI and env var both accept it
201
+ # -- but persistence runs through two whitelists that stop at xhigh. Both are
202
+ # widened; either alone changes nothing (docs/PLAYBOOK.md), which is also why
203
+ # both steps are required.
204
+ #
205
+ # The gate is one tiny function, and it is the whole choke point: the /effort
206
+ # save path writes settings only when it returns a value, the startup resolver
207
+ # reads settings back through it, and the model-picker flow persists through it
208
+ # too. The statement variants and the optional `||x==="max"` follow the matcher
209
+ # rules: minifiers flip `return e;` and `{return e}`, and a build where upstream
210
+ # has adopted the level itself is the goal already achieved, not a miss.
211
+ _EFFORT_GATE = compile_js(
212
+ rf"function ({IDENT})\(({IDENT})\)\{{"
213
+ rf'if\(\2==="low"\|\|\2==="medium"\|\|\2==="high"\|\|\2==="xhigh"'
214
+ rf'(\|\|\2==="max")?\)'
215
+ rf"(?:\{{return \2\}}|return \2;?)return;?\}}"
216
+ )
217
+
218
+ # The persisted-settings schema entry. Its zod chain ends in `.catch(void 0)`,
219
+ # so a clean binary reading a settings file that carries "max" (baked by us,
220
+ # then reverted by a Claude update) treats it as unset rather than failing the
221
+ # whole settings parse -- the property that makes this patch safe to lose.
222
+ _EFFORT_SCHEMA = compile_js(
223
+ rf'(effortLevel:{ARRAY_CALL})"low","medium","high","xhigh"(?:,"max")?(\]\))'
224
+ )
225
+
226
+
227
+ def _max_effort(content: str, _options: Options, outcome: Outcome) -> str:
228
+ """Let ``/effort max`` save as the default for new sessions."""
229
+ gate = outcome.step("gate", expect=True)
230
+ schema = outcome.step("schema", expect=True)
231
+
232
+ def widen_gate(match: re.Match[str]) -> str:
233
+ gate.candidates += 1
234
+ gate.applied += 1
235
+ if match.group(3):
236
+ return match.group(0)
237
+ name, value = match.group(1), match.group(2)
238
+ return (
239
+ f"function {name}({value}){{"
240
+ f'if({value}==="low"||{value}==="medium"||{value}==="high"'
241
+ f'||{value}==="xhigh"||{value}==="max")return {value};return}}'
242
+ )
243
+
244
+ output = _EFFORT_GATE.sub(widen_gate, content)
245
+
246
+ # No already-applied branch, unlike the gate above: the levels are spliced
247
+ # into the array the match found, so a bundle that already carries `max` is
248
+ # rewritten to itself. `widen_gate` needs one because its unconditional path
249
+ # also normalises the brace form, and would churn a bundle that was fine.
250
+ def widen_schema(match: re.Match[str]) -> str:
251
+ schema.candidates += 1
252
+ schema.applied += 1
253
+ return f'{match.group(1)}"low","medium","high","xhigh","max"{match.group(2)}'
254
+
255
+ return _EFFORT_SCHEMA.sub(widen_schema, output)
256
+
257
+
192
258
  PATCHES = [
193
259
  Patch(
194
260
  id="thinking-summaries",
195
261
  title="Fix blank thinking blocks",
196
262
  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
- group=GROUP_THINKING,
263
+ group=GROUP_OUTPUT,
198
264
  fn=_thinking_summaries,
199
265
  anchors=(_BUCKET_HEADER, "?.atis"),
200
266
  ),
@@ -202,8 +268,20 @@ PATCHES = [
202
268
  id="thinking-inline",
203
269
  title="Always show thinking",
204
270
  summary="Render thinking blocks inline instead of hiding them behind ctrl+o.",
205
- group=GROUP_THINKING,
271
+ group=GROUP_OUTPUT,
206
272
  fn=_thinking_inline,
207
273
  anchors=('case"thinking":', "isTranscriptMode:", "latestThinkingSummary"),
208
274
  ),
275
+ Patch(
276
+ id="max-effort",
277
+ title="Persist max effort",
278
+ summary="Let /effort max save as your default for new sessions, like the other levels.",
279
+ group=GROUP_MODELS,
280
+ fn=_max_effort,
281
+ anchors=(
282
+ '==="low"||',
283
+ '"low","medium","high","xhigh"]',
284
+ "Persisted effort level",
285
+ ),
286
+ ),
209
287
  ]
@@ -57,6 +57,8 @@ def applied_value(patch: Patch, outcome: Outcome, options: Options) -> str | Non
57
57
  return options.brand
58
58
  if patch.id == "version-marker":
59
59
  return options.version_suffix
60
+ if patch.id == "org-label":
61
+ return options.org_label or "hidden"
60
62
  if patch.id == "subagent-models" and options.subagent_models:
61
63
  return ", ".join(f"{a}={m}" for a, m in options.subagent_models.items())
62
64
  if patch.id == "codex-models" and options.codex_models:
@@ -256,7 +256,7 @@ wheels = [
256
256
 
257
257
  [[package]]
258
258
  name = "patch-cc"
259
- version = "0.2.0"
259
+ version = "0.2.1"
260
260
  source = { editable = "." }
261
261
  dependencies = [
262
262
  { name = "blessed" },
File without changes
File without changes
File without changes
File without changes
File without changes