apteva 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/App.hzbfeg94.js +217 -0
  2. package/dist/index.html +3 -1
  3. package/dist/styles.css +1 -1
  4. package/package.json +1 -1
  5. package/src/auth/index.ts +386 -0
  6. package/src/auth/middleware.ts +183 -0
  7. package/src/binary.ts +19 -1
  8. package/src/db.ts +570 -32
  9. package/src/routes/api.ts +913 -38
  10. package/src/routes/auth.ts +242 -0
  11. package/src/server.ts +60 -8
  12. package/src/web/App.tsx +61 -19
  13. package/src/web/components/agents/AgentCard.tsx +30 -41
  14. package/src/web/components/agents/AgentPanel.tsx +751 -11
  15. package/src/web/components/agents/AgentsView.tsx +81 -9
  16. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  17. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  18. package/src/web/components/auth/LoginPage.tsx +91 -0
  19. package/src/web/components/auth/index.ts +2 -0
  20. package/src/web/components/common/Icons.tsx +48 -0
  21. package/src/web/components/common/Modal.tsx +1 -1
  22. package/src/web/components/dashboard/Dashboard.tsx +91 -31
  23. package/src/web/components/index.ts +3 -0
  24. package/src/web/components/layout/Header.tsx +145 -15
  25. package/src/web/components/layout/Sidebar.tsx +81 -43
  26. package/src/web/components/mcp/McpPage.tsx +261 -32
  27. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  28. package/src/web/components/settings/SettingsPage.tsx +404 -18
  29. package/src/web/components/tasks/TasksPage.tsx +21 -19
  30. package/src/web/components/telemetry/TelemetryPage.tsx +271 -81
  31. package/src/web/context/AuthContext.tsx +230 -0
  32. package/src/web/context/ProjectContext.tsx +182 -0
  33. package/src/web/context/TelemetryContext.tsx +98 -76
  34. package/src/web/context/index.ts +5 -0
  35. package/src/web/hooks/useAgents.ts +18 -6
  36. package/src/web/hooks/useOnboarding.ts +20 -4
  37. package/src/web/hooks/useProviders.ts +15 -5
  38. package/src/web/icon.png +0 -0
  39. package/src/web/styles.css +12 -0
  40. package/src/web/types.ts +6 -0
  41. package/dist/App.0mzj9cz9.js +0 -213
package/src/db.ts CHANGED
@@ -11,6 +11,8 @@ export interface AgentFeatures {
11
11
  operator: boolean;
12
12
  mcp: boolean;
13
13
  realtime: boolean;
14
+ files: boolean;
15
+ agents: boolean;
14
16
  }
15
17
 
