claude-remote-agent 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.
Files changed (4) hide show
  1. package/agent.js +470 -0
  2. package/cli.js +51 -0
  3. package/package.json +33 -0
  4. package/pty-bridge.py +114 -0
package/agent.js ADDED
@@ -0,0 +1,470 @@
1
+ const crypto = require('crypto');
2
+ const os = require('os');
3
+ const WebSocket = require('ws');
4
+ const { spawn } = require('child_process');
5
+ const fs = require('fs');
6
+ const readline = require('readline');
7
+ const path = require('path');
8
+
9
+ const SERVER_URL = process.env.SERVER_URL || 'wss://claude.pishchykau.eu';
10
+ const AGENT_NAME = process.env.AGENT_NAME || os.hostname();
11
+ const AGENT_KEY = process.env.AGENT_KEY || crypto.randomBytes(24).toString('base64url');
12
+ const PTY_BRIDGE = path.join(__dirname, 'pty-bridge.py');
13
+
14
+ const sessions = new Map();
15
+ let ws = null;
16
+ let reconnectTimer = null;
17
+ let reconnectDelay = 1000;
18
+
19
+ console.log('');
20
+ console.log(' ╔══════════════════════════════════════╗');
21
+ console.log(' ║ Claude Remote Agent ║');
22
+ console.log(' ╠══════════════════════════════════════╣');
23
+ console.log(` ║ Name: ${pad(AGENT_NAME, 28)}║`);
24
+ console.log(` ║ Host: ${pad(os.hostname(), 28)}║`);
25
+ console.log(` ║ Key: ${pad(AGENT_KEY, 28)}║`);
26
+ console.log(' ╠══════════════════════════════════════╣');
27
+ console.log(` ║ Server: ${pad(SERVER_URL, 27)}║`);
28
+ console.log(' ╚══════════════════════════════════════╝');
29
+ console.log('');
30
+
31
+ function pad(str, len) {
32
+ if (str.length > len) return str.substring(0, len - 1) + '…';
33
+ return str + ' '.repeat(len - str.length);
34
+ }
35
+
36
+ function connect() {
37
+ const params = new URLSearchParams({
38
+ role: 'agent',
39
+ key: AGENT_KEY,
40
+ name: AGENT_NAME,
41
+ hostname: os.hostname(),
42
+ platform: `${os.platform()}/${os.arch()}`,
43
+ });
44
+
45
+ ws = new WebSocket(`${SERVER_URL}?${params}`);
46
+
47
+ ws.on('open', () => {
48
+ console.log(`[${ts()}] Connected to server`);
49
+ reconnectDelay = 1000;
50
+ });
51
+
52
+ ws.on('message', (raw) => {
53
+ try {
54
+ const msg = JSON.parse(raw);
55
+
56
+ switch (msg.type) {
57
+ case 'registered':
58
+ console.log(`[${ts()}] Registered as agent ${msg.id}`);
59
+ break;
60
+ case 'create-session':
61
+ createSession(msg.id, msg.cwd, msg.cols, msg.rows, msg.resumeId);
62
+ break;
63
+ case 'kill-session':
64
+ killSession(msg.id);
65
+ break;
66
+ case 'request':
67
+ handleRequest(msg);
68
+ break;
69
+ case 'input': {
70
+ const s = sessions.get(msg.sessionId);
71
+ if (s && s.proc && s.proc.stdin.writable) s.proc.stdin.write(msg.data);
72
+ break;
73
+ }
74
+ case 'resize': {
75
+ const s = sessions.get(msg.sessionId);
76
+ if (s && s.proc && s.proc.stdin.writable) {
77
+ s.proc.stdin.write(`\x1b_RESIZE:${msg.cols},${msg.rows}\x1b\\`);
78
+ }
79
+ break;
80
+ }
81
+ case 'create-chat-session':
82
+ createChatSession(msg.id, msg.cwd, msg.resumeId);
83
+ break;
84
+ case 'chat-message':
85
+ handleChatMessage(msg.sessionId, msg.text, msg.images);
86
+ break;
87
+ }
88
+ } catch (err) {
89
+ console.error(`[${ts()}] Message error:`, err.message);
90
+ }
91
+ });
92
+
93
+ ws.on('close', (code) => {
94
+ ws = null;
95
+ if (code === 4009) {
96
+ console.log(`[${ts()}] Replaced by another agent with the same key`);
97
+ }
98
+ console.log(`[${ts()}] Disconnected. Reconnecting in ${reconnectDelay / 1000}s...`);
99
+ reconnectTimer = setTimeout(connect, reconnectDelay);
100
+ reconnectDelay = Math.min(reconnectDelay * 1.5, 30000);
101
+ });
102
+
103
+ ws.on('error', (err) => {
104
+ if (err.code !== 'ECONNREFUSED') {
105
+ console.error(`[${ts()}] Error:`, err.message);
106
+ }
107
+ });
108
+ }
109
+
110
+ function buildEnv(cwd) {
111
+ const env = { ...process.env, TERM: 'xterm-256color', PTY_CWD: cwd };
112
+ const localBin = path.join(process.env.HOME, '.local', 'bin');
113
+ if (!env.PATH?.includes(localBin)) {
114
+ env.PATH = localBin + ':' + (env.PATH || '');
115
+ }
116
+ return env;
117
+ }
118
+
119
+ function resolveDir(cwd) {
120
+ let workDir = cwd || process.env.HOME;
121
+ if (workDir.startsWith('~')) {
122
+ workDir = workDir.replace(/^~/, process.env.HOME);
123
+ }
124
+ return workDir;
125
+ }
126
+
127
+ function createSession(id, cwd, cols, rows, resumeId) {
128
+ const workDir = resolveDir(cwd);
129
+ const env = buildEnv(workDir);
130
+ const cmd = resumeId ? ['claude', '--resume', resumeId] : ['claude'];
131
+
132
+ let proc;
133
+ try {
134
+ proc = spawn('python3', [PTY_BRIDGE, String(cols), String(rows), ...cmd], {
135
+ stdio: ['pipe', 'pipe', 'pipe'],
136
+ env,
137
+ });
138
+ } catch (err) {
139
+ console.error(`[${ts()}] Session ${id} spawn error:`, err.message);
140
+ send({ type: 'exit', sessionId: id, exitCode: -1 });
141
+ return;
142
+ }
143
+
144
+ sessions.set(id, { id, proc, cwd: workDir });
145
+ console.log(`[${ts()}] Session ${id.slice(0, 8)} started in ${workDir}`);
146
+
147
+ proc.stdout.on('data', (data) => {
148
+ send({ type: 'output', sessionId: id, data: data.toString('utf-8') });
149
+ });
150
+
151
+ proc.stderr.on('data', (data) => {
152
+ send({ type: 'output', sessionId: id, data: data.toString('utf-8') });
153
+ });
154
+
155
+ proc.on('exit', (exitCode) => {
156
+ console.log(`[${ts()}] Session ${id.slice(0, 8)} exited (${exitCode})`);
157
+ sessions.delete(id);
158
+ send({ type: 'exit', sessionId: id, exitCode });
159
+ });
160
+ }
161
+
162
+ function killSession(id) {
163
+ const session = sessions.get(id);
164
+ if (session && session.proc) {
165
+ session.proc.kill('SIGTERM');
166
+ }
167
+ sessions.delete(id);
168
+ console.log(`[${ts()}] Session ${id.slice(0, 8)} killed`);
169
+ }
170
+
171
+ function send(msg) {
172
+ if (ws && ws.readyState === 1) {
173
+ ws.send(JSON.stringify(msg));
174
+ }
175
+ }
176
+
177
+ function ts() {
178
+ return new Date().toLocaleTimeString();
179
+ }
180
+
181
+ // ── Conversation history ──
182
+
183
+ const CLAUDE_PROJECTS = path.join(process.env.HOME, '.claude', 'projects');
184
+
185
+ function resolveProjectDir(dir) {
186
+ const parts = dir.slice(1).split('-');
187
+ let resolved = '';
188
+ let i = 0;
189
+ while (i < parts.length) {
190
+ let found = false;
191
+ for (let j = parts.length; j > i; j--) {
192
+ const segment = parts.slice(i, j).join('-');
193
+ const test = resolved + '/' + segment;
194
+ if (fs.existsSync(test)) {
195
+ resolved = test;
196
+ i = j;
197
+ found = true;
198
+ break;
199
+ }
200
+ }
201
+ if (!found) {
202
+ resolved += '/' + parts[i];
203
+ i++;
204
+ }
205
+ }
206
+ return resolved;
207
+ }
208
+
209
+ async function handleRequest(msg) {
210
+ const { reqId, action, params } = msg;
211
+ try {
212
+ let data;
213
+ if (action === 'list-conversations') data = await listConversations();
214
+ else if (action === 'get-conversation') data = await getConversation(params.file);
215
+ else { sendResponse(reqId, null, 'Unknown action'); return; }
216
+ sendResponse(reqId, data);
217
+ } catch (err) {
218
+ sendResponse(reqId, null, err.message);
219
+ }
220
+ }
221
+
222
+ function sendResponse(reqId, data, error) {
223
+ send({ type: 'response', reqId, data, error });
224
+ }
225
+
226
+ async function listConversations() {
227
+ const projects = [];
228
+ if (!fs.existsSync(CLAUDE_PROJECTS)) return projects;
229
+
230
+ for (const dir of fs.readdirSync(CLAUDE_PROJECTS)) {
231
+ const dirPath = path.join(CLAUDE_PROJECTS, dir);
232
+ if (!fs.statSync(dirPath).isDirectory()) continue;
233
+
234
+ const jsonls = fs.readdirSync(dirPath).filter(f => f.endsWith('.jsonl'));
235
+ if (jsonls.length === 0) continue;
236
+
237
+ const projectPath = resolveProjectDir(dir);
238
+ const projectName = projectPath
239
+ .replace(/^\/Users\/[^/]+\//, '')
240
+ .replace(/^\/home\/[^/]+\//, '');
241
+
242
+ const convos = [];
243
+ for (const jf of jsonls) {
244
+ const filePath = path.join(dirPath, jf);
245
+ const sessionId = jf.replace('.jsonl', '');
246
+ const stat = fs.statSync(filePath);
247
+ let aiTitle = '';
248
+ let lastPrompt = '';
249
+ let msgCount = 0;
250
+
251
+ const rl = readline.createInterface({
252
+ input: fs.createReadStream(filePath),
253
+ crlfDelay: Infinity,
254
+ });
255
+
256
+ for await (const line of rl) {
257
+ try {
258
+ const obj = JSON.parse(line);
259
+ if (obj.type === 'ai-title') aiTitle = obj.aiTitle || '';
260
+ if (obj.type === 'last-prompt') lastPrompt = obj.lastPrompt || '';
261
+ if (obj.type === 'user' || obj.type === 'assistant') msgCount++;
262
+ } catch {}
263
+ }
264
+
265
+ convos.push({
266
+ sessionId,
267
+ file: path.join(dir, jf),
268
+ cwd: projectPath,
269
+ title: aiTitle || lastPrompt.slice(0, 80) || sessionId.slice(0, 8),
270
+ lastPrompt: lastPrompt.slice(0, 100),
271
+ msgCount,
272
+ updatedAt: stat.mtime.toISOString(),
273
+ });
274
+ }
275
+
276
+ convos.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
277
+ projects.push({ name: projectName, dir, conversations: convos });
278
+ }
279
+
280
+ projects.sort((a, b) => {
281
+ const aMax = a.conversations[0]?.updatedAt || '';
282
+ const bMax = b.conversations[0]?.updatedAt || '';
283
+ return bMax.localeCompare(aMax);
284
+ });
285
+
286
+ return projects;
287
+ }
288
+
289
+ async function getConversation(file) {
290
+ const filePath = path.join(CLAUDE_PROJECTS, file);
291
+ if (!filePath.startsWith(CLAUDE_PROJECTS)) throw new Error('Invalid path');
292
+ if (!fs.existsSync(filePath)) throw new Error('Not found');
293
+
294
+ const messages = [];
295
+ let aiTitle = '';
296
+
297
+ const rl = readline.createInterface({
298
+ input: fs.createReadStream(filePath),
299
+ crlfDelay: Infinity,
300
+ });
301
+
302
+ for await (const line of rl) {
303
+ try {
304
+ const obj = JSON.parse(line);
305
+ if (obj.type === 'ai-title') {
306
+ aiTitle = obj.aiTitle || '';
307
+ continue;
308
+ }
309
+ if (obj.type !== 'user' && obj.type !== 'assistant') continue;
310
+ if (!obj.message) continue;
311
+
312
+ const content = obj.message.content;
313
+
314
+ if (typeof content === 'string') {
315
+ if (content.trim()) messages.push({ role: obj.type, text: content });
316
+ } else if (Array.isArray(content)) {
317
+ for (const block of content) {
318
+ if (block.type === 'thinking') continue;
319
+ if (block.type === 'text') {
320
+ const t = block.text?.trim();
321
+ if (t) messages.push({ role: obj.type === 'user' ? 'user' : 'assistant', text: t });
322
+ } else if (block.type === 'tool_use') {
323
+ let input = '';
324
+ if (block.name === 'Bash') input = block.input?.command || '';
325
+ else if (block.name === 'Read') input = block.input?.file_path || '';
326
+ else if (block.name === 'Edit' || block.name === 'Write') input = block.input?.file_path || '';
327
+ else input = JSON.stringify(block.input || {}).slice(0, 200);
328
+ messages.push({
329
+ role: 'tool',
330
+ tool: block.name,
331
+ toolId: block.id,
332
+ description: block.input?.description || '',
333
+ input: input.slice(0, 500),
334
+ });
335
+ } else if (block.type === 'tool_result') {
336
+ const prev = messages[messages.length - 1];
337
+ if (prev && prev.role === 'tool' && prev.toolId === block.tool_use_id) {
338
+ let out = '';
339
+ if (typeof block.content === 'string') out = block.content;
340
+ else if (Array.isArray(block.content)) {
341
+ out = block.content.filter(b => b.type === 'text').map(b => b.text).join('\n');
342
+ }
343
+ prev.output = out.slice(0, 1000);
344
+ }
345
+ }
346
+ }
347
+ }
348
+ } catch {}
349
+ }
350
+
351
+ return { title: aiTitle, messages };
352
+ }
353
+
354
+ // ── Chat sessions (structured JSON mode) ──
355
+
356
+ const chatSessions = new Map();
357
+
358
+ function createChatSession(id, cwd, resumeId) {
359
+ const workDir = resolveDir(cwd);
360
+ chatSessions.set(id, { id, cwd: workDir, claudeSessionId: resumeId || null, busy: false });
361
+ console.log(`[${ts()}] Chat session ${id.slice(0, 8)} created (claude: ${resumeId ? resumeId.slice(0, 8) : 'new'})`);
362
+ send({ type: 'chat-session-ready', sessionId: id, cwd: workDir, claudeSessionId: resumeId || null });
363
+ }
364
+
365
+ function handleChatMessage(sessionId, text, images) {
366
+ const session = chatSessions.get(sessionId);
367
+ if (!session) { send({ type: 'chat-error', sessionId, error: 'Session not found' }); return; }
368
+ if (session.busy) { send({ type: 'chat-error', sessionId, error: 'Busy' }); return; }
369
+
370
+ session.busy = true;
371
+ send({ type: 'chat-thinking', sessionId });
372
+
373
+ const savedFiles = [];
374
+ if (images && images.length > 0) {
375
+ const tmpDir = path.join(os.tmpdir(), 'claude-remote-images');
376
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
377
+ for (const img of images) {
378
+ const ext = img.mimeType.split('/')[1] || 'png';
379
+ const fileName = `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
380
+ const filePath = path.join(tmpDir, fileName);
381
+ fs.writeFileSync(filePath, Buffer.from(img.base64, 'base64'));
382
+ savedFiles.push(filePath);
383
+ }
384
+ }
385
+
386
+ let prompt = text || '';
387
+ if (savedFiles.length > 0) {
388
+ const fileRefs = savedFiles.map(f => f).join(', ');
389
+ if (prompt) {
390
+ prompt = prompt + '\n\n[Attached images: ' + fileRefs + '] Use the Read tool to view them.';
391
+ } else {
392
+ prompt = '[Attached images: ' + fileRefs + '] Use the Read tool to view and describe them.';
393
+ }
394
+ }
395
+
396
+ const args = ['-p', '--output-format', 'stream-json', '--verbose'];
397
+ if (session.claudeSessionId) {
398
+ args.push('--resume', session.claudeSessionId);
399
+ }
400
+
401
+ const env = buildEnv(session.cwd);
402
+ const proc = spawn('claude', args, {
403
+ stdio: ['pipe', 'pipe', 'pipe'],
404
+ env,
405
+ cwd: session.cwd,
406
+ });
407
+
408
+ let buffer = '';
409
+
410
+ proc.stdout.on('data', (chunk) => {
411
+ buffer += chunk.toString();
412
+ const lines = buffer.split('\n');
413
+ buffer = lines.pop();
414
+ for (const line of lines) {
415
+ if (!line.trim()) continue;
416
+ try {
417
+ const obj = JSON.parse(line);
418
+ if (obj.type === 'assistant' && obj.message?.content) {
419
+ if (obj.session_id && !session.claudeSessionId) {
420
+ session.claudeSessionId = obj.session_id;
421
+ console.log(`[${ts()}] Chat ${sessionId.slice(0, 8)} got claude session: ${obj.session_id.slice(0, 8)}`);
422
+ }
423
+ for (const block of obj.message.content) {
424
+ if (block.type === 'text') {
425
+ send({ type: 'chat-text', sessionId, text: block.text, done: false });
426
+ } else if (block.type === 'tool_use') {
427
+ send({ type: 'chat-tool', sessionId, tool: block.name, input: block.input });
428
+ }
429
+ }
430
+ } else if (obj.type === 'result') {
431
+ if (obj.session_id && !session.claudeSessionId) {
432
+ session.claudeSessionId = obj.session_id;
433
+ console.log(`[${ts()}] Chat ${sessionId.slice(0, 8)} got claude session: ${obj.session_id.slice(0, 8)}`);
434
+ }
435
+ send({ type: 'chat-text', sessionId, text: obj.result || '', done: true });
436
+ }
437
+ } catch {}
438
+ }
439
+ });
440
+
441
+ proc.stderr.on('data', () => {});
442
+
443
+ proc.on('exit', (code) => {
444
+ session.busy = false;
445
+ send({ type: 'chat-done', sessionId, exitCode: code });
446
+ });
447
+
448
+ proc.stdin.write(prompt);
449
+ proc.stdin.end();
450
+ }
451
+
452
+ process.on('SIGINT', () => {
453
+ console.log(`\n[${ts()}] Shutting down...`);
454
+ for (const [, session] of sessions) {
455
+ if (session.proc) session.proc.kill('SIGTERM');
456
+ }
457
+ if (reconnectTimer) clearTimeout(reconnectTimer);
458
+ if (ws) ws.close();
459
+ process.exit();
460
+ });
461
+
462
+ process.on('SIGTERM', () => {
463
+ for (const [, session] of sessions) {
464
+ if (session.proc) session.proc.kill('SIGTERM');
465
+ }
466
+ if (ws) ws.close();
467
+ process.exit();
468
+ });
469
+
470
+ connect();
package/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ const args = process.argv.slice(2);
4
+ const opts = {};
5
+
6
+ for (let i = 0; i < args.length; i++) {
7
+ const arg = args[i];
8
+ if (arg.startsWith('--')) {
9
+ const eq = arg.indexOf('=');
10
+ if (eq !== -1) {
11
+ opts[arg.slice(2, eq)] = arg.slice(eq + 1);
12
+ } else if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
13
+ opts[arg.slice(2)] = args[++i];
14
+ } else {
15
+ opts[arg.slice(2)] = true;
16
+ }
17
+ }
18
+ }
19
+
20
+ if (opts.help) {
21
+ console.log(`
22
+ claude-remote-agent — connect this machine to Claude Remote
23
+
24
+ Usage:
25
+ claude-remote-agent --key <ACCESS_KEY> [options]
26
+ npx claude-remote-agent --key <ACCESS_KEY> [options]
27
+
28
+ Options:
29
+ --key <key> Access key for authentication (required)
30
+ --name <name> Agent display name (default: hostname)
31
+ --server <url> Relay server URL (default: wss://claude.pishchykau.eu)
32
+ --help Show this help
33
+
34
+ Environment variables (used as fallbacks):
35
+ AGENT_KEY Access key
36
+ AGENT_NAME Agent name
37
+ SERVER_URL Server URL
38
+ `);
39
+ process.exit(0);
40
+ }
41
+
42
+ if (opts.key) process.env.AGENT_KEY = opts.key;
43
+ if (opts.name) process.env.AGENT_NAME = opts.name;
44
+ if (opts.server) process.env.SERVER_URL = opts.server;
45
+
46
+ if (!process.env.AGENT_KEY) {
47
+ console.error('Error: --key is required. Run with --help for usage.');
48
+ process.exit(1);
49
+ }
50
+
51
+ require('./agent.js');
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "claude-remote-agent",
3
+ "version": "0.1.0",
4
+ "description": "Agent for Claude Remote — connects your machine to the relay server",
5
+ "bin": {
6
+ "claude-remote-agent": "./cli.js"
7
+ },
8
+ "files": [
9
+ "cli.js",
10
+ "agent.js",
11
+ "pty-bridge.py"
12
+ ],
13
+ "keywords": [
14
+ "claude",
15
+ "remote",
16
+ "agent",
17
+ "claude-code",
18
+ "websocket"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/AlehPishchykau/claude-remote.git",
23
+ "directory": "agent"
24
+ },
25
+ "homepage": "https://github.com/AlehPishchykau/claude-remote#readme",
26
+ "dependencies": {
27
+ "ws": "^8.16.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "license": "MIT"
33
+ }
package/pty-bridge.py ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ import pty, os, sys, select, signal, struct, fcntl, termios, json, errno
3
+
4
+ def set_winsize(fd, cols, rows):
5
+ winsize = struct.pack('HHHH', rows, cols, 0, 0)
6
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
7
+
8
+ def main():
9
+ cols = int(sys.argv[1]) if len(sys.argv) > 1 else 120
10
+ rows = int(sys.argv[2]) if len(sys.argv) > 2 else 40
11
+ cmd = sys.argv[3] if len(sys.argv) > 3 else 'claude'
12
+ cmd_args = sys.argv[3:] if len(sys.argv) > 3 else ['claude']
13
+ cwd = os.environ.get('PTY_CWD', os.environ.get('HOME', '/'))
14
+
15
+ master, slave = pty.openpty()
16
+ set_winsize(master, cols, rows)
17
+
18
+ pid = os.fork()
19
+ if pid == 0:
20
+ os.setsid()
21
+ fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
22
+ os.dup2(slave, 0)
23
+ os.dup2(slave, 1)
24
+ os.dup2(slave, 2)
25
+ os.close(master)
26
+ os.close(slave)
27
+ os.chdir(cwd)
28
+ os.environ['TERM'] = 'xterm-256color'
29
+ os.execvp(cmd, cmd_args)
30
+
31
+ os.close(slave)
32
+
33
+ def handle_sigwinch(signum, frame):
34
+ pass
35
+
36
+ signal.signal(signal.SIGWINCH, handle_sigwinch)
37
+
38
+ stdin_fd = sys.stdin.fileno()
39
+ os.set_blocking(stdin_fd, False)
40
+ os.set_blocking(master, False)
41
+
42
+ stdout = sys.stdout.buffer
43
+ try:
44
+ while True:
45
+ try:
46
+ rlist, _, _ = select.select([master, stdin_fd], [], [], 0.1)
47
+ except (select.error, OSError) as e:
48
+ if hasattr(e, 'errno') and e.errno == errno.EINTR:
49
+ continue
50
+ break
51
+
52
+ if master in rlist:
53
+ try:
54
+ data = os.read(master, 65536)
55
+ if not data:
56
+ break
57
+ stdout.write(data)
58
+ stdout.flush()
59
+ except OSError:
60
+ break
61
+
62
+ if stdin_fd in rlist:
63
+ try:
64
+ data = os.read(stdin_fd, 65536)
65
+ if not data:
66
+ break
67
+ if data.startswith(b'\x1b_RESIZE:'):
68
+ end = data.find(b'\x1b\\')
69
+ if end != -1:
70
+ payload = data[len(b'\x1b_RESIZE:'):end]
71
+ rest = data[end+2:]
72
+ try:
73
+ parts = payload.decode().split(',')
74
+ c, r = int(parts[0]), int(parts[1])
75
+ set_winsize(master, c, r)
76
+ os.kill(pid, signal.SIGWINCH)
77
+ except (ValueError, IndexError):
78
+ pass
79
+ if rest:
80
+ os.write(master, rest)
81
+ continue
82
+ os.write(master, data)
83
+ except OSError:
84
+ break
85
+
86
+ pid_result, status = os.waitpid(pid, os.WNOHANG)
87
+ if pid_result != 0:
88
+ try:
89
+ remaining = os.read(master, 65536)
90
+ if remaining:
91
+ stdout.write(remaining)
92
+ stdout.flush()
93
+ except OSError:
94
+ pass
95
+ break
96
+
97
+ except KeyboardInterrupt:
98
+ pass
99
+ finally:
100
+ try:
101
+ os.close(master)
102
+ except OSError:
103
+ pass
104
+ try:
105
+ os.kill(pid, signal.SIGTERM)
106
+ except OSError:
107
+ pass
108
+
109
+ if os.WIFEXITED(status):
110
+ sys.exit(os.WEXITSTATUS(status))
111
+ sys.exit(1)
112
+
113
+ if __name__ == '__main__':
114
+ main()