apteva 0.4.57 → 0.7.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.
Files changed (142) hide show
  1. package/README.md +216 -54
  2. package/cli.js +35 -0
  3. package/install.js +92 -0
  4. package/package.json +12 -79
  5. package/LICENSE +0 -63
  6. package/bin/apteva.js +0 -196
  7. package/dist/ActivityPage.kxzzb4yc.js +0 -3
  8. package/dist/ApiDocsPage.zq998hbm.js +0 -4
  9. package/dist/App.55rea8mn.js +0 -61
  10. package/dist/App.5ywb23z4.js +0 -53
  11. package/dist/App.6thds120.js +0 -4
  12. package/dist/App.9tctxzqm.js +0 -8
  13. package/dist/App.a8r8ttaz.js +0 -4
  14. package/dist/App.agsv5bje.js +0 -4
  15. package/dist/App.cepapqmx.js +0 -4
  16. package/dist/App.dp041gb3.js +0 -221
  17. package/dist/App.fds72zb5.js +0 -4
  18. package/dist/App.fg9qj2dq.js +0 -4
  19. package/dist/App.ndfejbm9.js +0 -4
  20. package/dist/App.nxmfmq1h.js +0 -13
  21. package/dist/App.qdfyt8ba.js +0 -4
  22. package/dist/App.x2d0ygt6.js +0 -4
  23. package/dist/App.yt9p4nr3.js +0 -20
  24. package/dist/App.zn4mw16t.js +0 -1
  25. package/dist/ConnectionsPage.8r96ryw7.js +0 -3
  26. package/dist/McpPage.3cwh0gnd.js +0 -3
  27. package/dist/SettingsPage.ykgdh5ev.js +0 -3
  28. package/dist/SkillsPage.4np1s65b.js +0 -3
  29. package/dist/TasksPage.4g08t7p6.js +0 -3
  30. package/dist/TelemetryPage.72w9pwcp.js +0 -3
  31. package/dist/TestsPage.z4fk3r7r.js +0 -3
  32. package/dist/ThreadsPage.63tcajeh.js +0 -3
  33. package/dist/apteva-kit.css +0 -1
  34. package/dist/icon.png +0 -0
  35. package/dist/index.html +0 -16
  36. package/dist/styles.css +0 -1
  37. package/scripts/postinstall.mjs +0 -102
  38. package/src/auth/index.ts +0 -394
  39. package/src/auth/middleware.ts +0 -213
  40. package/src/binary.ts +0 -536
  41. package/src/channels/index.ts +0 -40
  42. package/src/channels/telegram.ts +0 -311
  43. package/src/crypto.ts +0 -301
  44. package/src/db-tests.ts +0 -174
  45. package/src/db.ts +0 -3133
  46. package/src/integrations/agentdojo.ts +0 -559
  47. package/src/integrations/composio.ts +0 -437
  48. package/src/integrations/index.ts +0 -87
  49. package/src/integrations/skillsmp.ts +0 -318
  50. package/src/mcp-client.ts +0 -605
  51. package/src/mcp-handler.ts +0 -394
  52. package/src/mcp-platform.ts +0 -2403
  53. package/src/openapi.ts +0 -2410
  54. package/src/providers.ts +0 -597
  55. package/src/routes/api/agent-utils.ts +0 -890
  56. package/src/routes/api/agents.ts +0 -916
  57. package/src/routes/api/api-keys.ts +0 -95
  58. package/src/routes/api/channels.ts +0 -182
  59. package/src/routes/api/helpers.ts +0 -12
  60. package/src/routes/api/integrations.ts +0 -639
  61. package/src/routes/api/mcp.ts +0 -574
  62. package/src/routes/api/meta-agent.ts +0 -195
  63. package/src/routes/api/projects.ts +0 -112
  64. package/src/routes/api/providers.ts +0 -424
  65. package/src/routes/api/skills.ts +0 -537
  66. package/src/routes/api/system.ts +0 -333
  67. package/src/routes/api/telemetry.ts +0 -203
  68. package/src/routes/api/tests.ts +0 -148
  69. package/src/routes/api/triggers.ts +0 -518
  70. package/src/routes/api/users.ts +0 -148
  71. package/src/routes/api/webhooks.ts +0 -171
  72. package/src/routes/api.ts +0 -53
  73. package/src/routes/auth.ts +0 -251
  74. package/src/routes/share.ts +0 -86
  75. package/src/routes/static.ts +0 -131
  76. package/src/server.ts +0 -642
  77. package/src/test-runner.ts +0 -598
  78. package/src/triggers/agentdojo.ts +0 -253
  79. package/src/triggers/composio.ts +0 -264
  80. package/src/triggers/index.ts +0 -71
  81. package/src/tui/AgentList.tsx +0 -145
  82. package/src/tui/App.tsx +0 -102
  83. package/src/tui/Login.tsx +0 -104
  84. package/src/tui/api.ts +0 -72
  85. package/src/tui/index.tsx +0 -7
  86. package/src/web/App.tsx +0 -455
  87. package/src/web/components/activity/ActivityPage.tsx +0 -314
  88. package/src/web/components/activity/index.ts +0 -1
  89. package/src/web/components/agents/AgentCard.tsx +0 -189
  90. package/src/web/components/agents/AgentPanel.tsx +0 -2244
  91. package/src/web/components/agents/AgentsView.tsx +0 -180
  92. package/src/web/components/agents/CreateAgentModal.tsx +0 -475
  93. package/src/web/components/agents/index.ts +0 -4
  94. package/src/web/components/api/ApiDocsPage.tsx +0 -842
  95. package/src/web/components/auth/CreateAccountStep.tsx +0 -176
  96. package/src/web/components/auth/LoginPage.tsx +0 -91
  97. package/src/web/components/auth/index.ts +0 -2
  98. package/src/web/components/common/Icons.tsx +0 -250
  99. package/src/web/components/common/LoadingSpinner.tsx +0 -44
  100. package/src/web/components/common/Modal.tsx +0 -199
  101. package/src/web/components/common/Select.tsx +0 -97
  102. package/src/web/components/common/index.ts +0 -20
  103. package/src/web/components/connections/ConnectionsPage.tsx +0 -54
  104. package/src/web/components/connections/IntegrationsTab.tsx +0 -170
  105. package/src/web/components/connections/OverviewTab.tsx +0 -137
  106. package/src/web/components/connections/TriggersTab.tsx +0 -1346
  107. package/src/web/components/dashboard/Dashboard.tsx +0 -572
  108. package/src/web/components/dashboard/index.ts +0 -1
  109. package/src/web/components/index.ts +0 -21
  110. package/src/web/components/layout/ErrorBanner.tsx +0 -18
  111. package/src/web/components/layout/Header.tsx +0 -332
  112. package/src/web/components/layout/Sidebar.tsx +0 -231
  113. package/src/web/components/layout/index.ts +0 -3
  114. package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
  115. package/src/web/components/mcp/McpPage.tsx +0 -2515
  116. package/src/web/components/mcp/index.ts +0 -1
  117. package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
  118. package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
  119. package/src/web/components/onboarding/index.ts +0 -1
  120. package/src/web/components/settings/SettingsPage.tsx +0 -2776
  121. package/src/web/components/settings/index.ts +0 -1
  122. package/src/web/components/skills/SkillsPage.tsx +0 -1200
  123. package/src/web/components/tasks/TasksPage.tsx +0 -1116
  124. package/src/web/components/tasks/index.ts +0 -1
  125. package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
  126. package/src/web/components/tests/TestsPage.tsx +0 -594
  127. package/src/web/components/threads/ThreadsPage.tsx +0 -315
  128. package/src/web/context/AuthContext.tsx +0 -242
  129. package/src/web/context/ProjectContext.tsx +0 -214
  130. package/src/web/context/TelemetryContext.tsx +0 -299
  131. package/src/web/context/ThemeContext.tsx +0 -90
  132. package/src/web/context/UIModeContext.tsx +0 -49
  133. package/src/web/context/index.ts +0 -12
  134. package/src/web/hooks/index.ts +0 -3
  135. package/src/web/hooks/useAgents.ts +0 -115
  136. package/src/web/hooks/useOnboarding.ts +0 -20
  137. package/src/web/hooks/useProviders.ts +0 -75
  138. package/src/web/icon.png +0 -0
  139. package/src/web/index.html +0 -16
  140. package/src/web/styles.css +0 -118
  141. package/src/web/themes.ts +0 -162
  142. package/src/web/types.ts +0 -298
