feat(claude): active-project status line + /mcpctl switcher, and stop tests writing to ~/.claude

Claude Code had neither of the things opencode, pi and prime-agent all have: a
visible active project, and a way to change it from inside a session. It has no
plugin API that can draw a widget or open a picker, but it does run a command
for its status line and it does load slash commands — which is enough for both.

  - `mcpctl statusline` prints the active project (from .mcp.json, falling back
    to a .mcpctl-project marker) and is wired into settings.json. It reads the
    directory out of the JSON Claude Code pipes in, so it follows /cwd rather
    than reporting wherever the binary was launched. Prints nothing when no
    project is active: an empty line beats "none" on every unrelated repo.
  - `/mcpctl [project]` switches and reminds you to reconnect from /mcp.
    allowed-tools is scoped to the four exact mcpctl invocations it needs.

Three things found by running it rather than reasoning about it:

  - Claude Code REWRITES settings.json against its own schema and strips
    unknown keys from `statusLine` — our `_mcpctl_managed` marker came back
    gone, so ownership is now determined by the command string. (Hooks keep
    their marker; statusLine does not.) A composed line like
    `my-prompt && mcpctl statusline` is deliberately not claimed.
  - Every `!`-prefixed block in a slash command is permission-checked against
    allowed-tools. Omitting `statusline` failed the whole command before the
    model saw anything. A test now asserts every pre-executed command is
    covered.
  - Setting ANTHROPIC_AUTH_TOKEN *and* ANTHROPIC_API_KEY makes Claude Code warn
    that auth may not work; claude-vllm now sets only the former and clears an
    inherited API key.

Also fixes a pre-existing test-isolation bug this work would have made worse:
`config claude` wrote into the developer's real ~/.claude when the suite ran,
which is how an untagged duplicate of the skills-sync SessionStart hook got
there. Both the hook installer and the new UI installers now honour
CLAUDE_CONFIG_DIR (Claude Code's own override — correct behaviour first,
isolation second), `config claude` gains --claude-dir for parity with --pi-dir
and --opencode-dir, and the suite is verified to leave ~/.claude byte-identical.

Verified live: status line renders `mcpctl:homeautomation`, `/mcpctl docmost`
switches and the line updates to `mcpctl:docmost` in the same session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
This commit is contained in:
Michal
2026-08-09 19:06:06 +01:00
parent d7055a0953
commit b3a062ce28
10 changed files with 525 additions and 11 deletions

View File

@@ -139,10 +139,13 @@ fi
BASE="${BASE%/}"; BASE="${BASE%/v1}"
export ANTHROPIC_BASE_URL="$BASE"
# Both forms: the gateway accepts either, and which one Claude Code sends has
# changed between releases (Authorization: Bearer vs x-api-key).
# ANTHROPIC_AUTH_TOKEN only. Setting ANTHROPIC_API_KEY as well makes Claude Code
# warn that "auth may not work as expected" — it wants exactly one. AUTH_TOKEN
# is the right one for a third-party gateway (sent as `Authorization: Bearer`),
# and an inherited ANTHROPIC_API_KEY would otherwise take precedence over it, so
# clear it for the child process.
export ANTHROPIC_AUTH_TOKEN="$KEY"
export ANTHROPIC_API_KEY="$KEY"
unset ANTHROPIC_API_KEY
[ -n "$MODEL_ID" ] && export ANTHROPIC_MODEL="$MODEL_ID"
# Without a substitute, the background/summarisation calls ask the gateway for a
# real Haiku it does not serve, and every one of them 404s.
@@ -169,7 +172,8 @@ if [ "$PRINT_ENV" = 1 ]; then
printf 'export %s=%q\n' "$v" "${!v}"
done
# Never printed: the credential. Use --list to confirm one was found.
echo 'export ANTHROPIC_AUTH_TOKEN=<redacted> # and ANTHROPIC_API_KEY'
echo 'export ANTHROPIC_AUTH_TOKEN=<redacted>'
echo 'unset ANTHROPIC_API_KEY'
exit 0
fi