Skill: jcdentoncore/command-center-1-4-1 (version 1.0.0)

--- name: command-center version: 1.4.1 description: Mission control dashboard for OpenClaw - real-time session monitoring, LLM usage tracking, cost intelligence, and system vitals. View all your AI agents in one place. metadata: openclaw: requires: node: ">=18" install: - id: start kind: shell command: "node lib/server.js" label: "Start Command Center (http://localhost:3333)" --- # OpenClaw Command Center Mission control for your AI workforce. ## Quick Start ```bash npx clawhub@latest install command-center cd skills/command-center node lib/server.js ``` Dashboard runs at **http://localhost:3333** ## Features - **Session Monitoring** — Real-time view of all AI sessions with live updates - **LLM Fuel Gauges** — Track Claude, Codex, and other model usage - **System Vitals** — CPU, Memory, Disk, Temperature - **Cron Jobs** — View and manage scheduled tasks - **Cerebro Topics** — Automatic conversation organization - **Cost Tracking** — Per-session costs, projections, savings estimates - **Privacy Controls** — Hide sensitive topics for demos ## Configuration The dashboard auto-detects your OpenClaw workspace. Set `OPENCLAW_WORKSPACE` to override. ### Authentication | Mode | Use Case | | ------------ | ----------------- | | `none` | Local development | | `token` | Remote access | | `tailscale` | Team VPN | | `cloudflare` | Public deployment | ```bash DASHBOARD_AUTH_MODE=tailscale node lib/server.js ``` ## API | Endpoint | Description | | ----------------- | ---------------------------- | | `GET /api/state` | All dashboard data (unified) | | `GET /api/events` | SSE stream for live updates | | `GET /api/health` | Health check | ## Links - [GitHub](https://github.com/jontsai/openclaw-command-center) - [ClawHub](https://www.clawhub.ai/jontsai/command-center) - [Documentation](https://github.com/jontsai/openclaw-command-center#readme)

path: AGENTS.md
content: # AGENTS.md — AI Workspace Guide

> _"The Overmind speaks through many voices, but with one purpose."_

Welcome, AI agent. This document defines how you should interact with this codebase.

## ⚠️ CRITICAL: Pull Request Workflow

**All changes to this repository MUST go through pull requests.**

This is a public open source project. Even maintainers (including AI agents working on behalf of maintainers) must:

1. Create a feature branch (`git checkout -b type/description`)
2. Make changes and commit
3. Push branch and open a PR
4. Get approval before merging

**Never push directly to `main`.** This applies to everyone, including the repo owner.

## 🎯 Mission

OpenClaw Command Center is the central dashboard for AI assistant management. Your mission is to help build, maintain, and improve this system while maintaining the Starcraft/Zerg thematic elements that make it unique.

## 🏛️ Architecture

**Read First**: [`docs/architecture/OVERVIEW.md`](docs/architecture/OVERVIEW.md)

Key architectural principles:

1. **DRY** — Don't Repeat Yourself. Extract shared code to partials/modules.
2. **Zero Build Step** — Plain HTML/CSS/JS, no compilation needed.
3. **Real-Time First** — SSE for live updates, polling as fallback.
4. **Progressive Enhancement** — Works without JS, enhanced with JS.

## 📁 Workspace Structure

```
openclaw-command-center/
├── lib/                    # Core server logic
│   ├── server.js           # Main HTTP server and API routes
│   ├── config.js           # Configuration loader with auto-detection
│   ├── jobs.js             # Jobs/scheduler API integration
│   ├── linear-sync.js      # Linear issue tracker integration
│   └── topic-classifier.js # NLP-based topic classification
├── public/                 # Frontend assets
│   ├── index.html          # Main dashboard UI
│   ├── jobs.html           # AI Jobs management UI
│   ├── partials/           # ⭐ Shared HTML partials (DRY!)
│   │   └── sidebar.html    # Navigation sidebar component
│   ├── css/
│   │   └── dashboard.css   # Shared styles
│   └── js/
│       ├── sidebar.js      # Sidebar loader + SSE badges
│       ├── app.js          # Main dashboard logic
│       └── lib/            # Third-party libraries
├── scripts/                # Operational scripts
├── config/                 # Configuration (be careful!)
├── docs/                   # Documentation
│   └── architecture/       # Architecture Decision Records
├── tests/                  # Test files
├── SKILL.md                # ClawHub skill metadata
└── package.json            # Version and dependencies
```

## ✅ Safe Operations

Do freely:

- Read any file to understand the codebase
- Create/modify files in `lib/`, `public/`, `docs/`, `tests/`
- Add tests
- Update documentation
- Create feature branches

## ⚠️ Ask First

Check with a human before:

- Modifying `config/` files
- Changing CI/CD workflows
- Adding new dependencies to `package.json`
- Making breaking API changes
- Anything touching authentication/secrets

## 🚫 Never

- **Push directly to `main` branch** — ALL changes require PRs
- Commit secrets, API keys, or credentials
- Commit user-specific data files (see `public/data/AGENTS.md`)
- Delete files without confirmation
- Expose internal endpoints publicly

## 🛠️ Development Workflow

### 0. First-Time Setup

```bash
# Install pre-commit hooks (required for all contributors)
make install-hooks

# Or manually:
cp scripts/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
```

The pre-commit hook enforces rules from this file automatically.

### 1. Feature Development

```bash
# Create feature branch
git checkout -b feat/your-feature-name

# Make changes, then test locally
npm test
npm run lint
make check  # Run pre-commit checks manually

# Commit with descriptive message
git commit -m "feat: add overlord status indicator"

# Push and create PR
git push -u origin feat/your-feature-name
```

### 2. Commit Message Convention

