sessioncast-cli 2.0.3 → 2.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.
- package/dist/agent/runner.js +1 -2
- package/dist/agent/session-handler.d.ts +2 -0
- package/dist/agent/session-handler.js +78 -26
- package/dist/agent/tmux-executor.d.ts +16 -0
- package/dist/agent/tmux-executor.js +60 -0
- package/dist/agent/tmux.d.ts +10 -0
- package/dist/agent/tmux.js +14 -0
- package/dist/agent/websocket.d.ts +12 -0
- package/dist/agent/websocket.js +36 -1
- package/dist/api.js +3 -3
- package/dist/commands/sendkeys.js +125 -41
- package/dist/commands/sessions.js +77 -51
- package/package.json +1 -1
package/dist/agent/runner.js
CHANGED
|
@@ -74,8 +74,7 @@ class AgentRunner {
|
|
|
74
74
|
// If no config file found but agent token exists, create default config
|
|
75
75
|
if ((!finalPath || !fs.existsSync(finalPath)) && agentToken) {
|
|
76
76
|
console.log('Using OAuth authentication');
|
|
77
|
-
const
|
|
78
|
-
const machineId = `${hostname}-${Date.now()}`;
|
|
77
|
+
const machineId = os.hostname();
|
|
79
78
|
return {
|
|
80
79
|
machineId,
|
|
81
80
|
relay: (0, config_1.getRelayUrl)(),
|
|
@@ -15,6 +15,8 @@ export declare class TmuxSessionHandler {
|
|
|
15
15
|
private lastForceSendTime;
|
|
16
16
|
private lastChangeTime;
|
|
17
17
|
private captureTimer;
|
|
18
|
+
private lastPaneIds;
|
|
19
|
+
private lastPaneScreens;
|
|
18
20
|
private pendingUploads;
|
|
19
21
|
private uploadDir;
|
|
20
22
|
constructor(options: SessionHandlerOptions);
|
|
@@ -54,6 +54,9 @@ class TmuxSessionHandler {
|
|
|
54
54
|
this.lastForceSendTime = 0;
|
|
55
55
|
this.lastChangeTime = 0;
|
|
56
56
|
this.captureTimer = null;
|
|
57
|
+
// Multi-pane tracking
|
|
58
|
+
this.lastPaneIds = '';
|
|
59
|
+
this.lastPaneScreens = new Map();
|
|
57
60
|
// File upload handling
|
|
58
61
|
this.pendingUploads = new Map();
|
|
59
62
|
this.uploadDir = process.cwd(); // Default to current working directory
|
|
@@ -90,8 +93,8 @@ class TmuxSessionHandler {
|
|
|
90
93
|
console.log(`[${this.tmuxSession}] Disconnected: code=${code}, reason=${reason}`);
|
|
91
94
|
this.stopScreenCapture();
|
|
92
95
|
});
|
|
93
|
-
this.wsClient.on('keys', (keys) => {
|
|
94
|
-
this.handleKeys(keys);
|
|
96
|
+
this.wsClient.on('keys', (keys, paneId) => {
|
|
97
|
+
this.handleKeys(keys, paneId);
|
|
95
98
|
});
|
|
96
99
|
this.wsClient.on('resize', ({ cols, rows }) => {
|
|
97
100
|
console.log(`[${this.tmuxSession}] Resize: ${cols}x${rows}`);
|
|
@@ -281,8 +284,9 @@ class TmuxSessionHandler {
|
|
|
281
284
|
};
|
|
282
285
|
return types[ext] || 'application/octet-stream';
|
|
283
286
|
}
|
|
284
|
-
handleKeys(keys) {
|
|
285
|
-
|
|
287
|
+
handleKeys(keys, paneId) {
|
|
288
|
+
const target = paneId || this.tmuxSession;
|
|
289
|
+
tmux.sendKeys(target, keys, false);
|
|
286
290
|
}
|
|
287
291
|
startScreenCapture() {
|
|
288
292
|
if (this.captureTimer)
|
|
@@ -294,35 +298,83 @@ class TmuxSessionHandler {
|
|
|
294
298
|
return;
|
|
295
299
|
}
|
|
296
300
|
try {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
301
|
+
const panes = tmux.listPanes(this.tmuxSession);
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
const forceTime = (now - this.lastForceSendTime) >= FORCE_SEND_INTERVAL_MS;
|
|
304
|
+
if (panes && panes.length > 1) {
|
|
305
|
+
// Multi-pane mode
|
|
306
|
+
const currentPaneIds = panes.map(p => p.id).join(',');
|
|
307
|
+
if (currentPaneIds !== this.lastPaneIds || forceTime) {
|
|
308
|
+
// Layout changed or periodic resend for late-joining viewers
|
|
309
|
+
this.lastPaneIds = currentPaneIds;
|
|
310
|
+
this.wsClient.sendPaneLayout(panes);
|
|
311
|
+
}
|
|
312
|
+
// Capture each pane individually
|
|
313
|
+
for (const pane of panes) {
|
|
314
|
+
const screen = tmux.capturePaneById(this.tmuxSession, pane.id);
|
|
315
|
+
if (screen !== null) {
|
|
316
|
+
const lastScreen = this.lastPaneScreens.get(pane.id);
|
|
317
|
+
if (screen !== lastScreen || forceTime) {
|
|
318
|
+
this.lastPaneScreens.set(pane.id, screen);
|
|
319
|
+
if (screen !== lastScreen) {
|
|
320
|
+
this.lastChangeTime = now;
|
|
321
|
+
}
|
|
322
|
+
const fullOutput = '\x1b[2J\x1b[H' + screen;
|
|
323
|
+
const data = Buffer.from(fullOutput, 'utf-8');
|
|
324
|
+
const meta = { pane: pane.id };
|
|
325
|
+
if (USE_COMPRESSION && data.length > MIN_COMPRESS_SIZE) {
|
|
326
|
+
this.wsClient.sendScreenCompressedWithMeta(data, meta);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
this.wsClient.sendScreenWithMeta(data, meta);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
317
332
|
}
|
|
318
333
|
}
|
|
319
|
-
|
|
334
|
+
if (forceTime) {
|
|
335
|
+
this.lastForceSendTime = now;
|
|
336
|
+
}
|
|
337
|
+
// Adaptive sleep
|
|
320
338
|
const isActive = (now - this.lastChangeTime) < ACTIVE_THRESHOLD_MS;
|
|
321
339
|
const sleepMs = isActive ? CAPTURE_INTERVAL_ACTIVE_MS : CAPTURE_INTERVAL_IDLE_MS;
|
|
322
340
|
this.captureTimer = setTimeout(capture, sleepMs);
|
|
323
341
|
}
|
|
324
342
|
else {
|
|
325
|
-
|
|
343
|
+
// Single pane mode (existing logic)
|
|
344
|
+
if (this.lastPaneIds !== '') {
|
|
345
|
+
// Was multi-pane, now single — notify viewers
|
|
346
|
+
this.lastPaneIds = '';
|
|
347
|
+
this.lastPaneScreens.clear();
|
|
348
|
+
this.wsClient.sendPaneLayout([]);
|
|
349
|
+
}
|
|
350
|
+
const screen = tmux.capturePane(this.tmuxSession);
|
|
351
|
+
if (screen !== null) {
|
|
352
|
+
const changed = screen !== this.lastScreen;
|
|
353
|
+
if (changed || forceTime) {
|
|
354
|
+
this.lastScreen = screen;
|
|
355
|
+
this.lastForceSendTime = now;
|
|
356
|
+
if (changed) {
|
|
357
|
+
this.lastChangeTime = now;
|
|
358
|
+
}
|
|
359
|
+
// Send clear screen + content
|
|
360
|
+
const fullOutput = '\x1b[2J\x1b[H' + screen;
|
|
361
|
+
const data = Buffer.from(fullOutput, 'utf-8');
|
|
362
|
+
// Compress if enabled and data is large enough
|
|
363
|
+
if (USE_COMPRESSION && data.length > MIN_COMPRESS_SIZE) {
|
|
364
|
+
this.wsClient.sendScreenCompressed(data);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
this.wsClient.sendScreen(data);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Adaptive sleep: faster when active, slower when idle
|
|
371
|
+
const isActive = (now - this.lastChangeTime) < ACTIVE_THRESHOLD_MS;
|
|
372
|
+
const sleepMs = isActive ? CAPTURE_INTERVAL_ACTIVE_MS : CAPTURE_INTERVAL_IDLE_MS;
|
|
373
|
+
this.captureTimer = setTimeout(capture, sleepMs);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
this.captureTimer = setTimeout(capture, CAPTURE_INTERVAL_IDLE_MS);
|
|
377
|
+
}
|
|
326
378
|
}
|
|
327
379
|
}
|
|
328
380
|
catch (error) {
|
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
* Interface for tmux operations.
|
|
3
3
|
* Implementations handle platform-specific differences (Unix vs Windows/itmux).
|
|
4
4
|
*/
|
|
5
|
+
export interface PaneData {
|
|
6
|
+
id: string;
|
|
7
|
+
index: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
top: number;
|
|
11
|
+
left: number;
|
|
12
|
+
active: boolean;
|
|
13
|
+
title: string;
|
|
14
|
+
}
|
|
5
15
|
export interface TmuxExecutor {
|
|
6
16
|
listSessions(): string[];
|
|
7
17
|
capturePane(session: string): string | null;
|
|
18
|
+
capturePaneById(session: string, paneId: string): string | null;
|
|
19
|
+
listPanes(session: string): PaneData[] | null;
|
|
8
20
|
sendKeys(session: string, keys: string): boolean;
|
|
9
21
|
sendSpecialKey(session: string, key: string): boolean;
|
|
10
22
|
resizeWindow(session: string, cols: number, rows: number): boolean;
|
|
@@ -21,6 +33,8 @@ export interface TmuxExecutor {
|
|
|
21
33
|
export declare class UnixTmuxExecutor implements TmuxExecutor {
|
|
22
34
|
listSessions(): string[];
|
|
23
35
|
capturePane(session: string): string | null;
|
|
36
|
+
capturePaneById(session: string, paneId: string): string | null;
|
|
37
|
+
listPanes(session: string): PaneData[] | null;
|
|
24
38
|
sendKeys(session: string, keys: string): boolean;
|
|
25
39
|
sendSpecialKey(session: string, key: string): boolean;
|
|
26
40
|
resizeWindow(session: string, cols: number, rows: number): boolean;
|
|
@@ -42,6 +56,8 @@ export declare class WindowsTmuxExecutor implements TmuxExecutor {
|
|
|
42
56
|
private executeCommand;
|
|
43
57
|
listSessions(): string[];
|
|
44
58
|
capturePane(session: string): string | null;
|
|
59
|
+
capturePaneById(session: string, paneId: string): string | null;
|
|
60
|
+
listPanes(session: string): PaneData[] | null;
|
|
45
61
|
sendKeys(session: string, keys: string): boolean;
|
|
46
62
|
sendSpecialKey(session: string, key: string): boolean;
|
|
47
63
|
resizeWindow(session: string, cols: number, rows: number): boolean;
|
|
@@ -73,6 +73,35 @@ class UnixTmuxExecutor {
|
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
capturePaneById(session, paneId) {
|
|
77
|
+
try {
|
|
78
|
+
const output = (0, child_process_1.execSync)(`tmux capture-pane -t "${paneId}" -p -e -N`, {
|
|
79
|
+
encoding: 'utf-8',
|
|
80
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
81
|
+
maxBuffer: 10 * 1024 * 1024
|
|
82
|
+
});
|
|
83
|
+
return output.replace(/\n/g, '\r\n');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
listPanes(session) {
|
|
90
|
+
try {
|
|
91
|
+
const output = (0, child_process_1.execSync)(`tmux list-panes -t "${session}" -F "#{pane_id}:#{pane_index}:#{pane_width}:#{pane_height}:#{pane_top}:#{pane_left}:#{?pane_active,1,0}:#{pane_title}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
92
|
+
return output.trim().split('\n').filter(Boolean).map(line => {
|
|
93
|
+
const [id, index, width, height, top, left, active, ...titleParts] = line.split(':');
|
|
94
|
+
return {
|
|
95
|
+
id, index: parseInt(index), width: parseInt(width), height: parseInt(height),
|
|
96
|
+
top: parseInt(top), left: parseInt(left),
|
|
97
|
+
active: active === '1', title: titleParts.join(':') || ''
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
76
105
|
sendKeys(session, keys) {
|
|
77
106
|
try {
|
|
78
107
|
const escaped = this.escapeForShell(keys);
|
|
@@ -235,6 +264,37 @@ class WindowsTmuxExecutor {
|
|
|
235
264
|
return null;
|
|
236
265
|
}
|
|
237
266
|
}
|
|
267
|
+
capturePaneById(session, paneId) {
|
|
268
|
+
try {
|
|
269
|
+
const escaped = this.escapeSession(paneId);
|
|
270
|
+
const output = this.executeCommand(`tmux capture-pane -t '${escaped}' -p -e -N`);
|
|
271
|
+
if (!output)
|
|
272
|
+
return null;
|
|
273
|
+
return output.replace(/\n/g, '\r\n');
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
listPanes(session) {
|
|
280
|
+
try {
|
|
281
|
+
const escaped = this.escapeSession(session);
|
|
282
|
+
const output = this.executeCommand(`tmux list-panes -t '${escaped}' -F "#{pane_id}:#{pane_index}:#{pane_width}:#{pane_height}:#{pane_top}:#{pane_left}:#{?pane_active,1,0}:#{pane_title}"`);
|
|
283
|
+
if (!output)
|
|
284
|
+
return null;
|
|
285
|
+
return output.trim().split('\n').filter(Boolean).map(line => {
|
|
286
|
+
const [id, index, width, height, top, left, active, ...titleParts] = line.split(':');
|
|
287
|
+
return {
|
|
288
|
+
id, index: parseInt(index), width: parseInt(width), height: parseInt(height),
|
|
289
|
+
top: parseInt(top), left: parseInt(left),
|
|
290
|
+
active: active === '1', title: titleParts.join(':') || ''
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
238
298
|
sendKeys(session, keys) {
|
|
239
299
|
try {
|
|
240
300
|
const escapedSession = this.escapeSession(session);
|
package/dist/agent/tmux.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PaneData } from './tmux-executor';
|
|
1
2
|
import { TmuxSession } from './types';
|
|
2
3
|
/**
|
|
3
4
|
* Scan for all tmux sessions
|
|
@@ -43,3 +44,12 @@ export declare function getActivePane(sessionName: string): string | null;
|
|
|
43
44
|
* Get the current working directory of a pane
|
|
44
45
|
*/
|
|
45
46
|
export declare function getPaneCwd(sessionName: string, paneId?: string): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* List all panes in a tmux session
|
|
49
|
+
*/
|
|
50
|
+
export declare function listPanes(sessionName: string): PaneData[] | null;
|
|
51
|
+
/**
|
|
52
|
+
* Capture a specific pane by its pane ID (e.g., %0, %1)
|
|
53
|
+
*/
|
|
54
|
+
export declare function capturePaneById(sessionName: string, paneId: string): string | null;
|
|
55
|
+
export type { PaneData } from './tmux-executor';
|
package/dist/agent/tmux.js
CHANGED
|
@@ -11,6 +11,8 @@ exports.isAvailable = isAvailable;
|
|
|
11
11
|
exports.getVersion = getVersion;
|
|
12
12
|
exports.getActivePane = getActivePane;
|
|
13
13
|
exports.getPaneCwd = getPaneCwd;
|
|
14
|
+
exports.listPanes = listPanes;
|
|
15
|
+
exports.capturePaneById = capturePaneById;
|
|
14
16
|
const tmux_executor_1 = require("./tmux-executor");
|
|
15
17
|
// Lazy-initialized executor (created on first use)
|
|
16
18
|
let executor = null;
|
|
@@ -145,3 +147,15 @@ function getActivePane(sessionName) {
|
|
|
145
147
|
function getPaneCwd(sessionName, paneId) {
|
|
146
148
|
return getExecutor().getPaneCwd(sessionName, paneId);
|
|
147
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* List all panes in a tmux session
|
|
152
|
+
*/
|
|
153
|
+
function listPanes(sessionName) {
|
|
154
|
+
return getExecutor().listPanes(sessionName);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Capture a specific pane by its pane ID (e.g., %0, %1)
|
|
158
|
+
*/
|
|
159
|
+
function capturePaneById(sessionName, paneId) {
|
|
160
|
+
return getExecutor().capturePaneById(sessionName, paneId);
|
|
161
|
+
}
|
|
@@ -30,7 +30,19 @@ export declare class RelayWebSocketClient extends EventEmitter {
|
|
|
30
30
|
private scheduleReconnect;
|
|
31
31
|
send(message: Message): boolean;
|
|
32
32
|
sendScreen(data: Buffer): boolean;
|
|
33
|
+
sendScreenWithMeta(data: Buffer, meta: Record<string, string>): boolean;
|
|
33
34
|
sendScreenCompressed(data: Buffer): boolean;
|
|
35
|
+
sendScreenCompressedWithMeta(data: Buffer, meta: Record<string, string>): boolean;
|
|
36
|
+
sendPaneLayout(panes: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
index: number;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
top: number;
|
|
42
|
+
left: number;
|
|
43
|
+
active: boolean;
|
|
44
|
+
title: string;
|
|
45
|
+
}>): boolean;
|
|
34
46
|
/**
|
|
35
47
|
* Send file content to be displayed in the web FileViewer
|
|
36
48
|
* @param filename - The name of the file
|
package/dist/agent/websocket.js
CHANGED
|
@@ -119,7 +119,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
119
119
|
switch (message.type) {
|
|
120
120
|
case 'keys':
|
|
121
121
|
if (message.session === this.sessionId && message.payload) {
|
|
122
|
-
this.emit('keys', message.payload);
|
|
122
|
+
this.emit('keys', message.payload, message.meta?.pane);
|
|
123
123
|
}
|
|
124
124
|
break;
|
|
125
125
|
case 'resize':
|
|
@@ -249,6 +249,16 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
249
249
|
payload: base64Data
|
|
250
250
|
});
|
|
251
251
|
}
|
|
252
|
+
sendScreenWithMeta(data, meta) {
|
|
253
|
+
if (!this.isConnected)
|
|
254
|
+
return false;
|
|
255
|
+
return this.send({
|
|
256
|
+
type: 'screen',
|
|
257
|
+
session: this.sessionId,
|
|
258
|
+
payload: data.toString('base64'),
|
|
259
|
+
meta
|
|
260
|
+
});
|
|
261
|
+
}
|
|
252
262
|
sendScreenCompressed(data) {
|
|
253
263
|
if (!this.isConnected)
|
|
254
264
|
return false;
|
|
@@ -265,6 +275,31 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
265
275
|
return this.sendScreen(data);
|
|
266
276
|
}
|
|
267
277
|
}
|
|
278
|
+
sendScreenCompressedWithMeta(data, meta) {
|
|
279
|
+
if (!this.isConnected)
|
|
280
|
+
return false;
|
|
281
|
+
try {
|
|
282
|
+
const compressed = zlib.gzipSync(data);
|
|
283
|
+
return this.send({
|
|
284
|
+
type: 'screenGz',
|
|
285
|
+
session: this.sessionId,
|
|
286
|
+
payload: compressed.toString('base64'),
|
|
287
|
+
meta
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return this.sendScreenWithMeta(data, meta);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
sendPaneLayout(panes) {
|
|
295
|
+
if (!this.isConnected)
|
|
296
|
+
return false;
|
|
297
|
+
return this.send({
|
|
298
|
+
type: 'paneLayout',
|
|
299
|
+
session: this.sessionId,
|
|
300
|
+
payload: JSON.stringify(panes)
|
|
301
|
+
});
|
|
302
|
+
}
|
|
268
303
|
/**
|
|
269
304
|
* Send file content to be displayed in the web FileViewer
|
|
270
305
|
* @param filename - The name of the file
|
package/dist/api.js
CHANGED
|
@@ -8,12 +8,12 @@ const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
|
8
8
|
const config_1 = require("./config");
|
|
9
9
|
class ApiClient {
|
|
10
10
|
getHeaders() {
|
|
11
|
-
const
|
|
12
|
-
if (!
|
|
11
|
+
const token = (0, config_1.getApiKey)() || (0, config_1.getAccessToken)() || (0, config_1.getAgentToken)();
|
|
12
|
+
if (!token) {
|
|
13
13
|
throw new Error('Not logged in. Run: sessioncast login');
|
|
14
14
|
}
|
|
15
15
|
return {
|
|
16
|
-
'Authorization': `Bearer ${
|
|
16
|
+
'Authorization': `Bearer ${token}`,
|
|
17
17
|
'Content-Type': 'application/json'
|
|
18
18
|
};
|
|
19
19
|
}
|
|
@@ -6,56 +6,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.sendKeys = sendKeys;
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const ora_1 = __importDefault(require("ora"));
|
|
9
|
-
const
|
|
9
|
+
const ws_1 = __importDefault(require("ws"));
|
|
10
10
|
const config_1 = require("../config");
|
|
11
11
|
async function sendKeys(target, keys, options) {
|
|
12
12
|
if (!(0, config_1.isLoggedIn)()) {
|
|
13
|
-
console.log(chalk_1.default.red('Not logged in. Run: sessioncast login
|
|
13
|
+
console.log(chalk_1.default.red('Not logged in. Run: sessioncast login'));
|
|
14
14
|
process.exit(1);
|
|
15
15
|
}
|
|
16
|
-
// Parse target: "
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
console.log(chalk_1.default.
|
|
16
|
+
// Parse target: "session" or "machineId/session"
|
|
17
|
+
// Accept both "machineId/session" and "machineId:session" formats
|
|
18
|
+
const normalizedTarget = target.replace(':', '/');
|
|
19
|
+
const token = (0, config_1.getAccessToken)() || (0, config_1.getAgentToken)() || (0, config_1.getApiKey)();
|
|
20
|
+
if (!token) {
|
|
21
|
+
console.log(chalk_1.default.red('No auth token found. Run: sessioncast login'));
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const spinner = (0, ora_1.default)('Finding agent...').start();
|
|
24
|
+
const relayUrl = (0, config_1.getRelayUrl)();
|
|
25
|
+
const spinner = (0, ora_1.default)('Connecting to relay...').start();
|
|
27
26
|
try {
|
|
28
|
-
|
|
29
|
-
const agent = await api_1.api.findAgentByName(agentName);
|
|
30
|
-
if (!agent) {
|
|
31
|
-
spinner.stop();
|
|
32
|
-
console.log(chalk_1.default.red(`Agent not found: ${agentName}`));
|
|
33
|
-
console.log(chalk_1.default.gray('Run: sessioncast agents'));
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
if (!agent.isActive) {
|
|
37
|
-
spinner.stop();
|
|
38
|
-
console.log(chalk_1.default.red(`Agent is offline: ${agentName}`));
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
if (!agent.apiEnabled) {
|
|
42
|
-
spinner.stop();
|
|
43
|
-
console.log(chalk_1.default.red(`API is not enabled for agent: ${agentName}`));
|
|
44
|
-
console.log(chalk_1.default.gray('Enable API in agent settings at https://account.sessioncast.io'));
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
spinner.text = 'Sending keys...';
|
|
48
|
-
const result = await api_1.api.sendKeys(agent.id, sessionTarget, keys, !options.noEnter);
|
|
27
|
+
const sessionId = await sendKeysViaRelay(relayUrl, token, normalizedTarget, keys, !options.noEnter, spinner);
|
|
49
28
|
spinner.stop();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.log(chalk_1.default.gray('(Enter key was pressed)'));
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
console.log(chalk_1.default.red(`Failed to send keys: ${result.error || 'Unknown error'}`));
|
|
58
|
-
process.exit(1);
|
|
29
|
+
console.log(chalk_1.default.green(`✓ Keys sent to ${sessionId}`));
|
|
30
|
+
if (!options.noEnter) {
|
|
31
|
+
console.log(chalk_1.default.gray('(Enter key was pressed)'));
|
|
59
32
|
}
|
|
60
33
|
}
|
|
61
34
|
catch (error) {
|
|
@@ -64,3 +37,114 @@ async function sendKeys(target, keys, options) {
|
|
|
64
37
|
process.exit(1);
|
|
65
38
|
}
|
|
66
39
|
}
|
|
40
|
+
function sendKeysViaRelay(relayUrl, token, target, keys, enter, spinner) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const wsUrl = `${relayUrl}?token=${encodeURIComponent(token)}`;
|
|
43
|
+
const ws = new ws_1.default(wsUrl);
|
|
44
|
+
let resolved = false;
|
|
45
|
+
let sessionList = [];
|
|
46
|
+
const timeout = setTimeout(() => {
|
|
47
|
+
if (!resolved) {
|
|
48
|
+
resolved = true;
|
|
49
|
+
ws.close();
|
|
50
|
+
reject(new Error('Timeout waiting for relay response'));
|
|
51
|
+
}
|
|
52
|
+
}, 10000);
|
|
53
|
+
ws.on('open', () => {
|
|
54
|
+
// Request session list to find the target
|
|
55
|
+
ws.send(JSON.stringify({ type: 'listSessions' }));
|
|
56
|
+
});
|
|
57
|
+
ws.on('message', (data) => {
|
|
58
|
+
try {
|
|
59
|
+
const message = JSON.parse(data.toString());
|
|
60
|
+
if (message.type === 'sessionList' && message.sessions) {
|
|
61
|
+
sessionList = message.sessions;
|
|
62
|
+
spinner.text = `Found ${sessionList.length} sessions, finding target...`;
|
|
63
|
+
// Find matching session
|
|
64
|
+
const matched = findSession(sessionList, target);
|
|
65
|
+
if (!matched) {
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
resolved = true;
|
|
68
|
+
ws.close();
|
|
69
|
+
const available = sessionList.map(s => ` ${s.id} (${s.label || 'no label'}) [${s.status}]`).join('\n');
|
|
70
|
+
reject(new Error(`Session not found: ${target}\n\nAvailable sessions:\n${available || ' (none)'}`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (matched.status !== 'online') {
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
resolved = true;
|
|
76
|
+
ws.close();
|
|
77
|
+
reject(new Error(`Session is offline: ${matched.id}`));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Register as viewer first
|
|
81
|
+
ws.send(JSON.stringify({
|
|
82
|
+
type: 'register',
|
|
83
|
+
role: 'viewer',
|
|
84
|
+
session: matched.id,
|
|
85
|
+
}));
|
|
86
|
+
// Send keys
|
|
87
|
+
const payload = enter ? keys + '\n' : keys;
|
|
88
|
+
ws.send(JSON.stringify({
|
|
89
|
+
type: 'keys',
|
|
90
|
+
session: matched.id,
|
|
91
|
+
payload,
|
|
92
|
+
}));
|
|
93
|
+
// Small delay to ensure delivery, then close
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
if (!resolved) {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
resolved = true;
|
|
98
|
+
ws.close();
|
|
99
|
+
resolve(matched.id);
|
|
100
|
+
}
|
|
101
|
+
}, 300);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// ignore parse errors
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
ws.on('error', (err) => {
|
|
109
|
+
if (!resolved) {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
resolved = true;
|
|
112
|
+
reject(new Error(`WebSocket error: ${err.message}`));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
ws.on('close', () => {
|
|
116
|
+
if (!resolved) {
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
resolved = true;
|
|
119
|
+
reject(new Error('Connection closed unexpectedly'));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function findSession(sessions, target) {
|
|
125
|
+
// Exact match on session id
|
|
126
|
+
const exact = sessions.find(s => s.id === target);
|
|
127
|
+
if (exact)
|
|
128
|
+
return exact;
|
|
129
|
+
// Match by label (session name part)
|
|
130
|
+
const byLabel = sessions.find(s => s.label === target);
|
|
131
|
+
if (byLabel)
|
|
132
|
+
return byLabel;
|
|
133
|
+
// Match by partial: "machineId/session" where target might be partial machineId
|
|
134
|
+
const parts = target.split('/');
|
|
135
|
+
if (parts.length >= 2) {
|
|
136
|
+
const [machineHint, ...sessionParts] = parts;
|
|
137
|
+
const sessionName = sessionParts.join('/');
|
|
138
|
+
const match = sessions.find(s => s.id.includes(machineHint) && s.id.endsWith('/' + sessionName));
|
|
139
|
+
if (match)
|
|
140
|
+
return match;
|
|
141
|
+
}
|
|
142
|
+
// Match just session name (last part of id after /)
|
|
143
|
+
const bySessionName = sessions.find(s => {
|
|
144
|
+
const name = s.id.split('/').pop();
|
|
145
|
+
return name === target;
|
|
146
|
+
});
|
|
147
|
+
if (bySessionName)
|
|
148
|
+
return bySessionName;
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
@@ -6,75 +6,68 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.listSessions = listSessions;
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const ora_1 = __importDefault(require("ora"));
|
|
9
|
-
const
|
|
9
|
+
const ws_1 = __importDefault(require("ws"));
|
|
10
10
|
const config_1 = require("../config");
|
|
11
11
|
async function listSessions(agentName) {
|
|
12
12
|
if (!(0, config_1.isLoggedIn)()) {
|
|
13
|
-
console.log(chalk_1.default.red('Not logged in. Run: sessioncast login
|
|
13
|
+
console.log(chalk_1.default.red('Not logged in. Run: sessioncast login'));
|
|
14
14
|
process.exit(1);
|
|
15
15
|
}
|
|
16
|
-
const
|
|
16
|
+
const token = (0, config_1.getAccessToken)() || (0, config_1.getAgentToken)() || (0, config_1.getApiKey)();
|
|
17
|
+
if (!token) {
|
|
18
|
+
console.log(chalk_1.default.red('No auth token found. Run: sessioncast login'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const relayUrl = (0, config_1.getRelayUrl)();
|
|
22
|
+
const spinner = (0, ora_1.default)('Fetching sessions from relay...').start();
|
|
17
23
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
console.log(chalk_1.default.yellow('No online agents with API enabled.'));
|
|
24
|
+
const sessions = await fetchSessionsFromRelay(relayUrl, token);
|
|
25
|
+
spinner.stop();
|
|
26
|
+
if (sessions.length === 0) {
|
|
27
|
+
console.log(chalk_1.default.yellow('No sessions found.'));
|
|
23
28
|
return;
|
|
24
29
|
}
|
|
30
|
+
// Group by machineId
|
|
31
|
+
const grouped = sessions.reduce((acc, s) => {
|
|
32
|
+
const machine = s.machineId || 'unknown';
|
|
33
|
+
if (!acc[machine])
|
|
34
|
+
acc[machine] = [];
|
|
35
|
+
acc[machine].push(s);
|
|
36
|
+
return acc;
|
|
37
|
+
}, {});
|
|
25
38
|
// Filter by agent name if provided
|
|
26
|
-
let targetAgents = onlineAgents;
|
|
27
39
|
if (agentName) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
spinner.stop();
|
|
33
|
-
console.log(chalk_1.default.red(`Agent not found: ${agentName}`));
|
|
34
|
-
console.log(chalk_1.default.gray('Run: sessioncast agents'));
|
|
40
|
+
const matchedKey = Object.keys(grouped).find(k => k.toLowerCase().includes(agentName.toLowerCase()));
|
|
41
|
+
if (!matchedKey) {
|
|
42
|
+
console.log(chalk_1.default.red(`No agent matching: ${agentName}`));
|
|
43
|
+
console.log(chalk_1.default.gray('Available agents: ' + Object.keys(grouped).join(', ')));
|
|
35
44
|
process.exit(1);
|
|
36
45
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const sessions = await api_1.api.listSessions(agent.id);
|
|
44
|
-
allSessions.push({ agent, sessions });
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
// Skip failed agents
|
|
48
|
-
}
|
|
46
|
+
const filtered = {};
|
|
47
|
+
filtered[matchedKey] = grouped[matchedKey];
|
|
48
|
+
Object.keys(grouped).forEach(k => { if (k !== matchedKey)
|
|
49
|
+
delete grouped[k]; });
|
|
50
|
+
Object.assign(grouped, filtered);
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
console.log(chalk_1.default.bold('\nTmux Sessions:\n'));
|
|
56
|
-
// Table header
|
|
57
|
-
console.log(chalk_1.default.gray(padRight('AGENT', 16) +
|
|
58
|
-
padRight('SESSION', 16) +
|
|
59
|
-
padRight('WINDOWS', 10) +
|
|
60
|
-
padRight('ATTACHED', 10) +
|
|
52
|
+
console.log(chalk_1.default.bold('\nSessions:\n'));
|
|
53
|
+
console.log(chalk_1.default.gray(padRight('AGENT', 30) +
|
|
54
|
+
padRight('SESSION', 20) +
|
|
55
|
+
padRight('STATUS', 10) +
|
|
61
56
|
'TARGET'));
|
|
62
|
-
console.log(chalk_1.default.gray('─'.repeat(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
padRight(session.
|
|
71
|
-
padRight(String(session.windows), 10) +
|
|
72
|
-
padRight(attached, 10) +
|
|
57
|
+
console.log(chalk_1.default.gray('─'.repeat(80)));
|
|
58
|
+
for (const [machineId, machineSessions] of Object.entries(grouped)) {
|
|
59
|
+
for (const session of machineSessions) {
|
|
60
|
+
const label = session.label || session.id.split('/').pop() || session.id;
|
|
61
|
+
const statusColor = session.status === 'online' ? chalk_1.default.green : chalk_1.default.red;
|
|
62
|
+
const target = session.id;
|
|
63
|
+
console.log(padRight(machineId, 30) +
|
|
64
|
+
padRight(label, 20) +
|
|
65
|
+
padRight(statusColor(session.status), 10) +
|
|
73
66
|
chalk_1.default.cyan(target));
|
|
74
67
|
}
|
|
75
68
|
}
|
|
76
69
|
console.log();
|
|
77
|
-
console.log(chalk_1.default.gray('Use: sessioncast send <
|
|
70
|
+
console.log(chalk_1.default.gray('Use: sessioncast send <session-label> "command"'));
|
|
78
71
|
}
|
|
79
72
|
catch (error) {
|
|
80
73
|
spinner.stop();
|
|
@@ -82,6 +75,39 @@ async function listSessions(agentName) {
|
|
|
82
75
|
process.exit(1);
|
|
83
76
|
}
|
|
84
77
|
}
|
|
78
|
+
function fetchSessionsFromRelay(relayUrl, token) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const wsUrl = `${relayUrl}?token=${encodeURIComponent(token)}`;
|
|
81
|
+
const ws = new ws_1.default(wsUrl);
|
|
82
|
+
const timeout = setTimeout(() => {
|
|
83
|
+
ws.close();
|
|
84
|
+
reject(new Error('Timeout waiting for session list'));
|
|
85
|
+
}, 10000);
|
|
86
|
+
ws.on('open', () => {
|
|
87
|
+
ws.send(JSON.stringify({ type: 'listSessions' }));
|
|
88
|
+
});
|
|
89
|
+
ws.on('message', (data) => {
|
|
90
|
+
try {
|
|
91
|
+
const message = JSON.parse(data.toString());
|
|
92
|
+
if (message.type === 'sessionList' && message.sessions) {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
ws.close();
|
|
95
|
+
resolve(message.sessions);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
ws.on('error', (err) => {
|
|
103
|
+
clearTimeout(timeout);
|
|
104
|
+
reject(new Error(`WebSocket error: ${err.message}`));
|
|
105
|
+
});
|
|
106
|
+
ws.on('close', () => {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
85
111
|
function padRight(str, len) {
|
|
86
112
|
const plainStr = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
87
113
|
const padding = Math.max(0, len - plainStr.length);
|