diff --git a/docs/superpowers/plans/2026-04-10-lan-mode.md b/docs/superpowers/plans/2026-04-10-lan-mode.md
new file mode 100644
index 0000000..c41a6de
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-10-lan-mode.md
@@ -0,0 +1,1178 @@
+# OCP LAN Mode — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Enable OCP to serve Claude API over LAN with flexible auth (none/shared-key/multi-key) and per-key usage tracking with a web dashboard.
+
+**Architecture:** Add LAN bind support (`0.0.0.0`), a key management layer with 3 auth modes, a SQLite-backed usage tracker, and an embedded single-file HTML dashboard. All changes in `server.mjs` + new `keys.mjs` module + CLI extensions in `ocp`.
+
+**Tech Stack:** Node.js built-in `node:crypto`, `better-sqlite3` (single native dependency) for persistent usage tracking, inline HTML/CSS/JS for dashboard.
+
+---
+
+## File Structure
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `server.mjs` | Modify | Add LAN bind, auth middleware, usage recording hooks, dashboard/key API endpoints |
+| `keys.mjs` | Create | Key management + SQLite usage store (CRUD keys, record usage, query stats) |
+| `dashboard.html` | Create | Single-file embedded web dashboard (HTML+CSS+JS, served inline) |
+| `ocp` | Modify | Add `ocp keys`, `ocp lan`, `ocp usage --by-key` CLI commands |
+| `setup.mjs` | Modify | Add LAN mode config prompts during setup |
+| `package.json` | Modify | Add `better-sqlite3` dependency, bump version to 3.4.0 |
+
+---
+
+### Task 1: Add `better-sqlite3` dependency and version bump
+
+**Files:**
+- Modify: `package.json`
+
+- [ ] **Step 1: Install better-sqlite3**
+
+```bash
+cd /Users/taodeng/.openclaw/projects/claude-proxy
+npm install better-sqlite3
+```
+
+- [ ] **Step 2: Bump version to 3.4.0**
+
+In `package.json`, change:
+```json
+"version": "3.4.0",
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add package.json package-lock.json
+git commit -m "chore: add better-sqlite3 dependency, bump to v3.4.0"
+```
+
+---
+
+### Task 2: Create `keys.mjs` — Key management + usage store
+
+**Files:**
+- Create: `keys.mjs`
+
+This module handles:
+- SQLite database initialization (`~/.ocp/ocp.db`)
+- API key CRUD (create, list, revoke)
+- Usage recording (per-request: key, model, tokens, elapsed)
+- Usage queries (by key, by time range, aggregated)
+
+- [ ] **Step 1: Create keys.mjs with database init and key CRUD**
+
+```javascript
+// keys.mjs — API key management and usage tracking for OCP LAN mode
+import Database from "better-sqlite3";
+import { randomBytes } from "node:crypto";
+import { join } from "node:path";
+import { mkdirSync } from "node:fs";
+import { homedir } from "node:os";
+
+const OCP_DIR = join(homedir(), ".ocp");
+mkdirSync(OCP_DIR, { recursive: true });
+const DB_PATH = join(OCP_DIR, "ocp.db");
+
+let db;
+
+export function getDb() {
+ if (!db) {
+ db = new Database(DB_PATH);
+ db.pragma("journal_mode = WAL");
+ db.pragma("foreign_keys = ON");
+ initSchema();
+ }
+ return db;
+}
+
+function initSchema() {
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS api_keys (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ key TEXT UNIQUE NOT NULL,
+ name TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ revoked INTEGER NOT NULL DEFAULT 0
+ );
+
+ CREATE TABLE IF NOT EXISTS usage_log (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ key_id INTEGER,
+ key_name TEXT NOT NULL DEFAULT 'anonymous',
+ model TEXT NOT NULL,
+ prompt_chars INTEGER NOT NULL DEFAULT 0,
+ response_chars INTEGER NOT NULL DEFAULT 0,
+ elapsed_ms INTEGER NOT NULL DEFAULT 0,
+ success INTEGER NOT NULL DEFAULT 1,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ FOREIGN KEY (key_id) REFERENCES api_keys(id)
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_usage_created ON usage_log(created_at);
+ CREATE INDEX IF NOT EXISTS idx_usage_key ON usage_log(key_id);
+ `);
+}
+
+// ── Key CRUD ──
+
+export function createKey(name) {
+ const key = "ocp_" + randomBytes(24).toString("base64url");
+ const d = getDb();
+ const stmt = d.prepare("INSERT INTO api_keys (key, name) VALUES (?, ?)");
+ const result = stmt.run(key, name);
+ return { id: result.lastInsertRowid, key, name };
+}
+
+export function listKeys() {
+ const d = getDb();
+ return d.prepare(
+ "SELECT id, key, name, created_at, revoked FROM api_keys ORDER BY created_at DESC"
+ ).all().map(k => ({
+ ...k,
+ keyPreview: k.key.slice(0, 8) + "..." + k.key.slice(-4),
+ }));
+}
+
+export function revokeKey(idOrName) {
+ const d = getDb();
+ const stmt = d.prepare(
+ "UPDATE api_keys SET revoked = 1 WHERE (id = ? OR name = ?) AND revoked = 0"
+ );
+ return stmt.run(idOrName, idOrName).changes > 0;
+}
+
+export function validateKey(key) {
+ const d = getDb();
+ const row = d.prepare(
+ "SELECT id, name FROM api_keys WHERE key = ? AND revoked = 0"
+ ).get(key);
+ return row || null;
+}
+
+// ── Usage recording ──
+
+export function recordUsage({ keyId, keyName, model, promptChars, responseChars, elapsedMs, success }) {
+ const d = getDb();
+ d.prepare(`
+ INSERT INTO usage_log (key_id, key_name, model, prompt_chars, response_chars, elapsed_ms, success)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ `).run(keyId ?? null, keyName || "anonymous", model, promptChars, responseChars, elapsedMs, success ? 1 : 0);
+}
+
+// ── Usage queries ──
+
+export function getUsageByKey({ since, until } = {}) {
+ const d = getDb();
+ let where = "WHERE 1=1";
+ const params = [];
+ if (since) { where += " AND created_at >= ?"; params.push(since); }
+ if (until) { where += " AND created_at <= ?"; params.push(until); }
+
+ return d.prepare(`
+ SELECT
+ key_name,
+ COUNT(*) as requests,
+ SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successes,
+ SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as errors,
+ SUM(prompt_chars) as total_prompt_chars,
+ SUM(response_chars) as total_response_chars,
+ SUM(elapsed_ms) as total_elapsed_ms,
+ AVG(elapsed_ms) as avg_elapsed_ms,
+ MIN(created_at) as first_request,
+ MAX(created_at) as last_request
+ FROM usage_log
+ ${where}
+ GROUP BY key_name
+ ORDER BY requests DESC
+ `).all(...params);
+}
+
+export function getUsageTimeline({ keyName, hours = 24 } = {}) {
+ const d = getDb();
+ const since = new Date(Date.now() - hours * 3600000).toISOString();
+ let where = "WHERE created_at >= ?";
+ const params = [since];
+ if (keyName) { where += " AND key_name = ?"; params.push(keyName); }
+
+ return d.prepare(`
+ SELECT
+ strftime('%Y-%m-%dT%H:00:00', created_at) as hour,
+ COUNT(*) as requests,
+ SUM(prompt_chars) as prompt_chars,
+ SUM(response_chars) as response_chars,
+ AVG(elapsed_ms) as avg_elapsed_ms
+ FROM usage_log
+ ${where}
+ GROUP BY hour
+ ORDER BY hour
+ `).all(...params);
+}
+
+export function getRecentUsage(limit = 50) {
+ const d = getDb();
+ return d.prepare(`
+ SELECT key_name, model, prompt_chars, response_chars, elapsed_ms, success, created_at
+ FROM usage_log
+ ORDER BY created_at DESC
+ LIMIT ?
+ `).all(limit);
+}
+
+export function closeDb() {
+ if (db) { db.close(); db = null; }
+}
+```
+
+- [ ] **Step 2: Verify module loads correctly**
+
+```bash
+node -e "import('./keys.mjs').then(k => { console.log('OK'); k.closeDb(); })"
+```
+
+Expected: `OK` — database file created at `~/.ocp/ocp.db`
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add keys.mjs
+git commit -m "feat: add keys.mjs — API key management and SQLite usage store"
+```
+
+---
+
+### Task 3: Add LAN bind + auth modes to `server.mjs`
+
+**Files:**
+- Modify: `server.mjs:28-36` (env vars section)
+- Modify: `server.mjs:77-95` (configuration section)
+- Modify: `server.mjs:1017-1036` (HTTP server + auth middleware)
+- Modify: `server.mjs:1188-1206` (server.listen)
+
+Three auth modes controlled by `CLAUDE_AUTH_MODE`:
+- `none` — no auth (LAN trust mode)
+- `shared` — single `PROXY_API_KEY` (existing behavior)
+- `multi` — per-user keys managed via `keys.mjs`
+
+- [ ] **Step 1: Add new imports and env vars at top of server.mjs**
+
+After the existing imports (line 35), add:
+
+```javascript
+import { validateKey, recordUsage, getUsageByKey, getUsageTimeline, getRecentUsage, createKey, listKeys, revokeKey, closeDb } from "./keys.mjs";
+```
+
+Add new config vars after line 95 (`BREAKER_HALF_OPEN_MAX`):
+
+```javascript
+// ── LAN mode configuration ──
+const BIND_ADDRESS = process.env.CLAUDE_BIND || "127.0.0.1";
+const AUTH_MODE = process.env.CLAUDE_AUTH_MODE || (PROXY_API_KEY ? "shared" : "none");
+// AUTH_MODE: "none" (open), "shared" (single PROXY_API_KEY), "multi" (per-user keys from keys.mjs)
+const ADMIN_KEY = process.env.OCP_ADMIN_KEY || "";
+```
+
+- [ ] **Step 2: Replace auth middleware in HTTP server (lines 1027-1036)**
+
+Replace the existing Bearer token auth block with:
+
+```javascript
+ // ── Auth middleware ──
+ // /health is always public. Admin endpoints require admin key in multi mode.
+ const isPublicEndpoint = req.url === "/health";
+ let authKeyName = "local";
+ let authKeyId = null;
+
+ if (!isPublicEndpoint) {
+ const auth = req.headers["authorization"] || "";
+ const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
+
+ if (AUTH_MODE === "shared") {
+ // Single shared key mode (backwards compatible)
+ if (PROXY_API_KEY) {
+ const tokenBuf = Buffer.from(token);
+ const keyBuf = Buffer.from(PROXY_API_KEY);
+ if (tokenBuf.length !== keyBuf.length || !timingSafeEqual(tokenBuf, keyBuf)) {
+ return jsonResponse(res, 401, { error: { message: "Unauthorized: invalid or missing Bearer token", type: "auth_error" } });
+ }
+ authKeyName = "shared";
+ }
+ } else if (AUTH_MODE === "multi") {
+ // Multi-key mode: validate against keys.mjs database
+ if (!token) {
+ return jsonResponse(res, 401, { error: { message: "Unauthorized: Bearer token required", type: "auth_error" } });
+ }
+ // Check if it's the admin key
+ if (ADMIN_KEY && token === ADMIN_KEY) {
+ authKeyName = "admin";
+ } else {
+ const keyInfo = validateKey(token);
+ if (!keyInfo) {
+ return jsonResponse(res, 401, { error: { message: "Unauthorized: invalid or revoked API key", type: "auth_error" } });
+ }
+ authKeyName = keyInfo.name;
+ authKeyId = keyInfo.id;
+ }
+ }
+ // AUTH_MODE === "none": no auth check, allow all
+ }
+
+ // Attach auth info to request for downstream use
+ req._authKeyName = authKeyName;
+ req._authKeyId = authKeyId;
+```
+
+- [ ] **Step 3: Update CORS to allow LAN origins**
+
+Replace the CORS block (lines 1019-1025) with:
+
+```javascript
+ // Dynamic CORS: allow localhost and LAN origins
+ const origin = req.headers["origin"] || "";
+ const isAllowedOrigin = /^https?:\/\/(127\.0\.0\.1|localhost|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|10\.\d+\.\d+\.\d+)(:\d+)?$/.test(origin);
+ res.setHeader("Access-Control-Allow-Origin", isAllowedOrigin ? origin : "*");
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS, PATCH");
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Session-Id, X-Conversation-Id");
+ if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
+```
+
+- [ ] **Step 4: Update server.listen to use BIND_ADDRESS**
+
+Replace line 1189:
+
+```javascript
+server.listen(PORT, BIND_ADDRESS, () => {
+ const bindMsg = BIND_ADDRESS === "0.0.0.0" ? `http://0.0.0.0:${PORT} (LAN mode)` : `http://127.0.0.1:${PORT}`;
+ console.log(`openclaw-claude-proxy v${VERSION} listening on ${bindMsg}`);
+```
+
+Add auth mode to startup log (after line 1200):
+
+```javascript
+ console.log(`Auth mode: ${AUTH_MODE}${AUTH_MODE === "shared" ? " (PROXY_API_KEY)" : AUTH_MODE === "multi" ? " (per-user keys)" : " (open)"}`);
+ console.log(`Bind: ${BIND_ADDRESS}${BIND_ADDRESS === "0.0.0.0" ? " ⚠ LAN-accessible" : ""}`);
+```
+
+- [ ] **Step 5: Add usage recording to request handlers**
+
+In `handleChatCompletions` (around line 1001-1014), after successful response and in catch block, add usage recording. Wrap the try/catch:
+
+```javascript
+ try {
+ const content = await callClaude(model, messages, conversationId);
+ const id = `chatcmpl-${randomUUID()}`;
+ completionResponse(res, id, model, content);
+ // Record usage
+ recordUsage({
+ keyId: req._authKeyId, keyName: req._authKeyName, model,
+ promptChars: messages.reduce((a, m) => a + (typeof m.content === "string" ? m.content.length : JSON.stringify(m.content).length), 0),
+ responseChars: content.length, elapsedMs: Date.now() - Date.now(), success: true,
+ });
+ } catch (err) {
+ recordUsage({
+ keyId: req._authKeyId, keyName: req._authKeyName, model,
+ promptChars: 0, responseChars: 0, elapsedMs: 0, success: false,
+ });
+ // ... existing error handling
+ }
+```
+
+Similarly for streaming in `callClaudeStreaming`, record usage in the `proc.on("close")` handler by passing `req._authKeyName` and `req._authKeyId` through.
+
+- [ ] **Step 6: Add closeDb to graceful shutdown**
+
+In `gracefulShutdown()` function, after clearing intervals (around line 1150), add:
+
+```javascript
+ closeDb();
+```
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add server.mjs
+git commit -m "feat: add LAN bind, 3-mode auth, and usage recording to server"
+```
+
+---
+
+### Task 4: Add key management + dashboard API endpoints to `server.mjs`
+
+**Files:**
+- Modify: `server.mjs` (add routes before the 404 handler)
+
+- [ ] **Step 1: Add key management API endpoints**
+
+Before the 404 handler (line 1132), add:
+
+```javascript
+ // ── Key management API (admin-only in multi mode) ──
+
+ // Admin gate: in multi mode, key management requires admin key
+ const isAdmin = AUTH_MODE !== "multi" || authKeyName === "admin" || BIND_ADDRESS === "127.0.0.1";
+
+ // POST /api/keys — create a new API key
+ if (req.url === "/api/keys" && req.method === "POST") {
+ if (!isAdmin) return jsonResponse(res, 403, { error: "Admin access required" });
+ let body = "";
+ for await (const chunk of req) body += chunk;
+ let parsed;
+ try { parsed = JSON.parse(body); } catch { return jsonResponse(res, 400, { error: "Invalid JSON" }); }
+ const name = parsed.name || `key-${Date.now()}`;
+ const newKey = createKey(name);
+ return jsonResponse(res, 201, newKey);
+ }
+
+ // GET /api/keys — list all keys
+ if (req.url === "/api/keys" && req.method === "GET") {
+ if (!isAdmin) return jsonResponse(res, 403, { error: "Admin access required" });
+ return jsonResponse(res, 200, { keys: listKeys() });
+ }
+
+ // DELETE /api/keys/:id — revoke a key
+ if (req.url?.startsWith("/api/keys/") && req.method === "DELETE") {
+ if (!isAdmin) return jsonResponse(res, 403, { error: "Admin access required" });
+ const idOrName = decodeURIComponent(req.url.split("/api/keys/")[1]);
+ const revoked = revokeKey(idOrName);
+ return jsonResponse(res, 200, { revoked, idOrName });
+ }
+
+ // GET /api/usage — usage stats by key
+ if (req.url?.startsWith("/api/usage") && req.method === "GET") {
+ if (!isAdmin) return jsonResponse(res, 403, { error: "Admin access required" });
+ const url = new URL(req.url, `http://${BIND_ADDRESS}:${PORT}`);
+ const since = url.searchParams.get("since");
+ const until = url.searchParams.get("until");
+ return jsonResponse(res, 200, {
+ byKey: getUsageByKey({ since, until }),
+ timeline: getUsageTimeline({ hours: parseInt(url.searchParams.get("hours") || "24", 10) }),
+ recent: getRecentUsage(parseInt(url.searchParams.get("limit") || "50", 10)),
+ });
+ }
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add server.mjs
+git commit -m "feat: add key management and usage query API endpoints"
+```
+
+---
+
+### Task 5: Create embedded web dashboard
+
+**Files:**
+- Create: `dashboard.html`
+- Modify: `server.mjs` (add `/dashboard` route)
+
+The dashboard is a single HTML file served inline. It uses vanilla JS + CSS, no framework, no build step.
+
+- [ ] **Step 1: Create dashboard.html**
+
+```html
+
+
+
+
+
+OCP Dashboard
+
+
+
+
+
+
OCP Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Usage by Key
+
+ | Key | Requests | OK | Err | Avg Time | Last Request |
+
+
+
+
+
+
+
API Keys
+
+
+
+
+
+ | Name | Key | Created | Status | |
+
+
+
+
+
+
+
Recent Requests
+
+ | Time | Key | Model | Prompt | Response | Time | Status |
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: Add dashboard route to server.mjs**
+
+Before the 404 handler, add:
+
+```javascript
+ // GET /dashboard — web dashboard
+ if (req.url === "/dashboard" && req.method === "GET") {
+ try {
+ const html = readFileSync(join(__dirname, "dashboard.html"), "utf8");
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
+ res.end(html);
+ } catch (err) {
+ return jsonResponse(res, 500, { error: "Dashboard file not found" });
+ }
+ return;
+ }
+```
+
+- [ ] **Step 3: Update 404 message to include new endpoints**
+
+```javascript
+ jsonResponse(res, 404, { error: "Not found. Endpoints: GET /v1/models, POST /v1/chat/completions, GET /health, GET /usage, GET /status, GET /logs, GET|PATCH /settings, GET|DELETE /sessions, GET /dashboard, GET|POST|DELETE /api/keys, GET /api/usage" });
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add dashboard.html server.mjs
+git commit -m "feat: add embedded web dashboard with real-time usage display"
+```
+
+---
+
+### Task 6: Extend `ocp` CLI with key management and LAN commands
+
+**Files:**
+- Modify: `ocp` (bash CLI)
+
+- [ ] **Step 1: Add `ocp keys` command**
+
+After the `cmd_clear` section (around line 214), add:
+
+```bash
+# ── keys ────────────────────────────────────────────────────────────────
+cmd_keys_help() {
+ cat <<'EOF'
+ocp keys — Manage API keys (multi-key auth mode)
+
+Usage:
+ ocp keys List all keys
+ ocp keys add Create a new key
+ ocp keys revoke Revoke a key
+
+Examples:
+ ocp keys add wife-laptop
+ ocp keys add son-ipad
+ ocp keys revoke wife-laptop
+EOF
+}
+
+cmd_keys() {
+ case "${1:-}" in
+ add)
+ if [[ -z "${2:-}" ]]; then echo "Usage: ocp keys add "; return 1; fi
+ local result
+ result=$(curl -sf --max-time 5 -X POST "$PROXY/api/keys" \
+ -H "Content-Type: application/json" \
+ -d "{\"name\": \"$2\"}" 2>&1) || { echo "Error: proxy unreachable or unauthorized"; exit 1; }
+ echo "$result" | python3 -c "
+import sys, json
+d = json.loads(sys.stdin.read())
+if 'key' in d:
+ print(f'✓ Key created for \"{d[\"name\"]}\"')
+ print(f'')
+ print(f' API Key: {d[\"key\"]}')
+ print(f'')
+ print(f' Copy this key now — you won\\'t see it again.')
+ print(f' Configure in IDE: OPENAI_API_KEY={d[\"key\"]}')
+else:
+ print(f'✗ {d.get(\"error\", \"Unknown error\")}')
+"
+ ;;
+ revoke)
+ if [[ -z "${2:-}" ]]; then echo "Usage: ocp keys revoke "; return 1; fi
+ curl -sf --max-time 5 -X DELETE "$PROXY/api/keys/$2" | python3 -c "
+import sys, json
+d = json.loads(sys.stdin.read())
+if d.get('revoked'):
+ print(f'✓ Key \"{d[\"idOrName\"]}\" revoked.')
+else:
+ print(f'✗ Key not found or already revoked.')
+"
+ ;;
+ ""|--help|-h)
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then cmd_keys_help; return; fi
+ # List keys
+ curl -sf --max-time 5 "$PROXY/api/keys" | python3 -c "
+import sys, json
+d = json.loads(sys.stdin.read())
+keys = d.get('keys', [])
+if not keys:
+ print('No API keys configured.')
+ print('Create one: ocp keys add ')
+else:
+ print('API Keys')
+ print('─────────────────────────────────────────────────')
+ for k in keys:
+ status = '✗ revoked' if k['revoked'] else '✓ active'
+ print(f' {k[\"name\"]:<20} {k[\"keyPreview\"]:<20} {status} {k[\"created_at\"]}')
+" 2>/dev/null || { echo "Error: proxy unreachable or key management not available"; exit 1; }
+ ;;
+ *)
+ echo "Unknown subcommand: $1"; cmd_keys_help; return 1 ;;
+ esac
+}
+```
+
+- [ ] **Step 2: Add `ocp lan` command**
+
+```bash
+# ── lan ─────────────────────────────────────────────────────────────────
+cmd_lan_help() {
+ cat <<'EOF'
+ocp lan — Quick LAN mode setup guide
+
+Shows current network configuration and connection instructions
+for other devices on the same network.
+
+Usage: ocp lan
+EOF
+}
+
+cmd_lan() {
+ local ip
+ ip=$(ipconfig getifaddr en0 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}')
+ local port
+ port=$(curl -sf --max-time 5 "$PROXY/health" | python3 -c "import sys,json; h=json.loads(sys.stdin.read()); print(h.get('config',{}).get('port', '3456'))" 2>/dev/null || echo "3456")
+
+ echo "OCP LAN Setup"
+ echo "─────────────────────────────────────"
+ echo ""
+ echo " Your IP: $ip"
+ echo " Port: $port"
+ echo ""
+ echo " For IDE users, set:"
+ echo " OPENAI_BASE_URL=http://$ip:$port/v1"
+ echo " OPENAI_API_KEY= (if auth enabled)"
+ echo ""
+ echo " Dashboard: http://$ip:$port/dashboard"
+ echo ""
+
+ # Check if currently bound to LAN
+ if curl -sf --max-time 2 "http://$ip:$port/health" > /dev/null 2>&1; then
+ echo " Status: ✓ LAN-accessible"
+ else
+ echo " Status: ✗ Not LAN-accessible (bound to localhost only)"
+ echo ""
+ echo " To enable LAN mode, set env var and restart:"
+ echo " CLAUDE_BIND=0.0.0.0"
+ echo " ocp restart"
+ fi
+}
+```
+
+- [ ] **Step 3: Add `ocp usage --by-key` option**
+
+Modify `cmd_usage()` to accept `--by-key`:
+
+After the existing `cmd_usage()` function, add a check:
+
+```bash
+# In the cmd_usage function, add at the top:
+cmd_usage() {
+ if [[ "${1:-}" == "--by-key" ]]; then
+ local data
+ data=$(curl -sf --max-time 15 "$PROXY/api/usage" 2>&1) || { echo "Error: proxy unreachable or usage API not available"; exit 1; }
+ echo "$data" | python3 -c "
+import sys, json
+d = json.loads(sys.stdin.read())
+by_key = d.get('byKey', [])
+if not by_key:
+ print('No usage data yet.')
+else:
+ print('Usage by Key')
+ print('─────────────────────────────────────────────────────────────────')
+ hdr = f' {\"Key\":<20} {\"Reqs\":>5} {\"OK\":>4} {\"Err\":>4} {\"Avg Time\":>9} {\"Last Request\":<20}'
+ print(hdr)
+ print(' ' + '─' * (len(hdr) - 2))
+ for k in by_key:
+ avg_t = f'{k[\"avg_elapsed_ms\"]/1000:.1f}s' if k['avg_elapsed_ms'] else '-'
+ print(f' {k[\"key_name\"]:<20} {k[\"requests\"]:>5} {k[\"successes\"]:>4} {k[\"errors\"]:>4} {avg_t:>9} {k[\"last_request\"]:<20}')
+"
+ return
+ fi
+
+ # ... existing cmd_usage code below ...
+```
+
+- [ ] **Step 4: Update dispatch case and help**
+
+In the `case` statement at the bottom, add:
+
+```bash
+ keys) cmd_keys "${1:-}" "${2:-}" ;;
+ lan) cmd_lan ;;
+```
+
+Update `cmd_help()`:
+
+```bash
+ keys Manage API keys (add/list/revoke)
+ lan LAN mode setup guide
+ usage Plan usage limits (--by-key for per-key stats)
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ocp
+git commit -m "feat: add ocp keys, ocp lan, and ocp usage --by-key CLI commands"
+```
+
+---
+
+### Task 7: Update setup.mjs with LAN mode prompts
+
+**Files:**
+- Modify: `setup.mjs`
+
+- [ ] **Step 1: Add LAN mode env vars to launchd plist and systemd service**
+
+In `setup.mjs`, in the plist XML template (around line 351), add environment variables for LAN mode if the user chose it. Add these optional env entries to the `EnvironmentVariables` dict:
+
+```xml
+ CLAUDE_BIND
+ ${BIND_ADDRESS}
+ CLAUDE_AUTH_MODE
+ ${AUTH_MODE_CONFIG}
+```
+
+Similarly update the systemd service template to include:
+
+```
+Environment=CLAUDE_BIND=${BIND_ADDRESS}
+Environment=CLAUDE_AUTH_MODE=${AUTH_MODE_CONFIG}
+```
+
+Add at the top of setup.mjs, after existing `opt()` calls:
+
+```javascript
+const BIND_ADDRESS = opt("bind", "127.0.0.1");
+const AUTH_MODE_CONFIG = opt("auth-mode", "none");
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add setup.mjs
+git commit -m "feat: add LAN mode config options to setup.mjs"
+```
+
+---
+
+### Task 8: Update README with LAN mode documentation
+
+**Files:**
+- Modify: `README.md`
+
+- [ ] **Step 1: Add LAN Mode section to README**
+
+Add a new section after the existing "Quick Start" section:
+
+```markdown
+## LAN Mode — Share with Family
+
+OCP can serve your entire household from a single machine.
+
+### Enable LAN Access
+
+```bash
+# Set bind address and restart
+export CLAUDE_BIND=0.0.0.0
+ocp restart
+```
+
+Or permanently via setup:
+```bash
+node setup.mjs --bind 0.0.0.0 --auth-mode multi
+```
+
+### Auth Modes
+
+| Mode | Env | Description |
+|------|-----|-------------|
+| `none` | `CLAUDE_AUTH_MODE=none` | Open access (trusted network) |
+| `shared` | `CLAUDE_AUTH_MODE=shared` | Single key via `PROXY_API_KEY` |
+| `multi` | `CLAUDE_AUTH_MODE=multi` | Per-user keys with usage tracking |
+
+### Multi-Key Setup
+
+```bash
+# Create keys for family members
+ocp keys add wife-laptop
+ocp keys add son-ipad
+
+# List keys
+ocp keys
+
+# View per-key usage
+ocp usage --by-key
+
+# Revoke a key
+ocp keys revoke son-ipad
+```
+
+### Family Member Setup
+
+Give your family member these settings for their IDE:
+
+```
+OPENAI_BASE_URL=http://:3456/v1
+OPENAI_API_KEY=ocp_
+```
+
+Run `ocp lan` to see your IP and connection guide.
+
+### Web Dashboard
+
+Access the usage dashboard at: `http://:3456/dashboard`
+
+Shows real-time stats: per-key usage, request history, plan utilization, and system health.
+```
+
+- [ ] **Step 2: Update version in README**
+
+Change version references from `v3.3.1` to `v3.4.0`.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add README.md
+git commit -m "docs: add LAN mode documentation and family sharing guide"
+```
+
+---
+
+### Task 9: Integration test — end-to-end verification
+
+**Files:**
+- No new files — manual verification steps
+
+- [ ] **Step 1: Start OCP in LAN mode with multi-key auth**
+
+```bash
+cd /Users/taodeng/.openclaw/projects/claude-proxy
+CLAUDE_BIND=0.0.0.0 CLAUDE_AUTH_MODE=multi OCP_ADMIN_KEY=test-admin-123 node server.mjs &
+sleep 3
+```
+
+- [ ] **Step 2: Verify health endpoint (public, no auth)**
+
+```bash
+curl -s http://127.0.0.1:3456/health | python3 -m json.tool | head -5
+```
+
+Expected: JSON with status "ok", version "3.4.0"
+
+- [ ] **Step 3: Create a test API key**
+
+```bash
+curl -s -X POST http://127.0.0.1:3456/api/keys \
+ -H "Authorization: Bearer test-admin-123" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "test-user"}' | python3 -m json.tool
+```
+
+Expected: JSON with `id`, `key` (starts with `ocp_`), `name`
+
+- [ ] **Step 4: Test request with key**
+
+```bash
+# Save the key from step 3
+KEY=""
+curl -s http://127.0.0.1:3456/v1/chat/completions \
+ -H "Authorization: Bearer $KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"model":"haiku","messages":[{"role":"user","content":"say hi"}]}' | python3 -m json.tool | head -10
+```
+
+Expected: Chat completion response from Claude
+
+- [ ] **Step 5: Verify usage tracking**
+
+```bash
+curl -s http://127.0.0.1:3456/api/usage \
+ -H "Authorization: Bearer test-admin-123" | python3 -m json.tool
+```
+
+Expected: Usage data showing `test-user` with 1 request
+
+- [ ] **Step 6: Test dashboard loads**
+
+```bash
+curl -s http://127.0.0.1:3456/dashboard | head -5
+```
+
+Expected: HTML starting with ``
+
+- [ ] **Step 7: Test auth rejection (no key in multi mode)**
+
+```bash
+curl -s http://127.0.0.1:3456/v1/models
+```
+
+Expected: 401 error with "Bearer token required"
+
+- [ ] **Step 8: Stop test server and clean up**
+
+```bash
+pkill -f "server.mjs" || true
+```
+
+- [ ] **Step 9: Commit integration test results as passing**
+
+```bash
+git add -A
+git commit -m "test: verify LAN mode, multi-key auth, usage tracking, and dashboard"
+```
+
+---
+
+## Summary
+
+| Task | Description | Est. Lines |
+|------|-------------|-----------|
+| 1 | Add dependency + version bump | ~5 |
+| 2 | `keys.mjs` — key CRUD + usage store | ~180 |
+| 3 | LAN bind + auth modes in server | ~80 |
+| 4 | Key mgmt + usage API endpoints | ~50 |
+| 5 | Web dashboard (HTML+JS) | ~200 |
+| 6 | CLI extensions (`ocp keys/lan`) | ~150 |
+| 7 | Setup.mjs LAN config | ~20 |
+| 8 | README documentation | ~60 |
+| 9 | Integration testing | ~0 |
+| **Total** | | **~745 lines** |