@probelabs/probe 0.6.0-rc167 → 0.6.0-rc169

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * OpenAI Codex Engine using MCP server approach with event streaming
3
+ * Runs 'codex mcp-server' and handles codex/event notifications
4
+ */
5
+
6
+ import { spawn } from 'child_process';
7
+ import { randomBytes } from 'crypto';
8
+ import { createInterface } from 'readline';
9
+ import { BuiltInMCPServer } from '../mcp/built-in-server.js';
10
+ import { Session } from '../shared/Session.js';
11
+
12
+ /**
13
+ * Codex Engine using MCP Server with event streaming
14
+ */
15
+ export async function createCodexEngine(options = {}) {
16
+ const { agent, systemPrompt, customPrompt, debug, sessionId, allowedTools, model } = options;
17
+
18
+ const session = new Session(
19
+ sessionId || randomBytes(8).toString('hex'),
20
+ debug
21
+ );
22
+
23
+ // Start built-in MCP server for Probe tools
24
+ let mcpServer = null;
25
+ let mcpServerUrl = null;
26
+ let mcpServerName = null;
27
+
28
+ if (agent) {
29
+ mcpServer = new BuiltInMCPServer(agent, {
30
+ port: 0,
31
+ host: '127.0.0.1',
32
+ debug: debug
33
+ });
34
+
35
+ const { host, port } = await mcpServer.start();
36
+ mcpServerUrl = `http://${host}:${port}/mcp`;
37
+ mcpServerName = `probe_${session.id}`;
38
+
39
+ if (debug) {
40
+ console.log('[DEBUG] Built-in Probe MCP server started');
41
+ console.log('[DEBUG] Probe MCP URL:', mcpServerUrl);
42
+ }
43
+ }
44
+
45
+ // Start Codex MCP server
46
+ if (debug) {
47
+ console.log('[DEBUG] Starting Codex MCP server...');
48
+ }
49
+
50
+ const codexProcess = spawn('codex', ['mcp-server'], {
51
+ stdio: ['pipe', 'pipe', 'pipe']
52
+ });
53
+
54
+ // Setup JSON-RPC communication
55
+ let requestId = 0;
56
+ const pendingRequests = new Map();
57
+ const eventHandlers = new Map();
58
+
59
+ // Read stdout line by line
60
+ const stdoutReader = createInterface({
61
+ input: codexProcess.stdout,
62
+ crlfDelay: Infinity
63
+ });
64
+
65
+ stdoutReader.on('line', (line) => {
66
+ try {
67
+ const message = JSON.parse(line);
68
+
69
+ if (debug) {
70
+ if (message.method === 'codex/event') {
71
+ console.log(`[DEBUG] Codex event: ${message.params?.msg?.type}`);
72
+ }
73
+ }
74
+
75
+ // Handle responses to our requests
76
+ if (message.id !== undefined && pendingRequests.has(message.id)) {
77
+ const { resolve, reject } = pendingRequests.get(message.id);
78
+ pendingRequests.delete(message.id);
79
+
80
+ if (message.error) {
81
+ reject(new Error(message.error.message || JSON.stringify(message.error)));
82
+ } else {
83
+ resolve(message.result);
84
+ }
85
+ }
86
+
87
+ // Handle notifications (codex/event)
88
+ if (message.method === 'codex/event' && message.params) {
89
+ const requestId = message.params._meta?.requestId;
90
+ if (requestId !== undefined && eventHandlers.has(requestId)) {
91
+ eventHandlers.get(requestId)(message.params);
92
+ }
93
+ }
94
+ } catch (e) {
95
+ if (debug) {
96
+ console.error('[DEBUG] Failed to parse message:', line);
97
+ }
98
+ }
99
+ });
100
+
101
+ // Handle stderr
102
+ if (debug) {
103
+ codexProcess.stderr.on('data', (data) => {
104
+ console.error('[CODEX STDERR]', data.toString());
105
+ });
106
+ }
107
+
108
+ // Send JSON-RPC request
109
+ function sendRequest(method, params = {}) {
110
+ return new Promise((resolve, reject) => {
111
+ const id = ++requestId;
112
+ const request = {
113
+ jsonrpc: '2.0',
114
+ id,
115
+ method,
116
+ params
117
+ };
118
+
119
+ pendingRequests.set(id, { resolve, reject });
120
+
121
+ // Timeout after 10 minutes
122
+ setTimeout(() => {
123
+ if (pendingRequests.has(id)) {
124
+ pendingRequests.delete(id);
125
+ reject(new Error(`Request ${method} timed out after 10 minutes`));
126
+ }
127
+ }, 600000);
128
+
129
+ codexProcess.stdin.write(JSON.stringify(request) + '\n');
130
+ });
131
+ }
132
+
133
+ // Initialize MCP connection
134
+ await sendRequest('initialize', {
135
+ protocolVersion: '2024-11-05',
136
+ capabilities: { tools: {} },
137
+ clientInfo: {
138
+ name: 'probe-codex-client',
139
+ version: '1.0.0'
140
+ }
141
+ });
142
+
143
+ if (debug) {
144
+ console.log('[DEBUG] Connected to Codex MCP server');
145
+ console.log('[DEBUG] Session:', session.id);
146
+ }
147
+
148
+ const fullPrompt = combinePrompts(systemPrompt, customPrompt, agent);
149
+
150
+ return {
151
+ sessionId: session.id,
152
+ session,
153
+
154
+ /**
155
+ * Query Codex via MCP protocol with event streaming
156
+ */
157
+ async *query(prompt, opts = {}) {
158
+ // Build prompt
159
+ let finalPrompt = prompt;
160
+ if (!session.conversationId && fullPrompt) {
161
+ finalPrompt = `${fullPrompt}\n\n${prompt}`;
162
+ }
163
+
164
+ const isFollowUp = session.conversationId !== null;
165
+ const toolName = isFollowUp ? 'codex-reply' : 'codex';
166
+
167
+ // Build arguments
168
+ const toolArgs = { prompt: finalPrompt };
169
+
170
+ if (isFollowUp) {
171
+ toolArgs.conversationId = session.conversationId;
172
+ if (debug) {
173
+ console.log(`[DEBUG] Follow-up with conversationId: ${session.conversationId}`);
174
+ }
175
+ } else {
176
+ if (model) {
177
+ toolArgs.model = model;
178
+ }
179
+ if (mcpServerUrl && mcpServerName) {
180
+ toolArgs.config = {
181
+ mcp_servers: {
182
+ [mcpServerName]: { url: mcpServerUrl }
183
+ }
184
+ };
185
+ }
186
+ if (debug) {
187
+ console.log(`[DEBUG] Initial query with tool: ${toolName}`);
188
+ }
189
+ }
190
+
191
+ try {
192
+ const reqId = requestId + 1;
193
+ let fullResponse = '';
194
+ let gotSessionId = false;
195
+
196
+ // Register event handler for this request
197
+ const eventPromise = new Promise((resolve) => {
198
+ eventHandlers.set(reqId, (eventParams) => {
199
+ const msg = eventParams.msg;
200
+
201
+ // Extract session_id from session_configured event
202
+ if (msg.type === 'session_configured' && msg.session_id && !gotSessionId) {
203
+ session.setConversationId(msg.session_id);
204
+ gotSessionId = true;
205
+ }
206
+
207
+ // Collect agent messages
208
+ if (msg.type === 'raw_response_item' && msg.item?.role === 'assistant') {
209
+ const content = msg.item.content;
210
+ if (Array.isArray(content)) {
211
+ for (const part of content) {
212
+ if (part.type === 'text' && part.text) {
213
+ fullResponse += part.text;
214
+ }
215
+ }
216
+ }
217
+ }
218
+ });
219
+
220
+ // Mark as resolved when we're done
221
+ setTimeout(() => {
222
+ eventHandlers.delete(reqId);
223
+ resolve();
224
+ }, 600000); // 10 min timeout
225
+ });
226
+
227
+ // Call the tool
228
+ const resultPromise = sendRequest('tools/call', {
229
+ name: toolName,
230
+ arguments: toolArgs
231
+ });
232
+
233
+ // Wait for result
234
+ const result = await resultPromise;
235
+
236
+ // Clean up event handler
237
+ eventHandlers.delete(reqId);
238
+
239
+ // Parse result
240
+ if (result && result.content && Array.isArray(result.content)) {
241
+ for (const item of result.content) {
242
+ if (item.type === 'text' && item.text) {
243
+ yield {
244
+ type: 'text',
245
+ content: item.text
246
+ };
247
+ fullResponse = item.text; // Use final result if available
248
+ }
249
+ }
250
+ }
251
+
252
+ // If we got a response from events but not from result, yield it
253
+ if (fullResponse && (!result.content || result.content.length === 0)) {
254
+ yield {
255
+ type: 'text',
256
+ content: fullResponse
257
+ };
258
+ }
259
+
260
+ session.incrementMessageCount();
261
+
262
+ yield {
263
+ type: 'metadata',
264
+ data: {
265
+ sessionId: session.id,
266
+ conversationId: session.conversationId,
267
+ messageCount: session.messageCount
268
+ }
269
+ };
270
+
271
+ } catch (error) {
272
+ if (debug) {
273
+ console.error('[DEBUG] Codex query error:', error);
274
+ }
275
+ yield {
276
+ type: 'error',
277
+ error: error
278
+ };
279
+ }
280
+ },
281
+
282
+ /**
283
+ * Get session info
284
+ */
285
+ getSession() {
286
+ return session.getInfo();
287
+ },
288
+
289
+ /**
290
+ * Clean up resources
291
+ */
292
+ async close() {
293
+ try {
294
+ // Close readline interface first to remove event listeners
295
+ if (stdoutReader) {
296
+ stdoutReader.close();
297
+ if (debug) {
298
+ console.log('[DEBUG] Closed stdout reader');
299
+ }
300
+ }
301
+
302
+ // Clear all pending requests and event handlers
303
+ pendingRequests.clear();
304
+ eventHandlers.clear();
305
+
306
+ // Kill Codex process
307
+ if (codexProcess && !codexProcess.killed) {
308
+ codexProcess.kill();
309
+ if (debug) {
310
+ console.log('[DEBUG] Killed Codex MCP server process');
311
+ }
312
+ }
313
+
314
+ // Stop Probe MCP server
315
+ if (mcpServer) {
316
+ await mcpServer.stop();
317
+ if (debug) {
318
+ console.log('[DEBUG] Stopped Probe MCP server');
319
+ }
320
+ }
321
+
322
+ if (debug) {
323
+ console.log('[DEBUG] Engine closed, session:', session.id);
324
+ }
325
+ } catch (error) {
326
+ if (debug) {
327
+ console.error('[DEBUG] Error during cleanup:', error.message);
328
+ }
329
+ }
330
+ }
331
+ };
332
+ }
333
+
334
+ /**
335
+ * Combine prompts intelligently
336
+ */
337
+ function combinePrompts(systemPrompt, customPrompt, agent) {
338
+ if (!systemPrompt && customPrompt) {
339
+ return customPrompt;
340
+ }
341
+
342
+ if (systemPrompt && customPrompt) {
343
+ return systemPrompt + '\n\n## Additional Instructions\n' + customPrompt;
344
+ }
345
+
346
+ return systemPrompt || '';
347
+ }
@@ -9,42 +9,7 @@ import path from 'path';
9
9
  import os from 'os';
