@mmmbuto/nexuscli 0.5.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/nexuscli.js +117 -0
  4. package/frontend/dist/apple-touch-icon.png +0 -0
  5. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
  65. package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
  66. package/frontend/dist/browserconfig.xml +12 -0
  67. package/frontend/dist/favicon-16x16.png +0 -0
  68. package/frontend/dist/favicon-32x32.png +0 -0
  69. package/frontend/dist/favicon-48x48.png +0 -0
  70. package/frontend/dist/favicon.ico +0 -0
  71. package/frontend/dist/icon-192.png +0 -0
  72. package/frontend/dist/icon-512.png +0 -0
  73. package/frontend/dist/icon-maskable-192.png +0 -0
  74. package/frontend/dist/icon-maskable-512.png +0 -0
  75. package/frontend/dist/index.html +79 -0
  76. package/frontend/dist/manifest.json +75 -0
  77. package/frontend/dist/sw.js +122 -0
  78. package/frontend/package.json +28 -0
  79. package/lib/cli/api.js +156 -0
  80. package/lib/cli/boot.js +172 -0
  81. package/lib/cli/config.js +185 -0
  82. package/lib/cli/engines.js +257 -0
  83. package/lib/cli/init.js +660 -0
  84. package/lib/cli/logs.js +72 -0
  85. package/lib/cli/start.js +220 -0
  86. package/lib/cli/status.js +187 -0
  87. package/lib/cli/stop.js +64 -0
  88. package/lib/cli/uninstall.js +194 -0
  89. package/lib/cli/users.js +295 -0
  90. package/lib/cli/workspaces.js +337 -0
  91. package/lib/config/manager.js +233 -0
  92. package/lib/server/.env.example +20 -0
  93. package/lib/server/db/adapter.js +314 -0
  94. package/lib/server/db/drivers/better-sqlite3.js +38 -0
  95. package/lib/server/db/drivers/sql-js.js +75 -0
  96. package/lib/server/db/migrate.js +174 -0
  97. package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
  98. package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
  99. package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
  100. package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
  101. package/lib/server/db.js +2 -0
  102. package/lib/server/lib/cli-wrapper.js +164 -0
  103. package/lib/server/lib/output-parser.js +132 -0
  104. package/lib/server/lib/pty-adapter.js +57 -0
  105. package/lib/server/middleware/auth.js +103 -0
  106. package/lib/server/models/Conversation.js +259 -0
  107. package/lib/server/models/Message.js +228 -0
  108. package/lib/server/models/User.js +115 -0
  109. package/lib/server/package-lock.json +5895 -0
  110. package/lib/server/routes/auth.js +168 -0
  111. package/lib/server/routes/chat.js +206 -0
  112. package/lib/server/routes/codex.js +205 -0
  113. package/lib/server/routes/conversations.js +224 -0
  114. package/lib/server/routes/gemini.js +228 -0
  115. package/lib/server/routes/jobs.js +317 -0
  116. package/lib/server/routes/messages.js +60 -0
  117. package/lib/server/routes/models.js +198 -0
  118. package/lib/server/routes/sessions.js +285 -0
  119. package/lib/server/routes/upload.js +134 -0
  120. package/lib/server/routes/wake-lock.js +95 -0
  121. package/lib/server/routes/workspace.js +80 -0
  122. package/lib/server/routes/workspaces.js +142 -0
  123. package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
  124. package/lib/server/scripts/seed-users.js +37 -0
  125. package/lib/server/scripts/test-history-access.js +50 -0
  126. package/lib/server/server.js +227 -0
  127. package/lib/server/services/cache.js +85 -0
  128. package/lib/server/services/claude-wrapper.js +312 -0
  129. package/lib/server/services/cli-loader.js +384 -0
  130. package/lib/server/services/codex-output-parser.js +277 -0
  131. package/lib/server/services/codex-wrapper.js +224 -0
  132. package/lib/server/services/context-bridge.js +289 -0
  133. package/lib/server/services/gemini-output-parser.js +398 -0
  134. package/lib/server/services/gemini-wrapper.js +249 -0
  135. package/lib/server/services/history-sync.js +407 -0
  136. package/lib/server/services/output-parser.js +415 -0
  137. package/lib/server/services/session-manager.js +465 -0
  138. package/lib/server/services/summary-generator.js +259 -0
  139. package/lib/server/services/workspace-manager.js +516 -0
  140. package/lib/server/tests/history-sync.test.js +90 -0
  141. package/lib/server/tests/integration-session-sync.test.js +151 -0
  142. package/lib/server/tests/integration.test.js +76 -0
  143. package/lib/server/tests/performance.test.js +118 -0
  144. package/lib/server/tests/services.test.js +160 -0
  145. package/lib/setup/postinstall.js +216 -0
  146. package/lib/utils/paths.js +107 -0
  147. package/lib/utils/termux.js +145 -0
  148. package/package.json +82 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Output Parser for Codex CLI JSON Stream
