@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/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const path = require('path');
4
4
  const os = require('os');
5
5
  const fs = require('fs');
6
+ const { constants: osConstants } = require('os');
6
7
  const WebSocket = require('ws');
7
8
  const { execSync, spawn } = require('child_process');
8
9
  const readline = require('readline');
@@ -11,7 +12,7 @@ const updateNotifier = require('update-notifier');
11
12
  const pkg = require('./package.json');
12
13
  const { getConfig } = require('./auth');
13
14
  const { cleanupDaemonProcesses } = require('./daemon-control');
14
- const { attachInteractiveTerminal, getTerminalSize } = require('./interactive-terminal');
15
+ const { attachInteractiveTerminal, getTerminalSize, restoreTerminalModes } = require('./interactive-terminal');
15
16
  const { getRuntimeInfo } = require('./runtime-info');
16
17
  const { formatHostLabel, groupSessionsByHost, pickSessionTarget } = require('./session-routing');
17
18
  const { runInteractiveSkillInstaller } = require('./skill-installer');
@@ -43,9 +44,14 @@ function resetInteractiveInput(stream = process.stdin) {
43
44
  return;
44
45
  }
45
46
 
47
+ if (stream.isTTY && (stream.isRaw || stream.__teleptyRawModeActive)) {
48
+ restoreTerminalModes(process.stdout);
49
+ }
50
+
46
51
  if (stream.isTTY && typeof stream.setRawMode === 'function') {
47
52
  try {
48
53
  stream.setRawMode(false);
54
+ stream.__teleptyRawModeActive = false;
49
55
  } catch {
50
56
  // Ignore raw-mode reset failures when the TTY is already gone.
51
57
  }
@@ -101,6 +107,10 @@ process.stdin.on('error', (error) => {
101
107
  process.stderr.write(`\n❌ Telepty stdin error: ${error.message}\n`);
102
108
  });
103
109
 
110
+ process.on('exit', () => {
111
+ resetInteractiveInput(process.stdin);
112
+ });
113
+
104
114
  // Check for updates unless explicitly disabled for tests/CI.
105
115
  if (!process.env.NO_UPDATE_NOTIFIER && !process.env.TELEPTY_DISABLE_UPDATE_NOTIFIER) {
106
116
  updateNotifier({pkg}).notify({ isGlobal: true });
@@ -196,61 +206,46 @@ async function repairLocalDaemon(options = {}) {
196
206
 
197
207
  function getDiscoveryHosts() {
198
208
  const hosts = new Set();
199
-
200
209
  if (REMOTE_HOST && REMOTE_HOST !== '127.0.0.1') {
201
210
  hosts.add(REMOTE_HOST);
202
211
  } else {
203
212
  hosts.add('127.0.0.1');
204
213
  }
205
-
206
- const extraHosts = String(process.env.TELEPTY_DISCOVERY_HOSTS || '')
207
- .split(',')
208
- .map((host) => host.trim())
209
- .filter(Boolean);
210
- extraHosts.forEach((host) => hosts.add(host));
211
-
212
- // Include relay peers for cross-machine session discovery
213
- const relayPeers = String(process.env.TELEPTY_RELAY_PEERS || '')
214
- .split(',')
215
- .map((host) => host.trim())
216
- .filter(Boolean);
217
- relayPeers.forEach((host) => hosts.add(host));
218
-
219
- // Include SSH tunnel-connected peers
220
- const connectedHosts = crossMachine.getConnectedHosts();
221
- connectedHosts.forEach((host) => hosts.add(host));
222
-
223
214
  return Array.from(hosts);
224
215
  }
225
216
 
226
217
  async function discoverSessions(options = {}) {
227
218
  await ensureDaemonRunning();
228
- const hosts = getDiscoveryHosts();
229
219
  const allSessions = [];
230
220
 
231
221
  if (!options.silent) {
232
222
  process.stdout.write('\x1b[36m🔍 Discovering active sessions across connected machines...\x1b[0m\n');
233
223
  }
234
224
 
235
- await Promise.all(hosts.map(async (host) => {
236
- try {
237
- const res = await fetchWithAuth(`http://${host}:${PORT}/api/sessions`, {
238
- signal: AbortSignal.timeout(1500)
225
+ // Local daemon sessions
226
+ try {
227
+ const res = await fetchWithAuth(`http://127.0.0.1:${PORT}/api/sessions`, {
228
+ signal: AbortSignal.timeout(1500)
229
+ });
230
+ if (res.ok) {
231
+ const sessions = await res.json();
232
+ sessions.forEach((session) => {
233
+ allSessions.push({ host: '127.0.0.1', ...session });
239
234
  });
240
- if (res.ok) {
241
- const sessions = await res.json();
242
- sessions.forEach((session) => {
243
- allSessions.push({ host, ...session });
244
- });
245
- }
246
- } catch (e) {
247
- // Ignore nodes that don't have telepty running
248
235
  }
249
- }));
236
+ } catch {}
237
+
238
+ // Remote peer sessions via SSH direct
239
+ const remoteSessions = crossMachine.discoverAllRemoteSessions();
240
+ allSessions.push(...remoteSessions);
250
241
 
251
242
  return allSessions;
252
243
  }
253
244
 
245
+ function isRemoteSession(session) {
246
+ return session.remote === true || (session.host && session.host !== '127.0.0.1' && session.host.includes('@'));
247
+ }
248
+
254
249
  async function resolveSessionTarget(sessionRef, options = {}) {
255
250
  const sessions = options.sessions || await discoverSessions({ silent: true });
256
251
  return pickSessionTarget(sessionRef, sessions, REMOTE_HOST);
@@ -697,7 +692,8 @@ async function main() {
697
692
  command,
698
693
  cwd: process.cwd(),
699
694
  backend: detectedBackend,
700
- cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null
695
+ cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
696
+ cmux_surface_id: process.env.CMUX_SURFACE_ID || null
701
697
  })
702
698
  });
703
699
  const data = await res.json();
@@ -728,7 +724,7 @@ async function main() {
728
724
  };
729
725
  const cmdBase = path.basename(command).replace(/\..*$/, '');
730
726
  const promptPattern = PROMPT_PATTERNS[cmdBase] || /[❯>$#%]\s*$/;
731
- let promptReady = true; // assume ready initially for first inject
727
+ let promptReady = false; // wait for CLI prompt before accepting inject
732
728
  const injectQueue = [];
733
729
 
734
730
  let queueFlushTimer = null;
@@ -750,7 +746,7 @@ async function main() {
750
746
  if (injectQueue.length > 0) {
751
747
  flushInjectQueue();
752
748
  }
753
- }, 3000);
749
+ }, 15000);
754
750
  }
755
751
 
756
752
  // Connect to daemon WebSocket with auto-reconnect
@@ -776,7 +772,8 @@ async function main() {
776
772
  command,
777
773
  cwd: process.cwd(),
778
774
  backend: detectedBackend,
779
- cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null
775
+ cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
776
+ cmux_surface_id: process.env.CMUX_SURFACE_ID || null
780
777
  })