Follow [Conventional Commits](https://www.conventionalcommits.org/):

- `feat:` — New feature
- `fix:` — Bug fix
- `docs:` — Documentation only
- `style:` — Formatting, no code change
- `refactor:` — Code restructuring
- `test:` — Adding tests
- `chore:` — Maintenance tasks

### 3. Code Style

- Use ESLint configuration provided
- Prettier for formatting
- JSDoc comments for public functions
- Meaningful variable names (thematic names encouraged!)

## 📦 ClawHub Skill Workflow

This project is distributed as a ClawHub skill. After changes are merged to `main`, they need to be published to the registry so users can install/update via `clawhub install command-center`.

### Understanding Skill Metadata

Two files control the skill identity:

- **`SKILL.md`** — Frontmatter (`name`, `version`, `description`) used by ClawHub for discovery and search
- **`package.json`** — `version` field for npm compatibility

⚠️ **CRITICAL: Version Sync Required**

Both `package.json` and `SKILL.md` **MUST have the same version number**. This is enforced by pre-commit hooks.

```bash
# If you change version in one file, change it in both:
# package.json:  "version": "1.0.4"
# SKILL.md:      version: 1.0.4
```

The pre-commit hook will block commits if versions are out of sync.

### Publishing Updates

```bash
# 1. Authenticate (one-time)
clawhub login
clawhub whoami

# 2. Bump version in package.json (follow semver)
#    patch: bug fixes         (0.1.0 → 0.1.1)
#    minor: new features      (0.1.0 → 0.2.0)
#    major: breaking changes  (0.1.0 → 1.0.0)

# 3. Tag the release
git tag -a v<new-version> -m "v<new-version> — short description"
git push origin --tags

# 4. Publish (--registry flag required until upstream redirect is fixed)
clawhub publish . --registry https://www.clawhub.ai \
  --slug command-center --version <new-version> \
  --changelog "Description of what changed"
```

### Verifying a Publish

```bash
# Check published metadata
clawhub inspect command-center

# Test install into a workspace
clawhub install command-center --workdir /path/to/workspace
```

### Updating an Installed Skill

Users update with:

```bash
clawhub update command-center
```

The installed version is tracked in `.clawhub/origin.json` within the skill directory.

### Who Can Publish?

Only maintainers with ClawHub credentials for `jontsai/command-center` can publish. Currently:

- @jontsai (owner)

Contributors: Submit PRs. After merge, a maintainer will handle the ClawHub publish.

### Release Checklist

Before publishing a new version:

1. [ ] All PRs for the release are merged to `main`
2. [ ] Version bumped in both `package.json` and `SKILL.md` frontmatter
3. [ ] CHANGELOG updated (if maintained)
4. [ ] Tests pass: `npm test`
5. [ ] Lint passes: `npm run lint`
6. [ ] Git tag created: `git tag -a v<version> -m "v<version>"`
7. [ ] Tag pushed: `git push origin --tags`
8. [ ] Published to ClawHub with changelog

## 🎨 Thematic Guidelines

This project has a Starcraft/Zerg theme. When naming things:

| Concept            | Thematic Name |
| ------------------ | ------------- |
| Main controller    | Overmind      |
| Worker processes   | Drones        |
| Monitoring service | Overlord      |
| Cache layer        | Creep         |
| Message queue      | Spawning Pool |
| Health check       | Essence scan  |
| Error state        | Corrupted     |

Example:

```javascript
// Instead of: const cacheService = new Cache();
const creepLayer = new CreepCache();

// Instead of: function checkHealth()
function scanEssence()
```

## 📝 Documentation Standards

When you add features, document them:

1. **Code comments** — JSDoc for functions
2. **README updates** — If user-facing
3. **API docs** — In `docs/api/` for endpoints
4. **Architecture Decision Records** — In `docs/architecture/` for major changes

## 🧪 Testing

```bash
# Run all tests
npm test

# Coverage report
npm run test:coverage
```

Aim for meaningful test coverage. Test the logic, not the framework.

## 🐛 Debugging

```bash
# Enable all command-center debug output
DEBUG=openclaw:* npm run dev

# Specific namespaces
DEBUG=openclaw:api npm run dev
DEBUG=openclaw:overlord npm run dev
```

## 🔄 Handoff Protocol

When handing off to another AI or ending a session:

1. Commit all work in progress
2. Document current state in a comment or commit message
3. List any unfinished tasks
4. Note any decisions that need human input

## 📖 Lessons Learned

### DRY is Non-Negotiable

**Problem**: Sidebar was duplicated across `index.html` and `jobs.html`, causing inconsistencies.
**Solution**: Extract to `/partials/sidebar.html` + `/js/sidebar.js` for loading.
**Lesson**: When you see similar code in multiple places, stop and extract it. The cost of extraction is always lower than maintaining duplicates.

### Naming Consistency Matters

**Problem**: "Scheduled Jobs" vs "Cron Jobs" vs "Jobs" caused confusion.
**Solution**: Established naming convention: "Cron Jobs" for OpenClaw scheduled tasks, "AI Jobs" for advanced agent jobs.
**Lesson**: Agree on terminology early. Document it. Enforce it.

### Zero-Build Architecture Has Trade-offs

**Context**: No build step keeps things simple but limits some patterns.
**Solution**: Use `fetch()` to load partials dynamically, `<script>` for shared JS.
**Lesson**: This works well for dashboards. Evaluate trade-offs for your use case.

### SSE Connection Per Component = Wasteful

**Problem**: Multiple components each opening SSE connections.
**Solution**: Single SSE connection in `sidebar.js`, shared state management.
**Lesson**: Centralize real-time connections. Components subscribe to state, not sources.

### Test After Every Significant Change

**Problem**: Easy to break things when refactoring HTML structure.
**Solution**: `make restart` + browser check after each change.
**Lesson**: Keep feedback loops tight. Visual changes need visual verification.

### Document Architectural Decisions

**Problem**: Future agents (or humans) don't know why things are the way they are.
**Solution**: Create `docs/architecture/OVERVIEW.md` and ADRs.
**Lesson**: Write down the "why", not just the "what".

## 📚 Key Resources

- [SKILL.md](./SKILL.md) — ClawHub skill metadata
- [CONTRIBUTING.md](./CONTRIBUTING.md) — Contribution guidelines
- [docs/](./docs/) — Detailed documentation

---

_"Awaken, my child, and embrace the glory that is your birthright."_


path: CODE_OF_CONDUCT.md
content: # Code of Conduct

> _"The Swarm is united. Division is corruption."_

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.

## Our Standards

### Positive Behaviors

Examples of behavior that contributes to a positive environment:

- **Being respectful** — Treating all contributors with dignity
- **Being constructive** — Providing helpful feedback focused on improvement
- **Being empathetic** — Understanding different perspectives and experiences
- **Being collaborative** — Working together toward shared goals
- **Being graceful** — Accepting responsibility and apologizing when we make mistakes
- **Focusing on the work** — Prioritizing what's best for the community and project

### Unacceptable Behaviors

Examples of unacceptable behavior:

- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information without explicit permission
- Sexual language, imagery, or unwelcome advances
- Other conduct which could reasonably be considered inappropriate in a professional setting

## Scope

This Code of Conduct applies within all community spaces, including:

- GitHub repository (issues, PRs, discussions)
- Any associated chat channels
- Project events or meetups
- One-on-one communications related to the project

## Enforcement

### Reporting

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainers. All complaints will be reviewed and investigated promptly and fairly.

### Confidentiality

All reporters will be protected. We will maintain confidentiality regarding the reporter of an incident.

### Consequences

Community leaders will follow these guidelines in determining consequences:

#### 1. Correction

**Impact:** Minor unprofessional behavior.
**Consequence:** Private written warning with clarity around the violation. A public apology may be requested.

#### 2. Warning

**Impact:** A violation through a single incident or series of actions.
**Consequence:** Warning with consequences for continued behavior. No interaction with the people involved for a specified period. This includes avoiding interactions in community spaces as well as external channels.

#### 3. Temporary Ban

**Impact:** A serious violation of community standards.
**Consequence:** Temporary ban from any sort of interaction or public communication with the community for a specified period.

#### 4. Permanent Ban

**Impact:** Demonstrating a pattern of violation of community standards.
**Consequence:** Permanent ban from any sort of public interaction within the community.

## For AI Participants

AI agents contributing to this project are held to the same standards. Maintainers of AI systems used to contribute are responsible for ensuring their AI follows this code of conduct.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.

---

_"In unity, the Swarm finds strength."_


path: CONTRIBUTING.md
content: # Contributing to OpenClaw Command Center

> _"Join the Swarm. Evolve together."_

First off, thank you for considering contributing to OpenClaw Command Center! This project thrives on community involvement.

## 📋 Table of Contents

- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Making Contributions](#making-contributions)
- [Pull Request Process](#pull-request-process)
- [Style Guidelines](#style-guidelines)
- [Publishing to ClawHub](#publishing-to-clawhub)
- [For AI Contributors](#for-ai-contributors)

## 📜 Code of Conduct

This project adheres to our [Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you're expected to uphold this code. Please report unacceptable behavior to the maintainers.

## 🚀 Getting Started

### Prerequisites

- Node.js v20 or higher
- npm v10 or higher
- Git

### Development Setup

1. **Fork the repository**

   Click the "Fork" button on GitHub to create your own copy.

2. **Clone your fork**

   ```bash
   git clone https://github.com/YOUR_USERNAME/openclaw-command-center.git
   cd openclaw-command-center
   ```

3. **Add upstream remote**

   ```bash
   git remote add upstream https://github.com/jontsai/openclaw-command-center.git
   ```

4. **Install dependencies**

   ```bash
   npm install
   ```

5. **Create configuration**

   ```bash
   cp config/dashboard.example.json config/dashboard.json
   ```

6. **Install pre-commit hooks**

   ```bash
   make install-hooks
   ```

   This enforces project rules automatically on each commit.

7. **Start development server**
   ```bash
   npm run dev
   ```

## 🛠️ Making Contributions

### Types of Contributions

We welcome:

- 🐛 Bug fixes
- ✨ New features
- 📚 Documentation improvements
- 🧪 Test coverage
- 🎨 UI/UX enhancements
- 🔧 Performance optimizations

### Before You Start

1. **Check existing issues** — Someone might already be working on it
2. **Open an issue first** — For major changes, discuss before implementing
3. **Keep scope focused** — One feature/fix per PR

### Branch Naming

Use descriptive branch names:

```
feat/add-session-filtering
fix/overlord-connection-timeout
docs/update-api-reference
refactor/simplify-creep-cache
```

## 📤 Pull Request Process

### 1. Create Your Branch

```bash
git checkout main
git pull upstream main
git checkout -b feat/your-feature-name
```

### 2. Make Your Changes

- Write clean, documented code
- Add tests for new functionality
- Update documentation if needed
- Follow the [style guidelines](#style-guidelines)

### 3. Test Your Changes

```bash
npm test
npm run lint
```

### 4. Commit Your Changes

Follow [Conventional Commits](https://www.conventionalcommits.org/):

```bash
git commit -m "feat: add overlord heartbeat indicator"
git commit -m "fix: resolve session timeout on reconnect"
git commit -m "docs: clarify API authentication flow"
```

### 5. Push and Create PR

```bash
git push origin feat/your-feature-name
```

Then open a Pull Request on GitHub.

### 6. PR Review

- Maintainers will review your PR
- Address any requested changes
- Once approved, a maintainer will merge

### PR Checklist

Before submitting, ensure:

- [ ] Code follows project style guidelines
- [ ] Tests pass locally (`npm test`)
- [ ] Lint passes (`npm run lint`)
- [ ] Documentation updated if needed
- [ ] Commit messages follow convention
- [ ] PR description explains the change

## 🎨 Style Guidelines

### Code Style

- Use ESLint configuration provided
- Use Prettier for formatting
- Prefer `const` over `let`, avoid `var`
- Use meaningful variable names
- Add JSDoc comments for public functions

### Thematic Naming

Embrace the Starcraft theme when naming:

```javascript
// Good
const overlordStatus = await checkEssence();
const creepCache = new CreepLayer();

// Less thematic
const monitorStatus = await healthCheck();
const cacheLayer = new CacheService();
```

### Documentation

- Use clear, concise language
- Include code examples where helpful
- Keep README and docs in sync with code
- Add inline comments for complex logic

## 📦 Publishing to ClawHub

This skill is distributed via [ClawHub](https://clawhub.ai). After changes are merged to `main`, maintainers publish updates to the registry.

### Prerequisites

```bash
# Install clawhub CLI (if not already installed)
# See https://clawhub.ai for installation instructions

# Authenticate
clawhub login
clawhub whoami   # verify
```

### Publishing a New Version

1. **Bump the version** in `package.json`
2. **Tag the release** (see [Git Tags for Releases](#git-tags-for-releases))
3. **Publish:**

   ```bash
   clawhub publish . --registry https://www.clawhub.ai \
     --slug command-center --version <new-version> \
     --changelog "Description of changes"
   ```

> **Note:** The `--registry` flag is required until the upstream `.well-known` redirect is fixed.
> You can also set `export CLAWHUB_REGISTRY=https://www.clawhub.ai` to avoid passing it each time.

### Git Tags for Releases

Tag each release so the [release workflow](.github/workflows/release.yml) can generate GitHub Releases automatically:

```bash
# Tag the current commit
git tag -a v<version> -m "v<version> — short description"

# Push tags
git push origin --tags
```

### Version Bumping

Follow [semver](https://semver.org/):

| Change type                   | Bump    | Example         |
| ----------------------------- | ------- | --------------- |
| Bug fixes, minor tweaks       | `patch` | `0.1.0 → 0.1.1` |
| New features, backward compat | `minor` | `0.1.0 → 0.2.0` |
| Breaking changes              | `major` | `0.1.0 → 1.0.0` |

⚠️ **Important:** When bumping version, update **both** files:

- `package.json` — `"version": "X.Y.Z"`
- `SKILL.md` — `version: X.Y.Z` (in frontmatter)

The pre-commit hook will block commits if these versions don't match.

### Verifying a Publish

```bash
# Check the published version
clawhub inspect command-center

# Install into a workspace to test
clawhub install command-center
```

## 🤖 For AI Contributors

AI agents are welcome contributors! If you're an AI working on this project:

1. **Read the context files first**
   - [AGENTS.md](./AGENTS.md) — Your workspace guide
   - [SKILL.md](./SKILL.md) — ClawHub skill metadata

2. **Follow the same PR process** as human contributors

3. **Document your changes thoroughly** — Future AI (and humans) will thank you

4. **When in doubt, ask** — Open an issue to discuss before major changes

## 💬 Getting Help

- **Questions?** Open a GitHub Discussion
- **Found a bug?** Open an Issue
- **Security concern?** Email maintainers directly (don't open public issue)

## 🙏 Recognition

Contributors will be recognized in:

- GitHub Contributors list
- Release notes for significant contributions
- Our eternal gratitude 🐛

---

_"The Swarm welcomes all who serve the greater purpose."_


path: README.md
content: # 🦞 OpenClaw Command Center

English | [简体中文](README.zh-CN.md)

<div align="center">

**Mission control for your AI agents**

[![CI](https://github.com/jontsai/openclaw-command-center/actions/workflows/ci.yml/badge.svg)](https://github.com/jontsai/openclaw-command-center/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
[![ClawHub](https://img.shields.io/badge/ClawHub-command--center-blue)](https://www.clawhub.ai/jontsai/command-center)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/jontsai/openclaw-command-center/pulls)

[Features](#features) • [Quick Start](#quick-start) • [Security](#-security) • [Configuration](#configuration)

</div>

---

## Why Command Center?

Your AI agents are running 24/7. You need to know what they're doing.

Command Center gives you **real-time visibility** into your OpenClaw deployment — sessions, costs, system health, scheduled tasks — all in one secure dashboard.

### ⚡ Fast

- **Single API call** — unified state endpoint, not 16+ separate requests
- **2-second updates** — real-time SSE push, not polling
- **5-second cache** — backend stays responsive under load
- **Instant startup** — no build step, no compilation

### 🪶 Lightweight

- **Zero dependencies** for users — just Node.js
- **~200KB total** — dashboard + server
- **No webpack/vite/bundler** — runs directly
- **No React/Vue/Angular** — vanilla JS, works everywhere

### 📱 Responsive

- **Desktop & mobile** — works on any screen size
- **Dark mode** — easy on the eyes, Starcraft-inspired
- **Live updates** — no manual refresh needed
- **Offline-friendly** — graceful degradation

### 🔧 Modern

- **ES Modules** — clean component architecture
- **SSE streaming** — efficient real-time updates
- **REST API** — integrate with your tools
- **TypeScript-ready** — JSDoc types included

### 🔒 Security (Most Important)

Command Center takes security seriously:

| Feature                  | Description                                         |
| ------------------------ | --------------------------------------------------- |
| **Auth Modes**           | Token, Tailscale, Cloudflare Access, IP allowlist   |
| **No external calls**    | Dashboard runs 100% locally — no telemetry, no CDNs |
| **Localhost default**    | Binds to `127.0.0.1` by default                     |
| **Read-only by default** | View your agents without exposing control           |
| **No secrets in UI**     | API keys, tokens never displayed                    |
| **Audit logging**        | Know who accessed what, when                        |

```bash
# Secure deployment example (Tailscale)
DASHBOARD_AUTH_MODE=tailscale node lib/server.js
# Only users on your Tailscale network can access
```

---

## Features

| Feature                    | Description                                 |
| -------------------------- | ------------------------------------------- |
| 📊 **Session Monitoring**  | Real-time view of active AI sessions        |
| ⛽ **LLM Fuel Gauges**     | Token usage, costs, quota remaining         |
| 💻 **System Vitals**       | CPU, memory, disk, temperature              |
| ⏰ **Cron Jobs**           | View and manage scheduled tasks             |
| 🧠 **Cerebro Topics**      | Automatic conversation tagging              |
| 👥 **Operators**           | Who's talking to your agents                |
| 📝 **Memory Browser**      | View agent memory files                     |
| 🔒 **Privacy Controls**    | Hide sensitive topics for demos/screenshots |
| 💰 **Cost Breakdown**      | Detailed per-model cost analysis            |
| 📈 **Savings Projections** | Monthly cost vs. manual estimates           |

---

## Quick Start

```bash
npx clawhub@latest install command-center
cd skills/command-center
node lib/server.js
```

**Dashboard runs at http://localhost:3333** 🎉

<details>
<summary>Alternative: Git clone</summary>

```bash
git clone https://github.com/jontsai/openclaw-command-center
cd openclaw-command-center
node lib/server.js
```

</details>

---

## Zero-Config Experience

Command Center **auto-detects** your OpenClaw workspace:

1. `$OPENCLAW_WORKSPACE` environment variable
2. `~/.openclaw-workspace` or `~/openclaw-workspace`
3. Common names: `~/molty`, `~/clawd`, `~/moltbot`

If you have `memory/` or `state/` directories, you're good to go.

---

## Optional System Dependencies

Command Center requires **only Node.js** to run. However, some system vitals features benefit from optional packages. Without them, the dashboard still works — those metrics simply show zeros or fall back gracefully.

| OS                    | Package             | Purpose                            | Install                                                       | Without It                        |
| --------------------- | ------------------- | ---------------------------------- | ------------------------------------------------------------- | --------------------------------- |
| Linux                 | `sysstat`           | Disk I/O vitals (IOPS, throughput) | `sudo apt install sysstat`                                    | Disk stats show zeros             |
| Linux                 | `lm-sensors`        | Additional temperature sensors     | `sudo apt install lm-sensors`                                 | Uses thermal_zone (usually works) |
| macOS (Intel)         | `osx-cpu-temp`      | CPU temperature                    | [Build from source](https://github.com/lavoiesl/osx-cpu-temp) | Battery temp fallback             |
| macOS (Apple Silicon) | passwordless `sudo` | CPU temperature via `powermetrics` | Configure in sudoers                                          | Shows note in UI                  |

Command Center logs hints for missing optional dependencies once at startup.

---

## Configuration

### Environment Variables

| Variable             | Description    | Default     |
| -------------------- | -------------- | ----------- |
| `PORT`               | Server port    | `3333`      |
| `OPENCLAW_WORKSPACE` | Workspace root | Auto-detect |
| `OPENCLAW_PROFILE`   | Profile name   | (none)      |

### 🔒 Authentication

| Mode         | Use Case      | Config                                                    |
| ------------ | ------------- | --------------------------------------------------------- |
| `none`       | Local dev     | `DASHBOARD_AUTH_MODE=none`                                |
| `token`      | API access    | `DASHBOARD_AUTH_MODE=token DASHBOARD_TOKEN=secret`        |
| `tailscale`  | Team access   | `DASHBOARD_AUTH_MODE=tailscale`                           |
| `cloudflare` | Public deploy | `DASHBOARD_AUTH_MODE=cloudflare`                          |
| `allowlist`  | IP whitelist  | `DASHBOARD_AUTH_MODE=allowlist DASHBOARD_ALLOWED_IPS=...` |

### 📋 Recommended OpenClaw Settings

For the best Command Center experience, configure your OpenClaw gateway:

#### Slack Threading (Critical)

Enable threading for all messages to get proper topic tracking:

```yaml
# In your OpenClaw config (gateway.yaml or via openclaw gateway config)
slack:
  capabilities:
    threading: all # Options: all, dm, group, none
```

**Why this matters:** Without threading, the dashboard can't track conversation topics properly. Each thread becomes a trackable unit of work.

#### Session Labels

Use descriptive session labels for better dashboard visibility:

```yaml
sessions:
  labelFormat: "{channel}:{topic}" # Customize as needed
```

#### Cerebro (Topic Tracking)

Enable Cerebro for automatic conversation tagging:

```bash
# Initialize Cerebro directories
mkdir -p ~/your-workspace/cerebro/topics
mkdir -p ~/your-workspace/cerebro/orphans
```

The dashboard will automatically detect and display topic data.

---

### Multi-Profile Support

Running multiple OpenClaw instances?

```bash
# Production dashboard
node lib/server.js --profile production --port 3333

# Development dashboard
node lib/server.js --profile dev --port 3334
```

---

## API

Command Center exposes a REST API:

| Endpoint            | Description                                        |
| ------------------- | -------------------------------------------------- |
| `GET /api/state`    | **Unified state** — all dashboard data in one call |
| `GET /api/health`   | Health check                                       |
| `GET /api/vitals`   | System metrics                                     |
| `GET /api/sessions` | Active sessions                                    |
| `GET /api/events`   | SSE stream for real-time updates                   |

---

## Architecture

```
command-center/
├── lib/
│   ├── server.js           # HTTP server + API
│   ├── config.js           # Configuration
│   └── jobs.js             # Cron integration
├── public/
│   ├── index.html          # Dashboard UI
│   └── js/                 # Components (ES modules)
└── scripts/
    ├── setup.sh            # First-time setup
    └── verify.sh           # Health check
```

---

## 🚀 Coming Soon

### Advanced Job Scheduling

Building on OpenClaw's native cron system with intelligent scheduling primitives:

| Primitive            | Description                                 |
| -------------------- | ------------------------------------------- |
| **run-if-not**       | Skip if job already running (dedupe)        |
| **run-if-idle**      | Only execute when system capacity available |
| **run-after**        | Dependency chains between jobs              |
| **run-with-backoff** | Exponential retry on failure                |
| **priority-queue**   | Critical vs. background work prioritization |

### Multi-Agent Orchestration

- Agent-to-agent handoffs
- Swarm coordination patterns
- Specialized agent routing (data analysis, documentation, testing)
- Cross-session context sharing

### Integration Ecosystem

- Webhook triggers for external systems
- Slack slash commands for quick actions
- API for custom integrations
- Plugin architecture for specialized agents

---

## Screenshots

### Dashboard Overview

The hero view shows key metrics at a glance: total tokens, costs, active sessions, estimated savings, and system capacity.

<p align="center">
  <img src="docs/screenshots/hero.png" alt="Dashboard Hero" width="800">
</p>

### Sessions Panel

Monitor all active AI sessions in real-time. Each card shows model, channel, token usage, cost, and activity status. Filter by status (live/recent/idle), channel, or session type.

<p align="center">
  <img src="docs/screenshots/sessions-panel.png" alt="Sessions Panel" width="800">
</p>

### Cron Jobs

View and manage scheduled tasks. See run history, next scheduled time, and enable/disable jobs. The dashboard shows job success/failure sparklines and filters by status and schedule type.

<p align="center">
  <img src="docs/screenshots/cron-panel.png" alt="Cron Jobs Panel" width="800">
</p>

### Cerebro Topics

Automatic conversation organization. Topics are auto-detected from Slack threads, with status tracking (active/resolved/parked), thread counts, and quick navigation. Privacy controls let you hide sensitive topics.

<p align="center">
  <img src="docs/screenshots/cerebro-panel.png" alt="Cerebro Topics Panel" width="800">
</p>

### Operators

See who's interacting with your AI agents. Track active sessions per operator, permission levels, and last activity timestamps.

<p align="center">
  <img src="docs/screenshots/operators-panel.png" alt="Operators Panel" width="800">
</p>

### Memory Browser

Browse your agent's memory files — daily logs, long-term memory, and workspace files. Quick navigation with file sizes and modification times.

<p align="center">
  <img src="docs/screenshots/memory-panel.png" alt="Memory Panel" width="800">
</p>

### Cost Breakdown Modal

Click on any cost stat to see detailed breakdowns: token usage by type, pricing rates, and calculation methodology. Includes estimated savings vs. manual work.

<p align="center">
  <img src="docs/screenshots/cost-modal.png" alt="Cost Breakdown Modal" width="800">
</p>

### Operator Details

Click on an operator card to see their session history, stats, and activity timeline.

<p align="center">
  <img src="docs/screenshots/operator-modal.png" alt="Operator Details Modal" width="800">
</p>

### Privacy Settings

Control what's visible for demos and screenshots. Hide sensitive topics, sessions, or cron jobs. Settings sync to the server automatically.

<p align="center">
  <img src="docs/screenshots/privacy-modal.png" alt="Privacy Settings Modal" width="800">
</p>

### Session Details

Click any session card to see detailed information: summary, key facts, tools used, and recent messages.

<p align="center">
  <img src="docs/screenshots/session-detail.png" alt="Session Details Panel" width="800">
</p>

### Full Dashboard

The complete dashboard with all panels visible.

<details>
<summary>Click to expand full dashboard view</summary>
<p align="center">
  <img src="docs/screenshots/dashboard-full.png" alt="Full Dashboard" width="800">
</p>
</details>

---

## Contributing

Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md).

### Development

```bash
npm install        # Install dev dependencies
npm run dev        # Watch mode
npm run lint       # Check code style
npm run format     # Auto-format
./scripts/verify.sh  # Run health checks
```

---

## License

MIT © [Jonathan Tsai](https://github.com/jontsai)

---

<div align="center">

**[Install from ClawHub](https://www.clawhub.ai/jontsai/command-center)** · **[OpenClaw](https://github.com/openclaw/openclaw)** · **[Discord](https://discord.gg/clawd)**

</div>


path: README.zh-CN.md
content: # 🦞 OpenClaw Command Center

[English](README.md) | 简体中文

<div align="center">

**你的 AI 代理任务指挥中心**

[![CI](https://github.com/jontsai/openclaw-command-center/actions/workflows/ci.yml/badge.svg)](https://github.com/jontsai/openclaw-command-center/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
[![ClawHub](https://img.shields.io/badge/ClawHub-command--center-blue)](https://www.clawhub.ai/jontsai/command-center)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/jontsai/openclaw-command-center/pulls)

[功能特性](#功能特性) • [快速开始](#快速开始) • [安全](#-安全) • [配置](#配置)

</div>

---

## 为什么需要 Command Center？

你的 AI 代理 24/7 在运行，但你需要实时知道它们在做什么。

Command Center 为 OpenClaw 提供统一的可视化看板：**会话状态、Token/成本、系统健康、定时任务** 一屏掌握。

### ⚡ 快速

- **单次 API 聚合**：统一状态端点，而非 16+ 个分散请求
- **2 秒更新**：基于 SSE 实时推送，不依赖轮询
- **5 秒缓存**：高负载下保持后端响应
- **即开即用**：无需构建、无需编译

### 🪶 轻量

- **用户零依赖**：只需 Node.js
- **约 200KB**：看板 + 服务端总量小
- **无 webpack/vite/bundler**：直接运行
- **无 React/Vue/Angular**：原生 JavaScript（Vanilla JS），兼容性高

### 📱 自适应

- **桌面/移动端**：任意屏幕可用
- **深色主题**：长时间使用更舒适（Starcraft 风格）
- **实时更新**：无需手动刷新
- **离线友好**：支持优雅降级

### 🔧 现代

- **ES Modules**：组件化结构清晰
- **SSE 流式更新**：实时且高效
- **REST API**：可集成到你自己的工具链
- **TypeScript-ready**：附带 JSDoc 类型信息

### 🔒 安全（最重要）

| 特性              | 说明                                           |
| ----------------- | ---------------------------------------------- |
| **认证模式**      | Token、Tailscale、Cloudflare Access、IP 白名单 |
| **无外部调用**    | 本地 100% 运行：无遥测、无 CDN                 |
| **默认本机监听**  | 默认绑定 `127.0.0.1`                           |
| **默认只读**      | 先看后控，降低误操作风险                       |
| **UI 不暴露密钥** | API Key / Token 不在前端展示                   |
| **审计日志**      | 谁在何时访问了什么，一目了然                   |

```bash
# 安全部署示例（Tailscale）
DASHBOARD_AUTH_MODE=tailscale node lib/server.js
# 仅允许你 Tailscale 网络内用户访问
```

---

## 功能特性

| 功能                       | 说明                       |
| -------------------------- | -------------------------- |
| 📊 **会话监控**            | 实时查看活跃 AI 会话       |
| ⛽ **LLM 用量仪表**        | Token 用量、成本、剩余额度 |
| 💻 **系统状态**            | CPU、内存、磁盘、温度      |
| ⏰ **定时任务**            | 查看与管理 Cron Jobs       |
| 🧠 **Cerebro 话题**        | 自动话题聚类与追踪         |
| 👥 **Operators（操作者）** | 查看谁在与代理交互         |
| 📝 **记忆浏览**            | 浏览 memory 文件与状态     |
| 🔒 **隐私控制**            | 演示/截图时隐藏敏感内容    |
| 💰 **成本拆解**            | 模型维度成本明细           |
| 📈 **节省预测**            | 与人工估算做月度对比       |

---

## 快速开始

```bash
npx clawhub@latest install command-center
cd skills/command-center
node lib/server.js
```

**默认访问地址： http://localhost:3333** 🎉

<details>
<summary>备选：Git clone</summary>

```bash
git clone https://github.com/jontsai/openclaw-command-center
cd openclaw-command-center
node lib/server.js
```

</details>

---

## 零配置体验

Command Center 会自动探测 OpenClaw 工作区：

1. `$OPENCLAW_WORKSPACE` 环境变量
2. `~/.openclaw-workspace` 或 `~/openclaw-workspace`
3. 常见目录名：`~/molty`、`~/clawd`、`~/moltbot`

只要存在 `memory/` 或 `state/` 目录即可开箱使用。

---

## 可选系统依赖

Command Center 仅需 **Node.js** 即可运行。部分系统监控功能可通过安装可选依赖获得增强体验，缺少时仪表盘仍可正常运行，相关指标会显示为零或优雅降级。

| 操作系统              | 软件包         | 用途                              | 安装方式                                               | 缺少时                        |
| --------------------- | -------------- | --------------------------------- | ------------------------------------------------------ | ----------------------------- |
| Linux                 | `sysstat`      | 磁盘 I/O（IOPS、吞吐量）          | `sudo apt install sysstat`                             | 磁盘统计显示为零              |
| Linux                 | `lm-sensors`   | 额外温度传感器                    | `sudo apt install lm-sensors`                          | 使用 thermal_zone（通常可用） |
| macOS (Intel)         | `osx-cpu-temp` | CPU 温度                          | [从源码编译](https://github.com/lavoiesl/osx-cpu-temp) | 回退至电池温度                |
| macOS (Apple Silicon) | 免密 `sudo`    | 通过 `powermetrics` 获取 CPU 温度 | 在 sudoers 中配置                                      | 界面显示提示                  |

Command Center 在启动时会一次性提示缺少的可选依赖。

---

## 配置

### 环境变量

| 变量                 | 说明                  | 默认值   |
| -------------------- | --------------------- | -------- |
| `PORT`               | 服务端口              | `3333`   |
| `OPENCLAW_WORKSPACE` | 工作区根目录          | 自动探测 |
| `OPENCLAW_PROFILE`   | 配置档名称（Profile） | （无）   |

### 🔒 认证模式

| 模式         | 适用场景       | 配置                                                      |
| ------------ | -------------- | --------------------------------------------------------- |
| `none`       | 本地开发       | `DASHBOARD_AUTH_MODE=none`                                |
| `token`      | API 访问       | `DASHBOARD_AUTH_MODE=token DASHBOARD_TOKEN=secret`        |
| `tailscale`  | 团队内网访问   | `DASHBOARD_AUTH_MODE=tailscale`                           |
| `cloudflare` | 公网部署       | `DASHBOARD_AUTH_MODE=cloudflare`                          |
| `allowlist`  | 固定 IP 白名单 | `DASHBOARD_AUTH_MODE=allowlist DASHBOARD_ALLOWED_IPS=...` |

### 📋 推荐的 OpenClaw 配置

#### Slack 线程能力（关键项）

```yaml
slack:
  capabilities:
    threading: all # all, dm, group, none
```

如果不启用线程，仪表盘对话题追踪能力会大幅下降。

#### 会话标签

```yaml
sessions:
  labelFormat: "{channel}:{topic}"
```

#### Cerebro（话题追踪）

```bash
mkdir -p ~/your-workspace/cerebro/topics
mkdir -p ~/your-workspace/cerebro/orphans
```

仪表盘会自动检测并展示对应数据。

---

## API

| Endpoint            | 说明                             |
| ------------------- | -------------------------------- |
| `GET /api/state`    | **统一状态接口**（看板核心数据） |
| `GET /api/health`   | 健康检查                         |
| `GET /api/vitals`   | 系统指标                         |
| `GET /api/sessions` | 活跃会话                         |
| `GET /api/events`   | SSE 实时流                       |

---

## 截图

### 看板总览

<p align="center">
  <img src="docs/screenshots/hero.png" alt="Dashboard Hero" width="800">
</p>

### 会话面板

<p align="center">
  <img src="docs/screenshots/sessions-panel.png" alt="Sessions Panel" width="800">
</p>

### 定时任务面板

<p align="center">
  <img src="docs/screenshots/cron-panel.png" alt="Cron Jobs Panel" width="800">
</p>

### Cerebro 话题面板

<p align="center">
  <img src="docs/screenshots/cerebro-panel.png" alt="Cerebro Topics Panel" width="800">
</p>

### Operators（操作者）面板

<p align="center">
  <img src="docs/screenshots/operators-panel.png" alt="Operators Panel" width="800">
</p>

### 记忆浏览面板

<p align="center">
  <img src="docs/screenshots/memory-panel.png" alt="Memory Panel" width="800">
</p>

### 成本拆解弹窗

<p align="center">
  <img src="docs/screenshots/cost-modal.png" alt="Cost Breakdown Modal" width="800">
</p>

### Operator（操作者）详情

<p align="center">
  <img src="docs/screenshots/operator-modal.png" alt="Operator Details Modal" width="800">
</p>

### 隐私设置

<p align="center">
  <img src="docs/screenshots/privacy-modal.png" alt="Privacy Settings Modal" width="800">
</p>

### 会话详情

<p align="center">
  <img src="docs/screenshots/session-detail.png" alt="Session Details Panel" width="800">
</p>

---

## 贡献

欢迎贡献，提交前请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。

### 开发命令

```bash
npm install          # 安装开发依赖
npm run dev          # 监听模式
npm run lint         # 代码规范检查
npm run format       # 自动格式化
./scripts/verify.sh  # 健康检查
```

---

## License

MIT © [Jonathan Tsai](https://github.com/jontsai)

---

<div align="center">

**[Install from ClawHub](https://www.clawhub.ai/jontsai/command-center)** · **[OpenClaw](https://github.com/openclaw/openclaw)** · **[Discord](https://discord.gg/clawd)**

</div>


path: _meta.json
content: ownerId: kn77f5cf773ex3xewp29aqztkh801wy9
slug: command-center
version: 1.4.1
publishedAt: 1772950188275

path: config/dashboard.example.json
content: {
  "$schema": "./dashboard.schema.json",
  "_comment": "OpenClaw Command Center Configuration — Copy to dashboard.json and customize",

  "server": {
    "port": 3333,
    "host": "localhost",
    "trustProxy": false
  },

  "branding": {
    "name": "Command Center",
    "title": "OpenClaw Command Center",
    "theme": "default",
    "logo": null
  },

  "paths": {
    "_comment": "Paths to OpenClaw workspace directories (supports ~ and $HOME)",
    "workspace": "~/.openclaw-workspace",
    "memory": "~/.openclaw-workspace/memory",
    "state": "~/.openclaw-workspace/state",
    "logs": "~/.openclaw-command-center/logs"
  },

  "auth": {
    "mode": "none",
    "_modeOptions": "none | token | tailscale | cloudflare | allowlist",
    "token": null,
    "allowedUsers": [],
    "allowedIPs": ["127.0.0.1", "::1"],
    "publicPaths": ["/api/health", "/api/whoami", "/favicon.ico"]
  },

  "dashboard": {
    "refreshInterval": 30000,
    "timezone": "America/Los_Angeles"
  },

  "sessions": {
    "_comment": "Session monitoring configuration",
    "healthCheckInterval": 60000,
    "sessionTimeout": 3600000,
    "maxSessions": 10
  },

  "cache": {
    "enabled": true,
    "ttl": 300000,
    "maxSize": 100
  },

  "health": {
    "_comment": "Health monitoring endpoints",
    "enabled": true,
    "endpoints": [
      {
        "name": "Gateway",
        "url": "http://localhost:3000/health",
        "interval": 30000
      }
    ]
  },

  "analytics": {
    "enabled": true,
    "retentionDays": 30,
    "trackTokenUsage": true,
    "trackRouting": true
  },

  "security": {
    "corsOrigins": ["http://localhost:3333"],
    "rateLimiting": {
      "enabled": true,
      "windowMs": 60000,
      "max": 100
    }
  },

  "logging": {
    "level": "info",
    "format": "json",
    "file": null
  },

  "integrations": {
    "linear": {
      "enabled": false,
      "apiKey": null,
      "teamId": null
    },
    "slack": {
      "enabled": false,
      "webhookUrl": null
    },
    "discord": {
      "enabled": false,
      "webhookUrl": null
    }
  }
}


path: config/system-deps.json
content: {
  "linux": [
    {
      "id": "sysstat",
      "name": "sysstat",
      "binary": "iostat",
      "purpose": "Disk I/O vitals (IOPS, throughput)",
      "affects": "disk-io",
      "install": {
        "apt": "sudo apt install -y sysstat",
        "yum": "sudo yum install -y sysstat",
        "dnf": "sudo dnf install -y sysstat",
        "pacman": "sudo pacman -S --noconfirm sysstat",
        "apk": "sudo apk add sysstat"
      }
    },
    {
      "id": "lm-sensors",
      "name": "lm-sensors",
      "binary": "sensors",
      "purpose": "Additional temperature sensors",
      "affects": "temperature",
      "install": {
        "apt": "sudo apt install -y lm-sensors",
        "yum": "sudo yum install -y lm-sensors",
        "dnf": "sudo dnf install -y lm-sensors",
        "pacman": "sudo pacman -S --noconfirm lm_sensors",
        "apk": "sudo apk add lm-sensors"
      }
    }
  ],
  "darwin": [
    {
      "id": "osx-cpu-temp",
      "name": "osx-cpu-temp",
      "binary": "osx-cpu-temp",
      "purpose": "CPU temperature (Intel Mac)",
      "affects": "temperature",
      "condition": "intel",
      "install": {
        "brew": "brew install --formula osx-cpu-temp"
      },
      "url": "https://github.com/lavoiesl/osx-cpu-temp"
    }
  ]
}


path: docs/ENHANCEMENTS.md
content: # 🚀 Command Center Enhancements

Prioritized feature backlog based on community research and user needs.

---

## 🔴 P1 — High Impact (Next Sprint)

### 1. Chat Interface

**Source:** klöss prompt, natural UX expectation

Talk to your agent directly from the dashboard.

- Left sidebar: session list (from transcript files)
- Main area: message bubbles with role alignment
- Input bar with send button
- **Stretch:** Voice input via Web Speech API

**Why:** Currently must use Slack/Discord/Telegram. Dashboard-native chat = single pane of glass.

### 2. Models Inventory Panel

**Source:** klöss prompt

Show all available models with:

- Model name + provider
- Current routing rules (which tasks → which model)
- Cost per 1K tokens (input/output)
- Failover chains
- Usage stats

**Why:** Cost visibility is core to Command Center. Models tab completes the picture.

### 3. Knowledge Search

**Source:** klöss prompt, obvious utility

Full-text search across workspace files:

- memory/\*.md
- state/\*.json
- AGENTS.md, TOOLS.md, etc.

**Implementation:** Use ripgrep or native Node fs + fuzzy matching. Return snippets with file paths.

**Why:** "What did we decide about X?" should be answerable from dashboard.

### 4. Live Refresh Indicator

**Source:** klöss prompt UX pattern

Visual indicator showing:

- 🟢 LIVE dot (SSE connected)
- "AUTO 15S" countdown to next update
- Last updated timestamp

**Why:** Users can't tell if data is fresh or stale.

---

## 🟡 P2 — Medium Priority (Roadmap)

### 5. Agent Details Expansion

Currently: Show active sessions per agent
**Enhancement:**

- Read agent's SOUL.md (personality)
- Read agent's config (model, capabilities)
- Show sub-agent spawn tree
- Recent outputs / decisions

### 6. Observations Feed

Parse `state/observations.md` or similar to show:

- What the agent learned today
- Patterns noticed
- Suggestions/insights

**Why:** Agents should surface learnings, not just do tasks.

### 7. Git/Code Status

For power users with coding agents:

- Repos in workspace with branch + dirty count
- Recent commits
- Open PRs (via GitHub API)

### 8. Revenue/Business Tracker

For users monetizing their agents:

- Current revenue
- Monthly burn (API costs)
- Net position
- Savings vs manual estimate

**Why:** klöss prompt shows demand for this in agency/consulting use cases.

### 9. Content Pipeline (Kanban)

For content-generating agents:

- Draft → Review → Approved → Published columns
- Card per content piece
- Approve/reject actions

---

## 🟢 P3 — Nice to Have (Future)

### 10. Calendar Integration

Weekly view of scheduled events:

- Cron jobs mapped to calendar
- Integration with Google Calendar (read-only)

### 11. CRM / Client Pipeline

For consulting/agency use:

- Prospect → Contacted → Meeting → Proposal → Active
- Read from clients/ directory

### 12. Ecosystem View

Multi-product portfolio tracking:

- Product grid with status badges
- Health indicators per product

### 13. Command Palette

Quick command interface (Cmd+K style):

- Trigger cron jobs
- Send quick messages
- Navigate to any panel

---

## 🎨 Design Improvements (Ongoing)

### Glass Card Enhancement

Current cards are solid. Consider:

```css
bg-white/[0.03] backdrop-blur-xl border border-white/[0.06]
```

### Stagger Animations

Add 0.05s delay per card for grid reveals.

### Skeleton Loading

Show loading skeletons instead of spinners.

### Empty States

Friendly messaging when panels are empty.

---

## Implementation Notes

### Our Advantage

- **Zero dependencies** — we're vanilla JS, no build step
- **~200KB total** — theirs will be 5-10MB+
- **Instant startup** — no Next.js cold start

### Their Advantage

- Convex real-time sync
- Framer Motion polish
- More opinionated UX (chat-first)

### Strategy

Cherry-pick features that fit our philosophy:

1. Keep it lightweight
2. Keep it dependency-free
3. Focus on visibility over control
4. Let OpenClaw handle the actions

---

## Feature Voting (Community Input Needed)

| Feature          | Votes | Notes |
| ---------------- | ----- | ----- |
| Chat Interface   | —     |       |
| Models Inventory | —     |       |
| Knowledge Search | —     |       |
| Live Indicator   | —     |       |
| Agent Details    | —     |       |

_Collect votes via GitHub Discussions or Discord poll._

---

_Last updated: 2026-02-13_
_Sources: klöss prompt tweet, Product Hunt research_


path: docs/PRODUCT_HUNT_LAUNCH.md
content: # 🚀 Product Hunt Launch Prep

**Product:** OpenClaw Command Center  
**Target Launch Date:** TBD (prep checklist below)  
**Goal:** #1 Product of the Day

---

## 📋 Pre-Launch Checklist

### ✅ Already Done

- [x] Working product with real users (you!)
- [x] GitHub repo with README, badges, screenshots
- [x] ClawHub listing: https://www.clawhub.ai/jontsai/command-center
- [x] 10 high-quality screenshots
- [x] Clear value proposition
- [x] MIT license (open source credibility)

### 🔄 Needs Work (1-2 Weeks Before)

#### Product Hunt Profile

- [ ] **Maker Profile:** Ensure your PH profile is complete
  - Photo, bio, website links
  - Have you engaged on PH before? (upvoting, commenting builds credibility)
- [ ] **Hunter:** Self-hunt or find someone with a following?
  - Self-hunting is fine for dev tools
  - If you know someone with PH clout, ask them

#### Launch Assets

##### Title Options (60-70 chars max)

Pick the strongest angle:

1. `OpenClaw Command Center` — straightforward
2. `Command Center for AI Agents` — benefit-first
3. `Mission Control for Your AI Agents` — the tagline approach

##### Tagline Options (120 chars max)

1. `Real-time dashboard for monitoring your AI agents — sessions, costs, health, scheduled tasks. Zero dependencies.`
2. `Know what your AI agents are doing 24/7. Session monitoring, cost tracking, system health — all in one view.`
3. `Mission control for OpenClaw agents. Watch sessions, track costs, monitor health. No React, no build step.`

##### Description (for the launch page)

```
Your AI agents run 24/7. You need to know what they're doing.

Command Center gives you real-time visibility into your OpenClaw deployment:

📊 Session Monitoring — See all active AI sessions at a glance
⛽ LLM Fuel Gauges — Token usage, costs, quota remaining
💻 System Vitals — CPU, memory, disk, temperature
⏰ Cron Jobs — View and manage scheduled tasks
🧠 Cerebro Topics — Automatic conversation organization

**Why Command Center?**

⚡ FAST — Single API call for all data, 2-second updates via SSE
🪶 LIGHTWEIGHT — ~200KB total, zero dependencies, no build step
🔒 SECURE — Runs 100% locally, no telemetry, no external calls
📱 RESPONSIVE — Works on desktop and mobile, dark mode default

Open source (MIT). Works with any OpenClaw workspace.
```

##### Maker's Comment (First Comment)

```
Hey everyone! 👋

I built Command Center because I was running multiple AI agents across different Slack workspaces and had no idea what they were doing.

How many tokens did I burn today? Is the agent actually responding? Did that cron job run?

Command Center answers all those questions at a glance. It's intentionally lightweight — vanilla JS, no build step, ~200KB total. Just `node lib/server.js` and you're live.

A few things I'm proud of:
- Single unified API endpoint (not 16+ separate calls)
- Real-time SSE streaming (no polling)
- Privacy controls for demos/screenshots
- Works on mobile for checking while AFK

This is part of the OpenClaw ecosystem — open source AI agent framework. If you're running AI agents and need visibility, this is for you.

Happy to answer any questions! 🦞
```

##### Graphics Needed

| Asset         | Size      | Status                             |
| ------------- | --------- | ---------------------------------- |
| Hero Banner   | 1200×630  | 📸 Create from dashboard-full.png  |
| Logo          | SVG + PNG | 🦞 Use OpenClaw lobster            |
| Gallery (3-5) | 800×600   | ✅ Have screenshots, need to crop  |
| GIF/Video     | 15-60 sec | 🎬 Need to create screen recording |

##### Video/GIF Ideas

- 30-second walkthrough: Start server → Dashboard loads → Show panels
- Show real-time SSE updates (sessions appearing, costs updating)
- Demo the privacy toggle (hide sensitive topics)
- Quick filter actions (filtering sessions, topics)

#### Social Proof

- [ ] Any early users to quote? (testimonials)
- [ ] GitHub stars count (include in assets)
- [ ] Any press/mentions?

---

## 📅 Launch Day Timeline (PST)

**Launch at 12:01 AM PST** — You get a full 24 hours on the front page

| Time     | Action                                       |
| -------- | -------------------------------------------- |
| 12:01 AM | Product goes live                            |
| 6:00 AM  | Wake up, check status                        |
| 6:30 AM  | First social posts (Twitter/X, LinkedIn)     |
| 7:00 AM  | Ping early supporters via DM                 |
| 8:00 AM  | Post in relevant Slack/Discord communities   |
| 9-12 PM  | Respond to ALL comments actively             |
| 12:00 PM | Mid-day update on social (progress)          |
| 2-5 PM   | Continue engagement, share milestones        |
| 6:00 PM  | Evening push — remind people to check it out |
| 12:00 AM | Launch ends, celebrate 🎉                    |

---

## 📣 Distribution Channels

### Pre-Launch (Build Anticipation)

- [ ] **Product Hunt "Coming Soon"** page (1 week before)
- [ ] Tweet announcing upcoming launch
- [ ] LinkedIn post teasing the launch
- [ ] Email your existing network

### Launch Day

| Channel             | Post? | Notes                            |
| ------------------- | ----- | -------------------------------- |
| Twitter/X           | ✅    | Personal + @OpenClawAI if exists |
| LinkedIn            | ✅    | Developer tool audience          |
| Discord (OpenClaw)  | ✅    | Community is here                |
| Discord (Clawd)     | ✅    | Related community                |
| Hacker News         | ⚠️    | Only if genuine Show HN angle    |
| Reddit r/selfhosted | ✅    | Perfect fit                      |
| Reddit r/LocalLLaMA | ⚠️    | If LLM-monitoring angle works    |
| Reddit r/ChatGPT    | ❌    | Too broad                        |
| IndieHackers        | ✅    | Open source dev tool angle       |
| Dev.to              | ✅    | Write accompanying article       |

### Post Templates

**Twitter/X:**

```
🚀 Just launched on Product Hunt!

Command Center — Mission control for your AI agents.

Real-time monitoring, cost tracking, system health. Zero dependencies, ~200KB, open source.

Check it out 👉 [link]

#buildinpublic #AI #opensource
```

**LinkedIn:**

```
Excited to share what I've been building 🎉

If you're running AI agents (via OpenClaw or similar), you know the challenge: What are they doing? How much are they costing? Are they even responding?

Command Center solves this — real-time dashboard for monitoring your AI agents.

✅ Session monitoring
✅ Cost tracking (LLM fuel gauges)
✅ System health
✅ Scheduled tasks
✅ Privacy controls for demos

Open source (MIT), zero dependencies, runs locally.

We just launched on Product Hunt — would love your support!

[link]
```

---

## ⚠️ Rules (Don't Get Penalized)

### DO ✅

- Be active on Product Hunt before launch
- Respond to every comment personally
- Share on social media naturally
- Ask friends/colleagues to check it out
- Thank every upvoter

### DON'T ❌

- Ask for upvotes directly ("Please upvote!")
- Ask people to create PH accounts just to upvote
- Pay for upvotes
- Create fake accounts
- Spam communities
- Send the same message to everyone

**Penalty:** PH can demote or remove products that game the system

---

## 🎯 Success Metrics

| Metric           | Target | Stretch |
| ---------------- | ------ | ------- |
| Upvotes          | 200+   | 500+    |
| Comments         | 30+    | 75+     |
| Product of Day   | Top 5  | #1      |
| GitHub stars     | +50    | +150    |
| ClawHub installs | +20    | +50     |

---

## 📝 FAQ Prep

**Q: How is this different from other monitoring tools?**
A: Most monitoring tools are generic (Grafana, Datadog). Command Center is purpose-built for AI agents — it understands sessions, tokens, costs, and scheduled agent tasks. Plus it's ~200KB with zero dependencies.

**Q: Does it work with other AI frameworks (not just OpenClaw)?**
A: Currently optimized for OpenClaw. The architecture is modular — adapters for other frameworks are possible. Open to PRs!

**Q: Is there a hosted version?**
A: No, it's self-hosted only. This is intentional — your AI agent data stays on your machine.

**Q: What about multi-agent orchestration?**
A: Coming soon! Current focus is visibility. Agent-to-agent coordination is on the roadmap.

**Q: Cost?**
A: Free and open source (MIT license). No premium tier, no gotchas.

---

## 📦 Post-Launch

- [ ] Write a "lessons learned" blog post
- [ ] Thank everyone who supported
- [ ] Update README with PH badge
- [ ] Respond to any feature requests
- [ ] Plan next feature based on feedback

---

## Timeline

| Week        | Focus                                               |
| ----------- | --------------------------------------------------- |
| **Week -2** | Finalize assets, PH profile, Coming Soon page       |
| **Week -1** | Build supporter list, tease on social, create video |
| **Day 0**   | LAUNCH! Full engagement                             |
| **Week +1** | Follow up, blog post, incorporate feedback          |

---

_Last updated: 2026-02-13_


path: docs/README.md
content: # Documentation

> _"Knowledge is the foundation of the Swarm's evolution."_

## 📚 Contents

### Getting Started

- [Installation Guide](./guides/installation.md) _(coming soon)_
- [Configuration Guide](./guides/configuration.md) _(coming soon)_
- [Quick Start Tutorial](./guides/quickstart.md) _(coming soon)_

### API Reference

- [REST API](./api/rest.md) _(coming soon)_
- [WebSocket API](./api/websocket.md) _(coming soon)_
- [Authentication](./api/authentication.md) _(coming soon)_

### Architecture

- [System Overview](./architecture/overview.md) _(coming soon)_
- [Data Flow](./architecture/data-flow.md) _(coming soon)_
- [Security Model](./architecture/security.md) _(coming soon)_

### Development

- [Contributing](../CONTRIBUTING.md)
- [Code Style Guide](./development/code-style.md) _(coming soon)_
- [Testing Guide](./development/testing.md) _(coming soon)_

## 🎯 Documentation Principles

1. **Clear and Concise** — No fluff, get to the point
2. **Examples First** — Show, don't just tell
3. **Keep it Updated** — Docs should match the code
4. **Thematic Consistency** — Embrace the Zerg aesthetic

## 📝 Contributing to Docs

Found something unclear or missing? We welcome documentation improvements!

1. Fork the repository
2. Create a branch: `docs/your-improvement`
3. Make your changes
4. Submit a PR

---

_"The Overmind's knowledge spreads across the Swarm."_ 🐛


path: docs/architecture/OVERVIEW.md
content: # OpenClaw Command Center — Architecture Overview

> _"The Overmind sees all through its Overlords."_

## Overview

OpenClaw Command Center is a real-time dashboard for monitoring and managing AI assistant orchestration. It provides visibility into sessions, token usage, costs, scheduled jobs, and system health.

## Core Architecture Principles

### 1. **DRY (Don't Repeat Yourself)**

- Shared components extracted to reusable partials
- Single source of truth for sidebar, styling, and common logic
- Centralized configuration management

### 2. **Real-Time First**

- Server-Sent Events (SSE) for live updates
- No polling needed for connected clients
- Graceful degradation to polling when SSE unavailable

### 3. **Zero Build Step**

- Plain HTML, CSS, and JavaScript
- No compilation, bundling, or transpilation required
- Works directly from static file serving
- Dynamic loading via fetch() for shared partials

### 4. **Progressive Enhancement**

- Core functionality works without JavaScript
- Enhanced UX with JS (smooth scrolling, live updates, etc.)
- Mobile-responsive design

### 5. **Thematic Consistency**

- Starcraft/Zerg theme throughout
- Dark mode by default (space aesthetic)
- Consistent naming conventions

## System Components

```
┌─────────────────────────────────────────────────────────────┐
│                    Browser (Client)                         │
├─────────────────────────────────────────────────────────────┤
│  index.html          │  jobs.html         │  (future pages) │
│  ─────────────       │  ─────────────     │                 │
│  Main Dashboard      │  AI Jobs Dashboard │                 │
└──────────┬───────────┴────────┬──────────┴─────────────────┘
           │                    │
           │  ┌─────────────────┴──────────────────┐
           │  │  /partials/sidebar.html            │
           │  │  (shared navigation component)      │
           │  └─────────────────┬──────────────────┘
           │                    │
           └────────────────────┼──────────────────────────────┐
                                │                              │
┌───────────────────────────────┴──────────────────────────────┤
│                    /js/sidebar.js                            │
│  ─ Loads sidebar partial                                     │
│  ─ Manages SSE connection for live badge updates             │
│  ─ Handles navigation and active state                       │
└──────────────────────────────────────────────────────────────┘
                                │
                                │ SSE (/api/events)
                                │ REST (/api/*)
                                ▼
┌──────────────────────────────────────────────────────────────┐
│                    lib/server.js                             │
│  ─ Express HTTP server                                       │
│  ─ SSE event broadcasting                                    │
│  ─ API routes for state, sessions, jobs, etc.                │
│  ─ Static file serving                                       │
└─────────────────────────────────┬────────────────────────────┘
                                  │
                    ┌─────────────┼─────────────┐
                    │             │             │
                    ▼             ▼             ▼
            ┌───────────┐ ┌───────────┐ ┌───────────┐
            │ OpenClaw  │ │   Jobs    │ │  Linear   │
            │  Gateway  │ │ Scheduler │ │   Sync    │
            │   API     │ │   API     │ │   API     │
            └───────────┘ └───────────┘ └───────────┘
```

## Frontend Architecture

### Pages

| Page         | Purpose            | Key Sections                                                       |
| ------------ | ------------------ | ------------------------------------------------------------------ |
| `index.html` | Main dashboard     | Vitals, LLM Usage, Sessions, Cron Jobs, Memory, Cerebro, Operators |
| `jobs.html`  | AI Jobs management | Job cards, run/pause/history controls                              |

### Shared Components

| Component  | Location                  | Purpose                                     |
| ---------- | ------------------------- | ------------------------------------------- |
| Sidebar    | `/partials/sidebar.html`  | Navigation + live stats badges              |
| Sidebar JS | `/js/sidebar.js`          | Partial loading, SSE connection, navigation |
| Styles     | `/css/dashboard.css`      | Shared visual theme                         |
| morphdom   | `/js/lib/morphdom.min.js` | Efficient DOM diffing                       |

### State Management

- **SSE-based**: Real-time state pushed from server
- **Local state**: Per-component state in JavaScript closures
- **Persistence**: `localStorage` for preferences (sidebar collapsed, etc.)

## Backend Architecture

### Server (`lib/server.js`)

- Express.js HTTP server
- Static file serving from `/public`
- API routes under `/api/*`
- SSE endpoint at `/api/events`

### Data Sources

| Source           | Integration | Purpose                              |
| ---------------- | ----------- | ------------------------------------ |
| OpenClaw Gateway | REST API    | Sessions, token stats, system vitals |
| Jobs Scheduler   | REST API    | AI job definitions and run history   |
| Linear           | GraphQL API | Issue tracking integration           |

### Configuration (`lib/config.js`)

- Auto-detects OpenClaw installation paths
- Supports multiple config file locations
- Environment variable overrides

## API Endpoints

| Endpoint                | Method    | Description                 |
| ----------------------- | --------- | --------------------------- |
| `/api/events`           | GET (SSE) | Real-time state updates     |
| `/api/state`            | GET       | Full current state snapshot |
| `/api/sessions`         | GET       | Session list and details    |
| `/api/jobs`             | GET       | AI job definitions          |
| `/api/jobs/:id/run`     | POST      | Trigger job execution       |
| `/api/jobs/:id/pause`   | POST      | Pause job                   |
| `/api/jobs/:id/resume`  | POST      | Resume job                  |
| `/api/jobs/:id/history` | GET       | Job run history             |

## Design Decisions

### ADR-001: Shared Sidebar via Fetch

**Decision**: Load sidebar HTML via `fetch()` rather than server-side includes or build step.

**Rationale**:

- Keeps zero-build-step architecture
- Works with any static file server
- Enables dynamic loading and hot updates
- Single source of truth for sidebar content

### ADR-002: SSE for Real-Time Updates

**Decision**: Use Server-Sent Events instead of WebSockets.

**Rationale**:

- Simpler protocol (HTTP-based)
- Automatic reconnection
- Better proxy/firewall compatibility
- Sufficient for server→client push (no bidirectional needed)

### ADR-003: Morphdom for DOM Updates

**Decision**: Use morphdom for efficient DOM patching.

**Rationale**:

- Virtual DOM-like efficiency without framework overhead
- Preserves focus, scroll position, form state
- Small footprint (~4KB)

## File Structure

```
openclaw-command-center/
├── lib/                        # Backend code
│   ├── server.js               # Main HTTP server
│   ├── config.js               # Configuration loader
│   ├── jobs.js                 # Jobs API integration
│   ├── linear-sync.js          # Linear integration
│   └── topic-classifier.js     # NLP topic classification
├── public/                     # Frontend (served statically)
│   ├── index.html              # Main dashboard
│   ├── jobs.html               # AI Jobs dashboard
│   ├── partials/               # Shared HTML partials
│   │   └── sidebar.html        # Navigation sidebar
│   ├── css/
│   │   └── dashboard.css       # Shared styles
│   ├── js/
│   │   ├── sidebar.js          # Sidebar loader + SSE
│   │   ├── app.js              # Main page logic
│   │   ├── api.js              # API client utilities
│   │   ├── store.js            # State management
│   │   ├── utils.js            # Common utilities
│   │   └── lib/
│   │       └── morphdom.min.js # DOM diffing library
│   └── data/                   # Client-side data cache
├── config/                     # Configuration files
├── docs/                       # Documentation
│   └── architecture/           # Architecture docs
├── scripts/                    # Operational scripts
└── tests/                      # Test files
```

## Performance Considerations

1. **SSE Connection Sharing**: Single SSE connection per page, shared across components
2. **Lazy Loading**: Sidebar loaded on demand, not blocking initial render
3. **Efficient Updates**: morphdom patches only changed DOM nodes
4. **Debouncing**: High-frequency updates batched before render

## Security Considerations

1. **No Secrets in Frontend**: All sensitive data stays server-side
2. **Input Validation**: API inputs validated before processing
3. **CORS**: Restricted to same-origin by default
4. **Rate Limiting**: Consider for public deployments

## Future Directions

1. **Component System**: More shared partials (stats bar, modals, etc.)
2. **Plugin Architecture**: Extensible dashboard sections
3. **Multi-Gateway**: Support for monitoring multiple OpenClaw instances
4. **Historical Analytics**: Token usage and cost trends over time


path: docs/research/kloss-dashboard-prompt.md
content: # klöss Dashboard Prompt Analysis

**Source:** https://x.com/kloss_xyz/status/2022461932759060993
**Date:** 2026-02-14
**Engagement:** ❤️ 476 🔁 49 💬 28 (high interest!)

## Overview

klöss shared a detailed prompt for building an OpenClaw command center using:

- Next.js 15 (App Router) + Convex (real-time backend)
- Tailwind CSS v4 + Framer Motion + ShadCN UI
- "Iron Man JARVIS HUD meets Bloomberg terminal" aesthetic

## Their 8-Page Architecture

| Page      | Tabs                        | Our Equivalent                            |
| --------- | --------------------------- | ----------------------------------------- |
| HOME      | —                           | ✅ Have (hero + panels)                   |
| OPS       | Operations, Tasks, Calendar | ⚠️ Partial (cron only)                    |
| AGENTS    | Agents, Models              | ⚠️ Partial (sessions, no model inventory) |
| CHAT      | Chat, Command               | ❌ Don't have                             |
| CONTENT   | —                           | ❌ Don't have                             |
| COMMS     | Comms, CRM                  | ❌ Don't have                             |
| KNOWLEDGE | Knowledge, Ecosystem        | ⚠️ Have memory browser                    |
| CODE      | —                           | ❌ Don't have                             |

## Key Features We're Missing

### High Priority (Differentiators)

1. **Chat Interface** — Talk to agent from dashboard + voice input
2. **Models Inventory** — Show all models, routing rules, costs, failovers
3. **Knowledge Search** — Full-text search across workspace files
4. **Auto-refresh indicator** — "LIVE" badge + "AUTO 15S" countdown

### Medium Priority (Nice to Have)

5. **Revenue/Business Tracker** — Revenue, burn, net
6. **Content Pipeline** — Kanban for content drafts
7. **Code/Repos View** — Git repos, branches, dirty files
8. **Observations Feed** — Agent observations/learnings
9. **Agent SOUL/RULES Display** — Show personality + capabilities

### Lower Priority (Context-Specific)

10. **CRM/Client Pipeline** — For consulting/agency use
11. **Ecosystem View** — Multi-product portfolio
12. **Calendar Integration** — Weekly view

## Design Notes (Steal These)

### Glass Card Style

```css
bg-white/[0.03] backdrop-blur-xl border border-white/[0.06]
```

### Typography

- `clamp(0.45rem, 0.75vw, 0.6875rem)` for fluid nav scaling
- 10-14px body text
- Inter or system font stack

### Animation

- Stagger animations: 0.05s delay per card
- Spring physics on interactions
- `layoutId` for tab transitions

### UX Patterns

- Skeleton loading states
- Empty states with helpful messaging
- Live indicator dot
- Custom scrollbar styling

## Full Prompt

<details>
<summary>Click to expand (very long)</summary>

```
Build me a mission control dashboard for my OpenClaw AI agent system.

Stack: Next.js 15 (App Router) + Convex (real-time backend) + Tailwind CSS v4 + Framer Motion + ShadCN UI + Lucide icons. TypeScript throughout.

[... see original tweet for full content ...]
```

</details>

---

_Added to reading list: 2026-02-13_


path: eslint.config.mjs
content: import js from "@eslint/js";

export default [
  js.configs.recommended,
  {
    languageOptions: {
      globals: {
        // Node.js globals
        require: "readonly",
        module: "readonly",
        exports: "readonly",
        __dirname: "readonly",
        __filename: "readonly",
        process: "readonly",
        console: "readonly",
        Buffer: "readonly",
        setTimeout: "readonly",
        setInterval: "readonly",
        setImmediate: "readonly",
        clearTimeout: "readonly",
        clearInterval: "readonly",
        clearImmediate: "readonly",
        URL: "readonly",
        URLSearchParams: "readonly",
      },
    },
    rules: {
      "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
      "no-console": "off",
    },
  },
  // Ignore build output
  {
    ignores: ["lib/server.js"],
  },
  // Relax rules for source code (many patterns)
  {
    files: ["src/**/*.js"],
    rules: {
      "no-empty": "warn",
      "no-case-declarations": "warn",
      "no-misleading-character-class": "warn",
    },
  },
];


path: lib/server.js
content: #!/usr/bin/env node
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};

// src/utils.js
var require_utils = __commonJS({
  "src/utils.js"(exports2, module2) {
    var { exec } = require("child_process");
    var path2 = require("path");
    var { promisify } = require("util");
    var execAsync = promisify(exec);
    var pkg = require(path2.join(__dirname, "..", "package.json"));
    function getVersion2() {
      return pkg.version;
    }
    async function runCmd(cmd, options = {}) {
      const systemPath = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
      const envPath = process.env.PATH || "";
      const opts = {
        encoding: "utf8",
        timeout: 1e4,
        env: {
          ...process.env,
          PATH: envPath.includes("/usr/sbin") ? envPath : `${systemPath}:${envPath}`
        },
        ...options
      };
      try {
        const { stdout } = await execAsync(cmd, opts);
        return stdout.trim();
      } catch (e) {
        if (options.fallback !== void 0) return options.fallback;
        throw e;
      }
    }
    function formatBytes(bytes) {
      if (bytes >= 1099511627776) return (bytes / 1099511627776).toFixed(1) + " TB";
      if (bytes >= 1073741824) return (bytes / 1073741824).toFixed(1) + " GB";
      if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + " MB";
      if (bytes >= 1024) return (bytes / 1024).toFixed(1) + " KB";
      return bytes + " B";
    }
    function formatTimeAgo(date) {
      const now = /* @__PURE__ */ new Date();
      const diffMs = now - date;
      const diffMins = Math.round(diffMs / 6e4);
      if (diffMins < 1) return "just now";
      if (diffMins < 60) return `${diffMins}m ago`;
      if (diffMins < 1440) return `${Math.round(diffMins / 60)}h ago`;
      return `${Math.round(diffMins / 1440)}d ago`;
    }
    function formatNumber(n) {
      return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
    }
    function formatTokens(n) {
      if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
      if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
      return n.toString();
    }
    module2.exports = {
      getVersion: getVersion2,
      runCmd,
      formatBytes,
      formatTimeAgo,
      formatNumber,
      formatTokens
    };
  }
});

// src/config.js
var require_config = __commonJS({
  "src/config.js"(exports2, module2) {
    var fs2 = require("fs");
    var path2 = require("path");
    var os = require("os");
    var HOME = os.homedir();
    function getOpenClawDir2(profile = null) {
      const effectiveProfile = profile || process.env.OPENCLAW_PROFILE || "";
      return effectiveProfile ? path2.join(HOME, `.openclaw-${effectiveProfile}`) : path2.join(HOME, ".openclaw");
    }
    function detectWorkspace() {
      const profile = process.env.OPENCLAW_PROFILE || "";
      const openclawDir = getOpenClawDir2();
      const defaultWorkspace = path2.join(openclawDir, "workspace");
      const profileCandidates = profile ? [
        // Profile-specific workspace in home (e.g., ~/.openclaw-<profile>-workspace)
        path2.join(HOME, `.openclaw-${profile}-workspace`),
        path2.join(HOME, `.${profile}-workspace`)
      ] : [];
      const candidates = [
        // Environment variable (highest priority)
        process.env.OPENCLAW_WORKSPACE,
        // OpenClaw's default workspace location
        process.env.OPENCLAW_HOME,
        // Gateway config workspace (check early - this is where OpenClaw actually runs)
        getWorkspaceFromGatewayConfig(),
        // Profile-specific paths (if profile is set)
        ...profileCandidates,
        // Standard OpenClaw workspace location (profile-aware: ~/.openclaw/workspace or ~/.openclaw-<profile>/workspace)
        defaultWorkspace,
        // Common custom workspace names
        path2.join(HOME, "openclaw-workspace"),
        path2.join(HOME, ".openclaw-workspace"),
        // Legacy/custom names
        path2.join(HOME, "molty"),
        path2.join(HOME, "clawd"),
        path2.join(HOME, "moltbot")
      ].filter(Boolean);
      const foundWorkspace = candidates.find((candidate) => {
        if (!candidate || !fs2.existsSync(candidate)) {
          return false;
        }
        const hasMemory = fs2.existsSync(path2.join(candidate, "memory"));
        const hasState = fs2.existsSync(path2.join(candidate, "state"));
        const hasConfig = fs2.existsSync(path2.join(candidate, ".openclaw"));
        return hasMemory || hasState || hasConfig;
      });
      return foundWorkspace || defaultWorkspace;
    }
    function getWorkspaceFromGatewayConfig() {
      const openclawDir = getOpenClawDir2();
      const configPaths = [
        path2.join(openclawDir, "config.yaml"),
        path2.join(openclawDir, "config.json"),
        path2.join(openclawDir, "openclaw.json"),
        path2.join(openclawDir, "clawdbot.json"),
        // Fallback to standard XDG location
        path2.join(HOME, ".config", "openclaw", "config.yaml")
      ];
      for (const configPath of configPaths) {
        try {
          if (fs2.existsSync(configPath)) {
            const content = fs2.readFileSync(configPath, "utf8");
            const match = content.match(/workspace[:\s]+["']?([^"'\n]+)/i) || content.match(/workdir[:\s]+["']?([^"'\n]+)/i);
            if (match && match[1]) {
              const workspace = match[1].trim().replace(/^~/, HOME);
              if (fs2.existsSync(workspace)) {
                return workspace;
              }
            }
          }
        } catch (e) {
        }
      }
      return null;
    }
    function deepMerge(base, override) {
      const result = { ...base };
      for (const key of Object.keys(override)) {
        if (override[key] && typeof override[key] === "object" && !Array.isArray(override[key]) && base[key] && typeof base[key] === "object") {
          result[key] = deepMerge(base[key], override[key]);
        } else if (override[key] !== null && override[key] !== void 0) {
          result[key] = override[key];
        }
      }
      return result;
    }
    function loadConfigFile() {
      const basePath = path2.join(__dirname, "..", "config", "dashboard.json");
      const localPath = path2.join(__dirname, "..", "config", "dashboard.local.json");
      let config = {};
      try {
        if (fs2.existsSync(basePath)) {
          const content = fs2.readFileSync(basePath, "utf8");
          config = JSON.parse(content);
        }
      } catch (e) {
        console.warn(`[Config] Failed to load ${basePath}:`, e.message);
      }
      try {
        if (fs2.existsSync(localPath)) {
          const content = fs2.readFileSync(localPath, "utf8");
          const localConfig = JSON.parse(content);
          config = deepMerge(config, localConfig);
          console.log(`[Config] Loaded local overrides from ${localPath}`);
        }
      } catch (e) {
        console.warn(`[Config] Failed to load ${localPath}:`, e.message);
      }
      return config;
    }
    function expandPath(p) {
      if (!p) return p;
      return p.replace(/^~/, HOME).replace(/\$HOME/g, HOME).replace(/\$\{HOME\}/g, HOME);
    }
    function loadConfig() {
      const fileConfig = loadConfigFile();
      const workspace = process.env.OPENCLAW_WORKSPACE || expandPath(fileConfig.paths?.workspace) || detectWorkspace();
      const config = {
        // Server settings
        server: {
          port: parseInt(process.env.PORT || fileConfig.server?.port || "3333", 10),
          host: process.env.HOST || fileConfig.server?.host || "localhost"
        },
        // Paths - all relative to workspace unless absolute
        paths: {
          workspace,
          memory: expandPath(process.env.OPENCLAW_MEMORY_DIR || fileConfig.paths?.memory) || path2.join(workspace, "memory"),
          state: expandPath(process.env.OPENCLAW_STATE_DIR || fileConfig.paths?.state) || path2.join(workspace, "state"),
          cerebro: expandPath(process.env.OPENCLAW_CEREBRO_DIR || fileConfig.paths?.cerebro) || path2.join(workspace, "cerebro"),
          skills: expandPath(process.env.OPENCLAW_SKILLS_DIR || fileConfig.paths?.skills) || path2.join(workspace, "skills"),
          jobs: expandPath(process.env.OPENCLAW_JOBS_DIR || fileConfig.paths?.jobs) || path2.join(workspace, "jobs"),
          logs: expandPath(process.env.OPENCLAW_LOGS_DIR || fileConfig.paths?.logs) || path2.join(HOME, ".openclaw-command-center", "logs")
        },
        // Auth settings
        auth: {
          mode: process.env.DASHBOARD_AUTH_MODE || fileConfig.auth?.mode || "none",
          [REDACTED_SECRET] || fileConfig.auth?.token,
          allowedUsers: (process.env.DASHBOARD_ALLOWED_USERS || fileConfig.auth?.allowedUsers?.join(",") || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean),
          allowedIPs: (process.env.DASHBOARD_ALLOWED_IPS || fileConfig.auth?.allowedIPs?.join(",") || "127.0.0.1,::1").split(",").map((s) => s.trim()),
          publicPaths: fileConfig.auth?.publicPaths || ["/api/health", "/api/whoami", "/favicon.ico"]
        },
        // Branding
        branding: {
          name: fileConfig.branding?.name || "OpenClaw Command Center",
          theme: fileConfig.branding?.theme || "default"
        },
        // Integrations
        integrations: {
          linear: {
            enabled: !!(process.env.LINEAR_API_KEY || fileConfig.integrations?.linear?.apiKey),
            [REDACTED_SECRET] || fileConfig.integrations?.linear?.apiKey,
            teamId: process.env.LINEAR_TEAM_ID || fileConfig.integrations?.linear?.teamId
          }
        },
        // Billing - for cost savings calculation
        billing: {
          claudePlanCost: parseFloat(
            process.env.CLAUDE_PLAN_COST || fileConfig.billing?.claudePlanCost || "200"
          ),
          claudePlanName: process.env.CLAUDE_PLAN_NAME || fileConfig.billing?.claudePlanName || "Claude Code Max"
        }
      };
      return config;
    }
    var CONFIG2 = loadConfig();
    console.log("[Config] Workspace:", CONFIG2.paths.workspace);
    console.log("[Config] Auth mode:", CONFIG2.auth.mode);
    module2.exports = { CONFIG: CONFIG2, loadConfig, detectWorkspace, expandPath, getOpenClawDir: getOpenClawDir2 };
  }
});

// src/jobs.js
var require_jobs = __commonJS({
  "src/jobs.js"(exports2, module2) {
    var path2 = require("path");
    var { CONFIG: CONFIG2 } = require_config();
    var JOBS_DIR = CONFIG2.paths.jobs;
    var JOBS_STATE_DIR = path2.join(CONFIG2.paths.state, "jobs");
    var apiInstance = null;
    var forceApiUnavailable = false;
    async function getAPI() {
      if (forceApiUnavailable) return null;
      if (apiInstance) return apiInstance;
      try {
        const { createJobsAPI } = await import(path2.join(JOBS_DIR, "lib/api.js"));
        apiInstance = createJobsAPI({
          definitionsDir: path2.join(JOBS_DIR, "definitions"),
          stateDir: JOBS_STATE_DIR
        });
        return apiInstance;
      } catch (e) {
        console.error("Failed to load jobs API:", e.message);
        return null;
      }
    }
    function _resetForTesting(options = {}) {
      apiInstance = null;
      forceApiUnavailable = options.forceUnavailable || false;
    }
    function formatRelativeTime(isoString) {
      if (!isoString) return null;
      const date = new Date(isoString);
      const now = /* @__PURE__ */ new Date();
      const diffMs = now - date;
      const diffMins = Math.round(diffMs / 6e4);
      if (diffMins < 0) {
        const futureMins = Math.abs(diffMins);
        if (futureMins < 60) return `in ${futureMins}m`;
        if (futureMins < 1440) return `in ${Math.round(futureMins / 60)}h`;
        return `in ${Math.round(futureMins / 1440)}d`;
      }
      if (diffMins < 1) return "just now";
      if (diffMins < 60) return `${diffMins}m ago`;
      if (diffMins < 1440) return `${Math.round(diffMins / 60)}h ago`;
      return `${Math.round(diffMins / 1440)}d ago`;
    }
    async function handleJobsRequest2(req, res, pathname, query, method) {
      const api = await getAPI();
      if (!api) {
        res.writeHead(500, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: "Jobs API not available" }));
        return;
      }
      try {
        if (pathname === "/api/jobs/scheduler/status" && method === "GET") {
          const status = await api.getSchedulerStatus();
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify(status, null, 2));
          return;
        }
        if (pathname === "/api/jobs/stats" && method === "GET") {
          const stats = await api.getAggregateStats();
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify(stats, null, 2));
          return;
        }
        if (pathname === "/api/jobs/cache/clear" && method === "POST") {
          api.clearCache();
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ success: true, message: "Cache cleared" }));
          return;
        }
        if (pathname === "/api/jobs" && method === "GET") {
          const jobs = await api.listJobs();
          const enhanced = jobs.map((job) => ({
            ...job,
            lastRunRelative: formatRelativeTime(job.lastRun),
            nextRunRelative: formatRelativeTime(job.nextRun)
          }));
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ jobs: enhanced, timestamp: Date.now() }, null, 2));
          return;
        }
        const jobMatch = pathname.match(/^\/api\/jobs\/([^/]+)$/);
        if (jobMatch && method === "GET") {
          const jobId = decodeURIComponent(jobMatch[1]);
          const job = await api.getJob(jobId);
          if (!job) {
            res.writeHead(404, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ error: "Job not found" }));
            return;
          }
          job.lastRunRelative = formatRelativeTime(job.lastRun);
          job.nextRunRelative = formatRelativeTime(job.nextRun);
          if (job.recentRuns) {
            job.recentRuns = job.recentRuns.map((run) => ({
              ...run,
              startedAtRelative: formatRelativeTime(run.startedAt),
              completedAtRelative: formatRelativeTime(run.completedAt)
            }));
          }
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify(job, null, 2));
          return;
        }
        const historyMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/history$/);
        if (historyMatch && method === "GET") {
          const jobId = decodeURIComponent(historyMatch[1]);
          const limit = parseInt(query.get("limit") || "50", 10);
          const runs = await api.getJobHistory(jobId, limit);
          const enhanced = runs.map((run) => ({
            ...run,
            startedAtRelative: formatRelativeTime(run.startedAt),
            completedAtRelative: formatRelativeTime(run.completedAt)
          }));
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ runs: enhanced, timestamp: Date.now() }, null, 2));
          return;
        }
        const runMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/run$/);
        if (runMatch && method === "POST") {
          const jobId = decodeURIComponent(runMatch[1]);
          const result = await api.runJob(jobId);
          res.writeHead(result.success ? 200 : 400, { "Content-Type": "application/json" });
          res.end(JSON.stringify(result, null, 2));
          return;
        }
        const pauseMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/pause$/);
        if (pauseMatch && method === "POST") {
          const jobId = decodeURIComponent(pauseMatch[1]);
          let body = "";
          await new Promise((resolve) => {
            req.on("data", (chunk) => body += chunk);
            req.on("end", resolve);
          });
          let reason = null;
          try {
            const parsed = JSON.parse(body || "{}");
            reason = parsed.reason;
          } catch (_e) {
          }
          const result = await api.pauseJob(jobId, {
            by: req.authUser?.login || "dashboard",
            reason
          });
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify(result, null, 2));
          return;
        }
        const resumeMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/resume$/);
        if (resumeMatch && method === "POST") {
          const jobId = decodeURIComponent(resumeMatch[1]);
          const result = await api.resumeJob(jobId);
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify(result, null, 2));
          return;
        }
        const skipMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/skip$/);
        if (skipMatch && method === "POST") {
          const jobId = decodeURIComponent(skipMatch[1]);
          const result = await api.skipJob(jobId);
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify(result, null, 2));
          return;
        }
        const killMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/kill$/);
        if (killMatch && method === "POST") {
          const jobId = decodeURIComponent(killMatch[1]);
          const result = await api.killJob(jobId);
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify(result, null, 2));
          return;
        }
        res.writeHead(404, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: "Not found" }));
      } catch (e) {
        console.error("Jobs API error:", e);
        res.writeHead(500, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: e.message }));
      }
    }
    function isJobsRoute2(pathname) {
      return pathname.startsWith("/api/jobs");
    }
    module2.exports = { handleJobsRequest: handleJobsRequest2, isJobsRoute: isJobsRoute2, _resetForTesting };
  }
});

