genbox-agent 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SessionManager = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const crypto = __importStar(require("crypto"));
39
+ /**
40
+ * Manages tmux sessions running Claude Code.
41
+ */
42
+ class SessionManager {
43
+ genboxId;
44
+ TMUX_PREFIX = 'claude-';
45
+ sessions = new Map();
46
+ constructor(genboxId) {
47
+ this.genboxId = genboxId;
48
+ }
49
+ runTmux(args, timeout = 10000) {
50
+ try {
51
+ const stdout = (0, child_process_1.execSync)(`tmux ${args.join(' ')}`, {
52
+ timeout,
53
+ encoding: 'utf-8',
54
+ stdio: ['pipe', 'pipe', 'pipe'],
55
+ });
56
+ return { stdout: stdout || '', stderr: '', success: true };
57
+ }
58
+ catch (error) {
59
+ return {
60
+ stdout: error.stdout?.toString() || '',
61
+ stderr: error.stderr?.toString() || error.message,
62
+ success: false,
63
+ };
64
+ }
65
+ }
66
+ generateSessionId() {
67
+ return crypto
68
+ .createHash('sha256')
69
+ .update(`${this.genboxId}-${Date.now()}-${process.pid}`)
70
+ .digest('hex')
71
+ .slice(0, 16);
72
+ }
73
+ /**
74
+ * Create a new Claude Code session in tmux.
75
+ */
76
+ createSession(projectPath = '/home/dev') {
77
+ const sessionId = this.generateSessionId();
78
+ const sessionName = `${this.TMUX_PREFIX}${sessionId}`;
79
+ // Create a new detached tmux session
80
+ const result = this.runTmux([
81
+ 'new-session',
82
+ '-d',
83
+ '-s', sessionName,
84
+ '-c', projectPath,
85
+ ]);
86
+ if (!result.success) {
87
+ console.error(`Failed to create tmux session: ${result.stderr}`);
88
+ return null;
89
+ }
90
+ // Give tmux a moment to initialize
91
+ (0, child_process_1.execSync)('sleep 0.5');
92
+ // Start Claude Code in the session
93
+ this.runTmux([
94
+ 'send-keys',
95
+ '-t', sessionName,
96
+ 'claude',
97
+ 'Enter',
98
+ ]);
99
+ const session = {
100
+ sessionName,
101
+ sessionId,
102
+ projectPath,
103
+ createdAt: Date.now(),
104
+ status: 'active',
105
+ };
106
+ this.sessions.set(sessionId, session);
107
+ return session;
108
+ }
109
+ /**
110
+ * Send a prompt to a Claude Code session.
111
+ */
112
+ sendPrompt(sessionId, prompt) {
113
+ const session = this.sessions.get(sessionId);
114
+ const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
115
+ // Send the prompt text with literal flag to handle special characters
116
+ const textResult = this.runTmux([
117
+ 'send-keys',
118
+ '-t', sessionName,
119
+ '-l', // Literal flag
120
+ `"${prompt.replace(/"/g, '\\"')}"`,
121
+ ]);
122
+ if (!textResult.success) {
123
+ console.error(`Failed to send prompt text: ${textResult.stderr}`);
124
+ return false;
125
+ }
126
+ // Send Enter to submit the prompt
127
+ const enterResult = this.runTmux([
128
+ 'send-keys',
129
+ '-t', sessionName,
130
+ 'Enter',
131
+ ]);
132
+ if (!enterResult.success) {
133
+ console.error(`Failed to send Enter: ${enterResult.stderr}`);
134
+ return false;
135
+ }
136
+ return true;
137
+ }
138
+ /**
139
+ * Send a keystroke to a Claude Code session.
140
+ */
141
+ sendKeystroke(sessionId, key) {
142
+ const session = this.sessions.get(sessionId);
143
+ const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
144
+ // Map common key names to tmux key names
145
+ const keyMap = {
146
+ enter: 'Enter',
147
+ escape: 'Escape',
148
+ up: 'Up',
149
+ down: 'Down',
150
+ left: 'Left',
151
+ right: 'Right',
152
+ tab: 'Tab',
153
+ backspace: 'BSpace',
154
+ delete: 'DC',
155
+ 'ctrl-c': 'C-c',
156
+ 'ctrl-d': 'C-d',
157
+ 'ctrl-z': 'C-z',
158
+ y: 'y',
159
+ n: 'n',
160
+ };
161
+ const tmuxKey = keyMap[key.toLowerCase()] || key;
162
+ const result = this.runTmux([
163
+ 'send-keys',
164
+ '-t', sessionName,
165
+ tmuxKey,
166
+ ]);
167
+ return result.success;
168
+ }
169
+ /**
170
+ * Capture the terminal output from a session.
171
+ */
172
+ getOutput(sessionId, lines = 100) {
173
+ const session = this.sessions.get(sessionId);
174
+ const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
175
+ const result = this.runTmux([
176
+ 'capture-pane',
177
+ '-t', sessionName,
178
+ '-p', // Print to stdout
179
+ '-S', `-${lines}`, // Start from lines back
180
+ ]);
181
+ return result.success ? result.stdout : null;
182
+ }
183
+ /**
184
+ * Get the status of a session.
185
+ */
186
+ getSessionStatus(sessionId) {
187
+ const session = this.sessions.get(sessionId);
188
+ const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
189
+ // Check if session exists
190
+ const hasSession = this.runTmux(['has-session', '-t', sessionName]);
191
+ if (!hasSession.success) {
192
+ return 'ended';
193
+ }
194
+ // Get pane content to determine status
195
+ const output = this.getOutput(sessionId, 10);
196
+ if (output) {
197
+ const lowerOutput = output.toLowerCase();
198
+ const lastLine = output.split('\n').pop() || '';
199
+ if (lowerOutput.includes('waiting for input') || lastLine.includes('>')) {
200
+ return 'waiting_input';
201
+ }
202
+ if (lowerOutput.includes('error')) {
203
+ return 'error';
204
+ }
205
+ if (lowerOutput.includes('thinking') || lowerOutput.includes('working')) {
206
+ return 'active';
207
+ }
208
+ }
209
+ return 'idle';
210
+ }
211
+ /**
212
+ * Kill a Claude Code session.
213
+ */
214
+ killSession(sessionId) {
215
+ const session = this.sessions.get(sessionId);
216
+ const sessionName = session?.sessionName || `${this.TMUX_PREFIX}${sessionId}`;
217
+ const result = this.runTmux(['kill-session', '-t', sessionName]);
218
+ if (result.success) {
219
+ this.sessions.delete(sessionId);
220
+ }
221
+ return result.success;
222
+ }
223
+ /**
224
+ * List all Claude Code sessions.
225
+ */
226
+ listSessions() {
227
+ const result = this.runTmux([
228
+ 'list-sessions',
229
+ '-F', '#{session_name}:#{session_created}',
230
+ ]);
231
+ if (!result.success) {
232
+ return [];
233
+ }
234
+ const sessions = [];
235
+ for (const line of result.stdout.trim().split('\n')) {
236
+ if (!line || !line.startsWith(this.TMUX_PREFIX)) {
237
+ continue;
238
+ }
239
+ const [sessionName, createdAtStr] = line.split(':');
240
+ const sessionId = sessionName.replace(this.TMUX_PREFIX, '');
241
+ const createdAt = parseInt(createdAtStr, 10) * 1000 || 0;
242
+ sessions.push({
243
+ sessionId,
244
+ sessionName,
245
+ createdAt,
246
+ status: this.getSessionStatus(sessionId),
247
+ });
248
+ }
249
+ return sessions;
250
+ }
251
+ }
252
+ exports.SessionManager = SessionManager;
253
+ //# sourceMappingURL=session-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iDAAgD;AAChD,+CAAiC;AAUjC;;GAEG;AACH,MAAa,cAAc;IAII;IAHZ,WAAW,GAAG,SAAS,CAAC;IACjC,QAAQ,GAA+B,IAAI,GAAG,EAAE,CAAC;IAEzD,YAA6B,QAAgB;QAAhB,aAAQ,GAAR,QAAQ,CAAQ;IAAG,CAAC;IAEzC,OAAO,CAAC,IAAc,EAAE,OAAO,GAAG,KAAK;QAC7C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE;gBAChD,OAAO;gBACP,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;aAChC,CAAC,CAAC;YACH,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC7D,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,OAAO;gBACL,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;gBACtC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,KAAK,CAAC,OAAO;gBACjD,OAAO,EAAE,KAAK;aACf,CAAC;QACJ,CAAC;IACH,CAAC;IAEO,iBAAiB;QACvB,OAAO,MAAM;aACV,UAAU,CAAC,QAAQ,CAAC;aACpB,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;aACvD,MAAM,CAAC,KAAK,CAAC;aACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAClB,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,WAAW,GAAG,WAAW;QACrC,MAAM,SAAS,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC3C,MAAM,WAAW,GAAG,GAAG,IAAI,CAAC,WAAW,GAAG,SAAS,EAAE,CAAC;QAEtD,qCAAqC;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;YAC1B,aAAa;YACb,IAAI;YACJ,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,WAAW;SAClB,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,CAAC,kCAAkC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;YACjE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,mCAAmC;QACnC,IAAA,wBAAQ,EAAC,WAAW,CAAC,CAAC;QAEtB,mCAAmC;QACnC,IAAI,CAAC,OAAO,CAAC;YACX,WAAW;YACX,IAAI,EAAE,WAAW;YACjB,QAAQ;YACR,OAAO;SACR,CAAC,CAAC;QAEH,MAAM,OAAO,GAAkB;YAC7B,WAAW;YACX,SAAS;YACT,WAAW;YACX,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,MAAM,EAAE,QAAQ;SACjB,CAAC;QAEF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACtC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;OAEG;IACH,UAAU,CAAC,SAAiB,EAAE,MAAc;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,GAAG,IAAI,CAAC,WAAW,GAAG,SAAS,EAAE,CAAC;QAE9E,sEAAsE;QACtE,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC;YAC9B,WAAW;YACX,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,eAAe;YACrB,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG;SACnC,CAAC,CAAC;QAEH,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,CAAC,+BAA+B,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;YAClE,OAAO,KAAK,CAAC;QACf,CAAC;QAED,kCAAkC;QAClC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC;YAC/B,WAAW;YACX,IAAI,EAAE,WAAW;YACjB,OAAO;SACR,CAAC,CAAC;QAEH,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;YACzB,OAAO,CAAC,KAAK,CAAC,yBAAyB,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;YAC7D,OAAO,KAAK,CAAC;QACf,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,SAAiB,EAAE,GAAW;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,GAAG,IAAI,CAAC,WAAW,GAAG,SAAS,EAAE,CAAC;QAE9E,yCAAyC;QACzC,MAAM,MAAM,GAA2B;YACrC,KAAK,EAAE,OAAO;YACd,MAAM,EAAE,QAAQ;YAChB,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,OAAO;YACd,GAAG,EAAE,KAAK;YACV,SAAS,EAAE,QAAQ;YACnB,MAAM,EAAE,IAAI;YACZ,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,KAAK;YACf,CAAC,EAAE,GAAG;YACN,CAAC,EAAE,GAAG;SACP,CAAC;QAEF,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,GAAG,CAAC;QAEjD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;YAC1B,WAAW;YACX,IAAI,EAAE,WAAW;YACjB,OAAO;SACR,CAAC,CAAC;QAEH,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;IAED;;OAEG;IACH,SAAS,CAAC,SAAiB,EAAE,KAAK,GAAG,GAAG;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,GAAG,IAAI,CAAC,WAAW,GAAG,SAAS,EAAE,CAAC;QAE9E,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;YAC1B,cAAc;YACd,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,kBAAkB;YACxB,IAAI,EAAE,IAAI,KAAK,EAAE,EAAE,wBAAwB;SAC5C,CAAC,CAAC;QAEH,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IAED;;OAEG;IACH,gBAAgB,CAAC,SAAiB;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,GAAG,IAAI,CAAC,WAAW,GAAG,SAAS,EAAE,CAAC;QAE9E,0BAA0B;QAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC;QACpE,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YACxB,OAAO,OAAO,CAAC;QACjB,CAAC;QAED,uCAAuC;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAC7C,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;YACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YAEhD,IAAI,WAAW,CAAC,QAAQ,CAAC,mBAAmB,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBACxE,OAAO,eAAe,CAAC;YACzB,CAAC;YACD,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAClC,OAAO,OAAO,CAAC;YACjB,CAAC;YACD,IAAI,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;gBACxE,OAAO,QAAQ,CAAC;YAClB,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,SAAiB;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,GAAG,IAAI,CAAC,WAAW,GAAG,SAAS,EAAE,CAAC;QAE9E,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,cAAc,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC;QAEjE,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAClC,CAAC;QAED,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;IAED;;OAEG;IACH,YAAY;QAMV,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;YAC1B,eAAe;YACf,IAAI,EAAE,oCAAoC;SAC3C,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,QAAQ,GAKT,EAAE,CAAC;QAER,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACpD,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;gBAChD,SAAS;YACX,CAAC;YAED,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACpD,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;YAC5D,MAAM,SAAS,GAAG,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;YAEzD,QAAQ,CAAC,IAAI,CAAC;gBACZ,SAAS;gBACT,WAAW;gBACX,SAAS;gBACT,MAAM,EAAE,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;aACzC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF;AA/PD,wCA+PC"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "genbox-agent",
3
+ "version": "0.0.2",
4
+ "description": "Agent daemon for monitoring and remotely controlling Claude Code sessions in genbox VMs",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "genbox-agent": "./dist/daemon.js",
9
+ "claude-hook": "./dist/hook.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "start": "node dist/daemon.js",
14
+ "dev": "tsx watch src/daemon.ts",
15
+ "hook": "node dist/hook.js"
16
+ },
17
+ "dependencies": {
18
+ "socket.io-client": "^4.8.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.10.0",
22
+ "tsx": "^4.7.0",
23
+ "typescript": "^5.3.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ }
28
+ }
package/src/daemon.ts ADDED
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Genbox Agent Daemon
4
+ *
5
+ * WebSocket client that runs on each genbox VM and:
6
+ * 1. Connects to the backend monitoring service
7
+ * 2. Receives remote commands (send_prompt, send_keystroke, create_session)
8
+ * 3. Manages tmux sessions running Claude Code
9
+ * 4. Streams terminal output back to the dashboard
10
+ */
11
+
12
+ import { io, Socket } from 'socket.io-client';
13
+ import { SessionManager } from './session-manager';
14
+
15
+ // Configuration from environment
16
+ const GENBOX_ID = process.env.GENBOX_ID || '';
17
+ const GENBOX_TOKEN = process.env.GENBOX_TOKEN || '';
18
+ const MONITORING_WS_URL = process.env.MONITORING_WS_URL || 'http://localhost:3001';
19
+
20
+ // Output polling interval (ms)
21
+ const OUTPUT_POLL_INTERVAL = 2000;
22
+
23
+ interface RequestData {
24
+ sessionId?: string;
25
+ prompt?: string;
26
+ key?: string;
27
+ projectPath?: string;
28
+ lines?: number;
29
+ requestId?: string;
30
+ }
31
+
32
+ class GenboxAgentDaemon {
33
+ private socket: Socket | null = null;
34
+ private sessionManager: SessionManager;
35
+ private running = false;
36
+ private outputPollers: Map<string, NodeJS.Timeout> = new Map();
37
+
38
+ constructor(
39
+ private readonly genboxId: string,
40
+ private readonly token: string,
41
+ private readonly wsUrl: string,
42
+ ) {
43
+ this.sessionManager = new SessionManager(genboxId);
44
+ }
45
+
46
+ async connect(): Promise<boolean> {
47
+ return new Promise((resolve) => {
48
+ console.log(`Connecting to ${this.wsUrl}/claude-ws...`);
49
+
50
+ this.socket = io(`${this.wsUrl}/claude-ws`, {
51
+ query: { genboxId: this.genboxId, type: 'genbox' },
52
+ auth: { genboxId: this.genboxId, token: this.token },
53
+ transports: ['websocket', 'polling'],
54
+ reconnection: true,
55
+ reconnectionAttempts: Infinity,
56
+ reconnectionDelay: 1000,
57
+ reconnectionDelayMax: 30000,
58
+ });
59
+
60
+ this.socket.on('connect', () => {
61
+ console.log(`[${this.genboxId}] Connected to monitoring server`);
62
+ this.running = true;
63
+ this.onConnect();
64
+ resolve(true);
65
+ });
66
+
67
+ this.socket.on('disconnect', (reason) => {
68
+ console.log(`[${this.genboxId}] Disconnected: ${reason}`);
69
+ this.running = false;
70
+ });
71
+
72
+ this.socket.on('connect_error', (error) => {
73
+ console.error(`[${this.genboxId}] Connection error:`, error.message);
74
+ });
75
+
76
+ // Register event handlers
77
+ this.socket.on('send_prompt', this.onSendPrompt.bind(this));
78
+ this.socket.on('send_keystroke', this.onSendKeystroke.bind(this));
79
+ this.socket.on('create_session', this.onCreateSession.bind(this));
80
+ this.socket.on('get_output', this.onGetOutput.bind(this));
81
+ this.socket.on('kill_session', this.onKillSession.bind(this));
82
+ this.socket.on('list_sessions', this.onListSessions.bind(this));
83
+ this.socket.on('error', this.onError.bind(this));
84
+
85
+ // Timeout after 10 seconds
86
+ setTimeout(() => {
87
+ if (!this.socket?.connected) {
88
+ console.error('Connection timeout');
89
+ resolve(false);
90
+ }
91
+ }, 10000);
92
+ });
93
+ }
94
+
95
+ private onConnect(): void {
96
+ // Register as genbox agent
97
+ this.socket?.emit('register_agent', {
98
+ genboxId: this.genboxId,
99
+ token: this.token,
100
+ });
101
+
102
+ // Send initial session list
103
+ const sessions = this.sessionManager.listSessions();
104
+ this.socket?.emit('agent_sessions', {
105
+ genboxId: this.genboxId,
106
+ sessions,
107
+ });
108
+ }
109
+
110
+ private onSendPrompt(data: RequestData): void {
111
+ const { sessionId, prompt, requestId } = data;
112
+ if (!sessionId || !prompt) return;
113
+
114
+ console.log(`[${this.genboxId}] Sending prompt to ${sessionId}: ${prompt.slice(0, 50)}...`);
115
+
116
+ const success = this.sessionManager.sendPrompt(sessionId, prompt);
117
+
118
+ this.socket?.emit('prompt_result', {
119
+ genboxId: this.genboxId,
120
+ sessionId,
121
+ requestId,
122
+ success,
123
+ });
124
+
125
+ // Capture output after a delay
126
+ if (success) {
127
+ setTimeout(() => this.captureAndSendOutput(sessionId), 1000);
128
+ }
129
+ }
130
+
131
+ private onSendKeystroke(data: RequestData): void {
132
+ const { sessionId, key, requestId } = data;
133
+ if (!sessionId || !key) return;
134
+
135
+ console.log(`[${this.genboxId}] Sending keystroke to ${sessionId}: ${key}`);
136
+
137
+ const success = this.sessionManager.sendKeystroke(sessionId, key);
138
+
139
+ this.socket?.emit('keystroke_result', {
140
+ genboxId: this.genboxId,
141
+ sessionId,
142
+ requestId,
143
+ success,
144
+ });
145
+ }
146
+
147
+ private onCreateSession(data: RequestData): void {
148
+ const { projectPath = '/home/dev', requestId } = data;
149
+
150
+ console.log(`[${this.genboxId}] Creating new session at ${projectPath}`);
151
+
152
+ const session = this.sessionManager.createSession(projectPath);
153
+
154
+ if (session) {
155
+ this.socket?.emit('session_created', {
156
+ genboxId: this.genboxId,
157
+ sessionId: session.sessionId,
158
+ sessionName: session.sessionName,
159
+ projectPath: session.projectPath,
160
+ requestId,
161
+ success: true,
162
+ });
163
+
164
+ // Start output polling for this session
165
+ this.startOutputPolling(session.sessionId);
166
+ } else {
167
+ this.socket?.emit('session_created', {
168
+ genboxId: this.genboxId,
169
+ requestId,
170
+ success: false,
171
+ error: 'Failed to create session',
172
+ });
173
+ }
174
+ }
175
+
176
+ private onGetOutput(data: RequestData): void {
177
+ const { sessionId, lines = 100, requestId } = data;
178
+ if (!sessionId) return;
179
+
180
+ const output = this.sessionManager.getOutput(sessionId, lines);
181
+ const status = this.sessionManager.getSessionStatus(sessionId);
182
+
183
+ this.socket?.emit('output_update', {
184
+ genboxId: this.genboxId,
185
+ sessionId,
186
+ requestId,
187
+ output: output || '',
188
+ status,
189
+ });
190
+ }
191
+
192
+ private onKillSession(data: RequestData): void {
193
+ const { sessionId, requestId } = data;
194
+ if (!sessionId) return;
195
+
196
+ console.log(`[${this.genboxId}] Killing session ${sessionId}`);
197
+
198
+ const success = this.sessionManager.killSession(sessionId);
199
+ this.stopOutputPolling(sessionId);
200
+
201
+ this.socket?.emit('session_killed', {
202
+ genboxId: this.genboxId,
203
+ sessionId,
204
+ requestId,
205
+ success,
206
+ });
207
+ }
208
+
209
+ private onListSessions(data: RequestData): void {
210
+ const { requestId } = data;
211
+
212
+ const sessions = this.sessionManager.listSessions();
213
+
214
+ this.socket?.emit('sessions_list', {
215
+ genboxId: this.genboxId,
216
+ requestId,
217
+ sessions,
218
+ });
219
+ }
220
+
221
+ private onError(data: any): void {
222
+ console.error(`[${this.genboxId}] Error from server:`, data);
223
+ }
224
+
225
+ private startOutputPolling(sessionId: string): void {
226
+ if (this.outputPollers.has(sessionId)) return;
227
+
228
+ let lastOutput = '';
229
+
230
+ const poller = setInterval(() => {
231
+ if (!this.running) {
232
+ this.stopOutputPolling(sessionId);
233
+ return;
234
+ }
235
+
236
+ const output = this.sessionManager.getOutput(sessionId, 100);
237
+ const status = this.sessionManager.getSessionStatus(sessionId);
238
+
239
+ // Only send if output changed
240
+ if (output && output !== lastOutput) {
241
+ lastOutput = output;
242
+ this.socket?.emit('output_update', {
243
+ genboxId: this.genboxId,
244
+ sessionId,
245
+ output,
246
+ status,
247
+ });
248
+ }
249
+
250
+ // Stop polling if session ended
251
+ if (status === 'ended') {
252
+ this.stopOutputPolling(sessionId);
253
+ }
254
+ }, OUTPUT_POLL_INTERVAL);
255
+
256
+ this.outputPollers.set(sessionId, poller);
257
+ }
258
+
259
+ private stopOutputPolling(sessionId: string): void {
260
+ const poller = this.outputPollers.get(sessionId);
261
+ if (poller) {
262
+ clearInterval(poller);
263
+ this.outputPollers.delete(sessionId);
264
+ }
265
+ }
266
+
267
+ private captureAndSendOutput(sessionId: string): void {
268
+ const output = this.sessionManager.getOutput(sessionId, 100);
269
+ const status = this.sessionManager.getSessionStatus(sessionId);
270
+
271
+ if (output && this.socket?.connected) {
272
+ this.socket.emit('output_update', {
273
+ genboxId: this.genboxId,
274
+ sessionId,
275
+ output,
276
+ status,
277
+ });
278
+ }
279
+ }
280
+
281
+ async run(): Promise<void> {
282
+ console.log(`Starting Genbox Agent Daemon for ${this.genboxId}`);
283
+
284
+ const connected = await this.connect();
285
+ if (!connected) {
286
+ console.log('Initial connection failed, will keep retrying...');
287
+ }
288
+
289
+ // Keep running
290
+ await new Promise<void>((resolve) => {
291
+ const shutdown = () => {
292
+ console.log('Shutting down...');
293
+ this.running = false;
294
+
295
+ // Stop all output pollers
296
+ for (const sessionId of this.outputPollers.keys()) {
297
+ this.stopOutputPolling(sessionId);
298
+ }
299
+
300
+ this.socket?.disconnect();
301
+ resolve();
302
+ };
303
+
304
+ process.on('SIGTERM', shutdown);
305
+ process.on('SIGINT', shutdown);
306
+ });
307
+ }
308
+ }
309
+
310
+ // Main entry point
311
+ async function main(): Promise<void> {
312
+ if (!GENBOX_ID) {
313
+ console.error('Error: GENBOX_ID environment variable not set');
314
+ process.exit(1);
315
+ }
316
+
317
+ if (!GENBOX_TOKEN) {
318
+ console.error('Error: GENBOX_TOKEN environment variable not set');
319
+ process.exit(1);
320
+ }
321
+
322
+ const daemon = new GenboxAgentDaemon(GENBOX_ID, GENBOX_TOKEN, MONITORING_WS_URL);
323
+ await daemon.run();
324
+ }
325
+
326
+ main().catch((error) => {
327
+ console.error('Fatal error:', error);
328
+ process.exit(1);
329
+ });