16
18
  export const DEFAULT_FEATURES: AgentFeatures = {
@@ -20,6 +22,8 @@ export const DEFAULT_FEATURES: AgentFeatures = {
20
22
  operator: false,
21
23
  mcp: false,
22
24
  realtime: false,
25
+ files: false,
26
+ agents: false,
23
27
  };
24
28
 
25
29
  export interface Agent {
@@ -32,6 +36,25 @@ export interface Agent {
32
36
  port: number | null;
33
37
  features: AgentFeatures;
34
38
  mcp_servers: string[]; // Array of MCP server IDs
39
+ project_id: string | null; // Optional project grouping
40
+ created_at: string;
41
+ updated_at: string;
42
+ }
43
+
44
+ export interface Project {
45
+ id: string;
46
+ name: string;
47
+ description: string | null;
48
+ color: string; // Hex color for UI display
49
+ created_at: string;
50
+ updated_at: string;
51
+ }
52
+
53
+ export interface ProjectRow {
54
+ id: string;
55
+ name: string;
56
+ description: string | null;
57
+ color: string;
35
58
  created_at: string;
36
59
  updated_at: string;
37
60
  }
@@ -46,6 +69,7 @@ export interface AgentRow {
46
69
  port: number | null;
47
70
  features: string | null;
48
71
  mcp_servers: string | null;
72
+ project_id: string | null;
49
73
  created_at: string;
50
74
  updated_at: string;
51
75
  }
@@ -272,6 +296,58 @@ function runMigrations() {
272
296
  CREATE INDEX IF NOT EXISTS idx_telemetry_trace ON telemetry_events(trace_id);
273
297
  `,
274
298
  },
299
+ {
300
+ name: "010_create_users",
301
+ sql: `
302
+ CREATE TABLE IF NOT EXISTS users (
303
+ id TEXT PRIMARY KEY,
304
+ username TEXT UNIQUE NOT NULL,
305
+ password_hash TEXT NOT NULL,
306
+ email TEXT,
307
+ role TEXT NOT NULL DEFAULT 'user',
308
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
309
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
310
+ last_login_at TEXT
311
+ );
312
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username);
313
+ `,
314
+ },
315
+ {
316
+ name: "011_create_sessions",
317
+ sql: `
318
+ CREATE TABLE IF NOT EXISTS sessions (
319
+ id TEXT PRIMARY KEY,
320
+ user_id TEXT NOT NULL,
321
+ refresh_token_hash TEXT NOT NULL,
322
+ expires_at TEXT NOT NULL,
323
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
324
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
325
+ );
326
+ CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
327
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
328
+ `,
329
+ },
330
+ {
331
+ name: "012_create_projects",
332
+ sql: `
333
+ CREATE TABLE IF NOT EXISTS projects (
334
+ id TEXT PRIMARY KEY,
335
+ name TEXT NOT NULL,
336
+ description TEXT,
337
+ color TEXT NOT NULL DEFAULT '#6366f1',
338
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
339
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
340
+ );
341
+ CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name);
342
+ `,
343
+ },
344
+ {
345
+ name: "013_add_agent_project_id",
346
+ sql: `
347
+ ALTER TABLE agents ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
348
+ CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id);
349
+ `,
350
+ },
275
351
  ];
276
352
 
277
353
  // Check which migrations have been applied
@@ -289,6 +365,57 @@ function runMigrations() {
289
365
  db.run("INSERT INTO migrations (name) VALUES (?)", [migration.name]);
290
366
  }
291
367
  }
368
+
369
+ // Schema upgrade migrations (check actual table structure)
370
+ runSchemaUpgrades();
371
+ }
372
+
373
+ // Handle schema changes that require checking actual table structure
374
+ function runSchemaUpgrades() {
375
+ // Check if users table needs migration from email-based to username-based
376
+ const tableInfo = db.query("PRAGMA table_info(users)").all() as { name: string }[];
377
+ const columns = new Set(tableInfo.map(c => c.name));
378
+
379
+ // Old schema has 'email' as required + 'name', new schema has 'username' + optional 'email'
380
+ if (columns.has("name") && !columns.has("username")) {
381
+ console.log("[db] Migrating users table from email-based to username-based auth...");
382
+
383
+ // Get existing users
384
+ const existingUsers = db.query("SELECT * FROM users").all() as any[];
385
+
386
+ // Drop old table and indexes
387
+ db.run("DROP INDEX IF EXISTS idx_users_email");
388
+ db.run("DROP TABLE users");
389
+
390
+ // Create new schema
391
+ db.run(`
392
+ CREATE TABLE users (
393
+ id TEXT PRIMARY KEY,
394
+ username TEXT UNIQUE NOT NULL,
395
+ password_hash TEXT NOT NULL,
396
+ email TEXT,
397
+ role TEXT NOT NULL DEFAULT 'user',
398
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
399
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
400
+ last_login_at TEXT
401
+ )
402
+ `);
403
+ db.run("CREATE UNIQUE INDEX idx_users_username ON users(username)");
404
+
405
+ // Migrate existing users (use part before @ in email as username)
406
+ for (const user of existingUsers) {
407
+ const username = user.email.split("@")[0].replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 20);
408
+ db.run(
409
+ `INSERT INTO users (id, username, password_hash, email, role, created_at, updated_at, last_login_at)
410
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
411
+ [user.id, username, user.password_hash, user.email, user.role, user.created_at, user.updated_at, user.last_login_at]
412
+ );
413
+ }
414
+
415
+ if (existingUsers.length > 0) {
416
+ console.log(`[db] Migrated ${existingUsers.length} user(s). Usernames derived from email addresses.`);
417
+ }
418
+ }
292
419
  }
293
420
 
294
421
  // Agent CRUD operations
