From 69c78103455b0f689b1e46c8b583582e7e9c39c6 Mon Sep 17 00:00:00 2001 From: smoido Date: Mon, 7 Sep 2026 20:37:11 +0300 Subject: [PATCH] README: install commands first, sections reorganized --- README.md | 650 +++++++++++++++++++++++++---------------------- linux/install.sh | 16 +- 2 files changed, 361 insertions(+), 305 deletions(-) diff --git a/README.md b/README.md index 15956a4..d0660a3 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,10 @@ Switch Claude Code system-wide between **Anthropic**, **OpenRouter**, **Z.AI**, and a local **LM Studio** server — with named per-tier model presets. -Windows / PowerShell 5.1 with no external dependencies, and a POSIX port for -Linux and macOS that needs only python3 — including a theme-aware TUI and an -Omarchy bar widget. See [Linux / Omarchy](#linux--omarchy). +One write to `~/.claude/settings.json` covers the CLI, the VS Code extension, and +the desktop app. Restart Claude Code afterwards — nothing else. Windows +(PowerShell 5.1, no external dependencies) and a POSIX port for Linux and macOS +(python3 only), including a theme-aware TUI and an Omarchy bar widget. ``` claude-mode # interactive menu @@ -15,11 +16,124 @@ claude-mode zai # Z.AI GLM coding plan (preset: zai) claude-mode lmstudio # local server (preset: lmstudio) ``` -Works for the CLI, the VS Code extension, and the desktop app from a single -switch. Restart Claude Code afterwards — nothing else. +**Contents:** [Install](#install) · [First run](#first-run) · [The interactive +menu](#the-interactive-menu) · [Commands](#commands) · [Presets](#presets-shipped) · +[Context windows](#context-windows-and-early-auto-compaction) · [Z.AI](#zai-mode) · +[LM Studio](#lm-studio-mode) · [Live sessions](#live-sessions) · +[Troubleshooting](#troubleshooting) · [Design decisions](#design-decisions) · +[Layout](#layout) · [Linux / Omarchy](#linux--omarchy) · [The bar +widget](#the-omarchy-bar-widget) · [Uninstall](#uninstall) --- +## Install + +### Windows (PowerShell 5.1+) + +One line: + +```powershell +irm https://git.nebulm.com/smoido/claude-mode/raw/branch/master/install.ps1 | iex +``` + +Piped in like that there is no checkout, so the installer fetches the +repository archive to `%TEMP%` and installs from it. To pass switches +(`-Force`, `-SkipKeyPrompt`), download first and run the file: + +```powershell +irm https://git.nebulm.com/smoido/claude-mode/raw/branch/master/install.ps1 -OutFile install.ps1 +.\install.ps1 -Force +``` + +Or clone it — the normal path if you want to read the code and pull updates: + +```powershell +git clone https://git.nebulm.com/smoido/claude-mode.git +cd claude-mode +.\install.ps1 +``` + +Installs to `~/.claude-mode/` (ACL: you only), drops `claude-mode.cmd` into +`~/.local/bin` (already on your User PATH, next to `claude.exe`), and adds a +marked block to `~/Documents/WindowsPowerShell/profile.ps1`. + +If the profile doesn't load: `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`. + +### Linux / macOS + +One line: + +```bash +curl -fsSL https://git.nebulm.com/smoido/claude-mode/raw/branch/master/linux/install.sh | bash +``` + +Or clone: + +```bash +git clone https://git.nebulm.com/smoido/claude-mode.git +cd claude-mode +bash linux/install.sh # --skip-key-prompt to install without storing a key +``` + +Installs to `~/.claude-mode/`, symlinks `~/.local/bin/claude-mode`, and adds the +`claude` wrapper to `~/.bashrc` / `~/.zshrc` between markers. + +Secrets have no DPAPI equivalent here, so the vault picks the best backend +available and says which one it chose: macOS Keychain, libsecret, `pass`, or a +0600 file that is honestly labelled as unencrypted. + +### Omarchy bar widget + +```bash +bash omarchy/install.sh +``` + +Copies the plugin to `~/.config/omarchy/plugins/smoido.claude-mode/` and adds its +id to the bar layout in `~/.config/omarchy/shell.json` (backed up first). Needs a +checkout — clone first. See [the bar widget](#the-omarchy-bar-widget) for what it +does. + +### All three, and upgrades + +The installers never touch `~/.claude/settings.json` — that only happens on an +actual mode switch, which backs it up to `~/.claude-mode/backups/` first +(last 20 kept). + +Re-running any of them is a safe upgrade: scripts are overwritten, existing +presets are kept unless `-Force` / `--force` is passed. + +## First run + +A shipped preset is a starting point, not a working configuration. OpenRouter +and Z.AI have no key stored. LM Studio's model ids are whatever happened to be +installed on the machine this was packaged on, which is almost certainly not +yours. + +So the shipped presets carry `configured: false`, and preflight treats that as a +blocker with its own remedy: + +```bash +claude-mode setup openrouter # key, then models from OpenRouter's catalogue +claude-mode setup zai # key, then per-tier GLM models +claude-mode setup lmstudio # server URL, auth, then models from that server +claude-mode setup anthropic # nothing to do; it uses your existing login +``` + +Setup asks only what it cannot work out, shows the current model map before +offering to change it, and picks from the provider's own catalogue rather than +asking anyone to type a model id from memory. LM Studio maps one model across +every tier, since a local server has one loaded at a time and per-tier mapping +would just pay the load cost on every tier change; the remote gateways ask per +tier, which is the point of them. + +In a terminal, a switch that trips this offers to run setup there and then +rather than printing a command to type next. It sets `configured: true` on the +way out. + +**Absent means configured.** Presets that predate this, and any built by hand +with `preset new`, are nobody's business but yours and will not start demanding +a wizard. + ## The interactive menu Run `claude-mode` with no arguments. The mode you're already in is omitted — @@ -45,84 +159,6 @@ edit the preset that's currently live it re-applies immediately. When stdin is redirected (scripts, CI) the menu is skipped and `status` prints instead, so `claude-mode` is still safe in a pipeline. -## Why settings.json and not a profile export - -Three ways to make this persist. I picked the second. - -**1. Export the variables from `$PROFILE`.** -The obvious move, and the wrong one here. It only covers processes launched from -a PowerShell session that loaded the profile — which is exactly *not* how you use -Claude Code. The VS Code extension is spawned by VS Code, not by your shell, so -it would never see the exports. Same for the desktop app, `cmd.exe`, and any -terminal opened before the switch. Worse, the failure is silent: you switch to -`anthropic`, a shell opened five minutes ago still has `ANTHROPIC_BASE_URL` set, -and that session quietly keeps billing OpenRouter. - -**2. Rewrite the `env` block in `~/.claude/settings.json`.** ← chosen -Claude Code reads this file on every startup, from every launch context. One -write, and the next `claude` — CLI, extension, desktop — picks it up. A switch is -atomic: one file, one source of truth. `claude-mode anthropic` *deletes* the -managed keys rather than blanking them, so nothing can linger and break native -auth. The cost is that config is global rather than per-terminal. - -**3. Persistent User-scope environment variables (`setx`).** -Also global and reboot-proof, but strictly worse: new processes only, values sit -in the registry in plaintext, and a stale entry silently outranks whatever -`claude-mode` writes. This tool treats them as a fault condition — `status` and -`doctor` flag them and offer removal, backing the old value up first. - -The `claude` wrapper in the profile is a **safety net, not the mechanism**. It -strips inherited process-level copies of all thirteen managed variables before -launching `claude.exe`. Everything still works without it — including in VS Code, -which never loads the profile. - -## Why API keys are not in settings.json - -`settings.json` is a config file you'll hand-edit, diff, and possibly paste into -a bug report. A `sk-or-` or Z.AI token does not belong there. - -Keys are stored **DPAPI-encrypted** in `~/.claude-mode/vault/*.cred` — encrypted -against your Windows account on this machine, so copying the file elsewhere or -reading it as another user yields nothing — with the file ACL restricted to you. -Claude Code receives the key at runtime through `apiKeyHelper`, which decrypts -and prints it. `settings.json` holds only the base URL and model IDs. - -In `anthropic` mode the helper is removed from settings.json *and* returns -nothing when state says `anthropic` — belt and braces. LM Studio's `lmstudio` -token is a placeholder, not a secret, so it's written inline and the helper stays -out of it. - -### When the helper "is failing" - -Claude Code reports a broken helper as *your apiKeyHelper script is failing* and -shows its stderr under `/status`. Two causes account for nearly all of it. - -**A space in your home directory.** `apiKeyHelper` is a shell *command line*, not -a path, so `C:\Users\Firstname Lastname\.claude-mode\bin\claude-key-helper.cmd` -is split at the space and cmd tries to run `C:\Users\Firstname`. The value is now -quoted when it needs to be, on both ports — `shlex.quote` on POSIX, where a -`/Users/Firstname Lastname` home does the same thing. Paths that need no quoting -are written bare exactly as before, so no existing `settings.json` churns. - -`doctor` used to miss this, because it quoted the path itself before running it -and so tested something Claude Code never sees. It now reads the string out of -`settings.json`, says so when that string is not what a switch would write, and -executes *that* string through a shell. Re-running the switch rewrites it: - -``` -claude-mode openrouter default -claude-mode doctor -``` - -**A key stored by a different Windows account.** The vault is DPAPI `CurrentUser` -scope, so a key stored from an elevated or *run as* shell cannot be decrypted by -the account Claude Code runs as. Re-run `claude-mode set-key ` unelevated, -as yourself. - -Every helper failure path now names itself on stderr rather than exiting 1 in -silence, so `/status` distinguishes these from a missing preset or an empty -vault. The success paths stay silent — stdout carries the key and nothing else. - ## Commands ``` @@ -207,7 +243,7 @@ background tasks. Switching without `contextTokens` prints a warning. Adjust per preset: -```powershell +```bash # edit ~/.claude-mode/presets/.json -> "contextTokens": 262144 claude-mode doctor # re-checks declared vs actual ``` @@ -229,7 +265,7 @@ Per [Z.AI's Claude Code docs](https://docs.z.ai/devpack/tool/claude): All three extra variables are confirmed present in CLI 2.1.221. Setup: -```powershell +```bash claude-mode set-key zai # paste your key from https://z.ai/manage-apikey/apikey-list claude-mode zai claude-mode doctor # sends a 1-token request to prove the key works @@ -286,7 +322,7 @@ end-to-end with a cold JIT load, streaming, and tool calls. Honest caveat: KAT-Coder's template *does* contain the assertion, but I could not reproduce the failure against it here — cold JIT, streaming, tools, system blocks, and multi-turn `tool_result` all succeeded. Whether it trips seems to -depend on which parser strategy the runtime picks. If the model setup lands on +depend on which parser strategy the runtime picks. If the model you land on does spam, run setup again and pick another: ```bash @@ -298,67 +334,37 @@ the model must be **installed** — JIT loading handles "not loaded" fine. If you enable authentication in LM Studio, move that preset to a vault key: -```powershell +```bash claude-mode set-key lmstudio # then in ~/.claude-mode/presets/lmstudio.json: # "auth": { "mode": "vault", "keyRef": "lmstudio" } ``` -## CLI version +### LM Studio somewhere other than this machine -Verified against `claude.exe` **2.1.221** by scanning the binary — every variable -this tool writes is referenced by it: +It ships on loopback, but that is a default, not a constraint. A preset is just +a base URL and an auth block, so all of these are the same two fields: -`ANTHROPIC_BASE_URL` · `ANTHROPIC_AUTH_TOKEN` · `ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU,FABLE}_MODEL` · -`CLAUDE_CODE_SUBAGENT_MODEL` · `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` · -`CLAUDE_CODE_ATTRIBUTION_HEADER` · `CLAUDE_CODE_AUTO_COMPACT_WINDOW` · -`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` · `API_TIMEOUT_MS` · `apiKeyHelper` - -(On 2.1.89 the fable and gateway-discovery vars did not exist; the update to -2.1.221 added both.) - -## Setup - -One line, on any machine that can reach the repo (PowerShell 5.1 is fine): - -```powershell -irm https://git.nebulm.com/smoido/claude-mode/raw/branch/master/install.ps1 | iex +```bash +claude-mode preset url lmstudio http://192.168.1.40:1234 # another box on the LAN +claude-mode preset url lmstudio https://lms.example.net # through a tunnel or proxy +claude-mode preset auth lmstudio key lmstudio # that server wants an API key +claude-mode set-key lmstudio # store it (vault, not settings.json) +claude-mode preset auth lmstudio none # back to the open-server default ``` -Piped in like that there is no checkout, so the installer fetches the -repository archive to `%TEMP%` and installs from it. To pass switches -(`-Force`, `-SkipKeyPrompt`), download first and run the file: +`auth none` writes LM Studio's inline placeholder token, which is not a secret +and is what an unauthenticated server expects. `auth key` moves it to the vault +like every other credential — a real key on a public address is a real key. +Nothing here changes the shipped presets unless you ask it to; the local default +stays exactly as it was. -```powershell -irm https://git.nebulm.com/smoido/claude-mode/raw/branch/master/install.ps1 -OutFile install.ps1 -.\install.ps1 -Force -``` +## Live sessions -Or clone it — the normal path if you want to read the code and pull updates: +### Why a switch breaks a running session -```powershell -git clone https://git.nebulm.com/smoido/claude-mode.git -cd claude-mode -.\install.ps1 -``` - -All three install the same way: to `~/.claude-mode/` (ACL: you only), dropping -`claude-mode.cmd` into `~/.local/bin` (already on your User PATH, next to -`claude.exe`), and adding a marked block to -`~/Documents/WindowsPowerShell/profile.ps1`. -`~/.claude/settings.json` is **not** touched by the installer — only by an actual -mode switch, which backs it up to `~/.claude-mode/backups/` first (last 20 kept). - -Re-running any of them is a safe upgrade: scripts are overwritten, existing -presets are kept unless `-Force` is passed. - -If the profile doesn't load: `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`. - -## Restarting sessions - -A switch **breaks** running sessions. This originally said it did not affect -them; that was wrong, and the difference matters, because the two halves of the -config behave differently. +A switch **breaks** running sessions. The two halves of the config behave +differently, which is why. The static half — base URL, model ids, the env block — genuinely is read once at startup, and a running session keeps what it started with. @@ -375,7 +381,14 @@ switch reaches into a live session through the one thing that was never cached: | another preset of the *same* provider, same `keyRef` | same key, same endpoint — this one survives, on the model ids it started with | Either of the first two starts failing calls whenever the TTL happens to expire: -mid-turn as easily as between turns. +mid-turn as easily as between turns. So restart affected sessions — and +`claude-mode sessions --restart` will do it for you: + +- **CLI** — exit and relaunch `claude` +- **VS Code** — `Ctrl+Shift+P` → *Developer: Reload Window* +- **Desktop app** — quit and reopen + +`claude-mode status` shows what the *next* launch will use. ### The part that is not just an inconvenience @@ -441,6 +454,44 @@ session untouched. It refuses to touch a transcript written to in the last 90 seconds, since that one belongs to a session still alive. +### The switch asks first + +A switch stops and asks while any sessions are running, offering to restart them +(the only answer that ends with everything on the mode the bar now claims), close +them, proceed anyway, or abort — and abort is the default. Non-interactively it +refuses outright unless given `--yes`. + +``` +claude-mode sessions + running sessions (2) + 562250 pts/4 /home/smoido/Work working (this session - never touched) + 631644 pts/1 /home/smoido/Projects/api +``` + +Sessions are found through `/proc//exe`, which on Linux resolves to the +real `claude` binary — a process-name match would sweep up every shell that +merely mentions claude on its command line, including the one this is running +from. Two things are then filtered out: + +- **The calling session.** Killing the session that asked for the kill is not a + thing anyone means, so it is listed and never signalled. +- **Forks of a session.** A busy session spawns children off its own binary, + and they inherit the same `exe`. Without excluding anything whose parent is + itself claude, the count climbs and falls with how hard the machine is + thinking — it read 2, 5, 11 and 40 on the same two sessions before this. A + real session's parent is a terminal. + +`working` is a sampled-CPU heuristic — two reads of `utime + stime` 300ms apart +— so it is a good guess about which session is mid-turn, not a promise. + +`--stop` sends SIGTERM (never SIGKILL; Claude Code writes out its transcript on +the way down). `--restart` stops each session and reopens it — re-running the +parent terminal's own command line where there is one, so the same terminal, +flags and directory come back, and falling back to a fresh terminal in the +session's directory otherwise. Interactive runs confirm first; `--yes` is for +callers that have already asked, and `--dry-run` prints the plan and touches +nothing. + ### The bar notices for you Nothing tells you a session is unresumable until you try to resume it, by which @@ -458,123 +509,92 @@ rest of the file is never opened. Since that is the overwhelmingly common case, the whole sweep costs ~60ms for 59 transcripts, against ~5s for the obvious version that reads every byte of every one. -This is why a switch now asks before it writes rather than reporting afterwards. -So restart affected sessions — and `claude-mode sessions --restart` will do it -for you: +This is why a switch asks before it writes rather than reporting afterwards. -- **CLI** — exit and relaunch `claude` -- **VS Code** — `Ctrl+Shift+P` → *Developer: Reload Window* -- **Desktop app** — quit and reopen +## Troubleshooting -`claude-mode status` shows what the *next* launch will use. +### When the helper "is failing" -## Layout +Claude Code reports a broken helper as *your apiKeyHelper script is failing* and +shows its stderr under `/status`. Two causes account for nearly all of it. + +**A space in your home directory.** `apiKeyHelper` is a shell *command line*, not +a path, so `C:\Users\Firstname Lastname\.claude-mode\bin\claude-key-helper.cmd` +is split at the space and cmd tries to run `C:\Users\Firstname`. The value is now +quoted when it needs to be, on both ports — `shlex.quote` on POSIX, where a +`/Users/Firstname Lastname` home does the same thing. Paths that need no quoting +are written bare exactly as before, so no existing `settings.json` churns. + +`doctor` used to miss this, because it quoted the path itself before running it +and so tested something Claude Code never sees. It now reads the string out of +`settings.json`, says so when that string is not what a switch would write, and +executes *that* string through a shell. Re-running the switch rewrites it: ``` -~/.claude-mode/ - claude-mode.ps1 main script - state.json mode, active preset, and the exact env keys last written - presets/*.json provider + model maps - vault/*.cred DPAPI-encrypted keys (openrouter, zai, ...) - backups/ settings.json snapshots + removed env-var values - bin/claude-key-helper.cmd apiKeyHelper shim -~/.local/bin/claude-mode.cmd PATH entry point (works from any shell) -~/Documents/WindowsPowerShell/profile.ps1 - claude-mode + claude functions, between markers +claude-mode openrouter default +claude-mode doctor ``` -`state.json` records which env keys the last switch actually wrote, so a custom -`extraEnv` key (Z.AI's timeouts, LM Studio's attribution header) is removed when -you switch away — even though no other preset knows that key exists. +**A key stored by a different Windows account.** The vault is DPAPI `CurrentUser` +scope, so a key stored from an elevated or *run as* shell cannot be decrypted by +the account Claude Code runs as. Re-run `claude-mode set-key ` unelevated, +as yourself. -## Linux / Omarchy +Other known issues with their own sections: [context windows and early +compaction](#context-windows-and-early-auto-compaction), the [LM Studio template +parser spam](#the-server-error--unable-to-generate-parser-for-this-template-spam), +and [refusing a switch that would not work](#refusing-a-switch-that-would-not-work). -The `linux/` tree is a POSIX port of the same design: one write to -`~/.claude/settings.json`, secrets kept out of it, presets shared verbatim with -the Windows build. +## Design decisions -```bash -cd claude-code-switcher -bash linux/install.sh # --skip-key-prompt to install without storing a key -``` +### Why settings.json and not a profile export -Installs to `~/.claude-mode/`, symlinks `~/.local/bin/claude-mode`, and adds the -`claude` wrapper to `~/.bashrc` / `~/.zshrc` between markers. +Three ways to make this persist. I picked the second. -Secrets have no DPAPI equivalent here, so the vault picks the best backend -available and says which one it chose: macOS Keychain, libsecret, `pass`, or a -0600 file that is honestly labelled as unencrypted. +**1. Export the variables from `$PROFILE`.** +The obvious move, and the wrong one here. It only covers processes launched from +a PowerShell session that loaded the profile — which is exactly *not* how you use +Claude Code. The VS Code extension is spawned by VS Code, not by your shell, so +it would never see the exports. Same for the desktop app, `cmd.exe`, and any +terminal opened before the switch. Worse, the failure is silent: you switch to +`anthropic`, a shell opened five minutes ago still has `ANTHROPIC_BASE_URL` set, +and that session quietly keeps billing OpenRouter. -### The menu follows your desktop theme +**2. Rewrite the `env` block in `~/.claude/settings.json`.** ← chosen +Claude Code reads this file on every startup, from every launch context. One +write, and the next `claude` — CLI, extension, desktop — picks it up. A switch is +atomic: one file, one source of truth. `claude-mode anthropic` *deletes* the +managed keys rather than blanking them, so nothing can linger and break native +auth. The cost is that config is global rather than per-terminal. -The sixteen ANSI colour slots carry no guarantee about relative brightness, and -monochrome themes exploit that. Under Omarchy's Solitude, slot 36 — headings — -resolves to `#707070` and slot 31 — `FAIL` — to `#565d60`. Against `#cacccc` -body text on a `#101315` ground that is 3.8:1 and 2.8:1 where the body text is -11.6:1, so headings render as fine print and an error becomes the quietest -thing on screen. Exactly backwards. +**3. Persistent User-scope environment variables (`setx`).** +Also global and reboot-proof, but strictly worse: new processes only, values sit +in the registry in plaintext, and a stale entry silently outranks whatever +`claude-mode` writes. This tool treats them as a fault condition — `status` and +`doctor` flag them and offer removal, backing the old value up first. -So when Omarchy is present, the palette is derived from the theme it publishes -at `~/.local/state/omarchy/current/theme/colors.toml` instead. Every role is -measured against the background it will actually be drawn on and lifted toward -the foreground when it falls short, which keeps hue where the theme has any and -falls back to weight where it does not: +The `claude` wrapper in the profile is a **safety net, not the mechanism**. It +strips inherited process-level copies of all thirteen managed variables before +launching `claude.exe`. Everything still works without it — including in VS Code, +which never loads the profile. -| role | before (Solitude) | after | -|---|---|---| -| heading | 3.8:1 | 9.4:1 | -| `FAIL` | 2.8:1 | 5.2:1 | -| help text | 2.2:1 | 2.2:1 (recessive on purpose, floored) | +### Why API keys are not in settings.json -Light themes are handled by the same arithmetic — `mode` in `colors.toml` is -authoritative, so `catppuccin-latte` and `flexoki-light` keep their accents -rather than washing out. +`settings.json` is a config file you'll hand-edit, diff, and possibly paste into +a bug report. A `sk-or-` or Z.AI token does not belong there. -Overrides: `CLAUDE_MODE_THEME=/path/to/colors.toml` points it elsewhere, -`NO_COLOR` turns it off. Without Omarchy, or on a terminal that cannot do -truecolor, it falls back to the ANSI slots with the two roles the slots get -wrong corrected — bright red for `FAIL`, bold on headings. +Keys are stored **DPAPI-encrypted** in `~/.claude-mode/vault/*.cred` — encrypted +against your Windows account on this machine, so copying the file elsewhere or +reading it as another user yields nothing — with the file ACL restricted to you. +Claude Code receives the key at runtime through `apiKeyHelper`, which decrypts +and prints it. `settings.json` holds only the base URL and model IDs. -### `claude-mode health` +In `anthropic` mode the helper is removed from settings.json *and* returns +nothing when state says `anthropic` — belt and braces. LM Studio's `lmstudio` +token is a placeholder, not a secret, so it's written inline and the helper stays +out of it. -Rewrites `~/.claude-mode/health.json`, the machine-readable mirror of the -active configuration: mode, preset, model map, context window, key backend, and -the switchable preset catalogue. No key material. A switch and a `status` both -refresh it; the command exists for anything that wants to force it. - -## First-run setup - -A shipped preset is a starting point, not a working configuration. OpenRouter -and Z.AI have no key stored. LM Studio's model ids are whatever happened to be -installed on the machine this was packaged on, which is almost certainly not -yours. - -So the shipped presets carry `configured: false`, and preflight treats that as a -blocker with its own remedy: - -```bash -claude-mode setup openrouter # key, then models from OpenRouter's catalogue -claude-mode setup zai # key, then per-tier GLM models -claude-mode setup lmstudio # server URL, auth, then models from that server -claude-mode setup anthropic # nothing to do; it uses your existing login -``` - -Setup asks only what it cannot work out, shows the current model map before -offering to change it, and picks from the provider's own catalogue rather than -asking anyone to type a model id from memory. LM Studio maps one model across -every tier, since a local server has one loaded at a time and per-tier mapping -would just pay the load cost on every tier change; the remote gateways ask per -tier, which is the point of them. - -In a terminal, a switch that trips this offers to run setup there and then -rather than printing a command to type next. It sets `configured: true` on the -way out. - -**Absent means configured.** Presets that predate this, and any built by hand -with `preset new`, are nobody's business but yours and will not start demanding -a wizard. - -## Refusing a switch that would not work +### Refusing a switch that would not work A switch writes settings.json and is picked up by the *next* `claude` launch, so switching into a mode that cannot serve requests does not fail loudly — it @@ -616,83 +636,94 @@ an instance on a sleeping LAN box is exactly as absent as a loopback port with nothing behind it, and produces the identical silent breakage. Off-machine addresses get a longer timeout, not a pass. -### LM Studio somewhere other than this machine +### CLI version -It ships on loopback, but that is a default, not a constraint. A preset is just -a base URL and an auth block, so all of these are the same two fields: +Verified against `claude.exe` **2.1.221** by scanning the binary — every variable +this tool writes is referenced by it: -```bash -claude-mode preset url lmstudio http://192.168.1.40:1234 # another box on the LAN -claude-mode preset url lmstudio https://lms.example.net # through a tunnel or proxy -claude-mode preset auth lmstudio key lmstudio # that server wants an API key -claude-mode set-key lmstudio # store it (vault, not settings.json) -claude-mode preset auth lmstudio none # back to the open-server default -``` +`ANTHROPIC_BASE_URL` · `ANTHROPIC_AUTH_TOKEN` · `ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU,FABLE}_MODEL` · +`CLAUDE_CODE_SUBAGENT_MODEL` · `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` · +`CLAUDE_CODE_ATTRIBUTION_HEADER` · `CLAUDE_CODE_AUTO_COMPACT_WINDOW` · +`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` · `API_TIMEOUT_MS` · `apiKeyHelper` -`auth none` writes LM Studio's inline placeholder token, which is not a secret -and is what an unauthenticated server expects. `auth key` moves it to the vault -like every other credential — a real key on a public address is a real key. -Nothing here changes the shipped presets unless you ask it to; the local default -stays exactly as it was. +(On 2.1.89 the fable and gateway-discovery vars did not exist; the update to +2.1.221 added both.) -## Sessions still on the old provider +## Layout -A switch does not leave running sessions on the old provider — it breaks them. -See [Restarting sessions](#restarting-sessions) for the mechanism: their -endpoint is fixed at startup but their credential is re-fetched on a timer, so -it switches under them and the endpoint they are still pointed at refuses it. -Which is why this is a decision rather than a notification. - -A switch stops and asks while any are running, offering to restart them (the -only answer that ends with everything on the mode the bar now claims), close -them, proceed anyway, or abort — and abort is the default. Non-interactively it -refuses outright unless given `--yes`. +Windows: ``` -claude-mode sessions - running sessions (2) - 562250 pts/4 /home/smoido/Work working (this session - never touched) - 631644 pts/1 /home/smoido/Projects/api +~/.claude-mode/ + claude-mode.ps1 main script + state.json mode, active preset, and the exact env keys last written + presets/*.json provider + model maps + vault/*.cred DPAPI-encrypted keys (openrouter, zai, ...) + backups/ settings.json snapshots + removed env-var values + bin/claude-key-helper.cmd apiKeyHelper shim +~/.local/bin/claude-mode.cmd PATH entry point (works from any shell) +~/Documents/WindowsPowerShell/profile.ps1 + claude-mode + claude functions, between markers ``` -Sessions are found through `/proc//exe`, which on Linux resolves to the -real `claude` binary — a process-name match would sweep up every shell that -merely mentions claude on its command line, including the one this is running -from. Two things are then filtered out: +Linux / macOS: the same tree, with `bin/claude-mode` + `bin/claude-key-helper.sh`, +`bin/cm-json.py`, `bin/cm-vault.sh`, the symlink `~/.local/bin/claude-mode`, and +the wrapper block in `~/.bashrc` / `~/.zshrc`. -- **The calling session.** Killing the session that asked for the kill is not a - thing anyone means, so it is listed and never signalled. -- **Forks of a session.** A busy session spawns children off its own binary, - and they inherit the same `exe`. Without excluding anything whose parent is - itself claude, the count climbs and falls with how hard the machine is - thinking — it read 2, 5, 11 and 40 on the same two sessions before this. A - real session's parent is a terminal. +`state.json` records which env keys the last switch actually wrote, so a custom +`extraEnv` key (Z.AI's timeouts, LM Studio's attribution header) is removed when +you switch away — even though no other preset knows that key exists. -`working` is a sampled-CPU heuristic — two reads of `utime + stime` 300ms apart -— so it is a good guess about which session is mid-turn, not a promise. +## Linux / Omarchy -`--stop` sends SIGTERM (never SIGKILL; Claude Code writes out its transcript on -the way down). `--restart` stops each session and reopens it — re-running the -parent terminal's own command line where there is one, so the same terminal, -flags and directory come back, and falling back to a fresh terminal in the -session's directory otherwise. Interactive runs confirm first; `--yes` is for -callers that have already asked, and `--dry-run` prints the plan and touches -nothing. +The `linux/` tree is a POSIX port of the same design: one write to +`~/.claude/settings.json`, secrets kept out of it, presets shared verbatim with +the Windows build. Install is [up top](#linux--macos). + +### The menu follows your desktop theme + +The sixteen ANSI colour slots carry no guarantee about relative brightness, and +monochrome themes exploit that. Under Omarchy's Solitude, slot 36 — headings — +resolves to `#707070` and slot 31 — `FAIL` — to `#565d60`. Against `#cacccc` +body text on a `#101315` ground that is 3.8:1 and 2.8:1 where the body text is +11.6:1, so headings render as fine print and an error becomes the quietest +thing on screen. Exactly backwards. + +So when Omarchy is present, the palette is derived from the theme it publishes +at `~/.local/state/omarchy/current/theme/colors.toml` instead. Every role is +measured against the background it will actually be drawn on and lifted toward +the foreground when it falls short, which keeps hue where the theme has any and +falls back to weight where it does not: + +| role | before (Solitude) | after | +|---|---|---| +| heading | 3.8:1 | 9.4:1 | +| `FAIL` | 2.8:1 | 5.2:1 | +| help text | 2.2:1 | 2.2:1 (recessive on purpose, floored) | + +Light themes are handled by the same arithmetic — `mode` in `colors.toml` is +authoritative, so `catppuccin-latte` and `flexoki-light` keep their accents +rather than washing out. + +Overrides: `CLAUDE_MODE_THEME=/path/to/colors.toml` points it elsewhere, +`NO_COLOR` turns it off. Without Omarchy, or on a terminal that cannot do +truecolor, it falls back to the ANSI slots with the two roles the slots get +wrong corrected — bright red for `FAIL`, bold on headings. + +### `claude-mode health` + +Rewrites `~/.claude-mode/health.json`, the machine-readable mirror of the +active configuration: mode, preset, model map, context window, key backend, and +the switchable preset catalogue. No key material. A switch and a `status` both +refresh it; the command exists for anything that wants to force it. ## The Omarchy bar widget An icon in the Omarchy top bar showing which provider the next `claude` launch -will use, and a panel that switches it without a terminal. - -```bash -bash omarchy/install.sh -``` - -Copies the plugin to `~/.config/omarchy/plugins/smoido.claude-mode/` and adds -its id to the bar layout in `~/.config/omarchy/shell.json` (backed up first). -Both hot-reload, so nothing needs restarting — except after a change to -`Modes.js`, which the QML engine caches for the life of the process as a -`.pragma library` (`omarchy restart shell`). +will use, and a panel that switches it without a terminal. Install is +[up top](#omarchy-bar-widget). Both install steps hot-reload, so nothing needs +restarting — except after a change to `Modes.js`, which the QML engine caches +for the life of the process as a `.pragma library` (`omarchy restart shell`). The icon is the mode, and it is the provider's own logo: the Claude burst, the OpenRouter arrow, the Z.AI Z, the LM Studio mark. They are drawn as vector paths @@ -737,9 +768,9 @@ CLI does, and both can stop it: …* opens a terminal and runs the whole first-run flow there (a bar popup can host neither a hidden key prompt nor a filter-select model list), *Store the key…* opens a terminal for just the prompt, *Check again* re-runs the - preflight, and *Server settings…* opens the form below. The gear on any LM Studio preset row opens the same form without - waiting for a failure — per row, because two LM Studio presets can point at - two different machines. + preflight, and *Server settings…* opens the form below. The gear on any LM + Studio preset row opens the same form without waiting for a failure — per + row, because two LM Studio presets can point at two different machines. The form holds the base URL, a *Use local default* reset, and a switch for whether that server needs an API key. Saving rewrites the preset and drops @@ -749,6 +780,7 @@ CLI does, and both can stop it: *Switch and close*, *Switch only*, or *Cancel*. The session action is applied strictly **after** the write — restarting first would only bring them back up on the provider you just left. + - **Right-click** switches straight back to Anthropic. - **Middle-click** re-reads state. - **Hover** for mode, preset, and the opus/sonnet mapping. @@ -771,18 +803,28 @@ Placement is `right`, before `omarchy.agents`; override with `CM_BAR_SECTION` and `CM_BAR_BEFORE` when installing. Moving it later is a normal `omarchy bar move smoido.claude-mode --section
`. -Uninstall: - -```bash -rm -rf ~/.config/omarchy/plugins/smoido.claude-mode -# then remove the {"id": "smoido.claude-mode"} entry from ~/.config/omarchy/shell.json -``` - ## Uninstall +Windows: + ```powershell claude-mode anthropic # clean settings.json first Remove-Item ~\.claude-mode -Recurse -Force Remove-Item ~\.local\bin\claude-mode.cmd # then delete the block between the >>> claude-mode >>> markers in profile.ps1 ``` + +Linux / macOS: + +```bash +claude-mode anthropic # clean settings.json first +rm -rf ~/.claude-mode ~/.local/bin/claude-mode +# then delete the block between the >>> claude-mode >>> markers in ~/.bashrc / ~/.zshrc +``` + +Bar widget: + +```bash +rm -rf ~/.config/omarchy/plugins/smoido.claude-mode +# then remove the {"id": "smoido.claude-mode"} entry from ~/.config/omarchy/shell.json +``` diff --git a/linux/install.sh b/linux/install.sh index ef13f6f..994c472 100755 --- a/linux/install.sh +++ b/linux/install.sh @@ -20,7 +20,6 @@ for arg in "$@"; do esac done -SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="${CM_ROOT:-$HOME/.claude-mode}" BINDIR="$HOME/.local/bin" @@ -28,6 +27,21 @@ green() { printf ' \033[32mok \033[0m %s\n' "$*"; } warn() { printf ' \033[33mwarn\033[0m %s\n' "$*"; } fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; } +# A checkout next to this script is the normal source. Piped in +# (curl ... | bash) there is no script file and no checkout, so fetch the +# repository archive and install from that instead. +SRC="$(cd "$(dirname "${BASH_SOURCE[0]:-''}")" 2>/dev/null && pwd)" || SRC="" +if [ ! -f "$SRC/claude-mode" ] || [ ! -f "$SRC/cm-json.py" ]; then + REPO_URL="${CM_REPO_URL:-https://git.nebulm.com/smoido/claude-mode}" + command -v curl >/dev/null 2>&1 || { fail 'curl required to fetch the source'; exit 1; } + TMP="$(mktemp -d "${TMPDIR:-/tmp}/claude-mode-src.XXXXXX")" + printf 'no local checkout found - fetching source from %s\n' "$REPO_URL" + curl -fsSL "$REPO_URL/archive/master.tar.gz" | tar -xz -C "$TMP" + EXTRACTED="$(find "$TMP" -type f -name claude-mode -print -quit)" + [ -n "$EXTRACTED" ] || { fail 'archive did not contain linux/claude-mode - repo layout changed?'; exit 1; } + SRC="$(dirname "$EXTRACTED")" +fi + printf '\ninstalling claude-mode -> %s\n' "$ROOT" # --- preflight -------------------------------------------------------------