@dmsdc-ai/aigentry-telepty 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -55,3 +55,14 @@ npm run test:watch
55
55
  ```
56
56
 
57
57
  The automated suite covers config generation, daemon HTTP APIs, WebSocket attach/output flow, bus events, session deletion regressions, and CLI smoke tests against a real daemon process.
58
+
59
+ ## Skill Installation
60
+
61
+ The package installer opens the telepty skill TUI automatically when you run it in a terminal.
62
+
63
+ To reopen it later, run `telepty` and choose `Install telepty skills`.
64
+
65
+ The TUI lets you choose:
66
+ - which packaged skills to install
67
+ - which target clients to install into (`Claude Code`, `Codex`, `Gemini`)
68
+ - whether each target uses a global path, the current project path, or a custom path
package/cli.js CHANGED
@@ -9,6 +9,8 @@ const prompts = require('prompts');
9
9
  const updateNotifier = require('update-notifier');
10
10
  const pkg = require('./package.json');
11
11
  const { getConfig } = require('./auth');
12
+ const { attachInteractiveTerminal } = require('./interactive-terminal');
13
+ const { runInteractiveSkillInstaller } = require('./skill-installer');
12
14
  const args = process.argv.slice(2);
13
15
 
14
16
  // Check for updates unless explicitly disabled for tests/CI.
@@ -91,29 +93,24 @@ async function ensureDaemonRunning() {
91
93
  async function manageInteractiveAttach(sessionId, targetHost) {
92
94
  const wsUrl = `ws://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
93
95
  const ws = new WebSocket(wsUrl);
