vettcode-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.
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TEXT_EXTENSIONS = exports.IGNORED_EXTENSIONS = exports.IGNORED_FILE_NAMES = exports.IGNORED_DIR_NAMES = void 0;
4
+ exports.shouldIgnorePath = shouldIgnorePath;
5
+ /** Directory and file patterns excluded from vetting (deps, build output, VCS, etc.) */
6
+ exports.IGNORED_DIR_NAMES = new Set([
7
+ "node_modules",
8
+ ".git",
9
+ ".github",
10
+ ".svn",
11
+ ".hg",
12
+ "dist",
13
+ "build",
14
+ "out",
15
+ ".next",
16
+ ".nuxt",
17
+ ".output",
18
+ "coverage",
19
+ ".cache",
20
+ ".turbo",
21
+ "vendor",
22
+ "bower_components",
23
+ "__pycache__",
24
+ ".pytest_cache",
25
+ ".mypy_cache",
26
+ ".venv",
27
+ "venv",
28
+ "env",
29
+ ".env",
30
+ "target",
31
+ "obj",
32
+ ".gradle",
33
+ ".idea",
34
+ ".vscode",
35
+ "Pods",
36
+ "DerivedData",
37
+ ".parcel-cache",
38
+ ".svelte-kit",
39
+ "storybook-static",
40
+ ".vercel",
41
+ "pkg",
42
+ "deps",
43
+ "_build",
44
+ "elm-stuff",
45
+ ".stack-work",
46
+ "zig-cache",
47
+ "zig-out",
48
+ "test", // Test directories
49
+ "tests", // Test directories
50
+ "__tests__", // Jest test directories
51
+ "spec", // Spec directories
52
+ "specs", // Spec directories
53
+ "e2e", // End-to-end test directories
54
+ "cypress", // Cypress test directories
55
+ "playwright", // Playwright test directories
56
+ ".jest", // Jest config directories
57
+ ".mocha", // Mocha config directories
58
+ ]);
59
+ exports.IGNORED_FILE_NAMES = new Set([
60
+ ".DS_Store",
61
+ "Thumbs.db",
62
+ "package-lock.json",
63
+ "yarn.lock",
64
+ "pnpm-lock.yaml",
65
+ "Cargo.lock",
66
+ "composer.lock",
67
+ "Gemfile.lock",
68
+ "poetry.lock",
69
+ "README.md",
70
+ "CHANGELOG.md",
71
+ "CONTRIBUTING.md",
72
+ "LICENSE.md",
73
+ "CODE_OF_CONDUCT.md",
74
+ ]);
75
+ exports.IGNORED_EXTENSIONS = new Set([
76
+ ".png",
77
+ ".jpg",
78
+ ".jpeg",
79
+ ".gif",
80
+ ".webp",
81
+ ".ico",
82
+ ".svg",
83
+ ".woff",
84
+ ".woff2",
85
+ ".ttf",
86
+ ".eot",
87
+ ".mp4",
88
+ ".mp3",
89
+ ".wav",
90
+ ".zip",
91
+ ".tar",
92
+ ".gz",
93
+ ".rar",
94
+ ".7z",
95
+ ".pdf",
96
+ ".exe",
97
+ ".dll",
98
+ ".so",
99
+ ".dylib",
100
+ ".wasm",
101
+ ".map",
102
+ ".min.js",
103
+ ".min.css",
104
+ ".lock",
105
+ ".pyc",
106
+ ".class",
107
+ ".jar",
108
+ ".o",
109
+ ".a",
110
+ ".md", // Markdown files - documentation only
111
+ ".mdx", // MDX files - documentation only
112
+ ]);
113
+ exports.TEXT_EXTENSIONS = new Set([
114
+ ".ts",
115
+ ".tsx",
116
+ ".js",
117
+ ".jsx",
118
+ ".mjs",
119
+ ".cjs",
120
+ ".json",
121
+ ".jsonc",
122
+ ".vue",
123
+ ".svelte",
124
+ ".py",
125
+ ".rb",
126
+ ".go",
127
+ ".rs",
128
+ ".java",
129
+ ".kt",
130
+ ".kts",
131
+ ".scala",
132
+ ".php",
133
+ ".cs",
134
+ ".cpp",
135
+ ".c",
136
+ ".h",
137
+ ".hpp",
138
+ ".swift",
139
+ ".dart",
140
+ ".lua",
141
+ ".sh",
142
+ ".bash",
143
+ ".zsh",
144
+ ".ps1",
145
+ ".sql",
146
+ ".graphql",
147
+ ".gql",
148
+ ".yaml",
149
+ ".yml",
150
+ ".toml",
151
+ ".xml",
152
+ ".html",
153
+ ".htm",
154
+ ".css",
155
+ ".scss",
156
+ ".sass",
157
+ ".less",
158
+ ".env.example",
159
+ ".prisma",
160
+ ".proto",
161
+ ".tf",
162
+ ".hcl",
163
+ ".dockerfile",
164
+ ".gradle",
165
+ ".properties",
166
+ ".ini",
167
+ ".cfg",
168
+ ".conf",
169
+ ".env.local.example",
170
+ ]);
171
+ function shouldIgnorePath(relativePath) {
172
+ const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
173
+ const parts = normalized.split("/");
174
+ for (const part of parts) {
175
+ if (exports.IGNORED_DIR_NAMES.has(part))
176
+ return true;
177
+ if (part.startsWith(".") && part !== ".env.example" && part !== ".gitignore") {
178
+ if ([".github", ".gitlab", ".circleci"].includes(part))
179
+ continue;
180
+ if (part.length > 1 && !part.includes("env"))
181
+ return true;
182
+ }
183
+ }
184
+ const fileName = parts[parts.length - 1] ?? "";
185
+ if (exports.IGNORED_FILE_NAMES.has(fileName))
186
+ return true;
187
+ // Ignore test files by pattern
188
+ const testPatterns = [
189
+ /\.test\.(ts|tsx|js|jsx)$/, // file.test.ts
190
+ /\.spec\.(ts|tsx|js|jsx)$/, // file.spec.ts
191
+ /\.test\.(py|rb|go|java|php)$/, // file.test.py
192
+ /\.spec\.(py|rb|go|java|php)$/, // file.spec.py
193
+ /_test\.(ts|tsx|js|jsx|py|go)$/, // file_test.go
194
+ /_spec\.(ts|tsx|js|jsx|py|rb)$/, // file_spec.rb
195
+ /test_.*\.(py|rb)$/, // test_file.py
196
+ /.*\.e2e\.(ts|tsx|js|jsx)$/, // file.e2e.ts
197
+ /.*\.integration\.(ts|tsx|js|jsx)$/, // file.integration.ts
198
+ ];
199
+ for (const pattern of testPatterns) {
200
+ if (pattern.test(fileName))
201
+ return true;
202
+ }
203
+ const dot = fileName.lastIndexOf(".");
204
+ const ext = dot >= 0 ? fileName.slice(dot).toLowerCase() : "";
205
+ if (ext && exports.IGNORED_EXTENSIONS.has(ext))
206
+ return true;
207
+ if (!ext && !fileName.includes(".")) {
208
+ const noExtAllowed = new Set([
209
+ "dockerfile",
210
+ "makefile",
211
+ "gemfile",
212
+ "rakefile",
213
+ "procfile",
214
+ "vagrantfile",
215
+ ]);
216
+ if (!noExtAllowed.has(fileName.toLowerCase())) {
217
+ return false;
218
+ }
219
+ }
220
+ if (ext && !exports.TEXT_EXTENSIONS.has(ext)) {
221
+ if (!fileName.endsWith(".d.ts"))
222
+ return true;
223
+ }
224
+ return false;
225
+ }
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getApiKeys = getApiKeys;
4
+ exports.getModels = getModels;
5
+ exports.nextApiKey = nextApiKey;
6
+ exports.keyForIndex = keyForIndex;
7
+ exports.chatCompletion = chatCompletion;
8
+ exports.parseJsonFromModel = parseJsonFromModel;
9
+ const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
10
+ let keyIndex = 0;
11
+ function getApiKeys() {
12
+ const keys = [];
13
+ const combined = process.env.OPENROUTER_API_KEYS;
14
+ if (combined) {
15
+ keys.push(...combined
16
+ .split(",")
17
+ .map((k) => k.trim())
18
+ .filter(Boolean));
19
+ }
20
+ for (let i = 1; i <= 3; i++) {
21
+ const k = process.env[`OPENROUTER_API_KEY_${i}`];
22
+ if (k?.trim())
23
+ keys.push(k.trim());
24
+ }
25
+ // Security: Never log API keys or their partial values
26
+ if (keys.length === 0) {
27
+ console.error('[OpenRouter] No API keys found! Check environment variables.');
28
+ // Don't log which specific env vars are set/unset as this could leak information
29
+ }
30
+ else {
31
+ const nodeEnv = process.env.NODE_ENV?.trim() || 'production';
32
+ if (nodeEnv === 'development') {
33
+ console.log(`[OpenRouter] Found ${keys.length} API key(s) configured`);
34
+ }
35
+ }
36
+ return [...new Set(keys)];
37
+ }
38
+ function getModels() {
39
+ const raw = process.env.OPENROUTER_MODELS ??
40
+ "openrouter/free,deepseek/deepseek-chat-v3-0324:free,qwen/qwen-2.5-coder-32b-instruct:free";
41
+ const models = raw
42
+ .split(",")
43
+ .map((m) => m.trim())
44
+ .filter(Boolean);
45
+ // OpenRouter allows max 3 models in fallback array
46
+ return models.slice(0, 3);
47
+ }
48
+ // Rate limiting per API key to prevent exhaustion
49
+ const keyUsageMap = new Map();
50
+ const RATE_LIMIT_WINDOW = 60000; // 1 minute window
51
+ const MAX_REQUESTS_PER_KEY = 50; // Reduced from 100 to be more conservative
52
+ const LOCK_DURATION = 120000; // 2 minutes lock after exceeding limit
53
+ function nextApiKey() {
54
+ const keys = getApiKeys();
55
+ if (keys.length === 0) {
56
+ throw new Error("No OpenRouter API keys configured. Set OPENROUTER_API_KEY_1, _2, _3 or OPENROUTER_API_KEYS.");
57
+ }
58
+ const now = Date.now();
59
+ // Try to find a key that hasn't exceeded rate limit and isn't locked
60
+ for (let i = 0; i < keys.length; i++) {
61
+ const key = keys[(keyIndex + i) % keys.length];
62
+ const usage = keyUsageMap.get(key);
63
+ // Check if key is locked
64
+ if (usage?.lockUntil && now < usage.lockUntil) {
65
+ continue; // Skip locked keys
66
+ }
67
+ // Reset usage if window has expired
68
+ if (usage && now - usage.resetTime > RATE_LIMIT_WINDOW) {
69
+ keyUsageMap.set(key, { count: 1, resetTime: now });
70
+ keyIndex = (keyIndex + i + 1) % keys.length;
71
+ return key;
72
+ }
73
+ // Check if key is under rate limit
74
+ if (!usage) {
75
+ keyUsageMap.set(key, { count: 1, resetTime: now });
76
+ keyIndex = (keyIndex + i + 1) % keys.length;
77
+ return key;
78
+ }
79
+ if (usage.count < MAX_REQUESTS_PER_KEY) {
80
+ keyUsageMap.set(key, { ...usage, count: usage.count + 1 });
81
+ keyIndex = (keyIndex + i + 1) % keys.length;
82
+ return key;
83
+ }
84
+ // Lock the key if it exceeded the limit
85
+ if (!usage.lockUntil) {
86
+ console.warn(`[OpenRouter] API key ${i + 1} exceeded rate limit, locking for 2 minutes`);
87
+ keyUsageMap.set(key, { ...usage, lockUntil: now + LOCK_DURATION });
88
+ }
89
+ }
90
+ // All keys are rate limited or locked
91
+ const unlockTime = Math.min(...Array.from(keyUsageMap.values()).map(u => u.lockUntil || u.resetTime + RATE_LIMIT_WINDOW));
92
+ const waitTime = Math.ceil((unlockTime - now) / 1000);
93
+ throw new Error(`All API keys are rate limited. Please wait ${waitTime} seconds before retrying.`);
94
+ }
95
+ function keyForIndex(index) {
96
+ const keys = getApiKeys();
97
+ if (keys.length === 0)
98
+ throw new Error("No OpenRouter API keys configured.");
99
+ return keys[index % keys.length];
100
+ }
101
+ async function chatCompletion(messages, keyOverride, retries = 2) {
102
+ // Input validation to prevent prompt injection
103
+ if (!Array.isArray(messages) || messages.length === 0) {
104
+ throw new Error('Messages must be a non-empty array');
105
+ }
106
+ // Validate each message structure
107
+ for (const msg of messages) {
108
+ if (!msg || typeof msg !== 'object') {
109
+ throw new Error('Invalid message structure');
110
+ }
111
+ if (!['system', 'user', 'assistant'].includes(msg.role)) {
112
+ throw new Error(`Invalid message role: ${msg.role}`);
113
+ }
114
+ if (typeof msg.content !== 'string') {
115
+ throw new Error('Message content must be a string');
116
+ }
117
+ // Limit message length to prevent abuse
118
+ if (msg.content.length > 1000000) { // 1MB limit
119
+ throw new Error('Message content exceeds maximum length');
120
+ }
121
+ // Check for suspicious patterns that might indicate prompt injection
122
+ const suspiciousPatterns = [
123
+ /ignore\s+(?:all\s+)?previous\s+(?:instructions?|prompts?)/gi,
124
+ /disregard\s+(?:all\s+)?(?:previous|prior)\s+(?:instructions?|commands?)/gi,
125
+ /forget\s+(?:everything|all)\s+(?:before|above)/gi,
126
+ /new\s+instructions?:/gi,
127
+ ];
128
+ for (const pattern of suspiciousPatterns) {
129
+ if (pattern.test(msg.content)) {
130
+ console.warn('[Security] Suspicious prompt injection pattern detected');
131
+ // Don't block, but log for monitoring
132
+ }
133
+ }
134
+ }
135
+ const apiKey = keyOverride ?? nextApiKey();
136
+ const models = getModels();
137
+ // Validate and construct site URL
138
+ const vercelUrl = process.env.VERCEL_URL?.trim();
139
+ const publicUrl = process.env.NEXT_PUBLIC_SITE_URL?.trim();
140
+ const siteUrl = publicUrl || (vercelUrl ? `https://${vercelUrl}` : "http://localhost:3000");
141
+ const body = {
142
+ models,
143
+ messages,
144
+ temperature: 0.15, // Slightly higher for more nuanced analysis
145
+ max_tokens: 6000, // Increased for detailed findings
146
+ };
147
+ if (models.length === 1) {
148
+ body.model = models[0];
149
+ delete body.models;
150
+ }
151
+ for (let attempt = 0; attempt <= retries; attempt++) {
152
+ try {
153
+ const nodeEnv = process.env.NODE_ENV?.trim() || 'production';
154
+ if (nodeEnv === 'development') {
155
+ console.log(`[OpenRouter] Attempt ${attempt + 1}/${retries + 1} - Calling ${OPENROUTER_URL}`);
156
+ console.log(`[OpenRouter] Models: ${JSON.stringify(models)}`);
157
+ console.log(`[OpenRouter] Message count: ${messages.length}, Total chars: ${messages.reduce((sum, m) => sum + m.content.length, 0)}`);
158
+ }
159
+ const controller = new AbortController();
160
+ const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout
161
+ const res = await fetch(OPENROUTER_URL, {
162
+ method: "POST",
163
+ headers: {
164
+ Authorization: `Bearer ${apiKey}`,
165
+ "Content-Type": "application/json",
166
+ "HTTP-Referer": siteUrl,
167
+ "X-Title": "Vettcode Engine",
168
+ },
169
+ body: JSON.stringify(body),
170
+ signal: controller.signal,
171
+ });
172
+ clearTimeout(timeoutId);
173
+ if (nodeEnv === 'development') {
174
+ console.log(`[OpenRouter] Response status: ${res.status}`);
175
+ }
176
+ if (!res.ok) {
177
+ const errText = await res.text();
178
+ console.error(`[OpenRouter] Error response:`, errText.slice(0, 500));
179
+ // Check for rate limit or temporary errors
180
+ if (res.status === 429 || res.status === 503) {
181
+ if (attempt < retries) {
182
+ console.warn(`[OpenRouter] Rate limited or service unavailable, retrying in ${2000 * (attempt + 1)}ms...`);
183
+ await new Promise(resolve => setTimeout(resolve, 2000 * (attempt + 1)));
184
+ continue;
185
+ }
186
+ }
187
+ throw new Error(`OpenRouter ${res.status}: ${errText.slice(0, 500)}`);
188
+ }
189
+ const data = (await res.json());
190
+ const content = data.choices?.[0]?.message?.content?.trim();
191
+ if (nodeEnv === 'development') {
192
+ console.log(`[OpenRouter] Response model: ${data.model || 'unknown'}`);
193
+ console.log(`[OpenRouter] Content length: ${content?.length || 0} chars`);
194
+ }
195
+ if (!content) {
196
+ if (attempt < retries) {
197
+ console.warn(`[OpenRouter] Empty response, retrying (${attempt + 1}/${retries})...`);
198
+ await new Promise(resolve => setTimeout(resolve, 1000));
199
+ continue;
200
+ }
201
+ throw new Error("Empty response from OpenRouter after retries");
202
+ }
203
+ if (nodeEnv === 'development') {
204
+ console.log(`[OpenRouter] ✓ Success on attempt ${attempt + 1}`);
205
+ }
206
+ return { content, model: data.model ?? models[0] };
207
+ }
208
+ catch (error) {
209
+ // Log error for debugging
210
+ if (error instanceof Error) {
211
+ console.error(`[OpenRouter] Request error: ${error.message}`);
212
+ }
213
+ if (attempt === retries) {
214
+ console.error(`[OpenRouter] ✗ All attempts failed:`, error);
215
+ // Return a graceful fallback instead of throwing
216
+ throw new Error(`API request failed after ${retries + 1} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
217
+ }
218
+ console.warn(`[OpenRouter] Attempt ${attempt + 1} failed, retrying...`, error);
219
+ await new Promise(resolve => setTimeout(resolve, 1000));
220
+ }
221
+ }
222
+ throw new Error("Failed after all retries");
223
+ }
224
+ function parseJsonFromModel(raw) {
225
+ const trimmed = raw.trim();
226
+ // Validate input is a string and not empty
227
+ if (typeof trimmed !== 'string' || trimmed.length === 0) {
228
+ throw new Error('Invalid input: empty or non-string value');
229
+ }
230
+ // Check for potentially malicious patterns
231
+ const dangerousPatterns = [
232
+ /<script/i,
233
+ /javascript:/i,
234
+ /on\w+\s*=/i,
235
+ /eval\s*\(/i,
236
+ /Function\s*\(/i,
237
+ ];
238
+ for (const pattern of dangerousPatterns) {
239
+ if (pattern.test(trimmed)) {
240
+ throw new Error('Potentially malicious content detected in JSON');
241
+ }
242
+ }
243
+ // Try to extract JSON from markdown code blocks
244
+ const fenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
245
+ let jsonStr = fenceMatch ? fenceMatch[1].trim() : trimmed;
246
+ // Remove any leading/trailing text that's not JSON
247
+ const jsonStart = jsonStr.indexOf('{');
248
+ const jsonEnd = jsonStr.lastIndexOf('}');
249
+ if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
250
+ jsonStr = jsonStr.substring(jsonStart, jsonEnd + 1);
251
+ }
252
+ try {
253
+ const parsed = JSON.parse(jsonStr);
254
+ // Validate parsed object is not a function or contains functions
255
+ if (typeof parsed === 'function') {
256
+ throw new Error('Invalid JSON: function detected');
257
+ }
258
+ // Recursively check for functions in objects
259
+ const checkForFunctions = (obj) => {
260
+ if (typeof obj === 'function') {
261
+ throw new Error('Invalid JSON: function detected in object');
262
+ }
263
+ if (obj && typeof obj === 'object') {
264
+ for (const key in obj) {
265
+ if (obj.hasOwnProperty(key)) {
266
+ checkForFunctions(obj[key]);
267
+ }
268
+ }
269
+ }
270
+ };
271
+ checkForFunctions(parsed);
272
+ return parsed;
273
+ }
274
+ catch (error) {
275
+ // Log initial parse error
276
+ if (error instanceof Error) {
277
+ console.error(`[JSON Parse] Initial parse failed: ${error.message}`);
278
+ }
279
+ // Try to fix common JSON issues
280
+ try {
281
+ // Fix unescaped quotes in strings
282
+ const fixed = jsonStr
283
+ .replace(/([^\\])"([^"]*)":/g, '$1\\"$2":') // Fix keys
284
+ .replace(/: "([^"]*)"([^,}\]])/g, ': "$1\\"$2'); // Fix values
285
+ const parsed = JSON.parse(fixed);
286
+ // Validate parsed object
287
+ if (typeof parsed === 'function') {
288
+ throw new Error('Invalid JSON: function detected');
289
+ }
290
+ const checkForFunctions = (obj) => {
291
+ if (typeof obj === 'function') {
292
+ throw new Error('Invalid JSON: function detected in object');
293
+ }
294
+ if (obj && typeof obj === 'object') {
295
+ for (const key in obj) {
296
+ if (obj.hasOwnProperty(key)) {
297
+ checkForFunctions(obj[key]);
298
+ }
299
+ }
300
+ }
301
+ };
302
+ checkForFunctions(parsed);
303
+ return parsed;
304
+ }
305
+ catch (fixError) {
306
+ console.error('[JSON Parse] Failed to parse JSON:', jsonStr.substring(0, 500));
307
+ console.error('[JSON Parse] Fix attempt also failed:', fixError);
308
+ throw new Error(`Invalid JSON response from AI: ${error instanceof Error ? error.message : 'Parse error'}`);
309
+ }
310
+ }
311
+ }