@probelabs/probe 0.6.0-rc173 → 0.6.0-rc175
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/build/agent/ProbeAgent.d.ts +6 -0
- package/build/agent/ProbeAgent.js +69 -1
- package/build/agent/index.js +418 -281
- package/build/agent/probeTool.js +4 -13
- package/build/downloader.js +6 -2
- package/build/extract.js +13 -5
- package/build/extractor.js +6 -2
- package/build/query.js +9 -2
- package/build/search.js +10 -2
- package/build/tools/bash.js +5 -5
- package/build/tools/edit.js +7 -7
- package/build/tools/langchain.js +12 -3
- package/build/tools/vercel.js +25 -29
- package/build/utils/file-lister.js +9 -2
- package/build/utils/path-validation.js +58 -0
- package/build/utils/symlink-utils.js +63 -0
- package/cjs/agent/ProbeAgent.cjs +515 -376
- package/cjs/index.cjs +535 -387
- package/index.d.ts +12 -1
- package/package.json +2 -2
- package/src/agent/ProbeAgent.d.ts +6 -0
- package/src/agent/ProbeAgent.js +69 -1
- package/src/agent/probeTool.js +4 -13
- package/src/downloader.js +6 -2
- package/src/extract.js +13 -5
- package/src/extractor.js +6 -2
- package/src/query.js +9 -2
- package/src/search.js +10 -2
- package/src/tools/bash.js +5 -5
- package/src/tools/edit.js +7 -7
- package/src/tools/langchain.js +12 -3
- package/src/tools/vercel.js +25 -29
- package/src/utils/file-lister.js +9 -2
- package/src/utils/path-validation.js +58 -0
- package/src/utils/symlink-utils.js +63 -0
package/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface ProbeAgentOptions {
|
|
|
17
17
|
allowEdit?: boolean;
|
|
18
18
|
/** Search directory path */
|
|
19
19
|
path?: string;
|
|
20
|
+
/** Working directory for resolving relative paths (independent of allowedFolders security) */
|
|
21
|
+
cwd?: string;
|
|
20
22
|
/** Force specific AI provider */
|
|
21
23
|
provider?: 'anthropic' | 'openai' | 'google';
|
|
22
24
|
/** Override model name */
|
|
@@ -41,6 +43,12 @@ export interface ProbeAgentOptions {
|
|
|
41
43
|
allowedTools?: string[] | null;
|
|
42
44
|
/** Convenience flag to disable all tools (equivalent to allowedTools: []). Takes precedence over allowedTools if set. */
|
|
43
45
|
disableTools?: boolean;
|
|
46
|
+
/** Disable automatic mermaid diagram validation and fixing */
|
|
47
|
+
disableMermaidValidation?: boolean;
|
|
48
|
+
/** Disable automatic JSON validation and fixing (prevents infinite recursion in JsonFixingAgent) */
|
|
49
|
+
disableJsonValidation?: boolean;
|
|
50
|
+
/** Custom prompt to run after attempt_completion for validation/review (runs before mermaid/JSON validation) */
|
|
51
|
+
completionPrompt?: string;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
/**
|
|
@@ -246,7 +254,10 @@ export declare class ProbeAgent {
|
|
|
246
254
|
|
|
247
255
|
/** Allowed search folders */
|
|
248
256
|
readonly allowedFolders: string[];
|
|
249
|
-
|
|
257
|
+
|
|
258
|
+
/** Working directory for resolving relative paths */
|
|
259
|
+
readonly cwd: string | null;
|
|
260
|
+
|
|
250
261
|
/** Debug mode status */
|
|
251
262
|
readonly debug: boolean;
|
|
252
263
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@probelabs/probe",
|
|
3
|
-
"version": "0.6.0-
|
|
3
|
+
"version": "0.6.0-rc175",
|
|
4
4
|
"description": "Node.js wrapper for the probe code search tool",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"@ai-sdk/openai": "^2.0.10",
|
|
80
80
|
"@anthropic-ai/claude-agent-sdk": "^0.1.46",
|
|
81
81
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
82
|
-
"@probelabs/maid": "^0.0.
|
|
82
|
+
"@probelabs/maid": "^0.0.22",
|
|
83
83
|
"adm-zip": "^0.5.16",
|
|
84
84
|
"ai": "^5.0.0",
|
|
85
85
|
"ajv": "^8.17.1",
|
|
@@ -45,6 +45,12 @@ export interface ProbeAgentOptions {
|
|
|
45
45
|
retry?: RetryOptions;
|
|
46
46
|
/** Fallback configuration for multi-provider support */
|
|
47
47
|
fallback?: FallbackOptions | { auto: boolean };
|
|
48
|
+
/** Disable automatic mermaid diagram validation and fixing */
|
|
49
|
+
disableMermaidValidation?: boolean;
|
|
50
|
+
/** Disable automatic JSON validation and fixing (prevents infinite recursion in JsonFixingAgent) */
|
|
51
|
+
disableJsonValidation?: boolean;
|
|
52
|
+
/** Custom prompt to run after attempt_completion for validation/review (runs before mermaid/JSON validation) */
|
|
53
|
+
completionPrompt?: string;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
/**
|
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -95,6 +95,7 @@ export class ProbeAgent {
|
|
|
95
95
|
* @param {boolean} [options.allowEdit=false] - Allow the use of the 'implement' tool
|
|
96
96
|
* @param {boolean} [options.enableDelegate=false] - Enable the delegate tool for task distribution to subagents
|
|
97
97
|
* @param {string} [options.path] - Search directory path
|
|
98
|
+
* @param {string} [options.cwd] - Working directory for resolving relative paths (independent of allowedFolders)
|
|
98
99
|
* @param {string} [options.provider] - Force specific AI provider
|
|
99
100
|
* @param {string} [options.model] - Override model name
|
|
100
101
|
* @param {boolean} [options.debug] - Enable debug mode
|
|
@@ -123,6 +124,7 @@ export class ProbeAgent {
|
|
|
123
124
|
* @param {Array<Object>} [options.fallback.providers] - List of provider configurations for custom fallback
|
|
124
125
|
* @param {boolean} [options.fallback.stopOnSuccess=true] - Stop on first success
|
|
125
126
|
* @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
|
|
127
|
+
* @param {string} [options.completionPrompt] - Custom prompt to run after attempt_completion for validation/review (runs before mermaid/JSON validation)
|
|
126
128
|
*/
|
|
127
129
|
constructor(options = {}) {
|
|
128
130
|
// Basic configuration
|
|
@@ -147,6 +149,9 @@ export class ProbeAgent {
|
|
|
147
149
|
this.disableMermaidValidation = !!options.disableMermaidValidation;
|
|
148
150
|
this.disableJsonValidation = !!options.disableJsonValidation;
|
|
149
151
|
|
|
152
|
+
// Completion prompt for post-completion validation/review
|
|
153
|
+
this.completionPrompt = options.completionPrompt || null;
|
|
154
|
+
|
|
150
155
|
// Tool filtering configuration
|
|
151
156
|
// Parse allowedTools option: ['*'] = all tools, [] or null = no tools, ['tool1', 'tool2'] = specific tools
|
|
152
157
|
// Supports exclusion with '!' prefix: ['*', '!bash'] = all tools except bash
|
|
@@ -180,6 +185,9 @@ export class ProbeAgent {
|
|
|
180
185
|
this.allowedFolders = [process.cwd()];
|
|
181
186
|
}
|
|
182
187
|
|
|
188
|
+
// Working directory for resolving relative paths (separate from allowedFolders security)
|
|
189
|
+
this.cwd = options.cwd || null;
|
|
190
|
+
|
|
183
191
|
// API configuration
|
|
184
192
|
this.clientApiProvider = options.provider || null;
|
|
185
193
|
this.clientApiModel = options.model || null;
|
|
@@ -425,7 +433,8 @@ export class ProbeAgent {
|
|
|
425
433
|
const configOptions = {
|
|
426
434
|
sessionId: this.sessionId,
|
|
427
435
|
debug: this.debug,
|
|
428
|
-
|
|
436
|
+
// Use explicit cwd if set, otherwise fall back to first allowed folder
|
|
437
|
+
cwd: this.cwd || (this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd()),
|
|
429
438
|
allowedFolders: this.allowedFolders,
|
|
430
439
|
outline: this.outline,
|
|
431
440
|
allowEdit: this.allowEdit,
|
|
@@ -2743,6 +2752,63 @@ IMPORTANT: When using <attempt_complete>, this must be the ONLY content in your
|
|
|
2743
2752
|
// Continue even if storage fails
|
|
2744
2753
|
}
|
|
2745
2754
|
|
|
2755
|
+
// Completion prompt handling - run a follow-up prompt after attempt_completion for validation/review
|
|
2756
|
+
// This runs BEFORE mermaid validation and JSON schema validation
|
|
2757
|
+
// Skip if we're already in a completion prompt follow-up call or if no completion prompt is configured
|
|
2758
|
+
if (completionAttempted && this.completionPrompt && !options._completionPromptProcessed) {
|
|
2759
|
+
if (this.debug) {
|
|
2760
|
+
console.log('[DEBUG] Running completion prompt for post-completion validation/review...');
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
try {
|
|
2764
|
+
// Record completion prompt start in telemetry
|
|
2765
|
+
if (this.tracer) {
|
|
2766
|
+
this.tracer.recordEvent('completion_prompt.started', {
|
|
2767
|
+
'completion_prompt.original_result_length': finalResult?.length || 0
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
// Create the completion prompt with the current result as context
|
|
2772
|
+
const completionPromptMessage = `${this.completionPrompt}
|
|
2773
|
+
|
|
2774
|
+
Here is the result to review:
|
|
2775
|
+
<result>
|
|
2776
|
+
${finalResult}
|
|
2777
|
+
</result>
|
|
2778
|
+
|
|
2779
|
+
After reviewing, provide your final answer using attempt_completion.`;
|
|
2780
|
+
|
|
2781
|
+
// Make a follow-up call with the completion prompt
|
|
2782
|
+
// Pass _completionPromptProcessed to prevent infinite loops
|
|
2783
|
+
const completionResult = await this.answer(completionPromptMessage, [], {
|
|
2784
|
+
...options,
|
|
2785
|
+
_completionPromptProcessed: true
|
|
2786
|
+
});
|
|
2787
|
+
|
|
2788
|
+
// Update finalResult with the result from the completion prompt
|
|
2789
|
+
finalResult = completionResult;
|
|
2790
|
+
|
|
2791
|
+
if (this.debug) {
|
|
2792
|
+
console.log(`[DEBUG] Completion prompt finished. New result length: ${finalResult?.length || 0}`);
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
// Record completion prompt completion in telemetry
|
|
2796
|
+
if (this.tracer) {
|
|
2797
|
+
this.tracer.recordEvent('completion_prompt.completed', {
|
|
2798
|
+
'completion_prompt.final_result_length': finalResult?.length || 0
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
} catch (error) {
|
|
2802
|
+
console.error('[ERROR] Completion prompt failed:', error);
|
|
2803
|
+
// Keep the original result if completion prompt fails
|
|
2804
|
+
if (this.tracer) {
|
|
2805
|
+
this.tracer.recordEvent('completion_prompt.error', {
|
|
2806
|
+
'completion_prompt.error': error.message
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2746
2812
|
// Schema handling - format response according to provided schema
|
|
2747
2813
|
// Skip schema processing if result came from attempt_completion tool
|
|
2748
2814
|
// Don't apply schema formatting if we failed due to max iterations
|
|
@@ -3330,6 +3396,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
3330
3396
|
enableDelegate: this.enableDelegate,
|
|
3331
3397
|
path: this.allowedFolders[0], // Use first allowed folder as primary path
|
|
3332
3398
|
allowedFolders: [...this.allowedFolders],
|
|
3399
|
+
cwd: this.cwd, // Preserve explicit working directory
|
|
3333
3400
|
provider: this.clientApiProvider,
|
|
3334
3401
|
model: this.clientApiModel,
|
|
3335
3402
|
debug: this.debug,
|
|
@@ -3338,6 +3405,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
3338
3405
|
maxIterations: this.maxIterations,
|
|
3339
3406
|
disableMermaidValidation: this.disableMermaidValidation,
|
|
3340
3407
|
disableJsonValidation: this.disableJsonValidation,
|
|
3408
|
+
completionPrompt: this.completionPrompt,
|
|
3341
3409
|
allowedTools: allowedToolsArray,
|
|
3342
3410
|
enableMcp: !!this.mcpBridge,
|
|
3343
3411
|
mcpConfig: this.mcpConfig,
|
package/src/agent/probeTool.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from 'fs';
|
|
|
8
8
|
import { promises as fsPromises } from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { glob } from 'glob';
|
|
11
|
+
import { getEntryType } from '../utils/symlink-utils.js';
|
|
11
12
|
|
|
12
13
|
// Create an event emitter for tool calls (simplified for single-shot operations)
|
|
13
14
|
export const toolCallEmitter = new EventEmitter();
|
|
@@ -287,23 +288,13 @@ export const listFilesTool = {
|
|
|
287
288
|
|
|
288
289
|
// Format the results as ls-style output
|
|
289
290
|
const entries = await Promise.all(files.map(async (file) => {
|
|
290
|
-
const isDirectory = file.isDirectory();
|
|
291
291
|
const fullPath = path.join(targetDir, file.name);
|
|
292
|
-
|
|
293
|
-
let size = 0;
|
|
294
|
-
try {
|
|
295
|
-
const stats = await fsPromises.stat(fullPath);
|
|
296
|
-
size = stats.size;
|
|
297
|
-
} catch (statError) {
|
|
298
|
-
if (debug) {
|
|
299
|
-
console.log(`[DEBUG] Could not stat file ${file.name}:`, statError.message);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
292
|
+
const entryType = await getEntryType(file, fullPath);
|
|
302
293
|
|
|
303
294
|
return {
|
|
304
295
|
name: file.name,
|
|
305
|
-
isDirectory,
|
|
306
|
-
size
|
|
296
|
+
isDirectory: entryType.isDirectory,
|
|
297
|
+
size: entryType.size
|
|
307
298
|
};
|
|
308
299
|
}));
|
|
309
300
|
|
package/src/downloader.js
CHANGED
|
@@ -14,6 +14,7 @@ import os from 'os';
|
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
15
|
import { ensureBinDirectory } from './utils.js';
|
|
16
16
|
import { getPackageBinDir } from './directory-resolver.js';
|
|
17
|
+
import { getEntryType } from './utils/symlink-utils.js';
|
|
17
18
|
|
|
18
19
|
const exec = promisify(execCallback);
|
|
19
20
|
|
|
@@ -770,10 +771,13 @@ async function extractBinary(assetPath, outputDir) {
|
|
|
770
771
|
for (const entry of entries) {
|
|
771
772
|
const fullPath = path.join(dir, entry.name);
|
|
772
773
|
|
|
773
|
-
|
|
774
|
+
// Use shared utility to follow symlinks and get actual target type
|
|
775
|
+
const entryType = await getEntryType(entry, fullPath);
|
|
776
|
+
|
|
777
|
+
if (entryType.isDirectory) {
|
|
774
778
|
const result = await findBinary(fullPath);
|
|
775
779
|
if (result) return result;
|
|
776
|
-
} else if (
|
|
780
|
+
} else if (entryType.isFile) {
|
|
777
781
|
// Check if this is the binary we're looking for
|
|
778
782
|
if (entry.name === binaryName ||
|
|
779
783
|
entry.name === BINARY_NAME ||
|
package/src/extract.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { exec, spawn } from 'child_process';
|
|
7
7
|
import { promisify } from 'util';
|
|
8
8
|
import { getBinaryPath, buildCliArgs, escapeString } from './utils.js';
|
|
9
|
+
import { validateCwdPath } from './utils/path-validation.js';
|
|
9
10
|
|
|
10
11
|
const execAsync = promisify(exec);
|
|
11
12
|
|
|
@@ -27,6 +28,7 @@ const EXTRACT_FLAG_MAP = {
|
|
|
27
28
|
* @param {string[]} [options.files] - Files to extract from (can include line numbers with colon, e.g., "/path/to/file.rs:10")
|
|
28
29
|
* @param {string} [options.inputFile] - Path to a file containing unstructured text to extract file paths from
|
|
29
30
|
* @param {string|Buffer} [options.content] - Content to pipe to stdin (e.g., git diff output). Alternative to inputFile.
|
|
31
|
+
* @param {string} [options.cwd] - Working directory for resolving relative file paths
|
|
30
32
|
* @param {boolean} [options.allowTests] - Include test files
|
|
31
33
|
* @param {number} [options.contextLines] - Number of context lines to include
|
|
32
34
|
* @param {string} [options.format] - Output format ('markdown', 'plain', 'json', 'xml', 'color', 'outline-xml', 'outline-diff')
|
|
@@ -71,6 +73,10 @@ export async function extract(options) {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
// Get the working directory (cwd option sets working directory for relative file resolution)
|
|
77
|
+
// Validate and normalize the path to prevent path traversal attacks
|
|
78
|
+
const cwd = await validateCwdPath(options.cwd);
|
|
79
|
+
|
|
74
80
|
// Create a single log record with all extract parameters (only in debug mode)
|
|
75
81
|
if (process.env.DEBUG === '1') {
|
|
76
82
|
let logMessage = `\nExtract:`;
|
|
@@ -79,6 +85,7 @@ export async function extract(options) {
|
|
|
79
85
|
}
|
|
80
86
|
if (options.inputFile) logMessage += ` inputFile="${options.inputFile}"`;
|
|
81
87
|
if (options.content) logMessage += ` content=(${typeof options.content === 'string' ? options.content.length : options.content.byteLength} bytes)`;
|
|
88
|
+
if (options.cwd) logMessage += ` cwd="${cwd}"`;
|
|
82
89
|
if (options.allowTests) logMessage += " allowTests=true";
|
|
83
90
|
if (options.contextLines) logMessage += ` contextLines=${options.contextLines}`;
|
|
84
91
|
if (options.format) logMessage += ` format=${options.format}`;
|
|
@@ -88,14 +95,14 @@ export async function extract(options) {
|
|
|
88
95
|
|
|
89
96
|
// If content is provided, use spawn with stdin piping
|
|
90
97
|
if (hasContent) {
|
|
91
|
-
return extractWithStdin(binaryPath, cliArgs, options.content, options);
|
|
98
|
+
return extractWithStdin(binaryPath, cliArgs, options.content, options, cwd);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
// Otherwise use exec for simple command execution
|
|
95
102
|
const command = `${binaryPath} extract ${cliArgs.join(' ')}`;
|
|
96
103
|
|
|
97
104
|
try {
|
|
98
|
-
const { stdout, stderr } = await execAsync(command);
|
|
105
|
+
const { stdout, stderr } = await execAsync(command, { cwd });
|
|
99
106
|
|
|
100
107
|
if (stderr) {
|
|
101
108
|
console.error(`stderr: ${stderr}`);
|
|
@@ -104,7 +111,7 @@ export async function extract(options) {
|
|
|
104
111
|
return processExtractOutput(stdout, options);
|
|
105
112
|
} catch (error) {
|
|
106
113
|
// Enhance error message with command details
|
|
107
|
-
const errorMessage = `Error executing extract command: ${error.message}\nCommand: ${command}`;
|
|
114
|
+
const errorMessage = `Error executing extract command: ${error.message}\nCommand: ${command}\nCwd: ${cwd}`;
|
|
108
115
|
throw new Error(errorMessage);
|
|
109
116
|
}
|
|
110
117
|
}
|
|
@@ -113,10 +120,11 @@ export async function extract(options) {
|
|
|
113
120
|
* Extract with content piped to stdin
|
|
114
121
|
* @private
|
|
115
122
|
*/
|
|
116
|
-
function extractWithStdin(binaryPath, cliArgs, content, options) {
|
|
123
|
+
function extractWithStdin(binaryPath, cliArgs, content, options, cwd) {
|
|
117
124
|
return new Promise((resolve, reject) => {
|
|
118
125
|
const childProcess = spawn(binaryPath, ['extract', ...cliArgs], {
|
|
119
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
126
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
+
cwd
|
|
120
128
|
});
|
|
121
129
|
|
|
122
130
|
let stdout = '';
|
package/src/extractor.js
CHANGED
|
@@ -9,6 +9,7 @@ import tar from 'tar';
|
|
|
9
9
|
import AdmZip from 'adm-zip';
|
|
10
10
|
import os from 'os';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import { getEntryType } from './utils/symlink-utils.js';
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -148,10 +149,13 @@ async function findBinary(dir, baseDir, binaryName, isWindows) {
|
|
|
148
149
|
continue;
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
|
|
152
|
+
// Use shared utility to follow symlinks and get actual target type
|
|
153
|
+
const entryType = await getEntryType(entry, fullPath);
|
|
154
|
+
|
|
155
|
+
if (entryType.isDirectory) {
|
|
152
156
|
const result = await findBinary(fullPath, baseDir, binaryName, isWindows);
|
|
153
157
|
if (result) return result;
|
|
154
|
-
} else if (
|
|
158
|
+
} else if (entryType.isFile) {
|
|
155
159
|
// Check if this is the binary we're looking for
|
|
156
160
|
if (entry.name === binaryName ||
|
|
157
161
|
entry.name === BINARY_NAME ||
|
package/src/query.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { exec } from 'child_process';
|
|
7
7
|
import { promisify } from 'util';
|
|
8
8
|
import { getBinaryPath, buildCliArgs, escapeString } from './utils.js';
|
|
9
|
+
import { validateCwdPath } from './utils/path-validation.js';
|
|
9
10
|
|
|
10
11
|
const execAsync = promisify(exec);
|
|
11
12
|
|
|
@@ -26,6 +27,7 @@ const QUERY_FLAG_MAP = {
|
|
|
26
27
|
*
|
|
27
28
|
* @param {Object} options - Query options
|
|
28
29
|
* @param {string} options.path - Path to search in
|
|
30
|
+
* @param {string} [options.cwd] - Working directory for resolving relative paths (defaults to process.cwd())
|
|
29
31
|
* @param {string} options.pattern - The ast-grep pattern to search for
|
|
30
32
|
* @param {string} [options.language] - Programming language to search in
|
|
31
33
|
* @param {string[]} [options.ignore] - Patterns to ignore
|
|
@@ -62,9 +64,14 @@ export async function query(options) {
|
|
|
62
64
|
// Add pattern and path as positional arguments
|
|
63
65
|
cliArgs.push(escapeString(options.pattern), escapeString(options.path));
|
|
64
66
|
|
|
67
|
+
// Get the working directory (cwd option for resolving relative paths)
|
|
68
|
+
// Validate and normalize the path to prevent path traversal attacks
|
|
69
|
+
const cwd = await validateCwdPath(options.cwd);
|
|
70
|
+
|
|
65
71
|
// Create a single log record with all query parameters (only in debug mode)
|
|
66
72
|
if (process.env.DEBUG === '1') {
|
|
67
73
|
let logMessage = `Query: pattern="${options.pattern}" path="${options.path}"`;
|
|
74
|
+
if (options.cwd) logMessage += ` cwd="${options.cwd}"`;
|
|
68
75
|
if (options.language) logMessage += ` language=${options.language}`;
|
|
69
76
|
if (options.maxResults) logMessage += ` maxResults=${options.maxResults}`;
|
|
70
77
|
if (options.allowTests) logMessage += " allowTests=true";
|
|
@@ -75,7 +82,7 @@ export async function query(options) {
|
|
|
75
82
|
const command = `${binaryPath} query ${cliArgs.join(' ')}`;
|
|
76
83
|
|
|
77
84
|
try {
|
|
78
|
-
const { stdout, stderr } = await execAsync(command);
|
|
85
|
+
const { stdout, stderr } = await execAsync(command, { cwd });
|
|
79
86
|
|
|
80
87
|
if (stderr) {
|
|
81
88
|
console.error(`stderr: ${stderr}`);
|
|
@@ -110,7 +117,7 @@ export async function query(options) {
|
|
|
110
117
|
return stdout;
|
|
111
118
|
} catch (error) {
|
|
112
119
|
// Enhance error message with command details
|
|
113
|
-
const errorMessage = `Error executing query command: ${error.message}\nCommand: ${command}`;
|
|
120
|
+
const errorMessage = `Error executing query command: ${error.message}\nCommand: ${command}\nCwd: ${cwd}`;
|
|
114
121
|
throw new Error(errorMessage);
|
|
115
122
|
}
|
|
116
123
|
}
|
package/src/search.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { execFile } from 'child_process';
|
|
7
7
|
import { promisify } from 'util';
|
|
8
8
|
import { getBinaryPath, buildCliArgs } from './utils.js';
|
|
9
|
+
import { validateCwdPath } from './utils/path-validation.js';
|
|
9
10
|
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
|
|
@@ -38,6 +39,7 @@ const SEARCH_FLAG_MAP = {
|
|
|
38
39
|
*
|
|
39
40
|
* @param {Object} options - Search options
|
|
40
41
|
* @param {string} options.path - Path to search in
|
|
42
|
+
* @param {string} [options.cwd] - Working directory for resolving relative paths (defaults to process.cwd())
|
|
41
43
|
* @param {string|string[]} options.query - Search query or queries
|
|
42
44
|
* @param {boolean} [options.filesOnly] - Only output file paths
|
|
43
45
|
* @param {string[]} [options.ignore] - Patterns to ignore
|
|
@@ -125,9 +127,14 @@ export async function search(options) {
|
|
|
125
127
|
// Add query and path as positional arguments
|
|
126
128
|
const queries = Array.isArray(options.query) ? options.query : [options.query];
|
|
127
129
|
|
|
130
|
+
// Get the working directory (cwd option for resolving relative paths)
|
|
131
|
+
// Validate and normalize the path to prevent path traversal attacks
|
|
132
|
+
const cwd = await validateCwdPath(options.cwd);
|
|
133
|
+
|
|
128
134
|
// Create a single log record with all search parameters (only in debug mode)
|
|
129
135
|
if (process.env.DEBUG === '1') {
|
|
130
136
|
let logMessage = `\nSearch: query="${queries[0]}" path="${options.path}"`;
|
|
137
|
+
if (options.cwd) logMessage += ` cwd="${options.cwd}"`;
|
|
131
138
|
if (options.maxResults) logMessage += ` maxResults=${options.maxResults}`;
|
|
132
139
|
logMessage += ` maxTokens=${options.maxTokens}`;
|
|
133
140
|
logMessage += ` timeout=${options.timeout}`;
|
|
@@ -154,6 +161,7 @@ export async function search(options) {
|
|
|
154
161
|
try {
|
|
155
162
|
// Execute with execFile (no shell, prevents command injection)
|
|
156
163
|
const { stdout, stderr } = await execFileAsync(binaryPath, args, {
|
|
164
|
+
cwd,
|
|
157
165
|
timeout: options.timeout * 1000, // Convert seconds to milliseconds
|
|
158
166
|
maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large outputs
|
|
159
167
|
});
|
|
@@ -226,13 +234,13 @@ export async function search(options) {
|
|
|
226
234
|
} catch (error) {
|
|
227
235
|
// Check if the error is a timeout
|
|
228
236
|
if (error.code === 'ETIMEDOUT' || error.killed) {
|
|
229
|
-
const timeoutMessage = `Search operation timed out after ${options.timeout} seconds.\nBinary: ${binaryPath}\nArgs: ${args.join(' ')}`;
|
|
237
|
+
const timeoutMessage = `Search operation timed out after ${options.timeout} seconds.\nBinary: ${binaryPath}\nArgs: ${args.join(' ')}\nCwd: ${cwd}`;
|
|
230
238
|
console.error(timeoutMessage);
|
|
231
239
|
throw new Error(timeoutMessage);
|
|
232
240
|
}
|
|
233
241
|
|
|
234
242
|
// Enhance error message with command details
|
|
235
|
-
const errorMessage = `Error executing search command: ${error.message}\nBinary: ${binaryPath}\nArgs: ${args.join(' ')}`;
|
|
243
|
+
const errorMessage = `Error executing search command: ${error.message}\nBinary: ${binaryPath}\nArgs: ${args.join(' ')}\nCwd: ${cwd}`;
|
|
236
244
|
throw new Error(errorMessage);
|
|
237
245
|
}
|
|
238
246
|
}
|
package/src/tools/bash.js
CHANGED
|
@@ -22,15 +22,15 @@ import { executeBashCommand, formatExecutionResult, validateExecutionOptions } f
|
|
|
22
22
|
* @param {Object} [options.bashConfig.env={}] - Default environment variables
|
|
23
23
|
* @param {number} [options.bashConfig.maxBuffer] - Maximum output buffer size
|
|
24
24
|
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
25
|
-
* @param {string} [options.
|
|
25
|
+
* @param {string} [options.cwd] - Working directory from probe config
|
|
26
26
|
* @param {string[]} [options.allowedFolders] - Allowed directories for execution
|
|
27
27
|
* @returns {Object} Configured bash tool
|
|
28
28
|
*/
|
|
29
29
|
export const bashTool = (options = {}) => {
|
|
30
|
-
const {
|
|
30
|
+
const {
|
|
31
31
|
bashConfig = {},
|
|
32
32
|
debug = false,
|
|
33
|
-
|
|
33
|
+
cwd,
|
|
34
34
|
allowedFolders = []
|
|
35
35
|
} = options;
|
|
36
36
|
|
|
@@ -48,8 +48,8 @@ export const bashTool = (options = {}) => {
|
|
|
48
48
|
if (bashConfig.workingDirectory) {
|
|
49
49
|
return bashConfig.workingDirectory;
|
|
50
50
|
}
|
|
51
|
-
if (
|
|
52
|
-
return
|
|
51
|
+
if (cwd) {
|
|
52
|
+
return cwd;
|
|
53
53
|
}
|
|
54
54
|
if (allowedFolders && allowedFolders.length > 0) {
|
|
55
55
|
return allowedFolders[0];
|
package/src/tools/edit.js
CHANGED
|
@@ -40,7 +40,7 @@ function parseFileToolOptions(options = {}) {
|
|
|
40
40
|
return {
|
|
41
41
|
debug: options.debug || false,
|
|
42
42
|
allowedFolders: options.allowedFolders || [],
|
|
43
|
-
|
|
43
|
+
cwd: options.cwd
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -50,11 +50,11 @@ function parseFileToolOptions(options = {}) {
|
|
|
50
50
|
* @param {Object} [options] - Configuration options
|
|
51
51
|
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
52
52
|
* @param {string[]} [options.allowedFolders] - Allowed directories for file operations
|
|
53
|
-
* @param {string} [options.
|
|
53
|
+
* @param {string} [options.cwd] - Working directory
|
|
54
54
|
* @returns {Object} Configured edit tool
|
|
55
55
|
*/
|
|
56
56
|
export const editTool = (options = {}) => {
|
|
57
|
-
const { debug, allowedFolders,
|
|
57
|
+
const { debug, allowedFolders, cwd } = parseFileToolOptions(options);
|
|
58
58
|
|
|
59
59
|
return tool({
|
|
60
60
|
name: 'edit',
|
|
@@ -111,7 +111,7 @@ Important:
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// Resolve the file path
|
|
114
|
-
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(
|
|
114
|
+
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(cwd || process.cwd(), file_path);
|
|
115
115
|
|
|
116
116
|
if (debug) {
|
|
117
117
|
console.error(`[Edit] Attempting to edit file: ${resolvedPath}`);
|
|
@@ -182,11 +182,11 @@ Important:
|
|
|
182
182
|
* @param {Object} [options] - Configuration options
|
|
183
183
|
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
184
184
|
* @param {string[]} [options.allowedFolders] - Allowed directories for file operations
|
|
185
|
-
* @param {string} [options.
|
|
185
|
+
* @param {string} [options.cwd] - Working directory
|
|
186
186
|
* @returns {Object} Configured create tool
|
|
187
187
|
*/
|
|
188
188
|
export const createTool = (options = {}) => {
|
|
189
|
-
const { debug, allowedFolders,
|
|
189
|
+
const { debug, allowedFolders, cwd } = parseFileToolOptions(options);
|
|
190
190
|
|
|
191
191
|
return tool({
|
|
192
192
|
name: 'create',
|
|
@@ -235,7 +235,7 @@ Important:
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
// Resolve the file path
|
|
238
|
-
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(
|
|
238
|
+
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(cwd || process.cwd(), file_path);
|
|
239
239
|
|
|
240
240
|
if (debug) {
|
|
241
241
|
console.error(`[Create] Attempting to create file: ${resolvedPath}`);
|
package/src/tools/langchain.js
CHANGED
|
@@ -9,7 +9,9 @@ import { extract } from '../extract.js';
|
|
|
9
9
|
import { searchSchema, querySchema, extractSchema, searchDescription, queryDescription, extractDescription, parseTargets } from './common.js';
|
|
10
10
|
|
|
11
11
|
// LangChain tool for searching code
|
|
12
|
-
export function createSearchTool() {
|
|
12
|
+
export function createSearchTool(options = {}) {
|
|
13
|
+
const { cwd } = options;
|
|
14
|
+
|
|
13
15
|
return {
|
|
14
16
|
name: 'search',
|
|
15
17
|
description: searchDescription,
|
|
@@ -19,6 +21,7 @@ export function createSearchTool() {
|
|
|
19
21
|
const results = await search({
|
|
20
22
|
query: searchQuery,
|
|
21
23
|
path,
|
|
24
|
+
cwd, // Working directory for resolving relative paths
|
|
22
25
|
allow_tests,
|
|
23
26
|
exact,
|
|
24
27
|
json: false,
|
|
@@ -37,7 +40,9 @@ export function createSearchTool() {
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// LangChain tool for querying code
|
|
40
|
-
export function createQueryTool() {
|
|
43
|
+
export function createQueryTool(options = {}) {
|
|
44
|
+
const { cwd } = options;
|
|
45
|
+
|
|
41
46
|
return {
|
|
42
47
|
name: 'query',
|
|
43
48
|
description: queryDescription,
|
|
@@ -47,6 +52,7 @@ export function createQueryTool() {
|
|
|
47
52
|
const results = await query({
|
|
48
53
|
pattern,
|
|
49
54
|
path,
|
|
55
|
+
cwd, // Working directory for resolving relative paths
|
|
50
56
|
language,
|
|
51
57
|
allow_tests,
|
|
52
58
|
json: false
|
|
@@ -62,7 +68,9 @@ export function createQueryTool() {
|
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
// LangChain tool for extracting code
|
|
65
|
-
export function createExtractTool() {
|
|
71
|
+
export function createExtractTool(options = {}) {
|
|
72
|
+
const { cwd } = options;
|
|
73
|
+
|
|
66
74
|
return {
|
|
67
75
|
name: 'extract',
|
|
68
76
|
description: extractDescription,
|
|
@@ -74,6 +82,7 @@ export function createExtractTool() {
|
|
|
74
82
|
|
|
75
83
|
const results = await extract({
|
|
76
84
|
files,
|
|
85
|
+
cwd, // Working directory for resolving relative paths
|
|
77
86
|
allowTests: allow_tests,
|
|
78
87
|
contextLines: context_lines,
|
|
79
88
|
format
|