tinyagent 1.0.5 → 1.2.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/README.md +63 -6
- package/dist/auth-client.d.ts +14 -0
- package/dist/auth-client.d.ts.map +1 -0
- package/dist/auth-client.js +165 -0
- package/dist/auth-client.js.map +1 -0
- package/dist/cli.js +36 -29
- package/dist/cli.js.map +1 -1
- package/dist/firebase-auth-simple.d.ts +21 -0
- package/dist/firebase-auth-simple.d.ts.map +1 -0
- package/dist/firebase-auth-simple.js +218 -0
- package/dist/firebase-auth-simple.js.map +1 -0
- package/dist/firebase-auth.d.ts +22 -0
- package/dist/firebase-auth.d.ts.map +1 -0
- package/dist/firebase-auth.js +211 -0
- package/dist/firebase-auth.js.map +1 -0
- package/dist/qr-generator.d.ts.map +1 -1
- package/dist/qr-generator.js +1 -3
- package/dist/qr-generator.js.map +1 -1
- package/dist/shared-types/messages.d.ts +40 -2
- package/dist/shared-types/messages.d.ts.map +1 -1
- package/dist/shared-types/messages.js +5 -0
- package/dist/shared-types/messages.js.map +1 -1
- package/dist/shared-types/session.d.ts +1 -0
- package/dist/shared-types/session.d.ts.map +1 -1
- package/dist/shell-client-v2.d.ts +27 -0
- package/dist/shell-client-v2.d.ts.map +1 -1
- package/dist/shell-client-v2.js +466 -84
- package/dist/shell-client-v2.js.map +1 -1
- package/package.json +13 -4
package/dist/shell-client-v2.js
CHANGED
|
@@ -45,6 +45,7 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
45
45
|
const http_1 = __importDefault(require("http"));
|
|
46
46
|
const https_1 = __importDefault(require("https"));
|
|
47
47
|
const net_1 = __importDefault(require("net"));
|
|
48
|
+
const qr_generator_1 = require("./qr-generator");
|
|
48
49
|
const shared_types_1 = require("./shared-types");
|
|
49
50
|
const tunnel_manager_1 = require("./tunnel-manager");
|
|
50
51
|
class ShellClient {
|
|
@@ -61,6 +62,7 @@ class ShellClient {
|
|
|
61
62
|
terminalBuffer = '';
|
|
62
63
|
exposedPorts = new Set();
|
|
63
64
|
lastKnownPorts = new Set();
|
|
65
|
+
portActivity = {};
|
|
64
66
|
stdinListener;
|
|
65
67
|
lastInputSource = 'local';
|
|
66
68
|
lastInputTime = Date.now();
|
|
@@ -68,6 +70,16 @@ class ShellClient {
|
|
|
68
70
|
local: { cols: 80, rows: 24 },
|
|
69
71
|
mobile: { cols: 80, rows: 24 }
|
|
70
72
|
};
|
|
73
|
+
connectedClients = 1; // Start with 1 (self)
|
|
74
|
+
clientTypes = { mobile: 0, shell: 0, host: 0 };
|
|
75
|
+
statusLineInterval;
|
|
76
|
+
isShowingQR = false;
|
|
77
|
+
bufferedOutput = [];
|
|
78
|
+
currentCommand = '';
|
|
79
|
+
commandCheckInterval;
|
|
80
|
+
viewMode = 'desktop'; // For manual toggle
|
|
81
|
+
statusBarEnabled = true; // Toggle for status bar
|
|
82
|
+
scrollbackMode = false; // Track if we're in scrollback mode
|
|
71
83
|
constructor(options) {
|
|
72
84
|
this.options = options;
|
|
73
85
|
if (options.createTunnel !== false) {
|
|
@@ -80,6 +92,8 @@ class ShellClient {
|
|
|
80
92
|
rows: process.stdout.rows || 24
|
|
81
93
|
};
|
|
82
94
|
}
|
|
95
|
+
// Start status line updates
|
|
96
|
+
this.startStatusLine();
|
|
83
97
|
// Handle process exit to clean up terminal
|
|
84
98
|
process.on('exit', () => this.cleanup());
|
|
85
99
|
process.on('SIGINT', () => {
|
|
@@ -97,10 +111,15 @@ class ShellClient {
|
|
|
97
111
|
cols: process.stdout.columns || 80,
|
|
98
112
|
rows: process.stdout.rows || 24
|
|
99
113
|
};
|
|
100
|
-
//
|
|
101
|
-
if (this.
|
|
102
|
-
|
|
114
|
+
// Only resize PTY if we're in desktop view mode
|
|
115
|
+
if (this.viewMode === 'desktop' && this.ptyProcess) {
|
|
116
|
+
const ptyRows = this.statusBarEnabled
|
|
117
|
+
? Math.max(1, this.terminalDimensions.local.rows - 1) // Reserve line for status bar
|
|
118
|
+
: this.terminalDimensions.local.rows; // Use full height
|
|
119
|
+
this.ptyProcess.resize(this.terminalDimensions.local.cols, ptyRows);
|
|
103
120
|
}
|
|
121
|
+
// Update status line with new width
|
|
122
|
+
this.updateStatusLine();
|
|
104
123
|
}
|
|
105
124
|
});
|
|
106
125
|
}
|
|
@@ -117,6 +136,15 @@ class ShellClient {
|
|
|
117
136
|
timestamp: Date.now(),
|
|
118
137
|
clientType: 'shell'
|
|
119
138
|
});
|
|
139
|
+
// If password is provided, send it immediately after SESSION_INIT
|
|
140
|
+
if (this.options.password) {
|
|
141
|
+
this.sendMessage({
|
|
142
|
+
type: shared_types_1.MessageType.SESSION_AUTH,
|
|
143
|
+
sessionId: this.options.sessionId,
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
password: this.options.password
|
|
146
|
+
});
|
|
147
|
+
}
|
|
120
148
|
this.startHeartbeat();
|
|
121
149
|
});
|
|
122
150
|
this.ws.on('message', (data) => {
|
|
@@ -137,6 +165,7 @@ class ShellClient {
|
|
|
137
165
|
switch (message.type) {
|
|
138
166
|
case shared_types_1.MessageType.SESSION_READY:
|
|
139
167
|
console.log(chalk_1.default.green('Session ready'));
|
|
168
|
+
this.setupTerminalWithStatusBar();
|
|
140
169
|
this.startShell();
|
|
141
170
|
if (this.options.serverCommand) {
|
|
142
171
|
this.startServer();
|
|
@@ -152,13 +181,8 @@ class ShellClient {
|
|
|
152
181
|
// Mark mobile as last input source
|
|
153
182
|
this.lastInputSource = 'mobile';
|
|
154
183
|
this.lastInputTime = Date.now();
|
|
155
|
-
//
|
|
184
|
+
// Just write the data without resizing - user controls dimensions with Ctrl+R
|
|
156
185
|
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
186
|
this.ptyProcess.write(dataMsg.data);
|
|
163
187
|
}
|
|
164
188
|
break;
|
|
@@ -175,19 +199,17 @@ class ShellClient {
|
|
|
175
199
|
cols: resizeMsg.cols,
|
|
176
200
|
rows: resizeMsg.rows
|
|
177
201
|
};
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
202
|
+
// Always resize to mobile dimensions when receiving resize from mobile
|
|
203
|
+
if (this.ptyProcess) {
|
|
204
|
+
const ptyRows = this.statusBarEnabled
|
|
205
|
+
? Math.max(1, resizeMsg.rows - 1) // Reserve line for status bar
|
|
206
|
+
: resizeMsg.rows; // Use full height
|
|
207
|
+
this.ptyProcess.resize(resizeMsg.cols, ptyRows);
|
|
208
|
+
// Update view mode to mobile since we're resizing to mobile dimensions
|
|
209
|
+
this.viewMode = 'mobile';
|
|
210
|
+
this.updateStatusLine();
|
|
189
211
|
if (process.env.DEBUG || process.argv.includes('--verbose')) {
|
|
190
|
-
console.log(chalk_1.default.
|
|
212
|
+
console.log(chalk_1.default.green(`[TERMINAL RESIZE] Applied mobile dimensions: ${resizeMsg.cols}x${resizeMsg.rows}`));
|
|
191
213
|
}
|
|
192
214
|
}
|
|
193
215
|
break;
|
|
@@ -202,6 +224,33 @@ class ShellClient {
|
|
|
202
224
|
const httpMsg = message;
|
|
203
225
|
this.handleHttpRequest(httpMsg);
|
|
204
226
|
break;
|
|
227
|
+
case shared_types_1.MessageType.CLIENT_COUNT:
|
|
228
|
+
const countMsg = message;
|
|
229
|
+
// Subtract 1 to exclude the current shell client from the count
|
|
230
|
+
this.connectedClients = Math.max(0, countMsg.count - 1);
|
|
231
|
+
this.clientTypes = countMsg.clientTypes || { mobile: 0, shell: 0, host: 0 };
|
|
232
|
+
// Check if mobile client just connected
|
|
233
|
+
if (this.clientTypes.mobile > 0) {
|
|
234
|
+
// If we're showing QR code, hide it automatically
|
|
235
|
+
if (this.isShowingQR) {
|
|
236
|
+
this.hideQRCode();
|
|
237
|
+
}
|
|
238
|
+
// Switch to mobile view if in desktop mode
|
|
239
|
+
if (this.viewMode === 'desktop') {
|
|
240
|
+
console.log(chalk_1.default.green('\nMobile client connected, switching to mobile view'));
|
|
241
|
+
this.viewMode = 'mobile';
|
|
242
|
+
// Apply mobile dimensions if we have them
|
|
243
|
+
if (this.ptyProcess && this.terminalDimensions.mobile.cols > 0) {
|
|
244
|
+
const { cols, rows } = this.terminalDimensions.mobile;
|
|
245
|
+
const ptyRows = this.statusBarEnabled
|
|
246
|
+
? Math.max(1, rows - 1) // Reserve line for status bar
|
|
247
|
+
: rows; // Use full height
|
|
248
|
+
this.ptyProcess.resize(cols, ptyRows);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
this.updateStatusLine();
|
|
253
|
+
break;
|
|
205
254
|
}
|
|
206
255
|
}
|
|
207
256
|
startShell() {
|
|
@@ -235,25 +284,37 @@ class ShellClient {
|
|
|
235
284
|
const initialDimensions = this.lastInputSource === 'mobile'
|
|
236
285
|
? this.terminalDimensions.mobile
|
|
237
286
|
: this.terminalDimensions.local;
|
|
287
|
+
// Reserve one line for status bar if enabled
|
|
288
|
+
const ptyRows = this.statusBarEnabled
|
|
289
|
+
? Math.max(1, initialDimensions.rows - 1)
|
|
290
|
+
: initialDimensions.rows;
|
|
238
291
|
if (process.env.DEBUG || process.argv.includes('--verbose')) {
|
|
239
|
-
console.log(chalk_1.default.cyan(`[PTY INIT] Creating PTY with dimensions: ${initialDimensions.cols}x${
|
|
292
|
+
console.log(chalk_1.default.cyan(`[PTY INIT] Creating PTY with dimensions: ${initialDimensions.cols}x${ptyRows} (source: ${this.lastInputSource}, reserved 1 line for status)`));
|
|
240
293
|
}
|
|
241
294
|
this.ptyProcess = pty.spawn(this.options.shell, shellArgs, {
|
|
242
295
|
name: 'xterm-256color',
|
|
243
296
|
cols: initialDimensions.cols,
|
|
244
|
-
rows:
|
|
297
|
+
rows: ptyRows,
|
|
245
298
|
env: minimalEnv,
|
|
246
299
|
cwd: process.cwd()
|
|
247
300
|
});
|
|
248
301
|
// Handle PTY output
|
|
249
302
|
this.ptyProcess.onData((data) => {
|
|
303
|
+
// Detect commands in the output
|
|
304
|
+
this.detectCommand(data);
|
|
250
305
|
// Only log in debug mode or with --verbose flag
|
|
251
306
|
if (process.env.DEBUG || process.argv.includes('--verbose')) {
|
|
252
307
|
console.log(chalk_1.default.gray(`[SHELL OUTPUT] ${JSON.stringify(data)}`));
|
|
253
308
|
}
|
|
254
309
|
else {
|
|
255
|
-
//
|
|
256
|
-
|
|
310
|
+
// If showing QR code, buffer the output
|
|
311
|
+
if (this.isShowingQR) {
|
|
312
|
+
this.bufferedOutput.push(data);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Write directly to stdout to preserve terminal control sequences
|
|
316
|
+
process.stdout.write(data);
|
|
317
|
+
}
|
|
257
318
|
}
|
|
258
319
|
// Store in buffer for late-joining clients
|
|
259
320
|
this.terminalBuffer += data;
|
|
@@ -271,17 +332,29 @@ class ShellClient {
|
|
|
271
332
|
console.log(chalk_1.default.yellow(`Shell exited with code ${exitCode}`));
|
|
272
333
|
this.disconnect();
|
|
273
334
|
});
|
|
274
|
-
// Send initial buffer if any
|
|
335
|
+
// Send initial buffer if any (last 5000 chars to avoid overwhelming mobile)
|
|
275
336
|
if (this.terminalBuffer) {
|
|
276
337
|
setTimeout(() => {
|
|
338
|
+
// Send only the last portion of the buffer for mobile clients
|
|
339
|
+
const bufferToSend = this.terminalBuffer.length > 5000
|
|
340
|
+
? '...' + this.terminalBuffer.slice(-5000)
|
|
341
|
+
: this.terminalBuffer;
|
|
277
342
|
this.sendMessage({
|
|
278
343
|
type: shared_types_1.MessageType.SHELL_DATA,
|
|
279
344
|
sessionId: this.options.sessionId,
|
|
280
345
|
timestamp: Date.now(),
|
|
281
|
-
data:
|
|
346
|
+
data: bufferToSend
|
|
282
347
|
});
|
|
283
348
|
}, 100);
|
|
284
349
|
}
|
|
350
|
+
// Auto-start Claude if enabled
|
|
351
|
+
if (this.options.autoStartClaude) {
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
this.checkAndStartClaude();
|
|
354
|
+
}, 1000); // Wait a second for shell to initialize
|
|
355
|
+
}
|
|
356
|
+
// Start monitoring for running commands
|
|
357
|
+
this.startCommandMonitoring();
|
|
285
358
|
}
|
|
286
359
|
async startServer() {
|
|
287
360
|
if (!this.options.serverCommand)
|
|
@@ -357,11 +430,17 @@ class ShellClient {
|
|
|
357
430
|
async handleHttpRequest(request) {
|
|
358
431
|
// Use targetPort from request, or try to extract from headers, or default to 3000
|
|
359
432
|
const port = request.targetPort || 3000;
|
|
360
|
-
|
|
433
|
+
// Track port activity
|
|
434
|
+
if (!this.portActivity[port]) {
|
|
435
|
+
this.portActivity[port] = { lastAccess: Date.now(), requestCount: 0 };
|
|
436
|
+
}
|
|
437
|
+
this.portActivity[port].lastAccess = Date.now();
|
|
438
|
+
this.portActivity[port].requestCount++;
|
|
439
|
+
// Silently handle HTTP request
|
|
361
440
|
const makeRequest = (options, redirectCount = 0) => {
|
|
362
441
|
const protocol = options.port === 443 ? https_1.default : http_1.default;
|
|
363
442
|
const req = protocol.request(options, (res) => {
|
|
364
|
-
|
|
443
|
+
// Response received
|
|
365
444
|
// Handle redirects (3xx status codes)
|
|
366
445
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
367
446
|
if (redirectCount >= 5) {
|
|
@@ -382,7 +461,7 @@ class ShellClient {
|
|
|
382
461
|
return;
|
|
383
462
|
}
|
|
384
463
|
const location = res.headers.location;
|
|
385
|
-
|
|
464
|
+
// Following redirect
|
|
386
465
|
// Parse the redirect URL
|
|
387
466
|
let redirectUrl;
|
|
388
467
|
try {
|
|
@@ -407,7 +486,7 @@ class ShellClient {
|
|
|
407
486
|
}
|
|
408
487
|
// Only follow redirects to localhost
|
|
409
488
|
if (redirectUrl.hostname !== 'localhost' && redirectUrl.hostname !== '127.0.0.1') {
|
|
410
|
-
|
|
489
|
+
// Not following external redirect
|
|
411
490
|
// Send the redirect response as-is
|
|
412
491
|
this.sendMessage({
|
|
413
492
|
type: shared_types_1.MessageType.HTTP_RESPONSE,
|
|
@@ -523,64 +602,76 @@ class ShellClient {
|
|
|
523
602
|
makeRequest(initialOptions);
|
|
524
603
|
}
|
|
525
604
|
startPortDetection() {
|
|
605
|
+
if (this.options.verbose) {
|
|
606
|
+
console.log(chalk_1.default.gray('[HTTP] Starting port detection...'));
|
|
607
|
+
}
|
|
526
608
|
// Check for active ports every 5 seconds
|
|
527
609
|
this.portCheckInterval = setInterval(() => {
|
|
528
610
|
this.detectActivePorts();
|
|
529
611
|
}, 5000);
|
|
530
|
-
// Initial check
|
|
531
|
-
|
|
612
|
+
// Initial check with a small delay to ensure services are ready
|
|
613
|
+
setTimeout(() => {
|
|
614
|
+
if (this.options.verbose) {
|
|
615
|
+
console.log(chalk_1.default.gray('[HTTP] Initial port scan...'));
|
|
616
|
+
}
|
|
617
|
+
this.detectActivePorts();
|
|
618
|
+
}, 1000);
|
|
532
619
|
}
|
|
533
620
|
async detectActivePorts() {
|
|
534
|
-
const commonPorts = [3000, 3001, 4000, 4200, 5000, 5173, 8000, 8080, 8081, 9000];
|
|
621
|
+
const commonPorts = [3000, 3001, 3002, 3003, 3004, 3005, 4000, 4200, 5000, 5173, 8000, 8080, 8081, 9000];
|
|
535
622
|
const activePorts = new Set();
|
|
623
|
+
const portsWithActivity = new Set();
|
|
536
624
|
for (const port of commonPorts) {
|
|
537
625
|
if (await this.isPortActive(port)) {
|
|
538
626
|
activePorts.add(port);
|
|
627
|
+
// Check if this port has recent activity (within last 30 seconds)
|
|
628
|
+
if (this.portActivity[port] &&
|
|
629
|
+
(Date.now() - this.portActivity[port].lastAccess) < 30000) {
|
|
630
|
+
portsWithActivity.add(port);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Clean up old port activity data
|
|
635
|
+
Object.keys(this.portActivity).forEach(port => {
|
|
636
|
+
const portNum = parseInt(port);
|
|
637
|
+
if (!activePorts.has(portNum) ||
|
|
638
|
+
(Date.now() - this.portActivity[portNum].lastAccess) > 60000) {
|
|
639
|
+
delete this.portActivity[portNum];
|
|
539
640
|
}
|
|
641
|
+
});
|
|
642
|
+
// Debug logging
|
|
643
|
+
if (process.env.DEBUG_PORTS) {
|
|
644
|
+
console.log(chalk_1.default.gray(`[DEBUG] Active ports found: ${[...activePorts].join(', ') || 'none'}`));
|
|
645
|
+
console.log(chalk_1.default.gray(`[DEBUG] Ports with recent activity: ${[...portsWithActivity].join(', ') || 'none'}`));
|
|
540
646
|
}
|
|
541
647
|
// Check if ports have changed
|
|
542
648
|
const portsChanged = activePorts.size !== this.lastKnownPorts.size ||
|
|
543
|
-
[...activePorts].some(port => !this.lastKnownPorts.has(port))
|
|
649
|
+
[...activePorts].some(port => !this.lastKnownPorts.has(port)) ||
|
|
650
|
+
[...this.lastKnownPorts].some(port => !activePorts.has(port));
|
|
544
651
|
if (portsChanged) {
|
|
545
652
|
this.lastKnownPorts = new Set(activePorts);
|
|
546
653
|
this.exposedPorts = new Set(activePorts);
|
|
547
|
-
// Send updated port list to relay
|
|
654
|
+
// Send updated port list to relay with activity information
|
|
548
655
|
this.sendMessage({
|
|
549
656
|
type: shared_types_1.MessageType.REGISTER_PORT,
|
|
550
657
|
sessionId: this.options.sessionId,
|
|
551
658
|
timestamp: Date.now(),
|
|
552
|
-
ports: [...activePorts]
|
|
659
|
+
ports: [...activePorts],
|
|
660
|
+
portActivity: this.portActivity
|
|
553
661
|
});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
console.log(chalk_1.default.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
|
|
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
|
-
});
|
|
662
|
+
// Log detected ports only when they change
|
|
663
|
+
if (portsWithActivity.size > 0) {
|
|
664
|
+
console.log(chalk_1.default.blue(`[HTTP] Active ports: ${[...portsWithActivity].join(', ')}`));
|
|
665
|
+
console.log(chalk_1.default.blue(`[HTTP] Access your dev server at:`));
|
|
666
|
+
console.log(chalk_1.default.blue(` https://tinyagent.app/${this.options.sessionId}/`));
|
|
667
|
+
}
|
|
668
|
+
else if (this.options.verbose) {
|
|
669
|
+
// Only show verbose logs in verbose mode
|
|
670
|
+
if (activePorts.size > 0) {
|
|
671
|
+
console.log(chalk_1.default.gray(`[HTTP] Ports detected (${[...activePorts].join(', ')}), waiting for activity...`));
|
|
576
672
|
}
|
|
577
|
-
else {
|
|
578
|
-
|
|
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
|
-
});
|
|
673
|
+
else if (this.lastKnownPorts.size > 0) {
|
|
674
|
+
console.log(chalk_1.default.gray(`[HTTP] No active ports detected`));
|
|
584
675
|
}
|
|
585
676
|
}
|
|
586
677
|
}
|
|
@@ -588,7 +679,7 @@ class ShellClient {
|
|
|
588
679
|
isPortActive(port) {
|
|
589
680
|
return new Promise((resolve) => {
|
|
590
681
|
const socket = new net_1.default.Socket();
|
|
591
|
-
socket.setTimeout(
|
|
682
|
+
socket.setTimeout(300); // Increased timeout for better detection
|
|
592
683
|
socket.on('connect', () => {
|
|
593
684
|
socket.destroy();
|
|
594
685
|
resolve(true);
|
|
@@ -615,14 +706,7 @@ class ShellClient {
|
|
|
615
706
|
// Mark local as last input source
|
|
616
707
|
this.lastInputSource = 'local';
|
|
617
708
|
this.lastInputTime = Date.now();
|
|
618
|
-
//
|
|
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
|
-
}
|
|
709
|
+
// Don't auto-resize on input - user controls with Ctrl+R
|
|
626
710
|
// Send to local PTY
|
|
627
711
|
if (this.ptyProcess) {
|
|
628
712
|
this.ptyProcess.write(data);
|
|
@@ -633,15 +717,20 @@ class ShellClient {
|
|
|
633
717
|
this.disconnect();
|
|
634
718
|
process.exit(0);
|
|
635
719
|
}
|
|
636
|
-
//
|
|
637
|
-
if (data === '\
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
720
|
+
// Ctrl+S to show QR code (S for Show)
|
|
721
|
+
if (data === '\x13') { // Ctrl+S
|
|
722
|
+
this.showQRCode();
|
|
723
|
+
return; // Don't pass to PTY
|
|
724
|
+
}
|
|
725
|
+
// Ctrl+F to toggle between desktop and mobile view (F for Fit)
|
|
726
|
+
if (data === '\x06') { // Ctrl+F
|
|
727
|
+
this.toggleViewMode();
|
|
728
|
+
return; // Don't pass to PTY
|
|
729
|
+
}
|
|
730
|
+
// Ctrl+B to toggle status bar (B for Bar)
|
|
731
|
+
if (data === '\x02') { // Ctrl+B
|
|
732
|
+
this.toggleStatusBar();
|
|
733
|
+
return; // Don't pass to PTY
|
|
645
734
|
}
|
|
646
735
|
});
|
|
647
736
|
};
|
|
@@ -650,6 +739,11 @@ class ShellClient {
|
|
|
650
739
|
process.stdin.resume();
|
|
651
740
|
}
|
|
652
741
|
cleanup() {
|
|
742
|
+
// Reset scrolling region
|
|
743
|
+
if (process.stdout.isTTY) {
|
|
744
|
+
process.stdout.write('\x1b[r'); // Reset scroll region
|
|
745
|
+
process.stdout.write('\x1b[?25h'); // Show cursor
|
|
746
|
+
}
|
|
653
747
|
// Restore terminal settings
|
|
654
748
|
if (process.stdin.isTTY) {
|
|
655
749
|
process.stdin.setRawMode(false);
|
|
@@ -664,6 +758,12 @@ class ShellClient {
|
|
|
664
758
|
if (this.portCheckInterval) {
|
|
665
759
|
clearInterval(this.portCheckInterval);
|
|
666
760
|
}
|
|
761
|
+
if (this.statusLineInterval) {
|
|
762
|
+
clearInterval(this.statusLineInterval);
|
|
763
|
+
}
|
|
764
|
+
if (this.commandCheckInterval) {
|
|
765
|
+
clearInterval(this.commandCheckInterval);
|
|
766
|
+
}
|
|
667
767
|
if (this.ptyProcess) {
|
|
668
768
|
this.ptyProcess.kill();
|
|
669
769
|
}
|
|
@@ -674,6 +774,288 @@ class ShellClient {
|
|
|
674
774
|
this.tunnelManager.closeTunnel();
|
|
675
775
|
}
|
|
676
776
|
}
|
|
777
|
+
checkAndStartClaude() {
|
|
778
|
+
if (this.ptyProcess) {
|
|
779
|
+
// Simple approach: just try to run claude
|
|
780
|
+
// If it exists, it will start. If not, the error will show in terminal
|
|
781
|
+
console.log(chalk_1.default.green('\n✓ Attempting to start Claude...'));
|
|
782
|
+
// Just run claude without clearing - keep QR code visible
|
|
783
|
+
setTimeout(() => {
|
|
784
|
+
if (this.ptyProcess) {
|
|
785
|
+
const claudeCommand = this.options.resumeClaude ? 'claude --resume' : 'claude';
|
|
786
|
+
this.ptyProcess.write(`${claudeCommand}\n`);
|
|
787
|
+
// Set command as 'claude' when we start it
|
|
788
|
+
this.currentCommand = claudeCommand;
|
|
789
|
+
this.sendCommandUpdate(claudeCommand);
|
|
790
|
+
}
|
|
791
|
+
}, 500);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
startStatusLine() {
|
|
795
|
+
// Update status line every second
|
|
796
|
+
this.statusLineInterval = setInterval(() => {
|
|
797
|
+
this.updateStatusLine();
|
|
798
|
+
}, 1000);
|
|
799
|
+
// Initial update
|
|
800
|
+
this.updateStatusLine();
|
|
801
|
+
}
|
|
802
|
+
setupTerminalWithStatusBar() {
|
|
803
|
+
if (!process.stdout.isTTY)
|
|
804
|
+
return;
|
|
805
|
+
// Clear screen and set up scrolling region
|
|
806
|
+
process.stdout.write('\x1b[2J'); // Clear entire screen
|
|
807
|
+
process.stdout.write('\x1b[1;1H'); // Move to top
|
|
808
|
+
if (this.statusBarEnabled) {
|
|
809
|
+
// Set scrolling region to exclude top line (ANSI escape: CSI r)
|
|
810
|
+
const rows = process.stdout.rows || 24;
|
|
811
|
+
process.stdout.write(`\x1b[2;${rows}r`); // Set scroll region from line 2 to bottom
|
|
812
|
+
// Move cursor to second line
|
|
813
|
+
process.stdout.write('\x1b[2;1H');
|
|
814
|
+
// Initial status line update
|
|
815
|
+
this.updateStatusLine();
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
// No scroll region restriction
|
|
819
|
+
process.stdout.write('\x1b[r'); // Reset scroll region to full terminal
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
updateStatusLine() {
|
|
823
|
+
if (!process.stdout.isTTY || !this.statusBarEnabled)
|
|
824
|
+
return;
|
|
825
|
+
const cols = process.stdout.columns || 80;
|
|
826
|
+
const sessionInfo = `tinyagent session: ${this.options.sessionId}`;
|
|
827
|
+
const viewInfo = `[${this.viewMode}]`;
|
|
828
|
+
const mobileStatus = this.clientTypes.mobile > 0 ? '📱' : '';
|
|
829
|
+
const clientInfo = `${this.connectedClients} client${this.connectedClients !== 1 ? 's' : ''} connected ${mobileStatus}`;
|
|
830
|
+
const shortcuts = `[Ctrl+S: Show QR | Ctrl+F: Fit | Ctrl+B: Toggle Bar]`;
|
|
831
|
+
// For smaller terminals, use abbreviated version
|
|
832
|
+
let middlePart = shortcuts;
|
|
833
|
+
if (cols < 100) {
|
|
834
|
+
middlePart = '[^S:QR ^F:Fit ^B:Bar]';
|
|
835
|
+
}
|
|
836
|
+
else if (cols < 80) {
|
|
837
|
+
middlePart = '[^S ^F ^B]';
|
|
838
|
+
}
|
|
839
|
+
// Calculate spacing
|
|
840
|
+
const leftPart = `${sessionInfo} ${viewInfo}`;
|
|
841
|
+
const rightPart = clientInfo;
|
|
842
|
+
const totalLength = leftPart.length + middlePart.length + rightPart.length + 2; // +2 for surrounding spaces
|
|
843
|
+
const spacing = Math.max(2, cols - totalLength);
|
|
844
|
+
const leftSpacing = Math.floor(spacing / 2);
|
|
845
|
+
const rightSpacing = spacing - leftSpacing;
|
|
846
|
+
// Build status line (exactly cols width, no padding)
|
|
847
|
+
const content = ` ${leftPart}${' '.repeat(leftSpacing)}${middlePart}${' '.repeat(rightSpacing)}${rightPart} `;
|
|
848
|
+
const statusLine = chalk_1.default.bgBlue.white.bold(content.substring(0, cols));
|
|
849
|
+
// Save cursor position, move to top line (outside scroll region), print status, restore cursor
|
|
850
|
+
process.stdout.write('\x1b7'); // Save cursor position
|
|
851
|
+
process.stdout.write('\x1b[1;1H'); // Move to line 1, column 1
|
|
852
|
+
process.stdout.write(statusLine); // Write status
|
|
853
|
+
process.stdout.write('\x1b[K'); // Clear to end of line (in case terminal is wider)
|
|
854
|
+
process.stdout.write('\x1b8'); // Restore cursor position
|
|
855
|
+
}
|
|
856
|
+
hideQRCode() {
|
|
857
|
+
if (!this.isShowingQR)
|
|
858
|
+
return;
|
|
859
|
+
// Clear flag
|
|
860
|
+
this.isShowingQR = false;
|
|
861
|
+
// Switch back to main screen buffer (restores previous content)
|
|
862
|
+
process.stdout.write('\x1b[?1049l');
|
|
863
|
+
// Restore status line
|
|
864
|
+
this.updateStatusLine();
|
|
865
|
+
// Replay any buffered output that came in while QR was shown
|
|
866
|
+
if (this.bufferedOutput.length > 0) {
|
|
867
|
+
this.bufferedOutput.forEach(output => {
|
|
868
|
+
process.stdout.write(output);
|
|
869
|
+
});
|
|
870
|
+
this.bufferedOutput = [];
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
showQRCode() {
|
|
874
|
+
// Set flag to start buffering
|
|
875
|
+
this.isShowingQR = true;
|
|
876
|
+
this.bufferedOutput = [];
|
|
877
|
+
// Save cursor position before clearing
|
|
878
|
+
process.stdout.write('\x1b[?1049h'); // Switch to alternate screen buffer
|
|
879
|
+
// Clear screen and show QR code
|
|
880
|
+
console.clear();
|
|
881
|
+
this.updateStatusLine();
|
|
882
|
+
(0, qr_generator_1.generateConnectionQR)(this.options.sessionId, this.options.relayUrl);
|
|
883
|
+
console.log(chalk_1.default.gray('Press any key to return to your session...'));
|
|
884
|
+
// Set up a temporary stdin handler for returning from QR view
|
|
885
|
+
const qrExitHandler = (data) => {
|
|
886
|
+
// Remove this handler
|
|
887
|
+
process.stdin.removeListener('data', qrExitHandler);
|
|
888
|
+
// Hide QR code
|
|
889
|
+
this.hideQRCode();
|
|
890
|
+
// Re-enable the normal stdin handler
|
|
891
|
+
if (this.stdinListener) {
|
|
892
|
+
this.stdinListener();
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
// Remove the normal stdin listener temporarily
|
|
896
|
+
process.stdin.removeAllListeners('data');
|
|
897
|
+
// Add the QR exit handler
|
|
898
|
+
process.stdin.on('data', qrExitHandler);
|
|
899
|
+
}
|
|
900
|
+
toggleViewMode() {
|
|
901
|
+
// Toggle between desktop and mobile view
|
|
902
|
+
this.viewMode = this.viewMode === 'desktop' ? 'mobile' : 'desktop';
|
|
903
|
+
// Get the dimensions for the selected view
|
|
904
|
+
const dimensions = this.viewMode === 'desktop'
|
|
905
|
+
? this.terminalDimensions.local
|
|
906
|
+
: this.terminalDimensions.mobile;
|
|
907
|
+
if (this.ptyProcess && dimensions) {
|
|
908
|
+
const { cols, rows } = dimensions;
|
|
909
|
+
const ptyRows = this.statusBarEnabled ? Math.max(1, rows - 1) : rows; // Only reserve line if status bar enabled
|
|
910
|
+
console.log(chalk_1.default.yellow(`\nSwitched to ${this.viewMode} view: ${cols}x${rows}`));
|
|
911
|
+
this.ptyProcess.resize(cols, ptyRows);
|
|
912
|
+
// Update status line to show current mode
|
|
913
|
+
if (this.statusBarEnabled) {
|
|
914
|
+
this.updateStatusLine();
|
|
915
|
+
}
|
|
916
|
+
// Don't clear screen - just let the resize take effect
|
|
917
|
+
// The terminal will naturally adjust to the new size
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
toggleStatusBar() {
|
|
921
|
+
this.statusBarEnabled = !this.statusBarEnabled;
|
|
922
|
+
if (!this.statusBarEnabled) {
|
|
923
|
+
// Clear the status bar and reset scroll region
|
|
924
|
+
if (process.stdout.isTTY) {
|
|
925
|
+
process.stdout.write('\x1b[r'); // Reset scroll region
|
|
926
|
+
process.stdout.write('\x1b7'); // Save cursor
|
|
927
|
+
process.stdout.write('\x1b[1;1H'); // Move to top
|
|
928
|
+
process.stdout.write('\x1b[K'); // Clear line
|
|
929
|
+
process.stdout.write('\x1b8'); // Restore cursor
|
|
930
|
+
}
|
|
931
|
+
// Resize PTY to use full terminal height
|
|
932
|
+
if (this.ptyProcess && process.stdout.isTTY) {
|
|
933
|
+
const dimensions = this.viewMode === 'desktop'
|
|
934
|
+
? this.terminalDimensions.local
|
|
935
|
+
: this.terminalDimensions.mobile;
|
|
936
|
+
this.ptyProcess.resize(dimensions.cols, dimensions.rows);
|
|
937
|
+
}
|
|
938
|
+
console.log(chalk_1.default.gray('\nStatus bar disabled. Press Ctrl+B to re-enable.'));
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
// Re-enable status bar
|
|
942
|
+
this.setupTerminalWithStatusBar();
|
|
943
|
+
// Resize PTY to account for status bar
|
|
944
|
+
if (this.ptyProcess && process.stdout.isTTY) {
|
|
945
|
+
const dimensions = this.viewMode === 'desktop'
|
|
946
|
+
? this.terminalDimensions.local
|
|
947
|
+
: this.terminalDimensions.mobile;
|
|
948
|
+
const ptyRows = Math.max(1, dimensions.rows - 1);
|
|
949
|
+
this.ptyProcess.resize(dimensions.cols, ptyRows);
|
|
950
|
+
}
|
|
951
|
+
console.log(chalk_1.default.gray('\nStatus bar enabled.'));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
startCommandMonitoring() {
|
|
955
|
+
// Also periodically check what's actually running
|
|
956
|
+
this.commandCheckInterval = setInterval(() => {
|
|
957
|
+
this.checkRunningProcess();
|
|
958
|
+
}, 5000); // Check every 5 seconds
|
|
959
|
+
}
|
|
960
|
+
checkRunningProcess() {
|
|
961
|
+
if (!this.ptyProcess)
|
|
962
|
+
return;
|
|
963
|
+
// Get the PID of the shell's child process
|
|
964
|
+
try {
|
|
965
|
+
const ptyPid = this.ptyProcess.pid;
|
|
966
|
+
if (ptyPid) {
|
|
967
|
+
// Use ps to check what's running under our PTY
|
|
968
|
+
const { execSync } = require('child_process');
|
|
969
|
+
try {
|
|
970
|
+
// Get immediate children of the PTY process
|
|
971
|
+
const output = execSync(`ps -o pid,ppid,comm -p ${ptyPid} && ps -o pid,ppid,comm | grep "^[[:space:]]*[0-9]\+[[:space:]]\+${ptyPid}"`, { encoding: 'utf8' });
|
|
972
|
+
const lines = output.split('\n').filter((line) => line.trim());
|
|
973
|
+
// Look for child processes
|
|
974
|
+
for (const line of lines) {
|
|
975
|
+
const parts = line.trim().split(/\s+/);
|
|
976
|
+
if (parts.length >= 3 && parts[1] === String(ptyPid)) {
|
|
977
|
+
const processName = parts.slice(2).join(' ');
|
|
978
|
+
// Skip shell processes
|
|
979
|
+
if (!processName.includes('bash') && !processName.includes('zsh') && !processName.includes('sh')) {
|
|
980
|
+
const command = processName.split('/').pop() || processName;
|
|
981
|
+
if (command !== this.currentCommand) {
|
|
982
|
+
this.currentCommand = command;
|
|
983
|
+
this.sendCommandUpdate(command);
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// If we get here, no child process found - shell is idle
|
|
990
|
+
if (this.currentCommand !== 'idle') {
|
|
991
|
+
this.currentCommand = 'idle';
|
|
992
|
+
this.sendCommandUpdate('idle');
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
catch (e) {
|
|
996
|
+
// ps command failed, ignore
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
catch (e) {
|
|
1001
|
+
// Error getting process info
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
detectCommand(data) {
|
|
1005
|
+
// Split by newlines
|
|
1006
|
+
const lines = data.split(/\r?\n/);
|
|
1007
|
+
for (const line of lines) {
|
|
1008
|
+
// Remove ANSI escape codes
|
|
1009
|
+
const cleanLine = line.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
1010
|
+
// Look for command patterns (prompt followed by command)
|
|
1011
|
+
// Remove the $ anchor to catch partial commands
|
|
1012
|
+
const commandMatch = cleanLine.match(/[$#%>]\s+(.+)/);
|
|
1013
|
+
if (commandMatch && commandMatch[1]) {
|
|
1014
|
+
const potentialCommand = commandMatch[1].trim();
|
|
1015
|
+
// Filter out common non-commands and check if it's different
|
|
1016
|
+
if (potentialCommand &&
|
|
1017
|
+
potentialCommand.length > 1 &&
|
|
1018
|
+
potentialCommand !== this.currentCommand &&
|
|
1019
|
+
!potentialCommand.startsWith('\x1b')) {
|
|
1020
|
+
// Common command prefixes that indicate actual work
|
|
1021
|
+
const commandPrefixes = ['npm', 'yarn', 'node', 'python', 'git', 'vim', 'make', 'cargo', 'go', 'docker', 'kubectl', 'claude'];
|
|
1022
|
+
const isCommand = commandPrefixes.some(cmd => potentialCommand.startsWith(cmd));
|
|
1023
|
+
if (isCommand) {
|
|
1024
|
+
this.currentCommand = potentialCommand;
|
|
1025
|
+
this.sendCommandUpdate(potentialCommand);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
// Also check for known interactive programs in the output
|
|
1031
|
+
const interactivePrograms = [
|
|
1032
|
+
{ pattern: /Claude Code.*session started/i, command: 'claude' },
|
|
1033
|
+
{ pattern: /Welcome to Claude/i, command: 'claude' },
|
|
1034
|
+
{ pattern: /VIM - Vi IMproved/i, command: 'vim' },
|
|
1035
|
+
{ pattern: /GNU nano/i, command: 'nano' },
|
|
1036
|
+
{ pattern: /\[\d+\]\+\s+Running\s+(.+)/i, extract: true }, // Background job
|
|
1037
|
+
];
|
|
1038
|
+
for (const prog of interactivePrograms) {
|
|
1039
|
+
const match = data.match(prog.pattern);
|
|
1040
|
+
if (match) {
|
|
1041
|
+
const command = prog.extract && match[1] ? match[1].trim() : prog.command;
|
|
1042
|
+
if (command && command !== this.currentCommand) {
|
|
1043
|
+
this.currentCommand = command;
|
|
1044
|
+
this.sendCommandUpdate(command);
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
sendCommandUpdate(command) {
|
|
1051
|
+
// Send command update to relay
|
|
1052
|
+
this.sendMessage({
|
|
1053
|
+
type: shared_types_1.MessageType.COMMAND_UPDATE,
|
|
1054
|
+
sessionId: this.options.sessionId,
|
|
1055
|
+
timestamp: Date.now(),
|
|
1056
|
+
command: command
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
677
1059
|
disconnect() {
|
|
678
1060
|
this.cleanup();
|
|
679
1061
|
if (this.ws) {
|