@dmsdc-ai/aigentry-telepty 0.1.57 → 0.1.58

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.
@@ -0,0 +1 @@
1
+ RESOLVED - all 3 urgent issues fixed in 40b8e47
package/cli.js CHANGED
@@ -598,6 +598,12 @@ async function main() {
598
598
  return;
599
599
  }
600
600
 
601
+ if (cmd === 'tui' || cmd === 'dashboard') {
602
+ const { TuiDashboard } = require('./tui');
603
+ new TuiDashboard();
604
+ return;
605
+ }
606
+
601
607
  if (cmd === 'list') {
602
608
  try {
603
609
  const sessions = await discoverSessions({ silent: true });
@@ -674,6 +680,15 @@ async function main() {
674
680
  sessionId = `${folder}-${cli}`;
675
681
  }
676
682
 
683
+ // Override inherited TELEPTY_SESSION_ID — prevent parent session hijacking
684
+ // When launched via kitty @ launch, the parent's env leaks through.
685
+ // With --id flag, we always use the explicitly requested session ID.
686
+ if (process.env.TELEPTY_SESSION_ID && process.env.TELEPTY_SESSION_ID !== sessionId) {
687
+ console.error(`⚠️ [allow] Overriding inherited TELEPTY_SESSION_ID="${process.env.TELEPTY_SESSION_ID}" → "${sessionId}"`);
688
+ }
689
+ delete process.env.TELEPTY_SESSION_ID;
690
+ process.env.TELEPTY_SESSION_ID = sessionId;
691
+
677
692
  await ensureDaemonRunning({ requiredCapabilities: ['wrapped-sessions'] });
678
693
 
679
694
  // Register session with daemon
@@ -737,7 +752,9 @@ async function main() {
737
752
  }
738
753
 
739
754
  // Connect to daemon WebSocket with auto-reconnect
740
- const wsUrl = `ws://${REMOTE_HOST}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
755
+ // owner=1 tells daemon this is the allow bridge (owner), not an attach viewer.
756
+ // Daemon uses this to reclaim ownership even if a stale ownerWs is still registered.
757
+ const wsUrl = `ws://${REMOTE_HOST}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}&owner=1`;
741
758
  let daemonWs = null;
742
759
  let wsReady = false;
743
760
  let reconnectAttempts = 0;
@@ -778,10 +795,17 @@ async function main() {
778
795
  try {
779
796
  const msg = JSON.parse(message);
780
797
  if (msg.type === 'inject') {
781
- const isFollowUpCr = msg.data === '\r' && (Date.now() - lastInjectTextTime) < 1000;
782
- if (promptReady || isFollowUpCr) {
798
+ const isCr = msg.data === '\r';
799
+ if (isCr && injectQueue.length > 0) {
800
+ // CR with pending queued text — queue CR too and flush immediately.
801
+ // Prevents the busy-session bug where CR fires before queued text,
802
+ // causing empty submit followed by orphaned text without Return.
803
+ injectQueue.push(msg.data);
804
+ if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
805
+ flushInjectQueue();
806
+ } else if (isCr || promptReady) {
783
807
  child.write(msg.data);
784
- if (msg.data !== '\r' && msg.data.length > 1) {
808
+ if (!isCr && msg.data.length > 1) {
785
809
  promptReady = false;
786
810
  lastInjectTextTime = Date.now();
787
811
  }
@@ -1176,6 +1200,22 @@ async function main() {
1176
1200
  process.exit(1);
1177
1201
  }
1178
1202
 
1203
+ // Resolve full paths (kitty @ launch has no shell PATH)
1204
+ const cliParts = cli.split(' ');
1205
+ let teleptyFullPath, cliFullPath;
1206
+ try {
1207
+ teleptyFullPath = execSync('which telepty', { encoding: 'utf8' }).trim();
1208
+ } catch {
1209
+ teleptyFullPath = process.argv[1];
1210
+ }
1211
+ try {
1212
+ cliFullPath = execSync(`which ${cliParts[0]}`, { encoding: 'utf8' }).trim();
1213
+ } catch {
1214
+ cliFullPath = cliParts[0];
1215
+ }
1216
+ const cliFullArgs = cliParts.slice(1).join(' ');
1217
+ const nodeFullPath = process.execPath; // Bypass #!/usr/bin/env node shebang (nvm not in PATH for non-interactive shells)
1218
+
1179
1219
  // Generate kitty session file
1180
1220
  const sessionFile = path.join(os.tmpdir(), `telepty-session-${Date.now()}.conf`);
1181
1221
  let conf = '# Auto-generated telepty session\n';
@@ -1191,19 +1231,185 @@ async function main() {
1191
1231
  conf += `layout tall\n`;
1192
1232
  conf += `cd ${cwd}\n`;
1193
1233
  conf += `title ${name}\n`;
1194
- conf += `launch --type=window telepty allow --id ${sessionId} ${cli}\n`;
1234
+ conf += `env TELEPTY_SESSION_ID=\n`;
1235
+ conf += `env PATH=${process.env.PATH}\n`;
1236
+ conf += `launch --type=window /bin/zsh -c 'unset TELEPTY_SESSION_ID; ${nodeFullPath} ${teleptyFullPath} allow --id ${sessionId} ${cliFullPath}${cliFullArgs ? ' ' + cliFullArgs : ''}'\n`;
1195
1237
  });
1196
1238
 
1197
1239
  fs.writeFileSync(sessionFile, conf);
1198
1240
  console.log(`✅ Kitty session file: ${sessionFile}`);
1199
1241
  console.log(` ${projects.length} projects, CLI: ${cli}`);
1200
- console.log(`\n Launch: kitty --session ${sessionFile}\n`);
1201
1242
 
1202
1243
  // Auto-launch if --launch flag
1203
1244
  if (args.includes('--launch')) {
1204
- const { spawn } = require('child_process');
1205
- spawn('kitty', ['--session', sessionFile], { detached: true, stdio: 'ignore' }).unref();
1206
- console.log('🚀 Kitty launched.');
1245
+ const { spawn, execFileSync } = require('child_process');
1246
+
1247
+ // Detect existing kitty instance via remote control socket
1248
+ let kittySocket = null;
1249
+ try {
1250
+ const sockFiles = fs.readdirSync('/tmp').filter(f => f.startsWith('kitty-sock'));
1251
+ if (sockFiles.length > 0) {
1252
+ const candidate = '/tmp/' + sockFiles[0];
1253
+ // Verify socket is alive
1254
+ execFileSync('kitty', ['@', '--to', `unix:${candidate}`, 'ls'], {
1255
+ timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
1256
+ });
1257
+ kittySocket = candidate;
1258
+ }
1259
+ } catch { kittySocket = null; }
1260
+
1261
+ if (kittySocket) {
1262
+ // Launch tabs in existing kitty instance (single Dock icon, kitty @ controllable)
1263
+ let launched = 0;
1264
+ for (const p of projects) {
1265
+ const name = p.name;
1266
+ const cwd = p.cwd || path.join(projectsDir, name);
1267
+ const sessionIdForProject = `${name}-${cli.split(' ')[0]}`;
1268
+ const shellCmd = `unset TELEPTY_SESSION_ID; ${nodeFullPath} ${teleptyFullPath} allow --id ${sessionIdForProject} ${cliFullPath}${cliFullArgs ? ' ' + cliFullArgs : ''}`;
1269
+ const launchArgs = ['@', '--to', `unix:${kittySocket}`,
1270
+ 'launch', '--type=tab', '--tab-title', name, '--cwd', cwd,
1271
+ '--env', 'TELEPTY_SESSION_ID=',
1272
+ '--env', `PATH=${process.env.PATH}`,
1273
+ '/bin/zsh', '-c', shellCmd];
1274
+ try {
1275
+ execFileSync('kitty', launchArgs, { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
1276
+ launched++;
1277
+ } catch (e) {
1278
+ console.error(`⚠️ Failed to launch tab for ${name}: ${e.message}`);
1279
+ }
1280
+ }
1281
+ console.log(`🚀 ${launched}/${projects.length} tabs launched in existing kitty instance.`);
1282
+ } else {
1283
+ // No existing kitty — start a new instance with session file
1284
+ console.log(`\n Launch: kitty --session ${sessionFile}\n`);
1285
+ spawn('kitty', ['--session', sessionFile], { detached: true, stdio: 'ignore' }).unref();
1286
+ console.log('🚀 Kitty launched (new instance).');
1287
+ }
1288
+ } else {
1289
+ console.log(`\n Launch: kitty --session ${sessionFile}\n`);
1290
+ }
1291
+ return;
1292
+ }
1293
+
1294
+ if (cmd === 'layout') {
1295
+ const layoutType = args[1] || 'grid';
1296
+ const validLayouts = ['grid', 'tall', 'stack'];
1297
+ if (!validLayouts.includes(layoutType)) {
1298
+ console.error(`❌ Invalid layout: ${layoutType}. Valid: ${validLayouts.join(', ')}`);
1299
+ process.exit(1);
1300
+ }
1301
+
1302
+ await ensureDaemonRunning();
1303
+ const { execSync } = require('child_process');
1304
+
1305
+ // Get active session count for grid calculation
1306
+ try {
1307
+ const sessionsRes = await fetchWithAuth(`${DAEMON_URL}/api/sessions`);
1308
+ const sessionsList = await sessionsRes.json();
1309
+ const activeIds = Object.keys(sessionsList);
1310
+ if (activeIds.length === 0) {
1311
+ console.log('No active sessions to arrange.');
1312
+ return;
1313
+ }
1314
+ } catch (e) {
1315
+ console.error('❌ Could not fetch sessions:', e.message);
1316
+ process.exit(1);
1317
+ }
1318
+
1319
+ // Detect kitty process name
1320
+ let processName = 'kitty';
1321
+ try {
1322
+ execSync(`osascript -e 'tell application "System Events" to get name of first process whose name is "kitty"'`, {
1323
+ timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
1324
+ });
1325
+ } catch {
1326
+ processName = 'stable';
1327
+ }
1328
+
1329
+ // Get screen dimensions
1330
+ let screenW = 2560, screenH = 1440;
1331
+ try {
1332
+ const bounds = execSync(`osascript -e 'tell application "Finder" to get bounds of window of desktop'`, {
1333
+ encoding: 'utf8', timeout: 3000
1334
+ }).trim();
1335
+ const parts = bounds.split(', ');
1336
+ screenW = parseInt(parts[2]);
1337
+ screenH = parseInt(parts[3]);
1338
+ } catch {}
1339
+
1340
+ const menuBarH = 25;
1341
+ const dockH = 70;
1342
+ const usableH = screenH - menuBarH - dockH;
1343
+
1344
+ // Build AppleScript — collect windows from ALL kitty process instances
1345
+ // (kitty --session spawns separate processes, each with its own window)
1346
+ const collectWindows = `
1347
+ set wList to {}
1348
+ set kittyProcs to every process whose name is "${processName}"
1349
+ repeat with p in kittyProcs
1350
+ repeat with w in (every window of p)
1351
+ set end of wList to w
1352
+ end repeat
1353
+ end repeat
1354
+ set n to count of wList
1355
+ if n = 0 then return "0"`;
1356
+
1357
+ let layoutBody;
1358
+ if (layoutType === 'grid') {
1359
+ layoutBody = `
1360
+ set numCols to (n ^ 0.5) as integer
1361
+ if numCols * numCols < n then set numCols to numCols + 1
1362
+ set numRows to ((n - 1) div numCols) + 1
1363
+ set cellW to ${screenW} div numCols
1364
+ set cellH to ${usableH} div numRows
1365
+ repeat with i from 1 to n
1366
+ set c to ((i - 1) mod numCols)
1367
+ set r to ((i - 1) div numCols)
1368
+ set position of (item i of wList) to {c * cellW, ${menuBarH} + r * cellH}
1369
+ set size of (item i of wList) to {cellW, cellH}
1370
+ end repeat`;
1371
+ } else if (layoutType === 'tall') {
1372
+ layoutBody = `
1373
+ set halfW to ${screenW} div 2
1374
+ if n = 1 then
1375
+ set position of (item 1 of wList) to {0, ${menuBarH}}
1376
+ set size of (item 1 of wList) to {${screenW}, ${usableH}}
1377
+ else
1378
+ set position of (item 1 of wList) to {0, ${menuBarH}}
1379
+ set size of (item 1 of wList) to {halfW, ${usableH}}
1380
+ set rightH to ${usableH} div (n - 1)
1381
+ repeat with i from 2 to n
1382
+ set y to ${menuBarH} + ((i - 2) * rightH)
1383
+ set position of (item i of wList) to {halfW, y}
1384
+ set size of (item i of wList) to {halfW, rightH}
1385
+ end repeat
1386
+ end if`;
1387
+ } else if (layoutType === 'stack') {
1388
+ layoutBody = `
1389
+ set cellH to ${usableH} div n
1390
+ repeat with i from 1 to n
1391
+ set y to ${menuBarH} + ((i - 1) * cellH)
1392
+ set position of (item i of wList) to {0, y}
1393
+ set size of (item i of wList) to {${screenW}, cellH}
1394
+ end repeat`;
1395
+ }
1396
+
1397
+ const script = `
1398
+ tell application "System Events"
1399
+ ${collectWindows}
1400
+ ${layoutBody}
1401
+ return n
1402
+ end tell`;
1403
+
1404
+ try {
1405
+ const result = execSync(`osascript -e '${script}'`, { encoding: 'utf8', timeout: 10000 }).trim();
1406
+ if (result === '0') {
1407
+ console.log('⚠️ No kitty windows found. Sessions may be in tabs — use kitty @ launch --type=os-window for separate windows.');
1408
+ } else {
1409
+ console.log(`✅ Layout '${layoutType}' applied to ${result} kitty windows.`);
1410
+ }
1411
+ } catch (e) {
1412
+ console.error(`❌ Layout failed: ${e.message}`);
1207
1413
  }
1208
1414
  return;
1209
1415
  }
@@ -1731,6 +1937,7 @@ Usage:
1731
1937
  telepty listen Listen to the event bus and print JSON to stdout
1732
1938
  telepty monitor Human-readable real-time billboard of bus events
1733
1939
  telepty update Update telepty to the latest version
1940
+ telepty layout [grid|tall|stack] Arrange kitty windows on screen (default: grid)
1734
1941
 
1735
1942
  Handoff Commands:
1736
1943
  handoff list [--status=S] List handoffs (filter: pending/claimed/executing/completed)
package/daemon.js CHANGED
@@ -785,7 +785,10 @@ app.post('/api/sessions/:id/inject', (req, res) => {
785
785
  });
786
786
  kittyOk = true;
787
787
  console.log(`[INJECT] Kitty send-text for ${id} (window ${wid})`);
788
- } catch {}
788
+ } catch {
789
+ // Invalidate cached window ID — window may have changed or been closed
790
+ session.kittyWindowId = null;
791
+ }
789
792
  }
790
793
  if (!kittyOk) {
791
794
  // Fallback: WS (works with new allow bridges that have queue flush)
@@ -795,22 +798,37 @@ app.post('/api/sessions/:id/inject', (req, res) => {
795
798
 
796
799
  if (!no_enter) {
797
800
  setTimeout(() => {
801
+ // Primary: osascript keystroke (reliable across all CLIs)
802
+ const submitStrategy = getSubmitStrategy(session.command);
803
+ const keyCombo = submitStrategy === 'osascript_cmd_enter' ? 'cmd_enter' : 'return_key';
804
+ const osascriptOk = submitViaOsascript(id, keyCombo);
805
+
806
+ if (!osascriptOk) {
807
+ // Fallback: kitty send-text → WS
808
+ console.log(`[INJECT] osascript submit failed for ${id}, trying fallback`);
809
+ if (wid && sock) {
810
+ try {
811
+ require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\r'`, {
812
+ timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
813
+ });
814
+ } catch {
815
+ writeToSession('\r');
816
+ }
817
+ } else {
818
+ writeToSession('\r');
819
+ }
820
+ }
821
+
822
+ // Update tab title regardless of submit method
798
823
  if (wid && sock) {
799
824
  try {
800
- require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\r'`, {
801
- timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
802
- });
803
825
  require('child_process').execSync(`kitty @ --to unix:${sock} set-tab-title --match id:${wid} '⚡ telepty :: ${id}'`, {
804
826
  timeout: 2000, stdio: ['pipe', 'pipe', 'pipe']
805
827
  });
806
- } catch {
807
- writeToSession('\r');
808
- }
809
- } else {
810
- writeToSession('\r');
828
+ } catch {}
811
829
  }
812
830
  }, 500);
813
- submitResult = { deferred: true, strategy: kittyOk ? 'kitty_text_key' : 'ws_split_cr' };
831
+ submitResult = { deferred: true, strategy: 'osascript_with_fallback' };
814
832
  }
815
833
  } else {
816
834
  // Spawned sessions: direct PTY write
@@ -1336,6 +1354,21 @@ wss.on('connection', (ws, req) => {
1336
1354
  const url = new URL(req.url, 'http://' + req.headers.host);
1337
1355
  const sessionId = url.pathname.split('/').pop();
1338
1356
  const session = sessions[sessionId];
1357
+ // ?owner=1 indicates the allow bridge (PTY owner), not an attach viewer
1358
+ const isOwnerConnect = url.searchParams.get('owner') === '1';
1359
+
1360
+ // Ping/pong heartbeat — detect and terminate stale TCP half-open connections (30s interval)
1361
+ let isAlive = true;
1362
+ ws.on('pong', () => { isAlive = true; });
1363
+ const pingInterval = setInterval(() => {
1364
+ if (!isAlive) {
1365
+ console.log(`[WS] Terminating stale connection (no pong) for ${sessionId}`);
1366
+ ws.terminate();
1367
+ return;
1368
+ }
1369
+ isAlive = false;
1370
+ ws.ping();
1371
+ }, 30000);
1339
1372
 
1340
1373
  if (!session) {
1341
1374
  // Auto-register wrapped session on WS connect (supports reconnect after daemon restart)
@@ -1372,10 +1405,17 @@ wss.on('connection', (ws, req) => {
1372
1405
 
1373
1406
  const activeSession = sessions[sessionId];
1374
1407
 
1375
- // For wrapped sessions, first connector becomes the owner
1376
- if (activeSession.type === 'wrapped' && !activeSession.ownerWs) {
1408
+ // For wrapped sessions, first connector OR explicit ?owner=1 claim becomes the owner.
1409
+ // ?owner=1 reclaim handles the stale-ownerWs bug: allow bridge reconnects but stale TCP
1410
+ // half-open connection still holds ownerWs slot → reconnect wrongly becomes a viewer.
1411
+ if (activeSession.type === 'wrapped' && (!activeSession.ownerWs || isOwnerConnect)) {
1412
+ if (isOwnerConnect && activeSession.ownerWs && activeSession.ownerWs !== ws) {
1413
+ // Terminate the stale owner connection before claiming ownership
1414
+ console.log(`[WS] Replacing stale ownerWs for session ${sessionId}`);
1415
+ activeSession.ownerWs.terminate();
1416
+ }
1377
1417
  activeSession.ownerWs = ws;
1378
- console.log(`[WS] Wrap owner connected for session ${sessionId} (Total: ${activeSession.clients.size})`);
1418
+ console.log(`[WS] Wrap owner ${isOwnerConnect && activeSession.clients.size > 1 ? 're-' : ''}connected for session ${sessionId} (Total: ${activeSession.clients.size})`);
1379
1419
  } else {
1380
1420
  console.log(`[WS] Client attached to session ${sessionId} (Total: ${activeSession.clients.size})`);
1381
1421
  }
@@ -1417,6 +1457,7 @@ wss.on('connection', (ws, req) => {
1417
1457
  });
1418
1458
 
1419
1459
  ws.on('close', () => {
1460
+ clearInterval(pingInterval);
1420
1461
  activeSession.clients.delete(ws);
1421
1462
  if (activeSession.type === 'wrapped' && ws === activeSession.ownerWs) {
1422
1463
  activeSession.ownerWs = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
@@ -17,6 +17,7 @@
17
17
  "license": "ISC",
18
18
  "description": "",
19
19
  "dependencies": {
20
+ "blessed": "^0.1.81",
20
21
  "cors": "^2.8.6",
21
22
  "express": "^5.2.1",
22
23
  "node-pty": "^1.2.0-beta.11",
@@ -48,6 +48,20 @@ function pickSessionTarget(sessionRef, sessions, defaultHost = '127.0.0.1') {
48
48
  const matches = sessions.filter((session) => session.id === parsed.id);
49
49
 
50
50
  if (matches.length === 0) {
51
+ // Project-based fallback: match sessions whose ID starts with the requested prefix.
52
+ // e.g., "aigentry-dustcraw" matches "aigentry-dustcraw-001" or "aigentry-dustcraw-claude".
53
+ const prefixMatches = sessions.filter((s) =>
54
+ s.id.startsWith(parsed.id + '-') || s.id.startsWith(parsed.id)
55
+ );
56
+ if (prefixMatches.length === 1) {
57
+ return { id: prefixMatches[0].id, host: prefixMatches[0].host };
58
+ }
59
+ if (prefixMatches.length > 1) {
60
+ // Multiple candidates: prefer same host, then first alphabetically
61
+ const local = prefixMatches.find((s) => s.host === '127.0.0.1');
62
+ const best = local || prefixMatches.sort((a, b) => a.id.localeCompare(b.id))[0];
63
+ return { id: best.id, host: best.host };
64
+ }
51
65
  return null;
52
66
  }
53
67
 
package/tui.js ADDED
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+
3
+ const blessed = require('blessed');
4
+ const { getConfig } = require('./auth');
5
+
6
+ const PORT = process.env.PORT || 3848;
7
+ const DAEMON_URL = `http://localhost:${PORT}`;
8
+ const POLL_INTERVAL = 2000;
9
+ const STALE_THRESHOLD = 120; // seconds idle before "stale"
10
+
11
+ class TuiDashboard {
12
+ constructor() {
13
+ const cfg = getConfig();
14
+ this.token = cfg.authToken;
15
+ this.sessions = [];
16
+ this.selectedIndex = 0;
17
+ this.pollTimer = null;
18
+ this.busWs = null;
19
+ this.busLog = [];
20
+ this.setupScreen();
21
+ this.startPolling();
22
+ this.connectBus();
23
+ }
24
+
25
+ // ── API helpers ──────────────────────────────────────────────
26
+
27
+ async apiFetch(path, options = {}) {
28
+ const headers = { 'x-telepty-token': this.token, ...options.headers };
29
+ if (options.body) headers['Content-Type'] = 'application/json';
30
+ const res = await fetch(`${DAEMON_URL}${path}`, { ...options, headers });
31
+ return res.json();
32
+ }
33
+
34
+ async fetchSessions() {
35
+ try {
36
+ this.sessions = await this.apiFetch('/api/sessions');
37
+ this.sessions.sort((a, b) => a.id.localeCompare(b.id));
38
+ if (this.selectedIndex >= this.sessions.length) {
39
+ this.selectedIndex = Math.max(0, this.sessions.length - 1);
40
+ }
41
+ this.renderSessionList();
42
+ } catch {
43
+ this.setStatus('{red-fg}Daemon unreachable{/}');
44
+ }
45
+ }
46
+
47
+ async injectToSession(id, prompt) {
48
+ try {
49
+ const res = await this.apiFetch(`/api/sessions/${encodeURIComponent(id)}/inject`, {
50
+ method: 'POST',
51
+ body: JSON.stringify({ prompt })
52
+ });
53
+ if (res.success) {
54
+ this.setStatus(`{green-fg}Injected to ${id}{/}`);
55
+ } else {
56
+ this.setStatus(`{red-fg}Inject failed: ${res.error || 'unknown'}{/}`);
57
+ }
58
+ } catch (e) {
59
+ this.setStatus(`{red-fg}Inject error: ${e.message}{/}`);
60
+ }
61
+ }
62
+
63
+ async broadcastMessage(prompt) {
64
+ try {
65
+ const res = await this.apiFetch('/api/sessions/broadcast/inject', {
66
+ method: 'POST',
67
+ body: JSON.stringify({ prompt })
68
+ });
69
+ const ok = res.results?.successful?.length || 0;
70
+ this.setStatus(`{green-fg}Broadcast to ${ok} sessions{/}`);
71
+ } catch (e) {
72
+ this.setStatus(`{red-fg}Broadcast error: ${e.message}{/}`);
73
+ }
74
+ }
75
+
76
+ // ── Event Bus ────────────────────────────────────────────────
77
+
78
+ connectBus() {
79
+ try {
80
+ const WebSocket = require('ws');
81
+ this.busWs = new WebSocket(
82
+ `ws://localhost:${PORT}/api/bus?token=${encodeURIComponent(this.token)}`
83
+ );
84
+ this.busWs.on('message', (data) => {
85
+ try {
86
+ const msg = JSON.parse(data);
87
+ const ts = new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
88
+ let line = `${ts} `;
89
+ if (msg.type === 'inject_written') {
90
+ line += `{cyan-fg}inject{/} -> ${msg.target || '?'}`;
91
+ } else if (msg.type === 'injection') {
92
+ line += `{cyan-fg}broadcast{/} -> ${msg.target_agent || 'all'}`;
93
+ } else if (msg.type === 'message_routed') {
94
+ line += `{yellow-fg}${msg.from || '?'}{/} -> {green-fg}${msg.to || '?'}{/}`;
95
+ } else {
96
+ line += `{white-fg}${msg.type || 'event'}{/}`;
97
+ }
98
+ this.busLog.push(line);
99
+ if (this.busLog.length > 100) this.busLog.shift();
100
+ this.renderBusLog();
101
+ } catch { /* ignore malformed */ }
102
+ });
103
+ this.busWs.on('close', () => {
104
+ setTimeout(() => this.connectBus(), 3000);
105
+ });
106
+ this.busWs.on('error', () => {});
107
+ } catch { /* ws not available */ }
108
+ }
109
+
110
+ // ── Screen setup ─────────────────────────────────────────────
111
+
112
+ setupScreen() {
113
+ this.screen = blessed.screen({
114
+ smartCSR: true,
115
+ title: 'telepty dashboard',
116
+ fullUnicode: true
117
+ });
118
+
119
+ // Header
120
+ this.header = blessed.box({
121
+ parent: this.screen,
122
+ top: 0, left: 0, width: '100%', height: 1,
123
+ tags: true,
124
+ style: { fg: 'white', bg: 'blue' },
125
+ content: ' {bold}telepty dashboard{/bold}'
126
+ });
127
+
128
+ // Session list (left panel)
129
+ this.sessionList = blessed.list({
130
+ parent: this.screen,
131
+ top: 1, left: 0, width: '60%', bottom: 3,
132
+ border: { type: 'line' },
133
+ label: ' Sessions ',
134
+ tags: true,
135
+ keys: true,
136
+ vi: true,
137
+ mouse: true,
138
+ scrollbar: { ch: '│', style: { fg: 'cyan' } },
139
+ style: {
140
+ border: { fg: 'cyan' },
141
+ selected: { bg: 'blue', fg: 'white', bold: true },
142
+ item: { fg: 'white' }
143
+ }
144
+ });
145
+
146
+ this.sessionList.on('select item', (item, index) => {
147
+ this.selectedIndex = index;
148
+ });
149
+
150
+ // Event bus log (right panel)
151
+ this.busPanel = blessed.log({
152
+ parent: this.screen,
153
+ top: 1, left: '60%', right: 0, bottom: 3,
154
+ border: { type: 'line' },
155
+ label: ' Event Bus ',
156
+ tags: true,
157
+ scrollbar: { ch: '│', style: { fg: 'yellow' } },
158
+ style: {
159
+ border: { fg: 'yellow' }
160
+ }
161
+ });
162
+
163
+ // Shortcut bar
164
+ this.shortcutBar = blessed.box({
165
+ parent: this.screen,
166
+ bottom: 1, left: 0, width: '100%', height: 1,
167
+ tags: true,
168
+ style: { fg: 'white', bg: 'gray' },
169
+ content: ' {bold}i{/}:Inject {bold}b{/}:Broadcast {bold}r{/}:Refresh {bold}q{/}:Quit'
170
+ });
171
+
172
+ // Status bar
173
+ this.statusBar = blessed.box({
174
+ parent: this.screen,
175
+ bottom: 0, left: 0, width: '100%', height: 1,
176
+ tags: true,
177
+ style: { fg: 'white', bg: 'black' },
178
+ content: ' Ready'
179
+ });
180
+
181
+ // ── Keyboard handlers ──────────────────────────────────────
182
+
183
+ this.screen.key(['q', 'C-c'], () => {
184
+ this.cleanup();
185
+ process.exit(0);
186
+ });
187
+
188
+ this.screen.key(['i'], () => {
189
+ const session = this.sessions[this.selectedIndex];
190
+ if (!session) return this.setStatus('{red-fg}No session selected{/}');
191
+ this.promptInput(`Inject to ${session.id}:`, (text) => {
192
+ if (text) this.injectToSession(session.id, text);
193
+ });
194
+ });
195
+
196
+ this.screen.key(['b'], () => {
197
+ this.promptInput('Broadcast to all:', (text) => {
198
+ if (text) this.broadcastMessage(text);
199
+ });
200
+ });
201
+
202
+ this.screen.key(['r'], () => {
203
+ this.fetchSessions();
204
+ this.setStatus('{green-fg}Refreshed{/}');
205
+ });
206
+
207
+ this.sessionList.focus();
208
+ this.screen.render();
209
+ }
210
+
211
+ // ── Input prompt ─────────────────────────────────────────────
212
+
213
+ promptInput(label, callback) {
214
+ const inputBox = blessed.textbox({
215
+ parent: this.screen,
216
+ bottom: 0, left: 0, width: '100%', height: 3,
217
+ border: { type: 'line' },
218
+ label: ` ${label} `,
219
+ tags: true,
220
+ inputOnFocus: true,
221
+ style: {
222
+ border: { fg: 'green' },
223
+ fg: 'white'
224
+ }
225
+ });
226
+
227
+ inputBox.focus();
228
+ inputBox.readInput((err, value) => {
229
+ inputBox.destroy();
230
+ this.sessionList.focus();
231
+ this.screen.render();
232
+ if (!err && value) callback(value);
233
+ });
234
+ this.screen.render();
235
+ }
236
+
237
+ // ── Rendering ────────────────────────────────────────────────
238
+
239
+ getStatusInfo(session) {
240
+ const idle = session.idleSeconds;
241
+ const clients = session.active_clients || 0;
242
+
243
+ if (clients === 0) return { icon: '{red-fg}✕{/}', label: '{red-fg}dead{/}' };
244
+ if (idle !== null && idle > STALE_THRESHOLD) return { icon: '{yellow-fg}○{/}', label: '{yellow-fg}stale{/}' };
245
+ if (idle !== null && idle < 10) return { icon: '{green-fg}●{/}', label: '{green-fg}busy{/}' };
246
+ return { icon: '{green-fg}●{/}', label: '{white-fg}idle{/}' };
247
+ }
248
+
249
+ renderSessionList() {
250
+ const items = this.sessions.map((s) => {
251
+ const { icon, label } = this.getStatusInfo(s);
252
+ const shortId = s.id.replace(/^aigentry-/, '').replace(/-claude$/, '');
253
+ return ` ${icon} ${shortId.padEnd(24)} ${label} {gray-fg}C:${s.active_clients}{/}`;
254
+ });
255
+
256
+ this.sessionList.setItems(items);
257
+ this.sessionList.setLabel(` Sessions (${this.sessions.length}) `);
258
+ if (this.selectedIndex < items.length) {
259
+ this.sessionList.select(this.selectedIndex);
260
+ }
261
+ this.screen.render();
262
+ }
263
+
264
+ renderBusLog() {
265
+ // Show last N lines that fit
266
+ const height = this.busPanel.height - 2;
267
+ const visible = this.busLog.slice(-height);
268
+ this.busPanel.setContent(visible.join('\n'));
269
+ this.screen.render();
270
+ }
271
+
272
+ setStatus(msg) {
273
+ this.statusBar.setContent(` ${msg}`);
274
+ this.screen.render();
275
+ }
276
+
277
+ // ── Lifecycle ────────────────────────────────────────────────
278
+
279
+ startPolling() {
280
+ this.fetchSessions();
281
+ this.pollTimer = setInterval(() => this.fetchSessions(), POLL_INTERVAL);
282
+ }
283
+
284
+ cleanup() {
285
+ if (this.pollTimer) clearInterval(this.pollTimer);
286
+ if (this.busWs) this.busWs.close();
287
+ this.screen.destroy();
288
+ }
289
+ }
290
+
291
+ // ── Entry point ──────────────────────────────────────────────────
292
+
293
+ function main() {
294
+ new TuiDashboard();
295
+ }
296
+
297
+ module.exports = { TuiDashboard };
298
+
299
+ if (require.main === module) {
300
+ main();
301
+ }
package/URGENT_ISSUES.md DELETED
@@ -1,31 +0,0 @@
1
- # Telepty 긴급 수정 사항 3건
2
-
3
- ## 1. 데몬 재시작 시 전체 세션 끊김 → 자동 재접속
4
-
5
- **현상**: 데몬이 재시작되면 모든 allow 브릿지 세션이 끊기고 수동으로 다시 연결해야 함.
6
-
7
- **필요한 수정**:
8
- - allow 브릿지가 데몬 재시작/연결 끊김을 감지
9
- - 자동 재접속 retry loop (exponential backoff: 1s → 2s → 4s → max 30s)
10
- - 세션 메타데이터(ID, command, CWD)가 재접속 시 복원되어야 함
11
-
12
- ## 2. inject 시 엔터 미전송
13
-
14
- **현상**: inject된 메시지가 프롬프트에 표시되지만 엔터가 자동으로 안 눌림. 사용자가 수동으로 엔터를 눌러야 실행됨.
15
-
16
- **0.1.21에서 수정했다고 했으나 여전히 발생 중.**
17
-
18
- **필요한 수정**:
19
- - 모든 CLI(Claude, Codex, Gemini)에서 inject 후 엔터 자동 전송 보장
20
- - HTTP API inject (POST /api/sessions/:id/inject)도 동일하게 동작해야 함
21
- - deferred/split_cr 전략이 실제로 엔터를 전송하는지 검증
22
-
23
- ## 3. 통신 대상 세션 소실 시 자동 재검색
24
-
25
- **현상**: inject 대상 세션이 사라지면 에러 후 종료. 통신 단절.
26
-
27
- **필요한 수정**:
28
- - 세션이 사라지면 같은 프로젝트(CWD)에서 새로 생성된 세션을 자동 검색
29
- - 예: `aigentry-dustcraw-001`이 사라지고 `aigentry-dustcraw-002`가 생기면 자동 라우팅
30
- - project-based routing: CWD 또는 session name prefix로 매칭
31
- - `telepty inject --project aigentry-dustcraw "메시지"` 같은 project alias 지원 검토