ai-agent-session-center 2.0.2 → 2.0.3

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 (58) hide show
  1. package/README.md +484 -429
  2. package/docs/3D/ADAPTATION_GUIDE.md +592 -0
  3. package/docs/3D/index.html +754 -0
  4. package/docs/AGENT_TEAM_TASKS.md +716 -0
  5. package/docs/CYBERDROME_V2_SPEC.md +531 -0
  6. package/docs/ERROR_185_ANALYSIS.md +263 -0
  7. package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
  8. package/docs/SESSION_DETAIL_FEATURES.md +98 -0
  9. package/docs/_3d_multimedia_features.md +1080 -0
  10. package/docs/_frontend_features.md +1057 -0
  11. package/docs/_server_features.md +1077 -0
  12. package/docs/session-duplication-fixes.md +271 -0
  13. package/docs/session-terminal-linkage.md +412 -0
  14. package/package.json +63 -5
  15. package/public/apple-touch-icon.svg +21 -0
  16. package/public/css/dashboard.css +0 -161
  17. package/public/css/detail-panel.css +25 -0
  18. package/public/css/layout.css +18 -1
  19. package/public/css/modals.css +0 -26
  20. package/public/css/settings.css +0 -150
  21. package/public/css/terminal.css +34 -0
  22. package/public/favicon.svg +18 -0
  23. package/public/index.html +6 -26
  24. package/public/js/alarmManager.js +0 -21
  25. package/public/js/app.js +21 -7
  26. package/public/js/detailPanel.js +63 -64
  27. package/public/js/historyPanel.js +61 -55
  28. package/public/js/quickActions.js +132 -48
  29. package/public/js/sessionCard.js +5 -20
  30. package/public/js/sessionControls.js +8 -0
  31. package/public/js/settingsManager.js +0 -142
  32. package/server/apiRouter.js +60 -15
  33. package/server/apiRouter.ts +774 -0
  34. package/server/approvalDetector.ts +94 -0
  35. package/server/authManager.ts +144 -0
  36. package/server/autoIdleManager.ts +110 -0
  37. package/server/config.ts +121 -0
  38. package/server/constants.ts +150 -0
  39. package/server/db.ts +475 -0
  40. package/server/hookInstaller.d.ts +3 -0
  41. package/server/hookProcessor.ts +108 -0
  42. package/server/hookRouter.ts +18 -0
  43. package/server/hookStats.ts +116 -0
  44. package/server/index.js +15 -1
  45. package/server/index.ts +230 -0
  46. package/server/logger.ts +75 -0
  47. package/server/mqReader.ts +349 -0
  48. package/server/portManager.ts +55 -0
  49. package/server/processMonitor.ts +239 -0
  50. package/server/serverConfig.ts +29 -0
  51. package/server/sessionMatcher.js +17 -6
  52. package/server/sessionMatcher.ts +403 -0
  53. package/server/sessionStore.js +109 -3
  54. package/server/sessionStore.ts +1145 -0
  55. package/server/sshManager.js +167 -24
  56. package/server/sshManager.ts +671 -0
  57. package/server/teamManager.ts +289 -0
  58. package/server/wsManager.ts +200 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @module approvalDetector
