Compare commits

..
17 Commits
Author SHA1 Message Date
taodengandClaude Opus 4.6 3f10b459f5 feat: add ocp update command + fix restart for new service names
- `ocp update`: pulls latest from GitHub, syncs plugin, restarts proxy
- `ocp update --check`: shows current vs latest version without applying
- `ocp restart`: now handles both dev.ocp.proxy and legacy ai.openclaw.proxy
  service names on macOS/Linux
- Bumps to v3.2.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 08:18:41 +10:00
taodengandClaude Opus 4.6 b72e449337 fix: use neutral service names to avoid OpenClaw gateway detection
OpenClaw's gateway scans LaunchAgent plists and systemd units for
"openclaw" markers and flags them as conflicting gateway-like services.
Renamed service identifiers to avoid false positives:

- macOS: ai.openclaw.proxy → dev.ocp.proxy
- Linux: openclaw-proxy.service → ocp-proxy.service
- Logs: ~/.openclaw/logs/ → ~/.ocp/logs/

Setup auto-removes legacy service names on upgrade. Uninstall handles
both old and new names. Also bumps version to 3.1.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 08:13:55 +10:00
taodengandClaude Opus 4.6 2b05558b8b rebrand: Open Claude Proxy — multi-IDE positioning
Renamed from OpenClaw Control Plane to Open Claude Proxy. README
rewritten to position OCP as a universal proxy for any IDE that
speaks the OpenAI protocol, not just OpenClaw.

