@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 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 cp = spawn(process.argv[0], [process.argv[1], 'daemon'], {
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
- return { stopped: results.stopped.length, failed: results.failed.length, meta };
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
- return;
459
- }
460
-
461
- if (sessionsRes.ok && !meta) {
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 flushInjectQueue() {
1046
+ function flushBridgeMailbox() {
993
1047
  if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
994
1048
  if (idleCheckTimer) { clearInterval(idleCheckTimer); idleCheckTimer = null; }
995
- if (injectQueue.length === 0) return;
996
- const batch = injectQueue.splice(0);
1049
+ if (bridgePendingCount === 0) return;
997
1050
  let delay = 0;
998
- for (const item of batch) {
999
- setTimeout(() => child.write(item), delay);
1000
- delay += item === '\r' ? 0 : 100;
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() && injectQueue.length > 0) {
1009
- flushInjectQueue();
1075
+ if (isIdle() && bridgePendingCount > 0) {
1076
+ flushBridgeMailbox();
1010
1077
  }
1011
1078
  }, 500);
1012
- // Safety: flush after 15s regardless (prevent stuck queue)
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 (injectQueue.length > 0) {
1017
- flushInjectQueue();
1083
+ if (bridgePendingCount > 0) {
1084
+ flushBridgeMailbox();
1018
1085
  }
1019
- }, 15000);
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 && injectQueue.length > 0) {
1152
+ if (isCr && bridgePendingCount > 0) {
1086
1153
  // CR with pending queued text — queue CR too and flush immediately.
1087
- injectQueue.push(chunk);
1154
+ enqueueBridgeMessage(chunk);
1088
1155
  if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
1089
- flushInjectQueue();
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
- injectQueue.push(chunk);
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
- flushInjectQueue();
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
- flushInjectQueue();
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;