Skip to content

Commit 3f8b435

Browse files
authored
docs: split the README into a docs/ directory with a front-door README (#664)
The README had grown to 1,100 lines; a third of it was per-agent setup any given reader needs one section of. Content moves verbatim into focused pages: - docs/agents.md - all agent setups + Claude Design MCP + adapter instances - docs/configuration.md - env vars, endpoints, API key auth, SDK toggles, passthrough, CLI - docs/profiles.md - multi-profile, headless login, sticky routing - docs/deployment.md - NixOS/flake, Home Manager service, Docker - docs/plugins.md - plugin system + official scrub plugins - docs/development.md - architecture overview, testing, programmatic API The README keeps the banner, philosophy note, quick start, features, tested-agents table, FAQ (externally linked from issues), and a documentation index. Cross-file links rewritten and verified by an anchor-integrity check; the landing page's agent-guide link now points at docs/agents.md.
1 parent ce5e520 commit 3f8b435

8 files changed

Lines changed: 1001 additions & 969 deletions

File tree

README.md

Lines changed: 25 additions & 968 deletions
Large diffs are not rendered by default.

docs/agents.md

Lines changed: 347 additions & 0 deletions
Large diffs are not rendered by default.

docs/configuration.md

Lines changed: 190 additions & 0 deletions
Large diffs are not rendered by default.

docs/deployment.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Deployment
2+
3+
[← Back to README](../README.md)
4+
5+
This page covers installing and running Meridian beyond `npm install -g`: NixOS/Nix flakes, the Home Manager service, and Docker.
6+
7+
## NixOS / Nix Flake
8+
Meridian provides a Nix flake for declarative installation.
9+
10+
**Add to your flake inputs:**
11+
12+
```nix
13+
{
14+
inputs.meridian.url = "github:rynfar/meridian";
15+
}
16+
```
17+
18+
**Install the package** (via overlay or directly):
19+
20+
```nix
21+
# Option A: overlay
22+
nixpkgs.overlays = [ meridian.overlays.default ];
23+
environment.systemPackages = [ pkgs.meridian ];
24+
25+
# Option B: direct reference
26+
environment.systemPackages = [ meridian.packages.${system}.meridian ];
27+
```
28+
29+
**OpenCode plugin** -- the plugin file is included at `${pkgs.meridian}/lib/meridian/plugin/meridian.ts`. Since this path lives in the Nix store, you need to make it available to OpenCode:
30+
31+
If you generate your OpenCode config from Nix (e.g. via Home Manager), interpolate the path directly:
32+
33+
```nix
34+
# home-manager example
35+
xdg.configFile."opencode/opencode.json".text = builtins.toJSON {
36+
plugin = [ "${pkgs.meridian}/lib/meridian/plugin/meridian.ts" ];
37+
};
38+
```
39+
40+
If you don't manage your OpenCode config through Nix, symlink the plugin to a stable path and reference that instead:
41+
42+
```nix
43+
# configuration.nix or home-manager
44+
environment.etc."meridian/plugin/meridian.ts".source =
45+
"${pkgs.meridian}/lib/meridian/plugin/meridian.ts";
46+
```
47+
48+
Then in `~/.config/opencode/opencode.json`:
49+
50+
```json
51+
{ "plugin": ["/etc/meridian/plugin/meridian.ts"] }
52+
```
53+
54+
> **Important:** Do not use `meridian setup` on NixOS. It writes an absolute Nix store path (e.g. `/nix/store/...-meridian-1.x.x/lib/...`) into your OpenCode config, which will break on the next `nixos-rebuild switch` or `home-manager switch` when the store path changes. Use one of the approaches above instead.
55+
56+
> **Note:** Meridian's package depends on the unfree `claude-code` from nixpkgs instead of bundling its own binary. The flake accepts the unfree license when it builds the package and exports the finished derivation, so consuming it through the overlay or `packages.<system>.meridian` does not re-run nixpkgs' unfree check and needs no `allowUnfree` setting.
57+
58+
**Home Manager service** -- run Meridian as a user systemd service:
59+
60+
```nix
61+
# flake.nix
62+
{
63+
inputs.meridian.url = "github:rynfar/meridian";
64+
}
65+
66+
# home-manager config
67+
{
68+
imports = [ meridian.homeModules.default ];
69+
70+
services.meridian = {
71+
enable = true;
72+
settings = {
73+
port = 3456;
74+
host = "127.0.0.1";
75+
# passthrough = true;
76+
# defaultAgent = "opencode";
77+
# sonnetModel = "sonnet";
78+
# Load plugins from the Nix store (rendered to a plugins.json manifest).
79+
# The official scrub plugins ship prebuilt via the meridian overlay:
80+
# pluginConfig = [ { path = pkgs.meridianPlugins.opencode-scrub.path; } ];
81+
# pluginDir = "/path/to/extra/plugins";
82+
};
83+
# Extra env vars not covered by settings
84+
# environment = {
85+
# MERIDIAN_MAX_CONCURRENT = "20";
86+
# };
87+
};
88+
}
89+
```
90+
91+
The service starts automatically on login. Manage it with `systemctl --user {start,stop,restart,status} meridian`.
92+
93+
The module manages only the systemd user service — it does **not** put the `meridian` CLI on your `$PATH`. If you also want to run `meridian` from a shell, add the package yourself:
94+
95+
```nix
96+
home.packages = [ config.services.meridian.package ];
97+
```
98+
99+
The plugin path is also available as `config.services.meridian.opencode.pluginPath` for use in your OpenCode config:
100+
101+
```nix
102+
xdg.configFile."opencode/opencode.json".text = builtins.toJSON {
103+
plugin = [ config.services.meridian.opencode.pluginPath ];
104+
};
105+
```
106+
## Docker
107+
108+
Claude Code authentication requires a browser, which isn't available inside containers. Authenticate on your local machine first, then mount the credentials into Docker.
109+
110+
### Single account
111+
112+
```bash
113+
# 1. Authenticate locally (one time)
114+
claude login
115+
116+
# 2. Run with mounted credentials
117+
docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian
118+
```
119+
120+
Meridian refreshes OAuth tokens automatically — once the credentials are mounted, no further browser access is needed.
121+
122+
> **macOS hosts:** mounting `~/.claude` does **not** carry credentials into the container — on macOS the CLI stores OAuth tokens in the Keychain, not in files, so the container sees an empty credential store and requests fail with an authentication error. Use an [OAuth-token profile](#oauth-token-profiles-in-docker-no-volume-mount) instead (recommended), or run `claude login` once inside the container (`docker exec -it <name> claude login`).
123+
124+
### Multiple profiles in Docker
125+
126+
Authenticate each profile locally, then pass them to Docker via the `MERIDIAN_PROFILES` environment variable:
127+
128+
```bash
129+
# 1. Authenticate each account locally
130+
meridian profile add personal
131+
meridian profile add work # sign out of claude.ai first, sign into work account
132+
133+
# 2. Run Docker with profile configs pointing to mounted credential directories
134+
docker run \
135+
-v ~/.config/meridian/profiles/personal:/profiles/personal \
136+
-v ~/.config/meridian/profiles/work:/profiles/work \
137+
-e 'MERIDIAN_PROFILES=[{"id":"personal","claudeConfigDir":"/profiles/personal"},{"id":"work","claudeConfigDir":"/profiles/work"}]' \
138+
-e MERIDIAN_DEFAULT_PROFILE=personal \
139+
-p 3456:3456 meridian
140+
```
141+
142+
Switch profiles at runtime via the `x-meridian-profile` header or `meridian profile switch` (see [Multi-Profile Support](profiles.md)).
143+
144+
### OAuth-token profiles in Docker (no volume mount)
145+
146+
If you'd rather not mount a credential directory, generate a long-lived OAuth token on the host with `claude setup-token` and pass it as a profile. There's nothing to mount — the token alone is the credential:
147+
148+
```bash
149+
docker run \
150+
-e 'MERIDIAN_PROFILES=[{"id":"ci","oauthToken":"sk-ant-oat01-..."}]' \
151+
-e MERIDIAN_DEFAULT_PROFILE=ci \
152+
-p 3456:3456 meridian
153+
```
154+
155+
This is the recommended path for CI runners, ephemeral containers, and cross-host deployments where browser-based login isn't reachable. Treat the token like any other secret — inject it via your platform's secret store rather than committing it to your image or compose file.

docs/development.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Development
2+
3+
[← Back to README](../README.md)
4+
5+
Architecture overview, testing, and the programmatic API. See [`ARCHITECTURE.md`](../ARCHITECTURE.md) for the authoritative module map and dependency rules, and [`CLAUDE.md`](../CLAUDE.md) for coding guidelines.
6+
7+
## Architecture
8+
```
9+
src/proxy/
10+
├── server.ts ← HTTP orchestration (routes, SSE streaming, concurrency)
11+
├── adapter.ts ← AgentAdapter interface
12+
├── adapters/
13+
│ ├── detect.ts ← Agent detection from request headers
14+
│ ├── opencode.ts ← OpenCode adapter
15+
│ ├── forgecode.ts ← ForgeCode adapter
16+
│ ├── crush.ts ← Crush adapter
17+
│ ├── droid.ts ← Droid adapter
18+
│ ├── pi.ts ← Pi adapter
19+
│ ├── cherry.ts ← Cherry Studio adapter (internal mode + web search)
20+
│ ├── claudecode.ts ← Claude Code adapter (remote clients sharing a Max host)
21+
│ ├── openai.ts ← OpenAI-endpoint adapter (/v1/chat/completions)
22+
│ ├── codex.ts ← Codex CLI adapter (/v1/responses, forced passthrough)
23+
│ └── passthrough.ts ← LiteLLM passthrough adapter
24+
├── query.ts ← SDK query options builder
25+
├── errors.ts ← Error classification
26+
├── models.ts ← Model mapping (sonnet/opus/haiku, agentMode)
27+
├── tokenRefresh.ts ← Cross-platform OAuth token refresh
28+
├── openai.ts ← OpenAI ↔ Anthropic format translation (pure)
29+
├── openaiResponses.ts ← OpenAI Responses API ↔ Anthropic translation (pure)
30+
├── setup.ts ← OpenCode plugin configuration
31+
├── session/
32+
│ ├── lineage.ts ← Per-message hashing, mutation classification (pure)
33+
│ ├── fingerprint.ts ← Conversation fingerprinting
34+
│ └── cache.ts ← LRU session caches
35+
├── profiles.ts ← Multi-profile: resolve, list, switch auth contexts
36+
├── profileCli.ts ← CLI commands for profile management
37+
├── sessionStore.ts ← Cross-proxy file-based session persistence
38+
└── passthroughTools.ts ← Tool forwarding mode
39+
telemetry/
40+
├── ...
41+
├── profileBar.ts ← Shared site header (brand, nav, status, active profile)
42+
└── profilePage.ts ← Profile management page
43+
plugin/
44+
└── meridian.ts ← OpenCode plugin (session headers + agent mode)
45+
```
46+
47+
### Session Management
48+
49+
Every incoming request is classified:
50+
51+
| Classification | What Happened | Action |
52+
|---------------|---------------|--------|
53+
| **Continuation** | New messages appended | Resume SDK session |
54+
| **Compaction** | Agent summarized old messages | Resume (suffix preserved) |
55+
| **Undo** | User rolled back messages | Fork at rollback point |
56+
| **Diverged** | Completely different conversation | Start fresh |
57+
58+
Sessions are stored in-memory (LRU) and persisted to `~/.cache/meridian/sessions.json` for cross-proxy resume.
59+
60+
### Agent Detection
61+
62+
Agents are identified from request headers automatically:
63+
64+
| Signal | Adapter |
65+
|---|---|
66+
| `x-meridian-agent` header | Explicit override (any adapter) |
67+
| `x-opencode-session` or `x-session-affinity` header | OpenCode |
68+
| `opencode/` User-Agent | OpenCode |
69+
| `factory-cli/` User-Agent | Droid |
70+
| `Charm-Crush/` User-Agent | Crush |
71+
| `claude-cli/` User-Agent | Claude Code (unless `MERIDIAN_DEFAULT_AGENT` overrides — Pi mimics this UA) |
72+
| `litellm/` UA or `x-litellm-*` headers | LiteLLM passthrough |
73+
| *(anything else)* | `MERIDIAN_DEFAULT_AGENT` env var, or OpenCode |
74+
75+
### Adding a New Agent
76+
77+
Implement the `AgentAdapter` interface in `src/proxy/adapters/`. See [`adapters/opencode.ts`](../src/proxy/adapters/opencode.ts) for a reference.
78+
79+
## Testing
80+
81+
```bash
82+
npm test # unit + integration tests
83+
npm run build # build with bun + tsc
84+
```
85+
86+
| Tier | What | Speed |
87+
|------|------|-------|
88+
| Unit | Pure functions, no mocks | Fast |
89+
| Integration | HTTP layer with mocked SDK | Fast |
90+
| E2E | Real proxy + real Claude Max ([`E2E.md`](../E2E.md)) | Manual |
91+
92+
## Programmatic API
93+
94+
```typescript
95+
import { startProxyServer } from "@rynfar/meridian"
96+
97+
const instance = await startProxyServer({
98+
port: 3456,
99+
host: "127.0.0.1",
100+
silent: true,
101+
})
102+
103+
// instance.server — underlying http.Server
104+
await instance.close()
105+
```

docs/plugins.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Plugins
2+
3+
[← Back to README](../README.md)
4+
5+
Extend Meridian's behavior with composable plugins — no core modifications needed.
6+
7+
**Quick start:** Drop a `.ts` or `.js` file in `~/.config/meridian/plugins/` and restart.
8+
9+
```ts
10+
// ~/.config/meridian/plugins/my-plugin.ts
11+
export default {
12+
name: "my-plugin",
13+
onRequest(ctx) {
14+
// modify request context
15+
return { ...ctx, systemContext: ctx.systemContext + "\nBe concise." }
16+
},
17+
}
18+
```
19+
20+
- **Manage plugins** at `http://localhost:3456/plugins`
21+
- **Reload without restart:** `POST /plugins/reload`
22+
- **Full guide:** See [PLUGINS.md](../PLUGINS.md)
23+
24+
### Official plugins
25+
26+
Content-scoped scrubbers maintained alongside Meridian. Core stays a clean
27+
proxy — anything that rewrites client prompt content ships as one of these
28+
opt-in plugins instead:
29+
30+
| Plugin | What it does |
31+
|--------|--------------|
32+
| [`@rynfar/meridian-plugin-hermes-scrub`](https://github.com/rynfar/meridian-plugin-hermes-scrub) | Strips Hermes Agent's `# Finishing the job` harness block from the system prompt. Fixes empty-stream responses when proxying Hermes, and avoids its coding-harness fingerprint. |
33+
| [`@rynfar/meridian-plugin-pi-scrub`](https://github.com/rynfar/meridian-plugin-pi-scrub) | Strips Pi's coding-agent-harness prompt line that Anthropic meters as Extra Usage. |
34+
| [`@rynfar/meridian-plugin-opencode-scrub`](https://github.com/rynfar/meridian-plugin-opencode-scrub) | Strips OpenCode harness boilerplate from the system prompt before it reaches Claude. |
35+
36+
**Nix users:** the flake packages all three prebuilt — `pkgs.meridianPlugins.<name>` via the `meridian` overlay (or `meridian.legacyPackages.${system}.meridianPlugins`), each exposing `.path` for a `plugins.json` entry or the home-manager `pluginConfig` option. Pins are refreshed by a scheduled workflow that rebuilds every plugin before bumping.
37+
38+
Everyone else: install into Meridian's config dir and register the built file in
39+
`~/.config/meridian/plugins.json`:
40+
41+
```bash
42+
cd ~/.config/meridian
43+
npm install @rynfar/meridian-plugin-hermes-scrub
44+
```
45+
46+
```json
47+
{
48+
"plugins": [
49+
{ "path": "/Users/you/.config/meridian/node_modules/@rynfar/meridian-plugin-hermes-scrub/dist/index.js", "enabled": true }
50+
]
51+
}
52+
```
53+
54+
Paths must be absolute — the loader does not expand `~`.
55+
56+
Both plugin locations are configurable for the standalone CLI: `MERIDIAN_PLUGIN_DIR` overrides the auto-discovery directory and `MERIDIAN_PLUGIN_CONFIG` the manifest path (useful for Nix, containers, or running several instances with different plugin sets).

0 commit comments

Comments
 (0)