@dmsdc-ai/aigentry-telepty 0.4.3 → 0.4.5

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
@@ -23,6 +23,7 @@ const crossMachine = require('./cross-machine');
23
23
  const { parseHostSpec, buildDaemonUrl, buildDaemonWsUrl } = require('./host-spec');
24
24
  const { FileMailbox } = require('./src/mailbox/index');
25
25
  const readyRegistry = require('./src/prompt-symbol-registry');
26
+ const lifecycle = require('./src/lifecycle');
26
27
  const args = process.argv.slice(2);
27
28
  let pendingTerminalInputError = null;
28
29
  let simulatedPromptErrorInjected = false;
@@ -156,6 +157,11 @@ const fetchWithAuth = (url, options = {}) => {
156
157
  return fetch(url, { ...options, headers });
157
158
  };
158
159
 
160
+ function isSubmitForceDefaultEnabled(env = process.env) {
161
+ const value = (env.TELEPTY_SUBMIT_FORCE_DEFAULT || '').trim().toLowerCase();
162
+ return value === '1' || value === 'true' || value === 'yes' || value === 'on';
163
+ }
164
+
159
165
  async function getDaemonMeta(host = REMOTE_HOST) {
160
166
  try {
161
167
  const res = await fetchWithAuth(`${daemonUrl(host)}/api/meta`, {
@@ -206,6 +212,26 @@ function formatSessionHealth(session) {
206
212
  return status;
207
213
  }
208
214
 
215
+ function enrichSessionIdle(session, nowMs = Date.now()) {
216
+ const idleSeconds = typeof session.idleSeconds === 'number'
217
+ ? session.idleSeconds
218
+ : lifecycle.computeIdleSeconds(session.lastActivityAt, nowMs);
219
+ return {
220
+ ...session,
221
+ idleSeconds,
222
+ idle_seconds: idleSeconds
223
+ };
224
+ }
225
+
226
+ function formatSessionStatusWithIdle(session) {
227
+ const base = formatSessionHealth(session);
228
+ const idleSeconds = typeof session.idleSeconds === 'number' ? session.idleSeconds : null;
229
+ if (idleSeconds !== null && idleSeconds > 60) {
230
+ return `${base} 💤 idle (${lifecycle.formatIdleDuration(idleSeconds)})`;
231
+ }
232
+ return base;
233
+ }
234
+
209
235
  function formatApiError(data, fallback = 'Request failed.') {
210
236
  if (!data) {
211
237
  return fallback;
@@ -914,7 +940,25 @@ async function main() {
914
940
 
915
941
  if (cmd === 'list') {
916
942
  try {
917
- const sessions = await discoverSessions({ silent: true });
943
+ let sessions = await discoverSessions({ silent: true });
944
+ // Bridge merge: surface supervisor-managed sessions discovered via
945
+ // filesystem manifest scan. De-dup with daemon entries by session id.
946
+ // Daemon path remains source-of-truth when both surfaces report the
947
+ // same session; bridge fills the gap when daemon is down (P2 #430).
948
+ try {
949
+ const bridgeSessions = require('./src/bridge/j3-shim').list();
950
+ const seenIds = new Set(sessions.map((s) => s.id));
951
+ for (const bs of bridgeSessions) {
952
+ if (!seenIds.has(bs.id)) {
953
+ sessions.push(bs);
954
+ seenIds.add(bs.id);
955
+ }
956
+ }
957
+ } catch {
958
+ // Best-effort: daemon list still surfaced above.
959
+ }
960
+ const nowMs = Date.now();
961
+ sessions = sessions.map((session) => enrichSessionIdle(session, nowMs));
918
962
  if (args.includes('--json')) {
919
963
  console.log(JSON.stringify(sessions, null, 2));
920
964
  return;
@@ -927,7 +971,7 @@ async function main() {
927
971
  console.log(` Command: ${s.command}`);
928
972
  const autoEmoji = s.autoState ? s.autoState.emoji : '';
929
973
  const autoLabel = s.autoState ? s.autoState.state : '';
930
- console.log(` Status: ${formatSessionHealth(s)}${autoLabel ? ` ${autoEmoji} ${autoLabel}` : ''}`);
974
+ console.log(` Status: ${formatSessionStatusWithIdle(s)}${autoLabel ? ` ${autoEmoji} ${autoLabel}` : ''}`);
931
975
  console.log(` Terminal: ${formatSessionTerminal(s)}`);
932
976
  console.log(` CWD: ${s.cwd}`);
933
977
  console.log(` Clients: ${s.active_clients}`);
@@ -977,6 +1021,24 @@ async function main() {
977
1021
  allowArgs.splice(idIndex, 2);
978
1022
  }
979
1023
 
1024
+ // Extract per-session idle TTL override
1025
+ let idleTtl = null;
1026
+ const idleTtlIndex = allowArgs.indexOf('--idle-ttl');
1027
+ if (idleTtlIndex !== -1) {
1028
+ if (!allowArgs[idleTtlIndex + 1]) {
1029
+ console.error('❌ Usage: telepty allow [--id <session_id>] [--idle-ttl <duration|off>] <command> [args...]');
1030
+ process.exit(1);
1031
+ }
1032
+ idleTtl = allowArgs[idleTtlIndex + 1];
1033
+ try {
1034
+ lifecycle.parseDuration(idleTtl, { fieldName: 'idle_ttl' });
1035
+ } catch (err) {
1036
+ console.error(`❌ ${err.message}`);
1037
+ process.exit(1);
1038
+ }
1039
+ allowArgs.splice(idleTtlIndex, 2);
1040
+ }
1041
+
980
1042
  // Extract --auto-restart flag
981
1043
  const autoRestartIndex = allowArgs.indexOf('--auto-restart');
982
1044
  const autoRestart = autoRestartIndex !== -1;
@@ -1037,7 +1099,9 @@ async function main() {
1037
1099
  cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
1038
1100
  cmux_surface_id: process.env.CMUX_SURFACE_ID || null,
1039
1101
  term_program: terminalProgram,
1040
- term: terminalType
1102
+ term: terminalType,
1103
+ owner_pid: process.pid,
1104
+ ...(idleTtl !== null ? { idle_ttl: idleTtl } : {})
1041
1105
  })
1042
1106
  });
1043
1107
  const data = await res.json();
@@ -1060,6 +1124,27 @@ async function main() {
1060
1124
  const MAX_CRASHES = 3;
1061
1125
  const DEATH_LOG_PATH = path.join(os.homedir(), '.telepty', 'logs', 'session-deaths.log');
1062
1126
 
1127
+ function updateDaemonProcessMetadata() {
1128
+ const body = {
1129
+ session_id: sessionId,
1130
+ command,
1131
+ cwd: process.cwd(),
1132
+ backend: detectedBackend,
1133
+ cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
1134
+ cmux_surface_id: process.env.CMUX_SURFACE_ID || null,
1135
+ term_program: terminalProgram,
1136
+ term: terminalType,
1137
+ owner_pid: process.pid,
1138
+ ...(child && child.pid ? { pty_pid: child.pid } : {}),
1139
+ ...(idleTtl !== null ? { idle_ttl: idleTtl } : {})
1140
+ };
1141
+ fetchWithAuth(`${DAEMON_URL}/api/sessions/register`, {
1142
+ method: 'POST',
1143
+ headers: { 'Content-Type': 'application/json' },
1144
+ body: JSON.stringify(body)
1145
+ }).catch(() => {});
1146
+ }
1147
+
1063
1148
  function logSessionDeath(exitCode, signal, duration) {
1064
1149
  try {
1065
1150
  fs.mkdirSync(path.dirname(DEATH_LOG_PATH), { recursive: true });
@@ -1105,6 +1190,7 @@ async function main() {
1105
1190
  env: sessionEnv
1106
1191
  });
1107
1192
  sessionStartTime = Date.now();
1193
+ updateDaemonProcessMetadata();
1108
1194
  return child;
1109
1195
  }
1110
1196
 
@@ -1239,7 +1325,10 @@ async function main() {
1239
1325
  cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
1240
1326
  cmux_surface_id: process.env.CMUX_SURFACE_ID || null,
1241
1327
  term_program: terminalProgram,
1242
- term: terminalType
1328
+ term: terminalType,
1329
+ owner_pid: process.pid,
1330
+ ...(child && child.pid ? { pty_pid: child.pid } : {}),
1331
+ ...(idleTtl !== null ? { idle_ttl: idleTtl } : {})
1243
1332
  })
1244
1333
  });
1245
1334
  } catch (e) {
@@ -1644,8 +1733,14 @@ async function main() {
1644
1733
  // caller is confident the target REPL is ready (e.g., orchestrator is
1645
1734
  // visibly idle). See specs/2026-05-02-submit-force-and-retry.md
1646
1735
  const submitForceIndex = args.indexOf('--submit-force');
1647
- const submitForce = submitForceIndex !== -1;
1648
- if (submitForce) args.splice(submitForceIndex, 1);
1736
+ const noSubmitForceIndex = args.indexOf('--no-submit-force');
1737
+ const explicitSubmitForce = submitForceIndex !== -1;
1738
+ const explicitNoSubmitForce = noSubmitForceIndex !== -1;
1739
+ for (const index of [submitForceIndex, noSubmitForceIndex].filter((i) => i !== -1).sort((a, b) => b - a)) {
1740
+ args.splice(index, 1);
1741
+ }
1742
+ const submitForceFromEnv = !explicitSubmitForce && !explicitNoSubmitForce && isSubmitForceDefaultEnabled();
1743
+ const submitForce = explicitSubmitForce || submitForceFromEnv;
1649
1744
 
1650
1745
  // Extract --submit-retry N flag (default 1, clamp [0, 3]). On a 504
1651
1746
  // gated-failure with a retry-safe reason (gate timed out and body is
@@ -1737,6 +1832,23 @@ async function main() {
1737
1832
  referencePath = reference.referencePath;
1738
1833
  }
1739
1834
 
1835
+ // Bridge-first attempt for local supervisor-managed sessions (P2 #430).
1836
+ // Gated submit semantics (render-gate, retry, submit-force) stay on
1837
+ // daemon.js — P2 wire does not carry those yet — so we only bridge the
1838
+ // basic inject path. Bridge failure (no manifest, supervisor crashed
1839
+ // mid-call, etc.) falls through to the daemon HTTP path below.
1840
+ if (!useSubmit) {
1841
+ const bridgeShim = require('./src/bridge/j3-shim');
1842
+ if (bridgeShim.findSupervisorManifest(target.id)) {
1843
+ const bridgeRes = await bridgeShim.inject(target.id, `${injectPrompt}\r`, {});
1844
+ if (bridgeRes.success) {
1845
+ const refSuffix = referencePath ? ` (ref: ${referencePath})` : '';
1846
+ console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m' (bridge).${refSuffix}`);
1847
+ return;
1848
+ }
1849
+ }
1850
+ }
1851
+
1740
1852
  const body = buildInjectRequestBody(injectPrompt, {
1741
1853
  fromId,
1742
1854
  replyTo,
@@ -1764,6 +1876,9 @@ async function main() {
1764
1876
  // an Enter that genuinely never landed cannot double-submit.
1765
1877
  // See docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md
1766
1878
  if (useSubmit) {
1879
+ if (submitForceFromEnv) {
1880
+ console.error('[telepty inject] submit-force=env-default (TELEPTY_SUBMIT_FORCE_DEFAULT=1)');
1881
+ }
1767
1882
  const submitBody = {
1768
1883
  injected_body: injectPrompt || '',
1769
1884
  retries: 1,
@@ -2208,10 +2323,111 @@ async function main() {
2208
2323
  return;
2209
2324
  }
2210
2325
 
2326
+ if (cmd === 'kill') {
2327
+ const killArgs = args.slice(1);
2328
+ const force = killArgs.includes('--force');
2329
+ const timeoutIndex = killArgs.indexOf('--timeout');
2330
+ let timeout = 5;
2331
+ if (timeoutIndex !== -1) {
2332
+ if (!killArgs[timeoutIndex + 1]) {
2333
+ console.error('❌ Usage: telepty kill <session-id> [--force] [--timeout <sec>]');
2334
+ process.exit(1);
2335
+ }
2336
+ timeout = Number(killArgs[timeoutIndex + 1]);
2337
+ if (!Number.isFinite(timeout) || timeout < 0) {
2338
+ console.error('❌ --timeout must be a non-negative number of seconds.');
2339
+ process.exit(1);
2340
+ }
2341
+ killArgs.splice(timeoutIndex, 2);
2342
+ }
2343
+ const filtered = killArgs.filter((item) => item !== '--force');
2344
+ const sessionRef = filtered[0];
2345
+ if (!sessionRef) { console.error('❌ Usage: telepty kill <session-id> [--force] [--timeout <sec>]'); process.exit(1); }
2346
+
2347
+ try {
2348
+ const target = await resolveSessionTarget(sessionRef);
2349
+ if (!target) { console.error(`❌ Session '${sessionRef}' not found.`); process.exit(1); }
2350
+ const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/kill`, {
2351
+ method: 'POST',
2352
+ headers: { 'Content-Type': 'application/json' },
2353
+ body: JSON.stringify({ force, timeout, source: 'cli' })
2354
+ });
2355
+ const data = await res.json();
2356
+ if (!res.ok) {
2357
+ console.error(`❌ Error: ${data.error || 'Failed to kill session.'}`);
2358
+ process.exit(1);
2359
+ }
2360
+ console.log(`✅ Session '\x1b[36m${target.id}\x1b[0m' killed${data.kill && data.kill.escalated ? ' (escalated)' : ''}.`);
2361
+ } catch (e) {
2362
+ console.error(`❌ ${e.message || 'Failed to kill session.'}`);
2363
+ process.exit(1);
2364
+ }
2365
+ return;
2366
+ }
2367
+
2211
2368
  if (cmd === 'clean') {
2212
2369
  try {
2370
+ const cleanArgs = args.slice(1);
2371
+ const dryRun = cleanArgs.includes('--dry-run');
2372
+ const idle = cleanArgs.includes('--idle');
2373
+ const olderThanIndex = cleanArgs.indexOf('--older-than');
2374
+ let olderThanMs = null;
2375
+ if (olderThanIndex !== -1) {
2376
+ if (!cleanArgs[olderThanIndex + 1]) {
2377
+ console.error('❌ Usage: telepty clean [--older-than <duration>] [--idle] [--dry-run]');
2378
+ process.exit(1);
2379
+ }
2380
+ try {
2381
+ olderThanMs = lifecycle.parseDuration(cleanArgs[olderThanIndex + 1], { fieldName: '--older-than' });
2382
+ } catch (err) {
2383
+ console.error(`❌ ${err.message}`);
2384
+ process.exit(1);
2385
+ }
2386
+ if (olderThanMs == null) {
2387
+ console.error('❌ --older-than must be a duration like 30m, 1h, or 2d.');
2388
+ process.exit(1);
2389
+ }
2390
+ }
2391
+
2213
2392
  const sessions = await discoverSessions({ silent: true });
2214
2393
  if (sessions.length === 0) { console.log('No sessions found.'); return; }
2394
+ if (olderThanMs !== null) {
2395
+ const targets = lifecycle.selectCleanOlderThanTargets(sessions, {
2396
+ olderThanMs,
2397
+ idle,
2398
+ nowMs: Date.now()
2399
+ });
2400
+ if (targets.length === 0) {
2401
+ console.log(`✅ No ${idle ? 'idle ' : ''}sessions older than ${cleanArgs[olderThanIndex + 1]} found.`);
2402
+ return;
2403
+ }
2404
+ if (dryRun) {
2405
+ targets.forEach((target) => {
2406
+ console.log(` Would remove: \x1b[36m${target.id}\x1b[0m (${target.reference}, ${Math.floor(target.ageSeconds / 60)}m old)`);
2407
+ });
2408
+ console.log(`✅ Dry run: ${targets.length} session(s) would be removed.`);
2409
+ return;
2410
+ }
2411
+
2412
+ let cleaned = 0;
2413
+ for (const target of targets) {
2414
+ try {
2415
+ const host = target.session.host || '127.0.0.1';
2416
+ const res = await fetchWithAuth(`${daemonUrl(host)}/api/sessions/${encodeURIComponent(target.id)}/kill`, {
2417
+ method: 'POST',
2418
+ headers: { 'Content-Type': 'application/json' },
2419
+ body: JSON.stringify({ force: false, timeout: 5, source: 'clean', reason: idle ? 'CLEAN_IDLE_OLDER_THAN' : 'CLEAN_OLDER_THAN' })
2420
+ });
2421
+ if (res.ok) {
2422
+ console.log(` 🗑 Removed session: \x1b[36m${target.id}\x1b[0m (${target.reference})`);
2423
+ cleaned++;
2424
+ }
2425
+ } catch (_) {}
2426
+ }
2427
+ console.log(cleaned > 0 ? `✅ Cleaned ${cleaned} session(s).` : '✅ No sessions cleaned.');
2428
+ return;
2429
+ }
2430
+
2215
2431
  let cleaned = 0;
2216
2432
  for (const s of sessions) {
2217
2433
  if (s.healthStatus === 'STALE' || s.healthStatus === 'DISCONNECTED') {
@@ -3171,10 +3387,12 @@ Discuss the following topic from your project's perspective. Engage with other s
3171
3387
  \x1b[1mSession Management:\x1b[0m
3172
3388
  telepty daemon Start the background daemon (port 3848)
3173
3389
  telepty spawn --id <id> <command> [args...] Spawn a new background session
3174
- telepty allow [--id <id>] [--auto-restart] <command> [args...] Wrap a CLI for remote control
3390
+ telepty allow [--id <id>] [--idle-ttl 1h|off] [--auto-restart] <command> [args...] Wrap a CLI for remote control
3175
3391
  telepty list [--json] List sessions (local + Tailnet)
3176
3392
  telepty attach [id[@host]] Attach interactively (picker if no ID)
3177
3393
  telepty rename <old_id[@host]> <new_id> Rename a session
3394
+ telepty kill <id[@host]> [--force] [--timeout N] Gracefully terminate a session
3395
+ telepty clean [--older-than 7d] [--idle] [--dry-run] Clean ghost or old sessions
3178
3396
  telepty session info <id[@host]> [--json] Show session metadata
3179
3397
 
3180
3398
  \x1b[1mInject & Communicate:\x1b[0m