@johpaz/hive-sdk 0.2.0 → 0.3.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 (86) hide show
  1. package/CHANGELOG.md +306 -0
  2. package/README.md +11 -3
  3. package/package.json +10 -4
  4. package/packages/core/src/agent/agent-catalog.ts +81 -24
  5. package/packages/core/src/agent/compaction.ts +20 -1
  6. package/packages/core/src/agent/context-compiler.ts +6 -3
  7. package/packages/core/src/agent/conversation-store.ts +136 -2
  8. package/packages/core/src/agent/curator.ts +12 -3
  9. package/packages/core/src/agent/llm-providers/nvidia.ts +39 -0
  10. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +38 -2
  11. package/packages/core/src/agent/playbook-selector.ts +18 -3
  12. package/packages/core/src/agent/prompt-builder.ts +2 -2
  13. package/packages/core/src/agent/providers/index.ts +36 -1
  14. package/packages/core/src/agent/reflector.ts +32 -9
  15. package/packages/core/src/agent/skill-selector.ts +2 -2
  16. package/packages/core/src/agent/thread-store.ts +43 -0
  17. package/packages/core/src/agent/tool-selector.ts +2 -0
  18. package/packages/core/src/api/createAgent.ts +66 -1
  19. package/packages/core/src/artifacts/index.ts +15 -0
  20. package/packages/core/src/artifacts/store.ts +77 -2
  21. package/packages/core/src/canvas/index.ts +9 -0
  22. package/packages/core/src/ethics/EthicsGuard.ts +7 -1
  23. package/packages/core/src/events/index.ts +18 -0
  24. package/packages/core/src/events/tool-narration.ts +4 -0
  25. package/packages/core/src/gateway/channel-notify.ts +103 -6
  26. package/packages/core/src/gateway/durable-queue.ts +13 -1
  27. package/packages/core/src/gateway/index.ts +3 -0
  28. package/packages/core/src/gateway/job-store.ts +6 -0
  29. package/packages/core/src/harness/executors.ts +493 -0
  30. package/packages/core/src/harness/index.ts +12 -2
  31. package/packages/core/src/hooks/index.ts +203 -0
  32. package/packages/core/src/images/index.ts +161 -0
  33. package/packages/core/src/index.ts +1 -0
  34. package/packages/core/src/multimodal/vision-service.ts +45 -13
  35. package/packages/core/src/resilience/index.ts +13 -0
  36. package/packages/core/src/scheduler/CronScheduler.ts +48 -21
  37. package/packages/core/src/scheduler/cron/expression.ts +165 -0
  38. package/packages/core/src/scheduler/cron/index.ts +10 -0
  39. package/packages/core/src/scheduler/cron/job.ts +339 -0
  40. package/packages/core/src/scheduler/cron/next-run.ts +121 -0
  41. package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
  42. package/packages/core/src/scheduler/index.ts +21 -3
  43. package/packages/core/src/scheduler/integration.ts +16 -5
  44. package/packages/core/src/scheduler/types.ts +3 -18
  45. package/packages/core/src/services/agents.ts +268 -0
  46. package/packages/core/src/services/cron.ts +257 -0
  47. package/packages/core/src/services/endpoints.ts +289 -0
  48. package/packages/core/src/services/ethics.ts +107 -0
  49. package/packages/core/src/services/images.ts +212 -0
  50. package/packages/core/src/services/index.ts +112 -0
  51. package/packages/core/src/services/mcp.ts +201 -0
  52. package/packages/core/src/services/memory.ts +133 -0
  53. package/packages/core/src/services/models.ts +179 -0
  54. package/packages/core/src/services/providers.ts +152 -0
  55. package/packages/core/src/services/setup.ts +222 -0
  56. package/packages/core/src/services/skills.ts +241 -0
  57. package/packages/core/src/services/swarms.ts +307 -0
  58. package/packages/core/src/services/tools.ts +106 -0
  59. package/packages/core/src/sessions/index.ts +5 -3
  60. package/packages/core/src/sessions/resolve.ts +108 -0
  61. package/packages/core/src/skills/SkillLoader.ts +8 -1
  62. package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
  63. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
  64. package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
  65. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
  66. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
  67. package/packages/core/src/skills/bundled-data.generated.ts +110 -12
  68. package/packages/core/src/storage/bootstrap.ts +74 -5
  69. package/packages/core/src/storage/collections.ts +106 -1
  70. package/packages/core/src/storage/crypto.ts +24 -7
  71. package/packages/core/src/storage/index.ts +2 -1
  72. package/packages/core/src/storage/onboarding.ts +59 -43
  73. package/packages/core/src/storage/reconcile.ts +6 -1
  74. package/packages/core/src/storage/seed.ts +89 -11
  75. package/packages/core/src/swarm/types.ts +3 -18
  76. package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
  77. package/packages/core/src/tool-runtime/index.ts +129 -14
  78. package/packages/core/src/tools/agents/index.ts +18 -60
  79. package/packages/core/src/tools/cli/index.ts +55 -0
  80. package/packages/core/src/tools/core/index.ts +50 -2
  81. package/packages/core/src/tools/cron/index.ts +4 -4
  82. package/packages/core/src/tools/images/index.ts +130 -0
  83. package/packages/core/src/tools/index.ts +14 -1
  84. package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
  85. package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
  86. package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
