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 +27 -1
- package/dist/agent/runner.js +26 -23
- package/dist/agent/session-handler.d.ts +32 -2
- package/dist/agent/session-handler.js +241 -64
- package/dist/agent/tmux-executor.d.ts +39 -3
- package/dist/agent/tmux-executor.js +111 -5
- package/dist/agent/tmux.d.ts +14 -2
- package/dist/agent/tmux.js +23 -2
- package/dist/agent/types.d.ts +10 -0
- package/dist/agent/websocket.d.ts +34 -4
- package/dist/agent/websocket.js +80 -79
- package/dist/api.js +2 -5
- package/dist/commands/login.js +7 -29
- package/dist/index.js +3 -3
- package/package.json +3 -3
- package/LICENSE +0 -21
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
|
|
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)
|
package/dist/agent/runner.js
CHANGED
|
@@ -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
|
|
70
|
-
if (finalPath
|
|
71
|
-
console.log(
|
|
72
|
-
const
|
|
73
|
-
const
|
|
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
|
|
91
|
-
relay:
|
|
79
|
+
machineId,
|
|
80
|
+
relay: (0, config_1.getRelayUrl)(),
|
|
92
81
|
token: agentToken,
|
|
82
|
+
api: {
|
|
83
|
+
enabled: false
|
|
84
|
+
}
|
|
93
85
|
};
|
|
94
86
|
}
|
|
95
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
listPanes(session) {
|
|
64
64
|
try {
|
|
65
|
-
const output = (0, child_process_1.execSync)(`tmux
|
|
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
|
-
|
|
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('
|
|
346
|
-
'
|
|
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
|
}
|
package/dist/agent/tmux.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent/tmux.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|
package/dist/agent/websocket.js
CHANGED
|
@@ -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.
|
|
160
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
333
|
-
|
|
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
|
-
|
|
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 ${
|
|
16
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
20
17
|
'Content-Type': 'application/json'
|
|
21
18
|
};
|
|
22
19
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -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('
|
|
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('
|
|
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
|
|
201
|
-
console.log(chalk_1.default.gray('
|
|
202
|
-
console.log(chalk_1.default.
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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/
|
|
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": "
|
|
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": "
|
|
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.
|