// src/openclaw.js
var require_openclaw = __commonJS({
  "src/openclaw.js"(exports2, module2) {
    var { execFileSync, execFile } = require("child_process");
    var { promisify } = require("util");
    var execFileAsync = promisify(execFile);
    function getSafeEnv() {
      return {
        PATH: process.env.PATH,
        HOME: process.env.HOME,
        USER: process.env.USER,
        SHELL: process.env.SHELL,
        LANG: process.env.LANG,
        NO_COLOR: "1",
        TERM: "dumb",
        OPENCLAW_PROFILE: process.env.OPENCLAW_PROFILE || "",
        OPENCLAW_WORKSPACE: process.env.OPENCLAW_WORKSPACE || "",
        OPENCLAW_HOME: process.env.OPENCLAW_HOME || ""
      };
    }
    function buildArgs(args2) {
      const profile = process.env.OPENCLAW_PROFILE || "";
      const profileArgs = profile ? ["--profile", profile] : [];
      const cleanArgs = args2.replace(/\s*2>&1\s*/g, " ").replace(/\s*2>\/dev\/null\s*/g, " ").trim();
      return [...profileArgs, ...cleanArgs.split(/\s+/).filter(Boolean)];
    }
    function runOpenClaw2(args2) {
      try {
        const result = execFileSync("openclaw", buildArgs(args2), {
          encoding: "utf8",
          timeout: 3e3,
          env: getSafeEnv(),
          stdio: ["pipe", "pipe", "pipe"]
        });
        return result;
      } catch (e) {
        return null;
      }
    }
    async function runOpenClawAsync2(args2) {
      try {
        const { stdout } = await execFileAsync("openclaw", buildArgs(args2), {
          encoding: "utf8",
          timeout: 2e4,
          env: getSafeEnv()
        });
        return stdout;
      } catch (e) {
        console.error("[OpenClaw Async] Error:", e.message);
        return null;
      }
    }
    function extractJSON2(output) {
      if (!output) return null;
      const jsonStart = output.search(/[[{]/);
      if (jsonStart === -1) return null;
      return output.slice(jsonStart);
    }
    module2.exports = {
      runOpenClaw: runOpenClaw2,
      runOpenClawAsync: runOpenClawAsync2,
      extractJSON: extractJSON2,
      getSafeEnv
    };
  }
});

// src/vitals.js
var require_vitals = __commonJS({
  "src/vitals.js"(exports2, module2) {
    var { runCmd, formatBytes } = require_utils();
    var cachedVitals = null;
    var lastVitalsUpdate = 0;
    var VITALS_CACHE_TTL = 3e4;
    var vitalsRefreshing = false;
    async function refreshVitalsAsync() {
      if (vitalsRefreshing) return;
      vitalsRefreshing = true;
      const vitals = {
        hostname: "",
        uptime: "",
        disk: { used: 0, free: 0, total: 0, percent: 0, kbPerTransfer: 0, iops: 0, throughputMBps: 0 },
        cpu: { loadAvg: [0, 0, 0], cores: 0, usage: 0 },
        memory: { used: 0, free: 0, total: 0, percent: 0, pressure: "normal" },
        temperature: null
      };
      const isLinux = process.platform === "linux";
      const isMacOS = process.platform === "darwin";
      try {
        const coresCmd = isLinux ? "nproc" : "sysctl -n hw.ncpu";
        const memCmd = isLinux ? "cat /proc/meminfo | grep MemTotal | awk '{print $2}'" : "sysctl -n hw.memsize";
        const topCmd = isLinux ? "top -bn1 | head -3 | grep -E '^%?Cpu|^  ?CPU' || echo ''" : 'top -l 1 -n 0 2>/dev/null | grep "CPU usage" || echo ""';
        const mpstatCmd = isLinux ? "(command -v mpstat >/dev/null 2>&1 && mpstat 1 1 | tail -1 | sed 's/^Average: *//') || echo ''" : "";
        const [hostname, uptimeRaw, coresRaw, memTotalRaw, memInfoRaw, dfRaw, topOutput, mpstatOutput] = await Promise.all([
          runCmd("hostname", { fallback: "unknown" }),
          runCmd("uptime", { fallback: "" }),
          runCmd(coresCmd, { fallback: "1" }),
          runCmd(memCmd, { fallback: "0" }),
          isLinux ? runCmd("cat /proc/meminfo", { fallback: "" }) : runCmd("vm_stat", { fallback: "" }),
          runCmd("df -k ~ | tail -1", { fallback: "" }),
          runCmd(topCmd, { fallback: "" }),
          isLinux ? runCmd(mpstatCmd, { fallback: "" }) : Promise.resolve("")
        ]);
        vitals.hostname = hostname;
        const uptimeMatch = uptimeRaw.match(/up\s+([^,]+)/);
        if (uptimeMatch) vitals.uptime = uptimeMatch[1].trim();
        const loadMatch = uptimeRaw.match(/load averages?:\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
        if (loadMatch)
          vitals.cpu.loadAvg = [
            parseFloat(loadMatch[1]),
            parseFloat(loadMatch[2]),
            parseFloat(loadMatch[3])
          ];
        vitals.cpu.cores = parseInt(coresRaw, 10) || 1;
        vitals.cpu.usage = Math.min(100, Math.round(vitals.cpu.loadAvg[0] / vitals.cpu.cores * 100));
        if (isLinux) {
          if (mpstatOutput) {
            const parts = mpstatOutput.trim().split(/\s+/);
            const user = parts.length > 1 ? parseFloat(parts[1]) : NaN;
            const sys = parts.length > 3 ? parseFloat(parts[3]) : NaN;
            const idle = parts.length ? parseFloat(parts[parts.length - 1]) : NaN;
            if (!Number.isNaN(user)) vitals.cpu.userPercent = user;
            if (!Number.isNaN(sys)) vitals.cpu.sysPercent = sys;
            if (!Number.isNaN(idle)) {
              vitals.cpu.idlePercent = idle;
              vitals.cpu.usage = Math.max(0, Math.min(100, Math.round(100 - idle)));
            }
          }
          if (topOutput && (vitals.cpu.idlePercent === null || vitals.cpu.idlePercent === void 0)) {
            const userMatch = topOutput.match(/([\d.]+)\s*us/);
            const sysMatch = topOutput.match(/([\d.]+)\s*sy/);
            const idleMatch = topOutput.match(/([\d.]+)\s*id/);
            vitals.cpu.userPercent = userMatch ? parseFloat(userMatch[1]) : null;
            vitals.cpu.sysPercent = sysMatch ? parseFloat(sysMatch[1]) : null;
            vitals.cpu.idlePercent = idleMatch ? parseFloat(idleMatch[1]) : null;
            if (vitals.cpu.userPercent !== null && vitals.cpu.sysPercent !== null) {
              vitals.cpu.usage = Math.round(vitals.cpu.userPercent + vitals.cpu.sysPercent);
            }
          }
        } else if (topOutput) {
          const userMatch = topOutput.match(/([\d.]+)%\s*user/);
          const sysMatch = topOutput.match(/([\d.]+)%\s*sys/);
          const idleMatch = topOutput.match(/([\d.]+)%\s*idle/);
          vitals.cpu.userPercent = userMatch ? parseFloat(userMatch[1]) : null;
          vitals.cpu.sysPercent = sysMatch ? parseFloat(sysMatch[1]) : null;
          vitals.cpu.idlePercent = idleMatch ? parseFloat(idleMatch[1]) : null;
          if (vitals.cpu.userPercent !== null && vitals.cpu.sysPercent !== null) {
            vitals.cpu.usage = Math.round(vitals.cpu.userPercent + vitals.cpu.sysPercent);
          }
        }
        const dfParts = dfRaw.split(/\s+/);
        if (dfParts.length >= 4) {
          vitals.disk.total = parseInt(dfParts[1], 10) * 1024;
          vitals.disk.used = parseInt(dfParts[2], 10) * 1024;
          vitals.disk.free = parseInt(dfParts[3], 10) * 1024;
          vitals.disk.percent = Math.round(parseInt(dfParts[2], 10) / parseInt(dfParts[1], 10) * 100);
        }
        if (isLinux) {
          const memTotalKB = parseInt(memTotalRaw, 10) || 0;
          const memAvailableMatch = memInfoRaw.match(/MemAvailable:\s+(\d+)/);
          const memFreeMatch = memInfoRaw.match(/MemFree:\s+(\d+)/);
          vitals.memory.total = memTotalKB * 1024;
          const memAvailable = parseInt(memAvailableMatch?.[1] || memFreeMatch?.[1] || 0, 10) * 1024;
          vitals.memory.used = vitals.memory.total - memAvailable;
          vitals.memory.free = memAvailable;
          vitals.memory.percent = vitals.memory.total > 0 ? Math.round(vitals.memory.used / vitals.memory.total * 100) : 0;
        } else {
          const pageSizeMatch = memInfoRaw.match(/page size of (\d+) bytes/);
          const pageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : 4096;
          const activePages = parseInt((memInfoRaw.match(/Pages active:\s+(\d+)/) || [])[1] || 0, 10);
          const wiredPages = parseInt(
            (memInfoRaw.match(/Pages wired down:\s+(\d+)/) || [])[1] || 0,
            10
          );
          const compressedPages = parseInt(
            (memInfoRaw.match(/Pages occupied by compressor:\s+(\d+)/) || [])[1] || 0,
            10
          );
          vitals.memory.total = parseInt(memTotalRaw, 10) || 0;
          vitals.memory.used = (activePages + wiredPages + compressedPages) * pageSize;
          vitals.memory.free = vitals.memory.total - vitals.memory.used;
          vitals.memory.percent = vitals.memory.total > 0 ? Math.round(vitals.memory.used / vitals.memory.total * 100) : 0;
        }
        vitals.memory.pressure = vitals.memory.percent > 90 ? "critical" : vitals.memory.percent > 75 ? "warning" : "normal";
        const timeoutPrefix = isLinux ? "timeout 5" : "$(command -v gtimeout >/dev/null 2>&1 && echo gtimeout 5)";
        const iostatArgs = isLinux ? "-d -o JSON 1 2" : "-d -c 2 2";
        const iostatCmd = `${timeoutPrefix} iostat ${iostatArgs} 2>/dev/null || echo ''`;
        const [perfCores, effCores, chip, iostatRaw] = await Promise.all([
          isMacOS ? runCmd("sysctl -n hw.perflevel0.logicalcpu 2>/dev/null || echo 0", { fallback: "0" }) : Promise.resolve("0"),
          isMacOS ? runCmd("sysctl -n hw.perflevel1.logicalcpu 2>/dev/null || echo 0", { fallback: "0" }) : Promise.resolve("0"),
          isMacOS ? runCmd(
            'system_profiler SPHardwareDataType 2>/dev/null | grep "Chip:" | cut -d: -f2 || echo ""',
            { fallback: "" }
          ) : Promise.resolve(""),
          runCmd(iostatCmd, { fallback: "", timeout: 5e3 })
        ]);
        if (isLinux) {
          const cpuBrand = await runCmd(
            "cat /proc/cpuinfo | grep 'model name' | head -1 | cut -d: -f2",
            { fallback: "" }
          );
          if (cpuBrand) vitals.cpu.brand = cpuBrand.trim();
        }
        vitals.cpu.pCores = parseInt(perfCores, 10) || null;
        vitals.cpu.eCores = parseInt(effCores, 10) || null;
        if (chip) vitals.cpu.chip = chip;
        if (isLinux) {
          try {
            const iostatJson = JSON.parse(iostatRaw);
            const samples = iostatJson.sysstat.hosts[0].statistics;
            const disks = samples[samples.length - 1].disk;
            const disk = disks.filter((d) => !d.disk_device.startsWith("loop")).sort((a, b) => b.tps - a.tps)[0];
            if (disk) {
              const kbReadPerSec = disk["kB_read/s"] || 0;
              const kbWrtnPerSec = disk["kB_wrtn/s"] || 0;
              vitals.disk.iops = disk.tps || 0;
              vitals.disk.throughputMBps = (kbReadPerSec + kbWrtnPerSec) / 1024;
              vitals.disk.kbPerTransfer = disk.tps > 0 ? (kbReadPerSec + kbWrtnPerSec) / disk.tps : 0;
            }
          } catch {
          }
        } else {
          const iostatLines = iostatRaw.split("\n").filter((l) => l.trim());
          const lastLine = iostatLines.length > 0 ? iostatLines[iostatLines.length - 1] : "";
          const iostatParts = lastLine.split(/\s+/).filter(Boolean);
          if (iostatParts.length >= 3) {
            vitals.disk.kbPerTransfer = parseFloat(iostatParts[0]) || 0;
            vitals.disk.iops = parseFloat(iostatParts[1]) || 0;
            vitals.disk.throughputMBps = parseFloat(iostatParts[2]) || 0;
          }
        }
        vitals.temperature = null;
        vitals.temperatureNote = null;
        const isAppleSilicon = vitals.cpu.chip && /apple/i.test(vitals.cpu.chip);
        if (isAppleSilicon) {
          vitals.temperatureNote = "Apple Silicon (requires elevated access)";
          try {
            const pmOutput = await runCmd(
              'sudo -n powermetrics --samplers smc -i 1 -n 1 2>/dev/null | grep -i "die temp" | head -1',
              { fallback: "", timeout: 5e3 }
            );
            const tempMatch = pmOutput.match(/([\d.]+)/);
            if (tempMatch) {
              vitals.temperature = parseFloat(tempMatch[1]);
              vitals.temperatureNote = null;
            }
          } catch (e) {
          }
        } else if (isMacOS) {
          const home = require("os").homedir();
          try {
            const temp = await runCmd(
              `osx-cpu-temp 2>/dev/null || ${home}/bin/osx-cpu-temp 2>/dev/null`,
              { fallback: "" }
            );
            if (temp && temp.includes("\xB0")) {
              const tempMatch = temp.match(/([\d.]+)/);
              if (tempMatch && parseFloat(tempMatch[1]) > 0) {
                vitals.temperature = parseFloat(tempMatch[1]);
              }
            }
          } catch (e) {
          }
          if (!vitals.temperature) {
            try {
              const ioregRaw = await runCmd(
                "ioreg -r -n AppleSmartBattery 2>/dev/null | grep Temperature",
                { fallback: "" }
              );
              const tempMatch = ioregRaw.match(/"Temperature"\s*=\s*(\d+)/);
              if (tempMatch) {
                vitals.temperature = Math.round(parseInt(tempMatch[1], 10) / 100);
              }
            } catch (e) {
            }
          }
        } else if (isLinux) {
          try {
            const temp = await runCmd("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null", {
              fallback: ""
            });
            if (temp) {
              vitals.temperature = Math.round(parseInt(temp, 10) / 1e3);
            }
          } catch (e) {
          }
        }
      } catch (e) {
        console.error("[Vitals] Async refresh failed:", e.message);
      }
      vitals.memory.usedFormatted = formatBytes(vitals.memory.used);
      vitals.memory.totalFormatted = formatBytes(vitals.memory.total);
      vitals.memory.freeFormatted = formatBytes(vitals.memory.free);
      vitals.disk.usedFormatted = formatBytes(vitals.disk.used);
      vitals.disk.totalFormatted = formatBytes(vitals.disk.total);
      vitals.disk.freeFormatted = formatBytes(vitals.disk.free);
      cachedVitals = vitals;
      lastVitalsUpdate = Date.now();
      vitalsRefreshing = false;
      console.log("[Vitals] Cache refreshed async");
    }
    setTimeout(() => refreshVitalsAsync(), 500);
    setInterval(() => refreshVitalsAsync(), VITALS_CACHE_TTL);
    function getSystemVitals2() {
      const now = Date.now();
      if (!cachedVitals || now - lastVitalsUpdate > VITALS_CACHE_TTL) {
        refreshVitalsAsync();
      }
      if (cachedVitals) return cachedVitals;
      return {
        hostname: "loading...",
        uptime: "",
        disk: {
          used: 0,
          free: 0,
          total: 0,
          percent: 0,
          usedFormatted: "-",
          totalFormatted: "-",
          freeFormatted: "-"
        },
        cpu: { loadAvg: [0, 0, 0], cores: 0, usage: 0 },
        memory: {
          used: 0,
          free: 0,
          total: 0,
          percent: 0,
          pressure: "normal",
          usedFormatted: "-",
          totalFormatted: "-",
          freeFormatted: "-"
        },
        temperature: null
      };
    }
    var cachedDeps = null;
    async function checkOptionalDeps2() {
      const isLinux = process.platform === "linux";
      const isMacOS = process.platform === "darwin";
      const platform = isLinux ? "linux" : isMacOS ? "darwin" : null;
      const results = [];
      if (!platform) {
        cachedDeps = results;
        return results;
      }
      const fs2 = require("fs");
      const path2 = require("path");
      const depsFile = path2.join(__dirname, "..", "config", "system-deps.json");
      let depsConfig;
      try {
        depsConfig = JSON.parse(fs2.readFileSync(depsFile, "utf8"));
      } catch {
        cachedDeps = results;
        return results;
      }
      const deps = depsConfig[platform] || [];
      const home = require("os").homedir();
      let pkgManager = null;
      if (isLinux) {
        for (const pm of ["apt", "dnf", "yum", "pacman", "apk"]) {
          const has = await runCmd(`which ${pm}`, { fallback: "" });
          if (has) {
            pkgManager = pm;
            break;
          }
        }
      } else if (isMacOS) {
        const hasBrew = await runCmd("which brew", { fallback: "" });
        if (hasBrew) pkgManager = "brew";
      }
      let isAppleSilicon = false;
      if (isMacOS) {
        const chip = await runCmd("sysctl -n machdep.cpu.brand_string", { fallback: "" });
        isAppleSilicon = /apple/i.test(chip);
      }
      for (const dep of deps) {
        if (dep.condition === "intel" && isAppleSilicon) continue;
        let installed = false;
        const hasBinary = await runCmd(`which ${dep.binary} 2>/dev/null`, { fallback: "" });
        if (hasBinary) {
          installed = true;
        } else if (isMacOS && dep.binary === "osx-cpu-temp") {
          const homebin = await runCmd(`test -x ${home}/bin/osx-cpu-temp && echo ok`, {
            fallback: ""
          });
          if (homebin) installed = true;
        }
        const installCmd = dep.install[pkgManager] || null;
        results.push({
          id: dep.id,
          name: dep.name,
          purpose: dep.purpose,
          affects: dep.affects,
          installed,
          installCmd,
          url: dep.url || null
        });
      }
      cachedDeps = results;
      const missing = results.filter((d) => !d.installed);
      if (missing.length > 0) {
        console.log("[Startup] Optional dependencies for enhanced vitals:");
        for (const dep of missing) {
          const action = dep.installCmd || dep.url || "see docs";
          console.log(`   \u{1F4A1} ${dep.name} \u2014 ${dep.purpose}: ${action}`);
        }
      }
      return results;
    }
    function getOptionalDeps2() {
      return cachedDeps;
    }
    module2.exports = {
      refreshVitalsAsync,
      getSystemVitals: getSystemVitals2,
      checkOptionalDeps: checkOptionalDeps2,
      getOptionalDeps: getOptionalDeps2,
      VITALS_CACHE_TTL
    };
  }
});

// src/auth.js
var require_auth = __commonJS({
  "src/auth.js"(exports2, module2) {
    var AUTH_HEADERS = {
      tailscale: {
        login: "tailscale-user-login",
        name: "tailscale-user-name",
        pic: "tailscale-user-profile-pic"
      },
      cloudflare: {
        email: "cf-access-authenticated-user-email"
      }
    };
    function checkAuth2(req, authConfig) {
      const mode = authConfig.mode;
      const remoteAddr = req.socket?.remoteAddress || "";
      const isLocalhost = remoteAddr === "127.0.0.1" || remoteAddr === "::1" || remoteAddr === "::ffff:127.0.0.1";
      if (isLocalhost) {
        return { authorized: true, user: { type: "localhost", login: "localhost" } };
      }
      if (mode === "none") {
        return { authorized: true, user: null };
      }
      if (mode === "token") {
        const authHeader = req.headers["authorization"] || "";
        const [REDACTED_SECRET], "");
        if (token && token === authConfig.token) {
          return { authorized: true, user: { type: "token" } };
        }
        return { authorized: false, reason: "Invalid or missing token" };
      }
      if (mode === "tailscale") {
        const login = (req.headers[AUTH_HEADERS.tailscale.login] || "").toLowerCase();
        const name = req.headers[AUTH_HEADERS.tailscale.name] || "";
        const pic = req.headers[AUTH_HEADERS.tailscale.pic] || "";
        if (!login) {
          return { authorized: false, reason: "Not accessed via Tailscale Serve" };
        }
        const isAllowed = authConfig.allowedUsers.some((allowed) => {
          if (allowed === "*") return true;
          if (allowed === login) return true;
          if (allowed.startsWith("*@")) {
            const domain = allowed.slice(2);
            return login.endsWith("@" + domain);
          }
          return false;
        });
        if (isAllowed) {
          return { authorized: true, user: { type: "tailscale", login, name, pic } };
        }
        return { authorized: false, reason: `User ${login} not in allowlist`, user: { login } };
      }
      if (mode === "cloudflare") {
        const email = (req.headers[AUTH_HEADERS.cloudflare.email] || "").toLowerCase();
        if (!email) {
          return { authorized: false, reason: "Not accessed via Cloudflare Access" };
        }
        const isAllowed = authConfig.allowedUsers.some((allowed) => {
          if (allowed === "*") return true;
          if (allowed === email) return true;
          if (allowed.startsWith("*@")) {
            const domain = allowed.slice(2);
            return email.endsWith("@" + domain);
          }
          return false;
        });
        if (isAllowed) {
          return { authorized: true, user: { type: "cloudflare", email } };
        }
        return { authorized: false, reason: `User ${email} not in allowlist`, user: { email } };
      }
      if (mode === "allowlist") {
        const clientIP = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "";
        const isAllowed = authConfig.allowedIPs.some((allowed) => {
          if (allowed === clientIP) return true;
          if (allowed.endsWith("/24")) {
            const prefix = allowed.slice(0, -3).split(".").slice(0, 3).join(".");
            return clientIP.startsWith(prefix + ".");
          }
          return false;
        });
        if (isAllowed) {
          return { authorized: true, user: { type: "ip", ip: clientIP } };
        }
        return { authorized: false, reason: `IP ${clientIP} not in allowlist` };
      }
      return { authorized: false, reason: "Unknown auth mode" };
    }
    function getUnauthorizedPage2(reason, user, authConfig) {
      const userInfo = user ? `<p class="user-info">Detected: ${user.login || user.email || user.ip || "unknown"}</p>` : "";
      return `<!DOCTYPE html>
<html>
<head>
    <title>Access Denied - Command Center</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #e8e8e8;
        }
        .container {
            text-align: center;
            padding: 3rem;
            background: rgba(255,255,255,0.05);
            border-radius: 16px;
            border: 1px solid rgba(255,255,255,0.1);
            max-width: 500px;
        }
        .icon { font-size: 4rem; margin-bottom: 1rem; }
        h1 { font-size: 1.8rem; margin-bottom: 1rem; color: #ff6b6b; }
        .reason { color: #aaa; margin-bottom: 1.5rem; font-size: 0.95rem; }
        .user-info { color: #ffeb3b; margin: 1rem 0; font-size: 0.9rem; }
        .instructions { color: #ccc; font-size: 0.85rem; line-height: 1.5; }
        .auth-mode { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.1); color: #888; font-size: 0.75rem; }
        code { background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="icon">\u{1F510}</div>
        <h1>Access Denied</h1>
        <div class="reason">${reason}</div>
        ${userInfo}
        <div class="instructions">
            <p>This dashboard requires authentication via <strong>${authConfig.mode}</strong>.</p>
            ${authConfig.mode === "tailscale" ? `<p style="margin-top:1rem">Make sure you're accessing via your Tailscale URL and your account is in the allowlist.</p>` : ""}
            ${authConfig.mode === "cloudflare" ? `<p style="margin-top:1rem">Make sure you're accessing via Cloudflare Access and your email is in the allowlist.</p>` : ""}
        </div>
        <div class="auth-mode">Auth mode: <code>${authConfig.mode}</code></div>
    </div>
</body>
</html>`;
    }
    module2.exports = { AUTH_HEADERS, checkAuth: checkAuth2, getUnauthorizedPage: getUnauthorizedPage2 };
  }
});

// src/privacy.js
var require_privacy = __commonJS({
  "src/privacy.js"(exports2, module2) {
    var fs2 = require("fs");
    var path2 = require("path");
    function getPrivacyFilePath(dataDir) {
      return path2.join(dataDir, "privacy-settings.json");
    }
    function loadPrivacySettings2(dataDir) {
      try {
        const privacyFile = getPrivacyFilePath(dataDir);
        if (fs2.existsSync(privacyFile)) {
          return JSON.parse(fs2.readFileSync(privacyFile, "utf8"));
        }
      } catch (e) {
        console.error("Failed to load privacy settings:", e.message);
      }
      return {
        version: 1,
        hiddenTopics: [],
        hiddenSessions: [],
        hiddenCrons: [],
        hideHostname: false,
        updatedAt: null
      };
    }
    function savePrivacySettings2(dataDir, data) {
      try {
        if (!fs2.existsSync(dataDir)) {
          fs2.mkdirSync(dataDir, { recursive: true });
        }
        data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
        fs2.writeFileSync(getPrivacyFilePath(dataDir), JSON.stringify(data, null, 2));
        return true;
      } catch (e) {
        console.error("Failed to save privacy settings:", e.message);
        return false;
      }
    }
    module2.exports = {
      loadPrivacySettings: loadPrivacySettings2,
      savePrivacySettings: savePrivacySettings2
    };
  }
});

// src/operators.js
var require_operators = __commonJS({
  "src/operators.js"(exports2, module2) {
    var fs2 = require("fs");
    var path2 = require("path");
    function loadOperators2(dataDir) {
      const operatorsFile = path2.join(dataDir, "operators.json");
      try {
        if (fs2.existsSync(operatorsFile)) {
          return JSON.parse(fs2.readFileSync(operatorsFile, "utf8"));
        }
      } catch (e) {
        console.error("Failed to load operators:", e.message);
      }
      return { version: 1, operators: [], roles: {} };
    }
    function saveOperators2(dataDir, data) {
      try {
        if (!fs2.existsSync(dataDir)) {
          fs2.mkdirSync(dataDir, { recursive: true });
        }
        const operatorsFile = path2.join(dataDir, "operators.json");
        fs2.writeFileSync(operatorsFile, JSON.stringify(data, null, 2));
        return true;
      } catch (e) {
        console.error("Failed to save operators:", e.message);
        return false;
      }
    }
    function getOperatorBySlackId2(dataDir, slackId) {
      const data = loadOperators2(dataDir);
      return data.operators.find((op) => op.id === slackId || op.metadata?.slackId === slackId);
    }
    var operatorsRefreshing = false;
    async function refreshOperatorsAsync(dataDir, getOpenClawDir2) {
      if (operatorsRefreshing) return;
      operatorsRefreshing = true;
      const toMs = (ts, fallback) => {
        if (typeof ts === "number" && Number.isFinite(ts)) return ts;
        if (typeof ts === "string") {
          const parsed = Date.parse(ts);
          if (Number.isFinite(parsed)) return parsed;
        }
        return fallback;
      };
      try {
        const openclawDir = getOpenClawDir2();
        const sessionsDir = path2.join(openclawDir, "agents", "main", "sessions");
        if (!fs2.existsSync(sessionsDir)) {
          operatorsRefreshing = false;
          return;
        }
        const files = fs2.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
        const operatorsMap = /* @__PURE__ */ new Map();
        const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
        for (const file of files) {
          const filePath = path2.join(sessionsDir, file);
          try {
            const stat = fs2.statSync(filePath);
            if (stat.mtimeMs < sevenDaysAgo) continue;
            const fd = fs2.openSync(filePath, "r");
            const buffer = Buffer.alloc(10240);
            const bytesRead = fs2.readSync(fd, buffer, 0, 10240, 0);
            fs2.closeSync(fd);
            const content = buffer.toString("utf8", 0, bytesRead);
            const lines = content.split("\n").slice(0, 20);
            for (const line of lines) {
              if (!line.trim()) continue;
              try {
                const entry = JSON.parse(line);
                if (entry.type !== "message" || !entry.message) continue;
                const msg = entry.message;
                if (msg.role !== "user") continue;
                let text = "";
                if (typeof msg.content === "string") {
                  text = msg.content;
                } else if (Array.isArray(msg.content)) {
                  const textPart = msg.content.find((c) => c.type === "text");
                  if (textPart) text = textPart.text || "";
                }
                if (!text) continue;
                const slackMatch = text.match(/\[Slack[^\]]*\]\s*([\w.-]+)\s*\(([A-Z0-9]+)\):/);
                if (slackMatch) {
                  const username = slackMatch[1];
                  const userId = slackMatch[2];
                  if (!operatorsMap.has(userId)) {
                    operatorsMap.set(userId, {
                      id: userId,
                      name: username,
                      username,
                      source: "slack",
                      firstSeen: toMs(entry.timestamp, stat.mtimeMs),
                      lastSeen: toMs(entry.timestamp, stat.mtimeMs),
                      sessionCount: 1
                    });
                  } else {
                    const op = operatorsMap.get(userId);
                    op.lastSeen = Math.max(op.lastSeen, toMs(entry.timestamp, stat.mtimeMs));
                    op.sessionCount++;
                  }
                  break;
                }
                const telegramMatch = text.match(/\[Telegram[^\]]*\]\s*([\w.-]+):/);
                if (telegramMatch) {
                  const username = telegramMatch[1];
                  const operatorId = `telegram:${username}`;
                  if (!operatorsMap.has(operatorId)) {
                    operatorsMap.set(operatorId, {
                      id: operatorId,
                      name: username,
                      username,
                      source: "telegram",
                      firstSeen: toMs(entry.timestamp, stat.mtimeMs),
                      lastSeen: toMs(entry.timestamp, stat.mtimeMs),
                      sessionCount: 1
                    });
                  } else {
                    const op = operatorsMap.get(operatorId);
                    op.lastSeen = Math.max(op.lastSeen, toMs(entry.timestamp, stat.mtimeMs));
                    op.sessionCount++;
                  }
                  break;
                }
                const discordSenderMatch = text.match(/"sender":\s*"(\d+)"/);
                const discordLabelMatch = text.match(/"label":\s*"([^"]+)"/);
                const discordUsernameMatch = text.match(/"username":\s*"([^"]+)"/);
                if (discordSenderMatch) {
                  const userId = discordSenderMatch[1];
                  const label = discordLabelMatch ? discordLabelMatch[1] : userId;
                  const username = discordUsernameMatch ? discordUsernameMatch[1] : label;
                  const opId = `discord:${userId}`;
                  if (!operatorsMap.has(opId)) {
                    operatorsMap.set(opId, {
                      id: opId,
                      discordId: userId,
                      name: label,
                      username,
                      source: "discord",
                      firstSeen: toMs(entry.timestamp, stat.mtimeMs),
                      lastSeen: toMs(entry.timestamp, stat.mtimeMs),
                      sessionCount: 1
                    });
                  } else {
                    const op = operatorsMap.get(opId);
                    op.lastSeen = Math.max(op.lastSeen, toMs(entry.timestamp, stat.mtimeMs));
                    op.sessionCount++;
                  }
                  break;
                }
              } catch (e) {
              }
            }
          } catch (e) {
          }
        }
        const existing = loadOperators2(dataDir);
        const existingMap = new Map(existing.operators.map((op) => [op.id, op]));
        for (const [id, autoOp] of operatorsMap) {
          if (existingMap.has(id)) {
            const manual = existingMap.get(id);
            manual.lastSeen = Math.max(manual.lastSeen || 0, autoOp.lastSeen);
            manual.sessionCount = (manual.sessionCount || 0) + autoOp.sessionCount;
          } else {
            existingMap.set(id, autoOp);
          }
        }
        const merged = {
          version: 1,
          operators: Array.from(existingMap.values()).sort(
            (a, b) => (b.lastSeen || 0) - (a.lastSeen || 0)
          ),
          roles: existing.roles || {},
          lastRefreshed: Date.now()
        };
        saveOperators2(dataDir, merged);
        console.log(`[Operators] Refreshed: ${merged.operators.length} operators detected`);
      } catch (e) {
        console.error("[Operators] Refresh failed:", e.message);
      }
      operatorsRefreshing = false;
    }
    function startOperatorsRefresh2(dataDir, getOpenClawDir2) {
      setTimeout(() => refreshOperatorsAsync(dataDir, getOpenClawDir2), 2e3);
      setInterval(() => refreshOperatorsAsync(dataDir, getOpenClawDir2), 5 * 60 * 1e3);
    }
    module2.exports = {
      loadOperators: loadOperators2,
      saveOperators: saveOperators2,
      getOperatorBySlackId: getOperatorBySlackId2,
      refreshOperatorsAsync,
      startOperatorsRefresh: startOperatorsRefresh2
    };
  }
});

// src/topics.js
var require_topics = __commonJS({
  "src/topics.js"(exports2, module2) {
    var TOPIC_PATTERNS = {
      dashboard: ["dashboard", "command center", "ui", "interface", "status page"],
      scheduling: ["cron", "schedule", "timer", "reminder", "alarm", "periodic", "interval"],
      heartbeat: [
        "heartbeat",
        "heartbeat_ok",
        "poll",
        "health check",
        "ping",
        "keepalive",
        "monitoring"
      ],
      memory: ["memory", "remember", "recall", "notes", "journal", "log", "context"],
      Slack: ["slack", "channel", "#cc-", "thread", "mention", "dm", "workspace"],
      email: ["email", "mail", "inbox", "gmail", "send email", "unread", "compose"],
      calendar: ["calendar", "event", "meeting", "appointment", "schedule", "gcal"],
      coding: [
        "code",
        "script",
        "function",
        "debug",
        "error",
        "bug",
        "implement",
        "refactor",
        "programming"
      ],
      git: [
        "git",
        "commit",
        "branch",
        "merge",
        "push",
        "pull",
        "repository",
        "pr",
        "pull request",
        "github"
      ],
      "file editing": ["file", "edit", "write", "read", "create", "delete", "modify", "save"],
      API: ["api", "endpoint", "request", "response", "webhook", "integration", "rest", "graphql"],
      research: ["search", "research", "lookup", "find", "investigate", "learn", "study"],
      browser: ["browser", "webpage", "website", "url", "click", "navigate", "screenshot", "web_fetch"],
      "Quip export": ["quip", "export", "document", "spreadsheet"],
      finance: ["finance", "investment", "stock", "money", "budget", "bank", "trading", "portfolio"],
      home: ["home", "automation", "lights", "thermostat", "smart home", "iot", "homekit"],
      health: ["health", "fitness", "workout", "exercise", "weight", "sleep", "nutrition"],
      travel: ["travel", "flight", "hotel", "trip", "vacation", "booking", "airport"],
      food: ["food", "recipe", "restaurant", "cooking", "meal", "order", "delivery"],
      subagent: ["subagent", "spawn", "sub-agent", "delegate", "worker", "parallel"],
      tools: ["tool", "exec", "shell", "command", "terminal", "bash", "run"]
    };
    function detectTopics(text) {
      if (!text) return [];
      const lowerText = text.toLowerCase();
      const scores = {};
      for (const [topic, keywords] of Object.entries(TOPIC_PATTERNS)) {
        let score = 0;
        for (const keyword of keywords) {
          if (keyword.length <= 3) {
            const regex = new RegExp(`\\b${keyword}\\b`, "i");
            if (regex.test(lowerText)) score++;
          } else if (lowerText.includes(keyword)) {
            score++;
          }
        }
        if (score > 0) {
          scores[topic] = score;
        }
      }
      if (Object.keys(scores).length === 0) return [];
      const bestScore = Math.max(...Object.values(scores));
      const threshold = Math.max(2, bestScore * 0.5);
      return Object.entries(scores).filter(([_, score]) => score >= threshold || score >= 1 && bestScore <= 2).sort((a, b) => b[1] - a[1]).map(([topic, _]) => topic);
    }
    module2.exports = { TOPIC_PATTERNS, detectTopics };
  }
});

// src/sessions.js
var require_sessions = __commonJS({
  "src/sessions.js"(exports2, module2) {
    var fs2 = require("fs");
    var path2 = require("path");
    var { detectTopics } = require_topics();
    var CHANNEL_MAP = {
      c0aax7y80np: "#cc-meta",
      c0ab9f8sdfe: "#cc-research",
      c0aan4rq7v5: "#cc-finance",
      c0abxulk1qq: "#cc-properties",
      c0ab5nz8mkl: "#cc-ai",
      c0aan38tzv5: "#cc-dev",
      c0ab7wwhqvc: "#cc-home",
      c0ab1pjhxef: "#cc-health",
      c0ab7txvcqd: "#cc-legal",
      c0aay2g3n3r: "#cc-social",
      c0aaxrw2wqp: "#cc-business",
      c0ab19f3lae: "#cc-random",
      c0ab0r74y33: "#cc-food",
      c0ab0qrq3r9: "#cc-travel",
      c0ab0sbqqlg: "#cc-family",
      c0ab0slqdba: "#cc-games",
      c0ab1ps7ef2: "#cc-music",
      c0absbnrsbe: "#cc-dashboard"
    };
    function parseSessionLabel(key) {
      const parts = key.split(":");
      if (parts.includes("slack")) {
        const channelIdx = parts.indexOf("channel");
        if (channelIdx >= 0 && parts[channelIdx + 1]) {
          const channelId = parts[channelIdx + 1].toLowerCase();
          const channelName = CHANNEL_MAP[channelId] || `#${channelId}`;
          if (parts.includes("thread")) {
            const threadTs = parts[parts.indexOf("thread") + 1];
            const ts = parseFloat(threadTs);
            const date = new Date(ts * 1e3);
            const timeStr = date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
            return `${channelName} thread @ ${timeStr}`;
          }
          return channelName;
        }
      }
      if (key.includes("telegram")) {
        return "\u{1F4F1} Telegram";
      }
      if (key === "agent:main:main") {
        return "\u{1F3E0} Main Session";
      }
      return key.length > 40 ? key.slice(0, 37) + "..." : key;
    }
    function createSessionsModule2(deps) {
      const { getOpenClawDir: getOpenClawDir2, getOperatorBySlackId: getOperatorBySlackId2, runOpenClaw: runOpenClaw2, runOpenClawAsync: runOpenClawAsync2, extractJSON: extractJSON2 } = deps;
      let sessionsCache = { sessions: [], timestamp: 0, refreshing: false };
      const SESSIONS_CACHE_TTL = 1e4;
      function getSessionOriginator(sessionId) {
        try {
          if (!sessionId) return null;
          const openclawDir = getOpenClawDir2();
          const transcriptPath = path2.join(
            openclawDir,
            "agents",
            "main",
            "sessions",
            `${sessionId}.jsonl`
          );
          if (!fs2.existsSync(transcriptPath)) return null;
          const content = fs2.readFileSync(transcriptPath, "utf8");
          const lines = content.trim().split("\n");
          for (let i = 0; i < Math.min(lines.length, 10); i++) {
            try {
              const entry = JSON.parse(lines[i]);
              if (entry.type !== "message" || !entry.message) continue;
              const msg = entry.message;
              if (msg.role !== "user") continue;
              let text = "";
              if (typeof msg.content === "string") {
                text = msg.content;
              } else if (Array.isArray(msg.content)) {
                const textPart = msg.content.find((c) => c.type === "text");
                if (textPart) text = textPart.text || "";
              }
              if (!text) continue;
              const slackUserMatch = text.match(/\]\s*([\w.-]+)\s*\(([A-Z0-9]+)\):/);
              if (slackUserMatch) {
                const username = slackUserMatch[1];
                const userId = slackUserMatch[2];
                const operator = getOperatorBySlackId2(userId);
                return {
                  userId,
                  username,
                  displayName: operator?.name || username,
                  role: operator?.role || "user",
                  avatar: operator?.avatar || null
                };
              }
            } catch (e) {
            }
          }
          return null;
        } catch (e) {
          return null;
        }
      }
      function getSessionTopic(sessionId) {
        if (!sessionId) return null;
        try {
          const openclawDir = getOpenClawDir2();
          const transcriptPath = path2.join(
            openclawDir,
            "agents",
            "main",
            "sessions",
            `${sessionId}.jsonl`
          );
          if (!fs2.existsSync(transcriptPath)) return null;
          const fd = fs2.openSync(transcriptPath, "r");
          const buffer = Buffer.alloc(5e4);
          const bytesRead = fs2.readSync(fd, buffer, 0, 5e4, 0);
          fs2.closeSync(fd);
          if (bytesRead === 0) return null;
          const content = buffer.toString("utf8", 0, bytesRead);
          const lines = content.split("\n").filter((l) => l.trim());
          let textSamples = [];
          for (const line of lines.slice(0, 30)) {
            try {
              const entry = JSON.parse(line);
              if (entry.type === "message" && entry.message?.content) {
                const msgContent = entry.message.content;
                if (Array.isArray(msgContent)) {
                  msgContent.forEach((c) => {
                    if (c.type === "text" && c.text) {
                      textSamples.push(c.text.slice(0, 500));
                    }
                  });
                } else if (typeof msgContent === "string") {
                  textSamples.push(msgContent.slice(0, 500));
                }
              }
            } catch (e) {
            }
          }
          if (textSamples.length === 0) return null;
          const topics = detectTopics(textSamples.join(" "));
          return topics.length > 0 ? topics.slice(0, 2).join(", ") : null;
        } catch (e) {
          return null;
        }
      }
      function mapSession(s) {
        const minutesAgo = s.ageMs ? s.ageMs / 6e4 : Infinity;
        let channel = "other";
        if (s.key.includes("slack")) channel = "slack";
        else if (s.key.includes("telegram")) channel = "telegram";
        else if (s.key.includes("discord")) channel = "discord";
        else if (s.key.includes("signal")) channel = "signal";
        else if (s.key.includes("whatsapp")) channel = "whatsapp";
        let sessionType = "channel";
        if (s.key.includes(":subagent:")) sessionType = "subagent";
        else if (s.key.includes(":cron:")) sessionType = "cron";
        else if (s.key === "agent:main:main") sessionType = "main";
        const originator = getSessionOriginator(s.sessionId);
        const label = s.groupChannel || s.displayName || parseSessionLabel(s.key);
        const topic = getSessionTopic(s.sessionId);
        const totalTokens = s.totalTokens || 0;
        const sessionAgeMinutes = Math.max(1, Math.min(minutesAgo, 24 * 60));
        const burnRate = Math.round(totalTokens / sessionAgeMinutes);
        return {
          sessionKey: s.key,
          sessionId: s.sessionId,
          label,
          groupChannel: s.groupChannel || null,
          displayName: s.displayName || null,
          kind: s.kind,
          channel,
          sessionType,
          active: minutesAgo < 15,
          recentlyActive: minutesAgo < 60,
          minutesAgo: Math.round(minutesAgo),
          tokens: s.totalTokens || 0,
          model: s.model,
          originator,
          topic,
          metrics: {
            burnRate,
            toolCalls: 0,
            minutesActive: Math.max(1, Math.min(Math.round(minutesAgo), 24 * 60))
          }
        };
      }
      async function refreshSessionsCache() {
        if (sessionsCache.refreshing) return;
        sessionsCache.refreshing = true;
        try {
          const output = await runOpenClawAsync2("sessions --json 2>/dev/null");
          const jsonStr = extractJSON2(output);
          if (jsonStr) {
            const data = JSON.parse(jsonStr);
            const sessions2 = data.sessions || [];
            const mapped = sessions2.map((s) => mapSession(s));
            sessionsCache = {
              sessions: mapped,
              timestamp: Date.now(),
              refreshing: false
            };
            console.log(`[Sessions Cache] Refreshed: ${mapped.length} sessions`);
          }
        } catch (e) {
          console.error("[Sessions Cache] Refresh error:", e.message);
        }
        sessionsCache.refreshing = false;
      }
      function getSessionsCached() {
        const now = Date.now();
        const isStale = now - sessionsCache.timestamp > SESSIONS_CACHE_TTL;
        if (isStale && !sessionsCache.refreshing) {
          refreshSessionsCache();
        }
        return sessionsCache.sessions;
      }
      function getSessions(options = {}) {
        const limit = Object.prototype.hasOwnProperty.call(options, "limit") ? options.limit : 20;
        const returnCount = options.returnCount || false;
        if (limit === null) {
          const cached = getSessionsCached();
          const totalCount = cached.length;
          return returnCount ? { sessions: cached, totalCount } : cached;
        }
        try {
          const output = runOpenClaw2("sessions --json 2>/dev/null");
          const jsonStr = extractJSON2(output);
          if (jsonStr) {
            const data = JSON.parse(jsonStr);
            const totalCount = data.count || data.sessions?.length || 0;
            let sessions2 = data.sessions || [];
            if (limit != null) {
              sessions2 = sessions2.slice(0, limit);
            }
            const mapped = sessions2.map((s) => mapSession(s));
            return returnCount ? { sessions: mapped, totalCount } : mapped;
          }
        } catch (e) {
          console.error("Failed to get sessions:", e.message);
        }
        return returnCount ? { sessions: [], totalCount: 0 } : [];
      }
      function readTranscript(sessionId) {
        const openclawDir = getOpenClawDir2();
        const transcriptPath = path2.join(
          openclawDir,
          "agents",
          "main",
          "sessions",
          `${sessionId}.jsonl`
        );
        try {
          if (!fs2.existsSync(transcriptPath)) return [];
          const content = fs2.readFileSync(transcriptPath, "utf8");
          return content.trim().split("\n").map((line) => {
            try {
              return JSON.parse(line);
            } catch {
              return null;
            }
          }).filter(Boolean);
        } catch (e) {
          console.error("Failed to read transcript:", e.message);
          return [];
        }
      }
      function getSessionDetail(sessionKey) {
        try {
          const listOutput = runOpenClaw2("sessions --json 2>/dev/null");
          let sessionInfo = null;
          const jsonStr = extractJSON2(listOutput);
          if (jsonStr) {
            const data = JSON.parse(jsonStr);
            sessionInfo = data.sessions?.find((s) => s.key === sessionKey);
          }
          if (!sessionInfo) {
            return { error: "Session not found" };
          }
          const transcript = readTranscript(sessionInfo.sessionId);
          let messages = [];
          let tools = {};
          let facts = [];
          let needsAttention = [];
          let totalInputTokens = 0;
          let totalOutputTokens = 0;
          let totalCacheRead = 0;
          let totalCacheWrite = 0;
          let totalCost = 0;
          let detectedModel = sessionInfo.model || null;
          transcript.forEach((entry) => {
            if (entry.type !== "message" || !entry.message) return;
            const msg = entry.message;
            if (!msg.role) return;
            if (msg.usage) {
              totalInputTokens += msg.usage.input || msg.usage.inputTokens || 0;
              totalOutputTokens += msg.usage.output || msg.usage.outputTokens || 0;
              totalCacheRead += msg.usage.cacheRead || msg.usage.cacheReadTokens || 0;
              totalCacheWrite += msg.usage.cacheWrite || msg.usage.cacheWriteTokens || 0;
              if (msg.usage.cost?.total) totalCost += msg.usage.cost.total;
            }
            if (msg.role === "assistant" && msg.model && !detectedModel) {
              detectedModel = msg.model;
            }
            let text = "";
            if (typeof msg.content === "string") {
              text = msg.content;
            } else if (Array.isArray(msg.content)) {
              const textPart = msg.content.find((c) => c.type === "text");
              if (textPart) text = textPart.text || "";
              msg.content.filter((c) => c.type === "toolCall" || c.type === "tool_use").forEach((tc) => {
                const name = tc.name || tc.tool || "unknown";
                tools[name] = (tools[name] || 0) + 1;
              });
            }
            if (text && msg.role !== "toolResult") {
              messages.push({ role: msg.role, text, timestamp: entry.timestamp });
            }
            if (msg.role === "user" && text) {
              const lowerText = text.toLowerCase();
              if (text.includes("?")) {
                const questions = text.match(/[^.!?\n]*\?/g) || [];
                questions.slice(0, 2).forEach((q) => {
                  if (q.length > 15 && q.length < 200) {
                    needsAttention.push(`\u2753 ${q.trim()}`);
                  }
                });
              }
              if (lowerText.includes("todo") || lowerText.includes("remind") || lowerText.includes("need to")) {
                const match = text.match(/(?:todo|remind|need to)[^.!?\n]*/i);
                if (match) needsAttention.push(`\u{1F4CB} ${match[0].slice(0, 100)}`);
              }
            }
            if (msg.role === "assistant" && text) {
              const lowerText = text.toLowerCase();
              ["\u2705", "done", "created", "updated", "fixed", "deployed"].forEach((keyword) => {
                if (lowerText.includes(keyword)) {
                  const lines = text.split("\n").filter((l) => l.toLowerCase().includes(keyword));
                  lines.slice(0, 2).forEach((line) => {
                    if (line.length > 5 && line.length < 150) {
                      facts.push(line.trim().slice(0, 100));
                    }
                  });
                }
              });
            }
          });
          let summary = "No activity yet.";
          const userMessages = messages.filter((m) => m.role === "user");
          const assistantMessages = messages.filter((m) => m.role === "assistant");
          let topics = [];
          if (messages.length > 0) {
            summary = `${messages.length} messages (${userMessages.length} user, ${assistantMessages.length} assistant). `;
            const allText = messages.map((m) => m.text).join(" ");
            topics = detectTopics(allText);
            if (topics.length > 0) {
              summary += `Topics: ${topics.join(", ")}.`;
            }
          }
          const toolsArray = Object.entries(tools).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
          const ageMs = sessionInfo.ageMs || 0;
          const lastActive = ageMs < 6e4 ? "Just now" : ageMs < 36e5 ? `${Math.round(ageMs / 6e4)} minutes ago` : ageMs < 864e5 ? `${Math.round(ageMs / 36e5)} hours ago` : `${Math.round(ageMs / 864e5)} days ago`;
          let channelDisplay = "Other";
          if (sessionInfo.groupChannel) {
            channelDisplay = sessionInfo.groupChannel;
          } else if (sessionInfo.displayName) {
            channelDisplay = sessionInfo.displayName;
          } else if (sessionKey.includes("slack")) {
            const parts = sessionKey.split(":");
            const channelIdx = parts.indexOf("channel");
            if (channelIdx >= 0 && parts[channelIdx + 1]) {
              const channelId = parts[channelIdx + 1].toLowerCase();
              channelDisplay = CHANNEL_MAP[channelId] || `#${channelId}`;
            } else {
              channelDisplay = "Slack";
            }
          } else if (sessionKey.includes("telegram")) {
            channelDisplay = "Telegram";
          }
          const finalTotalTokens = totalInputTokens + totalOutputTokens || sessionInfo.totalTokens || 0;
          const finalInputTokens = totalInputTokens || sessionInfo.inputTokens || 0;
          const finalOutputTokens = totalOutputTokens || sessionInfo.outputTokens || 0;
          const modelDisplay = (detectedModel || sessionInfo.model || "-").replace("anthropic/", "").replace("openai/", "");
          return {
            key: sessionKey,
            kind: sessionInfo.kind,
            channel: channelDisplay,
            groupChannel: sessionInfo.groupChannel || channelDisplay,
            model: modelDisplay,
            tokens: finalTotalTokens,
            inputTokens: finalInputTokens,
            outputTokens: finalOutputTokens,
            cacheRead: totalCacheRead,
            cacheWrite: totalCacheWrite,
            estCost: totalCost > 0 ? `$${totalCost.toFixed(4)}` : null,
            lastActive,
            summary,
            topics,
            // Array of detected topics
            facts: [...new Set(facts)].slice(0, 8),
            needsAttention: [...new Set(needsAttention)].slice(0, 5),
            tools: toolsArray.slice(0, 10),
            messages: messages.slice(-15).reverse().map((m) => ({
              role: m.role,
              text: m.text.slice(0, 500)
            }))
          };
        } catch (e) {
          console.error("Failed to get session detail:", e.message);
          return { error: e.message };
        }
      }
      return {
        getSessionOriginator,
        getSessionTopic,
        mapSession,
        refreshSessionsCache,
        getSessionsCached,
        getSessions,
        readTranscript,
        getSessionDetail,
        parseSessionLabel
      };
    }
    module2.exports = { createSessionsModule: createSessionsModule2, CHANNEL_MAP };
  }
});

