ai-agent-session-center 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 (41) hide show
  1. package/README.md +618 -0
  2. package/bin/cli.js +20 -0
  3. package/hooks/dashboard-hook-codex.sh +67 -0
  4. package/hooks/dashboard-hook-gemini.sh +102 -0
  5. package/hooks/dashboard-hook.ps1 +147 -0
  6. package/hooks/dashboard-hook.sh +142 -0
  7. package/hooks/dashboard-hooks-backup.json +103 -0
  8. package/hooks/install-hooks.js +543 -0
  9. package/hooks/reset.js +357 -0
  10. package/hooks/setup-wizard.js +156 -0
  11. package/package.json +52 -0
  12. package/public/css/dashboard.css +10200 -0
  13. package/public/index.html +915 -0
  14. package/public/js/analyticsPanel.js +467 -0
  15. package/public/js/app.js +1148 -0
  16. package/public/js/browserDb.js +806 -0
  17. package/public/js/chartUtils.js +383 -0
  18. package/public/js/historyPanel.js +298 -0
  19. package/public/js/movementManager.js +155 -0
  20. package/public/js/navController.js +32 -0
  21. package/public/js/robotManager.js +526 -0
  22. package/public/js/sceneManager.js +7 -0
  23. package/public/js/sessionPanel.js +2477 -0
  24. package/public/js/settingsManager.js +924 -0
  25. package/public/js/soundManager.js +249 -0
  26. package/public/js/statsPanel.js +118 -0
  27. package/public/js/terminalManager.js +391 -0
  28. package/public/js/timelinePanel.js +278 -0
  29. package/public/js/wsClient.js +88 -0
  30. package/server/apiRouter.js +321 -0
  31. package/server/config.js +120 -0
  32. package/server/hookProcessor.js +55 -0
  33. package/server/hookRouter.js +18 -0
  34. package/server/hookStats.js +107 -0
  35. package/server/index.js +314 -0
  36. package/server/logger.js +67 -0
  37. package/server/mqReader.js +218 -0
  38. package/server/serverConfig.js +27 -0
  39. package/server/sessionStore.js +1049 -0
  40. package/server/sshManager.js +339 -0
  41. package/server/wsManager.js +83 -0
