ai-agent-session-center 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +484 -429
  2. package/docs/3D/ADAPTATION_GUIDE.md +592 -0
  3. package/docs/3D/index.html +754 -0
  4. package/docs/AGENT_TEAM_TASKS.md +716 -0
  5. package/docs/CYBERDROME_V2_SPEC.md +531 -0
  6. package/docs/ERROR_185_ANALYSIS.md +263 -0
  7. package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
  8. package/docs/SESSION_DETAIL_FEATURES.md +98 -0
  9. package/docs/_3d_multimedia_features.md +1080 -0
  10. package/docs/_frontend_features.md +1057 -0
  11. package/docs/_server_features.md +1077 -0
  12. package/docs/session-duplication-fixes.md +271 -0
  13. package/docs/session-terminal-linkage.md +412 -0
  14. package/package.json +63 -5
  15. package/public/apple-touch-icon.svg +21 -0
  16. package/public/css/dashboard.css +0 -161
  17. package/public/css/detail-panel.css +25 -0
  18. package/public/css/layout.css +18 -1
  19. package/public/css/modals.css +0 -26
  20. package/public/css/settings.css +0 -150
  21. package/public/css/terminal.css +34 -0
  22. package/public/favicon.svg +18 -0
  23. package/public/index.html +6 -26
  24. package/public/js/alarmManager.js +0 -21
  25. package/public/js/app.js +21 -7
  26. package/public/js/detailPanel.js +63 -64
  27. package/public/js/historyPanel.js +61 -55
  28. package/public/js/quickActions.js +132 -48
  29. package/public/js/sessionCard.js +5 -20
  30. package/public/js/sessionControls.js +8 -0
  31. package/public/js/settingsManager.js +0 -142
  32. package/server/apiRouter.js +60 -15
  33. package/server/apiRouter.ts +774 -0
  34. package/server/approvalDetector.ts +94 -0
  35. package/server/authManager.ts +144 -0
  36. package/server/autoIdleManager.ts +110 -0
  37. package/server/config.ts +121 -0
  38. package/server/constants.ts +150 -0
  39. package/server/db.ts +475 -0
  40. package/server/hookInstaller.d.ts +3 -0
  41. package/server/hookProcessor.ts +108 -0
  42. package/server/hookRouter.ts +18 -0
  43. package/server/hookStats.ts +116 -0
  44. package/server/index.js +15 -1
  45. package/server/index.ts +230 -0
  46. package/server/logger.ts +75 -0
  47. package/server/mqReader.ts +349 -0
  48. package/server/portManager.ts +55 -0
  49. package/server/processMonitor.ts +239 -0
  50. package/server/serverConfig.ts +29 -0
  51. package/server/sessionMatcher.js +17 -6
  52. package/server/sessionMatcher.ts +403 -0
  53. package/server/sessionStore.js +109 -3
  54. package/server/sessionStore.ts +1145 -0
  55. package/server/sshManager.js +167 -24
  56. package/server/sshManager.ts +671 -0
  57. package/server/teamManager.ts +289 -0
  58. package/server/wsManager.ts +200 -0