781
778
  });
782
779
  } catch (e) {
@@ -791,6 +788,10 @@ async function main() {
791
788
  // No resize trick on reconnect — it causes visible flickering across all
792
789
  // terminals when the daemon restarts and multiple sessions reconnect at once.
793
790
  reconnectAttempts = 0;
791
+ // Re-send ready on reconnect so new daemon knows CLI is ready
792
+ if (readyNotified && promptReady) {
793
+ daemonWs.send(JSON.stringify({ type: 'ready' }));
794
+ }
794
795
  });
795
796
 
796
797
  daemonWs.on('message', (message) => {
@@ -861,6 +862,31 @@ async function main() {
861
862
  child.resize(size.cols, size.rows);
862
863
  }
863
864
  });
865
+ let allowSessionClosed = false;
866
+ const allowSignalHandlers = new Map();
867
+
868
+ function closeAllowSession() {
869
+ if (allowSessionClosed) {
870
+ return false;
871
+ }
872
+
873
+ allowSessionClosed = true;
874
+ cleanupTerminal();
875
+ process.stdout.write(`\x1b]0;\x07`);
876
+ fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }).catch(() => {});
877
+ if (reconnectTimer) clearTimeout(reconnectTimer);
878
+ try {
879
+ daemonWs.close();
880
+ } catch {}
881
+ for (const [signalName, handler] of allowSignalHandlers) {
882
+ process.off(signalName, handler);
883
+ }
884
+ return true;
885
+ }
886
+
887
+ function exitAllowSession(code) {
888
+ setTimeout(() => process.exit(code), 25);
889
+ }
864
890
 
