vmcode-cli 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.
Files changed (56) hide show
  1. package/INSTALLATION_METHODS.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +199 -0
  4. package/bin/npm-wrapper.js +171 -0
  5. package/bin/rg +0 -0
  6. package/bin/rg.exe +0 -0
  7. package/config.yaml.example +159 -0
  8. package/package.json +42 -0
  9. package/requirements.txt +7 -0
  10. package/scripts/install.js +132 -0
  11. package/setup.bat +114 -0
  12. package/setup.sh +135 -0
  13. package/src/__init__.py +4 -0
  14. package/src/core/__init__.py +1 -0
  15. package/src/core/agentic.py +2342 -0
  16. package/src/core/chat_manager.py +1201 -0
  17. package/src/core/config_manager.py +269 -0
  18. package/src/core/init.py +161 -0
  19. package/src/core/sub_agent.py +174 -0
  20. package/src/exceptions.py +75 -0
  21. package/src/llm/__init__.py +1 -0
  22. package/src/llm/client.py +149 -0
  23. package/src/llm/config.py +445 -0
  24. package/src/llm/prompts.py +569 -0
  25. package/src/llm/providers.py +402 -0
  26. package/src/llm/token_tracker.py +220 -0
  27. package/src/ui/__init__.py +1 -0
  28. package/src/ui/banner.py +103 -0
  29. package/src/ui/commands.py +489 -0
  30. package/src/ui/displays.py +167 -0
  31. package/src/ui/main.py +351 -0
  32. package/src/ui/prompt_utils.py +162 -0
  33. package/src/utils/__init__.py +1 -0
  34. package/src/utils/editor.py +158 -0
  35. package/src/utils/gitignore_filter.py +149 -0
  36. package/src/utils/logger.py +254 -0
  37. package/src/utils/markdown.py +32 -0
  38. package/src/utils/settings.py +94 -0
  39. package/src/utils/tools/__init__.py +55 -0
  40. package/src/utils/tools/command_executor.py +217 -0
  41. package/src/utils/tools/create_file.py +143 -0
  42. package/src/utils/tools/definitions.py +193 -0
  43. package/src/utils/tools/directory.py +374 -0
  44. package/src/utils/tools/file_editor.py +345 -0
  45. package/src/utils/tools/file_helpers.py +109 -0
  46. package/src/utils/tools/file_reader.py +331 -0
  47. package/src/utils/tools/formatters.py +458 -0
  48. package/src/utils/tools/parallel_executor.py +195 -0
  49. package/src/utils/validation.py +117 -0
  50. package/src/utils/web_search.py +71 -0
  51. package/vmcode-proxy/.env.example +5 -0
  52. package/vmcode-proxy/README.md +235 -0
  53. package/vmcode-proxy/package-lock.json +947 -0
  54. package/vmcode-proxy/package.json +20 -0
  55. package/vmcode-proxy/server.js +248 -0
  56. package/vmcode-proxy/server.js.bak +157 -0
