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.
Files changed (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +221 -0
  4. package/dist/bin/ccm.d.ts +3 -0
  5. package/dist/bin/ccm.d.ts.map +1 -0
  6. package/dist/bin/ccm.js +128 -0
  7. package/dist/components/Dashboard.d.ts +3 -0
  8. package/dist/components/Dashboard.d.ts.map +1 -0
  9. package/dist/components/Dashboard.js +64 -0
  10. package/dist/components/SessionCard.d.ts +10 -0
  11. package/dist/components/SessionCard.d.ts.map +1 -0
  12. package/dist/components/SessionCard.js +16 -0
  13. package/dist/components/Spinner.d.ts +7 -0
  14. package/dist/components/Spinner.d.ts.map +1 -0
  15. package/dist/components/Spinner.js +39 -0
  16. package/dist/constants.d.ts +13 -0
  17. package/dist/constants.d.ts.map +1 -0
  18. package/dist/constants.js +17 -0
  19. package/dist/hook/handler.d.ts +9 -0
  20. package/dist/hook/handler.d.ts.map +1 -0
  21. package/dist/hook/handler.js +62 -0
  22. package/dist/hooks/useSessions.d.ts +7 -0
  23. package/dist/hooks/useSessions.d.ts.map +1 -0
  24. package/dist/hooks/useSessions.js +41 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +6 -0
  28. package/dist/setup/index.d.ts +39 -0
  29. package/dist/setup/index.d.ts.map +1 -0
  30. package/dist/setup/index.js +183 -0
  31. package/dist/store/file-store.d.ts +17 -0
  32. package/dist/store/file-store.d.ts.map +1 -0
  33. package/dist/store/file-store.js +134 -0
  34. package/dist/types/index.d.ts +22 -0
  35. package/dist/types/index.d.ts.map +1 -0
  36. package/dist/types/index.js +1 -0
  37. package/dist/utils/focus.d.ts +16 -0
  38. package/dist/utils/focus.d.ts.map +1 -0
  39. package/dist/utils/focus.js +109 -0
  40. package/dist/utils/prompt.d.ts +7 -0
  41. package/dist/utils/prompt.d.ts.map +1 -0
  42. package/dist/utils/prompt.js +20 -0
  43. package/dist/utils/status.d.ts +8 -0
  44. package/dist/utils/status.d.ts.map +1 -0
  45. package/dist/utils/status.js +10 -0
  46. package/dist/utils/time.d.ts +5 -0
  47. package/dist/utils/time.d.ts.map +1 -0
  48. package/dist/utils/time.js +19 -0
  49. package/dist/utils/tty-cache.d.ts +12 -0
  50. package/dist/utils/tty-cache.d.ts.map +1 -0
  51. package/dist/utils/tty-cache.js +36 -0
  52. 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
+ [![npm version](https://img.shields.io/npm/v/claude-code-monitor.svg)](https://www.npmjs.com/package/claude-code-monitor)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=ccm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ccm.d.ts","sourceRoot":"","sources":["../../src/bin/ccm.tsx"],"names":[],"mappings":""}
@@ -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,3 @@
1
+ import type React from 'react';
2
+ export declare function Dashboard(): React.ReactElement;
3
+ //# sourceMappingURL=Dashboard.d.ts.map
@@ -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,7 @@
1
+ import type React from 'react';
2
+ interface SpinnerProps {
3
+ color?: string;
4
+ }
5
+ export declare function Spinner({ color }: SpinnerProps): React.ReactElement;
6
+ export {};
7
+ //# sourceMappingURL=Spinner.d.ts.map
@@ -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
+ }