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
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, copyFileSync, chmodSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
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
|
+
const MAGENTA = '\x1b[35m';
|
|
19
|
+
|
|
20
|
+
// ── Log helpers ──
|
|
21
|
+
const ok = (msg) => console.log(` ${GREEN}✓${RESET} ${msg}`);
|
|
22
|
+
const warn = (msg) => console.log(` ${YELLOW}⚠${RESET} ${msg}`);
|
|
23
|
+
const fail = (msg) => console.log(` ${RED}✗${RESET} ${msg}`);
|
|
24
|
+
const info = (msg) => console.log(` ${DIM}→${RESET} ${msg}`);
|
|
25
|
+
const step = (n, total, label) => console.log(`\n${CYAN}[${n}/${total}]${RESET} ${BOLD}${label}${RESET}`);
|
|
26
|
+
|
|
27
|
+
// ── Platform-specific hook config ──
|
|
28
|
+
const HOOK_SCRIPT = isWindows ? 'dashboard-hook.ps1' : 'dashboard-hook.sh';
|
|
29
|
+
const HOOKS_DIR = join(homedir(), '.claude', 'hooks');
|
|
30
|
+
const HOOK_DEST = join(HOOKS_DIR, HOOK_SCRIPT);
|
|
31
|
+
const HOOK_COMMAND = isWindows
|
|
32
|
+
? `powershell -NoProfile -ExecutionPolicy Bypass -File "${HOOK_DEST}"`
|
|
33
|
+
: '~/.claude/hooks/dashboard-hook.sh';
|
|
34
|
+
const HOOK_PATTERN = 'dashboard-hook.';
|
|
35
|
+
const HOOK_SOURCE = 'ai-agent-session-center'; // Marker to identify our hooks in settings
|
|
36
|
+
|
|
37
|
+
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
38
|
+
|
|
39
|
+
// All possible hook events (Claude Code supports 14 lifecycle events)
|
|
40
|
+
const ALL_EVENTS = [
|
|
41
|
+
'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PostToolUseFailure',
|
|
42
|
+
'PermissionRequest', 'Stop', 'Notification', 'SubagentStart', 'SubagentStop',
|
|
43
|
+
'TeammateIdle', 'TaskCompleted', 'PreCompact', 'SessionEnd'
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Density levels: which events to register
|
|
47
|
+
const DENSITY_EVENTS = {
|
|
48
|
+
high: ALL_EVENTS,
|
|
49
|
+
medium: [
|
|
50
|
+
'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PostToolUseFailure',
|
|
51
|
+
'PermissionRequest', 'Stop', 'Notification', 'SubagentStart', 'SubagentStop',
|
|
52
|
+
'TaskCompleted', 'SessionEnd'
|
|
53
|
+
],
|
|
54
|
+
low: [
|
|
55
|
+
'SessionStart', 'UserPromptSubmit', 'PermissionRequest', 'Stop', 'SessionEnd'
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Parse CLI flags, then fall back to saved config
|
|
60
|
+
let density = 'medium';
|
|
61
|
+
let enabledClis = ['claude'];
|
|
62
|
+
const densityArgIdx = process.argv.indexOf('--density');
|
|
63
|
+
if (densityArgIdx >= 0 && process.argv[densityArgIdx + 1]) {
|
|
64
|
+
const val = process.argv[densityArgIdx + 1].toLowerCase();
|
|
65
|
+
if (DENSITY_EVENTS[val]) {
|
|
66
|
+
density = val;
|
|
67
|
+
} else {
|
|
68
|
+
console.error(`${RED}ERROR${RESET} Invalid density: "${val}" (use: high, medium, low)`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
// Read from saved config if no CLI flag
|
|
73
|
+
try {
|
|
74
|
+
const configPath = join(__dirname, '..', 'data', 'server-config.json');
|
|
75
|
+
const savedConfig = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
76
|
+
if (savedConfig.hookDensity && DENSITY_EVENTS[savedConfig.hookDensity]) {
|
|
77
|
+
density = savedConfig.hookDensity;
|
|
78
|
+
}
|
|
79
|
+
if (savedConfig.enabledClis) enabledClis = savedConfig.enabledClis;
|
|
80
|
+
} catch { /* no saved config, use default */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Parse --clis flag (e.g., --clis claude,gemini,codex)
|
|
84
|
+
const clisArgIdx = process.argv.indexOf('--clis');
|
|
85
|
+
if (clisArgIdx >= 0 && process.argv[clisArgIdx + 1]) {
|
|
86
|
+
enabledClis = process.argv[clisArgIdx + 1].split(',').map(s => s.trim().toLowerCase());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const uninstallMode = process.argv.includes('--uninstall');
|
|
90
|
+
const quietMode = process.argv.includes('--quiet');
|
|
91
|
+
|
|
92
|
+
const EVENTS = DENSITY_EVENTS[density];
|
|
93
|
+
const TOTAL_STEPS = uninstallMode ? 4 : 6;
|
|
94
|
+
|
|
95
|
+
// Gemini events by density
|
|
96
|
+
const GEMINI_DENSITY_EVENTS = {
|
|
97
|
+
high: ['SessionStart', 'BeforeAgent', 'BeforeTool', 'AfterTool', 'AfterAgent', 'SessionEnd', 'Notification'],
|
|
98
|
+
medium: ['SessionStart', 'BeforeAgent', 'AfterAgent', 'SessionEnd', 'Notification'],
|
|
99
|
+
low: ['SessionStart', 'AfterAgent', 'SessionEnd'],
|
|
100
|
+
};
|
|
101
|
+
const GEMINI_EVENTS = GEMINI_DENSITY_EVENTS[density] || GEMINI_DENSITY_EVENTS.medium;
|
|
102
|
+
|
|
103
|
+
// ── Banner ──
|
|
104
|
+
if (!quietMode) {
|
|
105
|
+
console.log(`\n${CYAN}╭──────────────────────────────────────────────╮${RESET}`);
|
|
106
|
+
console.log(`${CYAN}│${RESET} ${BOLD}AI Agent Session Center — Hook Setup${RESET} ${CYAN}│${RESET}`);
|
|
107
|
+
console.log(`${CYAN}╰──────────────────────────────────────────────╯${RESET}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ═══════════════════════════════════════════════
|
|
111
|
+
// STEP 1: Platform Detection
|
|
112
|
+
// ═══════════════════════════════════════════════
|
|
113
|
+
step(1, TOTAL_STEPS, 'Detecting platform...');
|
|
114
|
+
ok(`Platform: ${process.platform} (${isWindows ? 'PowerShell' : 'Bash'} hook)`);
|
|
115
|
+
ok(`Architecture: ${process.arch}`);
|
|
116
|
+
ok(`Node.js: ${process.version}`);
|
|
117
|
+
ok(`Home directory: ${homedir()}`);
|
|
118
|
+
info(`Settings path: ${SETTINGS_PATH}`);
|
|
119
|
+
info(`Hooks directory: ${HOOKS_DIR}`);
|
|
120
|
+
|
|
121
|
+
if (uninstallMode) {
|
|
122
|
+
info(`Mode: ${YELLOW}UNINSTALL${RESET} (removing all dashboard hooks)`);
|
|
123
|
+
} else {
|
|
124
|
+
info(`Enabled CLIs: ${BOLD}${enabledClis.join(', ')}${RESET}`);
|
|
125
|
+
info(`Density: ${BOLD}${density}${RESET} → ${EVENTS.length} of ${ALL_EVENTS.length} Claude events`);
|
|
126
|
+
info(`Claude events: ${EVENTS.join(', ')}`);
|
|
127
|
+
if (enabledClis.includes('gemini')) {
|
|
128
|
+
info(`Gemini events: ${GEMINI_EVENTS.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
if (enabledClis.includes('codex')) {
|
|
131
|
+
info(`Codex: agent-turn-complete (only event)`);
|
|
132
|
+
}
|
|
133
|
+
const excluded = ALL_EVENTS.filter(e => !EVENTS.includes(e));
|
|
134
|
+
if (excluded.length > 0) {
|
|
135
|
+
info(`Excluded (Claude): ${DIM}${excluded.join(', ')}${RESET}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ═══════════════════════════════════════════════
|
|
140
|
+
// STEP 2: Dependency Check
|
|
141
|
+
// ═══════════════════════════════════════════════
|
|
142
|
+
step(2, TOTAL_STEPS, 'Checking dependencies...');
|
|
143
|
+
|
|
144
|
+
if (!isWindows && !uninstallMode) {
|
|
145
|
+
// Check jq
|
|
146
|
+
try {
|
|
147
|
+
const jqPath = execSync('which jq', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
148
|
+
const jqVersion = execSync('jq --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
149
|
+
ok(`jq: ${jqVersion} (${jqPath})`);
|
|
150
|
+
} catch {
|
|
151
|
+
warn(`jq: ${YELLOW}NOT FOUND${RESET} — hook will work but without PID/TTY/tab enrichment`);
|
|
152
|
+
info(`Install jq for full session detection:`);
|
|
153
|
+
info(` ${DIM}brew install jq${RESET} (macOS)`);
|
|
154
|
+
info(` ${DIM}apt install jq${RESET} (Linux)`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check curl
|
|
158
|
+
try {
|
|
159
|
+
const curlPath = execSync('which curl', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
160
|
+
ok(`curl: found (${curlPath})`);
|
|
161
|
+
} catch {
|
|
162
|
+
fail(`curl: ${RED}NOT FOUND${RESET} — hook cannot send data to dashboard!`);
|
|
163
|
+
info('Install curl to enable hook communication');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check bash version
|
|
167
|
+
try {
|
|
168
|
+
const bashVersion = execSync('bash --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
169
|
+
.split('\n')[0].replace(/.*version\s+/, '').replace(/\(.*/, '').trim();
|
|
170
|
+
ok(`bash: ${bashVersion}`);
|
|
171
|
+
} catch {
|
|
172
|
+
info('bash: version unknown');
|
|
173
|
+
}
|
|
174
|
+
} else if (isWindows) {
|
|
175
|
+
try {
|
|
176
|
+
const psVersion = execSync('powershell -NoProfile -Command "$PSVersionTable.PSVersion.ToString()"',
|
|
177
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
178
|
+
ok(`PowerShell: ${psVersion}`);
|
|
179
|
+
} catch {
|
|
180
|
+
warn('PowerShell: version unknown');
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
info('Dependency check skipped (uninstall mode)');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ═══════════════════════════════════════════════
|
|
187
|
+
// STEP 3: Prepare Directories & Read Settings
|
|
188
|
+
// ═══════════════════════════════════════════════
|
|
189
|
+
step(3, TOTAL_STEPS, 'Preparing directories & reading settings...');
|
|
190
|
+
|
|
191
|
+
// Ensure ~/.claude/hooks/ exists
|
|
192
|
+
if (existsSync(HOOKS_DIR)) {
|
|
193
|
+
ok(`Hooks directory exists: ${HOOKS_DIR}`);
|
|
194
|
+
} else {
|
|
195
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
196
|
+
ok(`Created hooks directory: ${HOOKS_DIR}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Ensure ~/.claude/ exists
|
|
200
|
+
const claudeDir = join(homedir(), '.claude');
|
|
201
|
+
if (!existsSync(claudeDir)) {
|
|
202
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
203
|
+
ok(`Created ~/.claude/ directory`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Read current settings
|
|
207
|
+
let settings;
|
|
208
|
+
try {
|
|
209
|
+
const raw = readFileSync(SETTINGS_PATH, 'utf8');
|
|
210
|
+
settings = JSON.parse(raw);
|
|
211
|
+
const size = statSync(SETTINGS_PATH).size;
|
|
212
|
+
ok(`Settings file loaded: ${SETTINGS_PATH} (${formatBytes(size)})`);
|
|
213
|
+
|
|
214
|
+
// Report existing hooks
|
|
215
|
+
const existingHookEvents = Object.keys(settings.hooks || {});
|
|
216
|
+
if (existingHookEvents.length > 0) {
|
|
217
|
+
info(`Existing hook events in settings: ${existingHookEvents.join(', ')}`);
|
|
218
|
+
// Check for non-dashboard hooks
|
|
219
|
+
for (const event of existingHookEvents) {
|
|
220
|
+
const groups = settings.hooks[event] || [];
|
|
221
|
+
const otherHooks = groups.filter(g => !g.hooks?.some(h => h.command?.includes(HOOK_PATTERN)));
|
|
222
|
+
if (otherHooks.length > 0) {
|
|
223
|
+
info(` ${event}: ${otherHooks.length} non-dashboard hook(s) will be preserved`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
info('No existing hooks registered');
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
if (err.code === 'ENOENT') {
|
|
231
|
+
settings = {};
|
|
232
|
+
ok(`Creating new settings file: ${SETTINGS_PATH}`);
|
|
233
|
+
} else {
|
|
234
|
+
fail(`Failed to read settings: ${err.message}`);
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!settings.hooks) settings.hooks = {};
|
|
240
|
+
|
|
241
|
+
// ═══════════════════════════════════════════════
|
|
242
|
+
// UNINSTALL MODE
|
|
243
|
+
// ═══════════════════════════════════════════════
|
|
244
|
+
if (uninstallMode) {
|
|
245
|
+
step(4, TOTAL_STEPS, 'Removing dashboard hooks...');
|
|
246
|
+
|
|
247
|
+
let removed = 0;
|
|
248
|
+
for (const event of ALL_EVENTS) {
|
|
249
|
+
if (!settings.hooks[event]) continue;
|
|
250
|
+
const before = settings.hooks[event].length;
|
|
251
|
+
settings.hooks[event] = settings.hooks[event].filter(group =>
|
|
252
|
+
!group.hooks?.some(h => h.command?.includes(HOOK_PATTERN))
|
|
253
|
+
);
|
|
254
|
+
if (settings.hooks[event].length === 0) {
|
|
255
|
+
delete settings.hooks[event];
|
|
256
|
+
}
|
|
257
|
+
if (before !== (settings.hooks[event]?.length ?? 0)) {
|
|
258
|
+
removed++;
|
|
259
|
+
ok(`Removed hook for ${event}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (removed === 0) {
|
|
264
|
+
info('No dashboard hooks were found to remove');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
268
|
+
settings.hooks = {};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
272
|
+
ok(`Saved settings: ${removed} hook(s) removed`);
|
|
273
|
+
|
|
274
|
+
// Summary
|
|
275
|
+
printSummary(`Uninstall complete — ${removed} hook(s) removed`);
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ═══════════════════════════════════════════════
|
|
280
|
+
// STEP 4: Configure Hook Events
|
|
281
|
+
// ═══════════════════════════════════════════════
|
|
282
|
+
step(4, TOTAL_STEPS, `Configuring hook events (density: ${density})...`);
|
|
283
|
+
|
|
284
|
+
let added = 0;
|
|
285
|
+
let updated = 0;
|
|
286
|
+
let unchanged = 0;
|
|
287
|
+
let removedCount = 0;
|
|
288
|
+
|
|
289
|
+
// Add/update hooks for events in the selected density
|
|
290
|
+
for (const event of EVENTS) {
|
|
291
|
+
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
292
|
+
|
|
293
|
+
const existingIdx = settings.hooks[event].findIndex(group =>
|
|
294
|
+
group.hooks?.some(h => h.command?.includes(HOOK_PATTERN))
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
if (existingIdx >= 0) {
|
|
298
|
+
const group = settings.hooks[event][existingIdx];
|
|
299
|
+
const hookEntry = group.hooks.find(h => h.command?.includes(HOOK_PATTERN));
|
|
300
|
+
if (hookEntry && hookEntry.command !== HOOK_COMMAND) {
|
|
301
|
+
hookEntry.command = HOOK_COMMAND;
|
|
302
|
+
ok(`Updated hook command for ${event}`);
|
|
303
|
+
updated++;
|
|
304
|
+
} else {
|
|
305
|
+
info(`${event}: already registered ${DIM}(no change)${RESET}`);
|
|
306
|
+
unchanged++;
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
settings.hooks[event].push({
|
|
310
|
+
_source: HOOK_SOURCE,
|
|
311
|
+
hooks: [{
|
|
312
|
+
type: 'command',
|
|
313
|
+
command: HOOK_COMMAND,
|
|
314
|
+
async: true
|
|
315
|
+
}]
|
|
316
|
+
});
|
|
317
|
+
ok(`Added hook for ${GREEN}${event}${RESET}`);
|
|
318
|
+
added++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Remove hooks for events NOT in the selected density
|
|
323
|
+
const excludedEvents = ALL_EVENTS.filter(e => !EVENTS.includes(e));
|
|
324
|
+
for (const event of excludedEvents) {
|
|
325
|
+
if (!settings.hooks[event]) continue;
|
|
326
|
+
const before = settings.hooks[event].length;
|
|
327
|
+
settings.hooks[event] = settings.hooks[event].filter(group =>
|
|
328
|
+
!group.hooks?.some(h => h.command?.includes(HOOK_PATTERN))
|
|
329
|
+
);
|
|
330
|
+
if (settings.hooks[event].length === 0) {
|
|
331
|
+
delete settings.hooks[event];
|
|
332
|
+
}
|
|
333
|
+
if (before !== (settings.hooks[event]?.length ?? 0)) {
|
|
334
|
+
removedCount++;
|
|
335
|
+
ok(`Removed hook for ${YELLOW}${event}${RESET} ${DIM}(not in ${density} density)${RESET}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Write settings
|
|
340
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
341
|
+
info(`Settings saved: ${GREEN}${added} added${RESET}, ${CYAN}${updated} updated${RESET}, ${YELLOW}${removedCount} removed${RESET}, ${DIM}${unchanged} unchanged${RESET}`);
|
|
342
|
+
|
|
343
|
+
// ═══════════════════════════════════════════════
|
|
344
|
+
// STEP 5: Deploy Hook Scripts
|
|
345
|
+
// ═══════════════════════════════════════════════
|
|
346
|
+
step(5, TOTAL_STEPS, 'Deploying hook scripts...');
|
|
347
|
+
|
|
348
|
+
// Copy primary hook script
|
|
349
|
+
const src = join(__dirname, HOOK_SCRIPT);
|
|
350
|
+
if (!existsSync(src)) {
|
|
351
|
+
fail(`Hook script not found: ${src}`);
|
|
352
|
+
console.error(`\n${RED}ERROR${RESET}: Expected hook script at ${src}`);
|
|
353
|
+
console.error('Make sure you are running this from the project directory.');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const srcSize = statSync(src).size;
|
|
358
|
+
copyFileSync(src, HOOK_DEST);
|
|
359
|
+
if (!isWindows) {
|
|
360
|
+
chmodSync(HOOK_DEST, 0o755);
|
|
361
|
+
}
|
|
362
|
+
ok(`Deployed ${HOOK_SCRIPT} → ${HOOK_DEST} (${formatBytes(srcSize)}, ${isWindows ? 'standard' : 'chmod 755'})`);
|
|
363
|
+
|
|
364
|
+
// Copy alternate platform hook (for reference / dual-boot)
|
|
365
|
+
const altScript = isWindows ? 'dashboard-hook.sh' : 'dashboard-hook.ps1';
|
|
366
|
+
const altSrc = join(__dirname, altScript);
|
|
367
|
+
if (existsSync(altSrc)) {
|
|
368
|
+
const altDest = join(HOOKS_DIR, altScript);
|
|
369
|
+
copyFileSync(altSrc, altDest);
|
|
370
|
+
if (!isWindows && altScript.endsWith('.sh')) chmodSync(altDest, 0o755);
|
|
371
|
+
info(`Also copied ${altScript} → ${altDest} ${DIM}(reference copy)${RESET}`);
|
|
372
|
+
} else {
|
|
373
|
+
info(`Alternate hook ${altScript} not found ${DIM}(skipped)${RESET}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Deploy Gemini hook script
|
|
377
|
+
if (enabledClis.includes('gemini')) {
|
|
378
|
+
const geminiSrc = join(__dirname, 'dashboard-hook-gemini.sh');
|
|
379
|
+
const geminiHooksDir = join(homedir(), '.gemini', 'hooks');
|
|
380
|
+
const geminiDest = join(geminiHooksDir, 'dashboard-hook.sh');
|
|
381
|
+
if (existsSync(geminiSrc)) {
|
|
382
|
+
mkdirSync(geminiHooksDir, { recursive: true });
|
|
383
|
+
copyFileSync(geminiSrc, geminiDest);
|
|
384
|
+
chmodSync(geminiDest, 0o755);
|
|
385
|
+
ok(`Deployed dashboard-hook-gemini.sh → ${geminiDest}`);
|
|
386
|
+
} else {
|
|
387
|
+
fail(`Gemini hook script not found: ${geminiSrc}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Register in ~/.gemini/settings.json
|
|
391
|
+
const geminiSettingsPath = join(homedir(), '.gemini', 'settings.json');
|
|
392
|
+
try {
|
|
393
|
+
let gs;
|
|
394
|
+
try { gs = JSON.parse(readFileSync(geminiSettingsPath, 'utf8')); } catch { gs = {}; }
|
|
395
|
+
if (!gs.hooks) gs.hooks = {};
|
|
396
|
+
let gChanged = 0;
|
|
397
|
+
for (const event of GEMINI_EVENTS) {
|
|
398
|
+
if (!gs.hooks[event]) gs.hooks[event] = [];
|
|
399
|
+
const has = gs.hooks[event].some(g => g.hooks?.some(h => h.command?.includes('dashboard-hook')));
|
|
400
|
+
if (!has) {
|
|
401
|
+
gs.hooks[event].push({
|
|
402
|
+
_source: HOOK_SOURCE,
|
|
403
|
+
hooks: [{ type: 'command', command: `~/.gemini/hooks/dashboard-hook.sh ${event}` }]
|
|
404
|
+
});
|
|
405
|
+
gChanged++;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (gChanged) {
|
|
409
|
+
mkdirSync(join(homedir(), '.gemini'), { recursive: true });
|
|
410
|
+
writeFileSync(geminiSettingsPath, JSON.stringify(gs, null, 2) + '\n');
|
|
411
|
+
ok(`Registered ${gChanged} Gemini hook events in ~/.gemini/settings.json`);
|
|
412
|
+
} else {
|
|
413
|
+
info('Gemini hooks already registered');
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
warn(`Gemini hook registration: ${e.message}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Deploy Codex hook script
|
|
421
|
+
if (enabledClis.includes('codex')) {
|
|
422
|
+
const codexSrc = join(__dirname, 'dashboard-hook-codex.sh');
|
|
423
|
+
const codexHooksDir = join(homedir(), '.codex', 'hooks');
|
|
424
|
+
const codexDest = join(codexHooksDir, 'dashboard-hook.sh');
|
|
425
|
+
if (existsSync(codexSrc)) {
|
|
426
|
+
mkdirSync(codexHooksDir, { recursive: true });
|
|
427
|
+
copyFileSync(codexSrc, codexDest);
|
|
428
|
+
chmodSync(codexDest, 0o755);
|
|
429
|
+
ok(`Deployed dashboard-hook-codex.sh → ${codexDest}`);
|
|
430
|
+
} else {
|
|
431
|
+
fail(`Codex hook script not found: ${codexSrc}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Register in ~/.codex/config.toml
|
|
435
|
+
const codexConfigPath = join(homedir(), '.codex', 'config.toml');
|
|
436
|
+
try {
|
|
437
|
+
let toml = '';
|
|
438
|
+
try { toml = readFileSync(codexConfigPath, 'utf8'); } catch {}
|
|
439
|
+
if (!toml.includes('dashboard-hook')) {
|
|
440
|
+
mkdirSync(join(homedir(), '.codex'), { recursive: true });
|
|
441
|
+
const commentLine = `# [${HOOK_SOURCE}] Dashboard hook — safe to remove with "npm run reset"`;
|
|
442
|
+
const notifyLine = 'notify = ["~/.codex/hooks/dashboard-hook.sh"]';
|
|
443
|
+
if (toml && !toml.endsWith('\n')) toml += '\n';
|
|
444
|
+
toml += commentLine + '\n' + notifyLine + '\n';
|
|
445
|
+
writeFileSync(codexConfigPath, toml);
|
|
446
|
+
ok('Registered Codex notify hook in ~/.codex/config.toml');
|
|
447
|
+
} else {
|
|
448
|
+
info('Codex hook already registered');
|
|
449
|
+
}
|
|
450
|
+
} catch (e) {
|
|
451
|
+
warn(`Codex hook registration: ${e.message}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ═══════════════════════════════════════════════
|
|
456
|
+
// STEP 6: Verify Installation
|
|
457
|
+
// ═══════════════════════════════════════════════
|
|
458
|
+
step(6, TOTAL_STEPS, 'Verifying installation...');
|
|
459
|
+
|
|
460
|
+
let verifyOk = true;
|
|
461
|
+
|
|
462
|
+
// Check hook script is executable
|
|
463
|
+
if (!isWindows) {
|
|
464
|
+
try {
|
|
465
|
+
execSync(`test -x "${HOOK_DEST}"`, { stdio: 'ignore' });
|
|
466
|
+
ok('Hook script is executable');
|
|
467
|
+
} catch {
|
|
468
|
+
fail('Hook script is NOT executable');
|
|
469
|
+
verifyOk = false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Verify settings file is valid JSON
|
|
474
|
+
try {
|
|
475
|
+
const check = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
|
|
476
|
+
const registeredEvents = Object.keys(check.hooks || {}).filter(event =>
|
|
477
|
+
check.hooks[event]?.some(g => g.hooks?.some(h => h.command?.includes(HOOK_PATTERN)))
|
|
478
|
+
);
|
|
479
|
+
ok(`Settings file: valid JSON`);
|
|
480
|
+
ok(`Dashboard hooks registered: ${registeredEvents.length} event(s)`);
|
|
481
|
+
if (registeredEvents.length !== EVENTS.length) {
|
|
482
|
+
warn(`Expected ${EVENTS.length} events but found ${registeredEvents.length}`);
|
|
483
|
+
verifyOk = false;
|
|
484
|
+
}
|
|
485
|
+
} catch (err) {
|
|
486
|
+
fail(`Settings file invalid: ${err.message}`);
|
|
487
|
+
verifyOk = false;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Check hook destination exists
|
|
491
|
+
if (existsSync(HOOK_DEST)) {
|
|
492
|
+
ok(`Claude hook file exists at ${HOOK_DEST}`);
|
|
493
|
+
} else {
|
|
494
|
+
fail(`Claude hook file missing: ${HOOK_DEST}`);
|
|
495
|
+
verifyOk = false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Verify Gemini hooks
|
|
499
|
+
if (enabledClis.includes('gemini')) {
|
|
500
|
+
const geminiDest = join(homedir(), '.gemini', 'hooks', 'dashboard-hook.sh');
|
|
501
|
+
if (existsSync(geminiDest)) {
|
|
502
|
+
ok(`Gemini hook file exists at ${geminiDest}`);
|
|
503
|
+
} else {
|
|
504
|
+
fail(`Gemini hook file missing: ${geminiDest}`);
|
|
505
|
+
verifyOk = false;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Verify Codex hooks
|
|
510
|
+
if (enabledClis.includes('codex')) {
|
|
511
|
+
const codexDest = join(homedir(), '.codex', 'hooks', 'dashboard-hook.sh');
|
|
512
|
+
if (existsSync(codexDest)) {
|
|
513
|
+
ok(`Codex hook file exists at ${codexDest}`);
|
|
514
|
+
} else {
|
|
515
|
+
fail(`Codex hook file missing: ${codexDest}`);
|
|
516
|
+
verifyOk = false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ═══════════════════════════════════════════════
|
|
521
|
+
// Summary
|
|
522
|
+
// ═══════════════════════════════════════════════
|
|
523
|
+
if (verifyOk) {
|
|
524
|
+
printSummary(`Setup complete! (density: ${density}, ${EVENTS.length} events)`);
|
|
525
|
+
info('Hook captures: claude_pid, tty_path, term_program, tab_id, vscode_pid, tmux, window_id');
|
|
526
|
+
console.log(`\n Start the dashboard: ${BOLD}npm start${RESET}\n`);
|
|
527
|
+
} else {
|
|
528
|
+
console.log(`\n${YELLOW}────────────────────────────────────────────────${RESET}`);
|
|
529
|
+
console.log(` ${YELLOW}⚠ Setup completed with warnings${RESET}`);
|
|
530
|
+
console.log(`${YELLOW}────────────────────────────────────────────────${RESET}\n`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ── Utility functions ──
|
|
534
|
+
function formatBytes(bytes) {
|
|
535
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
536
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function printSummary(msg) {
|
|
540
|
+
console.log(`\n${GREEN}────────────────────────────────────────────────${RESET}`);
|
|
541
|
+
console.log(` ${GREEN}✓ ${msg}${RESET}`);
|
|
542
|
+
console.log(`${GREEN}────────────────────────────────────────────────${RESET}`);
|
|
543
|
+
}
|