94
- let inputHandler = null;
95
- let resizeHandler = null;
96
+ let cleanupTerminal = null;
96
97
  return new Promise((resolve) => {
97
98
  ws.on('open', () => {
98
99
  // Set Ghostty tab title to show session ID
99
100
  process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}\x07`);
100
101
  console.log(`\n\x1b[32mEntered room '${sessionId}'.\x1b[0m\n`);
101
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
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();
102
+ cleanupTerminal = attachInteractiveTerminal(process.stdin, process.stdout, {
103
+ onData: (d) => ws.send(JSON.stringify({ type: 'input', data: d.toString() })),
104
+ onResize: () => ws.send(JSON.stringify({ type: 'resize', cols: process.stdout.columns, rows: process.stdout.rows }))
105
+ });
107
106
  });
108
107
  ws.on('message', m => {
109
108
  const msg = JSON.parse(m);
110
109
  if (msg.type === 'output') process.stdout.write(msg.data);
111
110
  });
112
111
  ws.on('close', async () => {
113
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
114
112
  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);
113
+ if (cleanupTerminal) cleanupTerminal();
117
114
 
118
115
  // Check if other clients are still attached before destroying
119
116
  try {
@@ -152,6 +149,7 @@ async function manageInteractive() {
152
149
  { title: '🔌 Allow inject (Run CLI with inject)', value: 'allow' },
153
150
  { title: '💬 Send message to a room (Inject command)', value: 'inject' },
154
151
  { title: '📋 View all open rooms (List sessions)', value: 'list' },
152
+ { title: '🧠 Install telepty skills', value: 'install-skills' },
155
153
  { title: '🔄 Update telepty to latest version', value: 'update' },
156
154
  { title: '❌ Exit', value: 'exit' }
157
155
  ]
@@ -189,6 +187,15 @@ async function manageInteractive() {
189
187
  continue;
190
188
  }
191
189
 
190
+ if (response.action === 'install-skills') {
191
+ try {
192
+ await runInteractiveSkillInstaller({ packageRoot: __dirname, cwd: process.cwd() });
193
+ } catch (e) {
194
+ console.error(`\n❌ ${e.message}\n`);
195
+ }
196
+ continue;
197
+ }
198
+
192
199
  if (response.action === 'list') {
193
200
  console.log('\n');
194
201
  const sessions = await discoverSessions();
@@ -469,11 +476,13 @@ async function main() {
469
476
  process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}\x07`);
470
477
  console.log(`\x1b[32m⚡ '${command}' is now session '\x1b[36m${sessionId}\x1b[32m'. Inject allowed.\x1b[0m\n`);
471
478
 
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());
479
+ const cleanupTerminal = attachInteractiveTerminal(process.stdin, process.stdout, {
480
+ onData: (data) => {
481
+ child.write(data.toString());
482
+ },
483
+ onResize: () => {
484
+ child.resize(process.stdout.columns, process.stdout.rows);
485
+ }
477
486
  });
478
487
 
479
488
  // Relay PTY output to current terminal + send to daemon for attach clients
@@ -484,14 +493,9 @@ async function main() {
484
493
  }
485
494
  });
486
495
 
487
- // Handle terminal resize
488
- process.stdout.on('resize', () => {
489
- child.resize(process.stdout.columns, process.stdout.rows);
490
- });
491
-
492
496
  // Handle child exit
493
497
  child.onExit(({ exitCode }) => {
494
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
498
+ cleanupTerminal();
495
499
  process.stdout.write(`\x1b]0;\x07`);
496
500
  console.log(`\n\x1b[33mSession '${sessionId}' exited (code ${exitCode}).\x1b[0m`);
497
501
 
@@ -540,8 +544,7 @@ async function main() {
540
544
 
541
545
  const wsUrl = `ws://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
542
546
  const ws = new WebSocket(wsUrl);
543
- let inputHandler = null;
544
- let resizeHandler = null;
547
+ let cleanupTerminal = null;
545
548
 
546
549
  ws.on('open', () => {
547
550
  // Set Ghostty tab title to show session ID
@@ -549,25 +552,18 @@ async function main() {
549
552
  process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}${hostSuffix}\x07`);
550
553
  console.log(`\x1b[32mEntered room '${sessionId}'${hostSuffix ? ` (${targetHost})` : ''}.\x1b[0m\n`);
551
554
 
552
- if (process.stdin.isTTY) {
553
- process.stdin.setRawMode(true);
554
- }
555
-
556
- inputHandler = (data) => {
557
- ws.send(JSON.stringify({ type: 'input', data: data.toString() }));
558
- };
559
- process.stdin.on('data', inputHandler);
560
-
561
- resizeHandler = () => {
562
- ws.send(JSON.stringify({
563
- type: 'resize',
564
- cols: process.stdout.columns,
565
- rows: process.stdout.rows
566
- }));
567
- };
568
-
569
- process.stdout.on('resize', resizeHandler);
570
- resizeHandler(); // Initial resize
555
+ cleanupTerminal = attachInteractiveTerminal(process.stdin, process.stdout, {
556
+ onData: (data) => {
557
+ ws.send(JSON.stringify({ type: 'input', data: data.toString() }));
558
+ },
559
+ onResize: () => {
560
+ ws.send(JSON.stringify({
561
+ type: 'resize',
562
+ cols: process.stdout.columns,
563
+ rows: process.stdout.rows
564
+ }));
565
+ }
566
+ });
571
567
  });
572
568
 
573
569
  ws.on('message', (message) => {
@@ -578,12 +574,8 @@ async function main() {
578
574
  });
579
575
 
580
576
  ws.on('close', async (code, reason) => {
581
- if (process.stdin.isTTY) {
582
- process.stdin.setRawMode(false);
583
- }
584
577
  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);
578
+ if (cleanupTerminal) cleanupTerminal();
587
579
 
588
580
  // Check if other clients are still attached before destroying
589
581
  try {
package/install.js CHANGED
@@ -4,6 +4,7 @@ const { execSync, spawn } = require('child_process');
4
4
  const os = require('os');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
+ const { runInteractiveSkillInstaller } = require('./skill-installer');
7
8
 
8
9
  console.log("🚀 Installing @dmsdc-ai/aigentry-telepty...");
9
10
 
@@ -16,38 +17,69 @@ function run(cmd) {
16
17
  }
17
18
  }
18
19
 
19
- // 1. Install globally via npm
20
- console.log("📦 Installing package globally...");
21
- run("npm install -g @dmsdc-ai/aigentry-telepty");
20
+ function resolveInstalledPackageRoot() {
21
+ try {
22
+ const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
23
+ return path.join(globalRoot, '@dmsdc-ai', 'aigentry-telepty');
24
+ } catch (e) {
25
+ return __dirname;
26
+ }
27
+ }
28
+
29
+ async function installSkills() {
30
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
31
+ console.log('⏭️ Skipping interactive skill installation (no TTY).');
32
+ console.log(' Run `telepty` later and choose "Install telepty skills".');
33
+ return;
34
+ }
35
+
36
+ console.log('\n📋 Telepty skill installation');
22
37
 
23
- // 2. Find executable
24
- let teleptyPath = '';
25
- try {
26
- teleptyPath = execSync(os.platform() === 'win32' ? 'where telepty' : 'which telepty', { encoding: 'utf8' }).split('\n')[0].trim();
27
- } catch (e) {
28
- teleptyPath = 'telepty'; // fallback
38
+ try {
39
+ await runInteractiveSkillInstaller({
40
+ packageRoot: resolveInstalledPackageRoot(),
41
+ cwd: process.cwd()
42
+ });
43
+ } catch (e) {
44
+ console.warn('⚠️ Could not install telepty skills:', e.message);
45
+ }
29
46
  }
30
47
 
31
- // 3. Setup OS-specific autostart or background daemon
32
- const platform = os.platform();
33
-
34
- if (platform === 'win32') {
35
- console.log("⚙️ Setting up Windows background process...");
36
- // Launch daemon in background detaching from current console
37
- const subprocess = spawn(teleptyPath, ['daemon'], {
38
- detached: true,
39
- stdio: 'ignore',
40
- windowsHide: true
41
- });
42
- subprocess.unref();
43
- console.log("✅ Windows daemon started in background.");
44
-
45
- } else if (platform === 'darwin') {
46
- console.log("⚙️ Setting up macOS launchd service...");
47
- const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.aigentry.telepty.plist');
48
- fs.mkdirSync(path.dirname(plistPath), { recursive: true });
49
-
50
- const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
48
+ (async () => {
49
+ // 1. Install globally via npm
50
+ console.log("📦 Installing package globally...");
51
+ run("npm install -g @dmsdc-ai/aigentry-telepty");
52
+
53
+ // 2. Install telepty skills for supported clients
54
+ await installSkills();
55
+
56
+ // 3. Find executable
57
+ let teleptyPath = '';
58
+ try {
59
+ teleptyPath = execSync(os.platform() === 'win32' ? 'where telepty' : 'which telepty', { encoding: 'utf8' }).split('\n')[0].trim();
60
+ } catch (e) {
61
+ teleptyPath = 'telepty'; // fallback
62
+ }
63
+
64
+ // 4. Setup OS-specific autostart or background daemon
65
+ const platform = os.platform();
66
+
67
+ if (platform === 'win32') {
68
+ console.log("⚙️ Setting up Windows background process...");
69
+ const subprocess = spawn(teleptyPath, ['daemon'], {
70
+ detached: true,
71
+ stdio: 'ignore',
72
+ windowsHide: true
73
+ });
74
+ subprocess.unref();
75
+ console.log("✅ Windows daemon started in background.");
76
+
77
+ } else if (platform === 'darwin') {
78
+ console.log("⚙️ Setting up macOS launchd service...");
79
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.aigentry.telepty.plist');
80
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
81
+
82
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
51
83
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
52
84
  <plist version="1.0">
53
85
  <dict>
@@ -64,20 +96,19 @@ if (platform === 'win32') {
64
96
  <true/>
65
97
  </dict>
66
98
  </plist>`;
67
-
68
- fs.writeFileSync(plistPath, plistContent);
69
- try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`); } catch(e){}
70
- run(`launchctl load "${plistPath}"`);
71
- console.log("✅ macOS LaunchAgent installed and started.");
72
-
73
- } else {
74
- // Linux
75
- try {
76
- // Check if systemd is available and running as root/sudo
77
- execSync('systemctl --version', { stdio: 'ignore' });
78
- if (process.getuid && process.getuid() === 0) {
79
- console.log("⚙️ Setting up systemd service for Linux...");
80
- const serviceContent = `[Unit]
99
+
100
+ fs.writeFileSync(plistPath, plistContent);
101
+ try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`); } catch(e){}
102
+ run(`launchctl load "${plistPath}"`);
103
+ console.log("✅ macOS LaunchAgent installed and started.");
104
+
105
+ } else {
106
+ // Linux
107
+ try {
108
+ execSync('systemctl --version', { stdio: 'ignore' });
109
+ if (process.getuid && process.getuid() === 0) {
110
+ console.log("⚙️ Setting up systemd service for Linux...");
111
+ const serviceContent = `[Unit]
81
112
  Description=Telepty Daemon
82
113
  After=network.target
83
114
 
@@ -90,25 +121,26 @@ Environment=NODE_ENV=production
90
121
 
91
122
  [Install]
92
123
  WantedBy=multi-user.target`;
93
-
94
- fs.writeFileSync('/etc/systemd/system/telepty.service', serviceContent);
95
- run('systemctl daemon-reload');
96
- run('systemctl enable telepty');
97
- run('systemctl start telepty');
98
- console.log("✅ Systemd service installed and started.");
99
- process.exit(0);
100
- }
101
- } catch(e) {}
102
-
103
- // Fallback for Linux without systemd or non-root
104
- console.log("⚠️ Skipping systemd (no root or no systemd). Starting in background...");
105
- const subprocess = spawn(teleptyPath, ['daemon'], {
106
- detached: true,
107
- stdio: 'ignore'
108
- });
109
- subprocess.unref();
110
- console.log("✅ Linux daemon started in background using nohup equivalent.");
111
- }
112
124
 
113
- console.log("\n🎉 Installation complete! Telepty daemon is running.");
114
- console.log("👉 Try running: telepty attach\n");
125
+ fs.writeFileSync('/etc/systemd/system/telepty.service', serviceContent);
126
+ run('systemctl daemon-reload');
127
+ run('systemctl enable telepty');
128
+ run('systemctl start telepty');
129
+ console.log("✅ Systemd service installed and started.");
130
+ process.exit(0);
131
+ }
132
+ } catch(e) {}
133
+
134
+ // Fallback for Linux without systemd or non-root
135
+ console.log("⚠️ Skipping systemd (no root or no systemd). Starting in background...");
136
+ const subprocess = spawn(teleptyPath, ['daemon'], {
137
+ detached: true,
138
+ stdio: 'ignore'
139
+ });
140
+ subprocess.unref();
141
+ console.log("✅ Linux daemon started in background using nohup equivalent.");
142
+ }
143
+
144
+ console.log("\n🎉 Installation complete! Telepty daemon is running.");
145
+ console.log("👉 Try running: telepty attach\n");
146
+ })();
package/install.ps1 CHANGED
@@ -31,3 +31,4 @@ Write-Host "Success: Windows daemon started in background." -ForegroundColor Gre
31
31
 
