claude-code-monitor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/dist/bin/ccm.d.ts +3 -0
- package/dist/bin/ccm.d.ts.map +1 -0
- package/dist/bin/ccm.js +128 -0
- package/dist/components/Dashboard.d.ts +3 -0
- package/dist/components/Dashboard.d.ts.map +1 -0
- package/dist/components/Dashboard.js +64 -0
- package/dist/components/SessionCard.d.ts +10 -0
- package/dist/components/SessionCard.d.ts.map +1 -0
- package/dist/components/SessionCard.js +16 -0
- package/dist/components/Spinner.d.ts +7 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +39 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +17 -0
- package/dist/hook/handler.d.ts +9 -0
- package/dist/hook/handler.d.ts.map +1 -0
- package/dist/hook/handler.js +62 -0
- package/dist/hooks/useSessions.d.ts +7 -0
- package/dist/hooks/useSessions.d.ts.map +1 -0
- package/dist/hooks/useSessions.js +41 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/setup/index.d.ts +39 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +183 -0
- package/dist/store/file-store.d.ts +17 -0
- package/dist/store/file-store.d.ts.map +1 -0
- package/dist/store/file-store.js +134 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/focus.d.ts +16 -0
- package/dist/utils/focus.d.ts.map +1 -0
- package/dist/utils/focus.js +109 -0
- package/dist/utils/prompt.d.ts +7 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +20 -0
- package/dist/utils/status.d.ts +8 -0
- package/dist/utils/status.d.ts.map +1 -0
- package/dist/utils/status.js +10 -0
- package/dist/utils/time.d.ts +5 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +19 -0
- package/dist/utils/tty-cache.d.ts +12 -0
- package/dist/utils/tty-cache.d.ts.map +1 -0
- package/dist/utils/tty-cache.js +36 -0
- package/package.json +77 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-01-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release
|
|
13
|
+
- Real-time monitoring of multiple Claude Code sessions
|
|
14
|
+
- Terminal UI (TUI) with keyboard navigation
|
|
15
|
+
- Focus feature to switch to session's terminal tab
|
|
16
|
+
- Full support for iTerm2 and Terminal.app (TTY-based targeting)
|
|
17
|
+
- Limited support for Ghostty (app activation only)
|
|
18
|
+
- Automatic hook setup via `ccm setup`
|
|
19
|
+
- Session status tracking (running, waiting for input, stopped)
|
|
20
|
+
- File-based session state management (no server required)
|
|
21
|
+
- Session auto-cleanup on timeout (30 minutes) or TTY termination
|
|
22
|
+
- Commands: `ccm`, `ccm watch`, `ccm setup`, `ccm list`, `ccm clear`
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 onikan27
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Claude Code Monitor CLI
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/claude-code-monitor)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A CLI tool to monitor multiple Claude Code sessions in real-time from your terminal.
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img src="docs/demo.gif" alt="Claude Code Monitor Demo" width="1000">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 📑 Table of Contents
|
|
15
|
+
|
|
16
|
+
- [✨ Features](#-features)
|
|
17
|
+
- [📋 Requirements](#-requirements)
|
|
18
|
+
- [🚀 Installation](#-installation)
|
|
19
|
+
- [⚡ Quick Start](#-quick-start)
|
|
20
|
+
- [📖 Commands](#-commands)
|
|
21
|
+
- [⌨️ Keybindings](#️-keybindings-watch-mode)
|
|
22
|
+
- [🎨 Status Icons](#-status-icons)
|
|
23
|
+
- [🖥️ Supported Terminals](#️-supported-terminals)
|
|
24
|
+
- [💾 Data Storage](#-data-storage)
|
|
25
|
+
- [📦 Programmatic Usage](#-programmatic-usage)
|
|
26
|
+
- [🔧 Troubleshooting](#-troubleshooting)
|
|
27
|
+
- [🔒 Security](#-security)
|
|
28
|
+
- [⚠️ Disclaimer](#️-disclaimer)
|
|
29
|
+
- [📝 Changelog](#-changelog)
|
|
30
|
+
- [📄 License](#-license)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## ✨ Features
|
|
35
|
+
|
|
36
|
+
- 🔌 **Serverless** - File-based session state management (no API server required)
|
|
37
|
+
- 🔄 **Real-time** - Auto-updates on file changes
|
|
38
|
+
- 🎯 **Tab Focus** - Instantly switch to the terminal tab of a selected session
|
|
39
|
+
- 🎨 **Simple UI** - Displays only status and directory
|
|
40
|
+
- ⚡ **Easy Setup** - One command `ccm` for automatic setup and launch
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 📋 Requirements
|
|
45
|
+
|
|
46
|
+
- **macOS** (focus feature is macOS only)
|
|
47
|
+
- **Node.js** >= 18.0.0
|
|
48
|
+
- **Claude Code** installed
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🚀 Installation
|
|
53
|
+
|
|
54
|
+
### Global install (Recommended)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install -g claude-code-monitor
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Run with npx (no install required)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx claude-code-monitor
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **Note**: With npx, you must run `npx claude-code-monitor` each time (the `ccm` shortcut is only available with global install). Global install is recommended since this tool requires hook setup and is designed for continuous use.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## ⚡ Quick Start
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
ccm
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
On first run, it automatically sets up hooks and launches the monitor.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 📖 Commands
|
|
81
|
+
|
|
82
|
+
| Command | Alias | Description |
|
|
83
|
+
|---------|-------|-------------|
|
|
84
|
+
| `ccm` | - | Launch monitor TUI (auto-setup if not configured) |
|
|
85
|
+
| `ccm watch` | `ccm w` | Launch monitor TUI |
|
|
86
|
+
| `ccm setup` | - | Configure Claude Code hooks |
|
|
87
|
+
| `ccm list` | `ccm ls` | List sessions |
|
|
88
|
+
| `ccm clear` | - | Clear all sessions |
|
|
89
|
+
| `ccm --version` | `ccm -V` | Show version |
|
|
90
|
+
| `ccm --help` | `ccm -h` | Show help |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## ⌨️ Keybindings (watch mode)
|
|
95
|
+
|
|
96
|
+
| Key | Action |
|
|
97
|
+
|-----|--------|
|
|
98
|
+
| `↑` / `k` | Move up |
|
|
99
|
+
| `↓` / `j` | Move down |
|
|
100
|
+
| `Enter` / `f` | Focus selected session |
|
|
101
|
+
| `1-9` | Quick select & focus by number |
|
|
102
|
+
| `c` | Clear all sessions |
|
|
103
|
+
| `q` / `Esc` | Quit |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 🎨 Status Icons
|
|
108
|
+
|
|
109
|
+
| Icon | Status | Description |
|
|
110
|
+
|------|--------|-------------|
|
|
111
|
+
| `●` | Running | Claude Code is processing |
|
|
112
|
+
| `◐` | Waiting | Waiting for user input (e.g., permission prompt) |
|
|
113
|
+
| `✓` | Done | Session ended |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 🖥️ Supported Terminals
|
|
118
|
+
|
|
119
|
+
Focus feature works with the following terminals:
|
|
120
|
+
|
|
121
|
+
| Terminal | Focus Support | Notes |
|
|
122
|
+
|----------|--------------|-------|
|
|
123
|
+
| iTerm2 | ✅ Full | TTY-based window/tab targeting |
|
|
124
|
+
| Terminal.app | ✅ Full | TTY-based window/tab targeting |
|
|
125
|
+
| Ghostty | ⚠️ Limited | Activates app only (cannot target specific window/tab) |
|
|
126
|
+
|
|
127
|
+
> **Note**: Other terminals (Alacritty, kitty, Warp, etc.) can use monitoring but focus feature is not supported.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 💾 Data Storage
|
|
132
|
+
|
|
133
|
+
Session data is stored in `~/.claude-monitor/sessions.json`.
|
|
134
|
+
|
|
135
|
+
### What is stored
|
|
136
|
+
|
|
137
|
+
| Field | Description |
|
|
138
|
+
|-------|-------------|
|
|
139
|
+
| `session_id` | Claude Code session identifier |
|
|
140
|
+
| `cwd` | Working directory path |
|
|
141
|
+
| `tty` | Terminal device path (e.g., `/dev/ttys001`) |
|
|
142
|
+
| `status` | Session status (running/waiting_input/stopped) |
|
|
143
|
+
| `updated_at` | Last update timestamp |
|
|
144
|
+
|
|
145
|
+
Data is automatically removed after 30 minutes of inactivity or when the terminal session ends.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 📦 Programmatic Usage
|
|
150
|
+
|
|
151
|
+
Can also be used as a library:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { getSessions, getStatusDisplay } from 'claude-code-monitor';
|
|
155
|
+
|
|
156
|
+
const sessions = getSessions();
|
|
157
|
+
for (const session of sessions) {
|
|
158
|
+
const { symbol, label } = getStatusDisplay(session.status);
|
|
159
|
+
console.log(`${symbol} ${label}: ${session.cwd}`);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 🔧 Troubleshooting
|
|
166
|
+
|
|
167
|
+
### Sessions not showing
|
|
168
|
+
|
|
169
|
+
1. Run `ccm setup` to verify hook configuration
|
|
170
|
+
2. Check if `~/.claude/settings.json` contains hook settings
|
|
171
|
+
3. Restart Claude Code
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Check configuration
|
|
175
|
+
cat ~/.claude/settings.json | grep ccm
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Focus not working
|
|
179
|
+
|
|
180
|
+
1. Verify you're using macOS
|
|
181
|
+
2. Verify you're using iTerm2, Terminal.app, or Ghostty
|
|
182
|
+
3. Check System Preferences > Privacy & Security > Accessibility permissions
|
|
183
|
+
|
|
184
|
+
### Reset session data
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
ccm clear
|
|
188
|
+
# or
|
|
189
|
+
rm ~/.claude-monitor/sessions.json
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 🔒 Security
|
|
195
|
+
|
|
196
|
+
- This tool modifies `~/.claude/settings.json` to register hooks
|
|
197
|
+
- Focus feature uses AppleScript to control terminal applications
|
|
198
|
+
- All data is stored locally; no network requests are made
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## ⚠️ Disclaimer
|
|
203
|
+
|
|
204
|
+
This is an unofficial community tool and is not affiliated with, endorsed by, or associated with Anthropic.
|
|
205
|
+
"Claude" and "Claude Code" are trademarks of Anthropic.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 📝 Changelog
|
|
210
|
+
|
|
211
|
+
See [CHANGELOG.md](./CHANGELOG.md) for a list of changes.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 📄 License
|
|
216
|
+
|
|
217
|
+
MIT
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
<p align="center">Made with ❤️ for the Claude Code community</p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ccm.d.ts","sourceRoot":"","sources":["../../src/bin/ccm.tsx"],"names":[],"mappings":""}
|
package/dist/bin/ccm.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { render } from 'ink';
|
|
7
|
+
import { Dashboard } from '../components/Dashboard.js';
|
|
8
|
+
import { handleHookEvent } from '../hook/handler.js';
|
|
9
|
+
import { isHooksConfigured, setupHooks } from '../setup/index.js';
|
|
10
|
+
import { clearSessions, getSessions } from '../store/file-store.js';
|
|
11
|
+
import { getStatusDisplay } from '../utils/status.js';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const pkg = require('../../package.json');
|
|
14
|
+
const CLEAR_SCREEN = '\x1B[2J\x1B[0f';
|
|
15
|
+
/**
|
|
16
|
+
* Get TTY from ancestor processes
|
|
17
|
+
*/
|
|
18
|
+
const MAX_ANCESTOR_DEPTH = 5;
|
|
19
|
+
function getTtyFromAncestors() {
|
|
20
|
+
try {
|
|
21
|
+
let currentPid = process.ppid;
|
|
22
|
+
for (let i = 0; i < MAX_ANCESTOR_DEPTH; i++) {
|
|
23
|
+
const ttyName = execFileSync('ps', ['-o', 'tty=', '-p', String(currentPid)], {
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
26
|
+
}).trim();
|
|
27
|
+
const isValidTty = ttyName && ttyName !== '??' && ttyName !== '';
|
|
28
|
+
if (isValidTty) {
|
|
29
|
+
return `/dev/${ttyName}`;
|
|
30
|
+
}
|
|
31
|
+
const ppid = execFileSync('ps', ['-o', 'ppid=', '-p', String(currentPid)], {
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
34
|
+
}).trim();
|
|
35
|
+
if (!ppid)
|
|
36
|
+
break;
|
|
37
|
+
currentPid = parseInt(ppid, 10);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// TTY取得失敗は正常(バックグラウンド実行時など)
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const program = new Command();
|
|
46
|
+
program
|
|
47
|
+
.name('ccm')
|
|
48
|
+
.description('Claude Code Monitor - CLI-based session monitoring')
|
|
49
|
+
.version(pkg.version);
|
|
50
|
+
program
|
|
51
|
+
.command('watch')
|
|
52
|
+
.alias('w')
|
|
53
|
+
.description('Start the monitoring TUI')
|
|
54
|
+
.action(() => {
|
|
55
|
+
process.stdout.write(CLEAR_SCREEN);
|
|
56
|
+
const { waitUntilExit } = render(_jsx(Dashboard, {}));
|
|
57
|
+
waitUntilExit().catch(console.error);
|
|
58
|
+
});
|
|
59
|
+
program
|
|
60
|
+
.command('hook <event>')
|
|
61
|
+
.description('Handle a hook event from Claude Code (internal use)')
|
|
62
|
+
.action(async (event) => {
|
|
63
|
+
try {
|
|
64
|
+
const tty = getTtyFromAncestors();
|
|
65
|
+
await handleHookEvent(event, tty);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.error('Hook error:', e);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
program
|
|
73
|
+
.command('list')
|
|
74
|
+
.alias('ls')
|
|
75
|
+
.description('List all sessions')
|
|
76
|
+
.action(() => {
|
|
77
|
+
const sessions = getSessions();
|
|
78
|
+
if (sessions.length === 0) {
|
|
79
|
+
console.log('No active sessions');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const session of sessions) {
|
|
83
|
+
const cwd = session.cwd.replace(/^\/Users\/[^/]+/, '~');
|
|
84
|
+
const { symbol } = getStatusDisplay(session.status);
|
|
85
|
+
console.log(`${symbol} ${cwd}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
program
|
|
89
|
+
.command('clear')
|
|
90
|
+
.description('Clear all sessions')
|
|
91
|
+
.action(() => {
|
|
92
|
+
clearSessions();
|
|
93
|
+
console.log('Sessions cleared');
|
|
94
|
+
});
|
|
95
|
+
program
|
|
96
|
+
.command('setup')
|
|
97
|
+
.description('Setup Claude Code hooks for monitoring')
|
|
98
|
+
.action(async () => {
|
|
99
|
+
await setupHooks();
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Default action (when launched without arguments)
|
|
103
|
+
* - Run setup if not configured
|
|
104
|
+
* - Launch monitor if already configured
|
|
105
|
+
*/
|
|
106
|
+
async function defaultAction() {
|
|
107
|
+
if (!isHooksConfigured()) {
|
|
108
|
+
console.log('Initial setup required.\n');
|
|
109
|
+
await setupHooks();
|
|
110
|
+
// Verify setup was completed
|
|
111
|
+
if (!isHooksConfigured()) {
|
|
112
|
+
// Setup was cancelled
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.log('');
|
|
116
|
+
}
|
|
117
|
+
// Launch monitor
|
|
118
|
+
process.stdout.write(CLEAR_SCREEN);
|
|
119
|
+
const { waitUntilExit } = render(_jsx(Dashboard, {}));
|
|
120
|
+
await waitUntilExit();
|
|
121
|
+
}
|
|
122
|
+
// Default action when executed without commands
|
|
123
|
+
if (process.argv.length === 2) {
|
|
124
|
+
defaultAction().catch(console.error);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
program.parse();
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/components/Dashboard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B,wBAAgB,SAAS,IAAI,KAAK,CAAC,YAAY,CAoH9C"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { useSessions } from '../hooks/useSessions.js';
|
|
5
|
+
import { clearSessions } from '../store/file-store.js';
|
|
6
|
+
import { focusSession } from '../utils/focus.js';
|
|
7
|
+
import { SessionCard } from './SessionCard.js';
|
|
8
|
+
const QUICK_SELECT_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
9
|
+
export function Dashboard() {
|
|
10
|
+
const { sessions, loading, error } = useSessions();
|
|
11
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
+
const { exit } = useApp();
|
|
13
|
+
const focusSessionByIndex = (index) => {
|
|
14
|
+
const session = sessions[index];
|
|
15
|
+
if (session?.tty) {
|
|
16
|
+
focusSession(session.tty);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const handleQuickSelect = (input) => {
|
|
20
|
+
const index = parseInt(input, 10) - 1;
|
|
21
|
+
if (index < sessions.length) {
|
|
22
|
+
setSelectedIndex(index);
|
|
23
|
+
focusSessionByIndex(index);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const statusCounts = useMemo(() => sessions.reduce((counts, session) => {
|
|
27
|
+
counts[session.status]++;
|
|
28
|
+
return counts;
|
|
29
|
+
}, { running: 0, waiting_input: 0, stopped: 0 }), [sessions]);
|
|
30
|
+
useInput((input, key) => {
|
|
31
|
+
if (input === 'q' || key.escape) {
|
|
32
|
+
exit();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (key.upArrow || input === 'k') {
|
|
36
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (key.downArrow || input === 'j') {
|
|
40
|
+
setSelectedIndex((prev) => Math.min(sessions.length - 1, prev + 1));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (key.return || input === 'f') {
|
|
44
|
+
focusSessionByIndex(selectedIndex);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (QUICK_SELECT_KEYS.includes(input)) {
|
|
48
|
+
handleQuickSelect(input);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (input === 'c') {
|
|
52
|
+
clearSessions();
|
|
53
|
+
setSelectedIndex(0);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (loading) {
|
|
57
|
+
return _jsx(Text, { dimColor: true, children: "Loading..." });
|
|
58
|
+
}
|
|
59
|
+
if (error) {
|
|
60
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error.message] });
|
|
61
|
+
}
|
|
62
|
+
const { running, waiting_input: waitingInput, stopped } = statusCounts;
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Claude Code Monitor" }), _jsx(Text, { dimColor: true, children: " \u2502 " }), _jsxs(Text, { color: "green", children: ["\u25CF ", running] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "yellow", children: ["\u25D0 ", waitingInput] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "cyan", children: ["\u2713 ", stopped] })] }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", marginTop: 1, paddingX: 1, paddingY: 0, children: sessions.length === 0 ? (_jsx(Box, { paddingY: 1, children: _jsx(Text, { dimColor: true, children: "No active sessions" }) })) : (sessions.map((session, index) => (_jsx(SessionCard, { session: session, index: index, isSelected: index === selectedIndex }, `${session.session_id}:${session.tty || ''}`)))) }), _jsxs(Box, { marginTop: 1, justifyContent: "center", gap: 1, children: [_jsx(Text, { dimColor: true, children: "[\u2191\u2193]Select" }), _jsx(Text, { dimColor: true, children: "[Enter]Focus" }), _jsx(Text, { dimColor: true, children: "[1-9]Quick" }), _jsx(Text, { dimColor: true, children: "[c]Clear" }), _jsx(Text, { dimColor: true, children: "[q]Quit" })] })] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { Session } from '../types/index.js';
|
|
3
|
+
interface SessionCardProps {
|
|
4
|
+
session: Session;
|
|
5
|
+
index: number;
|
|
6
|
+
isSelected: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const SessionCard: React.NamedExoticComponent<SessionCardProps>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=SessionCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionCard.d.ts","sourceRoot":"","sources":["../../src/components/SessionCard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAKjD,UAAU,gBAAgB;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;CACrB;AAMD,eAAO,MAAM,WAAW,8CAiCtB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { memo } from 'react';
|
|
4
|
+
import { getStatusDisplay } from '../utils/status.js';
|
|
5
|
+
import { formatRelativeTime } from '../utils/time.js';
|
|
6
|
+
import { Spinner } from './Spinner.js';
|
|
7
|
+
function abbreviateHomePath(path) {
|
|
8
|
+
return path.replace(/^\/Users\/[^/]+/, '~');
|
|
9
|
+
}
|
|
10
|
+
export const SessionCard = memo(function SessionCard({ session, index, isSelected, }) {
|
|
11
|
+
const { symbol, color, label } = getStatusDisplay(session.status);
|
|
12
|
+
const dir = abbreviateHomePath(session.cwd);
|
|
13
|
+
const relativeTime = formatRelativeTime(session.updated_at);
|
|
14
|
+
const isRunning = session.status === 'running';
|
|
15
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [isSelected ? '>' : ' ', " [", index + 1, "]"] }), _jsx(Text, { children: " " }), _jsx(Box, { width: 10, children: isRunning ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { color: "green" }), _jsxs(Text, { color: color, children: [" ", label] })] })) : (_jsxs(Text, { color: color, children: [symbol, " ", label] })) }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: relativeTime.padEnd(8) }), _jsx(Text, { color: isSelected ? 'white' : 'gray', children: dir })] }));
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Spinner.d.ts","sourceRoot":"","sources":["../../src/components/Spinner.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA0C/B,UAAU,YAAY;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,OAAO,CAAC,EAAE,KAAe,EAAE,EAAE,YAAY,GAAG,KAAK,CAAC,YAAY,CAI7E"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
5
|
+
const FRAME_INTERVAL_MS = 120; // Slightly slower for less CPU usage
|
|
6
|
+
// Shared global spinner state - only ONE timer for all spinners
|
|
7
|
+
let globalFrame = 0;
|
|
8
|
+
let subscriberCount = 0;
|
|
9
|
+
let intervalId = null;
|
|
10
|
+
const listeners = new Set();
|
|
11
|
+
function subscribe(callback) {
|
|
12
|
+
listeners.add(callback);
|
|
13
|
+
subscriberCount++;
|
|
14
|
+
// Start timer only when first subscriber joins
|
|
15
|
+
if (subscriberCount === 1 && intervalId === null) {
|
|
16
|
+
intervalId = setInterval(() => {
|
|
17
|
+
globalFrame = (globalFrame + 1) % SPINNER_FRAMES.length;
|
|
18
|
+
for (const listener of listeners) {
|
|
19
|
+
listener();
|
|
20
|
+
}
|
|
21
|
+
}, FRAME_INTERVAL_MS);
|
|
22
|
+
}
|
|
23
|
+
return () => {
|
|
24
|
+
listeners.delete(callback);
|
|
25
|
+
subscriberCount--;
|
|
26
|
+
// Stop timer when last subscriber leaves
|
|
27
|
+
if (subscriberCount === 0 && intervalId !== null) {
|
|
28
|
+
clearInterval(intervalId);
|
|
29
|
+
intervalId = null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function getSnapshot() {
|
|
34
|
+
return globalFrame;
|
|
35
|
+
}
|
|
36
|
+
export function Spinner({ color = 'green' }) {
|
|
37
|
+
const frame = useSyncExternalStore(subscribe, getSnapshot);
|
|
38
|
+
return _jsx(Text, { color: color, children: SPINNER_FRAMES[frame] });
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for claude-code-monitor
|
|
3
|
+
*/
|
|
4
|
+
/** Package name used for npx commands */
|
|
5
|
+
export declare const PACKAGE_NAME = "claude-code-monitor";
|
|
6
|
+
/** Session timeout in milliseconds (30 minutes) */
|
|
7
|
+
export declare const SESSION_TIMEOUT_MS: number;
|
|
8
|
+
/** TTY cache TTL in milliseconds (30 seconds) */
|
|
9
|
+
export declare const TTY_CACHE_TTL_MS = 30000;
|
|
10
|
+
/** Hook event types supported by Claude Code */
|
|
11
|
+
export declare const HOOK_EVENTS: readonly ["UserPromptSubmit", "PreToolUse", "PostToolUse", "Notification", "Stop"];
|
|
12
|
+
export type HookEventName = (typeof HOOK_EVENTS)[number];
|
|
13
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,yCAAyC;AACzC,eAAO,MAAM,YAAY,wBAAwB,CAAC;AAElD,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,QAAiB,CAAC;AAEjD,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC,gDAAgD;AAChD,eAAO,MAAM,WAAW,oFAMd,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for claude-code-monitor
|
|
3
|
+
*/
|
|
4
|
+
/** Package name used for npx commands */
|
|
5
|
+
export const PACKAGE_NAME = 'claude-code-monitor';
|
|
6
|
+
/** Session timeout in milliseconds (30 minutes) */
|
|
7
|
+
export const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
8
|
+
/** TTY cache TTL in milliseconds (30 seconds) */
|
|
9
|
+
export const TTY_CACHE_TTL_MS = 30_000;
|
|
10
|
+
/** Hook event types supported by Claude Code */
|
|
11
|
+
export const HOOK_EVENTS = [
|
|
12
|
+
'UserPromptSubmit',
|
|
13
|
+
'PreToolUse',
|
|
14
|
+
'PostToolUse',
|
|
15
|
+
'Notification',
|
|
16
|
+
'Stop',
|
|
17
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HookEventName } from '../types/index.js';
|
|
2
|
+
/** @internal */
|
|
3
|
+
export declare const VALID_HOOK_EVENTS: ReadonlySet<string>;
|
|
4
|
+
/** @internal */
|
|
5
|
+
export declare function isValidHookEventName(name: string): name is HookEventName;
|
|
6
|
+
/** @internal */
|
|
7
|
+
export declare function isNonEmptyString(value: unknown): value is string;
|
|
8
|
+
export declare function handleHookEvent(eventName: string, tty?: string): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/hook/handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGlE,gBAAgB;AAChB,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAMhD,CAAC;AAEH,gBAAgB;AAChB,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,aAAa,CAExE;AAED,gBAAgB;AAChB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEhE;AAED,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmDpF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { updateSession } from '../store/file-store.js';
|
|
2
|
+
// Allowed hook event names (whitelist)
|
|
3
|
+
/** @internal */
|
|
4
|
+
export const VALID_HOOK_EVENTS = new Set([
|
|
5
|
+
'PreToolUse',
|
|
6
|
+
'PostToolUse',
|
|
7
|
+
'Notification',
|
|
8
|
+
'Stop',
|
|
9
|
+
'UserPromptSubmit',
|
|
10
|
+
]);
|
|
11
|
+
/** @internal */
|
|
12
|
+
export function isValidHookEventName(name) {
|
|
13
|
+
return VALID_HOOK_EVENTS.has(name);
|
|
14
|
+
}
|
|
15
|
+
/** @internal */
|
|
16
|
+
export function isNonEmptyString(value) {
|
|
17
|
+
return typeof value === 'string' && value.length > 0;
|
|
18
|
+
}
|
|
19
|
+
export async function handleHookEvent(eventName, tty) {
|
|
20
|
+
// Validate event name against whitelist
|
|
21
|
+
if (!isValidHookEventName(eventName)) {
|
|
22
|
+
console.error(`Invalid event name: ${eventName}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
// Read JSON from stdin
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for await (const chunk of process.stdin) {
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
}
|
|
30
|
+
const inputJson = Buffer.concat(chunks).toString('utf-8');
|
|
31
|
+
let hookPayload;
|
|
32
|
+
try {
|
|
33
|
+
hookPayload = JSON.parse(inputJson);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
console.error('Invalid JSON input');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
// Validate required fields
|
|
40
|
+
if (!isNonEmptyString(hookPayload.session_id)) {
|
|
41
|
+
console.error('Invalid or missing session_id');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
// Validate optional fields if present
|
|
45
|
+
if (hookPayload.cwd !== undefined && typeof hookPayload.cwd !== 'string') {
|
|
46
|
+
console.error('Invalid cwd: must be a string');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (hookPayload.notification_type !== undefined &&
|
|
50
|
+
typeof hookPayload.notification_type !== 'string') {
|
|
51
|
+
console.error('Invalid notification_type: must be a string');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const event = {
|
|
55
|
+
session_id: hookPayload.session_id,
|
|
56
|
+
cwd: hookPayload.cwd || process.cwd(),
|
|
57
|
+
tty,
|
|
58
|
+
hook_event_name: eventName,
|
|
59
|
+
notification_type: hookPayload.notification_type,
|
|
60
|
+
};
|
|
61
|
+
updateSession(event);
|
|
62
|
+
}
|