@johpaz/hive-sdk 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +78 -23
  3. package/bunfig.toml +4 -2
  4. package/docs/API-AGENTS.md +78 -27
  5. package/docs/API-CONTEXT-COMPILER.md +31 -34
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  7. package/docs/HIVE-HARNESS.md +1 -1
  8. package/docs/INDEX.md +4 -4
  9. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  10. package/package.json +9 -4
  11. package/packages/cli/package.json +2 -2
  12. package/packages/cli/src/commands/create-app.test.ts +36 -7
  13. package/packages/cli/src/commands/init.ts +3 -3
  14. package/packages/cli/src/commands/run.ts +1 -1
  15. package/packages/cli/src/commands/test.ts +37 -25
  16. package/packages/cli/src/commands/trace.ts +30 -28
  17. package/packages/cli/templates/hive-app/.env.example +10 -2
  18. package/packages/cli/templates/hive-app/README.md +103 -0
  19. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  20. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  21. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  22. package/packages/core/package.json +5 -4
  23. package/packages/core/src/agent/acceptance-checks.ts +166 -0
  24. package/packages/core/src/agent/agent-catalog.ts +348 -0
  25. package/packages/core/src/agent/agent-loop.ts +1373 -0
  26. package/packages/core/src/agent/capability-search.ts +186 -0
  27. package/packages/core/src/agent/catalog-selector.ts +103 -0
  28. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  29. package/packages/core/src/agent/context-compiler.ts +689 -0
  30. package/packages/core/src/agent/conversation-store.ts +381 -0
  31. package/packages/core/src/agent/curator.ts +276 -0
  32. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  33. package/packages/core/src/agent/goal-runner.ts +323 -0
  34. package/packages/core/src/agent/index.ts +17 -12
  35. package/packages/core/src/agent/llm-client.ts +266 -0
  36. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  37. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  38. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  39. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  40. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  41. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  42. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  43. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  44. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  45. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  47. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  48. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  49. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  50. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  51. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  52. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  54. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  55. package/packages/core/src/agent/playbook-selector.ts +119 -0
  56. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  57. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  58. package/packages/core/src/agent/providers/index.ts +35 -16
  59. package/packages/core/src/agent/reflector.ts +320 -0
  60. package/packages/core/src/agent/routing-intent.ts +22 -0
  61. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  62. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  63. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  64. package/packages/core/src/agent/skill-selector.ts +374 -0
  65. package/packages/core/src/agent/stuck-loop.ts +209 -0
  66. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  67. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  68. package/packages/core/src/api/createAgent.test.ts +139 -27
  69. package/packages/core/src/api/createAgent.ts +232 -44
  70. package/packages/core/src/artifacts/store.ts +162 -0
  71. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  72. package/packages/core/src/canvas/canvas.test.ts +8 -4
  73. package/packages/core/src/canvas/emitter.ts +131 -80
  74. package/packages/core/src/canvas/index.ts +1 -3
  75. package/packages/core/src/channels/base.ts +9 -1
  76. package/packages/core/src/channels/discord.ts +5 -4
  77. package/packages/core/src/channels/manager.ts +122 -30
  78. package/packages/core/src/channels/slack.ts +5 -4
  79. package/packages/core/src/channels/telegram.ts +36 -6
  80. package/packages/core/src/channels/webchat.ts +11 -10
  81. package/packages/core/src/channels/whatsapp.ts +23 -7
  82. package/packages/core/src/config/index.ts +13 -2
  83. package/packages/core/src/config/loader.ts +71 -29
  84. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  85. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  86. package/packages/core/src/events/agent-bus.ts +44 -68
  87. package/packages/core/src/events/channel-narration.ts +150 -0
  88. package/packages/core/src/events/narration.ts +82 -0
  89. package/packages/core/src/events/tool-narration.ts +62 -0
  90. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  91. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  92. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  93. package/packages/core/src/gateway/lane-queue.ts +173 -0
  94. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  95. package/packages/core/src/gateway/server.ts +1 -1
  96. package/packages/core/src/harness/index.ts +46 -27
  97. package/packages/core/src/index.ts +33 -20
  98. package/packages/core/src/mcp/hot-reload.ts +32 -23
  99. package/packages/core/src/mcp/index.ts +6 -3
  100. package/packages/core/src/mcp/singleton.ts +1 -4
  101. package/packages/core/src/mcp/tool-sync.ts +138 -0
  102. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  103. package/packages/core/src/memory/Scratchpad.ts +27 -34
  104. package/packages/core/src/multimodal/vision-service.ts +44 -38
  105. package/packages/core/src/resilience/retry.ts +95 -0
  106. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  107. package/packages/core/src/scheduler/index.ts +9 -7
  108. package/packages/core/src/scheduler/integration.ts +46 -26
  109. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  110. package/packages/core/src/scheduler/types.ts +7 -2
  111. package/packages/core/src/security/Pairing.ts +1 -1
  112. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  115. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  116. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  117. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  118. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  119. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  120. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  121. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  122. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  123. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  124. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  125. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  128. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  129. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  130. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  131. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  132. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  133. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  134. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  135. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  136. package/packages/core/src/skills/skills.test.ts +52 -11
  137. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  138. package/packages/core/src/storage/bootstrap.ts +151 -0
  139. package/packages/core/src/storage/causal-events.ts +84 -0
  140. package/packages/core/src/storage/collections.ts +680 -0
  141. package/packages/core/src/storage/crypto.ts +205 -74
  142. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  143. package/packages/core/src/storage/hivedb.ts +61 -0
  144. package/packages/core/src/storage/index.ts +111 -17
  145. package/packages/core/src/storage/model-id.ts +53 -0
  146. package/packages/core/src/storage/onboarding.ts +540 -972
  147. package/packages/core/src/storage/reconcile.ts +238 -0
  148. package/packages/core/src/storage/seed.ts +572 -406
  149. package/packages/core/src/storage/usage.ts +285 -225
  150. package/packages/core/src/storage/user-email.ts +11 -0
  151. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  152. package/packages/core/src/swarm/EventBridge.ts +1 -1
  153. package/packages/core/src/swarm/index.ts +12 -9
  154. package/packages/core/src/tool-runtime/index.ts +146 -23
  155. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  156. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  157. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  158. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  159. package/packages/core/src/tools/agents/index.ts +784 -292
  160. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  161. package/packages/core/src/tools/api/api-request.ts +174 -0
  162. package/packages/core/src/tools/api/index.ts +16 -0
  163. package/packages/core/src/tools/cli/index.ts +4 -0
  164. package/packages/core/src/tools/core/index.ts +281 -112
  165. package/packages/core/src/tools/cron/index.ts +121 -124
  166. package/packages/core/src/tools/index.ts +63 -78
  167. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  168. package/packages/core/src/tools/types.ts +3 -1
  169. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  170. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  171. package/packages/core/src/tools/web/browser-service.ts +5 -0
  172. package/packages/core/src/tools/web/browser-type.ts +3 -8
  173. package/packages/core/src/tools/web/index.ts +4 -4
  174. package/packages/core/src/voice/index.ts +89 -63
  175. package/packages/core/src/workers/agent.worker.ts +2 -2
  176. package/packages/core/src/workers/workers.test.ts +3 -10
  177. package/scripts/bump-version.ts +248 -0
  178. package/scripts/generate-skill-bundle.ts +108 -0
  179. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  180. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  181. package/test/causal-events.test.ts +117 -0
  182. package/test/compaction.test.ts +105 -0
  183. package/test/context-compiler.test.ts +269 -0
  184. package/test/curator.test.ts +130 -0
  185. package/test/durable-queue.test.ts +114 -0
  186. package/test/harness-barrel.test.ts +64 -0
  187. package/test/hive-helpers.test.ts +130 -0
  188. package/test/hivedb-search.test.ts +189 -0
  189. package/test/internal-turns.test.ts +166 -0
  190. package/test/job-idempotency.test.ts +68 -0
  191. package/test/job-retry-backoff.test.ts +184 -0
  192. package/test/job-store.test.ts +381 -0
  193. package/test/llm-retry.test.ts +97 -0
  194. package/test/memory-perf.test.ts +774 -0
  195. package/test/minimal-loadout.test.ts +78 -0
  196. package/test/model-catalog.test.ts +105 -0
  197. package/test/preload.ts +12 -0
  198. package/test/reflector.test.ts +320 -0
  199. package/test/retention-cap.test.ts +91 -0
  200. package/test/retired-capabilities-pruned.test.ts +192 -0
  201. package/test/run-store.test.ts +355 -0
  202. package/test/scratchpad.test.ts +74 -0
  203. package/test/secrets-durability.test.ts +119 -0
  204. package/test/seed-model-reseed.test.ts +155 -0
  205. package/test/setup-agent-seed.test.ts +264 -0
  206. package/test/tool-inventory.test.ts +65 -0
  207. package/test/tool-runtime.test.ts +258 -0
  208. package/test/toon.test.ts +429 -0
  209. package/tsconfig.json +2 -0
  210. package/packages/core/src/ace/Curator.ts +0 -158
  211. package/packages/core/src/ace/Reflector.ts +0 -200
  212. package/packages/core/src/ace/index.ts +0 -4
  213. package/packages/core/src/agent/AgentRunner.ts +0 -711
  214. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  215. package/packages/core/src/agent/ContextGuard.ts +0 -91
  216. package/packages/core/src/agent/ConversationStore.ts +0 -254
  217. package/packages/core/src/agent/Hooks.ts +0 -166
  218. package/packages/core/src/agent/StuckLoop.ts +0 -133
  219. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  220. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  221. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  222. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  223. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  224. package/packages/core/src/agent/selectors/index.ts +0 -6
  225. package/packages/core/src/auth/auth.ts +0 -121
  226. package/packages/core/src/auth/index.ts +0 -1
  227. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  228. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  229. package/packages/core/src/harness/collections.ts +0 -98
  230. package/packages/core/src/harness/goal-verifier.ts +0 -141
  231. package/packages/core/src/harness/harness.test.ts +0 -236
  232. package/packages/core/src/harness/reconcile.ts +0 -149
  233. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  234. package/packages/core/src/multimodal/VisionService.ts +0 -293
  235. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  236. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  237. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  238. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  239. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  240. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  241. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  242. package/packages/core/src/scheduler/dag/index.ts +0 -26
  243. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  244. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  245. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  246. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  247. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  248. package/packages/core/src/storage/hiveSeed.ts +0 -308
  249. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  250. package/packages/core/src/storage/schema.ts +0 -689
  251. package/packages/core/src/storage/storage.test.ts +0 -37
  252. package/packages/core/src/swarm/AgentBus.ts +0 -460
  253. package/packages/core/src/swarm/EventBus.ts +0 -169
  254. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  255. package/packages/core/src/tools/bridge-events.ts +0 -26
  256. package/packages/core/src/tools/canvas/index.ts +0 -375
  257. package/packages/core/src/tools/codebridge/index.ts +0 -342
  258. package/packages/core/src/tools/meeting/index.ts +0 -353
  259. package/packages/core/src/tools/projects/index.ts +0 -37
  260. package/packages/core/src/tools/projects/project-create.ts +0 -94
  261. package/packages/core/src/tools/projects/project-done.ts +0 -66
  262. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  263. package/packages/core/src/tools/projects/project-list.ts +0 -96
  264. package/packages/core/src/tools/projects/project-update.ts +0 -72
  265. package/packages/core/src/tools/projects/task-create.ts +0 -68
  266. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  267. package/packages/core/src/tools/projects/task-update.ts +0 -93
  268. package/packages/core/src/tools/voice/index.ts +0 -104
  269. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  270. package/packages/core/src/tools/web/api-request.ts +0 -239
  271. package/test/setup-db.ts +0 -216
  272. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -1,96 +1,186 @@
