@johpaz/hive 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CONTRIBUTING.md +44 -0
  2. package/README.md +310 -0
  3. package/package.json +96 -0
  4. package/packages/cli/package.json +28 -0
  5. package/packages/cli/src/commands/agent-run.ts +168 -0
  6. package/packages/cli/src/commands/agents.ts +398 -0
  7. package/packages/cli/src/commands/chat.ts +142 -0
  8. package/packages/cli/src/commands/config.ts +50 -0
  9. package/packages/cli/src/commands/cron.ts +161 -0
  10. package/packages/cli/src/commands/dev.ts +95 -0
  11. package/packages/cli/src/commands/doctor.ts +133 -0
  12. package/packages/cli/src/commands/gateway.ts +443 -0
  13. package/packages/cli/src/commands/logs.ts +57 -0
  14. package/packages/cli/src/commands/mcp.ts +175 -0
  15. package/packages/cli/src/commands/message.ts +77 -0
  16. package/packages/cli/src/commands/onboard.ts +1868 -0
  17. package/packages/cli/src/commands/security.ts +144 -0
  18. package/packages/cli/src/commands/service.ts +50 -0
  19. package/packages/cli/src/commands/sessions.ts +116 -0
  20. package/packages/cli/src/commands/skills.ts +187 -0
  21. package/packages/cli/src/commands/update.ts +25 -0
  22. package/packages/cli/src/index.ts +185 -0
  23. package/packages/cli/src/utils/token.ts +6 -0
  24. package/packages/code-bridge/README.md +78 -0
  25. package/packages/code-bridge/package.json +18 -0
  26. package/packages/code-bridge/src/index.ts +95 -0
  27. package/packages/code-bridge/src/process-manager.ts +212 -0
  28. package/packages/code-bridge/src/schemas.ts +133 -0
  29. package/packages/core/package.json +46 -0
  30. package/packages/core/src/agent/agent-loop.ts +369 -0
  31. package/packages/core/src/agent/compaction.ts +140 -0
  32. package/packages/core/src/agent/context-compiler.ts +378 -0
  33. package/packages/core/src/agent/context-guard.ts +91 -0
  34. package/packages/core/src/agent/context.ts +138 -0
  35. package/packages/core/src/agent/conversation-store.ts +198 -0
  36. package/packages/core/src/agent/curator.ts +158 -0
  37. package/packages/core/src/agent/hooks.ts +166 -0
  38. package/packages/core/src/agent/index.ts +116 -0
  39. package/packages/core/src/agent/llm-client.ts +503 -0
  40. package/packages/core/src/agent/native-tools.ts +505 -0
  41. package/packages/core/src/agent/prompt-builder.ts +532 -0
  42. package/packages/core/src/agent/providers/index.ts +167 -0
  43. package/packages/core/src/agent/providers.ts +1 -0
  44. package/packages/core/src/agent/reflector.ts +170 -0
  45. package/packages/core/src/agent/service.ts +64 -0
  46. package/packages/core/src/agent/stuck-loop.ts +133 -0
  47. package/packages/core/src/agent/supervisor.ts +39 -0
  48. package/packages/core/src/agent/tracer.ts +102 -0
  49. package/packages/core/src/agent/workspace.ts +110 -0
  50. package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
  51. package/packages/core/src/canvas/canvas-manager.ts +319 -0
  52. package/packages/core/src/canvas/canvas-tools.ts +420 -0
  53. package/packages/core/src/canvas/emitter.ts +115 -0
  54. package/packages/core/src/canvas/index.ts +2 -0
  55. package/packages/core/src/channels/base.ts +138 -0
  56. package/packages/core/src/channels/discord.ts +260 -0
  57. package/packages/core/src/channels/index.ts +7 -0
  58. package/packages/core/src/channels/manager.ts +383 -0
  59. package/packages/core/src/channels/slack.ts +287 -0
  60. package/packages/core/src/channels/telegram.ts +502 -0
  61. package/packages/core/src/channels/webchat.ts +128 -0
  62. package/packages/core/src/channels/whatsapp.ts +375 -0
  63. package/packages/core/src/config/index.ts +12 -0
  64. package/packages/core/src/config/loader.ts +529 -0
  65. package/packages/core/src/events/event-bus.ts +169 -0
  66. package/packages/core/src/gateway/index.ts +5 -0
  67. package/packages/core/src/gateway/initializer.ts +290 -0
  68. package/packages/core/src/gateway/lane-queue.ts +169 -0
  69. package/packages/core/src/gateway/resolver.ts +108 -0
  70. package/packages/core/src/gateway/router.ts +124 -0
  71. package/packages/core/src/gateway/server.ts +3317 -0
  72. package/packages/core/src/gateway/session.ts +95 -0
  73. package/packages/core/src/gateway/slash-commands.ts +192 -0
  74. package/packages/core/src/heartbeat/index.ts +157 -0
  75. package/packages/core/src/index.ts +19 -0
  76. package/packages/core/src/integrations/catalog.ts +286 -0
  77. package/packages/core/src/integrations/env.ts +64 -0
  78. package/packages/core/src/integrations/index.ts +2 -0
  79. package/packages/core/src/memory/index.ts +1 -0
  80. package/packages/core/src/memory/notes.ts +68 -0
  81. package/packages/core/src/plugins/api.ts +128 -0
  82. package/packages/core/src/plugins/index.ts +2 -0
  83. package/packages/core/src/plugins/loader.ts +365 -0
  84. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  85. package/packages/core/src/security/google-chat.ts +269 -0
  86. package/packages/core/src/security/index.ts +192 -0
  87. package/packages/core/src/security/pairing.ts +250 -0
  88. package/packages/core/src/security/rate-limit.ts +270 -0
  89. package/packages/core/src/security/signal.ts +321 -0
  90. package/packages/core/src/state/store.ts +312 -0
  91. package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
  92. package/packages/core/src/storage/crypto.ts +101 -0
  93. package/packages/core/src/storage/db-context.ts +333 -0
  94. package/packages/core/src/storage/onboarding.ts +1087 -0
  95. package/packages/core/src/storage/schema.ts +541 -0
  96. package/packages/core/src/storage/seed.ts +571 -0
  97. package/packages/core/src/storage/sqlite.ts +387 -0
  98. package/packages/core/src/storage/usage.ts +212 -0
  99. package/packages/core/src/tools/bridge-events.ts +74 -0
  100. package/packages/core/src/tools/browser.ts +275 -0
  101. package/packages/core/src/tools/codebridge.ts +421 -0
  102. package/packages/core/src/tools/coordinator-tools.ts +179 -0
  103. package/packages/core/src/tools/cron.ts +611 -0
  104. package/packages/core/src/tools/exec.ts +140 -0
  105. package/packages/core/src/tools/fs.ts +364 -0
  106. package/packages/core/src/tools/index.ts +12 -0
  107. package/packages/core/src/tools/memory.ts +176 -0
  108. package/packages/core/src/tools/notify.ts +113 -0
  109. package/packages/core/src/tools/project-management.ts +376 -0
  110. package/packages/core/src/tools/project.ts +375 -0
  111. package/packages/core/src/tools/read.ts +158 -0
  112. package/packages/core/src/tools/web.ts +436 -0
  113. package/packages/core/src/tools/workspace.ts +171 -0
  114. package/packages/core/src/utils/benchmark.ts +80 -0
  115. package/packages/core/src/utils/crypto.ts +73 -0
  116. package/packages/core/src/utils/date.ts +42 -0
  117. package/packages/core/src/utils/index.ts +4 -0
  118. package/packages/core/src/utils/logger.ts +388 -0
  119. package/packages/core/src/utils/retry.ts +70 -0
  120. package/packages/core/src/voice/index.ts +583 -0
  121. package/packages/core/tsconfig.json +9 -0
  122. package/packages/mcp/package.json +26 -0
  123. package/packages/mcp/src/config.ts +13 -0
  124. package/packages/mcp/src/index.ts +1 -0
  125. package/packages/mcp/src/logger.ts +42 -0
  126. package/packages/mcp/src/manager.ts +434 -0
  127. package/packages/mcp/src/transports/index.ts +67 -0
  128. package/packages/mcp/src/transports/sse.ts +241 -0
  129. package/packages/mcp/src/transports/websocket.ts +159 -0
  130. package/packages/skills/package.json +21 -0
  131. package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
  132. package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
  133. package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
  134. package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
  135. package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
  136. package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
  137. package/packages/skills/src/bundled/memory/SKILL.md +42 -0
  138. package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
  139. package/packages/skills/src/bundled/shell/SKILL.md +43 -0
  140. package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
  141. package/packages/skills/src/bundled/voice/SKILL.md +25 -0
  142. package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
  143. package/packages/skills/src/index.ts +1 -0
  144. package/packages/skills/src/loader.ts +282 -0
  145. package/packages/tools/package.json +43 -0
  146. package/packages/tools/src/browser/browser.test.ts +111 -0
  147. package/packages/tools/src/browser/index.ts +272 -0
  148. package/packages/tools/src/canvas/index.ts +220 -0
  149. package/packages/tools/src/cron/cron.test.ts +164 -0
  150. package/packages/tools/src/cron/index.ts +304 -0
  151. package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
  152. package/packages/tools/src/filesystem/index.ts +379 -0
  153. package/packages/tools/src/git/index.ts +239 -0
  154. package/packages/tools/src/index.ts +4 -0
  155. package/packages/tools/src/shell/detect-env.ts +70 -0
  156. package/packages/tools/tsconfig.json +9 -0
