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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# SessionCast CLI
|
|
2
|
+
|
|
3
|
+
Node.js agent and CLI for [SessionCast](https://sessioncast.io) - a real-time terminal sharing platform.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### Agent
|
|
8
|
+
- **Auto-discovery**: Automatically detects and connects tmux sessions
|
|
9
|
+
- **Real-time screen capture**: Streams terminal output with gzip compression
|
|
10
|
+
- **Circuit breaker**: Prevents reconnection storms with exponential backoff
|
|
11
|
+
- **Interactive control**: Supports keyboard input, resize, and session management
|
|
12
|
+
- **File viewer**: Cmd+Click on file paths to view files in browser
|
|
13
|
+
|
|
14
|
+
### CLI Commands
|
|
15
|
+
- `sessioncast login <api-key>` - Authenticate with API key
|
|
16
|
+
- `sessioncast logout` - Clear stored credentials
|
|
17
|
+
- `sessioncast status` - Check authentication status
|
|
18
|
+
- `sessioncast agents` - List registered agents
|
|
19
|
+
- `sessioncast list [agent]` - List tmux sessions
|
|
20
|
+
- `sessioncast send <target> <keys>` - Send keys to a session
|
|
21
|
+
- `sessioncast agent` - Start the agent
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g sessioncast-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
1. **Get your agent token** from [app.sessioncast.io](https://app.sessioncast.io)
|
|
32
|
+
|
|
33
|
+
2. **Create config file** `~/.sessioncast.yml`:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
machineId: my-machine
|
|
37
|
+
relay: wss://relay.sessioncast.io/ws
|
|
38
|
+
token: agt_your_agent_token_here
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. **Start the agent**:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
sessioncast agent
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
4. **View your sessions** at [app.sessioncast.io](https://app.sessioncast.io)
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Create `~/.sessioncast.yml` or `~/.tmux-remote.yml`:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
machineId: my-machine
|
|
55
|
+
relay: wss://relay.sessioncast.io/ws
|
|
56
|
+
token: agt_your_agent_token_here
|
|
57
|
+
|
|
58
|
+
# Optional: API configuration
|
|
59
|
+
api:
|
|
60
|
+
enabled: true
|
|
61
|
+
agentId: "your-agent-uuid"
|
|
62
|
+
|
|
63
|
+
exec:
|
|
64
|
+
enabled: true
|
|
65
|
+
shell: /bin/bash
|
|
66
|
+
workingDir: /home/user
|
|
67
|
+
defaultTimeout: 30000
|
|
68
|
+
|
|
69
|
+
llm:
|
|
70
|
+
enabled: false
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Environment Variables
|
|
74
|
+
|
|
75
|
+
- `SESSIONCAST_CONFIG` - Custom config file path
|
|
76
|
+
- `TMUX_REMOTE_CONFIG` - Alternative config file path
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
### Start the Agent
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Run agent (foreground)
|
|
84
|
+
sessioncast agent
|
|
85
|
+
|
|
86
|
+
# Run agent (background)
|
|
87
|
+
nohup sessioncast agent > /tmp/sessioncast-agent.log 2>&1 &
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Send Keys to Session
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Send text to a session
|
|
94
|
+
sessioncast send my-machine/dev "ls -la"
|
|
95
|
+
|
|
96
|
+
# Send special keys
|
|
97
|
+
sessioncast send my-machine/dev "Enter"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Architecture
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
┌─────────────┐ WebSocket ┌─────────────┐ WebSocket ┌─────────────┐
|
|
104
|
+
│ Agent │ ◄─────────────────► │ Relay │ ◄────────────────► │ Viewer │
|
|
105
|
+
│ (Node.js) │ screen/keys │ (Server) │ screen/keys │ (Web) │
|
|
106
|
+
└─────────────┘ └─────────────┘ └─────────────┘
|
|
107
|
+
│
|
|
108
|
+
│ tmux
|
|
109
|
+
▼
|
|
110
|
+
┌─────────────┐
|
|
111
|
+
│ tmux │
|
|
112
|
+
│ sessions │
|
|
113
|
+
└─────────────┘
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Circuit Breaker
|
|
117
|
+
|
|
118
|
+
The agent implements a circuit breaker pattern to prevent reconnection storms:
|
|
119
|
+
|
|
120
|
+
- **Max reconnect attempts**: 5
|
|
121
|
+
- **Base delay**: 1 second
|
|
122
|
+
- **Max delay**: 30 seconds (with exponential backoff + jitter)
|
|
123
|
+
- **Circuit breaker duration**: 2 minutes cooldown after max attempts
|
|
124
|
+
|
|
125
|
+
## Requirements
|
|
126
|
+
|
|
127
|
+
- Node.js >= 18
|
|
128
|
+
- tmux installed on the host machine
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
133
|
+
|
|
134
|
+
## Support
|
|
135
|
+
|
|
136
|
+
- Homepage: https://sessioncast.io
|
|
137
|
+
- Email: devload@sessioncast.io
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AgentConfig } from './types';
|
|
2
|
+
export declare class ApiWebSocketClient {
|
|
3
|
+
private ws;
|
|
4
|
+
private config;
|
|
5
|
+
private apiConfig;
|
|
6
|
+
private commandService;
|
|
7
|
+
private llmService;
|
|
8
|
+
private isConnected;
|
|
9
|
+
private reconnectAttempts;
|
|
10
|
+
private circuitBreakerOpen;
|
|
11
|
+
private circuitBreakerResetTime;
|
|
12
|
+
private reconnectTimer;
|
|
13
|
+
private destroyed;
|
|
14
|
+
constructor(config: AgentConfig);
|
|
15
|
+
start(): void;
|
|
16
|
+
private connect;
|
|
17
|
+
private registerAsApiAgent;
|
|
18
|
+
private handleMessage;
|
|
19
|
+
private handleExec;
|
|
20
|
+
private handleLlmChat;
|
|
21
|
+
private handleSendKeys;
|
|
22
|
+
private handleListSessions;
|
|
23
|
+
private sendApiResponse;
|
|
24
|
+
private send;
|
|
25
|
+
private scheduleReconnect;
|
|
26
|
+
stop(): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
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.ApiWebSocketClient = void 0;
|
|
40
|
+
const ws_1 = __importDefault(require("ws"));
|
|
41
|
+
const exec_service_1 = require("./exec-service");
|
|
42
|
+
const llm_service_1 = require("./llm-service");
|
|
43
|
+
const tmux = __importStar(require("./tmux"));
|
|
44
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
45
|
+
const BASE_RECONNECT_DELAY_MS = 2000;
|
|
46
|
+
const MAX_RECONNECT_DELAY_MS = 60000;
|
|
47
|
+
const CIRCUIT_BREAKER_DURATION_MS = 120000;
|
|
48
|
+
class ApiWebSocketClient {
|
|
49
|
+
constructor(config) {
|
|
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.config = config;
|
|
58
|
+
this.apiConfig = config.api || { enabled: false };
|
|
59
|
+
this.commandService = new exec_service_1.CommandExecutionService(this.apiConfig.exec);
|
|
60
|
+
this.llmService = new llm_service_1.LlmService(this.apiConfig.llm);
|
|
61
|
+
}
|
|
62
|
+
start() {
|
|
63
|
+
if (!this.apiConfig.enabled || !this.apiConfig.agentId) {
|
|
64
|
+
console.log('[API] API client disabled or no agentId configured');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Add jitter to prevent thundering herd
|
|
68
|
+
const jitter = Math.floor(Math.random() * 2000);
|
|
69
|
+
console.log(`[API] Starting in ${jitter}ms`);
|
|
70
|
+
setTimeout(() => this.connect(), jitter);
|
|
71
|
+
}
|
|
72
|
+
connect() {
|
|
73
|
+
if (this.destroyed)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
this.ws = new ws_1.default(this.config.relay);
|
|
77
|
+
this.ws.on('open', () => {
|
|
78
|
+
this.isConnected = true;
|
|
79
|
+
this.reconnectAttempts = 0;
|
|
80
|
+
this.circuitBreakerOpen = false;
|
|
81
|
+
console.log('[API] Connected to relay');
|
|
82
|
+
this.registerAsApiAgent();
|
|
83
|
+
});
|
|
84
|
+
this.ws.on('message', (data) => {
|
|
85
|
+
try {
|
|
86
|
+
const message = JSON.parse(data.toString());
|
|
87
|
+
this.handleMessage(message);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.error('[API] Failed to parse message:', e);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
this.ws.on('close', (code, reason) => {
|
|
94
|
+
this.isConnected = false;
|
|
95
|
+
console.log(`[API] Disconnected: code=${code}, reason=${reason.toString()}`);
|
|
96
|
+
if (!this.destroyed) {
|
|
97
|
+
this.scheduleReconnect();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.ws.on('error', (error) => {
|
|
101
|
+
console.error('[API] WebSocket error:', error.message);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error('[API] Connection error:', error.message);
|
|
106
|
+
if (!this.destroyed) {
|
|
107
|
+
this.scheduleReconnect();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
registerAsApiAgent() {
|
|
112
|
+
const meta = {
|
|
113
|
+
machineId: this.config.machineId,
|
|
114
|
+
agentId: this.apiConfig.agentId
|
|
115
|
+
};
|
|
116
|
+
if (this.config.token) {
|
|
117
|
+
meta.token = this.config.token;
|
|
118
|
+
}
|
|
119
|
+
this.send({
|
|
120
|
+
type: 'register',
|
|
121
|
+
role: 'host',
|
|
122
|
+
session: `api-${this.apiConfig.agentId}`,
|
|
123
|
+
meta
|
|
124
|
+
});
|
|
125
|
+
console.log(`[API] Registered as API agent: ${this.apiConfig.agentId}`);
|
|
126
|
+
}
|
|
127
|
+
async handleMessage(message) {
|
|
128
|
+
switch (message.type) {
|
|
129
|
+
case 'exec':
|
|
130
|
+
await this.handleExec(message);
|
|
131
|
+
break;
|
|
132
|
+
case 'llm_chat':
|
|
133
|
+
await this.handleLlmChat(message);
|
|
134
|
+
break;
|
|
135
|
+
case 'send_keys':
|
|
136
|
+
await this.handleSendKeys(message);
|
|
137
|
+
break;
|
|
138
|
+
case 'list_sessions':
|
|
139
|
+
await this.handleListSessions(message);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async handleExec(message) {
|
|
144
|
+
const meta = message.meta;
|
|
145
|
+
if (!meta?.requestId)
|
|
146
|
+
return;
|
|
147
|
+
try {
|
|
148
|
+
const payload = meta.payload ? JSON.parse(meta.payload) : {};
|
|
149
|
+
const { command, cwd, timeout, sessionId } = payload;
|
|
150
|
+
console.log(`[API] exec: command=${command}, cwd=${cwd}, timeout=${timeout}`);
|
|
151
|
+
const result = await this.commandService.executeCommand(command, cwd, timeout, sessionId);
|
|
152
|
+
this.sendApiResponse(meta.requestId, result);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.sendApiResponse(meta.requestId, {
|
|
156
|
+
exitCode: -1,
|
|
157
|
+
stdout: '',
|
|
158
|
+
stderr: `Error: ${error.message}`,
|
|
159
|
+
duration: 0
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async handleLlmChat(message) {
|
|
164
|
+
const meta = message.meta;
|
|
165
|
+
if (!meta?.requestId)
|
|
166
|
+
return;
|
|
167
|
+
try {
|
|
168
|
+
const payload = meta.payload ? JSON.parse(meta.payload) : {};
|
|
169
|
+
const { model, messages, temperature, max_tokens, stream } = payload;
|
|
170
|
+
console.log(`[API] llm_chat: model=${model}, messages=${messages?.length || 0}`);
|
|
171
|
+
const result = await this.llmService.chat(model, messages, temperature, max_tokens, stream);
|
|
172
|
+
this.sendApiResponse(meta.requestId, result);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.sendApiResponse(meta.requestId, {
|
|
176
|
+
error: {
|
|
177
|
+
message: error.message,
|
|
178
|
+
type: 'internal_error'
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async handleSendKeys(message) {
|
|
184
|
+
const meta = message.meta;
|
|
185
|
+
if (!meta?.requestId)
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
const payload = meta.payload ? JSON.parse(meta.payload) : {};
|
|
189
|
+
const { target, keys, enter = true } = payload;
|
|
190
|
+
if (!target || !keys) {
|
|
191
|
+
this.sendApiResponse(meta.requestId, {
|
|
192
|
+
success: false,
|
|
193
|
+
error: 'target and keys are required'
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
console.log(`[API] send_keys: target=${target}, keys=${keys}, enter=${enter}`);
|
|
198
|
+
const success = tmux.sendKeys(target, keys, enter);
|
|
199
|
+
this.sendApiResponse(meta.requestId, { success, target });
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
this.sendApiResponse(meta.requestId, {
|
|
203
|
+
success: false,
|
|
204
|
+
error: error.message
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async handleListSessions(message) {
|
|
209
|
+
const meta = message.meta;
|
|
210
|
+
if (!meta?.requestId)
|
|
211
|
+
return;
|
|
212
|
+
try {
|
|
213
|
+
console.log('[API] list_sessions');
|
|
214
|
+
const sessions = tmux.listSessions();
|
|
215
|
+
this.sendApiResponse(meta.requestId, { sessions });
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
this.sendApiResponse(meta.requestId, {
|
|
219
|
+
sessions: [],
|
|
220
|
+
error: error.message
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
sendApiResponse(requestId, response) {
|
|
225
|
+
this.send({
|
|
226
|
+
type: 'api_response',
|
|
227
|
+
meta: {
|
|
228
|
+
requestId,
|
|
229
|
+
payload: JSON.stringify(response)
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
send(message) {
|
|
234
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
this.ws.send(JSON.stringify(message));
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
scheduleReconnect() {
|
|
246
|
+
if (this.destroyed)
|
|
247
|
+
return;
|
|
248
|
+
// Check circuit breaker
|
|
249
|
+
if (this.circuitBreakerOpen) {
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
if (now < this.circuitBreakerResetTime) {
|
|
252
|
+
const remainingSeconds = Math.ceil((this.circuitBreakerResetTime - now) / 1000);
|
|
253
|
+
console.log(`[API] Circuit breaker open. Retry in ${remainingSeconds} seconds`);
|
|
254
|
+
this.reconnectTimer = setTimeout(() => this.scheduleReconnect(), this.circuitBreakerResetTime - now);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
console.log('[API] Circuit breaker reset');
|
|
259
|
+
this.circuitBreakerOpen = false;
|
|
260
|
+
this.reconnectAttempts = 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
this.reconnectAttempts++;
|
|
264
|
+
if (this.reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
|
|
265
|
+
console.error(`[API] Max reconnect attempts reached. Circuit breaker active for ${CIRCUIT_BREAKER_DURATION_MS / 1000}s`);
|
|
266
|
+
this.circuitBreakerOpen = true;
|
|
267
|
+
this.circuitBreakerResetTime = Date.now() + CIRCUIT_BREAKER_DURATION_MS;
|
|
268
|
+
this.reconnectAttempts = 0;
|
|
269
|
+
this.reconnectTimer = setTimeout(() => this.scheduleReconnect(), CIRCUIT_BREAKER_DURATION_MS);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const delay = Math.min(BASE_RECONNECT_DELAY_MS * Math.pow(2, this.reconnectAttempts - 1), MAX_RECONNECT_DELAY_MS);
|
|
273
|
+
const jitter = Math.random() * delay * 0.5;
|
|
274
|
+
const reconnectDelay = Math.floor(delay + jitter);
|
|
275
|
+
console.log(`[API] Reconnecting in ${reconnectDelay}ms (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
276
|
+
this.reconnectTimer = setTimeout(() => {
|
|
277
|
+
if (!this.isConnected && !this.destroyed) {
|
|
278
|
+
this.connect();
|
|
279
|
+
}
|
|
280
|
+
}, reconnectDelay);
|
|
281
|
+
}
|
|
282
|
+
stop() {
|
|
283
|
+
console.log('[API] Stopping');
|
|
284
|
+
this.destroyed = true;
|
|
285
|
+
if (this.reconnectTimer) {
|
|
286
|
+
clearTimeout(this.reconnectTimer);
|
|
287
|
+
this.reconnectTimer = null;
|
|
288
|
+
}
|
|
289
|
+
if (this.ws) {
|
|
290
|
+
this.ws.close();
|
|
291
|
+
this.ws = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
exports.ApiWebSocketClient = ApiWebSocketClient;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CommandExecutionService = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
class CommandExecutionService {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config || {
|
|
8
|
+
enabled: false,
|
|
9
|
+
shell: '/bin/bash',
|
|
10
|
+
defaultTimeout: 30000
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async executeCommand(command, cwd, timeout, sessionId) {
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
if (!this.config.enabled) {
|
|
16
|
+
return {
|
|
17
|
+
exitCode: -1,
|
|
18
|
+
stdout: '',
|
|
19
|
+
stderr: 'Command execution is disabled on this agent',
|
|
20
|
+
duration: 0
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Check allowed commands if configured
|
|
24
|
+
if (this.config.allowedCommands && this.config.allowedCommands.length > 0) {
|
|
25
|
+
const allowed = this.config.allowedCommands.some(pattern => command.startsWith(pattern) || new RegExp(pattern).test(command));
|
|
26
|
+
if (!allowed) {
|
|
27
|
+
return {
|
|
28
|
+
exitCode: -1,
|
|
29
|
+
stdout: '',
|
|
30
|
+
stderr: 'Command not in allowed list',
|
|
31
|
+
duration: 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const timeoutMs = timeout ?? this.config.defaultTimeout;
|
|
36
|
+
const workingDir = cwd ?? this.config.workingDir;
|
|
37
|
+
const shell = this.config.shell || '/bin/bash';
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
try {
|
|
40
|
+
// If sessionId is provided, run in tmux session
|
|
41
|
+
if (sessionId) {
|
|
42
|
+
try {
|
|
43
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${sessionId}" "${escapeForShell(command)}" Enter`, {
|
|
44
|
+
stdio: 'pipe',
|
|
45
|
+
timeout: timeoutMs
|
|
46
|
+
});
|
|
47
|
+
resolve({
|
|
48
|
+
exitCode: 0,
|
|
49
|
+
stdout: 'Command sent to tmux session',
|
|
50
|
+
stderr: '',
|
|
51
|
+
duration: Date.now() - startTime
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
resolve({
|
|
56
|
+
exitCode: -1,
|
|
57
|
+
stdout: '',
|
|
58
|
+
stderr: error.message || 'Failed to send to tmux',
|
|
59
|
+
duration: Date.now() - startTime
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Direct shell execution
|
|
65
|
+
const child = (0, child_process_1.spawn)(shell, ['-c', command], {
|
|
66
|
+
cwd: workingDir || undefined,
|
|
67
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
68
|
+
});
|
|
69
|
+
let stdout = '';
|
|
70
|
+
let stderr = '';
|
|
71
|
+
let killed = false;
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
killed = true;
|
|
74
|
+
child.kill('SIGKILL');
|
|
75
|
+
}, timeoutMs);
|
|
76
|
+
child.stdout.on('data', (data) => {
|
|
77
|
+
stdout += data.toString();
|
|
78
|
+
});
|
|
79
|
+
child.stderr.on('data', (data) => {
|
|
80
|
+
stderr += data.toString();
|
|
81
|
+
});
|
|
82
|
+
child.on('close', (code) => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
const duration = Date.now() - startTime;
|
|
85
|
+
if (killed) {
|
|
86
|
+
resolve({
|
|
87
|
+
exitCode: -1,
|
|
88
|
+
stdout: '',
|
|
89
|
+
stderr: `Command timed out after ${timeoutMs}ms`,
|
|
90
|
+
duration
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
resolve({
|
|
95
|
+
exitCode: code ?? -1,
|
|
96
|
+
stdout,
|
|
97
|
+
stderr,
|
|
98
|
+
duration
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
child.on('error', (error) => {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
resolve({
|
|
105
|
+
exitCode: -1,
|
|
106
|
+
stdout: '',
|
|
107
|
+
stderr: `Execution error: ${error.message}`,
|
|
108
|
+
duration: Date.now() - startTime
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
resolve({
|
|
114
|
+
exitCode: -1,
|
|
115
|
+
stdout: '',
|
|
116
|
+
stderr: `Execution error: ${error.message}`,
|
|
117
|
+
duration: Date.now() - startTime
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.CommandExecutionService = CommandExecutionService;
|
|
124
|
+
function escapeForShell(str) {
|
|
125
|
+
return str.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
|
|
126
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./tmux"), exports);
|
|
19
|
+
__exportStar(require("./websocket"), exports);
|
|
20
|
+
__exportStar(require("./session-handler"), exports);
|
|
21
|
+
__exportStar(require("./exec-service"), exports);
|
|
22
|
+
__exportStar(require("./llm-service"), exports);
|
|
23
|
+
__exportStar(require("./api-client"), exports);
|
|
24
|
+
__exportStar(require("./runner"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { LlmConfig, LlmMessage, LlmResponse } from './types';
|
|
2
|
+
export declare class LlmService {
|
|
3
|
+
private config;
|
|
4
|
+
constructor(config?: LlmConfig);
|
|
5
|
+
chat(model?: string, messages?: LlmMessage[], temperature?: number, maxTokens?: number, stream?: boolean): Promise<LlmResponse>;
|
|
6
|
+
private callOllama;
|
|
7
|
+
private convertOllamaToOpenAiFormat;
|
|
8
|
+
private callOpenAi;
|
|
9
|
+
}
|