@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.
- package/daemon.js +4 -1
- package/package.json +1 -1
- 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
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
|
}
|