@@ -0,0 +1,505 @@
1
+ /**
2
+ * Native tool registry — no LangChain dependencies.
3
+ *
4
+ * Collects all native tools and MCP tools as plain Tool objects.
5
+ * The agent loop calls execute() directly.
6
+ */
7
+
8
+ import { logger } from "../utils/logger"
9
+ import { loadConfig } from "../config/loader"
10
+ import type { LLMToolDef } from "./llm-client"
11
+ import type { MCPClientManager } from "@johpaz/hive-mcp"
12
+ import { encode } from "toon-format-parser"
13
+
14
+ const log = logger.child("native-tools")
15
+
16
+ // ─── Tool types ───────────────────────────────────────────────────────────────
17
+
18
+ export interface Tool {
19
+ name: string
20
+ description: string
21
+ parameters: Record<string, unknown>
22
+ execute: (params: Record<string, unknown>, config?: any) => Promise<unknown>
23
+ }
24
+
25
+ export interface ToolResult {
26
+ success: boolean
27
+ result?: unknown
28
+ error?: string
29
+ }
30
+
31
+ // ─── Tool name sanitization (Gemini requirement) ──────────────────────────────
32
+
33
+ function sanitizeName(name: string): string {
34
+ let s = name.replace(/[^a-zA-Z0-9_.\-:]/g, "_")
35
+ if (!/^[a-zA-Z_]/.test(s)) s = "_" + s
36
+ return s.length > 64 ? s.substring(0, 64) : s
37
+ }
38
+
39
+ // ─── Collect all native tools ─────────────────────────────────────────────────
40
+
41
+ export function collectNativeTools(): Tool[] {
42
+ const config = loadConfig()
43
+ const tools: Tool[] = []
44
+
45
+ // Lazy imports to avoid issues at module load time
46
+ const add = (t: Tool | null | undefined) => { if (t) tools.push(t) }
47
+
48
+ const { readTool, writeTool, editTool } = require("../tools/read")
49
+ add(readTool); add(writeTool); add(editTool)
50
+
51
+ const { createExecTool } = require("../tools/exec")
52
+ add(createExecTool(config))
53
+
54
+ const { createNotifyTool, createReportProgressTool } = require("../tools/notify")
55
+ add(createNotifyTool()); add(createReportProgressTool())
56
+
57
+ const { createCronTools } = require("../tools/cron")
58
+ for (const t of createCronTools(config)) add(t)
59
+
60
+ const { createCanvasTools } = require("../canvas/canvas-tools")
61
+ for (const t of createCanvasTools(config)) add(t)
62
+
63
+ const { createWebSearchTool, createWebFetchTool } = require("../tools/web")
64
+ add(createWebSearchTool(config)); add(createWebFetchTool(config))
65
+
66
+ const { createBrowserTools } = require("../tools/browser")
67
+ for (const t of createBrowserTools({})) add(t)
68
+
69
+ const { createProjectTools } = require("../tools/project")
70
+ for (const t of createProjectTools({})) add(t)
71
+
72
+ const { MemoryStore } = require("../memory/notes")
73
+ const {
74
+ memoryWriteTool, memoryReadTool, memoryListTool,
75
+ memorySearchTool, memoryDeleteTool
76
+ } = require("../tools/memory")
77
+ const memStore = new MemoryStore(config)
78
+ add(memoryWriteTool(memStore)); add(memoryReadTool(memStore))
79
+ add(memoryListTool(memStore)); add(memorySearchTool(memStore))
80
+ add(memoryDeleteTool(memStore))
81
+
82
+ const { createCodeBridgeTools } = require("../tools/codebridge")
83
+ for (const t of createCodeBridgeTools()) add(t)
84
+
85
+ const {
86
+ projectCreateTool, projectStartTool, projectUpdateTool,
87
+ projectDoneTool, projectFailTool, taskCreateTool, taskUpdateTool,
88
+ } = require("../tools/project-management")
89
+ add(projectCreateTool); add(projectStartTool); add(projectUpdateTool)
90
+ add(projectDoneTool); add(projectFailTool); add(taskCreateTool); add(taskUpdateTool)
91
+
92
+ // Native coordinator tools (no LangChain)
93
+ const { createAgentNative, saveNoteNative, findAgentNative, archiveAgentNative } = require("../tools/coordinator-tools")
94
+ add(createAgentNative); add(saveNoteNative); add(findAgentNative); add(archiveAgentNative)
95
+
96
+ log.info(`[native-tools] Collected ${tools.length} native tools`)
97
+ return tools
98
+ }
99
+
100
+ // ─── Collect MCP tools ────────────────────────────────────────────────────────
101
+
102
+ export async function collectMCPTools(mcpManager: MCPClientManager): Promise<Tool[]> {
103
+ const { getDb } = await import("../storage/sqlite")
104
+ const db = getDb()
105
+ const servers = db.query<any, []>("SELECT * FROM mcp_servers WHERE enabled = 1").all()
106
+ const tools: Tool[] = []
107
+
108
+ for (const server of servers) {
109
+ try {
110
+ const serverTools = mcpManager.getServerTools(server.name)
111
+ if (!serverTools || serverTools.length === 0) {
112
+ log.debug(`[native-tools] MCP server '${server.name}' is configured but has no loaded tools (status: ${server.status})`)
113
+ continue
114
+ }
115
+ for (const mcpTool of serverTools) {
116
+ const toolName = sanitizeName(`${server.name}__${mcpTool.name}`)
117
+ tools.push({
118
+ name: toolName,
119
+ description: mcpTool.description ?? `Tool ${mcpTool.name} from ${server.name}`,
120
+ parameters: mcpTool.inputSchema ?? { type: "object", properties: {} },
121
+ execute: async (params: Record<string, unknown>) => {
122
+ const result = await mcpManager.callTool(server.name, mcpTool.name, params)
123
+ if (typeof result === "string") return result
124
+ try {
125
+ return encode(result)
126
+ } catch {
127
+ return JSON.stringify(result)
128
+ }
129
+ },
130
+ })
131
+ }
132
+ } catch (err) {
133
+ log.warn(`[native-tools] Failed to load MCP tools from ${server.name}:`, err)
134
+ }
135
+ }
136
+
137
+ log.info(`[native-tools] Collected ${tools.length} MCP tools from ${servers.length} configured servers`)
138
+ return tools
139
+ }
140
+
141
+ // ─── Filter tools by agent assignment ────────────────────────────────────────
142
+
143
+ /**
144
+ * Level 2 filter: only keep tools the agent is assigned to.
145
+ * If tools_json is null → all tools are allowed.
146
+ */
147
+ export function filterToolsByAgent(
148
+ tools: Tool[],
149
+ agentToolsJson: string | null
150
+ ): Tool[] {
151
+ if (!agentToolsJson) return tools
152
+ try {
153
+ const allowed = new Set<string>(JSON.parse(agentToolsJson))
154
+ return tools.filter((t) => allowed.has(t.name))
155
+ } catch {
156
+ return tools
157
+ }
158
+ }
159
+
160
+ // ─── Tool Group definitions ───────────────────────────────────────────────────
161
+ //
162
+ // Each group maps to a DB category (from _guessCategory) + trigger keywords.
163
+ // Keywords are appended to EVERY tool in the group during FTS5 sync, so any
164
+ // query matching these words surfaces at least one tool from the group →
165
+ // then the entire group is expanded into the loadout.
166
+ //
167
+ // MCP servers are treated as dynamic groups: "mcp:<serverName>"
168
+ // Each server's tools are all included when any tool from that server matches.
169
+
170
+ const GROUP_KEYWORDS: Record<string, string> = {
171
+ scheduling: "programar recordar recordatorio reminder agendar agenda schedule cron alarm alarma diario semanal mensual recurrente recurring tiempo futuro",
172
+ projects: "proyecto tarea plan organizar milestone asignar equipo project task planning assign work tracking kanban sprint backlog",
173
+ filesystem: "archivo file leer escribir editar contenido document read write edit content source código fuente abrir guardar",
174
+ web: "buscar internet google web search fetch download noticias news información investigar research http url scrape navegar",
175
+ browser: "navegador browser click screenshot form formulario navegar scrape automatizar web página abrir abrir visitar UI",
176
+ memory: "recordar nota guardar memoria anotar note save memory remember store retrieve knowledge persistir base conocimiento",
177
+ code: "código code ejecutar run script bash terminal python javascript exec compile test deploy devops consola comando",
178
+ canvas: "canvas diagrama graph visualizar nodo edge diagram visualization flow chart mapa mental mind map",
179
+ agents: "agente worker especialista crear delegar hire assign sub-task coordinator manager contratar equipo",
180
+ core: "notificar avisar progreso nota guardar comunicar",
181
+ }
182
+
183
+ // ─── Tool-specific synonym map (extra keywords per individual tool) ───────────
184
+ const TOOL_SYNONYMS: Record<string, string> = {
185
+ // Scheduling
186
+ cron_add: "nueva tarea programada nueva alarma set alarm",
187
+ cron_list: "listar ver mostrar lista tareas programadas",
188
+ cron_remove: "eliminar borrar cancelar delete remove cancel",
189
+ cron_edit: "modificar cambiar update patch",
190
+ // Projects
191
+ project_create: "nuevo proyecto nuevo plan create new",
192
+ project_update: "actualizar estado update status",
193
+ project_done: "completar terminar cerrar complete finish done",
194
+ project_fail: "falló error problema issue failed",
195
+ task_create: "nueva tarea subtarea new subtask paso step",
196
+ task_update: "avance progreso progress paso completado",
197
+ // Filesystem
198
+ read: "ver mostrar show contenido content open",
199
+ write: "crear nuevo new save",
200
+ edit: "modificar cambiar patch cambio diff",
201
+ exec: "correr proceso bash shell comando",
202
+ // Web
203
+ web_search: "google busqueda encuentra find informacion noticias news",
204
+ web_fetch: "descargar download pagina página html",
205
+ // Memory
206
+ save_note: "apuntar anotar keep recordar",
207
+ memory_write: "almacenar guardar store",
208
+ memory_read: "recuperar retrieve obtener get",
209
+ memory_list: "ver notas show list",
210
+ memory_search: "encontrar find buscar nota note",
211
+ memory_delete: "borrar eliminar remove",
212
+ // Browser
213
+ browser_open: "abrir visitar navigate visit",
214
+ browser_screenshot: "captura foto imagen capture image",
215
+ browser_click: "hacer clic presionar click press button",
216
+ browser_type: "escribir teclear input",
217
+ browser_close: "cerrar close",
218
+ // Agents
219
+ create_agent: "nuevo agente especializado specialized nuevo worker",
220
+ report_progress: "informar avance estado progress",
221
+ notify: "enviar mensaje send message alerta alert",
222
+ // Canvas
223
+ canvas_update: "grafico diagrama nodo edge",
224
+ // Code bridge
225
+ bridge_exec: "correr ejecutar local development terminal",
226
+ code_bridge_exec: "código run local development",
227
+ }
228
+
229
+ // ─── Sync native + MCP tools to DB catalog (for FTS5 search) ─────────────────
230
+
231
+ /**
232
+ * Syncs ALL tools (native + MCP) into `tools` table + `tools_fts`.
233
+ * Descriptions are enriched with bilingual synonyms so FTS5 matches
234
+ * regardless of the user's language or phrasing.
235
+ * Called once per context compilation (~1ms for 80 tools).
236
+ */
237
+ export function syncNativeToolsToDB(tools: Tool[]): void {
238
+ try {
239
+ const { getDb } = require("../storage/sqlite")
240
+ const db = getDb()
241
+
242
+ db.run("BEGIN")
243
+ try {
244
+ const upsert = db.prepare(`
245
+ INSERT INTO tools (id, name, description, category, enabled, active)
246
+ VALUES (?, ?, ?, ?, 1, 1)
247
+ ON CONFLICT(name) DO UPDATE SET
248
+ description = excluded.description,
249
+ category = excluded.category,
250
+ enabled = 1,
251
+ active = 1
252
+ `)
253
+
254
+ db.run("DELETE FROM tools_fts")
255
+ const insertFts = db.prepare(
256
+ "INSERT INTO tools_fts(tool_name, name, description, category) VALUES (?, ?, ?, ?)"
257
+ )
258
+
259
+ for (const tool of tools) {
260
+ const cat = _guessCategory(tool.name)
261
+ // Enrich FTS5 description with synonyms (native tools use TOOL_SYNONYMS,
262
+ // MCP tools get category-based synonyms appended to their own description)
263
+ const enriched = _enrichDescription(tool.name, tool.description ?? "", cat)
264
+ upsert.run(tool.name, tool.name, tool.description ?? "", cat)
265
+ insertFts.run(tool.name, tool.name, enriched, cat)
266
+ }
267
+
268
+ db.run("COMMIT")
269
+ } catch (err) {
270
+ db.run("ROLLBACK")
271
+ throw err
272
+ }
273
+ } catch (err) {
274
+ log.warn("[native-tools] syncNativeToolsToDB failed (non-fatal):", err)
275
+ }
276
+ }
277
+
278
+ function _enrichDescription(name: string, description: string, category: string): string {
279
+ const parts: string[] = [description]
280
+
281
+ // 1. Tool-specific synonyms (extra precision per individual tool)
282
+ const toolSynonyms = TOOL_SYNONYMS[name]
283
+ if (toolSynonyms) parts.push(toolSynonyms)
284
+
285
+ // 2. Group keywords — appended to ALL tools in the category.
286
+ // This ensures any tool in the group can trigger the full group expansion.
287
+ const groupKeywords = GROUP_KEYWORDS[category] || GROUP_KEYWORDS["core"]
288
+ if (groupKeywords) parts.push(groupKeywords)
289
+
290
+ // 3. MCP tools: derive keywords from server name + tool suffix
291
+ if (name.includes("__")) {
292
+ const [server, toolSuffix = ""] = name.split("__")
293
+ const MCP_SUFFIX_KEYWORDS: Record<string, string> = {
294
+ search: "buscar search find encontrar web internet",
295
+ fetch: "descargar fetch download url http",
296
+ email: "email correo mail enviar send",
297
+ calendar: "calendario agenda schedule programar",
298
+ database: "base datos database query consulta sql",
299
+ file: "archivo file documento read write",
300
+ code: "código code ejecutar run script",
301
+ list: "listar list show ver mostrar",
302
+ create: "crear create nuevo new add",
303
+ delete: "borrar delete remove eliminar",
304
+ update: "actualizar update edit modificar",
305
+ }
306
+ for (const [key, extra] of Object.entries(MCP_SUFFIX_KEYWORDS)) {
307
+ if (toolSuffix.includes(key) || description.toLowerCase().includes(key)) {
308
+ parts.push(extra)
309
+ break
310
+ }
311
+ }
312
+ // Server name as keyword so "usa el servidor X" always matches
313
+ parts.push(server.replace(/_/g, " "))
314
+ }
315
+
316
+ return parts.join(" ")
317
+ }
318
+
319
+ export function getToolGroup(name: string): string {
320
+ return _guessCategory(name)
321
+ }
322
+
323
+ function _guessCategory(name: string): string {
324
+ if (name.startsWith("cron")) return "scheduling"
325
+ if (name.startsWith("project") || name.startsWith("task")) return "projects"
326
+ if (name.startsWith("browser")) return "browser"
327
+ if (name.startsWith("memory") || name === "save_note") return "memory"
328
+ if (["read", "write", "edit"].includes(name)) return "filesystem"
329
+ if (name === "web_search" || name === "web_fetch") return "web"
330
+ if (name.startsWith("canvas")) return "canvas"
331
+ if (name.startsWith("code") || name === "exec") return "code"
332
+ return "core"
333
+ }
334
+
335
+ // ─── Level 3: Tool Loadout via Group-based FTS5 ───────────────────────────────
336
+
337
+ /**
338
+ * Tools always included regardless of message content (ALWAYS_INCLUDE).
339
+ * These never consume optional group slots.
340
+ * Max total loadout = ALWAYS_INCLUDE + expanded groups, capped at MAX_TOTAL_TOOLS.
341
+ */
342
+ const CORE_TOOL_NAMES = new Set([
343
+ // Comms
344
+ "save_note", "notify", "report_progress",
345
+ // Filesystem
346
+ "read", "write", "edit",
347
+ // Cron/scheduling
348
+ "cron_add", "cron_list", "cron_remove", "cron_edit",
349
+ // Projects & tasks
350
+ "project_create", "task_create", "task_update",
351
+ "project_done", "project_fail", "project_update",
352
+ // Agent management
353
+ "create_agent", "find_agent",
354
+ ])
355
+
356
+ /** Hard cap on total tools sent to the model (critical for low-resource local models) */
357
+ const MAX_TOTAL_TOOLS = 20
358
+
359
+ /**
360
+ * Level 3 selection — Group Loading.
361
+ *
362
+ * Instead of picking individual tools, FTS5 identifies which GROUPS are
363
+ * relevant, then expands to ALL tools in those groups. This gives the model
364
+ * a coherent, complete toolset per semantic domain.
365
+ *
366
+ * Native groups → DB category (from _guessCategory)
367
+ * MCP groups → one group per server ("mcp:<serverName>")
368
+ *
369
+ * Examples:
370
+ * "hola" → ALWAYS_INCLUDE only (18 tools)
371
+ * "busca noticias" → ALWAYS_INCLUDE + web group (web_search + web_fetch)
372
+ * "recuérdame el médico" → ALWAYS_INCLUDE (cron tools already included)
373
+ * "lee y edita config.yaml" → ALWAYS_INCLUDE (read/write/edit already included)
374
+ * "usa el servidor X" → ALWAYS_INCLUDE + mcp:X group (all X tools)
375
+ */
376
+ export function selectToolLoadout(
377
+ tools: Tool[],
378
+ userMessage: string,
379
+ maxGroups = 3
380
+ ): Tool[] {
381
+ const coreTools = tools.filter((t) => CORE_TOOL_NAMES.has(t.name))
382
+ const candidates = tools.filter((t) => !CORE_TOOL_NAMES.has(t.name))
383
+
384
+ if (candidates.length === 0) return coreTools
385
+
386
+ const ftsQuery = _buildFTSQuery(userMessage)
387
+ if (!ftsQuery) return coreTools // purely conversational
388
+
389
+ // FTS5: fetch more results than needed to find enough distinct groups
390
+ let ftsRows: { tool_name: string; category: string }[] = []
391
+ try {
392
+ const { getDb } = require("../storage/sqlite")
393
+ const db = getDb()
394
+ ftsRows = db.query(`
395
+ SELECT tool_name, category FROM tools_fts
396
+ WHERE tools_fts MATCH ?
397
+ ORDER BY rank
398
+ LIMIT 40
399
+ `).all(ftsQuery) as { tool_name: string; category: string }[]
400
+ } catch {
401
+ return coreTools // FTS5 not ready yet (pre-first-sync)
402
+ }
403
+
404
+ // Extract groups in FTS5 rank order (first occurrence = best score)
405
+ const rankedGroups: string[] = []
406
+ const seenGroups = new Set<string>()
407
+ for (const row of ftsRows) {
408
+ const group = row.tool_name.includes("__")
409
+ ? `mcp:${row.tool_name.split("__")[0]}`
410
+ : row.category
411
+ if (!seenGroups.has(group)) {
412
+ seenGroups.add(group)
413
+ rankedGroups.push(group)
414
+ }
415
+ }
416
+
417
+ // Keep only top N groups
418
+ const topGroups = new Set(rankedGroups.slice(0, maxGroups))
419
+
420
+ if (topGroups.size === 0) return coreTools
421
+
422
+ // Expand: include ALL tools belonging to each selected group
423
+ const expanded = candidates.filter((t) => {
424
+ if (t.name.includes("__")) {
425
+ return topGroups.has(`mcp:${t.name.split("__")[0]}`)
426
+ }
427
+ return topGroups.has(_guessCategory(t.name))
428
+ })
429
+
430
+ // Cap total tools — trim expanded if needed (core tools are never trimmed)
431
+ const remaining = MAX_TOTAL_TOOLS - coreTools.length
432
+ const trimmedExpanded = expanded.slice(0, Math.max(0, remaining))
433
+
434
+ log.debug(
435
+ `[native-tools] Groups: [${[...topGroups].join(", ")}] ` +
436
+ `expanded=${expanded.length} trimmed=${trimmedExpanded.length} core=${coreTools.length}`
437
+ )
438
+
439
+ return [...coreTools, ...trimmedExpanded]
440
+ }
441
+
442
+ function _buildFTSQuery(text: string): string {
443
+ const STOPWORDS = new Set([
444
+ "que", "con", "para", "por", "una", "uno", "los", "las", "del",
445
+ "como", "esta", "esto", "ese", "eso", "the", "and", "for",
446
+ "with", "this", "that", "have", "will", "also",
447
+ ])
448
+ const words = text
449
+ .toLowerCase()
450
+ .replace(/['"*()[\]]/g, " ")
451
+ .split(/\s+/)
452
+ .filter((w) => w.length > 2 && !STOPWORDS.has(w))
453
+ .slice(0, 8)
454
+ return words.join(" OR ")
455
+ }
456
+
457
+ // ─── Convert tools to LLM tool definitions ───────────────────────────────────
458
+
459
+ export function toToolDefs(tools: Tool[]): LLMToolDef[] {
460
+ return tools.map((t) => ({
461
+ type: "function" as const,
462
+ function: {
463
+ name: sanitizeName(t.name),
464
+ description: t.description,
465
+ parameters: t.parameters as Record<string, unknown>,
466
+ },
467
+ }))
468
+ }
469
+
470
+ // ─── Execute a tool by name ───────────────────────────────────────────────────
471
+
472
+ export async function executeTool(
473
+ tools: Tool[],
474
+ toolName: string,
475
+ argsJson: string,
476
+ context: { user_id: string; thread_id: string; channel?: string }
477
+ ): Promise<string> {
478
+ const tool = tools.find((t) => sanitizeName(t.name) === toolName || t.name === toolName)
479
+ if (!tool) {
480
+ const available = tools.map((t) => t.name).join(", ")
481
+ return `[Tool Error] Unknown tool: "${toolName}". Available tools: ${available}`
482
+ }
483
+
484
+ let args: Record<string, unknown>
485
+ try {
486
+ args = JSON.parse(argsJson || "{}")
487
+ } catch {
488
+ args = {}
489
+ }
490
+
491
+ try {
492
+ log.info(`[native-tools] Executing ${toolName}`, { args })
493
+ const result = await tool.execute(args, { configurable: context })
494
+ if (typeof result === "string") return result
495
+ try {
496
+ return encode(result)
497
+ } catch {
498
+ return JSON.stringify(result)
499
+ }
500
+ } catch (err) {
501
+ const msg = (err as Error).message
502
+ log.error(`[native-tools] Tool ${toolName} failed: ${msg}`)
503
+ return `[Tool Error] ${toolName}: ${msg}`
504
+ }
505
+ }