Loading...
Loading...
Run a local observability and control dashboard for OpenClaw AI agents with real-time collaboration, task management, and safety-first defaults.
npx skill4agent add aradotso/hermes-skills openclaw-control-centerSkill by ara.so — Hermes Skills collection.
# Clone the repository
git clone https://github.com/TianyiDataScience/openclaw-control-center.git
cd openclaw-control-center
# Install dependencies
npm install
# Set up environment
cp .env.example .env
# Build the project
npm run build
# Run tests
npm test
# Run smoke tests
npm run smoke:ui
npm run smoke:hall
# Start development server
npm run dev:ui# Safety and security (defaults)
READONLY_MODE=true
LOCAL_TOKEN_AUTH_REQUIRED=true
LOCAL_API_TOKEN=your-long-random-secret-here
# Import/export protection
IMPORT_MUTATION_ENABLED=false
IMPORT_MUTATION_DRY_RUN=false
# Approval actions
APPROVAL_ACTIONS_ENABLED=false
APPROVAL_ACTIONS_DRY_RUN=true
# UI configuration
UI_PORT=4310
UI_BIND_ADDRESS=127.0.0.1
# For Tailscale or remote access
OPENCLAW_CONTROL_UI_URL=http://<tailscale-host>:4310/
# UI_BIND_ADDRESS=0.0.0.0 # Auto-set when OPENCLAW_CONTROL_UI_URL is providedcontrol-center/
├── src/
│ ├── ui/ # Frontend TypeScript/React components
│ ├── server/ # Backend API routes
│ ├── lib/ # Shared utilities
│ └── types/ # TypeScript definitions
├── docs/
│ ├── assets/ # Screenshots and images
│ └── FAQ.md # Troubleshooting guide
├── HALL.md # Shared collaboration style guide
└── .env # Local configuration# Start UI development server (recommended, cross-platform)
npm run dev:ui
# Single monitor pass without UI
npm run dev
# Build production bundle
npm run build
# Run all tests
npm test
# Smoke tests for UI and hall
npm run smoke:ui
npm run smoke:hall
# Linting
npm run lint
npm run lint:fix# English interface
http://127.0.0.1:4310/?section=overview&lang=en
# Chinese interface
http://127.0.0.1:4310/?section=overview&lang=zh
# Direct section access
http://127.0.0.1:4310/?section=collaboration&lang=en
http://127.0.0.1:4310/?section=staff&lang=en
http://127.0.0.1:4310/?section=tasks&lang=en// .env configuration for enabling mutations
READONLY_MODE=false
LOCAL_TOKEN_AUTH_REQUIRED=true
LOCAL_API_TOKEN=generate-a-secure-random-token-here
IMPORT_MUTATION_ENABLED=true
APPROVAL_ACTIONS_ENABLED=true
APPROVAL_ACTIONS_DRY_RUN=false// src/lib/api-client.ts
interface ApiRequest {
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: any;
requiresAuth?: boolean;
}
async function callApi({ endpoint, method, body, requiresAuth = true }: ApiRequest) {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// Add authorization for protected endpoints
if (requiresAuth) {
const token = process.env.LOCAL_API_TOKEN;
if (!token) {
throw new Error('LOCAL_API_TOKEN not configured');
}
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`http://127.0.0.1:4310${endpoint}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
return response.json();
}
// Example: Save task with authentication
async function saveTask(taskId: string, updates: any) {
return callApi({
endpoint: `/api/tasks/${taskId}`,
method: 'PUT',
body: updates,
requiresAuth: true,
});
}// src/types/hall.ts
interface HallMessage {
id: string;
taskId: string;
agentId: string;
content: string;
phase: 'discussion' | 'execution' | 'review' | 'blocked';
timestamp: string;
mentions?: string[];
}
interface ExecutionOrder {
taskId: string;
owners: string[];
currentOwnerIndex: number;
nextHandoff?: string;
}
// Example: Create hall task and arrange execution
async function createHallTask(content: string, roster: string[]) {
// 1. Create task in hall
const task = await callApi({
endpoint: '/api/hall/tasks',
method: 'POST',
body: { content, phase: 'discussion' },
});
// 2. Let agents discuss (wait for 2+ replies)
await waitForDiscussion(task.id, 2);
// 3. Arrange execution order
const executionOrder = await callApi({
endpoint: `/api/hall/tasks/${task.id}/execution-order`,
method: 'PUT',
body: {
owners: ['agent-manager', 'agent-builder', 'agent-qa'],
},
});
// 4. Start execution
const execution = await callApi({
endpoint: `/api/hall/tasks/${task.id}/start`,
method: 'POST',
});
return { task, executionOrder, execution };
}// src/lib/staff-monitor.ts
interface AgentStatus {
agentId: string;
displayName: string;
status: 'active' | 'idle' | 'blocked' | 'queued';
currentTask?: string;
lastActivity: string;
tokenUsage24h: number;
}
async function getStaffStatus(): Promise<AgentStatus[]> {
const response = await callApi({
endpoint: '/api/staff/status',
method: 'GET',
requiresAuth: false, // Read-only endpoint
});
return response.agents.map((agent: any) => ({
agentId: agent.id,
displayName: agent.display_name || agent.id,
status: determineStatus(agent),
currentTask: agent.current_task_id,
lastActivity: agent.last_seen,
tokenUsage24h: agent.token_usage_24h || 0,
}));
}
function determineStatus(agent: any): AgentStatus['status'] {
if (agent.blocked_reason) return 'blocked';
if (agent.current_task_id) return 'active';
if (agent.queued_tasks > 0) return 'queued';
return 'idle';
}// src/lib/approval-handler.ts
interface ApprovalRequest {
taskId: string;
action: 'approve' | 'reject' | 'request-changes';
comment?: string;
reviewerId: string;
}
async function handleTaskApproval(request: ApprovalRequest) {
// Ensure approval actions are enabled
if (process.env.APPROVAL_ACTIONS_ENABLED !== 'true') {
throw new Error('Approval actions are disabled');
}
const isDryRun = process.env.APPROVAL_ACTIONS_DRY_RUN === 'true';
const response = await callApi({
endpoint: `/api/tasks/${request.taskId}/approve`,
method: 'POST',
body: {
action: request.action,
comment: request.comment,
reviewer_id: request.reviewerId,
dry_run: isDryRun,
},
requiresAuth: true,
});
if (isDryRun) {
console.log('[DRY RUN] Would have:', request.action, request.taskId);
}
return response;
}// src/lib/usage-tracker.ts
interface UsageMetrics {
period: 'today' | '7d' | '30d';
tokenCount: number;
estimatedCost: number;
topConsumers: Array<{ agentId: string; tokens: number }>;
contextPressure: Array<{ sessionId: string; usage: number; limit: number }>;
}
async function getUsageMetrics(period: UsageMetrics['period']): Promise<UsageMetrics> {
const response = await callApi({
endpoint: `/api/usage/metrics?period=${period}`,
method: 'GET',
requiresAuth: false,
});
return {
period,
tokenCount: response.total_tokens,
estimatedCost: response.estimated_cost_usd,
topConsumers: response.agents
.sort((a: any, b: any) => b.tokens - a.tokens)
.slice(0, 5),
contextPressure: response.sessions.filter(
(s: any) => s.usage / s.limit > 0.75
),
};
}// Complete hall workflow from task creation to completion
async function runHallCollaboration(taskDescription: string) {
// 1. Create task
const task = await callApi({
endpoint: '/api/hall/tasks',
method: 'POST',
body: { content: taskDescription, phase: 'discussion' },
});
// 2. Wait for discussion (at least 2 agents should reply)
console.log('Waiting for agent discussion...');
await pollUntil(
async () => {
const messages = await callApi({
endpoint: `/api/hall/tasks/${task.id}/messages`,
method: 'GET',
requiresAuth: false,
});
return messages.length >= 2;
},
{ intervalMs: 2000, timeoutMs: 60000 }
);
// 3. Arrange execution order
await callApi({
endpoint: `/api/hall/tasks/${task.id}/execution-order`,
method: 'PUT',
body: {
owners: ['agent-planner', 'agent-builder', 'agent-qa'],
},
});
// 4. Start execution
await callApi({
endpoint: `/api/hall/tasks/${task.id}/start`,
method: 'POST',
});
// 5. Monitor execution with SSE
const eventSource = new EventSource(
`http://127.0.0.1:4310/api/hall/tasks/${task.id}/stream`
);
eventSource.onmessage = (event) => {
const update = JSON.parse(event.data);
console.log(`[${update.agentId}] ${update.content}`);
};
// 6. Wait for completion
await pollUntil(
async () => {
const status = await callApi({
endpoint: `/api/hall/tasks/${task.id}`,
method: 'GET',
requiresAuth: false,
});
return status.phase === 'review' || status.phase === 'blocked';
},
{ intervalMs: 5000, timeoutMs: 600000 }
);
eventSource.close();
}
async function pollUntil(
condition: () => Promise<boolean>,
options: { intervalMs: number; timeoutMs: number }
): Promise<void> {
const startTime = Date.now();
while (!(await condition())) {
if (Date.now() - startTime > options.timeoutMs) {
throw new Error('Polling timeout');
}
await new Promise((resolve) => setTimeout(resolve, options.intervalMs));
}
}// src/lib/memory-inspector.ts
async function inspectAgentMemory(agentId: string) {
// Get memory status
const status = await callApi({
endpoint: `/api/memory/${agentId}/status`,
method: 'GET',
requiresAuth: false,
});
console.log(`Memory status for ${agentId}:`, {
isUsable: status.is_usable,
isSearchable: status.is_searchable,
fileCount: status.file_count,
totalSizeKb: status.total_size_kb,
});
// Read daily memory
const dailyMemory = await callApi({
endpoint: `/api/memory/${agentId}/daily`,
method: 'GET',
requiresAuth: false,
});
console.log('Recent daily entries:', dailyMemory.entries.slice(-5));
// Search memory (if enabled)
if (status.is_searchable) {
const searchResults = await callApi({
endpoint: `/api/memory/${agentId}/search?q=task+completion`,
method: 'GET',
requiresAuth: false,
});
console.log('Search results:', searchResults.matches);
}
}// src/lib/document-manager.ts
async function updateSharedDocument(filename: string, content: string) {
// Ensure write mode is enabled
if (process.env.READONLY_MODE === 'true') {
throw new Error('Cannot save in READONLY_MODE');
}
return callApi({
endpoint: '/api/documents/shared',
method: 'PUT',
body: {
filename,
content,
},
requiresAuth: true,
});
}
async function getAgentDocument(agentId: string, docType: string) {
return callApi({
endpoint: `/api/documents/agent/${agentId}/${docType}`,
method: 'GET',
requiresAuth: false,
});
}~/.openclaw/openclaw.json{
"agents": [
{
"id": "agent-manager",
"display_name": "Manager",
"role": "Coordinates work and assigns tasks"
},
{
"id": "agent-builder",
"display_name": "Builder",
"role": "Implements features and writes code"
}
]
}// Verify OpenClaw is reachable
const health = await callApi({
endpoint: '/api/health',
method: 'GET',
requiresAuth: false,
});
console.log('OpenClaw connection:', health.openclaw_reachable);
console.log('Memory accessible:', health.memory_accessible);
console.log('Task storage:', health.task_storage_ready);LOCAL_API_TOKEN.env# Generate a secure token
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Add to .env
LOCAL_API_TOKEN=<generated-token>// Debug execution state
const taskState = await callApi({
endpoint: `/api/hall/tasks/${taskId}`,
method: 'GET',
requiresAuth: false,
});
console.log('Phase:', taskState.phase);
console.log('Owners:', taskState.execution_order?.owners);
console.log('Current owner index:', taskState.execution_order?.current_owner_index);const contextMetrics = await callApi({
endpoint: '/api/usage/context-pressure',
method: 'GET',
requiresAuth: false,
});
// Find sessions near limit
const critical = contextMetrics.sessions.filter(
(s: any) => s.usage / s.limit > 0.9
);
console.log('Critical sessions:', critical);READONLY_MODELOCAL_TOKEN_AUTH_REQUIREDcrypto.randomBytes(32).toString('hex')LOCAL_API_TOKENUI_BIND_ADDRESS=127.0.0.1APPROVAL_ACTIONS_DRY_RUN=true