@dmsdc-ai/aigentry-telepty 0.4.3 → 0.4.4
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 +329 -0
- package/README.md +17 -0
- package/cli.js +225 -7
- package/daemon.js +199 -1
- package/package.json +4 -4
- 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/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
|
package/daemon.js
CHANGED
|
@@ -16,6 +16,8 @@ const { SessionStateManager, STATE_DISPLAY, stripAnsi: stripAnsiState } = requir
|
|
|
16
16
|
const { classifyReportPrompt, buildAutoSummary } = require('./src/report-enforcement');
|
|
17
17
|
const submitGate = require('./src/submit-gate');
|
|
18
18
|
const readyRegistry = require('./src/prompt-symbol-registry');
|
|
19
|
+
const lifecycle = require('./src/lifecycle');
|
|
20
|
+
const { loadTeleptyConfig } = require('./src/config-file');
|
|
19
21
|
|
|
20
22
|
const config = getConfig();
|
|
21
23
|
const EXPECTED_TOKEN = config.authToken;
|
|
@@ -27,6 +29,7 @@ const SESSION_STALE_SECONDS = Math.max(1, Number(process.env.TELEPTY_SESSION_STA
|
|
|
27
29
|
const SESSION_CLEANUP_SECONDS = Math.max(SESSION_STALE_SECONDS, Number(process.env.TELEPTY_SESSION_CLEANUP_SECONDS || 300));
|
|
28
30
|
const DELIVERY_TIMEOUT_MS = Math.max(100, Number(process.env.TELEPTY_DELIVERY_TIMEOUT_MS || 5000));
|
|
29
31
|
const HEALTH_POLL_MS = Math.max(100, Number(process.env.TELEPTY_HEALTH_POLL_MS || 10000));
|
|
32
|
+
const IDLE_REAPER_POLL_MS = Math.max(100, Number(process.env.TELEPTY_IDLE_REAPER_POLL_MS || 60000));
|
|
30
33
|
const BOOTSTRAP_READY_TIMEOUT_MS = Math.max(500, Number(process.env.TELEPTY_BOOTSTRAP_READY_TIMEOUT_MS || 30000));
|
|
31
34
|
const WRAPPED_SUBMIT_DELAY_MS = 500;
|
|
32
35
|
|
|
@@ -139,7 +142,11 @@ function persistSessions() {
|
|
|
139
142
|
lastConnectedAt: s.lastConnectedAt || null,
|
|
140
143
|
lastDisconnectedAt: s.lastDisconnectedAt || null,
|
|
141
144
|
lastStateReportAt: s.lastStateReportAt || null,
|
|
142
|
-
stateReport: s.stateReport || null
|
|
145
|
+
stateReport: s.stateReport || null,
|
|
146
|
+
idleTtl: s.idleTtl || null,
|
|
147
|
+
idleTtlMs: s.idleTtlMs == null ? null : s.idleTtlMs,
|
|
148
|
+
ownerPid: s.ownerPid || null,
|
|
149
|
+
ptyPid: s.ptyPid || null
|
|
143
150
|
};
|
|
144
151
|
}
|
|
145
152
|
fs.mkdirSync(require('path').dirname(SESSION_PERSIST_PATH), { recursive: true });
|
|
@@ -266,6 +273,13 @@ const AUTO_REPORT_IDLE_SECONDS = Number(process.env.TELEPTY_AUTO_REPORT_IDLE_SEC
|
|
|
266
273
|
const sessions = {};
|
|
267
274
|
const handoffs = {};
|
|
268
275
|
const threads = {};
|
|
276
|
+
let teleptyConfig;
|
|
277
|
+
try {
|
|
278
|
+
teleptyConfig = loadTeleptyConfig();
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error(`[CONFIG] Failed to load telepty config: ${err.message}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
269
283
|
|
|
270
284
|
function broadcastBusEvent(event) {
|
|
271
285
|
const serialized = JSON.stringify(event);
|
|
@@ -357,6 +371,53 @@ function getSessionHealthReason(session, healthStatus) {
|
|
|
357
371
|
return session.ptyProcess && !session.ptyProcess.killed ? 'PTY_RUNNING' : 'PTY_EXITED';
|
|
358
372
|
}
|
|
359
373
|
|
|
374
|
+
function parseOptionalIdleTtl(body) {
|
|
375
|
+
if (!body || !Object.prototype.hasOwnProperty.call(body, 'idle_ttl')) {
|
|
376
|
+
return { present: false };
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
return {
|
|
380
|
+
present: true,
|
|
381
|
+
raw: body.idle_ttl == null ? 'off' : String(body.idle_ttl),
|
|
382
|
+
ms: lifecycle.parseDuration(body.idle_ttl == null ? 'off' : body.idle_ttl, { fieldName: 'idle_ttl' })
|
|
383
|
+
};
|
|
384
|
+
} catch (err) {
|
|
385
|
+
return { present: true, error: err.message };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function applyProcessMetadata(session, body) {
|
|
390
|
+
if (!session || !body) return;
|
|
391
|
+
const ownerPid = Number(body.owner_pid);
|
|
392
|
+
const ptyPid = Number(body.pty_pid);
|
|
393
|
+
if (Number.isInteger(ownerPid) && ownerPid > 0) {
|
|
394
|
+
session.ownerPid = ownerPid;
|
|
395
|
+
}
|
|
396
|
+
if (Number.isInteger(ptyPid) && ptyPid > 0) {
|
|
397
|
+
session.ptyPid = ptyPid;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function applyIdleTtlMetadata(session, parsedIdleTtl) {
|
|
402
|
+
if (!session || !parsedIdleTtl || !parsedIdleTtl.present || parsedIdleTtl.error) return;
|
|
403
|
+
session.idleTtl = parsedIdleTtl.raw;
|
|
404
|
+
session.idleTtlMs = parsedIdleTtl.ms;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function applyTimestampMetadata(session, body) {
|
|
408
|
+
if (!session || !body) return;
|
|
409
|
+
for (const [field, prop] of [
|
|
410
|
+
['created_at', 'createdAt'],
|
|
411
|
+
['last_activity_at', 'lastActivityAt']
|
|
412
|
+
]) {
|
|
413
|
+
if (!Object.prototype.hasOwnProperty.call(body, field)) continue;
|
|
414
|
+
const value = body[field] == null ? null : String(body[field]);
|
|
415
|
+
if (value && Number.isFinite(new Date(value).getTime())) {
|
|
416
|
+
session[prop] = value;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
360
421
|
function sleep(ms) {
|
|
361
422
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
362
423
|
}
|
|
@@ -1104,6 +1165,11 @@ function serializeSession(id, session, options = {}) {
|
|
|
1104
1165
|
healthReason,
|
|
1105
1166
|
disconnectedSeconds: disconnectedMs === null ? null : Math.floor(disconnectedMs / 1000),
|
|
1106
1167
|
lastStateReportAt: session.lastStateReportAt || null,
|
|
1168
|
+
idleTtl: session.idleTtl || null,
|
|
1169
|
+
idleTtlMs: session.idleTtlMs == null ? null : session.idleTtlMs,
|
|
1170
|
+
effectiveIdleTtlMs: lifecycle.effectiveIdleTtlMs(session, teleptyConfig),
|
|
1171
|
+
ownerPid: session.ownerPid || null,
|
|
1172
|
+
ptyPid: session.ptyPid || (session.ptyProcess && session.ptyProcess.pid) || null,
|
|
1107
1173
|
transport,
|
|
1108
1174
|
semantic,
|
|
1109
1175
|
autoState: autoState ? {
|
|
@@ -1123,6 +1189,53 @@ function serializeSession(id, session, options = {}) {
|
|
|
1123
1189
|
};
|
|
1124
1190
|
}
|
|
1125
1191
|
|
|
1192
|
+
async function teardownSessionById(id, options = {}) {
|
|
1193
|
+
const session = sessions[id];
|
|
1194
|
+
if (!session) {
|
|
1195
|
+
return { success: false, httpStatus: 404, error: 'Session not found' };
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const timeoutMs = Math.max(0, Number(options.timeoutMs ?? 5000));
|
|
1199
|
+
const force = options.force === true;
|
|
1200
|
+
const reason = options.reason || (force ? 'manual_force' : 'manual');
|
|
1201
|
+
session.isClosing = true;
|
|
1202
|
+
|
|
1203
|
+
const kill = await lifecycle.killSessionProcess(session, { timeoutMs, force });
|
|
1204
|
+
emitSessionLifecycleEvent('session_closed', id, session, {
|
|
1205
|
+
reason,
|
|
1206
|
+
force,
|
|
1207
|
+
pid: kill.pid,
|
|
1208
|
+
signal: kill.signal || null,
|
|
1209
|
+
escalated: kill.escalated === true,
|
|
1210
|
+
source: options.source || 'daemon'
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
if (session.clients) {
|
|
1214
|
+
session.clients.forEach(ws => {
|
|
1215
|
+
try { ws.close(1000, 'Session destroyed'); } catch {}
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
if (session.ownerWs) {
|
|
1219
|
+
try { session.ownerWs.close(1000, 'Session destroyed'); } catch {}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
delete sessions[id];
|
|
1223
|
+
sessionStateManager.unregister(id);
|
|
1224
|
+
try { mailbox.purge(id); } catch {}
|
|
1225
|
+
lifecycle.cleanupSessionArtifacts(id);
|
|
1226
|
+
persistSessions();
|
|
1227
|
+
|
|
1228
|
+
return {
|
|
1229
|
+
success: true,
|
|
1230
|
+
session_id: id,
|
|
1231
|
+
status: 'closed',
|
|
1232
|
+
reason,
|
|
1233
|
+
force,
|
|
1234
|
+
timeout_ms: timeoutMs,
|
|
1235
|
+
kill
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1126
1239
|
// Detect terminal environment at daemon startup
|
|
1127
1240
|
const DETECTED_TERMINAL = terminalBackend.detectTerminal();
|
|
1128
1241
|
console.log(`[DAEMON] Terminal backend: ${DETECTED_TERMINAL}`);
|
|
@@ -1145,6 +1258,10 @@ for (const [id, meta] of Object.entries(_persisted)) {
|
|
|
1145
1258
|
lastDisconnectedAt: meta.lastDisconnectedAt || meta.lastActivityAt || new Date().toISOString(),
|
|
1146
1259
|
lastStateReportAt: meta.lastStateReportAt || null,
|
|
1147
1260
|
stateReport: meta.stateReport || null,
|
|
1261
|
+
idleTtl: meta.idleTtl || null,
|
|
1262
|
+
idleTtlMs: meta.idleTtlMs == null ? null : meta.idleTtlMs,
|
|
1263
|
+
ownerPid: meta.ownerPid || null,
|
|
1264
|
+
ptyPid: meta.ptyPid || null,
|
|
1148
1265
|
clients: new Set(), isClosing: false, outputRing: [], ready: true, };
|
|
1149
1266
|
initializeBootstrapState(sessions[id]);
|
|
1150
1267
|
console.log(`[PERSIST] Restored session ${id} (awaiting reconnect)`);
|
|
@@ -1242,6 +1359,7 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
1242
1359
|
id: session_id,
|
|
1243
1360
|
type: 'spawned',
|
|
1244
1361
|
ptyProcess,
|
|
1362
|
+
ptyPid: ptyProcess.pid || null,
|
|
1245
1363
|
command,
|
|
1246
1364
|
cwd,
|
|
1247
1365
|
createdAt: new Date().toISOString(),
|
|
@@ -1311,6 +1429,10 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
1311
1429
|
app.post('/api/sessions/register', (req, res) => {
|
|
1312
1430
|
const { session_id, command, cwd = process.cwd(), backend, cmux_workspace_id, cmux_surface_id, term_program, term } = req.body;
|
|
1313
1431
|
if (!session_id) return res.status(400).json({ error: 'session_id is required' });
|
|
1432
|
+
const parsedIdleTtl = parseOptionalIdleTtl(req.body);
|
|
1433
|
+
if (parsedIdleTtl.error) {
|
|
1434
|
+
return res.status(400).json({ error: parsedIdleTtl.error, code: 'INVALID_IDLE_TTL' });
|
|
1435
|
+
}
|
|
1314
1436
|
// Idempotent: allow re-registration (update command/cwd, keep clients)
|
|
1315
1437
|
if (sessions[session_id]) {
|
|
1316
1438
|
const existing = sessions[session_id];
|
|
@@ -1333,6 +1455,9 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
1333
1455
|
existing.ready = true;
|
|
1334
1456
|
markSessionConnected(existing);
|
|
1335
1457
|
}
|
|
1458
|
+
applyProcessMetadata(existing, req.body);
|
|
1459
|
+
applyIdleTtlMetadata(existing, parsedIdleTtl);
|
|
1460
|
+
applyTimestampMetadata(existing, req.body);
|
|
1336
1461
|
initializeBootstrapState(existing);
|
|
1337
1462
|
console.log(`[REGISTER] Re-registered session ${session_id} (type: ${existing.type}, updated metadata)`);
|
|
1338
1463
|
return res.status(200).json({ session_id, type: existing.type, command: existing.command, cwd: existing.cwd, reregistered: true });
|
|
@@ -1360,12 +1485,17 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
1360
1485
|
lastDisconnectedAt: delivery_type === 'aterm' ? null : new Date().toISOString(),
|
|
1361
1486
|
lastStateReportAt: null,
|
|
1362
1487
|
stateReport: null,
|
|
1488
|
+
idleTtl: parsedIdleTtl.present ? parsedIdleTtl.raw : null,
|
|
1489
|
+
idleTtlMs: parsedIdleTtl.present ? parsedIdleTtl.ms : null,
|
|
1490
|
+
ownerPid: Number.isInteger(Number(req.body.owner_pid)) && Number(req.body.owner_pid) > 0 ? Number(req.body.owner_pid) : null,
|
|
1491
|
+
ptyPid: Number.isInteger(Number(req.body.pty_pid)) && Number(req.body.pty_pid) > 0 ? Number(req.body.pty_pid) : null,
|
|
1363
1492
|
clients: new Set(),
|
|
1364
1493
|
isClosing: false,
|
|
1365
1494
|
outputRing: [],
|
|
1366
1495
|
ready: true, // unknown commands remain injectable once registered (#150)
|
|
1367
1496
|
};
|
|
1368
1497
|
initializeBootstrapState(sessionRecord);
|
|
1498
|
+
applyTimestampMetadata(sessionRecord, req.body);
|
|
1369
1499
|
// Check for existing session with same base alias and emit replaced event
|
|
1370
1500
|
const baseAlias = session_id.replace(/-\d+$/, '');
|
|
1371
1501
|
const replaced = Object.keys(sessions).find(id => {
|
|
@@ -2364,6 +2494,35 @@ app.patch('/api/sessions/:id', (req, res) => {
|
|
|
2364
2494
|
res.json({ success: true, old_id: id, new_id });
|
|
2365
2495
|
});
|
|
2366
2496
|
|
|
2497
|
+
app.post('/api/sessions/:id/kill', async (req, res) => {
|
|
2498
|
+
const requestedId = req.params.id;
|
|
2499
|
+
const resolvedId = resolveSessionAlias(requestedId);
|
|
2500
|
+
if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
|
|
2501
|
+
|
|
2502
|
+
try {
|
|
2503
|
+
const timeoutSeconds = req.body && req.body.timeout != null
|
|
2504
|
+
? Number(req.body.timeout)
|
|
2505
|
+
: (req.body && req.body.timeout_sec != null ? Number(req.body.timeout_sec) : 5);
|
|
2506
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
|
|
2507
|
+
return res.status(400).json({ error: 'timeout must be a non-negative number of seconds', code: 'INVALID_TIMEOUT' });
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
const result = await teardownSessionById(resolvedId, {
|
|
2511
|
+
force: req.body && req.body.force === true,
|
|
2512
|
+
timeoutMs: Math.floor(timeoutSeconds * 1000),
|
|
2513
|
+
reason: req.body && req.body.reason ? String(req.body.reason) : 'manual',
|
|
2514
|
+
source: req.body && req.body.source ? String(req.body.source) : 'api'
|
|
2515
|
+
});
|
|
2516
|
+
if (!result.success) {
|
|
2517
|
+
return res.status(result.httpStatus || 500).json({ error: result.error || 'Failed to kill session' });
|
|
2518
|
+
}
|
|
2519
|
+
console.log(`[KILL] Session ${resolvedId} closed (reason=${result.reason}, force=${result.force}, pid=${result.kill.pid || 'none'})`);
|
|
2520
|
+
res.json(result);
|
|
2521
|
+
} catch (err) {
|
|
2522
|
+
res.status(500).json({ error: err.message || 'Failed to kill session' });
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2367
2526
|
app.delete('/api/sessions/:id', (req, res) => {
|
|
2368
2527
|
const requestedId = req.params.id;
|
|
2369
2528
|
const resolvedId = resolveSessionAlias(requestedId);
|
|
@@ -2381,6 +2540,7 @@ app.delete('/api/sessions/:id', (req, res) => {
|
|
|
2381
2540
|
delete sessions[id];
|
|
2382
2541
|
sessionStateManager.unregister(id);
|
|
2383
2542
|
try { mailbox.purge(id); } catch {}
|
|
2543
|
+
lifecycle.cleanupSessionArtifacts(id);
|
|
2384
2544
|
console.log(`[KILL] Session ${id} removed`);
|
|
2385
2545
|
persistSessions();
|
|
2386
2546
|
res.json({ success: true, status: 'closing' });
|
|
@@ -2389,6 +2549,7 @@ app.delete('/api/sessions/:id', (req, res) => {
|
|
|
2389
2549
|
delete sessions[id];
|
|
2390
2550
|
sessionStateManager.unregister(id);
|
|
2391
2551
|
try { mailbox.purge(id); } catch {}
|
|
2552
|
+
lifecycle.cleanupSessionArtifacts(id);
|
|
2392
2553
|
persistSessions();
|
|
2393
2554
|
console.log(`[KILL] Session ${id} force-removed (process cleanup error: ${err.message})`);
|
|
2394
2555
|
res.json({ success: true, status: 'force-removed' });
|
|
@@ -2769,6 +2930,43 @@ if (staleBroken > 0) {
|
|
|
2769
2930
|
mailboxDelivery.start();
|
|
2770
2931
|
|
|
2771
2932
|
const IDLE_THRESHOLD_SECONDS = 60;
|
|
2933
|
+
async function runIdleTtlSweep(nowMs = Date.now()) {
|
|
2934
|
+
const victims = lifecycle.selectIdleTtlVictims(sessions, teleptyConfig, { nowMs });
|
|
2935
|
+
for (const victim of victims) {
|
|
2936
|
+
const session = sessions[victim.id];
|
|
2937
|
+
if (!session || session._idleTtlKilling) continue;
|
|
2938
|
+
session._idleTtlKilling = true;
|
|
2939
|
+
broadcastSessionEvent('tracing', victim.id, session, {
|
|
2940
|
+
nowMs,
|
|
2941
|
+
extra: {
|
|
2942
|
+
action: 'idle_ttl_auto_kill',
|
|
2943
|
+
reason: 'IDLE_TTL',
|
|
2944
|
+
idle_duration: victim.idleSeconds,
|
|
2945
|
+
idle_duration_seconds: victim.idleSeconds,
|
|
2946
|
+
idle_ttl_ms: victim.ttlMs
|
|
2947
|
+
}
|
|
2948
|
+
});
|
|
2949
|
+
try {
|
|
2950
|
+
await teardownSessionById(victim.id, {
|
|
2951
|
+
force: false,
|
|
2952
|
+
timeoutMs: 5000,
|
|
2953
|
+
reason: 'IDLE_TTL',
|
|
2954
|
+
source: 'idle_reaper'
|
|
2955
|
+
});
|
|
2956
|
+
console.log(`[REAPER] Auto-killed ${victim.id} after ${victim.idleSeconds}s idle (ttl=${victim.ttlMs}ms)`);
|
|
2957
|
+
} catch (err) {
|
|
2958
|
+
session._idleTtlKilling = false;
|
|
2959
|
+
console.error(`[REAPER] Failed to auto-kill ${victim.id}: ${err.message}`);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
setInterval(() => {
|
|
2965
|
+
runIdleTtlSweep().catch((err) => {
|
|
2966
|
+
console.error(`[REAPER] Idle TTL sweep failed: ${err.message}`);
|
|
2967
|
+
});
|
|
2968
|
+
}, IDLE_REAPER_POLL_MS);
|
|
2969
|
+
|
|
2772
2970
|
setInterval(() => {
|
|
2773
2971
|
const now = Date.now();
|
|
2774
2972
|
for (const [id, session] of Object.entries(sessions)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-telepty",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"main": "daemon.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aigentry-telepty": "install.js",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"CHANGELOG.md"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
-
"test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
37
|
-
"test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js",
|
|
38
|
-
"test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
36
|
+
"test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
37
|
+
"test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js",
|
|
38
|
+
"test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
39
39
|
"regen-fixtures": "node scripts/regen-snippet-fixtures.js"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|