- name
- openclaw-nerve-cockpit
- description
- Real-time web cockpit for OpenClaw providing voice conversations, agent fleet control, kanban boards, workspace management, and usage visibility
- triggers
- ["set up nerve for openclaw","add voice control to my openclaw agent","create an openclaw dashboard","manage multiple openclaw agents with nerve","configure nerve kanban board","integrate nerve with openclaw gateway","troubleshoot nerve installation","add charts to openclaw responses"]
# OpenClaw Nerve Cockpit
> Skill by [ara.so](https://ara.so) — Hermes Skills collection.
Nerve is a real-time web cockpit for OpenClaw that transforms agent interaction from chat-only to full operational control. It provides voice conversations, multi-agent fleet management, automated kanban boards, workspace/file control, sub-agent sessions, inline charts, and comprehensive usage visibility.
## What Nerve Does
Nerve sits between your browser and the OpenClaw Gateway, providing:
- **Fleet Control**: Manage multiple agents from one interface, each with its own workspace, memory, identity, and skills
- **Voice Interface**: Push-to-talk, wake word, local Whisper transcription, multilingual commands, multiple TTS providers
- **Full Context**: Live workspace browser, tabbed editor, memory editing, config editing, skills inspection
- **Operations Layer**: Session trees, cron scheduling, kanban task boards, review flows, proposal inbox, model overrides
- **Rich Output**: Charts, diffs, previews, syntax-highlighted code, structured tool rendering, streaming UI
- **Observability**: Token usage, cost tracking, context meter, agent logs, event logs
## Architecture
```text
Browser ─── Nerve (:3080) ─── OpenClaw Gateway (:18789)
│ │
├─ WS ──────┤ proxied to gateway
├─ SSE ─────┤ file watchers, real-time sync
└─ REST ────┘ files, memories, TTS, models
```
Frontend: React 19, Tailwind CSS 4, shadcn/ui, Vite 7
Backend: Hono 4 on Node.js 22+
## Installation
### Quick Install (Recommended)
```bash
curl -fsSL https://raw.githubusercontent.com/daggerhashimoto/openclaw-nerve/master/install.sh | bash
```
The installer handles dependencies, clone, build, and launches the setup wizard with guided access modes (localhost, LAN, Tailscale tailnet IP, Tailscale Serve).
### Manual Install
```bash
git clone https://github.com/daggerhashimoto/openclaw-nerve.git
cd openclaw-nerve
npm install
npm run setup
npm run prod
```
### Install from Next Branch (Latest Features)
```bash
curl -fsSL https://raw.githubusercontent.com/daggerhashimoto/openclaw-nerve/master/install.sh | bash -s -- --branch next
```
Or switch existing install:
```bash
cd ~/nerve
git fetch origin
git switch next || git switch -c next --track origin/next
git pull --ff-only
npm install
npm run build
npm run prod
```
## Key Commands
```bash
# Development
npm run dev # Start frontend (Vite on :3080)
PORT=3081 npm run dev:server # Start backend (explicit split-port dev)
# Production
npm run prod # Build and run production server
# Management
npm run setup # Interactive setup wizard
npm run update -- --yes # Auto-update with rollback on failure
npm run build # Build frontend and backend
# Utilities
npm run lint # Lint TypeScript files
npm run type-check # TypeScript type checking
```
## Configuration
Nerve uses a `.env` file in the project root. Key variables:
### Core Settings
```bash
# Server binding
HOST=127.0.0.1 # Bind to localhost (default)
# HOST=0.0.0.0 # Bind to all interfaces (enables network access)
PORT=3080 # Server port
# OpenClaw Gateway
GATEWAY_URL=http://localhost:18789
GATEWAY_TOKEN= # Optional: if gateway requires auth
# Security (required when HOST=0.0.0.0)
PASSWORD_HASH= # bcrypt hash of UI password
SESSION_SECRET= # Random string for session signing (32+ chars)
TRUSTED_CONNECTION=false # Set true for server-side gateway token injection
```
### Voice Settings
```bash
# Voice input
WHISPER_MODEL_PATH=/path/to/whisper/model # Local Whisper model
WAKE_WORD=nerve # Wake word for voice activation
LANGUAGE=en # Transcription language
# TTS Provider (choose one)
TTS_PROVIDER=openai # Options: openai, elevenlabs, edge
OPENAI_API_KEY= # Required if TTS_PROVIDER=openai
ELEVENLABS_API_KEY= # Required if TTS_PROVIDER=elevenlabs
TTS_VOICE=alloy # Voice ID for chosen provider
```
### Advanced Settings
```bash
# Multi-agent setup
AGENT_CONFIG_DIR=/path/to/agents # Directory with per-agent configs
# Performance
WS_PING_INTERVAL=30000 # WebSocket ping interval (ms)
FILE_WATCH_DEBOUNCE=300 # File watcher debounce (ms)
# Logging
LOG_LEVEL=info # Options: debug, info, warn, error
```
### Generating PASSWORD_HASH
```typescript
// scripts/hash-password.ts
import bcrypt from 'bcrypt';
const password = process.argv[2];
if (!password) {
console.error('Usage: tsx scripts/hash-password.ts <password>');
process.exit(1);
}
const hash = await bcrypt.hash(password, 10);
console.log(hash);
```
Run:
```bash
npx tsx scripts/hash-password.ts mypassword
```
## Deployment Modes
### Local (Default)
Run Nerve and Gateway on the same machine. Best for reliability and simplicity.
```bash
HOST=127.0.0.1
PORT=3080
GATEWAY_URL=http://localhost:18789
```
### LAN Access
Expose Nerve on your local network.
```bash
HOST=0.0.0.0
PORT=3080
PASSWORD_HASH=<bcrypt_hash>
SESSION_SECRET=<random_32_char_string>
GATEWAY_URL=http://localhost:18789
```
### Tailscale Access
Expose via Tailscale tailnet IP or Tailscale Serve.
```bash
# Via tailnet IP
HOST=0.0.0.0
PORT=3080
PASSWORD_HASH=<bcrypt_hash>
SESSION_SECRET=<random_32_char_string>
# Via Tailscale Serve (alternative)
HOST=127.0.0.1
PORT=3080
# Run: tailscale serve https / http://127.0.0.1:3080
```
### Hybrid
Nerve local, Gateway in cloud.
```bash
HOST=127.0.0.1
PORT=3080
GATEWAY_URL=https://your-gateway.example.com
GATEWAY_TOKEN=<gateway_token>
```
### Full Cloud
Both Nerve and Gateway in cloud.
```bash
HOST=0.0.0.0
PORT=3080
PASSWORD_HASH=<bcrypt_hash>
SESSION_SECRET=<random_32_char_string>
GATEWAY_URL=https://your-gateway.example.com
GATEWAY_TOKEN=<gateway_token>
```
## Agent Markers for Rich Output
Nerve recognizes special markers in agent responses to render rich UI components.
### Charts
```markdown
{{CHART_START}}
{
"type": "line",
"data": {
"labels": ["Jan", "Feb", "Mar", "Apr"],
"datasets": [{
"label": "Revenue",
"data": [12000, 19000, 15000, 25000]
}]
}
}
{{CHART_END}}
```
Supported chart types: `line`, `bar`, `pie`, `doughnut`, `radar`, `scatter`
### Kanban Tasks
```markdown
{{KANBAN_START}}
{
"title": "Implement user authentication",
"description": "Add JWT-based auth to API endpoints",
"priority": "high",
"tags": ["security", "backend"],
"assignee": "agent-1"
}
{{KANBAN_END}}
```
### TTS Control
```markdown
{{TTS_START}}
This text will be read aloud using the configured TTS provider.
{{TTS_END}}
```
### Code Diffs
```diff
{{DIFF_START}}
- const old = "previous version";
+ const updated = "new version";
{{DIFF_END}}
```
## Code Examples
### Creating a Custom Backend Route
```typescript
// server/routes/custom.ts
import { Hono } from 'hono';
import type { AppContext } from '../types';
const custom = new Hono<AppContext>();
// Protected route (requires auth when HOST=0.0.0.0)
custom.get('/agent-status', async (c) => {
const gatewayUrl = c.env.GATEWAY_URL;
const token = c.env.GATEWAY_TOKEN;
const response = await fetch(`${gatewayUrl}/api/agents`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const agents = await response.json();
return c.json({
total: agents.length,
active: agents.filter(a => a.status === 'active').length,
agents,
});
});
export default custom;
```
Register in `server/index.ts`:
```typescript
import custom from './routes/custom';
app.route('/api/custom', custom);
```
### Adding a Frontend Component
```typescript
// src/components/AgentStatusCard.tsx
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
interface AgentStatus {
total: number;
active: number;
}
export function AgentStatusCard() {
const [status, setStatus] = useState<AgentStatus | null>(null);
useEffect(() => {
const fetchStatus = async () => {
const response = await fetch('/api/custom/agent-status');
const data = await response.json();
setStatus(data);
};
fetchStatus();
const interval = setInterval(fetchStatus, 5000);
return () => clearInterval(interval);
}, []);
if (!status) return <div>Loading...</div>;
return (
<Card>
<CardHeader>
<CardTitle>Agent Fleet Status</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{status.active}/{status.total}</div>
<p className="text-sm text-muted-foreground">Active agents</p>
</CardContent>
</Card>
);
}
```
### WebSocket Event Handling
```typescript
// src/hooks/useGatewayWebSocket.ts
import { useEffect, useRef, useState } from 'react';
interface GatewayMessage {
type: string;
payload: unknown;
}
export function useGatewayWebSocket(agentId: string) {
const [messages, setMessages] = useState<GatewayMessage[]>([]);
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/ws/gateway`);
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'subscribe', agentId }));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages((prev) => [...prev, message]);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
wsRef.current = ws;
return () => {
ws.close();
};
}, [agentId]);
return { messages, ws: wsRef.current };
}
```
### Server-Side Event Streaming
```typescript
// server/routes/events.ts
import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';
import type { AppContext } from '../types';
const events = new Hono<AppContext>();
events.get('/file-changes', async (c) => {
return streamSSE(c, async (stream) => {
const watcher = watchWorkspace(c.env.WORKSPACE_PATH);
watcher.on('change', (file) => {
stream.writeSSE({
data: JSON.stringify({ type: 'file_changed', file }),
});
});
// Keep connection alive
const ping = setInterval(() => {
stream.writeSSE({ data: 'ping' });
}, 30000);
// Cleanup on disconnect
stream.onAbort(() => {
clearInterval(ping);
watcher.close();
});
});
});
export default events;
```
### Custom TTS Provider Integration
```typescript
// server/services/tts/custom-provider.ts
import type { TTSProvider, TTSOptions } from './types';
export class CustomTTSProvider implements TTSProvider {
private apiKey: string;
private endpoint: string;
constructor(apiKey: string, endpoint: string) {
this.apiKey = apiKey;
this.endpoint = endpoint;
}
async synthesize(text: string, options: TTSOptions): Promise<Buffer> {
const response = await fetch(this.endpoint, {
View on GitHub