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.
- package/INSTALLATION_METHODS.md +181 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/bin/npm-wrapper.js +171 -0
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +159 -0
- package/package.json +42 -0
- package/requirements.txt +7 -0
- package/scripts/install.js +132 -0
- package/setup.bat +114 -0
- package/setup.sh +135 -0
- package/src/__init__.py +4 -0
- package/src/core/__init__.py +1 -0
- package/src/core/agentic.py +2342 -0
- package/src/core/chat_manager.py +1201 -0
- package/src/core/config_manager.py +269 -0
- package/src/core/init.py +161 -0
- package/src/core/sub_agent.py +174 -0
- package/src/exceptions.py +75 -0
- package/src/llm/__init__.py +1 -0
- package/src/llm/client.py +149 -0
- package/src/llm/config.py +445 -0
- package/src/llm/prompts.py +569 -0
- package/src/llm/providers.py +402 -0
- package/src/llm/token_tracker.py +220 -0
- package/src/ui/__init__.py +1 -0
- package/src/ui/banner.py +103 -0
- package/src/ui/commands.py +489 -0
- package/src/ui/displays.py +167 -0
- package/src/ui/main.py +351 -0
- package/src/ui/prompt_utils.py +162 -0
- package/src/utils/__init__.py +1 -0
- package/src/utils/editor.py +158 -0
- package/src/utils/gitignore_filter.py +149 -0
- package/src/utils/logger.py +254 -0
- package/src/utils/markdown.py +32 -0
- package/src/utils/settings.py +94 -0
- package/src/utils/tools/__init__.py +55 -0
- package/src/utils/tools/command_executor.py +217 -0
- package/src/utils/tools/create_file.py +143 -0
- package/src/utils/tools/definitions.py +193 -0
- package/src/utils/tools/directory.py +374 -0
- package/src/utils/tools/file_editor.py +345 -0
- package/src/utils/tools/file_helpers.py +109 -0
- package/src/utils/tools/file_reader.py +331 -0
- package/src/utils/tools/formatters.py +458 -0
- package/src/utils/tools/parallel_executor.py +195 -0
- package/src/utils/validation.py +117 -0
- package/src/utils/web_search.py +71 -0
- package/vmcode-proxy/.env.example +5 -0
- package/vmcode-proxy/README.md +235 -0
- package/vmcode-proxy/package-lock.json +947 -0
- package/vmcode-proxy/package.json +20 -0
- package/vmcode-proxy/server.js +248 -0
- 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
|
+
)
|