@johpaz/hive-core 1.0.7 → 1.0.10

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 (53) hide show
  1. package/package.json +10 -9
  2. package/src/agent/ethics.ts +70 -68
  3. package/src/agent/index.ts +48 -17
  4. package/src/agent/providers/index.ts +11 -5
  5. package/src/agent/soul.ts +19 -15
  6. package/src/agent/user.ts +19 -15
  7. package/src/agent/workspace.ts +6 -6
  8. package/src/agents/index.ts +4 -0
  9. package/src/agents/inter-agent-bus.test.ts +264 -0
  10. package/src/agents/inter-agent-bus.ts +279 -0
  11. package/src/agents/registry.test.ts +275 -0
  12. package/src/agents/registry.ts +273 -0
  13. package/src/agents/router.test.ts +229 -0
  14. package/src/agents/router.ts +251 -0
  15. package/src/agents/team-coordinator.test.ts +401 -0
  16. package/src/agents/team-coordinator.ts +480 -0
  17. package/src/canvas/canvas-manager.test.ts +159 -0
  18. package/src/canvas/canvas-manager.ts +219 -0
  19. package/src/canvas/canvas-tools.ts +189 -0
  20. package/src/canvas/index.ts +2 -0
  21. package/src/channels/whatsapp.ts +12 -12
  22. package/src/config/loader.ts +12 -9
  23. package/src/events/event-bus.test.ts +98 -0
  24. package/src/events/event-bus.ts +171 -0
  25. package/src/gateway/server.ts +131 -35
  26. package/src/index.ts +9 -1
  27. package/src/multi-agent/manager.ts +12 -12
  28. package/src/plugins/api.ts +129 -0
  29. package/src/plugins/index.ts +2 -0
  30. package/src/plugins/loader.test.ts +285 -0
  31. package/src/plugins/loader.ts +363 -0
  32. package/src/resilience/circuit-breaker.test.ts +129 -0
  33. package/src/resilience/circuit-breaker.ts +223 -0
  34. package/src/security/google-chat.test.ts +219 -0
  35. package/src/security/google-chat.ts +269 -0
  36. package/src/security/index.ts +5 -0
  37. package/src/security/pairing.test.ts +302 -0
  38. package/src/security/pairing.ts +250 -0
  39. package/src/security/rate-limit.test.ts +239 -0
  40. package/src/security/rate-limit.ts +270 -0
  41. package/src/security/signal.test.ts +92 -0
  42. package/src/security/signal.ts +321 -0
  43. package/src/state/store.test.ts +190 -0
  44. package/src/state/store.ts +310 -0
  45. package/src/storage/sqlite.ts +3 -3
  46. package/src/tools/cron.ts +42 -2
  47. package/src/tools/dynamic-registry.test.ts +226 -0
  48. package/src/tools/dynamic-registry.ts +258 -0
  49. package/src/tools/fs.test.ts +127 -0
  50. package/src/tools/fs.ts +364 -0
  51. package/src/tools/index.ts +1 -0
  52. package/src/tools/read.ts +23 -19
  53. package/src/utils/logger.ts +112 -33
