tinyagent 1.0.5

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +91 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/pty-wrapper.py +73 -0
  8. package/dist/qr-generator.d.ts +2 -0
  9. package/dist/qr-generator.d.ts.map +1 -0
  10. package/dist/qr-generator.js +50 -0
  11. package/dist/qr-generator.js.map +1 -0
  12. package/dist/shared-types/auth.d.ts +44 -0
  13. package/dist/shared-types/auth.d.ts.map +1 -0
  14. package/dist/shared-types/auth.js +3 -0
  15. package/dist/shared-types/auth.js.map +1 -0
  16. package/dist/shared-types/constants.d.ts +6 -0
  17. package/dist/shared-types/constants.d.ts.map +1 -0
  18. package/dist/shared-types/constants.js +9 -0
  19. package/dist/shared-types/constants.js.map +1 -0
  20. package/dist/shared-types/host.d.ts +61 -0
  21. package/dist/shared-types/host.d.ts.map +1 -0
  22. package/dist/shared-types/host.js +17 -0
  23. package/dist/shared-types/host.js.map +1 -0
  24. package/dist/shared-types/index.d.ts +6 -0
  25. package/dist/shared-types/index.d.ts.map +1 -0
  26. package/dist/shared-types/index.js +22 -0
  27. package/dist/shared-types/index.js.map +1 -0
  28. package/dist/shared-types/messages.d.ts +106 -0
  29. package/dist/shared-types/messages.d.ts.map +1 -0
  30. package/dist/shared-types/messages.js +22 -0
  31. package/dist/shared-types/messages.js.map +1 -0
  32. package/dist/shared-types/session.d.ts +21 -0
  33. package/dist/shared-types/session.d.ts.map +1 -0
  34. package/dist/shared-types/session.js +3 -0
  35. package/dist/shared-types/session.js.map +1 -0
  36. package/dist/shell-client-v2.d.ts +45 -0
  37. package/dist/shell-client-v2.d.ts.map +1 -0
  38. package/dist/shell-client-v2.js +685 -0
  39. package/dist/shell-client-v2.js.map +1 -0
  40. package/dist/shell-client.d.ts +34 -0
  41. package/dist/shell-client.d.ts.map +1 -0
  42. package/dist/shell-client.js +304 -0
  43. package/dist/shell-client.js.map +1 -0
  44. package/dist/tunnel-manager.d.ts +6 -0
  45. package/dist/tunnel-manager.d.ts.map +1 -0
  46. package/dist/tunnel-manager.js +27 -0
  47. package/dist/tunnel-manager.js.map +1 -0
  48. package/package.json +48 -0
