@dmsdc-ai/aigentry-telepty 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const path = require('path');
4
+ const os = require('os');
4
5
  const WebSocket = require('ws');
5
6
  const { execSync, spawn } = require('child_process');
6
7
  const readline = require('readline');
@@ -10,12 +11,14 @@ const pkg = require('./package.json');
10
11
  const { getConfig } = require('./auth');
11
12
  const args = process.argv.slice(2);
12
13
 
13
- // Check for updates
14
- updateNotifier({pkg}).notify({ isGlobal: true });
14
+ // Check for updates unless explicitly disabled for tests/CI.
15
+ if (!process.env.NO_UPDATE_NOTIFIER && !process.env.TELEPTY_DISABLE_UPDATE_NOTIFIER) {
16
+ updateNotifier({pkg}).notify({ isGlobal: true });
17
+ }
15
18
 
16
19
  // Support remote host via environment variable or default to localhost
17
20
  let REMOTE_HOST = process.env.TELEPTY_HOST || '127.0.0.1';
18
- const PORT = 3848;
21
+ const PORT = Number(process.env.TELEPTY_PORT || 3848);
19
22
  let DAEMON_URL = `http://${REMOTE_HOST}:${PORT}`;
20
23
  let WS_URL = `ws://${REMOTE_HOST}:${PORT}`;
21
24
 
@@ -27,25 +30,6 @@ const fetchWithAuth = (url, options = {}) => {
27
30
  return fetch(url, { ...options, headers });
28
31
  };
29
32
 
