diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a1d3492c..67c20d91 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -45,7 +45,7 @@ You sign **once** — the signature covers all of your current and future contri ### Prerequisites -- Node.js 18+ (22+ recommended for automatic SQLite fallback) +- Node.js 20+ (22+ recommended for automatic SQLite fallback) - npm 9+ ### Setup diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 75318a00..0497fe37 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -5,7 +5,7 @@ Architectural overview and technical reference for the Agent Dashboard system, c ![Claude Code](https://img.shields.io/badge/Claude_Code-orange?style=flat-square&logo=claude&logoColor=white) ![Claude Code Plugins](https://img.shields.io/badge/Claude_Code-Plugins_&_Skills-orange?style=flat-square&logo=anthropic&logoColor=white) ![Model Context Protocol](https://img.shields.io/badge/Model_Context_Protocol-1.0-0f766e?style=flat-square&logo=modelcontextprotocol&logoColor=white) -![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white) ![Python](https://img.shields.io/badge/Python-%3E%3D3.6-3776AB?style=flat-square&logo=python&logoColor=white) ![Express](https://img.shields.io/badge/Express-4.21-000000?style=flat-square&logo=express&logoColor=white) ![ws](https://img.shields.io/badge/ws-WebSocket_server-010101?style=flat-square&logo=socketdotio&logoColor=white) @@ -122,7 +122,7 @@ C4Context - Zero-config operation -- auto-discovers sessions from hook events - Never block Claude Code -- hooks fail silently with timeouts - Instant feedback -- WebSocket push, no polling -- Portable -- SQLite, no external services, runs on any OS with Node.js 18+ +- Portable -- SQLite, no external services, runs on any OS with Node.js 20+ - Extensible -- plugin marketplace with 10 plugins (53 skills, 14 agents, 30 slash commands, 3 CLI tools) --- @@ -342,7 +342,7 @@ graph TD | `server/compat-sqlite.js` | Compatibility wrapper that gives Node.js built-in `node:sqlite` (`DatabaseSync`) the same API as `better-sqlite3` — pragma, transaction, prepare. Used as automatic fallback when the native module is unavailable (Node 22+) | | `server/websocket.js` | WebSocket server on `/ws` path, 30s heartbeat with ping/pong dead connection detection, typed broadcast function. Upgrades run through the same Host-header allowlist and optional `DASHBOARD_TOKEN` check as the HTTP surface (`isWebSocketAuthorized`) | | `server/lib/security.js` | Network-hardening module (fix for GHSA-gr74-4xfh-6jw9). `resolveHost()` picks the bind address — `127.0.0.1` by default, widened only by `DASHBOARD_HOST` (logs a warning for non-loopback binds). `hostGuard` rejects requests whose `Host` header isn't in the loopback set or `DASHBOARD_ALLOWED_HOSTS` (DNS-rebinding defense). `corsOptions()` allows only loopback origins while letting No-Origin (curl/CLI) requests through. `tokenGuard` + `isWebSocketAuthorized` enforce the optional `DASHBOARD_TOKEN` on `/api/*` and WebSocket upgrades (accepted as `Authorization: Bearer`, `x-dashboard-token`, or `?token=`; off by default). Exempt paths and token-matching helpers (`tokensMatch`, `extractToken`) live here too | -| `routes/hooks.js` | Core event processing inside a SQLite transaction. Auto-creates sessions/agents. Handles 8 hook types: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionEnd, plus synthetic `Compaction` events. Manages the agent state machine plus the `awaiting_input_since` overlay (stamped on SessionStart for fresh CLIs, on non-error Stop, and on permission Notifications (which now also set agent status to `waiting`); cleared on UserPromptSubmit / PreToolUse / PostToolUse / SessionStart-resume / SessionEnd; SubagentStop intentionally does NOT clear it; and stamped by the 15 s watchdog on user-interrupt (Esc) recovery — see the `index.js` row — since `Esc` fires no hook). After `res.json()` returns on `SubagentStop`, fires a fire-and-forget `scanAndImportSubagents` (from `scripts/import-history.js`) that parses every `subagents/agent-*.jsonl`, pairs `tool_use` ↔ `tool_result` blocks by `tool_use_id`, and emits per-tool `PreToolUse` + `PostToolUse` events under each subagent's own `agent_id` — closes the gap where subagent-internal tool calls would otherwise never reach the events table. The same scan also reparents nested subagents under their true spawner (see the `import-history.js` row); it returns `{ created, reparented }`, and the follow-up `new_event` refetch nudge fires when **either** is non-zero so a pure re-parent (tree shape changed, no new rows) still refreshes the UI. The same scan attributes each subagent's tokens to **its own model** (resolved from the subagent transcript) and stamps `metadata.model` on the subagent row (issue #185), so a tiered pipeline (Opus orchestrator + Sonnet/Haiku subagents) is priced per real model rather than entirely at the orchestrator's rate; the parent-model bucket is skipped to avoid colliding with the main-transcript token writer's compaction baseline logic. Session reactivation on resume (including Stop/SubagentStop reactivation for imported completed/abandoned sessions), orphaned-session cleanup uses `DASHBOARD_STALE_MINUTES` (default 180). Uses a shared `TranscriptCache` instance (`server/lib/transcript-cache.js`) for extraction of tokens, API errors, turn durations, thinking blocks, and usage extras — stat-based caching with incremental byte-offset reads avoids re-reading entire JSONL files on every event. Detects compaction via `isCompactSummary` in JSONL transcripts and creates compaction agents + events (deduplicated by uuid). Token baselines (`baseline_*` columns) preserve pre-compaction totals so no usage is lost. Cache entries are evicted on SessionEnd. **SessionEnd preserves error state** — but only when the error is still unrecovered at the transcript tail (`isErrorAtTail`: latest API error with no successful turn after it); a transient error the CLI retried past finalizes as `completed` instead of freezing in a stale `error`. **Error recovery**: `UserPromptSubmit` and `PreToolUse` recover a session from `error`; additionally the 15 s watchdog now scans `error` sessions and self-heals one back to `active` when its transcript has progressed past the last API error (`isErrorAtTail` false) — closing the gap where a transient API error left an imported or sweep-monitored session (no live recovery hook) pinned in `error` forever. **Session naming**: on every event, syncs `sessions.name` from the transcript title surfaced by `TranscriptCache` and broadcasts `session_updated` — an explicit `custom-title` (`/rename`, `claude -n`, picker Ctrl+R) always wins, an `ai-title` (auto / plan-accept) only fills a placeholder/auto name (`Session ` or a cwd-folder import name) so a user-chosen name is never clobbered. When neither title exists, the session's **first user prompt** (surfaced by `TranscriptCache` as `firstUserMessage`; tool-result / meta / slash-command plumbing entries skipped) fills the placeholder session name plus the main agent's placeholder name and empty task (issue #201) — a later `ai-title` can still replace a descriptor-filled name, and the agent fill passes the in-flight `current_tool` through (the shared `updateAgent` statement writes that column verbatim) so it is never wiped mid-turn. The guarded `updateSessionName` no-ops on the unchanged case, so the broadcast path stays quiet; the 15 s error-watchdog runs the same sync for idle sessions that fire no hook after a rename. **Dead-session liveness reap**: the same watchdog completes any `active` session whose `cwd` has no running `claude` CLI process (probe via `lib/session-liveness.js`) — recovering a `SessionEnd` lost while the dashboard was down (e.g. Ctrl+C) that previously left the session in Waiting until the 3 h stale sweep; watchdog ticks are gated on the transcript mtime (fallback `updated_at` when no transcript exists) being older than `DASHBOARD_LIVENESS_IDLE_SECONDS` (default 60 s); the boot passes — immediately at startup and again ~5 s later (post-import) — skip the gate so a session quit moments before launch clears at once, disabled via `DASHBOARD_LIVENESS_PROBE=0` / on Windows / in containers, and a false completion self-heals through hook reactivation. Sessions whose `cwd` is not POSIX-absolute (household-hook-forwarded from another machine, e.g. a Windows `D:\…` path a local `/proc`/`lsof` scan can never match) are skipped by the reap — so a mixed local + forwarded deployment stays correct without disabling the whole probe | +| `routes/hooks.js` | Core event processing inside a SQLite transaction. Auto-creates sessions/agents. Handles 8 hook types: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionEnd, plus synthetic `Compaction` events. Manages the agent state machine plus the `awaiting_input_since` overlay (stamped on SessionStart for fresh CLIs — `startup`/`resume`/`clear` only, since a `compact`-source SessionStart fires mid-turn while Claude is working and is deliberately skipped so a working session stays Active — on non-error Stop, and on permission Notifications (which now also set agent status to `waiting`); cleared on UserPromptSubmit / PreToolUse / PostToolUse / SessionStart-resume / SessionEnd; SubagentStop intentionally does NOT clear it; and stamped by the 15 s watchdog on user-interrupt (Esc) recovery — see the `index.js` row — since `Esc` fires no hook). After `res.json()` returns on `SubagentStop`, fires a fire-and-forget `scanAndImportSubagents` (from `scripts/import-history.js`) that parses every `subagents/agent-*.jsonl`, pairs `tool_use` ↔ `tool_result` blocks by `tool_use_id`, and emits per-tool `PreToolUse` + `PostToolUse` events under each subagent's own `agent_id` — closes the gap where subagent-internal tool calls would otherwise never reach the events table. The same scan also reparents nested subagents under their true spawner (see the `import-history.js` row); it returns `{ created, reparented }`, and the follow-up `new_event` refetch nudge fires when **either** is non-zero so a pure re-parent (tree shape changed, no new rows) still refreshes the UI. The same scan attributes each subagent's tokens to **its own model** (resolved from the subagent transcript) and stamps `metadata.model` on the subagent row (issue #185), so a tiered pipeline (Opus orchestrator + Sonnet/Haiku subagents) is priced per real model rather than entirely at the orchestrator's rate; the parent-model bucket is skipped to avoid colliding with the main-transcript token writer's compaction baseline logic. Session reactivation on resume (including Stop/SubagentStop reactivation for imported completed/abandoned sessions), orphaned-session cleanup uses `DASHBOARD_STALE_MINUTES` (default 180). Uses a shared `TranscriptCache` instance (`server/lib/transcript-cache.js`) for extraction of tokens, API errors, turn durations, thinking blocks, and usage extras — stat-based caching with incremental byte-offset reads avoids re-reading entire JSONL files on every event. Detects compaction via `isCompactSummary` in JSONL transcripts and creates compaction agents + events (deduplicated by uuid). Token baselines (`baseline_*` columns) preserve pre-compaction totals so no usage is lost. Cache entries are evicted on SessionEnd. **SessionEnd preserves error state** — but only when the error is still unrecovered at the transcript tail (`isErrorAtTail`: latest API error with no successful turn after it); a transient error the CLI retried past finalizes as `completed` instead of freezing in a stale `error`. **Error recovery**: `UserPromptSubmit` and `PreToolUse` recover a session from `error`; additionally the 15 s watchdog now scans `error` sessions and self-heals one back to `active` when its transcript has progressed past the last API error (`isErrorAtTail` false) — closing the gap where a transient API error left an imported or sweep-monitored session (no live recovery hook) pinned in `error` forever. **Session naming**: on every event, syncs `sessions.name` from the transcript title surfaced by `TranscriptCache` and broadcasts `session_updated` — an explicit `custom-title` (`/rename`, `claude -n`, picker Ctrl+R) always wins, an `ai-title` (auto / plan-accept) only fills a placeholder/auto name (`Session ` or a cwd-folder import name) so a user-chosen name is never clobbered. When neither title exists, the session's **first user prompt** (surfaced by `TranscriptCache` as `firstUserMessage`; tool-result / meta / slash-command plumbing entries skipped) fills the placeholder session name plus the main agent's placeholder name and empty task (issue #201) — a later `ai-title` can still replace a descriptor-filled name, and the agent fill passes the in-flight `current_tool` through (the shared `updateAgent` statement writes that column verbatim) so it is never wiped mid-turn. The guarded `updateSessionName` no-ops on the unchanged case, so the broadcast path stays quiet; the 15 s error-watchdog runs the same sync for idle sessions that fire no hook after a rename. **Dead-session liveness reap**: the same watchdog completes any `active` session whose `cwd` has no running `claude` CLI process (probe via `lib/session-liveness.js`) — recovering a `SessionEnd` lost while the dashboard was down (e.g. Ctrl+C) that previously left the session in Waiting until the 3 h stale sweep; watchdog ticks are gated on the transcript mtime (fallback `updated_at` when no transcript exists) being older than `DASHBOARD_LIVENESS_IDLE_SECONDS` (default 60 s); the boot passes — immediately at startup and again ~5 s later (post-import) — skip the gate so a session quit moments before launch clears at once, disabled via `DASHBOARD_LIVENESS_PROBE=0` / on Windows / in containers, and a false completion self-heals through hook reactivation. Sessions whose `cwd` is not POSIX-absolute (household-hook-forwarded from another machine, e.g. a Windows `D:\…` path a local `/proc`/`lsof` scan can never match) are skipped by the reap — so a mixed local + forwarded deployment stays correct without disabling the whole probe | | `routes/sessions.js` | Standard CRUD with pagination. GET includes agent count via LEFT JOIN. POST is idempotent on session ID. GET `/:id/transcript` also surfaces `custom-title` lines as synthetic `session_event` (rename) messages — deduped, with `ai-title` excluded — so TUI-only `/rename` (which writes no user/assistant turn) is still visible in the conversation viewer. It also surfaces `system`/`local_command` lines: newer Claude Code builds write a local slash command's invocation and captured output (``, ``/`stderr`) as `system`/`local_command` entries with the TUI markup in a top-level `content` string (older builds used `user` messages), so the route re-emits those as user-side text and the client's `tuiSegments` parser renders the command pill + its output (e.g. `/color` → a `/color` pill plus "Session color set to: cyan"). Content-less `local_command` lines (e.g. `/clear`) and every other `system` subtype (`turn_duration`, `stop_hook_summary`, …) are dropped as noise | | `routes/agents.js` | CRUD with status/session_id filtering. PATCH broadcasts `agent_updated`. Agent-list responses (`GET /api/agents`, `GET /api/sessions/:id/agents`) attach a per-agent `cost` via `pricing.attachAgentCosts` — each subagent's OWN cost, computed from its `metadata.tokens` at current rates (main agents get 0; their cost is the session total), so a subagent card shows only what that subagent spent rather than the session total | | `routes/events.js` | Read-only event listing with session_id filter and pagination | diff --git a/Dockerfile b/Dockerfile index 63196440..b30c7fb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,13 @@ 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 ─────────────────────────────────────── diff --git a/INSTALL.md b/INSTALL.md index f5edaf15..cae4aa8c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,7 @@ A step-by-step guide to get the Claude Code Agent Monitor up and running on your | Requirement | Version | Notes | |---|---|---| -| Node.js | 18+ (22+ recommended) | Required for server and client | +| 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 | @@ -29,13 +29,16 @@ cd Claude-Code-Agent-Monitor npm run setup ``` -This installs all server and client dependencies in a single command. It is equivalent to: +This installs all server and client dependencies, plus the VS Code extension, and links the `ccam` CLI. + +A plain root install already covers server **and** client — a `postinstall` hook installs the client dependencies automatically, so this alone is enough to build and run the dashboard: ```bash npm install -cd client && npm install ``` +`npm run setup` additionally installs the VS Code extension and links the `ccam` CLI. (If you install with `--ignore-scripts`, the `postinstall` hook is skipped — run `cd client && npm install` manually in that case.) + Or via Makefile (also installs MCP dependencies): ```bash @@ -180,8 +183,8 @@ If you'd rather not keep a terminal window open, the project also ships an Elect |---|---| | Downloading a pre-built installer (macOS) | macOS — nothing else | | Downloading a pre-built installer (Windows) | Windows 10/11 (x64) — nothing else | -| Building the DMG locally (macOS) | macOS, Node.js 18+ (22+ recommended), npm 9+, and **Xcode command-line tools** (`xcode-select --install`) so the native `better-sqlite3` module can be rebuilt for Electron's ABI | -| Building the `.exe` locally (Windows) | Windows, Node.js 18+ (22+ recommended), npm 9+. `better-sqlite3` is fetched as a **prebuilt Electron binary** by `npm run desktop:install`, so no Visual Studio C++ toolchain is needed in the common case. If the build _does_ fail, `npm run desktop:install` prints the exact fix (Visual Studio Build Tools + "Desktop development with C++") plus a no-toolchain alternative and exits non-zero rather than failing silently | +| Building the DMG locally (macOS) | macOS, Node.js 20+ (22+ recommended), npm 9+, and **Xcode command-line tools** (`xcode-select --install`) so the native `better-sqlite3` module can be rebuilt for Electron's ABI | +| Building the `.exe` locally (Windows) | Windows, Node.js 20+ (22+ recommended), npm 9+. `better-sqlite3` is fetched as a **prebuilt Electron binary** by `npm run desktop:install`, so no Visual Studio C++ toolchain is needed in the common case. If the build _does_ fail, `npm run desktop:install` prints the exact fix (Visual Studio Build Tools + "Desktop development with C++") plus a no-toolchain alternative and exits non-zero rather than failing silently | ### Way 1 — Download a pre-built installer @@ -460,7 +463,7 @@ This is expected and **non-fatal**. `better-sqlite3` is a native C++ module list At runtime the server uses this fallback chain: -1. **`better-sqlite3`** — used when prebuilt binaries are available (Node 18/20/22 on Windows x64, macOS arm64/x64, Linux x64/arm64) +1. **`better-sqlite3`** — used when prebuilt binaries are available (Node 20/22/24 on Windows x64, macOS arm64/x64, Linux x64/arm64) 2. **`node:sqlite`** — Node.js built-in SQLite module, used automatically on Node 22+ when `better-sqlite3` is unavailable If you see an error box at startup saying *"SQLite backend not available"*, either: diff --git a/README-CN.md b/README-CN.md index fdc8331e..d5e074ce 100644 --- a/README-CN.md +++ b/README-CN.md @@ -7,7 +7,7 @@ ![Claude Code](https://img.shields.io/badge/Claude_Code-orange?style=flat-square&logo=claude&logoColor=white) ![Claude Code Plugins](https://img.shields.io/badge/Claude_Code-Plugins_&_Skills-orange?style=flat-square&logo=anthropic&logoColor=white) ![Model Context Protocol](https://img.shields.io/badge/Model_Context_Protocol-1.0-0f766e?style=flat-square&logo=modelcontextprotocol&logoColor=white) -![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white) ![Python](https://img.shields.io/badge/Python-%3E%3D3.6-3776AB?style=flat-square&logo=python&logoColor=white) ![Express](https://img.shields.io/badge/Express-4.21-000000?style=flat-square&logo=express&logoColor=white) ![ws](https://img.shields.io/badge/ws-WebSocket_server-010101?style=flat-square&logo=socketdotio&logoColor=white) @@ -321,7 +321,7 @@ Dashboard 提供全面的功能来监控和分析你的 Claude Code 会话和 Ag ### 前置条件 -- **Node.js** >= 18.0.0(推荐 22+) +- **Node.js** >= 20.0.0(推荐 22+) - **npm** >= 9.0.0 ### 1. 安装 @@ -550,7 +550,8 @@ stateDiagram-v2 ```mermaid stateDiagram-v2 - [*] --> waiting: SessionStart(status=active + 标志) + [*] --> waiting: SessionStart startup/resume/clear(status=active + 标志) + active --> active: SessionStart compact(回合中 — 保留状态,无标志) waiting --> active: UserPromptSubmit / PreToolUse / PostToolUse active --> waiting: Stop,非错误(标志重新盖上) active --> waiting: 权限 Notification(Agent → waiting) @@ -1159,7 +1160,7 @@ Dashboard 处理以下 Claude Code Hook 类型: | Hook 类型 | 触发时机 | Dashboard 操作 | | -------------- | ------------------------------ | -------------------------------------------------------------------------------------------- | -| `SessionStart` | Claude Code 会话开始 | 创建会话和主 Agent。盖上 `awaiting_input_since`(附带 `awaiting_reason=session_start`),使新会话立即落入**等待中**。重新激活恢复的会话。废弃无活动超过 `DASHBOARD_STALE_MINUTES`(默认 180)的孤立会话 | +| `SessionStart` | Claude Code 会话开始 | 创建会话和主 Agent。盖上 `awaiting_input_since`(附带 `awaiting_reason=session_start`),使新会话立即落入**等待中**——但 `compact` 来源的 SessionStart(回合中自动压缩)不改动标志,使正在工作的会话保持**活动**。重新激活恢复的会话。废弃无活动超过 `DASHBOARD_STALE_MINUTES`(默认 180)的孤立会话 | | `UserPromptSubmit` | 用户在提示符前按下回车 | 清除等待标志并将主 Agent 提升为 `working` — 文本响应回合开始的唯一可靠信号,因为它们不发出 `PreToolUse` | | `PreToolUse` | Agent 开始使用工具 | 清除等待标志,设置 Agent 为 `working`,设置 `current_tool`。如果工具是 `Agent`,创建子 Agent 记录 | | `PostToolUse` | 工具执行完成 | 清除等待标志(用于处理用户在工具运行期间批准权限提示的场景)。清除 `current_tool`。Agent 保持 `working` | diff --git a/README-KO.md b/README-KO.md index bf27a6ed..076879d5 100644 --- a/README-KO.md +++ b/README-KO.md @@ -7,7 +7,7 @@ Claude Code 에이전트 세션, 도구 사용, 서브에이전트 오케스트 ![Claude Code](https://img.shields.io/badge/Claude_Code-orange?style=flat-square&logo=claude&logoColor=white) ![Claude Code Plugins](https://img.shields.io/badge/Claude_Code-Plugins_&_Skills-orange?style=flat-square&logo=anthropic&logoColor=white) ![Model Context Protocol](https://img.shields.io/badge/Model_Context_Protocol-1.0-0f766e?style=flat-square&logo=modelcontextprotocol&logoColor=white) -![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white) ![Python](https://img.shields.io/badge/Python-%3E%3D3.6-3776AB?style=flat-square&logo=python&logoColor=white) ![Express](https://img.shields.io/badge/Express-4.21-000000?style=flat-square&logo=express&logoColor=white) ![ws](https://img.shields.io/badge/ws-WebSocket_server-010101?style=flat-square&logo=socketdotio&logoColor=white) @@ -319,7 +319,7 @@ flowchart LR ### 사전 요구 사항 -- **Node.js** >= 18.0.0 (22+ 권장) +- **Node.js** >= 20.0.0 (22+ 권장) - **npm** >= 9.0.0 ### 1. 설치 @@ -547,7 +547,8 @@ stateDiagram-v2 ```mermaid stateDiagram-v2 - [*] --> waiting: SessionStart (status=active + flag) + [*] --> waiting: SessionStart startup/resume/clear (status=active + flag) + active --> active: SessionStart compact (턴 도중 — 상태 유지, 플래그 없음) waiting --> active: UserPromptSubmit / PreToolUse / PostToolUse active --> waiting: Stop, non-error (flag re-stamped) active --> waiting: Permission Notification (agent → waiting) @@ -1246,7 +1247,7 @@ stateDiagram-v2 | Hook 유형 | 트리거 | 대시보드 동작 | | ------------------- | ------------------------------ | -------------------------------------------------------------------------------------------- | -| `SessionStart` | Claude Code 세션 시작 | 세션과 메인 에이전트를 생성합니다. `awaiting_input_since`를(`awaiting_reason=session_start`와 함께) 기록하여 새 세션이 **Waiting** 상태로 시작되게 합니다. 재개된 세션을 다시 활성화합니다. `DASHBOARD_STALE_MINUTES`(기본 180) 동안 활동이 없는 고아 세션은 중단 처리합니다 | +| `SessionStart` | Claude Code 세션 시작 | 세션과 메인 에이전트를 생성합니다. `awaiting_input_since`를(`awaiting_reason=session_start`와 함께) 기록하여 새 세션이 **Waiting** 상태로 시작되게 합니다. 단, `compact` 소스 SessionStart(턴 도중 자동 압축)는 플래그를 그대로 두어 작업 중인 세션이 **Active** 상태를 유지합니다. 재개된 세션을 다시 활성화합니다. `DASHBOARD_STALE_MINUTES`(기본 180) 동안 활동이 없는 고아 세션은 중단 처리합니다 | | `UserPromptSubmit` | 사용자가 프롬프트에서 Enter를 누름 | 대기 플래그를 지우고 메인 에이전트를 `working`으로 승격합니다 — 텍스트 전용 어시스턴트 턴은 `PreToolUse`를 발생시키지 않으므로, 이것이 해당 턴이 시작되었음을 알리는 유일한 신호입니다 | | `PreToolUse` | 에이전트가 도구 사용을 시작함 | 대기 플래그를 지우고, 에이전트를 `working`으로 설정하며, `current_tool`을 설정합니다. 도구가 `Agent`인 경우 서브에이전트 레코드를 생성합니다 | | `PostToolUse` | 도구 실행 완료 | 대기 플래그를 지웁니다(도구 실행 도중 Notification이 플래그를 기록한 권한 프롬프트 승인 상황을 처리). `current_tool`을 지웁니다. 에이전트는 `working` 상태를 유지합니다 | diff --git a/README-VN.md b/README-VN.md index ad107dfb..33ce22ba 100644 --- a/README-VN.md +++ b/README-VN.md @@ -7,7 +7,7 @@ Bảng điều khiển chuyên nghiệp để theo dõi và trực quan hóa cá ![Claude Code](https://img.shields.io/badge/Claude_Code-orange?style=flat-square&logo=claude&logoColor=white) ![Claude Code Plugins](https://img.shields.io/badge/Claude_Code-Plugins_&_Skills-orange?style=flat-square&logo=anthropic&logoColor=white) ![Model Context Protocol](https://img.shields.io/badge/Model_Context_Protocol-1.0-0f766e?style=flat-square&logo=modelcontextprotocol&logoColor=white) -![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white) ![Python](https://img.shields.io/badge/Python-%3E%3D3.6-3776AB?style=flat-square&logo=python&logoColor=white) ![Express](https://img.shields.io/badge/Express-4.21-000000?style=flat-square&logo=express&logoColor=white) ![ws](https://img.shields.io/badge/ws-WebSocket_server-010101?style=flat-square&logo=socketdotio&logoColor=white) @@ -319,7 +319,7 @@ Bảng điều khiển cung cấp một bộ tính năng toàn diện để giá ### Điều kiện tiên quyết -- **Node.js** >= 18.0.0 (khuyến nghị 22+) +- **Node.js** >= 20.0.0 (khuyến nghị 22+) - **npm** >= 9.0.0 ### 1. Cài đặt @@ -548,7 +548,8 @@ Trạng thái lưu trữ: `active | completed | error | abandoned`. Trạng thá ```mermaid stateDiagram-v2 - [*] --> waiting: SessionStart (status=active + cờ) + [*] --> waiting: SessionStart startup/resume/clear (status=active + cờ) + active --> active: SessionStart compact (giữa lượt — giữ nguyên trạng thái, không cờ) waiting --> active: UserPromptSubmit / PreToolUse / PostToolUse active --> waiting: Stop, không lỗi (cờ được đóng dấu lại) active --> waiting: Notification xin quyền (agent → waiting) @@ -1164,7 +1165,7 @@ Bảng điều khiển xử lý các loại hook Claude Code này: | Loại hook | Trigger | Hành động trên dashboard | | ----------------- | ------------------------------ | --------------------------------------------------------------------------------------------------- | -| `SessionStart` | Phiên Claude Code bắt đầu | Tạo phiên và Agent chính. Đóng dấu `awaiting_input_since` (với `awaiting_reason=session_start`) để phiên mới rơi vào **Đang chờ**. Kích hoạt lại các phiên đã tiếp tục. Bỏ các phiên mồ côi không hoạt động trong `DASHBOARD_STALE_MINUTES` (mặc định 180) | +| `SessionStart` | Phiên Claude Code bắt đầu | Tạo phiên và Agent chính. Đóng dấu `awaiting_input_since` (với `awaiting_reason=session_start`) để phiên mới rơi vào **Đang chờ** — trừ SessionStart nguồn `compact` (nén giữa lượt), vốn giữ nguyên cờ để phiên đang chạy vẫn **Hoạt động**. Kích hoạt lại các phiên đã tiếp tục. Bỏ các phiên mồ côi không hoạt động trong `DASHBOARD_STALE_MINUTES` (mặc định 180) | | `UserPromptSubmit`| Người dùng nhấn enter | Xóa cờ chờ và đẩy Agent chính sang `working` — tín hiệu duy nhất cho biết các lượt văn bản thuần đã bắt đầu, vì chúng không phát ra `PreToolUse` | | `PreToolUse` | Agent bắt đầu sử dụng tool | Xóa cờ chờ, đặt Agent thành `working`, đặt `current_tool`. Nếu tool là `Agent`, tạo bản ghi Subagent | | `PostToolUse` | Tool hoàn tất | Xóa cờ chờ (xử lý các phê duyệt prompt xin quyền mà Notification đã đóng dấu giữa lúc tool đang chạy). Xóa `current_tool`. Agent ở lại `working` | diff --git a/README.md b/README.md index b547f7e1..17c54d6d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A professional dashboard to track and visualize your Claude Code agent sessions, ![Claude Code](https://img.shields.io/badge/Claude_Code-orange?style=flat-square&logo=claude&logoColor=white) ![Claude Code Plugins](https://img.shields.io/badge/Claude_Code-Plugins_&_Skills-orange?style=flat-square&logo=anthropic&logoColor=white) ![Model Context Protocol](https://img.shields.io/badge/Model_Context_Protocol-1.0-0f766e?style=flat-square&logo=modelcontextprotocol&logoColor=white) -![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white) ![Python](https://img.shields.io/badge/Python-%3E%3D3.6-3776AB?style=flat-square&logo=python&logoColor=white) ![Express](https://img.shields.io/badge/Express-4.21-000000?style=flat-square&logo=express&logoColor=white) ![ws](https://img.shields.io/badge/ws-WebSocket_server-010101?style=flat-square&logo=socketdotio&logoColor=white) @@ -319,7 +319,7 @@ The dashboard offers a comprehensive set of features to monitor and analyze your ### Prerequisites -- **Node.js** >= 18.0.0 (22+ recommended) +- **Node.js** >= 20.0.0 (22+ recommended) - **npm** >= 9.0.0 ### 1. Install @@ -547,7 +547,8 @@ Persisted statuses: `active | completed | error | abandoned`. The ```mermaid stateDiagram-v2 - [*] --> waiting: SessionStart (status=active + flag) + [*] --> waiting: SessionStart startup/resume/clear (status=active + flag) + active --> active: SessionStart compact (mid-turn — state preserved, no flag) waiting --> active: UserPromptSubmit / PreToolUse / PostToolUse active --> waiting: Stop, non-error (flag re-stamped) active --> waiting: Permission Notification (agent → waiting) @@ -1246,7 +1247,7 @@ The dashboard processes these Claude Code hook types: | Hook Type | Trigger | Dashboard Action | | ------------------- | ------------------------------ | -------------------------------------------------------------------------------------------- | -| `SessionStart` | Claude Code session begins | Creates session and main agent. Stamps `awaiting_input_since` (with `awaiting_reason=session_start`) so a fresh session lands in **Waiting**. Reactivates resumed sessions. Abandons orphaned sessions with no activity for `DASHBOARD_STALE_MINUTES` (default 180) | +| `SessionStart` | Claude Code session begins | Creates session and main agent. Stamps `awaiting_input_since` (with `awaiting_reason=session_start`) so a fresh session lands in **Waiting** — except a `compact`-source SessionStart (mid-turn auto-compaction), which leaves the flag untouched so a working session stays **Active**. Reactivates resumed sessions. Abandons orphaned sessions with no activity for `DASHBOARD_STALE_MINUTES` (default 180) | | `UserPromptSubmit` | User hits enter on a prompt | Clears the waiting flag and promotes the main agent to `working` — the only signal that text-only assistant turns have started, since they emit no `PreToolUse` | | `PreToolUse` | Agent starts using a tool | Clears the waiting flag, sets agent to `working`, sets `current_tool`. If tool is `Agent`, creates a subagent record | | `PostToolUse` | Tool execution completed | Clears the waiting flag (handles permission-prompt approvals where the Notification stamped it mid-tool). Clears `current_tool`. Agent stays `working` | diff --git a/SETUP.md b/SETUP.md index 677b83f1..99ad8e62 100644 --- a/SETUP.md +++ b/SETUP.md @@ -484,7 +484,7 @@ These warnings are **harmless**. `better-sqlite3` is an optional dependency — You do **not** need Python, Visual Studio Build Tools, or any C++ compiler to run this project on Node 22+. -If you are on Node 18 or 20 and `better-sqlite3` prebuilds are not available for your platform, you have two options: +If you are on Node 20 or 21 and `better-sqlite3` prebuilds are not available for your platform (there is no `node:sqlite` fallback below Node 22), you have two options: 1. **Upgrade to Node.js 22+** — the built-in `node:sqlite` fallback requires no native compilation at all 2. **Install build tools** and run `npm rebuild better-sqlite3`: diff --git a/client/README.md b/client/README.md index cfbea6b4..045720ba 100644 --- a/client/README.md +++ b/client/README.md @@ -3,7 +3,7 @@ Enterprise-grade React + TypeScript dashboard for real-time Claude Code agent monitoring. ![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) +![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-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) ![Javascript](https://img.shields.io/badge/JavaScript-ES6-F7DF1E?style=flat-square&logo=javascript&logoColor=white) @@ -958,7 +958,7 @@ graph TB ### Prerequisites -- Node.js >= 18.0.0 +- Node.js >= 20.0.0 - npm >= 9.0.0 ### Setup diff --git a/desktop/package-lock.json b/desktop/package-lock.json index c3ccb23f..5caf0bea 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-dashboard-desktop", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-dashboard-desktop", - "version": "1.3.0", + "version": "1.4.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/desktop/package.json b/desktop/package.json index 5c1b487c..0269dfd9 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "agent-dashboard-desktop", - "version": "1.3.0", + "version": "1.4.0", "private": true, "description": "Native macOS and Windows desktop shell for Claude Code Agent Monitor.", "author": "Son Nguyen ", diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 5f4caabb..735ffd5c 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -180,8 +180,8 @@ CREATE TABLE sessions ( | `ended_at` | TEXT | YES | ISO 8601 timestamp on terminal transition | | `metadata` | TEXT | YES | JSON blob for extras (turn duration totals, thinking blocks, …) | | `updated_at` | TEXT | NO | Bumped on every event for staleness detection | -| `awaiting_input_since` | TEXT | YES | ISO 8601 stamp set when the session is **Waiting** (Stop, SessionStart, permission Notification, or watchdog user-interrupt/Esc recovery). NULL otherwise | -| `awaiting_reason` | TEXT | YES | Why the row is waiting: `notification`, `stop`, `session_start`, or `interrupted`. Set/cleared in lock-step with `awaiting_input_since` (SessionStart→`session_start`, Stop→`stop`, permission/input Notification→`notification`, watchdog/Esc recovery→`interrupted`). NULL otherwise | +| `awaiting_input_since` | TEXT | YES | ISO 8601 stamp set when the session is **Waiting** (Stop, SessionStart with source `startup`/`resume`/`clear`, permission Notification, or watchdog user-interrupt/Esc recovery). NULL otherwise. A SessionStart with source `compact` (auto-compaction fires mid-turn while Claude is working) leaves this column untouched, so a genuinely-active session is not mislabeled Waiting | +| `awaiting_reason` | TEXT | YES | Why the row is waiting: `notification`, `stop`, `session_start`, or `interrupted`. Set/cleared in lock-step with `awaiting_input_since` (SessionStart→`session_start`, Stop→`stop`, permission/input Notification→`notification`, watchdog/Esc recovery→`interrupted`). NULL otherwise. Exception: a `compact`-source SessionStart preserves the existing value (neither stamps `session_start` nor clears it) | | `transcript_path` | TEXT | YES | Absolute path to the session's JSONL transcript. Written by `routes/hooks.js` on the first event that carries it (subsequent events no-op via a SQL guard) and read by the periodic compaction sweep — so the sweep touches only active session rows instead of scanning the entire `events` table for `json_extract(data,'$.transcript_path')`. Backfilled once from `events` by the `db.js` migration | **Constraints:** @@ -192,7 +192,8 @@ CREATE TABLE sessions ( ```mermaid stateDiagram-v2 - [*] --> waiting: SessionStart (status=active + awaiting_input_since) + [*] --> waiting: SessionStart startup/resume/clear (status=active + awaiting_input_since) + active --> active: SessionStart compact (mid-turn — state preserved) waiting --> active: UserPromptSubmit / PreToolUse / PostToolUse active --> waiting: Stop (non-error) / Permission Notification active --> waiting: Esc cancel (watchdog marker or idle timeout) diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index d7168f22..06a3b2ba 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -142,7 +142,7 @@ graph TB ### Prerequisites -- Node.js >= 18.0.0 +- Node.js >= 20.0.0 - npm >= 9.0.0 ### Setup @@ -311,7 +311,7 @@ docker compose down ```dockerfile # Build stage -FROM node:18-alpine AS builder +FROM node:22-alpine AS builder WORKDIR /app @@ -325,7 +325,7 @@ COPY client ./client RUN cd client && npm run build # Production stage -FROM node:18-alpine +FROM node:22-alpine WORKDIR /app diff --git a/docs/HOOKS.md b/docs/HOOKS.md index bce7dabd..6affe67e 100644 --- a/docs/HOOKS.md +++ b/docs/HOOKS.md @@ -226,7 +226,7 @@ ls -la .githooks/ ### 1. SessionStart -Triggered when a Claude Code session starts (fresh launch, `--resume`, `/clear`, etc.). +Triggered when a Claude Code session starts. The `source` field distinguishes the trigger: `startup` (fresh launch), `resume` (`--resume`/`--continue`), `clear` (`/clear`), and `compact` — which fires **mid-turn** when auto-compaction kicks in while Claude is actively working, not at a fresh prompt. **Payload Example:** @@ -242,7 +242,7 @@ Triggered when a Claude Code session starts (fresh launch, `--resume`, `/clear`, **Purpose:** - Create the session and main-agent records on first contact -- Stamp `awaiting_input_since` (with `awaiting_reason` = `session_start`) so the dashboard shows the row in **Waiting** from the moment the CLI lands at a prompt +- Stamp `awaiting_input_since` (with `awaiting_reason` = `session_start`) so the dashboard shows the row in **Waiting** from the moment the CLI lands at a prompt — **only for `startup`/`resume`/`clear`**. A `compact`-source SessionStart fires mid-turn while Claude is working, so it leaves the awaiting flag untouched: a genuinely-active session stays **Active** (not flipped to Waiting), and a session that compacted while idle keeps its existing Waiting flag and reason - Reactivate completed/abandoned sessions on resume - Sweep other active sessions whose last activity is older than `DASHBOARD_STALE_MINUTES` (default 180), marking them `abandoned` with their agents `completed` diff --git a/docs/README.md b/docs/README.md index 15dd1f40..74b5236f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -176,7 +176,7 @@ graph TB end subgraph "Backend" - Express[Express Server
Node.js 18+] + Express[Express Server
Node.js 20+] DB[(SQLite Database)] WS[WebSocket Server] end @@ -214,7 +214,7 @@ graph TB | Layer | Technology | |-------|------------| | **Frontend** | React 18, TypeScript 5.7, Vite 6, Tailwind CSS | -| **Backend** | Node.js 18+, Express 4.21, WebSocket | +| **Backend** | Node.js 20+, Express 4.21, WebSocket | | **Database** | SQLite 3 (better-sqlite3 or node:sqlite) | | **Integration** | Claude Code Hooks, MCP Server | diff --git a/index.html b/index.html index 57677b82..eff09320 100644 --- a/index.html +++ b/index.html @@ -3775,7 +3775,7 @@

