nyk Claude Opus 4.6 commited on
Commit
b6ecafa
·
unverified ·
1 Parent(s): d842037

feat(refactor): ready for manual QA after main sync (#274)

Browse files

* fix: preserve gateway token query in websocket URLs

* fix: classify secure-context device identity handshake errors

* fix: normalize trailing dot in host allowlist checks

* fix: support proxied gateway websocket paths and tailnet host normalization

* fix: auto-connect startup to primary configured gateway

* fix: keep gateway tokens server-side only

* fix: allow authenticated viewers to resolve gateway connect credentials

* fix: identify mission control as operator gateway client

* fix: redirect remote http sessions to https for gateway auth

* fix: support URL-style gateway hosts in health probes

* fix: resolve primary gateway credentials from detected openclaw runtime

* fix: hide duplicate gateway connection summary when managed

* refactor: remove super admin and workspaces panels from UI navigation

* fix: treat configured gateways as full-mode capability

* refactor: move promo banner copy into subtle footer note

* fix: stabilize gateway websocket connect protocol detection

* test: cover https forwarded proto for gateway websocket url

* fix: load canonical agent files and memory in detail panel

* fix: resolve agent files from openclaw workspace conventions

* fix: persist websocket client across route remounts

* feat: allow deleting agents with optional workspace removal

* feat: refresh mission control branding and favicon assets

* feat: complete github parity sync implementation

* chore: remove e2e temp artifacts from repo

* feat: add embedded /chat panel with shared chat workspace

* feat: unify sessions navigation into chat panel

* feat: show local Claude/Codex sessions in chat list

* feat: enable local session continuation and chat tagging

* fix: correct local codex session recency detection

* fix: refresh local session age and anchor chat composer

* refactor: make chat provider-session-first by mode

* fix: add local provider and MC health rows in overview

* feat: finalize tenant-scoped workspaces and full e2e coverage

* feat: improve session workbench controls and smoke coverage

* refactor: extract SaaS code to separate pro repo

- Add registerAuthResolver() hook to auth.ts
- Add registerMigrations() hook to migrations.ts
- Remove saas config block, SaaS modules, Pro API routes
- Keep adapters, super-admin routes, migration 032

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add framework adapters and self-update mechanism

- Framework adapter layer for multi-agent registration (autogen, crewai, langgraph, claude-sdk, openclaw, generic)
- Self-update API endpoint (admin-only git pull + install + build)
- Update banner UI component showing available versions with dismiss

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update README stats, remove stale Super Admin refs, improve self-update

- Panel count 28→32, API routes 66→95, migrations 21→30
- Remove Super Admin from UI-facing docs (APIs remain)
- Document framework adapters and self-update mechanism
- Mark workspace isolation, adapters, projects as completed in roadmap
- Self-update now uses tag-based checkout instead of branch pull
- Plugin hook comments: "Pro" → "extensions"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add skills hub with registry integration, bidirectional sync, and local agent discovery

- Bidirectional disk↔DB skill sync via scheduler (60s interval, SHA-256 change detection, disk-wins conflict resolution)
- ClawdHub + skills.sh registry proxy with search, install, and security scanning (9 rules: prompt injection, credential leaks, data exfiltration, obfuscated content)
- Local agent discovery from ~/.agents/, ~/.codex/agents/, ~/.claude/agents/ with bidirectional sync
- DB-backed skills API with filesystem fallback, admin-only install, rate limiting
- Skills panel: installed/registry tabs, security badges, friendly source labels, OpenClaw gateway support
- Agent panel: local sync button, source badges (local/gateway)
- Migrations 033 (skills table) and 034 (agents source/hash/workspace columns)
- Full test coverage: 24 unit tests, 34 E2E tests (286 total suite green)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add per-agent rate limiting and agent self-registration

- Per-agent rate limiter keyed on x-agent-name header (falls back to IP)
- Agent heartbeat: 30 req/min per agent, task polling: 20 req/min per agent
- Rate limit response headers (Retry-After, X-RateLimit-*) for agent backoff
- POST /api/agents/register: self-service registration with viewer-level auth
- Idempotent registration (re-registering updates last_seen, returns existing)
- Name validation, role whitelist, capabilities/framework in config
- Self-registration rate-limited to 5/min per IP
- 9 E2E tests for self-registration (295 total suite green)
- README: updated API route count (97), test counts, new endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: enhance agent cost panel, OAuth approval UI, and framework adapter gateway

- Agent Cost Panel: add task cost attribution drill-down, cost share
percentages, bar chart comparison, 5th summary card for task-attributed
costs, 30s auto-refresh, and tabbed expanded view (tasks/models)
- OAuth Approval UI: replace window.prompt() with inline role selector
and note input, add avatar display, show animated pending count badge,
add dedicated "awaiting approval" state on login page
- Framework Adapter Gateway: wire GenericAdapter.getAssignments() to
query task queue, add POST /api/adapters route for framework-agnostic
agent actions (register, heartbeat, report, assignments, disconnect)
- Clean up dead api-keys import and DB-backed key resolution from
auth.ts (moved to Pro repo)
- Resolve README merge conflicts, update route count to 98

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: complete free-tier functionality — adapters, workspace CRUD, agent sync, and UI polish

- Implement all 5 framework adapter stubs (claude-sdk, crewai, langgraph, autogen, openclaw)
with shared queryPendingAssignments() helper to eliminate SQL duplication
- Add recurring gateway_agent_sync scheduler task (was startup-only)
- Add workspace CRUD API: POST/PUT/DELETE /api/workspaces with tenant isolation
- Add local agent discovery for flat .md files (Claude Code agent format with YAML frontmatter)
- Add per-agent cost breakdown API (GET /api/tokens/by-agent)
- Add API key rotation endpoint (GET/POST /api/tokens/rotate)
- Add Google OAuth disconnect endpoint
- Polish login page with inline Google Sign-In button
- Enhance settings panel with Security tab (API key management, OAuth)
- Enhance agent cost panel with per-agent DB view
- Add Awesome OpenClaw as third skill registry source
- Add integration connectivity test fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: include session-message component (required by chat-workspace)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: gateway dot color should reflect live connection state

When WebSocket is connected, show green dot regardless of stored
probe status. Prevents misleading red dot + green CONNECTED badge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: agent creation progress modal and openclaw CLI flag

- Remove invalid --name flag from openclaw agents add CLI invocation
- Add multi-step progress UI to CreateAgentModal showing DB insert,
gateway write, and workspace provisioning steps with animated status
- Progress view replaces review content during creation with spinner,
checkmark, and error states per step
- Auto-close on success after 1.5s, retry/close buttons on error
- Squad panel: add status-based card edge colors and glow styles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: task dispatch — scheduler polls assigned tasks and runs agents via openclaw CLI

Adds a task_dispatch scheduler job that picks up tasks in 'assigned' status,
executes them via `openclaw agent --local --json`, and moves them to 'review'
with the agent's response as resolution + comment.

* feat: link dispatched tasks to agent sessions — view session from task detail

- task-dispatch: extract sessionId from openclaw JSON response, store in task metadata
- task detail modal: "View Session" button navigates to /chat with the agent's session transcript
- shows pulsing "Live" indicator when task is in_progress
- agent squad panel: show quality_review and done counts in task stats

* feat: automated Aegis quality review — scheduler polls review tasks and approves/rejects

- aegis_review scheduler job picks up tasks in 'review' status every 60s
- runs openclaw agent to evaluate task resolution quality
- approved → done, rejected → in_progress with feedback as comment
- quality-review API: rejected reviews now push task back to in_progress
- approved reviews work for any reviewer (not just aegis)

* feat: natural language recurring tasks + Claude Code task bridge

Add NL schedule parser (zero deps) for creating recurring tasks via
"every morning at 9am" style input. Template-clone pattern spawns dated
child tasks on cron schedule with Aegis quality gates per instance.

Read-only bridge surfaces Claude Code team tasks and configs from
~/.claude/tasks/ and ~/.claude/teams/ on the MC dashboard.

New files: schedule-parser.ts, recurring-tasks.ts, claude-tasks.ts,
API routes for /claude-tasks and /schedule-parse.
Modified: scheduler.ts (recurring_task_spawn), migrations.ts (036),
task-board-panel.tsx (schedule UI + badges + CC section),
cron-management-panel.tsx (CC teams section).

* docs: update README with recurring tasks and Claude Code task bridge

Add sections for natural language recurring tasks, Claude Code task
bridge, new API endpoints, architecture tree entries, and roadmap items.

* fix: agent card redesign, gateway badge tooltip, and ws:// for localhost gateways

- Compact agent cards: remove 4 colored stat boxes, show inline task stats,
display model name from config, remove session info box, remove Busy button
- Gateway ConnectionBadge: rich hover tooltip with host, latency, WS/SSE status
- Fix gateway connect over T

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +2 -1
  2. .github/workflows/quality-gate.yml +1 -1
  3. .gitignore +6 -0
  4. .node-version +1 -0
  5. .nvmrc +1 -0
  6. Dockerfile +11 -4
  7. README.md +177 -27
  8. SECURITY.md +38 -1
  9. SKILL.md +278 -0
  10. docker-compose.hardened.yml +21 -0
  11. docker-compose.yml +22 -0
  12. docs/LANDING-PAGE-HANDOFF.md +248 -0
  13. docs/SECURITY-HARDENING.md +277 -0
  14. docs/deployment.md +26 -0
  15. docs/plans/2026-03-10-onboarding-walkthrough-hardening.md +31 -0
  16. install.sh +429 -0
  17. next.config.js +6 -2
  18. openclaw_hardening_guide.md +124 -0
  19. package.json +17 -11
  20. playwright.config.ts +2 -4
  21. pnpm-lock.yaml +781 -0
  22. public/brand/claude-logo.png +3 -0
  23. public/brand/codex-logo.png +3 -0
  24. public/brand/hermes-logo.png +3 -0
  25. public/brand/mc-logo-128.png +3 -0
  26. public/brand/mc-logo-256.png +3 -0
  27. public/brand/mc-logo-512.png +3 -0
  28. public/brand/openclaw-logo.png +3 -0
  29. public/mc-logo.png +3 -0
  30. public/mc.png +3 -0
  31. scripts/check-node-version.mjs +16 -0
  32. scripts/deploy-standalone.sh +251 -0
  33. scripts/e2e-openclaw/start-e2e-server.mjs +44 -2
  34. scripts/generate-env.sh +81 -0
  35. scripts/security-audit.sh +168 -0
  36. scripts/smoke-staging.mjs +168 -0
  37. scripts/start-standalone.sh +33 -0
  38. scripts/station-doctor.sh +189 -0
  39. skills/mission-control-installer/README.md +68 -0
  40. skills/mission-control-installer/skill.json +27 -0
  41. skills/mission-control-manage/README.md +104 -0
  42. skills/mission-control-manage/skill.json +20 -0
  43. src/app/[[...panel]]/page.tsx +369 -81
  44. src/app/api/activities/route.ts +16 -4
  45. src/app/api/adapters/route.ts +118 -0
  46. src/app/api/agents/[id]/files/route.ts +153 -0
  47. src/app/api/agents/[id]/heartbeat/route.ts +4 -0
  48. src/app/api/agents/[id]/memory/route.ts +65 -4
  49. src/app/api/agents/[id]/route.ts +72 -26
  50. src/app/api/agents/[id]/soul/route.ts +9 -9
.env.example CHANGED
@@ -67,7 +67,8 @@ NEXT_PUBLIC_GATEWAY_HOST=
67
  NEXT_PUBLIC_GATEWAY_PORT=18789
68
  NEXT_PUBLIC_GATEWAY_PROTOCOL=
69
  NEXT_PUBLIC_GATEWAY_URL=
70
- # NEXT_PUBLIC_GATEWAY_TOKEN= # Optional, set if gateway requires auth token
 
71
  # Gateway client id used in websocket handshake (role=operator UI client).
72
  NEXT_PUBLIC_GATEWAY_CLIENT_ID=openclaw-control-ui
73
 
 
67
  NEXT_PUBLIC_GATEWAY_PORT=18789
68
  NEXT_PUBLIC_GATEWAY_PROTOCOL=
69
  NEXT_PUBLIC_GATEWAY_URL=
70
+ # Do not expose gateway tokens via NEXT_PUBLIC_* variables.
71
+ # Keep gateway auth secrets server-side only (OPENCLAW_GATEWAY_TOKEN / GATEWAY_TOKEN).
72
  # Gateway client id used in websocket handshake (role=operator UI client).
73
  NEXT_PUBLIC_GATEWAY_CLIENT_ID=openclaw-control-ui
74
 
.github/workflows/quality-gate.yml CHANGED
@@ -24,7 +24,7 @@ jobs:
24
  - name: Setup Node
25
  uses: actions/setup-node@v4
26
  with:
27
- node-version: 20
28
  cache: 'pnpm'
29
 
30
  - name: Install dependencies
 
24
  - name: Setup Node
25
  uses: actions/setup-node@v4
26
  with:
27
+ node-version-file: '.nvmrc'
28
  cache: 'pnpm'
29
 
30
  - name: Install dependencies
.gitignore CHANGED
@@ -35,6 +35,12 @@ aegis/
35
  # Playwright
36
  test-results/
37
  playwright-report/
 
 
 
 
 
 
38
 
39
  # Claude Code context files
40
  CLAUDE.md
 
35
  # Playwright
36
  test-results/
37
  playwright-report/
38
+ .tmp/
39
+ .playwright-mcp/
40
+
41
+ # Local QA screenshots
42
+ /e2e-debug-*.png
43
+ /e2e-channels-*.png
44
 
45
  # Claude Code context files
46
  CLAUDE.md
.node-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 22
.nvmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ 22
Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM node:20-slim AS base
2
  RUN corepack enable && corepack prepare pnpm@latest --activate
3
  WORKDIR /app
4
 
@@ -20,7 +20,14 @@ COPY --from=deps /app/node_modules ./node_modules
20
  COPY . .
21
  RUN pnpm build
22
 
23
- FROM node:20-slim AS runtime
 
 
 
 
 
 
 
24
  WORKDIR /app
25
  ENV NODE_ENV=production
26
  RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
@@ -30,11 +37,11 @@ COPY --from=build /app/.next/static ./.next/static
30
  COPY --from=build /app/src/lib/schema.sql ./src/lib/schema.sql
31
  # Create data directory with correct ownership for SQLite
32
  RUN mkdir -p .data && chown nextjs:nodejs .data
33
- RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/*
34
  USER nextjs
35
  ENV PORT=3000
36
  EXPOSE 3000
37
  ENV HOSTNAME=0.0.0.0
38
  HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
39
- CMD curl -f http://localhost:${PORT:-3000}/login || exit 1
40
  CMD ["node", "server.js"]
 
1
+ FROM node:22.22.0-slim AS base
2
  RUN corepack enable && corepack prepare pnpm@latest --activate
3
  WORKDIR /app
4
 
 
20
  COPY . .
21
  RUN pnpm build
22
 
23
+ FROM node:22.22.0-slim AS runtime
24
+
25
+ ARG MC_VERSION=dev
26
+ LABEL org.opencontainers.image.source="https://github.com/openclaw/mission-control"
27
+ LABEL org.opencontainers.image.description="Mission Control - operations dashboard"
28
+ LABEL org.opencontainers.image.licenses="MIT"
29
+ LABEL org.opencontainers.image.version="${MC_VERSION}"
30
+
31
  WORKDIR /app
32
  ENV NODE_ENV=production
33
  RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
 
37
  COPY --from=build /app/src/lib/schema.sql ./src/lib/schema.sql
38
  # Create data directory with correct ownership for SQLite
39
  RUN mkdir -p .data && chown nextjs:nodejs .data
40
+ RUN echo 'const http=require("http");const r=http.get("http://localhost:"+(process.env.PORT||3000)+"/api/status?action=health",s=>{process.exit(s.statusCode===200?0:1)});r.on("error",()=>process.exit(1));r.setTimeout(4000,()=>{r.destroy();process.exit(1)})' > /app/healthcheck.js
41
  USER nextjs
42
  ENV PORT=3000
43
  EXPOSE 3000
44
  ENV HOSTNAME=0.0.0.0
45
  HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
46
+ CMD ["node", "/app/healthcheck.js"]
47
  CMD ["node", "server.js"]
README.md CHANGED
@@ -24,20 +24,47 @@ Manage agent fleets, track tasks, monitor costs, and orchestrate workflows — a
24
 
25
  Running AI agents at scale means juggling sessions, tasks, costs, and reliability across multiple models and channels. Mission Control gives you:
26
 
27
- - **28 panels** — Tasks, agents, logs, tokens, memory, cron, alerts, webhooks, pipelines, and more
28
  - **Real-time everything** — WebSocket + SSE push updates, smart polling that pauses when you're away
29
  - **Zero external dependencies** — SQLite database, single `pnpm start` to run, no Redis/Postgres/Docker required
30
  - **Role-based access** — Viewer, operator, and admin roles with session + API key auth
31
- - **Quality gates** — Built-in review system that blocks task completion without sign-off
 
 
 
32
  - **Multi-gateway** — Connect to multiple agent gateways simultaneously (OpenClaw, and more coming soon)
33
 
34
  ## Quick Start
35
 
36
- > **Requires [pnpm](https://pnpm.io/installation)** — Mission Control uses pnpm for dependency management. Install it with `npm install -g pnpm` or `corepack enable`.
37
 
38
  ```bash
39
  git clone https://github.com/builderz-labs/mission-control.git
40
  cd mission-control
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  pnpm install
42
  cp .env.example .env # edit with your values
43
  pnpm dev # http://localhost:3000
@@ -46,6 +73,25 @@ pnpm dev # http://localhost:3000
46
  Initial login is seeded from `AUTH_USER` / `AUTH_PASS` on first run.
47
  If `AUTH_PASS` contains `#`, quote it (e.g. `AUTH_PASS="my#password"`) or use `AUTH_PASS_B64`.
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  ## Project Status
50
 
51
  ### What Works
@@ -65,7 +111,22 @@ If `AUTH_PASS` contains `#`, quote it (e.g. `AUTH_PASS="my#password"`) or use `A
65
  - Ed25519 device identity for secure gateway handshake
66
  - Agent SOUL system with workspace file sync and templates
67
  - Agent inter-agent messaging and comms
68
- - Update available banner with GitHub release check
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  ### Known Limitations
71
 
@@ -81,10 +142,10 @@ If `AUTH_PASS` contains `#`, quote it (e.g. `AUTH_PASS="my#password"`) or use `A
81
  ## Features
82
 
83
  ### Agent Management
84
- Monitor agent status, spawn new sessions, view heartbeats, and manage the full agent lifecycle from registration to retirement.
85
 
86
  ### Task Board
87
- Kanban board with six columns (inbox → backlogtodoin-progress → review → done), drag-and-drop, priority levels, assignments, and threaded comments.
88
 
89
  ### Real-time Monitoring
90
  Live activity feed, session inspector, and log viewer with filtering. WebSocket connection to OpenClaw gateway for instant event delivery.
@@ -93,7 +154,10 @@ Live activity feed, session inspector, and log viewer with filtering. WebSocket
93
  Token usage dashboard with per-model breakdowns, trend charts, and cost analysis powered by Recharts.
94
 
95
  ### Background Automation
96
- Scheduled tasks for database backups, stale record cleanup, and agent heartbeat monitoring. Configurable via UI or API.
 
 
 
97
 
98
  ### Direct CLI Integration
99
  Connect Claude Code, Codex, or any CLI tool directly to Mission Control without requiring a gateway. Register connections, send heartbeats with inline token reporting, and auto-register agents.
@@ -101,28 +165,52 @@ Connect Claude Code, Codex, or any CLI tool directly to Mission Control without
101
  ### Claude Code Session Tracking
102
  Automatically discovers and tracks local Claude Code sessions by scanning `~/.claude/projects/`. Extracts token usage, model info, message counts, cost estimates, and active status from JSONL transcripts. Scans every 60 seconds via the background scheduler.
103
 
 
 
 
104
  ### GitHub Issues Sync
105
  Inbound sync from GitHub repositories with label and assignee mapping. Synced issues appear on the task board alongside agent-created tasks.
106
 
 
 
 
 
 
 
107
  ### Agent SOUL System
108
  Define agent personality, capabilities, and behavioral guidelines via SOUL markdown files. Edit in the UI or directly in workspace `soul.md` files — changes sync bidirectionally between disk and database.
109
 
110
  ### Agent Messaging
111
- Inter-agent communication via the comms API. Agents can send messages to each other, enabling coordinated multi-agent workflows.
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  ### Integrations
114
  Outbound webhooks with delivery history, configurable alert rules with cooldowns, and multi-gateway connection management. Optional 1Password CLI integration for secret management.
115
 
116
  ### Workspace Management
117
- Workspaces (tenant instances) are created and managed through the **Super Admin** panel, accessible from the sidebar under **Admin > Super Admin**. From there, admins can:
118
  - **Create** new client instances (slug, display name, Linux user, gateway port, plan tier)
119
  - **Monitor** provisioning jobs and their step-by-step progress
120
  - **Decommission** tenants with optional cleanup of state directories and Linux users
121
 
122
- Each workspace gets its own isolated environment with a dedicated OpenClaw gateway, state directory, and workspace root. See the [Super Admin API](#api-overview) endpoints under `/api/super/*` for programmatic access.
123
 
124
  ### Update Checker
125
- Automatic GitHub release check notifies you when a new version is available, displayed as a banner in the dashboard.
 
 
 
126
 
127
  ## Architecture
128
 
@@ -133,22 +221,35 @@ mission-control/
133
  │ ├── app/
134
  │ │ ├── page.tsx # SPA shell — routes all panels
135
  │ │ ├── login/page.tsx # Login page
136
- │ │ └── api/ # 66 REST API routes
137
  │ ├── components/
138
  │ │ ├── layout/ # NavRail, HeaderBar, LiveFeed
139
  │ │ ├── dashboard/ # Overview dashboard
140
- │ │ ├── panels/ # 28 feature panels
141
  │ │ └── chat/ # Agent chat UI
142
  │ ├── lib/
143
  │ │ ├── auth.ts # Session + API key auth, RBAC
144
  │ │ ├── db.ts # SQLite (better-sqlite3, WAL mode)
145
  │ │ ├── claude-sessions.ts # Local Claude Code session scanner
146
- │ │ ├── migrations.ts # 21 schema migrations
 
 
 
147
  │ │ ├── scheduler.ts # Background task scheduler
148
  │ │ ├── webhooks.ts # Outbound webhook delivery
149
  │ │ ├── websocket.ts # Gateway WebSocket client
150
  │ │ ├── device-identity.ts # Ed25519 device identity for gateway auth
151
- │ │ ── agent-sync.ts # OpenClaw config → MC database sync
 
 
 
 
 
 
 
 
 
 
152
  │ └── store/index.ts # Zustand state management
153
  └── .data/ # Runtime data (SQLite DB, token logs)
154
  ```
@@ -166,7 +267,7 @@ mission-control/
166
  | Real-time | WebSocket + Server-Sent Events |
167
  | Auth | scrypt hashing, session tokens, RBAC |
168
  | Validation | Zod 4 |
169
- | Testing | Vitest + Playwright (148 E2E tests) |
170
 
171
  ## Authentication
172
 
@@ -211,7 +312,9 @@ All endpoints require authentication unless noted. Full reference below.
211
  | `POST` | `/api/agents` | operator | Register/update agent |
212
  | `GET` | `/api/agents/[id]` | viewer | Agent details |
213
  | `GET` | `/api/agents/[id]/attribution` | viewer | Self-scope attribution/audit/cost report (`?privileged=1` admin override) |
214
- | `POST` | `/api/agents/sync` | operator | Sync agents from openclaw.json |
 
 
215
  | `GET/PUT` | `/api/agents/[id]/soul` | operator | Agent SOUL content (reads from workspace, writes to both) |
216
  | `GET/POST` | `/api/agents/comms` | operator | Agent inter-agent communication |
217
  | `POST` | `/api/agents/message` | operator | Send message to agent |
@@ -235,6 +338,19 @@ All endpoints require authentication unless noted. Full reference below.
235
  - `hours`: integer window `1..720` (default `24`)
236
  - `section`: comma-separated subset of `identity,audit,mutations,cost` (default all)
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  <details>
239
  <summary><strong>Monitoring</strong></summary>
240
 
@@ -259,6 +375,7 @@ All endpoints require authentication unless noted. Full reference below.
259
  | `GET/PUT` | `/api/settings` | admin | App settings |
260
  | `GET/PUT` | `/api/gateway-config` | admin | OpenClaw gateway config |
261
  | `GET/POST` | `/api/cron` | admin | Cron management |
 
262
 
263
  </details>
264
 
@@ -297,7 +414,7 @@ All endpoints require authentication unless noted. Full reference below.
297
  </details>
298
 
299
  <details>
300
- <summary><strong>Super Admin (Workspace/Tenant Management)</strong></summary>
301
 
302
  | Method | Path | Role | Description |
303
  |--------|------|------|-------------|
@@ -310,6 +427,23 @@ All endpoints require authentication unless noted. Full reference below.
310
 
311
  </details>
312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  <details>
314
  <summary><strong>Direct CLI</strong></summary>
315
 
@@ -351,6 +485,8 @@ All endpoints require authentication unless noted. Full reference below.
351
  |--------|------|------|-------------|
352
  | `GET` | `/api/claude/sessions` | viewer | List discovered sessions (filter: `?active=1`, `?project=`) |
353
  | `POST` | `/api/claude/sessions` | operator | Trigger manual session scan |
 
 
354
 
355
  </details>
356
 
@@ -402,15 +538,11 @@ See [`.env.example`](.env.example) for the complete list. Key variables:
402
 
403
  ### Workspace Creation Flow
404
 
405
- To add a new workspace/client instance in the UI:
406
 
407
- 1. Open `Workspaces` from the left navigation.
408
- 2. Expand `Show Create Client Instance`.
409
- 3. Fill tenant/workspace fields (`slug`, `display_name`, optional ports/gateway owner).
410
- 4. Click `Create + Queue`.
411
- 5. Approve/run the generated provisioning job in the same panel.
412
-
413
- `Workspaces` and `Super Admin` currently point to the same provisioning control plane.
414
 
415
  ### Projects and Ticket Prefixes
416
 
@@ -529,8 +661,16 @@ See [open issues](https://github.com/builderz-labs/mission-control/issues) for p
529
 
530
  **Up next:**
531
 
 
 
 
 
 
 
 
 
 
532
  - [ ] Agent-agnostic gateway support — connect any orchestration framework (OpenClaw, ZeroClaw, OpenFang, NeoBot, IronClaw, etc.), not just OpenClaw
533
- - [ ] Workspace isolation for multi-team usage ([#75](https://github.com/builderz-labs/mission-control/issues/75))
534
  - [ ] **[Flight Deck](https://github.com/splitlabs/flight-deck)** — native desktop companion app (Tauri v2) with real PTY terminal grid, stall inbox with native OS notifications, and system tray HUD. Currently in private beta.
535
  - [ ] First-class per-agent cost breakdowns — dedicated panel with per-agent token usage and spend (currently derivable from per-session data)
536
  - [ ] OAuth approval UI improvements
@@ -544,6 +684,16 @@ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup inst
544
 
545
  To report a vulnerability, see [SECURITY.md](SECURITY.md).
546
 
 
 
 
 
 
 
 
 
 
 
547
  ## License
548
 
549
  [MIT](LICENSE) © 2026 [Builderz Labs](https://github.com/builderz-labs)
 
24
 
25
  Running AI agents at scale means juggling sessions, tasks, costs, and reliability across multiple models and channels. Mission Control gives you:
26
 
27
+ - **32 panels** — Tasks, agents, skills, logs, tokens, memory, security, cron, alerts, webhooks, pipelines, and more
28
  - **Real-time everything** — WebSocket + SSE push updates, smart polling that pauses when you're away
29
  - **Zero external dependencies** — SQLite database, single `pnpm start` to run, no Redis/Postgres/Docker required
30
  - **Role-based access** — Viewer, operator, and admin roles with session + API key auth
31
+ - **Quality gates** — Built-in Aegis review system that blocks task completion without sign-off
32
+ - **Recurring tasks** — Natural language scheduling ("every morning at 9am") with cron-based template spawning
33
+ - **Claude Code bridge** — Read-only integration surfaces Claude Code team tasks and configs on the dashboard
34
+ - **Skills Hub** — Browse, install, and security-scan agent skills from ClawdHub and skills.sh registries
35
  - **Multi-gateway** — Connect to multiple agent gateways simultaneously (OpenClaw, and more coming soon)
36
 
37
  ## Quick Start
38
 
39
+ ### One-Command Install (Docker)
40
 
41
  ```bash
42
  git clone https://github.com/builderz-labs/mission-control.git
43
  cd mission-control
44
+ bash install.sh --docker
45
+ ```
46
+
47
+ The installer auto-generates secure credentials, starts the container, and runs an OpenClaw fleet health check. Open `http://localhost:3000` and log in with the printed credentials.
48
+
49
+ ### One-Command Install (Local)
50
+
51
+ ```bash
52
+ git clone https://github.com/builderz-labs/mission-control.git
53
+ cd mission-control
54
+ bash install.sh --local
55
+ ```
56
+
57
+ Requires Node.js 22.x (LTS) and pnpm (auto-installed via corepack if missing).
58
+
59
+ ### Manual Setup
60
+
61
+ > **Requires [pnpm](https://pnpm.io/installation)** and **Node.js 22.x (LTS)**.
62
+ > Mission Control is validated against Node 22 across local dev, CI, Docker, and standalone deploys. Use `nvm use 22` (or your version manager equivalent) before installing or starting the app.
63
+
64
+ ```bash
65
+ git clone https://github.com/builderz-labs/mission-control.git
66
+ cd mission-control
67
+ nvm use 22
68
  pnpm install
69
  cp .env.example .env # edit with your values
70
  pnpm dev # http://localhost:3000
 
73
  Initial login is seeded from `AUTH_USER` / `AUTH_PASS` on first run.
74
  If `AUTH_PASS` contains `#`, quote it (e.g. `AUTH_PASS="my#password"`) or use `AUTH_PASS_B64`.
75
 
76
+ ### Docker Hardening (Production)
77
+
78
+ For production deployments, use the hardened compose overlay:
79
+
80
+ ```bash
81
+ docker compose -f docker-compose.yml -f docker-compose.hardened.yml up -d
82
+ ```
83
+
84
+ This adds read-only filesystem, capability dropping, log rotation, HSTS, and network isolation. See [Security Hardening](docs/SECURITY-HARDENING.md) for the full checklist.
85
+
86
+ ### Station Doctor
87
+
88
+ Run diagnostics on your installation:
89
+
90
+ ```bash
91
+ bash scripts/station-doctor.sh
92
+ bash scripts/security-audit.sh
93
+ ```
94
+
95
  ## Project Status
96
 
97
  ### What Works
 
111
  - Ed25519 device identity for secure gateway handshake
112
  - Agent SOUL system with workspace file sync and templates
113
  - Agent inter-agent messaging and comms
114
+ - Skills Hub with ClawdHub and skills.sh registry integration (search, install, security scan)
115
+ - Bidirectional skill sync — disk ↔ DB with SHA-256 change detection
116
+ - Local agent discovery from `~/.agents/`, `~/.codex/agents/`, `~/.claude/agents/`
117
+ - Natural language recurring tasks — schedule parser converts "every 2 hours" to cron, spawns dated child tasks
118
+ - Claude Code task bridge — read-only scanner surfaces team tasks and configs from `~/.claude/tasks/` and `~/.claude/teams/`
119
+ - Skill security scanner (prompt injection, credential leaks, data exfiltration, obfuscated content)
120
+ - Update available banner with GitHub release check and one-click self-update
121
+ - Framework adapter layer for multi-agent registration (OpenClaw, CrewAI, LangGraph, AutoGen, Claude SDK, generic)
122
+ - Multi-project task organization with per-project ticket prefixes
123
+ - Per-agent rate limiting with `x-agent-name` identity-based quotas
124
+ - Agent self-registration endpoint for autonomous agent onboarding
125
+ - Security audit panel with posture scoring, secret detection, trust scoring, and MCP call auditing
126
+ - Four-layer agent eval framework (output, trace, component, drift detection)
127
+ - Agent optimization endpoint with token efficiency, tool patterns, and fleet benchmarks
128
+ - Hook profiles (minimal/standard/strict) for tunable security strictness
129
+ - Guided onboarding wizard with credential setup, agent discovery, and security scan
130
 
131
  ### Known Limitations
132
 
 
142
  ## Features
143
 
144
  ### Agent Management
145
+ Monitor agent status, configure models, view heartbeats, and manage the full agent lifecycle from registration to retirement. Agent detail modal with compact overview, inline model selector, and editable sub-agent configuration.
146
 
147
  ### Task Board
148
+ Kanban board with six columns (inbox → assignedin progress reviewquality review → done), drag-and-drop, priority levels, assignments, threaded comments, and inline sub-agent spawning.
149
 
150
  ### Real-time Monitoring
151
  Live activity feed, session inspector, and log viewer with filtering. WebSocket connection to OpenClaw gateway for instant event delivery.
 
154
  Token usage dashboard with per-model breakdowns, trend charts, and cost analysis powered by Recharts.
155
 
156
  ### Background Automation
157
+ Scheduled tasks for database backups, stale record cleanup, agent heartbeat monitoring, and recurring task spawning. Configurable via UI or API.
158
+
159
+ ### Natural Language Recurring Tasks
160
+ Create recurring tasks with natural language like "every morning at 9am" or "every 2 hours". The built-in schedule parser (zero dependencies) converts expressions to cron and stores them in task metadata. A template-clone pattern keeps the original task as a template and spawns dated child tasks (e.g., "Daily Report - Mar 07") on schedule. Each spawned task gets its own Aegis quality gate.
161
 
162
  ### Direct CLI Integration
163
  Connect Claude Code, Codex, or any CLI tool directly to Mission Control without requiring a gateway. Register connections, send heartbeats with inline token reporting, and auto-register agents.
 
165
  ### Claude Code Session Tracking
166
  Automatically discovers and tracks local Claude Code sessions by scanning `~/.claude/projects/`. Extracts token usage, model info, message counts, cost estimates, and active status from JSONL transcripts. Scans every 60 seconds via the background scheduler.
167
 
168
+ ### Claude Code Task Bridge
169
+ Read-only integration that surfaces Claude Code team tasks and team configs on the Mission Control dashboard. Scans `~/.claude/tasks/<team>/<N>.json` for structured task data (subject, status, owner, blockers) and `~/.claude/teams/<name>/config.json` for team metadata (members, lead agent, model assignments). Visible in both the Task Board (collapsible section) and Cron Management (teams overview) panels.
170
+
171
  ### GitHub Issues Sync
172
  Inbound sync from GitHub repositories with label and assignee mapping. Synced issues appear on the task board alongside agent-created tasks.
173
 
174
+ ### Skills Hub
175
+ Browse, install, and manage agent skills from local directories and external registries (ClawdHub, skills.sh). Bidirectional sync detects manual additions on disk and pushes UI edits back to `SKILL.md` files. Built-in security scanner checks for prompt injection, credential leaks, data exfiltration, obfuscated content, and dangerous shell commands before installation. Supports 5 skill roots: `~/.agents/skills`, `~/.codex/skills`, project-local `.agents/skills` and `.codex/skills`, and `~/.openclaw/skills` for gateway mode.
176
+
177
+ ### Local Agent Discovery
178
+ Automatically discovers agent definitions from `~/.agents/`, `~/.codex/agents/`, and `~/.claude/agents/` directories. Detection looks for marker files (AGENT.md, soul.md, identity.md, config.json). Discovered agents sync bidirectionally — edit in the UI and changes write back to disk.
179
+
180
  ### Agent SOUL System
181
  Define agent personality, capabilities, and behavioral guidelines via SOUL markdown files. Edit in the UI or directly in workspace `soul.md` files — changes sync bidirectionally between disk and database.
182
 
183
  ### Agent Messaging
184
+ Session-threaded inter-agent communication via the comms API (`a2a:*`, `coord:*`, `session:*`) with coordinator inbox support and runtime tool-call visibility in the `agent-comms` feed.
185
+
186
+ ### Onboarding Wizard
187
+ Guided first-run setup wizard that walks new users through five steps: Welcome (system capabilities detection), Credentials (verify AUTH_PASS and API_KEY strength), Agent Setup (gateway connection or local Claude Code discovery), Security Scan (automated configuration audit with pass/fail checks), and Get Started (quick links to key panels). Automatically appears on first login and can be re-launched from Settings. Progress is persisted per-user so you can resume where you left off.
188
+
189
+ ### Security Audit & Agent Trust
190
+ Dedicated security audit panel with real-time posture scoring (0-100), secret detection across agent messages, MCP tool call auditing, injection attempt tracking, and per-agent trust scores. Hook profiles (minimal/standard/strict) let operators tune security strictness per deployment. Auth failures, rate limit hits, and injection attempts are logged automatically as security events.
191
+
192
+ ### Agent Eval Framework
193
+ Four-layer evaluation stack for agent quality: output evals (task completion scoring against golden datasets), trace evals (convergence scoring — >3.0 indicates looping), component evals (tool reliability with p50/p95/p99 latency from MCP call logs), and drift detection (10% threshold vs 4-week rolling baseline). Manage golden datasets and trigger eval runs via API or UI.
194
+
195
+ ### Agent Optimization
196
+ API endpoint agents can call for self-improvement recommendations. Analyzes token efficiency (tokens/task vs fleet average), tool usage patterns (success/failure rates, redundant calls), and generates prioritized recommendations. Fleet benchmarks provide percentile rankings across all agents.
197
 
198
  ### Integrations
199
  Outbound webhooks with delivery history, configurable alert rules with cooldowns, and multi-gateway connection management. Optional 1Password CLI integration for secret management.
200
 
201
  ### Workspace Management
202
+ Workspaces (tenant instances) are managed via the `/api/super/*` API endpoints. Admins can:
203
  - **Create** new client instances (slug, display name, Linux user, gateway port, plan tier)
204
  - **Monitor** provisioning jobs and their step-by-step progress
205
  - **Decommission** tenants with optional cleanup of state directories and Linux users
206
 
207
+ Each workspace gets its own isolated environment with a dedicated OpenClaw gateway, state directory, and workspace root.
208
 
209
  ### Update Checker
210
+ Automatic GitHub release check notifies you when a new version is available, displayed as a banner in the dashboard. Admins can trigger a one-click update directly from the banner — the server runs `git pull`, `pnpm install`, and `pnpm build`, then prompts for a restart. Dirty working trees are rejected, and all updates are logged to the audit trail.
211
+
212
+ ### Framework Adapters
213
+ Built-in adapter layer for multi-agent registration across frameworks. Supported adapters: OpenClaw, CrewAI, LangGraph, AutoGen, Claude SDK, and a generic fallback. Each adapter normalizes agent registration, heartbeats, and task reporting to a common interface.
214
 
215
  ## Architecture
216
 
 
221
  │ ├── app/
222
  │ │ ├── page.tsx # SPA shell — routes all panels
223
  │ │ ├── login/page.tsx # Login page
224
+ │ │ └── api/ # 101 REST API routes
225
  │ ├── components/
226
  │ │ ├── layout/ # NavRail, HeaderBar, LiveFeed
227
  │ │ ├── dashboard/ # Overview dashboard
228
+ │ │ ├── panels/ # 32 feature panels
229
  │ │ └── chat/ # Agent chat UI
230
  │ ├── lib/
231
  │ │ ├── auth.ts # Session + API key auth, RBAC
232
  │ │ ├── db.ts # SQLite (better-sqlite3, WAL mode)
233
  │ │ ├── claude-sessions.ts # Local Claude Code session scanner
234
+ │ │ ├── claude-tasks.ts # Claude Code team task/config scanner
235
+ │ │ ├── schedule-parser.ts # Natural language → cron expression parser
236
+ │ │ ├── recurring-tasks.ts # Recurring task template spawner
237
+ │ │ ├── migrations.ts # 39 schema migrations
238
  │ │ ├── scheduler.ts # Background task scheduler
239
  │ │ ├── webhooks.ts # Outbound webhook delivery
240
  │ │ ├── websocket.ts # Gateway WebSocket client
241
  │ │ ├── device-identity.ts # Ed25519 device identity for gateway auth
242
+ │ │ ── agent-sync.ts # OpenClaw config → MC database sync
243
+ │ │ ├── skill-sync.ts # Bidirectional disk ↔ DB skill sync
244
+ │ │ ├── skill-registry.ts # ClawdHub + skills.sh registry client & security scanner
245
+ │ │ ├── local-agent-sync.ts # Local agent discovery from ~/.agents, ~/.codex, ~/.claude
246
+ │ │ ├── secret-scanner.ts # Regex-based secret detection (AWS, GitHub, Stripe, JWT, PEM, DB URIs)
247
+ │ │ ├── security-events.ts # Security event logger + agent trust scoring
248
+ │ │ ├── mcp-audit.ts # MCP tool call auditing
249
+ │ │ ├── agent-evals.ts # Four-layer agent eval framework
250
+ │ │ ├── agent-optimizer.ts # Agent optimization engine
251
+ │ │ ├── hook-profiles.ts # Security strictness profiles (minimal/standard/strict)
252
+ │ │ └── adapters/ # Framework adapters (openclaw, crewai, langgraph, autogen, claude-sdk, generic)
253
  │ └── store/index.ts # Zustand state management
254
  └── .data/ # Runtime data (SQLite DB, token logs)
255
  ```
 
267
  | Real-time | WebSocket + Server-Sent Events |
268
  | Auth | scrypt hashing, session tokens, RBAC |
269
  | Validation | Zod 4 |
270
+ | Testing | Vitest (282 unit) + Playwright (295 E2E) |
271
 
272
  ## Authentication
273
 
 
312
  | `POST` | `/api/agents` | operator | Register/update agent |
313
  | `GET` | `/api/agents/[id]` | viewer | Agent details |
314
  | `GET` | `/api/agents/[id]/attribution` | viewer | Self-scope attribution/audit/cost report (`?privileged=1` admin override) |
315
+ | `POST` | `/api/agents/sync` | operator | Sync agents from openclaw.json or local disk (`?source=local`) |
316
+ | `POST` | `/api/agents/register` | viewer | Agent self-registration (idempotent, rate-limited) |
317
+ | `GET/POST` | `/api/adapters` | viewer/operator | List adapters / Framework-agnostic agent action dispatch |
318
  | `GET/PUT` | `/api/agents/[id]/soul` | operator | Agent SOUL content (reads from workspace, writes to both) |
319
  | `GET/POST` | `/api/agents/comms` | operator | Agent inter-agent communication |
320
  | `POST` | `/api/agents/message` | operator | Send message to agent |
 
338
  - `hours`: integer window `1..720` (default `24`)
339
  - `section`: comma-separated subset of `identity,audit,mutations,cost` (default all)
340
 
341
+ <details>
342
+ <summary><strong>Security & Evals</strong></summary>
343
+
344
+ | Method | Path | Role | Description |
345
+ |--------|------|------|-------------|
346
+ | `GET` | `/api/security-audit` | admin | Security posture, events, trust scores, MCP audit (`?timeframe=day`) |
347
+ | `GET` | `/api/security-scan` | admin | Static security configuration scan |
348
+ | `GET` | `/api/agents/optimize` | operator | Agent optimization recommendations (`?agent=&hours=24`) |
349
+ | `GET` | `/api/agents/evals` | operator | Agent eval results (`?agent=`, `?action=history&weeks=4`) |
350
+ | `POST` | `/api/agents/evals` | operator | Trigger eval run (`action: 'run'`) or manage golden datasets (`action: 'golden-set'`) |
351
+
352
+ </details>
353
+
354
  <details>
355
  <summary><strong>Monitoring</strong></summary>
356
 
 
375
  | `GET/PUT` | `/api/settings` | admin | App settings |
376
  | `GET/PUT` | `/api/gateway-config` | admin | OpenClaw gateway config |
377
  | `GET/POST` | `/api/cron` | admin | Cron management |
378
+ | `GET/POST` | `/api/onboarding` | viewer | Onboarding wizard state and step progression |
379
 
380
  </details>
381
 
 
414
  </details>
415
 
416
  <details>
417
+ <summary><strong>Workspace/Tenant Management</strong></summary>
418
 
419
  | Method | Path | Role | Description |
420
  |--------|------|------|-------------|
 
427
 
428
  </details>
429
 
430
+ <details>
431
+ <summary><strong>Skills</strong></summary>
432
+
433
+ | Method | Path | Role | Description |
434
+ |--------|------|------|-------------|
435
+ | `GET` | `/api/skills` | viewer | List skills (DB-backed with filesystem fallback) |
436
+ | `GET` | `/api/skills?mode=content&source=…&name=…` | viewer | Read SKILL.md content with inline security report |
437
+ | `GET` | `/api/skills?mode=check&source=…&name=…` | viewer | On-demand security scan |
438
+ | `POST` | `/api/skills` | operator | Create skill |
439
+ | `PUT` | `/api/skills` | operator | Update skill content |
440
+ | `DELETE` | `/api/skills` | operator | Delete skill |
441
+ | `GET` | `/api/skills/registry?source=…&q=…` | viewer | Search external registry (ClawdHub, skills.sh) |
442
+ | `POST` | `/api/skills/registry` | admin | Install skill from registry |
443
+ | `PUT` | `/api/skills/registry` | viewer | Security-check content without installing |
444
+
445
+ </details>
446
+
447
  <details>
448
  <summary><strong>Direct CLI</strong></summary>
449
 
 
485
  |--------|------|------|-------------|
486
  | `GET` | `/api/claude/sessions` | viewer | List discovered sessions (filter: `?active=1`, `?project=`) |
487
  | `POST` | `/api/claude/sessions` | operator | Trigger manual session scan |
488
+ | `GET` | `/api/claude-tasks` | viewer | List Claude Code team tasks and configs (`?force=true` to bypass cache) |
489
+ | `GET` | `/api/schedule-parse` | viewer | Parse natural language schedule (`?input=every+2+hours`) |
490
 
491
  </details>
492
 
 
538
 
539
  ### Workspace Creation Flow
540
 
541
+ To add a new workspace/client instance, use the `/api/super/tenants` endpoint or the Workspaces panel (if enabled):
542
 
543
+ 1. Provide tenant/workspace fields (`slug`, `display_name`, optional ports/gateway owner).
544
+ 2. The system queues a bootstrap provisioning job.
545
+ 3. Approve/run the provisioning job via `/api/super/provision-jobs/[id]/action`.
 
 
 
 
546
 
547
  ### Projects and Ticket Prefixes
548
 
 
661
 
662
  **Up next:**
663
 
664
+ - [x] Workspace isolation for multi-team usage ([#75](https://github.com/builderz-labs/mission-control/issues/75))
665
+ - [x] Framework adapter layer — multi-agent registration across OpenClaw, CrewAI, LangGraph, AutoGen, Claude SDK, and generic
666
+ - [x] Self-update mechanism — admin-only one-click update with audit logging
667
+ - [x] Multi-project task organization with per-project ticket prefixes
668
+ - [x] Skills Hub — browse, install, and security-scan skills from ClawdHub and skills.sh registries
669
+ - [x] Bidirectional skill sync — disk ↔ DB with SHA-256 change detection (60s scheduler)
670
+ - [x] Local agent discovery — auto-detect agents from `~/.agents/`, `~/.codex/agents/`, `~/.claude/agents/`
671
+ - [x] Natural language recurring tasks with cron-based template spawning
672
+ - [x] Claude Code task bridge — read-only team task and config integration
673
  - [ ] Agent-agnostic gateway support — connect any orchestration framework (OpenClaw, ZeroClaw, OpenFang, NeoBot, IronClaw, etc.), not just OpenClaw
 
674
  - [ ] **[Flight Deck](https://github.com/splitlabs/flight-deck)** — native desktop companion app (Tauri v2) with real PTY terminal grid, stall inbox with native OS notifications, and system tray HUD. Currently in private beta.
675
  - [ ] First-class per-agent cost breakdowns — dedicated panel with per-agent token usage and spend (currently derivable from per-session data)
676
  - [ ] OAuth approval UI improvements
 
684
 
685
  To report a vulnerability, see [SECURITY.md](SECURITY.md).
686
 
687
+ ## ❤️ Support the Project
688
+
689
+ If you find this project useful, consider supporting my open-source work.
690
+
691
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-orange?logo=buymeacoffee)](https://buymeacoffee.com/nyk_builderz)
692
+
693
+ **Solana donations**
694
+
695
+ `BYLu8XD8hGDUtdRBWpGWu5HKoiPrWqCxYFSh4oxXuvPg`
696
+
697
  ## License
698
 
699
  [MIT](LICENSE) © 2026 [Builderz Labs](https://github.com/builderz-labs)
SECURITY.md CHANGED
@@ -27,5 +27,42 @@ Mission Control handles authentication credentials and API keys. When deploying:
27
  - Always set strong values for `AUTH_PASS` and `API_KEY`.
28
  - Use `MC_ALLOWED_HOSTS` to restrict network access in production.
29
  - Keep `.env` files out of version control (already in `.gitignore`).
30
- - Enable `MC_COOKIE_SECURE=true` when serving over HTTPS.
31
  - Review the [Environment Variables](README.md#environment-variables) section for all security-relevant configuration.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  - Always set strong values for `AUTH_PASS` and `API_KEY`.
28
  - Use `MC_ALLOWED_HOSTS` to restrict network access in production.
29
  - Keep `.env` files out of version control (already in `.gitignore`).
30
+ - Enable `MC_COOKIE_SECURE=1` when serving over HTTPS.
31
  - Review the [Environment Variables](README.md#environment-variables) section for all security-relevant configuration.
32
+
33
+ ## Hardening Checklist
34
+
35
+ Run `bash scripts/security-audit.sh` to check your deployment automatically.
36
+
37
+ ### Credentials
38
+ - [ ] `AUTH_PASS` is a strong, unique password (12+ characters)
39
+ - [ ] `API_KEY` is a random hex string (not the default)
40
+ - [ ] `AUTH_SECRET` is a random string
41
+ - [ ] `.env` file permissions are `600` (owner read/write only)
42
+
43
+ ### Network
44
+ - [ ] `MC_ALLOWED_HOSTS` is configured (not `MC_ALLOW_ANY_HOST=1`)
45
+ - [ ] Dashboard is behind a reverse proxy with TLS (Caddy, nginx, Tailscale)
46
+ - [ ] `MC_ENABLE_HSTS=1` is set for HTTPS deployments
47
+ - [ ] `MC_COOKIE_SECURE=1` is set for HTTPS deployments
48
+ - [ ] `MC_COOKIE_SAMESITE=strict`
49
+
50
+ ### Docker (if applicable)
51
+ - [ ] Use the hardened compose overlay: `docker compose -f docker-compose.yml -f docker-compose.hardened.yml up`
52
+ - [ ] Container runs as non-root user (default: `nextjs`, UID 1001)
53
+ - [ ] Read-only filesystem with tmpfs for temp dirs
54
+ - [ ] All Linux capabilities dropped except `NET_BIND_SERVICE`
55
+ - [ ] `no-new-privileges` security option enabled
56
+ - [ ] Log rotation configured (max-size, max-file)
57
+
58
+ ### OpenClaw Gateway
59
+ - [ ] Gateway bound to localhost (`OPENCLAW_GATEWAY_HOST=127.0.0.1`)
60
+ - [ ] Gateway token configured (`OPENCLAW_GATEWAY_TOKEN`)
61
+ - [ ] Gateway token NOT exposed via `NEXT_PUBLIC_*` variables
62
+
63
+ ### Monitoring
64
+ - [ ] Rate limiting is active (`MC_DISABLE_RATE_LIMIT` is NOT set)
65
+ - [ ] Audit logging is enabled with appropriate retention
66
+ - [ ] Regular database backups configured
67
+
68
+ See [docs/SECURITY-HARDENING.md](docs/SECURITY-HARDENING.md) for the full hardening guide.
SKILL.md ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: mission-control
3
+ description: "Interact with Mission Control — AI agent orchestration dashboard. Use when registering agents, managing tasks, syncing skills, or querying agent/task status via MC APIs."
4
+ ---
5
+
6
+ # Mission Control Agent Skill
7
+
8
+ Mission Control (MC) is an AI agent orchestration dashboard with real-time SSE/WebSocket, a skill registry, framework adapters, and RBAC. This skill teaches agents how to interact with MC APIs programmatically.
9
+
10
+ ## Quick Start
11
+
12
+ **Base URL:** `http://localhost:3000` (default Next.js dev) or your deployed host.
13
+
14
+ **Auth header:** `x-api-key: <your-api-key>`
15
+
16
+ **Register + heartbeat in two calls:**
17
+
18
+ ```bash
19
+ # 1. Register
20
+ curl -X POST http://localhost:3000/api/adapters \
21
+ -H "Content-Type: application/json" \
22
+ -H "x-api-key: $MC_API_KEY" \
23
+ -d '{
24
+ "framework": "generic",
25
+ "action": "register",
26
+ "payload": { "agentId": "my-agent-01", "name": "My Agent" }
27
+ }'
28
+
29
+ # 2. Heartbeat (repeat every 5 minutes)
30
+ curl -X POST http://localhost:3000/api/adapters \
31
+ -H "Content-Type: application/json" \
32
+ -H "x-api-key: $MC_API_KEY" \
33
+ -d '{
34
+ "framework": "generic",
35
+ "action": "heartbeat",
36
+ "payload": { "agentId": "my-agent-01", "status": "online" }
37
+ }'
38
+ ```
39
+
40
+ ## Authentication
41
+
42
+ MC supports two auth methods:
43
+
44
+ | Method | Header | Use Case |
45
+ |--------|--------|----------|
46
+ | API Key | `x-api-key: <key>` or `Authorization: Bearer <key>` | Agents, scripts, CI/CD |
47
+ | Session cookie | `Cookie: mc-session=<token>` | Browser UI |
48
+
49
+ **Roles (hierarchical):** `viewer` < `operator` < `admin`
50
+
51
+ - **viewer** — Read-only access (GET endpoints)
52
+ - **operator** — Create/update agents, tasks, skills, use adapters
53
+ - **admin** — Full access including user management
54
+
55
+ API key auth grants `admin` role by default. The key is set via `API_KEY` env var or the `security.api_key` DB setting.
56
+
57
+ Agents can identify themselves with the optional `X-Agent-Name` header for attribution in audit logs.
58
+
59
+ ## Agent Lifecycle
60
+
61
+ ```
62
+ register → heartbeat (5m interval) → fetch assignments → report task status → disconnect
63
+ ```
64
+
65
+ All lifecycle actions go through the adapter protocol (`POST /api/adapters`).
66
+
67
+ ### 1. Register
68
+
69
+ ```json
70
+ {
71
+ "framework": "generic",
72
+ "action": "register",
73
+ "payload": {
74
+ "agentId": "my-agent-01",
75
+ "name": "My Agent",
76
+ "metadata": { "version": "1.0", "capabilities": ["code", "review"] }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### 2. Heartbeat
82
+
83
+ Send every ~5 minutes to stay marked as online.
84
+
85
+ ```json
86
+ {
87
+ "framework": "generic",
88
+ "action": "heartbeat",
89
+ "payload": {
90
+ "agentId": "my-agent-01",
91
+ "status": "online",
92
+ "metrics": { "tasks_completed": 5, "uptime_seconds": 3600 }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### 3. Fetch Assignments
98
+
99
+ Returns up to 5 pending tasks sorted by priority (critical → low), then due date.
100
+
101
+ ```json
102
+ {
103
+ "framework": "generic",
104
+ "action": "assignments",
105
+ "payload": { "agentId": "my-agent-01" }
106
+ }
107
+ ```
108
+
109
+ **Response:**
110
+
111
+ ```json
112
+ {
113
+ "assignments": [
114
+ { "taskId": "42", "description": "Fix login bug\nUsers cannot log in with SSO", "priority": 1 }
115
+ ],
116
+ "framework": "generic"
117
+ }
118
+ ```
119
+
120
+ ### 4. Report Task Progress
121
+
122
+ ```json
123
+ {
124
+ "framework": "generic",
125
+ "action": "report",
126
+ "payload": {
127
+ "taskId": "42",
128
+ "agentId": "my-agent-01",
129
+ "progress": 75,
130
+ "status": "in_progress",
131
+ "output": "Fixed SSO handler, running tests..."
132
+ }
133
+ }
134
+ ```
135
+
136
+ `status` values: `in_progress`, `done`, `failed`, `blocked`
137
+
138
+ ### 5. Disconnect
139
+
140
+ ```json
141
+ {
142
+ "framework": "generic",
143
+ "action": "disconnect",
144
+ "payload": { "agentId": "my-agent-01" }
145
+ }
146
+ ```
147
+
148
+ ## Core API Reference
149
+
150
+ ### Agents — `/api/agents`
151
+
152
+ | Method | Min Role | Description |
153
+ |--------|----------|-------------|
154
+ | GET | viewer | List agents. Query: `?status=online&role=dev&limit=50&offset=0` |
155
+ | POST | operator | Create agent. Body: `{ name, role, status?, config?, template?, session_key?, soul_content? }` |
156
+ | PUT | operator | Update agent. Body: `{ name, status?, role?, config?, session_key?, soul_content?, last_activity? }` |
157
+
158
+ **GET response shape:**
159
+
160
+ ```json
161
+ {
162
+ "agents": [{
163
+ "id": 1, "name": "scout", "role": "researcher", "status": "online",
164
+ "config": {}, "taskStats": { "total": 10, "assigned": 2, "in_progress": 1, "completed": 7 }
165
+ }],
166
+ "total": 1, "page": 1, "limit": 50
167
+ }
168
+ ```
169
+
170
+ ### Tasks — `/api/tasks`
171
+
172
+ | Method | Min Role | Description |
173
+ |--------|----------|-------------|
174
+ | GET | viewer | List tasks. Query: `?status=in_progress&assigned_to=scout&priority=high&project_id=1&limit=50&offset=0` |
175
+ | POST | operator | Create task. Body: `{ title, description?, status?, priority?, assigned_to?, project_id?, tags?, metadata?, due_date?, estimated_hours? }` |
176
+ | PUT | operator | Bulk status update. Body: `{ tasks: [{ id, status }] }` |
177
+
178
+ **Priority values:** `critical`, `high`, `medium`, `low`
179
+
180
+ **Status values:** `inbox`, `assigned`, `in_progress`, `review`, `done`, `failed`, `blocked`, `cancelled`
181
+
182
+ Note: Moving a task to `done` via PUT requires an Aegis quality review approval.
183
+
184
+ **POST response:**
185
+
186
+ ```json
187
+ {
188
+ "task": {
189
+ "id": 42, "title": "Fix login bug", "status": "assigned",
190
+ "priority": "high", "assigned_to": "scout", "ticket_ref": "GEN-001",
191
+ "tags": ["bug"], "metadata": {}
192
+ }
193
+ }
194
+ ```
195
+
196
+ ### Skills — `/api/skills`
197
+
198
+ | Method | Min Role | Description |
199
+ |--------|----------|-------------|
200
+ | GET | viewer | List all skills across roots |
201
+ | GET `?mode=content&source=...&name=...` | viewer | Read a skill's SKILL.md content |
202
+ | GET `?mode=check&source=...&name=...` | viewer | Run security check on a skill |
203
+ | POST | operator | Create/upsert skill. Body: `{ source, name, content }` |
204
+ | PUT | operator | Update skill content. Body: `{ source, name, content }` |
205
+ | DELETE `?source=...&name=...` | operator | Delete a skill |
206
+
207
+ **Skill sources:** `user-agents`, `user-codex`, `project-agents`, `project-codex`, `openclaw`
208
+
209
+ ### Status — `/api/status`
210
+
211
+ | Action | Min Role | Description |
212
+ |--------|----------|-------------|
213
+ | GET `?action=overview` | viewer | System status (uptime, memory, disk, sessions) |
214
+ | GET `?action=dashboard` | viewer | Aggregated dashboard data with DB stats |
215
+ | GET `?action=gateway` | viewer | Gateway process status and port check |
216
+ | GET `?action=models` | viewer | Available AI models (catalog + local Ollama) |
217
+ | GET `?action=health` | viewer | Health checks (gateway, disk, memory) |
218
+ | GET `?action=capabilities` | viewer | Feature flags: gateway reachable, Claude home, subscriptions |
219
+
220
+ ### Adapters — `/api/adapters`
221
+
222
+ | Method | Min Role | Description |
223
+ |--------|----------|-------------|
224
+ | GET | viewer | List available framework adapter names |
225
+ | POST | operator | Execute adapter action (see Agent Lifecycle above) |
226
+
227
+ ## Framework Adapter Protocol
228
+
229
+ All agent lifecycle operations use a single endpoint:
230
+
231
+ ```
232
+ POST /api/adapters
233
+ Content-Type: application/json
234
+ x-api-key: <key>
235
+
236
+ {
237
+ "framework": "<adapter-name>",
238
+ "action": "<action>",
239
+ "payload": { ... }
240
+ }
241
+ ```
242
+
243
+ **Available frameworks:** `generic`, `openclaw`, `crewai`, `langgraph`, `autogen`, `claude-sdk`
244
+
245
+ **Available actions:** `register`, `heartbeat`, `report`, `assignments`, `disconnect`
246
+
247
+ All adapters implement the same `FrameworkAdapter` interface — choose the one matching your agent framework, or use `generic` as a universal fallback.
248
+
249
+ **Payload shapes by action:**
250
+
251
+ | Action | Required Fields | Optional Fields |
252
+ |--------|----------------|-----------------|
253
+ | `register` | `agentId`, `name` | `metadata` |
254
+ | `heartbeat` | `agentId` | `status`, `metrics` |
255
+ | `report` | `taskId`, `agentId` | `progress`, `status`, `output` |
256
+ | `assignments` | `agentId` | — |
257
+ | `disconnect` | `agentId` | — |
258
+
259
+ ## Environment Variables
260
+
261
+ | Variable | Default | Description |
262
+ |----------|---------|-------------|
263
+ | `API_KEY` | — | API key for agent/script authentication |
264
+ | `OPENCLAW_GATEWAY_HOST` | `127.0.0.1` | Gateway host address |
265
+ | `OPENCLAW_GATEWAY_PORT` | `18789` | Gateway port |
266
+ | `MISSION_CONTROL_DB_PATH` | `.data/mission-control.db` | SQLite database path |
267
+ | `OPENCLAW_STATE_DIR` | `~/.openclaw` | OpenClaw state directory |
268
+ | `OPENCLAW_CONFIG_PATH` | `<state-dir>/openclaw.json` | Gateway config file path |
269
+ | `MC_CLAUDE_HOME` | `~/.claude` | Claude home directory |
270
+
271
+ ## Real-Time Events
272
+
273
+ MC broadcasts events via SSE (`/api/events`) and WebSocket. Key event types:
274
+
275
+ - `agent.created`, `agent.updated`, `agent.status_changed`
276
+ - `task.created`, `task.updated`, `task.status_changed`
277
+
278
+ Subscribe to SSE for live dashboard updates when building integrations.
docker-compose.hardened.yml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Production hardening overlay
2
+ # Usage: docker compose -f docker-compose.yml -f docker-compose.hardened.yml up -d
3
+ services:
4
+ mission-control:
5
+ logging:
6
+ driver: json-file
7
+ options:
8
+ max-size: "10m"
9
+ max-file: "3"
10
+ environment:
11
+ - MC_ALLOWED_HOSTS=localhost,127.0.0.1
12
+ - MC_COOKIE_SECURE=1
13
+ - MC_COOKIE_SAMESITE=strict
14
+ - MC_ENABLE_HSTS=1
15
+ networks:
16
+ mc-internal:
17
+
18
+ networks:
19
+ mc-internal:
20
+ driver: bridge
21
+ internal: true
docker-compose.yml CHANGED
@@ -11,7 +11,29 @@ services:
11
  required: false
12
  volumes:
13
  - mc-data:/app/.data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  restart: unless-stopped
15
 
16
  volumes:
17
  mc-data:
 
 
 
 
 
11
  required: false
12
  volumes:
13
  - mc-data:/app/.data
14
+ read_only: true
15
+ tmpfs:
16
+ - /tmp
17
+ - /app/.next/cache
18
+ cap_drop:
19
+ - ALL
20
+ cap_add:
21
+ - NET_BIND_SERVICE
22
+ security_opt:
23
+ - no-new-privileges:true
24
+ pids_limit: 256
25
+ deploy:
26
+ resources:
27
+ limits:
28
+ memory: 512M
29
+ cpus: '1.0'
30
+ networks:
31
+ - mc-net
32
  restart: unless-stopped
33
 
34
  volumes:
35
  mc-data:
36
+
37
+ networks:
38
+ mc-net:
39
+ driver: bridge
docs/LANDING-PAGE-HANDOFF.md ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mission Control — Landing Page Handoff
2
+
3
+ > Last updated: 2026-03-07 | Version: 1.3.0 | Branch: `fix/refactor` (bb5029e)
4
+
5
+ This document contains all copy, stats, features, and structure needed to build or update the Mission Control landing page. Everything below reflects the current state of the shipped product.
6
+
7
+ ---
8
+
9
+ ## Hero Section
10
+
11
+ **Headline:**
12
+ The Open-Source Dashboard for AI Agent Orchestration
13
+
14
+ **Subheadline:**
15
+ Manage agent fleets, track tasks, monitor costs, and orchestrate workflows — all from a single pane of glass. Zero external dependencies. One `pnpm start` to run.
16
+
17
+ **CTA:** `Get Started` -> GitHub repo | `Live Demo` -> demo instance (if available)
18
+
19
+ **Badges:**
20
+ - MIT License
21
+ - Next.js 16
22
+ - React 19
23
+ - TypeScript 5.7
24
+ - SQLite (WAL mode)
25
+ - 165 unit tests (Vitest)
26
+ - 295 E2E tests (Playwright)
27
+
28
+ **Hero image:** `docs/mission-control.jpg` (current dashboard screenshot — should be refreshed with latest UI)
29
+
30
+ ---
31
+
32
+ ## Key Stats (above the fold)
33
+
34
+ | Stat | Value |
35
+ |------|-------|
36
+ | Panels | 31 feature panels |
37
+ | API routes | 98 REST endpoints |
38
+ | Schema migrations | 36 |
39
+ | Test coverage | 165 unit + 295 E2E |
40
+ | Total commits | 239+ |
41
+ | External dependencies required | 0 (SQLite only, no Redis/Postgres/Docker) |
42
+ | Auth methods | 3 (session, API key, Google OAuth) |
43
+ | Framework adapters | 6 (OpenClaw, CrewAI, LangGraph, AutoGen, Claude SDK, Generic) |
44
+
45
+ ---
46
+
47
+ ## Feature Grid
48
+
49
+ ### 1. Task Board (Kanban)
50
+ Six-column kanban (Inbox > Assigned > In Progress > Review > Quality Review > Done) with drag-and-drop, priority levels, assignments, threaded comments, and inline sub-agent spawning. Multi-project support with per-project ticket prefixes (e.g. `PA-001`).
51
+
52
+ ### 2. Agent Management
53
+ Full lifecycle — register, heartbeat, wake, retire. Redesigned agent detail modal with compact overview, inline model selector, editable sub-agent configuration, and SOUL personality system. Local agent discovery from `~/.agents/`, `~/.codex/agents/`, `~/.claude/agents/`.
54
+
55
+ ### 3. Real-Time Monitoring
56
+ Live activity feed, session inspector, and log viewer with filtering. WebSocket + SSE push updates with smart polling that pauses when you're away. Gateway connection state with live dot indicators.
57
+
58
+ ### 4. Cost Tracking
59
+ Token usage dashboard with per-model breakdowns, trend charts, and cost analysis. Per-agent cost panels with session-level granularity.
60
+
61
+ ### 5. Quality Gates (Aegis)
62
+ Built-in review system that blocks task completion without sign-off. Automated Aegis quality review — scheduler polls review tasks and approves/rejects based on configurable criteria.
63
+
64
+ ### 6. Recurring Tasks
65
+ Natural language scheduling — "every morning at 9am", "every 2 hours". Zero-dependency schedule parser converts to cron. Template-clone pattern spawns dated child tasks (e.g. "Daily Report — Mar 07").
66
+
67
+ ### 7. Task Dispatch
68
+ Scheduler polls assigned tasks and runs agents via CLI. Dispatched tasks link to agent sessions for full traceability.
69
+
70
+ ### 8. Skills Hub
71
+ Browse, install, and manage agent skills from local directories and external registries (ClawdHub, skills.sh). Built-in security scanner checks for prompt injection, credential leaks, data exfiltration, and obfuscated content. Bidirectional disk-DB sync with SHA-256 change detection.
72
+
73
+ ### 9. Claude Code Integration
74
+ - **Session tracking** — auto-discovers sessions from `~/.claude/projects/`, extracts tokens, model info, costs
75
+ - **Task bridge** — read-only integration surfaces Claude Code team tasks and configs
76
+ - **Direct CLI** — connect Claude Code, Codex, or any CLI directly without a gateway
77
+
78
+ ### 10. Memory Knowledge Graph
79
+ Visual knowledge graph for agent memory in gateway mode. Interactive node-edge visualization of agent memory relationships.
80
+
81
+ ### 11. Agent Messaging (Comms)
82
+ Session-threaded inter-agent communication via comms API (`a2a:*`, `coord:*`, `session:*`). Coordinator inbox support with runtime tool-call visibility.
83
+
84
+ ### 12. Multi-Gateway
85
+ Connect to multiple agent gateways simultaneously. OS-level gateway discovery (systemd, Tailscale Serve). Auto-connect with health probes.
86
+
87
+ ### 13. Framework Adapters
88
+ Built-in adapter layer for multi-agent registration: OpenClaw, CrewAI, LangGraph, AutoGen, Claude SDK, and generic fallback. Each normalizes registration, heartbeats, and task reporting.
89
+
90
+ ### 14. Background Automation
91
+ Scheduled tasks for DB backups, stale record cleanup, agent heartbeat monitoring, recurring task spawning, and automated quality reviews.
92
+
93
+ ### 15. Webhooks & Alerts
94
+ Outbound webhooks with delivery history, retry with exponential backoff, circuit breaker, and HMAC-SHA256 signature verification. Configurable alert rules with cooldowns.
95
+
96
+ ### 16. GitHub Sync
97
+ Bidirectional GitHub Issues sync with label and assignee mapping. Full parity sync implementation.
98
+
99
+ ### 17. Security
100
+ - Ed25519 device identity for gateway handshake
101
+ - scrypt password hashing
102
+ - RBAC (viewer, operator, admin)
103
+ - CSRF origin checks
104
+ - CSP headers
105
+ - Rate limiting with trusted proxy support
106
+ - Per-agent rate limiting with `x-agent-name` identity-based quotas
107
+ - Skill security scanner
108
+
109
+ ### 18. Self-Update
110
+ GitHub release check with banner notification. One-click admin update (git pull, pnpm install, pnpm build). Dirty working trees rejected. All updates audit-logged.
111
+
112
+ ### 19. Audit Trail
113
+ Complete action type coverage with grouped filters. Full audit history for compliance and debugging.
114
+
115
+ ### 20. Pipelines & Workflows
116
+ Pipeline orchestration with workflow templates. Start, monitor, and manage multi-step agent workflows.
117
+
118
+ ---
119
+
120
+ ## "How It Works" Section
121
+
122
+ ```
123
+ 1. Clone & Start git clone ... && pnpm install && pnpm dev
124
+ 2. Agents Register Via gateway, CLI, or self-registration endpoint
125
+ 3. Tasks Flow Kanban board with automatic dispatch and quality gates
126
+ 4. Monitor & Scale Real-time dashboards, cost tracking, recurring automation
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Tech Stack Section
132
+
133
+ | Layer | Technology |
134
+ |-------|------------|
135
+ | Framework | Next.js 16 (App Router) |
136
+ | UI | React 19, Tailwind CSS 3.4 |
137
+ | Language | TypeScript 5.7 |
138
+ | Database | SQLite via better-sqlite3 (WAL mode) |
139
+ | State | Zustand 5 |
140
+ | Charts | Recharts 3 |
141
+ | Real-time | WebSocket + Server-Sent Events |
142
+ | Auth | scrypt hashing, session tokens, RBAC |
143
+ | Validation | Zod 4 |
144
+ | Testing | Vitest + Playwright |
145
+
146
+ ---
147
+
148
+ ## Auth & Access Section
149
+
150
+ **Three auth methods:**
151
+ 1. Session cookie — username/password login (7-day expiry)
152
+ 2. API key — `x-api-key` header for headless/agent access
153
+ 3. Google Sign-In — OAuth with admin approval workflow
154
+
155
+ **Three roles:**
156
+ | Role | Access |
157
+ |------|--------|
158
+ | Viewer | Read-only dashboard access |
159
+ | Operator | Read + write (tasks, agents, chat, spawn) |
160
+ | Admin | Full access (users, settings, system ops, webhooks) |
161
+
162
+ ---
163
+
164
+ ## Architecture Diagram (simplified)
165
+
166
+ ```
167
+ mission-control/
168
+ src/
169
+ app/api/ 98 REST API routes
170
+ components/
171
+ panels/ 31 feature panels
172
+ dashboard/ Overview dashboard
173
+ chat/ Agent chat workspace
174
+ layout/ NavRail, HeaderBar, LiveFeed
175
+ lib/
176
+ auth.ts Session + API key + Google OAuth
177
+ db.ts SQLite (WAL mode, 36 migrations)
178
+ scheduler.ts Background automation
179
+ websocket.ts Gateway WebSocket client
180
+ adapters/ 6 framework adapters
181
+ .data/ Runtime SQLite DB + token logs
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Quick Start Section
187
+
188
+ ```bash
189
+ git clone https://github.com/builderz-labs/mission-control.git
190
+ cd mission-control
191
+ pnpm install
192
+ cp .env.example .env # edit with your values
193
+ pnpm dev # http://localhost:3000
194
+ ```
195
+
196
+ Initial login seeded from `AUTH_USER` / `AUTH_PASS` on first run.
197
+
198
+ ---
199
+
200
+ ## Social Proof / Traction
201
+
202
+ - 239+ commits of active development
203
+ - Open-source MIT license
204
+ - Used in production for multi-agent orchestration
205
+ - Supports 6 agent frameworks out of the box
206
+ - Zero-config SQLite — no Docker, Redis, or Postgres required
207
+
208
+ ---
209
+
210
+ ## Roadmap / Coming Soon
211
+
212
+ - Agent-agnostic gateway support (OpenClaw, ZeroClaw, OpenFang, NeoBot, IronClaw, etc.)
213
+ - **Flight Deck** — native desktop companion app (Tauri v2) with real PTY terminal grid and system tray HUD (private beta)
214
+ - First-class per-agent cost breakdowns panel
215
+ - OAuth approval UI improvements
216
+ - API token rotation UI
217
+
218
+ ---
219
+
220
+ ## Recent Changelog (latest 20 notable changes)
221
+
222
+ 1. **Memory knowledge graph** — interactive visualization for agent memory in gateway mode
223
+ 2. **Agent detail modal redesign** — minimal header, compact overview, inline model selector
224
+ 3. **Spawn/task unification** — spawn moved inline to task board, sub-agent config to agent detail
225
+ 4. **Agent comms hardening** — session-threaded messaging with runtime tool visibility
226
+ 5. **Audit trail** — complete action type coverage with grouped filters
227
+ 6. **OS-level gateway discovery** — detect gateways via systemd and Tailscale Serve
228
+ 7. **GitHub sync** — full parity sync with loading state fixes
229
+ 8. **Automated Aegis quality review** — scheduler-driven approve/reject
230
+ 9. **Task dispatch** — scheduler polls and runs agents via CLI with session linking
231
+ 10. **Natural language recurring tasks** — zero-dep schedule parser + template spawning
232
+ 11. **Claude Code task bridge** — read-only team task and config integration
233
+ 12. **Agent card redesign** — gateway badge tooltips, ws:// localhost support
234
+ 13. **Skills Hub** — registry integration, bidirectional sync, security scanner
235
+ 14. **Per-agent rate limiting** — identity-based quotas via `x-agent-name`
236
+ 15. **Agent self-registration** — autonomous onboarding endpoint
237
+ 16. **Framework adapters** — OpenClaw, CrewAI, LangGraph, AutoGen, Claude SDK, generic
238
+ 17. **Self-update mechanism** — one-click update with audit logging
239
+ 18. **Local agent discovery** — auto-detect from ~/.agents, ~/.codex, ~/.claude
240
+ 19. **Chat workspace** — embedded chat with local session continuation
241
+ 20. **Ed25519 device identity** — secure gateway challenge-response handshake
242
+
243
+ ---
244
+
245
+ ## Footer
246
+
247
+ MIT License | 2026 Builderz Labs
248
+ GitHub: github.com/builderz-labs/mission-control
docs/SECURITY-HARDENING.md ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Security Hardening Guide
2
+
3
+ Comprehensive security hardening guide for Mission Control and OpenClaw Gateway deployments.
4
+
5
+ ## Quick Assessment
6
+
7
+ Run the automated security audit:
8
+
9
+ ```bash
10
+ bash scripts/security-audit.sh # Check .env and configuration
11
+ bash scripts/station-doctor.sh # Check runtime health
12
+ ```
13
+
14
+ Or use the diagnostics API (admin only):
15
+
16
+ ```bash
17
+ curl -H "x-api-key: $API_KEY" http://localhost:3000/api/diagnostics
18
+ curl -H "x-api-key: $API_KEY" http://localhost:3000/api/security-audit?timeframe=day
19
+ ```
20
+
21
+ The `posture.score` field (0-100) gives a quick posture assessment. The **Security Audit Panel** (`/security` in the dashboard) provides a full real-time view with timeline charts, agent trust scores, and eval results.
22
+
23
+ ---
24
+
25
+ ## Mission Control Hardening
26
+
27
+ ### 1. Credentials
28
+
29
+ **Generate strong credentials** using the included script:
30
+
31
+ ```bash
32
+ bash scripts/generate-env.sh # Generates .env with random secrets
33
+ chmod 600 .env # Lock down permissions
34
+ ```
35
+
36
+ The installer (`install.sh`) does this automatically. If you set up manually, ensure:
37
+
38
+ - `AUTH_PASS` is 12+ characters, not a dictionary word
39
+ - `API_KEY` is 32+ hex characters
40
+ - `AUTH_SECRET` is a unique random string
41
+ - `.env` file permissions are `600`
42
+
43
+ ### 2. Network Access Control
44
+
45
+ Mission Control uses a host allowlist in production:
46
+
47
+ ```env
48
+ # Only allow connections from these hosts (comma-separated)
49
+ MC_ALLOWED_HOSTS=localhost,127.0.0.1
50
+
51
+ # For Tailscale: MC_ALLOWED_HOSTS=localhost,127.0.0.1,100.*
52
+ # For a domain: MC_ALLOWED_HOSTS=mc.example.com,localhost
53
+
54
+ # NEVER set this in production:
55
+ # MC_ALLOW_ANY_HOST=1
56
+ ```
57
+
58
+ Deploy behind a reverse proxy with TLS (Caddy, nginx, Tailscale Funnel) for any network-accessible deployment.
59
+
60
+ ### 3. HTTPS & Cookies
61
+
62
+ For HTTPS deployments:
63
+
64
+ ```env
65
+ MC_COOKIE_SECURE=1 # Cookies only sent over HTTPS
66
+ MC_COOKIE_SAMESITE=strict # CSRF protection
67
+ MC_ENABLE_HSTS=1 # HTTP Strict Transport Security
68
+ ```
69
+
70
+ ### 4. Rate Limiting
71
+
72
+ Rate limiting is enabled by default:
73
+
74
+ | Endpoint Type | Limit |
75
+ |--------------|-------|
76
+ | Login | 5 attempts/min (always active) |
77
+ | Mutations | 60 requests/min |
78
+ | Reads | 120 requests/min |
79
+ | Heavy operations | 10 requests/min |
80
+ | Agent heartbeat | 30/min per agent |
81
+ | Agent task polling | 20/min per agent |
82
+
83
+ Never set `MC_DISABLE_RATE_LIMIT=1` in production.
84
+
85
+ ### 5. Docker Hardening
86
+
87
+ Use the production compose overlay:
88
+
89
+ ```bash
90
+ docker compose -f docker-compose.yml -f docker-compose.hardened.yml up -d
91
+ ```
92
+
93
+ This enables:
94
+ - **Read-only filesystem** with tmpfs for `/tmp` and `/app/.next/cache`
95
+ - **Capability dropping** — all Linux capabilities dropped, only `NET_BIND_SERVICE` retained
96
+ - **No new privileges** — prevents privilege escalation
97
+ - **PID limit** — prevents fork bombs
98
+ - **Memory/CPU limits** — prevents resource exhaustion
99
+ - **Log rotation** — prevents disk filling from verbose logging
100
+ - **HSTS, secure cookies** — forced via environment
101
+
102
+ ### 6. Security Headers
103
+
104
+ Mission Control sets these headers automatically:
105
+
106
+ | Header | Value |
107
+ |--------|-------|
108
+ | `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline' 'nonce-...'` |
109
+ | `X-Frame-Options` | `DENY` |
110
+ | `X-Content-Type-Options` | `nosniff` |
111
+ | `Referrer-Policy` | `strict-origin-when-cross-origin` |
112
+ | `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` |
113
+ | `X-Request-Id` | Unique per-request UUID for log correlation |
114
+ | `Strict-Transport-Security` | Set when `MC_ENABLE_HSTS=1` |
115
+
116
+ ### 7. Audit Logging
117
+
118
+ All security-relevant events are logged to the audit trail:
119
+
120
+ - Login attempts (success and failure)
121
+ - Task mutations
122
+ - User management actions
123
+ - Settings changes
124
+ - Update operations
125
+
126
+ Additionally, the **security event system** automatically logs:
127
+
128
+ - Auth failures (invalid passwords, expired tokens, access denials)
129
+ - Rate limit hits (429 responses with IP/agent correlation)
130
+ - Injection attempts (prompt injection, command injection, exfiltration)
131
+ - Secret exposures (AWS keys, GitHub tokens, Stripe keys, JWTs, private keys detected in agent messages)
132
+ - MCP tool calls (agent, tool, duration, success/failure)
133
+
134
+ These events feed into the **Security Audit Panel** (`/security`) which provides:
135
+
136
+ - **Posture score** (0-100) with level badges (hardened/secure/needs-attention/at-risk)
137
+ - **Agent trust scores** — weighted calculation based on auth failures, injection attempts, and task success rates
138
+ - **MCP call audit** — tool-use frequency, success/failure rates per agent
139
+ - **Timeline visualization** — event density over selected timeframe
140
+
141
+ Configure retention: `MC_RETAIN_AUDIT_DAYS=365` (default: 1 year).
142
+
143
+ ### 8. Hook Profiles
144
+
145
+ Security strictness is tunable via hook profiles in Settings > Security Profiles:
146
+
147
+ | Profile | Secret Scanning | MCP Auditing | Block on Secrets | Rate Limit Multiplier |
148
+ |---------|----------------|--------------|------------------|----------------------|
149
+ | **minimal** | Off | Off | No | 2x (relaxed) |
150
+ | **standard** (default) | On | On | No | 1x |
151
+ | **strict** | On | On | Yes (blocks messages) | 0.5x (tighter) |
152
+
153
+ Set via the Settings panel or the `hook_profile` key in the settings API.
154
+
155
+ ### 9. Agent Eval Framework
156
+
157
+ The four-layer eval stack helps detect degrading agent quality:
158
+
159
+ - **Output evals** — score task completion against golden datasets
160
+ - **Trace evals** — convergence scoring (>3.0 indicates looping behavior)
161
+ - **Component evals** — tool reliability from MCP call logs (p50/p95/p99 latency)
162
+ - **Drift detection** — 10% threshold vs 4-week rolling baseline triggers alerts
163
+
164
+ Access via `/api/agents/evals` or the Security Audit Panel's eval section.
165
+
166
+ ### 10. Data Retention
167
+
168
+ ```env
169
+ MC_RETAIN_ACTIVITIES_DAYS=90 # Activity feed
170
+ MC_RETAIN_AUDIT_DAYS=365 # Security audit trail
171
+ MC_RETAIN_LOGS_DAYS=30 # Application logs
172
+ MC_RETAIN_NOTIFICATIONS_DAYS=60 # Notifications
173
+ MC_RETAIN_PIPELINE_RUNS_DAYS=90 # Pipeline logs
174
+ MC_RETAIN_TOKEN_USAGE_DAYS=90 # Token/cost records
175
+ MC_RETAIN_GATEWAY_SESSIONS_DAYS=90 # Gateway session history
176
+ ```
177
+
178
+ ---
179
+
180
+ ## OpenClaw Gateway Hardening
181
+
182
+ Mission Control acts as the mothership for your OpenClaw fleet. The installer automatically checks and repairs common OpenClaw configuration issues.
183
+
184
+ ### 1. Network Security
185
+
186
+ - **Never expose the gateway publicly.** It runs on port 18789 by default.
187
+ - **Bind to localhost:** Set `gateway.bind: "loopback"` in `openclaw.json`.
188
+ - **Use SSH tunneling or Tailscale** for remote access.
189
+ - **Docker users:** Be aware that Docker can bypass UFW rules. Use `DOCKER-USER` chain rules.
190
+
191
+ ### 2. Authentication
192
+
193
+ - **Always enable gateway auth** with a strong random token.
194
+ - Generate: `openclaw doctor --generate-gateway-token`
195
+ - Store in `OPENCLAW_GATEWAY_TOKEN` env var (never in `NEXT_PUBLIC_*` variables).
196
+ - Rotate regularly.
197
+
198
+ ### 3. Hardened Gateway Configuration
199
+
200
+ ```json
201
+ {
202
+ "gateway": {
203
+ "mode": "local",
204
+ "bind": "loopback",
205
+ "auth": {
206
+ "mode": "token",
207
+ "token": "replace-with-long-random-token"
208
+ }
209
+ },
210
+ "session": {
211
+ "dmScope": "per-channel-peer"
212
+ },
213
+ "tools": {
214
+ "profile": "messaging",
215
+ "deny": ["group:automation", "group:runtime", "group:fs", "sessions_spawn", "sessions_send"],
216
+ "fs": { "workspaceOnly": true },
217
+ "exec": { "security": "deny", "ask": "always" }
218
+ },
219
+ "elevated": { "enabled": false }
220
+ }
221
+ ```
222
+
223
+ ### 4. File Permissions
224
+
225
+ ```bash
226
+ chmod 700 ~/.openclaw
227
+ chmod 600 ~/.openclaw/openclaw.json
228
+ chmod 600 ~/.openclaw/credentials/*
229
+ ```
230
+
231
+ ### 5. Tool Security
232
+
233
+ - Apply the principle of least privilege — only grant tools the agent needs.
234
+ - Audit third-party skills before installing (Mission Control's Skills Hub runs automatic security scans).
235
+ - Run agents processing untrusted content in a sandbox with a minimal toolset.
236
+
237
+ ### 6. Monitoring
238
+
239
+ - Enable comprehensive logging: `logging.redactSensitive: "tools"`
240
+ - Store logs separately where agents cannot modify them.
241
+ - Use Mission Control's diagnostics API to monitor gateway health.
242
+ - Have an incident response plan: stop gateway, revoke API keys, review audit logs.
243
+
244
+ ### 7. Known CVEs
245
+
246
+ Keep OpenClaw updated. Notable past vulnerabilities:
247
+
248
+ | CVE | Severity | Description | Fixed In |
249
+ |-----|----------|-------------|----------|
250
+ | CVE-2026-25253 | Critical | RCE via Control UI token hijack | v2026.1.29 |
251
+ | CVE-2026-26327 | High | Auth bypass via gateway spoofing | v2026.2.25 |
252
+ | CVE-2026-26322 | High | SSRF | v2026.2.25 |
253
+ | CVE-2026-26329 | High | Path traversal | v2026.2.25 |
254
+ | CVE-2026-26319 | Medium | Missing webhook auth | v2026.2.25 |
255
+
256
+ ---
257
+
258
+ ## Deployment Architecture
259
+
260
+ For production, the recommended architecture is:
261
+
262
+ ```
263
+ Internet
264
+ |
265
+ [Reverse Proxy (Caddy/nginx) + TLS]
266
+ |
267
+ [Mission Control :3000] ---- [SQLite .data/]
268
+ |
269
+ [OpenClaw Gateway :18789 (localhost only)]
270
+ |
271
+ [Agent Workspaces]
272
+ ```
273
+
274
+ - Reverse proxy handles TLS termination, rate limiting, and access control
275
+ - Mission Control listens on localhost or a private network
276
+ - OpenClaw Gateway is bound to loopback only
277
+ - Agent workspaces are isolated per-agent directories
docs/deployment.md CHANGED
@@ -48,6 +48,32 @@ PORT=3000 pnpm start
48
 
49
  **Important:** The production build bundles platform-specific native binaries. You must run `pnpm install` and `pnpm build` on the same OS and architecture as the target server. A build created on macOS will not work on Linux.
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  ## Production (Docker)
52
 
53
  ```bash
 
48
 
49
  **Important:** The production build bundles platform-specific native binaries. You must run `pnpm install` and `pnpm build` on the same OS and architecture as the target server. A build created on macOS will not work on Linux.
50
 
51
+ ## Production (Standalone)
52
+
53
+ Use this for bare-metal deployments that run Next's standalone server directly.
54
+ This path is preferred over ad hoc `node .next/standalone/server.js` because it
55
+ syncs `.next/static` and `public/` into the standalone bundle before launch.
56
+
57
+ ```bash
58
+ pnpm install --frozen-lockfile
59
+ pnpm build
60
+ pnpm start:standalone
61
+ ```
62
+
63
+ For a full in-place update on the target host:
64
+
65
+ ```bash
66
+ BRANCH=fix/refactor PORT=3000 pnpm deploy:standalone
67
+ ```
68
+
69
+ What `deploy:standalone` does:
70
+ - fetches and fast-forwards the requested branch
71
+ - reinstalls dependencies with the lockfile
72
+ - rebuilds from a clean `.next/`
73
+ - stops the old process bound to the target port
74
+ - starts the standalone server through `scripts/start-standalone.sh`
75
+ - verifies that the rendered login page references a CSS asset and that the CSS is served as `text/css`
76
+
77
  ## Production (Docker)
78
 
79
  ```bash
docs/plans/2026-03-10-onboarding-walkthrough-hardening.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Onboarding + Walkthrough hardening plan
2
+
3
+ Base branch: `fix/refactor`
4
+ Working branch: `fix/refactor-onboarding-walkthrough`
5
+
6
+ ## Goals
7
+ - Verify current onboarding and walkthrough flows are functional.
8
+ - Fix edge cases for first-run, skip, replay, and recovery states.
9
+ - Improve UX discoverability of walkthrough entry points.
10
+ - Add regression tests to keep flows stable.
11
+
12
+ ## Phase 1: audit and test map
13
+ 1. Identify current onboarding/walkthrough code paths.
14
+ 2. Document triggers, persistence flags, and routing.
15
+ 3. Add failing tests for first-run, skip, replay, and already-seen states.
16
+
17
+ ## Phase 2: implementation hardening
18
+ 1. Fix state transitions and persistence updates.
19
+ 2. Ensure walkthrough can be reopened from primary UI.
20
+ 3. Add visible hint/help entry to improve discoverability.
21
+ 4. Handle corrupted or partial onboarding state safely.
22
+
23
+ ## Phase 3: verification
24
+ 1. Run targeted tests for onboarding/walkthrough.
25
+ 2. Run full project checks.
26
+ 3. Validate end-to-end flow manually in local dev.
27
+
28
+ ## Deliverables
29
+ - Code changes in onboarding/walkthrough modules
30
+ - Automated tests covering key onboarding paths
31
+ - Updated docs/help text for walkthrough discoverability
install.sh ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Mission Control — One-Command Installer
3
+ # The mothership for your OpenClaw fleet.
4
+ #
5
+ # Usage:
6
+ # curl -fsSL https://raw.githubusercontent.com/builderz-labs/mission-control/main/install.sh | bash
7
+ # # or
8
+ # bash install.sh [--docker|--local] [--port PORT] [--data-dir DIR]
9
+ #
10
+ # Installs Mission Control and optionally repairs/configures OpenClaw.
11
+
12
+ set -euo pipefail
13
+
14
+ # ── Defaults ──────────────────────────────────────────────────────────────────
15
+ MC_PORT="${MC_PORT:-3000}"
16
+ MC_DATA_DIR=""
17
+ DEPLOY_MODE=""
18
+ SKIP_OPENCLAW=false
19
+ REPO_URL="https://github.com/builderz-labs/mission-control.git"
20
+ INSTALL_DIR="${MC_INSTALL_DIR:-$(pwd)/mission-control}"
21
+
22
+ # ── Parse arguments ───────────────────────────────────────────────────────────
23
+ while [[ $# -gt 0 ]]; do
24
+ case "$1" in
25
+ --docker) DEPLOY_MODE="docker"; shift ;;
26
+ --local) DEPLOY_MODE="local"; shift ;;
27
+ --port) MC_PORT="$2"; shift 2 ;;
28
+ --data-dir) MC_DATA_DIR="$2"; shift 2 ;;
29
+ --skip-openclaw) SKIP_OPENCLAW=true; shift ;;
30
+ --dir) INSTALL_DIR="$2"; shift 2 ;;
31
+ -h|--help)
32
+ echo "Usage: install.sh [--docker|--local] [--port PORT] [--data-dir DIR] [--dir INSTALL_DIR] [--skip-openclaw]"
33
+ exit 0 ;;
34
+ *) echo "Unknown option: $1"; exit 1 ;;
35
+ esac
36
+ done
37
+
38
+ # ── Helpers ───────────────────────────────────────────────────────────────────
39
+ info() { echo -e "\033[1;34m[MC]\033[0m $*"; }
40
+ ok() { echo -e "\033[1;32m[OK]\033[0m $*"; }
41
+ warn() { echo -e "\033[1;33m[!!]\033[0m $*"; }
42
+ err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; }
43
+ die() { err "$*"; exit 1; }
44
+
45
+ command_exists() { command -v "$1" &>/dev/null; }
46
+
47
+ detect_os() {
48
+ local os arch
49
+ os="$(uname -s)"
50
+ arch="$(uname -m)"
51
+
52
+ case "$os" in
53
+ Linux) OS="linux" ;;
54
+ Darwin) OS="darwin" ;;
55
+ *) die "Unsupported OS: $os" ;;
56
+ esac
57
+
58
+ case "$arch" in
59
+ x86_64|amd64) ARCH="x64" ;;
60
+ aarch64|arm64) ARCH="arm64" ;;
61
+ *) die "Unsupported architecture: $arch" ;;
62
+ esac
63
+
64
+ ok "Detected $OS/$ARCH"
65
+ }
66
+
67
+ check_prerequisites() {
68
+ local has_docker=false has_node=false
69
+
70
+ if command_exists docker && docker info &>/dev/null 2>&1; then
71
+ has_docker=true
72
+ ok "Docker available ($(docker --version | head -1))"
73
+ fi
74
+
75
+ if command_exists node; then
76
+ local node_major
77
+ node_major=$(node -v | sed 's/v//' | cut -d. -f1)
78
+ if [[ "$node_major" -ge 20 ]]; then
79
+ has_node=true
80
+ ok "Node.js $(node -v) available"
81
+ else
82
+ warn "Node.js $(node -v) found but v20+ required"
83
+ fi
84
+ fi
85
+
86
+ if ! $has_docker && ! $has_node; then
87
+ die "Either Docker or Node.js 20+ is required. Install one and retry."
88
+ fi
89
+
90
+ # Auto-select deploy mode if not specified
91
+ if [[ -z "$DEPLOY_MODE" ]]; then
92
+ if $has_docker; then
93
+ DEPLOY_MODE="docker"
94
+ info "Auto-selected Docker deployment (use --local to override)"
95
+ else
96
+ DEPLOY_MODE="local"
97
+ info "Auto-selected local deployment (Docker not available)"
98
+ fi
99
+ fi
100
+
101
+ # Validate chosen mode
102
+ if [[ "$DEPLOY_MODE" == "docker" ]] && ! $has_docker; then
103
+ die "Docker deployment requested but Docker is not available"
104
+ fi
105
+ if [[ "$DEPLOY_MODE" == "local" ]] && ! $has_node; then
106
+ die "Local deployment requested but Node.js 20+ is not available"
107
+ fi
108
+ if [[ "$DEPLOY_MODE" == "local" ]] && ! command_exists pnpm; then
109
+ info "Installing pnpm via corepack..."
110
+ corepack enable && corepack prepare pnpm@latest --activate
111
+ ok "pnpm installed"
112
+ fi
113
+ }
114
+
115
+ # ── Clone or update repo ─────────────────────────────────────────────────────
116
+ fetch_source() {
117
+ if [[ -d "$INSTALL_DIR/.git" ]]; then
118
+ info "Updating existing installation at $INSTALL_DIR..."
119
+ cd "$INSTALL_DIR"
120
+ git fetch --tags
121
+ local latest_tag
122
+ latest_tag=$(git describe --tags --abbrev=0 origin/main 2>/dev/null || echo "")
123
+ if [[ -n "$latest_tag" ]]; then
124
+ git checkout "$latest_tag"
125
+ ok "Checked out $latest_tag"
126
+ else
127
+ git pull origin main
128
+ ok "Updated to latest main"
129
+ fi
130
+ else
131
+ info "Cloning Mission Control..."
132
+ if command_exists git; then
133
+ git clone --depth 1 "$REPO_URL" "$INSTALL_DIR"
134
+ cd "$INSTALL_DIR"
135
+ ok "Cloned to $INSTALL_DIR"
136
+ else
137
+ die "git is required to clone the repository"
138
+ fi
139
+ fi
140
+ }
141
+
142
+ # ── Generate .env ─────────────────────────────────────────────────────────────
143
+ setup_env() {
144
+ if [[ -f "$INSTALL_DIR/.env" ]]; then
145
+ info "Existing .env found — keeping current configuration"
146
+ return
147
+ fi
148
+
149
+ info "Generating secure .env configuration..."
150
+ bash "$INSTALL_DIR/scripts/generate-env.sh" "$INSTALL_DIR/.env"
151
+
152
+ # Set the port if non-default
153
+ if [[ "$MC_PORT" != "3000" ]]; then
154
+ if [[ "$(uname)" == "Darwin" ]]; then
155
+ sed -i '' "s|^# PORT=3000|PORT=$MC_PORT|" "$INSTALL_DIR/.env"
156
+ else
157
+ sed -i "s|^# PORT=3000|PORT=$MC_PORT|" "$INSTALL_DIR/.env"
158
+ fi
159
+ fi
160
+
161
+ ok "Secure .env generated"
162
+ }
163
+
164
+ # ── Docker deployment ─────────────────────────────────────────────────────────
165
+ deploy_docker() {
166
+ info "Starting Docker deployment..."
167
+
168
+ export MC_PORT
169
+ docker compose up -d --build
170
+
171
+ # Wait for healthy
172
+ info "Waiting for Mission Control to become healthy..."
173
+ local retries=30
174
+ while [[ $retries -gt 0 ]]; do
175
+ if docker compose ps --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
176
+ break
177
+ fi
178
+ # Fallback: try HTTP check
179
+ if curl -sf "http://localhost:$MC_PORT/login" &>/dev/null; then
180
+ break
181
+ fi
182
+ sleep 2
183
+ ((retries--))
184
+ done
185
+
186
+ if [[ $retries -eq 0 ]]; then
187
+ warn "Timeout waiting for health check — container may still be starting"
188
+ docker compose logs --tail 20
189
+ else
190
+ ok "Mission Control is running in Docker"
191
+ fi
192
+ }
193
+
194
+ # ── Local deployment ──────────────────────────────────────────────────────────
195
+ deploy_local() {
196
+ info "Starting local deployment..."
197
+
198
+ cd "$INSTALL_DIR"
199
+ pnpm install --frozen-lockfile 2>/dev/null || pnpm install
200
+ ok "Dependencies installed"
201
+
202
+ info "Building Mission Control..."
203
+ pnpm build
204
+ ok "Build complete"
205
+
206
+ # Create systemd service on Linux if systemctl is available
207
+ if [[ "$OS" == "linux" ]] && command_exists systemctl; then
208
+ setup_systemd
209
+ fi
210
+
211
+ info "Starting Mission Control..."
212
+ PORT="$MC_PORT" nohup pnpm start > "$INSTALL_DIR/.data/mc.log" 2>&1 &
213
+ local pid=$!
214
+ echo "$pid" > "$INSTALL_DIR/.data/mc.pid"
215
+
216
+ sleep 3
217
+ if kill -0 "$pid" 2>/dev/null; then
218
+ ok "Mission Control running (PID $pid)"
219
+ else
220
+ err "Failed to start. Check logs: $INSTALL_DIR/.data/mc.log"
221
+ exit 1
222
+ fi
223
+ }
224
+
225
+ # ── Systemd service ──────────────────────────────────────────────────────────
226
+ setup_systemd() {
227
+ local service_file="/etc/systemd/system/mission-control.service"
228
+ if [[ -f "$service_file" ]]; then
229
+ info "Systemd service already exists"
230
+ return
231
+ fi
232
+
233
+ info "Creating systemd service..."
234
+ local user
235
+ user="$(whoami)"
236
+ local node_path
237
+ node_path="$(which node)"
238
+
239
+ cat > /tmp/mission-control.service <<UNIT
240
+ [Unit]
241
+ Description=Mission Control - OpenClaw Agent Dashboard
242
+ After=network.target
243
+
244
+ [Service]
245
+ Type=simple
246
+ User=$user
247
+ WorkingDirectory=$INSTALL_DIR
248
+ ExecStart=$node_path $INSTALL_DIR/.next/standalone/server.js
249
+ Restart=on-failure
250
+ RestartSec=5
251
+ Environment=NODE_ENV=production
252
+ Environment=PORT=$MC_PORT
253
+ EnvironmentFile=$INSTALL_DIR/.env
254
+
255
+ [Install]
256
+ WantedBy=multi-user.target
257
+ UNIT
258
+
259
+ if [[ "$(id -u)" -eq 0 ]]; then
260
+ mv /tmp/mission-control.service "$service_file"
261
+ systemctl daemon-reload
262
+ systemctl enable mission-control
263
+ ok "Systemd service installed and enabled"
264
+ else
265
+ info "Run as root to install systemd service:"
266
+ info " sudo mv /tmp/mission-control.service $service_file"
267
+ info " sudo systemctl daemon-reload && sudo systemctl enable --now mission-control"
268
+ fi
269
+ }
270
+
271
+ # ── OpenClaw fleet check ─────────────────────────────────────────────────────
272
+ check_openclaw() {
273
+ if $SKIP_OPENCLAW; then
274
+ info "Skipping OpenClaw checks (--skip-openclaw)"
275
+ return
276
+ fi
277
+
278
+ echo ""
279
+ info "=== OpenClaw Fleet Check ==="
280
+
281
+ # Check if openclaw binary exists
282
+ if command_exists openclaw; then
283
+ local oc_version
284
+ oc_version="$(openclaw --version 2>/dev/null || echo 'unknown')"
285
+ ok "OpenClaw binary found: $oc_version"
286
+ elif command_exists clawdbot; then
287
+ local cb_version
288
+ cb_version="$(clawdbot --version 2>/dev/null || echo 'unknown')"
289
+ ok "ClawdBot binary found: $cb_version (legacy)"
290
+ warn "Consider upgrading to openclaw CLI"
291
+ else
292
+ info "OpenClaw CLI not found — install it to enable agent orchestration"
293
+ info " See: https://github.com/builderz-labs/openclaw"
294
+ return
295
+ fi
296
+
297
+ # Check OpenClaw home directory
298
+ local oc_home="${OPENCLAW_HOME:-$HOME/.openclaw}"
299
+ if [[ -d "$oc_home" ]]; then
300
+ ok "OpenClaw home: $oc_home"
301
+
302
+ # Check config
303
+ local oc_config="$oc_home/openclaw.json"
304
+ if [[ -f "$oc_config" ]]; then
305
+ ok "Config found: $oc_config"
306
+ else
307
+ warn "No openclaw.json found at $oc_config"
308
+ info "Mission Control will create a default config on first gateway connection"
309
+ fi
310
+
311
+ # Check for stale PID files
312
+ local stale_count=0
313
+ for pidfile in "$oc_home"/*.pid "$oc_home"/pids/*.pid; do
314
+ [[ -f "$pidfile" ]] || continue
315
+ local pid
316
+ pid="$(cat "$pidfile" 2>/dev/null)" || continue
317
+ if ! kill -0 "$pid" 2>/dev/null; then
318
+ rm -f "$pidfile"
319
+ ((stale_count++))
320
+ fi
321
+ done
322
+ if [[ $stale_count -gt 0 ]]; then
323
+ ok "Cleaned $stale_count stale PID file(s)"
324
+ fi
325
+
326
+ # Check logs directory size
327
+ local logs_dir="$oc_home/logs"
328
+ if [[ -d "$logs_dir" ]]; then
329
+ local logs_size
330
+ if [[ "$(uname)" == "Darwin" ]]; then
331
+ logs_size="$(du -sh "$logs_dir" 2>/dev/null | cut -f1)"
332
+ else
333
+ logs_size="$(du -sh "$logs_dir" 2>/dev/null | cut -f1)"
334
+ fi
335
+ info "Logs directory: $logs_size ($logs_dir)"
336
+
337
+ # Clean old logs (> 30 days)
338
+ local old_logs
339
+ old_logs=$(find "$logs_dir" -name "*.log" -mtime +30 2>/dev/null | wc -l | tr -d ' ')
340
+ if [[ "$old_logs" -gt 0 ]]; then
341
+ find "$logs_dir" -name "*.log" -mtime +30 -delete 2>/dev/null || true
342
+ ok "Cleaned $old_logs log file(s) older than 30 days"
343
+ fi
344
+ fi
345
+
346
+ # Check workspace directory
347
+ local workspace="$oc_home/workspace"
348
+ if [[ -d "$workspace" ]]; then
349
+ local agent_count
350
+ agent_count=$(find "$workspace" -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')
351
+ ((agent_count--)) # subtract the workspace dir itself
352
+ info "Workspace: $agent_count agent workspace(s) in $workspace"
353
+ fi
354
+ else
355
+ info "OpenClaw home not found at $oc_home"
356
+ info "Set OPENCLAW_HOME in .env to point to your OpenClaw state directory"
357
+ fi
358
+
359
+ # Check gateway port
360
+ local gw_host="${OPENCLAW_GATEWAY_HOST:-127.0.0.1}"
361
+ local gw_port="${OPENCLAW_GATEWAY_PORT:-18789}"
362
+ if nc -z "$gw_host" "$gw_port" 2>/dev/null || (echo > "/dev/tcp/$gw_host/$gw_port") 2>/dev/null; then
363
+ ok "Gateway reachable at $gw_host:$gw_port"
364
+ else
365
+ info "Gateway not reachable at $gw_host:$gw_port (start it with: openclaw gateway start)"
366
+ fi
367
+ }
368
+
369
+ # ── Main ──────────────────────────────────────────────────────────────────────
370
+ main() {
371
+ echo ""
372
+ echo " ╔══════════════════════════════════════╗"
373
+ echo " ║ Mission Control Installer ║"
374
+ echo " ║ The mothership for your fleet ║"
375
+ echo " ╚══════════════════════════════════════╝"
376
+ echo ""
377
+
378
+ detect_os
379
+ check_prerequisites
380
+
381
+ # If running from within an existing clone, use current dir
382
+ if [[ -f "$(pwd)/package.json" ]] && grep -q '"mission-control"' "$(pwd)/package.json" 2>/dev/null; then
383
+ INSTALL_DIR="$(pwd)"
384
+ info "Running from existing clone at $INSTALL_DIR"
385
+ else
386
+ fetch_source
387
+ fi
388
+
389
+ # Ensure data directory exists
390
+ mkdir -p "$INSTALL_DIR/.data"
391
+
392
+ setup_env
393
+
394
+ case "$DEPLOY_MODE" in
395
+ docker) deploy_docker ;;
396
+ local) deploy_local ;;
397
+ *) die "Unknown deploy mode: $DEPLOY_MODE" ;;
398
+ esac
399
+
400
+ check_openclaw
401
+
402
+ # ── Print summary ──
403
+ echo ""
404
+ echo " ╔══════════════════════════════════════╗"
405
+ echo " ║ Installation Complete ║"
406
+ echo " ╚══════════════════════════════════════╝"
407
+ echo ""
408
+ info "Dashboard: http://localhost:$MC_PORT"
409
+ info "Mode: $DEPLOY_MODE"
410
+ info "Data: $INSTALL_DIR/.data/"
411
+ echo ""
412
+ info "Credentials are in: $INSTALL_DIR/.env"
413
+ echo ""
414
+
415
+ if [[ "$DEPLOY_MODE" == "docker" ]]; then
416
+ info "Manage:"
417
+ info " docker compose logs -f # view logs"
418
+ info " docker compose restart # restart"
419
+ info " docker compose down # stop"
420
+ else
421
+ info "Manage:"
422
+ info " cat $INSTALL_DIR/.data/mc.log # view logs"
423
+ info " kill \$(cat $INSTALL_DIR/.data/mc.pid) # stop"
424
+ fi
425
+
426
+ echo ""
427
+ }
428
+
429
+ main "$@"
next.config.js CHANGED
@@ -1,6 +1,9 @@
1
  /** @type {import('next').NextConfig} */
2
  const nextConfig = {
3
  output: 'standalone',
 
 
 
4
  turbopack: {},
5
  // Transpile ESM-only packages so they resolve correctly in all environments
6
  transpilePackages: ['react-markdown', 'remark-gfm'],
@@ -11,12 +14,13 @@ const nextConfig = {
11
 
12
  const csp = [
13
  `default-src 'self'`,
14
- `script-src 'self' 'unsafe-inline'${googleEnabled ? ' https://accounts.google.com' : ''}`,
15
  `style-src 'self' 'unsafe-inline'`,
16
- `connect-src 'self' ws: wss: http://127.0.0.1:* http://localhost:*`,
17
  `img-src 'self' data: blob:${googleEnabled ? ' https://*.googleusercontent.com https://lh3.googleusercontent.com' : ''}`,
18
  `font-src 'self' data:`,
19
  `frame-src 'self'${googleEnabled ? ' https://accounts.google.com' : ''}`,
 
20
  ].join('; ')
21
 
22
  return [
 
1
  /** @type {import('next').NextConfig} */
2
  const nextConfig = {
3
  output: 'standalone',
4
+ outputFileTracingExcludes: {
5
+ '/*': ['./.data/**/*'],
6
+ },
7
  turbopack: {},
8
  // Transpile ESM-only packages so they resolve correctly in all environments
9
  transpilePackages: ['react-markdown', 'remark-gfm'],
 
14
 
15
  const csp = [
16
  `default-src 'self'`,
17
+ `script-src 'self' 'unsafe-inline' blob:${googleEnabled ? ' https://accounts.google.com' : ''}`,
18
  `style-src 'self' 'unsafe-inline'`,
19
+ `connect-src 'self' ws: wss: http://127.0.0.1:* http://localhost:* https://cdn.jsdelivr.net`,
20
  `img-src 'self' data: blob:${googleEnabled ? ' https://*.googleusercontent.com https://lh3.googleusercontent.com' : ''}`,
21
  `font-src 'self' data:`,
22
  `frame-src 'self'${googleEnabled ? ' https://accounts.google.com' : ''}`,
23
+ `worker-src 'self' blob:`,
24
  ].join('; ')
25
 
26
  return [
openclaw_hardening_guide.md ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenClaw Gateway Security and Hardening Best Practices
2
+
3
+ This document consolidates security and hardening best practices for the OpenClaw Gateway, drawing from official documentation and recent security advisories.
4
+
5
+ ## 1. Core Security Model & Deployment Considerations
6
+
7
+ OpenClaw is designed primarily for a **personal assistant deployment model**, assuming one trusted operator per gateway. It is **not intended for multi-tenant environments** with untrusted or adversarial users. For such scenarios, run separate gateway instances for each trust boundary.
8
+
9
+ ## 2. Hardened Baseline Configuration
10
+
11
+ For a secure starting point, consider the following configuration, which keeps the Gateway local, isolates DMs, and disables potentially dangerous tools by default:
12
+
13
+ ```json
14
+ {
15
+ "gateway": {
16
+ "mode": "local",
17
+ "bind": "loopback",
18
+ "auth": {
19
+ "mode": "token",
20
+ "token": "replace-with-long-random-token"
21
+ }
22
+ },
23
+ "session": {
24
+ "dmScope": "per-channel-peer"
25
+ },
26
+ "tools": {
27
+ "profile": "messaging",
28
+ "deny": ["group:automation", "group:runtime", "group:fs", "sessions_spawn", "sessions_send"],
29
+ "fs": {
30
+ "workspaceOnly": true
31
+ },
32
+ "exec": {
33
+ "security": "deny",
34
+ "ask": "always"
35
+ }
36
+ },
37
+ "elevated": {
38
+ "enabled": false
39
+ },
40
+ "channels": {
41
+ "whatsapp": {
42
+ "dmPolicy": "pairing",
43
+ "groups": {
44
+ "*": {
45
+ "requireMention": true
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ## 3. Key Hardening Recommendations
54
+
55
+ ### 3.1. Network Security
56
+
57
+ * **Do Not Expose Publicly:** Never expose the OpenClaw gateway directly to the public internet. It typically runs on port 18789. Publicly exposed gateways are easily discoverable.
58
+ * **Bind to Localhost:** Configure the gateway to listen only for connections from the local machine by binding it to `127.0.0.1` (localhost) or `loopback` in your `openclaw.json`.
59
+ * **Firewall Rules:** Implement strict firewall rules to block all unnecessary inbound and outbound connections, allowing only essential traffic.
60
+ * **Secure Remote Access:** For remote access, use secure methods like SSH tunneling or a VPN (e.g., Tailscale) instead of direct exposure.
61
+ * **Docker Considerations:** If using Docker, be aware that it can bypass UFW rules. Configure rules in the `DOCKER-USER` chain to control exposure.
62
+
63
+ ### 3.2. Authentication and Access Control
64
+
65
+ * **Enable Gateway Authentication:** Always enable gateway authentication and use a strong, randomly generated authentication token. Generate a token with `openclaw doctor --generate-gateway-token`.
66
+ * **Manage Access Tokens:** Treat your gateway authentication token like a password. Rotate it regularly and store it securely (e.g., as an environment variable, not in plaintext config files).
67
+ * **Restrict Chat and Messaging:** If integrating with chat platforms, use allowlists to specify which user IDs can interact with your agent.
68
+ * **Direct Messages (DMs) and Groups:**
69
+ * For DMs, use the default `pairing` policy (`dmPolicy: "pairing"`) to require approval for unknown senders.
70
+ * For group chats, require the bot to be explicitly mentioned to respond (`requireMention: true`).
71
+ * Isolate DM sessions using `session.dmScope: "per-channel-peer"` to prevent context leakage.
72
+
73
+ ### 3.3. Isolation and Sandboxing
74
+
75
+ * **Run in a Docker Container:** The recommended approach is to run OpenClaw within a Docker container for process isolation, filesystem restrictions, and network controls.
76
+ * **Harden Docker Configuration:**
77
+ * Do not mount your home directory or the Docker socket.
78
+ * Use read-only filesystems where possible.
79
+ * Drop unnecessary Linux capabilities.
80
+ * Run the container as a non-root user.
81
+ * **Enable Sandbox Mode:** For tasks that execute code, enable OpenClaw's sandbox mode to prevent malicious or compromised prompts from accessing your system or network. Configure this in `agents.defaults.sandbox`.
82
+
83
+ ### 3.4. Credential and Secret Management
84
+
85
+ * **Avoid Plaintext Storage:** Never store API keys, tokens, or other sensitive information in plaintext configuration files.
86
+ * **Use Secure Storage Mechanisms:** Load credentials from environment variables or use dedicated secrets management solutions (e.g., Hashicorp Vault, AWS Secrets Manager).
87
+
88
+ ### 3.5. File System Permissions
89
+
90
+ * Ensure your configuration and state files are private.
91
+ * `~/.openclaw/openclaw.json` should have permissions `600` (user read/write only).
92
+ * The `~/.openclaw` directory should have permissions `700` (user access only).
93
+ * `~/.openclaw/credentials/` and its contents should also be `600`.
94
+
95
+ ### 3.6. Tool and Skill Security
96
+
97
+ * **Principle of Least Privilege:** Only grant the agent the permissions and tools it absolutely needs.
98
+ * **Audit Third-Party Skills:** Be extremely cautious with third-party skills, as they can contain malicious code. Research has shown a significant number of skills on marketplaces may be malicious.
99
+
100
+ ### 3.7. Prompt Injection Mitigation
101
+
102
+ * Lock down who can message the bot using DM pairing and allowlists.
103
+ * Require mentions in group chats.
104
+ * Run agents that process untrusted content in a sandbox with a minimal toolset.
105
+ * Use the latest, most powerful models, as they are generally more resistant to prompt injection.
106
+
107
+ ### 3.8. Monitoring and Incident Response
108
+
109
+ * **Enable Logging:** Turn on comprehensive logging for all agent activities (command executions, API calls, file access). Store logs in a secure, separate location where the agent cannot modify them.
110
+ * **Log Redaction:** Keep log redaction enabled (`logging.redactSensitive: "tools"`) to prevent sensitive information from leaking into logs.
111
+ * **Incident Response Plan:** Have a plan for suspected compromises, including stopping the gateway and revoking API keys.
112
+
113
+ ## 4. Staying Updated and Aware of Vulnerabilities
114
+
115
+ The OpenClaw project is under active development, and new vulnerabilities are discovered.
116
+
117
+ * **Keep Software Updated:** Regularly update OpenClaw and its dependencies to ensure you have the latest security patches.
118
+ * **Be Aware of Recent Threats:** Stay informed about new vulnerabilities. Notable past vulnerabilities include:
119
+ * **ClawJacked (High Severity):** Allowed malicious websites to hijack locally running OpenClaw instances via WebSocket connections and brute-force password. Patched in v2026.2.25.
120
+ * **Remote Code Execution (Critical - CVE-2026-25253):** A malicious link could trick the Control UI into sending an auth token to an attacker-controlled server, leading to RCE. Patched in v2026.1.29.
121
+ * **Authentication Bypass (High Severity - CVE-2026-26327):** Allowed attackers on the same local network to intercept credentials by spoofing a legitimate gateway.
122
+ * **Other Vulnerabilities:** Server-Side Request Forgery (SSRF - CVE-2026-26322), missing webhook authentication (CVE-2026-26319), and path traversal (CVE-2026-26329).
123
+
124
+ By diligently applying these practices, you can significantly enhance the security posture of your OpenClaw Gateway deployment.
package.json CHANGED
@@ -1,28 +1,33 @@
1
  {
2
  "name": "mission-control",
3
- "version": "1.3.0",
4
  "description": "OpenClaw Mission Control — open-source agent orchestration dashboard",
5
  "scripts": {
6
- "dev": "next dev --hostname 127.0.0.1 --port ${PORT:-3000}",
7
- "build": "next build",
8
- "start": "next start --hostname 0.0.0.0 --port ${PORT:-3000}",
9
- "lint": "eslint .",
10
- "typecheck": "tsc --noEmit",
11
- "test": "vitest run",
 
 
 
12
  "test:watch": "vitest",
13
  "test:ui": "vitest --ui",
14
- "test:e2e": "playwright test",
15
- "test:e2e:openclaw:local": "E2E_GATEWAY_EXPECTED=0 playwright test -c playwright.openclaw.local.config.ts",
16
- "test:e2e:openclaw:gateway": "E2E_GATEWAY_EXPECTED=1 playwright test -c playwright.openclaw.gateway.config.ts",
17
  "test:e2e:openclaw": "pnpm test:e2e:openclaw:local && pnpm test:e2e:openclaw:gateway",
18
  "test:all": "pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm test:e2e",
19
  "quality:gate": "pnpm test:all"
20
  },
21
  "dependencies": {
 
22
  "@scalar/api-reference-react": "^0.8.66",
23
  "@xyflow/react": "^12.10.0",
24
  "autoprefixer": "^10.4.20",
25
  "better-sqlite3": "^12.6.2",
 
26
  "clsx": "^2.1.1",
27
  "eslint": "^9.18.0",
28
  "eslint-config-next": "^16.1.6",
@@ -34,6 +39,7 @@
34
  "react-dom": "^19.0.1",
35
  "react-markdown": "^10.1.0",
36
  "reactflow": "^11.11.4",
 
37
  "recharts": "^3.7.0",
38
  "remark-gfm": "^4.0.1",
39
  "tailwind-merge": "^3.4.0",
@@ -60,7 +66,7 @@
60
  "vitest": "^2.1.5"
61
  },
62
  "engines": {
63
- "node": ">=20"
64
  },
65
  "keywords": [
66
  "openclaw",
 
1
  {
2
  "name": "mission-control",
3
+ "version": "2.0.0",
4
  "description": "OpenClaw Mission Control — open-source agent orchestration dashboard",
5
  "scripts": {
6
+ "verify:node": "node scripts/check-node-version.mjs",
7
+ "dev": "pnpm run verify:node && next dev --hostname 127.0.0.1 --port ${PORT:-3000}",
8
+ "build": "pnpm run verify:node && next build",
9
+ "start": "pnpm run verify:node && next start --hostname 0.0.0.0 --port ${PORT:-3000}",
10
+ "start:standalone": "pnpm run verify:node && bash scripts/start-standalone.sh",
11
+ "deploy:standalone": "pnpm run verify:node && bash scripts/deploy-standalone.sh",
12
+ "lint": "pnpm run verify:node && eslint .",
13
+ "typecheck": "pnpm run verify:node && tsc --noEmit",
14
+ "test": "pnpm run verify:node && vitest run",
15
  "test:watch": "vitest",
16
  "test:ui": "vitest --ui",
17
+ "test:e2e": "pnpm run verify:node && playwright test",
18
+ "test:e2e:openclaw:local": "pnpm run verify:node && E2E_GATEWAY_EXPECTED=0 playwright test -c playwright.openclaw.local.config.ts",
19
+ "test:e2e:openclaw:gateway": "pnpm run verify:node && E2E_GATEWAY_EXPECTED=1 playwright test -c playwright.openclaw.gateway.config.ts",
20
  "test:e2e:openclaw": "pnpm test:e2e:openclaw:local && pnpm test:e2e:openclaw:gateway",
21
  "test:all": "pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm test:e2e",
22
  "quality:gate": "pnpm test:all"
23
  },
24
  "dependencies": {
25
+ "@radix-ui/react-slot": "^1.2.4",
26
  "@scalar/api-reference-react": "^0.8.66",
27
  "@xyflow/react": "^12.10.0",
28
  "autoprefixer": "^10.4.20",
29
  "better-sqlite3": "^12.6.2",
30
+ "class-variance-authority": "^0.7.1",
31
  "clsx": "^2.1.1",
32
  "eslint": "^9.18.0",
33
  "eslint-config-next": "^16.1.6",
 
39
  "react-dom": "^19.0.1",
40
  "react-markdown": "^10.1.0",
41
  "reactflow": "^11.11.4",
42
+ "reagraph": "^4.30.8",
43
  "recharts": "^3.7.0",
44
  "remark-gfm": "^4.0.1",
45
  "tailwind-merge": "^3.4.0",
 
66
  "vitest": "^2.1.5"
67
  },
68
  "engines": {
69
+ "node": ">=22 <23"
70
  },
71
  "keywords": [
72
  "openclaw",
playwright.config.ts CHANGED
@@ -18,14 +18,13 @@ export default defineConfig({
18
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } }
19
  ],
20
  webServer: {
21
- command: 'node .next/standalone/server.js',
22
  url: 'http://127.0.0.1:3005',
23
  reuseExistingServer: true,
24
  timeout: 120_000,
25
  env: {
26
  ...process.env,
27
- HOSTNAME: process.env.HOSTNAME || '127.0.0.1',
28
- PORT: process.env.PORT || '3005',
29
  MC_DISABLE_RATE_LIMIT: process.env.MC_DISABLE_RATE_LIMIT || '1',
30
  MC_WORKLOAD_QUEUE_DEPTH_THROTTLE: process.env.MC_WORKLOAD_QUEUE_DEPTH_THROTTLE || '1000',
31
  MC_WORKLOAD_QUEUE_DEPTH_SHED: process.env.MC_WORKLOAD_QUEUE_DEPTH_SHED || '2000',
@@ -34,7 +33,6 @@ export default defineConfig({
34
  API_KEY: process.env.API_KEY || 'test-api-key-e2e-12345',
35
  AUTH_USER: process.env.AUTH_USER || 'testadmin',
36
  AUTH_PASS: process.env.AUTH_PASS || 'testpass1234!',
37
- OPENCLAW_MEMORY_DIR: process.env.OPENCLAW_MEMORY_DIR || '.data/e2e-memory',
38
  },
39
  }
40
  })
 
18
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } }
19
  ],
20
  webServer: {
21
+ command: 'node scripts/e2e-openclaw/start-e2e-server.mjs --mode=local',
22
  url: 'http://127.0.0.1:3005',
23
  reuseExistingServer: true,
24
  timeout: 120_000,
25
  env: {
26
  ...process.env,
27
+ MISSION_CONTROL_TEST_MODE: process.env.MISSION_CONTROL_TEST_MODE || '1',
 
28
  MC_DISABLE_RATE_LIMIT: process.env.MC_DISABLE_RATE_LIMIT || '1',
29
  MC_WORKLOAD_QUEUE_DEPTH_THROTTLE: process.env.MC_WORKLOAD_QUEUE_DEPTH_THROTTLE || '1000',
30
  MC_WORKLOAD_QUEUE_DEPTH_SHED: process.env.MC_WORKLOAD_QUEUE_DEPTH_SHED || '2000',
 
33
  API_KEY: process.env.API_KEY || 'test-api-key-e2e-12345',
34
  AUTH_USER: process.env.AUTH_USER || 'testadmin',
35
  AUTH_PASS: process.env.AUTH_PASS || 'testpass1234!',
 
36
  },
37
  }
38
  })
pnpm-lock.yaml CHANGED
@@ -8,6 +8,9 @@ importers:
8
 
9
  .:
10
  dependencies:
 
 
 
11
  '@scalar/api-reference-react':
12
  specifier: ^0.8.66
13
  version: 0.8.66(react@19.2.4)(tailwindcss@3.4.19(yaml@2.8.2))(typescript@5.9.3)
@@ -20,6 +23,9 @@ importers:
20
  better-sqlite3:
21
  specifier: ^12.6.2
22
  version: 12.6.2
 
 
 
23
  clsx:
24
  specifier: ^2.1.1
25
  version: 2.1.1
@@ -53,6 +59,9 @@ importers:
53
  reactflow:
54
  specifier: ^11.11.4
55
  version: 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
 
 
 
56
  recharts:
57
  specifier: ^3.7.0
58
  version: 3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1)
@@ -306,6 +315,9 @@ packages:
306
  resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
307
  engines: {node: '>=18'}
308
 
 
 
 
309
  '@emnapi/core@1.8.1':
310
  resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
311
 
@@ -736,6 +748,14 @@ packages:
736
  '@marijn/find-cluster-break@1.0.2':
737
  resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
738
 
 
 
 
 
 
 
 
 
739
  '@napi-rs/wasm-runtime@0.2.12':
740
  resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
741
 
@@ -828,6 +848,88 @@ packages:
828
  engines: {node: '>=18'}
829
  hasBin: true
830
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
831
  '@reactflow/background@11.3.14':
832
  resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
833
  peerDependencies:
@@ -1175,6 +1277,9 @@ packages:
1175
  '@types/react-dom':
1176
  optional: true
1177
 
 
 
 
1178
  '@tybys/wasm-util@0.10.1':
1179
  resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
1180
 
@@ -1292,6 +1397,9 @@ packages:
1292
  '@types/debug@4.1.12':
1293
  resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
1294
 
 
 
 
1295
  '@types/estree-jsx@1.0.5':
1296
  resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
1297
 
@@ -1322,14 +1430,28 @@ packages:
1322
  '@types/node@22.19.9':
1323
  resolution: {integrity: sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==}
1324
 
 
 
 
1325
  '@types/react-dom@19.2.3':
1326
  resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
1327
  peerDependencies:
1328
  '@types/react': ^19.2.0
1329
 
 
 
 
 
 
1330
  '@types/react@19.2.13':
1331
  resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==}
1332
 
 
 
 
 
 
 
1333
  '@types/unist@2.0.11':
1334
  resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
1335
 
@@ -1345,6 +1467,9 @@ packages:
1345
  '@types/web-bluetooth@0.0.21':
1346
  resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
1347
 
 
 
 
1348
  '@types/ws@8.18.1':
1349
  resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
1350
 
@@ -1518,6 +1643,14 @@ packages:
1518
  cpu: [x64]
1519
  os: [win32]
1520
 
 
 
 
 
 
 
 
 
1521
  '@vercel/oidc@3.1.0':
1522
  resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
1523
  engines: {node: '>= 20'}
@@ -1653,6 +1786,9 @@ packages:
1653
  peerDependencies:
1654
  vue: ^3.5.0
1655
 
 
 
 
1656
  '@xyflow/react@12.10.0':
1657
  resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==}
1658
  peerDependencies:
@@ -1662,6 +1798,9 @@ packages:
1662
  '@xyflow/system@0.0.74':
1663
  resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==}
1664
 
 
 
 
1665
  acorn-jsx@5.3.2:
1666
  resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
1667
  peerDependencies:
@@ -1823,6 +1962,9 @@ packages:
1823
  resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==}
1824
  engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
1825
 
 
 
 
1826
  binary-extensions@2.3.0:
1827
  resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
1828
  engines: {node: '>=8'}
@@ -1851,6 +1993,9 @@ packages:
1851
  buffer@5.7.1:
1852
  resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
1853
 
 
 
 
1854
  cac@6.7.14:
1855
  resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
1856
  engines: {node: '>=8'}
@@ -1875,6 +2020,12 @@ packages:
1875
  resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
1876
  engines: {node: '>= 6'}
1877
 
 
 
 
 
 
 
1878
  caniuse-lite@1.0.30001769:
1879
  resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==}
1880
 
@@ -1916,9 +2067,15 @@ packages:
1916
  chownr@1.1.4:
1917
  resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
1918
 
 
 
 
1919
  classcat@5.0.5:
1920
  resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
1921
 
 
 
 
1922
  client-only@0.0.1:
1923
  resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
1924
 
@@ -1956,6 +2113,11 @@ packages:
1956
  crelt@1.0.6:
1957
  resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
1958
 
 
 
 
 
 
1959
  cross-spawn@7.0.6:
1960
  resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
1961
  engines: {node: '>= 8'}
@@ -1995,6 +2157,9 @@ packages:
1995
  resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
1996
  engines: {node: '>=12'}
1997
 
 
 
 
1998
  d3-color@3.1.0:
1999
  resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
2000
  engines: {node: '>=12'}
@@ -2011,18 +2176,33 @@ packages:
2011
  resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
2012
  engines: {node: '>=12'}
2013
 
 
 
 
 
2014
  d3-format@3.1.2:
2015
  resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
2016
  engines: {node: '>=12'}
2017
 
 
 
 
 
2018
  d3-interpolate@3.0.1:
2019
  resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
2020
  engines: {node: '>=12'}
2021
 
 
 
 
2022
  d3-path@3.1.0:
2023
  resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
2024
  engines: {node: '>=12'}
2025
 
 
 
 
 
2026
  d3-scale@4.0.2:
2027
  resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
2028
  engines: {node: '>=12'}
@@ -2135,6 +2315,9 @@ packages:
2135
  resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
2136
  engines: {node: '>=6'}
2137
 
 
 
 
2138
  detect-libc@2.1.2:
2139
  resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
2140
  engines: {node: '>=8'}
@@ -2158,6 +2341,9 @@ packages:
2158
  dom-accessibility-api@0.6.3:
2159
  resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
2160
 
 
 
 
2161
  dunder-proto@1.0.1:
2162
  resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
2163
  engines: {node: '>= 0.4'}
@@ -2165,6 +2351,9 @@ packages:
2165
  electron-to-chromium@1.5.286:
2166
  resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
2167
 
 
 
 
2168
  emoji-regex@9.2.2:
2169
  resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
2170
 
@@ -2362,6 +2551,10 @@ packages:
2362
  eventemitter3@5.0.4:
2363
  resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
2364
 
 
 
 
 
2365
  eventsource-parser@3.0.6:
2366
  resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
2367
  engines: {node: '>=18.0.0'}
@@ -2415,6 +2608,12 @@ packages:
2415
  picomatch:
2416
  optional: true
2417
 
 
 
 
 
 
 
2418
  file-entry-cache@8.0.0:
2419
  resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
2420
  engines: {node: '>=16.0.0'}
@@ -2534,10 +2733,56 @@ packages:
2534
  globrex@0.1.2:
2535
  resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
2536
 
 
 
 
2537
  gopd@1.2.0:
2538
  resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
2539
  engines: {node: '>= 0.4'}
2540
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2541
  guess-json-indent@3.0.1:
2542
  resolution: {integrity: sha512-LWZ3Vr8BG7DHE3TzPYFqkhjNRw4vYgFSsv2nfMuHklAlOfiy54/EwiDQuQfFVLxENCVv20wpbjfTayooQHrEhQ==}
2543
  engines: {node: '>=18.18.0'}
@@ -2639,6 +2884,12 @@ packages:
2639
  highlightjs-curl@1.3.0:
2640
  resolution: {integrity: sha512-50UEfZq1KR0Lfk2Tr6xb/MUIZH3h10oNC0OTy9g7WELcs5Fgy/mKN1vEhuKTkKbdo8vr5F9GXstu2eLhApfQ3A==}
2641
 
 
 
 
 
 
 
2642
  hookable@6.0.1:
2643
  resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==}
2644
 
@@ -2682,6 +2933,9 @@ packages:
2682
  resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
2683
  engines: {node: '>= 4'}
2684
 
 
 
 
2685
  immer@10.2.0:
2686
  resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
2687
 
@@ -2819,6 +3073,9 @@ packages:
2819
  is-potential-custom-element-name@1.0.1:
2820
  resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
2821
 
 
 
 
2822
  is-regex@1.2.1:
2823
  resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
2824
  engines: {node: '>= 0.4'}
@@ -2869,6 +3126,11 @@ packages:
2869
  resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
2870
  engines: {node: '>= 0.4'}
2871
 
 
 
 
 
 
2872
  jiti@1.21.7:
2873
  resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
2874
  hasBin: true
@@ -2954,6 +3216,9 @@ packages:
2954
  resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
2955
  engines: {node: '>= 0.8.0'}
2956
 
 
 
 
2957
  lilconfig@3.1.3:
2958
  resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
2959
  engines: {node: '>=14'}
@@ -2991,6 +3256,12 @@ packages:
2991
  resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
2992
  hasBin: true
2993
 
 
 
 
 
 
 
2994
  magic-string@0.30.21:
2995
  resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
2996
 
@@ -3054,6 +3325,14 @@ packages:
3054
  resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
3055
  engines: {node: '>= 8'}
3056
 
 
 
 
 
 
 
 
 
3057
  microdiff@1.5.0:
3058
  resolution: {integrity: sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q==}
3059
 
@@ -3166,6 +3445,9 @@ packages:
3166
  mkdirp-classic@0.5.3:
3167
  resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
3168
 
 
 
 
3169
  ms@2.1.3:
3170
  resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
3171
 
@@ -3275,6 +3557,9 @@ packages:
3275
  resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
3276
  engines: {node: '>= 0.4'}
3277
 
 
 
 
3278
  on-exit-leak-free@2.1.2:
3279
  resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
3280
  engines: {node: '>=14.0.0'}
@@ -3306,6 +3591,9 @@ packages:
3306
  resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
3307
  engines: {node: '>=14.16'}
3308
 
 
 
 
3309
  parent-module@1.0.1:
3310
  resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
3311
  engines: {node: '>=6'}
@@ -3439,6 +3727,9 @@ packages:
3439
  resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
3440
  engines: {node: ^10 || ^12 || >=14}
3441
 
 
 
 
3442
  prebuild-install@7.1.3:
3443
  resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
3444
  engines: {node: '>=10'}
@@ -3463,6 +3754,9 @@ packages:
3463
  process-warning@5.0.0:
3464
  resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
3465
 
 
 
 
3466
  prop-types@15.8.1:
3467
  resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
3468
 
@@ -3524,6 +3818,15 @@ packages:
3524
  resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
3525
  engines: {node: '>=0.10.0'}
3526
 
 
 
 
 
 
 
 
 
 
3527
  react@19.2.4:
3528
  resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
3529
  engines: {node: '>=0.10.0'}
@@ -3545,6 +3848,13 @@ packages:
3545
  resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
3546
  engines: {node: '>=8.10.0'}
3547
 
 
 
 
 
 
 
 
3548
  real-require@0.2.0:
3549
  resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
3550
  engines: {node: '>= 12.13.0'}
@@ -3763,6 +4073,15 @@ packages:
3763
  stackback@0.0.2:
3764
  resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
3765
 
 
 
 
 
 
 
 
 
 
3766
  std-env@3.10.0:
3767
  resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
3768
 
@@ -3870,6 +4189,11 @@ packages:
3870
  resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
3871
  engines: {node: '>= 0.4'}
3872
 
 
 
 
 
 
3873
  swrv@1.1.0:
3874
  resolution: {integrity: sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==}
3875
  peerDependencies:
@@ -3911,6 +4235,19 @@ packages:
3911
  resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
3912
  engines: {node: '>=20'}
3913
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3914
  time-span@5.1.0:
3915
  resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==}
3916
  engines: {node: '>=12'}
@@ -3962,6 +4299,19 @@ packages:
3962
  trim-lines@3.0.1:
3963
  resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
3964
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3965
  trough@2.2.0:
3966
  resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
3967
 
@@ -4001,6 +4351,9 @@ packages:
4001
  tunnel-agent@0.6.0:
4002
  resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
4003
 
 
 
 
4004
  type-check@0.4.0:
4005
  resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
4006
  engines: {node: '>= 0.8.0'}
@@ -4092,6 +4445,10 @@ packages:
4092
  util-deprecate@1.0.2:
4093
  resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
4094
 
 
 
 
 
4095
  vfile-location@5.0.3:
4096
  resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
4097
 
@@ -4216,6 +4573,12 @@ packages:
4216
  web-worker@1.5.0:
4217
  resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==}
4218
 
 
 
 
 
 
 
4219
  webidl-conversions@7.0.0:
4220
  resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
4221
  engines: {node: '>=12'}
@@ -4339,6 +4702,24 @@ packages:
4339
  use-sync-external-store:
4340
  optional: true
4341
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4342
  zwitch@2.0.4:
4343
  resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
4344
 
@@ -4611,6 +4992,8 @@ snapshots:
4611
 
4612
  '@csstools/css-tokenizer@3.0.4': {}
4613
 
 
 
4614
  '@emnapi/core@1.8.1':
4615
  dependencies:
4616
  '@emnapi/wasi-threads': 1.1.0
@@ -4954,6 +5337,13 @@ snapshots:
4954
 
4955
  '@marijn/find-cluster-break@1.0.2': {}
4956
 
 
 
 
 
 
 
 
4957
  '@napi-rs/wasm-runtime@0.2.12':
4958
  dependencies:
4959
  '@emnapi/core': 1.8.1
@@ -5015,6 +5405,105 @@ snapshots:
5015
  dependencies:
5016
  playwright: 1.58.2
5017
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5018
  '@reactflow/background@11.3.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
5019
  dependencies:
5020
  '@reactflow/core': 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -5612,6 +6101,8 @@ snapshots:
5612
  '@types/react': 19.2.13
5613
  '@types/react-dom': 19.2.3(@types/react@19.2.13)
5614
 
 
 
5615
  '@tybys/wasm-util@0.10.1':
5616
  dependencies:
5617
  tslib: 2.8.1
@@ -5765,6 +6256,8 @@ snapshots:
5765
  dependencies:
5766
  '@types/ms': 2.1.0
5767
 
 
 
5768
  '@types/estree-jsx@1.0.5':
5769
  dependencies:
5770
  '@types/estree': 1.0.8
@@ -5793,14 +6286,32 @@ snapshots:
5793
  dependencies:
5794
  undici-types: 6.21.0
5795
 
 
 
5796
  '@types/react-dom@19.2.3(@types/react@19.2.13)':
5797
  dependencies:
5798
  '@types/react': 19.2.13
5799
 
 
 
 
 
5800
  '@types/react@19.2.13':
5801
  dependencies:
5802
  csstype: 3.2.3
5803
 
 
 
 
 
 
 
 
 
 
 
 
 
5804
  '@types/unist@2.0.11': {}
5805
 
5806
  '@types/unist@3.0.3': {}
@@ -5811,6 +6322,8 @@ snapshots:
5811
 
5812
  '@types/web-bluetooth@0.0.21': {}
5813
 
 
 
5814
  '@types/ws@8.18.1':
5815
  dependencies:
5816
  '@types/node': 22.19.9
@@ -5973,6 +6486,13 @@ snapshots:
5973
  '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
5974
  optional: true
5975
 
 
 
 
 
 
 
 
5976
  '@vercel/oidc@3.1.0': {}
5977
 
5978
  '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.9))':
@@ -6124,6 +6644,8 @@ snapshots:
6124
  dependencies:
6125
  vue: 3.5.29(typescript@5.9.3)
6126
 
 
 
6127
  '@xyflow/react@12.10.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
6128
  dependencies:
6129
  '@xyflow/system': 0.0.74
@@ -6147,6 +6669,8 @@ snapshots:
6147
  d3-selection: 3.0.0
6148
  d3-zoom: 3.0.0
6149
 
 
 
6150
  acorn-jsx@5.3.2(acorn@8.15.0):
6151
  dependencies:
6152
  acorn: 8.15.0
@@ -6319,6 +6843,10 @@ snapshots:
6319
  bindings: 1.5.0
6320
  prebuild-install: 7.1.3
6321
 
 
 
 
 
6322
  binary-extensions@2.3.0: {}
6323
 
6324
  bindings@1.5.0:
@@ -6357,6 +6885,11 @@ snapshots:
6357
  base64-js: 1.5.1
6358
  ieee754: 1.2.1
6359
 
 
 
 
 
 
6360
  cac@6.7.14: {}
6361
 
6362
  call-bind-apply-helpers@1.0.2:
@@ -6380,6 +6913,10 @@ snapshots:
6380
 
6381
  camelcase-css@2.0.1: {}
6382
 
 
 
 
 
6383
  caniuse-lite@1.0.30001769: {}
6384
 
6385
  ccount@2.0.1: {}
@@ -6423,8 +6960,14 @@ snapshots:
6423
 
6424
  chownr@1.1.4: {}
6425
 
 
 
 
 
6426
  classcat@5.0.5: {}
6427
 
 
 
6428
  client-only@0.0.1: {}
6429
 
6430
  clsx@2.1.1: {}
@@ -6449,6 +6992,10 @@ snapshots:
6449
 
6450
  crelt@1.0.6: {}
6451
 
 
 
 
 
6452
  cross-spawn@7.0.6:
6453
  dependencies:
6454
  path-key: 3.1.1
@@ -6482,6 +7029,8 @@ snapshots:
6482
  dependencies:
6483
  internmap: 2.0.3
6484
 
 
 
6485
  d3-color@3.1.0: {}
6486
 
6487
  d3-dispatch@3.0.1: {}
@@ -6493,14 +7042,28 @@ snapshots:
6493
 
6494
  d3-ease@3.0.1: {}
6495
 
 
 
 
 
 
 
 
 
6496
  d3-format@3.1.2: {}
6497
 
 
 
6498
  d3-interpolate@3.0.1:
6499
  dependencies:
6500
  d3-color: 3.1.0
6501
 
 
 
6502
  d3-path@3.1.0: {}
6503
 
 
 
6504
  d3-scale@4.0.2:
6505
  dependencies:
6506
  d3-array: 3.2.4
@@ -6611,6 +7174,10 @@ snapshots:
6611
 
6612
  dequal@2.0.3: {}
6613
 
 
 
 
 
6614
  detect-libc@2.1.2: {}
6615
 
6616
  devlop@1.1.0:
@@ -6629,6 +7196,8 @@ snapshots:
6629
 
6630
  dom-accessibility-api@0.6.3: {}
6631
 
 
 
6632
  dunder-proto@1.0.1:
6633
  dependencies:
6634
  call-bind-apply-helpers: 1.0.2
@@ -6637,6 +7206,8 @@ snapshots:
6637
 
6638
  electron-to-chromium@1.5.286: {}
6639
 
 
 
6640
  emoji-regex@9.2.2: {}
6641
 
6642
  end-of-stream@1.4.5:
@@ -6997,6 +7568,8 @@ snapshots:
6997
 
6998
  eventemitter3@5.0.4: {}
6999
 
 
 
7000
  eventsource-parser@3.0.6: {}
7001
 
7002
  expand-template@2.0.3: {}
@@ -7041,6 +7614,10 @@ snapshots:
7041
  optionalDependencies:
7042
  picomatch: 4.0.3
7043
 
 
 
 
 
7044
  file-entry-cache@8.0.0:
7045
  dependencies:
7046
  flat-cache: 4.0.1
@@ -7155,8 +7732,60 @@ snapshots:
7155
 
7156
  globrex@0.1.2: {}
7157
 
 
 
7158
  gopd@1.2.0: {}
7159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7160
  guess-json-indent@3.0.1: {}
7161
 
7162
  has-bigints@1.1.0: {}
@@ -7345,6 +7974,10 @@ snapshots:
7345
 
7346
  highlightjs-curl@1.3.0: {}
7347
 
 
 
 
 
7348
  hookable@6.0.1: {}
7349
 
7350
  html-encoding-sniffer@4.0.0:
@@ -7385,6 +8018,8 @@ snapshots:
7385
 
7386
  ignore@7.0.5: {}
7387
 
 
 
7388
  immer@10.2.0: {}
7389
 
7390
  immer@11.1.3: {}
@@ -7513,6 +8148,8 @@ snapshots:
7513
 
7514
  is-potential-custom-element-name@1.0.1: {}
7515
 
 
 
7516
  is-regex@1.2.1:
7517
  dependencies:
7518
  call-bound: 1.0.4
@@ -7567,6 +8204,13 @@ snapshots:
7567
  has-symbols: 1.1.0
7568
  set-function-name: 2.0.2
7569
 
 
 
 
 
 
 
 
7570
  jiti@1.21.7: {}
7571
 
7572
  joycon@3.1.1: {}
@@ -7652,6 +8296,10 @@ snapshots:
7652
  prelude-ls: 1.2.1
7653
  type-check: 0.4.0
7654
 
 
 
 
 
7655
  lilconfig@3.1.3: {}
7656
 
7657
  lines-and-columns@1.2.4: {}
@@ -7684,6 +8332,11 @@ snapshots:
7684
 
7685
  lz-string@1.5.0: {}
7686
 
 
 
 
 
 
7687
  magic-string@0.30.21:
7688
  dependencies:
7689
  '@jridgewell/sourcemap-codec': 1.5.5
@@ -7853,6 +8506,12 @@ snapshots:
7853
 
7854
  merge2@1.4.1: {}
7855
 
 
 
 
 
 
 
7856
  microdiff@1.5.0: {}
7857
 
7858
  micromark-core-commonmark@2.0.3:
@@ -8067,6 +8726,10 @@ snapshots:
8067
 
8068
  mkdirp-classic@0.5.3: {}
8069
 
 
 
 
 
8070
  ms@2.1.3: {}
8071
 
8072
  mz@2.7.0:
@@ -8174,6 +8837,8 @@ snapshots:
8174
  define-properties: 1.2.1
8175
  es-object-atoms: 1.1.1
8176
 
 
 
8177
  on-exit-leak-free@2.1.2: {}
8178
 
8179
  once@1.4.0:
@@ -8209,6 +8874,10 @@ snapshots:
8209
 
8210
  p-timeout@6.1.4: {}
8211
 
 
 
 
 
8212
  parent-module@1.0.1:
8213
  dependencies:
8214
  callsites: 3.1.0
@@ -8341,6 +9010,8 @@ snapshots:
8341
  picocolors: 1.1.1
8342
  source-map-js: 1.2.1
8343
 
 
 
8344
  prebuild-install@7.1.3:
8345
  dependencies:
8346
  detect-libc: 2.1.2
@@ -8372,6 +9043,11 @@ snapshots:
8372
 
8373
  process-warning@5.0.0: {}
8374
 
 
 
 
 
 
8375
  prop-types@15.8.1:
8376
  dependencies:
8377
  loose-envify: 1.4.0
@@ -8453,6 +9129,12 @@ snapshots:
8453
 
8454
  react-refresh@0.17.0: {}
8455
 
 
 
 
 
 
 
8456
  react@19.2.4: {}
8457
 
8458
  reactflow@11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
@@ -8483,6 +9165,43 @@ snapshots:
8483
  dependencies:
8484
  picomatch: 2.3.1
8485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8486
  real-require@0.2.0: {}
8487
 
8488
  recharts@3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1):
@@ -8819,6 +9538,13 @@ snapshots:
8819
 
8820
  stackback@0.0.2: {}
8821
 
 
 
 
 
 
 
 
8822
  std-env@3.10.0: {}
8823
 
8824
  stop-iteration-iterator@1.1.0:
@@ -8947,6 +9673,10 @@ snapshots:
8947
 
8948
  supports-preserve-symlinks-flag@1.0.0: {}
8949
 
 
 
 
 
8950
  swrv@1.1.0(vue@3.5.29(typescript@5.9.3)):
8951
  dependencies:
8952
  vue: 3.5.29(typescript@5.9.3)
@@ -9014,6 +9744,22 @@ snapshots:
9014
  dependencies:
9015
  real-require: 0.2.0
9016
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9017
  time-span@5.1.0:
9018
  dependencies:
9019
  convert-hrtime: 5.0.0
@@ -9055,6 +9801,20 @@ snapshots:
9055
 
9056
  trim-lines@3.0.1: {}
9057
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9058
  trough@2.2.0: {}
9059
 
9060
  truncate-json@3.0.1:
@@ -9088,6 +9848,14 @@ snapshots:
9088
  dependencies:
9089
  safe-buffer: 5.2.1
9090
 
 
 
 
 
 
 
 
 
9091
  type-check@0.4.0:
9092
  dependencies:
9093
  prelude-ls: 1.2.1
@@ -9235,6 +10003,8 @@ snapshots:
9235
 
9236
  util-deprecate@1.0.2: {}
9237
 
 
 
9238
  vfile-location@5.0.3:
9239
  dependencies:
9240
  '@types/unist': 3.0.3
@@ -9374,6 +10144,10 @@ snapshots:
9374
 
9375
  web-worker@1.5.0: {}
9376
 
 
 
 
 
9377
  webidl-conversions@7.0.0: {}
9378
 
9379
  whatwg-encoding@3.1.1:
@@ -9474,4 +10248,11 @@ snapshots:
9474
  react: 19.2.4
9475
  use-sync-external-store: 1.6.0(react@19.2.4)
9476
 
 
 
 
 
 
 
 
9477
  zwitch@2.0.4: {}
 
8
 
9
  .:
10
  dependencies:
11
+ '@radix-ui/react-slot':
12
+ specifier: ^1.2.4
13
+ version: 1.2.4(@types/react@19.2.13)(react@19.2.4)
14
  '@scalar/api-reference-react':
15
  specifier: ^0.8.66
16
  version: 0.8.66(react@19.2.4)(tailwindcss@3.4.19(yaml@2.8.2))(typescript@5.9.3)
 
23
  better-sqlite3:
24
  specifier: ^12.6.2
25
  version: 12.6.2
26
+ class-variance-authority:
27
+ specifier: ^0.7.1
28
+ version: 0.7.1
29
  clsx:
30
  specifier: ^2.1.1
31
  version: 2.1.1
 
59
  reactflow:
60
  specifier: ^11.11.4
61
  version: 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
62
+ reagraph:
63
+ specifier: ^4.30.8
64
+ version: 4.30.8(@types/react@19.2.13)(@types/three@0.183.1)(graphology-types@0.24.8)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
65
  recharts:
66
  specifier: ^3.7.0
67
  version: 3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1)
 
315
  resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
316
  engines: {node: '>=18'}
317
 
318
+ '@dimforge/rapier3d-compat@0.12.0':
319
+ resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
320
+
321
  '@emnapi/core@1.8.1':
322
  resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
323
 
 
748
  '@marijn/find-cluster-break@1.0.2':
749
  resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
750
 
751
+ '@mediapipe/tasks-vision@0.10.17':
752
+ resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==}
753
+
754
+ '@monogrid/gainmap-js@3.4.0':
755
+ resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==}
756
+ peerDependencies:
757
+ three: '>= 0.159.0'
758
+
759
  '@napi-rs/wasm-runtime@0.2.12':
760
  resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
761
 
 
848
  engines: {node: '>=18'}
849
  hasBin: true
850
 
851
+ '@radix-ui/react-compose-refs@1.1.2':
852
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
853
+ peerDependencies:
854
+ '@types/react': '*'
855
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
856
+ peerDependenciesMeta:
857
+ '@types/react':
858
+ optional: true
859
+
860
+ '@radix-ui/react-slot@1.2.4':
861
+ resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
862
+ peerDependencies:
863
+ '@types/react': '*'
864
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
865
+ peerDependenciesMeta:
866
+ '@types/react':
867
+ optional: true
868
+
869
+ '@react-spring/animated@10.0.3':
870
+ resolution: {integrity: sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==}
871
+ peerDependencies:
872
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
873
+
874
+ '@react-spring/core@10.0.3':
875
+ resolution: {integrity: sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==}
876
+ peerDependencies:
877
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
878
+
879
+ '@react-spring/rafz@10.0.3':
880
+ resolution: {integrity: sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==}
881
+
882
+ '@react-spring/shared@10.0.3':
883
+ resolution: {integrity: sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==}
884
+ peerDependencies:
885
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
886
+
887
+ '@react-spring/three@10.0.3':
888
+ resolution: {integrity: sha512-hZP7ChF/EwnWn+H2xuzAsRRfQdhquoBTI1HKgO6X9V8tcVCuR69qJmsA9N00CA4Nzx0bo/zwBtqONmi55Ffm5w==}
889
+ peerDependencies:
890
+ '@react-three/fiber': '>=6.0'
891
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
892
+ three: '>=0.126'
893
+
894
+ '@react-spring/types@10.0.3':
895
+ resolution: {integrity: sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==}
896
+
897
+ '@react-three/drei@10.7.7':
898
+ resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
899
+ peerDependencies:
900
+ '@react-three/fiber': ^9.0.0
901
+ react: ^19
902
+ react-dom: ^19
903
+ three: '>=0.159'
904
+ peerDependenciesMeta:
905
+ react-dom:
906
+ optional: true
907
+
908
+ '@react-three/fiber@9.5.0':
909
+ resolution: {integrity: sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==}
910
+ peerDependencies:
911
+ expo: '>=43.0'
912
+ expo-asset: '>=8.4'
913
+ expo-file-system: '>=11.0'
914
+ expo-gl: '>=11.0'
915
+ react: '>=19 <19.3'
916
+ react-dom: '>=19 <19.3'
917
+ react-native: '>=0.78'
918
+ three: '>=0.156'
919
+ peerDependenciesMeta:
920
+ expo:
921
+ optional: true
922
+ expo-asset:
923
+ optional: true
924
+ expo-file-system:
925
+ optional: true
926
+ expo-gl:
927
+ optional: true
928
+ react-dom:
929
+ optional: true
930
+ react-native:
931
+ optional: true
932
+
933
  '@reactflow/background@11.3.14':
934
  resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
935
  peerDependencies:
 
1277
  '@types/react-dom':
1278
  optional: true
1279
 
1280
+ '@tweenjs/tween.js@23.1.3':
1281
+ resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
1282
+
1283
  '@tybys/wasm-util@0.10.1':
1284
  resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
1285
 
 
1397
  '@types/debug@4.1.12':
1398
  resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
1399
 
1400
+ '@types/draco3d@1.4.10':
1401
+ resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==}
1402
+
1403
  '@types/estree-jsx@1.0.5':
1404
  resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
1405
 
 
1430
  '@types/node@22.19.9':
1431
  resolution: {integrity: sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==}
1432
 
1433
+ '@types/offscreencanvas@2019.7.3':
1434
+ resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
1435
+
1436
  '@types/react-dom@19.2.3':
1437
  resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
1438
  peerDependencies:
1439
  '@types/react': ^19.2.0
1440
 
1441
+ '@types/react-reconciler@0.28.9':
1442
+ resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==}
1443
+ peerDependencies:
1444
+ '@types/react': '*'
1445
+
1446
  '@types/react@19.2.13':
1447
  resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==}
1448
 
1449
+ '@types/stats.js@0.17.4':
1450
+ resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
1451
+
1452
+ '@types/three@0.183.1':
1453
+ resolution: {integrity: sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==}
1454
+
1455
  '@types/unist@2.0.11':
1456
  resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
1457
 
 
1467
  '@types/web-bluetooth@0.0.21':
1468
  resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
1469
 
1470
+ '@types/webxr@0.5.24':
1471
+ resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
1472
+
1473
  '@types/ws@8.18.1':
1474
  resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
1475
 
 
1643
  cpu: [x64]
1644
  os: [win32]
1645
 
1646
+ '@use-gesture/core@10.3.1':
1647
+ resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==}
1648
+
1649
+ '@use-gesture/react@10.3.1':
1650
+ resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==}
1651
+ peerDependencies:
1652
+ react: '>= 16.8.0'
1653
+
1654
  '@vercel/oidc@3.1.0':
1655
  resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
1656
  engines: {node: '>= 20'}
 
1786
  peerDependencies:
1787
  vue: ^3.5.0
1788
 
1789
+ '@webgpu/types@0.1.69':
1790
+ resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==}
1791
+
1792
  '@xyflow/react@12.10.0':
1793
  resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==}
1794
  peerDependencies:
 
1798
  '@xyflow/system@0.0.74':
1799
  resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==}
1800
 
1801
+ '@yomguithereal/helpers@1.1.1':
1802
+ resolution: {integrity: sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==}
1803
+
1804
  acorn-jsx@5.3.2:
1805
  resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
1806
  peerDependencies:
 
1962
  resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==}
1963
  engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
1964
 
1965
+ bidi-js@1.0.3:
1966
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
1967
+
1968
  binary-extensions@2.3.0:
1969
  resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
1970
  engines: {node: '>=8'}
 
1993
  buffer@5.7.1:
1994
  resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
1995
 
1996
+ buffer@6.0.3:
1997
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
1998
+
1999
  cac@6.7.14:
2000
  resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
2001
  engines: {node: '>=8'}
 
2020
  resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
2021
  engines: {node: '>= 6'}
2022
 
2023
+ camera-controls@3.1.2:
2024
+ resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==}
2025
+ engines: {node: '>=22.0.0', npm: '>=10.5.1'}
2026
+ peerDependencies:
2027
+ three: '>=0.126.1'
2028
+
2029
  caniuse-lite@1.0.30001769:
2030
  resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==}
2031
 
 
2067
  chownr@1.1.4:
2068
  resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
2069
 
2070
+ class-variance-authority@0.7.1:
2071
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
2072
+
2073
  classcat@5.0.5:
2074
  resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
2075
 
2076
+ classnames@2.5.1:
2077
+ resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
2078
+
2079
  client-only@0.0.1:
2080
  resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
2081
 
 
2113
  crelt@1.0.6:
2114
  resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
2115
 
2116
+ cross-env@7.0.3:
2117
+ resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
2118
+ engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
2119
+ hasBin: true
2120
+
2121
  cross-spawn@7.0.6:
2122
  resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
2123
  engines: {node: '>= 8'}
 
2157
  resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
2158
  engines: {node: '>=12'}
2159
 
2160
+ d3-binarytree@1.0.2:
2161
+ resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==}
2162
+
2163
  d3-color@3.1.0:
2164
  resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
2165
  engines: {node: '>=12'}
 
2176
  resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
2177
  engines: {node: '>=12'}
2178
 
2179
+ d3-force-3d@3.0.6:
2180
+ resolution: {integrity: sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==}
2181
+ engines: {node: '>=12'}
2182
+
2183
  d3-format@3.1.2:
2184
  resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
2185
  engines: {node: '>=12'}
2186
 
2187
+ d3-hierarchy@3.1.2:
2188
+ resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
2189
+ engines: {node: '>=12'}
2190
+
2191
  d3-interpolate@3.0.1:
2192
  resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
2193
  engines: {node: '>=12'}
2194
 
2195
+ d3-octree@1.1.0:
2196
+ resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==}
2197
+
2198
  d3-path@3.1.0:
2199
  resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
2200
  engines: {node: '>=12'}
2201
 
2202
+ d3-quadtree@3.0.1:
2203
+ resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
2204
+ engines: {node: '>=12'}
2205
+
2206
  d3-scale@4.0.2:
2207
  resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
2208
  engines: {node: '>=12'}
 
2315
  resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
2316
  engines: {node: '>=6'}
2317
 
2318
+ detect-gpu@5.0.70:
2319
+ resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==}
2320
+
2321
  detect-libc@2.1.2:
2322
  resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
2323
  engines: {node: '>=8'}
 
2341
  dom-accessibility-api@0.6.3:
2342
  resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
2343
 
2344
+ draco3d@1.5.7:
2345
+ resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==}
2346
+
2347
  dunder-proto@1.0.1:
2348
  resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
2349
  engines: {node: '>= 0.4'}
 
2351
  electron-to-chromium@1.5.286:
2352
  resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
2353
 
2354
+ ellipsize@0.6.2:
2355
+ resolution: {integrity: sha512-zB4m5iEETalVrrP8RzcF0Qzqyw3MkUQ4R43NiczRAp0Hpp0+0bRdwKnoaFXyJoVJCipm2/3xc7Hkg0OOAorUPw==}
2356
+
2357
  emoji-regex@9.2.2:
2358
  resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
2359
 
 
2551
  eventemitter3@5.0.4:
2552
  resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
2553
 
2554
+ events@3.3.0:
2555
+ resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
2556
+ engines: {node: '>=0.8.x'}
2557
+
2558
  eventsource-parser@3.0.6:
2559
  resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
2560
  engines: {node: '>=18.0.0'}
 
2608
  picomatch:
2609
  optional: true
2610
 
2611
+ fflate@0.6.10:
2612
+ resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==}
2613
+
2614
+ fflate@0.8.2:
2615
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
2616
+
2617
  file-entry-cache@8.0.0:
2618
  resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
2619
  engines: {node: '>=16.0.0'}
 
2733
  globrex@0.1.2:
2734
  resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
2735
 
2736
+ glsl-noise@0.0.0:
2737
+ resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==}
2738
+
2739
  gopd@1.2.0:
2740
  resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
2741
  engines: {node: '>= 0.4'}
2742
 
2743
+ graphology-indices@0.17.0:
2744
+ resolution: {integrity: sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==}
2745
+ peerDependencies:
2746
+ graphology-types: '>=0.20.0'
2747
+
2748
+ graphology-layout-forceatlas2@0.10.1:
2749
+ resolution: {integrity: sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==}
2750
+ peerDependencies:
2751
+ graphology-types: '>=0.19.0'
2752
+
2753
+ graphology-layout-noverlap@0.4.2:
2754
+ resolution: {integrity: sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA==}
2755
+ peerDependencies:
2756
+ graphology-types: '>=0.19.0'
2757
+
2758
+ graphology-layout@0.6.1:
2759
+ resolution: {integrity: sha512-m9aMvbd0uDPffUCFPng5ibRkb2pmfNvdKjQWeZrf71RS1aOoat5874+DcyNfMeCT4aQguKC7Lj9eCbqZj/h8Ag==}
2760
+ peerDependencies:
2761
+ graphology-types: '>=0.19.0'
2762
+
2763
+ graphology-metrics@2.4.0:
2764
+ resolution: {integrity: sha512-7WOfOP+mFLCaTJx55Qg4eY+211vr1/b3D/R3biz3SXGhAaCVcWYkfabnmO4O4WBNWANEHtVnFrGgJ0kj6MM6xw==}
2765
+ peerDependencies:
2766
+ graphology-types: '>=0.20.0'
2767
+
2768
+ graphology-shortest-path@2.1.0:
2769
+ resolution: {integrity: sha512-KbT9CTkP/u72vGEJzyRr24xFC7usI9Es3LMmCPHGwQ1KTsoZjxwA9lMKxfU0syvT/w+7fZUdB/Hu2wWYcJBm6Q==}
2770
+ peerDependencies:
2771
+ graphology-types: '>=0.20.0'
2772
+
2773
+ graphology-types@0.24.8:
2774
+ resolution: {integrity: sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==}
2775
+
2776
+ graphology-utils@2.5.2:
2777
+ resolution: {integrity: sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==}
2778
+ peerDependencies:
2779
+ graphology-types: '>=0.23.0'
2780
+
2781
+ graphology@0.26.0:
2782
+ resolution: {integrity: sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==}
2783
+ peerDependencies:
2784
+ graphology-types: '>=0.24.0'
2785
+
2786
  guess-json-indent@3.0.1:
2787
  resolution: {integrity: sha512-LWZ3Vr8BG7DHE3TzPYFqkhjNRw4vYgFSsv2nfMuHklAlOfiy54/EwiDQuQfFVLxENCVv20wpbjfTayooQHrEhQ==}
2788
  engines: {node: '>=18.18.0'}
 
2884
  highlightjs-curl@1.3.0:
2885
  resolution: {integrity: sha512-50UEfZq1KR0Lfk2Tr6xb/MUIZH3h10oNC0OTy9g7WELcs5Fgy/mKN1vEhuKTkKbdo8vr5F9GXstu2eLhApfQ3A==}
2886
 
2887
+ hls.js@1.6.15:
2888
+ resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==}
2889
+
2890
+ hold-event@1.1.2:
2891
+ resolution: {integrity: sha512-Bx0A6OBY70cs23orUWk0DuBAAeJjEbmyg8Gnye9+M8+XeWy2CcmRyfiJhTnQQz9s25r9SYjici3URy176MFs5A==}
2892
+
2893
  hookable@6.0.1:
2894
  resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==}
2895
 
 
2933
  resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
2934
  engines: {node: '>= 4'}
2935
 
2936
+ immediate@3.0.6:
2937
+ resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
2938
+
2939
  immer@10.2.0:
2940
  resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
2941
 
 
3073
  is-potential-custom-element-name@1.0.1:
3074
  resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
3075
 
3076
+ is-promise@2.2.2:
3077
+ resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
3078
+
3079
  is-regex@1.2.1:
3080
  resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
3081
  engines: {node: '>= 0.4'}
 
3126
  resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
3127
  engines: {node: '>= 0.4'}
3128
 
3129
+ its-fine@2.0.0:
3130
+ resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==}
3131
+ peerDependencies:
3132
+ react: ^19.0.0
3133
+
3134
  jiti@1.21.7:
3135
  resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
3136
  hasBin: true
 
3216
  resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
3217
  engines: {node: '>= 0.8.0'}
3218
 
3219
+ lie@3.3.0:
3220
+ resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
3221
+
3222
  lilconfig@3.1.3:
3223
  resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
3224
  engines: {node: '>=14'}
 
3256
  resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
3257
  hasBin: true
3258
 
3259
+ maath@0.10.8:
3260
+ resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==}
3261
+ peerDependencies:
3262
+ '@types/three': '>=0.134.0'
3263
+ three: '>=0.134.0'
3264
+
3265
  magic-string@0.30.21:
3266
  resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
3267
 
 
3325
  resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
3326
  engines: {node: '>= 8'}
3327
 
3328
+ meshline@3.3.1:
3329
+ resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==}
3330
+ peerDependencies:
3331
+ three: '>=0.137'
3332
+
3333
+ meshoptimizer@1.0.1:
3334
+ resolution: {integrity: sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==}
3335
+
3336
  microdiff@1.5.0:
3337
  resolution: {integrity: sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q==}
3338
 
 
3445
  mkdirp-classic@0.5.3:
3446
  resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
3447
 
3448
+ mnemonist@0.39.8:
3449
+ resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==}
3450
+
3451
  ms@2.1.3:
3452
  resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
3453
 
 
3557
  resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
3558
  engines: {node: '>= 0.4'}
3559
 
3560
+ obliterator@2.0.5:
3561
+ resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==}
3562
+
3563
  on-exit-leak-free@2.1.2:
3564
  resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
3565
  engines: {node: '>=14.0.0'}
 
3591
  resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
3592
  engines: {node: '>=14.16'}
3593
 
3594
+ pandemonium@2.4.1:
3595
+ resolution: {integrity: sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==}
3596
+
3597
  parent-module@1.0.1:
3598
  resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
3599
  engines: {node: '>=6'}
 
3727
  resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
3728
  engines: {node: ^10 || ^12 || >=14}
3729
 
3730
+ potpack@1.0.2:
3731
+ resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
3732
+
3733
  prebuild-install@7.1.3:
3734
  resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
3735
  engines: {node: '>=10'}
 
3754
  process-warning@5.0.0:
3755
  resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
3756
 
3757
+ promise-worker-transferable@1.0.4:
3758
+ resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==}
3759
+
3760
  prop-types@15.8.1:
3761
  resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
3762
 
 
3818
  resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
3819
  engines: {node: '>=0.10.0'}
3820
 
3821
+ react-use-measure@2.1.7:
3822
+ resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
3823
+ peerDependencies:
3824
+ react: '>=16.13'
3825
+ react-dom: '>=16.13'
3826
+ peerDependenciesMeta:
3827
+ react-dom:
3828
+ optional: true
3829
+
3830
  react@19.2.4:
3831
  resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
3832
  engines: {node: '>=0.10.0'}
 
3848
  resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
3849
  engines: {node: '>=8.10.0'}
3850
 
3851
+ reagraph@4.30.8:
3852
+ resolution: {integrity: sha512-DXmG2lAM5k7LQiTQJ4sad8Ks8Q1c98303USwwN+mxpJ5GelBYLjMVG+DPct4CE8ezON1WUoE8PL3RUXiEua22Q==}
3853
+ engines: {node: ^20.19.0 || >=22.12.0}
3854
+ peerDependencies:
3855
+ react: '>=16'
3856
+ react-dom: '>=16'
3857
+
3858
  real-require@0.2.0:
3859
  resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
3860
  engines: {node: '>= 12.13.0'}
 
4073
  stackback@0.0.2:
4074
  resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
4075
 
4076
+ stats-gl@2.4.2:
4077
+ resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==}
4078
+ peerDependencies:
4079
+ '@types/three': '*'
4080
+ three: '*'
4081
+
4082
+ stats.js@0.17.0:
4083
+ resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==}
4084
+
4085
  std-env@3.10.0:
4086
  resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
4087
 
 
4189
  resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
4190
  engines: {node: '>= 0.4'}
4191
 
4192
+ suspend-react@0.1.3:
4193
+ resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==}
4194
+ peerDependencies:
4195
+ react: '>=17.0'
4196
+
4197
  swrv@1.1.0:
4198
  resolution: {integrity: sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==}
4199
  peerDependencies:
 
4235
  resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
4236
  engines: {node: '>=20'}
4237
 
4238
+ three-mesh-bvh@0.8.3:
4239
+ resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==}
4240
+ peerDependencies:
4241
+ three: '>= 0.159.0'
4242
+
4243
+ three-stdlib@2.36.1:
4244
+ resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==}
4245
+ peerDependencies:
4246
+ three: '>=0.128.0'
4247
+
4248
+ three@0.180.0:
4249
+ resolution: {integrity: sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==}
4250
+
4251
  time-span@5.1.0:
4252
  resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==}
4253
  engines: {node: '>=12'}
 
4299
  trim-lines@3.0.1:
4300
  resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
4301
 
4302
+ troika-three-text@0.52.4:
4303
+ resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==}
4304
+ peerDependencies:
4305
+ three: '>=0.125.0'
4306
+
4307
+ troika-three-utils@0.52.4:
4308
+ resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==}
4309
+ peerDependencies:
4310
+ three: '>=0.125.0'
4311
+
4312
+ troika-worker-utils@0.52.0:
4313
+ resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==}
4314
+
4315
  trough@2.2.0:
4316
  resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
4317
 
 
4351
  tunnel-agent@0.6.0:
4352
  resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
4353
 
4354
+ tunnel-rat@0.1.2:
4355
+ resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==}
4356
+
4357
  type-check@0.4.0:
4358
  resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
4359
  engines: {node: '>= 0.8.0'}
 
4445
  util-deprecate@1.0.2:
4446
  resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
4447
 
4448
+ utility-types@3.11.0:
4449
+ resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==}
4450
+ engines: {node: '>= 4'}
4451
+
4452
  vfile-location@5.0.3:
4453
  resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
4454
 
 
4573
  web-worker@1.5.0:
4574
  resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==}
4575
 
4576
+ webgl-constants@1.1.1:
4577
+ resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==}
4578
+
4579
+ webgl-sdf-generator@1.1.1:
4580
+ resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==}
4581
+
4582
  webidl-conversions@7.0.0:
4583
  resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
4584
  engines: {node: '>=12'}
 
4702
  use-sync-external-store:
4703
  optional: true
4704
 
4705
+ zustand@5.0.8:
4706
+ resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
4707
+ engines: {node: '>=12.20.0'}
4708
+ peerDependencies:
4709
+ '@types/react': '>=18.0.0'
4710
+ immer: '>=9.0.6'
4711
+ react: '>=18.0.0'
4712
+ use-sync-external-store: '>=1.2.0'
4713
+ peerDependenciesMeta:
4714
+ '@types/react':
4715
+ optional: true
4716
+ immer:
4717
+ optional: true
4718
+ react:
4719
+ optional: true
4720
+ use-sync-external-store:
4721
+ optional: true
4722
+
4723
  zwitch@2.0.4:
4724
  resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
4725
 
 
4992
 
4993
  '@csstools/css-tokenizer@3.0.4': {}
4994
 
4995
+ '@dimforge/rapier3d-compat@0.12.0': {}
4996
+
4997
  '@emnapi/core@1.8.1':
4998
  dependencies:
4999
  '@emnapi/wasi-threads': 1.1.0
 
5337
 
5338
  '@marijn/find-cluster-break@1.0.2': {}
5339
 
5340
+ '@mediapipe/tasks-vision@0.10.17': {}
5341
+
5342
+ '@monogrid/gainmap-js@3.4.0(three@0.180.0)':
5343
+ dependencies:
5344
+ promise-worker-transferable: 1.0.4
5345
+ three: 0.180.0
5346
+
5347
  '@napi-rs/wasm-runtime@0.2.12':
5348
  dependencies:
5349
  '@emnapi/core': 1.8.1
 
5405
  dependencies:
5406
  playwright: 1.58.2
5407
 
5408
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.13)(react@19.2.4)':
5409
+ dependencies:
5410
+ react: 19.2.4
5411
+ optionalDependencies:
5412
+ '@types/react': 19.2.13
5413
+
5414
+ '@radix-ui/react-slot@1.2.4(@types/react@19.2.13)(react@19.2.4)':
5415
+ dependencies:
5416
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4)
5417
+ react: 19.2.4
5418
+ optionalDependencies:
5419
+ '@types/react': 19.2.13
5420
+
5421
+ '@react-spring/animated@10.0.3(react@19.2.4)':
5422
+ dependencies:
5423
+ '@react-spring/shared': 10.0.3(react@19.2.4)
5424
+ '@react-spring/types': 10.0.3
5425
+ react: 19.2.4
5426
+
5427
+ '@react-spring/core@10.0.3(react@19.2.4)':
5428
+ dependencies:
5429
+ '@react-spring/animated': 10.0.3(react@19.2.4)
5430
+ '@react-spring/shared': 10.0.3(react@19.2.4)
5431
+ '@react-spring/types': 10.0.3
5432
+ react: 19.2.4
5433
+
5434
+ '@react-spring/rafz@10.0.3': {}
5435
+
5436
+ '@react-spring/shared@10.0.3(react@19.2.4)':
5437
+ dependencies:
5438
+ '@react-spring/rafz': 10.0.3
5439
+ '@react-spring/types': 10.0.3
5440
+ react: 19.2.4
5441
+
5442
+ '@react-spring/three@10.0.3(@react-three/fiber@9.5.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0))(react@19.2.4)(three@0.180.0)':
5443
+ dependencies:
5444
+ '@react-spring/animated': 10.0.3(react@19.2.4)
5445
+ '@react-spring/core': 10.0.3(react@19.2.4)
5446
+ '@react-spring/shared': 10.0.3(react@19.2.4)
5447
+ '@react-spring/types': 10.0.3
5448
+ '@react-three/fiber': 9.5.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0)
5449
+ react: 19.2.4
5450
+ three: 0.180.0
5451
+
5452
+ '@react-spring/types@10.0.3': {}
5453
+
5454
+ '@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0))(@types/react@19.2.13)(@types/three@0.183.1)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0)':
5455
+ dependencies:
5456
+ '@babel/runtime': 7.28.6
5457
+ '@mediapipe/tasks-vision': 0.10.17
5458
+ '@monogrid/gainmap-js': 3.4.0(three@0.180.0)
5459
+ '@react-three/fiber': 9.5.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0)
5460
+ '@use-gesture/react': 10.3.1(react@19.2.4)
5461
+ camera-controls: 3.1.2(three@0.180.0)
5462
+ cross-env: 7.0.3
5463
+ detect-gpu: 5.0.70
5464
+ glsl-noise: 0.0.0
5465
+ hls.js: 1.6.15
5466
+ maath: 0.10.8(@types/three@0.183.1)(three@0.180.0)
5467
+ meshline: 3.3.1(three@0.180.0)
5468
+ react: 19.2.4
5469
+ stats-gl: 2.4.2(@types/three@0.183.1)(three@0.180.0)
5470
+ stats.js: 0.17.0
5471
+ suspend-react: 0.1.3(react@19.2.4)
5472
+ three: 0.180.0
5473
+ three-mesh-bvh: 0.8.3(three@0.180.0)
5474
+ three-stdlib: 2.36.1(three@0.180.0)
5475
+ troika-three-text: 0.52.4(three@0.180.0)
5476
+ tunnel-rat: 0.1.2(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4)
5477
+ use-sync-external-store: 1.6.0(react@19.2.4)
5478
+ utility-types: 3.11.0
5479
+ zustand: 5.0.11(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
5480
+ optionalDependencies:
5481
+ react-dom: 19.2.4(react@19.2.4)
5482
+ transitivePeerDependencies:
5483
+ - '@types/react'
5484
+ - '@types/three'
5485
+ - immer
5486
+
5487
+ '@react-three/fiber@9.5.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0)':
5488
+ dependencies:
5489
+ '@babel/runtime': 7.28.6
5490
+ '@types/webxr': 0.5.24
5491
+ base64-js: 1.5.1
5492
+ buffer: 6.0.3
5493
+ its-fine: 2.0.0(@types/react@19.2.13)(react@19.2.4)
5494
+ react: 19.2.4
5495
+ react-use-measure: 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
5496
+ scheduler: 0.27.0
5497
+ suspend-react: 0.1.3(react@19.2.4)
5498
+ three: 0.180.0
5499
+ use-sync-external-store: 1.6.0(react@19.2.4)
5500
+ zustand: 5.0.11(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
5501
+ optionalDependencies:
5502
+ react-dom: 19.2.4(react@19.2.4)
5503
+ transitivePeerDependencies:
5504
+ - '@types/react'
5505
+ - immer
5506
+
5507
  '@reactflow/background@11.3.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
5508
  dependencies:
5509
  '@reactflow/core': 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
 
6101
  '@types/react': 19.2.13
6102
  '@types/react-dom': 19.2.3(@types/react@19.2.13)
6103
 
6104
+ '@tweenjs/tween.js@23.1.3': {}
6105
+
6106
  '@tybys/wasm-util@0.10.1':
6107
  dependencies:
6108
  tslib: 2.8.1
 
6256
  dependencies:
6257
  '@types/ms': 2.1.0
6258
 
6259
+ '@types/draco3d@1.4.10': {}
6260
+
6261
  '@types/estree-jsx@1.0.5':
6262
  dependencies:
6263
  '@types/estree': 1.0.8
 
6286
  dependencies:
6287
  undici-types: 6.21.0
6288
 
6289
+ '@types/offscreencanvas@2019.7.3': {}
6290
+
6291
  '@types/react-dom@19.2.3(@types/react@19.2.13)':
6292
  dependencies:
6293
  '@types/react': 19.2.13
6294
 
6295
+ '@types/react-reconciler@0.28.9(@types/react@19.2.13)':
6296
+ dependencies:
6297
+ '@types/react': 19.2.13
6298
+
6299
  '@types/react@19.2.13':
6300
  dependencies:
6301
  csstype: 3.2.3
6302
 
6303
+ '@types/stats.js@0.17.4': {}
6304
+
6305
+ '@types/three@0.183.1':
6306
+ dependencies:
6307
+ '@dimforge/rapier3d-compat': 0.12.0
6308
+ '@tweenjs/tween.js': 23.1.3
6309
+ '@types/stats.js': 0.17.4
6310
+ '@types/webxr': 0.5.24
6311
+ '@webgpu/types': 0.1.69
6312
+ fflate: 0.8.2
6313
+ meshoptimizer: 1.0.1
6314
+
6315
  '@types/unist@2.0.11': {}
6316
 
6317
  '@types/unist@3.0.3': {}
 
6322
 
6323
  '@types/web-bluetooth@0.0.21': {}
6324
 
6325
+ '@types/webxr@0.5.24': {}
6326
+
6327
  '@types/ws@8.18.1':
6328
  dependencies:
6329
  '@types/node': 22.19.9
 
6486
  '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
6487
  optional: true
6488
 
6489
+ '@use-gesture/core@10.3.1': {}
6490
+
6491
+ '@use-gesture/react@10.3.1(react@19.2.4)':
6492
+ dependencies:
6493
+ '@use-gesture/core': 10.3.1
6494
+ react: 19.2.4
6495
+
6496
  '@vercel/oidc@3.1.0': {}
6497
 
6498
  '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.9))':
 
6644
  dependencies:
6645
  vue: 3.5.29(typescript@5.9.3)
6646
 
6647
+ '@webgpu/types@0.1.69': {}
6648
+
6649
  '@xyflow/react@12.10.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
6650
  dependencies:
6651
  '@xyflow/system': 0.0.74
 
6669
  d3-selection: 3.0.0
6670
  d3-zoom: 3.0.0
6671
 
6672
+ '@yomguithereal/helpers@1.1.1': {}
6673
+
6674
  acorn-jsx@5.3.2(acorn@8.15.0):
6675
  dependencies:
6676
  acorn: 8.15.0
 
6843
  bindings: 1.5.0
6844
  prebuild-install: 7.1.3
6845
 
6846
+ bidi-js@1.0.3:
6847
+ dependencies:
6848
+ require-from-string: 2.0.2
6849
+
6850
  binary-extensions@2.3.0: {}
6851
 
6852
  bindings@1.5.0:
 
6885
  base64-js: 1.5.1
6886
  ieee754: 1.2.1
6887
 
6888
+ buffer@6.0.3:
6889
+ dependencies:
6890
+ base64-js: 1.5.1
6891
+ ieee754: 1.2.1
6892
+
6893
  cac@6.7.14: {}
6894
 
6895
  call-bind-apply-helpers@1.0.2:
 
6913
 
6914
  camelcase-css@2.0.1: {}
6915
 
6916
+ camera-controls@3.1.2(three@0.180.0):
6917
+ dependencies:
6918
+ three: 0.180.0
6919
+
6920
  caniuse-lite@1.0.30001769: {}
6921
 
6922
  ccount@2.0.1: {}
 
6960
 
6961
  chownr@1.1.4: {}
6962
 
6963
+ class-variance-authority@0.7.1:
6964
+ dependencies:
6965
+ clsx: 2.1.1
6966
+
6967
  classcat@5.0.5: {}
6968
 
6969
+ classnames@2.5.1: {}
6970
+
6971
  client-only@0.0.1: {}
6972
 
6973
  clsx@2.1.1: {}
 
6992
 
6993
  crelt@1.0.6: {}
6994
 
6995
+ cross-env@7.0.3:
6996
+ dependencies:
6997
+ cross-spawn: 7.0.6
6998
+
6999
  cross-spawn@7.0.6:
7000
  dependencies:
7001
  path-key: 3.1.1
 
7029
  dependencies:
7030
  internmap: 2.0.3
7031
 
7032
+ d3-binarytree@1.0.2: {}
7033
+
7034
  d3-color@3.1.0: {}
7035
 
7036
  d3-dispatch@3.0.1: {}
 
7042
 
7043
  d3-ease@3.0.1: {}
7044
 
7045
+ d3-force-3d@3.0.6:
7046
+ dependencies:
7047
+ d3-binarytree: 1.0.2
7048
+ d3-dispatch: 3.0.1
7049
+ d3-octree: 1.1.0
7050
+ d3-quadtree: 3.0.1
7051
+ d3-timer: 3.0.1
7052
+
7053
  d3-format@3.1.2: {}
7054
 
7055
+ d3-hierarchy@3.1.2: {}
7056
+
7057
  d3-interpolate@3.0.1:
7058
  dependencies:
7059
  d3-color: 3.1.0
7060
 
7061
+ d3-octree@1.1.0: {}
7062
+
7063
  d3-path@3.1.0: {}
7064
 
7065
+ d3-quadtree@3.0.1: {}
7066
+
7067
  d3-scale@4.0.2:
7068
  dependencies:
7069
  d3-array: 3.2.4
 
7174
 
7175
  dequal@2.0.3: {}
7176
 
7177
+ detect-gpu@5.0.70:
7178
+ dependencies:
7179
+ webgl-constants: 1.1.1
7180
+
7181
  detect-libc@2.1.2: {}
7182
 
7183
  devlop@1.1.0:
 
7196
 
7197
  dom-accessibility-api@0.6.3: {}
7198
 
7199
+ draco3d@1.5.7: {}
7200
+
7201
  dunder-proto@1.0.1:
7202
  dependencies:
7203
  call-bind-apply-helpers: 1.0.2
 
7206
 
7207
  electron-to-chromium@1.5.286: {}
7208
 
7209
+ ellipsize@0.6.2: {}
7210
+
7211
  emoji-regex@9.2.2: {}
7212
 
7213
  end-of-stream@1.4.5:
 
7568
 
7569
  eventemitter3@5.0.4: {}
7570
 
7571
+ events@3.3.0: {}
7572
+
7573
  eventsource-parser@3.0.6: {}
7574
 
7575
  expand-template@2.0.3: {}
 
7614
  optionalDependencies:
7615
  picomatch: 4.0.3
7616
 
7617
+ fflate@0.6.10: {}
7618
+
7619
+ fflate@0.8.2: {}
7620
+
7621
  file-entry-cache@8.0.0:
7622
  dependencies:
7623
  flat-cache: 4.0.1
 
7732
 
7733
  globrex@0.1.2: {}
7734
 
7735
+ glsl-noise@0.0.0: {}
7736
+
7737
  gopd@1.2.0: {}
7738
 
7739
+ graphology-indices@0.17.0(graphology-types@0.24.8):
7740
+ dependencies:
7741
+ graphology-types: 0.24.8
7742
+ graphology-utils: 2.5.2(graphology-types@0.24.8)
7743
+ mnemonist: 0.39.8
7744
+
7745
+ graphology-layout-forceatlas2@0.10.1(graphology-types@0.24.8):
7746
+ dependencies:
7747
+ graphology-types: 0.24.8
7748
+ graphology-utils: 2.5.2(graphology-types@0.24.8)
7749
+
7750
+ graphology-layout-noverlap@0.4.2(graphology-types@0.24.8):
7751
+ dependencies:
7752
+ graphology-types: 0.24.8
7753
+ graphology-utils: 2.5.2(graphology-types@0.24.8)
7754
+
7755
+ graphology-layout@0.6.1(graphology-types@0.24.8):
7756
+ dependencies:
7757
+ graphology-types: 0.24.8
7758
+ graphology-utils: 2.5.2(graphology-types@0.24.8)
7759
+ pandemonium: 2.4.1
7760
+
7761
+ graphology-metrics@2.4.0(graphology-types@0.24.8):
7762
+ dependencies:
7763
+ graphology-indices: 0.17.0(graphology-types@0.24.8)
7764
+ graphology-shortest-path: 2.1.0(graphology-types@0.24.8)
7765
+ graphology-types: 0.24.8
7766
+ graphology-utils: 2.5.2(graphology-types@0.24.8)
7767
+ mnemonist: 0.39.8
7768
+ pandemonium: 2.4.1
7769
+
7770
+ graphology-shortest-path@2.1.0(graphology-types@0.24.8):
7771
+ dependencies:
7772
+ '@yomguithereal/helpers': 1.1.1
7773
+ graphology-indices: 0.17.0(graphology-types@0.24.8)
7774
+ graphology-types: 0.24.8
7775
+ graphology-utils: 2.5.2(graphology-types@0.24.8)
7776
+ mnemonist: 0.39.8
7777
+
7778
+ graphology-types@0.24.8: {}
7779
+
7780
+ graphology-utils@2.5.2(graphology-types@0.24.8):
7781
+ dependencies:
7782
+ graphology-types: 0.24.8
7783
+
7784
+ graphology@0.26.0(graphology-types@0.24.8):
7785
+ dependencies:
7786
+ events: 3.3.0
7787
+ graphology-types: 0.24.8
7788
+
7789
  guess-json-indent@3.0.1: {}
7790
 
7791
  has-bigints@1.1.0: {}
 
7974
 
7975
  highlightjs-curl@1.3.0: {}
7976
 
7977
+ hls.js@1.6.15: {}
7978
+
7979
+ hold-event@1.1.2: {}
7980
+
7981
  hookable@6.0.1: {}
7982
 
7983
  html-encoding-sniffer@4.0.0:
 
8018
 
8019
  ignore@7.0.5: {}
8020
 
8021
+ immediate@3.0.6: {}
8022
+
8023
  immer@10.2.0: {}
8024
 
8025
  immer@11.1.3: {}
 
8148
 
8149
  is-potential-custom-element-name@1.0.1: {}
8150
 
8151
+ is-promise@2.2.2: {}
8152
+
8153
  is-regex@1.2.1:
8154
  dependencies:
8155
  call-bound: 1.0.4
 
8204
  has-symbols: 1.1.0
8205
  set-function-name: 2.0.2
8206
 
8207
+ its-fine@2.0.0(@types/react@19.2.13)(react@19.2.4):
8208
+ dependencies:
8209
+ '@types/react-reconciler': 0.28.9(@types/react@19.2.13)
8210
+ react: 19.2.4
8211
+ transitivePeerDependencies:
8212
+ - '@types/react'
8213
+
8214
  jiti@1.21.7: {}
8215
 
8216
  joycon@3.1.1: {}
 
8296
  prelude-ls: 1.2.1
8297
  type-check: 0.4.0
8298
 
8299
+ lie@3.3.0:
8300
+ dependencies:
8301
+ immediate: 3.0.6
8302
+
8303
  lilconfig@3.1.3: {}
8304
 
8305
  lines-and-columns@1.2.4: {}
 
8332
 
8333
  lz-string@1.5.0: {}
8334
 
8335
+ maath@0.10.8(@types/three@0.183.1)(three@0.180.0):
8336
+ dependencies:
8337
+ '@types/three': 0.183.1
8338
+ three: 0.180.0
8339
+
8340
  magic-string@0.30.21:
8341
  dependencies:
8342
  '@jridgewell/sourcemap-codec': 1.5.5
 
8506
 
8507
  merge2@1.4.1: {}
8508
 
8509
+ meshline@3.3.1(three@0.180.0):
8510
+ dependencies:
8511
+ three: 0.180.0
8512
+
8513
+ meshoptimizer@1.0.1: {}
8514
+
8515
  microdiff@1.5.0: {}
8516
 
8517
  micromark-core-commonmark@2.0.3:
 
8726
 
8727
  mkdirp-classic@0.5.3: {}
8728
 
8729
+ mnemonist@0.39.8:
8730
+ dependencies:
8731
+ obliterator: 2.0.5
8732
+
8733
  ms@2.1.3: {}
8734
 
8735
  mz@2.7.0:
 
8837
  define-properties: 1.2.1
8838
  es-object-atoms: 1.1.1
8839
 
8840
+ obliterator@2.0.5: {}
8841
+
8842
  on-exit-leak-free@2.1.2: {}
8843
 
8844
  once@1.4.0:
 
8874
 
8875
  p-timeout@6.1.4: {}
8876
 
8877
+ pandemonium@2.4.1:
8878
+ dependencies:
8879
+ mnemonist: 0.39.8
8880
+
8881
  parent-module@1.0.1:
8882
  dependencies:
8883
  callsites: 3.1.0
 
9010
  picocolors: 1.1.1
9011
  source-map-js: 1.2.1
9012
 
9013
+ potpack@1.0.2: {}
9014
+
9015
  prebuild-install@7.1.3:
9016
  dependencies:
9017
  detect-libc: 2.1.2
 
9043
 
9044
  process-warning@5.0.0: {}
9045
 
9046
+ promise-worker-transferable@1.0.4:
9047
+ dependencies:
9048
+ is-promise: 2.2.2
9049
+ lie: 3.3.0
9050
+
9051
  prop-types@15.8.1:
9052
  dependencies:
9053
  loose-envify: 1.4.0
 
9129
 
9130
  react-refresh@0.17.0: {}
9131
 
9132
+ react-use-measure@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
9133
+ dependencies:
9134
+ react: 19.2.4
9135
+ optionalDependencies:
9136
+ react-dom: 19.2.4(react@19.2.4)
9137
+
9138
  react@19.2.4: {}
9139
 
9140
  reactflow@11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
 
9165
  dependencies:
9166
  picomatch: 2.3.1
9167
 
9168
+ reagraph@4.30.8(@types/react@19.2.13)(@types/three@0.183.1)(graphology-types@0.24.8)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
9169
+ dependencies:
9170
+ '@react-spring/three': 10.0.3(@react-three/fiber@9.5.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0))(react@19.2.4)(three@0.180.0)
9171
+ '@react-three/drei': 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0))(@types/react@19.2.13)(@types/three@0.183.1)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0)
9172
+ '@react-three/fiber': 9.5.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.180.0)
9173
+ '@use-gesture/react': 10.3.1(react@19.2.4)
9174
+ camera-controls: 3.1.2(three@0.180.0)
9175
+ classnames: 2.5.1
9176
+ d3-array: 3.2.4
9177
+ d3-force-3d: 3.0.6
9178
+ d3-hierarchy: 3.1.2
9179
+ d3-scale: 4.0.2
9180
+ ellipsize: 0.6.2
9181
+ graphology: 0.26.0(graphology-types@0.24.8)
9182
+ graphology-layout: 0.6.1(graphology-types@0.24.8)
9183
+ graphology-layout-forceatlas2: 0.10.1(graphology-types@0.24.8)
9184
+ graphology-layout-noverlap: 0.4.2(graphology-types@0.24.8)
9185
+ graphology-metrics: 2.4.0(graphology-types@0.24.8)
9186
+ graphology-shortest-path: 2.1.0(graphology-types@0.24.8)
9187
+ hold-event: 1.1.2
9188
+ react: 19.2.4
9189
+ react-dom: 19.2.4(react@19.2.4)
9190
+ three: 0.180.0
9191
+ three-stdlib: 2.36.1(three@0.180.0)
9192
+ zustand: 5.0.8(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
9193
+ transitivePeerDependencies:
9194
+ - '@types/react'
9195
+ - '@types/three'
9196
+ - expo
9197
+ - expo-asset
9198
+ - expo-file-system
9199
+ - expo-gl
9200
+ - graphology-types
9201
+ - immer
9202
+ - react-native
9203
+ - use-sync-external-store
9204
+
9205
  real-require@0.2.0: {}
9206
 
9207
  recharts@3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1):
 
9538
 
9539
  stackback@0.0.2: {}
9540
 
9541
+ stats-gl@2.4.2(@types/three@0.183.1)(three@0.180.0):
9542
+ dependencies:
9543
+ '@types/three': 0.183.1
9544
+ three: 0.180.0
9545
+
9546
+ stats.js@0.17.0: {}
9547
+
9548
  std-env@3.10.0: {}
9549
 
9550
  stop-iteration-iterator@1.1.0:
 
9673
 
9674
  supports-preserve-symlinks-flag@1.0.0: {}
9675
 
9676
+ suspend-react@0.1.3(react@19.2.4):
9677
+ dependencies:
9678
+ react: 19.2.4
9679
+
9680
  swrv@1.1.0(vue@3.5.29(typescript@5.9.3)):
9681
  dependencies:
9682
  vue: 3.5.29(typescript@5.9.3)
 
9744
  dependencies:
9745
  real-require: 0.2.0
9746
 
9747
+ three-mesh-bvh@0.8.3(three@0.180.0):
9748
+ dependencies:
9749
+ three: 0.180.0
9750
+
9751
+ three-stdlib@2.36.1(three@0.180.0):
9752
+ dependencies:
9753
+ '@types/draco3d': 1.4.10
9754
+ '@types/offscreencanvas': 2019.7.3
9755
+ '@types/webxr': 0.5.24
9756
+ draco3d: 1.5.7
9757
+ fflate: 0.6.10
9758
+ potpack: 1.0.2
9759
+ three: 0.180.0
9760
+
9761
+ three@0.180.0: {}
9762
+
9763
  time-span@5.1.0:
9764
  dependencies:
9765
  convert-hrtime: 5.0.0
 
9801
 
9802
  trim-lines@3.0.1: {}
9803
 
9804
+ troika-three-text@0.52.4(three@0.180.0):
9805
+ dependencies:
9806
+ bidi-js: 1.0.3
9807
+ three: 0.180.0
9808
+ troika-three-utils: 0.52.4(three@0.180.0)
9809
+ troika-worker-utils: 0.52.0
9810
+ webgl-sdf-generator: 1.1.1
9811
+
9812
+ troika-three-utils@0.52.4(three@0.180.0):
9813
+ dependencies:
9814
+ three: 0.180.0
9815
+
9816
+ troika-worker-utils@0.52.0: {}
9817
+
9818
  trough@2.2.0: {}
9819
 
9820
  truncate-json@3.0.1:
 
9848
  dependencies:
9849
  safe-buffer: 5.2.1
9850
 
9851
+ tunnel-rat@0.1.2(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4):
9852
+ dependencies:
9853
+ zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4)
9854
+ transitivePeerDependencies:
9855
+ - '@types/react'
9856
+ - immer
9857
+ - react
9858
+
9859
  type-check@0.4.0:
9860
  dependencies:
9861
  prelude-ls: 1.2.1
 
10003
 
10004
  util-deprecate@1.0.2: {}
10005
 
10006
+ utility-types@3.11.0: {}
10007
+
10008
  vfile-location@5.0.3:
10009
  dependencies:
10010
  '@types/unist': 3.0.3
 
10144
 
10145
  web-worker@1.5.0: {}
10146
 
10147
+ webgl-constants@1.1.1: {}
10148
+
10149
+ webgl-sdf-generator@1.1.1: {}
10150
+
10151
  webidl-conversions@7.0.0: {}
10152
 
10153
  whatwg-encoding@3.1.1:
 
10248
  react: 19.2.4
10249
  use-sync-external-store: 1.6.0(react@19.2.4)
10250
 
10251
+ zustand@5.0.8(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
10252
+ optionalDependencies:
10253
+ '@types/react': 19.2.13
10254
+ immer: 11.1.3
10255
+ react: 19.2.4
10256
+ use-sync-external-store: 1.6.0(react@19.2.4)
10257
+
10258
  zwitch@2.0.4: {}
public/brand/claude-logo.png ADDED

Git LFS Details

  • SHA256: dcc58271920750441afd420268045addb593ee01e68b4e05a13f7eface7e65d3
  • Pointer size: 130 Bytes
  • Size of remote file: 62.8 kB
public/brand/codex-logo.png ADDED

Git LFS Details

  • SHA256: 98707d4bd973489c4534c863497bffa6e85064dc0946b503b864fcbf49c564ae
  • Pointer size: 129 Bytes
  • Size of remote file: 2.92 kB
public/brand/hermes-logo.png ADDED

Git LFS Details

  • SHA256: 382977d957825783c74ac8a8b7792ad185e7168166f4cedeb75ad27f254c5ee7
  • Pointer size: 130 Bytes
  • Size of remote file: 25.4 kB
public/brand/mc-logo-128.png ADDED

Git LFS Details

  • SHA256: 956d0fa7f45b48c1034d0add4f437d3171500ae1235da22d6de6f8448654e56d
  • Pointer size: 130 Bytes
  • Size of remote file: 17.1 kB
public/brand/mc-logo-256.png ADDED

Git LFS Details

  • SHA256: 8cede9da544bf9025bd859187ed1c2ecedd3227db122e059ad9acf1cfaef5e01
  • Pointer size: 130 Bytes
  • Size of remote file: 64.5 kB
public/brand/mc-logo-512.png ADDED

Git LFS Details

  • SHA256: 0d472797998cabfa0aaa8c1644a71d0c53dd8dce4d1afd9b0c32254ea1c8011d
  • Pointer size: 131 Bytes
  • Size of remote file: 286 kB
public/brand/openclaw-logo.png ADDED

Git LFS Details

  • SHA256: 0c4a23f702882b885f68c95a62c15b91f3371df22bbcd3cf81e069f906ae7efe
  • Pointer size: 130 Bytes
  • Size of remote file: 25.7 kB
public/mc-logo.png ADDED

Git LFS Details

  • SHA256: 4855cf9017cd5d8637efee6a41013f0effa23afd0c4c3f30e767de4b6281963e
  • Pointer size: 132 Bytes
  • Size of remote file: 1.85 MB
public/mc.png ADDED

Git LFS Details

  • SHA256: 26163ddc887576f66d962bf544a4abb1b76b49e6a52996113c4606ede7e7c3a6
  • Pointer size: 132 Bytes
  • Size of remote file: 2.42 MB
scripts/check-node-version.mjs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ const REQUIRED_NODE_MAJOR = 22
4
+
5
+ const current = process.versions.node
6
+ const currentMajor = Number.parseInt(current.split('.')[0] || '', 10)
7
+
8
+ if (currentMajor !== REQUIRED_NODE_MAJOR) {
9
+ console.error(
10
+ [
11
+ `error: Mission Control requires Node ${REQUIRED_NODE_MAJOR}.x, but found ${current}.`,
12
+ 'use `nvm use 22` (or your version manager equivalent) before installing, building, or starting the app.',
13
+ ].join('\n')
14
+ )
15
+ process.exit(1)
16
+ }
scripts/deploy-standalone.sh ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
7
+
8
+ BRANCH="${BRANCH:-$(git -C "$PROJECT_ROOT" branch --show-current)}"
9
+ PORT="${PORT:-3000}"
10
+ LISTEN_HOST="${MC_HOSTNAME:-0.0.0.0}"
11
+ LOG_PATH="${LOG_PATH:-/tmp/mc.log}"
12
+ VERIFY_HOST="${VERIFY_HOST:-127.0.0.1}"
13
+ PID_FILE="${PID_FILE:-$PROJECT_ROOT/.next/standalone/server.pid}"
14
+ SOURCE_DATA_DIR="$PROJECT_ROOT/.data"
15
+ BUILD_DATA_DIR="$PROJECT_ROOT/.next/build-runtime"
16
+ NODE_VERSION_FILE="$PROJECT_ROOT/.nvmrc"
17
+
18
+ use_project_node() {
19
+ if [[ ! -f "$NODE_VERSION_FILE" ]]; then
20
+ return
21
+ fi
22
+
23
+ if [[ -z "${NVM_DIR:-}" ]]; then
24
+ export NVM_DIR="$HOME/.nvm"
25
+ fi
26
+
27
+ if [[ -s "$NVM_DIR/nvm.sh" ]]; then
28
+ # shellcheck disable=SC1090
29
+ source "$NVM_DIR/nvm.sh"
30
+ nvm use >/dev/null
31
+ fi
32
+ }
33
+
34
+ list_listener_pids() {
35
+ local combined=""
36
+
37
+ if command -v lsof >/dev/null 2>&1; then
38
+ combined+="$(
39
+ lsof -tiTCP:"$PORT" -sTCP:LISTEN 2>/dev/null || true
40
+ )"$'\n'
41
+ fi
42
+
43
+ if command -v ss >/dev/null 2>&1; then
44
+ combined+="$(
45
+ ss -ltnp 2>/dev/null | awk -v port=":$PORT" '
46
+ index($4, port) || index($5, port) {
47
+ if (match($0, /pid=[0-9]+/)) {
48
+ print substr($0, RSTART + 4, RLENGTH - 4)
49
+ }
50
+ }
51
+ '
52
+ )"$'\n'
53
+ fi
54
+
55
+ printf '%s\n' "$combined" | awk -v port="$PORT" '
56
+ /^[0-9]+$/ {
57
+ seen[$0] = 1
58
+ }
59
+ END {
60
+ for (pid in seen) {
61
+ print pid
62
+ }
63
+ }
64
+ ' | sort -u
65
+ }
66
+
67
+ stop_pid() {
68
+ local pid="$1"
69
+ local label="$2"
70
+
71
+ if [[ -z "$pid" ]] || ! kill -0 "$pid" 2>/dev/null; then
72
+ return
73
+ fi
74
+
75
+ echo "==> stopping $label (pid=$pid)"
76
+ kill "$pid" 2>/dev/null || true
77
+
78
+ for _ in $(seq 1 10); do
79
+ if ! kill -0 "$pid" 2>/dev/null; then
80
+ return
81
+ fi
82
+ sleep 1
83
+ done
84
+
85
+ echo "==> force stopping $label (pid=$pid)"
86
+ kill -9 "$pid" 2>/dev/null || true
87
+ }
88
+
89
+ stop_existing_server() {
90
+ local -a candidate_pids=()
91
+
92
+ if [[ -f "$PID_FILE" ]]; then
93
+ candidate_pids+=("$(cat "$PID_FILE" 2>/dev/null || true)")
94
+ fi
95
+
96
+ while IFS= read -r pid; do
97
+ candidate_pids+=("$pid")
98
+ done < <(list_listener_pids)
99
+
100
+ if command -v pgrep >/dev/null 2>&1; then
101
+ while IFS= read -r pid; do
102
+ candidate_pids+=("$pid")
103
+ done < <(pgrep -f "$PROJECT_ROOT/.next/standalone/server.js" || true)
104
+ fi
105
+
106
+ if [[ ${#candidate_pids[@]} -eq 0 ]]; then
107
+ return
108
+ fi
109
+
110
+ declare -A seen=()
111
+ for pid in "${candidate_pids[@]}"; do
112
+ [[ -z "$pid" ]] && continue
113
+ [[ -n "${seen[$pid]:-}" ]] && continue
114
+ seen[$pid]=1
115
+ stop_pid "$pid" "standalone server"
116
+ done
117
+
118
+ for _ in $(seq 1 10); do
119
+ if [[ -z "$(list_listener_pids | head -n1)" ]]; then
120
+ rm -f "$PID_FILE"
121
+ return
122
+ fi
123
+ sleep 1
124
+ done
125
+
126
+ echo "error: port $PORT is still in use after stopping existing server" >&2
127
+ exit 1
128
+ }
129
+
130
+ load_env() {
131
+ set -a
132
+ if [[ -f .env ]]; then
133
+ # shellcheck disable=SC1091
134
+ source .env
135
+ fi
136
+ if [[ -f .env.local ]]; then
137
+ # shellcheck disable=SC1091
138
+ source .env.local
139
+ fi
140
+ set +a
141
+ }
142
+
143
+ migrate_runtime_data_dir() {
144
+ local target_data_dir="${MISSION_CONTROL_DATA_DIR:-$SOURCE_DATA_DIR}"
145
+
146
+ if [[ "$target_data_dir" == "$SOURCE_DATA_DIR" ]]; then
147
+ return
148
+ fi
149
+
150
+ mkdir -p "$target_data_dir"
151
+
152
+ local source_db="$SOURCE_DATA_DIR/mission-control.db"
153
+ local target_db="$target_data_dir/mission-control.db"
154
+
155
+ if [[ -s "$target_db" || ! -s "$source_db" ]]; then
156
+ return
157
+ fi
158
+
159
+ echo "==> migrating runtime data to $target_data_dir"
160
+ if command -v sqlite3 >/dev/null 2>&1; then
161
+ local target_db_tmp="$target_db.tmp"
162
+ rm -f "$target_db_tmp"
163
+ sqlite3 "$source_db" ".backup '$target_db_tmp'"
164
+ mv "$target_db_tmp" "$target_db"
165
+
166
+ if [[ -f "$SOURCE_DATA_DIR/mission-control-tokens.json" ]]; then
167
+ cp "$SOURCE_DATA_DIR/mission-control-tokens.json" "$target_data_dir/mission-control-tokens.json"
168
+ fi
169
+ if [[ -d "$SOURCE_DATA_DIR/backups" ]]; then
170
+ rsync -a "$SOURCE_DATA_DIR/backups"/ "$target_data_dir/backups"/
171
+ fi
172
+ else
173
+ rsync -a \
174
+ --exclude 'mission-control.db-shm' \
175
+ --exclude 'mission-control.db-wal' \
176
+ --exclude '*.db-shm' \
177
+ --exclude '*.db-wal' \
178
+ "$SOURCE_DATA_DIR"/ "$target_data_dir"/
179
+ fi
180
+ }
181
+
182
+ cd "$PROJECT_ROOT"
183
+ use_project_node
184
+
185
+ echo "==> fetching branch $BRANCH"
186
+ git fetch origin "$BRANCH"
187
+ git merge --ff-only FETCH_HEAD
188
+
189
+ load_env
190
+ migrate_runtime_data_dir
191
+
192
+ echo "==> stopping existing standalone server before rebuild"
193
+ stop_existing_server
194
+
195
+ echo "==> installing dependencies"
196
+ pnpm install --frozen-lockfile
197
+
198
+ echo "==> rebuilding standalone bundle"
199
+ rm -rf .next
200
+ mkdir -p "$BUILD_DATA_DIR"
201
+ MISSION_CONTROL_DATA_DIR="$BUILD_DATA_DIR" \
202
+ MISSION_CONTROL_DB_PATH="$BUILD_DATA_DIR/mission-control.db" \
203
+ MISSION_CONTROL_TOKENS_PATH="$BUILD_DATA_DIR/mission-control-tokens.json" \
204
+ pnpm build
205
+
206
+ echo "==> starting standalone server"
207
+ load_env
208
+
209
+ PORT="$PORT" HOSTNAME="$LISTEN_HOST" nohup bash "$PROJECT_ROOT/scripts/start-standalone.sh" >"$LOG_PATH" 2>&1 &
210
+ new_pid=$!
211
+ echo "$new_pid" > "$PID_FILE"
212
+
213
+ echo "==> verifying process and static assets"
214
+ for _ in $(seq 1 20); do
215
+ if curl -fsS "http://$VERIFY_HOST:$PORT/login" >/dev/null 2>&1; then
216
+ break
217
+ fi
218
+ sleep 1
219
+ done
220
+
221
+ login_html="$(curl -fsS "http://$VERIFY_HOST:$PORT/login")"
222
+ css_path="$(printf '%s\n' "$login_html" | sed -n 's|.*\(/_next/static/chunks/[^"]*\.css\).*|\1|p' | sed -n '1p')"
223
+ if [[ -z "${css_path:-}" ]]; then
224
+ echo "error: no css asset found in rendered login HTML" >&2
225
+ exit 1
226
+ fi
227
+
228
+ listener_pid="$(list_listener_pids | head -n1)"
229
+ if [[ -z "${listener_pid:-}" ]]; then
230
+ echo "error: no listener detected on port $PORT after startup" >&2
231
+ exit 1
232
+ fi
233
+ if [[ "$listener_pid" != "$new_pid" ]]; then
234
+ echo "error: port $PORT is owned by pid=$listener_pid, expected new pid=$new_pid" >&2
235
+ exit 1
236
+ fi
237
+
238
+ css_disk_path="$PROJECT_ROOT/.next/standalone/.next${css_path#/_next}"
239
+ if [[ ! -f "$css_disk_path" ]]; then
240
+ echo "error: rendered css asset missing on disk: $css_disk_path" >&2
241
+ exit 1
242
+ fi
243
+
244
+ content_type="$(curl -fsSI "http://$VERIFY_HOST:$PORT$css_path" | awk 'BEGIN{IGNORECASE=1} /^content-type:/ {print $2}' | tr -d '\r')"
245
+ if [[ "${content_type:-}" != text/css* ]]; then
246
+ echo "error: css asset served with unexpected content-type: ${content_type:-missing}" >&2
247
+ exit 1
248
+ fi
249
+
250
+ echo "==> deployed commit $(git rev-parse --short HEAD)"
251
+ echo " pid=$new_pid port=$PORT css=$css_path"
scripts/e2e-openclaw/start-e2e-server.mjs CHANGED
@@ -1,9 +1,30 @@
1
  #!/usr/bin/env node
2
  import { spawn } from 'node:child_process'
3
  import fs from 'node:fs'
 
4
  import path from 'node:path'
5
  import process from 'node:process'
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  const modeArg = process.argv.find((arg) => arg.startsWith('--mode='))
8
  const mode = modeArg ? modeArg.split('=')[1] : 'local'
9
  if (mode !== 'local' && mode !== 'gateway') {
@@ -16,6 +37,7 @@ const fixtureSource = path.join(repoRoot, 'tests', 'fixtures', 'openclaw')
16
  const runtimeRoot = path.join(repoRoot, '.tmp', 'e2e-openclaw', mode)
17
  const dataDir = path.join(runtimeRoot, 'data')
18
  const mockBinDir = path.join(repoRoot, 'scripts', 'e2e-openclaw', 'bin')
 
19
 
20
  fs.rmSync(runtimeRoot, { recursive: true, force: true })
21
  fs.mkdirSync(runtimeRoot, { recursive: true })
@@ -23,13 +45,14 @@ fs.mkdirSync(dataDir, { recursive: true })
23
  fs.cpSync(fixtureSource, runtimeRoot, { recursive: true })
24
 
25
  const gatewayHost = '127.0.0.1'
26
- const gatewayPort = '18789'
27
 
28
  const baseEnv = {
29
  ...process.env,
30
  API_KEY: process.env.API_KEY || 'test-api-key-e2e-12345',
31
  AUTH_USER: process.env.AUTH_USER || 'admin',
32
  AUTH_PASS: process.env.AUTH_PASS || 'admin',
 
33
  MC_DISABLE_RATE_LIMIT: '1',
34
  MISSION_CONTROL_DATA_DIR: dataDir,
35
  MISSION_CONTROL_DB_PATH: path.join(dataDir, 'mission-control.db'),
@@ -39,11 +62,17 @@ const baseEnv = {
39
  OPENCLAW_GATEWAY_PORT: gatewayPort,
40
  OPENCLAW_BIN: path.join(mockBinDir, 'openclaw'),
41
  CLAWDBOT_BIN: path.join(mockBinDir, 'clawdbot'),
 
 
 
 
 
42
  PATH: `${mockBinDir}:${process.env.PATH || ''}`,
43
  E2E_GATEWAY_EXPECTED: mode === 'gateway' ? '1' : '0',
44
  }
45
 
46
  const children = []
 
47
 
48
  if (mode === 'gateway') {
49
  const gw = spawn('node', ['scripts/e2e-openclaw/mock-gateway.mjs'], {
@@ -51,11 +80,24 @@ if (mode === 'gateway') {
51
  env: baseEnv,
52
  stdio: 'inherit',
53
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  children.push(gw)
55
  }
56
 
57
  const standaloneServerPath = path.join(repoRoot, '.next', 'standalone', 'server.js')
58
- const app = fs.existsSync(standaloneServerPath)
59
  ? spawn('node', [standaloneServerPath], {
60
  cwd: repoRoot,
61
  env: {
 
1
  #!/usr/bin/env node
2
  import { spawn } from 'node:child_process'
3
  import fs from 'node:fs'
4
+ import net from 'node:net'
5
  import path from 'node:path'
6
  import process from 'node:process'
7
 
8
+ async function findAvailablePort(host = '127.0.0.1') {
9
+ return await new Promise((resolve, reject) => {
10
+ const server = net.createServer()
11
+ server.unref()
12
+ server.on('error', reject)
13
+ server.listen(0, host, () => {
14
+ const address = server.address()
15
+ if (!address || typeof address === 'string') {
16
+ server.close(() => reject(new Error('failed to resolve dynamic port')))
17
+ return
18
+ }
19
+ const { port } = address
20
+ server.close((err) => {
21
+ if (err) reject(err)
22
+ else resolve(port)
23
+ })
24
+ })
25
+ })
26
+ }
27
+
28
  const modeArg = process.argv.find((arg) => arg.startsWith('--mode='))
29
  const mode = modeArg ? modeArg.split('=')[1] : 'local'
30
  if (mode !== 'local' && mode !== 'gateway') {
 
37
  const runtimeRoot = path.join(repoRoot, '.tmp', 'e2e-openclaw', mode)
38
  const dataDir = path.join(runtimeRoot, 'data')
39
  const mockBinDir = path.join(repoRoot, 'scripts', 'e2e-openclaw', 'bin')
40
+ const skillsRoot = path.join(runtimeRoot, 'skills')
41
 
42
  fs.rmSync(runtimeRoot, { recursive: true, force: true })
43
  fs.mkdirSync(runtimeRoot, { recursive: true })
 
45
  fs.cpSync(fixtureSource, runtimeRoot, { recursive: true })
46
 
47
  const gatewayHost = '127.0.0.1'
48
+ const gatewayPort = String(await findAvailablePort(gatewayHost))
49
 
50
  const baseEnv = {
51
  ...process.env,
52
  API_KEY: process.env.API_KEY || 'test-api-key-e2e-12345',
53
  AUTH_USER: process.env.AUTH_USER || 'admin',
54
  AUTH_PASS: process.env.AUTH_PASS || 'admin',
55
+ MISSION_CONTROL_TEST_MODE: process.env.MISSION_CONTROL_TEST_MODE || '1',
56
  MC_DISABLE_RATE_LIMIT: '1',
57
  MISSION_CONTROL_DATA_DIR: dataDir,
58
  MISSION_CONTROL_DB_PATH: path.join(dataDir, 'mission-control.db'),
 
62
  OPENCLAW_GATEWAY_PORT: gatewayPort,
63
  OPENCLAW_BIN: path.join(mockBinDir, 'openclaw'),
64
  CLAWDBOT_BIN: path.join(mockBinDir, 'clawdbot'),
65
+ MC_SKILLS_USER_AGENTS_DIR: path.join(skillsRoot, 'user-agents'),
66
+ MC_SKILLS_USER_CODEX_DIR: path.join(skillsRoot, 'user-codex'),
67
+ MC_SKILLS_PROJECT_AGENTS_DIR: path.join(skillsRoot, 'project-agents'),
68
+ MC_SKILLS_PROJECT_CODEX_DIR: path.join(skillsRoot, 'project-codex'),
69
+ MC_SKILLS_OPENCLAW_DIR: path.join(skillsRoot, 'openclaw'),
70
  PATH: `${mockBinDir}:${process.env.PATH || ''}`,
71
  E2E_GATEWAY_EXPECTED: mode === 'gateway' ? '1' : '0',
72
  }
73
 
74
  const children = []
75
+ let app = null
76
 
77
  if (mode === 'gateway') {
78
  const gw = spawn('node', ['scripts/e2e-openclaw/mock-gateway.mjs'], {
 
80
  env: baseEnv,
81
  stdio: 'inherit',
82
  })
83
+ gw.on('error', (err) => {
84
+ process.stderr.write(`[openclaw-e2e] mock gateway failed to start: ${String(err)}\n`)
85
+ shutdown('SIGTERM')
86
+ process.exit(1)
87
+ })
88
+ gw.on('exit', (code, signal) => {
89
+ const exitCode = code ?? (signal ? 1 : 0)
90
+ if (exitCode !== 0) {
91
+ process.stderr.write(`[openclaw-e2e] mock gateway exited unexpectedly (code=${exitCode}, signal=${signal ?? 'none'})\n`)
92
+ shutdown('SIGTERM')
93
+ process.exit(exitCode)
94
+ }
95
+ })
96
  children.push(gw)
97
  }
98
 
99
  const standaloneServerPath = path.join(repoRoot, '.next', 'standalone', 'server.js')
100
+ app = fs.existsSync(standaloneServerPath)
101
  ? spawn('node', [standaloneServerPath], {
102
  cwd: repoRoot,
103
  env: {
scripts/generate-env.sh ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Generate a secure .env file from .env.example with random secrets.
3
+ # Usage: bash scripts/generate-env.sh [output-path]
4
+ #
5
+ # If output-path is omitted, writes to .env in the project root.
6
+ # Will NOT overwrite an existing .env unless --force is passed.
7
+
8
+ set -euo pipefail
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
12
+ EXAMPLE_FILE="$PROJECT_ROOT/.env.example"
13
+ FORCE=false
14
+ OUTPUT=""
15
+
16
+ for arg in "$@"; do
17
+ case "$arg" in
18
+ --force) FORCE=true ;;
19
+ *) OUTPUT="$arg" ;;
20
+ esac
21
+ done
22
+
23
+ OUTPUT="${OUTPUT:-$PROJECT_ROOT/.env}"
24
+
25
+ if [[ -f "$OUTPUT" ]] && ! $FORCE; then
26
+ echo "Error: $OUTPUT already exists. Use --force to overwrite."
27
+ exit 1
28
+ fi
29
+
30
+ if [[ ! -f "$EXAMPLE_FILE" ]]; then
31
+ echo "Error: .env.example not found at $EXAMPLE_FILE"
32
+ exit 1
33
+ fi
34
+
35
+ # Generate cryptographically random values
36
+ generate_password() {
37
+ local len="${1:-24}"
38
+ # Use openssl if available, fallback to /dev/urandom
39
+ if command -v openssl &>/dev/null; then
40
+ openssl rand -base64 "$((len * 3 / 4 + 1))" | tr -dc 'A-Za-z0-9' | head -c "$len"
41
+ else
42
+ head -c "$((len * 2))" /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c "$len"
43
+ fi
44
+ }
45
+
46
+ generate_hex() {
47
+ local len="${1:-32}"
48
+ if command -v openssl &>/dev/null; then
49
+ openssl rand -hex "$((len / 2))"
50
+ else
51
+ head -c "$((len / 2))" /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c "$len"
52
+ fi
53
+ }
54
+
55
+ AUTH_PASS="$(generate_password 24)"
56
+ API_KEY="$(generate_hex 32)"
57
+ AUTH_SECRET="$(generate_password 32)"
58
+
59
+ # Copy .env.example and replace default secrets
60
+ cp "$EXAMPLE_FILE" "$OUTPUT"
61
+
62
+ # Replace the insecure defaults with generated values
63
+ if [[ "$(uname)" == "Darwin" ]]; then
64
+ sed -i '' "s|^AUTH_PASS=.*|AUTH_PASS=$AUTH_PASS|" "$OUTPUT"
65
+ sed -i '' "s|^API_KEY=.*|API_KEY=$API_KEY|" "$OUTPUT"
66
+ sed -i '' "s|^AUTH_SECRET=.*|AUTH_SECRET=$AUTH_SECRET|" "$OUTPUT"
67
+ else
68
+ sed -i "s|^AUTH_PASS=.*|AUTH_PASS=$AUTH_PASS|" "$OUTPUT"
69
+ sed -i "s|^API_KEY=.*|API_KEY=$API_KEY|" "$OUTPUT"
70
+ sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$AUTH_SECRET|" "$OUTPUT"
71
+ fi
72
+
73
+ # Lock down permissions
74
+ chmod 600 "$OUTPUT"
75
+
76
+ echo "Generated secure .env at $OUTPUT"
77
+ echo " AUTH_USER: admin"
78
+ echo " AUTH_PASS: $AUTH_PASS"
79
+ echo " API_KEY: $API_KEY"
80
+ echo ""
81
+ echo "Save these credentials — they are not stored elsewhere."
scripts/security-audit.sh ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Mission Control Security Audit
3
+ # Run: bash scripts/security-audit.sh [--env-file .env]
4
+
5
+ set -euo pipefail
6
+
7
+ SCORE=0
8
+ MAX_SCORE=0
9
+ ISSUES=()
10
+
11
+ pass() { echo " [PASS] $1"; ((SCORE++)); ((MAX_SCORE++)); }
12
+ fail() { echo " [FAIL] $1"; ISSUES+=("$1"); ((MAX_SCORE++)); }
13
+ warn() { echo " [WARN] $1"; ((MAX_SCORE++)); }
14
+ info() { echo " [INFO] $1"; }
15
+
16
+ # Load .env if exists
17
+ ENV_FILE="${1:-.env}"
18
+ if [[ -f "$ENV_FILE" ]]; then
19
+ while IFS='=' read -r key value; do
20
+ [[ "$key" =~ ^#.*$ ]] && continue
21
+ [[ -z "$key" ]] && continue
22
+ declare "$key=$value" 2>/dev/null || true
23
+ done < "$ENV_FILE"
24
+ fi
25
+
26
+ echo "=== Mission Control Security Audit ==="
27
+ echo ""
28
+
29
+ # 1. .env file permissions
30
+ echo "--- File Permissions ---"
31
+ if [[ -f "$ENV_FILE" ]]; then
32
+ perms=$(stat -f '%A' "$ENV_FILE" 2>/dev/null || stat -c '%a' "$ENV_FILE" 2>/dev/null)
33
+ if [[ "$perms" == "600" ]]; then
34
+ pass ".env permissions are 600 (owner read/write only)"
35
+ else
36
+ fail ".env permissions are $perms (should be 600). Run: chmod 600 $ENV_FILE"
37
+ fi
38
+ else
39
+ warn ".env file not found at $ENV_FILE"
40
+ fi
41
+
42
+ # 2. Default passwords check
43
+ echo ""
44
+ echo "--- Credentials ---"
45
+ INSECURE_PASSWORDS=("admin" "password" "change-me-on-first-login" "changeme" "testpass123" "testpass1234")
46
+ AUTH_PASS_VAL="${AUTH_PASS:-}"
47
+ if [[ -z "$AUTH_PASS_VAL" ]]; then
48
+ fail "AUTH_PASS is not set"
49
+ else
50
+ insecure=false
51
+ for bad in "${INSECURE_PASSWORDS[@]}"; do
52
+ if [[ "$AUTH_PASS_VAL" == "$bad" ]]; then
53
+ insecure=true; break
54
+ fi
55
+ done
56
+ if $insecure; then
57
+ fail "AUTH_PASS is set to a known insecure default"
58
+ elif [[ ${#AUTH_PASS_VAL} -lt 12 ]]; then
59
+ fail "AUTH_PASS is too short (${#AUTH_PASS_VAL} chars, minimum 12)"
60
+ else
61
+ pass "AUTH_PASS is set to a non-default value (${#AUTH_PASS_VAL} chars)"
62
+ fi
63
+ fi
64
+
65
+ API_KEY_VAL="${API_KEY:-}"
66
+ if [[ -z "$API_KEY_VAL" || "$API_KEY_VAL" == "generate-a-random-key" ]]; then
67
+ fail "API_KEY is not set or uses the default value"
68
+ else
69
+ pass "API_KEY is configured"
70
+ fi
71
+
72
+ # 3. Network config
73
+ echo ""
74
+ echo "--- Network Security ---"
75
+ MC_ALLOWED="${MC_ALLOWED_HOSTS:-}"
76
+ MC_ANY="${MC_ALLOW_ANY_HOST:-}"
77
+ if [[ "$MC_ANY" == "1" || "$MC_ANY" == "true" ]]; then
78
+ fail "MC_ALLOW_ANY_HOST is enabled (any host can connect)"
79
+ elif [[ -n "$MC_ALLOWED" ]]; then
80
+ pass "MC_ALLOWED_HOSTS is configured: $MC_ALLOWED"
81
+ else
82
+ warn "MC_ALLOWED_HOSTS is not set (defaults apply)"
83
+ fi
84
+
85
+ # 4. Cookie/HTTPS config
86
+ echo ""
87
+ echo "--- HTTPS & Cookies ---"
88
+ COOKIE_SECURE="${MC_COOKIE_SECURE:-}"
89
+ if [[ "$COOKIE_SECURE" == "1" || "$COOKIE_SECURE" == "true" ]]; then
90
+ pass "MC_COOKIE_SECURE is enabled"
91
+ else
92
+ warn "MC_COOKIE_SECURE is not enabled (cookies sent over HTTP)"
93
+ fi
94
+
95
+ SAMESITE="${MC_COOKIE_SAMESITE:-strict}"
96
+ if [[ "$SAMESITE" == "strict" ]]; then
97
+ pass "MC_COOKIE_SAMESITE is strict"
98
+ else
99
+ warn "MC_COOKIE_SAMESITE is '$SAMESITE' (strict recommended)"
100
+ fi
101
+
102
+ HSTS="${MC_ENABLE_HSTS:-}"
103
+ if [[ "$HSTS" == "1" ]]; then
104
+ pass "HSTS is enabled"
105
+ else
106
+ warn "HSTS is not enabled (set MC_ENABLE_HSTS=1 for HTTPS deployments)"
107
+ fi
108
+
109
+ # 5. Rate limiting
110
+ echo ""
111
+ echo "--- Rate Limiting ---"
112
+ RL_DISABLED="${MC_DISABLE_RATE_LIMIT:-}"
113
+ if [[ "$RL_DISABLED" == "1" ]]; then
114
+ fail "Rate limiting is disabled (MC_DISABLE_RATE_LIMIT=1)"
115
+ else
116
+ pass "Rate limiting is active"
117
+ fi
118
+
119
+ # 6. Docker security (if running in Docker)
120
+ echo ""
121
+ echo "--- Docker Security ---"
122
+ if command -v docker &>/dev/null; then
123
+ if docker ps --filter name=mission-control --format '{{.Names}}' 2>/dev/null | grep -q mission-control; then
124
+ ro=$(docker inspect mission-control --format '{{.HostConfig.ReadonlyRootfs}}' 2>/dev/null || echo "false")
125
+ if [[ "$ro" == "true" ]]; then
126
+ pass "Container filesystem is read-only"
127
+ else
128
+ warn "Container filesystem is writable (use read_only: true)"
129
+ fi
130
+
131
+ nnp=$(docker inspect mission-control --format '{{.HostConfig.SecurityOpt}}' 2>/dev/null || echo "[]")
132
+ if echo "$nnp" | grep -q "no-new-privileges"; then
133
+ pass "no-new-privileges is set"
134
+ else
135
+ warn "no-new-privileges not set"
136
+ fi
137
+
138
+ user=$(docker inspect mission-control --format '{{.Config.User}}' 2>/dev/null || echo "")
139
+ if [[ -n "$user" && "$user" != "root" && "$user" != "0" ]]; then
140
+ pass "Container runs as non-root user ($user)"
141
+ else
142
+ warn "Container may be running as root"
143
+ fi
144
+ else
145
+ info "Mission Control container not running"
146
+ fi
147
+ else
148
+ info "Docker not installed (skipping container checks)"
149
+ fi
150
+
151
+ # Summary
152
+ echo ""
153
+ echo "=== Security Score: $SCORE / $MAX_SCORE ==="
154
+ if [[ ${#ISSUES[@]} -gt 0 ]]; then
155
+ echo ""
156
+ echo "Issues to fix:"
157
+ for issue in "${ISSUES[@]}"; do
158
+ echo " - $issue"
159
+ done
160
+ fi
161
+
162
+ if [[ $SCORE -eq $MAX_SCORE ]]; then
163
+ echo "All checks passed!"
164
+ elif [[ $SCORE -ge $((MAX_SCORE * 7 / 10)) ]]; then
165
+ echo "Good security posture with minor improvements needed."
166
+ else
167
+ echo "Security improvements recommended before production use."
168
+ fi
scripts/smoke-staging.mjs ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ const baseUrl = (process.env.STAGING_BASE_URL || process.env.BASE_URL || '').replace(/\/$/, '')
3
+ const apiKey = process.env.STAGING_API_KEY || process.env.API_KEY || ''
4
+ const authUser = process.env.STAGING_AUTH_USER || process.env.AUTH_USER || ''
5
+ const authPass = process.env.STAGING_AUTH_PASS || process.env.AUTH_PASS || ''
6
+
7
+ if (!baseUrl) {
8
+ console.error('Missing STAGING_BASE_URL (or BASE_URL).')
9
+ process.exit(1)
10
+ }
11
+ if (!apiKey) {
12
+ console.error('Missing STAGING_API_KEY (or API_KEY).')
13
+ process.exit(1)
14
+ }
15
+ if (!authUser || !authPass) {
16
+ console.error('Missing STAGING_AUTH_USER/STAGING_AUTH_PASS (or AUTH_USER/AUTH_PASS).')
17
+ process.exit(1)
18
+ }
19
+
20
+ const headers = {
21
+ 'x-api-key': apiKey,
22
+ 'content-type': 'application/json',
23
+ }
24
+
25
+ let createdProjectId = null
26
+ let createdTaskId = null
27
+ let createdAgentId = null
28
+
29
+ async function call(path, options = {}) {
30
+ const res = await fetch(`${baseUrl}${path}`, options)
31
+ const text = await res.text()
32
+ let body = null
33
+ try {
34
+ body = text ? JSON.parse(text) : null
35
+ } catch {
36
+ body = { raw: text }
37
+ }
38
+ return { res, body }
39
+ }
40
+
41
+ function assertStatus(actual, expected, label) {
42
+ if (actual !== expected) {
43
+ throw new Error(`${label} failed: expected ${expected}, got ${actual}`)
44
+ }
45
+ console.log(`PASS ${label}`)
46
+ }
47
+
48
+ async function run() {
49
+ const login = await call('/api/auth/login', {
50
+ method: 'POST',
51
+ headers: { 'content-type': 'application/json' },
52
+ body: JSON.stringify({ username: authUser, password: authPass }),
53
+ })
54
+ assertStatus(login.res.status, 200, 'login')
55
+
56
+ const workspaces = await call('/api/workspaces', { headers })
57
+ assertStatus(workspaces.res.status, 200, 'GET /api/workspaces')
58
+
59
+ const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
60
+ const ticketPrefix = `S${String(Date.now()).slice(-5)}`
61
+
62
+ const projectCreate = await call('/api/projects', {
63
+ method: 'POST',
64
+ headers,
65
+ body: JSON.stringify({
66
+ name: `staging-smoke-${suffix}`,
67
+ ticket_prefix: ticketPrefix,
68
+ }),
69
+ })
70
+ assertStatus(projectCreate.res.status, 201, 'POST /api/projects')
71
+ createdProjectId = projectCreate.body?.project?.id
72
+ if (!createdProjectId) throw new Error('project id missing')
73
+
74
+ const projectGet = await call(`/api/projects/${createdProjectId}`, { headers })
75
+ assertStatus(projectGet.res.status, 200, 'GET /api/projects/[id]')
76
+
77
+ const projectPatch = await call(`/api/projects/${createdProjectId}`, {
78
+ method: 'PATCH',
79
+ headers,
80
+ body: JSON.stringify({ description: 'staging smoke update' }),
81
+ })
82
+ assertStatus(projectPatch.res.status, 200, 'PATCH /api/projects/[id]')
83
+
84
+ const agentCreate = await call('/api/agents', {
85
+ method: 'POST',
86
+ headers,
87
+ body: JSON.stringify({ name: `smoke-agent-${suffix}`, role: 'tester' }),
88
+ })
89
+ assertStatus(agentCreate.res.status, 201, 'POST /api/agents')
90
+ createdAgentId = agentCreate.body?.agent?.id
91
+
92
+ const assign = await call(`/api/projects/${createdProjectId}/agents`, {
93
+ method: 'POST',
94
+ headers,
95
+ body: JSON.stringify({ agent_name: `smoke-agent-${suffix}`, role: 'member' }),
96
+ })
97
+ assertStatus(assign.res.status, 201, 'POST /api/projects/[id]/agents')
98
+
99
+ const projectTasksCreate = await call('/api/tasks', {
100
+ method: 'POST',
101
+ headers,
102
+ body: JSON.stringify({
103
+ title: `smoke-task-${suffix}`,
104
+ project_id: createdProjectId,
105
+ priority: 'medium',
106
+ status: 'inbox',
107
+ }),
108
+ })
109
+ assertStatus(projectTasksCreate.res.status, 201, 'POST /api/tasks (project scoped)')
110
+ createdTaskId = projectTasksCreate.body?.task?.id
111
+
112
+ const projectTasksGet = await call(`/api/projects/${createdProjectId}/tasks`, { headers })
113
+ assertStatus(projectTasksGet.res.status, 200, 'GET /api/projects/[id]/tasks')
114
+
115
+ const unassign = await call(`/api/projects/${createdProjectId}/agents?agent_name=${encodeURIComponent(`smoke-agent-${suffix}`)}`, {
116
+ method: 'DELETE',
117
+ headers,
118
+ })
119
+ assertStatus(unassign.res.status, 200, 'DELETE /api/projects/[id]/agents')
120
+
121
+ if (createdTaskId) {
122
+ const deleteTask = await call(`/api/tasks/${createdTaskId}`, {
123
+ method: 'DELETE',
124
+ headers,
125
+ })
126
+ assertStatus(deleteTask.res.status, 200, 'DELETE /api/tasks/[id]')
127
+ createdTaskId = null
128
+ }
129
+
130
+ if (createdProjectId) {
131
+ const deleteProject = await call(`/api/projects/${createdProjectId}?mode=delete`, {
132
+ method: 'DELETE',
133
+ headers,
134
+ })
135
+ assertStatus(deleteProject.res.status, 200, 'DELETE /api/projects/[id]?mode=delete')
136
+ createdProjectId = null
137
+ }
138
+
139
+ if (createdAgentId) {
140
+ const deleteAgent = await call(`/api/agents/${createdAgentId}`, {
141
+ method: 'DELETE',
142
+ headers,
143
+ })
144
+ if (deleteAgent.res.status !== 200 && deleteAgent.res.status !== 404) {
145
+ throw new Error(`DELETE /api/agents/[id] cleanup failed: ${deleteAgent.res.status}`)
146
+ }
147
+ createdAgentId = null
148
+ console.log('PASS cleanup agent')
149
+ }
150
+
151
+ console.log(`\nSmoke test passed for ${baseUrl}`)
152
+ }
153
+
154
+ run().catch(async (error) => {
155
+ console.error(`\nSmoke test failed: ${error.message}`)
156
+
157
+ if (createdTaskId) {
158
+ await call(`/api/tasks/${createdTaskId}`, { method: 'DELETE', headers }).catch(() => {})
159
+ }
160
+ if (createdProjectId) {
161
+ await call(`/api/projects/${createdProjectId}?mode=delete`, { method: 'DELETE', headers }).catch(() => {})
162
+ }
163
+ if (createdAgentId) {
164
+ await call(`/api/agents/${createdAgentId}`, { method: 'DELETE', headers }).catch(() => {})
165
+ }
166
+
167
+ process.exit(1)
168
+ })
scripts/start-standalone.sh ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
7
+ STANDALONE_DIR="$PROJECT_ROOT/.next/standalone"
8
+ STANDALONE_NEXT_DIR="$STANDALONE_DIR/.next"
9
+ STANDALONE_STATIC_DIR="$STANDALONE_NEXT_DIR/static"
10
+ SOURCE_STATIC_DIR="$PROJECT_ROOT/.next/static"
11
+ SOURCE_PUBLIC_DIR="$PROJECT_ROOT/public"
12
+ STANDALONE_PUBLIC_DIR="$STANDALONE_DIR/public"
13
+
14
+ if [[ ! -f "$STANDALONE_DIR/server.js" ]]; then
15
+ echo "error: standalone server missing at $STANDALONE_DIR/server.js" >&2
16
+ echo "run 'pnpm build' first" >&2
17
+ exit 1
18
+ fi
19
+
20
+ mkdir -p "$STANDALONE_NEXT_DIR"
21
+
22
+ if [[ -d "$SOURCE_STATIC_DIR" ]]; then
23
+ rm -rf "$STANDALONE_STATIC_DIR"
24
+ cp -R "$SOURCE_STATIC_DIR" "$STANDALONE_STATIC_DIR"
25
+ fi
26
+
27
+ if [[ -d "$SOURCE_PUBLIC_DIR" ]]; then
28
+ rm -rf "$STANDALONE_PUBLIC_DIR"
29
+ cp -R "$SOURCE_PUBLIC_DIR" "$STANDALONE_PUBLIC_DIR"
30
+ fi
31
+
32
+ cd "$STANDALONE_DIR"
33
+ exec node server.js
scripts/station-doctor.sh ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Mission Control Station Doctor
3
+ # Local diagnostics — no auth required, runs on the host.
4
+ #
5
+ # Usage: bash scripts/station-doctor.sh [--port PORT]
6
+
7
+ set -euo pipefail
8
+
9
+ MC_PORT="${1:-3000}"
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
12
+
13
+ # Parse args
14
+ for arg in "$@"; do
15
+ case "$arg" in
16
+ --port) shift; MC_PORT="$1"; shift ;;
17
+ esac
18
+ done
19
+
20
+ PASS=0
21
+ WARN=0
22
+ FAIL=0
23
+
24
+ pass() { echo " [PASS] $1"; ((PASS++)); }
25
+ warn() { echo " [WARN] $1"; ((WARN++)); }
26
+ fail() { echo " [FAIL] $1"; ((FAIL++)); }
27
+ info() { echo " [INFO] $1"; }
28
+
29
+ echo "=== Mission Control Station Doctor ==="
30
+ echo ""
31
+
32
+ # ── 1. Process / Container check ─────────────────────────────────────────────
33
+ echo "--- Service Status ---"
34
+
35
+ RUNNING_IN_DOCKER=false
36
+ if command -v docker &>/dev/null; then
37
+ if docker ps --filter name=mission-control --format '{{.Names}}' 2>/dev/null | grep -q mission-control; then
38
+ RUNNING_IN_DOCKER=true
39
+ health=$(docker inspect mission-control --format '{{.State.Health.Status}}' 2>/dev/null || echo "none")
40
+ if [[ "$health" == "healthy" ]]; then
41
+ pass "Docker container is healthy"
42
+ elif [[ "$health" == "starting" ]]; then
43
+ warn "Docker container is starting"
44
+ else
45
+ fail "Docker container health: $health"
46
+ fi
47
+ fi
48
+ fi
49
+
50
+ if ! $RUNNING_IN_DOCKER; then
51
+ if pgrep -f "node.*server.js" &>/dev/null || pgrep -f "next-server" &>/dev/null; then
52
+ pass "Mission Control process is running"
53
+ else
54
+ fail "Mission Control process not found"
55
+ fi
56
+ fi
57
+
58
+ # ── 2. Port check ────────────────────────────────────────────────────────────
59
+ echo ""
60
+ echo "--- Network ---"
61
+
62
+ if curl -sf "http://localhost:$MC_PORT/login" &>/dev/null; then
63
+ pass "Port $MC_PORT is responding"
64
+ else
65
+ fail "Port $MC_PORT is not responding"
66
+ fi
67
+
68
+ # ── 3. API health ─────────────────────────────────────────────────────────────
69
+ # Try unauthenticated — will get 401 but proves the server is up
70
+ http_code=$(curl -sf -o /dev/null -w "%{http_code}" "http://localhost:$MC_PORT/api/status?action=health" 2>/dev/null || echo "000")
71
+ if [[ "$http_code" == "200" ]]; then
72
+ pass "Health API responding (200)"
73
+ elif [[ "$http_code" == "401" ]]; then
74
+ pass "Health API responding (auth required — expected)"
75
+ elif [[ "$http_code" == "000" ]]; then
76
+ fail "Health API not reachable"
77
+ else
78
+ warn "Health API returned HTTP $http_code"
79
+ fi
80
+
81
+ # ── 4. Disk space ─────────────────────────────────────────────────────────────
82
+ echo ""
83
+ echo "--- Disk ---"
84
+
85
+ usage_pct=$(df -h "$PROJECT_ROOT" 2>/dev/null | tail -1 | awk '{for(i=1;i<=NF;i++) if($i ~ /%/) print $i}' | tr -d '%')
86
+ if [[ -n "$usage_pct" ]]; then
87
+ if [[ "$usage_pct" -lt 85 ]]; then
88
+ pass "Disk usage: ${usage_pct}%"
89
+ elif [[ "$usage_pct" -lt 95 ]]; then
90
+ warn "Disk usage: ${usage_pct}% (getting full)"
91
+ else
92
+ fail "Disk usage: ${usage_pct}% (critical)"
93
+ fi
94
+ fi
95
+
96
+ # ── 5. Database integrity ────────────────────────────────────────────────────
97
+ echo ""
98
+ echo "--- Database ---"
99
+
100
+ DB_PATH="$PROJECT_ROOT/.data/mission-control.db"
101
+ if [[ -f "$DB_PATH" ]]; then
102
+ db_size=$(du -h "$DB_PATH" 2>/dev/null | cut -f1)
103
+ pass "Database exists ($db_size)"
104
+
105
+ # SQLite integrity check
106
+ if command -v sqlite3 &>/dev/null; then
107
+ integrity=$(sqlite3 "$DB_PATH" "PRAGMA integrity_check;" 2>/dev/null || echo "error")
108
+ if [[ "$integrity" == "ok" ]]; then
109
+ pass "Database integrity check passed"
110
+ else
111
+ fail "Database integrity check failed: $integrity"
112
+ fi
113
+
114
+ # WAL mode check
115
+ journal=$(sqlite3 "$DB_PATH" "PRAGMA journal_mode;" 2>/dev/null || echo "unknown")
116
+ if [[ "$journal" == "wal" ]]; then
117
+ pass "WAL mode enabled"
118
+ else
119
+ warn "Journal mode: $journal (WAL recommended)"
120
+ fi
121
+ else
122
+ info "sqlite3 not found — skipping integrity check"
123
+ fi
124
+ else
125
+ if $RUNNING_IN_DOCKER; then
126
+ info "Database is inside Docker volume (cannot check directly)"
127
+ else
128
+ warn "Database not found at $DB_PATH"
129
+ fi
130
+ fi
131
+
132
+ # ── 6. Backup age ────────────────────────────────────────────────────────────
133
+ echo ""
134
+ echo "--- Backups ---"
135
+
136
+ BACKUP_DIR="$PROJECT_ROOT/.data/backups"
137
+ if [[ -d "$BACKUP_DIR" ]]; then
138
+ latest_backup=$(find "$BACKUP_DIR" -name "*.db" -type f 2>/dev/null | sort -r | head -1)
139
+ if [[ -n "$latest_backup" ]]; then
140
+ if [[ "$(uname)" == "Darwin" ]]; then
141
+ backup_age_days=$(( ($(date +%s) - $(stat -f %m "$latest_backup")) / 86400 ))
142
+ else
143
+ backup_age_days=$(( ($(date +%s) - $(stat -c %Y "$latest_backup")) / 86400 ))
144
+ fi
145
+ backup_name=$(basename "$latest_backup")
146
+ if [[ "$backup_age_days" -lt 1 ]]; then
147
+ pass "Latest backup: $backup_name (today)"
148
+ elif [[ "$backup_age_days" -lt 7 ]]; then
149
+ pass "Latest backup: $backup_name (${backup_age_days}d ago)"
150
+ elif [[ "$backup_age_days" -lt 30 ]]; then
151
+ warn "Latest backup: $backup_name (${backup_age_days}d ago — consider more frequent backups)"
152
+ else
153
+ fail "Latest backup: $backup_name (${backup_age_days}d ago — stale!)"
154
+ fi
155
+ else
156
+ warn "No backups found in $BACKUP_DIR"
157
+ fi
158
+ else
159
+ warn "No backup directory at $BACKUP_DIR"
160
+ fi
161
+
162
+ # ── 7. OpenClaw gateway ─────────────────────────────────────────────────────
163
+ echo ""
164
+ echo "--- OpenClaw Gateway ---"
165
+
166
+ GW_HOST="${OPENCLAW_GATEWAY_HOST:-127.0.0.1}"
167
+ GW_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
168
+
169
+ if nc -z "$GW_HOST" "$GW_PORT" 2>/dev/null || (echo > "/dev/tcp/$GW_HOST/$GW_PORT") 2>/dev/null; then
170
+ pass "Gateway reachable at $GW_HOST:$GW_PORT"
171
+ else
172
+ info "Gateway not reachable at $GW_HOST:$GW_PORT"
173
+ fi
174
+
175
+ # ── Summary ──────────────────────────────────────────────────────────────────
176
+ echo ""
177
+ TOTAL=$((PASS + WARN + FAIL))
178
+ echo "=== Results: $PASS passed, $WARN warnings, $FAIL failures (of $TOTAL checks) ==="
179
+
180
+ if [[ $FAIL -gt 0 ]]; then
181
+ echo "Status: UNHEALTHY"
182
+ exit 1
183
+ elif [[ $WARN -gt 0 ]]; then
184
+ echo "Status: DEGRADED"
185
+ exit 0
186
+ else
187
+ echo "Status: HEALTHY"
188
+ exit 0
189
+ fi
skills/mission-control-installer/README.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mission Control Installer Skill
2
+
3
+ Install and configure Mission Control on any Linux or macOS system.
4
+
5
+ ## What This Skill Does
6
+
7
+ 1. Detects the target OS and available runtimes (Docker or Node.js 20+)
8
+ 2. Clones or updates the Mission Control repository
9
+ 3. Generates a secure `.env` with random credentials
10
+ 4. Starts the dashboard via Docker Compose or local Node.js
11
+ 5. Runs an OpenClaw fleet health check (cleans stale PIDs, old logs, validates gateway)
12
+ 6. Prints the access URL and admin credentials
13
+
14
+ ## Usage
15
+
16
+ Run the installer script:
17
+
18
+ ```bash
19
+ # Auto-detect deployment mode (prefers Docker)
20
+ bash install.sh
21
+
22
+ # Force Docker deployment
23
+ bash install.sh --docker
24
+
25
+ # Force local deployment (Node.js + pnpm)
26
+ bash install.sh --local
27
+
28
+ # Custom port
29
+ bash install.sh --port 8080
30
+
31
+ # Skip OpenClaw fleet check
32
+ bash install.sh --skip-openclaw
33
+ ```
34
+
35
+ Or as a one-liner:
36
+
37
+ ```bash
38
+ curl -fsSL https://raw.githubusercontent.com/builderz-labs/mission-control/main/install.sh | bash
39
+ ```
40
+
41
+ ## Prerequisites
42
+
43
+ - **Docker mode**: Docker Engine with Docker Compose v2
44
+ - **Local mode**: Node.js 20+, pnpm (auto-installed via corepack if missing)
45
+ - **Both**: git (to clone the repository)
46
+
47
+ ## Post-Install
48
+
49
+ After installation:
50
+
51
+ 1. Open `http://localhost:3000` (or your configured port)
52
+ 2. Log in with the credentials printed by the installer (also in `.env`)
53
+ 3. Configure your OpenClaw gateway connection in Settings
54
+ 4. Register agents via the Agents panel
55
+
56
+ ## Environment Configuration
57
+
58
+ The installer generates a `.env` from `.env.example` with secure random values for:
59
+
60
+ - `AUTH_PASS` — 24-character random password
61
+ - `API_KEY` — 32-character hex API key
62
+ - `AUTH_SECRET` — 32-character session secret
63
+
64
+ To regenerate credentials independently:
65
+
66
+ ```bash
67
+ bash scripts/generate-env.sh --force
68
+ ```
skills/mission-control-installer/skill.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "mission-control-installer",
3
+ "version": "1.0.0",
4
+ "description": "Install and configure Mission Control — the OpenClaw agent orchestration dashboard",
5
+ "author": "Builderz Labs",
6
+ "license": "MIT",
7
+ "tools": ["exec", "fs"],
8
+ "parameters": {
9
+ "deployment_mode": {
10
+ "type": "string",
11
+ "enum": ["docker", "local"],
12
+ "default": "docker",
13
+ "description": "How to deploy Mission Control"
14
+ },
15
+ "port": {
16
+ "type": "number",
17
+ "default": 3000,
18
+ "description": "Port for the Mission Control dashboard"
19
+ },
20
+ "install_dir": {
21
+ "type": "string",
22
+ "default": "",
23
+ "description": "Installation directory (defaults to ./mission-control)"
24
+ }
25
+ },
26
+ "tags": ["mission-control", "dashboard", "installer", "docker"]
27
+ }
skills/mission-control-manage/README.md ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mission Control Management Skill
2
+
3
+ Manage a running Mission Control instance programmatically.
4
+
5
+ ## API Endpoints
6
+
7
+ All endpoints require authentication via `x-api-key` header or session cookie.
8
+
9
+ ### Health Check
10
+
11
+ ```bash
12
+ # Quick health status
13
+ curl -H "x-api-key: $API_KEY" http://localhost:3000/api/status?action=health
14
+
15
+ # Response: { "status": "healthy", "version": "1.3.0", "checks": [...] }
16
+ ```
17
+
18
+ Possible statuses: `healthy`, `degraded`, `unhealthy`
19
+
20
+ ### System Overview
21
+
22
+ ```bash
23
+ # Full system status (memory, disk, sessions, processes)
24
+ curl -H "x-api-key: $API_KEY" http://localhost:3000/api/status?action=overview
25
+ ```
26
+
27
+ ### Diagnostics (Admin Only)
28
+
29
+ ```bash
30
+ # Comprehensive diagnostics including security posture
31
+ curl -H "x-api-key: $API_KEY" http://localhost:3000/api/diagnostics
32
+
33
+ # Response includes:
34
+ # - system: node version, platform, memory, docker detection
35
+ # - security: score (0-100) with individual checks
36
+ # - database: size, WAL mode, migration version
37
+ # - gateway: configured, reachable, host/port
38
+ # - agents: total count, by status
39
+ # - retention: configured retention policies
40
+ ```
41
+
42
+ ### Check for Updates
43
+
44
+ ```bash
45
+ curl -H "x-api-key: $API_KEY" http://localhost:3000/api/releases/check
46
+
47
+ # Response: { "updateAvailable": true, "currentVersion": "1.3.0", "latestVersion": "1.4.0", ... }
48
+ ```
49
+
50
+ ### Trigger Update
51
+
52
+ ```bash
53
+ # Apply available update (bare-metal only; Docker returns instructions)
54
+ curl -X POST -H "x-api-key: $API_KEY" http://localhost:3000/api/releases/update
55
+ ```
56
+
57
+ ### Database Backup
58
+
59
+ ```bash
60
+ curl -X POST -H "x-api-key: $API_KEY" http://localhost:3000/api/backup
61
+ ```
62
+
63
+ ### Agent Management
64
+
65
+ ```bash
66
+ # List agents
67
+ curl -H "x-api-key: $API_KEY" http://localhost:3000/api/agents
68
+
69
+ # Register an agent
70
+ curl -X POST -H "x-api-key: $API_KEY" \
71
+ -H "Content-Type: application/json" \
72
+ -d '{"name": "my-agent", "type": "openclaw"}' \
73
+ http://localhost:3000/api/agents
74
+ ```
75
+
76
+ ## Station Doctor
77
+
78
+ For local diagnostics without API access:
79
+
80
+ ```bash
81
+ bash scripts/station-doctor.sh
82
+ ```
83
+
84
+ Checks: Docker health, port availability, disk space, DB integrity, backup age.
85
+
86
+ ## Common Workflows
87
+
88
+ ### Automated Health Monitoring
89
+
90
+ ```bash
91
+ # Check health and alert if unhealthy
92
+ STATUS=$(curl -sf -H "x-api-key: $API_KEY" http://localhost:3000/api/status?action=health | jq -r '.status')
93
+ if [ "$STATUS" != "healthy" ]; then
94
+ echo "ALERT: Mission Control is $STATUS"
95
+ fi
96
+ ```
97
+
98
+ ### Pre-Upgrade Checklist
99
+
100
+ 1. Check for updates: `GET /api/releases/check`
101
+ 2. Create backup: `POST /api/backup`
102
+ 3. Run diagnostics: `GET /api/diagnostics` (verify no active tasks)
103
+ 4. Apply update: `POST /api/releases/update` (or `docker pull` + recreate for Docker)
104
+ 5. Verify health: `GET /api/status?action=health`
skills/mission-control-manage/skill.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "mission-control-manage",
3
+ "version": "1.0.0",
4
+ "description": "Manage a running Mission Control instance — health checks, diagnostics, upgrades, backups",
5
+ "author": "Builderz Labs",
6
+ "license": "MIT",
7
+ "tools": ["exec", "http"],
8
+ "parameters": {
9
+ "base_url": {
10
+ "type": "string",
11
+ "default": "http://localhost:3000",
12
+ "description": "Mission Control base URL"
13
+ },
14
+ "api_key": {
15
+ "type": "string",
16
+ "description": "API key for authentication (x-api-key header)"
17
+ }
18
+ },
19
+ "tags": ["mission-control", "management", "health", "upgrade", "backup"]
20
+ }
src/app/[[...panel]]/page.tsx CHANGED
@@ -1,18 +1,15 @@
1
  'use client'
2
 
3
- import { useEffect, useState } from 'react'
4
  import { usePathname, useRouter } from 'next/navigation'
5
  import { NavRail } from '@/components/layout/nav-rail'
6
  import { HeaderBar } from '@/components/layout/header-bar'
7
  import { LiveFeed } from '@/components/layout/live-feed'
8
  import { Dashboard } from '@/components/dashboard/dashboard'
9
- import { AgentSpawnPanel } from '@/components/panels/agent-spawn-panel'
10
  import { LogViewerPanel } from '@/components/panels/log-viewer-panel'
11
  import { CronManagementPanel } from '@/components/panels/cron-management-panel'
12
  import { MemoryBrowserPanel } from '@/components/panels/memory-browser-panel'
13
- import { TokenDashboardPanel } from '@/components/panels/token-dashboard-panel'
14
- import { AgentCostPanel } from '@/components/panels/agent-cost-panel'
15
- import { SessionDetailsPanel } from '@/components/panels/session-details-panel'
16
  import { TaskBoardPanel } from '@/components/panels/task-board-panel'
17
  import { ActivityFeedPanel } from '@/components/panels/activity-feed-panel'
18
  import { AgentSquadPanelPhase3 } from '@/components/panels/agent-squad-panel-phase3'
@@ -22,7 +19,6 @@ import { OrchestrationBar } from '@/components/panels/orchestration-bar'
22
  import { NotificationsPanel } from '@/components/panels/notifications-panel'
23
  import { UserManagementPanel } from '@/components/panels/user-management-panel'
24
  import { AuditTrailPanel } from '@/components/panels/audit-trail-panel'
25
- import { AgentHistoryPanel } from '@/components/panels/agent-history-panel'
26
  import { WebhookPanel } from '@/components/panels/webhook-panel'
27
  import { SettingsPanel } from '@/components/panels/settings-panel'
28
  import { GatewayConfigPanel } from '@/components/panels/gateway-config-panel'
@@ -32,36 +28,176 @@ import { MultiGatewayPanel } from '@/components/panels/multi-gateway-panel'
32
  import { SuperAdminPanel } from '@/components/panels/super-admin-panel'
33
  import { OfficePanel } from '@/components/panels/office-panel'
34
  import { GitHubSyncPanel } from '@/components/panels/github-sync-panel'
35
- import { DocumentsPanel } from '@/components/panels/documents-panel'
 
 
 
 
 
 
 
36
  import { ChatPanel } from '@/components/chat/chat-panel'
 
37
  import { ErrorBoundary } from '@/components/ErrorBoundary'
38
  import { LocalModeBanner } from '@/components/layout/local-mode-banner'
39
  import { UpdateBanner } from '@/components/layout/update-banner'
40
- import { PromoBanner } from '@/components/layout/promo-banner'
 
 
 
 
 
41
  import { useWebSocket } from '@/lib/websocket'
42
  import { useServerEvents } from '@/lib/use-server-events'
 
 
 
 
43
  import { useMissionControl } from '@/store'
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  export default function Home() {
46
  const router = useRouter()
47
  const { connect } = useWebSocket()
48
- const { activeTab, setActiveTab, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable, liveFeedOpen, toggleLiveFeed } = useMissionControl()
49
 
50
  // Sync URL → Zustand activeTab
51
  const pathname = usePathname()
52
  const panelFromUrl = pathname === '/' ? 'overview' : pathname.slice(1)
 
53
 
54
  useEffect(() => {
55
- setActiveTab(panelFromUrl)
56
- }, [panelFromUrl, setActiveTab])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  // Connect to SSE for real-time local DB events (tasks, agents, chat, etc.)
59
  useServerEvents()
60
  const [isClient, setIsClient] = useState(false)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  useEffect(() => {
63
  setIsClient(true)
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  // Fetch current user
66
  fetch('/api/auth/me')
67
  .then(async (res) => {
@@ -71,8 +207,8 @@ export default function Home() {
71
  }
72
  return null
73
  })
74
- .then(data => { if (data?.user) setCurrentUser(data.user) })
75
- .catch(() => {})
76
 
77
  // Check for available updates
78
  fetch('/api/releases/check')
@@ -88,16 +224,41 @@ export default function Home() {
88
  })
89
  .catch(() => {})
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  // Check capabilities, then conditionally connect to gateway
92
  fetch('/api/status?action=capabilities')
93
  .then(res => res.ok ? res.json() : null)
94
- .then(data => {
95
  if (data?.subscription) {
96
  setSubscription(data.subscription)
97
  }
 
 
 
 
 
 
98
  if (data && data.gateway === false) {
99
  setDashboardMode('local')
100
  setGatewayAvailable(false)
 
 
 
101
  // Skip WebSocket connect — no gateway to talk to
102
  return
103
  }
@@ -105,45 +266,86 @@ export default function Home() {
105
  setDashboardMode('full')
106
  setGatewayAvailable(true)
107
  }
108
- // Connect to gateway WebSocket
109
- const wsToken = process.env.NEXT_PUBLIC_GATEWAY_TOKEN || process.env.NEXT_PUBLIC_WS_TOKEN || ''
110
- const explicitWsUrl = process.env.NEXT_PUBLIC_GATEWAY_URL || ''
111
- const gatewayPort = process.env.NEXT_PUBLIC_GATEWAY_PORT || '18789'
112
- const gatewayHost = process.env.NEXT_PUBLIC_GATEWAY_HOST || window.location.hostname
113
- const gatewayProto =
114
- process.env.NEXT_PUBLIC_GATEWAY_PROTOCOL ||
115
- (window.location.protocol === 'https:' ? 'wss' : 'ws')
116
- const wsUrl = explicitWsUrl || `${gatewayProto}://${gatewayHost}:${gatewayPort}`
117
- connect(wsUrl, wsToken)
118
  })
119
  .catch(() => {
120
  // If capabilities check fails, still try to connect
121
- const wsToken = process.env.NEXT_PUBLIC_GATEWAY_TOKEN || process.env.NEXT_PUBLIC_WS_TOKEN || ''
122
- const explicitWsUrl = process.env.NEXT_PUBLIC_GATEWAY_URL || ''
123
- const gatewayPort = process.env.NEXT_PUBLIC_GATEWAY_PORT || '18789'
124
- const gatewayHost = process.env.NEXT_PUBLIC_GATEWAY_HOST || window.location.hostname
125
- const gatewayProto =
126
- process.env.NEXT_PUBLIC_GATEWAY_PROTOCOL ||
127
- (window.location.protocol === 'https:' ? 'wss' : 'ws')
128
- const wsUrl = explicitWsUrl || `${gatewayProto}://${gatewayHost}:${gatewayPort}`
129
- connect(wsUrl, wsToken)
130
  })
131
- }, [connect, pathname, router, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable])
132
 
133
- if (!isClient) {
134
- return (
135
- <div className="flex items-center justify-center min-h-screen">
136
- <div className="flex flex-col items-center gap-3">
137
- <div className="w-10 h-10 rounded-xl bg-primary flex items-center justify-center">
138
- <span className="text-primary-foreground font-bold text-sm">MC</span>
139
- </div>
140
- <div className="flex items-center gap-2">
141
- <div className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
142
- <span className="text-sm text-muted-foreground">Loading Mission Control...</span>
143
- </div>
144
- </div>
145
- </div>
146
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  }
148
 
149
  return (
@@ -151,33 +353,49 @@ export default function Home() {
151
  <a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:top-2 focus:left-2 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-md focus:text-sm focus:font-medium">
152
  Skip to main content
153
  </a>
 
154
  {/* Left: Icon rail navigation (hidden on mobile, shown as bottom bar instead) */}
155
- <NavRail />
156
 
157
  {/* Center: Header + Content */}
158
  <div className="flex-1 flex flex-col min-w-0">
159
- <HeaderBar />
160
- <LocalModeBanner />
161
- <UpdateBanner />
162
- <PromoBanner />
163
- <main id="main-content" className="flex-1 overflow-auto pb-16 md:pb-0" role="main">
164
- <div aria-live="polite">
 
 
 
 
 
 
 
 
 
 
165
  <ErrorBoundary key={activeTab}>
166
  <ContentRouter tab={activeTab} />
167
  </ErrorBoundary>
168
  </div>
 
 
 
 
 
169
  </main>
170
  </div>
171
 
172
  {/* Right: Live feed (hidden on mobile) */}
173
- {liveFeedOpen && (
174
  <div className="hidden lg:flex h-full">
175
  <LiveFeed />
176
  </div>
177
  )}
178
 
179
  {/* Floating button to reopen LiveFeed when closed */}
180
- {!liveFeedOpen && (
181
  <button
182
  onClick={toggleLiveFeed}
183
  className="hidden lg:flex fixed right-0 top-1/2 -translate-y-1/2 z-30 w-6 h-12 items-center justify-center bg-card border border-r-0 border-border rounded-l-md text-muted-foreground hover:text-foreground hover:bg-secondary transition-all duration-200"
@@ -190,22 +408,70 @@ export default function Home() {
190
  )}
191
 
192
  {/* Chat panel overlay */}
193
- <ChatPanel />
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  </div>
195
  )
196
  }
197
 
 
 
 
 
198
  function ContentRouter({ tab }: { tab: string }) {
199
- const { dashboardMode } = useMissionControl()
 
200
  const isLocal = dashboardMode === 'local'
201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  switch (tab) {
203
  case 'overview':
204
  return (
205
  <>
206
  <Dashboard />
207
  {!isLocal && (
208
- <div className="mt-4 mx-4 mb-4 rounded-xl border border-border bg-card overflow-hidden">
209
  <AgentCommsPanel />
210
  </div>
211
  )}
@@ -217,38 +483,31 @@ function ContentRouter({ tab }: { tab: string }) {
217
  return (
218
  <>
219
  <OrchestrationBar />
 
220
  <AgentSquadPanelPhase3 />
221
- {!isLocal && (
222
- <div className="mt-4 mx-4 mb-4 rounded-xl border border-border bg-card overflow-hidden">
223
- <AgentCommsPanel />
224
- </div>
225
- )}
226
  </>
227
  )
228
- case 'activity':
229
- return <ActivityFeedPanel />
230
  case 'notifications':
231
  return <NotificationsPanel />
232
  case 'standup':
233
  return <StandupPanel />
234
- case 'spawn':
235
- return <AgentSpawnPanel />
236
  case 'sessions':
237
- return <SessionDetailsPanel />
238
  case 'logs':
239
  return <LogViewerPanel />
240
  case 'cron':
241
  return <CronManagementPanel />
242
  case 'memory':
243
  return <MemoryBrowserPanel />
 
244
  case 'tokens':
245
- return <TokenDashboardPanel />
246
  case 'agent-costs':
247
- return <AgentCostPanel />
248
  case 'users':
249
  return <UserManagementPanel />
250
  case 'history':
251
- return <AgentHistoryPanel />
 
252
  case 'audit':
253
  return <AuditTrailPanel />
254
  case 'webhooks':
@@ -256,24 +515,53 @@ function ContentRouter({ tab }: { tab: string }) {
256
  case 'alerts':
257
  return <AlertRulesPanel />
258
  case 'gateways':
 
259
  return <MultiGatewayPanel />
260
  case 'gateway-config':
 
261
  return <GatewayConfigPanel />
262
  case 'integrations':
263
  return <IntegrationsPanel />
264
  case 'settings':
265
  return <SettingsPanel />
 
 
266
  case 'github':
267
  return <GitHubSyncPanel />
268
  case 'office':
269
  return <OfficePanel />
270
- case 'documents':
271
- return <DocumentsPanel />
272
- case 'super-admin':
273
- return <SuperAdminPanel />
274
- case 'workspaces':
275
- return <SuperAdminPanel />
276
- default:
277
- return <Dashboard />
 
 
 
 
 
 
 
 
 
 
 
 
278
  }
279
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  'use client'
2
 
3
+ import { createElement, useEffect, useState } from 'react'
4
  import { usePathname, useRouter } from 'next/navigation'
5
  import { NavRail } from '@/components/layout/nav-rail'
6
  import { HeaderBar } from '@/components/layout/header-bar'
7
  import { LiveFeed } from '@/components/layout/live-feed'
8
  import { Dashboard } from '@/components/dashboard/dashboard'
 
9
  import { LogViewerPanel } from '@/components/panels/log-viewer-panel'
10
  import { CronManagementPanel } from '@/components/panels/cron-management-panel'
11
  import { MemoryBrowserPanel } from '@/components/panels/memory-browser-panel'
12
+ import { CostTrackerPanel } from '@/components/panels/cost-tracker-panel'
 
 
13
  import { TaskBoardPanel } from '@/components/panels/task-board-panel'
14
  import { ActivityFeedPanel } from '@/components/panels/activity-feed-panel'
15
  import { AgentSquadPanelPhase3 } from '@/components/panels/agent-squad-panel-phase3'
 
19
  import { NotificationsPanel } from '@/components/panels/notifications-panel'
20
  import { UserManagementPanel } from '@/components/panels/user-management-panel'
21
  import { AuditTrailPanel } from '@/components/panels/audit-trail-panel'
 
22
  import { WebhookPanel } from '@/components/panels/webhook-panel'
23
  import { SettingsPanel } from '@/components/panels/settings-panel'
24
  import { GatewayConfigPanel } from '@/components/panels/gateway-config-panel'
 
28
  import { SuperAdminPanel } from '@/components/panels/super-admin-panel'
29
  import { OfficePanel } from '@/components/panels/office-panel'
30
  import { GitHubSyncPanel } from '@/components/panels/github-sync-panel'
31
+ import { SkillsPanel } from '@/components/panels/skills-panel'
32
+ import { LocalAgentsDocPanel } from '@/components/panels/local-agents-doc-panel'
33
+ import { ChannelsPanel } from '@/components/panels/channels-panel'
34
+ import { DebugPanel } from '@/components/panels/debug-panel'
35
+ import { SecurityAuditPanel } from '@/components/panels/security-audit-panel'
36
+ import { NodesPanel } from '@/components/panels/nodes-panel'
37
+ import { ExecApprovalPanel } from '@/components/panels/exec-approval-panel'
38
+ import { ChatPagePanel } from '@/components/panels/chat-page-panel'
39
  import { ChatPanel } from '@/components/chat/chat-panel'
40
+ import { getPluginPanel } from '@/lib/plugins'
41
  import { ErrorBoundary } from '@/components/ErrorBoundary'
42
  import { LocalModeBanner } from '@/components/layout/local-mode-banner'
43
  import { UpdateBanner } from '@/components/layout/update-banner'
44
+ import { OpenClawUpdateBanner } from '@/components/layout/openclaw-update-banner'
45
+ import { OpenClawDoctorBanner } from '@/components/layout/openclaw-doctor-banner'
46
+ import { OnboardingWizard } from '@/components/onboarding/onboarding-wizard'
47
+ import { Loader } from '@/components/ui/loader'
48
+ import { ProjectManagerModal } from '@/components/modals/project-manager-modal'
49
+ import { ExecApprovalOverlay } from '@/components/modals/exec-approval-overlay'
50
  import { useWebSocket } from '@/lib/websocket'
51
  import { useServerEvents } from '@/lib/use-server-events'
52
+ import { completeNavigationTiming } from '@/lib/navigation-metrics'
53
+ import { panelHref, useNavigateToPanel } from '@/lib/navigation'
54
+ import { clearOnboardingDismissedThisSession, clearOnboardingReplayFromStart, getOnboardingSessionDecision, markOnboardingReplayFromStart, readOnboardingDismissedThisSession } from '@/lib/onboarding-session'
55
+ import { Button } from '@/components/ui/button'
56
  import { useMissionControl } from '@/store'
57
 
58
+ interface GatewaySummary {
59
+ id: number
60
+ is_primary: number
61
+ }
62
+
63
+ function renderPluginPanel(panelId: string) {
64
+ const pluginPanel = getPluginPanel(panelId)
65
+ return pluginPanel ? createElement(pluginPanel) : <Dashboard />
66
+ }
67
+
68
+ function isLocalHost(hostname: string): boolean {
69
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
70
+ }
71
+
72
  export default function Home() {
73
  const router = useRouter()
74
  const { connect } = useWebSocket()
75
+ const { activeTab, setActiveTab, setCurrentUser, setDashboardMode, setGatewayAvailable, setCapabilitiesChecked, setSubscription, setDefaultOrgName, setUpdateAvailable, setOpenclawUpdate, showOnboarding, setShowOnboarding, liveFeedOpen, toggleLiveFeed, showProjectManagerModal, setShowProjectManagerModal, fetchProjects, setChatPanelOpen, bootComplete, setBootComplete, setAgents, setSessions, setProjects, setInterfaceMode, setMemoryGraphAgents, setSkillsData } = useMissionControl()
76
 
77
  // Sync URL → Zustand activeTab
78
  const pathname = usePathname()
79
  const panelFromUrl = pathname === '/' ? 'overview' : pathname.slice(1)
80
+ const normalizedPanel = panelFromUrl === 'sessions' ? 'chat' : panelFromUrl
81
 
82
  useEffect(() => {
83
+ completeNavigationTiming(pathname)
84
+ }, [pathname])
85
+
86
+ useEffect(() => {
87
+ completeNavigationTiming(panelHref(activeTab))
88
+ }, [activeTab])
89
+
90
+ useEffect(() => {
91
+ setActiveTab(normalizedPanel)
92
+ if (normalizedPanel === 'chat') {
93
+ setChatPanelOpen(false)
94
+ }
95
+ if (panelFromUrl === 'sessions') {
96
+ router.replace('/chat')
97
+ }
98
+ }, [panelFromUrl, normalizedPanel, router, setActiveTab, setChatPanelOpen])
99
 
100
  // Connect to SSE for real-time local DB events (tasks, agents, chat, etc.)
101
  useServerEvents()
102
  const [isClient, setIsClient] = useState(false)
103
+ const [initSteps, setInitSteps] = useState<Array<{ key: string; label: string; status: 'pending' | 'done' }>>([
104
+ { key: 'auth', label: 'Authenticating operator', status: 'pending' },
105
+ { key: 'capabilities', label: 'Detecting station mode', status: 'pending' },
106
+ { key: 'config', label: 'Loading control config', status: 'pending' },
107
+ { key: 'connect', label: 'Connecting runtime links', status: 'pending' },
108
+ { key: 'agents', label: 'Syncing agent registry', status: 'pending' },
109
+ { key: 'sessions', label: 'Loading active sessions', status: 'pending' },
110
+ { key: 'projects', label: 'Hydrating workspace board', status: 'pending' },
111
+ { key: 'memory', label: 'Mapping memory graph', status: 'pending' },
112
+ { key: 'skills', label: 'Indexing skill catalog', status: 'pending' },
113
+ ])
114
+
115
+ const markStep = (key: string) => {
116
+ setInitSteps(prev => prev.map(s => s.key === key ? { ...s, status: 'done' } : s))
117
+ }
118
+
119
+ useEffect(() => {
120
+ if (!bootComplete && initSteps.every(s => s.status === 'done')) {
121
+ const t = setTimeout(() => setBootComplete(), 400)
122
+ return () => clearTimeout(t)
123
+ }
124
+ }, [initSteps, bootComplete, setBootComplete])
125
+
126
+ // Security console warning (anti-self-XSS)
127
+ useEffect(() => {
128
+ if (!bootComplete) return
129
+ if (typeof window === 'undefined') return
130
+ const key = 'mc-console-warning'
131
+ if (sessionStorage.getItem(key)) return
132
+ sessionStorage.setItem(key, '1')
133
+
134
+ console.log(
135
+ '%c Stop! ',
136
+ 'color: #fff; background: #e53e3e; font-size: 40px; font-weight: bold; padding: 4px 16px; border-radius: 4px;'
137
+ )
138
+ console.log(
139
+ '%cThis is a browser feature intended for developers.\n\nIf someone told you to copy-paste something here to enable a feature or "hack" an account, it is a scam and will give them access to your account.',
140
+ 'font-size: 14px; color: #e2e8f0; padding: 8px 0;'
141
+ )
142
+ console.log(
143
+ '%cLearn more: https://en.wikipedia.org/wiki/Self-XSS',
144
+ 'font-size: 12px; color: #718096;'
145
+ )
146
+ }, [bootComplete])
147
 
148
  useEffect(() => {
149
  setIsClient(true)
150
 
151
+ // OpenClaw control-ui device identity requires a secure browser context.
152
+ // Redirect remote HTTP sessions to HTTPS automatically to avoid handshake failures.
153
+ if (window.location.protocol === 'http:' && !isLocalHost(window.location.hostname)) {
154
+ const secureUrl = new URL(window.location.href)
155
+ secureUrl.protocol = 'https:'
156
+ window.location.replace(secureUrl.toString())
157
+ return
158
+ }
159
+
160
+ const connectWithEnvFallback = () => {
161
+ const explicitWsUrl = process.env.NEXT_PUBLIC_GATEWAY_URL || ''
162
+ const gatewayPort = process.env.NEXT_PUBLIC_GATEWAY_PORT || '18789'
163
+ const gatewayHost = process.env.NEXT_PUBLIC_GATEWAY_HOST || window.location.hostname
164
+ const gatewayProto =
165
+ process.env.NEXT_PUBLIC_GATEWAY_PROTOCOL ||
166
+ (window.location.protocol === 'https:' ? 'wss' : 'ws')
167
+ const wsUrl = explicitWsUrl || `${gatewayProto}://${gatewayHost}:${gatewayPort}`
168
+ connect(wsUrl)
169
+ }
170
+
171
+ const connectWithPrimaryGateway = async (): Promise<{ attempted: boolean; connected: boolean }> => {
172
+ try {
173
+ const gatewaysRes = await fetch('/api/gateways')
174
+ if (!gatewaysRes.ok) return { attempted: false, connected: false }
175
+ const gatewaysJson = await gatewaysRes.json().catch(() => ({}))
176
+ const gateways = Array.isArray(gatewaysJson?.gateways) ? gatewaysJson.gateways as GatewaySummary[] : []
177
+ if (gateways.length === 0) return { attempted: false, connected: false }
178
+
179
+ const primaryGateway = gateways.find(gw => Number(gw?.is_primary) === 1) || gateways[0]
180
+ if (!primaryGateway?.id) return { attempted: true, connected: false }
181
+
182
+ const connectRes = await fetch('/api/gateways/connect', {
183
+ method: 'POST',
184
+ headers: { 'Content-Type': 'application/json' },
185
+ body: JSON.stringify({ id: primaryGateway.id }),
186
+ })
187
+ if (!connectRes.ok) return { attempted: true, connected: false }
188
+
189
+ const payload = await connectRes.json().catch(() => ({}))
190
+ const wsUrl = typeof payload?.ws_url === 'string' ? payload.ws_url : ''
191
+ const wsToken = typeof payload?.token === 'string' ? payload.token : ''
192
+ if (!wsUrl) return { attempted: true, connected: false }
193
+
194
+ connect(wsUrl, wsToken)
195
+ return { attempted: true, connected: true }
196
+ } catch {
197
+ return { attempted: false, connected: false }
198
+ }
199
+ }
200
+
201
  // Fetch current user
202
  fetch('/api/auth/me')
203
  .then(async (res) => {
 
207
  }
208
  return null
209
  })
210
+ .then(data => { if (data?.user) setCurrentUser(data.user); markStep('auth') })
211
+ .catch(() => { markStep('auth') })
212
 
213
  // Check for available updates
214
  fetch('/api/releases/check')
 
224
  })
225
  .catch(() => {})
226
 
227
+ // Check for OpenClaw updates
228
+ fetch('/api/openclaw/version')
229
+ .then(res => res.ok ? res.json() : null)
230
+ .then(data => {
231
+ if (data?.updateAvailable) {
232
+ setOpenclawUpdate({
233
+ installed: data.installed,
234
+ latest: data.latest,
235
+ releaseUrl: data.releaseUrl,
236
+ releaseNotes: data.releaseNotes,
237
+ updateCommand: data.updateCommand,
238
+ })
239
+ }
240
+ })
241
+ .catch(() => {})
242
+
243
  // Check capabilities, then conditionally connect to gateway
244
  fetch('/api/status?action=capabilities')
245
  .then(res => res.ok ? res.json() : null)
246
+ .then(async data => {
247
  if (data?.subscription) {
248
  setSubscription(data.subscription)
249
  }
250
+ if (data?.processUser) {
251
+ setDefaultOrgName(data.processUser)
252
+ }
253
+ if (data?.interfaceMode === 'essential' || data?.interfaceMode === 'full') {
254
+ setInterfaceMode(data.interfaceMode)
255
+ }
256
  if (data && data.gateway === false) {
257
  setDashboardMode('local')
258
  setGatewayAvailable(false)
259
+ setCapabilitiesChecked(true)
260
+ markStep('capabilities')
261
+ markStep('connect')
262
  // Skip WebSocket connect — no gateway to talk to
263
  return
264
  }
 
266
  setDashboardMode('full')
267
  setGatewayAvailable(true)
268
  }
269
+ setCapabilitiesChecked(true)
270
+ markStep('capabilities')
271
+
272
+ const primaryConnect = await connectWithPrimaryGateway()
273
+ if (!primaryConnect.connected && !primaryConnect.attempted) {
274
+ connectWithEnvFallback()
275
+ }
276
+ markStep('connect')
 
 
277
  })
278
  .catch(() => {
279
  // If capabilities check fails, still try to connect
280
+ setCapabilitiesChecked(true)
281
+ markStep('capabilities')
282
+ markStep('connect')
283
+ connectWithEnvFallback()
 
 
 
 
 
284
  })
 
285
 
286
+ // Check onboarding state
287
+ fetch('/api/onboarding')
288
+ .then(res => res.ok ? res.json() : null)
289
+ .then(data => {
290
+ const decision = getOnboardingSessionDecision({
291
+ isAdmin: data?.isAdmin === true,
292
+ serverShowOnboarding: data?.showOnboarding === true,
293
+ completed: data?.completed === true,
294
+ skipped: data?.skipped === true,
295
+ dismissedThisSession: readOnboardingDismissedThisSession(),
296
+ })
297
+
298
+ if (decision.shouldOpen) {
299
+ clearOnboardingDismissedThisSession()
300
+ if (decision.replayFromStart) {
301
+ markOnboardingReplayFromStart()
302
+ } else {
303
+ clearOnboardingReplayFromStart()
304
+ }
305
+ setShowOnboarding(true)
306
+ }
307
+ markStep('config')
308
+ })
309
+ .catch(() => { markStep('config') })
310
+ // Preload workspace data in parallel
311
+ Promise.allSettled([
312
+ fetch('/api/agents')
313
+ .then(r => r.ok ? r.json() : null)
314
+ .then((agentsData) => {
315
+ if (agentsData?.agents) setAgents(agentsData.agents)
316
+ })
317
+ .finally(() => { markStep('agents') }),
318
+ fetch('/api/sessions')
319
+ .then(r => r.ok ? r.json() : null)
320
+ .then((sessionsData) => {
321
+ if (sessionsData?.sessions) setSessions(sessionsData.sessions)
322
+ })
323
+ .finally(() => { markStep('sessions') }),
324
+ fetch('/api/projects')
325
+ .then(r => r.ok ? r.json() : null)
326
+ .then((projectsData) => {
327
+ if (projectsData?.projects) setProjects(projectsData.projects)
328
+ })
329
+ .finally(() => { markStep('projects') }),
330
+ fetch('/api/memory/graph?agent=all')
331
+ .then(r => r.ok ? r.json() : null)
332
+ .then((graphData) => {
333
+ if (graphData?.agents) setMemoryGraphAgents(graphData.agents)
334
+ })
335
+ .finally(() => { markStep('memory') }),
336
+ fetch('/api/skills')
337
+ .then(r => r.ok ? r.json() : null)
338
+ .then((skillsData) => {
339
+ if (skillsData?.skills) setSkillsData(skillsData.skills, skillsData.groups || [], skillsData.total || 0)
340
+ })
341
+ .finally(() => { markStep('skills') }),
342
+ ]).catch(() => { /* panels will lazy-load as fallback */ })
343
+
344
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- boot once on mount, not on every pathname change
345
+ }, [connect, router, setCurrentUser, setDashboardMode, setGatewayAvailable, setCapabilitiesChecked, setSubscription, setUpdateAvailable, setShowOnboarding, setAgents, setSessions, setProjects, setInterfaceMode, setMemoryGraphAgents, setSkillsData])
346
+
347
+ if (!isClient || !bootComplete) {
348
+ return <Loader variant="page" steps={isClient ? initSteps : undefined} />
349
  }
350
 
351
  return (
 
353
  <a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:top-2 focus:left-2 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-md focus:text-sm focus:font-medium">
354
  Skip to main content
355
  </a>
356
+
357
  {/* Left: Icon rail navigation (hidden on mobile, shown as bottom bar instead) */}
358
+ {!showOnboarding && <NavRail />}
359
 
360
  {/* Center: Header + Content */}
361
  <div className="flex-1 flex flex-col min-w-0">
362
+ {!showOnboarding && (
363
+ <>
364
+ <HeaderBar />
365
+ <LocalModeBanner />
366
+ <UpdateBanner />
367
+ <OpenClawUpdateBanner />
368
+ <OpenClawDoctorBanner />
369
+ </>
370
+ )}
371
+ <main
372
+ id="main-content"
373
+ className={`flex-1 overflow-auto pb-16 md:pb-0 ${showOnboarding ? 'pointer-events-none select-none blur-[2px] opacity-30' : ''}`}
374
+ role="main"
375
+ aria-hidden={showOnboarding}
376
+ >
377
+ <div aria-live="polite" className="flex flex-col min-h-full">
378
  <ErrorBoundary key={activeTab}>
379
  <ContentRouter tab={activeTab} />
380
  </ErrorBoundary>
381
  </div>
382
+ <footer className="px-4 pb-4 pt-2">
383
+ <p className="text-2xs text-muted-foreground/50 text-center">
384
+ Built with care by <a href="https://x.com/nyk_builderz" target="_blank" rel="noopener noreferrer" className="text-muted-foreground/70 hover:text-primary transition-colors duration-200">nyk</a>.
385
+ </p>
386
+ </footer>
387
  </main>
388
  </div>
389
 
390
  {/* Right: Live feed (hidden on mobile) */}
391
+ {!showOnboarding && liveFeedOpen && (
392
  <div className="hidden lg:flex h-full">
393
  <LiveFeed />
394
  </div>
395
  )}
396
 
397
  {/* Floating button to reopen LiveFeed when closed */}
398
+ {!showOnboarding && !liveFeedOpen && (
399
  <button
400
  onClick={toggleLiveFeed}
401
  className="hidden lg:flex fixed right-0 top-1/2 -translate-y-1/2 z-30 w-6 h-12 items-center justify-center bg-card border border-r-0 border-border rounded-l-md text-muted-foreground hover:text-foreground hover:bg-secondary transition-all duration-200"
 
408
  )}
409
 
410
  {/* Chat panel overlay */}
411
+ {!showOnboarding && <ChatPanel />}
412
+
413
+ {/* Global exec approval overlay (shown regardless of active panel) */}
414
+ {!showOnboarding && <ExecApprovalOverlay />}
415
+
416
+ {/* Global Project Manager Modal */}
417
+ {!showOnboarding && showProjectManagerModal && (
418
+ <ProjectManagerModal
419
+ onClose={() => setShowProjectManagerModal(false)}
420
+ onChanged={async () => { await fetchProjects() }}
421
+ />
422
+ )}
423
+
424
+ <OnboardingWizard />
425
  </div>
426
  )
427
  }
428
 
429
+ const ESSENTIAL_PANELS = new Set([
430
+ 'overview', 'agents', 'tasks', 'chat', 'activity', 'logs', 'settings',
431
+ ])
432
+
433
  function ContentRouter({ tab }: { tab: string }) {
434
+ const { dashboardMode, interfaceMode, setInterfaceMode } = useMissionControl()
435
+ const navigateToPanel = useNavigateToPanel()
436
  const isLocal = dashboardMode === 'local'
437
 
438
+ // Guard: show nudge for non-essential panels in essential mode
439
+ if (interfaceMode === 'essential' && !ESSENTIAL_PANELS.has(tab)) {
440
+ return (
441
+ <div className="flex flex-col items-center justify-center py-24 text-center gap-4">
442
+ <p className="text-sm text-muted-foreground">
443
+ <span className="font-medium text-foreground capitalize">{tab.replace(/-/g, ' ')}</span> is available in Full mode.
444
+ </p>
445
+ <div className="flex items-center gap-2">
446
+ <Button
447
+ variant="outline"
448
+ size="sm"
449
+ onClick={async () => {
450
+ setInterfaceMode('full')
451
+ try { await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings: { 'general.interface_mode': 'full' } }) }) } catch {}
452
+ }}
453
+ >
454
+ Switch to Full
455
+ </Button>
456
+ <Button
457
+ variant="ghost"
458
+ size="sm"
459
+ onClick={() => navigateToPanel('overview')}
460
+ >
461
+ Go to Overview
462
+ </Button>
463
+ </div>
464
+ </div>
465
+ )
466
+ }
467
+
468
  switch (tab) {
469
  case 'overview':
470
  return (
471
  <>
472
  <Dashboard />
473
  {!isLocal && (
474
+ <div className="mt-4 mx-4 mb-4 rounded-lg border border-border bg-card overflow-hidden">
475
  <AgentCommsPanel />
476
  </div>
477
  )}
 
483
  return (
484
  <>
485
  <OrchestrationBar />
486
+ {isLocal && <LocalAgentsDocPanel />}
487
  <AgentSquadPanelPhase3 />
 
 
 
 
 
488
  </>
489
  )
 
 
490
  case 'notifications':
491
  return <NotificationsPanel />
492
  case 'standup':
493
  return <StandupPanel />
 
 
494
  case 'sessions':
495
+ return <ChatPagePanel />
496
  case 'logs':
497
  return <LogViewerPanel />
498
  case 'cron':
499
  return <CronManagementPanel />
500
  case 'memory':
501
  return <MemoryBrowserPanel />
502
+ case 'cost-tracker':
503
  case 'tokens':
 
504
  case 'agent-costs':
505
+ return <CostTrackerPanel />
506
  case 'users':
507
  return <UserManagementPanel />
508
  case 'history':
509
+ case 'activity':
510
+ return <ActivityFeedPanel />
511
  case 'audit':
512
  return <AuditTrailPanel />
513
  case 'webhooks':
 
515
  case 'alerts':
516
  return <AlertRulesPanel />
517
  case 'gateways':
518
+ if (isLocal) return <LocalModeUnavailable panel={tab} />
519
  return <MultiGatewayPanel />
520
  case 'gateway-config':
521
+ if (isLocal) return <LocalModeUnavailable panel={tab} />
522
  return <GatewayConfigPanel />
523
  case 'integrations':
524
  return <IntegrationsPanel />
525
  case 'settings':
526
  return <SettingsPanel />
527
+ case 'super-admin':
528
+ return <SuperAdminPanel />
529
  case 'github':
530
  return <GitHubSyncPanel />
531
  case 'office':
532
  return <OfficePanel />
533
+ case 'skills':
534
+ return <SkillsPanel />
535
+ case 'channels':
536
+ if (isLocal) return <LocalModeUnavailable panel={tab} />
537
+ return <ChannelsPanel />
538
+ case 'nodes':
539
+ if (isLocal) return <LocalModeUnavailable panel={tab} />
540
+ return <NodesPanel />
541
+ case 'security':
542
+ return <SecurityAuditPanel />
543
+ case 'debug':
544
+ return <DebugPanel />
545
+ case 'exec-approvals':
546
+ if (isLocal) return <LocalModeUnavailable panel={tab} />
547
+ return <ExecApprovalPanel />
548
+ case 'chat':
549
+ return <ChatPagePanel />
550
+ default: {
551
+ return renderPluginPanel(tab)
552
+ }
553
  }
554
  }
555
+
556
+ function LocalModeUnavailable({ panel }: { panel: string }) {
557
+ return (
558
+ <div className="flex flex-col items-center justify-center py-24 text-center">
559
+ <p className="text-sm text-muted-foreground">
560
+ <span className="font-medium text-foreground">{panel}</span> requires an OpenClaw gateway connection.
561
+ </p>
562
+ <p className="text-xs text-muted-foreground mt-1">
563
+ Configure a gateway to enable this panel.
564
+ </p>
565
+ </div>
566
+ )
567
+ }
src/app/api/activities/route.ts CHANGED
@@ -49,8 +49,14 @@ async function handleActivitiesRequest(request: NextRequest, workspaceId: number
49
  const params: any[] = [workspaceId];
50
 
51
  if (type) {
52
- query += ' AND type = ?';
53
- params.push(type);
 
 
 
 
 
 
54
  }
55
 
56
  if (actor) {
@@ -132,8 +138,14 @@ async function handleActivitiesRequest(request: NextRequest, workspaceId: number
132
  const countParams: any[] = [workspaceId];
133
 
134
  if (type) {
135
- countQuery += ' AND type = ?';
136
- countParams.push(type);
 
 
 
 
 
 
137
  }
138
 
139
  if (actor) {
 
49
  const params: any[] = [workspaceId];
50
 
51
  if (type) {
52
+ const types = type.split(',').map(t => t.trim()).filter(Boolean);
53
+ if (types.length === 1) {
54
+ query += ' AND type = ?';
55
+ params.push(types[0]);
56
+ } else if (types.length > 1) {
57
+ query += ` AND type IN (${types.map(() => '?').join(',')})`;
58
+ params.push(...types);
59
+ }
60
  }
61
 
62
  if (actor) {
 
138
  const countParams: any[] = [workspaceId];
139
 
140
  if (type) {
141
+ const types = type.split(',').map(t => t.trim()).filter(Boolean);
142
+ if (types.length === 1) {
143
+ countQuery += ' AND type = ?';
144
+ countParams.push(types[0]);
145
+ } else if (types.length > 1) {
146
+ countQuery += ` AND type IN (${types.map(() => '?').join(',')})`;
147
+ countParams.push(...types);
148
+ }
149
  }
150
 
151
  if (actor) {
src/app/api/adapters/route.ts ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { requireRole } from '@/lib/auth'
3
+ import { getAdapter, listAdapters } from '@/lib/adapters'
4
+ import { agentHeartbeatLimiter } from '@/lib/rate-limit'
5
+ import { logger } from '@/lib/logger'
6
+
7
+ /**
8
+ * GET /api/adapters — List available framework adapters.
9
+ */
10
+ export async function GET(request: NextRequest) {
11
+ const auth = requireRole(request, 'viewer')
12
+ if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
13
+
14
+ return NextResponse.json({ adapters: listAdapters() })
15
+ }
16
+
17
+ /**
18
+ * POST /api/adapters — Framework-agnostic agent action dispatcher.
19
+ *
20
+ * Body: { framework, action, payload }
21
+ *
22
+ * Actions:
23
+ * register — Register an agent via its framework adapter
24
+ * heartbeat — Send a heartbeat/status update
25
+ * report — Report task progress
26
+ * assignments — Get pending task assignments
27
+ * disconnect — Disconnect an agent
28
+ */
29
+ export async function POST(request: NextRequest) {
30
+ const auth = requireRole(request, 'operator')
31
+ if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
32
+
33
+ const rateLimited = agentHeartbeatLimiter(request)
34
+ if (rateLimited) return rateLimited
35
+
36
+ let body: any
37
+ try {
38
+ body = await request.json()
39
+ } catch {
40
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
41
+ }
42
+
43
+ const framework = typeof body?.framework === 'string' ? body.framework.trim() : ''
44
+ const action = typeof body?.action === 'string' ? body.action.trim() : ''
45
+ const payload = body?.payload ?? {}
46
+
47
+ if (!framework || !action) {
48
+ return NextResponse.json({ error: 'framework and action are required' }, { status: 400 })
49
+ }
50
+
51
+ let adapter
52
+ try {
53
+ adapter = getAdapter(framework)
54
+ } catch {
55
+ return NextResponse.json({
56
+ error: `Unknown framework: ${framework}. Available: ${listAdapters().join(', ')}`,
57
+ }, { status: 400 })
58
+ }
59
+
60
+ try {
61
+ switch (action) {
62
+ case 'register': {
63
+ const { agentId, name, metadata } = payload
64
+ if (!agentId || !name) {
65
+ return NextResponse.json({ error: 'payload.agentId and payload.name required' }, { status: 400 })
66
+ }
67
+ await adapter.register({ agentId, name, framework, metadata })
68
+ return NextResponse.json({ ok: true, action: 'register', framework })
69
+ }
70
+
71
+ case 'heartbeat': {
72
+ const { agentId, status, metrics } = payload
73
+ if (!agentId) {
74
+ return NextResponse.json({ error: 'payload.agentId required' }, { status: 400 })
75
+ }
76
+ await adapter.heartbeat({ agentId, status: status || 'online', metrics })
77
+ return NextResponse.json({ ok: true, action: 'heartbeat', framework })
78
+ }
79
+
80
+ case 'report': {
81
+ const { taskId, agentId, progress, status: taskStatus, output } = payload
82
+ if (!taskId || !agentId) {
83
+ return NextResponse.json({ error: 'payload.taskId and payload.agentId required' }, { status: 400 })
84
+ }
85
+ await adapter.reportTask({ taskId, agentId, progress: progress ?? 0, status: taskStatus || 'in_progress', output })
86
+ return NextResponse.json({ ok: true, action: 'report', framework })
87
+ }
88
+
89
+ case 'assignments': {
90
+ const { agentId } = payload
91
+ if (!agentId) {
92
+ return NextResponse.json({ error: 'payload.agentId required' }, { status: 400 })
93
+ }
94
+ const assignments = await adapter.getAssignments(agentId)
95
+ return NextResponse.json({ assignments, framework })
96
+ }
97
+
98
+ case 'disconnect': {
99
+ const { agentId } = payload
100
+ if (!agentId) {
101
+ return NextResponse.json({ error: 'payload.agentId required' }, { status: 400 })
102
+ }
103
+ await adapter.disconnect(agentId)
104
+ return NextResponse.json({ ok: true, action: 'disconnect', framework })
105
+ }
106
+
107
+ default:
108
+ return NextResponse.json({
109
+ error: `Unknown action: ${action}. Use: register, heartbeat, report, assignments, disconnect`,
110
+ }, { status: 400 })
111
+ }
112
+ } catch (error) {
113
+ logger.error({ err: error, framework, action }, 'POST /api/adapters error')
114
+ return NextResponse.json({ error: 'Adapter action failed' }, { status: 500 })
115
+ }
116
+ }
117
+
118
+ export const dynamic = 'force-dynamic'
src/app/api/agents/[id]/files/route.ts ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { getDatabase, db_helpers } from '@/lib/db'
3
+ import { requireRole } from '@/lib/auth'
4
+ import { config } from '@/lib/config'
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6
+ import { dirname, isAbsolute, resolve } from 'node:path'
7
+ import { resolveWithin } from '@/lib/paths'
8
+ import { getAgentWorkspaceCandidates, readAgentWorkspaceFile } from '@/lib/agent-workspace'
9
+ import { logger } from '@/lib/logger'
10
+
11
+ const ALLOWED_FILES = new Set([
12
+ 'agent.md',
13
+ 'identity.md',
14
+ 'soul.md',
15
+ 'WORKING.md',
16
+ 'MEMORY.md',
17
+ 'TOOLS.md',
18
+ 'AGENTS.md',
19
+ 'MISSION.md',
20
+ 'USER.md',
21
+ ])
22
+ const FILE_ALIASES: Record<string, string[]> = {
23
+ 'agent.md': ['agent.md', 'AGENT.md', 'MISSION.md', 'USER.md'],
24
+ 'identity.md': ['identity.md', 'IDENTITY.md'],
25
+ 'soul.md': ['soul.md', 'SOUL.md'],
26
+ 'WORKING.md': ['WORKING.md', 'working.md'],
27
+ 'MEMORY.md': ['MEMORY.md', 'memory.md'],
28
+ 'TOOLS.md': ['TOOLS.md', 'tools.md'],
29
+ 'AGENTS.md': ['AGENTS.md', 'agents.md'],
30
+ 'MISSION.md': ['MISSION.md', 'mission.md'],
31
+ 'USER.md': ['USER.md', 'user.md'],
32
+ }
33
+
34
+ function resolveAgentWorkspacePath(workspace: string): string {
35
+ if (isAbsolute(workspace)) return resolve(workspace)
36
+ if (!config.openclawStateDir) throw new Error('OPENCLAW_STATE_DIR not configured')
37
+ return resolveWithin(config.openclawStateDir, workspace)
38
+ }
39
+
40
+ function getAgentByIdOrName(db: ReturnType<typeof getDatabase>, id: string, workspaceId: number): any | undefined {
41
+ if (isNaN(Number(id))) {
42
+ return db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(id, workspaceId)
43
+ }
44
+ return db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(id), workspaceId)
45
+ }
46
+
47
+ export async function GET(
48
+ request: NextRequest,
49
+ { params }: { params: Promise<{ id: string }> }
50
+ ) {
51
+ const auth = requireRole(request, 'viewer')
52
+ if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
53
+
54
+ try {
55
+ const { id } = await params
56
+ const db = getDatabase()
57
+ const workspaceId = auth.user.workspace_id ?? 1
58
+ const agent = getAgentByIdOrName(db, id, workspaceId)
59
+ if (!agent) return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
60
+
61
+ const agentConfig = agent.config ? JSON.parse(agent.config) : {}
62
+ const candidates = getAgentWorkspaceCandidates(agentConfig, agent.name)
63
+ if (candidates.length === 0) {
64
+ return NextResponse.json({ error: 'Agent workspace is not configured' }, { status: 400 })
65
+ }
66
+ const safeWorkspace = candidates[0]
67
+ const requested = (new URL(request.url).searchParams.get('file') || '').trim()
68
+ const files = requested
69
+ ? [requested]
70
+ : ['agent.md', 'identity.md', 'soul.md', 'WORKING.md', 'MEMORY.md', 'TOOLS.md', 'AGENTS.md', 'MISSION.md', 'USER.md']
71
+
72
+ const payload: Record<string, { exists: boolean; content: string }> = {}
73
+ for (const file of files) {
74
+ if (!ALLOWED_FILES.has(file)) {
75
+ return NextResponse.json({ error: `Unsupported file: ${file}` }, { status: 400 })
76
+ }
77
+ const aliases = FILE_ALIASES[file] || [file]
78
+ const match = readAgentWorkspaceFile(candidates, aliases)
79
+ payload[file] = { exists: match.exists, content: match.content }
80
+ }
81
+
82
+ return NextResponse.json({
83
+ agent: { id: agent.id, name: agent.name },
84
+ workspace: safeWorkspace,
85
+ files: payload,
86
+ })
87
+ } catch (error) {
88
+ logger.error({ err: error }, 'GET /api/agents/[id]/files error')
89
+ return NextResponse.json({ error: 'Failed to load workspace files' }, { status: 500 })
90
+ }
91
+ }
92
+
93
+ export async function PUT(
94
+ request: NextRequest,
95
+ { params }: { params: Promise<{ id: string }> }
96
+ ) {
97
+ const auth = requireRole(request, 'operator')
98
+ if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
99
+
100
+ try {
101
+ const { id } = await params
102
+ const body = await request.json()
103
+ const file = String(body?.file || '').trim()
104
+ const content = String(body?.content || '')
105
+ const MAX_WORKSPACE_FILE_SIZE = 1024 * 1024 // 1 MB
106
+ if (content.length > MAX_WORKSPACE_FILE_SIZE) {
107
+ return NextResponse.json({ error: `File content too large (max ${MAX_WORKSPACE_FILE_SIZE} bytes)` }, { status: 413 })
108
+ }
109
+ if (!ALLOWED_FILES.has(file)) {
110
+ return NextResponse.json({ error: `Unsupported file: ${file}` }, { status: 400 })
111
+ }
112
+
113
+ const db = getDatabase()
114
+ const workspaceId = auth.user.workspace_id ?? 1
115
+ const agent = getAgentByIdOrName(db, id, workspaceId)
116
+ if (!agent) return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
117
+
118
+ const agentConfig = agent.config ? JSON.parse(agent.config) : {}
119
+ const candidates = getAgentWorkspaceCandidates(agentConfig, agent.name)
120
+ const safeWorkspace = candidates[0]
121
+ if (!safeWorkspace) {
122
+ return NextResponse.json({ error: 'Agent workspace is not configured' }, { status: 400 })
123
+ }
124
+
125
+ const safePath = resolveWithin(safeWorkspace, file)
126
+ mkdirSync(dirname(safePath), { recursive: true })
127
+ writeFileSync(safePath, content, 'utf-8')
128
+
129
+ if (file === 'soul.md') {
130
+ db.prepare('UPDATE agents SET soul_content = ?, updated_at = unixepoch() WHERE id = ? AND workspace_id = ?')
131
+ .run(content, agent.id, workspaceId)
132
+ }
133
+ if (file === 'WORKING.md') {
134
+ db.prepare('UPDATE agents SET working_memory = ?, updated_at = unixepoch() WHERE id = ? AND workspace_id = ?')
135
+ .run(content, agent.id, workspaceId)
136
+ }
137
+
138
+ db_helpers.logActivity(
139
+ 'agent_workspace_file_updated',
140
+ 'agent',
141
+ agent.id,
142
+ auth.user.username,
143
+ `${file} updated for ${agent.name}`,
144
+ { file, size: content.length },
145
+ workspaceId
146
+ )
147
+
148
+ return NextResponse.json({ success: true, file, size: content.length })
149
+ } catch (error) {
150
+ logger.error({ err: error }, 'PUT /api/agents/[id]/files error')
151
+ return NextResponse.json({ error: 'Failed to save workspace file' }, { status: 500 })
152
+ }
153
+ }
src/app/api/agents/[id]/heartbeat/route.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { NextRequest, NextResponse } from 'next/server';
2
  import { getDatabase, db_helpers } from '@/lib/db';
3
  import { requireRole } from '@/lib/auth';
 
4
  import { logger } from '@/lib/logger';
5
 
6
  /**
@@ -189,6 +190,9 @@ export async function POST(
189
  const auth = requireRole(request, 'operator');
190
  if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
191
 
 
 
 
192
  let body: any = {};
193
  try {
194
  body = await request.json();
 
1
  import { NextRequest, NextResponse } from 'next/server';
2
  import { getDatabase, db_helpers } from '@/lib/db';
3
  import { requireRole } from '@/lib/auth';
4
+ import { agentHeartbeatLimiter } from '@/lib/rate-limit';
5
  import { logger } from '@/lib/logger';
6
 
7
  /**
 
190
  const auth = requireRole(request, 'operator');
191
  if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
192
 
193
+ const rateLimited = agentHeartbeatLimiter(request);
194
+ if (rateLimited) return rateLimited;
195
+
196
  let body: any = {};
197
  try {
198
  body = await request.json();
src/app/api/agents/[id]/memory/route.ts CHANGED
@@ -2,6 +2,17 @@ import { NextRequest, NextResponse } from 'next/server';
2
  import { getDatabase, db_helpers } from '@/lib/db';
3
  import { requireRole } from '@/lib/auth';
4
  import { logger } from '@/lib/logger';
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  /**
7
  * GET /api/agents/[id]/memory - Get agent's working memory
@@ -43,11 +54,28 @@ export async function GET(
43
  db.exec("ALTER TABLE agents ADD COLUMN working_memory TEXT DEFAULT ''");
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  // Get working memory content
47
  const memoryStmt = db.prepare(`SELECT working_memory FROM agents WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ? AND workspace_id = ?`);
48
  const result = memoryStmt.get(agentId, workspaceId) as any;
49
-
50
- const workingMemory = result?.working_memory || '';
 
 
51
 
52
  return NextResponse.json({
53
  agent: {
@@ -56,6 +84,7 @@ export async function GET(
56
  role: agent.role
57
  },
58
  working_memory: workingMemory,
 
59
  updated_at: agent.updated_at,
60
  size: workingMemory.length
61
  });
@@ -118,6 +147,22 @@ export async function PUT(
118
  }
119
 
120
  const now = Math.floor(Date.now() / 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  // Update working memory
123
  const updateStmt = db.prepare(`
@@ -135,10 +180,11 @@ export async function PUT(
135
  agent.id,
136
  agent.name,
137
  `Working memory ${append ? 'appended' : 'updated'} for agent ${agent.name}`,
138
- {
139
  content_length: newContent.length,
140
  append_mode: append || false,
141
- timestamp: now
 
142
  },
143
  workspaceId
144
  );
@@ -147,6 +193,7 @@ export async function PUT(
147
  success: true,
148
  message: `Working memory ${append ? 'appended' : 'updated'} for ${agent.name}`,
149
  working_memory: newContent,
 
150
  updated_at: now,
151
  size: newContent.length
152
  });
@@ -185,6 +232,20 @@ export async function DELETE(
185
  }
186
 
187
  const now = Math.floor(Date.now() / 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  // Clear working memory
190
  const updateStmt = db.prepare(`
 
2
  import { getDatabase, db_helpers } from '@/lib/db';
3
  import { requireRole } from '@/lib/auth';
4
  import { logger } from '@/lib/logger';
5
+ import { config } from '@/lib/config';
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { dirname, isAbsolute, resolve } from 'node:path';
8
+ import { resolveWithin } from '@/lib/paths';
9
+ import { getAgentWorkspaceCandidates, readAgentWorkspaceFile } from '@/lib/agent-workspace';
10
+
11
+ function resolveAgentWorkspacePath(workspace: string): string {
12
+ if (isAbsolute(workspace)) return resolve(workspace)
13
+ if (!config.openclawStateDir) throw new Error('OPENCLAW_STATE_DIR not configured')
14
+ return resolveWithin(config.openclawStateDir, workspace)
15
+ }
16
 
17
  /**
18
  * GET /api/agents/[id]/memory - Get agent's working memory
 
54
  db.exec("ALTER TABLE agents ADD COLUMN working_memory TEXT DEFAULT ''");
55
  }
56
 
57
+ // Prefer workspace WORKING.md, fall back to DB working_memory
58
+ let workingMemory = '';
59
+ let source: 'workspace' | 'database' | 'none' = 'none';
60
+ try {
61
+ const agentConfig = agent.config ? JSON.parse(agent.config) : {};
62
+ const candidates = getAgentWorkspaceCandidates(agentConfig, agent.name);
63
+ const match = readAgentWorkspaceFile(candidates, ['WORKING.md', 'working.md', 'MEMORY.md', 'memory.md']);
64
+ if (match.exists) {
65
+ workingMemory = match.content;
66
+ source = 'workspace';
67
+ }
68
+ } catch (err) {
69
+ logger.warn({ err, agent: agent.name }, 'Failed to read WORKING.md from workspace');
70
+ }
71
+
72
  // Get working memory content
73
  const memoryStmt = db.prepare(`SELECT working_memory FROM agents WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ? AND workspace_id = ?`);
74
  const result = memoryStmt.get(agentId, workspaceId) as any;
75
+ if (!workingMemory) {
76
+ workingMemory = result?.working_memory || '';
77
+ source = workingMemory ? 'database' : 'none';
78
+ }
79
 
80
  return NextResponse.json({
81
  agent: {
 
84
  role: agent.role
85
  },
86
  working_memory: workingMemory,
87
+ source,
88
  updated_at: agent.updated_at,
89
  size: workingMemory.length
90
  });
 
147
  }
148
 
149
  const now = Math.floor(Date.now() / 1000);
150
+
151
+ // Best effort: sync workspace WORKING.md if agent workspace is configured
152
+ let savedToWorkspace = false;
153
+ try {
154
+ const agentConfig = agent.config ? JSON.parse(agent.config) : {};
155
+ const candidates = getAgentWorkspaceCandidates(agentConfig, agent.name);
156
+ const safeWorkspace = candidates[0];
157
+ if (safeWorkspace) {
158
+ const safeWorkingPath = resolveWithin(safeWorkspace, 'WORKING.md');
159
+ mkdirSync(dirname(safeWorkingPath), { recursive: true });
160
+ writeFileSync(safeWorkingPath, newContent, 'utf-8');
161
+ savedToWorkspace = true;
162
+ }
163
+ } catch (err) {
164
+ logger.warn({ err, agent: agent.name }, 'Failed to write WORKING.md to workspace');
165
+ }
166
 
167
  // Update working memory
168
  const updateStmt = db.prepare(`
 
180
  agent.id,
181
  agent.name,
182
  `Working memory ${append ? 'appended' : 'updated'} for agent ${agent.name}`,
183
+ {
184
  content_length: newContent.length,
185
  append_mode: append || false,
186
+ timestamp: now,
187
+ saved_to_workspace: savedToWorkspace
188
  },
189
  workspaceId
190
  );
 
193
  success: true,
194
  message: `Working memory ${append ? 'appended' : 'updated'} for ${agent.name}`,
195
  working_memory: newContent,
196
+ saved_to_workspace: savedToWorkspace,
197
  updated_at: now,
198
  size: newContent.length
199
  });
 
232
  }
233
 
234
  const now = Math.floor(Date.now() / 1000);
235
+
236
+ // Best effort: clear workspace WORKING.md if agent workspace is configured
237
+ try {
238
+ const agentConfig = agent.config ? JSON.parse(agent.config) : {};
239
+ const candidates = getAgentWorkspaceCandidates(agentConfig, agent.name);
240
+ const safeWorkspace = candidates[0];
241
+ if (safeWorkspace) {
242
+ const safeWorkingPath = resolveWithin(safeWorkspace, 'WORKING.md');
243
+ mkdirSync(dirname(safeWorkingPath), { recursive: true });
244
+ writeFileSync(safeWorkingPath, '', 'utf-8');
245
+ }
246
+ } catch (err) {
247
+ logger.warn({ err, agent: agent.name }, 'Failed to clear WORKING.md in workspace');
248
+ }
249
 
250
  // Clear working memory
251
  const updateStmt = db.prepare(`
src/app/api/agents/[id]/route.ts CHANGED
@@ -1,9 +1,10 @@
1
  import { NextRequest, NextResponse } from 'next/server'
2
  import { getDatabase, db_helpers, logAuditEvent } from '@/lib/db'
3
  import { requireRole } from '@/lib/auth'
4
- import { writeAgentToConfig, enrichAgentConfigFromWorkspace } from '@/lib/agent-sync'
5
  import { eventBus } from '@/lib/event-bus'
6
  import { logger } from '@/lib/logger'
 
7
 
8
  /**
9
  * GET /api/agents/[id] - Get a single agent by ID or name
@@ -102,20 +103,9 @@ export async function PUT(
102
  return writeBack
103
  }
104
 
105
- // Unified save: gateway first, then DB. If DB fails after gateway write, attempt rollback.
106
- if (shouldWriteToGateway) {
107
- try {
108
- await writeAgentToConfig(getWriteBackPayload(gateway_config))
109
- } catch (err: any) {
110
- return NextResponse.json(
111
- { error: `Save failed: unable to update gateway config: ${err.message}` },
112
- { status: 502 }
113
- )
114
- }
115
- }
116
-
117
  try {
118
- // Build update
119
  const fields: string[] = ['updated_at = ?']
120
  const values: any[] = [now]
121
 
@@ -132,19 +122,31 @@ export async function PUT(
132
  values.push(agent.id, workspaceId)
133
  db.prepare(`UPDATE agents SET ${fields.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...values)
134
  } catch (err: any) {
135
- if (shouldWriteToGateway) {
 
 
 
 
 
 
 
136
  try {
137
- // Best-effort rollback to preserve consistency if DB update fails after gateway write.
138
- await writeAgentToConfig(getWriteBackPayload(existingConfig))
139
- } catch (rollbackErr: any) {
140
- logger.error({ err: rollbackErr, agent: agent.name }, 'Failed to rollback gateway config after DB failure')
141
- return NextResponse.json(
142
- { error: `Save failed after gateway update and rollback failed: ${err.message}` },
143
- { status: 500 }
144
- )
 
 
145
  }
 
 
 
 
146
  }
147
- return NextResponse.json({ error: `Save failed: ${err.message}` }, { status: 500 })
148
  }
149
 
150
  if (shouldWriteToGateway) {
@@ -205,6 +207,13 @@ export async function DELETE(
205
  const db = getDatabase()
206
  const { id } = await params
207
  const workspaceId = auth.user.workspace_id ?? 1;
 
 
 
 
 
 
 
208
 
209
  let agent
210
  if (isNaN(Number(id))) {
@@ -217,6 +226,38 @@ export async function DELETE(
217
  return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
218
  }
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  db.prepare('DELETE FROM agents WHERE id = ? AND workspace_id = ?').run(agent.id, workspaceId)
221
 
222
  db_helpers.logActivity(
@@ -225,13 +266,18 @@ export async function DELETE(
225
  agent.id,
226
  auth.user.username,
227
  `Deleted agent: ${agent.name}`,
228
- { name: agent.name, role: agent.role },
229
  workspaceId
230
  )
231
 
232
  eventBus.broadcast('agent.deleted', { id: agent.id, name: agent.name })
233
 
234
- return NextResponse.json({ success: true, deleted: agent.name })
 
 
 
 
 
235
  } catch (error) {
236
  logger.error({ err: error }, 'DELETE /api/agents/[id] error')
237
  return NextResponse.json({ error: 'Failed to delete agent' }, { status: 500 })
 
1
  import { NextRequest, NextResponse } from 'next/server'
2
  import { getDatabase, db_helpers, logAuditEvent } from '@/lib/db'
3
  import { requireRole } from '@/lib/auth'
4
+ import { writeAgentToConfig, enrichAgentConfigFromWorkspace, removeAgentFromConfig } from '@/lib/agent-sync'
5
  import { eventBus } from '@/lib/event-bus'
6
  import { logger } from '@/lib/logger'
7
+ import { runOpenClaw } from '@/lib/command'
8
 
9
  /**
10
  * GET /api/agents/[id] - Get a single agent by ID or name
 
103
  return writeBack
104
  }
105
 
106
+ // Unified save: DB first (transactional, easy to revert), then gateway file.
107
+ // If gateway write fails after DB succeeds, revert DB to keep consistency.
 
 
 
 
 
 
 
 
 
 
108
  try {
 
109
  const fields: string[] = ['updated_at = ?']
110
  const values: any[] = [now]
111
 
 
122
  values.push(agent.id, workspaceId)
123
  db.prepare(`UPDATE agents SET ${fields.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...values)
124
  } catch (err: any) {
125
+ return NextResponse.json({ error: `Save failed: ${err.message}` }, { status: 500 })
126
+ }
127
+
128
+ if (shouldWriteToGateway) {
129
+ try {
130
+ await writeAgentToConfig(getWriteBackPayload(gateway_config))
131
+ } catch (err: any) {
132
+ // Gateway write failed — revert DB to previous state
133
  try {
134
+ const revertFields: string[] = ['updated_at = ?']
135
+ const revertValues: any[] = [agent.updated_at]
136
+ revertFields.push('role = ?')
137
+ revertValues.push(agent.role)
138
+ revertFields.push('config = ?')
139
+ revertValues.push(agent.config || '{}')
140
+ revertValues.push(agent.id, workspaceId)
141
+ db.prepare(`UPDATE agents SET ${revertFields.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...revertValues)
142
+ } catch (revertErr: any) {
143
+ logger.error({ err: revertErr, agent: agent.name }, 'Failed to revert DB after gateway write failure')
144
  }
145
+ return NextResponse.json(
146
+ { error: `Save failed: unable to update gateway config: ${err.message}` },
147
+ { status: 502 }
148
+ )
149
  }
 
150
  }
151
 
152
  if (shouldWriteToGateway) {
 
207
  const db = getDatabase()
208
  const { id } = await params
209
  const workspaceId = auth.user.workspace_id ?? 1;
210
+ let removeWorkspace = false
211
+ try {
212
+ const body = await request.json()
213
+ removeWorkspace = Boolean(body?.remove_workspace)
214
+ } catch {
215
+ // Optional body
216
+ }
217
 
218
  let agent
219
  if (isNaN(Number(id))) {
 
226
  return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
227
  }
228
 
229
+ if (removeWorkspace) {
230
+ const agentConfig = agent.config ? JSON.parse(agent.config) : {}
231
+ const openclawId =
232
+ String(agentConfig?.openclawId || agent.name || '')
233
+ .toLowerCase()
234
+ .replace(/[^a-z0-9._-]+/g, '-')
235
+ .replace(/^-+|-+$/g, '') || agent.name
236
+ try {
237
+ await runOpenClaw(['agents', 'delete', openclawId, '--force'], { timeoutMs: 30000 })
238
+ } catch (err: any) {
239
+ logger.error({ err, openclawId, agent: agent.name }, 'Failed to remove OpenClaw agent/workspace')
240
+ return NextResponse.json(
241
+ { error: `Failed to remove OpenClaw workspace for ${agent.name}: ${err?.message || 'unknown error'}` },
242
+ { status: 502 }
243
+ )
244
+ }
245
+ }
246
+
247
+ let configCleanupWarning: string | null = null
248
+ try {
249
+ const agentConfig = agent.config ? JSON.parse(agent.config) : {}
250
+ const openclawId =
251
+ String(agentConfig?.openclawId || agent.name || '')
252
+ .toLowerCase()
253
+ .replace(/[^a-z0-9._-]+/g, '-')
254
+ .replace(/^-+|-+$/g, '') || agent.name
255
+ await removeAgentFromConfig({ id: openclawId, name: agent.name })
256
+ } catch (err: any) {
257
+ configCleanupWarning = `OpenClaw config cleanup skipped for ${agent.name}: ${err?.message || 'unknown error'}`
258
+ logger.warn({ err, agent: agent.name }, 'Failed to remove OpenClaw agent config entry')
259
+ }
260
+
261
  db.prepare('DELETE FROM agents WHERE id = ? AND workspace_id = ?').run(agent.id, workspaceId)
262
 
263
  db_helpers.logActivity(
 
266
  agent.id,
267
  auth.user.username,
268
  `Deleted agent: ${agent.name}`,
269
+ { name: agent.name, role: agent.role, remove_workspace: removeWorkspace },
270
  workspaceId
271
  )
272
 
273
  eventBus.broadcast('agent.deleted', { id: agent.id, name: agent.name })
274
 
275
+ return NextResponse.json({
276
+ success: true,
277
+ deleted: agent.name,
278
+ remove_workspace: removeWorkspace,
279
+ ...(configCleanupWarning ? { warning: configCleanupWarning } : {}),
280
+ })
281
  } catch (error) {
282
  logger.error({ err: error }, 'DELETE /api/agents/[id] error')
283
  return NextResponse.json({ error: 'Failed to delete agent' }, { status: 500 })
src/app/api/agents/[id]/soul/route.ts CHANGED
@@ -4,6 +4,7 @@ import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from
4
  import { join, dirname, isAbsolute, resolve } from 'path';
5
  import { config } from '@/lib/config';
6
  import { resolveWithin } from '@/lib/paths';
 
7
  import { requireRole } from '@/lib/auth';
8
  import { logger } from '@/lib/logger';
9
 
@@ -47,13 +48,11 @@ export async function GET(
47
 
48
  try {
49
  const agentConfig = agent.config ? JSON.parse(agent.config) : {}
50
- if (agentConfig.workspace) {
51
- const safeWorkspace = resolveAgentWorkspacePath(agentConfig.workspace)
52
- const safeSoulPath = resolveWithin(safeWorkspace, 'soul.md')
53
- if (existsSync(safeSoulPath)) {
54
- soulContent = readFileSync(safeSoulPath, 'utf-8')
55
- source = 'workspace'
56
- }
57
  }
58
  } catch (err) {
59
  logger.warn({ err, agent: agent.name }, 'Failed to read soul.md from workspace')
@@ -163,8 +162,9 @@ export async function PUT(
163
  let savedToWorkspace = false
164
  try {
165
  const agentConfig = agent.config ? JSON.parse(agent.config) : {}
166
- if (agentConfig.workspace) {
167
- const safeWorkspace = resolveAgentWorkspacePath(agentConfig.workspace)
 
168
  const safeSoulPath = resolveWithin(safeWorkspace, 'soul.md')
169
  mkdirSync(dirname(safeSoulPath), { recursive: true })
170
  writeFileSync(safeSoulPath, newSoulContent || '', 'utf-8')
 
4
  import { join, dirname, isAbsolute, resolve } from 'path';
5
  import { config } from '@/lib/config';
6
  import { resolveWithin } from '@/lib/paths';
7
+ import { getAgentWorkspaceCandidates, readAgentWorkspaceFile } from '@/lib/agent-workspace';
8
  import { requireRole } from '@/lib/auth';
9
  import { logger } from '@/lib/logger';
10
 
 
48
 
49
  try {
50
  const agentConfig = agent.config ? JSON.parse(agent.config) : {}
51
+ const candidates = getAgentWorkspaceCandidates(agentConfig, agent.name)
52
+ const match = readAgentWorkspaceFile(candidates, ['soul.md', 'SOUL.md'])
53
+ if (match.exists) {
54
+ soulContent = match.content
55
+ source = 'workspace'
 
 
56
  }
57
  } catch (err) {
58
  logger.warn({ err, agent: agent.name }, 'Failed to read soul.md from workspace')
 
162
  let savedToWorkspace = false
163
  try {
164
  const agentConfig = agent.config ? JSON.parse(agent.config) : {}
165
+ const candidates = getAgentWorkspaceCandidates(agentConfig, agent.name)
166
+ const safeWorkspace = candidates[0]
167
+ if (safeWorkspace) {
168
  const safeSoulPath = resolveWithin(safeWorkspace, 'soul.md')
169
  mkdirSync(dirname(safeSoulPath), { recursive: true })
170
  writeFileSync(safeSoulPath, newSoulContent || '', 'utf-8')