@quantiya/codevibe-codex-plugin 1.0.6 → 1.0.8

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 (66) hide show
  1. package/README.md +59 -106
  2. package/dist/server.js +22 -1051
  3. package/package.json +10 -12
  4. package/dist/approval-detector.d.ts +0 -38
  5. package/dist/approval-detector.d.ts.map +0 -1
  6. package/dist/approval-detector.js +0 -174
  7. package/dist/approval-detector.js.map +0 -1
  8. package/dist/appsync-client.d.ts +0 -69
  9. package/dist/appsync-client.d.ts.map +0 -1
  10. package/dist/appsync-client.js +0 -937
  11. package/dist/appsync-client.js.map +0 -1
  12. package/dist/auth-cli.d.ts +0 -11
  13. package/dist/auth-cli.d.ts.map +0 -1
  14. package/dist/auth-cli.js +0 -241
  15. package/dist/auth-cli.js.map +0 -1
  16. package/dist/config.d.ts +0 -29
  17. package/dist/config.d.ts.map +0 -1
  18. package/dist/config.js +0 -116
  19. package/dist/config.js.map +0 -1
  20. package/dist/crypto-service.d.ts +0 -115
  21. package/dist/crypto-service.d.ts.map +0 -1
  22. package/dist/crypto-service.js +0 -278
  23. package/dist/crypto-service.js.map +0 -1
  24. package/dist/event-mapper.d.ts +0 -39
  25. package/dist/event-mapper.d.ts.map +0 -1
  26. package/dist/event-mapper.js +0 -302
  27. package/dist/event-mapper.js.map +0 -1
  28. package/dist/key-manager.d.ts +0 -87
  29. package/dist/key-manager.d.ts.map +0 -1
  30. package/dist/key-manager.js +0 -287
  31. package/dist/key-manager.js.map +0 -1
  32. package/dist/logger.d.ts +0 -2
  33. package/dist/logger.d.ts.map +0 -1
  34. package/dist/logger.js +0 -18
  35. package/dist/logger.js.map +0 -1
  36. package/dist/prompt-parser.d.ts +0 -3
  37. package/dist/prompt-parser.d.ts.map +0 -1
  38. package/dist/prompt-parser.js +0 -8
  39. package/dist/prompt-parser.js.map +0 -1
  40. package/dist/prompt-responder.d.ts +0 -18
  41. package/dist/prompt-responder.d.ts.map +0 -1
  42. package/dist/prompt-responder.js +0 -78
  43. package/dist/prompt-responder.js.map +0 -1
  44. package/dist/server.d.ts +0 -8
  45. package/dist/server.d.ts.map +0 -1
  46. package/dist/server.js.map +0 -1
  47. package/dist/session-id-cache.d.ts +0 -16
  48. package/dist/session-id-cache.d.ts.map +0 -1
  49. package/dist/session-id-cache.js +0 -90
  50. package/dist/session-id-cache.js.map +0 -1
  51. package/dist/session-log-watcher.d.ts +0 -61
  52. package/dist/session-log-watcher.d.ts.map +0 -1
  53. package/dist/session-log-watcher.js +0 -372
  54. package/dist/session-log-watcher.js.map +0 -1
  55. package/dist/tmux-pane-observer.d.ts +0 -39
  56. package/dist/tmux-pane-observer.d.ts.map +0 -1
  57. package/dist/tmux-pane-observer.js +0 -255
  58. package/dist/tmux-pane-observer.js.map +0 -1
  59. package/dist/token-storage.d.ts +0 -39
  60. package/dist/token-storage.d.ts.map +0 -1
  61. package/dist/token-storage.js +0 -169
  62. package/dist/token-storage.js.map +0 -1
  63. package/dist/types.d.ts +0 -158
  64. package/dist/types.d.ts.map +0 -1
  65. package/dist/types.js +0 -17
  66. package/dist/types.js.map +0 -1
