The README had grown to 1,010 lines of user docs and design notes in one file,
and had gone stale: it still showed the old numbered menu, set up only three
providers, and listed preflight checks and a file layout that predate the last
three releases. It now carries install, first run, the full command
reference, the providers at a glance, troubleshooting and a docs index.
docs/:
- providers.md presets, defaults, the model cache, context windows, each
provider (OpenRouter's cost guard and guardrail check are
written up for the first time), adding a provider
- live-sessions.md what a switch does to running sessions, and repair
- design.md why settings.json, why keys stay out of it (and the vault
per platform), the preflight checks as they are now
- bar-widget.md the widget as it is now: providers from health.json, every
server provider's settings, the restart after an upgrade
- architecture.md the pieces, every file on disk and who writes it, the
contracts between them, where to change what
- development.md running the tests, the conventions the code follows,
working on the widget, releasing
CONTRIBUTING.md points at it.
Also:
- The per-project session listing used awk, which the CLI avoids because it
is missing from minimal images; it uses the script's own TSV helpers now,
and tests/static.sh fails on any awk in the CLI.
- tests/static.sh checks every relative Markdown link and #anchor.
- A unit test pins the managed env keys between cm-json.py and
claude-mode.ps1, which only a comment kept in step before.
- test_sessions covers the per-project listing, which nothing ran.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
131 lines
7.5 KiB
Markdown
131 lines
7.5 KiB
Markdown
# Architecture
|
|
|
|
How the pieces of claude-mode fit together, what each file on disk is for, and
|
|
where to go to change something. Why it is shaped this way is in
|
|
[design.md](design.md); how to work on it, in [development.md](development.md).
|
|
|
|
## The pieces
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
user([you]) --> cli["claude-mode<br/>(bash on Linux/macOS,<br/>PowerShell on Windows)"]
|
|
widget["bar widget<br/>(Omarchy, QML)"] -- runs --> cli
|
|
cli -- all JSON through --> engine["cm-json.py<br/>(POSIX only)"]
|
|
engine --> providers[(providers.json)]
|
|
engine --> presets[(presets/*.json)]
|
|
cli -- a switch writes --> settings[(~/.claude/settings.json)]
|
|
cli --> state[(state.json)]
|
|
cli --> health[(health.json)]
|
|
claude["Claude Code"] -- reads at startup --> settings
|
|
claude -- runs on a timer --> helper["key helper"]
|
|
helper --> state
|
|
helper --> vault[(vault)]
|
|
widget -- watches --> health
|
|
widget -- reads --> cache[(models-cache.json)]
|
|
```
|
|
|
|
- **The CLI** does everything that changes state. The POSIX port is bash for
|
|
control flow and terminal UI, with every piece of JSON handled by `cm-json.py`
|
|
— bash only ever sees flat lines of text. The Windows build is one PowerShell
|
|
script that does the same with `ConvertFrom-Json`.
|
|
- **A switch** rewrites the managed keys in `~/.claude/settings.json` (an `env`
|
|
block and `apiKeyHelper`) and records what it wrote in `state.json`. Claude Code
|
|
reads `settings.json` at startup, from every launch context.
|
|
- **The key helper** is what `apiKeyHelper` runs. It prints the active preset's key
|
|
from the vault and nothing else, so no key ever sits in `settings.json`. Claude
|
|
Code re-runs it on a timer, in every live session.
|
|
- **The bar widget** never reads config itself. It watches `health.json`, which the
|
|
CLI rewrites on every change, and runs the CLI for anything it does.
|
|
- **`providers.json`** describes every gateway provider as data, read by all three
|
|
(the widget through `health.json`). Behaviour that differs in kind is chosen by
|
|
name from it — see [providers.md](providers.md#adding-a-provider).
|
|
|
|
## Files on disk
|
|
|
|
Under `~/.claude-mode/`:
|
|
|
|
| file | written by | read by |
|
|
|---|---|---|
|
|
| `bin/claude-mode`, `bin/cm-json.py`, `bin/cm-vault.sh`, `bin/claude-key-helper.sh` | the installer | — (Windows: `claude-mode.ps1` at the top, `bin/claude-key-helper.ps1` + `.cmd`) |
|
|
| `providers.json` | the installer, every time | the CLI, the key helper's engine, the widget (via `health.json`) |
|
|
| `presets/*.json` | the installer seeds them; `preset`, `setup`, the panel edit them | everything |
|
|
| `state.json` | a switch; `preset rename` of the active preset | the CLI, the key helper, the widget |
|
|
| `defaults.json` | `preset default` (and rename/rm keep it right) | the CLI; published in `health.json` |
|
|
| `health.json` | a switch, `status`, `doctor`, preset edits, `health` | the widget |
|
|
| `models-cache.json` | every catalogue fetch | the panel's model picker |
|
|
| `ignored-sessions.json` | `repair-session --ignore` / `--unignore` | the session scan |
|
|
| `vault/` | `set-key` (file backend; DPAPI on Windows) | the key helper |
|
|
| `backups/` | a switch, before it writes `settings.json` (last 20 kept) | you, if needed |
|
|
| `VERSION` | the installer | the CLI, stamped into `health.json` |
|
|
|
|
Outside it: `~/.claude/settings.json` (a switch writes the managed keys and never
|
|
anything else), `~/.claude/projects/*/*.jsonl` (Claude Code's transcripts, which
|
|
`repair-session` reads and repairs), and on Omarchy
|
|
`~/.config/omarchy/plugins/smoido.claude-mode/` (the widget).
|
|
|
|
## The repository
|
|
|
|
```
|
|
claude-mode.ps1, install.ps1 the Windows build and its installer
|
|
bin/ the Windows key helper
|
|
linux/ the POSIX port: claude-mode, cm-json.py, cm-vault.sh,
|
|
claude-key-helper.sh, install.sh, bootstrap.sh
|
|
omarchy/ the bar widget and its installer
|
|
providers.json, presets/ shared by both builds, byte for byte
|
|
scripts/ test.sh, bump-version.sh, build-package.ps1
|
|
tests/ static, python, cli and windows suites
|
|
docs/ this
|
|
```
|
|
|
|
## Contracts between the pieces
|
|
|
|
These are the interfaces one piece relies on another to keep. Each is pinned by a
|
|
test.
|
|
|
|
- **`providers.json`** — its shape and the values each field may take
|
|
([providers.md](providers.md#adding-a-provider)); `tests/static.sh` enforces them.
|
|
- **`provider-tsv`** — `cm-json.py` hands bash one tab-separated row per provider,
|
|
and bash addresses the columns by number (bash 3.2 has no associative arrays).
|
|
The column order is `PROVIDER_TSV` in `cm-json.py`, mirrored in a comment in
|
|
`linux/claude-mode`: append only. Pinned by `tests/python`.
|
|
- **The managed env keys** — the keys a switch owns and clears: `BASE_MANAGED` in
|
|
`cm-json.py` and `$script:BaseManagedEnvKeys` in `claude-mode.ps1` must list the
|
|
same keys. Pinned by `tests/python`.
|
|
- **`health.json`** — what the widget reads: `mode`, `preset`, `version`, `models`,
|
|
`contextTokens`, `keyBackend`, `presets` (the catalogue), `providers` (id, title,
|
|
blurb, logo, logoScale, serverEditable, perServerCatalogue, defaultKeyRef,
|
|
defaultBaseUrl, serverHint, keyOptional), `defaultPresetFor`,
|
|
`defaultPresetChosen`. No key material, ever.
|
|
- **`claude-mode preflight <mode> [preset]`** — JSON the widget acts on: `ok`,
|
|
`code` (`ok`, `needs-setup`, `no-models`, `no-url`, `missing-key`,
|
|
`helper-missing`, `server-auth`, `server-wrong`, `server-unreachable`,
|
|
`no-preset`, `provider-mismatch`), `title`, `detail`, `remedy` (a command), and
|
|
`remedyKind`, which picks the panel's button (`setup`, `set-key`, `edit-preset`,
|
|
`set-url`, `needs-key`, `start-server`).
|
|
- **`claude-mode repair-session --json`** — the scan: `broken[]` and `ignored[]`
|
|
(each with `sessionId`, `project`, `path`, `dropLines`, `providers`, `mtime`, and
|
|
`reason` for the ignored), `count`, `ignoredCount`, `maxAgeDays`.
|
|
- **`models-cache.json`** — `providers.<id>` → `fetchedAt`, `ok`, `failedAt`,
|
|
`baseUrl` (per-server providers), `models[]` (`id`, and `contextTokens`,
|
|
`priceIn`, `priceOut`, `state`, `note` where known). No keys.
|
|
- **The key helper** — prints the key and nothing else on stdout; every failure
|
|
explains itself on stderr, because Claude Code shows that under `/status`. It
|
|
reads the preset in one open and re-reads `state.json` once on a miss, so a
|
|
rename of the active preset cannot catch it mid-lookup.
|
|
|
|
## Where to change what
|
|
|
|
| to change | POSIX | Windows |
|
|
|---|---|---|
|
|
| a provider's endpoint, auth template, setup, checks or look | `providers.json` | the same file |
|
|
| a shipped preset | `presets/*.json` | the same files |
|
|
| what a switch writes to `settings.json` | `cm-json.py` `cmd_apply`, `BASE_MANAGED` | `Set-ClaudeMode`, `$script:BaseManagedEnvKeys` |
|
|
| what refuses a switch | `linux/claude-mode` `cm_preflight` | `Set-ClaudeMode` guards |
|
|
| how a model list is fetched | `provider_catalogue` + a parser in `cm-json.py` | `Get-ProviderCatalogue` |
|
|
| a doctor check | `cmd_doctor`, `doctor_*` | `Invoke-Doctor`, `Test-PresetCatalogue`, `Test-OllamaContext` |
|
|
| setup | `cmd_setup`, `setup_server`, `setup_models` | — |
|
|
| session detection and repair | `cmd_sessions`, `cmd_repair_session`; `cm-json.py` `cmd_scan_sessions`, `cmd_repair_session` | — |
|
|
| key storage | `cm-vault.sh` | the DPAPI functions in `claude-mode.ps1` |
|
|
| the bar icon and state | `omarchy/smoido.claude-mode/BarWidget.qml` | — |
|
|
| the panel | `omarchy/smoido.claude-mode/Panel.qml` | — |
|