diff --git a/.claude/hooks/rtk-rewrite.sh b/.claude/hooks/rtk-rewrite.sh new file mode 100755 index 0000000..f7a42b5 --- /dev/null +++ b/.claude/hooks/rtk-rewrite.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# rtk-hook-version: 3 +# RTK Claude Code hook — rewrites commands to use rtk for token savings. +# Requires: rtk >= 0.23.0, jq +# +# This is a thin delegating hook: all rewrite logic lives in `rtk rewrite`, +# which is the single source of truth (src/discover/registry.rs). +# To add or change rewrite rules, edit the Rust registry — not this file. +# +# Exit code protocol for `rtk rewrite`: +# 0 + stdout Rewrite found, no deny/ask rule matched → auto-allow +# 1 No RTK equivalent → pass through unchanged +# 2 Deny rule matched → pass through (Claude Code native deny handles it) +# 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user + +if ! command -v jq &>/dev/null; then + echo "[rtk] WARNING: jq is not installed. Hook cannot rewrite commands. Install jq: https://jqlang.github.io/jq/download/" >&2 + exit 0 +fi + +if ! command -v rtk &>/dev/null; then + echo "[rtk] WARNING: rtk is not installed or not in PATH. Hook cannot rewrite commands. Install: https://github.com/rtk-ai/rtk#installation" >&2 + exit 0 +fi + +# Version guard: rtk rewrite was added in 0.23.0. +# Older binaries: warn once and exit cleanly (no silent failure). +RTK_VERSION=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) +if [ -n "$RTK_VERSION" ]; then + MAJOR=$(echo "$RTK_VERSION" | cut -d. -f1) + MINOR=$(echo "$RTK_VERSION" | cut -d. -f2) + # Require >= 0.23.0 + if [ "$MAJOR" -eq 0 ] && [ "$MINOR" -lt 23 ]; then + echo "[rtk] WARNING: rtk $RTK_VERSION is too old (need >= 0.23.0). Upgrade: cargo install rtk" >&2 + exit 0 + fi +fi + +INPUT=$(cat) +CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +if [ -z "$CMD" ]; then + exit 0 +fi + +# Delegate all rewrite + permission logic to the Rust binary. +REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) +EXIT_CODE=$? + +case $EXIT_CODE in + 0) + # Rewrite found, no permission rules matched — safe to auto-allow. + # If the output is identical, the command was already using RTK. + [ "$CMD" = "$REWRITTEN" ] && exit 0 + ;; + 1) + # No RTK equivalent — pass through unchanged. + exit 0 + ;; + 2) + # Deny rule matched — let Claude Code's native deny rule handle it. + exit 0 + ;; + 3) + # Ask rule matched — rewrite the command but do NOT auto-allow so that + # Claude Code prompts the user for confirmation. + ;; + *) + exit 0 + ;; +esac + +ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input') +UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') + +if [ "$EXIT_CODE" -eq 3 ]; then + # Ask: rewrite the command, omit permissionDecision so Claude Code prompts. + jq -n \ + --argjson updated "$UPDATED_INPUT" \ + '{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "updatedInput": $updated + } + }' +else + # Allow: rewrite the command and auto-allow. + jq -n \ + --argjson updated "$UPDATED_INPUT" \ + '{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow", + "permissionDecisionReason": "RTK auto-rewrite", + "updatedInput": $updated + } + }' +fi diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e480d46 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/rtk-rewrite.sh" + } + ] + } + ] + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c14ce1e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# AGENTS.md — Claw Dev Agent Rules + +## Tool Preferences + +| Task | Use | Not | +|------|-----|-----| +| Project overview | `tokei src/ shared/` | `ls -la` | +| Find files | `fd -e ts src/` | `find src -name "*.ts"` | +| Search content | `rg 'pattern' src/ -t ts` | `grep -r` | +| Structural search | `ast-grep run --lang ts --pattern '...' src/` | complex regex | +| Query JSON | `jq '.key' file.json` | `cat file.json \| grep` | +| Replace strings | `sd 'old' 'new' file.ts` | `sed -i` | +| Run tests | `node --test tests/*.test.mjs` | — | + +## Quick Reference + +```bash +tokei src/ shared/ # codebase stats +fd -e ts src/ # find TS files +rg 'pattern' src/ -t ts -l # search content +ast-grep run --lang ts --pattern '...' src/ # structural search +jq '.dependencies' package.json # query JSON +sd 'old' 'new' file.ts # safe replace +node --test tests/*.test.mjs # run all tests +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5af54eb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,78 @@ +# Claw Dev — Developer Guide + +## Supported Providers + +| Provider | Env Key | Default Model | +|----------|---------|---------------| +| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` | +| OpenAI | `OPENAI_API_KEY` | `gpt-4.1-mini` | +| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` | +| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` | +| GitHub Copilot | `COPILOT_TOKEN` | `openai/gpt-4.1-mini` | +| z.ai | `ZAI_API_KEY` | `glm-5` | +| Ollama | local | `qwen3` | + +Copy `.env.example` to `.env` and fill in your keys. + +## Codebase Overview + +```bash +tokei src/ shared/ # instant language + line count +``` + +## Recommended Tools + +All cross-platform (Linux / macOS / Windows). + +| Tool | Install | Replaces | Gain | +|------|---------|----------|------| +| **rtk** | `curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh \| sh` | — | -60/90% tokens on bash output | +| **rg** | `apt install ripgrep` / `brew install ripgrep` | grep | Fastest content search | +| **fd** | `apt install fd-find` / `brew install fd` | find | -99% output on targeted search | +| **ast-grep** | `npm install -g @ast-grep/cli` | regex grep | Semantic TS/JS code search | +| **jq** | `apt install jq` / `brew install jq` | grep on JSON | Precise JSON queries | +| **sd** | `cargo install sd` / `brew install sd` | sed | Cross-platform safe replace | +| **tokei** | `cargo install tokei` / `brew install tokei` | wc/ls | -75% vs ls for project overview | + +## Tool Usage + +### Project overview +```bash +tokei src/ shared/ # language breakdown +``` + +### Find files — fd over find +```bash +fd -e ts src/ # all .ts files +fd -g '*.test.mjs' tests/ # test files +# ❌ find src -name "*.ts" generates 43k chars for 1,332 files +``` + +### Search content — rg +```bash +rg 'provider' src/ -t ts # search TypeScript +rg 'GEMINI' .env.example # find env keys +``` + +### Structural search — ast-grep +```bash +ast-grep run --lang ts --pattern 'import { $A } from "$B"' src/ +ast-grep run --lang ts --pattern 'z.string()' src/ +``` + +### Query JSON/config +```bash +jq '.dependencies' package.json +jq 'keys' shared/providerModels.js 2>/dev/null +``` + +### Safe string replace — sd over sed +```bash +sd 'gemini-2.5-flash' 'gemini-3-flash' src/anthropicCompatProxy.ts +``` + +## Running Tests + +```bash +node --test tests/*.test.mjs +``` diff --git a/src/anthropicCompatProxy.ts b/src/anthropicCompatProxy.ts index 341b159..4256f7d 100644 --- a/src/anthropicCompatProxy.ts +++ b/src/anthropicCompatProxy.ts @@ -312,6 +312,18 @@ function resolveRequestModel(body: AnthropicMessageRequest): string { return requested; } + // The bundled client may send a Claude model id (e.g. "claude-sonnet-4-20250514") or a + // short alias ("sonnet", "opus", "haiku") regardless of the active provider. Non-Anthropic + // backends do not recognise these names and return 404. Remap them to ACTIVE_MODEL so the + // correct provider model is used. The catalog check above means an operator can still + // override this by adding a claude-* id to their provider catalog explicitly. + const isClaude = requested.startsWith("claude-"); + const isClaudeAlias = requested === "sonnet" || requested === "opus" || requested === "haiku"; + + if (isClaude || isClaudeAlias) { + return ACTIVE_MODEL; + } + // Allow custom model ids for user-managed providers even if they are not in the default catalog. if ( PROVIDER === "ollama" ||