@dmsdc-ai/aigentry-telepty 0.1.68 → 0.1.70

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/daemon.js CHANGED
@@ -8,6 +8,7 @@ const { getConfig } = require('./auth');
8
8
  const pkg = require('./package.json');
9
9
  const { claimDaemonState, clearDaemonState } = require('./daemon-control');
10
10
  const { checkEntitlement } = require('./entitlement');
11
+ const terminalBackend = require('./terminal-backend');
11
12
 
12
13
  const config = getConfig();
13
14
  const EXPECTED_TOKEN = config.authToken;
@@ -19,7 +20,7 @@ function persistSessions() {
19
20
  try {
20
21
  const data = {};
21
22
  for (const [id, s] of Object.entries(sessions)) {
22
- data[id] = { id, type: s.type, command: s.command, cwd: s.cwd, backend: s.backend || null, cmuxWorkspaceId: s.cmuxWorkspaceId || null, createdAt: s.createdAt, lastActivityAt: s.lastActivityAt || null };
23
+ data[id] = { id, type: s.type, command: s.command, cwd: s.cwd, backend: s.backend || null, cmuxWorkspaceId: s.cmuxWorkspaceId || null, cmuxSurfaceId: s.cmuxSurfaceId || null, createdAt: s.createdAt, lastActivityAt: s.lastActivityAt || null };
23
24
  }
24
25
  fs.mkdirSync(require('path').dirname(SESSION_PERSIST_PATH), { recursive: true });
25
26
  fs.writeFileSync(SESSION_PERSIST_PATH, JSON.stringify(data, null, 2));
@@ -138,6 +139,21 @@ const sessions = {};
138
139
  const handoffs = {};
139
140
  const threads = {};
140
141
 
142
+ function appendToOutputRing(session, data) {
143
+ if (!session.outputRing) session.outputRing = [];
144
+ session.outputRing.push(data);
145
+ // Keep total data under ~200KB limit by trimming old entries
146
+ let totalLen = session.outputRing.reduce((sum, d) => sum + d.length, 0);
147
+ while (totalLen > 200000 && session.outputRing.length > 1) {
148
+ totalLen -= session.outputRing[0].length;
149
+ session.outputRing.shift();
150
+ }
151
+ }
152
+
153
+ // Detect terminal environment at daemon startup
154
+ const DETECTED_TERMINAL = terminalBackend.detectTerminal();
155
+ console.log(`[DAEMON] Terminal backend: ${DETECTED_TERMINAL}`);
156
+
141
157
  // Restore persisted session metadata (wrapped sessions await reconnect)
142
158
  const _persisted = loadPersistedSessions();
143
159
  for (const [id, meta] of Object.entries(_persisted)) {
@@ -147,8 +163,7 @@ for (const [id, meta] of Object.entries(_persisted)) {
147
163
  command: meta.command || 'wrapped', cwd: meta.cwd || process.cwd(),
148
164
  createdAt: meta.createdAt || new Date().toISOString(),
149
165
  lastActivityAt: meta.lastActivityAt || new Date().toISOString(),
150
- clients: new Set(), isClosing: false
151
- };
166
+ clients: new Set(), isClosing: false, outputRing: [], ready: false, };
152
167
  console.log(`[PERSIST] Restored session ${id} (awaiting reconnect)`);
153
168
  }
154
169
  }
@@ -256,8 +271,10 @@ app.post('/api/sessions/spawn', (req, res) => {
256
271
  createdAt: new Date().toISOString(),
257
272
  lastActivityAt: new Date().toISOString(),
258
273
  clients: new Set(),
259
- isClosing: false
260
- };
274
+ isClosing: false,
275
+ outputRing: [],
276
+ ready: true,
277
+ };
261
278
  sessions[session_id] = sessionRecord;
262
279
 
263
280
  // Broadcast session creation to bus
@@ -279,6 +296,8 @@ app.post('/api/sessions/spawn', (req, res) => {
279
296
  return;
280
297
  }
281
298
 
299
+ appendToOutputRing(currentSession, data);
300
+
282
301
  // Send to direct WS clients
283
302
  currentSession.clients.forEach(ws => {
284
303
  if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'output', data }));
