@quantiya/codevibe-codex-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.env.example +29 -0
  2. package/README.md +155 -0
  3. package/bin/codevibe-codex +187 -0
  4. package/dist/approval-detector.d.ts +38 -0
  5. package/dist/approval-detector.d.ts.map +1 -0
  6. package/dist/approval-detector.js +174 -0
  7. package/dist/approval-detector.js.map +1 -0
  8. package/dist/appsync-client.d.ts +69 -0
  9. package/dist/appsync-client.d.ts.map +1 -0
  10. package/dist/appsync-client.js +937 -0
  11. package/dist/appsync-client.js.map +1 -0
  12. package/dist/auth-cli.d.ts +11 -0
  13. package/dist/auth-cli.d.ts.map +1 -0
  14. package/dist/auth-cli.js +241 -0
  15. package/dist/auth-cli.js.map +1 -0
  16. package/dist/config.d.ts +29 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +116 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/crypto-service.d.ts +115 -0
  21. package/dist/crypto-service.d.ts.map +1 -0
  22. package/dist/crypto-service.js +278 -0
  23. package/dist/crypto-service.js.map +1 -0
  24. package/dist/event-mapper.d.ts +24 -0
  25. package/dist/event-mapper.d.ts.map +1 -0
  26. package/dist/event-mapper.js +268 -0
  27. package/dist/event-mapper.js.map +1 -0
  28. package/dist/key-manager.d.ts +87 -0
  29. package/dist/key-manager.d.ts.map +1 -0
  30. package/dist/key-manager.js +287 -0
  31. package/dist/key-manager.js.map +1 -0
  32. package/dist/logger.d.ts +2 -0
  33. package/dist/logger.d.ts.map +1 -0
  34. package/dist/logger.js +18 -0
  35. package/dist/logger.js.map +1 -0
  36. package/dist/prompt-parser.d.ts +3 -0
  37. package/dist/prompt-parser.d.ts.map +1 -0
  38. package/dist/prompt-parser.js +8 -0
  39. package/dist/prompt-parser.js.map +1 -0
  40. package/dist/prompt-responder.d.ts +18 -0
  41. package/dist/prompt-responder.d.ts.map +1 -0
  42. package/dist/prompt-responder.js +78 -0
  43. package/dist/prompt-responder.js.map +1 -0
  44. package/dist/server.d.ts +8 -0
  45. package/dist/server.d.ts.map +1 -0
  46. package/dist/server.js +1045 -0
  47. package/dist/server.js.map +1 -0
  48. package/dist/session-id-cache.d.ts +16 -0
  49. package/dist/session-id-cache.d.ts.map +1 -0
  50. package/dist/session-id-cache.js +90 -0
  51. package/dist/session-id-cache.js.map +1 -0
  52. package/dist/session-log-watcher.d.ts +61 -0
  53. package/dist/session-log-watcher.d.ts.map +1 -0
  54. package/dist/session-log-watcher.js +372 -0
  55. package/dist/session-log-watcher.js.map +1 -0
  56. package/dist/tmux-pane-observer.d.ts +39 -0
  57. package/dist/tmux-pane-observer.d.ts.map +1 -0
  58. package/dist/tmux-pane-observer.js +255 -0
  59. package/dist/tmux-pane-observer.js.map +1 -0
  60. package/dist/token-storage.d.ts +39 -0
  61. package/dist/token-storage.d.ts.map +1 -0
  62. package/dist/token-storage.js +169 -0
  63. package/dist/token-storage.js.map +1 -0
  64. package/dist/types.d.ts +158 -0
  65. package/dist/types.d.ts.map +1 -0
  66. package/dist/types.js +17 -0
  67. package/dist/types.js.map +1 -0
  68. package/package.json +72 -0