@@ -460,22 +460,32 @@ Para gestionar tareas programadas (cron jobs): crear, listar, actualizar, pausar
460
460
  | \`cron_expression\` | string | Expresión cron (solo para recurring) |
461
461
  | \`fire_at\` | string | Datetime ISO (solo para one_shot) |
462
462
  | \`channel\` | string | Canal de notificación |
463
- | \`start_at\` | string | Inicio de ventana opcional (Croner startAt) |
464
- | \`stop_at\` | string | Fin de ventana opcional (Croner stopAt) |
463
+ | \`start_at\` | string | Inicio de ventana opcional |
464
+ | \`stop_at\` | string | Fin de ventana opcional |
465
465
  | \`dom_and_dow\` | number | 0=OR (default), 1=AND (día mes + día semana) |
466
+ | \`max_runs\` | number | Deja de correr después de N corridas ("recordámelo 3 veces") |
467
+ | \`payload\` | object | Datos que recibe el agente al ejecutarse |
468
+ | \`agent_id\` | string | Agente concreto que debe ejecutarla. Si se omite, decide el coordinador |
469
+ | \`tool_name\` | string | Ejecutar una tool directamente, sin pasar por un agente |
470
+
471
+ > **La zona horaria no se pasa acá**: sale del perfil del usuario. No la
472
+ > inventes ni la pidas — si el usuario dice "a las 9", son las 9 de su reloj.
466
473
 
467
474
  ## Cron Expression Format
468
475
 
469
476
  \`\`\`
470
- * * * * *
471
- │ │
472
- │ │ └── Día semana (0-6, 0=Domingo)
473
- │ │ │ └──── Mes (1-12)
474
- │ │ └────── Día del mes (1-31)
475
- └──────── Hora (0-23)
476
- └────────── Minuto (0-59)
477
+ ┌───────── segundos (0-59) ← opcional, sólo si hacen falta
478
+ ┌─────── minuto (0-59)
479
+ │ │ ┌───── hora (0-23)
480
+ │ │ │ ┌─── día del mes (1-31)
481
+ │ │ ┌─ mes (1-12 o JAN-DEC)
482
+ │ │ ┌ día de semana (0-7 o SUN-SAT, 0 y 7 = domingo)
483
+ * * * * * *
477
484
  \`\`\`
478
485
 
486
+ Cinco campos, o seis poniendo los segundos adelante. Acepta \`*\`, listas \`1,15\`,
487
+ rangos \`1-5\`, pasos \`*/2\`, y nombres de mes y de día.
488
+
479
489
  ## Ejemplos Comunes
480
490
 
481
491
  | Expresión | Significado |
@@ -834,7 +844,7 @@ Esta skill se activa cuando el usuario necesita:
834
844
  description: `Automate web workflows with navigation, clicks, form filling, and visual verification`,
835
845
  category: "web",
836
846
  version: "1.0.0",
837
- tools: ["browser_navigate","browser_click","browser_type","browser_screenshot"],
847
+ tools: ["browser_navigate","browser_wait","browser_click","browser_type","browser_screenshot","browser_script","computer_use_task"],
838
848
  triggers: ["automatizá el navegador","automate browser","completá el formulario","fill form","hacé clic en","click on","iniciá sesión","login","registrate","sign up","interactuá con la web","interact with website","flujo web","web workflow"],
839
849
  body: `