@@ -299,10 +426,10 @@ export const AgentDB = {
299
426
  const featuresJson = JSON.stringify(agent.features || DEFAULT_FEATURES);
300
427
  const mcpServersJson = JSON.stringify(agent.mcp_servers || []);
301
428
  const stmt = db.prepare(`
302
- INSERT INTO agents (id, name, model, provider, system_prompt, features, mcp_servers, status, port, created_at, updated_at)
303
- VALUES (?, ?, ?, ?, ?, ?, ?, 'stopped', NULL, ?, ?)
429
+ INSERT INTO agents (id, name, model, provider, system_prompt, features, mcp_servers, project_id, status, port, created_at, updated_at)
430
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'stopped', NULL, ?, ?)
304
431
  `);
305
- stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, mcpServersJson, now, now);
432
+ stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, mcpServersJson, agent.project_id || null, now, now);
306
433
  return this.findById(agent.id)!;
307
434
  },
308
435
 
@@ -364,6 +491,10 @@ export const AgentDB = {
364
491
  fields.push("mcp_servers = ?");
365
492
  values.push(JSON.stringify(updates.mcp_servers));
366
493
  }
494
+ if (updates.project_id !== undefined) {
495
+ fields.push("project_id = ?");
496
+ values.push(updates.project_id);
497
+ }
367
498
 
