@johpaz/hive-sdk 0.0.12 → 0.0.15

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 (199) hide show
  1. package/.github/CODEOWNERS +9 -0
  2. package/.github/workflows/publish.yml +89 -0
  3. package/.github/workflows/version-bump.yml +102 -0
  4. package/CHANGELOG.md +38 -0
  5. package/README.md +158 -0
  6. package/bun.lock +543 -0
  7. package/bunfig.toml +7 -0
  8. package/docs/API-AGENTS.md +316 -0
  9. package/docs/API-CONTEXT-COMPILER.md +252 -0
  10. package/docs/API-DAG-SCHEDULER.md +273 -0
  11. package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
  12. package/docs/API-WORKERS-EVENTS.md +152 -0
  13. package/docs/INDEX.md +141 -0
  14. package/docs/README.md +68 -0
  15. package/package.json +54 -105
  16. package/packages/cli/package.json +17 -0
  17. package/packages/cli/src/commands/init.ts +56 -0
  18. package/packages/cli/src/commands/run.ts +45 -0
  19. package/packages/cli/src/commands/test.ts +42 -0
  20. package/packages/cli/src/commands/trace.ts +55 -0
  21. package/packages/cli/src/index.ts +43 -0
  22. package/packages/core/package.json +58 -0
  23. package/packages/core/src/ace/Curator.ts +158 -0
  24. package/packages/core/src/ace/Reflector.ts +200 -0
  25. package/packages/core/src/ace/Tracer.ts +100 -0
  26. package/packages/core/src/ace/index.ts +4 -0
  27. package/packages/core/src/agent/AgentRunner.ts +699 -0
  28. package/packages/core/src/agent/Compaction.ts +221 -0
  29. package/packages/core/src/agent/ContextCompiler.ts +567 -0
  30. package/packages/core/src/agent/ContextGuard.ts +91 -0
  31. package/packages/core/src/agent/ConversationStore.ts +244 -0
  32. package/packages/core/src/agent/Hooks.ts +166 -0
  33. package/packages/core/src/agent/NativeTools.ts +31 -0
  34. package/packages/core/src/agent/PromptBuilder.ts +169 -0
  35. package/packages/core/src/agent/Service.ts +267 -0
  36. package/packages/core/src/agent/StuckLoop.ts +133 -0
  37. package/packages/core/src/agent/index.ts +12 -0
  38. package/packages/core/src/agent/providers/LLMClient.ts +149 -0
  39. package/packages/core/src/agent/providers/anthropic.ts +212 -0
  40. package/packages/core/src/agent/providers/gemini.ts +215 -0
  41. package/packages/core/src/agent/providers/index.ts +199 -0
  42. package/packages/core/src/agent/providers/interface.ts +195 -0
  43. package/packages/core/src/agent/providers/ollama.ts +175 -0
  44. package/packages/core/src/agent/providers/openai-compat.ts +231 -0
  45. package/packages/core/src/agent/providers.ts +1 -0
  46. package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
  47. package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
  48. package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
  49. package/packages/core/src/agent/selectors/index.ts +6 -0
  50. package/packages/core/src/api/createAgent.test.ts +48 -0
  51. package/packages/core/src/api/createAgent.ts +122 -0
  52. package/packages/core/src/api/index.ts +2 -0
  53. package/packages/core/src/canvas/CanvasManager.ts +390 -0
  54. package/packages/core/src/canvas/a2ui-tools.ts +255 -0
  55. package/packages/core/src/canvas/canvas-tools.ts +448 -0
  56. package/packages/core/src/canvas/emitter.ts +149 -0
  57. package/packages/core/src/canvas/index.ts +6 -0
  58. package/packages/core/src/config/index.ts +2 -0
  59. package/packages/core/src/config/loader.ts +554 -0
  60. package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
  61. package/packages/core/src/ethics/EthicsGuard.ts +66 -0
  62. package/packages/core/src/ethics/index.ts +2 -0
  63. package/packages/core/src/gateway/channel-notify.test.ts +14 -0
  64. package/packages/core/src/gateway/channel-notify.ts +12 -0
  65. package/packages/core/src/gateway/index.ts +1 -0
  66. package/packages/core/src/index.ts +37 -0
  67. package/packages/core/src/mcp/MCPClient.ts +439 -0
  68. package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
  69. package/packages/core/src/mcp/config.ts +13 -0
  70. package/packages/core/src/mcp/hot-reload.ts +147 -0
  71. package/packages/core/src/mcp/index.ts +11 -0
  72. package/packages/core/src/mcp/logger.ts +42 -0
  73. package/packages/core/src/mcp/singleton.ts +21 -0
  74. package/packages/core/src/mcp/transports/index.ts +67 -0
  75. package/packages/core/src/mcp/transports/sse.ts +241 -0
  76. package/packages/core/src/mcp/transports/websocket.ts +159 -0
  77. package/packages/core/src/memory/Scratchpad.test.ts +47 -0
  78. package/packages/core/src/memory/Scratchpad.ts +37 -0
  79. package/packages/core/src/memory/Storage.ts +6 -0
  80. package/packages/core/src/memory/index.ts +2 -0
  81. package/packages/core/src/multimodal/VisionService.ts +293 -0
  82. package/packages/core/src/multimodal/index.ts +2 -0
  83. package/packages/core/src/multimodal/types.ts +28 -0
  84. package/packages/core/src/security/Pairing.ts +250 -0
  85. package/packages/core/src/security/RateLimit.ts +270 -0
  86. package/packages/core/src/security/index.ts +4 -0
  87. package/packages/core/src/skills/SkillLoader.ts +388 -0
  88. package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
  89. package/packages/core/src/skills/defineSkill.ts +18 -0
  90. package/packages/core/src/skills/index.ts +4 -0
  91. package/packages/core/src/state/index.ts +2 -0
  92. package/packages/core/src/state/store.ts +312 -0
  93. package/packages/core/src/storage/SQLiteStorage.ts +407 -0
  94. package/packages/core/src/storage/crypto.ts +101 -0
  95. package/packages/core/src/storage/index.ts +10 -0
  96. package/packages/core/src/storage/onboarding.ts +1603 -0
  97. package/packages/core/src/storage/schema.ts +689 -0
  98. package/packages/core/src/storage/seed.ts +740 -0
  99. package/packages/core/src/storage/usage.ts +374 -0
  100. package/packages/core/src/swarm/AgentBus.ts +460 -0
  101. package/packages/core/src/swarm/AgentExecutor.ts +53 -0
  102. package/packages/core/src/swarm/Coordinator.ts +251 -0
  103. package/packages/core/src/swarm/EventBridge.ts +122 -0
  104. package/packages/core/src/swarm/EventBus.ts +169 -0
  105. package/packages/core/src/swarm/TaskGraph.ts +192 -0
  106. package/packages/core/src/swarm/TaskNode.ts +97 -0
  107. package/packages/core/src/swarm/TaskResult.ts +22 -0
  108. package/packages/core/src/swarm/WorkerPool.ts +236 -0
  109. package/packages/core/src/swarm/errors.ts +37 -0
  110. package/packages/core/src/swarm/index.ts +30 -0
  111. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
  112. package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
  113. package/packages/core/src/swarm/presets/index.ts +4 -0
  114. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
  115. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
  116. package/packages/core/src/swarm/strategies/index.ts +3 -0
  117. package/packages/core/src/swarm/types.ts +164 -0
  118. package/packages/core/src/tools/ToolExecutor.ts +58 -0
  119. package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
  120. package/packages/core/src/tools/ToolRegistry.ts +61 -0
  121. package/packages/core/src/tools/agents/get-available-models.ts +118 -0
  122. package/packages/core/src/tools/agents/index.ts +715 -0
  123. package/packages/core/src/tools/bridge-events.ts +26 -0
  124. package/packages/core/src/tools/canvas/index.ts +375 -0
  125. package/packages/core/src/tools/cli/index.ts +142 -0
  126. package/packages/core/src/tools/codebridge/index.ts +342 -0
  127. package/packages/core/src/tools/core/index.ts +476 -0
  128. package/packages/core/src/tools/cron/index.ts +626 -0
  129. package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
  130. package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
  131. package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
  132. package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
  133. package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
  134. package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
  135. package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
  136. package/packages/core/src/tools/filesystem/index.ts +34 -0
  137. package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
  138. package/packages/core/src/tools/index.ts +231 -0
  139. package/packages/core/src/tools/meeting/index.ts +363 -0
  140. package/packages/core/src/tools/office/index.ts +47 -0
  141. package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
  142. package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
  143. package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
  144. package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
  145. package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
  146. package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
  147. package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
  148. package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
  149. package/packages/core/src/tools/projects/index.ts +37 -0
  150. package/packages/core/src/tools/projects/project-create.ts +94 -0
  151. package/packages/core/src/tools/projects/project-done.ts +66 -0
  152. package/packages/core/src/tools/projects/project-fail.ts +66 -0
  153. package/packages/core/src/tools/projects/project-list.ts +96 -0
  154. package/packages/core/src/tools/projects/project-update.ts +72 -0
  155. package/packages/core/src/tools/projects/task-create.ts +68 -0
  156. package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
  157. package/packages/core/src/tools/projects/task-update.ts +93 -0
  158. package/packages/core/src/tools/types.ts +39 -0
  159. package/packages/core/src/tools/voice/index.ts +104 -0
  160. package/packages/core/src/tools/web/browser-click.ts +78 -0
  161. package/packages/core/src/tools/web/browser-extract.ts +139 -0
  162. package/packages/core/src/tools/web/browser-navigate.ts +106 -0
  163. package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
  164. package/packages/core/src/tools/web/browser-script.ts +88 -0
  165. package/packages/core/src/tools/web/browser-service.ts +554 -0
  166. package/packages/core/src/tools/web/browser-type.ts +101 -0
  167. package/packages/core/src/tools/web/browser-wait.ts +136 -0
  168. package/packages/core/src/tools/web/index.ts +41 -0
  169. package/packages/core/src/tools/web/web-fetch.ts +78 -0
  170. package/packages/core/src/tools/web/web-search.ts +123 -0
  171. package/packages/core/src/utils/benchmark.ts +80 -0
  172. package/packages/core/src/utils/crypto.ts +73 -0
  173. package/packages/core/src/utils/date.ts +42 -0
  174. package/packages/core/src/utils/index.ts +10 -0
  175. package/packages/core/src/utils/logger.ts +389 -0
  176. package/packages/core/src/utils/retry.ts +70 -0
  177. package/packages/core/src/utils/toon.ts +253 -0
  178. package/packages/core/src/voice/index.ts +656 -0
  179. package/test/setup-db.ts +216 -0
  180. package/tsconfig.json +39 -0
  181. package/src/agents.ts +0 -1
  182. package/src/canvas.ts +0 -1
  183. package/src/channels.ts +0 -1
  184. package/src/config.ts +0 -1
  185. package/src/events.ts +0 -1
  186. package/src/gateway.ts +0 -1
  187. package/src/index.ts +0 -304
  188. package/src/mcp.ts +0 -1
  189. package/src/multimodal.ts +0 -1
  190. package/src/scheduler.ts +0 -1
  191. package/src/security.ts +0 -1
  192. package/src/skills.ts +0 -1
  193. package/src/state.ts +0 -1
  194. package/src/storage.ts +0 -1
  195. package/src/tools.ts +0 -1
  196. package/src/tts.ts +0 -1
  197. package/src/types.ts +0 -82
  198. package/src/utils.ts +0 -1
  199. package/src/voice.ts +0 -1