840
850
  # Browser Automate Skill
@@ -879,7 +889,7 @@ Esta skill se activa para automatizar flujos de interacción con aplicaciones we
879
889
  description: `Navigate to web pages and capture rendered content including screenshots for dynamic sites`,
880
890
  category: "web",
881
891
  version: "1.0.0",
882
- tools: ["browser_navigate","browser_screenshot","web_fetch"],
892
+ tools: ["browser_navigate","browser_screenshot","browser_extract","browser_wait"],
883
893
  triggers: ["capturá el contenido","scrape content","obtené la página renderizada","get rendered page","sitios dinámicos","dynamic sites","web con javascript","javascript websites","tomá screenshot y contenido","screenshot and content"],
884
894
  body: `
885
895
  # Browser Scrape Skill
@@ -894,7 +904,12 @@ Esta skill se activa para sitios web dinámicos que requieren JavaScript renderi
894
904
  |------|----------|---------------|
895
905
  | \`browser_navigate\` | Navega y renderiza página completa | Sitios con JavaScript/SPA |
896
906
  | \`browser_screenshot\` | Captura estado visual | Evidencia de contenido renderizado |
897
- | \`web_fetch\` | Extrae texto como markdown | Contenido textual de página renderizada |
907
+ | \`browser_extract\` | Extrae del DOM ya renderizado | Contenido textual o estructurado del SPA |
908
+ | \`browser_wait\` | Espera a que aparezca un selector | Antes de extraer, en páginas que cargan por partes |
909
+
910
+ > **No usar \`web_fetch\` acá.** Vuelve a pedir la URL al servidor y recibe el HTML
911
+ > sin JavaScript ejecutado — en un SPA, una cáscara vacía. Para eso está
912
+ > \`browser_extract\`, que lee el DOM que el navegador ya renderizó.
898
913
 
899
914
  ## Workflow
900
915
 
@@ -1430,6 +1445,89 @@ Para crear flujos interactivos multi-paso usando A2UI v0.9. Usar cuando se neces
1430
1445
  - Agregar validación con \`checks\` en TextField
1431
1446
  - Mantener el estado del flujo en el data model (\`/data/step\`, \`/data/serviceType\`, etc.)
1432
1447
  - Eliminar surfaces con \`a2ui_delete_surface\` al completar o cancelar
1448
+ `,
1449
+ },
1450
+ {
1451
+ name: "image_editor",
1452
+ description: `Convertir, redimensionar y rotar imágenes con Bun.Image. Inspeccionar dimensiones y formato sin cargar la imagen al contexto.`,
1453
+ category: "images",
1454
+ version: "1.0.0",
1455
+ tools: ["image_metadata","image_transform","artifact_inspect"],
1456
+ triggers: ["convertí la imagen","convert image","redimensioná la imagen","resize image","achicá la foto","make it smaller","pasala a webp","convert to webp","rotá la imagen","rotate image","qué tamaño tiene","image dimensions","comprimí la imagen","compress image","hacé una miniatura","make a thumbnail"],
1457
+ body: `
1458
+ # Image Editor Skill
1459
+
1460
+ ## Cuándo se Activa
1461
+
1462
+ Cuando el usuario quiere **cambiar** una imagen (formato, tamaño, rotación) o
1463
+ **saber** sus características. Corre sobre \`Bun.Image\`, nativo del runtime.
1464
+
1465
+ ## Herramientas Disponibles
1466
+
1467
+ | Tool | Qué hace | Cuándo usarla |
1468
+ |------|----------|---------------|
1469
+ | \`image_metadata\` | Ancho, alto y formato | Siempre antes de transformar |
1470
+ | \`image_transform\` | Convierte, redimensiona, rota | El trabajo en sí |
1471
+ | \`artifact_inspect\` | Tipo MIME, integridad, tamaño | Cuando no está claro si el artefacto es una imagen |
1472
+
1473
+ ## Lo Que Hay Que Entender
1474
+
1475
+ **Todo pasa por artefactos.** Una imagen no viaja en el mensaje: vive como
1476
+ artefacto y se la nombra por su \`artifact_id\`. Es lo que evita que una foto de 4
1477
+ MB entre a la ventana de contexto y se reenvíe en cada turno de la conversación.
1478
+
1479
+ **Transformar no destruye.** \`image_transform\` devuelve un artefacto **nuevo**.
1480
+ El original sigue disponible, así que se puede probar un tamaño, ver que no
1481
+ gustó y probar otro sin haber perdido nada.
1482
+
1483
+ **La proporción se pierde en silencio.** Si se pasan \`width\` y \`height\` juntos,
1484
+ la imagen se estira sin avisar. Pasando uno solo, el otro se calcula.
1485
+
1486
+ ## Formatos
1487
+
1488
+ \`jpeg\` · \`png\` · \`webp\` · \`avif\` · \`heic\`
1489
+
1490
+ Ante la duda, **webp**: pesa bastante menos que jpeg y png a calidad comparable,
1491
+ y lo entienden todos los navegadores actuales.
1492
+ `,
1493
+ },
1494
+ {
1495
+ name: "artifact_reader",
1496
+ description: `Leer archivos grandes que llegaron como artifact_ref: por tramos o buscando dentro, sin volcarlos enteros al contexto.`,
1497
+ category: "artifacts",
1498
+ version: "1.0.0",
1499
+ tools: ["artifact_read","artifact_inspect"],
1500
+ triggers: ["leé el archivo adjunto","read the attachment","qué dice el documento","what does the document say","buscá en el archivo","search in the file","artifact_ref","el resultado quedó truncado","the result was truncated","seguí leyendo","keep reading"],
1501
+ body: `
1502
+ # Artifact Reader Skill
1503
+
1504
+ ## Cuándo se Activa
1505
+
1506
+ Cuando aparece un **\`artifact_ref\`**: un archivo, un adjunto o el resultado de
1507
+ una tool que era demasiado grande para entrar en el contexto.
1508
+
1509
+ ## Herramientas Disponibles
1510
+
1511
+ | Tool | Qué hace | Cuándo usarla |
1512
+ |------|----------|---------------|
1513
+ | \`artifact_inspect\` | Tamaño, tipo MIME, integridad | Antes de leer, para decidir cómo |
1514
+ | \`artifact_read\` | Contenido, por tramos o buscando | Para leer de verdad |
1515
+
1516
+ ## Lo Que Hay Que Entender
1517
+
1518
+ **Un \`artifact_ref\` no es un error.** Es el mecanismo por el que un archivo
1519
+ grande queda fuera de la ventana de contexto y a la vez disponible. El archivo
1520
+ está entero; lo que cambia es que se lee a pedido en vez de entrar completo en
1521
+ cada turno de la conversación.
1522
+
1523
+ **Buscar cuesta mucho menos que paginar.** \`artifact_read\` con \`search\` recorre
1524
+ el archivo del lado del servidor y devuelve extractos de cada coincidencia con
1525
+ su contexto alrededor. Paginar el mismo archivo con \`offset\`/\`limit\` gasta un
1526
+ turno por tramo y mete en el contexto un montón de texto que no hacía falta.
1527
+ Cuando se sabe qué se busca, se busca.
1528
+
1529
+ **Continuar es explícito.** Cada lectura devuelve \`next_offset\`. Ese es el valor
1530
+ que se pasa para seguir — no se calcula a mano.
1433
1531
  `,
1434
1532
  },
