apteva 0.2.2 → 0.2.5

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 (36) hide show
  1. package/dist/App.ggy88vnx.js +213 -0
  2. package/dist/index.html +1 -1
  3. package/dist/styles.css +1 -1
  4. package/package.json +6 -6
  5. package/src/binary.ts +271 -1
  6. package/src/crypto.ts +53 -0
  7. package/src/db.ts +532 -3
  8. package/src/mcp-client.ts +599 -0
  9. package/src/providers.ts +31 -0
  10. package/src/routes/api.ts +793 -65
  11. package/src/server.ts +122 -5
  12. package/src/web/App.tsx +39 -1
  13. package/src/web/components/agents/AgentCard.tsx +49 -1
  14. package/src/web/components/agents/AgentPanel.tsx +381 -0
  15. package/src/web/components/agents/AgentsView.tsx +27 -10
  16. package/src/web/components/agents/CreateAgentModal.tsx +46 -1
  17. package/src/web/components/agents/index.ts +1 -1
  18. package/src/web/components/common/Icons.tsx +58 -0
  19. package/src/web/components/common/Modal.tsx +2 -2
  20. package/src/web/components/common/Select.tsx +1 -1
  21. package/src/web/components/common/index.ts +14 -1
  22. package/src/web/components/dashboard/Dashboard.tsx +74 -25
  23. package/src/web/components/index.ts +5 -2
  24. package/src/web/components/layout/Sidebar.tsx +22 -2
  25. package/src/web/components/mcp/McpPage.tsx +1144 -0
  26. package/src/web/components/mcp/index.ts +1 -0
  27. package/src/web/components/onboarding/OnboardingWizard.tsx +5 -1
  28. package/src/web/components/settings/SettingsPage.tsx +312 -82
  29. package/src/web/components/tasks/TasksPage.tsx +129 -0
  30. package/src/web/components/tasks/index.ts +1 -0
  31. package/src/web/components/telemetry/TelemetryPage.tsx +316 -0
  32. package/src/web/hooks/useAgents.ts +25 -1
  33. package/src/web/styles.css +18 -0
  34. package/src/web/types.ts +95 -1
  35. package/dist/App.dc0hb2q1.js +0 -213
  36. package/src/web/components/agents/ChatPanel.tsx +0 -63
package/src/db.ts CHANGED
@@ -1,8 +1,27 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import { join } from "path";
3
3
  import { mkdirSync, existsSync } from "fs";
4
+ import { encryptObject, decryptObject } from "./crypto";
4
5
 
5
6
  // Types