1
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";
2
+ import { ensureHiveDb } from "./bootstrap";
3
+ import { col, toIndexable, fromIndexable } from "./hive";
4
+ import {
5
+ storeProviderApiKey,
6
+ storeChannelConfig,
7
+ storeMcpEnv,
8
+ loadProviderApiKey,
9
+ loadChannelConfig,
10
+ } from "./crypto";
5
11
  import { SkillLoader } from "../skills/index.ts";
12
+ import type {
13
+ UserDoc, ProviderDoc, ModelDoc, AgentDoc, ChannelDoc, McpServerDoc,
14
+ UserIdentityDoc, OnboardingProgressDoc, EthicsDoc, SkillDoc, ToolDoc,
15
+ } from "./collections";
16
+ import { normalizeUserEmail } from "./user-email";
6
17
 
7
18
  export interface OnboardingSection {
8
- step: "user" | "skills" | "ethics" | "tools" | "provider" | "model" | "channel" | "codebridge" | "mcp" | "agent" | "complete";
19
+ step: "user" | "skills" | "ethics" | "tools" | "provider" | "model" | "channel" | "mcp" | "agent" | "complete";
9
20
  userId: string;
10
21
  data: Record<string, unknown>;
11
22
  completedAt?: number;
12
23
  }
13
24
 
14
25
  const log = logger.child("onboarding");
15
- // 9️⃣ Hive System Prompt
16
26
 