package/dist/server.js ADDED
@@ -0,0 +1,1045 @@
1
+ "use strict";
2
+ /**
3
+ * CodeVibe Codex Plugin - Main Server
4
+ *
5
+ * Watches Codex CLI session logs and syncs events to iOS app via AWS AppSync.
6
+ * Receives mobile prompts via subscription and sends them to Codex via tmux.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ const uuid_1 = require("uuid");
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ // Import shared modules from codevibe-core
47
+ const codevibe_core_1 = require("@quantiya/codevibe-core");
48
+ // Import plugin-specific logger
49
+ const logger_1 = require("./logger");
50
+ // Import plugin-specific modules
51
+ const session_log_watcher_1 = require("./session-log-watcher");
52
+ const event_mapper_1 = require("./event-mapper");
53
+ const approval_detector_1 = require("./approval-detector");
54
+ const prompt_responder_1 = require("./prompt-responder");
55
+ const tmux_pane_observer_1 = require("./tmux-pane-observer");
56
+ const prompt_parser_1 = require("./prompt-parser");
57
+ class CodexCompanionServer {
58
+ constructor() {
59
+ this.sessionState = null;
60
+ this.unsubscribe = null;
61
+ this.sessionKey = null; // E2E encryption session key
62
+ this.pendingInteractivePrompt = null;
63
+ this.isInitializingSession = false;
64
+ this.bufferedLogEntries = [];
65
+ // AppSyncClient is created in start() after config is loaded
66
+ this.sessionWatcher = new session_log_watcher_1.SessionLogWatcher();
67
+ this.approvalDetector = new approval_detector_1.ApprovalDetector();
68
+ this.promptResponder = new prompt_responder_1.PromptResponder();
69
+ this.tmuxPaneObserver = new tmux_pane_observer_1.TmuxPaneObserver();
70
+ }
71
+ /**
72
+ * Start the server
73
+ */
74
+ async start() {
75
+ logger_1.logger.info('Starting CodeVibe Codex companion server', {
76
+ environment: (0, codevibe_core_1.getEnvironment)(),
77
+ });
78
+ // Create AppSyncClient (auto-configures from ENVIRONMENT env var)
79
+ this.appSyncClient = new codevibe_core_1.AppSyncClient();
80
+ // Authenticate with stored tokens
81
+ const authenticated = await this.appSyncClient.authenticateWithStoredTokens();
82
+ if (!authenticated) {
83
+ logger_1.logger.error('Authentication failed. Run "codevibe-codex login" first.');
84
+ console.error('Not authenticated. Run "codevibe-codex login" to sign in.');
85
+ process.exit(1);
86
+ }
87
+ logger_1.logger.info('Authenticated successfully', {
88
+ userId: this.appSyncClient.getCurrentUserId(),
89
+ email: this.appSyncClient.getCurrentUserEmail(),
90
+ });
91
+ // Register device encryption key for E2E encryption
92
+ await this.registerDeviceEncryptionKey();
93
+ // Set up event handlers
94
+ this.setupEventHandlers();
95
+ // Start watching session logs
96
+ this.sessionWatcher.start();
97
+ logger_1.logger.info('CodeVibe Codex companion server started');
98
+ }
99
+ /**
100
+ * Set up event handlers for session watcher and approval detector
101
+ */
102
+ setupEventHandlers() {
103
+ // Handle new Codex session
104
+ this.sessionWatcher.on('session-started', async (meta) => {
105
+ await this.handleSessionStarted(meta);
106
+ });
107
+ // Handle log entries
108
+ this.sessionWatcher.on('log-entry', async (entry) => {
109
+ await this.handleLogEntry(entry);
110
+ });
111
+ // Handle approval pending detection
112
+ this.approvalDetector.on('approval-pending', async (data) => {
113
+ await this.handleApprovalPending(data);
114
+ });
115
+ this.tmuxPaneObserver.on('prompt-candidate', async (candidate) => {
116
+ await this.handleTmuxPromptCandidate(candidate.snapshot);
117
+ });
118
+ this.tmuxPaneObserver.on('observer-error', (error) => {
119
+ logger_1.logger.debug('Tmux pane observer error', { error });
120
+ });
121
+ // Handle errors
122
+ this.sessionWatcher.on('error', (error) => {
123
+ logger_1.logger.error('Session watcher error:', error);
124
+ });
125
+ }
126
+ /**
127
+ * Register device encryption key with backend for E2E encryption
128
+ */
129
+ async registerDeviceEncryptionKey() {
130
+ try {
131
+ // Get or generate device key pair
132
+ const deviceId = await codevibe_core_1.keychainManager.getDeviceId();
133
+ const publicKey = await codevibe_core_1.keychainManager.getDevicePublicKey();
134
+ const platform = codevibe_core_1.keychainManager.getDevicePlatform();
135
+ const deviceName = codevibe_core_1.keychainManager.getDeviceName();
136
+ logger_1.logger.info('Registering device encryption key', { deviceId, platform, deviceName });
137
+ // Register with backend
138
+ await this.appSyncClient.registerDeviceKey(deviceId, publicKey, platform, deviceName);
139
+ codevibe_core_1.keychainManager.setIsRegistered(true);
140
+ logger_1.logger.info('Device encryption key registered successfully', { deviceId });
141
+ }
142
+ catch (error) {
143
+ // Don't fail startup if registration fails - encryption is optional
144
+ logger_1.logger.warn('Failed to register device encryption key (E2E encryption may not work):', error);
145
+ }
146
+ }
147
+ /**
148
+ * Handle new Codex session started
149
+ */
150
+ async handleSessionStarted(meta) {
151
+ logger_1.logger.info('Handling new Codex session', { codexSessionId: meta.id });
152
+ // Mark that session initialization is in progress so log entries
153
+ // arriving before sessionState is set are buffered instead of dropped.
154
+ this.isInitializingSession = true;
155
+ this.bufferedLogEntries = [];
156
+ // If we already have an active session, mark it inactive before starting a new one
157
+ if (this.sessionState) {
158
+ await this.endActiveSession('new-codex-session-started');
159
+ }
160
+ const projectPath = process.env.CODEX_WORKING_DIRECTORY || meta.cwd || process.cwd();
161
+ // Use Codex's actual runtime session ID so concurrent Codex sessions in the
162
+ // same directory appear as separate sessions in the iOS app.
163
+ const sessionId = this.generateSessionId(meta.id);
164
+ const userId = this.appSyncClient.getCurrentUserId();
165
+ // Base metadata (unencrypted)
166
+ const baseMetadata = {
167
+ codexSessionId: meta.id,
168
+ cliVersion: meta.cli_version,
169
+ modelProvider: meta.model_provider,
170
+ };
171
+ // Use centralized resume/create utility from codevibe-core
172
+ try {
173
+ const result = await (0, codevibe_core_1.resumeOrCreateSession)({
174
+ sessionId,
175
+ userId,
176
+ agentType: codevibe_core_1.AgentType.CODEX,
177
+ projectPath,
178
+ metadata: baseMetadata,
179
+ }, this.appSyncClient, logger_1.logger);
180
+ this.sessionKey = result.sessionKey;
181
+ }
182
+ catch (error) {
183
+ logger_1.logger.error('Failed to create/resume session:', error);
184
+ throw error;
185
+ }
186
+ try {
187
+ // Store session state
188
+ this.sessionState = {
189
+ sessionId,
190
+ userId,
191
+ projectPath,
192
+ cwd: meta.cwd,
193
+ createdAt: new Date(),
194
+ subscriptionActive: false,
195
+ metadata: baseMetadata,
196
+ codexSessionId: meta.id,
197
+ codexLogFile: this.sessionWatcher.getActiveLogFile() || undefined,
198
+ };
199
+ // Flush any log entries that arrived while we were initializing
200
+ await this.flushBufferedLogEntries();
201
+ await this.startTmuxObserver();
202
+ // Subscribe to mobile events
203
+ this.subscribeToMobileEvents(sessionId);
204
+ // Start heartbeat so iOS can detect if desktop is still connected
205
+ this.appSyncClient.startHeartbeat(sessionId);
206
+ }
207
+ catch (error) {
208
+ logger_1.logger.error('Failed to create session:', error);
209
+ this.bufferedLogEntries = [];
210
+ }
211
+ finally {
212
+ this.isInitializingSession = false;
213
+ }
214
+ }
215
+ /**
216
+ * Flush log entries that were buffered during async session initialization.
217
+ */
218
+ async flushBufferedLogEntries() {
219
+ if (this.bufferedLogEntries.length === 0) {
220
+ return;
221
+ }
222
+ const entries = this.bufferedLogEntries;
223
+ this.bufferedLogEntries = [];
224
+ logger_1.logger.info('Flushing buffered log entries after session initialization', {
225
+ count: entries.length,
226
+ sessionId: this.sessionState?.sessionId,
227
+ });
228
+ for (const entry of entries) {
229
+ await this.handleLogEntry(entry);
230
+ }
231
+ }
232
+ /**
233
+ * Handle a log entry from Codex
234
+ */
235
+ async handleLogEntry(entry) {
236
+ if (!this.sessionState) {
237
+ if (this.isInitializingSession) {
238
+ this.bufferedLogEntries.push(entry);
239
+ logger_1.logger.debug('Buffering log entry until session initialization completes', {
240
+ type: entry.type,
241
+ bufferedCount: this.bufferedLogEntries.length,
242
+ });
243
+ return;
244
+ }
245
+ logger_1.logger.warn('Received log entry but no active session');
246
+ return;
247
+ }
248
+ // Track tool calls for approval detection
249
+ if (entry.type === 'response_item' && entry.payload) {
250
+ const itemType = entry.payload.type;
251
+ if (itemType === 'function_call' || itemType === 'custom_tool_call') {
252
+ this.approvalDetector.onToolCallStart(entry.payload.call_id, entry.payload.name, entry.payload.arguments || entry.payload.input || '');
253
+ }
254
+ else if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
255
+ this.approvalDetector.onToolCallComplete(entry.payload.call_id);
256
+ if (this.pendingInteractivePrompt?.callId === entry.payload.call_id) {
257
+ this.pendingInteractivePrompt = null;
258
+ }
259
+ }
260
+ }
261
+ // Map to CodeVibe event
262
+ const eventInput = (0, event_mapper_1.mapLogEntryToEvent)(entry, this.sessionState.sessionId);
263
+ if (!eventInput) {
264
+ return; // Entry should not be synced
265
+ }
266
+ try {
267
+ // Encrypt event content if we have a session key
268
+ if (this.sessionKey) {
269
+ eventInput.content = codevibe_core_1.cryptoService.encryptContent(eventInput.content, this.sessionKey);
270
+ if (eventInput.metadata) {
271
+ const encryptedMetadata = codevibe_core_1.cryptoService.encryptMetadata(eventInput.metadata, this.sessionKey);
272
+ eventInput.metadata = { encrypted: encryptedMetadata };
273
+ }
274
+ eventInput.isEncrypted = true;
275
+ logger_1.logger.debug('Event encrypted', { type: eventInput.type });
276
+ }
277
+ await this.appSyncClient.createEvent(eventInput);
278
+ logger_1.logger.debug('Event synced to backend', { type: eventInput.type, encrypted: !!this.sessionKey });
279
+ }
280
+ catch (error) {
281
+ logger_1.logger.error('Failed to sync event:', error);
282
+ }
283
+ }
284
+ /**
285
+ * Handle approval pending notification
286
+ */
287
+ async handleApprovalPending(data) {
288
+ if (!this.sessionState) {
289
+ return;
290
+ }
291
+ logger_1.logger.info('Sending approval pending interactive prompt', data);
292
+ try {
293
+ const tmuxPrompt = await this.tryParseInteractivePromptFromTmux();
294
+ const parsedPrompt = tmuxPrompt?.parsedPrompt ?? null;
295
+ if (parsedPrompt &&
296
+ this.pendingInteractivePrompt &&
297
+ this.pendingInteractivePrompt.source === 'tmux' &&
298
+ this.pendingInteractivePrompt.promptText === parsedPrompt.promptText) {
299
+ logger_1.logger.debug('Skipping heuristic prompt because tmux prompt is already active', {
300
+ promptText: parsedPrompt.promptText,
301
+ });
302
+ return;
303
+ }
304
+ const toolDetails = this.buildToolDetailsForInteractivePrompt(data, tmuxPrompt?.snapshot);
305
+ const toolNameForMetadata = toolDetails.tool_name || this.mapToolNameForApproval(data.toolName);
306
+ const toolInputForMetadata = toolDetails.tool_input || this.buildFallbackToolInput(data);
307
+ const hasDetails = !!(toolNameForMetadata && toolInputForMetadata);
308
+ const promptPresentation = this.buildPromptPresentation(parsedPrompt);
309
+ const options = promptPresentation.options;
310
+ const fileLine = data.filePath ? `File: ${data.filePath}` : undefined;
311
+ let content = promptPresentation.content || `Codex is waiting for approval.\n${data.hint}`;
312
+ if (fileLine && !content.includes(fileLine)) {
313
+ content = `${content}\n${fileLine}`;
314
+ }
315
+ this.pendingInteractivePrompt = {
316
+ promptId: data.callId,
317
+ callId: data.callId,
318
+ kind: promptPresentation.kind,
319
+ options,
320
+ submitMap: promptPresentation.submitMap,
321
+ promptText: promptPresentation.promptText,
322
+ createdAt: Date.now(),
323
+ source: parsedPrompt ? 'tmux' : 'heuristic',
324
+ requiresFollowUpText: promptPresentation.requiresFollowUpText,
325
+ };
326
+ let metadata = {
327
+ isApprovalHint: true,
328
+ toolName: data.toolName,
329
+ toolInput: data.toolInput,
330
+ hint: data.hint,
331
+ callId: data.callId,
332
+ filePath: data.filePath,
333
+ diff: data.diff,
334
+ rawInput: data.rawInput,
335
+ tool_name: toolNameForMetadata,
336
+ tool_input: toolInputForMetadata,
337
+ has_details: hasDetails,
338
+ options,
339
+ instructions: promptPresentation.instructions,
340
+ prompt_source: parsedPrompt ? 'tmux' : 'heuristic',
341
+ };
342
+ let isEncrypted = false;
343
+ // Debug log before encryption so we can see what iOS should receive
344
+ logger_1.logger.debug('Interactive prompt (pre-encryption)', {
345
+ sessionId: this.sessionState.sessionId,
346
+ callId: data.callId,
347
+ contentPreview: content.substring(0, 200),
348
+ toolDetails,
349
+ metadata,
350
+ });
351
+ // Encrypt notification if we have a session key
352
+ if (this.sessionKey) {
353
+ content = codevibe_core_1.cryptoService.encryptContent(content, this.sessionKey);
354
+ const encryptedMetadata = codevibe_core_1.cryptoService.encryptMetadata(metadata, this.sessionKey);
355
+ metadata = { encrypted: encryptedMetadata };
356
+ isEncrypted = true;
357
+ }
358
+ await this.appSyncClient.createEvent({
359
+ sessionId: this.sessionState.sessionId,
360
+ type: codevibe_core_1.EventType.INTERACTIVE_PROMPT,
361
+ source: codevibe_core_1.EventSource.DESKTOP,
362
+ content,
363
+ metadata,
364
+ promptId: data.callId,
365
+ ...(isEncrypted ? { isEncrypted: true } : {}),
366
+ });
367
+ }
368
+ catch (error) {
369
+ logger_1.logger.error('Failed to send approval interactive prompt:', error);
370
+ }
371
+ }
372
+ async handleTmuxPromptCandidate(snapshot) {
373
+ if (!this.sessionState) {
374
+ return;
375
+ }
376
+ const parsedPrompt = (0, prompt_parser_1.parseInteractivePrompt)(snapshot);
377
+ if (!parsedPrompt) {
378
+ return;
379
+ }
380
+ if (this.pendingInteractivePrompt &&
381
+ this.pendingInteractivePrompt.source === 'tmux' &&
382
+ this.pendingInteractivePrompt.promptText === parsedPrompt.promptText) {
383
+ return;
384
+ }
385
+ const promptPresentation = this.buildPromptPresentation(parsedPrompt);
386
+ const pendingCall = this.getMostRecentPendingToolCall();
387
+ const pendingCallContext = pendingCall ? this.buildApprovalPromptContextFromPendingCall(pendingCall) : null;
388
+ const toolDetails = pendingCallContext
389
+ ? this.buildToolDetailsForInteractivePrompt(pendingCallContext, snapshot)
390
+ : {};
391
+ const toolNameForMetadata = toolDetails.tool_name || this.mapToolNameForApproval(pendingCall?.name);
392
+ const toolInputForMetadata = toolDetails.tool_input || (pendingCallContext ? this.buildFallbackToolInput(pendingCallContext) : undefined);
393
+ const hasDetails = !!(toolNameForMetadata && toolInputForMetadata);
394
+ const promptId = this.pendingInteractivePrompt?.callId || pendingCall?.callId || (0, uuid_1.v4)();
395
+ this.pendingInteractivePrompt = {
396
+ promptId,
397
+ callId: this.pendingInteractivePrompt?.callId || pendingCall?.callId,
398
+ kind: promptPresentation.kind,
399
+ options: promptPresentation.options,
400
+ submitMap: promptPresentation.submitMap,
401
+ promptText: promptPresentation.promptText,
402
+ createdAt: Date.now(),
403
+ source: 'tmux',
404
+ requiresFollowUpText: promptPresentation.requiresFollowUpText,
405
+ };
406
+ let metadata = {
407
+ options: promptPresentation.options,
408
+ instructions: promptPresentation.instructions,
409
+ prompt_source: 'tmux_live',
410
+ tool_name: toolNameForMetadata,
411
+ tool_input: toolInputForMetadata,
412
+ has_details: hasDetails,
413
+ };
414
+ let content = promptPresentation.content;
415
+ let isEncrypted = false;
416
+ if (this.sessionKey) {
417
+ content = codevibe_core_1.cryptoService.encryptContent(content, this.sessionKey);
418
+ const encryptedMetadata = codevibe_core_1.cryptoService.encryptMetadata(metadata, this.sessionKey);
419
+ metadata = { encrypted: encryptedMetadata };
420
+ isEncrypted = true;
421
+ }
422
+ try {
423
+ await this.appSyncClient.createEvent({
424
+ sessionId: this.sessionState.sessionId,
425
+ type: codevibe_core_1.EventType.INTERACTIVE_PROMPT,
426
+ source: codevibe_core_1.EventSource.DESKTOP,
427
+ content,
428
+ metadata,
429
+ promptId,
430
+ ...(isEncrypted ? { isEncrypted: true } : {}),
431
+ });
432
+ logger_1.logger.info('Sent tmux-detected interactive prompt', {
433
+ sessionId: this.sessionState.sessionId,
434
+ promptText: parsedPrompt.promptText,
435
+ kind: parsedPrompt.kind,
436
+ });
437
+ }
438
+ catch (error) {
439
+ logger_1.logger.error('Failed to send tmux-detected interactive prompt', { error });
440
+ }
441
+ }
442
+ async startTmuxObserver() {
443
+ const tmuxSession = process.env.CODEVIBE_CODEX_TMUX_SESSION;
444
+ if (!tmuxSession) {
445
+ logger_1.logger.debug('Skipping tmux pane observer start - no tmux session in environment');
446
+ return;
447
+ }
448
+ try {
449
+ await this.tmuxPaneObserver.start(tmuxSession);
450
+ }
451
+ catch (error) {
452
+ logger_1.logger.warn('Failed to start tmux pane observer', { tmuxSession, error });
453
+ }
454
+ }
455
+ async tryParseInteractivePromptFromTmux() {
456
+ try {
457
+ const snapshot = await this.tmuxPaneObserver.captureSnapshot();
458
+ const parsed = (0, prompt_parser_1.parseInteractivePrompt)(snapshot);
459
+ logger_1.logger.debug('tmux prompt parse result', {
460
+ parsed: !!parsed,
461
+ kind: parsed?.kind,
462
+ promptText: parsed?.promptText,
463
+ snapshotPreview: this.summarizePromptSnapshot(snapshot),
464
+ });
465
+ return {
466
+ parsedPrompt: parsed,
467
+ snapshot,
468
+ };
469
+ }
470
+ catch (error) {
471
+ logger_1.logger.debug('tmux prompt parsing unavailable', { error });
472
+ return null;
473
+ }
474
+ }
475
+ buildPromptPresentation(parsedPrompt) {
476
+ if (parsedPrompt) {
477
+ return {
478
+ content: parsedPrompt.promptText,
479
+ promptText: parsedPrompt.promptText,
480
+ kind: parsedPrompt.kind,
481
+ options: parsedPrompt.options,
482
+ submitMap: parsedPrompt.submitMap,
483
+ instructions: this.buildPromptInstructions(parsedPrompt),
484
+ requiresFollowUpText: parsedPrompt.requiresFollowUpText,
485
+ };
486
+ }
487
+ return {
488
+ content: 'Codex is waiting for approval.',
489
+ promptText: 'Codex is waiting for approval.',
490
+ kind: 'yes_no',
491
+ options: [
492
+ { number: '1', text: 'Yes (sends "y")' },
493
+ { number: '2', text: 'No, tell Codex what to change (sends "n <instructions>")' },
494
+ ],
495
+ submitMap: {
496
+ '1': 'y',
497
+ '2': 'n',
498
+ },
499
+ instructions: 'Reply with 1 to approve, or 2 followed by what to change',
500
+ requiresFollowUpText: true,
501
+ };
502
+ }
503
+ getMostRecentPendingToolCall() {
504
+ const pendingCalls = this.approvalDetector.getPendingCalls();
505
+ if (pendingCalls.length === 0) {
506
+ return null;
507
+ }
508
+ return pendingCalls.reduce((latest, current) => {
509
+ return current.timestamp > latest.timestamp ? current : latest;
510
+ });
511
+ }
512
+ buildApprovalPromptContextFromPendingCall(pendingCall) {
513
+ return {
514
+ toolName: pendingCall.name,
515
+ filePath: pendingCall.filePath,
516
+ diff: pendingCall.diff,
517
+ toolInput: pendingCall.parsedInput,
518
+ rawInput: pendingCall.input,
519
+ hint: pendingCall.filePath ? `File: ${pendingCall.filePath}` : `Tool: ${this.mapToolNameForApproval(pendingCall.name) || pendingCall.name}`,
520
+ };
521
+ }
522
+ buildPromptInstructions(parsedPrompt) {
523
+ if (parsedPrompt.kind === 'yes_no' && parsedPrompt.requiresFollowUpText) {
524
+ return 'Reply with 1 to approve, or 2 followed by what to change';
525
+ }
526
+ if (parsedPrompt.kind === 'yes_no') {
527
+ return 'Reply with 1 for yes or 2 for no';
528
+ }
529
+ if (parsedPrompt.kind === 'numbered') {
530
+ return 'Reply with the number of the option you want';
531
+ }
532
+ return 'Reply with your response';
533
+ }
534
+ summarizePromptSnapshot(snapshot) {
535
+ return snapshot
536
+ .split('\n')
537
+ .map((line) => line.trimEnd())
538
+ .filter((line) => line.length > 0)
539
+ .slice(-12)
540
+ .map((line) => line.slice(0, 160))
541
+ .join('\n');
542
+ }
543
+ translatePromptResponse(input) {
544
+ const prompt = this.pendingInteractivePrompt;
545
+ if (!prompt) {
546
+ return { primaryInput: input };
547
+ }
548
+ const trimmed = input.trim();
549
+ const match = trimmed.match(/^(\d+)(?:\s+([\s\S]+))?$/);
550
+ if (!match) {
551
+ return { primaryInput: input };
552
+ }
553
+ const selected = match[1];
554
+ const followUpText = match[2]?.trim();
555
+ const mapped = prompt.submitMap[selected];
556
+ if (!mapped) {
557
+ return { primaryInput: input };
558
+ }
559
+ if (prompt.requiresFollowUpText && followUpText) {
560
+ return {
561
+ primaryInput: mapped,
562
+ followUpInput: followUpText,
563
+ };
564
+ }
565
+ return { primaryInput: mapped };
566
+ }
567
+ /**
568
+ * Build tool metadata for INTERACTIVE_PROMPT so iOS can render previews/diffs.
569
+ * The mobile app expects metadata JSON with tool_name/tool_input (same shape as TOOL_USE content).
570
+ */
571
+ buildToolDetailsForInteractivePrompt(data, snapshot) {
572
+ const toolName = data.toolName;
573
+ const parsedInput = data.toolInput && typeof data.toolInput === 'object' ? data.toolInput : undefined;
574
+ // apply_patch → expose an Edit payload with extracted old/new buffers for iOS diff rendering.
575
+ if (toolName === 'apply_patch') {
576
+ const content = data.diff || data.rawInput;
577
+ if (content) {
578
+ const { oldString, newString, oldStartLine, newStartLine } = this.extractOldNewFromPatch(content);
579
+ const snapshotLineAnchors = snapshot ? this.extractDiffLineAnchorsFromSnapshot(snapshot) : {};
580
+ return {
581
+ tool_name: 'Edit',
582
+ tool_input: {
583
+ file_path: data.filePath,
584
+ content,
585
+ diff: data.diff,
586
+ raw_patch: data.rawInput,
587
+ old_string: oldString,
588
+ new_string: newString,
589
+ old_start_line: oldStartLine ?? snapshotLineAnchors.oldStartLine,
590
+ new_start_line: newStartLine ?? snapshotLineAnchors.newStartLine,
591
+ },
592
+ };
593
+ }
594
+ }
595
+ // shell_command / shell → treat as Bash so iOS shows command preview
596
+ if (toolName === 'shell_command' || toolName === 'shell') {
597
+ const command = parsedInput?.command || data.rawInput || data.hint;
598
+ if (command) {
599
+ return {
600
+ tool_name: 'Bash',
601
+ tool_input: {
602
+ command,
603
+ output: parsedInput?.output,
604
+ },
605
+ };
606
+ }
607
+ }
608
+ // Fallback: include whatever context we have
609
+ const fallbackInput = {};
610
+ if (data.filePath)
611
+ fallbackInput.file_path = data.filePath;
612
+ if (data.diff)
613
+ fallbackInput.diff = data.diff;
614
+ if (data.rawInput)
615
+ fallbackInput.raw_input = data.rawInput;
616
+ if (Object.keys(fallbackInput).length > 0) {
617
+ return {
618
+ tool_name: toolName || 'Tool',
619
+ tool_input: fallbackInput,
620
+ };
621
+ }
622
+ return {};
623
+ }
624
+ /**
625
+ * Fallback tool input for interactive prompts (keeps file path + diff/raw input).
626
+ */
627
+ buildFallbackToolInput(data) {
628
+ const input = {};
629
+ if (data.filePath)
630
+ input.file_path = data.filePath;
631
+ if (data.diff)
632
+ input.diff = data.diff;
633
+ if (data.rawInput)
634
+ input.raw_input = data.rawInput;
635
+ if (data.toolInput && typeof data.toolInput === 'object') {
636
+ input.parsed_input = data.toolInput;
637
+ }
638
+ return Object.keys(input).length > 0 ? input : undefined;
639
+ }
640
+ /**
641
+ * Map Codex tool names to mobile-friendly labels for approval prompts.
642
+ */
643
+ mapToolNameForApproval(name) {
644
+ if (!name)
645
+ return undefined;
646
+ const mapping = {
647
+ apply_patch: 'Edit',
648
+ shell_command: 'Bash',
649
+ shell: 'Bash',
650
+ };
651
+ return mapping[name] || name;
652
+ }
653
+ /**
654
+ * Extract old/new buffers from an apply_patch diff so the iOS diff UI can render.
655
+ * Includes hunk context lines and preserves start-line metadata for single-hunk patches.
656
+ */
657
+ extractOldNewFromPatch(patch) {
658
+ const oldLines = [];
659
+ const newLines = [];
660
+ const hunkHeaderPattern = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
661
+ let hunkCount = 0;
662
+ let currentOldLine = 0;
663
+ let currentNewLine = 0;
664
+ let oldStartLine;
665
+ let newStartLine;
666
+ for (const line of patch.split('\n')) {
667
+ const hunkMatch = line.match(hunkHeaderPattern);
668
+ if (hunkMatch) {
669
+ hunkCount += 1;
670
+ currentOldLine = Number.parseInt(hunkMatch[1], 10);
671
+ currentNewLine = Number.parseInt(hunkMatch[2], 10);
672
+ continue;
673
+ }
674
+ if (line.startsWith('***') ||
675
+ line.startsWith('---') ||
676
+ line.startsWith('+++') ||
677
+ line.startsWith('*** End Patch')) {
678
+ continue;
679
+ }
680
+ if (line.startsWith('-')) {
681
+ if (oldStartLine === undefined) {
682
+ oldStartLine = currentOldLine;
683
+ }
684
+ oldLines.push(line.slice(1));
685
+ currentOldLine += 1;
686
+ }
687
+ else if (line.startsWith('+')) {
688
+ if (newStartLine === undefined) {
689
+ newStartLine = currentNewLine;
690
+ }
691
+ newLines.push(line.slice(1));
692
+ currentNewLine += 1;
693
+ }
694
+ else if (line.startsWith(' ')) {
695
+ const contextLine = line.slice(1);
696
+ oldLines.push(contextLine);
697
+ newLines.push(contextLine);
698
+ currentOldLine += 1;
699
+ currentNewLine += 1;
700
+ }
701
+ }
702
+ // Use empty string instead of undefined so keys are always present in JSON
703
+ // This ensures iOS can detect the diff (checks for nil, not empty string)
704
+ return {
705
+ oldString: oldLines.join('\n'), // Empty string if no old lines (new file)
706
+ newString: newLines.join('\n'), // Empty string if no new lines (deleted file)
707
+ // Only attach exact start lines when the patch has a single hunk.
708
+ oldStartLine: hunkCount === 1 ? oldStartLine : undefined,
709
+ newStartLine: hunkCount === 1 ? newStartLine : undefined,
710
+ };
711
+ }
712
+ /**
713
+ * Codex's apply_patch payload often uses bare "@@" markers without absolute
714
+ * hunk coordinates. When that happens, recover visible line anchors from the
715
+ * tmux-rendered diff preview instead.
716
+ */
717
+ extractDiffLineAnchorsFromSnapshot(snapshot) {
718
+ const normalized = (0, prompt_parser_1.normalizeSnapshot)(snapshot);
719
+ let oldStartLine;
720
+ let newStartLine;
721
+ for (const line of normalized.split('\n')) {
722
+ const match = line.match(/^\s*(\d+)\s+(.*)$/);
723
+ if (!match) {
724
+ continue;
725
+ }
726
+ const lineNumber = Number.parseInt(match[1], 10);
727
+ const diffText = match[2];
728
+ if (!Number.isFinite(lineNumber)) {
729
+ continue;
730
+ }
731
+ if (diffText.startsWith('-')) {
732
+ oldStartLine ?? (oldStartLine = lineNumber);
733
+ continue;
734
+ }
735
+ if (diffText.startsWith('+')) {
736
+ newStartLine ?? (newStartLine = lineNumber);
737
+ continue;
738
+ }
739
+ oldStartLine ?? (oldStartLine = lineNumber);
740
+ newStartLine ?? (newStartLine = lineNumber);
741
+ }
742
+ logger_1.logger.debug('Recovered diff line anchors from tmux snapshot', {
743
+ oldStartLine,
744
+ newStartLine,
745
+ snapshotPreview: this.summarizePromptSnapshot(snapshot),
746
+ });
747
+ return { oldStartLine, newStartLine };
748
+ }
749
+ /**
750
+ * Subscribe to mobile events for this session
751
+ */
752
+ subscribeToMobileEvents(sessionId) {
753
+ logger_1.logger.info('Subscribing to mobile events', { sessionId });
754
+ // Clean up existing subscription
755
+ if (this.unsubscribe) {
756
+ this.unsubscribe();
757
+ }
758
+ this.unsubscribe = this.appSyncClient.subscribeToEvents(sessionId, async (event) => {
759
+ await this.handleMobileEvent(event);
760
+ }, (error) => {
761
+ logger_1.logger.error('Subscription error:', error);
762
+ });
763
+ if (this.sessionState) {
764
+ this.sessionState.subscriptionActive = true;
765
+ }
766
+ logger_1.logger.info('Subscribed to mobile events');
767
+ }
768
+ /**
769
+ * Download an attachment from S3 and save it to a temp file
770
+ * Returns the local file path
771
+ * @param attachment The attachment to download
772
+ * @param sessionId The session ID for organizing temp files
773
+ * @param eventIsEncrypted Whether the parent event is encrypted (fallback for attachment.isEncrypted)
774
+ */
775
+ async downloadAttachment(attachment, sessionId, eventIsEncrypted) {
776
+ try {
777
+ // Use attachment.isEncrypted if available, otherwise fall back to event.isEncrypted
778
+ // This handles AppSync subscription limitation where nested object fields may be null
779
+ const shouldDecrypt = attachment.isEncrypted ?? eventIsEncrypted ?? false;
780
+ logger_1.logger.info('Downloading attachment', {
781
+ id: attachment.id,
782
+ type: attachment.type,
783
+ filename: attachment.filename,
784
+ s3Key: attachment.s3Key,
785
+ attachmentIsEncrypted: attachment.isEncrypted,
786
+ eventIsEncrypted,
787
+ shouldDecrypt,
788
+ });
789
+ // Get pre-signed download URL
790
+ const { downloadUrl } = await this.appSyncClient.getAttachmentDownloadUrl(attachment.s3Key);
791
+ // Download the file
792
+ const response = await fetch(downloadUrl);
793
+ if (!response.ok) {
794
+ throw new Error(`Failed to download attachment: ${response.status} ${response.statusText}`);
795
+ }
796
+ let buffer = Buffer.from(await response.arrayBuffer());
797
+ // Decrypt if encrypted
798
+ if (shouldDecrypt && this.sessionKey) {
799
+ try {
800
+ logger_1.logger.info('Decrypting attachment', { id: attachment.id });
801
+ buffer = codevibe_core_1.cryptoService.decryptData(buffer, this.sessionKey);
802
+ logger_1.logger.info('Attachment decrypted successfully', {
803
+ id: attachment.id,
804
+ decryptedSize: buffer.length,
805
+ });
806
+ }
807
+ catch (decryptError) {
808
+ logger_1.logger.error('Failed to decrypt attachment:', { id: attachment.id, error: decryptError });
809
+ throw new Error('Failed to decrypt attachment');
810
+ }
811
+ }
812
+ else if (shouldDecrypt && !this.sessionKey) {
813
+ logger_1.logger.warn('Cannot decrypt attachment - no session key available', { id: attachment.id });
814
+ // Continue with encrypted data - Codex CLI won't be able to view it properly
815
+ }
816
+ // Create temp directory for this session if it doesn't exist
817
+ const tempDir = path.join(os.tmpdir(), 'codevibe-codex', sessionId);
818
+ if (!fs.existsSync(tempDir)) {
819
+ fs.mkdirSync(tempDir, { recursive: true });
820
+ }
821
+ // Determine file extension from MIME type or filename
822
+ let extension = '';
823
+ if (attachment.filename) {
824
+ const ext = path.extname(attachment.filename);
825
+ if (ext)
826
+ extension = ext;
827
+ }
828
+ if (!extension) {
829
+ // Fallback to MIME type
830
+ const mimeToExt = {
831
+ 'image/jpeg': '.jpg',
832
+ 'image/png': '.png',
833
+ 'image/gif': '.gif',
834
+ 'image/webp': '.webp',
835
+ 'image/heic': '.heic',
836
+ 'application/pdf': '.pdf',
837
+ };
838
+ extension = mimeToExt[attachment.type] || '.bin';
839
+ }
840
+ // Save to temp file
841
+ const filename = `attachment-${attachment.id}${extension}`;
842
+ const filePath = path.join(tempDir, filename);
843
+ fs.writeFileSync(filePath, buffer);
844
+ logger_1.logger.info('Attachment saved to temp file', {
845
+ id: attachment.id,
846
+ filePath,
847
+ size: buffer.length,
848
+ });
849
+ return filePath;
850
+ }
851
+ catch (error) {
852
+ logger_1.logger.error('Failed to download attachment:', { id: attachment.id, error });
853
+ return null;
854
+ }
855
+ }
856
+ /**
857
+ * Handle event from mobile app
858
+ * If the event has attachments, downloads them and includes file paths in prompt
859
+ */
860
+ async handleMobileEvent(event) {
861
+ // Debug: log raw attachment data to diagnose isEncrypted issue
862
+ if (event.attachments && event.attachments.length > 0) {
863
+ logger_1.logger.info('DEBUG: Raw attachment data from subscription', {
864
+ attachments: JSON.stringify(event.attachments),
865
+ eventIsEncrypted: event.isEncrypted,
866
+ });
867
+ }
868
+ logger_1.logger.info('Received mobile event', {
869
+ eventId: event.eventId,
870
+ type: event.type,
871
+ content: event.content?.substring(0, 50),
872
+ attachmentCount: event.attachments?.length || 0,
873
+ isEncrypted: event.isEncrypted,
874
+ });
875
+ if (!this.sessionState) {
876
+ logger_1.logger.warn('Received mobile event but no active session');
877
+ return;
878
+ }
879
+ // Decrypt event if encrypted
880
+ let decryptedContent = event.content || '';
881
+ if (event.isEncrypted && this.sessionKey) {
882
+ try {
883
+ decryptedContent = codevibe_core_1.cryptoService.decryptContent(event.content, this.sessionKey);
884
+ logger_1.logger.debug('Event decrypted successfully', { eventId: event.eventId });
885
+ }
886
+ catch (error) {
887
+ logger_1.logger.error('Failed to decrypt event:', { eventId: event.eventId, error });
888
+ // Fall back to original content (might not work, but better than nothing)
889
+ decryptedContent = event.content;
890
+ }
891
+ }
892
+ // Update delivery status to DELIVERED
893
+ try {
894
+ await this.appSyncClient.updateEventStatus({
895
+ eventId: event.eventId,
896
+ sessionId: event.sessionId,
897
+ timestamp: event.timestamp,
898
+ deliveryStatus: codevibe_core_1.DeliveryStatus.DELIVERED,
899
+ });
900
+ }
901
+ catch (error) {
902
+ logger_1.logger.error('Failed to update delivery status:', error);
903
+ }
904
+ // Handle based on event type
905
+ if (event.type === codevibe_core_1.EventType.USER_PROMPT || event.type === codevibe_core_1.EventType.PROMPT_RESPONSE) {
906
+ let prompt = decryptedContent;
907
+ const attachments = event.attachments || [];
908
+ // Download attachments and build file paths to include in prompt
909
+ const attachmentPaths = [];
910
+ if (attachments.length > 0) {
911
+ logger_1.logger.info('Downloading attachments for prompt', { count: attachments.length });
912
+ for (const attachment of attachments) {
913
+ const filePath = await this.downloadAttachment(attachment, this.sessionState.sessionId, event.isEncrypted // Pass event encryption status as fallback
914
+ );
915
+ if (filePath) {
916
+ attachmentPaths.push(filePath);
917
+ }
918
+ }
919
+ // If we have downloaded files, prepend them to the prompt
920
+ if (attachmentPaths.length > 0) {
921
+ const fileReferences = attachmentPaths
922
+ .map(p => `[Attached file: ${p}]`)
923
+ .join('\n');
924
+ // Format: file references first, then the user's message
925
+ if (prompt) {
926
+ prompt = `${fileReferences}\n\n${prompt}`;
927
+ }
928
+ else {
929
+ // No text content, just file references with instruction
930
+ prompt = `${fileReferences}\n\nPlease analyze the attached file(s).`;
931
+ }
932
+ logger_1.logger.info('Prompt updated with attachment paths', {
933
+ attachmentCount: attachmentPaths.length,
934
+ newPromptLength: prompt.length,
935
+ });
936
+ }
937
+ }
938
+ const translated = this.translatePromptResponse(prompt);
939
+ const success = await this.promptResponder.sendInput(this.sessionState.sessionId, translated.primaryInput);
940
+ if (success && translated.followUpInput) {
941
+ await this.promptResponder.sendInput(this.sessionState.sessionId, translated.followUpInput);
942
+ }
943
+ if (success && this.pendingInteractivePrompt && event.type === codevibe_core_1.EventType.PROMPT_RESPONSE) {
944
+ this.pendingInteractivePrompt = null;
945
+ }
946
+ // Update status to EXECUTED if successful
947
+ if (success) {
948
+ try {
949
+ await this.appSyncClient.updateEventStatus({
950
+ eventId: event.eventId,
951
+ sessionId: event.sessionId,
952
+ timestamp: event.timestamp,
953
+ deliveryStatus: codevibe_core_1.DeliveryStatus.EXECUTED,
954
+ });
955
+ }
956
+ catch (error) {
957
+ logger_1.logger.error('Failed to update executed status:', error);
958
+ }
959
+ }
960
+ }
961
+ }
962
+ /**
963
+ * Generate a backend session ID from Codex's runtime session ID.
964
+ *
965
+ * This keeps concurrent Codex sessions isolated even when they share the same
966
+ * working directory. The tradeoff is that cross-process resume is no longer
967
+ * represented as a single backend session.
968
+ */
969
+ generateSessionId(codexSessionId) {
970
+ return `codex-${codexSessionId}`;
971
+ }
972
+ /**
973
+ * Mark the current session inactive and clean up local state.
974
+ */
975
+ async endActiveSession(reason) {
976
+ if (!this.sessionState) {
977
+ return;
978
+ }
979
+ logger_1.logger.info('Ending active session', {
980
+ sessionId: this.sessionState.sessionId,
981
+ codexSessionId: this.sessionState.codexSessionId,
982
+ reason,
983
+ });
984
+ // Stop heartbeat
985
+ this.appSyncClient.stopHeartbeat(this.sessionState.sessionId);
986
+ if (this.unsubscribe) {
987
+ this.unsubscribe();
988
+ this.unsubscribe = null;
989
+ }
990
+ await this.tmuxPaneObserver.stop();
991
+ this.pendingInteractivePrompt = null;
992
+ this.isInitializingSession = false;
993
+ this.bufferedLogEntries = [];
994
+ if (this.sessionKey) {
995
+ codevibe_core_1.keychainManager.clearSessionKey(this.sessionState.sessionId);
996
+ this.sessionKey = null;
997
+ }
998
+ try {
999
+ await this.appSyncClient.updateSession({
1000
+ sessionId: this.sessionState.sessionId,
1001
+ status: codevibe_core_1.SessionStatus.INACTIVE,
1002
+ });
1003
+ }
1004
+ catch (error) {
1005
+ logger_1.logger.error('Failed to update session status:', error);
1006
+ }
1007
+ this.sessionState = null;
1008
+ }
1009
+ /**
1010
+ * Stop the server
1011
+ */
1012
+ async stop() {
1013
+ logger_1.logger.info('Stopping CodeVibe Codex companion server');
1014
+ // Mark any active session inactive and clean up
1015
+ await this.endActiveSession('shutdown');
1016
+ // Stop watching session logs
1017
+ this.sessionWatcher.stop();
1018
+ // Clear approval detector
1019
+ this.approvalDetector.shutdown();
1020
+ // Clear pending calls
1021
+ (0, event_mapper_1.clearPendingCalls)();
1022
+ // Cleanup subscriptions
1023
+ this.appSyncClient.cleanupSubscriptions();
1024
+ logger_1.logger.info('CodeVibe Codex companion server stopped');
1025
+ }
1026
+ }
1027
+ // Main entry point
1028
+ const server = new CodexCompanionServer();
1029
+ // Handle graceful shutdown
1030
+ process.on('SIGINT', async () => {
1031
+ logger_1.logger.info('Received SIGINT, shutting down...');
1032
+ await server.stop();
1033
+ process.exit(0);
1034
+ });
1035
+ process.on('SIGTERM', async () => {
1036
+ logger_1.logger.info('Received SIGTERM, shutting down...');
1037
+ await server.stop();
1038
+ process.exit(0);
1039
+ });
1040
+ // Start the server
1041
+ server.start().catch((error) => {
1042
+ logger_1.logger.error('Failed to start server:', error);
1043
+ process.exit(1);
1044
+ });
1045
+ //# sourceMappingURL=server.js.map