sessioncast-cli 1.1.5 → 2.0.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/README.md CHANGED
@@ -12,7 +12,8 @@ Node.js agent and CLI for [SessionCast](https://sessioncast.io) - a real-time te
12
12
  - **File viewer**: Cmd+Click on file paths to view files in browser
13
13
 
14
14
  ### CLI Commands
15
- - `sessioncast login <api-key>` - Authenticate with API key
15
+ - `sessioncast login` - Browser-based OAuth login (recommended)
16
+ - `sessioncast login <api-key>` - Authenticate with API key or agent token
16
17
  - `sessioncast logout` - Clear stored credentials
17
18
  - `sessioncast status` - Check authentication status
18
19
  - `sessioncast agents` - List registered agents
@@ -26,6 +27,31 @@ Node.js agent and CLI for [SessionCast](https://sessioncast.io) - a real-time te
26
27
  npm install -g sessioncast-cli
27
28
  ```
28
29
 
30
+ ### Requirements
31
+ - Node.js 18+
32
+ - tmux (Linux/macOS) or [itmux](https://github.com/itefixnet/itmux) (Windows)
33
+
34
+ ### Windows Setup
35
+
36
+ **Quick Install (PowerShell - Recommended)**:
37
+ ```powershell
38
+ # Download and extract itmux to C:\itmux
39
+ Invoke-WebRequest -Uri "https://github.com/itefixnet/itmux/releases/download/v1.1.0/itmux_1.1.0_x64_free.zip" -OutFile "$env:TEMP\itmux.zip"
40
+ Expand-Archive -Path "$env:TEMP\itmux.zip" -DestinationPath "C:\itmux" -Force
41
+
42
+ # Install SessionCast CLI
43
+ npm install -g sessioncast-cli
44
+ ```
45
+
46
+ **Manual Installation**:
47
+ 1. Download [itmux v1.1.0](https://github.com/itefixnet/itmux/releases/download/v1.1.0/itmux_1.1.0_x64_free.zip)
48
+ 2. Extract to one of these locations:
49
+ - `C:\itmux` (recommended)
50
+ - `%USERPROFILE%\itmux`
51
+ - Or set `ITMUX_HOME` environment variable
52
+ 3. Verify: `C:\itmux\bin\bash.exe` should exist
53
+ 4. Install CLI: `npm install -g sessioncast-cli`
54
+
29
55
  ## Quick Start
30
56
 
31
57
  1. **Get your agent token** from [app.sessioncast.io](https://app.sessioncast.io)
@@ -36,10 +36,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AgentRunner = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
39
40
  const yaml = __importStar(require("js-yaml"));
40
41
  const session_handler_1 = require("./session-handler");
41
42
  const api_client_1 = require("./api-client");
42
43
  const tmux = __importStar(require("./tmux"));
44
+ const config_1 = require("../config");
43
45
  const SCAN_INTERVAL_MS = 5000;
44
46
  class AgentRunner {
45
47
  constructor(config) {
@@ -50,6 +52,8 @@ class AgentRunner {
50
52
  this.config = config;
51
53
  }
52
54
  static loadConfig(configPath) {
55
+ // Check if agent token is available (for relay connection)
56
+ const agentToken = (0, config_1.getAgentToken)();
53
57
  // Check environment variable
54
58
  const envPath = process.env.SESSIONCAST_CONFIG || process.env.TMUX_REMOTE_CONFIG;
55
59
  // Try multiple default paths
@@ -66,33 +70,32 @@ class AgentRunner {
66
70
  }
67
71
  }
68
72
  }
69
- // If yml file found, load from file
70
- if (finalPath && fs.existsSync(finalPath)) {
71
- console.log(`Loading config from: ${finalPath}`);
72
- const content = fs.readFileSync(finalPath, 'utf-8');
73
- const ext = path.extname(finalPath).toLowerCase();
74
- if (ext === '.json') {
75
- return JSON.parse(content);
76
- }
77
- else {
78
- return yaml.load(content);
79
- }
80
- }
81
- // SaaS mode: load from conf store (after sessioncast login)
82
- const config_1 = require("../config");
83
- const agentToken = (0, config_1.getAgentToken)();
84
- const relayUrl = (0, config_1.getRelayUrl)();
85
- const machineId = (0, config_1.getMachineId)();
86
- if (agentToken) {
87
- const os = require("os");
88
- console.log('Loading config from login credentials (SaaS mode)');
73
+ // If no config file found but agent token exists, create default config
74
+ if ((!finalPath || !fs.existsSync(finalPath)) && agentToken) {
75
+ console.log('Using OAuth authentication');
76
+ const hostname = os.hostname();
77
+ const machineId = `${hostname}-${Date.now()}`;
89
78
  return {
90
- machineId: machineId || os.hostname(),
91
- relay: relayUrl || 'wss://relay.sessioncast.io/ws',
79
+ machineId,
80
+ relay: (0, config_1.getRelayUrl)(),
92
81
  token: agentToken,
82
+ api: {
83
+ enabled: false
84
+ }
93
85
  };
94
86
  }
95
- throw new Error('No config found. Run "sessioncast login" first, or create ~/.sessioncast.yml');
87
+ if (!finalPath || !fs.existsSync(finalPath)) {
88
+ throw new Error(`Config file not found. Tried: ${configPath || envPath || defaultPaths.join(', ')}`);
89
+ }
90
+ console.log(`Loading config from: ${finalPath}`);
91
+ const content = fs.readFileSync(finalPath, 'utf-8');
92
+ const ext = path.extname(finalPath).toLowerCase();
93
+ if (ext === '.json') {
94
+ return JSON.parse(content);
95
+ }
96
+ else {
97
+ return yaml.load(content);
98
+ }
96
99
  }
97
100
  async start() {
98
101
  if (this.running)
@@ -11,15 +11,45 @@ export declare class TmuxSessionHandler {
11
11
  private wsClient;
12
12
  private onCreateSession?;
13
13
  private running;
14
- private lastScreen;
14
+ private lastScreens;
15
+ private lastPaneLayoutJson;
15
16
  private lastForceSendTime;
16
17
  private lastChangeTime;
17
18
  private captureTimer;
19
+ private pendingUploads;
20
+ private uploadDir;
18
21
  constructor(options: SessionHandlerOptions);
19
22
  start(): void;
20
23
  private connectAndRun;
24
+ /**
25
+ * Handle file view request from web client
26
+ */
27
+ private handleRequestFileView;
28
+ /**
29
+ * Handle incoming file upload chunk
30
+ */
31
+ private handleUploadChunk;
32
+ /**
33
+ * Complete file upload by assembling chunks and writing to disk
34
+ */
35
+ private completeUpload;
36
+ /**
37
+ * Get upload directory (try to get tmux pane's current working directory)
38
+ */
39
+ private getUploadDirectory;
40
+ /**
41
+ * Sanitize filename to prevent path traversal attacks
42
+ */
43
+ private sanitizeFilename;
44
+ /**
45
+ * Get unique file path if file already exists
46
+ */
47
+ private getUniqueFilePath;
48
+ /**
49
+ * Get content type from file extension
50
+ */
51
+ private getContentType;
21
52
  private handleKeys;
22
- private handleFileViewRequest;
23
53
  private startScreenCapture;
24
54
  private stopScreenCapture;
25
55
  stop(): void;
@@ -38,26 +38,6 @@ const websocket_1 = require("./websocket");
38
38
  const tmux = __importStar(require("./tmux"));
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
- const os = __importStar(require("os"));
42
- function expandPath(filePath) {
43
- if (filePath.startsWith('~/')) {
44
- return path.join(os.homedir(), filePath.slice(2));
45
- }
46
- return filePath;
47
- }
48
- function getLanguage(filePath) {
49
- const ext = path.extname(filePath).toLowerCase();
50
- const langMap = {
51
- '.js': 'javascript', '.ts': 'typescript', '.tsx': 'typescript',
52
- '.jsx': 'javascript', '.json': 'json', '.md': 'markdown',
53
- '.py': 'python', '.java': 'java', '.sh': 'shell',
54
- '.css': 'css', '.html': 'html', '.yaml': 'yaml', '.yml': 'yaml',
55
- '.kt': 'kotlin', '.swift': 'swift', '.go': 'go', '.rs': 'rust',
56
- '.c': 'c', '.cpp': 'cpp', '.h': 'c', '.hpp': 'cpp',
57
- '.rb': 'ruby', '.php': 'php', '.sql': 'sql', '.xml': 'xml',
58
- };
59
- return langMap[ext] || 'text';
60
- }
61
41
  // Capture intervals
62
42
  const CAPTURE_INTERVAL_ACTIVE_MS = 50;
63
43
  const CAPTURE_INTERVAL_IDLE_MS = 200;
@@ -69,10 +49,14 @@ class TmuxSessionHandler {
69
49
  constructor(options) {
70
50
  this.wsClient = null;
71
51
  this.running = false;
72
- this.lastScreen = '';
52
+ this.lastScreens = new Map();
53
+ this.lastPaneLayoutJson = '';
73
54
  this.lastForceSendTime = 0;
74
55
  this.lastChangeTime = 0;
75
56
  this.captureTimer = null;
57
+ // File upload handling
58
+ this.pendingUploads = new Map();
59
+ this.uploadDir = process.cwd(); // Default to current working directory
76
60
  this.config = options.config;
77
61
  this.tmuxSession = options.tmuxSession;
78
62
  this.sessionId = `${options.config.machineId}/${options.tmuxSession}`;
@@ -100,14 +84,16 @@ class TmuxSessionHandler {
100
84
  });
101
85
  this.wsClient.on('connected', () => {
102
86
  console.log(`[${this.tmuxSession}] Connected to relay`);
87
+ // Reset pane layout cache so it gets re-sent on reconnection
88
+ this.lastPaneLayoutJson = '';
103
89
  this.startScreenCapture();
104
90
  });
105
91
  this.wsClient.on('disconnected', ({ code, reason }) => {
106
92
  console.log(`[${this.tmuxSession}] Disconnected: code=${code}, reason=${reason}`);
107
93
  this.stopScreenCapture();
108
94
  });
109
- this.wsClient.on('keys', (keys) => {
110
- this.handleKeys(keys);
95
+ this.wsClient.on('keys', (keys, meta) => {
96
+ this.handleKeys(keys, meta);
111
97
  });
112
98
  this.wsClient.on('resize', ({ cols, rows }) => {
113
99
  console.log(`[${this.tmuxSession}] Resize: ${cols}x${rows}`);
@@ -125,33 +111,181 @@ class TmuxSessionHandler {
125
111
  this.stop();
126
112
  });
127
113
  this.wsClient.on('requestFileView', (filePath) => {
128
- console.log(`[${this.tmuxSession}] File view request: ${filePath}`);
129
- this.handleFileViewRequest(filePath);
114
+ this.handleRequestFileView(filePath);
115
+ });
116
+ this.wsClient.on('uploadFile', (chunk) => {
117
+ this.handleUploadChunk(chunk);
130
118
  });
131
119
  this.wsClient.on('error', (error) => {
132
120
  console.error(`[${this.tmuxSession}] WebSocket error:`, error.message);
133
121
  });
134
122
  this.wsClient.connect();
135
123
  }
136
- handleKeys(keys) {
137
- tmux.sendKeys(this.tmuxSession, keys, false);
124
+ /**
125
+ * Handle file view request from web client
126
+ */
127
+ handleRequestFileView(filePath) {
128
+ console.log(`[${this.tmuxSession}] File view request: ${filePath}`);
129
+ try {
130
+ // Resolve file path (could be relative or absolute)
131
+ const resolvedPath = path.isAbsolute(filePath)
132
+ ? filePath
133
+ : path.resolve(this.uploadDir, filePath);
134
+ if (!fs.existsSync(resolvedPath)) {
135
+ console.log(`[${this.tmuxSession}] File not found: ${resolvedPath}`);
136
+ return;
137
+ }
138
+ const stat = fs.statSync(resolvedPath);
139
+ if (!stat.isFile()) {
140
+ console.log(`[${this.tmuxSession}] Not a file: ${resolvedPath}`);
141
+ return;
142
+ }
143
+ // Read file and send to viewer
144
+ const content = fs.readFileSync(resolvedPath);
145
+ const base64Content = content.toString('base64');
146
+ const filename = path.basename(resolvedPath);
147
+ const ext = path.extname(filename).toLowerCase();
148
+ // Determine content type
149
+ const contentType = this.getContentType(ext);
150
+ this.wsClient?.sendFileView(filename, base64Content, contentType, resolvedPath);
151
+ console.log(`[${this.tmuxSession}] Sent file view: ${filename}`);
152
+ }
153
+ catch (error) {
154
+ console.error(`[${this.tmuxSession}] Error reading file:`, error);
155
+ }
138
156
  }
139
- handleFileViewRequest(filePath) {
140
- if (!this.wsClient)
141
- return;
142
- const realPath = expandPath(filePath);
143
- const language = getLanguage(filePath);
157
+ /**
158
+ * Handle incoming file upload chunk
159
+ */
160
+ handleUploadChunk(chunk) {
161
+ const { filename, size, chunkIndex, totalChunks, payload } = chunk;
162
+ console.log(`[${this.tmuxSession}] Upload chunk: ${filename} (${chunkIndex + 1}/${totalChunks})`);
163
+ // Sanitize filename to prevent path traversal
164
+ const safeFilename = this.sanitizeFilename(filename);
165
+ const uploadKey = `${safeFilename}_${size}`;
166
+ // Get or create pending upload
167
+ let pending = this.pendingUploads.get(uploadKey);
168
+ if (!pending) {
169
+ pending = {
170
+ filename: safeFilename,
171
+ size,
172
+ chunks: new Map(),
173
+ totalChunks,
174
+ receivedAt: Date.now()
175
+ };
176
+ this.pendingUploads.set(uploadKey, pending);
177
+ }
178
+ // Store chunk
179
+ pending.chunks.set(chunkIndex, payload);
180
+ // Check if all chunks received
181
+ if (pending.chunks.size === totalChunks) {
182
+ this.completeUpload(uploadKey, pending);
183
+ }
184
+ }
185
+ /**
186
+ * Complete file upload by assembling chunks and writing to disk
187
+ */
188
+ completeUpload(uploadKey, pending) {
144
189
  try {
145
- const content = fs.readFileSync(realPath, 'utf-8');
146
- console.log(`[${this.tmuxSession}] File read: ${content.length} bytes`);
147
- this.wsClient.sendFileView(filePath, content, language);
190
+ // Assemble chunks in order
191
+ const chunks = [];
192
+ for (let i = 0; i < pending.totalChunks; i++) {
193
+ const chunkData = pending.chunks.get(i);
194
+ if (!chunkData) {
195
+ throw new Error(`Missing chunk ${i}`);
196
+ }
197
+ chunks.push(Buffer.from(chunkData, 'base64'));
198
+ }
199
+ const fileBuffer = Buffer.concat(chunks);
200
+ // Determine upload directory (try to get tmux pane's cwd)
201
+ const uploadPath = this.getUploadDirectory();
202
+ const filePath = path.join(uploadPath, pending.filename);
203
+ // Handle filename conflicts
204
+ const finalPath = this.getUniqueFilePath(filePath);
205
+ // Write file
206
+ fs.writeFileSync(finalPath, fileBuffer);
207
+ console.log(`[${this.tmuxSession}] File uploaded: ${finalPath}`);
208
+ // Send success notification
209
+ this.wsClient?.sendUploadComplete(pending.filename, finalPath);
210
+ // Cleanup
211
+ this.pendingUploads.delete(uploadKey);
148
212
  }
149
- catch (err) {
150
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
151
- console.error(`[${this.tmuxSession}] File read error: ${errorMessage}`);
152
- this.wsClient.sendFileView(filePath, `Error: ${errorMessage}`, 'text', errorMessage);
213
+ catch (error) {
214
+ console.error(`[${this.tmuxSession}] Upload failed:`, error);
215
+ this.wsClient?.sendUploadError(pending.filename, error.message);
216
+ this.pendingUploads.delete(uploadKey);
153
217
  }
154
218
  }
219
+ /**
220
+ * Get upload directory (try to get tmux pane's current working directory)
221
+ */
222
+ getUploadDirectory() {
223
+ try {
224
+ // Try to get pane's current directory from tmux
225
+ const paneId = tmux.getActivePane(this.tmuxSession);
226
+ if (paneId) {
227
+ const cwd = tmux.getPaneCwd(this.tmuxSession, paneId);
228
+ if (cwd && fs.existsSync(cwd)) {
229
+ return cwd;
230
+ }
231
+ }
232
+ }
233
+ catch {
234
+ // Ignore errors, fall back to default
235
+ }
236
+ return this.uploadDir;
237
+ }
238
+ /**
239
+ * Sanitize filename to prevent path traversal attacks
240
+ */
241
+ sanitizeFilename(filename) {
242
+ // Remove path separators and dangerous characters
243
+ return path.basename(filename).replace(/[<>:"|?*\x00-\x1f]/g, '_');
244
+ }
245
+ /**
246
+ * Get unique file path if file already exists
247
+ */
248
+ getUniqueFilePath(filePath) {
249
+ if (!fs.existsSync(filePath)) {
250
+ return filePath;
251
+ }
252
+ const dir = path.dirname(filePath);
253
+ const ext = path.extname(filePath);
254
+ const name = path.basename(filePath, ext);
255
+ let counter = 1;
256
+ let newPath;
257
+ do {
258
+ newPath = path.join(dir, `${name} (${counter})${ext}`);
259
+ counter++;
260
+ } while (fs.existsSync(newPath) && counter < 1000);
261
+ return newPath;
262
+ }
263
+ /**
264
+ * Get content type from file extension
265
+ */
266
+ getContentType(ext) {
267
+ const types = {
268
+ '.txt': 'text/plain',
269
+ '.md': 'text/markdown',
270
+ '.html': 'text/html',
271
+ '.css': 'text/css',
272
+ '.js': 'text/javascript',
273
+ '.ts': 'text/typescript',
274
+ '.json': 'application/json',
275
+ '.xml': 'application/xml',
276
+ '.png': 'image/png',
277
+ '.jpg': 'image/jpeg',
278
+ '.jpeg': 'image/jpeg',
279
+ '.gif': 'image/gif',
280
+ '.svg': 'image/svg+xml',
281
+ '.pdf': 'application/pdf',
282
+ };
283
+ return types[ext] || 'application/octet-stream';
284
+ }
285
+ handleKeys(keys, meta) {
286
+ const target = meta?.pane ? `${this.tmuxSession}.${meta.pane}` : this.tmuxSession;
287
+ tmux.sendKeys(target, keys, false);
288
+ }
155
289
  startScreenCapture() {
156
290
  if (this.captureTimer)
157
291
  return;
@@ -162,43 +296,86 @@ class TmuxSessionHandler {
162
296
  return;
163
297
  }
164
298
  try {
165
- const screen = tmux.capturePane(this.tmuxSession);
166
- if (screen !== null) {
167
- const now = Date.now();
168
- const changed = screen !== this.lastScreen;
169
- const forceTime = (now - this.lastForceSendTime) >= FORCE_SEND_INTERVAL_MS;
170
- if (changed || forceTime) {
171
- this.lastScreen = screen;
172
- this.lastForceSendTime = now;
173
- if (changed) {
174
- this.lastChangeTime = now;
175
- }
176
- // Send clear screen + content
177
- const fullOutput = '\x1b[2J\x1b[H' + screen;
178
- const data = Buffer.from(fullOutput, 'utf-8');
179
- // Compress if enabled and data is large enough
180
- if (USE_COMPRESSION && data.length > MIN_COMPRESS_SIZE) {
181
- this.wsClient.sendScreenCompressed(data);
299
+ const panes = tmux.listPanes(this.tmuxSession);
300
+ const now = Date.now();
301
+ const isMultiPane = panes.length > 1;
302
+ // Check if pane layout changed
303
+ const paneLayoutJson = JSON.stringify(panes);
304
+ if (paneLayoutJson !== this.lastPaneLayoutJson) {
305
+ this.lastPaneLayoutJson = paneLayoutJson;
306
+ this.wsClient.sendPaneLayout(panes);
307
+ // Clean up screens for removed panes
308
+ const currentPaneIds = new Set(panes.map(p => p.id));
309
+ for (const key of this.lastScreens.keys()) {
310
+ if (!currentPaneIds.has(key)) {
311
+ this.lastScreens.delete(key);
182
312
  }
183
- else {
184
- this.wsClient.sendScreen(data);
313
+ }
314
+ }
315
+ let anyChanged = false;
316
+ const forceTime = (now - this.lastForceSendTime) >= FORCE_SEND_INTERVAL_MS;
317
+ if (isMultiPane) {
318
+ // Multi-pane: capture each pane individually
319
+ for (const pane of panes) {
320
+ const screen = tmux.capturePane(this.tmuxSession, pane.id);
321
+ if (screen !== null) {
322
+ const lastScreen = this.lastScreens.get(pane.id) || '';
323
+ const changed = screen !== lastScreen;
324
+ if (changed || forceTime) {
325
+ this.lastScreens.set(pane.id, screen);
326
+ if (changed)
327
+ anyChanged = true;
328
+ const fullOutput = '\x1b[2J\x1b[H' + screen;
329
+ const data = Buffer.from(fullOutput, 'utf-8');
330
+ const paneMeta = { pane: pane.id, index: pane.index };
331
+ if (USE_COMPRESSION && data.length > MIN_COMPRESS_SIZE) {
332
+ this.wsClient.sendScreenCompressed(data, paneMeta);
333
+ }
334
+ else {
335
+ this.wsClient.sendScreen(data, paneMeta);
336
+ }
337
+ }
185
338
  }
186
339
  }
187
- // Adaptive sleep: faster when active, slower when idle
188
- const isActive = (now - this.lastChangeTime) < ACTIVE_THRESHOLD_MS;
189
- const sleepMs = isActive ? CAPTURE_INTERVAL_ACTIVE_MS : CAPTURE_INTERVAL_IDLE_MS;
190
- this.captureTimer = setTimeout(capture, sleepMs);
191
340
  }
192
341
  else {
193
- this.captureTimer = setTimeout(capture, CAPTURE_INTERVAL_IDLE_MS);
342
+ // Single pane: backward compatible (no meta)
343
+ const screen = tmux.capturePane(this.tmuxSession);
344
+ if (screen !== null) {
345
+ const lastScreen = this.lastScreens.get('_single') || '';
346
+ const changed = screen !== lastScreen;
347
+ if (changed || forceTime) {
348
+ this.lastScreens.set('_single', screen);
349
+ if (changed)
350
+ anyChanged = true;
351
+ const fullOutput = '\x1b[2J\x1b[H' + screen;
352
+ const data = Buffer.from(fullOutput, 'utf-8');
353
+ if (USE_COMPRESSION && data.length > MIN_COMPRESS_SIZE) {
354
+ this.wsClient.sendScreenCompressed(data);
355
+ }
356
+ else {
357
+ this.wsClient.sendScreen(data);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ if (anyChanged || forceTime) {
363
+ this.lastForceSendTime = now;
364
+ if (anyChanged) {
365
+ this.lastChangeTime = now;
366
+ }
194
367
  }
368
+ const isActive = (now - this.lastChangeTime) < ACTIVE_THRESHOLD_MS;
369
+ const sleepMs = isActive ? CAPTURE_INTERVAL_ACTIVE_MS : CAPTURE_INTERVAL_IDLE_MS;
370
+ this.captureTimer = setTimeout(capture, sleepMs);
195
371
  }
196
372
  catch (error) {
197
373
  console.error(`[${this.tmuxSession}] Screen capture error:`, error);
198
374
  this.captureTimer = setTimeout(capture, 500);
199
375
  }
200
376
  };
201
- capture();
377
+ // Small delay to ensure register message is processed by server first
378
+ setTimeout(capture, 300);
202
379
  console.log(`[${this.tmuxSession}] Screen capture started`);
203
380
  }
204
381
  stopScreenCapture() {
@@ -4,7 +4,17 @@
4
4
  */
5
5
  export interface TmuxExecutor {
6
6
  listSessions(): string[];
7
- capturePane(session: string): string | null;
7
+ listPanes(session: string): {
8
+ id: string;
9
+ index: number;
10
+ width: number;
11
+ height: number;
12
+ top: number;
13
+ left: number;
14
+ active: boolean;
15
+ title: string;
16
+ }[];
17
+ capturePane(session: string, paneId?: string): string | null;
8
18
  sendKeys(session: string, keys: string): boolean;
9
19
  sendSpecialKey(session: string, key: string): boolean;
10
20
  resizeWindow(session: string, cols: number, rows: number): boolean;
@@ -12,13 +22,25 @@ export interface TmuxExecutor {
12
22
  createSession(session: string, workingDir?: string): boolean;
13
23
  isAvailable(): boolean;
14
24
  getVersion(): string | null;
25
+ getActivePane(session: string): string | null;
26
+ getPaneCwd(session: string, paneId?: string): string | null;
15
27
  }
16
28
  /**
17
29
  * Unix/Linux/macOS implementation of TmuxExecutor.
18
30
  */
19
31
  export declare class UnixTmuxExecutor implements TmuxExecutor {
20
32
  listSessions(): string[];
21
- capturePane(session: string): string | null;
33
+ listPanes(session: string): {
34
+ id: string;
35
+ index: number;
36
+ width: number;
37
+ height: number;
38
+ top: number;
39
+ left: number;
40
+ active: boolean;
41
+ title: string;
42
+ }[];
43
+ capturePane(session: string, paneId?: string): string | null;
22
44
  sendKeys(session: string, keys: string): boolean;
23
45
  sendSpecialKey(session: string, key: string): boolean;
24
46
  resizeWindow(session: string, cols: number, rows: number): boolean;
@@ -26,6 +48,8 @@ export declare class UnixTmuxExecutor implements TmuxExecutor {
26
48
  createSession(session: string, workingDir?: string): boolean;
27
49
  isAvailable(): boolean;
28
50
  getVersion(): string | null;
51
+ getActivePane(session: string): string | null;
52
+ getPaneCwd(session: string, paneId?: string): string | null;
29
53
  private escapeForShell;
30
54
  }
31
55
  /**
@@ -37,7 +61,17 @@ export declare class WindowsTmuxExecutor implements TmuxExecutor {
37
61
  constructor(itmuxPath: string);
38
62
  private executeCommand;
39
63
  listSessions(): string[];
40
- capturePane(session: string): string | null;
64
+ listPanes(session: string): {
65
+ id: string;
66
+ index: number;
67
+ width: number;
68
+ height: number;
69
+ top: number;
70
+ left: number;
71
+ active: boolean;
72
+ title: string;
73
+ }[];
74
+ capturePane(session: string, paneId?: string): string | null;
41
75
  sendKeys(session: string, keys: string): boolean;
42
76
  sendSpecialKey(session: string, key: string): boolean;
43
77
  resizeWindow(session: string, cols: number, rows: number): boolean;
@@ -45,6 +79,8 @@ export declare class WindowsTmuxExecutor implements TmuxExecutor {
45
79
  createSession(session: string, workingDir?: string): boolean;
46
80
  isAvailable(): boolean;
47
81
  getVersion(): string | null;
82
+ getActivePane(session: string): string | null;
83
+ getPaneCwd(session: string, paneId?: string): string | null;
48
84
  private escapeSession;
49
85
  private windowsToCygwinPath;
50
86
  /**
@@ -60,9 +60,31 @@ class UnixTmuxExecutor {
60
60
  return [];
61
61
  }
62
62
  }
63
- capturePane(session) {
63
+ listPanes(session) {
64
64
  try {
65
- const output = (0, child_process_1.execSync)(`tmux capture-pane -t "${session}" -p -e -N`, {
65
+ 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}:#{pane_title}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
66
+ return output.trim().split('\n').filter(l => l.length > 0).map(line => {
67
+ const parts = line.split(':');
68
+ return {
69
+ id: parts[0],
70
+ index: parseInt(parts[1], 10),
71
+ width: parseInt(parts[2], 10),
72
+ height: parseInt(parts[3], 10),
73
+ top: parseInt(parts[4], 10),
74
+ left: parseInt(parts[5], 10),
75
+ active: parts[6] === '1',
76
+ title: parts.slice(7).join(':') // title may contain colons
77
+ };
78
+ });
79
+ }
80
+ catch {
81
+ return [];
82
+ }
83
+ }
84
+ capturePane(session, paneId) {
85
+ try {
86
+ const target = paneId ? `${session}.${paneId}` : session;
87
+ const output = (0, child_process_1.execSync)(`tmux capture-pane -t "${target}" -p -e -N`, {
66
88
  encoding: 'utf-8',
67
89
  stdio: ['pipe', 'pipe', 'pipe'],
68
90
  maxBuffer: 10 * 1024 * 1024
@@ -143,6 +165,31 @@ class UnixTmuxExecutor {
143
165
  return null;
144
166
  }
145
167
  }
168
+ getActivePane(session) {
169
+ try {
170
+ const output = (0, child_process_1.execSync)(`tmux display-message -t "${session}" -p "#{pane_id}"`, {
171
+ encoding: 'utf-8',
172
+ stdio: ['pipe', 'pipe', 'pipe']
173
+ });
174
+ return output.trim() || null;
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ }
180
+ getPaneCwd(session, paneId) {
181
+ try {
182
+ const target = paneId ? `${session}:${paneId}` : session;
183
+ const output = (0, child_process_1.execSync)(`tmux display-message -t "${target}" -p "#{pane_current_path}"`, {
184
+ encoding: 'utf-8',
185
+ stdio: ['pipe', 'pipe', 'pipe']
186
+ });
187
+ return output.trim() || null;
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
146
193
  escapeForShell(str) {
147
194
  return str.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
148
195
  }
@@ -198,9 +245,34 @@ class WindowsTmuxExecutor {
198
245
  return [];
199
246
  }
200
247
  }
201
- capturePane(session) {
248
+ listPanes(session) {
202
249
  try {
203
250
  const escaped = this.escapeSession(session);
251
+ const output = this.executeCommand(`tmux list-panes -t '${escaped}' -F '#{pane_id}:#{pane_index}:#{pane_width}:#{pane_height}:#{pane_top}:#{pane_left}:#{pane_active}:#{pane_title}'`);
252
+ if (!output)
253
+ return [];
254
+ return output.split('\n').map(s => s.trim()).filter(l => l.length > 0).map(line => {
255
+ const parts = line.split(':');
256
+ return {
257
+ id: parts[0],
258
+ index: parseInt(parts[1], 10),
259
+ width: parseInt(parts[2], 10),
260
+ height: parseInt(parts[3], 10),
261
+ top: parseInt(parts[4], 10),
262
+ left: parseInt(parts[5], 10),
263
+ active: parts[6] === '1',
264
+ title: parts.slice(7).join(':')
265
+ };
266
+ });
267
+ }
268
+ catch {
269
+ return [];
270
+ }
271
+ }
272
+ capturePane(session, paneId) {
273
+ try {
274
+ const target = paneId ? `${session}.${paneId}` : session;
275
+ const escaped = this.escapeSession(target);
204
276
  const output = this.executeCommand(`tmux capture-pane -t '${escaped}' -p -e -N`);
205
277
  if (!output)
206
278
  return null;
@@ -289,6 +361,27 @@ class WindowsTmuxExecutor {
289
361
  return null;
290
362
  }
291
363
  }
364
+ getActivePane(session) {
365
+ try {
366
+ const escaped = this.escapeSession(session);
367
+ const output = this.executeCommand(`tmux display-message -t '${escaped}' -p "#{pane_id}"`);
368
+ return output.trim() || null;
369
+ }
370
+ catch {
371
+ return null;
372
+ }
373
+ }
374
+ getPaneCwd(session, paneId) {
375
+ try {
376
+ const escapedSession = this.escapeSession(session);
377
+ const target = paneId ? `${escapedSession}:${paneId}` : escapedSession;
378
+ const output = this.executeCommand(`tmux display-message -t '${target}' -p "#{pane_current_path}"`);
379
+ return output.trim() || null;
380
+ }
381
+ catch {
382
+ return null;
383
+ }
384
+ }
292
385
  escapeSession(session) {
293
386
  return session.replace(/'/g, "'\\''");
294
387
  }
@@ -342,8 +435,21 @@ function createTmuxExecutor() {
342
435
  if (isWindows) {
343
436
  const itmuxPath = WindowsTmuxExecutor.findItmuxPath();
344
437
  if (!itmuxPath) {
345
- throw new Error('itmux not found. Please install itmux from https://github.com/itefixnet/itmux\n' +
346
- 'Set ITMUX_HOME environment variable or place itmux in a standard location.');
438
+ throw new Error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
439
+ ' itmux not found - Windows tmux package required\n' +
440
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' +
441
+ ' Download: https://github.com/itefixnet/itmux/releases/latest\n' +
442
+ ' Latest: https://github.com/itefixnet/itmux/releases/download/v1.1.0/itmux_1.1.0_x64_free.zip\n\n' +
443
+ ' Installation:\n' +
444
+ ' 1. Download and extract itmux_1.1.0_x64_free.zip\n' +
445
+ ' 2. Place in one of these locations:\n' +
446
+ ' • C:\\itmux\n' +
447
+ ' • %USERPROFILE%\\itmux\n' +
448
+ ' • Or set ITMUX_HOME environment variable\n\n' +
449
+ ' Quick install (PowerShell):\n' +
450
+ ' Invoke-WebRequest -Uri "https://github.com/itefixnet/itmux/releases/download/v1.1.0/itmux_1.1.0_x64_free.zip" -OutFile "$env:TEMP\\itmux.zip"\n' +
451
+ ' Expand-Archive -Path "$env:TEMP\\itmux.zip" -DestinationPath "C:\\itmux" -Force\n\n' +
452
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
347
453
  }
348
454
  return new WindowsTmuxExecutor(itmuxPath);
349
455
  }
@@ -1,4 +1,4 @@
1
- import { TmuxSession } from './types';
1
+ import { TmuxSession, PaneInfo } from './types';
2
2
  /**
3
3
  * Scan for all tmux sessions
4
4
  */
@@ -7,10 +7,14 @@ export declare function scanSessions(): string[];
7
7
  * Get detailed session info
8
8
  */
9
9
  export declare function listSessions(): TmuxSession[];
10
+ /**
11
+ * List all panes in a tmux session
12
+ */
13
+ export declare function listPanes(sessionName: string): PaneInfo[];
10
14
  /**
11
15
  * Capture tmux pane content with escape sequences (colors)
12
16
  */
13
- export declare function capturePane(sessionName: string): string | null;
17
+ export declare function capturePane(sessionName: string, paneId?: string): string | null;
14
18
  /**
15
19
  * Send keys to tmux session
16
20
  */
@@ -35,3 +39,11 @@ export declare function isAvailable(): boolean;
35
39
  * Get tmux version
36
40
  */
37
41
  export declare function getVersion(): string | null;
42
+ /**
43
+ * Get the active pane ID in a session
44
+ */
45
+ export declare function getActivePane(sessionName: string): string | null;
46
+ /**
47
+ * Get the current working directory of a pane
48
+ */
49
+ export declare function getPaneCwd(sessionName: string, paneId?: string): string | null;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.scanSessions = scanSessions;
4
4
  exports.listSessions = listSessions;
5
+ exports.listPanes = listPanes;
5
6
  exports.capturePane = capturePane;
6
7
  exports.sendKeys = sendKeys;
7
8
  exports.resizeWindow = resizeWindow;
@@ -9,6 +10,8 @@ exports.createSession = createSession;
9
10
  exports.killSession = killSession;
10
11
  exports.isAvailable = isAvailable;
11
12
  exports.getVersion = getVersion;
13
+ exports.getActivePane = getActivePane;
14
+ exports.getPaneCwd = getPaneCwd;
12
15
  const tmux_executor_1 = require("./tmux-executor");
13
16
  // Lazy-initialized executor (created on first use)
14
17
  let executor = null;
@@ -47,11 +50,17 @@ function listSessions() {
47
50
  return [];
48
51
  }
49
52
  }
53
+ /**
54
+ * List all panes in a tmux session
55
+ */
56
+ function listPanes(sessionName) {
57
+ return getExecutor().listPanes(sessionName);
58
+ }
50
59
  /**
51
60
  * Capture tmux pane content with escape sequences (colors)
52
61
  */
53
- function capturePane(sessionName) {
54
- return getExecutor().capturePane(sessionName);
62
+ function capturePane(sessionName, paneId) {
63
+ return getExecutor().capturePane(sessionName, paneId);
55
64
  }
56
65
  /**
57
66
  * Send keys to tmux session
@@ -131,3 +140,15 @@ function getVersion() {
131
140
  return null;
132
141
  }
133
142
  }
143
+ /**
144
+ * Get the active pane ID in a session
145
+ */
146
+ function getActivePane(sessionName) {
147
+ return getExecutor().getActivePane(sessionName);
148
+ }
149
+ /**
150
+ * Get the current working directory of a pane
151
+ */
152
+ function getPaneCwd(sessionName, paneId) {
153
+ return getExecutor().getPaneCwd(sessionName, paneId);
154
+ }
@@ -37,6 +37,16 @@ export interface TmuxSession {
37
37
  created?: string;
38
38
  attached: boolean;
39
39
  }
40
+ export interface PaneInfo {
41
+ id: string;
42
+ index: number;
43
+ width: number;
44
+ height: number;
45
+ top: number;
46
+ left: number;
47
+ active: boolean;
48
+ title: string;
49
+ }
40
50
  export interface ExecResult {
41
51
  exitCode: number;
42
52
  stdout: string;
@@ -26,13 +26,43 @@ export declare class RelayWebSocketClient extends EventEmitter {
26
26
  connect(): void;
27
27
  private registerAsHost;
28
28
  private handleMessage;
29
- private handleFileUpload;
30
29
  private handleError;
31
30
  private scheduleReconnect;
32
31
  send(message: Message): boolean;
33
- sendScreen(data: Buffer): boolean;
34
- sendScreenCompressed(data: Buffer): boolean;
35
- sendFileView(filePath: string, content: string, language: string, error?: string): boolean;
32
+ sendScreen(data: Buffer, paneMeta?: {
33
+ pane: string;
34
+ index: number;
35
+ }): boolean;
36
+ sendScreenCompressed(data: Buffer, paneMeta?: {
37
+ pane: string;
38
+ index: number;
39
+ }): boolean;
40
+ sendPaneLayout(panes: {
41
+ id: string;
42
+ index: number;
43
+ width: number;
44
+ height: number;
45
+ top: number;
46
+ left: number;
47
+ active: boolean;
48
+ title: string;
49
+ }[]): boolean;
50
+ /**
51
+ * Send file content to be displayed in the web FileViewer
52
+ * @param filename - The name of the file
53
+ * @param content - The file content (UTF-8 for text, base64 for images)
54
+ * @param contentType - MIME type (e.g., 'text/markdown', 'text/html', 'image/png')
55
+ * @param path - Optional file path
56
+ */
57
+ sendFileView(filename: string, content: string, contentType: string, path?: string): boolean;
58
+ /**
59
+ * Send upload complete notification to web viewer
60
+ */
61
+ sendUploadComplete(filename: string, path: string): boolean;
62
+ /**
63
+ * Send upload error notification to web viewer
64
+ */
65
+ sendUploadError(filename: string, error: string): boolean;
36
66
  getConnected(): boolean;
37
67
  destroy(): void;
38
68
  }
@@ -40,7 +40,6 @@ 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 fileUtils_1 = require("../utils/fileUtils");
44
43
  const MAX_RECONNECT_ATTEMPTS = 5;
45
44
  const BASE_RECONNECT_DELAY_MS = 2000;
46
45
  const MAX_RECONNECT_DELAY_MS = 60000;
@@ -71,8 +70,8 @@ class RelayWebSocketClient extends events_1.EventEmitter {
71
70
  this.isConnected = true;
72
71
  this.reconnectAttempts = 0;
73
72
  this.circuitBreakerOpen = false;
74
- this.emit('connected');
75
73
  this.registerAsHost();
74
+ this.emit('connected');
76
75
  });
77
76
  this.ws.on('message', (data) => {
78
77
  try {
@@ -117,14 +116,10 @@ class RelayWebSocketClient extends events_1.EventEmitter {
117
116
  });
118
117
  }
119
118
  handleMessage(message) {
120
- // Debug: log all incoming message types (except frequent ones)
121
- if (message.type !== 'pong' && message.type !== 'keys') {
122
- console.log(`[WS] Message received: type=${message.type}, session=${message.session}`);
123
- }
124
119
  switch (message.type) {
125
120
  case 'keys':
126
121
  if (message.session === this.sessionId && message.payload) {
127
- this.emit('keys', message.payload);
122
+ this.emit('keys', message.payload, message.meta);
128
123
  }
129
124
  break;
130
125
  case 'resize':
@@ -147,17 +142,20 @@ class RelayWebSocketClient extends events_1.EventEmitter {
147
142
  }
148
143
  break;
149
144
  case 'requestFileView':
150
- console.log(`[WS] requestFileView received: session=${message.session}, mySession=${this.sessionId}, filePath=${message.meta?.filePath}`);
151
145
  if (message.session === this.sessionId && message.meta?.filePath) {
152
146
  this.emit('requestFileView', message.meta.filePath);
153
147
  }
154
- else {
155
- console.log(`[WS] requestFileView ignored: sessionMatch=${message.session === this.sessionId}, hasFilePath=${!!message.meta?.filePath}`);
156
- }
157
148
  break;
158
149
  case 'uploadFile':
159
- if (message.session === this.sessionId && message.meta && message.payload) {
160
- this.handleFileUpload(message);
150
+ if (message.session === this.sessionId && message.payload && message.meta) {
151
+ this.emit('uploadFile', {
152
+ filename: message.meta.filename,
153
+ size: parseInt(message.meta.size || '0', 10),
154
+ mimeType: message.meta.mimeType,
155
+ chunkIndex: parseInt(message.meta.chunkIndex || '0', 10),
156
+ totalChunks: parseInt(message.meta.totalChunks || '1', 10),
157
+ payload: message.payload
158
+ });
161
159
  }
162
160
  break;
163
161
  case 'error':
@@ -167,55 +165,6 @@ class RelayWebSocketClient extends events_1.EventEmitter {
167
165
  this.emit('message', message);
168
166
  }
169
167
  }
170
- async handleFileUpload(message) {
171
- const meta = message.meta;
172
- if (!meta || !message.payload)
173
- return;
174
- try {
175
- const result = await (0, fileUtils_1.handleUploadChunk)(this.sessionId, {
176
- filename: meta.filename || 'unknown',
177
- size: meta.size || '0',
178
- mimeType: meta.mimeType || 'application/octet-stream',
179
- chunkIndex: meta.chunkIndex || '0',
180
- totalChunks: meta.totalChunks || '1',
181
- }, message.payload);
182
- // Only send response when upload is complete (result is not null)
183
- if (result) {
184
- if (result.success) {
185
- this.send({
186
- type: 'uploadComplete',
187
- session: this.sessionId,
188
- meta: {
189
- filename: meta.filename || 'unknown',
190
- path: result.path || '',
191
- success: 'true',
192
- },
193
- });
194
- }
195
- else {
196
- this.send({
197
- type: 'uploadError',
198
- session: this.sessionId,
199
- meta: {
200
- filename: meta.filename || 'unknown',
201
- error: result.error || 'Upload failed',
202
- },
203
- });
204
- }
205
- }
206
- }
207
- catch (e) {
208
- console.error('[WS] File upload error:', e);
209
- this.send({
210
- type: 'uploadError',
211
- session: this.sessionId,
212
- meta: {
213
- filename: meta.filename || 'unknown',
214
- error: e instanceof Error ? e.message : 'Upload failed',
215
- },
216
- });
217
- }
218
- }
219
168
  handleError(message) {
220
169
  const meta = message.meta;
221
170
  if (!meta)
@@ -290,47 +239,99 @@ class RelayWebSocketClient extends events_1.EventEmitter {
290
239
  return false;
291
240
  }
292
241
  }
293
- sendScreen(data) {
242
+ sendScreen(data, paneMeta) {
294
243
  if (!this.isConnected)
295
244
  return false;
296
245
  const base64Data = data.toString('base64');
297
- return this.send({
246
+ const msg = {
298
247
  type: 'screen',
299
248
  session: this.sessionId,
300
249
  payload: base64Data
301
- });
250
+ };
251
+ if (paneMeta) {
252
+ msg.meta = { pane: paneMeta.pane, index: String(paneMeta.index) };
253
+ }
254
+ return this.send(msg);
302
255
  }
303
- sendScreenCompressed(data) {
256
+ sendScreenCompressed(data, paneMeta) {
304
257
  if (!this.isConnected)
305
258
  return false;
306
259
  try {
307
260
  const compressed = zlib.gzipSync(data);
308
261
  const base64Data = compressed.toString('base64');
309
- return this.send({
262
+ const msg = {
310
263
  type: 'screenGz',
311
264
  session: this.sessionId,
312
265
  payload: base64Data
313
- });
266
+ };
267
+ if (paneMeta) {
268
+ msg.meta = { pane: paneMeta.pane, index: String(paneMeta.index) };
269
+ }
270
+ return this.send(msg);
314
271
  }
315
272
  catch {
316
- return this.sendScreen(data);
273
+ return this.sendScreen(data, paneMeta);
317
274
  }
318
275
  }
319
- sendFileView(filePath, content, language, error) {
276
+ sendPaneLayout(panes) {
277
+ if (!this.isConnected)
278
+ return false;
279
+ return this.send({
280
+ type: 'paneLayout',
281
+ session: this.sessionId,
282
+ payload: JSON.stringify(panes)
283
+ });
284
+ }
285
+ /**
286
+ * Send file content to be displayed in the web FileViewer
287
+ * @param filename - The name of the file
288
+ * @param content - The file content (UTF-8 for text, base64 for images)
289
+ * @param contentType - MIME type (e.g., 'text/markdown', 'text/html', 'image/png')
290
+ * @param path - Optional file path
291
+ */
292
+ sendFileView(filename, content, contentType, path) {
320
293
  if (!this.isConnected)
321
294
  return false;
322
- const meta = {
323
- filePath: filePath,
324
- language: language
325
- };
326
- if (error) {
327
- meta.error = error;
328
- }
329
295
  return this.send({
330
296
  type: 'file_view',
331
297
  session: this.sessionId,
332
- meta: meta,
333
- payload: Buffer.from(content).toString('base64')
298
+ meta: {
299
+ filename,
300
+ contentType,
301
+ path: path || ''
302
+ },
303
+ payload: content
304
+ });
305
+ }
306
+ /**
307
+ * Send upload complete notification to web viewer
308
+ */
309
+ sendUploadComplete(filename, path) {
310
+ if (!this.isConnected)
311
+ return false;
312
+ return this.send({
313
+ type: 'uploadComplete',
314
+ session: this.sessionId,
315
+ meta: {
316
+ filename,
317
+ path,
318
+ success: 'true'
319
+ }
320
+ });
321
+ }
322
+ /**
323
+ * Send upload error notification to web viewer
324
+ */
325
+ sendUploadError(filename, error) {
326
+ if (!this.isConnected)
327
+ return false;
328
+ return this.send({
329
+ type: 'uploadError',
330
+ session: this.sessionId,
331
+ meta: {
332
+ filename,
333
+ error
334
+ }
334
335
  });
335
336
  }
336
337
  getConnected() {
package/dist/api.js CHANGED
@@ -9,14 +9,11 @@ const config_1 = require("./config");
9
9
  class ApiClient {
10
10
  getHeaders() {
11
11
  const apiKey = (0, config_1.getApiKey)();
12
- const accessToken = (0, config_1.getAccessToken)();
13
- const agentToken = (0, config_1.getAgentToken)();
14
- const token = accessToken || apiKey || agentToken;
15
- if (!token) {
12
+ if (!apiKey) {
16
13
  throw new Error('Not logged in. Run: sessioncast login');
17
14
  }
18
15
  return {
19
- 'Authorization': `Bearer ${token}`,
16
+ 'Authorization': `Bearer ${apiKey}`,
20
17
  'Content-Type': 'application/json'
21
18
  };
22
19
  }
@@ -69,11 +69,11 @@ async function manualLogin(apiKey, options) {
69
69
  }
70
70
  if (apiKey.startsWith('agt_')) {
71
71
  (0, config_1.setAgentToken)(apiKey);
72
- console.log(chalk_1.default.green(' Agent token saved!'));
72
+ console.log(chalk_1.default.green('\u2713 Agent token saved!'));
73
73
  }
74
74
  else {
75
75
  (0, config_1.setApiKey)(apiKey);
76
- console.log(chalk_1.default.green(' API key saved!'));
76
+ console.log(chalk_1.default.green('\u2713 API key saved!'));
77
77
  }
78
78
  }
79
79
  async function browserLogin(options = {}) {
@@ -197,15 +197,9 @@ async function browserLogin(options = {}) {
197
197
  (0, config_1.setMachineId)(agentData.machineId || os.hostname());
198
198
  }
199
199
  spinner.succeed('Login successful!');
200
- console.log(chalk_1.default.green('\n You are now logged in to SessionCast\n'));
201
- console.log(chalk_1.default.gray(' Start the agent:'));
202
- console.log(chalk_1.default.white(' sessioncast agent\n'));
203
- console.log(chalk_1.default.gray(' Run in background:'));
204
- console.log(chalk_1.default.white(' nohup sessioncast agent > agent.log 2>&1 &\n'));
205
- console.log(chalk_1.default.gray(' Check logs:'));
206
- console.log(chalk_1.default.white(' tail -f agent.log\n'));
207
- await checkForUpdates();
208
- process.exit(0);
200
+ console.log(chalk_1.default.green('\n\u2713 You are now logged in to SessionCast\n'));
201
+ console.log(chalk_1.default.gray(' Run `sessioncast agent` to start the agent'));
202
+ console.log(chalk_1.default.gray(' Run `sessioncast status` to check your login status\n'));
209
203
  }
210
204
  catch (err) {
211
205
  spinner.fail(`Login failed: ${err.message}`);
@@ -218,12 +212,12 @@ async function logout() {
218
212
  return;
219
213
  }
220
214
  (0, config_1.clearAuth)();
221
- console.log(chalk_1.default.green(' Logged out successfully!'));
215
+ console.log(chalk_1.default.green('\u2713 Logged out successfully!'));
222
216
  }
223
217
  function status() {
224
218
  const accessToken = (0, config_1.getAccessToken)();
225
219
  if ((0, config_1.isLoggedIn)()) {
226
- console.log(chalk_1.default.green(' Logged in'));
220
+ console.log(chalk_1.default.green('\u2713 Logged in'));
227
221
  if (accessToken) {
228
222
  console.log(chalk_1.default.gray(' Auth method: OAuth'));
229
223
  }
@@ -236,19 +230,3 @@ function status() {
236
230
  console.log(chalk_1.default.gray('Run: sessioncast login'));
237
231
  }
238
232
  }
239
- const CURRENT_VERSION = require('../../package.json').version;
240
- async function checkForUpdates() {
241
- try {
242
- const res = await (0, node_fetch_1.default)('https://registry.npmjs.org/sessioncast-cli/latest', { timeout: 3000 });
243
- if (!res.ok) return;
244
- const data = await res.json();
245
- const latest = data.version;
246
- if (latest && latest !== CURRENT_VERSION) {
247
- console.log(chalk_1.default.yellow(` ⬆ Update available: ${CURRENT_VERSION} → ${latest}`));
248
- console.log(chalk_1.default.white(` npm install -g sessioncast-cli@latest\n`));
249
- }
250
- }
251
- catch (_e) {
252
- // silently ignore - network errors shouldn't block login
253
- }
254
- }
package/dist/index.js CHANGED
@@ -85,12 +85,12 @@ function showWelcome() {
85
85
  return;
86
86
  const { available, isWindows } = checkTmux();
87
87
  console.log('');
88
- console.log(chalk_1.default.green.bold(' SessionCast CLI installed'));
88
+ console.log(chalk_1.default.green.bold('\u2713 SessionCast CLI installed'));
89
89
  console.log('');
90
90
  if (!available) {
91
- console.log(chalk_1.default.yellow(' tmux not found'));
91
+ console.log(chalk_1.default.yellow('\u26a0 tmux not found'));
92
92
  if (isWindows) {
93
- console.log(chalk_1.default.gray(' Install itmux: https://github.com/phayte/itmux'));
93
+ console.log(chalk_1.default.gray(' Install itmux: https://github.com/itefixnet/itmux'));
94
94
  console.log(chalk_1.default.gray(' Or: choco install itmux'));
95
95
  }
96
96
  else if (os.platform() === 'darwin') {
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "sessioncast-cli",
3
- "version": "1.1.5",
3
+ "version": "2.0.1",
4
4
  "description": "SessionCast CLI - Control your agents from anywhere",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
- "sessioncast": "dist/index.js"
7
+ "sessioncast": "./dist/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc",
11
11
  "dev": "ts-node src/index.ts",
12
12
  "start": "node dist/index.js",
13
- "prepublishOnly": "echo 'skip build'"
13
+ "prepublishOnly": "npm run build"
14
14
  },
15
15
  "keywords": [
16
16
  "sessioncast",
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 SessionCast
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.