368
499
  if (fields.length > 0) {
369
500
  fields.push("updated_at = ?");
@@ -376,6 +507,16 @@ export const AgentDB = {
376
507
  return this.findById(id);
377
508
  },
378
509
 
510
+ // Find agents by project
511
+ findByProject(projectId: string | null): Agent[] {
512
+ if (projectId === null) {
513
+ const rows = db.query("SELECT * FROM agents WHERE project_id IS NULL ORDER BY created_at DESC").all() as AgentRow[];
514
+ return rows.map(rowToAgent);
515
+ }
516
+ const rows = db.query("SELECT * FROM agents WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as AgentRow[];
517
+ return rows.map(rowToAgent);
518
+ },
519
+
379
520
  // Delete agent
380
521
  delete(id: string): boolean {
381
522
  const result = db.run("DELETE FROM agents WHERE id = ?", [id]);
@@ -405,6 +546,107 @@ export const AgentDB = {
405
546
  },
406
547
  };
407
548
 
549
+ // Project CRUD operations
550
+ export const ProjectDB = {
551
+ // Create a new project
552
+ create(project: { name: string; description?: string | null; color?: string }): Project {
553
+ const id = generateId();
554
+ const now = new Date().toISOString();
555
+ const color = project.color || "#6366f1";
556
+
557
+ db.run(
558
+ `INSERT INTO projects (id, name, description, color, created_at, updated_at)
559
+ VALUES (?, ?, ?, ?, ?, ?)`,
560
+ [id, project.name, project.description || null, color, now, now]
561
+ );
562
+
563
+ return this.findById(id)!;
564
+ },
565
+
566
+ // Find project by ID
567
+ findById(id: string): Project | null {
568
+ const row = db.query("SELECT * FROM projects WHERE id = ?").get(id) as ProjectRow | null;
569
+ return row ? rowToProject(row) : null;
570
+ },
571
+
572
+ // Get all projects
573
+ findAll(): Project[] {
574
+ const rows = db.query("SELECT * FROM projects ORDER BY name ASC").all() as ProjectRow[];
575
+ return rows.map(rowToProject);
576
+ },
577
+
578
+ // Update project
579
+ update(id: string, updates: Partial<Omit<Project, "id" | "created_at">>): Project | null {
580
+ const project = this.findById(id);
581
+ if (!project) return null;
582
+
583
+ const fields: string[] = [];
584
+ const values: unknown[] = [];
585
+
586
+ if (updates.name !== undefined) {
587
+ fields.push("name = ?");
588
+ values.push(updates.name);
589
+ }
590
+ if (updates.description !== undefined) {
591
+ fields.push("description = ?");
592
+ values.push(updates.description);
593
+ }
594
+ if (updates.color !== undefined) {
595
+ fields.push("color = ?");
596
+ values.push(updates.color);
597
+ }
598
+
599
+ if (fields.length > 0) {
600
+ fields.push("updated_at = ?");
601
+ values.push(new Date().toISOString());
602
+ values.push(id);
603
+
604
+ db.run(`UPDATE projects SET ${fields.join(", ")} WHERE id = ?`, values);
605
+ }
606
+
607
+ return this.findById(id);
608
+ },
609
+
610
+ // Delete project (agents will have project_id set to NULL)
611
+ delete(id: string): boolean {
612
+ const result = db.run("DELETE FROM projects WHERE id = ?", [id]);
613
+ return result.changes > 0;
614
+ },
615
+
616
+ // Count projects
617
+ count(): number {
618
+ const row = db.query("SELECT COUNT(*) as count FROM projects").get() as { count: number };
619
+ return row.count;
620
+ },
621
+
622
+ // Get agent count per project
623
+ getAgentCounts(): Map<string | null, number> {
624
+ const rows = db.query(`
625
+ SELECT project_id, COUNT(*) as count
626
+ FROM agents
627
+ GROUP BY project_id
628
+ `).all() as { project_id: string | null; count: number }[];
629
+
630
+ const counts = new Map<string | null, number>();
631
+ for (const row of rows) {
632
+ counts.set(row.project_id, row.count);
633
+ }
634
+ return counts;
635
+ },
636
+ };
637
+
638
+ // Helper to convert DB row to Project type
639
+ function rowToProject(row: ProjectRow): Project {
640
+ return {
641
+ id: row.id,
642
+ name: row.name,
643
+ description: row.description,
644
+ color: row.color,
645
+ created_at: row.created_at,
646
+ updated_at: row.updated_at,
647
+ };
648
+ }
649
+
408
650
  // Thread CRUD operations
409
651
  export const ThreadDB = {
410
652
  create(id: string, agentId: string, title?: string): void {
@@ -493,6 +735,7 @@ function rowToAgent(row: AgentRow): Agent {
493
735
  port: row.port,
494
736
  features,
495
737
  mcp_servers,
738
+ project_id: row.project_id,
496
739
  created_at: row.created_at,
497
740
  updated_at: row.updated_at,
498
741
  };
@@ -695,6 +938,45 @@ function rowToMcpServer(row: McpServerRow): McpServer {
695
938
  }
696
939
 
697
940
  // Telemetry Event types
941
+ // User types
942
+ export interface User {
943
+ id: string;
944
+ username: string;
945
+ password_hash: string;
946
+ email: string | null; // Optional, for password recovery only
947
+ role: "admin" | "user";
948
+ created_at: string;
949
+ updated_at: string;
950
+ last_login_at: string | null;
951
+ }
952
+
953
+ export interface UserRow {
954
+ id: string;
955
+ username: string;
956
+ password_hash: string;
957
+ email: string | null;
958
+ role: string;
959
+ created_at: string;
960
+ updated_at: string;
961
+ last_login_at: string | null;
962
+ }
963
+
964
+ export interface Session {
965
+ id: string;
966
+ user_id: string;
967
+ refresh_token_hash: string;
968
+ expires_at: string;
969
+ created_at: string;
970
+ }
971
+
972
+ export interface SessionRow {
973
+ id: string;
974
+ user_id: string;
975
+ refresh_token_hash: string;
976
+ expires_at: string;
977
+ created_at: string;
978
+ }
979
+
698
980
  export interface TelemetryEvent {
699
981
  id: string;
700
982
  agent_id: string;
@@ -779,6 +1061,7 @@ export const TelemetryDB = {
779
1061
  // Query events with filters
780
1062
  query(filters: {
781
1063
  agent_id?: string;
1064
+ project_id?: string | null; // Filter by project (null = unassigned agents)
782
1065
  category?: string;
783
1066
  level?: string;
784
1067
  trace_id?: string;
@@ -791,27 +1074,35 @@ export const TelemetryDB = {
791
1074
  const params: unknown[] = [];
792
1075
 
793
1076
  if (filters.agent_id) {
794
- conditions.push("agent_id = ?");
1077
+ conditions.push("t.agent_id = ?");
795
1078
  params.push(filters.agent_id);
796
1079
  }
1080
+ if (filters.project_id !== undefined) {
1081
+ if (filters.project_id === null) {
1082
+ conditions.push("a.project_id IS NULL");
1083
+ } else {
1084
+ conditions.push("a.project_id = ?");
1085
+ params.push(filters.project_id);
1086
+ }
1087
+ }
797
1088
  if (filters.category) {
798
- conditions.push("category = ?");
1089
+ conditions.push("t.category = ?");
799
1090
  params.push(filters.category);
800
1091
  }
801
1092
  if (filters.level) {
802
- conditions.push("level = ?");
1093
+ conditions.push("t.level = ?");
803
1094
  params.push(filters.level);
804
1095
  }
805
1096
  if (filters.trace_id) {
806
- conditions.push("trace_id = ?");
1097
+ conditions.push("t.trace_id = ?");
807
1098
  params.push(filters.trace_id);
808
1099
  }
809
1100
  if (filters.since) {
810
- conditions.push("timestamp >= ?");
1101
+ conditions.push("t.timestamp >= ?");
811
1102
  params.push(filters.since);
812
1103
  }
813
1104
  if (filters.until) {
814
- conditions.push("timestamp <= ?");
1105
+ conditions.push("t.timestamp <= ?");
815
1106
  params.push(filters.until);
816
1107
  }
817
1108
 
@@ -819,7 +1110,11 @@ export const TelemetryDB = {
819
1110
  const limit = filters.limit || 100;
820
1111
  const offset = filters.offset || 0;
821
1112
 
822
- const sql = `SELECT * FROM telemetry_events ${where} ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
1113
+ // Join with agents table when filtering by project
1114
+ const needsJoin = filters.project_id !== undefined;
1115
+ const sql = needsJoin
1116
+ ? `SELECT t.* FROM telemetry_events t JOIN agents a ON t.agent_id = a.id ${where} ORDER BY t.timestamp DESC LIMIT ? OFFSET ?`
1117
+ : `SELECT * FROM telemetry_events t ${where} ORDER BY t.timestamp DESC LIMIT ? OFFSET ?`;
823
1118
  params.push(limit, offset);
824
1119
 
825
1120
  const rows = db.query(sql).all(...params) as TelemetryEventRow[];
@@ -829,6 +1124,7 @@ export const TelemetryDB = {
829
1124
  // Get usage stats
830
1125
  getUsage(filters: {
831
1126
  agent_id?: string;
1127
+ project_id?: string | null;
832
1128
  since?: string;
833
1129
  until?: string;
834
1130
  group_by?: "agent" | "day";
@@ -843,17 +1139,26 @@ export const TelemetryDB = {
843
1139
  }> {
844
1140
  const conditions: string[] = [];
845
1141
  const params: unknown[] = [];
1142
+ const needsJoin = filters.project_id !== undefined;
846
1143
 
847
1144
  if (filters.agent_id) {
848
- conditions.push("agent_id = ?");
1145
+ conditions.push("t.agent_id = ?");
849
1146
  params.push(filters.agent_id);
850
1147
  }
1148
+ if (filters.project_id !== undefined) {
1149
+ if (filters.project_id === null) {
1150
+ conditions.push("a.project_id IS NULL");
1151
+ } else {
1152
+ conditions.push("a.project_id = ?");
1153
+ params.push(filters.project_id);
1154
+ }
1155
+ }
851
1156
  if (filters.since) {
852
- conditions.push("timestamp >= ?");
1157
+ conditions.push("t.timestamp >= ?");
853
1158
  params.push(filters.since);
854
1159
  }
855
1160
  if (filters.until) {
856
- conditions.push("timestamp <= ?");
1161
+ conditions.push("t.timestamp <= ?");
857
1162
  params.push(filters.until);
858
1163
  }
859
1164
 
@@ -863,22 +1168,26 @@ export const TelemetryDB = {
863
1168
  let selectFields = "";
864
1169
 
865
1170
  if (filters.group_by === "day") {
866
- groupBy = "GROUP BY date(timestamp)";
867
- selectFields = "date(timestamp) as date,";
1171
+ groupBy = "GROUP BY date(t.timestamp)";
1172
+ selectFields = "date(t.timestamp) as date,";
868
1173
  } else if (filters.group_by === "agent") {
869
- groupBy = "GROUP BY agent_id";
870
- selectFields = "agent_id,";
1174
+ groupBy = "GROUP BY t.agent_id";
1175
+ selectFields = "t.agent_id as agent_id,";
871
1176
  }
872
1177
 
1178
+ const fromClause = needsJoin
1179
+ ? "FROM telemetry_events t JOIN agents a ON t.agent_id = a.id"
1180
+ : "FROM telemetry_events t";
1181
+
873
1182
  const sql = `
874
1183
  SELECT
875
1184
  ${selectFields}
876
- COALESCE(SUM(CASE WHEN category = 'LLM' THEN json_extract(data, '$.input_tokens') ELSE 0 END), 0) as input_tokens,
877
- COALESCE(SUM(CASE WHEN category = 'LLM' THEN json_extract(data, '$.output_tokens') ELSE 0 END), 0) as output_tokens,
878
- COALESCE(SUM(CASE WHEN category = 'LLM' THEN 1 ELSE 0 END), 0) as llm_calls,
879
- COALESCE(SUM(CASE WHEN category = 'TOOL' THEN 1 ELSE 0 END), 0) as tool_calls,
880
- COALESCE(SUM(CASE WHEN level = 'error' THEN 1 ELSE 0 END), 0) as errors
881
- FROM telemetry_events
1185
+ COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.input_tokens') ELSE 0 END), 0) as input_tokens,
1186
+ COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.output_tokens') ELSE 0 END), 0) as output_tokens,
1187
+ COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN 1 ELSE 0 END), 0) as llm_calls,
1188
+ COALESCE(SUM(CASE WHEN t.category = 'TOOL' THEN 1 ELSE 0 END), 0) as tool_calls,
1189
+ COALESCE(SUM(CASE WHEN t.level = 'error' THEN 1 ELSE 0 END), 0) as errors
1190
+ ${fromClause}
882
1191
  ${where}
883
1192
  ${groupBy}
884
1193
  `;
@@ -895,7 +1204,7 @@ export const TelemetryDB = {
895
1204
  },
896
1205
 
897
1206
  // Get summary stats
898
- getStats(agentId?: string): {
1207
+ getStats(filters: { agentId?: string; projectId?: string | null } = {}): {
899
1208
  total_events: number;
900
1209
  total_llm_calls: number;
901
1210
  total_tool_calls: number;
@@ -903,18 +1212,37 @@ export const TelemetryDB = {
903
1212
  total_input_tokens: number;
904
1213
  total_output_tokens: number;
905
1214
  } {
906
- const where = agentId ? "WHERE agent_id = ?" : "";
907
- const params = agentId ? [agentId] : [];
1215
+ const conditions: string[] = [];
1216
+ const params: unknown[] = [];
1217
+ const needsJoin = filters.projectId !== undefined;
1218
+
1219
+ if (filters.agentId) {
1220
+ conditions.push("t.agent_id = ?");
1221
+ params.push(filters.agentId);
1222
+ }
1223
+ if (filters.projectId !== undefined) {
1224
+ if (filters.projectId === null) {
1225
+ conditions.push("a.project_id IS NULL");
1226
+ } else {
1227
+ conditions.push("a.project_id = ?");
1228
+ params.push(filters.projectId);
1229
+ }
1230
+ }
1231
+
1232
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1233
+ const fromClause = needsJoin
1234
+ ? "FROM telemetry_events t JOIN agents a ON t.agent_id = a.id"
1235
+ : "FROM telemetry_events t";
908
1236
 
909
1237
  const sql = `
910
1238
  SELECT
911
1239
  COUNT(*) as total_events,
912
- COALESCE(SUM(CASE WHEN category = 'LLM' THEN 1 ELSE 0 END), 0) as total_llm_calls,
913
- COALESCE(SUM(CASE WHEN category = 'TOOL' THEN 1 ELSE 0 END), 0) as total_tool_calls,
914
- COALESCE(SUM(CASE WHEN level = 'error' THEN 1 ELSE 0 END), 0) as total_errors,
915
- COALESCE(SUM(CASE WHEN category = 'LLM' THEN json_extract(data, '$.input_tokens') ELSE 0 END), 0) as total_input_tokens,
916
- COALESCE(SUM(CASE WHEN category = 'LLM' THEN json_extract(data, '$.output_tokens') ELSE 0 END), 0) as total_output_tokens
917
- FROM telemetry_events
1240
+ COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN 1 ELSE 0 END), 0) as total_llm_calls,
1241
+ COALESCE(SUM(CASE WHEN t.category = 'TOOL' THEN 1 ELSE 0 END), 0) as total_tool_calls,
1242
+ COALESCE(SUM(CASE WHEN t.level = 'error' THEN 1 ELSE 0 END), 0) as total_errors,
1243
+ COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.input_tokens') ELSE 0 END), 0) as total_input_tokens,
1244
+ COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.output_tokens') ELSE 0 END), 0) as total_output_tokens
1245
+ ${fromClause}
918
1246
  ${where}
919
1247
  `;
920
1248
 
@@ -939,6 +1267,15 @@ export const TelemetryDB = {
939
1267
  return result.changes;
940
1268
  },
941
1269
 
1270
+ // Delete all events for an agent
1271
+ deleteByAgent(agentId: string): number {
1272
+ const result = db.run(
1273
+ "DELETE FROM telemetry_events WHERE agent_id = ?",
1274
+ [agentId]
1275
+ );
1276
+ return result.changes;
1277
+ },
1278
+
942
1279
  // Count events
943
1280
  count(agentId?: string): number {
944
1281
  if (agentId) {
@@ -969,6 +1306,207 @@ function rowToTelemetryEvent(row: TelemetryEventRow): TelemetryEvent {
969
1306
  };
970
1307
  }
971
1308
 
1309
+ // User operations
1310
+ export const UserDB = {
1311
+ // Create a new user
1312
+ create(user: { username: string; password_hash: string; email?: string | null; role?: "admin" | "user" }): User {
1313
+ const id = generateId();
1314
+ const now = new Date().toISOString();
1315
+ const role = user.role || "user";
1316
+
1317
+ db.run(
1318
+ `INSERT INTO users (id, username, password_hash, email, role, created_at, updated_at)
1319
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
1320
+ [id, user.username.toLowerCase(), user.password_hash, user.email || null, role, now, now]
1321
+ );
1322
+
1323
+ return this.findById(id)!;
1324
+ },
1325
+
1326
+ // Find user by ID
1327
+ findById(id: string): User | null {
1328
+ const row = db.query("SELECT * FROM users WHERE id = ?").get(id) as UserRow | null;
1329
+ return row ? rowToUser(row) : null;
1330
+ },
1331
+
1332
+ // Find user by username
1333
+ findByUsername(username: string): User | null {
1334
+ const row = db.query("SELECT * FROM users WHERE username = ?").get(username.toLowerCase()) as UserRow | null;
1335
+ return row ? rowToUser(row) : null;
1336
+ },
1337
+
1338
+ // Find user by email (for password recovery)
1339
+ findByEmail(email: string): User | null {
1340
+ const row = db.query("SELECT * FROM users WHERE email = ?").get(email.toLowerCase()) as UserRow | null;
1341
+ return row ? rowToUser(row) : null;
1342
+ },
1343
+
1344
+ // Get all users
1345
+ findAll(): User[] {
1346
+ const rows = db.query("SELECT * FROM users ORDER BY created_at DESC").all() as UserRow[];
1347
+ return rows.map(rowToUser);
1348
+ },
1349
+
1350
+ // Update user
1351
+ update(id: string, updates: Partial<Omit<User, "id" | "created_at">>): User | null {
1352
+ const user = this.findById(id);
1353
+ if (!user) return null;
1354
+
1355
+ const fields: string[] = [];
1356
+ const values: unknown[] = [];
1357
+
1358
+ if (updates.username !== undefined) {
1359
+ fields.push("username = ?");
1360
+ values.push(updates.username.toLowerCase());
1361
+ }
1362
+ if (updates.password_hash !== undefined) {
1363
+ fields.push("password_hash = ?");
1364
+ values.push(updates.password_hash);
1365
+ }
1366
+ if (updates.email !== undefined) {
1367
+ fields.push("email = ?");
1368
+ values.push(updates.email);
1369
+ }
1370
+ if (updates.role !== undefined) {
1371
+ fields.push("role = ?");
1372
+ values.push(updates.role);
1373
+ }
1374
+ if (updates.last_login_at !== undefined) {
1375
+ fields.push("last_login_at = ?");
1376
+ values.push(updates.last_login_at);
1377
+ }
1378
+
1379
+ if (fields.length > 0) {
1380
+ fields.push("updated_at = ?");
1381
+ values.push(new Date().toISOString());
1382
+ values.push(id);
1383
+
1384
+ db.run(`UPDATE users SET ${fields.join(", ")} WHERE id = ?`, values);
1385
+ }
1386
+
1387
+ return this.findById(id);
1388
+ },
1389
+
1390
+ // Delete user
1391
+ delete(id: string): boolean {
1392
+ const result = db.run("DELETE FROM users WHERE id = ?", [id]);
1393
+ return result.changes > 0;
1394
+ },
1395
+
1396
+ // Update last login
1397
+ updateLastLogin(id: string): void {
1398
+ db.run("UPDATE users SET last_login_at = ? WHERE id = ?", [new Date().toISOString(), id]);
1399
+ },
1400
+
1401
+ // Count users
1402
+ count(): number {
1403
+ const row = db.query("SELECT COUNT(*) as count FROM users").get() as { count: number };
1404
+ return row.count;
1405
+ },
1406
+
1407
+ // Check if any users exist
1408
+ hasUsers(): boolean {
1409
+ return this.count() > 0;
1410
+ },
1411
+
1412
+ // Count admins
1413
+ countAdmins(): number {
1414
+ const row = db.query("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get() as { count: number };
1415
+ return row.count;
1416
+ },
1417
+ };
1418
+
1419
+ // Helper to convert DB row to User type
1420
+ function rowToUser(row: UserRow): User {
1421
+ return {
1422
+ id: row.id,
1423
+ username: row.username,
1424
+ password_hash: row.password_hash,
1425
+ email: row.email,
1426
+ role: row.role as "admin" | "user",
1427
+ created_at: row.created_at,
1428
+ updated_at: row.updated_at,
1429
+ last_login_at: row.last_login_at,
1430
+ };
1431
+ }
1432
+
1433
+ // Session operations
1434
+ export const SessionDB = {
1435
+ // Create a new session
1436
+ create(session: { user_id: string; refresh_token_hash: string; expires_at: string }): Session {
1437
+ const id = generateId();
1438
+ const now = new Date().toISOString();
1439
+
1440
+ db.run(
1441
+ `INSERT INTO sessions (id, user_id, refresh_token_hash, expires_at, created_at)
1442
+ VALUES (?, ?, ?, ?, ?)`,
1443
+ [id, session.user_id, session.refresh_token_hash, session.expires_at, now]
1444
+ );
1445
+
1446
+ return this.findById(id)!;
1447
+ },
1448
+
1449
+ // Find session by ID
1450
+ findById(id: string): Session | null {
1451
+ const row = db.query("SELECT * FROM sessions WHERE id = ?").get(id) as SessionRow | null;
1452
+ return row ? rowToSession(row) : null;
1453
+ },
1454
+
1455
+ // Find session by refresh token hash
1456
+ findByTokenHash(tokenHash: string): Session | null {
1457
+ const row = db.query("SELECT * FROM sessions WHERE refresh_token_hash = ?").get(tokenHash) as SessionRow | null;
1458
+ return row ? rowToSession(row) : null;
1459
+ },
1460
+
1461
+ // Get all sessions for a user
1462
+ findByUser(userId: string): Session[] {
1463
+ const rows = db.query("SELECT * FROM sessions WHERE user_id = ? ORDER BY created_at DESC").all(userId) as SessionRow[];
1464
+ return rows.map(rowToSession);
1465
+ },
1466
+
1467
+ // Delete session
1468
+ delete(id: string): boolean {
1469
+ const result = db.run("DELETE FROM sessions WHERE id = ?", [id]);
1470
+ return result.changes > 0;
1471
+ },
1472
+
1473
+ // Delete session by token hash
1474
+ deleteByTokenHash(tokenHash: string): boolean {
1475
+ const result = db.run("DELETE FROM sessions WHERE refresh_token_hash = ?", [tokenHash]);
1476
+ return result.changes > 0;
1477
+ },
1478
+
1479
+ // Delete all sessions for a user
1480
+ deleteByUser(userId: string): number {
1481
+ const result = db.run("DELETE FROM sessions WHERE user_id = ?", [userId]);
1482
+ return result.changes;
1483
+ },
1484
+
1485
+ // Delete expired sessions
1486
+ deleteExpired(): number {
1487
+ const result = db.run("DELETE FROM sessions WHERE expires_at < ?", [new Date().toISOString()]);
1488
+ return result.changes;
1489
+ },
1490
+
1491
+ // Check if session is valid (exists and not expired)
1492
+ isValid(id: string): boolean {
1493
+ const session = this.findById(id);
1494
+ if (!session) return false;
1495
+ return new Date(session.expires_at) > new Date();
1496
+ },
1497
+ };
1498
+
1499
+ // Helper to convert DB row to Session type
1500
+ function rowToSession(row: SessionRow): Session {
1501
+ return {
1502
+ id: row.id,
1503
+ user_id: row.user_id,
1504
+ refresh_token_hash: row.refresh_token_hash,
1505
+ expires_at: row.expires_at,
1506
+ created_at: row.created_at,
1507
+ };
1508
+ }
1509
+
972
1510
  // Generate unique ID
973
1511
  export function generateId(): string {
974
1512
  return Math.random().toString(36).substring(2, 15);