nikcli-remote 1.0.9 → 1.0.11

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.
@@ -0,0 +1,1145 @@
1
+ import {
2
+ __esm,
3
+ __export,
4
+ __toCommonJS
5
+ } from "./chunk-GI5RMYH6.js";
6
+
7
+ // src/web-client.ts
8
+ var web_client_exports = {};
9
+ __export(web_client_exports, {
10
+ getWebClient: () => getWebClient
11
+ });
12
+ function getWebClient() {
13
+ return `<!DOCTYPE html>
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="UTF-8">
17
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
18
+ <meta name="apple-mobile-web-app-capable" content="yes">
19
+ <meta name="mobile-web-app-capable" content="yes">
20
+ <meta name="theme-color" content="#0d1117">
21
+ <title>NikCLI Remote</title>
22
+ <style>
23
+ :root {
24
+ --bg: #0d1117;
25
+ --bg-secondary: #161b22;
26
+ --fg: #e6edf3;
27
+ --fg-muted: #8b949e;
28
+ --accent: #58a6ff;
29
+ --success: #3fb950;
30
+ --warning: #d29922;
31
+ --error: #f85149;
32
+ --border: #30363d;
33
+ --font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
34
+ }
35
+
36
+ * {
37
+ box-sizing: border-box;
38
+ margin: 0;
39
+ padding: 0;
40
+ -webkit-tap-highlight-color: transparent;
41
+ }
42
+
43
+ html, body {
44
+ height: 100%;
45
+ background: var(--bg);
46
+ color: var(--fg);
47
+ font-family: var(--font-mono);
48
+ font-size: 14px;
49
+ overflow: hidden;
50
+ touch-action: manipulation;
51
+ }
52
+
53
+ #app {
54
+ display: flex;
55
+ flex-direction: column;
56
+ height: 100%;
57
+ height: 100dvh;
58
+ }
59
+
60
+ /* Header */
61
+ #header {
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: space-between;
65
+ padding: 12px 16px;
66
+ background: var(--bg-secondary);
67
+ border-bottom: 1px solid var(--border);
68
+ flex-shrink: 0;
69
+ }
70
+
71
+ #header h1 {
72
+ font-size: 16px;
73
+ font-weight: 600;
74
+ color: var(--accent);
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 8px;
78
+ }
79
+
80
+ #header h1::before {
81
+ content: '';
82
+ width: 10px;
83
+ height: 10px;
84
+ background: var(--accent);
85
+ border-radius: 2px;
86
+ }
87
+
88
+ #status {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 6px;
92
+ font-size: 12px;
93
+ color: var(--fg-muted);
94
+ }
95
+
96
+ #status-dot {
97
+ width: 8px;
98
+ height: 8px;
99
+ border-radius: 50%;
100
+ background: var(--error);
101
+ transition: background 0.3s;
102
+ }
103
+
104
+ #status-dot.connected {
105
+ background: var(--success);
106
+ }
107
+
108
+ #status-dot.connecting {
109
+ background: var(--warning);
110
+ animation: pulse 1s infinite;
111
+ }
112
+
113
+ @keyframes pulse {
114
+ 0%, 100% { opacity: 1; }
115
+ 50% { opacity: 0.5; }
116
+ }
117
+
118
+ /* Terminal */
119
+ #terminal-container {
120
+ flex: 1;
121
+ overflow: hidden;
122
+ position: relative;
123
+ }
124
+
125
+ #terminal {
126
+ height: 100%;
127
+ padding: 12px;
128
+ overflow-y: auto;
129
+ overflow-x: hidden;
130
+ font-size: 13px;
131
+ line-height: 1.5;
132
+ white-space: pre-wrap;
133
+ word-break: break-all;
134
+ -webkit-overflow-scrolling: touch;
135
+ }
136
+
137
+ #terminal::-webkit-scrollbar {
138
+ width: 6px;
139
+ }
140
+
141
+ #terminal::-webkit-scrollbar-track {
142
+ background: var(--bg);
143
+ }
144
+
145
+ #terminal::-webkit-scrollbar-thumb {
146
+ background: var(--border);
147
+ border-radius: 3px;
148
+ }
149
+
150
+ .cursor {
151
+ display: inline-block;
152
+ width: 8px;
153
+ height: 16px;
154
+ background: var(--fg);
155
+ animation: blink 1s step-end infinite;
156
+ vertical-align: text-bottom;
157
+ }
158
+
159
+ @keyframes blink {
160
+ 50% { opacity: 0; }
161
+ }
162
+
163
+ /* Notifications */
164
+ #notifications {
165
+ position: fixed;
166
+ top: 60px;
167
+ left: 12px;
168
+ right: 12px;
169
+ z-index: 1000;
170
+ pointer-events: none;
171
+ }
172
+
173
+ .notification {
174
+ background: var(--bg-secondary);
175
+ border: 1px solid var(--border);
176
+ border-radius: 8px;
177
+ padding: 12px 16px;
178
+ margin-bottom: 8px;
179
+ animation: slideIn 0.3s ease;
180
+ pointer-events: auto;
181
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
182
+ }
183
+
184
+ .notification.success { border-left: 3px solid var(--success); }
185
+ .notification.error { border-left: 3px solid var(--error); }
186
+ .notification.warning { border-left: 3px solid var(--warning); }
187
+ .notification.info { border-left: 3px solid var(--accent); }
188
+
189
+ .notification h4 {
190
+ font-size: 14px;
191
+ font-weight: 600;
192
+ margin-bottom: 4px;
193
+ }
194
+
195
+ .notification p {
196
+ font-size: 12px;
197
+ color: var(--fg-muted);
198
+ }
199
+
200
+ @keyframes slideIn {
201
+ from { transform: translateY(-20px); opacity: 0; }
202
+ to { transform: translateY(0); opacity: 1; }
203
+ }
204
+
205
+ /* Quick Keys */
206
+ #quickkeys {
207
+ display: grid;
208
+ grid-template-columns: repeat(6, 1fr);
209
+ gap: 6px;
210
+ padding: 8px 12px;
211
+ background: var(--bg-secondary);
212
+ border-top: 1px solid var(--border);
213
+ flex-shrink: 0;
214
+ }
215
+
216
+ .qkey {
217
+ background: var(--bg);
218
+ border: 1px solid var(--border);
219
+ border-radius: 6px;
220
+ padding: 10px 4px;
221
+ color: var(--fg);
222
+ font-size: 11px;
223
+ font-family: var(--font-mono);
224
+ text-align: center;
225
+ cursor: pointer;
226
+ user-select: none;
227
+ transition: background 0.1s, transform 0.1s;
228
+ }
229
+
230
+ .qkey:active {
231
+ background: var(--border);
232
+ transform: scale(0.95);
233
+ }
234
+
235
+ .qkey.wide {
236
+ grid-column: span 2;
237
+ }
238
+
239
+ .qkey.accent {
240
+ background: var(--accent);
241
+ border-color: var(--accent);
242
+ color: #fff;
243
+ }
244
+
245
+ /* Input */
246
+ #input-container {
247
+ padding: 12px;
248
+ background: var(--bg-secondary);
249
+ border-top: 1px solid var(--border);
250
+ flex-shrink: 0;
251
+ }
252
+
253
+ #input-row {
254
+ display: flex;
255
+ gap: 8px;
256
+ }
257
+
258
+ #input {
259
+ flex: 1;
260
+ background: var(--bg);
261
+ border: 1px solid var(--border);
262
+ border-radius: 8px;
263
+ padding: 12px 14px;
264
+ color: var(--fg);
265
+ font-family: var(--font-mono);
266
+ font-size: 16px;
267
+ outline: none;
268
+ transition: border-color 0.2s;
269
+ }
270
+
271
+ #input:focus {
272
+ border-color: var(--accent);
273
+ }
274
+
275
+ #input::placeholder {
276
+ color: var(--fg-muted);
277
+ }
278
+
279
+ #send {
280
+ background: var(--accent);
281
+ color: #fff;
282
+ border: none;
283
+ border-radius: 8px;
284
+ padding: 12px 20px;
285
+ font-size: 14px;
286
+ font-weight: 600;
287
+ font-family: var(--font-mono);
288
+ cursor: pointer;
289
+ transition: opacity 0.2s, transform 0.1s;
290
+ }
291
+
292
+ #send:active {
293
+ opacity: 0.8;
294
+ transform: scale(0.98);
295
+ }
296
+
297
+ /* Auth Screen */
298
+ #auth-screen {
299
+ position: fixed;
300
+ inset: 0;
301
+ background: var(--bg);
302
+ display: flex;
303
+ flex-direction: column;
304
+ align-items: center;
305
+ justify-content: center;
306
+ gap: 20px;
307
+ z-index: 2000;
308
+ }
309
+
310
+ #auth-screen.hidden {
311
+ display: none;
312
+ }
313
+
314
+ .spinner {
315
+ width: 40px;
316
+ height: 40px;
317
+ border: 3px solid var(--border);
318
+ border-top-color: var(--accent);
319
+ border-radius: 50%;
320
+ animation: spin 1s linear infinite;
321
+ }
322
+
323
+ @keyframes spin {
324
+ to { transform: rotate(360deg); }
325
+ }
326
+
327
+ #auth-screen p {
328
+ color: var(--fg-muted);
329
+ font-size: 14px;
330
+ }
331
+
332
+ #auth-screen .error {
333
+ color: var(--error);
334
+ }
335
+
336
+ /* Safe area padding for notched devices */
337
+ @supports (padding: env(safe-area-inset-bottom)) {
338
+ #input-container {
339
+ padding-bottom: calc(12px + env(safe-area-inset-bottom));
340
+ }
341
+ }
342
+
343
+ /* Landscape adjustments */
344
+ @media (max-height: 500px) {
345
+ #quickkeys {
346
+ grid-template-columns: repeat(12, 1fr);
347
+ padding: 6px 8px;
348
+ }
349
+ .qkey {
350
+ padding: 8px 2px;
351
+ font-size: 10px;
352
+ }
353
+ #terminal {
354
+ font-size: 12px;
355
+ }
356
+ }
357
+ </style>
358
+ </head>
359
+ <body>
360
+ <div id="app">
361
+ <div id="auth-screen">
362
+ <div class="spinner"></div>
363
+ <p id="auth-status">Connecting to NikCLI...</p>
364
+ </div>
365
+
366
+ <header id="header">
367
+ <h1>NikCLI Remote</h1>
368
+ <div id="status">
369
+ <span id="status-dot" class="connecting"></span>
370
+ <span id="status-text">Connecting</span>
371
+ </div>
372
+ </header>
373
+
374
+ <div id="terminal-container">
375
+ <div id="terminal"></div>
376
+ </div>
377
+
378
+ <div id="notifications"></div>
379
+
380
+ <div id="quickkeys">
381
+ <button class="qkey" data-key="\\t">Tab</button>
382
+ <button class="qkey" data-key="\\x1b[A">\u2191</button>
383
+ <button class="qkey" data-key="\\x1b[B">\u2193</button>
384
+ <button class="qkey" data-key="\\x1b[D">\u2190</button>
385
+ <button class="qkey" data-key="\\x1b[C">\u2192</button>
386
+ <button class="qkey" data-key="\\x1b">Esc</button>
387
+ <button class="qkey" data-key="\\x03">^C</button>
388
+ <button class="qkey" data-key="\\x04">^D</button>
389
+ <button class="qkey" data-key="\\x1a">^Z</button>
390
+ <button class="qkey" data-key="\\x0c">^L</button>
391
+ <button class="qkey wide accent" data-key="\\r">Enter \u23CE</button>
392
+ </div>
393
+
394
+ <div id="input-container">
395
+ <div id="input-row">
396
+ <input type="text" id="input" placeholder="Type command..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
397
+ <button id="send">Send</button>
398
+ </div>
399
+ </div>
400
+ </div>
401
+
402
+ <script>
403
+ (function() {
404
+ 'use strict';
405
+
406
+ // Parse URL params
407
+ const params = new URLSearchParams(location.search);
408
+ const token = params.get('t');
409
+ const sessionId = params.get('s');
410
+
411
+ // DOM elements
412
+ const terminal = document.getElementById('terminal');
413
+ const input = document.getElementById('input');
414
+ const sendBtn = document.getElementById('send');
415
+ const statusDot = document.getElementById('status-dot');
416
+ const statusText = document.getElementById('status-text');
417
+ const authScreen = document.getElementById('auth-screen');
418
+ const authStatus = document.getElementById('auth-status');
419
+ const notifications = document.getElementById('notifications');
420
+
421
+ // State
422
+ let ws = null;
423
+ let reconnectAttempts = 0;
424
+ const maxReconnectAttempts = 5;
425
+ let terminalEnabled = true;
426
+
427
+ // Connect to WebSocket
428
+ function connect() {
429
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
430
+ ws = new WebSocket(protocol + '//' + location.host);
431
+
432
+ ws.onopen = function() {
433
+ setStatus('connecting', 'Authenticating...');
434
+ ws.send(JSON.stringify({ type: 'auth', token: token }));
435
+ };
436
+
437
+ ws.onmessage = function(event) {
438
+ try {
439
+ const msg = JSON.parse(event.data);
440
+ handleMessage(msg);
441
+ } catch (e) {
442
+ console.error('Parse error:', e);
443
+ }
444
+ };
445
+
446
+ ws.onclose = function() {
447
+ setStatus('disconnected', 'Disconnected');
448
+ if (reconnectAttempts < maxReconnectAttempts) {
449
+ reconnectAttempts++;
450
+ const delay = Math.min(2000 * reconnectAttempts, 10000);
451
+ setTimeout(connect, delay);
452
+ } else {
453
+ authStatus.textContent = 'Connection failed. Refresh to retry.';
454
+ authStatus.classList.add('error');
455
+ authScreen.classList.remove('hidden');
456
+ }
457
+ };
458
+
459
+ ws.onerror = function() {
460
+ console.error('WebSocket error');
461
+ };
462
+ }
463
+
464
+ // Handle incoming message
465
+ function handleMessage(msg) {
466
+ switch (msg.type) {
467
+ case 'auth:required':
468
+ // Already sent auth on open
469
+ break;
470
+
471
+ case 'auth:success':
472
+ authScreen.classList.add('hidden');
473
+ setStatus('connected', 'Connected');
474
+ reconnectAttempts = 0;
475
+ terminalEnabled = msg.payload?.terminalEnabled !== false;
476
+ if (terminalEnabled) {
477
+ appendOutput('\\x1b[32mConnected to NikCLI\\x1b[0m\\n\\n');
478
+ }
479
+ break;
480
+
481
+ case 'auth:failed':
482
+ authStatus.textContent = 'Authentication failed';
483
+ authStatus.classList.add('error');
484
+ break;
485
+
486
+ case 'terminal:output':
487
+ if (msg.payload?.data) {
488
+ appendOutput(msg.payload.data);
489
+ }
490
+ break;
491
+
492
+ case 'terminal:exit':
493
+ appendOutput('\\n\\x1b[33m[Process exited with code ' + (msg.payload?.code || 0) + ']\\x1b[0m\\n');
494
+ break;
495
+
496
+ case 'notification':
497
+ showNotification(msg.payload);
498
+ break;
499
+
500
+ case 'session:end':
501
+ appendOutput('\\n\\x1b[31m[Session ended]\\x1b[0m\\n');
502
+ setStatus('disconnected', 'Session ended');
503
+ break;
504
+
505
+ case 'pong':
506
+ // Heartbeat response
507
+ break;
508
+
509
+ default:
510
+ console.log('Unknown message:', msg.type);
511
+ }
512
+ }
513
+
514
+ // Append text to terminal with ANSI support
515
+ function appendOutput(text) {
516
+ // Simple ANSI to HTML conversion
517
+ const html = ansiToHtml(text);
518
+ terminal.innerHTML += html;
519
+ terminal.scrollTop = terminal.scrollHeight;
520
+ }
521
+
522
+ // Basic ANSI to HTML
523
+ function ansiToHtml(text) {
524
+ const ansiColors = {
525
+ '30': '#6e7681', '31': '#f85149', '32': '#3fb950', '33': '#d29922',
526
+ '34': '#58a6ff', '35': '#bc8cff', '36': '#76e3ea', '37': '#e6edf3',
527
+ '90': '#6e7681', '91': '#f85149', '92': '#3fb950', '93': '#d29922',
528
+ '94': '#58a6ff', '95': '#bc8cff', '96': '#76e3ea', '97': '#ffffff'
529
+ };
530
+
531
+ let result = '';
532
+ let currentStyle = '';
533
+
534
+ const parts = text.split(/\\x1b\\[([0-9;]+)m/);
535
+ for (let i = 0; i < parts.length; i++) {
536
+ if (i % 2 === 0) {
537
+ // Text content
538
+ result += escapeHtml(parts[i]);
539
+ } else {
540
+ // ANSI code
541
+ const codes = parts[i].split(';');
542
+ for (const code of codes) {
543
+ if (code === '0') {
544
+ if (currentStyle) {
545
+ result += '</span>';
546
+ currentStyle = '';
547
+ }
548
+ } else if (code === '1') {
549
+ currentStyle = 'font-weight:bold;';
550
+ result += '<span style="' + currentStyle + '">';
551
+ } else if (ansiColors[code]) {
552
+ if (currentStyle) result += '</span>';
553
+ currentStyle = 'color:' + ansiColors[code] + ';';
554
+ result += '<span style="' + currentStyle + '">';
555
+ }
556
+ }
557
+ }
558
+ }
559
+
560
+ if (currentStyle) result += '</span>';
561
+ return result;
562
+ }
563
+
564
+ // Escape HTML
565
+ function escapeHtml(text) {
566
+ return text
567
+ .replace(/&/g, '&amp;')
568
+ .replace(/</g, '&lt;')
569
+ .replace(/>/g, '&gt;')
570
+ .replace(/"/g, '&quot;')
571
+ .replace(/\\n/g, '<br>')
572
+ .replace(/ /g, '&nbsp;');
573
+ }
574
+
575
+ // Set connection status
576
+ function setStatus(state, text) {
577
+ statusDot.className = state === 'connected' ? 'connected' :
578
+ state === 'connecting' ? 'connecting' : '';
579
+ statusText.textContent = text;
580
+ }
581
+
582
+ // Send data to terminal
583
+ function send(data) {
584
+ if (ws && ws.readyState === WebSocket.OPEN) {
585
+ ws.send(JSON.stringify({ type: 'terminal:input', data: data }));
586
+ }
587
+ }
588
+
589
+ // Show notification
590
+ function showNotification(n) {
591
+ if (!n) return;
592
+ const el = document.createElement('div');
593
+ el.className = 'notification ' + (n.type || 'info');
594
+ el.innerHTML = '<h4>' + escapeHtml(n.title || 'Notification') + '</h4>' +
595
+ '<p>' + escapeHtml(n.body || '') + '</p>';
596
+ notifications.appendChild(el);
597
+ setTimeout(function() { el.remove(); }, 5000);
598
+ }
599
+
600
+ // Event: Send button
601
+ sendBtn.onclick = function() {
602
+ if (input.value) {
603
+ send(input.value + '\\r');
604
+ input.value = '';
605
+ }
606
+ input.focus();
607
+ };
608
+
609
+ // Event: Enter key in input
610
+ input.onkeydown = function(e) {
611
+ if (e.key === 'Enter') {
612
+ e.preventDefault();
613
+ sendBtn.click();
614
+ }
615
+ };
616
+
617
+ // Event: Quick keys
618
+ document.querySelectorAll('.qkey').forEach(function(btn) {
619
+ btn.onclick = function() {
620
+ const key = btn.dataset.key;
621
+ const decoded = key
622
+ .replace(/\\\\x([0-9a-f]{2})/gi, function(_, hex) {
623
+ return String.fromCharCode(parseInt(hex, 16));
624
+ })
625
+ .replace(/\\\\t/g, '\\t')
626
+ .replace(/\\\\r/g, '\\r')
627
+ .replace(/\\\\n/g, '\\n');
628
+ send(decoded);
629
+ input.focus();
630
+ };
631
+ });
632
+
633
+ // Heartbeat
634
+ setInterval(function() {
635
+ if (ws && ws.readyState === WebSocket.OPEN) {
636
+ ws.send(JSON.stringify({ type: 'ping' }));
637
+ }
638
+ }, 25000);
639
+
640
+ // Handle resize
641
+ function sendResize() {
642
+ if (ws && ws.readyState === WebSocket.OPEN) {
643
+ const cols = Math.floor(terminal.clientWidth / 8);
644
+ const rows = Math.floor(terminal.clientHeight / 18);
645
+ ws.send(JSON.stringify({ type: 'terminal:resize', cols: cols, rows: rows }));
646
+ }
647
+ }
648
+
649
+ window.addEventListener('resize', sendResize);
650
+ setTimeout(sendResize, 1000);
651
+
652
+ // Start connection
653
+ if (token) {
654
+ connect();
655
+ } else {
656
+ authStatus.textContent = 'Invalid session URL';
657
+ authStatus.classList.add('error');
658
+ }
659
+ })();
660
+ </script>
661
+ </body>
662
+ </html>`;
663
+ }
664
+ var init_web_client = __esm({
665
+ "src/web-client.ts"() {
666
+ "use strict";
667
+ }
668
+ });
669
+
670
+ // src/server.ts
671
+ import { EventEmitter } from "events";
672
+ import { createServer } from "http";
673
+ import { WebSocketServer, WebSocket } from "ws";
674
+ import crypto from "crypto";
675
+ import os from "os";
676
+
677
+ // src/types.ts
678
+ var DEFAULT_CONFIG = {
679
+ port: 0,
680
+ host: "0.0.0.0",
681
+ enableTunnel: true,
682
+ tunnelProvider: "localtunnel",
683
+ maxConnections: 5,
684
+ heartbeatInterval: 3e4,
685
+ shell: "/bin/bash",
686
+ cols: 80,
687
+ rows: 24,
688
+ enableTerminal: true,
689
+ sessionTimeout: 0
690
+ };
691
+ var MessageTypes = {
692
+ // Auth
693
+ AUTH_REQUIRED: "auth:required",
694
+ AUTH: "auth",
695
+ AUTH_SUCCESS: "auth:success",
696
+ AUTH_FAILED: "auth:failed",
697
+ // Terminal
698
+ TERMINAL_OUTPUT: "terminal:output",
699
+ TERMINAL_INPUT: "terminal:input",
700
+ TERMINAL_RESIZE: "terminal:resize",
701
+ TERMINAL_EXIT: "terminal:exit",
702
+ TERMINAL_CLEAR: "terminal:clear",
703
+ // Notifications
704
+ NOTIFICATION: "notification",
705
+ // Heartbeat
706
+ PING: "ping",
707
+ PONG: "pong",
708
+ // Session
709
+ SESSION_INFO: "session:info",
710
+ SESSION_END: "session:end",
711
+ // Commands (NikCLI specific)
712
+ COMMAND: "command",
713
+ COMMAND_RESULT: "command:result",
714
+ // Agent events
715
+ AGENT_START: "agent:start",
716
+ AGENT_PROGRESS: "agent:progress",
717
+ AGENT_COMPLETE: "agent:complete",
718
+ AGENT_ERROR: "agent:error"
719
+ };
720
+
721
+ // src/server.ts
722
+ var RemoteServer = class extends EventEmitter {
723
+ config;
724
+ httpServer = null;
725
+ wss = null;
726
+ clients = /* @__PURE__ */ new Map();
727
+ session = null;
728
+ heartbeatTimer = null;
729
+ sessionTimeoutTimer = null;
730
+ isRunning = false;
731
+ sessionSecret;
732
+ // stdin/stdout proxy state
733
+ originalStdoutWrite = null;
734
+ originalStdinOn = null;
735
+ constructor(config = {}) {
736
+ super();
737
+ this.config = { ...DEFAULT_CONFIG, ...config };
738
+ this.sessionSecret = config.sessionSecret || this.generateSecret();
739
+ }
740
+ /**
741
+ * Generate a random secret
742
+ */
743
+ generateSecret() {
744
+ return crypto.randomBytes(16).toString("hex");
745
+ }
746
+ /**
747
+ * Start the remote server - creates WebSocket server that proxies stdin/stdout
748
+ */
749
+ async start(options = {}) {
750
+ if (this.isRunning) {
751
+ throw new Error("Server already running");
752
+ }
753
+ const sessionId = this.generateSessionId();
754
+ this.httpServer = createServer((req, res) => this.handleHttpRequest(req, res));
755
+ this.wss = new WebSocketServer({ server: this.httpServer });
756
+ this.setupWebSocketHandlers();
757
+ const port = await new Promise((resolve, reject) => {
758
+ this.httpServer.listen(this.config.port, this.config.host, () => {
759
+ const addr = this.httpServer.address();
760
+ resolve(typeof addr === "object" ? addr?.port || 0 : 0);
761
+ });
762
+ this.httpServer.on("error", reject);
763
+ });
764
+ const localIp = this.getLocalIP();
765
+ const localUrl = `http://${localIp}:${port}`;
766
+ this.session = {
767
+ id: sessionId,
768
+ name: options.name || `nikcli-${sessionId}`,
769
+ qrCode: "",
770
+ qrUrl: `${localUrl}?s=${sessionId}&t=${this.sessionSecret}`,
771
+ localUrl,
772
+ status: "waiting",
773
+ connectedDevices: [],
774
+ startedAt: /* @__PURE__ */ new Date(),
775
+ lastActivity: /* @__PURE__ */ new Date(),
776
+ port
777
+ };
778
+ if (options.processForStreaming) {
779
+ this.setupStdioProxy(options.processForStreaming.stdout, options.processForStreaming.stdin);
780
+ }
781
+ this.startHeartbeat();
782
+ if (this.config.sessionTimeout > 0) {
783
+ this.startSessionTimeout();
784
+ }
785
+ this.session.status = "waiting";
786
+ this.isRunning = true;
787
+ this.emit("started", this.session);
788
+ return this.session;
789
+ }
790
+ /**
791
+ * Setup stdin/stdout proxy to forward to WebSocket clients
792
+ */
793
+ setupStdioProxy(stdout, stdin) {
794
+ const originalWrite = stdout.write.bind(stdout);
795
+ stdout.write = ((data, encoding, cb) => {
796
+ const result = originalWrite(data, encoding, cb);
797
+ const text = data instanceof Buffer ? data.toString() : data;
798
+ const cleaned = this.cleanOutputForMobile(text);
799
+ this.broadcastToAll({ type: MessageTypes.TERMINAL_OUTPUT, payload: { data: cleaned } });
800
+ return result;
801
+ });
802
+ this.wss?.on("connection", (ws) => {
803
+ ws.on("message", (rawData) => {
804
+ try {
805
+ const msg = JSON.parse(rawData.toString());
806
+ if (msg.type === MessageTypes.TERMINAL_INPUT && msg.data) {
807
+ const inputData = Buffer.from(msg.data);
808
+ stdin.emit("data", inputData);
809
+ }
810
+ } catch {
811
+ }
812
+ });
813
+ });
814
+ }
815
+ /**
816
+ * Clean output for mobile display - remove ANSI codes and TUI artifacts
817
+ */
818
+ cleanOutputForMobile(text) {
819
+ let cleaned = text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\r/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
820
+ return cleaned;
821
+ }
822
+ /**
823
+ * Stop the server
824
+ */
825
+ async stop() {
826
+ if (!this.isRunning) return;
827
+ this.isRunning = false;
828
+ if (this.originalStdoutWrite && this.session) {
829
+ }
830
+ if (this.heartbeatTimer) {
831
+ clearInterval(this.heartbeatTimer);
832
+ this.heartbeatTimer = null;
833
+ }
834
+ if (this.sessionTimeoutTimer) {
835
+ clearTimeout(this.sessionTimeoutTimer);
836
+ this.sessionTimeoutTimer = null;
837
+ }
838
+ this.broadcastToAll({ type: MessageTypes.SESSION_END, payload: {} });
839
+ for (const client of this.clients.values()) {
840
+ client.ws.close(1e3, "Server shutting down");
841
+ }
842
+ this.clients.clear();
843
+ if (this.wss) {
844
+ this.wss.close();
845
+ this.wss = null;
846
+ }
847
+ if (this.httpServer) {
848
+ await new Promise((resolve) => {
849
+ this.httpServer.close(() => resolve());
850
+ });
851
+ this.httpServer = null;
852
+ }
853
+ if (this.session) {
854
+ this.session.status = "stopped";
855
+ }
856
+ this.emit("stopped");
857
+ }
858
+ /**
859
+ * Broadcast message to all authenticated clients
860
+ */
861
+ broadcastToAll(message) {
862
+ const data = JSON.stringify({
863
+ type: message.type,
864
+ payload: message.payload,
865
+ timestamp: message.timestamp || Date.now()
866
+ });
867
+ for (const client of this.clients.values()) {
868
+ if (client.authenticated && client.ws.readyState === WebSocket.OPEN) {
869
+ client.ws.send(data);
870
+ }
871
+ }
872
+ }
873
+ /**
874
+ * Public broadcast method for compatibility
875
+ */
876
+ broadcast(message) {
877
+ this.broadcastToAll(message);
878
+ }
879
+ /**
880
+ * Send notification to clients
881
+ */
882
+ notify(notification) {
883
+ this.broadcastToAll({
884
+ type: MessageTypes.NOTIFICATION,
885
+ payload: notification
886
+ });
887
+ }
888
+ /**
889
+ * Get current session
890
+ */
891
+ getSession() {
892
+ return this.session;
893
+ }
894
+ /**
895
+ * Check if server is running
896
+ */
897
+ isActive() {
898
+ return this.isRunning && this.session?.status !== "stopped";
899
+ }
900
+ /**
901
+ * Get connected client count
902
+ */
903
+ getConnectedCount() {
904
+ let count = 0;
905
+ for (const client of this.clients.values()) {
906
+ if (client.authenticated) count++;
907
+ }
908
+ return count;
909
+ }
910
+ /**
911
+ * Write data to all connected clients (for manual output streaming)
912
+ */
913
+ writeToClients(data) {
914
+ this.broadcastToAll({ type: MessageTypes.TERMINAL_OUTPUT, payload: { data } });
915
+ }
916
+ /**
917
+ * Alias for writeToClients - for compatibility
918
+ */
919
+ writeToTerminal(data) {
920
+ this.writeToClients(data);
921
+ }
922
+ /**
923
+ * Resize terminal (for compatibility - not used in direct streaming mode)
924
+ */
925
+ resizeTerminal(cols, rows) {
926
+ }
927
+ /**
928
+ * Setup WebSocket handlers
929
+ */
930
+ setupWebSocketHandlers() {
931
+ this.wss.on("connection", (ws, req) => {
932
+ const clientId = this.generateClientId();
933
+ const client = {
934
+ id: clientId,
935
+ ws,
936
+ authenticated: false,
937
+ device: {
938
+ id: clientId,
939
+ userAgent: req.headers["user-agent"],
940
+ ip: req.socket.remoteAddress,
941
+ connectedAt: /* @__PURE__ */ new Date(),
942
+ lastActivity: /* @__PURE__ */ new Date()
943
+ },
944
+ lastPing: Date.now()
945
+ };
946
+ if (this.clients.size >= this.config.maxConnections) {
947
+ ws.close(1013, "Max connections reached");
948
+ return;
949
+ }
950
+ this.clients.set(clientId, client);
951
+ ws.send(JSON.stringify({ type: MessageTypes.AUTH_REQUIRED, timestamp: Date.now() }));
952
+ ws.on("message", (data) => {
953
+ try {
954
+ const message = JSON.parse(data.toString());
955
+ this.handleClientMessage(client, message);
956
+ } catch {
957
+ }
958
+ });
959
+ ws.on("close", () => {
960
+ this.clients.delete(clientId);
961
+ if (this.session && client.authenticated) {
962
+ this.session.connectedDevices = this.session.connectedDevices.filter(
963
+ (d) => d.id !== clientId
964
+ );
965
+ if (this.session.connectedDevices.length === 0) {
966
+ this.session.status = "waiting";
967
+ }
968
+ this.emit("client:disconnected", client.device);
969
+ }
970
+ });
971
+ ws.on("error", (error) => {
972
+ this.emit("client:error", clientId, error);
973
+ });
974
+ ws.on("pong", () => {
975
+ client.lastPing = Date.now();
976
+ });
977
+ });
978
+ }
979
+ /**
980
+ * Handle client message
981
+ */
982
+ handleClientMessage(client, message) {
983
+ client.device.lastActivity = /* @__PURE__ */ new Date();
984
+ if (this.session) {
985
+ this.session.lastActivity = /* @__PURE__ */ new Date();
986
+ }
987
+ if (this.config.sessionTimeout > 0) {
988
+ this.resetSessionTimeout();
989
+ }
990
+ switch (message.type) {
991
+ case MessageTypes.AUTH:
992
+ this.handleAuth(client, message.token);
993
+ break;
994
+ case MessageTypes.TERMINAL_INPUT:
995
+ break;
996
+ case MessageTypes.TERMINAL_RESIZE:
997
+ this.emit("message", client, message);
998
+ break;
999
+ case MessageTypes.PING:
1000
+ client.ws.send(JSON.stringify({ type: MessageTypes.PONG, timestamp: Date.now() }));
1001
+ break;
1002
+ default:
1003
+ this.emit("message", client, message);
1004
+ }
1005
+ }
1006
+ /**
1007
+ * Handle authentication
1008
+ */
1009
+ handleAuth(client, token) {
1010
+ if (token === this.sessionSecret) {
1011
+ client.authenticated = true;
1012
+ if (this.session) {
1013
+ this.session.connectedDevices.push(client.device);
1014
+ this.session.status = "connected";
1015
+ }
1016
+ client.ws.send(
1017
+ JSON.stringify({
1018
+ type: MessageTypes.AUTH_SUCCESS,
1019
+ payload: {
1020
+ sessionId: this.session?.id
1021
+ },
1022
+ timestamp: Date.now()
1023
+ })
1024
+ );
1025
+ this.emit("client:connected", client.device);
1026
+ } else {
1027
+ client.ws.send(JSON.stringify({ type: MessageTypes.AUTH_FAILED, timestamp: Date.now() }));
1028
+ setTimeout(() => client.ws.close(1008, "Authentication failed"), 100);
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Handle HTTP request
1033
+ */
1034
+ handleHttpRequest(req, res) {
1035
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
1036
+ const path = url.pathname;
1037
+ res.setHeader("Access-Control-Allow-Origin", "*");
1038
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
1039
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1040
+ if (req.method === "OPTIONS") {
1041
+ res.writeHead(204);
1042
+ res.end();
1043
+ return;
1044
+ }
1045
+ if (path === "/" || path === "/index.html") {
1046
+ const { getWebClient: getWebClient2 } = (init_web_client(), __toCommonJS(web_client_exports));
1047
+ res.writeHead(200, {
1048
+ "Content-Type": "text/html; charset=utf-8",
1049
+ "Content-Security-Policy": "default-src 'self' 'unsafe-inline' 'unsafe-eval' https: data: ws: wss:"
1050
+ });
1051
+ res.end(getWebClient2());
1052
+ return;
1053
+ }
1054
+ if (path === "/health") {
1055
+ res.writeHead(200, { "Content-Type": "application/json" });
1056
+ res.end(JSON.stringify({ status: "ok", session: this.session?.id }));
1057
+ return;
1058
+ }
1059
+ if (path === "/api/session") {
1060
+ res.writeHead(200, { "Content-Type": "application/json" });
1061
+ res.end(
1062
+ JSON.stringify({
1063
+ id: this.session?.id,
1064
+ name: this.session?.name,
1065
+ status: this.session?.status,
1066
+ connectedDevices: this.session?.connectedDevices.length
1067
+ })
1068
+ );
1069
+ return;
1070
+ }
1071
+ res.writeHead(404, { "Content-Type": "text/plain" });
1072
+ res.end("Not Found");
1073
+ }
1074
+ /**
1075
+ * Start heartbeat
1076
+ */
1077
+ startHeartbeat() {
1078
+ this.heartbeatTimer = setInterval(() => {
1079
+ const now = Date.now();
1080
+ for (const [id, client] of this.clients) {
1081
+ if (now - client.lastPing > this.config.heartbeatInterval * 2) {
1082
+ client.ws.terminate();
1083
+ this.clients.delete(id);
1084
+ } else if (client.ws.readyState === WebSocket.OPEN) {
1085
+ client.ws.ping();
1086
+ }
1087
+ }
1088
+ }, this.config.heartbeatInterval);
1089
+ }
1090
+ /**
1091
+ * Start session timeout
1092
+ */
1093
+ startSessionTimeout() {
1094
+ this.sessionTimeoutTimer = setTimeout(() => {
1095
+ if (this.session?.connectedDevices.length === 0) {
1096
+ this.stop();
1097
+ }
1098
+ }, this.config.sessionTimeout);
1099
+ }
1100
+ /**
1101
+ * Reset session timeout
1102
+ */
1103
+ resetSessionTimeout() {
1104
+ if (this.sessionTimeoutTimer) {
1105
+ clearTimeout(this.sessionTimeoutTimer);
1106
+ }
1107
+ if (this.config.sessionTimeout > 0) {
1108
+ this.startSessionTimeout();
1109
+ }
1110
+ }
1111
+ /**
1112
+ * Get local IP
1113
+ */
1114
+ getLocalIP() {
1115
+ const interfaces = os.networkInterfaces();
1116
+ for (const name of Object.keys(interfaces)) {
1117
+ for (const iface of interfaces[name] || []) {
1118
+ if (iface.family === "IPv4" && !iface.internal) {
1119
+ return iface.address;
1120
+ }
1121
+ }
1122
+ }
1123
+ return "127.0.0.1";
1124
+ }
1125
+ /**
1126
+ * Generate session ID
1127
+ */
1128
+ generateSessionId() {
1129
+ return crypto.randomBytes(4).toString("hex");
1130
+ }
1131
+ /**
1132
+ * Generate client ID
1133
+ */
1134
+ generateClientId() {
1135
+ return "c_" + crypto.randomBytes(4).toString("hex");
1136
+ }
1137
+ };
1138
+
1139
+ export {
1140
+ DEFAULT_CONFIG,
1141
+ MessageTypes,
1142
+ getWebClient,
1143
+ init_web_client,
1144
+ RemoteServer
1145
+ };