@probelabs/probe 0.6.0-rc121 → 0.6.0-rc123
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/README.md +36 -2
- package/build/agent/ProbeAgent.js +50 -14
- package/build/agent/index.js +18689 -33570
- package/build/agent/mcp/client.js +69 -15
- package/build/agent/mcp/config.js +8 -8
- package/build/agent/mcp/xmlBridge.js +52 -7
- package/build/agent/schemaUtils.js +28 -15
- package/build/grep.js +152 -0
- package/build/index.js +2 -0
- package/build/mcp/index.js +105 -87
- package/build/mcp/index.ts +120 -97
- package/cjs/agent/ProbeAgent.cjs +13913 -2703
- package/cjs/index.cjs +23926 -12658
- package/index.d.ts +45 -0
- package/package.json +2 -2
- package/src/agent/ProbeAgent.js +50 -14
- package/src/agent/index.js +1 -1
- package/src/agent/mcp/client.js +69 -15
- package/src/agent/mcp/config.js +8 -8
- package/src/agent/mcp/xmlBridge.js +52 -7
- package/src/agent/schemaUtils.js +28 -15
- package/src/grep.js +152 -0
- package/src/index.js +2 -0
- package/src/mcp/index.ts +120 -97
|
@@ -128,14 +128,30 @@ export class MCPClientManager {
|
|
|
128
128
|
this.config = config || loadMCPConfiguration();
|
|
129
129
|
const servers = parseEnabledServers(this.config);
|
|
130
130
|
|
|
131
|
+
// Always log the number of servers found
|
|
132
|
+
console.error(`[MCP INFO] Found ${servers.length} enabled MCP server${servers.length !== 1 ? 's' : ''}`);
|
|
133
|
+
|
|
134
|
+
if (servers.length === 0) {
|
|
135
|
+
console.error('[MCP INFO] No MCP servers configured or enabled');
|
|
136
|
+
console.error('[MCP INFO] 0 MCP tools available');
|
|
137
|
+
return {
|
|
138
|
+
connected: 0,
|
|
139
|
+
total: 0,
|
|
140
|
+
tools: []
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
131
144
|
if (this.debug) {
|
|
132
|
-
console.error(
|
|
145
|
+
console.error('[MCP DEBUG] Server details:');
|
|
146
|
+
servers.forEach(server => {
|
|
147
|
+
console.error(`[MCP DEBUG] - ${server.name} (${server.transport})`);
|
|
148
|
+
});
|
|
133
149
|
}
|
|
134
150
|
|
|
135
151
|
// Connect to each enabled server
|
|
136
152
|
const connectionPromises = servers.map(server =>
|
|
137
153
|
this.connectToServer(server).catch(error => {
|
|
138
|
-
console.error(`[MCP] Failed to connect to ${server.name}:`, error.message);
|
|
154
|
+
console.error(`[MCP ERROR] Failed to connect to ${server.name}:`, error.message);
|
|
139
155
|
return null;
|
|
140
156
|
})
|
|
141
157
|
);
|
|
@@ -143,9 +159,23 @@ export class MCPClientManager {
|
|
|
143
159
|
const results = await Promise.all(connectionPromises);
|
|
144
160
|
const connectedCount = results.filter(Boolean).length;
|
|
145
161
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.error(`[MCP]
|
|
162
|
+
// Always log connection results
|
|
163
|
+
if (connectedCount === 0) {
|
|
164
|
+
console.error(`[MCP ERROR] Failed to connect to all ${servers.length} server${servers.length !== 1 ? 's' : ''}`);
|
|
165
|
+
console.error('[MCP INFO] 0 MCP tools available');
|
|
166
|
+
} else if (connectedCount < servers.length) {
|
|
167
|
+
console.error(`[MCP INFO] Successfully connected to ${connectedCount}/${servers.length} servers`);
|
|
168
|
+
console.error(`[MCP INFO] ${this.tools.size} MCP tool${this.tools.size !== 1 ? 's' : ''} available`);
|
|
169
|
+
} else {
|
|
170
|
+
console.error(`[MCP INFO] Successfully connected to all ${connectedCount} server${connectedCount !== 1 ? 's' : ''}`);
|
|
171
|
+
console.error(`[MCP INFO] ${this.tools.size} MCP tool${this.tools.size !== 1 ? 's' : ''} available`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (this.debug && this.tools.size > 0) {
|
|
175
|
+
console.error('[MCP DEBUG] Available tools:');
|
|
176
|
+
Array.from(this.tools.keys()).forEach(toolName => {
|
|
177
|
+
console.error(`[MCP DEBUG] - ${toolName}`);
|
|
178
|
+
});
|
|
149
179
|
}
|
|
150
180
|
|
|
151
181
|
return {
|
|
@@ -164,7 +194,7 @@ export class MCPClientManager {
|
|
|
164
194
|
|
|
165
195
|
try {
|
|
166
196
|
if (this.debug) {
|
|
167
|
-
console.error(`[MCP] Connecting to ${name} via ${serverConfig.transport}...`);
|
|
197
|
+
console.error(`[MCP DEBUG] Connecting to ${name} via ${serverConfig.transport}...`);
|
|
168
198
|
}
|
|
169
199
|
|
|
170
200
|
// Create transport
|
|
@@ -193,6 +223,7 @@ export class MCPClientManager {
|
|
|
193
223
|
|
|
194
224
|
// Fetch and register tools
|
|
195
225
|
const toolsResponse = await client.listTools();
|
|
226
|
+
const toolCount = toolsResponse?.tools?.length || 0;
|
|
196
227
|
|
|
197
228
|
if (toolsResponse && toolsResponse.tools) {
|
|
198
229
|
for (const tool of toolsResponse.tools) {
|
|
@@ -205,18 +236,19 @@ export class MCPClientManager {
|
|
|
205
236
|
});
|
|
206
237
|
|
|
207
238
|
if (this.debug) {
|
|
208
|
-
console.error(`[MCP]
|
|
239
|
+
console.error(`[MCP DEBUG] Registered tool: ${qualifiedName}`);
|
|
209
240
|
}
|
|
210
241
|
}
|
|
211
242
|
}
|
|
212
243
|
|
|
213
|
-
|
|
214
|
-
console.error(`[MCP] Connected to ${name} with ${toolsResponse?.tools?.length || 0} tools`);
|
|
215
|
-
}
|
|
244
|
+
console.error(`[MCP INFO] Connected to ${name}: ${toolCount} tool${toolCount !== 1 ? 's' : ''} loaded`);
|
|
216
245
|
|
|
217
246
|
return true;
|
|
218
247
|
} catch (error) {
|
|
219
|
-
console.error(`[MCP] Error connecting to ${name}:`, error.message);
|
|
248
|
+
console.error(`[MCP ERROR] Error connecting to ${name}:`, error.message);
|
|
249
|
+
if (this.debug) {
|
|
250
|
+
console.error(`[MCP DEBUG] Full error details:`, error);
|
|
251
|
+
}
|
|
220
252
|
return false;
|
|
221
253
|
}
|
|
222
254
|
}
|
|
@@ -239,7 +271,7 @@ export class MCPClientManager {
|
|
|
239
271
|
|
|
240
272
|
try {
|
|
241
273
|
if (this.debug) {
|
|
242
|
-
console.error(`[MCP] Calling ${toolName} with args:`, args);
|
|
274
|
+
console.error(`[MCP DEBUG] Calling ${toolName} with args:`, JSON.stringify(args, null, 2));
|
|
243
275
|
}
|
|
244
276
|
|
|
245
277
|
// Get timeout from config (default 30 seconds)
|
|
@@ -261,9 +293,16 @@ export class MCPClientManager {
|
|
|
261
293
|
timeoutPromise
|
|
262
294
|
]);
|
|
263
295
|
|
|
296
|
+
if (this.debug) {
|
|
297
|
+
console.error(`[MCP DEBUG] Tool ${toolName} executed successfully`);
|
|
298
|
+
}
|
|
299
|
+
|
|
264
300
|
return result;
|
|
265
301
|
} catch (error) {
|
|
266
|
-
console.error(`[MCP] Error calling tool ${toolName}:`, error);
|
|
302
|
+
console.error(`[MCP ERROR] Error calling tool ${toolName}:`, error.message);
|
|
303
|
+
if (this.debug) {
|
|
304
|
+
console.error(`[MCP DEBUG] Full error details:`, error);
|
|
305
|
+
}
|
|
267
306
|
throw error;
|
|
268
307
|
}
|
|
269
308
|
}
|
|
@@ -316,16 +355,27 @@ export class MCPClientManager {
|
|
|
316
355
|
async disconnect() {
|
|
317
356
|
const disconnectPromises = [];
|
|
318
357
|
|
|
358
|
+
if (this.clients.size === 0) {
|
|
359
|
+
if (this.debug) {
|
|
360
|
+
console.error('[MCP DEBUG] No MCP clients to disconnect');
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (this.debug) {
|
|
366
|
+
console.error(`[MCP DEBUG] Disconnecting from ${this.clients.size} MCP server${this.clients.size !== 1 ? 's' : ''}...`);
|
|
367
|
+
}
|
|
368
|
+
|
|
319
369
|
for (const [name, clientInfo] of this.clients.entries()) {
|
|
320
370
|
disconnectPromises.push(
|
|
321
371
|
clientInfo.client.close()
|
|
322
372
|
.then(() => {
|
|
323
373
|
if (this.debug) {
|
|
324
|
-
console.error(`[MCP] Disconnected from ${name}`);
|
|
374
|
+
console.error(`[MCP DEBUG] Disconnected from ${name}`);
|
|
325
375
|
}
|
|
326
376
|
})
|
|
327
377
|
.catch(error => {
|
|
328
|
-
console.error(`[MCP] Error disconnecting from ${name}:`, error);
|
|
378
|
+
console.error(`[MCP ERROR] Error disconnecting from ${name}:`, error.message);
|
|
329
379
|
})
|
|
330
380
|
);
|
|
331
381
|
}
|
|
@@ -333,6 +383,10 @@ export class MCPClientManager {
|
|
|
333
383
|
await Promise.all(disconnectPromises);
|
|
334
384
|
this.clients.clear();
|
|
335
385
|
this.tools.clear();
|
|
386
|
+
|
|
387
|
+
if (this.debug) {
|
|
388
|
+
console.error('[MCP DEBUG] All MCP connections closed');
|
|
389
|
+
}
|
|
336
390
|
}
|
|
337
391
|
}
|
|
338
392
|
|
|
@@ -51,8 +51,8 @@ export function loadMCPConfigurationFromPath(configPath) {
|
|
|
51
51
|
const content = readFileSync(configPath, 'utf8');
|
|
52
52
|
const config = JSON.parse(content);
|
|
53
53
|
|
|
54
|
-
if (process.env.DEBUG === '1') {
|
|
55
|
-
console.error(`[MCP] Loaded configuration from: ${configPath}`);
|
|
54
|
+
if (process.env.DEBUG === '1' || process.env.DEBUG_MCP === '1') {
|
|
55
|
+
console.error(`[MCP DEBUG] Loaded configuration from: ${configPath}`);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// Merge with environment variable overrides
|
|
@@ -94,12 +94,12 @@ export function loadMCPConfiguration() {
|
|
|
94
94
|
try {
|
|
95
95
|
const content = readFileSync(configPath, 'utf8');
|
|
96
96
|
config = JSON.parse(content);
|
|
97
|
-
if (process.env.DEBUG === '1') {
|
|
98
|
-
console.error(`[MCP] Loaded configuration from: ${configPath}`);
|
|
97
|
+
if (process.env.DEBUG === '1' || process.env.DEBUG_MCP === '1') {
|
|
98
|
+
console.error(`[MCP DEBUG] Loaded configuration from: ${configPath}`);
|
|
99
99
|
}
|
|
100
100
|
break;
|
|
101
101
|
} catch (error) {
|
|
102
|
-
console.error(`[MCP] Failed to parse config from ${configPath}:`, error.message);
|
|
102
|
+
console.error(`[MCP ERROR] Failed to parse config from ${configPath}:`, error.message);
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
}
|
|
@@ -209,12 +209,12 @@ export function parseEnabledServers(config) {
|
|
|
209
209
|
// Validate required fields based on transport
|
|
210
210
|
if (server.transport === 'stdio') {
|
|
211
211
|
if (!server.command) {
|
|
212
|
-
console.error(`[MCP] Server ${name} missing required 'command' for stdio transport`);
|
|
212
|
+
console.error(`[MCP ERROR] Server ${name} missing required 'command' for stdio transport`);
|
|
213
213
|
continue;
|
|
214
214
|
}
|
|
215
215
|
} else if (['websocket', 'sse', 'http'].includes(server.transport)) {
|
|
216
216
|
if (!server.url) {
|
|
217
|
-
console.error(`[MCP] Server ${name} missing required 'url' for ${server.transport} transport`);
|
|
217
|
+
console.error(`[MCP ERROR] Server ${name} missing required 'url' for ${server.transport} transport`);
|
|
218
218
|
continue;
|
|
219
219
|
}
|
|
220
220
|
}
|
|
@@ -301,7 +301,7 @@ export function saveConfig(config, path) {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
writeFileSync(path, JSON.stringify(config, null, 2), 'utf8');
|
|
304
|
-
console.
|
|
304
|
+
console.error(`[MCP INFO] Configuration saved to: ${path}`);
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
export default {
|
|
@@ -141,23 +141,44 @@ export class MCPXmlBridge {
|
|
|
141
141
|
|
|
142
142
|
if (!config) {
|
|
143
143
|
// No config provided - fall back to auto-discovery for backward compatibility
|
|
144
|
+
if (this.debug) {
|
|
145
|
+
console.error('[MCP DEBUG] No config provided, attempting auto-discovery...');
|
|
146
|
+
}
|
|
144
147
|
mcpConfigs = loadMCPConfiguration();
|
|
148
|
+
|
|
149
|
+
// Check if auto-discovery found anything
|
|
150
|
+
if (!mcpConfigs || !mcpConfigs.mcpServers || Object.keys(mcpConfigs.mcpServers).length === 0) {
|
|
151
|
+
console.error('[MCP WARNING] MCP enabled but no configuration found');
|
|
152
|
+
console.error('[MCP INFO] To use MCP, provide configuration via:');
|
|
153
|
+
console.error('[MCP INFO] - mcpConfig option when creating ProbeAgent');
|
|
154
|
+
console.error('[MCP INFO] - mcpConfigPath option pointing to a config file');
|
|
155
|
+
console.error('[MCP INFO] - Config file in standard locations (~/.mcp/config.json, etc.)');
|
|
156
|
+
console.error('[MCP INFO] - Environment variable MCP_CONFIG_PATH');
|
|
157
|
+
}
|
|
145
158
|
} else if (Array.isArray(config)) {
|
|
146
159
|
// Deprecated: Array of server configs (backward compatibility)
|
|
160
|
+
if (this.debug) {
|
|
161
|
+
console.error('[MCP DEBUG] Using deprecated array config format (consider using mcpConfig object)');
|
|
162
|
+
}
|
|
147
163
|
mcpConfigs = { mcpServers: config };
|
|
148
164
|
} else {
|
|
149
165
|
// New: Full config object provided directly
|
|
166
|
+
if (this.debug) {
|
|
167
|
+
console.error('[MCP DEBUG] Using provided MCP config object');
|
|
168
|
+
}
|
|
150
169
|
mcpConfigs = config;
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
if (!mcpConfigs || !mcpConfigs.mcpServers || Object.keys(mcpConfigs.mcpServers).length === 0) {
|
|
154
|
-
|
|
155
|
-
console.error('[MCP] No MCP servers configured');
|
|
156
|
-
}
|
|
173
|
+
console.error('[MCP INFO] 0 MCP tools available');
|
|
157
174
|
return;
|
|
158
175
|
}
|
|
159
176
|
|
|
160
177
|
try {
|
|
178
|
+
if (this.debug) {
|
|
179
|
+
console.error('[MCP DEBUG] Initializing MCP client manager...');
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
// Initialize the MCP client manager
|
|
162
183
|
this.mcpManager = new MCPClientManager({ debug: this.debug });
|
|
163
184
|
const result = await this.mcpManager.initialize(mcpConfigs);
|
|
@@ -165,17 +186,27 @@ export class MCPXmlBridge {
|
|
|
165
186
|
// Get tools from the manager
|
|
166
187
|
const vercelTools = this.mcpManager.getVercelTools();
|
|
167
188
|
this.mcpTools = vercelTools;
|
|
189
|
+
const toolCount = Object.keys(vercelTools).length;
|
|
168
190
|
|
|
169
191
|
// Generate XML definitions for all tools
|
|
170
192
|
for (const [name, tool] of Object.entries(vercelTools)) {
|
|
171
193
|
this.xmlDefinitions[name] = mcpToolToXmlDefinition(name, tool);
|
|
172
194
|
}
|
|
173
195
|
|
|
174
|
-
if (
|
|
175
|
-
console.error(
|
|
196
|
+
if (toolCount === 0) {
|
|
197
|
+
console.error('[MCP INFO] MCP initialization complete: 0 tools loaded');
|
|
198
|
+
} else {
|
|
199
|
+
console.error(`[MCP INFO] MCP initialization complete: ${toolCount} tool${toolCount !== 1 ? 's' : ''} loaded from ${result.connected} server${result.connected !== 1 ? 's' : ''}`);
|
|
200
|
+
|
|
201
|
+
if (this.debug) {
|
|
202
|
+
console.error('[MCP DEBUG] Tool definitions generated for XML bridge');
|
|
203
|
+
}
|
|
176
204
|
}
|
|
177
205
|
} catch (error) {
|
|
178
|
-
console.error('[MCP] Failed to initialize MCP connections:', error);
|
|
206
|
+
console.error('[MCP ERROR] Failed to initialize MCP connections:', error.message);
|
|
207
|
+
if (this.debug) {
|
|
208
|
+
console.error('[MCP DEBUG] Full error details:', error);
|
|
209
|
+
}
|
|
179
210
|
}
|
|
180
211
|
}
|
|
181
212
|
|
|
@@ -204,28 +235,42 @@ export class MCPXmlBridge {
|
|
|
204
235
|
const parsed = parseXmlMcpToolCall(xmlString, this.getToolNames());
|
|
205
236
|
|
|
206
237
|
if (!parsed) {
|
|
238
|
+
console.error('[MCP ERROR] No valid MCP tool call found in XML');
|
|
207
239
|
throw new Error('No valid MCP tool call found in XML');
|
|
208
240
|
}
|
|
209
241
|
|
|
210
242
|
const { toolName, params } = parsed;
|
|
211
243
|
|
|
212
244
|
if (this.debug) {
|
|
213
|
-
console.error(`[MCP] Executing MCP tool: ${toolName}
|
|
245
|
+
console.error(`[MCP DEBUG] Executing MCP tool: ${toolName}`);
|
|
246
|
+
console.error(`[MCP DEBUG] Parameters:`, JSON.stringify(params, null, 2));
|
|
214
247
|
}
|
|
215
248
|
|
|
216
249
|
const tool = this.mcpTools[toolName];
|
|
217
250
|
if (!tool) {
|
|
251
|
+
console.error(`[MCP ERROR] Unknown MCP tool: ${toolName}`);
|
|
252
|
+
console.error(`[MCP ERROR] Available tools: ${this.getToolNames().join(', ')}`);
|
|
218
253
|
throw new Error(`Unknown MCP tool: ${toolName}`);
|
|
219
254
|
}
|
|
220
255
|
|
|
221
256
|
try {
|
|
222
257
|
const result = await tool.execute(params);
|
|
258
|
+
|
|
259
|
+
if (this.debug) {
|
|
260
|
+
console.error(`[MCP DEBUG] Tool ${toolName} executed successfully`);
|
|
261
|
+
}
|
|
262
|
+
|
|
223
263
|
return {
|
|
224
264
|
success: true,
|
|
225
265
|
toolName,
|
|
226
266
|
result
|
|
227
267
|
};
|
|
228
268
|
} catch (error) {
|
|
269
|
+
console.error(`[MCP ERROR] Tool ${toolName} execution failed:`, error.message);
|
|
270
|
+
if (this.debug) {
|
|
271
|
+
console.error(`[MCP DEBUG] Full error details:`, error);
|
|
272
|
+
}
|
|
273
|
+
|
|
229
274
|
return {
|
|
230
275
|
success: false,
|
|
231
276
|
toolName,
|
|
@@ -40,7 +40,7 @@ export function decodeHtmlEntities(text) {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Clean AI response by extracting JSON content when response contains JSON
|
|
43
|
-
* Only processes responses that contain JSON structures { or [
|
|
43
|
+
* Only processes responses that contain JSON structures { or [
|
|
44
44
|
* @param {string} response - Raw AI response
|
|
45
45
|
* @returns {string} - Cleaned response with JSON boundaries extracted if applicable
|
|
46
46
|
*/
|
|
@@ -50,34 +50,47 @@ export function cleanSchemaResponse(response) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const trimmed = response.trim();
|
|
53
|
-
|
|
54
|
-
// First, look for JSON after code block markers
|
|
53
|
+
|
|
54
|
+
// First, look for JSON after code block markers - similar to mermaid extraction
|
|
55
|
+
// Try with json language specifier
|
|
56
|
+
const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
|
|
57
|
+
if (jsonBlockMatch) {
|
|
58
|
+
return jsonBlockMatch[1].trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Try any code block with JSON content
|
|
62
|
+
const anyBlockMatch = trimmed.match(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/);
|
|
63
|
+
if (anyBlockMatch) {
|
|
64
|
+
return anyBlockMatch[1].trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Legacy patterns for more specific matching
|
|
55
68
|
const codeBlockPatterns = [
|
|
56
69
|
/```json\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
|
|
57
70
|
/```\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
|
|
58
71
|
/`([{\[][\s\S]*?[}\]])`/
|
|
59
72
|
];
|
|
60
|
-
|
|
73
|
+
|
|
61
74
|
for (const pattern of codeBlockPatterns) {
|
|
62
75
|
const match = trimmed.match(pattern);
|
|
63
76
|
if (match) {
|
|
64
77
|
return match[1].trim();
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
|
-
|
|
80
|
+
|
|
68
81
|
// Look for code block start followed immediately by JSON
|
|
69
82
|
const codeBlockStartPattern = /```(?:json)?\s*\n?\s*([{\[])/;
|
|
70
83
|
const codeBlockMatch = trimmed.match(codeBlockStartPattern);
|
|
71
|
-
|
|
84
|
+
|
|
72
85
|
if (codeBlockMatch) {
|
|
73
86
|
const startIndex = codeBlockMatch.index + codeBlockMatch[0].length - 1; // Position of the bracket
|
|
74
|
-
|
|
87
|
+
|
|
75
88
|
// Find the matching closing bracket
|
|
76
89
|
const openChar = codeBlockMatch[1];
|
|
77
90
|
const closeChar = openChar === '{' ? '}' : ']';
|
|
78
91
|
let bracketCount = 1;
|
|
79
92
|
let endIndex = startIndex + 1;
|
|
80
|
-
|
|
93
|
+
|
|
81
94
|
while (endIndex < trimmed.length && bracketCount > 0) {
|
|
82
95
|
const char = trimmed[endIndex];
|
|
83
96
|
if (char === openChar) {
|
|
@@ -87,36 +100,36 @@ export function cleanSchemaResponse(response) {
|
|
|
87
100
|
}
|
|
88
101
|
endIndex++;
|
|
89
102
|
}
|
|
90
|
-
|
|
103
|
+
|
|
91
104
|
if (bracketCount === 0) {
|
|
92
105
|
return trimmed.substring(startIndex, endIndex);
|
|
93
106
|
}
|
|
94
107
|
}
|
|
95
|
-
|
|
108
|
+
|
|
96
109
|
// Fallback: Find JSON boundaries anywhere in the text
|
|
97
110
|
const firstBracket = Math.min(
|
|
98
111
|
trimmed.indexOf('{') >= 0 ? trimmed.indexOf('{') : Infinity,
|
|
99
112
|
trimmed.indexOf('[') >= 0 ? trimmed.indexOf('[') : Infinity
|
|
100
113
|
);
|
|
101
|
-
|
|
114
|
+
|
|
102
115
|
const lastBracket = Math.max(
|
|
103
116
|
trimmed.lastIndexOf('}'),
|
|
104
117
|
trimmed.lastIndexOf(']')
|
|
105
118
|
);
|
|
106
|
-
|
|
119
|
+
|
|
107
120
|
// Only extract if we found valid JSON boundaries
|
|
108
121
|
if (firstBracket < Infinity && lastBracket >= 0 && firstBracket < lastBracket) {
|
|
109
122
|
// Check if the response likely starts with JSON (directly or after minimal content)
|
|
110
123
|
const beforeFirstBracket = trimmed.substring(0, firstBracket).trim();
|
|
111
|
-
|
|
124
|
+
|
|
112
125
|
// If there's minimal content before the first bracket, extract the JSON
|
|
113
|
-
if (beforeFirstBracket === '' ||
|
|
126
|
+
if (beforeFirstBracket === '' ||
|
|
114
127
|
beforeFirstBracket.match(/^```\w*$/) ||
|
|
115
128
|
beforeFirstBracket.split('\n').length <= 2) {
|
|
116
129
|
return trimmed.substring(firstBracket, lastBracket + 1);
|
|
117
130
|
}
|
|
118
131
|
}
|
|
119
|
-
|
|
132
|
+
|
|
120
133
|
return response; // Return original if no extractable JSON found
|
|
121
134
|
}
|
|
122
135
|
|
package/build/grep.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grep functionality for the probe package
|
|
3
|
+
* @module grep
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execFile } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { getBinaryPath } from './utils.js';
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Flag mapping for grep options
|
|
14
|
+
* Maps option keys to command-line flags
|
|
15
|
+
*/
|
|
16
|
+
const GREP_FLAG_MAP = {
|
|
17
|
+
ignoreCase: '-i',
|
|
18
|
+
lineNumbers: '-n',
|
|
19
|
+
count: '-c',
|
|
20
|
+
filesWithMatches: '-l',
|
|
21
|
+
filesWithoutMatches: '-L',
|
|
22
|
+
invertMatch: '-v',
|
|
23
|
+
beforeContext: '-B',
|
|
24
|
+
afterContext: '-A',
|
|
25
|
+
context: '-C',
|
|
26
|
+
noGitignore: '--no-gitignore',
|
|
27
|
+
color: '--color',
|
|
28
|
+
maxCount: '-m'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Standard grep-style search across files (works with any file type, not just code)
|
|
33
|
+
*
|
|
34
|
+
* This provides a cross-platform grep interface that works on any OS and file type.
|
|
35
|
+
* Use this for searching non-code files (logs, config files, text files, etc.) that
|
|
36
|
+
* are not supported by probe's semantic search.
|
|
37
|
+
*
|
|
38
|
+
* For code files, prefer using the `search()` function which provides semantic,
|
|
39
|
+
* AST-aware search capabilities.
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} options - Grep options
|
|
42
|
+
* @param {string} options.pattern - Pattern to search for (regex)
|
|
43
|
+
* @param {string|string[]} options.paths - Path(s) to search in
|
|
44
|
+
* @param {boolean} [options.ignoreCase] - Case-insensitive search (-i)
|
|
45
|
+
* @param {boolean} [options.lineNumbers] - Show line numbers (-n)
|
|
46
|
+
* @param {boolean} [options.count] - Only show count of matches (-c)
|
|
47
|
+
* @param {boolean} [options.filesWithMatches] - Only show filenames with matches (-l)
|
|
48
|
+
* @param {boolean} [options.filesWithoutMatches] - Only show filenames without matches (-L)
|
|
49
|
+
* @param {boolean} [options.invertMatch] - Invert match, show non-matching lines (-v)
|
|
50
|
+
* @param {number} [options.beforeContext] - Lines of context before match (-B)
|
|
51
|
+
* @param {number} [options.afterContext] - Lines of context after match (-A)
|
|
52
|
+
* @param {number} [options.context] - Lines of context before and after match (-C)
|
|
53
|
+
* @param {boolean} [options.noGitignore] - Don't respect .gitignore files (--no-gitignore)
|
|
54
|
+
* @param {string} [options.color] - Colorize output: 'always', 'never', 'auto' (--color)
|
|
55
|
+
* @param {number} [options.maxCount] - Stop after N matches per file (-m)
|
|
56
|
+
* @param {Object} [options.binaryOptions] - Options for getting the binary
|
|
57
|
+
* @param {boolean} [options.binaryOptions.forceDownload] - Force download even if binary exists
|
|
58
|
+
* @param {string} [options.binaryOptions.version] - Specific version to download
|
|
59
|
+
* @returns {Promise<string>} - Grep results as string
|
|
60
|
+
* @throws {Error} If the grep operation fails
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // Search for "error" in log files (case-insensitive)
|
|
64
|
+
* const results = await grep({
|
|
65
|
+
* pattern: 'error',
|
|
66
|
+
* paths: '/var/log',
|
|
67
|
+
* ignoreCase: true,
|
|
68
|
+
* lineNumbers: true
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // Count occurrences of "TODO" in project
|
|
73
|
+
* const count = await grep({
|
|
74
|
+
* pattern: 'TODO',
|
|
75
|
+
* paths: '.',
|
|
76
|
+
* count: true
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // Find files containing "config" with context
|
|
81
|
+
* const matches = await grep({
|
|
82
|
+
* pattern: 'config',
|
|
83
|
+
* paths: '/etc',
|
|
84
|
+
* context: 2,
|
|
85
|
+
* filesWithMatches: true
|
|
86
|
+
* });
|
|
87
|
+
*/
|
|
88
|
+
export async function grep(options) {
|
|
89
|
+
if (!options || !options.pattern) {
|
|
90
|
+
throw new Error('Pattern is required');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!options.paths) {
|
|
94
|
+
throw new Error('Path(s) are required');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Get the binary path
|
|
98
|
+
const binaryPath = await getBinaryPath(options.binaryOptions || {});
|
|
99
|
+
|
|
100
|
+
// Build CLI arguments array for grep subcommand
|
|
101
|
+
// Using an array prevents command injection vulnerabilities
|
|
102
|
+
const cliArgs = ['grep'];
|
|
103
|
+
|
|
104
|
+
// Add flags from GREP_FLAG_MAP
|
|
105
|
+
for (const [key, flag] of Object.entries(GREP_FLAG_MAP)) {
|
|
106
|
+
const value = options[key];
|
|
107
|
+
if (value === undefined || value === null) continue;
|
|
108
|
+
|
|
109
|
+
if (typeof value === 'boolean' && value) {
|
|
110
|
+
// Boolean flag
|
|
111
|
+
cliArgs.push(flag);
|
|
112
|
+
} else if (typeof value === 'number') {
|
|
113
|
+
// Numeric option
|
|
114
|
+
cliArgs.push(flag, String(value));
|
|
115
|
+
} else if (typeof value === 'string') {
|
|
116
|
+
// String option (like color)
|
|
117
|
+
cliArgs.push(flag, value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Add pattern (no need to escape - execFile handles it securely)
|
|
122
|
+
cliArgs.push(options.pattern);
|
|
123
|
+
|
|
124
|
+
// Add paths (can be single string or array)
|
|
125
|
+
const paths = Array.isArray(options.paths) ? options.paths : [options.paths];
|
|
126
|
+
cliArgs.push(...paths);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Use execFile instead of exec to prevent command injection
|
|
130
|
+
// execFile does not spawn a shell and passes arguments as an array
|
|
131
|
+
const { stdout, stderr } = await execFileAsync(binaryPath, cliArgs, {
|
|
132
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
133
|
+
env: {
|
|
134
|
+
...process.env,
|
|
135
|
+
// Disable colors in stderr for cleaner output
|
|
136
|
+
NO_COLOR: '1'
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Return stdout (grep results)
|
|
141
|
+
return stdout;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Grep exit code 1 means "no matches found", which is not an error
|
|
144
|
+
if (error.code === 1 && !error.stderr) {
|
|
145
|
+
return error.stdout || '';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Other errors are real failures
|
|
149
|
+
const errorMessage = error.stderr || error.message || 'Unknown error';
|
|
150
|
+
throw new Error(`Grep failed: ${errorMessage}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
package/build/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { search } from './search.js';
|
|
11
11
|
import { query } from './query.js';
|
|
12
12
|
import { extract } from './extract.js';
|
|
13
|
+
import { grep } from './grep.js';
|
|
13
14
|
import { delegate } from './delegate.js';
|
|
14
15
|
import { getBinaryPath, setBinaryPath } from './utils.js';
|
|
15
16
|
import * as tools from './tools/index.js';
|
|
@@ -40,6 +41,7 @@ export {
|
|
|
40
41
|
search,
|
|
41
42
|
query,
|
|
42
43
|
extract,
|
|
44
|
+
grep,
|
|
43
45
|
delegate,
|
|
44
46
|
getBinaryPath,
|
|
45
47
|
setBinaryPath,
|