@dmsdc-ai/aigentry-telepty 0.1.40 → 0.1.42

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 (3) hide show
  1. package/cli.js +12 -0
  2. package/daemon.js +72 -43
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -705,7 +705,9 @@ async function main() {
705
705
  let promptReady = true; // assume ready initially for first inject
706
706
  const injectQueue = [];
707
707
 
708
+ let queueFlushTimer = null;
708
709
  function flushInjectQueue() {
710
+ if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
709
711
  if (injectQueue.length === 0) return;
710
712
  const batch = injectQueue.splice(0);
711
713
  let delay = 0;
@@ -715,6 +717,15 @@ async function main() {
715
717
  }
716
718
  promptReady = false;
717
719
  }
720
+ function scheduleQueueFlush() {
721
+ if (queueFlushTimer) return;
722
+ queueFlushTimer = setTimeout(() => {
723
+ queueFlushTimer = null;
724
+ if (injectQueue.length > 0) {
725
+ flushInjectQueue();
726
+ }
727
+ }, 3000);
728
+ }
718
729
 
719
730
  // Connect to daemon WebSocket with auto-reconnect
720
731
  const wsUrl = `ws://${REMOTE_HOST}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
@@ -767,6 +778,7 @@ async function main() {
767
778
  }
768
779
  } else {
769
780
  injectQueue.push(msg.data);
781
+ scheduleQueueFlush();
770
782
  }
771
783
  } else if (msg.type === 'resize') {
772
784
  child.resize(msg.cols, msg.rows);
package/daemon.js CHANGED
@@ -143,6 +143,7 @@ app.post('/api/sessions/spawn', (req, res) => {
143
143
  command,
144
144
  cwd,
145
145
  createdAt: new Date().toISOString(),
146
+ lastActivityAt: new Date().toISOString(),
146
147
  clients: new Set(),
147
148
  isClosing: false
148
149
  };
@@ -210,6 +211,7 @@ app.post('/api/sessions/register', (req, res) => {
210
211
  command: command || 'wrapped',
211
212
  cwd,
212
213
  createdAt: new Date().toISOString(),
214
+ lastActivityAt: new Date().toISOString(),
213
215
  clients: new Set(),
214
216
  isClosing: false
215
217
  };
@@ -252,14 +254,24 @@ app.post('/api/sessions/register', (req, res) => {
252
254
  });
253
255
 
254
256
  app.get('/api/sessions', (req, res) => {
255
- const list = Object.entries(sessions).map(([id, session]) => ({
256
- id,
257
- type: session.type || 'spawned',
258
- command: session.command,
259
- cwd: session.cwd,
260
- createdAt: session.createdAt,
261
- active_clients: session.clients.size
262
- }));
257
+ const idleGt = req.query.idle_gt ? Number(req.query.idle_gt) : null;
258
+ const now = Date.now();
259
+ let list = Object.entries(sessions).map(([id, session]) => {
260
+ const idleSeconds = session.lastActivityAt ? Math.floor((now - new Date(session.lastActivityAt).getTime()) / 1000) : null;
261
+ return {
262
+ id,
263
+ type: session.type || 'spawned',
264
+ command: session.command,
265
+ cwd: session.cwd,
266
+ createdAt: session.createdAt,
267
+ lastActivityAt: session.lastActivityAt || null,
268
+ idleSeconds,
269
+ active_clients: session.clients.size
270
+ };
271
+ });
272
+ if (idleGt !== null) {
273
+ list = list.filter(s => s.idleSeconds !== null && s.idleSeconds > idleGt);
274
+ }
263
275
  res.json(list);
264
276
  });
265
277
 
@@ -268,6 +280,7 @@ app.get('/api/sessions/:id', (req, res) => {
268
280
  const resolvedId = resolveSessionAlias(requestedId);
269
281
  if (!resolvedId) return res.status(404).json({ error: 'Session not found' });
270
282
  const session = sessions[resolvedId];
283
+ const idleSeconds = session.lastActivityAt ? Math.floor((Date.now() - new Date(session.lastActivityAt).getTime()) / 1000) : null;
271
284
  res.json({
272
285
  id: resolvedId,
273
286
  alias: requestedId !== resolvedId ? requestedId : null,
@@ -275,6 +288,8 @@ app.get('/api/sessions/:id', (req, res) => {
275
288
  command: session.command,
276
289
  cwd: session.cwd,
277
290
  createdAt: session.createdAt,
291
+ lastActivityAt: session.lastActivityAt || null,
292
+ idleSeconds,
278
293
  active_clients: session.clients ? session.clients.size : 0,
279
294
  lastInjectFrom: session.lastInjectFrom || null,
280
295
  lastInjectReplyTo: session.lastInjectReplyTo || null
@@ -611,6 +626,7 @@ app.post('/api/sessions/:id/inject', (req, res) => {
611
626
  if (from) session.lastInjectFrom = from;
612
627
  if (reply_to) session.lastInjectReplyTo = reply_to;
613
628
  if (thread_id) session.lastThreadId = thread_id;
629
+ session.lastActivityAt = new Date().toISOString();
614
630
 
615
631
  // Auto-prepend [from:] [reply-to:] header if from is set and not already in prompt
616
632
  let finalPrompt = prompt;
@@ -639,43 +655,30 @@ app.post('/api/sessions/:id/inject', (req, res) => {
639
655
  }
640
656
 
641
657
  let submitResult = null;
642
- if (session.type === 'wrapped' && !no_enter) {
643
- // Hybrid: text via WS (allow bridge handles it), Enter via kitty send-key
644
- if (!writeToSession(finalPrompt)) {
645
- return res.status(503).json({ error: 'Wrap process is not connected' });
646
- }
658
+ if (!writeToSession(finalPrompt)) {
659
+ return res.status(503).json({ error: 'Wrap process is not connected' });
660
+ }
661
+
662
+ if (!no_enter) {
663
+ // Universal split_cr: text first, \r after delay
664
+ // Allow bridge 0.1.40+ has 3s queue flush timeout, so \r always gets delivered
647
665
  setTimeout(() => {
648
- // Try kitty send-key Return (reliable for all CLIs)
649
- const windowId = findKittyWindowId(findKittySocket(), id);
650
- if (windowId) {
651
- try {
652
- const { execSync } = require('child_process');
653
- execSync(`kitty @ --to unix:${findKittySocket()} send-key --match id:${windowId} Return`, {
654
- timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
655
- });
656
- console.log(`[INJECT+SUBMIT] WS text + kitty Return for ${id} (window ${windowId})`);
657
- // Restore tab title after inject (Claude Code overwrites it)
658
- try {
659
- execSync(`kitty @ --to unix:${findKittySocket()} set-tab-title --match id:${windowId} '⚡ telepty :: ${id}'`, {
666
+ const ok = writeToSession('\r');
667
+ console.log(`[INJECT+SUBMIT] Split \\r for ${id}: ${ok ? 'success' : 'failed'}`);
668
+ // Restore kitty tab title (optional, no-op if not kitty)
669
+ try {
670
+ const sock = findKittySocket();
671
+ if (sock) {
672
+ if (!session.kittyWindowId) session.kittyWindowId = findKittyWindowId(sock, id);
673
+ if (session.kittyWindowId) {
674
+ require('child_process').execSync(`kitty @ --to unix:${sock} set-tab-title --match id:${session.kittyWindowId} '⚡ telepty :: ${id}'`, {
660
675
  timeout: 2000, stdio: ['pipe', 'pipe', 'pipe']
661
676
  });
662
- } catch {}
663
- } catch {
664
- writeToSession('\r');
665
- console.log(`[INJECT+SUBMIT] WS text + WS \\r fallback for ${id}`);
677
+ }
666
678
  }
667
- } else {
668
- writeToSession('\r');
669
- console.log(`[INJECT+SUBMIT] WS text + WS \\r (no kitty window) for ${id}`);
670
- }
671
- }, 500);
672
- submitResult = { deferred: true, strategy: 'ws_text_kitty_return' };
673
- } else if (session.type === 'wrapped') {
674
- // no_enter=true for wrapped
675
- if (!writeToSession(finalPrompt)) {
676
- return res.status(503).json({ error: 'Wrap process is not connected' });
677
- }
678
- submitResult = { strategy: 'ws_no_enter' };
679
+ } catch {}
680
+ }, 300);
681
+ submitResult = { deferred: true, strategy: 'split_cr' };
679
682
  } else {
680
683
  if (!writeToSession(finalPrompt)) {
681
684
  return res.status(503).json({ error: 'Wrap process is not connected' });
@@ -1050,8 +1053,11 @@ const server = app.listen(PORT, HOST, () => {
1050
1053
  console.log(`🚀 aigentry-telepty daemon listening on http://${HOST}:${PORT}`);
1051
1054
  });
