@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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/nexuscli.js +117 -0
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
- package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
- package/frontend/dist/browserconfig.xml +12 -0
- package/frontend/dist/favicon-16x16.png +0 -0
- package/frontend/dist/favicon-32x32.png +0 -0
- package/frontend/dist/favicon-48x48.png +0 -0
- package/frontend/dist/favicon.ico +0 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/icon-maskable-192.png +0 -0
- package/frontend/dist/icon-maskable-512.png +0 -0
- package/frontend/dist/index.html +79 -0
- package/frontend/dist/manifest.json +75 -0
- package/frontend/dist/sw.js +122 -0
- package/frontend/package.json +28 -0
- package/lib/cli/api.js +156 -0
- package/lib/cli/boot.js +172 -0
- package/lib/cli/config.js +185 -0
- package/lib/cli/engines.js +257 -0
- package/lib/cli/init.js +660 -0
- package/lib/cli/logs.js +72 -0
- package/lib/cli/start.js +220 -0
- package/lib/cli/status.js +187 -0
- package/lib/cli/stop.js +64 -0
- package/lib/cli/uninstall.js +194 -0
- package/lib/cli/users.js +295 -0
- package/lib/cli/workspaces.js +337 -0
- package/lib/config/manager.js +233 -0
- package/lib/server/.env.example +20 -0
- package/lib/server/db/adapter.js +314 -0
- package/lib/server/db/drivers/better-sqlite3.js +38 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
- package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
- package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
- package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
- package/lib/server/db.js +2 -0
- package/lib/server/lib/cli-wrapper.js +164 -0
- package/lib/server/lib/output-parser.js +132 -0
- package/lib/server/lib/pty-adapter.js +57 -0
- package/lib/server/middleware/auth.js +103 -0
- package/lib/server/models/Conversation.js +259 -0
- package/lib/server/models/Message.js +228 -0
- package/lib/server/models/User.js +115 -0
- package/lib/server/package-lock.json +5895 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/chat.js +206 -0
- package/lib/server/routes/codex.js +205 -0
- package/lib/server/routes/conversations.js +224 -0
- package/lib/server/routes/gemini.js +228 -0
- package/lib/server/routes/jobs.js +317 -0
- package/lib/server/routes/messages.js +60 -0
- package/lib/server/routes/models.js +198 -0
- package/lib/server/routes/sessions.js +285 -0
- package/lib/server/routes/upload.js +134 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspace.js +80 -0
- package/lib/server/routes/workspaces.js +142 -0
- package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
- package/lib/server/scripts/seed-users.js +37 -0
- package/lib/server/scripts/test-history-access.js +50 -0
- package/lib/server/server.js +227 -0
- package/lib/server/services/cache.js +85 -0
- package/lib/server/services/claude-wrapper.js +312 -0
- package/lib/server/services/cli-loader.js +384 -0
- package/lib/server/services/codex-output-parser.js +277 -0
- package/lib/server/services/codex-wrapper.js +224 -0
- package/lib/server/services/context-bridge.js +289 -0
- package/lib/server/services/gemini-output-parser.js +398 -0
- package/lib/server/services/gemini-wrapper.js +249 -0
- package/lib/server/services/history-sync.js +407 -0
- package/lib/server/services/output-parser.js +415 -0
- package/lib/server/services/session-manager.js +465 -0
- package/lib/server/services/summary-generator.js +259 -0
- package/lib/server/services/workspace-manager.js +516 -0
- package/lib/server/tests/history-sync.test.js +90 -0
- package/lib/server/tests/integration-session-sync.test.js +151 -0
- package/lib/server/tests/integration.test.js +76 -0
- package/lib/server/tests/performance.test.js +118 -0
- package/lib/server/tests/services.test.js +160 -0
- package/lib/setup/postinstall.js +216 -0
- package/lib/utils/paths.js +107 -0
- package/lib/utils/termux.js +145 -0
- 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;
|