@dmsdc-ai/aigentry-telepty 0.1.70 → 0.1.72
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 +152 -28
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -643,6 +643,11 @@ async function main() {
|
|
|
643
643
|
allowArgs.splice(idIndex, 2);
|
|
644
644
|
}
|
|
645
645
|
|
|
646
|
+
// Extract --auto-restart flag
|
|
647
|
+
const autoRestartIndex = allowArgs.indexOf('--auto-restart');
|
|
648
|
+
const autoRestart = autoRestartIndex !== -1;
|
|
649
|
+
if (autoRestart) allowArgs.splice(autoRestartIndex, 1);
|
|
650
|
+
|
|
646
651
|
// Strip optional -- separator for backward compat
|
|
647
652
|
const sepIndex = allowArgs.indexOf('--');
|
|
648
653
|
if (sepIndex !== -1) allowArgs.splice(sepIndex, 1);
|
|
@@ -708,13 +713,60 @@ async function main() {
|
|
|
708
713
|
|
|
709
714
|
// Spawn local PTY (preserves isTTY, env, shell config)
|
|
710
715
|
const pty = require('node-pty');
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
716
|
+
const sessionCwd = process.cwd();
|
|
717
|
+
const sessionEnv = { ...process.env, TELEPTY_SESSION_ID: sessionId };
|
|
718
|
+
let child = null;
|
|
719
|
+
let sessionStartTime = Date.now();
|
|
720
|
+
let crashCount = 0;
|
|
721
|
+
const MAX_CRASHES = 3;
|
|
722
|
+
const DEATH_LOG_PATH = path.join(os.homedir(), '.telepty', 'logs', 'session-deaths.log');
|
|
723
|
+
|
|
724
|
+
function logSessionDeath(exitCode, signal, duration) {
|
|
725
|
+
try {
|
|
726
|
+
fs.mkdirSync(path.dirname(DEATH_LOG_PATH), { recursive: true });
|
|
727
|
+
const entry = `[${new Date().toISOString()}] session=${sessionId} command=${command} exit=${exitCode} signal=${signal || 'none'} duration=${Math.round(duration / 1000)}s crashes=${crashCount}\n`;
|
|
728
|
+
fs.appendFileSync(DEATH_LOG_PATH, entry);
|
|
729
|
+
} catch {}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function emitDeathEvent(exitCode, signal, willRestart) {
|
|
733
|
+
if (wsReady && daemonWs && daemonWs.readyState === 1) {
|
|
734
|
+
daemonWs.send(JSON.stringify({
|
|
735
|
+
type: 'session_died',
|
|
736
|
+
session_id: sessionId,
|
|
737
|
+
exitCode, signal: signal || null,
|
|
738
|
+
duration: Date.now() - sessionStartTime,
|
|
739
|
+
crashCount,
|
|
740
|
+
willRestart,
|
|
741
|
+
timestamp: new Date().toISOString()
|
|
742
|
+
}));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function emitRestartEvent(attempt) {
|
|
747
|
+
if (wsReady && daemonWs && daemonWs.readyState === 1) {
|
|
748
|
+
daemonWs.send(JSON.stringify({
|
|
749
|
+
type: 'session_restarted',
|
|
750
|
+
session_id: sessionId,
|
|
751
|
+
attempt,
|
|
752
|
+
timestamp: new Date().toISOString()
|
|
753
|
+
}));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function spawnChild() {
|
|
758
|
+
child = pty.spawn(command, cmdArgs, {
|
|
759
|
+
name: 'xterm-256color',
|
|
760
|
+
cols: process.stdout.columns || 80,
|
|
761
|
+
rows: process.stdout.rows || 30,
|
|
762
|
+
cwd: sessionCwd,
|
|
763
|
+
env: sessionEnv
|
|
764
|
+
});
|
|
765
|
+
sessionStartTime = Date.now();
|
|
766
|
+
return child;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
spawnChild();
|
|
718
770
|
|
|
719
771
|
// Prompt-ready detection for safe inject delivery
|
|
720
772
|
const PROMPT_PATTERNS = {
|
|
@@ -726,10 +778,19 @@ async function main() {
|
|
|
726
778
|
const promptPattern = PROMPT_PATTERNS[cmdBase] || /[❯>$#%]\s*$/;
|
|
727
779
|
let promptReady = false; // wait for CLI prompt before accepting inject
|
|
728
780
|
const injectQueue = [];
|
|
781
|
+
let lastUserInputTime = 0; // timestamp of last user keystroke
|
|
782
|
+
const IDLE_THRESHOLD = 2000; // ms after last user input to consider idle
|
|
783
|
+
|
|
784
|
+
function isIdle() {
|
|
785
|
+
return promptReady && (Date.now() - lastUserInputTime > IDLE_THRESHOLD);
|
|
786
|
+
}
|
|
729
787
|
|
|
730
788
|
let queueFlushTimer = null;
|
|
789
|
+
let idleCheckTimer = null;
|
|
790
|
+
|
|
731
791
|
function flushInjectQueue() {
|
|
732
792
|
if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
|
|
793
|
+
if (idleCheckTimer) { clearInterval(idleCheckTimer); idleCheckTimer = null; }
|
|
733
794
|
if (injectQueue.length === 0) return;
|
|
734
795
|
const batch = injectQueue.splice(0);
|
|
735
796
|
let delay = 0;
|
|
@@ -739,14 +800,23 @@ async function main() {
|
|
|
739
800
|
}
|
|
740
801
|
promptReady = false;
|
|
741
802
|
}
|
|
742
|
-
function
|
|
743
|
-
if (
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (injectQueue.length > 0) {
|
|
803
|
+
function scheduleIdleFlush() {
|
|
804
|
+
if (idleCheckTimer) return;
|
|
805
|
+
// Poll every 500ms for idle state
|
|
806
|
+
idleCheckTimer = setInterval(() => {
|
|
807
|
+
if (isIdle() && injectQueue.length > 0) {
|
|
747
808
|
flushInjectQueue();
|
|
748
809
|
}
|
|
749
|
-
},
|
|
810
|
+
}, 500);
|
|
811
|
+
// Safety: flush after 15s regardless (prevent stuck queue)
|
|
812
|
+
if (!queueFlushTimer) {
|
|
813
|
+
queueFlushTimer = setTimeout(() => {
|
|
814
|
+
queueFlushTimer = null;
|
|
815
|
+
if (injectQueue.length > 0) {
|
|
816
|
+
flushInjectQueue();
|
|
817
|
+
}
|
|
818
|
+
}, 15000);
|
|
819
|
+
}
|
|
750
820
|
}
|
|
751
821
|
|
|
752
822
|
// Connect to daemon WebSocket with auto-reconnect
|
|
@@ -806,15 +876,18 @@ async function main() {
|
|
|
806
876
|
injectQueue.push(msg.data);
|
|
807
877
|
if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
|
|
808
878
|
flushInjectQueue();
|
|
809
|
-
} else if (isCr
|
|
879
|
+
} else if (isCr && isIdle()) {
|
|
880
|
+
// CR when idle — write immediately
|
|
881
|
+
child.write(msg.data);
|
|
882
|
+
} else if (!isCr && isIdle()) {
|
|
883
|
+
// Text when idle — write immediately
|
|
810
884
|
child.write(msg.data);
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
lastInjectTextTime = Date.now();
|
|
814
|
-
}
|
|
885
|
+
promptReady = false;
|
|
886
|
+
lastInjectTextTime = Date.now();
|
|
815
887
|
} else {
|
|
888
|
+
// Not idle (user typing or CLI busy) — queue for safe delivery
|
|
816
889
|
injectQueue.push(msg.data);
|
|
817
|
-
|
|
890
|
+
scheduleIdleFlush();
|
|
818
891
|
}
|
|
819
892
|
} else if (msg.type === 'resize') {
|
|
820
893
|
child.resize(msg.cols, msg.rows);
|
|
@@ -855,6 +928,7 @@ async function main() {
|
|
|
855
928
|
|
|
856
929
|
const cleanupTerminal = attachInteractiveTerminal(process.stdin, process.stdout, {
|
|
857
930
|
onData: (data) => {
|
|
931
|
+
lastUserInputTime = Date.now();
|
|
858
932
|
child.write(data.toString());
|
|
859
933
|
},
|
|
860
934
|
onResize: () => {
|
|
@@ -917,14 +991,64 @@ async function main() {
|
|
|
917
991
|
}
|
|
918
992
|
});
|
|
919
993
|
|
|
920
|
-
// Handle child exit
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
994
|
+
// Handle child exit with death tracking + auto-restart
|
|
995
|
+
function attachChildExitHandler() {
|
|
996
|
+
child.onExit(({ exitCode, signal }) => {
|
|
997
|
+
const duration = Date.now() - sessionStartTime;
|
|
998
|
+
const isAbnormal = exitCode !== 0 || signal;
|
|
999
|
+
const durationStr = duration > 60000 ? `${Math.round(duration / 60000)}m ${Math.round((duration % 60000) / 1000)}s` : `${Math.round(duration / 1000)}s`;
|
|
1000
|
+
|
|
1001
|
+
logSessionDeath(exitCode, signal, duration);
|
|
1002
|
+
|
|
1003
|
+
if (isAbnormal && autoRestart && crashCount < MAX_CRASHES) {
|
|
1004
|
+
crashCount++;
|
|
1005
|
+
const backoffMs = Math.min(1000 * Math.pow(2, crashCount - 1), 8000);
|
|
1006
|
+
const willRestart = true;
|
|
1007
|
+
emitDeathEvent(exitCode, signal, willRestart);
|
|
1008
|
+
console.log(`\n\x1b[33m⚠️ Session '${sessionId}' died (code ${exitCode}, signal ${signal || 'none'}, ${durationStr}). Restarting in ${backoffMs}ms (attempt ${crashCount}/${MAX_CRASHES})...\x1b[0m`);
|
|
1009
|
+
|
|
1010
|
+
setTimeout(() => {
|
|
1011
|
+
try {
|
|
1012
|
+
spawnChild();
|
|
1013
|
+
// Re-attach output relay, prompt detection, and exit handler
|
|
1014
|
+
child.onData((data) => {
|
|
1015
|
+
const rewritten = rewriteTitleSequences(data);
|
|
1016
|
+
process.stdout.write(rewritten);
|
|
1017
|
+
if (wsReady && daemonWs.readyState === 1) {
|
|
1018
|
+
daemonWs.send(JSON.stringify({ type: 'output', data }));
|
|
1019
|
+
}
|
|
1020
|
+
if (promptPattern.test(data)) {
|
|
1021
|
+
promptReady = true;
|
|
1022
|
+
flushInjectQueue();
|
|
1023
|
+
if (wsReady && daemonWs.readyState === 1) {
|
|
1024
|
+
daemonWs.send(JSON.stringify({ type: 'ready' }));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
attachChildExitHandler();
|
|
1029
|
+
emitRestartEvent(crashCount);
|
|
1030
|
+
console.log(`\x1b[32m✅ Session '${sessionId}' restarted (attempt ${crashCount}).\x1b[0m\n`);
|
|
1031
|
+
// Reset crash counter if session survives 30s
|
|
1032
|
+
setTimeout(() => { if (crashCount > 0) crashCount = 0; }, 30000);
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
console.error(`\x1b[31m❌ Failed to restart session '${sessionId}': ${err.message}\x1b[0m`);
|
|
1035
|
+
emitDeathEvent(exitCode, signal, false);
|
|
1036
|
+
if (!closeAllowSession()) return;
|
|
1037
|
+
exitAllowSession(exitCode || 1);
|
|
1038
|
+
}
|
|
1039
|
+
}, backoffMs);
|
|
1040
|
+
} else {
|
|
1041
|
+
if (isAbnormal && autoRestart && crashCount >= MAX_CRASHES) {
|
|
1042
|
+
console.log(`\n\x1b[31m❌ Session '${sessionId}' crashed ${MAX_CRASHES} times. Giving up.\x1b[0m`);
|
|
1043
|
+
}
|
|
1044
|
+
emitDeathEvent(exitCode, signal, false);
|
|
1045
|
+
if (!closeAllowSession()) return;
|
|
1046
|
+
console.log(`\n\x1b[33mSession '${sessionId}' exited (code ${exitCode}${signal ? ', signal ' + signal : ''}, ${durationStr}).\x1b[0m`);
|
|
1047
|
+
exitAllowSession(exitCode || 0);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
attachChildExitHandler();
|
|
928
1052
|
|
|
929
1053
|
for (const signalName of ['SIGTERM', 'SIGHUP', 'SIGQUIT']) {
|
|
930
1054
|
const handler = () => {
|
|
@@ -2093,7 +2217,7 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2093
2217
|
Usage:
|
|
2094
2218
|
telepty daemon Start the background daemon
|
|
2095
2219
|
telepty spawn --id <id> <command> [args...] Spawn a new background CLI
|
|
2096
|
-
telepty allow [--id <id>] <command> [args...]
|
|
2220
|
+
telepty allow [--id <id>] [--auto-restart] <command> [args...] Allow inject on a CLI (auto-restart on crash)
|
|
2097
2221
|
telepty list List all active sessions across discovered hosts
|
|
2098
2222
|
telepty attach [id[@host]] Attach to a session (Interactive picker if no ID)
|
|
2099
2223
|
telepty inject [--no-enter] [--from <id>] [--reply-to <id>] <id[@host]> "<prompt>" Inject text into a single session
|