apteva 0.2.3 → 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 +492 -3
  8. package/src/mcp-client.ts +599 -0
  9. package/src/providers.ts +31 -0
  10. package/src/routes/api.ts +786 -63
  11. package/src/server.ts +122 -5
  12. package/src/web/App.tsx +36 -1
  13. package/src/web/components/agents/AgentCard.tsx +22 -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 +7 -7
  17. package/src/web/components/agents/index.ts +1 -1
  18. package/src/web/components/common/Icons.tsx +8 -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 +1 -0
  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 +23 -0
  33. package/src/web/styles.css +18 -0
  34. package/src/web/types.ts +75 -1
  35. package/dist/App.wfhmfhx7.js +0 -213
  36. package/src/web/components/agents/ChatPanel.tsx +0 -63
package/src/db.ts CHANGED
@@ -1,6 +1,7 @@
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
6
7
  export interface AgentFeatures {
@@ -30,6 +31,7 @@ export interface Agent {
30
31
  status: "stopped" | "running";
31
32
  port: number | null;
32
33
  features: AgentFeatures;
34
+ mcp_servers: string[]; // Array of MCP server IDs
33
35
  created_at: string;
34
36
  updated_at: string;
35
37
  }
@@ -43,6 +45,7 @@ export interface AgentRow {
43
45
  status: string;
44
46
  port: number | null;
45
47
  features: string | null;
48
+ mcp_servers: string | null;
46
49
  created_at: string;
47
50
  updated_at: string;
48
51
  }
@@ -72,6 +75,32 @@ export interface ProviderKeyRow {
72
75
  created_at: string;
73
76
  }
74
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
+
75
104
  // Database instance
76
105
  let db: Database;
77
106
 
@@ -193,6 +222,56 @@ function runMigrations() {
193
222
  ALTER TABLE agents ADD COLUMN features TEXT DEFAULT '{"memory":true,"tasks":false,"vision":true,"operator":false,"mcp":false,"realtime":false}';
194
223
  `,
195
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
+ },
196
275
  ];
197
276
 
198
277
  // Check which migrations have been applied
@@ -218,11 +297,12 @@ export const AgentDB = {
218
297
  create(agent: Omit<Agent, "created_at" | "updated_at" | "status" | "port">): Agent {
219
298
  const now = new Date().toISOString();
220
299
  const featuresJson = JSON.stringify(agent.features || DEFAULT_FEATURES);
300
+ const mcpServersJson = JSON.stringify(agent.mcp_servers || []);
221
301
  const stmt = db.prepare(`
222
- INSERT INTO agents (id, name, model, provider, system_prompt, features, status, port, created_at, updated_at)
223
- 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, ?, ?)
224
304
  `);
225
- stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, now, now);
305
+ stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, mcpServersJson, now, now);
226
306
  return this.findById(agent.id)!;
227
307
  },
228
308
 
@@ -238,6 +318,12 @@ export const AgentDB = {
238
318
  return rows.map(rowToAgent);
239
319
  },
240
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
+
241
327
  // Update agent
242
328
  update(id: string, updates: Partial<Omit<Agent, "id" | "created_at">>): Agent | null {
243
329
  const agent = this.findById(id);
@@ -274,6 +360,10 @@ export const AgentDB = {
274
360
  fields.push("features = ?");
275
361
  values.push(JSON.stringify(updates.features));
276
362
  }
363
+ if (updates.mcp_servers !== undefined) {
364
+ fields.push("mcp_servers = ?");
365
+ values.push(JSON.stringify(updates.mcp_servers));
366
+ }
277
367
 
278
368
  if (fields.length > 0) {
279
369
  fields.push("updated_at = ?");
@@ -385,6 +475,14 @@ function rowToAgent(row: AgentRow): Agent {
385
475
  // Use defaults if parsing fails
386
476
  }
387
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
+ }
388
486
  return {
389
487
  id: row.id,
390
488
  name: row.name,
@@ -394,6 +492,7 @@ function rowToAgent(row: AgentRow): Agent {
394
492
  status: row.status as "stopped" | "running",
395
493
  port: row.port,
396
494
  features,
495
+ mcp_servers,
397
496
  created_at: row.created_at,
398
497
  updated_at: row.updated_at,
399
498
  };
@@ -480,6 +579,396 @@ function rowToProviderKey(row: ProviderKeyRow): ProviderKey {
480
579
  };
481
580
  }
482
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
+
483
972
  // Generate unique ID
484
973
  export function generateId(): string {
485
974
  return Math.random().toString(36).substring(2, 15);