@johpaz/hive-sdk 0.4.7 → 0.4.9
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 +91 -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/agent-loop.ts +10 -2
- package/packages/core/src/agent/capability-search.ts +72 -31
- package/packages/core/src/agent/compaction.ts +81 -35
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { col, toIndexable, nextId } from "./hive.ts"
|
|
2
|
-
import type {
|
|
2
|
+
import type { DocStore } from "./catalog.ts"
|
|
3
|
+
import { currentTenant } from "./tenant.ts"
|
|
3
4
|
import { logger } from "../utils/logger.ts"
|
|
4
5
|
import { catalogModelKey } from "./model-id.ts"
|
|
5
6
|
import { invalidateModelPricingCache } from "./usage.ts"
|
|
@@ -444,6 +445,7 @@ export const SEED_DATA: SeedData = {
|
|
|
444
445
|
{ id: "telegram", type: "telegram" },
|
|
445
446
|
{ id: "discord", type: "discord" },
|
|
446
447
|
{ id: "slack", type: "slack" },
|
|
448
|
+
{ id: "whatsapp_cloud", type: "whatsapp_cloud" },
|
|
447
449
|
{ id: "whatsapp", type: "whatsapp" },
|
|
448
450
|
],
|
|
449
451
|
|
|
@@ -486,7 +488,7 @@ import { expandToolAllowlist } from "../agent/delegation-runtime.ts"
|
|
|
486
488
|
const log = logger.child("seed");
|
|
487
489
|
|
|
488
490
|
/** Insert-only-if-absent — the HiveDB equivalent of SQL `INSERT OR IGNORE`. */
|
|
489
|
-
async function putIfAbsent<T>(c:
|
|
491
|
+
async function putIfAbsent<T>(c: DocStore<T>, id: string, doc: T): Promise<void> {
|
|
490
492
|
if (!(await c.get(id))) await c.put(id, doc)
|
|
491
493
|
}
|
|
492
494
|
|
|
@@ -791,146 +793,177 @@ export interface SeedOptions {
|
|
|
791
793
|
specialists?: SpecialistSeedMode
|
|
792
794
|
}
|
|
793
795
|
|
|
796
|
+
/**
|
|
797
|
+
* Resiembra el catálogo estático de providers y modelos.
|
|
798
|
+
*
|
|
799
|
+
* Vive aparte porque con inquilino activo NO se llama. En un host
|
|
800
|
+
* multi-inquilino el catálogo que vale es el del host —Hive Cloud le baja a cada
|
|
801
|
+
* enjambre los providers que su workspace configuró, con su `base_url` y su
|
|
802
|
+
* `context_window`—, así que resembrar el estático encima significaba borrar y
|
|
803
|
+
* recrear los 139 modelos de `SEED_DATA` en la partición de cada enjambre, en
|
|
804
|
+
* cada arranque, para que un instante después los pisara Postgres.
|
|
805
|
+
*/
|
|
806
|
+
async function reseedProvidersAndModels(): Promise<void> {
|
|
807
|
+
const now = Date.now();
|
|
808
|
+
|
|
809
|
+
// 4️⃣ Providers
|
|
810
|
+
const providersCol = await col<ProviderDoc>("providers");
|
|
811
|
+
let providerCount = 0;
|
|
812
|
+
for (const provider of SEED_DATA.providers) {
|
|
813
|
+
await putIfAbsent(providersCol, provider.id, {
|
|
814
|
+
id: provider.id, name: provider.name, base_url: provider.baseUrl || null,
|
|
815
|
+
category: (provider.category || "llm") as ProviderDoc["category"],
|
|
816
|
+
num_ctx: null, num_gpu: -1, enabled: false, active: false, created_at: now,
|
|
817
|
+
});
|
|
818
|
+
providerCount++;
|
|
819
|
+
}
|
|
820
|
+
// If OLLAMA_HOST is set (e.g. Docker pointing to host machine), always update Ollama's base_url
|
|
821
|
+
const ollamaHost = process.env.OLLAMA_HOST;
|
|
822
|
+
if (ollamaHost) {
|
|
823
|
+
const ollama = await providersCol.get("ollama");
|
|
824
|
+
if (ollama) {
|
|
825
|
+
await providersCol.put("ollama", { ...ollama.doc, base_url: ollamaHost }, { expectedVersion: ollama.version });
|
|
826
|
+
log.info(`[seed] ✅ Ollama base_url set to ${ollamaHost} (from OLLAMA_HOST env)`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// Older DBs were seeded with enabled=true hardcoded regardless of the user's own
|
|
830
|
+
// activation (active). enabled/active are otherwise always kept in sync by every
|
|
831
|
+
// mutation path (toggle, update, voice key save), so reconciling them here undoes
|
|
832
|
+
// exactly that stale default without touching providers the user genuinely activated.
|
|
833
|
+
let reconciledCount = 0;
|
|
834
|
+
for (const row of await providersCol.scan({})) {
|
|
835
|
+
if (row.doc.enabled !== row.doc.active) {
|
|
836
|
+
await providersCol.put(row.id, { ...row.doc, enabled: row.doc.active }, { expectedVersion: row.version });
|
|
837
|
+
reconciledCount++;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (reconciledCount > 0) {
|
|
841
|
+
log.info(`[seed] 🔧 Reconciled ${reconciledCount} provider(s) with a stale enabled≠active state`);
|
|
842
|
+
}
|
|
843
|
+
log.info(`[seed] ✅ ${providerCount} providers procesados`);
|
|
844
|
+
|
|
845
|
+
// 5️⃣ Models — wipe & recreate.
|
|
846
|
+
//
|
|
847
|
+
// Actualizar el catálogo es editar SEED_DATA.models y arrancar: las filas de
|
|
848
|
+
// catálogo se borran enteras y se vuelven a crear, así ningún campo viejo
|
|
849
|
+
// (context_window, capabilities, precio) sobrevive a una entrada renombrada
|
|
850
|
+
// o corregida. Un upsert en sitio no daba esa garantía.
|
|
851
|
+
//
|
|
852
|
+
// Dos cosas sí se preservan a propósito:
|
|
853
|
+
// - enabled/active por id, para no desactivar el modelo que el usuario
|
|
854
|
+
// eligió cada vez que se publica un catálogo nuevo;
|
|
855
|
+
// - las filas source != "catalog" (Ollama tags, /v1/models), que no tienen
|
|
856
|
+
// origen canónico desde el que recrearse.
|
|
857
|
+
log.info("[seed] 🔄 Re-seeding models (wipe & recreate)...");
|
|
858
|
+
const modelsCol = await col<ModelDoc>("models");
|
|
859
|
+
const beforeWipe = await modelsCol.scan({});
|
|
860
|
+
|
|
861
|
+
const activationState = new Map(
|
|
862
|
+
beforeWipe.map((e) => [e.id, { enabled: e.doc.enabled, active: e.doc.active }])
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
let wipedModels = 0;
|
|
866
|
+
for (const row of beforeWipe) {
|
|
867
|
+
if (row.doc.source !== "catalog") continue;
|
|
868
|
+
await modelsCol.delete(row.id);
|
|
869
|
+
wipedModels++;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
let modelCount = 0;
|
|
873
|
+
for (const model of SEED_DATA.models) {
|
|
874
|
+
const key = catalogModelKey(model.providerId, model.id);
|
|
875
|
+
const previous = activationState.get(key);
|
|
876
|
+
await modelsCol.put(key, {
|
|
877
|
+
id: key, provider_id: model.providerId, name: model.name,
|
|
878
|
+
model_type: model.modelType as ModelDoc["model_type"],
|
|
879
|
+
context_window: model.contextWindow || 0, capabilities: model.capabilities || null,
|
|
880
|
+
enabled: previous?.enabled ?? true, active: previous?.active ?? false,
|
|
881
|
+
source: "catalog",
|
|
882
|
+
input_per_1m: model.inputPer1M ?? null,
|
|
883
|
+
output_per_1m: model.outputPer1M ?? null,
|
|
884
|
+
});
|
|
885
|
+
modelCount++;
|
|
886
|
+
}
|
|
887
|
+
log.info(`[seed] 🗑️ ${wipedModels} modelo(s) de catálogo borrados y recreados desde SEED_DATA`);
|
|
888
|
+
invalidateModelPricingCache();
|
|
889
|
+
|
|
890
|
+
// An agent pointing at a model that just got removed would otherwise keep
|
|
891
|
+
// a dangling model_id — unlink it so loadAgentConfigFromDB() falls back to
|
|
892
|
+
// getDefaultLLM() instead of resolving a model_id that isn't there.
|
|
893
|
+
const liveModelIds = new Set((await modelsCol.scan({})).map((e) => e.id));
|
|
894
|
+
const agentsCol = await col<AgentDoc>("agents");
|
|
895
|
+
const allAgents = await agentsCol.scan({});
|
|
896
|
+
let unlinkedCount = 0;
|
|
897
|
+
for (const a of allAgents) {
|
|
898
|
+
if (a.doc.model_id && a.doc.model_id !== "__none__" && !liveModelIds.has(a.doc.model_id)) {
|
|
899
|
+
await agentsCol.put(a.id, { ...a.doc, model_id: toIndexable(null) }, { expectedVersion: a.version });
|
|
900
|
+
unlinkedCount++;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (unlinkedCount > 0) {
|
|
904
|
+
log.info(`[seed] 🔗 Unlinked ${unlinkedCount} agent(s) from model(s) no longer in the catalog`);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Los canales guardan el id del modelo de voz, no una FK: si el modelo salió
|
|
908
|
+
// del catálogo hay que repuntarlos a mano o la voz queda rota en silencio.
|
|
909
|
+
const voiceChannelsCol = await col<ChannelDoc>("channels");
|
|
910
|
+
let migratedVoice = 0;
|
|
911
|
+
for (const c of await voiceChannelsCol.scan({})) {
|
|
912
|
+
const patch: Partial<ChannelDoc> = {};
|
|
913
|
+
for (const field of ["stt_provider", "tts_provider"] as const) {
|
|
914
|
+
const current = c.doc[field];
|
|
915
|
+
if (current && current in RETIRED_VOICE_MODELS) {
|
|
916
|
+
patch[field] = RETIRED_VOICE_MODELS[current];
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (Object.keys(patch).length === 0) continue;
|
|
920
|
+
await voiceChannelsCol.put(c.id, { ...c.doc, ...patch }, { expectedVersion: c.version });
|
|
921
|
+
migratedVoice++;
|
|
922
|
+
log.info(`[seed] 🎙️ Canal ${c.id}: modelo de voz deprecado migrado ${JSON.stringify(patch)}`);
|
|
923
|
+
}
|
|
924
|
+
if (migratedVoice > 0) {
|
|
925
|
+
log.info(`[seed] 🎙️ ${migratedVoice} canal(es) repuntados a modelos de voz vigentes`);
|
|
926
|
+
}
|
|
927
|
+
log.info(`[seed] ✅ ${modelCount} models procesados`);
|
|
928
|
+
}
|
|
929
|
+
|
|
794
930
|
export async function seedAllData(opts?: SeedOptions): Promise<void> {
|
|
795
931
|
log.info("[seed] 🌱 Iniciando seed de datos predeterminados...")
|
|
796
932
|
const especialistas = opts?.specialists ?? "all"
|
|
797
933
|
|
|
798
|
-
|
|
934
|
+
// El catálogo —tools, skills y ética— es contenido de la INSTALACIÓN y vive
|
|
935
|
+
// una sola vez: con inquilino activo `col()` lo sirve compartido y cada
|
|
936
|
+
// enjambre guarda únicamente lo que activó (ver storage/catalog.ts).
|
|
937
|
+
// Resembrarlo acá escribiría una copia por enjambre, que es exactamente lo que
|
|
938
|
+
// esa separación viene a quitar. Lo mismo con providers y modelos, que en un
|
|
939
|
+
// host multi-inquilino los pone el host.
|
|
940
|
+
const enInquilino = currentTenant() !== null;
|
|
941
|
+
|
|
942
|
+
if (!enInquilino) await reseedToolsAndSkills(especialistas);
|
|
799
943
|
|
|
800
944
|
try {
|
|
801
945
|
const now = Date.now();
|
|
802
946
|
|
|
803
947
|
// 3️⃣ Ethics templates (globales)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
log.info(`[seed] ✅ ${ethicsCount} ethics templates procesados`);
|
|
814
|
-
|
|
815
|
-
// 4️⃣ Providers
|
|
816
|
-
const providersCol = await col<ProviderDoc>("providers");
|
|
817
|
-
let providerCount = 0;
|
|
818
|
-
for (const provider of SEED_DATA.providers) {
|
|
819
|
-
await putIfAbsent(providersCol, provider.id, {
|
|
820
|
-
id: provider.id, name: provider.name, base_url: provider.baseUrl || null,
|
|
821
|
-
category: (provider.category || "llm") as ProviderDoc["category"],
|
|
822
|
-
num_ctx: null, num_gpu: -1, enabled: false, active: false, created_at: now,
|
|
823
|
-
});
|
|
824
|
-
providerCount++;
|
|
825
|
-
}
|
|
826
|
-
// If OLLAMA_HOST is set (e.g. Docker pointing to host machine), always update Ollama's base_url
|
|
827
|
-
const ollamaHost = process.env.OLLAMA_HOST;
|
|
828
|
-
if (ollamaHost) {
|
|
829
|
-
const ollama = await providersCol.get("ollama");
|
|
830
|
-
if (ollama) {
|
|
831
|
-
await providersCol.put("ollama", { ...ollama.doc, base_url: ollamaHost }, { expectedVersion: ollama.version });
|
|
832
|
-
log.info(`[seed] ✅ Ollama base_url set to ${ollamaHost} (from OLLAMA_HOST env)`);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
// Older DBs were seeded with enabled=true hardcoded regardless of the user's own
|
|
836
|
-
// activation (active). enabled/active are otherwise always kept in sync by every
|
|
837
|
-
// mutation path (toggle, update, voice key save), so reconciling them here undoes
|
|
838
|
-
// exactly that stale default without touching providers the user genuinely activated.
|
|
839
|
-
let reconciledCount = 0;
|
|
840
|
-
for (const row of await providersCol.scan({})) {
|
|
841
|
-
if (row.doc.enabled !== row.doc.active) {
|
|
842
|
-
await providersCol.put(row.id, { ...row.doc, enabled: row.doc.active }, { expectedVersion: row.version });
|
|
843
|
-
reconciledCount++;
|
|
948
|
+
if (!enInquilino) {
|
|
949
|
+
const ethicsCol = await col<EthicsDoc>("ethics");
|
|
950
|
+
let ethicsCount = 0;
|
|
951
|
+
for (const ethics of SEED_DATA.ethics) {
|
|
952
|
+
await putIfAbsent(ethicsCol, ethics.id, {
|
|
953
|
+
id: ethics.id, name: ethics.name, description: ethics.description, content: ethics.content,
|
|
954
|
+
is_default: ethics.isDefault, enabled: true, active: ethics.isDefault,
|
|
955
|
+
});
|
|
956
|
+
ethicsCount++;
|
|
844
957
|
}
|
|
958
|
+
log.info(`[seed] ✅ ${ethicsCount} ethics templates procesados`);
|
|
845
959
|
}
|
|
846
|
-
if (reconciledCount > 0) {
|
|
847
|
-
log.info(`[seed] 🔧 Reconciled ${reconciledCount} provider(s) with a stale enabled≠active state`);
|
|
848
|
-
}
|
|
849
|
-
log.info(`[seed] ✅ ${providerCount} providers procesados`);
|
|
850
960
|
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
// Actualizar el catálogo es editar SEED_DATA.models y arrancar: las filas de
|
|
854
|
-
// catálogo se borran enteras y se vuelven a crear, así ningún campo viejo
|
|
855
|
-
// (context_window, capabilities, precio) sobrevive a una entrada renombrada
|
|
856
|
-
// o corregida. Un upsert en sitio no daba esa garantía.
|
|
857
|
-
//
|
|
858
|
-
// Dos cosas sí se preservan a propósito:
|
|
859
|
-
// - enabled/active por id, para no desactivar el modelo que el usuario
|
|
860
|
-
// eligió cada vez que se publica un catálogo nuevo;
|
|
861
|
-
// - las filas source != "catalog" (Ollama tags, /v1/models), que no tienen
|
|
862
|
-
// origen canónico desde el que recrearse.
|
|
863
|
-
log.info("[seed] 🔄 Re-seeding models (wipe & recreate)...");
|
|
864
|
-
const modelsCol = await col<ModelDoc>("models");
|
|
865
|
-
const beforeWipe = await modelsCol.scan({});
|
|
866
|
-
|
|
867
|
-
const activationState = new Map(
|
|
868
|
-
beforeWipe.map((e) => [e.id, { enabled: e.doc.enabled, active: e.doc.active }])
|
|
869
|
-
);
|
|
870
|
-
|
|
871
|
-
let wipedModels = 0;
|
|
872
|
-
for (const row of beforeWipe) {
|
|
873
|
-
if (row.doc.source !== "catalog") continue;
|
|
874
|
-
await modelsCol.delete(row.id);
|
|
875
|
-
wipedModels++;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
let modelCount = 0;
|
|
879
|
-
for (const model of SEED_DATA.models) {
|
|
880
|
-
const key = catalogModelKey(model.providerId, model.id);
|
|
881
|
-
const previous = activationState.get(key);
|
|
882
|
-
await modelsCol.put(key, {
|
|
883
|
-
id: key, provider_id: model.providerId, name: model.name,
|
|
884
|
-
model_type: model.modelType as ModelDoc["model_type"],
|
|
885
|
-
context_window: model.contextWindow || 0, capabilities: model.capabilities || null,
|
|
886
|
-
enabled: previous?.enabled ?? true, active: previous?.active ?? false,
|
|
887
|
-
source: "catalog",
|
|
888
|
-
input_per_1m: model.inputPer1M ?? null,
|
|
889
|
-
output_per_1m: model.outputPer1M ?? null,
|
|
890
|
-
});
|
|
891
|
-
modelCount++;
|
|
892
|
-
}
|
|
893
|
-
log.info(`[seed] 🗑️ ${wipedModels} modelo(s) de catálogo borrados y recreados desde SEED_DATA`);
|
|
894
|
-
invalidateModelPricingCache();
|
|
961
|
+
// 4️⃣ y 5️⃣ Providers y modelos
|
|
962
|
+
if (!enInquilino) await reseedProvidersAndModels();
|
|
895
963
|
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
// getDefaultLLM() instead of resolving a model_id that isn't there.
|
|
899
|
-
const liveModelIds = new Set((await modelsCol.scan({})).map((e) => e.id));
|
|
964
|
+
// Los agentes son del inquilino: cada enjambre tiene los suyos, y por eso
|
|
965
|
+
// esto sí corre siempre. Antes se declaraba dentro del bloque de modelos.
|
|
900
966
|
const agentsCol = await col<AgentDoc>("agents");
|
|
901
|
-
const allAgents = await agentsCol.scan({});
|
|
902
|
-
let unlinkedCount = 0;
|
|
903
|
-
for (const a of allAgents) {
|
|
904
|
-
if (a.doc.model_id && a.doc.model_id !== "__none__" && !liveModelIds.has(a.doc.model_id)) {
|
|
905
|
-
await agentsCol.put(a.id, { ...a.doc, model_id: toIndexable(null) }, { expectedVersion: a.version });
|
|
906
|
-
unlinkedCount++;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
if (unlinkedCount > 0) {
|
|
910
|
-
log.info(`[seed] 🔗 Unlinked ${unlinkedCount} agent(s) from model(s) no longer in the catalog`);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Los canales guardan el id del modelo de voz, no una FK: si el modelo salió
|
|
914
|
-
// del catálogo hay que repuntarlos a mano o la voz queda rota en silencio.
|
|
915
|
-
const voiceChannelsCol = await col<ChannelDoc>("channels");
|
|
916
|
-
let migratedVoice = 0;
|
|
917
|
-
for (const c of await voiceChannelsCol.scan({})) {
|
|
918
|
-
const patch: Partial<ChannelDoc> = {};
|
|
919
|
-
for (const field of ["stt_provider", "tts_provider"] as const) {
|
|
920
|
-
const current = c.doc[field];
|
|
921
|
-
if (current && current in RETIRED_VOICE_MODELS) {
|
|
922
|
-
patch[field] = RETIRED_VOICE_MODELS[current];
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
if (Object.keys(patch).length === 0) continue;
|
|
926
|
-
await voiceChannelsCol.put(c.id, { ...c.doc, ...patch }, { expectedVersion: c.version });
|
|
927
|
-
migratedVoice++;
|
|
928
|
-
log.info(`[seed] 🎙️ Canal ${c.id}: modelo de voz deprecado migrado ${JSON.stringify(patch)}`);
|
|
929
|
-
}
|
|
930
|
-
if (migratedVoice > 0) {
|
|
931
|
-
log.info(`[seed] 🎙️ ${migratedVoice} canal(es) repuntados a modelos de voz vigentes`);
|
|
932
|
-
}
|
|
933
|
-
log.info(`[seed] ✅ ${modelCount} models procesados`);
|
|
934
967
|
|
|
935
968
|
// 6️⃣ MCP servers
|
|
936
969
|
const mcpCol = await col<McpServerDoc>("mcpServers");
|
|
@@ -78,7 +78,7 @@ export async function resolveBestChannel(userId: string, explicitChannel?: strin
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (!bestChannel) {
|
|
81
|
-
const preferred = ["telegram", "discord", "slack", "whatsapp", "webchat"];
|
|
81
|
+
const preferred = ["telegram", "discord", "slack", "whatsapp_cloud", "whatsapp", "webchat"];
|
|
82
82
|
for (const p of preferred) {
|
|
83
83
|
if (identities.includes(p)) {
|
|
84
84
|
bestChannel = p;
|
|
@@ -113,7 +113,7 @@ export const cronCreateTool: Tool = {
|
|
|
113
113
|
agent_id: { type: "string", description: "Target agent ID (optional, defaults to Coordinator)" },
|
|
114
114
|
tool_name: { type: "string", description: "Specific tool to execute (optional)" },
|
|
115
115
|
max_runs: { type: "number", description: "Maximum executions (optional, null = unlimited)" },
|
|
116
|
-
channel: { type: "string", description: "Notification channel (system, telegram, discord, whatsapp, cli)" },
|
|
116
|
+
channel: { type: "string", description: "Notification channel (system, telegram, discord, whatsapp_cloud, whatsapp, cli)" },
|
|
117
117
|
start_at: { type: "string", description: "ISO 8601 datetime: start of execution window. Optional." },
|
|
118
118
|
stop_at: { type: "string", description: "ISO 8601 datetime: end of execution window. Optional." },
|
|
119
119
|
dom_and_dow: { type: "boolean", description: "If true, both day-of-month AND day-of-week must match. Default: false (OR logic)" },
|
|
@@ -593,6 +593,8 @@ class VoiceService {
|
|
|
593
593
|
case "discord":
|
|
594
594
|
return this.normalizeDiscordAudio(audioData);
|
|
595
595
|
case "whatsapp":
|
|
596
|
+
case "whatsapp_cloud":
|
|
597
|
+
// Las notas de voz son ogg/opus en los dos casos.
|
|
596
598
|
return this.normalizeWhatsAppAudio(audioData);
|
|
597
599
|
case "slack":
|
|
598
600
|
return this.normalizeSlackAudio(audioData);
|