diff --git a/.env.example b/.env.example index db8923d8..1e699285 100644 --- a/.env.example +++ b/.env.example @@ -58,3 +58,22 @@ # fs.watch watcher fires near-instantly regardless; this poll is the safety net. # Set to 0 to disable the poll while leaving the watcher running (default: 30000). # DASHBOARD_SESSION_SYNC_MS=30000 + +# ── Ingest filters ──────────────────────────────────────────────────────────── +# Comma-separated list of working-directory patterns whose hook events should be +# silently discarded before they are written to the database. Useful when you +# run Claude Code in private or scratch directories you never want to appear in +# the dashboard, analytics, or exports. +# +# Supported pattern forms (forward slashes, case-sensitive): +# /home/user/private exact match +# /home/user/work/* direct children only (no deeper nesting) +# /home/user/work/** /work and all descendants +# +# The hook handler responds 200 so Claude Code does not retry — the event is +# intentionally dropped rather than errored. Sessions whose first hook is ignored +# are never created, so they will not appear in the sessions list at all. For +# sessions already in the database (created before the filter was added), see the +# DELETE /api/sessions/:id endpoint. +# +# MONITOR_IGNORE_CWD=/home/user/private,/home/user/scratch/** diff --git a/README-RU.md b/README-RU.md new file mode 100644 index 00000000..25e77aae --- /dev/null +++ b/README-RU.md @@ -0,0 +1,376 @@ +# Agent Dashboard для Claude Code + +### Платформа мониторинга активности Claude Code агентов в режиме реального времени 🚀 + +Профессиональный дашборд для отслеживания и визуализации сессий Claude Code агентов, использования инструментов и оркестрации субагентов в реальном времени. Построен на Node.js, Express, React и SQLite, интегрируется напрямую с Claude Code через нативную систему хуков для бесшовного отслеживания сессий и аналитики. + +![Claude Code](https://img.shields.io/badge/Claude_Code-orange?style=flat-square&logo=claude&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white) +![React](https://img.shields.io/badge/React-18.3-61DAFB?style=flat-square&logo=react&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178C6?style=flat-square&logo=typescript&logoColor=white) +![SQLite](https://img.shields.io/badge/SQLite-3-003B57?style=flat-square&logo=sqlite&logoColor=white) +![WebSocket](https://img.shields.io/badge/WebSocket-RFC_6455-010101?style=flat-square&logo=socketdotio&logoColor=white) +![Docker](https://img.shields.io/badge/Docker-20.10-2496ED?style=flat-square&logo=docker&logoColor=white) +![MIT License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square) + +**Языки / Language Support**: [English](./README.md) · [中文](./README-CN.md) · [Tiếng Việt](./README-VN.md) · [한국어](./README-KO.md) · **Русский** + +--- + +## Содержание + +- [Обзор](#обзор) +- [Возможности](#возможности) +- [Быстрый старт](#быстрый-старт) +- [Как это работает](#как-это-работает) +- [Конфигурация](#конфигурация) +- [npm Скрипты](#npm-скрипты) +- [MCP Интеграция](#mcp-интеграция) +- [API](#api) +- [Хук-события](#хук-события) +- [Уведомления браузера](#уведомления-браузера) +- [VS Code Расширение](#vs-code-расширение) +- [Десктопное приложение](#десктопное-приложение-macos-и-windows) +- [Хранение данных](#хранение-данных) +- [Развёртывание](#развёртывание) +- [Структура проекта](#структура-проекта) +- [Устранение неполадок](#устранение-неполадок) +- [Участие в разработке](#участие-в-разработке) +- [Лицензия](#лицензия) + +--- + +## Обзор + +Отслеживайте сессии, мониторьте агентов в реальном времени, визуализируйте использование инструментов и наблюдайте за оркестрацией субагентов через профессиональный веб-интерфейс с тёмной темой. Интегрируется напрямую с Claude Code через нативную систему хуков. + +--- + +## Возможности + +- **Мониторинг сессий в реальном времени** — отслеживание активных, ожидающих, завершённых и заброшенных сессий +- **Аналитика агентов** — иерархия агентов, статусы, использование инструментов и затраты +- **Kanban-доска** — агенты и сессии, сгруппированные по статусу (работает / ожидает / завершён / ошибка) +- **Просмотр диалогов** — живой просмотр транскриптов с рендерингом Markdown и подсветкой кода +- **Временная шкала событий** — хронологическая лента событий с многомерной фильтрацией +- **Аналитика токенов** — статистика входных/выходных токенов, кэша и стоимости по моделям +- **WebSocket трансляции** — обновления UI в реальном времени без перезагрузки страницы +- **MCP Сервер** — каталог инструментов для интроспекции дашборда прямо из Claude Code +- **VS Code Расширение** — просмотр активных сессий прямо в редакторе +- **Десктопное приложение** — нативные приложения для macOS (Universal DMG) и Windows (NSIS) +- **Docker** — контейнерное развёртывание через docker-compose +- **OpenAPI / Swagger** — полная документация REST API +- **Фильтр директорий** — исключение приватных путей через `MONITOR_IGNORE_CWD` +- **Tabby** — симпатичный кот-компаньон в интерфейсе 🐱 + +--- + +## Быстрый старт + +### Требования + +- Node.js ≥ 18 (рекомендуется 22+ для нативного SQLite) +- npm ≥ 9 +- Claude Code установлен и настроен + +### Установка + +```bash +git clone https://github.com/hoangsonww/Claude-Code-Agent-Monitor.git +cd Claude-Code-Agent-Monitor +npm install +npm run build +npm start +``` + +Дашборд доступен по адресу **http://localhost:4820**. + +### Docker + +```bash +docker-compose up -d +``` + +### Настройка хуков + +Добавьте в `~/.claude/settings.json`: + +```json +{ + "hooks": { + "PreToolUse": [{ "command": "node /path/to/Claude-Code-Agent-Monitor/bin/hook.js" }], + "PostToolUse": [{ "command": "node /path/to/Claude-Code-Agent-Monitor/bin/hook.js" }], + "Stop": [{ "command": "node /path/to/Claude-Code-Agent-Monitor/bin/hook.js" }], + "SessionStart": [{ "command": "node /path/to/Claude-Code-Agent-Monitor/bin/hook.js" }], + "SessionEnd": [{ "command": "node /path/to/Claude-Code-Agent-Monitor/bin/hook.js" }], + "Notification": [{ "command": "node /path/to/Claude-Code-Agent-Monitor/bin/hook.js" }] + } +} +``` + +--- + +## Как это работает + +1. **Claude Code** запускает хук при каждом событии (использование инструмента, остановка, начало сессии и т.д.) +2. **Скрипт хука** (`bin/hook.js`) отправляет HTTP POST на сервер дашборда +3. **Express сервер** сохраняет данные в SQLite и транслирует обновления через WebSocket +4. **React UI** обновляется мгновенно через WebSocket соединение + +--- + +## Конфигурация + +Скопируйте `.env.example` в `.env`: + +```bash +cp .env.example .env +``` + +### Основные переменные + +| Переменная | По умолчанию | Описание | +|---|---|---| +| `DASHBOARD_PORT` | `4820` | Порт HTTP сервера | +| `DASHBOARD_HOST` | `127.0.0.1` | Сетевой интерфейс | +| `DASHBOARD_TOKEN` | — | Bearer-токен для аутентификации API | +| `CLAUDE_HOME` | `~/.claude` | Путь к данным Claude Code | +| `DASHBOARD_STALE_MINUTES` | `180` | Порог заброшенности сессии (мин) | +| `MONITOR_IGNORE_CWD` | — | Паттерны директорий для исключения | + +### Фильтрация директорий (MONITOR_IGNORE_CWD) + +Если вы работаете в приватных директориях, укажите их в `MONITOR_IGNORE_CWD`: + +```bash +# Точное совпадение +MONITOR_IGNORE_CWD=/home/user/private + +# Прямые дочерние директории +MONITOR_IGNORE_CWD=/home/user/work/* + +# Рекурсивно (все вложенные) +MONITOR_IGNORE_CWD=/home/user/scratch/** + +# Несколько паттернов через запятую +MONITOR_IGNORE_CWD=/home/user/private,/home/user/scratch/**,/tmp/* +``` + +Хук-события из игнорируемых директорий отбрасываются **до записи в БД** — они никогда не появятся в интерфейсе или аналитике. Сервер отвечает `200 OK` с `{ "ok": true, "ignored": true }`, чтобы Claude Code не делал повторных попыток. + +> **Важно:** по умолчанию сервер привязан к `127.0.0.1`. При открытии сетевого доступа обязательно установите `DASHBOARD_TOKEN`. + +--- + +## npm Скрипты + +| Скрипт | Описание | +|---|---| +| `npm start` | Запустить продакшн сервер | +| `npm run dev` | Dev режим с горячей перезагрузкой | +| `npm run build` | Собрать клиент | +| `npm test` | Запустить тесты (Vitest) | +| `npm run lint` | Проверить код ESLint | +| `npm run format` | Форматировать Prettier | + +--- + +## MCP Интеграция + +Дашборд включает MCP сервер в директории `mcp/`: + +```json +{ + "mcpServers": { + "agent-dashboard": { + "command": "node", + "args": ["/path/to/Claude-Code-Agent-Monitor/mcp/server.js"] + } + } +} +``` + +--- + +## API + +Swagger UI: **http://localhost:4820/api-docs** + +| Метод | Путь | Описание | +|---|---|---| +| `GET` | `/api/sessions` | Список сессий с пагинацией | +| `GET` | `/api/sessions/:id` | Детали сессии | +| `GET` | `/api/analytics` | Агрегированная статистика | +| `GET` | `/api/stats` | Сводная статистика | +| `POST` | `/api/hooks/event` | Приём хук-событий | +| `WebSocket` | `ws://localhost:4820` | Real-time трансляция | + +--- + +## Хук-события + +| Тип | Описание | +|---|---| +| `PreToolUse` | Начало использования инструмента | +| `PostToolUse` | Завершение вызова инструмента | +| `Stop` | Агент завершил ответ | +| `SubagentStop` | Субагент завершил работу | +| `SessionStart` | Начало сессии | +| `SessionEnd` | Завершение сессии | +| `Notification` | Системное уведомление | +| `UserPromptSubmit` | Пользователь отправил промпт | + +--- + +## Уведомления браузера + +Поддерживается Web Push API (VAPID): + +```bash +npx web-push generate-vapid-keys +``` + +Добавьте ключи в `.env` и разрешите уведомления в браузере через кнопку в интерфейсе. + +--- + +## VS Code Расширение + +```bash +cd vscode-extension +npm install +npm run package +# Установить .vsix: Extensions → Install from VSIX +``` + +--- + +## Десктопное приложение (macOS и Windows) + +```bash +cd desktop +npm install + +npm run build:mac # Universal DMG (arm64 + x64) +npm run build:win # NSIS installer + portable +``` + +Готовые релизы: [GitHub Releases](https://github.com/hoangsonww/Claude-Code-Agent-Monitor/releases) + +--- + +## Хранение данных + +| | Описание | +|---|---| +| По умолчанию | `~/.claude-monitor/data/dashboard.db` | +| `DASHBOARD_DATA_DIR` | Переопределение директории | +| `DASHBOARD_DB_PATH` | Полный путь к файлу БД | + +--- + +## Развёртывание + +```bash +# Docker Compose +docker-compose up -d + +# Helm +helm install agent-monitor deployments/helm/ + +# Kustomize +kubectl apply -k deployments/kustomize/ +``` + +Подробнее: [DEPLOYMENT.md](./DEPLOYMENT.md) + +--- + +## Структура проекта + +``` +Claude-Code-Agent-Monitor/ +├── bin/ # Скрипты хуков и CLI +├── client/ # React + TypeScript + Vite +│ └── src/ +│ ├── components/ # React компоненты +│ ├── pages/ # Страницы +│ └── locales/ # Переводы (i18n) +├── server/ # Express + SQLite +│ ├── routes/ # REST API +│ └── lib/ # Вспомогательные модули +├── mcp/ # MCP сервер +├── desktop/ # Electron приложение +├── vscode-extension/ # VS Code расширение +├── scripts/ # Утилиты +├── deployments/ # Docker, Kubernetes, Helm +└── docs/ # Документация +``` + +--- + +## Устранение неполадок + +### Хуки не отправляют данные + +```bash +curl http://localhost:4820/api/health +``` + +Проверьте пути в `~/.claude/settings.json` и логи сервера. + +### Ошибка SQLite при запуске + +``` +Error: better-sqlite3 could not be loaded +``` + +- **Вариант 1:** обновите Node.js до 22+ +- **Вариант 2:** `npm rebuild better-sqlite3` + +### Порт занят + +```bash +lsof -i :4820 # macOS/Linux +DASHBOARD_PORT=5000 npm start +``` + +### Данные не появляются + +Проверьте, не попадает ли рабочая директория под `MONITOR_IGNORE_CWD` в `.env`. + +--- + +## Участие в разработке + +```bash +gh repo fork hoangsonww/Claude-Code-Agent-Monitor +git checkout -b feat/ваша-фича +npm install && npm run dev +npm test && npm run lint +gh pr create +``` + +Хорошие задачи для начала: +- Переводы UI (`client/src/locales/`) +- Новые фильтры и параметры API +- Улучшение документации + +[Открытые issues](https://github.com/hoangsonww/Claude-Code-Agent-Monitor/issues) + +--- + +## Лицензия + +[MIT License](./LICENSE) + +--- + +

