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.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +91 -0
- package/dist/cli.js.map +1 -0
- package/dist/pty-wrapper.py +73 -0
- package/dist/qr-generator.d.ts +2 -0
- package/dist/qr-generator.d.ts.map +1 -0
- package/dist/qr-generator.js +50 -0
- package/dist/qr-generator.js.map +1 -0
- package/dist/shared-types/auth.d.ts +44 -0
- package/dist/shared-types/auth.d.ts.map +1 -0
- package/dist/shared-types/auth.js +3 -0
- package/dist/shared-types/auth.js.map +1 -0
- package/dist/shared-types/constants.d.ts +6 -0
- package/dist/shared-types/constants.d.ts.map +1 -0
- package/dist/shared-types/constants.js +9 -0
- package/dist/shared-types/constants.js.map +1 -0
- package/dist/shared-types/host.d.ts +61 -0
- package/dist/shared-types/host.d.ts.map +1 -0
- package/dist/shared-types/host.js +17 -0
- package/dist/shared-types/host.js.map +1 -0
- package/dist/shared-types/index.d.ts +6 -0
- package/dist/shared-types/index.d.ts.map +1 -0
- package/dist/shared-types/index.js +22 -0
- package/dist/shared-types/index.js.map +1 -0
- package/dist/shared-types/messages.d.ts +106 -0
- package/dist/shared-types/messages.d.ts.map +1 -0
- package/dist/shared-types/messages.js +22 -0
- package/dist/shared-types/messages.js.map +1 -0
- package/dist/shared-types/session.d.ts +21 -0
- package/dist/shared-types/session.d.ts.map +1 -0
- package/dist/shared-types/session.js +3 -0
- package/dist/shared-types/session.js.map +1 -0
- package/dist/shell-client-v2.d.ts +45 -0
- package/dist/shell-client-v2.d.ts.map +1 -0
- package/dist/shell-client-v2.js +685 -0
- package/dist/shell-client-v2.js.map +1 -0
- package/dist/shell-client.d.ts +34 -0
- package/dist/shell-client.d.ts.map +1 -0
- package/dist/shell-client.js +304 -0
- package/dist/shell-client.js.map +1 -0
- package/dist/tunnel-manager.d.ts +6 -0
- package/dist/tunnel-manager.d.ts.map +1 -0
- package/dist/tunnel-manager.js +27 -0
- package/dist/tunnel-manager.js.map +1 -0
- 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
|