sessioncast-cli 1.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/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/agent/api-client.d.ts +27 -0
- package/dist/agent/api-client.js +295 -0
- package/dist/agent/exec-service.d.ts +6 -0
- package/dist/agent/exec-service.js +126 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.js +24 -0
- package/dist/agent/llm-service.d.ts +9 -0
- package/dist/agent/llm-service.js +156 -0
- package/dist/agent/runner.d.ts +16 -0
- package/dist/agent/runner.js +187 -0
- package/dist/agent/session-handler.d.ts +28 -0
- package/dist/agent/session-handler.js +184 -0
- package/dist/agent/tmux.d.ts +29 -0
- package/dist/agent/tmux.js +157 -0
- package/dist/agent/types.d.ts +72 -0
- package/dist/agent/types.js +2 -0
- package/dist/agent/websocket.d.ts +45 -0
- package/dist/agent/websocket.js +288 -0
- package/dist/api.d.ts +31 -0
- package/dist/api.js +78 -0
- package/dist/commands/agent.d.ts +5 -0
- package/dist/commands/agent.js +19 -0
- package/dist/commands/agents.d.ts +1 -0
- package/dist/commands/agents.js +77 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +41 -0
- package/dist/commands/project.d.ts +33 -0
- package/dist/commands/project.js +359 -0
- package/dist/commands/sendkeys.d.ts +3 -0
- package/dist/commands/sendkeys.js +66 -0
- package/dist/commands/sessions.d.ts +1 -0
- package/dist/commands/sessions.js +89 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/dist/project/executor.d.ts +118 -0
- package/dist/project/executor.js +893 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +20 -0
- package/dist/project/manager.d.ts +79 -0
- package/dist/project/manager.js +397 -0
- package/dist/project/relay-client.d.ts +87 -0
- package/dist/project/relay-client.js +200 -0
- package/dist/project/types.d.ts +43 -0
- package/dist/project/types.js +3 -0
- package/package.json +59 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LlmService = void 0;
|
|
4
|
+
class LlmService {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config || {
|
|
7
|
+
enabled: false,
|
|
8
|
+
provider: 'ollama',
|
|
9
|
+
baseUrl: 'http://localhost:11434',
|
|
10
|
+
model: 'llama2'
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async chat(model, messages, temperature, maxTokens, stream) {
|
|
14
|
+
if (!this.config.enabled) {
|
|
15
|
+
return {
|
|
16
|
+
id: '',
|
|
17
|
+
object: 'error',
|
|
18
|
+
created: Date.now(),
|
|
19
|
+
model: '',
|
|
20
|
+
choices: [],
|
|
21
|
+
error: {
|
|
22
|
+
message: 'LLM is disabled on this agent',
|
|
23
|
+
type: 'service_unavailable'
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const provider = this.config.provider || 'ollama';
|
|
28
|
+
const actualModel = model || this.config.model;
|
|
29
|
+
try {
|
|
30
|
+
switch (provider.toLowerCase()) {
|
|
31
|
+
case 'ollama':
|
|
32
|
+
return await this.callOllama(actualModel, messages || [], temperature, maxTokens);
|
|
33
|
+
case 'openai':
|
|
34
|
+
return await this.callOpenAi(actualModel, messages || [], temperature, maxTokens);
|
|
35
|
+
default:
|
|
36
|
+
return {
|
|
37
|
+
id: '',
|
|
38
|
+
object: 'error',
|
|
39
|
+
created: Date.now(),
|
|
40
|
+
model: '',
|
|
41
|
+
choices: [],
|
|
42
|
+
error: {
|
|
43
|
+
message: `Unknown LLM provider: ${provider}`,
|
|
44
|
+
type: 'invalid_request'
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
id: '',
|
|
52
|
+
object: 'error',
|
|
53
|
+
created: Date.now(),
|
|
54
|
+
model: '',
|
|
55
|
+
choices: [],
|
|
56
|
+
error: {
|
|
57
|
+
message: error.message,
|
|
58
|
+
type: 'internal_error'
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async callOllama(model, messages, temperature, maxTokens) {
|
|
64
|
+
const baseUrl = this.config.baseUrl || 'http://localhost:11434';
|
|
65
|
+
const requestBody = {
|
|
66
|
+
model,
|
|
67
|
+
messages,
|
|
68
|
+
stream: false
|
|
69
|
+
};
|
|
70
|
+
const options = {};
|
|
71
|
+
if (temperature !== undefined) {
|
|
72
|
+
options.temperature = temperature;
|
|
73
|
+
}
|
|
74
|
+
if (maxTokens !== undefined) {
|
|
75
|
+
options.num_predict = maxTokens;
|
|
76
|
+
}
|
|
77
|
+
if (Object.keys(options).length > 0) {
|
|
78
|
+
requestBody.options = options;
|
|
79
|
+
}
|
|
80
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify(requestBody)
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`Ollama returned status ${response.status}`);
|
|
87
|
+
}
|
|
88
|
+
const ollamaResponse = await response.json();
|
|
89
|
+
return this.convertOllamaToOpenAiFormat(ollamaResponse, model);
|
|
90
|
+
}
|
|
91
|
+
convertOllamaToOpenAiFormat(ollamaResponse, model) {
|
|
92
|
+
const message = ollamaResponse.message || {};
|
|
93
|
+
const content = message.content || '';
|
|
94
|
+
const promptTokens = ollamaResponse.prompt_eval_count || 0;
|
|
95
|
+
const completionTokens = ollamaResponse.eval_count || 0;
|
|
96
|
+
return {
|
|
97
|
+
id: `chatcmpl-${Math.random().toString(36).substring(2, 10)}`,
|
|
98
|
+
object: 'chat.completion',
|
|
99
|
+
created: Math.floor(Date.now() / 1000),
|
|
100
|
+
model,
|
|
101
|
+
choices: [{
|
|
102
|
+
index: 0,
|
|
103
|
+
message: {
|
|
104
|
+
role: 'assistant',
|
|
105
|
+
content
|
|
106
|
+
},
|
|
107
|
+
finish_reason: 'stop'
|
|
108
|
+
}],
|
|
109
|
+
usage: {
|
|
110
|
+
prompt_tokens: promptTokens,
|
|
111
|
+
completion_tokens: completionTokens,
|
|
112
|
+
total_tokens: promptTokens + completionTokens
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async callOpenAi(model, messages, temperature, maxTokens) {
|
|
117
|
+
const baseUrl = this.config.baseUrl || 'https://api.openai.com/v1';
|
|
118
|
+
const apiKey = this.config.apiKey;
|
|
119
|
+
if (!apiKey) {
|
|
120
|
+
return {
|
|
121
|
+
id: '',
|
|
122
|
+
object: 'error',
|
|
123
|
+
created: Date.now(),
|
|
124
|
+
model: '',
|
|
125
|
+
choices: [],
|
|
126
|
+
error: {
|
|
127
|
+
message: 'OpenAI API key not configured',
|
|
128
|
+
type: 'configuration_error'
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const requestBody = {
|
|
133
|
+
model,
|
|
134
|
+
messages
|
|
135
|
+
};
|
|
136
|
+
if (temperature !== undefined) {
|
|
137
|
+
requestBody.temperature = temperature;
|
|
138
|
+
}
|
|
139
|
+
if (maxTokens !== undefined) {
|
|
140
|
+
requestBody.max_tokens = maxTokens;
|
|
141
|
+
}
|
|
142
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: {
|
|
145
|
+
'Content-Type': 'application/json',
|
|
146
|
+
'Authorization': `Bearer ${apiKey}`
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify(requestBody)
|
|
149
|
+
});
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
throw new Error(`OpenAI returned status ${response.status}`);
|
|
152
|
+
}
|
|
153
|
+
return await response.json();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.LlmService = LlmService;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AgentConfig } from './types';
|
|
2
|
+
export declare class AgentRunner {
|
|
3
|
+
private config;
|
|
4
|
+
private handlers;
|
|
5
|
+
private apiClient;
|
|
6
|
+
private scanTimer;
|
|
7
|
+
private running;
|
|
8
|
+
constructor(config: AgentConfig);
|
|
9
|
+
static loadConfig(configPath?: string): AgentConfig;
|
|
10
|
+
start(): Promise<void>;
|
|
11
|
+
private scanAndUpdateSessions;
|
|
12
|
+
private startSessionHandler;
|
|
13
|
+
private stopSessionHandler;
|
|
14
|
+
private createTmuxSession;
|
|
15
|
+
stop(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AgentRunner = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const yaml = __importStar(require("js-yaml"));
|
|
40
|
+
const session_handler_1 = require("./session-handler");
|
|
41
|
+
const api_client_1 = require("./api-client");
|
|
42
|
+
const tmux = __importStar(require("./tmux"));
|
|
43
|
+
const SCAN_INTERVAL_MS = 5000;
|
|
44
|
+
class AgentRunner {
|
|
45
|
+
constructor(config) {
|
|
46
|
+
this.handlers = new Map();
|
|
47
|
+
this.apiClient = null;
|
|
48
|
+
this.scanTimer = null;
|
|
49
|
+
this.running = false;
|
|
50
|
+
this.config = config;
|
|
51
|
+
}
|
|
52
|
+
static loadConfig(configPath) {
|
|
53
|
+
// Check environment variable
|
|
54
|
+
const envPath = process.env.SESSIONCAST_CONFIG || process.env.TMUX_REMOTE_CONFIG;
|
|
55
|
+
// Try multiple default paths
|
|
56
|
+
const defaultPaths = [
|
|
57
|
+
path.join(process.env.HOME || '', '.sessioncast.yml'),
|
|
58
|
+
path.join(process.env.HOME || '', '.tmux-remote.yml'),
|
|
59
|
+
];
|
|
60
|
+
let finalPath = configPath || envPath;
|
|
61
|
+
if (!finalPath) {
|
|
62
|
+
for (const p of defaultPaths) {
|
|
63
|
+
if (fs.existsSync(p)) {
|
|
64
|
+
finalPath = p;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!finalPath || !fs.existsSync(finalPath)) {
|
|
70
|
+
throw new Error(`Config file not found. Tried: ${configPath || envPath || defaultPaths.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
console.log(`Loading config from: ${finalPath}`);
|
|
73
|
+
const content = fs.readFileSync(finalPath, 'utf-8');
|
|
74
|
+
const ext = path.extname(finalPath).toLowerCase();
|
|
75
|
+
if (ext === '.json') {
|
|
76
|
+
return JSON.parse(content);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return yaml.load(content);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async start() {
|
|
83
|
+
if (this.running)
|
|
84
|
+
return;
|
|
85
|
+
this.running = true;
|
|
86
|
+
console.log('Starting SessionCast Agent...');
|
|
87
|
+
console.log(`Machine ID: ${this.config.machineId}`);
|
|
88
|
+
console.log(`Relay: ${this.config.relay}`);
|
|
89
|
+
console.log(`Token: ${this.config.token ? 'present' : 'none'}`);
|
|
90
|
+
// Start API client if configured
|
|
91
|
+
if (this.config.api?.enabled && this.config.api.agentId) {
|
|
92
|
+
this.apiClient = new api_client_1.ApiWebSocketClient(this.config);
|
|
93
|
+
this.apiClient.start();
|
|
94
|
+
}
|
|
95
|
+
// Initial scan
|
|
96
|
+
this.scanAndUpdateSessions();
|
|
97
|
+
// Schedule periodic scan
|
|
98
|
+
this.scanTimer = setInterval(() => {
|
|
99
|
+
this.scanAndUpdateSessions();
|
|
100
|
+
}, SCAN_INTERVAL_MS);
|
|
101
|
+
console.log(`Agent started with auto-discovery (scanning every ${SCAN_INTERVAL_MS / 1000}s)`);
|
|
102
|
+
// Handle shutdown
|
|
103
|
+
process.on('SIGINT', () => this.stop());
|
|
104
|
+
process.on('SIGTERM', () => this.stop());
|
|
105
|
+
}
|
|
106
|
+
scanAndUpdateSessions() {
|
|
107
|
+
try {
|
|
108
|
+
const currentSessions = new Set(tmux.scanSessions());
|
|
109
|
+
const trackedSessions = new Set(this.handlers.keys());
|
|
110
|
+
// Start handlers for new sessions
|
|
111
|
+
for (const session of currentSessions) {
|
|
112
|
+
if (!trackedSessions.has(session)) {
|
|
113
|
+
this.startSessionHandler(session);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Stop handlers for removed sessions
|
|
117
|
+
for (const session of trackedSessions) {
|
|
118
|
+
if (!currentSessions.has(session)) {
|
|
119
|
+
this.stopSessionHandler(session);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error('Error during session scan:', error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
startSessionHandler(tmuxSession) {
|
|
128
|
+
console.log(`Discovered new tmux session: ${tmuxSession}`);
|
|
129
|
+
const handler = new session_handler_1.TmuxSessionHandler({
|
|
130
|
+
config: this.config,
|
|
131
|
+
tmuxSession,
|
|
132
|
+
onCreateSession: (name) => this.createTmuxSession(name)
|
|
133
|
+
});
|
|
134
|
+
this.handlers.set(tmuxSession, handler);
|
|
135
|
+
handler.start();
|
|
136
|
+
console.log(`Started handler for session: ${this.config.machineId}/${tmuxSession}`);
|
|
137
|
+
}
|
|
138
|
+
stopSessionHandler(tmuxSession) {
|
|
139
|
+
console.log(`Tmux session removed: ${tmuxSession}`);
|
|
140
|
+
const handler = this.handlers.get(tmuxSession);
|
|
141
|
+
if (handler) {
|
|
142
|
+
handler.stop();
|
|
143
|
+
this.handlers.delete(tmuxSession);
|
|
144
|
+
console.log(`Stopped handler for session: ${this.config.machineId}/${tmuxSession}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
createTmuxSession(sessionName) {
|
|
148
|
+
// Sanitize session name
|
|
149
|
+
const sanitized = sessionName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
150
|
+
if (!sanitized) {
|
|
151
|
+
console.warn(`Invalid session name: ${sessionName}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (this.handlers.has(sanitized)) {
|
|
155
|
+
console.warn(`Session already exists: ${sanitized}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log(`Creating new tmux session: ${sanitized}`);
|
|
159
|
+
if (tmux.createSession(sanitized)) {
|
|
160
|
+
console.log(`Successfully created tmux session: ${sanitized}`);
|
|
161
|
+
// Force immediate scan
|
|
162
|
+
this.scanAndUpdateSessions();
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
console.error(`Failed to create tmux session: ${sanitized}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
stop() {
|
|
169
|
+
console.log('Shutting down Agent...');
|
|
170
|
+
this.running = false;
|
|
171
|
+
if (this.scanTimer) {
|
|
172
|
+
clearInterval(this.scanTimer);
|
|
173
|
+
this.scanTimer = null;
|
|
174
|
+
}
|
|
175
|
+
if (this.apiClient) {
|
|
176
|
+
this.apiClient.stop();
|
|
177
|
+
this.apiClient = null;
|
|
178
|
+
}
|
|
179
|
+
for (const handler of this.handlers.values()) {
|
|
180
|
+
handler.stop();
|
|
181
|
+
}
|
|
182
|
+
this.handlers.clear();
|
|
183
|
+
console.log('Agent shutdown complete');
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
exports.AgentRunner = AgentRunner;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AgentConfig } from './types';
|
|
2
|
+
interface SessionHandlerOptions {
|
|
3
|
+
config: AgentConfig;
|
|
4
|
+
tmuxSession: string;
|
|
5
|
+
onCreateSession?: (name: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare class TmuxSessionHandler {
|
|
8
|
+
private config;
|
|
9
|
+
private tmuxSession;
|
|
10
|
+
private sessionId;
|
|
11
|
+
private wsClient;
|
|
12
|
+
private onCreateSession?;
|
|
13
|
+
private running;
|
|
14
|
+
private lastScreen;
|
|
15
|
+
private lastForceSendTime;
|
|
16
|
+
private lastChangeTime;
|
|
17
|
+
private captureTimer;
|
|
18
|
+
constructor(options: SessionHandlerOptions);
|
|
19
|
+
start(): void;
|
|
20
|
+
private connectAndRun;
|
|
21
|
+
private handleKeys;
|
|
22
|
+
private startScreenCapture;
|
|
23
|
+
private stopScreenCapture;
|
|
24
|
+
stop(): void;
|
|
25
|
+
getSessionId(): string;
|
|
26
|
+
getTmuxSession(): string;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TmuxSessionHandler = void 0;
|
|
37
|
+
const websocket_1 = require("./websocket");
|
|
38
|
+
const tmux = __importStar(require("./tmux"));
|
|
39
|
+
// Capture intervals
|
|
40
|
+
const CAPTURE_INTERVAL_ACTIVE_MS = 50;
|
|
41
|
+
const CAPTURE_INTERVAL_IDLE_MS = 200;
|
|
42
|
+
const ACTIVE_THRESHOLD_MS = 2000;
|
|
43
|
+
const FORCE_SEND_INTERVAL_MS = 10000;
|
|
44
|
+
const USE_COMPRESSION = true;
|
|
45
|
+
const MIN_COMPRESS_SIZE = 512;
|
|
46
|
+
class TmuxSessionHandler {
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.wsClient = null;
|
|
49
|
+
this.running = false;
|
|
50
|
+
this.lastScreen = '';
|
|
51
|
+
this.lastForceSendTime = 0;
|
|
52
|
+
this.lastChangeTime = 0;
|
|
53
|
+
this.captureTimer = null;
|
|
54
|
+
this.config = options.config;
|
|
55
|
+
this.tmuxSession = options.tmuxSession;
|
|
56
|
+
this.sessionId = `${options.config.machineId}/${options.tmuxSession}`;
|
|
57
|
+
this.onCreateSession = options.onCreateSession;
|
|
58
|
+
}
|
|
59
|
+
start() {
|
|
60
|
+
if (this.running)
|
|
61
|
+
return;
|
|
62
|
+
this.running = true;
|
|
63
|
+
// Add jitter to prevent thundering herd
|
|
64
|
+
const jitter = Math.floor(Math.random() * 5000);
|
|
65
|
+
console.log(`[${this.tmuxSession}] Starting in ${jitter}ms`);
|
|
66
|
+
setTimeout(() => this.connectAndRun(), jitter);
|
|
67
|
+
}
|
|
68
|
+
connectAndRun() {
|
|
69
|
+
if (!this.running)
|
|
70
|
+
return;
|
|
71
|
+
this.wsClient = new websocket_1.RelayWebSocketClient({
|
|
72
|
+
url: this.config.relay,
|
|
73
|
+
sessionId: this.sessionId,
|
|
74
|
+
machineId: this.config.machineId,
|
|
75
|
+
token: this.config.token,
|
|
76
|
+
label: this.tmuxSession,
|
|
77
|
+
autoReconnect: true
|
|
78
|
+
});
|
|
79
|
+
this.wsClient.on('connected', () => {
|
|
80
|
+
console.log(`[${this.tmuxSession}] Connected to relay`);
|
|
81
|
+
this.startScreenCapture();
|
|
82
|
+
});
|
|
83
|
+
this.wsClient.on('disconnected', ({ code, reason }) => {
|
|
84
|
+
console.log(`[${this.tmuxSession}] Disconnected: code=${code}, reason=${reason}`);
|
|
85
|
+
this.stopScreenCapture();
|
|
86
|
+
});
|
|
87
|
+
this.wsClient.on('keys', (keys) => {
|
|
88
|
+
this.handleKeys(keys);
|
|
89
|
+
});
|
|
90
|
+
this.wsClient.on('resize', ({ cols, rows }) => {
|
|
91
|
+
console.log(`[${this.tmuxSession}] Resize: ${cols}x${rows}`);
|
|
92
|
+
tmux.resizeWindow(this.tmuxSession, cols, rows);
|
|
93
|
+
});
|
|
94
|
+
this.wsClient.on('createSession', (name) => {
|
|
95
|
+
console.log(`[${this.tmuxSession}] Create session request: ${name}`);
|
|
96
|
+
if (this.onCreateSession) {
|
|
97
|
+
this.onCreateSession(name);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.wsClient.on('killSession', () => {
|
|
101
|
+
console.log(`[${this.tmuxSession}] Kill session request`);
|
|
102
|
+
tmux.killSession(this.tmuxSession);
|
|
103
|
+
this.stop();
|
|
104
|
+
});
|
|
105
|
+
this.wsClient.on('error', (error) => {
|
|
106
|
+
console.error(`[${this.tmuxSession}] WebSocket error:`, error.message);
|
|
107
|
+
});
|
|
108
|
+
this.wsClient.connect();
|
|
109
|
+
}
|
|
110
|
+
handleKeys(keys) {
|
|
111
|
+
tmux.sendKeys(this.tmuxSession, keys, false);
|
|
112
|
+
}
|
|
113
|
+
startScreenCapture() {
|
|
114
|
+
if (this.captureTimer)
|
|
115
|
+
return;
|
|
116
|
+
const capture = () => {
|
|
117
|
+
if (!this.running || !this.wsClient?.getConnected()) {
|
|
118
|
+
// Wait and retry
|
|
119
|
+
this.captureTimer = setTimeout(capture, 500);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const screen = tmux.capturePane(this.tmuxSession);
|
|
124
|
+
if (screen !== null) {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
const changed = screen !== this.lastScreen;
|
|
127
|
+
const forceTime = (now - this.lastForceSendTime) >= FORCE_SEND_INTERVAL_MS;
|
|
128
|
+
if (changed || forceTime) {
|
|
129
|
+
this.lastScreen = screen;
|
|
130
|
+
this.lastForceSendTime = now;
|
|
131
|
+
if (changed) {
|
|
132
|
+
this.lastChangeTime = now;
|
|
133
|
+
}
|
|
134
|
+
// Send clear screen + content
|
|
135
|
+
const fullOutput = '\x1b[2J\x1b[H' + screen;
|
|
136
|
+
const data = Buffer.from(fullOutput, 'utf-8');
|
|
137
|
+
// Compress if enabled and data is large enough
|
|
138
|
+
if (USE_COMPRESSION && data.length > MIN_COMPRESS_SIZE) {
|
|
139
|
+
this.wsClient.sendScreenCompressed(data);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.wsClient.sendScreen(data);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Adaptive sleep: faster when active, slower when idle
|
|
146
|
+
const isActive = (now - this.lastChangeTime) < ACTIVE_THRESHOLD_MS;
|
|
147
|
+
const sleepMs = isActive ? CAPTURE_INTERVAL_ACTIVE_MS : CAPTURE_INTERVAL_IDLE_MS;
|
|
148
|
+
this.captureTimer = setTimeout(capture, sleepMs);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
this.captureTimer = setTimeout(capture, CAPTURE_INTERVAL_IDLE_MS);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error(`[${this.tmuxSession}] Screen capture error:`, error);
|
|
156
|
+
this.captureTimer = setTimeout(capture, 500);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
capture();
|
|
160
|
+
console.log(`[${this.tmuxSession}] Screen capture started`);
|
|
161
|
+
}
|
|
162
|
+
stopScreenCapture() {
|
|
163
|
+
if (this.captureTimer) {
|
|
164
|
+
clearTimeout(this.captureTimer);
|
|
165
|
+
this.captureTimer = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
stop() {
|
|
169
|
+
console.log(`[${this.tmuxSession}] Stopping`);
|
|
170
|
+
this.running = false;
|
|
171
|
+
this.stopScreenCapture();
|
|
172
|
+
if (this.wsClient) {
|
|
173
|
+
this.wsClient.destroy();
|
|
174
|
+
this.wsClient = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
getSessionId() {
|
|
178
|
+
return this.sessionId;
|
|
179
|
+
}
|
|
180
|
+
getTmuxSession() {
|
|
181
|
+
return this.tmuxSession;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
exports.TmuxSessionHandler = TmuxSessionHandler;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TmuxSession } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Scan for all tmux sessions
|
|
4
|
+
*/
|
|
5
|
+
export declare function scanSessions(): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Get detailed session info
|
|
8
|
+
*/
|
|
9
|
+
export declare function listSessions(): TmuxSession[];
|
|
10
|
+
/**
|
|
11
|
+
* Capture tmux pane content with escape sequences (colors)
|
|
12
|
+
*/
|
|
13
|
+
export declare function capturePane(sessionName: string): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Send keys to tmux session
|
|
16
|
+
*/
|
|
17
|
+
export declare function sendKeys(target: string, keys: string, enter?: boolean): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Resize tmux window
|
|
20
|
+
*/
|
|
21
|
+
export declare function resizeWindow(sessionName: string, cols: number, rows: number): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Create new tmux session
|
|
24
|
+
*/
|
|
25
|
+
export declare function createSession(sessionName: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Kill tmux session
|
|
28
|
+
*/
|
|
29
|
+
export declare function killSession(sessionName: string): boolean;
|