@@ -0,0 +1,219 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "bun:test";
2
+ import { GoogleChatChannel, type GoogleChatConfig } from "./google-chat.ts";
3
+ import { pairingService } from "./pairing.ts";
4
+ import { eventBus } from "../events/event-bus.ts";
5
+
6
+ describe("GoogleChatChannel", () => {
7
+ let channel: GoogleChatChannel;
8
+ const config: GoogleChatConfig = {
9
+ enabled: true,
10
+ dmPolicy: "pairing",
11
+ allowFrom: [],
12
+ webhookPort: 8888,
13
+ };
14
+
15
+ beforeEach(() => {
16
+ channel = new GoogleChatChannel("default", config);
17
+ pairingService.clear();
18
+ eventBus.removeAllListeners();
19
+ });
20
+
21
+ afterEach(async () => {
22
+ if (channel.isRunning()) {
23
+ await channel.stop();
24
+ }
25
+ });
26
+
27
+ describe("constructor", () => {
28
+ it("should set default values", () => {
29
+ const ch = new GoogleChatChannel("default", { enabled: true } as GoogleChatConfig);
30
+
31
+ expect(ch.config.dmPolicy).toBe("pairing");
32
+ expect(ch.config.webhookPort).toBe(8080);
33
+ expect(ch.config.webhookPath).toBe("/webhook/google-chat");
34
+ });
35
+ });
36
+
37
+ describe("start/stop", () => {
38
+ it("should start webhook server", async () => {
39
+ await channel.start();
40
+
41
+ expect(channel.isRunning()).toBe(true);
42
+
43
+ await channel.stop();
44
+ expect(channel.isRunning()).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe("handleChatMessage", () => {
49
+ it("should handle /myid command", async () => {
50
+ await channel.start();
51
+
52
+ const event = {
53
+ type: "MESSAGE",
54
+ eventTime: new Date().toISOString(),
55
+ space: {
56
+ name: "spaces/test",
57
+ displayName: "Test",
58
+ type: "DM" as const,
59
+ },
60
+ message: {
61
+ name: "spaces/test/messages/123",
62
+ sender: {
63
+ name: "users/123456",
64
+ displayName: "Test User",
65
+ },
66
+ createTime: new Date().toISOString(),
67
+ text: "/myid",
68
+ },
69
+ };
70
+
71
+ const handleChatMessage = (
72
+ channel as unknown as {
73
+ handleChatMessage: (e: typeof event, res: { writeHead: () => void; end: (s: string) => void }) => Promise<void>;
74
+ }
75
+ ).handleChatMessage;
76
+
77
+ const res = {
78
+ writeHead: vi.fn().mockReturnThis(),
79
+ end: vi.fn(),
80
+ };
81
+
82
+ await handleChatMessage.call(channel, event, res);
83
+
84
+ expect(res.writeHead).toHaveBeenCalledWith(200, { "Content-Type": "application/json" });
85
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining("123456"));
86
+ });
87
+
88
+ it("should handle /pair command", async () => {
89
+ await channel.start();
90
+
91
+ const code = pairingService.generateCode("google-chat", "123456");
92
+
93
+ const event = {
94
+ type: "MESSAGE",
95
+ eventTime: new Date().toISOString(),
96
+ space: {
97
+ name: "spaces/test",
98
+ displayName: "Test",
99
+ type: "DM" as const,
100
+ },
101
+ message: {
102
+ name: "spaces/test/messages/123",
103
+ sender: {
104
+ name: "users/123456",
105
+ displayName: "Test User",
106
+ },
107
+ createTime: new Date().toISOString(),
108
+ text: `/pair ${code}`,
109
+ },
110
+ };
111
+
112
+ const handleChatMessage = (
113
+ channel as unknown as {
114
+ handleChatMessage: (e: typeof event, res: { writeHead: () => void; end: (s: string) => void }) => Promise<void>;
115
+ }
116
+ ).handleChatMessage;
117
+
118
+ const res = {
119
+ writeHead: vi.fn().mockReturnThis(),
120
+ end: vi.fn(),
121
+ };
122
+
123
+ await handleChatMessage.call(channel, event, res);
124
+
125
+ expect(res.writeHead).toHaveBeenCalledWith(200, { "Content-Type": "application/json" });
126
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining("exitoso"));
127
+ expect(pairingService.isAllowed("google-chat", "123456")).toBe(true);
128
+ });
129
+
130
+ it("should reject unpaired users", async () => {
131
+ await channel.start();
132
+
133
+ const event = {
134
+ type: "MESSAGE",
135
+ eventTime: new Date().toISOString(),
136
+ space: {
137
+ name: "spaces/test",
138
+ displayName: "Test",
139
+ type: "DM" as const,
140
+ },
141
+ message: {
142
+ name: "spaces/test/messages/123",
143
+ sender: {
144
+ name: "users/999",
145
+ displayName: "Unknown User",
146
+ },
147
+ createTime: new Date().toISOString(),
148
+ text: "Hello",
149
+ },
150
+ };
151
+
152
+ const handleChatMessage = (
153
+ channel as unknown as {
154
+ handleChatMessage: (e: typeof event, res: { writeHead: () => void; end: (s: string) => void }) => Promise<void>;
155
+ }
156
+ ).handleChatMessage;
157
+
158
+ const res = {
159
+ writeHead: vi.fn().mockReturnThis(),
160
+ end: vi.fn(),
161
+ };
162
+
163
+ await handleChatMessage.call(channel, event, res);
164
+
165
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining("No est"));
166
+ });
167
+
168
+ it("should forward messages from allowed users", async () => {
169
+ await channel.start();
170
+
171
+ pairingService.generateCode("google-chat", "123456");
172
+ pairingService.approve(pairingService.listPending()[0]!.code);
173
+
174
+ const event = {
175
+ type: "MESSAGE",
176
+ eventTime: new Date().toISOString(),
177
+ space: {
178
+ name: "spaces/test",
179
+ displayName: "Test",
180
+ type: "DM" as const,
181
+ },
182
+ message: {
183
+ name: "spaces/test/messages/123",
184
+ sender: {
185
+ name: "users/123456",
186
+ displayName: "Test User",
187
+ },
188
+ createTime: new Date().toISOString(),
189
+ text: "Hello bot!",
190
+ },
191
+ };
192
+
193
+ const handleChatMessage = (
194
+ channel as unknown as {
195
+ handleChatMessage: (e: typeof event, res: { writeHead: () => void; end: (s: string) => void }) => Promise<void>;
196
+ }
197
+ ).handleChatMessage;
198
+
199
+ const res = {
200
+ writeHead: vi.fn().mockReturnThis(),
201
+ end: vi.fn(),
202
+ };
203
+
204
+ const messageHandler = vi.fn();
205
+ channel.onMessage(messageHandler);
206
+
207
+ await handleChatMessage.call(channel, event, res);
208
+
209
+ expect(res.writeHead).toHaveBeenCalledWith(200);
210
+ expect(res.end).toHaveBeenCalled();
211
+ expect(messageHandler).toHaveBeenCalledWith(
212
+ expect.objectContaining({
213
+ channel: "google-chat",
214
+ content: "Hello bot!",
215
+ })
216
+ );
217
+ });
218
+ });
219
+ });
@@ -0,0 +1,269 @@
1
+ import http, { type IncomingMessage, type ServerResponse, type Server } from "http";
2
+ import { BaseChannel, type ChannelConfig, type IncomingMessage as HiveIncomingMessage, type OutboundMessage } from "../channels/base.ts";
3
+ import { logger } from "../utils/logger.ts";
4
+ import { pairingService } from "./pairing.ts";
5
+
6
+ export interface GoogleChatConfig extends ChannelConfig {
7
+ projectId?: string;
8
+ serviceAccountKey?: string;
9
+ webhookPort?: number;
10
+ webhookPath?: string;
11
+ }
12
+
13
+ interface GoogleChatEvent {
14
+ type: string;
15
+ eventTime: string;
16
+ space: {
17
+ name: string;
18
+ displayName: string;
19
+ type: "ROOM" | "DM";
20
+ };
21
+ message?: {
22
+ name: string;
23
+ sender: {
24
+ name: string;
25
+ displayName: string;
26
+ };
27
+ createTime: string;
28
+ text: string;
29
+ thread?: {
30
+ name: string;
31
+ };
32
+ };
33
+ user?: {
34
+ name: string;
35
+ displayName: string;
36
+ };
37
+ }
38
+
39
+ export class GoogleChatChannel extends BaseChannel {
40
+ name = "google-chat";
41
+ accountId: string;
42
+ config: GoogleChatConfig;
43
+
44
+ private server?: Server;
45
+ private log = logger.child("google-chat");
46
+ private spaceCache: Map<string, { space: string; thread?: string }> = new Map();
47
+
48
+ constructor(accountId: string, config: GoogleChatConfig) {
49
+ super();
50
+ this.accountId = accountId;
51
+ this.config = {
52
+ ...config,
53
+ dmPolicy: config.dmPolicy ?? "pairing",
54
+ allowFrom: config.allowFrom ?? [],
55
+ enabled: config.enabled ?? true,
56
+ webhookPort: config.webhookPort ?? 8080,
57
+ webhookPath: config.webhookPath ?? "/webhook/google-chat",
58
+ };
59
+ }
60
+
61
+ async start(): Promise<void> {
62
+ this.server = http.createServer((req, res) => {
63
+ if (req.url === this.config.webhookPath && req.method === "POST") {
64
+ this.handleWebhook(req, res);
65
+ } else {
66
+ res.writeHead(404).end();
67
+ }
68
+ });
69
+
70
+ return new Promise((resolve, reject) => {
71
+ this.server!.listen(this.config.webhookPort, () => {
72
+ this.running = true;
73
+ this.log.info(
74
+ `Google Chat webhook listening on port ${this.config.webhookPort}${this.config.webhookPath}`
75
+ );
76
+ resolve();
77
+ });
78
+
79
+ this.server!.on("error", (error: Error) => {
80
+ this.log.error(`Server error: ${error.message}`);
81
+ reject(error);
82
+ });
83
+ });
84
+ }
85
+
86
+ private async handleWebhook(req: IncomingMessage, res: ServerResponse): Promise<void> {
87
+ let body = "";
88
+ req.on("data", (chunk) => (body += chunk));
89
+ req.on("end", async () => {
90
+ try {
91
+ const event: GoogleChatEvent = JSON.parse(body);
92
+
93
+ if (event.type === "ADDED_TO_SPACE") {
94
+ await this.handleAddedToSpace(event, res);
95
+ return;
96
+ }
97
+
98
+ if (event.type === "REMOVED_FROM_SPACE") {
99
+ this.log.info(`Removed from space: ${event.space.name}`);
100
+ res.writeHead(200).end();
101
+ return;
102
+ }
103
+
104
+ if (event.type === "MESSAGE" && event.message) {
105
+ await this.handleChatMessage(event, res);
106
+ return;
107
+ }
108
+
109
+ res.writeHead(200).end();
110
+ } catch (error) {
111
+ this.log.error(`Webhook error: ${(error as Error).message}`);
112
+ res.writeHead(500).end();
113
+ }
114
+ });
115
+ }
116
+
117
+ private async handleAddedToSpace(
118
+ event: GoogleChatEvent,
119
+ res: ServerResponse
120
+ ): Promise<void> {
121
+ const message =
122
+ event.space.type === "DM"
123
+ ? {
124
+ text: "¡Hola! Soy tu asistente AI. Envía un mensaje para comenzar.",
125
+ }
126
+ : {
127
+ text: "¡Gracias por añadirme al espacio! Mencióname con @bot para interactuar.",
128
+ };
129
+
130
+ res.writeHead(200, { "Content-Type": "application/json" });
131
+ res.end(JSON.stringify(message));
132
+ }
133
+
134
+ private async handleChatMessage(event: GoogleChatEvent, res: ServerResponse): Promise<void> {
135
+ if (!event.message) {
136
+ res.writeHead(200).end();
137
+ return;
138
+ }
139
+
140
+ const userId = event.message.sender.name.split("/").pop() ?? "unknown";
141
+ const spaceName = event.space.name;
142
+ const isDM = event.space.type === "DM";
143
+ const kind = isDM ? "direct" : "group";
144
+ const peerId = isDM ? userId : `${spaceName}:${userId}`;
145
+
146
+ if (event.message.text === "/myid") {
147
+ res.writeHead(200, { "Content-Type": "application/json" });
148
+ res.end(
149
+ JSON.stringify({
150
+ text: `🆔 Tu Google Chat ID es: ${userId}\n\nPara emparejar, solicita un código al administrador.`,
151
+ })
152
+ );
153
+ return;
154
+ }
155
+
156
+ if (event.message.text.startsWith("/pair ")) {
157
+ const code = event.message.text.split(" ")[1]?.trim();
158
+ const result = pairingService.approve(code ?? "");
159
+
160
+ res.writeHead(200, { "Content-Type": "application/json" });
161
+ res.end(
162
+ JSON.stringify({
163
+ text: result.success
164
+ ? "✅ ¡Emparejamiento exitoso!"
165
+ : `❌ ${result.error}`,
166
+ })
167
+ );
168
+ return;
169
+ }
170
+
171
+ if (this.config.dmPolicy === "pairing" && !pairingService.isAllowed("google-chat", userId)) {
172
+ this.log.debug(`Message from unpaired user: ${userId}`);
173
+ res.writeHead(200, { "Content-Type": "application/json" });
174
+ res.end(
175
+ JSON.stringify({
176
+ text:
177
+ "⛔ No estás emparejado.\n\n" +
178
+ "Tu ID: " +
179
+ userId +
180
+ "\n\n" +
181
+ "Solicita un código de emparejamiento al administrador.",
182
+ })
183
+ );
184
+ return;
185
+ }
186
+
187
+ if (!isDM && !this.isUserAllowed(peerId)) {
188
+ this.log.debug(`Message from unauthorized user: ${peerId}`);
189
+ res.writeHead(200).end();
190
+ return;
191
+ }
192
+
193
+ const sessionId = this.formatSessionId(peerId, kind);
194
+ this.spaceCache.set(sessionId, {
195
+ space: spaceName,
196
+ thread: event.message.thread?.name,
197
+ });
198
+
199
+ const incomingMessage: HiveIncomingMessage = {
200
+ sessionId,
201
+ channel: "google-chat",
202
+ accountId: this.accountId,
203
+ peerId,
204
+ peerKind: kind,
205
+ content: event.message.text,
206
+ metadata: {
207
+ googleChat: {
208
+ spaceName,
209
+ userId,
210
+ displayName: event.message.sender.displayName,
211
+ threadName: event.message.thread?.name,
212
+ },
213
+ },
214
+ };
215
+
216
+ res.writeHead(200).end();
217
+
218
+ await this.handleMessage(incomingMessage);
219
+ }
220
+
221
+ async stop(): Promise<void> {
222
+ if (this.server) {
223
+ return new Promise((resolve) => {
224
+ this.server!.close(() => {
225
+ this.running = false;
226
+ this.log.info("Google Chat channel stopped");
227
+ resolve();
228
+ });
229
+ });
230
+ }
231
+ }
232
+
233
+ async send(sessionId: string, message: OutboundMessage): Promise<void> {
234
+ const content = message.content ?? "";
235
+
236
+ if (!content || content.trim().length === 0) {
237
+ this.log.warn("Empty response, skipping send");
238
+ return;
239
+ }
240
+
241
+ const cached = this.spaceCache.get(sessionId);
242
+
243
+ if (!cached) {
244
+ this.log.warn(`No cached space for session: ${sessionId}`);
245
+ return;
246
+ }
247
+
248
+ if (message.type === "stream" && message.chunk) {
249
+ console.log(`[Google Chat] Stream chunk to ${cached.space}: ${message.chunk.slice(0, 50)}...`);
250
+ return;
251
+ }
252
+
253
+ console.log(`[Google Chat] Would send to ${cached.space}: ${content.slice(0, 100)}...`);
254
+ }
255
+
256
+ async sendMessage(space: string, content: string, thread?: string): Promise<void> {
257
+ console.log(`[Google Chat] Sending to ${space}: ${content.slice(0, 100)}...`);
258
+ if (thread) {
259
+ console.log(` Thread: ${thread}`);
260
+ }
261
+ }
262
+ }
263
+
264
+ export function createGoogleChatChannel(
265
+ accountId: string,
266
+ config: GoogleChatConfig
267
+ ): GoogleChatChannel {
268
+ return new GoogleChatChannel(accountId, config);
269
+ }
@@ -1,6 +1,11 @@
1
1
  import type { Config } from "../config/loader.ts";
2
2
  import { logger } from "../utils/logger.ts";
3
3
 
4
+ export * from "./pairing.ts";
5
+ export * from "./rate-limit.ts";
6
+ export * from "./signal.ts";
7
+ export * from "./google-chat.ts";
8
+
4
9
  export interface RateLimitConfig {
5
10
  windowMs: number;
6
11
  maxRequests: number;