10
10
  import { EventEmitter } from 'events';
11
11
  import { BuiltInMCPServer } from '../mcp/built-in-server.js';
12
-
13
- /**
14
- * Session manager for Claude Code conversations
15
- */
16
- class ClaudeSession {
17
- constructor(id, debug = false) {
18
- this.id = id;
19
- this.conversationId = null;
20
- this.messageCount = 0;
21
- this.debug = debug;
22
- }
23
-
24
- /**
25
- * Update session with Claude's conversation ID
26
- */
27
- setConversationId(convId) {
28
- this.conversationId = convId;
29
- if (this.debug) {
30
- console.log(`[Session ${this.id}] Conversation ID: ${convId}`);
31
- }
32
- }
33
-
34
- /**
35
- * Get resume arguments for continuing conversation
36
- */
37
- getResumeArgs() {
38
- if (this.conversationId && this.messageCount > 0) {
39
- return ['--resume', this.conversationId];
40
- }
41
- return [];
42
- }
43
-
44
- incrementMessageCount() {
45
- this.messageCount++;
46
- }
47
- }
12
+ import { Session } from '../shared/Session.js';
48
13
 
49
14
  /**
50
15
  * Enhanced Claude Code Engine
@@ -53,7 +18,7 @@ export async function createEnhancedClaudeCLIEngine(options = {}) {
53
18
  const { agent, systemPrompt, customPrompt, debug, sessionId, allowedTools } = options;
54
19
 
55
20
  // Create or reuse session
56
- const session = new ClaudeSession(
21
+ const session = new Session(
57
22
  sessionId || randomBytes(8).toString('hex'),
58
23
  debug
59
24
  );
@@ -335,11 +300,7 @@ export async function createEnhancedClaudeCLIEngine(options = {}) {
335
300
  * Get session info
336
301
  */
337
302
  getSession() {
338
- return {
339
- id: session.id,
340
- conversationId: session.conversationId,
341
- messageCount: session.messageCount
342
- };
303
+ return session.getInfo();
343
304
  },
344
305
 
345
306
  /**