@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 +129 -52
- package/cross-machine.js +174 -128
- package/daemon.js +230 -48
- package/interactive-terminal.js +46 -1
- package/package.json +1 -1
- package/terminal-backend.js +137 -0
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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 =
|
|
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
|
-
},
|
|
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
|
-
|
|
891
|
-
|
|
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
|
-
//
|
|
1051
|
-
if (target
|
|
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
|
-
|
|
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
|