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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VORACODE Session Manager — Create, resume, fork sessions
|
|
3
|
+
*
|
|
4
|
+
* Integrates with SQLite database for persistence.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { VoraDatabase } from "../storage/database";
|
|
8
|
+
import { ModelRouter } from "../models/router";
|
|
9
|
+
import { Agent } from "../engine/agent";
|
|
10
|
+
|
|
11
|
+
export interface SessionInfo {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
modelProvider: string;
|
|
15
|
+
modelName: string;
|
|
16
|
+
totalTokens: number;
|
|
17
|
+
totalTurns: number;
|
|
18
|
+
status: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class SessionManager {
|
|
24
|
+
private db: VoraDatabase;
|
|
25
|
+
private router: ModelRouter;
|
|
26
|
+
private agent: Agent;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
this.db = new VoraDatabase();
|
|
30
|
+
this.router = new ModelRouter();
|
|
31
|
+
this.agent = new Agent();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a new session
|
|
36
|
+
*/
|
|
37
|
+
create(modelRef?: string): string {
|
|
38
|
+
const provider = modelRef ? this.router.detectProvider(modelRef) : "auto";
|
|
39
|
+
const modelName = modelRef || "auto";
|
|
40
|
+
return this.db.createSession(provider, modelName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resume an existing session
|
|
45
|
+
*/
|
|
46
|
+
getSession(id: string): SessionInfo | null {
|
|
47
|
+
const session = this.db.getSession(id) as Record<string, unknown> | null;
|
|
48
|
+
if (!session) return null;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: session.id as string,
|
|
52
|
+
title: session.title as string,
|
|
53
|
+
modelProvider: session.model_provider as string,
|
|
54
|
+
modelName: session.model_name as string,
|
|
55
|
+
totalTokens: (session.total_tokens as number) || 0,
|
|
56
|
+
totalTurns: (session.total_turns as number) || 0,
|
|
57
|
+
status: session.status as string,
|
|
58
|
+
createdAt: session.created_at as string,
|
|
59
|
+
updatedAt: session.updated_at as string,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List all sessions
|
|
65
|
+
*/
|
|
66
|
+
list(limit = 10): SessionInfo[] {
|
|
67
|
+
return (this.db.listSessions(limit) as Record<string, unknown>[]).map((s) => ({
|
|
68
|
+
id: s.id as string,
|
|
69
|
+
title: s.title as string,
|
|
70
|
+
modelProvider: s.model_provider as string,
|
|
71
|
+
modelName: s.model_name as string,
|
|
72
|
+
totalTokens: (s.total_tokens as number) || 0,
|
|
73
|
+
totalTurns: (s.total_turns as number) || 0,
|
|
74
|
+
status: s.status as string,
|
|
75
|
+
createdAt: s.created_at as string,
|
|
76
|
+
updatedAt: s.updated_at as string,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Delete a session
|
|
82
|
+
*/
|
|
83
|
+
delete(id: string): void {
|
|
84
|
+
this.db.deleteSession(id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run a task in a session
|
|
89
|
+
*/
|
|
90
|
+
async run(sessionId: string, message: string, modelRef: string, options?: { maxTurns?: number }) {
|
|
91
|
+
return this.agent.runTurn(sessionId, message, modelRef, options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fork a session (create new from existing)
|
|
96
|
+
*/
|
|
97
|
+
fork(id: string): string | null {
|
|
98
|
+
const original = this.getSession(id);
|
|
99
|
+
if (!original) return null;
|
|
100
|
+
|
|
101
|
+
const newId = this.db.createSession(original.modelProvider, original.modelName, `${original.title} (fork)`);
|
|
102
|
+
return newId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get messages for a session
|
|
107
|
+
*/
|
|
108
|
+
getMessages(sessionId: string) {
|
|
109
|
+
return this.db.getMessages(sessionId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get database path
|
|
114
|
+
*/
|
|
115
|
+
getDbPath(): string {
|
|
116
|
+
return this.db.getDbPath();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VORACODE Self-Improvement Engine
|
|
3
|
+
*
|
|
4
|
+
* Learns from user patterns to suggest reusable skills.
|
|
5
|
+
* Privacy-first: all learning is local, no data sent anywhere.
|
|
6
|
+
*
|
|
7
|
+
* 3-Gate Learning System:
|
|
8
|
+
* Gate 1: Repetition (3+ similar tasks)
|
|
9
|
+
* Gate 2: Success rate (80%+ to auto-learn)
|
|
10
|
+
* Gate 3: User confirmation (always ask before creating skill)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { VoraDatabase } from "../../storage/database";
|
|
14
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
|
|
18
|
+
export interface TaskPattern {
|
|
19
|
+
id: number;
|
|
20
|
+
signature: string; // hash of task type + tools used
|
|
21
|
+
description: string; // human-readable description
|
|
22
|
+
toolSequence: string[]; // tools used in order
|
|
23
|
+
filesChanged: string[]; // file paths touched
|
|
24
|
+
commandsRun: string[]; // bash commands run
|
|
25
|
+
successCount: number;
|
|
26
|
+
failureCount: number;
|
|
27
|
+
lastSeen: string;
|
|
28
|
+
suggestedSkill: boolean; // has user been asked about this?
|
|
29
|
+
userDecision: "pending" | "accepted" | "rejected" | "never";
|
|
30
|
+
skillPath?: string; // path to generated SKILL.md
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class SelfImprovementEngine {
|
|
34
|
+
private db: VoraDatabase;
|
|
35
|
+
private skillsDir: string;
|
|
36
|
+
|
|
37
|
+
constructor(db: VoraDatabase) {
|
|
38
|
+
this.db = db;
|
|
39
|
+
this.skillsDir = join(homedir(), ".config", "voracode", "skills");
|
|
40
|
+
if (!existsSync(this.skillsDir)) {
|
|
41
|
+
mkdirSync(this.skillsDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
this.ensureTables();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private ensureTables(): void {
|
|
47
|
+
this.db.exec(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS task_patterns (
|
|
49
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
50
|
+
signature TEXT UNIQUE NOT NULL,
|
|
51
|
+
description TEXT NOT NULL,
|
|
52
|
+
tool_sequence TEXT NOT NULL,
|
|
53
|
+
files_changed TEXT,
|
|
54
|
+
commands_run TEXT,
|
|
55
|
+
success_count INTEGER DEFAULT 0,
|
|
56
|
+
failure_count INTEGER DEFAULT 0,
|
|
57
|
+
last_seen TEXT DEFAULT (datetime('now')),
|
|
58
|
+
suggested_skill INTEGER DEFAULT 0,
|
|
59
|
+
user_decision TEXT DEFAULT 'pending',
|
|
60
|
+
skill_path TEXT
|
|
61
|
+
)
|
|
62
|
+
`);
|
|
63
|
+
|
|
64
|
+
this.db.exec(`
|
|
65
|
+
CREATE TABLE IF NOT EXISTS user_preferences (
|
|
66
|
+
key TEXT PRIMARY KEY,
|
|
67
|
+
value TEXT NOT NULL,
|
|
68
|
+
category TEXT,
|
|
69
|
+
confidence REAL DEFAULT 0.5,
|
|
70
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
71
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
72
|
+
)
|
|
73
|
+
`);
|
|
74
|
+
|
|
75
|
+
this.db.exec(`
|
|
76
|
+
CREATE TABLE IF NOT EXISTS skill_history (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
skill_name TEXT NOT NULL,
|
|
79
|
+
version INTEGER DEFAULT 1,
|
|
80
|
+
action TEXT NOT NULL,
|
|
81
|
+
content TEXT NOT NULL,
|
|
82
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
83
|
+
)
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Record a completed task and check for patterns
|
|
89
|
+
*/
|
|
90
|
+
recordTask(
|
|
91
|
+
description: string,
|
|
92
|
+
toolsUsed: string[],
|
|
93
|
+
filesChanged: string[],
|
|
94
|
+
commandsRun: string[],
|
|
95
|
+
success: boolean,
|
|
96
|
+
): TaskPattern | null {
|
|
97
|
+
// Create a signature from tools used (not file paths — those are project specific)
|
|
98
|
+
const signature = this.createSignature(toolsUsed, description);
|
|
99
|
+
|
|
100
|
+
// Try to find existing pattern
|
|
101
|
+
const existing = this.db.query(
|
|
102
|
+
"SELECT * FROM task_patterns WHERE signature = ?",
|
|
103
|
+
).get(signature) as Record<string, unknown> | null;
|
|
104
|
+
|
|
105
|
+
if (existing) {
|
|
106
|
+
// Update existing pattern
|
|
107
|
+
const newSuccess = (existing.success_count as number) + (success ? 1 : 0);
|
|
108
|
+
const newFailure = (existing.failure_count as number) + (success ? 0 : 1);
|
|
109
|
+
const total = newSuccess + newFailure;
|
|
110
|
+
const successRate = total > 0 ? newSuccess / total : 0;
|
|
111
|
+
|
|
112
|
+
this.db.run(
|
|
113
|
+
`UPDATE task_patterns SET
|
|
114
|
+
success_count = ?, failure_count = ?, last_seen = datetime('now')
|
|
115
|
+
WHERE signature = ?`,
|
|
116
|
+
[newSuccess, newFailure, signature],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Check if we should suggest a skill
|
|
120
|
+
if (total >= 3 && successRate >= 0.8 && !existing.suggested_skill) {
|
|
121
|
+
const pattern: TaskPattern = {
|
|
122
|
+
id: existing.id as number,
|
|
123
|
+
signature,
|
|
124
|
+
description: existing.description as string,
|
|
125
|
+
toolSequence: JSON.parse(existing.tool_sequence as string),
|
|
126
|
+
filesChanged: JSON.parse((existing.files_changed as string) || "[]"),
|
|
127
|
+
commandsRun: JSON.parse((existing.commands_run as string) || "[]"),
|
|
128
|
+
successCount: newSuccess,
|
|
129
|
+
failureCount: newFailure,
|
|
130
|
+
lastSeen: new Date().toISOString(),
|
|
131
|
+
suggestedSkill: true,
|
|
132
|
+
userDecision: "pending",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Mark as suggested
|
|
136
|
+
this.db.run("UPDATE task_patterns SET suggested_skill = 1 WHERE signature = ?", [signature]);
|
|
137
|
+
|
|
138
|
+
return pattern;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Create new pattern
|
|
145
|
+
this.db.run(
|
|
146
|
+
`INSERT INTO task_patterns
|
|
147
|
+
(signature, description, tool_sequence, files_changed, commands_run, success_count, failure_count)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
149
|
+
[
|
|
150
|
+
signature,
|
|
151
|
+
description,
|
|
152
|
+
JSON.stringify(toolsUsed),
|
|
153
|
+
JSON.stringify(filesChanged),
|
|
154
|
+
JSON.stringify(commandsRun),
|
|
155
|
+
success ? 1 : 0,
|
|
156
|
+
success ? 0 : 1,
|
|
157
|
+
],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a pattern signature from tools and task description
|
|
165
|
+
*/
|
|
166
|
+
private createSignature(toolsUsed: string[], description: string): string {
|
|
167
|
+
// Normalize: lowercase, remove file paths, keep tool sequence
|
|
168
|
+
const tools = toolsUsed.map((t) => t.toLowerCase()).sort().join(",");
|
|
169
|
+
const taskType = this.classifyTask(description);
|
|
170
|
+
return `${taskType}|${tools}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Classify a task into a category
|
|
175
|
+
*/
|
|
176
|
+
private classifyTask(description: string): string {
|
|
177
|
+
const d = description.toLowerCase();
|
|
178
|
+
if (d.includes("create") || d.includes("new") || d.includes("make")) return "create";
|
|
179
|
+
if (d.includes("fix") || d.includes("bug") || d.includes("error")) return "fix";
|
|
180
|
+
if (d.includes("refactor") || d.includes("optimize") || d.includes("improve")) return "refactor";
|
|
181
|
+
if (d.includes("test") || d.includes("spec")) return "test";
|
|
182
|
+
if (d.includes("deploy") || d.includes("publish")) return "deploy";
|
|
183
|
+
if (d.includes("explain") || d.includes("understand") || d.includes("how")) return "explain";
|
|
184
|
+
return "general";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate a SKILL.md from a pattern
|
|
189
|
+
*/
|
|
190
|
+
generateSkill(pattern: TaskPattern): string {
|
|
191
|
+
const skillName = pattern.description
|
|
192
|
+
.toLowerCase()
|
|
193
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
194
|
+
.replace(/^-|-$/g, "")
|
|
195
|
+
.slice(0, 40) || "learned-skill";
|
|
196
|
+
|
|
197
|
+
const skillDir = join(this.skillsDir, skillName);
|
|
198
|
+
mkdirSync(skillDir, { recursive: true });
|
|
199
|
+
|
|
200
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
201
|
+
const content = `---
|
|
202
|
+
name: ${skillName}
|
|
203
|
+
description: Auto-learned skill for: ${pattern.description}
|
|
204
|
+
version: 1.0.0
|
|
205
|
+
source: learned
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## When to use
|
|
209
|
+
Use this skill when performing: ${pattern.description}
|
|
210
|
+
|
|
211
|
+
## Pattern discovered
|
|
212
|
+
- Tools used: ${pattern.toolSequence.join(", ")}
|
|
213
|
+
- Success rate: ${((pattern.successCount / (pattern.successCount + pattern.failureCount)) * 100).toFixed(0)}%
|
|
214
|
+
- Times seen: ${pattern.successCount + pattern.failureCount}
|
|
215
|
+
|
|
216
|
+
## Instructions
|
|
217
|
+
1. Follow the same tool sequence that worked before
|
|
218
|
+
2. Apply to the specific project structure
|
|
219
|
+
3. Verify the result matches expectations
|
|
220
|
+
|
|
221
|
+
## Notes
|
|
222
|
+
- This skill was auto-generated by VORACODE's self-improvement engine
|
|
223
|
+
- You can edit or delete it: voracode skill remove ${skillName}
|
|
224
|
+
- Rollback: voracode skill history ${skillName}
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
writeFileSync(skillPath, content, "utf-8");
|
|
228
|
+
|
|
229
|
+
// Save to history
|
|
230
|
+
this.db.run(
|
|
231
|
+
"INSERT INTO skill_history (skill_name, version, action, content) VALUES (?, ?, ?, ?)",
|
|
232
|
+
[skillName, 1, "created", content],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Update pattern with skill path
|
|
236
|
+
this.db.run(
|
|
237
|
+
"UPDATE task_patterns SET skill_path = ?, user_decision = 'accepted' WHERE signature = ?",
|
|
238
|
+
[skillPath, pattern.signature],
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return skillName;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* User rejects a suggested skill
|
|
246
|
+
*/
|
|
247
|
+
rejectPattern(signature: string, permanently: boolean = false): void {
|
|
248
|
+
this.db.run(
|
|
249
|
+
"UPDATE task_patterns SET user_decision = ? WHERE signature = ?",
|
|
250
|
+
[permanently ? "never" : "rejected", signature],
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Record a user preference
|
|
256
|
+
*/
|
|
257
|
+
recordPreference(key: string, value: string, category = "general"): void {
|
|
258
|
+
this.db.run(
|
|
259
|
+
`INSERT OR REPLACE INTO user_preferences
|
|
260
|
+
(key, value, category, updated_at) VALUES (?, ?, ?, datetime('now'))`,
|
|
261
|
+
[key, value, category],
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get all learned patterns
|
|
267
|
+
*/
|
|
268
|
+
getPatterns(): TaskPattern[] {
|
|
269
|
+
const rows = this.db.query("SELECT * FROM task_patterns ORDER BY last_seen DESC").all() as Record<string, unknown>[];
|
|
270
|
+
return rows.map((r) => ({
|
|
271
|
+
id: r.id as number,
|
|
272
|
+
signature: r.signature as string,
|
|
273
|
+
description: r.description as string,
|
|
274
|
+
toolSequence: JSON.parse(r.tool_sequence as string),
|
|
275
|
+
filesChanged: JSON.parse((r.files_changed as string) || "[]"),
|
|
276
|
+
commandsRun: JSON.parse((r.commands_run as string) || "[]"),
|
|
277
|
+
successCount: (r.success_count as number) || 0,
|
|
278
|
+
failureCount: (r.failure_count as number) || 0,
|
|
279
|
+
lastSeen: (r.last_seen as string) || new Date().toISOString(),
|
|
280
|
+
suggestedSkill: Boolean(r.suggested_skill),
|
|
281
|
+
userDecision: (r.user_decision as "pending" | "accepted" | "rejected" | "never") || "pending",
|
|
282
|
+
skillPath: r.skill_path as string | undefined,
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get user preferences
|
|
288
|
+
*/
|
|
289
|
+
getPreferences(): Record<string, string> {
|
|
290
|
+
const rows = this.db.query("SELECT key, value FROM user_preferences").all() as Record<string, unknown>[];
|
|
291
|
+
const prefs: Record<string, string> = {};
|
|
292
|
+
for (const r of rows) {
|
|
293
|
+
prefs[r.key as string] = r.value as string;
|
|
294
|
+
}
|
|
295
|
+
return prefs;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Rollback a skill to a previous version
|
|
300
|
+
*/
|
|
301
|
+
rollbackSkill(skillName: string): boolean {
|
|
302
|
+
const history = this.db.query(
|
|
303
|
+
"SELECT * FROM skill_history WHERE skill_name = ? ORDER BY created_at DESC LIMIT 2",
|
|
304
|
+
).all(skillName) as Record<string, unknown>[];
|
|
305
|
+
|
|
306
|
+
if (history.length < 2) return false;
|
|
307
|
+
|
|
308
|
+
const previousVersion = history[1];
|
|
309
|
+
const skillDir = join(this.skillsDir, skillName);
|
|
310
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
311
|
+
|
|
312
|
+
if (existsSync(skillPath)) {
|
|
313
|
+
writeFileSync(skillPath, previousVersion.content as string, "utf-8");
|
|
314
|
+
this.db.run(
|
|
315
|
+
"INSERT INTO skill_history (skill_name, version, action, content) VALUES (?, ?, ?, ?)",
|
|
316
|
+
[skillName, (previousVersion.version as number) + 1, "rollback", previousVersion.content as string],
|
|
317
|
+
);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get storage statistics
|
|
325
|
+
*/
|
|
326
|
+
getStats(): { patterns: number; preferences: number; skills: number; storageKB: number } {
|
|
327
|
+
const patterns = (this.db.query("SELECT COUNT(*) as c FROM task_patterns").get() as { c: number }).c;
|
|
328
|
+
const preferences = (this.db.query("SELECT COUNT(*) as c FROM user_preferences").get() as { c: number }).c;
|
|
329
|
+
const skills = (this.db.query("SELECT COUNT(*) as c FROM task_patterns WHERE skill_path IS NOT NULL").get() as { c: number }).c;
|
|
330
|
+
|
|
331
|
+
// Estimate storage: ~2KB per pattern, ~100B per preference
|
|
332
|
+
const storageKB = Math.round((patterns * 2 + preferences * 0.1) * 10) / 10;
|
|
333
|
+
|
|
334
|
+
return { patterns, preferences, skills, storageKB };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VORACODE Config — Configuration loader and manager
|
|
3
|
+
*
|
|
4
|
+
* Config loaded from (in priority order):
|
|
5
|
+
* 1. CLI flags (highest)
|
|
6
|
+
* 2. .voracode/config.json (project-level)
|
|
7
|
+
* 3. ~/.config/voracode/config.json (global, lowest)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
11
|
+
import { join, resolve } from "path";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
|
|
14
|
+
export interface VoraConfig {
|
|
15
|
+
version: string;
|
|
16
|
+
project?: {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
};
|
|
20
|
+
model: {
|
|
21
|
+
provider: string;
|
|
22
|
+
name: string;
|
|
23
|
+
fallback: string[];
|
|
24
|
+
};
|
|
25
|
+
context: {
|
|
26
|
+
maxTokens: number;
|
|
27
|
+
compression: "auto" | "off" | "aggressive";
|
|
28
|
+
smartLoading: boolean;
|
|
29
|
+
cacheEnabled: boolean;
|
|
30
|
+
};
|
|
31
|
+
security: {
|
|
32
|
+
sandbox: {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
timeout: number;
|
|
35
|
+
maxOutput: number;
|
|
36
|
+
};
|
|
37
|
+
network: {
|
|
38
|
+
blocked: boolean;
|
|
39
|
+
allowedDomains: string[];
|
|
40
|
+
};
|
|
41
|
+
rateLimit: {
|
|
42
|
+
callsPerMinute: number;
|
|
43
|
+
tokensPerHour: number;
|
|
44
|
+
};
|
|
45
|
+
auditLog: {
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
retentionDays: number;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
skills: {
|
|
51
|
+
dirs: string[];
|
|
52
|
+
autoLearn: boolean;
|
|
53
|
+
};
|
|
54
|
+
plugins: string[];
|
|
55
|
+
telemetry: {
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
localOnly: boolean;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const DEFAULT_CONFIG: VoraConfig = {
|
|
62
|
+
version: "1.0",
|
|
63
|
+
model: {
|
|
64
|
+
provider: "auto",
|
|
65
|
+
name: "auto",
|
|
66
|
+
fallback: ["deepseek", "groq"],
|
|
67
|
+
},
|
|
68
|
+
context: {
|
|
69
|
+
maxTokens: 128_000,
|
|
70
|
+
compression: "auto",
|
|
71
|
+
smartLoading: true,
|
|
72
|
+
cacheEnabled: true,
|
|
73
|
+
},
|
|
74
|
+
security: {
|
|
75
|
+
sandbox: {
|
|
76
|
+
enabled: true,
|
|
77
|
+
timeout: 30_000,
|
|
78
|
+
maxOutput: 1_048_576,
|
|
79
|
+
},
|
|
80
|
+
network: {
|
|
81
|
+
blocked: false,
|
|
82
|
+
allowedDomains: [
|
|
83
|
+
"api.openai.com",
|
|
84
|
+
"api.anthropic.com",
|
|
85
|
+
"api.deepseek.com",
|
|
86
|
+
"api.groq.com",
|
|
87
|
+
"api.openrouter.ai",
|
|
88
|
+
"api.github.com",
|
|
89
|
+
"api.huggingface.co",
|
|
90
|
+
"router.huggingface.co",
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
rateLimit: {
|
|
94
|
+
callsPerMinute: 10,
|
|
95
|
+
tokensPerHour: 1_000_000,
|
|
96
|
+
},
|
|
97
|
+
auditLog: {
|
|
98
|
+
enabled: true,
|
|
99
|
+
retentionDays: 30,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
skills: {
|
|
103
|
+
dirs: [
|
|
104
|
+
"~/.config/voracode/skills",
|
|
105
|
+
".voracode/skills",
|
|
106
|
+
],
|
|
107
|
+
autoLearn: false,
|
|
108
|
+
},
|
|
109
|
+
plugins: [],
|
|
110
|
+
telemetry: {
|
|
111
|
+
enabled: false,
|
|
112
|
+
localOnly: true,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export class ConfigManager {
|
|
117
|
+
private config: VoraConfig;
|
|
118
|
+
private globalPath: string;
|
|
119
|
+
|
|
120
|
+
constructor() {
|
|
121
|
+
this.globalPath = join(homedir(), ".config", "voracode", "config.json");
|
|
122
|
+
this.config = this.load();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private load(): VoraConfig {
|
|
126
|
+
// Start with defaults
|
|
127
|
+
const base = { ...DEFAULT_CONFIG };
|
|
128
|
+
|
|
129
|
+
// Load global config
|
|
130
|
+
const globalPath = join(homedir(), ".config", "voracode", "config.json");
|
|
131
|
+
if (existsSync(globalPath)) {
|
|
132
|
+
try {
|
|
133
|
+
const global = JSON.parse(readFileSync(globalPath, "utf-8"));
|
|
134
|
+
Object.assign(base, global);
|
|
135
|
+
} catch {
|
|
136
|
+
// Ignore invalid global config
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Load project config (cwd + .voracode)
|
|
141
|
+
const projectPath = join(process.cwd(), ".voracode", "config.json");
|
|
142
|
+
if (existsSync(projectPath)) {
|
|
143
|
+
try {
|
|
144
|
+
const project = JSON.parse(readFileSync(projectPath, "utf-8"));
|
|
145
|
+
Object.assign(base, project);
|
|
146
|
+
} catch {
|
|
147
|
+
// Ignore invalid project config
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return base;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get(): VoraConfig {
|
|
155
|
+
return { ...this.config };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getGlobalPath(): string {
|
|
159
|
+
return this.globalPath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ensureDefaults(): void {
|
|
163
|
+
const dir = join(homedir(), ".config", "voracode");
|
|
164
|
+
if (!existsSync(dir)) {
|
|
165
|
+
mkdirSync(dir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
if (!existsSync(this.globalPath)) {
|
|
168
|
+
writeFileSync(this.globalPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
set(key: string, value: unknown): void {
|
|
173
|
+
// Simple dot-path setter (e.g., "model.provider" = "deepseek")
|
|
174
|
+
const keys = key.split(".");
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
let target: any = this.config;
|
|
177
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
178
|
+
if (!(keys[i] in target)) target[keys[i]] = {};
|
|
179
|
+
target = target[keys[i]];
|
|
180
|
+
}
|
|
181
|
+
target[keys[keys.length - 1]] = value;
|
|
182
|
+
|
|
183
|
+
// Persist
|
|
184
|
+
writeFileSync(this.globalPath, JSON.stringify(this.config, null, 2));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getProviderBaseUrl(provider: string): string {
|
|
188
|
+
const urls: Record<string, string> = {
|
|
189
|
+
openai: "https://api.openai.com/v1",
|
|
190
|
+
deepseek: "https://api.deepseek.com/v1",
|
|
191
|
+
groq: "https://api.groq.com/openai/v1",
|
|
192
|
+
together: "https://api.together.xyz/v1",
|
|
193
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
194
|
+
ollama: "http://localhost:11434/v1",
|
|
195
|
+
huggingface: "https://router.huggingface.co/v1",
|
|
196
|
+
fireworks: "https://api.fireworks.ai/inference/v1",
|
|
197
|
+
cerebras: "https://api.cerebras.ai/v1",
|
|
198
|
+
sambanova: "https://api.sambanova.ai/v1",
|
|
199
|
+
cloudflare: "https://api.cloudflare.com/client/v4/ai",
|
|
200
|
+
};
|
|
201
|
+
return urls[provider] || "";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
detectProvider(modelName: string): string {
|
|
205
|
+
if (modelName.startsWith("gpt-") || modelName.startsWith("o3") || modelName.startsWith("o1")) return "openai";
|
|
206
|
+
if (modelName.startsWith("claude-") || modelName.startsWith("opus") || modelName.startsWith("sonnet")) return "anthropic";
|
|
207
|
+
if (modelName.startsWith("gemini-")) return "google";
|
|
208
|
+
if (modelName.startsWith("deepseek-")) return "deepseek";
|
|
209
|
+
if (modelName.startsWith("llama-") || modelName.startsWith("mixtral") || modelName.startsWith("qwen")) return "groq";
|
|
210
|
+
if (modelName.startsWith("command-")) return "cohere";
|
|
211
|
+
return "openai"; // default
|
|
212
|
+
}
|
|
213
|
+
}
|