+ Сделано с ❤️ сообществом Claude Code
+ English · + 中文 · + Tiếng Việt · + 한국어 · + Русский +

diff --git a/README.md b/README.md index 62274805..58c526f0 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ A professional dashboard to track and visualize your Claude Code agent sessions, ![MIT License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square) > [!TIP] -> See also: [README-CN.md](./README-CN.md) (中文版本), [README-VN.md](./README-VN.md) (Phiên bản tiếng Việt), and [README-KO.md](./README-KO.md) (한국어 버전) for localized documentation with region-specific tips and best practices. +> See also: [README-CN.md](./README-CN.md) (中文版本), [README-VN.md](./README-VN.md) (Phiên bản tiếng Việt), and [README-KO.md](./README-KO.md) (한국어 버전), and [README-RU.md](./README-RU.md) (Русская версия) for localized documentation with region-specific tips and best practices. --- diff --git a/server/__tests__/cwd-filter.test.js b/server/__tests__/cwd-filter.test.js new file mode 100644 index 00000000..86e6cbed --- /dev/null +++ b/server/__tests__/cwd-filter.test.js @@ -0,0 +1,152 @@ +/** + * @file Unit tests for server/lib/cwd-filter.js — the MONITOR_IGNORE_CWD + * ingest-time ignore filter. Tests cover all three pattern forms (exact, /*, + * /**), edge cases (empty env, bad types, backslash normalisation, trailing + * slashes), and the fast-path when no patterns are configured. + */ + +"use strict"; + +const { describe, it, beforeEach, afterEach } = require("node:test"); +const assert = require("node:assert/strict"); +const { buildPatterns } = require("../lib/cwd-filter"); + +// ─── helpers ─────────────────────────────────────────────────────────────── + +/** Run isCwdIgnored against a freshly-built pattern list without touching process.env */ +function match(raw, cwd) { + const patterns = buildPatterns(raw); + if (typeof cwd !== "string" || !cwd || patterns.length === 0) return false; + const norm = cwd.replace(/\\/g, "/").replace(/\/+$/, ""); + return patterns.some((test) => test(norm)); +} + +// ─── no patterns ─────────────────────────────────────────────────────────── + +describe("buildPatterns — empty / missing input", () => { + it("returns an empty array for undefined", () => { + assert.deepEqual(buildPatterns(undefined), []); + }); + it("returns an empty array for empty string", () => { + assert.deepEqual(buildPatterns(""), []); + }); + it("returns an empty array for whitespace-only", () => { + assert.deepEqual(buildPatterns(" , , "), []); + }); +}); + +// ─── exact match ─────────────────────────────────────────────────────────── + +describe("exact path match", () => { + const RAW = "/home/user/private"; + + it("matches the exact path", () => { + assert.ok(match(RAW, "/home/user/private")); + }); + it("does not match a child directory", () => { + assert.ok(!match(RAW, "/home/user/private/sub")); + }); + it("does not match a sibling directory", () => { + assert.ok(!match(RAW, "/home/user/other")); + }); + it("does not match a parent directory", () => { + assert.ok(!match(RAW, "/home/user")); + }); + it("strips a trailing slash from the cwd before comparing", () => { + assert.ok(match(RAW, "/home/user/private/")); + }); +}); + +// ─── /* direct children ──────────────────────────────────────────────────── + +describe("/* direct-children pattern", () => { + const RAW = "/home/user/work/*"; + + it("matches a direct child", () => { + assert.ok(match(RAW, "/home/user/work/projectA")); + }); + it("does NOT match a grandchild", () => { + assert.ok(!match(RAW, "/home/user/work/projectA/src")); + }); + it("does NOT match the prefix itself", () => { + assert.ok(!match(RAW, "/home/user/work")); + }); + it("does NOT match a sibling prefix", () => { + assert.ok(!match(RAW, "/home/user/works/projectA")); + }); + it("strips trailing slash from cwd before matching", () => { + assert.ok(match(RAW, "/home/user/work/projectA/")); + }); +}); + +// ─── /** recursive ───────────────────────────────────────────────────────── + +describe("/** recursive-descendant pattern", () => { + const RAW = "/home/user/scratch/**"; + + it("matches the prefix directory itself", () => { + assert.ok(match(RAW, "/home/user/scratch")); + }); + it("matches a direct child", () => { + assert.ok(match(RAW, "/home/user/scratch/a")); + }); + it("matches a deeply nested descendant", () => { + assert.ok(match(RAW, "/home/user/scratch/a/b/c/d")); + }); + it("does NOT match a sibling that starts with the same string", () => { + assert.ok(!match(RAW, "/home/user/scratch-backup")); + }); + it("does NOT match an unrelated path", () => { + assert.ok(!match(RAW, "/home/user/projects")); + }); +}); + +// ─── multiple patterns ───────────────────────────────────────────────────── + +describe("multiple comma-separated patterns", () => { + const RAW = "/home/user/private,/tmp/*,/home/user/scratch/**"; + + it("matches the exact-match pattern", () => { + assert.ok(match(RAW, "/home/user/private")); + }); + it("matches the /* pattern", () => { + assert.ok(match(RAW, "/tmp/session-abc")); + }); + it("matches the /** pattern", () => { + assert.ok(match(RAW, "/home/user/scratch/deep/path")); + }); + it("does NOT match an unrelated path", () => { + assert.ok(!match(RAW, "/home/user/public")); + }); + it("ignores extra whitespace around commas", () => { + assert.ok(match(" /home/user/private , /tmp/* ", "/home/user/private")); + }); +}); + +// ─── edge cases ──────────────────────────────────────────────────────────── + +describe("edge cases — non-string cwd values", () => { + it("returns false for null cwd", () => { + assert.ok(!match("/home/user/private", null)); + }); + it("returns false for numeric cwd", () => { + assert.ok(!match("/home/user/private", 42)); + }); + it("returns false for object cwd", () => { + assert.ok(!match("/home/user/private", {})); + }); + it("returns false for empty string cwd", () => { + assert.ok(!match("/home/user/private", "")); + }); +}); + +describe("Windows backslash normalisation", () => { + const RAW = "C:/Users/user/private"; + + it("matches when cwd uses Windows backslashes", () => { + assert.ok(match(RAW, "C:\\Users\\user\\private")); + }); + it("matches when pattern uses Windows backslashes", () => { + assert.ok(match("C:\\Users\\user\\private", "C:/Users/user/private")); + }); +}); diff --git a/server/lib/cwd-filter.js b/server/lib/cwd-filter.js new file mode 100644 index 00000000..d5eb77b8 --- /dev/null +++ b/server/lib/cwd-filter.js @@ -0,0 +1,65 @@ +/** + * @file Ingest-time CWD ignore filter. + * + * Reads MONITOR_IGNORE_CWD (comma-separated path patterns) at module-load + * time and exposes a single predicate that hooks.js calls before writing any + * event to the database. + * + * Supported pattern forms (forward-slash normalised, case-sensitive): + * /exact/path — strict equality + * /prefix/* — direct children only (no deeper nesting) + * /prefix/** — /prefix itself and all descendants + * + * See also: .env.example → MONITOR_IGNORE_CWD + */ + +"use strict"; + +/** + * Build an array of matcher functions from a raw MONITOR_IGNORE_CWD string. + * Exported so tests can call it with arbitrary input without touching process.env. + * + * @param {string} raw - comma-separated pattern string (may be empty / undefined) + * @returns {Array<(cwd: string) => boolean>} + */ +function buildPatterns(raw) { + if (!raw) return []; + return raw + .split(",") + .map((p) => p.trim()) + .filter(Boolean) + .map((p) => { + // Normalise Windows backslashes and strip trailing slashes + const norm = p.replace(/\\/g, "/").replace(/\/+$/, ""); + if (norm.endsWith("/**")) { + const prefix = norm.slice(0, -3); + return (cwd) => cwd === prefix || cwd.startsWith(prefix + "/"); + } + if (norm.endsWith("/*")) { + const prefix = norm.slice(0, -2); + return (cwd) => { + if (!cwd.startsWith(prefix + "/")) return false; + return !cwd.slice(prefix.length + 1).includes("/"); + }; + } + return (cwd) => cwd === norm; + }); +} + +// Patterns compiled once at startup from the live env variable. +const PATTERNS = buildPatterns(process.env.MONITOR_IGNORE_CWD); + +/** + * Returns true when the given working directory matches any configured + * ignore pattern and the hook event should be silently discarded. + * + * @param {unknown} cwd - the cwd field from the hook payload (may be any type) + * @returns {boolean} + */ +function isCwdIgnored(cwd) { + if (typeof cwd !== "string" || !cwd || PATTERNS.length === 0) return false; + const norm = cwd.replace(/\\/g, "/").replace(/\/+$/, ""); + return PATTERNS.some((test) => test(norm)); +} + +module.exports = { isCwdIgnored, buildPatterns }; diff --git a/server/routes/hooks.js b/server/routes/hooks.js index 097c48e4..37cde4ca 100644 --- a/server/routes/hooks.js +++ b/server/routes/hooks.js @@ -15,6 +15,7 @@ const { ingestWorkflowsForSession } = require("../lib/workflow-ingest"); // Required as a module object (not destructured) so tests can swap // `liveness.probeLiveCwds` and the watchdog picks the stub up at call time. const liveness = require("../lib/session-liveness"); +const { isCwdIgnored } = require("../lib/cwd-filter"); const router = Router(); @@ -940,6 +941,12 @@ router.post("/event", (req, res) => { }); } + // Drop hooks from ignored working directories (MONITOR_IGNORE_CWD). + // Return 200 so Claude Code does not retry — we intentionally discard the event. + if (isCwdIgnored(data.cwd)) { + return res.json({ ok: true, ignored: true }); + } + const result = processEvent(hook_type, data); if (!result) { return res.status(400).json({