865
891
  // Intercept terminal title escape sequences and prefix with session ID
866
892
  const titlePrefix = `\u26A1 ${sessionId}`;
@@ -872,6 +898,7 @@ async function main() {
872
898
  }
873
899
 
874
900
  // Relay PTY output to current terminal + send to daemon for attach clients
901
+ let readyNotified = false;
875
902
  child.onData((data) => {
876
903
  const rewritten = rewriteTitleSequences(data);
877
904
  process.stdout.write(rewritten);
@@ -882,22 +909,36 @@ async function main() {
882
909
  if (promptPattern.test(data)) {
883
910
  promptReady = true;
884
911
  flushInjectQueue();
912
+ // Notify daemon that CLI is ready for inject
913
+ if (!readyNotified && wsReady && daemonWs.readyState === 1) {
914
+ readyNotified = true;
915
+ daemonWs.send(JSON.stringify({ type: 'ready' }));
916
+ }
885
917
  }
886
918
  });
887
919
 
888
920
  // Handle child exit
889
921
  child.onExit(({ exitCode }) => {
890
- cleanupTerminal();
891
- process.stdout.write(`\x1b]0;\x07`);
922
+ if (!closeAllowSession()) {
923
+ return;
924
+ }
892
925
  console.log(`\n\x1b[33mSession '${sessionId}' exited (code ${exitCode}).\x1b[0m`);
893
-
894
- // Deregister from daemon
895
- fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }).catch(() => {});
896
- if (reconnectTimer) clearTimeout(reconnectTimer);
897
- daemonWs.close();
898
- process.exit(exitCode || 0);
926
+ exitAllowSession(exitCode || 0);
899
927
  });
900
928
 
929
+ for (const signalName of ['SIGTERM', 'SIGHUP', 'SIGQUIT']) {
930
+ const handler = () => {
931
+ closeAllowSession();
932
+ try {
933
+ child.kill(signalName);
934
+ } catch {}
935
+ const signalCode = osConstants.signals[signalName] || 1;
936
+ exitAllowSession(128 + signalCode);
937
+ };
938
+ allowSignalHandlers.set(signalName, handler);
939
+ process.on(signalName, handler);
940
+ }
941
+
901
942
  // Graceful shutdown on SIGINT (let child handle it via PTY)
902
943
  process.on('SIGINT', () => {});
903
944
 
@@ -1009,6 +1050,34 @@ async function main() {
1009
1050
  return;
1010
1051
  }
1011
1052
 