3
+ * Parses `codex exec --json` JSONL output
4
+ *
5
+ * JSON Event Types from Codex:
6
+ * - thread.started: Thread initialization with thread_id
7
+ * - turn.started: Turn begins
8
+ * - item.started: Item in progress (command_execution with status: in_progress)
9
+ * - item.completed: Item finished (reasoning, agent_message, command_execution)
10
+ * - turn.completed: Turn finished with usage stats
11
+ * - turn.failed: Turn failed with error
12
+ *
13
+ * Emits SSE events:
14
+ * - status (category: tool, reasoning, system) -> for StatusBar
15
+ * - response_chunk -> text content for chat
16
+ * - response_done -> final text
17
+ * - done -> completion with usage
18
+ * - error -> error message
19
+ */
20
+
21
+ class CodexOutputParser {
22
+ constructor() {
23
+ this.buffer = '';
24
+ this.finalResponse = '';
25
+ this.usage = null;
26
+ this.threadId = null;
27
+ this.pendingCommands = new Map(); // Track in-progress commands
28
+ }
29
+
30
+ /**
31
+ * Parse a chunk of stdout (may contain multiple JSON lines)
32
+ * @param {string} chunk - Raw stdout chunk from node-pty
33
+ * @returns {Array} Array of event objects for SSE
34
+ */
35
+ parse(chunk) {
36
+ const events = [];
37
+
38
+ // Add chunk to buffer
39
+ this.buffer += chunk;
40
+
41
+ // Process complete lines
42
+ const lines = this.buffer.split('\n');
43
+ this.buffer = lines.pop() || ''; // Keep incomplete last line
44
+
45
+ for (const line of lines) {
46
+ const trimmed = line.trim();
47
+ if (!trimmed) continue;
48
+
49
+ // Skip non-JSON lines (e.g., "Reading prompt from stdin...")
50
+ if (!trimmed.startsWith('{')) {
51
+ console.log('[CodexOutputParser] Non-JSON line (ignored):', trimmed.substring(0, 80));
52
+ continue;
53
+ }
54
+
55
+ try {
56
+ const json = JSON.parse(trimmed);
57
+ const lineEvents = this.parseJsonEvent(json);
58
+ events.push(...lineEvents);
59
+ } catch (e) {
60
+ console.log('[CodexOutputParser] JSON parse error:', e.message, '- Line:', trimmed.substring(0, 100));
61
+ }
62
+ }
63
+
64
+ return events;
65
+ }
66
+
67
+ /**
68
+ * Parse a single JSON event from Codex
69
+ * @param {Object} event - Parsed JSON object
70
+ * @returns {Array} Events to emit
71
+ */
72
+ parseJsonEvent(event) {
73
+ const events = [];
74
+
75
+ switch (event.type) {
76
+ case 'thread.started':
77
+ this.threadId = event.thread_id;
78
+ console.log('[CodexOutputParser] Thread started:', this.threadId);
79
+ events.push({
80
+ type: 'status',
81
+ category: 'system',
82
+ message: 'Session started',
83
+ icon: '🚀',
84
+ timestamp: new Date().toISOString(),
85
+ });
86
+ break;
87
+
88
+ case 'turn.started':
89
+ console.log('[CodexOutputParser] Turn started');
90
+ break;
91
+
92
+ case 'item.started':
93
+ // Command execution starting
94
+ if (event.item?.type === 'command_execution') {
95
+ const cmd = event.item.command || 'Unknown command';
96
+ this.pendingCommands.set(event.item.id, event.item);
97
+
98
+ events.push({
99
+ type: 'status',
100
+ category: 'tool',
101
+ message: `Bash: ${this.truncate(cmd, 60)}`,
102
+ icon: '🔧',
103
+ timestamp: new Date().toISOString(),
104
+ });
105
+ }
106
+ break;
107
+
108
+ case 'item.completed':
109
+ if (!event.item) break;
110
+
111
+ switch (event.item.type) {
112
+ case 'reasoning':
113
+ // Reasoning/thinking
114
+ const reasoningText = event.item.text || '';
115
+ if (reasoningText.trim()) {
116
+ events.push({
117
+ type: 'status',
118
+ category: 'reasoning',
119
+ message: `Thinking: ${this.truncate(reasoningText, 50)}`,
120
+ icon: '🧠',
121
+ timestamp: new Date().toISOString(),
122
+ });
123
+ }
124
+ break;
125
+
126
+ case 'command_execution':
127
+ // Command completed
128
+ const cmd = event.item.command || 'command';
129
+ const exitCode = event.item.exit_code;
130
+ const status = exitCode === 0 ? 'completed' : `failed (${exitCode})`;
131
+
132
+ events.push({
133
+ type: 'status',
134
+ category: 'tool',
135
+ message: `Bash: ${this.truncate(cmd, 40)} - ${status}`,
136
+ icon: exitCode === 0 ? '✅' : '❌',
137
+ toolOutput: this.truncateOutput(event.item.aggregated_output),
138
+ timestamp: new Date().toISOString(),
139
+ });
140
+
141
+ this.pendingCommands.delete(event.item.id);
142
+ break;
143
+
144
+ case 'agent_message':
145
+ // Final response text
146
+ const text = event.item.text || '';
147
+ if (text.trim()) {
148
+ this.finalResponse = text;
149
+ events.push({
150
+ type: 'response_chunk',
151
+ text: text,
152
+ isIncremental: false,
153
+ });
154
+ }
155
+ break;
156
+
157
+ case 'file_read':
158
+ events.push({
159
+ type: 'status',
160
+ category: 'tool',
161
+ message: `Reading: ${this.truncate(event.item.path || '', 50)}`,
162
+ icon: '📖',
163
+ timestamp: new Date().toISOString(),
164
+ });
165
+ break;
166
+
167
+ case 'file_write':
168
+ events.push({
169
+ type: 'status',
170
+ category: 'tool',
171
+ message: `Writing: ${this.truncate(event.item.path || '', 50)}`,
172
+ icon: '✍️',
173
+ timestamp: new Date().toISOString(),
174
+ });
175
+ break;
176
+
177
+ case 'file_edit':
178
+ events.push({
179
+ type: 'status',
180
+ category: 'tool',
181
+ message: `Editing: ${this.truncate(event.item.path || '', 50)}`,
182
+ icon: '📝',
183
+ timestamp: new Date().toISOString(),
184
+ });
185
+ break;
186
+
187
+ default:
188
+ console.log('[CodexOutputParser] Unknown item type:', event.item.type);
189
+ }
190
+ break;
191
+
192
+ case 'turn.completed':
193
+ // Turn finished with usage
194
+ this.usage = event.usage || null;
195
+ console.log('[CodexOutputParser] Turn completed, usage:', JSON.stringify(this.usage));
196
+
197
+ // Emit response_done
198
+ events.push({
199
+ type: 'response_done',
200
+ fullText: this.finalResponse,
201
+ });
202
+
203
+ // Emit done with usage
204
+ events.push({
205
+ type: 'done',
206
+ usage: {
207
+ prompt_tokens: this.usage?.input_tokens || 0,
208
+ completion_tokens: this.usage?.output_tokens || 0,
209
+ total_tokens: (this.usage?.input_tokens || 0) + (this.usage?.output_tokens || 0),
210
+ cached_tokens: this.usage?.cached_input_tokens || 0,
211
+ },
212
+ threadId: this.threadId,
213
+ });
214
+ break;
215
+
216
+ case 'turn.failed':
217
+ console.error('[CodexOutputParser] Turn failed:', event.error);
218
+ events.push({
219
+ type: 'error',
220
+ message: event.error || 'Unknown error',
221
+ });
222
+ break;
223
+
224
+ default:
225
+ console.log('[CodexOutputParser] Unknown event type:', event.type);
226
+ }
227
+
228
+ return events;
229
+ }
230
+
231
+ /**
232
+ * Truncate string to max length
233
+ */
234
+ truncate(str, maxLen) {
235
+ if (!str) return '';
236
+ if (str.length <= maxLen) return str;
237
+ return str.substring(0, maxLen) + '...';
238
+ }
239
+
240
+ /**
241
+ * Truncate tool output (can be very long)
242
+ */
243
+ truncateOutput(content) {
244
+ if (!content) return null;
245
+ const str = typeof content === 'string' ? content : JSON.stringify(content);
246
+ if (str.length > 500) {
247
+ return str.substring(0, 500) + '\n... (truncated)';
248
+ }
249
+ return str;
250
+ }
251
+
252
+ /**
253
+ * Get accumulated final response
254
+ */
255
+ getFinalResponse() {
256
+ return this.finalResponse;
257
+ }
258
+
259
+ /**
260
+ * Get usage statistics
261
+ */
262
+ getUsage() {
263
+ return this.usage;
264
+ }
265
+
266
+ /**
267
+ * Reset parser state for new request
268
+ */
269
+ reset() {
270
+ this.buffer = '';
271
+ this.finalResponse = '';
272
+ this.usage = null;
273
+ this.pendingCommands.clear();
274
+ }
275
+ }
276
+
277
+ module.exports = CodexOutputParser;
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Codex CLI Wrapper for NexusCLI (Termux)
3
+ * Uses `codex exec --json` for non-interactive JSONL output
4
+ *
5
+ * Based on NexusChat codex-cli-wrapper.js pattern
6
+ * Requires: codex-cli 0.62.1+ with exec subcommand
7
+ */
8
+
9
+ const { spawn, exec } = require('child_process');
10
+ const CodexOutputParser = require('./codex-output-parser');
11
+
12
+ class CodexWrapper {
13
+ constructor(options = {}) {
14
+ this.workspaceDir = options.workspaceDir || process.cwd();
15
+ this.codexBin = options.codexBin || 'codex';
16
+
17
+ // Track active sessions
18
+ this.activeSessions = new Set();
19
+
20
+ console.log('[CodexWrapper] Initialized');
21
+ console.log('[CodexWrapper] Workspace:', this.workspaceDir);
22
+ console.log('[CodexWrapper] Binary:', this.codexBin);
23
+ }
24
+
25
+ /**
26
+ * Send message and get response with streaming events
27
+ * @param {Object} options - Message options
28
+ * @param {string} options.prompt - User prompt
29
+ * @param {string} options.model - Model name (e.g., gpt-5.1-codex-max)
30
+ * @param {string} options.sessionId - Session ID for conversation continuity
31
+ * @param {string} options.reasoningEffort - Reasoning level (low, medium, high, xhigh)
32
+ * @param {string} options.workspacePath - Working directory override
33
+ * @param {string[]} options.imageFiles - Array of image file paths for multimodal
34
+ * @param {Function} options.onStatus - Callback for status events
35
+ * @returns {Promise<Object>} Response with text, usage
36
+ */
37
+ async sendMessage({ prompt, model, sessionId, reasoningEffort, workspacePath, imageFiles = [], onStatus }) {
38
+ return new Promise((resolve, reject) => {
39
+ const parser = new CodexOutputParser();
40
+ const cwd = workspacePath || this.workspaceDir;
41
+
42
+ // Build CLI arguments
43
+ const args = [
44
+ 'exec',
45
+ '--json', // JSONL output for parsing
46
+ '--skip-git-repo-check', // Allow non-git directories
47
+ '--dangerously-bypass-approvals-and-sandbox', // Full access (safety via CLAUDE.md policy)
48
+ '-C', cwd, // Working directory
49
+ ];
50
+
51
+ // Add model if specified
52
+ if (model) {
53
+ const baseModel = this.extractBaseModel(model);
54
+ args.push('-m', baseModel);
55
+ }
56
+
57
+ // Add reasoning effort config override
58
+ if (reasoningEffort) {
59
+ args.push('-c', `model_reasoning_effort="${reasoningEffort}"`);
60
+ }
61
+
62
+ // Add image files for multimodal support
63
+ if (imageFiles && imageFiles.length > 0) {
64
+ for (const imagePath of imageFiles) {
65
+ args.push('-i', imagePath);
66
+ }
67
+ console.log('[CodexWrapper] Attached', imageFiles.length, 'image(s)');
68
+ }
69
+
70
+ // Add prompt as argument (use '--' to separate from options)
71
+ args.push('--', prompt);
72
+
73
+ console.log('[CodexWrapper] Model:', model);
74
+ console.log('[CodexWrapper] Reasoning:', reasoningEffort);
75
+ console.log('[CodexWrapper] Session:', sessionId);
76
+ console.log('[CodexWrapper] CWD:', cwd);
77
+ console.log('[CodexWrapper] Args:', args.slice(0, 6).join(' ') + '...');
78
+
79
+ let stdout = '';
80
+
81
+ const proc = spawn(this.codexBin, args, {
82
+ cwd: cwd,
83
+ env: {
84
+ ...global.process.env,
85
+ TERM: 'xterm-256color',
86
+ },
87
+ });
88
+
89
+ proc.stdout.on('data', (data) => {
90
+ const str = data.toString();
91
+ stdout += str;
92
+ this.handleOutput(str, parser, onStatus);
93
+ });
94
+
95
+ proc.stderr.on('data', (data) => {
96
+ console.error('[CodexWrapper] stderr:', data.toString());
97
+ });
98
+
99
+ proc.on('close', (exitCode) => {
100
+ clearTimeout(timeout);
101
+ this.handleExit(exitCode, stdout, parser, prompt, resolve, reject);
102
+ });
103
+
104
+ // Timeout after 10 minutes
105
+ const timeout = setTimeout(() => {
106
+ console.error('[CodexWrapper] Timeout after 10 minutes');
107
+ proc.kill('SIGTERM');
108
+ reject(new Error('Codex CLI timeout'));
109
+ }, 600000);
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Handle output data from CLI
115
+ */
116
+ handleOutput(data, parser, onStatus) {
117
+ // Clean ANSI escape codes
118
+ const cleanData = data
119
+ .replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
120
+ .replace(/\x1B\[\?[0-9;]*[a-zA-Z]/g, '')
121
+ .replace(/\r/g, '');
122
+
123
+ // Parse and emit events
124
+ if (onStatus && cleanData.trim()) {
125
+ try {
126
+ const events = parser.parse(cleanData);
127
+ if (events.length > 0) {
128
+ console.log('[CodexWrapper] Parsed', events.length, 'events');
129
+ }
130
+ events.forEach(event => {
131
+ console.log('[CodexWrapper] → Emitting:', event.type, '-', event.category || '', '-', (event.message || event.text || '').substring(0, 50));
132
+ onStatus(event);
133
+ });
134
+ } catch (parseError) {
135
+ console.error('[CodexWrapper] Parser error:', parseError.message);
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Handle process exit
142
+ */
143
+ handleExit(exitCode, stdout, parser, prompt, resolve, reject) {
144
+ console.log('[CodexWrapper] Exit code:', exitCode);
145
+
146
+ if (exitCode !== 0 && exitCode !== null) {
147
+ console.error('[CodexWrapper] Error output:', stdout.substring(0, 500));
148
+ reject(new Error(`Codex CLI error (exit ${exitCode}): ${stdout.substring(0, 200)}`));
149
+ return;
150
+ }
151
+
152
+ const finalResponse = parser.getFinalResponse();
153
+ const usage = parser.getUsage();
154
+
155
+ console.log('[CodexWrapper] Final response length:', finalResponse.length);
156
+
157
+ // Calculate token counts (fallback)
158
+ const promptTokens = usage?.input_tokens || Math.ceil(prompt.length / 4);
159
+ const completionTokens = usage?.output_tokens || Math.ceil(finalResponse.length / 4);
160
+
161
+ resolve({
162
+ text: finalResponse,
163
+ usage: {
164
+ prompt_tokens: promptTokens,
165
+ completion_tokens: completionTokens,
166
+ total_tokens: promptTokens + completionTokens,
167
+ cached_tokens: usage?.cached_input_tokens || 0,
168
+ },
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Extract base model name (without reasoning suffix)
174
+ * The CLI uses model name + config override for reasoning
175
+ */
176
+ extractBaseModel(modelName) {
177
+ // Remove reasoning suffixes to get base model for CLI
178
+ return modelName
179
+ .replace(/-low$/, '')
180
+ .replace(/-medium$/, '')
181
+ .replace(/-high$/, '')
182
+ .replace(/-xhigh$/, '')
183
+ // Legacy suffixes
184
+ .replace(/-fast$/, '')
185
+ .replace(/-balanced$/, '')
186
+ .replace(/-instant$/, '');
187
+ }
188
+
189
+ /**
190
+ * Check if Codex CLI is available
191
+ */
192
+ async isAvailable() {
193
+ return new Promise((resolve) => {
194
+ exec(`${this.codexBin} --version`, (error, stdout) => {
195
+ if (error) {
196
+ console.log('[CodexWrapper] Codex CLI not available:', error.message);
197
+ resolve(false);
198
+ } else {
199
+ console.log('[CodexWrapper] Codex CLI version:', stdout.trim());
200
+ resolve(true);
201
+ }
202
+ });
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Check if exec subcommand is available
208
+ */
209
+ async hasExecSupport() {
210
+ return new Promise((resolve) => {
211
+ exec(`${this.codexBin} exec --help`, (error, stdout) => {
212
+ if (error) {
213
+ console.log('[CodexWrapper] exec subcommand not available');
214
+ resolve(false);
215
+ } else {
216
+ console.log('[CodexWrapper] exec subcommand available');
217
+ resolve(true);
218
+ }
219
+ });
220
+ });
221
+ }
222
+ }
223
+
224
+ module.exports = CodexWrapper;