diff --git a/README.md b/README.md index 68ba197..068c908 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OCP — Open Claude Proxy -> **Status: Stable (v3.4.0)** — Feature-complete. Bug fixes only. +> **Status: Stable (v3.5.0)** — Feature-complete. Bug fixes only. > **Already paying for Claude Pro/Max? Use your subscription as an OpenAI-compatible API — $0 extra cost.** @@ -29,9 +29,34 @@ Any tool that accepts `OPENAI_BASE_URL` works with OCP: | **OpenClaw** | `setup.mjs` auto-configures | | **Any OpenAI client** | Set base URL to `http://127.0.0.1:3456/v1` | -## Quick Start +## Installation + +OCP has two roles: **Server** (runs the proxy, needs Claude CLI) and **Client** (connects to a server, zero dependencies). + +``` +┌─ Server (always-on device) ─────────────────────────────┐ +│ Mac mini / NAS / Raspberry Pi / Desktop │ +│ Claude CLI + OCP server → bound to 0.0.0.0:3456 │ +└───────────────────────┬─────────────────────────────────┘ + │ LAN + ┌───────────────────┼───────────────────┐ + ▼ ▼ ▼ + Laptop Phone/Tablet Pi / Server + (client) (browser) (client) +``` + +--- + +### Server Setup + +> **Recommended:** Install OCP on a device that stays powered on — Mac mini, NAS, Raspberry Pi, or a desktop that doesn't sleep. This ensures all clients always have access. + +**Prerequisites:** +- Node.js 18+ +- [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli) installed and authenticated (`claude auth login`) ```bash +# 1. Clone and run setup git clone https://github.com/dtzp555-max/ocp.git cd ocp node setup.mjs @@ -41,59 +66,23 @@ The setup script will: 1. Verify Claude CLI is installed and authenticated 2. Start the proxy on port 3456 3. Install auto-start (launchd on macOS, systemd on Linux) +4. Symlink `ocp` to `/usr/local/bin` for CLI access -Then point your IDE to the proxy: - +**Single-machine use** — just set your IDE to use the proxy: ```bash export OPENAI_BASE_URL=http://127.0.0.1:3456/v1 ``` -### Verify - -```bash -curl http://127.0.0.1:3456/v1/models -# Returns: claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4 -``` - -## LAN Mode — Share with Family - -OCP can serve your entire household from a single machine. One Claude Pro/Max subscription, shared across all devices on your network. - -``` -Wife's laptop ──┐ -Son's iPad ───┼──→ OCP :3456 (your Mac) ──→ Claude subscription -Your Pi server ───┤ -Your desktop ──┘ -``` - -### Step 1: Enable LAN Access - -**Quick start (temporary, until restart):** -```bash -export CLAUDE_BIND=0.0.0.0 -export CLAUDE_AUTH_MODE=multi # per-user keys -export OCP_ADMIN_KEY=your-secret-admin-key -ocp restart -``` - -**Permanent (survives reboot):** +**LAN mode** — share with other devices on your network: ```bash +# Enable LAN access with per-user auth (recommended) node setup.mjs --bind 0.0.0.0 --auth-mode multi ``` -Then set your admin key in the launchd/systemd environment, or save it to a file: +Then create API keys for each person/device: ```bash -echo "your-secret-admin-key" > ~/.ocp/admin-key -chmod 600 ~/.ocp/admin-key -``` - -### Step 2: Create Keys for Family Members - -```bash -# Set admin key for CLI (or save to ~/.ocp/admin-key) export OCP_ADMIN_KEY=your-secret-admin-key -# Create a key for each person/device ocp keys add wife-laptop # ✓ Key created for "wife-laptop" # API Key: ocp_xDYzOB9ZKYzn... @@ -103,33 +92,72 @@ ocp keys add son-ipad ocp keys add pi-server ``` -### Step 3: Share Connection Info +Run `ocp lan` to see your IP and ready-to-share instructions. -Run `ocp lan` to see your IP and ready-to-share instructions: - -``` -$ ocp lan -OCP LAN Setup -───────────────────────────────────── - Your IP: 192.168.1.100 - Port: 3456 - - For IDE users, set: - OPENAI_BASE_URL=http://192.168.1.100:3456/v1 - OPENAI_API_KEY= - - Dashboard: http://192.168.1.100:3456/dashboard - - Status: ✓ LAN-accessible -``` - -Give each family member their key and these two settings: +**Verify:** ```bash -export OPENAI_BASE_URL=http://192.168.1.100:3456/v1 -export OPENAI_API_KEY=ocp_ +curl http://127.0.0.1:3456/v1/models +# Returns: claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4 ``` -### Step 4: Monitor Usage +--- + +### Client Setup + +> Clients do **not** need to install Node.js, Claude CLI, or the OCP repo. Only `curl` and `python3` are required (pre-installed on most Linux/Mac systems). + +**One-command setup** — download the lightweight `ocp-connect` script: + +```bash +curl -fsSL https://raw.githubusercontent.com/dtzp555-max/ocp/main/ocp-connect -o ocp-connect +chmod +x ocp-connect +./ocp-connect --key +``` + +Or as a one-liner (no file saved): +```bash +curl -fsSL https://raw.githubusercontent.com/dtzp555-max/ocp/main/ocp-connect | bash -s -- --key +``` + +Example: +``` +$ ./ocp-connect 192.168.1.100 --key ocp_xDYzOB9ZKYzn + +OCP Connect +───────────────────────────────────── + Remote: http://192.168.1.100:3456 + + Checking connectivity... + ✓ Connected + + Remote OCP v3.5.0 (auth: multi) + + Testing API access... + ✓ API accessible (3 models available) + + Written to /home/user/.bashrc: + OPENAI_BASE_URL=http://192.168.1.100:3456/v1 + OPENAI_API_KEY=ocp_xDYz... + + Running smoke test... + ✓ Smoke test passed: OK + + Done. Reload your shell to apply: + source /home/user/.bashrc +``` + +After running, reload your shell (`source ~/.bashrc` or `source ~/.zshrc`) and your IDE will automatically use the remote OCP. + +**Manual setup** — if you prefer not to use the script: +```bash +export OPENAI_BASE_URL=http://:3456/v1 +export OPENAI_API_KEY=ocp_ +``` +Add these lines to `~/.bashrc` or `~/.zshrc` to persist across sessions. + +--- + +### Monitoring (Server-side) ```bash # Per-key usage stats @@ -143,7 +171,9 @@ ocp keys # List all keys ocp keys revoke son-ipad # Revoke a key ``` -**Web Dashboard:** Open `http://:3456/dashboard` in any browser for real-time monitoring — per-key usage, request history, plan utilization, and system health. No login needed. +**Web Dashboard:** Open `http://:3456/dashboard` in any browser for real-time monitoring — per-key usage, request history, plan utilization, and system health. + +![OCP Dashboard](docs/images/dashboard.png) ### Auth Modes @@ -197,6 +227,7 @@ ocp health Proxy diagnostics ocp keys List all API keys (multi mode) ocp keys add Create a new API key ocp keys revoke Revoke an API key +ocp connect One-command LAN client setup ocp lan Show LAN connection info & IP ocp settings View tunable settings ocp settings Update a setting at runtime diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png new file mode 100644 index 0000000..772198e Binary files /dev/null and b/docs/images/dashboard.png differ diff --git a/ocp b/ocp index 5f6dcd4..47ae748 100755 --- a/ocp +++ b/ocp @@ -329,6 +329,203 @@ else: esac } +# ── connect ───────────────────────────────────────────────────────────── +cmd_connect_help() { + cat <<'EOF' +ocp connect — Connect this machine to a remote OCP as a LAN client + +Configures OPENAI_BASE_URL (and optionally OPENAI_API_KEY) in your shell +rc file so tools like Claude Code point to a remote OCP instance. + +Usage: + ocp connect [--port PORT] [--key API_KEY] + +Arguments: + host-ip IP address of the machine running OCP + --port PORT Port OCP listens on (default: 3456) + --key API_KEY API key to use (prompted if remote requires auth) + +Examples: + ocp connect 192.168.1.10 + ocp connect 192.168.1.10 --port 8080 + ocp connect 192.168.1.10 --key sk-abc123 +EOF +} + +cmd_connect() { + local host="" port=3456 key="" + + # Parse args + while [[ $# -gt 0 ]]; do + case "$1" in + --port) port="${2:?'--port requires a value'}"; shift 2 ;; + --key) key="${2:?'--key requires a value'}"; shift 2 ;; + --help|-h) cmd_connect_help; return 0 ;; + -*) echo "Unknown option: $1"; cmd_connect_help; return 1 ;; + *) host="$1"; shift ;; + esac + done + + if [[ -z "$host" ]]; then + echo "Error: host IP is required." + echo "" + cmd_connect_help + return 1 + fi + + if ! [[ "$host" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "Error: invalid host '$host'" + return 1 + fi + + local base_url="http://$host:$port" + + echo "OCP Connect" + echo "─────────────────────────────────────" + echo " Remote: $base_url" + echo "" + + # Step 1: Test connectivity via /health + echo " Checking connectivity..." + local health_json + health_json=$(curl -sf --max-time 10 "$base_url/health" 2>/dev/null) || { + echo " ✗ Cannot reach $base_url/health" + echo " Make sure OCP is running on $host and bound to 0.0.0.0 (LAN mode)." + return 1 + } + echo " ✓ Connected" + echo "" + + # Step 2: Show remote info + local remote_version auth_mode + remote_version=$(echo "$health_json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('version','?'))" 2>/dev/null || echo "?") + auth_mode=$(echo "$health_json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('authMode','none'))" 2>/dev/null || echo "none") + + echo " Remote OCP v$remote_version (auth: $auth_mode)" + echo "" + + # Step 3: Determine if key is needed + local needs_key=0 + if [[ "$auth_mode" != "none" ]]; then + needs_key=1 + fi + + if [[ $needs_key -eq 1 && -z "$key" ]]; then + echo " Remote requires authentication." + echo " Ask the admin to run: ocp keys add " + printf " Enter API key (or press Enter to skip): " + read -rs key /dev/null) && models_ok=1 + else + models_out=$(curl -sf --max-time 10 "$base_url/v1/models" 2>/dev/null) && models_ok=1 + fi + + if [[ $models_ok -eq 0 ]]; then + echo " ✗ API access failed — key may be invalid or revoked." + return 1 + fi + local model_count + model_count=$(echo "$models_out" | python3 -c "import sys,json; print(len(json.loads(sys.stdin.read()).get('data',[])))" 2>/dev/null || echo "?") + echo " ✓ API accessible ($model_count models available)" + echo "" + + # Step 5: Detect shell rc file + local rc_file + if [[ "${SHELL:-}" == */zsh ]]; then + rc_file="$HOME/.zshrc" + else + rc_file="$HOME/.bashrc" + fi + + # Step 6: Remove any previously written OCP LAN lines + if [[ -f "$rc_file" ]]; then + local tmp_rc + tmp_rc=$(mktemp) + if python3 - "$rc_file" "$tmp_rc" <<'PYEOF' +import sys +src, dst = sys.argv[1], sys.argv[2] +lines = open(src).readlines() +out = [] +skip = False +for line in lines: + s = line.rstrip('\n') + if s == '# OCP LAN (added by ocp connect)': + skip = True + continue + if skip and (s == '' or s.startswith('export OPENAI_BASE_URL=') or s.startswith('export OPENAI_API_KEY=')): + continue + skip = False + out.append(line) +open(dst, 'w').writelines(out) +PYEOF + then + cp "$tmp_rc" "$rc_file" + fi + rm -f "$tmp_rc" + fi + + # Step 7: Append new config + { + echo "" + echo "# OCP LAN (added by ocp connect)" + echo "export OPENAI_BASE_URL=$base_url/v1" + if [[ -n "$key" ]]; then + echo "export OPENAI_API_KEY=$key" + fi + } >> "$rc_file" + + echo " Written to $rc_file:" + echo " OPENAI_BASE_URL=$base_url/v1" + if [[ -n "$key" ]]; then + echo " OPENAI_API_KEY=${key:0:8}..." + fi + echo "" + + # Step 8: Quick smoke test — send a minimal chat completion + echo " Running smoke test..." + local chat_payload='{"model":"claude-haiku-4-5-20251001","messages":[{"role":"user","content":"Reply with OK only."}],"max_tokens":10}' + local chat_out chat_ok=0 + if [[ -n "$key" ]]; then + chat_out=$(curl -sf --max-time 30 \ + -H "Authorization: Bearer $key" \ + -H "Content-Type: application/json" \ + -d "$chat_payload" \ + "$base_url/v1/chat/completions" 2>/dev/null) && chat_ok=1 + else + chat_out=$(curl -sf --max-time 30 \ + -H "Content-Type: application/json" \ + -d "$chat_payload" \ + "$base_url/v1/chat/completions" 2>/dev/null) && chat_ok=1 + fi + + if [[ $chat_ok -eq 1 ]]; then + local reply + reply=$(echo "$chat_out" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d['choices'][0]['message']['content'].strip())" 2>/dev/null || echo "(response received)") + echo " ✓ Smoke test passed: $reply" + else + echo " ⚠ Smoke test failed (proxy is reachable but chat completion did not succeed)." + echo " The env vars have still been written. Check the remote OCP logs." + fi + + echo "" + echo " Done. Reload your shell to apply:" + echo " source $rc_file" +} + # ── lan ───────────────────────────────────────────────────────────────── cmd_lan_help() { cat <<'EOF' @@ -609,6 +806,7 @@ Commands: clear Clear all sessions keys Manage API keys (add/list/revoke) lan LAN mode setup guide + connect Connect to a remote OCP (sets env vars in rc file) restart Restart proxy restart gateway Restart gateway update Update OCP to latest version @@ -653,6 +851,7 @@ case "$subcmd" in clear) cmd_clear ;; keys) cmd_keys "${1:-}" "${2:-}" ;; lan) cmd_lan ;; + connect) cmd_connect "$@" ;; restart) cmd_restart "${1:-}" ;; update) cmd_update "${1:-}" ;; *) echo "Unknown command: $subcmd"; echo ""; cmd_help; exit 1 ;; diff --git a/ocp-connect b/ocp-connect new file mode 100755 index 0000000..65c6d72 --- /dev/null +++ b/ocp-connect @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# ocp-connect — Lightweight client script to connect to a remote OCP instance +# No dependencies beyond curl and python3 (available on most Linux/Mac systems) +# +# Install: +# curl -fsSL https://raw.githubusercontent.com/dtzp555-max/ocp/main/ocp-connect -o ocp-connect +# chmod +x ocp-connect +# +# Or run directly: +# curl -fsSL https://raw.githubusercontent.com/dtzp555-max/ocp/main/ocp-connect | bash -s -- --key +# +set -euo pipefail + +show_help() { + cat <<'EOF' +ocp-connect — Connect this machine to a remote OCP (Open Claude Proxy) + +Configures OPENAI_BASE_URL and OPENAI_API_KEY in your shell rc file +so tools like Claude Code, Cline, Aider, etc. point to the remote OCP. + +Usage: + ocp-connect [options] + +Options: + --port PORT Port OCP listens on (default: 3456) + --key API_KEY API key (prompted interactively if remote requires auth) + --help, -h Show this help + +Examples: + ocp-connect 192.168.1.10 + ocp-connect 192.168.1.10 --port 8080 + ocp-connect 192.168.1.10 --key ocp_abc123 + +What it does: + 1. Tests connectivity to the remote OCP + 2. Verifies your API key (if auth is enabled) + 3. Writes OPENAI_BASE_URL and OPENAI_API_KEY to ~/.bashrc or ~/.zshrc + 4. Runs a smoke test to confirm everything works + +After running, reload your shell: source ~/.bashrc (or ~/.zshrc) +EOF +} + +main() { + local host="" port=3456 key="" + + # Parse args + while [[ $# -gt 0 ]]; do + case "$1" in + --port) port="${2:?'--port requires a value'}"; shift 2 ;; + --key) key="${2:?'--key requires a value'}"; shift 2 ;; + --help|-h) show_help; exit 0 ;; + -*) echo "Unknown option: $1"; show_help; exit 1 ;; + *) host="$1"; shift ;; + esac + done + + if [[ -z "$host" ]]; then + echo "Error: host IP is required." + echo "" + show_help + exit 1 + fi + + if ! [[ "$host" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "Error: invalid host '$host'" + exit 1 + fi + + # Check dependencies + for cmd in curl python3; do + if ! command -v "$cmd" &>/dev/null; then + echo "Error: '$cmd' is required but not found." + exit 1 + fi + done + + local base_url="http://$host:$port" + + echo "OCP Connect" + echo "─────────────────────────────────────" + echo " Remote: $base_url" + echo "" + + # Step 1: Test connectivity via /health + echo " Checking connectivity..." + local health_json + health_json=$(curl -sf --max-time 10 "$base_url/health" 2>/dev/null) || { + echo " ✗ Cannot reach $base_url/health" + echo " Make sure OCP is running on $host and bound to 0.0.0.0 (LAN mode)." + exit 1 + } + echo " ✓ Connected" + echo "" + + # Step 2: Show remote info + local remote_version auth_mode + remote_version=$(echo "$health_json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('version','?'))" 2>/dev/null || echo "?") + auth_mode=$(echo "$health_json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('authMode','none'))" 2>/dev/null || echo "none") + + echo " Remote OCP v$remote_version (auth: $auth_mode)" + echo "" + + # Step 3: Determine if key is needed + if [[ "$auth_mode" != "none" && -z "$key" ]]; then + echo " Remote requires authentication." + echo " Ask the OCP admin to run: ocp keys add " + printf " Enter API key (or press Enter to skip): " + read -rs key /dev/null) && models_ok=1 + else + models_out=$(curl -sf --max-time 10 "$base_url/v1/models" 2>/dev/null) && models_ok=1 + fi + + if [[ $models_ok -eq 0 ]]; then + echo " ✗ API access failed — key may be invalid or revoked." + exit 1 + fi + local model_count + model_count=$(echo "$models_out" | python3 -c "import sys,json; print(len(json.loads(sys.stdin.read()).get('data',[])))" 2>/dev/null || echo "?") + echo " ✓ API accessible ($model_count models available)" + echo "" + + # Step 5: Detect shell rc file + local rc_file + if [[ "${SHELL:-}" == */zsh ]]; then + rc_file="$HOME/.zshrc" + elif [[ "${SHELL:-}" == */fish ]]; then + echo " Note: fish shell detected. Writing to ~/.bashrc — add to fish config manually." + rc_file="$HOME/.bashrc" + else + rc_file="$HOME/.bashrc" + fi + + # Step 6: Remove any previously written OCP LAN lines (idempotent) + if [[ -f "$rc_file" ]]; then + local tmp_rc + tmp_rc=$(mktemp) + if python3 - "$rc_file" "$tmp_rc" <<'PYEOF' +import sys +src, dst = sys.argv[1], sys.argv[2] +lines = open(src).readlines() +out = [] +skip = False +for line in lines: + s = line.rstrip('\n') + if s == '# OCP LAN (added by ocp connect)': + skip = True + continue + if skip and (s == '' or s.startswith('export OPENAI_BASE_URL=') or s.startswith('export OPENAI_API_KEY=')): + continue + skip = False + out.append(line) +open(dst, 'w').writelines(out) +PYEOF + then + cp "$tmp_rc" "$rc_file" + fi + rm -f "$tmp_rc" + fi + + # Step 7: Append new config + { + echo "" + echo "# OCP LAN (added by ocp connect)" + echo "export OPENAI_BASE_URL=$base_url/v1" + if [[ -n "$key" ]]; then + echo "export OPENAI_API_KEY=$key" + fi + } >> "$rc_file" + + echo " Written to $rc_file:" + echo " OPENAI_BASE_URL=$base_url/v1" + if [[ -n "$key" ]]; then + echo " OPENAI_API_KEY=${key:0:8}..." + fi + echo "" + + # Step 8: Quick smoke test + echo " Running smoke test..." + # Pick the first available model from /v1/models + local smoke_model + smoke_model=$(echo "$models_out" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d['data'][0]['id'])" 2>/dev/null || echo "claude-haiku-4-5-20251001") + local chat_payload="{\"model\":\"$smoke_model\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with OK only.\"}],\"max_tokens\":10}" + local chat_out chat_ok=0 + if [[ -n "$key" ]]; then + chat_out=$(curl -sf --max-time 30 \ + -H "Authorization: Bearer $key" \ + -H "Content-Type: application/json" \ + -d "$chat_payload" \ + "$base_url/v1/chat/completions" 2>/dev/null) && chat_ok=1 + else + chat_out=$(curl -sf --max-time 30 \ + -H "Content-Type: application/json" \ + -d "$chat_payload" \ + "$base_url/v1/chat/completions" 2>/dev/null) && chat_ok=1 + fi + + if [[ $chat_ok -eq 1 ]]; then + local reply + reply=$(echo "$chat_out" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d['choices'][0]['message']['content'].strip())" 2>/dev/null || echo "(response received)") + echo " ✓ Smoke test passed: $reply" + else + echo " ⚠ Smoke test failed (proxy is reachable but chat completion did not succeed)." + echo " The env vars have still been written. Check the remote OCP logs." + fi + + echo "" + echo " Done. Reload your shell to apply:" + echo " source $rc_file" +} + +main "$@" diff --git a/package.json b/package.json index b3d5d7a..cdb7eb5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openclaw-claude-proxy", - "version": "3.4.0", + "version": "3.5.0", "description": "OCP (Open Claude Proxy) — use your Claude Pro/Max subscription as an OpenAI-compatible API for any IDE. Works with Cline, OpenCode, Aider, Continue.dev, OpenClaw, and more.", "type": "module", "bin": { diff --git a/server.mjs b/server.mjs index 415723a..795d426 100644 --- a/server.mjs +++ b/server.mjs @@ -1123,6 +1123,7 @@ const server = createServer(async (req, res) => { uptimeHuman: `${Math.floor(uptimeMs / 3600000)}h ${Math.floor((uptimeMs % 3600000) / 60000)}m`, claudeBinary: CLAUDE, claudeBinaryOk: binaryOk, + authMode: AUTH_MODE, auth: authStatus, config: { timeout: TIMEOUT,