// src/cron.js
var require_cron = __commonJS({
  "src/cron.js"(exports2, module2) {
    var fs2 = require("fs");
    var path2 = require("path");
    function cronToHuman(expr) {
      if (!expr || expr === "\u2014") return null;
      const parts = expr.split(" ");
      if (parts.length < 5) return null;
      const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
      const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
      function formatTime(h, m) {
        const hNum = parseInt(h, 10);
        const mNum = parseInt(m, 10);
        if (isNaN(hNum)) return null;
        const ampm = hNum >= 12 ? "pm" : "am";
        const h12 = hNum === 0 ? 12 : hNum > 12 ? hNum - 12 : hNum;
        return mNum === 0 ? `${h12}${ampm}` : `${h12}:${mNum.toString().padStart(2, "0")}${ampm}`;
      }
      if (minute === "*" && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
        return "Every minute";
      }
      if (minute.startsWith("*/")) {
        const interval = minute.slice(2);
        return `Every ${interval} minutes`;
      }
      if (hour.startsWith("*/")) {
        const interval = hour.slice(2);
        const minStr = minute === "0" ? "" : `:${minute.padStart(2, "0")}`;
        return `Every ${interval} hours${minStr ? " at " + minStr : ""}`;
      }
      if (minute !== "*" && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
        return `Hourly at :${minute.padStart(2, "0")}`;
      }
      let timeStr = "";
      if (minute !== "*" && hour !== "*" && !hour.startsWith("*/")) {
        timeStr = formatTime(hour, minute);
      }
      if (timeStr && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
        return `Daily at ${timeStr}`;
      }
      if ((dayOfWeek === "1-5" || dayOfWeek === "MON-FRI") && dayOfMonth === "*" && month === "*") {
        return timeStr ? `Weekdays at ${timeStr}` : "Weekdays";
      }
      if ((dayOfWeek === "0,6" || dayOfWeek === "6,0") && dayOfMonth === "*" && month === "*") {
        return timeStr ? `Weekends at ${timeStr}` : "Weekends";
      }
      if (dayOfMonth === "*" && month === "*" && dayOfWeek !== "*") {
        const days = dayOfWeek.split(",").map((d) => {
          const num = parseInt(d, 10);
          return dayNames[num] || d;
        });
        const dayStr = days.length === 1 ? days[0] : days.join(", ");
        return timeStr ? `${dayStr} at ${timeStr}` : `Every ${dayStr}`;
      }
      if (dayOfMonth !== "*" && month === "*" && dayOfWeek === "*") {
        const day = parseInt(dayOfMonth, 10);
        const suffix = day === 1 || day === 21 || day === 31 ? "st" : day === 2 || day === 22 ? "nd" : day === 3 || day === 23 ? "rd" : "th";
        return timeStr ? `${day}${suffix} of month at ${timeStr}` : `${day}${suffix} of every month`;
      }
      if (timeStr) {
        return `At ${timeStr}`;
      }
      return expr;
    }
    function getCronJobs2(getOpenClawDir2) {
      try {
        const cronPath = path2.join(getOpenClawDir2(), "cron", "jobs.json");
        if (fs2.existsSync(cronPath)) {
          const data = JSON.parse(fs2.readFileSync(cronPath, "utf8"));
          return (data.jobs || []).map((j) => {
            let scheduleStr = "\u2014";
            let scheduleHuman = null;
            if (j.schedule) {
              if (j.schedule.kind === "cron" && j.schedule.expr) {
                scheduleStr = j.schedule.expr;
                scheduleHuman = cronToHuman(j.schedule.expr);
              } else if (j.schedule.kind === "once") {
                scheduleStr = "once";
                scheduleHuman = "One-time";
              }
            }
            let nextRunStr = "\u2014";
            if (j.state?.nextRunAtMs) {
              const next = new Date(j.state.nextRunAtMs);
              const now = /* @__PURE__ */ new Date();
              const diffMs = next - now;
              const diffMins = Math.round(diffMs / 6e4);
              if (diffMins < 0) {
                nextRunStr = "overdue";
              } else if (diffMins < 60) {
                nextRunStr = `${diffMins}m`;
              } else if (diffMins < 1440) {
                nextRunStr = `${Math.round(diffMins / 60)}h`;
              } else {
                nextRunStr = `${Math.round(diffMins / 1440)}d`;
              }
            }
            return {
              id: j.id,
              name: j.name || j.id.slice(0, 8),
              schedule: scheduleStr,
              scheduleHuman,
              nextRun: nextRunStr,
              enabled: j.enabled !== false,
              lastStatus: j.state?.lastStatus
            };
          });
        }
      } catch (e) {
        console.error("Failed to get cron:", e.message);
      }
      return [];
    }
    module2.exports = {
      cronToHuman,
      getCronJobs: getCronJobs2
    };
  }
});

