@dmsdc-ai/aigentry-telepty 0.1.5 → 0.1.7

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');
@@ -8,14 +9,17 @@ const prompts = require('prompts');
8
9
  const updateNotifier = require('update-notifier');
9
10
  const pkg = require('./package.json');
10
11
  const { getConfig } = require('./auth');
12
+ const { runInteractiveSkillInstaller } = require('./skill-installer');
11
13
  const args = process.argv.slice(2);
12
14
 
13
- // Check for updates
14
- updateNotifier({pkg}).notify({ isGlobal: true });
15
+ // Check for updates unless explicitly disabled for tests/CI.
16
+ if (!process.env.NO_UPDATE_NOTIFIER && !process.env.TELEPTY_DISABLE_UPDATE_NOTIFIER) {
17
+ updateNotifier({pkg}).notify({ isGlobal: true });
18
+ }
15
19
 
16
20
  // Support remote host via environment variable or default to localhost
17
21
  let REMOTE_HOST = process.env.TELEPTY_HOST || '127.0.0.1';
18
- const PORT = 3848;
22
+ const PORT = Number(process.env.TELEPTY_PORT || 3848);
19
23
  let DAEMON_URL = `http://${REMOTE_HOST}:${PORT}`;
20
24
  let WS_URL = `ws://${REMOTE_HOST}:${PORT}`;
21
25
 
@@ -27,25 +31,6 @@ const fetchWithAuth = (url, options = {}) => {
27
31
  return fetch(url, { ...options, headers });
28
32
  };
29
33
 
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
34
  async function discoverSessions() {
50
35
  await ensureDaemonRunning();
51
36
  const hosts = ['127.0.0.1'];
@@ -107,13 +92,19 @@ async function ensureDaemonRunning() {
107
92
  async function manageInteractiveAttach(sessionId, targetHost) {
108
93
  const wsUrl = `ws://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
109
94
  const ws = new WebSocket(wsUrl);
95
+ let inputHandler = null;
96
+ let resizeHandler = null;
110
97
  return new Promise((resolve) => {
111
98
  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`);
99
+ // Set Ghostty tab title to show session ID
100
+ process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}\x07`);
101
+ console.log(`\n\x1b[32mEntered room '${sessionId}'.\x1b[0m\n`);
113
102
  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();
103
+ inputHandler = (d) => ws.send(JSON.stringify({ type: 'input', data: d.toString() }));
104
+ resizeHandler = () => ws.send(JSON.stringify({ type: 'resize', cols: process.stdout.columns, rows: process.stdout.rows }));
105
+ process.stdin.on('data', inputHandler);
106
+ process.stdout.on('resize', resizeHandler);
107
+ resizeHandler();
117
108
  });
118
109
  ws.on('message', m => {
119
110
  const msg = JSON.parse(m);
@@ -121,14 +112,27 @@ async function manageInteractiveAttach(sessionId, targetHost) {
121
112
  });
122
113
  ws.on('close', async () => {
123
114
  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
115
+ process.stdout.write(`\x1b]0;\x07`); // Restore default terminal title
116
+ if (inputHandler) process.stdin.off('data', inputHandler);
117
+ if (resizeHandler) process.stdout.off('resize', resizeHandler);
118
+
119
+ // Check if other clients are still attached before destroying
128
120
  try {
129
- await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
130
- } catch(e) {}
131
-
121
+ const res = await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions`);
122
+ if (res.ok) {
123
+ const sessions = await res.json();
124
+ const session = sessions.find(s => s.id === sessionId);
125
+ if (session && session.active_clients > 0) {
126
+ console.log(`\n\x1b[33mLeft room '${sessionId}'. Other clients still attached — session kept alive.\x1b[0m\n`);
127
+ } else {
128
+ console.log(`\n\x1b[33mLeft room '${sessionId}'. No other clients — destroying session.\x1b[0m\n`);
129
+ await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
130
+ }
131
+ }
132
+ } catch(e) {
133
+ // Daemon unreachable, nothing to clean up
134
+ }
135
+
132
136
  resolve();
