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
@@ -0,0 +1,249 @@
1
+ // soundManager.js — Per-action configurable sound effects via Web Audio API
2
+ import * as settingsManager from './settingsManager.js';
3
+
4
+ let audioCtx = null;
5
+ let enabled = true;
6
+ let volume = 0.5;
7
+ let actionSounds = {}; // action -> soundName mapping
8
+
9
+ function getCtx() {
10
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
11
+ return audioCtx;
12
+ }
13
+
14
+ // ---- Synthesis helpers ----
15
+
16
+ function playTone(freq, duration, type = 'sine', vol = 1) {
17
+ const ctx = getCtx();
18
+ const osc = ctx.createOscillator();
19
+ const gain = ctx.createGain();
20
+ osc.type = type;
21
+ osc.frequency.setValueAtTime(freq, ctx.currentTime);
22
+ gain.gain.setValueAtTime(vol * volume * 0.3, ctx.currentTime);
23
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
24
+ osc.connect(gain);
25
+ gain.connect(ctx.destination);
26
+ osc.start(ctx.currentTime);
27
+ osc.stop(ctx.currentTime + duration);
28
+ }
29
+
30
+ function playSequence(freqs, spacing = 0.1, duration = 0.15, type = 'sine') {
31
+ freqs.forEach((f, i) => {
32
+ setTimeout(() => playTone(f, duration, type), i * spacing * 1000);
33
+ });
34
+ }
35
+
36
+ // ---- Sound Library ----
37
+
38
+ const soundLibrary = {
39
+ chirp: () => playTone(1200, 0.08, 'sine'),
40
+ ping: () => playTone(660, 0.2, 'sine'),
41
+ chime: () => playSequence([523, 659, 784], 0.08, 0.2),
42
+ ding: () => playTone(800, 0.25, 'triangle'),
43
+ blip: () => playTone(880, 0.05, 'square', 0.5),
44
+ swoosh: () => {
45
+ const ctx = getCtx();
46
+ const osc = ctx.createOscillator();
47
+ const gain = ctx.createGain();
48
+ osc.type = 'sine';
49
+ osc.frequency.setValueAtTime(300, ctx.currentTime);
50
+ osc.frequency.exponentialRampToValueAtTime(1200, ctx.currentTime + 0.25);
51
+ gain.gain.setValueAtTime(volume * 0.25, ctx.currentTime);
52
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
53
+ osc.connect(gain);
54
+ gain.connect(ctx.destination);
55
+ osc.start(ctx.currentTime);
56
+ osc.stop(ctx.currentTime + 0.3);
57
+ },
58
+ click: () => playTone(1200, 0.03, 'square', 0.2),
59
+ beep: () => playTone(440, 0.15, 'square', 0.4),
60
+ warble: () => {
61
+ const ctx = getCtx();
62
+ const osc = ctx.createOscillator();
63
+ const lfo = ctx.createOscillator();
64
+ const lfoGain = ctx.createGain();
65
+ const gain = ctx.createGain();
66
+ osc.type = 'sine';
67
+ osc.frequency.setValueAtTime(600, ctx.currentTime);
68
+ lfo.type = 'sine';
69
+ lfo.frequency.setValueAtTime(12, ctx.currentTime);
70
+ lfoGain.gain.setValueAtTime(50, ctx.currentTime);
71
+ lfo.connect(lfoGain);
72
+ lfoGain.connect(osc.frequency);
73
+ gain.gain.setValueAtTime(volume * 0.25, ctx.currentTime);
74
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
75
+ osc.connect(gain);
76
+ gain.connect(ctx.destination);
77
+ osc.start(ctx.currentTime);
78
+ lfo.start(ctx.currentTime);
79
+ osc.stop(ctx.currentTime + 0.3);
80
+ lfo.stop(ctx.currentTime + 0.3);
81
+ },
82
+ buzz: () => playTone(200, 0.12, 'sawtooth', 0.4),
83
+ cascade: () => playSequence([784, 659, 523, 392], 0.1, 0.2),
84
+ fanfare: () => playSequence([523, 659, 784, 1047, 1319], 0.08, 0.2),
85
+ alarm: () => playSequence([880, 660, 880, 660], 0.15, 0.15, 'square'),
86
+ thud: () => {
87
+ const ctx = getCtx();
88
+ const osc = ctx.createOscillator();
89
+ const gain = ctx.createGain();
90
+ osc.type = 'sine';
91
+ osc.frequency.setValueAtTime(80, ctx.currentTime);
92
+ osc.frequency.exponentialRampToValueAtTime(30, ctx.currentTime + 0.3);
93
+ gain.gain.setValueAtTime(volume * 0.5, ctx.currentTime);
94
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35);
95
+ osc.connect(gain);
96
+ gain.connect(ctx.destination);
97
+ osc.start(ctx.currentTime);
98
+ osc.stop(ctx.currentTime + 0.35);
99
+ },
100
+ urgentAlarm: () => {
101
+ // Loud 3-burst urgent alarm — impossible to miss
102
+ const ctx = getCtx();
103
+ const t = ctx.currentTime;
104
+ for (let burst = 0; burst < 3; burst++) {
105
+ const offset = burst * 0.4;
106
+ // High tone
107
+ const osc1 = ctx.createOscillator();
108
+ const gain1 = ctx.createGain();
109
+ osc1.type = 'square';
110
+ osc1.frequency.setValueAtTime(1000, t + offset);
111
+ osc1.frequency.setValueAtTime(800, t + offset + 0.1);
112
+ osc1.frequency.setValueAtTime(1000, t + offset + 0.2);
113
+ gain1.gain.setValueAtTime(volume * 0.5, t + offset);
114
+ gain1.gain.setValueAtTime(volume * 0.5, t + offset + 0.25);
115
+ gain1.gain.exponentialRampToValueAtTime(0.001, t + offset + 0.3);
116
+ osc1.connect(gain1);
117
+ gain1.connect(ctx.destination);
118
+ osc1.start(t + offset);
119
+ osc1.stop(t + offset + 0.3);
120
+ // Low undertone
121
+ const osc2 = ctx.createOscillator();
122
+ const gain2 = ctx.createGain();
123
+ osc2.type = 'sawtooth';
124
+ osc2.frequency.setValueAtTime(200, t + offset);
125
+ gain2.gain.setValueAtTime(volume * 0.3, t + offset);
126
+ gain2.gain.exponentialRampToValueAtTime(0.001, t + offset + 0.3);
127
+ osc2.connect(gain2);
128
+ gain2.connect(ctx.destination);
129
+ osc2.start(t + offset);
130
+ osc2.stop(t + offset + 0.3);
131
+ }
132
+ },
133
+ none: () => {}
134
+ };
135
+
136
+ // ---- Action types and default mapping ----
137
+
138
+ const defaultActionSounds = {
139
+ // Session events
140
+ sessionStart: 'chime',
141
+ sessionEnd: 'cascade',
142
+ promptSubmit: 'ping',
143
+ taskComplete: 'fanfare',
144
+ // Tool calls
145
+ toolRead: 'click',
146
+ toolWrite: 'blip',
147
+ toolEdit: 'blip',
148
+ toolBash: 'buzz',
149
+ toolGrep: 'click',
150
+ toolGlob: 'click',
151
+ toolWebFetch: 'swoosh',
152
+ toolTask: 'ding',
153
+ toolOther: 'click',
154
+ // System
155
+ approvalNeeded: 'alarm',
156
+ inputNeeded: 'chime',
157
+ alert: 'alarm',
158
+ kill: 'thud',
159
+ archive: 'ding',
160
+ subagentStart: 'chirp',
161
+ subagentStop: 'ping'
162
+ };
163
+
164
+ const actionLabels = {
165
+ sessionStart: 'Session Start',
166
+ sessionEnd: 'Session End',
167
+ promptSubmit: 'Prompt Submit',
168
+ taskComplete: 'Task Complete',
169
+ toolRead: 'Tool: Read',
170
+ toolWrite: 'Tool: Write',
171
+ toolEdit: 'Tool: Edit',
172
+ toolBash: 'Tool: Bash',
173
+ toolGrep: 'Tool: Grep',
174
+ toolGlob: 'Tool: Glob',
175
+ toolWebFetch: 'Tool: WebFetch',
176
+ toolTask: 'Tool: Task',
177
+ toolOther: 'Tool: Other',
178
+ approvalNeeded: 'Approval Needed',
179
+ inputNeeded: 'Input Needed',
180
+ alert: 'Alert',
181
+ kill: 'Kill',
182
+ archive: 'Archive',
183
+ subagentStart: 'Subagent Start',
184
+ subagentStop: 'Subagent Stop'
185
+ };
186
+
187
+ const actionCategories = {
188
+ 'Session Events': ['sessionStart', 'sessionEnd', 'promptSubmit', 'taskComplete'],
189
+ 'Tool Calls': ['toolRead', 'toolWrite', 'toolEdit', 'toolBash', 'toolGrep', 'toolGlob', 'toolWebFetch', 'toolTask', 'toolOther'],
190
+ 'System': ['approvalNeeded', 'inputNeeded', 'alert', 'kill', 'archive', 'subagentStart', 'subagentStop']
191
+ };
192
+
193
+ // ---- Public API ----
194
+
195
+ export function play(actionName) {
196
+ if (!enabled) return;
197
+ const soundName = actionSounds[actionName] || defaultActionSounds[actionName] || 'none';
198
+ const fn = soundLibrary[soundName];
199
+ if (fn) fn();
200
+ }
201
+
202
+ export function previewSound(soundName) {
203
+ const fn = soundLibrary[soundName];
204
+ if (fn) fn();
205
+ }
206
+
207
+ export function getSoundLibrary() {
208
+ return Object.keys(soundLibrary);
209
+ }
210
+
211
+ export function getActionSounds() {
212
+ return { ...defaultActionSounds, ...actionSounds };
213
+ }
214
+
215
+ export function getActionLabels() {
216
+ return { ...actionLabels };
217
+ }
218
+
219
+ export function getActionCategories() {
220
+ return actionCategories;
221
+ }
222
+
223
+ export function setActionSound(action, soundName) {
224
+ actionSounds[action] = soundName;
225
+ settingsManager.set('soundActions', JSON.stringify(actionSounds));
226
+ }
227
+
228
+ export function init() {
229
+ enabled = settingsManager.get('soundEnabled') === 'true';
230
+ volume = parseFloat(settingsManager.get('soundVolume')) || 0.5;
231
+
232
+ // Load per-action config
233
+ const saved = settingsManager.get('soundActions');
234
+ if (saved) {
235
+ try {
236
+ actionSounds = JSON.parse(saved);
237
+ } catch (e) {
238
+ actionSounds = {};
239
+ }
240
+ }
241
+
242
+ settingsManager.onChange('soundEnabled', (val) => { enabled = val === 'true'; });
243
+ settingsManager.onChange('soundVolume', (val) => { volume = parseFloat(val) || 0.5; });
244
+
245
+ // Resume audio context on first user interaction
246
+ document.addEventListener('click', () => {
247
+ if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
248
+ }, { once: true });
249
+ }
@@ -0,0 +1,118 @@
1
+ let wsConnected = false;
2
+
3
+ // Listen for WebSocket connection status events from wsClient
4
+ document.addEventListener('ws-status', (e) => {
5
+ wsConnected = e.detail === 'connected';
6
+ });
7
+
8
+ let historicalLoaded = false;
9
+ let lastHookStats = null;
10
+ let hookStatsVisible = false;
11
+
12
+ export async function loadHistoricalStats() {
13
+ // no-op: historical stats display removed from header
14
+ }
15
+
16
+ export function update(sessions) {
17
+ const el = document.getElementById('global-stats');
18
+ if (!el) return;
19
+ el.innerHTML = '';
20
+ // Re-add hook stats elements
21
+ if (lastHookStats) {
22
+ renderHookStatsBadge(el);
23
+ }
24
+ }
25
+
26
+ export function updateHookStats(stats) {
27
+ lastHookStats = stats;
28
+ const el = document.getElementById('global-stats');
29
+ if (!el) return;
30
+ renderHookStatsBadge(el);
31
+ }
32
+
33
+ function renderHookStatsBadge(container) {
34
+ if (!lastHookStats) return;
35
+
36
+ // Remove existing hook stats elements
37
+ container.querySelector('.hook-stats-toggle')?.remove();
38
+ container.querySelector('.hook-stats-panel')?.remove();
39
+
40
+ const { totalHooks, hooksPerMin, events } = lastHookStats;
41
+
42
+ // Calculate overall avg processing time across all events
43
+ let totalProcessing = 0;
44
+ let processingCount = 0;
45
+ for (const ev of Object.values(events)) {
46
+ if (ev.processing.avg > 0) {
47
+ totalProcessing += ev.processing.avg * ev.count;
48
+ processingCount += ev.count;
49
+ }
50
+ }
51
+ const avgProcessing = processingCount > 0 ? Math.round(totalProcessing / processingCount) : 0;
52
+
53
+ // Badge in header
54
+ const badge = document.createElement('span');
55
+ badge.className = 'stat hook-stats-toggle';
56
+ badge.title = 'Click to toggle hook performance details';
57
+ badge.style.cursor = 'pointer';
58
+ badge.innerHTML = `
59
+ <span class="stat-label">Hooks</span>
60
+ <span class="stat-value">${totalHooks} <span class="hook-rate">(${hooksPerMin}/min)</span></span>
61
+ <span class="stat-label" style="margin-left:8px">Avg</span>
62
+ <span class="stat-value">${avgProcessing}ms</span>
63
+ `;
64
+ badge.addEventListener('click', () => {
65
+ hookStatsVisible = !hookStatsVisible;
66
+ const panel = container.querySelector('.hook-stats-panel');
67
+ if (panel) panel.classList.toggle('hidden', !hookStatsVisible);
68
+ });
69
+ container.appendChild(badge);
70
+
71
+ // Detailed dropdown panel
72
+ const panel = document.createElement('div');
73
+ panel.className = `hook-stats-panel ${hookStatsVisible ? '' : 'hidden'}`;
74
+
75
+ // Build table rows sorted by count descending
76
+ const sortedEvents = Object.entries(events).sort((a, b) => b[1].count - a[1].count);
77
+ const rows = sortedEvents.map(([name, ev]) => {
78
+ const latStr = ev.latency.avg > 0
79
+ ? `${ev.latency.avg}ms <span class="hook-stat-dim">(p95: ${ev.latency.p95}ms)</span>`
80
+ : '<span class="hook-stat-dim">n/a</span>';
81
+ return `<tr>
82
+ <td class="hook-ev-name">${name}</td>
83
+ <td class="hook-ev-count">${ev.count}</td>
84
+ <td class="hook-ev-rate">${ev.rate}/min</td>
85
+ <td class="hook-ev-latency">${latStr}</td>
86
+ <td class="hook-ev-proc">${ev.processing.avg}ms <span class="hook-stat-dim">(p95: ${ev.processing.p95}ms)</span></td>
87
+ </tr>`;
88
+ }).join('');
89
+
90
+ panel.innerHTML = `
91
+ <div class="hook-stats-header">
92
+ <span>Hook Performance</span>
93
+ <button class="hook-stats-reset" title="Reset stats">Reset</button>
94
+ </div>
95
+ <table class="hook-stats-table">
96
+ <thead>
97
+ <tr>
98
+ <th>Event</th>
99
+ <th>Count</th>
100
+ <th>Rate</th>
101
+ <th>Delivery Latency</th>
102
+ <th>Server Processing</th>
103
+ </tr>
104
+ </thead>
105
+ <tbody>${rows}</tbody>
106
+ </table>
107
+ `;
108
+
109
+ panel.querySelector('.hook-stats-reset')?.addEventListener('click', async (e) => {
110
+ e.stopPropagation();
111
+ await fetch('/api/hook-stats/reset', { method: 'POST' });
112
+ lastHookStats = null;
113
+ container.querySelector('.hook-stats-toggle')?.remove();
114
+ panel.remove();
115
+ });
116
+
117
+ container.appendChild(panel);
118
+ }