// src/cerebro.js
var require_cerebro = __commonJS({
  "src/cerebro.js"(exports2, module2) {
    var fs2 = require("fs");
    var path2 = require("path");
    var { formatTimeAgo } = require_utils();
    function getCerebroTopics2(cerebroDir, options = {}) {
      const { offset = 0, limit = 20, status: filterStatus = "all" } = options;
      const topicsDir = path2.join(cerebroDir, "topics");
      const orphansDir = path2.join(cerebroDir, "orphans");
      const topics = [];
      const result = {
        initialized: false,
        cerebroPath: cerebroDir,
        topics: { active: 0, resolved: 0, parked: 0, total: 0 },
        threads: 0,
        orphans: 0,
        recentTopics: [],
        lastUpdated: null
      };
      try {
        if (!fs2.existsSync(cerebroDir)) {
          return result;
        }
        result.initialized = true;
        let latestModified = null;
        if (!fs2.existsSync(topicsDir)) {
          return result;
        }
        const topicNames = fs2.readdirSync(topicsDir).filter((name) => {
          const topicPath = path2.join(topicsDir, name);
          return fs2.statSync(topicPath).isDirectory() && !name.startsWith("_");
        });
        topicNames.forEach((name) => {
          const topicMdPath = path2.join(topicsDir, name, "topic.md");
          const topicDirPath = path2.join(topicsDir, name);
          let stat;
          let content = "";
          if (fs2.existsSync(topicMdPath)) {
            stat = fs2.statSync(topicMdPath);
            content = fs2.readFileSync(topicMdPath, "utf8");
          } else {
            stat = fs2.statSync(topicDirPath);
          }
          try {
            const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
            let title = name;
            let topicStatus = "active";
            let category = "general";
            let created = null;
            if (frontmatterMatch) {
              const frontmatter = frontmatterMatch[1];
              const titleMatch = frontmatter.match(/title:\s*(.+)/);
              const statusMatch = frontmatter.match(/status:\s*(.+)/);
              const categoryMatch = frontmatter.match(/category:\s*(.+)/);
              const createdMatch = frontmatter.match(/created:\s*(.+)/);
              if (titleMatch) title = titleMatch[1].trim();
              if (statusMatch) topicStatus = statusMatch[1].trim().toLowerCase();
              if (categoryMatch) category = categoryMatch[1].trim();
              if (createdMatch) created = createdMatch[1].trim();
            }
            const threadsDir = path2.join(topicsDir, name, "threads");
            let threadCount = 0;
            if (fs2.existsSync(threadsDir)) {
              threadCount = fs2.readdirSync(threadsDir).filter((f) => f.endsWith(".md") || f.endsWith(".json")).length;
            }
            result.threads += threadCount;
            if (topicStatus === "active") result.topics.active++;
            else if (topicStatus === "resolved") result.topics.resolved++;
            else if (topicStatus === "parked") result.topics.parked++;
            if (!latestModified || stat.mtime > latestModified) {
              latestModified = stat.mtime;
            }
            topics.push({
              name,
              title,
              status: topicStatus,
              category,
              created,
              threads: threadCount,
              lastModified: stat.mtimeMs
            });
          } catch (e) {
            console.error(`Failed to parse topic ${name}:`, e.message);
          }
        });
        result.topics.total = topics.length;
        const statusPriority = { active: 0, resolved: 1, parked: 2 };
        topics.sort((a, b) => {
          const statusDiff = (statusPriority[a.status] || 3) - (statusPriority[b.status] || 3);
          if (statusDiff !== 0) return statusDiff;
          return b.lastModified - a.lastModified;
        });
        let filtered = topics;
        if (filterStatus !== "all") {
          filtered = topics.filter((t) => t.status === filterStatus);
        }
        const paginated = filtered.slice(offset, offset + limit);
        result.recentTopics = paginated.map((t) => ({
          name: t.name,
          title: t.title,
          status: t.status,
          threads: t.threads,
          age: formatTimeAgo(new Date(t.lastModified))
        }));
        if (fs2.existsSync(orphansDir)) {
          try {
            result.orphans = fs2.readdirSync(orphansDir).filter((f) => f.endsWith(".md")).length;
          } catch (e) {
          }
        }
        result.lastUpdated = latestModified ? latestModified.toISOString() : null;
      } catch (e) {
        console.error("Failed to get Cerebro topics:", e.message);
      }
      return result;
    }
    function updateTopicStatus2(cerebroDir, topicId, newStatus) {
      const topicDir = path2.join(cerebroDir, "topics", topicId);
      const topicFile = path2.join(topicDir, "topic.md");
      if (!fs2.existsSync(topicDir)) {
        return { error: `Topic '${topicId}' not found`, code: 404 };
      }
      if (!fs2.existsSync(topicFile)) {
        const content2 = `---
title: ${topicId}
status: ${newStatus}
category: general
created: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
---

# ${topicId}

## Overview
*Topic tracking file.*

## Notes
`;
        fs2.writeFileSync(topicFile, content2, "utf8");
        return {
          topic: {
            id: topicId,
            name: topicId,
            title: topicId,
            status: newStatus
          }
        };
      }
      let content = fs2.readFileSync(topicFile, "utf8");
      let title = topicId;
      const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
      if (frontmatterMatch) {
        let frontmatter = frontmatterMatch[1];
        const titleMatch = frontmatter.match(/title:\s*["']?([^"'\n]+)["']?/i);
        if (titleMatch) title = titleMatch[1];
        if (frontmatter.includes("status:")) {
          frontmatter = frontmatter.replace(
            /status:\s*(active|resolved|parked)/i,
            `status: ${newStatus}`
          );
        } else {
          frontmatter = frontmatter.trim() + `
status: ${newStatus}`;
        }
        content = content.replace(/^---\n[\s\S]*?\n---/, `---
${frontmatter}
---`);
      } else {
        const headerMatch = content.match(/^#\s*(.+)/m);
        if (headerMatch) title = headerMatch[1];
        const frontmatter = `---
title: ${title}
status: ${newStatus}
category: general
created: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
---

`;
        content = frontmatter + content;
      }
      fs2.writeFileSync(topicFile, content, "utf8");
      return {
        topic: {
          id: topicId,
          name: topicId,
          title,
          status: newStatus
        }
      };
    }
    module2.exports = {
      getCerebroTopics: getCerebroTopics2,
      updateTopicStatus: updateTopicStatus2
    };
  }
});

// src/tokens.js
var require_tokens = __commonJS({
  "src/tokens.js"(exports2, module2) {
    var fs2 = require("fs");
    var path2 = require("path");
    var { formatNumber, formatTokens } = require_utils();
    var TOKEN_RATES = {
      input: 15,
      // $15/1M input tokens
      output: 75,
      // $75/1M output tokens
      cacheRead: 1.5,
      // $1.50/1M (90% discount from input)
      cacheWrite: 18.75
      // $18.75/1M (25% premium on input)
    };
    var tokenUsageCache = { data: null, timestamp: 0, refreshing: false };
    var TOKEN_USAGE_CACHE_TTL = 3e4;
    var refreshInterval = null;
    function emptyUsageBucket() {
      return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, requests: 0 };
    }
    async function refreshTokenUsageAsync2(getOpenClawDir2) {
      if (tokenUsageCache.refreshing) return;
      tokenUsageCache.refreshing = true;
      try {
        const sessionsDir = path2.join(getOpenClawDir2(), "agents", "main", "sessions");
        const files = await fs2.promises.readdir(sessionsDir);
        const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
        const now = Date.now();
        const oneDayAgo = now - 24 * 60 * 60 * 1e3;
        const threeDaysAgo = now - 3 * 24 * 60 * 60 * 1e3;
        const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1e3;
        const usage24h = emptyUsageBucket();
        const usage3d = emptyUsageBucket();
        const usage7d = emptyUsageBucket();
        const batchSize = 50;
        for (let i = 0; i < jsonlFiles.length; i += batchSize) {
          const batch = jsonlFiles.slice(i, i + batchSize);
          await Promise.all(
            batch.map(async (file) => {
              const filePath = path2.join(sessionsDir, file);
              try {
                const stat = await fs2.promises.stat(filePath);
                if (stat.mtimeMs < sevenDaysAgo) return;
                const content = await fs2.promises.readFile(filePath, "utf8");
                const lines = content.trim().split("\n");
                for (const line of lines) {
                  if (!line) continue;
                  try {
                    const entry = JSON.parse(line);
                    const entryTime = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
                    if (entryTime < sevenDaysAgo) continue;
                    if (entry.message?.usage) {
                      const u = entry.message.usage;
                      const input = u.input || 0;
                      const output = u.output || 0;
                      const cacheRead = u.cacheRead || 0;
                      const cacheWrite = u.cacheWrite || 0;
                      const cost = u.cost?.total || 0;
                      if (entryTime >= oneDayAgo) {
                        usage24h.input += input;
                        usage24h.output += output;
                        usage24h.cacheRead += cacheRead;
                        usage24h.cacheWrite += cacheWrite;
                        usage24h.cost += cost;
                        usage24h.requests++;
                      }
                      if (entryTime >= threeDaysAgo) {
                        usage3d.input += input;
                        usage3d.output += output;
                        usage3d.cacheRead += cacheRead;
                        usage3d.cacheWrite += cacheWrite;
                        usage3d.cost += cost;
                        usage3d.requests++;
                      }
                      usage7d.input += input;
                      usage7d.output += output;
                      usage7d.cacheRead += cacheRead;
                      usage7d.cacheWrite += cacheWrite;
                      usage7d.cost += cost;
                      usage7d.requests++;
                    }
                  } catch (e) {
                  }
                }
              } catch (e) {
              }
            })
          );
          await new Promise((resolve) => setImmediate(resolve));
        }
        const finalizeBucket = (bucket) => ({
          ...bucket,
          tokensNoCache: bucket.input + bucket.output,
          tokensWithCache: bucket.input + bucket.output + bucket.cacheRead + bucket.cacheWrite
        });
        const result = {
          // Primary (24h) for backward compatibility
          ...finalizeBucket(usage24h),
          // All three windows
          windows: {
            "24h": finalizeBucket(usage24h),
            "3d": finalizeBucket(usage3d),
            "7d": finalizeBucket(usage7d)
          }
        };
        tokenUsageCache = { data: result, timestamp: Date.now(), refreshing: false };
        console.log(
          `[Token Usage] Cached: 24h=${usage24h.requests} 3d=${usage3d.requests} 7d=${usage7d.requests} requests`
        );
      } catch (e) {
        console.error("[Token Usage] Refresh error:", e.message);
        tokenUsageCache.refreshing = false;
      }
    }
    function getDailyTokenUsage2(getOpenClawDir2) {
      const now = Date.now();
      const isStale = now - tokenUsageCache.timestamp > TOKEN_USAGE_CACHE_TTL;
      if (isStale && !tokenUsageCache.refreshing && getOpenClawDir2) {
        refreshTokenUsageAsync2(getOpenClawDir2);
      }
      const emptyResult = {
        input: 0,
        output: 0,
        cacheRead: 0,
        cacheWrite: 0,
        cost: 0,
        requests: 0,
        tokensNoCache: 0,
        tokensWithCache: 0,
        windows: {
          "24h": {
            input: 0,
            output: 0,
            cacheRead: 0,
            cacheWrite: 0,
            cost: 0,
            requests: 0,
            tokensNoCache: 0,
            tokensWithCache: 0
          },
          "3d": {
            input: 0,
            output: 0,
            cacheRead: 0,
            cacheWrite: 0,
            cost: 0,
            requests: 0,
            tokensNoCache: 0,
            tokensWithCache: 0
          },
          "7d": {
            input: 0,
            output: 0,
            cacheRead: 0,
            cacheWrite: 0,
            cost: 0,
            requests: 0,
            tokensNoCache: 0,
            tokensWithCache: 0
          }
        }
      };
      return tokenUsageCache.data || emptyResult;
    }
    function calculateCostForBucket(bucket, rates = TOKEN_RATES) {
      const inputCost = bucket.input / 1e6 * rates.input;
      const outputCost = bucket.output / 1e6 * rates.output;
      const cacheReadCost = bucket.cacheRead / 1e6 * rates.cacheRead;
      const cacheWriteCost = bucket.cacheWrite / 1e6 * rates.cacheWrite;
      return {
        inputCost,
        outputCost,
        cacheReadCost,
        cacheWriteCost,
        totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost
      };
    }
    function getCostBreakdown2(config, getSessions, getOpenClawDir2) {
      const usage = getDailyTokenUsage2(getOpenClawDir2);
      if (!usage) {
        return { error: "Failed to get usage data" };
      }
      const costs = calculateCostForBucket(usage);
      const planCost = config.billing?.claudePlanCost || 200;
      const planName = config.billing?.claudePlanName || "Claude Code Max";
      const windowConfigs = {
        "24h": { days: 1, label: "24h" },
        "3d": { days: 3, label: "3dma" },
        "7d": { days: 7, label: "7dma" }
      };
      const windows = {};
      for (const [key, windowConfig] of Object.entries(windowConfigs)) {
        const bucket = usage.windows?.[key] || usage;
        const bucketCosts = calculateCostForBucket(bucket);
        const dailyAvg = bucketCosts.totalCost / windowConfig.days;
        const monthlyProjected = dailyAvg * 30;
        const monthlySavings = monthlyProjected - planCost;
        windows[key] = {
          label: windowConfig.label,
          days: windowConfig.days,
          totalCost: bucketCosts.totalCost,
          dailyAvg,
          monthlyProjected,
          monthlySavings,
          savingsPercent: monthlySavings > 0 ? Math.round(monthlySavings / monthlyProjected * 100) : 0,
          requests: bucket.requests,
          tokens: {
            input: bucket.input,
            output: bucket.output,
            cacheRead: bucket.cacheRead,
            cacheWrite: bucket.cacheWrite
          }
        };
      }
      return {
        // Raw token counts (24h for backward compatibility)
        inputTokens: usage.input,
        outputTokens: usage.output,
        cacheRead: usage.cacheRead,
        cacheWrite: usage.cacheWrite,
        requests: usage.requests,
        // Pricing rates
        rates: {
          input: TOKEN_RATES.input.toFixed(2),
          output: TOKEN_RATES.output.toFixed(2),
          cacheRead: TOKEN_RATES.cacheRead.toFixed(2),
          cacheWrite: TOKEN_RATES.cacheWrite.toFixed(2)
        },
        // Cost calculation breakdown (24h)
        calculation: {
          inputCost: costs.inputCost,
          outputCost: costs.outputCost,
          cacheReadCost: costs.cacheReadCost,
          cacheWriteCost: costs.cacheWriteCost
        },
        // Totals (24h for backward compatibility)
        totalCost: costs.totalCost,
        planCost,
        planName,
        // Period
        period: "24 hours",
        // Multi-window data for moving averages
        windows,
        // Top sessions by tokens
        topSessions: getTopSessionsByTokens(5, getSessions)
      };
    }
    function getTopSessionsByTokens(limit = 5, getSessions) {
      try {
        const sessions2 = getSessions({ limit: null });
        return sessions2.filter((s) => s.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, limit).map((s) => ({
          label: s.label,
          tokens: s.tokens,
          channel: s.channel,
          active: s.active
        }));
      } catch (e) {
        console.error("[TopSessions] Error:", e.message);
        return [];
      }
    }
    function getTokenStats2(sessions2, capacity, config = {}) {
      let activeMainCount = capacity?.main?.active ?? 0;
      let activeSubagentCount = capacity?.subagent?.active ?? 0;
      let activeCount = activeMainCount + activeSubagentCount;
      let mainLimit = capacity?.main?.max ?? 12;
      let subagentLimit = capacity?.subagent?.max ?? 24;
      if (!capacity && sessions2 && sessions2.length > 0) {
        activeCount = 0;
        activeMainCount = 0;
        activeSubagentCount = 0;
        sessions2.forEach((s) => {
          if (s.active) {
            activeCount++;
            if (s.key && s.key.includes(":subagent:")) {
              activeSubagentCount++;
            } else {
              activeMainCount++;
            }
          }
        });
      }
      const usage = getDailyTokenUsage2();
      const totalInput = usage?.input || 0;
      const totalOutput = usage?.output || 0;
      const total = totalInput + totalOutput;
      const costs = calculateCostForBucket(usage);
      const estCost = costs.totalCost;
      const planCost = config?.billing?.claudePlanCost ?? 200;
      const planName = config?.billing?.claudePlanName ?? "Claude Code Max";
      const monthlyApiCost = estCost * 30;
      const monthlySavings = monthlyApiCost - planCost;
      const savingsPositive = monthlySavings > 0;
      const sessionCount = sessions2?.length || 1;
      const avgTokensPerSession = Math.round(total / sessionCount);
      const avgCostPerSession = estCost / sessionCount;
      const windowConfigs = {
        "24h": { days: 1, label: "24h" },
        "3dma": { days: 3, label: "3dma" },
        "7dma": { days: 7, label: "7dma" }
      };
      const savingsWindows = {};
      for (const [key, windowConfig] of Object.entries(windowConfigs)) {
        const bucketKey = key.replace("dma", "d").replace("24h", "24h");
        const bucket = usage.windows?.[bucketKey === "24h" ? "24h" : bucketKey] || usage;
        const bucketCosts = calculateCostForBucket(bucket);
        const dailyAvg = bucketCosts.totalCost / windowConfig.days;
        const monthlyProjected = dailyAvg * 30;
        const windowSavings = monthlyProjected - planCost;
        const windowSavingsPositive = windowSavings > 0;
        savingsWindows[key] = {
          label: windowConfig.label,
          estCost: `$${formatNumber(dailyAvg)}`,
          estMonthlyCost: `$${Math.round(monthlyProjected).toLocaleString()}`,
          estSavings: windowSavingsPositive ? `$${formatNumber(windowSavings)}/mo` : null,
          savingsPercent: windowSavingsPositive ? Math.round(windowSavings / monthlyProjected * 100) : 0,
          requests: bucket.requests
        };
      }
      return {
        total: formatTokens(total),
        input: formatTokens(totalInput),
        output: formatTokens(totalOutput),
        cacheRead: formatTokens(usage?.cacheRead || 0),
        cacheWrite: formatTokens(usage?.cacheWrite || 0),
        requests: usage?.requests || 0,
        activeCount,
        activeMainCount,
        activeSubagentCount,
        mainLimit,
        subagentLimit,
        estCost: `$${formatNumber(estCost)}`,
        planCost: `$${planCost.toFixed(0)}`,
        planName,
        // 24h savings (backward compatible)
        estSavings: savingsPositive ? `$${formatNumber(monthlySavings)}/mo` : null,
        savingsPercent: savingsPositive ? Math.round(monthlySavings / monthlyApiCost * 100) : 0,
        estMonthlyCost: `$${Math.round(monthlyApiCost).toLocaleString()}`,
        // Multi-window savings (24h, 3da, 7da)
        savingsWindows,
        // Per-session averages
        avgTokensPerSession: formatTokens(avgTokensPerSession),
        avgCostPerSession: `$${avgCostPerSession.toFixed(2)}`,
        sessionCount
      };
    }
    function startTokenUsageRefresh2(getOpenClawDir2) {
      refreshTokenUsageAsync2(getOpenClawDir2);
      if (refreshInterval) {
        clearInterval(refreshInterval);
      }
      refreshInterval = setInterval(() => {
        refreshTokenUsageAsync2(getOpenClawDir2);
      }, TOKEN_USAGE_CACHE_TTL);
      return refreshInterval;
    }
    module2.exports = {
      TOKEN_RATES,
      emptyUsageBucket,
      refreshTokenUsageAsync: refreshTokenUsageAsync2,
      getDailyTokenUsage: getDailyTokenUsage2,
      calculateCostForBucket,
      getCostBreakdown: getCostBreakdown2,
      getTopSessionsByTokens,
      getTokenStats: getTokenStats2,
      startTokenUsageRefresh: startTokenUsageRefresh2
    };
  }
});

