@latentforce/latentgraph 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Tools Executor for MCP
3
+ * Handles execution of local file tools matching extension's tools-executor.js
4
+ */
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import { exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import { getProjectTree as getProjectTreeSync } from '../utils/tree-scanner.js';
10
+ const execAsync = promisify(exec);
11
+ export class ToolsExecutor {
12
+ wsClient;
13
+ workspaceRoot;
14
+ tools;
15
+ constructor(wsClient, workspaceRoot) {
16
+ this.wsClient = wsClient;
17
+ this.workspaceRoot = workspaceRoot;
18
+ this.tools = {
19
+ 'get_tree_struct': this.getTreeStruct.bind(this),
20
+ 'read_file': this.readFile.bind(this),
21
+ 'get_file_info': this.getFileInfo.bind(this),
22
+ 'get_repository_root': this.getRepositoryRoot.bind(this),
23
+ 'get_project_tree': this.getProjectTree.bind(this),
24
+ 'execute_command': this.executeCommand.bind(this),
25
+ 'run_bash': this.runBash.bind(this),
26
+ 'glob_search': this.globSearch.bind(this),
27
+ 'grep_search': this.grepSearch.bind(this),
28
+ };
29
+ }
30
+ /**
31
+ * Execute a tool by name
32
+ */
33
+ async executeTool(toolName, params) {
34
+ console.log(`[ToolsExecutor] Executing: ${toolName}`);
35
+ console.log(`[ToolsExecutor] Params:`, JSON.stringify(params, null, 2));
36
+ if (!this.tools[toolName]) {
37
+ throw new Error(`Unknown tool: ${toolName}`);
38
+ }
39
+ try {
40
+ const result = await this.tools[toolName](params);
41
+ console.log(`[ToolsExecutor] ✓ ${toolName} completed`);
42
+ return result;
43
+ }
44
+ catch (error) {
45
+ console.error(`[ToolsExecutor] ✗ ${toolName} failed:`, error);
46
+ throw error;
47
+ }
48
+ }
49
+ /**
50
+ * Get available tool names
51
+ */
52
+ getAvailableTools() {
53
+ return Object.keys(this.tools);
54
+ }
55
+ /**
56
+ * Execute command (with auto-approval for MCP - no UI)
57
+ */
58
+ async executeCommand(params) {
59
+ const { command, working_directory = '.' } = params;
60
+ if (!command) {
61
+ return {
62
+ status: 'error',
63
+ error: 'command is required'
64
+ };
65
+ }
66
+ console.log(`[ToolsExecutor] Executing command: ${command}`);
67
+ const fullWorkingDir = path.join(this.workspaceRoot, working_directory);
68
+ try {
69
+ const { stdout, stderr } = await execAsync(command, {
70
+ cwd: fullWorkingDir,
71
+ timeout: 30000,
72
+ maxBuffer: 1024 * 1024
73
+ });
74
+ return {
75
+ status: 'success',
76
+ command: command,
77
+ working_directory: working_directory,
78
+ stdout: stdout,
79
+ stderr: stderr,
80
+ exit_code: 0
81
+ };
82
+ }
83
+ catch (error) {
84
+ return {
85
+ status: 'error',
86
+ command: command,
87
+ working_directory: working_directory,
88
+ stdout: error.stdout || '',
89
+ stderr: error.stderr || '',
90
+ exit_code: error.code || 1,
91
+ error_message: error.message
92
+ };
93
+ }
94
+ }
95
+ /**
96
+ * Get tree structure of a directory
97
+ */
98
+ async getTreeStruct(params) {
99
+ const { target_path = params.path || '.', depth = 3, exclude_patterns = ['.git', 'node_modules', '__pycache__', '.vscode', 'dist', 'build'] } = params;
100
+ const fullPath = path.join(this.workspaceRoot, target_path);
101
+ let file_count = 0;
102
+ let dir_count = 0;
103
+ let total_size = 0;
104
+ const scanDirectory = async (dirPath, currentDepth = 0, relativePath = '') => {
105
+ if (currentDepth >= depth) {
106
+ return [];
107
+ }
108
+ const items = [];
109
+ try {
110
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ if (exclude_patterns.some((pattern) => entry.name.includes(pattern))) {
113
+ continue;
114
+ }
115
+ const itemPath = path.join(dirPath, entry.name);
116
+ const itemRelativePath = path.join(relativePath, entry.name);
117
+ if (entry.isDirectory()) {
118
+ dir_count++;
119
+ const children = await scanDirectory(itemPath, currentDepth + 1, itemRelativePath);
120
+ items.push({
121
+ name: entry.name,
122
+ type: 'directory',
123
+ path: itemRelativePath,
124
+ children: children
125
+ });
126
+ }
127
+ else if (entry.isFile()) {
128
+ file_count++;
129
+ try {
130
+ const stats = await fs.stat(itemPath);
131
+ total_size += stats.size;
132
+ items.push({
133
+ name: entry.name,
134
+ type: 'file',
135
+ path: itemRelativePath,
136
+ size: stats.size
137
+ });
138
+ }
139
+ catch {
140
+ // Skip files we can't read
141
+ }
142
+ }
143
+ }
144
+ }
145
+ catch (err) {
146
+ console.error(`Error scanning ${dirPath}:`, err.message);
147
+ }
148
+ return items;
149
+ };
150
+ const tree = await scanDirectory(fullPath, 0, target_path);
151
+ return {
152
+ status: 'success',
153
+ tree: tree,
154
+ file_count: file_count,
155
+ dir_count: dir_count,
156
+ total_size: `${(total_size / (1024 * 1024)).toFixed(2)} MB`,
157
+ scanned_path: target_path,
158
+ workspace_root: this.workspaceRoot
159
+ };
160
+ }
161
+ /**
162
+ * Read file contents
163
+ */
164
+ async readFile(params) {
165
+ const { file_path } = params;
166
+ if (!file_path) {
167
+ return {
168
+ status: 'error',
169
+ error: 'file_path is required'
170
+ };
171
+ }
172
+ console.log(`[ToolsExecutor] Reading file: ${file_path}`);
173
+ try {
174
+ const fullPath = path.join(this.workspaceRoot, file_path);
175
+ const content = await fs.readFile(fullPath, 'utf8');
176
+ const stats = await fs.stat(fullPath);
177
+ return {
178
+ status: 'success',
179
+ file_path: file_path,
180
+ content: content,
181
+ size: stats.size,
182
+ lines: content.split('\n').length
183
+ };
184
+ }
185
+ catch (error) {
186
+ return {
187
+ status: 'error',
188
+ file_path: file_path,
189
+ error: error.message
190
+ };
191
+ }
192
+ }
193
+ /**
194
+ * Get file info
195
+ */
196
+ async getFileInfo(params) {
197
+ const { file_path } = params;
198
+ if (!file_path) {
199
+ return {
200
+ status: 'error',
201
+ error: 'file_path is required'
202
+ };
203
+ }
204
+ try {
205
+ const fullPath = path.join(this.workspaceRoot, file_path);
206
+ const stats = await fs.stat(fullPath);
207
+ return {
208
+ status: 'success',
209
+ file_path: file_path,
210
+ size: stats.size,
211
+ created: stats.birthtime,
212
+ modified: stats.mtime,
213
+ is_directory: stats.isDirectory()
214
+ };
215
+ }
216
+ catch (error) {
217
+ return {
218
+ status: 'error',
219
+ file_path: file_path,
220
+ error: error.message
221
+ };
222
+ }
223
+ }
224
+ /**
225
+ * Get repository root directory
226
+ */
227
+ async getRepositoryRoot(params) {
228
+ const { start_path = '.' } = params;
229
+ console.log(`[ToolsExecutor] Finding repository root from: ${start_path}`);
230
+ let currentPath = path.join(this.workspaceRoot, start_path);
231
+ try {
232
+ const stats = await fs.stat(currentPath);
233
+ if (stats.isFile()) {
234
+ currentPath = path.dirname(currentPath);
235
+ }
236
+ }
237
+ catch {
238
+ currentPath = this.workspaceRoot;
239
+ }
240
+ let searchPath = currentPath;
241
+ const maxDepth = 20;
242
+ let depth = 0;
243
+ while (depth < maxDepth) {
244
+ try {
245
+ const gitPath = path.join(searchPath, '.git');
246
+ await fs.access(gitPath);
247
+ const entries = await fs.readdir(searchPath);
248
+ const has_package_json = entries.includes('package.json');
249
+ const has_requirements_txt = entries.includes('requirements.txt');
250
+ const has_cargo_toml = entries.includes('Cargo.toml');
251
+ const has_go_mod = entries.includes('go.mod');
252
+ let project_type = 'unknown';
253
+ if (has_package_json)
254
+ project_type = 'node';
255
+ else if (has_requirements_txt)
256
+ project_type = 'python';
257
+ else if (has_cargo_toml)
258
+ project_type = 'rust';
259
+ else if (has_go_mod)
260
+ project_type = 'go';
261
+ return {
262
+ status: 'success',
263
+ repository_root: searchPath,
264
+ relative_to_workspace: path.relative(this.workspaceRoot, searchPath),
265
+ has_git: true,
266
+ project_type: project_type,
267
+ files_in_root: entries.length
268
+ };
269
+ }
270
+ catch {
271
+ const parentPath = path.dirname(searchPath);
272
+ if (parentPath === searchPath) {
273
+ return {
274
+ status: 'success',
275
+ repository_root: null,
276
+ has_git: false,
277
+ message: 'No .git folder found in any parent directory'
278
+ };
279
+ }
280
+ searchPath = parentPath;
281
+ depth++;
282
+ }
283
+ }
284
+ return {
285
+ status: 'success',
286
+ repository_root: null,
287
+ has_git: false,
288
+ message: `No .git folder found after searching ${maxDepth} levels up`
289
+ };
290
+ }
291
+ /**
292
+ * Returns combined output matching tools.py's run_bash format.
293
+ */
294
+ async runBash(params) {
295
+ const { command } = params;
296
+ if (!command) {
297
+ return { status: 'error', error: 'command is required' };
298
+ }
299
+ console.log(`[ToolsExecutor] run_bash: ${command}`);
300
+ try {
301
+ const { stdout, stderr } = await execAsync(command, {
302
+ cwd: this.workspaceRoot,
303
+ timeout: 30000,
304
+ maxBuffer: 1024 * 1024,
305
+ });
306
+ let output = stdout;
307
+ if (stderr)
308
+ output += `\n[STDERR]\n${stderr}`;
309
+ return {
310
+ status: 'success',
311
+ output: output.trim() || '[No output]',
312
+ };
313
+ }
314
+ catch (error) {
315
+ let output = error.stdout || '';
316
+ if (error.stderr)
317
+ output += `\n[STDERR]\n${error.stderr}`;
318
+ output += `\n[EXIT CODE: ${error.code || 1}]`;
319
+ return {
320
+ status: 'error',
321
+ output: output.trim(),
322
+ error: error.message,
323
+ };
324
+ }
325
+ }
326
+ /**
327
+ * Find files matching a glob pattern
328
+ */
329
+ async globSearch(params) {
330
+ const { pattern } = params;
331
+ if (!pattern) {
332
+ return { status: 'error', error: 'pattern is required' };
333
+ }
334
+ console.log(`[ToolsExecutor] Glob search: ${pattern}`);
335
+ let basePath = '.';
336
+ let namePattern = pattern;
337
+ const lastSlash = pattern.lastIndexOf('/');
338
+ if (lastSlash !== -1) {
339
+ basePath = pattern.substring(0, lastSlash).replace(/\/?\*\*$/, '') || '.';
340
+ namePattern = pattern.substring(lastSlash + 1);
341
+ }
342
+ const searchDir = path.join(this.workspaceRoot, basePath);
343
+ const excludes = [
344
+ '-not', '-path', '*/node_modules/*',
345
+ '-not', '-path', '*/.git/*',
346
+ '-not', '-path', '*/__pycache__/*',
347
+ '-not', '-path', '*/dist/*',
348
+ '-not', '-path', '*/build/*',
349
+ '-not', '-path', '*/.venv/*',
350
+ ];
351
+ // Quote searchDir to handle workspace paths that contain spaces
352
+ const cmd = `find "${searchDir}" -name "${namePattern}" ${excludes.join(' ')}`;
353
+ try {
354
+ const { stdout } = await execAsync(cmd, { timeout: 30000 });
355
+ const matches = stdout
356
+ .trim()
357
+ .split('\n')
358
+ .filter(Boolean)
359
+ .map(f => path.relative(this.workspaceRoot, f))
360
+ .slice(0, 200);
361
+ return {
362
+ status: 'success',
363
+ pattern,
364
+ matches,
365
+ count: matches.length,
366
+ };
367
+ }
368
+ catch (error) {
369
+ return { status: 'error', error: error.message };
370
+ }
371
+ }
372
+ /**
373
+ * Search file contents with a regex pattern
374
+ */
375
+ async grepSearch(params) {
376
+ const { pattern, file_glob } = params;
377
+ if (!pattern) {
378
+ return { status: 'error', error: 'pattern is required' };
379
+ }
380
+ console.log(`[ToolsExecutor] Grep search: ${pattern}${file_glob ? ` (${file_glob})` : ''}`);
381
+ const baseArgs = [
382
+ 'grep', '-rn', '-E',
383
+ '--exclude-dir=node_modules',
384
+ '--exclude-dir=.git',
385
+ '--exclude-dir=__pycache__',
386
+ '--exclude-dir=dist',
387
+ '--exclude-dir=build',
388
+ '--exclude-dir=.venv',
389
+ '--binary-files=without-match',
390
+ ];
391
+ let searchPath = '.';
392
+ if (file_glob) {
393
+ if (file_glob.includes('/')) {
394
+ const match = file_glob.match(/^([^*]+?)\/(?:\*\*\/)?(.+)$/);
395
+ if (match) {
396
+ searchPath = match[1];
397
+ const namePat = match[2];
398
+ if (namePat && namePat !== '*')
399
+ baseArgs.push(`--include=${namePat}`);
400
+ }
401
+ else {
402
+ baseArgs.push(`--include=${file_glob}`);
403
+ }
404
+ }
405
+ else {
406
+ baseArgs.push(`--include=${file_glob}`);
407
+ }
408
+ }
409
+ const args = [...baseArgs, `'${pattern.replace(/'/g, `'\\''`)}'`, searchPath];
410
+ try {
411
+ const { stdout } = await execAsync(args.join(' '), {
412
+ cwd: this.workspaceRoot,
413
+ timeout: 30000,
414
+ maxBuffer: 1024 * 1024,
415
+ });
416
+ const lines = stdout.trim().split('\n').filter(Boolean);
417
+ const truncated = lines.length > 500;
418
+ const output = truncated ? lines.slice(0, 500).join('\n') + `\n\n... [truncated — ${lines.length} total matches, showing first 500]` : stdout.trim();
419
+ return {
420
+ status: 'success',
421
+ pattern,
422
+ file_glob: file_glob || null,
423
+ output,
424
+ match_count: lines.length,
425
+ };
426
+ }
427
+ catch (error) {
428
+ if (error.code === 1 && !error.stderr) {
429
+ return { status: 'success', pattern, file_glob: file_glob || null, output: '', match_count: 0 };
430
+ }
431
+ return { status: 'error', error: error.message };
432
+ }
433
+ }
434
+ /**
435
+ * Get project tree — delegates to shared tree-scanner utility
436
+ */
437
+ async getProjectTree(params) {
438
+ const { depth = 0, exclude_patterns } = params;
439
+ console.log(`[ToolsExecutor] Getting project tree from workspace root: ${this.workspaceRoot}`);
440
+ console.log(`[ToolsExecutor] Depth limit: ${depth === 0 ? 'unlimited' : depth}`);
441
+ const result = getProjectTreeSync(this.workspaceRoot, {
442
+ depth,
443
+ ...(exclude_patterns && { exclude_patterns }),
444
+ });
445
+ console.log(`[ToolsExecutor] ✓ Scanned project tree:`);
446
+ console.log(`[ToolsExecutor] Files: ${result.file_count}`);
447
+ console.log(`[ToolsExecutor] Directories: ${result.dir_count}`);
448
+ console.log(`[ToolsExecutor] Total size: ${result.total_size_mb} MB`);
449
+ return {
450
+ status: 'success',
451
+ tree: result.tree,
452
+ file_count: result.file_count,
453
+ dir_count: result.dir_count,
454
+ total_size_bytes: result.total_size_bytes,
455
+ total_size_mb: result.total_size_mb,
456
+ scanned_from: result.scanned_from,
457
+ depth_limit: result.depth_limit,
458
+ actual_max_depth: result.actual_max_depth,
459
+ excluded_patterns: result.excluded_patterns,
460
+ };
461
+ }
462
+ }