A focused stack.

⚛️
React 18
+ TypeScript 5
Vite 6
HMR + bundle
-
🚂
Express 4
Node.js 18+
+
🚂
Express 4
Node.js 20+
🗃️
SQLite
better-sqlite3 + WAL
🔌
WebSocket
RFC 6455 push
📡
MCP 1.0
stdio / HTTP / REPL
diff --git a/package-lock.json b/package-lock.json index f3e98e04..86a85482 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "name": "agent-dashboard", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-dashboard", - "version": "1.3.0", + "version": "1.4.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", @@ -20,19 +21,22 @@ "web-push": "^3.6.7", "ws": "^8.18.0" }, + "bin": { + "ccam": "bin/ccam.js" + }, "devDependencies": { "concurrently": "^9.1.2", "js-yaml": "^4.1.0", "prettier": "^3.8.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "funding": { "url": "https://github.com/sponsors/hoangsonww" }, "optionalDependencies": { - "better-sqlite3": "^11.7.0" + "better-sqlite3": "^12.0.0" } }, "node_modules/@babel/runtime": { @@ -279,15 +283,18 @@ "optional": true }, "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" } }, "node_modules/bindings": { diff --git a/package.json b/package.json index 8f4ea86b..d6595c60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-dashboard", - "version": "1.3.0", + "version": "1.4.0", "bin": { "ccam": "bin/ccam.js" }, @@ -53,6 +53,7 @@ ], "scripts": { "prepare": "git config core.hooksPath .husky || true", + "postinstall": "node scripts/postinstall.js", "dev": "node scripts/dev.js", "dev:raw": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"", "dev:server": "node --watch server/index.js", @@ -108,7 +109,7 @@ "ws": "^8.18.0" }, "optionalDependencies": { - "better-sqlite3": "^11.7.0" + "better-sqlite3": "^12.0.0" }, "devDependencies": { "concurrently": "^9.1.2", @@ -116,6 +117,6 @@ "prettier": "^3.8.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/scripts/import-history.js b/scripts/import-history.js index e3a9581b..fc1a4290 100644 --- a/scripts/import-history.js +++ b/scripts/import-history.js @@ -2280,7 +2280,12 @@ function classifyJsonl(filePath) { // ancestor chain (rather than just parent/grandparent) stops workflow // inner-agent files from being misimported as bogus top-level sessions when a // user points the directory importer at a tree that contains workflow runs. - const segments = path.dirname(filePath).split(path.sep); + // Split on BOTH separators rather than the platform-specific path.sep: + // transcript paths can arrive in POSIX form even on Windows (forwarded + // household hooks, imported trees, the unit tests), and splitting a + // "/a/b/subagents/x.jsonl" path on "\\" would yield one giant segment that + // never equals "subagents", misclassifying every subagent as a session. + const segments = path.dirname(filePath).split(/[\\/]/); if (segments.includes("subagents")) return "subagent"; return "session"; } diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 00000000..1b1525d9 --- /dev/null +++ b/scripts/postinstall.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node +/** + * @file postinstall.js + * @description Root `postinstall` hook: after a bare `npm install` at the repo + * root, install the React client's dependencies too, so a single root install + * yields a buildable/runnable tree (the client's fonts and build deps live in + * `client/package.json`). The step is a safe no-op when the `client/` workspace + * is absent — production/Docker stages that copy only the root manifest, the + * MCP image's `file:..` link, and the published tarball all install without a + * client checkout, and must not fail here. Skipped entirely under + * `npm install --ignore-scripts` (run `cd client && npm install` manually then). + * @author Son Nguyen + */ + +const { spawnSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const clientDir = path.join(__dirname, "..", "client"); +const clientManifest = path.join(clientDir, "package.json"); + +// No client checkout in this context (Docker server/MCP stages, packed tarball, +// server-only installs). Nothing to do — succeed quietly so the parent install +// is not broken. +if (!fs.existsSync(clientManifest)) { + console.log("[postinstall] client/ not present — skipping client dependency install."); + process.exit(0); +} + +console.log("[postinstall] installing client dependencies (client/)..."); + +// `shell: true` is required on Windows so npm's `.cmd` shim resolves (Node +// rejects spawning `.cmd`/`.bat` directly since 18.20 / CVE-2024-27980); the +// fixed arg list has no shell-significant characters, so this stays safe. +const result = spawnSync("npm", ["install"], { + cwd: clientDir, + stdio: "inherit", + shell: true, +}); + +if (result.error) { + console.error("[postinstall] failed to launch npm for the client install:", result.error.message); + process.exit(1); +} + +process.exit(result.status === null ? 1 : result.status); diff --git a/server/README.md b/server/README.md index ab853666..a3dedf59 100644 --- a/server/README.md +++ b/server/README.md @@ -3,7 +3,7 @@ Enterprise-grade Node.js backend for Claude Code agent monitoring with real-time WebSocket updates. ![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) +![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white) ![Express](https://img.shields.io/badge/Express-4.21-000000?style=flat-square&logo=express&logoColor=white) ![Javascript](https://img.shields.io/badge/JavaScript-ES6-F7DF1E?style=flat-square&logo=javascript&logoColor=white) ![SQLite](https://img.shields.io/badge/SQLite-3-003B57?style=flat-square&logo=sqlite&logoColor=white) @@ -875,7 +875,8 @@ const DEFAULT_PRICING = [ ```mermaid stateDiagram-v2 - [*] --> waiting: SessionStart (status=active + flag) + [*] --> waiting: SessionStart startup/resume/clear (status=active + flag) + active --> active: SessionStart compact (mid-turn — state preserved, no flag) waiting --> active: UserPromptSubmit / PreToolUse / PostToolUse active --> waiting: Stop (non-error, flag re-stamped) active --> waiting: Permission Notification (agent → waiting) @@ -1340,7 +1341,7 @@ sudo systemctl start agent-dashboard ```dockerfile # Dockerfile (root of project) -FROM node:18-alpine +FROM node:22-alpine WORKDIR /app diff --git a/server/__tests__/api.test.js b/server/__tests__/api.test.js index 18bbb7f8..fafe3196 100644 --- a/server/__tests__/api.test.js +++ b/server/__tests__/api.test.js @@ -935,6 +935,97 @@ describe("Hook Event Processing", () => { assert.equal(main.awaiting_reason, "session_start"); }); + it("should NOT flip a genuinely-working session to Waiting on SessionStart source=compact", async () => { + // Auto-compaction fires SessionStart(source=compact) MID-TURN while Claude + // is actively working. The session is genuinely Active and must stay Active + // — a compact must not re-stamp it 'session_start' Waiting. This is the + // "in-progress session shows as Waiting" bug. + const sid = "hook-sess-compact-active"; + await post("/api/hooks/event", { + hook_type: "SessionStart", + data: { session_id: sid, source: "startup" }, + }); + // User submits a prompt → clears the session_start flag, main → working. + await post("/api/hooks/event", { + hook_type: "UserPromptSubmit", + data: { session_id: sid, prompt: "do a big task" }, + }); + const working = await fetch(`/api/sessions/${sid}`); + assert.equal( + working.body.session.awaiting_input_since, + null, + "precondition: session is Active (working) before compaction" + ); + + // Auto-compaction kicks in mid-turn. + await post("/api/hooks/event", { + hook_type: "SessionStart", + data: { session_id: sid, source: "compact" }, + }); + + const afterSess = await fetch(`/api/sessions/${sid}`); + assert.equal(afterSess.body.session.status, "active"); + assert.equal( + afterSess.body.session.awaiting_input_since, + null, + "mid-turn compact must NOT stamp the Waiting flag on a working session" + ); + assert.equal(afterSess.body.session.awaiting_reason, null); + + const agentsRes = await fetch(`/api/agents?session_id=${sid}`); + const main = agentsRes.body.agents.find((a) => a.type === "main"); + assert.equal(main.status, "working", "main agent should stay working through a compaction"); + assert.equal(main.awaiting_input_since, null); + }); + + it("should PRESERVE the Waiting flag on SessionStart source=compact when idle at the prompt", async () => { + // /compact run while the session sits idle at the prompt: it was already + // Waiting (from the prior Stop) and must stay Waiting. Compact must neither + // promote the agent to working nor overwrite the awaiting_reason. + const sid = "hook-sess-compact-idle"; + await post("/api/hooks/event", { + hook_type: "SessionStart", + data: { session_id: sid, source: "startup" }, + }); + await post("/api/hooks/event", { + hook_type: "UserPromptSubmit", + data: { session_id: sid, prompt: "answer this" }, + }); + await post("/api/hooks/event", { + hook_type: "Stop", + data: { session_id: sid, stop_reason: "end_turn" }, + }); + const before = await fetch(`/api/sessions/${sid}`); + assert.ok(before.body.session.awaiting_input_since, "precondition: Waiting after Stop"); + assert.equal(before.body.session.awaiting_reason, "stop"); + + // /compact at idle. + await post("/api/hooks/event", { + hook_type: "SessionStart", + data: { session_id: sid, source: "compact" }, + }); + + const after = await fetch(`/api/sessions/${sid}`); + assert.ok( + after.body.session.awaiting_input_since, + "idle compact must preserve the Waiting flag" + ); + assert.equal( + after.body.session.awaiting_reason, + "stop", + "reason must remain 'stop', not be overwritten by 'session_start'" + ); + + const agentsRes = await fetch(`/api/agents?session_id=${sid}`); + const main = agentsRes.body.agents.find((a) => a.type === "main"); + assert.equal( + main.status, + "waiting", + "main agent should stay waiting through an idle compaction" + ); + assert.ok(main.awaiting_input_since); + }); + it("should clear awaiting_input_since and promote main to working on UserPromptSubmit", async () => { // The bug this guards: text-only assistant turns emit no PreToolUse, // so without UserPromptSubmit the Waiting badge would persist for the diff --git a/server/__tests__/awaiting-subagent-guard.test.js b/server/__tests__/awaiting-subagent-guard.test.js index 65c0f883..00da18ec 100644 --- a/server/__tests__/awaiting-subagent-guard.test.js +++ b/server/__tests__/awaiting-subagent-guard.test.js @@ -15,6 +15,7 @@ * This test lives in the fork's own suite so a future upstream merge that * silently reverts the guard fails loudly here (home-network monitor fork, * see decisions/). + * @author Son Nguyen */ const { describe, it, before, after } = require("node:test"); diff --git a/server/routes/hooks.js b/server/routes/hooks.js index 883009ca..306a8db8 100644 --- a/server/routes/hooks.js +++ b/server/routes/hooks.js @@ -608,26 +608,38 @@ const processEvent = db.transaction((hookType, data) => { case "SessionStart": { summary = data.source === "resume" ? "Session resumed" : "Session started"; - // Reactivation is already handled above for non-active sessions. - // Promote main agent from waiting → working if needed. - if (mainAgent && mainAgent.status === "waiting") { - stmts.updateAgent.run(null, "working", null, null, null, null, mainAgentId); + // Claude Code fires SessionStart with source ∈ startup|resume|clear|compact. + // 'compact' is the odd one out: it fires MID-TURN when auto-compaction + // kicks in while Claude is actively working, so the session is NOT sitting + // at an empty prompt. Preserve the pre-compaction state verbatim — a + // session compacting mid-turn is genuinely Active (main agent 'working', + // no awaiting flag) and must stay Active; one that compacted while idle was + // already Waiting (awaiting flag from the prior Stop) and must stay Waiting. + // Without this guard a mid-turn compact stamps 'session_start' and flips a + // genuinely-working session to Waiting. startup/resume/clear DO land at a + // fresh prompt, so they still stamp the Waiting flag below. + if (data.source !== "compact") { + // Reactivation is already handled above for non-active sessions. + // Promote main agent from waiting → working if needed. + if (mainAgent && mainAgent.status === "waiting") { + stmts.updateAgent.run(null, "working", null, null, null, null, mainAgentId); + } + + // A just-started or just-resumed session is sitting at a prompt + // waiting for the user's first message — Claude Code hasn't done + // anything yet. Stamp awaiting_input_since so it lands in Waiting + // from the moment the dashboard sees it. UserPromptSubmit (when the + // user hits enter) or PreToolUse (when Claude actually runs a tool) + // will clear the flag. + const sessionStartTs = new Date().toISOString(); + stmts.setSessionAwaitingInput.run(sessionStartTs, "session_start", sessionId); + if (mainAgentId) + stmts.setAgentAwaitingInput.run(sessionStartTs, "session_start", mainAgentId); } - // A just-started or just-resumed session is sitting at a prompt - // waiting for the user's first message — Claude Code hasn't done - // anything yet. Stamp awaiting_input_since so it lands in Waiting - // from the moment the dashboard sees it. UserPromptSubmit (when the - // user hits enter) or PreToolUse (when Claude actually runs a tool) - // will clear the flag. - const sessionStartTs = new Date().toISOString(); - stmts.setSessionAwaitingInput.run(sessionStartTs, "session_start", sessionId); - if (mainAgentId) - stmts.setAgentAwaitingInput.run(sessionStartTs, "session_start", mainAgentId); - - // Single broadcast pair with the final state — agents and sessions - // are now connected/active with the waiting flag set, so WS clients - // see the Waiting badge as soon as the SessionStart event lands. + // Single broadcast pair with the final state — for non-compact sources the + // waiting flag is now set; for compact the pre-existing state is untouched. + // WS clients see the correct badge as soon as the SessionStart event lands. broadcast("session_updated", stmts.getSession.get(sessionId)); if (mainAgentId) broadcast("agent_updated", stmts.getAgent.get(mainAgentId)); @@ -1419,11 +1431,15 @@ function livenessReap({ ignoreIdleGate = false } = {}) { // deployment (this host has BOTH local sessions the probe should keep // reaping, AND remote household-hook sessions it must leave alone) // without sacrificing local crash detection via DASHBOARD_LIVENESS_PROBE=0. - // path.isAbsolute() resolves with POSIX semantics here (this function - // never reaches this point on win32 — probe.available is only true once - // probeLiveCwds() has already ruled that out), so it's equivalent to a - // leading-"/" check but self-documenting rather than a raw string test. - if (!path.isAbsolute(sess.cwd)) continue; + // The probe only ever reports POSIX cwds (/proc + lsof, and it bails out + // entirely on win32), so "could this cwd be local?" is precisely "is it + // POSIX-absolute?" — a leading-"/" check. Use path.posix.isAbsolute() + // explicitly rather than the platform-sensitive path.isAbsolute(): in + // production this only runs on POSIX hosts so the two are identical, but + // the unit tests mock probeLiveCwds() to exercise this reaper on a Windows + // host too, where bare path.isAbsolute("D:\\Git\\ai-deck") would be true + // and wrongly reap a forwarded remote session. posix keeps it host-agnostic. + if (!path.posix.isAbsolute(sess.cwd)) continue; let resolvedCwd; try { diff --git a/wiki/i18n-content.js b/wiki/i18n-content.js index 73188abb..1e945db6 100644 --- a/wiki/i18n-content.js +++ b/wiki/i18n-content.js @@ -162,8 +162,8 @@ window.__WIKI_CONTENT_I18N = { Trigger: "触发条件", "Dashboard Action": "仪表盘动作", "Claude Code session begins": "Claude Code 会话开始", - "Creates session and main agent. Stamps awaiting_input_since (with awaiting_reason = session_start) so the row lands in Waiting from the start (the CLI is at a prompt). Reactivates resumed sessions. Abandons orphaned sessions with no activity for DASHBOARD_STALE_MINUTES (default 180).": - "创建会话和主 agent。打上 awaiting_input_since(附带 awaiting_reason = session_start)时间戳,使该行从一开始就落入 Waiting(CLI 处于提示符状态)。重新激活已恢复的会话。将在 DASHBOARD_STALE_MINUTES(默认 180)期间无活动的孤立会话标记为放弃。", + "Creates session and main agent. Stamps awaiting_input_since (with awaiting_reason = session_start) so the row lands in Waiting from the start (the CLI is at a prompt) — except a compact-source SessionStart (mid-turn auto-compaction), which leaves the flag untouched so a working session stays Active. Reactivates resumed sessions. Abandons orphaned sessions with no activity for DASHBOARD_STALE_MINUTES (default 180).": + "创建会话和主 agent。打上 awaiting_input_since(附带 awaiting_reason = session_start)时间戳,使该行从一开始就落入 Waiting(CLI 处于提示符状态)——但 compact 来源的 SessionStart(回合中自动压缩)不改动标志,使正在工作的会话保持 Active。重新激活已恢复的会话。将在 DASHBOARD_STALE_MINUTES(默认 180)期间无活动的孤立会话标记为放弃。", "User hits enter on a prompt": "用户在提示符处按下回车", 'Clears the waiting flag and promotes the main agent to Working. The only reliable signal that text-only assistant turns have started — they emit no PreToolUse before Stop.': '清除等待标志,并将主 agent 提升为 Working。这是纯文本助手轮次已开始的唯一可靠信号——它们在 Stop 之前不会发出 PreToolUse。', @@ -1403,8 +1403,8 @@ window.__WIKI_CONTENT_I18N = { Trigger: "Điều kiện kích hoạt", "Dashboard Action": "Hành động trên Dashboard", "Claude Code session begins": "Phiên Claude Code bắt đầu", - "Creates session and main agent. Stamps awaiting_input_since (with awaiting_reason = session_start) so the row lands in Waiting from the start (the CLI is at a prompt). Reactivates resumed sessions. Abandons orphaned sessions with no activity for DASHBOARD_STALE_MINUTES (default 180).": - "Tạo phiên và agent chính. Đóng dấu thời gian awaiting_input_since (với awaiting_reason = session_start) để hàng đó nằm trong Waiting ngay từ đầu (CLI đang ở dấu nhắc). Kích hoạt lại các phiên được tiếp tục. Bỏ rơi các phiên mồ côi không có hoạt động trong DASHBOARD_STALE_MINUTES (mặc định 180).", + "Creates session and main agent. Stamps awaiting_input_since (with awaiting_reason = session_start) so the row lands in Waiting from the start (the CLI is at a prompt) — except a compact-source SessionStart (mid-turn auto-compaction), which leaves the flag untouched so a working session stays Active. Reactivates resumed sessions. Abandons orphaned sessions with no activity for DASHBOARD_STALE_MINUTES (default 180).": + "Tạo phiên và agent chính. Đóng dấu thời gian awaiting_input_since (với awaiting_reason = session_start) để hàng đó nằm trong Waiting ngay từ đầu (CLI đang ở dấu nhắc) — trừ SessionStart nguồn compact (nén giữa lượt), vốn giữ nguyên cờ để phiên đang chạy vẫn Active. Kích hoạt lại các phiên được tiếp tục. Bỏ rơi các phiên mồ côi không có hoạt động trong DASHBOARD_STALE_MINUTES (mặc định 180).", "User hits enter on a prompt": "Người dùng nhấn enter tại dấu nhắc", 'Clears the waiting flag and promotes the main agent to Working. The only reliable signal that text-only assistant turns have started — they emit no PreToolUse before Stop.': 'Xóa cờ chờ và nâng agent chính lên Working. Đây là tín hiệu đáng tin cậy duy nhất cho biết các lượt trợ lý chỉ-văn-bản đã bắt đầu — chúng không phát ra PreToolUse trước Stop.', @@ -2672,8 +2672,8 @@ window.__WIKI_CONTENT_I18N = { Trigger: "트리거", "Dashboard Action": "대시보드 동작", "Claude Code session begins": "Claude Code 세션 시작", - "Creates session and main agent. Stamps awaiting_input_since (with awaiting_reason = session_start) so the row lands in Waiting from the start (the CLI is at a prompt). Reactivates resumed sessions. Abandons orphaned sessions with no activity for DASHBOARD_STALE_MINUTES (default 180).": - "세션과 메인 에이전트를 생성합니다. CLI가 프롬프트 상태이므로 처음부터 Waiting에 위치하도록 awaiting_input_since를(awaiting_reason = session_start와 함께) 기록합니다. 재개된 세션을 다시 활성화합니다. DASHBOARD_STALE_MINUTES(기본값 180) 동안 활동이 없는 고아 세션은 중단 처리합니다.", + "Creates session and main agent. Stamps awaiting_input_since (with awaiting_reason = session_start) so the row lands in Waiting from the start (the CLI is at a prompt) — except a compact-source SessionStart (mid-turn auto-compaction), which leaves the flag untouched so a working session stays Active. Reactivates resumed sessions. Abandons orphaned sessions with no activity for DASHBOARD_STALE_MINUTES (default 180).": + "세션과 메인 에이전트를 생성합니다. CLI가 프롬프트 상태이므로 처음부터 Waiting에 위치하도록 awaiting_input_since를(awaiting_reason = session_start와 함께) 기록합니다. 단, compact 소스 SessionStart(턴 도중 자동 압축)는 플래그를 그대로 두어 작업 중인 세션이 Active 상태를 유지합니다. 재개된 세션을 다시 활성화합니다. DASHBOARD_STALE_MINUTES(기본값 180) 동안 활동이 없는 고아 세션은 중단 처리합니다.", "User hits enter on a prompt": "사용자가 프롬프트에서 엔터를 입력", 'Clears the waiting flag and promotes the main agent to Working. The only reliable signal that text-only assistant turns have started — they emit no PreToolUse before Stop.': '대기 플래그를 지우고 메인 에이전트를 Working으로 전환합니다. 텍스트로만 이루어진 어시스턴트 턴이 시작되었음을 알 수 있는 유일하게 신뢰할 수 있는 신호입니다 — 이런 턴은 Stop 이전에 PreToolUse를 전혀 발생시키지 않습니다.', diff --git a/wiki/index.html b/wiki/index.html index 391075dd..f0d4e49f 100644 --- a/wiki/index.html +++ b/wiki/index.html @@ -673,7 +673,7 @@

Claude Code Agent Monitor

- Node.js ≥ 18 + Node.js ≥ 20 Express 4.21 React 18.3 TypeScript 5.7 @@ -687,7 +687,7 @@

Claude Code Agent Monitor

Swagger 3.0 i18next 22.4 Mermaid 10.2 - better-sqlite3 11.7 + better-sqlite3 12 React Router 6.28 Lucide Icons D3.js 7 @@ -739,7 +739,7 @@

Claude Code Agent Monitor

MIT License