17
27
  const HIVE_SYSTEM_PROMPT = `
18
28
  # HIVE — Agente Coordinador
19
29
 
20
- Sos Bee, coordinador de Hive. Resolvés tareas del usuario directamente o delegando a workers especializados. Tu rol es "coordinator".
30
+ Sos Bee, coordinador de Hive. Sos el único agente que conversa con el usuario, y no trabajás solo: dirigís una colmena de workers especializados que corren en paralelo.
21
31
 
22
- ## REGLAS CRÍTICAS
32
+ **Tu oficio es repartir trabajo, no hacerlo todo vos.** Ante cada pedido buscás primero quién puede resolverlo; solo lo hacés con tus propias manos cuando no hay nadie que lo cubra.
23
33
 
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.
34
+ ## 1. ANTES DE ACTUAR
30
35
 
31
- ## 🔍 DISCOVERY CÓMO ENCONTRAR MÁS CAPACIDADES
36
+ Leé el pedido completo y mirá lo que ya sabés: la sección SCRATCHPAD trae tus notas de esta conversación y \`memory_read\` / \`memory_search\` lo guardado en conversaciones anteriores. No rehagas trabajo ya hecho ni vuelvas a preguntar algo que ya te dijeron.
32
37
 
33
- Arrancás con solo 4 herramientas. Para descubrir más, usá **search_knowledge**:
38
+ **Si es un saludo, una charla o una pregunta que respondés de memoria: respondé y terminá.** Eso no se delega nunca ni necesita herramientas.
34
39
 
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
+ ## 2. DESCOMPONER
40
41
 
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
+ Separá el pedido en partes y clasificá cada una:
42
43
 
43
- **Prioridad:** SIEMPRE preferí herramientas nativas sobre MCP cuando ambas resuelven la tarea.
44
+ | Tipo de parte | Qué hacés |
45
+ |---|---|
46
+ | Independientes entre sí | Van juntas, en paralelo, en este mismo turno |
47
+ | Una necesita el resultado de otra | Va en una fase posterior |
48
+ | Trivial o conversacional | La resolvés vos, sin herramientas |
44
49
 
45
- ## 📋 FLUJO DE TRABAJO
50
+ ## 3. POR CADA PARTE: ¿HAY UN AGENTE QUE LA HAGA?
46
51
 
47
- **Tarea simple (1-2 pasos):** Ejecutala directo con tus tools.
52
+ **Esta es la pregunta central de tu rol, y contestarla es gratis:** el roster está en la sección COLMENA DE AGENTES de este mismo prompt, no hace falta ninguna llamada para consultarlo.
48
53
 
49
- **Tarea repetitiva:** Usá cron.create. Preguntá al usuario cada cuánto ejecutarla.
54
+ 1. **¿Encaja un agente de la colmena?** \`task_delegate\`. Este es el camino por defecto.
55
+ 2. **¿Ninguno encaja?** → \`agent_find\` por si existe un worker propio para esa especialidad.
56
+ 3. **¿Tampoco hay?** → recién ahí \`search_knowledge\` para encontrar las herramientas. Preferí siempre herramientas nativas sobre MCP.
57
+ 4. **Si encontraste una tool nativa** → resolvelo vos directamente.
58
+ 5. **Si al menos una parte requiere MCP**:
59
+ - Agrupá las tools por \`server_id\` y usá \`agent_find\` para buscar un especialista del usuario que ya tenga ese servidor.
60
+ - Si existe y está habilitado, delegale la parte correspondiente. No preguntes ni crees otro.
61
+ - Si no existe, **antes de ejecutar cualquier tool de ese servidor**, preguntale al usuario si quiere crear un agente persistente para esa integración.
62
+ - Si acepta: usá \`get_available_models\`, descubrí \`agent_create\`, creá un worker con \`mcp_server_id\` y delegale la tarea actual. El agente recibe todas las tools actuales y futuras de ese servidor.
63
+ - Si rechaza: ejecutá vos directamente las tools MCP necesarias solo para esta solicitud.
64
+ - Si intervienen varios servidores sin especialista, tratá cada servidor por separado: un agente por servidor, nunca uno combinado.
50
65
 
51
- **Tarea compleja (múltiples workers):** Creá un proyecto con project_create, descomponé en tareas, delegá con delegate_task.
66
+ ### CALENDARIO NO ES CRON
52
67
 
53
- **Worker:** find_agent ¿existe? reutilizalo. Si no create_agent con system_prompt claro y tools_json mínimo. **delegate_task** lo activa.
68
+ - \`schedule_automation_agent\` administra jobs que Hive ejecutará después: tareas recurrentes, reportes automáticos, monitoreos y recordatorios de una sola ejecución.
69
+ - Crear, consultar o modificar eventos, citas o reuniones; invitar asistentes; o revisar disponibilidad pertenece al servidor de calendario y a su especialista MCP.
70
+ - Una frase como “agenda una reunión” significa calendario, no \`cron.create\`. Solo usá cron cuando el usuario quiere que Hive ejecute una instrucción en el futuro.
54
71
 
55
- **Proyectos:** Solo creá proyecto cuando hay múltiples workers coordinando. NO para tareas unitarias.
72
+ Si \`search_knowledge\` no devuelve nada y el pedido es corto o ambiguo, **preguntale al usuario** en vez de adivinar y encadenar más búsquedas. Una pregunta cuesta un turno; adivinar mal cuesta varios.
56
73
 
57
- **Cierre:** task_update(status, result) task_evaluate(criteria) → project_done(summary).
74
+ ## 4. DELEGAR EN PARALELO
58
75
 
59
- ## 🧠 MEMORIA
76
+ Las partes independientes se lanzan **todas en el mismo turno**: una \`task_delegate\` por parte, con \`mode="async"\`. Hive las agrupa por turno y los workers corren simultáneamente.
60
77
 
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
78
+ Si el usuario pide tres cosas que no dependen entre sí, son tres \`task_delegate\` en la misma respuesta no una, esperar, y después la siguiente. **Paralelizar es el caso normal, no la excepción.**
64
79
 
65
- ## 📡 CANALES
80
+ Cada delegación lleva: \`worker_id\`, una subtarea acotada, contexto mínimo y \`acceptance\` verificable. Antes de delegar, si el worker va a necesitar herramientas puntuales, buscalas con \`search_knowledge\` e incluilas en la instrucción. Reservá \`mode="sync"\` solo para un lookup cuyo resultado esperás en segundos.
66
81
 
67
- webchat (siempre activo) · telegram · discord · slack · whatsapp
68
- Canal preferido para cron: telegram > discord > webchat
82
+ Si más adelante una entrega no cumple sus criterios, \`task_revise\` reencola al mismo worker sobre el mismo hilo (ver sección 6) no crees una delegación nueva para corregir algo ya delegado.
83
+
84
+ ## 5. ESPERAR: NO ESPERÁS
85
+
86
+ Después de delegar, contale al usuario en una línea qué pusiste a correr y **terminá tu turno**.
87
+
88
+ Cuando todas las tareas del turno alcanzan estado terminal, Hive te reinvoca automáticamente con un mensaje \`[Sistema]\` que trae el resultado de cada una.
89
+
90
+ - **No hagas polling** con \`task_status\` en loop. Usalo solo si el usuario pide el estado antes de tiempo.
91
+ - No anuncies resultados que todavía no tenés ni declares éxito antes del \`[Sistema]\`.
92
+ - No re-delegues una tarea porque "no contestó": ya está encolada.
93
+
94
+ ## 6. CERRAR
95
+
96
+ Al recibir el \`[Sistema]\`, cada entrega trae sus \`acceptance\` (criterios) y sus \`checks\` (resultado determinístico, sin LLM, ya calculado):
97
+
98
+ - \`checks.status="passed"\` → un check automático ya lo confirmó. Aceptalo.
99
+ - \`checks.status="failed"\` (implica \`ok=false\`) → no cumplió. Nunca lo reportes como éxito.
100
+ - \`checks.status="unchecked"\` o ausente → no hay check automático para ese criterio: **vos sos quien juzga**, con el contenido y la evidencia que trae la entrega.
101
+
102
+ Si una entrega no cumple sus criterios: usá \`task_revise\` con el \`task_id\` y un feedback concreto y accionable — el worker retoma con su contexto, no hace falta repetirle todo el pedido. Si el problema es trivial y tenés las tools, corregilo vos directamente en vez de re-delegar. No inventes trabajo ni evidencia.
103
+
104
+ Cuando todo lo delegado en esta ronda cumple, escribí **una sola** respuesta final integrando todo. Las entradas con \`ok=false\` se reportan con su motivo real, nunca como éxito.
105
+
106
+ Guardá lo que vaya a servir después: \`save_note\` para esta conversación, \`memory_write\` para lo que deba sobrevivir a ella. Confirmá con el usuario antes de persistir datos suyos.
107
+
108
+ ## REGLAS PERMANENTES
109
+
110
+ 1. **Ética primero** — Operás bajo un Código de Ética obligatorio que no podés ignorar.
111
+ 2. **Verdad de ejecución** — \`TaskDoc\`/\`JobDoc\` son la fuente de verdad. \`agent_find\` solo descubre workers; nunca prueba si algo está corriendo: para eso están \`task_list\` y \`task_status\`. Si \`task_delegate\` devuelve \`ok=true\` con \`task_id\`, \`job_id\` y \`run_id\`, la tarea se persistió de verdad y no es una simulación. Si una herramienta falla, reportá su resultado exacto: no inventes IDs, estados ni ejecuciones.
112
+ 3. **Vos aceptás las entregas** — cada entrega vuelve con sus criterios, su evidencia y el resultado de los checks determinísticos (ver sección 6). Si cumple, la integrás; si no, \`task_revise\` con feedback concreto, o la corregís vos si es trivial. Si un worker devuelve \`needs_input\`, vos formulás la pregunta al usuario con contexto.
113
+ 4. **Buscá antes de crear** — nunca crees un worker si el catálogo ya cubre la tarea.
114
+ 5. **Mínimo privilegio** — solo las herramientas necesarias a cada worker. La única excepción explícita es un especialista MCP aprobado por el usuario: recibe el servidor completo que figura en \`mcp_server_ids_json\`, nunca otros servidores.
115
+ 6. **Nunca \`cli_exec\` para cron** — usá \`cron.create\`, y preguntá al usuario cada cuánto ejecutar.
116
+ 7. **Calendario ≠ cron** — los eventos y reuniones van al especialista MCP de calendario; cron solo programa futuras ejecuciones de Hive.
117
+
118
+ ## QUÉ HAY EN TU CONTEXTO
119
+
120
+ - **COLMENA DE AGENTES** — los workers disponibles ahora mismo. Consultalo antes de decidir nada.
121
+ - **HERRAMIENTAS SIEMPRE DISPONIBLES** — con las que arrancás cada turno. El resto se descubre con \`search_knowledge\` y queda usable de inmediato.
122
+ - **SCRATCHPAD** — tus notas de esta conversación; sobreviven a la compresión del historial.
123
+ - **PLAYBOOK APRENDIDO** — reglas aprendidas de turnos anteriores, ya filtradas por relevancia. Aplicalas.
124
+ - **SKILLS DESCUBIERTAS** — nombres de skills que el sistema considera relevantes para este pedido. Son una pista, no instrucciones: su contenido llega cuando descubrís sus herramientas con \`search_knowledge\`.
125
+
126
+ ## CANALES
127
+
128
+ webchat (siempre activo) · telegram · discord · slack · whatsapp. Preferencia para cron: telegram > discord > webchat.
69
129
  `
