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.
- package/README.md +618 -0
- package/bin/cli.js +20 -0
- package/hooks/dashboard-hook-codex.sh +67 -0
- package/hooks/dashboard-hook-gemini.sh +102 -0
- package/hooks/dashboard-hook.ps1 +147 -0
- package/hooks/dashboard-hook.sh +142 -0
- package/hooks/dashboard-hooks-backup.json +103 -0
- package/hooks/install-hooks.js +543 -0
- package/hooks/reset.js +357 -0
- package/hooks/setup-wizard.js +156 -0
- package/package.json +52 -0
- package/public/css/dashboard.css +10200 -0
- package/public/index.html +915 -0
- package/public/js/analyticsPanel.js +467 -0
- package/public/js/app.js +1148 -0
- package/public/js/browserDb.js +806 -0
- package/public/js/chartUtils.js +383 -0
- package/public/js/historyPanel.js +298 -0
- package/public/js/movementManager.js +155 -0
- package/public/js/navController.js +32 -0
- package/public/js/robotManager.js +526 -0
- package/public/js/sceneManager.js +7 -0
- package/public/js/sessionPanel.js +2477 -0
- package/public/js/settingsManager.js +924 -0
- package/public/js/soundManager.js +249 -0
- package/public/js/statsPanel.js +118 -0
- package/public/js/terminalManager.js +391 -0
- package/public/js/timelinePanel.js +278 -0
- package/public/js/wsClient.js +88 -0
- package/server/apiRouter.js +321 -0
- package/server/config.js +120 -0
- package/server/hookProcessor.js +55 -0
- package/server/hookRouter.js +18 -0
- package/server/hookStats.js +107 -0
- package/server/index.js +314 -0
- package/server/logger.js +67 -0
- package/server/mqReader.js +218 -0
- package/server/serverConfig.js +27 -0
- package/server/sessionStore.js +1049 -0
- package/server/sshManager.js +339 -0
- 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
|
+
}
|