1053
+ if (cmd === 'read-screen') {
1054
+ const sessionId = args[1];
1055
+ if (!sessionId) { console.error('❌ Usage: telepty read-screen <session_id> [--lines N] [--raw]'); process.exit(1); }
1056
+
1057
+ const linesIndex = args.indexOf('--lines');
1058
+ const lines = (linesIndex !== -1 && args[linesIndex + 1]) ? args[linesIndex + 1] : '50';
1059
+ const raw = args.includes('--raw');
1060
+
1061
+ try {
1062
+ const target = await resolveSessionTarget(sessionId);
1063
+ if (!target) {
1064
+ console.error(`❌ Session '${sessionId}' was not found on any discovered host.`);
1065
+ process.exit(1);
1066
+ }
1067
+
1068
+ const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/screen?lines=${lines}${raw ? '&raw=1' : ''}`);
1069
+ const data = await res.json();
1070
+ if (!res.ok) { console.error(`❌ Error: ${data.error}`); process.exit(1); }
1071
+
1072
+ if (!data.screen) {
1073
+ console.log('(empty screen)');
1074
+ } else {
1075
+ console.log(data.screen);
1076
+ }
1077
+ } catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
1078
+ return;
1079
+ }
1080
+
1012
1081
  if (cmd === 'inject') {
1013
1082
  // Check for --no-enter flag
1014
1083
  const noEnterIndex = args.indexOf('--no-enter');
@@ -1047,14 +1116,24 @@ async function main() {
1047
1116
  process.exit(1);
1048
1117
  }
1049
1118
 
1050
- // Entitlement: remote session check
1051
- if (target.host && target.host !== '127.0.0.1' && target.host !== 'localhost') {
1119
+ // Remote session: use SSH direct execution
1120
+ if (isRemoteSession(target)) {
1052
1121
  const { checkEntitlement } = require('./entitlement');
1053
1122
  const ent = checkEntitlement({ feature: 'telepty.remote_sessions' });
1054
1123
  if (!ent.allowed) {
1055
1124
  console.error(`⚠️ ${ent.reason}\n Upgrade: ${ent.upgrade_url}`);
1056
1125
  process.exit(1);
1057
1126
  }
1127
+ const result = crossMachine.remoteInject(target.peerName, target.id, prompt, {
1128
+ from: fromId,
1129
+ no_enter: noEnter
1130
+ });
1131
+ if (result.success) {
1132
+ console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m' @ ${target.peerName}.`);
1133
+ } else {
1134
+ console.error(`❌ Error: ${result.error}`);
1135
+ }
1136
+ return;
1058
1137
  }
1059
1138
 
1060
1139
  const body = { prompt, no_enter: noEnter };
@@ -1067,8 +1146,7 @@ async function main() {
1067
1146
  });
1068
1147
  const data = await res.json();
1069
1148
  if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
1070
- const hostSuffix = target.host === '127.0.0.1' ? '' : ` @ ${target.host}`;
1071
- console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m'${hostSuffix}.`);
1149
+ console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m'.`);
1072
1150
  } catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
1073
1151
  return;
1074
1152
  }
@@ -1879,8 +1957,6 @@ Discuss the following topic from your project's perspective. Engage with other s
1879
1957
  if (result.success) {
1880
1958
  console.log(`\x1b[32m✅ Connected to ${result.name}\x1b[0m`);
1881
1959
  console.log(` Machine ID: ${result.machineId}`);
1882
- console.log(` Local port: ${result.localPort}`);
1883
- console.log(` Version: ${result.version}`);
1884
1960
  console.log(`\nSessions on ${result.name} are now discoverable via \x1b[36mtelepty list\x1b[0m`);
1885
1961
  } else {
1886
1962
  console.error(`\x1b[31m❌ ${result.error}\x1b[0m`);
@@ -2021,6 +2097,7 @@ Usage:
2021
2097
  telepty list List all active sessions across discovered hosts
2022
2098
  telepty attach [id[@host]] Attach to a session (Interactive picker if no ID)
2023
2099
  telepty inject [--no-enter] [--from <id>] [--reply-to <id>] <id[@host]> "<prompt>" Inject text into a single session
2100
+ telepty read-screen <id[@host]> [--lines N] [--raw] Read session screen buffer
2024
2101
  telepty reply "<text>" Reply to the session that last injected into $TELEPTY_SESSION_ID
2025
2102
  telepty multicast <id1[@host],id2[@host]> "<prompt>" Inject text into multiple specific sessions
2026
2103
  telepty broadcast "<prompt>" Inject text into ALL active sessions