@dmsdc-ai/aigentry-telepty 0.1.58 → 0.1.60

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.
Files changed (3) hide show
  1. package/daemon.js +4 -1
  2. package/package.json +1 -1
  3. package/tui.js +190 -1
package/daemon.js CHANGED
@@ -792,7 +792,10 @@ app.post('/api/sessions/:id/inject', (req, res) => {
792
792
  }
793
793
  if (!kittyOk) {
794
794
  // Fallback: WS (works with new allow bridges that have queue flush)
795
- writeToSession(finalPrompt);
795
+ const wsOk = writeToSession(finalPrompt);
796
+ if (!wsOk) {
797
+ return res.status(503).json({ error: 'Process not connected' });
798
+ }
796
799
  console.log(`[INJECT] WS fallback for ${id}`);
797
800
  }
798
801
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
package/tui.js CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const blessed = require('blessed');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const { execSync, execFileSync } = require('child_process');
4
8
  const { getConfig } = require('./auth');
5
9
 
6
10
  const PORT = process.env.PORT || 3848;
7
11
  const DAEMON_URL = `http://localhost:${PORT}`;
8
12
  const POLL_INTERVAL = 2000;
9
13
  const STALE_THRESHOLD = 120; // seconds idle before "stale"
14
+ const PROJECTS_DIR = path.join(os.homedir(), 'projects');
15
+ const DEFAULT_CLI = 'claude --dangerously-skip-permissions';
10
16
 
11
17
  class TuiDashboard {
12
18
  constructor() {
@@ -73,6 +79,175 @@ class TuiDashboard {
73
79
  }
74
80
  }
75
81
 
82
+ // ── Session lifecycle (P1) ──────────────────────────────────
83
+
84
+ findKittySocket() {
85
+ try {
86
+ const files = fs.readdirSync('/tmp').filter(f => f.startsWith('kitty-sock'));
87
+ return files.length > 0 ? '/tmp/' + files[0] : null;
88
+ } catch { return null; }
89
+ }
90
+
91
+ findKittyWindowId(sock, sessionId) {
92
+ try {
93
+ const raw = execSync(`kitty @ --to unix:${sock} ls`, { timeout: 3000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
94
+ const data = JSON.parse(raw);
95
+ for (const osw of data) {
96
+ for (const tab of osw.tabs) {
97
+ for (const w of tab.windows) {
98
+ for (const p of (w.foreground_processes || [])) {
99
+ const cmd = (p.cmdline || []).join(' ');
100
+ if (cmd.includes('--id ' + sessionId) || cmd.includes('--id=' + sessionId)) {
101
+ return w.id;
102
+ }
103
+ }
104
+ // Also match by window title
105
+ if (w.title && w.title.includes(sessionId)) return w.id;
106
+ }
107
+ }
108
+ }
109
+ } catch {}
110
+ return null;
111
+ }
112
+
113
+ discoverProjects() {
114
+ try {
115
+ return fs.readdirSync(PROJECTS_DIR, { withFileTypes: true })
116
+ .filter(d => d.isDirectory() && d.name.startsWith('aigentry-') &&
117
+ fs.existsSync(path.join(PROJECTS_DIR, d.name, '.git')))
118
+ .map(d => ({ name: d.name, cwd: path.join(PROJECTS_DIR, d.name) }));
119
+ } catch { return []; }
120
+ }
121
+
122
+ async startSession(project) {
123
+ const sock = this.findKittySocket();
124
+ if (!sock) return this.setStatus('{red-fg}No kitty socket found{/}');
125
+
126
+ const cli = DEFAULT_CLI;
127
+ const cliParts = cli.split(' ');
128
+ let teleptyPath, cliPath;
129
+ try { teleptyPath = execSync('which telepty', { encoding: 'utf8' }).trim(); }
130
+ catch { teleptyPath = path.join(__dirname, 'cli.js'); }
131
+ try { cliPath = execSync(`which ${cliParts[0]}`, { encoding: 'utf8' }).trim(); }
132
+ catch { cliPath = cliParts[0]; }
133
+ const cliArgs = cliParts.slice(1).join(' ');
134
+ const nodePath = process.execPath;
135
+
136
+ const sessionId = `${project.name}-${cliParts[0]}`;
137
+ const shellCmd = `unset TELEPTY_SESSION_ID; ${nodePath} ${teleptyPath} allow --id ${sessionId} ${cliPath}${cliArgs ? ' ' + cliArgs : ''}`;
138
+
139
+ try {
140
+ execFileSync('kitty', ['@', '--to', `unix:${sock}`,
141
+ 'launch', '--type=tab', '--tab-title', project.name, '--cwd', project.cwd,
142
+ '--env', 'TELEPTY_SESSION_ID=',
143
+ '--env', `PATH=${process.env.PATH}`,
144
+ '/bin/zsh', '-c', shellCmd
145
+ ], { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
146
+ this.setStatus(`{green-fg}Started ${sessionId}{/}`);
147
+ setTimeout(() => this.fetchSessions(), 2000);
148
+ } catch (e) {
149
+ this.setStatus(`{red-fg}Start failed: ${e.message}{/}`);
150
+ }
151
+ }
152
+
153
+ async killSession(id) {
154
+ try {
155
+ // Send Ctrl+C to kitty window first
156
+ const sock = this.findKittySocket();
157
+ if (sock) {
158
+ const wid = this.findKittyWindowId(sock, id);
159
+ if (wid) {
160
+ try {
161
+ execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\x03'`, {
162
+ timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
163
+ });
164
+ } catch {}
165
+ }
166
+ }
167
+ // Deregister from daemon
168
+ await this.apiFetch(`/api/sessions/${encodeURIComponent(id)}`, { method: 'DELETE' });
169
+ this.setStatus(`{green-fg}Killed ${id}{/}`);
170
+ setTimeout(() => this.fetchSessions(), 1000);
171
+ } catch (e) {
172
+ this.setStatus(`{red-fg}Kill error: ${e.message}{/}`);
173
+ }
174
+ }
175
+
176
+ async purgeStale() {
177
+ const staleSessions = this.sessions.filter(s => {
178
+ const idle = s.idleSeconds;
179
+ return (idle !== null && idle > STALE_THRESHOLD) || s.active_clients === 0;
180
+ });
181
+ if (staleSessions.length === 0) {
182
+ return this.setStatus('{yellow-fg}No stale sessions to purge{/}');
183
+ }
184
+
185
+ const sock = this.findKittySocket();
186
+ let purged = 0;
187
+ for (const s of staleSessions) {
188
+ try {
189
+ // Send Ctrl+C to the session's kitty window
190
+ if (sock) {
191
+ const wid = this.findKittyWindowId(sock, s.id);
192
+ if (wid) {
193
+ execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\x03'`, {
194
+ timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
195
+ });
196
+ }
197
+ }
198
+ // Deregister from daemon
199
+ await this.apiFetch(`/api/sessions/${encodeURIComponent(s.id)}`, { method: 'DELETE' });
200
+ purged++;
201
+ } catch {}
202
+ }
203
+ this.setStatus(`{green-fg}Purged ${purged}/${staleSessions.length} stale sessions{/}`);
204
+ setTimeout(() => this.fetchSessions(), 1000);
205
+ }
206
+
207
+ showProjectPicker() {
208
+ const projects = this.discoverProjects();
209
+ // Filter out projects that already have active sessions
210
+ const activeIds = new Set(this.sessions.map(s => s.id));
211
+ const available = projects.filter(p => {
212
+ const expectedId = `${p.name}-claude`;
213
+ return !activeIds.has(expectedId);
214
+ });
215
+
216
+ if (available.length === 0) {
217
+ return this.setStatus('{yellow-fg}All projects already have active sessions{/}');
218
+ }
219
+
220
+ const picker = blessed.list({
221
+ parent: this.screen,
222
+ top: 'center', left: 'center',
223
+ width: '50%', height: Math.min(available.length + 2, 20),
224
+ border: { type: 'line' },
225
+ label: ' Start Session — Select Project ',
226
+ tags: true,
227
+ keys: true, vi: true, mouse: true,
228
+ items: available.map(p => ` ${p.name}`),
229
+ style: {
230
+ border: { fg: 'green' },
231
+ selected: { bg: 'green', fg: 'black', bold: true },
232
+ item: { fg: 'white' }
233
+ }
234
+ });
235
+
236
+ picker.focus();
237
+ picker.on('select', (item, index) => {
238
+ picker.destroy();
239
+ this.sessionList.focus();
240
+ this.screen.render();
241
+ this.startSession(available[index]);
242
+ });
243
+ picker.key(['escape', 'q'], () => {
244
+ picker.destroy();
245
+ this.sessionList.focus();
246
+ this.screen.render();
247
+ });
248
+ this.screen.render();
249
+ }
250
+
76
251
  // ── Event Bus ────────────────────────────────────────────────
77
252
 
78
253
  connectBus() {
@@ -166,7 +341,7 @@ class TuiDashboard {
166
341
  bottom: 1, left: 0, width: '100%', height: 1,
167
342
  tags: true,
168
343
  style: { fg: 'white', bg: 'gray' },
169
- content: ' {bold}i{/}:Inject {bold}b{/}:Broadcast {bold}r{/}:Refresh {bold}q{/}:Quit'
344
+ content: ' {bold}s{/}:Start {bold}k{/}:Kill {bold}i{/}:Inject {bold}b{/}:Broadcast {bold}p{/}:Purge {bold}r{/}:Refresh {bold}q{/}:Quit'
170
345
  });
171
346
 
172
347
  // Status bar
@@ -204,6 +379,20 @@ class TuiDashboard {
204
379
  this.setStatus('{green-fg}Refreshed{/}');
205
380
  });
206
381
 
382
+ this.screen.key(['s'], () => {
383
+ this.showProjectPicker();
384
+ });
385
+
386
+ this.screen.key(['k'], () => {
387
+ const session = this.sessions[this.selectedIndex];
388
+ if (!session) return this.setStatus('{red-fg}No session selected{/}');
389
+ this.killSession(session.id);
390
+ });
391
+
392
+ this.screen.key(['p'], () => {
393
+ this.purgeStale();
394
+ });
395
+
207
396
  this.sessionList.focus();
208
397
  this.screen.render();
209
398
  }