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.
- package/dist/App.ggy88vnx.js +213 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -6
- package/src/binary.ts +271 -1
- package/src/crypto.ts +53 -0
- package/src/db.ts +532 -3
- package/src/mcp-client.ts +599 -0
- package/src/providers.ts +31 -0
- package/src/routes/api.ts +793 -65
- package/src/server.ts +122 -5
- package/src/web/App.tsx +39 -1
- package/src/web/components/agents/AgentCard.tsx +49 -1
- package/src/web/components/agents/AgentPanel.tsx +381 -0
- package/src/web/components/agents/AgentsView.tsx +27 -10
- package/src/web/components/agents/CreateAgentModal.tsx +46 -1
- package/src/web/components/agents/index.ts +1 -1
- package/src/web/components/common/Icons.tsx +58 -0
- package/src/web/components/common/Modal.tsx +2 -2
- package/src/web/components/common/Select.tsx +1 -1
- package/src/web/components/common/index.ts +14 -1
- package/src/web/components/dashboard/Dashboard.tsx +74 -25
- package/src/web/components/index.ts +5 -2
- package/src/web/components/layout/Sidebar.tsx +22 -2
- package/src/web/components/mcp/McpPage.tsx +1144 -0
- package/src/web/components/mcp/index.ts +1 -0
- package/src/web/components/onboarding/OnboardingWizard.tsx +5 -1
- package/src/web/components/settings/SettingsPage.tsx +312 -82
- package/src/web/components/tasks/TasksPage.tsx +129 -0
- package/src/web/components/tasks/index.ts +1 -0
- package/src/web/components/telemetry/TelemetryPage.tsx +316 -0
- package/src/web/hooks/useAgents.ts +25 -1
- package/src/web/styles.css +18 -0
- package/src/web/types.ts +95 -1
- package/dist/App.dc0hb2q1.js +0 -213
- 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);
|