sessioncast-cli 2.3.0 → 2.3.1
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/dist/agent/browser-handler.d.ts +100 -0
- package/dist/agent/browser-handler.js +485 -0
- package/dist/agent/cdp-client.d.ts +28 -0
- package/dist/agent/cdp-client.js +115 -0
- package/dist/agent/chrome-finder.d.ts +5 -0
- package/dist/agent/chrome-finder.js +100 -0
- package/dist/agent/crypto.d.ts +2 -0
- package/dist/agent/crypto.js +24 -0
- package/dist/agent/runner.d.ts +7 -1
- package/dist/agent/runner.js +60 -2
- package/dist/agent/session-handler.js +2 -1
- package/dist/agent/tunnel-handler.d.ts +12 -0
- package/dist/agent/tunnel-handler.js +180 -0
- package/dist/agent/types.d.ts +21 -0
- package/dist/agent/websocket.d.ts +18 -0
- package/dist/agent/websocket.js +113 -15
- package/dist/commands/agent.js +1 -1
- package/dist/commands/tunnel.d.ts +10 -0
- package/dist/commands/tunnel.js +201 -0
- package/dist/index.js +23 -0
- package/package.json +3 -2
package/dist/agent/websocket.js
CHANGED
|
@@ -40,6 +40,8 @@ exports.RelayWebSocketClient = void 0;
|
|
|
40
40
|
const ws_1 = __importDefault(require("ws"));
|
|
41
41
|
const events_1 = require("events");
|
|
42
42
|
const zlib = __importStar(require("zlib"));
|
|
43
|
+
const zstd = __importStar(require("zstd-napi"));
|
|
44
|
+
const crypto_1 = require("./crypto");
|
|
43
45
|
const debug_1 = require("./debug");
|
|
44
46
|
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
45
47
|
const BASE_RECONNECT_DELAY_MS = 2000;
|
|
@@ -49,6 +51,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
49
51
|
constructor(options) {
|
|
50
52
|
super();
|
|
51
53
|
this.ws = null;
|
|
54
|
+
this.encKeyBuf = null;
|
|
52
55
|
this.isConnected = false;
|
|
53
56
|
this.reconnectAttempts = 0;
|
|
54
57
|
this.circuitBreakerOpen = false;
|
|
@@ -61,6 +64,10 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
61
64
|
this.token = options.token;
|
|
62
65
|
this.label = options.label || options.sessionId;
|
|
63
66
|
this.autoReconnect = options.autoReconnect ?? true;
|
|
67
|
+
if (options.encKey) {
|
|
68
|
+
this.encKeyBuf = Buffer.from(options.encKey, 'base64url');
|
|
69
|
+
}
|
|
70
|
+
this.skipAutoRegister = options.skipAutoRegister ?? false;
|
|
64
71
|
}
|
|
65
72
|
connect() {
|
|
66
73
|
if (this.destroyed)
|
|
@@ -72,7 +79,9 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
72
79
|
this.reconnectAttempts = 0;
|
|
73
80
|
this.circuitBreakerOpen = false;
|
|
74
81
|
this.emit('connected');
|
|
75
|
-
this.
|
|
82
|
+
if (!this.skipAutoRegister) {
|
|
83
|
+
this.registerAsHost();
|
|
84
|
+
}
|
|
76
85
|
});
|
|
77
86
|
this.ws.on('message', (data) => {
|
|
78
87
|
try {
|
|
@@ -116,9 +125,28 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
116
125
|
meta
|
|
117
126
|
});
|
|
118
127
|
}
|
|
128
|
+
decryptPayload(payload) {
|
|
129
|
+
if (!this.encKeyBuf)
|
|
130
|
+
return payload;
|
|
131
|
+
try {
|
|
132
|
+
const buf = Buffer.from(payload, 'base64');
|
|
133
|
+
const decrypted = (0, crypto_1.decrypt)(buf, this.encKeyBuf);
|
|
134
|
+
return decrypted.toString('utf-8');
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// If decryption fails, return as-is (unencrypted message)
|
|
138
|
+
return payload;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
119
141
|
handleMessage(message) {
|
|
120
142
|
(0, debug_1.debugLog)('WS:IN', message.type, message.session);
|
|
121
143
|
switch (message.type) {
|
|
144
|
+
case 'keysEnc':
|
|
145
|
+
if (message.session === this.sessionId && message.payload) {
|
|
146
|
+
const keys = this.decryptPayload(message.payload);
|
|
147
|
+
this.emit('keys', keys, message.meta?.pane);
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
122
150
|
case 'keys':
|
|
123
151
|
if (message.session === this.sessionId && message.payload) {
|
|
124
152
|
this.emit('keys', message.payload, message.meta?.pane);
|
|
@@ -148,6 +176,25 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
148
176
|
this.emit('createWorktree', message.meta.branch);
|
|
149
177
|
}
|
|
150
178
|
break;
|
|
179
|
+
case 'webInput':
|
|
180
|
+
if (message.session === this.sessionId && message.payload) {
|
|
181
|
+
try {
|
|
182
|
+
const inputEvent = JSON.parse(message.payload);
|
|
183
|
+
this.emit('webInput', inputEvent);
|
|
184
|
+
}
|
|
185
|
+
catch { /* ignore parse errors */ }
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
case 'webNavigate':
|
|
189
|
+
if (message.session === this.sessionId && message.meta?.url) {
|
|
190
|
+
this.emit('webNavigate', message.meta.url);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case 'requestSnapshot':
|
|
194
|
+
if (message.session === this.sessionId) {
|
|
195
|
+
this.emit('requestSnapshot');
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
151
198
|
case 'requestFileView':
|
|
152
199
|
if (message.session === this.sessionId && message.meta?.filePath) {
|
|
153
200
|
this.emit('requestFileView', message.meta.filePath);
|
|
@@ -268,17 +315,37 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
268
315
|
meta
|
|
269
316
|
});
|
|
270
317
|
}
|
|
318
|
+
compressAndEncrypt(data) {
|
|
319
|
+
// Step 1: Compress with zstd (fallback to gzip)
|
|
320
|
+
let compressed;
|
|
321
|
+
let compressType = 'zstd';
|
|
322
|
+
try {
|
|
323
|
+
compressed = zstd.compress(data, { compressionLevel: 3 });
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
compressed = zlib.gzipSync(data);
|
|
327
|
+
compressType = 'gz';
|
|
328
|
+
}
|
|
329
|
+
// Step 2: Encrypt if encKey is available
|
|
330
|
+
if (this.encKeyBuf) {
|
|
331
|
+
const encrypted = (0, crypto_1.encrypt)(compressed, this.encKeyBuf);
|
|
332
|
+
return {
|
|
333
|
+
payload: encrypted.toString('base64'),
|
|
334
|
+
type: 'screenEnc'
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// No encryption — send compressed only
|
|
338
|
+
return {
|
|
339
|
+
payload: compressed.toString('base64'),
|
|
340
|
+
type: compressType === 'zstd' ? 'screenZstd' : 'screenGz'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
271
343
|
sendScreenCompressed(data) {
|
|
272
344
|
if (!this.isConnected)
|
|
273
345
|
return false;
|
|
274
346
|
try {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
return this.send({
|
|
278
|
-
type: 'screenGz',
|
|
279
|
-
session: this.sessionId,
|
|
280
|
-
payload: base64Data
|
|
281
|
-
});
|
|
347
|
+
const { payload, type } = this.compressAndEncrypt(data);
|
|
348
|
+
return this.send({ type, session: this.sessionId, payload });
|
|
282
349
|
}
|
|
283
350
|
catch {
|
|
284
351
|
return this.sendScreen(data);
|
|
@@ -288,13 +355,8 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
288
355
|
if (!this.isConnected)
|
|
289
356
|
return false;
|
|
290
357
|
try {
|
|
291
|
-
const
|
|
292
|
-
return this.send({
|
|
293
|
-
type: 'screenGz',
|
|
294
|
-
session: this.sessionId,
|
|
295
|
-
payload: compressed.toString('base64'),
|
|
296
|
-
meta
|
|
297
|
-
});
|
|
358
|
+
const { payload, type } = this.compressAndEncrypt(data);
|
|
359
|
+
return this.send({ type, session: this.sessionId, payload, meta });
|
|
298
360
|
}
|
|
299
361
|
catch {
|
|
300
362
|
return this.sendScreenWithMeta(data, meta);
|
|
@@ -370,6 +432,42 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
370
432
|
}
|
|
371
433
|
});
|
|
372
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Send a single rrweb DOM event to the relay.
|
|
437
|
+
*/
|
|
438
|
+
sendWebDom(event) {
|
|
439
|
+
if (!this.isConnected)
|
|
440
|
+
return false;
|
|
441
|
+
return this.send({
|
|
442
|
+
type: 'webDom',
|
|
443
|
+
session: this.sessionId,
|
|
444
|
+
payload: JSON.stringify(event),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Send a batch of rrweb DOM events.
|
|
449
|
+
*/
|
|
450
|
+
sendWebDomBatch(events) {
|
|
451
|
+
if (!this.isConnected)
|
|
452
|
+
return false;
|
|
453
|
+
return this.send({
|
|
454
|
+
type: 'webDomBatch',
|
|
455
|
+
session: this.sessionId,
|
|
456
|
+
payload: JSON.stringify(events),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Send web page metadata (title, URL, viewport).
|
|
461
|
+
*/
|
|
462
|
+
sendWebMeta(meta) {
|
|
463
|
+
if (!this.isConnected)
|
|
464
|
+
return false;
|
|
465
|
+
return this.send({
|
|
466
|
+
type: 'webMeta',
|
|
467
|
+
session: this.sessionId,
|
|
468
|
+
meta,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
373
471
|
getConnected() {
|
|
374
472
|
return this.isConnected;
|
|
375
473
|
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -14,7 +14,7 @@ async function startAgent(options) {
|
|
|
14
14
|
(0, debug_1.setDebug)(true);
|
|
15
15
|
console.log(chalk_1.default.yellow('[DEBUG] Debug mode enabled'));
|
|
16
16
|
}
|
|
17
|
-
const config = runner_1.AgentRunner.loadConfig(options.config);
|
|
17
|
+
const config = await runner_1.AgentRunner.loadConfig(options.config);
|
|
18
18
|
const runner = new runner_1.AgentRunner(config);
|
|
19
19
|
await runner.start();
|
|
20
20
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
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.startTunnel = startTunnel;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const runner_1 = require("../agent/runner");
|
|
9
|
+
const browser_handler_1 = require("../agent/browser-handler");
|
|
10
|
+
const websocket_1 = require("../agent/websocket");
|
|
11
|
+
const debug_1 = require("../agent/debug");
|
|
12
|
+
const sentry_1 = require("../sentry");
|
|
13
|
+
const BATCH_INTERVAL_MS = 50;
|
|
14
|
+
const META_INTERVAL_MS = 15000;
|
|
15
|
+
const FULL_SNAPSHOT_TYPE = 2; // rrweb EventType.FullSnapshot
|
|
16
|
+
async function startTunnel(url, options) {
|
|
17
|
+
try {
|
|
18
|
+
if (options.debug) {
|
|
19
|
+
(0, debug_1.setDebug)(true);
|
|
20
|
+
console.log(chalk_1.default.yellow('[DEBUG] Debug mode enabled'));
|
|
21
|
+
}
|
|
22
|
+
// Load agent config for relay connection
|
|
23
|
+
const config = await runner_1.AgentRunner.loadConfig(options.config);
|
|
24
|
+
const cdpPort = options.cdpPort || 9222;
|
|
25
|
+
const width = options.width || 1280;
|
|
26
|
+
const height = options.height || 720;
|
|
27
|
+
console.log(chalk_1.default.bold('\n SessionCast Tunnel\n'));
|
|
28
|
+
console.log(` URL: ${chalk_1.default.cyan(url)}`);
|
|
29
|
+
console.log(` Viewport: ${width}x${height}`);
|
|
30
|
+
console.log(` Relay: ${config.relay}`);
|
|
31
|
+
console.log('');
|
|
32
|
+
// Start headless browser
|
|
33
|
+
const browser = new browser_handler_1.BrowserHandler();
|
|
34
|
+
console.log(chalk_1.default.gray(' Starting headless Chrome...'));
|
|
35
|
+
await browser.start(cdpPort, {
|
|
36
|
+
url,
|
|
37
|
+
width,
|
|
38
|
+
height,
|
|
39
|
+
chromePath: options.chromePath,
|
|
40
|
+
});
|
|
41
|
+
console.log(chalk_1.default.green(' Chrome started'));
|
|
42
|
+
// Build session ID for tunnel mode (tunnel-{hostname} matches web client detection)
|
|
43
|
+
const hostname = new URL(url).hostname;
|
|
44
|
+
const sessionId = `${config.machineId}/tunnel-${hostname}`;
|
|
45
|
+
// Connect to relay (skipAutoRegister: true — we send custom register with tunnel metadata)
|
|
46
|
+
const ws = new websocket_1.RelayWebSocketClient({
|
|
47
|
+
url: config.relay,
|
|
48
|
+
sessionId,
|
|
49
|
+
machineId: config.machineId,
|
|
50
|
+
token: config.token,
|
|
51
|
+
label: `tunnel:${hostname}`,
|
|
52
|
+
skipAutoRegister: true,
|
|
53
|
+
});
|
|
54
|
+
// Custom register handler with tunnel metadata
|
|
55
|
+
const registerTunnel = () => {
|
|
56
|
+
ws.send({
|
|
57
|
+
type: 'register',
|
|
58
|
+
role: 'host',
|
|
59
|
+
session: sessionId,
|
|
60
|
+
meta: {
|
|
61
|
+
label: `tunnel:${hostname}`,
|
|
62
|
+
machineId: config.machineId,
|
|
63
|
+
token: config.token,
|
|
64
|
+
tunnel: 'true',
|
|
65
|
+
tunnelUrl: url,
|
|
66
|
+
viewportWidth: String(width),
|
|
67
|
+
viewportHeight: String(height),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
// Note: connected/disconnected handlers are set up below with event batching
|
|
72
|
+
ws.on('error', (err) => {
|
|
73
|
+
(0, debug_1.debugLog)('Tunnel', 'WebSocket error:', err.message);
|
|
74
|
+
});
|
|
75
|
+
// Handle input events from viewer
|
|
76
|
+
ws.on('webInput', (event) => {
|
|
77
|
+
(0, debug_1.debugLog)('Tunnel', 'Input event:', event.inputType || event.type);
|
|
78
|
+
browser.dispatchInput(event).catch((err) => {
|
|
79
|
+
(0, debug_1.debugLog)('Tunnel', 'dispatchInput error:', err.message);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
// Handle snapshot request from relay (viewer joined / refreshed)
|
|
83
|
+
// Debounce to avoid excessive reloads when multiple viewers join at once
|
|
84
|
+
let snapshotTimer = null;
|
|
85
|
+
ws.on('requestSnapshot', () => {
|
|
86
|
+
(0, debug_1.debugLog)('Tunnel', 'Snapshot requested by relay');
|
|
87
|
+
if (snapshotTimer)
|
|
88
|
+
return; // already scheduled
|
|
89
|
+
snapshotTimer = setTimeout(() => {
|
|
90
|
+
snapshotTimer = null;
|
|
91
|
+
browser.takeFullSnapshot();
|
|
92
|
+
}, 1000);
|
|
93
|
+
});
|
|
94
|
+
// Handle navigation from viewer (localhost only for security)
|
|
95
|
+
ws.on('webNavigate', (navUrl) => {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = new URL(navUrl);
|
|
98
|
+
if (parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
|
|
99
|
+
console.log(chalk_1.default.yellow(` Blocked navigation to non-localhost URL: ${navUrl}`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
console.log(chalk_1.default.yellow(` Blocked navigation to invalid URL: ${navUrl}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.log(chalk_1.default.gray(` Navigating to: ${navUrl}`));
|
|
108
|
+
browser.navigate(navUrl).catch((err) => {
|
|
109
|
+
(0, debug_1.debugLog)('Tunnel', 'navigate error:', err.message);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
// Event batching — buffer events until WebSocket is connected
|
|
113
|
+
let eventBatch = [];
|
|
114
|
+
let batchTimer = null;
|
|
115
|
+
let wsConnected = false;
|
|
116
|
+
ws.on('connected', () => {
|
|
117
|
+
console.log(chalk_1.default.green(' Connected to relay'));
|
|
118
|
+
registerTunnel();
|
|
119
|
+
wsConnected = true;
|
|
120
|
+
// Flush any events buffered before connection
|
|
121
|
+
setTimeout(() => flushBatch(), 100);
|
|
122
|
+
// After reconnect, send a fresh FullSnapshot so viewers get current DOM state
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
browser.takeFullSnapshot();
|
|
125
|
+
}, 500);
|
|
126
|
+
});
|
|
127
|
+
ws.on('disconnected', () => {
|
|
128
|
+
console.log(chalk_1.default.yellow(' Disconnected from relay'));
|
|
129
|
+
wsConnected = false;
|
|
130
|
+
});
|
|
131
|
+
function flushBatch() {
|
|
132
|
+
if (eventBatch.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
// Don't flush if WebSocket isn't connected — keep buffered
|
|
135
|
+
if (!wsConnected)
|
|
136
|
+
return;
|
|
137
|
+
const batch = eventBatch;
|
|
138
|
+
eventBatch = [];
|
|
139
|
+
// FullSnapshot events get sent individually (they're large)
|
|
140
|
+
const fullSnapshots = batch.filter((e) => e.type === FULL_SNAPSHOT_TYPE);
|
|
141
|
+
const others = batch.filter((e) => e.type !== FULL_SNAPSHOT_TYPE);
|
|
142
|
+
for (const snap of fullSnapshots) {
|
|
143
|
+
ws.sendWebDom(snap);
|
|
144
|
+
}
|
|
145
|
+
if (others.length > 0) {
|
|
146
|
+
ws.sendWebDomBatch(others);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Stream DOM events from browser
|
|
150
|
+
browser.onDomEvent((event) => {
|
|
151
|
+
eventBatch.push(event);
|
|
152
|
+
// FullSnapshot: flush immediately (if connected)
|
|
153
|
+
if (event.type === FULL_SNAPSHOT_TYPE) {
|
|
154
|
+
flushBatch();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Otherwise batch with a timer
|
|
158
|
+
if (!batchTimer) {
|
|
159
|
+
batchTimer = setTimeout(() => {
|
|
160
|
+
batchTimer = null;
|
|
161
|
+
flushBatch();
|
|
162
|
+
}, BATCH_INTERVAL_MS);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// Periodically send page metadata
|
|
166
|
+
const metaTimer = setInterval(async () => {
|
|
167
|
+
if (!browser.isRunning())
|
|
168
|
+
return;
|
|
169
|
+
ws.sendWebMeta({
|
|
170
|
+
url: url,
|
|
171
|
+
viewportWidth: String(width),
|
|
172
|
+
viewportHeight: String(height),
|
|
173
|
+
timestamp: String(Date.now()),
|
|
174
|
+
});
|
|
175
|
+
}, META_INTERVAL_MS);
|
|
176
|
+
// Connect to relay
|
|
177
|
+
ws.connect();
|
|
178
|
+
console.log(chalk_1.default.green.bold(' Tunnel active'));
|
|
179
|
+
console.log(chalk_1.default.gray(' Press Ctrl+C to stop\n'));
|
|
180
|
+
// Graceful shutdown
|
|
181
|
+
const shutdown = () => {
|
|
182
|
+
console.log(chalk_1.default.gray('\n Shutting down tunnel...'));
|
|
183
|
+
clearInterval(metaTimer);
|
|
184
|
+
if (batchTimer)
|
|
185
|
+
clearTimeout(batchTimer);
|
|
186
|
+
flushBatch();
|
|
187
|
+
browser.stop();
|
|
188
|
+
ws.destroy();
|
|
189
|
+
console.log(chalk_1.default.green(' Tunnel stopped'));
|
|
190
|
+
process.exit(0);
|
|
191
|
+
};
|
|
192
|
+
process.on('SIGINT', shutdown);
|
|
193
|
+
process.on('SIGTERM', shutdown);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
(0, sentry_1.captureException)(error);
|
|
197
|
+
await (0, sentry_1.flush)();
|
|
198
|
+
console.error(chalk_1.default.red(`Error: ${error.message}`));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,7 @@ const agents_1 = require("./commands/agents");
|
|
|
46
46
|
const sessions_1 = require("./commands/sessions");
|
|
47
47
|
const sendkeys_1 = require("./commands/sendkeys");
|
|
48
48
|
const agent_1 = require("./commands/agent");
|
|
49
|
+
const tunnel_1 = require("./commands/tunnel");
|
|
49
50
|
const config_1 = require("./config");
|
|
50
51
|
const sentry_1 = require("./sentry");
|
|
51
52
|
// Initialize Sentry as early as possible
|
|
@@ -211,6 +212,28 @@ program
|
|
|
211
212
|
.option('-c, --config <path>', 'Path to config file')
|
|
212
213
|
.option('-d, --debug', 'Enable debug logging')
|
|
213
214
|
.action(agent_1.startAgent);
|
|
215
|
+
// Tunnel command
|
|
216
|
+
program
|
|
217
|
+
.command('tunnel <target>')
|
|
218
|
+
.description('Stream a local web service via headless Chrome (port number or URL)')
|
|
219
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
220
|
+
.option('-d, --debug', 'Enable debug logging')
|
|
221
|
+
.option('-W, --width <pixels>', 'Viewport width', '1280')
|
|
222
|
+
.option('-H, --height <pixels>', 'Viewport height', '720')
|
|
223
|
+
.option('--chrome-path <path>', 'Path to Chrome binary')
|
|
224
|
+
.option('--cdp-port <port>', 'Chrome DevTools Protocol port', '9222')
|
|
225
|
+
.action((target, opts) => {
|
|
226
|
+
// Accept port number (e.g., 3000) or full URL (e.g., http://localhost:3000)
|
|
227
|
+
const url = /^\d+$/.test(target) ? `http://localhost:${target}` : target;
|
|
228
|
+
(0, tunnel_1.startTunnel)(url, {
|
|
229
|
+
config: opts.config,
|
|
230
|
+
debug: opts.debug,
|
|
231
|
+
width: parseInt(opts.width, 10),
|
|
232
|
+
height: parseInt(opts.height, 10),
|
|
233
|
+
chromePath: opts.chromePath,
|
|
234
|
+
cdpPort: parseInt(opts.cdpPort, 10),
|
|
235
|
+
});
|
|
236
|
+
});
|
|
214
237
|
// Help examples
|
|
215
238
|
program.on('--help', () => {
|
|
216
239
|
console.log('');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sessioncast-cli",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "SessionCast CLI - Control your agents from anywhere",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"node-fetch": "^2.7.0",
|
|
51
51
|
"open": "^8.4.2",
|
|
52
52
|
"ora": "^5.4.1",
|
|
53
|
-
"ws": "^8.18.0"
|
|
53
|
+
"ws": "^8.18.0",
|
|
54
|
+
"zstd-napi": "^0.0.12"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@types/js-yaml": "^4.0.9",
|