package/dist/server.js CHANGED
@@ -1,1051 +1,22 @@
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
- // Wait briefly for JSONL log entry to be processed — tmux observer often
387
- // detects the prompt before the apply_patch log entry reaches the approval detector.
388
- let pendingCall = this.getMostRecentPendingToolCall();
389
- if (!pendingCall) {
390
- await new Promise((resolve) => setTimeout(resolve, 500));
391
- pendingCall = this.getMostRecentPendingToolCall();
392
- }
393
- const pendingCallContext = pendingCall ? this.buildApprovalPromptContextFromPendingCall(pendingCall) : null;
394
- const toolDetails = pendingCallContext
395
- ? this.buildToolDetailsForInteractivePrompt(pendingCallContext, snapshot)
396
- : {};
397
- const toolNameForMetadata = toolDetails.tool_name || this.mapToolNameForApproval(pendingCall?.name);
398
- const toolInputForMetadata = toolDetails.tool_input || (pendingCallContext ? this.buildFallbackToolInput(pendingCallContext) : undefined);
399
- const hasDetails = !!(toolNameForMetadata && toolInputForMetadata);
400
- const promptId = this.pendingInteractivePrompt?.callId || pendingCall?.callId || (0, uuid_1.v4)();
401
- this.pendingInteractivePrompt = {
402
- promptId,
403
- callId: this.pendingInteractivePrompt?.callId || pendingCall?.callId,
404
- kind: promptPresentation.kind,
405
- options: promptPresentation.options,
406
- submitMap: promptPresentation.submitMap,
407
- promptText: promptPresentation.promptText,
408
- createdAt: Date.now(),
409
- source: 'tmux',
410
- requiresFollowUpText: promptPresentation.requiresFollowUpText,
411
- };
412
- let metadata = {
413
- options: promptPresentation.options,
414
- instructions: promptPresentation.instructions,
415
- prompt_source: 'tmux_live',
416
- tool_name: toolNameForMetadata,
417
- tool_input: toolInputForMetadata,
418
- has_details: hasDetails,
419
- };
420
- let content = promptPresentation.content;
421
- let isEncrypted = false;
422
- if (this.sessionKey) {
423
- content = codevibe_core_1.cryptoService.encryptContent(content, this.sessionKey);
424
- const encryptedMetadata = codevibe_core_1.cryptoService.encryptMetadata(metadata, this.sessionKey);
425
- metadata = { encrypted: encryptedMetadata };
426
- isEncrypted = true;
427
- }
428
- try {
429
- await this.appSyncClient.createEvent({
430
- sessionId: this.sessionState.sessionId,
431
- type: codevibe_core_1.EventType.INTERACTIVE_PROMPT,
432
- source: codevibe_core_1.EventSource.DESKTOP,
433
- content,
434
- metadata,
435
- promptId,
436
- ...(isEncrypted ? { isEncrypted: true } : {}),
437
- });
438
- logger_1.logger.info('Sent tmux-detected interactive prompt', {
439
- sessionId: this.sessionState.sessionId,
440
- promptText: parsedPrompt.promptText,
441
- kind: parsedPrompt.kind,
442
- });
443
- }
444
- catch (error) {
445
- logger_1.logger.error('Failed to send tmux-detected interactive prompt', { error });
446
- }
447
- }
448
- async startTmuxObserver() {
449
- const tmuxSession = process.env.CODEVIBE_CODEX_TMUX_SESSION;
450
- if (!tmuxSession) {
451
- logger_1.logger.debug('Skipping tmux pane observer start - no tmux session in environment');
452
- return;
453
- }
454
- try {
455
- await this.tmuxPaneObserver.start(tmuxSession);
456
- }
457
- catch (error) {
458
- logger_1.logger.warn('Failed to start tmux pane observer', { tmuxSession, error });
459
- }
460
- }
461
- async tryParseInteractivePromptFromTmux() {
462
- try {
463
- const snapshot = await this.tmuxPaneObserver.captureSnapshot();
464
- const parsed = (0, prompt_parser_1.parseInteractivePrompt)(snapshot);
465
- logger_1.logger.debug('tmux prompt parse result', {
466
- parsed: !!parsed,
467
- kind: parsed?.kind,
468
- promptText: parsed?.promptText,
469
- snapshotPreview: this.summarizePromptSnapshot(snapshot),
470
- });
471
- return {
472
- parsedPrompt: parsed,
473
- snapshot,
474
- };
475
- }
476
- catch (error) {
477
- logger_1.logger.debug('tmux prompt parsing unavailable', { error });
478
- return null;
479
- }
480
- }
481
- buildPromptPresentation(parsedPrompt) {
482
- if (parsedPrompt) {
483
- return {
484
- content: parsedPrompt.promptText,
485
- promptText: parsedPrompt.promptText,
486
- kind: parsedPrompt.kind,
487
- options: parsedPrompt.options,
488
- submitMap: parsedPrompt.submitMap,
489
- instructions: this.buildPromptInstructions(parsedPrompt),
490
- requiresFollowUpText: parsedPrompt.requiresFollowUpText,
491
- };
492
- }
493
- return {
494
- content: 'Codex is waiting for approval.',
495
- promptText: 'Codex is waiting for approval.',
496
- kind: 'yes_no',
497
- options: [
498
- { number: '1', text: 'Yes (sends "y")' },
499
- { number: '2', text: 'No, tell Codex what to change (sends "n <instructions>")' },
500
- ],
501
- submitMap: {
502
- '1': 'y',
503
- '2': 'n',
504
- },
505
- instructions: 'Reply with 1 to approve, or 2 followed by what to change',
506
- requiresFollowUpText: true,
507
- };
508
- }
509
- getMostRecentPendingToolCall() {
510
- const pendingCalls = this.approvalDetector.getPendingCalls();
511
- if (pendingCalls.length === 0) {
512
- return null;
513
- }
514
- return pendingCalls.reduce((latest, current) => {
515
- return current.timestamp > latest.timestamp ? current : latest;
516
- });
517
- }
518
- buildApprovalPromptContextFromPendingCall(pendingCall) {
519
- return {
520
- toolName: pendingCall.name,
521
- filePath: pendingCall.filePath,
522
- diff: pendingCall.diff,
523
- toolInput: pendingCall.parsedInput,
524
- rawInput: pendingCall.input,
525
- hint: pendingCall.filePath ? `File: ${pendingCall.filePath}` : `Tool: ${this.mapToolNameForApproval(pendingCall.name) || pendingCall.name}`,
526
- };
527
- }
528
- buildPromptInstructions(parsedPrompt) {
529
- if (parsedPrompt.kind === 'yes_no' && parsedPrompt.requiresFollowUpText) {
530
- return 'Reply with 1 to approve, or 2 followed by what to change';
531
- }
532
- if (parsedPrompt.kind === 'yes_no') {
533
- return 'Reply with 1 for yes or 2 for no';
534
- }
535
- if (parsedPrompt.kind === 'numbered') {
536
- return 'Reply with the number of the option you want';
537
- }
538
- return 'Reply with your response';
539
- }
540
- summarizePromptSnapshot(snapshot) {
541
- return snapshot
542
- .split('\n')
543
- .map((line) => line.trimEnd())
544
- .filter((line) => line.length > 0)
545
- .slice(-12)
546
- .map((line) => line.slice(0, 160))
547
- .join('\n');
548
- }
549
- translatePromptResponse(input) {
550
- const prompt = this.pendingInteractivePrompt;
551
- if (!prompt) {
552
- return { primaryInput: input };
553
- }
554
- const trimmed = input.trim();
555
- const match = trimmed.match(/^(\d+)(?:[,.:;\-\s]+([\s\S]+))?$/);
556
- if (!match) {
557
- return { primaryInput: input };
558
- }
559
- const selected = match[1];
560
- const followUpText = match[2]?.trim();
561
- const mapped = prompt.submitMap[selected];
562
- if (!mapped) {
563
- return { primaryInput: input };
564
- }
565
- if (prompt.requiresFollowUpText && followUpText) {
566
- return {
567
- primaryInput: mapped,
568
- followUpInput: followUpText,
569
- };
570
- }
571
- return { primaryInput: mapped };
572
- }
573
- /**
574
- * Build tool metadata for INTERACTIVE_PROMPT so iOS can render previews/diffs.
575
- * The mobile app expects metadata JSON with tool_name/tool_input (same shape as TOOL_USE content).
576
- */
577
- buildToolDetailsForInteractivePrompt(data, snapshot) {
578
- const toolName = data.toolName;
579
- const parsedInput = data.toolInput && typeof data.toolInput === 'object' ? data.toolInput : undefined;
580
- // apply_patch → expose an Edit payload with extracted old/new buffers for iOS diff rendering.
581
- if (toolName === 'apply_patch') {
582
- const content = data.diff || data.rawInput;
583
- if (content) {
584
- const { oldString, newString, oldStartLine, newStartLine } = this.extractOldNewFromPatch(content);
585
- const snapshotLineAnchors = snapshot ? this.extractDiffLineAnchorsFromSnapshot(snapshot) : {};
586
- return {
587
- tool_name: 'Edit',
588
- tool_input: {
589
- file_path: data.filePath,
590
- content,
591
- diff: data.diff,
592
- raw_patch: data.rawInput,
593
- old_string: oldString,
594
- new_string: newString,
595
- old_start_line: oldStartLine ?? snapshotLineAnchors.oldStartLine,
596
- new_start_line: newStartLine ?? snapshotLineAnchors.newStartLine,
597
- },
598
- };
599
- }
600
- }
601
- // shell_command / shell → treat as Bash so iOS shows command preview
602
- if (toolName === 'shell_command' || toolName === 'shell') {
603
- const command = parsedInput?.command || data.rawInput || data.hint;
604
- if (command) {
605
- return {
606
- tool_name: 'Bash',
607
- tool_input: {
608
- command,
609
- output: parsedInput?.output,
610
- },
611
- };
612
- }
613
- }
614
- // Fallback: include whatever context we have
615
- const fallbackInput = {};
616
- if (data.filePath)
617
- fallbackInput.file_path = data.filePath;
618
- if (data.diff)
619
- fallbackInput.diff = data.diff;
620
- if (data.rawInput)
621
- fallbackInput.raw_input = data.rawInput;
622
- if (Object.keys(fallbackInput).length > 0) {
623
- return {
624
- tool_name: toolName || 'Tool',
625
- tool_input: fallbackInput,
626
- };
627
- }
628
- return {};
629
- }
630
- /**
631
- * Fallback tool input for interactive prompts (keeps file path + diff/raw input).
632
- */
633
- buildFallbackToolInput(data) {
634
- const input = {};
635
- if (data.filePath)
636
- input.file_path = data.filePath;
637
- if (data.diff)
638
- input.diff = data.diff;
639
- if (data.rawInput)
640
- input.raw_input = data.rawInput;
641
- if (data.toolInput && typeof data.toolInput === 'object') {
642
- input.parsed_input = data.toolInput;
643
- }
644
- return Object.keys(input).length > 0 ? input : undefined;
645
- }
646
- /**
647
- * Map Codex tool names to mobile-friendly labels for approval prompts.
648
- */
649
- mapToolNameForApproval(name) {
650
- if (!name)
651
- return undefined;
652
- const mapping = {
653
- apply_patch: 'Edit',
654
- shell_command: 'Bash',
655
- shell: 'Bash',
656
- };
657
- return mapping[name] || name;
658
- }
659
- /**
660
- * Extract old/new buffers from an apply_patch diff so the iOS diff UI can render.
661
- * Includes hunk context lines and preserves start-line metadata for single-hunk patches.
662
- */
663
- extractOldNewFromPatch(patch) {
664
- const oldLines = [];
665
- const newLines = [];
666
- const hunkHeaderPattern = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
667
- let hunkCount = 0;
668
- let currentOldLine = 0;
669
- let currentNewLine = 0;
670
- let oldStartLine;
671
- let newStartLine;
672
- for (const line of patch.split('\n')) {
673
- const hunkMatch = line.match(hunkHeaderPattern);
674
- if (hunkMatch) {
675
- hunkCount += 1;
676
- currentOldLine = Number.parseInt(hunkMatch[1], 10);
677
- currentNewLine = Number.parseInt(hunkMatch[2], 10);
678
- continue;
679
- }
680
- if (line.startsWith('***') ||
681
- line.startsWith('---') ||
682
- line.startsWith('+++') ||
683
- line.startsWith('*** End Patch')) {
684
- continue;
685
- }
686
- if (line.startsWith('-')) {
687
- if (oldStartLine === undefined) {
688
- oldStartLine = currentOldLine;
689
- }
690
- oldLines.push(line.slice(1));
691
- currentOldLine += 1;
692
- }
693
- else if (line.startsWith('+')) {
694
- if (newStartLine === undefined) {
695
- newStartLine = currentNewLine;
696
- }
697
- newLines.push(line.slice(1));
698
- currentNewLine += 1;
699
- }
700
- else if (line.startsWith(' ')) {
701
- const contextLine = line.slice(1);
702
- oldLines.push(contextLine);
703
- newLines.push(contextLine);
704
- currentOldLine += 1;
705
- currentNewLine += 1;
706
- }
707
- }
708
- // Use empty string instead of undefined so keys are always present in JSON
709
- // This ensures iOS can detect the diff (checks for nil, not empty string)
710
- return {
711
- oldString: oldLines.join('\n'), // Empty string if no old lines (new file)
712
- newString: newLines.join('\n'), // Empty string if no new lines (deleted file)
713
- // Only attach exact start lines when the patch has a single hunk.
714
- oldStartLine: hunkCount === 1 ? oldStartLine : undefined,
715
- newStartLine: hunkCount === 1 ? newStartLine : undefined,
716
- };
717
- }
718
- /**
719
- * Codex's apply_patch payload often uses bare "@@" markers without absolute
720
- * hunk coordinates. When that happens, recover visible line anchors from the
721
- * tmux-rendered diff preview instead.
722
- */
723
- extractDiffLineAnchorsFromSnapshot(snapshot) {
724
- const normalized = (0, prompt_parser_1.normalizeSnapshot)(snapshot);
725
- let oldStartLine;
726
- let newStartLine;
727
- for (const line of normalized.split('\n')) {
728
- const match = line.match(/^\s*(\d+)\s+(.*)$/);
729
- if (!match) {
730
- continue;
731
- }
732
- const lineNumber = Number.parseInt(match[1], 10);
733
- const diffText = match[2];
734
- if (!Number.isFinite(lineNumber)) {
735
- continue;
736
- }
737
- if (diffText.startsWith('-')) {
738
- oldStartLine ?? (oldStartLine = lineNumber);
739
- continue;
740
- }
741
- if (diffText.startsWith('+')) {
742
- newStartLine ?? (newStartLine = lineNumber);
743
- continue;
744
- }
745
- oldStartLine ?? (oldStartLine = lineNumber);
746
- newStartLine ?? (newStartLine = lineNumber);
747
- }
748
- logger_1.logger.debug('Recovered diff line anchors from tmux snapshot', {
749
- oldStartLine,
750
- newStartLine,
751
- snapshotPreview: this.summarizePromptSnapshot(snapshot),
752
- });
753
- return { oldStartLine, newStartLine };
754
- }
755
- /**
756
- * Subscribe to mobile events for this session
757
- */
758
- subscribeToMobileEvents(sessionId) {
759
- logger_1.logger.info('Subscribing to mobile events', { sessionId });
760
- // Clean up existing subscription
761
- if (this.unsubscribe) {
762
- this.unsubscribe();
763
- }
764
- this.unsubscribe = this.appSyncClient.subscribeToEvents(sessionId, async (event) => {
765
- await this.handleMobileEvent(event);
766
- }, (error) => {
767
- logger_1.logger.error('Subscription error:', error);
768
- });
769
- if (this.sessionState) {
770
- this.sessionState.subscriptionActive = true;
771
- }
772
- logger_1.logger.info('Subscribed to mobile events');
773
- }
774
- /**
775
- * Download an attachment from S3 and save it to a temp file
776
- * Returns the local file path
777
- * @param attachment The attachment to download
778
- * @param sessionId The session ID for organizing temp files
779
- * @param eventIsEncrypted Whether the parent event is encrypted (fallback for attachment.isEncrypted)
780
- */
781
- async downloadAttachment(attachment, sessionId, eventIsEncrypted) {
782
- try {
783
- // Use attachment.isEncrypted if available, otherwise fall back to event.isEncrypted
784
- // This handles AppSync subscription limitation where nested object fields may be null
785
- const shouldDecrypt = attachment.isEncrypted ?? eventIsEncrypted ?? false;
786
- logger_1.logger.info('Downloading attachment', {
787
- id: attachment.id,
788
- type: attachment.type,
789
- filename: attachment.filename,
790
- s3Key: attachment.s3Key,
791
- attachmentIsEncrypted: attachment.isEncrypted,
792
- eventIsEncrypted,
793
- shouldDecrypt,
794
- });
795
- // Get pre-signed download URL
796
- const { downloadUrl } = await this.appSyncClient.getAttachmentDownloadUrl(attachment.s3Key);
797
- // Download the file
798
- const response = await fetch(downloadUrl);
799
- if (!response.ok) {
800
- throw new Error(`Failed to download attachment: ${response.status} ${response.statusText}`);
801
- }
802
- let buffer = Buffer.from(await response.arrayBuffer());
803
- // Decrypt if encrypted
804
- if (shouldDecrypt && this.sessionKey) {
805
- try {
806
- logger_1.logger.info('Decrypting attachment', { id: attachment.id });
807
- buffer = codevibe_core_1.cryptoService.decryptData(buffer, this.sessionKey);
808
- logger_1.logger.info('Attachment decrypted successfully', {
809
- id: attachment.id,
810
- decryptedSize: buffer.length,
811
- });
812
- }
813
- catch (decryptError) {
814
- logger_1.logger.error('Failed to decrypt attachment:', { id: attachment.id, error: decryptError });
815
- throw new Error('Failed to decrypt attachment');
816
- }
817
- }
818
- else if (shouldDecrypt && !this.sessionKey) {
819
- logger_1.logger.warn('Cannot decrypt attachment - no session key available', { id: attachment.id });
820
- // Continue with encrypted data - Codex CLI won't be able to view it properly
821
- }
822
- // Create temp directory for this session if it doesn't exist
823
- const tempDir = path.join(os.tmpdir(), 'codevibe-codex', sessionId);
824
- if (!fs.existsSync(tempDir)) {
825
- fs.mkdirSync(tempDir, { recursive: true });
826
- }
827
- // Determine file extension from MIME type or filename
828
- let extension = '';
829
- if (attachment.filename) {
830
- const ext = path.extname(attachment.filename);
831
- if (ext)
832
- extension = ext;
833
- }
834
- if (!extension) {
835
- // Fallback to MIME type
836
- const mimeToExt = {
837
- 'image/jpeg': '.jpg',
838
- 'image/png': '.png',
839
- 'image/gif': '.gif',
840
- 'image/webp': '.webp',
841
- 'image/heic': '.heic',
842
- 'application/pdf': '.pdf',
843
- };
844
- extension = mimeToExt[attachment.type] || '.bin';
845
- }
846
- // Save to temp file
847
- const filename = `attachment-${attachment.id}${extension}`;
848
- const filePath = path.join(tempDir, filename);
849
- fs.writeFileSync(filePath, buffer);
850
- logger_1.logger.info('Attachment saved to temp file', {
851
- id: attachment.id,
852
- filePath,
853
- size: buffer.length,
854
- });
855
- return filePath;
856
- }
857
- catch (error) {
858
- logger_1.logger.error('Failed to download attachment:', { id: attachment.id, error });
859
- return null;
860
- }
861
- }
862
- /**
863
- * Handle event from mobile app
864
- * If the event has attachments, downloads them and includes file paths in prompt
865
- */
866
- async handleMobileEvent(event) {
867
- // Debug: log raw attachment data to diagnose isEncrypted issue
868
- if (event.attachments && event.attachments.length > 0) {
869
- logger_1.logger.info('DEBUG: Raw attachment data from subscription', {
870
- attachments: JSON.stringify(event.attachments),
871
- eventIsEncrypted: event.isEncrypted,
872
- });
873
- }
874
- logger_1.logger.info('Received mobile event', {
875
- eventId: event.eventId,
876
- type: event.type,
877
- content: event.content?.substring(0, 50),
878
- attachmentCount: event.attachments?.length || 0,
879
- isEncrypted: event.isEncrypted,
880
- });
881
- if (!this.sessionState) {
882
- logger_1.logger.warn('Received mobile event but no active session');
883
- return;
884
- }
885
- // Decrypt event if encrypted
886
- let decryptedContent = event.content || '';
887
- if (event.isEncrypted && this.sessionKey) {
888
- try {
889
- decryptedContent = codevibe_core_1.cryptoService.decryptContent(event.content, this.sessionKey);
890
- logger_1.logger.debug('Event decrypted successfully', { eventId: event.eventId });
891
- }
892
- catch (error) {
893
- logger_1.logger.error('Failed to decrypt event:', { eventId: event.eventId, error });
894
- // Fall back to original content (might not work, but better than nothing)
895
- decryptedContent = event.content;
896
- }
897
- }
898
- // Update delivery status to DELIVERED
899
- try {
900
- await this.appSyncClient.updateEventStatus({
901
- eventId: event.eventId,
902
- sessionId: event.sessionId,
903
- timestamp: event.timestamp,
904
- deliveryStatus: codevibe_core_1.DeliveryStatus.DELIVERED,
905
- });
906
- }
907
- catch (error) {
908
- logger_1.logger.error('Failed to update delivery status:', error);
909
- }
910
- // Handle based on event type
911
- if (event.type === codevibe_core_1.EventType.USER_PROMPT || event.type === codevibe_core_1.EventType.PROMPT_RESPONSE) {
912
- let prompt = decryptedContent;
913
- const attachments = event.attachments || [];
914
- // Download attachments and build file paths to include in prompt
915
- const attachmentPaths = [];
916
- if (attachments.length > 0) {
917
- logger_1.logger.info('Downloading attachments for prompt', { count: attachments.length });
918
- for (const attachment of attachments) {
919
- const filePath = await this.downloadAttachment(attachment, this.sessionState.sessionId, event.isEncrypted // Pass event encryption status as fallback
920
- );
921
- if (filePath) {
922
- attachmentPaths.push(filePath);
923
- }
924
- }
925
- // If we have downloaded files, prepend them to the prompt
926
- if (attachmentPaths.length > 0) {
927
- const fileReferences = attachmentPaths
928
- .map(p => `[Attached file: ${p}]`)
929
- .join('\n');
930
- // Format: file references first, then the user's message
931
- if (prompt) {
932
- prompt = `${fileReferences}\n\n${prompt}`;
933
- }
934
- else {
935
- // No text content, just file references with instruction
936
- prompt = `${fileReferences}\n\nPlease analyze the attached file(s).`;
937
- }
938
- logger_1.logger.info('Prompt updated with attachment paths', {
939
- attachmentCount: attachmentPaths.length,
940
- newPromptLength: prompt.length,
941
- });
942
- }
943
- }
944
- const translated = this.translatePromptResponse(prompt);
945
- const success = await this.promptResponder.sendInput(this.sessionState.sessionId, translated.primaryInput);
946
- if (success && translated.followUpInput) {
947
- await this.promptResponder.sendInput(this.sessionState.sessionId, translated.followUpInput);
948
- }
949
- if (success && this.pendingInteractivePrompt && event.type === codevibe_core_1.EventType.PROMPT_RESPONSE) {
950
- this.pendingInteractivePrompt = null;
951
- }
952
- // Update status to EXECUTED if successful
953
- if (success) {
954
- try {
955
- await this.appSyncClient.updateEventStatus({
956
- eventId: event.eventId,
957
- sessionId: event.sessionId,
958
- timestamp: event.timestamp,
959
- deliveryStatus: codevibe_core_1.DeliveryStatus.EXECUTED,
960
- });
961
- }
962
- catch (error) {
963
- logger_1.logger.error('Failed to update executed status:', error);
964
- }
965
- }
966
- }
967
- }
968
- /**
969
- * Generate a backend session ID from Codex's runtime session ID.
970
- *
971
- * This keeps concurrent Codex sessions isolated even when they share the same
972
- * working directory. The tradeoff is that cross-process resume is no longer
973
- * represented as a single backend session.
974
- */
975
- generateSessionId(codexSessionId) {
976
- return `codex-${codexSessionId}`;
977
- }
978
- /**
979
- * Mark the current session inactive and clean up local state.
980
- */
981
- async endActiveSession(reason) {
982
- if (!this.sessionState) {
983
- return;
984
- }
985
- logger_1.logger.info('Ending active session', {
986
- sessionId: this.sessionState.sessionId,
987
- codexSessionId: this.sessionState.codexSessionId,
988
- reason,
989
- });
990
- // Stop heartbeat
991
- this.appSyncClient.stopHeartbeat(this.sessionState.sessionId);
992
- if (this.unsubscribe) {
993
- this.unsubscribe();
994
- this.unsubscribe = null;
995
- }
996
- await this.tmuxPaneObserver.stop();
997
- this.pendingInteractivePrompt = null;
998
- this.isInitializingSession = false;
999
- this.bufferedLogEntries = [];
1000
- if (this.sessionKey) {
1001
- codevibe_core_1.keychainManager.clearSessionKey(this.sessionState.sessionId);
1002
- this.sessionKey = null;
1003
- }
1004
- try {
1005
- await this.appSyncClient.updateSession({
1006
- sessionId: this.sessionState.sessionId,
1007
- status: codevibe_core_1.SessionStatus.INACTIVE,
1008
- });
1009
- }
1010
- catch (error) {
1011
- logger_1.logger.error('Failed to update session status:', error);
1012
- }
1013
- this.sessionState = null;
1014
- }
1015
- /**
1016
- * Stop the server
1017
- */
1018
- async stop() {
1019
- logger_1.logger.info('Stopping CodeVibe Codex companion server');
1020
- // Mark any active session inactive and clean up
1021
- await this.endActiveSession('shutdown');
1022
- // Stop watching session logs
1023
- this.sessionWatcher.stop();
1024
- // Clear approval detector
1025
- this.approvalDetector.shutdown();
1026
- // Clear pending calls
1027
- (0, event_mapper_1.clearPendingCalls)();
1028
- // Cleanup subscriptions
1029
- this.appSyncClient.cleanupSubscriptions();
1030
- logger_1.logger.info('CodeVibe Codex companion server stopped');
1031
- }
1032
- }
1033
- // Main entry point
1034
- const server = new CodexCompanionServer();
1035
- // Handle graceful shutdown
1036
- process.on('SIGINT', async () => {
1037
- logger_1.logger.info('Received SIGINT, shutting down...');
1038
- await server.stop();
1039
- process.exit(0);
1040
- });
1041
- process.on('SIGTERM', async () => {
1042
- logger_1.logger.info('Received SIGTERM, shutting down...');
1043
- await server.stop();
1044
- process.exit(0);
1045
- });
1046
- // Start the server
1047
- server.start().catch((error) => {
1048
- logger_1.logger.error('Failed to start server:', error);
1049
- process.exit(1);
1050
- });
1051
- //# sourceMappingURL=server.js.map
1
+ "use strict";var st=Object.create;var k=Object.defineProperty;var rt=Object.getOwnPropertyDescriptor;var ot=Object.getOwnPropertyNames;var at=Object.getPrototypeOf,lt=Object.prototype.hasOwnProperty;var pt=(p,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of ot(t))!lt.call(p,n)&&n!==e&&k(p,n,{get:()=>t[n],enumerable:!(i=rt(t,n))||i.enumerable});return p};var y=(p,t,e)=>(e=p!=null?st(at(p)):{},pt(t||!p||!p.__esModule?k(e,"default",{value:p,enumerable:!0}):e,p));var it=require("uuid"),b=y(require("fs")),x=y(require("path")),nt=y(require("os")),c=require("@quantiya/codevibe-core");var O=y(require("os")),$=y(require("path")),W=require("@quantiya/codevibe-core"),s=(0,W.createLogger)({name:"codevibe-codex",logFile:$.default.join(O.default.tmpdir(),"codevibe-codex-mcp.log"),level:"debug"});var U=require("events"),g=y(require("fs")),C=y(require("path")),K=y(require("readline")),z=require("chokidar"),j=require("@quantiya/codevibe-core");var T=class extends U.EventEmitter{constructor(){super();this.watcher=null;this.filePositions=new Map;this.activeLogFile=null;this.sessionId=null;this.isWatching=!1;this.startTime=0;this.sessionsDir=null}start(){if(this.isWatching){s.warn("Session log watcher already running");return}let e=(0,j.getConfig)().codex.sessionsDir;this.sessionsDir=e,s.info("Starting Codex session log watcher",{sessionsDir:e}),g.existsSync(e)||(s.info("Codex sessions directory does not exist yet, creating...",{sessionsDir:e}),g.mkdirSync(e,{recursive:!0})),this.startTime=Date.now(),this.watcher=(0,z.watch)(e,{persistent:!0,ignoreInitial:!0,awaitWriteFinish:{stabilityThreshold:100,pollInterval:50},depth:4,ignored:i=>{let n=C.basename(i);return g.existsSync(i)&&g.statSync(i).isDirectory()?!1:!n.startsWith("rollout-")||!n.endsWith(".jsonl")}}),this.watcher.on("add",i=>{i.endsWith(".jsonl")&&this.onFileAdded(i)}),this.watcher.on("change",i=>{i.endsWith(".jsonl")&&this.onFileChanged(i)}),this.watcher.on("error",i=>{s.error("Watcher error:",i),this.emit("error",i)}),this.watcher.on("ready",()=>{s.info("Session log watcher ready"),this.bindRecentSessionFile()}),this.isWatching=!0,s.info("Session log watcher started")}stop(){this.watcher&&(this.watcher.close(),this.watcher=null),this.isWatching=!1,this.filePositions.clear(),this.activeLogFile=null,this.sessionId=null,this.sessionsDir=null,s.info("Session log watcher stopped")}getSessionId(){return this.sessionId}getActiveLogFile(){return this.activeLogFile}onFileAdded(e){try{let i=g.statSync(e),n=i.birthtimeMs||i.ctimeMs;if(n<this.startTime-5e3){s.debug("Ignoring old session file",{filePath:e,fileCreatedAt:new Date(n).toISOString(),startTime:new Date(this.startTime).toISOString()});return}}catch(i){s.warn("Could not check file creation time",{filePath:e,error:i})}if(this.activeLogFile){s.debug("Ignoring additional Codex session log for this server instance",{activeLogFile:this.activeLogFile,ignoredFile:e});return}s.info("New Codex session log detected",{filePath:e}),this.activeLogFile=e,this.filePositions.set(e,0),this.readNewLines(e)}onFileChanged(e){if(this.activeLogFile&&e!==this.activeLogFile){s.debug("Ignoring change for non-active session log",{activeLogFile:this.activeLogFile,ignoredFile:e});return}if(!this.activeLogFile){try{let i=g.statSync(e),n=i.birthtimeMs||i.ctimeMs,r=i.mtimeMs;if(n<this.startTime-5e3&&r<this.startTime-5e3){s.debug("Ignoring change for pre-existing session file",{filePath:e,fileCreatedAt:new Date(n).toISOString(),fileModifiedAt:new Date(r).toISOString(),startTime:new Date(this.startTime).toISOString()});return}}catch(i){s.warn("Could not check file creation time during change event",{filePath:e,error:i})}this.activeLogFile=e,this.filePositions.set(e,0)}this.readNewLines(e)}bindRecentSessionFile(){if(!(this.activeLogFile||!this.sessionsDir))try{let i=this.collectRecentSessionFiles(this.sessionsDir).sort((n,r)=>r.modifiedAt-n.modifiedAt)[0];if(!i)return;s.info("Binding recent session file missed during initial watch scan",{filePath:i.filePath,fileCreatedAt:new Date(i.createdAt).toISOString(),fileModifiedAt:new Date(i.modifiedAt).toISOString(),watcherStartTime:new Date(this.startTime).toISOString()}),this.activeLogFile=i.filePath,this.filePositions.set(i.filePath,0),this.readNewLines(i.filePath)}catch(e){s.warn("Failed to backfill recent session file",{error:e})}}collectRecentSessionFiles(e){let i=[],n=[e];for(;n.length>0;){let r=n.pop();if(!r)continue;let o;try{o=g.readdirSync(r,{withFileTypes:!0})}catch{continue}for(let a of o){let l=C.join(r,a.name);if(a.isDirectory()){n.push(l);continue}if(!(!a.isFile()||!a.name.startsWith("rollout-")||!a.name.endsWith(".jsonl")))try{let d=g.statSync(l),u=d.birthtimeMs||d.ctimeMs,h=d.mtimeMs;(u>=this.startTime||h>=this.startTime-5e3)&&i.push({filePath:l,createdAt:u,modifiedAt:h})}catch{}}}return i}async readNewLines(e){let i=this.filePositions.get(e)||0;try{if(g.statSync(e).size<=i)return;let r=g.createReadStream(e,{start:i,encoding:"utf-8"}),o=K.createInterface({input:r,crlfDelay:1/0}),a=i;for await(let l of o)if(a+=Buffer.byteLength(l,"utf-8")+1,!!l.trim())try{let d=JSON.parse(l);this.processLogEntry(d)}catch(d){s.warn("Failed to parse log line",{filePath:e,line:l.substring(0,100),error:d})}this.filePositions.set(e,a)}catch(n){s.error("Error reading log file",{filePath:e,error:n}),this.emit("error",n)}}processLogEntry(e){if(s.debug("Processing log entry",{type:e.type}),e.type==="session_meta"){let i=e.payload;this.sessionId=i.id,s.info("Codex session started",{sessionId:i.id,cwd:i.cwd,cliVersion:i.cli_version}),this.emit("session-started",i);return}this.emit("log-entry",e),e.type==="event_msg"&&e.payload?.type?this.emit(`event:${e.payload.type}`,e):e.type==="response_item"&&e.payload?.type&&this.emit(`response:${e.payload.type}`,e)}};var A=require("uuid"),v=require("@quantiya/codevibe-core");var S=new Map;function B(p,t){let e={sessionId:t,source:v.EventSource.DESKTOP};if(p.type==="event_msg"&&p.payload){let i=p.payload.type;switch(i){case"user_message":return{...e,type:v.EventType.USER_PROMPT,content:p.payload.message||"",metadata:{images:p.payload.images||[]}};case"agent_message":return{...e,type:v.EventType.ASSISTANT_RESPONSE,content:p.payload.message||""};case"agent_reasoning":return{...e,type:v.EventType.REASONING,content:p.payload.text||""};case"token_count":return s.debug("Skipping token_count entry"),null;default:return s.debug("Unknown event_msg type",{type:i}),null}}if(p.type==="response_item"&&p.payload){let i=p.payload.type;if(i==="function_call"){let{name:n,arguments:r,call_id:o}=p.payload,a={};try{a=JSON.parse(r||"{}")}catch{a={raw:r}}let l=(0,A.v4)();S.set(o,{name:n,input:r,eventId:l});let d=D(n),u=ct(n,a);return{...e,type:v.EventType.TOOL_USE,content:u,metadata:{toolName:d,toolInput:a,callId:o,status:"running"}}}if(i==="function_call_output"){let{call_id:n,output:r}=p.payload,o=S.get(n);S.delete(n);let a=o?.name?D(o.name):"Tool",l=ht(r,500);return{...e,type:v.EventType.TOOL_USE,content:`${a} completed:
2
+ ${l}`,metadata:{toolName:a,toolOutput:r,callId:n,status:"completed"}}}if(i==="custom_tool_call"){let{name:n,call_id:r,input:o,status:a}=p.payload;S.set(r,{name:n,input:o,eventId:(0,A.v4)()});let l=ut(o),{oldString:d,newString:u}=dt(o),h=l?`Editing: ${l.filePath}`:"Applying patch";return{...e,type:v.EventType.TOOL_USE,content:h,metadata:{tool_name:"Edit",tool_input:{file_path:l?.filePath||"",old_string:d,new_string:u},callId:r,status:a||"running"}}}if(i==="custom_tool_call_output"){let{call_id:n,output:r}=p.payload,o=S.get(n);S.delete(n);let a={};try{a=JSON.parse(r||"{}")}catch{a={raw:r}}let l=a.output?.includes("Success")||!a.error;return{...e,type:v.EventType.TOOL_USE,content:l?"File edit applied successfully":`Edit failed: ${a.error||"Unknown error"}`,metadata:{toolName:"Edit",toolOutput:a,callId:n,status:"completed",success:l}}}return s.debug("Unknown response_item type",{type:i}),null}return p.type==="turn_context"?(s.debug("Skipping turn_context entry"),null):(s.debug("Unhandled log entry type",{type:p.type}),null)}function D(p){return{shell_command:"Bash",shell:"Bash",apply_patch:"Edit",write_file:"Write",read_file:"Read",list_files:"Glob",search_files:"Grep",web_search:"WebSearch",web_fetch:"WebFetch"}[p]||p}function ct(p,t){switch(p){case"shell_command":case"shell":return`Running: ${t.command||"command"}`;case"read_file":return`Reading: ${t.file_path||t.path||"file"}`;case"write_file":return`Writing: ${t.file_path||t.path||"file"}`;case"list_files":return`Listing: ${t.path||"."}`;case"search_files":return`Searching for: ${t.pattern||t.query||"pattern"}`;case"web_search":return`Searching web: ${t.query||"query"}`;default:return`Running ${D(p)}`}}function dt(p){let t=[],e=[],i=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;for(let n of p.split(`
3
+ `))if(!(i.test(n)||n.startsWith("***")||n.startsWith("---")||n.startsWith("+++"))){if(n.startsWith("-"))t.push(n.slice(1));else if(n.startsWith("+"))e.push(n.slice(1));else if(n.startsWith(" ")){let r=n.slice(1);t.push(r),e.push(r)}}return{oldString:t.join(`
4
+ `),newString:e.join(`
5
+ `)}}function ut(p){if(!p)return null;let t=p.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);return t?{filePath:t[1].trim()}:null}function ht(p,t){return p?p.length<=t?p:p.substring(0,t)+"...":""}function H(){S.clear()}var q=require("events"),V=require("@quantiya/codevibe-core");var E=class extends q.EventEmitter{constructor(){super();this.pendingCalls=new Map;this.timers=new Map;this.timeoutMs=(0,V.getConfig)().codex.approvalTimeoutMs,s.info("Approval detector initialized",{timeoutMs:this.timeoutMs})}onToolCallStart(e,i,n){s.debug("Tool call started",{callId:e,name:i});let r=this.parseInput(n),o=this.extractFilePath(i,n,r),a=this.extractDiff(i,n,r),l={callId:e,name:i,input:n,filePath:o,diff:a,parsedInput:r,timestamp:Date.now(),notificationSent:!1};this.pendingCalls.set(e,l);let d=setTimeout(()=>{this.checkPendingCall(e)},this.timeoutMs);this.timers.set(e,d)}onToolCallComplete(e){s.debug("Tool call completed",{callId:e}),this.pendingCalls.delete(e);let i=this.timers.get(e);i&&(clearTimeout(i),this.timers.delete(e))}checkPendingCall(e){let i=this.pendingCalls.get(e);if(!i||i.notificationSent)return;let n=Date.now()-i.timestamp;s.info("Tool call still pending after timeout",{callId:e,name:i.name,elapsedMs:n}),i.notificationSent=!0,this.pendingCalls.set(e,i),this.emit("approval-pending",{callId:e,toolName:i.name,hint:this.extractHint(i.name,i.input,i.filePath),filePath:i.filePath,diff:i.diff,toolInput:i.parsedInput,rawInput:i.input,elapsedMs:n})}extractHint(e,i,n){if(n)return`File: ${n}`;if(e==="apply_patch"&&i){let r=i.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(r)return`File: ${r[1].trim()}`}if(e==="shell_command"||e==="shell")try{let r=JSON.parse(i);if(r.command)return`Command: ${r.command.substring(0,50)}${r.command.length>50?"...":""}`}catch{}return`Tool: ${this.mapToolName(e)}`}mapToolName(e){return{shell_command:"Bash",shell:"Bash",apply_patch:"File Edit",write_file:"Write File",read_file:"Read File"}[e]||e}parseInput(e){if(e)try{return JSON.parse(e)}catch{return}}extractFilePath(e,i,n){if(e==="apply_patch"&&i){let o=i.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(o)return o[1].trim()}let r=n?.file_path||n?.path||n?.filePath;if(r&&typeof r=="string")return r}extractDiff(e,i,n){if(e==="apply_patch"&&i)return i;if(n?.diff&&typeof n.diff=="string")return n.diff}getPendingCalls(){return Array.from(this.pendingCalls.values())}hasPendingCalls(){return this.pendingCalls.size>0}clear(){for(let e of this.timers.values())clearTimeout(e);this.timers.clear(),this.pendingCalls.clear(),s.debug("Approval detector cleared")}shutdown(){this.clear(),this.removeAllListeners(),s.info("Approval detector shutdown")}};var J=require("child_process"),X=require("util");var G=(0,X.promisify)(J.exec),_=class{async sendInput(t,e){s.info("Attempting to send input to Codex",{sessionId:t,input:e});try{let i=process.env.CODEVIBE_CODEX_TMUX_SESSION;return i?(s.info("Using tmux send-keys",{tmuxSession:i}),await this.sendViaTmux(i,e),s.info("Successfully sent input to Codex",{sessionId:t,input:e}),!0):(s.error("No tmux session found - codevibe-codex wrapper is required",{sessionId:t,hint:"Start Codex CLI using the codevibe-codex wrapper script"}),!1)}catch(i){return s.error("Failed to send input to Codex",{sessionId:t,error:i instanceof Error?i.message:String(i)}),!1}}async sendViaTmux(t,e){let i=e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");s.info("Sending via tmux",{sessionName:t,inputLength:e.length});try{let n=`tmux send-keys -t "${t}" -l "${i}"`;await G(n),await this.delay(500);let r=`tmux send-keys -t "${t}" Enter`;await G(r),s.info("tmux send-keys completed")}catch(n){throw s.error("tmux send-keys failed",{sessionName:t,error:n}),n}}delay(t){return new Promise(e=>setTimeout(e,t))}isApprovalResponse(t){let e=t.trim().toLowerCase();return["y","n","a","q","e","yes","no"].includes(e)||/^[0-9]+$/.test(e)}};var f=y(require("fs")),Y=y(require("os")),L=y(require("path")),Z=require("crypto"),Q=require("child_process"),tt=require("events"),et=require("util");var R=(0,et.promisify)(Q.exec),F=class extends tt.EventEmitter{constructor(){super(...arguments);this.sessionName=null;this.started=!1;this.pipeFilePath=null;this.filePosition=0;this.watcher=null;this.processing=!1;this.pendingRead=!1;this.lastPromptHash=null}async start(e){if(this.started&&this.sessionName===e){s.debug("Tmux pane observer already started",{sessionName:e});return}this.started&&await this.stop(),this.sessionName=e,this.started=!0,this.filePosition=0,this.lastPromptHash=null,this.pipeFilePath=L.join(Y.tmpdir(),`codevibe-codex-pane-${process.pid}.log`),f.mkdirSync(L.dirname(this.pipeFilePath),{recursive:!0}),f.writeFileSync(this.pipeFilePath,""),await this.enablePipePane(),this.startFileWatcher(),s.info("Tmux pane observer started",{sessionName:e})}async stop(){if(this.started){try{await this.disablePipePane()}catch(e){s.debug("Failed to disable tmux pipe-pane cleanly",{error:e})}if(this.watcher&&(this.watcher.close(),this.watcher=null),this.pipeFilePath)try{f.unlinkSync(this.pipeFilePath)}catch{}s.info("Tmux pane observer stopped",{sessionName:this.sessionName}),this.started=!1,this.sessionName=null,this.pipeFilePath=null,this.filePosition=0,this.processing=!1,this.pendingRead=!1,this.lastPromptHash=null,this.removeAllListeners("prompt-candidate"),this.removeAllListeners("observer-error")}}async captureSnapshot(e=120){if(!this.sessionName)throw new Error("Tmux pane observer is not started");let i=Math.max(1,Math.floor(e)),n=this.escapeShellArg(this.sessionName),r=`tmux capture-pane -p -e -S -${i} -t '${n}'`;try{let{stdout:o}=await R(r);return o}catch(o){throw s.error("Failed to capture tmux pane snapshot",{sessionName:this.sessionName,error:o}),this.emit("observer-error",o),o}}escapeShellArg(e){return e.replace(/'/g,"'\\''")}async enablePipePane(){if(!this.sessionName||!this.pipeFilePath)throw new Error("Tmux pane observer is not initialized");let e=this.escapeShellArg(this.sessionName),i=this.escapeShellArg(this.pipeFilePath),n=`tmux pipe-pane -O -t '${e}' "cat >> '${i}'"`;await R(n),s.debug("Enabled tmux pipe-pane mirroring",{sessionName:this.sessionName,pipeFilePath:this.pipeFilePath})}async disablePipePane(){if(!this.sessionName)return;let i=`tmux pipe-pane -t '${this.escapeShellArg(this.sessionName)}'`;await R(i)}startFileWatcher(){this.pipeFilePath&&(this.watcher=f.watch(this.pipeFilePath,e=>{e==="change"&&this.processFileChanges()}))}async processFileChanges(){if(this.pipeFilePath){if(this.processing){this.pendingRead=!0;return}this.processing=!0;try{do{this.pendingRead=!1;let e=this.readAppendedChunk();if(!e||!this.looksLikePromptDelta(e))continue;let i=await this.captureSnapshot();if(!i||!this.looksLikePromptSnapshot(i))continue;let n=this.hashPromptSnapshot(i);n!==this.lastPromptHash&&(this.lastPromptHash=n,this.emit("prompt-candidate",{rawDelta:e,snapshot:i,detectedAt:Date.now()}))}while(this.pendingRead)}catch(e){s.error("Failed to process tmux pane changes",{error:e}),this.emit("observer-error",e)}finally{this.processing=!1}}}readAppendedChunk(){if(!this.pipeFilePath)return"";let e=f.statSync(this.pipeFilePath);if(e.size<=this.filePosition)return"";let i=f.openSync(this.pipeFilePath,"r");try{let n=e.size-this.filePosition,r=Buffer.alloc(n);return f.readSync(i,r,0,n,this.filePosition),this.filePosition=e.size,r.toString("utf-8")}finally{f.closeSync(i)}}looksLikePromptDelta(e){return/\[(?:y\/n|Y\/n|y\/N)\]|\b(?:apply|approve|allow|reject|deny|continue)\b/i.test(e)}looksLikePromptSnapshot(e){let i=e.split(`
6
+ `).slice(-20).join(`
7
+ `);return/\[(?:y\/n|Y\/n|y\/N)\]|^\s*\d+\.\s+/im.test(i)}hashPromptSnapshot(e){let i=e.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g,"").replace(/\r/g,`
8
+ `).replace(/[ \t]+\n/g,`
9
+ `).trim();return(0,Z.createHash)("sha256").update(i).digest("hex")}};var w=require("@quantiya/codevibe-core");var M=class{constructor(){this.sessionState=null;this.unsubscribe=null;this.sessionKey=null;this.pendingInteractivePrompt=null;this.isInitializingSession=!1;this.bufferedLogEntries=[];this.sessionWatcher=new T,this.approvalDetector=new E,this.promptResponder=new _,this.tmuxPaneObserver=new F}async start(){s.info("Starting CodeVibe Codex companion server",{environment:(0,c.getEnvironment)()}),this.appSyncClient=new c.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()||(s.error('Authentication failed. Run "codevibe-codex login" first.'),console.error('Not authenticated. Run "codevibe-codex login" to sign in.'),process.exit(1)),s.info("Authenticated successfully",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await this.registerDeviceEncryptionKey(),this.setupEventHandlers(),this.sessionWatcher.start(),s.info("CodeVibe Codex companion server started")}setupEventHandlers(){this.sessionWatcher.on("session-started",async t=>{await this.handleSessionStarted(t)}),this.sessionWatcher.on("log-entry",async t=>{await this.handleLogEntry(t)}),this.approvalDetector.on("approval-pending",async t=>{await this.handleApprovalPending(t)}),this.tmuxPaneObserver.on("prompt-candidate",async t=>{await this.handleTmuxPromptCandidate(t.snapshot)}),this.tmuxPaneObserver.on("observer-error",t=>{s.debug("Tmux pane observer error",{error:t})}),this.sessionWatcher.on("error",t=>{s.error("Session watcher error:",t)})}async registerDeviceEncryptionKey(){try{let t=await c.keychainManager.getDeviceId(),e=await c.keychainManager.getDevicePublicKey(),i=c.keychainManager.getDevicePlatform(),n=c.keychainManager.getDeviceName();s.info("Registering device encryption key",{deviceId:t,platform:i,deviceName:n}),await this.appSyncClient.registerDeviceKey(t,e,i,n),c.keychainManager.setIsRegistered(!0),s.info("Device encryption key registered successfully",{deviceId:t})}catch(t){s.warn("Failed to register device encryption key (E2E encryption may not work):",t)}}async handleSessionStarted(t){s.info("Handling new Codex session",{codexSessionId:t.id}),this.isInitializingSession=!0,this.bufferedLogEntries=[],this.sessionState&&await this.endActiveSession("new-codex-session-started");let e=process.env.CODEX_WORKING_DIRECTORY||t.cwd||process.cwd(),i=this.generateSessionId(t.id),n=this.appSyncClient.getCurrentUserId(),r={codexSessionId:t.id,cliVersion:t.cli_version,modelProvider:t.model_provider};try{let o=await(0,c.resumeOrCreateSession)({sessionId:i,userId:n,agentType:c.AgentType.CODEX,projectPath:e,metadata:r},this.appSyncClient,s);this.sessionKey=o.sessionKey}catch(o){throw s.error("Failed to create/resume session:",o),o}try{this.sessionState={sessionId:i,userId:n,projectPath:e,cwd:t.cwd,createdAt:new Date,subscriptionActive:!1,metadata:r,codexSessionId:t.id,codexLogFile:this.sessionWatcher.getActiveLogFile()||void 0},await this.flushBufferedLogEntries(),await this.startTmuxObserver(),this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i)}catch(o){s.error("Failed to create session:",o),this.bufferedLogEntries=[]}finally{this.isInitializingSession=!1}}async flushBufferedLogEntries(){if(this.bufferedLogEntries.length===0)return;let t=this.bufferedLogEntries;this.bufferedLogEntries=[],s.info("Flushing buffered log entries after session initialization",{count:t.length,sessionId:this.sessionState?.sessionId});for(let e of t)await this.handleLogEntry(e)}async handleLogEntry(t){if(!this.sessionState){if(this.isInitializingSession){this.bufferedLogEntries.push(t),s.debug("Buffering log entry until session initialization completes",{type:t.type,bufferedCount:this.bufferedLogEntries.length});return}s.warn("Received log entry but no active session");return}if(t.type==="response_item"&&t.payload){let i=t.payload.type;i==="function_call"||i==="custom_tool_call"?this.approvalDetector.onToolCallStart(t.payload.call_id,t.payload.name,t.payload.arguments||t.payload.input||""):(i==="function_call_output"||i==="custom_tool_call_output")&&(this.approvalDetector.onToolCallComplete(t.payload.call_id),this.pendingInteractivePrompt?.callId===t.payload.call_id&&(this.pendingInteractivePrompt=null))}let e=B(t,this.sessionState.sessionId);if(e)try{if(this.sessionKey){if(e.content=c.cryptoService.encryptContent(e.content,this.sessionKey),e.metadata){let i=c.cryptoService.encryptMetadata(e.metadata,this.sessionKey);e.metadata={encrypted:i}}e.isEncrypted=!0,s.debug("Event encrypted",{type:e.type})}await this.appSyncClient.createEvent(e),s.debug("Event synced to backend",{type:e.type,encrypted:!!this.sessionKey})}catch(i){s.error("Failed to sync event:",i)}}async handleApprovalPending(t){if(this.sessionState){s.info("Sending approval pending interactive prompt",t);try{let e=await this.tryParseInteractivePromptFromTmux(),i=e?.parsedPrompt??null;if(i&&this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===i.promptText){s.debug("Skipping heuristic prompt because tmux prompt is already active",{promptText:i.promptText});return}let n=this.buildToolDetailsForInteractivePrompt(t,e?.snapshot),r=n.tool_name||this.mapToolNameForApproval(t.toolName),o=n.tool_input||this.buildFallbackToolInput(t),a=!!(r&&o),l=this.buildPromptPresentation(i),d=l.options,u=t.filePath?`File: ${t.filePath}`:void 0,h=l.content||`Codex is waiting for approval.
10
+ ${t.hint}`;u&&!h.includes(u)&&(h=`${h}
11
+ ${u}`),this.pendingInteractivePrompt={promptId:t.callId,callId:t.callId,kind:l.kind,options:d,submitMap:l.submitMap,promptText:l.promptText,createdAt:Date.now(),source:i?"tmux":"heuristic",requiresFollowUpText:l.requiresFollowUpText};let m={isApprovalHint:!0,toolName:t.toolName,toolInput:t.toolInput,hint:t.hint,callId:t.callId,filePath:t.filePath,diff:t.diff,rawInput:t.rawInput,tool_name:r,tool_input:o,has_details:a,options:d,instructions:l.instructions,prompt_source:i?"tmux":"heuristic"},P=!1;s.debug("Interactive prompt (pre-encryption)",{sessionId:this.sessionState.sessionId,callId:t.callId,contentPreview:h.substring(0,200),toolDetails:n,metadata:m}),this.sessionKey&&(h=c.cryptoService.encryptContent(h,this.sessionKey),m={encrypted:c.cryptoService.encryptMetadata(m,this.sessionKey)},P=!0),await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:h,metadata:m,promptId:t.callId,...P?{isEncrypted:!0}:{}})}catch(e){s.error("Failed to send approval interactive prompt:",e)}}}async handleTmuxPromptCandidate(t){if(!this.sessionState)return;let e=(0,w.parseInteractivePrompt)(t);if(!e||this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===e.promptText)return;let i=this.buildPromptPresentation(e),n=this.getMostRecentPendingToolCall();n||(await new Promise(I=>setTimeout(I,500)),n=this.getMostRecentPendingToolCall());let r=n?this.buildApprovalPromptContextFromPendingCall(n):null,o=r?this.buildToolDetailsForInteractivePrompt(r,t):{},a=o.tool_name||this.mapToolNameForApproval(n?.name),l=o.tool_input||(r?this.buildFallbackToolInput(r):void 0),d=!!(a&&l),u=this.pendingInteractivePrompt?.callId||n?.callId||(0,it.v4)();this.pendingInteractivePrompt={promptId:u,callId:this.pendingInteractivePrompt?.callId||n?.callId,kind:i.kind,options:i.options,submitMap:i.submitMap,promptText:i.promptText,createdAt:Date.now(),source:"tmux",requiresFollowUpText:i.requiresFollowUpText};let h={options:i.options,instructions:i.instructions,prompt_source:"tmux_live",tool_name:a,tool_input:l,has_details:d},m=i.content,P=!1;this.sessionKey&&(m=c.cryptoService.encryptContent(m,this.sessionKey),h={encrypted:c.cryptoService.encryptMetadata(h,this.sessionKey)},P=!0);try{await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:m,metadata:h,promptId:u,...P?{isEncrypted:!0}:{}}),s.info("Sent tmux-detected interactive prompt",{sessionId:this.sessionState.sessionId,promptText:e.promptText,kind:e.kind})}catch(I){s.error("Failed to send tmux-detected interactive prompt",{error:I})}}async startTmuxObserver(){let t=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!t){s.debug("Skipping tmux pane observer start - no tmux session in environment");return}try{await this.tmuxPaneObserver.start(t)}catch(e){s.warn("Failed to start tmux pane observer",{tmuxSession:t,error:e})}}async tryParseInteractivePromptFromTmux(){try{let t=await this.tmuxPaneObserver.captureSnapshot(),e=(0,w.parseInteractivePrompt)(t);return s.debug("tmux prompt parse result",{parsed:!!e,kind:e?.kind,promptText:e?.promptText,snapshotPreview:this.summarizePromptSnapshot(t)}),{parsedPrompt:e,snapshot:t}}catch(t){return s.debug("tmux prompt parsing unavailable",{error:t}),null}}buildPromptPresentation(t){return t?{content:t.promptText,promptText:t.promptText,kind:t.kind,options:t.options,submitMap:t.submitMap,instructions:this.buildPromptInstructions(t),requiresFollowUpText:t.requiresFollowUpText}:{content:"Codex is waiting for approval.",promptText:"Codex is waiting for approval.",kind:"yes_no",options:[{number:"1",text:'Yes (sends "y")'},{number:"2",text:'No, tell Codex what to change (sends "n <instructions>")'}],submitMap:{1:"y",2:"n"},instructions:"Reply with 1 to approve, or 2 followed by what to change",requiresFollowUpText:!0}}getMostRecentPendingToolCall(){let t=this.approvalDetector.getPendingCalls();return t.length===0?null:t.reduce((e,i)=>i.timestamp>e.timestamp?i:e)}buildApprovalPromptContextFromPendingCall(t){return{toolName:t.name,filePath:t.filePath,diff:t.diff,toolInput:t.parsedInput,rawInput:t.input,hint:t.filePath?`File: ${t.filePath}`:`Tool: ${this.mapToolNameForApproval(t.name)||t.name}`}}buildPromptInstructions(t){return t.kind==="yes_no"&&t.requiresFollowUpText?"Reply with 1 to approve, or 2 followed by what to change":t.kind==="yes_no"?"Reply with 1 for yes or 2 for no":t.kind==="numbered"?"Reply with the number of the option you want":"Reply with your response"}summarizePromptSnapshot(t){return t.split(`
12
+ `).map(e=>e.trimEnd()).filter(e=>e.length>0).slice(-12).map(e=>e.slice(0,160)).join(`
13
+ `)}translatePromptResponse(t){let e=this.pendingInteractivePrompt;if(!e)return{primaryInput:t};let n=t.trim().match(/^(\d+)(?:[,.:;\-\s]+([\s\S]+))?$/);if(!n)return{primaryInput:t};let r=n[1],o=n[2]?.trim(),a=e.submitMap[r];return a?e.requiresFollowUpText&&o?{primaryInput:a,followUpInput:o}:{primaryInput:a}:{primaryInput:t}}buildToolDetailsForInteractivePrompt(t,e){let i=t.toolName,n=t.toolInput&&typeof t.toolInput=="object"?t.toolInput:void 0;if(i==="apply_patch"){let o=t.diff||t.rawInput;if(o){let{oldString:a,newString:l,oldStartLine:d,newStartLine:u}=this.extractOldNewFromPatch(o),h=e?this.extractDiffLineAnchorsFromSnapshot(e):{};return{tool_name:"Edit",tool_input:{file_path:t.filePath,content:o,diff:t.diff,raw_patch:t.rawInput,old_string:a,new_string:l,old_start_line:d??h.oldStartLine,new_start_line:u??h.newStartLine}}}}if(i==="shell_command"||i==="shell"){let o=n?.command||t.rawInput||t.hint;if(o)return{tool_name:"Bash",tool_input:{command:o,output:n?.output}}}let r={};return t.filePath&&(r.file_path=t.filePath),t.diff&&(r.diff=t.diff),t.rawInput&&(r.raw_input=t.rawInput),Object.keys(r).length>0?{tool_name:i||"Tool",tool_input:r}:{}}buildFallbackToolInput(t){let e={};return t.filePath&&(e.file_path=t.filePath),t.diff&&(e.diff=t.diff),t.rawInput&&(e.raw_input=t.rawInput),t.toolInput&&typeof t.toolInput=="object"&&(e.parsed_input=t.toolInput),Object.keys(e).length>0?e:void 0}mapToolNameForApproval(t){return t?{apply_patch:"Edit",shell_command:"Bash",shell:"Bash"}[t]||t:void 0}extractOldNewFromPatch(t){let e=[],i=[],n=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/,r=0,o=0,a=0,l,d;for(let u of t.split(`
14
+ `)){let h=u.match(n);if(h){r+=1,o=Number.parseInt(h[1],10),a=Number.parseInt(h[2],10);continue}if(!(u.startsWith("***")||u.startsWith("---")||u.startsWith("+++")||u.startsWith("*** End Patch"))){if(u.startsWith("-"))l===void 0&&(l=o),e.push(u.slice(1)),o+=1;else if(u.startsWith("+"))d===void 0&&(d=a),i.push(u.slice(1)),a+=1;else if(u.startsWith(" ")){let m=u.slice(1);e.push(m),i.push(m),o+=1,a+=1}}}return{oldString:e.join(`
15
+ `),newString:i.join(`
16
+ `),oldStartLine:r===1?l:void 0,newStartLine:r===1?d:void 0}}extractDiffLineAnchorsFromSnapshot(t){let e=(0,w.normalizeSnapshot)(t),i,n;for(let r of e.split(`
17
+ `)){let o=r.match(/^\s*(\d+)\s+(.*)$/);if(!o)continue;let a=Number.parseInt(o[1],10),l=o[2];if(Number.isFinite(a)){if(l.startsWith("-")){i??=a;continue}if(l.startsWith("+")){n??=a;continue}i??=a,n??=a}}return s.debug("Recovered diff line anchors from tmux snapshot",{oldStartLine:i,newStartLine:n,snapshotPreview:this.summarizePromptSnapshot(t)}),{oldStartLine:i,newStartLine:n}}subscribeToMobileEvents(t){s.info("Subscribing to mobile events",{sessionId:t}),this.unsubscribe&&this.unsubscribe(),this.unsubscribe=this.appSyncClient.subscribeToEvents(t,async e=>{await this.handleMobileEvent(e)},e=>{s.error("Subscription error:",e)}),this.sessionState&&(this.sessionState.subscriptionActive=!0),s.info("Subscribed to mobile events")}async downloadAttachment(t,e,i){try{let n=t.isEncrypted??i??!1;s.info("Downloading attachment",{id:t.id,type:t.type,filename:t.filename,s3Key:t.s3Key,attachmentIsEncrypted:t.isEncrypted,eventIsEncrypted:i,shouldDecrypt:n});let{downloadUrl:r}=await this.appSyncClient.getAttachmentDownloadUrl(t.s3Key),o=await fetch(r);if(!o.ok)throw new Error(`Failed to download attachment: ${o.status} ${o.statusText}`);let a=Buffer.from(await o.arrayBuffer());if(n&&this.sessionKey)try{s.info("Decrypting attachment",{id:t.id}),a=c.cryptoService.decryptData(a,this.sessionKey),s.info("Attachment decrypted successfully",{id:t.id,decryptedSize:a.length})}catch(m){throw s.error("Failed to decrypt attachment:",{id:t.id,error:m}),new Error("Failed to decrypt attachment")}else n&&!this.sessionKey&&s.warn("Cannot decrypt attachment - no session key available",{id:t.id});let l=x.join(nt.tmpdir(),"codevibe-codex",e);b.existsSync(l)||b.mkdirSync(l,{recursive:!0});let d="";if(t.filename){let m=x.extname(t.filename);m&&(d=m)}d||(d={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[t.type]||".bin");let u=`attachment-${t.id}${d}`,h=x.join(l,u);return b.writeFileSync(h,a),s.info("Attachment saved to temp file",{id:t.id,filePath:h,size:a.length}),h}catch(n){return s.error("Failed to download attachment:",{id:t.id,error:n}),null}}async handleMobileEvent(t){if(t.attachments&&t.attachments.length>0&&s.info("DEBUG: Raw attachment data from subscription",{attachments:JSON.stringify(t.attachments),eventIsEncrypted:t.isEncrypted}),s.info("Received mobile event",{eventId:t.eventId,type:t.type,content:t.content?.substring(0,50),attachmentCount:t.attachments?.length||0,isEncrypted:t.isEncrypted}),!this.sessionState){s.warn("Received mobile event but no active session");return}let e=t.content||"";if(t.isEncrypted&&this.sessionKey)try{e=c.cryptoService.decryptContent(t.content,this.sessionKey),s.debug("Event decrypted successfully",{eventId:t.eventId})}catch(i){s.error("Failed to decrypt event:",{eventId:t.eventId,error:i}),e=t.content}try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:c.DeliveryStatus.DELIVERED})}catch(i){s.error("Failed to update delivery status:",i)}if(t.type===c.EventType.USER_PROMPT||t.type===c.EventType.PROMPT_RESPONSE){let i=e,n=t.attachments||[],r=[];if(n.length>0){s.info("Downloading attachments for prompt",{count:n.length});for(let l of n){let d=await this.downloadAttachment(l,this.sessionState.sessionId,t.isEncrypted);d&&r.push(d)}if(r.length>0){let l=r.map(d=>`[Attached file: ${d}]`).join(`
18
+ `);i?i=`${l}
19
+
20
+ ${i}`:i=`${l}
21
+
22
+ Please analyze the attached file(s).`,s.info("Prompt updated with attachment paths",{attachmentCount:r.length,newPromptLength:i.length})}}let o=this.translatePromptResponse(i),a=await this.promptResponder.sendInput(this.sessionState.sessionId,o.primaryInput);if(a&&o.followUpInput&&await this.promptResponder.sendInput(this.sessionState.sessionId,o.followUpInput),a&&this.pendingInteractivePrompt&&t.type===c.EventType.PROMPT_RESPONSE&&(this.pendingInteractivePrompt=null),a)try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:c.DeliveryStatus.EXECUTED})}catch(l){s.error("Failed to update executed status:",l)}}}generateSessionId(t){return`codex-${t}`}async endActiveSession(t){if(this.sessionState){s.info("Ending active session",{sessionId:this.sessionState.sessionId,codexSessionId:this.sessionState.codexSessionId,reason:t}),this.appSyncClient.stopHeartbeat(this.sessionState.sessionId),this.unsubscribe&&(this.unsubscribe(),this.unsubscribe=null),await this.tmuxPaneObserver.stop(),this.pendingInteractivePrompt=null,this.isInitializingSession=!1,this.bufferedLogEntries=[],this.sessionKey&&(c.keychainManager.clearSessionKey(this.sessionState.sessionId),this.sessionKey=null);try{await this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,status:c.SessionStatus.INACTIVE})}catch(e){s.error("Failed to update session status:",e)}this.sessionState=null}}async stop(){s.info("Stopping CodeVibe Codex companion server"),await this.endActiveSession("shutdown"),this.sessionWatcher.stop(),this.approvalDetector.shutdown(),H(),this.appSyncClient.cleanupSubscriptions(),s.info("CodeVibe Codex companion server stopped")}},N=new M;process.on("SIGINT",async()=>{s.info("Received SIGINT, shutting down..."),await N.stop(),process.exit(0)});process.on("SIGTERM",async()=>{s.info("Received SIGTERM, shutting down..."),await N.stop(),process.exit(0)});N.start().catch(p=>{s.error("Failed to start server:",p),process.exit(1)});