@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.
Files changed (156) hide show
  1. package/CONTRIBUTING.md +44 -0
  2. package/README.md +310 -0
  3. package/package.json +96 -0
  4. package/packages/cli/package.json +28 -0
  5. package/packages/cli/src/commands/agent-run.ts +168 -0
  6. package/packages/cli/src/commands/agents.ts +398 -0
  7. package/packages/cli/src/commands/chat.ts +142 -0
  8. package/packages/cli/src/commands/config.ts +50 -0
  9. package/packages/cli/src/commands/cron.ts +161 -0
  10. package/packages/cli/src/commands/dev.ts +95 -0
  11. package/packages/cli/src/commands/doctor.ts +133 -0
  12. package/packages/cli/src/commands/gateway.ts +443 -0
  13. package/packages/cli/src/commands/logs.ts +57 -0
  14. package/packages/cli/src/commands/mcp.ts +175 -0
  15. package/packages/cli/src/commands/message.ts +77 -0
  16. package/packages/cli/src/commands/onboard.ts +1868 -0
  17. package/packages/cli/src/commands/security.ts +144 -0
  18. package/packages/cli/src/commands/service.ts +50 -0
  19. package/packages/cli/src/commands/sessions.ts +116 -0
  20. package/packages/cli/src/commands/skills.ts +187 -0
  21. package/packages/cli/src/commands/update.ts +25 -0
  22. package/packages/cli/src/index.ts +185 -0
  23. package/packages/cli/src/utils/token.ts +6 -0
  24. package/packages/code-bridge/README.md +78 -0
  25. package/packages/code-bridge/package.json +18 -0
  26. package/packages/code-bridge/src/index.ts +95 -0
  27. package/packages/code-bridge/src/process-manager.ts +212 -0
  28. package/packages/code-bridge/src/schemas.ts +133 -0
  29. package/packages/core/package.json +46 -0
  30. package/packages/core/src/agent/agent-loop.ts +369 -0
  31. package/packages/core/src/agent/compaction.ts +140 -0
  32. package/packages/core/src/agent/context-compiler.ts +378 -0
  33. package/packages/core/src/agent/context-guard.ts +91 -0
  34. package/packages/core/src/agent/context.ts +138 -0
  35. package/packages/core/src/agent/conversation-store.ts +198 -0
  36. package/packages/core/src/agent/curator.ts +158 -0
  37. package/packages/core/src/agent/hooks.ts +166 -0
  38. package/packages/core/src/agent/index.ts +116 -0
  39. package/packages/core/src/agent/llm-client.ts +503 -0
  40. package/packages/core/src/agent/native-tools.ts +505 -0
  41. package/packages/core/src/agent/prompt-builder.ts +532 -0
  42. package/packages/core/src/agent/providers/index.ts +167 -0
  43. package/packages/core/src/agent/providers.ts +1 -0
  44. package/packages/core/src/agent/reflector.ts +170 -0
  45. package/packages/core/src/agent/service.ts +64 -0
  46. package/packages/core/src/agent/stuck-loop.ts +133 -0
  47. package/packages/core/src/agent/supervisor.ts +39 -0
  48. package/packages/core/src/agent/tracer.ts +102 -0
  49. package/packages/core/src/agent/workspace.ts +110 -0
  50. package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
  51. package/packages/core/src/canvas/canvas-manager.ts +319 -0
  52. package/packages/core/src/canvas/canvas-tools.ts +420 -0
  53. package/packages/core/src/canvas/emitter.ts +115 -0
  54. package/packages/core/src/canvas/index.ts +2 -0
  55. package/packages/core/src/channels/base.ts +138 -0
  56. package/packages/core/src/channels/discord.ts +260 -0
  57. package/packages/core/src/channels/index.ts +7 -0
  58. package/packages/core/src/channels/manager.ts +383 -0
  59. package/packages/core/src/channels/slack.ts +287 -0
  60. package/packages/core/src/channels/telegram.ts +502 -0
  61. package/packages/core/src/channels/webchat.ts +128 -0
  62. package/packages/core/src/channels/whatsapp.ts +375 -0
  63. package/packages/core/src/config/index.ts +12 -0
  64. package/packages/core/src/config/loader.ts +529 -0
  65. package/packages/core/src/events/event-bus.ts +169 -0
  66. package/packages/core/src/gateway/index.ts +5 -0
  67. package/packages/core/src/gateway/initializer.ts +290 -0
  68. package/packages/core/src/gateway/lane-queue.ts +169 -0
  69. package/packages/core/src/gateway/resolver.ts +108 -0
  70. package/packages/core/src/gateway/router.ts +124 -0
  71. package/packages/core/src/gateway/server.ts +3317 -0
  72. package/packages/core/src/gateway/session.ts +95 -0
  73. package/packages/core/src/gateway/slash-commands.ts +192 -0
  74. package/packages/core/src/heartbeat/index.ts +157 -0
  75. package/packages/core/src/index.ts +19 -0
  76. package/packages/core/src/integrations/catalog.ts +286 -0
  77. package/packages/core/src/integrations/env.ts +64 -0
  78. package/packages/core/src/integrations/index.ts +2 -0
  79. package/packages/core/src/memory/index.ts +1 -0
  80. package/packages/core/src/memory/notes.ts +68 -0
  81. package/packages/core/src/plugins/api.ts +128 -0
  82. package/packages/core/src/plugins/index.ts +2 -0
  83. package/packages/core/src/plugins/loader.ts +365 -0
  84. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  85. package/packages/core/src/security/google-chat.ts +269 -0
  86. package/packages/core/src/security/index.ts +192 -0
  87. package/packages/core/src/security/pairing.ts +250 -0
  88. package/packages/core/src/security/rate-limit.ts +270 -0
  89. package/packages/core/src/security/signal.ts +321 -0
  90. package/packages/core/src/state/store.ts +312 -0
  91. package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
  92. package/packages/core/src/storage/crypto.ts +101 -0
  93. package/packages/core/src/storage/db-context.ts +333 -0
  94. package/packages/core/src/storage/onboarding.ts +1087 -0
  95. package/packages/core/src/storage/schema.ts +541 -0
  96. package/packages/core/src/storage/seed.ts +571 -0
  97. package/packages/core/src/storage/sqlite.ts +387 -0
  98. package/packages/core/src/storage/usage.ts +212 -0
  99. package/packages/core/src/tools/bridge-events.ts +74 -0
  100. package/packages/core/src/tools/browser.ts +275 -0
  101. package/packages/core/src/tools/codebridge.ts +421 -0
  102. package/packages/core/src/tools/coordinator-tools.ts +179 -0
  103. package/packages/core/src/tools/cron.ts +611 -0
  104. package/packages/core/src/tools/exec.ts +140 -0
  105. package/packages/core/src/tools/fs.ts +364 -0
  106. package/packages/core/src/tools/index.ts +12 -0
  107. package/packages/core/src/tools/memory.ts +176 -0
  108. package/packages/core/src/tools/notify.ts +113 -0
  109. package/packages/core/src/tools/project-management.ts +376 -0
  110. package/packages/core/src/tools/project.ts +375 -0
  111. package/packages/core/src/tools/read.ts +158 -0
  112. package/packages/core/src/tools/web.ts +436 -0
  113. package/packages/core/src/tools/workspace.ts +171 -0
  114. package/packages/core/src/utils/benchmark.ts +80 -0
  115. package/packages/core/src/utils/crypto.ts +73 -0
  116. package/packages/core/src/utils/date.ts +42 -0
  117. package/packages/core/src/utils/index.ts +4 -0
  118. package/packages/core/src/utils/logger.ts +388 -0
  119. package/packages/core/src/utils/retry.ts +70 -0
  120. package/packages/core/src/voice/index.ts +583 -0
  121. package/packages/core/tsconfig.json +9 -0
  122. package/packages/mcp/package.json +26 -0
  123. package/packages/mcp/src/config.ts +13 -0
  124. package/packages/mcp/src/index.ts +1 -0
  125. package/packages/mcp/src/logger.ts +42 -0
  126. package/packages/mcp/src/manager.ts +434 -0
  127. package/packages/mcp/src/transports/index.ts +67 -0
  128. package/packages/mcp/src/transports/sse.ts +241 -0
  129. package/packages/mcp/src/transports/websocket.ts +159 -0
  130. package/packages/skills/package.json +21 -0
  131. package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
  132. package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
  133. package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
  134. package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
  135. package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
  136. package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
  137. package/packages/skills/src/bundled/memory/SKILL.md +42 -0
  138. package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
  139. package/packages/skills/src/bundled/shell/SKILL.md +43 -0
  140. package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
  141. package/packages/skills/src/bundled/voice/SKILL.md +25 -0
  142. package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
  143. package/packages/skills/src/index.ts +1 -0
  144. package/packages/skills/src/loader.ts +282 -0
  145. package/packages/tools/package.json +43 -0
  146. package/packages/tools/src/browser/browser.test.ts +111 -0
  147. package/packages/tools/src/browser/index.ts +272 -0
  148. package/packages/tools/src/canvas/index.ts +220 -0
  149. package/packages/tools/src/cron/cron.test.ts +164 -0
  150. package/packages/tools/src/cron/index.ts +304 -0
  151. package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
  152. package/packages/tools/src/filesystem/index.ts +379 -0
  153. package/packages/tools/src/git/index.ts +239 -0
  154. package/packages/tools/src/index.ts +4 -0
  155. package/packages/tools/src/shell/detect-env.ts +70 -0
  156. 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
+ }