@johpaz/hive-core 1.0.8 → 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.
- package/package.json +10 -9
- package/src/agent/ethics.ts +70 -68
- package/src/agent/index.ts +48 -17
- package/src/agent/providers/index.ts +11 -5
- package/src/agent/soul.ts +19 -15
- package/src/agent/user.ts +19 -15
- package/src/agent/workspace.ts +6 -6
- package/src/agents/index.ts +4 -0
- package/src/agents/inter-agent-bus.test.ts +264 -0
- package/src/agents/inter-agent-bus.ts +279 -0
- package/src/agents/registry.test.ts +275 -0
- package/src/agents/registry.ts +273 -0
- package/src/agents/router.test.ts +229 -0
- package/src/agents/router.ts +251 -0
- package/src/agents/team-coordinator.test.ts +401 -0
- package/src/agents/team-coordinator.ts +480 -0
- package/src/canvas/canvas-manager.test.ts +159 -0
- package/src/canvas/canvas-manager.ts +219 -0
- package/src/canvas/canvas-tools.ts +189 -0
- package/src/canvas/index.ts +2 -0
- package/src/channels/whatsapp.ts +12 -12
- package/src/config/loader.ts +12 -9
- package/src/events/event-bus.test.ts +98 -0
- package/src/events/event-bus.ts +171 -0
- package/src/gateway/server.ts +131 -35
- package/src/index.ts +9 -1
- package/src/multi-agent/manager.ts +12 -12
- package/src/plugins/api.ts +129 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/loader.test.ts +285 -0
- package/src/plugins/loader.ts +363 -0
- package/src/resilience/circuit-breaker.test.ts +129 -0
- package/src/resilience/circuit-breaker.ts +223 -0
- package/src/security/google-chat.test.ts +219 -0
- package/src/security/google-chat.ts +269 -0
- package/src/security/index.ts +5 -0
- package/src/security/pairing.test.ts +302 -0
- package/src/security/pairing.ts +250 -0
- package/src/security/rate-limit.test.ts +239 -0
- package/src/security/rate-limit.ts +270 -0
- package/src/security/signal.test.ts +92 -0
- package/src/security/signal.ts +321 -0
- package/src/state/store.test.ts +190 -0
- package/src/state/store.ts +310 -0
- package/src/storage/sqlite.ts +3 -3
- package/src/tools/cron.ts +42 -2
- package/src/tools/dynamic-registry.test.ts +226 -0
- package/src/tools/dynamic-registry.ts +258 -0
- package/src/tools/fs.test.ts +127 -0
- package/src/tools/fs.ts +364 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/read.ts +23 -19
- package/src/utils/logger.ts +112 -33
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { logger } from "../utils/logger.ts";
|
|
3
|
+
import { eventBus } from "../events/event-bus.ts";
|
|
4
|
+
|
|
5
|
+
export interface WebSocketLike {
|
|
6
|
+
readyState: number;
|
|
7
|
+
send(data: string): void;
|
|
8
|
+
on(event: "message", callback: (data: unknown) => void): void;
|
|
9
|
+
on(event: "close", callback: () => void): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const WebSocketState = {
|
|
13
|
+
CONNECTING: 0,
|
|
14
|
+
OPEN: 1,
|
|
15
|
+
CLOSING: 2,
|
|
16
|
+
CLOSED: 3,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface CanvasComponent {
|
|
20
|
+
id: string;
|
|
21
|
+
type: "button" | "form" | "chart" | "table" | "markdown" | "text" | "image";
|
|
22
|
+
props: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CanvasMessage {
|
|
26
|
+
type: "canvas:render" | "canvas:update" | "canvas:clear" | "canvas:interact";
|
|
27
|
+
payload: {
|
|
28
|
+
sessionId: string;
|
|
29
|
+
componentId?: string;
|
|
30
|
+
component?: CanvasComponent;
|
|
31
|
+
action?: string;
|
|
32
|
+
data?: unknown;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface InteractionEvent {
|
|
37
|
+
sessionId: string;
|
|
38
|
+
componentId: string;
|
|
39
|
+
action: string;
|
|
40
|
+
data: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PendingInteraction {
|
|
44
|
+
resolve: (data: unknown) => void;
|
|
45
|
+
reject: (error: Error) => void;
|
|
46
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class CanvasManager extends EventEmitter {
|
|
50
|
+
private sessions: Map<string, WebSocketLike> = new Map();
|
|
51
|
+
private pendingInteractions: Map<string, PendingInteraction> = new Map();
|
|
52
|
+
private log = logger.child("canvas");
|
|
53
|
+
|
|
54
|
+
registerSession(sessionId: string, ws: WebSocketLike): void {
|
|
55
|
+
this.sessions.set(sessionId, ws);
|
|
56
|
+
this.log.info(`Canvas session registered: ${sessionId}`);
|
|
57
|
+
|
|
58
|
+
ws.on("message", (data: unknown) => {
|
|
59
|
+
try {
|
|
60
|
+
const msg = JSON.parse(data as string) as CanvasMessage;
|
|
61
|
+
this.handleMessage(sessionId, msg);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.log.error(`Invalid canvas message: ${(error as Error).message}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
ws.on("close", () => {
|
|
68
|
+
this.sessions.delete(sessionId);
|
|
69
|
+
this.cleanupPendingInteractions(sessionId);
|
|
70
|
+
this.log.info(`Canvas session disconnected: ${sessionId}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
eventBus.emit("tool:completed" as any, {
|
|
74
|
+
toolName: "canvas:session:register",
|
|
75
|
+
result: { sessionId },
|
|
76
|
+
duration: 0,
|
|
77
|
+
success: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private handleMessage(sessionId: string, msg: CanvasMessage): void {
|
|
82
|
+
if (msg.type === "canvas:interact") {
|
|
83
|
+
const { componentId, action, data } = msg.payload;
|
|
84
|
+
|
|
85
|
+
if (componentId) {
|
|
86
|
+
this.resolveInteraction(sessionId, componentId, data);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.emit("interaction", {
|
|
90
|
+
sessionId,
|
|
91
|
+
componentId,
|
|
92
|
+
action,
|
|
93
|
+
data,
|
|
94
|
+
} as InteractionEvent);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private resolveInteraction(sessionId: string, componentId: string, data: unknown): void {
|
|
99
|
+
const key = `${sessionId}:${componentId}`;
|
|
100
|
+
const pending = this.pendingInteractions.get(key);
|
|
101
|
+
|
|
102
|
+
if (pending) {
|
|
103
|
+
clearTimeout(pending.timeoutId);
|
|
104
|
+
this.pendingInteractions.delete(key);
|
|
105
|
+
pending.resolve(data);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async render(sessionId: string, component: CanvasComponent): Promise<void> {
|
|
110
|
+
const ws = this.sessions.get(sessionId);
|
|
111
|
+
|
|
112
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
113
|
+
throw new Error(`Session not connected: ${sessionId}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const message: CanvasMessage = {
|
|
117
|
+
type: "canvas:render",
|
|
118
|
+
payload: { sessionId, component },
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
ws.send(JSON.stringify(message));
|
|
122
|
+
this.log.debug(`Rendered component ${component.id} to session ${sessionId}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async update(sessionId: string, component: CanvasComponent): Promise<void> {
|
|
126
|
+
const ws = this.sessions.get(sessionId);
|
|
127
|
+
|
|
128
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
129
|
+
throw new Error(`Session not connected: ${sessionId}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const message: CanvasMessage = {
|
|
133
|
+
type: "canvas:update",
|
|
134
|
+
payload: { sessionId, component },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
ws.send(JSON.stringify(message));
|
|
138
|
+
this.log.debug(`Updated component ${component.id} in session ${sessionId}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async clear(sessionId: string): Promise<void> {
|
|
142
|
+
const ws = this.sessions.get(sessionId);
|
|
143
|
+
|
|
144
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
145
|
+
throw new Error(`Session not connected: ${sessionId}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const message: CanvasMessage = {
|
|
149
|
+
type: "canvas:clear",
|
|
150
|
+
payload: { sessionId },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
ws.send(JSON.stringify(message));
|
|
154
|
+
this.log.debug(`Cleared canvas for session ${sessionId}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async waitForInteraction(
|
|
158
|
+
sessionId: string,
|
|
159
|
+
componentId: string,
|
|
160
|
+
timeout = 300000
|
|
161
|
+
): Promise<unknown> {
|
|
162
|
+
const key = `${sessionId}:${componentId}`;
|
|
163
|
+
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const timeoutId = setTimeout(() => {
|
|
166
|
+
this.pendingInteractions.delete(key);
|
|
167
|
+
reject(new Error(`Interaction timeout for ${componentId}`));
|
|
168
|
+
}, timeout);
|
|
169
|
+
|
|
170
|
+
this.pendingInteractions.set(key, { resolve, reject, timeoutId });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
handleInteraction(sessionId: string, componentId: string, data: unknown): void {
|
|
175
|
+
this.resolveInteraction(sessionId, componentId, data);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
isSessionConnected(sessionId: string): boolean {
|
|
179
|
+
const ws = this.sessions.get(sessionId);
|
|
180
|
+
return ws !== undefined && ws.readyState === WebSocketState.OPEN;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
getConnectedSessions(): string[] {
|
|
184
|
+
return Array.from(this.sessions.entries())
|
|
185
|
+
.filter(([_, ws]) => ws.readyState === WebSocketState.OPEN)
|
|
186
|
+
.map(([id]) => id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
getStats(): { totalSessions: number; activeSessions: number; pendingInteractions: number } {
|
|
190
|
+
return {
|
|
191
|
+
totalSessions: this.sessions.size,
|
|
192
|
+
activeSessions: this.getConnectedSessions().length,
|
|
193
|
+
pendingInteractions: this.pendingInteractions.size,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private cleanupPendingInteractions(sessionId: string): void {
|
|
198
|
+
for (const [key, pending] of this.pendingInteractions) {
|
|
199
|
+
if (key.startsWith(`${sessionId}:`)) {
|
|
200
|
+
clearTimeout(pending.timeoutId);
|
|
201
|
+
pending.reject(new Error(`Session disconnected: ${sessionId}`));
|
|
202
|
+
this.pendingInteractions.delete(key);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
clearAll(): void {
|
|
208
|
+
for (const pending of this.pendingInteractions.values()) {
|
|
209
|
+
clearTimeout(pending.timeoutId);
|
|
210
|
+
pending.reject(new Error("Canvas manager cleared"));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.pendingInteractions.clear();
|
|
214
|
+
this.sessions.clear();
|
|
215
|
+
this.log.info("Canvas manager cleared");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const canvasManager = new CanvasManager();
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { Tool } from "../tools/registry.ts";
|
|
2
|
+
import type { Config } from "../config/loader.ts";
|
|
3
|
+
import { canvasManager } from "./canvas-manager.ts";
|
|
4
|
+
import { logger } from "../utils/logger.ts";
|
|
5
|
+
|
|
6
|
+
export function createCanvasRenderTool(_config: Config): Tool {
|
|
7
|
+
const log = logger.child("canvas-render");
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
name: "canvas_render",
|
|
11
|
+
description: "Render a component on the user's canvas",
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
sessionId: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Session ID to render to",
|
|
18
|
+
},
|
|
19
|
+
component: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
id: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Unique component ID",
|
|
25
|
+
},
|
|
26
|
+
type: {
|
|
27
|
+
type: "string",
|
|
28
|
+
enum: ["button", "form", "chart", "table", "markdown", "text", "image"],
|
|
29
|
+
description: "Component type",
|
|
30
|
+
},
|
|
31
|
+
props: {
|
|
32
|
+
type: "object",
|
|
33
|
+
description: "Component properties",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: ["id", "type", "props"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ["sessionId", "component"],
|
|
40
|
+
},
|
|
41
|
+
execute: async (params: Record<string, unknown>) => {
|
|
42
|
+
const sessionId = params.sessionId as string;
|
|
43
|
+
const component = params.component as {
|
|
44
|
+
id: string;
|
|
45
|
+
type: "button" | "form" | "chart" | "table" | "markdown" | "text" | "image";
|
|
46
|
+
props: Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
log.debug(`Rendering component ${component.id} to session ${sessionId}`);
|
|
50
|
+
|
|
51
|
+
await canvasManager.render(sessionId, {
|
|
52
|
+
id: component.id,
|
|
53
|
+
type: component.type,
|
|
54
|
+
props: component.props,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
componentId: component.id,
|
|
60
|
+
sessionId,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createCanvasAskTool(_config: Config): Tool {
|
|
67
|
+
const log = logger.child("canvas-ask");
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
name: "canvas_ask",
|
|
71
|
+
description: "Display a form and wait for user response",
|
|
72
|
+
parameters: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
sessionId: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Session ID",
|
|
78
|
+
},
|
|
79
|
+
title: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Form title",
|
|
82
|
+
},
|
|
83
|
+
fields: {
|
|
84
|
+
type: "array",
|
|
85
|
+
items: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
name: { type: "string" },
|
|
89
|
+
label: { type: "string" },
|
|
90
|
+
type: { type: "string", enum: ["text", "email", "textarea", "select"] },
|
|
91
|
+
required: { type: "boolean" },
|
|
92
|
+
options: {
|
|
93
|
+
type: "array",
|
|
94
|
+
items: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
label: { type: "string" },
|
|
98
|
+
value: { type: "string" },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ["name", "label", "type"],
|
|
104
|
+
},
|
|
105
|
+
description: "Form fields",
|
|
106
|
+
},
|
|
107
|
+
timeout: {
|
|
108
|
+
type: "number",
|
|
109
|
+
description: "Timeout in milliseconds (default: 300000)",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ["sessionId", "fields"],
|
|
113
|
+
},
|
|
114
|
+
execute: async (params: Record<string, unknown>) => {
|
|
115
|
+
const sessionId = params.sessionId as string;
|
|
116
|
+
const title = (params.title as string) ?? "Form";
|
|
117
|
+
const fields = params.fields as Array<{
|
|
118
|
+
name: string;
|
|
119
|
+
label: string;
|
|
120
|
+
type: string;
|
|
121
|
+
required?: boolean;
|
|
122
|
+
options?: Array<{ label: string; value: string }>;
|
|
123
|
+
}>;
|
|
124
|
+
const timeout = (params.timeout as number) ?? 300000;
|
|
125
|
+
|
|
126
|
+
const formId = `form-${Date.now()}`;
|
|
127
|
+
|
|
128
|
+
log.debug(`Asking user via form ${formId}`);
|
|
129
|
+
|
|
130
|
+
await canvasManager.render(sessionId, {
|
|
131
|
+
id: formId,
|
|
132
|
+
type: "form",
|
|
133
|
+
props: { title, fields },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await canvasManager.waitForInteraction(sessionId, formId, timeout);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
formId,
|
|
142
|
+
data: response,
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
formId,
|
|
148
|
+
error: (error as Error).message,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createCanvasClearTool(_config: Config): Tool {
|
|
156
|
+
const log = logger.child("canvas-clear");
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
name: "canvas_clear",
|
|
160
|
+
description: "Clear the canvas for a session",
|
|
161
|
+
parameters: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
sessionId: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "Session ID to clear",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
required: ["sessionId"],
|
|
170
|
+
},
|
|
171
|
+
execute: async (params: Record<string, unknown>) => {
|
|
172
|
+
const sessionId = params.sessionId as string;
|
|
173
|
+
|
|
174
|
+
log.debug(`Clearing canvas for session ${sessionId}`);
|
|
175
|
+
|
|
176
|
+
await canvasManager.clear(sessionId);
|
|
177
|
+
|
|
178
|
+
return { success: true, sessionId };
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function createCanvasTools(config: Config): Tool[] {
|
|
184
|
+
return [
|
|
185
|
+
createCanvasRenderTool(config),
|
|
186
|
+
createCanvasAskTool(config),
|
|
187
|
+
createCanvasClearTool(config),
|
|
188
|
+
];
|
|
189
|
+
}
|
package/src/channels/whatsapp.ts
CHANGED
|
@@ -6,7 +6,7 @@ import makeWASocket, {
|
|
|
6
6
|
} from "@whiskeysockets/baileys";
|
|
7
7
|
import type { ChannelConfig, IncomingMessage, OutboundMessage } from "./base.ts";
|
|
8
8
|
import { BaseChannel } from "./base.ts";
|
|
9
|
-
import
|
|
9
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { logger } from "../utils/logger.ts";
|
|
12
12
|
|
|
@@ -29,7 +29,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
29
29
|
name = "whatsapp";
|
|
30
30
|
accountId: string;
|
|
31
31
|
config: WhatsAppConfig;
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
private socket: WASocket | null = null;
|
|
34
34
|
private connectionState: WhatsAppConnectionState = {
|
|
35
35
|
status: "disconnected",
|
|
@@ -50,11 +50,11 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
50
50
|
private getAuthPath(agentId: string, accountId: string): string {
|
|
51
51
|
const baseDir = process.env.HOME ?? "";
|
|
52
52
|
const authDir = path.join(baseDir, ".hive", "agents", agentId, "whatsapp", accountId);
|
|
53
|
-
|
|
54
|
-
if (!
|
|
55
|
-
|
|
53
|
+
|
|
54
|
+
if (!existsSync(authDir)) {
|
|
55
|
+
mkdirSync(authDir, { recursive: true });
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
return authDir;
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -65,7 +65,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
65
65
|
|
|
66
66
|
async stop(): Promise<void> {
|
|
67
67
|
this.running = false;
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
if (this.reconnectTimeout) {
|
|
70
70
|
clearTimeout(this.reconnectTimeout);
|
|
71
71
|
this.reconnectTimeout = null;
|
|
@@ -144,8 +144,8 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
144
144
|
|
|
145
145
|
if (statusCode === DisconnectReason.loggedOut) {
|
|
146
146
|
this.log.error("WhatsApp logged out - session invalidated. Need to re-scan QR.");
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
rmSync(this.authPath, { recursive: true, force: true });
|
|
148
|
+
mkdirSync(this.authPath, { recursive: true });
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
if (shouldReconnect && this.running) {
|
|
@@ -167,7 +167,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
167
167
|
console.log("\n" + "=".repeat(50));
|
|
168
168
|
console.log(" WHATSAPP QR CODE - Scan with your phone");
|
|
169
169
|
console.log("=".repeat(50) + "\n");
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
const qrcode = require("qrcode-terminal");
|
|
172
172
|
qrcode.generate(qr, { small: false }, (qrString: string) => {
|
|
173
173
|
console.log(qrString);
|
|
@@ -188,7 +188,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
188
188
|
messageTimestamp?: number;
|
|
189
189
|
pushName?: string;
|
|
190
190
|
};
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
if (typedMsg.key.fromMe) continue;
|
|
193
193
|
|
|
194
194
|
const from = typedMsg.key.remoteJid;
|
|
@@ -315,7 +315,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
315
315
|
const jid = this.getJid(sessionId);
|
|
316
316
|
try {
|
|
317
317
|
await this.socket.readMessages([
|
|
318
|
-
{
|
|
318
|
+
{ remoteJid: jid, id: messageId, fromMe: false }
|
|
319
319
|
]);
|
|
320
320
|
} catch {
|
|
321
321
|
// Ignore read receipt errors
|
package/src/config/loader.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as yaml from "js-yaml";
|
|
5
5
|
|
|
@@ -474,7 +474,7 @@ function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial
|
|
|
474
474
|
return result;
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
export function loadConfig(configPath?: string): Config {
|
|
477
|
+
export async function loadConfig(configPath?: string): Promise<Config> {
|
|
478
478
|
const paths = configPath
|
|
479
479
|
? [configPath]
|
|
480
480
|
: [
|
|
@@ -487,9 +487,10 @@ export function loadConfig(configPath?: string): Config {
|
|
|
487
487
|
let loadedConfig: Partial<Config> = {};
|
|
488
488
|
|
|
489
489
|
for (const p of paths) {
|
|
490
|
-
|
|
490
|
+
const file = Bun.file(p);
|
|
491
|
+
if (await file.exists()) {
|
|
491
492
|
try {
|
|
492
|
-
const content =
|
|
493
|
+
const content = await file.text();
|
|
493
494
|
const parsed = yaml.load(content);
|
|
494
495
|
loadedConfig = expandEnvInObject(parsed as Partial<Config>);
|
|
495
496
|
break;
|
|
@@ -509,7 +510,7 @@ export function loadConfig(configPath?: string): Config {
|
|
|
509
510
|
return result.data;
|
|
510
511
|
}
|
|
511
512
|
|
|
512
|
-
export function saveConfig(config: Config, configPath?: string): void {
|
|
513
|
+
export async function saveConfig(config: Config, configPath?: string): Promise<void> {
|
|
513
514
|
const paths = configPath
|
|
514
515
|
? [configPath]
|
|
515
516
|
: [
|
|
@@ -519,7 +520,8 @@ export function saveConfig(config: Config, configPath?: string): void {
|
|
|
519
520
|
|
|
520
521
|
let targetPath = paths[0];
|
|
521
522
|
for (const p of paths) {
|
|
522
|
-
|
|
523
|
+
const file = Bun.file(p);
|
|
524
|
+
if (await file.exists()) {
|
|
523
525
|
targetPath = p;
|
|
524
526
|
break;
|
|
525
527
|
}
|
|
@@ -527,15 +529,16 @@ export function saveConfig(config: Config, configPath?: string): void {
|
|
|
527
529
|
|
|
528
530
|
if (targetPath) {
|
|
529
531
|
const dir = path.dirname(targetPath);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
+
const dirFile = Bun.file(dir);
|
|
533
|
+
if (!(await dirFile.exists())) {
|
|
534
|
+
mkdirSync(dir, { recursive: true });
|
|
532
535
|
}
|
|
533
536
|
const yamlStr = yaml.dump(config, {
|
|
534
537
|
indent: 2,
|
|
535
538
|
lineWidth: -1,
|
|
536
539
|
noRefs: true,
|
|
537
540
|
});
|
|
538
|
-
|
|
541
|
+
await Bun.write(targetPath, yamlStr);
|
|
539
542
|
}
|
|
540
543
|
}
|
|
541
544
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { eventBus } from "../events/event-bus";
|
|
3
|
+
|
|
4
|
+
describe("TypedEventBus", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
eventBus.removeAllListeners();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("should emit and receive typed events", async () => {
|
|
10
|
+
let receivedData: any = null;
|
|
11
|
+
|
|
12
|
+
eventBus.on("message:received", (data) => {
|
|
13
|
+
receivedData = data;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
eventBus.emit("message:received", {
|
|
17
|
+
channel: "telegram",
|
|
18
|
+
userId: "123",
|
|
19
|
+
content: "hello",
|
|
20
|
+
timestamp: Date.now(),
|
|
21
|
+
sessionId: "session-1",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(receivedData).not.toBeNull();
|
|
25
|
+
expect(receivedData.channel).toBe("telegram");
|
|
26
|
+
expect(receivedData.content).toBe("hello");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should handle multiple listeners", () => {
|
|
30
|
+
const calls: string[] = [];
|
|
31
|
+
|
|
32
|
+
eventBus.on("agent:thinking", () => { calls.push("first"); });
|
|
33
|
+
eventBus.on("agent:thinking", () => { calls.push("second"); });
|
|
34
|
+
|
|
35
|
+
eventBus.emit("agent:thinking", {
|
|
36
|
+
agentId: "agent-1",
|
|
37
|
+
sessionId: "session-1",
|
|
38
|
+
stage: "planning",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(calls).toEqual(["first", "second"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return unsubscribe function", () => {
|
|
45
|
+
const calls: string[] = [];
|
|
46
|
+
|
|
47
|
+
const unsubscribe = eventBus.on("tool:executing", () => { calls.push("called"); });
|
|
48
|
+
|
|
49
|
+
eventBus.emit("tool:executing", {
|
|
50
|
+
toolName: "read",
|
|
51
|
+
args: { path: "/test" },
|
|
52
|
+
sessionId: "session-1",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(calls.length).toBe(1);
|
|
56
|
+
|
|
57
|
+
unsubscribe();
|
|
58
|
+
|
|
59
|
+
eventBus.emit("tool:executing", {
|
|
60
|
+
toolName: "write",
|
|
61
|
+
args: { path: "/test2" },
|
|
62
|
+
sessionId: "session-1",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(calls.length).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should handle once listener", () => {
|
|
69
|
+
const calls: number[] = [];
|
|
70
|
+
|
|
71
|
+
eventBus.once("session:started", () => { calls.push(1); });
|
|
72
|
+
|
|
73
|
+
eventBus.emit("session:started", {
|
|
74
|
+
sessionId: "s1",
|
|
75
|
+
agentId: "agent-1",
|
|
76
|
+
channel: "telegram",
|
|
77
|
+
userId: "user-1",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
eventBus.emit("session:started", {
|
|
81
|
+
sessionId: "s2",
|
|
82
|
+
agentId: "agent-1",
|
|
83
|
+
channel: "telegram",
|
|
84
|
+
userId: "user-1",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(calls.length).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should track listener count", () => {
|
|
91
|
+
eventBus.removeAllListeners("mcp:connected");
|
|
92
|
+
|
|
93
|
+
eventBus.on("mcp:connected", () => {});
|
|
94
|
+
eventBus.on("mcp:connected", () => {});
|
|
95
|
+
|
|
96
|
+
expect(eventBus.listenerCount("mcp:connected")).toBe(2);
|
|
97
|
+
});
|
|
98
|
+
});
|