@@ -53,6 +53,103 @@ function shellEscapeSingleQuote(str) {
53
53
  return str.replace(/'/g, "'\\''");
54
54
  }
55
55
 
56
+ // ---- Shell Ready Detection ----
57
+
58
+ // Match ANSI escape sequences (CSI + OSC + DEC private modes) for stripping from PTY output
59
+ const ANSI_ESC_RE = /\x1b\[[\x20-\x3f]*[0-9;]*[a-zA-Z@]|\x1b\].*?(?:\x07|\x1b\\)/g;
60
+
61
+ // Common shell prompt endings: $ (bash/zsh), % (zsh), # (root), > (fish/powershell),
62
+ // ❯ (starship), ➜ (oh-my-zsh robbyrussell), → » λ (other popular themes)
63
+ const SHELL_PROMPT_RE = /[#$%>❯➜→»λ]\s*$/;
64
+
65
+ /**
66
+ * Watch PTY output to detect when the shell is ready (prompt visible).
67
+ * Resolves `true` when a prompt is detected, `false` on timeout or PTY exit.
68
+ *
69
+ * Strategy:
70
+ * 1. Watch for prompt patterns in PTY output (50ms settle timer to avoid MOTD false matches)
71
+ * 2. After `probeDelayMs`, send a bare Enter to the PTY — if the shell is ready, this
72
+ * triggers a fresh prompt that gets detected immediately; if still loading, the Enter
73
+ * is harmlessly buffered by the TTY driver
74
+ * 3. Hard timeout as final fallback (command will still work — PTY input is buffered)
75
+ *
76
+ * @param {import('node-pty').IPty} ptyProcess
77
+ * @param {string} terminalId - For logging
78
+ * @param {number} timeoutMs - Max wait time before fallback
79
+ * @param {number} probeDelayMs - Delay before sending Enter probe (0 to disable)
80
+ * @returns {Promise<boolean>}
81
+ */
82
+ function detectShellReady(ptyProcess, terminalId, timeoutMs, probeDelayMs = 0) {
83
+ let resolveFn;
84
+ const promise = new Promise(r => { resolveFn = r; });
85
+ let buffer = '';
86
+ let done = false;
87
+ let settleTimer = null;
88
+ let probeTimer = null;
89
+
90
+ function finish(detected) {
91
+ if (done) return;
92
+ done = true;
93
+ clearTimeout(fallbackTimer);
94
+ clearTimeout(settleTimer);
95
+ clearTimeout(probeTimer);
96
+ dataDisp.dispose();
97
+ exitDisp.dispose();
98
+ resolveFn(detected);
99
+ }
100
+
101
+ function checkPrompt() {
102
+ const stripped = buffer.replace(ANSI_ESC_RE, '');
103
+ const lines = stripped.split(/[\r\n]+/);
104
+ // Find the last non-empty line
105
+ let lastLine = '';
106
+ for (let i = lines.length - 1; i >= 0; i--) {
107
+ if (lines[i].trim()) { lastLine = lines[i].trim(); break; }
108
+ }
109
+ // Shell prompts are short and end with $ % # >
110
+ if (lastLine && lastLine.length < 200 && SHELL_PROMPT_RE.test(lastLine)) {
111
+ log.debug('pty', `Shell prompt detected for ${terminalId}: "${lastLine.slice(-60)}"`);
112
+ finish(true);
113
+ }
114
+ }
115
+
116
+ const dataDisp = ptyProcess.onData((data) => {
117
+ if (done) return;
118
+ buffer += data;
119
+ // Cap buffer to avoid memory issues with large MOTD output
120
+ if (buffer.length > 4096) buffer = buffer.slice(-4096);
121
+ // Wait for output to settle (30ms of silence) before checking —
122
+ // MOTD lines arrive in bursts, but the final prompt is followed by silence
123
+ clearTimeout(settleTimer);
124
+ settleTimer = setTimeout(checkPrompt, 30);
125
+ });
126
+
127
+ const exitDisp = ptyProcess.onExit(() => {
128
+ log.debug('pty', `PTY ${terminalId} exited before shell ready detected`);
129
+ finish(false);
130
+ });
131
+
132
+ // Active probe: send Enter after delay to force a fresh prompt.
133
+ // This handles shells whose prompts don't match SHELL_PROMPT_RE
134
+ // (e.g., powerlevel10k, custom themes) — the new prompt after Enter
135
+ // gives another chance for detection, and even if detection still fails,
136
+ // the shorter hard timeout kicks in quickly.
137
+ if (probeDelayMs > 0) {
138
+ probeTimer = setTimeout(() => {
139
+ if (done) return;
140
+ log.debug('pty', `Sending Enter probe to ${terminalId} (no prompt detected after ${probeDelayMs}ms)`);
141
+ try { ptyProcess.write('\r'); } catch { /* pty may have exited */ }
142
+ }, probeDelayMs);
143
+ }
144
+
145
+ const fallbackTimer = setTimeout(() => {
146
+ log.warn('pty', `Shell ready detection timed out for ${terminalId} after ${timeoutMs}ms — sending command as fallback`);
147
+ finish(false);
148
+ }, timeoutMs);
149
+
150
+ return promise;
151
+ }
152
+
56
153
  // List available SSH keys from ~/.ssh/
57
154
  export function listSshKeys() {
58
155
  const sshDir = join(homedir(), '.ssh');
@@ -207,6 +304,15 @@ export function createTerminal(config, wsClient) {
207
304
 
208
305
  log.info('pty', `Spawned ${local ? 'local' : `remote (${config.host})`} terminal ${terminalId} (pid: ${ptyProcess.pid})`);
209
306
 
307
+ // Detect when the shell is ready (prompt visible) before sending commands.
308
+ // Local shells init in ~100-300ms; remote SSH can take seconds for key exchange.
309
+ // Probe sends an Enter after delay to force a prompt for shells with unrecognized prompts.
310
+ const shellReady = detectShellReady(
311
+ ptyProcess, terminalId,
312
+ local ? 1200 : 3000, // hard timeout
313
+ local ? 300 : 1000, // Enter probe delay
314
+ );
315
+
210
316
  terminals.set(terminalId, {
211
317
  pty: ptyProcess,
212
318
  sessionId: null,
@@ -214,6 +320,7 @@ export function createTerminal(config, wsClient) {
214
320
  wsClient,
215
321
  createdAt: Date.now(),
216
322
  outputBuffer: Buffer.alloc(0),
323
+ shellReady,
217
324
  });
218
325
 
219
326
  // Register pending link for session matching
@@ -250,47 +357,64 @@ export function createTerminal(config, wsClient) {
250
357
  cleanup(terminalId);
251
358
  });
252
359
 
253
- // Send the launch command after shell/SSH init
360
+ // Send the launch command once the shell is ready (prompt detected).
254
361
  // API keys are passed via env object to pty.spawn (above), not via shell commands.
255
362
  // For remote SSH sessions, we export the env var in the shell since env doesn't
256
363
  // propagate across SSH.
257
364
  // When skipAutoLaunch is true, the caller will write the command itself
258
365
  // (e.g., resume with || fallback that contains shell metacharacters).
259
366
  if (!skipAutoLaunch) {
260
- setTimeout(() => {
261
- let launchCmd;
262
-
263
- if (config.tmuxSession) {
264
- // Attach to existing tmux session (validated above as alphanumeric+dash+underscore+dot)
265
- launchCmd = `tmux attach -t '${shellEscapeSingleQuote(config.tmuxSession)}'`;
266
- } else if (config.useTmux) {
267
- // Wrap command in a new tmux session
268
- const tmuxName = `claude-${Date.now().toString(36)}`;
269
- let innerCmd = local ? '' : `cd '${shellEscapeSingleQuote(workDir)}' && `;
270
- if (!local && config.apiKey) {
367
+ // Build the launch command eagerly — only the write is deferred
368
+ let launchCmd;
369
+
370
+ if (config.tmuxSession) {
371
+ // Attach to existing tmux session (validated above as alphanumeric+dash+underscore+dot)
372
+ launchCmd = `tmux attach -t '${shellEscapeSingleQuote(config.tmuxSession)}'`;
373
+ } else if (config.useTmux) {
374
+ // Wrap command in a new tmux session
375
+ const tmuxName = `claude-${Date.now().toString(36)}`;
376
+ let innerCmd = local ? '' : `cd '${shellEscapeSingleQuote(workDir)}' && `;
377
+ if (!local) {
378
+ // Export terminal ID for hook matching over SSH
379
+ innerCmd += `export AGENT_MANAGER_TERMINAL_ID='${shellEscapeSingleQuote(terminalId)}' && `;
380
+ if (config.apiKey) {
271
381
  const envVar = command.startsWith('codex') ? 'OPENAI_API_KEY'
272
382
  : command.startsWith('gemini') ? 'GEMINI_API_KEY'
273
383
  : 'ANTHROPIC_API_KEY';
274
384
  innerCmd += `export ${envVar}='${shellEscapeSingleQuote(config.apiKey)}' && `;
275
385
  }
276
- innerCmd += command;
277
- launchCmd = `tmux new-session -s '${tmuxName}' '${shellEscapeSingleQuote(innerCmd)}'`;
278
- } else {
279
- // Direct launch
280
- launchCmd = local ? '' : `cd '${shellEscapeSingleQuote(workDir)}'`;
281
- if (!local && config.apiKey) {
282
- if (launchCmd) launchCmd += ' && ';
386
+ }
387
+ innerCmd += command;
388
+ launchCmd = `tmux new-session -s '${tmuxName}' '${shellEscapeSingleQuote(innerCmd)}'`;
389
+ } else {
390
+ // Direct launch
391
+ launchCmd = local ? '' : `cd '${shellEscapeSingleQuote(workDir)}'`;
392
+ if (!local) {
393
+ // Export AGENT_MANAGER_TERMINAL_ID on the remote side so hooks
394
+ // can include it for Priority 0/1 session matching (SSH doesn't
395
+ // forward env vars from the local PTY).
396
+ if (launchCmd) launchCmd += ' && ';
397
+ launchCmd += `export AGENT_MANAGER_TERMINAL_ID='${shellEscapeSingleQuote(terminalId)}'`;
398
+ if (config.apiKey) {
283
399
  const envVar = command.startsWith('codex') ? 'OPENAI_API_KEY'
284
400
  : command.startsWith('gemini') ? 'GEMINI_API_KEY'
285
401
  : 'ANTHROPIC_API_KEY';
286
- launchCmd += `export ${envVar}='${shellEscapeSingleQuote(config.apiKey)}'`;
402
+ launchCmd += ` && export ${envVar}='${shellEscapeSingleQuote(config.apiKey)}'`;
287
403
  }
288
- if (launchCmd) launchCmd += ' && ';
289
- launchCmd += command;
290
404
  }
405
+ if (launchCmd) launchCmd += ' && ';
406
+ launchCmd += command;
407
+ }
291
408
 
292
- ptyProcess.write(launchCmd + '\r');
293
- }, local ? 100 : 500);
409
+ // Wait for shell prompt before writing — replaces the old blind setTimeout
410
+ shellReady.then((detected) => {
411
+ const term = terminals.get(terminalId);
412
+ if (!term || !term.pty) return;
413
+ if (!detected) {
414
+ log.warn('pty', `Sending launch command to ${terminalId} despite no prompt detected`);
415
+ }
416
+ term.pty.write(launchCmd + '\r');
417
+ });
294
418
  }
295
419
 
296
420
  // Notify client terminal is ready
@@ -408,6 +532,25 @@ export function writeToTerminal(terminalId, data) {
408
532
  }
409
533
  }
410
534
 
535
+ /**
536
+ * Write data to a terminal after its shell is ready.
537
+ * Awaits the shell prompt detection before writing, so commands aren't lost
538
+ * if SSH hasn't finished connecting yet.
539
+ * @param {string} terminalId
540
+ * @param {string} data
541
+ * @returns {Promise<boolean>} true if written, false if terminal was gone
542
+ */
543
+ export async function writeWhenReady(terminalId, data) {
544
+ const term = terminals.get(terminalId);
545
+ if (!term) return false;
546
+ if (term.shellReady) await term.shellReady;
547
+ // Terminal might have been cleaned up while waiting
548
+ const termNow = terminals.get(terminalId);
549
+ if (!termNow || !termNow.pty) return false;
550
+ termNow.pty.write(data);
551
+ return true;
552
+ }
553
+
411
554
  export function resizeTerminal(terminalId, cols, rows) {
412
555
  const term = terminals.get(terminalId);
413
556
  if (term && term.pty) {