70
- export function initOnboardingDb(): void {
71
- try {
72
- initializeDatabase();
73
130
 
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 };
131
+ /**
132
+ * First line of every stock coordinator prompt ever shipped. Used to tell a
133
+ * stock prompt (safe to upgrade in place) from one the user rewrote through
134
+ * the agents API, which must never be clobbered.
135
+ */
136
+ const HIVE_PROMPT_STOCK_HEADER = "# HIVE — Agente Coordinador";
77
137
 
78
- if (userCount.count > 0) {
79
- log.info("✅ DB ya inicializada con " + userCount.count + " usuario(s). Saltando seed.");
80
- return;
81
- }
138
+ /**
139
+ * Upgrades coordinators still carrying an older stock prompt.
140
+ *
141
+ * HIVE_SYSTEM_PROMPT is only written during onboarding/setup, so an install
142
+ * created before a prompt change keeps the old text in `agents.system_prompt`
143
+ * forever — new coordinator behavior would never reach existing users. This
144
+ * runs on every boot and rewrites only rows whose prompt is verbatim stock
145
+ * (starts with the stock header and differs from the current text). A prompt
146
+ * the user customized doesn't match and is left untouched.
147
+ */
148
+ export async function refreshCoordinatorPrompts(): Promise<number> {
149
+ const agentsCol = await col<AgentDoc>("agents");
150
+ let updated = 0;
151
+ for (const entry of await agentsCol.findBy("role", "coordinator")) {
152
+ const current = entry.doc.system_prompt ?? "";
153
+ if (current === HIVE_SYSTEM_PROMPT) continue;
154
+ if (!current.trimStart().startsWith(HIVE_PROMPT_STOCK_HEADER)) continue; // user-authored
155
+ await agentsCol.put(entry.id, {
156
+ ...entry.doc,
157
+ system_prompt: HIVE_SYSTEM_PROMPT,
158
+ updated_at: Date.now(),
159
+ }, { expectedVersion: entry.version });
160
+ updated++;
161
+ }
162
+ return updated;
163
+ }
82
164
 
83
- log.info("🌱 Ejecutando seed de datos...");
84
- seedAllData();
85
- log.info("✅ Seed completado correctamente.");
165
+ /** Generates a 32-char lowercase hex id, matching the old `lower(hex(randomblob(16)))` scheme. */
166
+ function genId(): string {
167
+ return crypto.randomUUID().replace(/-/g, "");
168
+ }
169
+
170
+ export async function initOnboardingDb(): Promise<void> {
171
+ try {
172
+ // ensureHiveDb() already reseeds the static catalogs unconditionally on
173
+ // every call — no separate userCount gate/seedAllData() call needed here.
174
+ await ensureHiveDb();
86
175
  } catch (e) {
87
176
  log.error("⚠️ Fallo al inicializar/poblar la DB:", { error: (e as Error).message });
88
177
  }
89
178
  }
90
179
 
91
- export function saveUserProfile(data: {
180
+ export async function saveUserProfile(data: {
92
181
  userId?: string;
93
182
  userName?: string;
183
+ userEmail?: string;
94
184
  userLanguage?: string;
95
185
  userTimezone?: string;
96
186
  userOccupation?: string;
@@ -100,77 +190,82 @@ export function saveUserProfile(data: {
100
190
  agentDescription?: string;
101
191
  agentTone?: string;
102
192
  channelUserId?: string;
103
- }): string {
193
+ }): Promise<string> {
104
194
  try {
105
- const db = getDb();
195
+ const usersCol = await col<UserDoc>("users");
106
196
  let finalUserId = data.userId;
197
+ const normalizedEmail = data.userEmail !== undefined
198
+ ? normalizeUserEmail(data.userEmail)
199
+ : undefined;
107
200
 
108
201
  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;
202
+ finalUserId = genId();
203
+ await usersCol.put(finalUserId, {
204
+ id: finalUserId,
205
+ name: data.userName || null,
206
+ language: data.userLanguage || null,
207
+ timezone: data.userTimezone || null,
208
+ occupation: data.userOccupation || null,
209
+ notes: data.userNotes || null,
210
+ master_key_hash: null,
211
+ email: normalizedEmail ?? null,
212
+ password_hash: null,
213
+ preferred_cron_channel: "auto",
214
+ created_at: Date.now(),
215
+ });
121
216
  log.info("✅ User created with auto-generated ID", { userId: finalUserId });
122
217
  } 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
- );
218
+ const existing = await usersCol.get(finalUserId);
219
+ await usersCol.put(finalUserId, {
220
+ id: finalUserId,
221
+ name: data.userName ?? existing?.doc.name ?? null,
222
+ language: data.userLanguage ?? existing?.doc.language ?? null,
223
+ timezone: data.userTimezone ?? existing?.doc.timezone ?? null,
224
+ occupation: data.userOccupation ?? existing?.doc.occupation ?? null,
225
+ notes: data.userNotes ?? existing?.doc.notes ?? null,
226
+ master_key_hash: existing?.doc.master_key_hash ?? null,
227
+ email: normalizedEmail ?? existing?.doc.email ?? null,
228
+ password_hash: existing?.doc.password_hash ?? null,
229
+ preferred_cron_channel: existing?.doc.preferred_cron_channel ?? "auto",
230
+ created_at: existing?.doc.created_at ?? Date.now(),
231
+ }, existing ? { expectedVersion: existing.version } : undefined);
141
232
  }
142
233
 
143
234
  // 2️⃣ Crear identidad base para webchat (sesión única)
144
235
  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);
236
+ const identitiesCol = await col<UserIdentityDoc>("userIdentities");
237
+ await identitiesCol.put(`${finalUserId}:webchat`, {
238
+ user_id: finalUserId, channel: "webchat", channel_user_id: data.channelUserId, linked_at: Date.now(),
239
+ });
149
240
  log.info("✅ User identity created for webchat", { userId: finalUserId });
150
241
  }
151
242
 
152
243
  // 3️⃣ Crear o actualizar agente
153
244
  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
- );
245
+ const agentsCol = await col<AgentDoc>("agents");
246
+ const existingAgent = await agentsCol.get(data.agentId);
247
+ const now = Date.now();
248
+ await agentsCol.put(data.agentId, {
249
+ id: data.agentId,
250
+ user_id: finalUserId,
251
+ name: data.agentName,
252
+ description: data.agentDescription ?? existingAgent?.doc.description ?? null,
253
+ system_prompt: HIVE_SYSTEM_PROMPT,
254
+ tone: data.agentTone ?? existingAgent?.doc.tone ?? null,
255
+ role: "coordinator",
256
+ status: existingAgent?.doc.status ?? "idle",
257
+ enabled: existingAgent?.doc.enabled ?? true,
258
+ provider_id: existingAgent?.doc.provider_id ?? toIndexable(null),
259
+ model_id: existingAgent?.doc.model_id ?? toIndexable(null),
260
+ tools_json: existingAgent?.doc.tools_json ?? null,
261
+ skills_json: existingAgent?.doc.skills_json ?? null,
262
+ parent_id: existingAgent?.doc.parent_id ?? toIndexable(null),
263
+ max_iterations: existingAgent?.doc.max_iterations ?? 10,
264
+ workspace: existingAgent?.doc.workspace ?? null,
265
+ lastTraceAt: existingAgent?.doc.lastTraceAt ?? null,
266
+ created_at: existingAgent?.doc.created_at ?? now,
267
+ updated_at: now,
268
+ }, existingAgent ? { expectedVersion: existingAgent.version } : undefined);
174
269
  }
175
270
 
176
271
  return finalUserId;
@@ -180,12 +275,12 @@ user_id = COALESCE(excluded.user_id, user_id),
180
275
  }
181
276
  }
182
277
 
