@johpaz/hive-sdk 0.4.7 → 0.4.8
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.
- package/CHANGELOG.md +68 -0
- package/README.md +1 -1
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +81 -2
- package/docs/SECURITY-GUARDRAILS.md +21 -1
- package/package.json +1 -1
- package/packages/cli/templates/hive-app/hive.config.ts +7 -0
- package/packages/cli/templates/hive-app/src/main.ts +3 -0
- package/packages/core/src/agent/capability-search.ts +72 -31
- package/packages/core/src/channels/index.ts +1 -0
- package/packages/core/src/channels/manager.ts +40 -0
- package/packages/core/src/channels/whatsapp-cloud/channel.ts +334 -0
- package/packages/core/src/channels/whatsapp-cloud/client.ts +368 -0
- package/packages/core/src/channels/whatsapp-cloud/index.ts +3 -0
- package/packages/core/src/channels/whatsapp-cloud/webhook.ts +235 -0
- package/packages/core/src/channels/whatsapp.ts +58 -28
- package/packages/core/src/config/loader.ts +3 -1
- package/packages/core/src/gateway/server.ts +24 -0
- package/packages/core/src/index.ts +17 -0
- package/packages/core/src/mcp/MCPClient.ts +9 -0
- package/packages/core/src/mcp/config.ts +2 -1
- package/packages/core/src/mcp/transports/index.ts +44 -1
- package/packages/core/src/storage/catalog.ts +354 -0
- package/packages/core/src/storage/hive.ts +6 -1
- package/packages/core/src/storage/index.ts +13 -0
- package/packages/core/src/storage/seed.ts +161 -128
- package/packages/core/src/tools/cron/index.ts +2 -2
- package/packages/core/src/voice/index.ts +2 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catálogo compartido, activación por inquilino.
|
|
3
|
+
*
|
|
4
|
+
* ## El problema
|
|
5
|
+
*
|
|
6
|
+
* `tools`, `skills` y `ethics` guardan dos cosas en la misma fila: el CONTENIDO
|
|
7
|
+
* —el nombre de la tool, el cuerpo de la skill, el texto de la regla— y la
|
|
8
|
+
* ELECCIÓN de quien la usa, en `active`/`enabled`. Mientras hubo una instalación
|
|
9
|
+
* por usuario eso era correcto y simple.
|
|
10
|
+
*
|
|
11
|
+
* Con varios inquilinos en una sola HiveDB deja de serlo. `ensureHiveDb()`
|
|
12
|
+
* siembra los catálogos en la partición de CADA enjambre, así que las mismas 62
|
|
13
|
+
* tools y las mismas skills se copian tantas veces como enjambres haya, y se
|
|
14
|
+
* vuelven a escribir en cada arranque. El contenido es idéntico en todas: la
|
|
15
|
+
* única diferencia real entre dos particiones es qué eligió cada una.
|
|
16
|
+
*
|
|
17
|
+
* ## La separación
|
|
18
|
+
*
|
|
19
|
+
* **El contenido vive una sola vez**, en la colección sin prefijo de inquilino
|
|
20
|
+
* —la misma que ve la app de escritorio—, y lo escribe quien instala: el seed
|
|
21
|
+
* del SDK o, en Hive Cloud, la sincronización desde Postgres.
|
|
22
|
+
*
|
|
23
|
+
* **La elección vive en el inquilino**, en `catalogActivations`: una fila por
|
|
24
|
+
* elemento tocado, con `active`/`enabled`. Sin fila, se hereda lo que diga el
|
|
25
|
+
* catálogo. Un enjambre nuevo arranca entonces con CERO escrituras.
|
|
26
|
+
*
|
|
27
|
+
* **Lo propio del inquilino sigue siendo suyo.** No todo lo que cae en estas
|
|
28
|
+
* colecciones es catálogo: las tools de un endpoint de API (`services/endpoints`)
|
|
29
|
+
* y las skills o códigos de ética que alguien cree se escriben en la partición,
|
|
30
|
+
* como siempre. Compartir la colección entera se los filtraría a los demás
|
|
31
|
+
* inquilinos, que es exactamente lo que el prefijo vino a impedir.
|
|
32
|
+
*
|
|
33
|
+
* {@link catalogView} compone las tres capas detrás de la misma interfaz de
|
|
34
|
+
* colección, así que los ~30 sitios que hacen `col("tools")` no se enteran.
|
|
35
|
+
*
|
|
36
|
+
* Sin inquilino en scope nada de esto se activa: `col()` devuelve la colección
|
|
37
|
+
* de siempre y el modo local se comporta exactamente igual que antes.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import type { DocEntry, PutDocOptions, ScanOptions } from "@johpaz/hive-db";
|
|
41
|
+
import { getHiveDb } from "./hivedb.ts";
|
|
42
|
+
import { currentTenant, qualify } from "./tenant.ts";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Las colecciones cuyo contenido es catálogo de la instalación y no dato de un
|
|
46
|
+
* inquilino.
|
|
47
|
+
*
|
|
48
|
+
* `providers` y `models` NO están acá, y es deliberado: lo que baja a la
|
|
49
|
+
* partición de un enjambre no es el catálogo entero sino el subconjunto que su
|
|
50
|
+
* workspace configuró, con su `base_url` y su `context_window`. Ahí la fila por
|
|
51
|
+
* inquilino ES el dato correcto; lo que sobraba era que el SDK sembrara además
|
|
52
|
+
* su catálogo estático encima (ver `seedAllData`).
|
|
53
|
+
*/
|
|
54
|
+
export const CATALOG_COLLECTIONS = new Set(["tools", "skills", "ethics"]);
|
|
55
|
+
|
|
56
|
+
/** Dónde vive la elección de cada inquilino. Se prefija como cualquier otra. */
|
|
57
|
+
const ACTIVATIONS = "catalogActivations";
|
|
58
|
+
|
|
59
|
+
/** Campos que son elección del inquilino y no contenido del catálogo. */
|
|
60
|
+
const ACTIVATION_FIELDS = ["active", "enabled"] as const;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Lo que un inquilino decidió sobre una fila del catálogo.
|
|
64
|
+
*
|
|
65
|
+
* `hidden` cubre el borrado: un inquilino no puede borrar una fila compartida
|
|
66
|
+
* —es de todos—, pero sí sacarla de su vista. Es lo que permite que
|
|
67
|
+
* `pruneRetired()` y el borrado de una tool de endpoint sigan funcionando
|
|
68
|
+
* dentro de una partición sin tocar a nadie más.
|
|
69
|
+
*/
|
|
70
|
+
interface ActivationDoc {
|
|
71
|
+
id: string;
|
|
72
|
+
collection: string;
|
|
73
|
+
item_id: string;
|
|
74
|
+
active: boolean;
|
|
75
|
+
enabled: boolean;
|
|
76
|
+
hidden: boolean;
|
|
77
|
+
updated_at: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* La parte de `Collection` que usa el SDK.
|
|
82
|
+
*
|
|
83
|
+
* Existe porque `Collection` de hive-db es una clase con campos privados: nada
|
|
84
|
+
* que no salga de `db.collection()` puede hacerse pasar por ella, ni siquiera
|
|
85
|
+
* implementando los mismos métodos. La clase real satisface esta interfaz tal
|
|
86
|
+
* cual, así que tipar `col()` con ella no cambia nada para quien la recibe.
|
|
87
|
+
*/
|
|
88
|
+
export interface DocStore<T> {
|
|
89
|
+
put(id: string, doc: T, options?: PutDocOptions): Promise<number>;
|
|
90
|
+
get(id: string): Promise<DocEntry<T> | undefined>;
|
|
91
|
+
delete(id: string): Promise<boolean>;
|
|
92
|
+
scan(options?: ScanOptions): Promise<DocEntry<T>[]>;
|
|
93
|
+
count(): Promise<number>;
|
|
94
|
+
createIndex(field: string, options?: { unique?: boolean }): Promise<void>;
|
|
95
|
+
findBy(field: string, value: string | number | boolean, options?: ScanOptions): Promise<DocEntry<T>[]>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function activationId(collection: string, itemId: string): string {
|
|
99
|
+
return `${collection}:${itemId}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Aplica la elección del inquilino sobre la fila compartida. */
|
|
103
|
+
function withActivation<T>(doc: T, overlay: ActivationDoc | undefined): T {
|
|
104
|
+
if (!overlay) return doc;
|
|
105
|
+
const merged = { ...(doc as Record<string, unknown>) };
|
|
106
|
+
if ("active" in merged) merged.active = overlay.active;
|
|
107
|
+
if ("enabled" in merged) merged.enabled = overlay.enabled;
|
|
108
|
+
return merged as T;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `true` si lo único que cambia entre las dos filas es la elección.
|
|
113
|
+
*
|
|
114
|
+
* `updated_at` se ignora a propósito: todos los caminos de activación lo tocan
|
|
115
|
+
* (`toggleTool`, `applySeedPlan`, `updateSkill`), y tomarlo como contenido haría
|
|
116
|
+
* que encender una tool se guardara como una copia entera del catálogo por
|
|
117
|
+
* inquilino — justo lo que esto viene a evitar.
|
|
118
|
+
*/
|
|
119
|
+
function soloCambiaLaEleccion(nuevo: unknown, compartido: unknown): boolean {
|
|
120
|
+
const ignorar = new Set<string>([...ACTIVATION_FIELDS, "updated_at"]);
|
|
121
|
+
const a = nuevo as Record<string, unknown>;
|
|
122
|
+
const b = compartido as Record<string, unknown>;
|
|
123
|
+
const claves = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
124
|
+
for (const clave of claves) {
|
|
125
|
+
if (ignorar.has(clave)) continue;
|
|
126
|
+
if (JSON.stringify(a[clave]) !== JSON.stringify(b[clave])) return false;
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ordenarPorId<T>(entries: DocEntry<T>[]): DocEntry<T>[] {
|
|
132
|
+
return entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function aplicarOpciones<T>(entries: DocEntry<T>[], options: ScanOptions = {}): DocEntry<T>[] {
|
|
136
|
+
let salida = ordenarPorId(entries);
|
|
137
|
+
if (options.prefix) salida = salida.filter((e) => e.id.startsWith(options.prefix!));
|
|
138
|
+
if (options.start) salida = salida.filter((e) => e.id >= options.start!);
|
|
139
|
+
if (options.reverse) salida = salida.reverse();
|
|
140
|
+
if (options.offset) salida = salida.slice(options.offset);
|
|
141
|
+
if (options.limit != null) salida = salida.slice(0, options.limit);
|
|
142
|
+
return salida;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Vista de una colección de catálogo para el inquilino activo: lo compartido
|
|
147
|
+
* con su elección aplicada, más lo que el inquilino haya creado.
|
|
148
|
+
*
|
|
149
|
+
* **Versiones.** Para una fila compartida se devuelve la versión de la fila
|
|
150
|
+
* compartida, y `put()` la ignora al escribir la activación (que lleva su propia
|
|
151
|
+
* versión). El control optimista sigue siendo real dentro de cada capa; lo que
|
|
152
|
+
* no hay es una versión única que cubra a las dos a la vez. Los escritores de
|
|
153
|
+
* activación son toggles de usuario —leer, cambiar un booleano, escribir—, así
|
|
154
|
+
* que lo que se pierde es la detección de dos toggles simultáneos sobre el mismo
|
|
155
|
+
* elemento del mismo inquilino, y lo que se gana es no copiar el catálogo.
|
|
156
|
+
*/
|
|
157
|
+
export function catalogView<T>(
|
|
158
|
+
compartida: DocStore<T>,
|
|
159
|
+
propia: DocStore<T>,
|
|
160
|
+
activaciones: DocStore<ActivationDoc>,
|
|
161
|
+
nombre: string,
|
|
162
|
+
): DocStore<T> {
|
|
163
|
+
const leerActivacion = async (id: string): Promise<ActivationDoc | undefined> =>
|
|
164
|
+
(await activaciones.get(activationId(nombre, id)))?.doc;
|
|
165
|
+
|
|
166
|
+
const escribirActivacion = async (id: string, doc: T): Promise<void> => {
|
|
167
|
+
const actual = await activaciones.get(activationId(nombre, id));
|
|
168
|
+
const fila = doc as Record<string, unknown>;
|
|
169
|
+
await activaciones.put(activationId(nombre, id), {
|
|
170
|
+
id: activationId(nombre, id),
|
|
171
|
+
collection: nombre,
|
|
172
|
+
item_id: id,
|
|
173
|
+
active: fila.active === true,
|
|
174
|
+
enabled: "enabled" in fila ? fila.enabled === true : true,
|
|
175
|
+
hidden: false,
|
|
176
|
+
updated_at: Date.now(),
|
|
177
|
+
}, actual ? { expectedVersion: actual.version } : { expectedVersion: 0 });
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
async get(id) {
|
|
182
|
+
const local = await propia.get(id);
|
|
183
|
+
if (local) return local;
|
|
184
|
+
const compartido = await compartida.get(id);
|
|
185
|
+
if (!compartido) return undefined;
|
|
186
|
+
const overlay = await leerActivacion(id);
|
|
187
|
+
if (overlay?.hidden) return undefined;
|
|
188
|
+
return { ...compartido, doc: withActivation(compartido.doc, overlay) };
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
async put(id, doc, options) {
|
|
192
|
+
const local = await propia.get(id);
|
|
193
|
+
if (local) return propia.put(id, doc, options);
|
|
194
|
+
|
|
195
|
+
const compartido = await compartida.get(id);
|
|
196
|
+
if (compartido) {
|
|
197
|
+
if (soloCambiaLaEleccion(doc, compartido.doc)) {
|
|
198
|
+
await escribirActivacion(id, doc);
|
|
199
|
+
return compartido.version;
|
|
200
|
+
}
|
|
201
|
+
// El inquilino editó el CONTENIDO de una fila del catálogo: se queda con
|
|
202
|
+
// su propia copia. `expectedVersion` venía de la fila compartida y acá
|
|
203
|
+
// no aplica —la copia todavía no existe—, así que no se reenvía.
|
|
204
|
+
return propia.put(id, doc);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return propia.put(id, doc, options);
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
async delete(id) {
|
|
211
|
+
const local = await propia.get(id);
|
|
212
|
+
if (local) {
|
|
213
|
+
await activaciones.delete(activationId(nombre, id)).catch(() => false);
|
|
214
|
+
return propia.delete(id);
|
|
215
|
+
}
|
|
216
|
+
const compartido = await compartida.get(id);
|
|
217
|
+
if (!compartido) return false;
|
|
218
|
+
const actual = await activaciones.get(activationId(nombre, id));
|
|
219
|
+
if (actual?.doc.hidden) return false;
|
|
220
|
+
await activaciones.put(activationId(nombre, id), {
|
|
221
|
+
id: activationId(nombre, id),
|
|
222
|
+
collection: nombre,
|
|
223
|
+
item_id: id,
|
|
224
|
+
active: false,
|
|
225
|
+
enabled: false,
|
|
226
|
+
hidden: true,
|
|
227
|
+
updated_at: Date.now(),
|
|
228
|
+
}, actual ? { expectedVersion: actual.version } : { expectedVersion: 0 });
|
|
229
|
+
return true;
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
async scan(options) {
|
|
233
|
+
const [compartidos, propios, overlays] = await Promise.all([
|
|
234
|
+
compartida.scan({}),
|
|
235
|
+
propia.scan({}),
|
|
236
|
+
activaciones.scan({ prefix: `${nombre}:` }),
|
|
237
|
+
]);
|
|
238
|
+
const porItem = new Map(overlays.map((e) => [e.doc.item_id, e.doc]));
|
|
239
|
+
|
|
240
|
+
const porId = new Map<string, DocEntry<T>>();
|
|
241
|
+
for (const entrada of compartidos) {
|
|
242
|
+
const overlay = porItem.get(entrada.id);
|
|
243
|
+
if (overlay?.hidden) continue;
|
|
244
|
+
porId.set(entrada.id, { ...entrada, doc: withActivation(entrada.doc, overlay) });
|
|
245
|
+
}
|
|
246
|
+
// Lo propio gana: si el inquilino editó una fila del catálogo, su copia es
|
|
247
|
+
// la que vale para él.
|
|
248
|
+
for (const entrada of propios) porId.set(entrada.id, entrada);
|
|
249
|
+
|
|
250
|
+
return aplicarOpciones([...porId.values()], options);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
async count() {
|
|
254
|
+
return (await this.scan({})).length;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
async createIndex(field, options) {
|
|
258
|
+
// Los índices de la colección compartida los crea el bootstrap sin
|
|
259
|
+
// inquilino, una sola vez. Acá sólo corresponde el de la partición.
|
|
260
|
+
await propia.createIndex(field, options);
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
async findBy(field, value, options) {
|
|
264
|
+
// Sobre la vista combinada no hay un índice del motor que cubra las dos
|
|
265
|
+
// capas, así que se filtra en memoria. El catálogo son decenas de filas,
|
|
266
|
+
// no millones, y el único llamador es la búsqueda de skills por categoría.
|
|
267
|
+
const todas = await this.scan({});
|
|
268
|
+
const filtradas = todas.filter(
|
|
269
|
+
(e) => (e.doc as Record<string, unknown>)[field] === value,
|
|
270
|
+
);
|
|
271
|
+
return aplicarOpciones(filtradas, options);
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Handle a la colección de catálogo compartida (sin prefijo de inquilino). */
|
|
277
|
+
export async function sharedCatalogCol<T>(name: string): Promise<DocStore<T>> {
|
|
278
|
+
const db = await getHiveDb();
|
|
279
|
+
return db.collection<T>(name);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* La vista de catálogo de esta colección para el inquilino activo, ya cableada.
|
|
284
|
+
*
|
|
285
|
+
* Es lo que devuelve `col()` cuando corresponde; vive acá para que `hive.ts` no
|
|
286
|
+
* tenga que conocer ni la colección de activaciones ni cómo se componen las
|
|
287
|
+
* capas.
|
|
288
|
+
*/
|
|
289
|
+
export async function catalogCol<T>(name: string): Promise<DocStore<T>> {
|
|
290
|
+
const db = await getHiveDb();
|
|
291
|
+
return catalogView<T>(
|
|
292
|
+
db.collection<T>(name),
|
|
293
|
+
db.collection<T>(qualify(name)),
|
|
294
|
+
db.collection<ActivationDoc>(qualify(ACTIVATIONS)),
|
|
295
|
+
name,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Enciende o apaga un elemento del catálogo para el inquilino activo, sin tocar
|
|
301
|
+
* la fila compartida.
|
|
302
|
+
*
|
|
303
|
+
* Es lo que usa un host multi-inquilino —Hive Cloud— para bajar a cada enjambre
|
|
304
|
+
* lo que su workspace activó, en vez de escribirle una copia del catálogo.
|
|
305
|
+
*/
|
|
306
|
+
export async function setCatalogActivation(
|
|
307
|
+
collection: string,
|
|
308
|
+
itemId: string,
|
|
309
|
+
eleccion: { active: boolean; enabled?: boolean },
|
|
310
|
+
): Promise<void> {
|
|
311
|
+
if (!CATALOG_COLLECTIONS.has(collection)) {
|
|
312
|
+
throw new Error(`setCatalogActivation: "${collection}" no es una colección de catálogo`);
|
|
313
|
+
}
|
|
314
|
+
const db = await getHiveDb();
|
|
315
|
+
const activaciones = db.collection<ActivationDoc>(qualify(ACTIVATIONS));
|
|
316
|
+
const id = activationId(collection, itemId);
|
|
317
|
+
const actual = await activaciones.get(id);
|
|
318
|
+
await activaciones.put(id, {
|
|
319
|
+
id,
|
|
320
|
+
collection,
|
|
321
|
+
item_id: itemId,
|
|
322
|
+
active: eleccion.active,
|
|
323
|
+
enabled: eleccion.enabled ?? eleccion.active,
|
|
324
|
+
hidden: false,
|
|
325
|
+
updated_at: Date.now(),
|
|
326
|
+
}, actual ? { expectedVersion: actual.version } : { expectedVersion: 0 });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Devuelve al inquilino a lo que diga el catálogo para ese elemento. */
|
|
330
|
+
export async function clearCatalogActivation(collection: string, itemId: string): Promise<boolean> {
|
|
331
|
+
const db = await getHiveDb();
|
|
332
|
+
const activaciones = db.collection<ActivationDoc>(qualify(ACTIVATIONS));
|
|
333
|
+
return activaciones.delete(activationId(collection, itemId));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Lo que este inquilino tiene decidido sobre una colección del catálogo. */
|
|
337
|
+
export async function listCatalogActivations(
|
|
338
|
+
collection: string,
|
|
339
|
+
): Promise<Array<{ itemId: string; active: boolean; enabled: boolean; hidden: boolean }>> {
|
|
340
|
+
const db = await getHiveDb();
|
|
341
|
+
const activaciones = db.collection<ActivationDoc>(qualify(ACTIVATIONS));
|
|
342
|
+
const filas = await activaciones.scan({ prefix: `${collection}:` });
|
|
343
|
+
return filas.map((e) => ({
|
|
344
|
+
itemId: e.doc.item_id,
|
|
345
|
+
active: e.doc.active,
|
|
346
|
+
enabled: e.doc.enabled,
|
|
347
|
+
hidden: e.doc.hidden,
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** `true` si esta colección se resuelve como catálogo compartido ahora mismo. */
|
|
352
|
+
export function esCatalogoCompartido(name: string): boolean {
|
|
353
|
+
return currentTenant() !== null && CATALOG_COLLECTIONS.has(name);
|
|
354
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { getHiveDb } from "./hivedb.ts";
|
|
11
11
|
import { qualify } from "./tenant.ts";
|
|
12
|
+
import { catalogCol, esCatalogoCompartido, type DocStore } from "./catalog.ts";
|
|
12
13
|
|
|
13
14
|
const MAX_RETRIES = 5;
|
|
14
15
|
|
|
@@ -46,7 +47,11 @@ export function fromIndexable(value: string | null | undefined): string | null {
|
|
|
46
47
|
* El handle se construye en cada llamada a propósito: cachearlo en una variable
|
|
47
48
|
* de módulo lo dejaría atado al tenant que lo creó primero.
|
|
48
49
|
*/
|
|
49
|
-
export async function col<T>(name: string) {
|
|
50
|
+
export async function col<T>(name: string): Promise<DocStore<T>> {
|
|
51
|
+
// El catálogo (`tools`, `skills`, `ethics`) es contenido de la instalación, no
|
|
52
|
+
// de un inquilino: con tenant activo se sirve compartido, con la elección de
|
|
53
|
+
// este inquilino aplicada encima. Ver storage/catalog.ts.
|
|
54
|
+
if (esCatalogoCompartido(name)) return catalogCol<T>(name);
|
|
50
55
|
const db = await getHiveDb();
|
|
51
56
|
return db.collection<T>(qualify(name));
|
|
52
57
|
}
|
|
@@ -41,6 +41,19 @@ export {
|
|
|
41
41
|
BROADCAST,
|
|
42
42
|
} from "./hive.ts";
|
|
43
43
|
|
|
44
|
+
// ─── Catálogo compartido y activación por inquilino ──────────────────────────
|
|
45
|
+
// El contenido del catálogo (tools, skills, ética) se instala una sola vez; cada
|
|
46
|
+
// inquilino guarda sólo lo que activó — ver storage/catalog.ts.
|
|
47
|
+
export {
|
|
48
|
+
CATALOG_COLLECTIONS,
|
|
49
|
+
setCatalogActivation,
|
|
50
|
+
clearCatalogActivation,
|
|
51
|
+
listCatalogActivations,
|
|
52
|
+
sharedCatalogCol,
|
|
53
|
+
esCatalogoCompartido,
|
|
54
|
+
} from "./catalog.ts";
|
|
55
|
+
export type { DocStore } from "./catalog.ts";
|
|
56
|
+
|
|
44
57
|
// ─── Shapes de documento ─────────────────────────────────────────────────────
|
|
45
58
|
export type * from "./collections.ts";
|
|
46
59
|
|