@@ -0,0 +1,685 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ShellClient = void 0;
40
+ const ws_1 = __importDefault(require("ws"));
41
+ // @ts-ignore - node-pty types not available
42
+ const pty = __importStar(require("node-pty"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const ora_1 = __importDefault(require("ora"));
45
+ const http_1 = __importDefault(require("http"));
46
+ const https_1 = __importDefault(require("https"));
47
+ const net_1 = __importDefault(require("net"));
48
+ const shared_types_1 = require("./shared-types");
49
+ const tunnel_manager_1 = require("./tunnel-manager");
50
+ class ShellClient {
51
+ options;
52
+ ws;
53
+ ptyProcess; // node-pty IPty
54
+ serverProcess; // node-pty IPty
55
+ tunnelManager;
56
+ heartbeatInterval;
57
+ reconnectTimeout;
58
+ portCheckInterval;
59
+ isConnected = false;
60
+ spinner = (0, ora_1.default)();
61
+ terminalBuffer = '';
62
+ exposedPorts = new Set();
63
+ lastKnownPorts = new Set();
64
+ stdinListener;
65
+ lastInputSource = 'local';
66
+ lastInputTime = Date.now();
67
+ terminalDimensions = {
68
+ local: { cols: 80, rows: 24 },
69
+ mobile: { cols: 80, rows: 24 }
70
+ };
71
+ constructor(options) {
72
+ this.options = options;
73
+ if (options.createTunnel !== false) {
74
+ this.tunnelManager = new tunnel_manager_1.TunnelManager();
75
+ }
76
+ // Get initial terminal dimensions
77
+ if (process.stdout.isTTY) {
78
+ this.terminalDimensions.local = {
79
+ cols: process.stdout.columns || 80,
80
+ rows: process.stdout.rows || 24
81
+ };
82
+ }
83
+ // Handle process exit to clean up terminal
84
+ process.on('exit', () => this.cleanup());
85
+ process.on('SIGINT', () => {
86
+ this.cleanup();
87
+ process.exit(0);
88
+ });
89
+ process.on('SIGTERM', () => {
90
+ this.cleanup();
91
+ process.exit(0);
92
+ });
93
+ // Handle terminal resize
94
+ process.stdout.on('resize', () => {
95
+ if (process.stdout.isTTY) {
96
+ this.terminalDimensions.local = {
97
+ cols: process.stdout.columns || 80,
98
+ rows: process.stdout.rows || 24
99
+ };
100
+ // If local was the last input source, resize the PTY
101
+ if (this.lastInputSource === 'local' && this.ptyProcess) {
102
+ this.ptyProcess.resize(this.terminalDimensions.local.cols, this.terminalDimensions.local.rows);
103
+ }
104
+ }
105
+ });
106
+ }
107
+ async connect() {
108
+ this.spinner.start('Connecting to relay server...');
109
+ const wsUrl = `${this.options.relayUrl}/ws/${this.options.sessionId}`;
110
+ this.ws = new ws_1.default(wsUrl);
111
+ this.ws.on('open', () => {
112
+ this.spinner.succeed('Connected to relay server');
113
+ this.isConnected = true;
114
+ this.sendMessage({
115
+ type: shared_types_1.MessageType.SESSION_INIT,
116
+ sessionId: this.options.sessionId,
117
+ timestamp: Date.now(),
118
+ clientType: 'shell'
119
+ });
120
+ this.startHeartbeat();
121
+ });
122
+ this.ws.on('message', (data) => {
123
+ this.handleMessage(JSON.parse(data.toString()));
124
+ });
125
+ this.ws.on('close', () => {
126
+ this.isConnected = false;
127
+ console.log(chalk_1.default.yellow('Connection closed'));
128
+ this.cleanup();
129
+ this.scheduleReconnect();
130
+ });
131
+ this.ws.on('error', (error) => {
132
+ this.spinner.fail(`WebSocket error: ${error.message}`);
133
+ this.cleanup();
134
+ });
135
+ }
136
+ handleMessage(message) {
137
+ switch (message.type) {
138
+ case shared_types_1.MessageType.SESSION_READY:
139
+ console.log(chalk_1.default.green('Session ready'));
140
+ this.startShell();
141
+ if (this.options.serverCommand) {
142
+ this.startServer();
143
+ }
144
+ // Always start port detection for HTTP tunneling
145
+ this.startPortDetection();
146
+ break;
147
+ case shared_types_1.MessageType.SHELL_DATA:
148
+ const dataMsg = message;
149
+ if (process.env.DEBUG || process.argv.includes('--verbose')) {
150
+ console.log(chalk_1.default.cyan(`[RECEIVED FROM MOBILE] ${JSON.stringify(dataMsg.data)}`));
151
+ }
152
+ // Mark mobile as last input source
153
+ this.lastInputSource = 'mobile';
154
+ this.lastInputTime = Date.now();
155
+ // Use mobile dimensions if it was the last input
156
+ if (this.ptyProcess) {
157
+ const { cols, rows } = this.terminalDimensions.mobile;
158
+ if (process.env.DEBUG || process.argv.includes('--verbose')) {
159
+ console.log(chalk_1.default.magenta(`[INPUT SOURCE: MOBILE] Input received, resizing to mobile dimensions: ${cols}x${rows}`));
160
+ }
161
+ this.ptyProcess.resize(cols, rows);
162
+ this.ptyProcess.write(dataMsg.data);
163
+ }
164
+ break;
165
+ case shared_types_1.MessageType.SHELL_RESIZE:
166
+ const resizeMsg = message;
167
+ if (process.env.DEBUG || process.argv.includes('--verbose')) {
168
+ console.log(chalk_1.default.blue(`[TERMINAL RESIZE] Received from mobile: ${resizeMsg.cols}x${resizeMsg.rows}`));
169
+ console.log(chalk_1.default.yellow(`[TERMINAL RESIZE] Current dimensions - Local: ${this.terminalDimensions.local.cols}x${this.terminalDimensions.local.rows}, Mobile: ${this.terminalDimensions.mobile.cols}x${this.terminalDimensions.mobile.rows}`));
170
+ console.log(chalk_1.default.yellow(`[TERMINAL RESIZE] Last input source: ${this.lastInputSource}`));
171
+ }
172
+ // Store mobile dimensions
173
+ const previousMobileDimensions = { ...this.terminalDimensions.mobile };
174
+ this.terminalDimensions.mobile = {
175
+ cols: resizeMsg.cols,
176
+ rows: resizeMsg.rows
177
+ };
178
+ // Apply resize if:
179
+ // 1. Mobile was the last input source, OR
180
+ // 2. This is the first resize from mobile (mobile dimensions were default 80x24)
181
+ const isFirstMobileResize = previousMobileDimensions.cols === 80 && previousMobileDimensions.rows === 24;
182
+ if ((this.lastInputSource === 'mobile' || isFirstMobileResize) && this.ptyProcess) {
183
+ if (process.env.DEBUG || process.argv.includes('--verbose')) {
184
+ console.log(chalk_1.default.green(`[TERMINAL RESIZE] Applying mobile dimensions: ${resizeMsg.cols}x${resizeMsg.rows}`));
185
+ }
186
+ this.ptyProcess.resize(resizeMsg.cols, resizeMsg.rows);
187
+ }
188
+ else {
189
+ if (process.env.DEBUG || process.argv.includes('--verbose')) {
190
+ console.log(chalk_1.default.red(`[TERMINAL RESIZE] Not applying - last input was from ${this.lastInputSource}`));
191
+ }
192
+ }
193
+ break;
194
+ case shared_types_1.MessageType.COMMAND:
195
+ const cmdMsg = message;
196
+ this.handleCommand(cmdMsg);
197
+ break;
198
+ case shared_types_1.MessageType.SESSION_ERROR:
199
+ console.error(chalk_1.default.red(`Session error: ${message.error}`));
200
+ break;
201
+ case shared_types_1.MessageType.HTTP_REQUEST:
202
+ const httpMsg = message;
203
+ this.handleHttpRequest(httpMsg);
204
+ break;
205
+ }
206
+ }
207
+ startShell() {
208
+ if (this.ptyProcess) {
209
+ console.log(chalk_1.default.yellow('Shell already started'));
210
+ return;
211
+ }
212
+ if (process.env.DEBUG || process.argv.includes('--verbose')) {
213
+ console.log(chalk_1.default.blue(`Starting shell: ${this.options.shell}`));
214
+ console.log(chalk_1.default.blue('[DEBUG] Using shell-client-v2 with node-pty'));
215
+ }
216
+ // Set up local terminal input handling
217
+ this.setupLocalInput();
218
+ // Parse shell args for login shell
219
+ const shellArgs = [];
220
+ if (this.options.shell.includes('bash') || this.options.shell.includes('zsh')) {
221
+ shellArgs.push('-l'); // login shell
222
+ }
223
+ // Create a minimal environment to let the login shell set up its own PATH
224
+ const minimalEnv = {
225
+ TERM: 'xterm-256color',
226
+ USER: process.env.USER,
227
+ HOME: process.env.HOME,
228
+ SHELL: process.env.SHELL,
229
+ LANG: process.env.LANG,
230
+ LC_ALL: process.env.LC_ALL,
231
+ // Explicitly set a basic PATH to ensure system commands are available
232
+ PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
233
+ };
234
+ // Create PTY with initial size based on last input source
235
+ const initialDimensions = this.lastInputSource === 'mobile'
236
+ ? this.terminalDimensions.mobile
237
+ : this.terminalDimensions.local;
238
+ if (process.env.DEBUG || process.argv.includes('--verbose')) {
239
+ console.log(chalk_1.default.cyan(`[PTY INIT] Creating PTY with dimensions: ${initialDimensions.cols}x${initialDimensions.rows} (source: ${this.lastInputSource})`));
240
+ }
241
+ this.ptyProcess = pty.spawn(this.options.shell, shellArgs, {
242
+ name: 'xterm-256color',
243
+ cols: initialDimensions.cols,
244
+ rows: initialDimensions.rows,
245
+ env: minimalEnv,
246
+ cwd: process.cwd()
247
+ });
248
+ // Handle PTY output
249
+ this.ptyProcess.onData((data) => {
250
+ // Only log in debug mode or with --verbose flag
251
+ if (process.env.DEBUG || process.argv.includes('--verbose')) {
252
+ console.log(chalk_1.default.gray(`[SHELL OUTPUT] ${JSON.stringify(data)}`));
253
+ }
254
+ else {
255
+ // Write directly to stdout to preserve terminal control sequences
256
+ process.stdout.write(data);
257
+ }
258
+ // Store in buffer for late-joining clients
259
+ this.terminalBuffer += data;
260
+ if (this.terminalBuffer.length > 10000) {
261
+ this.terminalBuffer = this.terminalBuffer.slice(-10000);
262
+ }
263
+ this.sendMessage({
264
+ type: shared_types_1.MessageType.SHELL_DATA,
265
+ sessionId: this.options.sessionId,
266
+ timestamp: Date.now(),
267
+ data: data
268
+ });
269
+ });
270
+ this.ptyProcess.onExit(({ exitCode }) => {
271
+ console.log(chalk_1.default.yellow(`Shell exited with code ${exitCode}`));
272
+ this.disconnect();
273
+ });
274
+ // Send initial buffer if any
275
+ if (this.terminalBuffer) {
276
+ setTimeout(() => {
277
+ this.sendMessage({
278
+ type: shared_types_1.MessageType.SHELL_DATA,
279
+ sessionId: this.options.sessionId,
280
+ timestamp: Date.now(),
281
+ data: this.terminalBuffer
282
+ });
283
+ }, 100);
284
+ }
285
+ }
286
+ async startServer() {
287
+ if (!this.options.serverCommand)
288
+ return;
289
+ console.log(chalk_1.default.blue(`Starting server: ${this.options.serverCommand}`));
290
+ this.serverProcess = pty.spawn('sh', ['-c', this.options.serverCommand], {
291
+ name: 'xterm',
292
+ env: { ...process.env, PORT: this.options.serverPort?.toString() }
293
+ });
294
+ this.serverProcess.onData((data) => {
295
+ console.log(chalk_1.default.gray(`[SERVER] ${data.trim()}`));
296
+ });
297
+ if (this.tunnelManager && this.options.serverPort) {
298
+ setTimeout(async () => {
299
+ try {
300
+ const tunnelUrl = await this.tunnelManager.createTunnel(this.options.serverPort);
301
+ console.log(chalk_1.default.green(`Tunnel created: ${tunnelUrl}`));
302
+ this.sendMessage({
303
+ type: shared_types_1.MessageType.TUNNEL_URL,
304
+ sessionId: this.options.sessionId,
305
+ timestamp: Date.now(),
306
+ url: tunnelUrl,
307
+ port: this.options.serverPort
308
+ });
309
+ }
310
+ catch (error) {
311
+ console.error(chalk_1.default.red(`Failed to create tunnel: ${error}`));
312
+ }
313
+ }, 3000);
314
+ }
315
+ }
316
+ handleCommand(message) {
317
+ switch (message.command) {
318
+ case 'start_server':
319
+ if (!this.serverProcess) {
320
+ this.startServer();
321
+ }
322
+ break;
323
+ case 'stop_server':
324
+ if (this.serverProcess) {
325
+ this.serverProcess.kill();
326
+ this.serverProcess = undefined;
327
+ }
328
+ break;
329
+ case 'terminate':
330
+ this.disconnect();
331
+ break;
332
+ }
333
+ }
334
+ sendMessage(message) {
335
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
336
+ this.ws.send(JSON.stringify(message));
337
+ }
338
+ }
339
+ startHeartbeat() {
340
+ this.heartbeatInterval = setInterval(() => {
341
+ this.sendMessage({
342
+ type: shared_types_1.MessageType.HEARTBEAT,
343
+ sessionId: this.options.sessionId,
344
+ timestamp: Date.now()
345
+ });
346
+ }, shared_types_1.HEARTBEAT_INTERVAL);
347
+ }
348
+ scheduleReconnect() {
349
+ if (this.reconnectTimeout)
350
+ return;
351
+ this.reconnectTimeout = setTimeout(() => {
352
+ console.log(chalk_1.default.blue('Attempting to reconnect...'));
353
+ this.reconnectTimeout = undefined;
354
+ this.connect();
355
+ }, shared_types_1.RECONNECT_DELAY);
356
+ }
357
+ async handleHttpRequest(request) {
358
+ // Use targetPort from request, or try to extract from headers, or default to 3000
359
+ const port = request.targetPort || 3000;
360
+ console.log(chalk_1.default.magenta(`[HTTP] ${request.method} ${request.path} → localhost:${port}`));
361
+ const makeRequest = (options, redirectCount = 0) => {
362
+ const protocol = options.port === 443 ? https_1.default : http_1.default;
363
+ const req = protocol.request(options, (res) => {
364
+ console.log(chalk_1.default.blue(`[HTTP] Response ${res.statusCode} for ${request.path}`));
365
+ // Handle redirects (3xx status codes)
366
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
367
+ if (redirectCount >= 5) {
368
+ // Too many redirects
369
+ console.error(chalk_1.default.red(`[HTTP] Too many redirects for ${request.path}`));
370
+ this.sendMessage({
371
+ type: shared_types_1.MessageType.HTTP_RESPONSE,
372
+ sessionId: this.options.sessionId,
373
+ timestamp: Date.now(),
374
+ streamId: request.streamId,
375
+ head: {
376
+ status: 310,
377
+ headers: { 'content-type': 'text/plain' }
378
+ },
379
+ data: Buffer.from('Too many redirects').toString('base64'),
380
+ end: true
381
+ });
382
+ return;
383
+ }
384
+ const location = res.headers.location;
385
+ console.log(chalk_1.default.yellow(`[HTTP] Following redirect to ${location}`));
386
+ // Parse the redirect URL
387
+ let redirectUrl;
388
+ try {
389
+ if (location.startsWith('http://') || location.startsWith('https://')) {
390
+ redirectUrl = new URL(location);
391
+ }
392
+ else if (location.startsWith('//')) {
393
+ redirectUrl = new URL(`http:${location}`);
394
+ }
395
+ else if (location.startsWith('/')) {
396
+ redirectUrl = new URL(location, `http://localhost:${port}`);
397
+ }
398
+ else {
399
+ // Relative URL
400
+ const basePath = request.path.substring(0, request.path.lastIndexOf('/') + 1);
401
+ redirectUrl = new URL(location, `http://localhost:${port}${basePath}`);
402
+ }
403
+ }
404
+ catch (e) {
405
+ console.error(chalk_1.default.red(`[HTTP] Invalid redirect URL: ${location}`));
406
+ redirectUrl = new URL(location, `http://localhost:${port}`);
407
+ }
408
+ // Only follow redirects to localhost
409
+ if (redirectUrl.hostname !== 'localhost' && redirectUrl.hostname !== '127.0.0.1') {
410
+ console.log(chalk_1.default.yellow(`[HTTP] Not following external redirect to ${redirectUrl.hostname}`));
411
+ // Send the redirect response as-is
412
+ this.sendMessage({
413
+ type: shared_types_1.MessageType.HTTP_RESPONSE,
414
+ sessionId: this.options.sessionId,
415
+ timestamp: Date.now(),
416
+ streamId: request.streamId,
417
+ head: {
418
+ status: res.statusCode,
419
+ headers: res.headers
420
+ }
421
+ });
422
+ res.on('data', (chunk) => {
423
+ this.sendMessage({
424
+ type: shared_types_1.MessageType.HTTP_RESPONSE,
425
+ sessionId: this.options.sessionId,
426
+ timestamp: Date.now(),
427
+ streamId: request.streamId,
428
+ data: chunk.toString('base64')
429
+ });
430
+ });
431
+ res.on('end', () => {
432
+ this.sendMessage({
433
+ type: shared_types_1.MessageType.HTTP_RESPONSE,
434
+ sessionId: this.options.sessionId,
435
+ timestamp: Date.now(),
436
+ streamId: request.streamId,
437
+ end: true
438
+ });
439
+ });
440
+ return;
441
+ }
442
+ // Follow the redirect
443
+ const newOptions = {
444
+ hostname: 'localhost',
445
+ port: parseInt(redirectUrl.port) || port,
446
+ path: redirectUrl.pathname + redirectUrl.search,
447
+ method: 'GET', // Redirects typically become GET requests
448
+ headers: {
449
+ ...request.headers,
450
+ host: `localhost:${parseInt(redirectUrl.port) || port}`
451
+ }
452
+ };
453
+ makeRequest(newOptions, redirectCount + 1);
454
+ return;
455
+ }
456
+ // Send response head
457
+ this.sendMessage({
458
+ type: shared_types_1.MessageType.HTTP_RESPONSE,
459
+ sessionId: this.options.sessionId,
460
+ timestamp: Date.now(),
461
+ streamId: request.streamId,
462
+ head: {
463
+ status: res.statusCode || 200,
464
+ headers: res.headers
465
+ }
466
+ });
467
+ // Stream response body
468
+ res.on('data', (chunk) => {
469
+ this.sendMessage({
470
+ type: shared_types_1.MessageType.HTTP_RESPONSE,
471
+ sessionId: this.options.sessionId,
472
+ timestamp: Date.now(),
473
+ streamId: request.streamId,
474
+ data: chunk.toString('base64')
475
+ });
476
+ });
477
+ res.on('end', () => {
478
+ this.sendMessage({
479
+ type: shared_types_1.MessageType.HTTP_RESPONSE,
480
+ sessionId: this.options.sessionId,
481
+ timestamp: Date.now(),
482
+ streamId: request.streamId,
483
+ end: true
484
+ });
485
+ });
486
+ });
487
+ req.on('error', (error) => {
488
+ console.error(chalk_1.default.red(`[HTTP] Request error: ${error.message}`));
489
+ // Send error response
490
+ this.sendMessage({
491
+ type: shared_types_1.MessageType.HTTP_RESPONSE,
492
+ sessionId: this.options.sessionId,
493
+ timestamp: Date.now(),
494
+ streamId: request.streamId,
495
+ head: {
496
+ status: 502,
497
+ headers: { 'content-type': 'text/plain' }
498
+ },
499
+ data: Buffer.from(`Gateway Error: ${error.message}`).toString('base64'),
500
+ end: true
501
+ });
502
+ });
503
+ // Write request body if present
504
+ if (request.body && request.method !== 'GET' && request.method !== 'HEAD') {
505
+ const body = typeof request.body === 'string'
506
+ ? Buffer.from(request.body, 'base64')
507
+ : request.body;
508
+ req.write(body);
509
+ }
510
+ req.end();
511
+ };
512
+ // Start the request
513
+ const initialOptions = {
514
+ hostname: 'localhost',
515
+ port,
516
+ path: request.path,
517
+ method: request.method,
518
+ headers: {
519
+ ...request.headers,
520
+ host: `localhost:${port}`
521
+ }
522
+ };
523
+ makeRequest(initialOptions);
524
+ }
525
+ startPortDetection() {
526
+ // Check for active ports every 5 seconds
527
+ this.portCheckInterval = setInterval(() => {
528
+ this.detectActivePorts();
529
+ }, 5000);
530
+ // Initial check
531
+ this.detectActivePorts();
532
+ }
533
+ async detectActivePorts() {
534
+ const commonPorts = [3000, 3001, 4000, 4200, 5000, 5173, 8000, 8080, 8081, 9000];
535
+ const activePorts = new Set();
536
+ for (const port of commonPorts) {
537
+ if (await this.isPortActive(port)) {
538
+ activePorts.add(port);
539
+ }
540
+ }
541
+ // Check if ports have changed
542
+ const portsChanged = activePorts.size !== this.lastKnownPorts.size ||
543
+ [...activePorts].some(port => !this.lastKnownPorts.has(port));
544
+ if (portsChanged) {
545
+ this.lastKnownPorts = new Set(activePorts);
546
+ this.exposedPorts = new Set(activePorts);
547
+ // Send updated port list to relay
548
+ this.sendMessage({
549
+ type: shared_types_1.MessageType.REGISTER_PORT,
550
+ sessionId: this.options.sessionId,
551
+ timestamp: Date.now(),
552
+ ports: [...activePorts]
553
+ });
554
+ if (activePorts.size > 0) {
555
+ console.log(chalk_1.default.green(`[HTTP] Exposing ports: ${[...activePorts].join(', ')}`));
556
+ console.log(chalk_1.default.green(`[HTTP] Access your dev server at:`));
557
+ // Determine if connecting to production or local
558
+ const isProduction = this.options.relayUrl.includes('tinyagent.app');
559
+ const isLocal = this.options.relayUrl.includes('localhost') ||
560
+ this.options.relayUrl.includes('127.0.0.1') ||
561
+ this.options.relayUrl.match(/192\.168\.|10\.|172\./);
562
+ if (isProduction) {
563
+ // Production URLs with subdomain-based port routing
564
+ [...activePorts].forEach(port => {
565
+ console.log(chalk_1.default.cyan(` https://${this.options.sessionId}-${port}.tinyagent.app/`));
566
+ });
567
+ }
568
+ else if (isLocal) {
569
+ // Local development URLs with nip.io
570
+ const relayHost = new URL(this.options.relayUrl).hostname;
571
+ const relayPort = new URL(this.options.relayUrl).port || '8080';
572
+ const ipForNipIo = relayHost.replace(/\./g, '-');
573
+ [...activePorts].forEach(port => {
574
+ console.log(chalk_1.default.cyan(` http://${this.options.sessionId}-${port}.${ipForNipIo}.nip.io:${relayPort}/`));
575
+ });
576
+ }
577
+ else {
578
+ // Custom domain
579
+ const publicUrl = process.env.PUBLIC_URL || 'https://tinyagent.app';
580
+ const hostname = new URL(publicUrl).hostname;
581
+ [...activePorts].forEach(port => {
582
+ console.log(chalk_1.default.cyan(` https://${this.options.sessionId}-${port}.${hostname}/`));
583
+ });
584
+ }
585
+ }
586
+ }
587
+ }
588
+ isPortActive(port) {
589
+ return new Promise((resolve) => {
590
+ const socket = new net_1.default.Socket();
591
+ socket.setTimeout(100);
592
+ socket.on('connect', () => {
593
+ socket.destroy();
594
+ resolve(true);
595
+ });
596
+ socket.on('timeout', () => {
597
+ socket.destroy();
598
+ resolve(false);
599
+ });
600
+ socket.on('error', () => {
601
+ resolve(false);
602
+ });
603
+ socket.connect(port, 'localhost');
604
+ });
605
+ }
606
+ setupLocalInput() {
607
+ // Set stdin to raw mode to capture all keystrokes
608
+ if (process.stdin.isTTY) {
609
+ process.stdin.setRawMode(true);
610
+ }
611
+ process.stdin.setEncoding('utf8');
612
+ // Handle local keyboard input
613
+ this.stdinListener = () => {
614
+ process.stdin.on('data', (data) => {
615
+ // Mark local as last input source
616
+ this.lastInputSource = 'local';
617
+ this.lastInputTime = Date.now();
618
+ // Resize PTY to local dimensions when typing locally
619
+ if (this.ptyProcess && this.terminalDimensions.local) {
620
+ const { cols, rows } = this.terminalDimensions.local;
621
+ if (process.env.DEBUG || process.argv.includes('--verbose')) {
622
+ console.log(chalk_1.default.green(`[INPUT SOURCE: LOCAL] Input received, resizing to local dimensions: ${cols}x${rows}`));
623
+ }
624
+ this.ptyProcess.resize(cols, rows);
625
+ }
626
+ // Send to local PTY
627
+ if (this.ptyProcess) {
628
+ this.ptyProcess.write(data);
629
+ }
630
+ // Special handling for Ctrl+C to exit
631
+ if (data === '\x03') {
632
+ console.log(chalk_1.default.yellow('\nReceived Ctrl+C, disconnecting...'));
633
+ this.disconnect();
634
+ process.exit(0);
635
+ }
636
+ // Debug: Manual resize test with Ctrl+R (only in verbose mode)
637
+ if (data === '\x12' && (process.env.DEBUG || process.argv.includes('--verbose'))) { // Ctrl+R
638
+ console.log(chalk_1.default.yellow('\nManual resize test - switching to mobile dimensions'));
639
+ this.lastInputSource = 'mobile';
640
+ if (this.ptyProcess) {
641
+ const { cols, rows } = this.terminalDimensions.mobile;
642
+ console.log(chalk_1.default.green(`Resizing to mobile: ${cols}x${rows}`));
643
+ this.ptyProcess.resize(cols, rows);
644
+ }
645
+ }
646
+ });
647
+ };
648
+ this.stdinListener();
649
+ // Resume stdin to start receiving data
650
+ process.stdin.resume();
651
+ }
652
+ cleanup() {
653
+ // Restore terminal settings
654
+ if (process.stdin.isTTY) {
655
+ process.stdin.setRawMode(false);
656
+ }
657
+ process.stdin.pause();
658
+ if (this.heartbeatInterval) {
659
+ clearInterval(this.heartbeatInterval);
660
+ }
661
+ if (this.reconnectTimeout) {
662
+ clearTimeout(this.reconnectTimeout);
663
+ }
664
+ if (this.portCheckInterval) {
665
+ clearInterval(this.portCheckInterval);
666
+ }
667
+ if (this.ptyProcess) {
668
+ this.ptyProcess.kill();
669
+ }
670
+ if (this.serverProcess) {
671
+ this.serverProcess.kill();
672
+ }
673
+ if (this.tunnelManager) {
674
+ this.tunnelManager.closeTunnel();
675
+ }
676
+ }
677
+ disconnect() {
678
+ this.cleanup();
679
+ if (this.ws) {
680
+ this.ws.close();
681
+ }
682
+ }
683
+ }
684
+ exports.ShellClient = ShellClient;
685
+ //# sourceMappingURL=shell-client-v2.js.map