@johpaz/hive-sdk 0.4.6 → 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 +90 -0
- package/README.md +1 -1
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +83 -4
- package/docs/SECURITY-GUARDRAILS.md +21 -1
- package/docs/UPGRADING.md +20 -0
- 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 +3 -1
- package/packages/core/src/agent/capability-search.ts +72 -31
- package/packages/core/src/agent/context-compiler.ts +6 -2
- package/packages/core/src/agent/reflector.ts +23 -10
- 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/causal-events.ts +50 -19
- package/packages/core/src/storage/hive.ts +6 -1
- package/packages/core/src/storage/index.ts +14 -1
- 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,11 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
downloadMediaMessage,
|
|
6
|
-
type WASocket,
|
|
7
|
-
type ConnectionState,
|
|
8
|
-
type WAMessage,
|
|
1
|
+
import type {
|
|
2
|
+
WASocket,
|
|
3
|
+
ConnectionState,
|
|
4
|
+
WAMessage,
|
|
9
5
|
} from "@whiskeysockets/baileys";
|
|
10
6
|
import type { ChannelConfig, IncomingMessage, OutboundMessage } from "./base.ts";
|
|
11
7
|
import { BaseChannel } from "./base.ts";
|
|
@@ -15,18 +11,44 @@ import { homedir } from "node:os";
|
|
|
15
11
|
import { logger } from "../utils/logger.ts";
|
|
16
12
|
import { updateDoc } from "../storage/hive.ts";
|
|
17
13
|
import type { ChannelDoc } from "../storage/collections.ts";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Baileys se carga recién cuando alguien conecta este canal de verdad.
|
|
17
|
+
*
|
|
18
|
+
* Antes se importaba arriba de todo. Como el índice del SDK re-exporta este
|
|
19
|
+
* archivo, cualquiera que importara `@johpaz/hive-sdk` —hive-cloud, por
|
|
20
|
+
* ejemplo, que no usa ningún canal del SDK— se traía Baileys entero y, de
|
|
21
|
+
* paso, el parche de `process.stderr.write` de acá abajo, en cada proceso.
|
|
22
|
+
* Diferido, quien no usa WhatsApp por código QR no paga nada.
|
|
23
|
+
*/
|
|
24
|
+
let baileys: typeof import("@whiskeysockets/baileys") | null = null;
|
|
25
|
+
|
|
26
|
+
async function loadBaileys(): Promise<NonNullable<typeof baileys>> {
|
|
27
|
+
if (!baileys) {
|
|
28
|
+
filterBunWsWarnings();
|
|
29
|
+
baileys = await import("@whiskeysockets/baileys");
|
|
30
|
+
}
|
|
31
|
+
return baileys;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let stderrFiltered = false;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Baileys usa el paquete `ws`, que dispara "[bun] Warning: ws.WebSocket
|
|
38
|
+
* 'upgrade' event is not implemented in bun" y parecidos. Bun los escribe
|
|
39
|
+
* directo a stderr desde código nativo, sin pasar por `process.emitWarning`,
|
|
40
|
+
* así que la única forma de filtrarlos es envolver `process.stderr.write`.
|
|
41
|
+
*/
|
|
42
|
+
function filterBunWsWarnings(): void {
|
|
43
|
+
if (stderrFiltered) return;
|
|
44
|
+
stderrFiltered = true;
|
|
45
|
+
const original = process.stderr.write.bind(process.stderr);
|
|
46
|
+
(process.stderr as any).write = function (chunk: string | Buffer, ...args: unknown[]) {
|
|
47
|
+
const str = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString() : "";
|
|
48
|
+
if (str.includes("[bun] Warning:") && str.includes("not implemented in bun")) return true;
|
|
49
|
+
return original(chunk, ...(args as any[]));
|
|
50
|
+
};
|
|
51
|
+
}
|
|
30
52
|
|
|
31
53
|
export interface WhatsAppConfig extends ChannelConfig {
|
|
32
54
|
accountId: string;
|
|
@@ -114,6 +136,9 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
114
136
|
this.log.info("Connecting to WhatsApp...");
|
|
115
137
|
|
|
116
138
|
try {
|
|
139
|
+
const { default: makeWASocket, useMultiFileAuthState, fetchLatestBaileysVersion } =
|
|
140
|
+
await loadBaileys();
|
|
141
|
+
|
|
117
142
|
const { state, saveCreds } = await useMultiFileAuthState(this.authPath);
|
|
118
143
|
const { version } = await fetchLatestBaileysVersion();
|
|
119
144
|
this.connectionState.waVersion = version.join(".");
|
|
@@ -170,13 +195,13 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
170
195
|
if (qr) {
|
|
171
196
|
this.connectionState.status = "qr";
|
|
172
197
|
this.connectionState.qrCode = qr;
|
|
173
|
-
this.printQR(qr);
|
|
198
|
+
void this.printQR(qr);
|
|
174
199
|
this.log.info("Scan the QR code above with WhatsApp");
|
|
175
200
|
}
|
|
176
201
|
|
|
177
202
|
if (connection === "close") {
|
|
178
203
|
const statusCode = (lastDisconnect?.error as { output?: { statusCode: number } })?.output?.statusCode;
|
|
179
|
-
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
204
|
+
const shouldReconnect = statusCode !== baileys!.DisconnectReason.loggedOut;
|
|
180
205
|
|
|
181
206
|
this.connectionState.status = "disconnected";
|
|
182
207
|
this.log.warn(`WhatsApp disconnected: ${statusCode}`);
|
|
@@ -186,8 +211,8 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
186
211
|
} catch { /* ignore DB errors */ }
|
|
187
212
|
|
|
188
213
|
const needsSessionClear =
|
|
189
|
-
statusCode === DisconnectReason.loggedOut ||
|
|
190
|
-
statusCode === DisconnectReason.badSession;
|
|
214
|
+
statusCode === baileys!.DisconnectReason.loggedOut ||
|
|
215
|
+
statusCode === baileys!.DisconnectReason.badSession;
|
|
191
216
|
// NOTE: 515 (restartRequired) is sent by WhatsApp AFTER a successful pairing
|
|
192
217
|
// to signal Baileys to reconnect with the new credentials — do NOT clear session.
|
|
193
218
|
|
|
@@ -233,11 +258,16 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
233
258
|
return { ...this.connectionState };
|
|
234
259
|
}
|
|
235
260
|
|
|
236
|
-
private printQR(qr: string): void {
|
|
261
|
+
private async printQR(qr: string): Promise<void> {
|
|
237
262
|
this.log.info("\n" + "=".repeat(50));
|
|
238
263
|
this.log.info(" WHATSAPP QR CODE - Scan with your phone");
|
|
239
264
|
this.log.info("=".repeat(50) + "\n");
|
|
240
265
|
|
|
266
|
+
// @ts-ignore — no type definitions for qrcode-terminal
|
|
267
|
+
const qrcodeTerminal = (await import("qrcode-terminal")).default as {
|
|
268
|
+
generate: (text: string, opts: { small: boolean }, cb: (qr: string) => void) => void;
|
|
269
|
+
};
|
|
270
|
+
|
|
241
271
|
qrcodeTerminal.generate(qr, { small: false }, (qrString: string) => {
|
|
242
272
|
this.log.info(qrString);
|
|
243
273
|
});
|
|
@@ -307,7 +337,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
307
337
|
|
|
308
338
|
if (hasAudio && this.socket) {
|
|
309
339
|
try {
|
|
310
|
-
audioBuffer = await downloadMediaMessage(
|
|
340
|
+
audioBuffer = await baileys!.downloadMediaMessage(
|
|
311
341
|
typedMsg as unknown as WAMessage,
|
|
312
342
|
"buffer",
|
|
313
343
|
{},
|
|
@@ -320,7 +350,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
320
350
|
|
|
321
351
|
if (hasImage && this.socket) {
|
|
322
352
|
try {
|
|
323
|
-
imageBuffer = await downloadMediaMessage(
|
|
353
|
+
imageBuffer = await baileys!.downloadMediaMessage(
|
|
324
354
|
typedMsg as unknown as WAMessage,
|
|
325
355
|
"buffer",
|
|
326
356
|
{},
|
|
@@ -336,7 +366,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
336
366
|
|
|
337
367
|
if (hasDocument && this.socket) {
|
|
338
368
|
try {
|
|
339
|
-
documentBuffer = await downloadMediaMessage(
|
|
369
|
+
documentBuffer = await baileys!.downloadMediaMessage(
|
|
340
370
|
typedMsg as unknown as WAMessage,
|
|
341
371
|
"buffer",
|
|
342
372
|
{},
|
|
@@ -6,7 +6,7 @@ import { availableParallelism, homedir } from "node:os";
|
|
|
6
6
|
|
|
7
7
|
const LogLevelSchema = z.enum(["debug", "info", "warn", "error"]);
|
|
8
8
|
const DMPolicySchema = z.enum(["open", "pairing", "allowlist"]);
|
|
9
|
-
const TransportSchema = z.enum(["stdio", "sse", "websocket"]);
|
|
9
|
+
const TransportSchema = z.enum(["stdio", "sse", "websocket", "http"]);
|
|
10
10
|
|
|
11
11
|
export function loadEnv(hiveDir: string): void {
|
|
12
12
|
const envPath = path.join(hiveDir, ".env");
|
|
@@ -559,6 +559,8 @@ function buildDefaultConfig(): Config {
|
|
|
559
559
|
slack: 40000,
|
|
560
560
|
webchat: 100000,
|
|
561
561
|
whatsapp: 65536,
|
|
562
|
+
// Meta rechaza cualquier `text.body` de más de 4096 caracteres.
|
|
563
|
+
whatsapp_cloud: 4096,
|
|
562
564
|
},
|
|
563
565
|
skillScanning: true,
|
|
564
566
|
warnOnInsecureConfig: true,
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* - POST /chat — chat with an agent
|
|
6
6
|
* - GET /status — health check
|
|
7
7
|
* - WebSocket /ws — real-time streaming
|
|
8
|
+
* - GET|POST /webhooks/whatsapp-cloud/:accountId — WhatsApp por la API oficial
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { logger } from "../utils/logger.ts";
|
|
11
12
|
import { runAgent } from "../agent/agent-loop.ts";
|
|
12
13
|
import type { MCPClientManager } from "../mcp/index.ts";
|
|
14
|
+
import type { ChannelManager } from "../channels/manager.ts";
|
|
13
15
|
|
|
14
16
|
const log = logger.child("gateway");
|
|
15
17
|
|
|
@@ -18,6 +20,12 @@ export interface GatewayConfig {
|
|
|
18
20
|
port?: number;
|
|
19
21
|
agentId?: string;
|
|
20
22
|
mcpManager?: MCPClientManager | null;
|
|
23
|
+
/**
|
|
24
|
+
* Hace falta para los canales que reciben por webhook, como WhatsApp por la
|
|
25
|
+
* API oficial de Meta. Sin él, el gateway no tiene a quién entregarle lo que
|
|
26
|
+
* Meta empuja.
|
|
27
|
+
*/
|
|
28
|
+
channelManager?: ChannelManager | null;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
export async function startGateway(config: GatewayConfig = {}) {
|
|
@@ -54,6 +62,22 @@ export async function startGateway(config: GatewayConfig = {}) {
|
|
|
54
62
|
return handleChat(req, agentId, config.mcpManager);
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
// WhatsApp por la API oficial: Meta empuja acá cada mensaje del número.
|
|
66
|
+
// El gateway escucha en 127.0.0.1 y Meta exige HTTPS público, así que
|
|
67
|
+
// esta ruta necesita un proxy inverso o un túnel por delante.
|
|
68
|
+
const whatsappCloud = url.pathname.match(/^\/webhooks\/whatsapp-cloud\/([^/]+)$/);
|
|
69
|
+
if (whatsappCloud) {
|
|
70
|
+
if (!config.channelManager) {
|
|
71
|
+
log.warn("llegó un webhook de WhatsApp pero el gateway no tiene ChannelManager");
|
|
72
|
+
return new Response("Channels not configured", { status: 503 });
|
|
73
|
+
}
|
|
74
|
+
return config.channelManager.handleWebhook(
|
|
75
|
+
"whatsapp_cloud",
|
|
76
|
+
decodeURIComponent(whatsappCloud[1]!),
|
|
77
|
+
req
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
return new Response("Not Found", { status: 404 });
|
|
58
82
|
},
|
|
59
83
|
websocket: {
|
|
@@ -84,6 +84,23 @@ export { BaseChannel } from "./channels/base.ts";
|
|
|
84
84
|
export { TelegramChannel } from "./channels/telegram.ts";
|
|
85
85
|
export { DiscordChannel } from "./channels/discord.ts";
|
|
86
86
|
export { WhatsAppChannel } from "./channels/whatsapp.ts";
|
|
87
|
+
export {
|
|
88
|
+
WhatsAppCloudChannel,
|
|
89
|
+
WhatsAppCloudClient,
|
|
90
|
+
WhatsAppCloudError,
|
|
91
|
+
splitWhatsAppText,
|
|
92
|
+
parseWebhook as parseWhatsAppWebhook,
|
|
93
|
+
verifySignature as verifyWhatsAppSignature,
|
|
94
|
+
verifyChallenge as verifyWhatsAppChallenge,
|
|
95
|
+
} from "./channels/whatsapp-cloud/index.ts";
|
|
96
|
+
export type {
|
|
97
|
+
WhatsAppCloudConfig,
|
|
98
|
+
WhatsAppTemplate,
|
|
99
|
+
WhatsAppInbound,
|
|
100
|
+
WhatsAppWebhookEvent,
|
|
101
|
+
WhatsAppStatusEvent,
|
|
102
|
+
WhatsAppReferral,
|
|
103
|
+
} from "./channels/whatsapp-cloud/index.ts";
|
|
87
104
|
export { SlackChannel } from "./channels/slack.ts";
|
|
88
105
|
export { WebChatChannel } from "./channels/webchat.ts";
|
|
89
106
|
|
|
@@ -183,6 +183,15 @@ export class MCPClientManager {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
case "http": {
|
|
187
|
+
const url = state.config.url;
|
|
188
|
+
if (!url) throw new Error("Streamable HTTP transport requires 'url' config");
|
|
189
|
+
return createTransport({
|
|
190
|
+
type: "http",
|
|
191
|
+
http: { url, headers: state.config.headers },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
default:
|
|
187
196
|
throw new Error(`Unknown transport type: ${transportType}`);
|
|
188
197
|
}
|
|
@@ -3,7 +3,8 @@ export interface MCPConfig {
|
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export interface MCPServerConfig {
|
|
6
|
-
|
|
6
|
+
/** "http" es Streamable HTTP, el transporte remoto estándar de MCP. "sse" es el HTTP+SSE original, de dos endpoints. */
|
|
7
|
+
transport: "stdio" | "sse" | "websocket" | "http";
|
|
7
8
|
enabled?: boolean;
|
|
8
9
|
command?: string;
|
|
9
10
|
args?: string[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
2
3
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
4
|
|
|
4
5
|
// CORRECCIÓN 1 — quitar extensión .ts de los imports
|
|
@@ -18,13 +19,37 @@ export interface StdioTransportConfig {
|
|
|
18
19
|
env?: Record<string, string>;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Streamable HTTP: el transporte remoto ESTÁNDAR de MCP hoy.
|
|
24
|
+
*
|
|
25
|
+
* No es lo mismo que `sse`. Aquel es el transporte HTTP+SSE original, de dos
|
|
26
|
+
* endpoints (un GET que abre el stream y un POST aparte para enviar), que la
|
|
27
|
+
* especificación reemplazó por éste: un único endpoint que acepta POST y
|
|
28
|
+
* devuelve JSON o un stream SSE según haga falta.
|
|
29
|
+
*
|
|
30
|
+
* Sin este caso, un servidor MCP remoto moderno es inalcanzable: no expone el
|
|
31
|
+
* `/sse` de dos endpoints que espera `SSETransport` y la conexión muere en un
|
|
32
|
+
* 404. Se descubrió intentando conectar un navegador headless que sólo sirve
|
|
33
|
+
* Streamable HTTP, pero no es un caso particular de ese servidor: es lo que
|
|
34
|
+
* publica hoy cualquier MCP remoto.
|
|
35
|
+
*/
|
|
36
|
+
export interface HttpTransportConfig {
|
|
37
|
+
/** URL del endpoint MCP, normalmente terminado en `/mcp`. */
|
|
38
|
+
url: string;
|
|
39
|
+
/** Cabeceras extra en cada petición — típicamente la autorización. */
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
/** Sesión previa a reanudar, si el servidor emitió un id de sesión. */
|
|
42
|
+
sessionId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type TransportType = "stdio" | "sse" | "websocket" | "http";
|
|
22
46
|
|
|
23
47
|
export interface TransportOptions {
|
|
24
48
|
type: TransportType;
|
|
25
49
|
stdio?: StdioTransportConfig;
|
|
26
50
|
sse?: SSETransportConfig;
|
|
27
51
|
websocket?: WebSocketTransportConfig;
|
|
52
|
+
http?: HttpTransportConfig;
|
|
28
53
|
}
|
|
29
54
|
|
|
30
55
|
export function createTransport(options: TransportOptions): Transport {
|
|
@@ -58,6 +83,24 @@ export function createTransport(options: TransportOptions): Transport {
|
|
|
58
83
|
return new WebSocketTransport(options.websocket);
|
|
59
84
|
}
|
|
60
85
|
|
|
86
|
+
case "http": {
|
|
87
|
+
if (!options.http) {
|
|
88
|
+
throw new Error("http config required for Streamable HTTP transport");
|
|
89
|
+
}
|
|
90
|
+
// La implementación la pone el SDK oficial de MCP, que ya es dependencia
|
|
91
|
+
// nuestra: no hay nada que reescribir acá, sólo faltaba el caso.
|
|
92
|
+
let url: URL;
|
|
93
|
+
try {
|
|
94
|
+
url = new URL(options.http.url);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error(`Invalid MCP endpoint URL: ${options.http.url}`);
|
|
97
|
+
}
|
|
98
|
+
return new StreamableHTTPClientTransport(url, {
|
|
99
|
+
requestInit: options.http.headers ? { headers: options.http.headers } : undefined,
|
|
100
|
+
sessionId: options.http.sessionId,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
61
104
|
default: {
|
|
62
105
|
// exhaustive check — TypeScript avisa si falta un caso
|
|
63
106
|
const _exhaustive: never = options.type;
|