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.
- package/README.md +484 -429
- package/docs/3D/ADAPTATION_GUIDE.md +592 -0
- package/docs/3D/index.html +754 -0
- package/docs/AGENT_TEAM_TASKS.md +716 -0
- package/docs/CYBERDROME_V2_SPEC.md +531 -0
- package/docs/ERROR_185_ANALYSIS.md +263 -0
- package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
- package/docs/SESSION_DETAIL_FEATURES.md +98 -0
- package/docs/_3d_multimedia_features.md +1080 -0
- package/docs/_frontend_features.md +1057 -0
- package/docs/_server_features.md +1077 -0
- package/docs/session-duplication-fixes.md +271 -0
- package/docs/session-terminal-linkage.md +412 -0
- package/package.json +63 -5
- package/public/apple-touch-icon.svg +21 -0
- package/public/css/dashboard.css +0 -161
- package/public/css/detail-panel.css +25 -0
- package/public/css/layout.css +18 -1
- package/public/css/modals.css +0 -26
- package/public/css/settings.css +0 -150
- package/public/css/terminal.css +34 -0
- package/public/favicon.svg +18 -0
- package/public/index.html +6 -26
- package/public/js/alarmManager.js +0 -21
- package/public/js/app.js +21 -7
- package/public/js/detailPanel.js +63 -64
- package/public/js/historyPanel.js +61 -55
- package/public/js/quickActions.js +132 -48
- package/public/js/sessionCard.js +5 -20
- package/public/js/sessionControls.js +8 -0
- package/public/js/settingsManager.js +0 -142
- package/server/apiRouter.js +60 -15
- package/server/apiRouter.ts +774 -0
- package/server/approvalDetector.ts +94 -0
- package/server/authManager.ts +144 -0
- package/server/autoIdleManager.ts +110 -0
- package/server/config.ts +121 -0
- package/server/constants.ts +150 -0
- package/server/db.ts +475 -0
- package/server/hookInstaller.d.ts +3 -0
- package/server/hookProcessor.ts +108 -0
- package/server/hookRouter.ts +18 -0
- package/server/hookStats.ts +116 -0
- package/server/index.js +15 -1
- package/server/index.ts +230 -0
- package/server/logger.ts +75 -0
- package/server/mqReader.ts +349 -0
- package/server/portManager.ts +55 -0
- package/server/processMonitor.ts +239 -0
- package/server/serverConfig.ts +29 -0
- package/server/sessionMatcher.js +17 -6
- package/server/sessionMatcher.ts +403 -0
- package/server/sessionStore.js +109 -3
- package/server/sessionStore.ts +1145 -0
- package/server/sshManager.js +167 -24
- package/server/sshManager.ts +671 -0
- package/server/teamManager.ts +289 -0
- package/server/wsManager.ts +200 -0
package/server/sshManager.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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) {
|