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,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scanSessions = scanSessions;
|
|
4
|
+
exports.listSessions = listSessions;
|
|
5
|
+
exports.capturePane = capturePane;
|
|
6
|
+
exports.sendKeys = sendKeys;
|
|
7
|
+
exports.resizeWindow = resizeWindow;
|
|
8
|
+
exports.createSession = createSession;
|
|
9
|
+
exports.killSession = killSession;
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
/**
|
|
12
|
+
* Scan for all tmux sessions
|
|
13
|
+
*/
|
|
14
|
+
function scanSessions() {
|
|
15
|
+
try {
|
|
16
|
+
const output = (0, child_process_1.execSync)('tmux ls -F "#{session_name}"', {
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
19
|
+
});
|
|
20
|
+
return output
|
|
21
|
+
.trim()
|
|
22
|
+
.split('\n')
|
|
23
|
+
.filter(s => s.length > 0);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// tmux not running or no sessions
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get detailed session info
|
|
32
|
+
*/
|
|
33
|
+
function listSessions() {
|
|
34
|
+
try {
|
|
35
|
+
const output = (0, child_process_1.execSync)('tmux list-sessions -F "#{session_name}|#{session_windows}|#{session_created}|#{session_attached}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
36
|
+
return output
|
|
37
|
+
.trim()
|
|
38
|
+
.split('\n')
|
|
39
|
+
.filter(line => line.length > 0)
|
|
40
|
+
.map(line => {
|
|
41
|
+
const [name, windows, created, attached] = line.split('|');
|
|
42
|
+
return {
|
|
43
|
+
name,
|
|
44
|
+
windows: parseInt(windows, 10) || 1,
|
|
45
|
+
created: created || undefined,
|
|
46
|
+
attached: attached === '1'
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Capture tmux pane content with escape sequences (colors)
|
|
56
|
+
*/
|
|
57
|
+
function capturePane(sessionName) {
|
|
58
|
+
try {
|
|
59
|
+
const output = (0, child_process_1.execSync)(`tmux capture-pane -t "${sessionName}" -p -e -N`, {
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
62
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB
|
|
63
|
+
});
|
|
64
|
+
// Normalize line endings
|
|
65
|
+
return output.replace(/\n/g, '\r\n');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Send keys to tmux session
|
|
73
|
+
*/
|
|
74
|
+
function sendKeys(target, keys, enter = true) {
|
|
75
|
+
try {
|
|
76
|
+
// Handle special keys
|
|
77
|
+
if (keys === '\x03') {
|
|
78
|
+
// Ctrl+C
|
|
79
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${target}" C-c`, { stdio: 'pipe' });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (keys === '\x04') {
|
|
83
|
+
// Ctrl+D
|
|
84
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${target}" C-d`, { stdio: 'pipe' });
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
// For Enter key only
|
|
88
|
+
if (keys === '\n' || keys === '\r\n') {
|
|
89
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${target}" Enter`, { stdio: 'pipe' });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
// For text with newline at end (command + enter)
|
|
93
|
+
if (keys.endsWith('\n')) {
|
|
94
|
+
const cmd = keys.slice(0, -1);
|
|
95
|
+
if (cmd) {
|
|
96
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${target}" -l "${escapeForShell(cmd)}"`, { stdio: 'pipe' });
|
|
97
|
+
}
|
|
98
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${target}" Enter`, { stdio: 'pipe' });
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
// Regular text input
|
|
102
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${target}" -l "${escapeForShell(keys)}"`, { stdio: 'pipe' });
|
|
103
|
+
if (enter) {
|
|
104
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${target}" Enter`, { stdio: 'pipe' });
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Resize tmux window
|
|
114
|
+
*/
|
|
115
|
+
function resizeWindow(sessionName, cols, rows) {
|
|
116
|
+
try {
|
|
117
|
+
(0, child_process_1.execSync)(`tmux resize-window -t "${sessionName}" -x ${cols} -y ${rows}`, { stdio: 'pipe' });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Create new tmux session
|
|
126
|
+
*/
|
|
127
|
+
function createSession(sessionName) {
|
|
128
|
+
try {
|
|
129
|
+
// Sanitize session name
|
|
130
|
+
const sanitized = sessionName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
131
|
+
if (!sanitized)
|
|
132
|
+
return false;
|
|
133
|
+
(0, child_process_1.execSync)(`tmux new-session -d -s "${sanitized}"`, { stdio: 'pipe' });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Kill tmux session
|
|
142
|
+
*/
|
|
143
|
+
function killSession(sessionName) {
|
|
144
|
+
try {
|
|
145
|
+
(0, child_process_1.execSync)(`tmux kill-session -t "${sessionName}"`, { stdio: 'pipe' });
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Escape string for shell
|
|
154
|
+
*/
|
|
155
|
+
function escapeForShell(str) {
|
|
156
|
+
return str.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
|
|
157
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface AgentConfig {
|
|
2
|
+
machineId: string;
|
|
3
|
+
relay: string;
|
|
4
|
+
token: string;
|
|
5
|
+
api?: ApiConfig;
|
|
6
|
+
}
|
|
7
|
+
export interface ApiConfig {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
agentId?: string;
|
|
10
|
+
exec?: ExecConfig;
|
|
11
|
+
llm?: LlmConfig;
|
|
12
|
+
}
|
|
13
|
+
export interface ExecConfig {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
shell: string;
|
|
16
|
+
workingDir?: string;
|
|
17
|
+
allowedCommands?: string[];
|
|
18
|
+
defaultTimeout: number;
|
|
19
|
+
}
|
|
20
|
+
export interface LlmConfig {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
provider: 'ollama' | 'openai';
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
model: string;
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface Message {
|
|
28
|
+
type: string;
|
|
29
|
+
role?: string;
|
|
30
|
+
session?: string;
|
|
31
|
+
payload?: string;
|
|
32
|
+
meta?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
export interface TmuxSession {
|
|
35
|
+
name: string;
|
|
36
|
+
windows: number;
|
|
37
|
+
created?: string;
|
|
38
|
+
attached: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface ExecResult {
|
|
41
|
+
exitCode: number;
|
|
42
|
+
stdout: string;
|
|
43
|
+
stderr: string;
|
|
44
|
+
duration: number;
|
|
45
|
+
}
|
|
46
|
+
export interface LlmMessage {
|
|
47
|
+
role: 'system' | 'user' | 'assistant';
|
|
48
|
+
content: string;
|
|
49
|
+
}
|
|
50
|
+
export interface LlmResponse {
|
|
51
|
+
id: string;
|
|
52
|
+
object: string;
|
|
53
|
+
created: number;
|
|
54
|
+
model: string;
|
|
55
|
+
choices: Array<{
|
|
56
|
+
index: number;
|
|
57
|
+
message: {
|
|
58
|
+
role: string;
|
|
59
|
+
content: string;
|
|
60
|
+
};
|
|
61
|
+
finish_reason: string;
|
|
62
|
+
}>;
|
|
63
|
+
usage?: {
|
|
64
|
+
prompt_tokens: number;
|
|
65
|
+
completion_tokens: number;
|
|
66
|
+
total_tokens: number;
|
|
67
|
+
};
|
|
68
|
+
error?: {
|
|
69
|
+
message: string;
|
|
70
|
+
type: string;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { Message } from './types';
|
|
3
|
+
interface WebSocketClientOptions {
|
|
4
|
+
url: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
machineId: string;
|
|
7
|
+
token: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
autoReconnect?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class RelayWebSocketClient extends EventEmitter {
|
|
12
|
+
private ws;
|
|
13
|
+
private url;
|
|
14
|
+
private sessionId;
|
|
15
|
+
private machineId;
|
|
16
|
+
private token;
|
|
17
|
+
private label;
|
|
18
|
+
private autoReconnect;
|
|
19
|
+
private isConnected;
|
|
20
|
+
private reconnectAttempts;
|
|
21
|
+
private circuitBreakerOpen;
|
|
22
|
+
private circuitBreakerResetTime;
|
|
23
|
+
private reconnectTimer;
|
|
24
|
+
private destroyed;
|
|
25
|
+
constructor(options: WebSocketClientOptions);
|
|
26
|
+
connect(): void;
|
|
27
|
+
private registerAsHost;
|
|
28
|
+
private handleMessage;
|
|
29
|
+
private handleError;
|
|
30
|
+
private scheduleReconnect;
|
|
31
|
+
send(message: Message): boolean;
|
|
32
|
+
sendScreen(data: Buffer): boolean;
|
|
33
|
+
sendScreenCompressed(data: Buffer): boolean;
|
|
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
|
+
getConnected(): boolean;
|
|
43
|
+
destroy(): void;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.RelayWebSocketClient = void 0;
|
|
40
|
+
const ws_1 = __importDefault(require("ws"));
|
|
41
|
+
const events_1 = require("events");
|
|
42
|
+
const zlib = __importStar(require("zlib"));
|
|
43
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
44
|
+
const BASE_RECONNECT_DELAY_MS = 2000;
|
|
45
|
+
const MAX_RECONNECT_DELAY_MS = 60000;
|
|
46
|
+
const CIRCUIT_BREAKER_DURATION_MS = 120000;
|
|
47
|
+
class RelayWebSocketClient extends events_1.EventEmitter {
|
|
48
|
+
constructor(options) {
|
|
49
|
+
super();
|
|
50
|
+
this.ws = null;
|
|
51
|
+
this.isConnected = false;
|
|
52
|
+
this.reconnectAttempts = 0;
|
|
53
|
+
this.circuitBreakerOpen = false;
|
|
54
|
+
this.circuitBreakerResetTime = 0;
|
|
55
|
+
this.reconnectTimer = null;
|
|
56
|
+
this.destroyed = false;
|
|
57
|
+
this.url = options.url;
|
|
58
|
+
this.sessionId = options.sessionId;
|
|
59
|
+
this.machineId = options.machineId;
|
|
60
|
+
this.token = options.token;
|
|
61
|
+
this.label = options.label || options.sessionId;
|
|
62
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
63
|
+
}
|
|
64
|
+
connect() {
|
|
65
|
+
if (this.destroyed)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
this.ws = new ws_1.default(this.url);
|
|
69
|
+
this.ws.on('open', () => {
|
|
70
|
+
this.isConnected = true;
|
|
71
|
+
this.reconnectAttempts = 0;
|
|
72
|
+
this.circuitBreakerOpen = false;
|
|
73
|
+
this.emit('connected');
|
|
74
|
+
this.registerAsHost();
|
|
75
|
+
});
|
|
76
|
+
this.ws.on('message', (data) => {
|
|
77
|
+
try {
|
|
78
|
+
const message = JSON.parse(data.toString());
|
|
79
|
+
this.handleMessage(message);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error('Failed to parse message:', e);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
this.ws.on('close', (code, reason) => {
|
|
86
|
+
this.isConnected = false;
|
|
87
|
+
this.emit('disconnected', { code, reason: reason.toString() });
|
|
88
|
+
if (this.autoReconnect && !this.destroyed) {
|
|
89
|
+
this.scheduleReconnect();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
this.ws.on('error', (error) => {
|
|
93
|
+
this.emit('error', error);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
this.emit('error', error);
|
|
98
|
+
if (this.autoReconnect && !this.destroyed) {
|
|
99
|
+
this.scheduleReconnect();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
registerAsHost() {
|
|
104
|
+
const meta = {
|
|
105
|
+
label: this.label,
|
|
106
|
+
machineId: this.machineId,
|
|
107
|
+
};
|
|
108
|
+
if (this.token) {
|
|
109
|
+
meta.token = this.token;
|
|
110
|
+
}
|
|
111
|
+
this.send({
|
|
112
|
+
type: 'register',
|
|
113
|
+
role: 'host',
|
|
114
|
+
session: this.sessionId,
|
|
115
|
+
meta
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
handleMessage(message) {
|
|
119
|
+
switch (message.type) {
|
|
120
|
+
case 'keys':
|
|
121
|
+
if (message.session === this.sessionId && message.payload) {
|
|
122
|
+
this.emit('keys', message.payload);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
case 'resize':
|
|
126
|
+
if (message.session === this.sessionId && message.meta) {
|
|
127
|
+
const cols = parseInt(message.meta.cols, 10);
|
|
128
|
+
const rows = parseInt(message.meta.rows, 10);
|
|
129
|
+
if (!isNaN(cols) && !isNaN(rows)) {
|
|
130
|
+
this.emit('resize', { cols, rows });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
case 'createSession':
|
|
135
|
+
if (message.meta?.sessionName) {
|
|
136
|
+
this.emit('createSession', message.meta.sessionName);
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case 'killSession':
|
|
140
|
+
if (message.session === this.sessionId) {
|
|
141
|
+
this.emit('killSession');
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
case 'error':
|
|
145
|
+
this.handleError(message);
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
this.emit('message', message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
handleError(message) {
|
|
152
|
+
const meta = message.meta;
|
|
153
|
+
if (!meta)
|
|
154
|
+
return;
|
|
155
|
+
if (meta.code === 'LIMIT_EXCEEDED') {
|
|
156
|
+
console.error('============================================================');
|
|
157
|
+
console.error('SESSION LIMIT EXCEEDED');
|
|
158
|
+
console.error('============================================================');
|
|
159
|
+
console.error(`Resource: ${meta.resource}`);
|
|
160
|
+
console.error(`Current: ${meta.current}, Max: ${meta.max}`);
|
|
161
|
+
console.error(`Message: ${meta.messageEn}`);
|
|
162
|
+
console.error(`한국어: ${meta.messageKo}`);
|
|
163
|
+
console.error(`Upgrade at: ${meta.upgradeUrl}`);
|
|
164
|
+
console.error('============================================================');
|
|
165
|
+
// Stop reconnection and exit
|
|
166
|
+
this.autoReconnect = false;
|
|
167
|
+
this.destroy();
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.error(`Error: code=${meta.code}, message=${meta.messageEn}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
scheduleReconnect() {
|
|
175
|
+
if (this.destroyed)
|
|
176
|
+
return;
|
|
177
|
+
// Check circuit breaker
|
|
178
|
+
if (this.circuitBreakerOpen) {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
if (now < this.circuitBreakerResetTime) {
|
|
181
|
+
const remainingSeconds = Math.ceil((this.circuitBreakerResetTime - now) / 1000);
|
|
182
|
+
console.log(`Circuit breaker open. Retry in ${remainingSeconds} seconds`);
|
|
183
|
+
this.reconnectTimer = setTimeout(() => this.scheduleReconnect(), this.circuitBreakerResetTime - now);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.log('Circuit breaker reset');
|
|
188
|
+
this.circuitBreakerOpen = false;
|
|
189
|
+
this.reconnectAttempts = 0;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
this.reconnectAttempts++;
|
|
193
|
+
// Check max attempts
|
|
194
|
+
if (this.reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
|
|
195
|
+
console.error(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Circuit breaker active for ${CIRCUIT_BREAKER_DURATION_MS / 1000} seconds`);
|
|
196
|
+
this.circuitBreakerOpen = true;
|
|
197
|
+
this.circuitBreakerResetTime = Date.now() + CIRCUIT_BREAKER_DURATION_MS;
|
|
198
|
+
this.reconnectAttempts = 0;
|
|
199
|
+
this.reconnectTimer = setTimeout(() => this.scheduleReconnect(), CIRCUIT_BREAKER_DURATION_MS);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Exponential backoff with jitter
|
|
203
|
+
const delay = Math.min(BASE_RECONNECT_DELAY_MS * Math.pow(2, this.reconnectAttempts - 1), MAX_RECONNECT_DELAY_MS);
|
|
204
|
+
const jitter = Math.random() * delay * 0.5;
|
|
205
|
+
const reconnectDelay = Math.floor(delay + jitter);
|
|
206
|
+
console.log(`Reconnecting in ${reconnectDelay}ms (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
207
|
+
this.reconnectTimer = setTimeout(() => {
|
|
208
|
+
if (!this.isConnected && !this.destroyed) {
|
|
209
|
+
this.connect();
|
|
210
|
+
}
|
|
211
|
+
}, reconnectDelay);
|
|
212
|
+
}
|
|
213
|
+
send(message) {
|
|
214
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
this.ws.send(JSON.stringify(message));
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
sendScreen(data) {
|
|
226
|
+
if (!this.isConnected)
|
|
227
|
+
return false;
|
|
228
|
+
const base64Data = data.toString('base64');
|
|
229
|
+
return this.send({
|
|
230
|
+
type: 'screen',
|
|
231
|
+
session: this.sessionId,
|
|
232
|
+
payload: base64Data
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
sendScreenCompressed(data) {
|
|
236
|
+
if (!this.isConnected)
|
|
237
|
+
return false;
|
|
238
|
+
try {
|
|
239
|
+
const compressed = zlib.gzipSync(data);
|
|
240
|
+
const base64Data = compressed.toString('base64');
|
|
241
|
+
return this.send({
|
|
242
|
+
type: 'screenGz',
|
|
243
|
+
session: this.sessionId,
|
|
244
|
+
payload: base64Data
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return this.sendScreen(data);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Send file content to be displayed in the web FileViewer
|
|
253
|
+
* @param filename - The name of the file
|
|
254
|
+
* @param content - The file content (UTF-8 for text, base64 for images)
|
|
255
|
+
* @param contentType - MIME type (e.g., 'text/markdown', 'text/html', 'image/png')
|
|
256
|
+
* @param path - Optional file path
|
|
257
|
+
*/
|
|
258
|
+
sendFileView(filename, content, contentType, path) {
|
|
259
|
+
if (!this.isConnected)
|
|
260
|
+
return false;
|
|
261
|
+
return this.send({
|
|
262
|
+
type: 'file_view',
|
|
263
|
+
session: this.sessionId,
|
|
264
|
+
meta: {
|
|
265
|
+
filename,
|
|
266
|
+
contentType,
|
|
267
|
+
path: path || ''
|
|
268
|
+
},
|
|
269
|
+
payload: content
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
getConnected() {
|
|
273
|
+
return this.isConnected;
|
|
274
|
+
}
|
|
275
|
+
destroy() {
|
|
276
|
+
this.destroyed = true;
|
|
277
|
+
this.autoReconnect = false;
|
|
278
|
+
if (this.reconnectTimer) {
|
|
279
|
+
clearTimeout(this.reconnectTimer);
|
|
280
|
+
this.reconnectTimer = null;
|
|
281
|
+
}
|
|
282
|
+
if (this.ws) {
|
|
283
|
+
this.ws.close();
|
|
284
|
+
this.ws = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
exports.RelayWebSocketClient = RelayWebSocketClient;
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface Agent {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string | null;
|
|
4
|
+
machineId: string | null;
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
apiEnabled: boolean;
|
|
7
|
+
lastConnectedAt: string | null;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TmuxSession {
|
|
11
|
+
name: string;
|
|
12
|
+
windows: number;
|
|
13
|
+
created: string | null;
|
|
14
|
+
attached: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface SendKeysResult {
|
|
17
|
+
success: boolean;
|
|
18
|
+
agentId: string;
|
|
19
|
+
target: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
declare class ApiClient {
|
|
23
|
+
private getHeaders;
|
|
24
|
+
listAgents(): Promise<Agent[]>;
|
|
25
|
+
getAgent(agentId: string): Promise<Agent>;
|
|
26
|
+
listSessions(agentId: string): Promise<TmuxSession[]>;
|
|
27
|
+
sendKeys(agentId: string, target: string, keys: string, enter?: boolean): Promise<SendKeysResult>;
|
|
28
|
+
findAgentByName(name: string): Promise<Agent | null>;
|
|
29
|
+
}
|
|
30
|
+
export declare const api: ApiClient;
|
|
31
|
+
export {};
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.api = void 0;
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
class ApiClient {
|
|
10
|
+
getHeaders() {
|
|
11
|
+
const apiKey = (0, config_1.getApiKey)();
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
throw new Error('Not logged in. Run: sessioncast login');
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
17
|
+
'Content-Type': 'application/json'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async listAgents() {
|
|
21
|
+
const url = `${(0, config_1.getApiUrl)()}/api/v1/agents`;
|
|
22
|
+
const response = await (0, node_fetch_1.default)(url, {
|
|
23
|
+
method: 'GET',
|
|
24
|
+
headers: this.getHeaders()
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
28
|
+
throw new Error(error.message || 'Failed to list agents');
|
|
29
|
+
}
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
return data.agents;
|
|
32
|
+
}
|
|
33
|
+
async getAgent(agentId) {
|
|
34
|
+
const url = `${(0, config_1.getApiUrl)()}/api/v1/agents/${agentId}`;
|
|
35
|
+
const response = await (0, node_fetch_1.default)(url, {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
headers: this.getHeaders()
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error('Agent not found');
|
|
41
|
+
}
|
|
42
|
+
return await response.json();
|
|
43
|
+
}
|
|
44
|
+
async listSessions(agentId) {
|
|
45
|
+
const url = `${(0, config_1.getApiUrl)()}/api/v1/agents/${agentId}/sessions`;
|
|
46
|
+
const response = await (0, node_fetch_1.default)(url, {
|
|
47
|
+
method: 'GET',
|
|
48
|
+
headers: this.getHeaders()
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
52
|
+
throw new Error(error.message || 'Failed to list sessions');
|
|
53
|
+
}
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
return data.sessions;
|
|
56
|
+
}
|
|
57
|
+
async sendKeys(agentId, target, keys, enter = true) {
|
|
58
|
+
const url = `${(0, config_1.getApiUrl)()}/api/v1/agents/${agentId}/send-keys`;
|
|
59
|
+
const response = await (0, node_fetch_1.default)(url, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: this.getHeaders(),
|
|
62
|
+
body: JSON.stringify({ target, keys, enter })
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
66
|
+
throw new Error(error.message || 'Failed to send keys');
|
|
67
|
+
}
|
|
68
|
+
return await response.json();
|
|
69
|
+
}
|
|
70
|
+
async findAgentByName(name) {
|
|
71
|
+
const agents = await this.listAgents();
|
|
72
|
+
// Search by label or machineId
|
|
73
|
+
return agents.find(a => a.label?.toLowerCase() === name.toLowerCase() ||
|
|
74
|
+
a.machineId?.toLowerCase() === name.toLowerCase() ||
|
|
75
|
+
a.id === name) || null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.api = new ApiClient();
|