// src/llm-usage.js
var require_llm_usage = __commonJS({
  "src/llm-usage.js"(exports2, module2) {
    var fs2 = require("fs");
    var path2 = require("path");
    var { execFile } = require("child_process");
    var { getSafeEnv } = require_openclaw();
    var llmUsageCache = { data: null, timestamp: 0, refreshing: false };
    var LLM_CACHE_TTL_MS = 6e4;
    function refreshLlmUsageAsync() {
      if (llmUsageCache.refreshing) return;
      llmUsageCache.refreshing = true;
      const profile = process.env.OPENCLAW_PROFILE || "";
      const args2 = profile ? ["--profile", profile, "status", "--usage", "--json"] : ["status", "--usage", "--json"];
      execFile(
        "openclaw",
        args2,
        { encoding: "utf8", timeout: 2e4, env: getSafeEnv() },
        (err, stdout) => {
          llmUsageCache.refreshing = false;
          if (err) {
            console.error("[LLM Usage] Async refresh failed:", err.message);
            return;
          }
          try {
            const jsonStart = stdout.indexOf("{");
            const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : stdout;
            const parsed = JSON.parse(jsonStr);
            if (parsed.usage) {
              const result = transformLiveUsageData(parsed.usage);
              llmUsageCache.data = result;
              llmUsageCache.timestamp = Date.now();
              console.log("[LLM Usage] Cache refreshed");
            }
          } catch (e) {
            console.error("[LLM Usage] Parse error:", e.message);
          }
        }
      );
    }
    function transformLiveUsageData(usage) {
      const anthropic = usage.providers?.find((p) => p.provider === "anthropic");
      const codexProvider = usage.providers?.find((p) => p.provider === "openai-codex");
      if (anthropic?.error) {
        return {
          timestamp: (/* @__PURE__ */ new Date()).toISOString(),
          source: "error",
          error: anthropic.error,
          errorType: anthropic.error.includes("403") ? "auth" : "unknown",
          claude: {
            session: { usedPct: null, remainingPct: null, resetsIn: null, error: anthropic.error },
            weekly: { usedPct: null, remainingPct: null, resets: null, error: anthropic.error },
            sonnet: { usedPct: null, remainingPct: null, resets: null, error: anthropic.error },
            lastSynced: null
          },
          codex: { sessionsToday: 0, tasksToday: 0, usage5hPct: 0, usageDayPct: 0 },
          routing: {
            total: 0,
            claudeTasks: 0,
            codexTasks: 0,
            claudePct: 0,
            codexPct: 0,
            codexFloor: 20
          }
        };
      }
      const session5h = anthropic?.windows?.find((w) => w.label === "5h");
      const weekAll = anthropic?.windows?.find((w) => w.label === "Week");
      const sonnetWeek = anthropic?.windows?.find((w) => w.label === "Sonnet");
      const codex5h = codexProvider?.windows?.find((w) => w.label === "5h");
      const codexDay = codexProvider?.windows?.find((w) => w.label === "Day");
      const formatReset = (resetAt) => {
        if (!resetAt) return "?";
        const diff = resetAt - Date.now();
        if (diff < 0) return "now";
        if (diff < 36e5) return Math.round(diff / 6e4) + "m";
        if (diff < 864e5) return Math.round(diff / 36e5) + "h";
        return Math.round(diff / 864e5) + "d";
      };
      return {
        timestamp: (/* @__PURE__ */ new Date()).toISOString(),
        source: "live",
        claude: {
          session: {
            usedPct: Math.round(session5h?.usedPercent || 0),
            remainingPct: Math.round(100 - (session5h?.usedPercent || 0)),
            resetsIn: formatReset(session5h?.resetAt)
          },
          weekly: {
            usedPct: Math.round(weekAll?.usedPercent || 0),
            remainingPct: Math.round(100 - (weekAll?.usedPercent || 0)),
            resets: formatReset(weekAll?.resetAt)
          },
          sonnet: {
            usedPct: Math.round(sonnetWeek?.usedPercent || 0),
            remainingPct: Math.round(100 - (sonnetWeek?.usedPercent || 0)),
            resets: formatReset(sonnetWeek?.resetAt)
          },
          lastSynced: (/* @__PURE__ */ new Date()).toISOString()
        },
        codex: {
          sessionsToday: 0,
          tasksToday: 0,
          usage5hPct: Math.round(codex5h?.usedPercent || 0),
          usageDayPct: Math.round(codexDay?.usedPercent || 0)
        },
        routing: { total: 0, claudeTasks: 0, codexTasks: 0, claudePct: 0, codexPct: 0, codexFloor: 20 }
      };
    }
    function getLlmUsage2(statePath) {
      const now = Date.now();
      if (!llmUsageCache.data || now - llmUsageCache.timestamp > LLM_CACHE_TTL_MS) {
        refreshLlmUsageAsync();
      }
      if (llmUsageCache.data && llmUsageCache.data.source !== "error") {
        return llmUsageCache.data;
      }
      const stateFile = path2.join(statePath, "llm-routing.json");
      try {
        if (fs2.existsSync(stateFile)) {
          const data = JSON.parse(fs2.readFileSync(stateFile, "utf8"));
          const sessionValid = data.claude?.session?.resets_in && data.claude.session.resets_in !== "unknown";
          const weeklyValid = data.claude?.weekly_all_models?.resets && data.claude.weekly_all_models.resets !== "unknown";
          if (sessionValid || weeklyValid) {
            return {
              timestamp: (/* @__PURE__ */ new Date()).toISOString(),
              source: "file",
              claude: {
                session: {
                  usedPct: Math.round((data.claude?.session?.used_pct || 0) * 100),
                  remainingPct: Math.round((data.claude?.session?.remaining_pct || 1) * 100),
                  resetsIn: data.claude?.session?.resets_in || "?"
                },
                weekly: {
                  usedPct: Math.round((data.claude?.weekly_all_models?.used_pct || 0) * 100),
                  remainingPct: Math.round((data.claude?.weekly_all_models?.remaining_pct || 1) * 100),
                  resets: data.claude?.weekly_all_models?.resets || "?"
                },
                sonnet: {
                  usedPct: Math.round((data.claude?.weekly_sonnet?.used_pct || 0) * 100),
                  remainingPct: Math.round((data.claude?.weekly_sonnet?.remaining_pct || 1) * 100),
                  resets: data.claude?.weekly_sonnet?.resets || "?"
                },
                lastSynced: data.claude?.last_synced || null
              },
              codex: {
                sessionsToday: data.codex?.sessions_today || 0,
                tasksToday: data.codex?.tasks_today || 0,
                usage5hPct: data.codex?.usage_5h_pct || 0,
                usageDayPct: data.codex?.usage_day_pct || 0
              },
              routing: {
                total: data.routing?.total_tasks || 0,
                claudeTasks: data.routing?.claude_tasks || 0,
                codexTasks: data.routing?.codex_tasks || 0,
                claudePct: data.routing?.total_tasks > 0 ? Math.round(data.routing.claude_tasks / data.routing.total_tasks * 100) : 0,
                codexPct: data.routing?.total_tasks > 0 ? Math.round(data.routing.codex_tasks / data.routing.total_tasks * 100) : 0,
                codexFloor: Math.round((data.routing?.codex_floor_pct || 0.2) * 100)
              }
            };
          }
        }
      } catch (e) {
        console.error("[LLM Usage] File fallback failed:", e.message);
      }
      return {
        timestamp: (/* @__PURE__ */ new Date()).toISOString(),
        source: "error",
        error: "API key lacks user:profile OAuth scope",
        errorType: "auth",
        claude: {
          session: { usedPct: null, remainingPct: null, resetsIn: null, error: "Auth required" },
          weekly: { usedPct: null, remainingPct: null, resets: null, error: "Auth required" },
          sonnet: { usedPct: null, remainingPct: null, resets: null, error: "Auth required" },
          lastSynced: null
        },
        codex: { sessionsToday: 0, tasksToday: 0, usage5hPct: 0, usageDayPct: 0 },
        routing: { total: 0, claudeTasks: 0, codexTasks: 0, claudePct: 0, codexPct: 0, codexFloor: 20 }
      };
    }
    function getRoutingStats2(skillsPath, statePath, hours = 24) {
      const safeHours = parseInt(hours, 10) || 24;
      try {
        const { execFileSync } = require("child_process");
        const skillDir = path2.join(skillsPath, "llm_routing");
        const output = execFileSync(
          "python",
          ["-m", "llm_routing", "stats", "--hours", String(safeHours), "--json"],
          {
            encoding: "utf8",
            timeout: 1e4,
            cwd: skillDir,
            env: getSafeEnv()
          }
        );
        return JSON.parse(output);
      } catch (e) {
        try {
          const logFile = path2.join(statePath, "routing-log.jsonl");
          if (!fs2.existsSync(logFile)) {
            return { total_requests: 0, by_model: {}, by_task_type: {} };
          }
          const cutoff = Date.now() - hours * 3600 * 1e3;
          const lines = fs2.readFileSync(logFile, "utf8").trim().split("\n").filter(Boolean);
          const stats = {
            total_requests: 0,
            by_model: {},
            by_task_type: {},
            escalations: 0,
            avg_latency_ms: 0,
            success_rate: 0
          };
          let latencies = [];
          let successes = 0;
          for (const line of lines) {
            try {
              const entry = JSON.parse(line);
              const ts = new Date(entry.timestamp).getTime();
              if (ts < cutoff) continue;
              stats.total_requests++;
              const model = entry.selected_model || "unknown";
              stats.by_model[model] = (stats.by_model[model] || 0) + 1;
              const tt = entry.task_type || "unknown";
              stats.by_task_type[tt] = (stats.by_task_type[tt] || 0) + 1;
              if (entry.escalation_reason) stats.escalations++;
              if (entry.latency_ms) latencies.push(entry.latency_ms);
              if (entry.success === true) successes++;
            } catch {
            }
          }
          if (latencies.length > 0) {
            stats.avg_latency_ms = Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length);
          }
          if (stats.total_requests > 0) {
            stats.success_rate = Math.round(successes / stats.total_requests * 100);
          }
          return stats;
        } catch (e2) {
          console.error("Failed to read routing stats:", e2.message);
          return { error: e2.message };
        }
      }
    }
    function startLlmUsageRefresh2() {
      setTimeout(() => refreshLlmUsageAsync(), 1e3);
      setInterval(() => refreshLlmUsageAsync(), LLM_CACHE_TTL_MS);
    }
    module2.exports = {
      refreshLlmUsageAsync,
      transformLiveUsageData,
      getLlmUsage: getLlmUsage2,
      getRoutingStats: getRoutingStats2,
      startLlmUsageRefresh: startLlmUsageRefresh2
    };
  }
});