30
- async function ensureDaemonRunning() {
31
- if (REMOTE_HOST !== '127.0.0.1') return; // Only auto-start local daemon
32
- try {
33
- const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions`);
34
- if (res.ok) return; // Already running
35
- } catch (e) {
36
- // Not running, let's start it
37
- process.stdout.write('\x1b[33m⚙️ Auto-starting local telepty daemon...\x1b[0m\n');
38
- const cp = spawn(process.argv[0], [process.argv[1], 'daemon'], {
39
- detached: true,
40
- stdio: 'ignore'
41
- });
42
- cp.unref();
43
-
44
- // Wait a brief moment for the daemon to boot up
45
- await new Promise(r => setTimeout(r, 1000));
46
- }
47
- }
48
-
49
33
  async function discoverSessions() {
50
34
  await ensureDaemonRunning();
51
35
  const hosts = ['127.0.0.1'];
@@ -107,13 +91,19 @@ async function ensureDaemonRunning() {
107
91
  async function manageInteractiveAttach(sessionId, targetHost) {
108
92
  const wsUrl = `ws://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
109
93
  const ws = new WebSocket(wsUrl);
94
+ let inputHandler = null;
95
+ let resizeHandler = null;
110
96
  return new Promise((resolve) => {
111
97
  ws.on('open', () => {
112
- console.log(`\n\x1b[32mEntered room '${sessionId}'. The room will be destroyed if you exit (Ctrl+C or exit command).\x1b[0m\n`);
98
+ // Set Ghostty tab title to show session ID
99
+ process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}\x07`);
100
+ console.log(`\n\x1b[32mEntered room '${sessionId}'.\x1b[0m\n`);
113
101
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
114
- process.stdin.on('data', d => ws.send(JSON.stringify({ type: 'input', data: d.toString() })));
115
- const resizer = () => ws.send(JSON.stringify({ type: 'resize', cols: process.stdout.columns, rows: process.stdout.rows }));
116
- process.stdout.on('resize', resizer); resizer();
102
+ inputHandler = (d) => ws.send(JSON.stringify({ type: 'input', data: d.toString() }));
103
+ resizeHandler = () => ws.send(JSON.stringify({ type: 'resize', cols: process.stdout.columns, rows: process.stdout.rows }));
104
+ process.stdin.on('data', inputHandler);
105
+ process.stdout.on('resize', resizeHandler);
106
+ resizeHandler();
117
107
  });
118
108
  ws.on('message', m => {
119
109
  const msg = JSON.parse(m);
@@ -121,14 +111,27 @@ async function manageInteractiveAttach(sessionId, targetHost) {
121
111
  });
122
112
  ws.on('close', async () => {
123
113
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
124
- console.log(`\n\x1b[33mLeft room '${sessionId}'. Destroying session to prevent zombies...\x1b[0m\n`);
125
- process.stdin.removeAllListeners('data');
126
-
127
- // Auto-kill session when the primary creator leaves
114
+ process.stdout.write(`\x1b]0;\x07`); // Restore default terminal title
115
+ if (inputHandler) process.stdin.off('data', inputHandler);
116
+ if (resizeHandler) process.stdout.off('resize', resizeHandler);
117
+
118
+ // Check if other clients are still attached before destroying
128
119
  try {
129
- await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
130
- } catch(e) {}
131
-
120
+ const res = await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions`);
121
+ if (res.ok) {
122
+ const sessions = await res.json();
123
+ const session = sessions.find(s => s.id === sessionId);
124
+ if (session && session.active_clients > 0) {
125
+ console.log(`\n\x1b[33mLeft room '${sessionId}'. Other clients still attached — session kept alive.\x1b[0m\n`);
126
+ } else {
127
+ console.log(`\n\x1b[33mLeft room '${sessionId}'. No other clients — destroying session.\x1b[0m\n`);
128
+ await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
129
+ }
130
+ }
131
+ } catch(e) {
132
+ // Daemon unreachable, nothing to clean up
133
+ }
134
+
132
135
  resolve();
133
136
  });
134
137
  });
@@ -146,6 +149,7 @@ async function manageInteractive() {
146
149
  choices: [
147
150
  { title: '🖥️ Enter a room (Attach to session)', value: 'attach' },
148
151
  { title: '➕ Create a new room (Spawn session)', value: 'spawn' },
152
+ { title: '🔌 Allow inject (Run CLI with inject)', value: 'allow' },
149
153
  { title: '💬 Send message to a room (Inject command)', value: 'inject' },
150
154
  { title: '📋 View all open rooms (List sessions)', value: 'list' },
151
155
  { title: '🔄 Update telepty to latest version', value: 'update' },
@@ -215,7 +219,7 @@ async function manageInteractive() {
215
219
  try {
216
220
  const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/spawn`, {
217
221
  method: 'POST', headers: { 'Content-Type': 'application/json' },
218
- body: JSON.stringify({ session_id: id, command, args: [], cwd: process.cwd(), cols, rows })
222
+ body: JSON.stringify({ session_id: id, command, args: [], cwd: process.cwd(), cols, rows, type: 'USER' })
219
223
  });
220
224
  const data = await res.json();
221
225
  if (!res.ok) console.error(`\n❌ Error: ${data.error}\n`);
@@ -231,6 +235,20 @@ async function manageInteractive() {
231
235
  continue;
232
236
  }
233
237
 
238
+ if (response.action === 'allow') {
239
+ const { id, command } = await prompts([
240
+ { type: 'text', name: 'id', message: 'Enter session ID (e.g. my-claude):', validate: v => v ? true : 'Required' },
241
+ { type: 'text', name: 'command', message: 'Enter command to run (e.g. claude, codex, gemini, bash):', initial: 'bash' }
242
+ ]);
243
+ if (!id || !command) continue;
244
+
245
+ // Delegate to the allow command handler by setting up args and calling main flow
246
+ process.argv.splice(2, process.argv.length - 2, 'allow', '--id', id, command);
247
+ args.length = 0;
248
+ args.push('allow', '--id', id, command);
249
+ return main();
250
+ }
251
+
234
252
  if (response.action === 'attach' || response.action === 'inject') {
235
253
  const sessions = await discoverSessions();
236
254
  if (sessions.length === 0) {
@@ -346,7 +364,7 @@ async function main() {
346
364
  try {
347
365
  const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/spawn`, {
348
366
  method: 'POST', headers: { 'Content-Type': 'application/json' },
349
- body: JSON.stringify({ session_id: sessionId, command: command, args: cmdArgs, cwd: process.cwd(), cols, rows })
367
+ body: JSON.stringify({ session_id: sessionId, command: command, args: cmdArgs, cwd: process.cwd(), cols, rows, type: 'USER' })
350
368
  });
351
369
  const data = await res.json();
352
370
  if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
@@ -356,6 +374,139 @@ async function main() {
356
374
  return;
357
375
  }
358
376
 
377
+ if (cmd === 'allow' || cmd === 'enable' || cmd === 'wrap') {
378
+ // Parse arguments: telepty allow [--id <session_id>] <command> [args...]
379
+ // Also supports legacy: telepty allow [--id <session_id>] -- <command> [args...]
380
+ const allowArgs = args.slice(1);
381
+
382
+ // Extract --id flag
383
+ let sessionId;
384
+ const idIndex = allowArgs.indexOf('--id');
385
+ if (idIndex !== -1 && allowArgs[idIndex + 1]) {
386
+ sessionId = allowArgs[idIndex + 1];
387
+ allowArgs.splice(idIndex, 2);
388
+ }
389
+
390
+ // Strip optional -- separator for backward compat
391
+ const sepIndex = allowArgs.indexOf('--');
392
+ if (sepIndex !== -1) allowArgs.splice(sepIndex, 1);
393
+
394
+ const command = allowArgs[0];
395
+ const cmdArgs = allowArgs.slice(1);
396
+
397
+ if (!command) {
398
+ console.error('❌ Usage: telepty allow [--id <session_id>] <command> [args...]');
399
+ process.exit(1);
400
+ }
401
+
402
+ // Default session ID = command name
403
+ if (!sessionId) {
404
+ sessionId = path.basename(command);
405
+ }
406
+
407
+ await ensureDaemonRunning();
408
+
409
+ // Register session with daemon
410
+ try {
411
+ const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/register`, {
412
+ method: 'POST',
413
+ headers: { 'Content-Type': 'application/json' },
414
+ body: JSON.stringify({ session_id: sessionId, command, cwd: process.cwd() })
415
+ });
416
+ const data = await res.json();
417
+ if (!res.ok) {
418
+ console.error(`❌ Error: ${data.error}`);
419
+ process.exit(1);
420
+ }
421
+ } catch (e) {
422
+ console.error('❌ Failed to register with daemon:', e.message);
423
+ process.exit(1);
424
+ }
425
+
426
+ // Spawn local PTY (preserves isTTY, env, shell config)
427
+ const pty = require('node-pty');
428
+ const child = pty.spawn(command, cmdArgs, {
429
+ name: 'xterm-256color',
430
+ cols: process.stdout.columns || 80,
431
+ rows: process.stdout.rows || 30,
432
+ cwd: process.cwd(),
433
+ env: { ...process.env, TELEPTY_SESSION_ID: sessionId }
434
+ });
435
+
436
+ // Connect to daemon WebSocket for inject reception and output relay
437
+ const wsUrl = `ws://${REMOTE_HOST}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
438
+ const daemonWs = new WebSocket(wsUrl);
439
+ let wsReady = false;
440
+
441
+ daemonWs.on('open', () => {
442
+ wsReady = true;
443
+ });
444
+
445
+ // Receive inject messages from daemon
446
+ daemonWs.on('message', (message) => {
447
+ try {
448
+ const msg = JSON.parse(message);
449
+ if (msg.type === 'inject') {
450
+ child.write(msg.data);
451
+ } else if (msg.type === 'resize') {
452
+ child.resize(msg.cols, msg.rows);
453
+ }
454
+ } catch (e) {
455
+ // ignore malformed messages
456
+ }
457
+ });
458
+
459
+ daemonWs.on('close', () => {
460
+ wsReady = false;
461
+ console.error(`\n\x1b[33m⚠️ Disconnected from daemon. Inject unavailable. Session continues locally.\x1b[0m`);
462
+ });
463
+
464
+ daemonWs.on('error', () => {
465
+ // silently handle
466
+ });
467
+
468
+ // Set terminal title
469
+ process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}\x07`);
470
+ console.log(`\x1b[32m⚡ '${command}' is now session '\x1b[36m${sessionId}\x1b[32m'. Inject allowed.\x1b[0m\n`);
471
+
472
+ // Enter raw mode and relay stdin to local PTY
473
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
474
+
475
+ process.stdin.on('data', (data) => {
476
+ child.write(data.toString());
477
+ });
478
+
479
+ // Relay PTY output to current terminal + send to daemon for attach clients
480
+ child.onData((data) => {
481
+ process.stdout.write(data);
482
+ if (wsReady && daemonWs.readyState === 1) {
483
+ daemonWs.send(JSON.stringify({ type: 'output', data }));
484
+ }
485
+ });
486
+
487
+ // Handle terminal resize
488
+ process.stdout.on('resize', () => {
489
+ child.resize(process.stdout.columns, process.stdout.rows);
490
+ });
491
+
492
+ // Handle child exit
493
+ child.onExit(({ exitCode }) => {
494
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
495
+ process.stdout.write(`\x1b]0;\x07`);
496
+ console.log(`\n\x1b[33mSession '${sessionId}' exited (code ${exitCode}).\x1b[0m`);
497
+
498
+ // Deregister from daemon
499
+ fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }).catch(() => {});
500
+ daemonWs.close();
501
+ process.exit(exitCode || 0);
502
+ });
503
+
504
+ // Graceful shutdown on SIGINT (let child handle it via PTY)
505
+ process.on('SIGINT', () => {});
506
+
507
+ return;
508
+ }
509
+
359
510
  if (cmd === 'attach') {
360
511
  let sessionId = args[1];
361
512
  let targetHost = REMOTE_HOST;
@@ -389,19 +540,25 @@ async function main() {
389
540
 
390
541
  const wsUrl = `ws://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
391
542
  const ws = new WebSocket(wsUrl);
543
+ let inputHandler = null;
544
+ let resizeHandler = null;
392
545
 
393
546
  ws.on('open', () => {
394
- console.log(`\x1b[32mEntered room '${sessionId}' at ${targetHost}. The room will be destroyed if you exit.\x1b[0m\n`);
395
-
547
+ // Set Ghostty tab title to show session ID
548
+ const hostSuffix = targetHost === '127.0.0.1' ? '' : ` @ ${targetHost}`;
549
+ process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}${hostSuffix}\x07`);
550
+ console.log(`\x1b[32mEntered room '${sessionId}'${hostSuffix ? ` (${targetHost})` : ''}.\x1b[0m\n`);
551
+
396
552
  if (process.stdin.isTTY) {
397
553
  process.stdin.setRawMode(true);
398
554
  }
399
555
 
400
- process.stdin.on('data', (data) => {
556
+ inputHandler = (data) => {
401
557
  ws.send(JSON.stringify({ type: 'input', data: data.toString() }));
402
- });
558
+ };
559
+ process.stdin.on('data', inputHandler);
403
560
 
404
- const resizeHandler = () => {
561
+ resizeHandler = () => {
405
562
  ws.send(JSON.stringify({
406
563
  type: 'resize',
407
564
  cols: process.stdout.columns,
@@ -424,9 +581,23 @@ async function main() {
424
581
  if (process.stdin.isTTY) {
425
582
  process.stdin.setRawMode(false);
426
583
  }
427
- console.log(`\n\x1b[33mLeft room. Destroying session '${sessionId}' to prevent zombies... (Code: ${code})\x1b[0m`);
584
+ process.stdout.write(`\x1b]0;\x07`); // Restore default terminal title
585
+ if (inputHandler) process.stdin.off('data', inputHandler);
586
+ if (resizeHandler) process.stdout.off('resize', resizeHandler);
587
+
588
+ // Check if other clients are still attached before destroying
428
589
  try {
429
- await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
590
+ const res = await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions`);
591
+ if (res.ok) {
592
+ const allSessions = await res.json();
593
+ const session = allSessions.find(s => s.id === sessionId);
594
+ if (session && session.active_clients > 0) {
595
+ console.log(`\n\x1b[33mLeft room '${sessionId}'. Other clients still attached — session kept alive.\x1b[0m`);
596
+ } else {
597
+ console.log(`\n\x1b[33mLeft room '${sessionId}'. No other clients — destroying session.\x1b[0m`);
598
+ await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
599
+ }
600
+ }
430
601
  } catch(e) {}
431
602
  process.exit(0);
432
603
  });
@@ -490,9 +661,30 @@ async function main() {
490
661
  return;
491
662
  }
492
663
 
493
- if (cmd === 'listen') {
664
+ if (cmd === 'rename') {
665
+ const oldId = args[1]; const newId = args[2];
666
+ if (!oldId || !newId) { console.error('❌ Usage: telepty rename <old_id> <new_id>'); process.exit(1); }
667
+ try {
668
+ const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(oldId)}`, {
669
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ new_id: newId })
670
+ });
671
+ const data = await res.json();
672
+ if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
673
+ console.log(`✅ Session renamed: '\x1b[36m${oldId}\x1b[0m' → '\x1b[36m${newId}\x1b[0m'`);
674
+ } catch (e) { console.error('❌ Failed to connect to daemon. Is it running?'); }
675
+ return;
676
+ }
677
+
678
+ if (cmd === 'listen' || cmd === 'monitor') {
494
679
  await ensureDaemonRunning();
495
- console.log('\x1b[36m👂 Listening to the telepty event bus...\x1b[0m');
680
+
681
+ if (cmd === 'monitor') {
682
+ console.log('\x1b[36m\x1b[1m📺 Telepty Event Billboard\x1b[0m');
683
+ console.log('Listening for background agent communications...\n');
684
+ } else {
685
+ console.log('\x1b[36m👂 Listening to the telepty event bus...\x1b[0m');
686
+ }
687
+
496
688
  const wsUrl = `ws://${REMOTE_HOST}:${PORT}/api/bus?token=${encodeURIComponent(TOKEN)}`;
497
689
  const ws = new WebSocket(wsUrl);
498
690
 
@@ -501,8 +693,34 @@ async function main() {
501
693
  });
502
694
 
503
695
  ws.on('message', (message) => {
504
- // Print raw JSON to stdout so agents can parse it
505
- console.log(message.toString());
696
+ const raw = message.toString();
697
+ if (cmd === 'listen') {
698
+ // Raw JSON output for machines
699
+ console.log(raw);
700
+ } else {
701
+ // Human readable billboard output
702
+ try {
703
+ const msg = JSON.parse(raw);
704
+ const time = new Date().toLocaleTimeString();
705
+ const sender = msg.sender || msg.from || 'Unknown';
706
+ const target = msg.target_agent || msg.to || 'Bus';
707
+
708
+ let preview = msg.content || msg.message || msg.payload || msg.data;
709
+ if (msg.type === 'session_spawn') {
710
+ console.log(`\x1b[90m[${time}]\x1b[0m 🚀 \x1b[32m\x1b[1mNew Session\x1b[0m: \x1b[36m${msg.session_id}\x1b[0m (${msg.command})`);
711
+ return;
712
+ }
713
+
714
+ if (typeof preview === 'object') preview = JSON.stringify(preview);
715
+ if (preview && preview.length > 200) preview = preview.substring(0, 197) + '...';
716
+
717
+ console.log(`\x1b[90m[${time}]\x1b[0m \x1b[32m\x1b[1m${sender}\x1b[0m ➔ \x1b[33m\x1b[1m${target}\x1b[0m`);
718
+ if (preview) console.log(` \x1b[37m${preview}\x1b[0m\n`);
719
+ } catch (e) {
720
+ // Fallback if not valid JSON
721
+ console.log(`\x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m 📦 \x1b[37m${raw}\x1b[0m\n`);
722
+ }
723
+ }
506
724
  });
507
725
 
508
726
  ws.on('close', () => {
@@ -522,12 +740,15 @@ async function main() {
522
740
  Usage:
523
741
  telepty daemon Start the background daemon
524
742
  telepty spawn --id <id> <command> [args...] Spawn a new background CLI
743
+ telepty allow [--id <id>] <command> [args...] Allow inject on a CLI
525
744
  telepty list List all active sessions
526
745
  telepty attach [id] Attach to a session (Interactive picker if no ID)
527
746
  telepty inject [--no-enter] <id> "<prompt>" Inject text into a single session
528
747
  telepty multicast <id1,id2> "<prompt>" Inject text into multiple specific sessions
529
748
  telepty broadcast "<prompt>" Inject text into ALL active sessions
749
+ telepty rename <old_id> <new_id> Rename a session (updates terminal title too)
530
750
  telepty listen Listen to the event bus and print JSON to stdout
751
+ telepty monitor Human-readable real-time billboard of bus events
531
752
  telepty update Update telepty to the latest version
532
753
  `);
533
754
  }