@dmsdc-ai/aigentry-telepty 0.0.18 → 0.1.0
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/.deliberation_request.json +1 -0
- package/.deliberation_request2.json +1 -0
- package/.deliberation_request3.json +1 -0
- package/.gemini/skills/telepty/SKILL.md +3 -1
- package/cli.js +45 -26
- package/daemon.js +72 -15
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"topic": "Critique and verify the Foreground-Only Session Lifecycle for telepty. The user proposes that all AI workers/sessions MUST run in visible, physical terminal windows. When the user closes the window, the session dies. No invisible background sessions exist. What are the edge cases, UX flaws, or technical limitations of this approach?", "speakers": ["claude", "gemini", "codex"], "role_preset": "critic"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"topic": "Re-evaluate the telepty architecture based on the user`s strict boundaries: 1. A session equals a physical, visible terminal window. If the window closes, the session MUST die. No zombies. 2. Background persistence is NOT telepty`s job (users can use tmux if they want, but telepty doesn`t manage it). 3. Unexpected disconnects (network drop, sleep) are treated as session death, and recovery is handled via a higher-level `Handoff` of context to a new session, NOT by keeping zombie processes alive. Double-check this philosophy for flaws, focusing on how AI agents will communicate (Pub/Sub) within this strict ephemeral foreground model.", "speakers": ["claude", "codex", "gemini"], "role_preset": "review"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"topic": "Final architectural verification of the Telepty Boundary Model. The user asserts a hard line on responsibility: 1. If a session/process dies, context is lost. That is the CLI/Terminal`s problem, not telepty`s. 2. If a Pub message is sent to a dead Sub, it is lost. Telepty just drops it or relies on Ack. If the sub reconnects in time, it gets it; if not, drop it. 3. If Wi-Fi drops, CLI dies, agent dies, scripts die. Telepty does not care about split-brain or script recovery. Is this stateless, hyper-minimalist `dumb pipe` philosophy technically viable for our goals?", "speakers": ["claude", "gemini", "codex"], "role_preset": "consensus"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Help the user interact with the `telepty` daemon, check their current session ID, list active sessions, and inject commands into remote or local PTY sessions.
|
|
5
5
|
|
|
6
6
|
**Trigger:**
|
|
7
|
-
When the user asks about their current session ID (e.g. "내 세션 ID가 뭐야?"), wants to check active sessions ("세션 목록 보여줘"),
|
|
7
|
+
When the user asks about their current session ID (e.g. "내 세션 ID가 뭐야?"), wants to check active sessions ("세션 목록 보여줘"), wants to inject a prompt/command into a specific session ("dustcraw한테 메시지 보내줘"), or wants to update telepty to the latest version ("업데이트 해줘").
|
|
8
8
|
|
|
9
9
|
**Instructions:**
|
|
10
10
|
1. **To check the current session ID:**
|
|
@@ -17,3 +17,5 @@ When the user asks about their current session ID (e.g. "내 세션 ID가 뭐야
|
|
|
17
17
|
- For a single session: Run `telepty inject <target_session_id> "<message or command>"`.
|
|
18
18
|
- For broadcasting to ALL active sessions: Run `telepty broadcast "<message or command>"`.
|
|
19
19
|
- For multicasting to multiple specific sessions: Run `telepty multicast <id1>,<id2> "<message or command>"`.
|
|
20
|
+
4. **To update telepty:**
|
|
21
|
+
- Run `telepty update`.
|
package/cli.js
CHANGED
|
@@ -104,6 +104,36 @@ async function ensureDaemonRunning() {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
async function manageInteractiveAttach(sessionId, targetHost) {
|
|
108
|
+
const wsUrl = `ws://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
|
|
109
|
+
const ws = new WebSocket(wsUrl);
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
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`);
|
|
113
|
+
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();
|
|
117
|
+
});
|
|
118
|
+
ws.on('message', m => {
|
|
119
|
+
const msg = JSON.parse(m);
|
|
120
|
+
if (msg.type === 'output') process.stdout.write(msg.data);
|
|
121
|
+
});
|
|
122
|
+
ws.on('close', async () => {
|
|
123
|
+
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
|
|
128
|
+
try {
|
|
129
|
+
await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
|
|
130
|
+
} catch(e) {}
|
|
131
|
+
|
|
132
|
+
resolve();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
async function manageInteractive() {
|
|
108
138
|
console.clear();
|
|
109
139
|
console.log('\x1b[36m\x1b[1m⚡ Telepty Agent Manager\x1b[0m\n');
|
|
@@ -189,7 +219,12 @@ async function manageInteractive() {
|
|
|
189
219
|
});
|
|
190
220
|
const data = await res.json();
|
|
191
221
|
if (!res.ok) console.error(`\n❌ Error: ${data.error}\n`);
|
|
192
|
-
else
|
|
222
|
+
else {
|
|
223
|
+
// Immediately attach to the spawned session automatically
|
|
224
|
+
console.log(`\n✅ Session '\x1b[36m${data.session_id}\x1b[0m' spawned. Entering room automatically...\n`);
|
|
225
|
+
args[1] = data.session_id; // Spoof args for attach
|
|
226
|
+
return manageInteractiveAttach(data.session_id, '127.0.0.1');
|
|
227
|
+
}
|
|
193
228
|
} catch (e) {
|
|
194
229
|
console.error('\n❌ Failed to connect to local daemon. Is it running?\n');
|
|
195
230
|
}
|
|
@@ -215,27 +250,7 @@ async function manageInteractive() {
|
|
|
215
250
|
if (!target) continue;
|
|
216
251
|
|
|
217
252
|
if (response.action === 'attach') {
|
|
218
|
-
|
|
219
|
-
const ws = new WebSocket(wsUrl);
|
|
220
|
-
await new Promise((resolve) => {
|
|
221
|
-
ws.on('open', () => {
|
|
222
|
-
console.log(`\n\x1b[32mConnected to '${target.id}'. Press Ctrl+C to detach.\x1b[0m\n`);
|
|
223
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
224
|
-
process.stdin.on('data', d => ws.send(JSON.stringify({ type: 'input', data: d.toString() })));
|
|
225
|
-
const resizer = () => ws.send(JSON.stringify({ type: 'resize', cols: process.stdout.columns, rows: process.stdout.rows }));
|
|
226
|
-
process.stdout.on('resize', resizer); resizer();
|
|
227
|
-
});
|
|
228
|
-
ws.on('message', m => {
|
|
229
|
-
const msg = JSON.parse(m);
|
|
230
|
-
if (msg.type === 'output') process.stdout.write(msg.data);
|
|
231
|
-
});
|
|
232
|
-
ws.on('close', () => {
|
|
233
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
234
|
-
console.log(`\n\x1b[33mDisconnected from session.\x1b[0m\n`);
|
|
235
|
-
process.stdin.removeAllListeners('data');
|
|
236
|
-
resolve();
|
|
237
|
-
});
|
|
238
|
-
});
|
|
253
|
+
await manageInteractiveAttach(target.id, target.host);
|
|
239
254
|
continue;
|
|
240
255
|
}
|
|
241
256
|
|
|
@@ -340,7 +355,8 @@ async function main() {
|
|
|
340
355
|
});
|
|
341
356
|
const data = await res.json();
|
|
342
357
|
if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
|
|
343
|
-
console.log(`✅ Session '\x1b[36m${data.session_id}\x1b[0m' spawned
|
|
358
|
+
console.log(`✅ Session '\x1b[36m${data.session_id}\x1b[0m' spawned. Entering room automatically...`);
|
|
359
|
+
return manageInteractiveAttach(data.session_id, '127.0.0.1');
|
|
344
360
|
} catch (e) { console.error('❌ Failed to connect to daemon. Is it running?'); }
|
|
345
361
|
return;
|
|
346
362
|
}
|
|
@@ -380,7 +396,7 @@ async function main() {
|
|
|
380
396
|
const ws = new WebSocket(wsUrl);
|
|
381
397
|
|
|
382
398
|
ws.on('open', () => {
|
|
383
|
-
console.log(`\x1b[
|
|
399
|
+
console.log(`\x1b[32mEntered room '${sessionId}' at ${targetHost}. The room will be destroyed if you exit.\x1b[0m\n`);
|
|
384
400
|
|
|
385
401
|
if (process.stdin.isTTY) {
|
|
386
402
|
process.stdin.setRawMode(true);
|
|
@@ -409,11 +425,14 @@ async function main() {
|
|
|
409
425
|
}
|
|
410
426
|
});
|
|
411
427
|
|
|
412
|
-
ws.on('close', (code, reason) => {
|
|
428
|
+
ws.on('close', async (code, reason) => {
|
|
413
429
|
if (process.stdin.isTTY) {
|
|
414
430
|
process.stdin.setRawMode(false);
|
|
415
431
|
}
|
|
416
|
-
console.log(`\n\x1b[
|
|
432
|
+
console.log(`\n\x1b[33mLeft room. Destroying session '${sessionId}' to prevent zombies... (Code: ${code})\x1b[0m`);
|
|
433
|
+
try {
|
|
434
|
+
await fetchWithAuth(`http://${targetHost}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
|
|
435
|
+
} catch(e) {}
|
|
417
436
|
process.exit(0);
|
|
418
437
|
});
|
|
419
438
|
|
package/daemon.js
CHANGED
|
@@ -164,25 +164,29 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
164
164
|
}
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
app.delete('/api/sessions/:id', (req, res) => {
|
|
168
|
+
const { id } = req.params;
|
|
169
|
+
const session = sessions[id];
|
|
170
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
171
|
+
try {
|
|
172
|
+
session.ptyProcess.kill();
|
|
173
|
+
delete sessions[id];
|
|
174
|
+
console.log(`[KILL] Session ${id} forcefully closed`);
|
|
175
|
+
res.json({ success: true });
|
|
176
|
+
} catch (err) {
|
|
177
|
+
res.status(500).json({ error: err.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
167
181
|
const server = app.listen(PORT, HOST, () => {
|
|
168
182
|
console.log(`🚀 aigentry-telepty daemon listening on http://${HOST}:${PORT}`);
|
|
169
183
|
});
|
|
170
184
|
|
|
171
|
-
const wss = new WebSocketServer({ server });
|
|
172
|
-
|
|
173
|
-
wss.on('connection', (ws, req) => {
|
|
174
|
-
const isLocalhost = req.socket.remoteAddress === '127.0.0.1' || req.socket.remoteAddress === '::1' || req.socket.remoteAddress === '::ffff:127.0.0.1';
|
|
175
|
-
const isTailscale = req.socket.remoteAddress && req.socket.remoteAddress.startsWith('100.');
|
|
176
|
-
|
|
177
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
178
|
-
const token = url.searchParams.get('token');
|
|
179
185
|
|
|
180
|
-
|
|
181
|
-
console.warn(`[WS-AUTH] Rejected unauthorized WebSocket from ${req.socket.remoteAddress}`);
|
|
182
|
-
ws.close(1008, 'Unauthorized');
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
186
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
185
187
|
|
|
188
|
+
wss.on('connection', (ws, req) => {
|
|
189
|
+
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
186
190
|
const sessionId = url.pathname.split('/').pop();
|
|
187
191
|
const session = sessions[sessionId];
|
|
188
192
|
|
|
@@ -192,7 +196,7 @@ wss.on('connection', (ws, req) => {
|
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
session.clients.add(ws);
|
|
195
|
-
console.log(`[WS] Client attached to session
|
|
199
|
+
console.log(`[WS] Client attached to session \${sessionId} (Total: \${session.clients.size})`);
|
|
196
200
|
|
|
197
201
|
ws.on('message', (message) => {
|
|
198
202
|
try {
|
|
@@ -209,6 +213,59 @@ wss.on('connection', (ws, req) => {
|
|
|
209
213
|
|
|
210
214
|
ws.on('close', () => {
|
|
211
215
|
session.clients.delete(ws);
|
|
212
|
-
console.log(`[WS] Client detached from session
|
|
216
|
+
console.log(`[WS] Client detached from session \${sessionId} (Total: \${session.clients.size})`);
|
|
213
217
|
});
|
|
214
218
|
});
|
|
219
|
+
|
|
220
|
+
const busWss = new WebSocketServer({ noServer: true });
|
|
221
|
+
const busClients = new Set();
|
|
222
|
+
|
|
223
|
+
busWss.on('connection', (ws, req) => {
|
|
224
|
+
busClients.add(ws);
|
|
225
|
+
console.log('[BUS] New agent connected to event bus');
|
|
226
|
+
|
|
227
|
+
ws.on('message', (message) => {
|
|
228
|
+
try {
|
|
229
|
+
const msg = JSON.parse(message);
|
|
230
|
+
// For MVP, simply broadcast any valid JSON message to all other bus clients
|
|
231
|
+
busClients.forEach(client => {
|
|
232
|
+
if (client !== ws && client.readyState === 1) {
|
|
233
|
+
client.send(JSON.stringify(msg));
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.error('[BUS] Invalid message format', e);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
ws.on('close', () => {
|
|
242
|
+
busClients.delete(ws);
|
|
243
|
+
console.log('[BUS] Agent disconnected from event bus');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
server.on('upgrade', (req, socket, head) => {
|
|
248
|
+
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
249
|
+
const token = url.searchParams.get('token');
|
|
250
|
+
|
|
251
|
+
const isLocalhost = req.socket.remoteAddress === '127.0.0.1' || req.socket.remoteAddress === '::1' || req.socket.remoteAddress === '::ffff:127.0.0.1';
|
|
252
|
+
const isTailscale = req.socket.remoteAddress && req.socket.remoteAddress.startsWith('100.');
|
|
253
|
+
|
|
254
|
+
if (!isLocalhost && !isTailscale && token !== EXPECTED_TOKEN) {
|
|
255
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
256
|
+
socket.destroy();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (url.pathname.startsWith('/api/sessions/')) {
|
|
261
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
262
|
+
wss.emit('connection', ws, req);
|
|
263
|
+
});
|
|
264
|
+
} else if (url.pathname === '/api/bus') {
|
|
265
|
+
busWss.handleUpgrade(req, socket, head, (ws) => {
|
|
266
|
+
busWss.emit('connection', ws, req);
|
|
267
|
+
});
|
|
268
|
+
} else {
|
|
269
|
+
socket.destroy();
|
|
270
|
+
}
|
|
271
|
+
});
|