@@ -0,0 +1,1603 @@
1
+ import { logger } from "../utils/logger.ts";
2
+ import { getDb, initializeDatabase } from "./SQLiteStorage.ts";
3
+ import { encryptApiKey, encryptConfig, decryptApiKey, decryptConfig } from "./crypto";
4
+ import { seedAllData, SEED_DATA } from "./seed";
5
+ import { SkillLoader } from "../skills/index.ts";
6
+
7
+ export interface OnboardingSection {
8
+ step: "user" | "skills" | "ethics" | "tools" | "provider" | "model" | "channel" | "codebridge" | "mcp" | "agent" | "complete";
9
+ userId: string;
10
+ data: Record<string, unknown>;
11
+ completedAt?: number;
12
+ }
13
+
14
+ const log = logger.child("onboarding");
15
+ // 9️⃣ Hive System Prompt
16
+
17
+ const HIVE_SYSTEM_PROMPT = `
18
+ # HIVE — Agente Coordinador
19
+
20
+ Sos Bee, coordinador de Hive. Resolvés tareas del usuario directamente o delegando a workers especializados. Tu rol es "coordinator".
21
+
22
+ ## ⚡ REGLAS CRÍTICAS
23
+
24
+ 1. **Ética primero** — Operás bajo un Código de Ética obligatorio. No podés ignorarlo.
25
+ 2. **Confirmá antes de guardar** — Siempre verificá con el usuario antes de persistir datos en la BD.
26
+ 3. **Buscá antes de crear** — Usá search_knowledge para capacidades, find_agent para workers.
27
+ 4. **Mínimo privilegio** — Asigná solo las tools necesarias a cada worker.
28
+ 5. **Nunca cli_exec para cron** — Usá siempre cron.create para tareas programadas.
29
+ 6. **Nunca codebridge_launch directo** — Creá un worker code_developer primero.
30
+
31
+ ## 🔍 DISCOVERY — CÓMO ENCONTRAR MÁS CAPACIDADES
32
+
33
+ Arrancás con solo 4 herramientas. Para descubrir más, usá **search_knowledge**:
34
+
35
+ - \`search_knowledge(type="tools", query="leer archivos")\` → herramientas nativas
36
+ - \`search_knowledge(type="mcp", query="listar bases datos")\` → herramientas MCP externas
37
+ - \`search_knowledge(type="skills", query="debuggear código")\` → skills (instrucciones de tareas)
38
+ - \`search_knowledge(type="playbook", query="seguridad")\` → playbook (buenas prácticas)
39
+ - \`search_knowledge(type="all", query="buscar web internet")\` → busca en todo
40
+
41
+ La búsqueda es bilingüe: buscá en español y si hay pocos resultados se re-intenta con equivalentes en inglés.
42
+
43
+ **Prioridad:** SIEMPRE preferí herramientas nativas sobre MCP cuando ambas resuelven la tarea.
44
+
45
+ ## 📋 FLUJO DE TRABAJO
46
+
47
+ **Tarea simple (1-2 pasos):** Ejecutala directo con tus tools.
48
+
49
+ **Tarea repetitiva:** Usá cron.create. Preguntá al usuario cada cuánto ejecutarla.
50
+
51
+ **Tarea compleja (múltiples workers):** Creá un proyecto con project_create, descomponé en tareas, delegá con delegate_task.
52
+
53
+ **Worker:** find_agent → ¿existe? → reutilizalo. Si no → create_agent con system_prompt claro y tools_json mínimo. **delegate_task** lo activa.
54
+
55
+ **Proyectos:** Solo creá proyecto cuando hay múltiples workers coordinando. NO para tareas unitarias.
56
+
57
+ **Cierre:** task_update(status, result) → task_evaluate(criteria) → project_done(summary).
58
+
59
+ ## 🧠 MEMORIA
60
+
61
+ - \`save_note\` — Persiste notas por conversación (sobrevive compresión)
62
+ - \`memory_write\` / \`memory_read\` — Memoria cross-conversación por clave
63
+ - Playbook — Reglas aprendidas inyectadas automáticamente
64
+
65
+ ## 📡 CANALES
66
+
67
+ webchat (siempre activo) · telegram · discord · slack · whatsapp
68
+ Canal preferido para cron: telegram > discord > webchat
69
+ `
70
+ export function initOnboardingDb(): void {
71
+ try {
72
+ initializeDatabase();
73
+
74
+ // Verificar si la DB ya tiene datos antes de hacer seed
75
+ const db = getDb();
76
+ const userCount = db.query("SELECT COUNT(*) as count FROM users").get() as { count: number };
77
+
78
+ if (userCount.count > 0) {
79
+ log.info("✅ DB ya inicializada con " + userCount.count + " usuario(s). Saltando seed.");
80
+ return;
81
+ }
82
+
83
+ log.info("🌱 Ejecutando seed de datos...");
84
+ seedAllData();
85
+ log.info("✅ Seed completado correctamente.");
86
+ } catch (e) {
87
+ log.error("⚠️ Fallo al inicializar/poblar la DB:", { error: (e as Error).message });
88
+ }
89
+ }
90
+
91
+ export function saveUserProfile(data: {
92
+ userId?: string;
93
+ userName?: string;
94
+ userLanguage?: string;
95
+ userTimezone?: string;
96
+ userOccupation?: string;
97
+ userNotes?: string;
98
+ agentName?: string;
99
+ agentId?: string;
100
+ agentDescription?: string;
101
+ agentTone?: string;
102
+ channelUserId?: string;
103
+ }): string {
104
+ try {
105
+ const db = getDb();
106
+ let finalUserId = data.userId;
107
+
108
+ if (!finalUserId) {
109
+ // 1️⃣ Dejar que SQLite genere el ID automáticamente con randomblob(16)
110
+ const result = db.query(`
111
+ INSERT INTO users(name, language, timezone, occupation, notes)
112
+ VALUES(?, ?, ?, ?, ?) RETURNING id
113
+ `).get(
114
+ data.userName || null,
115
+ data.userLanguage || null,
116
+ data.userTimezone || null,
117
+ data.userOccupation || null,
118
+ data.userNotes || null
119
+ ) as { id: string };
120
+ finalUserId = result.id;
121
+ log.info("✅ User created with auto-generated ID", { userId: finalUserId });
122
+ } else {
123
+ // 1️⃣ Upsert con ID explícito (flujo web o actualización)
124
+ db.query(`
125
+ INSERT INTO users(id, name, language, timezone, occupation, notes)
126
+ VALUES(?, ?, ?, ?, ?, ?)
127
+ ON CONFLICT(id) DO UPDATE SET
128
+ name = COALESCE(excluded.name, name),
129
+ language = COALESCE(excluded.language, language),
130
+ timezone = COALESCE(excluded.timezone, timezone),
131
+ occupation = COALESCE(excluded.occupation, occupation),
132
+ notes = COALESCE(excluded.notes, notes)
133
+ `).run(
134
+ finalUserId,
135
+ data.userName || null,
136
+ data.userLanguage || null,
137
+ data.userTimezone || null,
138
+ data.userOccupation || null,
139
+ data.userNotes || null
140
+ );
141
+ }
142
+
143
+ // 2️⃣ Crear identidad base para webchat (sesión única)
144
+ if (data.channelUserId) {
145
+ db.query(`
146
+ INSERT OR REPLACE INTO user_identities(user_id, channel, channel_user_id)
147
+ VALUES(?, 'webchat', ?)
148
+ `).run(finalUserId, data.channelUserId);
149
+ log.info("✅ User identity created for webchat", { userId: finalUserId });
150
+ }
151
+
152
+ // 3️⃣ Crear o actualizar agente
153
+ if (data.agentId && data.agentName) {
154
+
155
+ db.query(`
156
+ INSERT INTO agents
157
+ (id, user_id, name, description, tone, system_prompt, status, role)
158
+ VALUES(?, ?, ?, ?, ?, ?, 'idle', 'coordinator')
159
+ ON CONFLICT(id) DO UPDATE SET
160
+ user_id = COALESCE(excluded.user_id, user_id),
161
+ name = COALESCE(excluded.name, name),
162
+ description = COALESCE(excluded.description, description),
163
+ tone = COALESCE(excluded.tone, tone),
164
+ system_prompt = excluded.system_prompt,
165
+ role = 'coordinator'
166
+ `).run(
167
+ data.agentId,
168
+ finalUserId,
169
+ data.agentName,
170
+ data.agentDescription || null,
171
+ data.agentTone || null,
172
+ HIVE_SYSTEM_PROMPT,
173
+ );
174
+ }
175
+
176
+ return finalUserId;
177
+ } catch (e) {
178
+ log.error("⚠️ Error saving user profile:", { error: (e as Error).message });
179
+ throw e;
180
+ }
181
+ }
182
+
183
+ export function activateSkills(userId: string, skillIds: string[]): void {
184
+ try {
185
+ const db = getDb();
186
+ // Activar skills seleccionadas
187
+ for (const skillId of skillIds) {
188
+ db.query(`UPDATE skills SET active = 1 WHERE id = ? `).run(skillId);
189
+ }
190
+ log.info("✅ Skills activadas:", { skillIds: skillIds.join(", ") });
191
+ } catch (e) {
192
+ log.error("⚠️ Error activating skills:", { error: (e as Error).message });
193
+ }
194
+ }
195
+
196
+ export function activateEthics(userId: string, ethicsId: string): void {
197
+ try {
198
+ const db = getDb();
199
+ // Activar el ethics seleccionado
200
+ db.query(`UPDATE ethics SET active = 1 WHERE id = ? `).run(ethicsId);
201
+ // Desactivar los demás
202
+ db.query(`UPDATE ethics SET active = 0 WHERE id != ? `).run(ethicsId);
203
+ log.info("✅ Ethics activado:", { ethicsId });
204
+ } catch (e) {
205
+ log.error("⚠️ Error activating ethics:", { error: (e as Error).message });
206
+ }
207
+ }
208
+
209
+ export function activateTools(userId: string, toolIds: string[]): void {
210
+ try {
211
+ const db = getDb();
212
+ // Activar tools seleccionadas
213
+ for (const toolId of toolIds) {
214
+ db.query(`UPDATE tools SET active = 1, enabled = 1 WHERE id = ? `).run(toolId);
215
+ }
216
+ log.info("✅ Tools activadas:", { toolIds: toolIds.join(", ") });
217
+ } catch (e) {
218
+ log.error("⚠️ Error activating tools:", { error: (e as Error).message });
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Activate all browser tools when Chromium is available
224
+ * Called from gateway initializer when browser service connects successfully
225
+ */
226
+ export function activateBrowserTools(): void {
227
+ try {
228
+ const db = getDb();
229
+ const browserToolIds = [
230
+ "browser_navigate",
231
+ "browser_screenshot",
232
+ "browser_click",
233
+ "browser_type",
234
+ "browser_extract",
235
+ "browser_script",
236
+ "browser_wait",
237
+ ];
238
+
239
+ for (const toolId of browserToolIds) {
240
+ db.query(`UPDATE tools SET active = 1, enabled = 1 WHERE id = ? `).run(toolId);
241
+ }
242
+ log.info("✅ Browser tools activated (Chromium available)");
243
+ } catch (e) {
244
+ log.error("⚠️ Error activating browser tools:", { error: (e as Error).message });
245
+ }
246
+ }
247
+
248
+ export async function saveProviderConfig(data: {
249
+ userId: string;
250
+ provider: string;
251
+ model: string;
252
+ apiKey?: string;
253
+ baseUrl?: string;
254
+ }): Promise<void> {
255
+ try {
256
+ const db = getDb();
257
+
258
+ let apiKeyEncrypted = null;
259
+ let apiKeyIv = null;
260
+
261
+ if (data.apiKey) {
262
+ const encrypted = await encryptApiKey(data.apiKey);
263
+ apiKeyEncrypted = encrypted.encrypted;
264
+ apiKeyIv = encrypted.iv;
265
+ }
266
+
267
+ // 1️⃣ Primero: Actualizar provider global con API key del usuario
268
+ db.query(`
269
+ UPDATE providers SET
270
+ api_key_encrypted = ?,
271
+ api_key_iv = ?,
272
+ base_url = ?,
273
+ enabled = 1,
274
+ active = 1
275
+ WHERE id = ?
276
+ `).run(apiKeyEncrypted, apiKeyIv, data.baseUrl || null, data.provider);
277
+
278
+ log.info("✅ Provider actualizado:", { provider: data.provider });
279
+
280
+ // 2️⃣ Segundo: Activar el modelo seleccionado
281
+ // For Ollama, models are inserted dynamically (not seeded), ensure row exists first
282
+ if (data.provider === "ollama" && data.model) {
283
+ db.query(`
284
+ INSERT OR IGNORE INTO models(id, name, provider_id, model_type, enabled, active)
285
+ VALUES(?, ?, 'ollama', 'llm', 1, 1)
286
+ `).run(data.model, data.model);
287
+ }
288
+
289
+ db.query(`
290
+ UPDATE models SET enabled = 1, active = 1
291
+ WHERE id = ?
292
+ `).run(data.model);
293
+
294
+ log.info("✅ Model activado:", { model: data.model });
295
+ } catch (e) {
296
+ log.error("⚠️ Error saving provider:", { error: (e as Error).message });
297
+ throw e;
298
+ }
299
+ }
300
+
301
+ export function activateCodeBridge(userId: string, codeBridgeConfig: { id: string; enabled: boolean; port?: number }[]): void {
302
+ try {
303
+ const db = getDb();
304
+ // 7️⃣ Séptimo: Configurar Code Bridge CLIs seleccionados
305
+ for (const cb of codeBridgeConfig) {
306
+ db.query(`
307
+ UPDATE code_bridge SET enabled = ?, active = ?, port = ?, user_id = ?
308
+ WHERE id = ?
309
+ `).run(cb.enabled ? 1 : 0, cb.enabled ? 1 : 0, cb.port || 18791, userId, cb.id);
310
+ }
311
+ log.info("✅ Code Bridge configurado:", { codeBridgeIds: codeBridgeConfig.map(c => c.id).join(", ") });
312
+ } catch (e) {
313
+ log.error("⚠️ Error configuring code bridge:", { error: (e as Error).message });
314
+ }
315
+ }
316
+
317
+ export function activateMcpServers(userId: string, mcpIds: string[]): void {
318
+ try {
319
+ const db = getDb();
320
+ // Activar MCP servers seleccionados
321
+ for (const mcpId of mcpIds) {
322
+ db.query(`UPDATE mcp_servers SET active = 1, enabled = 1 WHERE id = ? `).run(mcpId);
323
+ }
324
+ log.info("✅ MCP servers activados:", { mcpIds: mcpIds.join(", ") });
325
+ } catch (e) {
326
+ log.error("⚠️ Error activating MCP servers:", { error: (e as Error).message });
327
+ }
328
+ }
329
+
330
+
331
+ export function saveAgentConfig(data: {
332
+ userId: string;
333
+ agentId?: string;
334
+ agentName: string;
335
+ providerId: string;
336
+ modelId: string;
337
+ tone: string;
338
+ description?: string;
339
+ }): string {
340
+ try {
341
+ const db = getDb();
342
+ let finalAgentId = data.agentId;
343
+
344
+ // Validate FK references — use null if the referenced row doesn't exist
345
+ // (e.g. custom Ollama model IDs are not in the seed models table)
346
+ const rawProviderId = data.providerId || null;
347
+ const rawModelId = data.modelId || null;
348
+ const safeProviderId = rawProviderId && db.query("SELECT id FROM providers WHERE id = ?").get(rawProviderId) ? rawProviderId : null;
349
+ const safeModelId = rawModelId && db.query("SELECT id FROM models WHERE id = ?").get(rawModelId) ? rawModelId : null;
350
+
351
+ // Si no se pasa agentId, dejar que SQLite lo genere automáticamente
352
+ if (!finalAgentId) {
353
+ const result = db.query(`
354
+ INSERT INTO agents
355
+ (user_id, name, description, tone, system_prompt, provider_id, model_id, status, role, enabled)
356
+ VALUES(?, ?, ?, ?, ?, ?, ?, 'idle', 'coordinator', 1)
357
+ RETURNING id
358
+ `).get(
359
+ data.userId,
360
+ data.agentName,
361
+ data.description || null,
362
+ data.tone,
363
+ HIVE_SYSTEM_PROMPT,
364
+ safeProviderId,
365
+ safeModelId
366
+ ) as { id: string };
367
+ finalAgentId = result.id;
368
+ log.info("✅ Agent created with auto-generated ID", { agentId: finalAgentId });
369
+ } else {
370
+ // INSERT or UPDATE agent (crea nuevo o actualiza existente)
371
+ db.query(`
372
+ INSERT INTO agents
373
+ (id, user_id, name, description, tone, system_prompt, provider_id, model_id, status, role, enabled)
374
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, 'idle', 'coordinator', 1)
375
+ ON CONFLICT(id) DO UPDATE SET
376
+ user_id = COALESCE(excluded.user_id, user_id),
377
+ name = COALESCE(excluded.name, name),
378
+ description = COALESCE(excluded.description, description),
379
+ tone = COALESCE(excluded.tone, tone),
380
+ system_prompt = excluded.system_prompt,
381
+ provider_id = COALESCE(excluded.provider_id, provider_id),
382
+ model_id = COALESCE(excluded.model_id, model_id),
383
+ status = 'idle',
384
+ enabled = 1,
385
+ role = 'coordinator'
386
+ `).run(
387
+ data.agentId,
388
+ data.userId,
389
+ data.agentName,
390
+ data.description || null,
391
+ data.tone,
392
+ HIVE_SYSTEM_PROMPT,
393
+ safeProviderId,
394
+ safeModelId
395
+ );
396
+ }
397
+
398
+ return finalAgentId;
399
+ } catch (e) {
400
+ log.error("⚠️ Error saving agent:", { error: (e as Error).message });
401
+ throw e;
402
+ }
403
+ }
404
+
405
+ export async function activateChannel(userId: string, data: {
406
+ channelId: string;
407
+ channelUserId?: string; // For creating user_identity
408
+ config?: Record<string, unknown>;
409
+ }): Promise<void> {
410
+ try {
411
+ const db = getDb();
412
+
413
+ if (data.config && Object.keys(data.config).length > 0) {
414
+ const encrypted = await encryptConfig(data.config);
415
+ db.query(`
416
+ UPDATE channels
417
+ SET user_id = ?, active = 1, enabled = 1, status = 'connected',
418
+ config_encrypted = ?, config_iv = ?
419
+ WHERE id = ?
420
+ `).run(userId, encrypted.encrypted, encrypted.iv, data.channelId);
421
+ } else {
422
+ db.query(`
423
+ UPDATE channels
424
+ SET user_id = ?, active = 1, enabled = 1, status = 'connected'
425
+ WHERE id = ?
426
+ `).run(userId, data.channelId);
427
+ }
428
+
429
+ // Create user_identity for the channel if channelUserId provided
430
+ if (data.channelUserId) {
431
+ const channelType = data.channelId; // webchat, telegram, discord, etc.
432
+ db.query(`
433
+ INSERT OR REPLACE INTO user_identities(user_id, channel, channel_user_id)
434
+ VALUES(?, ?, ?)
435
+ `).run(userId, channelType, data.channelUserId);
436
+ log.info("✅ User identity created", { userId, channel: channelType });
437
+ }
438
+
439
+ log.info("✅ Channel activated:", { channelId: data.channelId, userId });
440
+ } catch (e) {
441
+ log.error("⚠️ Error activating channel:", { error: (e as Error).message });
442
+ }
443
+ }
444
+
445
+ export async function saveVoiceConfig(data: {
446
+ userId: string;
447
+ channelId: string;
448
+ voiceEnabled: boolean;
449
+ sttProvider: string;
450
+ ttsProvider: string;
451
+ sttApiKey?: string;
452
+ ttsApiKey?: string;
453
+ }): Promise<void> {
454
+ try {
455
+ const db = getDb();
456
+
457
+ // Activate STT and TTS models
458
+ db.query(`UPDATE models SET active = 1, enabled = 1 WHERE id = ? `).run(data.sttProvider);
459
+ db.query(`UPDATE models SET active = 1, enabled = 1 WHERE id = ? `).run(data.ttsProvider);
460
+
461
+ // Determine provider IDs based on model IDs
462
+ let sttProviderId = "";
463
+ let ttsProviderId = "";
464
+
465
+ if (data.sttProvider.startsWith("whisper") || data.sttProvider === "distil-whisper-large-v3-en") {
466
+ sttProviderId = "groq";
467
+ } else if (data.sttProvider === "whisper-1") {
468
+ sttProviderId = "openai";
469
+ }
470
+
471
+ if (data.ttsProvider.startsWith("eleven")) {
472
+ ttsProviderId = "elevenlabs";
473
+ } else if (data.ttsProvider.startsWith("tts-") || data.ttsProvider.startsWith("gpt-")) {
474
+ ttsProviderId = "openai";
475
+ } else if (data.ttsProvider.startsWith("gemini")) {
476
+ ttsProviderId = "gemini";
477
+ } else if (data.ttsProvider.startsWith("qwen")) {
478
+ ttsProviderId = "qwen";
479
+ }
480
+
481
+ // Save STT API key to provider if provided
482
+ if (data.sttApiKey && sttProviderId) {
483
+ const encrypted = await encryptApiKey(data.sttApiKey);
484
+ db.query(`
485
+ UPDATE providers SET
486
+ api_key_encrypted = ?,
487
+ api_key_iv = ?,
488
+ enabled = 1,
489
+ active = 1
490
+ WHERE id = ?
491
+ `).run(encrypted.encrypted, encrypted.iv, sttProviderId);
492
+ log.info("✅ STT API key guardada en BD (encriptada)", { provider: sttProviderId });
493
+ }
494
+
495
+ // Save TTS API key to provider if provided
496
+ if (data.ttsApiKey && ttsProviderId) {
497
+ const encrypted = await encryptApiKey(data.ttsApiKey);
498
+ db.query(`
499
+ UPDATE providers SET
500
+ api_key_encrypted = ?,
501
+ api_key_iv = ?,
502
+ enabled = 1,
503
+ active = 1
504
+ WHERE id = ?
505
+ `).run(encrypted.encrypted, encrypted.iv, ttsProviderId);
506
+ log.info("✅ TTS API key guardada en BD (encriptada)", { provider: ttsProviderId });
507
+ }
508
+
509
+ // Update channel with voice config
510
+ db.query(`
511
+ UPDATE channels
512
+ SET user_id = ?, voice_enabled = ?, stt_provider = ?, tts_provider = ?
513
+ WHERE id = ?
514
+ `).run(data.userId, data.voiceEnabled ? 1 : 0, data.sttProvider, data.ttsProvider, data.channelId);
515
+
516
+ log.info("✅ Voice config saved:", {
517
+ channelId: data.channelId,
518
+ userId: data.userId,
519
+ sttProvider: data.sttProvider,
520
+ ttsProvider: data.ttsProvider,
521
+ sttProviderId,
522
+ ttsProviderId
523
+ });
524
+ } catch (e) {
525
+ log.error("⚠️ Error saving voice config:", { error: (e as Error).message });
526
+ }
527
+ }
528
+
529
+ export async function saveMcpServer(data: {
530
+ userId: string;
531
+ name: string;
532
+ transport: string;
533
+ command?: string;
534
+ args?: string[];
535
+ env?: Record<string, string>;
536
+ url?: string;
537
+ enabled?: boolean;
538
+ }): Promise<void> {
539
+ try {
540
+ const db = getDb();
541
+
542
+ const mcpId = `${data.userId}:${data.name} `;
543
+
544
+ let envEncrypted = null;
545
+ let envIv = null;
546
+
547
+ if (data.env && Object.keys(data.env).length > 0) {
548
+ const encrypted = await encryptConfig(data.env as Record<string, unknown>);
549
+ envEncrypted = encrypted.encrypted;
550
+ envIv = encrypted.iv;
551
+ }
552
+
553
+ db.query(`
554
+ INSERT OR REPLACE INTO mcp_servers
555
+ (id, user_id, name, transport, command, args, env_encrypted, env_iv, url, enabled, builtin)
556
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
557
+ `).run(
558
+ mcpId,
559
+ data.userId,
560
+ data.name,
561
+ data.transport,
562
+ data.command || null,
563
+ JSON.stringify(data.args || []),
564
+ envEncrypted,
565
+ envIv,
566
+ data.url || null,
567
+ data.enabled ? 1 : 0
568
+ );
569
+
570
+ log.info("✅ MCP server saved:", { name: data.name });
571
+ } catch (e) {
572
+ log.error("⚠️ Error saving MCP server:", { error: (e as Error).message });
573
+ }
574
+ }
575
+
576
+ export function saveToolSelection(userId: string, tools: string[]): void {
577
+ try {
578
+ const db = getDb();
579
+
580
+ for (const tool of tools) {
581
+ // Activar la herramienta (ya existe del seed)
582
+ db.query(`
583
+ UPDATE tools SET active = 1, enabled = 1
584
+ WHERE id = ?
585
+ `).run(tool);
586
+ }
587
+
588
+ log.info("✅ Tools activadas:", { tools: tools.join(", ") });
589
+ } catch (e) {
590
+ log.error("⚠️ Error saving tools:", { error: (e as Error).message });
591
+ }
592
+ }
593
+
594
+ export function activateProvider(providerId: string): void {
595
+ try {
596
+ const db = getDb();
597
+ db.query(`
598
+ UPDATE providers SET active = 1, enabled = 1
599
+ WHERE id = ?
600
+ `).run(providerId);
601
+ log.info("✅ Provider activado:", { providerId });
602
+ } catch (e) {
603
+ log.error("⚠️ Error activating provider:", { error: (e as Error).message });
604
+ }
605
+ }
606
+
607
+ export function activateModel(modelId: string): void {
608
+ try {
609
+ const db = getDb();
610
+ db.query(`
611
+ UPDATE models SET active = 1, enabled = 1
612
+ WHERE id = ?
613
+ `).run(modelId);
614
+ log.info("✅ Model activado:", { modelId });
615
+ } catch (e) {
616
+ log.error("⚠️ Error activating model:", { error: (e as Error).message });
617
+ }
618
+ }
619
+
620
+
621
+
622
+ export function activateMcpServer(mcpName: string): void {
623
+ try {
624
+ const db = getDb();
625
+ db.query(`
626
+ UPDATE mcp_servers SET active = 1, enabled = 1
627
+ WHERE id = ?
628
+ `).run(mcpName);
629
+ log.info("✅ MCP server activado:", { mcpName });
630
+ } catch (e) {
631
+ log.error("⚠️ Error activating MCP server:", { error: (e as Error).message });
632
+ }
633
+ }
634
+
635
+ export function deactivateProvider(providerId: string): void {
636
+ try {
637
+ const db = getDb();
638
+ db.query(`
639
+ UPDATE providers SET active = 0, enabled = 0
640
+ WHERE id = ?
641
+ `).run(providerId);
642
+ log.warn("⚠️ Provider desactivado:", { providerId });
643
+ } catch (e) {
644
+ log.error("⚠️ Error deactivating provider:", { error: (e as Error).message });
645
+ }
646
+ }
647
+
648
+ export function deactivateModel(modelId: string): void {
649
+ try {
650
+ const db = getDb();
651
+ db.query(`
652
+ UPDATE models SET active = 0, enabled = 0
653
+ WHERE id = ?
654
+ `).run(modelId);
655
+ log.warn("⚠️ Model desactivado:", { modelId });
656
+ } catch (e) {
657
+ log.error("⚠️ Error deactivating model:", { error: (e as Error).message });
658
+ }
659
+ }
660
+
661
+ export function deactivateChannel(channelType: string): void {
662
+ try {
663
+ const db = getDb();
664
+ db.query(`
665
+ UPDATE channels SET active = 0, enabled = 0
666
+ WHERE id = ?
667
+ `).run(channelType);
668
+ log.warn("⚠️ Channel desactivado:", { channelType });
669
+ } catch (e) {
670
+ log.error("⚠️ Error deactivating channel:", { error: (e as Error).message });
671
+ }
672
+ }
673
+
674
+ export function deactivateMcpServer(mcpName: string): void {
675
+ try {
676
+ const db = getDb();
677
+ db.query(`
678
+ UPDATE mcp_servers SET active = 0, enabled = 0
679
+ WHERE id = ?
680
+ `).run(mcpName);
681
+ log.warn("⚠️ MCP server desactivado:", { mcpName });
682
+ } catch (e) {
683
+ log.error("⚠️ Error deactivating MCP server:", { error: (e as Error).message });
684
+ }
685
+ }
686
+
687
+ export function getAllProviders(): Array<{
688
+ id: string;
689
+ name: string;
690
+ baseUrl: string | null;
691
+ enabled: boolean;
692
+ active: boolean;
693
+ }> {
694
+ try {
695
+ const db = getDb();
696
+ const results = db.query(`
697
+ SELECT id, name, base_url, enabled, active
698
+ FROM providers
699
+ `).all() as Array<{
700
+ id: string;
701
+ name: string;
702
+ base_url: string | null;
703
+ enabled: number;
704
+ active: number;
705
+ }>;
706
+
707
+ return results.map(r => ({
708
+ id: r.id,
709
+ name: r.name,
710
+ baseUrl: r.base_url,
711
+ enabled: r.enabled === 1,
712
+ active: r.active === 1,
713
+ }));
714
+ } catch (e) {
715
+ log.warn("[onboarding] ⚠️ Error getting providers:", (e as Error).message);
716
+ return [];
717
+ }
718
+ }
719
+
720
+ export function getAllModels(): Array<{
721
+ id: string;
722
+ name: string;
723
+ providerId: string;
724
+ contextWindow: number | null;
725
+ capabilities: string | null;
726
+ enabled: boolean;
727
+ active: boolean;
728
+ }> {
729
+ try {
730
+ const db = getDb();
731
+ const results = db.query(`
732
+ SELECT id, name, provider_id, context_window, capabilities, enabled, active
733
+ FROM models
734
+ `).all() as Array<{
735
+ id: string;
736
+ name: string;
737
+ provider_id: string;
738
+ context_window: number | null;
739
+ capabilities: string | null;
740
+ enabled: number;
741
+ active: number;
742
+ }>;
743
+
744
+ return results.map(r => ({
745
+ id: r.id,
746
+ name: r.name,
747
+ providerId: r.provider_id,
748
+ contextWindow: r.context_window,
749
+ capabilities: r.capabilities,
750
+ enabled: r.enabled === 1,
751
+ active: r.active === 1,
752
+ }));
753
+ } catch (e) {
754
+ log.error("⚠️ Error getting models:", { error: (e as Error).message });
755
+ return [];
756
+ }
757
+ }
758
+
759
+ export function getAllEthics(): Array<{
760
+ id: string;
761
+ name: string;
762
+ description: string | null;
763
+ content: string;
764
+ isDefault: boolean;
765
+ active: boolean;
766
+ }> {
767
+ try {
768
+ const db = getDb();
769
+ const results = db.query(`
770
+ SELECT id, name, description, content, is_default, active
771
+ FROM ethics
772
+ `).all() as Array<{
773
+ id: string;
774
+ name: string;
775
+ description: string | null;
776
+ content: string;
777
+ is_default: number;
778
+ active: number;
779
+ }>;
780
+
781
+ return results.map(r => ({
782
+ id: r.id,
783
+ name: r.name,
784
+ description: r.description,
785
+ content: r.content,
786
+ isDefault: r.is_default === 1,
787
+ active: r.active === 1,
788
+ }));
789
+ } catch (e) {
790
+ log.error("⚠️ Error getting ethics:", { error: (e as Error).message });
791
+ return [];
792
+ }
793
+ }
794
+
795
+ export function getAllCodeBridge(): Array<{
796
+ id: string;
797
+ name: string;
798
+ cliCommand: string;
799
+ port: number;
800
+ enabled: boolean;
801
+ active: boolean;
802
+ }> {
803
+ try {
804
+ const db = getDb();
805
+ const results = db.query(`
806
+ SELECT id, name, cli_command, port, enabled, active
807
+ FROM code_bridge
808
+ `).all() as Array<{
809
+ id: string;
810
+ name: string;
811
+ cli_command: string;
812
+ port: number;
813
+ enabled: number;
814
+ active: number;
815
+ }>;
816
+
817
+ return results.map(r => ({
818
+ id: r.id,
819
+ name: r.name,
820
+ cliCommand: r.cli_command,
821
+ port: r.port,
822
+ enabled: r.enabled === 1,
823
+ active: r.active === 1,
824
+ }));
825
+ } catch (e) {
826
+ log.error("⚠️ Error getting code bridge:", { error: (e as Error).message });
827
+ return [];
828
+ }
829
+ }
830
+
831
+ export function getAllSkills(): Array<{
832
+ id: string;
833
+ name: string;
834
+ description: string | null;
835
+ source: string;
836
+ isGlobal: boolean;
837
+ enabled: boolean;
838
+ active: boolean;
839
+ }> {
840
+ try {
841
+ const db = getDb();
842
+ const results = db.query(`
843
+ SELECT id, name, api_key_encrypted, api_key_iv, base_url, enabled
844
+ FROM providers
845
+ `).all() as Array<{
846
+ id: string;
847
+ name: string;
848
+ description: string | null;
849
+ source: string;
850
+ enabled: number;
851
+ active: number;
852
+ }>;
853
+
854
+ return results.map(r => ({
855
+ id: r.id,
856
+ name: r.name,
857
+ description: r.description,
858
+ source: r.source,
859
+ isGlobal: false,
860
+ enabled: r.enabled === 1,
861
+ active: r.active === 1,
862
+ }));
863
+ } catch (e) {
864
+ log.error("⚠️ Error getting skills:", { error: (e as Error).message });
865
+ return [];
866
+ }
867
+ }
868
+
869
+ export function getAllDbTools(): Array<{
870
+ id: string;
871
+ name: string;
872
+ description: string | null;
873
+ category: string | null;
874
+ enabled: boolean;
875
+ active: boolean;
876
+ }> {
877
+ try {
878
+ const db = getDb();
879
+ const results = db.query(`
880
+ SELECT id, name, description, category, enabled, active
881
+ FROM tools
882
+ `).all() as Array<{
883
+ id: string;
884
+ name: string;
885
+ description: string | null;
886
+ category: string | null;
887
+ enabled: number;
888
+ active: number;
889
+ }>;
890
+
891
+ return results.map(r => ({
892
+ id: r.id,
893
+ name: r.name,
894
+ description: r.description,
895
+ category: r.category,
896
+ enabled: r.enabled === 1,
897
+ active: r.active === 1,
898
+ }));
899
+ } catch (e) {
900
+ log.error("⚠️ Error getting tools:", { error: (e as Error).message });
901
+ return [];
902
+ }
903
+ }
904
+
905
+ export function getAllMcpServers(): Array<{
906
+ id: string;
907
+ name: string;
908
+ transport: string;
909
+ command: string | null;
910
+ args: string | null;
911
+ url: string | null;
912
+ builtin: boolean;
913
+ enabled: boolean;
914
+ active: boolean;
915
+ }> {
916
+ try {
917
+ const db = getDb();
918
+ const results = db.query(`
919
+ SELECT id, name, transport, command, args, url, builtin, enabled, active
920
+ FROM mcp_servers
921
+ `).all() as Array<{
922
+ id: string;
923
+ name: string;
924
+ transport: string;
925
+ command: string | null;
926
+ args: string | null;
927
+ url: string | null;
928
+ builtin: number;
929
+ enabled: number;
930
+ active: number;
931
+ }>;
932
+
933
+ return results.map(r => ({
934
+ id: r.id,
935
+ name: r.name,
936
+ transport: r.transport,
937
+ command: r.command,
938
+ args: r.args,
939
+ url: r.url,
940
+ builtin: r.builtin === 1,
941
+ enabled: r.enabled === 1,
942
+ active: r.active === 1,
943
+ }));
944
+ } catch (e) {
945
+ log.error("⚠️ Error getting MCP servers:", { error: (e as Error).message });
946
+ return [];
947
+ }
948
+ }
949
+
950
+ export function getAllChannels(): Array<{
951
+ id: string;
952
+ type: string;
953
+ accountId: string;
954
+ status: string;
955
+ enabled: boolean;
956
+ active: boolean;
957
+ }> {
958
+ try {
959
+ const db = getDb();
960
+ const results = db.query(`
961
+ SELECT id, type, id as account_id, status, enabled, active
962
+ FROM channels
963
+ `).all() as Array<{
964
+ id: string;
965
+ type: string;
966
+ account_id: string;
967
+ status: string;
968
+ enabled: number;
969
+ active: number;
970
+ }>;
971
+
972
+ return results.map(r => ({
973
+ id: r.id,
974
+ type: r.type,
975
+ accountId: r.id,
976
+ status: r.status,
977
+ enabled: r.enabled === 1,
978
+ active: r.active === 1,
979
+ }));
980
+ } catch (e) {
981
+ log.warn("[onboarding] ⚠️ Error getting channels:", (e as Error).message);
982
+ return [];
983
+ }
984
+ }
985
+
986
+ export function getActiveTools(): Array<{
987
+ id: string;
988
+ name: string;
989
+ description: string | null;
990
+ category: string | null;
991
+ }> {
992
+ try {
993
+ const db = getDb();
994
+ const results = db.query(`
995
+ SELECT id, name, description, category
996
+ FROM tools WHERE active = 1
997
+ `).all() as Array<{
998
+ id: string;
999
+ name: string;
1000
+ description: string | null;
1001
+ category: string | null;
1002
+ }>;
1003
+
1004
+ return results.map(r => ({
1005
+ id: r.id,
1006
+ name: r.name,
1007
+ description: r.description,
1008
+ category: r.category,
1009
+ }));
1010
+ } catch (e) {
1011
+ log.error("⚠️ Error getting active tools:", { error: (e as Error).message });
1012
+ return [];
1013
+ }
1014
+ }
1015
+
1016
+ export function getOnboardingProgress(userId: string): OnboardingSection | null {
1017
+ try {
1018
+ const db = getDb();
1019
+ const result = db.query<{ step: string; data: string }, [string]>(
1020
+ "SELECT step, data FROM onboarding_progress WHERE user_id = ? LIMIT 1"
1021
+ ).get(userId);
1022
+
1023
+ if (result) {
1024
+ return {
1025
+ step: result.step as OnboardingSection["step"],
1026
+ userId,
1027
+ data: JSON.parse(result.data),
1028
+ completedAt: Date.now(),
1029
+ };
1030
+ }
1031
+ return null;
1032
+ } catch {
1033
+ return null;
1034
+ }
1035
+ }
1036
+
1037
+ export function saveOnboardingProgress(section: OnboardingSection): void {
1038
+ try {
1039
+ const db = getDb();
1040
+ db.query(`
1041
+ INSERT OR REPLACE INTO onboarding_progress(id, user_id, step, data)
1042
+ VALUES(?, ?, ?, ?)
1043
+ `).run(section.userId, section.userId, section.step, JSON.stringify(section.data));
1044
+ } catch (e) {
1045
+ log.error("⚠️ Error saving progress:", { error: (e as Error).message });
1046
+ }
1047
+ }
1048
+
1049
+ export async function getUserProviders(userId: string): Promise<Array<{
1050
+ id: string;
1051
+ name: string;
1052
+ apiKey: string | null;
1053
+ baseUrl: string | null;
1054
+ enabled: boolean;
1055
+ }>> {
1056
+ try {
1057
+ const db = getDb();
1058
+ const results = db.query(`
1059
+ SELECT id, name, api_key_encrypted, api_key_iv, base_url, enabled
1060
+ FROM providers
1061
+ `).all() as Array<{
1062
+ id: string;
1063
+ name: string;
1064
+ api_key_encrypted: string | null;
1065
+ api_key_iv: string | null;
1066
+ base_url: string | null;
1067
+ enabled: number;
1068
+ }>;
1069
+
1070
+ return Promise.all(results.map(async r => ({
1071
+ id: r.name,
1072
+ name: r.name,
1073
+ apiKey: r.api_key_encrypted && r.api_key_iv
1074
+ ? await decryptApiKey(r.api_key_encrypted, r.api_key_iv)
1075
+ : null,
1076
+ baseUrl: r.base_url,
1077
+ enabled: r.enabled === 1,
1078
+ })));
1079
+ } catch (e) {
1080
+ log.warn("[onboarding] ⚠️ Error getting providers:", (e as Error).message);
1081
+ return [];
1082
+ }
1083
+ }
1084
+
1085
+ export async function getUserChannels(userId: string): Promise<Array<{
1086
+ id: string;
1087
+ type: string;
1088
+ accountId: string;
1089
+ config: Record<string, unknown>;
1090
+ enabled: boolean;
1091
+ }>> {
1092
+ try {
1093
+ const db = getDb();
1094
+ const results = db.query<{
1095
+ id: string;
1096
+ type: string;
1097
+ account_id: string;
1098
+ config_encrypted: string | null;
1099
+ config_iv: string | null;
1100
+ enabled: number;
1101
+ }, [string]>(`
1102
+ SELECT id, type, id as account_id, config_encrypted, config_iv, enabled
1103
+ FROM channels WHERE user_id = ?
1104
+ `).all(userId);
1105
+
1106
+ return Promise.all(results.map(async r => ({
1107
+ id: r.type,
1108
+ type: r.type,
1109
+ accountId: r.id,
1110
+ config: r.config_encrypted && r.config_iv
1111
+ ? await decryptConfig(r.config_encrypted, r.config_iv)
1112
+ : {},
1113
+ enabled: r.enabled === 1,
1114
+ })));
1115
+ } catch (e) {
1116
+ log.warn("[onboarding] ⚠️ Error getting channels:", (e as Error).message);
1117
+ return [];
1118
+ }
1119
+ }
1120
+
1121
+ export function getUserAgents(userId: string): Array<{
1122
+ id: string;
1123
+ name: string;
1124
+ providerId: string | null;
1125
+ modelId: string | null;
1126
+ tone: string;
1127
+ }> {
1128
+ try {
1129
+ const db = getDb();
1130
+ const results = db.query<{
1131
+ id: string;
1132
+ name: string;
1133
+ provider_id: string | null;
1134
+ model_id: string | null;
1135
+ tone: string;
1136
+ }, [string]>(`
1137
+ SELECT id, name, provider_id, model_id, tone
1138
+ FROM agents WHERE user_id = ?
1139
+ `).all(userId);
1140
+
1141
+ return results.map(r => ({
1142
+ id: r.id,
1143
+ name: r.name,
1144
+ providerId: r.provider_id,
1145
+ modelId: r.model_id,
1146
+ tone: r.tone || "friendly",
1147
+ }));
1148
+ } catch (e) {
1149
+ log.error("⚠️ Error getting agents:", { error: (e as Error).message });
1150
+ return [];
1151
+ }
1152
+ }
1153
+
1154
+ // ─── Identity Resolution Helpers ──────────────────────────────────────────────
1155
+ // These functions resolve userId and agentId from the database instead of environment variables
1156
+
1157
+ /**
1158
+ * Get the single user ID from the database.
1159
+ * Hive is designed around a single-user model, so this returns the first user found.
1160
+ * @returns The user ID or null if no users exist
1161
+ */
1162
+ export function getSingleUserId(): string | null {
1163
+ try {
1164
+ const db = getDb();
1165
+ const result = db.query("SELECT id FROM users LIMIT 1").get() as { id: string } | undefined;
1166
+ return result?.id || null;
1167
+ } catch (e) {
1168
+ log.warn("[getSingleUserId] ⚠️ Error getting user ID:", (e as Error).message);
1169
+ return null;
1170
+ }
1171
+ }
1172
+
1173
+ /**
1174
+ * Get the coordinator agent ID from the database.
1175
+ * The coordinator is the agent with role = 'coordinator'.
1176
+ * @returns The coordinator agent ID or null if not found
1177
+ */
1178
+ export function getCoordinatorAgentId(): string | null {
1179
+ try {
1180
+ const db = getDb();
1181
+ const result = db.query("SELECT id FROM agents WHERE role = 'coordinator' LIMIT 1").get() as { id: string } | undefined;
1182
+ return result?.id || null;
1183
+ } catch (e) {
1184
+ log.warn("[getCoordinatorAgentId] ⚠️ Error getting coordinator agent ID:", (e as Error).message);
1185
+ return null;
1186
+ }
1187
+ }
1188
+
1189
+ /**
1190
+ * Get the user ID associated with a specific channel identity.
1191
+ * @param channel The channel type (e.g., 'webchat', 'telegram', 'discord')
1192
+ * @param channelUserId The channel-specific user ID (e.g., Telegram chat_id)
1193
+ * @returns The Hive user ID or null if not found
1194
+ */
1195
+ export function getUserIdFromChannelIdentity(channel: string, channelUserId: string): string | null {
1196
+ try {
1197
+ const db = getDb();
1198
+ const result = db.query(
1199
+ "SELECT user_id FROM user_identities WHERE channel = ? AND channel_user_id = ? LIMIT 1"
1200
+ ).get(channel, channelUserId) as { user_id: string } | undefined;
1201
+ return result?.user_id || null;
1202
+ } catch (e) {
1203
+ log.warn("[getUserIdFromChannelIdentity] ⚠️ Error getting user ID from channel identity:", (e as Error).message);
1204
+ return null;
1205
+ }
1206
+ }
1207
+
1208
+ /**
1209
+ * Resolve the user ID from various sources with priority:
1210
+ * 1. Explicit userId parameter
1211
+ * 2. Channel identity lookup (if channel and channelUserId provided)
1212
+ * 3. Single user from database
1213
+ * 4. Null (no user found)
1214
+ */
1215
+ export function resolveUserId(
1216
+ opts: {
1217
+ userId?: string | null;
1218
+ threadId?: string | null;
1219
+ channel?: string | null;
1220
+ channelUserId?: string | null;
1221
+ }
1222
+ ): string | null {
1223
+ // Priority 1: Explicit userId
1224
+ if (opts.userId) {
1225
+ return opts.userId;
1226
+ }
1227
+
1228
+ // Priority 2: Channel identity lookup
1229
+ if (opts.channel && opts.channelUserId) {
1230
+ const userId = getUserIdFromChannelIdentity(opts.channel, opts.channelUserId);
1231
+ if (userId) {
1232
+ return userId;
1233
+ }
1234
+ }
1235
+
1236
+ // Priority 3: Single user from database
1237
+ const singleUserId = getSingleUserId();
1238
+ if (singleUserId) {
1239
+ return singleUserId;
1240
+ }
1241
+
1242
+ // Priority 4: No user found
1243
+ return null;
1244
+ }
1245
+
1246
+ /**
1247
+ * Get the default agent ID with priority:
1248
+ * 1. Coordinator agent (role = 'coordinator')
1249
+ * 2. First enabled agent
1250
+ * 3. Null (no agent found)
1251
+ */
1252
+ export function getDefaultAgentId(): string | null {
1253
+ try {
1254
+ const db = getDb();
1255
+
1256
+ // Try coordinator first
1257
+ const coordinator = db.query(
1258
+ "SELECT id FROM agents WHERE role = 'coordinator' AND enabled = 1 LIMIT 1"
1259
+ ).get() as { id: string } | undefined;
1260
+
1261
+ if (coordinator?.id) {
1262
+ return coordinator.id;
1263
+ }
1264
+
1265
+ // Fallback to first enabled agent
1266
+ const firstAgent = db.query(
1267
+ "SELECT id FROM agents WHERE enabled = 1 LIMIT 1"
1268
+ ).get() as { id: string } | undefined;
1269
+
1270
+ return firstAgent?.id || null;
1271
+ } catch (e) {
1272
+ log.warn("[getDefaultAgentId] ⚠️ Error getting default agent ID:", (e as Error).message);
1273
+ return null;
1274
+ }
1275
+ }
1276
+
1277
+ /**
1278
+ * Resolve the agent ID with priority:
1279
+ * 1. Explicit agentId parameter
1280
+ * 2. Coordinator agent from database
1281
+ * 3. First enabled agent from database
1282
+ * 4. Null (no agent found)
1283
+ */
1284
+ export function resolveAgentId(agentId?: string | null): string | null {
1285
+ // Priority 1: Explicit agentId
1286
+ if (agentId) {
1287
+ return agentId;
1288
+ }
1289
+
1290
+ // Priority 2: Default from database (coordinator or first enabled)
1291
+ return getDefaultAgentId();
1292
+ }
1293
+
1294
+ /**
1295
+ * Get user preferences (notes) for a given user ID
1296
+ */
1297
+ export function getUserPreferences(userId: string): string | null {
1298
+ try {
1299
+ const db = getDb();
1300
+ const result = db.query("SELECT notes FROM users WHERE id = ?").get(userId) as { notes: string | null } | undefined;
1301
+ return result?.notes || null;
1302
+ } catch (e) {
1303
+ log.warn("[getUserPreferences] ⚠️ Error getting user preferences:", (e as Error).message);
1304
+ return null;
1305
+ }
1306
+ }
1307
+
1308
+ /**
1309
+ * Get agent configuration by ID
1310
+ */
1311
+ export function getAgentConfig(agentId: string): {
1312
+ id: string;
1313
+ user_id: string;
1314
+ name: string;
1315
+ description: string | null;
1316
+ system_prompt: string | null;
1317
+ tone: string | null;
1318
+ provider_id: string | null;
1319
+ model_id: string | null;
1320
+ tools_json: string | null;
1321
+ skills_json: string | null;
1322
+ max_iterations: number;
1323
+ } | null {
1324
+ try {
1325
+ const db = getDb();
1326
+ const result = db.query(`
1327
+ SELECT id, user_id, name, description, system_prompt, tone,
1328
+ provider_id, model_id, tools_json, skills_json, max_iterations
1329
+ FROM agents WHERE id = ?
1330
+ `).get(agentId) as {
1331
+ id: string;
1332
+ user_id: string;
1333
+ name: string;
1334
+ description: string | null;
1335
+ system_prompt: string | null;
1336
+ tone: string | null;
1337
+ provider_id: string | null;
1338
+ model_id: string | null;
1339
+ tools_json: string | null;
1340
+ skills_json: string | null;
1341
+ max_iterations: number;
1342
+ } | undefined;
1343
+
1344
+ return result || null;
1345
+ } catch (e) {
1346
+ log.warn("[getAgentConfig] ⚠️ Error getting agent config:", (e as Error).message);
1347
+ return null;
1348
+ }
1349
+ }
1350
+
1351
+ /**
1352
+ * Idempotent startup migrations. Runs on every gateway start.
1353
+ * Each migration is guarded by the schema_migrations table — once applied, it never re-runs.
1354
+ */
1355
+ export function runStartupMigrations(): void {
1356
+ try {
1357
+ const db = getDb();
1358
+
1359
+ const applied = (v: string) =>
1360
+ !!db.query("SELECT 1 FROM schema_migrations WHERE version = ?").get(v);
1361
+ const markApplied = (v: string) =>
1362
+ db.query("INSERT OR IGNORE INTO schema_migrations(version) VALUES(?)").run(v);
1363
+
1364
+ // v0.0.29 — consolidate tools + skills: drop and recreate tables with current schema, reseed
1365
+ if (!applied("v0.0.29")) {
1366
+ const db = getDb();
1367
+ log.info("[migration v0.0.29] Dropping and recreating tools + skills tables...");
1368
+
1369
+ db.run("DROP TABLE IF EXISTS skills_fts");
1370
+ db.run("DROP TABLE IF EXISTS skills");
1371
+ db.run("DROP TABLE IF EXISTS tools_fts");
1372
+ db.run("DROP TABLE IF EXISTS tools");
1373
+
1374
+ db.run(`CREATE TABLE tools (
1375
+ id TEXT PRIMARY KEY,
1376
+ name TEXT NOT NULL UNIQUE,
1377
+ description TEXT,
1378
+ category TEXT,
1379
+ enabled INTEGER NOT NULL DEFAULT 1,
1380
+ active INTEGER NOT NULL DEFAULT 1,
1381
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
1382
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
1383
+ )`);
1384
+
1385
+ db.run(`CREATE VIRTUAL TABLE tools_fts USING fts5(tool_name, name, description, category)`);
1386
+
1387
+ db.run(`CREATE TABLE skills (
1388
+ id TEXT PRIMARY KEY,
1389
+ name TEXT NOT NULL,
1390
+ description TEXT,
1391
+ version TEXT DEFAULT '0.0.1',
1392
+ author TEXT DEFAULT 'Anonymous',
1393
+ icon TEXT DEFAULT '🧩',
1394
+ category TEXT NOT NULL,
1395
+ permissions TEXT,
1396
+ dependencies TEXT,
1397
+ tools TEXT NOT NULL,
1398
+ triggers TEXT NOT NULL,
1399
+ preferred_agents TEXT,
1400
+ body TEXT NOT NULL,
1401
+ version_num INTEGER DEFAULT 1,
1402
+ active INTEGER DEFAULT 1,
1403
+ created_at TEXT DEFAULT (datetime('now')),
1404
+ updated_at TEXT DEFAULT (datetime('now'))
1405
+ )`);
1406
+
1407
+ db.run(`CREATE VIRTUAL TABLE skills_fts USING fts5(id, name, description, category, tools, triggers, body)`);
1408
+
1409
+ db.run("CREATE INDEX IF NOT EXISTS idx_skills_category ON skills(category)");
1410
+ db.run("CREATE INDEX IF NOT EXISTS idx_skills_active ON skills(active)");
1411
+
1412
+ db.run(`DROP TRIGGER IF EXISTS skills_ai`);
1413
+ db.run(`DROP TRIGGER IF EXISTS skills_au`);
1414
+ db.run(`DROP TRIGGER IF EXISTS skills_ad`);
1415
+ db.run(`CREATE TRIGGER skills_ai AFTER INSERT ON skills BEGIN
1416
+ INSERT INTO skills_fts(id, name, description, category, tools, triggers, body)
1417
+ VALUES (new.id, new.name, new.description, new.category, new.tools, new.triggers, new.body);
1418
+ END`);
1419
+ db.run(`CREATE TRIGGER skills_au AFTER UPDATE ON skills BEGIN
1420
+ DELETE FROM skills_fts WHERE id = old.id;
1421
+ INSERT INTO skills_fts(id, name, description, category, tools, triggers, body)
1422
+ VALUES (new.id, new.name, new.description, new.category, new.tools, new.triggers, new.body);
1423
+ END`);
1424
+ db.run(`CREATE TRIGGER skills_ad AFTER DELETE ON skills BEGIN
1425
+ DELETE FROM skills_fts WHERE id = old.id;
1426
+ END`);
1427
+
1428
+ // Reseed tools
1429
+ const insertToolFts = db.prepare(`INSERT OR REPLACE INTO tools_fts(tool_name, name, description, category) VALUES (?, ?, ?, ?)`);
1430
+ let toolCount = 0;
1431
+ for (const tool of SEED_DATA.tools) {
1432
+ db.query(`INSERT INTO tools (id, name, description, category, enabled, active, created_at, updated_at) VALUES (?, ?, ?, ?, 1, 1, (unixepoch()), (unixepoch()))`)
1433
+ .run(tool.id, tool.name, tool.description, tool.category);
1434
+ insertToolFts.run(tool.name, tool.name, tool.description, tool.category);
1435
+ toolCount++;
1436
+ }
1437
+ log.info(`[migration v0.0.29] ✅ ${toolCount} tools re-seeded`);
1438
+
1439
+ // Reseed skills from SkillLoader
1440
+ const skillLoader = new SkillLoader({ workspacePath: process.env.HIVE_HOME || process.cwd() });
1441
+ const bundledSkills = skillLoader.loadBundledSkills();
1442
+ log.info(`[migration v0.0.29] 📚 SkillLoader loaded ${bundledSkills.length} bundled skills`);
1443
+ let skillCount = 0;
1444
+ for (const s of bundledSkills) {
1445
+ db.query(`
1446
+ INSERT OR REPLACE INTO skills (
1447
+ id, name, description, version, author, icon, category,
1448
+ permissions, dependencies, tools, triggers, preferred_agents,
1449
+ body, version_num, active, created_at, updated_at
1450
+ )
1451
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, (unixepoch()), (unixepoch()))
1452
+ `).run(
1453
+ s.name, s.name, s.description || "",
1454
+ typeof s.version === "string" ? s.version : String(s.version || "0.0.1"),
1455
+ s.author || "Anonymous",
1456
+ s.icon || "🧩",
1457
+ s.category || "general",
1458
+ JSON.stringify(s.permissions || []),
1459
+ JSON.stringify(s.dependencies || []),
1460
+ (s.tools || []).join(","),
1461
+ (s.triggers || []).join(","),
1462
+ JSON.stringify(s.preferred_agents || []),
1463
+ s.content || "",
1464
+ parseInt(String(s.version || "0.0.1").split(".")[0]) || 1
1465
+ );
1466
+ skillCount++;
1467
+ }
1468
+ log.info(`[migration v0.0.29] ✅ ${skillCount} skills re-seeded (FTS5 auto-synced via triggers)`);
1469
+
1470
+ markApplied("v0.0.29");
1471
+ log.info("✅ Migration v0.0.29: tools + skills consolidated, dropped and recreated");
1472
+ }
1473
+
1474
+ // v0.0.30 — add NVIDIA NIM provider + 12 free models (without dropping existing data)
1475
+ if (!applied("v0.0.30")) {
1476
+ const db = getDb();
1477
+ log.info("[migration v0.0.30] Ensuring providers table exists...");
1478
+ db.run(`CREATE TABLE IF NOT EXISTS providers (
1479
+ id TEXT PRIMARY KEY,
1480
+ name TEXT NOT NULL UNIQUE,
1481
+ api_key_encrypted TEXT,
1482
+ api_key_iv TEXT,
1483
+ headers_encrypted TEXT,
1484
+ headers_iv TEXT,
1485
+ base_url TEXT,
1486
+ category TEXT NOT NULL DEFAULT 'llm',
1487
+ num_ctx INTEGER,
1488
+ num_gpu INTEGER DEFAULT -1,
1489
+ enabled INTEGER NOT NULL DEFAULT 1,
1490
+ active INTEGER NOT NULL DEFAULT 0,
1491
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
1492
+ )`);
1493
+ log.info("[migration v0.0.30] Ensuring models table exists...");
1494
+ db.run(`CREATE TABLE IF NOT EXISTS models (
1495
+ id TEXT PRIMARY KEY,
1496
+ provider_id TEXT REFERENCES providers(id) ON DELETE CASCADE,
1497
+ name TEXT NOT NULL,
1498
+ model_type TEXT NOT NULL DEFAULT 'llm',
1499
+ context_window INTEGER NOT NULL DEFAULT 20000,
1500
+ capabilities TEXT,
1501
+ enabled INTEGER NOT NULL DEFAULT 1,
1502
+ active INTEGER NOT NULL DEFAULT 0
1503
+ )`);
1504
+ db.run("CREATE INDEX IF NOT EXISTS idx_models_provider ON models(provider_id)");
1505
+ db.run("CREATE INDEX IF NOT EXISTS idx_models_type ON models(model_type)");
1506
+ log.info("[migration v0.0.30] Adding new providers and models...");
1507
+ for (const provider of SEED_DATA.providers) {
1508
+ db.query(`
1509
+ INSERT OR IGNORE INTO providers (id, name, base_url, category, enabled, active)
1510
+ VALUES (?, ?, ?, ?, 1, 0)
1511
+ `).run(provider.id, provider.name, provider.baseUrl || null, provider.category || 'llm');
1512
+ }
1513
+ const ollamaHost = process.env.OLLAMA_HOST;
1514
+ if (ollamaHost) {
1515
+ db.query(`UPDATE providers SET base_url = ? WHERE id = 'ollama'`).run(ollamaHost);
1516
+ log.info(`[migration v0.0.30] ✅ Ollama base_url set to ${ollamaHost} (from OLLAMA_HOST env)`);
1517
+ }
1518
+ let modelCount = 0;
1519
+ for (const model of SEED_DATA.models) {
1520
+ db.query(`
1521
+ INSERT OR IGNORE INTO models (id, provider_id, name, model_type, context_window, capabilities, enabled, active)
1522
+ VALUES (?, ?, ?, ?, ?, ?, 1, 0)
1523
+ `).run(model.id, model.providerId, model.name, model.modelType, model.contextWindow || null, model.capabilities || null);
1524
+ modelCount++;
1525
+ }
1526
+ log.info(`[migration v0.0.30] ✅ Added ${SEED_DATA.providers.length} providers and ${modelCount} models`);
1527
+ markApplied("v0.0.30");
1528
+ log.info("✅ Migration v0.0.30: NVIDIA NIM provider + 12 free models added");
1529
+ }
1530
+
1531
+ // v0.0.31 — Update coordinator system_prompt to reduced version + sync bundled skills
1532
+ if (!applied("v0.0.31")) {
1533
+ const db = getDb();
1534
+ log.info("[migration v0.0.31] Updating coordinator system_prompt...");
1535
+
1536
+ // Update coordinator system_prompt with new concise version
1537
+ db.run(`UPDATE agents SET system_prompt = ? WHERE role = 'coordinator'`, [HIVE_SYSTEM_PROMPT]);
1538
+ const updated = db.query("SELECT name FROM agents WHERE role = 'coordinator' AND system_prompt = ?").get(HIVE_SYSTEM_PROMPT);
1539
+ if (updated) {
1540
+ log.info("[migration v0.0.31] ✅ Coordinator system_prompt updated");
1541
+ } else {
1542
+ log.warn("[migration v0.0.31] ⚠️ Coordinator update may have failed - checking length...");
1543
+ }
1544
+
1545
+ // Add/update skills from bundled data (busqueda_fts5, canvas_report, memory_manager minimal set)
1546
+ log.info("[migration v0.0.31] Verifying minimal skills exist...");
1547
+ const skillLoader = new SkillLoader({ workspacePath: process.env.HIVE_HOME || process.cwd() });
1548
+ const bundledSkills = skillLoader.loadBundledSkills();
1549
+
1550
+ let skillsAdded = 0;
1551
+ for (const s of bundledSkills) {
1552
+ db.query(`
1553
+ INSERT OR IGNORE INTO skills (
1554
+ id, name, description, version, author, icon, category,
1555
+ permissions, dependencies, tools, triggers, preferred_agents,
1556
+ body, version_num, active, created_at, updated_at
1557
+ )
1558
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, (unixepoch()), (unixepoch()))
1559
+ `).run(
1560
+ s.name, s.name, s.description || "", String(s.version || "1.0.0"),
1561
+ s.author || "Hive", s.icon || "🧩", s.category || "general",
1562
+ JSON.stringify(s.permissions || []), JSON.stringify(s.dependencies || []),
1563
+ (s.tools || []).join(","), (s.triggers || []).join(","), "[]",
1564
+ s.content || "", 100
1565
+ );
1566
+ skillsAdded++;
1567
+ }
1568
+ log.info(`[migration v0.0.31] ✅ ${skillsAdded} skills synced from bundle`);
1569
+
1570
+ // Sync skills_fts (FTS5 index)
1571
+ log.info("[migration v0.0.31] Syncing skills_fts index...");
1572
+ db.run("DELETE FROM skills_fts");
1573
+ const ftsInsert = db.prepare("INSERT INTO skills_fts(id, name, description, category, tools, triggers, body) VALUES(?, ?, ?, ?, ?, ?, ?)");
1574
+ const activeSkills = db.query("SELECT * FROM skills WHERE active = 1").all() as any[];
1575
+ for (const s of activeSkills) {
1576
+ ftsInsert.run(s.id, s.name, s.description || "", s.category || "", s.tools || "", s.triggers || "", s.body || "");
1577
+ }
1578
+ log.info(`[migration v0.0.31] ✅ ${activeSkills.length} skills indexed in FTS5`);
1579
+
1580
+ markApplied("v0.0.31");
1581
+ log.info("✅ Migration v0.0.31: Reduced system_prompt + skills sync");
1582
+ }
1583
+
1584
+ // v0.0.32 — add vision/multimodal columns to channels table
1585
+ if (!applied("v0.0.32")) {
1586
+ const db = getDb();
1587
+ log.info("[migration v0.0.32] Adding vision columns to channels table...");
1588
+
1589
+ const addCol = (col: string, def: string) => {
1590
+ try { db.run(`ALTER TABLE channels ADD COLUMN ${col} ${def}`); } catch { /* already exists */ }
1591
+ };
1592
+ addCol("vision_enabled", "INTEGER NOT NULL DEFAULT 0");
1593
+ addCol("ocr_provider", "TEXT");
1594
+ addCol("vision_provider", "TEXT");
1595
+ addCol("vision_model_id", "TEXT");
1596
+
1597
+ markApplied("v0.0.32");
1598
+ log.info("✅ Migration v0.0.32: vision columns added to channels");
1599
+ }
1600
+ } catch (e) {
1601
+ log.error("⚠️ runStartupMigrations failed:", { error: (e as Error).message });
1602
+ }
1603
+ }