npm run setup # Auto-detect OpenClaw config, write .env.local
npm run dev # Start dev server (Turbopack, port 3000)
npm test # Run all 781 tests via Vitest (32 suites)
npx tsc --noEmit # Type-check (expect 0 errors)
npx next build # Production buildThe npm package is
clawport-ui. The CLI command isclawport. The separateclawportnpm package is unrelated.
npm install -g clawport-ui
clawport setup # Auto-detect config, write .env.local into package dir
clawport dev # Start dev server
clawport start # Build + start production server
clawport status # Check gateway reachability + env config
clawport help # Show usageThe CLI resolves its own package root via import.meta.url, so all commands work regardless of the user's current working directory. Entry point: bin/clawport.mjs.
ClawPort is a Next.js 16 dashboard for managing OpenClaw AI agents. It provides an org chart (Org Map), direct agent chat with multimodal support, cron monitoring, a cost dashboard, an activity console with live log streaming, and memory browsing. All AI calls route through the OpenClaw gateway -- no separate API keys needed.
- Next.js 16.1.6 (App Router, Turbopack)
- React 19.2.3, TypeScript 5
- Tailwind CSS 4 with CSS custom properties for theming
- Vitest 4 with jsdom environment
- OpenAI SDK (routed to Claude via OpenClaw gateway at localhost:18789)
- React Flow (@xyflow/react) for org chart
WORKSPACE_PATH # Required -- path to .openclaw workspace (auto-detected)
OPENCLAW_BIN # Required -- path to openclaw binary
OPENCLAW_GATEWAY_TOKEN # Required -- gateway auth token
ELEVENLABS_API_KEY # Optional -- voice indicatorsRun npm run setup to auto-detect all required values from your local OpenClaw installation.
Workspace detection order: ~/.openclaw/agents/main/workspace (current layout) → ~/.openclaw/workspace (legacy). Falls back to manual prompt if neither exists.
Global install: When installed via npm install -g clawport-ui, .env.local may not be writable in the package directory. Setup falls back to ~/.config/clawport-ui/.env.local (XDG-compliant). The CLI (bin/clawport.mjs) checks both locations when loading env vars.
next.config.mjs sets allowedDevOrigins: ["*"] so the dev server works over Tailscale, LAN, or any non-localhost origin without cross-origin errors.
loadRegistry() checks:
1. $WORKSPACE_PATH/clawport/agents.json (user override)
2. Auto-discovered from $WORKSPACE_PATH (agents/ directory scan)
3. Bundled lib/agents.json (default example)
lib/agents-registry.ts exports loadRegistry(). lib/agents.ts calls it to build the full agent list (merging in SOUL.md content from the workspace).
Auto-discovery scans $WORKSPACE_PATH/agents/ for subdirectories containing a SOUL.md file. Each becomes an agent entry with sensible defaults (color from rotating palette, name from SOUL.md heading or directory slug). If $WORKSPACE_PATH/SOUL.md exists, it becomes the root orchestrator. This means any OpenClaw workspace works out of the box -- no agents.json needed.
Users can still drop a clawport/agents.json into their workspace for full control over names, colors, hierarchy, and tools.
OnboardingWizard / Settings page
-> ClawPortSettings.operatorName (localStorage)
-> settings-provider.tsx (React context)
-> NavLinks.tsx (dynamic initials + display name)
-> ConversationView.tsx (sends operatorName in POST body)
-> /api/chat/[id] route (injects into system prompt: "You are speaking with {operatorName}")
No hardcoded operator names anywhere. Falls back to "Operator" / "??" when unset.
Client -> POST /api/chat/[id] -> OpenAI SDK -> localhost:18789/v1/chat/completions -> Claude
(streaming SSE response)
The gateway's HTTP endpoint strips image_url content. Vision uses the CLI agent pipeline:
Client resizes image to 1200px max (Canvas API)
-> base64 data URL in message
-> POST /api/chat/[id]
-> Detects image in LATEST user message only (not history)
-> execFile: openclaw gateway call chat.send --params <json> --token <token>
-> Polls: openclaw gateway call chat.history every 2s
-> Matches response by timestamp >= sendTs
-> Returns assistant text via SSE
Key files: lib/anthropic.ts (send + poll logic), app/api/chat/[id]/route.ts (routing)
Why send-then-poll? chat.send is async -- it returns {runId, status: "started"} immediately. The --expect-final flag doesn't block for this method. We poll chat.history until the assistant's response appears.
Why CLI and not WebSocket? The gateway WebSocket requires device keypair signing for operator.write scope (needed by chat.send). The CLI has the device keys; custom clients don't.
Why resize to 1200px? macOS ARG_MAX is 1MB. Unresized photos can produce multi-MB base64 that exceeds CLI argument limits (E2BIG error). 1200px JPEG at 0.85 quality keeps base64 well under 1MB.
Browser MediaRecorder (webm/opus or mp4)
-> AudioContext AnalyserNode captures waveform (40-60 samples)
-> Stop -> audioBlob + waveform data
-> POST /api/transcribe (Whisper via gateway)
-> Transcription text sent as message content
-> Audio data URL + waveform stored in message for playback
Key files: lib/audio-recorder.ts, lib/transcribe.ts, components/chat/VoiceMessage.tsx
Messages stored in localStorage as JSON. Media attachments are base64 data URLs (not blob URLs -- those don't survive reload). The conversations.ts module provides addMessage(), updateLastMessage(), and parseMedia(). Messages have three roles: user, assistant, and system (slash command results). System messages are never sent to the API -- they're filtered out before building the request.
Client-side slash commands in the chat input, handled entirely in the browser (never sent to the gateway):
User types "/" -> matchCommands() shows autocomplete dropdown
-> Arrow keys navigate, Enter/Tab selects, Escape dismisses
-> parseSlashCommand() validates input
-> executeCommand() returns content string + optional action
-> System message rendered as accent-bordered card
| Command | Description |
|---|---|
/clear |
Clear conversation history |
/help |
Show available commands |
/info |
Show agent profile summary (name, title, tools, memory) |
/soul |
Show agent's SOUL.md persona document |
/tools |
List agent's available tools |
/crons |
Show agent's scheduled cron jobs |
Key files: lib/slash-commands.ts (command registry, parser, matcher, executor), lib/slash-commands.test.ts (35 tests)
System message rendering: System messages skip avatar/timestamp/spacing logic -- shouldShowAvatar and shouldShowTimestamp look through system messages to the previous non-system message for grouping. Media parsing is also skipped for system messages.
The Cost Dashboard (app/costs/page.tsx) provides token usage, cost analysis, and AI-powered optimization derived from cron run data:
GET /api/costs
-> getCronRuns() (lib/cron-runs.ts) reads run history from workspace
-> computeCostSummary() (lib/costs.ts) transforms runs into:
- Per-run costs (model pricing lookup)
- Per-job aggregation (total + median cost)
- Daily cost timeline
- Model breakdown (token distribution)
- Anomaly detection (runs >5x median tokens)
- Week-over-week comparison
- Cache savings estimation
- Optimization score (0-100, four sub-dimensions)
- Optimization insights (actionable recommendations with projected savings)
Optimization engine: computeOptimizationScore() produces a 0-100 composite score from cache utilization, model tiering, anomaly count, and output efficiency. computeOptimizationInsights() derives actionable findings: cache enablement, model downgrades (Opus->Sonnet->Haiku), anomaly alerts, verbose output detection. Each insight includes a projected savings amount and an AI action prompt.
AI Cost Analysis: The page includes a collapsible AI analysis panel (same SSE streaming pattern as pipeline health check). buildCostAnalysisPrompt() generates a detailed prompt from the cost summary. An inline chat appears after the analysis completes for follow-up questions. Insight "Fix" buttons send targeted prompts to the inline chat.
Key files: lib/costs.ts (all computation, 36 tests), components/costs/CostsPage.tsx (UI with optimization score, insights, AI analysis, daily bar chart, job table, model breakdown, anomaly alerts)
Pricing (per 1M tokens): Opus 4.6 $5/$25, Sonnet 4.6 $3/$15, Haiku 4.5 $1/$5. Cache reads 0.1x input, cache writes 1.25x (5-min TTL) or 2x (1-hr TTL). Batch API 50% discount. Falls back to Sonnet pricing for unknown models. Prefix matching handles versioned model IDs.
The Activity page (app/activity/page.tsx) shows a log browser for historical cron and config events. Live streaming is handled by a global floating widget:
"Open Live Stream" button (Activity page)
-> dispatches CustomEvent('clawport:open-stream-widget')
-> LiveStreamWidget (components/LiveStreamWidget.tsx) listens, opens expanded
-> fetch('/api/logs/stream') -> SSE stream -> parseSSEBuffer() (lib/sse.ts)
-> Lines rendered with level pills (INF/WRN/ERR/DBG), click to expand raw JSON
The widget is mounted in app/layout.tsx (global, survives navigation). Three visual states: hidden (default), collapsed pill, expanded panel. Collapsing does NOT stop the stream. Close stops + hides.
Key files: components/LiveStreamWidget.tsx (widget), lib/sse.ts (SSE parser), app/api/logs/stream/route.ts (SSE endpoint spawning openclaw logs --follow --json)
The Pipelines tab (PipelineGraph.tsx) includes an AI health check that analyzes the full cron pipeline system:
"Pipeline Health Check" button
-> buildHealthCheckPrompt(crons, pipelines, agents) (lib/pipeline-utils.ts)
-> POST /api/chat/${rootAgent.id} (SSE stream)
-> Streaming UI: spinner + skeleton -> streamed text with blinking cursor -> "Complete"
-> Inline chat appears below results for follow-up questions
Health check prompt includes: agent ownership per job (flags UNOWNED jobs), pipeline edges, schedule gaps, missing deliveries, and recommendations.
Inline chat pattern: After the health check completes, a chat input appears. Follow-up messages inject the original health check prompt + response as context, then append the conversation history. Uses the same SSE streaming pattern as PipelineDetailPanel.
Clear pipelines: The "Clear Pipelines" button POSTs [] to /api/pipelines, resetting to the empty state with the setup wizard.
Key files: components/crons/PipelineGraph.tsx (UI), lib/pipeline-utils.ts (buildHealthCheckPrompt, buildPipelineLayout), app/api/pipelines/route.ts (GET/POST)
Five themes defined via CSS custom properties in app/globals.css:
- Dark (default), Glass, Color, Light, System
- Components use semantic tokens:
--bg,--text-primary,--accent,--separator, etc. - Theme state managed by
app/providers.tsxThemeProvider (localStorage)
components/OnboardingWizard.tsx -- 5-step first-run setup wizard:
- Welcome -- portal name, subtitle, operator name (with live sidebar preview)
- Theme -- pick from available themes (applies live)
- Accent Color -- color preset grid
- Voice Chat -- microphone permission test (optional)
- Overview -- feature summary (Agent Map, Chat, Kanban, Crons, Memory)
First-run detection: checks localStorage('clawport-onboarded'). If absent, wizard shows automatically.
Mounting: OnboardingWizard is rendered in app/layout.tsx (always present, self-hides when not needed).
Re-run: settings page has a button that renders <OnboardingWizard forceOpen onClose={...} />. When forceOpen is true, the wizard pre-populates from current settings and does not set clawport-onboarded on completion.
lib/env.ts exports requireEnv(name) -- throws a clear error with the missing variable name and a pointer to .env.example.
Critical pattern: call requireEnv() inside functions, never at module top level. This prevents imports from crashing during next build or test runs when env vars are not set.
Used by: lib/memory.ts, lib/cron-runs.ts, lib/kanban/chat-store.ts, lib/crons.ts
| Route | Method | Purpose |
|---|---|---|
/api/agents |
GET | All agents from registry + SOUL.md |
/api/chat/[id] |
POST | Agent chat -- text (streaming) or vision (send+poll) |
/api/crons |
GET | Cron jobs via openclaw cron list --json |
/api/memory |
GET | Memory dashboard: files, config, status, stats |
/api/costs |
GET | Cost summary computed from cron run token usage |
/api/logs |
GET | Historical log entries (cron runs + config audit) |
/api/logs/stream |
GET | SSE stream of live logs via openclaw logs --follow --json |
/api/pipelines |
GET | Load pipeline config from workspace |
/api/pipelines |
POST | Save (or clear) pipeline config -- expects JSON array |
/api/tts |
POST | Text-to-speech via OpenClaw |
/api/transcribe |
POST | Audio transcription via Whisper |
| File | Purpose |
|---|---|
lib/agents.ts |
Agent list builder -- calls loadRegistry(), merges SOUL.md |
lib/agents-registry.ts |
loadRegistry() -- workspace override -> bundled fallback |
lib/agents.json |
Bundled default agent registry |
lib/anthropic.ts |
Vision pipeline: hasImageContent, extractImageAttachments, buildTextPrompt, sendViaOpenClaw (send + poll), execCli |
lib/audio-recorder.ts |
createAudioRecorder() -- MediaRecorder + waveform via AnalyserNode |
lib/conversations.ts |
Conversation store with localStorage persistence |
lib/crons.ts |
Cron data fetching via CLI, dynamic agent matching by ID prefix |
lib/env.ts |
requireEnv(name) -- safe env var access with clear errors |
lib/multimodal.ts |
buildApiContent() -- converts Message+Media to OpenAI API format |
lib/settings.ts |
ClawPortSettings type, loadSettings(), saveSettings() (localStorage) |
lib/transcribe.ts |
transcribe(audioBlob) -- Whisper API with graceful fallback |
lib/memory.ts |
Memory dashboard: getMemoryFiles() (dynamic discovery), getMemoryConfig() (openclaw.json reader), getMemoryStatus() (CLI status), computeMemoryStats() (pure stats) |
lib/validation.ts |
validateChatMessages() -- validates text + multimodal content arrays |
lib/sse.ts |
parseSSEBuffer(), parseSSELine() -- client-safe SSE stream parser |
lib/logs.ts |
getLogEntries(), computeLogSummary() -- historical log parsing (cron + config) |
lib/costs.ts |
getCostSummary() -- cost analysis, optimization score/insights, AI analysis prompt builder |
lib/sanitize.ts |
renderMarkdown(), colorizeJson(), escapeHtml() -- safe HTML rendering |
lib/slash-commands.ts |
Slash command registry, parser (parseSlashCommand), matcher (matchCommands), executor (executeCommand) |
lib/id.ts |
generateId() -- UUID generator with fallback for non-secure contexts (HTTP, older browsers) |
lib/pipeline-utils.ts |
Pipeline layout builder, health check prompt builder, cron context builder -- all pure functions |
lib/cron-pipelines.ts |
Pipeline types, getAllPipelineJobNames() |
lib/cron-pipelines.server.ts |
loadPipelines() -- reads pipeline config from workspace |
| Component | Purpose |
|---|---|
ConversationView.tsx |
Main chat: messages, input, recording, paste/drop, file staging, slash commands. Sends operatorName in POST body. |
VoiceMessage.tsx |
Waveform playback: play/pause + animated bar visualization |
FileAttachment.tsx |
File bubble: icon by type + name + size + download |
MediaPreview.tsx |
Pre-send strip of staged attachments with remove buttons |
AgentList.tsx |
Desktop agent sidebar with unread badges |
| Component | Purpose |
|---|---|
PipelineGraph.tsx |
React Flow pipeline graph, health check panel with inline chat, standalone crons grid |
PipelineDetailPanel.tsx |
Slide-in panel for individual job details with inline agent chat |
PipelineWizard.tsx |
AI-powered pipeline auto-detection wizard |
WeeklySchedule.tsx |
Weekly schedule heatmap visualization |
| Component | Purpose |
|---|---|
OnboardingWizard.tsx |
5-step first-run setup wizard (name, theme, accent, mic, overview) |
NavLinks.tsx |
Sidebar nav with dynamic operator initials + name from settings |
Sidebar.tsx |
Sidebar layout shell |
AgentAvatar.tsx |
Agent emoji/image avatar with optional background |
DynamicFavicon.tsx |
Updates favicon based on portal emoji/icon settings |
LiveStreamWidget.tsx |
Global floating live log stream widget (hidden/collapsed/expanded) |
| File | Purpose |
|---|---|
bin/clawport.mjs |
CLI entry point -- clawport dev, clawport setup, clawport status, etc. Resolves package root via import.meta.url. Loads .env.local from package dir or ~/.config/clawport-ui/ fallback via getEnvLocalPath() |
scripts/setup.mjs |
npm run setup / clawport setup -- auto-detects WORKSPACE_PATH (current + legacy paths), OPENCLAW_BIN, gateway token; writes .env.local. Falls back to ~/.config/clawport-ui/ if package dir is not writable. Accepts --cwd=<path> flag. Can auto-enable HTTP chat completions endpoint in openclaw.json. |
32 test suites, 781 tests total.
npx vitest run # All tests
npx vitest run lib/anthropic.test.ts # Single suite
npx vitest --watch # Watch modeKey test patterns:
vi.mock('child_process')for CLI tests (anthropic.ts)vi.useFakeTimers({ shouldAdvanceTime: true })for polling testsvi.stubEnv()for environment variable tests- jsdom environment for DOM-dependent tests
- No external charting/media libraries -- native Web APIs (Canvas, MediaRecorder, AudioContext)
- Base64 data URLs for all persisted media (not blob URLs)
- CSS custom properties for theming -- no Tailwind color classes directly
- Inline styles referencing CSS vars (e.g.,
style={{ color: 'var(--text-primary)' }}) - Tests colocated with source:
lib/foo.ts+lib/foo.test.ts - Agent chat uses
claude-sonnet-4-6model via OpenClaw gateway - No em dashes in agent responses (enforced via system prompt)
- Call
requireEnv()inside functions, not at module top level - No hardcoded operator names -- use
operatorNamefrom settings context
ClawPort is the UI layer for OpenClaw, the AI agent runtime. The boundary is deliberate: ClawPort reads workspace data and calls gateway endpoints but never executes agents directly.
Gateway protocol: All AI calls route through the OpenClaw gateway at localhost:18789. Text chat uses /v1/chat/completions (streaming SSE). Vision uses the CLI pipeline (openclaw gateway call chat.send + poll chat.history) because the HTTP endpoint strips image content and chat.send requires device keypair signing.
CLI integration: ClawPort shells out to openclaw for operations not exposed via HTTP -- cron list, logs --follow, gateway call. The binary path is set via OPENCLAW_BIN.
ACP (Agent Client Protocol): OpenClaw's protocol for client-agent interaction. Sessions are scoped by sessionKey, authenticated via device keypairs. ClawPort uses ACP implicitly through the gateway and CLI.
Scope boundary: Dashboard features (visualization, chat UI, settings, theming) belong here. Agent execution, gateway protocol, cron scheduling, memory storage, and CLI commands belong in OpenClaw.
Recent OpenClaw features: ACP sessions (persistent conversations), backup commands (openclaw backup create/restore), context engine plugins (pluggable agent context providers), TUI workspace inference, output provenance tracking.
See docs/OPENCLAW.md for the full integration reference.
Edit lib/agents.json (or drop a custom agents.json into $WORKSPACE_PATH/clawport/). Auto-appears in map, chat, and detail pages.
Create $WORKSPACE_PATH/clawport/agents.json with your own agent entries. ClawPort loads this instead of the bundled default. Format matches lib/agents.json.
Go to Settings page and click "Re-run Setup Wizard". This opens the wizard with forceOpen so it pre-populates current values and does not reset the clawport-onboarded flag.
- Add the field to
ClawPortSettingsinterface inlib/settings.ts - Add a default value in
DEFAULTS - Add parsing logic in
loadSettings() - Add a setter method in
app/settings-provider.tsx - Consume via
useSettings()hook in components
- Add the command to
COMMANDSarray inlib/slash-commands.ts - Add a
caseinexecuteCommand()switch statement - Add tests in
lib/slash-commands.test.ts - The autocomplete dropdown picks up new commands automatically
Edit app/api/chat/[id]/route.ts -- change the model field in openai.chat.completions.create().
Add a [data-theme="name"] block in app/globals.css with all CSS custom properties. Add the theme ID to lib/themes.ts.
- Check server console for
sendViaOpenClaw execFile error:orsendViaOpenClaw: timed out - Test CLI directly:
openclaw gateway call chat.send --params '{"sessionKey":"agent:main:clawport","idempotencyKey":"test","message":"describe","attachments":[]}' --token <token> --json - Check history:
openclaw gateway call chat.history --params '{"sessionKey":"agent:main:clawport"}' --token <token> --json - Verify gateway is running:
openclaw gateway call health --token <token>