@johpaz/hive 1.1.0
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/CONTRIBUTING.md +44 -0
- package/README.md +310 -0
- package/package.json +96 -0
- package/packages/cli/package.json +28 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +50 -0
- package/packages/cli/src/commands/cron.ts +161 -0
- package/packages/cli/src/commands/dev.ts +95 -0
- package/packages/cli/src/commands/doctor.ts +133 -0
- package/packages/cli/src/commands/gateway.ts +443 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/onboard.ts +1868 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +187 -0
- package/packages/cli/src/commands/update.ts +25 -0
- package/packages/cli/src/index.ts +185 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/code-bridge/README.md +78 -0
- package/packages/code-bridge/package.json +18 -0
- package/packages/code-bridge/src/index.ts +95 -0
- package/packages/code-bridge/src/process-manager.ts +212 -0
- package/packages/code-bridge/src/schemas.ts +133 -0
- package/packages/core/package.json +46 -0
- package/packages/core/src/agent/agent-loop.ts +369 -0
- package/packages/core/src/agent/compaction.ts +140 -0
- package/packages/core/src/agent/context-compiler.ts +378 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/context.ts +138 -0
- package/packages/core/src/agent/conversation-store.ts +198 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/index.ts +116 -0
- package/packages/core/src/agent/llm-client.ts +503 -0
- package/packages/core/src/agent/native-tools.ts +505 -0
- package/packages/core/src/agent/prompt-builder.ts +532 -0
- package/packages/core/src/agent/providers/index.ts +167 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +170 -0
- package/packages/core/src/agent/service.ts +64 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/supervisor.ts +39 -0
- package/packages/core/src/agent/tracer.ts +102 -0
- package/packages/core/src/agent/workspace.ts +110 -0
- package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
- package/packages/core/src/canvas/canvas-manager.ts +319 -0
- package/packages/core/src/canvas/canvas-tools.ts +420 -0
- package/packages/core/src/canvas/emitter.ts +115 -0
- package/packages/core/src/canvas/index.ts +2 -0
- package/packages/core/src/channels/base.ts +138 -0
- package/packages/core/src/channels/discord.ts +260 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +383 -0
- package/packages/core/src/channels/slack.ts +287 -0
- package/packages/core/src/channels/telegram.ts +502 -0
- package/packages/core/src/channels/webchat.ts +128 -0
- package/packages/core/src/channels/whatsapp.ts +375 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +529 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +290 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/server.ts +3317 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +192 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +19 -0
- package/packages/core/src/integrations/catalog.ts +286 -0
- package/packages/core/src/integrations/env.ts +64 -0
- package/packages/core/src/integrations/index.ts +2 -0
- package/packages/core/src/memory/index.ts +1 -0
- package/packages/core/src/memory/notes.ts +68 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/db-context.ts +333 -0
- package/packages/core/src/storage/onboarding.ts +1087 -0
- package/packages/core/src/storage/schema.ts +541 -0
- package/packages/core/src/storage/seed.ts +571 -0
- package/packages/core/src/storage/sqlite.ts +387 -0
- package/packages/core/src/storage/usage.ts +212 -0
- package/packages/core/src/tools/bridge-events.ts +74 -0
- package/packages/core/src/tools/browser.ts +275 -0
- package/packages/core/src/tools/codebridge.ts +421 -0
- package/packages/core/src/tools/coordinator-tools.ts +179 -0
- package/packages/core/src/tools/cron.ts +611 -0
- package/packages/core/src/tools/exec.ts +140 -0
- package/packages/core/src/tools/fs.ts +364 -0
- package/packages/core/src/tools/index.ts +12 -0
- package/packages/core/src/tools/memory.ts +176 -0
- package/packages/core/src/tools/notify.ts +113 -0
- package/packages/core/src/tools/project-management.ts +376 -0
- package/packages/core/src/tools/project.ts +375 -0
- package/packages/core/src/tools/read.ts +158 -0
- package/packages/core/src/tools/web.ts +436 -0
- package/packages/core/src/tools/workspace.ts +171 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +4 -0
- package/packages/core/src/utils/logger.ts +388 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/voice/index.ts +583 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/mcp/package.json +26 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +42 -0
- package/packages/mcp/src/manager.ts +434 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +241 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/package.json +21 -0
- package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
- package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
- package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
- package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
- package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
- package/packages/skills/src/bundled/memory/SKILL.md +42 -0
- package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
- package/packages/skills/src/bundled/shell/SKILL.md +43 -0
- package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
- package/packages/skills/src/bundled/voice/SKILL.md +25 -0
- package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +282 -0
- package/packages/tools/package.json +43 -0
- package/packages/tools/src/browser/browser.test.ts +111 -0
- package/packages/tools/src/browser/index.ts +272 -0
- package/packages/tools/src/canvas/index.ts +220 -0
- package/packages/tools/src/cron/cron.test.ts +164 -0
- package/packages/tools/src/cron/index.ts +304 -0
- package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
- package/packages/tools/src/filesystem/index.ts +379 -0
- package/packages/tools/src/git/index.ts +239 -0
- package/packages/tools/src/index.ts +4 -0
- package/packages/tools/src/shell/detect-env.ts +70 -0
- package/packages/tools/tsconfig.json +9 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { logger } from "../utils/logger.ts";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { getHiveDir } from "../config/loader.ts";
|
|
6
|
+
import { SCHEMA, PROJECTS_SCHEMA, CONTEXT_ENGINE_SCHEMA } from "./schema.ts";
|
|
7
|
+
|
|
8
|
+
function getDbPath(): string {
|
|
9
|
+
return path.join(getHiveDir(), "data", "hive.db");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getDbPathLazy(): string {
|
|
13
|
+
return getDbPath();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let _db: Database | null = null;
|
|
17
|
+
|
|
18
|
+
export interface ChatMessageRow {
|
|
19
|
+
id: string;
|
|
20
|
+
session_id: string;
|
|
21
|
+
thread_id: string;
|
|
22
|
+
role: string;
|
|
23
|
+
content: string;
|
|
24
|
+
created_at: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface NoteRow {
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
content: string;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getDb(): Database {
|
|
36
|
+
if (!_db) throw new Error("DB no inicializada. Llama initializeDatabase() primero.");
|
|
37
|
+
return _db;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export function initializeDatabase(): Database {
|
|
42
|
+
const hiveDir = getHiveDir();
|
|
43
|
+
const dir = path.join(hiveDir, "data");
|
|
44
|
+
if (!existsSync(dir)) {
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const dbPath = getDbPath();
|
|
49
|
+
const dbFileExists = existsSync(dbPath);
|
|
50
|
+
|
|
51
|
+
_db = new Database(dbPath, { create: true });
|
|
52
|
+
|
|
53
|
+
// Use type assertion to avoid deprecated signature with bindings
|
|
54
|
+
(_db as any).exec(SCHEMA);
|
|
55
|
+
(_db as any).exec(PROJECTS_SCHEMA);
|
|
56
|
+
(_db as any).exec(CONTEXT_ENGINE_SCHEMA);
|
|
57
|
+
|
|
58
|
+
ensureSchemaSync();
|
|
59
|
+
|
|
60
|
+
return _db;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ensureColumnExists(tableName: string, columnName: string, columnDefinition: string): void {
|
|
64
|
+
if (!_db) return;
|
|
65
|
+
try {
|
|
66
|
+
const info = _db.query(`PRAGMA table_info(${tableName})`).all() as any[];
|
|
67
|
+
const exists = info.some((col: any) => col.name === columnName);
|
|
68
|
+
|
|
69
|
+
if (!exists) {
|
|
70
|
+
logger.info(`🛠️ Añadiendo columna faltante '${columnName}' a la tabla '${tableName}'`);
|
|
71
|
+
_db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDefinition}`);
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logger.warn(`⚠️ No se pudo verificar/añadir la columna '${columnName}' en '${tableName}':`, { error: (err as Error).message });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ensureSchemaSync(): void {
|
|
79
|
+
if (!_db) return;
|
|
80
|
+
|
|
81
|
+
// Sync mcp_servers
|
|
82
|
+
ensureColumnExists("mcp_servers", "tools_count", "INTEGER DEFAULT 0");
|
|
83
|
+
ensureColumnExists("mcp_servers", "status", "TEXT NOT NULL DEFAULT 'disconnected'");
|
|
84
|
+
ensureColumnExists("mcp_servers", "env_encrypted", "TEXT");
|
|
85
|
+
ensureColumnExists("mcp_servers", "env_iv", "TEXT");
|
|
86
|
+
ensureColumnExists("mcp_servers", "headers_encrypted", "TEXT");
|
|
87
|
+
ensureColumnExists("mcp_servers", "headers_iv", "TEXT");
|
|
88
|
+
|
|
89
|
+
// Sync providers
|
|
90
|
+
ensureColumnExists("providers", "api_key_encrypted", "TEXT");
|
|
91
|
+
ensureColumnExists("providers", "api_key_iv", "TEXT");
|
|
92
|
+
ensureColumnExists("providers", "headers_encrypted", "TEXT");
|
|
93
|
+
ensureColumnExists("providers", "headers_iv", "TEXT");
|
|
94
|
+
ensureColumnExists("providers", "num_ctx", "INTEGER");
|
|
95
|
+
ensureColumnExists("providers", "num_gpu", "INTEGER DEFAULT -1");
|
|
96
|
+
|
|
97
|
+
// Sync agents (new Context Engine columns — safe no-ops if already present)
|
|
98
|
+
ensureColumnExists("agents", "headers_encrypted", "TEXT");
|
|
99
|
+
ensureColumnExists("agents", "headers_iv", "TEXT");
|
|
100
|
+
ensureColumnExists("agents", "system_prompt", "TEXT");
|
|
101
|
+
ensureColumnExists("agents", "role", "TEXT NOT NULL DEFAULT 'coordinator'");
|
|
102
|
+
ensureColumnExists("agents", "tools_json", "TEXT");
|
|
103
|
+
ensureColumnExists("agents", "skills_json", "TEXT");
|
|
104
|
+
ensureColumnExists("agents", "parent_id", "TEXT");
|
|
105
|
+
ensureColumnExists("agents", "max_iterations", "INTEGER NOT NULL DEFAULT 10");
|
|
106
|
+
ensureColumnExists("agents", "updated_at", "INTEGER NOT NULL DEFAULT (unixepoch())");
|
|
107
|
+
|
|
108
|
+
// Sync tasks (new Context Engine columns)
|
|
109
|
+
ensureColumnExists("tasks", "priority", "INTEGER NOT NULL DEFAULT 0");
|
|
110
|
+
ensureColumnExists("tasks", "depends_on", "TEXT");
|
|
111
|
+
ensureColumnExists("tasks", "error", "TEXT");
|
|
112
|
+
ensureColumnExists("tasks", "completed_at", "INTEGER");
|
|
113
|
+
|
|
114
|
+
// Sync cron_jobs
|
|
115
|
+
ensureColumnExists("cron_jobs", "max_runs", "INTEGER");
|
|
116
|
+
ensureColumnExists("cron_jobs", "run_count", "INTEGER NOT NULL DEFAULT 0");
|
|
117
|
+
ensureColumnExists("cron_jobs", "expires_at", "INTEGER");
|
|
118
|
+
|
|
119
|
+
// hive_capabilities: create if not exists (applied via CONTEXT_ENGINE_SCHEMA IF NOT EXISTS)
|
|
120
|
+
// No column migrations needed — table is seeded fresh each startup via INSERT OR REPLACE
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
export class DatabaseService {
|
|
125
|
+
private log = logger.child("sqlite");
|
|
126
|
+
|
|
127
|
+
private get db(): Database {
|
|
128
|
+
if (!_db) {
|
|
129
|
+
initializeDatabase();
|
|
130
|
+
}
|
|
131
|
+
return _db!;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public addMessage(sessionId: string, role: string, content: string, metadata?: Record<string, unknown>): void {
|
|
135
|
+
const stmt = this.db.query(`
|
|
136
|
+
INSERT INTO messages (id, session_id, thread_id, role, content, metadata)
|
|
137
|
+
VALUES ($id, $sessionId, $threadId, $role, $content, $metadata)
|
|
138
|
+
`);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const id = crypto.randomUUID();
|
|
142
|
+
stmt.run({
|
|
143
|
+
$id: id,
|
|
144
|
+
$sessionId: sessionId,
|
|
145
|
+
$threadId: sessionId,
|
|
146
|
+
$role: role,
|
|
147
|
+
$content: content,
|
|
148
|
+
$metadata: metadata ? JSON.stringify(metadata) : null
|
|
149
|
+
});
|
|
150
|
+
this.log.debug(`Message added for session ${sessionId}`);
|
|
151
|
+
} catch (error: any) {
|
|
152
|
+
this.log.error(`Failed to add message: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public getMessages(sessionId: string, limit: number = 100): ChatMessageRow[] {
|
|
157
|
+
const stmt = this.db.query(`
|
|
158
|
+
SELECT * FROM messages
|
|
159
|
+
WHERE session_id = $sessionId
|
|
160
|
+
ORDER BY created_at DESC
|
|
161
|
+
LIMIT $limit
|
|
162
|
+
`);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const rows = stmt.all({
|
|
166
|
+
$sessionId: sessionId,
|
|
167
|
+
$limit: limit
|
|
168
|
+
}) as ChatMessageRow[];
|
|
169
|
+
|
|
170
|
+
return rows.reverse();
|
|
171
|
+
} catch (error: any) {
|
|
172
|
+
this.log.error(`Failed to get messages: ${error.message}`);
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public close(): void {
|
|
178
|
+
if (_db) {
|
|
179
|
+
_db.close();
|
|
180
|
+
_db = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public writeNote(title: string, content: string): NoteRow {
|
|
185
|
+
const stmt = this.db.query(`
|
|
186
|
+
INSERT INTO notes (id, title, content)
|
|
187
|
+
VALUES ($id, $title, $content)
|
|
188
|
+
ON CONFLICT(title) DO UPDATE SET
|
|
189
|
+
content = excluded.content,
|
|
190
|
+
updatedAt = CURRENT_TIMESTAMP
|
|
191
|
+
RETURNING *
|
|
192
|
+
`);
|
|
193
|
+
return stmt.get({
|
|
194
|
+
$id: crypto.randomUUID(),
|
|
195
|
+
$title: title,
|
|
196
|
+
$content: content
|
|
197
|
+
}) as NoteRow;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public readNote(title: string): NoteRow | null {
|
|
201
|
+
const stmt = this.db.query(`SELECT * FROM notes WHERE title = $title`);
|
|
202
|
+
return stmt.get({ $title: title }) as NoteRow | null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public listNotes(): NoteRow[] {
|
|
206
|
+
const stmt = this.db.query(`SELECT * FROM notes ORDER BY updatedAt DESC`);
|
|
207
|
+
return stmt.all() as NoteRow[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public searchNotes(queryText: string): NoteRow[] {
|
|
211
|
+
const stmt = this.db.query(`
|
|
212
|
+
SELECT * FROM notes
|
|
213
|
+
WHERE title LIKE $query OR content LIKE $query
|
|
214
|
+
ORDER BY updatedAt DESC
|
|
215
|
+
`);
|
|
216
|
+
return stmt.all({ $query: `%${queryText}%` }) as NoteRow[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public deleteNote(title: string): boolean {
|
|
220
|
+
const stmt = this.db.query(`DELETE FROM notes WHERE title = $title`);
|
|
221
|
+
const result = stmt.run({ $title: title });
|
|
222
|
+
return result.changes > 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public updateMCPServer(id: string, updates: any): void {
|
|
226
|
+
const fields = [];
|
|
227
|
+
const values: any = { $id: id };
|
|
228
|
+
|
|
229
|
+
if (updates.enabled !== undefined) {
|
|
230
|
+
fields.push("enabled = $enabled");
|
|
231
|
+
values.$enabled = updates.enabled ? 1 : 0;
|
|
232
|
+
}
|
|
233
|
+
if (updates.active !== undefined) {
|
|
234
|
+
fields.push("active = $active");
|
|
235
|
+
values.$active = updates.active ? 1 : 0;
|
|
236
|
+
}
|
|
237
|
+
if (updates.status !== undefined) {
|
|
238
|
+
fields.push("status = $status");
|
|
239
|
+
values.$status = updates.status;
|
|
240
|
+
}
|
|
241
|
+
if (updates.tools_count !== undefined) {
|
|
242
|
+
fields.push("tools_count = $tools_count");
|
|
243
|
+
values.$tools_count = updates.tools_count;
|
|
244
|
+
}
|
|
245
|
+
if (updates.transport !== undefined) {
|
|
246
|
+
fields.push("transport = $transport");
|
|
247
|
+
values.$transport = updates.transport;
|
|
248
|
+
}
|
|
249
|
+
if (updates.command !== undefined) {
|
|
250
|
+
fields.push("command = $command");
|
|
251
|
+
values.$command = updates.command;
|
|
252
|
+
}
|
|
253
|
+
if (updates.args !== undefined) {
|
|
254
|
+
fields.push("args = $args");
|
|
255
|
+
values.$args = JSON.stringify(updates.args);
|
|
256
|
+
}
|
|
257
|
+
if (updates.url !== undefined) {
|
|
258
|
+
fields.push("url = $url");
|
|
259
|
+
values.$url = updates.url;
|
|
260
|
+
}
|
|
261
|
+
if (updates.env_encrypted !== undefined) {
|
|
262
|
+
fields.push("env_encrypted = $env_encrypted");
|
|
263
|
+
values.$env_encrypted = updates.env_encrypted;
|
|
264
|
+
}
|
|
265
|
+
if (updates.env_iv !== undefined) {
|
|
266
|
+
fields.push("env_iv = $env_iv");
|
|
267
|
+
values.$env_iv = updates.env_iv;
|
|
268
|
+
}
|
|
269
|
+
if (updates.headers_encrypted !== undefined) {
|
|
270
|
+
fields.push("headers_encrypted = $headers_encrypted");
|
|
271
|
+
values.$headers_encrypted = updates.headers_encrypted;
|
|
272
|
+
}
|
|
273
|
+
if (updates.headers_iv !== undefined) {
|
|
274
|
+
fields.push("headers_iv = $headers_iv");
|
|
275
|
+
values.$headers_iv = updates.headers_iv;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (fields.length === 0) return;
|
|
279
|
+
|
|
280
|
+
const query = `UPDATE mcp_servers SET ${fields.join(", ")} WHERE id = $id`;
|
|
281
|
+
try {
|
|
282
|
+
this.db.query(query).run(values);
|
|
283
|
+
this.log.debug(`MCP server ${id} updated in DB`);
|
|
284
|
+
} catch (error: any) {
|
|
285
|
+
this.log.error(`Failed to update MCP server ${id}: ${error.message}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public listMCPServers(): any[] {
|
|
290
|
+
try {
|
|
291
|
+
return this.db.query("SELECT * FROM mcp_servers").all();
|
|
292
|
+
} catch (error: any) {
|
|
293
|
+
this.log.error(`Failed to list MCP servers: ${error.message}`);
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public createTask(task: {
|
|
299
|
+
project_id: string;
|
|
300
|
+
agent_id?: string | null;
|
|
301
|
+
parent_task_id?: number | null;
|
|
302
|
+
name: string;
|
|
303
|
+
description?: string | null;
|
|
304
|
+
}): number {
|
|
305
|
+
const result = this.db.query(`
|
|
306
|
+
INSERT INTO tasks (project_id, agent_id, parent_task_id, name, description)
|
|
307
|
+
VALUES (?, ?, ?, ?, ?)
|
|
308
|
+
`).run(
|
|
309
|
+
task.project_id,
|
|
310
|
+
task.agent_id ?? null,
|
|
311
|
+
task.parent_task_id ?? null,
|
|
312
|
+
task.name,
|
|
313
|
+
task.description ?? null
|
|
314
|
+
);
|
|
315
|
+
return Number(result.lastInsertRowid);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
public updateTask(taskId: number, updates: {
|
|
319
|
+
status?: string;
|
|
320
|
+
progress?: number;
|
|
321
|
+
result?: string;
|
|
322
|
+
agent_id?: string | null;
|
|
323
|
+
}): boolean {
|
|
324
|
+
const fields: string[] = ["updated_at = unixepoch()"];
|
|
325
|
+
const values: any[] = [];
|
|
326
|
+
|
|
327
|
+
if (updates.status !== undefined) { fields.push("status = ?"); values.push(updates.status); }
|
|
328
|
+
if (updates.progress !== undefined) { fields.push("progress = ?"); values.push(updates.progress); }
|
|
329
|
+
if (updates.result !== undefined) { fields.push("result = ?"); values.push(updates.result); }
|
|
330
|
+
if (updates.agent_id !== undefined) { fields.push("agent_id = ?"); values.push(updates.agent_id); }
|
|
331
|
+
|
|
332
|
+
values.push(taskId);
|
|
333
|
+
const res = this.db.query(`UPDATE tasks SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
334
|
+
return res.changes > 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
public getTasksByProject(projectId: string): any[] {
|
|
338
|
+
return this.db.query(
|
|
339
|
+
"SELECT * FROM tasks WHERE project_id = ? ORDER BY id ASC"
|
|
340
|
+
).all(projectId) as any[];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public getProjectWithTasks(projectId: string): any | null {
|
|
344
|
+
const project = this.db.query("SELECT * FROM projects WHERE id = ?").get(projectId) as any;
|
|
345
|
+
if (!project) return null;
|
|
346
|
+
project.tasks = this.getTasksByProject(projectId);
|
|
347
|
+
return project;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
public recalcProjectProgress(projectId: string): number {
|
|
351
|
+
const row = this.db.query(
|
|
352
|
+
"SELECT AVG(progress) as avg_progress FROM tasks WHERE project_id = ?"
|
|
353
|
+
).get(projectId) as any;
|
|
354
|
+
const avg = Math.round(row?.avg_progress ?? 0);
|
|
355
|
+
this.db.query("UPDATE projects SET progress = ?, updated_at = unixepoch() WHERE id = ?").run(avg, projectId);
|
|
356
|
+
return avg;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public saveMCPServer(server: any): void {
|
|
360
|
+
try {
|
|
361
|
+
this.db.query(`
|
|
362
|
+
INSERT OR REPLACE INTO mcp_servers (id, name, transport, command, args, url, env_encrypted, env_iv, headers_encrypted, headers_iv, enabled, active, builtin, tools_count, status)
|
|
363
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
364
|
+
`).run(
|
|
365
|
+
server.id || server.name,
|
|
366
|
+
server.name,
|
|
367
|
+
server.transport,
|
|
368
|
+
server.command || null,
|
|
369
|
+
JSON.stringify(server.args || []),
|
|
370
|
+
server.url || null,
|
|
371
|
+
server.env_encrypted || null,
|
|
372
|
+
server.env_iv || null,
|
|
373
|
+
server.headers_encrypted || null,
|
|
374
|
+
server.headers_iv || null,
|
|
375
|
+
server.enabled ? 1 : 0,
|
|
376
|
+
server.active ? 1 : 0,
|
|
377
|
+
server.builtin ? 1 : 0,
|
|
378
|
+
server.tools_count || 0,
|
|
379
|
+
server.status || "disconnected"
|
|
380
|
+
);
|
|
381
|
+
} catch (error: any) {
|
|
382
|
+
this.log.error(`Failed to save MCP server ${server.name}: ${error.message}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export const dbService = new DatabaseService();
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { getDb } from "./sqlite";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { logger } from "../utils/logger";
|
|
4
|
+
|
|
5
|
+
const log = logger.child("usage");
|
|
6
|
+
|
|
7
|
+
const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }> = {
|
|
8
|
+
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15 },
|
|
9
|
+
"claude-opus-4-6": { inputPer1M: 15, outputPer1M: 75 },
|
|
10
|
+
"claude-haiku-4-6": { inputPer1M: 0.8, outputPer1M: 4 },
|
|
11
|
+
"gpt-5.2": { inputPer1M: 2.5, outputPer1M: 10 },
|
|
12
|
+
"gpt-5.1": { inputPer1M: 5, outputPer1M: 15 },
|
|
13
|
+
"gpt-5.2-codex": { inputPer1M: 3, outputPer1M: 15 },
|
|
14
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4 },
|
|
15
|
+
"gemini-3-flash-preview": { inputPer1M: 0.075, outputPer1M: 0.3 },
|
|
16
|
+
"gemini-2.5-flash": { inputPer1M: 0.1, outputPer1M: 0.4 },
|
|
17
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 5 },
|
|
18
|
+
"gemini-3.1-pro-preview": { inputPer1M: 1.25, outputPer1M: 5 },
|
|
19
|
+
"deepseek-chat": { inputPer1M: 0.14, outputPer1M: 0.28 },
|
|
20
|
+
"deepseek-reasoner": { inputPer1M: 0.14, outputPer1M: 0.28 },
|
|
21
|
+
"deepseek-coder": { inputPer1M: 0.14, outputPer1M: 0.28 },
|
|
22
|
+
"kimi-k2.5": { inputPer1M: 0.5, outputPer1M: 2 },
|
|
23
|
+
"kimi-k2-thinking": { inputPer1M: 0.5, outputPer1M: 2 },
|
|
24
|
+
"kimi-k2-turbo-preview": { inputPer1M: 0.3, outputPer1M: 1.5 },
|
|
25
|
+
"meta-llama/llama-3.3-70b-instruct": { inputPer1M: 0.88, outputPer1M: 0.88 },
|
|
26
|
+
"google/gemini-2.0-flash-exp:free": { inputPer1M: 0, outputPer1M: 0 },
|
|
27
|
+
"deepseek/deepseek-r1:free": { inputPer1M: 0, outputPer1M: 0 },
|
|
28
|
+
"anthropic/claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15 },
|
|
29
|
+
"llama3.3:8b": { inputPer1M: 0, outputPer1M: 0 },
|
|
30
|
+
"qwen2.5:7b": { inputPer1M: 0, outputPer1M: 0 },
|
|
31
|
+
"mistral:7b": { inputPer1M: 0, outputPer1M: 0 },
|
|
32
|
+
"phi4:14b": { inputPer1M: 0, outputPer1M: 0 },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function calculateCost(model: string, inputTokens: number, outputTokens: number): number {
|
|
36
|
+
const pricing = MODEL_PRICING[model] || { inputPer1M: 0, outputPer1M: 0 };
|
|
37
|
+
const inputCost = (inputTokens / 1_000_000) * pricing.inputPer1M;
|
|
38
|
+
const outputCost = (outputTokens / 1_000_000) * pricing.outputPer1M;
|
|
39
|
+
return inputCost + outputCost;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UsageRecord {
|
|
43
|
+
id: string;
|
|
44
|
+
provider: string;
|
|
45
|
+
model: string;
|
|
46
|
+
input_tokens: number;
|
|
47
|
+
output_tokens: number;
|
|
48
|
+
cost_usd: number;
|
|
49
|
+
latency_ms: number | null;
|
|
50
|
+
toon_saved_tokens: number;
|
|
51
|
+
toon_saved_cost: number;
|
|
52
|
+
created_at: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UsageSummary {
|
|
56
|
+
totalTokens: number;
|
|
57
|
+
totalInputTokens: number;
|
|
58
|
+
totalOutputTokens: number;
|
|
59
|
+
totalCostUsd: number;
|
|
60
|
+
toonSavedTokens: number;
|
|
61
|
+
toonSavedCost: number;
|
|
62
|
+
toonSavingsPercent: number;
|
|
63
|
+
byProvider: Record<string, { tokens: number; costUsd: number; inputTokens: number; outputTokens: number }>;
|
|
64
|
+
byModel: Record<string, { tokens: number; costUsd: number; provider: string; inputTokens: number; outputTokens: number }>;
|
|
65
|
+
recentRecords: UsageRecord[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function recordUsage(options: {
|
|
69
|
+
provider: string;
|
|
70
|
+
model: string;
|
|
71
|
+
inputTokens: number;
|
|
72
|
+
outputTokens: number;
|
|
73
|
+
latencyMs?: number;
|
|
74
|
+
toonSavedTokens?: number;
|
|
75
|
+
toonSavedCost?: number;
|
|
76
|
+
}): void {
|
|
77
|
+
try {
|
|
78
|
+
const db = getDb();
|
|
79
|
+
const costUsd = calculateCost(options.model, options.inputTokens, options.outputTokens);
|
|
80
|
+
|
|
81
|
+
db.prepare(`
|
|
82
|
+
INSERT INTO usage_records (id, provider, model, input_tokens, output_tokens, cost_usd, latency_ms, toon_saved_tokens, toon_saved_cost, created_at)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
84
|
+
`).run(
|
|
85
|
+
randomUUID(),
|
|
86
|
+
options.provider,
|
|
87
|
+
options.model,
|
|
88
|
+
options.inputTokens,
|
|
89
|
+
options.outputTokens,
|
|
90
|
+
costUsd,
|
|
91
|
+
options.latencyMs || null,
|
|
92
|
+
options.toonSavedTokens || 0,
|
|
93
|
+
options.toonSavedCost || 0,
|
|
94
|
+
Math.floor(Date.now() / 1000)
|
|
95
|
+
);
|
|
96
|
+
log.info(`[USAGE RECORDED] provider=${options.provider} model=${options.model} input=${options.inputTokens} output=${options.outputTokens} cost=$${costUsd.toFixed(4)} toonSaved=${options.toonSavedTokens || 0}`);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("Failed to record usage:", error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getUsageStats(hours: number = 24): UsageSummary {
|
|
103
|
+
log.info(`[USAGE STATS] Fetching stats for last ${hours} hours`);
|
|
104
|
+
const db = getDb();
|
|
105
|
+
const since = Math.floor(Date.now() / 1000) - (hours * 3600);
|
|
106
|
+
|
|
107
|
+
const totals = db.prepare(`
|
|
108
|
+
SELECT
|
|
109
|
+
COALESCE(SUM(input_tokens), 0) as total_input,
|
|
110
|
+
COALESCE(SUM(output_tokens), 0) as total_output,
|
|
111
|
+
COALESCE(SUM(cost_usd), 0) as total_cost,
|
|
112
|
+
COALESCE(SUM(toon_saved_tokens), 0) as toon_saved_tokens,
|
|
113
|
+
COALESCE(SUM(toon_saved_cost), 0) as toon_saved_cost
|
|
114
|
+
FROM usage_records
|
|
115
|
+
WHERE created_at >= ?
|
|
116
|
+
`).get(since) as { total_input: number; total_output: number; total_cost: number; toon_saved_tokens: number; toon_saved_cost: number };
|
|
117
|
+
|
|
118
|
+
const byProvider = db.prepare(`
|
|
119
|
+
SELECT
|
|
120
|
+
provider,
|
|
121
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
122
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
123
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
124
|
+
FROM usage_records
|
|
125
|
+
WHERE created_at >= ?
|
|
126
|
+
GROUP BY provider
|
|
127
|
+
`).all(since) as Array<{ provider: string; input_tokens: number; output_tokens: number; cost_usd: number }>;
|
|
128
|
+
|
|
129
|
+
const byModel = db.prepare(`
|
|
130
|
+
SELECT
|
|
131
|
+
model,
|
|
132
|
+
provider,
|
|
133
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
134
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
135
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
136
|
+
FROM usage_records
|
|
137
|
+
WHERE created_at >= ?
|
|
138
|
+
GROUP BY model
|
|
139
|
+
ORDER BY cost_usd DESC
|
|
140
|
+
`).all(since) as Array<{ model: string; provider: string; input_tokens: number; output_tokens: number; cost_usd: number }>;
|
|
141
|
+
|
|
142
|
+
const recentRecords = db.prepare(`
|
|
143
|
+
SELECT * FROM usage_records
|
|
144
|
+
WHERE created_at >= ?
|
|
145
|
+
ORDER BY created_at DESC
|
|
146
|
+
LIMIT 20
|
|
147
|
+
`).all(since) as UsageRecord[];
|
|
148
|
+
|
|
149
|
+
const providerMap: UsageSummary["byProvider"] = {};
|
|
150
|
+
for (const p of byProvider) {
|
|
151
|
+
providerMap[p.provider] = {
|
|
152
|
+
inputTokens: p.input_tokens,
|
|
153
|
+
outputTokens: p.output_tokens,
|
|
154
|
+
tokens: p.input_tokens + p.output_tokens,
|
|
155
|
+
costUsd: p.cost_usd
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const modelMap: UsageSummary["byModel"] = {};
|
|
160
|
+
for (const m of byModel) {
|
|
161
|
+
modelMap[m.model] = {
|
|
162
|
+
provider: m.provider,
|
|
163
|
+
inputTokens: m.input_tokens,
|
|
164
|
+
outputTokens: m.output_tokens,
|
|
165
|
+
tokens: m.input_tokens + m.output_tokens,
|
|
166
|
+
costUsd: m.cost_usd
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const totalTokens = totals.total_input + totals.total_output;
|
|
171
|
+
const toonSavingsPercent = totalTokens > 0
|
|
172
|
+
? (totals.toon_saved_tokens / totalTokens) * 100
|
|
173
|
+
: 0;
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
totalTokens,
|
|
177
|
+
totalInputTokens: totals.total_input,
|
|
178
|
+
totalOutputTokens: totals.total_output,
|
|
179
|
+
totalCostUsd: totals.total_cost,
|
|
180
|
+
toonSavedTokens: totals.toon_saved_tokens,
|
|
181
|
+
toonSavedCost: totals.toon_saved_cost,
|
|
182
|
+
toonSavingsPercent,
|
|
183
|
+
byProvider: providerMap,
|
|
184
|
+
byModel: modelMap,
|
|
185
|
+
recentRecords
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getProviderPricing(provider: string, model: string): { inputPer1M: number; outputPer1M: number } {
|
|
190
|
+
return MODEL_PRICING[model] || { inputPer1M: 0, outputPer1M: 0 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function estimateCostForTokens(model: string, tokens: number): number {
|
|
194
|
+
const pricing = MODEL_PRICING[model] || { inputPer1M: 0, outputPer1M: 0 };
|
|
195
|
+
return (tokens / 1_000_000) * pricing.inputPer1M;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Pending TOON savings — set before LLM invoke, consumed in handleLLMEnd callback
|
|
199
|
+
let pendingToonTokens = 0;
|
|
200
|
+
let pendingToonCost = 0;
|
|
201
|
+
|
|
202
|
+
export function setPendingToonSavings(tokens: number, cost: number): void {
|
|
203
|
+
pendingToonTokens = tokens;
|
|
204
|
+
pendingToonCost = cost;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function consumePendingToonSavings(): { tokens: number; cost: number } {
|
|
208
|
+
const result = { tokens: pendingToonTokens, cost: pendingToonCost };
|
|
209
|
+
pendingToonTokens = 0;
|
|
210
|
+
pendingToonCost = 0;
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge events pub/sub — allows tools (terminal, codebridge) to emit events
|
|
3
|
+
* that get forwarded to UI clients connected to /bridge-events WebSocket.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface BridgeEvent {
|
|
7
|
+
type: BridgeEventType;
|
|
8
|
+
data: BridgeEventData;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type BridgeEventType =
|
|
13
|
+
| "bridge:cmd_start"
|
|
14
|
+
| "bridge:cmd_output"
|
|
15
|
+
| "bridge:cmd_done"
|
|
16
|
+
| "bridge:cmd_error"
|
|
17
|
+
| "agent:started"
|
|
18
|
+
| "agent:output"
|
|
19
|
+
| "agent:finished"
|
|
20
|
+
| "agent:error"
|
|
21
|
+
| "agent:cancelled";
|
|
22
|
+
|
|
23
|
+
export interface BridgeCmdStartData {
|
|
24
|
+
processId: string;
|
|
25
|
+
command: string;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BridgeCmdOutputData {
|
|
31
|
+
processId: string;
|
|
32
|
+
chunk: string;
|
|
33
|
+
stream: "stdout" | "stderr";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BridgeCmdDoneData {
|
|
37
|
+
processId: string;
|
|
38
|
+
exitCode: number;
|
|
39
|
+
success: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BridgeCmdErrorData {
|
|
43
|
+
processId: string;
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type BridgeEventData =
|
|
48
|
+
| BridgeCmdStartData
|
|
49
|
+
| BridgeCmdOutputData
|
|
50
|
+
| BridgeCmdDoneData
|
|
51
|
+
| BridgeCmdErrorData
|
|
52
|
+
| Record<string, unknown>;
|
|
53
|
+
|
|
54
|
+
const subscribers = new Set<{ send: (data: string) => void }>();
|
|
55
|
+
|
|
56
|
+
export function subscribeBridge(ws: { send: (data: string) => void }) {
|
|
57
|
+
subscribers.add(ws);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function unsubscribeBridge(ws: { send: (data: string) => void }) {
|
|
61
|
+
subscribers.delete(ws);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function emitBridge(type: BridgeEventType, data: BridgeEventData) {
|
|
65
|
+
const event: BridgeEvent = { type, data, timestamp: Date.now() };
|
|
66
|
+
const payload = JSON.stringify(event);
|
|
67
|
+
for (const ws of subscribers) {
|
|
68
|
+
try {
|
|
69
|
+
ws.send(payload);
|
|
70
|
+
} catch {
|
|
71
|
+
subscribers.delete(ws);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|