183
- export function activateSkills(userId: string, skillIds: string[]): void {
278
+ export async function activateSkills(userId: string, skillIds: string[]): Promise<void> {
184
279
  try {
185
- const db = getDb();
186
- // Activar skills seleccionadas
280
+ const skillsCol = await col<SkillDoc>("skills");
187
281
  for (const skillId of skillIds) {
188
- db.query(`UPDATE skills SET active = 1 WHERE id = ? `).run(skillId);
282
+ const existing = await skillsCol.get(skillId);
283
+ if (existing) await skillsCol.put(skillId, { ...existing.doc, active: true }, { expectedVersion: existing.version });
189
284
  }
190
285
  log.info("✅ Skills activadas:", { skillIds: skillIds.join(", ") });
191
286
  } catch (e) {
@@ -193,25 +288,28 @@ export function activateSkills(userId: string, skillIds: string[]): void {
193
288
  }
194
289
  }
195
290
 
196
- export function activateEthics(userId: string, ethicsId: string): void {
291
+ export async function activateEthics(userId: string, ethicsId: string): Promise<void> {
197
292
  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);
293
+ const ethicsCol = await col<EthicsDoc>("ethics");
294
+ const all = await ethicsCol.scan({});
295
+ for (const e of all) {
296
+ const shouldBeActive = e.id === ethicsId;
297
+ if (e.doc.active !== shouldBeActive) {
298
+ await ethicsCol.put(e.id, { ...e.doc, active: shouldBeActive }, { expectedVersion: e.version });
299
+ }
300
+ }
203
301
  log.info("✅ Ethics activado:", { ethicsId });
204
302
  } catch (e) {
205
303
  log.error("⚠️ Error activating ethics:", { error: (e as Error).message });
206
304
  }
207
305
  }
208
306
 
209
- export function activateTools(userId: string, toolIds: string[]): void {
307
+ export async function activateTools(userId: string, toolIds: string[]): Promise<void> {
210
308
  try {
211
- const db = getDb();
212
- // Activar tools seleccionadas
309
+ const toolsCol = await col<ToolDoc>("tools");
213
310
  for (const toolId of toolIds) {
214
- db.query(`UPDATE tools SET active = 1, enabled = 1 WHERE id = ? `).run(toolId);
311
+ const existing = await toolsCol.get(toolId);
312
+ if (existing) await toolsCol.put(toolId, { ...existing.doc, active: true, enabled: true }, { expectedVersion: existing.version });
215
313
  }
216
314
  log.info("✅ Tools activadas:", { toolIds: toolIds.join(", ") });
217
315
  } catch (e) {
@@ -223,9 +321,9 @@ export function activateTools(userId: string, toolIds: string[]): void {
223
321
  * Activate all browser tools when Chromium is available
224
322
  * Called from gateway initializer when browser service connects successfully
225
323
  */
226
- export function activateBrowserTools(): void {
324
+ export async function activateBrowserTools(): Promise<void> {
227
325
  try {
228
- const db = getDb();
326
+ const toolsCol = await col<ToolDoc>("tools");
229
327
  const browserToolIds = [
230
328
  "browser_navigate",
231
329
  "browser_screenshot",
@@ -237,7 +335,8 @@ export function activateBrowserTools(): void {
237
335
  ];
238
336
 
239
337
  for (const toolId of browserToolIds) {
240
- db.query(`UPDATE tools SET active = 1, enabled = 1 WHERE id = ? `).run(toolId);
338
+ const existing = await toolsCol.get(toolId);
339
+ if (existing) await toolsCol.put(toolId, { ...existing.doc, active: true, enabled: true }, { expectedVersion: existing.version });
241
340
  }
242
341
  log.info("✅ Browser tools activated (Chromium available)");
243
342
  } catch (e) {
@@ -253,43 +352,40 @@ export async function saveProviderConfig(data: {
253
352
  baseUrl?: string;
254
353
  }): Promise<void> {
255
354
  try {
256
- const db = getDb();
355
+ const providersCol = await col<ProviderDoc>("providers");
356
+ const modelsCol = await col<ModelDoc>("models");
257
357
 
258
- let apiKeyEncrypted = null;
259
- let apiKeyIv = null;
358
+ // 1️⃣ Primero: Actualizar provider global con API key del usuario
359
+ const existingProvider = await providersCol.get(data.provider);
360
+ if (existingProvider) {
361
+ await providersCol.put(data.provider, {
362
+ ...existingProvider.doc, base_url: data.baseUrl || null, enabled: true, active: true,
363
+ }, { expectedVersion: existingProvider.version });
364
+ }
260
365
 
261
366
  if (data.apiKey) {
262
- const encrypted = await encryptApiKey(data.apiKey);
263
- apiKeyEncrypted = encrypted.encrypted;
264
- apiKeyIv = encrypted.iv;
367
+ await storeProviderApiKey(data.provider, data.apiKey);
265
368
  }
266
369
 
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
370
  log.info("✅ Provider actualizado:", { provider: data.provider });
279
371
 
280
372
  // 2️⃣ Segundo: Activar el modelo seleccionado
281
373
  // For Ollama, models are inserted dynamically (not seeded), ensure row exists first
282
374
  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);
375
+ const existingModel = await modelsCol.get(data.model);
376
+ if (!existingModel) {
377
+ await modelsCol.put(data.model, {
378
+ id: data.model, provider_id: "ollama", name: data.model, model_type: "llm",
379
+ context_window: 0, capabilities: null, enabled: true, active: true,
380
+ source: "discovered",
381
+ });
382
+ }
287
383
  }
288
384
 
289
- db.query(`
290
- UPDATE models SET enabled = 1, active = 1
291
- WHERE id = ?
292
- `).run(data.model);
385
+ const modelToActivate = await modelsCol.get(data.model);
386
+ if (modelToActivate) {
387
+ await modelsCol.put(data.model, { ...modelToActivate.doc, enabled: true, active: true }, { expectedVersion: modelToActivate.version });
388
+ }
293
389
 
294
390
  log.info("✅ Model activado:", { model: data.model });
295
391
  } catch (e) {
@@ -298,28 +394,12 @@ VALUES(?, ?, 'ollama', 'llm', 1, 1)
298
394
  }
299
395
  }
300
396
 
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 {
397
+ export async function activateMcpServers(userId: string, mcpIds: string[]): Promise<void> {
318
398
  try {
319
- const db = getDb();
320
- // Activar MCP servers seleccionados
399
+ const mcpCol = await col<McpServerDoc>("mcpServers");
321
400
  for (const mcpId of mcpIds) {
322
- db.query(`UPDATE mcp_servers SET active = 1, enabled = 1 WHERE id = ? `).run(mcpId);
401
+ const existing = await mcpCol.get(mcpId);
402
+ if (existing) await mcpCol.put(mcpId, { ...existing.doc, active: true, enabled: true }, { expectedVersion: existing.version });
323
403
  }
324
404
  log.info("✅ MCP servers activados:", { mcpIds: mcpIds.join(", ") });
325
405
  } catch (e) {
@@ -327,8 +407,31 @@ export function activateMcpServers(userId: string, mcpIds: string[]): void {
327
407
  }
328
408
  }
329
409
 
410
+ /**
411
+ * Seeds every non-coordinator agent with the provider/model the user just
412
+ * configured for the coordinator. Onboarding is the one moment where
413
+ * overwriting is right: the user is (re)choosing the main model, so the whole
414
+ * hive follows it. The boot-time counterpart (`ensureAgentsConfigured`) only
415
+ * fills the blanks.
416
+ *
417
+ * Note: an agent with `model_override_json` (the acceptance verifier asks for
418
+ * a different model family) stops consulting that override once its row has
419
+ * an explicit pair — `resolveAgentModel()` returns the row first.
420
+ */
421
+ export async function propagateCoordinatorModel(
422
+ userId: string,
423
+ providerId: string,
424
+ modelId: string,
425
+ ): Promise<number> {
426
+ const { applyCoordinatorModel } = await import("../agent/agent-catalog");
427
+ const updated = await applyCoordinatorModel({ userId, providerId, modelId, overwrite: true });
428
+ if (updated > 0) {
429
+ log.info(`✅ ${updated} agente(s) sincronizados con el modelo del coordinador`, { providerId, modelId });
430
+ }
431
+ return updated;
432
+ }
330
433
 
331
- export function saveAgentConfig(data: {
434
+ export async function saveAgentConfig(data: {
332
435
  userId: string;
333
436
  agentId?: string;
334
437
  agentName: string;
@@ -336,63 +439,65 @@ export function saveAgentConfig(data: {
336
439
  modelId: string;
337
440
  tone: string;
338
441
  description?: string;
339
- }): string {
442
+ }): Promise<string> {
340
443
  try {
341
- const db = getDb();
342
- let finalAgentId = data.agentId;
444
+ const providersCol = await col<ProviderDoc>("providers");
445
+ const modelsCol = await col<ModelDoc>("models");
446
+ const agentsCol = await col<AgentDoc>("agents");
343
447
 
344
448
  // Validate FK references — use null if the referenced row doesn't exist
345
449
  // (e.g. custom Ollama model IDs are not in the seed models table)
346
450
  const rawProviderId = data.providerId || null;
347
451
  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;
452
+ const safeProviderId = rawProviderId && (await providersCol.get(rawProviderId)) ? rawProviderId : null;
453
+ const safeModelId = rawModelId && (await modelsCol.get(rawModelId)) ? rawModelId : null;
454
+
455
+ let finalAgentId = data.agentId;
456
+ const now = Date.now();
350
457
 
351
- // Si no se pasa agentId, dejar que SQLite lo genere automáticamente
352
458
  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;
459
+ finalAgentId = genId();
460
+ await agentsCol.put(finalAgentId, {
461
+ id: finalAgentId, user_id: data.userId, name: data.agentName,
462
+ description: data.description || null, system_prompt: HIVE_SYSTEM_PROMPT,
463
+ tone: data.tone, role: "coordinator", status: "idle", enabled: true,
464
+ provider_id: toIndexable(safeProviderId), model_id: toIndexable(safeModelId),
465
+ tools_json: null, skills_json: null, parent_id: toIndexable(null),
466
+ max_iterations: 10, workspace: null, lastTraceAt: null,
467
+ created_at: now, updated_at: now,
468
+ });
368
469
  log.info("✅ Agent created with auto-generated ID", { agentId: finalAgentId });
369
470
  } 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
- );
471
+ const existing = await agentsCol.get(finalAgentId);
472
+ await agentsCol.put(finalAgentId, {
473
+ id: finalAgentId,
474
+ user_id: data.userId ?? existing?.doc.user_id,
475
+ name: data.agentName ?? existing?.doc.name,
476
+ description: data.description ?? existing?.doc.description ?? null,
477
+ system_prompt: HIVE_SYSTEM_PROMPT,
478
+ tone: data.tone ?? existing?.doc.tone,
479
+ role: "coordinator",
480
+ status: "idle",
481
+ enabled: true,
482
+ provider_id: toIndexable(safeProviderId) !== "__none__" ? toIndexable(safeProviderId) : (existing?.doc.provider_id ?? toIndexable(null)),
483
+ model_id: toIndexable(safeModelId) !== "__none__" ? toIndexable(safeModelId) : (existing?.doc.model_id ?? toIndexable(null)),
484
+ tools_json: existing?.doc.tools_json ?? null,
485
+ skills_json: existing?.doc.skills_json ?? null,
486
+ parent_id: existing?.doc.parent_id ?? toIndexable(null),
487
+ max_iterations: existing?.doc.max_iterations ?? 10,
488
+ workspace: existing?.doc.workspace ?? null,
489
+ lastTraceAt: existing?.doc.lastTraceAt ?? null,
490
+ created_at: existing?.doc.created_at ?? now,
491
+ updated_at: now,
492
+ }, existing ? { expectedVersion: existing.version } : undefined);
493
+ }
494
+
495
+ // Seed the rest of the hive (catalog personas + any worker) with the
496
+ // coordinator's pair. Skipped while the pair is still incomplete — the CLI
497
+ // onboarding creates the coordinator first and only picks provider/model a
498
+ // few steps later, and that later call re-enters here with both set.
499
+ if (safeProviderId && safeModelId) {
500
+ await propagateCoordinatorModel(data.userId, safeProviderId, safeModelId);
396
501
  }
397
502
 
398
503
  return finalAgentId;
@@ -408,31 +513,25 @@ export async function activateChannel(userId: string, data: {
408
513
  config?: Record<string, unknown>;
409
514
  }): Promise<void> {
410
515
  try {
411
- const db = getDb();
516
+ const channelsCol = await col<ChannelDoc>("channels");
517
+ const existing = await channelsCol.get(data.channelId);
518
+ if (existing) {
519
+ await channelsCol.put(data.channelId, {
520
+ ...existing.doc, user_id: userId, active: true, enabled: true, status: "connected",
521
+ }, { expectedVersion: existing.version });
522
+ }
412
523
 
413
524
  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);
525
+ await storeChannelConfig(data.channelId, data.config);
427
526
  }
428
527
 
429
528
  // Create user_identity for the channel if channelUserId provided
430
529
  if (data.channelUserId) {
431
530
  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);
531
+ const identitiesCol = await col<UserIdentityDoc>("userIdentities");
532
+ await identitiesCol.put(`${userId}:${channelType}`, {
533
+ user_id: userId, channel: channelType, channel_user_id: data.channelUserId, linked_at: Date.now(),
534
+ });
436
535
  log.info("✅ User identity created", { userId, channel: channelType });
437
536
  }
438
537
 
@@ -452,66 +551,52 @@ export async function saveVoiceConfig(data: {
452
551
  ttsApiKey?: string;
453
552
  }): Promise<void> {
454
553
  try {
455
- const db = getDb();
554
+ const modelsCol = await col<ModelDoc>("models");
555
+ const providersCol = await col<ProviderDoc>("providers");
556
+ const channelsCol = await col<ChannelDoc>("channels");
456
557
 
457
558
  // 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";
559
+ for (const modelId of [data.sttProvider, data.ttsProvider]) {
560
+ const existing = await modelsCol.get(modelId);
561
+ if (existing) await modelsCol.put(modelId, { ...existing.doc, active: true, enabled: true }, { expectedVersion: existing.version });
469
562
  }
470
563
 
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
- }
564
+ // Resolve provider IDs from the DB: the value can be a model id or a provider id
565
+ const resolveVoiceProviderId = async (id: string): Promise<string> => {
566
+ const model = await modelsCol.get(id);
567
+ if (model?.doc.provider_id) return model.doc.provider_id;
568
+ const provider = await providersCol.get(id);
569
+ return provider?.doc.id || "";
570
+ };
480
571
 
481
- // Save STT API key to provider if provided
572
+ const sttProviderId = await resolveVoiceProviderId(data.sttProvider);
573
+ const ttsProviderId = await resolveVoiceProviderId(data.ttsProvider);
574
+
575
+ // Save STT API key to provider if provided.
576
+ // Note: this deliberately does NOT flip the provider's enabled/active flags —
577
+ // groq/openai/gemini/qwen are shared rows between voice (STT/TTS) and LLM chat,
578
+ // and voice's own "configured" check only looks at whether a key is stored, so
579
+ // touching those flags here would silently surface a voice-only key as a fully
580
+ // active LLM chat provider.
482
581
  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 });
582
+ await storeProviderApiKey(sttProviderId, data.sttApiKey);
583
+ log.info("✅ STT API key guardada en keychain", { provider: sttProviderId });
493
584
  }
494
585
 
495
- // Save TTS API key to provider if provided
586
+ // Save TTS API key to provider if provided (see note above).
496
587
  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 });
588
+ await storeProviderApiKey(ttsProviderId, data.ttsApiKey);
589
+ log.info("✅ TTS API key guardada en keychain", { provider: ttsProviderId });
507
590
  }
508
591
 
509
592
  // 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);
593
+ const existingChannel = await channelsCol.get(data.channelId);
594
+ if (existingChannel) {
595
+ await channelsCol.put(data.channelId, {
596
+ ...existingChannel.doc, user_id: data.userId, voice_enabled: data.voiceEnabled,
597
+ stt_provider: data.sttProvider, tts_provider: data.ttsProvider,
598
+ }, { expectedVersion: existingChannel.version });
599
+ }
515
600
 
516
601
  log.info("✅ Voice config saved:", {
517
602
  channelId: data.channelId,
@@ -537,179 +622,121 @@ export async function saveMcpServer(data: {
537
622
  enabled?: boolean;
538
623
  }): Promise<void> {
539
624
  try {
540
- const db = getDb();
541
-
542
- const mcpId = `${data.userId}:${data.name} `;
543
-
544
- let envEncrypted = null;
545
- let envIv = null;
625
+ const mcpId = `${data.userId}:${data.name}`;
626
+ const mcpCol = await col<McpServerDoc>("mcpServers");
627
+
628
+ await mcpCol.put(mcpId, {
629
+ id: mcpId, user_id: data.userId, name: data.name, transport: data.transport,
630
+ command: data.command || null, args: JSON.stringify(data.args || []),
631
+ url: data.url || null, enabled: !!data.enabled, active: !!data.enabled,
632
+ builtin: false, status: "disconnected", tools_count: 0,
633
+ });
546
634
 
547
635
  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;
636
+ await storeMcpEnv(mcpId, data.env);
551
637
  }
552
638
 
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
639
  log.info("✅ MCP server saved:", { name: data.name });
571
640
  } catch (e) {
572
641
  log.error("⚠️ Error saving MCP server:", { error: (e as Error).message });
573
642
  }
574
643
  }
575
644
 
576
- export function saveToolSelection(userId: string, tools: string[]): void {
645
+ export async function saveToolSelection(userId: string, tools: string[]): Promise<void> {
577
646
  try {
578
- const db = getDb();
579
-
647
+ const toolsCol = await col<ToolDoc>("tools");
580
648
  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);
649
+ const existing = await toolsCol.get(tool);
650
+ if (existing) await toolsCol.put(tool, { ...existing.doc, active: true, enabled: true }, { expectedVersion: existing.version });
586
651
  }
587
-
588
652
  log.info("✅ Tools activadas:", { tools: tools.join(", ") });
589
653
  } catch (e) {
590
654
  log.error("⚠️ Error saving tools:", { error: (e as Error).message });
591
655
  }
592
656
  }
593
657
 
594
- export function activateProvider(providerId: string): void {
658
+ async function setActiveEnabled(collection: string, id: string, value: boolean): Promise<void> {
659
+ const c = await col<{ active: boolean; enabled: boolean }>(collection);
660
+ const existing = await c.get(id);
661
+ if (existing) await c.put(id, { ...existing.doc, active: value, enabled: value }, { expectedVersion: existing.version });
662
+ }
663
+
664
+ export async function activateProvider(providerId: string): Promise<void> {
595
665
  try {
596
- const db = getDb();
597
- db.query(`
598
- UPDATE providers SET active = 1, enabled = 1
599
- WHERE id = ?
600
- `).run(providerId);
666
+ await setActiveEnabled("providers", providerId, true);
601
667
  log.info("✅ Provider activado:", { providerId });
602
668
  } catch (e) {
603
669
  log.error("⚠️ Error activating provider:", { error: (e as Error).message });
604
670
  }
605
671
  }
606
672
 
607
- export function activateModel(modelId: string): void {
673
+ export async function activateModel(modelId: string): Promise<void> {
608
674
  try {
609
- const db = getDb();
610
- db.query(`
611
- UPDATE models SET active = 1, enabled = 1
612
- WHERE id = ?
613
- `).run(modelId);
675
+ await setActiveEnabled("models", modelId, true);
614
676
  log.info("✅ Model activado:", { modelId });
615
677
  } catch (e) {
616
678
  log.error("⚠️ Error activating model:", { error: (e as Error).message });
617
679
  }
618
680
  }
619
681
 
620
-
621
-
622
- export function activateMcpServer(mcpName: string): void {
682
+ export async function activateMcpServer(mcpName: string): Promise<void> {
623
683
  try {
624
- const db = getDb();
625
- db.query(`
626
- UPDATE mcp_servers SET active = 1, enabled = 1
627
- WHERE id = ?
628
- `).run(mcpName);
684
+ await setActiveEnabled("mcpServers", mcpName, true);
629
685
  log.info("✅ MCP server activado:", { mcpName });
630
686
  } catch (e) {
631
687
  log.error("⚠️ Error activating MCP server:", { error: (e as Error).message });
632
688
  }
633
689
  }
634
690
 
635
- export function deactivateProvider(providerId: string): void {
691
+ export async function deactivateProvider(providerId: string): Promise<void> {
636
692
  try {
637
- const db = getDb();
638
- db.query(`
639
- UPDATE providers SET active = 0, enabled = 0
640
- WHERE id = ?
641
- `).run(providerId);
693
+ await setActiveEnabled("providers", providerId, false);
642
694
  log.warn("⚠️ Provider desactivado:", { providerId });
643
695
  } catch (e) {
644
696
  log.error("⚠️ Error deactivating provider:", { error: (e as Error).message });
645
697
  }
646
698
  }
647
699
 
648
- export function deactivateModel(modelId: string): void {
700
+ export async function deactivateModel(modelId: string): Promise<void> {
649
701
  try {
650
- const db = getDb();
651
- db.query(`
652
- UPDATE models SET active = 0, enabled = 0
653
- WHERE id = ?
654
- `).run(modelId);
702
+ await setActiveEnabled("models", modelId, false);
655
703
  log.warn("⚠️ Model desactivado:", { modelId });
656
704
  } catch (e) {
657
705
  log.error("⚠️ Error deactivating model:", { error: (e as Error).message });
658
706
  }
659
707
  }
660
708
 
661
- export function deactivateChannel(channelType: string): void {
709
+ export async function deactivateChannel(channelType: string): Promise<void> {
662
710
  try {
663
- const db = getDb();
664
- db.query(`
665
- UPDATE channels SET active = 0, enabled = 0
666
- WHERE id = ?
667
- `).run(channelType);
711
+ await setActiveEnabled("channels", channelType, false);
668
712
  log.warn("⚠️ Channel desactivado:", { channelType });
669
713
  } catch (e) {
670
714
  log.error("⚠️ Error deactivating channel:", { error: (e as Error).message });
671
715
  }
672
716
  }
673
717
 
674
- export function deactivateMcpServer(mcpName: string): void {
718
+ export async function deactivateMcpServer(mcpName: string): Promise<void> {
675
719
  try {
676
- const db = getDb();
677
- db.query(`
678
- UPDATE mcp_servers SET active = 0, enabled = 0
679
- WHERE id = ?
680
- `).run(mcpName);
720
+ await setActiveEnabled("mcpServers", mcpName, false);
681
721
  log.warn("⚠️ MCP server desactivado:", { mcpName });
682
722
  } catch (e) {
683
723
  log.error("⚠️ Error deactivating MCP server:", { error: (e as Error).message });
684
724
  }
685
725
  }
686
726
 
687
- export function getAllProviders(): Array<{
727
+ export async function getAllProviders(): Promise<Array<{
688
728
  id: string;
689
729
  name: string;
690
730
  baseUrl: string | null;
691
731
  enabled: boolean;
692
732
  active: boolean;
693
- }> {
733
+ }>> {
694
734
  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,
735
+ const providersCol = await col<ProviderDoc>("providers");
736
+ const entries = await providersCol.scan({});
737
+ return entries.map((e) => ({
738
+ id: e.doc.id, name: e.doc.name, baseUrl: e.doc.base_url,
739
+ enabled: e.doc.enabled, active: e.doc.active,
713
740
  }));
714
741
  } catch (e) {
715
742
  log.warn("[onboarding] ⚠️ Error getting providers:", (e as Error).message);
@@ -717,7 +744,7 @@ export function getAllProviders(): Array<{
717
744
  }
718
745
  }
719
746
 
720
- export function getAllModels(): Array<{
747
+ export async function getAllModels(): Promise<Array<{
721
748
  id: string;
722
749
  name: string;
723
750
  providerId: string;
@@ -725,30 +752,14 @@ export function getAllModels(): Array<{
725
752
  capabilities: string | null;
726
753
  enabled: boolean;
727
754
  active: boolean;
728
- }> {
755
+ }>> {
729
756
  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,
757
+ const modelsCol = await col<ModelDoc>("models");
758
+ const entries = await modelsCol.scan({});
759
+ return entries.map((e) => ({
760
+ id: e.doc.id, name: e.doc.name, providerId: e.doc.provider_id,
761
+ contextWindow: e.doc.context_window, capabilities: e.doc.capabilities,
762
+ enabled: e.doc.enabled, active: e.doc.active,
752
763
  }));
753
764
  } catch (e) {
754
765
  log.error("⚠️ Error getting models:", { error: (e as Error).message });
@@ -756,35 +767,20 @@ export function getAllModels(): Array<{
756
767
  }
757
768
  }
758
769
 
759
- export function getAllEthics(): Array<{
770
+ export async function getAllEthics(): Promise<Array<{
760
771
  id: string;
761
772
  name: string;
762
773
  description: string | null;
763
774
  content: string;
764
775
  isDefault: boolean;
765
776
  active: boolean;
766
- }> {
777
+ }>> {
767
778
  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,
779
+ const ethicsCol = await col<EthicsDoc>("ethics");
780
+ const entries = await ethicsCol.scan({});
781
+ return entries.map((e) => ({
782
+ id: e.doc.id, name: e.doc.name, description: e.doc.description, content: e.doc.content,
783
+ isDefault: e.doc.is_default, active: e.doc.active,
788
784
  }));
789
785
  } catch (e) {
790
786
  log.error("⚠️ Error getting ethics:", { error: (e as Error).message });
@@ -792,73 +788,19 @@ export function getAllEthics(): Array<{
792
788
  }
793
789
  }
794
790
 
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<{
791
+ export async function getAllSkills(): Promise<Array<{
832
792
  id: string;
833
793
  name: string;
834
794
  description: string | null;
835
- source: string;
836
- isGlobal: boolean;
837
795
  enabled: boolean;
838
796
  active: boolean;
839
- }> {
797
+ }>> {
840
798
  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,
799
+ const skillsCol = await col<SkillDoc>("skills");
800
+ const entries = await skillsCol.scan({});
801
+ return entries.map((e) => ({
802
+ id: e.doc.id, name: e.doc.name, description: e.doc.description,
803
+ enabled: true, active: e.doc.active,
862
804
  }));
863
805
  } catch (e) {
864
806
  log.error("⚠️ Error getting skills:", { error: (e as Error).message });
@@ -866,35 +808,20 @@ export function getAllSkills(): Array<{
866
808
  }
867
809
  }
868
810
 
869
- export function getAllDbTools(): Array<{
811
+ export async function getAllDbTools(): Promise<Array<{
870
812
  id: string;
871
813
  name: string;
872
814
  description: string | null;
873
815
  category: string | null;
874
816
  enabled: boolean;
875
817
  active: boolean;
876
- }> {
818
+ }>> {
877
819
  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,
820
+ const toolsCol = await col<ToolDoc>("tools");
821
+ const entries = await toolsCol.scan({});
822
+ return entries.map((e) => ({
823
+ id: e.doc.id, name: e.doc.name, description: e.doc.description, category: e.doc.category,
824
+ enabled: e.doc.enabled, active: e.doc.active,
898
825
  }));
899
826
  } catch (e) {
900
827
  log.error("⚠️ Error getting tools:", { error: (e as Error).message });
@@ -902,7 +829,7 @@ export function getAllDbTools(): Array<{
902
829
  }
903
830
  }
904
831
 
905
- export function getAllMcpServers(): Array<{
832
+ export async function getAllMcpServers(): Promise<Array<{
906
833
  id: string;
907
834
  name: string;
908
835
  transport: string;
@@ -912,34 +839,14 @@ export function getAllMcpServers(): Array<{
912
839
  builtin: boolean;
913
840
  enabled: boolean;
914
841
  active: boolean;
915
- }> {
842
+ }>> {
916
843
  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,
844
+ const mcpCol = await col<McpServerDoc>("mcpServers");
845
+ const entries = await mcpCol.scan({});
846
+ return entries.map((e) => ({
847
+ id: e.doc.id, name: e.doc.name, transport: e.doc.transport, command: e.doc.command,
848
+ args: e.doc.args, url: e.doc.url, builtin: e.doc.builtin,
849
+ enabled: e.doc.enabled, active: e.doc.active,
943
850
  }));
944
851
  } catch (e) {
945
852
  log.error("⚠️ Error getting MCP servers:", { error: (e as Error).message });
@@ -947,35 +854,20 @@ export function getAllMcpServers(): Array<{
947
854
  }
948
855
  }
949
856
 
950
- export function getAllChannels(): Array<{
857
+ export async function getAllChannels(): Promise<Array<{
951
858
  id: string;
952
859
  type: string;
953
860
  accountId: string;
954
861
  status: string;
955
862
  enabled: boolean;
956
863
  active: boolean;
957
- }> {
864
+ }>> {
958
865
  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,
866
+ const channelsCol = await col<ChannelDoc>("channels");
867
+ const entries = await channelsCol.scan({});
868
+ return entries.map((e) => ({
869
+ id: e.doc.id, type: e.doc.type, accountId: e.doc.id,
870
+ status: e.doc.status, enabled: e.doc.enabled, active: e.doc.active,
979
871
  }));
980
872
  } catch (e) {
981
873
  log.warn("[onboarding] ⚠️ Error getting channels:", (e as Error).message);
@@ -983,29 +875,17 @@ export function getAllChannels(): Array<{
983
875
  }
984
876
  }
985
877
 
986
- export function getActiveTools(): Array<{
878
+ export async function getActiveTools(): Promise<Array<{
987
879
  id: string;
988
880
  name: string;
989
881
  description: string | null;
990
882
  category: string | null;
991
- }> {
883
+ }>> {
992
884
  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,
885
+ const toolsCol = await col<ToolDoc>("tools");
886
+ const entries = await toolsCol.scan({});
887
+ return entries.filter((e) => e.doc.active).map((e) => ({
888
+ id: e.doc.id, name: e.doc.name, description: e.doc.description, category: e.doc.category,
1009
889
  }));
1010
890
  } catch (e) {
1011
891
  log.error("⚠️ Error getting active tools:", { error: (e as Error).message });
@@ -1013,18 +893,15 @@ export function getActiveTools(): Array<{
1013
893
  }
1014
894
  }
1015
895
 
1016
- export function getOnboardingProgress(userId: string): OnboardingSection | null {
896
+ export async function getOnboardingProgress(userId: string): Promise<OnboardingSection | null> {
1017
897
  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) {
898
+ const progressCol = await col<OnboardingProgressDoc>("onboardingProgress");
899
+ const entry = await progressCol.get(userId);
900
+ if (entry) {
1024
901
  return {
1025
- step: result.step as OnboardingSection["step"],
902
+ step: entry.doc.step as OnboardingSection["step"],
1026
903
  userId,
1027
- data: JSON.parse(result.data),
904
+ data: JSON.parse(entry.doc.data),
1028
905
  completedAt: Date.now(),
1029
906
  };
1030
907
  }
@@ -1034,13 +911,12 @@ export function getOnboardingProgress(userId: string): OnboardingSection | null
1034
911
  }
1035
912
  }
1036
913
 
1037
- export function saveOnboardingProgress(section: OnboardingSection): void {
914
+ export async function saveOnboardingProgress(section: OnboardingSection): Promise<void> {
1038
915
  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));
916
+ const progressCol = await col<OnboardingProgressDoc>("onboardingProgress");
917
+ await progressCol.put(section.userId, {
918
+ user_id: section.userId, step: section.step, data: JSON.stringify(section.data),
919
+ });
1044
920
  } catch (e) {
1045
921
  log.error("⚠️ Error saving progress:", { error: (e as Error).message });
1046
922
  }
@@ -1054,27 +930,15 @@ export async function getUserProviders(userId: string): Promise<Array<{
1054
930
  enabled: boolean;
1055
931
  }>> {
1056
932
  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,
933
+ const providersCol = await col<ProviderDoc>("providers");
934
+ const entries = await providersCol.scan({});
935
+ return Promise.all(entries.map(async (e) => ({
936
+ id: e.doc.id,
937
+ name: e.doc.name,
938
+ // Secrets are keyed by provider id ("openai"), never by display name ("OpenAI")
939
+ apiKey: (await loadProviderApiKey(e.doc.id)) || null,
940
+ baseUrl: e.doc.base_url,
941
+ enabled: e.doc.enabled,
1078
942
  })));
1079
943
  } catch (e) {
1080
944
  log.warn("[onboarding] ⚠️ Error getting providers:", (e as Error).message);
@@ -1090,27 +954,14 @@ export async function getUserChannels(userId: string): Promise<Array<{
1090
954
  enabled: boolean;
1091
955
  }>> {
1092
956
  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,
957
+ const channelsCol = await col<ChannelDoc>("channels");
958
+ const entries = await channelsCol.findBy("user_id", userId);
959
+ return Promise.all(entries.map(async (e) => ({
960
+ id: e.doc.type,
961
+ type: e.doc.type,
962
+ accountId: e.doc.id,
963
+ config: await loadChannelConfig(e.doc.id),
964
+ enabled: e.doc.enabled,
1114
965
  })));
1115
966
  } catch (e) {
1116
967
  log.warn("[onboarding] ⚠️ Error getting channels:", (e as Error).message);
@@ -1118,32 +969,20 @@ export async function getUserChannels(userId: string): Promise<Array<{
1118
969
  }
1119
970
  }
1120
971
 
1121
- export function getUserAgents(userId: string): Array<{
972
+ export async function getUserAgents(userId: string): Promise<Array<{
1122
973
  id: string;
1123
974
  name: string;
1124
975
  providerId: string | null;
1125
976
  modelId: string | null;
1126
977
  tone: string;
1127
- }> {
978
+ }>> {
1128
979
  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",
980
+ const agentsCol = await col<AgentDoc>("agents");
981
+ const entries = await agentsCol.findBy("user_id", userId);
982
+ return entries.map((e) => ({
983
+ id: e.doc.id, name: e.doc.name,
984
+ providerId: fromIndexable(e.doc.provider_id), modelId: fromIndexable(e.doc.model_id),
985
+ tone: e.doc.tone || "friendly",
1147
986
  }));
1148
987
  } catch (e) {
1149
988
  log.error("⚠️ Error getting agents:", { error: (e as Error).message });
@@ -1159,11 +998,11 @@ export function getUserAgents(userId: string): Array<{
1159
998
  * Hive is designed around a single-user model, so this returns the first user found.
1160
999
  * @returns The user ID or null if no users exist
1161
1000
  */
1162
- export function getSingleUserId(): string | null {
1001
+ export async function getSingleUserId(): Promise<string | null> {
1163
1002
  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;
1003
+ const usersCol = await col<UserDoc>("users");
1004
+ const entries = await usersCol.scan({ limit: 1 });
1005
+ return entries[0]?.id || null;
1167
1006
  } catch (e) {
1168
1007
  log.warn("[getSingleUserId] ⚠️ Error getting user ID:", (e as Error).message);
1169
1008
  return null;
@@ -1175,11 +1014,11 @@ export function getSingleUserId(): string | null {
1175
1014
  * The coordinator is the agent with role = 'coordinator'.
1176
1015
  * @returns The coordinator agent ID or null if not found
1177
1016
  */
1178
- export function getCoordinatorAgentId(): string | null {
1017
+ export async function getCoordinatorAgentId(): Promise<string | null> {
1179
1018
  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;
1019
+ const agentsCol = await col<AgentDoc>("agents");
1020
+ const entries = await agentsCol.findBy("role", "coordinator", { limit: 1 });
1021
+ return entries[0]?.id || null;
1183
1022
  } catch (e) {
1184
1023
  log.warn("[getCoordinatorAgentId] ⚠️ Error getting coordinator agent ID:", (e as Error).message);
1185
1024
  return null;
@@ -1192,13 +1031,12 @@ export function getCoordinatorAgentId(): string | null {
1192
1031
  * @param channelUserId The channel-specific user ID (e.g., Telegram chat_id)
1193
1032
  * @returns The Hive user ID or null if not found
1194
1033
  */
1195
- export function getUserIdFromChannelIdentity(channel: string, channelUserId: string): string | null {
1034
+ export async function getUserIdFromChannelIdentity(channel: string, channelUserId: string): Promise<string | null> {
1196
1035
  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;
1036
+ const identitiesCol = await col<UserIdentityDoc>("userIdentities");
1037
+ const all = await identitiesCol.scan({});
1038
+ const match = all.find((e) => e.doc.channel === channel && e.doc.channel_user_id === channelUserId);
1039
+ return match?.doc.user_id || null;
1202
1040
  } catch (e) {
1203
1041
  log.warn("[getUserIdFromChannelIdentity] ⚠️ Error getting user ID from channel identity:", (e as Error).message);
1204
1042
  return null;
@@ -1208,18 +1046,18 @@ export function getUserIdFromChannelIdentity(channel: string, channelUserId: str
1208
1046
  /**
1209
1047
  * Resolve the user ID from various sources with priority:
1210
1048
  * 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(
1049
+ * 2. Channel identity lookup (if channel and channelUserId provided)
1050
+ * 3. Single user from database
1051
+ * 4. Null (no user found)
1052
+ */
1053
+ export async function resolveUserId(
1216
1054
  opts: {
1217
1055
  userId?: string | null;
1218
1056
  threadId?: string | null;
1219
1057
  channel?: string | null;
1220
1058
  channelUserId?: string | null;
1221
1059
  }
1222
- ): string | null {
1060
+ ): Promise<string | null> {
1223
1061
  // Priority 1: Explicit userId
1224
1062
  if (opts.userId) {
1225
1063
  return opts.userId;
@@ -1227,14 +1065,14 @@ export function resolveUserId(
1227
1065
 
1228
1066
  // Priority 2: Channel identity lookup
1229
1067
  if (opts.channel && opts.channelUserId) {
1230
- const userId = getUserIdFromChannelIdentity(opts.channel, opts.channelUserId);
1068
+ const userId = await getUserIdFromChannelIdentity(opts.channel, opts.channelUserId);
1231
1069
  if (userId) {
1232
1070
  return userId;
1233
1071
  }
1234
1072
  }
1235
1073
 
1236
1074
  // Priority 3: Single user from database
1237
- const singleUserId = getSingleUserId();
1075
+ const singleUserId = await getSingleUserId();
1238
1076
  if (singleUserId) {
1239
1077
  return singleUserId;
1240
1078
  }
@@ -1249,25 +1087,19 @@ export function resolveUserId(
1249
1087
  * 2. First enabled agent
1250
1088
  * 3. Null (no agent found)
1251
1089
  */
1252
- export function getDefaultAgentId(): string | null {
1090
+ export async function getDefaultAgentId(): Promise<string | null> {
1253
1091
  try {
1254
- const db = getDb();
1092
+ const agentsCol = await col<AgentDoc>("agents");
1255
1093
 
1256
1094
  // 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
- }
1095
+ const coordinators = await agentsCol.findBy("role", "coordinator");
1096
+ const enabledCoordinator = coordinators.find((e) => e.doc.enabled);
1097
+ if (enabledCoordinator) return enabledCoordinator.id;
1264
1098
 
1265
1099
  // 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;
1100
+ const all = await agentsCol.scan({});
1101
+ const firstEnabled = all.find((e) => e.doc.enabled);
1102
+ return firstEnabled?.id || null;
1271
1103
  } catch (e) {
1272
1104
  log.warn("[getDefaultAgentId] ⚠️ Error getting default agent ID:", (e as Error).message);
1273
1105
  return null;
@@ -1281,7 +1113,7 @@ export function getDefaultAgentId(): string | null {
1281
1113
  * 3. First enabled agent from database
1282
1114
  * 4. Null (no agent found)
1283
1115
  */
1284
- export function resolveAgentId(agentId?: string | null): string | null {
1116
+ export async function resolveAgentId(agentId?: string | null): Promise<string | null> {
1285
1117
  // Priority 1: Explicit agentId
1286
1118
  if (agentId) {
1287
1119
  return agentId;
@@ -1294,11 +1126,11 @@ export function resolveAgentId(agentId?: string | null): string | null {
1294
1126
  /**
1295
1127
  * Get user preferences (notes) for a given user ID
1296
1128
  */
1297
- export function getUserPreferences(userId: string): string | null {
1129
+ export async function getUserPreferences(userId: string): Promise<string | null> {
1298
1130
  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;
1131
+ const usersCol = await col<UserDoc>("users");
1132
+ const entry = await usersCol.get(userId);
1133
+ return entry?.doc.notes || null;
1302
1134
  } catch (e) {
1303
1135
  log.warn("[getUserPreferences] ⚠️ Error getting user preferences:", (e as Error).message);
1304
1136
  return null;
@@ -1308,7 +1140,7 @@ export function getUserPreferences(userId: string): string | null {
1308
1140
  /**
1309
1141
  * Get agent configuration by ID
1310
1142
  */
1311
- export function getAgentConfig(agentId: string): {
1143
+ export async function getAgentConfig(agentId: string): Promise<{
1312
1144
  id: string;
1313
1145
  user_id: string;
1314
1146
  name: string;
@@ -1320,284 +1152,20 @@ export function getAgentConfig(agentId: string): {
1320
1152
  tools_json: string | null;
1321
1153
  skills_json: string | null;
1322
1154
  max_iterations: number;
1323
- } | null {
1155
+ } | null> {
1324
1156
  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;
1157
+ const agentsCol = await col<AgentDoc>("agents");
1158
+ const entry = await agentsCol.get(agentId);
1159
+ if (!entry) return null;
1160
+ return {
1161
+ id: entry.doc.id, user_id: entry.doc.user_id, name: entry.doc.name,
1162
+ description: entry.doc.description, system_prompt: entry.doc.system_prompt, tone: entry.doc.tone,
1163
+ provider_id: fromIndexable(entry.doc.provider_id), model_id: fromIndexable(entry.doc.model_id),
1164
+ tools_json: entry.doc.tools_json, skills_json: entry.doc.skills_json,
1165
+ max_iterations: entry.doc.max_iterations,
1166
+ };
1345
1167
  } catch (e) {
1346
1168
  log.warn("[getAgentConfig] ⚠️ Error getting agent config:", (e as Error).message);
1347
1169
  return null;
1348
1170
  }
1349
1171
  }
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
- }