genbox-agent 1.0.123 → 1.0.125
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/genbox-agent.d.ts +18 -0
- package/dist/genbox-agent.d.ts.map +1 -0
- package/dist/genbox-agent.js +1170 -0
- package/dist/genbox-agent.js.map +1 -0
- package/dist/readers/gemini-session-reader.d.ts +7 -0
- package/dist/readers/gemini-session-reader.d.ts.map +1 -1
- package/dist/readers/gemini-session-reader.js +154 -12
- package/dist/readers/gemini-session-reader.js.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/port-finder.js +3 -3
- package/dist/server/port-finder.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Unified Genbox Agent Daemon
|
|
5
|
+
*
|
|
6
|
+
* WebSocket client that runs on each genbox VM and supports multiple AI CLI tools:
|
|
7
|
+
* - Claude Code
|
|
8
|
+
* - Gemini CLI
|
|
9
|
+
* - OpenAI Codex CLI
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* 1. Connects to the backend monitoring service
|
|
13
|
+
* 2. Receives remote commands (send_prompt, send_keystroke, create_session)
|
|
14
|
+
* 3. Manages dtach sessions for all supported AI CLIs
|
|
15
|
+
* 4. Streams terminal output back to the dashboard
|
|
16
|
+
* 5. Auto-detects installed AI CLIs
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
// Handle --version and --help BEFORE any imports or env checks
|
|
53
|
+
const earlyArgs = process.argv.slice(2);
|
|
54
|
+
if (earlyArgs.includes('--version') || earlyArgs.includes('-v') || earlyArgs.includes('-V')) {
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
56
|
+
const pkg = require('../package.json');
|
|
57
|
+
console.log(pkg.version);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
|
|
61
|
+
console.log(`
|
|
62
|
+
genbox-agent - Unified agent for AI CLI sessions
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
genbox-agent --native [--server] Run in local/native mode
|
|
66
|
+
genbox-agent --version Show version number
|
|
67
|
+
genbox-agent --help Show this help message
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--native Run in local mode (no cloud connection required)
|
|
71
|
+
--server, -s Enable HTTP/WebSocket server for desktop app
|
|
72
|
+
--port <port> Server port (default: 47191)
|
|
73
|
+
--no-sync Disable background sync
|
|
74
|
+
--control-token Set authentication token for server
|
|
75
|
+
|
|
76
|
+
Environment Variables (for cloud mode):
|
|
77
|
+
GENBOX_ID The genbox instance ID
|
|
78
|
+
GENBOX_TOKEN Authentication token for backend
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
genbox-agent --native --server Start daemon for desktop app
|
|
82
|
+
genbox-agent --native Start daemon without server
|
|
83
|
+
`);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
const socket_io_client_1 = require("socket.io-client");
|
|
87
|
+
const child_process_1 = require("child_process");
|
|
88
|
+
const providers_1 = require("./providers");
|
|
89
|
+
// Configuration from environment
|
|
90
|
+
const GENBOX_ID = process.env.GENBOX_ID || '';
|
|
91
|
+
const GENBOX_TOKEN = process.env.GENBOX_TOKEN || '';
|
|
92
|
+
const MONITORING_WS_URL = process.env.MONITORING_WS_URL || 'http://localhost:3001';
|
|
93
|
+
// Output polling interval (ms) - lower for smoother terminal updates
|
|
94
|
+
const OUTPUT_POLL_INTERVAL = 500;
|
|
95
|
+
// System stats reporting interval (ms)
|
|
96
|
+
const STATS_INTERVAL = 10000; // 10 seconds
|
|
97
|
+
/**
|
|
98
|
+
* Collect system stats from the local machine (cross-platform: macOS + Linux)
|
|
99
|
+
*/
|
|
100
|
+
function collectSystemStats() {
|
|
101
|
+
try {
|
|
102
|
+
const isMacOS = process.platform === 'darwin';
|
|
103
|
+
let load1 = 0, load5 = 0, load15 = 0;
|
|
104
|
+
let diskTotal = 0, diskUsed = 0, diskPct = 0;
|
|
105
|
+
let memTotal = 0, memUsed = 0, memPct = 0;
|
|
106
|
+
let swapTotal = 0, swapUsed = 0, swapPct = 0;
|
|
107
|
+
let procCount = 0;
|
|
108
|
+
let uptimeSec = 0;
|
|
109
|
+
if (isMacOS) {
|
|
110
|
+
// macOS: Load averages from sysctl
|
|
111
|
+
try {
|
|
112
|
+
const loadAvgRaw = (0, child_process_1.execSync)("sysctl -n vm.loadavg", { encoding: 'utf8' }).trim();
|
|
113
|
+
// Output format: { 1.23 1.45 1.67 }
|
|
114
|
+
const match = loadAvgRaw.match(/\{\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\}/);
|
|
115
|
+
if (match) {
|
|
116
|
+
[, load1, load5, load15] = match.map(v => parseFloat(v) || 0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch { /* ignore */ }
|
|
120
|
+
// macOS: Disk usage (use -m for megabytes instead of -BM)
|
|
121
|
+
try {
|
|
122
|
+
const dfOutput = (0, child_process_1.execSync)("df -m / | tail -1", { encoding: 'utf8' }).trim();
|
|
123
|
+
const dfParts = dfOutput.split(/\s+/);
|
|
124
|
+
// macOS df -m output: Filesystem 512-blocks Used Available Capacity ...
|
|
125
|
+
diskTotal = parseInt(dfParts[1] || '0', 10);
|
|
126
|
+
diskUsed = parseInt(dfParts[2] || '0', 10);
|
|
127
|
+
diskPct = parseInt(dfParts[4]?.replace('%', '') || '0', 10);
|
|
128
|
+
}
|
|
129
|
+
catch { /* ignore */ }
|
|
130
|
+
// macOS: Memory usage via vm_stat
|
|
131
|
+
try {
|
|
132
|
+
const vmStatOutput = (0, child_process_1.execSync)("vm_stat", { encoding: 'utf8' });
|
|
133
|
+
const pageSize = 16384; // Default page size on macOS (can also get from vm_stat output)
|
|
134
|
+
const pageSizeMatch = vmStatOutput.match(/page size of (\d+) bytes/);
|
|
135
|
+
const actualPageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : pageSize;
|
|
136
|
+
const freeMatch = vmStatOutput.match(/Pages free:\s+(\d+)/);
|
|
137
|
+
const activeMatch = vmStatOutput.match(/Pages active:\s+(\d+)/);
|
|
138
|
+
const inactiveMatch = vmStatOutput.match(/Pages inactive:\s+(\d+)/);
|
|
139
|
+
const wiredMatch = vmStatOutput.match(/Pages wired down:\s+(\d+)/);
|
|
140
|
+
const compressedMatch = vmStatOutput.match(/Pages occupied by compressor:\s+(\d+)/);
|
|
141
|
+
const freePages = parseInt(freeMatch?.[1] || '0', 10);
|
|
142
|
+
const activePages = parseInt(activeMatch?.[1] || '0', 10);
|
|
143
|
+
const inactivePages = parseInt(inactiveMatch?.[1] || '0', 10);
|
|
144
|
+
const wiredPages = parseInt(wiredMatch?.[1] || '0', 10);
|
|
145
|
+
const compressedPages = parseInt(compressedMatch?.[1] || '0', 10);
|
|
146
|
+
// Get total memory from sysctl
|
|
147
|
+
const totalMemBytes = parseInt((0, child_process_1.execSync)("sysctl -n hw.memsize", { encoding: 'utf8' }).trim(), 10);
|
|
148
|
+
memTotal = Math.round(totalMemBytes / 1024 / 1024); // Convert to MB
|
|
149
|
+
const usedPages = activePages + wiredPages + compressedPages;
|
|
150
|
+
memUsed = Math.round((usedPages * actualPageSize) / 1024 / 1024);
|
|
151
|
+
memPct = memTotal > 0 ? Math.round((memUsed * 100) / memTotal) : 0;
|
|
152
|
+
}
|
|
153
|
+
catch { /* ignore */ }
|
|
154
|
+
// macOS: Swap usage via sysctl
|
|
155
|
+
try {
|
|
156
|
+
const swapUsageOutput = (0, child_process_1.execSync)("sysctl -n vm.swapusage", { encoding: 'utf8' }).trim();
|
|
157
|
+
// Output format: total = 2048.00M used = 1024.00M free = 1024.00M ...
|
|
158
|
+
const totalMatch = swapUsageOutput.match(/total\s*=\s*([\d.]+)M/);
|
|
159
|
+
const usedMatch = swapUsageOutput.match(/used\s*=\s*([\d.]+)M/);
|
|
160
|
+
swapTotal = Math.round(parseFloat(totalMatch?.[1] || '0'));
|
|
161
|
+
swapUsed = Math.round(parseFloat(usedMatch?.[1] || '0'));
|
|
162
|
+
swapPct = swapTotal > 0 ? Math.round((swapUsed * 100) / swapTotal) : 0;
|
|
163
|
+
}
|
|
164
|
+
catch { /* ignore */ }
|
|
165
|
+
// macOS: Uptime via sysctl
|
|
166
|
+
try {
|
|
167
|
+
const bootTimeOutput = (0, child_process_1.execSync)("sysctl -n kern.boottime", { encoding: 'utf8' }).trim();
|
|
168
|
+
// Output format: { sec = 1234567890, usec = 123456 } ...
|
|
169
|
+
const secMatch = bootTimeOutput.match(/sec\s*=\s*(\d+)/);
|
|
170
|
+
if (secMatch) {
|
|
171
|
+
const bootTimeSec = parseInt(secMatch[1], 10);
|
|
172
|
+
uptimeSec = Math.floor(Date.now() / 1000) - bootTimeSec;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch { /* ignore */ }
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// Linux: Load averages from /proc/loadavg
|
|
179
|
+
const loadAvgRaw = (0, child_process_1.execSync)("cat /proc/loadavg | awk '{print $1,$2,$3}'", { encoding: 'utf8' }).trim();
|
|
180
|
+
[load1, load5, load15] = loadAvgRaw.split(' ').map(parseFloat);
|
|
181
|
+
// Linux: Disk usage (root filesystem)
|
|
182
|
+
const dfOutput = (0, child_process_1.execSync)("df -BM / | tail -1", { encoding: 'utf8' }).trim();
|
|
183
|
+
const dfParts = dfOutput.split(/\s+/);
|
|
184
|
+
diskTotal = parseInt(dfParts[1]?.replace('M', '') || '0', 10);
|
|
185
|
+
diskUsed = parseInt(dfParts[2]?.replace('M', '') || '0', 10);
|
|
186
|
+
diskPct = parseInt(dfParts[4]?.replace('%', '') || '0', 10);
|
|
187
|
+
// Linux: Memory usage
|
|
188
|
+
const memOutput = (0, child_process_1.execSync)("free -m | grep Mem", { encoding: 'utf8' }).trim();
|
|
189
|
+
const memParts = memOutput.split(/\s+/);
|
|
190
|
+
memTotal = parseInt(memParts[1] || '0', 10);
|
|
191
|
+
memUsed = parseInt(memParts[2] || '0', 10);
|
|
192
|
+
memPct = memTotal > 0 ? Math.round((memUsed * 100) / memTotal) : 0;
|
|
193
|
+
// Linux: Swap usage
|
|
194
|
+
const swapOutput = (0, child_process_1.execSync)("free -m | grep Swap", { encoding: 'utf8' }).trim();
|
|
195
|
+
const swapParts = swapOutput.split(/\s+/);
|
|
196
|
+
swapTotal = parseInt(swapParts[1] || '0', 10);
|
|
197
|
+
swapUsed = parseInt(swapParts[2] || '0', 10);
|
|
198
|
+
swapPct = swapTotal > 0 ? Math.round((swapUsed * 100) / swapTotal) : 0;
|
|
199
|
+
// Linux: Uptime in seconds
|
|
200
|
+
uptimeSec = parseInt((0, child_process_1.execSync)("awk '{print int($1)}' /proc/uptime", { encoding: 'utf8' }).trim(), 10);
|
|
201
|
+
}
|
|
202
|
+
// Cross-platform: Process count
|
|
203
|
+
procCount = parseInt((0, child_process_1.execSync)("ps aux | wc -l", { encoding: 'utf8' }).trim(), 10);
|
|
204
|
+
// Docker services
|
|
205
|
+
let dockerServices = [];
|
|
206
|
+
try {
|
|
207
|
+
const dockerOutput = (0, child_process_1.execSync)("docker ps -a --format '{{.Names}}|{{.Status}}' 2>/dev/null | head -20", { encoding: 'utf8', timeout: 5000 }).trim();
|
|
208
|
+
if (dockerOutput) {
|
|
209
|
+
dockerServices = dockerOutput.split('\n').filter(Boolean).map(line => {
|
|
210
|
+
const [name, status] = line.split('|');
|
|
211
|
+
let health;
|
|
212
|
+
if (status?.includes('(healthy)'))
|
|
213
|
+
health = 'healthy';
|
|
214
|
+
else if (status?.includes('(unhealthy)'))
|
|
215
|
+
health = 'unhealthy';
|
|
216
|
+
else if (status?.includes('(health:'))
|
|
217
|
+
health = 'starting';
|
|
218
|
+
return { name: name || '', status: status || '', ...(health && { health }) };
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Docker not available or command failed
|
|
224
|
+
}
|
|
225
|
+
// PM2 services
|
|
226
|
+
let pm2Services = [];
|
|
227
|
+
try {
|
|
228
|
+
const pm2Output = (0, child_process_1.execSync)("source /home/dev/.nvm/nvm.sh 2>/dev/null && pm2 jlist 2>/dev/null", { encoding: 'utf8', shell: '/bin/bash', timeout: 5000 }).trim();
|
|
229
|
+
if (pm2Output) {
|
|
230
|
+
const pm2Data = JSON.parse(pm2Output);
|
|
231
|
+
pm2Services = pm2Data.map((app) => ({
|
|
232
|
+
name: app.name || '',
|
|
233
|
+
status: app.pm2_env?.status || 'unknown',
|
|
234
|
+
cpu: app.monit?.cpu || 0,
|
|
235
|
+
memory: Math.round((app.monit?.memory || 0) / 1024 / 1024), // Convert to MB
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// PM2 not available or command failed
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
loadAvg: [load1 || 0, load5 || 0, load15 || 0],
|
|
244
|
+
diskUsagePercent: diskPct,
|
|
245
|
+
diskUsedGb: Math.round(diskUsed / 1024),
|
|
246
|
+
diskTotalGb: Math.round(diskTotal / 1024),
|
|
247
|
+
memoryUsagePercent: memPct,
|
|
248
|
+
memoryUsedMb: memUsed,
|
|
249
|
+
memoryTotalMb: memTotal,
|
|
250
|
+
swapUsagePercent: swapPct,
|
|
251
|
+
processCount: procCount,
|
|
252
|
+
uptimeSeconds: uptimeSec,
|
|
253
|
+
dockerServices,
|
|
254
|
+
pm2Services,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error('Failed to collect system stats:', error);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
class UnifiedGenboxDaemon {
|
|
263
|
+
genboxId;
|
|
264
|
+
token;
|
|
265
|
+
wsUrl;
|
|
266
|
+
socket = null;
|
|
267
|
+
sessionManager;
|
|
268
|
+
registry;
|
|
269
|
+
running = false;
|
|
270
|
+
outputPollers = new Map();
|
|
271
|
+
statsInterval = null;
|
|
272
|
+
// Mapping from session IDs (from hooks) to dtach socket paths
|
|
273
|
+
knownSessions = new Map();
|
|
274
|
+
// Storage manager for message sync (ContinuousJsonlSync handles file watching)
|
|
275
|
+
storageManager = null;
|
|
276
|
+
/**
|
|
277
|
+
* Find a known session by ID, supporting both exact and partial matches
|
|
278
|
+
* (dashboard may show truncated IDs like "84a7b76b" instead of full UUID)
|
|
279
|
+
*/
|
|
280
|
+
findKnownSession(sessionId) {
|
|
281
|
+
// Try exact match first
|
|
282
|
+
const exact = this.knownSessions.get(sessionId);
|
|
283
|
+
if (exact)
|
|
284
|
+
return exact;
|
|
285
|
+
// Try partial match (sessionId might be truncated or full UUID might start with it)
|
|
286
|
+
for (const [fullId, mapping] of this.knownSessions.entries()) {
|
|
287
|
+
if (fullId.startsWith(sessionId) || sessionId.startsWith(fullId)) {
|
|
288
|
+
return mapping;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
constructor(genboxId, token, wsUrl) {
|
|
294
|
+
this.genboxId = genboxId;
|
|
295
|
+
this.token = token;
|
|
296
|
+
this.wsUrl = wsUrl;
|
|
297
|
+
this.registry = providers_1.ProviderRegistry.getInstance();
|
|
298
|
+
this.sessionManager = new providers_1.UnifiedSessionManager(genboxId);
|
|
299
|
+
}
|
|
300
|
+
async connect() {
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
console.log(`[${this.genboxId}] Connecting to ${this.wsUrl}/ai-monitoring...`);
|
|
303
|
+
this.socket = (0, socket_io_client_1.io)(`${this.wsUrl}/ai-monitoring`, {
|
|
304
|
+
query: { genboxId: this.genboxId, type: 'genbox' },
|
|
305
|
+
auth: { genboxId: this.genboxId, token: this.token },
|
|
306
|
+
transports: ['websocket', 'polling'],
|
|
307
|
+
reconnection: true,
|
|
308
|
+
reconnectionAttempts: Infinity,
|
|
309
|
+
reconnectionDelay: 1000,
|
|
310
|
+
reconnectionDelayMax: 30000,
|
|
311
|
+
});
|
|
312
|
+
this.socket.on('connect', () => {
|
|
313
|
+
console.log(`[${this.genboxId}] Connected to monitoring server`);
|
|
314
|
+
this.running = true;
|
|
315
|
+
this.onConnect();
|
|
316
|
+
resolve(true);
|
|
317
|
+
});
|
|
318
|
+
this.socket.on('disconnect', (reason) => {
|
|
319
|
+
console.log(`[${this.genboxId}] Disconnected: ${reason}`);
|
|
320
|
+
this.running = false;
|
|
321
|
+
});
|
|
322
|
+
this.socket.on('connect_error', (error) => {
|
|
323
|
+
console.error(`[${this.genboxId}] Connection error:`, error.message);
|
|
324
|
+
});
|
|
325
|
+
// Register event handlers
|
|
326
|
+
this.socket.on('send_prompt', this.onSendPrompt.bind(this));
|
|
327
|
+
this.socket.on('send_keystroke', this.onSendKeystroke.bind(this));
|
|
328
|
+
this.socket.on('create_session', this.onCreateSession.bind(this));
|
|
329
|
+
this.socket.on('get_output', this.onGetOutput.bind(this));
|
|
330
|
+
this.socket.on('kill_session', this.onKillSession.bind(this));
|
|
331
|
+
this.socket.on('list_sessions', this.onListSessions.bind(this));
|
|
332
|
+
this.socket.on('get_providers', this.onGetProviders.bind(this));
|
|
333
|
+
this.socket.on('known_sessions', this.onKnownSessions.bind(this));
|
|
334
|
+
this.socket.on('error', this.onError.bind(this));
|
|
335
|
+
// Timeout after 10 seconds
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
if (!this.socket?.connected) {
|
|
338
|
+
console.error('Connection timeout');
|
|
339
|
+
resolve(false);
|
|
340
|
+
}
|
|
341
|
+
}, 10000);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
onConnect() {
|
|
345
|
+
// Get detected providers
|
|
346
|
+
const detectedProviders = this.sessionManager.getDetectedProviders();
|
|
347
|
+
const providers = [];
|
|
348
|
+
for (const [provider, detection] of detectedProviders) {
|
|
349
|
+
if (detection.detected) {
|
|
350
|
+
providers.push({ provider, version: detection.version });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Register as genbox agent with provider info
|
|
354
|
+
this.socket?.emit('register_agent', {
|
|
355
|
+
genboxId: this.genboxId,
|
|
356
|
+
token: this.token,
|
|
357
|
+
providers,
|
|
358
|
+
});
|
|
359
|
+
// Send initial session list
|
|
360
|
+
const sessions = this.sessionManager.listAllSessions();
|
|
361
|
+
this.socket?.emit('agent_sessions', {
|
|
362
|
+
genboxId: this.genboxId,
|
|
363
|
+
sessions,
|
|
364
|
+
});
|
|
365
|
+
// Start output polling for all existing sessions
|
|
366
|
+
// Note: JSONL message sync is handled by ContinuousJsonlSync (not per-session watchers)
|
|
367
|
+
for (const session of sessions) {
|
|
368
|
+
this.startOutputPolling(session.sessionId);
|
|
369
|
+
}
|
|
370
|
+
if (sessions.length > 0) {
|
|
371
|
+
console.log(`[${this.genboxId}] Started output polling for ${sessions.length} existing sessions`);
|
|
372
|
+
}
|
|
373
|
+
// Start system stats polling
|
|
374
|
+
this.startStatsPolling();
|
|
375
|
+
// Send initial stats immediately
|
|
376
|
+
this.sendSystemStats();
|
|
377
|
+
console.log(`[${this.genboxId}] Detected providers:`, providers.map(p => p.provider).join(', '));
|
|
378
|
+
}
|
|
379
|
+
startStatsPolling() {
|
|
380
|
+
// Clear any existing interval
|
|
381
|
+
this.stopStatsPolling();
|
|
382
|
+
this.statsInterval = setInterval(() => {
|
|
383
|
+
if (this.running && this.socket?.connected) {
|
|
384
|
+
this.sendSystemStats();
|
|
385
|
+
}
|
|
386
|
+
}, STATS_INTERVAL);
|
|
387
|
+
console.log(`[${this.genboxId}] Started system stats polling (every ${STATS_INTERVAL / 1000}s)`);
|
|
388
|
+
}
|
|
389
|
+
stopStatsPolling() {
|
|
390
|
+
if (this.statsInterval) {
|
|
391
|
+
clearInterval(this.statsInterval);
|
|
392
|
+
this.statsInterval = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
sendSystemStats() {
|
|
396
|
+
const stats = collectSystemStats();
|
|
397
|
+
if (stats && this.socket?.connected) {
|
|
398
|
+
this.socket.emit('system_stats', {
|
|
399
|
+
genboxId: this.genboxId,
|
|
400
|
+
stats,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
onSendPrompt(data) {
|
|
405
|
+
const { sessionId, prompt, requestId } = data;
|
|
406
|
+
if (!sessionId || !prompt)
|
|
407
|
+
return;
|
|
408
|
+
console.log(`[${this.genboxId}] Sending prompt to ${sessionId}: ${prompt.slice(0, 50)}...`);
|
|
409
|
+
let success = false;
|
|
410
|
+
const knownSession = this.findKnownSession(sessionId);
|
|
411
|
+
if (knownSession?.dtachSocket) {
|
|
412
|
+
// Use direct dtach commands for known sessions
|
|
413
|
+
success = this.sendPromptToDtach(knownSession.dtachSocket, prompt);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
// Fall back to session manager
|
|
417
|
+
success = this.sessionManager.sendPrompt(sessionId, prompt);
|
|
418
|
+
}
|
|
419
|
+
this.socket?.emit('prompt_result', {
|
|
420
|
+
genboxId: this.genboxId,
|
|
421
|
+
sessionId,
|
|
422
|
+
requestId,
|
|
423
|
+
success,
|
|
424
|
+
});
|
|
425
|
+
// Capture output after a delay
|
|
426
|
+
if (success) {
|
|
427
|
+
setTimeout(() => this.captureAndSendOutput(sessionId), 1000);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
onSendKeystroke(data) {
|
|
431
|
+
const { sessionId, key, requestId } = data;
|
|
432
|
+
if (!sessionId || !key)
|
|
433
|
+
return;
|
|
434
|
+
console.log(`[${this.genboxId}] Sending keystroke to ${sessionId}: ${key}`);
|
|
435
|
+
let success = false;
|
|
436
|
+
const knownSession = this.findKnownSession(sessionId);
|
|
437
|
+
if (knownSession?.dtachSocket) {
|
|
438
|
+
// Use direct dtach commands for known sessions
|
|
439
|
+
success = this.sendKeystrokeToDtach(knownSession.dtachSocket, key);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// Fall back to session manager
|
|
443
|
+
success = this.sessionManager.sendKeystroke(sessionId, key);
|
|
444
|
+
}
|
|
445
|
+
this.socket?.emit('keystroke_result', {
|
|
446
|
+
genboxId: this.genboxId,
|
|
447
|
+
sessionId,
|
|
448
|
+
requestId,
|
|
449
|
+
success,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Key mapping for dtach (raw terminal byte sequences)
|
|
454
|
+
*/
|
|
455
|
+
keyMap = {
|
|
456
|
+
enter: '\r',
|
|
457
|
+
escape: '\x1b',
|
|
458
|
+
up: '\x1b[A',
|
|
459
|
+
down: '\x1b[B',
|
|
460
|
+
left: '\x1b[D',
|
|
461
|
+
right: '\x1b[C',
|
|
462
|
+
tab: '\t',
|
|
463
|
+
backspace: '\x7f',
|
|
464
|
+
delete: '\x1b[3~',
|
|
465
|
+
'ctrl-c': '\x03',
|
|
466
|
+
'ctrl-d': '\x04',
|
|
467
|
+
'ctrl-z': '\x1a',
|
|
468
|
+
y: 'y',
|
|
469
|
+
n: 'n',
|
|
470
|
+
};
|
|
471
|
+
sendPromptToDtach(socketPath, prompt) {
|
|
472
|
+
const { spawnSync } = require('child_process');
|
|
473
|
+
// Send the prompt text followed by Enter (carriage return)
|
|
474
|
+
const data = prompt + '\r';
|
|
475
|
+
const result = spawnSync('dtach', ['-p', socketPath], {
|
|
476
|
+
input: data,
|
|
477
|
+
timeout: 5000,
|
|
478
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
479
|
+
});
|
|
480
|
+
return result.status === 0;
|
|
481
|
+
}
|
|
482
|
+
sendKeystrokeToDtach(socketPath, key) {
|
|
483
|
+
const { spawnSync } = require('child_process');
|
|
484
|
+
// Map friendly key name to raw bytes
|
|
485
|
+
const keyBytes = this.keyMap[key.toLowerCase()] || key;
|
|
486
|
+
const result = spawnSync('dtach', ['-p', socketPath], {
|
|
487
|
+
input: keyBytes,
|
|
488
|
+
timeout: 5000,
|
|
489
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
490
|
+
});
|
|
491
|
+
return result.status === 0;
|
|
492
|
+
}
|
|
493
|
+
async onCreateSession(data) {
|
|
494
|
+
const { projectPath = '/home/dev', provider, requestId } = data;
|
|
495
|
+
console.log(`[${this.genboxId}] Creating new ${provider || 'default'} session at ${projectPath}`);
|
|
496
|
+
const session = await this.sessionManager.createSession(projectPath, provider);
|
|
497
|
+
if (session) {
|
|
498
|
+
this.socket?.emit('session_created', {
|
|
499
|
+
genboxId: this.genboxId,
|
|
500
|
+
sessionId: session.sessionId,
|
|
501
|
+
sessionName: session.sessionName,
|
|
502
|
+
provider: session.provider,
|
|
503
|
+
projectPath: session.projectPath,
|
|
504
|
+
requestId,
|
|
505
|
+
success: true,
|
|
506
|
+
});
|
|
507
|
+
// Start output polling for this session
|
|
508
|
+
this.startOutputPolling(session.sessionId);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
this.socket?.emit('session_created', {
|
|
512
|
+
genboxId: this.genboxId,
|
|
513
|
+
requestId,
|
|
514
|
+
success: false,
|
|
515
|
+
error: 'Failed to create session',
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Check if a dtach socket is alive
|
|
521
|
+
*/
|
|
522
|
+
isDtachSocketAlive(socketPath) {
|
|
523
|
+
const { spawnSync } = require('child_process');
|
|
524
|
+
const fs = require('fs');
|
|
525
|
+
if (!fs.existsSync(socketPath)) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
const result = spawnSync('dtach', ['-p', socketPath], {
|
|
530
|
+
input: '',
|
|
531
|
+
timeout: 2000,
|
|
532
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
533
|
+
});
|
|
534
|
+
return result.status === 0;
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Read output from a dtach session's log file
|
|
542
|
+
*/
|
|
543
|
+
readDtachOutput(socketPath, lines) {
|
|
544
|
+
const fs = require('fs');
|
|
545
|
+
const logPath = socketPath.replace('.sock', '.log');
|
|
546
|
+
if (!fs.existsSync(logPath)) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
551
|
+
const allLines = content.split('\n');
|
|
552
|
+
return allLines.slice(-lines).join('\n');
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
onGetOutput(data) {
|
|
559
|
+
const { sessionId, lines = 100, requestId } = data;
|
|
560
|
+
if (!sessionId)
|
|
561
|
+
return;
|
|
562
|
+
console.log(`[${this.genboxId}] get_output request for session ${sessionId}`);
|
|
563
|
+
let output = null;
|
|
564
|
+
let status = 'idle';
|
|
565
|
+
// Check if we have a known session mapping (session ID may have changed from original)
|
|
566
|
+
// Support both exact match and partial match (first 8 chars of UUID)
|
|
567
|
+
const knownSession = this.findKnownSession(sessionId);
|
|
568
|
+
console.log(`[${this.genboxId}] Known session lookup result:`, knownSession ? `found dtach=${knownSession.dtachSocket}` : 'not found');
|
|
569
|
+
if (knownSession?.dtachSocket) {
|
|
570
|
+
// Check if dtach socket is alive
|
|
571
|
+
if (!this.isDtachSocketAlive(knownSession.dtachSocket)) {
|
|
572
|
+
// Session doesn't exist - it ended
|
|
573
|
+
console.log(`[${this.genboxId}] Dtach socket ${knownSession.dtachSocket} not alive, session ended`);
|
|
574
|
+
status = 'ended';
|
|
575
|
+
output = ''; // Session ended, no live output available
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
// Read from dtach session's log file
|
|
579
|
+
const capturedOutput = this.readDtachOutput(knownSession.dtachSocket, lines);
|
|
580
|
+
if (capturedOutput !== null) {
|
|
581
|
+
output = capturedOutput;
|
|
582
|
+
status = this.analyzeClaudeOutput(capturedOutput);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
// Fall back to session manager
|
|
588
|
+
output = this.sessionManager.getOutput(sessionId, lines);
|
|
589
|
+
status = this.sessionManager.getSessionStatus(sessionId);
|
|
590
|
+
}
|
|
591
|
+
this.socket?.emit('output_update', {
|
|
592
|
+
genboxId: this.genboxId,
|
|
593
|
+
sessionId,
|
|
594
|
+
requestId,
|
|
595
|
+
output: output || '',
|
|
596
|
+
status,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
onKillSession(data) {
|
|
600
|
+
const { sessionId, requestId } = data;
|
|
601
|
+
if (!sessionId)
|
|
602
|
+
return;
|
|
603
|
+
console.log(`[${this.genboxId}] Killing session ${sessionId}`);
|
|
604
|
+
const success = this.sessionManager.killSession(sessionId);
|
|
605
|
+
this.stopOutputPolling(sessionId);
|
|
606
|
+
this.socket?.emit('session_killed', {
|
|
607
|
+
genboxId: this.genboxId,
|
|
608
|
+
sessionId,
|
|
609
|
+
requestId,
|
|
610
|
+
success,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
onListSessions(data) {
|
|
614
|
+
const { requestId } = data;
|
|
615
|
+
const sessions = this.sessionManager.listAllSessions();
|
|
616
|
+
this.socket?.emit('sessions_list', {
|
|
617
|
+
genboxId: this.genboxId,
|
|
618
|
+
requestId,
|
|
619
|
+
sessions,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
onGetProviders(data) {
|
|
623
|
+
const { requestId } = data;
|
|
624
|
+
const detectedProviders = this.sessionManager.getDetectedProviders();
|
|
625
|
+
const providers = [];
|
|
626
|
+
for (const [provider, detection] of detectedProviders) {
|
|
627
|
+
providers.push({
|
|
628
|
+
provider,
|
|
629
|
+
detected: detection.detected,
|
|
630
|
+
version: detection.version,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
this.socket?.emit('providers_list', {
|
|
634
|
+
genboxId: this.genboxId,
|
|
635
|
+
requestId,
|
|
636
|
+
providers,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
onError(data) {
|
|
640
|
+
console.error(`[${this.genboxId}] Error from server:`, data);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Handle known sessions from the API (sessions created via hooks, not daemon)
|
|
644
|
+
* These need session ID -> dtach socket mappings to poll output
|
|
645
|
+
*
|
|
646
|
+
* IMPORTANT: When Claude starts in a daemon-created dtach session, Claude uses
|
|
647
|
+
* its own internal session ID. The API updates the session ID and sends us
|
|
648
|
+
* the new mapping. We need to:
|
|
649
|
+
* 1. Stop polling for the old session ID (if any)
|
|
650
|
+
* 2. Update our mapping
|
|
651
|
+
* 3. Start polling with the new session ID
|
|
652
|
+
*
|
|
653
|
+
* Note: API may still send 'tmuxSession' field for backwards compatibility,
|
|
654
|
+
* but we treat it as a dtach socket path.
|
|
655
|
+
*/
|
|
656
|
+
onKnownSessions(data) {
|
|
657
|
+
if (!data.sessions || !Array.isArray(data.sessions))
|
|
658
|
+
return;
|
|
659
|
+
for (const session of data.sessions) {
|
|
660
|
+
// Support both old 'tmuxSession' and new 'dtachSocket' field names
|
|
661
|
+
const socketPath = session.dtachSocket || session.tmuxSession;
|
|
662
|
+
if (session.sessionId && socketPath) {
|
|
663
|
+
// Check if we already have a mapping for this socket with a different session ID
|
|
664
|
+
// This happens when daemon creates session, then Claude starts with its own session ID
|
|
665
|
+
let oldSessionId;
|
|
666
|
+
for (const [existingSessionId, mapping] of this.knownSessions.entries()) {
|
|
667
|
+
if (mapping.dtachSocket === socketPath && existingSessionId !== session.sessionId) {
|
|
668
|
+
oldSessionId = existingSessionId;
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// Also check outputPollers for sessions that poll this socket
|
|
673
|
+
// The daemon-created sessions use sessionId that matches socket name suffix
|
|
674
|
+
const socketSuffix = socketPath.replace(/.*\/(claude-|gemini-|codex-)/, '').replace('.sock', '');
|
|
675
|
+
if (this.outputPollers.has(socketSuffix) && socketSuffix !== session.sessionId) {
|
|
676
|
+
console.log(`[${this.genboxId}] Session ID changed: ${socketSuffix} -> ${session.sessionId} (socket: ${socketPath})`);
|
|
677
|
+
this.stopOutputPolling(socketSuffix);
|
|
678
|
+
}
|
|
679
|
+
if (oldSessionId) {
|
|
680
|
+
console.log(`[${this.genboxId}] Session ID changed: ${oldSessionId} -> ${session.sessionId} (socket: ${socketPath})`);
|
|
681
|
+
this.stopOutputPolling(oldSessionId);
|
|
682
|
+
this.knownSessions.delete(oldSessionId);
|
|
683
|
+
}
|
|
684
|
+
// Store the new mapping
|
|
685
|
+
console.log(`[${this.genboxId}] Storing session mapping: ${session.sessionId} -> ${socketPath}`);
|
|
686
|
+
this.knownSessions.set(session.sessionId, {
|
|
687
|
+
dtachSocket: socketPath,
|
|
688
|
+
projectPath: session.projectPath,
|
|
689
|
+
});
|
|
690
|
+
// Start output polling for this session
|
|
691
|
+
// Note: JSONL message sync is handled by ContinuousJsonlSync (not per-session watchers)
|
|
692
|
+
if (session.status !== 'ended') {
|
|
693
|
+
this.startOutputPollingForKnownSession(session.sessionId, socketPath);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (data.sessions.length > 0) {
|
|
698
|
+
console.log(`[${this.genboxId}] Received ${data.sessions.length} known sessions from API`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
startOutputPolling(sessionId) {
|
|
702
|
+
if (this.outputPollers.has(sessionId))
|
|
703
|
+
return;
|
|
704
|
+
let lastOutput = '';
|
|
705
|
+
const poller = setInterval(() => {
|
|
706
|
+
if (!this.running) {
|
|
707
|
+
this.stopOutputPolling(sessionId);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const output = this.sessionManager.getOutput(sessionId, 100);
|
|
711
|
+
const status = this.sessionManager.getSessionStatus(sessionId);
|
|
712
|
+
// Only send if output changed
|
|
713
|
+
if (output && output !== lastOutput) {
|
|
714
|
+
lastOutput = output;
|
|
715
|
+
this.socket?.emit('output_update', {
|
|
716
|
+
genboxId: this.genboxId,
|
|
717
|
+
sessionId,
|
|
718
|
+
output,
|
|
719
|
+
status,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
// Stop polling if session ended
|
|
723
|
+
if (status === 'ended') {
|
|
724
|
+
this.stopOutputPolling(sessionId);
|
|
725
|
+
}
|
|
726
|
+
}, OUTPUT_POLL_INTERVAL);
|
|
727
|
+
this.outputPollers.set(sessionId, poller);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Start output polling for a known session (from API, not discovered by daemon)
|
|
731
|
+
* Uses the dtach socket path directly for session monitoring
|
|
732
|
+
*/
|
|
733
|
+
startOutputPollingForKnownSession(sessionId, socketPath) {
|
|
734
|
+
if (this.outputPollers.has(sessionId))
|
|
735
|
+
return;
|
|
736
|
+
console.log(`[${this.genboxId}] Starting output polling for session ${sessionId}, socket=${socketPath}`);
|
|
737
|
+
let lastOutput = '';
|
|
738
|
+
const poller = setInterval(() => {
|
|
739
|
+
if (!this.running) {
|
|
740
|
+
this.stopOutputPolling(sessionId);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
// Check if dtach socket is alive
|
|
744
|
+
if (!this.isDtachSocketAlive(socketPath)) {
|
|
745
|
+
// Session doesn't exist, emit ended status and stop polling
|
|
746
|
+
console.log(`[${this.genboxId}] Dtach socket ${socketPath} not alive, emitting ended status for ${sessionId}`);
|
|
747
|
+
this.socket?.emit('output_update', {
|
|
748
|
+
genboxId: this.genboxId,
|
|
749
|
+
sessionId,
|
|
750
|
+
output: lastOutput, // Keep last known output
|
|
751
|
+
status: 'ended',
|
|
752
|
+
});
|
|
753
|
+
this.stopOutputPolling(sessionId);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
// Read output from the log file
|
|
757
|
+
const output = this.readDtachOutput(socketPath, 100);
|
|
758
|
+
if (output === null) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
// Only send if output changed
|
|
762
|
+
if (output && output !== lastOutput) {
|
|
763
|
+
lastOutput = output;
|
|
764
|
+
// Analyze status from output
|
|
765
|
+
const status = this.analyzeClaudeOutput(output);
|
|
766
|
+
console.log(`[${this.genboxId}] Emitting output_update for ${sessionId}, status=${status}, outputLen=${output.length}`);
|
|
767
|
+
this.socket?.emit('output_update', {
|
|
768
|
+
genboxId: this.genboxId,
|
|
769
|
+
sessionId,
|
|
770
|
+
output,
|
|
771
|
+
status,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}, OUTPUT_POLL_INTERVAL);
|
|
775
|
+
this.outputPollers.set(sessionId, poller);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Analyze Claude output to determine session status
|
|
779
|
+
*/
|
|
780
|
+
analyzeClaudeOutput(output) {
|
|
781
|
+
const lowerOutput = output.toLowerCase();
|
|
782
|
+
const lines = output.split('\n');
|
|
783
|
+
const nonEmptyLines = lines.filter(l => l.trim());
|
|
784
|
+
const lastFewLines = nonEmptyLines.slice(-5);
|
|
785
|
+
// Check for prompt indicators
|
|
786
|
+
for (const line of lastFewLines) {
|
|
787
|
+
if (line.trim().startsWith('>') || line.includes('> Try')) {
|
|
788
|
+
return 'waiting_input';
|
|
789
|
+
}
|
|
790
|
+
if (line.includes('? for shortcuts')) {
|
|
791
|
+
return 'waiting_input';
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (lowerOutput.includes('welcome back') || lowerOutput.includes('claude code v')) {
|
|
795
|
+
return 'waiting_input';
|
|
796
|
+
}
|
|
797
|
+
if (lowerOutput.includes('thinking') || lowerOutput.includes('analyzing')) {
|
|
798
|
+
return 'active';
|
|
799
|
+
}
|
|
800
|
+
return 'idle';
|
|
801
|
+
}
|
|
802
|
+
stopOutputPolling(sessionId) {
|
|
803
|
+
const poller = this.outputPollers.get(sessionId);
|
|
804
|
+
if (poller) {
|
|
805
|
+
clearInterval(poller);
|
|
806
|
+
this.outputPollers.delete(sessionId);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Set storage manager for message sync
|
|
811
|
+
* Note: JSONL message sync is now handled by ContinuousJsonlSync, not per-session watchers
|
|
812
|
+
*/
|
|
813
|
+
setStorageManager(storageManager) {
|
|
814
|
+
this.storageManager = storageManager;
|
|
815
|
+
console.log(`[${this.genboxId}] Storage manager initialized for session file watching`);
|
|
816
|
+
}
|
|
817
|
+
captureAndSendOutput(sessionId) {
|
|
818
|
+
let output = null;
|
|
819
|
+
let status = 'idle';
|
|
820
|
+
const knownSession = this.findKnownSession(sessionId);
|
|
821
|
+
if (knownSession?.dtachSocket) {
|
|
822
|
+
// Read from dtach session's log file
|
|
823
|
+
const capturedOutput = this.readDtachOutput(knownSession.dtachSocket, 100);
|
|
824
|
+
if (capturedOutput !== null) {
|
|
825
|
+
output = capturedOutput;
|
|
826
|
+
status = this.analyzeClaudeOutput(capturedOutput);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
output = this.sessionManager.getOutput(sessionId, 100);
|
|
831
|
+
status = this.sessionManager.getSessionStatus(sessionId);
|
|
832
|
+
}
|
|
833
|
+
if (output && this.socket?.connected) {
|
|
834
|
+
this.socket.emit('output_update', {
|
|
835
|
+
genboxId: this.genboxId,
|
|
836
|
+
sessionId,
|
|
837
|
+
output,
|
|
838
|
+
status,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async run() {
|
|
843
|
+
console.log(`Starting Unified Genbox Agent Daemon for ${this.genboxId}`);
|
|
844
|
+
const connected = await this.connect();
|
|
845
|
+
if (!connected) {
|
|
846
|
+
console.log('Initial connection failed, will keep retrying...');
|
|
847
|
+
}
|
|
848
|
+
// Keep running
|
|
849
|
+
await new Promise((resolve) => {
|
|
850
|
+
const shutdown = () => {
|
|
851
|
+
console.log('Shutting down...');
|
|
852
|
+
this.running = false;
|
|
853
|
+
// Stop stats polling
|
|
854
|
+
this.stopStatsPolling();
|
|
855
|
+
// Stop all output pollers
|
|
856
|
+
for (const sessionId of this.outputPollers.keys()) {
|
|
857
|
+
this.stopOutputPolling(sessionId);
|
|
858
|
+
}
|
|
859
|
+
// Note: JSONL sync is handled by ContinuousJsonlSync which is stopped separately
|
|
860
|
+
this.socket?.disconnect();
|
|
861
|
+
resolve();
|
|
862
|
+
};
|
|
863
|
+
process.on('SIGTERM', shutdown);
|
|
864
|
+
process.on('SIGINT', shutdown);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function parseArgs() {
|
|
869
|
+
const args = process.argv.slice(2);
|
|
870
|
+
const result = {
|
|
871
|
+
local: false,
|
|
872
|
+
native: false,
|
|
873
|
+
server: false,
|
|
874
|
+
noSync: false,
|
|
875
|
+
};
|
|
876
|
+
for (let i = 0; i < args.length; i++) {
|
|
877
|
+
const arg = args[i];
|
|
878
|
+
if (arg === '--local' || arg === '-l') {
|
|
879
|
+
result.local = true;
|
|
880
|
+
}
|
|
881
|
+
else if (arg === '--native') {
|
|
882
|
+
result.native = true;
|
|
883
|
+
}
|
|
884
|
+
else if (arg === '--server' || arg === '-s') {
|
|
885
|
+
result.server = true;
|
|
886
|
+
}
|
|
887
|
+
else if (arg === '--port' && args[i + 1]) {
|
|
888
|
+
result.port = parseInt(args[++i], 10);
|
|
889
|
+
}
|
|
890
|
+
else if (arg === '--control-token' && args[i + 1]) {
|
|
891
|
+
result.controlToken = args[++i];
|
|
892
|
+
}
|
|
893
|
+
else if (arg === '--no-sync') {
|
|
894
|
+
result.noSync = true;
|
|
895
|
+
}
|
|
896
|
+
else if (arg === '--session-id' && args[i + 1]) {
|
|
897
|
+
result.sessionId = args[++i];
|
|
898
|
+
}
|
|
899
|
+
else if (arg === '--session-name' && args[i + 1]) {
|
|
900
|
+
result.sessionName = args[++i];
|
|
901
|
+
}
|
|
902
|
+
else if (arg === '--dashboard-url' && args[i + 1]) {
|
|
903
|
+
result.dashboardUrl = args[++i];
|
|
904
|
+
}
|
|
905
|
+
else if (arg === '--user-id' && args[i + 1]) {
|
|
906
|
+
result.userId = args[++i];
|
|
907
|
+
}
|
|
908
|
+
else if ((arg === '--dtach-socket' || arg === '--tmux-session') && args[i + 1]) {
|
|
909
|
+
// Support both --dtach-socket and legacy --tmux-session for backwards compatibility
|
|
910
|
+
result.dtachSocket = args[++i];
|
|
911
|
+
}
|
|
912
|
+
else if (arg === 'start') {
|
|
913
|
+
// Ignore 'start' command
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return result;
|
|
917
|
+
}
|
|
918
|
+
// Main entry point
|
|
919
|
+
async function main() {
|
|
920
|
+
const cliArgs = parseArgs();
|
|
921
|
+
// Import server components
|
|
922
|
+
const { AgentServer } = await Promise.resolve().then(() => __importStar(require('./server')));
|
|
923
|
+
const { generateControlToken } = await Promise.resolve().then(() => __importStar(require('./server/auth')));
|
|
924
|
+
const { getStorageManager, closeStorageManager } = await Promise.resolve().then(() => __importStar(require('./storage/manager')));
|
|
925
|
+
const { BackgroundSync } = await Promise.resolve().then(() => __importStar(require('./sync/background-sync')));
|
|
926
|
+
const { ContinuousJsonlSync } = await Promise.resolve().then(() => __importStar(require('./sync/continuous-jsonl-sync')));
|
|
927
|
+
const { StateWatchdog } = await Promise.resolve().then(() => __importStar(require('./state-watchdog')));
|
|
928
|
+
const { isDaemonRunning, BASE_PORT } = await Promise.resolve().then(() => __importStar(require('./server/port-finder')));
|
|
929
|
+
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
|
930
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
931
|
+
// Local/Native mode: run with local server, optionally sync to cloud
|
|
932
|
+
if (cliArgs.local || cliArgs.native) {
|
|
933
|
+
// Import killExistingDaemons
|
|
934
|
+
const { killExistingDaemons } = await Promise.resolve().then(() => __importStar(require('./server/port-finder')));
|
|
935
|
+
// CRITICAL: Kill any existing daemon processes to ensure single instance
|
|
936
|
+
// This is more aggressive than just checking - it ensures clean startup
|
|
937
|
+
const killed = killExistingDaemons();
|
|
938
|
+
if (killed > 0) {
|
|
939
|
+
console.log(`[native] Killed ${killed} existing daemon process(es)`);
|
|
940
|
+
// Wait a moment for ports to be released
|
|
941
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
942
|
+
}
|
|
943
|
+
// Double-check if daemon is still running (in case kill failed)
|
|
944
|
+
const existingDaemon = isDaemonRunning();
|
|
945
|
+
if (existingDaemon.running) {
|
|
946
|
+
console.log(`[native] Daemon still running on port ${BASE_PORT}` +
|
|
947
|
+
(existingDaemon.pid ? ` (PID: ${existingDaemon.pid})` : '') +
|
|
948
|
+
` - detected via ${existingDaemon.source || 'unknown'}`);
|
|
949
|
+
console.log('[native] Could not kill existing daemon. Exiting.');
|
|
950
|
+
console.log('[native] Manual cleanup: kill ' + (existingDaemon.pid || '<PID>') +
|
|
951
|
+
` && rm -f ~/.genbox/locks/port-${BASE_PORT}.lock`);
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
console.log('Starting Genbox Agent in local/native mode...');
|
|
955
|
+
const genboxId = cliArgs.sessionId || process.env.GENBOX_ID || `native-${Date.now()}`;
|
|
956
|
+
const controlToken = cliArgs.controlToken || process.env.CONTROL_TOKEN || generateControlToken();
|
|
957
|
+
// Create session manager for the server handlers
|
|
958
|
+
const registry = providers_1.ProviderRegistry.getInstance();
|
|
959
|
+
const sessionManager = new providers_1.UnifiedSessionManager(genboxId);
|
|
960
|
+
// Ensure Claude hooks are configured globally
|
|
961
|
+
const claudeProvider = registry.getProvider('claude', genboxId);
|
|
962
|
+
if (claudeProvider && 'ensureGlobalHooks' in claudeProvider) {
|
|
963
|
+
claudeProvider.ensureGlobalHooks();
|
|
964
|
+
}
|
|
965
|
+
// Initialize OpenCode provider and recover any persisted sessions
|
|
966
|
+
const opencodeProvider = registry.getProvider('opencode', genboxId);
|
|
967
|
+
if (opencodeProvider && 'loadPersistedSessions' in opencodeProvider) {
|
|
968
|
+
console.log('[native] Recovering OpenCode sessions...');
|
|
969
|
+
opencodeProvider.loadPersistedSessions();
|
|
970
|
+
}
|
|
971
|
+
// Initialize storage manager (before handlers so we can use it)
|
|
972
|
+
const storageManager = getStorageManager();
|
|
973
|
+
console.log('[native] Storage initialized');
|
|
974
|
+
// Create server handlers
|
|
975
|
+
const handlers = {
|
|
976
|
+
listSessions: () => sessionManager.listAllSessions(),
|
|
977
|
+
getSession: (sessionId) => {
|
|
978
|
+
const sessions = sessionManager.listAllSessions();
|
|
979
|
+
return sessions.find(s => s.sessionId === sessionId) || null;
|
|
980
|
+
},
|
|
981
|
+
getSessionOutput: (sessionId, lines) => sessionManager.getOutput(sessionId, lines || 100),
|
|
982
|
+
getSessionStatus: (sessionId) => sessionManager.getSessionStatus(sessionId),
|
|
983
|
+
sendPrompt: (sessionId, prompt) => {
|
|
984
|
+
// Update session state to 'thinking' BEFORE sending prompt
|
|
985
|
+
// This ensures the dtach session is in processing state when Claude hook fires
|
|
986
|
+
storageManager.sessions.updateState(sessionId, 'thinking');
|
|
987
|
+
return sessionManager.sendPrompt(sessionId, prompt);
|
|
988
|
+
},
|
|
989
|
+
sendKeystroke: (sessionId, key) => sessionManager.sendKeystroke(sessionId, key),
|
|
990
|
+
createSession: (projectPath, provider, model) => sessionManager.createSession(projectPath, provider, model),
|
|
991
|
+
resumeSession: (sessionId, projectPath, provider, model) => sessionManager.resumeSession(sessionId, projectPath, provider, model),
|
|
992
|
+
killSession: (sessionId) => sessionManager.killSession(sessionId),
|
|
993
|
+
getSystemStats: () => collectSystemStats(),
|
|
994
|
+
getAvailableModels: () => sessionManager.getAvailableModels(),
|
|
995
|
+
};
|
|
996
|
+
// Start server
|
|
997
|
+
const server = new AgentServer({
|
|
998
|
+
port: cliArgs.port,
|
|
999
|
+
genboxId,
|
|
1000
|
+
controlToken,
|
|
1001
|
+
enableCors: true,
|
|
1002
|
+
}, handlers);
|
|
1003
|
+
// Set storage manager on server for event recording
|
|
1004
|
+
server.setStorageManager(storageManager);
|
|
1005
|
+
// Start continuous JSONL sync (replaces old event-driven JSONLWatcher)
|
|
1006
|
+
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
1007
|
+
const continuousSync = new ContinuousJsonlSync(storageManager, {
|
|
1008
|
+
syncInterval: 5000, // Sync every 5 seconds
|
|
1009
|
+
claudeProjectsDir,
|
|
1010
|
+
enabled: true,
|
|
1011
|
+
});
|
|
1012
|
+
continuousSync.setBroadcastCallback((event) => server.broadcastEvent(event));
|
|
1013
|
+
continuousSync.start();
|
|
1014
|
+
console.log('[native] Continuous JSONL sync started (5s interval)');
|
|
1015
|
+
// Start state watchdog to recover stuck sessions (60s threshold)
|
|
1016
|
+
const stateWatchdog = new StateWatchdog(storageManager, {
|
|
1017
|
+
checkIntervalMs: 15000, // Check every 15 seconds
|
|
1018
|
+
stuckThresholdMs: 60000, // 60 seconds = stuck
|
|
1019
|
+
});
|
|
1020
|
+
stateWatchdog.setBroadcastCallback((event) => server.broadcastEvent(event));
|
|
1021
|
+
stateWatchdog.start();
|
|
1022
|
+
console.log('[native] State watchdog started (60s threshold)');
|
|
1023
|
+
const port = await server.start();
|
|
1024
|
+
console.log(`[native] Server running on http://localhost:${port}`);
|
|
1025
|
+
console.log(`[native] Control token: ${controlToken}`);
|
|
1026
|
+
console.log(`[native] Genbox ID: ${genboxId}`);
|
|
1027
|
+
// Optionally sync to cloud if not disabled
|
|
1028
|
+
let backgroundSync = null;
|
|
1029
|
+
if (!cliArgs.noSync && MONITORING_WS_URL && MONITORING_WS_URL !== 'http://localhost:3001') {
|
|
1030
|
+
console.log(`[native] Cloud sync enabled to ${MONITORING_WS_URL}`);
|
|
1031
|
+
backgroundSync = new BackgroundSync(storageManager, {
|
|
1032
|
+
apiUrl: MONITORING_WS_URL,
|
|
1033
|
+
genboxId,
|
|
1034
|
+
genboxToken: controlToken,
|
|
1035
|
+
enabled: true,
|
|
1036
|
+
syncInterval: 30000, // 30 seconds
|
|
1037
|
+
});
|
|
1038
|
+
backgroundSync.start();
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
console.log('[native] Cloud sync disabled (--no-sync or no MONITORING_WS_URL)');
|
|
1042
|
+
}
|
|
1043
|
+
// Keep running until shutdown
|
|
1044
|
+
await new Promise((resolve) => {
|
|
1045
|
+
const shutdown = async () => {
|
|
1046
|
+
console.log('[native] Shutting down...');
|
|
1047
|
+
if (backgroundSync) {
|
|
1048
|
+
backgroundSync.stop();
|
|
1049
|
+
}
|
|
1050
|
+
stateWatchdog.stop();
|
|
1051
|
+
continuousSync.stop();
|
|
1052
|
+
await server.stop();
|
|
1053
|
+
closeStorageManager();
|
|
1054
|
+
resolve();
|
|
1055
|
+
};
|
|
1056
|
+
process.on('SIGTERM', shutdown);
|
|
1057
|
+
process.on('SIGINT', shutdown);
|
|
1058
|
+
});
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
// Cloud mode: require GENBOX_ID and GENBOX_TOKEN
|
|
1062
|
+
if (!GENBOX_ID) {
|
|
1063
|
+
console.error('Error: GENBOX_ID environment variable not set');
|
|
1064
|
+
console.error('For local mode, use: genbox-agent --native');
|
|
1065
|
+
console.error('For local mode with server, use: genbox-agent --native --server');
|
|
1066
|
+
process.exit(1);
|
|
1067
|
+
}
|
|
1068
|
+
if (!GENBOX_TOKEN) {
|
|
1069
|
+
console.error('Error: GENBOX_TOKEN environment variable not set');
|
|
1070
|
+
console.error('For local mode, use: genbox-agent --native');
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
1073
|
+
// Cloud mode with optional local server
|
|
1074
|
+
const daemon = new UnifiedGenboxDaemon(GENBOX_ID, GENBOX_TOKEN, MONITORING_WS_URL);
|
|
1075
|
+
// Initialize OpenCode provider for cloud mode (recover persisted sessions)
|
|
1076
|
+
const cloudRegistry = providers_1.ProviderRegistry.getInstance();
|
|
1077
|
+
const cloudOpencodeProvider = cloudRegistry.getProvider('opencode', GENBOX_ID);
|
|
1078
|
+
if (cloudOpencodeProvider && 'loadPersistedSessions' in cloudOpencodeProvider) {
|
|
1079
|
+
console.log('[cloud] Recovering OpenCode sessions...');
|
|
1080
|
+
cloudOpencodeProvider.loadPersistedSessions();
|
|
1081
|
+
}
|
|
1082
|
+
// Start local server if --server flag is set
|
|
1083
|
+
if (cliArgs.server) {
|
|
1084
|
+
const controlToken = cliArgs.controlToken || process.env.CONTROL_TOKEN || generateControlToken();
|
|
1085
|
+
// Initialize storage manager (before handlers so we can use it)
|
|
1086
|
+
const storageManager = getStorageManager();
|
|
1087
|
+
console.log('[cloud] Storage initialized');
|
|
1088
|
+
const handlers = {
|
|
1089
|
+
listSessions: () => daemon['sessionManager'].listAllSessions(),
|
|
1090
|
+
getSession: (sessionId) => {
|
|
1091
|
+
const sessions = daemon['sessionManager'].listAllSessions();
|
|
1092
|
+
return sessions.find(s => s.sessionId === sessionId) || null;
|
|
1093
|
+
},
|
|
1094
|
+
getSessionOutput: (sessionId, lines) => daemon['sessionManager'].getOutput(sessionId, lines || 100),
|
|
1095
|
+
getSessionStatus: (sessionId) => daemon['sessionManager'].getSessionStatus(sessionId),
|
|
1096
|
+
sendPrompt: (sessionId, prompt) => {
|
|
1097
|
+
// Update session state to 'thinking' BEFORE sending prompt
|
|
1098
|
+
storageManager.sessions.updateState(sessionId, 'thinking');
|
|
1099
|
+
return daemon['sessionManager'].sendPrompt(sessionId, prompt);
|
|
1100
|
+
},
|
|
1101
|
+
sendKeystroke: (sessionId, key) => daemon['sessionManager'].sendKeystroke(sessionId, key),
|
|
1102
|
+
createSession: (projectPath, provider, model) => daemon['sessionManager'].createSession(projectPath, provider, model),
|
|
1103
|
+
killSession: (sessionId) => daemon['sessionManager'].killSession(sessionId),
|
|
1104
|
+
getSystemStats: () => collectSystemStats(),
|
|
1105
|
+
getAvailableModels: () => daemon['sessionManager'].getAvailableModels(),
|
|
1106
|
+
};
|
|
1107
|
+
const server = new AgentServer({
|
|
1108
|
+
port: cliArgs.port,
|
|
1109
|
+
genboxId: GENBOX_ID,
|
|
1110
|
+
controlToken,
|
|
1111
|
+
enableCors: true,
|
|
1112
|
+
}, handlers);
|
|
1113
|
+
// Set storage manager on server for event recording
|
|
1114
|
+
server.setStorageManager(storageManager);
|
|
1115
|
+
// Set storage manager on daemon for session file watching
|
|
1116
|
+
daemon.setStorageManager(storageManager);
|
|
1117
|
+
// Start continuous JSONL sync (replaces old event-driven JSONLWatcher)
|
|
1118
|
+
const claudeProjectsDirCloud = path.join(os.homedir(), '.claude', 'projects');
|
|
1119
|
+
const continuousSyncCloud = new ContinuousJsonlSync(storageManager, {
|
|
1120
|
+
syncInterval: 5000, // Sync every 5 seconds
|
|
1121
|
+
claudeProjectsDir: claudeProjectsDirCloud,
|
|
1122
|
+
enabled: true,
|
|
1123
|
+
});
|
|
1124
|
+
continuousSyncCloud.setBroadcastCallback((event) => server.broadcastEvent(event));
|
|
1125
|
+
continuousSyncCloud.start();
|
|
1126
|
+
console.log('[cloud] Continuous JSONL sync started (5s interval)');
|
|
1127
|
+
// Start state watchdog to recover stuck sessions (60s threshold)
|
|
1128
|
+
const stateWatchdogCloud = new StateWatchdog(storageManager, {
|
|
1129
|
+
checkIntervalMs: 15000, // Check every 15 seconds
|
|
1130
|
+
stuckThresholdMs: 60000, // 60 seconds = stuck
|
|
1131
|
+
});
|
|
1132
|
+
stateWatchdogCloud.setBroadcastCallback((event) => server.broadcastEvent(event));
|
|
1133
|
+
stateWatchdogCloud.start();
|
|
1134
|
+
console.log('[cloud] State watchdog started (60s threshold)');
|
|
1135
|
+
const port = await server.start();
|
|
1136
|
+
console.log(`[cloud] Local server running on http://localhost:${port}`);
|
|
1137
|
+
console.log(`[cloud] Control token: ${controlToken}`);
|
|
1138
|
+
// Start background sync (cloud mode always syncs)
|
|
1139
|
+
const backgroundSync = new BackgroundSync(storageManager, {
|
|
1140
|
+
apiUrl: MONITORING_WS_URL,
|
|
1141
|
+
genboxId: GENBOX_ID,
|
|
1142
|
+
genboxToken: GENBOX_TOKEN,
|
|
1143
|
+
enabled: true,
|
|
1144
|
+
syncInterval: 30000, // 30 seconds
|
|
1145
|
+
});
|
|
1146
|
+
backgroundSync.start();
|
|
1147
|
+
// Add shutdown handler for server
|
|
1148
|
+
const originalShutdown = process.listeners('SIGTERM')[0];
|
|
1149
|
+
process.removeAllListeners('SIGTERM');
|
|
1150
|
+
process.removeAllListeners('SIGINT');
|
|
1151
|
+
const shutdown = async () => {
|
|
1152
|
+
console.log('[cloud] Shutting down server...');
|
|
1153
|
+
backgroundSync.stop();
|
|
1154
|
+
stateWatchdogCloud.stop();
|
|
1155
|
+
continuousSyncCloud.stop();
|
|
1156
|
+
await server.stop();
|
|
1157
|
+
closeStorageManager();
|
|
1158
|
+
if (originalShutdown)
|
|
1159
|
+
originalShutdown();
|
|
1160
|
+
};
|
|
1161
|
+
process.on('SIGTERM', shutdown);
|
|
1162
|
+
process.on('SIGINT', shutdown);
|
|
1163
|
+
}
|
|
1164
|
+
await daemon.run();
|
|
1165
|
+
}
|
|
1166
|
+
main().catch((error) => {
|
|
1167
|
+
console.error('Fatal error:', error);
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
});
|
|
1170
|
+
//# sourceMappingURL=genbox-agent.js.map
|