1052
1055
 
1056
+ const IDLE_THRESHOLD_SECONDS = 60;
1053
1057
  setInterval(() => {
1058
+ const now = Date.now();
1054
1059
  for (const [id, session] of Object.entries(sessions)) {
1060
+ const idleSeconds = session.lastActivityAt ? Math.floor((now - new Date(session.lastActivityAt).getTime()) / 1000) : null;
1055
1061
  const healthMsg = JSON.stringify({
1056
1062
  type: 'session_health',
1057
1063
  session_id: id,
@@ -1059,13 +1065,34 @@ setInterval(() => {
1059
1065
  alive: session.type === 'wrapped' ? (session.ownerWs && session.ownerWs.readyState === 1) : (session.ptyProcess && !session.ptyProcess.killed),
1060
1066
  pid: session.ptyProcess?.pid || null,
1061
1067
  type: session.type,
1062
- clients: session.clients ? session.clients.size : 0
1068
+ clients: session.clients ? session.clients.size : 0,
1069
+ idleSeconds
1063
1070
  },
1064
1071
  timestamp: new Date().toISOString()
1065
1072
  });
1066
1073
  busClients.forEach(client => {
1067
1074
  if (client.readyState === 1) client.send(healthMsg);
1068
1075
  });
1076
+
1077
+ // Emit session.idle when idle exceeds threshold
1078
+ if (idleSeconds !== null && idleSeconds >= IDLE_THRESHOLD_SECONDS && !session._idleEmitted) {
1079
+ session._idleEmitted = true;
1080
+ const idleMsg = JSON.stringify({
1081
+ type: 'session.idle',
1082
+ session_id: id,
1083
+ idleSeconds,
1084
+ lastActivityAt: session.lastActivityAt,
1085
+ timestamp: new Date().toISOString()
1086
+ });
1087
+ busClients.forEach(client => {
1088
+ if (client.readyState === 1) client.send(idleMsg);
1089
+ });
1090
+ console.log(`[IDLE] Session ${id} idle for ${idleSeconds}s`);
1091
+ }
1092
+ // Reset idle flag when activity resumes
1093
+ if (idleSeconds !== null && idleSeconds < IDLE_THRESHOLD_SECONDS) {
1094
+ session._idleEmitted = false;
1095
+ }
1069
1096
  }
1070
1097
  }, 10000);
1071
1098
 
@@ -1098,6 +1125,7 @@ wss.on('connection', (ws, req) => {
1098
1125
  command: 'wrapped',
1099
1126
  cwd: process.cwd(),
1100
1127
  createdAt: new Date().toISOString(),
1128
+ lastActivityAt: new Date().toISOString(),
1101
1129
  clients: new Set([ws]),
1102
1130
  isClosing: false
1103
1131
  };
@@ -1136,8 +1164,9 @@ wss.on('connection', (ws, req) => {
1136
1164
 
1137
1165
  if (activeSession.type === 'wrapped') {
1138
1166
  if (ws === activeSession.ownerWs) {
1139
- // Owner sending output -> broadcast to other clients
1167
+ // Owner sending output -> broadcast to other clients + update activity
1140
1168
  if (type === 'output') {
1169
+ activeSession.lastActivityAt = new Date().toISOString();
1141
1170
  activeSession.clients.forEach(client => {
1142
1171
  if (client !== ws && client.readyState === 1) {
1143
1172
  client.send(JSON.stringify({ type: 'output', data }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",