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,117 @@
|
|
|
1
|
+
"""Command validation and duplicate detection."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shlex
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Commands that overlap with native tools (blocked - use the tool instead)
|
|
8
|
+
BLOCKED_OVERLAPS = {
|
|
9
|
+
# Code search (use rg tool)
|
|
10
|
+
"rg", "rg.exe", "ripgrep",
|
|
11
|
+
|
|
12
|
+
# File reading (use read_file tool)
|
|
13
|
+
"cat", "get-content", "type",
|
|
14
|
+
|
|
15
|
+
# Directory listing (use list_directory tool)
|
|
16
|
+
"ls", "get-childitem", "dir",
|
|
17
|
+
|
|
18
|
+
# File creation (use create_file tool)
|
|
19
|
+
"touch", "new-item",
|
|
20
|
+
|
|
21
|
+
# File editing (use edit_file tool)
|
|
22
|
+
"set-content", "add-content", "echo", "tee",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def normalize_for_comparison(command):
|
|
27
|
+
"""Normalize command for duplicate detection."""
|
|
28
|
+
# Remove shell prefix (e.g., "powershell ") and extra whitespace
|
|
29
|
+
cmd = command.strip().lower()
|
|
30
|
+
if cmd.startswith("powershell "):
|
|
31
|
+
cmd = cmd[11:].strip()
|
|
32
|
+
return cmd
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_for_duplicate(chat_manager, command):
|
|
36
|
+
"""Check if command was already run and return simple warning.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
tuple: (is_duplicate, redirect_message)
|
|
40
|
+
"""
|
|
41
|
+
normalized = normalize_for_comparison(command)
|
|
42
|
+
|
|
43
|
+
if normalized in chat_manager.command_history:
|
|
44
|
+
redirect_msg = (
|
|
45
|
+
f"exit_code=DUPLICATE\n"
|
|
46
|
+
f"This exact command was already executed: {command}\n\n"
|
|
47
|
+
f"The result is already in your conversation history.\n"
|
|
48
|
+
f"Try a DIFFERENT command with different search terms, "
|
|
49
|
+
f"different file paths, or a different approach entirely."
|
|
50
|
+
)
|
|
51
|
+
return True, redirect_msg
|
|
52
|
+
|
|
53
|
+
# Add to history
|
|
54
|
+
chat_manager.command_history.append(normalized)
|
|
55
|
+
return False, None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _tokenize_segment(segment):
|
|
59
|
+
use_posix = os.name != "nt"
|
|
60
|
+
try:
|
|
61
|
+
return shlex.split(segment, posix=use_posix)
|
|
62
|
+
except ValueError:
|
|
63
|
+
return segment.split()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def check_command(command):
|
|
67
|
+
"""Validate command against safety checks.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
command: Command string to validate
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
tuple: (is_safe, reason) - is_safe is True if command is allowed
|
|
74
|
+
If is_safe is True, the caller should check approval requirements separately
|
|
75
|
+
"""
|
|
76
|
+
command = command.strip()
|
|
77
|
+
if not command:
|
|
78
|
+
return False, "empty command"
|
|
79
|
+
|
|
80
|
+
# Strip "powershell " prefix if present (legacy support for Windows users)
|
|
81
|
+
if command.lower().startswith("powershell "):
|
|
82
|
+
command = command[len("powershell "):].strip()
|
|
83
|
+
|
|
84
|
+
# Block dangerous operators (command chaining and redirection)
|
|
85
|
+
# Allow && for conditional chaining (stops on error - safer than ; or &)
|
|
86
|
+
blocked_operators = (";", ">", "<", "`", "|")
|
|
87
|
+
if any(token in command for token in blocked_operators):
|
|
88
|
+
return False, "contains disallowed shell operators"
|
|
89
|
+
|
|
90
|
+
# Note: && is allowed for conditional chaining. The agent will display
|
|
91
|
+
# a warning in debug mode when using && for multi-step commands.
|
|
92
|
+
|
|
93
|
+
# After stripping prefix, reject if it still starts with "powershell"
|
|
94
|
+
if command.lower().startswith("powershell"):
|
|
95
|
+
return False, "nested powershell invocation"
|
|
96
|
+
|
|
97
|
+
# Tokenize and validate command name
|
|
98
|
+
tokens = _tokenize_segment(command)
|
|
99
|
+
if not tokens:
|
|
100
|
+
return False, "empty command"
|
|
101
|
+
|
|
102
|
+
cmd_name = tokens[0].lower()
|
|
103
|
+
|
|
104
|
+
# Block commands that overlap with native tools
|
|
105
|
+
if cmd_name in BLOCKED_OVERLAPS:
|
|
106
|
+
tool_map = {
|
|
107
|
+
"rg": "rg tool", "rg.exe": "rg tool", "ripgrep": "rg tool",
|
|
108
|
+
"cat": "read_file tool", "get-content": "read_file tool", "type": "read_file tool",
|
|
109
|
+
"ls": "list_directory tool", "get-childitem": "list_directory tool", "dir": "list_directory tool",
|
|
110
|
+
"touch": "create_file tool", "new-item": "create_file tool",
|
|
111
|
+
"set-content": "edit_file tool", "add-content": "edit_file tool", "echo": "edit_file tool", "tee": "edit_file tool",
|
|
112
|
+
}
|
|
113
|
+
tool_suggestion = tool_map.get(cmd_name, "appropriate native tool")
|
|
114
|
+
return False, f"command '{cmd_name}' overlaps with {tool_suggestion}. Use the native tool instead."
|
|
115
|
+
|
|
116
|
+
# Allow all other commands
|
|
117
|
+
return True, None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Web search using DuckDuckGo (no API key required)."""
|
|
2
|
+
|
|
3
|
+
from ddgs import DDGS
|
|
4
|
+
from exceptions import LLMConnectionError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run_web_search(arguments, console):
|
|
8
|
+
"""Execute web search using DuckDuckGo and return formatted results.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
arguments: {
|
|
12
|
+
"query": "search terms to look for",
|
|
13
|
+
"num_results": 5 # optional, number of results (default: 5, max: 10)
|
|
14
|
+
}
|
|
15
|
+
console: Rich console for output
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
str: Formatted search results with metadata for model consumption
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
LLMConnectionError: If network search fails
|
|
22
|
+
"""
|
|
23
|
+
query = arguments.get("query")
|
|
24
|
+
num_results = arguments.get("num_results", 5)
|
|
25
|
+
|
|
26
|
+
if not query:
|
|
27
|
+
raise LLMConnectionError(
|
|
28
|
+
"Missing required parameter: query",
|
|
29
|
+
details={"arguments": arguments}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Validate and clamp num_results between 1 and 10
|
|
33
|
+
try:
|
|
34
|
+
num_results = max(1, min(10, int(num_results)))
|
|
35
|
+
except (ValueError, TypeError):
|
|
36
|
+
num_results = 5
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
with DDGS() as ddgs:
|
|
40
|
+
results = list(ddgs.text(query, max_results=num_results))
|
|
41
|
+
|
|
42
|
+
if not results:
|
|
43
|
+
return "results_found=0\nNo results found.\n\n"
|
|
44
|
+
|
|
45
|
+
# Format results for model only (not displayed to console)
|
|
46
|
+
output_lines = []
|
|
47
|
+
for idx, result in enumerate(results, 1):
|
|
48
|
+
title = result.get("title", "Untitled")
|
|
49
|
+
url = result.get("href", "N/A")
|
|
50
|
+
body = result.get("body", "No content")
|
|
51
|
+
|
|
52
|
+
output_lines.append(f"[{idx}] {title}")
|
|
53
|
+
output_lines.append(f"URL: {url}")
|
|
54
|
+
output_lines.append(f"Snippet: {body}")
|
|
55
|
+
if idx < len(results):
|
|
56
|
+
output_lines.append("")
|
|
57
|
+
|
|
58
|
+
# Build result string with metadata for model
|
|
59
|
+
result_content = "\n".join(output_lines)
|
|
60
|
+
return f"results_found={len(results)}\n{result_content}\n\n"
|
|
61
|
+
|
|
62
|
+
except LLMConnectionError:
|
|
63
|
+
# Re-raise our custom exceptions
|
|
64
|
+
raise
|
|
65
|
+
except Exception as e:
|
|
66
|
+
console.print(f"Web search failed: {e}", style="red")
|
|
67
|
+
raise LLMConnectionError(
|
|
68
|
+
f"Failed to perform web search",
|
|
69
|
+
details={"query": query, "original_error": str(e)}
|
|
70
|
+
)
|
|
71
|
+
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# vmCode Proxy Server
|
|
2
|
+
|
|
3
|
+
Production-ready proxy server for vmCode free tier using OpenRouter's `z-ai/glm-4.5-air:free` model.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Rate Limiting**: Multi-tier protection
|
|
8
|
+
- 15-minute window: 100 requests per IP
|
|
9
|
+
- Daily per-IP: 500 requests (free tier)
|
|
10
|
+
- Daily global: 10,000 requests (hard limit)
|
|
11
|
+
- **Security**: CORS enabled, request validation, 60s timeout
|
|
12
|
+
- **Performance**: Gzip compression, memory cleanup
|
|
13
|
+
- **Monitoring**: `/health` and `/stats` endpoints
|
|
14
|
+
- **Reliability**: Graceful shutdown, error handling
|
|
15
|
+
|
|
16
|
+
## What This Does
|
|
17
|
+
|
|
18
|
+
- Holds your OpenRouter API key (never exposed to users)
|
|
19
|
+
- Forwards requests to OpenRouter's free models
|
|
20
|
+
- Allows vmCode users to use the app without configuring API keys
|
|
21
|
+
- Prevents abuse with IP-based rate limiting
|
|
22
|
+
|
|
23
|
+
## Local Development
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Install dependencies
|
|
27
|
+
npm install
|
|
28
|
+
|
|
29
|
+
# Set up environment variables
|
|
30
|
+
cp .env.example .env
|
|
31
|
+
# Edit .env and add your OpenRouter API key
|
|
32
|
+
|
|
33
|
+
# Run locally
|
|
34
|
+
npm start
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The server will run on `http://localhost:3000`
|
|
38
|
+
|
|
39
|
+
## Deploy to Vercel
|
|
40
|
+
|
|
41
|
+
### 1. Install Vercel CLI
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g vercel
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Deploy
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# From the vmcode-proxy directory
|
|
51
|
+
vercel login
|
|
52
|
+
vercel --prod
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Set Environment Variable
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
vercel env add OPENROUTER_API_KEY production
|
|
59
|
+
# Paste your OpenRouter API key when prompted
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 4. Get Your URL
|
|
63
|
+
|
|
64
|
+
After deployment, Vercel will give you a URL like:
|
|
65
|
+
`https://vmcode-proxy.vercel.app`
|
|
66
|
+
|
|
67
|
+
## Update vmCode Config
|
|
68
|
+
|
|
69
|
+
After deploying, update your vmCode config to point to your deployed proxy:
|
|
70
|
+
|
|
71
|
+
In `config.yaml`:
|
|
72
|
+
```yaml
|
|
73
|
+
VMCODE_FREE_API_BASE: "https://your-deployed-url.vercel.app"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Or update the default in `src/llm/config.py`:
|
|
77
|
+
```python
|
|
78
|
+
"api_base": _CONFIG.get("VMCODE_FREE_API_BASE", "https://your-deployed-url.vercel.app"),
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Testing
|
|
82
|
+
|
|
83
|
+
### Test the Proxy Directly
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
curl -X POST https://your-deployed-url.vercel.app/chat \
|
|
87
|
+
-H "Content-Type: application/json" \
|
|
88
|
+
-d '{
|
|
89
|
+
"messages": [
|
|
90
|
+
{"role": "user", "content": "Hello!"}
|
|
91
|
+
]
|
|
92
|
+
}'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Test Health Endpoint
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
curl https://your-deployed-url.vercel.app/health
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Rate Limiting
|
|
102
|
+
|
|
103
|
+
The proxy enforces multiple rate limits to prevent abuse:
|
|
104
|
+
|
|
105
|
+
| Limit | Window | Scope |
|
|
106
|
+
|-------|--------|-------|
|
|
107
|
+
| 100 requests | 15 minutes | Per IP |
|
|
108
|
+
| 500 requests | 24 hours | Per IP (free tier) |
|
|
109
|
+
| 10,000 requests | 24 hours | Global (all IPs) |
|
|
110
|
+
|
|
111
|
+
When limits are exceeded, the proxy returns HTTP 429 with reset time.
|
|
112
|
+
|
|
113
|
+
## Monitoring
|
|
114
|
+
|
|
115
|
+
### Health Endpoint
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
curl https://your-deployed-url.vercel.app/health
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"status": "ok",
|
|
125
|
+
"service": "vmcode-proxy",
|
|
126
|
+
"todayRequests": 1234,
|
|
127
|
+
"dailyLimit": 10000,
|
|
128
|
+
"percentUsed": 12,
|
|
129
|
+
"ipUsage": {
|
|
130
|
+
"requests": 50,
|
|
131
|
+
"limit": 500,
|
|
132
|
+
"percentUsed": 10
|
|
133
|
+
},
|
|
134
|
+
"activeIps": 42
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Stats Endpoint
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
curl https://your-deployed-url.vercel.app/stats
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Returns detailed usage statistics including per-IP breakdown.
|
|
145
|
+
|
|
146
|
+
### OpenRouter Dashboard
|
|
147
|
+
|
|
148
|
+
Check OpenRouter dashboard for:
|
|
149
|
+
- Usage statistics
|
|
150
|
+
- Cost tracking
|
|
151
|
+
- Which free models are being used
|
|
152
|
+
|
|
153
|
+
Set up billing alerts to control costs:
|
|
154
|
+
- $10, $50, $100 alerts recommended
|
|
155
|
+
|
|
156
|
+
## Costs
|
|
157
|
+
|
|
158
|
+
- **Vercel:** Free tier (up to 100GB bandwidth/month)
|
|
159
|
+
- **OpenRouter:** Pay for what you use
|
|
160
|
+
- Free models: ~$0-0.10 per 1M tokens
|
|
161
|
+
- Estimated: $0-10/month for first 1000 users
|
|
162
|
+
|
|
163
|
+
## Troubleshooting
|
|
164
|
+
|
|
165
|
+
### "OPENROUTER_API_KEY not configured"
|
|
166
|
+
|
|
167
|
+
Make sure you set the environment variable in Vercel:
|
|
168
|
+
```bash
|
|
169
|
+
vercel env ls
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 500 errors from proxy
|
|
173
|
+
|
|
174
|
+
Check Vercel logs:
|
|
175
|
+
```bash
|
|
176
|
+
vercel logs
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 429 Rate limit exceeded
|
|
180
|
+
|
|
181
|
+
The client has exceeded one of the rate limits:
|
|
182
|
+
- Wait until the reset time
|
|
183
|
+
- Reduce request frequency
|
|
184
|
+
- Contact admin if limits need adjustment
|
|
185
|
+
|
|
186
|
+
### 504 Request timeout
|
|
187
|
+
|
|
188
|
+
The upstream OpenRouter API took too long (>60s). This is rare but can happen during high load.
|
|
189
|
+
|
|
190
|
+
### Slow responses
|
|
191
|
+
|
|
192
|
+
OpenRouter's `z-ai/glm-4.5-air:free` model may take longer than direct API calls. This is normal.
|
|
193
|
+
|
|
194
|
+
## Architecture
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
User → vmcode-proxy → OpenRouter /free models
|
|
198
|
+
↓
|
|
199
|
+
Rate Limiting
|
|
200
|
+
Request Validation
|
|
201
|
+
Error Handling
|
|
202
|
+
Memory Cleanup
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Memory Management:**
|
|
206
|
+
- IP usage entries are automatically cleaned up after 2 days
|
|
207
|
+
- Cleanup runs every hour to prevent memory leaks
|
|
208
|
+
- Active IPs count shown in health endpoint
|
|
209
|
+
|
|
210
|
+
**Graceful Shutdown:**
|
|
211
|
+
- On SIGTERM/SIGINT, server waits 10s for in-flight requests
|
|
212
|
+
- Force shutdown after second signal
|
|
213
|
+
|
|
214
|
+
## Security
|
|
215
|
+
|
|
216
|
+
- **API Keys**: Stored in environment variables, never in code
|
|
217
|
+
- **CORS**: Enabled for cross-origin requests
|
|
218
|
+
- **Request Validation**: Validates message array structure
|
|
219
|
+
- **Timeout**: 60s timeout prevents hanging requests
|
|
220
|
+
- **Rate Limiting**: Prevents abuse and DDoS
|
|
221
|
+
|
|
222
|
+
## Scaling
|
|
223
|
+
|
|
224
|
+
**Current Setup (Single Server):**
|
|
225
|
+
- In-memory rate limiting
|
|
226
|
+
- Suitable for up to ~10,000 daily users
|
|
227
|
+
|
|
228
|
+
**For Larger Scale:**
|
|
229
|
+
- Add Redis for distributed rate limiting
|
|
230
|
+
- Use a load balancer with multiple instances
|
|
231
|
+
- Add authentication for per-user limits
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
Same as vmCode-CLI project.
|