genbox-agent 0.0.2
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/dist/daemon.d.ts +12 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +272 -0
- package/dist/daemon.js.map +1 -0
- package/dist/hook.d.ts +15 -0
- package/dist/hook.d.ts.map +1 -0
- package/dist/hook.js +207 -0
- package/dist/hook.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/session-manager.d.ts +52 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +253 -0
- package/dist/session-manager.js.map +1 -0
- package/package.json +28 -0
- package/src/daemon.ts +329 -0
- package/src/hook.ts +200 -0
- package/src/index.ts +1 -0
- package/src/session-manager.ts +270 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,253 @@
|
|
|
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.SessionManager = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
/**
|
|
40
|
+
* Manages tmux sessions running Claude Code.
|
|
41
|
+
*/
|
|
42
|
+
class SessionManager {
|
|
43
|
+
genboxId;
|
|
44
|
+
TMUX_PREFIX = 'claude-';
|
|
45
|
+
sessions = new Map();
|
|
46
|
+
constructor(genboxId) {
|
|
47
|
+
this.genboxId = genboxId;
|
|
48
|
+
}
|
|
49
|
+
runTmux(args, timeout = 10000) {
|
|
50
|
+
try {
|
|
51
|
+
const stdout = (0, child_process_1.execSync)(`tmux ${args.join(' ')}`, {
|
|
52
|
+
timeout,
|
|
53
|
+
encoding: 'utf-8',
|
|
54
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
55
|
+
});
|
|
56
|
+
return { stdout: stdout || '', stderr: '', success: true };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
stdout: error.stdout?.toString() || '',
|
|
61
|
+
stderr: error.stderr?.toString() || error.message,
|
|
62
|
+
success: false,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
generateSessionId() {
|
|
67
|
+
return crypto
|
|
68
|
+
.createHash('sha256')
|
|
69
|
+
.update(`${this.genboxId}-${Date.now()}-${process.pid}`)
|
|
70
|
+
.digest('hex')
|
|
71
|
+
.slice(0, 16);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a new Claude Code session in tmux.
|
|
75
|
+
*/
|
|
76
|
+
createSession(projectPath = '/home/dev') {
|
|
77
|
+
const sessionId = this.generateSessionId();
|
|
78
|
+
const sessionName = `${this.TMUX_PREFIX}${sessionId}`;
|
|
79
|
+
// Create a new detached tmux session
|
|
80
|
+
const result = this.runTmux([
|
|
81
|
+
'new-session',
|
|
82
|
+
'-d',
|
|
83
|
+
'-s', sessionName,
|
|
84
|
+
'-c', projectPath,
|
|
85
|
+
]);
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
console.error(`Failed to create tmux session: ${result.stderr}`);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
// Give tmux a moment to initialize
|
|
91
|
+
(0, child_process_1.execSync)('sleep 0.5');
|
|
92
|
+
// Start Claude Code in the session
|
|
93
|
+
this.runTmux([
|
|
94
|
+
'send-keys',
|
|
95
|
+
'-t', sessionName,
|
|
96
|
+
'claude',
|
|
97
|
+
'Enter',
|
|
98
|
+
]);
|
|
99
|
+
const session = {
|
|
100
|
+
sessionName,
|
|
101
|
+
sessionId,
|
|
102
|
+
projectPath,
|
|
103
|
+
createdAt: Date.now(),
|
|
104
|
+
status: 'active',
|
|
105
|
+
};
|
|
106
|
+
this.sessions.set(sessionId, session);
|
|
107
|
+
return session;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Send a prompt to a Claude Code session.
|
|
111
|
+
*/
|
|
112
|
+
sendPrompt(sessionId, prompt) {
|
|
113
|
+
const session = this.sessions.get(sessionId);
|
|
114
|
+
const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
|
|
115
|
+
// Send the prompt text with literal flag to handle special characters
|
|
116
|
+
const textResult = this.runTmux([
|
|
117
|
+
'send-keys',
|
|
118
|
+
'-t', sessionName,
|
|
119
|
+
'-l', // Literal flag
|
|
120
|
+
`"${prompt.replace(/"/g, '\\"')}"`,
|
|
121
|
+
]);
|
|
122
|
+
if (!textResult.success) {
|
|
123
|
+
console.error(`Failed to send prompt text: ${textResult.stderr}`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
// Send Enter to submit the prompt
|
|
127
|
+
const enterResult = this.runTmux([
|
|
128
|
+
'send-keys',
|
|
129
|
+
'-t', sessionName,
|
|
130
|
+
'Enter',
|
|
131
|
+
]);
|
|
132
|
+
if (!enterResult.success) {
|
|
133
|
+
console.error(`Failed to send Enter: ${enterResult.stderr}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Send a keystroke to a Claude Code session.
|
|
140
|
+
*/
|
|
141
|
+
sendKeystroke(sessionId, key) {
|
|
142
|
+
const session = this.sessions.get(sessionId);
|
|
143
|
+
const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
|
|
144
|
+
// Map common key names to tmux key names
|
|
145
|
+
const keyMap = {
|
|
146
|
+
enter: 'Enter',
|
|
147
|
+
escape: 'Escape',
|
|
148
|
+
up: 'Up',
|
|
149
|
+
down: 'Down',
|
|
150
|
+
left: 'Left',
|
|
151
|
+
right: 'Right',
|
|
152
|
+
tab: 'Tab',
|
|
153
|
+
backspace: 'BSpace',
|
|
154
|
+
delete: 'DC',
|
|
155
|
+
'ctrl-c': 'C-c',
|
|
156
|
+
'ctrl-d': 'C-d',
|
|
157
|
+
'ctrl-z': 'C-z',
|
|
158
|
+
y: 'y',
|
|
159
|
+
n: 'n',
|
|
160
|
+
};
|
|
161
|
+
const tmuxKey = keyMap[key.toLowerCase()] || key;
|
|
162
|
+
const result = this.runTmux([
|
|
163
|
+
'send-keys',
|
|
164
|
+
'-t', sessionName,
|
|
165
|
+
tmuxKey,
|
|
166
|
+
]);
|
|
167
|
+
return result.success;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Capture the terminal output from a session.
|
|
171
|
+
*/
|
|
172
|
+
getOutput(sessionId, lines = 100) {
|
|
173
|
+
const session = this.sessions.get(sessionId);
|
|
174
|
+
const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
|
|
175
|
+
const result = this.runTmux([
|
|
176
|
+
'capture-pane',
|
|
177
|
+
'-t', sessionName,
|
|
178
|
+
'-p', // Print to stdout
|
|
179
|
+
'-S', `-${lines}`, // Start from lines back
|
|
180
|
+
]);
|
|
181
|
+
return result.success ? result.stdout : null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get the status of a session.
|
|
185
|
+
*/
|
|
186
|
+
getSessionStatus(sessionId) {
|
|
187
|
+
const session = this.sessions.get(sessionId);
|
|
188
|
+
const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
|
|
189
|
+
// Check if session exists
|
|
190
|
+
const hasSession = this.runTmux(['has-session', '-t', sessionName]);
|
|
191
|
+
if (!hasSession.success) {
|
|
192
|
+
return 'ended';
|
|
193
|
+
}
|
|
194
|
+
// Get pane content to determine status
|
|
195
|
+
const output = this.getOutput(sessionId, 10);
|
|
196
|
+
if (output) {
|
|
197
|
+
const lowerOutput = output.toLowerCase();
|
|
198
|
+
const lastLine = output.split('\n').pop() || '';
|
|
199
|
+
if (lowerOutput.includes('waiting for input') || lastLine.includes('>')) {
|
|
200
|
+
return 'waiting_input';
|
|
201
|
+
}
|
|
202
|
+
if (lowerOutput.includes('error')) {
|
|
203
|
+
return 'error';
|
|
204
|
+
}
|
|
205
|
+
if (lowerOutput.includes('thinking') || lowerOutput.includes('working')) {
|
|
206
|
+
return 'active';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return 'idle';
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Kill a Claude Code session.
|
|
213
|
+
*/
|
|
214
|
+
killSession(sessionId) {
|
|
215
|
+
const session = this.sessions.get(sessionId);
|
|
216
|
+
const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
|
|
217
|
+
const result = this.runTmux(['kill-session', '-t', sessionName]);
|
|
218
|
+
if (result.success) {
|
|
219
|
+
this.sessions.delete(sessionId);
|
|
220
|
+
}
|
|
221
|
+
return result.success;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* List all Claude Code sessions.
|
|
225
|
+
*/
|
|
226
|
+
listSessions() {
|
|
227
|
+
const result = this.runTmux([
|
|
228
|
+
'list-sessions',
|
|
229
|
+
'-F', '#{session_name}:#{session_created}',
|
|
230
|
+
]);
|
|
231
|
+
if (!result.success) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
const sessions = [];
|
|
235
|
+
for (const line of result.stdout.trim().split('\n')) {
|
|
236
|
+
if (!line || !line.startsWith(this.TMUX_PREFIX)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const [sessionName, createdAtStr] = line.split(':');
|
|
240
|
+
const sessionId = sessionName.replace(this.TMUX_PREFIX, '');
|
|
241
|
+
const createdAt = parseInt(createdAtStr, 10) * 1000 || 0;
|
|
242
|
+
sessions.push({
|
|
243
|
+
sessionId,
|
|
244
|
+
sessionName,
|
|
245
|
+
createdAt,
|
|
246
|
+
status: this.getSessionStatus(sessionId),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return sessions;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
exports.SessionManager = SessionManager;
|
|
253
|
+
//# sourceMappingURL=session-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "genbox-agent",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Agent daemon for monitoring and remotely controlling Claude Code sessions in genbox VMs",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"genbox-agent": "./dist/daemon.js",
|
|
9
|
+
"claude-hook": "./dist/hook.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/daemon.js",
|
|
14
|
+
"dev": "tsx watch src/daemon.ts",
|
|
15
|
+
"hook": "node dist/hook.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"socket.io-client": "^4.8.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.10.0",
|
|
22
|
+
"tsx": "^4.7.0",
|
|
23
|
+
"typescript": "^5.3.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/daemon.ts
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Genbox Agent Daemon
|
|
4
|
+
*
|
|
5
|
+
* WebSocket client that runs on each genbox VM and:
|
|
6
|
+
* 1. Connects to the backend monitoring service
|
|
7
|
+
* 2. Receives remote commands (send_prompt, send_keystroke, create_session)
|
|
8
|
+
* 3. Manages tmux sessions running Claude Code
|
|
9
|
+
* 4. Streams terminal output back to the dashboard
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { io, Socket } from 'socket.io-client';
|
|
13
|
+
import { SessionManager } from './session-manager';
|
|
14
|
+
|
|
15
|
+
// Configuration from environment
|
|
16
|
+
const GENBOX_ID = process.env.GENBOX_ID || '';
|
|
17
|
+
const GENBOX_TOKEN = process.env.GENBOX_TOKEN || '';
|
|
18
|
+
const MONITORING_WS_URL = process.env.MONITORING_WS_URL || 'http://localhost:3001';
|
|
19
|
+
|
|
20
|
+
// Output polling interval (ms)
|
|
21
|
+
const OUTPUT_POLL_INTERVAL = 2000;
|
|
22
|
+
|
|
23
|
+
interface RequestData {
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
prompt?: string;
|
|
26
|
+
key?: string;
|
|
27
|
+
projectPath?: string;
|
|
28
|
+
lines?: number;
|
|
29
|
+
requestId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class GenboxAgentDaemon {
|
|
33
|
+
private socket: Socket | null = null;
|
|
34
|
+
private sessionManager: SessionManager;
|
|
35
|
+
private running = false;
|
|
36
|
+
private outputPollers: Map<string, NodeJS.Timeout> = new Map();
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly genboxId: string,
|
|
40
|
+
private readonly token: string,
|
|
41
|
+
private readonly wsUrl: string,
|
|
42
|
+
) {
|
|
43
|
+
this.sessionManager = new SessionManager(genboxId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async connect(): Promise<boolean> {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
console.log(`Connecting to ${this.wsUrl}/claude-ws...`);
|
|
49
|
+
|
|
50
|
+
this.socket = io(`${this.wsUrl}/claude-ws`, {
|
|
51
|
+
query: { genboxId: this.genboxId, type: 'genbox' },
|
|
52
|
+
auth: { genboxId: this.genboxId, token: this.token },
|
|
53
|
+
transports: ['websocket', 'polling'],
|
|
54
|
+
reconnection: true,
|
|
55
|
+
reconnectionAttempts: Infinity,
|
|
56
|
+
reconnectionDelay: 1000,
|
|
57
|
+
reconnectionDelayMax: 30000,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.socket.on('connect', () => {
|
|
61
|
+
console.log(`[${this.genboxId}] Connected to monitoring server`);
|
|
62
|
+
this.running = true;
|
|
63
|
+
this.onConnect();
|
|
64
|
+
resolve(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.socket.on('disconnect', (reason) => {
|
|
68
|
+
console.log(`[${this.genboxId}] Disconnected: ${reason}`);
|
|
69
|
+
this.running = false;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
this.socket.on('connect_error', (error) => {
|
|
73
|
+
console.error(`[${this.genboxId}] Connection error:`, error.message);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Register event handlers
|
|
77
|
+
this.socket.on('send_prompt', this.onSendPrompt.bind(this));
|
|
78
|
+
this.socket.on('send_keystroke', this.onSendKeystroke.bind(this));
|
|
79
|
+
this.socket.on('create_session', this.onCreateSession.bind(this));
|
|
80
|
+
this.socket.on('get_output', this.onGetOutput.bind(this));
|
|
81
|
+
this.socket.on('kill_session', this.onKillSession.bind(this));
|
|
82
|
+
this.socket.on('list_sessions', this.onListSessions.bind(this));
|
|
83
|
+
this.socket.on('error', this.onError.bind(this));
|
|
84
|
+
|
|
85
|
+
// Timeout after 10 seconds
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
if (!this.socket?.connected) {
|
|
88
|
+
console.error('Connection timeout');
|
|
89
|
+
resolve(false);
|
|
90
|
+
}
|
|
91
|
+
}, 10000);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private onConnect(): void {
|
|
96
|
+
// Register as genbox agent
|
|
97
|
+
this.socket?.emit('register_agent', {
|
|
98
|
+
genboxId: this.genboxId,
|
|
99
|
+
token: this.token,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Send initial session list
|
|
103
|
+
const sessions = this.sessionManager.listSessions();
|
|
104
|
+
this.socket?.emit('agent_sessions', {
|
|
105
|
+
genboxId: this.genboxId,
|
|
106
|
+
sessions,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private onSendPrompt(data: RequestData): void {
|
|
111
|
+
const { sessionId, prompt, requestId } = data;
|
|
112
|
+
if (!sessionId || !prompt) return;
|
|
113
|
+
|
|
114
|
+
console.log(`[${this.genboxId}] Sending prompt to ${sessionId}: ${prompt.slice(0, 50)}...`);
|
|
115
|
+
|
|
116
|
+
const success = this.sessionManager.sendPrompt(sessionId, prompt);
|
|
117
|
+
|
|
118
|
+
this.socket?.emit('prompt_result', {
|
|
119
|
+
genboxId: this.genboxId,
|
|
120
|
+
sessionId,
|
|
121
|
+
requestId,
|
|
122
|
+
success,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Capture output after a delay
|
|
126
|
+
if (success) {
|
|
127
|
+
setTimeout(() => this.captureAndSendOutput(sessionId), 1000);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private onSendKeystroke(data: RequestData): void {
|
|
132
|
+
const { sessionId, key, requestId } = data;
|
|
133
|
+
if (!sessionId || !key) return;
|
|
134
|
+
|
|
135
|
+
console.log(`[${this.genboxId}] Sending keystroke to ${sessionId}: ${key}`);
|
|
136
|
+
|
|
137
|
+
const success = this.sessionManager.sendKeystroke(sessionId, key);
|
|
138
|
+
|
|
139
|
+
this.socket?.emit('keystroke_result', {
|
|
140
|
+
genboxId: this.genboxId,
|
|
141
|
+
sessionId,
|
|
142
|
+
requestId,
|
|
143
|
+
success,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private onCreateSession(data: RequestData): void {
|
|
148
|
+
const { projectPath = '/home/dev', requestId } = data;
|
|
149
|
+
|
|
150
|
+
console.log(`[${this.genboxId}] Creating new session at ${projectPath}`);
|
|
151
|
+
|
|
152
|
+
const session = this.sessionManager.createSession(projectPath);
|
|
153
|
+
|
|
154
|
+
if (session) {
|
|
155
|
+
this.socket?.emit('session_created', {
|
|
156
|
+
genboxId: this.genboxId,
|
|
157
|
+
sessionId: session.sessionId,
|
|
158
|
+
sessionName: session.sessionName,
|
|
159
|
+
projectPath: session.projectPath,
|
|
160
|
+
requestId,
|
|
161
|
+
success: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Start output polling for this session
|
|
165
|
+
this.startOutputPolling(session.sessionId);
|
|
166
|
+
} else {
|
|
167
|
+
this.socket?.emit('session_created', {
|
|
168
|
+
genboxId: this.genboxId,
|
|
169
|
+
requestId,
|
|
170
|
+
success: false,
|
|
171
|
+
error: 'Failed to create session',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private onGetOutput(data: RequestData): void {
|
|
177
|
+
const { sessionId, lines = 100, requestId } = data;
|
|
178
|
+
if (!sessionId) return;
|
|
179
|
+
|
|
180
|
+
const output = this.sessionManager.getOutput(sessionId, lines);
|
|
181
|
+
const status = this.sessionManager.getSessionStatus(sessionId);
|
|
182
|
+
|
|
183
|
+
this.socket?.emit('output_update', {
|
|
184
|
+
genboxId: this.genboxId,
|
|
185
|
+
sessionId,
|
|
186
|
+
requestId,
|
|
187
|
+
output: output || '',
|
|
188
|
+
status,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private onKillSession(data: RequestData): void {
|
|
193
|
+
const { sessionId, requestId } = data;
|
|
194
|
+
if (!sessionId) return;
|
|
195
|
+
|
|
196
|
+
console.log(`[${this.genboxId}] Killing session ${sessionId}`);
|
|
197
|
+
|
|
198
|
+
const success = this.sessionManager.killSession(sessionId);
|
|
199
|
+
this.stopOutputPolling(sessionId);
|
|
200
|
+
|
|
201
|
+
this.socket?.emit('session_killed', {
|
|
202
|
+
genboxId: this.genboxId,
|
|
203
|
+
sessionId,
|
|
204
|
+
requestId,
|
|
205
|
+
success,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private onListSessions(data: RequestData): void {
|
|
210
|
+
const { requestId } = data;
|
|
211
|
+
|
|
212
|
+
const sessions = this.sessionManager.listSessions();
|
|
213
|
+
|
|
214
|
+
this.socket?.emit('sessions_list', {
|
|
215
|
+
genboxId: this.genboxId,
|
|
216
|
+
requestId,
|
|
217
|
+
sessions,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private onError(data: any): void {
|
|
222
|
+
console.error(`[${this.genboxId}] Error from server:`, data);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private startOutputPolling(sessionId: string): void {
|
|
226
|
+
if (this.outputPollers.has(sessionId)) return;
|
|
227
|
+
|
|
228
|
+
let lastOutput = '';
|
|
229
|
+
|
|
230
|
+
const poller = setInterval(() => {
|
|
231
|
+
if (!this.running) {
|
|
232
|
+
this.stopOutputPolling(sessionId);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const output = this.sessionManager.getOutput(sessionId, 100);
|
|
237
|
+
const status = this.sessionManager.getSessionStatus(sessionId);
|
|
238
|
+
|
|
239
|
+
// Only send if output changed
|
|
240
|
+
if (output && output !== lastOutput) {
|
|
241
|
+
lastOutput = output;
|
|
242
|
+
this.socket?.emit('output_update', {
|
|
243
|
+
genboxId: this.genboxId,
|
|
244
|
+
sessionId,
|
|
245
|
+
output,
|
|
246
|
+
status,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Stop polling if session ended
|
|
251
|
+
if (status === 'ended') {
|
|
252
|
+
this.stopOutputPolling(sessionId);
|
|
253
|
+
}
|
|
254
|
+
}, OUTPUT_POLL_INTERVAL);
|
|
255
|
+
|
|
256
|
+
this.outputPollers.set(sessionId, poller);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private stopOutputPolling(sessionId: string): void {
|
|
260
|
+
const poller = this.outputPollers.get(sessionId);
|
|
261
|
+
if (poller) {
|
|
262
|
+
clearInterval(poller);
|
|
263
|
+
this.outputPollers.delete(sessionId);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private captureAndSendOutput(sessionId: string): void {
|
|
268
|
+
const output = this.sessionManager.getOutput(sessionId, 100);
|
|
269
|
+
const status = this.sessionManager.getSessionStatus(sessionId);
|
|
270
|
+
|
|
271
|
+
if (output && this.socket?.connected) {
|
|
272
|
+
this.socket.emit('output_update', {
|
|
273
|
+
genboxId: this.genboxId,
|
|
274
|
+
sessionId,
|
|
275
|
+
output,
|
|
276
|
+
status,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async run(): Promise<void> {
|
|
282
|
+
console.log(`Starting Genbox Agent Daemon for ${this.genboxId}`);
|
|
283
|
+
|
|
284
|
+
const connected = await this.connect();
|
|
285
|
+
if (!connected) {
|
|
286
|
+
console.log('Initial connection failed, will keep retrying...');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Keep running
|
|
290
|
+
await new Promise<void>((resolve) => {
|
|
291
|
+
const shutdown = () => {
|
|
292
|
+
console.log('Shutting down...');
|
|
293
|
+
this.running = false;
|
|
294
|
+
|
|
295
|
+
// Stop all output pollers
|
|
296
|
+
for (const sessionId of this.outputPollers.keys()) {
|
|
297
|
+
this.stopOutputPolling(sessionId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.socket?.disconnect();
|
|
301
|
+
resolve();
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
process.on('SIGTERM', shutdown);
|
|
305
|
+
process.on('SIGINT', shutdown);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Main entry point
|
|
311
|
+
async function main(): Promise<void> {
|
|
312
|
+
if (!GENBOX_ID) {
|
|
313
|
+
console.error('Error: GENBOX_ID environment variable not set');
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!GENBOX_TOKEN) {
|
|
318
|
+
console.error('Error: GENBOX_TOKEN environment variable not set');
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const daemon = new GenboxAgentDaemon(GENBOX_ID, GENBOX_TOKEN, MONITORING_WS_URL);
|
|
323
|
+
await daemon.run();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
main().catch((error) => {
|
|
327
|
+
console.error('Fatal error:', error);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
});
|