package/src/db.ts DELETED
@@ -1,3133 +0,0 @@
1
- import { Database } from "bun:sqlite";
2
- import { join } from "path";
3
- import { mkdirSync, existsSync } from "fs";
4
- import { encrypt, decrypt, encryptObject, decryptObject } from "./crypto";
5
- import { randomBytes, createHash } from "crypto";
6
-
7
- // Types
8
- export interface MultiAgentConfig {
9
- enabled: boolean;
10
- group?: string; // Defaults to projectId if not specified
11
- }
12
-
13
- export interface AgentBuiltinTools {
14
- webSearch: boolean;
15
- webFetch: boolean;
16
- }
17
-
18
- export interface OperatorConfig {
19
- enabled: boolean;
20
- browser_provider?: string; // "browserengine" | "browserbase" | "steel" | "cdp"
21
- display_width?: number;
22
- display_height?: number;
23
- max_actions_per_turn?: number;
24
- }
25
-
26
- export interface RealtimeConfig {
27
- enabled: boolean;
28
- sttProvider?: string;
29
- sttModel?: string;
30
- ttsProvider?: string;
31
- ttsModel?: string;
32
- }
33
-
34
- export interface AgentFeatures {
35
- memory: boolean;
36
- tasks: boolean;
37
- vision: boolean;
38
- operator: boolean | OperatorConfig; // Can be boolean for backwards compat or full config
39
- mcp: boolean;
40
- realtime: boolean | RealtimeConfig; // Can be boolean for backwards compat or full config
41
- files: boolean;
42
- agents: boolean | MultiAgentConfig; // Can be boolean for backwards compat or full config
43
- builtinTools?: AgentBuiltinTools;
44
- maxTurns?: number; // Max agentic loop iterations per request (default 50)
45
- }
46
-
47
- export const DEFAULT_FEATURES: AgentFeatures = {
48
- memory: true,
49
- tasks: false,
50
- vision: true,
51
- operator: false,
52
- mcp: false,
53
- realtime: false,
54
- files: false,
55
- agents: false,
56
- builtinTools: { webSearch: false, webFetch: false },
57
- maxTurns: 50,
58
- };
59
-
60
- // Helper to normalize operator feature to OperatorConfig
61
- export function getOperatorConfig(features: AgentFeatures): OperatorConfig {
62
- const op = features.operator;
63
- if (typeof op === "boolean") {
64
- return { enabled: op };
65
- }
66
- return op;
67
- }
68
-
69
- // Helper to normalize realtime feature to RealtimeConfig
70
- export function getRealtimeConfig(features: AgentFeatures): RealtimeConfig {
71
- const rt = features.realtime;
72
- if (typeof rt === "boolean") {
73
- return { enabled: rt };
74
- }
75
- return rt;
76
- }
77
-
78
- // Helper to check if realtime is enabled
79
- export function isRealtimeEnabled(features: AgentFeatures): boolean {
80
- if (typeof features.realtime === "boolean") return features.realtime;
81
- return features.realtime.enabled;
82
- }
83
-
84
- // Helper to normalize agents feature to MultiAgentConfig
85
- export function getMultiAgentConfig(features: AgentFeatures, projectId?: string | null): MultiAgentConfig {
86
- const agents = features.agents;
87
- if (typeof agents === "boolean") {
88
- return {
89
- enabled: agents,
90
- group: projectId || undefined,
91
- };
92
- }
93
- // Only keep known fields (strip legacy "mode" coordinator/worker)
94
- return {
95
- enabled: agents.enabled,
96
- group: agents.group || projectId || undefined,
97
- };
98
- }
99
-
100
- export interface Agent {
101
- id: string;
102
- name: string;
103
- model: string;
104
- provider: string;
105
- system_prompt: string;
106
- status: "stopped" | "running";
107
- port: number | null;
108
- features: AgentFeatures;
109
- mcp_servers: string[]; // Array of MCP server IDs
110
- skills: string[]; // Array of Skill IDs
111
- project_id: string | null; // Optional project grouping
112
- api_key_encrypted: string | null; // Encrypted API key for agent authentication
113
- created_at: string;
114
- updated_at: string;
115
- }
116
-
117
- export interface Project {
118
- id: string;
119
- name: string;
120
- description: string | null;
121
- color: string; // Hex color for UI display
122
- created_at: string;
123
- updated_at: string;
124
- }
125
-
126
- export interface ProjectRow {
127
- id: string;
128
- name: string;
129
- description: string | null;
130
- color: string;
131
- created_at: string;
132
- updated_at: string;
133
- }
134
-
135
- export interface AgentRow {
136
- id: string;
137
- name: string;
138
- model: string;
139
- provider: string;
140
- system_prompt: string;
141
- status: string;
142
- port: number | null;
143
- features: string | null;
144
- mcp_servers: string | null;
145
- skills: string | null;
146
- project_id: string | null;
147
- api_key_encrypted: string | null;
148
- created_at: string;
149
- updated_at: string;
150
- }
151
-
152
- export interface Settings {
153
- key: string;
154
- value: string;
155
- }
156
-
157
- export interface ProviderKey {
158
- id: string;
159
- provider_id: string;
160
- encrypted_key: string;
161
- key_hint: string;
162
- is_valid: boolean;
163
- last_tested_at: string | null;
164
- created_at: string;
165
- project_id: string | null; // NULL = global, otherwise project-scoped
166
- name: string | null; // Optional display name (e.g., "Production", "Development")
167
- }
168
-
169
- export interface ProviderKeyRow {
170
- id: string;
171
- provider_id: string;
172
- encrypted_key: string;
173
- key_hint: string;
174
- is_valid: number;
175
- last_tested_at: string | null;
176
- created_at: string;
177
- project_id: string | null;
178
- name: string | null;
179
- }
180
-
181
- export interface McpServer {
182
- id: string;
183
- name: string;
184
- type: "npm" | "pip" | "github" | "http" | "custom" | "local";
185
- package: string | null; // npm or pip package name
186
- pip_module: string | null; // For pip type: the module to run (e.g., "late.mcp")
187
- command: string | null;
188
- args: string | null;
189
- env: Record<string, string>;
190
- url: string | null; // For http type: the remote server URL
191
- headers: Record<string, string>; // For http type: auth headers
192
- port: number | null;
193
- status: "stopped" | "running";
194
- source: string | null; // e.g., "composio", "smithery", null for local
195
- project_id: string | null; // null = global, otherwise project-scoped
196
- created_at: string;
197
- }
198
-
199
- // Skill types
200
- export interface Skill {
201
- id: string;
202
- name: string;
203
- description: string;
204
- content: string; // Full SKILL.md body (markdown)
205
- version: string; // Semantic version (e.g., "1.0.0")
206
- license: string | null;
207
- compatibility: string | null;
208
- metadata: Record<string, string>;
209
- allowed_tools: string[];
210
- source: "local" | "skillsmp" | "github" | "import";
211
- source_url: string | null;
212
- enabled: boolean;
213
- project_id: string | null; // null = global, otherwise project-scoped
214
- created_at: string;
215
- updated_at: string;
216
- }
217
-
218
- export interface SkillRow {
219
- id: string;
220
- name: string;
221
- description: string;
222
- content: string;
223
- version: string;
224
- license: string | null;
225
- compatibility: string | null;
226
- metadata: string | null;
227
- allowed_tools: string | null;
228
- source: string;
229
- source_url: string | null;
230
- enabled: number;
231
- project_id: string | null;
232
- created_at: string;
233
- updated_at: string;
234
- }
235
-
236
- // Subscription: maps trigger events to agents for routing
237
- export interface Subscription {
238
- id: string;
239
- trigger_slug: string;
240
- trigger_instance_id: string | null;
241
- agent_id: string;
242
- enabled: boolean;
243
- project_id: string | null;
244
- created_at: string;
245
- updated_at: string;
246
- }
247
-
248
- export interface SubscriptionRow {
249
- id: string;
250
- trigger_slug: string;
251
- trigger_instance_id: string | null;
252
- agent_id: string;
253
- enabled: number;
254
- project_id: string | null;
255
- created_at: string;
256
- updated_at: string;
257
- }
258
-
259
- // Channel: external messaging platform bound to an agent
260
- export interface Channel {
261
- id: string;
262
- type: "telegram"; // future: "slack", "discord"
263
- name: string;
264
- agent_id: string;
265
- config: string; // encrypted JSON
266
- status: "stopped" | "running" | "error";
267
- error: string | null;
268
- project_id: string | null;
269
- created_at: string;
270
- updated_at: string;
271
- }
272
-
273
- export interface ChannelRow {
274
- id: string;
275
- type: string;
276
- name: string;
277
- agent_id: string;
278
- config: string;
279
- status: string;
280
- error: string | null;
281
- project_id: string | null;
282
- created_at: string;
283
- updated_at: string;
284
- }
285
-
286
- export interface McpServerRow {
287
- id: string;
288
- name: string;
289
- type: string;
290
- package: string | null;
291
- pip_module: string | null;
292
- command: string | null;
293
- args: string | null;
294
- env: string | null;
295
- url: string | null;
296
- headers: string | null;
297
- port: number | null;
298
- status: string;
299
- source: string | null;
300
- project_id: string | null;
301
- created_at: string;
302
- }
303
-
304
- // MCP Server Tool types (for local servers)
305
- export interface McpServerTool {
306
- id: string;
307
- server_id: string;
308
- name: string;
309
- description: string;
310
- input_schema: Record<string, any>;
311
- handler_type: "mock" | "http" | "javascript";
312
- mock_response: Record<string, any> | null;
313
- http_config: { method?: string; url: string; headers?: Record<string, string>; body?: any } | null;
314
- code: string | null;
315
- enabled: boolean;
316
- created_at: string;
317
- }
318
-
319
- interface McpServerToolRow {
320
- id: string;
321
- server_id: string;
322
- name: string;
323
- description: string;
324
- input_schema: string;
325
- handler_type: string;
326
- mock_response: string | null;
327
- http_config: string | null;
328
- code: string | null;
329
- enabled: number;
330
- created_at: string;
331
- }
332
-
333
- // Database instance
334
- let db: Database;
335
-
336
- // Initialize database
337
- export function initDatabase(dataDir: string): Database {
338
- // Ensure data directory exists
339
- if (!existsSync(dataDir)) {
340
- mkdirSync(dataDir, { recursive: true });
341
- }
342
-
343
- const dbPath = join(dataDir, "apteva.db");
344
- db = new Database(dbPath);
345
-
346
- // Enable WAL mode for better concurrent access
347
- db.run("PRAGMA journal_mode = WAL");
348
- db.run("PRAGMA busy_timeout = 5000");
349
- db.run("PRAGMA foreign_keys = ON");
350
-
351
- // Performance PRAGMAs
352
- db.run("PRAGMA synchronous = NORMAL"); // Safe with WAL, much faster than FULL
353
- db.run("PRAGMA cache_size = -20000"); // 20MB page cache (negative = KB)
354
- db.run("PRAGMA mmap_size = 30000000"); // 30MB memory-mapped I/O
355
- db.run("PRAGMA temp_store = MEMORY"); // Keep temp tables in memory
356
-
357
- // Run migrations
358
- runMigrations();
359
-
360
- // Auto-set instance_url from env if not already configured
361
- const envInstanceUrl = process.env.INSTANCE_URL || process.env.PUBLIC_URL;
362
- if (envInstanceUrl) {
363
- const current = SettingsDB.get("instance_url");
364
- if (!current) {
365
- SettingsDB.set("instance_url", envInstanceUrl.replace(/\/+$/, ""));
366
- }
367
- }
368
-
369
- // Database initialized silently
370
- return db;
371
- }
372
-
373
- // Get database instance
374
- export function getDb(): Database {
375
- if (!db) {
376
- throw new Error("Database not initialized. Call initDatabase() first.");
377
- }
378
- return db;
379
- }
380
-
381
- // Migrations
382
- function runMigrations() {
383
- // Create migrations table if not exists
384
- db.run(`
385
- CREATE TABLE IF NOT EXISTS migrations (
386
- id INTEGER PRIMARY KEY AUTOINCREMENT,
387
- name TEXT NOT NULL UNIQUE,
388
- applied_at TEXT DEFAULT CURRENT_TIMESTAMP
389
- )
390
- `);
391
-
392
- const migrations: { name: string; sql: string }[] = [
393
- {
394
- name: "001_create_agents",
395
- sql: `
396
- CREATE TABLE IF NOT EXISTS agents (
397
- id TEXT PRIMARY KEY,
398
- name TEXT NOT NULL,
399
- model TEXT NOT NULL,
400
- provider TEXT NOT NULL,
401
- system_prompt TEXT NOT NULL DEFAULT 'You are a helpful assistant.',
402
- status TEXT NOT NULL DEFAULT 'stopped',
403
- port INTEGER,
404
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
405
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
406
- );
407
- CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
408
- CREATE INDEX IF NOT EXISTS idx_agents_provider ON agents(provider);
409
- `,
410
- },
411
- {
412
- name: "002_create_settings",
413
- sql: `
414
- CREATE TABLE IF NOT EXISTS settings (
415
- key TEXT PRIMARY KEY,
416
- value TEXT NOT NULL,
417
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
418
- );
419
- `,
420
- },
421
- {
422
- name: "003_create_threads",
423
- sql: `
424
- CREATE TABLE IF NOT EXISTS threads (
425
- id TEXT PRIMARY KEY,
426
- agent_id TEXT NOT NULL,
427
- title TEXT,
428
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
429
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
430
- FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
431
- );
432
- CREATE INDEX IF NOT EXISTS idx_threads_agent ON threads(agent_id);
433
- `,
434
- },
435
- {
436
- name: "004_create_messages",
437
- sql: `
438
- CREATE TABLE IF NOT EXISTS messages (
439
- id TEXT PRIMARY KEY,
440
- thread_id TEXT NOT NULL,
441
- role TEXT NOT NULL,
442
- content TEXT NOT NULL,
443
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
444
- FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
445
- );
446
- CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id);
447
- `,
448
- },
449
- {
450
- name: "005_create_provider_keys",
451
- sql: `
452
- CREATE TABLE IF NOT EXISTS provider_keys (
453
- id TEXT PRIMARY KEY,
454
- provider_id TEXT NOT NULL UNIQUE,
455
- encrypted_key TEXT NOT NULL,
456
- key_hint TEXT,
457
- is_valid INTEGER DEFAULT 1,
458
- last_tested_at TEXT,
459
- created_at TEXT DEFAULT CURRENT_TIMESTAMP
460
- );
461
- CREATE INDEX IF NOT EXISTS idx_provider_keys_provider ON provider_keys(provider_id);
462
- `,
463
- },
464
- {
465
- name: "006_add_agent_features",
466
- sql: `
467
- ALTER TABLE agents ADD COLUMN features TEXT DEFAULT '{"memory":true,"tasks":false,"vision":true,"operator":false,"mcp":false,"realtime":false}';
468
- `,
469
- },
470
- {
471
- name: "007_create_mcp_servers",
472
- sql: `
473
- CREATE TABLE IF NOT EXISTS mcp_servers (
474
- id TEXT PRIMARY KEY,
475
- name TEXT NOT NULL,
476
- type TEXT NOT NULL DEFAULT 'npm',
477
- package TEXT,
478
- command TEXT,
479
- args TEXT,
480
- env TEXT DEFAULT '{}',
481
- port INTEGER,
482
- status TEXT NOT NULL DEFAULT 'stopped',
483
- created_at TEXT DEFAULT CURRENT_TIMESTAMP
484
- );
485
- CREATE INDEX IF NOT EXISTS idx_mcp_servers_status ON mcp_servers(status);
486
- `,
487
- },
488
- {
489
- name: "008_add_agent_mcp_servers",
490
- sql: `
491
- ALTER TABLE agents ADD COLUMN mcp_servers TEXT DEFAULT '[]';
492
- `,
493
- },
494
- {
495
- name: "009_create_telemetry",
496
- sql: `
497
- CREATE TABLE IF NOT EXISTS telemetry_events (
498
- id TEXT PRIMARY KEY,
499
- agent_id TEXT NOT NULL,
500
- timestamp TEXT NOT NULL,
501
- category TEXT NOT NULL,
502
- type TEXT NOT NULL,
503
- level TEXT NOT NULL,
504
- trace_id TEXT,
505
- span_id TEXT,
506
- thread_id TEXT,
507
- data TEXT,
508
- metadata TEXT,
509
- duration_ms INTEGER,
510
- error TEXT,
511
- received_at TEXT NOT NULL
512
- );
513
- CREATE INDEX IF NOT EXISTS idx_telemetry_agent ON telemetry_events(agent_id);
514
- CREATE INDEX IF NOT EXISTS idx_telemetry_time ON telemetry_events(timestamp);
515
- CREATE INDEX IF NOT EXISTS idx_telemetry_agent_time ON telemetry_events(agent_id, timestamp DESC);
516
- CREATE INDEX IF NOT EXISTS idx_telemetry_category ON telemetry_events(category);
517
- CREATE INDEX IF NOT EXISTS idx_telemetry_level ON telemetry_events(level);
518
- CREATE INDEX IF NOT EXISTS idx_telemetry_trace ON telemetry_events(trace_id);
519
- `,
520
- },
521
- {
522
- name: "010_create_users",
523
- sql: `
524
- CREATE TABLE IF NOT EXISTS users (
525
- id TEXT PRIMARY KEY,
526
- username TEXT UNIQUE NOT NULL,
527
- password_hash TEXT NOT NULL,
528
- email TEXT,
529
- role TEXT NOT NULL DEFAULT 'user',
530
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
531
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
532
- last_login_at TEXT
533
- );
534
- CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username);
535
- `,
536
- },
537
- {
538
- name: "011_create_sessions",
539
- sql: `
540
- CREATE TABLE IF NOT EXISTS sessions (
541
- id TEXT PRIMARY KEY,
542
- user_id TEXT NOT NULL,
543
- refresh_token_hash TEXT NOT NULL,
544
- expires_at TEXT NOT NULL,
545
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
546
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
547
- );
548
- CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
549
- CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
550
- `,
551
- },
552
- {
553
- name: "012_create_projects",
554
- sql: `
555
- CREATE TABLE IF NOT EXISTS projects (
556
- id TEXT PRIMARY KEY,
557
- name TEXT NOT NULL,
558
- description TEXT,
559
- color TEXT NOT NULL DEFAULT '#6366f1',
560
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
561
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
562
- );
563
- CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name);
564
- `,
565
- },
566
- {
567
- name: "013_add_agent_project_id",
568
- sql: `
569
- ALTER TABLE agents ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
570
- CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id);
571
- `,
572
- },
573
- {
574
- name: "014_add_mcp_server_url_headers",
575
- sql: `
576
- ALTER TABLE mcp_servers ADD COLUMN url TEXT;
577
- ALTER TABLE mcp_servers ADD COLUMN headers TEXT DEFAULT '{}';
578
- ALTER TABLE mcp_servers ADD COLUMN source TEXT;
579
- `,
580
- },
581
- {
582
- name: "015_add_agent_api_key",
583
- sql: `
584
- ALTER TABLE agents ADD COLUMN api_key_encrypted TEXT;
585
- `,
586
- },
587
- {
588
- name: "016_create_skills",
589
- sql: `
590
- CREATE TABLE IF NOT EXISTS skills (
591
- id TEXT PRIMARY KEY,
592
- name TEXT NOT NULL,
593
- description TEXT NOT NULL,
594
- content TEXT NOT NULL,
595
- license TEXT,
596
- compatibility TEXT,
597
- metadata TEXT DEFAULT '{}',
598
- allowed_tools TEXT DEFAULT '[]',
599
- source TEXT NOT NULL DEFAULT 'local',
600
- source_url TEXT,
601
- enabled INTEGER DEFAULT 1,
602
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
603
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
604
- );
605
- CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
606
- CREATE INDEX IF NOT EXISTS idx_skills_source ON skills(source);
607
- CREATE INDEX IF NOT EXISTS idx_skills_enabled ON skills(enabled);
608
- `,
609
- },
610
- {
611
- name: "017_add_skills_to_agents",
612
- sql: `
613
- ALTER TABLE agents ADD COLUMN skills TEXT DEFAULT '[]';
614
- `,
615
- },
616
- {
617
- name: "018_add_skill_version",
618
- sql: `
619
- ALTER TABLE skills ADD COLUMN version TEXT DEFAULT '1.0.0';
620
- `,
621
- },
622
- {
623
- name: "019_add_mcp_server_project_id",
624
- sql: `
625
- ALTER TABLE mcp_servers ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
626
- CREATE INDEX IF NOT EXISTS idx_mcp_servers_project ON mcp_servers(project_id);
627
- `,
628
- },
629
- {
630
- name: "020_add_skill_project_id",
631
- sql: `
632
- ALTER TABLE skills ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
633
- CREATE INDEX IF NOT EXISTS idx_skills_project ON skills(project_id);
634
- `,
635
- },
636
- {
637
- name: "021_add_mcp_server_pip_module",
638
- sql: `
639
- ALTER TABLE mcp_servers ADD COLUMN pip_module TEXT;
640
- `,
641
- },
642
- {
643
- name: "022_add_provider_keys_project_id",
644
- sql: `
645
- -- Add project_id column for project-scoped integration keys
646
- -- NULL project_id means global (default)
647
- ALTER TABLE provider_keys ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE CASCADE;
648
- ALTER TABLE provider_keys ADD COLUMN name TEXT;
649
-
650
- -- Create index for project lookups
651
- CREATE INDEX IF NOT EXISTS idx_provider_keys_project ON provider_keys(project_id);
652
-
653
- -- Create unique index on (provider_id, project_id) - allows one key per provider per project
654
- -- Note: SQLite treats NULL as distinct, so we use COALESCE
655
- CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
656
- `,
657
- },
658
- {
659
- name: "023_create_test_cases",
660
- sql: `
661
- CREATE TABLE IF NOT EXISTS test_cases (
662
- id TEXT PRIMARY KEY,
663
- name TEXT NOT NULL,
664
- description TEXT,
665
- agent_id TEXT NOT NULL,
666
- input_message TEXT NOT NULL,
667
- eval_criteria TEXT NOT NULL,
668
- timeout_ms INTEGER DEFAULT 60000,
669
- project_id TEXT,
670
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
671
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
672
- );
673
- CREATE INDEX IF NOT EXISTS idx_test_cases_agent ON test_cases(agent_id);
674
- CREATE INDEX IF NOT EXISTS idx_test_cases_project ON test_cases(project_id);
675
- `,
676
- },
677
- {
678
- name: "025_create_api_keys",
679
- sql: `
680
- CREATE TABLE IF NOT EXISTS api_keys (
681
- id TEXT PRIMARY KEY,
682
- name TEXT NOT NULL,
683
- key_hash TEXT NOT NULL UNIQUE,
684
- key_prefix TEXT NOT NULL,
685
- user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
686
- expires_at TEXT,
687
- last_used_at TEXT,
688
- is_active INTEGER DEFAULT 1,
689
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
690
- );
691
- CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash) WHERE is_active = 1;
692
- CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id);
693
- `,
694
- },
695
- {
696
- name: "024_create_test_runs",
697
- sql: `
698
- CREATE TABLE IF NOT EXISTS test_runs (
699
- id TEXT PRIMARY KEY,
700
- test_case_id TEXT NOT NULL,
701
- status TEXT NOT NULL DEFAULT 'running',
702
- agent_response TEXT,
703
- judge_reasoning TEXT,
704
- duration_ms INTEGER,
705
- error TEXT,
706
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
707
- FOREIGN KEY (test_case_id) REFERENCES test_cases(id) ON DELETE CASCADE
708
- );
709
- CREATE INDEX IF NOT EXISTS idx_test_runs_test_case ON test_runs(test_case_id);
710
- CREATE INDEX IF NOT EXISTS idx_test_runs_status ON test_runs(status);
711
- `,
712
- },
713
- {
714
- name: "026_behavior_tests",
715
- sql: `
716
- -- Recreate test_cases with nullable agent_id and input_message
717
- CREATE TABLE IF NOT EXISTS test_cases_new (
718
- id TEXT PRIMARY KEY,
719
- name TEXT NOT NULL,
720
- description TEXT,
721
- behavior TEXT,
722
- agent_id TEXT,
723
- input_message TEXT,
724
- eval_criteria TEXT NOT NULL DEFAULT '',
725
- timeout_ms INTEGER DEFAULT 300000,
726
- project_id TEXT,
727
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
728
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
729
- );
730
- INSERT OR IGNORE INTO test_cases_new (id, name, description, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at)
731
- SELECT id, name, description, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at FROM test_cases;
732
- DROP TABLE IF EXISTS test_cases;
733
- ALTER TABLE test_cases_new RENAME TO test_cases;
734
- CREATE INDEX IF NOT EXISTS idx_test_cases_agent ON test_cases(agent_id);
735
- CREATE INDEX IF NOT EXISTS idx_test_cases_project ON test_cases(project_id);
736
-
737
- -- Add planner columns to test_runs
738
- ALTER TABLE test_runs ADD COLUMN generated_message TEXT;
739
- ALTER TABLE test_runs ADD COLUMN selected_agent_id TEXT;
740
- ALTER TABLE test_runs ADD COLUMN selected_agent_name TEXT;
741
- ALTER TABLE test_runs ADD COLUMN planner_reasoning TEXT;
742
- `,
743
- },
744
- {
745
- name: "027_fix_test_cases_nullable",
746
- sql: `
747
- -- Recreate test_cases with nullable agent_id and input_message
748
- CREATE TABLE IF NOT EXISTS test_cases_new (
749
- id TEXT PRIMARY KEY,
750
- name TEXT NOT NULL,
751
- description TEXT,
752
- behavior TEXT,
753
- agent_id TEXT,
754
- input_message TEXT,
755
- eval_criteria TEXT NOT NULL DEFAULT '',
756
- timeout_ms INTEGER DEFAULT 300000,
757
- project_id TEXT,
758
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
759
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
760
- );
761
- INSERT OR IGNORE INTO test_cases_new (id, name, description, behavior, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at)
762
- SELECT id, name, description, behavior, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at FROM test_cases;
763
- DROP TABLE IF EXISTS test_cases;
764
- ALTER TABLE test_cases_new RENAME TO test_cases;
765
- CREATE INDEX IF NOT EXISTS idx_test_cases_agent ON test_cases(agent_id);
766
- CREATE INDEX IF NOT EXISTS idx_test_cases_project ON test_cases(project_id);
767
- `,
768
- },
769
- {
770
- name: "028_add_test_run_score",
771
- sql: `ALTER TABLE test_runs ADD COLUMN score INTEGER;`,
772
- },
773
- {
774
- name: "030_create_mcp_server_tools",
775
- sql: `
776
- CREATE TABLE IF NOT EXISTS mcp_server_tools (
777
- id TEXT PRIMARY KEY,
778
- server_id TEXT NOT NULL,
779
- name TEXT NOT NULL,
780
- description TEXT NOT NULL,
781
- input_schema TEXT NOT NULL DEFAULT '{}',
782
- handler_type TEXT NOT NULL DEFAULT 'mock',
783
- mock_response TEXT DEFAULT '{}',
784
- http_config TEXT DEFAULT NULL,
785
- code TEXT DEFAULT NULL,
786
- enabled INTEGER DEFAULT 1,
787
- created_at TEXT DEFAULT (datetime('now')),
788
- FOREIGN KEY (server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE
789
- );
790
- CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_server_tools_name ON mcp_server_tools(server_id, name);
791
- CREATE INDEX IF NOT EXISTS idx_mcp_server_tools_server ON mcp_server_tools(server_id);
792
- `,
793
- },
794
- {
795
- name: "031_create_subscriptions",
796
- sql: `
797
- CREATE TABLE IF NOT EXISTS subscriptions (
798
- id TEXT PRIMARY KEY,
799
- trigger_slug TEXT NOT NULL,
800
- trigger_instance_id TEXT,
801
- agent_id TEXT NOT NULL,
802
- enabled INTEGER NOT NULL DEFAULT 1,
803
- project_id TEXT,
804
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
805
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
806
- FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
807
- );
808
- CREATE INDEX IF NOT EXISTS idx_subscriptions_agent ON subscriptions(agent_id);
809
- CREATE INDEX IF NOT EXISTS idx_subscriptions_trigger_slug ON subscriptions(trigger_slug);
810
- CREATE INDEX IF NOT EXISTS idx_subscriptions_trigger_instance ON subscriptions(trigger_instance_id);
811
- `,
812
- },
813
- {
814
- name: "032_create_channels",
815
- sql: `
816
- CREATE TABLE IF NOT EXISTS channels (
817
- id TEXT PRIMARY KEY,
818
- type TEXT NOT NULL,
819
- name TEXT NOT NULL,
820
- agent_id TEXT NOT NULL,
821
- config TEXT NOT NULL,
822
- status TEXT DEFAULT 'stopped',
823
- error TEXT,
824
- project_id TEXT,
825
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
826
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
827
- FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
828
- );
829
- CREATE INDEX IF NOT EXISTS idx_channels_agent ON channels(agent_id);
830
- CREATE INDEX IF NOT EXISTS idx_channels_status ON channels(status);
831
- `,
832
- },
833
- {
834
- name: "033_add_telemetry_seen",
835
- sql: `
836
- ALTER TABLE telemetry_events ADD COLUMN seen INTEGER DEFAULT 0;
837
- CREATE INDEX IF NOT EXISTS idx_telemetry_seen ON telemetry_events(seen);
838
- `,
839
- },
840
- {
841
- name: "029_fix_provider_keys_unique_constraint",
842
- sql: `
843
- -- Recreate provider_keys table without UNIQUE constraint on provider_id alone
844
- -- This allows multiple keys per provider (one per project)
845
- CREATE TABLE IF NOT EXISTS provider_keys_new (
846
- id TEXT PRIMARY KEY,
847
- provider_id TEXT NOT NULL,
848
- encrypted_key TEXT NOT NULL,
849
- key_hint TEXT,
850
- is_valid INTEGER DEFAULT 1,
851
- last_tested_at TEXT,
852
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
853
- project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
854
- name TEXT
855
- );
856
- INSERT OR IGNORE INTO provider_keys_new (id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at, project_id, name)
857
- SELECT id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at, project_id, name FROM provider_keys;
858
- DROP TABLE IF EXISTS provider_keys;
859
- ALTER TABLE provider_keys_new RENAME TO provider_keys;
860
- CREATE INDEX IF NOT EXISTS idx_provider_keys_provider ON provider_keys(provider_id);
861
- CREATE INDEX IF NOT EXISTS idx_provider_keys_project ON provider_keys(project_id);
862
- CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
863
- `,
864
- },
865
- {
866
- name: "034_repair_provider_keys_project_support",
867
- sql: `
868
- -- Repair: migrations 022 and 029 may have failed but were marked as applied.
869
- -- Recreate provider_keys with project_id support from whatever current state.
870
- CREATE TABLE IF NOT EXISTS provider_keys_repair (
871
- id TEXT PRIMARY KEY,
872
- provider_id TEXT NOT NULL,
873
- encrypted_key TEXT NOT NULL,
874
- key_hint TEXT,
875
- is_valid INTEGER DEFAULT 1,
876
- last_tested_at TEXT,
877
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
878
- project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
879
- name TEXT
880
- );
881
- INSERT OR IGNORE INTO provider_keys_repair (id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at)
882
- SELECT id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at FROM provider_keys;
883
- DROP TABLE IF EXISTS provider_keys;
884
- ALTER TABLE provider_keys_repair RENAME TO provider_keys;
885
- CREATE INDEX IF NOT EXISTS idx_provider_keys_provider ON provider_keys(provider_id);
886
- CREATE INDEX IF NOT EXISTS idx_provider_keys_project ON provider_keys(project_id);
887
- CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
888
- `,
889
- },
890
- {
891
- name: "035_add_telemetry_cost",
892
- sql: `
893
- ALTER TABLE telemetry_events ADD COLUMN cost REAL DEFAULT 0;
894
- `,
895
- },
896
- ];
897
-
898
- // Check which migrations have been applied
899
- const applied = new Set<string>();
900
- const rows = db.query("SELECT name FROM migrations").all() as { name: string }[];
901
- for (const row of rows) {
902
- applied.add(row.name);
903
- }
904
-
905
- // Run pending migrations
906
- for (const migration of migrations) {
907
- if (!applied.has(migration.name)) {
908
- try {
909
- // Migration runs silently (exec supports multi-statement SQL)
910
- db.exec(migration.sql);
911
- db.run("INSERT INTO migrations (name) VALUES (?)", [migration.name]);
912
- } catch (err) {
913
- // Log error but continue - some migrations may fail if partially applied
914
- console.error(`[db] Migration ${migration.name} failed:`, err);
915
- // Still mark as applied to avoid retrying broken migrations
916
- try {
917
- db.run("INSERT INTO migrations (name) VALUES (?)", [migration.name]);
918
- } catch {
919
- // Ignore if already marked
920
- }
921
- }
922
- }
923
- }
924
-
925
- // Schema upgrade migrations (check actual table structure)
926
- runSchemaUpgrades();
927
- }
928
-
929
- // Handle schema changes that require checking actual table structure
930
- function runSchemaUpgrades() {
931
- // Check if users table needs migration from email-based to username-based
932
- const tableInfo = db.query("PRAGMA table_info(users)").all() as { name: string }[];
933
- const columns = new Set(tableInfo.map(c => c.name));
934
-
935
- // Old schema has 'email' as required + 'name', new schema has 'username' + optional 'email'
936
- if (columns.has("name") && !columns.has("username")) {
937
- console.log("[db] Migrating users table from email-based to username-based auth...");
938
-
939
- // Get existing users
940
- const existingUsers = db.query("SELECT * FROM users").all() as any[];
941
-
942
- // Drop old table and indexes
943
- db.run("DROP INDEX IF EXISTS idx_users_email");
944
- db.run("DROP TABLE users");
945
-
946
- // Create new schema
947
- db.run(`
948
- CREATE TABLE users (
949
- id TEXT PRIMARY KEY,
950
- username TEXT UNIQUE NOT NULL,
951
- password_hash TEXT NOT NULL,
952
- email TEXT,
953
- role TEXT NOT NULL DEFAULT 'user',
954
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
955
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
956
- last_login_at TEXT
957
- )
958
- `);
959
- db.run("CREATE UNIQUE INDEX idx_users_username ON users(username)");
960
-
961
- // Migrate existing users (use part before @ in email as username)
962
- for (const user of existingUsers) {
963
- const username = user.email.split("@")[0].replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 20);
964
- db.run(
965
- `INSERT INTO users (id, username, password_hash, email, role, created_at, updated_at, last_login_at)
966
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
967
- [user.id, username, user.password_hash, user.email, user.role, user.created_at, user.updated_at, user.last_login_at]
968
- );
969
- }
970
-
971
- if (existingUsers.length > 0) {
972
- console.log(`[db] Migrated ${existingUsers.length} user(s). Usernames derived from email addresses.`);
973
- }
974
- }
975
-
976
- // Assign permanent ports to MCP servers that don't have one yet
977
- // (HTTP-type servers don't need a local proxy port)
978
- const mcpWithoutPort = db.query("SELECT id FROM mcp_servers WHERE port IS NULL AND type NOT IN ('http', 'local')").all() as { id: string }[];
979
- if (mcpWithoutPort.length > 0) {
980
- const MCP_BASE_PORT = 4500;
981
- const maxRow = db.query("SELECT MAX(port) as max_port FROM mcp_servers").get() as { max_port: number | null };
982
- let nextPort = maxRow.max_port !== null ? maxRow.max_port + 1 : MCP_BASE_PORT;
983
- for (const row of mcpWithoutPort) {
984
- db.run("UPDATE mcp_servers SET port = ? WHERE id = ?", [nextPort, row.id]);
985
- nextPort++;
986
- }
987
- }
988
- }
989
-
990
- // Generate a unique API key for an agent
991
- function generateAgentApiKey(agentId: string): string {
992
- const randomPart = randomBytes(24).toString("hex");
993
- return `agt_${randomPart}`;
994
- }
995
-
996
- // Agent CRUD operations
997
- export const AgentDB = {
998
- // Get the next available port for a new agent (starting from 4100)
999
- getNextAvailablePort(): number {
1000
- const BASE_PORT = 4100;
1001
- const row = db.query("SELECT MAX(port) as max_port FROM agents").get() as { max_port: number | null };
1002
- if (row.max_port === null) {
1003
- return BASE_PORT;
1004
- }
1005
- return row.max_port + 1;
1006
- },
1007
-
1008
- // Create a new agent with a permanently assigned port and API key
1009
- create(agent: Omit<Agent, "created_at" | "updated_at" | "status" | "api_key_encrypted"> & { port?: number }): Agent {
1010
- const now = new Date().toISOString();
1011
- const featuresJson = JSON.stringify(agent.features || DEFAULT_FEATURES);
1012
- const mcpServersJson = JSON.stringify(agent.mcp_servers || []);
1013
- const skillsJson = JSON.stringify(agent.skills || []);
1014
- // Assign port permanently at creation time
1015
- const port = agent.port ?? this.getNextAvailablePort();
1016
- // Generate and encrypt API key
1017
- const apiKey = generateAgentApiKey(agent.id);
1018
- const apiKeyEncrypted = encrypt(apiKey);
1019
- const stmt = db.prepare(`
1020
- INSERT INTO agents (id, name, model, provider, system_prompt, features, mcp_servers, skills, project_id, status, port, api_key_encrypted, created_at, updated_at)
1021
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'stopped', ?, ?, ?, ?)
1022
- `);
1023
- stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, mcpServersJson, skillsJson, agent.project_id || null, port, apiKeyEncrypted, now, now);
1024
- return this.findById(agent.id)!;
1025
- },
1026
-
1027
- // Find agent by ID
1028
- findById(id: string): Agent | null {
1029
- const row = db.query("SELECT * FROM agents WHERE id = ?").get(id) as AgentRow | null;
1030
- return row ? rowToAgent(row) : null;
1031
- },
1032
-
1033
- // Get all agents
1034
- findAll(): Agent[] {
1035
- const rows = db.query("SELECT * FROM agents ORDER BY created_at DESC").all() as AgentRow[];
1036
- return rows.map(rowToAgent);
1037
- },
1038
-
1039
- // Get running agents
1040
- findRunning(): Agent[] {
1041
- const rows = db.query("SELECT * FROM agents WHERE status = 'running'").all() as AgentRow[];
1042
- return rows.map(rowToAgent);
1043
- },
1044
-
1045
-
1046
- // Update agent
1047
- update(id: string, updates: Partial<Omit<Agent, "id" | "created_at">>): Agent | null {
1048
- const agent = this.findById(id);
1049
- if (!agent) return null;
1050
-
1051
- const fields: string[] = [];
1052
- const values: unknown[] = [];
1053
-
1054
- if (updates.name !== undefined) {
1055
- fields.push("name = ?");
1056
- values.push(updates.name);
1057
- }
1058
- if (updates.model !== undefined) {
1059
- fields.push("model = ?");
1060
- values.push(updates.model);
1061
- }
1062
- if (updates.provider !== undefined) {
1063
- fields.push("provider = ?");
1064
- values.push(updates.provider);
1065
- }
1066
- if (updates.system_prompt !== undefined) {
1067
- fields.push("system_prompt = ?");
1068
- values.push(updates.system_prompt);
1069
- }
1070
- if (updates.status !== undefined) {
1071
- fields.push("status = ?");
1072
- values.push(updates.status);
1073
- }
1074
- if (updates.port !== undefined) {
1075
- fields.push("port = ?");
1076
- values.push(updates.port);
1077
- }
1078
- if (updates.features !== undefined) {
1079
- fields.push("features = ?");
1080
- values.push(JSON.stringify(updates.features));
1081
- }
1082
- if (updates.mcp_servers !== undefined) {
1083
- fields.push("mcp_servers = ?");
1084
- values.push(JSON.stringify(updates.mcp_servers));
1085
- }
1086
- if (updates.skills !== undefined) {
1087
- fields.push("skills = ?");
1088
- values.push(JSON.stringify(updates.skills));
1089
- }
1090
- if (updates.project_id !== undefined) {
1091
- fields.push("project_id = ?");
1092
- values.push(updates.project_id);
1093
- }
1094
- if (fields.length > 0) {
1095
- fields.push("updated_at = ?");
1096
- values.push(new Date().toISOString());
1097
- values.push(id);
1098
-
1099
- db.run(`UPDATE agents SET ${fields.join(", ")} WHERE id = ?`, values);
1100
- }
1101
-
1102
- return this.findById(id);
1103
- },
1104
-
1105
- // Find agents by project
1106
- findByProject(projectId: string | null): Agent[] {
1107
- if (projectId === null) {
1108
- const rows = db.query("SELECT * FROM agents WHERE project_id IS NULL ORDER BY created_at DESC").all() as AgentRow[];
1109
- return rows.map(rowToAgent);
1110
- }
1111
- const rows = db.query("SELECT * FROM agents WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as AgentRow[];
1112
- return rows.map(rowToAgent);
1113
- },
1114
-
1115
- // Find agents that have a specific skill
1116
- findBySkill(skillId: string): Agent[] {
1117
- // Use json_each to properly search the JSON array (avoids full table scan with LIKE)
1118
- const rows = db.query(
1119
- `SELECT DISTINCT a.* FROM agents a, json_each(a.skills) AS s WHERE s.value = ? ORDER BY a.created_at DESC`
1120
- ).all(skillId) as AgentRow[];
1121
- return rows.map(rowToAgent);
1122
- },
1123
-
1124
- // Delete agent
1125
- delete(id: string): boolean {
1126
- const result = db.run("DELETE FROM agents WHERE id = ?", [id]);
1127
- return result.changes > 0;
1128
- },
1129
-
1130
- // Set agent status (port is permanently assigned, don't change it)
1131
- setStatus(id: string, status: "stopped" | "running"): Agent | null {
1132
- return this.update(id, { status });
1133
- },
1134
-
1135
- // Reset all agents to stopped (on server restart) - keep ports as they're permanent
1136
- resetAllStatus(): void {
1137
- db.run("UPDATE agents SET status = 'stopped'");
1138
- },
1139
-
1140
- // Count agents
1141
- count(): number {
1142
- const row = db.query("SELECT COUNT(*) as count FROM agents").get() as { count: number };
1143
- return row.count;
1144
- },
1145
-
1146
- // Count running agents
1147
- countRunning(): number {
1148
- const row = db.query("SELECT COUNT(*) as count FROM agents WHERE status = 'running'").get() as { count: number };
1149
- return row.count;
1150
- },
1151
-
1152
- // In-memory cache for decrypted API keys (avoids expensive scryptSync on every request)
1153
- _apiKeyCache: new Map<string, string>(),
1154
-
1155
- // Get decrypted API key for an agent (cached)
1156
- getApiKey(id: string): string | null {
1157
- // Check cache first
1158
- const cached = this._apiKeyCache.get(id);
1159
- if (cached) return cached;
1160
-
1161
- const agent = this.findById(id);
1162
- if (!agent || !agent.api_key_encrypted) {
1163
- return null;
1164
- }
1165
- try {
1166
- const key = decrypt(agent.api_key_encrypted);
1167
- if (key) this._apiKeyCache.set(id, key);
1168
- return key;
1169
- } catch {
1170
- return null;
1171
- }
1172
- },
1173
-
1174
- // Regenerate API key for an agent
1175
- regenerateApiKey(id: string): string | null {
1176
- const agent = this.findById(id);
1177
- if (!agent) return null;
1178
-
1179
- const newApiKey = generateAgentApiKey(id);
1180
- const encrypted = encrypt(newApiKey);
1181
- const now = new Date().toISOString();
1182
-
1183
- db.run(
1184
- "UPDATE agents SET api_key_encrypted = ?, updated_at = ? WHERE id = ?",
1185
- [encrypted, now, id]
1186
- );
1187
-
1188
- // Update cache
1189
- this._apiKeyCache.set(id, newApiKey);
1190
- return newApiKey;
1191
- },
1192
-
1193
- // Ensure agent has an API key (for migration of existing agents)
1194
- ensureApiKey(id: string): string | null {
1195
- const agent = this.findById(id);
1196
- if (!agent) return null;
1197
-
1198
- // If agent already has a key, return it
1199
- if (agent.api_key_encrypted) {
1200
- try {
1201
- const key = decrypt(agent.api_key_encrypted);
1202
- if (key) this._apiKeyCache.set(id, key);
1203
- return key;
1204
- } catch {
1205
- // Key is corrupted, regenerate
1206
- }
1207
- }
1208
-
1209
- // Generate new key for agents without one
1210
- return this.regenerateApiKey(id);
1211
- },
1212
- };
1213
-
1214
- // Project CRUD operations
1215
- export const ProjectDB = {
1216
- // Create a new project
1217
- create(project: { name: string; description?: string | null; color?: string }): Project {
1218
- const id = generateId();
1219
- const now = new Date().toISOString();
1220
- const color = project.color || "#6366f1";
1221
-
1222
- db.run(
1223
- `INSERT INTO projects (id, name, description, color, created_at, updated_at)
1224
- VALUES (?, ?, ?, ?, ?, ?)`,
1225
- [id, project.name, project.description || null, color, now, now]
1226
- );
1227
-
1228
- return this.findById(id)!;
1229
- },
1230
-
1231
- // Find project by ID
1232
- findById(id: string): Project | null {
1233
- const row = db.query("SELECT * FROM projects WHERE id = ?").get(id) as ProjectRow | null;
1234
- return row ? rowToProject(row) : null;
1235
- },
1236
-
1237
- // Get all projects
1238
- findAll(): Project[] {
1239
- const rows = db.query("SELECT * FROM projects ORDER BY name ASC").all() as ProjectRow[];
1240
- return rows.map(rowToProject);
1241
- },
1242
-
1243
- // Update project
1244
- update(id: string, updates: Partial<Omit<Project, "id" | "created_at">>): Project | null {
1245
- const project = this.findById(id);
1246
- if (!project) return null;
1247
-
1248
- const fields: string[] = [];
1249
- const values: unknown[] = [];
1250
-
1251
- if (updates.name !== undefined) {
1252
- fields.push("name = ?");
1253
- values.push(updates.name);
1254
- }
1255
- if (updates.description !== undefined) {
1256
- fields.push("description = ?");
1257
- values.push(updates.description);
1258
- }
1259
- if (updates.color !== undefined) {
1260
- fields.push("color = ?");
1261
- values.push(updates.color);
1262
- }
1263
-
1264
- if (fields.length > 0) {
1265
- fields.push("updated_at = ?");
1266
- values.push(new Date().toISOString());
1267
- values.push(id);
1268
-
1269
- db.run(`UPDATE projects SET ${fields.join(", ")} WHERE id = ?`, values);
1270
- }
1271
-
1272
- return this.findById(id);
1273
- },
1274
-
1275
- // Delete project with full cleanup
1276
- // FK constraints handle: agents, mcp_servers, skills (SET NULL), provider_keys (CASCADE)
1277
- // Manual cleanup: subscriptions, test_cases (no FK on project_id)
1278
- delete(id: string): boolean {
1279
- db.run("UPDATE subscriptions SET project_id = NULL WHERE project_id = ?", [id]);
1280
- db.run("UPDATE test_cases SET project_id = NULL WHERE project_id = ?", [id]);
1281
- const result = db.run("DELETE FROM projects WHERE id = ?", [id]);
1282
- return result.changes > 0;
1283
- },
1284
-
1285
- // Count projects
1286
- count(): number {
1287
- const row = db.query("SELECT COUNT(*) as count FROM projects").get() as { count: number };
1288
- return row.count;
1289
- },
1290
-
1291
- // Get agent count per project (excludes meta agent)
1292
- getAgentCounts(): Map<string | null, number> {
1293
- const rows = db.query(`
1294
- SELECT project_id, COUNT(*) as count
1295
- FROM agents
1296
- WHERE id != 'apteva-assistant'
1297
- GROUP BY project_id
1298
- `).all() as { project_id: string | null; count: number }[];
1299
-
1300
- const counts = new Map<string | null, number>();
1301
- for (const row of rows) {
1302
- counts.set(row.project_id, row.count);
1303
- }
1304
- return counts;
1305
- },
1306
- };
1307
-
1308
- // Helper to convert DB row to Project type
1309
- function rowToProject(row: ProjectRow): Project {
1310
- return {
1311
- id: row.id,
1312
- name: row.name,
1313
- description: row.description,
1314
- color: row.color,
1315
- created_at: row.created_at,
1316
- updated_at: row.updated_at,
1317
- };
1318
- }
1319
-
1320
- // Thread CRUD operations
1321
- export const ThreadDB = {
1322
- create(id: string, agentId: string, title?: string): void {
1323
- const now = new Date().toISOString();
1324
- db.run(
1325
- "INSERT INTO threads (id, agent_id, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
1326
- [id, agentId, title || null, now, now]
1327
- );
1328
- },
1329
-
1330
- findById(id: string) {
1331
- return db.query("SELECT * FROM threads WHERE id = ?").get(id);
1332
- },
1333
-
1334
- findByAgent(agentId: string) {
1335
- return db.query("SELECT * FROM threads WHERE agent_id = ? ORDER BY updated_at DESC").all(agentId);
1336
- },
1337
-
1338
- delete(id: string): boolean {
1339
- const result = db.run("DELETE FROM threads WHERE id = ?", [id]);
1340
- return result.changes > 0;
1341
- },
1342
- };
1343
-
1344
- // Message CRUD operations
1345
- export const MessageDB = {
1346
- create(id: string, threadId: string, role: string, content: string): void {
1347
- db.run(
1348
- "INSERT INTO messages (id, thread_id, role, content) VALUES (?, ?, ?, ?)",
1349
- [id, threadId, role, content]
1350
- );
1351
- // Update thread's updated_at
1352
- db.run("UPDATE threads SET updated_at = CURRENT_TIMESTAMP WHERE id = ?", [threadId]);
1353
- },
1354
-
1355
- findByThread(threadId: string) {
1356
- return db.query("SELECT * FROM messages WHERE thread_id = ? ORDER BY created_at ASC").all(threadId);
1357
- },
1358
- };
1359
-
1360
- // Settings operations
1361
- export const SettingsDB = {
1362
- get(key: string): string | null {
1363
- const row = db.query("SELECT value FROM settings WHERE key = ?").get(key) as { value: string } | null;
1364
- return row?.value ?? null;
1365
- },
1366
-
1367
- set(key: string, value: string): void {
1368
- db.run(
1369
- "INSERT INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP",
1370
- [key, value, value]
1371
- );
1372
- },
1373
-
1374
- delete(key: string): boolean {
1375
- const result = db.run("DELETE FROM settings WHERE key = ?", [key]);
1376
- return result.changes > 0;
1377
- },
1378
- };
1379
-
1380
- // Helper to convert DB row to Agent type
1381
- function rowToAgent(row: AgentRow): Agent {
1382
- let features = DEFAULT_FEATURES;
1383
- if (row.features) {
1384
- try {
1385
- features = { ...DEFAULT_FEATURES, ...JSON.parse(row.features) };
1386
- // Strip legacy "mode" from multi-agent config
1387
- if (features.agents && typeof features.agents === "object") {
1388
- const { mode, ...rest } = features.agents as any;
1389
- features.agents = rest;
1390
- }
1391
- } catch {
1392
- // Use defaults if parsing fails
1393
- }
1394
- }
1395
- let mcp_servers: string[] = [];
1396
- if (row.mcp_servers) {
1397
- try {
1398
- mcp_servers = JSON.parse(row.mcp_servers);
1399
- } catch {
1400
- // Use empty array if parsing fails
1401
- }
1402
- }
1403
- let skills: string[] = [];
1404
- if (row.skills) {
1405
- try {
1406
- skills = JSON.parse(row.skills);
1407
- } catch {
1408
- // Use empty array if parsing fails
1409
- }
1410
- }
1411
- return {
1412
- id: row.id,
1413
- name: row.name,
1414
- model: row.model,
1415
- provider: row.provider,
1416
- system_prompt: row.system_prompt,
1417
- status: row.status as "stopped" | "running",
1418
- port: row.port,
1419
- features,
1420
- mcp_servers,
1421
- skills,
1422
- project_id: row.project_id,
1423
- api_key_encrypted: row.api_key_encrypted,
1424
- created_at: row.created_at,
1425
- updated_at: row.updated_at,
1426
- };
1427
- }
1428
-
1429
- // Provider Keys operations
1430
- export const ProviderKeysDB = {
1431
- // Save or update a provider key (project_id: null = global)
1432
- save(providerId: string, encryptedKey: string, keyHint: string, projectId: string | null = null, name: string | null = null): ProviderKey {
1433
- const existing = this.findByProviderAndProject(providerId, projectId);
1434
- const now = new Date().toISOString();
1435
-
1436
- if (existing) {
1437
- db.run(
1438
- "UPDATE provider_keys SET encrypted_key = ?, key_hint = ?, name = ?, is_valid = 1, last_tested_at = NULL, created_at = ? WHERE id = ?",
1439
- [encryptedKey, keyHint, name, now, existing.id]
1440
- );
1441
- return this.findById(existing.id)!;
1442
- } else {
1443
- const id = generateId();
1444
- db.run(
1445
- "INSERT INTO provider_keys (id, provider_id, encrypted_key, key_hint, is_valid, created_at, project_id, name) VALUES (?, ?, ?, ?, 1, ?, ?, ?)",
1446
- [id, providerId, encryptedKey, keyHint, now, projectId, name]
1447
- );
1448
- return this.findById(id)!;
1449
- }
1450
- },
1451
-
1452
- // Find key by ID
1453
- findById(id: string): ProviderKey | null {
1454
- const row = db.query("SELECT * FROM provider_keys WHERE id = ?").get(id) as ProviderKeyRow | null;
1455
- return row ? rowToProviderKey(row) : null;
1456
- },
1457
-
1458
- // Find key by provider (global only - for backwards compatibility)
1459
- findByProvider(providerId: string): ProviderKey | null {
1460
- const row = db.query("SELECT * FROM provider_keys WHERE provider_id = ? AND project_id IS NULL").get(providerId) as ProviderKeyRow | null;
1461
- return row ? rowToProviderKey(row) : null;
1462
- },
1463
-
1464
- // Find key by provider and project
1465
- findByProviderAndProject(providerId: string, projectId: string | null): ProviderKey | null {
1466
- const row = projectId
1467
- ? db.query("SELECT * FROM provider_keys WHERE provider_id = ? AND project_id = ?").get(providerId, projectId) as ProviderKeyRow | null
1468
- : db.query("SELECT * FROM provider_keys WHERE provider_id = ? AND project_id IS NULL").get(providerId) as ProviderKeyRow | null;
1469
- return row ? rowToProviderKey(row) : null;
1470
- },
1471
-
1472
- // Find all keys for a provider (global + all projects)
1473
- findAllByProvider(providerId: string): ProviderKey[] {
1474
- const rows = db.query("SELECT * FROM provider_keys WHERE provider_id = ? ORDER BY (project_id IS NOT NULL), created_at DESC").all(providerId) as ProviderKeyRow[];
1475
- return rows.map(rowToProviderKey);
1476
- },
1477
-
1478
- // Find all keys for a project
1479
- findByProject(projectId: string): ProviderKey[] {
1480
- const rows = db.query("SELECT * FROM provider_keys WHERE project_id = ? ORDER BY provider_id, created_at DESC").all(projectId) as ProviderKeyRow[];
1481
- return rows.map(rowToProviderKey);
1482
- },
1483
-
1484
- // Get all provider keys
1485
- findAll(): ProviderKey[] {
1486
- const rows = db.query("SELECT * FROM provider_keys ORDER BY provider_id, (project_id IS NOT NULL), created_at DESC").all() as ProviderKeyRow[];
1487
- return rows.map(rowToProviderKey);
1488
- },
1489
-
1490
- // Get list of provider IDs that have keys configured (global keys only for backwards compat)
1491
- getConfiguredProviders(): string[] {
1492
- const rows = db.query("SELECT DISTINCT provider_id FROM provider_keys WHERE project_id IS NULL").all() as { provider_id: string }[];
1493
- return rows.map(r => r.provider_id);
1494
- },
1495
-
1496
- // Get list of provider IDs that have keys configured (including project-scoped)
1497
- getAllConfiguredProviders(): string[] {
1498
- const rows = db.query("SELECT DISTINCT provider_id FROM provider_keys").all() as { provider_id: string }[];
1499
- return rows.map(r => r.provider_id);
1500
- },
1501
-
1502
- // Update validity status after testing
1503
- setValidity(id: string, isValid: boolean): void {
1504
- db.run(
1505
- "UPDATE provider_keys SET is_valid = ?, last_tested_at = ? WHERE id = ?",
1506
- [isValid ? 1 : 0, new Date().toISOString(), id]
1507
- );
1508
- },
1509
-
1510
- // Delete a provider key by ID
1511
- deleteById(id: string): boolean {
1512
- const result = db.run("DELETE FROM provider_keys WHERE id = ?", [id]);
1513
- return result.changes > 0;
1514
- },
1515
-
1516
- // Delete a provider key (global only - for backwards compatibility)
1517
- delete(providerId: string): boolean {
1518
- const result = db.run("DELETE FROM provider_keys WHERE provider_id = ? AND project_id IS NULL", [providerId]);
1519
- return result.changes > 0;
1520
- },
1521
-
1522
- // Delete provider key by provider and project
1523
- deleteByProviderAndProject(providerId: string, projectId: string | null): boolean {
1524
- const result = projectId
1525
- ? db.run("DELETE FROM provider_keys WHERE provider_id = ? AND project_id = ?", [providerId, projectId])
1526
- : db.run("DELETE FROM provider_keys WHERE provider_id = ? AND project_id IS NULL", [providerId]);
1527
- return result.changes > 0;
1528
- },
1529
-
1530
- // Check if any keys are configured
1531
- hasAnyKeys(): boolean {
1532
- const row = db.query("SELECT COUNT(*) as count FROM provider_keys").get() as { count: number };
1533
- return row.count > 0;
1534
- },
1535
-
1536
- // Count configured providers
1537
- count(): number {
1538
- const row = db.query("SELECT COUNT(*) as count FROM provider_keys").get() as { count: number };
1539
- return row.count;
1540
- },
1541
- };
1542
-
1543
- // Helper to convert DB row to ProviderKey type
1544
- function rowToProviderKey(row: ProviderKeyRow): ProviderKey {
1545
- return {
1546
- id: row.id,
1547
- provider_id: row.provider_id,
1548
- encrypted_key: row.encrypted_key,
1549
- key_hint: row.key_hint,
1550
- is_valid: row.is_valid === 1,
1551
- last_tested_at: row.last_tested_at,
1552
- created_at: row.created_at,
1553
- project_id: row.project_id,
1554
- name: row.name,
1555
- };
1556
- }
1557
-
1558
- // MCP Server operations
1559
- export const McpServerDB = {
1560
- // Get the next available port for a new MCP server (starting from 4500)
1561
- getNextAvailablePort(): number {
1562
- const BASE_PORT = 4500;
1563
- const row = db.query("SELECT MAX(port) as max_port FROM mcp_servers").get() as { max_port: number | null };
1564
- if (row.max_port === null) {
1565
- return BASE_PORT;
1566
- }
1567
- return row.max_port + 1;
1568
- },
1569
-
1570
- create(server: Omit<McpServer, "created_at" | "status" | "port">): McpServer {
1571
- const now = new Date().toISOString();
1572
- // Encrypt env vars and headers (credentials) before storing
1573
- const envEncrypted = encryptObject(server.env || {});
1574
- const headersEncrypted = encryptObject(server.headers || {});
1575
- // Assign port permanently at creation time (like agents)
1576
- // HTTP and local servers don't need a local proxy port
1577
- const port = (server.type === "http" || server.type === "local") ? null : this.getNextAvailablePort();
1578
- console.log(`[McpServerDB.create] id=${server.id} name=${server.name} type=${server.type} source=${server.source} project_id=${server.project_id} url=${server.url?.substring(0, 60)}`);
1579
- const stmt = db.prepare(`
1580
- INSERT INTO mcp_servers (id, name, type, package, pip_module, command, args, env, url, headers, source, project_id, status, port, created_at)
1581
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'stopped', ?, ?)
1582
- `);
1583
- stmt.run(
1584
- server.id, server.name, server.type, server.package, server.pip_module || null, server.command, server.args,
1585
- envEncrypted, server.url || null, headersEncrypted, server.source || null, server.project_id || null, port, now
1586
- );
1587
- const created = this.findById(server.id);
1588
- console.log(`[McpServerDB.create] findById after INSERT: ${created ? `found id=${created.id} project_id=${created.project_id}` : "NOT FOUND"}`);
1589
- if (!created) {
1590
- console.error(`[McpServerDB.create] CRITICAL: INSERT succeeded but findById returned null for id=${server.id}`);
1591
- }
1592
- return created!;
1593
- },
1594
-
1595
- findById(id: string): McpServer | null {
1596
- const row = db.query("SELECT * FROM mcp_servers WHERE id = ?").get(id) as McpServerRow | null;
1597
- return row ? rowToMcpServer(row) : null;
1598
- },
1599
-
1600
- findByIds(ids: string[]): Map<string, McpServer> {
1601
- if (ids.length === 0) return new Map();
1602
- const placeholders = ids.map(() => "?").join(",");
1603
- const rows = db.query(`SELECT * FROM mcp_servers WHERE id IN (${placeholders})`).all(...ids) as McpServerRow[];
1604
- const map = new Map<string, McpServer>();
1605
- for (const row of rows) map.set(row.id, rowToMcpServer(row));
1606
- return map;
1607
- },
1608
-
1609
- findAll(): McpServer[] {
1610
- const rows = db.query("SELECT * FROM mcp_servers ORDER BY created_at DESC").all() as McpServerRow[];
1611
- return rows.map(rowToMcpServer);
1612
- },
1613
-
1614
- // Light version: skips expensive decryption for listing endpoints
1615
- findAllLight(): McpServer[] {
1616
- const rows = db.query("SELECT * FROM mcp_servers ORDER BY created_at DESC").all() as McpServerRow[];
1617
- return rows.map(rowToMcpServerLight);
1618
- },
1619
-
1620
- // Light batch load by IDs: skips decryption (used by toApiAgentsBatch)
1621
- findByIdsLight(ids: string[]): Map<string, McpServer> {
1622
- if (ids.length === 0) return new Map();
1623
- const placeholders = ids.map(() => "?").join(",");
1624
- const rows = db.query(`SELECT * FROM mcp_servers WHERE id IN (${placeholders})`).all(...ids) as McpServerRow[];
1625
- const map = new Map<string, McpServer>();
1626
- for (const row of rows) map.set(row.id, rowToMcpServerLight(row));
1627
- return map;
1628
- },
1629
-
1630
- findRunning(): McpServer[] {
1631
- const rows = db.query("SELECT * FROM mcp_servers WHERE status = 'running'").all() as McpServerRow[];
1632
- return rows.map(rowToMcpServer);
1633
- },
1634
-
1635
- update(id: string, updates: Partial<Omit<McpServer, "id" | "created_at">>): McpServer | null {
1636
- const server = this.findById(id);
1637
- if (!server) return null;
1638
-
1639
- const fields: string[] = [];
1640
- const values: unknown[] = [];
1641
-
1642
- if (updates.name !== undefined) {
1643
- fields.push("name = ?");
1644
- values.push(updates.name);
1645
- }
1646
- if (updates.type !== undefined) {
1647
- fields.push("type = ?");
1648
- values.push(updates.type);
1649
- }
1650
- if (updates.package !== undefined) {
1651
- fields.push("package = ?");
1652
- values.push(updates.package);
1653
- }
1654
- if (updates.pip_module !== undefined) {
1655
- fields.push("pip_module = ?");
1656
- values.push(updates.pip_module);
1657
- }
1658
- if (updates.command !== undefined) {
1659
- fields.push("command = ?");
1660
- values.push(updates.command);
1661
- }
1662
- if (updates.args !== undefined) {
1663
- fields.push("args = ?");
1664
- values.push(updates.args);
1665
- }
1666
- if (updates.env !== undefined) {
1667
- fields.push("env = ?");
1668
- // Encrypt env vars (credentials) before storing
1669
- values.push(encryptObject(updates.env));
1670
- }
1671
- if (updates.url !== undefined) {
1672
- fields.push("url = ?");
1673
- values.push(updates.url);
1674
- }
1675
- if (updates.headers !== undefined) {
1676
- fields.push("headers = ?");
1677
- // Encrypt headers (may contain auth tokens) before storing
1678
- values.push(encryptObject(updates.headers));
1679
- }
1680
- if (updates.source !== undefined) {
1681
- fields.push("source = ?");
1682
- values.push(updates.source);
1683
- }
1684
- if (updates.project_id !== undefined) {
1685
- fields.push("project_id = ?");
1686
- values.push(updates.project_id);
1687
- }
1688
- if (updates.port !== undefined) {
1689
- fields.push("port = ?");
1690
- values.push(updates.port);
1691
- }
1692
- if (updates.status !== undefined) {
1693
- fields.push("status = ?");
1694
- values.push(updates.status);
1695
- }
1696
-
1697
- if (fields.length > 0) {
1698
- values.push(id);
1699
- db.run(`UPDATE mcp_servers SET ${fields.join(", ")} WHERE id = ?`, values);
1700
- }
1701
-
1702
- return this.findById(id);
1703
- },
1704
-
1705
- setStatus(id: string, status: "stopped" | "running", port?: number): McpServer | null {
1706
- // Port is permanently assigned — only update if explicitly provided
1707
- const updates: Partial<Omit<McpServer, "id" | "created_at">> = { status };
1708
- if (port !== undefined) {
1709
- updates.port = port;
1710
- }
1711
- return this.update(id, updates);
1712
- },
1713
-
1714
- delete(id: string): boolean {
1715
- const result = db.run("DELETE FROM mcp_servers WHERE id = ?", [id]);
1716
- return result.changes > 0;
1717
- },
1718
-
1719
- resetAllStatus(): void {
1720
- // Keep ports as they're permanently assigned (like agents)
1721
- db.run("UPDATE mcp_servers SET status = 'stopped'");
1722
- },
1723
-
1724
- count(): number {
1725
- const row = db.query("SELECT COUNT(*) as count FROM mcp_servers").get() as { count: number };
1726
- return row.count;
1727
- },
1728
-
1729
- // Find servers by project (null = global only)
1730
- findByProject(projectId: string | null): McpServer[] {
1731
- if (projectId === null) {
1732
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1733
- return rows.map(rowToMcpServer);
1734
- }
1735
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as McpServerRow[];
1736
- return rows.map(rowToMcpServer);
1737
- },
1738
-
1739
- // Find servers available for an agent (global + agent's project)
1740
- findForAgent(agentProjectId: string | null): McpServer[] {
1741
- if (agentProjectId === null) {
1742
- // Agent has no project, only show global servers
1743
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1744
- return rows.map(rowToMcpServer);
1745
- }
1746
- // Agent has a project, show global + project servers
1747
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL OR project_id = ? ORDER BY created_at DESC").all(agentProjectId) as McpServerRow[];
1748
- return rows.map(rowToMcpServer);
1749
- },
1750
-
1751
- // Find global servers only
1752
- findGlobal(): McpServer[] {
1753
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1754
- return rows.map(rowToMcpServer);
1755
- },
1756
-
1757
- // Light versions (skip decryption) for listing endpoints
1758
- findByProjectLight(projectId: string | null): McpServer[] {
1759
- if (projectId === null) {
1760
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1761
- return rows.map(rowToMcpServerLight);
1762
- }
1763
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as McpServerRow[];
1764
- return rows.map(rowToMcpServerLight);
1765
- },
1766
-
1767
- findForAgentLight(agentProjectId: string | null): McpServer[] {
1768
- if (agentProjectId === null) {
1769
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1770
- return rows.map(rowToMcpServerLight);
1771
- }
1772
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL OR project_id = ? ORDER BY created_at DESC").all(agentProjectId) as McpServerRow[];
1773
- return rows.map(rowToMcpServerLight);
1774
- },
1775
-
1776
- findGlobalLight(): McpServer[] {
1777
- const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1778
- return rows.map(rowToMcpServerLight);
1779
- },
1780
- };
1781
-
1782
- // MCP Server Tool CRUD operations (for local servers)
1783
- export const McpServerToolDB = {
1784
- create(tool: Omit<McpServerTool, "created_at">): McpServerTool {
1785
- const now = new Date().toISOString();
1786
- db.run(
1787
- `INSERT INTO mcp_server_tools (id, server_id, name, description, input_schema, handler_type, mock_response, http_config, code, enabled, created_at)
1788
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1789
- [
1790
- tool.id,
1791
- tool.server_id,
1792
- tool.name,
1793
- tool.description,
1794
- JSON.stringify(tool.input_schema || {}),
1795
- tool.handler_type || "mock",
1796
- tool.mock_response ? JSON.stringify(tool.mock_response) : null,
1797
- tool.http_config ? JSON.stringify(tool.http_config) : null,
1798
- tool.code || null,
1799
- tool.enabled ? 1 : 0,
1800
- now,
1801
- ],
1802
- );
1803
- return this.findById(tool.id)!;
1804
- },
1805
-
1806
- findById(id: string): McpServerTool | null {
1807
- const row = db.query("SELECT * FROM mcp_server_tools WHERE id = ?").get(id) as McpServerToolRow | null;
1808
- return row ? rowToMcpServerTool(row) : null;
1809
- },
1810
-
1811
- findByServer(serverId: string): McpServerTool[] {
1812
- const rows = db.query(
1813
- "SELECT * FROM mcp_server_tools WHERE server_id = ? ORDER BY created_at ASC",
1814
- ).all(serverId) as McpServerToolRow[];
1815
- return rows.map(rowToMcpServerTool);
1816
- },
1817
-
1818
- findByServerAndName(serverId: string, name: string): McpServerTool | null {
1819
- const row = db.query(
1820
- "SELECT * FROM mcp_server_tools WHERE server_id = ? AND name = ?",
1821
- ).get(serverId, name) as McpServerToolRow | null;
1822
- return row ? rowToMcpServerTool(row) : null;
1823
- },
1824
-
1825
- update(id: string, updates: Partial<Omit<McpServerTool, "id" | "server_id" | "created_at">>): McpServerTool | null {
1826
- const tool = this.findById(id);
1827
- if (!tool) return null;
1828
-
1829
- const fields: string[] = [];
1830
- const values: unknown[] = [];
1831
-
1832
- if (updates.name !== undefined) { fields.push("name = ?"); values.push(updates.name); }
1833
- if (updates.description !== undefined) { fields.push("description = ?"); values.push(updates.description); }
1834
- if (updates.input_schema !== undefined) { fields.push("input_schema = ?"); values.push(JSON.stringify(updates.input_schema)); }
1835
- if (updates.handler_type !== undefined) { fields.push("handler_type = ?"); values.push(updates.handler_type); }
1836
- if (updates.mock_response !== undefined) { fields.push("mock_response = ?"); values.push(updates.mock_response ? JSON.stringify(updates.mock_response) : null); }
1837
- if (updates.http_config !== undefined) { fields.push("http_config = ?"); values.push(updates.http_config ? JSON.stringify(updates.http_config) : null); }
1838
- if (updates.code !== undefined) { fields.push("code = ?"); values.push(updates.code); }
1839
- if (updates.enabled !== undefined) { fields.push("enabled = ?"); values.push(updates.enabled ? 1 : 0); }
1840
-
1841
- if (fields.length > 0) {
1842
- values.push(id);
1843
- db.run(`UPDATE mcp_server_tools SET ${fields.join(", ")} WHERE id = ?`, values);
1844
- }
1845
- return this.findById(id);
1846
- },
1847
-
1848
- delete(id: string): boolean {
1849
- const result = db.run("DELETE FROM mcp_server_tools WHERE id = ?", [id]);
1850
- return result.changes > 0;
1851
- },
1852
-
1853
- deleteByServer(serverId: string): number {
1854
- const result = db.run("DELETE FROM mcp_server_tools WHERE server_id = ?", [serverId]);
1855
- return result.changes;
1856
- },
1857
-
1858
- count(serverId: string): number {
1859
- const row = db.query("SELECT COUNT(*) as count FROM mcp_server_tools WHERE server_id = ?").get(serverId) as { count: number };
1860
- return row.count;
1861
- },
1862
- };
1863
-
1864
- function rowToMcpServerTool(row: McpServerToolRow): McpServerTool {
1865
- let input_schema: Record<string, any> = {};
1866
- try { input_schema = JSON.parse(row.input_schema); } catch { /* */ }
1867
- let mock_response: Record<string, any> | null = null;
1868
- if (row.mock_response) { try { mock_response = JSON.parse(row.mock_response); } catch { /* */ } }
1869
- let http_config: McpServerTool["http_config"] = null;
1870
- if (row.http_config) { try { http_config = JSON.parse(row.http_config); } catch { /* */ } }
1871
-
1872
- return {
1873
- id: row.id,
1874
- server_id: row.server_id,
1875
- name: row.name,
1876
- description: row.description,
1877
- input_schema,
1878
- handler_type: row.handler_type as McpServerTool["handler_type"],
1879
- mock_response,
1880
- http_config,
1881
- code: row.code,
1882
- enabled: row.enabled === 1,
1883
- created_at: row.created_at,
1884
- };
1885
- }
1886
-
1887
- // Helper to convert DB row to McpServer type
1888
- function rowToMcpServer(row: McpServerRow): McpServer {
1889
- // Decrypt env vars and headers (handles both encrypted and legacy unencrypted data)
1890
- const env = row.env ? decryptObject(row.env) : {};
1891
- const headers = row.headers ? decryptObject(row.headers) : {};
1892
- return {
1893
- id: row.id,
1894
- name: row.name,
1895
- type: row.type as McpServer["type"],
1896
- package: row.package,
1897
- pip_module: row.pip_module,
1898
- command: row.command,
1899
- args: row.args,
1900
- env,
1901
- url: row.url,
1902
- headers,
1903
- port: row.port,
1904
- status: row.status as "stopped" | "running",
1905
- source: row.source,
1906
- project_id: row.project_id,
1907
- created_at: row.created_at,
1908
- };
1909
- }
1910
-
1911
- // Light version: skips expensive decryption of env/headers for listing endpoints
1912
- function rowToMcpServerLight(row: McpServerRow): McpServer {
1913
- return {
1914
- id: row.id,
1915
- name: row.name,
1916
- type: row.type as McpServer["type"],
1917
- package: row.package,
1918
- pip_module: row.pip_module,
1919
- command: row.command,
1920
- args: row.args,
1921
- env: {},
1922
- url: row.url,
1923
- headers: {},
1924
- port: row.port,
1925
- status: row.status as "stopped" | "running",
1926
- source: row.source,
1927
- project_id: row.project_id,
1928
- created_at: row.created_at,
1929
- };
1930
- }
1931
-
1932
- // Telemetry Event types
1933
- // User types
1934
- export interface User {
1935
- id: string;
1936
- username: string;
1937
- password_hash: string;
1938
- email: string | null; // Optional, for password recovery only
1939
- role: "admin" | "user";
1940
- created_at: string;
1941
- updated_at: string;
1942
- last_login_at: string | null;
1943
- }
1944
-
1945
- export interface UserRow {
1946
- id: string;
1947
- username: string;
1948
- password_hash: string;
1949
- email: string | null;
1950
- role: string;
1951
- created_at: string;
1952
- updated_at: string;
1953
- last_login_at: string | null;
1954
- }
1955
-
1956
- export interface Session {
1957
- id: string;
1958
- user_id: string;
1959
- refresh_token_hash: string;
1960
- expires_at: string;
1961
- created_at: string;
1962
- }
1963
-
1964
- export interface SessionRow {
1965
- id: string;
1966
- user_id: string;
1967
- refresh_token_hash: string;
1968
- expires_at: string;
1969
- created_at: string;
1970
- }
1971
-
1972
- export interface TelemetryEvent {
1973
- id: string;
1974
- agent_id: string;
1975
- timestamp: string;
1976
- category: string;
1977
- type: string;
1978
- level: string;
1979
- trace_id: string | null;
1980
- span_id: string | null;
1981
- thread_id: string | null;
1982
- data: Record<string, unknown> | null;
1983
- metadata: Record<string, unknown> | null;
1984
- duration_ms: number | null;
1985
- error: string | null;
1986
- received_at: string;
1987
- seen?: boolean;
1988
- }
1989
-
1990
- interface TelemetryEventRow {
1991
- id: string;
1992
- agent_id: string;
1993
- timestamp: string;
1994
- category: string;
1995
- type: string;
1996
- level: string;
1997
- trace_id: string | null;
1998
- span_id: string | null;
1999
- thread_id: string | null;
2000
- data: string | null;
2001
- metadata: string | null;
2002
- duration_ms: number | null;
2003
- error: string | null;
2004
- received_at: string;
2005
- seen?: number;
2006
- }
2007
-
2008
- // Telemetry operations
2009
- export const TelemetryDB = {
2010
- // Insert batch of events
2011
- insertBatch(agentId: string, events: Array<{
2012
- id: string;
2013
- timestamp: string;
2014
- category: string;
2015
- type: string;
2016
- level: string;
2017
- trace_id?: string;
2018
- span_id?: string;
2019
- thread_id?: string;
2020
- data?: Record<string, unknown>;
2021
- metadata?: Record<string, unknown>;
2022
- duration_ms?: number;
2023
- error?: string;
2024
- cost?: number;
2025
- }>): number {
2026
- const now = new Date().toISOString();
2027
- const stmt = db.prepare(`
2028
- INSERT OR IGNORE INTO telemetry_events
2029
- (id, agent_id, timestamp, category, type, level, trace_id, span_id, thread_id, data, metadata, duration_ms, error, received_at, cost)
2030
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2031
- `);
2032
-
2033
- // Wrap in transaction for massive speedup (single fsync instead of one per row)
2034
- let inserted = 0;
2035
- const insertAll = db.transaction(() => {
2036
- for (const event of events) {
2037
- const result = stmt.run(
2038
- event.id,
2039
- agentId,
2040
- event.timestamp,
2041
- event.category,
2042
- event.type,
2043
- event.level,
2044
- event.trace_id || null,
2045
- event.span_id || null,
2046
- event.thread_id || null,
2047
- event.data ? JSON.stringify(event.data) : null,
2048
- event.metadata ? JSON.stringify(event.metadata) : null,
2049
- event.duration_ms || null,
2050
- event.error || null,
2051
- now,
2052
- event.cost || 0
2053
- );
2054
- if (result.changes > 0) inserted++;
2055
- }
2056
- });
2057
- insertAll();
2058
- return inserted;
2059
- },
2060
-
2061
- // Query events with filters
2062
- query(filters: {
2063
- agent_id?: string;
2064
- project_id?: string | null; // Filter by project (null = unassigned agents)
2065
- category?: string;
2066
- type?: string;
2067
- level?: string;
2068
- trace_id?: string;
2069
- since?: string;
2070
- until?: string;
2071
- limit?: number;
2072
- offset?: number;
2073
- } = {}): TelemetryEvent[] {
2074
- const conditions: string[] = [];
2075
- const params: unknown[] = [];
2076
-
2077
- if (filters.agent_id) {
2078
- conditions.push("t.agent_id = ?");
2079
- params.push(filters.agent_id);
2080
- }
2081
- if (filters.project_id !== undefined) {
2082
- if (filters.project_id === null) {
2083
- conditions.push("a.project_id IS NULL");
2084
- } else {
2085
- conditions.push("a.project_id = ?");
2086
- params.push(filters.project_id);
2087
- }
2088
- }
2089
- if (filters.category) {
2090
- conditions.push("t.category = ?");
2091
- params.push(filters.category);
2092
- }
2093
- if (filters.type) {
2094
- conditions.push("t.type = ?");
2095
- params.push(filters.type);
2096
- }
2097
- if (filters.level) {
2098
- conditions.push("t.level = ?");
2099
- params.push(filters.level);
2100
- }
2101
- if (filters.trace_id) {
2102
- conditions.push("t.trace_id = ?");
2103
- params.push(filters.trace_id);
2104
- }
2105
- if (filters.since) {
2106
- conditions.push("t.timestamp >= ?");
2107
- params.push(filters.since);
2108
- }
2109
- if (filters.until) {
2110
- conditions.push("t.timestamp <= ?");
2111
- params.push(filters.until);
2112
- }
2113
-
2114
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2115
- const limit = filters.limit || 100;
2116
- const offset = filters.offset || 0;
2117
-
2118
- // Join with agents table when filtering by project
2119
- const needsJoin = filters.project_id !== undefined;
2120
- const sql = needsJoin
2121
- ? `SELECT t.* FROM telemetry_events t JOIN agents a ON t.agent_id = a.id ${where} ORDER BY t.timestamp DESC LIMIT ? OFFSET ?`
2122
- : `SELECT * FROM telemetry_events t ${where} ORDER BY t.timestamp DESC LIMIT ? OFFSET ?`;
2123
- params.push(limit, offset);
2124
-
2125
- const rows = db.query(sql).all(...params) as TelemetryEventRow[];
2126
- return rows.map(rowToTelemetryEvent);
2127
- },
2128
-
2129
- // Get usage stats
2130
- getUsage(filters: {
2131
- agent_id?: string;
2132
- project_id?: string | null;
2133
- since?: string;
2134
- until?: string;
2135
- group_by?: "agent" | "day" | "project";
2136
- } = {}): Array<{
2137
- agent_id?: string;
2138
- project_id?: string;
2139
- date?: string;
2140
- input_tokens: number;
2141
- output_tokens: number;
2142
- cache_creation_tokens: number;
2143
- cache_read_tokens: number;
2144
- reasoning_tokens: number;
2145
- llm_calls: number;
2146
- tool_calls: number;
2147
- errors: number;
2148
- cost: number;
2149
- }> {
2150
- const conditions: string[] = [];
2151
- const params: unknown[] = [];
2152
- const needsJoin = filters.project_id !== undefined;
2153
-
2154
- if (filters.agent_id) {
2155
- conditions.push("t.agent_id = ?");
2156
- params.push(filters.agent_id);
2157
- }
2158
- if (filters.project_id !== undefined) {
2159
- if (filters.project_id === null) {
2160
- conditions.push("a.project_id IS NULL");
2161
- } else {
2162
- conditions.push("a.project_id = ?");
2163
- params.push(filters.project_id);
2164
- }
2165
- }
2166
- if (filters.since) {
2167
- conditions.push("t.timestamp >= ?");
2168
- params.push(filters.since);
2169
- }
2170
- if (filters.until) {
2171
- conditions.push("t.timestamp <= ?");
2172
- params.push(filters.until);
2173
- }
2174
-
2175
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2176
-
2177
- let groupBy = "";
2178
- let selectFields = "";
2179
-
2180
- if (filters.group_by === "day") {
2181
- groupBy = "GROUP BY date(t.timestamp)";
2182
- selectFields = "date(t.timestamp) as date,";
2183
- } else if (filters.group_by === "agent") {
2184
- groupBy = "GROUP BY t.agent_id";
2185
- selectFields = "t.agent_id as agent_id,";
2186
- } else if (filters.group_by === "project") {
2187
- groupBy = "GROUP BY a.project_id";
2188
- selectFields = "a.project_id as project_id,";
2189
- }
2190
-
2191
- const needsProjectJoin = needsJoin || filters.group_by === "project";
2192
- const fromClause = needsProjectJoin
2193
- ? "FROM telemetry_events t JOIN agents a ON t.agent_id = a.id"
2194
- : "FROM telemetry_events t";
2195
-
2196
- const sql = `
2197
- SELECT
2198
- ${selectFields}
2199
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.input_tokens') ELSE 0 END), 0) as input_tokens,
2200
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.output_tokens') ELSE 0 END), 0) as output_tokens,
2201
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.cache_creation_tokens') ELSE 0 END), 0) as cache_creation_tokens,
2202
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.cache_read_tokens') ELSE 0 END), 0) as cache_read_tokens,
2203
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.reasoning_tokens') ELSE 0 END), 0) as reasoning_tokens,
2204
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN 1 ELSE 0 END), 0) as llm_calls,
2205
- COALESCE(SUM(CASE WHEN t.category = 'TOOL' THEN 1 ELSE 0 END), 0) as tool_calls,
2206
- COALESCE(SUM(CASE WHEN t.level = 'error' THEN 1 ELSE 0 END), 0) as errors,
2207
- COALESCE(SUM(t.cost), 0) as cost
2208
- ${fromClause}
2209
- ${where}
2210
- ${groupBy}
2211
- `;
2212
-
2213
- return db.query(sql).all(...params) as Array<{
2214
- agent_id?: string;
2215
- project_id?: string;
2216
- date?: string;
2217
- input_tokens: number;
2218
- output_tokens: number;
2219
- cache_creation_tokens: number;
2220
- cache_read_tokens: number;
2221
- reasoning_tokens: number;
2222
- llm_calls: number;
2223
- tool_calls: number;
2224
- errors: number;
2225
- cost: number;
2226
- }>;
2227
- },
2228
-
2229
- // Get summary stats
2230
- getStats(filters: { agentId?: string; projectId?: string | null; since?: string; until?: string } = {}): {
2231
- total_events: number;
2232
- total_llm_calls: number;
2233
- total_tool_calls: number;
2234
- total_errors: number;
2235
- total_input_tokens: number;
2236
- total_output_tokens: number;
2237
- total_cache_creation_tokens: number;
2238
- total_cache_read_tokens: number;
2239
- total_reasoning_tokens: number;
2240
- total_cost: number;
2241
- } {
2242
- const conditions: string[] = [];
2243
- const params: unknown[] = [];
2244
- const needsJoin = filters.projectId !== undefined;
2245
-
2246
- if (filters.agentId) {
2247
- conditions.push("t.agent_id = ?");
2248
- params.push(filters.agentId);
2249
- }
2250
- if (filters.projectId !== undefined) {
2251
- if (filters.projectId === null) {
2252
- conditions.push("a.project_id IS NULL");
2253
- } else {
2254
- conditions.push("a.project_id = ?");
2255
- params.push(filters.projectId);
2256
- }
2257
- }
2258
- if (filters.since) {
2259
- conditions.push("t.timestamp >= ?");
2260
- params.push(filters.since);
2261
- }
2262
- if (filters.until) {
2263
- conditions.push("t.timestamp <= ?");
2264
- params.push(filters.until);
2265
- }
2266
-
2267
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2268
- const fromClause = needsJoin
2269
- ? "FROM telemetry_events t JOIN agents a ON t.agent_id = a.id"
2270
- : "FROM telemetry_events t";
2271
-
2272
- const sql = `
2273
- SELECT
2274
- COUNT(*) as total_events,
2275
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN 1 ELSE 0 END), 0) as total_llm_calls,
2276
- COALESCE(SUM(CASE WHEN t.category = 'TOOL' THEN 1 ELSE 0 END), 0) as total_tool_calls,
2277
- COALESCE(SUM(CASE WHEN t.level = 'error' THEN 1 ELSE 0 END), 0) as total_errors,
2278
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.input_tokens') ELSE 0 END), 0) as total_input_tokens,
2279
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.output_tokens') ELSE 0 END), 0) as total_output_tokens,
2280
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.cache_creation_tokens') ELSE 0 END), 0) as total_cache_creation_tokens,
2281
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.cache_read_tokens') ELSE 0 END), 0) as total_cache_read_tokens,
2282
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.reasoning_tokens') ELSE 0 END), 0) as total_reasoning_tokens,
2283
- COALESCE(SUM(t.cost), 0) as total_cost
2284
- ${fromClause}
2285
- ${where}
2286
- `;
2287
-
2288
- return db.query(sql).get(...params) as {
2289
- total_events: number;
2290
- total_llm_calls: number;
2291
- total_tool_calls: number;
2292
- total_errors: number;
2293
- total_input_tokens: number;
2294
- total_output_tokens: number;
2295
- total_cache_creation_tokens: number;
2296
- total_cache_read_tokens: number;
2297
- total_reasoning_tokens: number;
2298
- total_cost: number;
2299
- };
2300
- },
2301
-
2302
- // Delete old events (retention)
2303
- deleteOlderThan(days: number): number {
2304
- const cutoff = new Date();
2305
- cutoff.setDate(cutoff.getDate() - days);
2306
- const result = db.run(
2307
- "DELETE FROM telemetry_events WHERE timestamp < ?",
2308
- [cutoff.toISOString()]
2309
- );
2310
- return result.changes;
2311
- },
2312
-
2313
- // Delete all events for an agent
2314
- deleteByAgent(agentId: string): number {
2315
- const result = db.run(
2316
- "DELETE FROM telemetry_events WHERE agent_id = ?",
2317
- [agentId]
2318
- );
2319
- return result.changes;
2320
- },
2321
-
2322
- // Count events
2323
- count(agentId?: string): number {
2324
- if (agentId) {
2325
- const row = db.query("SELECT COUNT(*) as count FROM telemetry_events WHERE agent_id = ?").get(agentId) as { count: number };
2326
- return row.count;
2327
- }
2328
- const row = db.query("SELECT COUNT(*) as count FROM telemetry_events").get() as { count: number };
2329
- return row.count;
2330
- },
2331
-
2332
- // --- Notification helpers (piggyback on telemetry with `seen` flag) ---
2333
-
2334
- // Notification-worthy filter: errors + agent crashes
2335
- getNotifications(limit = 50): TelemetryEvent[] {
2336
- const rows = db.query(`
2337
- SELECT * FROM telemetry_events
2338
- WHERE (level = 'error' OR (category = 'system' AND type = 'agent_stopped') OR category = 'ERROR')
2339
- ORDER BY timestamp DESC
2340
- LIMIT ?
2341
- `).all(limit) as TelemetryEventRow[];
2342
- return rows.map(rowToTelemetryEvent);
2343
- },
2344
-
2345
- getUnseenCount(): number {
2346
- const row = db.query(`
2347
- SELECT COUNT(*) as count FROM telemetry_events
2348
- WHERE seen = 0
2349
- AND (level = 'error' OR (category = 'system' AND type = 'agent_stopped') OR category = 'ERROR')
2350
- `).get() as { count: number };
2351
- return row.count;
2352
- },
2353
-
2354
- markSeen(ids: string[]): number {
2355
- if (ids.length === 0) return 0;
2356
- const placeholders = ids.map(() => "?").join(",");
2357
- const result = db.run(
2358
- `UPDATE telemetry_events SET seen = 1 WHERE id IN (${placeholders})`,
2359
- ids
2360
- );
2361
- return result.changes;
2362
- },
2363
-
2364
- markAllSeen(): number {
2365
- const result = db.run(
2366
- `UPDATE telemetry_events SET seen = 1 WHERE seen = 0 AND (level = 'error' OR (category = 'system' AND type = 'agent_stopped') OR category = 'ERROR')`
2367
- );
2368
- return result.changes;
2369
- },
2370
- };
2371
-
2372
- function rowToTelemetryEvent(row: TelemetryEventRow): TelemetryEvent {
2373
- return {
2374
- id: row.id,
2375
- agent_id: row.agent_id,
2376
- timestamp: row.timestamp,
2377
- category: row.category,
2378
- type: row.type,
2379
- level: row.level,
2380
- trace_id: row.trace_id,
2381
- span_id: row.span_id,
2382
- thread_id: row.thread_id,
2383
- data: row.data ? JSON.parse(row.data) : null,
2384
- metadata: row.metadata ? JSON.parse(row.metadata) : null,
2385
- duration_ms: row.duration_ms,
2386
- error: row.error,
2387
- received_at: row.received_at,
2388
- seen: row.seen === 1,
2389
- };
2390
- }
2391
-
2392
- // User operations
2393
- export const UserDB = {
2394
- // Create a new user
2395
- create(user: { username: string; password_hash: string; email?: string | null; role?: "admin" | "user" }): User {
2396
- const id = generateId();
2397
- const now = new Date().toISOString();
2398
- const role = user.role || "user";
2399
-
2400
- db.run(
2401
- `INSERT INTO users (id, username, password_hash, email, role, created_at, updated_at)
2402
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
2403
- [id, user.username.toLowerCase(), user.password_hash, user.email || null, role, now, now]
2404
- );
2405
-
2406
- return this.findById(id)!;
2407
- },
2408
-
2409
- // Find user by ID
2410
- findById(id: string): User | null {
2411
- const row = db.query("SELECT * FROM users WHERE id = ?").get(id) as UserRow | null;
2412
- return row ? rowToUser(row) : null;
2413
- },
2414
-
2415
- // Find user by username
2416
- findByUsername(username: string): User | null {
2417
- const row = db.query("SELECT * FROM users WHERE username = ?").get(username.toLowerCase()) as UserRow | null;
2418
- return row ? rowToUser(row) : null;
2419
- },
2420
-
2421
- // Find user by email (for password recovery)
2422
- findByEmail(email: string): User | null {
2423
- const row = db.query("SELECT * FROM users WHERE email = ?").get(email.toLowerCase()) as UserRow | null;
2424
- return row ? rowToUser(row) : null;
2425
- },
2426
-
2427
- // Get all users
2428
- findAll(): User[] {
2429
- const rows = db.query("SELECT * FROM users ORDER BY created_at DESC").all() as UserRow[];
2430
- return rows.map(rowToUser);
2431
- },
2432
-
2433
- // Update user
2434
- update(id: string, updates: Partial<Omit<User, "id" | "created_at">>): User | null {
2435
- const user = this.findById(id);
2436
- if (!user) return null;
2437
-
2438
- const fields: string[] = [];
2439
- const values: unknown[] = [];
2440
-
2441
- if (updates.username !== undefined) {
2442
- fields.push("username = ?");
2443
- values.push(updates.username.toLowerCase());
2444
- }
2445
- if (updates.password_hash !== undefined) {
2446
- fields.push("password_hash = ?");
2447
- values.push(updates.password_hash);
2448
- }
2449
- if (updates.email !== undefined) {
2450
- fields.push("email = ?");
2451
- values.push(updates.email);
2452
- }
2453
- if (updates.role !== undefined) {
2454
- fields.push("role = ?");
2455
- values.push(updates.role);
2456
- }
2457
- if (updates.last_login_at !== undefined) {
2458
- fields.push("last_login_at = ?");
2459
- values.push(updates.last_login_at);
2460
- }
2461
-
2462
- if (fields.length > 0) {
2463
- fields.push("updated_at = ?");
2464
- values.push(new Date().toISOString());
2465
- values.push(id);
2466
-
2467
- db.run(`UPDATE users SET ${fields.join(", ")} WHERE id = ?`, values);
2468
- }
2469
-
2470
- return this.findById(id);
2471
- },
2472
-
2473
- // Delete user
2474
- delete(id: string): boolean {
2475
- const result = db.run("DELETE FROM users WHERE id = ?", [id]);
2476
- return result.changes > 0;
2477
- },
2478
-
2479
- // Update last login
2480
- updateLastLogin(id: string): void {
2481
- db.run("UPDATE users SET last_login_at = ? WHERE id = ?", [new Date().toISOString(), id]);
2482
- },
2483
-
2484
- // Count users
2485
- count(): number {
2486
- const row = db.query("SELECT COUNT(*) as count FROM users").get() as { count: number };
2487
- return row.count;
2488
- },
2489
-
2490
- // Check if any users exist
2491
- hasUsers(): boolean {
2492
- return this.count() > 0;
2493
- },
2494
-
2495
- // Count admins
2496
- countAdmins(): number {
2497
- const row = db.query("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get() as { count: number };
2498
- return row.count;
2499
- },
2500
- };
2501
-
2502
- // Helper to convert DB row to User type
2503
- function rowToUser(row: UserRow): User {
2504
- return {
2505
- id: row.id,
2506
- username: row.username,
2507
- password_hash: row.password_hash,
2508
- email: row.email,
2509
- role: row.role as "admin" | "user",
2510
- created_at: row.created_at,
2511
- updated_at: row.updated_at,
2512
- last_login_at: row.last_login_at,
2513
- };
2514
- }
2515
-
2516
- // Session operations
2517
- export const SessionDB = {
2518
- // Create a new session
2519
- create(session: { user_id: string; refresh_token_hash: string; expires_at: string }): Session {
2520
- const id = generateId();
2521
- const now = new Date().toISOString();
2522
-
2523
- db.run(
2524
- `INSERT INTO sessions (id, user_id, refresh_token_hash, expires_at, created_at)
2525
- VALUES (?, ?, ?, ?, ?)`,
2526
- [id, session.user_id, session.refresh_token_hash, session.expires_at, now]
2527
- );
2528
-
2529
- return this.findById(id)!;
2530
- },
2531
-
2532
- // Find session by ID
2533
- findById(id: string): Session | null {
2534
- const row = db.query("SELECT * FROM sessions WHERE id = ?").get(id) as SessionRow | null;
2535
- return row ? rowToSession(row) : null;
2536
- },
2537
-
2538
- // Find session by refresh token hash
2539
- findByTokenHash(tokenHash: string): Session | null {
2540
- const row = db.query("SELECT * FROM sessions WHERE refresh_token_hash = ?").get(tokenHash) as SessionRow | null;
2541
- return row ? rowToSession(row) : null;
2542
- },
2543
-
2544
- // Get all sessions for a user
2545
- findByUser(userId: string): Session[] {
2546
- const rows = db.query("SELECT * FROM sessions WHERE user_id = ? ORDER BY created_at DESC").all(userId) as SessionRow[];
2547
- return rows.map(rowToSession);
2548
- },
2549
-
2550
- // Delete session
2551
- delete(id: string): boolean {
2552
- const result = db.run("DELETE FROM sessions WHERE id = ?", [id]);
2553
- return result.changes > 0;
2554
- },
2555
-
2556
- // Delete session by token hash
2557
- deleteByTokenHash(tokenHash: string): boolean {
2558
- const result = db.run("DELETE FROM sessions WHERE refresh_token_hash = ?", [tokenHash]);
2559
- return result.changes > 0;
2560
- },
2561
-
2562
- // Delete all sessions for a user
2563
- deleteByUser(userId: string): number {
2564
- const result = db.run("DELETE FROM sessions WHERE user_id = ?", [userId]);
2565
- return result.changes;
2566
- },
2567
-
2568
- // Delete expired sessions
2569
- deleteExpired(): number {
2570
- const result = db.run("DELETE FROM sessions WHERE expires_at < ?", [new Date().toISOString()]);
2571
- return result.changes;
2572
- },
2573
-
2574
- // Check if session is valid (exists and not expired)
2575
- isValid(id: string): boolean {
2576
- const session = this.findById(id);
2577
- if (!session) return false;
2578
- return new Date(session.expires_at) > new Date();
2579
- },
2580
- };
2581
-
2582
- // Helper to convert DB row to Session type
2583
- function rowToSession(row: SessionRow): Session {
2584
- return {
2585
- id: row.id,
2586
- user_id: row.user_id,
2587
- refresh_token_hash: row.refresh_token_hash,
2588
- expires_at: row.expires_at,
2589
- created_at: row.created_at,
2590
- };
2591
- }
2592
-
2593
- // API Key types
2594
- export interface ApiKey {
2595
- id: string;
2596
- name: string;
2597
- key_hash: string;
2598
- key_prefix: string;
2599
- user_id: string;
2600
- expires_at: string | null;
2601
- last_used_at: string | null;
2602
- is_active: boolean;
2603
- created_at: string;
2604
- }
2605
-
2606
- interface ApiKeyRow {
2607
- id: string;
2608
- name: string;
2609
- key_hash: string;
2610
- key_prefix: string;
2611
- user_id: string;
2612
- expires_at: string | null;
2613
- last_used_at: string | null;
2614
- is_active: number;
2615
- created_at: string;
2616
- }
2617
-
2618
- function rowToApiKey(row: ApiKeyRow): ApiKey {
2619
- return {
2620
- id: row.id,
2621
- name: row.name,
2622
- key_hash: row.key_hash,
2623
- key_prefix: row.key_prefix,
2624
- user_id: row.user_id,
2625
- expires_at: row.expires_at,
2626
- last_used_at: row.last_used_at,
2627
- is_active: row.is_active === 1,
2628
- created_at: row.created_at,
2629
- };
2630
- }
2631
-
2632
- // API Key operations
2633
- export const ApiKeyDB = {
2634
- // Create a new API key (returns the raw key only at creation time)
2635
- create(data: { name: string; user_id: string; expires_at?: string | null }): { apiKey: ApiKey; rawKey: string } {
2636
- const id = generateId();
2637
- const rawKey = `apt_${randomBytes(24).toString("hex")}`;
2638
- const keyHash = createHash("sha256").update(rawKey).digest("hex");
2639
- const keyPrefix = rawKey.slice(0, 10);
2640
- const now = new Date().toISOString();
2641
-
2642
- db.run(
2643
- `INSERT INTO api_keys (id, name, key_hash, key_prefix, user_id, expires_at, created_at)
2644
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
2645
- [id, data.name, keyHash, keyPrefix, data.user_id, data.expires_at || null, now]
2646
- );
2647
-
2648
- return { apiKey: this.findById(id)!, rawKey };
2649
- },
2650
-
2651
- // Find by ID
2652
- findById(id: string): ApiKey | null {
2653
- const row = db.query("SELECT * FROM api_keys WHERE id = ?").get(id) as ApiKeyRow | null;
2654
- return row ? rowToApiKey(row) : null;
2655
- },
2656
-
2657
- // Validate a raw key - returns the API key record and user if valid
2658
- validate(rawKey: string): { apiKey: ApiKey; user: User } | null {
2659
- const keyHash = createHash("sha256").update(rawKey).digest("hex");
2660
- const row = db.query(
2661
- "SELECT * FROM api_keys WHERE key_hash = ? AND is_active = 1"
2662
- ).get(keyHash) as ApiKeyRow | null;
2663
-
2664
- if (!row) return null;
2665
-
2666
- const apiKey = rowToApiKey(row);
2667
-
2668
- // Check expiration
2669
- if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) {
2670
- return null;
2671
- }
2672
-
2673
- // Load the user
2674
- const user = UserDB.findById(apiKey.user_id);
2675
- if (!user) return null;
2676
-
2677
- // Update last_used_at
2678
- db.run("UPDATE api_keys SET last_used_at = ? WHERE id = ?", [new Date().toISOString(), apiKey.id]);
2679
-
2680
- return { apiKey, user };
2681
- },
2682
-
2683
- // List all keys for a user (does not expose hash)
2684
- findByUser(userId: string): ApiKey[] {
2685
- const rows = db.query(
2686
- "SELECT * FROM api_keys WHERE user_id = ? ORDER BY created_at DESC"
2687
- ).all(userId) as ApiKeyRow[];
2688
- return rows.map(rowToApiKey);
2689
- },
2690
-
2691
- // Revoke a key
2692
- revoke(id: string, userId: string): boolean {
2693
- const result = db.run(
2694
- "UPDATE api_keys SET is_active = 0 WHERE id = ? AND user_id = ?",
2695
- [id, userId]
2696
- );
2697
- return result.changes > 0;
2698
- },
2699
-
2700
- // Delete a key
2701
- delete(id: string, userId: string): boolean {
2702
- const result = db.run(
2703
- "DELETE FROM api_keys WHERE id = ? AND user_id = ?",
2704
- [id, userId]
2705
- );
2706
- return result.changes > 0;
2707
- },
2708
-
2709
- // Count active keys for a user
2710
- countByUser(userId: string): number {
2711
- const row = db.query(
2712
- "SELECT COUNT(*) as count FROM api_keys WHERE user_id = ? AND is_active = 1"
2713
- ).get(userId) as { count: number };
2714
- return row.count;
2715
- },
2716
- };
2717
-
2718
- // Skill operations
2719
- export const SkillDB = {
2720
- // Create a new skill
2721
- create(skill: Omit<Skill, "id" | "created_at" | "updated_at">): Skill {
2722
- const id = generateId();
2723
- const now = new Date().toISOString();
2724
- const metadataJson = JSON.stringify(skill.metadata || {});
2725
- const allowedToolsJson = JSON.stringify(skill.allowed_tools || []);
2726
-
2727
- db.run(
2728
- `INSERT INTO skills (id, name, description, content, version, license, compatibility, metadata, allowed_tools, source, source_url, enabled, project_id, created_at, updated_at)
2729
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2730
- [
2731
- id,
2732
- skill.name,
2733
- skill.description,
2734
- skill.content,
2735
- skill.version || "1.0.0",
2736
- skill.license || null,
2737
- skill.compatibility || null,
2738
- metadataJson,
2739
- allowedToolsJson,
2740
- skill.source,
2741
- skill.source_url || null,
2742
- skill.enabled ? 1 : 0,
2743
- skill.project_id || null,
2744
- now,
2745
- now,
2746
- ]
2747
- );
2748
-
2749
- return this.findById(id)!;
2750
- },
2751
-
2752
- // Find skill by ID
2753
- findById(id: string): Skill | null {
2754
- const row = db.query("SELECT * FROM skills WHERE id = ?").get(id) as SkillRow | null;
2755
- return row ? rowToSkill(row) : null;
2756
- },
2757
-
2758
- findByIds(ids: string[]): Map<string, Skill> {
2759
- if (ids.length === 0) return new Map();
2760
- const placeholders = ids.map(() => "?").join(",");
2761
- const rows = db.query(`SELECT * FROM skills WHERE id IN (${placeholders})`).all(...ids) as SkillRow[];
2762
- const map = new Map<string, Skill>();
2763
- for (const row of rows) map.set(row.id, rowToSkill(row));
2764
- return map;
2765
- },
2766
-
2767
- // Find skill by name
2768
- findByName(name: string): Skill | null {
2769
- const row = db.query("SELECT * FROM skills WHERE name = ?").get(name) as SkillRow | null;
2770
- return row ? rowToSkill(row) : null;
2771
- },
2772
-
2773
- // Check if skill exists by name
2774
- exists(name: string): boolean {
2775
- const row = db.query("SELECT 1 FROM skills WHERE name = ?").get(name);
2776
- return row !== null;
2777
- },
2778
-
2779
- // Get all skills
2780
- findAll(): Skill[] {
2781
- const rows = db.query("SELECT * FROM skills ORDER BY name ASC").all() as SkillRow[];
2782
- return rows.map(rowToSkill);
2783
- },
2784
-
2785
- // Get enabled skills
2786
- findEnabled(): Skill[] {
2787
- const rows = db.query("SELECT * FROM skills WHERE enabled = 1 ORDER BY name ASC").all() as SkillRow[];
2788
- return rows.map(rowToSkill);
2789
- },
2790
-
2791
- // Update skill
2792
- update(id: string, updates: Partial<Omit<Skill, "id" | "created_at">>): Skill | null {
2793
- const skill = this.findById(id);
2794
- if (!skill) return null;
2795
-
2796
- const fields: string[] = [];
2797
- const values: unknown[] = [];
2798
-
2799
- if (updates.name !== undefined) {
2800
- fields.push("name = ?");
2801
- values.push(updates.name);
2802
- }
2803
- if (updates.description !== undefined) {
2804
- fields.push("description = ?");
2805
- values.push(updates.description);
2806
- }
2807
- if (updates.content !== undefined) {
2808
- fields.push("content = ?");
2809
- values.push(updates.content);
2810
- }
2811
- if (updates.version !== undefined) {
2812
- fields.push("version = ?");
2813
- values.push(updates.version);
2814
- }
2815
- if (updates.license !== undefined) {
2816
- fields.push("license = ?");
2817
- values.push(updates.license);
2818
- }
2819
- if (updates.compatibility !== undefined) {
2820
- fields.push("compatibility = ?");
2821
- values.push(updates.compatibility);
2822
- }
2823
- if (updates.metadata !== undefined) {
2824
- fields.push("metadata = ?");
2825
- values.push(JSON.stringify(updates.metadata));
2826
- }
2827
- if (updates.allowed_tools !== undefined) {
2828
- fields.push("allowed_tools = ?");
2829
- values.push(JSON.stringify(updates.allowed_tools));
2830
- }
2831
- if (updates.source !== undefined) {
2832
- fields.push("source = ?");
2833
- values.push(updates.source);
2834
- }
2835
- if (updates.source_url !== undefined) {
2836
- fields.push("source_url = ?");
2837
- values.push(updates.source_url);
2838
- }
2839
- if (updates.enabled !== undefined) {
2840
- fields.push("enabled = ?");
2841
- values.push(updates.enabled ? 1 : 0);
2842
- }
2843
- if (updates.project_id !== undefined) {
2844
- fields.push("project_id = ?");
2845
- values.push(updates.project_id);
2846
- }
2847
-
2848
- if (fields.length > 0) {
2849
- fields.push("updated_at = ?");
2850
- values.push(new Date().toISOString());
2851
- values.push(id);
2852
-
2853
- db.run(`UPDATE skills SET ${fields.join(", ")} WHERE id = ?`, values);
2854
- }
2855
-
2856
- return this.findById(id);
2857
- },
2858
-
2859
- // Toggle skill enabled/disabled
2860
- setEnabled(id: string, enabled: boolean): Skill | null {
2861
- return this.update(id, { enabled });
2862
- },
2863
-
2864
- // Delete skill
2865
- delete(id: string): boolean {
2866
- const result = db.run("DELETE FROM skills WHERE id = ?", [id]);
2867
- return result.changes > 0;
2868
- },
2869
-
2870
- // Count skills
2871
- count(): number {
2872
- const row = db.query("SELECT COUNT(*) as count FROM skills").get() as { count: number };
2873
- return row.count;
2874
- },
2875
-
2876
- // Count enabled skills
2877
- countEnabled(): number {
2878
- const row = db.query("SELECT COUNT(*) as count FROM skills WHERE enabled = 1").get() as { count: number };
2879
- return row.count;
2880
- },
2881
-
2882
- // Find skills by project (null = global only)
2883
- findByProject(projectId: string | null): Skill[] {
2884
- if (projectId === null) {
2885
- const rows = db.query("SELECT * FROM skills WHERE project_id IS NULL ORDER BY name ASC").all() as SkillRow[];
2886
- return rows.map(rowToSkill);
2887
- }
2888
- const rows = db.query("SELECT * FROM skills WHERE project_id = ? ORDER BY name ASC").all(projectId) as SkillRow[];
2889
- return rows.map(rowToSkill);
2890
- },
2891
-
2892
- // Find skills available for an agent (global + agent's project)
2893
- findForAgent(agentProjectId: string | null): Skill[] {
2894
- if (agentProjectId === null) {
2895
- // Agent has no project, only show global skills
2896
- const rows = db.query("SELECT * FROM skills WHERE project_id IS NULL ORDER BY name ASC").all() as SkillRow[];
2897
- return rows.map(rowToSkill);
2898
- }
2899
- // Agent has a project, show global + project skills
2900
- const rows = db.query("SELECT * FROM skills WHERE project_id IS NULL OR project_id = ? ORDER BY name ASC").all(agentProjectId) as SkillRow[];
2901
- return rows.map(rowToSkill);
2902
- },
2903
-
2904
- // Find global skills only
2905
- findGlobal(): Skill[] {
2906
- const rows = db.query("SELECT * FROM skills WHERE project_id IS NULL ORDER BY name ASC").all() as SkillRow[];
2907
- return rows.map(rowToSkill);
2908
- },
2909
- };
2910
-
2911
- // Helper to convert DB row to Skill type
2912
- function rowToSkill(row: SkillRow): Skill {
2913
- let metadata: Record<string, string> = {};
2914
- if (row.metadata) {
2915
- try {
2916
- metadata = JSON.parse(row.metadata);
2917
- } catch {
2918
- // Use empty object if parsing fails
2919
- }
2920
- }
2921
- let allowed_tools: string[] = [];
2922
- if (row.allowed_tools) {
2923
- try {
2924
- allowed_tools = JSON.parse(row.allowed_tools);
2925
- } catch {
2926
- // Use empty array if parsing fails
2927
- }
2928
- }
2929
- return {
2930
- id: row.id,
2931
- name: row.name,
2932
- description: row.description,
2933
- content: row.content,
2934
- version: row.version || "1.0.0",
2935
- license: row.license,
2936
- compatibility: row.compatibility,
2937
- metadata,
2938
- allowed_tools,
2939
- source: row.source as Skill["source"],
2940
- source_url: row.source_url,
2941
- enabled: row.enabled === 1,
2942
- project_id: row.project_id,
2943
- created_at: row.created_at,
2944
- updated_at: row.updated_at,
2945
- };
2946
- }
2947
-
2948
- // Subscription row → Subscription
2949
- function rowToSubscription(row: SubscriptionRow): Subscription {
2950
- return {
2951
- id: row.id,
2952
- trigger_slug: row.trigger_slug,
2953
- trigger_instance_id: row.trigger_instance_id,
2954
- agent_id: row.agent_id,
2955
- enabled: row.enabled === 1,
2956
- project_id: row.project_id,
2957
- created_at: row.created_at,
2958
- updated_at: row.updated_at,
2959
- };
2960
- }
2961
-
2962
- export const SubscriptionDB = {
2963
- create(sub: Omit<Subscription, "id" | "created_at" | "updated_at">): Subscription {
2964
- const id = generateId();
2965
- const now = new Date().toISOString();
2966
- db.run(
2967
- `INSERT INTO subscriptions (id, trigger_slug, trigger_instance_id, agent_id, enabled, project_id, created_at, updated_at)
2968
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
2969
- [id, sub.trigger_slug, sub.trigger_instance_id || null, sub.agent_id, sub.enabled ? 1 : 0, sub.project_id || null, now, now]
2970
- );
2971
- return this.findById(id)!;
2972
- },
2973
-
2974
- findById(id: string): Subscription | null {
2975
- const row = db.query("SELECT * FROM subscriptions WHERE id = ?").get(id) as SubscriptionRow | null;
2976
- return row ? rowToSubscription(row) : null;
2977
- },
2978
-
2979
- findByTriggerInstanceId(instanceId: string): Subscription[] {
2980
- const rows = db.query("SELECT * FROM subscriptions WHERE trigger_instance_id = ?").all(instanceId) as SubscriptionRow[];
2981
- return rows.map(rowToSubscription);
2982
- },
2983
-
2984
- findByTriggerSlug(slug: string): Subscription[] {
2985
- const rows = db.query("SELECT * FROM subscriptions WHERE trigger_slug = ?").all(slug) as SubscriptionRow[];
2986
- return rows.map(rowToSubscription);
2987
- },
2988
-
2989
- findByAgentId(agentId: string): Subscription[] {
2990
- const rows = db.query("SELECT * FROM subscriptions WHERE agent_id = ?").all(agentId) as SubscriptionRow[];
2991
- return rows.map(rowToSubscription);
2992
- },
2993
-
2994
- // Batch load subscriptions for multiple agents (1 query instead of N)
2995
- findByAgentIds(agentIds: string[]): Map<string, Subscription[]> {
2996
- const result = new Map<string, Subscription[]>();
2997
- if (agentIds.length === 0) return result;
2998
- const placeholders = agentIds.map(() => "?").join(",");
2999
- const rows = db.query(`SELECT * FROM subscriptions WHERE agent_id IN (${placeholders})`).all(...agentIds) as SubscriptionRow[];
3000
- for (const row of rows) {
3001
- const sub = rowToSubscription(row);
3002
- const list = result.get(sub.agent_id) || [];
3003
- list.push(sub);
3004
- result.set(sub.agent_id, list);
3005
- }
3006
- return result;
3007
- },
3008
-
3009
- findAll(projectId?: string | null): Subscription[] {
3010
- if (projectId) {
3011
- const rows = db.query("SELECT * FROM subscriptions WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as SubscriptionRow[];
3012
- return rows.map(rowToSubscription);
3013
- }
3014
- const rows = db.query("SELECT * FROM subscriptions ORDER BY created_at DESC").all() as SubscriptionRow[];
3015
- return rows.map(rowToSubscription);
3016
- },
3017
-
3018
- update(id: string, updates: Partial<Pick<Subscription, "trigger_slug" | "trigger_instance_id" | "agent_id" | "enabled">>): Subscription | null {
3019
- const sub = this.findById(id);
3020
- if (!sub) return null;
3021
-
3022
- const fields: string[] = [];
3023
- const values: (string | number | null)[] = [];
3024
-
3025
- if (updates.trigger_slug !== undefined) { fields.push("trigger_slug = ?"); values.push(updates.trigger_slug); }
3026
- if (updates.trigger_instance_id !== undefined) { fields.push("trigger_instance_id = ?"); values.push(updates.trigger_instance_id || null); }
3027
- if (updates.agent_id !== undefined) { fields.push("agent_id = ?"); values.push(updates.agent_id); }
3028
- if (updates.enabled !== undefined) { fields.push("enabled = ?"); values.push(updates.enabled ? 1 : 0); }
3029
-
3030
- if (fields.length === 0) return sub;
3031
-
3032
- fields.push("updated_at = ?");
3033
- values.push(new Date().toISOString());
3034
- values.push(id);
3035
-
3036
- db.run(`UPDATE subscriptions SET ${fields.join(", ")} WHERE id = ?`, values);
3037
- return this.findById(id);
3038
- },
3039
-
3040
- delete(id: string): boolean {
3041
- const result = db.run("DELETE FROM subscriptions WHERE id = ?", [id]);
3042
- return result.changes > 0;
3043
- },
3044
- };
3045
-
3046
- // --- Channel DB ---
3047
-
3048
- function rowToChannel(row: ChannelRow): Channel {
3049
- return {
3050
- id: row.id,
3051
- type: row.type as Channel["type"],
3052
- name: row.name,
3053
- agent_id: row.agent_id,
3054
- config: row.config,
3055
- status: row.status as Channel["status"],
3056
- error: row.error,
3057
- project_id: row.project_id,
3058
- created_at: row.created_at,
3059
- updated_at: row.updated_at,
3060
- };
3061
- }
3062
-
3063
- export const ChannelDB = {
3064
- create(channel: { type: string; name: string; agent_id: string; config: string; project_id?: string | null }): Channel {
3065
- const id = generateId();
3066
- const now = new Date().toISOString();
3067
- db.run(
3068
- `INSERT INTO channels (id, type, name, agent_id, config, status, project_id, created_at, updated_at)
3069
- VALUES (?, ?, ?, ?, ?, 'stopped', ?, ?, ?)`,
3070
- [id, channel.type, channel.name, channel.agent_id, channel.config, channel.project_id || null, now, now]
3071
- );
3072
- return this.findById(id)!;
3073
- },
3074
-
3075
- findById(id: string): Channel | null {
3076
- const row = db.query("SELECT * FROM channels WHERE id = ?").get(id) as ChannelRow | null;
3077
- return row ? rowToChannel(row) : null;
3078
- },
3079
-
3080
- findAll(): Channel[] {
3081
- const rows = db.query("SELECT * FROM channels ORDER BY created_at DESC").all() as ChannelRow[];
3082
- return rows.map(rowToChannel);
3083
- },
3084
-
3085
- findByAgentId(agentId: string): Channel[] {
3086
- const rows = db.query("SELECT * FROM channels WHERE agent_id = ?").all(agentId) as ChannelRow[];
3087
- return rows.map(rowToChannel);
3088
- },
3089
-
3090
- findRunning(): Channel[] {
3091
- const rows = db.query("SELECT * FROM channels WHERE status = 'running'").all() as ChannelRow[];
3092
- return rows.map(rowToChannel);
3093
- },
3094
-
3095
- update(id: string, updates: { name?: string; agent_id?: string; config?: string; project_id?: string | null }): Channel | null {
3096
- const channel = this.findById(id);
3097
- if (!channel) return null;
3098
-
3099
- const fields: string[] = [];
3100
- const values: (string | null)[] = [];
3101
-
3102
- if (updates.name !== undefined) { fields.push("name = ?"); values.push(updates.name); }
3103
- if (updates.agent_id !== undefined) { fields.push("agent_id = ?"); values.push(updates.agent_id); }
3104
- if (updates.config !== undefined) { fields.push("config = ?"); values.push(updates.config); }
3105
- if (updates.project_id !== undefined) { fields.push("project_id = ?"); values.push(updates.project_id || null); }
3106
-
3107
- if (fields.length === 0) return channel;
3108
-
3109
- fields.push("updated_at = ?");
3110
- values.push(new Date().toISOString());
3111
- values.push(id);
3112
-
3113
- db.run(`UPDATE channels SET ${fields.join(", ")} WHERE id = ?`, values);
3114
- return this.findById(id);
3115
- },
3116
-
3117
- setStatus(id: string, status: Channel["status"], error?: string | null): void {
3118
- db.run(
3119
- "UPDATE channels SET status = ?, error = ?, updated_at = ? WHERE id = ?",
3120
- [status, error || null, new Date().toISOString(), id]
3121
- );
3122
- },
3123
-
3124
- delete(id: string): boolean {
3125
- const result = db.run("DELETE FROM channels WHERE id = ?", [id]);
3126
- return result.changes > 0;
3127
- },
3128
- };
3129
-
3130
- // Generate unique ID
3131
- export function generateId(): string {
3132
- return Math.random().toString(36).substring(2, 15);
3133
- }