mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-22 21:45:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b72e449337 | ||
|
|
2b05558b8b | ||
|
|
d9b7c0d905 | ||
|
|
27cd8b1735 | ||
|
|
9dd35c90ae | ||
|
|
c005a8fd27 | ||
|
|
77a3f48cfd | ||
|
|
7390a19ec6 | ||
|
|
04643217ae | ||
|
|
3de1868313 | ||
|
|
75cff38f4c | ||
|
|
a8a0af43c9 | ||
|
|
77a6f8ed59 | ||
|
|
b5be1c04f4 | ||
|
|
1f1b3871da | ||
|
|
f6f4f68ecd | ||
|
|
47e39d7820 | ||
|
|
7434f6adc0 | ||
|
|
8a7951134e | ||
|
|
47434a8ba7 |
@@ -1,172 +1,180 @@
|
|||||||
# openclaw-claude-proxy
|
# OCP — Open Claude Proxy
|
||||||
|
|
||||||
> **Already paying for Claude Pro/Max? Use it as your OpenClaw model provider — $0 extra API cost.**
|
> **Status: Stable (v3.0.0)** — Feature-complete. Bug fixes only.
|
||||||
|
|
||||||
A lightweight, zero-dependency proxy that lets [OpenClaw](https://github.com/openclaw/openclaw) agents talk to Claude through your existing subscription. One command to set up, one file to run.
|
> **Already paying for Claude Pro/Max? Use your subscription as an OpenAI-compatible API — $0 extra cost.**
|
||||||
|
|
||||||
## v2.0.0 — Major Upgrade
|
OCP turns your Claude Pro/Max subscription into a standard OpenAI-compatible API on localhost. Any tool that speaks the OpenAI protocol can use it — no separate API key, no extra billing.
|
||||||
|
|
||||||
**What's new:**
|
|
||||||
- **On-demand spawning** — eliminates the pool crash loops, DEGRADED states, and stdin timeout errors from v1.x. Each request spawns a fresh `claude -p` process with stdin written immediately. No more stale workers, no more backoff spirals.
|
|
||||||
- **Session management** — multi-turn conversations use `--resume` to avoid resending full history. Reduces token waste and enables Claude Code's built-in context compression on long conversations.
|
|
||||||
- **Full tool access** — expanded default tools (Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch, Agent). Configurable via `CLAUDE_ALLOWED_TOOLS` or bypass all checks with `CLAUDE_SKIP_PERMISSIONS=true`.
|
|
||||||
- **System prompt pass-through** — set `CLAUDE_SYSTEM_PROMPT` to inject context into every request.
|
|
||||||
- **MCP config support** — set `CLAUDE_MCP_CONFIG` to load MCP servers (Telegram, etc.) into claude -p calls.
|
|
||||||
- **Concurrency control** — `CLAUDE_MAX_CONCURRENT` prevents runaway process spawning (default: 5).
|
|
||||||
- **Auth health monitoring** — periodic `claude auth status` checks with status exposed on `/health`.
|
|
||||||
- **Session API** — `GET /sessions` to list, `DELETE /sessions` to clear active sessions.
|
|
||||||
- **Improved diagnostics** — `/health` endpoint shows stats, active sessions, recent errors, auth status, and full config.
|
|
||||||
|
|
||||||
**Coexistence with Claude Code interactive mode:**
|
|
||||||
OCP and Claude Code (interactive/Telegram) run on completely different paths and can coexist on the same machine without conflict:
|
|
||||||
- OCP: `localhost:3456` (HTTP) → spawns `claude -p` processes (per-request, stateless)
|
|
||||||
- CC: MCP protocol (in-process) → persistent interactive session
|
|
||||||
- No shared ports, no shared processes, no shared sessions
|
|
||||||
|
|
||||||
**Daemon advantage over CC:**
|
|
||||||
OCP runs as a system daemon (launchd/systemd) that auto-starts on boot and auto-recovers from crashes. Unlike Claude Code interactive mode, OCP does not require a terminal session to stay open — it survives disconnects, reboots, and SSH drops. Combined with OpenClaw's memory system, this means your agents never lose continuity.
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
```
|
```
|
||||||
OpenClaw Gateway → proxy (localhost:3456) → claude -p CLI → Anthropic (via OAuth)
|
Cline ──┐
|
||||||
|
OpenCode ───┤
|
||||||
|
Aider ───┼──→ OCP :3456 ──→ Claude CLI ──→ Your subscription
|
||||||
|
Continue.dev ───┤
|
||||||
|
OpenClaw ───┘
|
||||||
```
|
```
|
||||||
|
|
||||||
The proxy translates OpenAI-compatible `/v1/chat/completions` requests into `claude -p` CLI calls. Anthropic sees normal Claude Code usage under your subscription — no API billing, no separate key.
|
One proxy. Multiple IDEs. All models. **$0 API cost.**
|
||||||
|
|
||||||
## Prerequisites
|
## Supported Tools
|
||||||
|
|
||||||
- **Node.js** >= 18
|
Any tool that accepts `OPENAI_BASE_URL` works with OCP:
|
||||||
- **Claude CLI** installed and authenticated (`claude login`)
|
|
||||||
- **OpenClaw** installed
|
|
||||||
|
|
||||||
## Quick Start (Node.js)
|
| Tool | Configuration |
|
||||||
|
|------|--------------|
|
||||||
|
| **Cline** | Settings → `OPENAI_BASE_URL=http://127.0.0.1:3456/v1` |
|
||||||
|
| **OpenCode** | `OPENAI_BASE_URL=http://127.0.0.1:3456/v1` |
|
||||||
|
| **Aider** | `aider --openai-api-base http://127.0.0.1:3456/v1` |
|
||||||
|
| **Continue.dev** | config.json → `apiBase: "http://127.0.0.1:3456/v1"` |
|
||||||
|
| **OpenClaw** | `setup.mjs` auto-configures |
|
||||||
|
| **Any OpenAI client** | Set base URL to `http://127.0.0.1:3456/v1` |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/dtzp555-max/openclaw-claude-proxy.git
|
git clone https://github.com/dtzp555-max/ocp.git
|
||||||
cd openclaw-claude-proxy
|
cd ocp
|
||||||
|
|
||||||
# Auto-configure OpenClaw + start proxy + install auto-start
|
|
||||||
node setup.mjs
|
node setup.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
That's it. The setup script will:
|
The setup script will:
|
||||||
1. Verify Claude CLI is installed and authenticated
|
1. Verify Claude CLI is installed and authenticated
|
||||||
2. Add `claude-local` provider to `openclaw.json`
|
2. Start the proxy on port 3456
|
||||||
3. Add auth profiles to all agents
|
3. Install auto-start (launchd on macOS, systemd on Linux)
|
||||||
4. Start the proxy
|
|
||||||
5. Install auto-start on login (launchd on macOS, systemd on Linux)
|
|
||||||
|
|
||||||
Then set your preferred Claude model as default:
|
Then point your IDE to the proxy:
|
||||||
```bash
|
|
||||||
openclaw config set agents.defaults.model.primary "claude-local/claude-opus-4-6"
|
|
||||||
openclaw gateway restart
|
|
||||||
```
|
|
||||||
|
|
||||||
## Session Management (v2.0)
|
|
||||||
|
|
||||||
Multi-turn conversations can use sessions to avoid resending full message history on every request.
|
|
||||||
|
|
||||||
**How to enable:** Include a `session_id` or `conversation_id` field in your request body, or set the `X-Session-Id` / `X-Conversation-Id` header.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"model": "claude-opus-4-6",
|
|
||||||
"session_id": "conv-abc-123",
|
|
||||||
"messages": [
|
|
||||||
{"role": "user", "content": "Hello"},
|
|
||||||
{"role": "assistant", "content": "Hi there!"},
|
|
||||||
{"role": "user", "content": "What did I just say?"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**First request** with a new session_id: all messages are sent, session is persisted via `--session-id`.
|
|
||||||
**Subsequent requests** with the same session_id: only the latest user message is sent via `--resume`, reducing token consumption.
|
|
||||||
|
|
||||||
Sessions expire after 1 hour of inactivity (configurable via `CLAUDE_SESSION_TTL`).
|
|
||||||
|
|
||||||
**API endpoints:**
|
|
||||||
- `GET /sessions` — list all active sessions
|
|
||||||
- `DELETE /sessions` — clear all sessions
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
- **Localhost only** — the proxy binds to `127.0.0.1` and is not exposed to the internet or your local network
|
|
||||||
- **Bearer token auth (optional)** — set `PROXY_API_KEY` to require a Bearer token on all requests (except `/health`). When unset, auth is disabled for backwards compatibility
|
|
||||||
- **No API keys for Claude** — authentication to Anthropic goes through Claude CLI's OAuth session, no Anthropic credentials are stored in the proxy
|
|
||||||
- **Auto-start via launchd/systemd** — `node setup.mjs` installs a user-level launch agent (macOS) or systemd user service (Linux) so the proxy starts automatically on login
|
|
||||||
- **Remove auto-start** at any time:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node uninstall.mjs
|
export OPENAI_BASE_URL=http://127.0.0.1:3456/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Manual Install
|
### Verify
|
||||||
|
|
||||||
### 1. Start the proxy
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node server.mjs
|
curl http://127.0.0.1:3456/v1/models
|
||||||
# or in background:
|
# Returns: claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4
|
||||||
bash start.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Configure OpenClaw
|
## Built-in Usage Monitoring
|
||||||
|
|
||||||
Add to `~/.openclaw/openclaw.json` under `models.providers`:
|
Check your subscription usage from the terminal:
|
||||||
|
|
||||||
```json
|
```
|
||||||
"claude-local": {
|
$ ocp usage
|
||||||
"baseUrl": "http://127.0.0.1:3456/v1",
|
Plan Usage Limits
|
||||||
"api": "openai-completions",
|
─────────────────────────────────────
|
||||||
"apiKey": "<your PROXY_API_KEY, or omit if auth disabled>",
|
Current session 21% used
|
||||||
"models": [
|
Resets in 3h 12m (Tue, Mar 28, 10:00 PM)
|
||||||
{
|
|
||||||
"id": "claude-opus-4-6",
|
Weekly (all models) 45% used
|
||||||
"name": "Claude Opus 4.6",
|
Resets in 4d 2h (Tue, Mar 31, 12:00 AM)
|
||||||
"reasoning": true,
|
|
||||||
"input": ["text"],
|
Extra usage off
|
||||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
|
||||||
"contextWindow": 200000,
|
Model Stats
|
||||||
"maxTokens": 16384
|
Model Req OK Er AvgT MaxT AvgP MaxP
|
||||||
},
|
──────────────────────────────────────────────────────
|
||||||
{
|
opus 5 5 0 32s 87s 42K 43K
|
||||||
"id": "claude-sonnet-4-6",
|
sonnet 18 18 0 20s 45s 36K 56K
|
||||||
"name": "Claude Sonnet 4.6",
|
Total 23
|
||||||
"reasoning": true,
|
|
||||||
"input": ["text"],
|
Proxy: up 6h 32m | 23 reqs | 0 err | 0 timeout
|
||||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
|
||||||
"contextWindow": 200000,
|
|
||||||
"maxTokens": 16384
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "claude-haiku-4",
|
|
||||||
"name": "Claude Haiku 4",
|
|
||||||
"reasoning": false,
|
|
||||||
"input": ["text"],
|
|
||||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
|
||||||
"contextWindow": 200000,
|
|
||||||
"maxTokens": 8192
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Set as default model
|
### All Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
ocp usage Plan usage limits & model stats
|
||||||
|
ocp status Quick overview
|
||||||
|
ocp health Proxy diagnostics
|
||||||
|
ocp settings View tunable settings
|
||||||
|
ocp settings <k> <v> Update a setting at runtime
|
||||||
|
ocp logs [N] [level] Recent logs (default: 20, error)
|
||||||
|
ocp models Available models
|
||||||
|
ocp sessions Active sessions
|
||||||
|
ocp clear Clear all sessions
|
||||||
|
ocp restart Restart proxy
|
||||||
|
ocp restart gateway Restart gateway
|
||||||
|
ocp version Show version
|
||||||
|
ocp --help Command reference
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install the CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openclaw config set agents.defaults.model.primary "claude-local/claude-opus-4-6"
|
# Symlink to PATH (recommended)
|
||||||
openclaw gateway restart
|
sudo ln -sf $(pwd)/ocp /usr/local/bin/ocp
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
ocp --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Cloud/Linux servers:** If `ocp: command not found`, the binary isn't in PATH. Full path: `~/.openclaw/projects/ocp/ocp`
|
||||||
|
|
||||||
|
### Runtime Settings (No Restart Needed)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ocp settings maxPromptChars 200000
|
||||||
|
✓ maxPromptChars = 200000
|
||||||
|
|
||||||
|
$ ocp settings maxConcurrent 4
|
||||||
|
✓ maxConcurrent = 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
Your IDE → OCP (localhost:3456) → claude -p CLI → Anthropic (via subscription)
|
||||||
|
```
|
||||||
|
|
||||||
|
OCP translates OpenAI-compatible `/v1/chat/completions` requests into `claude -p` CLI calls. Anthropic sees normal Claude Code usage — no API billing, no separate key needed.
|
||||||
|
|
||||||
## Available Models
|
## Available Models
|
||||||
|
|
||||||
| Model ID | Claude CLI model | Notes |
|
| Model ID | Notes |
|
||||||
|----------|-----------------|-------|
|
|----------|-------|
|
||||||
| `claude-opus-4-6` | claude-opus-4-6 | Most capable, slower |
|
| `claude-opus-4-6` | Most capable, slower |
|
||||||
| `claude-sonnet-4-6` | claude-sonnet-4-6 | Good balance of speed/quality |
|
| `claude-sonnet-4-6` | Good balance of speed/quality |
|
||||||
| `claude-haiku-4` | claude-haiku-4-5-20251001 | Fastest, lightweight |
|
| `claude-haiku-4` | Fastest, lightweight |
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/v1/models` | GET | List available models |
|
||||||
|
| `/v1/chat/completions` | POST | Chat completion (streaming + non-streaming) |
|
||||||
|
| `/health` | GET | Comprehensive health check |
|
||||||
|
| `/usage` | GET | Plan usage limits + per-model stats |
|
||||||
|
| `/status` | GET | Combined overview (usage + health) |
|
||||||
|
| `/settings` | GET/PATCH | View or update settings at runtime |
|
||||||
|
| `/logs` | GET | Recent log entries (`?n=20&level=error`) |
|
||||||
|
| `/sessions` | GET/DELETE | List or clear active sessions |
|
||||||
|
|
||||||
|
## OpenClaw Integration
|
||||||
|
|
||||||
|
OCP was originally built for [OpenClaw](https://github.com/openclaw/openclaw) and includes deep integration:
|
||||||
|
|
||||||
|
- **`setup.mjs`** auto-configures the `claude-local` provider in `openclaw.json`
|
||||||
|
- **Gateway plugin** registers `/ocp` as a native slash command in Telegram/Discord
|
||||||
|
- **Multi-agent** — 8 concurrent requests sharing one subscription
|
||||||
|
|
||||||
|
### Install the Gateway Plugin
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r ocp-plugin/ ~/.openclaw/extensions/ocp/
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `~/.openclaw/openclaw.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"allow": ["ocp"],
|
||||||
|
"entries": { "ocp": { "enabled": true } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart: `openclaw gateway restart`
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
@@ -174,83 +182,35 @@ openclaw gateway restart
|
|||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `CLAUDE_PROXY_PORT` | `3456` | Listen port |
|
| `CLAUDE_PROXY_PORT` | `3456` | Listen port |
|
||||||
| `CLAUDE_BIN` | *(auto-detect)* | Path to claude binary |
|
| `CLAUDE_BIN` | *(auto-detect)* | Path to claude binary |
|
||||||
| `CLAUDE_TIMEOUT` | `300000` | Request timeout (ms) |
|
| `CLAUDE_TIMEOUT` | `300000` | Overall request timeout (ms) |
|
||||||
|
| `CLAUDE_FIRST_BYTE_TIMEOUT` | `90000` | Base first-byte timeout (ms) |
|
||||||
|
| `CLAUDE_MAX_CONCURRENT` | `8` | Max concurrent claude processes |
|
||||||
|
| `CLAUDE_MAX_PROMPT_CHARS` | `150000` | Prompt truncation limit (chars) |
|
||||||
|
| `CLAUDE_SESSION_TTL` | `3600000` | Session expiry (ms, default: 1 hour) |
|
||||||
| `CLAUDE_ALLOWED_TOOLS` | `Bash,Read,...,Agent` | Comma-separated tools to pre-approve |
|
| `CLAUDE_ALLOWED_TOOLS` | `Bash,Read,...,Agent` | Comma-separated tools to pre-approve |
|
||||||
| `CLAUDE_SKIP_PERMISSIONS` | `false` | Set `true` to bypass all permission checks |
|
| `CLAUDE_SKIP_PERMISSIONS` | `false` | Bypass all permission checks |
|
||||||
| `CLAUDE_SYSTEM_PROMPT` | *(empty)* | System prompt appended to all requests |
|
|
||||||
| `CLAUDE_MCP_CONFIG` | *(empty)* | Path to MCP server config JSON file |
|
|
||||||
| `CLAUDE_SESSION_TTL` | `3600000` | Session expiry in ms (default: 1 hour) |
|
|
||||||
| `CLAUDE_MAX_CONCURRENT` | `5` | Max concurrent claude processes |
|
|
||||||
| `PROXY_API_KEY` | *(unset)* | Bearer token for API authentication |
|
| `PROXY_API_KEY` | *(unset)* | Bearer token for API authentication |
|
||||||
|
|
||||||
## API Endpoints
|
## Security
|
||||||
|
|
||||||
- `GET /v1/models` — List available models
|
- **Localhost only** — binds to `127.0.0.1`, not exposed to the network
|
||||||
- `POST /v1/chat/completions` — Chat completion (streaming + non-streaming)
|
- **Bearer token auth (optional)** — set `PROXY_API_KEY` to require auth
|
||||||
- `GET /health` — Comprehensive health check (stats, sessions, auth, config)
|
- **No API keys needed** — authentication goes through Claude CLI's OAuth session
|
||||||
- `GET /sessions` — List active sessions
|
- **Auto-start** — launchd (macOS) / systemd (Linux)
|
||||||
- `DELETE /sessions` — Clear all sessions
|
|
||||||
|
|
||||||
## Authentication
|
## Known Issues
|
||||||
|
|
||||||
The proxy supports optional Bearer token authentication via the `PROXY_API_KEY` environment variable.
|
### `/ocp` command returns "Unknown skill: ocp" (OpenClaw only)
|
||||||
|
|
||||||
**When `PROXY_API_KEY` is set**, all requests (except `GET /health`) must include a valid `Authorization: Bearer <token>` header. Requests with a missing or invalid token receive a `401 Unauthorized` response.
|
The `/ocp` plugin command may intermittently stop working in Telegram/Discord. This is caused by an OpenClaw gateway bug ([openclaw/openclaw#26895](https://github.com/openclaw/openclaw/issues/26895)).
|
||||||
|
|
||||||
**When `PROXY_API_KEY` is not set**, authentication is disabled and all requests are accepted.
|
**Workaround:**
|
||||||
|
```
|
||||||
```bash
|
/new
|
||||||
# Start with auth enabled
|
/ocp restart
|
||||||
PROXY_API_KEY=my-secret-token node server.mjs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture: v1 vs v2
|
**Important:** Do not add `ocp` to agent `skills` lists or place a `SKILL.md` in workspace skills — this creates a routing conflict.
|
||||||
|
|
||||||
| | v1.x (pool) | v2.0 (on-demand) |
|
|
||||||
|---|---|---|
|
|
||||||
| Process lifecycle | Pre-spawn idle workers | Spawn per request |
|
|
||||||
| Crash handling | Backoff → DEGRADED → manual restart | No crash loops (no idle workers) |
|
|
||||||
| Session support | None (stateless) | --resume with session tracking |
|
|
||||||
| Tool access | 6 tools hardcoded | Configurable, expanded defaults |
|
|
||||||
| System prompt | None | CLAUDE_SYSTEM_PROMPT env |
|
|
||||||
| MCP support | None | CLAUDE_MCP_CONFIG env |
|
|
||||||
| Concurrency | Unlimited (dangerous) | CLAUDE_MAX_CONCURRENT limit |
|
|
||||||
| Auth monitoring | None | Periodic health checks |
|
|
||||||
| Diagnostics | Basic /health | Full stats, sessions, errors |
|
|
||||||
|
|
||||||
## Coexistence with Claude Code
|
|
||||||
|
|
||||||
OCP and Claude Code interactive mode (including Telegram bots) are completely independent:
|
|
||||||
|
|
||||||
| | OCP (this proxy) | CC interactive |
|
|
||||||
|---|---|---|
|
|
||||||
| Protocol | HTTP (localhost:3456) | MCP (in-process) |
|
|
||||||
| Process model | Per-request spawn | Persistent session |
|
|
||||||
| Lifecycle | Daemon (auto-start, auto-recover) | Requires terminal |
|
|
||||||
| Permission model | Pre-approved tools | Interactive prompts |
|
|
||||||
| Use case | Automated agent work | Human-in-the-loop |
|
|
||||||
|
|
||||||
Both can run on the same machine simultaneously. No shared state, no port conflicts.
|
|
||||||
|
|
||||||
## Recovery after OpenClaw upgrade
|
|
||||||
|
|
||||||
OpenClaw upgrades (`npm update -g openclaw`) **do not overwrite** the user config at `~/.openclaw/openclaw.json`. However, if the claude-local models stop working after an upgrade:
|
|
||||||
|
|
||||||
### One-command recovery
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/.openclaw/projects/claude-proxy # or wherever you cloned it
|
|
||||||
git pull # pull latest version
|
|
||||||
node setup.mjs # reconfigure OpenClaw + start proxy
|
|
||||||
openclaw gateway restart
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Cost shows as $0 because billing goes through your Claude subscription
|
|
||||||
- Each request spawns a `claude -p` process; concurrent requests are capped by `CLAUDE_MAX_CONCURRENT`
|
|
||||||
- The proxy must run on the same machine as the Claude CLI (uses local OAuth)
|
|
||||||
- Session data is stored by Claude CLI on disk; session map is in-memory (lost on proxy restart)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ocp — OpenClaw Proxy CLI
|
||||||
|
# Usage: ocp <command> [args]
|
||||||
|
#
|
||||||
|
# Talks to the local claude-proxy at http://127.0.0.1:3456
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROXY="http://127.0.0.1:3456"
|
||||||
|
|
||||||
|
_json() { python3 -m json.tool 2>/dev/null || cat; }
|
||||||
|
|
||||||
|
_bar() {
|
||||||
|
local pct=$1 width=20
|
||||||
|
local filled=$(( pct * width / 100 ))
|
||||||
|
local empty=$(( width - filled ))
|
||||||
|
printf '['
|
||||||
|
printf '█%.0s' $(seq 1 $filled 2>/dev/null) || true
|
||||||
|
printf '░%.0s' $(seq 1 $empty 2>/dev/null) || true
|
||||||
|
printf '] %d%%' "$pct"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── usage ────────────────────────────────────────────────────────────────
|
||||||
|
cmd_usage_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp usage — Show Claude plan usage limits
|
||||||
|
|
||||||
|
Displays current session utilization, weekly limits, extra usage status,
|
||||||
|
and proxy request statistics. Data is fetched from the Anthropic API
|
||||||
|
via a minimal probe call (cached for 5 minutes).
|
||||||
|
|
||||||
|
Usage: ocp usage
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_usage() {
|
||||||
|
local data
|
||||||
|
data=$(curl -sf --max-time 15 "$PROXY/usage" 2>&1) || { echo "Error: proxy unreachable"; exit 1; }
|
||||||
|
|
||||||
|
echo "$data" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
|
||||||
|
d = json.loads(sys.stdin.read())
|
||||||
|
p = d['plan']
|
||||||
|
s = p['currentSession']
|
||||||
|
w = p['weeklyLimits']['allModels']
|
||||||
|
e = p['extraUsage']
|
||||||
|
px = d['proxy']
|
||||||
|
models = d.get('models', {})
|
||||||
|
|
||||||
|
print('Plan Usage Limits')
|
||||||
|
print('─────────────────────────────────────')
|
||||||
|
print(f' Current session {s[\"percent\"]:>4} used')
|
||||||
|
print(f' Resets in {s[\"resetsIn\"]} ({s[\"resetsAtHuman\"]})')
|
||||||
|
print()
|
||||||
|
print(f' Weekly (all models) {w[\"percent\"]:>4} used')
|
||||||
|
print(f' Resets in {w[\"resetsIn\"]} ({w[\"resetsAtHuman\"]})')
|
||||||
|
print()
|
||||||
|
status_icon = 'on' if e['status'] == 'allowed' else 'off'
|
||||||
|
print(f' Extra usage {status_icon}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
if models:
|
||||||
|
print('Model Stats (since proxy start)')
|
||||||
|
print('─────────────────────────────────────────────────────────────────────')
|
||||||
|
hdr = f' {\"Model\":<25} {\"Reqs\":>5} {\"OK\":>4} {\"Err\":>4} {\"Avg Time\":>9} {\"Max Time\":>9} {\"Avg Prompt\":>11} {\"Max Prompt\":>11}'
|
||||||
|
print(hdr)
|
||||||
|
print(' ' + '─' * (len(hdr) - 2))
|
||||||
|
total_reqs = 0
|
||||||
|
for model in sorted(models):
|
||||||
|
m = models[model]
|
||||||
|
total_reqs += m['requests']
|
||||||
|
avg_t = f'{m[\"avgElapsed\"]/1000:.0f}s' if m['avgElapsed'] else '-'
|
||||||
|
max_t = f'{m[\"maxElapsed\"]/1000:.0f}s' if m['maxElapsed'] else '-'
|
||||||
|
avg_p = f'{m[\"avgPromptChars\"]/1000:.0f}K' if m['avgPromptChars'] else '-'
|
||||||
|
max_p = f'{m[\"maxPromptChars\"]/1000:.0f}K' if m['maxPromptChars'] else '-'
|
||||||
|
print(f' {model:<25} {m[\"requests\"]:>5} {m[\"successes\"]:>4} {m[\"errors\"]:>4} {avg_t:>9} {max_t:>9} {avg_p:>11} {max_p:>11}')
|
||||||
|
print(f' {\"Total\":<25} {total_reqs:>5}')
|
||||||
|
else:
|
||||||
|
print(' No model requests yet.')
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f'Proxy: up {px[\"uptime\"]} | {px[\"totalRequests\"]} reqs | {px[\"activeRequests\"]} active | {px[\"errors\"]} err | {px[\"timeouts\"]} timeout')
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── status ───────────────────────────────────────────────────────────────
|
||||||
|
cmd_status_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp status — Quick combined overview (usage + health)
|
||||||
|
|
||||||
|
Usage: ocp status
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
curl -sf --max-time 15 "$PROXY/status" | _json
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── health ───────────────────────────────────────────────────────────────
|
||||||
|
cmd_health_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp health — Proxy health and diagnostics
|
||||||
|
|
||||||
|
Shows proxy status, version, uptime, auth, config, sessions, and recent errors.
|
||||||
|
|
||||||
|
Usage: ocp health
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_health() {
|
||||||
|
curl -sf --max-time 10 "$PROXY/health" | _json
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── logs ─────────────────────────────────────────────────────────────────
|
||||||
|
cmd_logs_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp logs — Show recent proxy log entries
|
||||||
|
|
||||||
|
Usage: ocp logs [N] [LEVEL]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
N Number of entries to show (default: 20, max: 200)
|
||||||
|
LEVEL Filter level: error, warn, info, all (default: error)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
ocp logs Last 20 errors
|
||||||
|
ocp logs 50 all Last 50 entries of any level
|
||||||
|
ocp logs 10 warn Last 10 warnings
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
local n=${1:-20}
|
||||||
|
local level=${2:-error}
|
||||||
|
curl -sf --max-time 10 "$PROXY/logs?n=$n&level=$level" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.loads(sys.stdin.read())
|
||||||
|
entries = d.get('entries', [])
|
||||||
|
if not entries:
|
||||||
|
print(f'No {d.get(\"level\",\"\")} log entries.')
|
||||||
|
else:
|
||||||
|
for e in entries:
|
||||||
|
if 'raw' in e:
|
||||||
|
print(e['raw'][:200])
|
||||||
|
else:
|
||||||
|
ts = e.get('ts','')[:19]
|
||||||
|
ev = e.get('event','?')
|
||||||
|
lvl = e.get('level','')
|
||||||
|
model = e.get('model','')
|
||||||
|
extra = ' '.join(f'{k}={v}' for k,v in e.items() if k not in ('ts','event','level','model'))
|
||||||
|
parts = [ts, lvl.upper(), ev]
|
||||||
|
if model: parts.append(model)
|
||||||
|
if extra: parts.append(extra)
|
||||||
|
print(' | '.join(parts))
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── models ───────────────────────────────────────────────────────────────
|
||||||
|
cmd_models_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp models — List available Claude models
|
||||||
|
|
||||||
|
Usage: ocp models
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_models() {
|
||||||
|
curl -sf --max-time 5 "$PROXY/v1/models" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.loads(sys.stdin.read())
|
||||||
|
for m in d.get('data', []):
|
||||||
|
print(f\" {m['id']}\")
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── sessions ─────────────────────────────────────────────────────────────
|
||||||
|
cmd_sessions_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp sessions — List active CLI sessions
|
||||||
|
|
||||||
|
Usage: ocp sessions
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_sessions() {
|
||||||
|
curl -sf --max-time 5 "$PROXY/sessions" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.loads(sys.stdin.read())
|
||||||
|
sessions = d.get('sessions', [])
|
||||||
|
if not sessions:
|
||||||
|
print('No active sessions.')
|
||||||
|
else:
|
||||||
|
for s in sessions:
|
||||||
|
print(f\" {s['id'][:16]}... model={s['model']} msgs={s['messages']} last={s['lastUsed']}\")
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── clear ────────────────────────────────────────────────────────────────
|
||||||
|
cmd_clear_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp clear — Clear all active CLI sessions
|
||||||
|
|
||||||
|
Usage: ocp clear
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_clear() {
|
||||||
|
local result
|
||||||
|
result=$(curl -sf --max-time 5 -X DELETE "$PROXY/sessions")
|
||||||
|
local count
|
||||||
|
count=$(echo "$result" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['cleared'])")
|
||||||
|
echo "Cleared $count sessions."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── restart ──────────────────────────────────────────────────────────────
|
||||||
|
cmd_restart_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp restart — Restart the proxy or gateway
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ocp restart Restart the Claude proxy service
|
||||||
|
ocp restart gateway Restart the OpenClaw gateway
|
||||||
|
(briefly disconnects all Telegram/Discord bots)
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_restart() {
|
||||||
|
if [[ "${1:-}" == "gateway" ]]; then
|
||||||
|
echo "Restarting gateway..."
|
||||||
|
openclaw gateway restart 2>&1
|
||||||
|
else
|
||||||
|
echo "Restarting proxy..."
|
||||||
|
launchctl kickstart -k gui/501/ai.openclaw.proxy 2>/dev/null || {
|
||||||
|
echo "launchctl failed, trying kill + restart..."
|
||||||
|
pkill -f "claude-proxy/server.mjs" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
cd "$HOME/.openclaw/projects/claude-proxy" && nohup node server.mjs >> "$HOME/.openclaw/logs/proxy.log" 2>&1 &
|
||||||
|
}
|
||||||
|
sleep 3
|
||||||
|
if curl -sf --max-time 5 "$PROXY/health" > /dev/null 2>&1; then
|
||||||
|
echo "✓ Proxy restarted successfully."
|
||||||
|
cmd_usage
|
||||||
|
else
|
||||||
|
echo "✗ Proxy not responding after restart."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── settings ─────────────────────────────────────────────────────────────
|
||||||
|
cmd_settings_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp settings — View or update proxy settings at runtime
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ocp settings Show all tunable settings
|
||||||
|
ocp settings <key> <value> Update a setting (no restart needed)
|
||||||
|
|
||||||
|
Tunable keys:
|
||||||
|
timeout Overall request timeout (ms) [30000 - 600000]
|
||||||
|
firstByteTimeout Base first-byte timeout (ms) [15000 - 300000]
|
||||||
|
maxConcurrent Max concurrent claude processes [1 - 32]
|
||||||
|
sessionTTL Session idle expiry (ms) [60000 - 86400000]
|
||||||
|
maxPromptChars Prompt truncation limit (chars) [10000 - 1000000]
|
||||||
|
tiers.opus.base Opus first-byte timeout base (ms) [30000 - 600000]
|
||||||
|
tiers.opus.perChar Opus per-char timeout (ms/char) [0 - 0.01]
|
||||||
|
tiers.sonnet.base Sonnet first-byte timeout base (ms) [30000 - 600000]
|
||||||
|
tiers.sonnet.perChar Sonnet per-char timeout (ms/char) [0 - 0.01]
|
||||||
|
tiers.haiku.base Haiku first-byte timeout base (ms) [15000 - 300000]
|
||||||
|
tiers.haiku.perChar Haiku per-char timeout (ms/char) [0 - 0.01]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
ocp settings maxPromptChars 200000
|
||||||
|
ocp settings tiers.sonnet.base 150000
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_settings() {
|
||||||
|
if [[ -z "${1:-}" ]]; then
|
||||||
|
# GET — show all settings
|
||||||
|
curl -sf --max-time 5 "$PROXY/settings" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.loads(sys.stdin.read())
|
||||||
|
print('OCP Settings')
|
||||||
|
print('─────────────────────────────────────')
|
||||||
|
for k in ('timeout','firstByteTimeout','maxConcurrent','sessionTTL','maxPromptChars'):
|
||||||
|
v = d.get(k, {})
|
||||||
|
val = v.get('value', '?')
|
||||||
|
unit = v.get('unit', '')
|
||||||
|
desc = v.get('desc', '')
|
||||||
|
print(f' {k:<20} {val:>8} {unit:<6} {desc}')
|
||||||
|
t = d.get('tiers', {})
|
||||||
|
print()
|
||||||
|
print('Timeout Tiers (first-byte):')
|
||||||
|
for tier in ('opus','sonnet','haiku'):
|
||||||
|
info = t.get(tier, {})
|
||||||
|
print(f' {tier:<8} base={info.get(\"base\",\"?\"):>6}ms perChar={info.get(\"perPromptChar\",\"?\")}')
|
||||||
|
"
|
||||||
|
elif [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||||
|
cmd_settings_help
|
||||||
|
elif [[ -z "${2:-}" ]]; then
|
||||||
|
echo "Usage: ocp settings <key> <value>"
|
||||||
|
echo "Run 'ocp settings --help' for available keys."
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
# PATCH — update a setting
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
local result
|
||||||
|
result=$(curl -s --max-time 5 -X PATCH "$PROXY/settings" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"$key\": $value}" 2>&1)
|
||||||
|
if echo "$result" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); errs=d.get('errors',[]); exit(1 if errs else 0)" 2>/dev/null; then
|
||||||
|
echo "✓ $key = $value"
|
||||||
|
else
|
||||||
|
echo "$result" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.loads(sys.stdin.read())
|
||||||
|
for e in d.get('errors', []):
|
||||||
|
print(f'✗ {e}')
|
||||||
|
" 2>/dev/null || echo "✗ $result"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── help ─────────────────────────────────────────────────────────────────
|
||||||
|
cmd_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ocp — OpenClaw Proxy CLI
|
||||||
|
|
||||||
|
Usage: ocp <command> [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
usage Plan usage limits
|
||||||
|
status Quick overview (usage + health)
|
||||||
|
health Proxy diagnostics
|
||||||
|
settings View or update tunable settings
|
||||||
|
logs [N] [level] Recent logs (default: 20, error)
|
||||||
|
models Available models
|
||||||
|
sessions Active sessions
|
||||||
|
clear Clear all sessions
|
||||||
|
restart Restart proxy
|
||||||
|
restart gateway Restart gateway
|
||||||
|
|
||||||
|
Run 'ocp <command> --help' for details on a specific command.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── dispatch ─────────────────────────────────────────────────────────────
|
||||||
|
# Check for --help/-h on any subcommand: ocp <cmd> --help
|
||||||
|
subcmd="${1:-help}"
|
||||||
|
shift 2>/dev/null || true
|
||||||
|
|
||||||
|
# Global --help
|
||||||
|
if [[ "$subcmd" == "--help" || "$subcmd" == "-h" || "$subcmd" == "help" ]]; then
|
||||||
|
cmd_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Per-subcommand --help
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
|
||||||
|
fn="cmd_${subcmd}_help"
|
||||||
|
if declare -f "$fn" > /dev/null 2>&1; then
|
||||||
|
"$fn"
|
||||||
|
else
|
||||||
|
echo "No help available for '$subcmd'."
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$subcmd" in
|
||||||
|
usage) cmd_usage ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
health) cmd_health ;;
|
||||||
|
settings) cmd_settings "${1:-}" "${2:-}" ;;
|
||||||
|
logs) cmd_logs "${1:-20}" "${2:-error}" ;;
|
||||||
|
models) cmd_models ;;
|
||||||
|
sessions) cmd_sessions ;;
|
||||||
|
clear) cmd_clear ;;
|
||||||
|
restart) cmd_restart "${1:-}" ;;
|
||||||
|
*) echo "Unknown command: $subcmd"; echo ""; cmd_help; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
/**
|
||||||
|
* OCP Plugin — registers /ocp as a native slash command in OpenClaw gateway.
|
||||||
|
* Calls the local claude-proxy at http://127.0.0.1:3456 and formats the response.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PROXY = "http://127.0.0.1:3456";
|
||||||
|
|
||||||
|
// Wrap output in monospace code block for Telegram/Discord alignment
|
||||||
|
function mono(text) { return "```\n" + text + "\n```"; }
|
||||||
|
|
||||||
|
async function fetchJSON(path) {
|
||||||
|
const resp = await fetch(`${PROXY}${path}`, { signal: AbortSignal.timeout(15000) });
|
||||||
|
if (!resp.ok) throw new Error(`proxy ${resp.status}: ${resp.statusText}`);
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bar(pct, width = 16) {
|
||||||
|
const filled = Math.round(pct * width);
|
||||||
|
return "█".repeat(filled) + "░".repeat(width - filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtMs(ms) {
|
||||||
|
return ms >= 1000 ? `${(ms / 1000).toFixed(0)}s` : `${ms}ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtChars(c) {
|
||||||
|
return c >= 1000 ? `${(c / 1000).toFixed(0)}K` : `${c}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Subcommand handlers ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function cmdUsage() {
|
||||||
|
const d = await fetchJSON("/usage");
|
||||||
|
const plan = d.plan || {};
|
||||||
|
const s = plan.currentSession || {};
|
||||||
|
const w = plan.weeklyLimits?.allModels || {};
|
||||||
|
const e = plan.extraUsage || {};
|
||||||
|
const px = d.proxy;
|
||||||
|
const models = d.models || {};
|
||||||
|
|
||||||
|
let out = "";
|
||||||
|
|
||||||
|
// Show subscription info if available
|
||||||
|
if (plan.subscription) {
|
||||||
|
out += `Plan: ${plan.subscription} (${plan.rateLimitTier || "default"})\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.utilization !== null && s.utilization !== undefined) {
|
||||||
|
// Full plan usage data available (API key mode)
|
||||||
|
out += "Plan Usage Limits\n";
|
||||||
|
out += "─────────────────────────────\n";
|
||||||
|
out += `Current session ${bar(s.utilization)} ${s.percent}\n`;
|
||||||
|
out += ` Resets in ${s.resetsIn} (${s.resetsAtHuman})\n\n`;
|
||||||
|
out += `Weekly (all) ${bar(w.utilization)} ${w.percent}\n`;
|
||||||
|
out += ` Resets in ${w.resetsIn} (${w.resetsAtHuman})\n\n`;
|
||||||
|
out += `Extra usage ${e.status === "allowed" ? "on" : "off"}\n\n`;
|
||||||
|
} else {
|
||||||
|
// No API key — show note
|
||||||
|
out += "Session/weekly %: claude.ai/settings\n";
|
||||||
|
out += "(Anthropic OAuth API doesn't expose limits yet)\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelNames = Object.keys(models).sort();
|
||||||
|
if (modelNames.length > 0) {
|
||||||
|
out += "Model Stats\n";
|
||||||
|
const hdr = `${"Model".padEnd(14)} ${"Req".padStart(4)} ${"OK".padStart(3)} ${"Er".padStart(3)} ${"AvgT".padStart(5)} ${"MaxT".padStart(5)} ${"AvgP".padStart(5)} ${"MaxP".padStart(5)}`;
|
||||||
|
out += hdr + "\n";
|
||||||
|
out += "─".repeat(hdr.length) + "\n";
|
||||||
|
let total = 0;
|
||||||
|
for (const name of modelNames) {
|
||||||
|
const m = models[name];
|
||||||
|
total += m.requests;
|
||||||
|
const short = name.replace("claude-", "").replace("-4-5-20251001", "").replace("-4-6", "");
|
||||||
|
out += `${short.padEnd(14)} ${String(m.requests).padStart(4)} ${String(m.successes).padStart(3)} ${String(m.errors).padStart(3)} ${fmtMs(m.avgElapsed).padStart(5)} ${fmtMs(m.maxElapsed).padStart(5)} ${fmtChars(m.avgPromptChars).padStart(5)} ${fmtChars(m.maxPromptChars).padStart(5)}\n`;
|
||||||
|
}
|
||||||
|
out += `${"Total".padEnd(14)} ${String(total).padStart(4)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
out += `\nProxy: up ${px.uptime} | ${px.totalRequests} reqs | ${px.errors} err | ${px.timeouts} timeout`;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdHealth() {
|
||||||
|
const d = await fetchJSON("/health");
|
||||||
|
let out = `Status: ${d.status} | v${d.version}\n`;
|
||||||
|
out += `Uptime: ${d.uptimeHuman}\n`;
|
||||||
|
out += `Auth: ${d.auth?.ok ? "ok" : d.auth?.message || "unknown"}\n`;
|
||||||
|
out += `Binary: ${d.claudeBinaryOk ? "ok" : "missing"}\n`;
|
||||||
|
out += `Sessions: ${d.sessions?.length || 0} active\n`;
|
||||||
|
out += `Requests: ${d.stats?.totalRequests || 0} total, ${d.stats?.activeRequests || 0} active\n`;
|
||||||
|
out += `Errors: ${d.stats?.errors || 0} | Timeouts: ${d.stats?.timeouts || 0}\n`;
|
||||||
|
if (d.recentErrors?.length) {
|
||||||
|
out += "\nRecent errors:\n";
|
||||||
|
for (const e of d.recentErrors.slice(-3)) {
|
||||||
|
out += ` ${e.time?.slice(11, 19) || "?"} ${e.message}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdStatus() {
|
||||||
|
const d = await fetchJSON("/status");
|
||||||
|
const icon = d.proxy?.status === "ok" ? "🟢" : d.proxy?.status === "degraded" ? "🟡" : "🔴";
|
||||||
|
let out = `${icon} ${d.proxy?.status} | v${d.proxy?.version} | up ${d.proxy?.uptime} | auth ${d.proxy?.auth}\n`;
|
||||||
|
out += `Sessions: ${d.proxy?.activeSessions || 0}\n`;
|
||||||
|
out += `Requests: ${d.requests?.total || 0} | active ${d.requests?.active || 0} | err ${d.requests?.errors || 0} | timeout ${d.requests?.timeouts || 0}\n`;
|
||||||
|
if (d.plan?.currentSession) {
|
||||||
|
out += `\nSession: ${d.plan.currentSession.percent} (resets ${d.plan.currentSession.resetsIn})\n`;
|
||||||
|
out += `Weekly: ${d.plan.weeklyLimits?.allModels?.percent} (resets ${d.plan.weeklyLimits?.allModels?.resetsIn})`;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdSettings(args) {
|
||||||
|
if (!args) {
|
||||||
|
const d = await fetchJSON("/settings");
|
||||||
|
let out = "OCP Settings\n─────────────────────────────\n";
|
||||||
|
for (const k of ["timeout", "firstByteTimeout", "maxConcurrent", "sessionTTL", "maxPromptChars"]) {
|
||||||
|
const v = d[k];
|
||||||
|
if (v) out += `${k.padEnd(20)} ${String(v.value).padStart(8)} ${(v.unit || "").padEnd(6)} ${v.desc}\n`;
|
||||||
|
}
|
||||||
|
if (d.tiers) {
|
||||||
|
out += "\nTimeout tiers:\n";
|
||||||
|
for (const t of ["opus", "sonnet", "haiku"]) {
|
||||||
|
const info = d.tiers[t];
|
||||||
|
if (info) out += ` ${t.padEnd(8)} base=${info.base}ms perChar=${info.perPromptChar}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse "key value"
|
||||||
|
const parts = args.trim().split(/\s+/);
|
||||||
|
if (parts.length < 2 || parts[0] === "--help" || parts[0] === "-h") {
|
||||||
|
return "Usage: /ocp settings <key> <value>\nKeys: timeout, firstByteTimeout, maxConcurrent, sessionTTL, maxPromptChars, tiers.opus.base, tiers.sonnet.base, tiers.haiku.base, tiers.*.perChar";
|
||||||
|
}
|
||||||
|
const [key, val] = parts;
|
||||||
|
const numVal = Number(val);
|
||||||
|
if (isNaN(numVal)) return `Error: value must be a number, got "${val}"`;
|
||||||
|
|
||||||
|
const resp = await fetch(`${PROXY}/settings`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ [key]: numVal }),
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
const d = await resp.json();
|
||||||
|
if (d.errors?.length) return `✗ ${d.errors.join("; ")}`;
|
||||||
|
return `✓ ${key} = ${numVal}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdModels() {
|
||||||
|
const d = await fetchJSON("/v1/models");
|
||||||
|
return (d.data || []).map((m) => ` ${m.id}`).join("\n") || "No models.";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdSessions() {
|
||||||
|
const d = await fetchJSON("/sessions");
|
||||||
|
if (!d.sessions?.length) return "No active sessions.";
|
||||||
|
return d.sessions.map((s) => ` ${s.id.slice(0, 16)}… model=${s.model} msgs=${s.messages}`).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdClear() {
|
||||||
|
const resp = await fetch(`${PROXY}/sessions`, { method: "DELETE", signal: AbortSignal.timeout(5000) });
|
||||||
|
const d = await resp.json();
|
||||||
|
return `Cleared ${d.cleared} sessions.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdVersion() {
|
||||||
|
const d = await fetchJSON("/health");
|
||||||
|
return `OCP v${d.version || "?"}\nUptime: ${d.uptimeHuman || "?"}\nNode: ${process.version}\nPlatform: ${process.platform} ${process.arch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdTest() {
|
||||||
|
const t0 = Date.now();
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${PROXY}/v1/chat/completions`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "claude-haiku-4-5-20251001",
|
||||||
|
messages: [{ role: "user", content: "say ok" }],
|
||||||
|
max_tokens: 5,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(30000),
|
||||||
|
});
|
||||||
|
const d = await resp.json();
|
||||||
|
const elapsed = Date.now() - t0;
|
||||||
|
if (d.choices?.[0]?.message?.content) {
|
||||||
|
return `✓ Proxy OK (${elapsed}ms)\n Model: haiku\n Response: "${d.choices[0].message.content.slice(0, 50)}"`;
|
||||||
|
}
|
||||||
|
return `✗ Unexpected response: ${JSON.stringify(d).slice(0, 100)}`;
|
||||||
|
} catch (e) {
|
||||||
|
return `✗ Test failed (${Date.now() - t0}ms): ${e.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdRestart(args) {
|
||||||
|
const target = (args || "").trim().toLowerCase();
|
||||||
|
const { execSync } = await import("node:child_process");
|
||||||
|
try {
|
||||||
|
if (target === "gateway") {
|
||||||
|
execSync("launchctl kickstart -k gui/501/ai.openclaw.gateway", { timeout: 15000 });
|
||||||
|
return "✓ Gateway restarted";
|
||||||
|
} else if (target === "all") {
|
||||||
|
execSync("launchctl kickstart -k gui/501/ai.openclaw.proxy", { timeout: 15000 });
|
||||||
|
// Gateway restart will kill this plugin too, so do it last
|
||||||
|
execSync("launchctl kickstart -k gui/501/ai.openclaw.gateway", { timeout: 15000 });
|
||||||
|
return "✓ Proxy + Gateway restarted";
|
||||||
|
} else {
|
||||||
|
execSync("launchctl kickstart -k gui/501/ai.openclaw.proxy", { timeout: 15000 });
|
||||||
|
return "✓ Proxy restarted";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Try systemd for Linux
|
||||||
|
try {
|
||||||
|
if (target === "gateway") {
|
||||||
|
execSync("systemctl --user restart openclaw-gateway", { timeout: 15000 });
|
||||||
|
return "✓ Gateway restarted";
|
||||||
|
} else {
|
||||||
|
execSync("systemctl --user restart openclaw-proxy 2>/dev/null || pkill -f 'node.*server.mjs' && sleep 2 && cd ~/.openclaw/projects/*/; node server.mjs &", { timeout: 15000, shell: true });
|
||||||
|
return "✓ Proxy restarted";
|
||||||
|
}
|
||||||
|
} catch (e2) {
|
||||||
|
return `✗ Restart failed: ${e2.message?.slice(0, 100)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdLogs(args) {
|
||||||
|
const parts = (args || "").trim().split(/\s+/);
|
||||||
|
const n = parseInt(parts[0]) || 20;
|
||||||
|
const level = parts[1] || "error";
|
||||||
|
const d = await fetchJSON(`/logs?n=${n}&level=${level}`);
|
||||||
|
if (!d.entries?.length) return `No ${level} log entries.`;
|
||||||
|
return d.entries.map((e) => {
|
||||||
|
if (e.raw) return e.raw.slice(0, 120);
|
||||||
|
return `${(e.ts || "").slice(11, 19)} ${(e.level || "").toUpperCase()} ${e.event || "?"} ${e.model || ""}`;
|
||||||
|
}).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmdHelp() {
|
||||||
|
return `OCP Commands
|
||||||
|
─────────────────────────────
|
||||||
|
/ocp usage Plan usage & model stats
|
||||||
|
/ocp status Quick overview
|
||||||
|
/ocp health Proxy diagnostics
|
||||||
|
/ocp settings View tunable settings
|
||||||
|
/ocp settings <k> <v> Update a setting
|
||||||
|
/ocp logs [N] [level] Recent logs (default: 20, error)
|
||||||
|
/ocp models Available models
|
||||||
|
/ocp sessions Active sessions
|
||||||
|
/ocp clear Clear all sessions
|
||||||
|
/ocp restart Restart proxy
|
||||||
|
/ocp restart gateway Restart gateway
|
||||||
|
/ocp restart all Restart both
|
||||||
|
/ocp version Version & platform info
|
||||||
|
/ocp test End-to-end proxy test`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Plugin entry point ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function (api) {
|
||||||
|
console.log("[ocp] OCP plugin loading, registering /ocp command...");
|
||||||
|
api.registerCommand({
|
||||||
|
name: "ocp",
|
||||||
|
description: "OpenClaw Proxy commands — usage, health, settings, logs, etc.",
|
||||||
|
acceptsArgs: true,
|
||||||
|
requireAuth: true,
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const raw = (ctx.args || "").trim();
|
||||||
|
const spaceIdx = raw.indexOf(" ");
|
||||||
|
const subcmd = spaceIdx === -1 ? raw : raw.slice(0, spaceIdx);
|
||||||
|
const subargs = spaceIdx === -1 ? "" : raw.slice(spaceIdx + 1).trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let text;
|
||||||
|
switch (subcmd) {
|
||||||
|
case "usage": text = await cmdUsage(); break;
|
||||||
|
case "health": text = await cmdHealth(); break;
|
||||||
|
case "status": text = await cmdStatus(); break;
|
||||||
|
case "settings": text = await cmdSettings(subargs || null); break;
|
||||||
|
case "models": text = await cmdModels(); break;
|
||||||
|
case "sessions": text = await cmdSessions(); break;
|
||||||
|
case "clear": text = await cmdClear(); break;
|
||||||
|
case "restart": text = await cmdRestart(subargs); break;
|
||||||
|
case "version": text = await cmdVersion(); break;
|
||||||
|
case "test": text = await cmdTest(); break;
|
||||||
|
case "logs": text = await cmdLogs(subargs); break;
|
||||||
|
case "help": case "--help": case "-h": case "":
|
||||||
|
text = cmdHelp(); break;
|
||||||
|
default:
|
||||||
|
text = `Unknown subcommand: ${subcmd}\n\n${cmdHelp()}`;
|
||||||
|
}
|
||||||
|
return { text: mono(text) };
|
||||||
|
} catch (err) {
|
||||||
|
return { text: `OCP error: ${err.message}` };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"id": "ocp",
|
||||||
|
"name": "OCP Commands",
|
||||||
|
"description": "Slash commands for the OpenClaw Proxy — /ocp usage, /ocp settings, /ocp health, etc.",
|
||||||
|
"version": "3.1.0",
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"proxyUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "http://127.0.0.1:3456",
|
||||||
|
"description": "URL of the Claude proxy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "ocp",
|
||||||
|
"version": "3.1.0",
|
||||||
|
"description": "Slash commands for the OpenClaw Proxy",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"keywords": ["openclaw", "plugin", "ocp", "proxy"],
|
||||||
|
"license": "MIT",
|
||||||
|
"openclaw": {
|
||||||
|
"type": "plugin",
|
||||||
|
"id": "ocp",
|
||||||
|
"pluginManifest": "openclaw.plugin.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-4
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "openclaw-claude-proxy",
|
"name": "openclaw-claude-proxy",
|
||||||
"version": "2.4.0",
|
"version": "3.1.0",
|
||||||
"description": "OpenAI-compatible proxy for Claude CLI v2 — per-model circuit breaker, adaptive first-byte timeout, structured logging",
|
"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",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"openclaw-claude-proxy": "./server.mjs"
|
"openclaw-claude-proxy": "./server.mjs",
|
||||||
|
"ocp": "./ocp"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.mjs",
|
"start": "node server.mjs",
|
||||||
@@ -23,6 +24,6 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/dtzp555-max/openclaw-claude-proxy"
|
"url": "https://github.com/dtzp555-max/ocp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+856
-234
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
|||||||
* 4. Creates start.sh for easy launch
|
* 4. Creates start.sh for easy launch
|
||||||
* 5. Optionally starts the proxy
|
* 5. Optionally starts the proxy
|
||||||
*/
|
*/
|
||||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { join, dirname } from "node:path";
|
import { join, dirname } from "node:path";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
@@ -291,20 +291,47 @@ if (!DRY_RUN) {
|
|||||||
const logsDir = join(OPENCLAW_DIR, "logs");
|
const logsDir = join(OPENCLAW_DIR, "logs");
|
||||||
if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
|
if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Use neutral service names to avoid OpenClaw gateway's extra-service detection.
|
||||||
|
// OpenClaw scans LaunchAgent plists and systemd units for "openclaw" / "clawdbot"
|
||||||
|
// markers and flags them as conflicting gateway-like services. Using "dev.ocp.*"
|
||||||
|
// and "ocp-proxy" keeps the proxy invisible to that heuristic.
|
||||||
|
const OCP_HOME = join(HOME, ".ocp");
|
||||||
|
const ocpLogsDir = join(OCP_HOME, "logs");
|
||||||
|
if (!existsSync(ocpLogsDir)) mkdirSync(ocpLogsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Uninstall legacy service names if present (upgrade path)
|
||||||
|
if (platform === "darwin") {
|
||||||
|
const legacyPlist = join(HOME, "Library", "LaunchAgents", "ai.openclaw.proxy.plist");
|
||||||
|
if (existsSync(legacyPlist)) {
|
||||||
|
try { execSync(`launchctl bootout gui/$(id -u) "${legacyPlist}" 2>/dev/null`); } catch { /* ignore */ }
|
||||||
|
try { unlinkSync(legacyPlist); } catch { /* ignore */ }
|
||||||
|
log(`Removed legacy plist: ai.openclaw.proxy`);
|
||||||
|
}
|
||||||
|
} else if (platform === "linux") {
|
||||||
|
const legacyService = join(HOME, ".config", "systemd", "user", "openclaw-proxy.service");
|
||||||
|
if (existsSync(legacyService)) {
|
||||||
|
try { execSync(`systemctl --user stop openclaw-proxy 2>/dev/null`); } catch { /* ignore */ }
|
||||||
|
try { execSync(`systemctl --user disable openclaw-proxy 2>/dev/null`); } catch { /* ignore */ }
|
||||||
|
try { unlinkSync(legacyService); } catch { /* ignore */ }
|
||||||
|
try { execSync(`systemctl --user daemon-reload`); } catch { /* ignore */ }
|
||||||
|
log(`Removed legacy systemd service: openclaw-proxy`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (platform === "darwin") {
|
if (platform === "darwin") {
|
||||||
// macOS: launchd
|
// macOS: launchd
|
||||||
const plistDir = join(HOME, "Library", "LaunchAgents");
|
const plistDir = join(HOME, "Library", "LaunchAgents");
|
||||||
if (!existsSync(plistDir)) mkdirSync(plistDir, { recursive: true });
|
if (!existsSync(plistDir)) mkdirSync(plistDir, { recursive: true });
|
||||||
|
|
||||||
const plistPath = join(plistDir, "ai.openclaw.proxy.plist");
|
const plistPath = join(plistDir, "dev.ocp.proxy.plist");
|
||||||
const logPath = join(logsDir, "proxy.log");
|
const logPath = join(ocpLogsDir, "proxy.log");
|
||||||
|
|
||||||
const plistXml = `<?xml version="1.0" encoding="UTF-8"?>
|
const plistXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>Label</key>
|
<key>Label</key>
|
||||||
<string>ai.openclaw.proxy</string>
|
<string>dev.ocp.proxy</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>${nodeBin}</string>
|
<string>${nodeBin}</string>
|
||||||
@@ -330,27 +357,28 @@ if (!DRY_RUN) {
|
|||||||
writeFileSync(plistPath, plistXml);
|
writeFileSync(plistPath, plistXml);
|
||||||
log(`Plist written: ${plistPath}`);
|
log(`Plist written: ${plistPath}`);
|
||||||
|
|
||||||
// Unload first (in case it was already loaded) then load
|
// Bootout first (in case it was already loaded) then bootstrap
|
||||||
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`); } catch { /* ignore */ }
|
try { execSync(`launchctl bootout gui/$(id -u) "${plistPath}" 2>/dev/null`); } catch { /* ignore */ }
|
||||||
execSync(`launchctl load "${plistPath}"`);
|
execSync(`launchctl bootstrap gui/$(id -u) "${plistPath}"`);
|
||||||
log(`launchctl loaded ai.openclaw.proxy`);
|
log(`launchctl loaded dev.ocp.proxy`);
|
||||||
|
|
||||||
} else if (platform === "linux") {
|
} else if (platform === "linux") {
|
||||||
// Linux: systemd user service
|
// Linux: systemd user service
|
||||||
const systemdDir = join(HOME, ".config", "systemd", "user");
|
const systemdDir = join(HOME, ".config", "systemd", "user");
|
||||||
if (!existsSync(systemdDir)) mkdirSync(systemdDir, { recursive: true });
|
if (!existsSync(systemdDir)) mkdirSync(systemdDir, { recursive: true });
|
||||||
|
|
||||||
const servicePath = join(systemdDir, "openclaw-proxy.service");
|
const servicePath = join(systemdDir, "ocp-proxy.service");
|
||||||
const logPath = join(logsDir, "proxy.log");
|
const logPath = join(ocpLogsDir, "proxy.log");
|
||||||
|
|
||||||
const serviceUnit = `[Unit]
|
const serviceUnit = `[Unit]
|
||||||
Description=OpenClaw Claude Proxy
|
Description=OCP — Open Claude Proxy
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=${nodeBin} ${serverPath}
|
ExecStart=${nodeBin} ${serverPath}
|
||||||
Environment=CLAUDE_PROXY_PORT=${PORT}
|
Environment=CLAUDE_PROXY_PORT=${PORT}
|
||||||
Restart=always
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
StandardOutput=append:${logPath}
|
StandardOutput=append:${logPath}
|
||||||
StandardError=append:${logPath}
|
StandardError=append:${logPath}
|
||||||
|
|
||||||
@@ -362,8 +390,8 @@ WantedBy=default.target
|
|||||||
log(`Service file written: ${servicePath}`);
|
log(`Service file written: ${servicePath}`);
|
||||||
|
|
||||||
execSync(`systemctl --user daemon-reload`);
|
execSync(`systemctl --user daemon-reload`);
|
||||||
execSync(`systemctl --user enable openclaw-proxy`);
|
execSync(`systemctl --user enable ocp-proxy`);
|
||||||
execSync(`systemctl --user start openclaw-proxy`);
|
execSync(`systemctl --user start ocp-proxy`);
|
||||||
log(`systemd user service enabled and started`);
|
log(`systemd user service enabled and started`);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
PORT=${CLAUDE_PROXY_PORT:-3456}
|
PORT=${CLAUDE_PROXY_PORT:-3456}
|
||||||
if ! lsof -i :$PORT -sTCP:LISTEN &>/dev/null; then
|
if ! lsof -i :$PORT -sTCP:LISTEN &>/dev/null; then
|
||||||
unset CLAUDECODE
|
unset CLAUDECODE
|
||||||
nohup node "/Users/taodeng/.openclaw/projects/claude-proxy/openclaw-claude-proxy/server.mjs" \
|
nohup node "/Users/taodeng/.openclaw/projects/claude-proxy/server.mjs" \
|
||||||
>> "/Users/taodeng/.openclaw/logs/claude-proxy.log" \
|
>> "/Users/taodeng/.openclaw/logs/claude-proxy.log" \
|
||||||
2>> "/Users/taodeng/.openclaw/logs/claude-proxy.err.log" &
|
2>> "/Users/taodeng/.openclaw/logs/claude-proxy.err.log" &
|
||||||
echo "claude-proxy started on port $PORT (pid $!)"
|
echo "claude-proxy started on port $PORT (pid $!)"
|
||||||
|
|||||||
+35
-23
@@ -3,6 +3,9 @@
|
|||||||
* openclaw-claude-proxy uninstaller
|
* openclaw-claude-proxy uninstaller
|
||||||
*
|
*
|
||||||
* Stops and removes the launchd (macOS) or systemd (Linux) auto-start entry.
|
* Stops and removes the launchd (macOS) or systemd (Linux) auto-start entry.
|
||||||
|
* Handles both legacy (ai.openclaw.proxy / openclaw-proxy) and current
|
||||||
|
* (dev.ocp.proxy / ocp-proxy) service names.
|
||||||
|
*
|
||||||
* Run: node uninstall.mjs
|
* Run: node uninstall.mjs
|
||||||
*/
|
*/
|
||||||
import { existsSync, unlinkSync } from "node:fs";
|
import { existsSync, unlinkSync } from "node:fs";
|
||||||
@@ -15,43 +18,52 @@ const HOME = homedir();
|
|||||||
function log(msg) { console.log(` ✓ ${msg}`); }
|
function log(msg) { console.log(` ✓ ${msg}`); }
|
||||||
function warn(msg) { console.log(` ⚠ ${msg}`); }
|
function warn(msg) { console.log(` ⚠ ${msg}`); }
|
||||||
|
|
||||||
console.log("\n🗑 Uninstalling openclaw-claude-proxy auto-start...\n");
|
console.log("\n🗑 Uninstalling OCP auto-start...\n");
|
||||||
|
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
|
|
||||||
if (platform === "darwin") {
|
if (platform === "darwin") {
|
||||||
const plistPath = join(HOME, "Library", "LaunchAgents", "ai.openclaw.proxy.plist");
|
// Remove current service
|
||||||
|
const plistPath = join(HOME, "Library", "LaunchAgents", "dev.ocp.proxy.plist");
|
||||||
if (existsSync(plistPath)) {
|
if (existsSync(plistPath)) {
|
||||||
try {
|
try { execSync(`launchctl bootout gui/$(id -u) "${plistPath}" 2>/dev/null`); } catch { /* ignore */ }
|
||||||
execSync(`launchctl unload "${plistPath}" 2>/dev/null`);
|
|
||||||
log("launchd service stopped and unloaded");
|
|
||||||
} catch {
|
|
||||||
warn("launchctl unload failed (service may not have been running)");
|
|
||||||
}
|
|
||||||
unlinkSync(plistPath);
|
unlinkSync(plistPath);
|
||||||
log(`Plist removed: ${plistPath}`);
|
log(`Removed: ${plistPath}`);
|
||||||
} else {
|
}
|
||||||
warn(`Plist not found: ${plistPath}`);
|
|
||||||
|
// Remove legacy service
|
||||||
|
const legacyPath = join(HOME, "Library", "LaunchAgents", "ai.openclaw.proxy.plist");
|
||||||
|
if (existsSync(legacyPath)) {
|
||||||
|
try { execSync(`launchctl bootout gui/$(id -u) "${legacyPath}" 2>/dev/null`); } catch { /* ignore */ }
|
||||||
|
unlinkSync(legacyPath);
|
||||||
|
log(`Removed legacy: ${legacyPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(plistPath) && !existsSync(legacyPath)) {
|
||||||
|
warn("No plist found (service may not have been installed)");
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (platform === "linux") {
|
} else if (platform === "linux") {
|
||||||
const servicePath = join(HOME, ".config", "systemd", "user", "openclaw-proxy.service");
|
// Remove current service
|
||||||
|
const servicePath = join(HOME, ".config", "systemd", "user", "ocp-proxy.service");
|
||||||
try { execSync(`systemctl --user stop openclaw-proxy 2>/dev/null`); } catch { /* ignore */ }
|
try { execSync(`systemctl --user stop ocp-proxy 2>/dev/null`); } catch { /* ignore */ }
|
||||||
log("systemd service stopped");
|
try { execSync(`systemctl --user disable ocp-proxy 2>/dev/null`); } catch { /* ignore */ }
|
||||||
|
|
||||||
try { execSync(`systemctl --user disable openclaw-proxy 2>/dev/null`); } catch { /* ignore */ }
|
|
||||||
log("systemd service disabled");
|
|
||||||
|
|
||||||
if (existsSync(servicePath)) {
|
if (existsSync(servicePath)) {
|
||||||
unlinkSync(servicePath);
|
unlinkSync(servicePath);
|
||||||
log(`Service file removed: ${servicePath}`);
|
log(`Removed: ${servicePath}`);
|
||||||
} else {
|
}
|
||||||
warn(`Service file not found: ${servicePath}`);
|
|
||||||
|
// Remove legacy service
|
||||||
|
const legacyPath = join(HOME, ".config", "systemd", "user", "openclaw-proxy.service");
|
||||||
|
try { execSync(`systemctl --user stop openclaw-proxy 2>/dev/null`); } catch { /* ignore */ }
|
||||||
|
try { execSync(`systemctl --user disable openclaw-proxy 2>/dev/null`); } catch { /* ignore */ }
|
||||||
|
if (existsSync(legacyPath)) {
|
||||||
|
unlinkSync(legacyPath);
|
||||||
|
log(`Removed legacy: ${legacyPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try { execSync(`systemctl --user daemon-reload`); } catch { /* ignore */ }
|
try { execSync(`systemctl --user daemon-reload`); } catch { /* ignore */ }
|
||||||
|
log("systemd daemon reloaded");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
warn(`Auto-start not supported on ${platform} — nothing to remove`);
|
warn(`Auto-start not supported on ${platform} — nothing to remove`);
|
||||||
|
|||||||
Reference in New Issue
Block a user