SLOPSHOPPER

dashboard

The context window's fill and the main loop's tokens/s in the status line

newstatus
v0.1.0no licenseupdated 2026-10-03Cohey0727/CodingAgentTools/claude/mods/dashboard
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · dashboard
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ dashboard: 97.4k / 200k 49%
README

CodingAgentTools

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, one configs.jsonc driving 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:

HeadingProviderEndpointAPIFlagship model
SubscriptionsDeepSeekhttps://api.deepseek.com/anthropicanthropicdeepseek-v4-pro
SubscriptionsLocal (llama.cpp)http://127.0.0.1:11301anthropicdefault
Subscriptionsgtr (llama.cpp behind Cloudflare)https://gtr-halogen.spaghetti-monster.comopenaidefault
Command CodeCommand Codehttps://api.commandcode.ai/provideropenai, and anthropic for Claudedeepseek/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-server on your own machine, which serves the Anthropic shape on /v1/messages. Here that server is LlamaGate (~/Workspace/LlamaGate): just start brings it up on 127.0.0.1:11301, just profiles lists the models it can load and just start <profile> swaps to one. There is no account and no key, so its API_KEY is a placeholder the CLIs merely require to be non-empty. Both llama.cpp providers use the fixed model id default: llama-server answers with whatever it has loaded and ignores the requested name, so swapping the model on the server needs no edit here. Keep context_window at or below the server's --ctx-size.

Note: gtr is a llama-server on another machine, published through a Cloudflare tunnel and gated by Cloudflare Access. Requests without Access credentials get a 302 to the login page, so its REQUEST_HEADERS carry an Access service token (CF-Access-Client-Id / CF-Access-Client-Secret), whose values come from .env. Like Local, its API_KEY is only a placeholder unless llama-server runs with --api-key. It answers the OpenAI shape; GTR9_API in .env switches 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/messages and 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 declares catalog and make update rewrites 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.

Layout

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.

Requirements

  • macOS / Linux with bash, make and python3 (bin/models.py reads the provider configs)
  • OpenCode v2 (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 (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 (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 (reasonix on your PATH) — optional: ``bash npm install -g reasonix # or brew install esengine/reasonix/reasonix ``
  • Codewhale (codewhale on your PATH) — optional: ``bash curl -fsSL https://codewhale.net/install.sh | sh ``
  • DeepSeek Harness (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 first
  • Command Code (cmd) — 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 (nginx) — only for make serve, and make setup installs or upgrades it with brew install nginx once SERVE_HOST or SERVE_OPENCODE_HOST is set

Every 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.

  • An API key for whichever provider(s) you use

Setup

make setup

One interactive wizard does everything:

  1. Check the providers whose token you want to set or change (arrows + Space, Enter to confirm). Providers that already have a token show ✅ and start unchecked, so Enter alone leaves every token as it is. The last two rows, dsh web and opencode web, are not providers: checking one asks for the hostname make serve serves it under
  2. Paste the API token of each checked provider — an empty answer keeps the existing token
  3. configs.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
  4. pi itself is updated, the pi packages that add /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 them
  5. Every provider whose key resolves is registered in the global config of every CLI — one generator each — with every model in configs.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 add
  6. You get a warning if any of the CLIs those configs are for is missing from your PATH
  7. Every skill, every subagent and AGENTS.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.

Make targets

TargetWhat it does
make setupBoth halves: the provider wizard, then the skill, AGENTS.md, OpenCode extension, pi extension and Claude Code mod install
make setup-providersThe wizard above only: tokens, .env upkeep, pi packages, DeepSeek Harness, Command Code, OpenCode plugins, and every global config
make setup-skillsThe 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 checkValidate configs.jsonc, then refuse any concrete name outside it (see CLAUDE.md). What the pre-commit hook runs
make hooksInstall the lefthook pre-commit hook that runs make check
make listEvery provider with its heading, endpoint and models with their tags, then every skill, subagent, OpenCode extension and pi extension with its install status
make updateFetch 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-globalRe-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-globalRe-generate OpenCode's global config from configs.jsonc — run it after editing it
make crush-globalRe-generate Crush's global ~/.config/crush/crushrc from configs.jsonc
make reasonix-globalRe-generate Reasonix's global ~/.reasonix/config.toml, and the keys it reads from ~/.reasonix/.env
make codewhale-globalRe-generate Codewhale's global ~/.codewhale/config.toml, keys included, written at 600
make dsh-globalRe-generate DeepSeek Harness's home patch ~/.dsh/cordis.patch.yml, and the keys it reads from ~/.dsh/.env
make serveRun 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 uninstallRemove 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 helpThe target list above, on the terminal

Usage

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.

dsh web and OpenCode through a Cloudflare Tunnel

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 onWhat
127.0.0.1:3081nginx — every route of the tunnel points here; it tells the apps apart by Host
127.0.0.1:3080dsh 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

Source 1 files
hooks/register.ts 71 lines
1import type { EngineInterface, Register, SessionContextUsage, TurnStepResult } from 'claude-code'
2
3// A reply this short is mostly latency; its rate would drag the figure down.
4const MIN_TOKENS_FOR_RATE = 24
5
6const trimZeros = (fixed: string) => fixed.replace(/\.?0+$/, '')
7
8const fmtTokens = (n: number) =>
9  n < 1000
10    ? `${Math.round(n)}`
11    : n < 1_000_000
12      ? `${trimZeros((n / 1000).toFixed(1))}k`
13      : `${trimZeros((n / 1_000_000).toFixed(2))}M`
14
15const fmtRate = (tps: number) => `${tps < 100 ? tps.toFixed(1) : Math.round(tps)}`
16
17// Module variables start over on a reload; session.start reads the window again.
18let context: SessionContextUsage | null = null
19let tps: number | null = null
20
21function showStatus($: EngineInterface) {
22  const parts: string[] = []
23  if (context?.tokens !== undefined && context.window > 0) {
24    const percent = context.percent ?? Math.round((context.tokens / context.window) * 100)
25    parts.push(`${fmtTokens(context.tokens)} / ${fmtTokens(context.window)} ${percent}%`)
26  }
27  if (tps !== null) parts.push(`${fmtRate(tps)} t/s`)
28  $.ui.status(parts.length > 0 ? parts.join(' ') : undefined)
29}
30
31export const register: Register = on => {
32  on('session.start', async ($, e, next) => {
33    context = (await $.session.usage().catch(() => null))?.context ?? null
34    showStatus($)
35    return next(e)
36  })
37
38  on('session.measure', async ($, e, next) => {
39    if (e.changed.includes('context')) {
40      context = e.context
41      showStatus($)
42    }
43    return next(e)
44  })
45
46  // Output tokens per second of the main loop's responses, from the first
47  // streamed piece of any kind to the stop, so a reply whose thinking streams
48  // no visible text is not counted as fast.
49  on('turn.step', async function* ($, e, next) {
50    const stream = next(e)
51    if (e.agentId !== undefined) {
52      yield* stream
53      return stream.result
54    }
55    let firstAt: number | null = null
56    let stopAt: number | null = null
57    for await (const chunk of stream) {
58      yield chunk
59      if (firstAt === null) firstAt = await $.clock.now()
60      if (chunk.kind === 'stop') stopAt = await $.clock.now()
61    }
62    const result: TurnStepResult = await stream.result
63    const output = result.usage?.output_tokens ?? 0
64    if (firstAt !== null && stopAt !== null && stopAt > firstAt && output >= MIN_TOKENS_FOR_RATE) {
65      tps = output / ((stopAt - firstAt) / 1000)
66      showStatus($)
67    }
68    return result
69  })
70}
71