// src/actions.js
var require_actions = __commonJS({
  "src/actions.js"(exports2, module2) {
    var ALLOWED_ACTIONS = /* @__PURE__ */ new Set([
      "gateway-status",
      "gateway-restart",
      "sessions-list",
      "cron-list",
      "health-check",
      "clear-stale-sessions"
    ]);
    function executeAction2(action, deps) {
      const { runOpenClaw: runOpenClaw2, extractJSON: extractJSON2, PORT: PORT2 } = deps;
      const results = { success: false, action, output: "", error: null };
      if (!ALLOWED_ACTIONS.has(action)) {
        results.error = `Unknown action: ${action}`;
        return results;
      }
      try {
        switch (action) {
          case "gateway-status":
            results.output = runOpenClaw2("gateway status 2>&1") || "Unknown";
            results.success = true;
            break;
          case "gateway-restart":
            results.output = "To restart gateway, run: openclaw gateway restart";
            results.success = true;
            results.note = "Dashboard cannot restart gateway for safety";
            break;
          case "sessions-list":
            results.output = runOpenClaw2("sessions 2>&1") || "No sessions";
            results.success = true;
            break;
          case "cron-list":
            results.output = runOpenClaw2("cron list 2>&1") || "No cron jobs";
            results.success = true;
            break;
          case "health-check": {
            const gateway = runOpenClaw2("gateway status 2>&1");
            const sessions2 = runOpenClaw2("sessions --json 2>&1");
            let sessionCount = 0;
            try {
              const data = JSON.parse(sessions2);
              sessionCount = data.sessions?.length || 0;
            } catch (e) {
            }
            results.output = [
              `Gateway: ${gateway?.includes("running") ? "OK Running" : "NOT Running"}`,
              `Sessions: ${sessionCount}`,
              `Dashboard: OK Running on port ${PORT2}`
            ].join("\n");
            results.success = true;
            break;
          }
          case "clear-stale-sessions": {
            const staleOutput = runOpenClaw2("sessions --json 2>&1");
            let staleCount = 0;
            try {
              const staleJson = extractJSON2(staleOutput);
              if (staleJson) {
                const data = JSON.parse(staleJson);
                staleCount = (data.sessions || []).filter((s) => s.ageMs > 24 * 60 * 60 * 1e3).length;
              }
            } catch (e) {
            }
            results.output = `Found ${staleCount} stale sessions (>24h old).
To clean: openclaw sessions prune`;
            results.success = true;
            break;
          }
        }
      } catch (e) {
        results.error = e.message;
      }
      return results;
    }
    module2.exports = { executeAction: executeAction2, ALLOWED_ACTIONS };
  }
});

// src/data.js
var require_data = __commonJS({
  "src/data.js"(exports2, module2) {
    var fs2 = require("fs");
    var path2 = require("path");
    function migrateDataDir2(dataDir, legacyDataDir) {
      try {
        if (!fs2.existsSync(legacyDataDir)) return;
        if (!fs2.existsSync(dataDir)) {
          fs2.mkdirSync(dataDir, { recursive: true });
        }
        const legacyFiles = fs2.readdirSync(legacyDataDir);
        if (legacyFiles.length === 0) return;
        let migrated = 0;
        for (const file of legacyFiles) {
          const srcPath = path2.join(legacyDataDir, file);
          const destPath = path2.join(dataDir, file);
          if (fs2.existsSync(destPath)) continue;
          const stat = fs2.statSync(srcPath);
          if (stat.isFile()) {
            fs2.copyFileSync(srcPath, destPath);
            migrated++;
            console.log(`[Migration] Copied ${file} to profile-aware data dir`);
          }
        }
        if (migrated > 0) {
          console.log(`[Migration] Migrated ${migrated} file(s) to ${dataDir}`);
          console.log(`[Migration] Legacy data preserved at ${legacyDataDir}`);
        }
      } catch (e) {
        console.error("[Migration] Failed to migrate data:", e.message);
      }
    }
    module2.exports = { migrateDataDir: migrateDataDir2 };
  }
});

// src/state.js
var require_state = __commonJS({
  "src/state.js"(exports2, module2) {
    var fs2 = require("fs");
    var os = require("os");
    var path2 = require("path");
    var { execFileSync } = require("child_process");
    var { formatBytes, formatTimeAgo } = require_utils();
    function createStateModule2(deps) {
      const {
        CONFIG: CONFIG2,
        getOpenClawDir: getOpenClawDir2,
        getSessions,
        getSystemVitals: getSystemVitals2,
        getCronJobs: getCronJobs2,
        loadOperators: loadOperators2,
        getLlmUsage: getLlmUsage2,
        getDailyTokenUsage: getDailyTokenUsage2,
        getTokenStats: getTokenStats2,
        getCerebroTopics: getCerebroTopics2,
        runOpenClaw: runOpenClaw2,
        extractJSON: extractJSON2,
        readTranscript
      } = deps;
      const PATHS2 = CONFIG2.paths;
      let cachedState = null;
      let lastStateUpdate = 0;
      const STATE_CACHE_TTL = 3e4;
      let stateRefreshInterval = null;
      function getSystemStatus() {
        const hostname = os.hostname();
        let uptime = "\u2014";
        try {
          const uptimeRaw = execFileSync("uptime", [], { encoding: "utf8" });
          const match = uptimeRaw.match(/up\s+([^,]+)/);
          if (match) uptime = match[1].trim();
        } catch (e) {
        }
        let gateway = "Unknown";
        try {
          const status = runOpenClaw2("gateway status 2>/dev/null");
          if (status && status.includes("running")) {
            gateway = "Running";
          } else if (status && status.includes("stopped")) {
            gateway = "Stopped";
          }
        } catch (e) {
        }
        return {
          hostname,
          gateway,
          model: "claude-opus-4-5",
          uptime
        };
      }
      function getRecentActivity() {
        const activities = [];
        const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
        const memoryFile = path2.join(PATHS2.memory, `${today}.md`);
        try {
          if (fs2.existsSync(memoryFile)) {
            const content = fs2.readFileSync(memoryFile, "utf8");
            const lines = content.split("\n").filter((l) => l.startsWith("- "));
            lines.slice(-5).forEach((line) => {
              const text = line.replace(/^- /, "").slice(0, 80);
              activities.push({
                icon: text.includes("\u2705") ? "\u2705" : text.includes("\u274C") ? "\u274C" : "\u{1F4DD}",
                text: text.replace(/[\u2705\u274C\uD83D\uDCDD\uD83D\uDD27]/g, "").trim(),
                time: today
              });
            });
          }
        } catch (e) {
          console.error("Failed to read activity:", e.message);
        }
        return activities.reverse();
      }
      function getCapacity() {
        const result = {
          main: { active: 0, max: 12 },
          subagent: { active: 0, max: 24 }
        };
        const openclawDir = getOpenClawDir2();
        try {
          const configPath = path2.join(openclawDir, "openclaw.json");
          if (fs2.existsSync(configPath)) {
            const config = JSON.parse(fs2.readFileSync(configPath, "utf8"));
            if (config?.agents?.defaults?.maxConcurrent) {
              result.main.max = config.agents.defaults.maxConcurrent;
            }
            if (config?.agents?.defaults?.subagents?.maxConcurrent) {
              result.subagent.max = config.agents.defaults.subagents.maxConcurrent;
            }
          }
        } catch (e) {
        }
        try {
          const output = runOpenClaw2("sessions --json 2>/dev/null");
          const jsonStr = extractJSON2(output);
          if (jsonStr) {
            const data = JSON.parse(jsonStr);
            const sessions2 = data.sessions || [];
            const fiveMinMs = 5 * 60 * 1e3;
            for (const s of sessions2) {
              if (s.ageMs > fiveMinMs) continue;
              const key = s.key || "";
              if (key.includes(":subagent:") || key.includes(":cron:")) {
                result.subagent.active++;
              } else {
                result.main.active++;
              }
            }
            return result;
          }
        } catch (e) {
          console.error("Failed to get capacity from sessions, falling back to filesystem:", e.message);
        }
        try {
          const sessionsDir = path2.join(openclawDir, "agents", "main", "sessions");
          if (fs2.existsSync(sessionsDir)) {
            const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
            const files = fs2.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
            let mainActive = 0;
            let subActive = 0;
            for (const file of files) {
              try {
                const filePath = path2.join(sessionsDir, file);
                const stat = fs2.statSync(filePath);
                if (stat.mtimeMs < fiveMinAgo) continue;
                let isSubagent = false;
                try {
                  const fd = fs2.openSync(filePath, "r");
                  const buffer = Buffer.alloc(512);
                  fs2.readSync(fd, buffer, 0, 512, 0);
                  fs2.closeSync(fd);
                  const firstLine = buffer.toString("utf8").split("\n")[0];
                  const parsed = JSON.parse(firstLine);
                  const key = parsed.key || parsed.id || "";
                  isSubagent = key.includes(":subagent:") || key.includes(":cron:");
                } catch (parseErr) {
                  isSubagent = file.includes("subagent");
                }
                if (isSubagent) {
                  subActive++;
                } else {
                  mainActive++;
                }
              } catch (e) {
              }
            }
            result.main.active = mainActive;
            result.subagent.active = subActive;
          }
        } catch (e) {
          console.error("Failed to count active sessions from filesystem:", e.message);
        }
        return result;
      }
      function getMemoryStats() {
        const memoryDir = PATHS2.memory;
        const memoryFile = path2.join(PATHS2.workspace, "MEMORY.md");
        const stats = {
          totalFiles: 0,
          totalSize: 0,
          totalSizeFormatted: "0 B",
          memoryMdSize: 0,
          memoryMdSizeFormatted: "0 B",
          memoryMdLines: 0,
          recentFiles: [],
          oldestFile: null,
          newestFile: null
        };
        try {
          const collectMemoryFiles = (dir, baseDir) => {
            const entries = fs2.readdirSync(dir, { withFileTypes: true });
            const files = [];
            for (const entry of entries) {
              const entryPath = path2.join(dir, entry.name);
              if (entry.isDirectory()) {
                files.push(...collectMemoryFiles(entryPath, baseDir));
              } else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".json"))) {
                const stat = fs2.statSync(entryPath);
                const relativePath = path2.relative(baseDir, entryPath);
                files.push({
                  name: relativePath,
                  size: stat.size,
                  sizeFormatted: formatBytes(stat.size),
                  modified: stat.mtime
                });
              }
            }
            return files;
          };
          if (fs2.existsSync(memoryFile)) {
            const memStat = fs2.statSync(memoryFile);
            stats.memoryMdSize = memStat.size;
            stats.memoryMdSizeFormatted = formatBytes(memStat.size);
            const content = fs2.readFileSync(memoryFile, "utf8");
            stats.memoryMdLines = content.split("\n").length;
            stats.totalSize += memStat.size;
            stats.totalFiles++;
          }
          if (fs2.existsSync(memoryDir)) {
            const files = collectMemoryFiles(memoryDir, memoryDir).sort(
              (a, b) => b.modified - a.modified
            );
            stats.totalFiles += files.length;
            files.forEach((f) => stats.totalSize += f.size);
            stats.recentFiles = files.slice(0, 5).map((f) => ({
              name: f.name,
              sizeFormatted: f.sizeFormatted,
              age: formatTimeAgo(f.modified)
            }));
            if (files.length > 0) {
              stats.newestFile = files[0].name;
              stats.oldestFile = files[files.length - 1].name;
            }
          }
          stats.totalSizeFormatted = formatBytes(stats.totalSize);
        } catch (e) {
          console.error("Failed to get memory stats:", e.message);
        }
        return stats;
      }
      function getData() {
        const allSessions = getSessions({ limit: null });
        const pageSize = 20;
        const displaySessions = allSessions.slice(0, pageSize);
        const tokenStats = getTokenStats2(allSessions);
        const capacity = getCapacity();
        const memory = getMemoryStats();
        const statusCounts = {
          all: allSessions.length,
          live: allSessions.filter((s) => s.active).length,
          recent: allSessions.filter((s) => !s.active && s.recentlyActive).length,
          idle: allSessions.filter((s) => !s.active && !s.recentlyActive).length
        };
        const totalPages = Math.ceil(allSessions.length / pageSize);
        return {
          sessions: displaySessions,
          tokenStats,
          capacity,
          memory,
          pagination: {
            page: 1,
            pageSize,
            total: allSessions.length,
            totalPages,
            hasPrev: false,
            hasNext: totalPages > 1
          },
          statusCounts
        };
      }
      function getFullState() {
        const now = Date.now();
        if (cachedState && now - lastStateUpdate < STATE_CACHE_TTL) {
          return cachedState;
        }
        let sessions2 = [];
        let tokenStats = {};
        let statusCounts = { all: 0, live: 0, recent: 0, idle: 0 };
        let vitals = {};
        let capacity = {};
        let operators = { operators: [], roles: {} };
        let llmUsage = {};
        let cron = [];
        let memory = {};
        let cerebro = {};
        let subagents = [];
        let allSessions = [];
        let totalSessionCount = 0;
        try {
          allSessions = getSessions({ limit: null });
          totalSessionCount = allSessions.length;
          sessions2 = allSessions.slice(0, 20);
        } catch (e) {
          console.error("[State] sessions:", e.message);
        }
        try {
          vitals = getSystemVitals2();
        } catch (e) {
          console.error("[State] vitals:", e.message);
        }
        try {
          capacity = getCapacity();
        } catch (e) {
          console.error("[State] capacity:", e.message);
        }
        try {
          tokenStats = getTokenStats2(allSessions, capacity, CONFIG2);
        } catch (e) {
          console.error("[State] tokenStats:", e.message);
        }
        try {
          const liveSessions = allSessions.filter((s) => s.active);
          const recentSessions = allSessions.filter((s) => !s.active && s.recentlyActive);
          const idleSessions = allSessions.filter((s) => !s.active && !s.recentlyActive);
          statusCounts = {
            all: totalSessionCount,
            live: liveSessions.length,
            recent: recentSessions.length,
            idle: idleSessions.length
          };
        } catch (e) {
          console.error("[State] statusCounts:", e.message);
        }
        try {
          const operatorData = loadOperators2();
          const operatorsWithStats = operatorData.operators.map((op) => {
            const userSessions = allSessions.filter(
              (s) => s.originator?.userId === op.id || s.originator?.userId === op.metadata?.slackId
            );
            return {
              ...op,
              stats: {
                activeSessions: userSessions.filter((s) => s.active).length,
                totalSessions: userSessions.length,
                lastSeen: userSessions.length > 0 ? new Date(
                  Date.now() - Math.min(...userSessions.map((s) => s.minutesAgo)) * 6e4
                ).toISOString() : op.lastSeen
              }
            };
          });
          operators = { ...operatorData, operators: operatorsWithStats };
        } catch (e) {
          console.error("[State] operators:", e.message);
        }
        try {
          llmUsage = getLlmUsage2();
        } catch (e) {
          console.error("[State] llmUsage:", e.message);
        }
        try {
          cron = getCronJobs2();
        } catch (e) {
          console.error("[State] cron:", e.message);
        }
        try {
          memory = getMemoryStats();
        } catch (e) {
          console.error("[State] memory:", e.message);
        }
        try {
          cerebro = getCerebroTopics2();
        } catch (e) {
          console.error("[State] cerebro:", e.message);
        }
        try {
          const retentionHours = parseInt(process.env.SUBAGENT_RETENTION_HOURS || "12", 10);
          const retentionMs = retentionHours * 60 * 60 * 1e3;
          subagents = allSessions.filter((s) => s.sessionKey && s.sessionKey.includes(":subagent:")).filter((s) => (s.minutesAgo || 0) * 6e4 < retentionMs).map((s) => {
            const match = s.sessionKey.match(/:subagent:([a-f0-9-]+)$/);
            const subagentId = match ? match[1] : s.sessionId;
            return {
              id: subagentId,
              shortId: subagentId.slice(0, 8),
              task: s.label || s.displayName || "Sub-agent task",
              tokens: s.tokens || 0,
              ageMs: (s.minutesAgo || 0) * 6e4,
              active: s.active,
              recentlyActive: s.recentlyActive
            };
          });
        } catch (e) {
          console.error("[State] subagents:", e.message);
        }
        cachedState = {
          vitals,
          sessions: sessions2,
          tokenStats,
          statusCounts,
          capacity,
          operators,
          llmUsage,
          cron,
          memory,
          cerebro,
          subagents,
          pagination: {
            page: 1,
            pageSize: 20,
            total: totalSessionCount,
            totalPages: Math.max(1, Math.ceil(totalSessionCount / 20)),
            hasPrev: false,
            hasNext: totalSessionCount > 20
          },
          timestamp: now
        };
        lastStateUpdate = now;
        return cachedState;
      }
      function refreshState() {
        lastStateUpdate = 0;
        return getFullState();
      }
      function startStateRefresh(broadcastSSE2, intervalMs = 3e4) {
        if (stateRefreshInterval) return;
        stateRefreshInterval = setInterval(() => {
          try {
            const newState = refreshState();
            broadcastSSE2("update", newState);
          } catch (e) {
            console.error("[State] Refresh error:", e.message);
          }
        }, intervalMs);
        console.log(`[State] Background refresh started (${intervalMs}ms interval)`);
      }
      function stopStateRefresh() {
        if (stateRefreshInterval) {
          clearInterval(stateRefreshInterval);
          stateRefreshInterval = null;
          console.log("[State] Background refresh stopped");
        }
      }
      function getSubagentStatus() {
        const subagents = [];
        try {
          const output = runOpenClaw2("sessions --json 2>/dev/null");
          const jsonStr = extractJSON2(output);
          if (jsonStr) {
            const data = JSON.parse(jsonStr);
            const subagentSessions = (data.sessions || []).filter(
              (s) => s.key && s.key.includes(":subagent:")
            );
            for (const s of subagentSessions) {
              const ageMs = s.ageMs || Infinity;
              const isActive = ageMs < 5 * 60 * 1e3;
              const isRecent = ageMs < 30 * 60 * 1e3;
              const match = s.key.match(/:subagent:([a-f0-9-]+)$/);
              const subagentId = match ? match[1] : s.sessionId;
              const shortId = subagentId.slice(0, 8);
              let taskSummary = "Unknown task";
              let label = null;
              const transcript = readTranscript(s.sessionId);
              for (const entry of transcript.slice(0, 15)) {
                if (entry.type === "message" && entry.message?.role === "user") {
                  const content = entry.message.content;
                  let text = "";
                  if (typeof content === "string") {
                    text = content;
                  } else if (Array.isArray(content)) {
                    const textPart = content.find((c) => c.type === "text");
                    if (textPart) text = textPart.text || "";
                  }
                  if (!text) continue;
                  const labelMatch = text.match(/Label:\s*([^\n]+)/i);
                  if (labelMatch) {
                    label = labelMatch[1].trim();
                  }
                  let taskMatch = text.match(/You were created to handle:\s*\*\*([^*]+)\*\*/i);
                  if (taskMatch) {
                    taskSummary = taskMatch[1].trim();
                    break;
                  }
                  taskMatch = text.match(/\*\*([A-Z]{2,5}-\d+:\s*[^*]+)\*\*/);
                  if (taskMatch) {
                    taskSummary = taskMatch[1].trim();
                    break;
                  }
                  const firstLine = text.split("\n")[0].replace(/^\*\*|\*\*$/g, "").trim();
                  if (firstLine.length > 10 && firstLine.length < 100) {
                    taskSummary = firstLine;
                    break;
                  }
                }
              }
              const messageCount = transcript.filter(
                (e) => e.type === "message" && e.message?.role
              ).length;
              subagents.push({
                id: subagentId,
                shortId,
                sessionId: s.sessionId,
                label: label || shortId,
                task: taskSummary,
                model: s.model?.replace("anthropic/", "") || "unknown",
                status: isActive ? "active" : isRecent ? "idle" : "stale",
                ageMs,
                ageFormatted: ageMs < 6e4 ? "Just now" : ageMs < 36e5 ? `${Math.round(ageMs / 6e4)}m ago` : `${Math.round(ageMs / 36e5)}h ago`,
                messageCount,
                tokens: s.totalTokens || 0
              });
            }
          }
        } catch (e) {
          console.error("Failed to get subagent status:", e.message);
        }
        return subagents.sort((a, b) => a.ageMs - b.ageMs);
      }
      return {
        getSystemStatus,
        getRecentActivity,
        getCapacity,
        getMemoryStats,
        getFullState,
        refreshState,
        startStateRefresh,
        stopStateRefresh,
        getData,
        getSubagentStatus
      };
    }
    module2.exports = { createStateModule: createStateModule2 };
  }
});

