@dmsdc-ai/aigentry-telepty 0.1.75 → 0.1.77
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/.claude/commands/telepty-inject.md +45 -7
- package/cli.js +559 -59
- package/cross-machine.js +68 -8
- package/daemon.js +697 -441
- package/package.json +1 -1
- package/session-routing.js +7 -5
- package/shared-context.js +147 -0
package/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ const { cleanupDaemonProcesses } = require('./daemon-control');
|
|
|
15
15
|
const { attachInteractiveTerminal, getTerminalSize, restoreTerminalModes } = require('./interactive-terminal');
|
|
16
16
|
const { getRuntimeInfo } = require('./runtime-info');
|
|
17
17
|
const { formatHostLabel, groupSessionsByHost, pickSessionTarget } = require('./session-routing');
|
|
18
|
+
const { buildSharedContextPrompt, createSharedContextDescriptor, ensureSharedContextFile } = require('./shared-context');
|
|
18
19
|
const { runInteractiveSkillInstaller } = require('./skill-installer');
|
|
19
20
|
const crossMachine = require('./cross-machine');
|
|
20
21
|
const args = process.argv.slice(2);
|
|
@@ -144,6 +145,195 @@ async function getDaemonMeta(host = REMOTE_HOST) {
|
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
function detectTerminalProgram(env = process.env) {
|
|
149
|
+
const rawTermProgram = typeof env.TERM_PROGRAM === 'string' ? env.TERM_PROGRAM.trim() : '';
|
|
150
|
+
if (rawTermProgram) {
|
|
151
|
+
return rawTermProgram;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (env.TMUX) {
|
|
155
|
+
return 'tmux';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const term = typeof env.TERM === 'string' ? env.TERM.toLowerCase() : '';
|
|
159
|
+
if (term.includes('kitty')) return 'kitty';
|
|
160
|
+
if (term.includes('ghostty')) return 'ghostty';
|
|
161
|
+
if (term.includes('tmux')) return 'tmux';
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function formatSessionTerminal(session) {
|
|
167
|
+
const terminal = session.terminal || session.termProgram || null;
|
|
168
|
+
const term = session.term || null;
|
|
169
|
+
if (terminal && term) {
|
|
170
|
+
return `${terminal} (${term})`;
|
|
171
|
+
}
|
|
172
|
+
return terminal || term || 'unknown';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatSessionHealth(session) {
|
|
176
|
+
const status = session.healthStatus || 'UNKNOWN';
|
|
177
|
+
const reason = session.healthReason || null;
|
|
178
|
+
if (reason && reason !== status) {
|
|
179
|
+
return `${status} (${reason})`;
|
|
180
|
+
}
|
|
181
|
+
return status;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatApiError(data, fallback = 'Request failed.') {
|
|
185
|
+
if (!data) {
|
|
186
|
+
return fallback;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const code = data.code ? `[${data.code}] ` : '';
|
|
190
|
+
const message = data.error || fallback;
|
|
191
|
+
return `${code}${message}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildInjectRequestBody(prompt, options = {}) {
|
|
195
|
+
const body = {
|
|
196
|
+
prompt,
|
|
197
|
+
no_enter: options.noEnter === true
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (options.fromId) body.from = options.fromId;
|
|
201
|
+
if (options.replyTo) body.reply_to = options.replyTo;
|
|
202
|
+
if (options.replyExpected) body.reply_expected = true;
|
|
203
|
+
|
|
204
|
+
return body;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildSessionStateReportBody(options = {}) {
|
|
208
|
+
const body = {
|
|
209
|
+
phase: options.phase,
|
|
210
|
+
source: options.source || 'self_report'
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (options.currentTask !== undefined) body.current_task = options.currentTask;
|
|
214
|
+
if (options.blocker !== undefined) body.blocker = options.blocker;
|
|
215
|
+
if (options.needsInput !== undefined) body.needs_input = options.needsInput;
|
|
216
|
+
if (options.threadId !== undefined) body.thread_id = options.threadId;
|
|
217
|
+
if (options.seq !== undefined) body.seq = options.seq;
|
|
218
|
+
|
|
219
|
+
return body;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function splitSessionsByTransport(sessions) {
|
|
223
|
+
const local = [];
|
|
224
|
+
const remoteByPeer = new Map();
|
|
225
|
+
|
|
226
|
+
for (const session of sessions) {
|
|
227
|
+
if (!isRemoteSession(session)) {
|
|
228
|
+
local.push(session);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const peerName = session.peerName || session.host;
|
|
233
|
+
if (!remoteByPeer.has(peerName)) {
|
|
234
|
+
remoteByPeer.set(peerName, []);
|
|
235
|
+
}
|
|
236
|
+
remoteByPeer.get(peerName).push(session);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { local, remoteByPeer };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function parseRefOption(argv) {
|
|
243
|
+
const refIndex = argv.indexOf('--ref');
|
|
244
|
+
if (refIndex === -1) {
|
|
245
|
+
return { useRef: false, refFilePath: null };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
argv.splice(refIndex, 1);
|
|
249
|
+
const candidate = argv[refIndex];
|
|
250
|
+
if (!candidate || candidate.startsWith('--')) {
|
|
251
|
+
return { useRef: true, refFilePath: null };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
if (fs.statSync(candidate).isFile()) {
|
|
256
|
+
argv.splice(refIndex, 1);
|
|
257
|
+
return { useRef: true, refFilePath: candidate };
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
// Fall through to inline ref mode when the candidate is not a readable file.
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { useRef: true, refFilePath: null };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function createSharedReferenceDescriptor(prompt, refFilePath) {
|
|
267
|
+
if (refFilePath) {
|
|
268
|
+
const fileContent = fs.readFileSync(refFilePath, 'utf8');
|
|
269
|
+
return createSharedContextDescriptor(fileContent);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return createSharedContextDescriptor(prompt);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function buildSharedReferenceInjectPrompt(referencePath, message = '') {
|
|
276
|
+
const basePrompt = buildSharedContextPrompt(referencePath);
|
|
277
|
+
const normalizedMessage = String(message ?? '').trim();
|
|
278
|
+
return normalizedMessage ? `${basePrompt} ${normalizedMessage}` : basePrompt;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function ensureLocalSharedReference(descriptor, message = '') {
|
|
282
|
+
const reference = ensureSharedContextFile(descriptor);
|
|
283
|
+
return {
|
|
284
|
+
descriptor: reference,
|
|
285
|
+
referencePath: reference.promptPath,
|
|
286
|
+
prompt: buildSharedReferenceInjectPrompt(reference.promptPath, message)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function ensureRemoteSharedReference(peerName, descriptor, message = '') {
|
|
291
|
+
const result = crossMachine.remoteEnsureSharedContext(peerName, descriptor);
|
|
292
|
+
if (!result.success) {
|
|
293
|
+
throw new Error(result.error || `Failed to prepare shared context on ${peerName}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const referencePath = result.promptPath || descriptor.promptPath;
|
|
297
|
+
return {
|
|
298
|
+
descriptor,
|
|
299
|
+
referencePath,
|
|
300
|
+
prompt: buildSharedReferenceInjectPrompt(referencePath, message)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function printSessionInfo(session, options = {}) {
|
|
305
|
+
const host = options.host || session.host || '127.0.0.1';
|
|
306
|
+
console.log('\x1b[1mSession Info:\x1b[0m');
|
|
307
|
+
console.log(` - ID: \x1b[36m${session.id}\x1b[0m`);
|
|
308
|
+
console.log(` Host: ${formatHostLabel(host)}`);
|
|
309
|
+
console.log(` Command: ${session.command}`);
|
|
310
|
+
console.log(` Type: ${session.type || 'unknown'}`);
|
|
311
|
+
console.log(` Status: ${formatSessionHealth(session)}`);
|
|
312
|
+
console.log(` Terminal: ${session.terminal || session.termProgram || 'unknown'}`);
|
|
313
|
+
console.log(` TERM: ${session.term || 'n/a'}`);
|
|
314
|
+
console.log(` CWD: ${session.cwd}`);
|
|
315
|
+
console.log(` Clients: ${session.active_clients ?? 0}`);
|
|
316
|
+
if (session.createdAt) {
|
|
317
|
+
console.log(` Started: ${new Date(session.createdAt).toLocaleString()}`);
|
|
318
|
+
}
|
|
319
|
+
if (session.lastActivityAt) {
|
|
320
|
+
console.log(` Last Activity: ${new Date(session.lastActivityAt).toLocaleString()}`);
|
|
321
|
+
}
|
|
322
|
+
if (typeof session.idleSeconds === 'number') {
|
|
323
|
+
console.log(` Idle: ${session.idleSeconds}s`);
|
|
324
|
+
}
|
|
325
|
+
if (session.semantic && session.semantic.phase) {
|
|
326
|
+
console.log(` Phase: ${session.semantic.phase}`);
|
|
327
|
+
}
|
|
328
|
+
if (session.semantic && session.semantic.current_task) {
|
|
329
|
+
console.log(` Current Task: ${session.semantic.current_task}`);
|
|
330
|
+
}
|
|
331
|
+
if (session.semantic && session.semantic.blocker) {
|
|
332
|
+
console.log(` Blocker: ${session.semantic.blocker}`);
|
|
333
|
+
}
|
|
334
|
+
console.log('');
|
|
335
|
+
}
|
|
336
|
+
|
|
147
337
|
function startDetachedDaemon() {
|
|
148
338
|
const cp = spawn(process.argv[0], [process.argv[1], 'daemon'], {
|
|
149
339
|
detached: true,
|
|
@@ -423,7 +613,7 @@ async function manageInteractive() {
|
|
|
423
613
|
console.log('\x1b[1mAvailable Sessions:\x1b[0m');
|
|
424
614
|
sessions.forEach(s => {
|
|
425
615
|
const hostLabel = formatHostLabel(s.host);
|
|
426
|
-
console.log(` - \x1b[36m${s.id}\x1b[0m (\x1b[33m${hostLabel}\x1b[0m) [${s.command}] - Clients: ${s.active_clients}`);
|
|
616
|
+
console.log(` - \x1b[36m${s.id}\x1b[0m (\x1b[33m${hostLabel}\x1b[0m) [${s.command}] - Status: ${s.healthStatus || 'UNKNOWN'} - Clients: ${s.active_clients}`);
|
|
427
617
|
});
|
|
428
618
|
}
|
|
429
619
|
console.log('\n');
|
|
@@ -530,7 +720,7 @@ async function manageInteractive() {
|
|
|
530
720
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: promptText })
|
|
531
721
|
});
|
|
532
722
|
const data = await res.json();
|
|
533
|
-
if (!res.ok) console.error(`\n❌
|
|
723
|
+
if (!res.ok) console.error(`\n❌ ${formatApiError(data)}\n`);
|
|
534
724
|
else console.log(`\n✅ Injected successfully into '\x1b[36m${target.id}\x1b[0m'.\n`);
|
|
535
725
|
} catch (e) { console.error('\n❌ Failed to connect.\n'); }
|
|
536
726
|
continue;
|
|
@@ -589,12 +779,18 @@ async function main() {
|
|
|
589
779
|
if (cmd === 'list') {
|
|
590
780
|
try {
|
|
591
781
|
const sessions = await discoverSessions({ silent: true });
|
|
782
|
+
if (args.includes('--json')) {
|
|
783
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
592
786
|
if (sessions.length === 0) { console.log('No active sessions found.'); return; }
|
|
593
787
|
console.log('\x1b[1mActive Sessions:\x1b[0m');
|
|
594
788
|
sessions.forEach(s => {
|
|
595
789
|
console.log(` - ID: \x1b[36m${s.id}\x1b[0m`);
|
|
596
790
|
console.log(` Host: ${formatHostLabel(s.host)}`);
|
|
597
791
|
console.log(` Command: ${s.command}`);
|
|
792
|
+
console.log(` Status: ${formatSessionHealth(s)}`);
|
|
793
|
+
console.log(` Terminal: ${formatSessionTerminal(s)}`);
|
|
598
794
|
console.log(` CWD: ${s.cwd}`);
|
|
599
795
|
console.log(` Clients: ${s.active_clients}`);
|
|
600
796
|
console.log(` Started: ${new Date(s.createdAt).toLocaleString()}`);
|
|
@@ -688,6 +884,8 @@ async function main() {
|
|
|
688
884
|
const detectedBackend = process.env.CMUX_WORKSPACE_ID ? 'cmux' : (findKittySocketCli() ? 'kitty' : 'pty');
|
|
689
885
|
|
|
690
886
|
// Register session with daemon
|
|
887
|
+
const terminalProgram = detectTerminalProgram(process.env);
|
|
888
|
+
const terminalType = process.env.TERM || null;
|
|
691
889
|
try {
|
|
692
890
|
const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/register`, {
|
|
693
891
|
method: 'POST',
|
|
@@ -698,7 +896,9 @@ async function main() {
|
|
|
698
896
|
cwd: process.cwd(),
|
|
699
897
|
backend: detectedBackend,
|
|
700
898
|
cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
|
|
701
|
-
cmux_surface_id: process.env.CMUX_SURFACE_ID || null
|
|
899
|
+
cmux_surface_id: process.env.CMUX_SURFACE_ID || null,
|
|
900
|
+
term_program: terminalProgram,
|
|
901
|
+
term: terminalType
|
|
702
902
|
})
|
|
703
903
|
});
|
|
704
904
|
const data = await res.json();
|
|
@@ -843,7 +1043,9 @@ async function main() {
|
|
|
843
1043
|
cwd: process.cwd(),
|
|
844
1044
|
backend: detectedBackend,
|
|
845
1045
|
cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
|
|
846
|
-
cmux_surface_id: process.env.CMUX_SURFACE_ID || null
|
|
1046
|
+
cmux_surface_id: process.env.CMUX_SURFACE_ID || null,
|
|
1047
|
+
term_program: terminalProgram,
|
|
1048
|
+
term: terminalType
|
|
847
1049
|
})
|
|
848
1050
|
});
|
|
849
1051
|
} catch (e) {
|
|
@@ -868,24 +1070,35 @@ async function main() {
|
|
|
868
1070
|
try {
|
|
869
1071
|
const msg = JSON.parse(message);
|
|
870
1072
|
if (msg.type === 'inject') {
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1073
|
+
const chunks = [];
|
|
1074
|
+
const rawData = typeof msg.data === 'string' ? msg.data : String(msg.data ?? '');
|
|
1075
|
+
// Keep text+CR combined — do NOT split them.
|
|
1076
|
+
chunks.push(rawData);
|
|
1077
|
+
|
|
1078
|
+
for (const chunk of chunks) {
|
|
1079
|
+
if (!chunk) {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const isCr = chunk === '\r';
|
|
1084
|
+
if (isCr && injectQueue.length > 0) {
|
|
1085
|
+
// CR with pending queued text — queue CR too and flush immediately.
|
|
1086
|
+
injectQueue.push(chunk);
|
|
1087
|
+
if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
|
|
1088
|
+
flushInjectQueue();
|
|
1089
|
+
} else if (isCr) {
|
|
1090
|
+
// CR always written immediately — never idle-gated.
|
|
1091
|
+
child.write(chunk);
|
|
1092
|
+
} else if (isIdle()) {
|
|
1093
|
+
// Text when idle — write immediately.
|
|
1094
|
+
child.write(chunk);
|
|
1095
|
+
promptReady = false;
|
|
1096
|
+
lastInjectTextTime = Date.now();
|
|
1097
|
+
} else {
|
|
1098
|
+
// Text when not idle — queue for safe delivery.
|
|
1099
|
+
injectQueue.push(chunk);
|
|
1100
|
+
scheduleIdleFlush();
|
|
1101
|
+
}
|
|
889
1102
|
}
|
|
890
1103
|
} else if (msg.type === 'resize') {
|
|
891
1104
|
child.resize(msg.cols, msg.rows);
|
|
@@ -1201,10 +1414,17 @@ async function main() {
|
|
|
1201
1414
|
}
|
|
1202
1415
|
|
|
1203
1416
|
if (cmd === 'inject') {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1417
|
+
const { useRef, refFilePath } = parseRefOption(args);
|
|
1418
|
+
|
|
1419
|
+
if (args.includes('--no-enter')) {
|
|
1420
|
+
console.error('❌ telepty inject always submits after text. Use `telepty enter <session_id>` to send Enter only.');
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Extract --submit flag (terminal-level submit instead of deferred PTY CR)
|
|
1425
|
+
const submitIndex = args.indexOf('--submit');
|
|
1426
|
+
const useSubmit = submitIndex !== -1;
|
|
1427
|
+
if (useSubmit) args.splice(submitIndex, 1);
|
|
1208
1428
|
|
|
1209
1429
|
// Extract --from flag
|
|
1210
1430
|
let fromId;
|
|
@@ -1229,8 +1449,10 @@ async function main() {
|
|
|
1229
1449
|
const replyExpected = replyExpectedIndex !== -1;
|
|
1230
1450
|
if (replyExpected) args.splice(replyExpectedIndex, 1);
|
|
1231
1451
|
|
|
1232
|
-
const sessionId = args[1];
|
|
1233
|
-
|
|
1452
|
+
const sessionId = args[1];
|
|
1453
|
+
const hasPromptArgument = args.length >= 3;
|
|
1454
|
+
const prompt = args.slice(2).join(' ');
|
|
1455
|
+
if (!sessionId || (!refFilePath && !hasPromptArgument)) { console.error('❌ Usage: telepty inject [--ref [file]] [--from <id>] [--reply-to <id>] <session_id> "<prompt text>"'); process.exit(1); }
|
|
1234
1456
|
try {
|
|
1235
1457
|
const target = await resolveSessionTarget(sessionId);
|
|
1236
1458
|
if (!target) {
|
|
@@ -1238,6 +1460,10 @@ async function main() {
|
|
|
1238
1460
|
process.exit(1);
|
|
1239
1461
|
}
|
|
1240
1462
|
|
|
1463
|
+
let injectPrompt = prompt;
|
|
1464
|
+
let referencePath = null;
|
|
1465
|
+
const refDescriptor = useRef ? createSharedReferenceDescriptor(prompt, refFilePath) : null;
|
|
1466
|
+
|
|
1241
1467
|
// Remote session: use SSH direct execution
|
|
1242
1468
|
if (isRemoteSession(target)) {
|
|
1243
1469
|
const { checkEntitlement } = require('./entitlement');
|
|
@@ -1246,33 +1472,139 @@ async function main() {
|
|
|
1246
1472
|
console.error(`⚠️ ${ent.reason}\n Upgrade: ${ent.upgrade_url}`);
|
|
1247
1473
|
process.exit(1);
|
|
1248
1474
|
}
|
|
1249
|
-
|
|
1475
|
+
|
|
1476
|
+
if (useRef) {
|
|
1477
|
+
const reference = ensureRemoteSharedReference(target.peerName, refDescriptor, refFilePath ? prompt : '');
|
|
1478
|
+
injectPrompt = reference.prompt;
|
|
1479
|
+
referencePath = reference.referencePath;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const result = crossMachine.remoteInject(target.peerName, target.id, injectPrompt, {
|
|
1250
1483
|
from: fromId,
|
|
1251
|
-
|
|
1484
|
+
reply_to: replyTo,
|
|
1485
|
+
reply_expected: replyExpected
|
|
1252
1486
|
});
|
|
1253
1487
|
if (result.success) {
|
|
1254
|
-
|
|
1488
|
+
const refSuffix = referencePath ? ` (ref: ${referencePath})` : '';
|
|
1489
|
+
console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m' @ ${target.peerName}.${refSuffix}`);
|
|
1255
1490
|
} else {
|
|
1256
|
-
console.error(`❌
|
|
1491
|
+
console.error(`❌ ${result.error}`);
|
|
1257
1492
|
}
|
|
1258
1493
|
return;
|
|
1259
1494
|
}
|
|
1260
1495
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1496
|
+
if (useRef) {
|
|
1497
|
+
const reference = ensureLocalSharedReference(refDescriptor, refFilePath ? prompt : '');
|
|
1498
|
+
injectPrompt = reference.prompt;
|
|
1499
|
+
referencePath = reference.referencePath;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const body = buildInjectRequestBody(injectPrompt, {
|
|
1503
|
+
fromId,
|
|
1504
|
+
replyTo,
|
|
1505
|
+
replyExpected,
|
|
1506
|
+
noEnter: useSubmit
|
|
1507
|
+
});
|
|
1265
1508
|
|
|
1266
1509
|
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
1267
1510
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
1268
1511
|
});
|
|
1269
1512
|
const data = await res.json();
|
|
1270
|
-
if (!res.ok) { console.error(`❌
|
|
1271
|
-
|
|
1513
|
+
if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
|
|
1514
|
+
const refSuffix = referencePath ? ` (ref: ${referencePath})` : '';
|
|
1515
|
+
console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m'.${refSuffix}`);
|
|
1516
|
+
|
|
1517
|
+
// Terminal-level submit: POST /submit after text injection
|
|
1518
|
+
if (useSubmit) {
|
|
1519
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1520
|
+
try {
|
|
1521
|
+
const submitRes = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
|
|
1522
|
+
method: 'POST',
|
|
1523
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1524
|
+
body: JSON.stringify({ pre_delay_ms: 200, retries: 2, retry_delay_ms: 500 })
|
|
1525
|
+
});
|
|
1526
|
+
const submitData = await submitRes.json();
|
|
1527
|
+
if (submitRes.ok) {
|
|
1528
|
+
console.log(`✅ Submitted via ${submitData.strategy}${submitData.attempts > 1 ? ` (${submitData.attempts} attempts)` : ''}.`);
|
|
1529
|
+
} else {
|
|
1530
|
+
console.error(`⚠️ Submit failed: ${formatApiError(submitData)}`);
|
|
1531
|
+
}
|
|
1532
|
+
} catch (submitErr) {
|
|
1533
|
+
console.error(`⚠️ Submit failed: ${submitErr.message}`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1272
1536
|
} catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
|
|
1273
1537
|
return;
|
|
1274
1538
|
}
|
|
1275
1539
|
|
|
1540
|
+
if (cmd === 'enter') {
|
|
1541
|
+
const sessionId = args[1];
|
|
1542
|
+
if (!sessionId) { console.error('❌ Usage: telepty enter <session_id>'); process.exit(1); }
|
|
1543
|
+
|
|
1544
|
+
try {
|
|
1545
|
+
const target = await resolveSessionTarget(sessionId);
|
|
1546
|
+
if (!target) {
|
|
1547
|
+
console.error(`❌ Session '${sessionId}' was not found on any discovered host.`);
|
|
1548
|
+
process.exit(1);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
if (isRemoteSession(target)) {
|
|
1552
|
+
const { checkEntitlement } = require('./entitlement');
|
|
1553
|
+
const ent = checkEntitlement({ feature: 'telepty.remote_sessions' });
|
|
1554
|
+
if (!ent.allowed) {
|
|
1555
|
+
console.error(`⚠️ ${ent.reason}\n Upgrade: ${ent.upgrade_url}`);
|
|
1556
|
+
process.exit(1);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const result = crossMachine.remoteInject(target.peerName, target.id, '', {});
|
|
1560
|
+
if (result.success) {
|
|
1561
|
+
console.log(`✅ Enter sent successfully into '\x1b[36m${target.id}\x1b[0m' @ ${target.peerName}.`);
|
|
1562
|
+
} else {
|
|
1563
|
+
console.error(`❌ ${result.error}`);
|
|
1564
|
+
}
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
1569
|
+
method: 'POST',
|
|
1570
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1571
|
+
body: JSON.stringify(buildInjectRequestBody('', {}))
|
|
1572
|
+
});
|
|
1573
|
+
const data = await res.json();
|
|
1574
|
+
if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
|
|
1575
|
+
console.log(`✅ Enter sent successfully into '\x1b[36m${target.id}\x1b[0m'.`);
|
|
1576
|
+
} catch (e) {
|
|
1577
|
+
console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`);
|
|
1578
|
+
}
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (cmd === 'send-key') {
|
|
1583
|
+
const sessionId = args[1];
|
|
1584
|
+
const key = (args[2] || '').toLowerCase();
|
|
1585
|
+
if (!sessionId || !key) { console.error('❌ Usage: telepty send-key <session_id> <key>\n Supported keys: enter'); process.exit(1); }
|
|
1586
|
+
if (key !== 'enter' && key !== 'return') {
|
|
1587
|
+
console.error(`❌ Unsupported key: '${key}'. Supported keys: enter`);
|
|
1588
|
+
process.exit(1);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
try {
|
|
1592
|
+
const target = await resolveSessionTarget(sessionId);
|
|
1593
|
+
if (!target) {
|
|
1594
|
+
console.error(`❌ Session '${sessionId}' was not found on any discovered host.`);
|
|
1595
|
+
process.exit(1);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/submit`, { method: 'POST' });
|
|
1599
|
+
const data = await res.json();
|
|
1600
|
+
if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
|
|
1601
|
+
console.log(`✅ Key '${key}' sent to '\x1b[36m${target.id}\x1b[0m'. (strategy: ${data.strategy})`);
|
|
1602
|
+
} catch (e) {
|
|
1603
|
+
console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`);
|
|
1604
|
+
}
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1276
1608
|
if (cmd === 'reply') {
|
|
1277
1609
|
const mySessionId = process.env.TELEPTY_SESSION_ID;
|
|
1278
1610
|
if (!mySessionId) { console.error('❌ TELEPTY_SESSION_ID env var is required for reply command'); process.exit(1); }
|
|
@@ -1291,12 +1623,94 @@ async function main() {
|
|
|
1291
1623
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
1292
1624
|
});
|
|
1293
1625
|
const data = await res.json();
|
|
1294
|
-
if (!res.ok) { console.error(`❌
|
|
1626
|
+
if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
|
|
1295
1627
|
console.log(`✅ Reply sent to '\x1b[36m${replyTo}\x1b[0m'.`);
|
|
1296
1628
|
} catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
|
|
1297
1629
|
return;
|
|
1298
1630
|
}
|
|
1299
1631
|
|
|
1632
|
+
if (cmd === 'status-report') {
|
|
1633
|
+
const reportArgs = args.slice(1);
|
|
1634
|
+
let sessionId = process.env.TELEPTY_SESSION_ID || undefined;
|
|
1635
|
+
|
|
1636
|
+
const idIndex = reportArgs.indexOf('--id');
|
|
1637
|
+
if (idIndex !== -1) {
|
|
1638
|
+
if (!reportArgs[idIndex + 1]) {
|
|
1639
|
+
console.error('❌ Usage: telepty status-report [--id <session_id>] --phase <phase> [--task <text>] [--blocker <text>] [--needs-input] [--thread-id <id>] [--seq <n>]');
|
|
1640
|
+
process.exit(1);
|
|
1641
|
+
}
|
|
1642
|
+
sessionId = reportArgs[idIndex + 1];
|
|
1643
|
+
reportArgs.splice(idIndex, 2);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function takeFlagValue(flag) {
|
|
1647
|
+
const index = reportArgs.indexOf(flag);
|
|
1648
|
+
if (index === -1) return undefined;
|
|
1649
|
+
if (!reportArgs[index + 1]) {
|
|
1650
|
+
console.error(`❌ ${flag} requires a value.`);
|
|
1651
|
+
process.exit(1);
|
|
1652
|
+
}
|
|
1653
|
+
const value = reportArgs[index + 1];
|
|
1654
|
+
reportArgs.splice(index, 2);
|
|
1655
|
+
return value;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const phase = takeFlagValue('--phase');
|
|
1659
|
+
const currentTask = takeFlagValue('--task') ?? takeFlagValue('--current-task');
|
|
1660
|
+
const blocker = takeFlagValue('--blocker');
|
|
1661
|
+
const threadId = takeFlagValue('--thread-id');
|
|
1662
|
+
const source = takeFlagValue('--source');
|
|
1663
|
+
const seqRaw = takeFlagValue('--seq');
|
|
1664
|
+
const needsInputIndex = reportArgs.indexOf('--needs-input');
|
|
1665
|
+
const needsInput = needsInputIndex !== -1;
|
|
1666
|
+
if (needsInput) reportArgs.splice(needsInputIndex, 1);
|
|
1667
|
+
|
|
1668
|
+
if (!sessionId || !phase || reportArgs.length > 0) {
|
|
1669
|
+
console.error('❌ Usage: telepty status-report [--id <session_id>] --phase <phase> [--task <text>] [--blocker <text>] [--needs-input] [--thread-id <id>] [--seq <n>]');
|
|
1670
|
+
process.exit(1);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
if (seqRaw !== undefined && (!Number.isInteger(Number(seqRaw)) || Number(seqRaw) < 0)) {
|
|
1674
|
+
console.error('❌ --seq must be a non-negative integer.');
|
|
1675
|
+
process.exit(1);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
try {
|
|
1679
|
+
const target = await resolveSessionTarget(sessionId);
|
|
1680
|
+
if (!target) {
|
|
1681
|
+
console.error(`❌ Session '${sessionId}' was not found on any discovered host.`);
|
|
1682
|
+
process.exit(1);
|
|
1683
|
+
}
|
|
1684
|
+
if (isRemoteSession(target)) {
|
|
1685
|
+
console.error('❌ telepty status-report currently supports local daemon sessions only.');
|
|
1686
|
+
process.exit(1);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/state`, {
|
|
1690
|
+
method: 'POST',
|
|
1691
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1692
|
+
body: JSON.stringify(buildSessionStateReportBody({
|
|
1693
|
+
phase,
|
|
1694
|
+
currentTask,
|
|
1695
|
+
blocker,
|
|
1696
|
+
needsInput,
|
|
1697
|
+
threadId,
|
|
1698
|
+
source,
|
|
1699
|
+
seq: seqRaw === undefined ? undefined : Number(seqRaw)
|
|
1700
|
+
}))
|
|
1701
|
+
});
|
|
1702
|
+
const data = await res.json();
|
|
1703
|
+
if (!res.ok) {
|
|
1704
|
+
console.error(`❌ ${formatApiError(data)}`);
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
console.log(`✅ Session state reported for '\x1b[36m${target.id}\x1b[0m' (${phase}).`);
|
|
1708
|
+
} catch (e) {
|
|
1709
|
+
console.error(`❌ ${e.message || 'Failed to report session state.'}`);
|
|
1710
|
+
}
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1300
1714
|
if (cmd === 'multicast') {
|
|
1301
1715
|
const sessionIdsRaw = args[1]; const prompt = args.slice(2).join(' ');
|
|
1302
1716
|
if (!sessionIdsRaw || !prompt) { console.error('❌ Usage: telepty multicast <id1,id2,...> "<prompt text>"'); process.exit(1); }
|
|
@@ -1324,7 +1738,7 @@ async function main() {
|
|
|
1324
1738
|
});
|
|
1325
1739
|
const data = await res.json();
|
|
1326
1740
|
if (!res.ok) {
|
|
1327
|
-
throw new Error(data
|
|
1741
|
+
throw new Error(formatApiError(data, `Multicast failed on ${host}`));
|
|
1328
1742
|
}
|
|
1329
1743
|
aggregate.successful.push(...data.results.successful.map((id) => `${id}@${host}`));
|
|
1330
1744
|
aggregate.failed.push(...data.results.failed.map((item) => ({ ...item, host })));
|
|
@@ -1332,37 +1746,70 @@ async function main() {
|
|
|
1332
1746
|
|
|
1333
1747
|
console.log(`✅ Context multicasted successfully to ${aggregate.successful.length} session(s).`);
|
|
1334
1748
|
if (aggregate.failed.length > 0) {
|
|
1335
|
-
console.warn(`⚠️ Failed to inject into ${aggregate.failed.length} session(s):`, aggregate.failed.map((item) => `${item.id}@${item.host}
|
|
1749
|
+
console.warn(`⚠️ Failed to inject into ${aggregate.failed.length} session(s):`, aggregate.failed.map((item) => `${item.id}@${item.host} [${item.code || 'UNKNOWN'}] ${item.error || ''}`.trim()).join(', '));
|
|
1336
1750
|
}
|
|
1337
1751
|
} catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
|
|
1338
1752
|
return;
|
|
1339
1753
|
}
|
|
1340
1754
|
|
|
1341
1755
|
if (cmd === 'broadcast') {
|
|
1756
|
+
const { useRef, refFilePath } = parseRefOption(args);
|
|
1757
|
+
|
|
1342
1758
|
const prompt = args.slice(1).join(' ');
|
|
1343
|
-
if (!prompt) { console.error('❌ Usage: telepty broadcast "<prompt text>"'); process.exit(1); }
|
|
1759
|
+
if (!prompt && !refFilePath) { console.error('❌ Usage: telepty broadcast [--ref [file]] "<prompt text>"'); process.exit(1); }
|
|
1344
1760
|
try {
|
|
1345
1761
|
const discovered = await discoverSessions({ silent: true });
|
|
1346
|
-
const grouped = groupSessionsByHost(discovered);
|
|
1347
1762
|
const aggregate = { successful: [], failed: [] };
|
|
1763
|
+
const { local, remoteByPeer } = splitSessionsByTransport(discovered);
|
|
1764
|
+
let descriptor = useRef ? createSharedReferenceDescriptor(prompt, refFilePath) : null;
|
|
1765
|
+
let referencePath = null;
|
|
1766
|
+
|
|
1767
|
+
if (local.length > 0) {
|
|
1768
|
+
let localPrompt = prompt;
|
|
1769
|
+
if (useRef) {
|
|
1770
|
+
const reference = ensureLocalSharedReference(descriptor, refFilePath ? prompt : '');
|
|
1771
|
+
descriptor = reference.descriptor;
|
|
1772
|
+
referencePath = reference.referencePath;
|
|
1773
|
+
localPrompt = reference.prompt;
|
|
1774
|
+
}
|
|
1348
1775
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1776
|
+
for (const host of groupSessionsByHost(local).keys()) {
|
|
1777
|
+
const res = await fetchWithAuth(`http://${host}:${PORT}/api/sessions/broadcast/inject`, {
|
|
1778
|
+
method: 'POST',
|
|
1779
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1780
|
+
body: JSON.stringify({ prompt: localPrompt })
|
|
1781
|
+
});
|
|
1782
|
+
const data = await res.json();
|
|
1783
|
+
if (!res.ok) {
|
|
1784
|
+
throw new Error(formatApiError(data, `Broadcast failed on ${host}`));
|
|
1785
|
+
}
|
|
1786
|
+
aggregate.successful.push(...data.results.successful.map((id) => `${id}@${host}`));
|
|
1787
|
+
aggregate.failed.push(...data.results.failed.map((item) => ({ ...item, host })));
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
for (const [peerName, sessions] of remoteByPeer.entries()) {
|
|
1792
|
+
let remotePrompt = prompt;
|
|
1793
|
+
if (useRef) {
|
|
1794
|
+
const reference = ensureRemoteSharedReference(peerName, descriptor, refFilePath ? prompt : '');
|
|
1795
|
+
referencePath ||= reference.referencePath;
|
|
1796
|
+
remotePrompt = reference.prompt;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
for (const session of sessions) {
|
|
1800
|
+
const result = crossMachine.remoteInject(peerName, session.id, remotePrompt);
|
|
1801
|
+
if (result.success) {
|
|
1802
|
+
aggregate.successful.push(`${session.id}@${session.host}`);
|
|
1803
|
+
} else {
|
|
1804
|
+
aggregate.failed.push({ id: session.id, host: session.host, error: result.error });
|
|
1805
|
+
}
|
|
1358
1806
|
}
|
|
1359
|
-
aggregate.successful.push(...data.results.successful.map((id) => `${id}@${host}`));
|
|
1360
|
-
aggregate.failed.push(...data.results.failed.map((item) => ({ ...item, host })));
|
|
1361
1807
|
}
|
|
1362
1808
|
|
|
1363
|
-
|
|
1809
|
+
const refSuffix = referencePath ? ` (ref: ${referencePath})` : '';
|
|
1810
|
+
console.log(`✅ Context broadcasted successfully to ${aggregate.successful.length} active session(s).${refSuffix}`);
|
|
1364
1811
|
if (aggregate.failed.length > 0) {
|
|
1365
|
-
console.warn(`⚠️ Failed to inject into ${aggregate.failed.length} session(s):`, aggregate.failed.map((item) => `${item.id}@${item.host}
|
|
1812
|
+
console.warn(`⚠️ Failed to inject into ${aggregate.failed.length} session(s):`, aggregate.failed.map((item) => `${item.id}@${item.host} [${item.code || 'UNKNOWN'}] ${item.error || ''}`.trim()).join(', '));
|
|
1366
1813
|
}
|
|
1367
1814
|
} catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
|
|
1368
1815
|
return;
|
|
@@ -1389,6 +1836,56 @@ async function main() {
|
|
|
1389
1836
|
return;
|
|
1390
1837
|
}
|
|
1391
1838
|
|
|
1839
|
+
if (cmd === 'session' && args[1] === 'info') {
|
|
1840
|
+
const sessionRef = args[2];
|
|
1841
|
+
if (!sessionRef) {
|
|
1842
|
+
console.error('❌ Usage: telepty session info <id[@host]>');
|
|
1843
|
+
process.exit(1);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
try {
|
|
1847
|
+
const sessions = await discoverSessions({ silent: true });
|
|
1848
|
+
const target = await resolveSessionTarget(sessionRef, { sessions });
|
|
1849
|
+
if (!target) {
|
|
1850
|
+
console.error(`❌ Session '${sessionRef}' was not found on any discovered host.`);
|
|
1851
|
+
process.exit(1);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
if (args.includes('--json')) {
|
|
1855
|
+
if (isRemoteSession(target)) {
|
|
1856
|
+
console.log(JSON.stringify(target, null, 2));
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}`);
|
|
1861
|
+
const data = await res.json();
|
|
1862
|
+
if (!res.ok) {
|
|
1863
|
+
console.error(`❌ Error: ${data.error}`);
|
|
1864
|
+
process.exit(1);
|
|
1865
|
+
}
|
|
1866
|
+
console.log(JSON.stringify({ host: target.host, ...data }, null, 2));
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (isRemoteSession(target)) {
|
|
1871
|
+
printSessionInfo(target, { host: target.host });
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}`);
|
|
1876
|
+
const data = await res.json();
|
|
1877
|
+
if (!res.ok) {
|
|
1878
|
+
console.error(`❌ Error: ${data.error}`);
|
|
1879
|
+
process.exit(1);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
printSessionInfo(data, { host: target.host });
|
|
1883
|
+
} catch (e) {
|
|
1884
|
+
console.error(`❌ ${e.message || 'Failed to fetch session info.'}`);
|
|
1885
|
+
}
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1392
1889
|
if (cmd === 'session' && args[1] === 'start') {
|
|
1393
1890
|
// Generate kitty session file and launch
|
|
1394
1891
|
const configArg = args.find(a => a.startsWith('--config='));
|
|
@@ -2216,14 +2713,17 @@ Usage:
|
|
|
2216
2713
|
telepty daemon Start the background daemon
|
|
2217
2714
|
telepty spawn --id <id> <command> [args...] Spawn a new background CLI
|
|
2218
2715
|
telepty allow [--id <id>] [--auto-restart] <command> [args...] Allow inject on a CLI (auto-restart on crash)
|
|
2219
|
-
telepty list
|
|
2716
|
+
telepty list [--json] List all active sessions across discovered hosts
|
|
2220
2717
|
telepty attach [id[@host]] Attach to a session (Interactive picker if no ID)
|
|
2221
|
-
telepty inject [--
|
|
2718
|
+
telepty inject [--ref [file]] [--from <id>] [--reply-to <id>] <id[@host]> "<prompt>" Inject text into a single session
|
|
2719
|
+
telepty enter <id[@host]> Send only Enter/Return to a single session
|
|
2222
2720
|
telepty read-screen <id[@host]> [--lines N] [--raw] Read session screen buffer
|
|
2223
2721
|
telepty reply "<text>" Reply to the session that last injected into $TELEPTY_SESSION_ID
|
|
2722
|
+
telepty status-report [--id <id>] --phase <phase> [--task <text>] [--blocker <text>] [--needs-input] [--thread-id <id>] [--seq N]
|
|
2224
2723
|
telepty multicast <id1[@host],id2[@host]> "<prompt>" Inject text into multiple specific sessions
|
|
2225
|
-
telepty broadcast "<prompt>"
|
|
2724
|
+
telepty broadcast [--ref [file]] "<prompt>" Inject text into ALL active sessions
|
|
2226
2725
|
telepty rename <old_id[@host]> <new_id> Rename a session (updates terminal title too)
|
|
2726
|
+
telepty session info <id[@host]> [--json] Show detailed session metadata
|
|
2227
2727
|
telepty connect <user@host> [--name N] [--port P] Connect to a remote machine via SSH tunnel
|
|
2228
2728
|
telepty disconnect <name> | --all Disconnect from a remote machine
|
|
2229
2729
|
telepty peers [--remove <name>] List connected and known peers
|