Added: supported tools table (Cline, OpenCode, Aider, Continue.dev),
simplified Quick Start, architecture diagram showing multi-IDE flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:06:41 +10:00
taodengandClaude Opus 4.6 d9b7c0d905 docs: add Known Issues — /ocp Unknown skill workaround
Documented the intermittent "Unknown skill: ocp" issue caused by
OpenClaw gateway session routing bug (#26895). Workaround: /new
followed by /ocp restart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:50:17 +10:00
taodengandClaude Opus 4.6 27cd8b1735 fix: correct server.mjs path in start.sh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:38:18 +10:00
taodengandClaude Opus 4.6 9dd35c90ae docs: remove premature Known Issues section
The slash session workaround was incomplete — root cause still under
investigation. Removed to avoid misleading users. Will add back with
accurate info once the issue is fully resolved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:04:38 +10:00
taodengandClaude Opus 4.6 c005a8fd27 remove backends command — OCP is stable v3, no Phase 2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:57:58 +10:00
taodengandClaude Opus 4.6 77a3f48cfd feat: add restart, version, test, backends commands to /ocp plugin
New subcommands:
- /ocp restart [gateway|all] — restart proxy, gateway, or both
- /ocp version — show version, uptime, platform info
- /ocp test — end-to-end proxy test (sends haiku request)
- /ocp backends — list registered backends with health status

Also includes clean-slash-sessions.sh workaround for OpenClaw #26895.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 09:09:23 +10:00
taodengandClaude Opus 4.6 7390a19ec6 docs: add prominent warning about slash session bug near plugin install
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:37:10 +10:00
taodengandClaude Opus 4.6 04643217ae docs: add known issue — slash session hijacks plugin commands
Document OpenClaw gateway bug (#26895, #54485) where telegram:slash
sessions interfere with registerCommand plugin routing. Include
workaround script and warning not to add ocp to agent skills lists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:36:42 +10:00
taodengandClaude Opus 4.6 3de1868313 fix: read OAuth token from Linux credentials file as fallback
On Linux (no macOS keychain), read ~/.claude/.credentials.json first.
Falls through to macOS keychain if file not found. Fixes /ocp usage
showing "No OAuth token" on Linux servers and RPi.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:42:16 +10:00
taodengandClaude Opus 4.6 75cff38f4c docs: improve CLI install instructions for cloud/Linux users
Common issue: ocp not in PATH after clone. Added explicit symlink
instructions with absolute paths and a troubleshooting note.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 19:52:22 +10:00
taodengandClaude Opus 4.6 a8a0af43c9 docs: mark OCP as stable v3.0.0, update repo name and description
- README: add "Status: Stable" badge, rename title to OCP — OpenClaw Control Plane
- package.json: update description and repo URL to dtzp555-max/ocp
- No code changes. Feature-complete, bug fixes only going forward.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 19:49:16 +10:00
taodengandClaude Opus 4.6 77a6f8ed59 revert: rollback Phase 1 and OAuth fix — broke plan usage functionality
Reverts b5be1c0 and 1f1b387. These changes broke the OAuth token
authentication for fetching plan usage data from Anthropic API,
causing /ocp usage to show 0% with "unknown" reset times.

Root cause: code changes inadvertently altered the fetchUsageFromApi
flow, breaking the existing working OAuth x-api-key authentication.

This restores server.mjs to the last known working state (f6f4f68).
Phase 1 modular files removed — will be re-implemented in a separate
branch with proper isolation testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 18:16:04 +10:00
taodengandClaude Opus 4.6 b5be1c04f4 fix: auto-refresh expired OAuth token for plan usage data (v3.0.1)
When the Claude Code OAuth token expires, /ocp usage showed 0% for all
plan limits. OCP now detects expiry and triggers a refresh via Claude CLI,
then re-reads the updated token from keychain. The 5-minute usage cache
prevents excessive refresh attempts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 16:18:15 +10:00
taodengandClaude Opus 4.6 1f1b3871da feat: Phase 1 — modular backend architecture (v4.0-alpha)
Internal refactor: extract BackendAdapter, ModelRegistry, AgentRouter,
SessionManager, and StatsCollector as standalone modules. Claude CLI
logic encapsulated in ClaudeCliAdapter.

External API completely unchanged — all existing endpoints behave
identically. Legacy request path (callClaude/callClaudeStreaming)
untouched. v4 modules load in parallel for validation.

New endpoints:
  GET /backends  — backend health and status
  GET /routing   — agent routing table

Architecture:
  unified-chunk.mjs    — streaming protocol (delta/done/error)
  backends/base.mjs    — BackendAdapter interface
  backends/claude-cli.mjs — Claude CLI adapter (core)
  model-registry.mjs   — model → backend mapping (Layer 1)
  agent-router.mjs     — agent policy routing (Layer 2)
  session-manager.mjs  — conversation session management
  stats-collector.mjs  — per-model/backend/agent metrics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:02:53 +10:00
taodengandClaude Opus 4.6 f6f4f68ecd Add upgrade guide for v2.x skill-based /ocp users
Explains removing old skills/ocp/SKILL.md to avoid conflict
with the new plugin-based command handler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:07:29 +10:00
10 changed files with 417 additions and 253 deletions
+116 -195
View File
@@ -1,42 +1,89 @@
# openclaw-claude-proxy (OCP) # 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. Now with built-in plan usage monitoring, runtime settings, and a CLI. > **Already paying for Claude Pro/Max? Use your subscription as an OpenAI-compatible API — $0 extra cost.**
## What's New in v3.0.0 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.
### `/ocp` — Your Proxy Command Center ```
Cline ──┐
OpenCode ───┤
Aider ───┼──→ OCP :3456 ──→ Claude CLI ──→ Your subscription
Continue.dev ───┤
OpenClaw ───┘
```
Full management interface available from Telegram, Discord, or any terminal. One proxy. Multiple IDEs. All models. **$0 API cost.**
## Supported Tools
Any tool that accepts `OPENAI_BASE_URL` works with OCP:
| 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
git clone https://github.com/dtzp555-max/ocp.git
cd ocp
node setup.mjs
```
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)
Then point your IDE to 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
```
## Built-in Usage Monitoring
Check your subscription usage from the terminal:
``` ```
$ ocp usage $ ocp usage
Plan Usage Limits Plan Usage Limits
───────────────────────────────────── ─────────────────────────────────────
Current session 3% used Current session 21% used
Resets in 4h 32m (Tue, Mar 24, 10:00 PM) Resets in 3h 12m (Tue, Mar 28, 10:00 PM)
Weekly (all models) 3% used Weekly (all models) 45% used
Resets in 6d 6h (Tue, Mar 31, 12:00 AM) Resets in 4d 2h (Tue, Mar 31, 12:00 AM)
Extra usage off Extra usage off
Model Stats Model Stats
Model Req OK Er AvgT MaxT AvgP MaxP Model Req OK Er AvgT MaxT AvgP MaxP
────────────────────────────────────────────────────── ──────────────────────────────────────────────────────
haiku 1 1 0 6s 6s 0K 0K opus 5 5 0 32s 87s 42K 43K
opus 2 2 0 20s 26s 42K 43K sonnet 18 18 0 20s 45s 36K 56K
sonnet 2 2 0 24s 24s 41K 41K Total 23
Total 5
Proxy: up 0h 37m | 5 reqs | 0 err | 0 timeout Proxy: up 6h 32m | 23 reqs | 0 err | 0 timeout
``` ```
**All commands:** ### All Commands
``` ```
$ ocp --help
ocp usage Plan usage limits & model stats ocp usage Plan usage limits & model stats
ocp status Quick overview ocp status Quick overview
ocp health Proxy diagnostics ocp health Proxy diagnostics
@@ -48,109 +95,73 @@ ocp sessions Active sessions
ocp clear Clear all sessions ocp clear Clear all sessions
ocp restart Restart proxy ocp restart Restart proxy
ocp restart gateway Restart gateway ocp restart gateway Restart gateway
ocp version Show version
ocp --help Command reference
``` ```
In **Telegram/Discord**, use `/ocp usage`, `/ocp settings`, etc. — registered as a native slash command via the OCP gateway plugin. ### Install the CLI
```bash
# Symlink to PATH (recommended)
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) ### Runtime Settings (No Restart Needed)
```
$ ocp settings
OCP Settings
─────────────────────────────────────
timeout 300000 ms Overall request timeout
firstByteTimeout 90000 ms Base first-byte timeout
maxConcurrent 8 Max concurrent claude processes
sessionTTL 3600000 ms Session idle expiry
maxPromptChars 150000 chars Prompt truncation limit
Timeout Tiers (first-byte):
opus base=150000ms perChar=0.0005
sonnet base=120000ms perChar=0.0005
haiku base= 45000ms perChar=0.0001
```
Change any setting live:
``` ```
$ ocp settings maxPromptChars 200000 $ ocp settings maxPromptChars 200000
✓ maxPromptChars = 200000 ✓ maxPromptChars = 200000
$ ocp settings maxConcurrent 999 $ ocp settings maxConcurrent 4
maxConcurrent: value 999 out of range [1, 32] maxConcurrent = 4
``` ```
### Circuit Breaker Removed
The v2.5.0 circuit breaker has been **removed entirely**. It was designed for direct API connections but caused cascading failures in the CLI-proxy architecture — once API got briefly slow, the breaker blocked ALL agents for 120s+, making the problem worse. With CLI spawning, timeouts are transient and don't benefit from back-off.
### Prompt Truncation Guard
New safety valve prevents runaway context from conversation history accumulation (a recurring issue where prompts balloon from 40K to 400K+ chars).
- Default limit: **150K characters** (configurable via `maxPromptChars`)
- When exceeded: keeps system messages + as many recent messages as fit
- Logs `prompt_truncated` events for monitoring
### Increased Timeouts
| Model | Old Base | New Base | Per 100K chars |
|-------|----------|----------|----------------|
| Opus | 90s | **150s** | +50s |
| Sonnet | 60s | **120s** | +50s |
| Haiku | 30s | **45s** | +10s |
---
## How It Works ## How It Works
``` ```
OpenClaw Gateway → proxy (localhost:3456) → claude -p CLI → Anthropic (via OAuth) Your IDE → OCP (localhost:3456) → claude -p CLI → Anthropic (via subscription)
``` ```
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. 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.
## Quick Start ## Available Models
```bash | Model ID | Notes |
git clone https://github.com/dtzp555-max/openclaw-claude-proxy.git |----------|-------|
cd openclaw-claude-proxy | `claude-opus-4-6` | Most capable, slower |
| `claude-sonnet-4-6` | Good balance of speed/quality |
| `claude-haiku-4` | Fastest, lightweight |
# Auto-configure OpenClaw + start proxy + install auto-start ## API Endpoints
node setup.mjs
```
The setup script will: | Endpoint | Method | Description |
1. Verify Claude CLI is installed and authenticated |----------|--------|-------------|
2. Add `claude-local` provider to `openclaw.json` | `/v1/models` | GET | List available models |
3. Start the proxy and install auto-start (launchd on macOS, systemd on Linux) | `/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 |
Then set your preferred model: ## OpenClaw Integration
```bash
openclaw config set agents.defaults.model.primary "claude-local/claude-sonnet-4-6"
openclaw gateway restart
```
### Install the CLI OCP was originally built for [OpenClaw](https://github.com/openclaw/openclaw) and includes deep integration:
The `ocp` command is included in the repo: - **`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
```bash ### Install the Gateway Plugin
# Option 1: symlink to PATH
ln -sf $(pwd)/ocp /usr/local/bin/ocp
# Option 2: npm link (if installed globally)
npm link
```
### Install the Gateway Plugin (for Telegram/Discord)
Copy the plugin to the OpenClaw extensions directory:
```bash ```bash
cp -r ocp-plugin/ ~/.openclaw/extensions/ocp/ cp -r ocp-plugin/ ~/.openclaw/extensions/ocp/
# Or into the bundled extensions:
cp -r ocp-plugin/ /opt/homebrew/lib/node_modules/openclaw/dist/extensions/ocp/
``` ```
Add to `~/.openclaw/openclaw.json`: Add to `~/.openclaw/openclaw.json`:
@@ -163,30 +174,7 @@ Add to `~/.openclaw/openclaw.json`:
} }
``` ```
Restart the gateway: `openclaw gateway restart` Restart: `openclaw gateway restart`
## 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 | View tunable settings |
| `/settings` | PATCH | Update settings at runtime |
| `/logs` | GET | Recent log entries (`?n=20&level=error`) |
| `/sessions` | GET | List active sessions |
| `/sessions` | DELETE | Clear all sessions |
## Available Models
| Model ID | Claude CLI Model | Notes |
|----------|-----------------|-------|
| `claude-opus-4-6` | claude-opus-4-6 | Most capable, slower |
| `claude-sonnet-4-6` | claude-sonnet-4-6 | Good balance of speed/quality |
| `claude-haiku-4` | claude-haiku-4-5-20251001 | Fastest, lightweight |
## Environment Variables ## Environment Variables
@@ -201,95 +189,28 @@ Restart the gateway: `openclaw gateway restart`
| `CLAUDE_SESSION_TTL` | `3600000` | Session expiry (ms, default: 1 hour) | | `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` | 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 |
| `PROXY_API_KEY` | *(unset)* | Bearer token for API authentication | | `PROXY_API_KEY` | *(unset)* | Bearer token for API authentication |
## Session Management
Multi-turn conversations use `--resume` to avoid resending full history on every request.
Include a `session_id` field in the request body or `X-Session-Id` header:
```json
{
"model": "claude-sonnet-4-6",
"session_id": "conv-abc-123",
"messages": [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi!"},
{"role": "user", "content": "What did I just say?"}
]
}
```
Sessions expire after 1 hour of inactivity (configurable via `CLAUDE_SESSION_TTL`).
## Security ## Security
- **Localhost only** — binds to `127.0.0.1`, not exposed to the network - **Localhost only** — binds to `127.0.0.1`, not exposed to the network
- **Bearer token auth (optional)** — set `PROXY_API_KEY` to require auth on all requests except `/health` - **Bearer token auth (optional)** — set `PROXY_API_KEY` to require auth
- **No API keys** — authentication to Anthropic goes through Claude CLI's OAuth session - **No API keys needed** — authentication goes through Claude CLI's OAuth session
- **Auto-start** — launchd (macOS) / systemd (Linux) via `node setup.mjs` - **Auto-start** — launchd (macOS) / systemd (Linux)
- **Remove auto-start**: `node uninstall.mjs`
## Architecture ## Known Issues
| | v1.x (pool) | v2.0+ (on-demand) | ### `/ocp` command returns "Unknown skill: ocp" (OpenClaw only)
|---|---|---|
| Process lifecycle | Pre-spawn idle workers | Spawn per request |
| Crash handling | Backoff spiral | No crash loops |
| Session support | None | `--resume` with tracking |
| Tool access | 6 hardcoded | Configurable, expanded |
| Prompt guard | None | Truncation at 150K chars |
| Monitoring | Basic `/health` | `/usage`, `/status`, `/settings`, `/logs` |
| CLI | None | `ocp` command |
| Gateway plugin | None | `/ocp` slash command |
## Coexistence with Claude Code 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)).
OCP and Claude Code interactive mode are completely independent: **Workaround:**
```
| | OCP (this proxy) | Claude Code | /new
|---|---|---| /ocp restart
| Protocol | HTTP (localhost:3456) | MCP (in-process) |
| Process model | Per-request spawn | Persistent session |
| Lifecycle | Daemon (auto-start) | Requires terminal |
| Use case | Automated agent work | Human-in-the-loop |
Both run on the same machine simultaneously. No shared state, no port conflicts.
## Recovery After OpenClaw Upgrade
```bash
cd ~/.openclaw/projects/claude-proxy
git pull
node setup.mjs
openclaw gateway restart
``` ```
## Changelog **Important:** Do not add `ocp` to agent `skills` lists or place a `SKILL.md` in workspace skills — this creates a routing conflict.
### v3.0.0 (2026-03-24)
- **`/ocp` CLI** — full management from terminal (`ocp usage`, `ocp settings`, etc.)
- **`/ocp` gateway plugin** — native slash command in Telegram/Discord
- **Plan usage monitoring** — real-time session/weekly limits via Anthropic API rate-limit headers
- **Per-model stats** — request count, avg/max elapsed time, avg/max prompt size
- **Runtime settings** — `PATCH /settings` to tune timeouts, concurrency, prompt limits without restart
- **Prompt truncation** — auto-truncate prompts exceeding 150K chars to prevent timeout cascades
- **Circuit breaker removed** — caused more harm than good in CLI-proxy architecture
- **Timeout increases** — Opus 150s, Sonnet 120s, Haiku 45s (base first-byte)
- **New endpoints** — `/usage`, `/status`, `/settings`, `/logs`
### v2.5.0 (2026-03-22)
- Sliding-window circuit breaker (replaced consecutive-count)
- Graduated backoff, multi-probe half-open
- Increased default timeouts for large agent prompts
### v2.0.0
- On-demand spawning (replaced pool architecture)
- Session management with `--resume`
- Full tool access, system prompt, MCP config support
## License ## License
+114 -5
View File
@@ -231,12 +231,25 @@ cmd_restart() {
openclaw gateway restart 2>&1 openclaw gateway restart 2>&1
else else
echo "Restarting proxy..." echo "Restarting proxy..."
launchctl kickstart -k gui/501/ai.openclaw.proxy 2>/dev/null || { # Try current service name, then legacy, then manual restart
echo "launchctl failed, trying kill + restart..." local uid
pkill -f "claude-proxy/server.mjs" 2>/dev/null || true uid=$(id -u)
if launchctl kickstart -k "gui/$uid/dev.ocp.proxy" 2>/dev/null; then
true
elif launchctl kickstart -k "gui/$uid/ai.openclaw.proxy" 2>/dev/null; then
true
elif systemctl --user restart ocp-proxy 2>/dev/null; then
true
elif systemctl --user restart openclaw-proxy 2>/dev/null; then
true
else
echo "Service restart failed, trying kill + restart..."
pkill -f "server.mjs.*CLAUDE_PROXY" 2>/dev/null || pkill -f "claude-proxy/server.mjs" 2>/dev/null || true
sleep 1 sleep 1
cd "$HOME/.openclaw/projects/claude-proxy" && nohup node server.mjs >> "$HOME/.openclaw/logs/proxy.log" 2>&1 & local script_dir
} script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
nohup node "$script_dir/server.mjs" >> "$HOME/.ocp/logs/proxy.log" 2>&1 &
fi
sleep 3 sleep 3
if curl -sf --max-time 5 "$PROXY/health" > /dev/null 2>&1; then if curl -sf --max-time 5 "$PROXY/health" > /dev/null 2>&1; then
echo "✓ Proxy restarted successfully." echo "✓ Proxy restarted successfully."
@@ -323,6 +336,99 @@ for e in d.get('errors', []):
fi fi
} }
# ── update ──────────────────────────────────────────────────────────────
cmd_update_help() {
cat <<'EOF'
ocp update — Update OCP to the latest version
Pulls the latest code from GitHub, restarts the proxy service,
and optionally syncs the plugin to the OpenClaw extensions directory.
Usage:
ocp update Pull latest and restart
ocp update --check Check for updates without applying
EOF
}
cmd_update() {
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Check-only mode
if [[ "${1:-}" == "--check" ]]; then
cd "$script_dir"
git fetch origin main --quiet 2>/dev/null
local local_ver remote_ver
local_ver=$(python3 -c "import json; print(json.load(open('package.json'))['version'])" 2>/dev/null || echo "?")
remote_ver=$(git show origin/main:package.json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['version'])" 2>/dev/null || echo "?")
local behind
behind=$(git rev-list HEAD..origin/main --count 2>/dev/null || echo "?")
echo "OCP Update Check"
echo "─────────────────────────────────────"
echo " Current: v$local_ver"
echo " Latest: v$remote_ver"
if [[ "$behind" == "0" ]]; then
echo " Status: ✓ Up to date"
else
echo " Status: $behind commit(s) behind"
echo ""
echo " Run 'ocp update' to apply."
fi
return 0
fi
echo "Updating OCP..."
echo ""
# 1. Pull latest
cd "$script_dir"
local old_ver
old_ver=$(python3 -c "import json; print(json.load(open('package.json'))['version'])" 2>/dev/null || echo "?")
echo " Pulling latest from GitHub..."
if ! git pull origin main --ff-only 2>&1 | sed 's/^/ /'; then
echo " ✗ Git pull failed. Resolve conflicts manually in: $script_dir"
return 1
fi
local new_ver
new_ver=$(python3 -c "import json; print(json.load(open('package.json'))['version'])" 2>/dev/null || echo "?")
if [[ "$old_ver" == "$new_ver" ]]; then
echo " ✓ Already at latest (v$new_ver)"
else
echo " ✓ Updated v$old_ver → v$new_ver"
fi
# 2. Sync plugin to extensions dir
local ext_dir="$HOME/.openclaw/extensions/ocp"
if [[ -d "$ext_dir" && -d "$script_dir/ocp-plugin" ]]; then
echo ""
echo " Syncing OCP plugin..."
cp "$script_dir/ocp-plugin/index.js" "$ext_dir/index.js" 2>/dev/null
cp "$script_dir/ocp-plugin/package.json" "$ext_dir/package.json" 2>/dev/null
cp "$script_dir/ocp-plugin/openclaw.plugin.json" "$ext_dir/openclaw.plugin.json" 2>/dev/null
echo " ✓ Plugin synced to $ext_dir"
fi
# 3. Restart proxy
echo ""
echo " Restarting proxy..."
cmd_restart > /dev/null 2>&1
sleep 2
if curl -sf --max-time 5 "$PROXY/health" > /dev/null 2>&1; then
local running_ver
running_ver=$(curl -sf --max-time 5 "$PROXY/health" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['version'])" 2>/dev/null || echo "?")
echo " ✓ Proxy running (v$running_ver)"
else
echo " ⚠ Proxy not responding — check: ocp health"
fi
echo ""
echo "Done."
}
# ── help ───────────────────────────────────────────────────────────────── # ── help ─────────────────────────────────────────────────────────────────
cmd_help() { cmd_help() {
cat <<'EOF' cat <<'EOF'
@@ -341,6 +447,8 @@ Commands:
clear Clear all sessions clear Clear all sessions
restart Restart proxy restart Restart proxy
restart gateway Restart gateway restart gateway Restart gateway
update Update OCP to latest version
update --check Check for updates
Run 'ocp <command> --help' for details on a specific command. Run 'ocp <command> --help' for details on a specific command.
EOF EOF
@@ -380,5 +488,6 @@ case "$subcmd" in
sessions) cmd_sessions ;; sessions) cmd_sessions ;;
clear) cmd_clear ;; clear) cmd_clear ;;
restart) cmd_restart "${1:-}" ;; restart) cmd_restart "${1:-}" ;;
update) cmd_update "${1:-}" ;;
*) echo "Unknown command: $subcmd"; echo ""; cmd_help; exit 1 ;; *) echo "Unknown command: $subcmd"; echo ""; cmd_help; exit 1 ;;
esac esac
+95 -11
View File
@@ -31,19 +31,34 @@ function fmtChars(c) {
async function cmdUsage() { async function cmdUsage() {
const d = await fetchJSON("/usage"); const d = await fetchJSON("/usage");
const s = d.plan.currentSession; const plan = d.plan || {};
const w = d.plan.weeklyLimits.allModels; const s = plan.currentSession || {};
const e = d.plan.extraUsage; const w = plan.weeklyLimits?.allModels || {};
const e = plan.extraUsage || {};
const px = d.proxy; const px = d.proxy;
const models = d.models || {}; const models = d.models || {};
let out = "Plan Usage Limits\n"; let out = "";
out += "─────────────────────────────\n";
out += `Current session ${bar(s.utilization)} ${s.percent}\n`; // Show subscription info if available
out += ` Resets in ${s.resetsIn} (${s.resetsAtHuman})\n\n`; if (plan.subscription) {
out += `Weekly (all) ${bar(w.utilization)} ${w.percent}\n`; out += `Plan: ${plan.subscription} (${plan.rateLimitTier || "default"})\n`;
out += ` Resets in ${w.resetsIn} (${w.resetsAtHuman})\n\n`; }
out += `Extra usage ${e.status === "allowed" ? "on" : "off"}\n\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(); const modelNames = Object.keys(models).sort();
if (modelNames.length > 0) { if (modelNames.length > 0) {
@@ -151,6 +166,67 @@ async function cmdClear() {
return `Cleared ${d.cleared} sessions.`; 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) { async function cmdLogs(args) {
const parts = (args || "").trim().split(/\s+/); const parts = (args || "").trim().split(/\s+/);
const n = parseInt(parts[0]) || 20; const n = parseInt(parts[0]) || 20;
@@ -174,7 +250,12 @@ function cmdHelp() {
/ocp logs [N] [level] Recent logs (default: 20, error) /ocp logs [N] [level] Recent logs (default: 20, error)
/ocp models Available models /ocp models Available models
/ocp sessions Active sessions /ocp sessions Active sessions
/ocp clear Clear all 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 ────────────────────────────────────────────────── // ── Plugin entry point ──────────────────────────────────────────────────
@@ -202,6 +283,9 @@ export default function (api) {
case "models": text = await cmdModels(); break; case "models": text = await cmdModels(); break;
case "sessions": text = await cmdSessions(); break; case "sessions": text = await cmdSessions(); break;
case "clear": text = await cmdClear(); 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 "logs": text = await cmdLogs(subargs); break;
case "help": case "--help": case "-h": case "": case "help": case "--help": case "-h": case "":
text = cmdHelp(); break; text = cmdHelp(); break;
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "ocp", "id": "ocp",
"name": "OCP Commands", "name": "OCP Commands",
"description": "Slash commands for the OpenClaw Proxy — /ocp usage, /ocp settings, /ocp health, etc.", "description": "Slash commands for the OpenClaw Proxy — /ocp usage, /ocp settings, /ocp health, etc.",
"version": "1.0.0", "version": "3.2.0",
"configSchema": { "configSchema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ocp", "name": "ocp",
"version": "1.0.0", "version": "3.2.0",
"description": "Slash commands for the OpenClaw Proxy", "description": "Slash commands for the OpenClaw Proxy",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "openclaw-claude-proxy", "name": "openclaw-claude-proxy",
"version": "3.0.0", "version": "3.2.0",
"description": "OpenAI-compatible proxy for Claude CLI — plan usage monitoring, runtime settings, prompt truncation, OCP CLI", "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",
@@ -24,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"
} }
} }
+10
View File
@@ -43,6 +43,7 @@ import { randomUUID, timingSafeEqual } from "node:crypto";
import { readFileSync, accessSync, constants } from "node:fs"; import { readFileSync, accessSync, constants } from "node:fs";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { homedir } from "node:os";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const _pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8")); const _pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
@@ -687,6 +688,15 @@ let usageCache = { data: null, fetchedAt: 0 };
const USAGE_CACHE_TTL = 300000; // 5 min const USAGE_CACHE_TTL = 300000; // 5 min
function getOAuthToken() { function getOAuthToken() {
// Try Linux file-based credentials first
try {
const credPath = join(homedir(), ".claude", ".credentials.json");
const creds = JSON.parse(readFileSync(credPath, "utf8"));
const token = creds?.claudeAiOauth?.accessToken;
if (token) return token;
} catch { /* fall through to macOS keychain */ }
// Try macOS keychain
try { try {
const raw = execFileSync("security", [ const raw = execFileSync("security", [
"find-generic-password", "-s", "Claude Code-credentials", "-w" "find-generic-password", "-s", "Claude Code-credentials", "-w"
+41 -13
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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`);