sessioncast-cli 2.3.0 → 2.4.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.
@@ -214,19 +214,81 @@ async function logout() {
214
214
  (0, config_1.clearAuth)();
215
215
  console.log(chalk_1.default.green('\u2713 Logged out successfully!'));
216
216
  }
217
+ function formatExpiry(expiresAt) {
218
+ const now = Date.now();
219
+ if (now > expiresAt) {
220
+ return chalk_1.default.red('expired');
221
+ }
222
+ const remaining = expiresAt - now;
223
+ const hours = Math.floor(remaining / (1000 * 60 * 60));
224
+ const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
225
+ if (hours > 24) {
226
+ const days = Math.floor(hours / 24);
227
+ return chalk_1.default.green(`${days}d ${hours % 24}h remaining`);
228
+ }
229
+ if (hours > 0) {
230
+ return chalk_1.default.green(`${hours}h ${minutes}m remaining`);
231
+ }
232
+ return chalk_1.default.yellow(`${minutes}m remaining`);
233
+ }
217
234
  function status() {
218
235
  const accessToken = (0, config_1.getAccessToken)();
219
- if ((0, config_1.isLoggedIn)()) {
220
- console.log(chalk_1.default.green('\u2713 Logged in'));
221
- if (accessToken) {
222
- console.log(chalk_1.default.gray(' Auth method: OAuth'));
223
- }
224
- else {
225
- console.log(chalk_1.default.gray(' Auth method: API Key / Agent Token'));
226
- }
236
+ const rawAccessToken = (0, config_1.getRawAccessToken)();
237
+ const agentToken = (0, config_1.getAgentToken)();
238
+ const apiKey = (0, config_1.getApiKey)();
239
+ const refreshToken = (0, config_1.getRefreshToken)();
240
+ const machineId = (0, config_1.getMachineId)();
241
+ const expiresAt = (0, config_1.getTokenExpiresAt)();
242
+ console.log('');
243
+ if (!(0, config_1.isLoggedIn)()) {
244
+ console.log(chalk_1.default.yellow(' Not logged in'));
245
+ console.log(chalk_1.default.gray(' Run: sessioncast login\n'));
246
+ return;
247
+ }
248
+ console.log(chalk_1.default.green.bold(' \u2713 Logged in\n'));
249
+ // Auth method
250
+ if (accessToken) {
251
+ console.log(chalk_1.default.gray(' Auth method: ') + 'OAuth');
252
+ }
253
+ else if (rawAccessToken) {
254
+ console.log(chalk_1.default.gray(' Auth method: ') + chalk_1.default.yellow('OAuth (token expired)'));
255
+ }
256
+ else if (apiKey) {
257
+ console.log(chalk_1.default.gray(' Auth method: ') + 'API Key');
227
258
  }
228
259
  else {
229
- console.log(chalk_1.default.yellow('Not logged in'));
230
- console.log(chalk_1.default.gray('Run: sessioncast login'));
260
+ console.log(chalk_1.default.gray(' Auth method: ') + 'Agent Token');
261
+ }
262
+ if (machineId) {
263
+ console.log(chalk_1.default.gray(' Machine ID: ') + machineId);
264
+ }
265
+ console.log('');
266
+ // Tokens
267
+ console.log(chalk_1.default.bold(' Tokens'));
268
+ if (agentToken) {
269
+ console.log(chalk_1.default.gray(' Agent Token: ') + agentToken);
270
+ }
271
+ if (rawAccessToken) {
272
+ const expiryStr = expiresAt ? ` (${formatExpiry(expiresAt)})` : '';
273
+ console.log(chalk_1.default.gray(' Access Token: ') + rawAccessToken + expiryStr);
274
+ }
275
+ if (refreshToken) {
276
+ console.log(chalk_1.default.gray(' Refresh Token: ') + refreshToken);
277
+ }
278
+ if (apiKey) {
279
+ console.log(chalk_1.default.gray(' API Key: ') + apiKey);
280
+ }
281
+ if (!agentToken && !rawAccessToken && !refreshToken && !apiKey) {
282
+ console.log(chalk_1.default.gray(' (none)'));
231
283
  }
284
+ console.log('');
285
+ // Endpoints
286
+ console.log(chalk_1.default.bold(' Endpoints'));
287
+ console.log(chalk_1.default.gray(' API: ') + (0, config_1.getApiUrl)());
288
+ console.log(chalk_1.default.gray(' Auth: ') + (0, config_1.getAuthUrl)());
289
+ console.log(chalk_1.default.gray(' Relay: ') + (0, config_1.getRelayUrl)());
290
+ console.log('');
291
+ // Config path
292
+ console.log(chalk_1.default.gray(` Config: ${(0, config_1.getConfigPath)()}`));
293
+ console.log('');
232
294
  }
@@ -0,0 +1,10 @@
1
+ interface TunnelOptions {
2
+ config?: string;
3
+ debug?: boolean;
4
+ width?: number;
5
+ height?: number;
6
+ chromePath?: string;
7
+ cdpPort?: number;
8
+ }
9
+ export declare function startTunnel(url: string, options: TunnelOptions): Promise<void>;
10
+ export {};
@@ -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/config.d.ts CHANGED
@@ -33,4 +33,7 @@ export declare function clearAuth(): void;
33
33
  export declare function isLoggedIn(): boolean;
34
34
  export declare function hasSeenWelcome(): boolean;
35
35
  export declare function setSeenWelcome(): void;
36
+ export declare function getConfigPath(): string;
37
+ export declare function getRawAccessToken(): string | undefined;
38
+ export declare function getTokenExpiresAt(): number | undefined;
36
39
  export default config;
package/dist/config.js CHANGED
@@ -24,6 +24,9 @@ exports.clearAuth = clearAuth;
24
24
  exports.isLoggedIn = isLoggedIn;
25
25
  exports.hasSeenWelcome = hasSeenWelcome;
26
26
  exports.setSeenWelcome = setSeenWelcome;
27
+ exports.getConfigPath = getConfigPath;
28
+ exports.getRawAccessToken = getRawAccessToken;
29
+ exports.getTokenExpiresAt = getTokenExpiresAt;
27
30
  const conf_1 = __importDefault(require("conf"));
28
31
  const config = new conf_1.default({
29
32
  projectName: 'sessioncast',
@@ -110,4 +113,15 @@ function hasSeenWelcome() {
110
113
  function setSeenWelcome() {
111
114
  config.set('hasSeenWelcome', true);
112
115
  }
116
+ // Config file path
117
+ function getConfigPath() {
118
+ return config.path;
119
+ }
120
+ // Raw token access (bypass expiry check, for status display)
121
+ function getRawAccessToken() {
122
+ return config.get('accessToken');
123
+ }
124
+ function getTokenExpiresAt() {
125
+ return config.get('tokenExpiresAt');
126
+ }
113
127
  exports.default = config;
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
@@ -184,7 +185,7 @@ program
184
185
  // Status command
185
186
  program
186
187
  .command('status')
187
- .description('Check login status')
188
+ .description('Check login status and token info')
188
189
  .action(login_1.status);
189
190
  // Agents command
190
191
  program
@@ -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.0",
3
+ "version": "2.4.0",
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",