3
+ * Detects when a tool call is pending user approval by starting category-based timers.
4
+ * If PostToolUse does not arrive within the timeout, the session transitions to approval/input status.
5
+ * PermissionRequest events provide a direct signal that bypasses the timeout heuristic.
6
+ */
7
+ import { execSync } from 'child_process';
8
+ import { getToolTimeout, getToolCategory, getWaitingStatus, getWaitingLabel } from './config.js';
9
+ import { SESSION_STATUS, ANIMATION_STATE } from './constants.js';
10
+ import log from './logger.js';
11
+ import type { Session } from '../src/types/session.js';
12
+
13
+ /**
14
+ * Validate PID as a positive integer.
15
+ */
16
+ function validatePid(pid: unknown): number | null {
17
+ const n = parseInt(String(pid), 10);
18
+ return Number.isFinite(n) && n > 0 ? n : null;
19
+ }
20
+
21
+ /** session_id -> timeout for tool approval detection */
22
+ const pendingToolTimers = new Map<string, ReturnType<typeof setTimeout>>();
23
+
24
+ /**
25
+ * Check if a PID has any child processes (i.e. a command is running).
26
+ */
27
+ export function hasChildProcesses(pid: number): boolean {
28
+ const validPid = validatePid(pid);
29
+ if (!validPid) return false;
30
+ try {
31
+ const out = execSync(`pgrep -P ${validPid} 2>/dev/null`, { encoding: 'utf-8', timeout: 2000 });
32
+ return out.trim().length > 0;
33
+ } catch (e: unknown) {
34
+ log.debug('session', `hasChildProcesses check failed for pid=${validPid}: ${(e as Error).message}`);
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Start an approval detection timer for a tool invocation.
41
+ * If PostToolUse doesn't arrive within the timeout, transitions session to approval/input.
42
+ */
43
+ export function startApprovalTimer(
44
+ sessionId: string,
45
+ session: Session,
46
+ toolName: string,
47
+ toolInputSummary: string,
48
+ broadcastFn: (session: Session) => Promise<void>,
49
+ ): void {
50
+ clearTimeout(pendingToolTimers.get(sessionId));
51
+
52
+ const approvalTimeout = getToolTimeout(toolName);
53
+ if (approvalTimeout > 0) {
54
+ session.pendingTool = toolName;
55
+ session.pendingToolDetail = toolInputSummary;
56
+ const timer = setTimeout(async () => {
57
+ pendingToolTimers.delete(sessionId);
58
+ if (session.status === SESSION_STATUS.WORKING && session.pendingTool) {
59
+ const category = getToolCategory(session.pendingTool);
60
+ if (category === 'slow' && session.cachedPid && hasChildProcesses(session.cachedPid)) {
61
+ return; // Command is running, not waiting for approval
62
+ }
63
+
64
+ const waitingStatus = getWaitingStatus(session.pendingTool) || SESSION_STATUS.APPROVAL;
65
+ (session as Session).status = waitingStatus as Session['status'];
66
+ session.animationState = ANIMATION_STATE.WAITING;
67
+ session.waitingDetail = getWaitingLabel(session.pendingTool, session.pendingToolDetail || '');
68
+ try {
69
+ await broadcastFn(session);
70
+ } catch (e: unknown) {
71
+ log.warn('session', `Approval broadcast failed: ${(e as Error).message}`);
72
+ }
73
+ }
74
+ }, approvalTimeout);
75
+ pendingToolTimers.set(sessionId, timer);
76
+ } else {
77
+ session.pendingTool = null;
78
+ session.pendingToolDetail = null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Clear a pending approval timer for a session.
84
+ * Also resets the pending tool and waiting detail on the session.
85
+ */
86
+ export function clearApprovalTimer(sessionId: string, session: Session | null): void {
87
+ clearTimeout(pendingToolTimers.get(sessionId));
88
+ pendingToolTimers.delete(sessionId);
89
+ if (session) {
90
+ session.pendingTool = null;
91
+ session.pendingToolDetail = null;
92
+ session.waitingDetail = null;
93
+ }
94
+ }
@@ -0,0 +1,144 @@
1
+ // authManager.ts — Password authentication with scrypt hashing and token sessions
2
+ import { scryptSync, randomBytes, timingSafeEqual } from 'crypto';
3
+ import { config } from './serverConfig.js';
4
+ import log from './logger.js';
5
+ import type { IncomingMessage } from 'http';
6
+ import type { Request, Response, NextFunction } from 'express';
7
+
8
+ const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
9
+ const SCRYPT_KEYLEN = 64;
10
+
11
+ // In-memory token store: Map<token, { createdAt: number }>
12
+ const tokens = new Map<string, { createdAt: number }>();
13
+
14
+ /**
15
+ * Hash a plaintext password with a random salt.
16
+ * @returns "salt:hash" (both hex-encoded)
17
+ */
18
+ export function hashPassword(password: string): string {
19
+ const salt = randomBytes(16).toString('hex');
20
+ const hash = scryptSync(password, salt, SCRYPT_KEYLEN).toString('hex');
21
+ return `${salt}:${hash}`;
22
+ }
23
+
24
+ /**
25
+ * Verify a plaintext password against a stored "salt:hash" string.
26
+ * Uses timing-safe comparison to prevent timing attacks.
27
+ */
28
+ export function verifyPassword(password: string, stored: string): boolean {
29
+ if (!stored || !stored.includes(':')) return false;
30
+ const [salt, storedHash] = stored.split(':');
31
+ const derived = scryptSync(password, salt, SCRYPT_KEYLEN).toString('hex');
32
+ if (derived.length !== storedHash.length) return false;
33
+ return timingSafeEqual(Buffer.from(derived, 'hex'), Buffer.from(storedHash, 'hex'));
34
+ }
35
+
36
+ /**
37
+ * Create a new auth token with 24h TTL.
38
+ */
39
+ export function createToken(): string {
40
+ const token = randomBytes(32).toString('hex');
41
+ tokens.set(token, { createdAt: Date.now() });
42
+ return token;
43
+ }
44
+
45
+ /**
46
+ * Validate a token exists and has not expired.
47
+ * Expired tokens are removed on check.
48
+ */
49
+ export function validateToken(token: string | null): boolean {
50
+ if (!token) return false;
51
+ const entry = tokens.get(token);
52
+ if (!entry) return false;
53
+ if (Date.now() - entry.createdAt > TOKEN_TTL_MS) {
54
+ tokens.delete(token);
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+
60
+ /**
61
+ * Remove a token (logout).
62
+ */
63
+ export function removeToken(token: string): void {
64
+ if (token) tokens.delete(token);
65
+ }
66
+
67
+ /**
68
+ * Check if password authentication is enabled.
69
+ */
70
+ export function isPasswordEnabled(): boolean {
71
+ return Boolean(config.passwordHash);
72
+ }
73
+
74
+ /**
75
+ * Parse the auth_token cookie from a raw Cookie header string.
76
+ */
77
+ export function parseCookieToken(cookieHeader: string | undefined): string | null {
78
+ if (!cookieHeader) return null;
79
+ const match = cookieHeader.match(/(?:^|;\s*)auth_token=([^;]+)/);
80
+ return match ? match[1] : null;
81
+ }
82
+
83
+ /**
84
+ * Extract token from request: cookie, Authorization header, or query string.
85
+ */
86
+ export function extractToken(req: IncomingMessage): string | null {
87
+ // 1. Cookie
88
+ const cookieToken = parseCookieToken(req.headers.cookie);
89
+ if (cookieToken) return cookieToken;
90
+ // 2. Authorization: Bearer <token>
91
+ const authHeader = req.headers.authorization;
92
+ if (authHeader && authHeader.startsWith('Bearer ')) return authHeader.slice(7);
93
+ // 3. Query string (?token=xxx) — used by WebSocket
94
+ if (req.url && req.url.includes('token=')) {
95
+ try {
96
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
97
+ return url.searchParams.get('token');
98
+ } catch { /* ignore parse errors */ }
99
+ }
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Express middleware: protect routes that require authentication.
105
+ * Checks cookie, Authorization header, and query string.
106
+ * Skips auth check if password is not enabled.
107
+ */
108
+ export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
109
+ if (!isPasswordEnabled()) {
110
+ next();
111
+ return;
112
+ }
113
+ const token = extractToken(req);
114
+ if (validateToken(token)) {
115
+ next();
116
+ return;
117
+ }
118
+ log.debug('auth', `Unauthorized request: ${req.method} ${req.originalUrl}`);
119
+ res.status(401).json({ error: 'Unauthorized' });
120
+ }
121
+
122
+ /**
123
+ * Periodic cleanup of expired tokens (runs every hour).
124
+ */
125
+ let cleanupTimer: ReturnType<typeof setInterval> | null = null;
126
+
127
+ export function startTokenCleanup(): void {
128
+ if (cleanupTimer) return;
129
+ cleanupTimer = setInterval(() => {
130
+ const now = Date.now();
131
+ for (const [token, entry] of tokens) {
132
+ if (now - entry.createdAt > TOKEN_TTL_MS) {
133
+ tokens.delete(token);
134
+ }
135
+ }
136
+ }, 60 * 60 * 1000);
137
+ }
138
+
139
+ export function stopTokenCleanup(): void {
140
+ if (cleanupTimer) {
141
+ clearInterval(cleanupTimer);
142
+ cleanupTimer = null;
143
+ }
144
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @module autoIdleManager
3
+ * Transitions sessions to idle/waiting after configurable inactivity timeouts.
4
+ * Prevents sessions from being stuck in transient states (prompting, working, approval)
5
+ * when hooks are missed or the user abandons the session. Also cleans up stale pendingResume entries.
6
+ */
7
+ import { AUTO_IDLE_TIMEOUTS } from './config.js';
8
+ import { SESSION_STATUS, ANIMATION_STATE, WS_TYPES } from './constants.js';
9
+ import log from './logger.js';
10
+ import type { Session, PendingResume } from '../src/types/session.js';
11
+ import type { ServerMessage } from '../src/types/websocket.js';
12
+
13
+ let idleInterval: ReturnType<typeof setInterval> | null = null;
14
+ let pendingResumeCleanupInterval: ReturnType<typeof setInterval> | null = null;
15
+
16
+ /**
17
+ * Start the auto-idle check interval.
18
+ * Transitions sessions to idle/waiting if no activity for configured durations.
19
+ */
20
+ export function startAutoIdle(sessions: Map<string, Session>): void {
21
+ if (idleInterval) return;
22
+
23
+ idleInterval = setInterval(() => {
24
+ const now = Date.now();
25
+ for (const [_id, session] of sessions) {
26
+ if (session.status === SESSION_STATUS.ENDED || session.status === SESSION_STATUS.IDLE) continue;
27
+ const elapsed = now - session.lastActivityAt;
28
+
29
+ if (session.status === SESSION_STATUS.APPROVAL && elapsed > AUTO_IDLE_TIMEOUTS.approval) {
30
+ session.status = SESSION_STATUS.IDLE;
31
+ session.animationState = ANIMATION_STATE.IDLE;
32
+ session.emote = null;
33
+ session.pendingTool = null;
34
+ session.pendingToolDetail = null;
35
+ session.waitingDetail = null;
36
+ } else if (session.status === SESSION_STATUS.INPUT && elapsed > AUTO_IDLE_TIMEOUTS.input) {
37
+ session.status = SESSION_STATUS.IDLE;
38
+ session.animationState = ANIMATION_STATE.IDLE;
39
+ session.emote = null;
40
+ session.pendingTool = null;
41
+ session.pendingToolDetail = null;
42
+ session.waitingDetail = null;
43
+ } else if (session.status === SESSION_STATUS.PROMPTING && elapsed > AUTO_IDLE_TIMEOUTS.prompting) {
44
+ session.status = SESSION_STATUS.WAITING;
45
+ session.animationState = ANIMATION_STATE.WAITING;
46
+ session.emote = null;
47
+ } else if (session.status === SESSION_STATUS.WAITING && elapsed > AUTO_IDLE_TIMEOUTS.waiting) {
48
+ session.status = SESSION_STATUS.IDLE;
49
+ session.animationState = ANIMATION_STATE.IDLE;
50
+ session.emote = null;
51
+ } else if (session.status !== SESSION_STATUS.WAITING && session.status !== SESSION_STATUS.PROMPTING
52
+ && session.status !== SESSION_STATUS.APPROVAL && session.status !== SESSION_STATUS.INPUT
53
+ && session.status !== SESSION_STATUS.CONNECTING
54
+ && elapsed > AUTO_IDLE_TIMEOUTS.working) {
55
+ session.status = SESSION_STATUS.IDLE;
56
+ session.animationState = ANIMATION_STATE.IDLE;
57
+ session.emote = null;
58
+ }
59
+ }
60
+ }, 10000);
61
+ }
62
+
63
+ /**
64
+ * Stop the auto-idle check interval.
65
+ */
66
+ export function stopAutoIdle(): void {
67
+ if (idleInterval) {
68
+ clearInterval(idleInterval);
69
+ idleInterval = null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Start cleaning up stale pendingResume entries.
75
+ */
76
+ export function startPendingResumeCleanup(
77
+ pendingResume: Map<string, PendingResume>,
78
+ sessions: Map<string, Session>,
79
+ broadcastFn: (data: ServerMessage) => Promise<void>,
80
+ ): void {
81
+ if (pendingResumeCleanupInterval) return;
82
+
83
+ pendingResumeCleanupInterval = setInterval(() => {
84
+ const now = Date.now();
85
+ for (const [termId, pending] of pendingResume) {
86
+ if (now - pending.timestamp > 120000) { // 2 minutes
87
+ pendingResume.delete(termId);
88
+ const session = sessions.get(pending.oldSessionId);
89
+ if (session && session.status === SESSION_STATUS.CONNECTING) {
90
+ session.status = SESSION_STATUS.ENDED;
91
+ session.animationState = ANIMATION_STATE.DEATH;
92
+ session.isHistorical = true;
93
+ session.terminalId = null;
94
+ log.info('session', `RESUME TIMEOUT: reverted session ${pending.oldSessionId?.slice(0, 8)} back to ended`);
95
+ broadcastFn({ type: WS_TYPES.SESSION_UPDATE, session: { ...session } }).catch(() => {});
96
+ }
97
+ }
98
+ }
99
+ }, 30000);
100
+ }
101
+
102
+ /**
103
+ * Stop the pending resume cleanup interval.
104
+ */
105
+ export function stopPendingResumeCleanup(): void {
106
+ if (pendingResumeCleanupInterval) {
107
+ clearInterval(pendingResumeCleanupInterval);
108
+ pendingResumeCleanupInterval = null;
109
+ }
110
+ }
@@ -0,0 +1,121 @@
1
+ // config.ts — Extracted session status & approval detection configuration
2
+ import { config as serverConfig } from './serverConfig.js';
3
+ import type { ToolCategory } from '../src/types/settings.js';
4
+
5
+ // ---- Tool Categories for Approval Detection ----
6
+ // When PreToolUse fires, we start a timer. If PostToolUse doesn't arrive
7
+ // within the timeout, the tool is likely pending user interaction.
8
+ // NOTE: PermissionRequest event (when available at medium+ density) provides
9
+ // a direct signal for approval-needed state, replacing the timeout heuristic.
10
+
11
+ export const TOOL_CATEGORIES: Record<ToolCategory, string[]> = {
12
+ // Tools that complete instantly when auto-approved (3s timeout)
13
+ fast: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'NotebookEdit'],
14
+ // Tools that ALWAYS require user interaction — not approval, but input (3s timeout)
15
+ userInput: ['AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode'],
16
+ // Tools that can be slow but not minutes-slow (15s timeout)
17
+ medium: ['WebFetch', 'WebSearch'],
18
+ // Tools that can run for minutes but still need approval detection (8s timeout).
19
+ // Tradeoff: auto-approved long-running commands (npm install, builds) will
20
+ // briefly show as "approval" after 8s until PostToolUse clears it.
21
+ slow: ['Bash', 'Task'],
22
+ };
23
+
24
+ export const TOOL_TIMEOUTS: Record<ToolCategory, number> = {
25
+ fast: 3000,
26
+ userInput: 3000,
27
+ medium: 15000,
28
+ slow: 8000,
29
+ };
30
+
31
+ // Status to set when each category's timeout fires
32
+ export const WAITING_REASONS: Record<ToolCategory, string> = {
33
+ fast: 'approval', // "NEEDS YOUR APPROVAL"
34
+ userInput: 'input', // "WAITING FOR YOUR ANSWER"
35
+ medium: 'approval', // "NEEDS YOUR APPROVAL"
36
+ slow: 'approval', // "NEEDS YOUR APPROVAL"
37
+ };
38
+
39
+ // Human-readable labels for waitingDetail per category
40
+ export const WAITING_LABELS: Record<string, (toolName: string, detail: string) => string> = {
41
+ approval: (toolName: string, detail: string) =>
42
+ detail ? `Approve ${toolName}: ${detail}` : `Approve ${toolName}`,
43
+ input: (toolName: string, _detail: string) => {
44
+ if (toolName === 'AskUserQuestion') return 'Waiting for your answer';
45
+ if (toolName === 'EnterPlanMode') return 'Review plan mode request';
46
+ if (toolName === 'ExitPlanMode') return 'Review plan';
47
+ return `Waiting for input on ${toolName}`;
48
+ },
49
+ };
50
+
51
+ // ---- Auto-Idle Timeouts ----
52
+ // Sessions transition to idle/waiting if no activity for these durations (ms)
53
+ export const AUTO_IDLE_TIMEOUTS: Record<string, number> = {
54
+ prompting: 30_000, // prompting -> waiting (user likely cancelled)
55
+ waiting: 120_000, // waiting -> idle (2 min)
56
+ working: 180_000, // working -> idle (3 min)
57
+ approval: 600_000, // approval -> idle (10 min safety net)
58
+ input: 600_000, // input -> idle (10 min safety net)
59
+ };
60
+
61
+ // ---- Process Liveness Check ----
62
+ // How often to check if session PIDs are still alive (ms).
63
+ // When a user closes VS Code, JetBrains, or terminal abruptly, the SessionEnd
64
+ // hook never fires. This monitor detects dead processes and auto-ends sessions.
65
+ export const PROCESS_CHECK_INTERVAL: number = serverConfig.processCheckInterval || 15_000;
66
+
67
+ // ---- Animation State Mappings ----
68
+ export const STATUS_ANIMATIONS: Record<string, { animationState: string; emote: string | null }> = {
69
+ idle: { animationState: 'Idle', emote: null },
70
+ prompting: { animationState: 'Walking', emote: 'Wave' },
71
+ working: { animationState: 'Running', emote: null },
72
+ approval: { animationState: 'Waiting', emote: null },
73
+ input: { animationState: 'Waiting', emote: null },
74
+ waiting: { animationState: 'Waiting', emote: 'ThumbsUp' },
75
+ ended: { animationState: 'Death', emote: null },
76
+ };
77
+
78
+ // ---- Precomputed Tool -> Category Lookup ----
79
+ // Built once at import time for O(1) lookups in hot path
80
+ const _toolToCategory = new Map<string, ToolCategory>();
81
+ for (const [category, tools] of Object.entries(TOOL_CATEGORIES)) {
82
+ for (const tool of tools) {
83
+ _toolToCategory.set(tool, category as ToolCategory);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Get the category for a tool name.
89
+ * @returns 'fast' | 'userInput' | 'medium' | 'slow' | null (no timeout)
90
+ */
91
+ export function getToolCategory(toolName: string): ToolCategory | null {
92
+ return _toolToCategory.get(toolName) || null;
93
+ }
94
+
95
+ /**
96
+ * Get the approval/input timeout for a tool, or 0 if no detection applies.
97
+ */
98
+ export function getToolTimeout(toolName: string): number {
99
+ const cat = getToolCategory(toolName);
100
+ return cat ? (TOOL_TIMEOUTS[cat] || 0) : 0;
101
+ }
102
+
103
+ /**
104
+ * Get the waiting status to set when a tool's timeout fires.
105
+ * @returns 'approval' | 'input' | null
106
+ */
107
+ export function getWaitingStatus(toolName: string): string | null {
108
+ const cat = getToolCategory(toolName);
109
+ return cat ? (WAITING_REASONS[cat] || null) : null;
110
+ }
111
+
112
+ /**
113
+ * Get the human-readable waitingDetail label for a tool.
114
+ */
115
+ export function getWaitingLabel(toolName: string, detail: string): string | null {
116
+ const cat = getToolCategory(toolName);
117
+ if (!cat) return null;
118
+ const status = WAITING_REASONS[cat];
119
+ const labelFn = WAITING_LABELS[status];
120
+ return labelFn ? labelFn(toolName, detail) : null;
121
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @module constants
3
+ * Centralized magic strings for event types, session statuses, animation states,
4
+ * and WebSocket message types. Shared by all server modules to eliminate string duplication.
5
+ */
6
+
7
+ // Hook event types (Claude Code lifecycle events)
8
+ export const EVENT_TYPES = {
9
+ SESSION_START: 'SessionStart',
10
+ SESSION_END: 'SessionEnd',
11
+ USER_PROMPT_SUBMIT: 'UserPromptSubmit',
12
+ PRE_TOOL_USE: 'PreToolUse',
13
+ POST_TOOL_USE: 'PostToolUse',
14
+ POST_TOOL_USE_FAILURE: 'PostToolUseFailure',
15
+ PERMISSION_REQUEST: 'PermissionRequest',
16
+ STOP: 'Stop',
17
+ SUBAGENT_START: 'SubagentStart',
18
+ SUBAGENT_STOP: 'SubagentStop',
19
+ TEAMMATE_IDLE: 'TeammateIdle',
20
+ TASK_COMPLETED: 'TaskCompleted',
21
+ PRE_COMPACT: 'PreCompact',
22
+ NOTIFICATION: 'Notification',
23
+ // Gemini events
24
+ BEFORE_AGENT: 'BeforeAgent',
25
+ BEFORE_TOOL: 'BeforeTool',
26
+ AFTER_TOOL: 'AfterTool',
27
+ AFTER_AGENT: 'AfterAgent',
28
+ // Codex events
29
+ AGENT_TURN_COMPLETE: 'agent-turn-complete',
30
+ } as const;
31
+
32
+ // All Claude hook events (used for hook density configuration)
33
+ export const ALL_CLAUDE_HOOK_EVENTS: string[] = [
34
+ EVENT_TYPES.SESSION_START,
35
+ EVENT_TYPES.USER_PROMPT_SUBMIT,
36
+ EVENT_TYPES.PRE_TOOL_USE,
37
+ EVENT_TYPES.POST_TOOL_USE,
38
+ EVENT_TYPES.POST_TOOL_USE_FAILURE,
39
+ EVENT_TYPES.PERMISSION_REQUEST,
40
+ EVENT_TYPES.STOP,
41
+ EVENT_TYPES.NOTIFICATION,
42
+ EVENT_TYPES.SUBAGENT_START,
43
+ EVENT_TYPES.SUBAGENT_STOP,
44
+ EVENT_TYPES.TEAMMATE_IDLE,
45
+ EVENT_TYPES.TASK_COMPLETED,
46
+ EVENT_TYPES.PRE_COMPACT,
47
+ EVENT_TYPES.SESSION_END,
48
+ ];
49
+
50
+ // Known event types set (all transports — Claude, Gemini, Codex)
51
+ export const KNOWN_EVENTS: Set<string> = new Set([
52
+ ...ALL_CLAUDE_HOOK_EVENTS,
53
+ EVENT_TYPES.BEFORE_AGENT,
54
+ EVENT_TYPES.BEFORE_TOOL,
55
+ EVENT_TYPES.AFTER_TOOL,
56
+ EVENT_TYPES.AFTER_AGENT,
57
+ EVENT_TYPES.AGENT_TURN_COMPLETE,
58
+ ]);
59
+
60
+ // Hook density presets — which events to register at each density level
61
+ export const DENSITY_EVENTS: Record<string, string[]> = {
62
+ high: ALL_CLAUDE_HOOK_EVENTS,
63
+ medium: [
64
+ EVENT_TYPES.SESSION_START,
65
+ EVENT_TYPES.USER_PROMPT_SUBMIT,
66
+ EVENT_TYPES.PRE_TOOL_USE,
67
+ EVENT_TYPES.POST_TOOL_USE,
68
+ EVENT_TYPES.POST_TOOL_USE_FAILURE,
69
+ EVENT_TYPES.PERMISSION_REQUEST,
70
+ EVENT_TYPES.STOP,
71
+ EVENT_TYPES.NOTIFICATION,
72
+ EVENT_TYPES.SUBAGENT_START,
73
+ EVENT_TYPES.SUBAGENT_STOP,
74
+ EVENT_TYPES.TASK_COMPLETED,
75
+ EVENT_TYPES.SESSION_END,
76
+ ],
77
+ low: [
78
+ EVENT_TYPES.SESSION_START,
79
+ EVENT_TYPES.USER_PROMPT_SUBMIT,
80
+ EVENT_TYPES.PERMISSION_REQUEST,
81
+ EVENT_TYPES.STOP,
82
+ EVENT_TYPES.SESSION_END,
83
+ ],
84
+ };
85
+
86
+ // Session statuses
87
+ export const SESSION_STATUS = {
88
+ IDLE: 'idle',
89
+ PROMPTING: 'prompting',
90
+ WORKING: 'working',
91
+ APPROVAL: 'approval',
92
+ INPUT: 'input',
93
+ WAITING: 'waiting',
94
+ ENDED: 'ended',
95
+ CONNECTING: 'connecting',
96
+ } as const;
97
+
98
+ // Animation states
99
+ export const ANIMATION_STATE = {
100
+ IDLE: 'Idle',
101
+ WALKING: 'Walking',
102
+ RUNNING: 'Running',
103
+ WAITING: 'Waiting',
104
+ DEATH: 'Death',
105
+ DANCE: 'Dance',
106
+ } as const;
107
+
108
+ // Emote names
109
+ export const EMOTE = {
110
+ WAVE: 'Wave',
111
+ THUMBS_UP: 'ThumbsUp',
112
+ JUMP: 'Jump',
113
+ YES: 'Yes',
114
+ } as const;
115
+
116
+ // WebSocket message types
117
+ export const WS_TYPES = {
118
+ SESSION_UPDATE: 'session_update',
119
+ SESSION_REMOVED: 'session_removed',
120
+ TEAM_UPDATE: 'team_update',
121
+ HOOK_STATS: 'hook_stats',
122
+ SNAPSHOT: 'snapshot',
123
+ TERMINAL_OUTPUT: 'terminal_output',
124
+ TERMINAL_READY: 'terminal_ready',
125
+ TERMINAL_CLOSED: 'terminal_closed',
126
+ TERMINAL_INPUT: 'terminal_input',
127
+ TERMINAL_RESIZE: 'terminal_resize',
128
+ TERMINAL_DISCONNECT: 'terminal_disconnect',
129
+ TERMINAL_SUBSCRIBE: 'terminal_subscribe',
130
+ UPDATE_QUEUE_COUNT: 'update_queue_count',
131
+ REPLAY: 'replay',
132
+ CLEAR_BROWSER_DB: 'clearBrowserDb',
133
+ } as const;
134
+
135
+ // Session sources
136
+ export const SESSION_SOURCE = {
137
+ SSH: 'ssh',
138
+ VSCODE: 'vscode',
139
+ JETBRAINS: 'jetbrains',
140
+ ITERM: 'iterm',
141
+ WARP: 'warp',
142
+ KITTY: 'kitty',
143
+ GHOSTTY: 'ghostty',
144
+ ALACRITTY: 'alacritty',
145
+ WEZTERM: 'wezterm',
146
+ HYPER: 'hyper',
147
+ TERMINAL: 'terminal',
148
+ TMUX: 'tmux',
149
+ UNKNOWN: 'unknown',
150
+ } as const;