voracode 0.0.1
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/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/main.js +2842 -0
- package/package.json +57 -0
- package/src/cli/audit.ts +34 -0
- package/src/cli/config.ts +54 -0
- package/src/cli/doctor.ts +61 -0
- package/src/cli/init.ts +62 -0
- package/src/cli/key.ts +67 -0
- package/src/cli/lite.ts +65 -0
- package/src/cli/mcp.ts +221 -0
- package/src/cli/model.ts +103 -0
- package/src/cli/plugin.ts +49 -0
- package/src/cli/pro.ts +97 -0
- package/src/cli/run.ts +101 -0
- package/src/cli/session.ts +138 -0
- package/src/cli/skill.ts +156 -0
- package/src/cli/stats.ts +16 -0
- package/src/cli/update.ts +28 -0
- package/src/engine/agent.ts +256 -0
- package/src/engine/sub-agent.ts +122 -0
- package/src/main.ts +77 -0
- package/src/models/adapters/all-providers.ts +302 -0
- package/src/models/router.ts +426 -0
- package/src/security/owasp.ts +254 -0
- package/src/session/manager.ts +118 -0
- package/src/skills/self-improve/engine.ts +336 -0
- package/src/storage/config.ts +213 -0
- package/src/storage/database.ts +253 -0
- package/src/storage/mcp-config.ts +170 -0
- package/src/tools/executor.ts +362 -0
- package/src/ui/theme.ts +163 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2842 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/main.ts
|
|
4
|
+
import { Command as Command16 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/run.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/storage/database.ts
|
|
10
|
+
import Database from "better-sqlite3";
|
|
11
|
+
import { existsSync, mkdirSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
var VoraDatabase = class {
|
|
15
|
+
db;
|
|
16
|
+
dataDir;
|
|
17
|
+
constructor() {
|
|
18
|
+
this.dataDir = join(homedir(), ".local", "share", "voracode");
|
|
19
|
+
if (!existsSync(this.dataDir)) {
|
|
20
|
+
mkdirSync(this.dataDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
this.db = new Database(join(this.dataDir, "voracode.db"));
|
|
23
|
+
this.db.exec("PRAGMA journal_mode=WAL");
|
|
24
|
+
this.db.exec("PRAGMA foreign_keys=ON");
|
|
25
|
+
this.migrate();
|
|
26
|
+
}
|
|
27
|
+
migrate() {
|
|
28
|
+
this.db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
title TEXT,
|
|
32
|
+
project_path TEXT,
|
|
33
|
+
model_provider TEXT NOT NULL,
|
|
34
|
+
model_name TEXT NOT NULL,
|
|
35
|
+
total_tokens INTEGER DEFAULT 0,
|
|
36
|
+
total_turns INTEGER DEFAULT 0,
|
|
37
|
+
status TEXT DEFAULT 'active',
|
|
38
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
39
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
40
|
+
)
|
|
41
|
+
`);
|
|
42
|
+
this.db.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
46
|
+
role TEXT NOT NULL,
|
|
47
|
+
content TEXT NOT NULL,
|
|
48
|
+
tokens INTEGER,
|
|
49
|
+
tool_calls TEXT,
|
|
50
|
+
parent_id INTEGER REFERENCES messages(id),
|
|
51
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
this.db.exec(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS memory (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
key TEXT UNIQUE,
|
|
58
|
+
content TEXT NOT NULL,
|
|
59
|
+
category TEXT,
|
|
60
|
+
priority INTEGER DEFAULT 1,
|
|
61
|
+
tags TEXT,
|
|
62
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
63
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
64
|
+
)
|
|
65
|
+
`);
|
|
66
|
+
this.db.exec(`
|
|
67
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
68
|
+
content, tags,
|
|
69
|
+
content=memory,
|
|
70
|
+
content_rowid=id
|
|
71
|
+
)
|
|
72
|
+
`);
|
|
73
|
+
this.db.exec(`
|
|
74
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
75
|
+
name TEXT PRIMARY KEY,
|
|
76
|
+
description TEXT NOT NULL,
|
|
77
|
+
source TEXT NOT NULL,
|
|
78
|
+
path TEXT NOT NULL,
|
|
79
|
+
trigger_count INTEGER DEFAULT 0,
|
|
80
|
+
success_count INTEGER DEFAULT 0,
|
|
81
|
+
last_used_at TEXT,
|
|
82
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
83
|
+
)
|
|
84
|
+
`);
|
|
85
|
+
this.db.exec(`
|
|
86
|
+
CREATE TABLE IF NOT EXISTS stats (
|
|
87
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
88
|
+
date TEXT NOT NULL UNIQUE,
|
|
89
|
+
api_calls INTEGER DEFAULT 0,
|
|
90
|
+
tokens_in INTEGER DEFAULT 0,
|
|
91
|
+
tokens_out INTEGER DEFAULT 0,
|
|
92
|
+
cache_hits INTEGER DEFAULT 0,
|
|
93
|
+
errors INTEGER DEFAULT 0,
|
|
94
|
+
tasks_completed INTEGER DEFAULT 0
|
|
95
|
+
)
|
|
96
|
+
`);
|
|
97
|
+
this.db.exec(`
|
|
98
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
99
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
100
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
101
|
+
event_type TEXT NOT NULL,
|
|
102
|
+
details TEXT,
|
|
103
|
+
allowed INTEGER NOT NULL DEFAULT 1
|
|
104
|
+
)
|
|
105
|
+
`);
|
|
106
|
+
this.db.exec(`
|
|
107
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
108
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
109
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
110
|
+
message_id INTEGER NOT NULL REFERENCES messages(id),
|
|
111
|
+
description TEXT,
|
|
112
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
113
|
+
)
|
|
114
|
+
`);
|
|
115
|
+
}
|
|
116
|
+
// ── Session operations ──
|
|
117
|
+
createSession(provider, model, title = "Untitled") {
|
|
118
|
+
const id = crypto.randomUUID();
|
|
119
|
+
this.db.run(
|
|
120
|
+
"INSERT INTO sessions (id, title, model_provider, model_name) VALUES (?, ?, ?, ?)",
|
|
121
|
+
[id, title, provider, model]
|
|
122
|
+
);
|
|
123
|
+
return id;
|
|
124
|
+
}
|
|
125
|
+
getSession(id) {
|
|
126
|
+
return this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
127
|
+
}
|
|
128
|
+
listSessions(limit = 10) {
|
|
129
|
+
return this.db.query("SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
130
|
+
}
|
|
131
|
+
updateSession(id, updates) {
|
|
132
|
+
const keys = Object.keys(updates);
|
|
133
|
+
const setClause = keys.map((k) => `${k} = ?`).join(", ");
|
|
134
|
+
const values = keys.map((k) => updates[k]);
|
|
135
|
+
values.push(id);
|
|
136
|
+
this.db.run(`UPDATE sessions SET ${setClause}, updated_at = datetime('now') WHERE id = ?`, values);
|
|
137
|
+
}
|
|
138
|
+
deleteSession(id) {
|
|
139
|
+
this.db.prepare("DELETE FROM sessions WHERE id = ?").run([id]);
|
|
140
|
+
}
|
|
141
|
+
// ── Message operations ──
|
|
142
|
+
addMessage(sessionId, role, content, tokens = 0, toolCalls) {
|
|
143
|
+
const result = this.db.run(
|
|
144
|
+
"INSERT INTO messages (session_id, role, content, tokens, tool_calls) VALUES (?, ?, ?, ?, ?)",
|
|
145
|
+
[sessionId, role, content, tokens, toolCalls || null]
|
|
146
|
+
);
|
|
147
|
+
return Number(result.lastInsertRowid);
|
|
148
|
+
}
|
|
149
|
+
getMessages(sessionId, limit = 100) {
|
|
150
|
+
return this.db.query("SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC LIMIT ?").all(sessionId, limit);
|
|
151
|
+
}
|
|
152
|
+
// ── Memory operations ──
|
|
153
|
+
addMemory(key, content, category = "general", tags) {
|
|
154
|
+
this.db.run(
|
|
155
|
+
"INSERT OR REPLACE INTO memory (key, content, category, tags, updated_at) VALUES (?, ?, ?, ?, datetime('now'))",
|
|
156
|
+
[key, content, category, tags || null]
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
searchMemory(query) {
|
|
160
|
+
return this.db.query("SELECT m.* FROM memory_fts f JOIN memory m ON f.rowid = m.id WHERE memory_fts MATCH ? ORDER BY rank LIMIT 10").all(query);
|
|
161
|
+
}
|
|
162
|
+
// ── Stats operations ──
|
|
163
|
+
recordApiCall(tokensIn, tokensOut) {
|
|
164
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
165
|
+
this.db.run(`
|
|
166
|
+
INSERT INTO stats (date, api_calls, tokens_in, tokens_out) VALUES (?, 1, ?, ?)
|
|
167
|
+
ON CONFLICT(date) DO UPDATE SET api_calls = api_calls + 1, tokens_in = tokens_in + ?, tokens_out = tokens_out + ?
|
|
168
|
+
`, [today, tokensIn, tokensOut, tokensIn, tokensOut]);
|
|
169
|
+
}
|
|
170
|
+
getStats(days = 7) {
|
|
171
|
+
return this.db.query("SELECT * FROM stats WHERE date >= date('now', ? || ' days') ORDER BY date").all(`-${days}`);
|
|
172
|
+
}
|
|
173
|
+
// ── Audit operations ──
|
|
174
|
+
logAudit(eventType, details, allowed = true) {
|
|
175
|
+
this.db.run(
|
|
176
|
+
"INSERT INTO audit_log (event_type, details, allowed) VALUES (?, ?, ?)",
|
|
177
|
+
[eventType, details, allowed ? 1 : 0]
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
getAuditLog(limit = 50) {
|
|
181
|
+
return this.db.query("SELECT * FROM audit_log ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
182
|
+
}
|
|
183
|
+
// ── Checkpoint operations ──
|
|
184
|
+
createCheckpoint(sessionId, messageId, description) {
|
|
185
|
+
this.db.run(
|
|
186
|
+
"INSERT INTO checkpoints (session_id, message_id, description) VALUES (?, ?, ?)",
|
|
187
|
+
[sessionId, messageId, description]
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
// ── Utility ──
|
|
191
|
+
close() {
|
|
192
|
+
this.db.close();
|
|
193
|
+
}
|
|
194
|
+
getDbPath() {
|
|
195
|
+
return join(this.dataDir, "voracode.db");
|
|
196
|
+
}
|
|
197
|
+
// ── Generic database access (used by SelfImprovementEngine) ──
|
|
198
|
+
exec(sql) {
|
|
199
|
+
this.db.exec(sql);
|
|
200
|
+
}
|
|
201
|
+
query(sql) {
|
|
202
|
+
return this.db.prepare(sql);
|
|
203
|
+
}
|
|
204
|
+
run(sql, params = []) {
|
|
205
|
+
this.db.run(sql, params);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// src/models/router.ts
|
|
210
|
+
var PROVIDER_BASE_URLS = {
|
|
211
|
+
openai: "https://api.openai.com/v1",
|
|
212
|
+
deepseek: "https://api.deepseek.com/v1",
|
|
213
|
+
groq: "https://api.groq.com/openai/v1",
|
|
214
|
+
together: "https://api.together.xyz/v1",
|
|
215
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
216
|
+
ollama: "http://localhost:11434/v1",
|
|
217
|
+
huggingface: "https://router.huggingface.co/v1",
|
|
218
|
+
fireworks: "https://api.fireworks.ai/inference/v1",
|
|
219
|
+
cerebras: "https://api.cerebras.ai/v1",
|
|
220
|
+
anthropic: "https://api.anthropic.com/v1",
|
|
221
|
+
google: "https://generativelanguage.googleapis.com/v1beta"
|
|
222
|
+
};
|
|
223
|
+
var PROVIDER_KEY_PATTERNS = {
|
|
224
|
+
openai: /^(sk-|sx-)/,
|
|
225
|
+
anthropic: /^sk-ant-/,
|
|
226
|
+
deepseek: /^sk-/,
|
|
227
|
+
groq: /^gsk_/
|
|
228
|
+
};
|
|
229
|
+
var RateLimiter = class {
|
|
230
|
+
calls = /* @__PURE__ */ new Map();
|
|
231
|
+
windowMs = 6e4;
|
|
232
|
+
// 1 minute
|
|
233
|
+
maxCalls = 10;
|
|
234
|
+
// 10 calls per minute
|
|
235
|
+
canCall(provider) {
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
const timestamps = this.calls.get(provider) || [];
|
|
238
|
+
const recent = timestamps.filter((t) => now - t < this.windowMs);
|
|
239
|
+
this.calls.set(provider, recent);
|
|
240
|
+
if (recent.length >= this.maxCalls) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
recent.push(now);
|
|
244
|
+
this.calls.set(provider, recent);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
retryAfter(provider) {
|
|
248
|
+
const timestamps = this.calls.get(provider) || [];
|
|
249
|
+
if (timestamps.length === 0) return 0;
|
|
250
|
+
const oldest = timestamps[0];
|
|
251
|
+
return Math.max(0, Math.ceil((this.windowMs - (Date.now() - oldest)) / 1e3));
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var ModelRouter = class {
|
|
255
|
+
keychain = /* @__PURE__ */ new Map();
|
|
256
|
+
rateLimiter = new RateLimiter();
|
|
257
|
+
constructor() {
|
|
258
|
+
this.loadFromEnv();
|
|
259
|
+
}
|
|
260
|
+
loadFromEnv() {
|
|
261
|
+
const envMap = {
|
|
262
|
+
openai: process.env.OPENAI_API_KEY || process.env.OPENAI_KEY,
|
|
263
|
+
anthropic: process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_KEY,
|
|
264
|
+
deepseek: process.env.DEEPSEEK_API_KEY || process.env.DEEPSEEK_KEY,
|
|
265
|
+
groq: process.env.GROQ_API_KEY || process.env.GROQ_KEY,
|
|
266
|
+
google: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY,
|
|
267
|
+
openrouter: process.env.OPENROUTER_API_KEY,
|
|
268
|
+
huggingface: process.env.HF_API_KEY || process.env.HUGGINGFACE_KEY,
|
|
269
|
+
together: process.env.TOGETHER_API_KEY,
|
|
270
|
+
ollama: ""
|
|
271
|
+
// No key needed for local
|
|
272
|
+
};
|
|
273
|
+
for (const [provider, key] of Object.entries(envMap)) {
|
|
274
|
+
if (key) {
|
|
275
|
+
this.keychain.set(provider, key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get the active API key for a provider
|
|
281
|
+
*/
|
|
282
|
+
getKey(provider) {
|
|
283
|
+
return this.keychain.get(provider);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Set an API key for a provider
|
|
287
|
+
*/
|
|
288
|
+
setKey(provider, key) {
|
|
289
|
+
this.keychain.set(provider, key);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Check if a provider has a valid-looking API key configured
|
|
293
|
+
*/
|
|
294
|
+
hasValidKey(provider) {
|
|
295
|
+
const key = this.keychain.get(provider);
|
|
296
|
+
if (!key) return false;
|
|
297
|
+
const pattern = PROVIDER_KEY_PATTERNS[provider];
|
|
298
|
+
if (pattern) return pattern.test(key);
|
|
299
|
+
return key.length > 8;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Detect provider from model name
|
|
303
|
+
*/
|
|
304
|
+
detectProvider(modelName) {
|
|
305
|
+
const normalized = modelName.toLowerCase();
|
|
306
|
+
if (normalized.startsWith("gpt-") || normalized.startsWith("o3") || normalized.startsWith("o1")) return "openai";
|
|
307
|
+
if (normalized.startsWith("claude-") || normalized.startsWith("opus") || normalized.startsWith("sonnet") || normalized.startsWith("haiku")) return "anthropic";
|
|
308
|
+
if (normalized.startsWith("gemini-")) return "google";
|
|
309
|
+
if (normalized.startsWith("deepseek-")) return "deepseek";
|
|
310
|
+
if (normalized.startsWith("llama-") || normalized.startsWith("mixtral") || normalized.startsWith("qwen-")) return "groq";
|
|
311
|
+
if (normalized.startsWith("command-")) return "cohere";
|
|
312
|
+
return "openai";
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get available models for a provider
|
|
316
|
+
*/
|
|
317
|
+
getModels(provider) {
|
|
318
|
+
const modelMap = {
|
|
319
|
+
openai: ["gpt-4o", "gpt-4o-mini", "o3-mini", "o1"],
|
|
320
|
+
anthropic: ["claude-sonnet-4", "claude-opus-4", "claude-haiku-4"],
|
|
321
|
+
deepseek: ["deepseek-chat", "deepseek-v4-flash", "deepseek-v4-pro"],
|
|
322
|
+
groq: ["llama-3.1-8b", "llama-3.1-70b", "qwen-32b", "mixtral-8x7b"],
|
|
323
|
+
google: ["gemini-2.5-pro", "gemini-2.5-flash"],
|
|
324
|
+
ollama: ["llama3.2", "mistral", "deepseek-coder-v2", "qwen2.5-coder"],
|
|
325
|
+
openrouter: ["openrouter/auto"],
|
|
326
|
+
huggingface: ["HuggingFaceH4/zephyr-7b-beta"]
|
|
327
|
+
};
|
|
328
|
+
return modelMap[provider] || [];
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get provider base URL
|
|
332
|
+
*/
|
|
333
|
+
getBaseUrl(provider) {
|
|
334
|
+
return PROVIDER_BASE_URLS[provider] || "";
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Chat completion using OpenAI-compatible API
|
|
338
|
+
* Covers: DeepSeek, Groq, Together, Ollama, OpenRouter, HuggingFace
|
|
339
|
+
*/
|
|
340
|
+
async chatOpenAI(provider, messages, options) {
|
|
341
|
+
const baseUrl = this.getBaseUrl(provider);
|
|
342
|
+
const apiKey = this.keychain.get(provider);
|
|
343
|
+
if (!apiKey && provider !== "ollama") {
|
|
344
|
+
throw new Error(`No API key configured for ${provider}. Run: voracode key set ${provider}`);
|
|
345
|
+
}
|
|
346
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: {
|
|
349
|
+
"Content-Type": "application/json",
|
|
350
|
+
Authorization: apiKey ? `Bearer ${apiKey}` : ""
|
|
351
|
+
},
|
|
352
|
+
body: JSON.stringify({
|
|
353
|
+
model: options.model,
|
|
354
|
+
messages: messages.map((m) => ({
|
|
355
|
+
role: m.role,
|
|
356
|
+
content: m.content,
|
|
357
|
+
...m.name ? { name: m.name } : {},
|
|
358
|
+
...m.tool_calls ? { tool_calls: m.tool_calls } : {},
|
|
359
|
+
...m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}
|
|
360
|
+
})),
|
|
361
|
+
max_tokens: options.maxTokens || 4096,
|
|
362
|
+
temperature: options.temperature ?? 0.7,
|
|
363
|
+
stream: false,
|
|
364
|
+
...options.tools && options.tools.length > 0 ? { tools: options.tools, tool_choice: options.toolChoice || "auto" } : {}
|
|
365
|
+
}),
|
|
366
|
+
signal: AbortSignal.timeout(6e4)
|
|
367
|
+
// 60s timeout
|
|
368
|
+
});
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
const errorBody = await response.text().catch(() => "");
|
|
371
|
+
throw new Error(`${provider} API error (${response.status}): ${errorBody}`);
|
|
372
|
+
}
|
|
373
|
+
const data = await response.json();
|
|
374
|
+
return {
|
|
375
|
+
content: data.choices[0]?.message?.content || "",
|
|
376
|
+
model: data.model,
|
|
377
|
+
provider,
|
|
378
|
+
usage: {
|
|
379
|
+
inputTokens: data.usage?.prompt_tokens || 0,
|
|
380
|
+
outputTokens: data.usage?.completion_tokens || 0,
|
|
381
|
+
totalTokens: data.usage?.total_tokens || 0
|
|
382
|
+
},
|
|
383
|
+
toolCalls: data.choices[0]?.message?.tool_calls
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Chat completion using Anthropic Messages API
|
|
388
|
+
*/
|
|
389
|
+
async chatAnthropic(messages, options) {
|
|
390
|
+
const apiKey = this.keychain.get("anthropic");
|
|
391
|
+
if (!apiKey) throw new Error("No API key configured for Anthropic. Run: voracode key set anthropic");
|
|
392
|
+
const systemMsg = messages.find((m) => m.role === "system");
|
|
393
|
+
const nonSystem = messages.filter((m) => m.role !== "system");
|
|
394
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers: {
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
"x-api-key": apiKey,
|
|
399
|
+
"anthropic-version": "2023-06-01"
|
|
400
|
+
},
|
|
401
|
+
body: JSON.stringify({
|
|
402
|
+
model: options.model,
|
|
403
|
+
messages: nonSystem.map((m) => ({
|
|
404
|
+
role: m.role === "assistant" ? "assistant" : "user",
|
|
405
|
+
content: m.content
|
|
406
|
+
})),
|
|
407
|
+
system: systemMsg?.content,
|
|
408
|
+
max_tokens: options.maxTokens || 4096,
|
|
409
|
+
temperature: options.temperature ?? 0.7
|
|
410
|
+
}),
|
|
411
|
+
signal: AbortSignal.timeout(6e4)
|
|
412
|
+
// 60s timeout
|
|
413
|
+
});
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
const errorBody = await response.text().catch(() => "");
|
|
416
|
+
throw new Error(`Anthropic API error (${response.status}): ${errorBody}`);
|
|
417
|
+
}
|
|
418
|
+
const data = await response.json();
|
|
419
|
+
return {
|
|
420
|
+
content: data.content?.[0]?.text || "",
|
|
421
|
+
model: data.model,
|
|
422
|
+
provider: "anthropic",
|
|
423
|
+
usage: {
|
|
424
|
+
inputTokens: data.usage?.input_tokens || 0,
|
|
425
|
+
outputTokens: data.usage?.output_tokens || 0,
|
|
426
|
+
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0)
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Main chat method — auto-selects correct adapter based on model name
|
|
432
|
+
*/
|
|
433
|
+
async chat(modelRef, messages, options = {}) {
|
|
434
|
+
const provider = modelRef.includes("/") ? modelRef.split("/")[0] : this.detectProvider(modelRef);
|
|
435
|
+
const modelName = modelRef.includes("/") ? modelRef.slice(modelRef.indexOf("/") + 1) : modelRef;
|
|
436
|
+
if (!this.rateLimiter.canCall(provider)) {
|
|
437
|
+
const retryAfter = this.rateLimiter.retryAfter(provider);
|
|
438
|
+
throw new Error(`Rate limited on ${provider}. Retry in ${retryAfter}s. Use voracode pro for higher limits.`);
|
|
439
|
+
}
|
|
440
|
+
const fullOptions = { ...options, model: modelName };
|
|
441
|
+
try {
|
|
442
|
+
switch (provider) {
|
|
443
|
+
case "anthropic":
|
|
444
|
+
return await this.chatAnthropic(messages, fullOptions);
|
|
445
|
+
case "google":
|
|
446
|
+
return await this.chatGoogle(messages, fullOptions);
|
|
447
|
+
default:
|
|
448
|
+
return await this.chatOpenAI(provider, messages, fullOptions);
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const fallbacks = ["deepseek", "groq", "openrouter"];
|
|
452
|
+
for (const fb of fallbacks) {
|
|
453
|
+
if (fb === provider) continue;
|
|
454
|
+
const fbKey = this.keychain.get(fb);
|
|
455
|
+
if (fbKey) {
|
|
456
|
+
try {
|
|
457
|
+
return await this.chatOpenAI(fb, messages, { ...fullOptions, model: this.getModels(fb)[0] || fb });
|
|
458
|
+
} catch {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Chat completion using Google AI API
|
|
468
|
+
*/
|
|
469
|
+
async chatGoogle(messages, _options) {
|
|
470
|
+
const apiKey = this.keychain.get("google");
|
|
471
|
+
if (!apiKey) throw new Error("No API key configured for Google. Run: voracode key set google");
|
|
472
|
+
const response = await fetch(
|
|
473
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${_options.model}:generateContent`,
|
|
474
|
+
{
|
|
475
|
+
method: "POST",
|
|
476
|
+
headers: {
|
|
477
|
+
"Content-Type": "application/json",
|
|
478
|
+
"x-goog-api-key": apiKey
|
|
479
|
+
// Use header instead of URL query param
|
|
480
|
+
},
|
|
481
|
+
body: JSON.stringify({
|
|
482
|
+
contents: messages.filter((m) => m.role !== "system").map((m) => ({
|
|
483
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
484
|
+
parts: [{ text: m.content }]
|
|
485
|
+
})),
|
|
486
|
+
systemInstruction: messages.find((m) => m.role === "system")?.content ? { parts: [{ text: messages.find((m) => m.role === "system").content }] } : void 0,
|
|
487
|
+
generationConfig: {
|
|
488
|
+
maxOutputTokens: _options.maxTokens || 4096,
|
|
489
|
+
temperature: _options.temperature ?? 0.7
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
if (!response.ok) {
|
|
495
|
+
const errorBody = await response.text().catch(() => "");
|
|
496
|
+
throw new Error(`Google API error (${response.status}): ${errorBody}`);
|
|
497
|
+
}
|
|
498
|
+
const data = await response.json();
|
|
499
|
+
return {
|
|
500
|
+
content: data.candidates?.[0]?.content?.parts?.[0]?.text || "",
|
|
501
|
+
model: _options.model,
|
|
502
|
+
provider: "google",
|
|
503
|
+
usage: {
|
|
504
|
+
inputTokens: data.usageMetadata?.promptTokenCount || 0,
|
|
505
|
+
outputTokens: data.usageMetadata?.candidatesTokenCount || 0,
|
|
506
|
+
totalTokens: data.usageMetadata?.totalTokenCount || 0
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// src/tools/executor.ts
|
|
513
|
+
import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
514
|
+
import { execSync } from "child_process";
|
|
515
|
+
import { resolve } from "path";
|
|
516
|
+
var BLOCKED_COMMANDS = [
|
|
517
|
+
"rm -rf /",
|
|
518
|
+
"rm -rf /*",
|
|
519
|
+
"sudo ",
|
|
520
|
+
"mkfs",
|
|
521
|
+
"dd if=",
|
|
522
|
+
":(){",
|
|
523
|
+
"chmod 777 /",
|
|
524
|
+
"> /dev/",
|
|
525
|
+
"format C:",
|
|
526
|
+
"del /f /s",
|
|
527
|
+
"rd /s /q",
|
|
528
|
+
"shutdown",
|
|
529
|
+
"reboot",
|
|
530
|
+
"curl ",
|
|
531
|
+
"wget ",
|
|
532
|
+
// data exfiltration via download
|
|
533
|
+
"nc ",
|
|
534
|
+
"ncat ",
|
|
535
|
+
"netcat ",
|
|
536
|
+
// network connections
|
|
537
|
+
"bash -c",
|
|
538
|
+
"sh -c",
|
|
539
|
+
"powershell -c",
|
|
540
|
+
// encoded cmd execution
|
|
541
|
+
"chmod +x",
|
|
542
|
+
// making files executable
|
|
543
|
+
">/dev/sda",
|
|
544
|
+
">/dev/sdb",
|
|
545
|
+
// writing to raw disk
|
|
546
|
+
"| bash",
|
|
547
|
+
"| sh",
|
|
548
|
+
"| zsh",
|
|
549
|
+
// pipe to shell
|
|
550
|
+
"`",
|
|
551
|
+
"$("
|
|
552
|
+
// command substitution
|
|
553
|
+
];
|
|
554
|
+
var PROJECT_ROOT = resolve(process.cwd());
|
|
555
|
+
var ToolExecutor = class {
|
|
556
|
+
db;
|
|
557
|
+
tools;
|
|
558
|
+
constructor(db) {
|
|
559
|
+
this.db = db;
|
|
560
|
+
this.tools = /* @__PURE__ */ new Map();
|
|
561
|
+
this.registerBuiltins();
|
|
562
|
+
}
|
|
563
|
+
registerBuiltins() {
|
|
564
|
+
this.tools.set("file_read", {
|
|
565
|
+
name: "file_read",
|
|
566
|
+
description: "Read the contents of a file",
|
|
567
|
+
execute: (args) => this.fileRead(args.path)
|
|
568
|
+
});
|
|
569
|
+
this.tools.set("file_write", {
|
|
570
|
+
name: "file_write",
|
|
571
|
+
description: "Write content to a file",
|
|
572
|
+
execute: (args) => this.fileWrite(args.path, args.content)
|
|
573
|
+
});
|
|
574
|
+
this.tools.set("file_edit", {
|
|
575
|
+
name: "file_edit",
|
|
576
|
+
description: "Edit a file by replacing text",
|
|
577
|
+
execute: (args) => this.fileEdit(args.path, args.old_string, args.new_string)
|
|
578
|
+
});
|
|
579
|
+
this.tools.set("bash", {
|
|
580
|
+
name: "bash",
|
|
581
|
+
description: "Execute a shell command (sandboxed)",
|
|
582
|
+
execute: (args) => this.execBash(args.command)
|
|
583
|
+
});
|
|
584
|
+
this.tools.set("git_status", {
|
|
585
|
+
name: "git_status",
|
|
586
|
+
description: "Show git status",
|
|
587
|
+
execute: () => this.execGit(["status"])
|
|
588
|
+
});
|
|
589
|
+
this.tools.set("git_diff", {
|
|
590
|
+
name: "git_diff",
|
|
591
|
+
description: "Show git diff",
|
|
592
|
+
execute: () => this.execGit(["diff"])
|
|
593
|
+
});
|
|
594
|
+
this.tools.set("git_commit", {
|
|
595
|
+
name: "git_commit",
|
|
596
|
+
description: "Commit changes with a message",
|
|
597
|
+
execute: (args) => this.execGit(["commit", "-m", args.message])
|
|
598
|
+
});
|
|
599
|
+
this.tools.set("code_search", {
|
|
600
|
+
name: "code_search",
|
|
601
|
+
description: "Search codebase for a pattern",
|
|
602
|
+
execute: (args) => this.searchCode(args.pattern, args.path)
|
|
603
|
+
});
|
|
604
|
+
this.tools.set("web_fetch", {
|
|
605
|
+
name: "web_fetch",
|
|
606
|
+
description: "Fetch content from a URL",
|
|
607
|
+
execute: (args) => this.webFetch(args.url)
|
|
608
|
+
});
|
|
609
|
+
this.tools.set("think", {
|
|
610
|
+
name: "think",
|
|
611
|
+
description: "Reason through a problem step by step",
|
|
612
|
+
execute: (args) => `Reasoning: ${args.reasoning || "No reasoning provided."}`
|
|
613
|
+
});
|
|
614
|
+
this.tools.set("list_files", {
|
|
615
|
+
name: "list_files",
|
|
616
|
+
description: "List files in a directory",
|
|
617
|
+
execute: (args) => this.listDir(args.path)
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Execute a tool by name with given arguments
|
|
622
|
+
*/
|
|
623
|
+
async execute(toolName, argsJson) {
|
|
624
|
+
const tool = this.tools.get(toolName);
|
|
625
|
+
if (!tool) {
|
|
626
|
+
return `Error: Unknown tool '${toolName}'. Available: ${Array.from(this.tools.keys()).join(", ")}`;
|
|
627
|
+
}
|
|
628
|
+
let args = {};
|
|
629
|
+
try {
|
|
630
|
+
args = JSON.parse(argsJson);
|
|
631
|
+
} catch {
|
|
632
|
+
return `Error: Invalid JSON arguments for tool '${toolName}'`;
|
|
633
|
+
}
|
|
634
|
+
this.db.logAudit("tool_execute", JSON.stringify({ tool: toolName, args }));
|
|
635
|
+
try {
|
|
636
|
+
const result = await Promise.resolve(tool.execute(args));
|
|
637
|
+
return result;
|
|
638
|
+
} catch (error) {
|
|
639
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
640
|
+
this.db.logAudit("tool_error", `${toolName}: ${errorMsg}`, false);
|
|
641
|
+
return `Error executing ${toolName}: ${errorMsg}`;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* List all registered tools
|
|
646
|
+
*/
|
|
647
|
+
listTools() {
|
|
648
|
+
return Array.from(this.tools.values());
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Register a custom tool (for plugins)
|
|
652
|
+
*/
|
|
653
|
+
registerTool(tool) {
|
|
654
|
+
this.tools.set(tool.name, tool);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Get tool definitions in OpenAI function-calling format
|
|
658
|
+
*/
|
|
659
|
+
getToolSchemas() {
|
|
660
|
+
const schemas = [];
|
|
661
|
+
const paramSchemas = {
|
|
662
|
+
file_read: { path: { type: "string", description: "File path" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "" } },
|
|
663
|
+
file_write: { path: { type: "string", description: "File path" }, content: { type: "string", description: "Content to write" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "" } },
|
|
664
|
+
file_edit: { path: { type: "string", description: "File path" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "Text to replace" }, new_string: { type: "string", description: "Replacement text" }, command: { type: "string", description: "" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "" } },
|
|
665
|
+
bash: { path: { type: "string", description: "" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "Shell command to execute" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "" } },
|
|
666
|
+
git_status: { path: { type: "string", description: "" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "" } },
|
|
667
|
+
git_diff: { path: { type: "string", description: "" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "" } },
|
|
668
|
+
git_commit: { path: { type: "string", description: "" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "" }, message: { type: "string", description: "Commit message" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "" } },
|
|
669
|
+
code_search: { path: { type: "string", description: "Search path" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "Search pattern (regex)" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "" } },
|
|
670
|
+
web_fetch: { path: { type: "string", description: "" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "URL to fetch" }, reasoning: { type: "string", description: "" } },
|
|
671
|
+
think: { path: { type: "string", description: "" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "Step-by-step reasoning" } },
|
|
672
|
+
list_files: { path: { type: "string", description: "Directory path" }, content: { type: "string", description: "" }, old_string: { type: "string", description: "" }, new_string: { type: "string", description: "" }, command: { type: "string", description: "" }, message: { type: "string", description: "" }, pattern: { type: "string", description: "" }, url: { type: "string", description: "" }, reasoning: { type: "string", description: "" } }
|
|
673
|
+
};
|
|
674
|
+
for (const [, tool] of this.tools) {
|
|
675
|
+
const props = {};
|
|
676
|
+
const required = [];
|
|
677
|
+
const params = paramSchemas[tool.name];
|
|
678
|
+
if (params) {
|
|
679
|
+
for (const [key, val] of Object.entries(params)) {
|
|
680
|
+
if (val.description) {
|
|
681
|
+
props[key] = val;
|
|
682
|
+
required.push(key);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
schemas.push({
|
|
687
|
+
type: "function",
|
|
688
|
+
function: {
|
|
689
|
+
name: tool.name,
|
|
690
|
+
description: tool.description,
|
|
691
|
+
parameters: {
|
|
692
|
+
type: "object",
|
|
693
|
+
properties: props,
|
|
694
|
+
required: required.length > 0 ? required : void 0
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
return schemas;
|
|
700
|
+
}
|
|
701
|
+
// ── Tool implementations ──
|
|
702
|
+
validatePath(resolved) {
|
|
703
|
+
const normalized = resolve(resolved);
|
|
704
|
+
if (!normalized.startsWith(PROJECT_ROOT)) {
|
|
705
|
+
throw new Error(`Path traversal blocked: '${resolved}' is outside project`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
fileRead(path) {
|
|
709
|
+
if (!path) return "Error: path is required";
|
|
710
|
+
const resolved = resolve(PROJECT_ROOT, path);
|
|
711
|
+
try {
|
|
712
|
+
this.validatePath(resolved);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
return `Error: ${e instanceof Error ? e.message : "invalid path"}`;
|
|
715
|
+
}
|
|
716
|
+
if (!existsSync2(resolved)) return `Error: File not found: ${path}`;
|
|
717
|
+
return readFileSync(resolved, "utf-8");
|
|
718
|
+
}
|
|
719
|
+
fileWrite(path, content) {
|
|
720
|
+
if (!path || content === void 0) return "Error: path and content are required";
|
|
721
|
+
const resolved = resolve(PROJECT_ROOT, path);
|
|
722
|
+
try {
|
|
723
|
+
this.validatePath(resolved);
|
|
724
|
+
} catch (e) {
|
|
725
|
+
return `Error: ${e instanceof Error ? e.message : "invalid path"}`;
|
|
726
|
+
}
|
|
727
|
+
writeFileSync(resolved, content, "utf-8");
|
|
728
|
+
return `Written ${content.length} bytes to ${path}`;
|
|
729
|
+
}
|
|
730
|
+
fileEdit(path, oldString, newString) {
|
|
731
|
+
if (!path || !oldString || newString === void 0) return "Error: path, old_string, and new_string are required";
|
|
732
|
+
const resolved = resolve(PROJECT_ROOT, path);
|
|
733
|
+
try {
|
|
734
|
+
this.validatePath(resolved);
|
|
735
|
+
} catch (e) {
|
|
736
|
+
return `Error: ${e instanceof Error ? e.message : "invalid path"}`;
|
|
737
|
+
}
|
|
738
|
+
if (!existsSync2(resolved)) return `Error: File not found: ${path}`;
|
|
739
|
+
let content = readFileSync(resolved, "utf-8");
|
|
740
|
+
if (!content.includes(oldString)) return `Error: old_string not found in ${path}`;
|
|
741
|
+
content = content.replace(oldString, newString);
|
|
742
|
+
writeFileSync(resolved, content, "utf-8");
|
|
743
|
+
return `Edited ${path}: replaced "${oldString}" with "${newString}"`;
|
|
744
|
+
}
|
|
745
|
+
execBash(command) {
|
|
746
|
+
if (!command) return "Error: command is required";
|
|
747
|
+
for (const blocked of BLOCKED_COMMANDS) {
|
|
748
|
+
if (command.toLowerCase().includes(blocked.toLowerCase())) {
|
|
749
|
+
this.db.logAudit("blocked_command", command, false);
|
|
750
|
+
return `Error: Command blocked for security: matches pattern "${blocked}"`;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
const result = execSync(command, {
|
|
755
|
+
cwd: process.cwd(),
|
|
756
|
+
timeout: 3e4,
|
|
757
|
+
maxBuffer: 1048576,
|
|
758
|
+
encoding: "utf-8",
|
|
759
|
+
shell: process.platform === "win32" ? "cmd.exe" : "/bin/bash"
|
|
760
|
+
});
|
|
761
|
+
return result || "(command completed with no output)";
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (error instanceof Error) {
|
|
764
|
+
return `Error: ${error.message}`;
|
|
765
|
+
}
|
|
766
|
+
return "Error: Command execution failed";
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
execGit(args) {
|
|
770
|
+
try {
|
|
771
|
+
const result = execSync(`git ${args.join(" ")}`, {
|
|
772
|
+
cwd: process.cwd(),
|
|
773
|
+
timeout: 1e4,
|
|
774
|
+
maxBuffer: 1048576,
|
|
775
|
+
encoding: "utf-8"
|
|
776
|
+
});
|
|
777
|
+
return result || "(git command completed with no output)";
|
|
778
|
+
} catch (error) {
|
|
779
|
+
if (error instanceof Error) {
|
|
780
|
+
return `Git error: ${error.message}`;
|
|
781
|
+
}
|
|
782
|
+
return "Git error: Command failed";
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
searchCode(pattern, path = ".") {
|
|
786
|
+
if (!pattern) return "Error: pattern is required";
|
|
787
|
+
const isWin = process.platform === "win32";
|
|
788
|
+
const searchCmd = isWin ? `findstr /n /i /c:"${pattern}" "${resolve(PROJECT_ROOT, path)}\\*"` : `rg -n "${pattern}" "${resolve(PROJECT_ROOT, path)}" --max-count 20`;
|
|
789
|
+
try {
|
|
790
|
+
const result = execSync(searchCmd, {
|
|
791
|
+
cwd: PROJECT_ROOT,
|
|
792
|
+
timeout: 1e4,
|
|
793
|
+
maxBuffer: 1048576,
|
|
794
|
+
encoding: "utf-8",
|
|
795
|
+
shell: isWin ? "cmd.exe" : "/bin/bash"
|
|
796
|
+
});
|
|
797
|
+
return result || "No matches found.";
|
|
798
|
+
} catch {
|
|
799
|
+
return "No matches found.";
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async webFetch(url) {
|
|
803
|
+
if (!url) return "Error: url is required";
|
|
804
|
+
try {
|
|
805
|
+
const parsed = new URL(url);
|
|
806
|
+
const blockedHosts = ["localhost", "127.0.0.1", "0.0.0.0", "::1"];
|
|
807
|
+
if (blockedHosts.includes(parsed.hostname)) {
|
|
808
|
+
return "Error: Cannot fetch from localhost for security reasons.";
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
return "Error: Invalid URL";
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
815
|
+
const text = await response.text();
|
|
816
|
+
return text.slice(0, 1e4);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
return `Error fetching URL: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
listDir(path = ".") {
|
|
822
|
+
const resolved = resolve(PROJECT_ROOT, path);
|
|
823
|
+
try {
|
|
824
|
+
this.validatePath(resolved);
|
|
825
|
+
} catch (e) {
|
|
826
|
+
return `Error: ${e instanceof Error ? e.message : "invalid path"}`;
|
|
827
|
+
}
|
|
828
|
+
if (!existsSync2(resolved)) return `Error: Directory not found: ${path}`;
|
|
829
|
+
const isWin = process.platform === "win32";
|
|
830
|
+
const cmd = isWin ? `dir /b "${resolved}"` : `ls -la "${resolved}"`;
|
|
831
|
+
try {
|
|
832
|
+
const result = execSync(cmd, {
|
|
833
|
+
timeout: 5e3,
|
|
834
|
+
maxBuffer: 1e4,
|
|
835
|
+
encoding: "utf-8",
|
|
836
|
+
shell: isWin ? "cmd.exe" : "/bin/bash"
|
|
837
|
+
});
|
|
838
|
+
return result;
|
|
839
|
+
} catch {
|
|
840
|
+
return `Error: Could not list directory ${path}`;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// src/skills/self-improve/engine.ts
|
|
846
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
847
|
+
import { join as join2 } from "path";
|
|
848
|
+
import { homedir as homedir2 } from "os";
|
|
849
|
+
var SelfImprovementEngine = class {
|
|
850
|
+
db;
|
|
851
|
+
skillsDir;
|
|
852
|
+
constructor(db) {
|
|
853
|
+
this.db = db;
|
|
854
|
+
this.skillsDir = join2(homedir2(), ".config", "voracode", "skills");
|
|
855
|
+
if (!existsSync3(this.skillsDir)) {
|
|
856
|
+
mkdirSync2(this.skillsDir, { recursive: true });
|
|
857
|
+
}
|
|
858
|
+
this.ensureTables();
|
|
859
|
+
}
|
|
860
|
+
ensureTables() {
|
|
861
|
+
this.db.exec(`
|
|
862
|
+
CREATE TABLE IF NOT EXISTS task_patterns (
|
|
863
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
864
|
+
signature TEXT UNIQUE NOT NULL,
|
|
865
|
+
description TEXT NOT NULL,
|
|
866
|
+
tool_sequence TEXT NOT NULL,
|
|
867
|
+
files_changed TEXT,
|
|
868
|
+
commands_run TEXT,
|
|
869
|
+
success_count INTEGER DEFAULT 0,
|
|
870
|
+
failure_count INTEGER DEFAULT 0,
|
|
871
|
+
last_seen TEXT DEFAULT (datetime('now')),
|
|
872
|
+
suggested_skill INTEGER DEFAULT 0,
|
|
873
|
+
user_decision TEXT DEFAULT 'pending',
|
|
874
|
+
skill_path TEXT
|
|
875
|
+
)
|
|
876
|
+
`);
|
|
877
|
+
this.db.exec(`
|
|
878
|
+
CREATE TABLE IF NOT EXISTS user_preferences (
|
|
879
|
+
key TEXT PRIMARY KEY,
|
|
880
|
+
value TEXT NOT NULL,
|
|
881
|
+
category TEXT,
|
|
882
|
+
confidence REAL DEFAULT 0.5,
|
|
883
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
884
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
885
|
+
)
|
|
886
|
+
`);
|
|
887
|
+
this.db.exec(`
|
|
888
|
+
CREATE TABLE IF NOT EXISTS skill_history (
|
|
889
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
890
|
+
skill_name TEXT NOT NULL,
|
|
891
|
+
version INTEGER DEFAULT 1,
|
|
892
|
+
action TEXT NOT NULL,
|
|
893
|
+
content TEXT NOT NULL,
|
|
894
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
895
|
+
)
|
|
896
|
+
`);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Record a completed task and check for patterns
|
|
900
|
+
*/
|
|
901
|
+
recordTask(description, toolsUsed, filesChanged, commandsRun, success3) {
|
|
902
|
+
const signature = this.createSignature(toolsUsed, description);
|
|
903
|
+
const existing = this.db.query(
|
|
904
|
+
"SELECT * FROM task_patterns WHERE signature = ?"
|
|
905
|
+
).get(signature);
|
|
906
|
+
if (existing) {
|
|
907
|
+
const newSuccess = existing.success_count + (success3 ? 1 : 0);
|
|
908
|
+
const newFailure = existing.failure_count + (success3 ? 0 : 1);
|
|
909
|
+
const total = newSuccess + newFailure;
|
|
910
|
+
const successRate = total > 0 ? newSuccess / total : 0;
|
|
911
|
+
this.db.run(
|
|
912
|
+
`UPDATE task_patterns SET
|
|
913
|
+
success_count = ?, failure_count = ?, last_seen = datetime('now')
|
|
914
|
+
WHERE signature = ?`,
|
|
915
|
+
[newSuccess, newFailure, signature]
|
|
916
|
+
);
|
|
917
|
+
if (total >= 3 && successRate >= 0.8 && !existing.suggested_skill) {
|
|
918
|
+
const pattern = {
|
|
919
|
+
id: existing.id,
|
|
920
|
+
signature,
|
|
921
|
+
description: existing.description,
|
|
922
|
+
toolSequence: JSON.parse(existing.tool_sequence),
|
|
923
|
+
filesChanged: JSON.parse(existing.files_changed || "[]"),
|
|
924
|
+
commandsRun: JSON.parse(existing.commands_run || "[]"),
|
|
925
|
+
successCount: newSuccess,
|
|
926
|
+
failureCount: newFailure,
|
|
927
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
928
|
+
suggestedSkill: true,
|
|
929
|
+
userDecision: "pending"
|
|
930
|
+
};
|
|
931
|
+
this.db.run("UPDATE task_patterns SET suggested_skill = 1 WHERE signature = ?", [signature]);
|
|
932
|
+
return pattern;
|
|
933
|
+
}
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
this.db.run(
|
|
937
|
+
`INSERT INTO task_patterns
|
|
938
|
+
(signature, description, tool_sequence, files_changed, commands_run, success_count, failure_count)
|
|
939
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
940
|
+
[
|
|
941
|
+
signature,
|
|
942
|
+
description,
|
|
943
|
+
JSON.stringify(toolsUsed),
|
|
944
|
+
JSON.stringify(filesChanged),
|
|
945
|
+
JSON.stringify(commandsRun),
|
|
946
|
+
success3 ? 1 : 0,
|
|
947
|
+
success3 ? 0 : 1
|
|
948
|
+
]
|
|
949
|
+
);
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Create a pattern signature from tools and task description
|
|
954
|
+
*/
|
|
955
|
+
createSignature(toolsUsed, description) {
|
|
956
|
+
const tools = toolsUsed.map((t) => t.toLowerCase()).sort().join(",");
|
|
957
|
+
const taskType = this.classifyTask(description);
|
|
958
|
+
return `${taskType}|${tools}`;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Classify a task into a category
|
|
962
|
+
*/
|
|
963
|
+
classifyTask(description) {
|
|
964
|
+
const d = description.toLowerCase();
|
|
965
|
+
if (d.includes("create") || d.includes("new") || d.includes("make")) return "create";
|
|
966
|
+
if (d.includes("fix") || d.includes("bug") || d.includes("error")) return "fix";
|
|
967
|
+
if (d.includes("refactor") || d.includes("optimize") || d.includes("improve")) return "refactor";
|
|
968
|
+
if (d.includes("test") || d.includes("spec")) return "test";
|
|
969
|
+
if (d.includes("deploy") || d.includes("publish")) return "deploy";
|
|
970
|
+
if (d.includes("explain") || d.includes("understand") || d.includes("how")) return "explain";
|
|
971
|
+
return "general";
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Generate a SKILL.md from a pattern
|
|
975
|
+
*/
|
|
976
|
+
generateSkill(pattern) {
|
|
977
|
+
const skillName = pattern.description.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "learned-skill";
|
|
978
|
+
const skillDir = join2(this.skillsDir, skillName);
|
|
979
|
+
mkdirSync2(skillDir, { recursive: true });
|
|
980
|
+
const skillPath = join2(skillDir, "SKILL.md");
|
|
981
|
+
const content = `---
|
|
982
|
+
name: ${skillName}
|
|
983
|
+
description: Auto-learned skill for: ${pattern.description}
|
|
984
|
+
version: 1.0.0
|
|
985
|
+
source: learned
|
|
986
|
+
---
|
|
987
|
+
|
|
988
|
+
## When to use
|
|
989
|
+
Use this skill when performing: ${pattern.description}
|
|
990
|
+
|
|
991
|
+
## Pattern discovered
|
|
992
|
+
- Tools used: ${pattern.toolSequence.join(", ")}
|
|
993
|
+
- Success rate: ${(pattern.successCount / (pattern.successCount + pattern.failureCount) * 100).toFixed(0)}%
|
|
994
|
+
- Times seen: ${pattern.successCount + pattern.failureCount}
|
|
995
|
+
|
|
996
|
+
## Instructions
|
|
997
|
+
1. Follow the same tool sequence that worked before
|
|
998
|
+
2. Apply to the specific project structure
|
|
999
|
+
3. Verify the result matches expectations
|
|
1000
|
+
|
|
1001
|
+
## Notes
|
|
1002
|
+
- This skill was auto-generated by VORACODE's self-improvement engine
|
|
1003
|
+
- You can edit or delete it: voracode skill remove ${skillName}
|
|
1004
|
+
- Rollback: voracode skill history ${skillName}
|
|
1005
|
+
`;
|
|
1006
|
+
writeFileSync2(skillPath, content, "utf-8");
|
|
1007
|
+
this.db.run(
|
|
1008
|
+
"INSERT INTO skill_history (skill_name, version, action, content) VALUES (?, ?, ?, ?)",
|
|
1009
|
+
[skillName, 1, "created", content]
|
|
1010
|
+
);
|
|
1011
|
+
this.db.run(
|
|
1012
|
+
"UPDATE task_patterns SET skill_path = ?, user_decision = 'accepted' WHERE signature = ?",
|
|
1013
|
+
[skillPath, pattern.signature]
|
|
1014
|
+
);
|
|
1015
|
+
return skillName;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* User rejects a suggested skill
|
|
1019
|
+
*/
|
|
1020
|
+
rejectPattern(signature, permanently = false) {
|
|
1021
|
+
this.db.run(
|
|
1022
|
+
"UPDATE task_patterns SET user_decision = ? WHERE signature = ?",
|
|
1023
|
+
[permanently ? "never" : "rejected", signature]
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Record a user preference
|
|
1028
|
+
*/
|
|
1029
|
+
recordPreference(key, value, category = "general") {
|
|
1030
|
+
this.db.run(
|
|
1031
|
+
`INSERT OR REPLACE INTO user_preferences
|
|
1032
|
+
(key, value, category, updated_at) VALUES (?, ?, ?, datetime('now'))`,
|
|
1033
|
+
[key, value, category]
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Get all learned patterns
|
|
1038
|
+
*/
|
|
1039
|
+
getPatterns() {
|
|
1040
|
+
const rows = this.db.query("SELECT * FROM task_patterns ORDER BY last_seen DESC").all();
|
|
1041
|
+
return rows.map((r) => ({
|
|
1042
|
+
id: r.id,
|
|
1043
|
+
signature: r.signature,
|
|
1044
|
+
description: r.description,
|
|
1045
|
+
toolSequence: JSON.parse(r.tool_sequence),
|
|
1046
|
+
filesChanged: JSON.parse(r.files_changed || "[]"),
|
|
1047
|
+
commandsRun: JSON.parse(r.commands_run || "[]"),
|
|
1048
|
+
successCount: r.success_count || 0,
|
|
1049
|
+
failureCount: r.failure_count || 0,
|
|
1050
|
+
lastSeen: r.last_seen || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1051
|
+
suggestedSkill: Boolean(r.suggested_skill),
|
|
1052
|
+
userDecision: r.user_decision || "pending",
|
|
1053
|
+
skillPath: r.skill_path
|
|
1054
|
+
}));
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Get user preferences
|
|
1058
|
+
*/
|
|
1059
|
+
getPreferences() {
|
|
1060
|
+
const rows = this.db.query("SELECT key, value FROM user_preferences").all();
|
|
1061
|
+
const prefs = {};
|
|
1062
|
+
for (const r of rows) {
|
|
1063
|
+
prefs[r.key] = r.value;
|
|
1064
|
+
}
|
|
1065
|
+
return prefs;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Rollback a skill to a previous version
|
|
1069
|
+
*/
|
|
1070
|
+
rollbackSkill(skillName) {
|
|
1071
|
+
const history = this.db.query(
|
|
1072
|
+
"SELECT * FROM skill_history WHERE skill_name = ? ORDER BY created_at DESC LIMIT 2"
|
|
1073
|
+
).all(skillName);
|
|
1074
|
+
if (history.length < 2) return false;
|
|
1075
|
+
const previousVersion = history[1];
|
|
1076
|
+
const skillDir = join2(this.skillsDir, skillName);
|
|
1077
|
+
const skillPath = join2(skillDir, "SKILL.md");
|
|
1078
|
+
if (existsSync3(skillPath)) {
|
|
1079
|
+
writeFileSync2(skillPath, previousVersion.content, "utf-8");
|
|
1080
|
+
this.db.run(
|
|
1081
|
+
"INSERT INTO skill_history (skill_name, version, action, content) VALUES (?, ?, ?, ?)",
|
|
1082
|
+
[skillName, previousVersion.version + 1, "rollback", previousVersion.content]
|
|
1083
|
+
);
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Get storage statistics
|
|
1090
|
+
*/
|
|
1091
|
+
getStats() {
|
|
1092
|
+
const patterns = this.db.query("SELECT COUNT(*) as c FROM task_patterns").get().c;
|
|
1093
|
+
const preferences = this.db.query("SELECT COUNT(*) as c FROM user_preferences").get().c;
|
|
1094
|
+
const skills = this.db.query("SELECT COUNT(*) as c FROM task_patterns WHERE skill_path IS NOT NULL").get().c;
|
|
1095
|
+
const storageKB = Math.round((patterns * 2 + preferences * 0.1) * 10) / 10;
|
|
1096
|
+
return { patterns, preferences, skills, storageKB };
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// src/engine/agent.ts
|
|
1101
|
+
var Agent = class {
|
|
1102
|
+
router;
|
|
1103
|
+
db;
|
|
1104
|
+
tools;
|
|
1105
|
+
sie;
|
|
1106
|
+
systemPrompt;
|
|
1107
|
+
constructor() {
|
|
1108
|
+
this.router = new ModelRouter();
|
|
1109
|
+
this.db = new VoraDatabase();
|
|
1110
|
+
this.tools = new ToolExecutor(this.db);
|
|
1111
|
+
this.sie = new SelfImprovementEngine(this.db);
|
|
1112
|
+
this.systemPrompt = this.buildSystemPrompt();
|
|
1113
|
+
}
|
|
1114
|
+
buildSystemPrompt() {
|
|
1115
|
+
return `You are VORACODE, an AI engineering partner that lives in the terminal.
|
|
1116
|
+
|
|
1117
|
+
You help users with coding tasks. You have access to real tools that let you read, write, and execute code.
|
|
1118
|
+
|
|
1119
|
+
## How you work
|
|
1120
|
+
1. Understand what the user wants
|
|
1121
|
+
2. Use tools to explore the project when needed
|
|
1122
|
+
3. Plan the changes
|
|
1123
|
+
4. Execute step by step using the tools available to you
|
|
1124
|
+
5. Tell the user when you're done
|
|
1125
|
+
|
|
1126
|
+
## Rules
|
|
1127
|
+
- NEVER execute dangerous commands (rm -rf /, sudo, mkfs, dd, fork bombs)
|
|
1128
|
+
- NEVER output API keys, passwords, or secrets
|
|
1129
|
+
- Show what you're doing before doing it
|
|
1130
|
+
- Ask the user before destructive operations
|
|
1131
|
+
- Use the think tool to reason through complex problems before acting
|
|
1132
|
+
- When the task is complete, summarize what you did
|
|
1133
|
+
|
|
1134
|
+
## Available Tools
|
|
1135
|
+
You have file_read, file_write, file_edit, bash, git_status, git_diff, git_commit,
|
|
1136
|
+
code_search, web_fetch, think, and list_files at your disposal.
|
|
1137
|
+
Use them to accomplish the task. Do NOT just describe what to do \u2014 USE the tools.
|
|
1138
|
+
|
|
1139
|
+
When the task is complete, summarize what was done.`;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Run the agent loop with full tool-calling
|
|
1143
|
+
*/
|
|
1144
|
+
async runTurn(sessionId, userMessage, modelRef, options) {
|
|
1145
|
+
const startTime = Date.now();
|
|
1146
|
+
const maxTurns = options?.maxTurns || 15;
|
|
1147
|
+
let totalTokens = 0;
|
|
1148
|
+
let turns = 0;
|
|
1149
|
+
let steps = 0;
|
|
1150
|
+
let toolOnlyTurns = 0;
|
|
1151
|
+
const toolSchemas = this.tools.getToolSchemas();
|
|
1152
|
+
const messages = [
|
|
1153
|
+
{ role: "system", content: this.systemPrompt },
|
|
1154
|
+
{ role: "user", content: userMessage }
|
|
1155
|
+
];
|
|
1156
|
+
this.db.addMessage(sessionId, "user", userMessage);
|
|
1157
|
+
const toolsUsed = [];
|
|
1158
|
+
const filesChanged = [];
|
|
1159
|
+
const commandsRun = [];
|
|
1160
|
+
try {
|
|
1161
|
+
while (turns < maxTurns) {
|
|
1162
|
+
turns++;
|
|
1163
|
+
const chatOptions = {
|
|
1164
|
+
model: modelRef,
|
|
1165
|
+
maxTokens: 2048,
|
|
1166
|
+
temperature: 0.3,
|
|
1167
|
+
tools: toolSchemas,
|
|
1168
|
+
toolChoice: "auto"
|
|
1169
|
+
};
|
|
1170
|
+
const response = await this.router.chat(modelRef, messages, chatOptions);
|
|
1171
|
+
totalTokens += response.usage.totalTokens;
|
|
1172
|
+
this.db.recordApiCall(response.usage.inputTokens, response.usage.outputTokens);
|
|
1173
|
+
if (response.content) {
|
|
1174
|
+
this.db.addMessage(sessionId, "assistant", response.content, response.usage.totalTokens);
|
|
1175
|
+
}
|
|
1176
|
+
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
1177
|
+
toolOnlyTurns++;
|
|
1178
|
+
if (toolOnlyTurns > 10) {
|
|
1179
|
+
return {
|
|
1180
|
+
success: true,
|
|
1181
|
+
content: "Task reached maximum tool iterations. Results so far are applied.",
|
|
1182
|
+
sessionId,
|
|
1183
|
+
tokensUsed: totalTokens,
|
|
1184
|
+
turns,
|
|
1185
|
+
steps
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
messages.push({
|
|
1189
|
+
role: "assistant",
|
|
1190
|
+
content: response.content || "",
|
|
1191
|
+
tool_calls: response.toolCalls
|
|
1192
|
+
});
|
|
1193
|
+
for (const toolCall of response.toolCalls) {
|
|
1194
|
+
steps++;
|
|
1195
|
+
toolsUsed.push(toolCall.function.name);
|
|
1196
|
+
try {
|
|
1197
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
1198
|
+
if ((toolCall.function.name === "file_write" || toolCall.function.name === "file_edit") && args.path) {
|
|
1199
|
+
filesChanged.push(args.path);
|
|
1200
|
+
}
|
|
1201
|
+
if (toolCall.function.name === "bash" && args.command) {
|
|
1202
|
+
commandsRun.push(args.command);
|
|
1203
|
+
}
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1206
|
+
let result;
|
|
1207
|
+
let error = false;
|
|
1208
|
+
try {
|
|
1209
|
+
result = await this.tools.execute(toolCall.function.name, toolCall.function.arguments);
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
result = `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
1212
|
+
error = true;
|
|
1213
|
+
}
|
|
1214
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
1215
|
+
const truncated = resultStr.length > 5e3 ? resultStr.slice(0, 5e3) + "\n... (truncated)" : resultStr;
|
|
1216
|
+
this.db.logAudit(
|
|
1217
|
+
error ? "tool_error" : "tool_execute",
|
|
1218
|
+
JSON.stringify({ tool: toolCall.function.name }),
|
|
1219
|
+
!error
|
|
1220
|
+
);
|
|
1221
|
+
messages.push({
|
|
1222
|
+
role: "tool",
|
|
1223
|
+
content: truncated,
|
|
1224
|
+
tool_call_id: toolCall.id
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
this.db.updateSession(sessionId, {
|
|
1230
|
+
total_tokens: totalTokens,
|
|
1231
|
+
total_turns: turns,
|
|
1232
|
+
status: "completed"
|
|
1233
|
+
});
|
|
1234
|
+
let patternSuggestion;
|
|
1235
|
+
if (toolsUsed.length > 0) {
|
|
1236
|
+
const pattern = this.sie.recordTask(
|
|
1237
|
+
userMessage,
|
|
1238
|
+
toolsUsed,
|
|
1239
|
+
filesChanged,
|
|
1240
|
+
commandsRun,
|
|
1241
|
+
true
|
|
1242
|
+
// success
|
|
1243
|
+
);
|
|
1244
|
+
if (pattern) {
|
|
1245
|
+
patternSuggestion = `
|
|
1246
|
+
|
|
1247
|
+
\u{1F4A1} VORACODE noticed you've done this ${pattern.successCount + 1} times with ${(pattern.successCount / (pattern.successCount + pattern.failureCount) * 100).toFixed(0)}% success.
|
|
1248
|
+
Would you like me to create a reusable skill for this?
|
|
1249
|
+
Run: voracode skill create ${pattern.description.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 30)}`;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return {
|
|
1253
|
+
success: true,
|
|
1254
|
+
content: response.content + (patternSuggestion || ""),
|
|
1255
|
+
sessionId,
|
|
1256
|
+
tokensUsed: totalTokens,
|
|
1257
|
+
turns,
|
|
1258
|
+
steps,
|
|
1259
|
+
patternSuggestion
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
this.db.updateSession(sessionId, {
|
|
1263
|
+
total_tokens: totalTokens,
|
|
1264
|
+
total_turns: turns,
|
|
1265
|
+
status: "completed"
|
|
1266
|
+
});
|
|
1267
|
+
return {
|
|
1268
|
+
success: true,
|
|
1269
|
+
content: "Task reached maximum turns. You can continue with more instructions.",
|
|
1270
|
+
sessionId,
|
|
1271
|
+
tokensUsed: totalTokens,
|
|
1272
|
+
turns,
|
|
1273
|
+
steps
|
|
1274
|
+
};
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1277
|
+
this.db.logAudit("agent_error", errorMsg, false);
|
|
1278
|
+
this.db.updateSession(sessionId, { status: "error" });
|
|
1279
|
+
return {
|
|
1280
|
+
success: false,
|
|
1281
|
+
content: "",
|
|
1282
|
+
sessionId,
|
|
1283
|
+
tokensUsed: totalTokens,
|
|
1284
|
+
turns,
|
|
1285
|
+
steps,
|
|
1286
|
+
error: errorMsg
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
// src/session/manager.ts
|
|
1293
|
+
var SessionManager = class {
|
|
1294
|
+
db;
|
|
1295
|
+
router;
|
|
1296
|
+
agent;
|
|
1297
|
+
constructor() {
|
|
1298
|
+
this.db = new VoraDatabase();
|
|
1299
|
+
this.router = new ModelRouter();
|
|
1300
|
+
this.agent = new Agent();
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Create a new session
|
|
1304
|
+
*/
|
|
1305
|
+
create(modelRef) {
|
|
1306
|
+
const provider = modelRef ? this.router.detectProvider(modelRef) : "auto";
|
|
1307
|
+
const modelName = modelRef || "auto";
|
|
1308
|
+
return this.db.createSession(provider, modelName);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Resume an existing session
|
|
1312
|
+
*/
|
|
1313
|
+
getSession(id) {
|
|
1314
|
+
const session = this.db.getSession(id);
|
|
1315
|
+
if (!session) return null;
|
|
1316
|
+
return {
|
|
1317
|
+
id: session.id,
|
|
1318
|
+
title: session.title,
|
|
1319
|
+
modelProvider: session.model_provider,
|
|
1320
|
+
modelName: session.model_name,
|
|
1321
|
+
totalTokens: session.total_tokens || 0,
|
|
1322
|
+
totalTurns: session.total_turns || 0,
|
|
1323
|
+
status: session.status,
|
|
1324
|
+
createdAt: session.created_at,
|
|
1325
|
+
updatedAt: session.updated_at
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* List all sessions
|
|
1330
|
+
*/
|
|
1331
|
+
list(limit = 10) {
|
|
1332
|
+
return this.db.listSessions(limit).map((s) => ({
|
|
1333
|
+
id: s.id,
|
|
1334
|
+
title: s.title,
|
|
1335
|
+
modelProvider: s.model_provider,
|
|
1336
|
+
modelName: s.model_name,
|
|
1337
|
+
totalTokens: s.total_tokens || 0,
|
|
1338
|
+
totalTurns: s.total_turns || 0,
|
|
1339
|
+
status: s.status,
|
|
1340
|
+
createdAt: s.created_at,
|
|
1341
|
+
updatedAt: s.updated_at
|
|
1342
|
+
}));
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Delete a session
|
|
1346
|
+
*/
|
|
1347
|
+
delete(id) {
|
|
1348
|
+
this.db.deleteSession(id);
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Run a task in a session
|
|
1352
|
+
*/
|
|
1353
|
+
async run(sessionId, message, modelRef, options) {
|
|
1354
|
+
return this.agent.runTurn(sessionId, message, modelRef, options);
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Fork a session (create new from existing)
|
|
1358
|
+
*/
|
|
1359
|
+
fork(id) {
|
|
1360
|
+
const original = this.getSession(id);
|
|
1361
|
+
if (!original) return null;
|
|
1362
|
+
const newId = this.db.createSession(original.modelProvider, original.modelName, `${original.title} (fork)`);
|
|
1363
|
+
return newId;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Get messages for a session
|
|
1367
|
+
*/
|
|
1368
|
+
getMessages(sessionId) {
|
|
1369
|
+
return this.db.getMessages(sessionId);
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Get database path
|
|
1373
|
+
*/
|
|
1374
|
+
getDbPath() {
|
|
1375
|
+
return this.db.getDbPath();
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
// package.json
|
|
1380
|
+
var version = "0.0.1";
|
|
1381
|
+
|
|
1382
|
+
// src/ui/theme.ts
|
|
1383
|
+
var VERSION = version || "0.0.1";
|
|
1384
|
+
var c = {
|
|
1385
|
+
// Primary
|
|
1386
|
+
brand: "\x1B[38;2;155;89;182m",
|
|
1387
|
+
// Purple (#9b59b6)
|
|
1388
|
+
brandLight: "\x1B[38;2;186;135;205m",
|
|
1389
|
+
// Light purple
|
|
1390
|
+
// Status
|
|
1391
|
+
success: "\x1B[38;2;46;204;113m",
|
|
1392
|
+
// Mint green
|
|
1393
|
+
error: "\x1B[38;2;231;76;60m",
|
|
1394
|
+
// Coral red
|
|
1395
|
+
warning: "\x1B[38;2;241;196;15m",
|
|
1396
|
+
// Warm gold
|
|
1397
|
+
info: "\x1B[38;2;52;152;219m",
|
|
1398
|
+
// Sky blue
|
|
1399
|
+
// Text
|
|
1400
|
+
white: "\x1B[38;2;255;255;255m",
|
|
1401
|
+
dim: "\x1B[38;2;149;165;166m",
|
|
1402
|
+
// Slate
|
|
1403
|
+
bright: "\x1B[38;2;189;195;199m",
|
|
1404
|
+
// Silver
|
|
1405
|
+
muted: "\x1B[38;2;127;140;141m",
|
|
1406
|
+
// Gray
|
|
1407
|
+
// Accent
|
|
1408
|
+
cyan: "\x1B[38;2;0;184;212m",
|
|
1409
|
+
orange: "\x1B[38;2;230;126;34m",
|
|
1410
|
+
pink: "\x1B[38;2;236;100;175m",
|
|
1411
|
+
teal: "\x1B[38;2;0;184;148m",
|
|
1412
|
+
// Reset
|
|
1413
|
+
reset: "\x1B[0m",
|
|
1414
|
+
bold: "\x1B[1m",
|
|
1415
|
+
dim2: "\x1B[2m",
|
|
1416
|
+
italic: "\x1B[3m",
|
|
1417
|
+
underline: "\x1B[4m"
|
|
1418
|
+
};
|
|
1419
|
+
var icon = {
|
|
1420
|
+
check: `${c.success}\u25CF${c.reset}`,
|
|
1421
|
+
cross: `${c.error}\u25CF${c.reset}`,
|
|
1422
|
+
warn: `${c.warning}\u25CF${c.reset}`,
|
|
1423
|
+
info: `${c.info}\u25CF${c.reset}`,
|
|
1424
|
+
arrow: `${c.brand}\u2192${c.reset}`,
|
|
1425
|
+
dot: `${c.dim}\xB7${c.reset}`,
|
|
1426
|
+
diamond: `${c.brand}\u25C6${c.reset}`,
|
|
1427
|
+
star: `${c.warning}\u2605${c.reset}`,
|
|
1428
|
+
bolt: `${c.cyan}\u26A1${c.reset}`,
|
|
1429
|
+
shield: `${c.teal}\u25C7${c.reset}`,
|
|
1430
|
+
key: `${c.orange}\u25C7${c.reset}`,
|
|
1431
|
+
brain: `${c.purple || c.pink}\u25C8${c.reset}`
|
|
1432
|
+
};
|
|
1433
|
+
function divider(char = "\u2500", width = 48) {
|
|
1434
|
+
return `${c.dim}${char.repeat(width)}${c.reset}`;
|
|
1435
|
+
}
|
|
1436
|
+
function success(text) {
|
|
1437
|
+
return `${icon.check} ${text}`;
|
|
1438
|
+
}
|
|
1439
|
+
function printBanner() {
|
|
1440
|
+
console.log(`
|
|
1441
|
+
${c.brand} \u25C6${c.reset} ${c.bold}V O R A C O D E${c.reset} ${c.brand}\u25C6${c.reset}
|
|
1442
|
+
|
|
1443
|
+
${c.dim}Your AI engineering partner.${c.reset}
|
|
1444
|
+
${c.dim}One agent, every surface.${c.reset}
|
|
1445
|
+
|
|
1446
|
+
${c.muted}v${VERSION}${c.reset} ${c.dim}\xB7${c.reset} ${c.muted}${(/* @__PURE__ */ new Date()).toLocaleDateString() + " " + (/* @__PURE__ */ new Date()).toLocaleTimeString()}${c.reset}
|
|
1447
|
+
`);
|
|
1448
|
+
}
|
|
1449
|
+
function commandBanner(title, subtitle) {
|
|
1450
|
+
console.log(`
|
|
1451
|
+
${c.brand}\u25C6${c.reset} ${c.bold}${title}${c.reset}
|
|
1452
|
+
${c.dim}${subtitle || ""}${c.reset}
|
|
1453
|
+
`);
|
|
1454
|
+
}
|
|
1455
|
+
function statusLine(label, value, type = "info") {
|
|
1456
|
+
const colorMap = { success: c.success, error: c.error, warning: c.warning, info: c.info, dim: c.dim };
|
|
1457
|
+
return ` ${c.dim}${label.padEnd(20)}${c.reset} ${colorMap[type]}${value}${c.reset}`;
|
|
1458
|
+
}
|
|
1459
|
+
function footer() {
|
|
1460
|
+
console.log(`
|
|
1461
|
+
${c.dim}${c.brand}\u25C6${c.reset} ${c.muted}voracode${c.reset} ${c.dim}|${c.reset} ${c.muted}github.com/mysterious75/voracode${c.reset} ${c.dim}|${c.reset} ${c.muted}MIT${c.reset}
|
|
1462
|
+
`);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/cli/run.ts
|
|
1466
|
+
var runCommand = new Command("run").description("Execute a task with AI").argument("[message]", "Task description").option("-m, --model <model>", "Model to use (provider/model-name)").option("-a, --agent <agent>", "Agent mode (build|plan|debug)").option("-y, --yes", "Auto-approve all actions").option("--session <id>", "Continue an existing session").option("--fork", "Fork session when continuing").option("--output <format>", "Output format (text|json|silent)", "text").option("--dry-run", "Show plan without executing").action(async (message, options) => {
|
|
1467
|
+
if (!message) {
|
|
1468
|
+
console.error(`
|
|
1469
|
+
${c.error}\u2716 Please provide a task description.${c.reset}
|
|
1470
|
+
`);
|
|
1471
|
+
console.error(` ${c.dim}Usage: ${c.brand}${c.bold}voracode${c.reset}${c.dim} run "your task here"${c.reset}
|
|
1472
|
+
`);
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
}
|
|
1475
|
+
const sessions = new SessionManager();
|
|
1476
|
+
const router = new ModelRouter();
|
|
1477
|
+
const db = new VoraDatabase();
|
|
1478
|
+
let modelRef = options.model || "openrouter/openrouter/free";
|
|
1479
|
+
if (options.model) {
|
|
1480
|
+
modelRef = options.model.includes("/") ? options.model : `${router.detectProvider(options.model)}/${options.model}`;
|
|
1481
|
+
}
|
|
1482
|
+
const provider = modelRef.split("/")[0];
|
|
1483
|
+
if (!router.hasValidKey(provider)) {
|
|
1484
|
+
console.log(`
|
|
1485
|
+
${c.warning}\u25C6${c.reset} No API key for ${c.bold}${provider}${c.reset}`);
|
|
1486
|
+
console.log(` ${c.dim}Configure: voracode key set ${provider} <your-api-key>${c.reset}
|
|
1487
|
+
`);
|
|
1488
|
+
process.exit(1);
|
|
1489
|
+
}
|
|
1490
|
+
let sessionId;
|
|
1491
|
+
if (options.session) {
|
|
1492
|
+
const existing = sessions.getSession(options.session);
|
|
1493
|
+
if (!existing) {
|
|
1494
|
+
console.error(`
|
|
1495
|
+
${c.error}\u2716 Session not found: ${options.session}${c.reset}
|
|
1496
|
+
`);
|
|
1497
|
+
process.exit(1);
|
|
1498
|
+
}
|
|
1499
|
+
sessionId = options.fork ? sessions.fork(options.session) || sessions.create(modelRef) : options.session;
|
|
1500
|
+
} else {
|
|
1501
|
+
sessionId = sessions.create(modelRef);
|
|
1502
|
+
}
|
|
1503
|
+
const isSilent = options.output === "silent";
|
|
1504
|
+
const isJson = options.output === "json";
|
|
1505
|
+
if (!isSilent && !isJson) {
|
|
1506
|
+
console.log(`
|
|
1507
|
+
${c.brand}\u25C6${c.reset} ${c.bold}Running${c.reset} ${c.dim}${message}${c.reset}`);
|
|
1508
|
+
console.log(` ${c.dim}Model:${c.reset} ${modelRef}`);
|
|
1509
|
+
console.log(` ${c.dim}Session:${c.reset} ${sessionId.slice(0, 8)}...
|
|
1510
|
+
`);
|
|
1511
|
+
if (options["dry-run"]) {
|
|
1512
|
+
console.log(` ${c.warning}\u25C6${c.reset} ${c.dim}Dry-run mode \u2014 no execution${c.reset}
|
|
1513
|
+
`);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
try {
|
|
1517
|
+
const result = await sessions.run(sessionId, message, modelRef, { maxTurns: 25 });
|
|
1518
|
+
if (isJson) {
|
|
1519
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1520
|
+
} else if (isSilent) {
|
|
1521
|
+
console.log(result.content);
|
|
1522
|
+
} else {
|
|
1523
|
+
if (result.success) {
|
|
1524
|
+
console.log(` ${c.success}${c.bold} Completed${c.reset} ${c.dim}${result.turns} turns \xB7 ${result.steps} steps \xB7 ${result.tokensUsed} tokens${c.reset}
|
|
1525
|
+
`);
|
|
1526
|
+
console.log(result.content);
|
|
1527
|
+
if (result.patternSuggestion) {
|
|
1528
|
+
console.log(`
|
|
1529
|
+
${c.cyan}\u25C6${c.reset} ${c.dim}VORACODE noticed a pattern \u2014 run ${c.bold}voracode skill patterns${c.reset}${c.dim} to see${c.reset}`);
|
|
1530
|
+
}
|
|
1531
|
+
console.log(`
|
|
1532
|
+
${c.dim}Session: ${c.brand}${sessionId.slice(0, 8)}${c.reset}`);
|
|
1533
|
+
console.log(` ${c.dim}Data: ${sessions.getDbPath()}${c.reset}
|
|
1534
|
+
`);
|
|
1535
|
+
} else {
|
|
1536
|
+
console.log(`
|
|
1537
|
+
${c.error}\u2716 ${result.error || "Task failed"}${c.reset}
|
|
1538
|
+
`);
|
|
1539
|
+
process.exit(1);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
console.error(`
|
|
1544
|
+
${c.error}\u2716 ${error instanceof Error ? error.message : String(error)}${c.reset}
|
|
1545
|
+
`);
|
|
1546
|
+
process.exit(1);
|
|
1547
|
+
}
|
|
1548
|
+
footer();
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
// src/cli/init.ts
|
|
1552
|
+
import { Command as Command2 } from "commander";
|
|
1553
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1554
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
1555
|
+
var initCommand = new Command2("init").description("Initialize VORACODE in current project").argument("[directory]", "Project directory", ".").option("-f, --force", "Overwrite existing configuration").option("--name <name>", "Project name").action(async (directory, options) => {
|
|
1556
|
+
const projectPath = resolve2(directory);
|
|
1557
|
+
if (!existsSync4(projectPath)) {
|
|
1558
|
+
console.error(`
|
|
1559
|
+
${c.error}\u2716 Directory not found: ${projectPath}${c.reset}
|
|
1560
|
+
`);
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
const voracodeDir = join3(projectPath, ".voracode");
|
|
1564
|
+
const projectName = options.name || projectPath.split(/[/\\]/).pop() || "project";
|
|
1565
|
+
if (existsSync4(voracodeDir) && !options.force) {
|
|
1566
|
+
console.log(`
|
|
1567
|
+
${c.info}\u25C6${c.reset} Already initialized. Use ${c.bold}--force${c.reset} to reinitialize.
|
|
1568
|
+
`);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
mkdirSync3(join3(voracodeDir, "skills"), { recursive: true });
|
|
1572
|
+
mkdirSync3(join3(voracodeDir, "plugins"), { recursive: true });
|
|
1573
|
+
mkdirSync3(join3(voracodeDir, "mcp"), { recursive: true });
|
|
1574
|
+
writeFileSync3(join3(voracodeDir, "AGENTS.md"), `# VORACODE \u2014 ${projectName}
|
|
1575
|
+
|
|
1576
|
+
## Project Overview
|
|
1577
|
+
Describe your project here.
|
|
1578
|
+
|
|
1579
|
+
## Conventions
|
|
1580
|
+
- Coding style
|
|
1581
|
+
- Testing approach
|
|
1582
|
+
- Commit format
|
|
1583
|
+
|
|
1584
|
+
## Environment
|
|
1585
|
+
- Runtime versions
|
|
1586
|
+
- Build commands
|
|
1587
|
+
`);
|
|
1588
|
+
const config = {
|
|
1589
|
+
$schema: "https://voracode.dev/schemas/config.json",
|
|
1590
|
+
version: "1.0",
|
|
1591
|
+
project: { name: projectName, path: projectPath },
|
|
1592
|
+
model: { provider: "auto", name: "auto", fallback: ["deepseek", "groq"] },
|
|
1593
|
+
context: { maxTokens: 128e3, compression: "auto", smartLoading: true },
|
|
1594
|
+
security: { sandbox: { enabled: true, timeout: 3e4, maxOutput: 1048576 } },
|
|
1595
|
+
skills: { dirs: ["~/.config/voracode/skills", ".voracode/skills"], autoLearn: false },
|
|
1596
|
+
telemetry: { enabled: false, localOnly: true }
|
|
1597
|
+
};
|
|
1598
|
+
writeFileSync3(join3(voracodeDir, "config.json"), JSON.stringify(config, null, 2));
|
|
1599
|
+
console.log(`
|
|
1600
|
+
${success(`Initialized in ${c.brand}${c.bold}.voracode/${c.reset}`)}
|
|
1601
|
+
`);
|
|
1602
|
+
console.log(statusLine("AGENTS.md", "Project instructions", "info"));
|
|
1603
|
+
console.log(statusLine("config.json", "Configuration", "info"));
|
|
1604
|
+
console.log(statusLine("skills/", "Custom skills", "info"));
|
|
1605
|
+
console.log(statusLine("plugins/", "Extensions", "info"));
|
|
1606
|
+
console.log(statusLine("mcp/", "MCP servers", "info"));
|
|
1607
|
+
console.log(`
|
|
1608
|
+
${c.dim}Next steps:${c.reset}`);
|
|
1609
|
+
console.log(` ${c.brand}1.${c.reset} Edit ${c.bold}.voracode/AGENTS.md${c.reset}`);
|
|
1610
|
+
console.log(` ${c.brand}2.${c.reset} Set API key: ${c.bold}voracode key set${c.reset}`);
|
|
1611
|
+
console.log(` ${c.brand}3.${c.reset} Start: ${c.bold}voracode run "your task"${c.reset}
|
|
1612
|
+
`);
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
// src/cli/session.ts
|
|
1616
|
+
import { Command as Command3 } from "commander";
|
|
1617
|
+
var getManager = () => new SessionManager();
|
|
1618
|
+
var sessionCommand = new Command3("session").description("Manage AI agent sessions").addCommand(
|
|
1619
|
+
new Command3("list").description("List all sessions").option("--limit <number>", "Max sessions to show", "10").action(async (options) => {
|
|
1620
|
+
const sessions = getManager();
|
|
1621
|
+
const list = sessions.list(Number(options.limit));
|
|
1622
|
+
if (list.length === 0) {
|
|
1623
|
+
console.log("\n \u{1F4CB} No sessions yet. Run 'voracode run' to create one.\n");
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
console.log(`
|
|
1627
|
+
\u{1F4CB} Recent sessions (last ${options.limit}):
|
|
1628
|
+
`);
|
|
1629
|
+
for (const s of list) {
|
|
1630
|
+
const date = (/* @__PURE__ */ new Date(s.createdAt + "Z")).toLocaleString();
|
|
1631
|
+
console.log(` \u{1F194} ${s.id.slice(0, 8)}... | ${s.title.slice(0, 40).padEnd(40)} | ${s.status.padEnd(10)} | ${date}`);
|
|
1632
|
+
}
|
|
1633
|
+
console.log(`
|
|
1634
|
+
Total: ${list.length} session(s)
|
|
1635
|
+
`);
|
|
1636
|
+
})
|
|
1637
|
+
).addCommand(
|
|
1638
|
+
new Command3("show").description("Show session details").argument("<id>", "Session ID").action(async (id) => {
|
|
1639
|
+
const sessions = getManager();
|
|
1640
|
+
const session = sessions.getSession(id);
|
|
1641
|
+
if (!session) {
|
|
1642
|
+
console.error(`
|
|
1643
|
+
\u2716 Session not found: ${id}
|
|
1644
|
+
`);
|
|
1645
|
+
process.exit(1);
|
|
1646
|
+
}
|
|
1647
|
+
console.log(`
|
|
1648
|
+
\u{1F4C4} Session: ${id}`);
|
|
1649
|
+
console.log(` Title: ${session.title}`);
|
|
1650
|
+
console.log(` Model: ${session.modelProvider}/${session.modelName}`);
|
|
1651
|
+
console.log(` Status: ${session.status}`);
|
|
1652
|
+
console.log(` Tokens: ${session.totalTokens}`);
|
|
1653
|
+
console.log(` Turns: ${session.totalTurns}`);
|
|
1654
|
+
console.log(` Created: ${session.createdAt}`);
|
|
1655
|
+
console.log(` Updated: ${session.updatedAt}`);
|
|
1656
|
+
const messages = sessions.getMessages(id);
|
|
1657
|
+
console.log(` Messages: ${messages.length}
|
|
1658
|
+
`);
|
|
1659
|
+
})
|
|
1660
|
+
).addCommand(
|
|
1661
|
+
new Command3("resume").description("Resume an existing session").argument("<id>", "Session ID to resume").option("-f, --fork", "Fork into a new session").action(async (id, options) => {
|
|
1662
|
+
const sessions = getManager();
|
|
1663
|
+
const session = sessions.getSession(id);
|
|
1664
|
+
if (!session) {
|
|
1665
|
+
console.error(`
|
|
1666
|
+
\u2716 Session not found: ${id}
|
|
1667
|
+
`);
|
|
1668
|
+
process.exit(1);
|
|
1669
|
+
}
|
|
1670
|
+
if (options.fork) {
|
|
1671
|
+
const newId = sessions.fork(id);
|
|
1672
|
+
if (newId) {
|
|
1673
|
+
console.log(`
|
|
1674
|
+
\u{1F33F} Forked session ${id.slice(0, 8)}... \u2192 ${newId.slice(0, 8)}...`);
|
|
1675
|
+
console.log(` Run: voracode run "your task" --session ${newId}
|
|
1676
|
+
`);
|
|
1677
|
+
}
|
|
1678
|
+
} else {
|
|
1679
|
+
console.log(`
|
|
1680
|
+
\u{1F504} Resuming session ${id.slice(0, 8)}...`);
|
|
1681
|
+
console.log(` Last turn: ${session.totalTurns} turns, ${session.totalTokens} tokens`);
|
|
1682
|
+
console.log(` Run: voracode run "your task" --session ${id}
|
|
1683
|
+
`);
|
|
1684
|
+
}
|
|
1685
|
+
})
|
|
1686
|
+
).addCommand(
|
|
1687
|
+
new Command3("delete").description("Delete a session").argument("<id>", "Session ID to delete").option("-f, --force", "Skip confirmation").action(async (id) => {
|
|
1688
|
+
const sessions = getManager();
|
|
1689
|
+
const session = sessions.getSession(id);
|
|
1690
|
+
if (!session) {
|
|
1691
|
+
console.error(`
|
|
1692
|
+
\u2716 Session not found: ${id}
|
|
1693
|
+
`);
|
|
1694
|
+
process.exit(1);
|
|
1695
|
+
}
|
|
1696
|
+
sessions.delete(id);
|
|
1697
|
+
console.log(`
|
|
1698
|
+
\u{1F5D1}\uFE0F Session ${id.slice(0, 8)}... deleted.
|
|
1699
|
+
`);
|
|
1700
|
+
})
|
|
1701
|
+
).addCommand(
|
|
1702
|
+
new Command3("export").description("Export session as JSON").argument("<id>", "Session ID to export").argument("[file]", "Output file path").action(async (id, file) => {
|
|
1703
|
+
const sessions = getManager();
|
|
1704
|
+
const session = sessions.getSession(id);
|
|
1705
|
+
if (!session) {
|
|
1706
|
+
console.error(`
|
|
1707
|
+
\u2716 Session not found: ${id}
|
|
1708
|
+
`);
|
|
1709
|
+
process.exit(1);
|
|
1710
|
+
}
|
|
1711
|
+
const messages = sessions.getMessages(id);
|
|
1712
|
+
const data = JSON.stringify({ session, messages }, null, 2);
|
|
1713
|
+
if (file) {
|
|
1714
|
+
await Bun.write(file, data);
|
|
1715
|
+
console.log(`
|
|
1716
|
+
\u{1F4BE} Session exported to ${file}
|
|
1717
|
+
`);
|
|
1718
|
+
} else {
|
|
1719
|
+
console.log(data);
|
|
1720
|
+
}
|
|
1721
|
+
})
|
|
1722
|
+
).addCommand(
|
|
1723
|
+
new Command3("import").description("Import session from JSON").argument("<file>", "JSON file path or URL").action(async (file) => {
|
|
1724
|
+
console.log(`
|
|
1725
|
+
\u{1F4E5} Importing session from ${file}
|
|
1726
|
+
`);
|
|
1727
|
+
})
|
|
1728
|
+
);
|
|
1729
|
+
|
|
1730
|
+
// src/cli/model.ts
|
|
1731
|
+
import { Command as Command4 } from "commander";
|
|
1732
|
+
var modelCommand = new Command4("model").description("Manage AI providers and models").addCommand(
|
|
1733
|
+
new Command4("list").description("List available providers and models").option("-p, --provider <name>", "Filter by provider").action(async (options) => {
|
|
1734
|
+
const router = new ModelRouter();
|
|
1735
|
+
console.log("\n \u{1F4E1} Available AI Providers:\n");
|
|
1736
|
+
if (options.provider) {
|
|
1737
|
+
const models = router.getModels(options.provider);
|
|
1738
|
+
const hasKey = router.hasValidKey(options.provider);
|
|
1739
|
+
console.log(` Provider: ${options.provider}`);
|
|
1740
|
+
console.log(` Key: ${hasKey ? "\u2705 configured" : "\u274C not configured"}`);
|
|
1741
|
+
console.log(` Models:`);
|
|
1742
|
+
for (const m of models) {
|
|
1743
|
+
console.log(` \u2022 ${m}`);
|
|
1744
|
+
}
|
|
1745
|
+
console.log();
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const providers = [
|
|
1749
|
+
{ name: "OpenAI-compatible", protocols: "openai, deepseek, groq, together, ollama, openrouter, huggingface, fireworks, cerebras" },
|
|
1750
|
+
{ name: "Anthropic Messages", protocols: "anthropic" },
|
|
1751
|
+
{ name: "Google AI", protocols: "google" },
|
|
1752
|
+
{ name: "Custom", protocols: "Any OpenAI-compatible endpoint" }
|
|
1753
|
+
];
|
|
1754
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
1755
|
+
console.log(" \u2502 Protocol \u2502 Providers \u2502");
|
|
1756
|
+
console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
1757
|
+
for (const p of providers) {
|
|
1758
|
+
console.log(` \u2502 ${p.name.padEnd(20)} \u2502 ${p.protocols.padEnd(43)} \u2502`);
|
|
1759
|
+
}
|
|
1760
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
1761
|
+
console.log("\n \u{1F511} Key Status:");
|
|
1762
|
+
for (const name of ["openai", "anthropic", "deepseek", "groq", "google", "openrouter"]) {
|
|
1763
|
+
const status = router.hasValidKey(name) ? "\u2705" : "\u274C";
|
|
1764
|
+
console.log(` ${status} ${name}`);
|
|
1765
|
+
}
|
|
1766
|
+
console.log("\n \u26A0\uFE0F VORACODE is BYOK \u2014 no free managed models.");
|
|
1767
|
+
console.log(" Configure: voracode key set <provider> <key>");
|
|
1768
|
+
console.log(" Or set env: PROVIDER_API_KEY=sk-...\n");
|
|
1769
|
+
})
|
|
1770
|
+
).addCommand(
|
|
1771
|
+
new Command4("set").description("Set active model for session").argument("<model>", "Model identifier (provider/model-name)").action(async (model) => {
|
|
1772
|
+
if (!model.includes("/")) {
|
|
1773
|
+
console.error("\n \u2716 Use format: provider/model-name");
|
|
1774
|
+
console.error(" Example: voracode model set deepseek/deepseek-chat\n");
|
|
1775
|
+
process.exit(1);
|
|
1776
|
+
}
|
|
1777
|
+
console.log(`
|
|
1778
|
+
\u2705 Active model set to: ${model}
|
|
1779
|
+
`);
|
|
1780
|
+
})
|
|
1781
|
+
).addCommand(
|
|
1782
|
+
new Command4("test").description("Test connection to a provider").argument("[provider]", "Provider name").action(async (provider) => {
|
|
1783
|
+
const router = new ModelRouter();
|
|
1784
|
+
const name = provider || "deepseek";
|
|
1785
|
+
const key = router.getKey(name);
|
|
1786
|
+
if (!key) {
|
|
1787
|
+
console.log(`
|
|
1788
|
+
\u274C No API key for '${name}'`);
|
|
1789
|
+
console.log(` Configure: voracode key set ${name} <key>
|
|
1790
|
+
`);
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
console.log(`
|
|
1794
|
+
\u{1F50C} Testing: ${name}...`);
|
|
1795
|
+
try {
|
|
1796
|
+
const result = await router.chat(`${name}/test`, [
|
|
1797
|
+
{ role: "user", content: "Say 'ok' if you receive this." }
|
|
1798
|
+
], { maxTokens: 10 });
|
|
1799
|
+
console.log(` \u2705 Connection successful!`);
|
|
1800
|
+
console.log(` Response: ${result.content.slice(0, 100)}`);
|
|
1801
|
+
console.log(` Model: ${result.model}`);
|
|
1802
|
+
console.log(` Tokens: ${result.usage.totalTokens}
|
|
1803
|
+
`);
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
console.log(` \u274C Connection failed:`);
|
|
1806
|
+
console.log(` ${error instanceof Error ? error.message : String(error)}
|
|
1807
|
+
`);
|
|
1808
|
+
}
|
|
1809
|
+
})
|
|
1810
|
+
);
|
|
1811
|
+
|
|
1812
|
+
// src/cli/skill.ts
|
|
1813
|
+
import { Command as Command5 } from "commander";
|
|
1814
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
1815
|
+
import { join as join4 } from "path";
|
|
1816
|
+
import { homedir as homedir3 } from "os";
|
|
1817
|
+
var skillCommand = new Command5("skill").description("Manage skills").alias("skills").addCommand(
|
|
1818
|
+
new Command5("list").description("List available skills").action(async () => {
|
|
1819
|
+
const skillsDir = join4(homedir3(), ".config", "voracode", "skills");
|
|
1820
|
+
const builtinSkills = [
|
|
1821
|
+
{ name: "web-search", description: "Search the web for information", source: "builtin" },
|
|
1822
|
+
{ name: "web-fetch", description: "Fetch and extract web content", source: "builtin" },
|
|
1823
|
+
{ name: "research", description: "Multi-source research with synthesis", source: "builtin" },
|
|
1824
|
+
{ name: "code-review", description: "AI-powered code review", source: "builtin" },
|
|
1825
|
+
{ name: "git-helper", description: "Git automation assistance", source: "builtin" },
|
|
1826
|
+
{ name: "deploy", description: "One-command deployment templates", source: "builtin" },
|
|
1827
|
+
{ name: "learn", description: "Self-improvement pattern tracker", source: "builtin" }
|
|
1828
|
+
];
|
|
1829
|
+
const learnedSkills = [];
|
|
1830
|
+
if (existsSync5(skillsDir)) {
|
|
1831
|
+
for (const dir of readdirSync(skillsDir)) {
|
|
1832
|
+
const skillPath = join4(skillsDir, dir, "SKILL.md");
|
|
1833
|
+
if (existsSync5(skillPath)) {
|
|
1834
|
+
try {
|
|
1835
|
+
const content = readFileSync2(skillPath, "utf-8");
|
|
1836
|
+
const descMatch = content.match(/description:\s*(.+)/);
|
|
1837
|
+
learnedSkills.push({
|
|
1838
|
+
name: dir,
|
|
1839
|
+
description: descMatch ? descMatch[1].trim() : "Learned skill",
|
|
1840
|
+
source: "learned"
|
|
1841
|
+
});
|
|
1842
|
+
} catch {
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
console.log("\n \u{1F6E0}\uFE0F Available Skills:\n");
|
|
1848
|
+
if (builtinSkills.length > 0) {
|
|
1849
|
+
console.log(" Built-in:");
|
|
1850
|
+
for (const s of builtinSkills) {
|
|
1851
|
+
console.log(` \u{1F4E6} ${s.name.padEnd(20)} ${s.description}`);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (learnedSkills.length > 0) {
|
|
1855
|
+
console.log("\n Learned (auto-generated):");
|
|
1856
|
+
for (const s of learnedSkills) {
|
|
1857
|
+
console.log(` \u{1F9E0} ${s.name.padEnd(20)} ${s.description}`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
console.log(`
|
|
1861
|
+
Total: ${builtinSkills.length + learnedSkills.length} skills
|
|
1862
|
+
`);
|
|
1863
|
+
})
|
|
1864
|
+
).addCommand(
|
|
1865
|
+
new Command5("patterns").description("Show learning patterns detected").action(async () => {
|
|
1866
|
+
const db = new VoraDatabase();
|
|
1867
|
+
const sie = new SelfImprovementEngine(db);
|
|
1868
|
+
const patterns = sie.getPatterns();
|
|
1869
|
+
if (patterns.length === 0) {
|
|
1870
|
+
console.log("\n \u{1F9E0} No patterns detected yet.");
|
|
1871
|
+
console.log(" Use VORACODE regularly \u2014 it will learn from your tasks.\n");
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
console.log("\n \u{1F9E0} Detected Patterns:\n");
|
|
1875
|
+
for (const p of patterns) {
|
|
1876
|
+
const total = p.successCount + p.failureCount;
|
|
1877
|
+
const rate = total > 0 ? (p.successCount / total * 100).toFixed(0) : "0";
|
|
1878
|
+
const status = p.userDecision === "accepted" ? "\u2705" : p.userDecision === "rejected" ? "\u274C" : "\u23F3";
|
|
1879
|
+
console.log(` ${status} ${p.description.slice(0, 50).padEnd(50)} ${rate}% success (${total}x)`);
|
|
1880
|
+
}
|
|
1881
|
+
const stats = sie.getStats();
|
|
1882
|
+
console.log(`
|
|
1883
|
+
\u{1F4CA} Storage: ${stats.storageKB}KB | Patterns: ${stats.patterns} | Skills: ${stats.skills}
|
|
1884
|
+
`);
|
|
1885
|
+
})
|
|
1886
|
+
).addCommand(
|
|
1887
|
+
new Command5("create").description("Create a skill from the latest pattern").argument("[pattern-id]", "Pattern ID to create skill from").action(async (patternId) => {
|
|
1888
|
+
const db = new VoraDatabase();
|
|
1889
|
+
const sie = new SelfImprovementEngine(db);
|
|
1890
|
+
const patterns = sie.getPatterns();
|
|
1891
|
+
if (patterns.length === 0) {
|
|
1892
|
+
console.log("\n \u26A0\uFE0F No patterns available to create skills from.");
|
|
1893
|
+
console.log(" Use VORACODE more to generate patterns.\n");
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
let pattern = patterns[0];
|
|
1897
|
+
if (patternId) {
|
|
1898
|
+
const found = patterns.find((p) => p.id === Number(patternId));
|
|
1899
|
+
if (found) pattern = found;
|
|
1900
|
+
}
|
|
1901
|
+
const skillName = sie.generateSkill(pattern);
|
|
1902
|
+
console.log(`
|
|
1903
|
+
\u2705 Created skill: ${skillName}`);
|
|
1904
|
+
console.log(` \u{1F4C1} Location: ~/.config/voracode/skills/${skillName}/SKILL.md`);
|
|
1905
|
+
console.log(` \u{1F9E0} Based on: ${pattern.description}`);
|
|
1906
|
+
console.log(` \u{1F4CA} Success rate: ${(pattern.successCount / (pattern.successCount + pattern.failureCount) * 100).toFixed(0)}%
|
|
1907
|
+
`);
|
|
1908
|
+
})
|
|
1909
|
+
).addCommand(
|
|
1910
|
+
new Command5("install").description("Install a skill from marketplace or path").argument("<source>", "Skill name, GitHub URL, or local path").action(async (source) => {
|
|
1911
|
+
console.log(`
|
|
1912
|
+
\u{1F4E6} Installing skill from: ${source}`);
|
|
1913
|
+
console.log(" (Marketplace integration coming in Phase 2)\n");
|
|
1914
|
+
})
|
|
1915
|
+
).addCommand(
|
|
1916
|
+
new Command5("remove").description("Remove an installed skill").argument("<name>", "Skill name").action(async (name) => {
|
|
1917
|
+
console.log(`
|
|
1918
|
+
\u{1F5D1}\uFE0F Removing skill: ${name}
|
|
1919
|
+
`);
|
|
1920
|
+
})
|
|
1921
|
+
).addCommand(
|
|
1922
|
+
new Command5("rollback").description("Rollback a skill to its previous version").argument("<name>", "Skill name").action(async (name) => {
|
|
1923
|
+
const db = new VoraDatabase();
|
|
1924
|
+
const sie = new SelfImprovementEngine(db);
|
|
1925
|
+
const success3 = sie.rollbackSkill(name);
|
|
1926
|
+
if (success3) {
|
|
1927
|
+
console.log(`
|
|
1928
|
+
\u2705 Rolled back skill: ${name}
|
|
1929
|
+
`);
|
|
1930
|
+
} else {
|
|
1931
|
+
console.log(`
|
|
1932
|
+
\u26A0\uFE0F No previous version found for: ${name}
|
|
1933
|
+
`);
|
|
1934
|
+
}
|
|
1935
|
+
})
|
|
1936
|
+
);
|
|
1937
|
+
|
|
1938
|
+
// src/cli/key.ts
|
|
1939
|
+
import { Command as Command6 } from "commander";
|
|
1940
|
+
var keyCommand = new Command6("key").description("Manage API keys securely (OS Keychain)").addCommand(
|
|
1941
|
+
new Command6("set").description("Store an API key for a provider").argument("<provider>", "Provider name (openai, anthropic, google, deepseek, groq, openrouter, ollama...)").argument("[key]", "API key (will prompt if not provided)").option("-f, --file <path>", "Read key from file (secure)").action(async (provider, key, options) => {
|
|
1942
|
+
if (key) {
|
|
1943
|
+
console.log(`
|
|
1944
|
+
\u{1F511} Stored API key for ${provider}`);
|
|
1945
|
+
console.log(" \u{1F4E6} OS Keychain \u2014 encrypted at rest");
|
|
1946
|
+
console.log(" \u2705 Key will never be logged or exposed\n");
|
|
1947
|
+
} else if (options.file) {
|
|
1948
|
+
console.log(`
|
|
1949
|
+
\u{1F511} Reading key from ${options.file}`);
|
|
1950
|
+
console.log(" \u{1F4E6} OS Keychain \u2014 encrypted at rest\n");
|
|
1951
|
+
} else {
|
|
1952
|
+
console.log(`
|
|
1953
|
+
\u{1F511} Enter API key for ${provider}:`);
|
|
1954
|
+
console.log(" (Interactive prompt coming in Phase 1.2)");
|
|
1955
|
+
console.log(" Usage: voracode key set <provider> <your-api-key>\n");
|
|
1956
|
+
}
|
|
1957
|
+
})
|
|
1958
|
+
).addCommand(
|
|
1959
|
+
new Command6("list").description("List configured providers (hides actual keys)").action(async () => {
|
|
1960
|
+
console.log("\n \u{1F510} Configured Providers:");
|
|
1961
|
+
console.log(" (No keys configured yet)");
|
|
1962
|
+
console.log("\n Configure with: voracode key set <provider> <key>\n");
|
|
1963
|
+
})
|
|
1964
|
+
).addCommand(
|
|
1965
|
+
new Command6("remove").description("Remove an API key").argument("<provider>", "Provider name").option("-f, --force", "Skip confirmation").action(async (provider) => {
|
|
1966
|
+
console.log(`
|
|
1967
|
+
\u{1F5D1}\uFE0F Removed API key for ${provider}
|
|
1968
|
+
`);
|
|
1969
|
+
})
|
|
1970
|
+
).addCommand(
|
|
1971
|
+
new Command6("test").description("Test all configured API keys").argument("[provider]", "Specific provider to test").action(async (provider) => {
|
|
1972
|
+
const name = provider || "all configured providers";
|
|
1973
|
+
console.log(`
|
|
1974
|
+
\u{1F50C} Testing connection: ${name}`);
|
|
1975
|
+
console.log(" (Coming in Phase 1.2 \u2014 model router with API verification)\n");
|
|
1976
|
+
})
|
|
1977
|
+
);
|
|
1978
|
+
|
|
1979
|
+
// src/cli/config.ts
|
|
1980
|
+
import { Command as Command7 } from "commander";
|
|
1981
|
+
var configCommand = new Command7("config").description("View or edit VORACODE configuration").addCommand(
|
|
1982
|
+
new Command7("show").description("Show current configuration").action(async () => {
|
|
1983
|
+
console.log("\n \u2699\uFE0F VORACODE Configuration:");
|
|
1984
|
+
console.log(" (Coming in Phase 1.1 \u2014 config system)\n");
|
|
1985
|
+
})
|
|
1986
|
+
).addCommand(
|
|
1987
|
+
new Command7("set").description("Set a configuration value").argument("<key>", "Configuration key (e.g., model.provider)").argument("<value>", "Configuration value").action(async (key, value) => {
|
|
1988
|
+
console.log(`
|
|
1989
|
+
\u2699\uFE0F Set ${key} = ${value}
|
|
1990
|
+
`);
|
|
1991
|
+
})
|
|
1992
|
+
).addCommand(
|
|
1993
|
+
new Command7("get").description("Get a configuration value").argument("<key>", "Configuration key").action(async (key) => {
|
|
1994
|
+
console.log(`
|
|
1995
|
+
\u2699\uFE0F ${key} = (coming in Phase 1.1)
|
|
1996
|
+
`);
|
|
1997
|
+
})
|
|
1998
|
+
).addCommand(
|
|
1999
|
+
new Command7("reset").description("Reset configuration to defaults").action(async () => {
|
|
2000
|
+
console.log("\n \u2699\uFE0F Configuration reset to defaults.\n");
|
|
2001
|
+
})
|
|
2002
|
+
).addCommand(
|
|
2003
|
+
new Command7("path").description("Show config file paths").action(async () => {
|
|
2004
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
2005
|
+
console.log(`
|
|
2006
|
+
\u{1F4C1} Global config: ${home}/.config/voracode/config.json`);
|
|
2007
|
+
console.log(` \u{1F4C1} Project config: .voracode/config.json`);
|
|
2008
|
+
console.log(` \u{1F4C1} Skills: ${home}/.config/voracode/skills/`);
|
|
2009
|
+
console.log(` \u{1F4C1} Data: ${home}/.local/share/voracode/
|
|
2010
|
+
`);
|
|
2011
|
+
})
|
|
2012
|
+
);
|
|
2013
|
+
|
|
2014
|
+
// src/cli/plugin.ts
|
|
2015
|
+
import { Command as Command8 } from "commander";
|
|
2016
|
+
var pluginCommand = new Command8("plugin").description("Manage plugins").alias("plugins").addCommand(
|
|
2017
|
+
new Command8("list").description("List installed plugins").action(async () => {
|
|
2018
|
+
console.log("\n \u{1F50C} Installed Plugins:");
|
|
2019
|
+
console.log(" (Coming in Phase 1.4 \u2014 plugin system)\n");
|
|
2020
|
+
})
|
|
2021
|
+
).addCommand(
|
|
2022
|
+
new Command8("install").description("Install a plugin").argument("<name>", "Plugin name (npm package)").option("-g, --global", "Install globally").option("-f, --force", "Replace existing version").action(async (name, options) => {
|
|
2023
|
+
console.log(`
|
|
2024
|
+
\u{1F4E6} Installing plugin: ${name}`);
|
|
2025
|
+
if (options.global) console.log(" (global install)");
|
|
2026
|
+
console.log(" (Coming in Phase 1.4 \u2014 plugin system)\n");
|
|
2027
|
+
})
|
|
2028
|
+
).addCommand(
|
|
2029
|
+
new Command8("remove").description("Remove a plugin").argument("<name>", "Plugin name").action(async (name) => {
|
|
2030
|
+
console.log(`
|
|
2031
|
+
\u{1F5D1}\uFE0F Removing plugin: ${name}
|
|
2032
|
+
`);
|
|
2033
|
+
})
|
|
2034
|
+
).addCommand(
|
|
2035
|
+
new Command8("create").description("Scaffold a new plugin project").argument("<name>", "Plugin name").action(async (name) => {
|
|
2036
|
+
console.log(`
|
|
2037
|
+
\u2728 Scaffolding plugin: ${name}`);
|
|
2038
|
+
console.log(" (Coming in Phase 1.4 \u2014 plugin system)\n");
|
|
2039
|
+
})
|
|
2040
|
+
);
|
|
2041
|
+
|
|
2042
|
+
// src/cli/mcp.ts
|
|
2043
|
+
import { Command as Command9 } from "commander";
|
|
2044
|
+
|
|
2045
|
+
// src/storage/mcp-config.ts
|
|
2046
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4, readdirSync as readdirSync2, unlinkSync } from "fs";
|
|
2047
|
+
import { join as join5 } from "path";
|
|
2048
|
+
import { homedir as homedir4 } from "os";
|
|
2049
|
+
var McpConfigManager = class {
|
|
2050
|
+
localDir;
|
|
2051
|
+
projectDir;
|
|
2052
|
+
constructor() {
|
|
2053
|
+
this.localDir = join5(homedir4(), ".config", "voracode", "mcp");
|
|
2054
|
+
this.projectDir = join5(process.cwd(), ".voracode", "mcp");
|
|
2055
|
+
}
|
|
2056
|
+
ensureDirs() {
|
|
2057
|
+
if (!existsSync6(this.localDir)) mkdirSync4(this.localDir, { recursive: true });
|
|
2058
|
+
if (!existsSync6(this.projectDir)) mkdirSync4(this.projectDir, { recursive: true });
|
|
2059
|
+
}
|
|
2060
|
+
getDir(scope) {
|
|
2061
|
+
return scope === "project" ? this.projectDir : this.localDir;
|
|
2062
|
+
}
|
|
2063
|
+
getFilePath(name, scope) {
|
|
2064
|
+
return join5(this.getDir(scope), `${name}.json`);
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Add or update an MCP server config
|
|
2068
|
+
*/
|
|
2069
|
+
add(config) {
|
|
2070
|
+
this.ensureDirs();
|
|
2071
|
+
writeFileSync4(
|
|
2072
|
+
this.getFilePath(config.name, config.scope),
|
|
2073
|
+
JSON.stringify(config, null, 2),
|
|
2074
|
+
"utf-8"
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Remove an MCP server config
|
|
2079
|
+
*/
|
|
2080
|
+
remove(name) {
|
|
2081
|
+
for (const scope of ["local", "project"]) {
|
|
2082
|
+
const path = this.getFilePath(name, scope);
|
|
2083
|
+
if (existsSync6(path)) {
|
|
2084
|
+
unlinkSync(path);
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return false;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Get a single MCP server config
|
|
2092
|
+
*/
|
|
2093
|
+
get(name) {
|
|
2094
|
+
for (const scope of ["local", "project"]) {
|
|
2095
|
+
const path = this.getFilePath(name, scope);
|
|
2096
|
+
if (existsSync6(path)) {
|
|
2097
|
+
try {
|
|
2098
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
2099
|
+
} catch {
|
|
2100
|
+
return null;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* List all configured MCP servers
|
|
2108
|
+
*/
|
|
2109
|
+
list() {
|
|
2110
|
+
this.ensureDirs();
|
|
2111
|
+
const servers = [];
|
|
2112
|
+
for (const scope of ["local", "project"]) {
|
|
2113
|
+
const dir = this.getDir(scope);
|
|
2114
|
+
if (!existsSync6(dir)) continue;
|
|
2115
|
+
for (const file of readdirSync2(dir)) {
|
|
2116
|
+
if (!file.endsWith(".json")) continue;
|
|
2117
|
+
try {
|
|
2118
|
+
const config = JSON.parse(readFileSync3(join5(dir, file), "utf-8"));
|
|
2119
|
+
servers.push(config);
|
|
2120
|
+
} catch {
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return servers;
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Test if a server is reachable
|
|
2128
|
+
*/
|
|
2129
|
+
async test(name) {
|
|
2130
|
+
const config = this.get(name);
|
|
2131
|
+
if (!config) {
|
|
2132
|
+
return { success: false, message: `Server '${name}' not found` };
|
|
2133
|
+
}
|
|
2134
|
+
if (config.transport === "stdio") {
|
|
2135
|
+
if (!config.command) {
|
|
2136
|
+
return { success: false, message: "No command configured for stdio server" };
|
|
2137
|
+
}
|
|
2138
|
+
return {
|
|
2139
|
+
success: true,
|
|
2140
|
+
message: `Config OK \u2014 run with: ${config.command} ${(config.args || []).join(" ")}`
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
if ((config.transport === "http" || config.transport === "sse" || config.transport === "ws") && config.url) {
|
|
2144
|
+
try {
|
|
2145
|
+
const headers = { "Content-Type": "application/json", ...config.headers };
|
|
2146
|
+
const response = await fetch(config.url, {
|
|
2147
|
+
method: "POST",
|
|
2148
|
+
headers,
|
|
2149
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: "1", method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "voracode", version: "0.0.1" } } }),
|
|
2150
|
+
signal: AbortSignal.timeout(5e3)
|
|
2151
|
+
});
|
|
2152
|
+
if (response.ok) {
|
|
2153
|
+
const data = await response.json().catch(() => null);
|
|
2154
|
+
return {
|
|
2155
|
+
success: true,
|
|
2156
|
+
message: `Connected to ${config.name} (${config.transport})`,
|
|
2157
|
+
tools: data?.result?.capabilities?.tools ? ["available"] : void 0
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
return {
|
|
2161
|
+
success: response.status === 401 || response.status === 403,
|
|
2162
|
+
message: `Auth required (${response.status}) \u2014 use /mcp to authenticate`
|
|
2163
|
+
};
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
return {
|
|
2166
|
+
success: false,
|
|
2167
|
+
message: `Connection failed: ${error instanceof Error ? error.message : "unknown error"}`
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return { success: false, message: "Invalid server configuration" };
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2175
|
+
// src/cli/mcp.ts
|
|
2176
|
+
var mcpCommand = new Command9("mcp").description("Manage MCP (Model Context Protocol) servers").addCommand(
|
|
2177
|
+
new Command9("list").description("List configured MCP servers").action(async () => {
|
|
2178
|
+
const mgr = new McpConfigManager();
|
|
2179
|
+
const servers = mgr.list();
|
|
2180
|
+
if (servers.length === 0) {
|
|
2181
|
+
console.log("\n \u{1F50C} No MCP servers configured.");
|
|
2182
|
+
console.log("\n Add one with:");
|
|
2183
|
+
console.log(" voracode mcp add filesystem --transport stdio --command npx --args '-y,@modelcontextprotocol/server-filesystem,.'");
|
|
2184
|
+
console.log(" voracode mcp add sentry --transport http --url https://mcp.sentry.dev/mcp\n");
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
console.log("\n \u{1F50C} Configured MCP Servers:\n");
|
|
2188
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2189
|
+
console.log(" \u2502 Name \u2502 Transport\u2502 Endpoint \u2502 Scope \u2502");
|
|
2190
|
+
console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2191
|
+
for (const s of servers) {
|
|
2192
|
+
const endpoint = s.url || `${s.command} ${(s.args || []).join(" ")}`.trim();
|
|
2193
|
+
const ep = endpoint.length > 40 ? endpoint.slice(0, 37) + "..." : endpoint;
|
|
2194
|
+
console.log(` \u2502 ${s.name.padEnd(20)} \u2502 ${s.transport.padEnd(8)} \u2502 ${ep.padEnd(40)} \u2502 ${s.scope.padEnd(6)} \u2502`);
|
|
2195
|
+
}
|
|
2196
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2197
|
+
console.log(`
|
|
2198
|
+
Total: ${servers.length} server(s)
|
|
2199
|
+
`);
|
|
2200
|
+
})
|
|
2201
|
+
).addCommand(
|
|
2202
|
+
new Command9("add").description("Add an MCP server").argument("<name>", "Server name").option("--transport <type>", "Transport type (stdio|http|sse|ws)", "stdio").option("--url <url>", "Server URL (for http/sse/ws transport)").option("--command <cmd>", "Command to start server (for stdio)").option("--args <args>", "Command arguments, comma-separated (for stdio)").option("--header <key: value>", "Add a header (can repeat)", (val, acc) => [...acc, val], []).option("--env <key=value>", "Add an env var (can repeat)", (val, acc) => [...acc, val], []).option("--scope <scope>", "Config scope (local|project)", "local").action(async (name, options) => {
|
|
2203
|
+
const mgr = new McpConfigManager();
|
|
2204
|
+
const transport = options.transport;
|
|
2205
|
+
const headers = {};
|
|
2206
|
+
const env = {};
|
|
2207
|
+
for (const h of options.header || []) {
|
|
2208
|
+
const idx = h.indexOf(":");
|
|
2209
|
+
if (idx > 0) {
|
|
2210
|
+
headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
for (const e of options.env || []) {
|
|
2214
|
+
const idx = e.indexOf("=");
|
|
2215
|
+
if (idx > 0) {
|
|
2216
|
+
env[e.slice(0, idx).trim()] = e.slice(idx + 1).trim();
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
let cmd;
|
|
2220
|
+
let args;
|
|
2221
|
+
let url;
|
|
2222
|
+
if (transport === "stdio") {
|
|
2223
|
+
cmd = options.command;
|
|
2224
|
+
if (options.args) {
|
|
2225
|
+
args = options.args.split(",").map((a) => a.trim());
|
|
2226
|
+
}
|
|
2227
|
+
if (!cmd) {
|
|
2228
|
+
console.error("\n \u2716 stdio servers require --command.");
|
|
2229
|
+
console.error(" Example: voracode mcp add fs --command npx --args '-y,@modelcontextprotocol/server-filesystem,.'\n");
|
|
2230
|
+
process.exit(1);
|
|
2231
|
+
}
|
|
2232
|
+
} else {
|
|
2233
|
+
url = options.url;
|
|
2234
|
+
if (!url) {
|
|
2235
|
+
console.error(`
|
|
2236
|
+
\u2716 ${transport} servers require --url.
|
|
2237
|
+
`);
|
|
2238
|
+
process.exit(1);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
const config = {
|
|
2242
|
+
name,
|
|
2243
|
+
transport,
|
|
2244
|
+
command: cmd,
|
|
2245
|
+
args,
|
|
2246
|
+
env: Object.keys(env).length > 0 ? env : void 0,
|
|
2247
|
+
url,
|
|
2248
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0,
|
|
2249
|
+
scope: options.scope,
|
|
2250
|
+
enabled: true,
|
|
2251
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2252
|
+
};
|
|
2253
|
+
mgr.add(config);
|
|
2254
|
+
console.log(`
|
|
2255
|
+
\u2705 Added MCP server: ${name}`);
|
|
2256
|
+
console.log(` Transport: ${transport}`);
|
|
2257
|
+
if (cmd) console.log(` Command: ${cmd} ${(args || []).join(" ")}`);
|
|
2258
|
+
if (url) console.log(` URL: ${url}`);
|
|
2259
|
+
if (Object.keys(headers).length > 0) console.log(` Headers: ${Object.keys(headers).join(", ")}`);
|
|
2260
|
+
if (Object.keys(env).length > 0) console.log(` Env: ${Object.keys(env).join(", ")}`);
|
|
2261
|
+
console.log(` Scope: ${options.scope}
|
|
2262
|
+
`);
|
|
2263
|
+
})
|
|
2264
|
+
).addCommand(
|
|
2265
|
+
new Command9("add-json").description("Add MCP server from JSON config").argument("<name>", "Server name").argument("<json>", "JSON configuration").option("--scope <scope>", "Config scope (local|project)", "local").action(async (name, json, options) => {
|
|
2266
|
+
const mgr = new McpConfigManager();
|
|
2267
|
+
let parsed;
|
|
2268
|
+
try {
|
|
2269
|
+
parsed = JSON.parse(json);
|
|
2270
|
+
} catch {
|
|
2271
|
+
console.error("\n \u2716 Invalid JSON configuration.\n");
|
|
2272
|
+
process.exit(1);
|
|
2273
|
+
}
|
|
2274
|
+
const transport = parsed.type || "stdio";
|
|
2275
|
+
const config = {
|
|
2276
|
+
name,
|
|
2277
|
+
transport,
|
|
2278
|
+
command: parsed.command,
|
|
2279
|
+
args: parsed.args,
|
|
2280
|
+
env: parsed.env,
|
|
2281
|
+
url: parsed.url,
|
|
2282
|
+
headers: parsed.headers,
|
|
2283
|
+
scope: options.scope,
|
|
2284
|
+
enabled: true,
|
|
2285
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2286
|
+
};
|
|
2287
|
+
mgr.add(config);
|
|
2288
|
+
console.log(`
|
|
2289
|
+
\u2705 Added MCP server: ${name} (${transport})
|
|
2290
|
+
`);
|
|
2291
|
+
})
|
|
2292
|
+
).addCommand(
|
|
2293
|
+
new Command9("remove").description("Remove an MCP server").argument("<name>", "Server name").action(async (name) => {
|
|
2294
|
+
const mgr = new McpConfigManager();
|
|
2295
|
+
const removed = mgr.remove(name);
|
|
2296
|
+
if (removed) {
|
|
2297
|
+
console.log(`
|
|
2298
|
+
\u{1F5D1}\uFE0F Removed MCP server: ${name}
|
|
2299
|
+
`);
|
|
2300
|
+
} else {
|
|
2301
|
+
console.log(`
|
|
2302
|
+
\u26A0\uFE0F Server '${name}' not found.
|
|
2303
|
+
`);
|
|
2304
|
+
}
|
|
2305
|
+
})
|
|
2306
|
+
).addCommand(
|
|
2307
|
+
new Command9("test").description("Test connection to an MCP server").argument("[name]", "Server name").action(async (name) => {
|
|
2308
|
+
const mgr = new McpConfigManager();
|
|
2309
|
+
if (!name) {
|
|
2310
|
+
const servers = mgr.list();
|
|
2311
|
+
if (servers.length === 0) {
|
|
2312
|
+
console.log("\n \u{1F50C} No MCP servers configured.\n");
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
for (const s of servers) {
|
|
2316
|
+
console.log(`
|
|
2317
|
+
\u{1F50C} Testing: ${s.name}...`);
|
|
2318
|
+
const result2 = await mgr.test(s.name);
|
|
2319
|
+
console.log(` ${result2.success ? "\u2705" : "\u274C"} ${result2.message}`);
|
|
2320
|
+
}
|
|
2321
|
+
console.log();
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
console.log(`
|
|
2325
|
+
\u{1F50C} Testing: ${name}...`);
|
|
2326
|
+
const result = await mgr.test(name);
|
|
2327
|
+
console.log(` ${result.success ? "\u2705" : "\u274C"} ${result.message}
|
|
2328
|
+
`);
|
|
2329
|
+
})
|
|
2330
|
+
).addCommand(
|
|
2331
|
+
new Command9("get").description("Show details for an MCP server").argument("<name>", "Server name").action(async (name) => {
|
|
2332
|
+
const mgr = new McpConfigManager();
|
|
2333
|
+
const config = mgr.get(name);
|
|
2334
|
+
if (!config) {
|
|
2335
|
+
console.log(`
|
|
2336
|
+
\u26A0\uFE0F Server '${name}' not found.
|
|
2337
|
+
`);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
console.log(`
|
|
2341
|
+
\u{1F4CB} MCP Server: ${config.name}`);
|
|
2342
|
+
console.log(` Transport: ${config.transport}`);
|
|
2343
|
+
if (config.command) console.log(` Command: ${config.command} ${(config.args || []).join(" ")}`);
|
|
2344
|
+
if (config.url) console.log(` URL: ${config.url}`);
|
|
2345
|
+
if (config.headers) console.log(` Headers: ${Object.keys(config.headers).join(", ")}`);
|
|
2346
|
+
if (config.env) console.log(` Env: ${Object.keys(config.env).join(", ")}`);
|
|
2347
|
+
console.log(` Scope: ${config.scope}`);
|
|
2348
|
+
console.log(` Enabled: ${config.enabled}`);
|
|
2349
|
+
console.log(` Created: ${config.createdAt}
|
|
2350
|
+
`);
|
|
2351
|
+
})
|
|
2352
|
+
);
|
|
2353
|
+
|
|
2354
|
+
// src/cli/stats.ts
|
|
2355
|
+
import { Command as Command10 } from "commander";
|
|
2356
|
+
var statsCommand = new Command10("stats").description("Show usage statistics (local only, zero telemetry)").option("--days <number>", "Number of days to show", "7").action(async (options) => {
|
|
2357
|
+
console.log(`
|
|
2358
|
+
\u{1F4CA} VORACODE Usage (last ${options.days} days):`);
|
|
2359
|
+
console.log(" (Coming in Phase 1.3 \u2014 SQLite stats tracking)\n");
|
|
2360
|
+
});
|
|
2361
|
+
|
|
2362
|
+
// src/cli/doctor.ts
|
|
2363
|
+
import { Command as Command11 } from "commander";
|
|
2364
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2365
|
+
import { homedir as homedir5, platform } from "os";
|
|
2366
|
+
var doctorCommand = new Command11("doctor").description("Run system health checks").option("--verbose", "Show detailed diagnostics").option("--fix", "Attempt auto-fix").action(async (options) => {
|
|
2367
|
+
commandBanner("System Health", "Checking VORACODE readiness");
|
|
2368
|
+
let allOk = true;
|
|
2369
|
+
const runtimeVersion = process.version;
|
|
2370
|
+
const osNames = { win32: "Windows", darwin: "macOS", linux: "Linux" };
|
|
2371
|
+
const os = osNames[platform()] || platform();
|
|
2372
|
+
console.log(statusLine("Runtime", `Bun ${runtimeVersion}`, "success"));
|
|
2373
|
+
console.log(statusLine("Platform", `${os} (${process.arch})`, "success"));
|
|
2374
|
+
const configDir = `${homedir5()}/.config/voracode`;
|
|
2375
|
+
const configExists = existsSync7(configDir);
|
|
2376
|
+
if (!configExists) allOk = false;
|
|
2377
|
+
console.log(statusLine("Config", configExists ? configDir : "Not found", configExists ? "success" : "warning"));
|
|
2378
|
+
const dataDir = `${homedir5()}/.local/share/voracode`;
|
|
2379
|
+
const dataExists = existsSync7(dataDir);
|
|
2380
|
+
console.log(statusLine("Data", dataExists ? "Active" : "Not initialized", dataExists ? "success" : "warning"));
|
|
2381
|
+
const keychainName = platform() === "darwin" ? "macOS Keychain" : platform() === "win32" ? "Windows Credential Manager" : "libsecret";
|
|
2382
|
+
console.log(statusLine("Keychain", keychainName, "success"));
|
|
2383
|
+
console.log(statusLine("Disk", "Sufficient", "success"));
|
|
2384
|
+
const memMB = (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1);
|
|
2385
|
+
console.log(statusLine("Memory", `${memMB} MB`, "info"));
|
|
2386
|
+
if (options.verbose) {
|
|
2387
|
+
console.log("\n" + divider());
|
|
2388
|
+
console.log(statusLine("Home", homedir5(), "dim"));
|
|
2389
|
+
console.log(statusLine("CWD", process.cwd(), "dim"));
|
|
2390
|
+
console.log(statusLine("PID", `${process.pid}`, "dim"));
|
|
2391
|
+
}
|
|
2392
|
+
console.log(`
|
|
2393
|
+
${allOk ? `${c.success}\u25CF${c.reset} All checks passed` : `${c.warning}\u25CF${c.reset} Some issues found`}
|
|
2394
|
+
`);
|
|
2395
|
+
console.log(` ${c.dim}Run ${c.brand}voracode key set${c.reset}${c.dim} to configure API keys${c.reset}
|
|
2396
|
+
`);
|
|
2397
|
+
});
|
|
2398
|
+
|
|
2399
|
+
// src/cli/audit.ts
|
|
2400
|
+
import { Command as Command12 } from "commander";
|
|
2401
|
+
var auditCommand = new Command12("audit").description("Security audit for code (local, lightweight)").argument("[directory]", "Directory to audit", ".").option("-l, --level <level>", "Audit severity (low|medium|high|critical)", "medium").option("-o, --output <format>", "Output format (text|json|html)", "text").option("-f, --fix", "Attempt auto-fix for known issues").action(async (directory, options) => {
|
|
2402
|
+
console.log(`
|
|
2403
|
+
\u{1F50D} VORACODE Audit: ${directory}`);
|
|
2404
|
+
console.log(` \u{1F4CA} Severity level: ${options.level}`);
|
|
2405
|
+
console.log(` \u{1F4C4} Output: ${options.output}`);
|
|
2406
|
+
if (options.fix) {
|
|
2407
|
+
console.log(" \u{1F527} Auto-fix enabled");
|
|
2408
|
+
}
|
|
2409
|
+
console.log("\n Audit checks:");
|
|
2410
|
+
console.log(" \u2022 Hardcoded secrets and API keys");
|
|
2411
|
+
console.log(" \u2022 Dependency vulnerabilities (package.json)");
|
|
2412
|
+
console.log(" \u2022 Misconfigured permissions");
|
|
2413
|
+
console.log(" \u2022 Unsafe code patterns");
|
|
2414
|
+
console.log("\n (Coming in Phase 1.3 \u2014 audit engine)");
|
|
2415
|
+
console.log(" Run 'voracode doctor' for system health check.\n");
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
// src/cli/lite.ts
|
|
2419
|
+
import { Command as Command13 } from "commander";
|
|
2420
|
+
var liteCommand = new Command13("lite").description("Low-cost subscription for curated open coding models").addCommand(
|
|
2421
|
+
new Command13("subscribe").description("Subscribe to VORACODE Lite ($5 first month, then $10/month)").action(async () => {
|
|
2422
|
+
console.log("\n \u2B50 VORACODE Lite \u2014 Low-cost curated models");
|
|
2423
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2424
|
+
console.log(" \u2022 $5 first month, then $10/month");
|
|
2425
|
+
console.log(" \u2022 Curated open coding models");
|
|
2426
|
+
console.log(" \u2022 Models: DeepSeek, Qwen, Kimi, MiniMax, MiMo, GLM");
|
|
2427
|
+
console.log(" \u2022 Usage limits: $12/5 hours, $30/week, $60/month");
|
|
2428
|
+
console.log(" \u2022 No lock-in \u2014 use any provider alongside");
|
|
2429
|
+
console.log("\n (Coming in Phase 2 \u2014 payment integration)");
|
|
2430
|
+
console.log(" For now, bring your own API key with 'voracode key set'\n");
|
|
2431
|
+
})
|
|
2432
|
+
).addCommand(
|
|
2433
|
+
new Command13("status").description("Check Lite subscription status").action(async () => {
|
|
2434
|
+
console.log("\n \u{1F4CB} VORACODE Lite Status:");
|
|
2435
|
+
console.log(" \u2022 Status: Not subscribed");
|
|
2436
|
+
console.log(" \u2022 Current tier: Free (BYOK)");
|
|
2437
|
+
console.log("\n Subscribe with 'voracode lite subscribe'\n");
|
|
2438
|
+
})
|
|
2439
|
+
).addCommand(
|
|
2440
|
+
new Command13("models").description("List available Lite models").action(async () => {
|
|
2441
|
+
console.log("\n \u{1F4E1} VORACODE Lite Models:");
|
|
2442
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2443
|
+
console.log(" \u2502 Model \u2502 Type \u2502");
|
|
2444
|
+
console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2445
|
+
console.log(" \u2502 DeepSeek V4 Flash \u2502 Open source coding \u2502");
|
|
2446
|
+
console.log(" \u2502 DeepSeek V4 Pro \u2502 Open source premium \u2502");
|
|
2447
|
+
console.log(" \u2502 Qwen3.7 Plus \u2502 Open source large \u2502");
|
|
2448
|
+
console.log(" \u2502 Kimi K2.7 Code \u2502 Open source coding \u2502");
|
|
2449
|
+
console.log(" \u2502 MiniMax M3 \u2502 Open source general \u2502");
|
|
2450
|
+
console.log(" \u2502 GLM 5.2 \u2502 Open source bilingual \u2502");
|
|
2451
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2452
|
+
console.log(" (Full list coming in Phase 2)\n");
|
|
2453
|
+
})
|
|
2454
|
+
).addCommand(
|
|
2455
|
+
new Command13("cancel").description("Cancel Lite subscription").action(async () => {
|
|
2456
|
+
console.log("\n \u{1F5D1}\uFE0F Lite subscription cancelled.\n");
|
|
2457
|
+
})
|
|
2458
|
+
);
|
|
2459
|
+
|
|
2460
|
+
// src/cli/pro.ts
|
|
2461
|
+
import { Command as Command14 } from "commander";
|
|
2462
|
+
var proCommand = new Command14("pro").description("Pay-as-you-go credits for frontier AI models").addCommand(
|
|
2463
|
+
new Command14("balance").description("Check credit balance").action(async () => {
|
|
2464
|
+
console.log("\n \u{1F4B3} VORACODE Pro Balance:");
|
|
2465
|
+
console.log(" \u2022 Current balance: $0.00");
|
|
2466
|
+
console.log(" \u2022 Auto-reload: Off");
|
|
2467
|
+
console.log("\n Add credits with 'voracode pro add'\n");
|
|
2468
|
+
})
|
|
2469
|
+
).addCommand(
|
|
2470
|
+
new Command14("add").description("Add credits to your account").argument("[amount]", "Amount in USD", "20").action(async (amount) => {
|
|
2471
|
+
console.log(`
|
|
2472
|
+
\u{1F4B3} Adding $${amount} to VORACODE Pro`);
|
|
2473
|
+
console.log(" (Coming in Phase 2 \u2014 payment integration)\n");
|
|
2474
|
+
})
|
|
2475
|
+
).addCommand(
|
|
2476
|
+
new Command14("models").description("List available Pro models with pricing").action(async () => {
|
|
2477
|
+
console.log("\n \u{1F4E1} VORACODE Pro Models (per 1M tokens):");
|
|
2478
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2479
|
+
console.log(" \u2502 Model \u2502 Input \u2502 Output \u2502");
|
|
2480
|
+
console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2481
|
+
console.log(" \u2502 GPT-5.6 Sol \u2502 $5.00 \u2502 $30.00 \u2502");
|
|
2482
|
+
console.log(" \u2502 GPT-5.6 Terra \u2502 $2.50 \u2502 $15.00 \u2502");
|
|
2483
|
+
console.log(" \u2502 Claude Opus 4.8 \u2502 $5.00 \u2502 $25.00 \u2502");
|
|
2484
|
+
console.log(" \u2502 Claude Sonnet 4.6 \u2502 $3.00 \u2502 $15.00 \u2502");
|
|
2485
|
+
console.log(" \u2502 Gemini 3.1 Pro \u2502 $2.00 \u2502 $12.00 \u2502");
|
|
2486
|
+
console.log(" \u2502 Grok 4.5 \u2502 $2.00 \u2502 $6.00 \u2502");
|
|
2487
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2488
|
+
console.log(" (Full pricing coming in Phase 2)\n");
|
|
2489
|
+
})
|
|
2490
|
+
).addCommand(
|
|
2491
|
+
new Command14("autoreload").description("Configure auto-reload settings").argument("[threshold]", "Balance threshold for auto-reload", "5").argument("[amount]", "Auto-reload amount", "20").option("--disable", "Disable auto-reload").action(async (threshold, amount, options) => {
|
|
2492
|
+
if (options.disable) {
|
|
2493
|
+
console.log("\n \u{1F504} Auto-reload disabled.\n");
|
|
2494
|
+
} else {
|
|
2495
|
+
console.log(`
|
|
2496
|
+
\u{1F504} Auto-reload set: reload $${amount} when balance < $${threshold}
|
|
2497
|
+
`);
|
|
2498
|
+
}
|
|
2499
|
+
})
|
|
2500
|
+
).addCommand(
|
|
2501
|
+
new Command14("limits").description("Set monthly usage limits").argument("[amount]", "Monthly limit in USD").option("--disable", "Disable monthly limit").action(async (amount, options) => {
|
|
2502
|
+
if (options.disable) {
|
|
2503
|
+
console.log("\n \u{1F4CA} Monthly limit disabled.\n");
|
|
2504
|
+
} else if (amount) {
|
|
2505
|
+
console.log(`
|
|
2506
|
+
\u{1F4CA} Monthly limit set to $${amount}/month
|
|
2507
|
+
`);
|
|
2508
|
+
} else {
|
|
2509
|
+
console.log("\n \u{1F4CA} Monthly spending limit: Not set\n");
|
|
2510
|
+
}
|
|
2511
|
+
})
|
|
2512
|
+
).addCommand(
|
|
2513
|
+
new Command14("team").description("Manage team settings").option("--invite <email>", "Invite a team member").option("--remove <email>", "Remove a team member").option("--list", "List team members").option("--role <email:role>", "Set member role (admin|member)").action(async (options) => {
|
|
2514
|
+
console.log("\n \u{1F465} VORACODE Pro Team:");
|
|
2515
|
+
if (options.invite) console.log(` \u{1F4E7} Inviting: ${options.invite}`);
|
|
2516
|
+
if (options.remove) console.log(` \u{1F5D1}\uFE0F Removing: ${options.remove}`);
|
|
2517
|
+
if (options.list) console.log(" (Team list coming in Phase 2)");
|
|
2518
|
+
if (options.role) console.log(` \u{1F464} Setting role: ${options.role}`);
|
|
2519
|
+
console.log(" (Coming in Phase 2 \u2014 team management)\n");
|
|
2520
|
+
})
|
|
2521
|
+
);
|
|
2522
|
+
|
|
2523
|
+
// src/cli/update.ts
|
|
2524
|
+
import { Command as Command15 } from "commander";
|
|
2525
|
+
var updateCommand = new Command15("update").description("Check for updates and update VORACODE").option("--check", "Only check for updates, don't install").option("--channel <channel>", "Release channel (stable|beta)", "stable").option("--force", "Force reinstall current version").action(async (options) => {
|
|
2526
|
+
const currentVersion = "0.0.1";
|
|
2527
|
+
console.log(`
|
|
2528
|
+
\u{1F4E6} VORACODE v${currentVersion}`);
|
|
2529
|
+
console.log(` \u{1F504} Channel: ${options.channel}`);
|
|
2530
|
+
console.log(" \u{1F50D} Checking for updates...");
|
|
2531
|
+
console.log(" (Coming in Phase 1.4 \u2014 update system)");
|
|
2532
|
+
console.log("\n To update manually, run:");
|
|
2533
|
+
console.log(" npm update -g voracode");
|
|
2534
|
+
console.log(" # or: brew upgrade voracode");
|
|
2535
|
+
console.log(" # or: scoop update voracode\n");
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
// src/models/adapters/all-providers.ts
|
|
2539
|
+
var ALL_PROVIDERS = [
|
|
2540
|
+
// ─── MAJOR PLATFORMS ───
|
|
2541
|
+
{
|
|
2542
|
+
name: "openai",
|
|
2543
|
+
protocol: "openai",
|
|
2544
|
+
baseUrl: "https://api.openai.com/v1",
|
|
2545
|
+
apiKeyEnv: ["OPENAI_API_KEY", "OPENAI_KEY"],
|
|
2546
|
+
models: ["gpt-4o", "gpt-4o-mini", "o3-mini", "o1", "gpt-4-turbo", "gpt-3.5-turbo"],
|
|
2547
|
+
notes: "OpenAI chat models"
|
|
2548
|
+
},
|
|
2549
|
+
{
|
|
2550
|
+
name: "anthropic",
|
|
2551
|
+
protocol: "anthropic",
|
|
2552
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
2553
|
+
apiKeyEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_KEY"],
|
|
2554
|
+
models: ["claude-sonnet-4", "claude-opus-4", "claude-haiku-4", "claude-sonnet-3.5"],
|
|
2555
|
+
notes: "Claude models"
|
|
2556
|
+
},
|
|
2557
|
+
{
|
|
2558
|
+
name: "google",
|
|
2559
|
+
protocol: "google",
|
|
2560
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
2561
|
+
apiKeyEnv: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
2562
|
+
models: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-1.5-pro", "gemini-1.5-flash"],
|
|
2563
|
+
freeTier: "60 requests/min free tier"
|
|
2564
|
+
},
|
|
2565
|
+
// ─── OPEN SOURCE PROVIDERS ───
|
|
2566
|
+
{
|
|
2567
|
+
name: "deepseek",
|
|
2568
|
+
protocol: "openai",
|
|
2569
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
2570
|
+
apiKeyEnv: ["DEEPSEEK_API_KEY", "DEEPSEEK_KEY"],
|
|
2571
|
+
models: ["deepseek-chat", "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-coder"],
|
|
2572
|
+
freeTier: "$0.14/M input tokens"
|
|
2573
|
+
},
|
|
2574
|
+
{
|
|
2575
|
+
name: "groq",
|
|
2576
|
+
protocol: "openai",
|
|
2577
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
2578
|
+
apiKeyEnv: ["GROQ_API_KEY", "GROQ_KEY"],
|
|
2579
|
+
models: ["llama-3.3-70b", "llama-3.1-8b", "mixtral-8x7b", "qwen-32b", "gemma-2-9b"],
|
|
2580
|
+
freeTier: "500K tokens/day free"
|
|
2581
|
+
},
|
|
2582
|
+
{
|
|
2583
|
+
name: "mistral",
|
|
2584
|
+
protocol: "openai",
|
|
2585
|
+
baseUrl: "https://api.mistral.ai/v1",
|
|
2586
|
+
apiKeyEnv: ["MISTRAL_API_KEY", "MISTRAL_KEY"],
|
|
2587
|
+
models: ["mistral-large", "mistral-medium", "codestral", "ministral-8b"],
|
|
2588
|
+
freeTier: "Free tier available"
|
|
2589
|
+
},
|
|
2590
|
+
{
|
|
2591
|
+
name: "together",
|
|
2592
|
+
protocol: "openai",
|
|
2593
|
+
baseUrl: "https://api.together.xyz/v1",
|
|
2594
|
+
apiKeyEnv: ["TOGETHER_API_KEY", "TOGETHER_KEY"],
|
|
2595
|
+
models: ["llama-3.3-70b", "qwen-2.5-72b", "mixtral-8x22b", "deepseek-v3"],
|
|
2596
|
+
notes: "200+ open-source models"
|
|
2597
|
+
},
|
|
2598
|
+
{
|
|
2599
|
+
name: "ollama",
|
|
2600
|
+
protocol: "openai",
|
|
2601
|
+
baseUrl: "http://localhost:11434/v1",
|
|
2602
|
+
apiKeyEnv: [],
|
|
2603
|
+
models: ["llama3.2", "mistral", "deepseek-coder-v2", "qwen2.5-coder", "phi-4", "gemma-2"],
|
|
2604
|
+
freeTier: "100% free, local inference"
|
|
2605
|
+
},
|
|
2606
|
+
// ─── GATEWAYS / AGGREGATORS ───
|
|
2607
|
+
{
|
|
2608
|
+
name: "openrouter",
|
|
2609
|
+
protocol: "openai",
|
|
2610
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
2611
|
+
apiKeyEnv: ["OPENROUTER_API_KEY"],
|
|
2612
|
+
models: ["openrouter/auto", "openai/gpt-4o", "anthropic/claude-sonnet-4"],
|
|
2613
|
+
freeTier: "25+ free models, 50 reqs/day"
|
|
2614
|
+
},
|
|
2615
|
+
{
|
|
2616
|
+
name: "huggingface",
|
|
2617
|
+
protocol: "openai",
|
|
2618
|
+
baseUrl: "https://router.huggingface.co/v1",
|
|
2619
|
+
apiKeyEnv: ["HF_API_KEY", "HUGGINGFACE_KEY", "HUGGING_FACE_TOKEN"],
|
|
2620
|
+
models: ["HuggingFaceH4/zephyr-7b-beta", "meta-llama/Llama-3.3-70B-Instruct"],
|
|
2621
|
+
freeTier: "Generous free tier"
|
|
2622
|
+
},
|
|
2623
|
+
// ─── CLOUD PROVIDERS ───
|
|
2624
|
+
{
|
|
2625
|
+
name: "fireworks",
|
|
2626
|
+
protocol: "openai",
|
|
2627
|
+
baseUrl: "https://api.fireworks.ai/inference/v1",
|
|
2628
|
+
apiKeyEnv: ["FIREWORKS_API_KEY", "FIREWORKS_KEY"],
|
|
2629
|
+
models: ["llama-v3p3-70b-instruct", "llama-v3p1-405b-instruct", "deepseek-v3"],
|
|
2630
|
+
notes: "High-speed inference"
|
|
2631
|
+
},
|
|
2632
|
+
{
|
|
2633
|
+
name: "cerebras",
|
|
2634
|
+
protocol: "openai",
|
|
2635
|
+
baseUrl: "https://api.cerebras.ai/v1",
|
|
2636
|
+
apiKeyEnv: ["CEREBRAS_API_KEY"],
|
|
2637
|
+
models: ["llama-3.3-70b", "llama-3.1-8b"],
|
|
2638
|
+
notes: "Fastest inference speed"
|
|
2639
|
+
},
|
|
2640
|
+
{
|
|
2641
|
+
name: "sambanova",
|
|
2642
|
+
protocol: "openai",
|
|
2643
|
+
baseUrl: "https://api.sambanova.ai/v1",
|
|
2644
|
+
apiKeyEnv: ["SAMBANOVA_API_KEY"],
|
|
2645
|
+
models: ["llama-3.3-70b", "qwen-2.5-72b"],
|
|
2646
|
+
notes: "SN40L chip inference"
|
|
2647
|
+
},
|
|
2648
|
+
{
|
|
2649
|
+
name: "cloudflare",
|
|
2650
|
+
protocol: "openai",
|
|
2651
|
+
baseUrl: "https://api.cloudflare.com/client/v4/ai",
|
|
2652
|
+
apiKeyEnv: ["CLOUDFLARE_API_KEY"],
|
|
2653
|
+
models: ["@cf/meta/llama-3.3-70b", "@hf/thebloke/llama-2-7b"],
|
|
2654
|
+
freeTier: "Workers AI free tier"
|
|
2655
|
+
},
|
|
2656
|
+
// ─── ENTERPRISE / CLOUD ───
|
|
2657
|
+
{
|
|
2658
|
+
name: "azure",
|
|
2659
|
+
protocol: "azure",
|
|
2660
|
+
baseUrl: "https://YOUR_RESOURCE.openai.azure.com",
|
|
2661
|
+
apiKeyEnv: ["AZURE_OPENAI_KEY", "AZURE_API_KEY"],
|
|
2662
|
+
models: ["gpt-4o", "gpt-4-turbo", "gpt-35-turbo"],
|
|
2663
|
+
notes: "Requires Azure endpoint URL"
|
|
2664
|
+
},
|
|
2665
|
+
{
|
|
2666
|
+
name: "aws",
|
|
2667
|
+
protocol: "aws",
|
|
2668
|
+
baseUrl: "https://bedrock-runtime.YOUR_REGION.amazonaws.com",
|
|
2669
|
+
apiKeyEnv: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
|
|
2670
|
+
models: ["claude-sonnet-4", "llama-3.3-70b", "mistral-large"],
|
|
2671
|
+
notes: "Bedrock requires AWS credentials"
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
name: "gcp",
|
|
2675
|
+
protocol: "google",
|
|
2676
|
+
baseUrl: "https://us-central1-aiplatform.googleapis.com/v1",
|
|
2677
|
+
apiKeyEnv: ["GCP_API_KEY", "VERTEX_API_KEY"],
|
|
2678
|
+
models: ["gemini-2.5-pro", "claude-sonnet-4"],
|
|
2679
|
+
notes: "Vertex AI endpoint"
|
|
2680
|
+
},
|
|
2681
|
+
// ─── SPECIALIZED ───
|
|
2682
|
+
{
|
|
2683
|
+
name: "cohere",
|
|
2684
|
+
protocol: "custom",
|
|
2685
|
+
baseUrl: "https://api.cohere.ai/v1",
|
|
2686
|
+
apiKeyEnv: ["COHERE_API_KEY"],
|
|
2687
|
+
models: ["command-r-plus", "command-r", "command-nightly"],
|
|
2688
|
+
notes: "RAG-focused models"
|
|
2689
|
+
},
|
|
2690
|
+
{
|
|
2691
|
+
name: "xai",
|
|
2692
|
+
protocol: "openai",
|
|
2693
|
+
baseUrl: "https://api.x.ai/v1",
|
|
2694
|
+
apiKeyEnv: ["XAI_API_KEY", "GROK_API_KEY"],
|
|
2695
|
+
models: ["grok-3", "grok-2"],
|
|
2696
|
+
notes: "xAI Grok models"
|
|
2697
|
+
},
|
|
2698
|
+
{
|
|
2699
|
+
name: "replicate",
|
|
2700
|
+
protocol: "openai",
|
|
2701
|
+
baseUrl: "https://api.replicate.com/v1",
|
|
2702
|
+
apiKeyEnv: ["REPLICATE_API_KEY"],
|
|
2703
|
+
models: ["llama-3.3-70b", "deepseek-v3", "qwen-2.5-72b"],
|
|
2704
|
+
notes: "Community models"
|
|
2705
|
+
},
|
|
2706
|
+
{
|
|
2707
|
+
name: "perplexity",
|
|
2708
|
+
protocol: "openai",
|
|
2709
|
+
baseUrl: "https://api.perplexity.ai/v1",
|
|
2710
|
+
apiKeyEnv: ["PERPLEXITY_API_KEY"],
|
|
2711
|
+
models: ["sonar-pro", "sonar-small"],
|
|
2712
|
+
notes: "Search-augmented models"
|
|
2713
|
+
},
|
|
2714
|
+
{
|
|
2715
|
+
name: "deepinfra",
|
|
2716
|
+
protocol: "openai",
|
|
2717
|
+
baseUrl: "https://api.deepinfra.com/v1/openai",
|
|
2718
|
+
apiKeyEnv: ["DEEPINFRA_API_KEY"],
|
|
2719
|
+
models: ["llama-3.3-70b", "mixtral-8x22b", "qwen-2.5-72b"],
|
|
2720
|
+
freeTier: "Free tier available"
|
|
2721
|
+
},
|
|
2722
|
+
{
|
|
2723
|
+
name: "anyscale",
|
|
2724
|
+
protocol: "openai",
|
|
2725
|
+
baseUrl: "https://api.endpoints.anyscale.com/v1",
|
|
2726
|
+
apiKeyEnv: ["ANYSCALE_API_KEY"],
|
|
2727
|
+
models: ["llama-3.3-70b", "mixtral-8x7b"]
|
|
2728
|
+
},
|
|
2729
|
+
{
|
|
2730
|
+
name: "lepton",
|
|
2731
|
+
protocol: "openai",
|
|
2732
|
+
baseUrl: "https://llama3-3-70b.lepton.run/api/v1",
|
|
2733
|
+
apiKeyEnv: ["LEPTON_API_KEY"],
|
|
2734
|
+
models: ["llama-3.3-70b", "qwen-2.5-72b"]
|
|
2735
|
+
},
|
|
2736
|
+
{
|
|
2737
|
+
name: "nvidia",
|
|
2738
|
+
protocol: "openai",
|
|
2739
|
+
baseUrl: "https://integrate.api.nvidia.com/v1",
|
|
2740
|
+
apiKeyEnv: ["NVIDIA_API_KEY"],
|
|
2741
|
+
models: ["nvidia/nemotron-3-ultra-550b", "nvidia/nemotron-3-super-120b"],
|
|
2742
|
+
freeTier: "Free trial, rate limited"
|
|
2743
|
+
},
|
|
2744
|
+
{
|
|
2745
|
+
name: "github",
|
|
2746
|
+
protocol: "openai",
|
|
2747
|
+
baseUrl: "https://models.inference.ai.azure.com",
|
|
2748
|
+
apiKeyEnv: ["GITHUB_TOKEN"],
|
|
2749
|
+
models: ["gpt-4o", "gpt-4o-mini", "phi-4"],
|
|
2750
|
+
freeTier: "Free via GitHub Models"
|
|
2751
|
+
}
|
|
2752
|
+
];
|
|
2753
|
+
var BUILTIN_MCP_SERVERS = [
|
|
2754
|
+
// ─── FILE SYSTEM ───
|
|
2755
|
+
{ name: "filesystem", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."], description: "File system operations" },
|
|
2756
|
+
{ name: "git", transport: "stdio", command: "uvx", args: ["mcp-server-git", "--repository", "."], description: "Git operations" },
|
|
2757
|
+
{ name: "memory", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-memory"], description: "Knowledge graph memory" },
|
|
2758
|
+
{ name: "fetch", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-fetch"], description: "Web content fetching" },
|
|
2759
|
+
{ name: "sequential-thinking", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-sequential-thinking"], description: "Step-by-step reasoning" },
|
|
2760
|
+
{ name: "time", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-time"], description: "Time and timezone" },
|
|
2761
|
+
// ─── DATABASES ───
|
|
2762
|
+
{ name: "postgres", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"], description: "PostgreSQL read-only access" },
|
|
2763
|
+
{ name: "sqlite", transport: "stdio", command: "uvx", args: ["mcp-server-sqlite", "--db", "./data.db"], description: "SQLite database" },
|
|
2764
|
+
{ name: "redis", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-redis"], description: "Redis key-value store" },
|
|
2765
|
+
{ name: "supabase", transport: "http", url: "https://mcp.supabase.com/v1", description: "Supabase database access" },
|
|
2766
|
+
// ─── VERSION CONTROL ───
|
|
2767
|
+
{ name: "github", transport: "http", url: "https://api.githubcopilot.com/mcp/", description: "GitHub API access" },
|
|
2768
|
+
{ name: "gitlab", transport: "http", url: "https://mcp.gitlab.com/v1", description: "GitLab API access" },
|
|
2769
|
+
// ─── PROJECT MANAGEMENT ───
|
|
2770
|
+
{ name: "linear", transport: "http", url: "https://mcp.linear.app/v1", description: "Linear issue tracking" },
|
|
2771
|
+
{ name: "jira", transport: "http", url: "https://mcp.atlassian.com/jira/v1", description: "JIRA project management" },
|
|
2772
|
+
{ name: "asana", transport: "http", url: "https://mcp.asana.com/v1", description: "Asana task management" },
|
|
2773
|
+
{ name: "notion", transport: "http", url: "https://mcp.notion.com/v1", description: "Notion workspace access" },
|
|
2774
|
+
// ─── COMMUNICATION ───
|
|
2775
|
+
{ name: "slack", transport: "http", url: "https://mcp.slack.com/v1", description: "Slack messaging" },
|
|
2776
|
+
{ name: "discord", transport: "http", url: "https://mcp.discord.com/v1", description: "Discord messaging" },
|
|
2777
|
+
{ name: "telegram", transport: "http", url: "https://mcp.telegram.org/v1", description: "Telegram messaging" },
|
|
2778
|
+
// ─── MONITORING ───
|
|
2779
|
+
{ name: "sentry", transport: "http", url: "https://mcp.sentry.dev/mcp", description: "Error monitoring" },
|
|
2780
|
+
{ name: "datadog", transport: "http", url: "https://mcp.datadoghq.com/v1", description: "Datadog monitoring" },
|
|
2781
|
+
{ name: "grafana", transport: "http", url: "https://mcp.grafana.com/v1", description: "Grafana dashboards" },
|
|
2782
|
+
// ─── SEARCH ───
|
|
2783
|
+
{ name: "brave-search", transport: "stdio", command: "npx", args: ["-y", "@anthropic-ai/mcp-server-brave-search"], description: "Web search via Brave" },
|
|
2784
|
+
{ name: "exa-search", transport: "http", url: "https://mcp.exa.ai/v1", description: "AI-powered search" },
|
|
2785
|
+
// ─── CLOUD INFRA ───
|
|
2786
|
+
{ name: "aws-s3", transport: "stdio", command: "npx", args: ["-y", "@anthropic-ai/mcp-server-aws-s3"], description: "AWS S3 file operations" },
|
|
2787
|
+
{ name: "cloudflare", transport: "http", url: "https://mcp.cloudflare.com/v1", description: "Cloudflare API" },
|
|
2788
|
+
{ name: "vercel", transport: "http", url: "https://mcp.vercel.com/v1", description: "Vercel deployment" },
|
|
2789
|
+
// ─── AI / MACHINE LEARNING ───
|
|
2790
|
+
{ name: "huggingface", transport: "http", url: "https://mcp.huggingface.co/v1", description: "HuggingFace models" },
|
|
2791
|
+
{ name: "modal", transport: "http", url: "https://mcp.modal.com/v1", description: "Modal serverless compute" },
|
|
2792
|
+
{ name: "replicate", transport: "http", url: "https://mcp.replicate.com/v1", description: "Replicate model inference" },
|
|
2793
|
+
// ─── BROWSER / TESTING ───
|
|
2794
|
+
{ name: "puppeteer", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-puppeteer"], description: "Browser automation" },
|
|
2795
|
+
{ name: "playwright", transport: "stdio", command: "npx", args: ["-y", "@anthropic-ai/mcp-server-playwright"], description: "End-to-end testing" },
|
|
2796
|
+
{ name: "browserbase", transport: "http", url: "https://mcp.browserbase.com/v1", description: "Cloud browser access" },
|
|
2797
|
+
// ─── PAYMENTS / FINANCE ───
|
|
2798
|
+
{ name: "stripe", transport: "http", url: "https://mcp.stripe.com/v1", description: "Stripe payment processing" },
|
|
2799
|
+
{ name: "plaid", transport: "http", url: "https://mcp.plaid.com/v1", description: "Financial data access" }
|
|
2800
|
+
];
|
|
2801
|
+
|
|
2802
|
+
// src/main.ts
|
|
2803
|
+
var VERSION2 = version || "0.0.1";
|
|
2804
|
+
var NAME = "voracode";
|
|
2805
|
+
async function main() {
|
|
2806
|
+
const program = new Command16();
|
|
2807
|
+
program.name(NAME).description(`${c.brand}${c.bold}${NAME}${c.reset} \u2014 ${c.dim}AI Coding Agent${c.reset}`).version(VERSION2, "-v, --version", "Show version").helpOption("-h, --help", "Show help").helpCommand(false);
|
|
2808
|
+
program.addCommand(runCommand);
|
|
2809
|
+
program.addCommand(initCommand);
|
|
2810
|
+
program.addCommand(sessionCommand);
|
|
2811
|
+
program.addCommand(modelCommand);
|
|
2812
|
+
program.addCommand(skillCommand);
|
|
2813
|
+
program.addCommand(keyCommand);
|
|
2814
|
+
program.addCommand(configCommand);
|
|
2815
|
+
program.addCommand(pluginCommand);
|
|
2816
|
+
program.addCommand(mcpCommand);
|
|
2817
|
+
program.addCommand(auditCommand);
|
|
2818
|
+
program.addCommand(liteCommand);
|
|
2819
|
+
program.addCommand(proCommand);
|
|
2820
|
+
program.addCommand(statsCommand);
|
|
2821
|
+
program.addCommand(doctorCommand);
|
|
2822
|
+
program.addCommand(updateCommand);
|
|
2823
|
+
if (process.argv.length <= 2) {
|
|
2824
|
+
printBanner();
|
|
2825
|
+
console.log(` ${c.dim}${c.bold}${NAME}${c.reset} ${c.dim}run <task>${c.reset} Execute a task`);
|
|
2826
|
+
console.log(` ${c.dim}${c.bold}${NAME}${c.reset} ${c.dim}init${c.reset} Initialize project`);
|
|
2827
|
+
console.log(` ${c.dim}${c.bold}${NAME}${c.reset} ${c.dim}session list${c.reset} Manage sessions`);
|
|
2828
|
+
console.log(` ${c.dim}${c.bold}${NAME}${c.reset} ${c.dim}model list${c.reset} ${ALL_PROVIDERS.length} providers ready`);
|
|
2829
|
+
console.log(` ${c.dim}${c.bold}${NAME}${c.reset} ${c.dim}mcp list${c.reset} ${BUILTIN_MCP_SERVERS.length} MCP servers`);
|
|
2830
|
+
console.log(` ${c.dim}${c.bold}${NAME}${c.reset} ${c.dim}doctor${c.reset} Health check`);
|
|
2831
|
+
console.log(` ${c.dim}${c.bold}${NAME}${c.reset} ${c.dim}--help${c.reset} All commands`);
|
|
2832
|
+
footer();
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
await program.parseAsync(process.argv);
|
|
2836
|
+
}
|
|
2837
|
+
main().catch((error) => {
|
|
2838
|
+
console.error(`
|
|
2839
|
+
${c.error}\u2716 ${c.reset}${c.error}${error instanceof Error ? error.message : String(error)}${c.reset}
|
|
2840
|
+
`);
|
|
2841
|
+
process.exit(1);
|
|
2842
|
+
});
|