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
@@ -0,0 +1,7 @@
1
+ import type { Session } from '../types/index.js';
2
+ export declare function useSessions(): {
3
+ sessions: Session[];
4
+ loading: boolean;
5
+ error: Error | null;
6
+ };
7
+ //# sourceMappingURL=useSessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSessions.d.ts","sourceRoot":"","sources":["../../src/hooks/useSessions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAIjD,wBAAgB,WAAW,IAAI;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB,CAyCA"}
@@ -0,0 +1,41 @@
1
+ import chokidar from 'chokidar';
2
+ import { useEffect, useState } from 'react';
3
+ import { getSessions, getStorePath } from '../store/file-store.js';
4
+ const REFRESH_INTERVAL_MS = 60_000; // タイムアウト検出のための定期リフレッシュ(chokidarが主で、これはバックアップ)
5
+ export function useSessions() {
6
+ const [sessions, setSessions] = useState([]);
7
+ const [loading, setLoading] = useState(true);
8
+ const [error, setError] = useState(null);
9
+ useEffect(() => {
10
+ const loadSessions = () => {
11
+ try {
12
+ const data = getSessions();
13
+ setSessions(data);
14
+ setError(null);
15
+ }
16
+ catch (e) {
17
+ setError(e instanceof Error ? e : new Error('Failed to load sessions'));
18
+ }
19
+ finally {
20
+ setLoading(false);
21
+ }
22
+ };
23
+ // Initial load
24
+ loadSessions();
25
+ // Watch file changes
26
+ const storePath = getStorePath();
27
+ const watcher = chokidar.watch(storePath, {
28
+ persistent: true,
29
+ ignoreInitial: true,
30
+ });
31
+ watcher.on('change', loadSessions);
32
+ watcher.on('add', loadSessions);
33
+ // Periodic refresh (for timeout detection)
34
+ const interval = setInterval(loadSessions, REFRESH_INTERVAL_MS);
35
+ return () => {
36
+ watcher.close();
37
+ clearInterval(interval);
38
+ };
39
+ }, []);
40
+ return { sessions, loading, error };
41
+ }
@@ -0,0 +1,5 @@
1
+ export { clearSessions, getSession, getSessions, getStorePath, } from './store/file-store.js';
2
+ export type { HookEvent, HookEventName, Session, SessionStatus, StoreData, } from './types/index.js';
3
+ export { focusSession, getSupportedTerminals, isMacOS } from './utils/focus.js';
4
+ export { getStatusDisplay } from './utils/status.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,SAAS,EACT,aAAa,EACb,OAAO,EACP,aAAa,EACb,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Types
2
+ // Store functions
3
+ export { clearSessions, getSession, getSessions, getStorePath, } from './store/file-store.js';
4
+ export { focusSession, getSupportedTerminals, isMacOS } from './utils/focus.js';
5
+ // Utilities
6
+ export { getStatusDisplay } from './utils/status.js';
@@ -0,0 +1,39 @@
1
+ /** @internal */
2
+ export interface HookConfig {
3
+ type: 'command';
4
+ command: string;
5
+ }
6
+ /** @internal */
7
+ export interface HookEntry {
8
+ matcher?: string;
9
+ hooks: HookConfig[];
10
+ }
11
+ /** @internal */
12
+ export interface Settings {
13
+ hooks?: Record<string, HookEntry[]>;
14
+ [key: string]: unknown;
15
+ }
16
+ /**
17
+ * Check if the ccm hook is already configured for the given event
18
+ * @internal
19
+ */
20
+ export declare function hasCcmHookForEvent(entries: HookEntry[] | undefined, eventName: string): boolean;
21
+ /**
22
+ * Create a hook entry for the given event
23
+ * @internal
24
+ */
25
+ export declare function createHookEntry(eventName: string, baseCommand: string): HookEntry;
26
+ /**
27
+ * Determine which hooks need to be added or skipped
28
+ * @internal
29
+ */
30
+ export declare function categorizeHooks(settings: Settings): {
31
+ toAdd: string[];
32
+ toSkip: string[];
33
+ };
34
+ /**
35
+ * Check if hooks are already configured
36
+ */
37
+ export declare function isHooksConfigured(): boolean;
38
+ export declare function setupHooks(): Promise<void>;
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"AAUA,gBAAgB;AAChB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gBAAgB;AAChB,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,gBAAgB;AAChB,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACpC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAUD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAG/F;AAaD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,CAcjF;AAkBD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAazF;AA+CD;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAwB3C;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAyChD"}
@@ -0,0 +1,183 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { HOOK_EVENTS, PACKAGE_NAME } from '../constants.js';
6
+ import { askConfirmation } from '../utils/prompt.js';
7
+ const CLAUDE_DIR = join(homedir(), '.claude');
8
+ const SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
9
+ /**
10
+ * Check if a command string is a ccm hook command for the given event
11
+ * @internal
12
+ */
13
+ function isCcmHookCommand(command, eventName) {
14
+ return command === `ccm hook ${eventName}` || command === `npx ${PACKAGE_NAME} hook ${eventName}`;
15
+ }
16
+ /**
17
+ * Check if the ccm hook is already configured for the given event
18
+ * @internal
19
+ */
20
+ export function hasCcmHookForEvent(entries, eventName) {
21
+ if (!entries)
22
+ return false;
23
+ return entries.some((entry) => entry.hooks.some((h) => isCcmHookCommand(h.command, eventName)));
24
+ }
25
+ /**
26
+ * Check if ccm command is in PATH and return the appropriate command
27
+ */
28
+ function getCcmCommand() {
29
+ const result = spawnSync('which', ['ccm'], { encoding: 'utf-8' });
30
+ if (result.status === 0) {
31
+ return 'ccm';
32
+ }
33
+ return `npx ${PACKAGE_NAME}`;
34
+ }
35
+ /**
36
+ * Create a hook entry for the given event
37
+ * @internal
38
+ */
39
+ export function createHookEntry(eventName, baseCommand) {
40
+ const entry = {
41
+ hooks: [
42
+ {
43
+ type: 'command',
44
+ command: `${baseCommand} hook ${eventName}`,
45
+ },
46
+ ],
47
+ };
48
+ // Events other than UserPromptSubmit require a matcher
49
+ if (eventName !== 'UserPromptSubmit') {
50
+ entry.matcher = '';
51
+ }
52
+ return entry;
53
+ }
54
+ /**
55
+ * Load existing settings.json or return empty settings
56
+ */
57
+ function loadSettings() {
58
+ if (!existsSync(SETTINGS_FILE)) {
59
+ return {};
60
+ }
61
+ try {
62
+ const content = readFileSync(SETTINGS_FILE, 'utf-8');
63
+ return JSON.parse(content);
64
+ }
65
+ catch {
66
+ console.error('Warning: Failed to parse existing settings.json, creating new one');
67
+ return {};
68
+ }
69
+ }
70
+ /**
71
+ * Determine which hooks need to be added or skipped
72
+ * @internal
73
+ */
74
+ export function categorizeHooks(settings) {
75
+ const toAdd = [];
76
+ const toSkip = [];
77
+ for (const eventName of HOOK_EVENTS) {
78
+ if (hasCcmHookForEvent(settings.hooks?.[eventName], eventName)) {
79
+ toSkip.push(eventName);
80
+ }
81
+ else {
82
+ toAdd.push(eventName);
83
+ }
84
+ }
85
+ return { toAdd, toSkip };
86
+ }
87
+ /**
88
+ * Display setup preview to the user
89
+ */
90
+ function showSetupPreview(hooksToAdd, hooksToSkip, settingsExist) {
91
+ console.log(`Target file: ${SETTINGS_FILE}`);
92
+ console.log(settingsExist ? '(file exists, will be modified)' : '(file will be created)');
93
+ console.log('');
94
+ console.log('The following hooks will be added:');
95
+ for (const eventName of hooksToAdd) {
96
+ console.log(` [add] ${eventName}`);
97
+ }
98
+ if (hooksToSkip.length > 0) {
99
+ console.log('');
100
+ console.log('Already configured (will be skipped):');
101
+ for (const eventName of hooksToSkip) {
102
+ console.log(` [skip] ${eventName}`);
103
+ }
104
+ }
105
+ console.log('');
106
+ }
107
+ /**
108
+ * Apply hooks to settings and save to file
109
+ */
110
+ function applyHooks(settings, hooksToAdd, baseCommand) {
111
+ if (!settings.hooks) {
112
+ settings.hooks = {};
113
+ }
114
+ for (const eventName of hooksToAdd) {
115
+ const existing = settings.hooks[eventName];
116
+ if (!existing) {
117
+ settings.hooks[eventName] = [createHookEntry(eventName, baseCommand)];
118
+ }
119
+ else {
120
+ existing.push(createHookEntry(eventName, baseCommand));
121
+ }
122
+ }
123
+ writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
124
+ }
125
+ /**
126
+ * Check if hooks are already configured
127
+ */
128
+ export function isHooksConfigured() {
129
+ if (!existsSync(SETTINGS_FILE)) {
130
+ return false;
131
+ }
132
+ try {
133
+ const content = readFileSync(SETTINGS_FILE, 'utf-8');
134
+ const settings = JSON.parse(content);
135
+ if (!settings.hooks) {
136
+ return false;
137
+ }
138
+ // Check if all hook events are configured
139
+ for (const eventName of HOOK_EVENTS) {
140
+ if (!hasCcmHookForEvent(settings.hooks[eventName], eventName)) {
141
+ return false;
142
+ }
143
+ }
144
+ return true;
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ }
150
+ export async function setupHooks() {
151
+ console.log('Claude Code Monitor Setup');
152
+ console.log('=========================');
153
+ console.log('');
154
+ const baseCommand = getCcmCommand();
155
+ console.log(`Using command: ${baseCommand}`);
156
+ console.log('');
157
+ // Ensure .claude directory exists
158
+ if (!existsSync(CLAUDE_DIR)) {
159
+ mkdirSync(CLAUDE_DIR, { recursive: true });
160
+ }
161
+ const settingsExist = existsSync(SETTINGS_FILE);
162
+ const settings = loadSettings();
163
+ const { toAdd: hooksToAdd, toSkip: hooksToSkip } = categorizeHooks(settings);
164
+ // No changes needed
165
+ if (hooksToAdd.length === 0) {
166
+ console.log('All hooks already configured. No changes needed.');
167
+ console.log('');
168
+ console.log(`Start monitoring with: ${baseCommand} watch`);
169
+ return;
170
+ }
171
+ showSetupPreview(hooksToAdd, hooksToSkip, settingsExist);
172
+ const confirmed = await askConfirmation('Do you want to apply these changes?');
173
+ if (!confirmed) {
174
+ console.log('');
175
+ console.log('Setup cancelled. No changes were made.');
176
+ return;
177
+ }
178
+ applyHooks(settings, hooksToAdd, baseCommand);
179
+ console.log('');
180
+ console.log(`Setup complete! Added ${hooksToAdd.length} hook(s) to ${SETTINGS_FILE}`);
181
+ console.log('');
182
+ console.log(`Start monitoring with: ${baseCommand} watch`);
183
+ }
@@ -0,0 +1,17 @@
1
+ import type { HookEvent, Session, SessionStatus, StoreData } from '../types/index.js';
2
+ export { isTtyAlive } from '../utils/tty-cache.js';
3
+ export declare function readStore(): StoreData;
4
+ export declare function writeStore(data: StoreData): void;
5
+ /** @internal */
6
+ export declare function getSessionKey(sessionId: string, tty?: string): string;
7
+ /** @internal */
8
+ export declare function removeOldSessionsOnSameTty(sessions: Record<string, Session>, newSessionId: string, tty: string): void;
9
+ /** @internal */
10
+ export declare function determineStatus(event: HookEvent, currentStatus?: SessionStatus): SessionStatus;
11
+ export declare function updateSession(event: HookEvent): Session;
12
+ export declare function getSessions(): Session[];
13
+ export declare function getSession(sessionId: string, tty?: string): Session | undefined;
14
+ export declare function removeSession(sessionId: string, tty?: string): void;
15
+ export declare function clearSessions(): void;
16
+ export declare function getStorePath(): string;
17
+ //# sourceMappingURL=file-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../../src/store/file-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAItF,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAkBnD,wBAAgB,SAAS,IAAI,SAAS,CAWrC;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAIhD;AAED,gBAAgB;AAChB,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,gBAAgB;AAChB,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,GACV,IAAI,CAMN;AAED,gBAAgB;AAChB,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,aAAa,CAAC,EAAE,aAAa,GAAG,aAAa,CA8B9F;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAyBvD;AAED,wBAAgB,WAAW,IAAI,OAAO,EAAE,CAwBvC;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAI/E;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAKnE;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC"}
@@ -0,0 +1,134 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { SESSION_TIMEOUT_MS } from '../constants.js';
5
+ import { isTtyAlive } from '../utils/tty-cache.js';
6
+ // Re-export for backward compatibility
7
+ export { isTtyAlive } from '../utils/tty-cache.js';
8
+ const STORE_DIR = join(homedir(), '.claude-monitor');
9
+ const STORE_FILE = join(STORE_DIR, 'sessions.json');
10
+ function ensureStoreDir() {
11
+ if (!existsSync(STORE_DIR)) {
12
+ mkdirSync(STORE_DIR, { recursive: true, mode: 0o700 });
13
+ }
14
+ }
15
+ function getEmptyStoreData() {
16
+ return {
17
+ sessions: {},
18
+ updated_at: new Date().toISOString(),
19
+ };
20
+ }
21
+ export function readStore() {
22
+ ensureStoreDir();
23
+ if (!existsSync(STORE_FILE)) {
24
+ return getEmptyStoreData();
25
+ }
26
+ try {
27
+ const content = readFileSync(STORE_FILE, 'utf-8');
28
+ return JSON.parse(content);
29
+ }
30
+ catch {
31
+ return getEmptyStoreData();
32
+ }
33
+ }
34
+ export function writeStore(data) {
35
+ ensureStoreDir();
36
+ data.updated_at = new Date().toISOString();
37
+ writeFileSync(STORE_FILE, JSON.stringify(data), { encoding: 'utf-8', mode: 0o600 });
38
+ }
39
+ /** @internal */
40
+ export function getSessionKey(sessionId, tty) {
41
+ return tty ? `${sessionId}:${tty}` : sessionId;
42
+ }
43
+ /** @internal */
44
+ export function removeOldSessionsOnSameTty(sessions, newSessionId, tty) {
45
+ for (const [key, session] of Object.entries(sessions)) {
46
+ if (session.tty === tty && session.session_id !== newSessionId) {
47
+ delete sessions[key];
48
+ }
49
+ }
50
+ }
51
+ /** @internal */
52
+ export function determineStatus(event, currentStatus) {
53
+ // Explicit stop event
54
+ if (event.hook_event_name === 'Stop') {
55
+ return 'stopped';
56
+ }
57
+ // UserPromptSubmit starts a new operation, so resume even if stopped
58
+ if (event.hook_event_name === 'UserPromptSubmit') {
59
+ return 'running';
60
+ }
61
+ // Keep stopped state (don't resume except for UserPromptSubmit)
62
+ if (currentStatus === 'stopped') {
63
+ return 'stopped';
64
+ }
65
+ // Active operation event
66
+ if (event.hook_event_name === 'PreToolUse') {
67
+ return 'running';
68
+ }
69
+ // Waiting for permission prompt
70
+ const isPermissionPrompt = event.hook_event_name === 'Notification' && event.notification_type === 'permission_prompt';
71
+ if (isPermissionPrompt) {
72
+ return 'waiting_input';
73
+ }
74
+ // Default: running for other events (PostToolUse, etc.)
75
+ return 'running';
76
+ }
77
+ export function updateSession(event) {
78
+ const store = readStore();
79
+ const key = getSessionKey(event.session_id, event.tty);
80
+ const now = new Date().toISOString();
81
+ // Remove old session if a different session exists on the same TTY
82
+ // (e.g., when a new session starts after /clear)
83
+ if (event.tty) {
84
+ removeOldSessionsOnSameTty(store.sessions, event.session_id, event.tty);
85
+ }
86
+ const existing = store.sessions[key];
87
+ const session = {
88
+ session_id: event.session_id,
89
+ cwd: event.cwd,
90
+ tty: event.tty ?? existing?.tty,
91
+ status: determineStatus(event, existing?.status),
92
+ created_at: existing?.created_at ?? now,
93
+ updated_at: now,
94
+ };
95
+ store.sessions[key] = session;
96
+ writeStore(store);
97
+ return session;
98
+ }
99
+ export function getSessions() {
100
+ const store = readStore();
101
+ const now = Date.now();
102
+ let hasChanges = false;
103
+ for (const [key, session] of Object.entries(store.sessions)) {
104
+ const lastUpdateMs = new Date(session.updated_at).getTime();
105
+ const isSessionActive = now - lastUpdateMs <= SESSION_TIMEOUT_MS;
106
+ const isTtyStillAlive = isTtyAlive(session.tty);
107
+ const shouldRemoveSession = !isSessionActive || !isTtyStillAlive;
108
+ if (shouldRemoveSession) {
109
+ delete store.sessions[key];
110
+ hasChanges = true;
111
+ }
112
+ }
113
+ if (hasChanges) {
114
+ writeStore(store);
115
+ }
116
+ return Object.values(store.sessions).sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
117
+ }
118
+ export function getSession(sessionId, tty) {
119
+ const store = readStore();
120
+ const key = getSessionKey(sessionId, tty);
121
+ return store.sessions[key];
122
+ }
123
+ export function removeSession(sessionId, tty) {
124
+ const store = readStore();
125
+ const key = getSessionKey(sessionId, tty);
126
+ delete store.sessions[key];
127
+ writeStore(store);
128
+ }
129
+ export function clearSessions() {
130
+ writeStore(getEmptyStoreData());
131
+ }
132
+ export function getStorePath() {
133
+ return STORE_FILE;
134
+ }
@@ -0,0 +1,22 @@
1
+ export type HookEventName = 'PreToolUse' | 'PostToolUse' | 'Notification' | 'Stop' | 'UserPromptSubmit';
2
+ export interface HookEvent {
3
+ session_id: string;
4
+ cwd: string;
5
+ tty?: string;
6
+ hook_event_name: HookEventName;
7
+ notification_type?: string;
8
+ }
9
+ export type SessionStatus = 'running' | 'waiting_input' | 'stopped';
10
+ export interface Session {
11
+ session_id: string;
12
+ cwd: string;
13
+ tty?: string;
14
+ status: SessionStatus;
15
+ created_at: string;
16
+ updated_at: string;
17
+ }
18
+ export interface StoreData {
19
+ sessions: Record<string, Session>;
20
+ updated_at: string;
21
+ }
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,aAAa,GACb,cAAc,GACd,MAAM,GACN,kBAAkB,CAAC;AAGvB,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,aAAa,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAGD,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAAC;AAGpE,MAAM,WAAW,OAAO;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAGD,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;CACpB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Sanitize a string for safe use in AppleScript.
3
+ * Escapes backslashes, double quotes, and control characters to prevent injection.
4
+ * @internal
5
+ */
6
+ export declare function sanitizeForAppleScript(str: string): string;
7
+ /**
8
+ * Validate TTY path format.
9
+ * Only allows paths like /dev/ttys000, /dev/pts/0, etc.
10
+ * @internal
11
+ */
12
+ export declare function isValidTtyPath(tty: string): boolean;
13
+ export declare function isMacOS(): boolean;
14
+ export declare function focusSession(tty: string): boolean;
15
+ export declare function getSupportedTerminals(): string[];
16
+ //# sourceMappingURL=focus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/utils/focus.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEnD;AA4ED,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAYjD;AAED,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD"}
@@ -0,0 +1,109 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ /**
3
+ * Sanitize a string for safe use in AppleScript.
4
+ * Escapes backslashes, double quotes, and control characters to prevent injection.
5
+ * @internal
6
+ */
7
+ export function sanitizeForAppleScript(str) {
8
+ return str
9
+ .replace(/\\/g, '\\\\')
10
+ .replace(/"/g, '\\"')
11
+ .replace(/\n/g, '\\n')
12
+ .replace(/\r/g, '\\r')
13
+ .replace(/\t/g, '\\t');
14
+ }
15
+ /**
16
+ * Validate TTY path format.
17
+ * Only allows paths like /dev/ttys000, /dev/pts/0, etc.
18
+ * @internal
19
+ */
20
+ export function isValidTtyPath(tty) {
21
+ return /^\/dev\/(ttys?\d+|pts\/\d+)$/.test(tty);
22
+ }
23
+ function executeAppleScript(script) {
24
+ try {
25
+ const result = execFileSync('osascript', ['-e', script], {
26
+ encoding: 'utf-8',
27
+ stdio: ['pipe', 'pipe', 'pipe'],
28
+ }).trim();
29
+ return result === 'true';
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ function buildITerm2Script(tty) {
36
+ const safeTty = sanitizeForAppleScript(tty);
37
+ return `
38
+ tell application "iTerm2"
39
+ repeat with aWindow in windows
40
+ repeat with aTab in tabs of aWindow
41
+ repeat with aSession in sessions of aTab
42
+ if tty of aSession is "${safeTty}" then
43
+ select aSession
44
+ select aTab
45
+ tell aWindow to select
46
+ activate
47
+ return true
48
+ end if
49
+ end repeat
50
+ end repeat
51
+ end repeat
52
+ return false
53
+ end tell
54
+ `;
55
+ }
56
+ function buildTerminalAppScript(tty) {
57
+ const safeTty = sanitizeForAppleScript(tty);
58
+ return `
59
+ tell application "Terminal"
60
+ repeat with aWindow in windows
61
+ repeat with aTab in tabs of aWindow
62
+ if tty of aTab is "${safeTty}" then
63
+ set selected of aTab to true
64
+ set index of aWindow to 1
65
+ activate
66
+ return true
67
+ end if
68
+ end repeat
69
+ end repeat
70
+ return false
71
+ end tell
72
+ `;
73
+ }
74
+ function buildGhosttyScript() {
75
+ return `
76
+ tell application "Ghostty"
77
+ activate
78
+ end tell
79
+ return true
80
+ `;
81
+ }
82
+ function focusITerm2(tty) {
83
+ return executeAppleScript(buildITerm2Script(tty));
84
+ }
85
+ function focusTerminalApp(tty) {
86
+ return executeAppleScript(buildTerminalAppScript(tty));
87
+ }
88
+ function focusGhostty() {
89
+ return executeAppleScript(buildGhosttyScript());
90
+ }
91
+ export function isMacOS() {
92
+ return process.platform === 'darwin';
93
+ }
94
+ export function focusSession(tty) {
95
+ if (!isMacOS())
96
+ return false;
97
+ if (!isValidTtyPath(tty))
98
+ return false;
99
+ // Try each terminal in order (use the first one that succeeds)
100
+ const focusStrategies = [
101
+ () => focusITerm2(tty),
102
+ () => focusTerminalApp(tty),
103
+ () => focusGhostty(),
104
+ ];
105
+ return focusStrategies.some((tryFocus) => tryFocus());
106
+ }
107
+ export function getSupportedTerminals() {
108
+ return ['iTerm2', 'Terminal.app', 'Ghostty'];
109
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Ask for user confirmation with Y/n prompt
3
+ * @param message - The message to display
4
+ * @returns true if user confirms (Enter, 'y', or 'yes'), false otherwise
5
+ */
6
+ export declare function askConfirmation(message: string): Promise<boolean>;
7
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/utils/prompt.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAcvE"}
@@ -0,0 +1,20 @@
1
+ import * as readline from 'node:readline';
2
+ /**
3
+ * Ask for user confirmation with Y/n prompt
4
+ * @param message - The message to display
5
+ * @returns true if user confirms (Enter, 'y', or 'yes'), false otherwise
6
+ */
7
+ export async function askConfirmation(message) {
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ });
12
+ return new Promise((resolve) => {
13
+ rl.question(`${message} [Y/n]: `, (answer) => {
14
+ rl.close();
15
+ const normalized = answer.trim().toLowerCase();
16
+ // Accept: Enter only, 'y', or 'yes'
17
+ resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
18
+ });
19
+ });
20
+ }
@@ -0,0 +1,8 @@
1
+ import type { SessionStatus } from '../types/index.js';
2
+ export interface StatusDisplay {
3
+ symbol: string;
4
+ color: string;
5
+ label: string;
6
+ }
7
+ export declare function getStatusDisplay(status: SessionStatus): StatusDisplay;
8
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/utils/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CASrE"}
@@ -0,0 +1,10 @@
1
+ export function getStatusDisplay(status) {
2
+ switch (status) {
3
+ case 'running':
4
+ return { symbol: '●', color: 'green', label: 'Running' };
5
+ case 'waiting_input':
6
+ return { symbol: '◐', color: 'yellow', label: 'Waiting' };
7
+ case 'stopped':
8
+ return { symbol: '✓', color: 'cyan', label: 'Done' };
9
+ }
10
+ }