32
32
  Write-Host "`nInstallation complete! Telepty daemon is running." -ForegroundColor Cyan
33
33
  Write-Host "Next step: Try running: telepty attach" -ForegroundColor Yellow
34
+ Write-Host "Optional: Run telepty and choose 'Install telepty skills' to add skills for Claude Code, Codex, or Gemini" -ForegroundColor Yellow
package/install.sh CHANGED
@@ -116,3 +116,4 @@ fi
116
116
  echo ""
117
117
  echo "🎉 Installation complete! Telepty daemon is running in the background."
118
118
  echo "👉 Try running: telepty attach"
119
+ echo "👉 Optional: run telepty and choose 'Install telepty skills' to add skills for Claude Code, Codex, or Gemini"
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ function removeListener(stream, eventName, handler) {
4
+ if (!handler || !stream) {
5
+ return;
6
+ }
7
+
8
+ if (typeof stream.off === 'function') {
9
+ stream.off(eventName, handler);
10
+ return;
11
+ }
12
+
13
+ if (typeof stream.removeListener === 'function') {
14
+ stream.removeListener(eventName, handler);
15
+ }
16
+ }
17
+
18
+ function attachInteractiveTerminal(input, output, handlers = {}) {
19
+ const { onData = null, onResize = null } = handlers;
20
+
21
+ if (input && input.isTTY && typeof input.setRawMode === 'function') {
22
+ input.setRawMode(true);
23
+ }
24
+
25
+ if (input && typeof input.resume === 'function') {
26
+ input.resume();
27
+ }
28
+
29
+ if (input && onData) {
30
+ input.on('data', onData);
31
+ }
32
+
33
+ if (output && onResize) {
34
+ output.on('resize', onResize);
35
+ onResize();
36
+ }
37
+
38
+ return () => {
39
+ removeListener(input, 'data', onData);
40
+ removeListener(output, 'resize', onResize);
41
+
42
+ if (input && input.isTTY && typeof input.setRawMode === 'function') {
43
+ input.setRawMode(false);
44
+ }
45
+
46
+ if (input && typeof input.pause === 'function') {
47
+ input.pause();
48
+ }
49
+ };
50
+ }
51
+
52
+ module.exports = {
53
+ attachInteractiveTerminal
54
+ };
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "telepty": "cli.js",
7
7
  "telepty-install": "install.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "node --test test/auth.test.js test/daemon.test.js test/cli.test.js",
11
- "test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/cli.test.js",
12
- "test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/cli.test.js"
10
+ "test": "node --test test/auth.test.js test/daemon.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js",
11
+ "test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js",
12
+ "test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js"
13
13
  },
14
14
  "keywords": [],
15
15
  "author": "",