7
+ export interface AgentFeatures {
8
+ memory: boolean;
9
+ tasks: boolean;
10
+ vision: boolean;
11
+ operator: boolean;
12
+ mcp: boolean;
13
+ realtime: boolean;
14
+ }
15
+
16
+ export const DEFAULT_FEATURES: AgentFeatures = {
17
+ memory: true,
18
+ tasks: false,
19
+ vision: true,
20
+ operator: false,
21
+ mcp: false,
22
+ realtime: false,
23
+ };
24
+
6
25
  export interface Agent {
7
26
  id: string;
8
27
  name: string;
@@ -11,6 +30,8 @@ export interface Agent {
11
30
  system_prompt: string;
12
31
  status: "stopped" | "running";
13
32
  port: number | null;
33
+ features: AgentFeatures;
34
+ mcp_servers: string[]; // Array of MCP server IDs
14
35
  created_at: string;
15
36
  updated_at: string;
16
37
  }
@@ -23,6 +44,8 @@ export interface AgentRow {
23
44
  system_prompt: string;
24
45
  status: string;
25
46
  port: number | null;
47
+ features: string | null;
48
+ mcp_servers: string | null;
26
49
  created_at: string;
27
50
  updated_at: string;
28
51
  }
@@ -52,6 +75,32 @@ export interface ProviderKeyRow {
52
75
  created_at: string;
53
76
  }
54
77
 
78
+ export interface McpServer {
79
+ id: string;
80
+ name: string;
81
+ type: "npm" | "github" | "http" | "custom";
82
+ package: string | null;
83
+ command: string | null;
84
+ args: string | null;
85
+ env: Record<string, string>;
86
+ port: number | null;
87
+ status: "stopped" | "running";
88
+ created_at: string;
89
+ }
90
+
91
+ export interface McpServerRow {
92
+ id: string;
93
+ name: string;
94
+ type: string;
95
+ package: string | null;
96
+ command: string | null;
97
+ args: string | null;
98
+ env: string | null;
99
+ port: number | null;
100
+ status: string;
101
+ created_at: string;
102
+ }
103
+
55
104
  // Database instance
56
105
  let db: Database;
57
106
 
@@ -167,6 +216,62 @@ function runMigrations() {
167
216
  CREATE INDEX IF NOT EXISTS idx_provider_keys_provider ON provider_keys(provider_id);
168
217
  `,
169
218
  },
219
+ {
220
+ name: "006_add_agent_features",
221
+ sql: `
222
+ ALTER TABLE agents ADD COLUMN features TEXT DEFAULT '{"memory":true,"tasks":false,"vision":true,"operator":false,"mcp":false,"realtime":false}';
223
+ `,
224
+ },
225
+ {
226
+ name: "007_create_mcp_servers",
227
+ sql: `
228
+ CREATE TABLE IF NOT EXISTS mcp_servers (
229
+ id TEXT PRIMARY KEY,
230
+ name TEXT NOT NULL,
231
+ type TEXT NOT NULL DEFAULT 'npm',
232
+ package TEXT,
233
+ command TEXT,
234
+ args TEXT,
235
+ env TEXT DEFAULT '{}',
236
+ port INTEGER,
237
+ status TEXT NOT NULL DEFAULT 'stopped',
238
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
239
+ );
240
+ CREATE INDEX IF NOT EXISTS idx_mcp_servers_status ON mcp_servers(status);
241
+ `,
242
+ },
243
+ {
244
+ name: "008_add_agent_mcp_servers",
245
+ sql: `
246
+ ALTER TABLE agents ADD COLUMN mcp_servers TEXT DEFAULT '[]';
247
+ `,
248
+ },
249
+ {
250
+ name: "009_create_telemetry",
251
+ sql: `
252
+ CREATE TABLE IF NOT EXISTS telemetry_events (
253
+ id TEXT PRIMARY KEY,
254
+ agent_id TEXT NOT NULL,
255
+ timestamp TEXT NOT NULL,
256
+ category TEXT NOT NULL,
257
+ type TEXT NOT NULL,
258
+ level TEXT NOT NULL,
259
+ trace_id TEXT,
260
+ span_id TEXT,
261
+ thread_id TEXT,
262
+ data TEXT,
263
+ metadata TEXT,
264
+ duration_ms INTEGER,
265
+ error TEXT,
266
+ received_at TEXT NOT NULL
267
+ );
268
+ CREATE INDEX IF NOT EXISTS idx_telemetry_agent ON telemetry_events(agent_id);
269
+ CREATE INDEX IF NOT EXISTS idx_telemetry_time ON telemetry_events(timestamp);
270
+ CREATE INDEX IF NOT EXISTS idx_telemetry_category ON telemetry_events(category);
271
+ CREATE INDEX IF NOT EXISTS idx_telemetry_level ON telemetry_events(level);
272
+ CREATE INDEX IF NOT EXISTS idx_telemetry_trace ON telemetry_events(trace_id);
273
+ `,
274
+ },
170
275
  ];
171
276
 
172
277
  // Check which migrations have been applied
@@ -191,11 +296,13 @@ export const AgentDB = {
191
296
  // Create a new agent
192
297
  create(agent: Omit<Agent, "created_at" | "updated_at" | "status" | "port">): Agent {
193
298
  const now = new Date().toISOString();
299
+ const featuresJson = JSON.stringify(agent.features || DEFAULT_FEATURES);
300
+ const mcpServersJson = JSON.stringify(agent.mcp_servers || []);
194
301
  const stmt = db.prepare(`
195
- INSERT INTO agents (id, name, model, provider, system_prompt, status, port, created_at, updated_at)
196
- VALUES (?, ?, ?, ?, ?, 'stopped', NULL, ?, ?)
302
+ INSERT INTO agents (id, name, model, provider, system_prompt, features, mcp_servers, status, port, created_at, updated_at)
303
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'stopped', NULL, ?, ?)
197
304
  `);
198
- stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, now, now);
305
+ stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, mcpServersJson, now, now);
199
306
  return this.findById(agent.id)!;
200
307
  },
201
308
 
@@ -211,6 +318,12 @@ export const AgentDB = {
211
318
  return rows.map(rowToAgent);
212
319
  },
213
320
 
321
+ // Get running agents (for auto-restart)
322
+ findRunning(): Agent[] {
323
+ const rows = db.query("SELECT * FROM agents WHERE status = 'running'").all() as AgentRow[];
324
+ return rows.map(rowToAgent);
325
+ },
326
+
214
327
  // Update agent
215
328
  update(id: string, updates: Partial<Omit<Agent, "id" | "created_at">>): Agent | null {
216
329
  const agent = this.findById(id);
@@ -243,6 +356,14 @@ export const AgentDB = {
243
356
  fields.push("port = ?");
244
357
  values.push(updates.port);
245
358
  }
359
+ if (updates.features !== undefined) {
360
+ fields.push("features = ?");
361
+ values.push(JSON.stringify(updates.features));
362
+ }
363
+ if (updates.mcp_servers !== undefined) {
364
+ fields.push("mcp_servers = ?");
365
+ values.push(JSON.stringify(updates.mcp_servers));
366
+ }
246
367
 
247
368
  if (fields.length > 0) {
248
369
  fields.push("updated_at = ?");
@@ -346,6 +467,22 @@ export const SettingsDB = {
346
467
 
347
468
  // Helper to convert DB row to Agent type
348
469
  function rowToAgent(row: AgentRow): Agent {
470
+ let features = DEFAULT_FEATURES;
471
+ if (row.features) {
472
+ try {
473
+ features = { ...DEFAULT_FEATURES, ...JSON.parse(row.features) };
474
+ } catch {
475
+ // Use defaults if parsing fails
476
+ }
477
+ }
478
+ let mcp_servers: string[] = [];
479
+ if (row.mcp_servers) {
480
+ try {
481
+ mcp_servers = JSON.parse(row.mcp_servers);
482
+ } catch {
483
+ // Use empty array if parsing fails
484
+ }
485
+ }
349
486
  return {
350
487
  id: row.id,
351
488
  name: row.name,
@@ -354,6 +491,8 @@ function rowToAgent(row: AgentRow): Agent {
354
491
  system_prompt: row.system_prompt,
355
492
  status: row.status as "stopped" | "running",
356
493
  port: row.port,
494
+ features,
495
+ mcp_servers,
357
496
  created_at: row.created_at,
358
497
  updated_at: row.updated_at,
359
498
  };
@@ -440,6 +579,396 @@ function rowToProviderKey(row: ProviderKeyRow): ProviderKey {
440
579
  };
441
580
  }
442
581
 
582
+ // MCP Server operations
583
+ export const McpServerDB = {
584
+ create(server: Omit<McpServer, "created_at" | "status" | "port">): McpServer {
585
+ const now = new Date().toISOString();
586
+ // Encrypt env vars (credentials) before storing
587
+ const envEncrypted = encryptObject(server.env || {});
588
+ const stmt = db.prepare(`
589
+ INSERT INTO mcp_servers (id, name, type, package, command, args, env, status, port, created_at)
590
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'stopped', NULL, ?)
591
+ `);
592
+ stmt.run(server.id, server.name, server.type, server.package, server.command, server.args, envEncrypted, now);
593
+ return this.findById(server.id)!;
594
+ },
595
+
596
+ findById(id: string): McpServer | null {
597
+ const row = db.query("SELECT * FROM mcp_servers WHERE id = ?").get(id) as McpServerRow | null;
598
+ return row ? rowToMcpServer(row) : null;
599
+ },
600
+
601
+ findAll(): McpServer[] {
602
+ const rows = db.query("SELECT * FROM mcp_servers ORDER BY created_at DESC").all() as McpServerRow[];
603
+ return rows.map(rowToMcpServer);
604
+ },
605
+
606
+ findRunning(): McpServer[] {
607
+ const rows = db.query("SELECT * FROM mcp_servers WHERE status = 'running'").all() as McpServerRow[];
608
+ return rows.map(rowToMcpServer);
609
+ },
610
+
611
+ update(id: string, updates: Partial<Omit<McpServer, "id" | "created_at">>): McpServer | null {
612
+ const server = this.findById(id);
613
+ if (!server) return null;
614
+
615
+ const fields: string[] = [];
616
+ const values: unknown[] = [];
617
+
618
+ if (updates.name !== undefined) {
619
+ fields.push("name = ?");
620
+ values.push(updates.name);
621
+ }
622
+ if (updates.type !== undefined) {
623
+ fields.push("type = ?");
624
+ values.push(updates.type);
625
+ }
626
+ if (updates.package !== undefined) {
627
+ fields.push("package = ?");
628
+ values.push(updates.package);
629
+ }
630
+ if (updates.command !== undefined) {
631
+ fields.push("command = ?");
632
+ values.push(updates.command);
633
+ }
634
+ if (updates.args !== undefined) {
635
+ fields.push("args = ?");
636
+ values.push(updates.args);
637
+ }
638
+ if (updates.env !== undefined) {
639
+ fields.push("env = ?");
640
+ // Encrypt env vars (credentials) before storing
641
+ values.push(encryptObject(updates.env));
642
+ }
643
+ if (updates.port !== undefined) {
644
+ fields.push("port = ?");
645
+ values.push(updates.port);
646
+ }
647
+ if (updates.status !== undefined) {
648
+ fields.push("status = ?");
649
+ values.push(updates.status);
650
+ }
651
+
652
+ if (fields.length > 0) {
653
+ values.push(id);
654
+ db.run(`UPDATE mcp_servers SET ${fields.join(", ")} WHERE id = ?`, values);
655
+ }
656
+
657
+ return this.findById(id);
658
+ },
659
+
660
+ setStatus(id: string, status: "stopped" | "running", port?: number): McpServer | null {
661
+ return this.update(id, { status, port: port ?? null });
662
+ },
663
+
664
+ delete(id: string): boolean {
665
+ const result = db.run("DELETE FROM mcp_servers WHERE id = ?", [id]);
666
+ return result.changes > 0;
667
+ },
668
+
669
+ resetAllStatus(): void {
670
+ db.run("UPDATE mcp_servers SET status = 'stopped', port = NULL");
671
+ },
672
+
673
+ count(): number {
674
+ const row = db.query("SELECT COUNT(*) as count FROM mcp_servers").get() as { count: number };
675
+ return row.count;
676
+ },
677
+ };
678
+
679
+ // Helper to convert DB row to McpServer type
680
+ function rowToMcpServer(row: McpServerRow): McpServer {
681
+ // Decrypt env vars (handles both encrypted and legacy unencrypted data)
682
+ const env = row.env ? decryptObject(row.env) : {};
683
+ return {
684
+ id: row.id,
685
+ name: row.name,
686
+ type: row.type as McpServer["type"],
687
+ package: row.package,
688
+ command: row.command,
689
+ args: row.args,
690
+ env,
691
+ port: row.port,
692
+ status: row.status as "stopped" | "running",
693
+ created_at: row.created_at,
694
+ };
695
+ }
696
+
697
+ // Telemetry Event types
698
+ export interface TelemetryEvent {
699
+ id: string;
700
+ agent_id: string;
701
+ timestamp: string;
702
+ category: string;
703
+ type: string;
704
+ level: string;
705
+ trace_id: string | null;
706
+ span_id: string | null;
707
+ thread_id: string | null;
708
+ data: Record<string, unknown> | null;
709
+ metadata: Record<string, unknown> | null;
710
+ duration_ms: number | null;
711
+ error: string | null;
712
+ received_at: string;
713
+ }
714
+
715
+ interface TelemetryEventRow {
716
+ id: string;
717
+ agent_id: string;
718
+ timestamp: string;
719
+ category: string;
720
+ type: string;
721
+ level: string;
722
+ trace_id: string | null;
723
+ span_id: string | null;
724
+ thread_id: string | null;
725
+ data: string | null;
726
+ metadata: string | null;
727
+ duration_ms: number | null;
728
+ error: string | null;
729
+ received_at: string;
730
+ }
731
+
732
+ // Telemetry operations
733
+ export const TelemetryDB = {
734
+ // Insert batch of events
735
+ insertBatch(agentId: string, events: Array<{
736
+ id: string;
737
+ timestamp: string;
738
+ category: string;
739
+ type: string;
740
+ level: string;
741
+ trace_id?: string;
742
+ span_id?: string;
743
+ thread_id?: string;
744
+ data?: Record<string, unknown>;
745
+ metadata?: Record<string, unknown>;
746
+ duration_ms?: number;
747
+ error?: string;
748
+ }>): number {
749
+ const now = new Date().toISOString();
750
+ const stmt = db.prepare(`
751
+ INSERT OR IGNORE INTO telemetry_events
752
+ (id, agent_id, timestamp, category, type, level, trace_id, span_id, thread_id, data, metadata, duration_ms, error, received_at)
753
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
754
+ `);
755
+
756
+ let inserted = 0;
757
+ for (const event of events) {
758
+ const result = stmt.run(
759
+ event.id,
760
+ agentId,
761
+ event.timestamp,
762
+ event.category,
763
+ event.type,
764
+ event.level,
765
+ event.trace_id || null,
766
+ event.span_id || null,
767
+ event.thread_id || null,
768
+ event.data ? JSON.stringify(event.data) : null,
769
+ event.metadata ? JSON.stringify(event.metadata) : null,
770
+ event.duration_ms || null,
771
+ event.error || null,
772
+ now
773
+ );
774
+ if (result.changes > 0) inserted++;
775
+ }
776
+ return inserted;
777
+ },
778
+
779
+ // Query events with filters
780
+ query(filters: {
781
+ agent_id?: string;
782
+ category?: string;
783
+ level?: string;
784
+ trace_id?: string;
785
+ since?: string;
786
+ until?: string;
787
+ limit?: number;
788
+ offset?: number;
789
+ } = {}): TelemetryEvent[] {
790
+ const conditions: string[] = [];
791
+ const params: unknown[] = [];
792
+
793
+ if (filters.agent_id) {
794
+ conditions.push("agent_id = ?");
795
+ params.push(filters.agent_id);
796
+ }
797
+ if (filters.category) {
798
+ conditions.push("category = ?");
799
+ params.push(filters.category);
800
+ }
801
+ if (filters.level) {
802
+ conditions.push("level = ?");
803
+ params.push(filters.level);
804
+ }
805
+ if (filters.trace_id) {
806
+ conditions.push("trace_id = ?");
807
+ params.push(filters.trace_id);
808
+ }
809
+ if (filters.since) {
810
+ conditions.push("timestamp >= ?");
811
+ params.push(filters.since);
812
+ }
813
+ if (filters.until) {
814
+ conditions.push("timestamp <= ?");
815
+ params.push(filters.until);
816
+ }
817
+
818
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
819
+ const limit = filters.limit || 100;
820
+ const offset = filters.offset || 0;
821
+
822
+ const sql = `SELECT * FROM telemetry_events ${where} ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
823
+ params.push(limit, offset);
824
+
825
+ const rows = db.query(sql).all(...params) as TelemetryEventRow[];
826
+ return rows.map(rowToTelemetryEvent);
827
+ },
828
+
829
+ // Get usage stats
830
+ getUsage(filters: {
831
+ agent_id?: string;
832
+ since?: string;
833
+ until?: string;
834
+ group_by?: "agent" | "day";
835
+ } = {}): Array<{
836
+ agent_id?: string;
837
+ date?: string;
838
+ input_tokens: number;
839
+ output_tokens: number;
840
+ llm_calls: number;
841
+ tool_calls: number;
842
+ errors: number;
843
+ }> {
844
+ const conditions: string[] = [];
845
+ const params: unknown[] = [];
846
+
847
+ if (filters.agent_id) {
848
+ conditions.push("agent_id = ?");
849
+ params.push(filters.agent_id);
850
+ }
851
+ if (filters.since) {
852
+ conditions.push("timestamp >= ?");
853
+ params.push(filters.since);
854
+ }
855
+ if (filters.until) {
856
+ conditions.push("timestamp <= ?");
857
+ params.push(filters.until);
858
+ }
859
+
860
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
861
+
862
+ let groupBy = "";
863
+ let selectFields = "";
864
+
865
+ if (filters.group_by === "day") {
866
+ groupBy = "GROUP BY date(timestamp)";
867
+ selectFields = "date(timestamp) as date,";
868
+ } else if (filters.group_by === "agent") {
869
+ groupBy = "GROUP BY agent_id";
870
+ selectFields = "agent_id,";
871
+ }
872
+
873
+ const sql = `
874
+ SELECT
875
+ ${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
882
+ ${where}
883
+ ${groupBy}
884
+ `;
885
+
886
+ return db.query(sql).all(...params) as Array<{
887
+ agent_id?: string;
888
+ date?: string;
889
+ input_tokens: number;
890
+ output_tokens: number;
891
+ llm_calls: number;
892
+ tool_calls: number;
893
+ errors: number;
894
+ }>;
895
+ },
896
+
897
+ // Get summary stats
898
+ getStats(agentId?: string): {
899
+ total_events: number;
900
+ total_llm_calls: number;
901
+ total_tool_calls: number;
902
+ total_errors: number;
903
+ total_input_tokens: number;
904
+ total_output_tokens: number;
905
+ } {
906
+ const where = agentId ? "WHERE agent_id = ?" : "";
907
+ const params = agentId ? [agentId] : [];
908
+
909
+ const sql = `
910
+ SELECT
911
+ 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
918
+ ${where}
919
+ `;
920
+
921
+ return db.query(sql).get(...params) as {
922
+ total_events: number;
923
+ total_llm_calls: number;
924
+ total_tool_calls: number;
925
+ total_errors: number;
926
+ total_input_tokens: number;
927
+ total_output_tokens: number;
928
+ };
929
+ },
930
+
931
+ // Delete old events (retention)
932
+ deleteOlderThan(days: number): number {
933
+ const cutoff = new Date();
934
+ cutoff.setDate(cutoff.getDate() - days);
935
+ const result = db.run(
936
+ "DELETE FROM telemetry_events WHERE timestamp < ?",
937
+ [cutoff.toISOString()]
938
+ );
939
+ return result.changes;
940
+ },
941
+
942
+ // Count events
943
+ count(agentId?: string): number {
944
+ if (agentId) {
945
+ const row = db.query("SELECT COUNT(*) as count FROM telemetry_events WHERE agent_id = ?").get(agentId) as { count: number };
946
+ return row.count;
947
+ }
948
+ const row = db.query("SELECT COUNT(*) as count FROM telemetry_events").get() as { count: number };
949
+ return row.count;
950
+ },
951
+ };
952
+
953
+ function rowToTelemetryEvent(row: TelemetryEventRow): TelemetryEvent {
954
+ return {
955
+ id: row.id,
956
+ agent_id: row.agent_id,
957
+ timestamp: row.timestamp,
958
+ category: row.category,
959
+ type: row.type,
960
+ level: row.level,
961
+ trace_id: row.trace_id,
962
+ span_id: row.span_id,
963
+ thread_id: row.thread_id,
964
+ data: row.data ? JSON.parse(row.data) : null,
965
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
966
+ duration_ms: row.duration_ms,
967
+ error: row.error,
968
+ received_at: row.received_at,
969
+ };
970
+ }
971
+
443
972
  // Generate unique ID
444
973
  export function generateId(): string {
445
974
  return Math.random().toString(36).substring(2, 15);