package/hooks/reset.js ADDED
@@ -0,0 +1,357 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, rmSync, copyFileSync, readdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { homedir } from 'os';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const PROJECT_ROOT = join(__dirname, '..');
8
+ const isWindows = process.platform === 'win32';
9
+
10
+ // ── ANSI colors ──
11
+ const RESET = '\x1b[0m';
12
+ const BOLD = '\x1b[1m';
13
+ const DIM = '\x1b[2m';
14
+ const GREEN = '\x1b[32m';
15
+ const YELLOW = '\x1b[33m';
16
+ const RED = '\x1b[31m';
17
+ const CYAN = '\x1b[36m';
18
+
19
+ const ok = (msg) => console.log(` ${GREEN}✓${RESET} ${msg}`);
20
+ const warn = (msg) => console.log(` ${YELLOW}⚠${RESET} ${msg}`);
21
+ const info = (msg) => console.log(` ${DIM}→${RESET} ${msg}`);
22
+ const step = (n, total, label) => console.log(`\n${CYAN}[${n}/${total}]${RESET} ${BOLD}${label}${RESET}`);
23
+
24
+ // ── Paths ──
25
+ const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
26
+ const GEMINI_SETTINGS_PATH = join(homedir(), '.gemini', 'settings.json');
27
+ const CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
28
+ const HOOKS_DIR = join(homedir(), '.claude', 'hooks');
29
+ const GEMINI_HOOKS_DIR = join(homedir(), '.gemini', 'hooks');
30
+ const CODEX_HOOKS_DIR = join(homedir(), '.codex', 'hooks');
31
+ const HOOK_PATTERN = 'dashboard-hook';
32
+ const HOOK_SOURCE = 'ai-agent-session-center'; // Must match the _source marker in hook groups
33
+ const DATA_DIR = join(PROJECT_ROOT, 'data');
34
+ const BACKUP_DIR = join(PROJECT_ROOT, 'data', 'backups');
35
+ const MQ_DIR = isWindows
36
+ ? join(process.env.TEMP || process.env.TMP || 'C:\\Temp', 'claude-session-center')
37
+ : '/tmp/claude-session-center';
38
+
39
+ const ALL_EVENTS = [
40
+ 'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PostToolUseFailure',
41
+ 'PermissionRequest', 'Stop', 'Notification', 'SubagentStart', 'SubagentStop',
42
+ 'TeammateIdle', 'TaskCompleted', 'PreCompact', 'SessionEnd'
43
+ ];
44
+
45
+ const GEMINI_ALL_EVENTS = [
46
+ 'SessionStart', 'BeforeAgent', 'BeforeTool', 'AfterTool',
47
+ 'AfterAgent', 'SessionEnd', 'Notification'
48
+ ];
49
+
50
+ const TOTAL_STEPS = 6;
51
+
52
+ // ── Banner ──
53
+ console.log(`\n${RED}╭──────────────────────────────────────────────╮${RESET}`);
54
+ console.log(`${RED}│${RESET} ${BOLD}AI Agent Session Center — Full Reset${RESET} ${RED}│${RESET}`);
55
+ console.log(`${RED}╰──────────────────────────────────────────────╯${RESET}`);
56
+
57
+ // ═══════════════════════════════════════════════
58
+ // STEP 1: Create backup
59
+ // ═══════════════════════════════════════════════
60
+ step(1, TOTAL_STEPS, 'Backing up current state...');
61
+
62
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
63
+ const backupPath = join(BACKUP_DIR, `reset-${timestamp}`);
64
+ mkdirSync(backupPath, { recursive: true });
65
+ ok(`Backup directory: ${DIM}${backupPath}${RESET}`);
66
+
67
+ let backedUp = 0;
68
+
69
+ // Backup server-config.json
70
+ const configPath = join(DATA_DIR, 'server-config.json');
71
+ if (existsSync(configPath)) {
72
+ copyFileSync(configPath, join(backupPath, 'server-config.json'));
73
+ ok('Backed up server-config.json');
74
+ backedUp++;
75
+ }
76
+
77
+ // Backup sessions.db
78
+ const dbPath = join(DATA_DIR, 'sessions.db');
79
+ if (existsSync(dbPath)) {
80
+ copyFileSync(dbPath, join(backupPath, 'sessions.db'));
81
+ ok('Backed up sessions.db');
82
+ backedUp++;
83
+ }
84
+
85
+ // Backup ~/.claude/settings.json
86
+ if (existsSync(SETTINGS_PATH)) {
87
+ copyFileSync(SETTINGS_PATH, join(backupPath, 'claude-settings.json'));
88
+ ok('Backed up ~/.claude/settings.json');
89
+ backedUp++;
90
+ }
91
+
92
+ // Backup deployed hook scripts (Claude)
93
+ for (const script of ['dashboard-hook.sh', 'dashboard-hook.ps1']) {
94
+ const deployed = join(HOOKS_DIR, script);
95
+ if (existsSync(deployed)) {
96
+ copyFileSync(deployed, join(backupPath, `claude-${script}`));
97
+ ok(`Backed up Claude ${script}`);
98
+ backedUp++;
99
+ }
100
+ }
101
+
102
+ // Backup ~/.gemini/settings.json
103
+ if (existsSync(GEMINI_SETTINGS_PATH)) {
104
+ copyFileSync(GEMINI_SETTINGS_PATH, join(backupPath, 'gemini-settings.json'));
105
+ ok('Backed up ~/.gemini/settings.json');
106
+ backedUp++;
107
+ }
108
+
109
+ // Backup Gemini hook script
110
+ const geminiHook = join(GEMINI_HOOKS_DIR, 'dashboard-hook.sh');
111
+ if (existsSync(geminiHook)) {
112
+ copyFileSync(geminiHook, join(backupPath, 'gemini-dashboard-hook.sh'));
113
+ ok('Backed up Gemini dashboard-hook.sh');
114
+ backedUp++;
115
+ }
116
+
117
+ // Backup ~/.codex/config.toml
118
+ if (existsSync(CODEX_CONFIG_PATH)) {
119
+ copyFileSync(CODEX_CONFIG_PATH, join(backupPath, 'codex-config.toml'));
120
+ ok('Backed up ~/.codex/config.toml');
121
+ backedUp++;
122
+ }
123
+
124
+ // Backup Codex hook script
125
+ const codexHook = join(CODEX_HOOKS_DIR, 'dashboard-hook.sh');
126
+ if (existsSync(codexHook)) {
127
+ copyFileSync(codexHook, join(backupPath, 'codex-dashboard-hook.sh'));
128
+ ok('Backed up Codex dashboard-hook.sh');
129
+ backedUp++;
130
+ }
131
+
132
+ info(`${backedUp} file(s) backed up`);
133
+
134
+ // ═══════════════════════════════════════════════
135
+ // STEP 2: Remove hooks from all CLI settings
136
+ // ═══════════════════════════════════════════════
137
+ step(2, TOTAL_STEPS, 'Removing dashboard hooks from settings...');
138
+
139
+ // Helper: check if a hook group belongs to this project
140
+ // Uses dual matching: _source marker (preferred) OR command pattern (legacy/fallback)
141
+ function isOurHookGroup(group) {
142
+ if (group._source === HOOK_SOURCE) return true;
143
+ return group.hooks?.some(h => h.command?.includes(HOOK_PATTERN));
144
+ }
145
+
146
+ // Helper: clean dashboard hooks from a JSON settings file
147
+ // SAFETY: Only removes hook groups that match our _source marker or command pattern.
148
+ // All other hooks (user's custom hooks, other tools) are preserved untouched.
149
+ function cleanJsonSettings(path, events, label) {
150
+ if (!existsSync(path)) {
151
+ info(`${label} settings not found — skipping`);
152
+ return;
153
+ }
154
+ try {
155
+ const settings = JSON.parse(readFileSync(path, 'utf8'));
156
+ let removed = 0;
157
+ let preserved = 0;
158
+
159
+ if (settings.hooks) {
160
+ // Only iterate events we know about — never touch unknown event keys
161
+ for (const event of events) {
162
+ if (!settings.hooks[event]) continue;
163
+ const before = settings.hooks[event].length;
164
+ const kept = [];
165
+ for (const group of settings.hooks[event]) {
166
+ if (isOurHookGroup(group)) {
167
+ ok(`[${label}] Removing hook for ${event}${group._source ? ' (source: ' + group._source + ')' : ''}`);
168
+ } else {
169
+ kept.push(group);
170
+ preserved++;
171
+ }
172
+ }
173
+ const diff = before - kept.length;
174
+ if (diff > 0) removed += diff;
175
+ if (kept.length === 0) {
176
+ delete settings.hooks[event];
177
+ } else {
178
+ settings.hooks[event] = kept;
179
+ }
180
+ }
181
+
182
+ if (Object.keys(settings.hooks).length === 0) {
183
+ delete settings.hooks;
184
+ }
185
+ }
186
+
187
+ writeFileSync(path, JSON.stringify(settings, null, 2) + '\n');
188
+
189
+ if (removed > 0) {
190
+ ok(`[${label}] ${removed} dashboard hook(s) removed`);
191
+ } else {
192
+ info(`[${label}] No dashboard hooks found`);
193
+ }
194
+
195
+ // Report all preserved hooks in detail so user can verify nothing was touched
196
+ const remainingEvents = Object.keys(settings.hooks || {});
197
+ if (remainingEvents.length > 0) {
198
+ info(`${YELLOW}Preserved${RESET} ${preserved} non-dashboard hook(s) across ${remainingEvents.length} event(s) in ${label}:`);
199
+ for (const event of remainingEvents) {
200
+ const groups = settings.hooks[event] || [];
201
+ for (const group of groups) {
202
+ const cmds = (group.hooks || []).map(h => h.command || h.type || '?').join(', ');
203
+ info(` ${DIM}${event}: ${cmds}${RESET}`);
204
+ }
205
+ }
206
+ }
207
+ } catch (e) {
208
+ warn(`Could not parse ${label} settings: ${e.message}`);
209
+ }
210
+ }
211
+
212
+ // Claude
213
+ cleanJsonSettings(SETTINGS_PATH, ALL_EVENTS, 'Claude');
214
+
215
+ // Gemini
216
+ cleanJsonSettings(GEMINI_SETTINGS_PATH, GEMINI_ALL_EVENTS, 'Gemini');
217
+
218
+ // Codex (TOML — remove only our comment + notify lines)
219
+ // SAFETY: Only removes lines matching our comment marker or the specific dashboard-hook notify.
220
+ // All other Codex config lines are preserved untouched.
221
+ if (existsSync(CODEX_CONFIG_PATH)) {
222
+ try {
223
+ const toml = readFileSync(CODEX_CONFIG_PATH, 'utf8');
224
+ const lines = toml.split('\n');
225
+ const kept = [];
226
+ let removed = 0;
227
+ for (const line of lines) {
228
+ const isOurComment = line.includes(`[${HOOK_SOURCE}]`);
229
+ const isOurNotify = line.includes(HOOK_PATTERN) && line.trimStart().startsWith('notify');
230
+ if (isOurComment || isOurNotify) {
231
+ ok(`[Codex] Removing line: ${DIM}${line.trim()}${RESET}`);
232
+ removed++;
233
+ } else {
234
+ kept.push(line);
235
+ }
236
+ }
237
+ if (removed > 0) {
238
+ writeFileSync(CODEX_CONFIG_PATH, kept.join('\n'));
239
+ ok(`[Codex] ${removed} line(s) removed from config.toml`);
240
+ // Show what's left
241
+ const remaining = kept.filter(l => l.trim()).length;
242
+ if (remaining > 0) {
243
+ info(`${YELLOW}Preserved${RESET} ${remaining} other line(s) in Codex config.toml`);
244
+ }
245
+ } else {
246
+ info('[Codex] No dashboard hooks found in config.toml');
247
+ }
248
+ } catch (e) {
249
+ warn(`Could not parse Codex config: ${e.message}`);
250
+ }
251
+ } else {
252
+ info('Codex config.toml not found');
253
+ }
254
+
255
+ // ═══════════════════════════════════════════════
256
+ // STEP 3: Remove deployed hook scripts
257
+ // ═══════════════════════════════════════════════
258
+ step(3, TOTAL_STEPS, 'Removing deployed hook scripts...');
259
+
260
+ // SAFETY: Verify file content belongs to our project before deleting.
261
+ // Only remove scripts that contain "AI Agent Session Center" or "claude-session-center" identifiers.
262
+ function safeRemoveHookScript(filePath, label) {
263
+ if (!existsSync(filePath)) {
264
+ info(`${label} not found ${DIM}(already clean)${RESET}`);
265
+ return;
266
+ }
267
+ try {
268
+ const content = readFileSync(filePath, 'utf8');
269
+ const isOurs = content.includes('AI Agent Session Center')
270
+ || content.includes('claude-session-center')
271
+ || content.includes(HOOK_SOURCE);
272
+ if (isOurs) {
273
+ unlinkSync(filePath);
274
+ ok(`Removed ${label}: ${DIM}${filePath}${RESET}`);
275
+ } else {
276
+ warn(`${YELLOW}SKIPPED${RESET} ${label}: ${filePath} — file does not contain our project marker, may belong to another tool`);
277
+ }
278
+ } catch (e) {
279
+ warn(`Could not verify ${label}: ${e.message}`);
280
+ }
281
+ }
282
+
283
+ // Claude hooks
284
+ for (const script of ['dashboard-hook.sh', 'dashboard-hook.ps1']) {
285
+ safeRemoveHookScript(join(HOOKS_DIR, script), `Claude ${script}`);
286
+ }
287
+
288
+ // Gemini hook
289
+ safeRemoveHookScript(join(GEMINI_HOOKS_DIR, 'dashboard-hook.sh'), 'Gemini dashboard-hook.sh');
290
+
291
+ // Codex hook
292
+ safeRemoveHookScript(join(CODEX_HOOKS_DIR, 'dashboard-hook.sh'), 'Codex dashboard-hook.sh');
293
+
294
+ // ═══════════════════════════════════════════════
295
+ // STEP 4: Clean local data
296
+ // ═══════════════════════════════════════════════
297
+ step(4, TOTAL_STEPS, 'Cleaning local data...');
298
+
299
+ // Remove server-config.json
300
+ if (existsSync(configPath)) {
301
+ unlinkSync(configPath);
302
+ ok('Removed server-config.json');
303
+ } else {
304
+ info('server-config.json not found');
305
+ }
306
+
307
+ // Remove sessions.db + WAL files
308
+ for (const dbFile of ['sessions.db', 'sessions.db-shm', 'sessions.db-wal']) {
309
+ const p = join(DATA_DIR, dbFile);
310
+ if (existsSync(p)) {
311
+ unlinkSync(p);
312
+ ok(`Removed ${dbFile}`);
313
+ }
314
+ }
315
+
316
+ // Remove MQ queue directory
317
+ if (existsSync(MQ_DIR)) {
318
+ rmSync(MQ_DIR, { recursive: true, force: true });
319
+ ok(`Removed MQ directory: ${MQ_DIR}`);
320
+ } else {
321
+ info('MQ directory not found');
322
+ }
323
+
324
+ // ═══════════════════════════════════════════════
325
+ // STEP 5: Clear browser IndexedDB (if server is running)
326
+ // ═══════════════════════════════════════════════
327
+ step(5, TOTAL_STEPS, 'Clearing browser IndexedDB...');
328
+
329
+ try {
330
+ const resp = await fetch('http://localhost:3333/api/reset', { method: 'POST', signal: AbortSignal.timeout(2000) });
331
+ if (resp.ok) {
332
+ ok('Sent clearBrowserDb signal to all connected browsers');
333
+ } else {
334
+ warn(`Server responded with ${resp.status}`);
335
+ }
336
+ } catch {
337
+ info('Server not running — browser DB will be cleared on next connect (empty snapshot)');
338
+ }
339
+
340
+ // ═══════════════════════════════════════════════
341
+ // STEP 6: Summary
342
+ // ═══════════════════════════════════════════════
343
+ step(6, TOTAL_STEPS, 'Summary');
344
+
345
+ // List backup contents
346
+ const backupFiles = readdirSync(backupPath);
347
+ info(`Backup location: ${BOLD}${backupPath}${RESET}`);
348
+ for (const f of backupFiles) {
349
+ info(` ${DIM}${f}${RESET}`);
350
+ }
351
+
352
+ console.log(`\n${GREEN}────────────────────────────────────────────────${RESET}`);
353
+ console.log(` ${GREEN}✓ Reset complete${RESET}`);
354
+ console.log(`${GREEN}────────────────────────────────────────────────${RESET}`);
355
+ console.log(`\n To set up again: ${BOLD}npm run setup${RESET} ${DIM}(interactive wizard)${RESET}`);
356
+ console.log(` To quick start: ${BOLD}npm start${RESET} ${DIM}(uses defaults)${RESET}`);
357
+ console.log(` To restore backup: ${DIM}cp ${backupPath}/* data/${RESET}\n`);
@@ -0,0 +1,156 @@
1
+ import { createInterface } from 'readline';
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { execSync } from 'child_process';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const PROJECT_ROOT = join(__dirname, '..');
9
+ const CONFIG_PATH = join(PROJECT_ROOT, 'data', 'server-config.json');
10
+
11
+ // ── ANSI colors ──
12
+ const RESET = '\x1b[0m';
13
+ const BOLD = '\x1b[1m';
14
+ const DIM = '\x1b[2m';
15
+ const GREEN = '\x1b[32m';
16
+ const YELLOW = '\x1b[33m';
17
+ const RED = '\x1b[31m';
18
+ const CYAN = '\x1b[36m';
19
+
20
+ const ok = (msg) => console.log(` ${GREEN}✓${RESET} ${msg}`);
21
+ const info = (msg) => console.log(` ${DIM}→${RESET} ${msg}`);
22
+
23
+ // ── Load existing config (if re-running setup) ──
24
+ let existing = {};
25
+ try {
26
+ existing = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
27
+ } catch { /* first run */ }
28
+
29
+ // ── readline helper ──
30
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
31
+
32
+ function ask(prompt) {
33
+ return new Promise(resolve => rl.question(prompt, resolve));
34
+ }
35
+
36
+ async function choose(stepNum, totalSteps, label, options, defaultIdx = 0) {
37
+ console.log(`\n${CYAN}[${stepNum}/${totalSteps}]${RESET} ${BOLD}${label}${RESET}`);
38
+ for (let i = 0; i < options.length; i++) {
39
+ const marker = i === defaultIdx ? ` ${GREEN}← default${RESET}` : '';
40
+ console.log(` ${DIM}[${i + 1}]${RESET} ${options[i].label}${marker}`);
41
+ }
42
+ const answer = await ask(` ${DIM}>${RESET} `);
43
+ const idx = answer.trim() === '' ? defaultIdx : parseInt(answer.trim(), 10) - 1;
44
+ if (idx < 0 || idx >= options.length || isNaN(idx)) {
45
+ console.log(` ${YELLOW}Invalid choice, using default${RESET}`);
46
+ return options[defaultIdx];
47
+ }
48
+ return options[idx];
49
+ }
50
+
51
+ async function askValue(stepNum, totalSteps, label, defaultVal) {
52
+ console.log(`\n${CYAN}[${stepNum}/${totalSteps}]${RESET} ${BOLD}${label}${RESET}`);
53
+ const answer = await ask(` ${DIM}(default: ${defaultVal}) >${RESET} `);
54
+ return answer.trim() || String(defaultVal);
55
+ }
56
+
57
+ // ── Main ──
58
+ const TOTAL = 5;
59
+
60
+ console.log(`\n${CYAN}╭──────────────────────────────────────────────╮${RESET}`);
61
+ console.log(`${CYAN}│${RESET} ${BOLD}AI Agent Session Center — Setup Wizard${RESET} ${CYAN}│${RESET}`);
62
+ console.log(`${CYAN}╰──────────────────────────────────────────────╯${RESET}`);
63
+
64
+ if (Object.keys(existing).length > 0) {
65
+ info(`Existing config found — current values shown as defaults`);
66
+ }
67
+
68
+ // 1. Port
69
+ const portStr = await askValue(1, TOTAL, 'Server port', existing.port || 3333);
70
+ const port = parseInt(portStr, 10) || 3333;
71
+
72
+ // 2. AI CLI selection
73
+ const cliOptions = [
74
+ { label: `Claude Code only`, value: ['claude'] },
75
+ { label: `Claude Code + Gemini CLI`, value: ['claude', 'gemini'] },
76
+ { label: `Claude Code + Codex CLI`, value: ['claude', 'codex'] },
77
+ { label: `All (Claude + Gemini + Codex)`, value: ['claude', 'gemini', 'codex'] },
78
+ ];
79
+ const currentCliIdx = (() => {
80
+ const ec = existing.enabledClis || ['claude'];
81
+ if (ec.length === 1 && ec[0] === 'claude') return 0;
82
+ if (ec.length === 2 && ec.includes('gemini')) return 1;
83
+ if (ec.length === 2 && ec.includes('codex')) return 2;
84
+ if (ec.length === 3) return 3;
85
+ return 0;
86
+ })();
87
+ const cliChoice = await choose(2, TOTAL, 'Which AI CLIs do you want to hook?', cliOptions, currentCliIdx);
88
+
89
+ // 3. Hook density
90
+ const densityOptions = [
91
+ { label: `high ${DIM}— All 14 events (includes TeammateIdle, PreCompact)${RESET}`, value: 'high' },
92
+ { label: `medium ${DIM}— 12 events (best balance of detail vs overhead)${RESET}`, value: 'medium' },
93
+ { label: `low ${DIM}— 5 events (minimal: start, prompt, permission, stop, end)${RESET}`, value: 'low' },
94
+ ];
95
+ const currentDensityIdx = densityOptions.findIndex(o => o.value === (existing.hookDensity || 'medium'));
96
+ const density = await choose(3, TOTAL, 'Hook trace density', densityOptions, currentDensityIdx >= 0 ? currentDensityIdx : 1);
97
+
98
+ // 4. Debug mode
99
+ const debugOptions = [
100
+ { label: `Off`, value: false },
101
+ { label: `On ${DIM}— Verbose logging for troubleshooting${RESET}`, value: true },
102
+ ];
103
+ const currentDebugIdx = existing.debug ? 1 : 0;
104
+ const debug = await choose(4, TOTAL, 'Debug mode?', debugOptions, currentDebugIdx);
105
+
106
+ // 5. Session history retention
107
+ const historyOptions = [
108
+ { label: `12 hours`, value: 12 },
109
+ { label: `24 hours`, value: 24 },
110
+ { label: `48 hours`, value: 48 },
111
+ { label: `7 days`, value: 168 },
112
+ ];
113
+ const currentHistIdx = historyOptions.findIndex(o => o.value === (existing.sessionHistoryHours || 24));
114
+ const history = await choose(5, TOTAL, 'Session history retention', historyOptions, currentHistIdx >= 0 ? currentHistIdx : 1);
115
+
116
+ rl.close();
117
+
118
+ // ── Save config ──
119
+ const configData = {
120
+ port,
121
+ enabledClis: cliChoice.value,
122
+ hookDensity: density.value,
123
+ debug: debug.value,
124
+ sessionHistoryHours: history.value,
125
+ };
126
+
127
+ const dataDir = join(PROJECT_ROOT, 'data');
128
+ if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
129
+ writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2) + '\n');
130
+ console.log('');
131
+ ok(`Config saved to ${DIM}data/server-config.json${RESET}`);
132
+
133
+ // ── Print chosen config ──
134
+ info(`Port: ${BOLD}${configData.port}${RESET}`);
135
+ info(`Enabled CLIs: ${BOLD}${configData.enabledClis.join(', ')}${RESET}`);
136
+ info(`Hook density: ${BOLD}${configData.hookDensity}${RESET}`);
137
+ info(`Debug: ${BOLD}${configData.debug ? 'ON' : 'OFF'}${RESET}`);
138
+ info(`History retention: ${BOLD}${configData.sessionHistoryHours}h${RESET}`);
139
+
140
+ // ── Install hooks with chosen density ──
141
+ console.log('');
142
+ info('Installing hooks...');
143
+ try {
144
+ execSync(`node "${join(__dirname, 'install-hooks.js')}" --density ${configData.hookDensity} --clis ${configData.enabledClis.join(',')}`, {
145
+ stdio: 'inherit',
146
+ cwd: PROJECT_ROOT,
147
+ });
148
+ } catch (e) {
149
+ console.log(` ${RED}✗${RESET} Hook installation failed: ${e.message}`);
150
+ }
151
+
152
+ console.log(`\n${GREEN}────────────────────────────────────────────────${RESET}`);
153
+ console.log(` ${GREEN}✓ Setup complete!${RESET}`);
154
+ console.log(`${GREEN}────────────────────────────────────────────────${RESET}`);
155
+ console.log(`\n Starting server on port ${BOLD}${configData.port}${RESET}...`);
156
+ console.log(` Browser will open automatically.\n`);
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "ai-agent-session-center",
3
+ "version": "1.0.0",
4
+ "description": "A real-time dashboard for monitoring AI agent sessions (Claude Code, Gemini CLI, Codex) with 3D visualization",
5
+ "type": "module",
6
+ "main": "server/index.js",
7
+ "bin": {
8
+ "ai-agent-session-center": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node server/index.js",
12
+ "start:no-open": "node server/index.js --no-open",
13
+ "setup": "npm install --ignore-scripts && node hooks/setup-wizard.js && node server/index.js",
14
+ "debug": "node server/index.js --debug",
15
+ "reset": "node hooks/reset.js",
16
+ "install-hooks": "node hooks/install-hooks.js",
17
+ "uninstall-hooks": "node hooks/install-hooks.js --uninstall",
18
+ "postinstall": "chmod +x node_modules/node-pty/prebuilds/darwin-*/spawn-helper 2>/dev/null || true"
19
+ },
20
+ "keywords": [
21
+ "claude-code",
22
+ "gemini-cli",
23
+ "codex",
24
+ "ai-agent",
25
+ "dashboard",
26
+ "monitoring",
27
+ "three.js",
28
+ "websocket",
29
+ "developer-tools"
30
+ ],
31
+ "author": "Kason Zhan",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/coding-by-feng/ai-agent-session-center"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "dependencies": {
41
+ "express": "^5.0.0",
42
+ "node-pty": "^1.1.0",
43
+ "ws": "^8.18.0"
44
+ },
45
+ "files": [
46
+ "server/**/*",
47
+ "public/**/*",
48
+ "hooks/**/*",
49
+ "bin/**/*",
50
+ "data/.gitkeep"
51
+ ]
52
+ }