Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61cc37ba32 | |||
| e972267a42 |
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-code-agent-monitor-plugins",
|
||||
"name": "agenthub",
|
||||
"description": "Official plugin marketplace for AgentHub — the `agenthub` plugin installs the dashboard itself (hooks, server, CLI), plus 10 focused plugins for analytics, cost guardrails, productivity, developer tools, AI insights, session forensics, workflow/fleet intelligence, reliability/SLOs, config & memory governance, and dashboard connectivity. Every plugin is powered by the local Agent Monitor API.",
|
||||
"owner": {
|
||||
"name": "smartgift",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "agenthub",
|
||||
@@ -16,61 +16,61 @@
|
||||
},
|
||||
{
|
||||
"name": "agenthub-analytics",
|
||||
"path": "plugins/agenthub-analytics",
|
||||
"source": "./plugins/agenthub-analytics",
|
||||
"description": "Deep analytics on Claude Code sessions — token usage (total_input/total_output/total_cache_read/total_cache_write with compaction baselines pre-summed), cost breakdowns via the pricing engine, usage trends over 365 days, cache efficiency, model mix, and productivity scoring.",
|
||||
"tags": ["analytics", "tokens", "cost", "productivity"]
|
||||
},
|
||||
{
|
||||
"name": "agenthub-cost-guard",
|
||||
"path": "plugins/agenthub-cost-guard",
|
||||
"source": "./plugins/agenthub-cost-guard",
|
||||
"description": "Budget guardrails — set spend limits, forecast week/month-end cost from daily trends, surface cost-threshold alerts via the alert-rule API, and quantify model-routing savings.",
|
||||
"tags": ["budget", "cost", "forecast", "alerts"]
|
||||
},
|
||||
{
|
||||
"name": "agenthub-productivity",
|
||||
"path": "plugins/agenthub-productivity",
|
||||
"source": "./plugins/agenthub-productivity",
|
||||
"description": "Productivity automation — daily standups, weekly/monthly reviews, sprint summaries, time-of-day focus analysis, and workflow optimization using the workflow intelligence API's 11 datasets.",
|
||||
"tags": ["productivity", "reports", "workflows", "standup"]
|
||||
},
|
||||
{
|
||||
"name": "agenthub-devtools",
|
||||
"path": "plugins/agenthub-devtools",
|
||||
"source": "./plugins/agenthub-devtools",
|
||||
"description": "Developer tools for the Agent Monitor — session debugging with full event-chain inspection, data-integrity inspection, event tracing, transcript search, hook diagnostics, data export (JSON/CSV/Markdown), and system health checks.",
|
||||
"tags": ["devtools", "debug", "diagnostics", "export"]
|
||||
},
|
||||
{
|
||||
"name": "agenthub-insights",
|
||||
"path": "plugins/agenthub-insights",
|
||||
"source": "./plugins/agenthub-insights",
|
||||
"description": "AI-powered insights — pattern detection via workflow intelligence (tool flow transitions, agent co-occurrence), anomaly alerting, trend forecasting, regression watch, benchmarking, optimization recommendations, and session comparison.",
|
||||
"tags": ["insights", "patterns", "anomaly", "optimization"]
|
||||
},
|
||||
{
|
||||
"name": "agenthub-sessions",
|
||||
"path": "plugins/agenthub-sessions",
|
||||
"source": "./plugins/agenthub-sessions",
|
||||
"description": "Session forensics — search by project/model/status, reconstruct event timelines, replay transcripts turn-by-turn, roll up activity per working directory, and review stale sessions before cleanup.",
|
||||
"tags": ["sessions", "search", "transcript", "replay"]
|
||||
},
|
||||
{
|
||||
"name": "agenthub-workflows",
|
||||
"path": "plugins/agenthub-workflows",
|
||||
"source": "./plugins/agenthub-workflows",
|
||||
"description": "Multi-agent orchestration & fleet intelligence — map the subagent DAG, audit model delegation and effectiveness, report concurrency lanes, trace error propagation by depth, and review Workflow-tool fleet runs.",
|
||||
"tags": ["workflows", "orchestration", "subagents", "fleet"]
|
||||
},
|
||||
{
|
||||
"name": "agenthub-quality",
|
||||
"path": "plugins/agenthub-quality",
|
||||
"source": "./plugins/agenthub-quality",
|
||||
"description": "Reliability & SLOs — scan for API errors and tool failures, audit hook delivery health (PreToolUse/PostToolUse balance), track completion/error-rate SLOs with error budgets, and alert on reliability regressions.",
|
||||
"tags": ["reliability", "errors", "slo", "quality"]
|
||||
},
|
||||
{
|
||||
"name": "agenthub-config",
|
||||
"path": "plugins/agenthub-config",
|
||||
"source": "./plugins/agenthub-config",
|
||||
"description": "Claude Code config & memory governance — audit skills/subagents/commands/MCP servers/hooks/settings via the Config Explorer API and curate the file-based memory store (CLAUDE.md + per-project auto-memory files).",
|
||||
"tags": ["config", "memory", "governance", "mcp"]
|
||||
},
|
||||
{
|
||||
"name": "agenthub-dashboard",
|
||||
"path": "plugins/agenthub-dashboard",
|
||||
"source": "./plugins/agenthub-dashboard",
|
||||
"description": "Dashboard connector — quick status checks, one-line metrics, live activity watch, and endpoint probing against the Agent Monitor API.",
|
||||
"tags": ["dashboard", "status", "connector"]
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git",
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": ["dashboard", "monitoring", "sessions", "cost", "lanes", "claude-code"],
|
||||
"commands": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Optional dev-container image for AgentHub.
|
||||
# Node 22 to match the production Dockerfile and the project's engines.
|
||||
# Node 22 to match the project's engines.
|
||||
# Adds the native-addon toolchain (better-sqlite3 builds via node-gyp) plus
|
||||
# Python 3 (statusline.py and helper scripts) and the sqlite3 CLI.
|
||||
# Python 3 (scripts/expand-ts-module-docs.py) and the sqlite3 CLI.
|
||||
#
|
||||
# This image is ONLY used by Dev Containers / Codespaces. Host-based development
|
||||
# (npm run dev / npm start) is unaffected.
|
||||
|
||||
@@ -22,13 +22,13 @@ dependencies (`npm run setup`).
|
||||
|
||||
| Component | Detail |
|
||||
| ---------------- | ------------------------------------------------------------------- |
|
||||
| Base image | `mcr.microsoft.com/devcontainers/javascript-node:22` (matches prod) |
|
||||
| Base image | `mcr.microsoft.com/devcontainers/javascript-node:22` |
|
||||
| Native toolchain | `build-essential` + `python3` so `better-sqlite3` compiles |
|
||||
| Python | `python3` / `python` for `statusline.py` and helper scripts |
|
||||
| Python | `python3` / `python` for `scripts/expand-ts-module-docs.py` |
|
||||
| sqlite3 CLI | inspect the dashboard DB during development |
|
||||
| Features | GitHub CLI, Docker-in-Docker (build/run the project's own Dockerfile) |
|
||||
| Features | GitHub CLI |
|
||||
| Forwarded ports | `4820` (server API + WebSocket), `5173` (Vite client) |
|
||||
| Editor | ESLint + Prettier (format on save), Vitest, Docker, YAML, Tailwind |
|
||||
| Editor | ESLint + Prettier (format on save), Vitest, YAML, Tailwind |
|
||||
|
||||
## Everyday commands
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
// Used only when you choose "Dev Containers: Reopen in Container" (VS Code) or
|
||||
// open the repo in a GitHub Codespace. It changes nothing for host-based dev.
|
||||
//
|
||||
// Covers the full project: the Express server (4820), the React/Vite client
|
||||
// (5173) and the VS Code extension. The native `better-sqlite3`
|
||||
// addon builds here (build-essential + python3 are installed in the Dockerfile).
|
||||
// Covers the full project: the Express server (4820) and the React/Vite
|
||||
// client (5173). The native `better-sqlite3` addon builds here
|
||||
// (build-essential + python3 are installed in the Dockerfile).
|
||||
//
|
||||
// NOTE (issue #193): Claude Code hooks are a HOST-side concern. This container
|
||||
// intentionally does NOT bind-mount ~/.claude and does NOT install hooks — the
|
||||
@@ -20,10 +20,7 @@
|
||||
},
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
// Lets you build/run the project's own production Dockerfile + docker-compose
|
||||
// from inside the dev container (e.g. to reproduce issue #193 deliberately).
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
|
||||
// Server (API + WebSocket) and the Vite client dev server. VS Code forwards
|
||||
@@ -40,7 +37,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
// Install root + client + vscode-extension deps.
|
||||
// Install root + client deps.
|
||||
// Never installs Claude Code hooks (host-only — see note above).
|
||||
"postCreateCommand": "bash .devcontainer/post-create.sh",
|
||||
"waitFor": "postCreateCommand",
|
||||
@@ -59,7 +56,6 @@
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"vitest.explorer",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"redhat.vscode-yaml",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"ms-python.python",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
set -euo pipefail
|
||||
|
||||
echo "▶ Installing server + client + vscode-extension dependencies (npm run setup)…"
|
||||
echo "▶ Installing server + client dependencies (npm run setup)…"
|
||||
npm run setup
|
||||
|
||||
cat <<'EOF'
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
node_modules/
|
||||
client/node_modules/
|
||||
client/dist/
|
||||
data/
|
||||
.git/
|
||||
.github/
|
||||
*.md
|
||||
!README.md
|
||||
images/
|
||||
wiki/
|
||||
index.html
|
||||
og-image.svg
|
||||
favicon.svg
|
||||
.prettierrc
|
||||
.prettierignore
|
||||
@@ -13,13 +13,3 @@
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.db binary
|
||||
|
||||
# ── GitHub Linguist overrides — keep language stats honest ──────────────────
|
||||
# wiki/mermaid.min.js is a vendored, minified third-party library (Mermaid);
|
||||
# it is not project source code and must not count toward language stats.
|
||||
wiki/mermaid.min.js linguist-vendored
|
||||
|
||||
# wiki/i18n-content.js is machine-assembled translation data (see its header:
|
||||
# "AUTO-GENERATED wiki body-content translations ... Do not hand-edit").
|
||||
# It is a data bundle keyed by English innerHTML, not hand-written JavaScript.
|
||||
wiki/i18n-content.js linguist-generated
|
||||
|
||||
@@ -9,17 +9,5 @@ package-lock.json
|
||||
client/package-lock.json
|
||||
.vs/
|
||||
*.md
|
||||
index.html
|
||||
wiki/index.html
|
||||
*.yml
|
||||
*.yaml
|
||||
desktop/out/
|
||||
desktop/release/
|
||||
desktop/node_modules/
|
||||
desktop/package-lock.json
|
||||
desktop/assets/*.svg
|
||||
desktop/assets/*.png
|
||||
desktop/assets/*.icns
|
||||
desktop/assets/icon.iconset/
|
||||
wiki/mermaid.min.js
|
||||
fonts/*.woff2
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
# Superpowers Workspace
|
||||
|
||||
This directory contains project-specific configuration and artifacts for the Superpowers workflow.
|
||||
|
||||
It acts as the working memory layer for agent-driven development, enabling structured planning, execution, and iteration across sessions.
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Superpowers transforms coding agents into structured, process-driven collaborators. Instead of jumping straight into code, the agent:
|
||||
|
||||
1. Clarifies intent
|
||||
2. Produces a design
|
||||
3. Breaks work into executable steps
|
||||
4. Executes via subagents
|
||||
5. Verifies and iterates
|
||||
|
||||
This directory stores the outputs of that workflow so progress is persistent, inspectable, and reproducible.
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Typical contents may include:
|
||||
|
||||
```
|
||||
.superpowers/
|
||||
├── brainstorm/ # Design explorations and refined specs
|
||||
├── plans/ # Task breakdowns and execution plans
|
||||
├── reviews/ # Code review outputs and feedback
|
||||
├── runs/ # Execution logs or agent traces
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
> Exact structure may evolve depending on which skills are triggered.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
Superpowers operates through automatic skill activation:
|
||||
|
||||
### 1. Brainstorming
|
||||
|
||||
* Refines vague ideas into concrete specs
|
||||
* Explores alternatives
|
||||
* Produces structured, reviewable design docs
|
||||
|
||||
### 2. Planning
|
||||
|
||||
* Converts approved designs into granular tasks
|
||||
* Each task is:
|
||||
|
||||
* Small (2–5 min)
|
||||
* Explicit (exact file paths + code)
|
||||
* Verifiable
|
||||
|
||||
### 3. Execution
|
||||
|
||||
* Tasks are executed by subagents
|
||||
* Includes:
|
||||
|
||||
* Spec compliance checks
|
||||
* Code quality review
|
||||
* Can run sequentially or in parallel
|
||||
|
||||
### 4. Verification
|
||||
|
||||
* Enforces test-first development (TDD)
|
||||
* Ensures correctness before completion
|
||||
* Prevents silent regressions
|
||||
|
||||
### 5. Completion
|
||||
|
||||
* Validates final state
|
||||
* Offers merge / PR / discard options
|
||||
* Cleans up working branches
|
||||
|
||||
---
|
||||
|
||||
## Key Principles
|
||||
|
||||
* **Test-first development** (RED → GREEN → REFACTOR)
|
||||
* **Small, deterministic tasks**
|
||||
* **Explicit over implicit**
|
||||
* **Process over intuition**
|
||||
* **Verification over assumption**
|
||||
|
||||
---
|
||||
|
||||
## How to Use
|
||||
|
||||
You don’t interact with this directory directly most of the time.
|
||||
|
||||
Instead:
|
||||
|
||||
* Start a task in your coding agent (Claude, Cursor, etc.)
|
||||
* Let Superpowers skills activate automatically
|
||||
* Review outputs when prompted (designs, plans, reviews)
|
||||
|
||||
Artifacts will be written here as the workflow progresses.
|
||||
|
||||
---
|
||||
|
||||
## When to Look Here
|
||||
|
||||
Check this directory when you want to:
|
||||
|
||||
* Review the current plan
|
||||
* Inspect prior design decisions
|
||||
* Debug agent behavior
|
||||
* Resume interrupted work
|
||||
* Audit what was executed
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
* Files here are **source-of-truth for agent state**
|
||||
* Safe to commit (recommended for team workflows)
|
||||
* Avoid manual edits unless you understand the workflow implications
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
* Project root `CLAUDE.md` → global agent context
|
||||
* `.claude/` → rules, skills, and subagents
|
||||
* Superpowers upstream docs →
|
||||
@@ -1 +0,0 @@
|
||||
{"reason":"idle timeout","timestamp":1774536800234}
|
||||
@@ -1 +0,0 @@
|
||||
1542
|
||||
@@ -39,8 +39,6 @@ Architectural overview and technical reference for the Agent Dashboard system, c
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
@@ -63,11 +61,10 @@ Architectural overview and technical reference for the Agent Dashboard system, c
|
||||
- [Browser Notification System](#browser-notification-system)
|
||||
- [Update Notifier Subsystem](#update-notifier-subsystem)
|
||||
- [Sagi Companion Subsystem](#sagi-companion-subsystem)
|
||||
- [VS Code Extension Architecture](#vs-code-extension-architecture)
|
||||
- [Security Considerations](#security-considerations)
|
||||
- [Performance Characteristics](#performance-characteristics)
|
||||
- [Deployment Modes](#deployment-modes)
|
||||
- [Statusline Utility](#statusline-utility)
|
||||
- [Model Name Formatting](#model-name-formatting-client)
|
||||
- [Technology Choices](#technology-choices)
|
||||
- [Build & Run Targets](#build--run-targets)
|
||||
|
||||
@@ -473,57 +470,37 @@ graph TD
|
||||
|
||||
### Self-hosted assets (no external CDN)
|
||||
|
||||
Nothing the dashboard or docs render is fetched from a third-party CDN at runtime — all fonts and scripts are served locally, so every surface works fully offline and leaks nothing to external hosts.
|
||||
Nothing the dashboard renders is fetched from a third-party CDN at runtime — fonts are served locally, so the UI works fully offline and leaks nothing to external hosts.
|
||||
|
||||
- **React app fonts** — Inter + JetBrains Mono are imported from `@fontsource` (latin subset) in `client/src/main.tsx`. Vite bundles the per-weight WOFF2 into `client/dist/assets/` with content hashes at build time; there is no Google Fonts `<link>`. Importing the `latin-*` subset entry points keeps the emitted set to one WOFF2 per weight.
|
||||
- **Static pages (landing + wiki)** — load a self-hosted `@font-face` sheet at the repo-root `fonts/` directory (`fonts/fonts.css` + the `*.woff2` files). The root `index.html` references `fonts/fonts.css`; the wiki references `../fonts/fonts.css` (relative paths resolve under GitHub Pages).
|
||||
- **Wiki Mermaid** — vendored as `wiki/mermaid.min.js` (the genuine minified `mermaid@10.9.6` from npm, with a provenance banner; `.prettierignore`d) and loaded via a local `<script>` instead of `cdn.jsdelivr.net`.
|
||||
- **VS Code extension** — the inline `getErrorHtml()` error page dropped its Google Fonts loader for a system font stack (no bundler / local font path available in that webview).
|
||||
|
||||
Net effect: no `fonts.googleapis.com`, `fonts.gstatic.com`, or `cdn.jsdelivr.net` requests anywhere (verified by `git grep`).
|
||||
Net effect: no `fonts.googleapis.com` or `fonts.gstatic.com` requests anywhere (verified by `git grep`).
|
||||
|
||||
### PWA Architecture
|
||||
|
||||
The project ships three independent Progressive Web Apps. Each has its own Web App Manifest and Service Worker, so the browser treats them as separate installable applications with isolated caches.
|
||||
The dashboard ships as a Progressive Web App with its own Web App Manifest and
|
||||
Service Worker (`client/public/manifest.json` + `client/public/sw.js`), so the
|
||||
browser treats it as an installable application with its own cache.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PWA Surface Map │
|
||||
├──────────────────┬──────────────────┬───────────────────────────┤
|
||||
│ Dashboard │ Landing Page │ Wiki │
|
||||
│ (client/) │ (root) │ (wiki/) │
|
||||
├──────────────────┼──────────────────┼───────────────────────────┤
|
||||
│ manifest.json │ manifest.json │ manifest.json │
|
||||
│ sw.js │ sw.js │ sw.js │
|
||||
│ id: dashboard │ id: landing │ id: wiki │
|
||||
├──────────────────┼──────────────────┼───────────────────────────┤
|
||||
│ Precache: │ Precache: │ Precache: │
|
||||
│ /, manifest, │ index.html, │ index.html, style.css, │
|
||||
│ favicon.svg │ favicon, og-img, │ script.js, manifest, │
|
||||
│ │ manifest │ favicon │
|
||||
│ Runtime cache: │ Runtime cache: │ Runtime cache: │
|
||||
│ JS/CSS bundles │ screenshot PNGs │ (all precached) │
|
||||
│ (cache-first) │ (cache-first) │ │
|
||||
│ │ │ │
|
||||
│ Skip: /api/*, │ N/A │ N/A │
|
||||
│ /ws, __vite │ │ │
|
||||
│ │ │ │
|
||||
│ + Push notifs │ │ │
|
||||
│ (VAPID pipeline) │ │ │
|
||||
└──────────────────┴──────────────────┴───────────────────────────┘
|
||||
```
|
||||
|
||||
**Service Worker lifecycle (all three):**
|
||||
**Service Worker lifecycle:**
|
||||
|
||||
1. **Install** → `skipWaiting()` — new SW activates immediately, no waiting for tabs to close.
|
||||
2. **Activate** → old caches deleted (keyed by `CACHE_NAME`: `dashboard-v1`, `landing-v1`, `wiki-v1`). Bump the version string to force a cache bust.
|
||||
2. **Activate** → old caches deleted (keyed by `CACHE_NAME`, e.g. `dashboard-v2`). Bump the version string to force a cache bust.
|
||||
3. **Fetch** → Navigation requests are network-first with offline fallback to cached HTML. Static assets are cache-first with runtime caching on miss.
|
||||
|
||||
**Dashboard SW specifics:** The fetch handler skips `/api/*`, `/ws`, and Vite HMR (`__vite`) URLs so live data and development tooling are never cached. Only responses with `response.type === "basic"` (same-origin) are stored. The existing push notification handlers (`push`, `notificationclick`) are preserved alongside the caching logic.
|
||||
The fetch handler skips `/api/*`, `/ws`, and Vite HMR (`__vite`) URLs so live data
|
||||
and development tooling are never cached. Only responses with
|
||||
`response.type === "basic"` (same-origin) are stored. The push notification
|
||||
handlers (`push`, `notificationclick`) live alongside the caching logic — their
|
||||
payload icon is the same-origin `/favicon.svg` (see `server/lib/push.js`).
|
||||
|
||||
**Manifest icons:** All three manifests reference `favicon.svg` with `sizes="any"` and `type="image/svg+xml"` — supported in Chrome 107+, Firefox 110+, Edge 107+. Two icon entries per manifest: one with `purpose: "any"` and one with `purpose: "maskable"`.
|
||||
**Manifest icons:** `favicon.svg` with `sizes="any"` and `type="image/svg+xml"` —
|
||||
supported in Chrome 107+, Firefox 110+, Edge 107+. Two icon entries: one with
|
||||
`purpose: "any"` and one with `purpose: "maskable"`.
|
||||
|
||||
**iOS meta tags:** All HTML files include `<meta name="apple-mobile-web-app-capable" content="yes">` and `<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">` for standalone home-screen mode on Safari.
|
||||
**iOS meta tags:** `client/index.html` includes
|
||||
`<meta name="apple-mobile-web-app-capable" content="yes">` and
|
||||
`<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">`
|
||||
for standalone home-screen mode on Safari.
|
||||
|
||||
### Client Module Graph
|
||||
|
||||
@@ -1056,7 +1033,7 @@ flowchart TD
|
||||
- Uses Node.js `http` module directly — no dependencies.
|
||||
- Resolution order is **env override → discovery file → default**:
|
||||
- `CLAUDE_DASHBOARD_PORT` forces a single target (no fan-out, no discovery).
|
||||
- Otherwise `server/lib/server-info.js` reads `~/.claude/.agenthub.json`, prunes dead-PID entries, and returns one live `port` per unique `dataDir` (lowest port wins when Docker and `npm run dev` share `~/.claude/agenthub`). The handler POSTs to each returned port in parallel.
|
||||
- Otherwise `server/lib/server-info.js` reads `~/.claude/.agenthub.json`, prunes dead-PID entries, and returns one live `port` per unique `dataDir` (lowest port wins when two instances share `~/.claude/agenthub`). The handler POSTs to each returned port in parallel.
|
||||
- If neither yields anything, the handler falls back to `4820`.
|
||||
- Per-target promises never reject — a dead listener can't starve the others, and the handler can wait on `Promise.all` for clean exit timing.
|
||||
|
||||
@@ -1779,7 +1756,7 @@ Skills are namespaced: `/agenthub-analytics:session-report`, `/agenthub-producti
|
||||
| **agenthub-config** | `config-audit`, `memory-review`, `skill-inventory`, `mcp-audit`, `hook-inventory` | `config-auditor` | — | — |
|
||||
| **agenthub-dashboard** | `dashboard-status`, `quick-stats` | — | — | — |
|
||||
|
||||
**Totals**: 10 plugins, 53 skills, 14 agents, 30 slash commands, 3 CLI tools, 3 hook configurations. Each plugin is installable via `claude plugin install <name>@smartgift-claude-code-monitor`, and a server test (`server/__tests__/plugins-marketplace.test.js`) validates the marketplace↔directory bijection plus every `plugin.json`, agent, skill, and command.
|
||||
**Totals**: 10 plugins, 53 skills, 14 agents, 30 slash commands, 3 CLI tools, 3 hook configurations. Each plugin is installable via `claude plugin install <name>@agenthub`, and a server test (`server/__tests__/plugins-marketplace.test.js`) validates the marketplace↔directory bijection plus every `plugin.json`, agent, skill, and command.
|
||||
|
||||
### Data Model Grounding
|
||||
|
||||
@@ -1808,8 +1785,8 @@ Plugins compute these from raw API data:
|
||||
|
||||
```bash
|
||||
# Marketplace install
|
||||
claude plugin marketplace add Smartgift-AI/Claude-Code-Monitor
|
||||
claude plugin install agenthub-analytics@smartgift-claude-code-monitor
|
||||
claude plugin marketplace add https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git
|
||||
claude plugin install agenthub-analytics@agenthub
|
||||
|
||||
# Local development testing
|
||||
claude --plugin-dir plugins/agenthub-analytics
|
||||
@@ -2116,7 +2093,7 @@ The same shape is used by `GET /status`, `POST /check`, and the `update_status`
|
||||
|
||||
The dashboard does not expose an apply/restart endpoint by design. A process cannot reliably replace itself without an external supervisor, and several real constraints make an in-process self-update path strictly worse than letting the user run two commands in a terminal:
|
||||
|
||||
- **Supervisor ambiguity.** `npm run dev` (concurrently), `npm start`, `pm2`, `systemd`, `launchd`, and Docker each need different restart logic; an in-process helper could only encode one of them and would silently mis-restart the rest.
|
||||
- **Supervisor ambiguity.** `npm run dev` (concurrently), `npm start`, `pm2`, `systemd`, and `launchd` each need different restart logic; an in-process helper could only encode one of them and would silently mis-restart the rest.
|
||||
- **Silent failures.** `npm install` / `npm run build` / port-release timing issues surface as a dead server with no user-facing feedback once the original process has exited.
|
||||
- **No rollback.** A partial pull + install leaves a broken checkout with no atomic recovery — the working tree is mutated mid-flight.
|
||||
- **Branch coverage.** Even with the situation-aware `manual_command` produced by the detection layer, an automatic apply would still need branch-aware integration (rebase vs merge vs switch) and merge-conflict handling. That belongs in the user's shell, not in a background daemon.
|
||||
@@ -2224,64 +2201,6 @@ Sagi's only contact with the rest of the app is four light, additive touchpoints
|
||||
|
||||
---
|
||||
|
||||
## VS Code Extension Architecture
|
||||
|
||||
The **AgentHub** VS Code extension provides an integrated monitoring experience directly within the editor. It communicates with the local dashboard server via standard HTTP APIs and renders the dashboard UI in a webview.
|
||||
|
||||
<p align="center">
|
||||
<img src="vscode-extension/vscode.png" alt="VS Code Extension Screenshot" width="100%">
|
||||
</p>
|
||||
|
||||
### Extension Components
|
||||
|
||||
| Component | Responsibility |
|
||||
| --- | --- |
|
||||
| **Extension Host** (`extension.js`) | Manages the extension lifecycle, registers commands, creates the status bar item, and coordinates the webview panel. |
|
||||
| **Sidebar Provider** (`sidebar.js`) | Implements the `TreeDataProvider` for the Activity Bar. It performs background polling of the dashboard APIs (`/api/stats`, `/api/analytics`, `/api/sessions`) every 5 seconds. |
|
||||
| **Status Bar Item** | Provides a persistent "Pulse" indicator in the VS Code status bar, showing active session and agent counts. |
|
||||
| **Webview Panel** | A native VS Code tab that renders the dashboard React application. It supports deep linking to specific sessions or sub-pages. |
|
||||
|
||||
### Data Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "VS Code Process"
|
||||
SB[Status Bar]
|
||||
SIDE[Sidebar TreeView]
|
||||
WV[Webview Panel]
|
||||
end
|
||||
|
||||
subgraph "Extension Host"
|
||||
EH[extension.js]
|
||||
SP[sidebar.js]
|
||||
end
|
||||
|
||||
subgraph "Dashboard Server (localhost)"
|
||||
API["/api/stats<br/>/api/analytics<br/>/api/sessions"]
|
||||
DS[Express Server]
|
||||
end
|
||||
|
||||
SIDE -->|Poll 5s| SP
|
||||
SP -->|HTTP GET| API
|
||||
EH -->|HTTP GET| API
|
||||
SB -->|Pulse| EH
|
||||
WV -->|Iframe src| DS
|
||||
```
|
||||
|
||||
### Key Implementation Details
|
||||
|
||||
1. **Auto-Detection**: The extension checks both port `5173` (Vite dev server) and `4820` (Production server) on `localhost`. It prioritizes the production port for API data but can render the UI from either.
|
||||
2. **Real-time Status**: The `SidebarProvider` uses a background loop with `onDidChangeTreeData` to automatically toggle between **Online** and **Offline** states as the local server starts or stops.
|
||||
3. **Deep Linking**: Commands like `claude-code-agent-monitor.openDashboard` accept arguments (e.g., a session ID or page path like `analytics`) to route the webview to specific views within the React SPA.
|
||||
4. **Theme Awareness**: The Activity Bar icon (`icon.svg` or `apple-touch-icon.png`) and sidebar icons use VS Code's `ThemeIcon` and `ThemeColor` to ensure they adapt to Light, Dark, and High Contrast themes.
|
||||
|
||||
For the extension source code, refer to the [vscode-extension/](./vscode-extension/) directory.
|
||||
|
||||
> [!TIP]
|
||||
> Extension on VS Code Marketplace: [AgentHub](https://marketplace.visualstudio.com/items?itemName=hoangsonw.claude-code-agent-monitor)
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
| Area | Approach |
|
||||
@@ -2368,59 +2287,7 @@ graph LR
|
||||
| **File watching** | `node --watch` + Vite HMR | None |
|
||||
| **Source maps** | Inline | External files |
|
||||
|
||||
### Container (Docker / Podman)
|
||||
|
||||
A multi-stage `Dockerfile` builds the client and server into a single production image. Both Docker and Podman are fully supported — the image is OCI-compliant.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "Multi-Stage Build"
|
||||
S1["Stage 1: server-deps\nnode:22-alpine\nnpm ci --omit=dev"]
|
||||
S2["Stage 2: client-build\nnode:22-alpine\nnpm ci + vite build"]
|
||||
S3["Stage 3: runtime\nnode:22-alpine\nCopies node_modules + client/dist"]
|
||||
S1 --> S3
|
||||
S2 --> S3
|
||||
end
|
||||
|
||||
subgraph "Container Runtime"
|
||||
VOL1["~/.claude (ro)\nlegacy session import"]
|
||||
VOL2["agent-monitor-data\nSQLite persistence"]
|
||||
S3 -->|"EXPOSE 4820"| SRV["node server/index.js\nport 4820"]
|
||||
VOL1 --> SRV
|
||||
VOL2 --> SRV
|
||||
end
|
||||
|
||||
style S3 fill:#339933,stroke:#5cb85c,color:#fff
|
||||
style SRV fill:#6366f1,stroke:#818cf8,color:#fff
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker compose up -d --build
|
||||
|
||||
# Podman Compose
|
||||
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build
|
||||
|
||||
# Plain Docker / Podman (equivalent)
|
||||
docker build -t agent-monitor .
|
||||
docker run -d -p 4820:4820 \
|
||||
-v "$HOME/.claude:/root/.claude:ro" \
|
||||
-v agent-monitor-data:/app/data \
|
||||
agent-monitor
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> **Hook note:** Claude Code hooks run on the host, not inside the container. The containerized server still receives hook events via HTTP on `localhost:4820` — run `npm run install-hooks` on the host after the container is up. `scripts/install-hooks.js` detects container execution and refuses there (issue #193) so it cannot write a container-internal handler path into a bind-mounted host `~/.claude`; the containerized server's boot-time auto-install is skipped for the same reason. Override with `AGENTHUB_ALLOW_CONTAINER_HOOKS=1` only when Claude Code itself runs inside the container.
|
||||
|
||||
---
|
||||
|
||||
## Statusline Utility
|
||||
|
||||
The `statusline/` directory contains a standalone CLI statusline for Claude Code, separate from the web dashboard. It renders a color-coded bar at the bottom of the Claude Code terminal showing model, user, working directory, git branch, context window usage, per-direction token counts, and session cost in USD.
|
||||
|
||||
### Model Name Formatting (Client)
|
||||
## Model Name Formatting (Client)
|
||||
|
||||
The `client/src/lib/format.ts` module exports a `formatModelName()` utility that converts raw model identifiers stored in the database into human-readable display names throughout the UI (everywhere **except** the Settings page, which shows raw patterns for pricing rule configuration).
|
||||
|
||||
@@ -2447,60 +2314,6 @@ The function handles:
|
||||
|
||||
Components that consume this: SessionDetail, Analytics (donut chart + breakdown), Dashboard (model stats), MessageList, AgentCard, SessionDrillIn, and EventDetail.
|
||||
|
||||
### Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CC as Claude Code
|
||||
participant SH as statusline-command.sh
|
||||
participant PY as statusline.py
|
||||
participant GIT as git CLI
|
||||
|
||||
CC->>SH: stdin (JSON payload)
|
||||
SH->>PY: Pipes stdin through
|
||||
PY->>PY: Parse JSON (model, cwd, context_window, cost)
|
||||
PY->>GIT: git symbolic-ref --short HEAD
|
||||
GIT-->>PY: Branch name
|
||||
PY->>PY: Build ANSI-colored segments (incl. tokens by direction, cost)
|
||||
PY-->>CC: stdout (formatted statusline)
|
||||
```
|
||||
|
||||
### Segments
|
||||
|
||||
| Segment | Source | Color Logic |
|
||||
| ------------ | ------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| Model | `data.model.display_name` | Always cyan |
|
||||
| User | `$USERNAME` / `$USER` env var | Always green |
|
||||
| Working Dir | `data.workspace.current_dir` | Always yellow, `~` prefix for home |
|
||||
| Git Branch | `git symbolic-ref --short HEAD` | Always magenta, hidden outside git repos |
|
||||
| Context Bar | `data.context_window.used_percentage` | Green < 50%, Yellow 50–79%, Red >= 80% |
|
||||
| Token Counts | `data.context_window.current_usage` | Green `↑` input, cyan `↓` output, dim `c` cache reads |
|
||||
| Session Cost | `data.cost.total_cost_usd` | Green < $5, Yellow $5–$20, Red >= $20 (shown on API and subscription plans) |
|
||||
|
||||
### Integration
|
||||
|
||||
The statusline is configured in `~/.claude/settings.json` via the `statusLine` key:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "bash \"/path/to/.claude/statusline-command.sh\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Claude Code invokes this command on each update, piping a JSON payload to stdin. The script reads the JSON, extracts fields, runs `git` for branch info, and prints ANSI-formatted output to stdout.
|
||||
|
||||
**Design decisions:**
|
||||
|
||||
- **Python 3.6+** -- available on virtually all systems, handles ANSI and JSON natively
|
||||
- **No dependencies** -- uses only stdlib (`sys`, `json`, `os`, `subprocess`)
|
||||
- **Shell wrapper** -- `statusline-command.sh` sets `PYTHONUTF8=1` for Windows Unicode support and resolves the absolute path to the Python script
|
||||
- **Fail-safe** -- exits silently on empty input or JSON parse errors, never blocks Claude Code
|
||||
|
||||
---
|
||||
|
||||
## Technology Choices
|
||||
|
||||
| Technology | Why This Over Alternatives |
|
||||
@@ -2528,8 +2341,6 @@ make build Build the React client for production
|
||||
make start Start the production server
|
||||
make test Run all tests (server + client)
|
||||
make format Format all files with Prettier
|
||||
make docker-up Start via docker-compose
|
||||
make docker-down Stop docker-compose stack
|
||||
```
|
||||
|
||||
See `Makefile` for the complete set of 25 targets covering setup, dev, testing, formatting, data management, Codex extensions, and Docker/Podman workflows.
|
||||
See `Makefile` for the complete set of 17 targets covering setup, dev, testing, formatting, and data management.
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# Multi-stage Dockerfile for AgentHub - a Node.js server with a React client.
|
||||
# This setup optimizes the final image size by separating the build and runtime stages.
|
||||
# The first stage installs only the production dependencies for the server, while the second stage builds the React client.
|
||||
# The final stage combines the necessary files and dependencies to run the application in production.
|
||||
# Compatibility: This setup is compatible with both Podman and Docker. Runnable on any platform that supports Node.js and Alpine Linux.
|
||||
#
|
||||
# Author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
|
||||
# ── Stage 1: Install server production deps ───────────────────────────
|
||||
FROM node:22-alpine AS server-deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
# The root `postinstall` hook (scripts/postinstall.js) fires during `npm ci`, so
|
||||
# the file must exist here or npm aborts with MODULE_NOT_FOUND before installing
|
||||
# anything. It self-skips when client/ is absent (as it is in this stage), so
|
||||
# copying just the one script keeps this deps-cache layer from busting on
|
||||
# unrelated scripts/ edits. Do NOT use --ignore-scripts: that would also skip
|
||||
# better-sqlite3's prebuild fetch and silently drop the native SQLite driver.
|
||||
COPY scripts/postinstall.js ./scripts/postinstall.js
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# ── Stage 2: Build React client ───────────────────────────────────────
|
||||
FROM node:22-alpine AS client-build
|
||||
WORKDIR /app/client
|
||||
COPY client/package.json client/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY client/ ./
|
||||
# vite.config.ts stamps the UI version from the repo-root package.json (one level
|
||||
# up from the client dir). Provide it here so the built client shows the real
|
||||
# release version; the config falls back gracefully if it is ever absent.
|
||||
COPY package.json /app/package.json
|
||||
RUN npm run build
|
||||
|
||||
# ── Stage 3: Production runtime ───────────────────────────────────────
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
|
||||
# Runs Claude Code sessions inside a named tmux session per lane so both the
|
||||
# dashboard (via node-pty attach) and a real terminal can share one live pane.
|
||||
RUN apk add --no-cache tmux
|
||||
|
||||
COPY --from=server-deps /app/node_modules ./node_modules/
|
||||
COPY package.json ./
|
||||
COPY server/ ./server/
|
||||
COPY scripts/ ./scripts/
|
||||
COPY statusline/ ./statusline/
|
||||
COPY --from=client-build /app/client/dist ./client/dist/
|
||||
|
||||
RUN mkdir -p data
|
||||
|
||||
EXPOSE 4820
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# In a container the app MUST bind all interfaces: the server binds loopback by
|
||||
# default (GHSA-gr74-4xfh-6jw9), but a container's loopback is a separate
|
||||
# namespace the published port cannot reach, so a loopback bind makes the port
|
||||
# unreachable. Inside a container the trust boundary is the *host* port publish
|
||||
# (keep it on 127.0.0.1 — see docker-compose.yml / INSTALL.md), not this bind.
|
||||
ENV DASHBOARD_HOST=0.0.0.0
|
||||
|
||||
# The recommended ~/.claude bind mount is read-only, so the server cannot write
|
||||
# its default data dir (~/.claude/agenthub). Persist to the mounted
|
||||
# volume at /app/data instead.
|
||||
ENV DASHBOARD_DATA_DIR=/app/data
|
||||
|
||||
CMD ["node", "server/index.js"]
|
||||
@@ -7,8 +7,8 @@ A step-by-step guide to get AgentHub — the local control surface for Claude Co
|
||||
If all you want is a working dashboard, skip every step below. From Claude Code:
|
||||
|
||||
```
|
||||
/plugin marketplace add Smartgift-AI/Claude-Code-Monitor
|
||||
/plugin install agenthub@claude-code-agent-monitor-plugins
|
||||
/plugin marketplace add https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git
|
||||
/plugin install agenthub@agenthub
|
||||
```
|
||||
|
||||
The next session start installs the hooks, boots the server, puts the `agenthub` CLI
|
||||
@@ -30,7 +30,7 @@ dashboard, run it from source, or stay on Node 20.
|
||||
| Node.js | 20+ (22+ recommended) | Required for server and client |
|
||||
| npm | 9+ | Comes with Node.js |
|
||||
| Claude Code | 2.x+ | Required for hook integration |
|
||||
| Python | 3.6+ | Optional — statusline utility only |
|
||||
| Python | 3.6+ | Optional — only for `scripts/expand-ts-module-docs.py` (doc generation) |
|
||||
| Git | Any | For cloning the repository |
|
||||
| `superpowers` plugin | Any | Optional — required only by `/agenthub:ship-feature` and `/agenthub:ship-feature-lane`, which drive its skills step by step (and which the built-in pipeline templates name as their per-node commands). Nothing else in AgentHub needs it; `/agenthub-doctor` reports it as `WARN` when absent. **Installed automatically** by the plugin's `SessionStart` bootstrap (opt out with `AGENTHUB_AUTO_INSTALL_PLUGINS=0`); by hand: `claude plugin install superpowers@claude-plugins-official` |
|
||||
|
||||
@@ -39,8 +39,8 @@ dashboard, run it from source, or stay on Node 20.
|
||||
## Step 1 — Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git
|
||||
cd Claude-Code-Monitor
|
||||
git clone https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git
|
||||
cd AgentHub
|
||||
```
|
||||
|
||||
---
|
||||
@@ -85,7 +85,7 @@ This starts two processes concurrently:
|
||||
Open **http://localhost:5173** in your browser.
|
||||
|
||||
> [!TIP]
|
||||
> When you run the dashboard directly on the host with `npm run dev` or `npm start`, the server automatically writes the Claude Code hook configuration to `~/.claude/settings.json`. If you run the dashboard in Docker or Podman, install hooks from the host with `npm run install-hooks` after the container is up.
|
||||
> When you run the dashboard directly on the host with `npm run dev` or `npm start`, the server automatically writes the Claude Code hook configuration to `~/.claude/settings.json`. If you run the dashboard inside a container, install hooks from the host with `npm run install-hooks` instead — a hook path written from inside a container points at the container filesystem.
|
||||
|
||||
---
|
||||
|
||||
@@ -120,8 +120,6 @@ The dashboard is a Progressive Web App. After opening it in a supported browser
|
||||
2. Once installed, the dashboard launches in its own window with no browser chrome
|
||||
3. Offline support: previously visited pages and assets are served from the Service Worker cache when the network is unavailable
|
||||
|
||||
The landing page and wiki are also installable PWAs with their own manifests and service workers — visit each in a browser to install independently.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — (Optional) Import existing Claude Code history
|
||||
@@ -223,89 +221,6 @@ See [`.codex/README.md`](./.codex/README.md) for Codex extension details.
|
||||
|
||||
---
|
||||
|
||||
## Optional: VS Code extension
|
||||
|
||||
**AgentHub** is also available as a dedicated VS Code extension for seamless, integrated monitoring.
|
||||
|
||||
<p align="center">
|
||||
<img src="vscode-extension/vscode.png" alt="VS Code Extension Screenshot" width="100%">
|
||||
</p>
|
||||
|
||||
### Features
|
||||
- **Real-time Sidebar**: Monitor agent status, health, and usage stats in the Activity Bar.
|
||||
- **Pulse Status Bar**: High-level session and agent counts in the bottom status bar.
|
||||
- **Direct Navigation**: Jump to specific dashboard pages or recent sessions.
|
||||
- **Embedded Dashboard**: Full dashboard interface within a native VS Code tab.
|
||||
|
||||
### Installation
|
||||
1. Open the [vscode-extension](./vscode-extension) folder in VS Code.
|
||||
2. Install via the Marketplace or package it manually:
|
||||
```bash
|
||||
cd vscode-extension
|
||||
npm install
|
||||
# Generate .vsix for local install
|
||||
npm run package
|
||||
```
|
||||
3. After installation, ensure the main dashboard server is running (`npm run dev`).
|
||||
4. Look for the **Radar icon** in your VS Code Activity Bar.
|
||||
|
||||
For advanced configuration, refer to the [.vscode](./.vscode) and [vscode-extension](./vscode-extension) directories.
|
||||
|
||||
> [!TIP]
|
||||
> Extension on VS Code Marketplace: [AgentHub](https://marketplace.visualstudio.com/items?itemName=hoangsonw.claude-code-agent-monitor)
|
||||
|
||||
---
|
||||
|
||||
## Container mode (Docker / Podman)
|
||||
|
||||
The repository includes both a multi-stage `Dockerfile` and a `docker-compose.yml` file. Docker and Podman are both supported.
|
||||
|
||||
### Compose
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker compose up -d --build
|
||||
|
||||
# Podman Compose
|
||||
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build
|
||||
```
|
||||
|
||||
Open **http://localhost:4820** in your browser.
|
||||
|
||||
### Plain Docker / Podman
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker build -t agent-monitor .
|
||||
docker run -d --name agent-monitor \
|
||||
-p 127.0.0.1:4820:4820 \
|
||||
-v "$HOME/.claude:/root/.claude:ro" \
|
||||
-v agent-monitor-data:/app/data \
|
||||
agent-monitor
|
||||
|
||||
# Podman
|
||||
podman build -t agent-monitor .
|
||||
podman run -d --name agent-monitor \
|
||||
-p 127.0.0.1:4820:4820 \
|
||||
-v "$HOME/.claude:/root/.claude:ro" \
|
||||
-v "$HOME/.claude/agenthub:/app/data" \
|
||||
agent-monitor
|
||||
```
|
||||
|
||||
### Container notes
|
||||
|
||||
| Mount | Purpose |
|
||||
|---|---|
|
||||
| `~/.claude:/root/.claude:ro` | Lets the server import legacy Claude session history |
|
||||
| `~/.claude/agenthub:/app/data` | **Canonical SQLite database** (shared with native installs) |
|
||||
|
||||
> [!NOTE]
|
||||
> The image sets `DASHBOARD_HOST=0.0.0.0` and `DASHBOARD_DATA_DIR=/app/data` internally (baked into the `Dockerfile`). The `0.0.0.0` bind is required because a container's loopback is a separate namespace the published port cannot reach; `/app/data` is bind-mounted to `~/.claude/agenthub` on the host so Docker uses the same database as `npm start`. The trust boundary is the **host** port publish — the examples use `-p 127.0.0.1:4820:4820`, so the dashboard is local-only. To expose it on a LAN, publish on `0.0.0.0` (`-p 4820:4820`) **and** set `DASHBOARD_TOKEN`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Claude Code hooks run on the host, not inside the container. After the container is healthy on `http://localhost:4820`, run `npm run install-hooks` on the host so Claude Code posts hook events back to the containerized server. The installer refuses to run inside a container (issue #193) to avoid writing a container-internal handler path into a bind-mounted `~/.claude`; use `AGENTHUB_ALLOW_CONTAINER_HOOKS=1` only if you run Claude Code inside the same container.
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `npm run setup` shows `better-sqlite3` errors
|
||||
|
||||
@@ -94,30 +94,6 @@ import-history: ## Import sessions from ~/.claude/ history
|
||||
clear-data: ## Delete all data rows (preserves schema)
|
||||
node scripts/clear-data.js
|
||||
|
||||
# ─── Docker / Podman ───────────────────────────────────────────────────────
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build: ## Build dashboard Docker image
|
||||
docker build -t claude-agent-monitor:local .
|
||||
|
||||
.PHONY: docker-up
|
||||
docker-up: ## Start via docker-compose
|
||||
docker compose up -d
|
||||
|
||||
.PHONY: docker-down
|
||||
docker-down: ## Stop docker-compose stack
|
||||
docker compose down
|
||||
|
||||
.PHONY: docker-logs
|
||||
docker-logs: ## Tail docker-compose logs
|
||||
docker compose logs -f
|
||||
|
||||
|
||||
.PHONY: podman-build
|
||||
podman-build: ## Build dashboard Podman image
|
||||
podman build -t localhost/claude-agent-monitor:local .
|
||||
|
||||
|
||||
# ─── Help ───────────────────────────────────────────────────────────────────
|
||||
|
||||
.PHONY: help
|
||||
|
||||
@@ -68,8 +68,8 @@ Two commands on a machine that has nothing but Claude Code, no clone and no
|
||||
`npm run setup`:
|
||||
|
||||
```
|
||||
/plugin marketplace add Smartgift-AI/Claude-Code-Monitor
|
||||
/plugin install agenthub@claude-code-agent-monitor-plugins
|
||||
/plugin marketplace add https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git
|
||||
/plugin install agenthub@agenthub
|
||||
```
|
||||
|
||||
The first session start installs the hooks, boots the server, puts `agenthub` on
|
||||
@@ -83,7 +83,7 @@ Details, including what to delete on uninstall:
|
||||
## Install from a checkout
|
||||
|
||||
```bash
|
||||
npm run setup # root, client and vscode-extension dependencies
|
||||
npm run setup # root and client dependencies
|
||||
npm run build # builds the client into client/dist
|
||||
npm start # serves the built client and the API on :4820
|
||||
```
|
||||
|
||||
@@ -41,8 +41,8 @@ Hai lệnh, trên máy chỉ có Claude Code, không cần clone repo, không c
|
||||
`npm run setup`:
|
||||
|
||||
```
|
||||
/plugin marketplace add Smartgift-AI/Claude-Code-Monitor
|
||||
/plugin install agenthub@claude-code-agent-monitor-plugins
|
||||
/plugin marketplace add https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git
|
||||
/plugin install agenthub@agenthub
|
||||
```
|
||||
|
||||
Lần session-start đầu tiên sẽ cài hooks, boot server, đưa `agenthub` vào PATH và
|
||||
@@ -55,7 +55,7 @@ cần xóa lúc uninstall: [`docs/PLUGINS.md`](docs/PLUGINS.md).
|
||||
## Cài từ checkout
|
||||
|
||||
```bash
|
||||
npm run setup # cài dependency cho root, client và vscode-extension
|
||||
npm run setup # cài dependency cho root và client
|
||||
npm run build # build client vào client/dist
|
||||
npm start # serve client đã build + API trên :4820
|
||||
```
|
||||
|
||||
@@ -48,48 +48,11 @@ npm run install-hooks
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Container note: do not rely on hook auto-install from inside Docker or Podman. The hook path written by a container would point at the container filesystem, not the host. Start the container first, then run `npm run install-hooks` on the host. As a safeguard (issue #193), the installer now **detects container execution and refuses to run** (exiting non-zero) so it can never poison a bind-mounted host `~/.claude`; the containerized server logs the same guidance instead of silently writing a bad path. If you genuinely run Claude Code inside the same container, override with `AGENTHUB_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks`.
|
||||
> Container note: if you run the dashboard inside a container (a Dev Container, Codespaces, or your own image), do not install hooks from in there — the hook path a container writes points at the container filesystem, not the host. Run `npm run install-hooks` on the host instead. As a safeguard (issue #193) the installer **detects container execution and refuses to run** (exiting non-zero) so it can never poison a bind-mounted host `~/.claude`. If Claude Code itself genuinely runs inside that container, override with `AGENTHUB_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks`.
|
||||
|
||||
> [!NOTE]
|
||||
> Prefer a ready-made dev environment? This repo ships an **optional** Dev Container (`.devcontainer/`) for VS Code / GitHub Codespaces — Node 22, native build tools for `better-sqlite3`, Python, and ports `4820`/`5173` preconfigured. It's purely opt-in and changes nothing for host-based development. See [`.devcontainer/README.md`](.devcontainer/README.md). (Hooks remain host-side there too.)
|
||||
|
||||
### Container runtime (Docker / Podman)
|
||||
|
||||
The repo includes both a multi-stage `Dockerfile` and a `docker-compose.yml` file. The container image serves the built client and API on port `4820`, stores SQLite data under `/app/data`, and can import legacy Claude history from a read-only `~/.claude` mount.
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker compose up -d --build
|
||||
|
||||
# Podman Compose
|
||||
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build
|
||||
|
||||
# Plain Docker
|
||||
docker build -t agent-monitor .
|
||||
docker run -d --name agent-monitor \
|
||||
-p 127.0.0.1:4820:4820 \
|
||||
-v "$HOME/.claude:/root/.claude:ro" \
|
||||
-v agent-monitor-data:/app/data \
|
||||
agent-monitor
|
||||
|
||||
# Plain Podman
|
||||
podman build -t agent-monitor .
|
||||
podman run -d --name agent-monitor \
|
||||
-p 127.0.0.1:4820:4820 \
|
||||
-v "$HOME/.claude:/root/.claude:ro" \
|
||||
-v "$HOME/.claude/agenthub:/app/data" \
|
||||
agent-monitor
|
||||
```
|
||||
|
||||
Container-specific behavior:
|
||||
|
||||
- The dashboard is available at `http://localhost:4820`
|
||||
- The image sets `DASHBOARD_HOST=0.0.0.0` (bind inside the container — its loopback is a separate namespace the published port cannot reach) and `DASHBOARD_DATA_DIR=/app/data` internally; both are baked into the `Dockerfile`
|
||||
- The examples publish on `127.0.0.1` only, so the dashboard is local-only. To expose it on a LAN, publish on `0.0.0.0` (`-p 4820:4820`) **and** set `DASHBOARD_TOKEN`
|
||||
- `~/.claude:/root/.claude:ro` is used for history import only
|
||||
- `~/.claude/agenthub:/app/data` is the **canonical** SQLite database (shared with native installs)
|
||||
- Claude Code hooks still execute on the host, so install them from the host with `npm run install-hooks`
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -136,39 +99,18 @@ This repository ships extension files for both agent ecosystems:
|
||||
|
||||
See [`.codex/README.md`](./.codex/README.md) for Codex extension details.
|
||||
|
||||
### VS Code extension setup
|
||||
|
||||
**AgentHub** is available as an integrated VS Code extension for seamless monitoring within your editor.
|
||||
|
||||
- **Activity Bar View**: Adds a custom "Radar" icon to the activity bar providing real-time agent health, token counts, and session stats.
|
||||
- **Status Bar Integration**: Displays live session and agent pulse counts in the bottom bar.
|
||||
- **Embedded Dashboard**: Renders the full web dashboard directly in a VS Code editor tab.
|
||||
- **Automated Detection**: Automatically finds your dashboard server on ports `5173` or `4820`.
|
||||
|
||||
<p align="center">
|
||||
<img src="vscode-extension/vscode.png" alt="VS Code Extension Screenshot" width="100%">
|
||||
</p>
|
||||
|
||||
To install or develop the extension:
|
||||
1. Open the [vscode-extension](./vscode-extension) directory in VS Code.
|
||||
2. Run `npm install` and `npm run package` to generate a local `.vsix` installer.
|
||||
3. For developer details, see [vscode-extension/README.md](./vscode-extension/README.md).
|
||||
|
||||
> [!TIP]
|
||||
> Extension on VS Code Marketplace: [AgentHub](https://marketplace.visualstudio.com/items?itemName=hoangsonw.claude-code-agent-monitor)
|
||||
|
||||
### PWA configuration (optional)
|
||||
|
||||
The dashboard, landing page, and wiki each ship as independent Progressive Web Apps. No configuration is required — manifests and service workers are included out of the box.
|
||||
The dashboard ships as a Progressive Web App. No configuration is required — its manifest and service worker are included out of the box.
|
||||
|
||||
**Customising the manifest:** Edit the `manifest.json` in the relevant directory (`client/public/` for dashboard, root for landing, `wiki/` for wiki). Common fields to change:
|
||||
**Customising the manifest:** Edit `client/public/manifest.json`. Common fields to change:
|
||||
|
||||
- `name` / `short_name` — displayed on the home screen / dock
|
||||
- `theme_color` — address bar / title bar tint (default: `#6366f1`)
|
||||
- `background_color` — splash screen background
|
||||
- `start_url` — entry point when launched from home screen
|
||||
|
||||
**Updating the service worker cache:** Each SW has a `CACHE_NAME` constant (e.g. `dashboard-v2`). After deploying new assets, bump the version string to force browsers to re-fetch — though for the dashboard this is rarely needed: hashed `/assets/*` URLs are immutable per build, everything else is fetched network-first with cache fallback, and a `controllerchange` listener in the client reloads the page exactly once when a new SW takes over, so a rebuild propagates without a hard refresh.
|
||||
**Updating the service worker cache:** The SW has a `CACHE_NAME` constant (e.g. `dashboard-v2`). After deploying new assets, bump the version string to force browsers to re-fetch — though for the dashboard this is rarely needed: hashed `/assets/*` URLs are immutable per build, everything else is fetched network-first with cache fallback, and a `controllerchange` listener in the client reloads the page exactly once when a new SW takes over, so a rebuild propagates without a hard refresh.
|
||||
|
||||
**Browser support:** PWA install prompts appear in Chrome 107+, Edge 107+, and Firefox 110+ (desktop and Android). Safari supports `apple-mobile-web-app-capable` for iOS home-screen mode but does not show an install banner.
|
||||
|
||||
@@ -349,16 +291,6 @@ Commonly used targets:
|
||||
| `make format-check` | `npm run format:check` | Check formatting without writing |
|
||||
| `make seed` | `npm run seed` | Load demo data |
|
||||
| `make clear-data` | `npm run clear-data` | Delete all data rows |
|
||||
| `make docker-up` | `docker compose up -d` | Start via docker-compose |
|
||||
| `make docker-down` | `docker compose down` | Stop docker-compose stack |
|
||||
|
||||
---
|
||||
|
||||
## Statusline (optional)
|
||||
|
||||
The `statusline/` directory contains a standalone terminal statusline for Claude Code showing model, working directory, git branch, context window usage, and token counts. It is independent of the web dashboard.
|
||||
|
||||
See [statusline/README.md](./statusline/README.md) for installation instructions.
|
||||
|
||||
---
|
||||
|
||||
@@ -464,36 +396,3 @@ And make sure Claude Code posts hooks to the new port:
|
||||
CLAUDE_DASHBOARD_PORT=4821 claude
|
||||
# or edit scripts/hook-handler.js and change the default port
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Docker / Podman container starts but no sessions appear
|
||||
|
||||
**Check 1 — Is the container healthy?**
|
||||
|
||||
```bash
|
||||
curl http://localhost:4820/api/health
|
||||
# Expected: {"status":"ok","timestamp":"..."}
|
||||
```
|
||||
|
||||
**Check 2 — Did you install hooks on the host?**
|
||||
|
||||
Hooks run on the host machine, not inside the container. After the container is up:
|
||||
|
||||
```bash
|
||||
npm run install-hooks
|
||||
```
|
||||
|
||||
**Check 3 — Are hooks pointing to the right port?**
|
||||
|
||||
Open `~/.claude/settings.json` and verify the hook commands reference `localhost:4820` (or whatever port the container is mapped to). If you changed the port mapping, update hooks accordingly.
|
||||
|
||||
---
|
||||
|
||||
### Docker build fails during `npm ci`
|
||||
|
||||
If the build fails in Stage 1 with `better-sqlite3` errors, this is expected and should not block the build — `better-sqlite3` is an optional dependency. If the build still fails:
|
||||
|
||||
- Ensure you are using the latest Dockerfile (it should use `node:22-alpine` and **not** install `python3`, `make`, or `g++`)
|
||||
- Run `docker build --no-cache -t agent-monitor .` to force a clean rebuild
|
||||
- Check that `package.json` has `better-sqlite3` under `optionalDependencies`, not `dependencies`
|
||||
|
||||
@@ -19,8 +19,6 @@ Enterprise-grade React + TypeScript dashboard for real-time Claude Code agent mo
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
"name": "SmartGift",
|
||||
"url": "https://smartgift.vn"
|
||||
},
|
||||
"codeRepository": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"featureList": "Real-time dashboard, Kanban board, Session tracking, Activity feed, Token usage analytics, Cost tracking, Browser notifications, WebSocket live updates, CLI statusline"
|
||||
"codeRepository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"featureList": "Real-time dashboard, Kanban board, Session tracking, Activity feed, Token usage analytics, Cost tracking, Browser notifications, WebSocket live updates"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -123,9 +123,9 @@ async function notify(title: string, body: string) {
|
||||
try {
|
||||
if ("serviceWorker" in navigator) {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
await registration.showNotification(title, { body, icon: "/favicon.ico", silent: false });
|
||||
await registration.showNotification(title, { body, icon: "/favicon.svg", silent: false });
|
||||
} else {
|
||||
new Notification(title, { body, icon: "/favicon.ico" });
|
||||
new Notification(title, { body, icon: "/favicon.svg" });
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore
|
||||
|
||||
@@ -127,9 +127,7 @@ describe("agentOriginLabel - parent chain walk", () => {
|
||||
|
||||
describe("projectFromCwd - session-cwd fallback for the dir prefix", () => {
|
||||
it("returns the last path segment of a POSIX cwd", () => {
|
||||
expect(projectFromCwd("/Users/dev/WebstormProjects/Claude-Code-Agent-Monitor")).toBe(
|
||||
"Claude-Code-Agent-Monitor"
|
||||
);
|
||||
expect(projectFromCwd("/Users/dev/WebstormProjects/AgentHub")).toBe("AgentHub");
|
||||
});
|
||||
|
||||
it("ignores a trailing slash", () => {
|
||||
|
||||
@@ -1235,7 +1235,7 @@ export interface UpdateStatusPayload {
|
||||
* Drives whether the "Update available" call-to-action is shown. */
|
||||
update_available: boolean;
|
||||
/** Absolute path to the detected git repository root. Example:
|
||||
* `"/Users/dev/Claude-Code-Agent-Monitor"`. */
|
||||
* `"/Users/dev/AgentHub"`. */
|
||||
repo_root?: string;
|
||||
/** Resolved ref compared against, e.g. "upstream/master" or "origin/main";
|
||||
* null when no remote could be resolved (e.g. no remotes configured). */
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
services:
|
||||
agent-monitor:
|
||||
build: .
|
||||
container_name: agent-monitor
|
||||
ports:
|
||||
# Publish on the host loopback only, so the dashboard is not reachable
|
||||
# from the LAN out of the box (matches the server's default trust
|
||||
# boundary — GHSA-gr74-4xfh-6jw9). To expose it on a LAN, change this to
|
||||
# "${DASHBOARD_PORT:-4820}:4820" and set DASHBOARD_TOKEN below.
|
||||
- "127.0.0.1:${DASHBOARD_PORT:-4820}:4820"
|
||||
volumes:
|
||||
# Same SQLite file as `npm start` / `npm run dev` on the host.
|
||||
- ${CLAUDE_HOME:-~/.claude}/agenthub:/app/data
|
||||
- ${CLAUDE_HOME:-~/.claude}:/root/.claude:ro
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DASHBOARD_PORT=4820
|
||||
# Optional — required when a Prometheus-in-Docker scraper reaches this
|
||||
# container as anything other than loopback (e.g. host.docker.internal).
|
||||
- DASHBOARD_ALLOWED_HOSTS=${DASHBOARD_ALLOWED_HOSTS:-}
|
||||
# DASHBOARD_HOST=0.0.0.0 and DASHBOARD_DATA_DIR=/app/data are baked into
|
||||
# the image (see Dockerfile) so both `docker run` and Compose work as-is.
|
||||
restart: unless-stopped
|
||||
@@ -53,8 +53,8 @@ flowchart LR
|
||||
`npm run setup` ends with a fail-soft `npm link` (the `link-cli` script), so after a normal local setup `agenthub` is on your PATH from any directory:
|
||||
|
||||
```bash
|
||||
git clone https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git
|
||||
cd Claude-Code-Agent-Monitor
|
||||
git clone https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git
|
||||
cd AgentHub
|
||||
npm run setup # installs deps AND links agenthub globally
|
||||
agenthub help
|
||||
```
|
||||
|
||||
@@ -64,7 +64,7 @@ graph TB
|
||||
```
|
||||
|
||||
**Database Location:**
|
||||
- **Canonical (default):** `~/.claude/agenthub/dashboard.db` — shared by `npm start`, `npm run dev`, Docker (bind mount), and the desktop app when it uses the same data dir
|
||||
- **Canonical (default):** `~/.claude/agenthub/dashboard.db` — shared by `npm start`, `npm run dev`, and the desktop app when it uses the same data dir
|
||||
- **Override:** set `DASHBOARD_DATA_DIR` (directory) or `DASHBOARD_DB_PATH` (file path) for tests or custom deployments
|
||||
- **Legacy:** repo-local `./data/dashboard.db` is migrated into the canonical location on first launch (see `server/db.js`)
|
||||
- **Pre-rebrand:** an install that predates AgentHub keeps its data in `~/.claude/agent-dashboard/`. Server startup renames that directory to `~/.claude/agenthub/` once, before anything opens the database (`migrateLegacyDataDir()` in `server/lib/claude-home.js`). It is a same-filesystem rename, so it is instant even at multiple gigabytes and never leaves two copies. If the rename fails, or `DASHBOARD_DATA_DIR` is set, nothing moves and the old directory keeps being used.
|
||||
|
||||
@@ -10,7 +10,6 @@ Enterprise deployment strategies for Agent Dashboard across development, staging
|
||||
- [Deployment Architecture](#deployment-architecture)
|
||||
- [Local Development](#local-development)
|
||||
- [Production Deployment](#production-deployment)
|
||||
- [Docker Deployment](#docker-deployment)
|
||||
- [Cloud Deployment](#cloud-deployment)
|
||||
- [Process Management](#process-management)
|
||||
- [Monitoring & Logging](#monitoring--logging)
|
||||
@@ -26,7 +25,6 @@ Enterprise deployment strategies for Agent Dashboard across development, staging
|
||||
Agent Dashboard supports multiple deployment modes:
|
||||
|
||||
- **Local Development** - Hot reload for rapid iteration
|
||||
- **Docker** - Containerized deployment with Docker/Podman
|
||||
- **PM2** - Process management for production
|
||||
- **Systemd** - System service on Linux
|
||||
- **Cloud** - Deploy to AWS, Azure, GCP, or other cloud providers
|
||||
@@ -35,7 +33,6 @@ Agent Dashboard supports multiple deployment modes:
|
||||
graph TB
|
||||
subgraph "Deployment Modes"
|
||||
Dev[Local Development<br/>npm run dev]
|
||||
Docker[Docker Container<br/>docker compose up]
|
||||
PM2[PM2 Process Manager<br/>pm2 start]
|
||||
Systemd[Systemd Service<br/>systemctl start]
|
||||
Cloud[Cloud Platform<br/>Kubernetes, ECS, etc.]
|
||||
@@ -48,8 +45,6 @@ graph TB
|
||||
end
|
||||
|
||||
Dev --> DevEnv
|
||||
Docker --> DevEnv
|
||||
Docker --> StagingEnv
|
||||
PM2 --> ProdEnv
|
||||
Systemd --> ProdEnv
|
||||
Cloud --> ProdEnv
|
||||
@@ -260,118 +255,14 @@ CODEX_HOME= # where Codex keeps sessions/ (default ~
|
||||
LOG_LEVEL=info # debug | info | warn | error
|
||||
```
|
||||
|
||||
> **Reverse-proxy / Docker exposure:** the app binds loopback, so publish it to a
|
||||
> **Reverse-proxy / container exposure:** the app binds loopback, so publish it to a
|
||||
> network only through a proxy you control that adds TLS + auth, or set
|
||||
> `DASHBOARD_HOST=0.0.0.0` **with** `DASHBOARD_TOKEN`. A `-p 4820:4820` Docker
|
||||
> mapping assumes a trusted host network — do not expose it publicly without a
|
||||
> `DASHBOARD_HOST=0.0.0.0` **with** `DASHBOARD_TOKEN`. A published `4820` port
|
||||
> assumes a trusted host network — do not expose it publicly without a
|
||||
> token and a proxy.
|
||||
|
||||
---
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
### Docker Compose (Recommended)
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
agenthub:
|
||||
build: .
|
||||
ports:
|
||||
- "4820:4820"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=4820
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:4820/api/sessions"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
```
|
||||
|
||||
### Build & Run
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
docker compose build
|
||||
|
||||
# Start container
|
||||
docker compose up -d
|
||||
|
||||
# View logs
|
||||
docker compose logs -f
|
||||
|
||||
# Stop container
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Multi-Stage Dockerfile
|
||||
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json ./
|
||||
COPY client/package*.json ./client/
|
||||
RUN npm ci && cd client && npm ci
|
||||
|
||||
# Build client
|
||||
COPY client ./client
|
||||
RUN cd client && npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built artifacts
|
||||
COPY --from=builder /app/client/dist ./client/dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY server ./server
|
||||
COPY package.json ./
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
EXPOSE 4820
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:4820/api/sessions', (res) => process.exit(res.statusCode === 200 ? 0 : 1))"
|
||||
|
||||
CMD ["node", "server/index.js"]
|
||||
```
|
||||
|
||||
### Container Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Docker Host"
|
||||
subgraph "Container"
|
||||
App[Node.js App<br/>:4820]
|
||||
Volume[Volume Mount<br/>/app/data]
|
||||
end
|
||||
end
|
||||
|
||||
Host[Host Filesystem<br/>./data] -->|Bind Mount| Volume
|
||||
App --> Volume
|
||||
|
||||
Client[External Clients] -->|Port 4820| App
|
||||
|
||||
style App fill:#2496ED,color:#fff
|
||||
style Volume fill:#FFA500
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cloud Deployment
|
||||
|
||||
### AWS (Elastic Beanstalk)
|
||||
@@ -395,6 +286,9 @@ eb open
|
||||
|
||||
### Kubernetes
|
||||
|
||||
The repo ships no container image or `Dockerfile` — build your own from
|
||||
`npm ci && npm run build` and push it to your registry, then reference it here.
|
||||
|
||||
```yaml
|
||||
# k8s/deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
@@ -603,19 +497,16 @@ graph TB
|
||||
subgraph "Log Aggregation"
|
||||
Stdout --> PM2[PM2 Logs]
|
||||
Stdout --> Systemd[Systemd Journal]
|
||||
Stdout --> Docker[Docker Logs]
|
||||
end
|
||||
|
||||
subgraph "Monitoring Tools"
|
||||
PM2 --> File[Log Files]
|
||||
Systemd --> Journalctl[journalctl]
|
||||
Docker --> DockerLogs[docker logs]
|
||||
end
|
||||
|
||||
subgraph "Analysis"
|
||||
File --> Splunk[Splunk/ELK]
|
||||
Journalctl --> Splunk
|
||||
DockerLogs --> Splunk
|
||||
end
|
||||
|
||||
style App fill:#000000,color:#fff
|
||||
@@ -912,7 +803,7 @@ NODE_ENV=development node server/index.js
|
||||
|
||||
This deployment guide covers:
|
||||
|
||||
- ✅ **Multiple deployment modes** - Local, Docker, PM2, Systemd, Cloud
|
||||
- ✅ **Multiple deployment modes** - Local, PM2, Systemd, Cloud
|
||||
- ✅ **Production best practices** - Environment variables, health checks, logging
|
||||
- ✅ **Process management** - PM2, systemd service files
|
||||
- ✅ **Monitoring & logging** - Metrics endpoint, log aggregation
|
||||
|
||||
@@ -578,8 +578,8 @@ function sendToServer(hookType, payload) {
|
||||
> Each server appends its `{port, pid, startedAt, dataDir}` entry on
|
||||
> startup and removes it on a clean shutdown. The handler **prunes any
|
||||
> entry whose PID is no longer alive** and POSTs the hook payload to one
|
||||
> port per **unique SQLite data directory** (lowest port wins when Docker
|
||||
> and `npm run dev` share `~/.claude/agenthub`).
|
||||
> port per **unique SQLite data directory** (lowest port wins when two
|
||||
> instances share `~/.claude/agenthub`).
|
||||
> 3. If neither yields a target, the handler falls back to `4820`.
|
||||
>
|
||||
> Dashboards with **different** databases (e.g. the packaged desktop app using
|
||||
|
||||
@@ -9,13 +9,13 @@ Every plugin is powered by the local Agent Monitor REST API at `http://localhost
|
||||
### Add the marketplace
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add Smartgift-AI/Claude-Code-Monitor
|
||||
claude plugin marketplace add https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git
|
||||
```
|
||||
|
||||
### Install the dashboard itself
|
||||
|
||||
```bash
|
||||
claude plugin install agenthub@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub@agenthub
|
||||
```
|
||||
|
||||
That is the whole install: no clone, no `npm run setup`, no `npm run install-hooks`, no manual `npm start`. See [The `agenthub` plugin](#the-agenthub-plugin) below for what it does on first session start.
|
||||
@@ -39,16 +39,16 @@ marketplace name, not per source).
|
||||
### Install a focused plugin
|
||||
|
||||
```bash
|
||||
claude plugin install agenthub-analytics@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-cost-guard@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-productivity@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-devtools@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-insights@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-sessions@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-workflows@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-quality@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-config@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-dashboard@claude-code-agent-monitor-plugins
|
||||
claude plugin install agenthub-analytics@agenthub
|
||||
claude plugin install agenthub-cost-guard@agenthub
|
||||
claude plugin install agenthub-productivity@agenthub
|
||||
claude plugin install agenthub-devtools@agenthub
|
||||
claude plugin install agenthub-insights@agenthub
|
||||
claude plugin install agenthub-sessions@agenthub
|
||||
claude plugin install agenthub-workflows@agenthub
|
||||
claude plugin install agenthub-quality@agenthub
|
||||
claude plugin install agenthub-config@agenthub
|
||||
claude plugin install agenthub-dashboard@agenthub
|
||||
```
|
||||
|
||||
### Or install locally during development
|
||||
@@ -250,7 +250,7 @@ single port.
|
||||
|
||||
### Uninstalling
|
||||
|
||||
`claude plugin uninstall agenthub@claude-code-agent-monitor-plugins` only removes
|
||||
`claude plugin uninstall agenthub@agenthub` only removes
|
||||
the plugin from the enabled list — verified against a real install/uninstall,
|
||||
not assumed. It does **not**:
|
||||
|
||||
@@ -545,6 +545,13 @@ my-plugin/
|
||||
|
||||
Structure is validated by `server/__tests__/plugins-marketplace.test.js`, which enforces the marketplace↔directory bijection, `plugin.json` shape, name/dir agreement, and required frontmatter on every agent / skill / command.
|
||||
|
||||
Two of those shape rules exist because getting them wrong makes a plugin **listed but uninstallable**, with no other symptom — the marketplace loads fine and only `claude plugin install` fails:
|
||||
|
||||
- A subdirectory entry declares **`"source": "./plugins/<name>"`**, never `"path"`. `path` is not a source key the CLI accepts (`This plugin's marketplace entry is invalid: source: Invalid input`).
|
||||
- A `plugin.json`'s **`repository` is a string**, not npm's `{ "type", "url" }` object (`Validation errors: repository: Invalid input`).
|
||||
|
||||
`server/__tests__/repo-links.test.js` guards the link side: no GitHub `owner/repo` marketplace shorthand (it resolves to github.com only — this repo is on Gitea, so the shorthand cannot work and no `GH_HOST`-style env var redirects it), no `raw.githubusercontent.com` / `*.github.io` / GitHub `/blob/<branch>/` URLs, no retired repository name, and no stale committed `openapi.yaml`.
|
||||
|
||||
### Testing locally
|
||||
|
||||
```bash
|
||||
@@ -555,20 +562,20 @@ claude --plugin-dir /path/to/my-plugin # then use /my-plugin:my-skill some arg
|
||||
|
||||
### Dashboard not reachable
|
||||
```bash
|
||||
cd /path/to/Claude-Code-Agent-Monitor
|
||||
cd /path/to/AgentHub
|
||||
npm start # or: npm run dev
|
||||
```
|
||||
|
||||
### Hooks not installed
|
||||
```bash
|
||||
cd /path/to/Claude-Code-Agent-Monitor
|
||||
cd /path/to/AgentHub
|
||||
npm run setup
|
||||
```
|
||||
|
||||
### Plugin not found
|
||||
```bash
|
||||
claude plugin marketplace list
|
||||
claude plugin marketplace add Smartgift-AI/Claude-Code-Monitor
|
||||
claude plugin marketplace add https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -429,7 +429,7 @@ This documentation covers:
|
||||
|
||||
- ✅ **Complete architecture** - Frontend, backend, database, integrations
|
||||
- ✅ **API reference** - REST endpoints, WebSocket events
|
||||
- ✅ **Deployment guides** - Docker, PM2, systemd, cloud
|
||||
- ✅ **Deployment guides** - PM2, systemd, cloud
|
||||
- ✅ **Performance tuning** - Database, server, client optimizations
|
||||
- ✅ **Integration guides** - Hooks, MCP, custom clients
|
||||
- ✅ **Internationalization** - Language resources, switching flow, locale formatting, rollout checklist
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="100%" stop-color="#818cf8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#a5b4fc" stop-opacity="0.9"/>
|
||||
<stop offset="100%" stop-color="#c7d2fe" stop-opacity="0.6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Hexagon background -->
|
||||
<polygon points="16,2 28,9 28,23 16,30 4,23 4,9" fill="url(#bg)" rx="2"/>
|
||||
<!-- Center node -->
|
||||
<circle cx="16" cy="16" r="3" fill="white" opacity="0.95"/>
|
||||
<!-- Three connector lines radiating outward -->
|
||||
<line x1="16" y1="13" x2="16" y2="7" stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
|
||||
<line x1="18.6" y1="17.5" x2="24" y2="20.5" stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
|
||||
<line x1="13.4" y1="17.5" x2="8" y2="20.5" stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
|
||||
<!-- Outer nodes -->
|
||||
<circle cx="16" cy="6" r="1.8" fill="url(#glow)"/>
|
||||
<circle cx="24.5" cy="21" r="1.8" fill="url(#glow)"/>
|
||||
<circle cx="7.5" cy="21" r="1.8" fill="url(#glow)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,109 +0,0 @@
|
||||
/**
|
||||
* @file fonts.css
|
||||
* @description Self-hosted Inter + JetBrains Mono @font-face declarations — no Google Fonts dependency.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
/* Inter */
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("inter-latin-400-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("inter-latin-500-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("inter-latin-600-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("inter-latin-700-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url("inter-latin-800-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("inter-latin-900-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* JetBrains Mono */
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("jetbrains-mono-latin-400-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("jetbrains-mono-latin-500-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("jetbrains-mono-latin-600-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("jetbrains-mono-latin-700-normal.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"id": "claude-code-agent-monitor-landing",
|
||||
"name": "AgentHub",
|
||||
"short_name": "CC Monitor",
|
||||
"description": "Local-first monitoring dashboard for Claude Code sessions.",
|
||||
"author": "Nguyễn Ngọc Trí Vĩ",
|
||||
"start_url": ".",
|
||||
"scope": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#6366f1",
|
||||
"background_color": "#07070b",
|
||||
"orientation": "any",
|
||||
"categories": ["developer-tools", "productivity"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
|
||||
<defs>
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0d0d16"/>
|
||||
<stop offset="100%" stop-color="#12121e"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hexGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="100%" stop-color="#818cf8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="nodeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#a5b4fc" stop-opacity="0.9"/>
|
||||
<stop offset="100%" stop-color="#c7d2fe" stop-opacity="0.6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="titleGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#e2e2f0"/>
|
||||
<stop offset="55%" stop-color="#a5b4fc"/>
|
||||
<stop offset="100%" stop-color="#818cf8"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="glow1" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.18"/>
|
||||
<stop offset="100%" stop-color="#6366f1" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="glow2" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#10b981" stop-opacity="0.10"/>
|
||||
<stop offset="100%" stop-color="#10b981" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<filter id="blur1">
|
||||
<feGaussianBlur stdDeviation="40"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="630" fill="url(#bgGrad)"/>
|
||||
|
||||
<!-- Ambient glow blobs -->
|
||||
<ellipse cx="200" cy="200" rx="340" ry="280" fill="url(#glow1)" filter="url(#blur1)"/>
|
||||
<ellipse cx="1050" cy="480" rx="280" ry="220" fill="url(#glow2)" filter="url(#blur1)"/>
|
||||
|
||||
<!-- Grid dots (subtle background texture) -->
|
||||
<g opacity="0.07" fill="#6366f1">
|
||||
<circle cx="80" cy="80" r="1.5"/><circle cx="160" cy="80" r="1.5"/><circle cx="240" cy="80" r="1.5"/><circle cx="320" cy="80" r="1.5"/>
|
||||
<circle cx="80" cy="160" r="1.5"/><circle cx="160" cy="160" r="1.5"/><circle cx="240" cy="160" r="1.5"/><circle cx="320" cy="160" r="1.5"/>
|
||||
<circle cx="80" cy="240" r="1.5"/><circle cx="160" cy="240" r="1.5"/><circle cx="240" cy="240" r="1.5"/><circle cx="320" cy="240" r="1.5"/>
|
||||
<circle cx="900" cy="400" r="1.5"/><circle cx="980" cy="400" r="1.5"/><circle cx="1060" cy="400" r="1.5"/><circle cx="1140" cy="400" r="1.5"/>
|
||||
<circle cx="900" cy="480" r="1.5"/><circle cx="980" cy="480" r="1.5"/><circle cx="1060" cy="480" r="1.5"/><circle cx="1140" cy="480" r="1.5"/>
|
||||
<circle cx="900" cy="560" r="1.5"/><circle cx="980" cy="560" r="1.5"/><circle cx="1060" cy="560" r="1.5"/><circle cx="1140" cy="560" r="1.5"/>
|
||||
</g>
|
||||
|
||||
<!-- Large background hex (decorative) -->
|
||||
<polygon points="980,120 1120,200 1120,360 980,440 840,360 840,200"
|
||||
fill="none" stroke="#6366f1" stroke-width="1" opacity="0.12"/>
|
||||
<polygon points="980,150 1095,215 1095,345 980,410 865,345 865,215"
|
||||
fill="none" stroke="#6366f1" stroke-width="0.5" opacity="0.08"/>
|
||||
|
||||
<!-- Left-side decorative connection lines -->
|
||||
<line x1="60" y1="560" x2="200" y2="460" stroke="#6366f1" stroke-width="1" opacity="0.15"/>
|
||||
<line x1="200" y1="460" x2="380" y2="540" stroke="#6366f1" stroke-width="1" opacity="0.15"/>
|
||||
<circle cx="60" cy="560" r="3" fill="#6366f1" opacity="0.3"/>
|
||||
<circle cx="200" cy="460" r="3" fill="#6366f1" opacity="0.3"/>
|
||||
<circle cx="380" cy="540" r="3" fill="#6366f1" opacity="0.3"/>
|
||||
|
||||
<!-- ── Main icon (hex + nodes) centered-left ── -->
|
||||
<g transform="translate(128, 265)">
|
||||
<!-- Hex -->
|
||||
<polygon points="64,0 112,28 112,92 64,120 16,92 16,28" fill="url(#hexGrad)"/>
|
||||
<!-- Center node -->
|
||||
<circle cx="64" cy="60" r="11" fill="white" opacity="0.95"/>
|
||||
<!-- Lines -->
|
||||
<line x1="64" y1="49" x2="64" y2="18" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
|
||||
<line x1="73.5" y1="65.5" x2="97" y2="79" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
|
||||
<line x1="54.5" y1="65.5" x2="31" y2="79" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
|
||||
<!-- Outer nodes -->
|
||||
<circle cx="64" cy="14" r="7" fill="url(#nodeGrad)"/>
|
||||
<circle cx="100" cy="80" r="7" fill="url(#nodeGrad)"/>
|
||||
<circle cx="28" cy="80" r="7" fill="url(#nodeGrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- ── Typography ── -->
|
||||
<!-- Title -->
|
||||
<text x="290" y="298"
|
||||
font-family="'Inter', 'Helvetica Neue', Arial, sans-serif"
|
||||
font-size="72" font-weight="800" letter-spacing="-2"
|
||||
fill="url(#titleGrad)">Agent Dashboard</text>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<text x="292" y="355"
|
||||
font-family="'Inter', 'Helvetica Neue', Arial, sans-serif"
|
||||
font-size="26" font-weight="400"
|
||||
fill="#8888aa">Real-time monitoring platform for Claude Code agent activity</text>
|
||||
|
||||
<!-- Divider line -->
|
||||
<line x1="290" y1="385" x2="840" y2="385" stroke="#252538" stroke-width="1"/>
|
||||
|
||||
<!-- Tech pills -->
|
||||
<g transform="translate(290, 408)" font-family="'Inter', 'Helvetica Neue', Arial, sans-serif" font-size="15" font-weight="600">
|
||||
<!-- Node.js -->
|
||||
<rect x="0" y="0" width="88" height="28" rx="6" fill="#33993320" stroke="#33993340"/>
|
||||
<text x="44" y="19" text-anchor="middle" fill="#6dbf6d">Node.js 18+</text>
|
||||
<!-- React -->
|
||||
<rect x="100" y="0" width="82" height="28" rx="6" fill="#61dafb18" stroke="#61dafb30"/>
|
||||
<text x="141" y="19" text-anchor="middle" fill="#61dafb">React 18</text>
|
||||
<!-- TypeScript -->
|
||||
<rect x="194" y="0" width="100" height="28" rx="6" fill="#3178c618" stroke="#3178c630"/>
|
||||
<text x="244" y="19" text-anchor="middle" fill="#5a9fd4">TypeScript</text>
|
||||
<!-- SQLite -->
|
||||
<rect x="306" y="0" width="72" height="28" rx="6" fill="#003b5718" stroke="#005f8a30"/>
|
||||
<text x="342" y="19" text-anchor="middle" fill="#5bb8e0">SQLite</text>
|
||||
<!-- WebSocket -->
|
||||
<rect x="390" y="0" width="100" height="28" rx="6" fill="#10b98118" stroke="#10b98130"/>
|
||||
<text x="440" y="19" text-anchor="middle" fill="#34d399">WebSocket</text>
|
||||
<!-- MIT -->
|
||||
<rect x="502" y="0" width="62" height="28" rx="6" fill="#f59e0b18" stroke="#f59e0b30"/>
|
||||
<text x="533" y="19" text-anchor="middle" fill="#fbbf24">MIT</text>
|
||||
</g>
|
||||
|
||||
<!-- Bottom author strip -->
|
||||
<text x="290" y="580"
|
||||
font-family="'Inter', 'Helvetica Neue', Arial, sans-serif"
|
||||
font-size="18" font-weight="500"
|
||||
fill="#55556a">github.com/SmartGift · smartgift.vn</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.4 KiB |
@@ -3,15 +3,15 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Agent Dashboard for Claude Code API
|
||||
version: 1.4.6
|
||||
version: 1.5.0
|
||||
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
|
||||
contact:
|
||||
name: Nguyễn Ngọc Trí Vĩ
|
||||
email: vinnt@smartgift.vn
|
||||
url: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor
|
||||
url: https://git.smartgift.io.vn/Smartgift-AI/AgentHub
|
||||
externalDocs:
|
||||
description: Project documentation
|
||||
url: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor#readme
|
||||
url: https://git.smartgift.io.vn/Smartgift-AI/AgentHub#readme
|
||||
servers:
|
||||
- url: http://localhost:4820
|
||||
description: Local dashboard server (default)
|
||||
@@ -363,7 +363,7 @@ components:
|
||||
version:
|
||||
type: string
|
||||
description: Dashboard release version from package.json
|
||||
example: 1.4.6
|
||||
example: 1.5.0
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -4051,7 +4051,7 @@ components:
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
- /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
- /Users/dev/AgentHub
|
||||
- /Users/son/code/another-project
|
||||
sources:
|
||||
type: array
|
||||
@@ -4540,7 +4540,7 @@ paths:
|
||||
- id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
|
||||
name: Refactor pricing route + add cost endpoint
|
||||
status: active
|
||||
cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
cwd: /Users/dev/AgentHub
|
||||
model: claude-opus-4-20250514
|
||||
started_at: '2026-06-25T14:02:11.004Z'
|
||||
ended_at: null
|
||||
@@ -4554,7 +4554,7 @@ paths:
|
||||
- id: 1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9
|
||||
name: Fix flaky transcript pagination test
|
||||
status: completed
|
||||
cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
cwd: /Users/dev/AgentHub
|
||||
model: claude-sonnet-4-20250514
|
||||
started_at: '2026-06-24T09:12:00.000Z'
|
||||
ended_at: '2026-06-24T09:48:32.501Z'
|
||||
@@ -4583,7 +4583,7 @@ paths:
|
||||
example:
|
||||
id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
|
||||
name: Refactor pricing route + add cost endpoint
|
||||
cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
cwd: /Users/dev/AgentHub
|
||||
model: claude-opus-4-20250514
|
||||
metadata:
|
||||
source: hook
|
||||
@@ -4600,7 +4600,7 @@ paths:
|
||||
id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
|
||||
name: Refactor pricing route + add cost endpoint
|
||||
status: active
|
||||
cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
cwd: /Users/dev/AgentHub
|
||||
model: claude-opus-4-20250514
|
||||
started_at: '2026-06-25T14:02:11.004Z'
|
||||
ended_at: null
|
||||
@@ -4623,7 +4623,7 @@ paths:
|
||||
id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
|
||||
name: Refactor pricing route + add cost endpoint
|
||||
status: active
|
||||
cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
cwd: /Users/dev/AgentHub
|
||||
model: claude-opus-4-20250514
|
||||
started_at: '2026-06-25T14:02:11.004Z'
|
||||
ended_at: null
|
||||
@@ -4667,7 +4667,7 @@ paths:
|
||||
id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
|
||||
name: Refactor pricing route + add cost endpoint
|
||||
status: active
|
||||
cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
cwd: /Users/dev/AgentHub
|
||||
model: claude-opus-4-20250514
|
||||
started_at: '2026-06-25T14:02:11.004Z'
|
||||
ended_at: null
|
||||
@@ -4762,7 +4762,7 @@ paths:
|
||||
id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
|
||||
name: Refactor pricing route + add cost endpoint
|
||||
status: completed
|
||||
cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
cwd: /Users/dev/AgentHub
|
||||
model: claude-opus-4-20250514
|
||||
started_at: '2026-06-25T14:02:11.004Z'
|
||||
ended_at: '2026-06-25T15:07:44.220Z'
|
||||
@@ -6119,7 +6119,7 @@ paths:
|
||||
$ref: '#/components/schemas/SettingsInfoResponse'
|
||||
example:
|
||||
db:
|
||||
path: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor/server/data/agent-monitor.db
|
||||
path: /Users/dev/AgentHub/server/data/agent-monitor.db
|
||||
size: 48922624
|
||||
counts:
|
||||
sessions: 412
|
||||
@@ -6697,7 +6697,7 @@ paths:
|
||||
summary: Behind origin — update available
|
||||
value:
|
||||
git_repo: true
|
||||
repo_root: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
repo_root: /Users/dev/AgentHub
|
||||
update_available: true
|
||||
commits_behind: 4
|
||||
remote_ref: origin/master
|
||||
@@ -6709,7 +6709,7 @@ paths:
|
||||
summary: Up to date with origin
|
||||
value:
|
||||
git_repo: true
|
||||
repo_root: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
repo_root: /Users/dev/AgentHub
|
||||
update_available: false
|
||||
commits_behind: 0
|
||||
remote_ref: origin/master
|
||||
@@ -6754,7 +6754,7 @@ paths:
|
||||
summary: Behind origin — update available
|
||||
value:
|
||||
git_repo: true
|
||||
repo_root: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
repo_root: /Users/dev/AgentHub
|
||||
update_available: true
|
||||
commits_behind: 4
|
||||
remote_ref: origin/master
|
||||
@@ -6766,7 +6766,7 @@ paths:
|
||||
summary: Up to date with origin
|
||||
value:
|
||||
git_repo: true
|
||||
repo_root: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
repo_root: /Users/dev/AgentHub
|
||||
update_available: false
|
||||
commits_behind: 0
|
||||
remote_ref: origin/master
|
||||
@@ -10114,7 +10114,7 @@ paths:
|
||||
example:
|
||||
items:
|
||||
- kind: dashboard
|
||||
path: /Users/dev/Claude-Code-Agent-Monitor
|
||||
path: /Users/dev/AgentHub
|
||||
label: Dashboard server
|
||||
- kind: home
|
||||
path: /Users/dev
|
||||
@@ -10965,7 +10965,7 @@ paths:
|
||||
$ref: '#/components/schemas/SessionsFacetsResponse'
|
||||
example:
|
||||
cwds:
|
||||
- /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
|
||||
- /Users/dev/AgentHub
|
||||
- /Users/son/code/another-project
|
||||
sources:
|
||||
- local
|
||||
@@ -11633,4 +11633,4 @@ paths:
|
||||
errors: 0
|
||||
'400':
|
||||
description: Missing file, invalid JSON, or unrecognized export format
|
||||
x-issues-url: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/issues
|
||||
x-issues-url: https://git.smartgift.io.vn/Smartgift-AI/AgentHub/issues
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"author": "Nguyễn Ngọc Trí Vĩ",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
"url": "git+https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/issues"
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub/issues"
|
||||
},
|
||||
"license": "UNLICENSED",
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor#readme",
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub#readme",
|
||||
"keywords": [
|
||||
"claude",
|
||||
"claude-code",
|
||||
@@ -55,7 +55,7 @@
|
||||
"dev:client": "cd client && npm run dev",
|
||||
"build": "cd client && npm run build",
|
||||
"start": "node server/index.js",
|
||||
"setup": "npm install && (cd client && npm install) && (cd vscode-extension && npm install) && npm run link-cli",
|
||||
"setup": "npm install && (cd client && npm install) && npm run link-cli",
|
||||
"update:pull-setup": "git pull --ff-only && npm run setup",
|
||||
"install-hooks": "node scripts/install-hooks.js",
|
||||
"seed": "node scripts/seed.js",
|
||||
@@ -68,9 +68,7 @@
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"openapi:yaml": "node scripts/generate-openapi-yaml.js",
|
||||
"link-cli": "npm link || node -e \"console.warn('agenthub CLI not linked globally (no permission?). Link manually with: npm link')\"",
|
||||
"docker:up": "docker compose up -d --build",
|
||||
"docker:down": "docker compose down"
|
||||
"link-cli": "npm link || node -e \"console.warn('agenthub CLI not linked globally (no permission?). Link manually with: npm link')\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@lydell/node-pty": "^1.2.0-beta.15",
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"analytics",
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": ["config", "memory", "governance", "audit", "mcp", "hooks", "claude-code"],
|
||||
"categories": ["config", "governance", "monitoring"]
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": ["budget", "cost", "forecast", "alerts", "spend", "pricing", "claude-code"],
|
||||
"categories": ["cost", "monitoring", "alerts"]
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": ["dashboard", "connector", "real-time", "monitoring", "claude-code"],
|
||||
"categories": ["integration", "monitoring"]
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": ["devtools", "debugging", "diagnostics", "export", "health-check", "claude-code"],
|
||||
"categories": ["devtools", "debugging", "diagnostics"]
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"insights",
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": ["productivity", "standup", "reports", "sprint", "workflow", "claude-code"],
|
||||
"categories": ["productivity", "workflow", "reporting"]
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"reliability",
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": ["sessions", "search", "transcript", "replay", "timeline", "cleanup", "claude-code"],
|
||||
"categories": ["sessions", "monitoring", "devtools"]
|
||||
|
||||
@@ -6,11 +6,8 @@
|
||||
"name": "Nguyễn Ngọc Trí Vĩ",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
"repository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"workflows",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# https://smartgift.github.io/Claude-Code-Agent-Monitor/
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://smartgift.github.io/Claude-Code-Agent-Monitor/sitemap.xml
|
||||
@@ -15,7 +15,11 @@ const { createOpenApiSpec } = require("../server/openapi");
|
||||
|
||||
const OUT = path.join(__dirname, "..", "openapi.yaml");
|
||||
|
||||
function main() {
|
||||
/** The exact bytes `openapi.yaml` must contain for the current spec. Exported so
|
||||
* the drift test can compare against the committed file without duplicating the
|
||||
* dump options — a second copy of them would let the two definitions diverge,
|
||||
* which is the very staleness this file exists to prevent. */
|
||||
function render() {
|
||||
const spec = createOpenApiSpec();
|
||||
const body = yaml.dump(spec, {
|
||||
lineWidth: -1, // don't wrap long strings (keeps descriptions/examples intact)
|
||||
@@ -25,10 +29,15 @@ function main() {
|
||||
const header =
|
||||
"# DO NOT EDIT BY HAND. Generated from server/openapi.js via `npm run openapi:yaml`.\n" +
|
||||
"# This YAML mirrors the live spec served at GET /api/openapi.json.\n";
|
||||
fs.writeFileSync(OUT, header + body, "utf8");
|
||||
const stat = fs.statSync(OUT);
|
||||
const pathCount = Object.keys(spec.paths || {}).length;
|
||||
console.log(`Wrote ${OUT} (${pathCount} paths, ${stat.size} bytes).`);
|
||||
return { text: header + body, pathCount: Object.keys(spec.paths || {}).length };
|
||||
}
|
||||
|
||||
main();
|
||||
function main() {
|
||||
const { text, pathCount } = render();
|
||||
fs.writeFileSync(OUT, text, "utf8");
|
||||
console.log(`Wrote ${OUT} (${pathCount} paths, ${fs.statSync(OUT).size} bytes).`);
|
||||
}
|
||||
|
||||
if (require.main === module) main();
|
||||
|
||||
module.exports = { render, OUT };
|
||||
|
||||
@@ -89,7 +89,7 @@ function containerRefusalMessage() {
|
||||
"",
|
||||
" → Install hooks ON THE HOST instead:",
|
||||
" npm run install-hooks",
|
||||
" # or: node /path/to/Claude-Code-Agent-Monitor/scripts/install-hooks.js",
|
||||
" # or: node /path/to/AgentHub/scripts/install-hooks.js",
|
||||
"",
|
||||
" The host handler POSTs to http://localhost:4820, which the container already",
|
||||
" publishes — so a host-installed hook reaches the containerized dashboard.",
|
||||
|
||||
@@ -13,8 +13,6 @@ Enterprise-grade Node.js backend for Claude Code agent monitoring with real-time
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -1532,38 +1530,6 @@ pm2 start server/index.js --name agenthub
|
||||
sudo systemctl start agenthub
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
```dockerfile
|
||||
# Dockerfile (root of project)
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json ./
|
||||
COPY client/package*.json ./client/
|
||||
RUN npm ci --production && cd client && npm ci --production
|
||||
|
||||
# Build client
|
||||
COPY client ./client
|
||||
RUN cd client && npm run build
|
||||
|
||||
# Copy server
|
||||
COPY server ./server
|
||||
COPY data ./data
|
||||
|
||||
EXPOSE 4820
|
||||
|
||||
CMD ["node", "server/index.js"]
|
||||
```
|
||||
|
||||
```bash
|
||||
# Build and run
|
||||
docker build -t agenthub .
|
||||
docker run -p 127.0.0.1:4820:4820 -v "$HOME/.claude/agenthub:/app/data" agenthub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -185,10 +185,7 @@ describe("OpenAPI / Swagger", () => {
|
||||
// so the spec must not advertise one (a LICENSE url would 404).
|
||||
assert.equal(res.body.info.license, undefined);
|
||||
assert.equal(res.body["x-issues-url"], pkg.bugs.url);
|
||||
assert.match(
|
||||
res.body.info.contact.url,
|
||||
/git\.smartgift\.io\.vn\/Smartgift-AI\/Claude-Code-Monitor/
|
||||
);
|
||||
assert.match(res.body.info.contact.url, /git\.smartgift\.io\.vn\/Smartgift-AI\/AgentHub/);
|
||||
|
||||
for (const pathName of EXPECTED_API_PATHS) {
|
||||
assert.ok(res.body.paths[pathName], `Expected path ${pathName} to be documented`);
|
||||
|
||||
@@ -248,7 +248,7 @@ describe("legacy hook cleanup", () => {
|
||||
TMP_HOME,
|
||||
"plugins",
|
||||
"cache",
|
||||
"claude-code-agent-monitor-plugins",
|
||||
"agenthub",
|
||||
"agenthub",
|
||||
"abc123",
|
||||
"scripts",
|
||||
|
||||
@@ -79,16 +79,21 @@ describe("plugin marketplace", () => {
|
||||
|
||||
for (const entry of subdirEntries) {
|
||||
describe(`entry: ${entry.name}`, () => {
|
||||
it("has name, path, description, tags", () => {
|
||||
it("has name, source, description, tags", () => {
|
||||
assert.equal(typeof entry.name, "string");
|
||||
assert.equal(entry.path, `plugins/${entry.name}`);
|
||||
// `source`, not `path`: the `claude plugin install` CLI validates the
|
||||
// entry against a schema that only knows `source`, so a `path` entry
|
||||
// fails with "This plugin's marketplace entry is invalid: source:
|
||||
// Invalid input" — the plugin is listed but uninstallable.
|
||||
assert.equal(entry.source, `./plugins/${entry.name}`);
|
||||
assert.equal(entry.path, undefined, "`path` is not a source key the CLI accepts");
|
||||
assert.equal(typeof entry.description, "string");
|
||||
assert.ok(entry.description.length > 20);
|
||||
assert.ok(Array.isArray(entry.tags) && entry.tags.length > 0);
|
||||
});
|
||||
|
||||
it("path exists on disk", () => {
|
||||
assert.ok(fs.existsSync(path.join(REPO_ROOT, entry.path)));
|
||||
it("source exists on disk", () => {
|
||||
assert.ok(fs.existsSync(path.join(REPO_ROOT, entry.source)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @file repo-links.test.js
|
||||
* @description Guards the two staleness classes that shipped broken commands to
|
||||
* users for weeks: (1) GitHub-shaped references in a repo that is self-hosted on
|
||||
* Gitea — the `owner/repo` marketplace shorthand resolves to github.com ONLY
|
||||
* (verified against the CLI: it rejects `host/owner/repo` outright and ignores
|
||||
* GH_HOST / GITHUB_HOST / CLAUDE_CODE_MARKETPLACE_URL), plus raw.githubusercontent
|
||||
* / *.github.io / `/blob/<branch>` URLs that 404 against Gitea; and (2) a stale
|
||||
* committed `openapi.yaml`, which had silently drifted a whole minor version
|
||||
* behind package.json. Pure file reads plus one in-process spec render.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { describe, it } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
// Only git-tracked text files: keeps node_modules, client/dist and every
|
||||
// build artifact out without hand-maintaining an ignore list.
|
||||
const TRACKED_EXT = /\.(md|json|js|cjs|mjs|ts|tsx|sh|ya?ml|html|css)$/;
|
||||
|
||||
// This file states every banned pattern verbatim — in the rules below and in
|
||||
// the comments explaining them — so it must exclude itself or it fails on its
|
||||
// own source. Derived from __filename rather than hardcoded so a rename can't
|
||||
// silently re-arm the self-report.
|
||||
const SELF = path.relative(REPO_ROOT, __filename);
|
||||
|
||||
function trackedFiles() {
|
||||
// No GIT_* scrub here, deliberately: under this repo's pre-commit hook
|
||||
// GIT_INDEX_FILE points at the index being committed, so `ls-files` lints
|
||||
// exactly what is about to land. Scrubbing it would lint the previous commit
|
||||
// instead and let a newly staged violation through.
|
||||
const out = execFileSync("git", ["ls-files", "-z"], {
|
||||
cwd: REPO_ROOT,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 20_000_000,
|
||||
});
|
||||
return (
|
||||
out
|
||||
.split("\0")
|
||||
.filter((f) => f && TRACKED_EXT.test(f))
|
||||
.filter((f) => f !== SELF)
|
||||
// Dated design docs and plans record what was true when written; rewriting
|
||||
// history to satisfy a lint would be a lie about the decision record.
|
||||
.filter((f) => !f.startsWith("docs/superpowers/"))
|
||||
// Generated from server/openapi-extra/* (which IS linted): the drift test
|
||||
// below covers it, so linting both would only double-report one fix.
|
||||
.filter((f) => f !== "openapi.yaml" && !f.endsWith("package-lock.json"))
|
||||
);
|
||||
}
|
||||
|
||||
// Each rule: what to look for, and what to do instead. A `skip` pattern is
|
||||
// tested against the MATCHED TEXT ONLY, never the surrounding line — a skip
|
||||
// written against nearby prose silently never fires. Add one only when the
|
||||
// exception is visible inside the match itself.
|
||||
const RULES = [
|
||||
{
|
||||
name: "GitHub owner/repo marketplace shorthand",
|
||||
// `claude plugin marketplace add Smartgift-AI/AgentHub` — resolves to
|
||||
// github.com and fails, because this repo lives on Gitea.
|
||||
re: /plugin marketplace add\s+(?!https?:\/\/)(?!\.{0,2}\/)(?!["'$])[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*/g,
|
||||
fix: "use the full https:// clone URL — the shorthand is GitHub-only",
|
||||
// Anthropic's own marketplace really does live on GitHub.
|
||||
skip: [/anthropics\/claude-plugins-official/],
|
||||
},
|
||||
{
|
||||
name: "GitHub URL for a Smartgift repo",
|
||||
re: /(?:https?:\/\/)?(?:www\.)?github\.com\/[Ss]martgift[\w-]*\/[\w.-]+/g,
|
||||
fix: "point at git.smartgift.io.vn",
|
||||
skip: [],
|
||||
},
|
||||
{
|
||||
name: "raw.githubusercontent.com",
|
||||
// Our repos are not on GitHub, so every raw URL 404s. Same-origin paths or
|
||||
// a Gitea /raw/branch/<ref>/ URL instead.
|
||||
re: /raw\.githubusercontent\.com\/[\w-]+\/[\w.-]+/g,
|
||||
fix: "use a same-origin path, or a Gitea /raw/branch/<ref>/ URL",
|
||||
skip: [],
|
||||
},
|
||||
{
|
||||
name: "GitHub Pages host for a Smartgift site",
|
||||
re: /[\w-]*smartgift[\w-]*\.github\.io/gi,
|
||||
fix: "there is no GitHub Pages site — link the Gitea docs directory",
|
||||
skip: [],
|
||||
},
|
||||
{
|
||||
name: "GitHub /blob/<branch>/ path syntax on a Gitea URL",
|
||||
// Gitea serves files at /src/branch/<ref>/, never /blob/<ref>/.
|
||||
re: /git\.smartgift\.io\.vn\/[^\s"')\]]*\/blob\/[\w.-]+\//g,
|
||||
fix: "Gitea serves files at /src/branch/<ref>/",
|
||||
skip: [],
|
||||
},
|
||||
{
|
||||
name: "retired repository name",
|
||||
// Gitea 301-redirects the old name, so these never fail loudly — they just
|
||||
// rot until someone turns the redirect off.
|
||||
re: /Claude-Code-Monitor|Claude-Code-Agent-Monitor/g,
|
||||
fix: "the repository is Smartgift-AI/AgentHub",
|
||||
skip: [],
|
||||
},
|
||||
];
|
||||
|
||||
describe("repo links and generated files", () => {
|
||||
const files = trackedFiles();
|
||||
|
||||
it("finds tracked text files to lint", () => {
|
||||
assert.ok(files.length > 50, `expected a populated file list, got ${files.length}`);
|
||||
});
|
||||
|
||||
for (const rule of RULES) {
|
||||
it(`no ${rule.name}`, () => {
|
||||
const hits = [];
|
||||
for (const rel of files) {
|
||||
const text = fs.readFileSync(path.join(REPO_ROOT, rel), "utf8");
|
||||
// A fresh regex per file: the /g rules carry lastIndex between calls.
|
||||
for (const m of text.matchAll(new RegExp(rule.re.source, rule.re.flags))) {
|
||||
if (rule.skip.some((s) => s.test(m[0]))) continue;
|
||||
const line = text.slice(0, m.index).split("\n").length;
|
||||
hits.push(`${rel}:${line}: ${m[0].trim()}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(hits, [], `${rule.fix}\n ${hits.join("\n ")}`);
|
||||
});
|
||||
}
|
||||
|
||||
it("committed openapi.yaml matches the generator output", () => {
|
||||
const { render, OUT } = require("../../scripts/generate-openapi-yaml");
|
||||
assert.equal(
|
||||
fs.readFileSync(OUT, "utf8"),
|
||||
render().text,
|
||||
"openapi.yaml is stale — run `npm run openapi:yaml` and commit the result"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,7 @@ function loadOrCreateVapidKeys() {
|
||||
const vapidKeys = loadOrCreateVapidKeys();
|
||||
|
||||
webpush.setVapidDetails(
|
||||
"https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
"https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
|
||||
vapidKeys.publicKey,
|
||||
vapidKeys.privateKey
|
||||
);
|
||||
@@ -94,9 +94,12 @@ async function sendPushToAll(db, title, body) {
|
||||
const payload = JSON.stringify({
|
||||
title,
|
||||
body,
|
||||
icon: "https://raw.githubusercontent.com/Smartgift-AI/Claude-Code-Monitor/main/client/public/favicon.ico",
|
||||
badge:
|
||||
"https://raw.githubusercontent.com/Smartgift-AI/Claude-Code-Monitor/main/client/public/favicon.ico",
|
||||
// Same-origin path, resolved by the service worker against its own scope
|
||||
// (client/public/sw.js spreads these straight into showNotification). An
|
||||
// absolute forge/raw URL would 404 — this repo is self-hosted on Gitea and
|
||||
// ships favicon.svg, not favicon.ico.
|
||||
icon: "/favicon.svg",
|
||||
badge: "/favicon.svg",
|
||||
silent: false,
|
||||
sound: "default",
|
||||
});
|
||||
|
||||
@@ -231,8 +231,8 @@ function resolveAllDashboardPorts() {
|
||||
/**
|
||||
* Ports that should receive hook POSTs. When several live servers share the
|
||||
* same SQLite data directory, only the lowest port per directory is returned
|
||||
* so parallel instances (Docker + dev, two terminals on the same DB) never
|
||||
* double-ingest events.
|
||||
* so parallel instances (two terminals on the same DB) never double-ingest
|
||||
* events.
|
||||
*
|
||||
* @returns {number[]}
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,7 @@ const { execFile } = require("child_process");
|
||||
const DEFAULT_ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
// This build tracks its OWN repository only: origin points at
|
||||
// git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor. "upstream" is
|
||||
// git.smartgift.io.vn/Smartgift-AI/AgentHub. "upstream" is
|
||||
// deliberately absent from the priority list — a stray upstream remote must
|
||||
// never make the update checker report commits from somebody else's repo.
|
||||
const REMOTE_PRIORITY = ["origin"];
|
||||
|
||||
@@ -40,10 +40,7 @@ const schemas = {
|
||||
description:
|
||||
"Distinct, non-empty session working directories (the `cwd` column), sorted ascending. Powers the cwd filter dropdown.",
|
||||
items: { type: "string" },
|
||||
example: [
|
||||
"/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
"/Users/son/code/another-project",
|
||||
],
|
||||
example: ["/Users/dev/AgentHub", "/Users/son/code/another-project"],
|
||||
},
|
||||
sources: {
|
||||
type: "array",
|
||||
@@ -527,10 +524,7 @@ const paths = {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/SessionsFacetsResponse" },
|
||||
example: {
|
||||
cwds: [
|
||||
"/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
"/Users/son/code/another-project",
|
||||
],
|
||||
cwds: ["/Users/dev/AgentHub", "/Users/son/code/another-project"],
|
||||
sources: ["local", "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -97,7 +97,7 @@ const GENERIC_WEBHOOK_TARGET_EXAMPLE = {
|
||||
|
||||
const SETTINGS_INFO_EXAMPLE = {
|
||||
db: {
|
||||
path: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor/server/data/agent-monitor.db",
|
||||
path: "/Users/dev/AgentHub/server/data/agent-monitor.db",
|
||||
size: 48922624,
|
||||
counts: {
|
||||
sessions: 412,
|
||||
@@ -351,7 +351,7 @@ const IMPORT_UPLOAD_EXAMPLE = {
|
||||
|
||||
const UPDATE_STATUS_AVAILABLE_EXAMPLE = {
|
||||
git_repo: true,
|
||||
repo_root: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
repo_root: "/Users/dev/AgentHub",
|
||||
update_available: true,
|
||||
commits_behind: 4,
|
||||
remote_ref: "origin/master",
|
||||
@@ -363,7 +363,7 @@ const UPDATE_STATUS_AVAILABLE_EXAMPLE = {
|
||||
|
||||
const UPDATE_STATUS_UP_TO_DATE_EXAMPLE = {
|
||||
git_repo: true,
|
||||
repo_root: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
repo_root: "/Users/dev/AgentHub",
|
||||
update_available: false,
|
||||
commits_behind: 0,
|
||||
remote_ref: "origin/master",
|
||||
|
||||
@@ -43,7 +43,7 @@ const exampleSession = {
|
||||
id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
name: "Refactor pricing route + add cost endpoint",
|
||||
status: "active",
|
||||
cwd: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
cwd: "/Users/dev/AgentHub",
|
||||
model: "claude-opus-4-20250514",
|
||||
started_at: "2026-06-25T14:02:11.004Z",
|
||||
ended_at: null,
|
||||
@@ -60,7 +60,7 @@ const exampleCompletedSession = {
|
||||
id: "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9",
|
||||
name: "Fix flaky transcript pagination test",
|
||||
status: "completed",
|
||||
cwd: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
cwd: "/Users/dev/AgentHub",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
started_at: "2026-06-24T09:12:00.000Z",
|
||||
ended_at: "2026-06-24T09:48:32.501Z",
|
||||
@@ -176,7 +176,7 @@ const paths = {
|
||||
example: {
|
||||
id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
name: "Refactor pricing route + add cost endpoint",
|
||||
cwd: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
cwd: "/Users/dev/AgentHub",
|
||||
model: "claude-opus-4-20250514",
|
||||
metadata: { source: "hook", git_branch: "feat/spend-budgets" },
|
||||
},
|
||||
|
||||
@@ -875,7 +875,7 @@ const paths = {
|
||||
items: [
|
||||
{
|
||||
kind: "dashboard",
|
||||
path: "/Users/dev/Claude-Code-Agent-Monitor",
|
||||
path: "/Users/dev/AgentHub",
|
||||
label: "Dashboard server",
|
||||
},
|
||||
{ kind: "home", path: "/Users/dev", label: "Home" },
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://smartgift.github.io/Claude-Code-Agent-Monitor/</loc>
|
||||
<lastmod>2026-07-29</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -52,7 +52,6 @@ i18n architecture: **Supported languages** list, `supportedLngs`, the 15 namespa
|
||||
- `INSTALL.md`, `SETUP.md` — install/run commands.
|
||||
- `CLAUDE.md`, `AGENTS.md` — agent working guides; update when commands, file locations, or workflows change.
|
||||
- `docs/README.md` — docs index; add a link when a new `docs/*.md` is created.
|
||||
- `vscode-extension/README.md`, `statusline/README.md` — surface-specific; update only when that surface changes.
|
||||
|
||||
## Consistency invariants
|
||||
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
# Claude Code Statusline
|
||||
|
||||
A color-coded statusline for Claude Code showing model, user, working directory, git branch, context window usage, token counts, and session cost.
|
||||
|
||||
## Preview
|
||||
|
||||
Example statusline with all segments visible in a git repo and 24% context window usage:
|
||||
| Segment | Color | Example |
|
||||
| ----------- | -------------------- | -------------------------------------------------- |
|
||||
| Model | Cyan | `Sonnet 4.6` |
|
||||
| User | Green | `nguyens6` |
|
||||
| CWD | Yellow | `~/agenthub/client` |
|
||||
| Git branch | Magenta | `main` (hidden outside git repos) |
|
||||
| Context bar | Green → Yellow → Red | `████████░░ 79%` |
|
||||
| Tokens | Green / Cyan / Dim | `3↑ 2↓ 156586c` (green `↑` in, cyan `↓` out, dim `c` cache reads) |
|
||||
| Cost (USD) | Green → Yellow → Red | `$0.4231` (session total — shown on API and subscription plans) |
|
||||
|
||||
Context bar color thresholds:
|
||||
|
||||
- **Green** — under 50% used
|
||||
- **Yellow** — 50–79% used
|
||||
- **Red** — 80%+ used
|
||||
|
||||
Cost color thresholds:
|
||||
|
||||
- **Green** — under $5
|
||||
- **Yellow** — $5–$20
|
||||
- **Red** — $20+
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.6+
|
||||
- Git (for branch detection)
|
||||
- Claude Code 2.x+
|
||||
|
||||
## Installation
|
||||
|
||||
**1. Copy both files into your Claude config directory:**
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
cp statusline.py ~/.claude/statusline.py
|
||||
cp statusline-command.sh ~/.claude/statusline-command.sh
|
||||
chmod +x ~/.claude/statusline-command.sh
|
||||
|
||||
# Windows (Git Bash)
|
||||
cp statusline.py "$HOME/.claude/statusline.py"
|
||||
cp statusline-command.sh "$HOME/.claude/statusline-command.sh"
|
||||
```
|
||||
|
||||
**2. Update the path in `statusline-command.sh`:**
|
||||
|
||||
Open `~/.claude/statusline-command.sh` and replace the path with your own home directory:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
PYTHONUTF8=1 python3 "/your/home/.claude/statusline.py"
|
||||
```
|
||||
|
||||
On Windows this looks like:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
PYTHONUTF8=1 python3 "C:/Users/YOUR_USERNAME/.claude/statusline.py"
|
||||
```
|
||||
|
||||
On macOS/Linux:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
python3 "$HOME/.claude/statusline.py"
|
||||
```
|
||||
|
||||
**3. Add to `~/.claude/settings.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "bash \"/path/to/home/.claude/statusline-command.sh\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Windows example:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "bash \"C:/Users/YOUR_USERNAME/.claude/statusline-command.sh\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
macOS/Linux example:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "bash \"/home/YOUR_USERNAME/.claude/statusline-command.sh\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. Restart Claude Code** — fully exit and relaunch. Claude Code does not hot-reload `settings.json`, so the new statusline will not show up until the process is restarted.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**The statusline still shows the default (e.g. `[Model] 📁 cwd`) after restarting.**
|
||||
|
||||
Claude Code resolves `statusLine` from multiple settings files, in this precedence (later wins):
|
||||
|
||||
1. `~/.claude/settings.json` (user, global)
|
||||
2. `~/.claude/settings.local.json` (user, local — **often overrides the global**)
|
||||
3. `<project>/.claude/settings.json` (project, shared)
|
||||
4. `<project>/.claude/settings.local.json` (project, local)
|
||||
|
||||
If any of those later files defines its own `statusLine` block, it **replaces** the global one — yours will never run. Grep for it:
|
||||
|
||||
```bash
|
||||
grep -l statusLine ~/.claude/settings*.json $(find . -maxdepth 3 -name 'settings*.json' -path '*.claude*' 2>/dev/null)
|
||||
```
|
||||
|
||||
For each file that defines a competing `statusLine`, either delete that block (to fall back to the global one) or point it at the same script:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "bash \"/path/to/home/.claude/statusline-command.sh\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**The statusline is blank / missing segments.**
|
||||
|
||||
Run the script manually with a sample payload to confirm it works:
|
||||
|
||||
```bash
|
||||
echo '{"model":{"display_name":"Sonnet 4.6"},"workspace":{"current_dir":"'"$HOME"'"},"context_window":{"used_percentage":25,"current_usage":{"input_tokens":1000,"output_tokens":500,"cache_read_input_tokens":200}},"cost":{"total_cost_usd":0.4231}}' \
|
||||
| sh ~/.claude/statusline-command.sh
|
||||
```
|
||||
|
||||
If nothing prints, check that `python3` is on your `PATH` and `~/.claude/statusline.py` is readable. The script always exits 0 by design so Claude Code never blocks — errors are silent, so test from the shell first.
|
||||
|
||||
## How It Works
|
||||
|
||||
Claude Code pipes a JSON object to the statusline command's stdin on each update. The script reads that JSON, extracts the relevant fields, and prints a color-coded string using ANSI escape codes.
|
||||
|
||||
Key fields used from the JSON payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": { "display_name": "Sonnet 4.6" },
|
||||
"workspace": { "current_dir": "C:\\Users\\..." },
|
||||
"context_window": {
|
||||
"used_percentage": 79,
|
||||
"current_usage": {
|
||||
"input_tokens": 3,
|
||||
"output_tokens": 2,
|
||||
"cache_read_input_tokens": 156586
|
||||
}
|
||||
},
|
||||
"cost": { "total_cost_usd": 0.4231 }
|
||||
}
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
Edit `statusline.py` directly to change colors, reorder segments, or remove ones you don't want. Each segment is clearly labeled with a comment.
|
||||
|
||||
Color constants at the top of the file:
|
||||
|
||||
```python
|
||||
CYAN = '\033[0;36m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[0;33m'
|
||||
MAGENTA = '\033[0;35m'
|
||||
RED = '\033[0;31m'
|
||||
DIM = '\033[2m'
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# @file statusline-command.sh — wrapper that Claude Code invokes for its custom statusline;
|
||||
# pipes the JSON event payload into statusline.py (update the path to match your install).
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
#
|
||||
PYTHONUTF8=1 python3 "C:/Users/nguyens6/.claude/statusline.py"
|
||||
# IMPORTANT:
|
||||
# Put the statusline.py file in the .claude folder in your user directory (e.g. C:/Users/nguyens6/.claude/statusline.py) and make sure to update the path in the above command if you place it somewhere else.
|
||||
# Change the above path to the location of your statusline.py file. You can also change the command to run a different script if you want to display something else in your status line.
|
||||
@@ -1,136 +0,0 @@
|
||||
"""
|
||||
statusline.py — Claude Code custom statusline renderer.
|
||||
|
||||
Reads a JSON event payload from stdin (emitted by Claude Code on every turn)
|
||||
and prints a single ANSI-colored status line to stdout. Renders the model
|
||||
display name, current user, working directory (with home collapsed to ``~``),
|
||||
active git branch, a color-coded context window usage bar (green / yellow /
|
||||
red thresholds), and token counts. Always exits 0 so it can never block
|
||||
Claude Code — missing fields are silently skipped.
|
||||
|
||||
Intended to be referenced from Claude Code's ``statusLine`` setting (see
|
||||
``statusline-command.sh`` for the wrapper) or installed to
|
||||
``~/.claude/statusline.py``.
|
||||
|
||||
:file: statusline/statusline.py
|
||||
:module: Claude-Code-Agent-Monitor.statusline
|
||||
:author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
:maintainer: Nguyễn Ngọc Trí Vĩ (a.k.a. David Nguyen, smartgift)
|
||||
:copyright: (c) 2026 Nguyễn Ngọc Trí Vĩ
|
||||
:license: MIT
|
||||
:repository: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor
|
||||
:requires: Python 3.6+
|
||||
:encoding: utf-8
|
||||
|
||||
@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
"""
|
||||
|
||||
__file_name__ = "statusline.py"
|
||||
__module__ = "Claude-Code-Agent-Monitor.statusline"
|
||||
__author__ = "Nguyễn Ngọc Trí Vĩ"
|
||||
__email__ = "vinnt@smartgift.vn"
|
||||
__maintainer__ = "Nguyễn Ngọc Trí Vĩ"
|
||||
__copyright__ = "Copyright (c) 2026 Nguyễn Ngọc Trí Vĩ"
|
||||
__license__ = "MIT"
|
||||
__version__ = "1.0.0"
|
||||
__status__ = "Production"
|
||||
__repository__ = "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor"
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
CYAN = '\033[0;36m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[0;33m'
|
||||
MAGENTA = '\033[0;35m'
|
||||
RED = '\033[0;31m'
|
||||
DIM = '\033[2m'
|
||||
RESET = '\033[0m'
|
||||
|
||||
raw = sys.stdin.read().strip()
|
||||
if not raw:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
parts = []
|
||||
|
||||
# Model
|
||||
model = (data.get('model') or {}).get('display_name', '')
|
||||
if model:
|
||||
parts.append(f"{CYAN}{model}{RESET}")
|
||||
|
||||
# User
|
||||
user = os.environ.get('USERNAME') or os.environ.get('USER', '')
|
||||
if user:
|
||||
parts.append(f"{GREEN}{user}{RESET}")
|
||||
|
||||
# CWD — strip home prefix
|
||||
cwd = (data.get('workspace') or {}).get('current_dir') or data.get('cwd', '')
|
||||
if cwd:
|
||||
home = os.path.expanduser('~') # C:\Users\nguyens6
|
||||
if cwd.startswith(home):
|
||||
cwd = '~' + cwd[len(home):].replace('\\', '/')
|
||||
else:
|
||||
cwd = cwd.replace('\\', '/')
|
||||
parts.append(f"{YELLOW}{cwd}{RESET}")
|
||||
|
||||
# Git branch
|
||||
git_dir = (data.get('workspace') or {}).get('current_dir') or data.get('cwd', '')
|
||||
if git_dir:
|
||||
try:
|
||||
branch = subprocess.check_output(
|
||||
['git', '-C', git_dir, '--no-optional-locks', 'symbolic-ref', '--short', 'HEAD'],
|
||||
stderr=subprocess.DEVNULL
|
||||
).decode().strip()
|
||||
except Exception:
|
||||
try:
|
||||
branch = subprocess.check_output(
|
||||
['git', '-C', git_dir, '--no-optional-locks', 'rev-parse', '--short', 'HEAD'],
|
||||
stderr=subprocess.DEVNULL
|
||||
).decode().strip()
|
||||
except Exception:
|
||||
branch = ''
|
||||
if branch:
|
||||
parts.append(f"{MAGENTA}{branch}{RESET}")
|
||||
|
||||
# Context bar
|
||||
ctx = data.get('context_window') or {}
|
||||
used_pct = ctx.get('used_percentage')
|
||||
if used_pct is not None:
|
||||
bar_len = 10
|
||||
filled = round(bar_len * used_pct / 100)
|
||||
bar = '█' * filled + '░' * (bar_len - filled)
|
||||
color = RED if used_pct >= 80 else YELLOW if used_pct >= 50 else GREEN
|
||||
parts.append(f"{color}{bar} {used_pct}%{RESET}")
|
||||
|
||||
# Tokens
|
||||
usage = ctx.get('current_usage') or {}
|
||||
in_tok = usage.get('input_tokens')
|
||||
out_tok = usage.get('output_tokens')
|
||||
cache = usage.get('cache_read_input_tokens')
|
||||
if in_tok is not None and out_tok is not None:
|
||||
tok_parts = [f"{GREEN}{in_tok}↑{RESET}", f"{CYAN}{out_tok}↓{RESET}"]
|
||||
if cache:
|
||||
tok_parts.append(f"{DIM}{cache}c{RESET}")
|
||||
parts.append(' '.join(tok_parts))
|
||||
|
||||
# Session cost (USD) — shown on both API and subscription plans
|
||||
cost = (data.get('cost') or {}).get('total_cost_usd')
|
||||
if cost is not None:
|
||||
try:
|
||||
cost_f = float(cost)
|
||||
cost_color = RED if cost_f >= 20 else YELLOW if cost_f >= 5 else GREEN
|
||||
parts.append(f"{cost_color}${cost_f:.4f}{RESET}")
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
sep = f"{DIM} | {RESET}"
|
||||
print(sep.join(parts))
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* @file sw.js
|
||||
* @description Service worker for the project landing page — precaches the shell (index, favicon, OG image, manifest) and manages cache lifecycle for offline support.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const CACHE_NAME = "landing-v1";
|
||||
const PRECACHE = ["./", "./index.html", "./favicon.svg", "./og-image.svg", "./manifest.json"];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE)));
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) =>
|
||||
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
||||
)
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
if (request.method !== "GET") return;
|
||||
|
||||
if (request.mode === "navigate") {
|
||||
// Network-first for HTML
|
||||
event.respondWith(fetch(request).catch(() => caches.match("./index.html")));
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache-first for assets, lazy-cache on miss
|
||||
event.respondWith(
|
||||
caches.match(request).then(
|
||||
(cached) =>
|
||||
cached ||
|
||||
fetch(request).then((response) => {
|
||||
if (response.ok) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||
}
|
||||
return response;
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"outFiles": [],
|
||||
"presentation": {
|
||||
"group": "1_extension",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (clean profile)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--disable-extensions",
|
||||
"--profile-temp"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "1_extension",
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (open repo)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/.."],
|
||||
"presentation": {
|
||||
"group": "1_extension",
|
||||
"order": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "package",
|
||||
"type": "shell",
|
||||
"command": "npx vsce package",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
# AgentHub for VS Code
|
||||
|
||||
**AgentHub** brings a high-fidelity, real-time dashboard for your Claude Code AI sessions directly into your VS Code workspace. Monitor token usage, track agent health, and view deep analytics without switching windows.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🚀 Key Features
|
||||
|
||||
### 📊 Integrated Dashboard
|
||||
Open the full agent monitoring dashboard in a dedicated VS Code tab. Supports real-time updates via WebSockets and local-first data storage.
|
||||
|
||||
### 🩺 Live Agent Health
|
||||
Track all 5 agent states in your sidebar with color-coded status indicators:
|
||||
- **Working**: Active subagents currently processing tasks.
|
||||
- **Connected**: Agents initialized and ready.
|
||||
- **Idle**: Standby agents.
|
||||
- **Completed**: Successfully finished tasks.
|
||||
- **Error/Failed**: Instantly spot where things went wrong.
|
||||
|
||||
### 📈 Usage & Analytics
|
||||
- **Token Tracking**: Real-time display of Billions (B) or Millions (M) of tokens consumed.
|
||||
- **Cost Calculation**: Live estimate of total USD cost based on current model pricing.
|
||||
- **Event Frequency**: High-signal stats on events per session and subagent spawning.
|
||||
|
||||
### 🧭 Deep Navigation
|
||||
Jump directly to specific views from the sidebar:
|
||||
- **Main Dashboard**: Global overview.
|
||||
- **Analytics Hub**: Deep dives into model performance.
|
||||
- **Agent Board**: Kanban-style view of subagent progress.
|
||||
- **Recent Sessions**: One-click access to your last 10 Claude sessions.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Getting Started
|
||||
|
||||
### 1. Requirements
|
||||
Ensure you have the AgentHub server running locally:
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 2. Installation
|
||||
- Search for **"AgentHub"** in the VS Code Marketplace.
|
||||
- Click **Install**.
|
||||
|
||||
### 3. Usage
|
||||
- Click the **Radar/Dashboard icon** in the Activity Bar.
|
||||
- The extension automatically detects your local server on ports **5173** (dev) or **4820** (prod).
|
||||
- Use the **Refresh** button in the sidebar to manually force an update.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Publishing & Development
|
||||
|
||||
To generate a `.vsix` file for manual installation or marketplace upload:
|
||||
|
||||
1. Install `vsce` globally:
|
||||
```bash
|
||||
npm install -g @vscode/vsce
|
||||
```
|
||||
2. Navigate to the extension folder:
|
||||
```bash
|
||||
cd vscode-extension
|
||||
```
|
||||
3. Run the package command:
|
||||
```bash
|
||||
vsce package
|
||||
```
|
||||
*This will generate `claude-code-agent-monitor-1.0.0.vsix`.*
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
Internal SmartGift build — all rights reserved.
|
||||
|
||||
Developed with ❤️ for the AI Engineer community.
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,339 +0,0 @@
|
||||
/**
|
||||
* @file Main extension file for AgentHub VSCode extension
|
||||
* Sets up the extension, registers commands, and manages the status bar item.
|
||||
* Implements a dynamic dashboard view that checks for active servers on ports 5173 and 4820.
|
||||
* Provides real-time status updates in the sidebar and status bar with background polling.
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const vscode = require("vscode");
|
||||
const http = require("http");
|
||||
const { DashboardWebviewProvider } = require("./sidebar");
|
||||
|
||||
let statusBarItem;
|
||||
let outputChannel;
|
||||
|
||||
function activate(context) {
|
||||
outputChannel = vscode.window.createOutputChannel("Claude Code Monitor");
|
||||
outputChannel.appendLine("[activate] " + new Date().toISOString());
|
||||
context.subscriptions.push(outputChannel);
|
||||
|
||||
const statusProvider = new DashboardWebviewProvider(context, outputChannel);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider("claude-code-monitor-view", statusProvider, {
|
||||
webviewOptions: { retainContextWhenHidden: true },
|
||||
})
|
||||
);
|
||||
|
||||
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
||||
statusBarItem.command = "claude-code-agent-monitor.openDashboard";
|
||||
context.subscriptions.push(statusBarItem);
|
||||
|
||||
updateStatusBar();
|
||||
const statusInterval = setInterval(() => {
|
||||
updateStatusBar();
|
||||
}, 5000); // Auto-refresh every 5 seconds
|
||||
|
||||
let openDashboard = vscode.commands.registerCommand(
|
||||
"claude-code-agent-monitor.openDashboard",
|
||||
async (target) => {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"agentMonitor",
|
||||
"AgentHub",
|
||||
vscode.ViewColumn.One,
|
||||
{ enableScripts: true, retainContextWhenHidden: true }
|
||||
);
|
||||
|
||||
const updateWebview = async () => {
|
||||
const ports = [5173, 4820];
|
||||
let activePort = null;
|
||||
for (const port of ports) {
|
||||
if (await checkPort(port)) {
|
||||
activePort = port;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (activePort) {
|
||||
let suffix = "";
|
||||
if (target) {
|
||||
suffix = target.includes("-") ? `/sessions/${target}` : `/${target}`;
|
||||
}
|
||||
panel.webview.html = getDashboardHtml(activePort, suffix);
|
||||
} else {
|
||||
panel.webview.html = getErrorHtml();
|
||||
}
|
||||
};
|
||||
|
||||
panel.webview.onDidReceiveMessage((m) => {
|
||||
if (m.command === "retry") updateWebview();
|
||||
});
|
||||
await updateWebview();
|
||||
}
|
||||
);
|
||||
|
||||
let openInBrowser = vscode.commands.registerCommand(
|
||||
"claude-code-agent-monitor.openInBrowser",
|
||||
async () => {
|
||||
const isDevUp = await checkPort(5173);
|
||||
const url = isDevUp ? "http://localhost:5173" : "http://localhost:4820";
|
||||
vscode.env.openExternal(vscode.Uri.parse(url));
|
||||
}
|
||||
);
|
||||
|
||||
let refreshStatus = vscode.commands.registerCommand(
|
||||
"claude-code-agent-monitor.refreshStatus",
|
||||
() => {
|
||||
statusProvider.refresh();
|
||||
updateStatusBar();
|
||||
vscode.window.showInformationMessage("Claude Code Monitor refreshed.");
|
||||
}
|
||||
);
|
||||
|
||||
let clearHistory = vscode.commands.registerCommand(
|
||||
"claude-code-agent-monitor.clearHistory",
|
||||
async () => {
|
||||
if (
|
||||
(await vscode.window.showWarningMessage("Clear all history?", { modal: true }, "Yes")) ===
|
||||
"Yes"
|
||||
) {
|
||||
try {
|
||||
const res = await request("DELETE", 4820, "/api/sessions");
|
||||
if (res.statusCode === 200) {
|
||||
statusProvider.refresh();
|
||||
updateStatusBar();
|
||||
vscode.window.showInformationMessage("History cleared.");
|
||||
}
|
||||
} catch (e) {
|
||||
vscode.window.showErrorMessage("Failed to clear history.");
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
context.subscriptions.push(openDashboard, openInBrowser, refreshStatus, clearHistory);
|
||||
context.subscriptions.push({ dispose: () => clearInterval(statusInterval) });
|
||||
}
|
||||
|
||||
async function updateStatusBar() {
|
||||
try {
|
||||
const stats = await fetchJson(4820, "/api/stats");
|
||||
if (stats) {
|
||||
statusBarItem.text = `$(pulse) Claude: ${stats.sessions || 0}s | ${stats.agents || 0}a`;
|
||||
statusBarItem.show();
|
||||
} else statusBarItem.hide();
|
||||
} catch (e) {
|
||||
statusBarItem.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function checkPort(port) {
|
||||
return new Promise((r) => {
|
||||
const req = http.get({ hostname: "localhost", port, path: "/", timeout: 500 }, (res) => {
|
||||
r(true);
|
||||
res.resume();
|
||||
});
|
||||
req.on("error", () => r(false));
|
||||
});
|
||||
}
|
||||
|
||||
function fetchJson(port, path) {
|
||||
return new Promise((res, rej) => {
|
||||
const req = http.get({ hostname: "localhost", port, path, timeout: 800 }, (r) => {
|
||||
let d = "";
|
||||
r.on("data", (c) => (d += c));
|
||||
r.on("end", () => {
|
||||
try {
|
||||
res(JSON.parse(d));
|
||||
} catch (e) {
|
||||
rej(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on("error", rej);
|
||||
});
|
||||
}
|
||||
|
||||
function request(method, port, path) {
|
||||
return new Promise((res, rej) => {
|
||||
const req = http.request({ hostname: "localhost", port, path, method }, res);
|
||||
req.on("error", rej);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function getDashboardHtml(port, suffix) {
|
||||
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
||||
<style>body,html{margin:0;padding:0;height:100%;width:100%;overflow:hidden;background:#0f111a;}
|
||||
iframe{border:none;height:calc(100% - 30px);width:100%;}
|
||||
.t{height:30px;background:#1a1a2e;display:flex;align-items:center;padding:0 10px;font-size:11px;color:#888;border-bottom:1px solid #2e2e48;font-family:sans-serif;}
|
||||
.d{width:8px;height:8px;border-radius:50%;background:#10b981;margin-right:8px;box-shadow:0 0 5px #10b981;}
|
||||
.u{margin-left:auto;opacity:0.5;}</style></head>
|
||||
<body><div class="t"><div class="d"></div>Live Dashboard: ${suffix || "/"} <div class="u">localhost:${port}</div></div>
|
||||
<iframe src="http://localhost:${port}${suffix}"></iframe></body></html>`;
|
||||
}
|
||||
|
||||
function getErrorHtml() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0c14;
|
||||
--card: #141624;
|
||||
--accent: #6366f1;
|
||||
--accent-glow: rgba(99, 102, 241, 0.15);
|
||||
--text: #ffffff;
|
||||
--text-muted: #94a3b8;
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--code-bg: #000000;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
margin: 0; padding: 20px;
|
||||
background-color: var(--bg);
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.1) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.1) 0px, transparent 50%);
|
||||
color: var(--text);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px; width: 100%;
|
||||
animation: fadeIn 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.hero { text-align: center; margin-bottom: 48px; }
|
||||
.logo-wrap {
|
||||
width: 80px; height: 80px; background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: 24px; margin: 0 auto 24px; display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 0 40px var(--accent-glow); position: relative;
|
||||
}
|
||||
.logo-wrap::after {
|
||||
content: ''; position: absolute; inset: -1px; border-radius: 24px;
|
||||
padding: 1px; background: linear-gradient(45deg, var(--accent), transparent);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor; mask-composite: exclude;
|
||||
}
|
||||
h1 { font-size: 36px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 12px 0; }
|
||||
.subtitle { color: var(--text-muted); font-size: 18px; font-weight: 300; }
|
||||
|
||||
.main-card {
|
||||
background: rgba(20, 22, 36, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 32px;
|
||||
padding: 40px;
|
||||
display: grid; grid-template-columns: 1.2fr 1fr; gap: 40px;
|
||||
}
|
||||
@media (max-width: 768px) { .main-card { grid-template-columns: 1fr; padding: 24px; } }
|
||||
|
||||
.setup-guide h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent); margin: 0 0 24px 0; }
|
||||
.timeline { position: relative; padding-left: 32px; }
|
||||
.timeline::before { content: ''; position: absolute; left: 7px; top: 8px; bottom: 8px; width: 1px; background: var(--border); }
|
||||
.t-step { position: relative; margin-bottom: 32px; }
|
||||
.t-step::before {
|
||||
content: ''; position: absolute; left: -32px; top: 6px; width: 16px; height: 16px;
|
||||
background: var(--bg); border: 2px solid var(--accent); border-radius: 50%; z-index: 1;
|
||||
}
|
||||
.t-label { font-size: 15px; font-weight: 500; margin-bottom: 8px; color: #fff; }
|
||||
.t-code {
|
||||
background: var(--code-bg); padding: 12px 16px; border-radius: 12px;
|
||||
font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px; color: #a5b4fc;
|
||||
border: 1px solid rgba(255,255,255,0.05); white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.side-panel { display: flex; flex-direction: column; justify-content: center; gap: 24px; }
|
||||
.info-box { background: rgba(255,255,255,0.03); border-radius: 20px; padding: 24px; border: 1px solid var(--border); }
|
||||
.info-box h3 { font-size: 15px; margin: 0 0 8px 0; display: flex; align-items: center; gap: 8px; }
|
||||
.info-box p { font-size: 13px; color: var(--text-muted); margin: 0; line-height: 1.5; }
|
||||
|
||||
.btn {
|
||||
width: 100%; padding: 16px; border-radius: 14px; font-size: 15px; font-weight: 600;
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
cursor: pointer; transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); text-decoration: none;
|
||||
}
|
||||
.btn-primary { background: var(--accent); color: #fff; border: none; box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25); }
|
||||
.btn-primary:hover { background: #4f46e5; transform: translateY(-2px); box-shadow: 0 12px 32px rgba(99, 102, 241, 0.35); }
|
||||
.btn-secondary { background: rgba(255,255,255,0.05); color: #fff; border: 1px solid var(--border); }
|
||||
.btn-secondary:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }
|
||||
|
||||
.footer { text-align: center; margin-top: 48px; display: flex; justify-content: center; gap: 32px; }
|
||||
.footer a { color: var(--text-muted); text-decoration: none; font-size: 14px; transition: color 0.2s; }
|
||||
.footer a:hover { color: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="hero">
|
||||
<div class="logo-wrap">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--accent)">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M12 8v4"/><path d="M12 16h.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>Dashboard Offline</h1>
|
||||
<div class="subtitle">Unable to connect to the Claude Code Monitor server</div>
|
||||
</div>
|
||||
|
||||
<div class="main-card">
|
||||
<div class="setup-guide">
|
||||
<h2>Quick Setup Guide</h2>
|
||||
<div class="timeline">
|
||||
<div class="t-step">
|
||||
<div class="t-label">Initialize Repository</div>
|
||||
<div class="t-code">git clone https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor.git
|
||||
cd Claude-Code-Agent-Monitor
|
||||
npm run setup</div>
|
||||
</div>
|
||||
<div class="t-step" style="margin-bottom: 0;">
|
||||
<div class="t-label">Launch Dashboard</div>
|
||||
<div class="t-code">npm run dev</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-panel">
|
||||
<div class="info-box">
|
||||
<h3>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 16v-4"/>
|
||||
<path d="M12 8h.01"/>
|
||||
</svg>
|
||||
Troubleshooting
|
||||
</h3>
|
||||
<p>Ensure no other services are occupying ports 5173 or 4820. If the problem persists, check the documentation.</p>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="acquireVsCodeApi().postMessage({command:'retry'})">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
Retry Connection
|
||||
</button>
|
||||
|
||||
<a href="https://smartgift.github.io/Claude-Code-Agent-Monitor/" class="btn btn-secondary">
|
||||
View Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<a href="https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor">GitHub</a>
|
||||
<a href="https://smartgift.github.io/Claude-Code-Agent-Monitor/">Wiki</a>
|
||||
<a href="https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/issues">Support</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function deactivate() {
|
||||
if (statusBarItem) statusBarItem.dispose();
|
||||
}
|
||||
module.exports = { activate, deactivate };
|
||||
|
Before Width: | Height: | Size: 688 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4Z" fill="currentColor" fill-opacity="0.8"/>
|
||||
<circle cx="12" cy="12" r="3" fill="currentColor"/>
|
||||
<path d="M12 7V9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M12 15V17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M7 12H9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M15 12H17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 783 B |
@@ -1,115 +0,0 @@
|
||||
{
|
||||
"name": "claude-code-agent-monitor",
|
||||
"displayName": "AgentHub",
|
||||
"description": "Professional integrated dashboard for monitoring Claude Code AI agent activity, token usage, and costs directly in VS Code.",
|
||||
"version": "1.2.4",
|
||||
"publisher": "hoangsonw",
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Data Science",
|
||||
"Machine Learning",
|
||||
"Programming Languages",
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onView:claude-code-monitor-view"
|
||||
],
|
||||
"main": "./extension.js",
|
||||
"icon": "apple-touch-icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#0f111a",
|
||||
"theme": "dark"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor"
|
||||
},
|
||||
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/blob/master/vscode-extension/README.md",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "claude-code-agent-monitor.openDashboard",
|
||||
"title": "Open Agent Dashboard",
|
||||
"category": "Claude Code",
|
||||
"icon": "$(dashboard)"
|
||||
},
|
||||
{
|
||||
"command": "claude-code-agent-monitor.refreshStatus",
|
||||
"title": "Refresh Dashboard Status",
|
||||
"category": "Claude Code",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "claude-code-agent-monitor.clearHistory",
|
||||
"title": "Clear Monitor History",
|
||||
"category": "Claude Code",
|
||||
"icon": "$(trash)"
|
||||
},
|
||||
{
|
||||
"command": "claude-code-agent-monitor.openInBrowser",
|
||||
"title": "Open Dashboard in Browser",
|
||||
"category": "Claude Code",
|
||||
"icon": "$(globe)"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "claude-code-monitor",
|
||||
"title": "AgentHub",
|
||||
"icon": "icon.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": {
|
||||
"claude-code-monitor": [
|
||||
{
|
||||
"id": "claude-code-monitor-view",
|
||||
"name": "Monitor Control Center",
|
||||
"type": "webview"
|
||||
}
|
||||
]
|
||||
},
|
||||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
"command": "claude-code-agent-monitor.openDashboard",
|
||||
"when": "view == claude-code-monitor-view",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "claude-code-agent-monitor.refreshStatus",
|
||||
"when": "view == claude-code-monitor-view",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "claude-code-agent-monitor.openInBrowser",
|
||||
"when": "view == claude-code-monitor-view",
|
||||
"group": "navigation@3"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.75.0",
|
||||
"vsce": "^2.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"package": "vsce package",
|
||||
"publish": "vsce publish"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
"anthropic",
|
||||
"ai",
|
||||
"agent",
|
||||
"monitor",
|
||||
"dashboard",
|
||||
"tokens",
|
||||
"llm"
|
||||
],
|
||||
"author": "Nguyễn Ngọc Trí Vĩ",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -1,747 +0,0 @@
|
||||
/**
|
||||
* @file Sidebar provider for AgentHub VSCode extension
|
||||
* Renders a rich, real-time WebviewView showing backend status, live agent
|
||||
* health, usage analytics, recent sessions, and quick navigation.
|
||||
*
|
||||
* Replaces the previous flat TreeDataProvider with a styled WebviewView that
|
||||
* pushes JSON snapshots from background polling and receives action messages
|
||||
* (open dashboard, open session, refresh, browser, clear history) back from
|
||||
* the webview UI.
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const vscode = require("vscode");
|
||||
const http = require("http");
|
||||
|
||||
const POLL_INTERVAL_MS = 5000;
|
||||
const SPARK_HISTORY = 20;
|
||||
|
||||
class DashboardWebviewProvider {
|
||||
constructor(context, output) {
|
||||
this.context = context;
|
||||
this.output = output;
|
||||
this.view = null;
|
||||
this.status = "Offline";
|
||||
this.data = {};
|
||||
this.history = {
|
||||
sessions: [],
|
||||
agents: [],
|
||||
tokens: [],
|
||||
cost: [],
|
||||
};
|
||||
this._pollHandle = null;
|
||||
this._fetching = null;
|
||||
}
|
||||
|
||||
log(msg) {
|
||||
if (this.output) {
|
||||
try {
|
||||
this.output.appendLine("[" + new Date().toISOString() + "] " + msg);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
resolveWebviewView(view) {
|
||||
this.view = view;
|
||||
view.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [],
|
||||
};
|
||||
view.webview.html = this.getHtml();
|
||||
|
||||
view.webview.onDidReceiveMessage((msg) => this.handleMessage(msg));
|
||||
view.onDidChangeVisibility(() => {
|
||||
if (this.view && this.view.visible) this.refresh();
|
||||
});
|
||||
view.onDidDispose(() => {
|
||||
this.view = null;
|
||||
if (this._pollHandle) clearInterval(this._pollHandle);
|
||||
this._pollHandle = null;
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
if (!this._pollHandle) {
|
||||
this._pollHandle = setInterval(() => this.refresh(), POLL_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
return this.fetchAll()
|
||||
.then(() => {
|
||||
try {
|
||||
this.pushSnapshot();
|
||||
} catch (e) {
|
||||
this.log("pushSnapshot threw: " + e.message);
|
||||
}
|
||||
})
|
||||
.catch((e) => this.log("refresh threw: " + e.message));
|
||||
}
|
||||
|
||||
handleMessage(msg) {
|
||||
if (!msg || !msg.command) return;
|
||||
switch (msg.command) {
|
||||
case "openDashboard":
|
||||
vscode.commands.executeCommand("claude-code-agent-monitor.openDashboard", msg.target || "");
|
||||
break;
|
||||
case "openInBrowser":
|
||||
vscode.commands.executeCommand("claude-code-agent-monitor.openInBrowser");
|
||||
break;
|
||||
case "refresh":
|
||||
vscode.commands.executeCommand("claude-code-agent-monitor.refreshStatus");
|
||||
break;
|
||||
case "clearHistory":
|
||||
vscode.commands.executeCommand("claude-code-agent-monitor.clearHistory");
|
||||
break;
|
||||
case "ready":
|
||||
this.log("webview READY");
|
||||
this.refresh();
|
||||
break;
|
||||
case "log":
|
||||
this.log("[webview] " + msg.text);
|
||||
break;
|
||||
case "error":
|
||||
this.log("[webview ERROR] " + msg.text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pushSnapshot() {
|
||||
if (!this.view) {
|
||||
this.log("pushSnapshot skipped: view is null");
|
||||
return;
|
||||
}
|
||||
const d = this.data || {};
|
||||
const t = (d.analytics && d.analytics.tokens) || {};
|
||||
const totalTokens =
|
||||
(t.total_input || 0) +
|
||||
(t.total_output || 0) +
|
||||
(t.total_cache_read || 0) +
|
||||
(t.total_cache_write || 0);
|
||||
|
||||
const snapshot = {
|
||||
status: this.status,
|
||||
port: d.port || null,
|
||||
stats: {
|
||||
ws: (d.stats && d.stats.ws_connections) || 0,
|
||||
sessions: (d.stats && d.stats.total_sessions) || 0,
|
||||
events: (d.stats && d.stats.total_events) || 0,
|
||||
agents_total:
|
||||
(d.stats &&
|
||||
d.stats.agents_by_status &&
|
||||
Object.values(d.stats.agents_by_status).reduce((a, b) => a + b, 0)) ||
|
||||
0,
|
||||
agents_by_status: (d.stats && d.stats.agents_by_status) || {},
|
||||
},
|
||||
analytics: {
|
||||
tokens: {
|
||||
input: t.total_input || 0,
|
||||
output: t.total_output || 0,
|
||||
cache_read: t.total_cache_read || 0,
|
||||
cache_write: t.total_cache_write || 0,
|
||||
total: totalTokens,
|
||||
},
|
||||
cost: (d.analytics && d.analytics.total_cost) || 0,
|
||||
subagents: (d.analytics && d.analytics.total_subagents) || 0,
|
||||
},
|
||||
sessions: (Array.isArray(d.sessions) ? d.sessions : []).slice(0, 12).map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name || s.id.substring(0, 8),
|
||||
status: s.status || "unknown",
|
||||
model: s.model || "unknown",
|
||||
started_at: s.started_at,
|
||||
})),
|
||||
history: this.history,
|
||||
ts: Date.now(),
|
||||
};
|
||||
|
||||
this.view.webview.postMessage({ type: "snapshot", payload: snapshot });
|
||||
}
|
||||
|
||||
pushHistory(snapshot) {
|
||||
const cap = (arr, v) => {
|
||||
arr.push(v);
|
||||
if (arr.length > SPARK_HISTORY) arr.shift();
|
||||
};
|
||||
cap(this.history.sessions, snapshot.stats.sessions);
|
||||
cap(this.history.agents, snapshot.stats.agents_total);
|
||||
cap(this.history.tokens, snapshot.analytics.tokens.total);
|
||||
cap(this.history.cost, snapshot.analytics.cost);
|
||||
}
|
||||
|
||||
async fetchAll() {
|
||||
if (this._fetching) return this._fetching;
|
||||
this._fetching = (async () => {
|
||||
const ports = [4820, 5173];
|
||||
let foundActive = false;
|
||||
|
||||
for (const p of ports) {
|
||||
const up = await this.ping(p);
|
||||
this.log("ping " + p + " => " + up);
|
||||
if (up) {
|
||||
this.status = "Online";
|
||||
this.data.port = p;
|
||||
foundActive = true;
|
||||
try {
|
||||
this.data.stats = await this.f(4820, "/api/stats");
|
||||
this.data.analytics = await this.f(4820, "/api/analytics");
|
||||
const sess = await this.f(4820, "/api/sessions?limit=12");
|
||||
this.data.sessions = Array.isArray(sess)
|
||||
? sess
|
||||
: (sess && (sess.sessions || sess.rows || sess.data)) || [];
|
||||
if (!Array.isArray(this.data.sessions)) this.data.sessions = [];
|
||||
this.log("fetched stats+analytics+sessions ok");
|
||||
} catch (e) {
|
||||
this.log("fetch error: " + (e && e.message ? e.message : e));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundActive) {
|
||||
this.status = "Offline";
|
||||
this.data = {};
|
||||
}
|
||||
this.log("fetchAll done, status=" + this.status);
|
||||
})();
|
||||
try {
|
||||
await this._fetching;
|
||||
} finally {
|
||||
this._fetching = null;
|
||||
}
|
||||
|
||||
// Update sparkline history off the freshest snapshot
|
||||
const t = (this.data.analytics && this.data.analytics.tokens) || {};
|
||||
const totalTokens =
|
||||
(t.total_input || 0) +
|
||||
(t.total_output || 0) +
|
||||
(t.total_cache_read || 0) +
|
||||
(t.total_cache_write || 0);
|
||||
this.pushHistory({
|
||||
stats: {
|
||||
sessions: (this.data.stats && this.data.stats.total_sessions) || 0,
|
||||
agents_total:
|
||||
(this.data.stats &&
|
||||
this.data.stats.agents_by_status &&
|
||||
Object.values(this.data.stats.agents_by_status).reduce((a, b) => a + b, 0)) ||
|
||||
0,
|
||||
},
|
||||
analytics: {
|
||||
tokens: { total: totalTokens },
|
||||
cost: (this.data.analytics && this.data.analytics.total_cost) || 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ping(p) {
|
||||
return new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = (v) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
try {
|
||||
req.destroy();
|
||||
} catch (_) {}
|
||||
resolve(v);
|
||||
};
|
||||
const req = http.get(
|
||||
{ hostname: "127.0.0.1", port: p, path: p === 4820 ? "/api/health" : "/", timeout: 800 },
|
||||
(res) => {
|
||||
res.resume();
|
||||
finish(true);
|
||||
}
|
||||
);
|
||||
req.on("error", () => finish(false));
|
||||
req.on("timeout", () => finish(false));
|
||||
});
|
||||
}
|
||||
|
||||
f(p, path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let done = false;
|
||||
const finish = (fn, v) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
try {
|
||||
req.destroy();
|
||||
} catch (_) {}
|
||||
fn(v);
|
||||
};
|
||||
const req = http.get({ hostname: "127.0.0.1", port: p, path, timeout: 1500 }, (r) => {
|
||||
let d = "";
|
||||
r.on("data", (c) => (d += c));
|
||||
r.on("end", () => {
|
||||
try {
|
||||
finish(resolve, JSON.parse(d));
|
||||
} catch (e) {
|
||||
finish(reject, e);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on("error", (e) => finish(reject, e));
|
||||
req.on("timeout", () => finish(reject, new Error("timeout")));
|
||||
});
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
const nonce = makeNonce();
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: var(--vscode-sideBar-background, #1e1e2e);
|
||||
--fg: var(--vscode-foreground, #e6e6e6);
|
||||
--muted: var(--vscode-descriptionForeground, #9aa0a6);
|
||||
--border: var(--vscode-panel-border, rgba(255,255,255,0.08));
|
||||
--hover: var(--vscode-list-hoverBackground, rgba(255,255,255,0.05));
|
||||
--accent: var(--vscode-charts-blue, #6366f1);
|
||||
--accent-2: var(--vscode-charts-purple, #a855f7);
|
||||
--green: var(--vscode-charts-green, #10b981);
|
||||
--red: var(--vscode-charts-red, #ef4444);
|
||||
--yellow: var(--vscode-charts-yellow, #f59e0b);
|
||||
--orange: var(--vscode-charts-orange, #f97316);
|
||||
--card: color-mix(in srgb, var(--bg) 70%, white 5%);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: var(--bg); color: var(--fg);
|
||||
font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif);
|
||||
font-size: 12px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
body {
|
||||
padding: 10px 10px 18px;
|
||||
animation: fade .35s ease both;
|
||||
}
|
||||
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
/* ===== Header ===== */
|
||||
.hero {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--accent) 18%, transparent),
|
||||
color-mix(in srgb, var(--accent-2) 12%, transparent));
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 12px;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.hero::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: radial-gradient(circle at 90% 0%, color-mix(in srgb, var(--accent) 30%, transparent), transparent 60%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.logo {
|
||||
width: 30px; height: 30px; border-radius: 9px;
|
||||
background: color-mix(in srgb, var(--accent) 30%, var(--bg));
|
||||
display: grid; place-items: center; flex-shrink: 0;
|
||||
box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
}
|
||||
.logo svg { width: 18px; height: 18px; }
|
||||
.hero-title { font-weight: 600; font-size: 13px; letter-spacing: .2px; }
|
||||
.hero-sub { color: var(--muted); font-size: 10.5px; margin-top: 1px; }
|
||||
.pill {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 3px 8px; border-radius: 999px; font-size: 10.5px; font-weight: 600;
|
||||
background: color-mix(in srgb, var(--green) 18%, transparent);
|
||||
color: var(--green); border: 1px solid color-mix(in srgb, var(--green) 35%, transparent);
|
||||
margin-left: auto; position: relative; z-index: 1;
|
||||
}
|
||||
.pill.off {
|
||||
background: color-mix(in srgb, var(--red) 18%, transparent);
|
||||
color: var(--red); border-color: color-mix(in srgb, var(--red) 35%, transparent);
|
||||
}
|
||||
.pill .dot {
|
||||
width: 7px; height: 7px; border-radius: 50%; background: currentColor;
|
||||
box-shadow: 0 0 0 0 currentColor;
|
||||
animation: pulse 1.6s ease-out infinite;
|
||||
}
|
||||
.pill.off .dot { animation: none; }
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 60%, transparent); }
|
||||
70% { box-shadow: 0 0 0 6px color-mix(in srgb, currentColor 0%, transparent); }
|
||||
100% { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 0%, transparent); }
|
||||
}
|
||||
|
||||
/* ===== Section ===== */
|
||||
.section { margin-top: 14px; }
|
||||
.section-h {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
|
||||
color: var(--muted); margin: 0 2px 6px;
|
||||
}
|
||||
.section-h .chip {
|
||||
font-size: 9.5px; letter-spacing: .04em; text-transform: none;
|
||||
padding: 2px 7px; border-radius: 999px; background: var(--hover);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
/* ===== Stats grid ===== */
|
||||
.stats {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
|
||||
}
|
||||
.stat {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 10px 8px;
|
||||
overflow: hidden; min-width: 0;
|
||||
display: flex; flex-direction: column; gap: 2px;
|
||||
transition: transform .15s ease, border-color .15s ease;
|
||||
}
|
||||
.stat:hover { transform: translateY(-1px); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); }
|
||||
.stat .label {
|
||||
color: var(--muted); font-size: 10.5px;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.stat .label svg { width: 11px; height: 11px; opacity: .85; flex-shrink: 0; }
|
||||
.stat .val {
|
||||
font-size: 17px; font-weight: 600; margin-top: 2px; letter-spacing: -.01em;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.stat .sub {
|
||||
color: var(--muted); font-size: 10px; margin-top: 1px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.stat .spark-wrap {
|
||||
margin-top: 6px; height: 18px; width: 100%;
|
||||
display: flex; align-items: flex-end;
|
||||
}
|
||||
.stat svg.spark { display: block; width: 100%; height: 18px; opacity: .9; }
|
||||
|
||||
/* ===== Agent health bar ===== */
|
||||
.health {
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: 10px; padding: 10px;
|
||||
}
|
||||
.bar {
|
||||
display: flex; height: 8px; border-radius: 999px; overflow: hidden;
|
||||
background: var(--hover);
|
||||
}
|
||||
.bar > span { transition: width .4s cubic-bezier(.2,.7,.2,1); }
|
||||
.legend {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px; margin-top: 8px;
|
||||
font-size: 10.5px; color: var(--muted);
|
||||
}
|
||||
.legend .row { display: flex; align-items: center; gap: 6px; }
|
||||
.legend .swatch { width: 8px; height: 8px; border-radius: 2px; }
|
||||
.legend .num { color: var(--fg); font-variant-numeric: tabular-nums; margin-left: auto; font-weight: 600; }
|
||||
|
||||
/* ===== Sessions ===== */
|
||||
.session {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 7px 8px; border-radius: 8px; cursor: pointer;
|
||||
border: 1px solid transparent; transition: background .12s ease, border-color .12s ease;
|
||||
}
|
||||
.session:hover { background: var(--hover); border-color: var(--border); }
|
||||
.session .avatar {
|
||||
width: 22px; height: 22px; border-radius: 6px;
|
||||
display: grid; place-items: center; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--accent) 22%, transparent);
|
||||
color: var(--accent);
|
||||
}
|
||||
.session .avatar svg { width: 12px; height: 12px; }
|
||||
.session.completed .avatar { background: color-mix(in srgb, var(--green) 22%, transparent); color: var(--green); }
|
||||
.session.error .avatar { background: color-mix(in srgb, var(--red) 22%, transparent); color: var(--red); }
|
||||
.session.active .avatar, .session.working .avatar { background: color-mix(in srgb, var(--yellow) 22%, transparent); color: var(--yellow); }
|
||||
.session .meta { min-width: 0; flex: 1; }
|
||||
.session .name { font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.session .sub { color: var(--muted); font-size: 10px; display: flex; gap: 6px; align-items: center; }
|
||||
.session .badge {
|
||||
background: var(--hover); padding: 1px 6px; border-radius: 999px; font-size: 9.5px;
|
||||
}
|
||||
|
||||
/* ===== Buttons / nav ===== */
|
||||
.nav { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||||
.nav button, .actions button {
|
||||
all: unset; cursor: pointer;
|
||||
display: flex; align-items: center; gap: 7px; padding: 8px 10px;
|
||||
border-radius: 8px; background: var(--card); border: 1px solid var(--border);
|
||||
font-size: 11.5px; transition: transform .12s ease, background .12s ease, border-color .12s ease;
|
||||
}
|
||||
.nav button:hover, .actions button:hover {
|
||||
background: var(--hover);
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.nav button svg, .actions button svg { width: 13px; height: 13px; opacity: .9; }
|
||||
.actions { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; }
|
||||
.actions button.danger:hover {
|
||||
border-color: color-mix(in srgb, var(--red) 50%, var(--border));
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
/* ===== Empty / offline ===== */
|
||||
.empty {
|
||||
text-align: center; color: var(--muted); padding: 16px 8px;
|
||||
border: 1px dashed var(--border); border-radius: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.empty .big { font-size: 13px; color: var(--fg); margin-bottom: 4px; font-weight: 600; }
|
||||
.empty code {
|
||||
display: inline-block; margin-top: 6px; padding: 3px 7px; border-radius: 6px;
|
||||
background: var(--hover); font-family: var(--vscode-editor-font-family, Consolas, monospace);
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
/* ===== Skeleton ===== */
|
||||
.skel {
|
||||
background: linear-gradient(90deg, var(--hover) 0%, color-mix(in srgb, var(--hover) 60%, transparent) 50%, var(--hover) 100%);
|
||||
background-size: 200% 100%; animation: shimmer 1.4s linear infinite;
|
||||
border-radius: 6px;
|
||||
}
|
||||
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hero">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M12 8v4"/><path d="M12 16h.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="hero-title">Claude Code Monitor</div>
|
||||
<div class="hero-sub" id="hero-sub">Connecting…</div>
|
||||
</div>
|
||||
<div class="pill off" id="pill"><span class="dot"></span><span id="pill-text">Offline</span></div>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<div class="empty">
|
||||
<div class="big">Loading…</div>
|
||||
Polling backend for live data.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${nonce}">
|
||||
(function(){
|
||||
try {
|
||||
const vscode = acquireVsCodeApi();
|
||||
window.addEventListener('error', (ev) => {
|
||||
try { vscode.postMessage({ command: 'error', text: (ev && ev.message) + ' @ ' + (ev && ev.filename) + ':' + (ev && ev.lineno) }); } catch(_){}
|
||||
try { document.getElementById('hero-sub').textContent = 'JS ERROR: ' + (ev && ev.message); } catch(_){}
|
||||
});
|
||||
// Synchronous proof-of-life so we can see the script ran:
|
||||
try { document.getElementById('hero-sub').textContent = 'script ready · awaiting data'; } catch(_){}
|
||||
vscode.postMessage({ command: 'log', text: 'script booted' });
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const fmt = (n) => {
|
||||
n = Number(n) || 0;
|
||||
if (n >= 1e9) return (n/1e9).toFixed(2) + 'B';
|
||||
if (n >= 1e6) return (n/1e6).toFixed(2) + 'M';
|
||||
if (n >= 1e3) return (n/1e3).toFixed(1) + 'k';
|
||||
return String(n);
|
||||
};
|
||||
const ago = (iso) => {
|
||||
if (!iso) return '';
|
||||
const s = (Date.now() - new Date(iso).getTime()) / 1000;
|
||||
if (s < 60) return Math.max(1, Math.floor(s)) + 's ago';
|
||||
if (s < 3600) return Math.floor(s/60) + 'm ago';
|
||||
if (s < 86400) return Math.floor(s/3600) + 'h ago';
|
||||
return Math.floor(s/86400) + 'd ago';
|
||||
};
|
||||
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
|
||||
const sparkline = (values, color) => {
|
||||
const v = (values || []).map(Number).filter(x => !isNaN(x));
|
||||
if (v.length < 2) return '<div class="spark-wrap"></div>';
|
||||
const w = 100, h = 18, pad = 1.5;
|
||||
const min = Math.min.apply(null, v), max = Math.max.apply(null, v);
|
||||
const range = (max - min) || 1;
|
||||
const step = (w - pad*2) / (v.length - 1);
|
||||
const pts = v.map((x, i) => [pad + i*step, h - pad - ((x - min) / range) * (h - pad*2)]);
|
||||
const d = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');
|
||||
const area = d + ' L ' + (w-pad).toFixed(1) + ' ' + (h-pad) + ' L ' + pad + ' ' + (h-pad) + ' Z';
|
||||
return '<div class="spark-wrap"><svg class="spark" viewBox="0 0 '+w+' '+h+'" preserveAspectRatio="none"><path d="'+area+'" fill="'+color+'" opacity="0.18"/><path d="'+d+'" fill="none" stroke="'+color+'" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"/></svg></div>';
|
||||
};
|
||||
|
||||
const ICONS = {
|
||||
sessions: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9"/><path d="M3 4v5h5"/></svg>',
|
||||
agents: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 0 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 0 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 0 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>',
|
||||
tokens: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/><path d="M8 12h8M12 8v8"/></svg>',
|
||||
cost: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/></svg>',
|
||||
play: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
|
||||
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg>',
|
||||
error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/></svg>',
|
||||
pulse: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9-6-18-3 9H2"/></svg>',
|
||||
dashboard: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg>',
|
||||
chart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"/><path d="M7 14l3-3 4 4 5-7"/></svg>',
|
||||
kanban: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18"/></svg>',
|
||||
list: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>',
|
||||
settings: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 0 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 0 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 0 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>',
|
||||
refresh: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
|
||||
globe: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20a15 15 0 0 1 0-20z"/></svg>',
|
||||
trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>',
|
||||
};
|
||||
|
||||
const STATUS_ORDER = [
|
||||
['working', 'var(--yellow)'],
|
||||
['connected', 'var(--accent-2)'],
|
||||
['idle', 'var(--green)'],
|
||||
['completed', 'var(--accent)'],
|
||||
['error', 'var(--red)'],
|
||||
];
|
||||
|
||||
const renderOffline = () => {
|
||||
$('content').innerHTML = \`
|
||||
<div class="empty">
|
||||
<div class="big">Backend offline</div>
|
||||
Start the dashboard to begin monitoring.
|
||||
<div><code>npm run dev</code></div>
|
||||
<div style="margin-top:10px;">
|
||||
<button class="" onclick="postCmd('refresh')" style="all:unset; cursor:pointer; padding:6px 12px; border-radius:8px; border:1px solid var(--border); background:var(--card);">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
};
|
||||
|
||||
const renderOnline = (s) => {
|
||||
const ah = s.stats.agents_by_status || {};
|
||||
const totalA = STATUS_ORDER.reduce((sum, [k]) => sum + (ah[k] || 0), 0) || 1;
|
||||
const segs = STATUS_ORDER
|
||||
.map(([k, c]) => '<span style="background:'+c+';width:'+(((ah[k]||0)/totalA)*100)+'%"></span>')
|
||||
.join('');
|
||||
const legend = STATUS_ORDER
|
||||
.map(([k, c]) => '<div class="row"><span class="swatch" style="background:'+c+'"></span>'+k+'<span class="num">'+(ah[k]||0)+'</span></div>')
|
||||
.join('');
|
||||
|
||||
const sessions = (s.sessions || []).length
|
||||
? s.sessions.map(x => {
|
||||
const cls = (x.status === 'completed' || x.status === 'active' || x.status === 'error' || x.status === 'working') ? x.status : '';
|
||||
const icon = x.status === 'completed' ? ICONS.check : x.status === 'error' ? ICONS.error : ICONS.play;
|
||||
return \`
|
||||
<div class="session \${cls}" data-id="\${esc(x.id)}" title="\${esc(x.id)} · \${esc(x.model)}">
|
||||
<div class="avatar">\${icon}</div>
|
||||
<div class="meta">
|
||||
<div class="name">\${esc(x.name)}</div>
|
||||
<div class="sub"><span>\${esc(ago(x.started_at))}</span><span class="badge">\${esc(x.model)}</span></div>
|
||||
</div>
|
||||
</div>\`;
|
||||
}).join('')
|
||||
: '<div class="empty">No recent sessions yet.</div>';
|
||||
|
||||
$('content').innerHTML = \`
|
||||
<div class="section">
|
||||
<div class="section-h">Live Stats <span class="chip">auto · 5s</span></div>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="label">\${ICONS.sessions} Sessions</div>
|
||||
<div class="val">\${fmt(s.stats.sessions)}</div>
|
||||
<div class="sub">\${fmt(s.stats.events)} events</div>
|
||||
\${sparkline(s.history.sessions, 'var(--accent)')}
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">\${ICONS.agents} Agents</div>
|
||||
<div class="val">\${fmt(s.stats.agents_total)}</div>
|
||||
<div class="sub">\${s.stats.ws} WS live</div>
|
||||
\${sparkline(s.history.agents, 'var(--accent-2)')}
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">\${ICONS.tokens} Tokens</div>
|
||||
<div class="val">\${fmt(s.analytics.tokens.total)}</div>
|
||||
<div class="sub">in \${fmt(s.analytics.tokens.input)} · out \${fmt(s.analytics.tokens.output)}</div>
|
||||
\${sparkline(s.history.tokens, 'var(--green)')}
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">\${ICONS.cost} Cost</div>
|
||||
<div class="val">$\${(s.analytics.cost || 0).toFixed(2)}</div>
|
||||
<div class="sub">\${s.analytics.subagents} subagents</div>
|
||||
\${sparkline(s.history.cost, 'var(--orange)')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-h">Agent Health <span class="chip">\${totalA} total</span></div>
|
||||
<div class="health">
|
||||
<div class="bar">\${segs}</div>
|
||||
<div class="legend">\${legend}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-h">Recent Sessions <span class="chip">\${(s.sessions||[]).length}</span></div>
|
||||
<div id="sessions">\${sessions}</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-h">Quick Navigation</div>
|
||||
<div class="nav">
|
||||
<button data-nav="">\${ICONS.dashboard} Dashboard</button>
|
||||
<button data-nav="analytics">\${ICONS.chart} Analytics</button>
|
||||
<button data-nav="kanban">\${ICONS.kanban} Agent Board</button>
|
||||
<button data-nav="sessions">\${ICONS.list} Sessions</button>
|
||||
<button data-nav="settings" style="grid-column: span 2;">\${ICONS.settings} System Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-h">Actions</div>
|
||||
<div class="actions">
|
||||
<button data-act="refresh">\${ICONS.refresh} Refresh</button>
|
||||
<button data-act="openInBrowser">\${ICONS.globe} Browser</button>
|
||||
<button data-act="clearHistory" class="danger">\${ICONS.trash} Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
document.querySelectorAll('[data-nav]').forEach(b => b.addEventListener('click', () => {
|
||||
vscode.postMessage({ command: 'openDashboard', target: b.getAttribute('data-nav') });
|
||||
}));
|
||||
document.querySelectorAll('[data-act]').forEach(b => b.addEventListener('click', () => {
|
||||
vscode.postMessage({ command: b.getAttribute('data-act') });
|
||||
}));
|
||||
document.querySelectorAll('.session').forEach(el => el.addEventListener('click', () => {
|
||||
vscode.postMessage({ command: 'openDashboard', target: el.getAttribute('data-id') });
|
||||
}));
|
||||
};
|
||||
|
||||
const postCmd = (c, target) => vscode.postMessage({ command: c, target });
|
||||
window.postCmd = postCmd;
|
||||
|
||||
const apply = (s) => {
|
||||
const isOn = s.status === 'Online';
|
||||
const pill = $('pill');
|
||||
pill.classList.toggle('off', !isOn);
|
||||
$('pill-text').textContent = isOn ? 'Online' : 'Offline';
|
||||
$('hero-sub').textContent = isOn
|
||||
? ('localhost:' + (s.port || '4820') + ' · live')
|
||||
: 'No backend detected on 4820 / 5173';
|
||||
if (isOn) renderOnline(s); else renderOffline();
|
||||
};
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
const m = e.data;
|
||||
if (m && m.type === 'snapshot') apply(m.payload);
|
||||
});
|
||||
|
||||
vscode.postMessage({ command: 'ready' });
|
||||
} catch (e) {
|
||||
try { document.getElementById('hero-sub').textContent = 'BOOT ERROR: ' + (e && e.message); } catch(_){}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
function makeNonce() {
|
||||
let s = "";
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (let i = 0; i < 32; i++) s += chars[Math.floor(Math.random() * chars.length)];
|
||||
return s;
|
||||
}
|
||||
|
||||
module.exports = { DashboardWebviewProvider };
|
||||
|
Before Width: | Height: | Size: 988 KiB |