sessioncast-cli 1.1.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/dist/agent/runner.js +26 -23
- package/dist/agent/session-handler.d.ts +30 -1
- package/dist/agent/session-handler.js +166 -36
- package/dist/agent/tmux-executor.d.ts +6 -0
- package/dist/agent/tmux-executor.js +61 -2
- package/dist/agent/tmux.d.ts +8 -0
- package/dist/agent/tmux.js +14 -0
- package/dist/agent/websocket.d.ts +16 -2
- package/dist/agent/websocket.js +54 -70
- 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/dist/autopilot/index.d.ts +0 -94
- package/dist/autopilot/index.js +0 -322
- package/dist/autopilot/mission-analyzer.d.ts +0 -27
- package/dist/autopilot/mission-analyzer.js +0 -232
- package/dist/autopilot/project-detector.d.ts +0 -12
- package/dist/autopilot/project-detector.js +0 -326
- package/dist/autopilot/source-scanner.d.ts +0 -26
- package/dist/autopilot/source-scanner.js +0 -285
- package/dist/autopilot/speckit-generator.d.ts +0 -60
- package/dist/autopilot/speckit-generator.js +0 -511
- package/dist/autopilot/types.d.ts +0 -110
- package/dist/autopilot/types.js +0 -6
- package/dist/autopilot/workflow-generator.d.ts +0 -33
- package/dist/autopilot/workflow-generator.js +0 -278
- package/dist/project/executor.d.ts +0 -73
- package/dist/project/executor.js +0 -437
- package/dist/project/index.d.ts +0 -4
- package/dist/project/index.js +0 -20
- package/dist/project/manager.d.ts +0 -66
- package/dist/project/manager.js +0 -290
- package/dist/project/relay-client.d.ts +0 -37
- package/dist/project/relay-client.js +0 -204
- package/dist/project/types.d.ts +0 -48
- package/dist/project/types.js +0 -3
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)
|
|
@@ -15,11 +15,40 @@ export declare class TmuxSessionHandler {
|
|
|
15
15
|
private lastForceSendTime;
|
|
16
16
|
private lastChangeTime;
|
|
17
17
|
private captureTimer;
|
|
18
|
+
private pendingUploads;
|
|
19
|
+
private uploadDir;
|
|
18
20
|
constructor(options: SessionHandlerOptions);
|
|
19
21
|
start(): void;
|
|
20
22
|
private connectAndRun;
|
|
23
|
+
/**
|
|
24
|
+
* Handle file view request from web client
|
|
25
|
+
*/
|
|
26
|
+
private handleRequestFileView;
|
|
27
|
+
/**
|
|
28
|
+
* Handle incoming file upload chunk
|
|
29
|
+
*/
|
|
30
|
+
private handleUploadChunk;
|
|
31
|
+
/**
|
|
32
|
+
* Complete file upload by assembling chunks and writing to disk
|
|
33
|
+
*/
|
|
34
|
+
private completeUpload;
|
|
35
|
+
/**
|
|
36
|
+
* Get upload directory (try to get tmux pane's current working directory)
|
|
37
|
+
*/
|
|
38
|
+
private getUploadDirectory;
|
|
39
|
+
/**
|
|
40
|
+
* Sanitize filename to prevent path traversal attacks
|
|
41
|
+
*/
|
|
42
|
+
private sanitizeFilename;
|
|
43
|
+
/**
|
|
44
|
+
* Get unique file path if file already exists
|
|
45
|
+
*/
|
|
46
|
+
private getUniqueFilePath;
|
|
47
|
+
/**
|
|
48
|
+
* Get content type from file extension
|
|
49
|
+
*/
|
|
50
|
+
private getContentType;
|
|
21
51
|
private handleKeys;
|
|
22
|
-
private handleFileViewRequest;
|
|
23
52
|
private startScreenCapture;
|
|
24
53
|
private stopScreenCapture;
|
|
25
54
|
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;
|
|
@@ -73,6 +53,9 @@ class TmuxSessionHandler {
|
|
|
73
53
|
this.lastForceSendTime = 0;
|
|
74
54
|
this.lastChangeTime = 0;
|
|
75
55
|
this.captureTimer = null;
|
|
56
|
+
// File upload handling
|
|
57
|
+
this.pendingUploads = new Map();
|
|
58
|
+
this.uploadDir = process.cwd(); // Default to current working directory
|
|
76
59
|
this.config = options.config;
|
|
77
60
|
this.tmuxSession = options.tmuxSession;
|
|
78
61
|
this.sessionId = `${options.config.machineId}/${options.tmuxSession}`;
|
|
@@ -125,32 +108,179 @@ class TmuxSessionHandler {
|
|
|
125
108
|
this.stop();
|
|
126
109
|
});
|
|
127
110
|
this.wsClient.on('requestFileView', (filePath) => {
|
|
128
|
-
|
|
129
|
-
|
|
111
|
+
this.handleRequestFileView(filePath);
|
|
112
|
+
});
|
|
113
|
+
this.wsClient.on('uploadFile', (chunk) => {
|
|
114
|
+
this.handleUploadChunk(chunk);
|
|
130
115
|
});
|
|
131
116
|
this.wsClient.on('error', (error) => {
|
|
132
117
|
console.error(`[${this.tmuxSession}] WebSocket error:`, error.message);
|
|
133
118
|
});
|
|
134
119
|
this.wsClient.connect();
|
|
135
120
|
}
|
|
136
|
-
|
|
137
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Handle file view request from web client
|
|
123
|
+
*/
|
|
124
|
+
handleRequestFileView(filePath) {
|
|
125
|
+
console.log(`[${this.tmuxSession}] File view request: ${filePath}`);
|
|
126
|
+
try {
|
|
127
|
+
// Resolve file path (could be relative or absolute)
|
|
128
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
129
|
+
? filePath
|
|
130
|
+
: path.resolve(this.uploadDir, filePath);
|
|
131
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
132
|
+
console.log(`[${this.tmuxSession}] File not found: ${resolvedPath}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const stat = fs.statSync(resolvedPath);
|
|
136
|
+
if (!stat.isFile()) {
|
|
137
|
+
console.log(`[${this.tmuxSession}] Not a file: ${resolvedPath}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Read file and send to viewer
|
|
141
|
+
const content = fs.readFileSync(resolvedPath);
|
|
142
|
+
const base64Content = content.toString('base64');
|
|
143
|
+
const filename = path.basename(resolvedPath);
|
|
144
|
+
const ext = path.extname(filename).toLowerCase();
|
|
145
|
+
// Determine content type
|
|
146
|
+
const contentType = this.getContentType(ext);
|
|
147
|
+
this.wsClient?.sendFileView(filename, base64Content, contentType, resolvedPath);
|
|
148
|
+
console.log(`[${this.tmuxSession}] Sent file view: ${filename}`);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error(`[${this.tmuxSession}] Error reading file:`, error);
|
|
152
|
+
}
|
|
138
153
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
154
|
+
/**
|
|
155
|
+
* Handle incoming file upload chunk
|
|
156
|
+
*/
|
|
157
|
+
handleUploadChunk(chunk) {
|
|
158
|
+
const { filename, size, chunkIndex, totalChunks, payload } = chunk;
|
|
159
|
+
console.log(`[${this.tmuxSession}] Upload chunk: ${filename} (${chunkIndex + 1}/${totalChunks})`);
|
|
160
|
+
// Sanitize filename to prevent path traversal
|
|
161
|
+
const safeFilename = this.sanitizeFilename(filename);
|
|
162
|
+
const uploadKey = `${safeFilename}_${size}`;
|
|
163
|
+
// Get or create pending upload
|
|
164
|
+
let pending = this.pendingUploads.get(uploadKey);
|
|
165
|
+
if (!pending) {
|
|
166
|
+
pending = {
|
|
167
|
+
filename: safeFilename,
|
|
168
|
+
size,
|
|
169
|
+
chunks: new Map(),
|
|
170
|
+
totalChunks,
|
|
171
|
+
receivedAt: Date.now()
|
|
172
|
+
};
|
|
173
|
+
this.pendingUploads.set(uploadKey, pending);
|
|
174
|
+
}
|
|
175
|
+
// Store chunk
|
|
176
|
+
pending.chunks.set(chunkIndex, payload);
|
|
177
|
+
// Check if all chunks received
|
|
178
|
+
if (pending.chunks.size === totalChunks) {
|
|
179
|
+
this.completeUpload(uploadKey, pending);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Complete file upload by assembling chunks and writing to disk
|
|
184
|
+
*/
|
|
185
|
+
completeUpload(uploadKey, pending) {
|
|
144
186
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
187
|
+
// Assemble chunks in order
|
|
188
|
+
const chunks = [];
|
|
189
|
+
for (let i = 0; i < pending.totalChunks; i++) {
|
|
190
|
+
const chunkData = pending.chunks.get(i);
|
|
191
|
+
if (!chunkData) {
|
|
192
|
+
throw new Error(`Missing chunk ${i}`);
|
|
193
|
+
}
|
|
194
|
+
chunks.push(Buffer.from(chunkData, 'base64'));
|
|
195
|
+
}
|
|
196
|
+
const fileBuffer = Buffer.concat(chunks);
|
|
197
|
+
// Determine upload directory (try to get tmux pane's cwd)
|
|
198
|
+
const uploadPath = this.getUploadDirectory();
|
|
199
|
+
const filePath = path.join(uploadPath, pending.filename);
|
|
200
|
+
// Handle filename conflicts
|
|
201
|
+
const finalPath = this.getUniqueFilePath(filePath);
|
|
202
|
+
// Write file
|
|
203
|
+
fs.writeFileSync(finalPath, fileBuffer);
|
|
204
|
+
console.log(`[${this.tmuxSession}] File uploaded: ${finalPath}`);
|
|
205
|
+
// Send success notification
|
|
206
|
+
this.wsClient?.sendUploadComplete(pending.filename, finalPath);
|
|
207
|
+
// Cleanup
|
|
208
|
+
this.pendingUploads.delete(uploadKey);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.error(`[${this.tmuxSession}] Upload failed:`, error);
|
|
212
|
+
this.wsClient?.sendUploadError(pending.filename, error.message);
|
|
213
|
+
this.pendingUploads.delete(uploadKey);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get upload directory (try to get tmux pane's current working directory)
|
|
218
|
+
*/
|
|
219
|
+
getUploadDirectory() {
|
|
220
|
+
try {
|
|
221
|
+
// Try to get pane's current directory from tmux
|
|
222
|
+
const paneId = tmux.getActivePane(this.tmuxSession);
|
|
223
|
+
if (paneId) {
|
|
224
|
+
const cwd = tmux.getPaneCwd(this.tmuxSession, paneId);
|
|
225
|
+
if (cwd && fs.existsSync(cwd)) {
|
|
226
|
+
return cwd;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
148
229
|
}
|
|
149
|
-
catch
|
|
150
|
-
|
|
151
|
-
console.error(`[${this.tmuxSession}] File read error: ${errorMessage}`);
|
|
152
|
-
this.wsClient.sendFileView(filePath, `Error: ${errorMessage}`, 'text', errorMessage);
|
|
230
|
+
catch {
|
|
231
|
+
// Ignore errors, fall back to default
|
|
153
232
|
}
|
|
233
|
+
return this.uploadDir;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Sanitize filename to prevent path traversal attacks
|
|
237
|
+
*/
|
|
238
|
+
sanitizeFilename(filename) {
|
|
239
|
+
// Remove path separators and dangerous characters
|
|
240
|
+
return path.basename(filename).replace(/[<>:"|?*\x00-\x1f]/g, '_');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get unique file path if file already exists
|
|
244
|
+
*/
|
|
245
|
+
getUniqueFilePath(filePath) {
|
|
246
|
+
if (!fs.existsSync(filePath)) {
|
|
247
|
+
return filePath;
|
|
248
|
+
}
|
|
249
|
+
const dir = path.dirname(filePath);
|
|
250
|
+
const ext = path.extname(filePath);
|
|
251
|
+
const name = path.basename(filePath, ext);
|
|
252
|
+
let counter = 1;
|
|
253
|
+
let newPath;
|
|
254
|
+
do {
|
|
255
|
+
newPath = path.join(dir, `${name} (${counter})${ext}`);
|
|
256
|
+
counter++;
|
|
257
|
+
} while (fs.existsSync(newPath) && counter < 1000);
|
|
258
|
+
return newPath;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get content type from file extension
|
|
262
|
+
*/
|
|
263
|
+
getContentType(ext) {
|
|
264
|
+
const types = {
|
|
265
|
+
'.txt': 'text/plain',
|
|
266
|
+
'.md': 'text/markdown',
|
|
267
|
+
'.html': 'text/html',
|
|
268
|
+
'.css': 'text/css',
|
|
269
|
+
'.js': 'text/javascript',
|
|
270
|
+
'.ts': 'text/typescript',
|
|
271
|
+
'.json': 'application/json',
|
|
272
|
+
'.xml': 'application/xml',
|
|
273
|
+
'.png': 'image/png',
|
|
274
|
+
'.jpg': 'image/jpeg',
|
|
275
|
+
'.jpeg': 'image/jpeg',
|
|
276
|
+
'.gif': 'image/gif',
|
|
277
|
+
'.svg': 'image/svg+xml',
|
|
278
|
+
'.pdf': 'application/pdf',
|
|
279
|
+
};
|
|
280
|
+
return types[ext] || 'application/octet-stream';
|
|
281
|
+
}
|
|
282
|
+
handleKeys(keys) {
|
|
283
|
+
tmux.sendKeys(this.tmuxSession, keys, false);
|
|
154
284
|
}
|
|
155
285
|
startScreenCapture() {
|
|
156
286
|
if (this.captureTimer)
|
|
@@ -12,6 +12,8 @@ export interface TmuxExecutor {
|
|
|
12
12
|
createSession(session: string, workingDir?: string): boolean;
|
|
13
13
|
isAvailable(): boolean;
|
|
14
14
|
getVersion(): string | null;
|
|
15
|
+
getActivePane(session: string): string | null;
|
|
16
|
+
getPaneCwd(session: string, paneId?: string): string | null;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Unix/Linux/macOS implementation of TmuxExecutor.
|
|
@@ -26,6 +28,8 @@ export declare class UnixTmuxExecutor implements TmuxExecutor {
|
|
|
26
28
|
createSession(session: string, workingDir?: string): boolean;
|
|
27
29
|
isAvailable(): boolean;
|
|
28
30
|
getVersion(): string | null;
|
|
31
|
+
getActivePane(session: string): string | null;
|
|
32
|
+
getPaneCwd(session: string, paneId?: string): string | null;
|
|
29
33
|
private escapeForShell;
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
@@ -45,6 +49,8 @@ export declare class WindowsTmuxExecutor implements TmuxExecutor {
|
|
|
45
49
|
createSession(session: string, workingDir?: string): boolean;
|
|
46
50
|
isAvailable(): boolean;
|
|
47
51
|
getVersion(): string | null;
|
|
52
|
+
getActivePane(session: string): string | null;
|
|
53
|
+
getPaneCwd(session: string, paneId?: string): string | null;
|
|
48
54
|
private escapeSession;
|
|
49
55
|
private windowsToCygwinPath;
|
|
50
56
|
/**
|
|
@@ -143,6 +143,31 @@ class UnixTmuxExecutor {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
getActivePane(session) {
|
|
147
|
+
try {
|
|
148
|
+
const output = (0, child_process_1.execSync)(`tmux display-message -t "${session}" -p "#{pane_id}"`, {
|
|
149
|
+
encoding: 'utf-8',
|
|
150
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
151
|
+
});
|
|
152
|
+
return output.trim() || null;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
getPaneCwd(session, paneId) {
|
|
159
|
+
try {
|
|
160
|
+
const target = paneId ? `${session}:${paneId}` : session;
|
|
161
|
+
const output = (0, child_process_1.execSync)(`tmux display-message -t "${target}" -p "#{pane_current_path}"`, {
|
|
162
|
+
encoding: 'utf-8',
|
|
163
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
164
|
+
});
|
|
165
|
+
return output.trim() || null;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
146
171
|
escapeForShell(str) {
|
|
147
172
|
return str.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
|
|
148
173
|
}
|
|
@@ -289,6 +314,27 @@ class WindowsTmuxExecutor {
|
|
|
289
314
|
return null;
|
|
290
315
|
}
|
|
291
316
|
}
|
|
317
|
+
getActivePane(session) {
|
|
318
|
+
try {
|
|
319
|
+
const escaped = this.escapeSession(session);
|
|
320
|
+
const output = this.executeCommand(`tmux display-message -t '${escaped}' -p "#{pane_id}"`);
|
|
321
|
+
return output.trim() || null;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
getPaneCwd(session, paneId) {
|
|
328
|
+
try {
|
|
329
|
+
const escapedSession = this.escapeSession(session);
|
|
330
|
+
const target = paneId ? `${escapedSession}:${paneId}` : escapedSession;
|
|
331
|
+
const output = this.executeCommand(`tmux display-message -t '${target}' -p "#{pane_current_path}"`);
|
|
332
|
+
return output.trim() || null;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
292
338
|
escapeSession(session) {
|
|
293
339
|
return session.replace(/'/g, "'\\''");
|
|
294
340
|
}
|
|
@@ -342,8 +388,21 @@ function createTmuxExecutor() {
|
|
|
342
388
|
if (isWindows) {
|
|
343
389
|
const itmuxPath = WindowsTmuxExecutor.findItmuxPath();
|
|
344
390
|
if (!itmuxPath) {
|
|
345
|
-
throw new Error('
|
|
346
|
-
'
|
|
391
|
+
throw new Error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
392
|
+
' itmux not found - Windows tmux package required\n' +
|
|
393
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' +
|
|
394
|
+
' Download: https://github.com/itefixnet/itmux/releases/latest\n' +
|
|
395
|
+
' Latest: https://github.com/itefixnet/itmux/releases/download/v1.1.0/itmux_1.1.0_x64_free.zip\n\n' +
|
|
396
|
+
' Installation:\n' +
|
|
397
|
+
' 1. Download and extract itmux_1.1.0_x64_free.zip\n' +
|
|
398
|
+
' 2. Place in one of these locations:\n' +
|
|
399
|
+
' • C:\\itmux\n' +
|
|
400
|
+
' • %USERPROFILE%\\itmux\n' +
|
|
401
|
+
' • Or set ITMUX_HOME environment variable\n\n' +
|
|
402
|
+
' Quick install (PowerShell):\n' +
|
|
403
|
+
' 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' +
|
|
404
|
+
' Expand-Archive -Path "$env:TEMP\\itmux.zip" -DestinationPath "C:\\itmux" -Force\n\n' +
|
|
405
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
347
406
|
}
|
|
348
407
|
return new WindowsTmuxExecutor(itmuxPath);
|
|
349
408
|
}
|
package/dist/agent/tmux.d.ts
CHANGED
|
@@ -35,3 +35,11 @@ export declare function isAvailable(): boolean;
|
|
|
35
35
|
* Get tmux version
|
|
36
36
|
*/
|
|
37
37
|
export declare function getVersion(): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Get the active pane ID in a session
|
|
40
|
+
*/
|
|
41
|
+
export declare function getActivePane(sessionName: string): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Get the current working directory of a pane
|
|
44
|
+
*/
|
|
45
|
+
export declare function getPaneCwd(sessionName: string, paneId?: string): string | null;
|
package/dist/agent/tmux.js
CHANGED
|
@@ -9,6 +9,8 @@ exports.createSession = createSession;
|
|
|
9
9
|
exports.killSession = killSession;
|
|
10
10
|
exports.isAvailable = isAvailable;
|
|
11
11
|
exports.getVersion = getVersion;
|
|
12
|
+
exports.getActivePane = getActivePane;
|
|
13
|
+
exports.getPaneCwd = getPaneCwd;
|
|
12
14
|
const tmux_executor_1 = require("./tmux-executor");
|
|
13
15
|
// Lazy-initialized executor (created on first use)
|
|
14
16
|
let executor = null;
|
|
@@ -131,3 +133,15 @@ function getVersion() {
|
|
|
131
133
|
return null;
|
|
132
134
|
}
|
|
133
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Get the active pane ID in a session
|
|
138
|
+
*/
|
|
139
|
+
function getActivePane(sessionName) {
|
|
140
|
+
return getExecutor().getActivePane(sessionName);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the current working directory of a pane
|
|
144
|
+
*/
|
|
145
|
+
function getPaneCwd(sessionName, paneId) {
|
|
146
|
+
return getExecutor().getPaneCwd(sessionName, paneId);
|
|
147
|
+
}
|
|
@@ -26,13 +26,27 @@ 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
32
|
sendScreen(data: Buffer): boolean;
|
|
34
33
|
sendScreenCompressed(data: Buffer): boolean;
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Send file content to be displayed in the web FileViewer
|
|
36
|
+
* @param filename - The name of the file
|
|
37
|
+
* @param content - The file content (UTF-8 for text, base64 for images)
|
|
38
|
+
* @param contentType - MIME type (e.g., 'text/markdown', 'text/html', 'image/png')
|
|
39
|
+
* @param path - Optional file path
|
|
40
|
+
*/
|
|
41
|
+
sendFileView(filename: string, content: string, contentType: string, path?: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Send upload complete notification to web viewer
|
|
44
|
+
*/
|
|
45
|
+
sendUploadComplete(filename: string, path: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Send upload error notification to web viewer
|
|
48
|
+
*/
|
|
49
|
+
sendUploadError(filename: string, error: string): boolean;
|
|
36
50
|
getConnected(): boolean;
|
|
37
51
|
destroy(): void;
|
|
38
52
|
}
|