@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
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Imágenes como servicio — para el usuario final, no para el agente.
3
+ *
4
+ * Las tools de `tools/images/` existen para que el agente manipule imágenes
5
+ * dentro de un turno, y trabajan con ids de artefacto porque devolverle bytes a
6
+ * un modelo es lo que llena la ventana de contexto.
7
+ *
8
+ * Esto es lo otro: la superficie para que una aplicación ofrezca "convertí y
9
+ * redimensioná tus imágenes" como funcionalidad propia. **Entra y sale por
10
+ * bytes** —una app móvil o web sube un archivo y descarga el resultado— y por
11
+ * dentro se persiste como artefacto para que exista una galería y un historial.
12
+ *
13
+ * La retención la decide el usuario. Los artefactos internos se limpian solos a
14
+ * los 7 días, pero lo que alguien sube o transforma nace **sin expiración**:
15
+ * borrárselo a la semana convertiría un servicio en una pérdida de datos.
16
+ */
17
+
18
+ import { measureImage, transformImage, type ImageFormat, type TransformOptions, imagesSupported } from "../images/index.ts";
19
+ import {
20
+ createArtifact, readArtifactBytes, listArtifacts, setArtifactRetention, deleteArtifact,
21
+ } from "../artifacts/store.ts";
22
+ import type { ArtifactDoc } from "../storage/collections.ts";
23
+ import { resolveUserId } from "../storage/onboarding.ts";
24
+ import { logger } from "../utils/logger.ts";
25
+
26
+ const log = logger.child("services/images");
27
+
28
+ /** Lo que ve la UI de una imagen guardada. Nunca incluye los bytes. */
29
+ export interface StoredImage {
30
+ id: string;
31
+ mimeType: string;
32
+ width: number | null;
33
+ height: number | null;
34
+ bytes: number;
35
+ createdAt: number;
36
+ /** `null` = no expira. */
37
+ expiresAt: number | null;
38
+ }
39
+
40
+ export type ImageInput = Uint8Array | ArrayBuffer | Buffer | string;
41
+
42
+ function toSummary(doc: ArtifactDoc): StoredImage {
43
+ return {
44
+ id: doc.id,
45
+ mimeType: doc.mime_type,
46
+ width: doc.width,
47
+ height: doc.height,
48
+ bytes: doc.size,
49
+ createdAt: doc.created_at,
50
+ expiresAt: doc.expires_at,
51
+ };
52
+ }
53
+
54
+ function toBytes(input: ImageInput): Uint8Array {
55
+ if (typeof input === "string") return Uint8Array.from(Buffer.from(input, "base64"));
56
+ if (input instanceof Uint8Array) return input;
57
+ return new Uint8Array(input as ArrayBuffer);
58
+ }
59
+
60
+ async function owner(userId?: string): Promise<string> {
61
+ if (userId) return userId;
62
+ return (await resolveUserId({}).catch(() => null)) ?? "";
63
+ }
64
+
65
+ function requireSupport(): void {
66
+ if (!imagesSupported()) {
67
+ throw new Error("El procesamiento de imágenes necesita Bun >= 1.4 (Bun.Image no está disponible)");
68
+ }
69
+ }
70
+
71
+ export interface UploadOptions {
72
+ userId?: string;
73
+ mimeType?: string;
74
+ /** Cuándo caduca. Por omisión **nunca**: es del usuario, no basura transitoria. */
75
+ expiresAt?: number | null;
76
+ }
77
+
78
+ /** Guarda una imagen y devuelve su ficha. Acepta bytes o base64. */
79
+ export async function uploadImage(input: ImageInput, opts: UploadOptions = {}): Promise<StoredImage> {
80
+ requireSupport();
81
+ const bytes = toBytes(input);
82
+ const meta = await measureImage(bytes);
83
+
84
+ const doc = await createArtifact({
85
+ bytes,
86
+ mimeType: opts.mimeType ?? `image/${meta.format}`,
87
+ kind: "image",
88
+ userId: await owner(opts.userId),
89
+ width: meta.width,
90
+ height: meta.height,
91
+ expiresAt: opts.expiresAt !== undefined ? opts.expiresAt : null,
92
+ });
93
+ return toSummary(doc);
94
+ }
95
+
96
+ export interface TransformResult extends StoredImage {
97
+ /** Los bytes del resultado, para que la UI los descargue sin otra llamada. */
98
+ data: Uint8Array;
99
+ }
100
+
101
+ /**
102
+ * De dónde sale la imagen: una ya guardada o datos crudos.
103
+ *
104
+ * Es explícito a propósito. La alternativa —aceptar un `string` y adivinar si es
105
+ * un id o un base64 por su longitud— funciona hasta que alguien manda una imagen
106
+ * de 4×4 píxeles, cuyo base64 es más corto que un UUID.
107
+ */
108
+ export type TransformSource = { imageId: string } | { data: ImageInput };
109
+
110
+ export interface ServiceTransformOptions extends TransformOptions {
111
+ userId?: string;
112
+ expiresAt?: number | null;
113
+ }
114
+
115
+ /**
116
+ * Transforma una imagen y guarda el resultado como una nueva.
117
+ *
118
+ * Acepta un id de una imagen ya guardada o bytes sueltos, y devuelve **la ficha
119
+ * y los bytes**: una UI que acaba de pedir una conversión quiere el archivo, no
120
+ * tener que ir a buscarlo. El original nunca se modifica.
121
+ */
122
+ export async function transformStoredImage(
123
+ source: TransformSource,
124
+ opts: ServiceTransformOptions = {},
125
+ ): Promise<TransformResult> {
126
+ requireSupport();
127
+
128
+ let entrada: Uint8Array;
129
+ if ("imageId" in source) {
130
+ const datos = await readArtifactBytes(source.imageId);
131
+ if (!datos) throw new Error(`No encontré la imagen ${source.imageId}`);
132
+ entrada = datos.bytes;
133
+ } else {
134
+ entrada = toBytes(source.data);
135
+ }
136
+
137
+ const { bytes, metadata } = await transformImage(entrada, opts);
138
+ const doc = await createArtifact({
139
+ bytes,
140
+ mimeType: `image/${metadata.format}`,
141
+ kind: "image",
142
+ userId: await owner(opts.userId),
143
+ width: metadata.width,
144
+ height: metadata.height,
145
+ expiresAt: opts.expiresAt !== undefined ? opts.expiresAt : null,
146
+ });
147
+
148
+ log.info(`imagen transformada → ${doc.id} (${metadata.width}x${metadata.height} ${metadata.format})`);
149
+ return { ...toSummary(doc), data: bytes };
150
+ }
151
+
152
+ /** Los bytes de una imagen guardada, para descargarla. */
153
+ export async function getImageBytes(
154
+ imageId: string,
155
+ ): Promise<{ data: Uint8Array; mimeType: string } | null> {
156
+ const datos = await readArtifactBytes(imageId);
157
+ return datos ? { data: new Uint8Array(datos.bytes), mimeType: datos.mimeType } : null;
158
+ }
159
+
160
+ /** La galería del usuario, de la más reciente a la más vieja. */
161
+ export async function listImages(
162
+ userId?: string,
163
+ opts: { limit?: number; includeExpired?: boolean } = {},
164
+ ): Promise<StoredImage[]> {
165
+ const docs = await listArtifacts(await owner(userId), { kind: "image", ...opts });
166
+ return docs.map(toSummary);
167
+ }
168
+
169
+ /**
170
+ * Cambia cuándo caduca. `null` = conservarla indefinidamente.
171
+ *
172
+ * Es el control que pediste: el usuario decide qué guarda y qué deja ir.
173
+ */
174
+ export async function setImageRetention(imageId: string, expiresAt: number | null): Promise<StoredImage> {
175
+ const doc = await setArtifactRetention(imageId, expiresAt);
176
+ if (!doc) throw new Error(`No encontré la imagen ${imageId}`);
177
+ return toSummary(doc);
178
+ }
179
+
180
+ /** Borra la imagen y su archivo, ahora. */
181
+ export async function deleteImage(imageId: string): Promise<boolean> {
182
+ return deleteArtifact(imageId);
183
+ }
184
+
185
+ // ─── Presets ─────────────────────────────────────────────────────────────────
186
+
187
+ /**
188
+ * Tamaños de uso corriente, para que quien monte la UI no tenga que decidir
189
+ * cuánto es "una miniatura". Son un punto de partida, no una imposición:
190
+ * `transformStoredImage` acepta cualquier combinación.
191
+ */
192
+ export const IMAGE_PRESETS = {
193
+ /** Vista en cuadrícula. */
194
+ thumbnail: { width: 256, height: 256, format: "webp" as ImageFormat, quality: 75 },
195
+ /** Publicar en una página sin que pese de más. */
196
+ web: { width: 1600, format: "webp" as ImageFormat, quality: 82 },
197
+ /** Foto de perfil. */
198
+ avatar: { width: 512, height: 512, format: "webp" as ImageFormat, quality: 85 },
199
+ } as const;
200
+
201
+ export type ImagePreset = keyof typeof IMAGE_PRESETS;
202
+
203
+ /** Aplica un preset. Atajo sobre `transformStoredImage`. */
204
+ export async function applyPreset(
205
+ source: TransformSource,
206
+ preset: ImagePreset,
207
+ opts: { userId?: string; expiresAt?: number | null } = {},
208
+ ): Promise<TransformResult> {
209
+ const p = IMAGE_PRESETS[preset];
210
+ if (!p) throw new Error(`Preset desconocido: ${preset}. Disponibles: ${Object.keys(IMAGE_PRESETS).join(", ")}`);
211
+ return transformStoredImage(source, { ...p, ...opts });
212
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Services — la superficie que maneja una interfaz, no el modelo.
3
+ *
4
+ * El SDK nació para que lo condujera un LLM: casi todo el CRUD vivía dentro de
5
+ * las tools (`cronCreateTool`, `memoryWriteTool`, `agentCreateTool`…), con
6
+ * argumentos con forma de LLM y respuestas escritas para un prompt. Montar una
7
+ * UI encima obligaba a llamar `tool.execute({...})` y parsear prosa, o a
8
+ * escribir consultas crudas contra HiveDB conociendo un esquema privado.
9
+ *
10
+ * Acá vive la implementación y las tools pasan a envolverla: una sola
11
+ * implementación con dos consumidores, el modelo y la aplicación.
12
+ *
13
+ * Es deliberadamente **agnóstico del framework** — funciones, no rutas HTTP.
14
+ * Una app móvil o de escritorio que embeba el runtime no quiere un servidor; y
15
+ * quien haga una UI web monta sus rutas encima en unas pocas líneas, que es
16
+ * exactamente lo que hace hive (ver `gateway/routes/conversations.ts`, delgada
17
+ * porque toda su lógica está en `agent/thread-store.ts`).
18
+ *
19
+ * Convención: estas funciones **lanzan** ante un error en vez de devolver
20
+ * `{ok:false}`. Quien construye una UI quiere `try/catch`, no inspeccionar un
21
+ * campo. La traducción al formato del modelo la hace el envoltorio de la tool.
22
+ */
23
+
24
+ export * as memory from "./memory.ts";
25
+ export * as agents from "./agents.ts";
26
+ export * as skills from "./skills.ts";
27
+ export * as cron from "./cron.ts";
28
+ export * as tools from "./tools.ts";
29
+ export * as ethics from "./ethics.ts";
30
+ export * as providers from "./providers.ts";
31
+ export * as models from "./models.ts";
32
+ export * as mcp from "./mcp.ts";
33
+ export * as swarms from "./swarms.ts";
34
+ export * as endpoints from "./endpoints.ts";
35
+ export * as setup from "./setup.ts";
36
+ export * as images from "./images.ts";
37
+
38
+ // También sueltas, para quien prefiera importar la función directa.
39
+ export {
40
+ writeMemory, readMemory, listMemories, searchMemories, deleteMemory,
41
+ type MemoryEntry, type MemorySearchHit,
42
+ } from "./memory.ts";
43
+
44
+ export {
45
+ createAgent, getAgent, listAgents, updateAgent, deleteAgent,
46
+ assignTools, assignSkills, assignMcpServers, enableAgent, disableAgent,
47
+ type AgentSummary, type CreateAgentInput, type UpdateAgentInput, type ListAgentsOptions,
48
+ } from "./agents.ts";
49
+
50
+ export {
51
+ createSkill, getSkill, listSkills, updateSkill, deleteSkill, toggleSkill,
52
+ importSkillFromDisk,
53
+ type SkillSummary, type CreateSkillInput, type UpdateSkillInput,
54
+ } from "./skills.ts";
55
+
56
+ export {
57
+ createCronJob, getCronJob, listCronJobs, updateCronJob, deleteCronJob,
58
+ pauseCronJob, resumeCronJob, triggerCronJob, getCronHistory, hasScheduler,
59
+ type CronJobSummary, type CreateCronInput, type UpdateCronInput,
60
+ } from "./cron.ts";
61
+
62
+ export {
63
+ listTools, getTool, toggleTool, updateToolMetadata,
64
+ type ToolSummary,
65
+ } from "./tools.ts";
66
+
67
+ export {
68
+ listEthics, getEthics, createEthics, updateEthics, toggleEthics, deleteEthics,
69
+ type EthicsSummary,
70
+ } from "./ethics.ts";
71
+
72
+ export {
73
+ listProviders, getProvider, createProvider, updateProvider, toggleProvider, deleteProvider,
74
+ type ProviderSummary,
75
+ } from "./providers.ts";
76
+
77
+ export {
78
+ listModels, getModel, createModel, toggleModel, deleteModel, renameModel, agentsUsingModel,
79
+ type ModelSummary,
80
+ } from "./models.ts";
81
+
82
+ export {
83
+ listMcpServers, getMcpServer, createMcpServer, updateMcpServer,
84
+ testMcpServer, toggleMcpServer, deleteMcpServer,
85
+ type McpServerSummary, type CreateMcpInput,
86
+ } from "./mcp.ts";
87
+
88
+ export {
89
+ createSwarm, getSwarm, listSwarms, updateSwarm, deleteSwarm, toggleSwarm, runSwarm,
90
+ type SwarmSummary, type SwarmMember, type CreateSwarmInput, type UpdateSwarmInput,
91
+ type RunSwarmOptions,
92
+ } from "./swarms.ts";
93
+
94
+ export {
95
+ createEndpoint, getEndpoint, listEndpoints, updateEndpoint, deleteEndpoint,
96
+ toggleEndpoint, testEndpoint, registerEndpointTools, buildEndpointTool, toolNameFor,
97
+ type EndpointSummary, type CreateEndpointInput,
98
+ } from "./endpoints.ts";
99
+
100
+ export {
101
+ planSeedFor, applySeedPlan, enableCatalogAgent, enableCatalogAgents,
102
+ disableCatalogAgent, listEnabledCatalogAgents, listCatalogPersonas,
103
+ planActivationFor, CATALOG_AGENT_IDS,
104
+ type SeedPlan, type ActivationGap,
105
+ } from "./setup.ts";
106
+
107
+ export {
108
+ uploadImage, transformStoredImage, getImageBytes, listImages,
109
+ setImageRetention, deleteImage, applyPreset, IMAGE_PRESETS,
110
+ type StoredImage, type TransformResult, type TransformSource,
111
+ type UploadOptions, type ServiceTransformOptions, type ImagePreset,
112
+ } from "./images.ts";
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Servidores MCP — la API, no la ruta.
3
+ *
4
+ * Un servidor MCP es la vía real para que alguien sume capacidades propias sin
5
+ * escribir código dentro del SDK: levanta un proceso que expone tools por el
6
+ * protocolo, lo registra acá, y sus tools quedan disponibles para los agentes.
7
+ *
8
+ * Añade algo que hive no tiene: **`testMcpServer()`**. Allí probar un servidor
9
+ * consiste en crearlo, activarlo y esperar a que el hot-reload lo conecte (~2s)
10
+ * para ver si el estado quedó en `connected` o `error`. Para una UI eso es un
11
+ * "guardá y cruzá los dedos"; acá se puede intentar la conexión y responder si
12
+ * funcionó, sin dejar una fila a medio configurar.
13
+ *
14
+ * Las credenciales van cifradas por `storage/crypto.ts`: `headers` para los
15
+ * transportes HTTP, `env` para los de stdio. Nunca se devuelven en claro.
16
+ */
17
+
18
+ import { col } from "../storage/hive.ts";
19
+ import type { McpServerDoc } from "../storage/collections.ts";
20
+ import {
21
+ storeMcpHeaders, loadMcpHeaders, storeMcpEnv, deleteMcpSecrets, maskApiKey,
22
+ } from "../storage/crypto.ts";
23
+ import { getMCPManager } from "../mcp/singleton.ts";
24
+ import { slugify } from "./agents.ts";
25
+ import { logger } from "../utils/logger.ts";
26
+
27
+ const log = logger.child("services/mcp");
28
+
29
+ export interface McpServerSummary {
30
+ id: string;
31
+ name: string;
32
+ transport: string;
33
+ command: string | null;
34
+ args: string[];
35
+ url: string | null;
36
+ enabled: boolean;
37
+ active: boolean;
38
+ builtin: boolean;
39
+ /** Estado en vivo si hay manager conectado; si no, el último persistido. */
40
+ status: string;
41
+ toolsCount: number;
42
+ /** Sólo qué cabeceras hay configuradas, con el valor enmascarado. */
43
+ maskedHeaders: Record<string, string>;
44
+ }
45
+
46
+ export interface CreateMcpInput {
47
+ name: string;
48
+ transport?: "stdio" | "http" | "sse";
49
+ command?: string | null;
50
+ args?: string[];
51
+ url?: string | null;
52
+ headers?: Record<string, string>;
53
+ env?: Record<string, string>;
54
+ enabled?: boolean;
55
+ }
56
+
57
+ function parseArgs(raw: string | null): string[] {
58
+ if (!raw) return [];
59
+ try {
60
+ const v = JSON.parse(raw);
61
+ return Array.isArray(v) ? v.map(String) : [];
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ /** Enmascara los valores: una UI necesita saber qué hay, no cuál es. */
68
+ function maskHeaders(headers: Record<string, unknown>): Record<string, string> {
69
+ const out: Record<string, string> = {};
70
+ for (const [k, v] of Object.entries(headers)) out[k] = maskApiKey(String(v));
71
+ return out;
72
+ }
73
+
74
+ async function mcpCol() {
75
+ return col<McpServerDoc>("mcpServers");
76
+ }
77
+
78
+ async function toSummary(doc: McpServerDoc): Promise<McpServerSummary> {
79
+ const headers = await loadMcpHeaders(doc.id).catch(() => ({}));
80
+ const manager = getMCPManager();
81
+ const live = manager?.getServerStatus?.(doc.name);
82
+ return {
83
+ id: doc.id,
84
+ name: doc.name,
85
+ transport: doc.transport,
86
+ command: doc.command,
87
+ args: parseArgs(doc.args),
88
+ url: doc.url,
89
+ enabled: doc.enabled,
90
+ active: doc.active,
91
+ builtin: doc.builtin,
92
+ status: live ?? doc.status,
93
+ toolsCount: doc.tools_count,
94
+ maskedHeaders: maskHeaders(headers as Record<string, unknown>),
95
+ };
96
+ }
97
+
98
+ export async function listMcpServers(opts?: { includeDisabled?: boolean }): Promise<McpServerSummary[]> {
99
+ const rows = await (await mcpCol()).scan({});
100
+ const docs = rows.map((e) => e.doc).filter((d) => (opts?.includeDisabled ? true : d.enabled));
101
+ return Promise.all(docs.map(toSummary));
102
+ }
103
+
104
+ export async function getMcpServer(id: string): Promise<McpServerSummary | null> {
105
+ const entry = await (await mcpCol()).get(id);
106
+ return entry ? toSummary(entry.doc) : null;
107
+ }
108
+
109
+ export async function createMcpServer(input: CreateMcpInput): Promise<McpServerSummary> {
110
+ if (!input.name?.trim()) throw new Error("El servidor MCP necesita un nombre");
111
+
112
+ const transport = input.transport ?? (input.url ? "http" : "stdio");
113
+ if (transport === "stdio" && !input.command) throw new Error("Un servidor stdio necesita `command`");
114
+ if (transport !== "stdio" && !input.url) throw new Error(`Un servidor ${transport} necesita \`url\``);
115
+
116
+ const c = await mcpCol();
117
+ const id = slugify(input.name).replace(/_/g, "-");
118
+ if (await c.get(id)) throw new Error(`Ya existe un servidor MCP con id "${id}"`);
119
+
120
+ const doc: McpServerDoc = {
121
+ id,
122
+ name: input.name,
123
+ transport,
124
+ command: input.command ?? null,
125
+ args: JSON.stringify(input.args ?? []),
126
+ url: input.url ?? null,
127
+ enabled: input.enabled ?? true,
128
+ active: false,
129
+ builtin: false,
130
+ status: "disconnected",
131
+ tools_count: 0,
132
+ };
133
+
134
+ await c.put(id, doc, { expectedVersion: 0 });
135
+ if (input.headers) await storeMcpHeaders(id, input.headers);
136
+ if (input.env) await storeMcpEnv(id, input.env);
137
+ log.info(`servidor MCP "${input.name}" registrado (${id})`);
138
+ return toSummary(doc);
139
+ }
140
+
141
+ export async function updateMcpServer(
142
+ id: string,
143
+ changes: Partial<CreateMcpInput>,
144
+ ): Promise<McpServerSummary> {
145
+ const c = await mcpCol();
146
+ const entry = await c.get(id);
147
+ if (!entry) throw new Error(`No existe el servidor MCP "${id}"`);
148
+
149
+ const doc: McpServerDoc = { ...entry.doc };
150
+ if (changes.name !== undefined) doc.name = changes.name;
151
+ if (changes.transport !== undefined) doc.transport = changes.transport;
152
+ if (changes.command !== undefined) doc.command = changes.command;
153
+ if (changes.args !== undefined) doc.args = JSON.stringify(changes.args);
154
+ if (changes.url !== undefined) doc.url = changes.url;
155
+ if (changes.enabled !== undefined) doc.enabled = changes.enabled;
156
+
157
+ await c.put(id, doc, { expectedVersion: entry.version });
158
+ if (changes.headers) await storeMcpHeaders(id, changes.headers);
159
+ if (changes.env) await storeMcpEnv(id, changes.env);
160
+ return toSummary(doc);
161
+ }
162
+
163
+ /**
164
+ * Intenta conectar el servidor y reporta el resultado.
165
+ *
166
+ * Requiere un `MCPClientManager` activo: sin él no hay quién hable el protocolo,
167
+ * y devolver "ok" sería mentir. Con manager, el estado que se devuelve es el
168
+ * real tras el intento, no el que estaba guardado.
169
+ */
170
+ export async function testMcpServer(id: string): Promise<{ ok: boolean; status: string; toolsCount?: number; error?: string }> {
171
+ const entry = await (await mcpCol()).get(id);
172
+ if (!entry) throw new Error(`No existe el servidor MCP "${id}"`);
173
+
174
+ const manager = getMCPManager();
175
+ if (!manager) {
176
+ throw new Error("Probar un servidor MCP requiere un MCPClientManager activo");
177
+ }
178
+
179
+ try {
180
+ await manager.connectServer(entry.doc.name);
181
+ const status = manager.getServerStatus?.(entry.doc.name) ?? "unknown";
182
+ return { ok: status === "connected", status };
183
+ } catch (error) {
184
+ return { ok: false, status: "error", error: (error as Error).message };
185
+ }
186
+ }
187
+
188
+ export async function toggleMcpServer(id: string, enabled: boolean): Promise<McpServerSummary> {
189
+ return updateMcpServer(id, { enabled });
190
+ }
191
+
192
+ export async function deleteMcpServer(id: string): Promise<boolean> {
193
+ const c = await mcpCol();
194
+ const entry = await c.get(id);
195
+ if (!entry) return false;
196
+ if (entry.doc.builtin) throw new Error("Un servidor MCP incorporado no se puede borrar; desactívalo");
197
+
198
+ await c.delete(id);
199
+ await deleteMcpSecrets(id).catch(() => {});
200
+ return true;
201
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Memoria de largo plazo — la API, no la tool.
3
+ *
4
+ * Estas operaciones sólo existían dentro de `memoryWriteTool` y sus cuatro
5
+ * hermanas (`tools/agents/index.ts`), con argumentos con forma de LLM y
6
+ * respuestas escritas para un prompt. Una UI que quisiera listar o editar la
7
+ * memoria del usuario tenía que llamar `tool.execute({...})` y parsear prosa,
8
+ * o escribir consultas crudas contra HiveDB.
9
+ *
10
+ * Acá vive la implementación; las tools pasan a ser envoltorios que la llaman y
11
+ * traducen el resultado al formato que espera el modelo. Una implementación,
12
+ * dos consumidores.
13
+ *
14
+ * Estas funciones **lanzan** en vez de devolver `{ok:false}`: un error es un
15
+ * error, y quien construya una UI quiere un `try/catch`, no inspeccionar un
16
+ * campo. La traducción a `{ok:false, error}` la hace el envoltorio de la tool.
17
+ *
18
+ * **Cada memoria pertenece a un usuario.** La colección era global al proceso
19
+ * —coherente con hive, que es mono-usuario— pero eso no sirve para un runtime
20
+ * donde cada quien arma su colmena: dos usuarios no podían tener una memoria
21
+ * con el mismo título, y cualquiera veía la del otro. `userId` es opcional en la
22
+ * firma y se resuelve solo cuando no viene, para no romper a quien ya llamaba
23
+ * estas funciones.
24
+ */
25
+
26
+ import { col } from "../storage/hive.ts";
27
+ import type { MemoryDoc } from "../storage/collections.ts";
28
+ import { resolveUserId } from "../storage/onboarding.ts";
29
+
30
+ export interface MemoryEntry {
31
+ title: string;
32
+ content: string;
33
+ createdAt: number;
34
+ updatedAt: number;
35
+ }
36
+
37
+ export interface MemorySearchHit {
38
+ title: string;
39
+ snippet: string;
40
+ }
41
+
42
+ const SNIPPET_CHARS = 200;
43
+
44
+ /**
45
+ * `${userId}:${title}` — el título solo no alcanza como id en cuanto hay más de
46
+ * un usuario. Guardar dos veces el mismo título del mismo usuario actualiza; el
47
+ * de otro usuario es otra memoria.
48
+ */
49
+ const memoryId = (userId: string, title: string) => `${userId}:${title}`;
50
+
51
+ /** Sin userId explícito se resuelve el del contexto; `""` es el caso mono-usuario. */
52
+ async function resolveOwner(userId?: string): Promise<string> {
53
+ if (userId) return userId;
54
+ return (await resolveUserId({}).catch(() => null)) ?? "";
55
+ }
56
+
57
+ function toEntry(doc: MemoryDoc): MemoryEntry {
58
+ return {
59
+ title: doc.title,
60
+ content: doc.content,
61
+ createdAt: doc.created_at,
62
+ updatedAt: doc.updated_at,
63
+ };
64
+ }
65
+
66
+ async function memoryCol() {
67
+ return col<MemoryDoc>("memory");
68
+ }
69
+
70
+ /** Guarda o actualiza una entrada. Devuelve la entrada resultante. */
71
+ export async function writeMemory(title: string, content: string, userId?: string): Promise<MemoryEntry> {
72
+ if (!title.trim()) throw new Error("La memoria necesita un título");
73
+
74
+ const owner = await resolveOwner(userId);
75
+ const id = memoryId(owner, title);
76
+ const c = await memoryCol();
77
+ const existing = await c.get(id);
78
+ const now = Date.now();
79
+ const doc: MemoryDoc = {
80
+ id,
81
+ user_id: owner,
82
+ title,
83
+ content,
84
+ created_at: existing?.doc.created_at ?? now,
85
+ updated_at: now,
86
+ };
87
+ await c.put(id, doc, { expectedVersion: existing?.version ?? 0 });
88
+ return toEntry(doc);
89
+ }
90
+
91
+ /** La entrada, o null si no existe. */
92
+ export async function readMemory(title: string, userId?: string): Promise<MemoryEntry | null> {
93
+ const owner = await resolveOwner(userId);
94
+ const entry = await (await memoryCol()).get(memoryId(owner, title));
95
+ return entry ? toEntry(entry.doc) : null;
96
+ }
97
+
98
+ /** Todas las entradas, de la más reciente a la más vieja. */
99
+ export async function listMemories(userId?: string): Promise<MemoryEntry[]> {
100
+ const owner = await resolveOwner(userId);
101
+ const rows = await (await memoryCol()).findBy("user_id", owner);
102
+ return rows.map((e) => toEntry(e.doc)).sort((a, b) => b.updatedAt - a.updatedAt);
103
+ }
104
+
105
+ /**
106
+ * Búsqueda por subcadena en título y contenido.
107
+ *
108
+ * Deliberadamente simple: no pasa por el índice BM25 porque la memoria no se
109
+ * indexa ahí. Para colecciones grandes conviene revisarlo, pero cambiar el
110
+ * comportamiento ahora rompería lo que el modelo ya espera.
111
+ */
112
+ export async function searchMemories(query: string, userId?: string): Promise<MemorySearchHit[]> {
113
+ const needle = query.toLowerCase();
114
+ const owner = await resolveOwner(userId);
115
+ const rows = await (await memoryCol()).findBy("user_id", owner);
116
+ return rows
117
+ .map((e) => e.doc)
118
+ .filter((n) => n.content.toLowerCase().includes(needle) || n.title.toLowerCase().includes(needle))
119
+ .map((n) => ({
120
+ title: n.title,
121
+ snippet: n.content.slice(0, SNIPPET_CHARS) + (n.content.length > SNIPPET_CHARS ? "..." : ""),
122
+ }));
123
+ }
124
+
125
+ /** Borra una entrada. `false` si no existía. */
126
+ export async function deleteMemory(title: string, userId?: string): Promise<boolean> {
127
+ const owner = await resolveOwner(userId);
128
+ const id = memoryId(owner, title);
129
+ const c = await memoryCol();
130
+ if (!(await c.get(id))) return false;
131
+ await c.delete(id);
132
+ return true;
133
+ }