@@ -0,0 +1,374 @@
1
+ """Directory listing operations."""
2
+
3
+ import fnmatch
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional, Tuple, Dict, List
7
+
8
+ from .file_helpers import (
9
+ _is_fast_ignored,
10
+ _is_ignored_cached,
11
+ _register_gitignore_spec
12
+ )
13
+ from .formatters import format_file_result
14
+
15
+ # Directory listing truncation thresholds
16
+ TRUNCATION_THRESHOLD = 100 # Total items to trigger truncation
17
+ MAX_FILES_PER_FOLDER = 10 # Max files to show per folder when truncating
18
+ MAX_TOTAL_ITEMS = 500 # Hard upper limit for total items to collect (prevents context explosion)
19
+
20
+
21
+ def _group_items_by_directory(items, show_files, show_dirs) -> Dict:
22
+ """Group items by their parent directory for smart truncation.
23
+
24
+ Args:
25
+ items: List of (kind, rel_path, size, raw_path, line_count) tuples
26
+ show_files: Whether files are included in results
27
+ show_dirs: Whether directories are included in results
28
+
29
+ Returns:
30
+ Dict mapping parent_dir -> {'dirs': [dir_items], 'files': [file_items]}
31
+ """
32
+ groups = {}
33
+
34
+ for kind, rel_path, size, raw_path, line_count in items:
35
+ # Get parent directory
36
+ if '/' in rel_path:
37
+ parent_dir = Path(rel_path).parent
38
+ else:
39
+ parent_dir = Path('.') # Root level
40
+
41
+ if parent_dir not in groups:
42
+ groups[parent_dir] = {'dirs': [], 'files': []}
43
+
44
+ if kind == 'DIR ':
45
+ groups[parent_dir]['dirs'].append((kind, rel_path, size, raw_path, line_count))
46
+ else: # FILE
47
+ groups[parent_dir]['files'].append((kind, rel_path, size, raw_path, line_count))
48
+
49
+ return groups
50
+
51
+
52
+ def _apply_smart_truncation(items, show_files, show_dirs, hit_limit=False) -> Tuple[List, Optional[Dict]]:
53
+ """Apply smart truncation to directory listing results.
54
+
55
+ Truncation Strategy:
56
+ - If total items < TRUNCATION_THRESHOLD: No truncation
57
+ - If total items >= TRUNCATION_THRESHOLD:
58
+ * ALL directories are shown (preserve structure)
59
+ * Files are sampled: top MAX_FILES_PER_FOLDER per directory
60
+ * Truncation metadata returned for message generation
61
+
62
+ Args:
63
+ items: List of (kind, rel_path, size, raw_path) tuples
64
+ show_files: Whether files are included in results
65
+ show_dirs: Whether directories are included in results
66
+ hit_limit: Whether we hit MAX_TOTAL_ITEMS during collection
67
+
68
+ Returns:
69
+ Tuple of:
70
+ - List of items after truncation
71
+ - None if no truncation, or dict with truncation metadata
72
+ """
73
+ total_count = len(items)
74
+
75
+ # Fast path: no truncation needed
76
+ if total_count < TRUNCATION_THRESHOLD:
77
+ return items, None
78
+
79
+ # Group items by parent directory
80
+ groups = _group_items_by_directory(items, show_files, show_dirs)
81
+
82
+ # Apply truncation per directory
83
+ truncated_items = []
84
+ total_dirs_shown = 0
85
+ total_files_shown = 0
86
+ total_files_omitted = 0
87
+
88
+ # Sort directories alphabetically for consistent output
89
+ for parent_dir in sorted(groups.keys(), key=lambda p: str(p)):
90
+ group = groups[parent_dir]
91
+
92
+ # Always include all directories
93
+ if show_dirs:
94
+ truncated_items.extend(group['dirs'])
95
+ total_dirs_shown += len(group['dirs'])
96
+
97
+ # Truncate files if needed
98
+ if show_files and group['files']:
99
+ # Sort files alphabetically and take top N
100
+ sorted_files = sorted(group['files'], key=lambda x: x[1]) # Sort by rel_path
101
+ files_to_show = sorted_files[:MAX_FILES_PER_FOLDER]
102
+ truncated_items.extend(files_to_show)
103
+
104
+ total_files_shown += len(files_to_show)
105
+ total_files_omitted += len(group['files']) - len(files_to_show)
106
+
107
+ # Build truncation info
108
+ truncation_info = {
109
+ 'total': total_count,
110
+ 'shown': len(truncated_items),
111
+ 'omitted': total_count - len(truncated_items),
112
+ 'dirs_shown': total_dirs_shown,
113
+ 'files_shown': total_files_shown,
114
+ 'files_omitted': total_files_omitted,
115
+ 'all_dirs_shown': True
116
+ }
117
+
118
+ return truncated_items, truncation_info
119
+
120
+
121
+ def _validate_directory_path(
122
+ path_str: str,
123
+ repo_root: Path
124
+ ) -> Tuple[Optional[Path], Optional[str]]:
125
+ """Validate and resolve path for directory listing.
126
+
127
+ Args:
128
+ path_str: Path string to validate
129
+ repo_root: Repository root directory
130
+
131
+ Returns:
132
+ (resolved_path, error_message) - error_message is None if valid
133
+
134
+ Checks:
135
+ - Path resolution
136
+ - Path exists
137
+ - Path is a directory (not a file)
138
+ """
139
+ try:
140
+ # Resolve path
141
+ raw_path = Path(path_str)
142
+ if not raw_path.is_absolute():
143
+ raw_path = repo_root / raw_path
144
+ resolved = raw_path.resolve()
145
+
146
+ # Check if it exists
147
+ if not resolved.exists():
148
+ return None, "Directory not found"
149
+
150
+ # Check if it's a file (user error, show helpful message)
151
+ if resolved.is_file():
152
+ return None, "Path is a file, not a directory. Use read_file instead."
153
+
154
+ return resolved, None
155
+
156
+ except Exception as e:
157
+ return None, str(e)
158
+
159
+
160
+ def list_directory(
161
+ path_str: str,
162
+ repo_root: Path,
163
+ recursive: bool = False,
164
+ show_files: bool = True,
165
+ show_dirs: bool = True,
166
+ pattern: Optional[str] = None,
167
+ gitignore_spec = None
168
+ ) -> str:
169
+ """List directory contents.
170
+
171
+ Directory listing that respects .gitignore and shows file sizes in a
172
+ consistent format.
173
+
174
+ Args:
175
+ path_str: Path string to the directory to list
176
+ repo_root: Repository root directory (for path resolution)
177
+ recursive: List recursively
178
+ show_files: Include files in output
179
+ show_dirs: Include directories in output
180
+ pattern: Optional glob pattern to filter results (e.g., "*.py")
181
+ gitignore_spec: Optional PathSpec for .gitignore filtering
182
+
183
+ Returns:
184
+ str: Formatted result with exit_code, items_count, and directory listing
185
+ """
186
+ try:
187
+ # Validate path
188
+ resolved, error = _validate_directory_path(path_str, repo_root)
189
+ if error:
190
+ return format_file_result(
191
+ exit_code=1,
192
+ error=error,
193
+ path=path_str
194
+ )
195
+
196
+ def _match_pattern(rel_path: Path) -> bool:
197
+ if not pattern:
198
+ return True
199
+ return fnmatch.fnmatch(rel_path.as_posix(), pattern)
200
+
201
+ spec_key = _register_gitignore_spec(gitignore_spec) if gitignore_spec is not None else 0
202
+
203
+ def _is_ignored(path: Path) -> bool:
204
+ if path.name == ".gitignore":
205
+ return True
206
+ if ".git" in path.parts:
207
+ return True
208
+ if gitignore_spec is None:
209
+ return False
210
+ # Fast-path check first
211
+ if _is_fast_ignored(path):
212
+ return True
213
+ # Use cached gitignore check
214
+ return _is_ignored_cached(str(path), str(repo_root), spec_key)
215
+
216
+ # Collect items
217
+ items = []
218
+ base_dir = resolved
219
+ hit_limit = False # Track if we hit MAX_TOTAL_ITEMS
220
+
221
+ def _count_lines(file_path: Path) -> int:
222
+ """Count lines in a file efficiently."""
223
+ try:
224
+ with open(file_path, 'rb') as f:
225
+ return sum(1 for _ in f) - 1 # Subtract 1 for last newline, but handle empty files
226
+ except (OSError, IOError):
227
+ return 0
228
+
229
+ def _add_item(kind, rel_path, size_str, raw_path, line_count=None):
230
+ nonlocal hit_limit
231
+ if kind == "FILE" and not show_files:
232
+ return
233
+ if kind == "DIR " and not show_dirs:
234
+ return
235
+ if not _match_pattern(rel_path):
236
+ return
237
+ if hit_limit:
238
+ return
239
+ if len(items) >= MAX_TOTAL_ITEMS:
240
+ hit_limit = True
241
+ return
242
+ items.append((kind, str(rel_path), size_str, raw_path, line_count))
243
+
244
+ if recursive:
245
+ stack = [resolved]
246
+ while stack and not hit_limit:
247
+ current = stack.pop()
248
+ try:
249
+ with os.scandir(current) as it:
250
+ for entry in it:
251
+ try:
252
+ if entry.is_symlink():
253
+ continue
254
+ except OSError:
255
+ continue
256
+
257
+ is_dir = entry.is_dir(follow_symlinks=False)
258
+ is_file = entry.is_file(follow_symlinks=False)
259
+
260
+ if not is_dir and not is_file:
261
+ continue
262
+
263
+ entry_path = Path(entry.path)
264
+
265
+ if _is_ignored(entry_path):
266
+ continue
267
+
268
+ rel_path = entry_path.relative_to(base_dir)
269
+
270
+ if is_file:
271
+ try:
272
+ size = f"{entry.stat(follow_symlinks=False).st_size:>10}"
273
+ line_count = _count_lines(entry_path)
274
+ except OSError:
275
+ size = " ?"
276
+ line_count = 0
277
+ _add_item("FILE", rel_path, size, entry_path, line_count)
278
+ else:
279
+ _add_item("DIR ", rel_path, " ", entry_path)
280
+ stack.append(entry_path)
281
+ except PermissionError:
282
+ continue
283
+ else:
284
+ with os.scandir(resolved) as it:
285
+ for entry in it:
286
+ try:
287
+ if entry.is_symlink():
288
+ continue
289
+ except OSError:
290
+ continue
291
+
292
+ is_dir = entry.is_dir(follow_symlinks=False)
293
+ is_file = entry.is_file(follow_symlinks=False)
294
+
295
+ if not is_dir and not is_file:
296
+ continue
297
+
298
+ entry_path = Path(entry.path)
299
+
300
+ if _is_ignored(entry_path):
301
+ continue
302
+
303
+ rel_path = entry_path.relative_to(base_dir)
304
+
305
+ if is_file:
306
+ try:
307
+ size = f"{entry.stat(follow_symlinks=False).st_size:>10}"
308
+ line_count = _count_lines(entry_path)
309
+ except OSError:
310
+ size = " ?"
311
+ line_count = 0
312
+ _add_item("FILE", rel_path, size, entry_path, line_count)
313
+ else:
314
+ _add_item("DIR ", rel_path, " ", entry_path)
315
+
316
+ # Sort: directories first, then alphabetically
317
+ items.sort(key=lambda x: (0 if x[0] == "DIR " else 1, x[1]))
318
+
319
+ # Format output
320
+ if not items:
321
+ return format_file_result(
322
+ exit_code=0,
323
+ content="(empty directory)",
324
+ path=str(resolved.relative_to(repo_root)),
325
+ items_count=0
326
+ )
327
+
328
+ # Apply smart truncation if needed
329
+ truncated_items, truncation_info = _apply_smart_truncation(items, show_files, show_dirs, hit_limit)
330
+
331
+ # Build lines with truncation message
332
+ lines = []
333
+ for kind, rel_path, size, _, line_count in truncated_items:
334
+ if kind == "FILE":
335
+ lines.append(f"{kind} {rel_path} {line_count:6} lines {size} bytes")
336
+ else:
337
+ lines.append(f"{kind} {rel_path} {line_count:6} lines")
338
+
339
+ # Add truncation message at end
340
+ if truncation_info:
341
+ lines.append("")
342
+ msg = f"[{truncation_info['files_omitted']} file(s) omitted ({truncation_info['shown']} shown from {truncation_info['total']} total items)]"
343
+ if hit_limit:
344
+ msg += f"\n[WARNING: Listing stopped at {MAX_TOTAL_ITEMS} items to prevent context overflow. Use filters or specific paths to explore further.]"
345
+ lines.append(msg)
346
+
347
+ content = "\n".join(lines)
348
+
349
+ # Update truncation info to indicate if we hit the hard limit
350
+ if truncation_info and hit_limit:
351
+ truncation_info['hit_limit'] = True
352
+ truncation_info['max_items'] = MAX_TOTAL_ITEMS
353
+
354
+ return format_file_result(
355
+ exit_code=0,
356
+ content=content,
357
+ path=str(resolved.relative_to(repo_root)),
358
+ items_count=truncation_info['total'] if truncation_info else len(items),
359
+ truncated=(truncation_info is not None),
360
+ truncation_info=truncation_info
361
+ )
362
+
363
+ except PermissionError:
364
+ return format_file_result(
365
+ exit_code=1,
366
+ error="Permission denied",
367
+ path=path_str
368
+ )
369
+ except Exception as e:
370
+ return format_file_result(
371
+ exit_code=1,
372
+ error=str(e),
373
+ path=path_str
374
+ )