Names the session and its tmux window "#<number> <title>" once gh pr create opens a PR

Run OpenCode, pi, Crush, Reasonix, Codewhale and DeepSeek Harness on the LLM backends you pay for (DeepSeek · Command Code · your own llama.cpp) — one repo, one
make setup, oneconfigs.jsoncdriving all six CLIs, and the skills and global instruction file they share.
One repo that generates the global config of six CLIs covering every provider — OpenCode, the pi coding agent, Crush, Reasonix, Codewhale and DeepSeek Harness:
| Heading | Provider | Endpoint | API | Flagship model |
|---|---|---|---|---|
| Subscriptions | DeepSeek | https://api.deepseek.com/anthropic | anthropic | deepseek-v4-pro |
| Subscriptions | Local (llama.cpp) | http://127.0.0.1:11301 | anthropic | default |
| Subscriptions | gtr (llama.cpp behind Cloudflare) | https://gtr-halogen.spaghetti-monster.com | openai | default |
| Command Code | Command Code | https://api.commandcode.ai/provider | openai, and anthropic for Claude | deepseek/deepseek-v4.1-flash |
make setup writes every provider into each CLI's global config, so a bare opencode gets them all under /models, a bare pi under /model, crush, reasonix and codewhale each start with the whole set, and so does dsh web — dsh has no default profile, so a bare dsh only answers --profile <name> is required.
There is no proxy or translation layer: each CLI talks to each endpoint in the API it already speaks — Anthropic Messages (/v1/messages) or OpenAI Chat Completions (/v1/chat/completions), chosen per model. Every model any of them can reach is declared in one place: configs.jsonc at the repo root, in git, grouped by the heading it sits under in OpenCode's model dialog, with tags naming the model each CLI starts on. It holds no secret — an API key is written there as ${DEEPSEEK_API_KEY} and read from the single gitignored .env beside it.
Note: Local is not a hosted service — it points at a
llama-serveron your own machine, which serves the Anthropic shape on/v1/messages. Here that server is LlamaGate (~/Workspace/LlamaGate):just startbrings it up on127.0.0.1:11301,just profileslists the models it can load andjust start <profile>swaps to one. There is no account and no key, so itsAPI_KEYis a placeholder the CLIs merely require to be non-empty. Both llama.cpp providers use the fixed model iddefault: llama-server answers with whatever it has loaded and ignores the requested name, so swapping the model on the server needs no edit here. Keepcontext_windowat or below the server's--ctx-size.
Note: gtr is a
llama-serveron another machine, published through a Cloudflare tunnel and gated by Cloudflare Access. Requests without Access credentials get a 302 to the login page, so itsREQUEST_HEADERScarry an Access service token (CF-Access-Client-Id/CF-Access-Client-Secret), whose values come from.env. Like Local, itsAPI_KEYis only a placeholder unlessllama-serverruns with--api-key. It answers the OpenAI shape;GTR9_APIin.envswitches the CLIs to the Anthropic one if it is ever put behind such a route.
Note: Command Code's Provider API serves Claude only on
/v1/messagesand every other model only on/v1/chat/completions, answering 400 on the wrong one — so its Claude models carry"api": "anthropic"and the rest follow the provider's"api": "openai". It needs a paid plan: the Go plan has no API access. Its model list is not maintained by hand: the provider declarescatalogandmake updaterewrites it from the Provider API's own list.
The other half of the repo is what those CLIs run with: the skills under skills/ and the single global instruction file AGENTS.md, symlinked into every CLI's config directory by the same make setup — see Skills and global instructions. The Command Code CLI (cmd) takes only this half: make setup installs it and links it the skills and AGENTS.md, but writes it no provider.
AGENTS.md # the one global instruction file, linked into every CLI
skills/<name>/SKILL.md # a skill, linked into ~/.claude/skills and ~/.agents/skills
agents/<name>.md # a subagent (none yet), linked into ~/.claude/agents and ~/.agents/agents
opencode/command/<name>.md # an OpenCode slash command (none yet), linked into ~/.config/opencode/command
opencode/plugin/<name>.js # an OpenCode plugin, linked into ~/.config/opencode/plugin
pi/extensions/<name>.ts # a pi extension, linked into ~/.pi/agent/extensions
pi/agents/<name>.md # a pi subagent definition, linked into ~/.pi/agent/agents
claude/mods/<name>/ # a Claude Code mod (function hooks), loaded through CLAUDE_CODE_PLUGIN_DIRS in ~/.claude/settings.json
configs.jsonc # every provider by heading: endpoint, API, models, tags, ${VAR} references (in git)
.env # the values those references point at (gitignored, chmod 600)
.env.example # the same variables, empty (in git)
bin/ui.sh # banner, colors and the output helpers every script shares
bin/models.py # reads configs.jsonc for every other script: validates it, resolves tags and routes
bin/common.sh # shared resolution: configs.jsonc through models.py, values from .env
bin/style-check.sh # refuse any name configs.jsonc owns from appearing anywhere else
bin/model-ref.sh # "<route id>/<model>" for one provider, so nothing else spells a model id
bin/models-update.sh # fetches the live catalog of every provider that names one (`make update`)
bin/models-update.py # rewrites that provider's models in configs.jsonc from it
bin/setup.sh # provider wizard: pick providers, paste tokens, install (`make setup-providers`)
bin/pi-global-models.sh # registers every provider in pi's global models.json (`make pi-global`)
bin/opencode-global-config.sh # registers every provider in OpenCode's global config (`make opencode-global`)
bin/crush-global-config.sh # registers every provider in Crush's global crushrc (`make crush-global`)
bin/reasonix-global-config.sh # registers every provider in Reasonix's global config.toml (`make reasonix-global`)
bin/codewhale-global-config.sh # registers every provider in Codewhale's global config.toml (`make codewhale-global`)
bin/dsh-global-config.sh # registers every provider in DeepSeek Harness's home patch (`make dsh-global`)
bin/serve.sh # runs dsh web and OpenCode behind nginx for their hostnames (`make serve`)
bin/serve.nginx.conf # that nginx's config; each start fills it in for the run
bin/serve.dsh.nginx.conf # its server for dsh web, handing out dsh's startup token
bin/serve.opencode.nginx.conf # its server for OpenCode, sending OpenCode's server password
bin/skills-common.sh # where skills, subagents, AGENTS.md and the OpenCode and pi extensions are installed
bin/skills-setup.sh # links them there (`make setup-skills`)
bin/removed-skills.txt # skills no longer shipped; setup deletes their old links
bin/skills-list.sh # their install status (part of `make list`)
bin/skills-uninstall.sh # removes only the symlinks pointing back here and the claude/mods entry (part of `make uninstall`)
bin/list.sh # everything this repo manages (`make list`)
bin/help.sh # target overview (`make help`)
docs/migrations/ # upgrade notes for existing checkouts
Makefile # setup / setup-providers / setup-skills / list / uninstall / <agent>-global / serve / help
Adding a provider is a new entry in configs.jsonc plus its key in .env; adding a skill is a new skills/<name>/SKILL.md and a make setup-skills. An OpenCode slash command is a new opencode/command/<name>.md, and a plugin a new opencode/plugin/<name>.js default-exporting { id, setup } — same make setup-skills. So is a pi extension, a new pi/extensions/<name>.ts, and a pi subagent, a new pi/agents/<name>.md.
bash, make and python3 (bin/models.py reads the provider configs)opencode on your PATH) — only OpenCode itself; this repo writes its config. make setup installs or upgrades it with the v2 installer: ``bash curl -fsSL https://opencode.ai/v2/install | bash ``pi on your PATH) — only pi itself; like OpenCode it gets the generated models.json. Not bundled by this repo either: ``bash curl -fsSL https://pi.dev/install.sh | sh ` (the older @mariozechner/pi-coding-agent` package is deprecated and resolves environment references differently)crush on your PATH) — optional, and like the two above it gets a generated config: ``bash brew install charmbracelet/tap/crush # macOS (Homebrew) # or npm install -g @charmland/crush ``reasonix on your PATH) — optional: ``bash npm install -g reasonix # or brew install esengine/reasonix/reasonix ``codewhale on your PATH) — optional: ``bash curl -fsSL https://codewhale.net/install.sh | sh ``dsh) — one of the two CLIs make setup installs itself, with npm install -g @deepseek-ai/dsh@latest, upgrading a dsh already on your PATH. That needs npm and Node.js ^22.19.0 || >=24.0.0. It is a developer preview: read its safety notice firstcmd) — the other one, installed with npm install -g command-code@latest, upgrading a cmd already there. That needs npm and Node.js >=22. It gets no generated config, only the skills and AGENTS.md, and it updates itself in the background (cmd update does it on demand)nginx) — only for make serve, and make setup installs or upgrades it with brew install nginx once SERVE_HOST or SERVE_OPENCODE_HOST is setEvery one of these is optional. A generator writes its config whether or not the CLI is installed, and make setup says which of them it could not find on your PATH.
make setup
One interactive wizard does everything:
dsh web and opencode web, are not providers: checking one asks for the hostname make serve serves it underconfigs.jsonc is validated before anything is written; .env is created from .env.example if missing (chmod 600), gets any variables added to .env.example since, and picks up keys still sitting in the old providers/<name>/.env files/loop, /goal, MCP and subagents are installed into pi's user settings (~/.pi/agent/settings.json), and pi's model catalogs are refreshed, and DeepSeek Harness and Command Code are installed with npm install -g <package>@latest — and nginx with brew install nginx once SERVE_HOST or SERVE_OPENCODE_HOST is set, for make serve. Anything of these already installed is upgraded to its latest version, so re-running make setup is also how you update themconfigs.jsonc, not just the tagged ones, and all of them starting on the default provider. pi also gets the keys OpenCode's /connect holds, for Zen and Go. OpenCode itself is installed or upgraded with its v2 installer, and the OpenCode plugins with opencode plugin addAGENTS.md are symlinked into the places each CLI reads them from, and OpenCode gets this repo's slash commands and plugins — /loop and /goal among them — in ~/.config/opencode, and pi gets its dashboard in ~/.pi/agent/extensions (details below)To rotate a token, pick up new settings or add a provider later, just re-run make setup. make setup-providers and make setup-skills each run one half on its own; only the provider half prompts.
Coming from an older checkout? docs/migrations/ has the per-variable mapping — see 2026-08-15 — pi 対応と .env の共通設定化, 2026-08-15 — GLM-5.3, 2026-08-24 — OpenCode ランチャー廃止とグローバル設定生成, 2026-09-03 — pi ランチャー廃止とグローバル models.json 生成, 2026-09-05 — OpenCode の lean エージェント, 2026-09-08 — claude-code-settings の統合, 2026-09-08 — OpenCode の /goal, 2026-09-09 — providers/ 廃止と configs.jsonc への集約, 2026-09-11 — OpenCode の Subscriptions 見出し, 2026-09-13 — OpenCode の /loop, 2026-09-13 — Claude Code ランチャー廃止と見出し・API 別の configs.jsonc, 2026-09-18 — pi のダッシュボード拡張, 2026-09-18 — pi から OpenCode Zen / Go を使う, 2026-09-18 — pi の MCP・サブエージェントと起動モデル, 2026-09-18 — pi の provider id を label に揃える, 2026-09-27 — OpenCode v2 移行, and 2026-10-03 — pi 1.0.
| Target | What it does |
|---|---|
make setup | Both halves: the provider wizard, then the skill, AGENTS.md, OpenCode extension, pi extension and Claude Code mod install |
make setup-providers | The wizard above only: tokens, .env upkeep, pi packages, DeepSeek Harness, Command Code, OpenCode plugins, and every global config |
make setup-skills | The shared assets only: skills/, agents/, AGENTS.md, opencode/ and pi/ into every agent CLI, and claude/mods/ into Claude Code's CLAUDE_CODE_PLUGIN_DIRS |
make check | Validate configs.jsonc, then refuse any concrete name outside it (see CLAUDE.md). What the pre-commit hook runs |
make hooks | Install the lefthook pre-commit hook that runs make check |
make list | Every provider with its heading, endpoint and models with their tags, then every skill, subagent, OpenCode extension and pi extension with its install status |
make update | Fetch the live model catalog (catalog) of every provider that names one and rewrite its models in configs.jsonc — ids, context windows and per-model API follow the catalog, tags stay hand-written — then regenerate every global config. One command brings everything up to date |
make pi-global | Re-generate pi's global ~/.pi/agent/models.json from configs.jsonc, set the startup model in ~/.pi/agent/settings.json, and link the keys OpenCode's /connect holds into ~/.pi/agent/auth.json — run it after changing a model or endpoint, or connecting a provider in OpenCode |
make opencode-global | Re-generate OpenCode's global config from configs.jsonc — run it after editing it |
make crush-global | Re-generate Crush's global ~/.config/crush/crushrc from configs.jsonc |
make reasonix-global | Re-generate Reasonix's global ~/.reasonix/config.toml, and the keys it reads from ~/.reasonix/.env |
make codewhale-global | Re-generate Codewhale's global ~/.codewhale/config.toml, keys included, written at 600 |
make dsh-global | Re-generate DeepSeek Harness's home patch ~/.dsh/cordis.patch.yml, and the keys it reads from ~/.dsh/.env |
make serve | Run dsh web and OpenCode behind nginx for the hostnames SERVE_HOST and SERVE_OPENCODE_HOST name, so each opens there without a login of its own — see dsh web and OpenCode through a Cloudflare Tunnel. Each starts only when its hostname is set |
make uninstall | Remove the packages each agent lists, every global config this repo generated and the token files beside them, the symlinks pointing back into this repo (pi extensions included), the plugin shims generated from it, the claude/mods entry in Claude Code's CLAUDE_CODE_PLUGIN_DIRS, and the entries in pi's auth.json that read OpenCode's keys. The .env is left alone |
make help | The target list above, on the terminal |
opencode # OpenCode — every configured provider is in /models
pi # pi — every configured provider is in /model
crush # Crush — every configured provider is in its model picker
reasonix # Reasonix — every configured provider is in /model
codewhale # Codewhale — every configured provider is in its model picker
dsh web # DeepSeek Harness — every configured provider is in the Web UI's model picker
make setup (and make pi-global) write every provider that has a token into ~/.pi/agent/models.json, so a bare pi has all of them and /model switches mid-session. Each is filed under its label — the name OpenCode's model dialog puts before its models — or its name when it has none:
pi # starts on the model pi.overrides names
pi --model DeepSeek/deepseek-v4-pro # or pick at launch time
pi --model commandcode/claude-opus-5
pi also has OpenCode Zen and OpenCode Go built in, as opencode and opencode-go. They are not providers in configs.jsonc: you connect them once with /connect in OpenCode, and make setup (and make pi-global) gives pi an entry in ~/.pi/agent/auth.json for every key OpenCode stored that pi has a provider for. The entry is a command reading the key back out of OpenCode's ~/.local/share/opencode/auth.json, so no key is copied and one rotated in OpenCode needs no re-run. An entry you made yourself with pi's /login is left in place, and OAuth logins are not shared: pi refreshes those through its own /login. make setup also refreshes pi's model catalogs, so the Zen and Go lists include models those services added after the installed pi was released.
pi --model opencode-go/deepseek-v4.1-flash # OpenCode Go, on the key OpenCode's /connect stored
pi --list-models opencode # what Zen and Go serve
The two generators otherwise start you on the default provider; pi.overrides and opencode.overrides in configs.jsonc can point them elsewhere. See Default provider. For pi that means defaultProvider / defaultModel in ~/.pi/agent/settings.json, the two keys Ctrl+S in /model writes — so a re-run replaces a pick you saved there. The rest of that file is left as it is. Writing them needs python3; without it the two keys are skipped and pi starts wherever it was.
make setup (and make opencode-global) write every provider that has a token into the global ~/.config/opencode/opencode.json, so a bare opencode starts with all of them and /models switches mid-session — each under the heading configs.jsonc files it under, beside OpenCode's own Zen and Go (see OpenCode's model dialog):
opencode # starts on the model opencode.overrides names
opencode --model deepseek-anthropic/deepseek-v4-pro # or pick at launch time
opencode --model commandcode-openai/deepseek/deepseek-v4.1-flash
Note small_model — the model OpenCode names a session with, and its only use for one — stays at the one opencode.overrides sets even after you switch the main model via /models.
Crush, Reasonix, Codewhale and DeepSeek Harness work the same way — one generated global config each:
crush # starts on the large slot: the default provider's model
reasonix # starts on default_model in ~/.reasonix/config.toml
codewhale # starts on default_text_model in ~/.codewhale/config.toml
dsh web # starts on agent-default-model in ~/.dsh/cordis.patch.yml
dsh --profile headless "run the tests" # the same providers, one task, no browser
DeepSeek Harness has no terminal UI of its own: dsh web serves one on http://127.0.0.1:3080, and --profile headless answers one task and exits. A model picked or a provider edited in the Web UI is saved to ~/.dsh/settings.yaml, which dsh merges over the generated patch — so it survives make dsh-global, and once a model is saved there a re-run no longer changes the one dsh starts on.
Crush has two model slots rather than a free choice per session — large is what the interactive agent runs on, small what it delegates cheap work to — and the generated crushrc points them at the default provider's default and small models. Naming a model it does not know is not an error there: Crush silently falls back to a model of its own choosing and writes that correction back to disk, so a hand-edit that misspells one is easy to miss.
make serve runs dsh web and OpenCode's Web UI for a browser elsewhere, reached through a Cloudflare Tunnel and signed in to by Cloudflare Access alone — no dsh token, no OpenCode password. SERVE_HOST and SERVE_OPENCODE_HOST name the tunnel's public hostnames — set them by checking dsh web and opencode web in make setup, in .env, or in the environment (SERVE_HOST=dsh.example.com make serve). Each app starts only when its hostname is set; with neither, nothing starts.
make serve
# dsh web: http://127.0.0.1:3080/?token=… ← on this machine
# dsh web: https://dsh.example.com/ — through the tunnel, no token needed
# opencode: https://oc.example.com/ — through the tunnel, no password needed
| Listens on | What |
|---|---|
127.0.0.1:3081 | nginx — every route of the tunnel points here; it tells the apps apart by Host |
127.0.0.1:3080 | dsh web, which also accepts SERVE_HOST besides loopback as the Host and Origin of a request (--trusted-host) |
| 127.0.0.1:3082 | opencode serve, a server of its own beside OpenCode's
hooks/register.ts 37 lines1import type { Register } from 'claude-code'
2
3const PR_CREATE = /\bgh\s+pr\s+create\b/
4// gh pr create prints the new PR's URL on stdout.
5const PR_URL = /https:\/\/\S+\/pull\/\d+/
6
7// A tool.call hook holds its turn, so $.command.run refuses to run /rename
8// from there; the name waits for the main loop's turn to end.
9let pendingName: string | null = null
10
11export const register: Register = on => {
12 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
13 const ran = await next(e)
14 if (ran.deny !== undefined || ran.isError === true || !PR_CREATE.test(e.command)) return ran
15 const url = ran.result.stdout.match(PR_URL)?.[0]
16 if (url === undefined) return ran
17
18 const view = await $.process.run(['gh', 'pr', 'view', url, '--json', 'number,title', '--jq', '"#\\(.number) \\(.title)"'])
19 const name = view.stdout.trim()
20 if (view.exitCode !== 0 || name === '') return ran
21
22 pendingName = name
23 const pane = await $.env.get('TMUX_PANE')
24 if (pane !== undefined) await $.process.run(['tmux', 'rename-window', '-t', pane, name])
25 return ran
26 })
27
28 on('turn.complete', async ($, e, next) => {
29 if (e.agentId === undefined && pendingName !== null) {
30 const name = pendingName
31 pendingName = null
32 $.command.run({ command: 'rename', args: name }).catch((err: unknown) => $.ui.toast(`pr-title: /rename failed: ${String(err)}`))
33 }
34 return next(e)
35 })
36}
37