apteva 0.4.57 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -54
- package/cli.js +35 -0
- package/install.js +92 -0
- package/package.json +15 -76
- package/LICENSE +0 -63
- package/bin/apteva.js +0 -196
- package/dist/ActivityPage.kxzzb4yc.js +0 -3
- package/dist/ApiDocsPage.zq998hbm.js +0 -4
- package/dist/App.55rea8mn.js +0 -61
- package/dist/App.5ywb23z4.js +0 -53
- package/dist/App.6thds120.js +0 -4
- package/dist/App.9tctxzqm.js +0 -8
- package/dist/App.a8r8ttaz.js +0 -4
- package/dist/App.agsv5bje.js +0 -4
- package/dist/App.cepapqmx.js +0 -4
- package/dist/App.dp041gb3.js +0 -221
- package/dist/App.fds72zb5.js +0 -4
- package/dist/App.fg9qj2dq.js +0 -4
- package/dist/App.ndfejbm9.js +0 -4
- package/dist/App.nxmfmq1h.js +0 -13
- package/dist/App.qdfyt8ba.js +0 -4
- package/dist/App.x2d0ygt6.js +0 -4
- package/dist/App.yt9p4nr3.js +0 -20
- package/dist/App.zn4mw16t.js +0 -1
- package/dist/ConnectionsPage.8r96ryw7.js +0 -3
- package/dist/McpPage.3cwh0gnd.js +0 -3
- package/dist/SettingsPage.ykgdh5ev.js +0 -3
- package/dist/SkillsPage.4np1s65b.js +0 -3
- package/dist/TasksPage.4g08t7p6.js +0 -3
- package/dist/TelemetryPage.72w9pwcp.js +0 -3
- package/dist/TestsPage.z4fk3r7r.js +0 -3
- package/dist/ThreadsPage.63tcajeh.js +0 -3
- package/dist/apteva-kit.css +0 -1
- package/dist/icon.png +0 -0
- package/dist/index.html +0 -16
- package/dist/styles.css +0 -1
- package/scripts/postinstall.mjs +0 -102
- package/src/auth/index.ts +0 -394
- package/src/auth/middleware.ts +0 -213
- package/src/binary.ts +0 -536
- package/src/channels/index.ts +0 -40
- package/src/channels/telegram.ts +0 -311
- package/src/crypto.ts +0 -301
- package/src/db-tests.ts +0 -174
- package/src/db.ts +0 -3133
- package/src/integrations/agentdojo.ts +0 -559
- package/src/integrations/composio.ts +0 -437
- package/src/integrations/index.ts +0 -87
- package/src/integrations/skillsmp.ts +0 -318
- package/src/mcp-client.ts +0 -605
- package/src/mcp-handler.ts +0 -394
- package/src/mcp-platform.ts +0 -2403
- package/src/openapi.ts +0 -2410
- package/src/providers.ts +0 -597
- package/src/routes/api/agent-utils.ts +0 -890
- package/src/routes/api/agents.ts +0 -916
- package/src/routes/api/api-keys.ts +0 -95
- package/src/routes/api/channels.ts +0 -182
- package/src/routes/api/helpers.ts +0 -12
- package/src/routes/api/integrations.ts +0 -639
- package/src/routes/api/mcp.ts +0 -574
- package/src/routes/api/meta-agent.ts +0 -195
- package/src/routes/api/projects.ts +0 -112
- package/src/routes/api/providers.ts +0 -424
- package/src/routes/api/skills.ts +0 -537
- package/src/routes/api/system.ts +0 -333
- package/src/routes/api/telemetry.ts +0 -203
- package/src/routes/api/tests.ts +0 -148
- package/src/routes/api/triggers.ts +0 -518
- package/src/routes/api/users.ts +0 -148
- package/src/routes/api/webhooks.ts +0 -171
- package/src/routes/api.ts +0 -53
- package/src/routes/auth.ts +0 -251
- package/src/routes/share.ts +0 -86
- package/src/routes/static.ts +0 -131
- package/src/server.ts +0 -642
- package/src/test-runner.ts +0 -598
- package/src/triggers/agentdojo.ts +0 -253
- package/src/triggers/composio.ts +0 -264
- package/src/triggers/index.ts +0 -71
- package/src/tui/AgentList.tsx +0 -145
- package/src/tui/App.tsx +0 -102
- package/src/tui/Login.tsx +0 -104
- package/src/tui/api.ts +0 -72
- package/src/tui/index.tsx +0 -7
- package/src/web/App.tsx +0 -455
- package/src/web/components/activity/ActivityPage.tsx +0 -314
- package/src/web/components/activity/index.ts +0 -1
- package/src/web/components/agents/AgentCard.tsx +0 -189
- package/src/web/components/agents/AgentPanel.tsx +0 -2244
- package/src/web/components/agents/AgentsView.tsx +0 -180
- package/src/web/components/agents/CreateAgentModal.tsx +0 -475
- package/src/web/components/agents/index.ts +0 -4
- package/src/web/components/api/ApiDocsPage.tsx +0 -842
- package/src/web/components/auth/CreateAccountStep.tsx +0 -176
- package/src/web/components/auth/LoginPage.tsx +0 -91
- package/src/web/components/auth/index.ts +0 -2
- package/src/web/components/common/Icons.tsx +0 -250
- package/src/web/components/common/LoadingSpinner.tsx +0 -44
- package/src/web/components/common/Modal.tsx +0 -199
- package/src/web/components/common/Select.tsx +0 -97
- package/src/web/components/common/index.ts +0 -20
- package/src/web/components/connections/ConnectionsPage.tsx +0 -54
- package/src/web/components/connections/IntegrationsTab.tsx +0 -170
- package/src/web/components/connections/OverviewTab.tsx +0 -137
- package/src/web/components/connections/TriggersTab.tsx +0 -1346
- package/src/web/components/dashboard/Dashboard.tsx +0 -572
- package/src/web/components/dashboard/index.ts +0 -1
- package/src/web/components/index.ts +0 -21
- package/src/web/components/layout/ErrorBanner.tsx +0 -18
- package/src/web/components/layout/Header.tsx +0 -332
- package/src/web/components/layout/Sidebar.tsx +0 -231
- package/src/web/components/layout/index.ts +0 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
- package/src/web/components/mcp/McpPage.tsx +0 -2515
- package/src/web/components/mcp/index.ts +0 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
- package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
- package/src/web/components/onboarding/index.ts +0 -1
- package/src/web/components/settings/SettingsPage.tsx +0 -2776
- package/src/web/components/settings/index.ts +0 -1
- package/src/web/components/skills/SkillsPage.tsx +0 -1200
- package/src/web/components/tasks/TasksPage.tsx +0 -1116
- package/src/web/components/tasks/index.ts +0 -1
- package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
- package/src/web/components/tests/TestsPage.tsx +0 -594
- package/src/web/components/threads/ThreadsPage.tsx +0 -315
- package/src/web/context/AuthContext.tsx +0 -242
- package/src/web/context/ProjectContext.tsx +0 -214
- package/src/web/context/TelemetryContext.tsx +0 -299
- package/src/web/context/ThemeContext.tsx +0 -90
- package/src/web/context/UIModeContext.tsx +0 -49
- package/src/web/context/index.ts +0 -12
- package/src/web/hooks/index.ts +0 -3
- package/src/web/hooks/useAgents.ts +0 -115
- package/src/web/hooks/useOnboarding.ts +0 -20
- package/src/web/hooks/useProviders.ts +0 -75
- package/src/web/icon.png +0 -0
- package/src/web/index.html +0 -16
- package/src/web/styles.css +0 -118
- package/src/web/themes.ts +0 -162
- 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
|
-
}
|