apteva 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +63 -0
- package/README.md +84 -0
- package/bin/agent-linux-amd64 +0 -0
- package/bin/apteva.js +144 -0
- package/dist/App.g02zmbqf.js +213 -0
- package/dist/App.g02zmbqf.js.map +37 -0
- package/dist/App.mq6jqare.js +1 -0
- package/dist/apteva-kit.css +1 -0
- package/dist/index.html +14 -0
- package/dist/styles.css +1 -0
- package/package.json +65 -0
- package/src/binary.ts +116 -0
- package/src/crypto.ts +152 -0
- package/src/db.ts +446 -0
- package/src/providers.ts +255 -0
- package/src/routes/api.ts +380 -0
- package/src/routes/static.ts +47 -0
- package/src/server.ts +134 -0
- package/src/web/App.tsx +218 -0
- package/src/web/components/agents/AgentCard.tsx +71 -0
- package/src/web/components/agents/AgentsView.tsx +69 -0
- package/src/web/components/agents/ChatPanel.tsx +63 -0
- package/src/web/components/agents/CreateAgentModal.tsx +128 -0
- package/src/web/components/agents/index.ts +4 -0
- package/src/web/components/common/Icons.tsx +61 -0
- package/src/web/components/common/LoadingSpinner.tsx +44 -0
- package/src/web/components/common/Modal.tsx +16 -0
- package/src/web/components/common/Select.tsx +96 -0
- package/src/web/components/common/index.ts +4 -0
- package/src/web/components/dashboard/Dashboard.tsx +136 -0
- package/src/web/components/dashboard/index.ts +1 -0
- package/src/web/components/index.ts +11 -0
- package/src/web/components/layout/ErrorBanner.tsx +18 -0
- package/src/web/components/layout/Header.tsx +26 -0
- package/src/web/components/layout/Sidebar.tsx +66 -0
- package/src/web/components/layout/index.ts +3 -0
- package/src/web/components/onboarding/OnboardingWizard.tsx +344 -0
- package/src/web/components/onboarding/index.ts +1 -0
- package/src/web/components/settings/SettingsPage.tsx +285 -0
- package/src/web/components/settings/index.ts +1 -0
- package/src/web/hooks/index.ts +3 -0
- package/src/web/hooks/useAgents.ts +62 -0
- package/src/web/hooks/useOnboarding.ts +25 -0
- package/src/web/hooks/useProviders.ts +65 -0
- package/src/web/index.html +21 -0
- package/src/web/styles.css +23 -0
- package/src/web/types.ts +43 -0
package/src/db.ts
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { mkdirSync, existsSync } from "fs";
|
|
4
|
+
|
|
5
|
+
// Types
|
|
6
|
+
export interface Agent {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
model: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
system_prompt: string;
|
|
12
|
+
status: "stopped" | "running";
|
|
13
|
+
port: number | null;
|
|
14
|
+
created_at: string;
|
|
15
|
+
updated_at: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AgentRow {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
model: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
system_prompt: string;
|
|
24
|
+
status: string;
|
|
25
|
+
port: number | null;
|
|
26
|
+
created_at: string;
|
|
27
|
+
updated_at: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Settings {
|
|
31
|
+
key: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ProviderKey {
|
|
36
|
+
id: string;
|
|
37
|
+
provider_id: string;
|
|
38
|
+
encrypted_key: string;
|
|
39
|
+
key_hint: string;
|
|
40
|
+
is_valid: boolean;
|
|
41
|
+
last_tested_at: string | null;
|
|
42
|
+
created_at: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ProviderKeyRow {
|
|
46
|
+
id: string;
|
|
47
|
+
provider_id: string;
|
|
48
|
+
encrypted_key: string;
|
|
49
|
+
key_hint: string;
|
|
50
|
+
is_valid: number;
|
|
51
|
+
last_tested_at: string | null;
|
|
52
|
+
created_at: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Database instance
|
|
56
|
+
let db: Database;
|
|
57
|
+
|
|
58
|
+
// Initialize database
|
|
59
|
+
export function initDatabase(dataDir: string): Database {
|
|
60
|
+
// Ensure data directory exists
|
|
61
|
+
if (!existsSync(dataDir)) {
|
|
62
|
+
mkdirSync(dataDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dbPath = join(dataDir, "apteva.db");
|
|
66
|
+
db = new Database(dbPath);
|
|
67
|
+
|
|
68
|
+
// Enable WAL mode for better concurrent access
|
|
69
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
70
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
71
|
+
|
|
72
|
+
// Run migrations
|
|
73
|
+
runMigrations();
|
|
74
|
+
|
|
75
|
+
// Database initialized silently
|
|
76
|
+
return db;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get database instance
|
|
80
|
+
export function getDb(): Database {
|
|
81
|
+
if (!db) {
|
|
82
|
+
throw new Error("Database not initialized. Call initDatabase() first.");
|
|
83
|
+
}
|
|
84
|
+
return db;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Migrations
|
|
88
|
+
function runMigrations() {
|
|
89
|
+
// Create migrations table if not exists
|
|
90
|
+
db.run(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
92
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
name TEXT NOT NULL UNIQUE,
|
|
94
|
+
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
95
|
+
)
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
const migrations: { name: string; sql: string }[] = [
|
|
99
|
+
{
|
|
100
|
+
name: "001_create_agents",
|
|
101
|
+
sql: `
|
|
102
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
103
|
+
id TEXT PRIMARY KEY,
|
|
104
|
+
name TEXT NOT NULL,
|
|
105
|
+
model TEXT NOT NULL,
|
|
106
|
+
provider TEXT NOT NULL,
|
|
107
|
+
system_prompt TEXT NOT NULL DEFAULT 'You are a helpful assistant.',
|
|
108
|
+
status TEXT NOT NULL DEFAULT 'stopped',
|
|
109
|
+
port INTEGER,
|
|
110
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
111
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
112
|
+
);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_agents_provider ON agents(provider);
|
|
115
|
+
`,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "002_create_settings",
|
|
119
|
+
sql: `
|
|
120
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
121
|
+
key TEXT PRIMARY KEY,
|
|
122
|
+
value TEXT NOT NULL,
|
|
123
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
124
|
+
);
|
|
125
|
+
`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "003_create_threads",
|
|
129
|
+
sql: `
|
|
130
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
131
|
+
id TEXT PRIMARY KEY,
|
|
132
|
+
agent_id TEXT NOT NULL,
|
|
133
|
+
title TEXT,
|
|
134
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
135
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
136
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
|
137
|
+
);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_threads_agent ON threads(agent_id);
|
|
139
|
+
`,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "004_create_messages",
|
|
143
|
+
sql: `
|
|
144
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
145
|
+
id TEXT PRIMARY KEY,
|
|
146
|
+
thread_id TEXT NOT NULL,
|
|
147
|
+
role TEXT NOT NULL,
|
|
148
|
+
content TEXT NOT NULL,
|
|
149
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
150
|
+
FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
|
|
151
|
+
);
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id);
|
|
153
|
+
`,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "005_create_provider_keys",
|
|
157
|
+
sql: `
|
|
158
|
+
CREATE TABLE IF NOT EXISTS provider_keys (
|
|
159
|
+
id TEXT PRIMARY KEY,
|
|
160
|
+
provider_id TEXT NOT NULL UNIQUE,
|
|
161
|
+
encrypted_key TEXT NOT NULL,
|
|
162
|
+
key_hint TEXT,
|
|
163
|
+
is_valid INTEGER DEFAULT 1,
|
|
164
|
+
last_tested_at TEXT,
|
|
165
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
166
|
+
);
|
|
167
|
+
CREATE INDEX IF NOT EXISTS idx_provider_keys_provider ON provider_keys(provider_id);
|
|
168
|
+
`,
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
// Check which migrations have been applied
|
|
173
|
+
const applied = new Set<string>();
|
|
174
|
+
const rows = db.query("SELECT name FROM migrations").all() as { name: string }[];
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
applied.add(row.name);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Run pending migrations
|
|
180
|
+
for (const migration of migrations) {
|
|
181
|
+
if (!applied.has(migration.name)) {
|
|
182
|
+
// Migration runs silently
|
|
183
|
+
db.run(migration.sql);
|
|
184
|
+
db.run("INSERT INTO migrations (name) VALUES (?)", [migration.name]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Agent CRUD operations
|
|
190
|
+
export const AgentDB = {
|
|
191
|
+
// Create a new agent
|
|
192
|
+
create(agent: Omit<Agent, "created_at" | "updated_at" | "status" | "port">): Agent {
|
|
193
|
+
const now = new Date().toISOString();
|
|
194
|
+
const stmt = db.prepare(`
|
|
195
|
+
INSERT INTO agents (id, name, model, provider, system_prompt, status, port, created_at, updated_at)
|
|
196
|
+
VALUES (?, ?, ?, ?, ?, 'stopped', NULL, ?, ?)
|
|
197
|
+
`);
|
|
198
|
+
stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, now, now);
|
|
199
|
+
return this.findById(agent.id)!;
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// Find agent by ID
|
|
203
|
+
findById(id: string): Agent | null {
|
|
204
|
+
const row = db.query("SELECT * FROM agents WHERE id = ?").get(id) as AgentRow | null;
|
|
205
|
+
return row ? rowToAgent(row) : null;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
// Get all agents
|
|
209
|
+
findAll(): Agent[] {
|
|
210
|
+
const rows = db.query("SELECT * FROM agents ORDER BY created_at DESC").all() as AgentRow[];
|
|
211
|
+
return rows.map(rowToAgent);
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// Update agent
|
|
215
|
+
update(id: string, updates: Partial<Omit<Agent, "id" | "created_at">>): Agent | null {
|
|
216
|
+
const agent = this.findById(id);
|
|
217
|
+
if (!agent) return null;
|
|
218
|
+
|
|
219
|
+
const fields: string[] = [];
|
|
220
|
+
const values: unknown[] = [];
|
|
221
|
+
|
|
222
|
+
if (updates.name !== undefined) {
|
|
223
|
+
fields.push("name = ?");
|
|
224
|
+
values.push(updates.name);
|
|
225
|
+
}
|
|
226
|
+
if (updates.model !== undefined) {
|
|
227
|
+
fields.push("model = ?");
|
|
228
|
+
values.push(updates.model);
|
|
229
|
+
}
|
|
230
|
+
if (updates.provider !== undefined) {
|
|
231
|
+
fields.push("provider = ?");
|
|
232
|
+
values.push(updates.provider);
|
|
233
|
+
}
|
|
234
|
+
if (updates.system_prompt !== undefined) {
|
|
235
|
+
fields.push("system_prompt = ?");
|
|
236
|
+
values.push(updates.system_prompt);
|
|
237
|
+
}
|
|
238
|
+
if (updates.status !== undefined) {
|
|
239
|
+
fields.push("status = ?");
|
|
240
|
+
values.push(updates.status);
|
|
241
|
+
}
|
|
242
|
+
if (updates.port !== undefined) {
|
|
243
|
+
fields.push("port = ?");
|
|
244
|
+
values.push(updates.port);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (fields.length > 0) {
|
|
248
|
+
fields.push("updated_at = ?");
|
|
249
|
+
values.push(new Date().toISOString());
|
|
250
|
+
values.push(id);
|
|
251
|
+
|
|
252
|
+
db.run(`UPDATE agents SET ${fields.join(", ")} WHERE id = ?`, values);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return this.findById(id);
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
// Delete agent
|
|
259
|
+
delete(id: string): boolean {
|
|
260
|
+
const result = db.run("DELETE FROM agents WHERE id = ?", [id]);
|
|
261
|
+
return result.changes > 0;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
// Set agent status
|
|
265
|
+
setStatus(id: string, status: "stopped" | "running", port?: number): Agent | null {
|
|
266
|
+
return this.update(id, { status, port: port ?? null });
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
// Reset all agents to stopped (on server restart)
|
|
270
|
+
resetAllStatus(): void {
|
|
271
|
+
db.run("UPDATE agents SET status = 'stopped', port = NULL");
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// Count agents
|
|
275
|
+
count(): number {
|
|
276
|
+
const row = db.query("SELECT COUNT(*) as count FROM agents").get() as { count: number };
|
|
277
|
+
return row.count;
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
// Count running agents
|
|
281
|
+
countRunning(): number {
|
|
282
|
+
const row = db.query("SELECT COUNT(*) as count FROM agents WHERE status = 'running'").get() as { count: number };
|
|
283
|
+
return row.count;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Thread CRUD operations
|
|
288
|
+
export const ThreadDB = {
|
|
289
|
+
create(id: string, agentId: string, title?: string): void {
|
|
290
|
+
const now = new Date().toISOString();
|
|
291
|
+
db.run(
|
|
292
|
+
"INSERT INTO threads (id, agent_id, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
293
|
+
[id, agentId, title || null, now, now]
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
findById(id: string) {
|
|
298
|
+
return db.query("SELECT * FROM threads WHERE id = ?").get(id);
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
findByAgent(agentId: string) {
|
|
302
|
+
return db.query("SELECT * FROM threads WHERE agent_id = ? ORDER BY updated_at DESC").all(agentId);
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
delete(id: string): boolean {
|
|
306
|
+
const result = db.run("DELETE FROM threads WHERE id = ?", [id]);
|
|
307
|
+
return result.changes > 0;
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Message CRUD operations
|
|
312
|
+
export const MessageDB = {
|
|
313
|
+
create(id: string, threadId: string, role: string, content: string): void {
|
|
314
|
+
db.run(
|
|
315
|
+
"INSERT INTO messages (id, thread_id, role, content) VALUES (?, ?, ?, ?)",
|
|
316
|
+
[id, threadId, role, content]
|
|
317
|
+
);
|
|
318
|
+
// Update thread's updated_at
|
|
319
|
+
db.run("UPDATE threads SET updated_at = CURRENT_TIMESTAMP WHERE id = ?", [threadId]);
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
findByThread(threadId: string) {
|
|
323
|
+
return db.query("SELECT * FROM messages WHERE thread_id = ? ORDER BY created_at ASC").all(threadId);
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Settings operations
|
|
328
|
+
export const SettingsDB = {
|
|
329
|
+
get(key: string): string | null {
|
|
330
|
+
const row = db.query("SELECT value FROM settings WHERE key = ?").get(key) as { value: string } | null;
|
|
331
|
+
return row?.value ?? null;
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
set(key: string, value: string): void {
|
|
335
|
+
db.run(
|
|
336
|
+
"INSERT INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP",
|
|
337
|
+
[key, value, value]
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
delete(key: string): boolean {
|
|
342
|
+
const result = db.run("DELETE FROM settings WHERE key = ?", [key]);
|
|
343
|
+
return result.changes > 0;
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Helper to convert DB row to Agent type
|
|
348
|
+
function rowToAgent(row: AgentRow): Agent {
|
|
349
|
+
return {
|
|
350
|
+
id: row.id,
|
|
351
|
+
name: row.name,
|
|
352
|
+
model: row.model,
|
|
353
|
+
provider: row.provider,
|
|
354
|
+
system_prompt: row.system_prompt,
|
|
355
|
+
status: row.status as "stopped" | "running",
|
|
356
|
+
port: row.port,
|
|
357
|
+
created_at: row.created_at,
|
|
358
|
+
updated_at: row.updated_at,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Provider Keys operations
|
|
363
|
+
export const ProviderKeysDB = {
|
|
364
|
+
// Save or update a provider key
|
|
365
|
+
save(providerId: string, encryptedKey: string, keyHint: string): ProviderKey {
|
|
366
|
+
const existing = this.findByProvider(providerId);
|
|
367
|
+
const now = new Date().toISOString();
|
|
368
|
+
|
|
369
|
+
if (existing) {
|
|
370
|
+
db.run(
|
|
371
|
+
"UPDATE provider_keys SET encrypted_key = ?, key_hint = ?, is_valid = 1, last_tested_at = NULL, created_at = ? WHERE provider_id = ?",
|
|
372
|
+
[encryptedKey, keyHint, now, providerId]
|
|
373
|
+
);
|
|
374
|
+
} else {
|
|
375
|
+
const id = generateId();
|
|
376
|
+
db.run(
|
|
377
|
+
"INSERT INTO provider_keys (id, provider_id, encrypted_key, key_hint, is_valid, created_at) VALUES (?, ?, ?, ?, 1, ?)",
|
|
378
|
+
[id, providerId, encryptedKey, keyHint, now]
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return this.findByProvider(providerId)!;
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
// Find key by provider
|
|
386
|
+
findByProvider(providerId: string): ProviderKey | null {
|
|
387
|
+
const row = db.query("SELECT * FROM provider_keys WHERE provider_id = ?").get(providerId) as ProviderKeyRow | null;
|
|
388
|
+
return row ? rowToProviderKey(row) : null;
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
// Get all provider keys (without the actual encrypted key for listing)
|
|
392
|
+
findAll(): ProviderKey[] {
|
|
393
|
+
const rows = db.query("SELECT * FROM provider_keys ORDER BY created_at DESC").all() as ProviderKeyRow[];
|
|
394
|
+
return rows.map(rowToProviderKey);
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
// Get list of provider IDs that have keys configured
|
|
398
|
+
getConfiguredProviders(): string[] {
|
|
399
|
+
const rows = db.query("SELECT provider_id FROM provider_keys").all() as { provider_id: string }[];
|
|
400
|
+
return rows.map(r => r.provider_id);
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
// Update validity status after testing
|
|
404
|
+
setValidity(providerId: string, isValid: boolean): void {
|
|
405
|
+
db.run(
|
|
406
|
+
"UPDATE provider_keys SET is_valid = ?, last_tested_at = ? WHERE provider_id = ?",
|
|
407
|
+
[isValid ? 1 : 0, new Date().toISOString(), providerId]
|
|
408
|
+
);
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
// Delete a provider key
|
|
412
|
+
delete(providerId: string): boolean {
|
|
413
|
+
const result = db.run("DELETE FROM provider_keys WHERE provider_id = ?", [providerId]);
|
|
414
|
+
return result.changes > 0;
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
// Check if any keys are configured
|
|
418
|
+
hasAnyKeys(): boolean {
|
|
419
|
+
const row = db.query("SELECT COUNT(*) as count FROM provider_keys").get() as { count: number };
|
|
420
|
+
return row.count > 0;
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
// Count configured providers
|
|
424
|
+
count(): number {
|
|
425
|
+
const row = db.query("SELECT COUNT(*) as count FROM provider_keys").get() as { count: number };
|
|
426
|
+
return row.count;
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Helper to convert DB row to ProviderKey type
|
|
431
|
+
function rowToProviderKey(row: ProviderKeyRow): ProviderKey {
|
|
432
|
+
return {
|
|
433
|
+
id: row.id,
|
|
434
|
+
provider_id: row.provider_id,
|
|
435
|
+
encrypted_key: row.encrypted_key,
|
|
436
|
+
key_hint: row.key_hint,
|
|
437
|
+
is_valid: row.is_valid === 1,
|
|
438
|
+
last_tested_at: row.last_tested_at,
|
|
439
|
+
created_at: row.created_at,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Generate unique ID
|
|
444
|
+
export function generateId(): string {
|
|
445
|
+
return Math.random().toString(36).substring(2, 15);
|
|
446
|
+
}
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { ProviderKeysDB, SettingsDB } from "./db";
|
|
2
|
+
import { encrypt, decrypt, createKeyHint, validateKeyFormat } from "./crypto";
|
|
3
|
+
|
|
4
|
+
// Provider configuration with API URLs and key testing endpoints
|
|
5
|
+
export const PROVIDERS = {
|
|
6
|
+
anthropic: {
|
|
7
|
+
id: "anthropic",
|
|
8
|
+
name: "Anthropic",
|
|
9
|
+
displayName: "Anthropic (Claude)",
|
|
10
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
11
|
+
docsUrl: "https://console.anthropic.com/settings/keys",
|
|
12
|
+
testEndpoint: "https://api.anthropic.com/v1/messages",
|
|
13
|
+
models: [
|
|
14
|
+
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4", recommended: true },
|
|
15
|
+
{ value: "claude-opus-4-20250514", label: "Claude Opus 4" },
|
|
16
|
+
{ value: "claude-4-5-sonnet", label: "Claude 4.5 Sonnet" },
|
|
17
|
+
{ value: "claude-4-5-haiku", label: "Claude 4.5 Haiku (Fast)" },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
openai: {
|
|
21
|
+
id: "openai",
|
|
22
|
+
name: "OpenAI",
|
|
23
|
+
displayName: "OpenAI (GPT)",
|
|
24
|
+
envVar: "OPENAI_API_KEY",
|
|
25
|
+
docsUrl: "https://platform.openai.com/api-keys",
|
|
26
|
+
testEndpoint: "https://api.openai.com/v1/models",
|
|
27
|
+
models: [
|
|
28
|
+
{ value: "gpt-4o", label: "GPT-4o", recommended: true },
|
|
29
|
+
{ value: "gpt-4o-mini", label: "GPT-4o Mini (Fast)" },
|
|
30
|
+
{ value: "gpt-4-turbo", label: "GPT-4 Turbo" },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
groq: {
|
|
34
|
+
id: "groq",
|
|
35
|
+
name: "Groq",
|
|
36
|
+
displayName: "Groq (Ultra-fast)",
|
|
37
|
+
envVar: "GROQ_API_KEY",
|
|
38
|
+
docsUrl: "https://console.groq.com/keys",
|
|
39
|
+
testEndpoint: "https://api.groq.com/openai/v1/models",
|
|
40
|
+
models: [
|
|
41
|
+
{ value: "llama-3.3-70b-versatile", label: "Llama 3.3 70B", recommended: true },
|
|
42
|
+
{ value: "llama-3.1-8b-instant", label: "Llama 3.1 8B (Instant)" },
|
|
43
|
+
{ value: "mixtral-8x7b-32768", label: "Mixtral 8x7B" },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
gemini: {
|
|
47
|
+
id: "gemini",
|
|
48
|
+
name: "Google",
|
|
49
|
+
displayName: "Google (Gemini)",
|
|
50
|
+
envVar: "GEMINI_API_KEY",
|
|
51
|
+
docsUrl: "https://aistudio.google.com/app/apikey",
|
|
52
|
+
testEndpoint: "https://generativelanguage.googleapis.com/v1/models",
|
|
53
|
+
models: [
|
|
54
|
+
{ value: "gemini-2.0-flash", label: "Gemini 2.0 Flash", recommended: true },
|
|
55
|
+
{ value: "gemini-1.5-pro", label: "Gemini 1.5 Pro" },
|
|
56
|
+
{ value: "gemini-1.5-flash", label: "Gemini 1.5 Flash" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
fireworks: {
|
|
60
|
+
id: "fireworks",
|
|
61
|
+
name: "Fireworks",
|
|
62
|
+
displayName: "Fireworks AI",
|
|
63
|
+
envVar: "FIREWORKS_API_KEY",
|
|
64
|
+
docsUrl: "https://fireworks.ai/api-keys",
|
|
65
|
+
testEndpoint: "https://api.fireworks.ai/inference/v1/models",
|
|
66
|
+
models: [
|
|
67
|
+
{ value: "accounts/fireworks/models/llama-v3p3-70b-instruct", label: "Llama 3.3 70B", recommended: true },
|
|
68
|
+
{ value: "accounts/fireworks/models/deepseek-v3", label: "DeepSeek V3" },
|
|
69
|
+
{ value: "accounts/fireworks/models/qwen2p5-72b-instruct", label: "Qwen 2.5 72B" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
xai: {
|
|
73
|
+
id: "xai",
|
|
74
|
+
name: "xAI",
|
|
75
|
+
displayName: "xAI (Grok)",
|
|
76
|
+
envVar: "XAI_API_KEY",
|
|
77
|
+
docsUrl: "https://console.x.ai/",
|
|
78
|
+
testEndpoint: "https://api.x.ai/v1/models",
|
|
79
|
+
models: [
|
|
80
|
+
{ value: "grok-2-latest", label: "Grok 2", recommended: true },
|
|
81
|
+
{ value: "grok-beta", label: "Grok Beta" },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
moonshot: {
|
|
85
|
+
id: "moonshot",
|
|
86
|
+
name: "Moonshot",
|
|
87
|
+
displayName: "Moonshot AI (Kimi)",
|
|
88
|
+
envVar: "MOONSHOT_API_KEY",
|
|
89
|
+
docsUrl: "https://platform.moonshot.cn/console/api-keys",
|
|
90
|
+
testEndpoint: "https://api.moonshot.cn/v1/models",
|
|
91
|
+
models: [
|
|
92
|
+
{ value: "moonshot-v1-128k", label: "Moonshot V1 128K", recommended: true },
|
|
93
|
+
{ value: "moonshot-v1-32k", label: "Moonshot V1 32K" },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
together: {
|
|
97
|
+
id: "together",
|
|
98
|
+
name: "Together",
|
|
99
|
+
displayName: "Together AI",
|
|
100
|
+
envVar: "TOGETHER_API_KEY",
|
|
101
|
+
docsUrl: "https://api.together.xyz/settings/api-keys",
|
|
102
|
+
testEndpoint: "https://api.together.xyz/v1/models",
|
|
103
|
+
models: [
|
|
104
|
+
{ value: "meta-llama/Llama-3.3-70B-Instruct-Turbo", label: "Llama 3.3 70B", recommended: true },
|
|
105
|
+
{ value: "deepseek-ai/DeepSeek-R1", label: "DeepSeek R1" },
|
|
106
|
+
{ value: "deepseek-ai/DeepSeek-V3", label: "DeepSeek V3" },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
venice: {
|
|
110
|
+
id: "venice",
|
|
111
|
+
name: "Venice",
|
|
112
|
+
displayName: "Venice AI",
|
|
113
|
+
envVar: "VENICE_API_KEY",
|
|
114
|
+
docsUrl: "https://venice.ai/settings/api",
|
|
115
|
+
testEndpoint: "https://api.venice.ai/api/v1/models",
|
|
116
|
+
models: [
|
|
117
|
+
{ value: "llama-3.3-70b", label: "Llama 3.3 70B", recommended: true },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
} as const;
|
|
121
|
+
|
|
122
|
+
export type ProviderId = keyof typeof PROVIDERS;
|
|
123
|
+
|
|
124
|
+
// Provider Keys Management
|
|
125
|
+
export const ProviderKeys = {
|
|
126
|
+
// Save an API key (encrypts before storing)
|
|
127
|
+
async save(providerId: string, apiKey: string): Promise<{ success: boolean; error?: string }> {
|
|
128
|
+
// Validate format
|
|
129
|
+
const validation = validateKeyFormat(providerId, apiKey);
|
|
130
|
+
if (!validation.valid) {
|
|
131
|
+
return { success: false, error: validation.error };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const encryptedKey = encrypt(apiKey.trim());
|
|
136
|
+
const keyHint = createKeyHint(apiKey.trim());
|
|
137
|
+
ProviderKeysDB.save(providerId, encryptedKey, keyHint);
|
|
138
|
+
return { success: true };
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return { success: false, error: `Failed to save key: ${err}` };
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// Get decrypted API key for a provider
|
|
145
|
+
getDecrypted(providerId: string): string | null {
|
|
146
|
+
const record = ProviderKeysDB.findByProvider(providerId);
|
|
147
|
+
if (!record) return null;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
return decrypt(record.encrypted_key);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error(`Failed to decrypt key for ${providerId}:`, err);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// Check if a provider has a key configured
|
|
158
|
+
hasKey(providerId: string): boolean {
|
|
159
|
+
return ProviderKeysDB.findByProvider(providerId) !== null;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
// Get all configured providers with their status (without exposing keys)
|
|
163
|
+
getAll(): Array<{
|
|
164
|
+
provider_id: string;
|
|
165
|
+
key_hint: string;
|
|
166
|
+
is_valid: boolean;
|
|
167
|
+
last_tested_at: string | null;
|
|
168
|
+
created_at: string;
|
|
169
|
+
}> {
|
|
170
|
+
return ProviderKeysDB.findAll().map(k => ({
|
|
171
|
+
provider_id: k.provider_id,
|
|
172
|
+
key_hint: k.key_hint,
|
|
173
|
+
is_valid: k.is_valid,
|
|
174
|
+
last_tested_at: k.last_tested_at,
|
|
175
|
+
created_at: k.created_at,
|
|
176
|
+
}));
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Delete a provider key
|
|
180
|
+
delete(providerId: string): boolean {
|
|
181
|
+
return ProviderKeysDB.delete(providerId);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// Test if an API key is valid by making a test request
|
|
185
|
+
// TODO: Implement actual API testing per provider (Anthropic needs POST, others GET)
|
|
186
|
+
async test(providerId: string, apiKey?: string): Promise<{ valid: boolean; error?: string }> {
|
|
187
|
+
const key = apiKey || this.getDecrypted(providerId);
|
|
188
|
+
if (!key) {
|
|
189
|
+
return { valid: false, error: "No API key available" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const provider = PROVIDERS[providerId as ProviderId];
|
|
193
|
+
if (!provider) {
|
|
194
|
+
return { valid: false, error: "Unknown provider" };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// For now, just validate format - actual API testing to be implemented later
|
|
198
|
+
const validation = validateKeyFormat(providerId, key);
|
|
199
|
+
if (!validation.valid) {
|
|
200
|
+
return { valid: false, error: validation.error };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { valid: true };
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// Get list of provider IDs that have valid keys
|
|
207
|
+
getConfiguredProviders(): string[] {
|
|
208
|
+
return ProviderKeysDB.getConfiguredProviders();
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Onboarding status management
|
|
213
|
+
export const Onboarding = {
|
|
214
|
+
isComplete(): boolean {
|
|
215
|
+
return SettingsDB.get("onboarding_completed") === "true";
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
complete(): void {
|
|
219
|
+
SettingsDB.set("onboarding_completed", "true");
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
reset(): void {
|
|
223
|
+
SettingsDB.delete("onboarding_completed");
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
getStatus(): {
|
|
227
|
+
completed: boolean;
|
|
228
|
+
providers_configured: string[];
|
|
229
|
+
has_any_keys: boolean;
|
|
230
|
+
} {
|
|
231
|
+
return {
|
|
232
|
+
completed: this.isComplete(),
|
|
233
|
+
providers_configured: ProviderKeys.getConfiguredProviders(),
|
|
234
|
+
has_any_keys: ProviderKeysDB.hasAnyKeys(),
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Get provider list with configuration status for frontend
|
|
240
|
+
export function getProvidersWithStatus() {
|
|
241
|
+
const configuredProviders = new Set(ProviderKeys.getConfiguredProviders());
|
|
242
|
+
const keyStatuses = new Map(
|
|
243
|
+
ProviderKeys.getAll().map(k => [k.provider_id, k])
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return Object.values(PROVIDERS).map(provider => ({
|
|
247
|
+
id: provider.id,
|
|
248
|
+
name: provider.displayName,
|
|
249
|
+
docsUrl: provider.docsUrl,
|
|
250
|
+
models: provider.models,
|
|
251
|
+
hasKey: configuredProviders.has(provider.id),
|
|
252
|
+
keyHint: keyStatuses.get(provider.id)?.key_hint || null,
|
|
253
|
+
isValid: keyStatuses.get(provider.id)?.is_valid ?? null,
|
|
254
|
+
}));
|
|
255
|
+
}
|