@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/CHANGELOG.md +385 -0
- package/README.md +17 -0
- package/cli.js +225 -7
- package/daemon.js +240 -3
- package/package.json +6 -4
- package/scripts/postinstall.js +94 -0
- package/src/bridge/j3-shim.js +264 -0
- package/src/bridge/supervisor-ipc.js +330 -0
- package/src/bridge/supervisor-launcher.js +193 -0
- package/src/config-file.js +86 -0
- package/src/lifecycle.js +237 -0
- package/src/prompt-symbol-registry.js +34 -4
- package/src/submit-gate.js +7 -1
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
|
-
|
|
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: ${
|
|
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
|
|
1648
|
-
|
|
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
|