@probelabs/probe 0.6.0-rc174 → 0.6.0-rc176

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.
@@ -170,6 +170,19 @@ export class SimpleAppTracer {
170
170
  }
171
171
  }
172
172
 
173
+ /**
174
+ * Record a generic event (used by completionPrompt and other features)
175
+ */
176
+ // visor-disable: SimpleAppTracer uses this.sessionId because it's a per-session instance. AppTracer extracts from attributes because it's a singleton managing multiple sessions. Different architectures require different approaches.
177
+ recordEvent(name, attributes = {}) {
178
+ if (!this.isEnabled()) return;
179
+
180
+ this.addEvent(name, {
181
+ 'session.id': this.sessionId,
182
+ ...attributes
183
+ });
184
+ }
185
+
173
186
  /**
174
187
  * Record delegation events
175
188
  */
package/build/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/build/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/build/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
  }
@@ -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.defaultPath] - Default working directory from probe config
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
- defaultPath,
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 (defaultPath) {
52
- return defaultPath;
51
+ if (cwd) {
52
+ return cwd;
53
53
  }
54
54
  if (allowedFolders && allowedFolders.length > 0) {
55
55
  return allowedFolders[0];
@@ -40,7 +40,7 @@ function parseFileToolOptions(options = {}) {
40
40
  return {
41
41
  debug: options.debug || false,
42
42
  allowedFolders: options.allowedFolders || [],
43
- defaultPath: options.defaultPath
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.defaultPath] - Default working directory
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, defaultPath } = parseFileToolOptions(options);
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(defaultPath || process.cwd(), file_path);
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.defaultPath] - Default working directory
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, defaultPath } = parseFileToolOptions(options);
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(defaultPath || process.cwd(), file_path);
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}`);
@@ -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,7 +21,8 @@ export function createSearchTool() {
19
21
  const results = await search({
20
22
  query: searchQuery,
21
23
  path,
22
- allow_tests,
24
+ cwd, // Working directory for resolving relative paths
25
+ allowTests: allow_tests ?? true,
23
26
  exact,
24
27
  json: false,
25
28
  maxResults,
@@ -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,8 +52,9 @@ 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
- allow_tests,
57
+ allowTests: allow_tests ?? true,
52
58
  json: false
53
59
  });
54
60
 
@@ -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,7 +82,8 @@ export function createExtractTool() {
74
82
 
75
83
  const results = await extract({
76
84
  files,
77
- allowTests: allow_tests,
85
+ cwd, // Working directory for resolving relative paths
86
+ allowTests: allow_tests ?? true,
78
87
  contextLines: context_lines,
79
88
  format
80
89
  });
@@ -31,15 +31,15 @@ export const searchTool = (options = {}) => {
31
31
  // Use parameter maxTokens if provided, otherwise use the default
32
32
  const effectiveMaxTokens = paramMaxTokens || maxTokens;
33
33
 
34
- // Use the path from parameters if provided, otherwise use defaultPath from config
35
- let searchPath = path || options.defaultPath || '.';
34
+ // Use the path from parameters if provided, otherwise use cwd from config
35
+ let searchPath = path || options.cwd || '.';
36
36
 
37
- // If path is "." or "./", use the defaultPath if available
38
- if ((searchPath === "." || searchPath === "./") && options.defaultPath) {
37
+ // If path is "." or "./", use the cwd if available
38
+ if ((searchPath === "." || searchPath === "./") && options.cwd) {
39
39
  if (debug) {
40
- console.error(`Using default path "${options.defaultPath}" instead of "${searchPath}"`);
40
+ console.error(`Using cwd "${options.cwd}" instead of "${searchPath}"`);
41
41
  }
42
- searchPath = options.defaultPath;
42
+ searchPath = options.cwd;
43
43
  }
44
44
 
45
45
  if (debug) {
@@ -49,7 +49,8 @@ export const searchTool = (options = {}) => {
49
49
  const searchOptions = {
50
50
  query: searchQuery,
51
51
  path: searchPath,
52
- allowTests: allow_tests,
52
+ cwd: options.cwd, // Working directory for resolving relative paths
53
+ allowTests: allow_tests ?? true,
53
54
  exact,
54
55
  json: false,
55
56
  maxTokens: effectiveMaxTokens,
@@ -89,15 +90,15 @@ export const queryTool = (options = {}) => {
89
90
  inputSchema: querySchema,
90
91
  execute: async ({ pattern, path, language, allow_tests }) => {
91
92
  try {
92
- // Use the path from parameters if provided, otherwise use defaultPath from config
93
- let queryPath = path || options.defaultPath || '.';
93
+ // Use the path from parameters if provided, otherwise use cwd from config
94
+ let queryPath = path || options.cwd || '.';
94
95
 
95
- // If path is "." or "./", use the defaultPath if available
96
- if ((queryPath === "." || queryPath === "./") && options.defaultPath) {
96
+ // If path is "." or "./", use the cwd if available
97
+ if ((queryPath === "." || queryPath === "./") && options.cwd) {
97
98
  if (debug) {
98
- console.error(`Using default path "${options.defaultPath}" instead of "${queryPath}"`);
99
+ console.error(`Using cwd "${options.cwd}" instead of "${queryPath}"`);
99
100
  }
100
- queryPath = options.defaultPath;
101
+ queryPath = options.cwd;
101
102
  }
102
103
 
103
104
  if (debug) {
@@ -107,8 +108,9 @@ export const queryTool = (options = {}) => {
107
108
  const results = await query({
108
109
  pattern,
109
110
  path: queryPath,
111
+ cwd: options.cwd, // Working directory for resolving relative paths
110
112
  language,
111
- allow_tests,
113
+ allowTests: allow_tests ?? true,
112
114
  json: false
113
115
  });
114
116
 
@@ -137,28 +139,20 @@ export const extractTool = (options = {}) => {
137
139
  inputSchema: extractSchema,
138
140
  execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format }) => {
139
141
  try {
140
- // Use the defaultPath from config for context
141
- let extractPath = options.defaultPath || '.';
142
-
143
- // If path is "." or "./", use the defaultPath if available
144
- if ((extractPath === "." || extractPath === "./") && options.defaultPath) {
145
- if (debug) {
146
- console.error(`Using default path "${options.defaultPath}" instead of "${extractPath}"`);
147
- }
148
- extractPath = options.defaultPath;
149
- }
142
+ // Use the cwd from config for working directory
143
+ const effectiveCwd = options.cwd || '.';
150
144
 
151
145
  if (debug) {
152
146
  if (targets) {
153
- console.error(`Executing extract with targets: "${targets}", path: "${extractPath}", context lines: ${context_lines || 10}`);
147
+ console.error(`Executing extract with targets: "${targets}", cwd: "${effectiveCwd}", context lines: ${context_lines || 10}`);
154
148
  } else if (input_content) {
155
- console.error(`Executing extract with input content, path: "${extractPath}", context lines: ${context_lines || 10}`);
149
+ console.error(`Executing extract with input content, cwd: "${effectiveCwd}", context lines: ${context_lines || 10}`);
156
150
  }
157
151
  }
158
152
 
159
153
  // Create a temporary file for input content if provided
160
154
  let tempFilePath = null;
161
- let extractOptions = { path: extractPath };
155
+ let extractOptions = { cwd: effectiveCwd };
162
156
 
163
157
  if (input_content) {
164
158
  // Import required modules
@@ -184,7 +178,8 @@ export const extractTool = (options = {}) => {
184
178
  // Set up extract options with input file
185
179
  extractOptions = {
186
180
  inputFile: tempFilePath,
187
- allowTests: allow_tests,
181
+ cwd: effectiveCwd,
182
+ allowTests: allow_tests ?? true,
188
183
  contextLines: context_lines,
189
184
  format: effectiveFormat
190
185
  };
@@ -202,7 +197,8 @@ export const extractTool = (options = {}) => {
202
197
  // Set up extract options with files
203
198
  extractOptions = {
204
199
  files,
205
- allowTests: allow_tests,
200
+ cwd: effectiveCwd,
201
+ allowTests: allow_tests ?? true,
206
202
  contextLines: context_lines,
207
203
  format: effectiveFormat
208
204
  };
@@ -241,12 +237,12 @@ export const extractTool = (options = {}) => {
241
237
  * @param {Object} [options] - Configuration options
242
238
  * @param {boolean} [options.debug=false] - Enable debug logging
243
239
  * @param {number} [options.timeout=300] - Default timeout in seconds
244
- * @param {string} [options.defaultPath] - Default path to use if not specified in call
240
+ * @param {string} [options.cwd] - Working directory to use if not specified in call
245
241
  * @param {string[]} [options.allowedFolders] - Allowed folders for workspace isolation
246
242
  * @returns {Object} Configured delegate tool
247
243
  */
248
244
  export const delegateTool = (options = {}) => {
249
- const { debug = false, timeout = 300, defaultPath, allowedFolders } = options;
245
+ const { debug = false, timeout = 300, cwd, allowedFolders } = options;
250
246
 
251
247
  return tool({
252
248
  name: 'delegate',
@@ -289,8 +285,8 @@ export const delegateTool = (options = {}) => {
289
285
  }
290
286
 
291
287
  // Use inherited path if not specified in AI call
292
- // Priority: explicit path > defaultPath > first allowedFolder
293
- const effectivePath = path || defaultPath || (allowedFolders && allowedFolders[0]);
288
+ // Priority: explicit path > cwd > first allowedFolder
289
+ const effectivePath = path || cwd || (allowedFolders && allowedFolders[0]);
294
290
 
295
291
  if (debug) {
296
292
  console.error(`Executing delegate with task: "${task.substring(0, 100)}${task.length > 100 ? '...' : ''}"`);
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Path validation utilities for the probe package
3
+ * @module utils/path-validation
4
+ */
5
+
6
+ import path from 'path';
7
+ import { promises as fs } from 'fs';
8
+
9
+ /**
10
+ * Validates and normalizes a path to be used as working directory (cwd).
11
+ *
12
+ * Security considerations:
13
+ * - Normalizes path to resolve '..' and '.' components
14
+ * - Returns absolute path to prevent ambiguity
15
+ * - Does NOT restrict access to specific directories (that's the responsibility
16
+ * of higher-level components like ProbeAgent with allowedFolders)
17
+ *
18
+ * @param {string} inputPath - The path to validate
19
+ * @param {string} [defaultPath] - Default path to use if inputPath is not provided
20
+ * @returns {Promise<string>} Normalized absolute path
21
+ * @throws {Error} If the path is invalid or doesn't exist
22
+ */
23
+ export async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
24
+ // Use default if not provided
25
+ const targetPath = inputPath || defaultPath;
26
+
27
+ // Normalize and resolve to absolute path
28
+ // This handles '..' traversal and makes the path unambiguous
29
+ const normalizedPath = path.normalize(path.resolve(targetPath));
30
+
31
+ // Verify the path exists and is a directory
32
+ try {
33
+ const stats = await fs.stat(normalizedPath);
34
+ if (!stats.isDirectory()) {
35
+ throw new Error(`Path is not a directory: ${normalizedPath}`);
36
+ }
37
+ } catch (error) {
38
+ if (error.code === 'ENOENT') {
39
+ throw new Error(`Path does not exist: ${normalizedPath}`);
40
+ }
41
+ throw error;
42
+ }
43
+
44
+ return normalizedPath;
45
+ }
46
+
47
+ /**
48
+ * Validates a path option without requiring it to exist.
49
+ * Use this for paths that might be created or are optional.
50
+ *
51
+ * @param {string} inputPath - The path to validate
52
+ * @param {string} [defaultPath] - Default path to use if inputPath is not provided
53
+ * @returns {string} Normalized absolute path
54
+ */
55
+ export function normalizePath(inputPath, defaultPath = process.cwd()) {
56
+ const targetPath = inputPath || defaultPath;
57
+ return path.normalize(path.resolve(targetPath));
58
+ }