orc-server 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/__tests__/auth.test.js +49 -0
  2. package/dist/__tests__/watcher.test.js +58 -0
  3. package/dist/claude/chatService.js +175 -0
  4. package/dist/claude/sessionBrowser.js +743 -0
  5. package/dist/claude/watcher.js +242 -0
  6. package/dist/config.js +44 -0
  7. package/dist/files/browser.js +227 -0
  8. package/dist/files/reader.js +159 -0
  9. package/dist/files/search.js +124 -0
  10. package/dist/git/gitHandler.js +177 -0
  11. package/dist/git/gitService.js +299 -0
  12. package/dist/git/index.js +8 -0
  13. package/dist/http/server.js +96 -0
  14. package/dist/index.js +77 -0
  15. package/dist/ssh/index.js +9 -0
  16. package/dist/ssh/sshHandler.js +205 -0
  17. package/dist/ssh/sshManager.js +329 -0
  18. package/dist/terminal/index.js +11 -0
  19. package/dist/terminal/localTerminalHandler.js +176 -0
  20. package/dist/terminal/localTerminalManager.js +497 -0
  21. package/dist/terminal/terminalWebSocket.js +136 -0
  22. package/dist/types.js +2 -0
  23. package/dist/utils/logger.js +42 -0
  24. package/dist/websocket/auth.js +18 -0
  25. package/dist/websocket/server.js +631 -0
  26. package/package.json +66 -0
  27. package/web-dist/assets/highlight-l0sNRNKZ.js +1 -0
  28. package/web-dist/assets/index-C8TJGN-T.css +41 -0
  29. package/web-dist/assets/index-DjLLxjMD.js +39 -0
  30. package/web-dist/assets/markdown-C_j0ZeeY.js +51 -0
  31. package/web-dist/assets/react-vendor-CqP5oCk4.js +9 -0
  32. package/web-dist/assets/xterm-BCk906R6.js +9 -0
  33. package/web-dist/icon-192.png +0 -0
  34. package/web-dist/icon-512.png +0 -0
  35. package/web-dist/index.html +23 -0
  36. package/web-dist/manifest.json +24 -0
  37. package/web-dist/sw.js +35 -0