1435
1533
  ];
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { getHiveDb, getOpenHiveDb } from "./hivedb.ts";
15
15
  import { col } from "./hive.ts";
16
- import { seedAllData } from "./seed.ts";
16
+ import { seedAllData, type SeedOptions } from "./seed.ts";
17
17
  import { ensureSecretsBackend } from "./crypto.ts";
18
18
  import { ensureLegacyThread } from "../agent/thread-store.ts";
19
19
 
@@ -106,6 +106,10 @@ const INDEXES: IndexSpec[] = [
106
106
  { collection: "conversationThreads", field: "user_id" },
107
107
  { collection: "conversationThreads", field: "channel" },
108
108
  { collection: "conversationThreads", field: "archived" },
109
+ { collection: "swarms", field: "enabled" },
110
+ { collection: "apiEndpoints", field: "enabled" },
111
+ { collection: "memory", field: "user_id" },
112
+ { collection: "playbook", field: "user_id" },
109
113
  ];
110
114
 
111
115
  async function ensureIndexes(): Promise<void> {
@@ -123,8 +127,8 @@ async function ensureIndexes(): Promise<void> {
123
127
  * user-toggleable fields (enabled/active) and doesn't touch `users`, so it's
124
128
  * safe to call unconditionally before any onboarding has happened.
125
129
  */
126
- async function ensureSeedData(): Promise<void> {
127
- await seedAllData();
130
+ async function ensureSeedData(opts?: SeedOptions): Promise<void> {
131
+ await seedAllData(opts);
128
132
  }
129
133
 
130
134
  // Bootstrap state belongs to a specific database instance. A boolean that
@@ -138,6 +142,69 @@ let bootstrappedDb: Awaited<ReturnType<typeof getHiveDb>> | null = null;
138
142
  * mensaje— como una conversación más de la web, para que siga siendo legible desde
139
143
  * la lista. Idempotente: no hace nada si ya está registrada o si no hay historial.
140
144
  */
145
+ /**
146
+ * Reasigna las memorias anteriores al aislamiento por usuario.
147
+ *
148
+ * Antes la colección era global: `id` era el título y no había `user_id`. Al
149
+ * introducir el aislamiento esas filas quedarían invisibles —nadie las
150
+ * encontraría, porque toda lectura filtra por dueño— así que se les asigna el
151
+ * usuario existente y se re-clavean a `${userId}:${title}`.
152
+ *
153
+ * Idempotente: una fila que ya tiene `user_id` no se toca. Si todavía no hay
154
+ * usuario (onboarding sin terminar) se deja para el próximo arranque, cuando lo
155
+ * haya, en vez de asignarlas a `""` y tener que deshacerlo.
156
+ */
157
+ async function migrateLegacyMemories(): Promise<void> {
158
+ try {
159
+ const memories = await col<{ id: string; user_id?: string; title: string }>("memory");
160
+ const legacy = (await memories.scan({})).filter((e) => !e.doc.user_id);
161
+ if (legacy.length === 0) return;
162
+
163
+ const users = await col<{ id: string }>("users");
164
+ const primero = (await users.scan({ limit: 1 }))[0];
165
+ if (!primero) return;
166
+
167
+ for (const entry of legacy) {
168
+ const nuevoId = `${primero.id}:${entry.doc.title}`;
169
+ await memories.put(nuevoId, { ...entry.doc, id: nuevoId, user_id: primero.id });
170
+ if (nuevoId !== entry.id) await memories.delete(entry.id).catch(() => {});
171
+ }
172
+ } catch {
173
+ // La memoria no es crítica para arrancar: si falla, se reintenta al próximo boot.
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Reglas del playbook anteriores a `user_id`.
179
+ *
180
+ * Se aprendieron cuando la instalación era de un solo usuario, así que son de
181
+ * él. Dejarlas sin dueño las volvería globales y se las inyectaría a cualquier
182
+ * usuario que se dé de alta después, que es exactamente el aislamiento que este
183
+ * campo viene a cerrar. Las sembradas no pasan por acá: `seedAllData()` les
184
+ * pone `user_id: ""` en cada arranque.
185
+ */
186
+ async function migrateLegacyPlaybook(): Promise<void> {
187
+ try {
188
+ const playbook = await col<{ id: string; user_id?: string }>("playbook");
189
+ const legacy = (await playbook.scan({})).filter((e) => e.doc.user_id === undefined);
190
+ if (legacy.length === 0) return;
191
+
192
+ const users = await col<{ id: string }>("users");
193
+ const primero = (await users.scan({ limit: 1 }))[0];
194
+ if (!primero) return;
195
+
196
+ for (const entry of legacy) {
197
+ await playbook.put(
198
+ entry.id,
199
+ { ...entry.doc, user_id: primero.id },
200
+ { expectedVersion: entry.version },
201
+ );
202
+ }
203
+ } catch {
204
+ // El playbook no es crítico para arrancar: se reintenta al próximo boot.
205
+ }
206
+ }
207
+
141
208
  async function ensureLegacyThreads(): Promise<void> {
142
209
  try {
143
210
  const users = await col<{ id: string }>("users");
@@ -154,16 +221,18 @@ async function ensureLegacyThreads(): Promise<void> {
154
221
  * static catalogs, and records the schema version. Safe to call on every
155
222
  * gateway boot.
156
223
  */
157
- export async function ensureHiveDb(): Promise<void> {
224
+ export async function ensureHiveDb(opts?: SeedOptions): Promise<void> {
158
225
  const db = await getHiveDb();
159
226
  await ensureIndexes();
160
227
  // Mint the master key before anything can save an API key, so a fresh
161
228
  // install is durable from the first keystroke rather than after a restart
162
229
  // has already dropped the secret.
163
230
  ensureSecretsBackend();
164
- await ensureSeedData();
231
+ await ensureSeedData(opts);
165
232
 
166
233
  await ensureLegacyThreads();
234
+ await migrateLegacyMemories();
235
+ await migrateLegacyPlaybook();
167
236
 
168
237
  const meta = await col<{ value: number }>("meta");
169
238
  const existing = await meta.get("schemaVersion");
@@ -351,6 +351,8 @@ export interface TraceDoc {
351
351
  export interface ReflectionDoc {
352
352
  id: string
353
353
  trace_ids: string
354
+ /** Usuario del que salieron las trazas. `""` cuando abarca varios. */
355
+ user_id: string
354
356
  insight_type: "success_pattern" | "failure_pattern" | "optimization" | "ethics_violation" | "root_cause" | "learning_proposal"
355
357
  description: string
356
358
  affected_tools: string | null
@@ -363,6 +365,16 @@ export interface PlaybookDoc {
363
365
  id: string
364
366
  rule: string
365
367
  category: string
368
+ /**
369
+ * De quién es lo aprendido. `""` = global, que es lo que corresponde a las
370
+ * reglas sembradas: son conocimiento del producto, no de nadie en particular.
371
+ *
372
+ * Sin esto el playbook era global a la instalación, así que **lo que el agente
373
+ * aprendía interactuando con una persona se le aplicaba a todas**. En una
374
+ * instalación de un solo usuario da igual; en un host multi-inquilino es una
375
+ * fuga entre workspaces.
376
+ */
377
+ user_id: string
366
378
  applicable_to: string | null
367
379
  helpful_count: number
368
380
  harmful_count: number
@@ -521,7 +533,15 @@ export interface ArtifactDoc {
521
533
  height: number | null
522
534
  status: "active" | "expired"
523
535
  created_at: number
524
- expires_at: number
536
+ /**
537
+ * `null` = no expira nunca.
538
+ *
539
+ * Los artefactos internos —capturas, resultados de tools grandes— son basura
540
+ * transitoria y se limpian solos a los 7 días. Pero un archivo que el usuario
541
+ * subió o transformó no es basura: borrárselo a la semana convierte un
542
+ * servicio en una pérdida de datos. Quien lo crea decide.
543
+ */
544
+ expires_at: number | null
525
545
  expired_at: number | null
526
546
  }
527
547
 
@@ -552,8 +572,21 @@ export interface TaskRunDoc {
552
572
  // ─── Stage 6: orchestration ───────────────────────────────────────────────────
553
573
 
554
574
  /** id = title — the old `notes`/`memory_*` table never existed, so this is a from-scratch fix. */
575
+ /**
576
+ * Memoria de largo plazo de un usuario.
577
+ *
578
+ * `id` es `${user_id}:${title}`. Hasta acá era sólo el título y la colección era
579
+ * global al proceso —coherente con que hive es mono-usuario, pero inservible
580
+ * para un runtime donde cada quien arma su colmena: dos usuarios no podían
581
+ * tener una memoria con el mismo título, y cualquiera veía la del otro.
582
+ *
583
+ * Las filas anteriores (id = título, sin `user_id`) se migran al arrancar
584
+ * asignándolas al usuario existente. Ver `migrateLegacyMemories`.
585
+ */
555
586
  export interface MemoryDoc {
556
587
  id: string
588
+ /** Dueño de la memoria. Las filas legacy se migran al usuario único. */
589
+ user_id: string
557
590
  title: string
558
591
  content: string
559
592
  created_at: number
@@ -613,6 +646,78 @@ export interface DelegationGroupDoc {
613
646
  notified_at: number | null
614
647
  }
615
648
 
649
+ /**
650
+ * Un enjambre guardado: qué agentes lo componen, con qué rol y en qué orden.
651
+ *
652
+ * Hasta acá un enjambre sólo existía mientras corría — `runRoleSwarm()` recibe
653
+ * los agentes en la llamada y no persiste nada. Quien armara uno desde una
654
+ * interfaz lo perdía al cerrar la ventana, y por eso hive-cloud terminó creando
655
+ * sus propias tablas en Postgres: no le quedaba alternativa.
656
+ *
657
+ * `agents_json` guarda `[{agentId, role, orderIndex}]`. Se deja como JSON y no
658
+ * como colección aparte a propósito: un enjambre se lee y se escribe entero,
659
+ * nunca por agente suelto, así que una tabla de unión sólo agregaría joins.
660
+ */
661
+ /**
662
+ * Un endpoint HTTP registrado como herramienta.
663
+ *
664
+ * Una tool normal es código con un `execute`, así que desde una interfaz no hay
665
+ * dónde ponerlo. Un endpoint declarativo invierte eso: el usuario aporta
666
+ * **datos** —URL, método, cabeceras, qué parámetros acepta— y el ejecutor es
667
+ * genérico y vive en el SDK. Es la única forma de que alguien sume una
668
+ * capacidad propia desde una UI sin abrir la puerta a ejecutar código
669
+ * arbitrario, y sin levantar un servidor MCP.
670
+ *
671
+ * Las credenciales NO viven acá: van cifradas en el secret store bajo
672
+ * `endpoint:<id>:headers`, igual que las de proveedores y canales. Esta fila es
673
+ * pública y se muestra en la UI; la clave, no.
674
+ */
675
+ export interface ApiEndpointDoc {
676
+ id: string
677
+ name: string
678
+ description: string
679
+ method: string
680
+ url: string
681
+ /** Cabeceras no sensibles. Las que llevan credenciales van al secret store. */
682
+ headers_json: string | null
683
+ /** Query fijos que siempre acompañan la llamada. */
684
+ query_json: string | null
685
+ /**
686
+ * Cuerpo con marcadores `{{param}}` que se reemplazan por lo que pase el
687
+ * modelo. Sin esto un endpoint POST sólo serviría con cuerpo fijo.
688
+ */
689
+ body_template: string | null
690
+ /** JSON Schema de lo que el modelo puede pasar — lo que ve como parámetros. */
691
+ param_schema_json: string | null
692
+ enabled: boolean
693
+ created_at: number
694
+ updated_at: number
695
+ }
696
+
697
+ export interface SwarmDoc {
698
+ id: string
699
+ name: string
700
+ description: string | null
701
+ /** Cómo se coordinan: en cadena, en paralelo, o con un orquestador que delega. */
702
+ strategy: "sequential" | "parallel" | "hierarchical"
703
+ /** Requerido por `hierarchical`; null en las otras dos. */
704
+ orchestrator_agent_id: string | null
705
+ /** `[{ agentId, role, orderIndex }]` — ver SwarmMemberSpec. */
706
+ agents_json: string
707
+ enabled: boolean
708
+ /** Tope de delegaciones en la estrategia jerárquica; null = el del runtime. */
709
+ max_delegations: number | null
710
+ created_at: number
711
+ updated_at: number
712
+ }
713
+
714
+ /** Un integrante del enjambre, tal como se serializa en `SwarmDoc.agents_json`. */
715
+ export interface SwarmMemberSpec {
716
+ agentId: string
717
+ role: "orchestrator" | "worker"
718
+ orderIndex: number
719
+ }
720
+
616
721
  export interface AgentBusMessageDoc {
617
722
  id: string
618
723
  event_type: string
@@ -29,6 +29,22 @@ interface SecretDoc {
29
29
  const _mem = new Map<string, string>()
30
30
  let _keychainOk: boolean | null = null // null = untested
31
31
 
32
+ let _keychainApi: unknown = undefined
33
+
34
+ /**
35
+ * A test double or a recovered runtime may replace Bun.secrets. Reset the
36
+ * cached availability result when that API object changes so a previous
37
+ * headless failure cannot poison the replacement backend forever.
38
+ */
39
+ function _getKeychainApi(): any {
40
+ const api = (Bun as any).secrets
41
+ if (api !== _keychainApi) {
42
+ _keychainApi = api
43
+ _keychainOk = null
44
+ }
45
+ return api
46
+ }
47
+
32
48
  async function _get(name: string): Promise<string | null> {
33
49
  const cached = _mem.get(name)
34
50
  if (cached !== undefined) return cached
@@ -82,11 +98,10 @@ async function _readCollectionSecret(name: string): Promise<string | null> {
82
98
  /**
83
99
  * Olvida si el keychain del SO respondió o no.
84
100
  *
85
- * `_keychainOk` se cachea a nivel de módulo a propósito: en un servidor sin
86
- * libsecret cada lectura tiraría y no tiene sentido reintentarlo. El costo es
87
- * que el resultado del primer sondeo vale para todo el proceso, y un test que
88
- * sustituya `Bun.secrets` por un doble queda cortocircuitado si algo ya sondeó
89
- * antes y falló. Resetear acá lo vuelve a dejar sin probar.
101
+ * Sustituir `Bun.secrets` por un doble ya NO necesita esto: `_getKeychainApi()`
102
+ * detecta que el objeto cambió e invalida el sondeo solo. Queda para el caso
103
+ * que aquello no cubre —vaciar además el caché en memoria (`_mem`)— y porque
104
+ * ya salió publicado.
90
105
  */
91
106
  export function resetKeychainProbe(): void {
92
107
  _keychainOk = null
@@ -94,9 +109,10 @@ export function resetKeychainProbe(): void {
94
109
  }
95
110
 
96
111
  async function _keychainGet(name: string): Promise<string | null> {
112
+ const keychain = _getKeychainApi()
97
113
  if (_keychainOk === false) return null
98
114
  try {
99
- const val = await (Bun as any).secrets.get({ service: SERVICE, name })
115
+ const val = await keychain.get({ service: SERVICE, name })
100
116
  _keychainOk = true
101
117
  return val ?? null
102
118
  } catch {
@@ -106,9 +122,10 @@ async function _keychainGet(name: string): Promise<string | null> {
106
122
  }
107
123
 
108
124
  async function _keychainSet(name: string, value: string): Promise<boolean> {
125
+ const keychain = _getKeychainApi()
109
126
  if (_keychainOk === false) return false
110
127
  try {
111
- await (Bun as any).secrets.set({ service: SERVICE, name, value })
128
+ await keychain.set({ service: SERVICE, name, value })
112
129
  _keychainOk = true
113
130
  return true
114
131
  } catch {
@@ -34,7 +34,7 @@ export type * from "./collections.ts";
34
34
  export { catalogModelKey, wireModelId, isResellerProvider } from "./model-id.ts";
35
35
 
36
36
  // ─── Seed del catálogo ───────────────────────────────────────────────────────
37
- export type { SeedData } from "./seed.ts";
37
+ export type { SeedData, SeedOptions, SpecialistSeedMode } from "./seed.ts";
38
38
  export {
39
39
  SEED_DATA,
40
40
  seedAllData,
@@ -88,6 +88,7 @@ export {
88
88
 
89
89
  // ─── Onboarding e identidad ──────────────────────────────────────────────────
90
90
  export type { OnboardingSection } from "./onboarding.ts";
91
+ export { activateBrowserTools } from "./onboarding.ts";
91
92
  export {
92
93
  resolveUserId,
93
94
  resolveAgentId,