@@ -304,7 +323,7 @@ app.post('/api/sessions/spawn', (req, res) => {
304
323
  });
305
324
 
306
325
  app.post('/api/sessions/register', (req, res) => {
307
- const { session_id, command, cwd = process.cwd(), backend, cmux_workspace_id } = req.body;
326
+ const { session_id, command, cwd = process.cwd(), backend, cmux_workspace_id, cmux_surface_id } = req.body;
308
327
  if (!session_id) return res.status(400).json({ error: 'session_id is required' });
309
328
  // Entitlement: check session limit for new registrations
310
329
  if (!sessions[session_id]) {
@@ -322,6 +341,7 @@ app.post('/api/sessions/register', (req, res) => {
322
341
  if (cwd) existing.cwd = cwd;
323
342
  if (backend) existing.backend = backend;
324
343
  if (cmux_workspace_id) existing.cmuxWorkspaceId = cmux_workspace_id;
344
+ if (cmux_surface_id) existing.cmuxSurfaceId = cmux_surface_id;
325
345
  console.log(`[REGISTER] Re-registered session ${session_id} (updated metadata)`);
326
346
  return res.status(200).json({ session_id, type: 'wrapped', command: existing.command, cwd: existing.cwd, reregistered: true });
327
347
  }
@@ -335,11 +355,14 @@ app.post('/api/sessions/register', (req, res) => {
335
355
  cwd,
336
356
  backend: backend || 'kitty',
337
357
  cmuxWorkspaceId: cmux_workspace_id || null,
358
+ cmuxSurfaceId: cmux_surface_id || null,
338
359
  createdAt: new Date().toISOString(),
339
360
  lastActivityAt: new Date().toISOString(),
340
361
  clients: new Set(),
341
- isClosing: false
342
- };
362
+ isClosing: false,
363
+ outputRing: [],
364
+ ready: false,
365
+ };
343
366
  // Check for existing session with same base alias and emit replaced event
344
367
  const baseAlias = session_id.replace(/-\d+$/, '');
345
368
  const replaced = Object.keys(sessions).find(id => {
@@ -393,10 +416,12 @@ app.get('/api/sessions', (req, res) => {
393
416
  cwd: session.cwd,
394
417
  backend: session.backend || 'kitty',
395
418
  cmuxWorkspaceId: session.cmuxWorkspaceId || null,
419
+ cmuxSurfaceId: session.cmuxSurfaceId || null,
396
420
  createdAt: session.createdAt,
397
421
  lastActivityAt: session.lastActivityAt || null,
398
422
  idleSeconds,
399
- active_clients: session.clients.size
423
+ active_clients: session.clients.size,
424
+ ready: session.ready || false
400
425
  };
401
426
  });
402
427
  if (idleGt !== null) {
@@ -436,6 +461,7 @@ app.get('/api/meta', (req, res) => {
436
461
  host: HOST,
437
462
  port: Number(PORT),
438
463
  machine_id: MACHINE_ID,
464
+ terminal: DETECTED_TERMINAL,
439
465
  capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon', 'handoff-inbox', 'deliberation-threads', 'cross-machine']
440
466
  });
441
467
  });
@@ -464,6 +490,27 @@ app.post('/api/sessions/multicast/inject', (req, res) => {
464
490
  const session = sessions[id];
465
491
  if (session) {
466
492
  try {
493
+ // cmux per-session backend (text + enter)
494
+ if (session.backend === 'cmux') {
495
+ const ok = terminalBackend.cmuxSendText(id, prompt);
496
+ if (ok) {
497
+ setTimeout(() => terminalBackend.cmuxSendEnter(id), 300);
498
+ results.successful.push({ id, strategy: 'cmux_auto' });
499
+ // Broadcast injection to bus
500
+ const busMsg = JSON.stringify({
501
+ type: 'injection',
502
+ sender: 'cli',
503
+ target_agent: id,
504
+ content: prompt,
505
+ timestamp: new Date().toISOString()
506
+ });
507
+ busClients.forEach(client => {
508
+ if (client.readyState === 1) client.send(busMsg);
509
+ });
510
+ return; // skip WS path for this session
511
+ }
512
+ }
513
+
467
514
  // Inject text first, then \r separately after delay
468
515
  if (session.type === 'wrapped') {
469
516
  if (session.ownerWs && session.ownerWs.readyState === 1) {
@@ -516,6 +563,16 @@ app.post('/api/sessions/broadcast/inject', (req, res) => {
516
563
  Object.keys(sessions).forEach(id => {
517
564
  const session = sessions[id];
518
565
  try {
566
+ // cmux per-session backend (text + enter)
567
+ if (session.backend === 'cmux') {
568
+ const ok = terminalBackend.cmuxSendText(id, prompt);
569
+ if (ok) {
570
+ setTimeout(() => terminalBackend.cmuxSendEnter(id), 300);
571
+ results.successful.push({ id, strategy: 'cmux_auto' });
572
+ return; // skip WS path for this session
573
+ }
574
+ }
575
+
519
576
  // Inject text first, then \r separately after delay
520
577
  if (session.type === 'wrapped') {
521
578
  if (session.ownerWs && session.ownerWs.readyState === 1) {
@@ -729,8 +786,11 @@ app.post('/api/sessions/:id/submit', (req, res) => {
729
786
  console.log(`[SUBMIT] Session ${id} (${session.command}) using strategy: ${strategy}`);
730
787
 
731
788
  let success = false;
732
- // cmux backend takes priority
733
- if (session.backend === 'cmux' && session.cmuxWorkspaceId) {
789
+ // cmux per-session backend
790
+ if (session.backend === 'cmux') {
791
+ success = terminalBackend.cmuxSendEnter(id);
792
+ }
793
+ if (!success && session.backend === 'cmux' && session.cmuxWorkspaceId) {
734
794
  success = submitViaCmux(id);
735
795
  }
736
796
  if (!success) {
@@ -768,8 +828,11 @@ app.post('/api/sessions/submit-all', (req, res) => {
768
828
  const strategy = getSubmitStrategy(session.command);
769
829
  let success = false;
770
830
 
771
- // cmux backend takes priority
772
- if (session.backend === 'cmux' && session.cmuxWorkspaceId) {
831
+ // cmux per-session backend
832
+ if (session.backend === 'cmux') {
833
+ success = terminalBackend.cmuxSendEnter(id);
834
+ }
835
+ if (!success && session.backend === 'cmux' && session.cmuxWorkspaceId) {
773
836
  success = submitViaCmux(id);
774
837
  }
775
838
  if (!success) {
@@ -835,68 +898,115 @@ app.post('/api/sessions/:id/inject', (req, res) => {
835
898
 
836
899
  let submitResult = null;
837
900
  if (session.type === 'wrapped') {
838
- // For wrapped sessions: try kitty send-text (bypasses allow bridge queue)
839
- // then WS as fallback, then kitty send-key Return for enter
840
- const sock = findKittySocket();
841
- if (!session.kittyWindowId && sock) session.kittyWindowId = findKittyWindowId(sock, id);
842
- const wid = session.kittyWindowId;
901
+ // For wrapped sessions: try cmux send (daemon-level auto-detect),
902
+ // then kitty send-text (bypasses allow bridge queue),
903
+ // then WS as fallback, then submit via consistent path for CR.
904
+ //
905
+ // When session is NOT ready (CLI hasn't shown prompt yet), skip cmux/kitty
906
+ // because they bypass the allow-bridge's prompt-ready queue.
907
+ // The WS path sends to the allow-bridge which queues until CLI is ready.
908
+ const sock = session.ready ? findKittySocket() : null;
909
+ if (sock && !session.kittyWindowId) session.kittyWindowId = findKittyWindowId(sock, id);
910
+ const wid = session.ready ? session.kittyWindowId : null;
843
911
 
844
912
  let kittyOk = false;
845
- if (wid && sock) {
846
- // Kitty send-text primary (bypasses allow bridge queue)
913
+ let cmuxOk = false;
914
+ let deliveryPath = null; // 'cmux', 'kitty', 'ws'
915
+
916
+ // cmux per-session backend: send text directly to surface (only when ready)
917
+ if (session.ready && session.backend === 'cmux') {
918
+ cmuxOk = terminalBackend.cmuxSendText(id, finalPrompt);
919
+ if (cmuxOk) {
920
+ deliveryPath = 'cmux';
921
+ console.log(`[INJECT] cmux send for ${id}`);
922
+ }
923
+ }
924
+
925
+ if (!cmuxOk && wid && sock) {
926
+ // Kitty send-text primary (only when ready — bypasses allow bridge queue)
847
927
  try {
848
928
  const escaped = finalPrompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
849
929
  require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} '${escaped}'`, {
850
930
  timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
851
931
  });
852
932
  kittyOk = true;
933
+ deliveryPath = 'kitty';
853
934
  console.log(`[INJECT] Kitty send-text for ${id} (window ${wid})`);
854
935
  } catch {
855
936
  // Invalidate cached window ID — window may have changed or been closed
856
937
  session.kittyWindowId = null;
857
938
  }
858
939
  }
859
- if (!kittyOk) {
860
- // Fallback: WS (works with new allow bridges that have queue flush)
940
+ if (!cmuxOk && !kittyOk) {
941
+ // WS path: allow-bridge has its own prompt-ready queue
861
942
  const wsOk = writeToSession(finalPrompt);
862
943
  if (!wsOk) {
863
944
  return res.status(503).json({ error: 'Process not connected' });
864
945
  }
865
- console.log(`[INJECT] WS fallback for ${id}`);
946
+ deliveryPath = 'ws';
947
+ if (!session.ready) {
948
+ console.log(`[INJECT] WS (not ready, allow-bridge will queue) for ${id}`);
949
+ } else {
950
+ console.log(`[INJECT] WS fallback for ${id}`);
951
+ }
866
952
  }
867
953
 
868
954
  if (!no_enter) {
869
955
  setTimeout(() => {
870
956
  let submitted = false;
871
957
 
872
- // 1. cmux backend: use cmux send-key
873
- if (session.backend === 'cmux' && session.cmuxWorkspaceId) {
874
- submitted = submitViaCmux(id);
875
- if (submitted) console.log(`[INJECT] cmux submit for ${id}`);
876
- }
877
-
878
- // 2. kitty/default backend: osascript primary
879
- if (!submitted) {
880
- const submitStrategy = getSubmitStrategy(session.command);
881
- const keyCombo = submitStrategy === 'osascript_cmd_enter' ? 'cmd_enter' : 'return_key';
882
- submitted = submitViaOsascript(id, keyCombo);
883
- }
884
-
885
- // 3. Fallback: kitty send-text → WS
886
- if (!submitted) {
887
- console.log(`[INJECT] submit fallback for ${id}`);
958
+ // Use the SAME path that delivered text for CR to guarantee ordering
959
+ if (deliveryPath === 'cmux') {
960
+ // cmux: send-key return via same surface
961
+ if (session.backend === 'cmux') {
962
+ submitted = terminalBackend.cmuxSendEnter(id);
963
+ if (submitted) console.log(`[INJECT] cmux submit for ${id}`);
964
+ }
965
+ if (!submitted && session.backend === 'cmux' && session.cmuxWorkspaceId) {
966
+ submitted = submitViaCmux(id);
967
+ if (submitted) console.log(`[INJECT] cmux session-level submit for ${id}`);
968
+ }
969
+ } else if (deliveryPath === 'kitty') {
970
+ // kitty: send-text CR via same window (not osascript!)
888
971
  if (wid && sock) {
889
972
  try {
890
973
  require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\r'`, {
891
974
  timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
892
975
  });
976
+ submitted = true;
977
+ console.log(`[INJECT] kitty submit for ${id} (window ${wid})`);
893
978
  } catch {
894
- writeToSession('\r');
979
+ session.kittyWindowId = null;
895
980
  }
896
- } else {
897
- writeToSession('\r');
898
981
  }
899
982
  }
983
+ // deliveryPath === 'ws' or any fallback:
984
+ // Try terminal-level submit first (bypasses PTY ICRNL which converts CR→LF)
985
+ // This matters for cmux/kitty sessions where text went via WS but
986
+ // the application expects CR(13) not LF(10) from Enter.
987
+ if (!submitted && session.backend === 'cmux') {
988
+ submitted = terminalBackend.cmuxSendEnter(id);
989
+ if (submitted) console.log(`[INJECT] cmux submit (fallback) for ${id}`);
990
+ }
991
+ if (!submitted && session.backend === 'cmux' && session.cmuxWorkspaceId) {
992
+ submitted = submitViaCmux(id);
993
+ if (submitted) console.log(`[INJECT] cmux session-level submit (fallback) for ${id}`);
994
+ }
995
+ if (!submitted && wid && sock) {
996
+ try {
997
+ require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\r'`, {
998
+ timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
999
+ });
1000
+ submitted = true;
1001
+ console.log(`[INJECT] kitty submit (fallback) for ${id}`);
1002
+ } catch {
1003
+ session.kittyWindowId = null;
1004
+ }
1005
+ }
1006
+ if (!submitted) {
1007
+ writeToSession('\r');
1008
+ console.log(`[INJECT] WS submit for ${id}`);
1009
+ }
900
1010
 
901
1011
  // Update tab title (kitty-specific, safe to fail)
902
1012
  if (wid && sock) {
@@ -907,7 +1017,7 @@ app.post('/api/sessions/:id/inject', (req, res) => {
907
1017
  } catch {}
908
1018
  }
909
1019
  }, 500);
910
- submitResult = { deferred: true, strategy: session.backend === 'cmux' ? 'cmux_with_fallback' : 'osascript_with_fallback' };
1020
+ submitResult = { deferred: true, strategy: deliveryPath || 'ws' };
911
1021
  }
912
1022
  } else {
913
1023
  // Spawned sessions: direct PTY write
@@ -979,6 +1089,53 @@ app.post('/api/sessions/:id/inject', (req, res) => {
979
1089
  }
980
1090
  });
981
1091
 
1092
+ // GET /api/sessions/:id/screen — read current screen buffer
1093
+ app.get('/api/sessions/:id/screen', (req, res) => {
1094
+ const requestedId = req.params.id;
1095
+ const resolvedId = resolveSessionAlias(requestedId);
1096
+ if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
1097
+ const session = sessions[resolvedId];
1098
+
1099
+ const lines = parseInt(req.query.lines) || 50;
1100
+ const raw = req.query.raw === '1' || req.query.raw === 'true';
1101
+
1102
+ if (!session.outputRing || session.outputRing.length === 0) {
1103
+ return res.json({ session_id: resolvedId, screen: '', lines: 0, raw: false });
1104
+ }
1105
+
1106
+ // Join all buffered output
1107
+ const fullOutput = session.outputRing.join('');
1108
+
1109
+ // Strip ANSI escape sequences for clean text
1110
+ function stripAnsi(str) {
1111
+ return str
1112
+ .replace(/[[0-9;]*[a-zA-Z]/g, '') // CSI sequences
1113
+ .replace(/][^]*/g, '') // OSC sequences (BEL terminated)
1114
+ .replace(/][^]*\\/g, '') // OSC sequences (ST terminated)
1115
+ .replace(/[()][AB012]/g, '') // Character set selection
1116
+ .replace(/[>=<]/g, '') // Keypad mode
1117
+ .replace(/[[?]?[0-9;]*[hlsurm]/g, '') // Mode set/reset
1118
+ .replace(/[[0-9;]*[ABCDHJ]/g, '') // Cursor movement
1119
+ .replace(/[[0-9;]*[KG]/g, '') // Line clearing
1120
+ .replace(/\r/g, ''); // Carriage returns
1121
+ }
1122
+
1123
+ const cleaned = raw ? fullOutput : stripAnsi(fullOutput);
1124
+
1125
+ // Take last N lines
1126
+ const allLines = cleaned.split('\n');
1127
+ const lastLines = allLines.slice(-lines);
1128
+ const screen = lastLines.join('\n').trim();
1129
+
1130
+ res.json({
1131
+ session_id: resolvedId,
1132
+ screen,
1133
+ lines: lastLines.length,
1134
+ total_lines: allLines.length,
1135
+ raw: !!raw
1136
+ });
1137
+ });
1138
+
982
1139
  app.patch('/api/sessions/:id', (req, res) => {
983
1140
  const requestedId = req.params.id;
984
1141
  const resolvedId = resolveSessionAlias(requestedId);
@@ -1060,13 +1217,22 @@ function busAutoRoute(msg) {
1060
1217
  const prompt = (msg.payload && msg.payload.prompt) || msg.content || msg.prompt || JSON.stringify(msg);
1061
1218
  const inject_id = crypto.randomUUID();
1062
1219
 
1063
- // Write to session (kitty primary, WS fallback)
1220
+ // Write to session (cmux auto-detect > kitty > session-level cmux > WS fallback)
1064
1221
  const sock = findKittySocket();
1065
1222
  if (!targetSession.kittyWindowId && sock) targetSession.kittyWindowId = findKittyWindowId(sock, targetId);
1066
1223
  const wid = targetSession.kittyWindowId;
1067
1224
  let delivered = false;
1068
1225
 
1069
- if (wid && sock && targetSession.type === 'wrapped') {
1226
+ // cmux per-session backend: send text + enter to surface
1227
+ if (!delivered && targetSession.backend === 'cmux') {
1228
+ const textOk = terminalBackend.cmuxSendText(targetId, prompt);
1229
+ if (textOk) {
1230
+ setTimeout(() => terminalBackend.cmuxSendEnter(targetId), 500);
1231
+ delivered = true;
1232
+ }
1233
+ }
1234
+
1235
+ if (!delivered && wid && sock && targetSession.type === 'wrapped') {
1070
1236
  try {
1071
1237
  const escaped = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
1072
1238
  require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} '${escaped}'`, {
@@ -1082,7 +1248,7 @@ function busAutoRoute(msg) {
1082
1248
  delivered = true;
1083
1249
  } catch {}
1084
1250
  }
1085
- // cmux backend: use WS for text, cmux send-key for enter
1251
+ // Session-level cmux backend: use WS for text, cmux send-key for enter
1086
1252
  if (!delivered && targetSession.backend === 'cmux' && targetSession.cmuxWorkspaceId) {
1087
1253
  if (targetSession.type === 'wrapped' && targetSession.ownerWs && targetSession.ownerWs.readyState === 1) {
1088
1254
  targetSession.ownerWs.send(JSON.stringify({ type: 'inject', data: prompt }));
@@ -1469,8 +1635,10 @@ wss.on('connection', (ws, req) => {
1469
1635
  createdAt: new Date().toISOString(),
1470
1636
  lastActivityAt: new Date().toISOString(),
1471
1637
  clients: new Set([ws]),
1472
- isClosing: false
1473
- };
1638
+ isClosing: false,
1639
+ outputRing: [],
1640
+ ready: false,
1641
+ };
1474
1642
  sessions[sessionId] = autoSession;
1475
1643
  console.log(`[WS] Auto-registered wrapped session ${sessionId} on reconnect`);
1476
1644
  // Set tab title via kitty (no \x0c redraw — it causes flickering on multi-session reconnect)
@@ -1515,11 +1683,25 @@ wss.on('connection', (ws, req) => {
1515
1683
  // Owner sending output -> broadcast to other clients + update activity
1516
1684
  if (type === 'output') {
1517
1685
  activeSession.lastActivityAt = new Date().toISOString();
1686
+ appendToOutputRing(activeSession, data);
1518
1687
  activeSession.clients.forEach(client => {
1519
1688
  if (client !== ws && client.readyState === 1) {
1520
1689
  client.send(JSON.stringify({ type: 'output', data }));
1521
1690
  }
1522
1691
  });
1692
+ } else if (type === 'ready') {
1693
+ activeSession.ready = true;
1694
+ activeSession.lastActivityAt = new Date().toISOString();
1695
+ console.log(`[READY] Session ${sessionId} CLI is ready for inject`);
1696
+ // Broadcast readiness to bus (cmux/kitty paths now enabled for this session)
1697
+ const readyMsg = JSON.stringify({
1698
+ type: 'session_ready',
1699
+ session_id: sessionId,
1700
+ timestamp: new Date().toISOString()
1701
+ });
1702
+ busClients.forEach(client => {
1703
+ if (client.readyState === 1) client.send(readyMsg);
1704
+ });
1523
1705
  }
1524
1706
  } else {
1525
1707
  // Non-owner client input -> forward to owner as inject
@@ -1,5 +1,25 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
4
+
5
+ const TERMINAL_CLEANUP_SEQUENCE = [
6
+ '\x1b[?1049l', // Leave the alternate screen if the child died before cleanup.
7
+ '\x1b[?25h', // Ensure the cursor is visible again.
8
+ '\x1b[?1l', // Disable application cursor keys.
9
+ '\x1b>', // Disable application keypad mode.
10
+ '\x1b[?1000l',
11
+ '\x1b[?1002l',
12
+ '\x1b[?1003l',
13
+ '\x1b[?1004l',
14
+ '\x1b[?1005l',
15
+ '\x1b[?1006l',
16
+ '\x1b[?1007l',
17
+ '\x1b[?1015l',
18
+ '\x1b[<u', // Disable kitty keyboard protocol.
19
+ '\x1b[>4;0m', // Disable modifyOtherKeys.
20
+ '\x1b[?2004l' // Disable bracketed paste.
21
+ ].join('');
22
+
3
23
  function getTerminalSize(output, fallback = {}) {
4
24
  const envCols = Number.parseInt(process.env.COLUMNS || '', 10);
5
25
  const envRows = Number.parseInt(process.env.LINES || '', 10);
@@ -31,10 +51,30 @@ function removeListener(stream, eventName, handler) {
31
51
  }
32
52
  }
33
53
 
54
+ function restoreTerminalModes(output) {
55
+ if (!output) {
56
+ return;
57
+ }
58
+
59
+ try {
60
+ if (typeof output.fd === 'number') {
61
+ fs.writeSync(output.fd, TERMINAL_CLEANUP_SEQUENCE);
62
+ return;
63
+ }
64
+
65
+ if (typeof output.write === 'function') {
66
+ output.write(TERMINAL_CLEANUP_SEQUENCE);
67
+ }
68
+ } catch {
69
+ // Ignore cleanup failures when the TTY is already gone.
70
+ }
71
+ }
72
+
34
73
  function attachInteractiveTerminal(input, output, handlers = {}) {
35
74
  const { onData = null, onResize = null } = handlers;
36
75
 
37
76
  if (input && input.isTTY && typeof input.setRawMode === 'function') {
77
+ input.__teleptyRawModeActive = true;
38
78
  input.setRawMode(true);
39
79
  }
40
80
 
@@ -57,15 +97,20 @@ function attachInteractiveTerminal(input, output, handlers = {}) {
57
97
 
58
98
  if (input && input.isTTY && typeof input.setRawMode === 'function') {
59
99
  input.setRawMode(false);
100
+ input.__teleptyRawModeActive = false;
60
101
  }
61
102
 
62
103
  if (input && typeof input.pause === 'function') {
63
104
  input.pause();
64
105
  }
106
+
107
+ restoreTerminalModes(output);
65
108
  };
66
109
  }
67
110
 
68
111
  module.exports = {
69
112
  attachInteractiveTerminal,
70
- getTerminalSize
113
+ getTerminalSize,
114
+ restoreTerminalModes,
115
+ TERMINAL_CLEANUP_SEQUENCE
71
116
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.68",
3
+ "version": "0.1.70",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",