133
137
  });
134
138
  });
@@ -146,8 +150,10 @@ async function manageInteractive() {
146
150
  choices: [
147
151
  { title: '🖥️ Enter a room (Attach to session)', value: 'attach' },
148
152
  { title: '➕ Create a new room (Spawn session)', value: 'spawn' },
153
+ { title: '🔌 Allow inject (Run CLI with inject)', value: 'allow' },
149
154
  { title: '💬 Send message to a room (Inject command)', value: 'inject' },
150
155
  { title: '📋 View all open rooms (List sessions)', value: 'list' },
156
+ { title: '🧠 Install telepty skills', value: 'install-skills' },
151
157
  { title: '🔄 Update telepty to latest version', value: 'update' },
152
158
  { title: '❌ Exit', value: 'exit' }
153
159
  ]
@@ -185,6 +191,15 @@ async function manageInteractive() {
185
191
  continue;
186
192
  }
187
193
 
194
+ if (response.action === 'install-skills') {
195
+ try {
196
+ await runInteractiveSkillInstaller({ packageRoot: __dirname, cwd: process.cwd() });
197
+ } catch (e) {
198
+ console.error(`\n❌ ${e.message}\n`);
199
+ }
200
+ continue;
201
+ }
202
+
188
203
  if (response.action === 'list') {
189
204
  console.log('\n');
190
205
  const sessions = await discoverSessions();
@@ -215,7 +230,7 @@ async function manageInteractive() {
215
230
  try {
216
231
  const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/spawn`, {
217
232
  method: 'POST', headers: { 'Content-Type': 'application/json' },
218
- body: JSON.stringify({ session_id: id, command, args: [], cwd: process.cwd(), cols, rows })
233
+ body: JSON.stringify({ session_id: id, command, args: [], cwd: process.cwd(), cols, rows, type: 'USER' })
219
234
  });
220
235
  const data = await res.json();
221
236
  if (!res.ok) console.error(`\n❌ Error: ${data.error}\n`);
@@ -231,6 +246,20 @@ async function manageInteractive() {
231
246
  continue;
232
247
  }
233
248
 
249
+ if (response.action === 'allow') {
250
+ const { id, command } = await prompts([
251
+ { type: 'text', name: 'id', message: 'Enter session ID (e.g. my-claude):', validate: v => v ? true : 'Required' },
252
+ { type: 'text', name: 'command', message: 'Enter command to run (e.g. claude, codex, gemini, bash):', initial: 'bash' }
253
+ ]);
254
+ if (!id || !command) continue;
255
+
256
+ // Delegate to the allow command handler by setting up args and calling main flow
257
+ process.argv.splice(2, process.argv.length - 2, 'allow', '--id', id, command);
258
+ args.length = 0;
259
+ args.push('allow', '--id', id, command);
260
+ return main();
261
+ }
262
+
234
263
  if (response.action === 'attach' || response.action === 'inject') {
235
264
  const sessions = await discoverSessions();
236
265
  if (sessions.length === 0) {
@@ -346,7 +375,7 @@ async function main() {
346
375
  try {
347
376
  const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/spawn`, {
348
377
  method: 'POST', headers: { 'Content-Type': 'application/json' },
349
- body: JSON.stringify({ session_id: sessionId, command: command, args: cmdArgs, cwd: process.cwd(), cols, rows })
378
+ body: JSON.stringify({ session_id: sessionId, command: command, args: cmdArgs, cwd: process.cwd(), cols, rows, type: 'USER' })
350
379
  });
351
380
  const data = await res.json();
352
381
  if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
@@ -356,6 +385,139 @@ async function main() {
356
385
  return;
357
386
  }
358
387
 
388
+ if (cmd === 'allow' || cmd === 'enable' || cmd === 'wrap') {
389
+ // Parse arguments: telepty allow [--id <session_id>] <command> [args...]
390
+ // Also supports legacy: telepty allow [--id <session_id>] -- <command> [args...]
391
+ const allowArgs = args.slice(1);
392
+
393
+ // Extract --id flag
394
+ let sessionId;
395
+ const idIndex = allowArgs.indexOf('--id');
396
+ if (idIndex !== -1 && allowArgs[idIndex + 1]) {
397
+ sessionId = allowArgs[idIndex + 1];
398
+ allowArgs.splice(idIndex, 2);
399
+ }
400
+
401
+ // Strip optional -- separator for backward compat
402
+ const sepIndex = allowArgs.indexOf('--');
403
+ if (sepIndex !== -1) allowArgs.splice(sepIndex, 1);
404
+
405
+ const command = allowArgs[0];
406
+ const cmdArgs = allowArgs.slice(1);
407
+
408
+ if (!command) {
409
+ console.error('❌ Usage: telepty allow [--id <session_id>] <command> [args...]');
410
+ process.exit(1);
411
+ }
412
+
413
+ // Default session ID = command name
414
+ if (!sessionId) {
415
+ sessionId = path.basename(command);
416
+ }
417
+
418
+ await ensureDaemonRunning();
419
+
420
+ // Register session with daemon
421
+ try {
422
+ const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/register`, {
423
+ method: 'POST',
424
+ headers: { 'Content-Type': 'application/json' },
425
+ body: JSON.stringify({ session_id: sessionId, command, cwd: process.cwd() })
426
+ });
427
+ const data = await res.json();
428
+ if (!res.ok) {
429
+ console.error(`❌ Error: ${data.error}`);
430
+ process.exit(1);
431
+ }
432
+ } catch (e) {
433
+ console.error('❌ Failed to register with daemon:', e.message);
434
+ process.exit(1);
435
+ }
436
+
437
+ // Spawn local PTY (preserves isTTY, env, shell config)
438
+ const pty = require('node-pty');
439
+ const child = pty.spawn(command, cmdArgs, {
440
+ name: 'xterm-256color',
441
+ cols: process.stdout.columns || 80,
442
+ rows: process.stdout.rows || 30,
443
+ cwd: process.cwd(),
444
+ env: { ...process.env, TELEPTY_SESSION_ID: sessionId }
445
+ });
446
+
447
+ // Connect to daemon WebSocket for inject reception and output relay
448
+ const wsUrl = `ws://${REMOTE_HOST}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
449
+ const daemonWs = new WebSocket(wsUrl);
450
+ let wsReady = false;
451
+
452
+ daemonWs.on('open', () => {
453
+ wsReady = true;
454
+ });
455
+
456
+ // Receive inject messages from daemon
457
+ daemonWs.on('message', (message) => {
458
+ try {
459
+ const msg = JSON.parse(message);
460
+ if (msg.type === 'inject') {
461
+ child.write(msg.data);
462
+ } else if (msg.type === 'resize') {
463
+ child.resize(msg.cols, msg.rows);
464
+ }
465
+ } catch (e) {
466
+ // ignore malformed messages
467
+ }
468
+ });
469
+
470
+ daemonWs.on('close', () => {
471
+ wsReady = false;
472
+ console.error(`\n\x1b[33m⚠️ Disconnected from daemon. Inject unavailable. Session continues locally.\x1b[0m`);
473
+ });
474
+
475
+ daemonWs.on('error', () => {
476
+ // silently handle
477
+ });
478
+
479
+ // Set terminal title
480
+ process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}\x07`);
481
+ console.log(`\x1b[32m⚡ '${command}' is now session '\x1b[36m${sessionId}\x1b[32m'. Inject allowed.\x1b[0m\n`);
482
+
483
+ // Enter raw mode and relay stdin to local PTY
484
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
485
+
486
+ process.stdin.on('data', (data) => {
487
+ child.write(data.toString());
488
+ });
489
+
490
+ // Relay PTY output to current terminal + send to daemon for attach clients
491
+ child.onData((data) => {
492
+ process.stdout.write(data);
493
+ if (wsReady && daemonWs.readyState === 1) {
494
+ daemonWs.send(JSON.stringify({ type: 'output', data }));
495
+ }
496
+ });
497
+
498
+ // Handle terminal resize
499
+ process.stdout.on('resize', () => {
500
+ child.resize(process.stdout.columns, process.stdout.rows);
501
+ });
502
+
503
+ // Handle child exit
504
+ child.onExit(({ exitCode }) => {
505
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
506
+ process.stdout.write(`\x1b]0;\x07`);
507
+ console.log(`\n\x1b[33mSession '${sessionId}' exited (code ${exitCode}).\x1b[0m`);
508
+
509
+ // Deregister from daemon
510
+ fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }).catch(() => {});
511
+ daemonWs.close();
512
+ process.exit(exitCode || 0);
513
+ });
514
+
515
+ // Graceful shutdown on SIGINT (let child handle it via PTY)
516
+ process.on('SIGINT', () => {});
517
+
518
+ return;
519
+ }
520
+
359
521
  if (cmd === 'attach') {
360
522
  let sessionId = args[1];
361
523
  let targetHost = REMOTE_HOST;
@@ -389,19 +551,25 @@ async function main() {
389
551
 
390
552
  const wsUrl = `ws://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
391
553
  const ws = new WebSocket(wsUrl);
554
+ let inputHandler = null;
555
+ let resizeHandler = null;
392
556
 
393
557
  ws.on('open', () => {
394
- console.log(`\x1b[32mEntered room '${sessionId}' at ${targetHost}. The room will be destroyed if you exit.\x1b[0m\n`);
395
-
558
+ // Set Ghostty tab title to show session ID
559
+ const hostSuffix = targetHost === '127.0.0.1' ? '' : ` @ ${targetHost}`;
560
+ process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}${hostSuffix}\x07`);
561
+ console.log(`\x1b[32mEntered room '${sessionId}'${hostSuffix ? ` (${targetHost})` : ''}.\x1b[0m\n`);
562
+
396
563
  if (process.stdin.isTTY) {
397
564
  process.stdin.setRawMode(true);
398
565
  }
399
566
 
400
- process.stdin.on('data', (data) => {
567
+ inputHandler = (data) => {
401
568
  ws.send(JSON.stringify({ type: 'input', data: data.toString() }));
402
- });
569
+ };
570
+ process.stdin.on('data', inputHandler);
403
571
 
404
- const resizeHandler = () => {
572
+ resizeHandler = () => {
405
573
  ws.send(JSON.stringify({
406
574
  type: 'resize',
407
575
  cols: process.stdout.columns,
@@ -424,9 +592,23 @@ async function main() {
424
592
  if (process.stdin.isTTY) {
425
593
  process.stdin.setRawMode(false);
426
594
  }
427
- console.log(`\n\x1b[33mLeft room. Destroying session '${sessionId}' to prevent zombies... (Code: ${code})\x1b[0m`);
595
+ process.stdout.write(`\x1b]0;\x07`); // Restore default terminal title
596
+ if (inputHandler) process.stdin.off('data', inputHandler);
597
+ if (resizeHandler) process.stdout.off('resize', resizeHandler);
598
+
599
+ // Check if other clients are still attached before destroying
428
600
  try {
429
- await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
601
+ const res = await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions`);
602
+ if (res.ok) {
603
+ const allSessions = await res.json();
604
+ const session = allSessions.find(s => s.id === sessionId);
605
+ if (session && session.active_clients > 0) {
606
+ console.log(`\n\x1b[33mLeft room '${sessionId}'. Other clients still attached — session kept alive.\x1b[0m`);
607
+ } else {
608
+ console.log(`\n\x1b[33mLeft room '${sessionId}'. No other clients — destroying session.\x1b[0m`);
609
+ await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
610
+ }
611
+ }
430
612
  } catch(e) {}
431
613
  process.exit(0);
432
614
  });
@@ -490,6 +672,20 @@ async function main() {
490
672
  return;
491
673
  }
492
674
 
675
+ if (cmd === 'rename') {
676
+ const oldId = args[1]; const newId = args[2];
677
+ if (!oldId || !newId) { console.error('❌ Usage: telepty rename <old_id> <new_id>'); process.exit(1); }
678
+ try {
679
+ const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(oldId)}`, {
680
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ new_id: newId })
681
+ });
682
+ const data = await res.json();
683
+ if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
684
+ console.log(`✅ Session renamed: '\x1b[36m${oldId}\x1b[0m' → '\x1b[36m${newId}\x1b[0m'`);
685
+ } catch (e) { console.error('❌ Failed to connect to daemon. Is it running?'); }
686
+ return;
687
+ }
688
+
493
689
  if (cmd === 'listen' || cmd === 'monitor') {
494
690
  await ensureDaemonRunning();
495
691
 
@@ -518,14 +714,19 @@ async function main() {
518
714
  const msg = JSON.parse(raw);
519
715
  const time = new Date().toLocaleTimeString();
520
716
  const sender = msg.sender || msg.from || 'Unknown';
521
- const target = msg.target_agent || msg.to || 'Broadcast';
717
+ const target = msg.target_agent || msg.to || 'Bus';
718
+
719
+ let preview = msg.content || msg.message || msg.payload || msg.data;
720
+ if (msg.type === 'session_spawn') {
721
+ console.log(`\x1b[90m[${time}]\x1b[0m 🚀 \x1b[32m\x1b[1mNew Session\x1b[0m: \x1b[36m${msg.session_id}\x1b[0m (${msg.command})`);
722
+ return;
723
+ }
522
724
 
523
- let preview = msg.content || msg.message || msg.payload || JSON.stringify(msg);
524
725
  if (typeof preview === 'object') preview = JSON.stringify(preview);
525
- if (preview.length > 200) preview = preview.substring(0, 197) + '...';
726
+ if (preview && preview.length > 200) preview = preview.substring(0, 197) + '...';
526
727
 
527
728
  console.log(`\x1b[90m[${time}]\x1b[0m \x1b[32m\x1b[1m${sender}\x1b[0m ➔ \x1b[33m\x1b[1m${target}\x1b[0m`);
528
- console.log(` \x1b[37m${preview}\x1b[0m\n`);
729
+ if (preview) console.log(` \x1b[37m${preview}\x1b[0m\n`);
529
730
  } catch (e) {
530
731
  // Fallback if not valid JSON
531
732
  console.log(`\x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m 📦 \x1b[37m${raw}\x1b[0m\n`);
@@ -550,11 +751,13 @@ async function main() {
550
751
  Usage:
551
752
  telepty daemon Start the background daemon
552
753
  telepty spawn --id <id> <command> [args...] Spawn a new background CLI
754
+ telepty allow [--id <id>] <command> [args...] Allow inject on a CLI
553
755
  telepty list List all active sessions
554
756
  telepty attach [id] Attach to a session (Interactive picker if no ID)
555
757
  telepty inject [--no-enter] <id> "<prompt>" Inject text into a single session
556
758
  telepty multicast <id1,id2> "<prompt>" Inject text into multiple specific sessions
557
759
  telepty broadcast "<prompt>" Inject text into ALL active sessions
760
+ telepty rename <old_id> <new_id> Rename a session (updates terminal title too)
558
761
  telepty listen Listen to the event bus and print JSON to stdout
559
762
  telepty monitor Human-readable real-time billboard of bus events
560
763
  telepty update Update telepty to the latest version