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.
@@ -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