@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,257 @@
1
+ /**
2
+ * Tareas programadas — la API, no la tool.
3
+ *
4
+ * A diferencia de la memoria, acá la implementación ya existía y era buena:
5
+ * `CronScheduler` (`scheduler/CronScheduler.ts`) tiene `create`, `update`,
6
+ * `delete`, `pause`, `resume`, `trigger`, `getHistory` y `listTasks`. El
7
+ * problema era el alcance: sólo se llegaba a ella desde dentro de las ocho
8
+ * `cron*Tool`, con argumentos con forma de LLM.
9
+ *
10
+ * Este servicio es la fachada. Conserva el comportamiento **híbrido** que ya
11
+ * tenían las tools y que hive replica en su ruta HTTP: si hay un scheduler
12
+ * corriendo se delega en él —es quien sabe calcular la próxima ejecución y
13
+ * rearmar los timers—; si no, se opera directo sobre `cronJobs`, para que un
14
+ * proceso que sólo administra tareas (una UI, un script) no necesite levantar
15
+ * el scheduler entero.
16
+ *
17
+ * Un job creado sin scheduler queda persistido y lo recoge el próximo arranque.
18
+ */
19
+
20
+ import { col } from "../storage/hive.ts";
21
+ import type { CronJobDoc, TaskRunDoc } from "../storage/collections.ts";
22
+ import { getSchedulerInstance } from "../tools/cron/index.ts";
23
+ import { logger } from "../utils/logger.ts";
24
+
25
+ const log = logger.child("services/cron");
26
+
27
+ export interface CreateCronInput {
28
+ name: string;
29
+ task: string;
30
+ taskType: "recurring" | "one_shot";
31
+ /** Requerido para `recurring`. */
32
+ cronExpression?: string;
33
+ /** Requerido para `one_shot` (ISO 8601). */
34
+ fireAt?: string;
35
+ timezone?: string;
36
+ agentId?: string | null;
37
+ channel?: string;
38
+ payload?: Record<string, unknown>;
39
+ toolName?: string | null;
40
+ maxRuns?: number | null;
41
+ startAt?: string;
42
+ stopAt?: string;
43
+ domAndDow?: boolean;
44
+ }
45
+
46
+ export interface CronJobSummary {
47
+ id: string;
48
+ name: string;
49
+ task: string;
50
+ taskType: CronJobDoc["task_type"];
51
+ status: CronJobDoc["status"];
52
+ cronExpression: string | null;
53
+ fireAt: string | null;
54
+ nextRun?: string | null;
55
+ }
56
+
57
+ function toSummary(doc: CronJobDoc): CronJobSummary {
58
+ return {
59
+ id: doc.id,
60
+ name: doc.name,
61
+ task: doc.task,
62
+ taskType: doc.task_type,
63
+ status: doc.status,
64
+ cronExpression: doc.cron_expression,
65
+ fireAt: doc.fire_at,
66
+ nextRun: (doc as { next_run?: string | null }).next_run ?? null,
67
+ };
68
+ }
69
+
70
+ async function jobsCol() {
71
+ return col<CronJobDoc>("cronJobs");
72
+ }
73
+
74
+ /** true cuando hay un scheduler vivo capaz de disparar las tareas. */
75
+ export function hasScheduler(): boolean {
76
+ return !!getSchedulerInstance();
77
+ }
78
+
79
+ export async function createCronJob(input: CreateCronInput): Promise<CronJobSummary> {
80
+ if (!input.name?.trim()) throw new Error("La tarea necesita un nombre");
81
+ if (input.taskType === "recurring" && !input.cronExpression) {
82
+ throw new Error("Una tarea recurrente necesita `cronExpression`");
83
+ }
84
+ if (input.taskType === "one_shot" && !input.fireAt) {
85
+ throw new Error("Una tarea de una sola vez necesita `fireAt`");
86
+ }
87
+
88
+ const timezone = input.timezone ?? "UTC";
89
+ const scheduler = getSchedulerInstance();
90
+
91
+ if (scheduler) {
92
+ const res = await scheduler.create({
93
+ name: input.name,
94
+ task: input.task,
95
+ task_type: input.taskType,
96
+ cron_expression: input.cronExpression,
97
+ fire_at: input.fireAt,
98
+ timezone,
99
+ start_at: input.startAt,
100
+ stop_at: input.stopAt,
101
+ dom_and_dow: input.domAndDow,
102
+ agent_id: input.agentId ?? null,
103
+ channel: input.channel,
104
+ payload: input.payload,
105
+ tool_name: input.toolName ?? null,
106
+ max_runs: input.maxRuns ?? null,
107
+ });
108
+ const doc = (await (await jobsCol()).get(res.id))?.doc;
109
+ return doc ? { ...toSummary(doc), nextRun: res.nextRun ?? null } : {
110
+ id: res.id, name: input.name, task: input.task, taskType: input.taskType,
111
+ status: "active", cronExpression: input.cronExpression ?? null,
112
+ fireAt: input.fireAt ?? null, nextRun: res.nextRun ?? null,
113
+ };
114
+ }
115
+
116
+ // Sin scheduler: se persiste igual y el próximo arranque la recoge.
117
+ const c = await jobsCol();
118
+ const id = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
119
+ const now = new Date().toISOString();
120
+ const doc = {
121
+ id,
122
+ name: input.name,
123
+ task: input.task,
124
+ task_type: input.taskType,
125
+ cron_expression: input.cronExpression ?? null,
126
+ fire_at: input.fireAt ?? null,
127
+ timezone,
128
+ start_at: input.startAt ?? null,
129
+ stop_at: input.stopAt ?? null,
130
+ dom_and_dow: input.domAndDow ? 1 : 0,
131
+ max_runs: input.maxRuns ?? null,
132
+ agent_id: input.agentId ?? "",
133
+ channel: input.channel ?? "webchat",
134
+ payload_json: JSON.stringify(input.payload ?? { prompt: input.task }),
135
+ tool_name: input.toolName ?? null,
136
+ status: "active",
137
+ created_at: now,
138
+ updated_at: now,
139
+ } as unknown as CronJobDoc;
140
+
141
+ await c.put(id, doc, { expectedVersion: 0 });
142
+ log.info(`tarea "${input.name}" creada sin scheduler (${id}) — se activará al próximo arranque`);
143
+ return toSummary(doc);
144
+ }
145
+
146
+ export async function listCronJobs(status?: CronJobDoc["status"]): Promise<CronJobSummary[]> {
147
+ const rows = await (await jobsCol()).scan({});
148
+ return rows
149
+ .map((e) => e.doc)
150
+ .filter((d) => (status ? d.status === status : true))
151
+ .map(toSummary);
152
+ }
153
+
154
+ export async function getCronJob(id: string): Promise<CronJobSummary | null> {
155
+ const entry = await (await jobsCol()).get(id);
156
+ return entry ? toSummary(entry.doc) : null;
157
+ }
158
+
159
+ /** Cambia el estado de una tarea. `false` si no existe. */
160
+ async function setStatus(id: string, status: CronJobDoc["status"]): Promise<boolean> {
161
+ const c = await jobsCol();
162
+ const entry = await c.get(id);
163
+ if (!entry) return false;
164
+ await c.put(id, { ...entry.doc, status, updated_at: new Date().toISOString() } as CronJobDoc,
165
+ { expectedVersion: entry.version });
166
+ return true;
167
+ }
168
+
169
+ export interface UpdateCronInput {
170
+ name?: string;
171
+ task?: string;
172
+ cronExpression?: string;
173
+ fireAt?: string;
174
+ timezone?: string;
175
+ channel?: string;
176
+ maxRuns?: number | null;
177
+ }
178
+
179
+ /**
180
+ * Edita una tarea. Con scheduler se delega en él, porque cambiar la expresión
181
+ * cron exige recalcular la próxima ejecución y rearmar el timer — hacerlo sólo
182
+ * en la BD dejaría la tarea corriendo con el horario viejo hasta el reinicio.
183
+ */
184
+ export async function updateCronJob(id: string, changes: UpdateCronInput): Promise<CronJobSummary> {
185
+ const scheduler = getSchedulerInstance();
186
+
187
+ if (scheduler) {
188
+ const ok = await scheduler.update(id, {
189
+ name: changes.name,
190
+ task: changes.task,
191
+ cron_expression: changes.cronExpression,
192
+ fire_at: changes.fireAt,
193
+ timezone: changes.timezone,
194
+ channel: changes.channel,
195
+ max_runs: changes.maxRuns,
196
+ });
197
+ if (!ok) throw new Error(`No existe la tarea "${id}"`);
198
+ } else {
199
+ const c = await jobsCol();
200
+ const entry = await c.get(id);
201
+ if (!entry) throw new Error(`No existe la tarea "${id}"`);
202
+
203
+ const doc = { ...entry.doc, updated_at: new Date().toISOString() } as CronJobDoc;
204
+ if (changes.name !== undefined) doc.name = changes.name;
205
+ if (changes.task !== undefined) doc.task = changes.task;
206
+ if (changes.cronExpression !== undefined) doc.cron_expression = changes.cronExpression;
207
+ if (changes.fireAt !== undefined) doc.fire_at = changes.fireAt;
208
+ if (changes.maxRuns !== undefined) (doc as { max_runs?: number | null }).max_runs = changes.maxRuns;
209
+ await c.put(id, doc, { expectedVersion: entry.version });
210
+ }
211
+
212
+ const actualizada = await getCronJob(id);
213
+ if (!actualizada) throw new Error(`No existe la tarea "${id}"`);
214
+ return actualizada;
215
+ }
216
+
217
+ export async function pauseCronJob(id: string): Promise<boolean> {
218
+ const scheduler = getSchedulerInstance();
219
+ if (scheduler) return await scheduler.pause(id);
220
+ return setStatus(id, "paused");
221
+ }
222
+
223
+ export async function resumeCronJob(id: string): Promise<boolean> {
224
+ const scheduler = getSchedulerInstance();
225
+ if (scheduler) return await scheduler.resume(id);
226
+ return setStatus(id, "active");
227
+ }
228
+
229
+ export async function deleteCronJob(id: string): Promise<boolean> {
230
+ const scheduler = getSchedulerInstance();
231
+ if (scheduler) return await scheduler.delete(id);
232
+
233
+ const c = await jobsCol();
234
+ if (!(await c.get(id))) return false;
235
+ await c.delete(id);
236
+ return true;
237
+ }
238
+
239
+ /**
240
+ * Dispara la tarea ahora. Requiere scheduler: sin él no hay nada que ejecute,
241
+ * y devolver `true` sería mentir.
242
+ */
243
+ export function triggerCronJob(id: string): boolean {
244
+ const scheduler = getSchedulerInstance();
245
+ if (!scheduler) throw new Error("Disparar una tarea requiere un scheduler activo");
246
+ return scheduler.trigger(id);
247
+ }
248
+
249
+ export async function getCronHistory(id: string, limit = 50): Promise<TaskRunDoc[]> {
250
+ const rows = await (await col<TaskRunDoc>("taskRuns")).scan({});
251
+ return rows
252
+ .map((e) => e.doc)
253
+ .filter((r) => (r as { task_id?: string }).task_id === id)
254
+ .sort((a, b) => String((b as { started_at?: string }).started_at ?? "")
255
+ .localeCompare(String((a as { started_at?: string }).started_at ?? "")))
256
+ .slice(0, limit);
257
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Endpoints HTTP como herramientas — lo más cerca de "crear una tool desde la UI".
3
+ *
4
+ * Una tool normal es código con un `execute`, y desde una interfaz no hay dónde
5
+ * ponerlo. Este módulo invierte el problema: el usuario aporta **datos** —URL,
6
+ * método, cabeceras, qué parámetros acepta— y el ejecutor es genérico y vive
7
+ * acá. Así alguien suma una capacidad propia sin escribir código en el SDK y sin
8
+ * levantar un servidor MCP, que eran las dos únicas vías.
9
+ *
10
+ * Al registrarse, un endpoint hace tres cosas:
11
+ * 1. guarda su definición (`apiEndpoints`),
12
+ * 2. cifra sus credenciales aparte, y
13
+ * 3. escribe una fila en `tools` y reindexa, para que `search_knowledge` lo
14
+ * descubra. Sin ese último paso el modelo nunca sabría que existe: el
15
+ * loadout inicial es mínimo por diseño.
16
+ *
17
+ * La credencial nunca vuelve al llamador ni aparece en el resultado de una
18
+ * ejecución — es el motivo por el que un endpoint es más seguro que darle al
19
+ * modelo una `api_request` con la clave escrita en el prompt.
20
+ */
21
+
22
+ import { col } from "../storage/hive.ts";
23
+ import type { ApiEndpointDoc, ToolDoc } from "../storage/collections.ts";
24
+ import { storeSecret, loadSecret, deleteSecret } from "../storage/crypto.ts";
25
+ import { apiRequestTool } from "../tools/api/api-request.ts";
26
+ import { registerAppTool } from "../tools/index.ts";
27
+ import { syncToolCatalogToIndex } from "../agent/tool-selector.ts";
28
+ import type { Tool } from "../tools/types.ts";
29
+ import { slugify } from "./agents.ts";
30
+ import { logger } from "../utils/logger.ts";
31
+
32
+ const log = logger.child("services/endpoints");
33
+
34
+ const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
35
+
36
+ /** Las cabeceras con credenciales viven en el secret store, no en la fila. */
37
+ const secretKey = (id: string) => `endpoint:${id}:headers`;
38
+
39
+ export interface EndpointSummary {
40
+ id: string;
41
+ /** El nombre con el que el modelo la llama. */
42
+ toolName: string;
43
+ name: string;
44
+ description: string;
45
+ method: string;
46
+ url: string;
47
+ headers: Record<string, string>;
48
+ query: Record<string, string>;
49
+ bodyTemplate: string | null;
50
+ paramSchema: Record<string, unknown> | null;
51
+ /** Qué cabeceras secretas hay configuradas — nunca sus valores. */
52
+ secretHeaderNames: string[];
53
+ enabled: boolean;
54
+ }
55
+
56
+ export interface CreateEndpointInput {
57
+ name: string;
58
+ description: string;
59
+ url: string;
60
+ method?: string;
61
+ /** Cabeceras visibles (Content-Type, Accept…). */
62
+ headers?: Record<string, string>;
63
+ /** Cabeceras con credenciales: se cifran y no vuelven a salir. */
64
+ secretHeaders?: Record<string, string>;
65
+ query?: Record<string, string>;
66
+ bodyTemplate?: string | null;
67
+ /** JSON Schema de los parámetros que el modelo puede pasar. */
68
+ paramSchema?: Record<string, unknown> | null;
69
+ enabled?: boolean;
70
+ }
71
+
72
+ function parseObj(raw: string | null): Record<string, string> {
73
+ try {
74
+ return raw ? (JSON.parse(raw) as Record<string, string>) : {};
75
+ } catch {
76
+ return {};
77
+ }
78
+ }
79
+
80
+ /** El nombre de la tool se deriva del id: `endpoint_<slug>`. */
81
+ export const toolNameFor = (id: string) => `endpoint_${id}`;
82
+
83
+ async function endpointsCol() {
84
+ return col<ApiEndpointDoc>("apiEndpoints");
85
+ }
86
+
87
+ async function toSummary(doc: ApiEndpointDoc): Promise<EndpointSummary> {
88
+ const secretos = await loadSecret(secretKey(doc.id)).catch(() => null);
89
+ let nombres: string[] = [];
90
+ try {
91
+ nombres = secretos ? Object.keys(JSON.parse(secretos)) : [];
92
+ } catch {
93
+ nombres = [];
94
+ }
95
+ return {
96
+ id: doc.id,
97
+ toolName: toolNameFor(doc.id),
98
+ name: doc.name,
99
+ description: doc.description,
100
+ method: doc.method,
101
+ url: doc.url,
102
+ headers: parseObj(doc.headers_json),
103
+ query: parseObj(doc.query_json),
104
+ bodyTemplate: doc.body_template,
105
+ paramSchema: doc.param_schema_json ? JSON.parse(doc.param_schema_json) : null,
106
+ secretHeaderNames: nombres,
107
+ enabled: doc.enabled,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Reemplaza `{{param}}` por lo que el modelo haya pasado.
113
+ *
114
+ * Los valores se serializan como JSON cuando no son strings, para que un número
115
+ * o un booleano no terminen como `[object Object]` dentro del cuerpo.
116
+ */
117
+ function fillTemplate(template: string, params: Record<string, unknown>): string {
118
+ return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => {
119
+ const v = params[key];
120
+ if (v === undefined) return "";
121
+ return typeof v === "string" ? v : JSON.stringify(v);
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Construye la tool ejecutable de un endpoint.
127
+ *
128
+ * El ejecutor es `api_request`: no se reimplementa el cliente HTTP, con su
129
+ * manejo de timeouts, redacción y detección de content-type. Lo que agrega esta
130
+ * capa es resolver la credencial cifrada en el momento de llamar, de modo que
131
+ * nunca pase por el prompt ni por el resultado.
132
+ */
133
+ export function buildEndpointTool(ep: EndpointSummary): Tool {
134
+ return {
135
+ name: ep.toolName,
136
+ description: ep.description,
137
+ parameters: (ep.paramSchema as Tool["parameters"]) ?? { type: "object", properties: {} },
138
+ execute: async (params: Record<string, unknown>) => {
139
+ const secretos = await loadSecret(secretKey(ep.id)).catch(() => null);
140
+ let secretHeaders: Record<string, string> = {};
141
+ try {
142
+ secretHeaders = secretos ? JSON.parse(secretos) : {};
143
+ } catch {
144
+ secretHeaders = {};
145
+ }
146
+
147
+ const url = new URL(fillTemplate(ep.url, params));
148
+ for (const [k, v] of Object.entries(ep.query)) url.searchParams.set(k, fillTemplate(v, params));
149
+
150
+ return apiRequestTool.execute({
151
+ method: ep.method,
152
+ url: url.toString(),
153
+ headers: { ...ep.headers, ...secretHeaders },
154
+ ...(ep.bodyTemplate ? { body: fillTemplate(ep.bodyTemplate, params) } : {}),
155
+ });
156
+ },
157
+ };
158
+ }
159
+
160
+ /** Registra en el proceso las tools de todos los endpoints habilitados. */
161
+ export async function registerEndpointTools(): Promise<number> {
162
+ const eps = await listEndpoints();
163
+ for (const ep of eps) registerAppTool(buildEndpointTool(ep));
164
+ return eps.length;
165
+ }
166
+
167
+ /** La fila en `tools` es lo que hace que `search_knowledge` lo encuentre. */
168
+ async function upsertToolRow(ep: EndpointSummary): Promise<void> {
169
+ const c = await col<ToolDoc>("tools");
170
+ const name = ep.toolName;
171
+ const existing = await c.get(name);
172
+ const now = Date.now();
173
+ await c.put(name, {
174
+ id: name,
175
+ name,
176
+ description: ep.description,
177
+ category: "api",
178
+ enabled: true,
179
+ active: ep.enabled,
180
+ created_at: existing?.doc.created_at ?? now,
181
+ updated_at: now,
182
+ }, { expectedVersion: existing?.version ?? 0 });
183
+ await syncToolCatalogToIndex().catch((e) => log.warn(`no pude reindexar: ${(e as Error).message}`));
184
+ }
185
+
186
+ export async function createEndpoint(input: CreateEndpointInput): Promise<EndpointSummary> {
187
+ if (!input.name?.trim()) throw new Error("El endpoint necesita un nombre");
188
+ if (!input.description?.trim()) {
189
+ throw new Error("El endpoint necesita una descripción: es lo que el modelo lee para decidir si le sirve");
190
+ }
191
+ if (!input.url?.trim()) throw new Error("El endpoint necesita una URL");
192
+
193
+ const method = (input.method ?? "GET").toUpperCase();
194
+ if (!ALLOWED_METHODS.includes(method)) throw new Error(`Método no permitido: ${method}`);
195
+
196
+ const c = await endpointsCol();
197
+ const id = slugify(input.name);
198
+ if (await c.get(id)) throw new Error(`Ya existe un endpoint con id "${id}"`);
199
+
200
+ const now = Date.now();
201
+ const doc: ApiEndpointDoc = {
202
+ id,
203
+ name: input.name,
204
+ description: input.description,
205
+ method,
206
+ url: input.url,
207
+ headers_json: input.headers ? JSON.stringify(input.headers) : null,
208
+ query_json: input.query ? JSON.stringify(input.query) : null,
209
+ body_template: input.bodyTemplate ?? null,
210
+ param_schema_json: input.paramSchema ? JSON.stringify(input.paramSchema) : null,
211
+ enabled: input.enabled ?? true,
212
+ created_at: now,
213
+ updated_at: now,
214
+ };
215
+
216
+ await c.put(id, doc, { expectedVersion: 0 });
217
+ if (input.secretHeaders) await storeSecret(secretKey(id), JSON.stringify(input.secretHeaders));
218
+
219
+ const summary = await toSummary(doc);
220
+ await upsertToolRow(summary);
221
+ registerAppTool(buildEndpointTool(summary));
222
+ log.info(`endpoint "${input.name}" registrado como tool ${summary.toolName}`);
223
+ return summary;
224
+ }
225
+
226
+ export async function getEndpoint(id: string): Promise<EndpointSummary | null> {
227
+ const entry = await (await endpointsCol()).get(id);
228
+ return entry ? toSummary(entry.doc) : null;
229
+ }
230
+
231
+ export async function listEndpoints(opts?: { includeDisabled?: boolean }): Promise<EndpointSummary[]> {
232
+ const rows = await (await endpointsCol()).scan({});
233
+ const docs = rows.map((e) => e.doc).filter((d) => (opts?.includeDisabled ? true : d.enabled));
234
+ return Promise.all(docs.map(toSummary));
235
+ }
236
+
237
+ export async function updateEndpoint(id: string, changes: Partial<CreateEndpointInput>): Promise<EndpointSummary> {
238
+ const c = await endpointsCol();
239
+ const entry = await c.get(id);
240
+ if (!entry) throw new Error(`No existe el endpoint "${id}"`);
241
+
242
+ const doc: ApiEndpointDoc = { ...entry.doc, updated_at: Date.now() };
243
+ if (changes.name !== undefined) doc.name = changes.name;
244
+ if (changes.description !== undefined) doc.description = changes.description;
245
+ if (changes.url !== undefined) doc.url = changes.url;
246
+ if (changes.method !== undefined) {
247
+ const m = changes.method.toUpperCase();
248
+ if (!ALLOWED_METHODS.includes(m)) throw new Error(`Método no permitido: ${m}`);
249
+ doc.method = m;
250
+ }
251
+ if (changes.headers !== undefined) doc.headers_json = JSON.stringify(changes.headers);
252
+ if (changes.query !== undefined) doc.query_json = JSON.stringify(changes.query);
253
+ if (changes.bodyTemplate !== undefined) doc.body_template = changes.bodyTemplate;
254
+ if (changes.paramSchema !== undefined) {
255
+ doc.param_schema_json = changes.paramSchema ? JSON.stringify(changes.paramSchema) : null;
256
+ }
257
+ if (changes.enabled !== undefined) doc.enabled = changes.enabled;
258
+
259
+ await c.put(id, doc, { expectedVersion: entry.version });
260
+ if (changes.secretHeaders) await storeSecret(secretKey(id), JSON.stringify(changes.secretHeaders));
261
+
262
+ const summary = await toSummary(doc);
263
+ await upsertToolRow(summary);
264
+ registerAppTool(buildEndpointTool(summary));
265
+ return summary;
266
+ }
267
+
268
+ export const toggleEndpoint = (id: string, enabled: boolean) => updateEndpoint(id, { enabled });
269
+
270
+ /**
271
+ * Llama al endpoint sin pasar por el modelo, para que una UI pueda probarlo
272
+ * antes de dejárselo a un agente.
273
+ */
274
+ export async function testEndpoint(id: string, params: Record<string, unknown> = {}): Promise<unknown> {
275
+ const ep = await getEndpoint(id);
276
+ if (!ep) throw new Error(`No existe el endpoint "${id}"`);
277
+ return buildEndpointTool(ep).execute(params);
278
+ }
279
+
280
+ export async function deleteEndpoint(id: string): Promise<boolean> {
281
+ const c = await endpointsCol();
282
+ if (!(await c.get(id))) return false;
283
+
284
+ await c.delete(id);
285
+ await deleteSecret(secretKey(id)).catch(() => {});
286
+ await (await col<ToolDoc>("tools")).delete(toolNameFor(id)).catch(() => {});
287
+ await syncToolCatalogToIndex().catch(() => {});
288
+ return true;
289
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Código de ética — la API, no la ruta.
3
+ *
4
+ * Es el texto que se le inyecta al agente como marco obligatorio y que no puede
5
+ * ignorar. En hive el CRUD vive inline en `gateway/routes/ethics.ts`.
6
+ *
7
+ * `is_default` marca la plantilla que trae el sistema: se puede desactivar, pero
8
+ * borrarla dejaría la instalación sin ninguna referencia si el usuario no
9
+ * escribió la suya, así que se protege.
10
+ */
11
+
12
+ import { col } from "../storage/hive.ts";
13
+ import type { EthicsDoc } from "../storage/collections.ts";
14
+
15
+ export interface EthicsSummary {
16
+ id: string;
17
+ name: string;
18
+ description: string | null;
19
+ content: string;
20
+ isDefault: boolean;
21
+ enabled: boolean;
22
+ active: boolean;
23
+ }
24
+
25
+ const toSummary = (d: EthicsDoc): EthicsSummary => ({
26
+ id: d.id,
27
+ name: d.name,
28
+ description: d.description,
29
+ content: d.content,
30
+ isDefault: d.is_default,
31
+ enabled: d.enabled,
32
+ active: d.active,
33
+ });
34
+
35
+ async function ethicsCol() {
36
+ return col<EthicsDoc>("ethics");
37
+ }
38
+
39
+ export async function listEthics(opts?: { includeInactive?: boolean }): Promise<EthicsSummary[]> {
40
+ const rows = await (await ethicsCol()).scan({});
41
+ return rows
42
+ .map((e) => e.doc)
43
+ .filter((d) => (opts?.includeInactive ? true : d.active))
44
+ .map(toSummary);
45
+ }
46
+
47
+ export async function getEthics(id: string): Promise<EthicsSummary | null> {
48
+ const entry = await (await ethicsCol()).get(id);
49
+ return entry ? toSummary(entry.doc) : null;
50
+ }
51
+
52
+ export async function createEthics(input: {
53
+ id?: string;
54
+ name: string;
55
+ content: string;
56
+ description?: string | null;
57
+ active?: boolean;
58
+ }): Promise<EthicsSummary> {
59
+ if (!input.name?.trim()) throw new Error("El código de ética necesita un nombre");
60
+ if (!input.content?.trim()) throw new Error("El código de ética necesita contenido");
61
+
62
+ const c = await ethicsCol();
63
+ const id = input.id ?? crypto.randomUUID();
64
+ if (await c.get(id)) throw new Error(`Ya existe un código de ética con id "${id}"`);
65
+
66
+ const doc: EthicsDoc = {
67
+ id,
68
+ name: input.name,
69
+ description: input.description ?? null,
70
+ content: input.content,
71
+ is_default: false,
72
+ enabled: true,
73
+ active: input.active ?? false,
74
+ };
75
+ await c.put(id, doc, { expectedVersion: 0 });
76
+ return toSummary(doc);
77
+ }
78
+
79
+ export async function updateEthics(
80
+ id: string,
81
+ changes: { name?: string; content?: string; description?: string | null; active?: boolean },
82
+ ): Promise<EthicsSummary> {
83
+ const c = await ethicsCol();
84
+ const entry = await c.get(id);
85
+ if (!entry) throw new Error(`No existe el código de ética "${id}"`);
86
+
87
+ const doc: EthicsDoc = { ...entry.doc };
88
+ if (changes.name !== undefined) doc.name = changes.name;
89
+ if (changes.content !== undefined) doc.content = changes.content;
90
+ if (changes.description !== undefined) doc.description = changes.description;
91
+ if (changes.active !== undefined) doc.active = changes.active;
92
+
93
+ await c.put(id, doc, { expectedVersion: entry.version });
94
+ return toSummary(doc);
95
+ }
96
+
97
+ export const toggleEthics = (id: string, active: boolean) => updateEthics(id, { active });
98
+
99
+ /** No borra la plantilla del sistema: dejaría la instalación sin referencia. */
100
+ export async function deleteEthics(id: string): Promise<boolean> {
101
+ const c = await ethicsCol();
102
+ const entry = await c.get(id);
103
+ if (!entry) return false;
104
+ if (entry.doc.is_default) throw new Error("El código de ética por defecto no se puede borrar; desactívalo");
105
+ await c.delete(id);
106
+ return true;
107
+ }