@dmsdc-ai/aigentry-telepty 0.1.87 → 0.1.89
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 +165 -26
- package/daemon.js +238 -41
- package/package.json +1 -1
- package/protocol/mailbox.md +244 -0
- package/session-state.js +496 -0
- package/src/mailbox/config.js +36 -0
- package/src/mailbox/delivery.js +132 -0
- package/src/mailbox/index.js +384 -0
- package/src/mailbox/notifier.js +103 -0
- package/src/mailbox/storage.js +185 -0
package/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const { formatHostLabel, groupSessionsByHost, pickSessionTarget } = require('./s
|
|
|
18
18
|
const { buildSharedContextPrompt, createSharedContextDescriptor, ensureSharedContextFile } = require('./shared-context');
|
|
19
19
|
const { runInteractiveSkillInstaller } = require('./skill-installer');
|
|
20
20
|
const crossMachine = require('./cross-machine');
|
|
21
|
+
const { FileMailbox } = require('./src/mailbox/index');
|
|
21
22
|
const args = process.argv.slice(2);
|
|
22
23
|
let pendingTerminalInputError = null;
|
|
23
24
|
let simulatedPromptErrorInjected = false;
|
|
@@ -334,8 +335,22 @@ function printSessionInfo(session, options = {}) {
|
|
|
334
335
|
console.log('');
|
|
335
336
|
}
|
|
336
337
|
|
|
338
|
+
function resolveTeleptyEntryPoint() {
|
|
339
|
+
// After npm upgrade, process.argv[1] still points to the OLD version's cli.js.
|
|
340
|
+
// Resolve the current telepty binary from PATH, which npm updates on install.
|
|
341
|
+
try {
|
|
342
|
+
const cmd = process.platform === 'win32' ? 'where telepty' : 'which telepty';
|
|
343
|
+
const binPath = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split('\n')[0];
|
|
344
|
+
if (binPath && fs.existsSync(binPath)) {
|
|
345
|
+
return fs.realpathSync(binPath);
|
|
346
|
+
}
|
|
347
|
+
} catch {}
|
|
348
|
+
return process.argv[1];
|
|
349
|
+
}
|
|
350
|
+
|
|
337
351
|
function startDetachedDaemon() {
|
|
338
|
-
const
|
|
352
|
+
const entryPoint = resolveTeleptyEntryPoint();
|
|
353
|
+
const cp = spawn(process.argv[0], [entryPoint, 'daemon'], {
|
|
339
354
|
detached: true,
|
|
340
355
|
stdio: 'ignore'
|
|
341
356
|
});
|
|
@@ -391,7 +406,11 @@ async function repairLocalDaemon(options = {}) {
|
|
|
391
406
|
startDetachedDaemon();
|
|
392
407
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
393
408
|
const meta = await getDaemonMeta('127.0.0.1');
|
|
394
|
-
|
|
409
|
+
const versionMatch = meta && meta.version === pkg.version;
|
|
410
|
+
if (meta && !versionMatch) {
|
|
411
|
+
process.stdout.write(`\x1b[33m⚠️ Daemon restarted but version mismatch (running v${meta.version}, expected v${pkg.version})\x1b[0m\n`);
|
|
412
|
+
}
|
|
413
|
+
return { stopped: results.stopped.length, failed: results.failed.length, meta, versionMatch };
|
|
395
414
|
}
|
|
396
415
|
|
|
397
416
|
function getDiscoveryHosts() {
|
|
@@ -455,10 +474,14 @@ async function ensureDaemonRunning(options = {}) {
|
|
|
455
474
|
});
|
|
456
475
|
|
|
457
476
|
if (sessionsRes.ok && hasCapabilities) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
477
|
+
// Version mismatch: running daemon is older than installed CLI
|
|
478
|
+
if (meta && meta.version !== pkg.version) {
|
|
479
|
+
process.stdout.write(`\x1b[33m⚙️ Daemon version mismatch (running v${meta.version}, installed v${pkg.version}). Restarting...\x1b[0m\n`);
|
|
480
|
+
cleanupDaemonProcesses();
|
|
481
|
+
} else {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
} else if (sessionsRes.ok && !meta) {
|
|
462
485
|
process.stdout.write('\x1b[33m⚙️ Found an older local telepty daemon. Restarting it...\x1b[0m\n');
|
|
463
486
|
cleanupDaemonProcesses();
|
|
464
487
|
} else if (sessionsRes.ok && meta) {
|
|
@@ -978,10 +1001,41 @@ async function main() {
|
|
|
978
1001
|
const cmdBase = path.basename(command).replace(/\..*$/, '');
|
|
979
1002
|
const promptPattern = PROMPT_PATTERNS[cmdBase] || /[❯>$#%]\s*$/;
|
|
980
1003
|
let promptReady = false; // wait for CLI prompt before accepting inject
|
|
981
|
-
const injectQueue = [];
|
|
982
1004
|
let lastUserInputTime = 0; // timestamp of last user keystroke
|
|
983
1005
|
const IDLE_THRESHOLD = 2000; // ms after last user input to consider idle
|
|
984
1006
|
|
|
1007
|
+
// Mailbox-backed inject queue (replaces in-memory array for crash resilience)
|
|
1008
|
+
const bridgeMailbox = new FileMailbox({
|
|
1009
|
+
root: path.join(os.homedir(), '.aigentry', 'mailbox', 'bridge'),
|
|
1010
|
+
});
|
|
1011
|
+
const bridgeTarget = sessionId;
|
|
1012
|
+
let bridgeMsgSeq = 0;
|
|
1013
|
+
let bridgePendingCount = 0;
|
|
1014
|
+
|
|
1015
|
+
// Recover undelivered messages from a previous crash
|
|
1016
|
+
try {
|
|
1017
|
+
const leftover = bridgeMailbox.peek(bridgeTarget).filter(m => m.state === 'pending' || m.state === 'in_flight');
|
|
1018
|
+
bridgePendingCount = leftover.length;
|
|
1019
|
+
if (bridgePendingCount > 0) {
|
|
1020
|
+
console.log(`\x1b[33m[BRIDGE] Recovered ${bridgePendingCount} undelivered message(s) from previous session\x1b[0m`);
|
|
1021
|
+
}
|
|
1022
|
+
} catch {}
|
|
1023
|
+
|
|
1024
|
+
function enqueueBridgeMessage(text) {
|
|
1025
|
+
const msgId = `${sessionId}:${Date.now()}:${++bridgeMsgSeq}`;
|
|
1026
|
+
try {
|
|
1027
|
+
bridgeMailbox.enqueue({
|
|
1028
|
+
msg_id: msgId, from: 'daemon', to: bridgeTarget,
|
|
1029
|
+
payload: text, created_at: Math.floor(Date.now() / 1000), attempt: 0,
|
|
1030
|
+
});
|
|
1031
|
+
bridgePendingCount++;
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
// Fallback: write directly if mailbox fails
|
|
1034
|
+
console.error(`[BRIDGE] Mailbox enqueue failed, writing directly: ${err.message}`);
|
|
1035
|
+
child.write(text);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
985
1039
|
function isIdle() {
|
|
986
1040
|
return promptReady && (Date.now() - lastUserInputTime > IDLE_THRESHOLD);
|
|
987
1041
|
}
|
|
@@ -989,34 +1043,47 @@ async function main() {
|
|
|
989
1043
|
let queueFlushTimer = null;
|
|
990
1044
|
let idleCheckTimer = null;
|
|
991
1045
|
|
|
992
|
-
function
|
|
1046
|
+
function flushBridgeMailbox() {
|
|
993
1047
|
if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
|
|
994
1048
|
if (idleCheckTimer) { clearInterval(idleCheckTimer); idleCheckTimer = null; }
|
|
995
|
-
if (
|
|
996
|
-
const batch = injectQueue.splice(0);
|
|
1049
|
+
if (bridgePendingCount === 0) return;
|
|
997
1050
|
let delay = 0;
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1051
|
+
const batch = [];
|
|
1052
|
+
// Dequeue all pending messages
|
|
1053
|
+
while (true) {
|
|
1054
|
+
const msg = bridgeMailbox.dequeue(bridgeTarget);
|
|
1055
|
+
if (!msg) break;
|
|
1056
|
+
batch.push(msg);
|
|
1057
|
+
}
|
|
1058
|
+
if (batch.length === 0) { bridgePendingCount = 0; return; }
|
|
1059
|
+
for (const msg of batch) {
|
|
1060
|
+
const text = msg.payload;
|
|
1061
|
+
const msgId = msg.msg_id;
|
|
1062
|
+
setTimeout(() => {
|
|
1063
|
+
child.write(text);
|
|
1064
|
+
try { bridgeMailbox.ack(bridgeTarget, msgId); } catch {}
|
|
1065
|
+
}, delay);
|
|
1066
|
+
delay += text === '\r' ? 0 : 100;
|
|
1067
|
+
}
|
|
1068
|
+
bridgePendingCount = Math.max(0, bridgePendingCount - batch.length);
|
|
1002
1069
|
promptReady = false;
|
|
1003
1070
|
}
|
|
1004
1071
|
function scheduleIdleFlush() {
|
|
1005
1072
|
if (idleCheckTimer) return;
|
|
1006
1073
|
// Poll every 500ms for idle state
|
|
1007
1074
|
idleCheckTimer = setInterval(() => {
|
|
1008
|
-
if (isIdle() &&
|
|
1009
|
-
|
|
1075
|
+
if (isIdle() && bridgePendingCount > 0) {
|
|
1076
|
+
flushBridgeMailbox();
|
|
1010
1077
|
}
|
|
1011
1078
|
}, 500);
|
|
1012
|
-
// Safety: flush after
|
|
1079
|
+
// Safety: flush after 5s regardless (prevent stuck queue when prompt not detected)
|
|
1013
1080
|
if (!queueFlushTimer) {
|
|
1014
1081
|
queueFlushTimer = setTimeout(() => {
|
|
1015
1082
|
queueFlushTimer = null;
|
|
1016
|
-
if (
|
|
1017
|
-
|
|
1083
|
+
if (bridgePendingCount > 0) {
|
|
1084
|
+
flushBridgeMailbox();
|
|
1018
1085
|
}
|
|
1019
|
-
},
|
|
1086
|
+
}, 5000);
|
|
1020
1087
|
}
|
|
1021
1088
|
}
|
|
1022
1089
|
|
|
@@ -1082,11 +1149,11 @@ async function main() {
|
|
|
1082
1149
|
}
|
|
1083
1150
|
|
|
1084
1151
|
const isCr = chunk === '\r';
|
|
1085
|
-
if (isCr &&
|
|
1152
|
+
if (isCr && bridgePendingCount > 0) {
|
|
1086
1153
|
// CR with pending queued text — queue CR too and flush immediately.
|
|
1087
|
-
|
|
1154
|
+
enqueueBridgeMessage(chunk);
|
|
1088
1155
|
if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
|
|
1089
|
-
|
|
1156
|
+
flushBridgeMailbox();
|
|
1090
1157
|
} else if (isCr) {
|
|
1091
1158
|
// CR always written immediately — never idle-gated.
|
|
1092
1159
|
child.write(chunk);
|
|
@@ -1097,7 +1164,7 @@ async function main() {
|
|
|
1097
1164
|
lastInjectTextTime = Date.now();
|
|
1098
1165
|
} else {
|
|
1099
1166
|
// Text when not idle — queue for safe delivery.
|
|
1100
|
-
|
|
1167
|
+
enqueueBridgeMessage(chunk);
|
|
1101
1168
|
scheduleIdleFlush();
|
|
1102
1169
|
}
|
|
1103
1170
|
}
|
|
@@ -1162,6 +1229,8 @@ async function main() {
|
|
|
1162
1229
|
|
|
1163
1230
|
allowSessionClosed = true;
|
|
1164
1231
|
cleanupTerminal();
|
|
1232
|
+
// Purge bridge mailbox on clean exit (undelivered messages are stale)
|
|
1233
|
+
try { bridgeMailbox.purge(bridgeTarget); } catch {}
|
|
1165
1234
|
process.stdout.write(`\x1b]0;\x07`);
|
|
1166
1235
|
fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }).catch(() => {});
|
|
1167
1236
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
@@ -1198,7 +1267,7 @@ async function main() {
|
|
|
1198
1267
|
// Detect prompt in output to enable inject delivery
|
|
1199
1268
|
if (promptPattern.test(data)) {
|
|
1200
1269
|
promptReady = true;
|
|
1201
|
-
|
|
1270
|
+
flushBridgeMailbox();
|
|
1202
1271
|
// Notify daemon that CLI is ready for inject
|
|
1203
1272
|
if (!readyNotified && wsReady && daemonWs.readyState === 1) {
|
|
1204
1273
|
readyNotified = true;
|
|
@@ -1235,7 +1304,7 @@ async function main() {
|
|
|
1235
1304
|
}
|
|
1236
1305
|
if (promptPattern.test(data)) {
|
|
1237
1306
|
promptReady = true;
|
|
1238
|
-
|
|
1307
|
+
flushBridgeMailbox();
|
|
1239
1308
|
if (wsReady && daemonWs.readyState === 1) {
|
|
1240
1309
|
daemonWs.send(JSON.stringify({ type: 'ready' }));
|
|
1241
1310
|
}
|
|
@@ -1634,6 +1703,76 @@ async function main() {
|
|
|
1634
1703
|
return;
|
|
1635
1704
|
}
|
|
1636
1705
|
|
|
1706
|
+
if (cmd === 'status') {
|
|
1707
|
+
const statusSessionId = args[1] || process.env.TELEPTY_SESSION_ID;
|
|
1708
|
+
if (!statusSessionId) {
|
|
1709
|
+
console.error('❌ Usage: telepty status <session_id>');
|
|
1710
|
+
process.exit(1);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
try {
|
|
1714
|
+
const target = await resolveSessionTarget(statusSessionId);
|
|
1715
|
+
if (!target) {
|
|
1716
|
+
console.error(`❌ Session '${statusSessionId}' was not found on any discovered host.`);
|
|
1717
|
+
process.exit(1);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/state`);
|
|
1721
|
+
const data = await res.json();
|
|
1722
|
+
if (!res.ok) {
|
|
1723
|
+
console.error(`❌ ${formatApiError(data)}`);
|
|
1724
|
+
process.exit(1);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
const auto = data.auto || {};
|
|
1728
|
+
const selfReport = data.self_report || {};
|
|
1729
|
+
|
|
1730
|
+
// Color-coded state display
|
|
1731
|
+
const stateColors = {
|
|
1732
|
+
running: '\x1b[32m', // green
|
|
1733
|
+
idle: '\x1b[33m', // yellow
|
|
1734
|
+
thinking: '\x1b[36m', // cyan
|
|
1735
|
+
stuck: '\x1b[31m', // red
|
|
1736
|
+
waiting_input: '\x1b[35m', // magenta
|
|
1737
|
+
};
|
|
1738
|
+
const stateColor = stateColors[auto.state] || '\x1b[37m';
|
|
1739
|
+
const reset = '\x1b[0m';
|
|
1740
|
+
|
|
1741
|
+
console.log(`\n Session: \x1b[36m${data.session_id}${reset}`);
|
|
1742
|
+
console.log(` Auto State: ${stateColor}${auto.state || 'unknown'}${reset} (confidence: ${auto.confidence != null ? (auto.confidence * 100).toFixed(0) + '%' : '?'})`);
|
|
1743
|
+
if (auto.since) {
|
|
1744
|
+
const durationMs = auto.duration_ms || 0;
|
|
1745
|
+
const durationStr = durationMs < 60000
|
|
1746
|
+
? `${(durationMs / 1000).toFixed(0)}s`
|
|
1747
|
+
: `${(durationMs / 60000).toFixed(1)}m`;
|
|
1748
|
+
console.log(` Since: ${auto.since} (${durationStr} ago)`);
|
|
1749
|
+
}
|
|
1750
|
+
if (auto.detail) {
|
|
1751
|
+
console.log(` Trigger: ${auto.detail.trigger || '-'}`);
|
|
1752
|
+
if (auto.detail.matched_line) console.log(` Matched: "${auto.detail.matched_line}"`);
|
|
1753
|
+
if (auto.detail.silence_ms) console.log(` Silence: ${(auto.detail.silence_ms / 1000).toFixed(1)}s`);
|
|
1754
|
+
if (auto.detail.repeat_count) console.log(` Error repeats: ${auto.detail.repeat_count}`);
|
|
1755
|
+
}
|
|
1756
|
+
if (auto.last_output_preview) {
|
|
1757
|
+
const preview = auto.last_output_preview.replace(/\n/g, '\\n').slice(-80);
|
|
1758
|
+
console.log(` Last output: "${preview}"`);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
if (selfReport.phase) {
|
|
1762
|
+
console.log(`\n Self-report:`);
|
|
1763
|
+
console.log(` Phase: ${selfReport.phase}`);
|
|
1764
|
+
if (selfReport.current_task) console.log(` Task: ${selfReport.current_task}`);
|
|
1765
|
+
if (selfReport.blocker) console.log(` Blocker: \x1b[31m${selfReport.blocker}${reset}`);
|
|
1766
|
+
if (selfReport.needs_input) console.log(` Needs input: \x1b[35myes${reset}`);
|
|
1767
|
+
}
|
|
1768
|
+
console.log('');
|
|
1769
|
+
} catch (e) {
|
|
1770
|
+
console.error(`❌ ${e.message || 'Failed to get session state.'}`);
|
|
1771
|
+
process.exit(1);
|
|
1772
|
+
}
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1637
1776
|
if (cmd === 'status-report') {
|
|
1638
1777
|
const reportArgs = args.slice(1);
|
|
1639
1778
|
let sessionId = process.env.TELEPTY_SESSION_ID || undefined;
|