@@ -0,0 +1,497 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.LocalTerminalConnection = exports.localTerminalManager = exports.ZellijTerminalConnection = void 0;
37
+ const pty = __importStar(require("node-pty"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const fs = __importStar(require("fs"));
41
+ const child_process_1 = require("child_process");
42
+ const logger_1 = require("../utils/logger");
43
+ /**
44
+ * Find the full path to a command
45
+ */
46
+ function findCommand(cmd) {
47
+ const home = process.env.HOME || '';
48
+ const additionalPaths = [
49
+ path.join(home, '.local', 'bin'),
50
+ path.join(home, '.npm-global', 'bin'),
51
+ path.join(home, 'bin'),
52
+ '/usr/local/bin',
53
+ '/opt/homebrew/bin',
54
+ ];
55
+ for (const dir of additionalPaths) {
56
+ const fullPath = path.join(dir, cmd);
57
+ if (fs.existsSync(fullPath)) {
58
+ return fullPath;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Get extended PATH
65
+ */
66
+ function getExtendedPath() {
67
+ const home = process.env.HOME || '';
68
+ const currentPath = process.env.PATH || '';
69
+ const additionalPaths = [
70
+ path.join(home, '.local', 'bin'),
71
+ path.join(home, '.npm-global', 'bin'),
72
+ path.join(home, 'bin'),
73
+ '/opt/homebrew/bin',
74
+ '/usr/local/bin',
75
+ ];
76
+ return [...additionalPaths, currentPath].join(':');
77
+ }
78
+ /**
79
+ * Check if zellij is available
80
+ */
81
+ function isZellijAvailable() {
82
+ try {
83
+ (0, child_process_1.execSync)('which zellij', { stdio: 'ignore' });
84
+ return true;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ /**
91
+ * Strip ANSI escape codes from a string
92
+ */
93
+ function stripAnsi(str) {
94
+ // eslint-disable-next-line no-control-regex
95
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
96
+ }
97
+ /**
98
+ * List existing zellij sessions (names only, all sessions)
99
+ */
100
+ function listZellijSessions() {
101
+ try {
102
+ const output = (0, child_process_1.execSync)('zellij list-sessions -s 2>/dev/null || true', {
103
+ encoding: 'utf-8',
104
+ env: { ...process.env, PATH: getExtendedPath() },
105
+ });
106
+ return output.trim().split('\n').map(stripAnsi).filter(s => s.length > 0);
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ }
112
+ /**
113
+ * List alive (non-EXITED) zellij sessions
114
+ * Parses full `zellij list-sessions` output to check status
115
+ */
116
+ function listAliveZellijSessions() {
117
+ try {
118
+ const output = (0, child_process_1.execSync)('zellij list-sessions 2>/dev/null || true', {
119
+ encoding: 'utf-8',
120
+ env: { ...process.env, PATH: getExtendedPath() },
121
+ });
122
+ const lines = output.trim().split('\n').filter(s => s.length > 0);
123
+ return lines
124
+ .map(line => stripAnsi(line))
125
+ .filter(line => !line.includes('(EXITED'))
126
+ .map(line => line.split(' ')[0])
127
+ .filter(name => name.length > 0);
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ }
133
+ /**
134
+ * Kill a zellij session
135
+ */
136
+ function killZellijSession(sessionName) {
137
+ try {
138
+ (0, child_process_1.execSync)(`zellij kill-session ${sessionName} 2>/dev/null || true`, {
139
+ env: { ...process.env, PATH: getExtendedPath() },
140
+ });
141
+ return true;
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
147
+ /**
148
+ * Manages zellij-backed terminal sessions for a client
149
+ */
150
+ class ZellijTerminalConnection {
151
+ constructor(clientId) {
152
+ this.sessions = new Map();
153
+ this.dataCallbacks = new Map();
154
+ this.closeCallbacks = new Map();
155
+ this.clientId = clientId;
156
+ this.zellijAvailable = isZellijAvailable();
157
+ if (this.zellijAvailable) {
158
+ logger_1.logger.info('Zellij is available, using zellij-backed sessions');
159
+ }
160
+ else {
161
+ logger_1.logger.warn('Zellij not found, falling back to plain PTY');
162
+ }
163
+ }
164
+ /**
165
+ * Generate a unique zellij session name
166
+ */
167
+ generateSessionName(sessionId, type) {
168
+ // Use a prefix to identify our sessions
169
+ return `orc-${type}-${sessionId.replace(/[^a-zA-Z0-9]/g, '-')}`;
170
+ }
171
+ /**
172
+ * Check if a session exists
173
+ */
174
+ hasTerminal(sessionId) {
175
+ return this.sessions.has(sessionId);
176
+ }
177
+ /**
178
+ * Rebind callbacks for reconnection
179
+ */
180
+ rebindCallbacks(sessionId, onData, onClose) {
181
+ const session = this.sessions.get(sessionId);
182
+ if (!session) {
183
+ return false;
184
+ }
185
+ this.dataCallbacks.set(sessionId, onData);
186
+ this.closeCallbacks.set(sessionId, onClose);
187
+ // If PTY process exists and is running, we're good
188
+ if (session.ptyProcess) {
189
+ logger_1.logger.info(`Rebound callbacks for session ${sessionId}`);
190
+ return true;
191
+ }
192
+ // PTY was closed but zellij session might still exist - reattach
193
+ if (this.zellijAvailable) {
194
+ const zellijName = this.generateSessionName(sessionId, session.type);
195
+ const existingSessions = listZellijSessions();
196
+ if (existingSessions.includes(zellijName)) {
197
+ logger_1.logger.info(`Reattaching to existing zellij session ${zellijName}`);
198
+ return this.attachToZellijSession(sessionId, session.type, onData, onClose);
199
+ }
200
+ }
201
+ return false;
202
+ }
203
+ /**
204
+ * Attach to an existing or new zellij session
205
+ */
206
+ attachToZellijSession(sessionId, type, onData, onClose, options) {
207
+ const zellijName = this.generateSessionName(sessionId, type);
208
+ const cols = options?.cols || 80;
209
+ const rows = options?.rows || 24;
210
+ const cwd = options?.cwd || process.env.HOME || process.cwd();
211
+ try {
212
+ // zellij attach -c will create if not exists
213
+ const ptyProcess = pty.spawn('zellij', ['attach', '-c', zellijName], {
214
+ name: 'xterm-256color',
215
+ cols,
216
+ rows,
217
+ cwd,
218
+ env: {
219
+ ...process.env,
220
+ PATH: getExtendedPath(),
221
+ TERM: 'xterm-256color',
222
+ COLORTERM: 'truecolor',
223
+ },
224
+ });
225
+ this.setupPtyHandlers(sessionId, ptyProcess, onData, onClose);
226
+ const session = this.sessions.get(sessionId);
227
+ if (session) {
228
+ session.ptyProcess = ptyProcess;
229
+ }
230
+ else {
231
+ this.sessions.set(sessionId, {
232
+ name: zellijName,
233
+ type,
234
+ createdAt: Date.now(),
235
+ ptyProcess,
236
+ });
237
+ }
238
+ logger_1.logger.info(`Attached to zellij session ${zellijName} (${cols}x${rows})`);
239
+ return true;
240
+ }
241
+ catch (error) {
242
+ logger_1.logger.error(`Failed to attach to zellij session ${zellijName}:`, error);
243
+ return false;
244
+ }
245
+ }
246
+ /**
247
+ * Setup PTY event handlers
248
+ */
249
+ setupPtyHandlers(sessionId, ptyProcess, onData, onClose) {
250
+ this.dataCallbacks.set(sessionId, onData);
251
+ this.closeCallbacks.set(sessionId, onClose);
252
+ ptyProcess.onData((data) => {
253
+ const callback = this.dataCallbacks.get(sessionId);
254
+ if (callback) {
255
+ callback(data);
256
+ }
257
+ });
258
+ ptyProcess.onExit(({ exitCode, signal }) => {
259
+ logger_1.logger.info(`PTY for session ${sessionId} exited with code ${exitCode}, signal ${signal}`);
260
+ const session = this.sessions.get(sessionId);
261
+ if (session) {
262
+ session.ptyProcess = null;
263
+ }
264
+ // Note: Don't remove the session - zellij session is still running
265
+ // Only call close callback if client needs to know
266
+ const callback = this.closeCallbacks.get(sessionId);
267
+ if (callback) {
268
+ callback();
269
+ }
270
+ });
271
+ }
272
+ /**
273
+ * Start a new terminal session
274
+ */
275
+ startTerminal(sessionId, onData, onClose, options = { type: 'shell' }) {
276
+ // Check if session already exists
277
+ if (this.sessions.has(sessionId)) {
278
+ return this.rebindCallbacks(sessionId, onData, onClose);
279
+ }
280
+ const type = options.type;
281
+ if (this.zellijAvailable) {
282
+ // Create zellij session
283
+ const zellijName = this.generateSessionName(sessionId, type);
284
+ // Check if this zellij session already exists before attaching
285
+ // (handles reconnect case where zellij session persists across WebSocket disconnects)
286
+ const existingSessions = listZellijSessions();
287
+ const isExistingSession = existingSessions.includes(zellijName);
288
+ this.sessions.set(sessionId, {
289
+ name: zellijName,
290
+ type,
291
+ createdAt: Date.now(),
292
+ ptyProcess: null,
293
+ });
294
+ // Attach to zellij session
295
+ const attached = this.attachToZellijSession(sessionId, type, onData, onClose, options);
296
+ if (!attached) {
297
+ this.sessions.delete(sessionId);
298
+ return false;
299
+ }
300
+ // If claude type and this is a NEW session (not a reconnect), run claude command
301
+ if (type === 'claude' && !isExistingSession) {
302
+ setTimeout(() => {
303
+ const claudePath = findCommand('claude') || 'claude';
304
+ const args = options.claudeArgs?.join(' ') || '';
305
+ this.writeToTerminal(sessionId, `${claudePath} ${args}\n`);
306
+ }, 500);
307
+ }
308
+ return true;
309
+ }
310
+ else {
311
+ // Fallback to plain PTY
312
+ return this.startPlainPty(sessionId, onData, onClose, options);
313
+ }
314
+ }
315
+ /**
316
+ * Fallback: start plain PTY without zellij
317
+ */
318
+ startPlainPty(sessionId, onData, onClose, options) {
319
+ const cols = options.cols || 80;
320
+ const rows = options.rows || 24;
321
+ const cwd = options.cwd || process.env.HOME || process.cwd();
322
+ let command;
323
+ let args;
324
+ if (options.type === 'claude') {
325
+ const claudePath = findCommand('claude');
326
+ command = claudePath || 'claude';
327
+ args = options.claudeArgs || [];
328
+ }
329
+ else {
330
+ command = process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : 'bash');
331
+ args = [];
332
+ }
333
+ try {
334
+ const ptyProcess = pty.spawn(command, args, {
335
+ name: 'xterm-256color',
336
+ cols,
337
+ rows,
338
+ cwd,
339
+ env: {
340
+ ...process.env,
341
+ PATH: getExtendedPath(),
342
+ TERM: 'xterm-256color',
343
+ COLORTERM: 'truecolor',
344
+ },
345
+ });
346
+ this.sessions.set(sessionId, {
347
+ name: sessionId,
348
+ type: options.type,
349
+ createdAt: Date.now(),
350
+ ptyProcess,
351
+ });
352
+ this.setupPtyHandlers(sessionId, ptyProcess, onData, onClose);
353
+ logger_1.logger.info(`Started plain PTY ${options.type} session ${sessionId} (${cols}x${rows})`);
354
+ return true;
355
+ }
356
+ catch (error) {
357
+ logger_1.logger.error(`Failed to start plain PTY ${sessionId}:`, error);
358
+ return false;
359
+ }
360
+ }
361
+ /**
362
+ * Write to terminal
363
+ */
364
+ writeToTerminal(sessionId, data) {
365
+ const session = this.sessions.get(sessionId);
366
+ if (!session || !session.ptyProcess) {
367
+ logger_1.logger.warn(`Terminal ${sessionId} not found or not attached`);
368
+ return false;
369
+ }
370
+ session.ptyProcess.write(data);
371
+ return true;
372
+ }
373
+ /**
374
+ * Resize terminal
375
+ */
376
+ resizeTerminal(sessionId, cols, rows) {
377
+ const session = this.sessions.get(sessionId);
378
+ if (!session || !session.ptyProcess) {
379
+ logger_1.logger.warn(`Terminal ${sessionId} not found for resize`);
380
+ return false;
381
+ }
382
+ session.ptyProcess.resize(cols, rows);
383
+ logger_1.logger.debug(`Resized terminal ${sessionId} to ${cols}x${rows}`);
384
+ return true;
385
+ }
386
+ /**
387
+ * Close terminal (detach from zellij, but don't kill the session)
388
+ */
389
+ closeTerminal(sessionId, killSession = false) {
390
+ const session = this.sessions.get(sessionId);
391
+ if (!session) {
392
+ logger_1.logger.warn(`Terminal ${sessionId} not found for close`);
393
+ return false;
394
+ }
395
+ // Kill the PTY (detach from zellij)
396
+ if (session.ptyProcess) {
397
+ session.ptyProcess.kill();
398
+ session.ptyProcess = null;
399
+ }
400
+ // Optionally kill the zellij session too
401
+ if (killSession && this.zellijAvailable) {
402
+ killZellijSession(session.name);
403
+ logger_1.logger.info(`Killed zellij session ${session.name}`);
404
+ }
405
+ this.sessions.delete(sessionId);
406
+ this.dataCallbacks.delete(sessionId);
407
+ this.closeCallbacks.delete(sessionId);
408
+ logger_1.logger.info(`Closed terminal ${sessionId}${killSession ? ' (session killed)' : ' (session preserved)'}`);
409
+ return true;
410
+ }
411
+ /**
412
+ * Get list of active terminals
413
+ */
414
+ getActiveTerminals() {
415
+ return Array.from(this.sessions.keys());
416
+ }
417
+ /**
418
+ * Get terminal info
419
+ */
420
+ getTerminalInfo(sessionId) {
421
+ const session = this.sessions.get(sessionId);
422
+ if (!session)
423
+ return null;
424
+ return {
425
+ type: session.type,
426
+ createdAt: session.createdAt,
427
+ zellijSession: this.zellijAvailable ? session.name : undefined,
428
+ };
429
+ }
430
+ /**
431
+ * Close all terminals
432
+ */
433
+ closeAll(killSessions = false) {
434
+ for (const [sessionId] of this.sessions) {
435
+ this.closeTerminal(sessionId, killSessions);
436
+ }
437
+ }
438
+ /**
439
+ * List alive zellij sessions managed by this server
440
+ */
441
+ static listManagedSessions() {
442
+ return listAliveZellijSessions().filter(s => s.startsWith('orc-'));
443
+ }
444
+ /**
445
+ * Kill a zellij session by sessionId (without needing a connection)
446
+ * Tries both shell and claude session names
447
+ */
448
+ static killSessionById(sessionId) {
449
+ const sanitized = sessionId.replace(/[^a-zA-Z0-9]/g, '-');
450
+ const shellName = `orc-shell-${sanitized}`;
451
+ const claudeName = `orc-claude-${sanitized}`;
452
+ let killed = false;
453
+ const existingSessions = listZellijSessions();
454
+ if (existingSessions.includes(shellName)) {
455
+ killed = killZellijSession(shellName) || killed;
456
+ }
457
+ if (existingSessions.includes(claudeName)) {
458
+ killed = killZellijSession(claudeName) || killed;
459
+ }
460
+ return killed;
461
+ }
462
+ }
463
+ exports.ZellijTerminalConnection = ZellijTerminalConnection;
464
+ exports.LocalTerminalConnection = ZellijTerminalConnection;
465
+ /**
466
+ * Manager for all client connections
467
+ */
468
+ class ZellijTerminalManager {
469
+ constructor() {
470
+ this.connections = new Map();
471
+ }
472
+ getConnection(clientId) {
473
+ return this.connections.get(clientId);
474
+ }
475
+ getOrCreateConnection(clientId) {
476
+ let connection = this.connections.get(clientId);
477
+ if (!connection) {
478
+ connection = new ZellijTerminalConnection(clientId);
479
+ this.connections.set(clientId, connection);
480
+ logger_1.logger.info(`Created terminal connection for client ${clientId}`);
481
+ }
482
+ return connection;
483
+ }
484
+ removeConnection(clientId, killSessions = false) {
485
+ const connection = this.connections.get(clientId);
486
+ if (connection) {
487
+ connection.closeAll(killSessions);
488
+ this.connections.delete(clientId);
489
+ logger_1.logger.info(`Removed terminal connection for client ${clientId}`);
490
+ }
491
+ }
492
+ getConnectionCount() {
493
+ return this.connections.size;
494
+ }
495
+ }
496
+ // Export with the same interface for compatibility
497
+ exports.localTerminalManager = new ZellijTerminalManager();
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.terminalWebSocketHandler = exports.TerminalWebSocketHandler = void 0;
7
+ const ws_1 = __importDefault(require("ws"));
8
+ const url_1 = require("url");
9
+ const localTerminalManager_1 = require("./localTerminalManager");
10
+ const auth_1 = require("../websocket/auth");
11
+ const logger_1 = require("../utils/logger");
12
+ class TerminalWebSocketHandler {
13
+ constructor() {
14
+ // Map from WebSocket to sessionId for cleanup
15
+ this.wsToSession = new Map();
16
+ this.authManager = new auth_1.AuthManager();
17
+ }
18
+ /**
19
+ * Check if a request should be handled by this handler
20
+ */
21
+ shouldHandle(request) {
22
+ try {
23
+ const url = new url_1.URL(request.url || '', `http://${request.headers.host}`);
24
+ return url.pathname === '/terminal';
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Handle a new terminal WebSocket connection
32
+ */
33
+ handleConnection(ws, request) {
34
+ try {
35
+ const url = new url_1.URL(request.url || '', `http://${request.headers.host}`);
36
+ const token = url.searchParams.get('token') || '';
37
+ const sessionId = url.searchParams.get('sessionId');
38
+ const type = (url.searchParams.get('type') || 'shell');
39
+ const cols = parseInt(url.searchParams.get('cols') || '80', 10);
40
+ const rows = parseInt(url.searchParams.get('rows') || '24', 10);
41
+ const claudeArgsParam = url.searchParams.get('claudeArgs');
42
+ const cwd = url.searchParams.get('cwd') || undefined;
43
+ // Validate token
44
+ if (!this.authManager.validateToken(token)) {
45
+ logger_1.logger.warn('Terminal WebSocket: invalid token');
46
+ ws.close(4001, 'Invalid token');
47
+ return;
48
+ }
49
+ if (!sessionId) {
50
+ logger_1.logger.warn('Terminal WebSocket: missing sessionId');
51
+ ws.close(4002, 'Missing sessionId');
52
+ return;
53
+ }
54
+ // Generate a unique client ID for this connection
55
+ const clientId = this.authManager.generateClientId();
56
+ this.wsToSession.set(ws, { clientId, sessionId });
57
+ logger_1.logger.info(`Terminal WebSocket connected: clientId=${clientId}, sessionId=${sessionId}, type=${type}, ${cols}x${rows}`);
58
+ const connection = localTerminalManager_1.localTerminalManager.getOrCreateConnection(clientId);
59
+ const options = {
60
+ type,
61
+ cols,
62
+ rows,
63
+ cwd,
64
+ claudeArgs: claudeArgsParam ? JSON.parse(decodeURIComponent(claudeArgsParam)) : undefined,
65
+ };
66
+ const success = connection.startTerminal(sessionId, (data) => {
67
+ // Send terminal output as text frame
68
+ if (ws.readyState === ws_1.default.OPEN) {
69
+ ws.send(data);
70
+ }
71
+ }, () => {
72
+ // Terminal closed
73
+ if (ws.readyState === ws_1.default.OPEN) {
74
+ ws.close(1000, 'Terminal closed');
75
+ }
76
+ }, options);
77
+ if (!success) {
78
+ logger_1.logger.error(`Failed to start terminal: sessionId=${sessionId}`);
79
+ ws.close(4003, 'Failed to start terminal');
80
+ return;
81
+ }
82
+ // Handle incoming messages
83
+ ws.on('message', (data, isBinary) => {
84
+ if (isBinary) {
85
+ // Binary frame = control message
86
+ this.handleControlMessage(ws, clientId, sessionId, data);
87
+ }
88
+ else {
89
+ // Text frame = terminal input
90
+ connection.writeToTerminal(sessionId, data.toString());
91
+ }
92
+ });
93
+ ws.on('close', () => {
94
+ logger_1.logger.info(`Terminal WebSocket closed: clientId=${clientId}, sessionId=${sessionId}`);
95
+ // Detach but don't kill the zellij session
96
+ connection.closeTerminal(sessionId, false);
97
+ localTerminalManager_1.localTerminalManager.removeConnection(clientId, false);
98
+ this.wsToSession.delete(ws);
99
+ });
100
+ ws.on('error', (error) => {
101
+ logger_1.logger.error(`Terminal WebSocket error: ${error.message}`);
102
+ });
103
+ }
104
+ catch (error) {
105
+ logger_1.logger.error('Terminal WebSocket connection error:', error);
106
+ ws.close(4000, 'Connection error');
107
+ }
108
+ }
109
+ handleControlMessage(ws, clientId, sessionId, data) {
110
+ try {
111
+ const message = JSON.parse(data.toString());
112
+ switch (message.type) {
113
+ case 'resize':
114
+ if (message.cols && message.rows) {
115
+ const connection = localTerminalManager_1.localTerminalManager.getConnection(clientId);
116
+ if (connection) {
117
+ connection.resizeTerminal(sessionId, message.cols, message.rows);
118
+ logger_1.logger.debug(`Terminal resized: ${sessionId} -> ${message.cols}x${message.rows}`);
119
+ }
120
+ }
121
+ break;
122
+ case 'ping':
123
+ // Respond with pong as binary frame
124
+ ws.send(Buffer.from(JSON.stringify({ type: 'pong' })));
125
+ break;
126
+ default:
127
+ logger_1.logger.warn(`Unknown control message type: ${message.type}`);
128
+ }
129
+ }
130
+ catch (error) {
131
+ logger_1.logger.error('Failed to parse control message:', error);
132
+ }
133
+ }
134
+ }
135
+ exports.TerminalWebSocketHandler = TerminalWebSocketHandler;
136
+ exports.terminalWebSocketHandler = new TerminalWebSocketHandler();
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logger = void 0;
4
+ const config_1 = require("../config");
5
+ var LogLevel;
6
+ (function (LogLevel) {
7
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
8
+ LogLevel[LogLevel["INFO"] = 1] = "INFO";
9
+ LogLevel[LogLevel["WARN"] = 2] = "WARN";
10
+ LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
11
+ })(LogLevel || (LogLevel = {}));
12
+ const LEVEL_MAP = {
13
+ debug: LogLevel.DEBUG,
14
+ info: LogLevel.INFO,
15
+ warn: LogLevel.WARN,
16
+ error: LogLevel.ERROR,
17
+ };
18
+ class Logger {
19
+ constructor() {
20
+ this.level = LEVEL_MAP[config_1.CONFIG.logLevel] || LogLevel.INFO;
21
+ }
22
+ log(level, message, ...args) {
23
+ if (level >= this.level) {
24
+ const timestamp = new Date().toISOString();
25
+ const levelName = LogLevel[level];
26
+ console.log(`[${timestamp}] [${levelName}]`, message, ...args);
27
+ }
28
+ }
29
+ debug(message, ...args) {
30
+ this.log(LogLevel.DEBUG, message, ...args);
31
+ }
32
+ info(message, ...args) {
33
+ this.log(LogLevel.INFO, message, ...args);
34
+ }
35
+ warn(message, ...args) {
36
+ this.log(LogLevel.WARN, message, ...args);
37
+ }
38
+ error(message, ...args) {
39
+ this.log(LogLevel.ERROR, message, ...args);
40
+ }
41
+ }
42
+ exports.logger = new Logger();
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthManager = void 0;
4
+ const config_1 = require("../config");
5
+ const logger_1 = require("../utils/logger");
6
+ class AuthManager {
7
+ validateToken(token) {
8
+ if (!config_1.CONFIG.authToken) {
9
+ logger_1.logger.warn('No AUTH_TOKEN configured, accepting all connections');
10
+ return true;
11
+ }
12
+ return token === config_1.CONFIG.authToken;
13
+ }
14
+ generateClientId() {
15
+ return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
16
+ }
17
+ }
18
+ exports.AuthManager = AuthManager;