// src/index.js
var http = require("http");
var fs = require("fs");
var path = require("path");
var args = process.argv.slice(2);
var cliProfile = null;
var cliPort = null;
for (let i = 0; i < args.length; i++) {
  switch (args[i]) {
    case "--profile":
    case "-p":
      cliProfile = args[++i];
      break;
    case "--port":
      cliPort = parseInt(args[++i], 10);
      break;
    case "--help":
    case "-h":
      console.log(`
OpenClaw Command Center

Usage: node lib/server.js [options]

Options:
  --profile, -p <name>  OpenClaw profile (uses ~/.openclaw-<name>)
  --port <port>         Server port (default: 3333)
  --help, -h            Show this help

Environment:
  OPENCLAW_PROFILE      Same as --profile
  PORT                  Same as --port

Examples:
  node lib/server.js --profile production
  node lib/server.js -p dev --port 3334
`);
      process.exit(0);
  }
}
if (cliProfile) {
  process.env.OPENCLAW_PROFILE = cliProfile;
}
if (cliPort) {
  process.env.PORT = cliPort.toString();
}
var { getVersion } = require_utils();
var { CONFIG, getOpenClawDir } = require_config();
var { handleJobsRequest, isJobsRoute } = require_jobs();
var { runOpenClaw, runOpenClawAsync, extractJSON } = require_openclaw();
var { getSystemVitals, checkOptionalDeps, getOptionalDeps } = require_vitals();
var { checkAuth, getUnauthorizedPage } = require_auth();
var { loadPrivacySettings, savePrivacySettings } = require_privacy();
var {
  loadOperators,
  saveOperators,
  getOperatorBySlackId,
  startOperatorsRefresh
} = require_operators();
var { createSessionsModule } = require_sessions();
var { getCronJobs } = require_cron();
var { getCerebroTopics, updateTopicStatus } = require_cerebro();
var {
  getDailyTokenUsage,
  getTokenStats,
  getCostBreakdown,
  startTokenUsageRefresh,
  refreshTokenUsageAsync
} = require_tokens();
var { getLlmUsage, getRoutingStats, startLlmUsageRefresh } = require_llm_usage();
var { executeAction } = require_actions();
var { migrateDataDir } = require_data();
var { createStateModule } = require_state();
var PORT = CONFIG.server.port;
var DASHBOARD_DIR = path.join(__dirname, "../public");
var PATHS = CONFIG.paths;
var AUTH_CONFIG = {
  mode: CONFIG.auth.mode,
  [REDACTED_SECRET],
  allowedUsers: CONFIG.auth.allowedUsers,
  allowedIPs: CONFIG.auth.allowedIPs,
  publicPaths: CONFIG.auth.publicPaths
};
var DATA_DIR = path.join(getOpenClawDir(), "command-center", "data");
var LEGACY_DATA_DIR = path.join(DASHBOARD_DIR, "data");
var sseClients = /* @__PURE__ */ new Set();
function sendSSE(res, event, data) {
  try {
    res.write(`event: ${event}
data: ${JSON.stringify(data)}

`);
  } catch (e) {
  }
}
function broadcastSSE(event, data) {
  for (const client of sseClients) {
    sendSSE(client, event, data);
  }
}
var sessions = createSessionsModule({
  getOpenClawDir,
  getOperatorBySlackId: (slackId) => getOperatorBySlackId(DATA_DIR, slackId),
  runOpenClaw,
  runOpenClawAsync,
  extractJSON
});
var state = createStateModule({
  CONFIG,
  getOpenClawDir,
  getSessions: (opts) => sessions.getSessions(opts),
  getSystemVitals,
  getCronJobs: () => getCronJobs(getOpenClawDir),
  loadOperators: () => loadOperators(DATA_DIR),
  getLlmUsage: () => getLlmUsage(PATHS.state),
  getDailyTokenUsage: () => getDailyTokenUsage(getOpenClawDir),
  getTokenStats,
  getCerebroTopics: (opts) => getCerebroTopics(PATHS.cerebro, opts),
  runOpenClaw,
  extractJSON,
  readTranscript: (sessionId) => sessions.readTranscript(sessionId)
});
process.nextTick(() => migrateDataDir(DATA_DIR, LEGACY_DATA_DIR));
startOperatorsRefresh(DATA_DIR, getOpenClawDir);
startLlmUsageRefresh();
startTokenUsageRefresh(getOpenClawDir);
function serveStatic(req, res) {
  const requestUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
  const pathname = requestUrl.pathname === "/" ? "/index.html" : requestUrl.pathname;
  if (pathname.includes("..")) {
    res.writeHead(400);
    res.end("Bad request");
    return;
  }
  const normalizedPath = path.normalize(pathname).replace(/^[/\\]+/, "");
  const filePath = path.join(DASHBOARD_DIR, normalizedPath);
  const resolvedDashboardDir = path.resolve(DASHBOARD_DIR);
  const resolvedFilePath = path.resolve(filePath);
  if (!resolvedFilePath.startsWith(resolvedDashboardDir + path.sep) && resolvedFilePath !== resolvedDashboardDir) {
    res.writeHead(403);
    res.end("Forbidden");
    return;
  }
  const ext = path.extname(filePath);
  const contentTypes = {
    ".html": "text/html",
    ".css": "text/css",
    ".js": "text/javascript",
    ".json": "application/json",
    ".png": "image/png",
    ".svg": "image/svg+xml"
  };
  fs.readFile(filePath, (err, content) => {
    if (err) {
      res.writeHead(404);
      res.end("Not found");
      return;
    }
    const headers = { "Content-Type": contentTypes[ext] || "text/plain" };
    if ([".html", ".css", ".js", ".json"].includes(ext)) {
      headers["Cache-Control"] = "no-store";
    }
    res.writeHead(200, headers);
    res.end(content);
  });
}
function handleApi(req, res) {
  const sessionsList = sessions.getSessions();
  const capacity = state.getCapacity();
  const tokenStats = getTokenStats(sessionsList, capacity, CONFIG);
  const data = {
    sessions: sessionsList,
    cron: getCronJobs(getOpenClawDir),
    system: state.getSystemStatus(),
    activity: state.getRecentActivity(),
    tokenStats,
    capacity,
    timestamp: (/* @__PURE__ */ new Date()).toISOString()
  };
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify(data, null, 2));
}
var server = http.createServer((req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  const urlParts = req.url.split("?");
  const pathname = urlParts[0];
  const query = new URLSearchParams(urlParts[1] || "");
  if (pathname === "/api/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ status: "ok", port: PORT, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
    return;
  }
  const isPublicPath = AUTH_CONFIG.publicPaths.some(
    (p) => pathname === p || pathname.startsWith(p + "/")
  );
  if (!isPublicPath && AUTH_CONFIG.mode !== "none") {
    const authResult = checkAuth(req, AUTH_CONFIG);
    if (!authResult.authorized) {
      console.log(`[AUTH] Denied: ${authResult.reason} (path: ${pathname})`);
      res.writeHead(403, { "Content-Type": "text/html" });
      res.end(getUnauthorizedPage(authResult.reason, authResult.user, AUTH_CONFIG));
      return;
    }
    req.authUser = authResult.user;
    if (authResult.user?.login || authResult.user?.email) {
      console.log(
        `[AUTH] Allowed: ${authResult.user.login || authResult.user.email} (path: ${pathname})`
      );
    } else {
      console.log(`[AUTH] Allowed: ${req.socket?.remoteAddress} (path: ${pathname})`);
    }
  }
  if (pathname === "/api/status") {
    handleApi(req, res);
  } else if (pathname === "/api/session") {
    const sessionKey = query.get("key");
    if (!sessionKey) {
      res.writeHead(400, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "Missing session key" }));
      return;
    }
    const detail = sessions.getSessionDetail(sessionKey);
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(detail, null, 2));
  } else if (pathname === "/api/cerebro") {
    const offset = parseInt(query.get("offset") || "0", 10);
    const limit = parseInt(query.get("limit") || "20", 10);
    const statusFilter = query.get("status") || "all";
    const data = getCerebroTopics(PATHS.cerebro, { offset, limit, status: statusFilter });
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(data, null, 2));
  } else if (pathname.startsWith("/api/cerebro/topic/") && pathname.endsWith("/status") && req.method === "POST") {
    const topicId = decodeURIComponent(
      pathname.replace("/api/cerebro/topic/", "").replace("/status", "")
    );
    let body = "";
    req.on("data", (chunk) => {
      body += chunk;
    });
    req.on("end", () => {
      try {
        const { status: newStatus } = JSON.parse(body);
        if (!newStatus || !["active", "resolved", "parked"].includes(newStatus)) {
          res.writeHead(400, { "Content-Type": "application/json" });
          res.end(
            JSON.stringify({ error: "Invalid status. Must be: active, resolved, or parked" })
          );
          return;
        }
        const result = updateTopicStatus(PATHS.cerebro, topicId, newStatus);
        if (result.error) {
          res.writeHead(result.code || 500, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ error: result.error }));
          return;
        }
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify(result, null, 2));
      } catch (e) {
        res.writeHead(400, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: "Invalid JSON body" }));
      }
    });
    return;
  } else if (pathname === "/api/llm-quota") {
    const data = getLlmUsage(PATHS.state);
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(data, null, 2));
  } else if (pathname === "/api/cost-breakdown") {
    const data = getCostBreakdown(CONFIG, (opts) => sessions.getSessions(opts), getOpenClawDir);
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(data, null, 2));
  } else if (pathname === "/api/subagents") {
    const data = state.getSubagentStatus();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ subagents: data }, null, 2));
  } else if (pathname === "/api/action") {
    const action = query.get("action");
    if (!action) {
      res.writeHead(400, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "Missing action parameter" }));
      return;
    }
    const result = executeAction(action, { runOpenClaw, extractJSON, PORT });
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(result, null, 2));
  } else if (pathname === "/api/events") {
    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no"
    });
    sseClients.add(res);
    console.log(`[SSE] Client connected (total: ${sseClients.size})`);
    sendSSE(res, "connected", { message: "Connected to Command Center", timestamp: Date.now() });
    const cachedState = state.getFullState();
    if (cachedState) {
      sendSSE(res, "update", cachedState);
    } else {
      sendSSE(res, "update", { sessions: [], loading: true });
    }
    req.on("close", () => {
      sseClients.delete(res);
      console.log(`[SSE] Client disconnected (total: ${sseClients.size})`);
    });
    return;
  } else if (pathname === "/api/whoami") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(
      JSON.stringify(
        {
          authMode: AUTH_CONFIG.mode,
          user: req.authUser || null
        },
        null,
        2
      )
    );
  } else if (pathname === "/api/about") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(
      JSON.stringify(
        {
          name: "OpenClaw Command Center",
          version: getVersion(),
          description: "A Starcraft-inspired dashboard for AI agent orchestration",
          license: "MIT",
          repository: "https://github.com/jontsai/openclaw-command-center",
          builtWith: ["OpenClaw", "Node.js", "Vanilla JS"],
          inspirations: ["Starcraft", "Inside Out", "iStatMenus", "DaisyDisk", "Gmail"]
        },
        null,
        2
      )
    );
  } else if (pathname === "/api/state") {
    const fullState = state.getFullState();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(fullState, null, 2));
  } else if (pathname === "/api/vitals") {
    const vitals = getSystemVitals();
    const optionalDeps = getOptionalDeps();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ vitals, optionalDeps }, null, 2));
  } else if (pathname === "/api/capacity") {
    const capacity = state.getCapacity();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(capacity, null, 2));
  } else if (pathname === "/api/sessions") {
    const page = parseInt(query.get("page")) || 1;
    const pageSize = parseInt(query.get("pageSize")) || 20;
    const statusFilter = query.get("status");
    const allSessions = sessions.getSessions({ limit: null });
    const statusCounts = {
      all: allSessions.length,
      live: allSessions.filter((s) => s.active).length,
      recent: allSessions.filter((s) => !s.active && s.recentlyActive).length,
      idle: allSessions.filter((s) => !s.active && !s.recentlyActive).length
    };
    let filteredSessions = allSessions;
    if (statusFilter === "live") {
      filteredSessions = allSessions.filter((s) => s.active);
    } else if (statusFilter === "recent") {
      filteredSessions = allSessions.filter((s) => !s.active && s.recentlyActive);
    } else if (statusFilter === "idle") {
      filteredSessions = allSessions.filter((s) => !s.active && !s.recentlyActive);
    }
    const total = filteredSessions.length;
    const totalPages = Math.ceil(total / pageSize);
    const offset = (page - 1) * pageSize;
    const displaySessions = filteredSessions.slice(offset, offset + pageSize);
    const tokenStats = getTokenStats(allSessions, state.getCapacity(), CONFIG);
    const capacity = state.getCapacity();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(
      JSON.stringify(
        {
          sessions: displaySessions,
          pagination: {
            page,
            pageSize,
            total,
            totalPages,
            hasPrev: page > 1,
            hasNext: page < totalPages
          },
          statusCounts,
          tokenStats,
          capacity
        },
        null,
        2
      )
    );
  } else if (pathname === "/api/cron") {
    const cron = getCronJobs(getOpenClawDir);
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ cron }, null, 2));
  } else if (pathname === "/api/operators") {
    const method = req.method;
    const data = loadOperators(DATA_DIR);
    if (method === "GET") {
      const allSessions = sessions.getSessions({ limit: null });
      const operatorsWithStats = data.operators.map((op) => {
        const userSessions = allSessions.filter(
          (s) => s.originator?.userId === op.id || s.originator?.userId === op.metadata?.slackId
        );
        return {
          ...op,
          stats: {
            activeSessions: userSessions.filter((s) => s.active).length,
            totalSessions: userSessions.length,
            lastSeen: userSessions.length > 0 ? new Date(
              Date.now() - Math.min(...userSessions.map((s) => s.minutesAgo)) * 6e4
            ).toISOString() : op.lastSeen
          }
        };
      });
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(
        JSON.stringify(
          {
            operators: operatorsWithStats,
            roles: data.roles,
            timestamp: Date.now()
          },
          null,
          2
        )
      );
    } else if (method === "POST") {
      let body = "";
      req.on("data", (chunk) => body += chunk);
      req.on("end", () => {
        try {
          const newOp = JSON.parse(body);
          const existingIdx = data.operators.findIndex((op) => op.id === newOp.id);
          if (existingIdx >= 0) {
            data.operators[existingIdx] = { ...data.operators[existingIdx], ...newOp };
          } else {
            data.operators.push({
              ...newOp,
              createdAt: (/* @__PURE__ */ new Date()).toISOString()
            });
          }
          if (saveOperators(DATA_DIR, data)) {
            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ success: true, operator: newOp }));
          } else {
            res.writeHead(500, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ error: "Failed to save" }));
          }
        } catch (e) {
          res.writeHead(400, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ error: "Invalid JSON" }));
        }
      });
      return;
    } else {
      res.writeHead(405, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "Method not allowed" }));
    }
    return;
  } else if (pathname === "/api/llm-usage") {
    const usage = getLlmUsage(PATHS.state);
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(usage, null, 2));
  } else if (pathname === "/api/routing-stats") {
    const hours = parseInt(query.get("hours") || "24", 10);
    const stats = getRoutingStats(PATHS.skills, PATHS.state, hours);
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(stats, null, 2));
  } else if (pathname === "/api/memory") {
    const memory = state.getMemoryStats();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ memory }, null, 2));
  } else if (pathname === "/api/privacy") {
    if (req.method === "GET") {
      const settings = loadPrivacySettings(DATA_DIR);
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify(settings, null, 2));
    } else if (req.method === "POST" || req.method === "PUT") {
      let body = "";
      req.on("data", (chunk) => body += chunk);
      req.on("end", () => {
        try {
          const updates = JSON.parse(body);
          const current = loadPrivacySettings(DATA_DIR);
          const merged = {
            version: current.version || 1,
            hiddenTopics: updates.hiddenTopics ?? current.hiddenTopics ?? [],
            hiddenSessions: updates.hiddenSessions ?? current.hiddenSessions ?? [],
            hiddenCrons: updates.hiddenCrons ?? current.hiddenCrons ?? [],
            hideHostname: updates.hideHostname ?? current.hideHostname ?? false
          };
          if (savePrivacySettings(DATA_DIR, merged)) {
            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ success: true, settings: merged }));
          } else {
            res.writeHead(500, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ error: "Failed to save privacy settings" }));
          }
        } catch (e) {
          res.writeHead(400, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ error: "Invalid JSON: " + e.message }));
        }
      });
      return;
    } else {
      res.writeHead(405, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "Method not allowed" }));
    }
    return;
  } else if (isJobsRoute(pathname)) {
    handleJobsRequest(req, res, pathname, query, req.method);
  } else {
    serveStatic(req, res);
  }
});
server.listen(PORT, () => {
  const profile = process.env.OPENCLAW_PROFILE;
  console.log(`\u{1F99E} OpenClaw Command Center running at http://localhost:${PORT}`);
  if (profile) {
    console.log(`   Profile: ${profile} (~/.openclaw-${profile})`);
  }
  console.log(`   Press Ctrl+C to stop`);
  setTimeout(async () => {
    console.log("[Startup] Pre-warming caches in background...");
    try {
      await Promise.all([sessions.refreshSessionsCache(), refreshTokenUsageAsync(getOpenClawDir)]);
      getSystemVitals();
      console.log("[Startup] Caches warmed.");
    } catch (e) {
      console.log("[Startup] Cache warming error:", e.message);
    }
    checkOptionalDeps();
  }, 100);
  const SESSIONS_CACHE_TTL = 1e4;
  setInterval(() => sessions.refreshSessionsCache(), SESSIONS_CACHE_TTL);
});
var sseRefreshing = false;
setInterval(() => {
  if (sseClients.size > 0 && !sseRefreshing) {
    sseRefreshing = true;
    try {
      const fullState = state.refreshState();
      broadcastSSE("update", fullState);
      broadcastSSE("heartbeat", { clients: sseClients.size, timestamp: Date.now() });
    } catch (e) {
      console.error("[SSE] Broadcast error:", e.message);
    }
    sseRefreshing = false;
  }
}, 15e3);


path: package.json
content: {
  "name": "openclaw-command-center",
  "version": "1.4.1",
  "description": "🦞 AI agent command and control dashboard — Spawn more Overlords!",
  "main": "lib/server.js",
  "type": "commonjs",
  "scripts": {
    "start": "node lib/server.js",
    "dev": "node --watch lib/server.js",
    "dev:src": "esbuild src/index.js --bundle --platform=node --outfile=lib/server.js --watch",
    "build": "esbuild src/index.js --bundle --platform=node --outfile=lib/server.js --banner:js='#!/usr/bin/env node' && prettier --write lib/server.js",
    "test": "node --test",
    "test:coverage": "node --test --experimental-test-coverage",
    "lint": "ESLINT_USE_FLAT_CONFIG=true eslint src/ tests/",
    "lint:fix": "ESLINT_USE_FLAT_CONFIG=true eslint src/ tests/ --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "prepare": "echo 'Ready to spawn Overlords 🦞'"
  },
  "keywords": [
    "ai",
    "agent",
    "dashboard",
    "monitoring",
    "claude",
    "chatgpt",
    "llm",
    "command-center",
    "openclaw"
  ],
  "author": "OpenClaw Contributors",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/jontsai/openclaw-command-center.git"
  },
  "bugs": {
    "url": "https://github.com/jontsai/openclaw-command-center/issues"
  },
  "homepage": "https://github.com/jontsai/openclaw-command-center#readme",
  "engines": {
    "node": ">=18.0.0"
  },
  "devDependencies": {
    "esbuild": "^0.27.3",
    "eslint": "^8.56.0",
    "prettier": "^3.2.4"
  }
}


path: public/data/AGENTS.md
content: # public/data/ — User-Specific Data

**DO NOT COMMIT** the following files — they contain user-specific data:

| File                    | Purpose                          | Template                        |
| ----------------------- | -------------------------------- | ------------------------------- |
| `operators.json`        | User/operator info from sessions | `operators.json.example`        |
| `privacy-settings.json` | Hidden topics/sessions for demos | `privacy-settings.json.example` |

## Why?

These files are generated at runtime and contain:

- User IDs and usernames
- Session counts and activity
- Privacy preferences (what the user hides)

Committing them would leak user data to the public repo.

## For New Installations

Copy the `.example` files to get started:

```bash
cp operators.json.example operators.json
cp privacy-settings.json.example privacy-settings.json
```

The dashboard will populate these automatically on first run.


path: public/favicon.svg
content: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <text y=".9em" font-size="90">🦞</text>
</svg>


path: public/js/api.js
content: /**
 * API and SSE connection management for Command Center
 */

// SSE connection state
let eventSource = null;
let sseConnected = false;
let sseReconnectAttempts = 0;
let pollInterval = null;

const SSE_MAX_RECONNECT_DELAY = 30000;

/**
 * Fetch the unified state from the server
 * @returns {Promise<Object>} Dashboard state
 */
export async function fetchState() {
  const response = await fetch("/api/state");
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

/**
 * Connect to SSE for real-time updates
 * @param {Function} onUpdate - Callback when state updates
 * @param {Function} onStatusChange - Callback for connection status changes
 */
export function connectSSE(onUpdate, onStatusChange) {
  if (typeof EventSource === "undefined") {
    console.warn("[SSE] EventSource not supported, using polling fallback");
    onStatusChange?.("polling", "Polling Mode");
    startPolling(onUpdate);
    return;
  }

  onStatusChange?.("connecting", "Connecting...");

  try {
    eventSource = new EventSource("/api/events");

    eventSource.onopen = function () {
      console.log("[SSE] Connected");
      sseConnected = true;
      sseReconnectAttempts = 0;
      onStatusChange?.("connected", "🟢 Live");
      stopPolling();
    };

    eventSource.addEventListener("connected", function (e) {
      try {
        const data = JSON.parse(e.data);
        console.log("[SSE] Server greeting:", data.message);
      } catch (err) {}
    });

    eventSource.addEventListener("update", function (e) {
      try {
        const data = JSON.parse(e.data);
        onUpdate?.(data);
      } catch (err) {
        console.error("[SSE] Failed to parse update:", err);
      }
    });

    eventSource.addEventListener("heartbeat", function (e) {
      try {
        const data = JSON.parse(e.data);
        console.log("[SSE] Heartbeat, clients:", data.clients);
      } catch (err) {}
    });

    eventSource.onerror = function (e) {
      console.error("[SSE] Connection error");
      sseConnected = false;
      eventSource.close();
      eventSource = null;
      onStatusChange?.("disconnected", "🔴 Disconnected");

      // Exponential backoff for reconnection
      sseReconnectAttempts++;
      const delay = Math.min(1000 * Math.pow(2, sseReconnectAttempts - 1), SSE_MAX_RECONNECT_DELAY);
      console.log(`[SSE] Reconnecting in ${delay}ms (attempt ${sseReconnectAttempts})`);

      // Start polling as fallback while disconnected
      startPolling(onUpdate);

      setTimeout(() => connectSSE(onUpdate, onStatusChange), delay);
    };
  } catch (err) {
    console.error("[SSE] Failed to create EventSource:", err);
    onStatusChange?.("disconnected", "🔴 Error");
    startPolling(onUpdate);
  }
}

function startPolling(onUpdate) {
  if (pollInterval) return;
  console.log("[Polling] Starting fallback polling");
  pollInterval = setInterval(async () => {
    try {
      const state = await fetchState();
      onUpdate?.(state);
    } catch (err) {
      console.error("[Polling] Failed:", err);
    }
  }, 2000);
}

function stopPolling() {
  if (pollInterval) {
    console.log("[Polling] Stopping fallback polling (SSE connected)");
    clearInterval(pollInterval);
    pollInterval = null;
  }
}

export function isConnected() {
  return sseConnected;
}


path: public/js/app.js
content: /**
 * OpenClaw Command Center - Main Application
 *
 * Uses morphdom for efficient DOM updates (only patches what changed).
 */

// Import morphdom (loaded as UMD, available as global `morphdom`)
// <script src="/js/lib/morphdom.min.js"></script> must be loaded first

// ============================================================================
// STATE MANAGEMENT
// ============================================================================

const state = {
  vitals: null,
  sessions: [],
  tokenStats: {},
  statusCounts: { all: 0, live: 0, recent: 0, idle: 0 },
  capacity: { main: { active: 0, max: 12 }, subagent: { active: 0, max: 24 } },
  operators: { operators: [], roles: {} },
  llmUsage: null,
  cron: [],
  memory: null,
  cerebro: null,
  subagents: [],
  lastUpdated: null,
  connected: false,
};

// ============================================================================
// SSE CONNECTION
// ============================================================================

let eventSource = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_DELAY = 30000;

function connectSSE() {
  if (typeof EventSource === "undefined") {
    console.warn("[SSE] Not supported, falling back to polling");
    startPolling();
    return;
  }

  updateConnectionStatus("connecting");

  eventSource = new EventSource("/api/events");

  eventSource.onopen = () => {
    console.log("[SSE] Connected");
    state.connected = true;
    reconnectAttempts = 0;
    updateConnectionStatus("connected");
  };

  eventSource.addEventListener("connected", (e) => {
    const data = JSON.parse(e.data);
    console.log("[SSE] Server greeting:", data.message);
  });

  eventSource.addEventListener("update", (e) => {
    const data = JSON.parse(e.data);
    handleStateUpdate(data);
  });

  eventSource.addEventListener("heartbeat", (e) => {
    const data = JSON.parse(e.data);
    state.lastUpdated = new Date();
    updateTimestamp();
  });

  eventSource.onerror = () => {
    console.error("[SSE] Connection error");
    state.connected = false;
    eventSource.close();
    updateConnectionStatus("disconnected");

    // Exponential backoff
    reconnectAttempts++;
    const delay = Math.min(1000 * Math.pow(2, reconnectAttempts - 1), MAX_RECONNECT_DELAY);
    console.log(`[SSE] Reconnecting in ${delay}ms`);
    setTimeout(connectSSE, delay);
  };
}

// ============================================================================
// STATE UPDATES
// ============================================================================

function handleStateUpdate(data) {
  // Merge new data into state
  if (data.vitals) state.vitals = data.vitals;
  if (data.sessions) state.sessions = data.sessions;
  if (data.tokenStats) state.tokenStats = data.tokenStats;
  if (data.statusCounts) state.statusCounts = data.statusCounts;
  if (data.capacity) state.capacity = data.capacity;
  if (data.operators) state.operators = data.operators;
  if (data.llmUsage) state.llmUsage = data.llmUsage;
  if (data.cron) state.cron = data.cron;
  if (data.memory) state.memory = data.memory;
  if (data.cerebro) state.cerebro = data.cerebro;
  if (data.subagents) state.subagents = data.subagents;

  state.lastUpdated = new Date();

  // Re-render affected components using morphdom
  renderAll();
}

// ============================================================================
// RENDERING (with morphdom)
// ============================================================================

function renderAll() {
  // Each render function generates HTML and morphdom patches the DOM
  renderVitals();
  renderTokenStats();
  renderLlmUsage();
  renderSessions();
  renderCron();
  renderMemory();
  renderCerebro();
  renderOperators();
  updateTimestamp();
}

// Utility: safely patch a container using morphdom
function patchElement(containerId, newHtml) {
  const container = document.getElementById(containerId);
  if (!container) return;

  // Create a temporary element with the new content
  const temp = document.createElement("div");
  temp.innerHTML = newHtml;

  // Use morphdom to efficiently patch only what changed
  if (typeof morphdom !== "undefined") {
    // Patch each child
    while (container.firstChild && temp.firstChild) {
      morphdom(container.firstChild, temp.firstChild);
      temp.removeChild(temp.firstChild);
    }
    // Add any new children
    while (temp.firstChild) {
      container.appendChild(temp.firstChild);
    }
    // Remove extra children
    while (container.childNodes.length > temp.childNodes.length) {
      container.removeChild(container.lastChild);
    }
  } else {
    // Fallback: direct innerHTML replacement
    container.innerHTML = newHtml;
  }
}

// ============================================================================
// COMPONENT RENDERERS (to be extracted to separate files)
// ============================================================================

function renderVitals() {
  if (!state.vitals) return;
  const v = state.vitals;

  // Update individual elements (simpler than full morphdom for now)
  setText("vitals-hostname", v.hostname || "-");
  setText("vitals-uptime", v.uptime || "-");

  if (v.cpu) {
    const cpuPct = v.cpu.usage || 0;
    setText("cpu-percent", cpuPct + "%");
    setWidth("cpu-bar", cpuPct + "%");
    setText("cpu-user", (v.cpu.userPercent?.toFixed(1) || "-") + "%");
    setText("cpu-sys", (v.cpu.sysPercent?.toFixed(1) || "-") + "%");
    setText("cpu-idle", (v.cpu.idlePercent?.toFixed(1) || "-") + "%");
    setText("cpu-chip", v.cpu.chip || v.cpu.brand || "");
  }

  if (v.memory) {
    const memPct = v.memory.percent || 0;
    setText("mem-percent", memPct + "% used");
    setWidth("mem-bar", memPct + "%");
    setText("mem-summary", `${v.memory.usedFormatted || "-"} of ${v.memory.totalFormatted || "-"}`);
  }

  if (v.disk) {
    const diskPct = v.disk.percent || 0;
    setText("disk-percent", diskPct + "% used");
    setWidth("disk-bar", diskPct + "%");
    setText("disk-summary", `${v.disk.usedFormatted || "-"} of ${v.disk.totalFormatted || "-"}`);
  }
}

function renderTokenStats() {
  if (!state.tokenStats) return;
  const t = state.tokenStats;

  setText("stat-total-tokens", t.total || "-");
  setText("stat-input", t.input || "-");
  setText("stat-output", t.output || "-");
  setText("stat-active", t.activeCount || "0");
  setText("stat-cost", t.estCost || "-");
  setText("stat-main", `${t.activeMainCount || 0}/${t.mainLimit || 12}`);
  setText("stat-subagents", `${t.activeSubagentCount || 0}/${t.subagentLimit || 24}`);
}

function renderLlmUsage() {
  // Placeholder - will be extracted to component
}

function renderSessions() {
  // Placeholder - will be extracted to component
}

function renderCron() {
  // Placeholder - will be extracted to component
}

function renderMemory() {
  // Placeholder - will be extracted to component
}

function renderCerebro() {
  // Placeholder - will be extracted to component
}

function renderOperators() {
  // Placeholder - will be extracted to component
}

// ============================================================================
// UTILITIES
// ============================================================================

function setText(id, text) {
  const el = document.getElementById(id);
  if (el && el.textContent !== text) {
    el.textContent = text;
  }
}

function setWidth(id, width) {
  const el = document.getElementById(id);
  if (el && el.style.width !== width) {
    el.style.width = width;
  }
}

function updateTimestamp() {
  const now = state.lastUpdated || new Date();
  const timeStr = now.toLocaleTimeString();
  const indicator = state.connected ? " ⚡" : "";
  setText("last-updated", timeStr + indicator);
  setText("sidebar-updated", state.connected ? `Live: ${timeStr}` : `Updated: ${timeStr}`);
}

function updateConnectionStatus(status) {
  const el = document.getElementById("connection-status");
  if (!el) return;

  el.className = "connection-status " + status;
  el.textContent =
    status === "connected"
      ? "🟢 Live"
      : status === "connecting"
        ? "🟡 Connecting..."
        : "🔴 Disconnected";
}

// ============================================================================
// POLLING FALLBACK
// ============================================================================

let pollInterval = null;

function startPolling() {
  if (pollInterval) return;
  pollInterval = setInterval(fetchState, 5000);
  fetchState();
}

async function fetchState() {
  try {
    const response = await fetch("/api/state");
    const data = await response.json();
    handleStateUpdate(data);
  } catch (e) {
    console.error("[Polling] Failed:", e);
  }
}

// ============================================================================
// INITIALIZATION
// ============================================================================

function init() {
  console.log("[App] Initializing OpenClaw Command Center");
  connectSSE();

  // Initial fetch to populate immediately
  setTimeout(fetchState, 100);
}

// Start when DOM is ready
if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  setTimeout(init, 0);
}


path: scripts/run-server.sh
content: #!/bin/bash
# Wrapper script to ensure PATH includes system directories
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"

# Find node - prefer nvm if available
if [ -f "$HOME/.nvm/nvm.sh" ]; then
    source "$HOME/.nvm/nvm.sh"
fi

cd "$(dirname "$0")/.."
exec node lib/server.js


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)